Compare commits

...

126 Commits

Author SHA1 Message Date
oSumAtrIX
a9ae45fe63 build: Bump version to v1.11.2 2023-10-07 02:01:10 +02:00
oSumAtrIX
61bb39b46f build: Bump dependencies to improve merging integrations speed 2023-10-07 02:01:10 +02:00
Benjamin Halko
2ad106f7d7 fix(export-settings): remove boolean workaround 2023-10-07 02:01:09 +02:00
Benjamin
8fd4fe0e55 feat(patcher): improve logs (#1299)
Co-authored-by: Ushie <ushiekane@gmail.com>
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-10-07 02:01:09 +02:00
oSumAtrIX
b1c9aedac3 build: Bump version to v1.11.1 2023-10-05 17:45:56 +02:00
oSumAtrIX
a80415be02 build: Bump dependencies 2023-10-05 17:45:56 +02:00
oSumAtrIX
d9acd0d74b chore: Merge branch dev to main (#1329) 2023-10-05 01:35:08 +02:00
oSumAtrIX
7ae09159ba build: Bump version to v1.11.0 2023-10-05 01:23:48 +02:00
oSumAtrIX
a709abd80c perf: Do not load patches twice (#1328) 2023-10-04 21:58:25 +02:00
Benjamin Halko
cd07f39b69 fix: reset patches after patching 2023-10-04 12:16:56 -07:00
Benjamin Halko
f7c11d07a8 fix(export-settings): export patches as json object 2023-10-04 11:33:13 -07:00
oSumAtrIX
b07439d402 fix: Reload patches 2023-10-04 19:38:34 +02:00
oSumAtrIX
d8eadc2a2d feat: Use simpler wording 2023-10-04 19:01:17 +02:00
oSumAtrIX
3a88d4d3e6 build: Bump dependencies 2023-10-04 18:39:24 +02:00
oSumAtrIX
012110f008 perf: Do not load patches twice 2023-10-04 18:39:24 +02:00
oSumAtrIX
4de274bf62 feat: Export settings migration activity (#1308) 2023-10-01 19:12:31 +02:00
oSumAtrIX
76b89baee3 build: Bump dependencies (#1311) 2023-10-01 19:03:26 +02:00
oSumAtrIX
697ae92031 Apply suggestions from code review [skip ci] 2023-10-01 19:02:49 +02:00
oSumAtrIX
c87f92b346 feat: Adjust install dialog labels 2023-10-01 06:21:03 +02:00
oSumAtrIX
6961bb7fd0 fix: Do not delete cached downloads 2023-10-01 04:48:24 +02:00
oSumAtrIX
6e26130744 chore: Add todo 2023-09-30 22:15:46 +02:00
oSumAtrIX
123a375a27 refactor: Remove unused strings 2023-09-30 22:14:51 +02:00
oSumAtrIX
2b4b3ca0a5 fix: Retrieve app information from patched app 2023-09-30 21:40:03 +02:00
oSumAtrIX
c4a795418f fix: Move installation log to correct place 2023-09-30 21:13:32 +02:00
oSumAtrIX
91837ebade feat: Remove original package name in app info view 2023-09-30 21:07:26 +02:00
oSumAtrIX
0492e910ea fix: Fill the preferred action 2023-09-30 20:11:53 +02:00
oSumAtrIX
36c86e22b1 fix: Load installed apps 2023-09-30 20:03:09 +02:00
oSumAtrIX
6bdc0c7bb2 feat: Simplify label 2023-09-30 19:58:45 +02:00
aAbed
1e8d8f749a fix: do not ask for patches consent before initializing model 2023-09-30 14:57:48 +05:45
oSumAtrIX
2e8e3b0d1e fix: Do not hardcode any predefined packages 2023-09-29 20:12:39 +02:00
oSumAtrIX
15b8613d3c feat: Only log relevant records 2023-09-29 19:40:22 +02:00
oSumAtrIX
8ce266bc94 perf: Reduce amount of network requests 2023-09-29 18:39:07 +02:00
oSumAtrIX
8661d72e45 feat: Simplify label 2023-09-29 17:00:34 +02:00
oSumAtrIX
62505f2543 build: Bump dependencies 2023-09-28 17:36:10 +02:00
Pun Butrach
37986c58ec docs(building): correct path to gradle.properties 2023-09-28 21:45:41 +07:00
Benjamin Halko
2968d96fe9 remove log import 2023-09-27 14:42:11 -07:00
Benjamin Halko
e7c8d0e78c use same fingerprint 2023-09-27 14:36:39 -07:00
Benjamin Halko
83cbb34a5b use revanced fingerprint 2023-09-27 14:27:38 -07:00
Benjamin Halko
7559c7b67e verify fingerprint of calling app 2023-09-27 14:17:29 -07:00
Benjamin Halko
02822f4b38 remove user interaction 2023-09-27 13:47:59 -07:00
Benjamin Halko
96736afb94 make bars transparent 2023-09-27 12:32:19 -07:00
Benjamin Halko
72ae132fcd make export settings activity transparent 2023-09-27 12:21:27 -07:00
Benjamin Halko
2250e1bcab convert Booleans to Ints 2023-09-26 16:46:49 -07:00
Benjamin Halko
d9d5b746c3 Added ExportSettingsActivity 2023-09-26 16:21:46 -07:00
Benjamin Halko
f1ea306291 change booleans to numbers 2023-09-25 14:42:40 -07:00
Benjamin Halko
378d62395a remove newlines from base64 output 2023-09-25 10:15:56 -07:00
Benjamin Halko
99c92069b9 export saved patches and keystore 2023-09-25 08:43:10 -07:00
Benjamin Halko
2a89ef797f feat: share settings 2023-09-24 16:56:25 -07:00
Ushie
5838550188 build: bump version to v1.10.3 2023-09-23 13:10:46 +03:00
Ushie
e0e01ae3ee chore: merge dev to main (#1300) 2023-09-23 13:10:08 +03:00
Benjamin
0983ba8a0f fix: search bar overflow (#1301) 2023-09-23 00:51:25 +03:00
Ushie
0bfa776ce7 fix: npe when loading patch bundle on android 8 2023-09-23 00:24:17 +03:00
Pun Butrach
d2b09936d1 chore: merge dev to main (#1295) 2023-09-22 22:08:13 +07:00
Pun Butrach
68e9f0f7c1 build: bump version 1.10.2
!!
2023-09-22 22:05:31 +07:00
Benjamin
c3d345de80 fix: force disable material you on Android 11 and below (#1293) 2023-09-22 21:05:13 +07:00
Pun Butrach
385c0e246a build: use correct version code
The user won't notice it :shhh: we're fine, continue as normal.
2023-09-21 19:32:10 +07:00
Benjamin Halko
5ead49a5b7 build: bump version to v1.10.1 2023-09-20 20:06:19 -07:00
Benjamin
c0760b1347 chore: merge dev to main (#1289) 2023-09-20 20:04:10 -07:00
Benjamin Halko
e01b323aee fix: make entire theme item clickable 2023-09-20 20:03:47 -07:00
Benjamin Halko
6f4866ef63 fix: default theme not following system (#1288) 2023-09-20 20:03:15 -07:00
Benjamin Halko
1b6d72661c build: bump version to v1.10.0 2023-09-20 17:40:23 -07:00
Benjamin
c59d4aea81 chore: merge dev to main (#1239) 2023-09-20 17:35:01 -07:00
Benjamin
6260a80738 feat(settings - appearance): add system option (#1279)
Closes #1260
2023-09-21 03:25:23 +03:00
Benjamin
e75d3c8273 fix(install-type): update padding and enable radio list scrolling (#1287)
fix(install-type): update padding and enable radio list scrolling
2023-09-20 17:24:37 -07:00
Benjamin Halko
b7acb475e9 fix: update install type dialog padding 2023-09-20 16:57:13 -07:00
Benjamin
42b6bbff7c fix: update youtube link (#1286) 2023-09-21 02:16:55 +03:00
aAbed
4b8542b35b fix: load patches via PatchBundle (#1242) 2023-09-21 01:35:32 +03:00
Julienraptor01
9ad1d6cbfb fix(custom-sources): ignore casing when checking if default repo is being used (#1281) 2023-09-21 00:42:29 +03:00
KobeW50
4cdd9acd73 docs(readme): add documentation and minor fixes (#1264)
Co-authored-by: Pun Butrach <pun.butrach@gmail.com>
2023-09-16 17:38:06 +07:00
Benjamin
f4b0a695d6 fix: improve app list loading speed (#1166) 2023-09-15 22:58:15 +07:00
Benjamin
b525ea1ba4 ci: remove analyze workflow (#1262)
Code style is not enforced so analysis is not needed.
2023-09-12 23:40:00 +07:00
Pun Butrach
c1fc2c4766 ci: bump actions/checkout to v4 2023-09-10 14:43:58 +07:00
Ushie
5c733932c7 ci(pr-build): revert "sign apk with keystore (#1231)"
This reverts commit 8bf08ff464, as it fails for PRs originating from forks
2023-09-08 01:14:13 +02:00
Ushie
d1218616ec docs(readme): minor improvements 2023-09-08 01:14:13 +02:00
aAbed
2bf6a03d56 fix: back button closing the app from any page 2023-09-08 01:14:13 +02:00
Pun Butrach
b6ee63c1ea ci(pr-build): sign apk with keystore (#1231) 2023-09-08 01:14:13 +02:00
oSumAtrIX
6d08efdcd7 chore: fix issue template 2023-09-08 01:13:42 +02:00
Ushie
a0a43a5651 build: bump version to v1.9.5 2023-09-08 01:13:42 +02:00
Ushie
3af2f5b032 chore: merge dev to main (#1177) 2023-09-04 04:15:12 +03:00
Ushie
8f54b226b4 refactor(patches-selector): improve universal patches header 2023-09-03 21:14:09 +03:00
Ushie
9f64011b26 fix: npe when patching on android 8 2023-09-03 20:54:42 +03:00
Pun Butrach
c5fc54e721 fix(installer): open the patched app after install (#1233) 2023-09-03 21:48:28 +07:00
Jay Gajjar
fc8a4fc5b6 fix: Don't use 'BuildContext's across async gaps. (#1148) 2023-09-03 01:47:20 +03:00
Benjamin
6f9ab232ae chore: ignore the root .gradle folder (#1160) 2023-09-02 09:29:49 -07:00
aAbed
8cb96f1e45 fix: permissions handling at first launch 2023-08-31 19:34:12 +05:45
Pun Butrach
5733acb77a build(dependency): update patcher to v14.2.2
https://github.com/ReVanced/revanced-patcher/releases/tag/v14.2.2
2023-08-31 20:35:24 +07:00
oSumAtrIX
e49bcb2a69 chore: simplify issue templates (#1165) 2023-08-31 17:36:29 +07:00
Ushie
42e41c399f fix: hide install button on error 2023-08-28 19:05:30 +03:00
Ushie
166a3180d3 build: correct version code 2023-08-28 03:20:15 +03:00
Ushie
3bf4982f23 chore: merge dev to main (#1163) 2023-08-28 02:47:47 +03:00
oSumAtrIX
f4e1cccfac build: bump version to v1.9.4 2023-08-28 01:44:46 +02:00
oSumAtrIX
7911a8f49e fix: properly log messages and progress 2023-08-28 01:44:46 +02:00
oSumAtrIX
64a96fc3ce fix: close before returning 2023-08-28 01:44:46 +02:00
BenjaminHalko
8e2cfbddc5 fix: ignore the root .gradle folder 2023-08-27 14:05:39 -07:00
oSumAtrIX
45fae3f0fd build: bump ReVanced Patcher back to v14.2.1
This reverts the previous regression with the dependency to ReVanced Patcher.
2023-08-27 22:35:03 +02:00
aAbed
e45a7824c1 build: bump version to v1.9.3 2023-08-27 13:27:53 +07:00
aAbed
5d72c48a76 chore: merge dev to main (#1157) 2023-08-27 13:27:01 +07:00
aAbed
d6169c6fa2 fix: broken settings page 2023-08-27 11:55:21 +05:45
Ushie
9df6d52e2d build: bump version to v1.9.2 2023-08-27 05:40:50 +03:00
Ushie
239de8e923 chore: merge dev to main (#1156) 2023-08-27 05:40:13 +03:00
Ushie
7d553a87f3 build: revert patcher to v11.0.4 2023-08-27 05:39:24 +03:00
Ushie
557b42bc56 build: bump version to 1.9.1 2023-08-27 03:58:30 +03:00
Ushie
8423914748 chore: merge dev to main (#1155) 2023-08-27 03:57:57 +03:00
Ushie
07dce23794 build: bump patcher to v14.2.0 2023-08-27 03:55:56 +03:00
Ushie
18fd0552db build: bump version to v1.9.0 2023-08-27 02:23:36 +03:00
Ushie
d537d48f8e chore: merge dev to main (#1125) 2023-08-27 02:21:48 +03:00
Ushie
b456512bbb build: bump patcher to v14.1.0 (#1153)
Co-authored-by: aAbed <aabedhkhan@gmail.com>
2023-08-27 02:21:16 +03:00
Pun Butrach
d9953b1473 fix(installer): use correct elevation level 2023-08-24 19:00:50 +07:00
aAbed
c6ac898390 fix: universal patches not selectable 2023-08-17 18:25:23 +05:45
Pun Butrach
43f98cec43 build: make variant more identifiable
Release variant will use the "ReVanced Manager" app name, Debug will use "ReVanced Manager Debug"
2023-08-15 21:31:34 +07:00
Pun Butrach
63175cdec6 ci(build): create debug variant instead of release 2023-08-15 20:55:11 +07:00
Pun Butrach
6436a1ec61 build: debug apk come with application suffix
Instead of using the same one as Release, app.revanced.manager.flutter - We add .debug to it, yay, co-installation of two variants!
2023-08-15 20:37:43 +07:00
aAbed
c400619338 feat: disable changing patches selection by default (#1132)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-08-15 16:05:27 +07:00
Ushie
dcd5ba41cf build: add mavenlocal to repositories 2023-08-14 03:40:05 +03:00
Pun Butrach
7525e52fab fix(installer): use correct bg colour for dialog 2023-08-12 15:07:04 +07:00
Pun Butrach
72fd24e624 fix: use i18n translation for installer page 2023-08-12 15:06:17 +07:00
Pun Butrach
11d8f9fd30 refactor: apply changes according to Dart
trailing commas
2023-08-12 14:53:54 +07:00
Pun Butrach
4b0c8cecc8 ci(build): update event trigger
This add ".github/workflows/pr-build.yml" to build on every change to the CI Build to check if there's no error when it got modified.

And also remove "fastlane/**" because that's not related to the ReVanced Manager's code
2023-08-12 13:56:18 +07:00
Dhruvan Bhalara
372ce174c9 fix: overlapping issue in application selection page (#1128) 2023-08-11 19:19:44 +03:00
kitadai31
381daff980 fix: exclude x86 aapt2 binary from release builds (#1126) 2023-08-11 19:19:33 +03:00
Subhamoy Biswas
94acebbebd docs(fixed): replaced some emoji icons, not rendering on chromium based browsers on windows (#1123) 2023-08-11 10:17:46 +07:00
aAbed
f90f6e81ee feat: patch apps without internet (#1114) 2023-08-11 08:11:19 +07:00
Dhruvan Bhalara
97e37f304b refactor: migrate MediaQuery properties to InheritedModel (#1115)
Co-authored-by: Ushie <ushiekane@gmail.com>
2023-08-10 15:25:07 +03:00
Pun Butrach
f5e45ead26 ci(build): don't build on every pull request unrelated to application 2023-08-10 19:14:13 +07:00
Pun Butrach
14f765f4b4 ci(build): don't cache Gradle
Gradle take up so much space in cache that it wouldn't make sense for us to cache it since we are rapidly hitting the cache limit every time.
2023-08-10 19:12:15 +07:00
Pun Butrach
b77d46b2c2 feat(installer): redesign utility options (#1062)
Co-authored-by: Ushie <ushiekane@gmail.com>
2023-08-10 19:02:30 +07:00
55 changed files with 1499 additions and 1243 deletions

View File

@@ -1,5 +1,5 @@
name: 🐞 Bug report name: 🐞 Bug report
description: Report a very clearly broken issue. description: Create a new bug report.
title: 'bug: <title>' title: 'bug: <title>'
labels: [bug] labels: [bug]
body: body:
@@ -8,53 +8,20 @@ body:
value: | value: |
# ReVanced Manager bug report # ReVanced Manager bug report
Important to note that your issue may have already been reported before. Please check for existing issues [here](https://github.com/revanced/revanced-manager/labels/bug). Please check for existing issues [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
- type: dropdown
attributes:
label: Type
options:
- Error while running the manager
- Error at runtime
- Cosmetic
- Other
validations:
required: true
- type: textarea - type: textarea
attributes: attributes:
label: Bug description label: Bug description
description: How did you find the bug? Any additional details that might help? description: |
- Describe your bug in detail
- Add steps to reproduce the bug if possible (Step 1. Download some files. Step 2. ...)
- Add images and videos if possible
- List selected patches if applicable
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Steps to reproduce label: Version of ReVanced Manager and version & name of application you tried to patch
description: Add the steps to reproduce this bug, including your environment.
placeholder: Step 1. Download some files. Step 2. ...
validations:
required: true
- type: textarea
attributes:
label: Android version
description: Android version used.
validations:
required: true
- type: textarea
attributes:
label: Manager version
description: Manager version used.
validations:
required: true
- type: textarea
attributes:
label: Target package name
description: App you tried to patch.
validations:
required: true
- type: textarea
attributes:
label: Target package version.
description: Version of the app you tried to patch.
validations: validations:
required: true required: true
- type: dropdown - type: dropdown
@@ -64,57 +31,31 @@ body:
- Non-root - Non-root
- Root - Root
validations: validations:
required: true required: false
- type: textarea - type: textarea
attributes: attributes:
label: Patches selected. label: Device logs
description: Patches you selected for the app. description: Export logs in ReVanced Manager settings.
validations:
required: true
- type: textarea
attributes:
label: Device logs (exported using Manager settings).
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so there is no need for backticks.
render: shell render: shell
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Installer logs (exported using Installer menu option) [unneeded if the issue is not during patching]. label: Patcher logs
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so there is no need for backticks. description: Export logs in "Patcher" screen.
render: shell render: shell
validations: validations:
required: false required: false
- type: textarea
attributes:
label: Screenshots or video
description: Add screenshots or videos that show the bug here.
placeholder: Drag and drop the screenshots/videos into this box.
validations:
required: false
- type: textarea
attributes:
label: Solution
description: If applicable, add a possible solution.
validations:
required: false
- type: textarea
attributes:
label: Additional context
description: Add additional context here.
validations:
required: false
- type: checkboxes - type: checkboxes
id: acknowledgments
attributes: attributes:
label: Acknowledgments label: Acknowledgements
description: Your issue will be closed if you haven't done these steps. description: Your issue will be closed if you don't follow the checklist below!
options: options:
- label: I have searched the existing issues; this is new and no duplicate or related to another open issue. - label: This request is not a duplicate of an existing issue.
required: true required: true
- label: I have written a short but informative title. - label: I have chosen an appropriate title.
required: true required: true
- label: I properly filled out all of the requested information in this issue. - label: All requested information has been provided properly.
required: true required: true
- label: The issue is solely related to ReVanced Manager and not caused by patches. - label: The issue is solely related to the ReVanced Manager
required: true required: true

View File

@@ -1,52 +1,42 @@
name: ⭐ Feature request name: ⭐ Feature request
description: Create a detailed feature request. description: Create a new feature request.
title: 'feat: <title>' title: 'feat: <title>'
labels: [feature-request] labels: [feature-request]
body: body:
- type: dropdown - type: markdown
attributes: attributes:
label: Type value: |
options: # ReVanced Manager feature request
- Functionality
- Cosmetic Please check for existing feature requests [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
- Other
validations:
required: true
- type: textarea - type: textarea
attributes: attributes:
label: Issue label: Feature description
description: What is the current problem. Why does it require a feature request? description: Describe your feature in detail.
validations:
required: true
- type: textarea
attributes:
label: Feature
description: Describe your feature in detail. How does it solve the issue?
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Motivation label: Motivation
description: Why should your feature should be considered? description: Explain why the lack of it is a problem.
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Additional context label: Additional context
description: Add additional context here. description: In case there is something else you want to add.
validations: validations:
required: false required: false
- type: checkboxes - type: checkboxes
id: acknowledgements
attributes: attributes:
label: Acknowledgements label: Acknowledgements
description: Your issue will be closed if you haven't done these steps. description: Your issue will be closed if you don't follow the checklist below!
options: options:
- label: I have searched the existing issues and this is a new and no duplicate or related to another open issue. - label: This request is not a duplicate of an existing issue.
required: true required: true
- label: I have written a short but informative title. - label: I have chosen an appropriate title.
required: true required: true
- label: I filled out all of the requested information in this issue properly. - label: All requested information has been provided properly.
required: true required: true
- label: The issue is related solely to the ReVanced Manager - label: The issue is solely related to the ReVanced Manager
required: true required: true

View File

@@ -1,38 +0,0 @@
name: Analyze Code
on:
push:
branches: [ "dev" ]
paths:
- "**.dart"
- ".github/workflows/analyze.yml"
pull_request:
branches: [ "main", "dev" ]
types:
- opened
- reopened
- synchronize
- ready_for_review
paths:
- "**.dart"
- ".github/workflows/analyze.yml"
jobs:
build:
name: "Static analysis & format check"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
cache: true
- name: Install Flutter dependencies
run: flutter pub get
- name: Generate files with Builder
run: flutter packages pub run build_runner build --delete-conflicting-outputs
- name: Analyze code
uses: ValentinVignal/action-dart-analyze@v0.15
with:
fail-on: warning

View File

@@ -2,6 +2,11 @@ name: PR Build
on: on:
pull_request: pull_request:
paths:
- ".github/workflows/pr-build.yml"
- "android/**"
- "assets/**"
- "lib/**"
jobs: jobs:
build: build:
@@ -9,7 +14,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
# Make sure the release step uses its own credentials: # Make sure the release step uses its own credentials:
# https://github.com/cycjimmy/semantic-release-action#private-packages # https://github.com/cycjimmy/semantic-release-action#private-packages
@@ -20,7 +25,6 @@ jobs:
with: with:
java-version: '11' java-version: '11'
distribution: 'zulu' distribution: 'zulu'
cache: 'gradle'
- name: Setup Flutter - name: Setup Flutter
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
@@ -33,9 +37,9 @@ jobs:
- name: Build with Flutter - name: Build with Flutter
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: flutter build apk run: flutter build apk --debug
- name: Upload build - name: Upload build
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: revanced-manager name: revanced-manager
path: build/app/outputs/flutter-apk/app-release.apk path: build/app/outputs/flutter-apk/app-debug.apk

View File

@@ -9,7 +9,7 @@ jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set env - name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Set up JDK 11 - name: Set up JDK 11

1
.gitignore vendored
View File

@@ -58,6 +58,7 @@ unlinked.ds
unlinked_spec.ds unlinked_spec.ds
# Android related # Android related
.gradle/
**/android/**/gradle-wrapper.jar **/android/**/gradle-wrapper.jar
**/android/.gradle **/android/.gradle
**/android/captures/ **/android/captures/

View File

@@ -3,29 +3,33 @@
The official ReVanced Manager based on Flutter. The official ReVanced Manager based on Flutter.
## 🔽 Download ## 🔽 Download
To download latest Manager, go [here](https://github.com/revanced/revanced-manager/releases/latest) and install the provided APK file.
You can obtain ReVanced Manager by downloading it from either [revanced.app/download](https://revanced.app/download) or [GitHub Releases](https://github.com/ReVanced/revanced-manager/releases)
## 📝 Prerequisites ## 📝 Prerequisites
1. Android 8 or higher 1. Android 8 or higher
2. Does not work on some armv7 devices 2. Incompatible with certain ARMv7 devices
## 📃 Documentation
The documentation can be found [here](https://github.com/revanced/revanced-manager/tree/main/docs).
## 🔴 Issues ## 🔴 Issues
For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose). For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose).
## 💭 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 ## 🌐 Translation
[![Crowdin](https://badges.crowdin.net/revanced/localized.svg)](https://crowdin.com/project/revanced) [![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) We're accepting translations on [Crowdin](https://translate.revanced.app).
## 🛠️ Building Manager from source ## 🛠️ Building Manager from source
1. Setup flutter environment for your [platform](https://docs.flutter.dev/get-started/install) 1. Setup flutter environment for your [platform](https://docs.flutter.dev/get-started/install)
2. Clone the repository locally 2. Clone the repository locally
3. Add your github token in gradle.properties like [this](/docs/4_building.md) 3. Add your GitHub token in gradle.properties like [this](/docs/4_building.md)
4. Open the project in terminal 4. Open the project in terminal
5. Run `flutter pub get` 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) 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` 7. To build release APK run `flutter build apk`

View File

@@ -54,7 +54,21 @@ android {
release { release {
shrinkResources false shrinkResources false
minifyEnabled false minifyEnabled false
resValue "string", "app_name", "ReVanced Manager"
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
ndk {
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
}
}
debug {
shrinkResources false
minifyEnabled false
resValue "string", "app_name", "ReVanced Manager Debug"
applicationIdSuffix ".debug"
signingConfig signingConfigs.debug
ndk {
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
}
} }
} }
@@ -71,10 +85,9 @@ 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:11.0.4" implementation "app.revanced:revanced-patcher:16.0.2"
// 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")
} }

View File

@@ -20,13 +20,12 @@
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<application <application
android:label="ReVanced Manager" android:label="@string/app_name"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:largeHeap="true" android:largeHeap="true"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:extractNativeLibs="true" android:extractNativeLibs="true">
android:enableOnBackInvokedCallback="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@@ -43,6 +42,10 @@
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".ExportSettingsActivity"
android:exported="true">
</activity>
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />

View File

@@ -0,0 +1,83 @@
package app.revanced.manager.flutter
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Base64
import org.json.JSONObject
import java.io.ByteArrayInputStream
import java.io.File
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.MessageDigest
class ExportSettingsActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val callingPackageName = getCallingPackage()!!
if (getFingerprint(callingPackageName) == getFingerprint(getPackageName())) {
// Create JSON Object
val json = JSONObject()
// Default Data
json.put("keystorePassword", "s3cur3p@ssw0rd")
// Load Shared Preferences
val sharedPreferences = getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
val allEntries: Map<String, *> = sharedPreferences.getAll()
for ((key, value) in allEntries.entries) {
json.put(key.replace("flutter.", ""), value)
}
// Load keystore
val keystoreFile = File(getExternalFilesDir(null), "/revanced-manager.keystore")
if (keystoreFile.exists()) {
val keystoreBytes = keystoreFile.readBytes()
val keystoreBase64 = Base64.encodeToString(keystoreBytes, Base64.DEFAULT)
json.put("keystore", keystoreBase64)
}
// Load saved patches
val storedPatchesFile = File(filesDir.parentFile.absolutePath, "/app_flutter/selected-patches.json")
if (storedPatchesFile.exists()) {
val patchesBytes = storedPatchesFile.readBytes()
val patches = String(patchesBytes, Charsets.UTF_8)
json.put("patches", JSONObject(patches))
}
// Send data back
val resultIntent = Intent()
resultIntent.putExtra("data", json.toString())
setResult(Activity.RESULT_OK, resultIntent)
finish()
} else {
val resultIntent = Intent()
setResult(Activity.RESULT_CANCELED)
finish()
}
}
fun getFingerprint(packageName: String): String {
// Get the signature of the app that matches the package name
val packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
val signature = packageInfo.signatures[0]
// Get the raw certificate data
val rawCert = signature.toByteArray()
// Generate an X509Certificate from the data
val certFactory = CertificateFactory.getInstance("X509")
val x509Cert = certFactory.generateCertificate(ByteArrayInputStream(rawCert)) as X509Certificate
// Get the SHA256 fingerprint
val fingerprint = MessageDigest.getInstance("SHA256").digest(x509Cert.encoded).joinToString("") {
"%02x".format(it)
}
return fingerprint
}
}

View File

@@ -1,28 +1,30 @@
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 app.revanced.manager.flutter.utils.Aapt import app.revanced.manager.flutter.utils.Aapt
import app.revanced.manager.flutter.utils.aligning.ZipAligner import app.revanced.manager.flutter.utils.aligning.ZipAligner
import app.revanced.manager.flutter.utils.signing.Signer import app.revanced.manager.flutter.utils.signing.Signer
import app.revanced.manager.flutter.utils.zip.ZipFile import app.revanced.manager.flutter.utils.zip.ZipFile
import app.revanced.manager.flutter.utils.zip.structures.ZipEntry import app.revanced.manager.flutter.utils.zip.structures.ZipEntry
import app.revanced.patcher.PatchBundleLoader
import app.revanced.patcher.PatchSet
import app.revanced.patcher.Patcher import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherOptions import app.revanced.patcher.PatcherOptions
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages import app.revanced.patcher.patch.PatchResult
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.logging.Logger
import app.revanced.patcher.util.patch.PatchBundle
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
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.cancel
import kotlinx.coroutines.runBlocking
import org.json.JSONArray
import org.json.JSONObject
import java.io.File import java.io.File
import java.io.PrintWriter
private const val PATCHER_CHANNEL = "app.revanced.manager.flutter/patcher" import java.io.StringWriter
private const val INSTALLER_CHANNEL = "app.revanced.manager.flutter/installer" import java.lang.Error
import java.util.logging.LogRecord
import java.util.logging.Logger
class MainActivity : FlutterActivity() { class MainActivity : FlutterActivity() {
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
@@ -30,14 +32,23 @@ class MainActivity : FlutterActivity() {
private var cancel: Boolean = false private var cancel: Boolean = false
private var stopResult: MethodChannel.Result? = null private var stopResult: MethodChannel.Result? = null
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { private lateinit var patches: PatchSet
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
val mainChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, PATCHER_CHANNEL)
installerChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, INSTALLER_CHANNEL) val patcherChannel = "app.revanced.manager.flutter/patcher"
val installerChannel = "app.revanced.manager.flutter/installer"
val mainChannel =
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, patcherChannel)
this.installerChannel =
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, installerChannel)
mainChannel.setMethodCallHandler { call, result -> mainChannel.setMethodCallHandler { call, result ->
when (call.method) { when (call.method) {
"runPatcher" -> { "runPatcher" -> {
val patchBundleFilePath = call.argument<String>("patchBundleFilePath")
val originalFilePath = call.argument<String>("originalFilePath") val originalFilePath = call.argument<String>("originalFilePath")
val inputFilePath = call.argument<String>("inputFilePath") val inputFilePath = call.argument<String>("inputFilePath")
val patchedFilePath = call.argument<String>("patchedFilePath") val patchedFilePath = call.argument<String>("patchedFilePath")
@@ -48,7 +59,7 @@ class MainActivity : FlutterActivity() {
val keyStoreFilePath = call.argument<String>("keyStoreFilePath") val keyStoreFilePath = call.argument<String>("keyStoreFilePath")
val keystorePassword = call.argument<String>("keystorePassword") val keystorePassword = call.argument<String>("keystorePassword")
if (patchBundleFilePath != null && if (
originalFilePath != null && originalFilePath != null &&
inputFilePath != null && inputFilePath != null &&
patchedFilePath != null && patchedFilePath != null &&
@@ -62,7 +73,6 @@ class MainActivity : FlutterActivity() {
cancel = false cancel = false
runPatcher( runPatcher(
result, result,
patchBundleFilePath,
originalFilePath, originalFilePath,
inputFilePath, inputFilePath,
patchedFilePath, patchedFilePath,
@@ -73,14 +83,55 @@ class MainActivity : FlutterActivity() {
keyStoreFilePath, keyStoreFilePath,
keystorePassword keystorePassword
) )
} else { } else result.notImplemented()
result.notImplemented()
}
} }
"stopPatcher" -> { "stopPatcher" -> {
cancel = true cancel = true
stopResult = result stopResult = result
} }
"getPatches" -> {
val patchBundleFilePath = call.argument<String>("patchBundleFilePath")!!
val cacheDirPath = call.argument<String>("cacheDirPath")!!
try {
patches = PatchBundleLoader.Dex(
File(patchBundleFilePath),
optimizedDexDirectory = File(cacheDirPath)
)
} catch (ex: Exception) {
return@setMethodCallHandler result.notImplemented()
} catch (err: Error) {
return@setMethodCallHandler result.notImplemented()
}
JSONArray().apply {
patches.forEach {
JSONObject().apply {
put("name", it.name)
put("description", it.description)
put("excluded", !it.use)
put("compatiblePackages", JSONArray().apply {
it.compatiblePackages?.forEach { compatiblePackage ->
val compatiblePackageJson = JSONObject().apply {
put("name", compatiblePackage.name)
put(
"versions",
JSONArray().apply {
compatiblePackage.versions?.forEach { version ->
put(version)
}
})
}
put(compatiblePackageJson)
}
})
}.let(::put)
}
}.toString().let(result::success)
}
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
@@ -88,7 +139,6 @@ class MainActivity : FlutterActivity() {
private fun runPatcher( private fun runPatcher(
result: MethodChannel.Result, result: MethodChannel.Result,
patchBundleFilePath: String,
originalFilePath: String, originalFilePath: String,
inputFilePath: String, inputFilePath: String,
patchedFilePath: String, patchedFilePath: String,
@@ -105,179 +155,141 @@ class MainActivity : FlutterActivity() {
val outFile = File(outFilePath) val outFile = File(outFilePath)
val integrations = File(integrationsPath) val integrations = File(integrationsPath)
val keyStoreFile = File(keyStoreFilePath) val keyStoreFile = File(keyStoreFilePath)
val cacheDir = File(cacheDirPath)
Thread { Thread {
try { fun updateProgress(progress: Double, header: String, log: String) {
handler.post { handler.post {
installerChannel.invokeMethod( installerChannel.invokeMethod(
"update", "update",
mapOf( mapOf(
"progress" to 0.1, "progress" to progress,
"header" to "", "header" to header,
"log" to "Copying original apk" "log" to log
) )
) )
} }
}
if(cancel) { fun postStop() = handler.post { stopResult!!.success(null) }
handler.post { stopResult!!.success(null) }
// Setup logger
Logger.getLogger("").apply {
handlers.forEach {
it.close()
removeHandler(it)
}
object : java.util.logging.Handler() {
override fun publish(record: LogRecord) {
if (record.loggerName?.startsWith("app.revanced") != true) return
updateProgress(-1.0, "", record.message)
}
override fun flush() = Unit
override fun close() = flush()
}.let(::addHandler)
}
try {
updateProgress(0.0, "", "Copying APK")
if (cancel) {
postStop()
return@Thread return@Thread
} }
originalFile.copyTo(inputFile, true) originalFile.copyTo(inputFile, true)
handler.post { if (cancel) {
installerChannel.invokeMethod( postStop()
"update", return@Thread
mapOf( }
"progress" to 0.2,
"header" to "Unpacking apk...", updateProgress(0.05, "Reading APK...", "Reading APK")
"log" to "Unpacking input apk"
) val patcher = Patcher(
PatcherOptions(
inputFile,
cacheDir,
Aapt.binary(applicationContext).absolutePath,
cacheDir.path,
) )
} )
if(cancel) { if (cancel) {
handler.post { stopResult!!.success(null) } postStop()
return@Thread return@Thread
} }
val patcher = updateProgress(0.1, "Loading patches...", "Loading patches")
Patcher(
PatcherOptions(
inputFile,
cacheDirPath,
Aapt.binary(applicationContext).absolutePath,
cacheDirPath,
logger = ManagerLogger()
)
)
if(cancel) { val patches = patches.filter { patch ->
handler.post { stopResult!!.success(null) } val isCompatible = patch.compatiblePackages?.any {
it.name == patcher.context.packageMetadata.packageName
} ?: false
val compatibleOrUniversal =
isCompatible || patch.compatiblePackages.isNullOrEmpty()
compatibleOrUniversal && selectedPatches.any { it == patch.name }
}
if (cancel) {
postStop()
return@Thread return@Thread
} }
handler.post { updateProgress(0.15, "Executing...", "")
installerChannel.invokeMethod(
"update",
mapOf("progress" to 0.3, "header" to "", "log" to "")
)
}
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 0.4,
"header" to "Merging integrations...",
"log" to "Merging integrations"
)
)
}
if(cancel) { // Update the progress bar every time a patch is executed from 0.15 to 0.7
handler.post { stopResult!!.success(null) } val totalPatchesCount = patches.size
return@Thread val progressStep = 0.55 / totalPatchesCount
} var progress = 0.15
patcher.addIntegrations(listOf(integrations)) {} patcher.apply {
acceptIntegrations(listOf(integrations))
acceptPatches(patches)
if(cancel) { runBlocking {
handler.post { stopResult!!.success(null) } apply(false).collect { patchResult: PatchResult ->
return@Thread if (cancel) {
} handler.post { stopResult!!.success(null) }
this.cancel()
this@apply.close()
return@collect
}
handler.post { val msg = patchResult.exception?.let {
installerChannel.invokeMethod( val writer = StringWriter()
"update", it.printStackTrace(PrintWriter(writer))
mapOf( "${patchResult.patch.name} failed: $writer"
"progress" to 0.5, } ?: run {
"header" to "Applying patches...", "${patchResult.patch.name} succeeded"
"log" to "" }
)
)
}
if(cancel) { updateProgress(progress, "", msg)
handler.post { stopResult!!.success(null) } progress += progressStep
return@Thread
}
val patches = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE) {
PatchBundle.Dex(
patchBundleFilePath,
DexClassLoader(
patchBundleFilePath,
cacheDirPath,
null,
javaClass.classLoader
)
).loadPatches().filter { patch ->
(patch.compatiblePackages?.any { it.name == patcher.context.packageMetadata.packageName } == true || patch.compatiblePackages.isNullOrEmpty()) &&
selectedPatches.any { it == patch.patchName }
}
} else {
TODO("VERSION.SDK_INT < CUPCAKE")
}
if(cancel) {
handler.post { stopResult!!.success(null) }
return@Thread
}
patcher.addPatches(patches)
patcher.executePatches().forEach { (patch, res) ->
if (res.isSuccess) {
val msg = "Applied $patch"
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 0.5,
"header" to "",
"log" to msg
)
)
} }
if(cancel) {
handler.post { stopResult!!.success(null) }
return@Thread
}
return@forEach
}
val msg =
"Failed to apply $patch: " + "${res.exceptionOrNull()!!.message ?: res.exceptionOrNull()!!.cause!!::class.simpleName}"
handler.post {
installerChannel.invokeMethod(
"update",
mapOf("progress" to 0.5, "header" to "", "log" to msg)
)
}
if(cancel) {
handler.post { stopResult!!.success(null) }
return@Thread
} }
} }
handler.post { if (cancel) {
installerChannel.invokeMethod( postStop()
"update", patcher.close()
mapOf(
"progress" to 0.7,
"header" to "Repacking apk...",
"log" to "Repacking patched apk"
)
)
}
if(cancel) {
handler.post { stopResult!!.success(null) }
return@Thread return@Thread
} }
val res = patcher.save()
updateProgress(0.8, "Building...", "")
val res = patcher.get()
patcher.close()
ZipFile(patchedFile).use { file -> ZipFile(patchedFile).use { file ->
res.dexFiles.forEach { res.dexFiles.forEach {
if(cancel) { if (cancel) {
handler.post { stopResult!!.success(null) } postStop()
return@Thread return@Thread
} }
file.addEntryCompressData( file.addEntryCompressData(
@@ -296,90 +308,35 @@ class MainActivity : FlutterActivity() {
ZipAligner::getEntryAlignment ZipAligner::getEntryAlignment
) )
} }
if(cancel) {
handler.post { stopResult!!.success(null) } if (cancel) {
postStop()
return@Thread return@Thread
} }
handler.post {
installerChannel.invokeMethod( updateProgress(0.9, "Signing...", "Signing APK")
"update",
mapOf(
"progress" to 0.9,
"header" to "Signing apk...",
"log" to ""
)
)
}
try { try {
Signer("ReVanced", keystorePassword).signApk( Signer("ReVanced", keystorePassword)
patchedFile, .signApk(patchedFile, outFile, keyStoreFile)
outFile,
keyStoreFile
)
} catch (e: Exception) { } catch (e: Exception) {
//log to console print("Error signing APK: ${e.message}")
print("Error signing apk: ${e.message}")
e.printStackTrace() e.printStackTrace()
} }
handler.post { updateProgress(1.0, "Patched", "Patched")
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 1.0,
"header" to "Finished!",
"log" to "Finished!"
)
)
}
} catch (ex: Throwable) { } catch (ex: Throwable) {
val stack = ex.stackTraceToString() if (!cancel) {
handler.post { val stack = ex.stackTraceToString()
installerChannel.invokeMethod( updateProgress(
"update", -100.0,
mapOf( "Failed",
"progress" to -100.0, "An error occurred:\n$stack"
"header" to "Aborted...",
"log" to "An error occurred! Aborted\nError:\n$stack"
)
) )
} }
} }
handler.post { result.success(null) } handler.post { result.success(null) }
}.start() }.start()
} }
inner class ManagerLogger : Logger {
override fun error(msg: String) {
handler.post {
installerChannel
.invokeMethod(
"update",
mapOf("progress" to -1.0, "header" to "", "log" to msg)
)
}
}
override fun warn(msg: String) {
handler.post {
installerChannel.invokeMethod(
"update",
mapOf("progress" to -1.0, "header" to "", "log" to msg)
)
}
}
override fun info(msg: String) {
handler.post {
installerChannel.invokeMethod(
"update",
mapOf("progress" to -1.0, "header" to "", "log" to msg)
)
}
}
override fun trace(_msg: String) { /* unused */
}
}
} }

View File

@@ -1,5 +1,5 @@
buildscript { buildscript {
ext.kotlin_version = '1.7.10' ext.kotlin_version = '1.9.0'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
@@ -22,6 +22,7 @@ allprojects {
password = (project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN")) as String password = (project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN")) as String
} }
} }
mavenLocal()
} }
} }

View File

@@ -11,6 +11,7 @@
"noButton": "No", "noButton": "No",
"warning": "Warning", "warning": "Warning",
"notice": "Notice", "notice": "Notice",
"noShowAgain": "Don't show this again",
"new": "New", "new": "New",
"navigationView": { "navigationView": {
"dashboardTab": "Dashboard", "dashboardTab": "Dashboard",
@@ -22,13 +23,13 @@
"widgetTitle": "Dashboard", "widgetTitle": "Dashboard",
"updatesSubtitle": "Updates", "updatesSubtitle": "Updates",
"patchedSubtitle": "Patched applications", "patchedSubtitle": "Patched apps",
"noUpdates": "No updates available", "noUpdates": "No updates available",
"WIP": "Work in progress...", "WIP": "Work in progress...",
"noInstallations": "No patched applications installed", "noInstallations": "No patched apps installed",
"installUpdate": "Continue to install the update?", "installUpdate": "Continue to install the update?",
"updateDialogTitle": "Update Manager", "updateDialogTitle": "Update Manager",
@@ -55,9 +56,7 @@
"updatesDisabled": "Updating a patched app is currently disabled. Repatch the app again." "updatesDisabled": "Updating a patched app is currently disabled. Repatch the app again."
}, },
"applicationItem": { "applicationItem": {
"patchButton": "Patch", "infoButton": "Info"
"infoButton": "Info",
"changelogLabel": "Changelog"
}, },
"latestCommitCard": { "latestCommitCard": {
"loadingLabel": "Loading...", "loadingLabel": "Loading...",
@@ -70,9 +69,8 @@
"widgetTitle": "Patcher", "widgetTitle": "Patcher",
"patchButton": "Patch", "patchButton": "Patch",
"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?",
"armv7WarningDialogText": "Patching on ARMv7 devices is not yet supported and might fail. Proceed anyways?", "armv7WarningDialogText": "Patching on ARMv7 devices is not yet supported and might fail. Proceed anyways?",
"splitApkWarningDialogText": "Patching a split APK is not yet supported and might fail. Proceed anyways?",
"removedPatchesWarningDialogText": "The following patches have been removed since the last time you used them.\n\n{patches}\n\nProceed anyways?" "removedPatchesWarningDialogText": "The following patches have been removed since the last time you used them.\n\n{patches}\n\nProceed anyways?"
}, },
"appSelectorCard": { "appSelectorCard": {
@@ -136,12 +134,20 @@
"unsupportedPatchVersion": "Patch is not supported for this app version. Enable the experimental toggle in settings to proceed.", "unsupportedPatchVersion": "Patch is not supported for this app version. Enable the experimental toggle in settings to proceed.",
"newPatchDialogText": "This is a new patch that has been added since the last time you have patched this app.", "newPatchDialogText": "This is a new patch that has been added since the last time you have patched this app.",
"newPatch": "New patch" "newPatch": "New patch",
"patchesChangeWarningDialogText": "It is recommended to use the default selection of patches because changing it may cause unexpected issues.\n\nIf you know what you are doing, you can enable \"Enable changing selection\" in the settings.",
"patchesChangeWarningDialogButton": "Use default selection"
}, },
"installerView": { "installerView": {
"widgetTitle": "Installer", "widgetTitle": "Installer",
"installType": "Select install type",
"installTypeDescription": "Select the installation type to proceed with.",
"installButton": "Install", "installButton": "Install",
"installRootButton": "Install as Root", "installRootType": "Mount",
"installNonRootType": "Normal",
"pressBackAgain": "Press back again to cancel", "pressBackAgain": "Press back again to cancel",
"openButton": "Open", "openButton": "Open",
"shareButton": "Share file", "shareButton": "Share file",
@@ -149,14 +155,12 @@
"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", "exportApkButtonTooltip": "Export patched APK",
"exportApkMenuOption": "Export APK", "exportLogButtonTooltip": "Export log",
"shareLogMenuOption": "Share log",
"screenshotDetected": "A screenshot has been detected. If you are trying to share the log, please share a text copy instead.\n\nCopy log to clipboard?",
"copiedToClipboard": "Copied log to clipboard",
"installErrorDialogTitle": "Error",
"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.",
"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, cannot exit..." "noExit": "Installer is still running, cannot exit..."
}, },
"settingsView": { "settingsView": {
@@ -169,8 +173,10 @@
"exportSectionTitle": "Import & export", "exportSectionTitle": "Import & export",
"logsSectionTitle": "Logs", "logsSectionTitle": "Logs",
"darkThemeLabel": "Dark mode", "themeModeLabel": "App theme",
"darkThemeHint": "Welcome to the dark side", "systemThemeLabel": "System",
"lightThemeLabel": "Light",
"darkThemeLabel": "Dark",
"dynamicThemeLabel": "Material You", "dynamicThemeLabel": "Material You",
"dynamicThemeHint": "Enjoy an experience closer to your device", "dynamicThemeHint": "Enjoy an experience closer to your device",
@@ -200,6 +206,11 @@
"logsLabel": "Logs", "logsLabel": "Logs",
"logsHint": "Share Manager's logs", "logsHint": "Share Manager's logs",
"enablePatchesSelectionLabel": "Enable changing selection",
"enablePatchesSelectionHint": "Enable changing the selection of patches.",
"enablePatchesSelectionWarningText": "Changing the default selection of patches may cause unexpected issues.\n\nEnable anyways?",
"disablePatchesSelectionWarningText": "You are about to disable changing the selection of patches.\nThe default selection of patches will be restored.\n\nDisable anyways?",
"autoUpdatePatchesLabel": "Auto update patches", "autoUpdatePatchesLabel": "Auto update patches",
"autoUpdatePatchesHint": "Automatically update ReVanced Patches to the latest version", "autoUpdatePatchesHint": "Automatically update ReVanced Patches to the latest version",
"experimentalUniversalPatchesLabel": "Experimental universal patches support", "experimentalUniversalPatchesLabel": "Experimental universal patches support",
@@ -269,7 +280,6 @@
"rootDialogText": "App was installed with superuser permissions, but currently ReVanced Manager has no permissions.\nPlease grant superuser permissions 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",
"originalPackageNameLabel": "Original package name",
"installTypeLabel": "Installation type", "installTypeLabel": "Installation type",
"rootTypeLabel": "Root", "rootTypeLabel": "Root",
"nonRootTypeLabel": "Non-root", "nonRootTypeLabel": "Non-root",

View File

@@ -2,7 +2,7 @@
In order to use ReVanced on your Android device, ReVanced Manager must be installed. In order to use ReVanced on your Android device, ReVanced Manager must be installed.
## 🪜 Installation steps ## Installation steps
1. Download the latest version of ReVanced Manager from [here](https://github.com/revanced/revanced-manager/releases/latest) 1. Download the latest version of ReVanced Manager from [here](https://github.com/revanced/revanced-manager/releases/latest)
2. Install ReVanced Manager 2. Install ReVanced Manager
@@ -11,4 +11,4 @@ In order to use ReVanced on your Android device, ReVanced Manager must be instal
The next page will guide you through using ReVanced Manager. The next page will guide you through using ReVanced Manager.
Continue: [🪛 Usage](2_usage.md) Continue: [🛠️ Usage](2_usage.md)

View File

@@ -2,7 +2,7 @@
The following pages will guide you through using ReVanced Manager to patch apps. The following pages will guide you through using ReVanced Manager to patch apps.
## 🪜 Steps to patch apps ## Steps to patch apps
1. Navigate to the **Patcher** tab from the bottom navigation bar 1. Navigate to the **Patcher** tab from the bottom navigation bar
2. Tap on the **Select an app** card 2. Tap on the **Select an app** card

View File

@@ -2,7 +2,7 @@
After patching an app, you may want to manage it. This page will guide you through managing patched apps. After patching an app, you may want to manage it. This page will guide you through managing patched apps.
## 🪜 Steps to manage patched apps ## Steps to manage patched apps
1. Tap on the **Dashboard** tab in the bottom navigation bar 1. Tap on the **Dashboard** tab in the bottom navigation bar
2. Tap on the **Info** button for the app you want to manage 2. Tap on the **Info** button for the app you want to manage

View File

@@ -2,7 +2,7 @@
In order to keep up with the latest features and bug fixes, it is recommended to keep ReVanced Manager up to date. In order to keep up with the latest features and bug fixes, it is recommended to keep ReVanced Manager up to date.
## 🪜 Updating steps ## Updating steps
1. Navigate to the **Dashboard** tab from the bottom navigation bar 1. Navigate to the **Dashboard** tab from the bottom navigation bar
2. Tap on the **Update** button in the **Updates** section 2. Tap on the **Update** button in the **Updates** section

View File

@@ -2,7 +2,7 @@
ReVanced Manager has settings that can be configured to your liking. ReVanced Manager has settings that can be configured to your liking.
## 🪛 Essential settings ## Essential settings
- ### 🔗 API URL - ### 🔗 API URL

View File

@@ -13,4 +13,4 @@ The following pages will guide you through using ReVanced Manager to patch apps,
The next page will guide you through troubleshooting ReVanced Manager. The next page will guide you through troubleshooting ReVanced Manager.
Continue: [🛟 Troubleshooting](3_troubleshooting.md) Continue: [ Troubleshooting](3_troubleshooting.md)

View File

@@ -1,4 +1,4 @@
# 🛟 Troubleshooting # Troubleshooting
In case you encounter any issues while using ReVanced Manager, please refer to this page for possible solutions. In case you encounter any issues while using ReVanced Manager, please refer to this page for possible solutions.
@@ -28,4 +28,4 @@ In case you encounter any issues while using ReVanced Manager, please refer to t
The next page will teach you how to build ReVanced Manager from source. The next page will teach you how to build ReVanced Manager from source.
Continue: [🛠️ Building from source](4_building.md) Continue: [🔨 Building from source](4_building.md)

View File

@@ -12,7 +12,7 @@ This page will guide you through building ReVanced Manager from source.
3. Create a GitHub personal access token with the `read:packages` scope [here](https://github.com/settings/tokens/new?scopes=read:packages&description=ReVanced) 3. Create a GitHub personal access token with the `read:packages` scope [here](https://github.com/settings/tokens/new?scopes=read:packages&description=ReVanced)
4. Add your GitHub username and the token to `~/.gradle/gradle.properties` 4. Add your GitHub username and the token to `~/android/gradle.properties`
```properties ```properties
gpr.user = YourUsername gpr.user = YourUsername

View File

@@ -11,8 +11,8 @@ This documentation explains how to use [ReVanced Manager](https://github.com/rev
2. [🧰 Managing patched apps](2_2_managing.md) 2. [🧰 Managing patched apps](2_2_managing.md)
3. [🔄 Updating ReVanced Manager](2_3_updating.md) 3. [🔄 Updating ReVanced Manager](2_3_updating.md)
4. [⚙️ Configuring ReVanced Manager](2_4_settings.md) 4. [⚙️ Configuring ReVanced Manager](2_4_settings.md)
3. [🛟 Troubleshooting](3_troubleshooting.md) 3. [ Troubleshooting](3_troubleshooting.md)
4. [🛠 Building from source](4_building.md) 4. [🔨 Building from source](4_building.md)
## ⏭️ Start here ## ⏭️ Start here

View File

@@ -1,5 +1,4 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:revanced_manager/utils/string.dart';
part 'patch.g.dart'; part 'patch.g.dart';
@@ -9,26 +8,19 @@ class Patch {
required this.name, required this.name,
required this.description, required this.description,
required this.excluded, required this.excluded,
required this.dependencies,
required this.compatiblePackages, required this.compatiblePackages,
}); });
factory Patch.fromJson(Map<String, dynamic> json) => _$PatchFromJson(json); factory Patch.fromJson(Map<String, dynamic> json) => _$PatchFromJson(json);
final String name; final String name;
final String description; final String? description;
final bool excluded; final bool excluded;
final List<String> dependencies;
final List<Package> compatiblePackages; final List<Package> compatiblePackages;
Map<String, dynamic> toJson() => _$PatchToJson(this); Map<String, dynamic> toJson() => _$PatchToJson(this);
String getSimpleName() { String getSimpleName() {
return name return name;
.replaceAll('-', ' ')
.split('-')
.join(' ')
.toTitleCase()
.replaceFirst('Microg', 'MicroG');
} }
} }

View File

@@ -9,23 +9,19 @@ 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,
required this.patchDate, required this.patchDate,
this.isRooted = false, this.isRooted = false,
this.isFromStorage = false, this.isFromStorage = false,
this.hasUpdates = false,
this.appliedPatches = const [], this.appliedPatches = const [],
this.changelog = const [],
}); });
factory PatchedApplication.fromJson(Map<String, dynamic> json) => factory PatchedApplication.fromJson(Map<String, dynamic> json) =>
_$PatchedApplicationFromJson(json); _$PatchedApplicationFromJson(json);
String name; String name;
String packageName; String packageName;
String originalPackageName;
String version; String version;
final String apkFilePath; final String apkFilePath;
@JsonKey( @JsonKey(
@@ -36,9 +32,7 @@ class PatchedApplication {
DateTime patchDate; DateTime patchDate;
bool isRooted; bool isRooted;
bool isFromStorage; bool isFromStorage;
bool hasUpdates;
List<String> appliedPatches; List<String> appliedPatches;
List<String> changelog;
Map<String, dynamic> toJson() => _$PatchedApplicationToJson(this); Map<String, dynamic> toJson() => _$PatchedApplicationToJson(this);

View File

@@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
@@ -6,12 +5,13 @@ import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.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/app/app.locator.dart';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.dart';
@lazySingleton @lazySingleton
class GithubAPI { class GithubAPI {
late Dio _dio = Dio(); late Dio _dio = Dio();
late final ManagerAPI _managerAPI = locator<ManagerAPI>();
final _cacheOptions = CacheOptions( final _cacheOptions = CacheOptions(
store: MemCacheStore(), store: MemCacheStore(),
@@ -19,17 +19,6 @@ class GithubAPI {
priority: CachePriority.high, priority: CachePriority.high,
); );
final Map<String, String> repoAppPath = {
'com.google.android.youtube': 'youtube',
'com.google.android.apps.youtube.music': 'music',
'com.twitter.android': 'twitter',
'com.reddit.frontpage': 'reddit',
'com.zhiliaoapp.musically': 'tiktok',
'de.dwd.warnapp': 'warnwetter',
'com.garzotto.pflotsh.ecmwf_a': 'ecmwf',
'com.spotify.music': 'spotify',
};
Future<void> initialize(String repoUrl) async { Future<void> initialize(String repoUrl) async {
try { try {
_dio = Dio( _dio = Dio(
@@ -140,38 +129,6 @@ class GithubAPI {
} }
} }
Future<List<String>> getCommits(
String packageName,
String repoName,
DateTime since,
) async {
final String path =
'src/main/kotlin/app/revanced/patches/${repoAppPath[packageName]}';
try {
final response = await _dio.get(
'/repos/$repoName/commits',
queryParameters: {
'path': path,
'since': since.toIso8601String(),
},
);
final List<dynamic> commits = response.data;
return commits
.map(
(commit) => commit['commit']['message'].split('\n')[0] +
' - ' +
commit['commit']['author']['name'] +
'\n' as String,
)
.toList();
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
return [];
}
Future<File?> getLatestReleaseFile( Future<File?> getLatestReleaseFile(
String extension, String extension,
String repoName, String repoName,
@@ -201,8 +158,14 @@ class GithubAPI {
String extension, String extension,
String repoName, String repoName,
String version, String version,
String url,
) async { ) async {
try { try {
if (url.isNotEmpty) {
return await DefaultCacheManager().getSingleFile(
url,
);
}
final Map<String, dynamic>? release = final Map<String, dynamic>? release =
await getPatchesRelease(repoName, version); await getPatchesRelease(repoName, version);
if (release != null) { if (release != null) {
@@ -211,8 +174,14 @@ class GithubAPI {
(asset) => (asset['name'] as String).endsWith(extension), (asset) => (asset['name'] as String).endsWith(extension),
); );
if (asset != null) { if (asset != null) {
final String downloadUrl = asset['browser_download_url'];
if (extension == '.apk') {
_managerAPI.setIntegrationsDownloadURL(downloadUrl);
} else {
_managerAPI.setPatchesDownloadURL(downloadUrl);
}
return await DefaultCacheManager().getSingleFile( return await DefaultCacheManager().getSingleFile(
asset['browser_download_url'], downloadUrl,
); );
} }
} }
@@ -223,21 +192,4 @@ class GithubAPI {
} }
return null; return null;
} }
Future<List<Patch>> getPatches(String repoName, String version) async {
List<Patch> patches = [];
try {
final File? f = await getPatchesReleaseFile('.json', repoName, version);
if (f != null) {
final List<dynamic> list = jsonDecode(f.readAsStringSync());
patches = list.map((patch) => Patch.fromJson(patch)).toList();
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
return patches;
}
} }

View File

@@ -2,6 +2,8 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:device_apps/device_apps.dart'; import 'package:device_apps/device_apps.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.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:path_provider/path_provider.dart';
@@ -9,8 +11,10 @@ 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/patcher_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:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/utils/check_for_supported_patch.dart'; import 'package:revanced_manager/utils/check_for_supported_patch.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:timeago/timeago.dart'; import 'package:timeago/timeago.dart';
@@ -23,6 +27,7 @@ class ManagerAPI {
final String patcherRepo = 'revanced-patcher'; final String patcherRepo = 'revanced-patcher';
final String cliRepo = 'revanced-cli'; final String cliRepo = 'revanced-cli';
late SharedPreferences _prefs; late SharedPreferences _prefs;
List<Patch> patches = [];
bool isRooted = false; bool isRooted = false;
String storedPatchesFile = '/selected-patches.json'; String storedPatchesFile = '/selected-patches.json';
String keystoreFile = String keystoreFile =
@@ -37,12 +42,14 @@ class ManagerAPI {
String defaultManagerRepo = 'revanced/revanced-manager'; String defaultManagerRepo = 'revanced/revanced-manager';
String? patchesVersion = ''; String? patchesVersion = '';
String? integrationsVersion = ''; String? integrationsVersion = '';
bool isDefaultPatchesRepo() { bool isDefaultPatchesRepo() {
return getPatchesRepo() == 'revanced/revanced-patches'; return getPatchesRepo().toLowerCase() == 'revanced/revanced-patches';
} }
bool isDefaultIntegrationsRepo() { bool isDefaultIntegrationsRepo() {
return getIntegrationsRepo() == 'revanced/revanced-integrations'; return getIntegrationsRepo().toLowerCase() ==
'revanced/revanced-integrations';
} }
Future<void> initialize() async { Future<void> initialize() async {
@@ -76,6 +83,14 @@ class ManagerAPI {
await _prefs.setString('repoUrl', url); await _prefs.setString('repoUrl', url);
} }
String getPatchesDownloadURL() {
return _prefs.getString('patchesDownloadURL') ?? '';
}
Future<void> setPatchesDownloadURL(String value) async {
await _prefs.setString('patchesDownloadURL', value);
}
String getPatchesRepo() { String getPatchesRepo() {
return _prefs.getString('patchesRepo') ?? defaultPatchesRepo; return _prefs.getString('patchesRepo') ?? defaultPatchesRepo;
} }
@@ -99,6 +114,30 @@ class ManagerAPI {
return _prefs.getBool('patchesAutoUpdate') ?? false; return _prefs.getBool('patchesAutoUpdate') ?? false;
} }
bool isPatchesChangeEnabled() {
return _prefs.getBool('patchesChangeEnabled') ?? false;
}
void setPatchesChangeEnabled(bool value) {
_prefs.setBool('patchesChangeEnabled', value);
}
bool showPatchesChangeWarning() {
return _prefs.getBool('showPatchesChangeWarning') ?? true;
}
void setPatchesChangeWarning(bool value) {
_prefs.setBool('showPatchesChangeWarning', !value);
}
bool isChangingToggleModified() {
return _prefs.getBool('isChangingToggleModified') ?? false;
}
void setChangingToggleModified(bool value) {
_prefs.setBool('isChangingToggleModified', value);
}
Future<void> setPatchesAutoUpdate(bool value) async { Future<void> setPatchesAutoUpdate(bool value) async {
await _prefs.setBool('patchesAutoUpdate', value); await _prefs.setBool('patchesAutoUpdate', value);
} }
@@ -119,6 +158,14 @@ class ManagerAPI {
await _prefs.setStringList('savedPatches-$packageName', patchesJson); await _prefs.setStringList('savedPatches-$packageName', patchesJson);
} }
String getIntegrationsDownloadURL() {
return _prefs.getString('integrationsDownloadURL') ?? '';
}
Future<void> setIntegrationsDownloadURL(String value) async {
await _prefs.setString('integrationsDownloadURL', value);
}
List<Patch> getUsedPatches(String packageName) { List<Patch> getUsedPatches(String packageName) {
final List<String> patchesJson = final List<String> patchesJson =
_prefs.getStringList('usedPatches-$packageName') ?? []; _prefs.getStringList('usedPatches-$packageName') ?? [];
@@ -154,12 +201,12 @@ class ManagerAPI {
await _prefs.setBool('useDynamicTheme', value); await _prefs.setBool('useDynamicTheme', value);
} }
bool getUseDarkTheme() { int getThemeMode() {
return _prefs.getBool('useDarkTheme') ?? false; return _prefs.getInt('themeMode') ?? 2;
} }
Future<void> setUseDarkTheme(bool value) async { Future<void> setThemeMode(int value) async {
await _prefs.setBool('useDarkTheme', value); await _prefs.setInt('themeMode', value);
} }
bool areUniversalPatchesEnabled() { bool areUniversalPatchesEnabled() {
@@ -257,26 +304,51 @@ class ManagerAPI {
} }
Future<List<Patch>> getPatches() async { Future<List<Patch>> getPatches() async {
try { if (patches.isNotEmpty) {
final String repoName = getPatchesRepo(); return patches;
final String currentVersion = await getCurrentPatchesVersion();
return await _githubAPI.getPatches(repoName, currentVersion);
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return [];
} }
final File? patchBundleFile = await downloadPatches();
final Directory appCache = await getTemporaryDirectory();
Directory('${appCache.path}/cache').createSync();
final Directory workDir =
Directory('${appCache.path}/cache').createTempSync('tmp-');
final Directory cacheDir = Directory('${workDir.path}/cache');
cacheDir.createSync();
if (patchBundleFile != null) {
try {
final String patchesJson = await PatcherAPI.patcherChannel.invokeMethod(
'getPatches',
{
'patchBundleFilePath': patchBundleFile.path,
'cacheDirPath': cacheDir.path,
},
);
final List<dynamic> patchesJsonList = jsonDecode(patchesJson);
patches = patchesJsonList
.map((patchJson) => Patch.fromJson(patchJson))
.toList();
return patches;
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
return List.empty();
} }
Future<File?> downloadPatches() async { Future<File?> downloadPatches() async {
try { try {
final String repoName = getPatchesRepo(); final String repoName = getPatchesRepo();
final String currentVersion = await getCurrentPatchesVersion(); final String currentVersion = await getCurrentPatchesVersion();
final String url = getPatchesDownloadURL();
return await _githubAPI.getPatchesReleaseFile( return await _githubAPI.getPatchesReleaseFile(
'.jar', '.jar',
repoName, repoName,
currentVersion, currentVersion,
url,
); );
} on Exception catch (e) { } on Exception catch (e) {
if (kDebugMode) { if (kDebugMode) {
@@ -290,10 +362,12 @@ class ManagerAPI {
try { try {
final String repoName = getIntegrationsRepo(); final String repoName = getIntegrationsRepo();
final String currentVersion = await getCurrentIntegrationsVersion(); final String currentVersion = await getCurrentIntegrationsVersion();
final String url = getIntegrationsDownloadURL();
return await _githubAPI.getPatchesReleaseFile( return await _githubAPI.getPatchesReleaseFile(
'.apk', '.apk',
repoName, repoName,
currentVersion, currentVersion,
url,
); );
} on Exception catch (e) { } on Exception catch (e) {
if (kDebugMode) { if (kDebugMode) {
@@ -384,27 +458,38 @@ class ManagerAPI {
Future<String> getCurrentPatchesVersion() async { Future<String> getCurrentPatchesVersion() async {
patchesVersion = _prefs.getString('patchesVersion') ?? '0.0.0'; patchesVersion = _prefs.getString('patchesVersion') ?? '0.0.0';
if (patchesVersion == '0.0.0' || isPatchesAutoUpdate()) { if (patchesVersion == '0.0.0' || isPatchesAutoUpdate()) {
patchesVersion = await getLatestPatchesVersion() ?? '0.0.0'; final String newPatchesVersion =
await setCurrentPatchesVersion(patchesVersion!); await getLatestPatchesVersion() ?? '0.0.0';
if (patchesVersion != newPatchesVersion && newPatchesVersion != '0.0.0') {
await setCurrentPatchesVersion(newPatchesVersion);
}
} }
return patchesVersion!; return patchesVersion!;
} }
Future<void> setCurrentPatchesVersion(String version) async { Future<void> setCurrentPatchesVersion(String version) async {
await _prefs.setString('patchesVersion', version); await _prefs.setString('patchesVersion', version);
await setPatchesDownloadURL('');
await downloadPatches();
} }
Future<String> getCurrentIntegrationsVersion() async { Future<String> getCurrentIntegrationsVersion() async {
integrationsVersion = _prefs.getString('integrationsVersion') ?? '0.0.0'; integrationsVersion = _prefs.getString('integrationsVersion') ?? '0.0.0';
if (integrationsVersion == '0.0.0' || isPatchesAutoUpdate()) { if (integrationsVersion == '0.0.0' || isPatchesAutoUpdate()) {
integrationsVersion = await getLatestIntegrationsVersion() ?? '0.0.0'; final String newIntegrationsVersion =
await setCurrentIntegrationsVersion(integrationsVersion!); await getLatestIntegrationsVersion() ?? '0.0.0';
if (integrationsVersion != newIntegrationsVersion &&
newIntegrationsVersion != '0.0.0') {
await setCurrentIntegrationsVersion(newIntegrationsVersion);
}
} }
return integrationsVersion!; return integrationsVersion!;
} }
Future<void> setCurrentIntegrationsVersion(String version) async { Future<void> setCurrentIntegrationsVersion(String version) async {
await _prefs.setString('integrationsVersion', version); await _prefs.setString('integrationsVersion', version);
await setIntegrationsDownloadURL('');
await downloadIntegrations();
} }
Future<List<PatchedApplication>> getAppsToRemove( Future<List<PatchedApplication>> getAppsToRemove(
@@ -420,94 +505,108 @@ class ManagerAPI {
return toRemove; return toRemove;
} }
Future<List<PatchedApplication>> getUnsavedApps( Future<List<PatchedApplication>> getMountedApps() async {
List<PatchedApplication> patchedApps, final List<PatchedApplication> mountedApps = [];
) async {
final List<PatchedApplication> unsavedApps = [];
final bool hasRootPermissions = await _rootAPI.hasRootPermissions(); final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
if (hasRootPermissions) { if (hasRootPermissions) {
final List<String> installedApps = await _rootAPI.getInstalledApps(); final List<String> installedApps = await _rootAPI.getInstalledApps();
for (final String packageName in installedApps) { for (final String packageName in installedApps) {
if (!patchedApps.any((app) => app.packageName == packageName)) {
final ApplicationWithIcon? application = await DeviceApps.getApp(
packageName,
true,
) as ApplicationWithIcon?;
if (application != null) {
unsavedApps.add(
PatchedApplication(
name: application.appName,
packageName: application.packageName,
originalPackageName: application.packageName,
version: application.versionName!,
apkFilePath: application.apkFilePath,
icon: application.icon,
patchDate: DateTime.now(),
isRooted: true,
),
);
}
}
}
}
final List<Application> userApps =
await DeviceApps.getInstalledApplications();
for (final Application app in userApps) {
if (app.packageName.startsWith('app.revanced') &&
!app.packageName.startsWith('app.revanced.manager.') &&
!patchedApps.any((uapp) => uapp.packageName == app.packageName)) {
final ApplicationWithIcon? application = await DeviceApps.getApp( final ApplicationWithIcon? application = await DeviceApps.getApp(
app.packageName, packageName,
true, true,
) as ApplicationWithIcon?; ) as ApplicationWithIcon?;
if (application != null) { if (application != null) {
unsavedApps.add( mountedApps.add(
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,
patchDate: DateTime.now(), patchDate: DateTime.now(),
isRooted: true,
), ),
); );
} }
} }
} }
return unsavedApps;
return mountedApps;
}
Future<void> showPatchesChangeWarningDialog(BuildContext context) {
final ValueNotifier<bool> noShow =
ValueNotifier(!showPatchesChangeWarning());
return showDialog(
barrierDismissible: false,
context: context,
builder: (context) => WillPopScope(
onWillPop: () async => false,
child: AlertDialog(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
title: I18nText('warning'),
content: ValueListenableBuilder(
valueListenable: noShow,
builder: (context, value, child) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
I18nText(
'patchItem.patchesChangeWarningDialogText',
child: const Text(
'',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(height: 8),
CheckboxListTile(
value: value,
contentPadding: EdgeInsets.zero,
title: I18nText(
'noShowAgain',
),
onChanged: (selected) {
noShow.value = selected!;
},
),
],
);
},
),
actions: [
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () {
setPatchesChangeWarning(noShow.value);
Navigator.of(context).pop();
},
),
],
),
),
);
} }
Future<void> reAssessSavedApps() async { Future<void> reAssessSavedApps() async {
final List<PatchedApplication> patchedApps = getPatchedApps(); final List<PatchedApplication> patchedApps = getPatchedApps();
final List<PatchedApplication> unsavedApps =
await getUnsavedApps(patchedApps); // Remove apps that are not installed anymore.
patchedApps.addAll(unsavedApps);
final List<PatchedApplication> toRemove = final List<PatchedApplication> toRemove =
await getAppsToRemove(patchedApps); await getAppsToRemove(patchedApps);
patchedApps.removeWhere((a) => toRemove.contains(a)); patchedApps.removeWhere((a) => toRemove.contains(a));
for (final PatchedApplication app in patchedApps) {
app.hasUpdates = // Determine all apps that are installed by mounting.
await hasAppUpdates(app.originalPackageName, app.patchDate); final List<PatchedApplication> mountedApps = await getMountedApps();
app.changelog = mountedApps.removeWhere(
await getAppChangelog(app.originalPackageName, app.patchDate); (app) => patchedApps
if (!app.hasUpdates) { .any((patchedApp) => patchedApp.packageName == app.packageName),
final String? currentInstalledVersion = );
(await DeviceApps.getApp(app.packageName))?.versionName; patchedApps.addAll(mountedApps);
if (currentInstalledVersion != null) {
final String currentSavedVersion = app.version;
final int currentInstalledVersionInt = int.parse(
currentInstalledVersion.replaceAll(RegExp('[^0-9]'), ''),
);
final int currentSavedVersionInt = int.parse(
currentSavedVersion.replaceAll(RegExp('[^0-9]'), ''),
);
if (currentInstalledVersionInt > currentSavedVersionInt) {
app.hasUpdates = true;
}
}
}
}
await setPatchedApps(patchedApps); await setPatchedApps(patchedApps);
} }
@@ -524,37 +623,6 @@ class ManagerAPI {
return !existsNonRoot; return !existsNonRoot;
} }
Future<bool> hasAppUpdates(
String packageName,
DateTime patchDate,
) async {
final List<String> commits = await _githubAPI.getCommits(
packageName,
getPatchesRepo(),
patchDate,
);
return commits.isNotEmpty;
}
Future<List<String>> getAppChangelog(
String packageName,
DateTime patchDate,
) async {
List<String> newCommits = await _githubAPI.getCommits(
packageName,
getPatchesRepo(),
patchDate,
);
if (newCommits.isEmpty) {
newCommits = await _githubAPI.getCommits(
packageName,
getPatchesRepo(),
patchDate,
);
}
return newCommits;
}
Future<bool> isSplitApk(PatchedApplication patchedApp) async { Future<bool> isSplitApk(PatchedApplication patchedApp) async {
Application? app; Application? app;
if (patchedApp.isFromStorage) { if (patchedApp.isFromStorage) {
@@ -622,6 +690,8 @@ class ManagerAPI {
Future<void> resetLastSelectedPatches() async { Future<void> resetLastSelectedPatches() async {
final File selectedPatchesFile = File(storedPatchesFile); final File selectedPatchesFile = File(storedPatchesFile);
selectedPatchesFile.deleteSync(); if (selectedPatchesFile.existsSync()) {
selectedPatchesFile.deleteSync();
}
} }
} }

View File

@@ -25,11 +25,14 @@ class PatcherAPI {
late Directory _tmpDir; late Directory _tmpDir;
late File _keyStoreFile; late File _keyStoreFile;
List<Patch> _patches = []; List<Patch> _patches = [];
List<Patch> _universalPatches = [];
List<String> _compatiblePackages = [];
Map filteredPatches = <String, List<Patch>>{}; Map filteredPatches = <String, List<Patch>>{};
File? _outFile; File? outFile;
Future<void> initialize() async { Future<void> initialize() async {
await _loadPatches(); await loadPatches();
await _managerAPI.downloadIntegrations();
final Directory appCache = await getTemporaryDirectory(); final Directory appCache = await getTemporaryDirectory();
_dataDir = await getExternalStorageDirectory() ?? appCache; _dataDir = await getExternalStorageDirectory() ?? appCache;
_tmpDir = Directory('${appCache.path}/patcher'); _tmpDir = Directory('${appCache.path}/patcher');
@@ -43,7 +46,23 @@ class PatcherAPI {
} }
} }
Future<void> _loadPatches() async { List<String> getCompatiblePackages() {
final List<String> compatiblePackages = [];
for (final Patch patch in _patches) {
for (final Package package in patch.compatiblePackages) {
if (!compatiblePackages.contains(package.name)) {
compatiblePackages.add(package.name);
}
}
}
return compatiblePackages;
}
List<Patch> getUniversalPatches() {
return _patches.where((patch) => patch.compatiblePackages.isEmpty).toList();
}
Future<void> loadPatches() async {
try { try {
if (_patches.isEmpty) { if (_patches.isEmpty) {
_patches = await _managerAPI.getPatches(); _patches = await _managerAPI.getPatches();
@@ -54,6 +73,9 @@ class PatcherAPI {
} }
_patches = List.empty(); _patches = List.empty();
} }
_compatiblePackages = getCompatiblePackages();
_universalPatches = getUniversalPatches();
} }
Future<List<ApplicationWithIcon>> getFilteredInstalledApps( Future<List<ApplicationWithIcon>> getFilteredInstalledApps(
@@ -61,48 +83,42 @@ class PatcherAPI {
) async { ) async {
final List<ApplicationWithIcon> filteredApps = []; final List<ApplicationWithIcon> filteredApps = [];
final bool allAppsIncluded = final bool allAppsIncluded =
_patches.any((patch) => patch.compatiblePackages.isEmpty) && _universalPatches.isNotEmpty && showUniversalPatches;
showUniversalPatches;
if (allAppsIncluded) { if (allAppsIncluded) {
final allPackages = await DeviceApps.getInstalledApplications( final appList = await DeviceApps.getInstalledApplications(
includeAppIcons: true, includeAppIcons: true,
onlyAppsWithLaunchIntent: true, onlyAppsWithLaunchIntent: true,
); );
for (final pkg in allPackages) {
if (!filteredApps.any((app) => app.packageName == pkg.packageName)) { for (final app in appList) {
final appInfo = await DeviceApps.getApp( filteredApps.add(app as ApplicationWithIcon);
pkg.packageName,
true,
) as ApplicationWithIcon?;
if (appInfo != null) {
filteredApps.add(appInfo);
}
}
} }
} }
for (final Patch patch in _patches) { for (final packageName in _compatiblePackages) {
for (final Package package in patch.compatiblePackages) { try {
try { if (!filteredApps.any((app) => app.packageName == packageName)) {
if (!filteredApps.any((app) => app.packageName == package.name)) { final ApplicationWithIcon? app = await DeviceApps.getApp(
final ApplicationWithIcon? app = await DeviceApps.getApp( packageName,
package.name, true,
true, ) as ApplicationWithIcon?;
) as ApplicationWithIcon?; if (app != null) {
if (app != null) { filteredApps.add(app);
filteredApps.add(app);
}
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
} }
} }
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
} }
} }
return filteredApps; return filteredApps;
} }
List<Patch> getFilteredPatches(String packageName) { List<Patch> getFilteredPatches(String packageName) {
if (!_compatiblePackages.contains(packageName)) {
return _universalPatches;
}
final List<Patch> patches = _patches final List<Patch> patches = _patches
.where( .where(
(patch) => (patch) =>
@@ -130,55 +146,20 @@ class PatcherAPI {
.toList(); .toList();
} }
Future<bool> needsResourcePatching(
List<Patch> selectedPatches,
) async {
return selectedPatches.any(
(patch) => patch.dependencies.any(
(dep) => dep.contains('resource-'),
),
);
}
Future<bool> needsSettingsPatch(List<Patch> selectedPatches) async {
return selectedPatches.any(
(patch) => patch.dependencies.any(
(dep) => dep.contains('settings'),
),
);
}
Future<void> runPatcher( Future<void> runPatcher(
String packageName, String packageName,
String apkFilePath, String apkFilePath,
List<Patch> selectedPatches, List<Patch> selectedPatches,
) async { ) async {
final bool includeSettings = await needsSettingsPatch(selectedPatches);
if (includeSettings) {
try {
final Patch? settingsPatch = _patches.firstWhereOrNull(
(patch) =>
patch.name.contains('settings') &&
patch.compatiblePackages.any((pack) => pack.name == packageName),
);
if (settingsPatch != null) {
selectedPatches.add(settingsPatch);
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
final File? patchBundleFile = await _managerAPI.downloadPatches();
final File? integrationsFile = await _managerAPI.downloadIntegrations(); final File? integrationsFile = await _managerAPI.downloadIntegrations();
if (patchBundleFile != null) {
if (integrationsFile != null) {
_dataDir.createSync(); _dataDir.createSync();
_tmpDir.createSync(); _tmpDir.createSync();
final Directory workDir = _tmpDir.createTempSync('tmp-'); final Directory workDir = _tmpDir.createTempSync('tmp-');
final File inputFile = File('${workDir.path}/base.apk'); final File inputFile = File('${workDir.path}/base.apk');
final File patchedFile = File('${workDir.path}/patched.apk'); final File patchedFile = File('${workDir.path}/patched.apk');
_outFile = File('${workDir.path}/out.apk'); outFile = File('${workDir.path}/out.apk');
final Directory cacheDir = Directory('${workDir.path}/cache'); final Directory cacheDir = Directory('${workDir.path}/cache');
cacheDir.createSync(); cacheDir.createSync();
final String originalFilePath = apkFilePath; final String originalFilePath = apkFilePath;
@@ -186,12 +167,11 @@ class PatcherAPI {
await patcherChannel.invokeMethod( await patcherChannel.invokeMethod(
'runPatcher', 'runPatcher',
{ {
'patchBundleFilePath': patchBundleFile.path,
'originalFilePath': originalFilePath, 'originalFilePath': originalFilePath,
'inputFilePath': inputFile.path, 'inputFilePath': inputFile.path,
'patchedFilePath': patchedFile.path, 'patchedFilePath': patchedFile.path,
'outFilePath': _outFile!.path, 'outFilePath': outFile!.path,
'integrationsPath': integrationsFile!.path, 'integrationsPath': integrationsFile.path,
'selectedPatches': selectedPatches.map((p) => p.name).toList(), 'selectedPatches': selectedPatches.map((p) => p.name).toList(),
'cacheDirPath': cacheDir.path, 'cacheDirPath': cacheDir.path,
'keyStoreFilePath': _keyStoreFile.path, 'keyStoreFilePath': _keyStoreFile.path,
@@ -217,7 +197,7 @@ class PatcherAPI {
} }
Future<bool> installPatchedFile(PatchedApplication patchedApp) async { Future<bool> installPatchedFile(PatchedApplication patchedApp) async {
if (_outFile != null) { if (outFile != null) {
try { try {
if (patchedApp.isRooted) { if (patchedApp.isRooted) {
final bool hasRootPermissions = await _rootAPI.hasRootPermissions(); final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
@@ -225,11 +205,11 @@ class PatcherAPI {
return _rootAPI.installApp( return _rootAPI.installApp(
patchedApp.packageName, patchedApp.packageName,
patchedApp.apkFilePath, patchedApp.apkFilePath,
_outFile!.path, outFile!.path,
); );
} }
} else { } else {
final install = await InstallPlugin.installApk(_outFile!.path); final install = await InstallPlugin.installApk(outFile!.path);
return install['isSuccess']; return install['isSuccess'];
} }
} on Exception catch (e) { } on Exception catch (e) {
@@ -244,11 +224,11 @@ class PatcherAPI {
void exportPatchedFile(String appName, String version) { void exportPatchedFile(String appName, String version) {
try { try {
if (_outFile != null) { if (outFile != null) {
final String newName = _getFileName(appName, version); final String newName = _getFileName(appName, version);
CRFileSaver.saveFileWithDialog( CRFileSaver.saveFileWithDialog(
SaveFileDialogParams( SaveFileDialogParams(
sourceFilePath: _outFile!.path, sourceFilePath: outFile!.path,
destinationFileName: newName, destinationFileName: newName,
), ),
); );
@@ -262,12 +242,12 @@ class PatcherAPI {
void sharePatchedFile(String appName, String version) { void sharePatchedFile(String appName, String version) {
try { try {
if (_outFile != null) { if (outFile != null) {
final String newName = _getFileName(appName, version); final String newName = _getFileName(appName, version);
final int lastSeparator = _outFile!.path.lastIndexOf('/'); final int lastSeparator = outFile!.path.lastIndexOf('/');
final String newPath = final String newPath =
_outFile!.path.substring(0, lastSeparator + 1) + newName; outFile!.path.substring(0, lastSeparator + 1) + newName;
final File shareFile = _outFile!.copySync(newPath); final File shareFile = outFile!.copySync(newPath);
ShareExtend.share(shareFile.path, 'file'); ShareExtend.share(shareFile.path, 'file');
} }
} on Exception catch (e) { } on Exception catch (e) {
@@ -283,7 +263,7 @@ class PatcherAPI {
return newName; return newName;
} }
Future<void> sharePatcherLog(String logs) async { Future<void> exportPatcherLog(String logs) async {
final Directory appCache = await getTemporaryDirectory(); final Directory appCache = await getTemporaryDirectory();
final Directory logDir = Directory('${appCache.path}/logs'); final Directory logDir = Directory('${appCache.path}/logs');
logDir.createSync(); logDir.createSync();
@@ -293,10 +273,15 @@ class PatcherAPI {
.replaceAll(':', '') .replaceAll(':', '')
.replaceAll('T', '') .replaceAll('T', '')
.replaceAll('.', ''); .replaceAll('.', '');
final File log = final String fileName = 'revanced-manager_patcher_$dateTime.txt';
File('${logDir.path}/revanced-manager_patcher_$dateTime.log'); final File log = File('${logDir.path}/$fileName');
log.writeAsStringSync(logs); log.writeAsStringSync(logs);
ShareExtend.share(log.path, 'file'); CRFileSaver.saveFileWithDialog(
SaveFileDialogParams(
sourceFilePath: log.path,
destinationFileName: fileName,
),
);
} }
String getSuggestedVersion(String packageName) { String getSuggestedVersion(String packageName) {

View File

@@ -7,12 +7,15 @@ import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.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:synchronized/synchronized.dart';
import 'package:timeago/timeago.dart'; import 'package:timeago/timeago.dart';
@lazySingleton @lazySingleton
class RevancedAPI { class RevancedAPI {
late Dio _dio = Dio(); late Dio _dio = Dio();
final Lock getToolsLock = Lock();
final _cacheOptions = CacheOptions( final _cacheOptions = CacheOptions(
store: MemCacheStore(), store: MemCacheStore(),
maxStale: const Duration(days: 1), maxStale: const Duration(days: 1),
@@ -66,21 +69,23 @@ class RevancedAPI {
Future<Map<String, dynamic>?> _getLatestRelease( Future<Map<String, dynamic>?> _getLatestRelease(
String extension, String extension,
String repoName, String repoName,
) async { ) {
try { return getToolsLock.synchronized(() async {
final response = await _dio.get('/tools'); try {
final List<dynamic> tools = response.data['tools']; final response = await _dio.get('/tools');
return tools.firstWhereOrNull( final List<dynamic> tools = response.data['tools'];
(t) => return tools.firstWhereOrNull(
t['repository'] == repoName && (t) =>
(t['name'] as String).endsWith(extension), t['repository'] == repoName &&
); (t['name'] as String).endsWith(extension),
} on Exception catch (e) { );
if (kDebugMode) { } on Exception catch (e) {
print(e); if (kDebugMode) {
print(e);
}
return null;
} }
return null; });
}
} }
Future<String?> getLatestReleaseVersion( Future<String?> getLatestReleaseVersion(

View File

@@ -1,12 +1,16 @@
import 'dart:ui';
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:dynamic_themes/dynamic_themes.dart'; import 'package:dynamic_themes/dynamic_themes.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.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/theme.dart'; import 'package:revanced_manager/theme.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
class DynamicThemeBuilder extends StatelessWidget { class DynamicThemeBuilder extends StatefulWidget {
const DynamicThemeBuilder({ const DynamicThemeBuilder({
Key? key, Key? key,
required this.title, required this.title,
@@ -17,6 +21,35 @@ class DynamicThemeBuilder extends StatelessWidget {
final Widget home; final Widget home;
final Iterable<LocalizationsDelegate> localizationsDelegates; final Iterable<LocalizationsDelegate> localizationsDelegates;
@override
State<DynamicThemeBuilder> createState() => _DynamicThemeBuilderState();
}
class _DynamicThemeBuilderState extends State<DynamicThemeBuilder> with WidgetsBindingObserver {
Brightness brightness = PlatformDispatcher.instance.platformBrightness;
final ManagerAPI _managerAPI = locator<ManagerAPI>();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangePlatformBrightness() {
setState(() {
brightness = PlatformDispatcher.instance.platformBrightness;
});
if (_managerAPI.getThemeMode() < 2) {
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
systemNavigationBarIconBrightness:
brightness == Brightness.light ? Brightness.dark : Brightness.light,
),
);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DynamicColorBuilder( return DynamicColorBuilder(
@@ -50,24 +83,32 @@ class DynamicThemeBuilder extends StatelessWidget {
return DynamicTheme( return DynamicTheme(
themeCollection: ThemeCollection( themeCollection: ThemeCollection(
themes: { themes: {
0: lightCustomTheme, 0: brightness == Brightness.light ? lightCustomTheme : darkCustomTheme,
1: darkCustomTheme, 1: brightness == Brightness.light ? lightDynamicTheme : darkDynamicTheme,
2: lightDynamicTheme, 2: lightCustomTheme,
3: darkDynamicTheme, 3: lightDynamicTheme,
4: darkCustomTheme,
5: darkDynamicTheme,
}, },
fallbackTheme: lightCustomTheme, fallbackTheme: PlatformDispatcher.instance.platformBrightness == Brightness.light ? lightCustomTheme : darkCustomTheme,
), ),
builder: (context, theme) => MaterialApp( builder: (context, theme) => MaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
title: title, title: widget.title,
navigatorKey: StackedService.navigatorKey, navigatorKey: StackedService.navigatorKey,
onGenerateRoute: StackedRouter().onGenerateRoute, onGenerateRoute: StackedRouter().onGenerateRoute,
theme: theme, theme: theme,
home: home, home: widget.home,
localizationsDelegates: localizationsDelegates, localizationsDelegates: widget.localizationsDelegates,
), ),
); );
}, },
); );
} }
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
} }

View File

@@ -54,7 +54,7 @@ class _AppSelectorViewState extends State<AppSelectorView> {
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
), ),
bottom: PreferredSize( bottom: PreferredSize(
preferredSize: const Size.fromHeight(64.0), preferredSize: const Size.fromHeight(66.0),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 8.0, vertical: 8.0,
@@ -94,7 +94,7 @@ class _AppSelectorViewState extends State<AppSelectorView> {
padding: const EdgeInsets.symmetric(horizontal: 12.0) padding: const EdgeInsets.symmetric(horizontal: 12.0)
.copyWith( .copyWith(
bottom: bottom:
MediaQuery.of(context).viewPadding.bottom + 8.0, MediaQuery.viewPaddingOf(context).bottom + 8.0,
), ),
child: Column( child: Column(
children: [ children: [
@@ -133,6 +133,7 @@ class _AppSelectorViewState extends State<AppSelectorView> {
), ),
) )
.toList(), .toList(),
const SizedBox(height: 70.0),
], ],
), ),
), ),

View File

@@ -73,7 +73,6 @@ 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,
@@ -90,10 +89,14 @@ class AppSelectorViewModel extends BaseViewModel {
await DeviceApps.getApp(packageName, true) as ApplicationWithIcon?; await DeviceApps.getApp(packageName, true) as ApplicationWithIcon?;
if (app != null) { if (app != null) {
if (await checkSplitApk(packageName) && !isRooted) { if (await checkSplitApk(packageName) && !isRooted) {
return showSelectFromStorageDialog(context); if (context.mounted) {
return showSelectFromStorageDialog(context);
}
} else if (!await checkSplitApk(packageName) || isRooted) { } else if (!await checkSplitApk(packageName) || isRooted) {
selectApp(app); selectApp(app);
Navigator.pop(context); if (context.mounted) {
Navigator.pop(context);
}
} }
} }
} }
@@ -198,7 +201,6 @@ 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,

View File

@@ -57,7 +57,7 @@ class ContributorsView extends StatelessWidget {
title: 'contributorsView.managerContributors', title: 'contributorsView.managerContributors',
contributors: model.managerContributors, contributors: model.managerContributors,
), ),
SizedBox(height: MediaQuery.of(context).viewPadding.bottom), SizedBox(height: MediaQuery.viewPaddingOf(context).bottom),
], ],
), ),
), ),

View File

@@ -37,7 +37,6 @@ class HomeViewModel extends BaseViewModel {
DateTime? _lastUpdate; DateTime? _lastUpdate;
bool showUpdatableApps = false; bool showUpdatableApps = false;
List<PatchedApplication> patchedInstalledApps = []; List<PatchedApplication> patchedInstalledApps = [];
List<PatchedApplication> patchedUpdatableApps = [];
String? _latestManagerVersion = ''; String? _latestManagerVersion = '';
File? downloadedApk; File? downloadedApk;
@@ -82,7 +81,7 @@ class HomeViewModel extends BaseViewModel {
_toast.showBottom('homeView.errorDownloadMessage'); _toast.showBottom('homeView.errorDownloadMessage');
} }
} }
_getPatchedApps();
_managerAPI.reAssessSavedApps().then((_) => _getPatchedApps()); _managerAPI.reAssessSavedApps().then((_) => _getPatchedApps());
} }
@@ -108,10 +107,6 @@ class HomeViewModel extends BaseViewModel {
void _getPatchedApps() { void _getPatchedApps() {
patchedInstalledApps = _managerAPI.getPatchedApps().toList(); patchedInstalledApps = _managerAPI.getPatchedApps().toList();
patchedUpdatableApps = _managerAPI
.getPatchedApps()
.where((app) => app.hasUpdates == true)
.toList();
notifyListeners(); notifyListeners();
} }
@@ -256,9 +251,9 @@ class HomeViewModel extends BaseViewModel {
final String integrationsVersion = final String integrationsVersion =
await _managerAPI.getLatestIntegrationsVersion() ?? '0.0.0'; await _managerAPI.getLatestIntegrationsVersion() ?? '0.0.0';
if (patchesVersion != '0.0.0' && integrationsVersion != '0.0.0') { if (patchesVersion != '0.0.0' && integrationsVersion != '0.0.0') {
_toast.showBottom('homeView.downloadedMessage');
await _managerAPI.setCurrentPatchesVersion(patchesVersion); await _managerAPI.setCurrentPatchesVersion(patchesVersion);
await _managerAPI.setCurrentIntegrationsVersion(integrationsVersion); await _managerAPI.setCurrentIntegrationsVersion(integrationsVersion);
_toast.showBottom('homeView.downloadedMessage');
forceRefresh(context); forceRefresh(context);
} else { } else {
_toast.showBottom('homeView.errorDownloadMessage'); _toast.showBottom('homeView.errorDownloadMessage');
@@ -469,11 +464,7 @@ class HomeViewModel extends BaseViewModel {
} }
Future<void> forceRefresh(BuildContext context) async { Future<void> forceRefresh(BuildContext context) async {
await Future.delayed(const Duration(seconds: 1)); _managerAPI.clearAllData();
if (_lastUpdate == null ||
_lastUpdate!.difference(DateTime.now()).inSeconds > 2) {
_managerAPI.clearAllData();
}
_toast.showBottom('homeView.refreshSuccess'); _toast.showBottom('homeView.refreshSuccess');
initialize(context); initialize(context);
} }

View File

@@ -4,8 +4,6 @@ import 'package:google_fonts/google_fonts.dart';
import 'package:revanced_manager/ui/views/installer/installer_viewmodel.dart'; import 'package:revanced_manager/ui/views/installer/installer_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/installerView/gradient_progress_indicator.dart'; import 'package:revanced_manager/ui/widgets/installerView/gradient_progress_indicator.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_popup_menu.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';
@@ -22,6 +20,57 @@ class InstallerView extends StatelessWidget {
top: false, top: false,
bottom: false, bottom: false,
child: Scaffold( child: Scaffold(
floatingActionButton: Visibility(
visible: !model.isPatching && !model.hasErrors,
child: FloatingActionButton.extended(
label: I18nText(
model.isInstalled
? 'installerView.openButton'
: 'installerView.installButton',
),
icon: model.isInstalled
? const Icon(Icons.open_in_new)
: const Icon(Icons.file_download_outlined),
onPressed: model.isInstalled
? () => {
model.openApp(),
model.cleanPatcher(),
Navigator.of(context).pop(),
}
: () => model.installTypeDialog(context),
elevation: 0,
),
),
floatingActionButtonLocation:
FloatingActionButtonLocation.endContained,
bottomNavigationBar: Visibility(
visible: !model.isPatching,
child: BottomAppBar(
child: Row(
children: <Widget>[
Visibility(
visible: !model.hasErrors,
child: IconButton.filledTonal(
tooltip: FlutterI18n.translate(
context,
'installerView.exportApkButtonTooltip',
),
icon: const Icon(Icons.save),
onPressed: () => model.onButtonPressed(0),
),
),
IconButton.filledTonal(
tooltip: FlutterI18n.translate(
context,
'installerView.exportLogButtonTooltip',
),
icon: const Icon(Icons.post_add),
onPressed: () => model.onButtonPressed(1),
),
],
),
),
),
body: CustomScrollView( body: CustomScrollView(
controller: model.scrollController, controller: model.scrollController,
slivers: <Widget>[ slivers: <Widget>[
@@ -35,44 +84,6 @@ class InstallerView extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
onBackButtonPressed: () => model.onWillPop(context), onBackButtonPressed: () => model.onWillPop(context),
actions: <Widget>[
Visibility(
visible: !model.isPatching,
child: CustomPopupMenu(
onSelected: (value) => model.onMenuSelection(value),
children: {
if (!model.hasErrors)
0: I18nText(
'installerView.shareApkMenuOption',
child: const Text(
'',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
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( bottom: PreferredSize(
preferredSize: const Size(double.infinity, 1.0), preferredSize: const Size(double.infinity, 1.0),
child: GradientProgressIndicator(progress: model.progress), child: GradientProgressIndicator(progress: model.progress),
@@ -96,72 +107,6 @@ class InstallerView extends StatelessWidget {
), ),
), ),
), ),
SliverFillRemaining(
hasScrollBody: false,
child: Align(
alignment: Alignment.bottomCenter,
child: Visibility(
visible: !model.isPatching && !model.hasErrors,
child: Padding(
padding: const EdgeInsets.all(20.0).copyWith(top: 0.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Visibility(
visible: model.isInstalled,
child: CustomMaterialButton(
label: I18nText('installerView.openButton'),
isExpanded: true,
onPressed: () {
model.openApp();
model.cleanPatcher();
Navigator.of(context).pop();
},
),
),
Visibility(
visible: !model.isInstalled && model.isRooted,
child: CustomMaterialButton(
isFilled: false,
label:
I18nText('installerView.installRootButton'),
isExpanded: true,
onPressed: () => model.installResult(
context,
true,
),
),
),
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,
),
),
),
],
),
),
),
),
),
SliverFillRemaining(
hasScrollBody: false,
child: SizedBox(
height: MediaQuery.of(context).viewPadding.bottom,
),
),
], ],
), ),
), ),

View File

@@ -15,6 +15,8 @@ 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:revanced_manager/utils/about_info.dart';
import 'package:screenshot_callback/screenshot_callback.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:wakelock/wakelock.dart'; import 'package:wakelock/wakelock.dart';
@@ -29,6 +31,7 @@ class InstallerViewModel extends BaseViewModel {
'app.revanced.manager.flutter/installer', 'app.revanced.manager.flutter/installer',
); );
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
final ScreenshotCallback screenshotCallback = ScreenshotCallback();
double? progress = 0.0; double? progress = 0.0;
String logs = ''; String logs = '';
String headerLogs = ''; String headerLogs = '';
@@ -38,6 +41,7 @@ class InstallerViewModel extends BaseViewModel {
bool hasErrors = false; bool hasErrors = false;
bool isCanceled = false; bool isCanceled = false;
bool cancel = false; bool cancel = false;
bool showPopupScreenshotWarning = true;
Future<void> initialize(BuildContext context) async { Future<void> initialize(BuildContext context) async {
isRooted = await _rootAPI.isRooted(); isRooted = await _rootAPI.isRooted();
@@ -64,6 +68,12 @@ class InstallerViewModel extends BaseViewModel {
} // ignore } // ignore
} }
} }
screenshotCallback.addListener(() {
if (showPopupScreenshotWarning) {
showPopupScreenshotWarning = false;
screenshotDetected(context);
}
});
await Wakelock.enable(); await Wakelock.enable();
await handlePlatformChannelMethods(); await handlePlatformChannelMethods();
await runPatcher(); await runPatcher();
@@ -130,28 +140,28 @@ class InstallerViewModel extends BaseViewModel {
Future<void> runPatcher() async { Future<void> runPatcher() async {
try { try {
update(0.0, 'Initializing...', 'Initializing installer'); await _patcherAPI.runPatcher(
if (_patches.isNotEmpty) { _app.packageName,
try { _app.apkFilePath,
update(0.1, '', 'Creating working directory'); _patches,
await _patcherAPI.runPatcher( );
_app.packageName, } on Exception catch (e) {
_app.apkFilePath, update(
_patches, -100.0,
); 'Failed...',
} on Exception catch (e) { 'Something went wrong:\n$e',
update( );
-100.0, if (kDebugMode) {
'Aborted...', print(e);
'An error occurred! Aborted\nError:\n$e',
);
if (kDebugMode) {
print(e);
}
}
} else {
update(-100.0, 'Aborted...', 'No app or patches selected! Aborted');
} }
}
// Necessary to reset the state of patches by reloading them
// in a later patching process.
_managerAPI.patches.clear();
await _patcherAPI.loadPatches();
try {
if (FlutterBackground.isBackgroundExecutionEnabled) { if (FlutterBackground.isBackgroundExecutionEnabled) {
try { try {
FlutterBackground.disableBackgroundExecution(); FlutterBackground.disableBackgroundExecution();
@@ -169,12 +179,164 @@ class InstallerViewModel extends BaseViewModel {
} }
} }
Future<void> copyLogs() async {
final info = await AboutInfo.getInfo();
final formattedLogs = [
'```',
'~ Device Info',
'ReVanced Manager: ${info['version']}',
'Build: ${info['flavor']}',
'Model: ${info['model']}',
'Android version: ${info['androidVersion']}',
'Supported architectures: ${info['supportedArch'].join(", ")}',
'\n~ Patch Info',
'App: ${_app.packageName} v${_app.version}',
'Patches version: ${_managerAPI.patchesVersion}',
'Patches: ${_patches.map((p) => p.name).toList().join(", ")}',
'\n~ Settings',
'Enabled changing patches: ${_managerAPI.isPatchesChangeEnabled()}',
'Enabled universal patches: ${_managerAPI.areUniversalPatchesEnabled()}',
'Enabled experimental patches: ${_managerAPI.areExperimentalPatchesEnabled()}',
'Patches source: ${_managerAPI.getPatchesRepo()}',
'Integration source: ${_managerAPI.getIntegrationsRepo()}',
'\n~ Logs',
logs,
'```',
];
Clipboard.setData(ClipboardData(text: formattedLogs.join('\n')));
_toast.showBottom('installerView.copiedToClipboard');
}
Future<void> screenshotDetected(BuildContext context) async {
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText(
'warning',
),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
icon: const Icon(Icons.warning),
content: SingleChildScrollView(
child: I18nText('installerView.screenshotDetected'),
),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () {
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('yesButton'),
onPressed: () {
copyLogs();
showPopupScreenshotWarning = true;
Navigator.of(context).pop();
},
),
],
),
);
}
Future<void> installTypeDialog(BuildContext context) async {
final ValueNotifier<int> installType = ValueNotifier(0);
if (isRooted) {
await showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: I18nText(
'installerView.installType',
),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
icon: const Icon(Icons.file_download_outlined),
contentPadding: const EdgeInsets.symmetric(vertical: 16),
content: SingleChildScrollView(
child: ValueListenableBuilder(
valueListenable: installType,
builder: (context, value, child) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 10,
),
child: I18nText(
'installerView.installTypeDescription',
child: Text(
'',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.secondary,
),
),
),
),
RadioListTile(
title: I18nText('installerView.installNonRootType'),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16),
value: 0,
groupValue: value,
onChanged: (selected) {
installType.value = selected!;
},
),
RadioListTile(
title: I18nText('installerView.installRootType'),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16),
value: 1,
groupValue: value,
onChanged: (selected) {
installType.value = selected!;
},
),
],
);
},
),
),
actions: [
CustomMaterialButton(
label: I18nText('cancelButton'),
isFilled: false,
onPressed: () {
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('installerView.installButton'),
onPressed: () {
Navigator.of(context).pop();
installResult(context, installType.value == 1);
},
),
],
),
);
} else {
installResult(context, false);
}
}
Future<void> stopPatcher() async { Future<void> stopPatcher() async {
try { try {
isCanceled = true; isCanceled = true;
update(0.5, 'Aborting...', 'Canceling patching process'); update(0.5, 'Canceling...', 'Canceling patching process');
await _patcherAPI.stopPatcher(); await _patcherAPI.stopPatcher();
update(-100.0, 'Aborted...', 'Press back to exit'); update(-100.0, 'Canceled...', 'Press back to exit');
} on Exception catch (e) { } on Exception catch (e) {
if (kDebugMode) { if (kDebugMode) {
print(e); print(e);
@@ -185,56 +347,33 @@ class InstallerViewModel extends BaseViewModel {
Future<void> installResult(BuildContext context, bool installAsRoot) async { Future<void> installResult(BuildContext context, bool installAsRoot) async {
try { try {
_app.isRooted = installAsRoot; _app.isRooted = installAsRoot;
final bool hasMicroG = update(
_patches.any((p) => p.name.endsWith('MicroG support')); 1.0,
final bool rootMicroG = installAsRoot && hasMicroG; 'Installing...',
final bool rootFromStorage = installAsRoot && _app.isFromStorage; _app.isRooted
final bool ytWithoutRootMicroG = ? 'Installing patched file using root method'
!installAsRoot && !hasMicroG && _app.packageName.contains('youtube'); : 'Installing patched file using nonroot method',
if (rootMicroG || rootFromStorage || ytWithoutRootMicroG) { );
return showDialog( isInstalled = await _patcherAPI.installPatchedFile(_app);
context: context, if (isInstalled) {
builder: (context) => AlertDialog( _app.isFromStorage = false;
title: I18nText('installerView.installErrorDialogTitle'), _app.patchDate = DateTime.now();
backgroundColor: Theme.of(context).colorScheme.secondaryContainer, _app.appliedPatches = _patches.map((p) => p.name).toList();
content: I18nText(
rootMicroG // In case a patch changed the app name or package name,
? 'installerView.installErrorDialogText1' // update the app info.
: rootFromStorage final app =
? 'installerView.installErrorDialogText3' await DeviceApps.getAppFromStorage(_patcherAPI.outFile!.path);
: 'installerView.installErrorDialogText2', if (app != null) {
), _app.name = app.appName;
actions: <Widget>[ _app.packageName = app.packageName;
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () => Navigator.of(context).pop(),
),
],
),
);
} else {
update(
1.0,
'Installing...',
_app.isRooted
? 'Installing patched file using root method'
: 'Installing patched file using nonroot method',
);
isInstalled = await _patcherAPI.installPatchedFile(_app);
if (isInstalled) {
update(1.0, 'Installed!', 'Installed!');
_app.isFromStorage = false;
_app.patchDate = DateTime.now();
_app.appliedPatches = _patches.map((p) => p.name).toList();
if (hasMicroG) {
_app.name += ' ReVanced';
_app.packageName = _app.packageName.replaceFirst(
'com.google.',
'app.revanced.',
);
}
await _managerAPI.savePatchedApp(_app);
} }
await _managerAPI.savePatchedApp(_app);
update(1.0, 'Installed!', 'Installed!');
} else {
// TODO(aabed): Show error message.
} }
} on Exception catch (e) { } on Exception catch (e) {
if (kDebugMode) { if (kDebugMode) {
@@ -253,20 +392,6 @@ class InstallerViewModel extends BaseViewModel {
} }
} }
void shareResult() {
try {
_patcherAPI.sharePatchedFile(_app.name, _app.version);
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
void shareLog() {
_patcherAPI.sharePatcherLog(logs);
}
Future<void> cleanPatcher() async { Future<void> cleanPatcher() async {
try { try {
_patcherAPI.cleanPatcher(); _patcherAPI.cleanPatcher();
@@ -284,16 +409,13 @@ class InstallerViewModel extends BaseViewModel {
DeviceApps.openApp(_app.packageName); DeviceApps.openApp(_app.packageName);
} }
void onMenuSelection(int value) { void onButtonPressed(int value) {
switch (value) { switch (value) {
case 0: case 0:
shareResult();
break;
case 1:
exportResult(); exportResult();
break; break;
case 2: case 1:
shareLog(); copyLogs();
break; break;
} }
} }
@@ -315,6 +437,7 @@ class InstallerViewModel extends BaseViewModel {
} else { } else {
_patcherAPI.cleanPatcher(); _patcherAPI.cleanPatcher();
} }
screenshotCallback.dispose();
Navigator.of(context).pop(); Navigator.of(context).pop();
return true; return true;
} }

View File

@@ -1,4 +1,5 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'package:device_info_plus/device_info_plus.dart';
import 'package:dynamic_themes/dynamic_themes.dart'; import 'package:dynamic_themes/dynamic_themes.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@@ -18,25 +19,35 @@ class NavigationViewModel extends IndexTrackingViewModel {
Future<void> initialize(BuildContext context) async { Future<void> initialize(BuildContext context) async {
locator<Toast>().initialize(context); locator<Toast>().initialize(context);
final SharedPreferences prefs = await SharedPreferences.getInstance(); final SharedPreferences prefs = await SharedPreferences.getInstance();
requestManageExternalStorage(); await requestManageExternalStorage();
if (prefs.getBool('permissionsRequested') == null) { if (prefs.getBool('permissionsRequested') == null) {
await Permission.storage.request(); await Permission.storage.request();
await Permission.manageExternalStorage.request();
await prefs.setBool('permissionsRequested', true); await prefs.setBool('permissionsRequested', true);
RootAPI().hasRootPermissions().then( await RootAPI().hasRootPermissions().then(
(value) => Permission.requestInstallPackages.request().then( (value) => Permission.requestInstallPackages.request().then(
(value) => Permission.ignoreBatteryOptimizations.request(), (value) => Permission.ignoreBatteryOptimizations.request(),
), ),
); );
} }
if (prefs.getBool('useDarkTheme') == null) { final dynamicTheme = DynamicTheme.of(context)!;
final bool isDark = if (prefs.getInt('themeMode') == null) {
MediaQuery.of(context).platformBrightness != Brightness.light; await prefs.setInt('themeMode', 0);
await prefs.setBool('useDarkTheme', isDark); await dynamicTheme.setTheme(0);
await DynamicTheme.of(context)!.setTheme(isDark ? 1 : 0);
} }
// Force disable Material You on Android 11 and below
if (dynamicTheme.themeId.isOdd) {
const int android12SdkVersion = 31;
final AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo;
if (info.version.sdkInt < android12SdkVersion) {
await prefs.setInt('themeMode', 0);
await prefs.setBool('useDynamicTheme', false);
await dynamicTheme.setTheme(0);
}
}
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle( SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle( SystemUiOverlayStyle(

View File

@@ -44,49 +44,6 @@ class PatcherViewModel extends BaseViewModel {
return selectedApp == null; return selectedApp == null;
} }
Future<bool> isValidPatchConfig() async {
final bool needsResourcePatching = await _patcherAPI.needsResourcePatching(
selectedPatches,
);
if (needsResourcePatching && selectedApp != null) {
final bool isSplit = await _managerAPI.isSplitApk(selectedApp!);
return !isSplit;
}
return true;
}
Future<void> showPatchConfirmationDialog(BuildContext context) async {
final bool isValid = await isValidPatchConfig();
if (context.mounted) {
if (isValid) {
showArmv7WarningDialog(context);
} else {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('warning'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('patcherView.splitApkWarningDialogText'),
actions: <Widget>[
CustomMaterialButton(
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(),
),
CustomMaterialButton(
label: I18nText('yesButton'),
isFilled: false,
onPressed: () {
Navigator.of(context).pop();
showArmv7WarningDialog(context);
},
),
],
),
);
}
}
}
Future<void> showRemovedPatchesDialog(BuildContext context) async { Future<void> showRemovedPatchesDialog(BuildContext context) async {
if (removedPatches.isNotEmpty) { if (removedPatches.isNotEmpty) {
return showDialog( return showDialog(
@@ -115,7 +72,7 @@ class PatcherViewModel extends BaseViewModel {
), ),
); );
} else { } else {
showArmv7WarningDialog(context); showArmv7WarningDialog(context); // TODO(aabed): Find out why this is here
} }
} }
@@ -185,12 +142,16 @@ class PatcherViewModel extends BaseViewModel {
this.selectedPatches.clear(); this.selectedPatches.clear();
removedPatches.clear(); removedPatches.clear();
final List<String> selectedPatches = final List<String> selectedPatches =
await _managerAPI.getSelectedPatches(selectedApp!.originalPackageName); await _managerAPI.getSelectedPatches(selectedApp!.packageName);
final List<Patch> patches = final List<Patch> patches =
_patcherAPI.getFilteredPatches(selectedApp!.originalPackageName); _patcherAPI.getFilteredPatches(selectedApp!.packageName);
this this
.selectedPatches .selectedPatches
.addAll(patches.where((patch) => selectedPatches.contains(patch.name))); .addAll(patches.where((patch) => selectedPatches.contains(patch.name)));
if (!_managerAPI.isPatchesChangeEnabled()) {
this.selectedPatches.clear();
this.selectedPatches.addAll(patches.where((patch) => !patch.excluded));
}
if (!_managerAPI.areExperimentalPatchesEnabled()) { if (!_managerAPI.areExperimentalPatchesEnabled()) {
this.selectedPatches.removeWhere((patch) => !isPatchSupported(patch)); this.selectedPatches.removeWhere((patch) => !isPatchSupported(patch));
} }
@@ -199,7 +160,7 @@ class PatcherViewModel extends BaseViewModel {
.selectedPatches .selectedPatches
.removeWhere((patch) => patch.compatiblePackages.isEmpty); .removeWhere((patch) => patch.compatiblePackages.isEmpty);
} }
final usedPatches = _managerAPI.getUsedPatches(selectedApp!.originalPackageName); final usedPatches = _managerAPI.getUsedPatches(selectedApp!.packageName);
for (final patch in usedPatches){ for (final patch in usedPatches){
if (!patches.any((p) => p.name == patch.name)){ if (!patches.any((p) => p.name == patch.name)){
removedPatches.add('\u2022 ${patch.name}'); removedPatches.add('\u2022 ${patch.name}');

View File

@@ -20,6 +20,17 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
String _query = ''; String _query = '';
final _managerAPI = locator<ManagerAPI>(); final _managerAPI = locator<ManagerAPI>();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (!_managerAPI.isPatchesChangeEnabled() &&
_managerAPI.showPatchesChangeWarning()) {
_managerAPI.showPatchesChangeWarningDialog(context);
}
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ViewModelBuilder<PatchesSelectorViewModel>.reactive( return ViewModelBuilder<PatchesSelectorViewModel>.reactive(
@@ -70,7 +81,7 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
child: Container( child: Container(
margin: const EdgeInsets.only(top: 12, bottom: 12), margin: const EdgeInsets.only(top: 12, bottom: 12),
padding: padding:
const EdgeInsets.symmetric(horizontal: 6, vertical: 6), const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
@@ -87,7 +98,8 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
), ),
), ),
CustomPopupMenu( CustomPopupMenu(
onSelected: (value) => {model.onMenuSelection(value)}, onSelected: (value) =>
{model.onMenuSelection(value, context)},
children: { children: {
0: I18nText( 0: I18nText(
'patchesSelectorView.loadPatchesSelection', 'patchesSelectorView.loadPatchesSelection',
@@ -102,7 +114,7 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
), ),
], ],
bottom: PreferredSize( bottom: PreferredSize(
preferredSize: const Size.fromHeight(64.0), preferredSize: const Size.fromHeight(66.0),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 8.0, vertical: 8.0,
@@ -139,7 +151,7 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
: Padding( : Padding(
padding: padding:
const EdgeInsets.symmetric(horizontal: 12.0).copyWith( const EdgeInsets.symmetric(horizontal: 12.0).copyWith(
bottom: MediaQuery.of(context).viewPadding.bottom + 8.0, bottom: MediaQuery.viewPaddingOf(context).bottom + 8.0,
), ),
child: Column( child: Column(
children: [ children: [
@@ -152,7 +164,11 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
'patchesSelectorView.defaultTooltip', 'patchesSelectorView.defaultTooltip',
), ),
onPressed: () { onPressed: () {
model.selectDefaultPatches(); if (_managerAPI.isPatchesChangeEnabled()) {
model.selectDefaultPatches();
} else {
model.showPatchesChangeDialog(context);
}
}, },
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
@@ -163,7 +179,11 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
'patchesSelectorView.noneTooltip', 'patchesSelectorView.noneTooltip',
), ),
onPressed: () { onPressed: () {
model.clearPatches(); if (_managerAPI.isPatchesChangeEnabled()) {
model.clearPatches();
} else {
model.showPatchesChangeDialog(context);
}
}, },
), ),
], ],
@@ -174,18 +194,20 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
return PatchItem( return PatchItem(
name: patch.name, name: patch.name,
simpleName: patch.getSimpleName(), simpleName: patch.getSimpleName(),
description: patch.description, description: patch.description ?? '',
packageVersion: model.getAppInfo().version, packageVersion: model.getAppInfo().version,
supportedPackageVersions: supportedPackageVersions:
model.getSupportedVersions(patch), model.getSupportedVersions(patch),
isUnsupported: !isPatchSupported(patch), isUnsupported: !isPatchSupported(patch),
isChangeEnabled:
_managerAPI.isPatchesChangeEnabled(),
isNew: model.isPatchNew( isNew: model.isPatchNew(
patch, patch,
model.getAppInfo().packageName, model.getAppInfo().packageName,
), ),
isSelected: model.isSelected(patch), isSelected: model.isSelected(patch),
onChanged: (value) => onChanged: (value) =>
model.selectPatch(patch, value), model.selectPatch(patch, value, context),
); );
} else { } else {
return Container(); return Container();
@@ -200,8 +222,23 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 10.0, vertical: 10.0,
), ),
child: I18nText( child: Container(
'patchesSelectorView.universalPatches', padding: const EdgeInsets.only(
top: 10.0,
bottom: 10.0,
left: 5.0,
),
child: I18nText(
'patchesSelectorView.universalPatches',
child: Text(
'',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
), ),
), ),
...model.getQueriedPatches(_query).map((patch) { ...model.getQueriedPatches(_query).map((patch) {
@@ -209,16 +246,21 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
return PatchItem( return PatchItem(
name: patch.name, name: patch.name,
simpleName: patch.getSimpleName(), simpleName: patch.getSimpleName(),
description: patch.description, description: patch.description ?? '',
packageVersion: packageVersion:
model.getAppInfo().version, model.getAppInfo().version,
supportedPackageVersions: supportedPackageVersions:
model.getSupportedVersions(patch), model.getSupportedVersions(patch),
isUnsupported: !isPatchSupported(patch), isUnsupported: !isPatchSupported(patch),
isChangeEnabled:
_managerAPI.isPatchesChangeEnabled(),
isNew: false, isNew: false,
isSelected: model.isSelected(patch), isSelected: model.isSelected(patch),
onChanged: (value) => onChanged: (value) => model.selectPatch(
model.selectPatch(patch, value), patch,
value,
context,
),
); );
} else { } else {
return Container(); return Container();

View File

@@ -1,4 +1,6 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
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';
@@ -6,6 +8,7 @@ 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/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/utils/check_for_supported_patch.dart'; import 'package:revanced_manager/utils/check_for_supported_patch.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
@@ -25,7 +28,7 @@ class PatchesSelectorViewModel extends BaseViewModel {
getPatchesVersion().whenComplete(() => notifyListeners()); getPatchesVersion().whenComplete(() => notifyListeners());
patches.addAll( patches.addAll(
_patcherAPI.getFilteredPatches( _patcherAPI.getFilteredPatches(
selectedApp!.originalPackageName, selectedApp!.packageName,
), ),
); );
patches.sort((a, b) { patches.sort((a, b) {
@@ -45,31 +48,70 @@ class PatchesSelectorViewModel extends BaseViewModel {
); );
} }
void selectPatch(Patch patch, bool isSelected) { void selectPatch(Patch patch, bool isSelected, BuildContext context) {
if (isSelected && !selectedPatches.contains(patch)) { if (_managerAPI.isPatchesChangeEnabled()) {
selectedPatches.add(patch); if (isSelected && !selectedPatches.contains(patch)) {
selectedPatches.add(patch);
} else {
selectedPatches.remove(patch);
}
notifyListeners();
} else { } else {
selectedPatches.remove(patch); showPatchesChangeDialog(context);
} }
notifyListeners(); }
Future<void> showPatchesChangeDialog(BuildContext context) async {
return showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
title: I18nText('warning'),
content: I18nText(
'patchItem.patchesChangeWarningDialogText',
child: const Text(
'',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
actions: [
CustomMaterialButton(
isFilled: false,
label: I18nText('okButton'),
onPressed: () => Navigator.of(context).pop(),
),
CustomMaterialButton(
label: I18nText('patchItem.patchesChangeWarningDialogButton'),
onPressed: () {
Navigator.of(context)
..pop()
..pop();
},
),
],
),
);
} }
void selectDefaultPatches() { void selectDefaultPatches() {
selectedPatches.clear(); selectedPatches.clear();
if (locator<PatcherViewModel>().selectedApp?.packageName != null) {
if (_managerAPI.areExperimentalPatchesEnabled() == false) {
selectedPatches.addAll( selectedPatches.addAll(
patches.where( _patcherAPI
(element) => element.excluded == false && isPatchSupported(element), .getFilteredPatches(
), locator<PatcherViewModel>().selectedApp!.packageName,
)
.where(
(element) =>
!element.excluded &&
(_managerAPI.areExperimentalPatchesEnabled() ||
isPatchSupported(element)),
),
); );
} }
if (_managerAPI.areExperimentalPatchesEnabled()) {
selectedPatches
.addAll(patches.where((element) => element.excluded == false));
}
notifyListeners(); notifyListeners();
} }
@@ -133,10 +175,10 @@ class PatchesSelectorViewModel extends BaseViewModel {
} }
} }
void onMenuSelection(value) { void onMenuSelection(value, BuildContext context) {
switch (value) { switch (value) {
case 0: case 0:
loadSelectedPatches(); loadSelectedPatches(context);
break; break;
} }
} }
@@ -145,23 +187,30 @@ class PatchesSelectorViewModel extends BaseViewModel {
final List<String> selectedPatches = final List<String> selectedPatches =
this.selectedPatches.map((patch) => patch.name).toList(); this.selectedPatches.map((patch) => patch.name).toList();
await _managerAPI.setSelectedPatches( await _managerAPI.setSelectedPatches(
locator<PatcherViewModel>().selectedApp!.originalPackageName, locator<PatcherViewModel>().selectedApp!.packageName,
selectedPatches, selectedPatches,
); );
} }
Future<void> loadSelectedPatches() async { Future<void> loadSelectedPatches(BuildContext context) async {
final List<String> selectedPatches = await _managerAPI.getSelectedPatches( if (_managerAPI.isPatchesChangeEnabled()) {
locator<PatcherViewModel>().selectedApp!.originalPackageName, final List<String> selectedPatches = await _managerAPI.getSelectedPatches(
); locator<PatcherViewModel>().selectedApp!.packageName,
if (selectedPatches.isNotEmpty) { );
this.selectedPatches.clear(); if (selectedPatches.isNotEmpty) {
this.selectedPatches.addAll( this.selectedPatches.clear();
patches.where((patch) => selectedPatches.contains(patch.name)), this.selectedPatches.addAll(
); patches.where((patch) => selectedPatches.contains(patch.name)),
);
if (!_managerAPI.areExperimentalPatchesEnabled()) {
this.selectedPatches.removeWhere((patch) => !isPatchSupported(patch));
}
} else {
locator<Toast>().showBottom('patchesSelectorView.noSavedPatches');
}
notifyListeners();
} else { } else {
locator<Toast>().showBottom('patchesSelectorView.noSavedPatches'); showPatchesChangeDialog(context);
} }
notifyListeners();
} }
} }

View File

@@ -129,6 +129,7 @@ class SManageSources extends BaseViewModel {
'${_orgIntSourceController.text.trim()}/${_intSourceController.text.trim()}', '${_orgIntSourceController.text.trim()}/${_intSourceController.text.trim()}',
); );
_managerAPI.setCurrentPatchesVersion('0.0.0'); _managerAPI.setCurrentPatchesVersion('0.0.0');
_managerAPI.setCurrentIntegrationsVersion('0.0.0');
_toast.showBottom('settingsView.restartAppForChanges'); _toast.showBottom('settingsView.restartAppForChanges');
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
@@ -158,6 +159,7 @@ class SManageSources extends BaseViewModel {
_managerAPI.setPatchesRepo(''); _managerAPI.setPatchesRepo('');
_managerAPI.setIntegrationsRepo(''); _managerAPI.setIntegrationsRepo('');
_managerAPI.setCurrentPatchesVersion('0.0.0'); _managerAPI.setCurrentPatchesVersion('0.0.0');
_managerAPI.setCurrentIntegrationsVersion('0.0.0');
_toast.showBottom('settingsView.restartAppForChanges'); _toast.showBottom('settingsView.restartAppForChanges');
Navigator.of(context) Navigator.of(context)
..pop() ..pop()

View File

@@ -8,6 +8,7 @@ import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.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/settings_section.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
final _settingViewModel = SettingsViewModel(); final _settingViewModel = SettingsViewModel();
@@ -24,37 +25,114 @@ class SUpdateTheme extends BaseViewModel {
Future<void> setUseDynamicTheme(BuildContext context, bool value) async { Future<void> setUseDynamicTheme(BuildContext context, bool value) async {
await _managerAPI.setUseDynamicTheme(value); await _managerAPI.setUseDynamicTheme(value);
final int currentTheme = DynamicTheme.of(context)!.themeId; final int currentTheme = (DynamicTheme.of(context)!.themeId ~/ 2) * 2;
if (currentTheme.isEven) { await DynamicTheme.of(context)!.setTheme(currentTheme + (value ? 1 : 0));
await DynamicTheme.of(context)!.setTheme(value ? 2 : 0);
} else {
await DynamicTheme.of(context)!.setTheme(value ? 3 : 1);
}
notifyListeners(); notifyListeners();
} }
bool getDarkThemeStatus() { int getThemeMode() {
return _managerAPI.getUseDarkTheme(); return _managerAPI.getThemeMode();
} }
Future<void> setUseDarkTheme(BuildContext context, bool value) async { Future<void> setThemeMode(BuildContext context, int value) async {
await _managerAPI.setUseDarkTheme(value); await _managerAPI.setThemeMode(value);
final int currentTheme = DynamicTheme.of(context)!.themeId; final bool isDynamicTheme = DynamicTheme.of(context)!.themeId.isEven;
if (currentTheme < 2) { await DynamicTheme.of(context)!.setTheme(value * 2 + (isDynamicTheme ? 0 : 1));
await DynamicTheme.of(context)!.setTheme(value ? 1 : 0); final bool isLight = value != 2 && (value == 1 || DynamicTheme.of(context)!.theme.brightness == Brightness.light);
} else {
await DynamicTheme.of(context)!.setTheme(value ? 3 : 2);
}
SystemChrome.setSystemUIOverlayStyle( SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle( SystemUiOverlayStyle(
systemNavigationBarIconBrightness: systemNavigationBarIconBrightness:
value ? Brightness.light : Brightness.dark, isLight ? Brightness.dark : Brightness.light,
), ),
); );
notifyListeners(); notifyListeners();
} }
I18nText getThemeModeName() {
switch (getThemeMode()) {
case 0:
return I18nText('settingsView.systemThemeLabel');
case 1:
return I18nText('settingsView.lightThemeLabel');
case 2:
return I18nText('settingsView.darkThemeLabel');
default:
return I18nText('settingsView.systemThemeLabel');
}
}
Future<void> showThemeDialog(BuildContext context) async {
final ValueNotifier<int> newTheme = ValueNotifier(getThemeMode());
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('settingsView.themeModeLabel'),
icon: const Icon(Icons.palette),
contentPadding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: SingleChildScrollView(
child: ValueListenableBuilder(
valueListenable: newTheme,
builder: (context, value, child) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
RadioListTile(
title: I18nText('settingsView.systemThemeLabel'),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
value: 0,
groupValue: value,
onChanged: (value) {
newTheme.value = value!;
},
),
RadioListTile(
title: I18nText('settingsView.lightThemeLabel'),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
value: 1,
groupValue: value,
onChanged: (value) {
newTheme.value = value!;
},
),
RadioListTile(
title: I18nText('settingsView.darkThemeLabel'),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
value: 2,
groupValue: value,
onChanged: (value) {
newTheme.value = value!;
},
),
],
);
},
),
),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('cancelButton'),
onPressed: () {
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () {
setThemeMode(context, newTheme.value);
Navigator.of(context).pop();
},
),
],
),
);
}
} }
final sUpdateTheme = SUpdateTheme();
class SUpdateThemeUI extends StatelessWidget { class SUpdateThemeUI extends StatelessWidget {
const SUpdateThemeUI({super.key}); const SUpdateThemeUI({super.key});
@@ -63,10 +141,10 @@ class SUpdateThemeUI extends StatelessWidget {
return SettingsSection( return SettingsSection(
title: 'settingsView.appearanceSectionTitle', title: 'settingsView.appearanceSectionTitle',
children: <Widget>[ children: <Widget>[
SwitchListTile( ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText( title: I18nText(
'settingsView.darkThemeLabel', 'settingsView.themeModeLabel',
child: const Text( child: const Text(
'', '',
style: TextStyle( style: TextStyle(
@@ -75,12 +153,11 @@ class SUpdateThemeUI extends StatelessWidget {
), ),
), ),
), ),
subtitle: I18nText('settingsView.darkThemeHint'), trailing: CustomMaterialButton(
value: SUpdateTheme().getDarkThemeStatus(), label: sUpdateTheme.getThemeModeName(),
onChanged: (value) => SUpdateTheme().setUseDarkTheme( onPressed: () => { sUpdateTheme.showThemeDialog(context) },
context,
value,
), ),
onTap: () => { sUpdateTheme.showThemeDialog(context) },
), ),
FutureBuilder<int>( FutureBuilder<int>(
future: _settingViewModel.getSdkVersion(), future: _settingViewModel.getSdkVersion(),

View File

@@ -3,6 +3,8 @@ 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:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.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';
@@ -10,8 +12,10 @@ 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/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/views/patches_selector/patches_selector_viewmodel.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_update_language.dart'; import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_update_language.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_update_theme.dart'; import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_update_theme.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.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';
@@ -19,6 +23,9 @@ import 'package:stacked_services/stacked_services.dart';
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 PatchesSelectorViewModel _patchesSelectorViewModel =
PatchesSelectorViewModel();
final PatcherViewModel _patcherViewModel = locator<PatcherViewModel>();
final Toast _toast = locator<Toast>(); final Toast _toast = locator<Toast>();
final SUpdateLanguage sUpdateLanguage = SUpdateLanguage(); final SUpdateLanguage sUpdateLanguage = SUpdateLanguage();
@@ -37,6 +44,88 @@ class SettingsViewModel extends BaseViewModel {
notifyListeners(); notifyListeners();
} }
bool isPatchesChangeEnabled() {
return _managerAPI.isPatchesChangeEnabled();
}
Future<void> showPatchesChangeEnableDialog(
bool value,
BuildContext context,
) async {
if (value) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
title: I18nText('warning'),
content: I18nText(
'settingsView.enablePatchesSelectionWarningText',
child: const Text(
'',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
actions: [
CustomMaterialButton(
isFilled: false,
label: I18nText('yesButton'),
onPressed: () {
_managerAPI.setChangingToggleModified(true);
_managerAPI.setPatchesChangeEnabled(true);
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('noButton'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
),
);
} else {
return showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
title: I18nText('warning'),
content: I18nText(
'settingsView.disablePatchesSelectionWarningText',
child: const Text(
'',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
actions: [
CustomMaterialButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () {
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('yesButton'),
onPressed: () {
_managerAPI.setChangingToggleModified(true);
_patchesSelectorViewModel.selectDefaultPatches();
_managerAPI.setPatchesChangeEnabled(false);
Navigator.of(context).pop();
},
),
],
),
);
}
}
bool areUniversalPatchesEnabled() { bool areUniversalPatchesEnabled() {
return _managerAPI.areUniversalPatchesEnabled(); return _managerAPI.areUniversalPatchesEnabled();
} }
@@ -90,26 +179,30 @@ class SettingsViewModel extends BaseViewModel {
} }
} }
Future<void> importPatches() async { Future<void> importPatches(BuildContext context) async {
try { if (isPatchesChangeEnabled()) {
final FilePickerResult? result = await FilePicker.platform.pickFiles( try {
type: FileType.custom, final FilePickerResult? result = await FilePicker.platform.pickFiles(
allowedExtensions: ['json'], type: FileType.custom,
); allowedExtensions: ['json'],
if (result != null && result.files.single.path != null) { );
final File inFile = File(result.files.single.path!); if (result != null && result.files.single.path != null) {
inFile.copySync(_managerAPI.storedPatchesFile); final File inFile = File(result.files.single.path!);
inFile.delete(); inFile.copySync(_managerAPI.storedPatchesFile);
if (locator<PatcherViewModel>().selectedApp != null) { inFile.delete();
locator<PatcherViewModel>().loadLastSelectedPatches(); if (_patcherViewModel.selectedApp != null) {
_patcherViewModel.loadLastSelectedPatches();
}
_toast.showBottom('settingsView.importedPatches');
} }
_toast.showBottom('settingsView.importedPatches'); } on Exception catch (e) {
if (kDebugMode) {
print(e);
}
_toast.showBottom('settingsView.jsonSelectorErrorMessage');
} }
} on Exception catch (e) { } else {
if (kDebugMode) { _managerAPI.showPatchesChangeWarningDialog(context);
print(e);
}
_toast.showBottom('settingsView.jsonSelectorErrorMessage');
} }
} }

View File

@@ -222,22 +222,6 @@ 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

@@ -13,7 +13,6 @@ 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';
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/utils/string.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
class AppInfoViewModel extends BaseViewModel { class AppInfoViewModel extends BaseViewModel {
@@ -147,17 +146,7 @@ class AppInfoViewModel extends BaseViewModel {
} }
String getAppliedPatchesString(List<String> appliedPatches) { String getAppliedPatchesString(List<String> appliedPatches) {
final List<String> names = appliedPatches return '\u2022 ${appliedPatches.join('\n\u2022 ')}';
.map(
(p) => p
.replaceAll('-', ' ')
.split('-')
.join(' ')
.toTitleCase()
.replaceFirst('Microg', 'MicroG'),
)
.toList();
return '\u2022 ${names.join('\n\u2022 ')}';
} }
void openApp(PatchedApplication app) { void openApp(PatchedApplication app) {

View File

@@ -7,7 +7,7 @@ class AppSkeletonLoader extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width; final screenWidth = MediaQuery.sizeOf(context).width;
return ListView.builder( return ListView.builder(
shrinkWrap: true, shrinkWrap: true,
itemCount: 7, itemCount: 7,

View File

@@ -79,8 +79,6 @@ class InstalledAppsCard extends StatelessWidget {
icon: app.icon, icon: app.icon,
name: app.name, name: app.name,
patchDate: app.patchDate, patchDate: app.patchDate,
changelog: app.changelog,
isUpdatableApp: false,
onPressed: () => onPressed: () =>
locator<HomeViewModel>().navigateToAppInfo(app), locator<HomeViewModel>().navigateToAppInfo(app),
), ),

View File

@@ -25,7 +25,7 @@ class _GradientProgressIndicatorState extends State<GradientProgressIndicator> {
), ),
), ),
height: 5, height: 5,
width: MediaQuery.of(context).size.width * widget.progress!, width: MediaQuery.sizeOf(context).width * widget.progress!,
), ),
); );
} }

View File

@@ -19,6 +19,7 @@ class PatchItem extends StatefulWidget {
required this.isNew, required this.isNew,
required this.isSelected, required this.isSelected,
required this.onChanged, required this.onChanged,
required this.isChangeEnabled,
this.child, this.child,
}) : super(key: key); }) : super(key: key);
final String name; final String name;
@@ -30,6 +31,7 @@ class PatchItem extends StatefulWidget {
final bool isNew; final bool isNew;
bool isSelected; bool isSelected;
final Function(bool) onChanged; final Function(bool) onChanged;
final bool isChangeEnabled;
final Widget? child; final Widget? child;
final toast = locator<Toast>(); final toast = locator<Toast>();
final _managerAPI = locator<ManagerAPI>(); final _managerAPI = locator<ManagerAPI>();
@@ -58,11 +60,13 @@ class _PatchItemState extends State<PatchItem> {
!widget._managerAPI.areExperimentalPatchesEnabled()) { !widget._managerAPI.areExperimentalPatchesEnabled()) {
widget.isSelected = false; widget.isSelected = false;
widget.toast.showBottom('patchItem.unsupportedPatchVersion'); widget.toast.showBottom('patchItem.unsupportedPatchVersion');
} else { } else if (widget.isChangeEnabled) {
widget.isSelected = !widget.isSelected; widget.isSelected = !widget.isSelected;
} }
}); });
widget.onChanged(widget.isSelected); if (!widget.isUnsupported || widget._managerAPI.areExperimentalPatchesEnabled()) {
widget.onChanged(widget.isSelected);
}
}, },
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
@@ -124,11 +128,13 @@ class _PatchItemState extends State<PatchItem> {
widget.toast.showBottom( widget.toast.showBottom(
'patchItem.unsupportedPatchVersion', 'patchItem.unsupportedPatchVersion',
); );
} else { } else if (widget.isChangeEnabled) {
widget.isSelected = newValue!; widget.isSelected = newValue!;
} }
}); });
widget.onChanged(widget.isSelected); if (!widget.isUnsupported || widget._managerAPI.areExperimentalPatchesEnabled()) {
widget.onChanged(widget.isSelected);
}
}, },
), ),
), ),
@@ -186,7 +192,7 @@ class _PatchItemState extends State<PatchItem> {
), ),
), ),
), ),
) ),
], ],
), ),
widget.child ?? const SizedBox(), widget.child ?? const SizedBox(),

View File

@@ -8,8 +8,9 @@ class OptionsTextField extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sHeight = MediaQuery.of(context).size.height; final size = MediaQuery.sizeOf(context);
final sWidth = MediaQuery.of(context).size.width; final sHeight = size.height;
final sWidth = size.width;
return Container( return Container(
margin: const EdgeInsets.only(top: 12, bottom: 6), margin: const EdgeInsets.only(top: 12, bottom: 6),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,

View File

@@ -6,6 +6,7 @@ import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_man
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_sources.dart'; import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_sources.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/settings_auto_update_patches.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_auto_update_patches.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_enable_patches_selection.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_experimental_patches.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_experimental_patches.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_experimental_universal_patches.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_experimental_universal_patches.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
@@ -25,6 +26,7 @@ class SAdvancedSection extends StatelessWidget {
SManageSourcesUI(), SManageSourcesUI(),
// SManageKeystorePasswordUI(), // SManageKeystorePasswordUI(),
SAutoUpdatePatches(), SAutoUpdatePatches(),
SEnablePatchesSelection(),
SExperimentalUniversalPatches(), SExperimentalUniversalPatches(),
SExperimentalPatches(), SExperimentalPatches(),
ListTile( ListTile(

View File

@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
class SEnablePatchesSelection extends StatefulWidget {
const SEnablePatchesSelection({super.key});
@override
State<SEnablePatchesSelection> createState() => _SEnablePatchesSelectionState();
}
final _settingsViewModel = SettingsViewModel();
class _SEnablePatchesSelectionState extends State<SEnablePatchesSelection> {
@override
Widget build(BuildContext context) {
return SwitchListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.enablePatchesSelectionLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.enablePatchesSelectionHint'),
value: _settingsViewModel.isPatchesChangeEnabled(),
onChanged: (value) async {
await _settingsViewModel.showPatchesChangeEnableDialog(value, context);
setState(() {});
},
);
}
}

View File

@@ -43,7 +43,7 @@ class SExportSection extends StatelessWidget {
), ),
), ),
subtitle: I18nText('settingsView.importPatchesHint'), subtitle: I18nText('settingsView.importPatchesHint'),
onTap: () => _settingsViewModel.importPatches(), onTap: () => _settingsViewModel.importPatches(context),
), ),
ListTile( ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
@@ -73,10 +73,10 @@ class SExportSection extends StatelessWidget {
), ),
), ),
subtitle: I18nText('settingsView.importKeystoreHint'), subtitle: I18nText('settingsView.importKeystoreHint'),
onTap: () async{ onTap: () async {
await _settingsViewModel.importKeystore(); await _settingsViewModel.importKeystore();
final sManageKeystorePassword = SManageKeystorePassword(); final sManageKeystorePassword = SManageKeystorePassword();
if(context.mounted){ if (context.mounted) {
sManageKeystorePassword.showKeystoreDialog(context); sManageKeystorePassword.showKeystoreDialog(context);
} }
}, },

View File

@@ -75,8 +75,8 @@ class SocialMediaWidget extends StatelessWidget {
SocialMediaItem( SocialMediaItem(
icon: FaIcon(FontAwesomeIcons.youtube), icon: FaIcon(FontAwesomeIcons.youtube),
title: Text('YouTube'), title: Text('YouTube'),
subtitle: Text('youtube.com/revanced'), subtitle: Text('youtube.com/@revanced'),
url: 'https://youtube.com/revanced', url: 'https://youtube.com/@revanced',
), ),
], ],
), ),

View File

@@ -1,6 +1,5 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:expandable/expandable.dart';
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/ui/widgets/shared/custom_card.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
@@ -13,151 +12,84 @@ class ApplicationItem extends StatefulWidget {
required this.icon, required this.icon,
required this.name, required this.name,
required this.patchDate, required this.patchDate,
required this.changelog,
required this.isUpdatableApp,
required this.onPressed, required this.onPressed,
}) : super(key: key); }) : super(key: key);
final Uint8List icon; final Uint8List icon;
final String name; final String name;
final DateTime patchDate; final DateTime patchDate;
final List<String> changelog;
final bool isUpdatableApp;
final Function() onPressed; final Function() onPressed;
@override @override
State<ApplicationItem> createState() => _ApplicationItemState(); State<ApplicationItem> createState() => _ApplicationItemState();
} }
class _ApplicationItemState extends State<ApplicationItem> class _ApplicationItemState extends State<ApplicationItem> {
with TickerProviderStateMixin {
late AnimationController _animationController;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ExpandableController expController = ExpandableController();
return Container( return Container(
margin: const EdgeInsets.only(bottom: 16.0), margin: const EdgeInsets.only(bottom: 16.0),
child: CustomCard( child: CustomCard(
onTap: () { child: Row(
expController.toggle(); mainAxisAlignment: MainAxisAlignment.spaceBetween,
_animationController.isCompleted children: [
? _animationController.reverse() Flexible(
: _animationController.forward(); child: Row(
},
child: ExpandablePanel(
controller: expController,
theme: const ExpandableThemeData(
inkWellBorderRadius: BorderRadius.all(Radius.circular(16)),
tapBodyToCollapse: false,
tapBodyToExpand: false,
tapHeaderToExpand: false,
hasIcon: false,
animationDuration: Duration(milliseconds: 450),
),
header: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Row(
children: [
SizedBox(
width: 40,
child: Image.memory(widget.icon, height: 40, width: 40),
),
const SizedBox(width: 19),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
widget.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
Text(
format(widget.patchDate),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
),
Row(
children: [ children: [
RotationTransition( SizedBox(
turns: Tween(begin: 0.0, end: 0.50) width: 40,
.animate(_animationController), child: Image.memory(widget.icon, height: 40, width: 40),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.arrow_drop_down),
),
), ),
const SizedBox(width: 8), const SizedBox(width: 19),
Column( Expanded(
mainAxisAlignment: MainAxisAlignment.center, child: Column(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
CustomMaterialButton( Text(
label: widget.isUpdatableApp widget.name,
? I18nText('applicationItem.patchButton') maxLines: 1,
: I18nText('applicationItem.infoButton'), overflow: TextOverflow.ellipsis,
onPressed: widget.onPressed, style: const TextStyle(
), fontSize: 16,
], fontWeight: FontWeight.w500,
),
),
Text(
format(widget.patchDate),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
), ),
], ],
), ),
],
),
collapsed: const SizedBox(),
expanded: Padding(
padding: const EdgeInsets.only(
top: 16.0,
left: 4.0,
right: 4.0,
bottom: 4.0,
), ),
child: Column( Row(
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: <Widget>[ const SizedBox(width: 8),
I18nText( Column(
'applicationItem.changelogLabel', mainAxisAlignment: MainAxisAlignment.center,
child: const Text( crossAxisAlignment: CrossAxisAlignment.end,
'', children: <Widget>[
style: TextStyle(fontWeight: FontWeight.w700), CustomMaterialButton(
), label: I18nText('applicationItem.infoButton'),
onPressed: widget.onPressed,
),
],
), ),
const SizedBox(height: 4),
Text('\u2022 ${widget.changelog.join('\n\u2022 ')}'),
], ],
), ),
), ],
), ),
), ),
); );

View File

@@ -4,7 +4,7 @@ homepage: https://github.com/revanced/revanced-manager
publish_to: 'none' publish_to: 'none'
version: 1.8.0+100800000 version: 1.11.2+101100200
environment: environment:
sdk: '>=3.0.0 <4.0.0' sdk: '>=3.0.0 <4.0.0'
@@ -75,6 +75,8 @@ dependencies:
flutter_markdown: ^0.6.14 flutter_markdown: ^0.6.14
dio_cache_interceptor: ^3.4.0 dio_cache_interceptor: ^3.4.0
install_plugin: ^2.1.0 install_plugin: ^2.1.0
screenshot_callback: ^3.0.1
synchronized: ^3.1.0
dev_dependencies: dev_dependencies:
json_serializable: ^6.6.1 json_serializable: ^6.6.1