mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2026-01-11 21:56:17 +00:00
Compare commits
103 Commits
v1.15.1
...
v1.19.0-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93482b0041 | ||
|
|
5b2c55142e | ||
|
|
088a3b7c28 | ||
|
|
b115643034 | ||
|
|
07fc964f9c | ||
|
|
3e51caf111 | ||
|
|
deb1ba339f | ||
|
|
70a1086edf | ||
|
|
c7d975e612 | ||
|
|
7104d6d6dd | ||
|
|
d933997c89 | ||
|
|
bea99bb4c6 | ||
|
|
f38a593434 | ||
|
|
85950991ab | ||
|
|
6fd740f8c0 | ||
|
|
ec2c2d8ccb | ||
|
|
efb2d5ef32 | ||
|
|
acb1e2434b | ||
|
|
8e52abda9a | ||
|
|
9764326242 | ||
|
|
97c33d6c54 | ||
|
|
30d5f3ad3f | ||
|
|
079c0defaf | ||
|
|
cf1afddb9e | ||
|
|
b07b9351c4 | ||
|
|
7911459817 | ||
|
|
ef9b1d5c2d | ||
|
|
fd2780624a | ||
|
|
4f22e88e42 | ||
|
|
0d45fe4a97 | ||
|
|
28ae2766f0 | ||
|
|
edd86024b9 | ||
|
|
f3c78c2c24 | ||
|
|
3f96608398 | ||
|
|
9ed43efe5d | ||
|
|
975180b075 | ||
|
|
c94eb7a48e | ||
|
|
85c8006977 | ||
|
|
850bdc4a4d | ||
|
|
ca95e32164 | ||
|
|
a141ec85b0 | ||
|
|
1298a96b0e | ||
|
|
1928b150ad | ||
|
|
7426f5484d | ||
|
|
11a8f313b0 | ||
|
|
69b6ef07a1 | ||
|
|
7a1ba9dabf | ||
|
|
48a739c94e | ||
|
|
9680f0cf12 | ||
|
|
5b1c89a0c5 | ||
|
|
8a1ab478a3 | ||
|
|
2ae8d49526 | ||
|
|
a0b673c138 | ||
|
|
c56c445fb7 | ||
|
|
c23275f2fe | ||
|
|
8b28a33b73 | ||
|
|
f8d086a743 | ||
|
|
c06d15de5f | ||
|
|
dcaf1f54e4 | ||
|
|
aa91abb022 | ||
|
|
67d204eb02 | ||
|
|
18e680b298 | ||
|
|
6ef1b072e8 | ||
|
|
ca9ef544ce | ||
|
|
7831a3438d | ||
|
|
1fad90441c | ||
|
|
7f26c5bd45 | ||
|
|
637641cf54 | ||
|
|
d9a316abbb | ||
|
|
093cfa5269 | ||
|
|
61a025de4d | ||
|
|
7df1ae7ed8 | ||
|
|
eb6e75b156 | ||
|
|
8480b3ac3d | ||
|
|
6339a31fec | ||
|
|
71b5bb3f8f | ||
|
|
a9878dbbdf | ||
|
|
b1fb9dd7d3 | ||
|
|
de51fbd7be | ||
|
|
c9412a97d0 | ||
|
|
63c29bdd75 | ||
|
|
bace26063d | ||
|
|
7931eb97b9 | ||
|
|
9df89c7b74 | ||
|
|
78978276c4 | ||
|
|
011eddbbc5 | ||
|
|
4e9f3fe1dc | ||
|
|
9bd48c19ff | ||
|
|
c0516c3665 | ||
|
|
06ff36c836 | ||
|
|
98c16eb1dc | ||
|
|
35d80840e5 | ||
|
|
ad3a778fb9 | ||
|
|
d25d1efe9c | ||
|
|
de58dff423 | ||
|
|
ec26e4d8d1 | ||
|
|
c24da41505 | ||
|
|
f785185e1c | ||
|
|
5ed3ed9a2d | ||
|
|
a3adace60e | ||
|
|
3db4de09e2 | ||
|
|
70b2ee0a84 | ||
|
|
e7d82850c9 |
2
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
2
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: ⭐ Feature request
|
||||
description: Create a new feature request.
|
||||
title: 'feat: <title>'
|
||||
labels: [feature-request]
|
||||
labels: 'feature request'
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
114
.github/workflows/pr-build.yml
vendored
114
.github/workflows/pr-build.yml
vendored
@@ -1,45 +1,121 @@
|
||||
name: PR Build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/pr-build.yml"
|
||||
- "android/**"
|
||||
- "assets/**"
|
||||
- "lib/**"
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
# Flutter
|
||||
flutter-branch:
|
||||
description: Flutter branch
|
||||
type: choice
|
||||
default: 'stable'
|
||||
options:
|
||||
- stable
|
||||
- beta
|
||||
- dev
|
||||
- master
|
||||
flutter-cache:
|
||||
description: Cache
|
||||
type: boolean
|
||||
default: true
|
||||
# Application configuration
|
||||
app-flavour:
|
||||
description: App flavour
|
||||
default: 'release'
|
||||
type: choice
|
||||
options:
|
||||
- release
|
||||
- debug
|
||||
- profile
|
||||
# Pull Request
|
||||
pr-number:
|
||||
description: PR number (No hashtag)
|
||||
required: true
|
||||
|
||||
run-name: "Build PR ${{ inputs.pr-number }}"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
name: Build the application
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Setup
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh repo clone "${{ github.repository }}"
|
||||
cd revanced-manager
|
||||
gh repo set-default "${{ github.repository }}"
|
||||
gh pr checkout "${{ inputs.pr-number }}"
|
||||
|
||||
echo "DATETIME=$( TZ='UTC+0' date --rfc-email )" >> $GITHUB_ENV
|
||||
echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# Make sure the release step uses its own credentials:
|
||||
# https://github.com/cycjimmy/semantic-release-action#private-packages
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup JDK
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'zulu'
|
||||
cache: gradle
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
channel: ${{ inputs.flutter-branch }}
|
||||
cache: ${{ inputs.flutter-cache }}
|
||||
|
||||
- name: Install Flutter dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Generate files with Builder
|
||||
run: flutter packages pub run build_runner build --delete-conflicting-outputs
|
||||
run: dart run build_runner build --delete-conflicting-outputs
|
||||
|
||||
- name: Build with Flutter
|
||||
env:
|
||||
continue-on-error: true
|
||||
id: flutter-build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: flutter build apk --debug
|
||||
run: |
|
||||
flutter build apk --"${{ inputs.app-flavour }}";
|
||||
|
||||
- name: Prepare to comment
|
||||
run: |
|
||||
if [[ "${{ steps.flutter-build.outcome }}" == "success" ]]; then
|
||||
echo "MESSAGE=✅ ReVanced Manager ${{ env.COMMIT_HASH }} build succeeded." >> $GITHUB_ENV
|
||||
else
|
||||
echo "MESSAGE=🚫 ReVanced Manager ${{ env.COMMIT_HASH }} build failed." >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: "Comment to Pull Request #${{ inputs.pr-number }}"
|
||||
uses: thollander/actions-comment-pull-request@v2
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
pr_number: ${{ inputs.pr-number }}
|
||||
mode: recreate
|
||||
message: |
|
||||
## ⚒️ ReVanced PR Build workflow
|
||||
|
||||
${{ env.MESSAGE }}
|
||||
|
||||
You can see more details in run [${{ github.run_id }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})!
|
||||
|
||||
### ⚙️ Overview
|
||||
- App flavor: ${{ inputs.app-flavour }}
|
||||
- Branch: ${{ inputs.flutter-branch }}
|
||||
- Start time: ${{ env.DATETIME }}
|
||||
|
||||
- name: Upload build
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: revanced-manager
|
||||
path: build/app/outputs/flutter-apk/app-debug.apk
|
||||
if-no-files-found: error
|
||||
name: revanced-manager-(${{ env.COMMIT_HASH }}-${{ inputs.pr-number }}-${{ inputs.app-flavour }})-${{ inputs.flutter-branch }}
|
||||
path: |
|
||||
build/app/outputs/flutter-apk/app-${{ inputs.app-flavour }}.apk
|
||||
build/app/outputs/flutter-apk/app-${{ inputs.app-flavour }}.apk.sha1
|
||||
|
||||
56
.github/workflows/release-build.yml
vendored
56
.github/workflows/release-build.yml
vendored
@@ -1,50 +1,58 @@
|
||||
name: "Release Build"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
paths:
|
||||
- ".github/workflows/release-build.yml"
|
||||
- "android/**"
|
||||
- "assets/**"
|
||||
- "lib/**"
|
||||
- ".releaserc.js"
|
||||
- "pubspec.yaml"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set env
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: "17"
|
||||
distribution: "zulu"
|
||||
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
|
||||
- name: Cache Node modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
key: npm-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Setup semantic-release
|
||||
run: npm install
|
||||
|
||||
- name: Set up Flutter
|
||||
run: flutter pub get
|
||||
|
||||
- name: Generate files with Builder
|
||||
run: flutter packages pub run build_runner build --delete-conflicting-outputs
|
||||
run: dart run build_runner build --delete-conflicting-outputs
|
||||
|
||||
- name: Build with Flutter
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
|
||||
run: flutter build apk
|
||||
- name: Sign APK
|
||||
id: sign_apk
|
||||
uses: ilharp/sign-android-release@v1
|
||||
with:
|
||||
releaseDir: build/app/outputs/apk/release
|
||||
signingKey: ${{ secrets.SIGNING_KEYSTORE }}
|
||||
signingKey: "keystore.jks"
|
||||
keyStorePassword: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
|
||||
keyAlias: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
keyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||
- name: Add version to APK
|
||||
run: mv ${{steps.sign_apk.outputs.signedFile}} revanced-manager-${{ env.RELEASE_VERSION }}.apk
|
||||
- name: Publish release APK
|
||||
uses: "marvinpinto/action-automatic-releases@latest"
|
||||
with:
|
||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
prerelease: false
|
||||
files: revanced-manager-${{ env.RELEASE_VERSION }}.apk
|
||||
run: |
|
||||
echo "${{ secrets.SIGNING_KEYSTORE }}" | base64 --decode > android/app/keystore.jks
|
||||
npx semantic-release
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,6 +9,7 @@
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
local.properties
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
|
||||
84
.releaserc.js
Normal file
84
.releaserc.js
Normal file
@@ -0,0 +1,84 @@
|
||||
module.exports = {
|
||||
"branches": [
|
||||
"main",
|
||||
{
|
||||
"name": "dev",
|
||||
"prerelease": true
|
||||
}
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"@semantic-release/commit-analyzer", {
|
||||
"releaseRules": [
|
||||
{ "type": "build", "scope": "Needs bump", "release": "patch" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"@semantic-release/changelog",
|
||||
[
|
||||
"@semantic-release/release-notes-generator",
|
||||
{
|
||||
"preset": "conventionalcommits",
|
||||
writerOpts: {
|
||||
commitPartial: "* {{subject}} ([{{author.name}}]({{~@root.host}}/{{~@root.owner}}/{{~@root.repository}}/commit/{{hash}}))\n",
|
||||
mainTemplate: `
|
||||
{{#each commitGroups}}
|
||||
{{#if title}}
|
||||
## {{title}}
|
||||
{{/if}}
|
||||
{{#each commits}}
|
||||
{{> commit root=@root}}
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
`
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"@droidsolutions-oss/semantic-release-update-file",
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"path": ["pubspec.yaml"],
|
||||
"type": "flutter",
|
||||
"branches": ["main", "dev"]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/exec",
|
||||
{
|
||||
"prepareCmd": "flutter build apk"
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/git",
|
||||
{
|
||||
"assets": [
|
||||
"pubspec.yaml"
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/github",
|
||||
{
|
||||
"assets": [
|
||||
{
|
||||
"path": "build/app/outputs/apk/release/revanced-manager*.apk"
|
||||
}
|
||||
],
|
||||
"successComment": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"@saithodev/semantic-release-backmerge",
|
||||
{
|
||||
"backmergeBranches": [{"from": "main", "to": "dev"}],
|
||||
"clearWorkspace": true
|
||||
}
|
||||
]
|
||||
],
|
||||
|
||||
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="main.dart" type="FlutterRunConfigurationType" factoryName="Flutter">
|
||||
<option name="filePath" value="$PROJECT_DIR$/lib/main.dart" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
103
CONTRIBUTING.md
Normal file
103
CONTRIBUTING.md
Normal file
@@ -0,0 +1,103 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source
|
||||
width="256px"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||
>
|
||||
<img
|
||||
width="256px"
|
||||
src="assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||
>
|
||||
</picture>
|
||||
<br>
|
||||
<a href="https://revanced.app/">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo-round.svg" />
|
||||
<img height="24px" src="assets/revanced-logo/revanced-logo-round.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="http://revanced.app/discord">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://reddit.com/r/revancedapp">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://t.me/app_revanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://x.com/revancedapp">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
Continuing the legacy of Vanced
|
||||
</p>
|
||||
|
||||
# 👋 Contribution guidelines
|
||||
|
||||
This document describes how to contribute to ReVanced Manager.
|
||||
|
||||
## 📖 Resources to help you get started
|
||||
|
||||
* The [documentation](/docs/README.md) provides steps to build ReVanced Manager from source
|
||||
* Our [backlog](https://github.com/orgs/ReVanced/projects/12) is where we keep track of what we're working on
|
||||
* [Issues](https://github.com/ReVanced/revanced-manager/issues) are where we keep track of bugs and feature requests
|
||||
|
||||
## 🙏 Submitting a feature request
|
||||
|
||||
Features can be requested by opening an issue using the
|
||||
[feature request issue template](https://github.com/ReVanced/revanced-manager/issues/new?assignees=&labels=feature-request&projects=&template=feature-issue.yml&title=feat%3A+%3Ctitle%3E).
|
||||
|
||||
> [!NOTE]
|
||||
> We may reject your request at the discretion of ReVanced Manager's maintainers,
|
||||
> please provide good motivation for a request to be accepted.
|
||||
|
||||
## 🐞 Submitting a bug report
|
||||
|
||||
If you encounter a bug while using the ReVanced Manager app, open an issue using the
|
||||
[bug report issue template](https://github.com/ReVanced/revanced-manager/issues/new?assignees=&labels=bug&projects=&template=bug-issue.yml&title=bug%3A+%3Ctitle%3E).
|
||||
|
||||
## 📝 How to contribute
|
||||
|
||||
> [!TIP]
|
||||
> We recommend that you discuss your changes with
|
||||
> the maintainers of ReVanced Manager before contributing.
|
||||
> This will help you determine whether your change is acceptable.
|
||||
|
||||
1. Fork the repository and create a new branch based off the `dev` branch
|
||||
2. Commit your changes
|
||||
3. Open a pull request to the `dev` branch and reference issues that your pull request closes
|
||||
4. The maintainers of ReVanced Manager will review and provide suggestions.
|
||||
Once your pull request is approved and merged, it will be included in the next release of ReVanced Manager
|
||||
|
||||
## 🤚 I want to contribute but don't know how to code
|
||||
|
||||
Even if you don't know how to code, you can still contribute by
|
||||
translating ReVanced Manager on [Crowdin](https://translate.revanced.app/).
|
||||
|
||||
❤️ Thank you for considering contributing to ReVanced Manager.
|
||||
104
README.md
104
README.md
@@ -1,35 +1,97 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source
|
||||
width="256px"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||
>
|
||||
<img
|
||||
width="256px"
|
||||
src="assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||
>
|
||||
</picture>
|
||||
<br>
|
||||
<a href="https://revanced.app/">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo-round.svg" />
|
||||
<img height="24px" src="assets/revanced-logo/revanced-logo-round.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="http://revanced.app/discord">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://reddit.com/r/revancedapp">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://t.me/app_revanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://x.com/revancedapp">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
Continuing the legacy of Vanced
|
||||
</p>
|
||||
|
||||
# 💊 ReVanced Manager
|
||||
|
||||
The official ReVanced Manager based on Flutter.
|
||||
[](https://github.com/ReVanced/revanced-manager/commits "")
|
||||
[](https://github.com/ReVanced/revanced-manager/commits "")
|
||||
|
||||
ReVanced Manager is an Android application that uses ReVanced Patcher to add, remove, and modify existing functionalities in Android applications.
|
||||
|
||||
## 💪 Features
|
||||
|
||||
Some of the features we provide are:
|
||||
|
||||
* 📱 **Portable**: ReVanced Patcher that fit in your pocket;
|
||||
* 🤗 **Intuitive UI**: Help you manage your patched applications with easy-to-use interface;
|
||||
* 🛠️ **Customization**: Patch with third-party sources;
|
||||
* ✨ And a **lot more!**
|
||||
|
||||
## 🔽 Download
|
||||
|
||||
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)
|
||||
You can get ReVanced Manager by downloading from [ReVanced site](https://revanced.app/download) or [GitHub releases](https://github.com/ReVanced/revanced-manager/releases).
|
||||
|
||||
## 📝 Prerequisites
|
||||
## 📚 Everything else
|
||||
|
||||
1. Android 8 or higher
|
||||
2. Incompatible with certain ARMv7 devices
|
||||
### 📄 Documentation
|
||||
|
||||
## 📃 Documentation
|
||||
The documentation can be found [here](https://github.com/revanced/revanced-manager/tree/main/docs).
|
||||
Documentation on how to use the application is available [here](/docs/README.md).
|
||||
|
||||
## 🔴 Issues
|
||||
### 👋 Contributing
|
||||
|
||||
For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose).
|
||||
Thank you for considering contributing to ReVanced Manager, you can find the contribution guidelines [here](/CONTRIBUTING.md).
|
||||
|
||||
## 🌐 Translation
|
||||
### 🔴 Issues
|
||||
|
||||
[](https://crowdin.com/project/revanced)
|
||||
For suggestions and bug reports, open an issue [here](https://github.com/ReVanced/revanced-manager/issues/choose).
|
||||
|
||||
We're accepting translations on [Crowdin](https://translate.revanced.app).
|
||||
## ⚖️ License
|
||||
|
||||
## 🛠️ Building Manager from source
|
||||
|
||||
1. Setup flutter environment for your [platform](https://docs.flutter.dev/get-started/install)
|
||||
2. Clone the repository locally
|
||||
3. Add your GitHub token in gradle.properties like [this](/docs/4_building.md)
|
||||
4. Open the project in terminal
|
||||
5. Run `flutter pub get` in terminal
|
||||
6. Then `flutter packages pub run build_runner build --delete-conflicting-outputs` (Must be done on each git pull)
|
||||
7. To build release APK run `flutter build apk`
|
||||
ReVanced Manager adopts the [GNU General Public License 3.0](/LICENSE), [tl;dr](https://www.tldrlegal.com/license/gnu-general-public-license-v3-gpl-3): You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build & install instructions.
|
||||
|
||||
77
SECURITY.md
Normal file
77
SECURITY.md
Normal file
@@ -0,0 +1,77 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source
|
||||
width="256px"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||
>
|
||||
<img
|
||||
width="256px"
|
||||
src="assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||
>
|
||||
</picture>
|
||||
<br>
|
||||
<a href="https://revanced.app/">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo-round.svg" />
|
||||
<img height="24px" src="assets/revanced-logo/revanced-logo-round.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="http://revanced.app/discord">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://reddit.com/r/revancedapp">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://t.me/app_revanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://x.com/revancedapp">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
Continuing the legacy of Vanced
|
||||
</p>
|
||||
|
||||
# 🔒 Security Policy
|
||||
|
||||
This document describes how to report security vulnerabilities for ReVanced Manager.
|
||||
|
||||
## 🚨 Reporting a Vulnerability
|
||||
|
||||
Please open an issue in our [advisory tracker](https://github.com/ReVanced/revanced-manager/security/advisories/new) or reach out privately to us on [Discord](https://discord.gg/revanced).
|
||||
|
||||
If a vulnerability is confirmed and accepted, you can join our [Discord](https://discord.gg/revanced) server to receive a special contributor role.
|
||||
|
||||
### ⏳ Supported Versions
|
||||
|
||||
| Version | Branch | Supported |
|
||||
| ------- | ------------|------------------- |
|
||||
|  | main | :white_check_mark: |
|
||||
|  | dev | :white_check_mark: |
|
||||
|  | compose-dev | :white_check_mark: |
|
||||
@@ -21,7 +21,6 @@ linter:
|
||||
- always_declare_return_types
|
||||
- require_trailing_commas
|
||||
- always_put_control_body_on_new_line
|
||||
- always_require_non_null_named_parameters
|
||||
- always_use_package_imports # we do this commonly
|
||||
- annotate_overrides
|
||||
- avoid_bool_literals_in_conditional_expressions
|
||||
@@ -40,8 +39,6 @@ linter:
|
||||
- avoid_relative_lib_imports
|
||||
- avoid_renaming_method_parameters
|
||||
- avoid_return_types_on_setters
|
||||
- avoid_returning_null
|
||||
- avoid_returning_null_for_future
|
||||
- avoid_returning_null_for_void
|
||||
- avoid_setters_without_getters
|
||||
- avoid_shadowing_type_parameters
|
||||
@@ -126,6 +123,7 @@ linter:
|
||||
- slash_for_doc_comments
|
||||
- sort_child_properties_last
|
||||
- sort_constructors_first
|
||||
- sort_pub_dependencies
|
||||
- sort_unnamed_constructors_first
|
||||
- test_types_in_equals
|
||||
- throw_in_finally
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
}
|
||||
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
@@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) {
|
||||
}
|
||||
}
|
||||
|
||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||
if (flutterRoot == null) {
|
||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = '1'
|
||||
@@ -21,10 +22,6 @@ if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
android {
|
||||
compileSdk 34
|
||||
ndkVersion flutter.ndkVersion
|
||||
@@ -50,11 +47,30 @@ android {
|
||||
release {
|
||||
shrinkResources false
|
||||
minifyEnabled false
|
||||
resValue "string", "app_name", "ReVanced Manager"
|
||||
signingConfig signingConfigs.debug
|
||||
ndk {
|
||||
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
|
||||
}
|
||||
if (System.getenv("signingKey") != null) {
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
storeFile = file(System.getenv("signingKey"))
|
||||
storePassword = System.getenv("keyStorePassword")
|
||||
keyAlias = System.getenv("keyAlias")
|
||||
keyPassword = System.getenv("keyPassword")
|
||||
}
|
||||
}
|
||||
signingConfig = signingConfigs.release
|
||||
resValue "string", "app_name", "ReVanced Manager"
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.all {
|
||||
outputFileName = "revanced-manager-v${flutterVersionName}.apk"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
signingConfig = signingConfigs.debug
|
||||
resValue "string", "app_name", "ReVanced Manager Debug"
|
||||
applicationIdSuffix ".debug"
|
||||
}
|
||||
}
|
||||
debug {
|
||||
shrinkResources false
|
||||
@@ -66,6 +82,16 @@ android {
|
||||
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
|
||||
}
|
||||
}
|
||||
profile {
|
||||
shrinkResources false
|
||||
minifyEnabled false
|
||||
resValue "string", "app_name", "ReVanced Manager Profile"
|
||||
applicationIdSuffix ".profile"
|
||||
signingConfig signingConfigs.debug
|
||||
ndk {
|
||||
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
|
||||
}
|
||||
}
|
||||
}
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
@@ -92,7 +118,7 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
|
||||
// ReVanced
|
||||
implementation "app.revanced:revanced-patcher:19.0.0"
|
||||
implementation "app.revanced:revanced-patcher:19.1.0"
|
||||
|
||||
// Signing & aligning
|
||||
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
@@ -32,17 +33,17 @@
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"/>
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ExportSettingsActivity"
|
||||
android:exported="true">
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ExportSettingsActivity"
|
||||
android:exported="true">
|
||||
</activity>
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
@@ -55,5 +56,22 @@
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<receiver
|
||||
android:name=".utils.packageInstaller.InstallerReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="APP_INSTALL_ACTION" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".utils.packageInstaller.UninstallerReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="APP_UNINSTALL_ACTION" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
package app.revanced.manager.flutter
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.app.SearchManager
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import app.revanced.manager.flutter.utils.Aapt
|
||||
import app.revanced.manager.flutter.utils.aligning.ZipAligner
|
||||
import app.revanced.manager.flutter.utils.packageInstaller.InstallerReceiver
|
||||
import app.revanced.manager.flutter.utils.packageInstaller.UninstallerReceiver
|
||||
import app.revanced.manager.flutter.utils.signing.Signer
|
||||
import app.revanced.manager.flutter.utils.zip.ZipFile
|
||||
import app.revanced.manager.flutter.utils.zip.structures.ZipEntry
|
||||
@@ -41,6 +48,17 @@ class MainActivity : FlutterActivity() {
|
||||
|
||||
val patcherChannel = "app.revanced.manager.flutter/patcher"
|
||||
val installerChannel = "app.revanced.manager.flutter/installer"
|
||||
val openBrowserChannel = "app.revanced.manager.flutter/browser"
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, openBrowserChannel).setMethodCallHandler { call, result ->
|
||||
if (call.method == "openBrowser") {
|
||||
val searchQuery = call.argument<String>("query")
|
||||
openBrowser(searchQuery)
|
||||
result.success(null)
|
||||
} else {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
val mainChannel =
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, patcherChannel)
|
||||
@@ -171,11 +189,32 @@ class MainActivity : FlutterActivity() {
|
||||
}.toString().let(result::success)
|
||||
}
|
||||
|
||||
"installApk" -> {
|
||||
val apkPath = call.argument<String>("apkPath")!!
|
||||
PackageInstallerManager.result = result
|
||||
installApk(apkPath)
|
||||
}
|
||||
|
||||
"uninstallApp" -> {
|
||||
val packageName = call.argument<String>("packageName")!!
|
||||
uninstallApp(packageName)
|
||||
PackageInstallerManager.result = result
|
||||
}
|
||||
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openBrowser(query: String?) {
|
||||
val intent = Intent(Intent.ACTION_WEB_SEARCH).apply {
|
||||
putExtra(SearchManager.QUERY, query)
|
||||
}
|
||||
if (intent.resolveActivity(packageManager) != null) {
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(InternalCoroutinesApi::class)
|
||||
private fun runPatcher(
|
||||
result: MethodChannel.Result,
|
||||
@@ -327,7 +366,7 @@ class MainActivity : FlutterActivity() {
|
||||
return@Thread
|
||||
}
|
||||
|
||||
updateProgress(0.8, "Building...", "")
|
||||
updateProgress(0.75, "Building...", "")
|
||||
|
||||
val res = patcher.get()
|
||||
patcher.close()
|
||||
@@ -360,17 +399,11 @@ class MainActivity : FlutterActivity() {
|
||||
return@Thread
|
||||
}
|
||||
|
||||
updateProgress(0.9, "Signing...", "Signing APK")
|
||||
updateProgress(0.8, "Signing...", "Signing APK")
|
||||
|
||||
try {
|
||||
Signer("ReVanced", keystorePassword)
|
||||
.signApk(patchedFile, outFile, keyStoreFile)
|
||||
} catch (e: Exception) {
|
||||
print("Error signing APK: ${e.message}")
|
||||
e.printStackTrace()
|
||||
}
|
||||
Signer("ReVanced", keystorePassword).signApk(patchedFile, outFile, keyStoreFile)
|
||||
|
||||
updateProgress(1.0, "Patched", "Patched")
|
||||
updateProgress(.85, "Patched", "Patched APK")
|
||||
} catch (ex: Throwable) {
|
||||
if (!cancel) {
|
||||
val stack = ex.stackTraceToString()
|
||||
@@ -385,4 +418,44 @@ class MainActivity : FlutterActivity() {
|
||||
handler.post { result.success(null) }
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun installApk(apkPath: String) {
|
||||
val packageInstaller: PackageInstaller = applicationContext.packageManager.packageInstaller
|
||||
val sessionParams = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
||||
val sessionId: Int = packageInstaller.createSession(sessionParams)
|
||||
val session: PackageInstaller.Session = packageInstaller.openSession(sessionId)
|
||||
session.use { activeSession ->
|
||||
val sessionOutputStream = activeSession.openWrite(applicationContext.packageName, 0, -1)
|
||||
sessionOutputStream.use { outputStream ->
|
||||
val apkFile = File(apkPath)
|
||||
apkFile.inputStream().use { inputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
val receiverIntent = Intent(applicationContext, InstallerReceiver::class.java).apply {
|
||||
action = "APP_INSTALL_ACTION"
|
||||
}
|
||||
val receiverPendingIntent = PendingIntent.getBroadcast(context, sessionId, receiverIntent, PackageInstallerManager.flags)
|
||||
session.commit(receiverPendingIntent.intentSender)
|
||||
session.close()
|
||||
}
|
||||
|
||||
private fun uninstallApp(packageName: String) {
|
||||
val packageInstaller: PackageInstaller = applicationContext.packageManager.packageInstaller
|
||||
val receiverIntent = Intent(applicationContext, UninstallerReceiver::class.java).apply {
|
||||
action = "APP_UNINSTALL_ACTION"
|
||||
}
|
||||
val receiverPendingIntent = PendingIntent.getBroadcast(context, 0, receiverIntent, PackageInstallerManager.flags)
|
||||
packageInstaller.uninstall(packageName, receiverPendingIntent.intentSender)
|
||||
}
|
||||
|
||||
object PackageInstallerManager {
|
||||
var result: MethodChannel.Result? = null
|
||||
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package app.revanced.manager.flutter.utils.packageInstaller
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import app.revanced.manager.flutter.MainActivity
|
||||
|
||||
class InstallerReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)) {
|
||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
|
||||
if (confirmationIntent != null) {
|
||||
context.startActivity(confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
val packageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
|
||||
val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
||||
val otherPackageName = intent.getStringExtra(PackageInstaller.EXTRA_OTHER_PACKAGE_NAME)
|
||||
MainActivity.PackageInstallerManager.result!!.success(mapOf(
|
||||
"status" to status,
|
||||
"packageName" to packageName,
|
||||
"message" to message,
|
||||
"otherPackageName" to otherPackageName
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package app.revanced.manager.flutter.utils.packageInstaller
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import app.revanced.manager.flutter.MainActivity
|
||||
|
||||
class UninstallerReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)) {
|
||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
|
||||
if (confirmationIntent != null) {
|
||||
context.startActivity(confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
MainActivity.PackageInstallerManager.result!!.success(status)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,13 +22,14 @@ allprojects {
|
||||
}
|
||||
}
|
||||
|
||||
layout.buildDirectory.set(file("../build"))
|
||||
var root = layout.buildDirectory.get().asFile.absolutePath
|
||||
rootProject.buildDir = '../build'
|
||||
subprojects {
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
subprojects {
|
||||
project.layout.buildDirectory.set(file("$root/${project.name}"))
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
tasks.register("clean", Delete) {
|
||||
delete layout.buildDirectory
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=9d926787066a081739e8200858338b4a69e837c3a821a33aca9db09dd4a41026
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
include ':app'
|
||||
pluginManagement {
|
||||
def flutterSdkPath = {
|
||||
def properties = new Properties()
|
||||
file("local.properties").withInputStream { properties.load(it) }
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
return flutterSdkPath
|
||||
}
|
||||
settings.ext.flutterSdkPath = flutterSdkPath()
|
||||
|
||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
||||
def properties = new Properties()
|
||||
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
|
||||
|
||||
assert localPropertiesFile.exists()
|
||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
||||
plugins {
|
||||
id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false
|
||||
}
|
||||
}
|
||||
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
||||
include ":app"
|
||||
|
||||
apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
"noShowAgain": "Don't show this again",
|
||||
"add": "Add",
|
||||
"remove": "Remove",
|
||||
"showChangelogButton": "Show changelog",
|
||||
"showUpdateButton": "Show update",
|
||||
"navigationView": {
|
||||
"dashboardTab": "Dashboard",
|
||||
"patcherTab": "Patcher",
|
||||
@@ -27,6 +29,7 @@
|
||||
|
||||
"updatesSubtitle": "Updates",
|
||||
"patchedSubtitle": "Patched apps",
|
||||
"changeLaterSubtitle": "You can change this in the settings at a later time.",
|
||||
|
||||
"noUpdates": "No updates available",
|
||||
|
||||
@@ -35,20 +38,25 @@
|
||||
"noInstallations": "No patched apps installed",
|
||||
"installUpdate": "Continue to install the update?",
|
||||
|
||||
"updateDialogTitle": "Update Manager",
|
||||
"updatePatchesDialogTitle": "Update ReVanced Patches",
|
||||
"updateSheetTitle": "Update ReVanced Manager",
|
||||
"updateDialogTitle": "New update available",
|
||||
"updatePatchesSheetTitle": "Update ReVanced Patches",
|
||||
"updateChangelogTitle": "Changelog",
|
||||
|
||||
"patchesConsentDialogText": "ReVanced Patches needs to be downloaded.",
|
||||
"patchesConsentDialogText2": "This will connect you to {url}.",
|
||||
"patchesConsentDialogText3": "Auto update?",
|
||||
"patchesConsentDialogText3Sub": "You can change this in settings at a later time.",
|
||||
"updateDialogText": "A new update is available for {file}.\n\nThe currently installed version is {version}.",
|
||||
|
||||
"downloadConsentDialogTitle": "Download necessary files?",
|
||||
"downloadConsentDialogText": "ReVanced Manager needs to download necessary files to work properly.",
|
||||
"downloadConsentDialogText2": "This will connect you to {url}.",
|
||||
|
||||
"checkUpdateDialogTitle": "Check for updates?",
|
||||
"checkUpdateDialogText": "Do you want ReVanced Manager to check for updates automatically?",
|
||||
|
||||
"notificationTitle": "Update downloaded",
|
||||
"notificationText": "Tap to install the update",
|
||||
|
||||
"downloadingMessage": "Downloading update...",
|
||||
"downloadedMessage": "Update downloaded!",
|
||||
"downloadedMessage": "Update downloaded",
|
||||
|
||||
"installingMessage": "Installing update...",
|
||||
|
||||
@@ -72,22 +80,21 @@
|
||||
"widgetTitle": "Patcher",
|
||||
"patchButton": "Patch",
|
||||
|
||||
"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. Continue 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\nContinue anyways?",
|
||||
"requiredOptionDialogText" : "Some patch options have to be set."
|
||||
},
|
||||
"appSelectorCard": {
|
||||
"widgetTitle": "Select an application",
|
||||
"widgetTitleSelected": "Selected application",
|
||||
"widgetSubtitle": "No application selected",
|
||||
"widgetTitle": "Select an app",
|
||||
"widgetTitleSelected": "Selected app",
|
||||
"widgetSubtitle": "No app selected",
|
||||
|
||||
"noAppsLabel": "No applications found",
|
||||
"notInstalled":"App not installed",
|
||||
|
||||
"currentVersion": "Current",
|
||||
"suggestedVersion": "Suggested",
|
||||
"allVersions": "All versions"
|
||||
"anyVersion": "Any version"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Select patches",
|
||||
@@ -101,18 +108,20 @@
|
||||
"widgetSubtitle": "We are online!"
|
||||
},
|
||||
"appSelectorView": {
|
||||
"viewTitle": "Select an application",
|
||||
"searchBarHint": "Search applications",
|
||||
"viewTitle": "Select an app",
|
||||
"searchBarHint": "Search app",
|
||||
|
||||
"storageButton": "Storage",
|
||||
"selectFromStorageButton": "Select from storage",
|
||||
|
||||
|
||||
"errorMessage": "Unable to use selected application",
|
||||
|
||||
"downloadToast": "Download function is not available yet",
|
||||
|
||||
"requireSuggestedAppVersionDialogText": "The version of the app you have selected does not match the suggested version which can lead to unexpected issues. Please use the suggested version.\n\nSelected version: {selected}\nSuggested version: {suggested}\n\nTo continue anyway, disable \"Require suggested app version\" in the settings.",
|
||||
|
||||
"featureNotAvailable": "Feature not implemented",
|
||||
"featureNotAvailableText": "This application is a split APK and cannot be selected. Unfortunately, this feature is only available for rooted users at the moment. However, you can still install the application by selecting its APK files from your device's storage instead"
|
||||
"featureNotAvailableText": "This app is a split APK and can only be patched and installed reliably by mounting with root permissions. However, you can patch and install a full APK by selecting it from storage."
|
||||
},
|
||||
"patchesSelectorView": {
|
||||
"viewTitle": "Select patches",
|
||||
@@ -156,22 +165,24 @@
|
||||
"unsupportedPatchVersion": "Patch is not supported for this app version.",
|
||||
"unsupportedRequiredOption": "This patch contains a required option that is not supported by this app",
|
||||
|
||||
"patchesChangeWarningDialogText": "It is recommended to use the default patch selection and options. Changing them may result in unexpected issues.\n\nYou'll need to turn on \"Allow changing patch selection\" in settings before changing any patch selection.",
|
||||
"patchesChangeWarningDialogText": "It is recommended to use the default patch selection and options. Changing them may result in unexpected issues.\n\nYou'll need to turn on \"Allow changing patch selection\" in the settings before changing any patch selection.",
|
||||
"patchesChangeWarningDialogButton": "Use default selection"
|
||||
},
|
||||
"installerView": {
|
||||
"widgetTitle": "Installer",
|
||||
"installType": "Select install type",
|
||||
"installTypeDescription": "Select the installation type to proceed with.",
|
||||
"installTypeDescription": "Select the installation type to continue with.",
|
||||
|
||||
"installButton": "Install",
|
||||
"installRootType": "Mount",
|
||||
"installNonRootType": "Normal",
|
||||
"installNonRootType": "Regular",
|
||||
|
||||
"warning": "Disable auto updates for the patched app to avoid unexpected issues.",
|
||||
|
||||
"pressBackAgain": "Press back again to cancel",
|
||||
"openButton": "Open",
|
||||
"shareButton": "Share file",
|
||||
|
||||
|
||||
"notificationTitle": "ReVanced Manager is patching",
|
||||
"notificationText": "Tap to return to the installer",
|
||||
|
||||
@@ -204,7 +215,7 @@
|
||||
"englishOption": "English",
|
||||
|
||||
"sourcesLabel": "Sources",
|
||||
"sourcesLabelHint": "Configure your sources",
|
||||
"sourcesLabelHint": "Configure the source of patches and integrations",
|
||||
"sourcesIntegrationsLabel": "Integrations source",
|
||||
"sourcesResetDialogTitle": "Reset",
|
||||
"sourcesResetDialogText": "Are you sure you want to reset your sources to their default values?",
|
||||
@@ -212,13 +223,13 @@
|
||||
"sourcesUpdateNote": "Note: Patches will be updated to the latest version automatically.\n\nThis will reveal your IP address to the server.",
|
||||
|
||||
"apiURLLabel": "API URL",
|
||||
"apiURLHint": "Configure your API URL",
|
||||
"apiURLHint": "Configure the URL of the API to use",
|
||||
"selectApiURL": "API URL",
|
||||
"hostRepositoryLabel": "Repository API",
|
||||
"orgPatchesLabel": "Patches organization",
|
||||
"sourcesPatchesLabel": "Patches source",
|
||||
"orgIntegrationsLabel": "Integrations organization",
|
||||
|
||||
|
||||
"contributorsLabel": "Contributors",
|
||||
"contributorsHint": "A list of contributors of ReVanced",
|
||||
|
||||
@@ -226,16 +237,23 @@
|
||||
"logsHint": "Share ReVanced Manager logs",
|
||||
|
||||
"enablePatchesSelectionLabel": "Allow changing patch selection",
|
||||
"enablePatchesSelectionHint": "Allow changing the selection of patches",
|
||||
"enablePatchesSelectionHint": "Do not prevent selecting or deselecting patches",
|
||||
"enablePatchesSelectionWarningText": "Changing the 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",
|
||||
"autoUpdatePatchesHint": "Automatically update patches to the latest version",
|
||||
"showUpdateDialogLabel": "Show update dialog",
|
||||
"showUpdateDialogHint": "Show a dialog when a new update is available",
|
||||
"universalPatchesLabel": "Show universal patches",
|
||||
"universalPatchesHint": "Display all apps and universal patches (may slow down the app list)",
|
||||
|
||||
"versionCompatibilityCheckLabel": "Version compatibility check",
|
||||
"versionCompatibilityCheckHint": "Restricts patches to supported app versions",
|
||||
"versionCompatibilityCheckHint": "Prevent selecting patches that are not compatible with the selected app version",
|
||||
|
||||
"requireSuggestedAppVersionLabel": "Require suggested app version",
|
||||
"requireSuggestedAppVersionHint": "Prevent selecting an app with a version that is not the suggested",
|
||||
"requireSuggestedAppVersionDialogText": "Selecting an app that is not the suggested version may cause unexpected issues.\n\nDo you want to proceed anyways?",
|
||||
|
||||
"aboutLabel": "About",
|
||||
"snackbarMessage": "Copied to clipboard",
|
||||
@@ -296,22 +314,23 @@
|
||||
"widgetTitle": "App info",
|
||||
"openButton": "Open",
|
||||
"uninstallButton": "Uninstall",
|
||||
"unpatchButton": "Unpatch",
|
||||
"unmountButton": "Unmount",
|
||||
"rootDialogTitle": "Error",
|
||||
|
||||
"unpatchDialogText": "Are you sure you want to unpatch this app?",
|
||||
"unmountDialogText": "Are you sure you want to unmount this app?",
|
||||
"uninstallDialogText": "Are you sure you want to uninstall this app?",
|
||||
"rootDialogText": "App was installed with superuser permissions, but currently ReVanced Manager has no permissions.\nPlease grant superuser permissions first.",
|
||||
|
||||
"packageNameLabel": "Package name",
|
||||
"installTypeLabel": "Installation type",
|
||||
"rootTypeLabel": "Root",
|
||||
"nonRootTypeLabel": "Non-root",
|
||||
"mountTypeLabel": "Mount",
|
||||
"regularTypeLabel": "Regular",
|
||||
"patchedDateLabel": "Patched date",
|
||||
"appliedPatchesLabel": "Applied patches",
|
||||
|
||||
"patchedDateHint": "{date} at {time}",
|
||||
"appliedPatchesHint": "{quantity} applied patches",
|
||||
|
||||
|
||||
"updateNotImplemented": "This feature has not been implemented yet"
|
||||
},
|
||||
"contributorsView": {
|
||||
@@ -321,5 +340,34 @@
|
||||
"integrationsContributors": "Integrations contributors",
|
||||
"cliContributors": "CLI contributors",
|
||||
"managerContributors": "Manager contributors"
|
||||
},
|
||||
"installErrorDialog": {
|
||||
"mount_version_mismatch": "Version mismatch",
|
||||
"mount_no_root": "No root access",
|
||||
"mount_missing_installation": "Installation not found",
|
||||
|
||||
"status_failure_blocked": "Installation blocked",
|
||||
"install_failed_verification_failure": "Verification failed",
|
||||
"status_failure_invalid": "Installation invalid",
|
||||
"install_failed_version_downgrade": "Can't downgrade",
|
||||
"status_failure_conflict": "Installation conflict",
|
||||
"status_failure_storage": "Installation storage issue",
|
||||
"status_failure_incompatible": "Installation incompatible",
|
||||
"status_failure_timeout": "Installation timeout",
|
||||
"status_unknown": "Installation failed",
|
||||
|
||||
"mount_version_mismatch_description": "The installation failed due to the installed app being a different version than the patched app.\n\nInstall the version of the app you are mounting and try again.",
|
||||
"mount_no_root_description": "The installation failed due to root access not being granted.\n\nGrant root access to ReVanced Manager and try again.",
|
||||
"mount_missing_installation_description": "The installation failed due to the unpatched app not being installed on this device in order to mount over it.\n\nInstall the unpatched app before mounting and try again.",
|
||||
|
||||
"status_failure_timeout_description": "The installation took too long to finish.\n\nWould you like to try again?",
|
||||
"status_failure_storage_description": "The installation failed due to insufficient storage.\n\nFree up some space and try again.",
|
||||
"status_failure_invalid_description": "The installation failed due to the patched app being invalid.\n\nUninstall the app and try again?",
|
||||
"status_failure_incompatible_description": "The app is incompatible with this device.\n\nContact the developer of the app and ask for support.",
|
||||
"status_failure_conflict_description": "The installation was prevented by an existing installation of the app.\n\nUninstall the installed app and try again?",
|
||||
"status_failure_blocked_description": "The installation was blocked by {packageName}.\n\nAdjust your security settings and try again.",
|
||||
"install_failed_verification_failure_description": "The installation failed due to a verification issue.\n\nAdjust your security settings and try again.",
|
||||
"install_failed_version_downgrade_description": "The installation failed due to the patched app being a lower version than the installed app.\n\nUninstall the app and try again?",
|
||||
"status_unknown_description": "The installation failed due to an unknown reason. Please try again."
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 11 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 11 KiB |
1
assets/revanced-logo/revanced-logo-round.svg
Normal file
1
assets/revanced-logo/revanced-logo-round.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 800 800" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><g id="Logo"><g id="Ring"><circle id="Ring-Background" serif:id="Ring Background" cx="400" cy="400" r="400" style="fill:#1b1b1b;"/><path id="Ring1" serif:id="Ring" d="M400,0c220.766,0 400,179.234 400,400c-0,220.766 -179.234,400 -400,400c-220.766,-0 -400,-179.234 -400,-400c0,-220.766 179.234,-400 400,-400Zm-0,36c200.897,-0 364,163.103 364,364c0,200.897 -163.103,364 -364,364c-200.897,0 -364,-163.103 -364,-364c-0,-200.897 163.103,-364 364,-364Z" style="fill:url(#_Linear1);"/></g><g id="Shape"><path id="V-Shape" serif:id="V Shape" d="M538.74,269.872c1.481,-3.382 1.157,-7.283 -0.863,-10.373c-2.021,-3.091 -5.464,-4.954 -9.156,-4.954c-5.148,0 -10.435,0 -14.165,0c-3.1,0 -5.907,1.834 -7.153,4.672c-12.468,28.396 -78.273,178.273 -100.25,228.328c-1.246,2.838 -4.053,4.671 -7.154,4.671c-3.1,0 -5.907,-1.833 -7.153,-4.671c-21.977,-50.055 -87.782,-199.932 -100.25,-228.328c-1.246,-2.838 -4.053,-4.672 -7.153,-4.672c-3.73,0 -9.017,0 -14.164,0c-3.693,0 -7.135,1.863 -9.156,4.954c-2.02,3.09 -2.344,6.991 -0.863,10.373c23.557,53.766 101.872,232.519 117.871,269.034c1.743,3.979 5.674,6.549 10.018,6.549c6.293,-0 15.408,-0 21.701,-0c4.344,-0 8.275,-2.57 10.018,-6.549c15.999,-36.515 94.315,-215.268 117.872,-269.034Z" style="fill:#fff;"/><path id="Diamond" d="M408.119,395.312c-1.675,2.901 -4.77,4.688 -8.119,4.688c-3.349,-0 -6.444,-1.787 -8.119,-4.688c-16.997,-29.44 -56.156,-97.264 -73.153,-126.704c-1.675,-2.901 -1.675,-6.474 0,-9.375c1.675,-2.901 4.77,-4.688 8.119,-4.688c33.995,0 112.311,0 146.306,0c3.349,0 6.444,1.787 8.119,4.688c1.675,2.901 1.675,6.474 -0,9.375c-16.997,29.44 -56.156,97.264 -73.153,126.704Z" style="fill:url(#_Linear2);"/></g></g><defs><linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(4.89859e-14,800,-800,4.89859e-14,400.001,3.31681e-10)"><stop offset="0" style="stop-color:#f04e98;stop-opacity:1"/><stop offset="0.5" style="stop-color:#5f65d4;stop-opacity:1"/><stop offset="1" style="stop-color:#4e98f0;stop-opacity:1"/></linearGradient><linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.77155e-14,289.317,-282.535,1.73003e-14,400,254.545)"><stop offset="0" style="stop-color:#f04e98;stop-opacity:1"/><stop offset="0.5" style="stop-color:#5f65d4;stop-opacity:1"/><stop offset="1" style="stop-color:#4e98f0;stop-opacity:1"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -5,14 +5,16 @@ In case you encounter any issues while using ReVanced Manager, please refer to t
|
||||
- 💉 Patching fails with an error
|
||||
|
||||
Make sure ReVanced Manager is up to date by following [🔄 Updating ReVanced Manager](2_3_updating.md) and select the **Default** button when choosing patches.
|
||||
|
||||
|
||||
- 🚫 App not installed as package conflicts with an existing package
|
||||
|
||||
An existing installation of the app you're trying to patch is conflicting with the patched app. Uninstall the existing app before installing the patched app.
|
||||
An existing installation of the app you're trying to patch conflicts with the patched app (i.e., signature mismatch or downgrade). Uninstall the existing app before installing the patched app.
|
||||
|
||||
- ❗️ Error code `135`, `139` or `1` when patching the app
|
||||
|
||||
Your device is not supported. Refer to the [Prerequisites](0_prerequisites.md) page for supported devices.
|
||||
You may be trying to patch a split APK[^1]. This may not work under certain circumstances. In such a case, patch a full APK.
|
||||
|
||||
Your device may otherwise be unsupported. Please look at the [Prerequisites](0_prerequisites.md) page for supported devices.
|
||||
|
||||
Alternatively, you can use [ReVanced CLI](https://github.com/revanced/revanced-cli) to patch the app.
|
||||
|
||||
@@ -25,3 +27,5 @@ 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.
|
||||
|
||||
Continue: [🔨 Building from source](4_building.md)
|
||||
|
||||
[^1]: https://developer.android.com/guide/app-bundle/app-bundle-format
|
||||
|
||||
@@ -2,29 +2,31 @@
|
||||
|
||||
This page will guide you through building ReVanced Manager from source.
|
||||
|
||||
1. Setup the Flutter environment for your [platform](https://docs.flutter.dev/get-started/install)
|
||||
1\. Setup the Flutter environment for your [platform](https://docs.flutter.dev/get-started/install)
|
||||
|
||||
2. Clone the repository
|
||||
2\. Clone the repository
|
||||
|
||||
```sh
|
||||
git clone https://github.com/revanced/revanced-manager.git && cd revanced-manager
|
||||
```
|
||||
3. Get dependencies
|
||||
3\. Get dependencies
|
||||
|
||||
```sh
|
||||
flutter pub get
|
||||
```
|
||||
|
||||
4. Delete conflicting outputs
|
||||
4\. Delete conflicting outputs
|
||||
|
||||
> [!TIP]
|
||||
> Must be run every time you sync your local repository with the remote repository.
|
||||
|
||||
```sh
|
||||
flutter packages pub run build_runner build --delete-conflicting-outputs
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
```
|
||||
|
||||
> [!Note]
|
||||
> Must be run every time you sync your local repository with the remote repository.
|
||||
|
||||
5. Build the APK
|
||||
|
||||
5\. Build the APK
|
||||
|
||||
```sh
|
||||
flutter build apk
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
7
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,7 +0,0 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
249
gradlew
vendored
249
gradlew
vendored
@@ -1,249 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
92
gradlew.bat
vendored
92
gradlew.bat
vendored
@@ -1,92 +0,0 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -8,6 +8,7 @@ import 'package:revanced_manager/services/download_manager.dart';
|
||||
import 'package:revanced_manager/services/github_api.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/services/revanced_api.dart';
|
||||
import 'package:revanced_manager/services/root_api.dart';
|
||||
import 'package:revanced_manager/ui/theme/dynamic_theme_builder.dart';
|
||||
import 'package:revanced_manager/ui/views/navigation/navigation_view.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@@ -24,13 +25,20 @@ Future main() async {
|
||||
final String repoUrl = locator<ManagerAPI>().getRepoUrl();
|
||||
locator<GithubAPI>().initialize(repoUrl);
|
||||
tz.initializeTimeZones();
|
||||
|
||||
// TODO(aAbed): remove in the future, keep it for now during migration.
|
||||
final rootAPI = RootAPI();
|
||||
if (await rootAPI.hasRootPermissions()) {
|
||||
await rootAPI.removeOrphanedFiles();
|
||||
}
|
||||
|
||||
prefs = await SharedPreferences.getInstance();
|
||||
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({Key? key}) : super(key: key);
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -75,9 +75,8 @@ class Option {
|
||||
if (json['valueType'] == null) {
|
||||
final type = json['optionClassType'];
|
||||
if (type is String) {
|
||||
json['valueType'] = type
|
||||
.replaceAll('PatchOption', '')
|
||||
.replaceAll('List', 'Array');
|
||||
json['valueType'] =
|
||||
type.replaceAll('PatchOption', '').replaceAll('List', 'Array');
|
||||
|
||||
json['optionClassType'] = null;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ class DownloadManager {
|
||||
);
|
||||
|
||||
Future<void> initialize() async {
|
||||
_userAgent = 'ReVanced-Manager/${await _managerAPI.getCurrentManagerVersion()}';
|
||||
_userAgent =
|
||||
'ReVanced-Manager/${await _managerAPI.getCurrentManagerVersion()}';
|
||||
}
|
||||
|
||||
Dio initDio(String url) {
|
||||
@@ -72,4 +73,3 @@ class DownloadManager {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ class GithubAPI {
|
||||
int updates = 0;
|
||||
final String currentVersion =
|
||||
await _managerAPI.getCurrentManagerVersion();
|
||||
while (response.data[updates]['tag_name'] != 'v$currentVersion') {
|
||||
while (response.data[updates]['tag_name'] != currentVersion) {
|
||||
updates++;
|
||||
}
|
||||
for (int i = 1; i < updates; i++) {
|
||||
|
||||
@@ -15,7 +15,8 @@ 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/root_api.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||
import 'package:revanced_manager/services/toast.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_checkbox_list_tile.dart';
|
||||
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:timeago/timeago.dart';
|
||||
@@ -24,6 +25,7 @@ import 'package:timeago/timeago.dart';
|
||||
class ManagerAPI {
|
||||
final RevancedAPI _revancedAPI = locator<RevancedAPI>();
|
||||
final GithubAPI _githubAPI = locator<GithubAPI>();
|
||||
final Toast _toast = locator<Toast>();
|
||||
final RootAPI _rootAPI = RootAPI();
|
||||
final String patcherRepo = 'revanced-patcher';
|
||||
final String cliRepo = 'revanced-cli';
|
||||
@@ -34,6 +36,7 @@ class ManagerAPI {
|
||||
Patch? selectedPatch;
|
||||
BuildContext? ctx;
|
||||
bool isRooted = false;
|
||||
bool suggestedAppVersionSelected = true;
|
||||
bool isDynamicThemeAvailable = false;
|
||||
String storedPatchesFile = '/selected-patches.json';
|
||||
String keystoreFile =
|
||||
@@ -50,20 +53,31 @@ class ManagerAPI {
|
||||
String? integrationsVersion = '';
|
||||
|
||||
bool isDefaultPatchesRepo() {
|
||||
return getPatchesRepo().toLowerCase() == 'revanced/revanced-patches';
|
||||
return getPatchesRepo().toLowerCase() == defaultPatchesRepo;
|
||||
}
|
||||
|
||||
bool isDefaultIntegrationsRepo() {
|
||||
return getIntegrationsRepo().toLowerCase() ==
|
||||
'revanced/revanced-integrations';
|
||||
defaultIntegrationsRepo;
|
||||
}
|
||||
|
||||
Future<void> initialize() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
isRooted = await _rootAPI.isRooted();
|
||||
isDynamicThemeAvailable = (await getSdkVersion()) >= 31; // ANDROID_12_SDK_VERSION = 31
|
||||
isDynamicThemeAvailable =
|
||||
(await getSdkVersion()) >= 31; // ANDROID_12_SDK_VERSION = 31
|
||||
storedPatchesFile =
|
||||
(await getApplicationDocumentsDirectory()).path + storedPatchesFile;
|
||||
|
||||
// Migrate to new API URL if not done yet as the old one is sunset.
|
||||
final bool hasMigrated = _prefs.getBool('migratedToNewApiUrl') ?? false;
|
||||
if (!hasMigrated) {
|
||||
final String apiUrl = getApiUrl().toLowerCase();
|
||||
if (apiUrl.contains('releases.revanced.app')) {
|
||||
await setApiUrl(''); // Reset to default.
|
||||
_prefs.setBool('migratedToNewApiUrl', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> getSdkVersion() async {
|
||||
@@ -81,6 +95,7 @@ class ManagerAPI {
|
||||
}
|
||||
await _revancedAPI.clearAllCache();
|
||||
await _prefs.setString('apiUrl', url);
|
||||
_toast.showBottom('settingsView.restartAppForChanges');
|
||||
}
|
||||
|
||||
String getRepoUrl() {
|
||||
@@ -113,12 +128,12 @@ class ManagerAPI {
|
||||
await _prefs.setString('patchesRepo', value);
|
||||
}
|
||||
|
||||
bool getPatchesConsent() {
|
||||
return _prefs.getBool('patchesConsent') ?? false;
|
||||
bool getDownloadConsent() {
|
||||
return _prefs.getBool('downloadConsent') ?? false;
|
||||
}
|
||||
|
||||
Future<void> setPatchesConsent(bool consent) async {
|
||||
await _prefs.setBool('patchesConsent', consent);
|
||||
void setDownloadConsent(bool consent) {
|
||||
_prefs.setBool('downloadConsent', consent);
|
||||
}
|
||||
|
||||
bool isPatchesAutoUpdate() {
|
||||
@@ -141,6 +156,14 @@ class ManagerAPI {
|
||||
_prefs.setBool('showPatchesChangeWarning', !value);
|
||||
}
|
||||
|
||||
bool showUpdateDialog() {
|
||||
return _prefs.getBool('showUpdateDialog') ?? true;
|
||||
}
|
||||
|
||||
void setShowUpdateDialog(bool value) {
|
||||
_prefs.setBool('showUpdateDialog', value);
|
||||
}
|
||||
|
||||
bool isChangingToggleModified() {
|
||||
return _prefs.getBool('isChangingToggleModified') ?? false;
|
||||
}
|
||||
@@ -149,8 +172,8 @@ class ManagerAPI {
|
||||
_prefs.setBool('isChangingToggleModified', value);
|
||||
}
|
||||
|
||||
Future<void> setPatchesAutoUpdate(bool value) async {
|
||||
await _prefs.setBool('patchesAutoUpdate', value);
|
||||
void setPatchesAutoUpdate(bool value) {
|
||||
_prefs.setBool('patchesAutoUpdate', value);
|
||||
}
|
||||
|
||||
List<Patch> getSavedPatches(String packageName) {
|
||||
@@ -259,6 +282,14 @@ class ManagerAPI {
|
||||
await _prefs.setBool('versionCompatibilityCheckEnabled', value);
|
||||
}
|
||||
|
||||
bool isRequireSuggestedAppVersionEnabled() {
|
||||
return _prefs.getBool('requireSuggestedAppVersionEnabled') ?? true;
|
||||
}
|
||||
|
||||
Future<void> enableRequireSuggestedAppVersionStatus(bool value) async {
|
||||
await _prefs.setBool('requireSuggestedAppVersionEnabled', value);
|
||||
}
|
||||
|
||||
Future<void> setKeystorePassword(String password) async {
|
||||
await _prefs.setString('keystorePassword', password);
|
||||
}
|
||||
@@ -485,7 +516,11 @@ class ManagerAPI {
|
||||
|
||||
Future<String> getCurrentManagerVersion() async {
|
||||
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
return packageInfo.version;
|
||||
String version = packageInfo.version;
|
||||
if (!version.startsWith('v')) {
|
||||
version = 'v$version';
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
Future<String> getCurrentPatchesVersion() async {
|
||||
@@ -573,10 +608,9 @@ class ManagerAPI {
|
||||
return showDialog(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (context) => WillPopScope(
|
||||
onWillPop: () async => false,
|
||||
builder: (context) => PopScope(
|
||||
canPop: false,
|
||||
child: AlertDialog(
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
title: I18nText('warning'),
|
||||
content: ValueListenableBuilder(
|
||||
valueListenable: noShow,
|
||||
@@ -596,7 +630,7 @@ class ManagerAPI {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
CheckboxListTile(
|
||||
HapticCheckboxListTile(
|
||||
value: value,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: I18nText(
|
||||
@@ -611,12 +645,12 @@ class ManagerAPI {
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
CustomMaterialButton(
|
||||
label: I18nText('okButton'),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
setPatchesChangeWarning(noShow.value);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: I18nText('okButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -624,7 +658,7 @@ class ManagerAPI {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> reAssessSavedApps() async {
|
||||
Future<void> rePatchedSavedApps() async {
|
||||
final List<PatchedApplication> patchedApps = getPatchedApps();
|
||||
|
||||
// Remove apps that are not installed anymore.
|
||||
|
||||
@@ -3,10 +3,11 @@ import 'dart:io';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:device_apps/device_apps.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
|
||||
import 'package:flutter_i18n/widgets/I18nText.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:install_plugin/install_plugin.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/models/patch.dart';
|
||||
@@ -18,7 +19,7 @@ import 'package:share_plus/share_plus.dart';
|
||||
@lazySingleton
|
||||
class PatcherAPI {
|
||||
static const patcherChannel =
|
||||
MethodChannel('app.revanced.manager.flutter/patcher');
|
||||
MethodChannel('app.revanced.manager.flutter/patcher');
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final RootAPI _rootAPI = RootAPI();
|
||||
late Directory _dataDir;
|
||||
@@ -79,7 +80,8 @@ class PatcherAPI {
|
||||
}
|
||||
|
||||
Future<List<ApplicationWithIcon>> getFilteredInstalledApps(
|
||||
bool showUniversalPatches,) async {
|
||||
bool showUniversalPatches,
|
||||
) async {
|
||||
final List<ApplicationWithIcon> filteredApps = [];
|
||||
final bool allAppsIncluded =
|
||||
_universalPatches.isNotEmpty && showUniversalPatches;
|
||||
@@ -121,11 +123,11 @@ class PatcherAPI {
|
||||
final List<Patch> patches = _patches
|
||||
.where(
|
||||
(patch) =>
|
||||
patch.compatiblePackages.isEmpty ||
|
||||
!patch.name.contains('settings') &&
|
||||
patch.compatiblePackages
|
||||
.any((pack) => pack.name == packageName),
|
||||
)
|
||||
patch.compatiblePackages.isEmpty ||
|
||||
!patch.name.contains('settings') &&
|
||||
patch.compatiblePackages
|
||||
.any((pack) => pack.name == packageName),
|
||||
)
|
||||
.toList();
|
||||
if (!_managerAPI.areUniversalPatchesEnabled()) {
|
||||
filteredPatches[packageName] = patches
|
||||
@@ -137,22 +139,27 @@ class PatcherAPI {
|
||||
return filteredPatches[packageName];
|
||||
}
|
||||
|
||||
Future<List<Patch>> getAppliedPatches(List<String> appliedPatches,) async {
|
||||
Future<List<Patch>> getAppliedPatches(
|
||||
List<String> appliedPatches,
|
||||
) async {
|
||||
return _patches
|
||||
.where((patch) => appliedPatches.contains(patch.name))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<void> runPatcher(String packageName,
|
||||
String apkFilePath,
|
||||
List<Patch> selectedPatches,) async {
|
||||
Future<void> runPatcher(
|
||||
String packageName,
|
||||
String apkFilePath,
|
||||
List<Patch> selectedPatches,
|
||||
) async {
|
||||
final File? integrationsFile = await _managerAPI.downloadIntegrations();
|
||||
final Map<String, Map<String, dynamic>> options = {};
|
||||
for (final patch in selectedPatches) {
|
||||
if (patch.options.isNotEmpty) {
|
||||
final Map<String, dynamic> patchOptions = {};
|
||||
for (final option in patch.options) {
|
||||
final patchOption = _managerAPI.getPatchOption(packageName, patch.name, option.key);
|
||||
final patchOption =
|
||||
_managerAPI.getPatchOption(packageName, patch.name, option.key);
|
||||
if (patchOption != null) {
|
||||
patchOptions[patchOption.key] = patchOption.value;
|
||||
}
|
||||
@@ -194,131 +201,315 @@ class PatcherAPI {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stopPatcher() async {
|
||||
try {
|
||||
await patcherChannel.invokeMethod('stopPatcher');
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
Future<void> stopPatcher() async {
|
||||
try {
|
||||
await patcherChannel.invokeMethod('stopPatcher');
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> installPatchedFile(PatchedApplication patchedApp) async {
|
||||
if (outFile != null) {
|
||||
try {
|
||||
if (patchedApp.isRooted) {
|
||||
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||
if (hasRootPermissions) {
|
||||
return _rootAPI.installApp(
|
||||
patchedApp.packageName,
|
||||
patchedApp.apkFilePath,
|
||||
outFile!.path,
|
||||
);
|
||||
Future<int> installPatchedFile(
|
||||
BuildContext context,
|
||||
PatchedApplication patchedApp,
|
||||
) async {
|
||||
if (outFile != null) {
|
||||
_managerAPI.ctx = context;
|
||||
try {
|
||||
if (patchedApp.isRooted) {
|
||||
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||
final packageVersion = await DeviceApps.getApp(patchedApp.packageName)
|
||||
.then((app) => app?.versionName);
|
||||
if (!hasRootPermissions) {
|
||||
installErrorDialog(1);
|
||||
} else if (packageVersion == null) {
|
||||
installErrorDialog(1.2);
|
||||
} else if (packageVersion == patchedApp.version) {
|
||||
return await _rootAPI.install(
|
||||
patchedApp.packageName,
|
||||
patchedApp.apkFilePath,
|
||||
outFile!.path,
|
||||
)
|
||||
? 0
|
||||
: 1;
|
||||
} else {
|
||||
installErrorDialog(1.1);
|
||||
}
|
||||
} else {
|
||||
if (await _rootAPI.hasRootPermissions()) {
|
||||
await _rootAPI.uninstall(patchedApp.packageName);
|
||||
}
|
||||
if (context.mounted) {
|
||||
return await installApk(
|
||||
context,
|
||||
outFile!.path,
|
||||
);
|
||||
}
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
Future<int> installApk(
|
||||
BuildContext context,
|
||||
String apkPath,
|
||||
) async {
|
||||
try {
|
||||
final status = await patcherChannel.invokeMethod('installApk', {
|
||||
'apkPath': apkPath,
|
||||
});
|
||||
final int statusCode = status['status'];
|
||||
final String message = status['message'];
|
||||
final bool hasExtra =
|
||||
message.contains('INSTALL_FAILED_VERIFICATION_FAILURE') ||
|
||||
message.contains('INSTALL_FAILED_VERSION_DOWNGRADE');
|
||||
if (statusCode == 0 || (statusCode == 3 && !hasExtra)) {
|
||||
return statusCode;
|
||||
} else {
|
||||
final install = await InstallPlugin.installApk(outFile!.path);
|
||||
return install['isSuccess'];
|
||||
_managerAPI.ctx = context;
|
||||
return await installErrorDialog(
|
||||
statusCode,
|
||||
status,
|
||||
hasExtra,
|
||||
);
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return false;
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void exportPatchedFile(String appName, String version) {
|
||||
try {
|
||||
if (outFile != null) {
|
||||
final String newName = _getFileName(appName, version);
|
||||
FlutterFileDialog.saveFile(
|
||||
params: SaveFileDialogParams(
|
||||
sourceFilePath: outFile!.path,
|
||||
fileName: newName,
|
||||
),
|
||||
);
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void sharePatchedFile(String appName, String version) {
|
||||
try {
|
||||
if (outFile != null) {
|
||||
final String newName = _getFileName(appName, version);
|
||||
final int lastSeparator = outFile!.path.lastIndexOf('/');
|
||||
final String newPath =
|
||||
outFile!.path.substring(0, lastSeparator + 1) + newName;
|
||||
final File shareFile = outFile!.copySync(newPath);
|
||||
Share.shareXFiles([XFile(shareFile.path)]);
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _getFileName(String appName, String version) {
|
||||
final String prefix = appName.toLowerCase().replaceAll(' ', '-');
|
||||
final String newName = '$prefix-revanced_v$version.apk';
|
||||
return newName;
|
||||
}
|
||||
|
||||
Future<void> exportPatcherLog(String logs) async {
|
||||
final Directory appCache = await getTemporaryDirectory();
|
||||
final Directory logDir = Directory('${appCache.path}/logs');
|
||||
logDir.createSync();
|
||||
final String dateTime = DateTime.now()
|
||||
.toIso8601String()
|
||||
.replaceAll('-', '')
|
||||
.replaceAll(':', '')
|
||||
.replaceAll('T', '')
|
||||
.replaceAll('.', '');
|
||||
final String fileName = 'revanced-manager_patcher_$dateTime.txt';
|
||||
final File log = File('${logDir.path}/$fileName');
|
||||
log.writeAsStringSync(logs);
|
||||
FlutterFileDialog.saveFile(
|
||||
params: SaveFileDialogParams(
|
||||
sourceFilePath: log.path,
|
||||
fileName: fileName,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String getSuggestedVersion(String packageName) {
|
||||
final Map<String, int> versions = {};
|
||||
for (final Patch patch in _patches) {
|
||||
final Package? package = patch.compatiblePackages.firstWhereOrNull(
|
||||
(pack) => pack.name == packageName,
|
||||
Future<int> installErrorDialog(
|
||||
num statusCode, [
|
||||
status,
|
||||
bool hasExtra = false,
|
||||
]) async {
|
||||
final String statusValue = InstallStatus.byCode(
|
||||
hasExtra ? double.parse('$statusCode.1') : statusCode,
|
||||
);
|
||||
if (package != null) {
|
||||
for (final String version in package.versions) {
|
||||
versions.update(
|
||||
version,
|
||||
(value) => versions[version]! + 1,
|
||||
ifAbsent: () => 1,
|
||||
bool cleanInstall = false;
|
||||
final bool isFixable = statusCode == 4 || statusCode == 5;
|
||||
await showDialog(
|
||||
context: _managerAPI.ctx!,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
title: I18nText('installErrorDialog.$statusValue'),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
I18nText(
|
||||
'installErrorDialog.${statusValue}_description',
|
||||
translationParams: statusCode == 2
|
||||
? {
|
||||
'packageName': status['otherPackageName'],
|
||||
}
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: (status == null)
|
||||
? <Widget>[
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: I18nText('okButton'),
|
||||
),
|
||||
]
|
||||
: <Widget>[
|
||||
if (!isFixable)
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: I18nText('cancelButton'),
|
||||
)
|
||||
else
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: I18nText('cancelButton'),
|
||||
),
|
||||
if (isFixable)
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
final int response = await patcherChannel.invokeMethod(
|
||||
'uninstallApp',
|
||||
{'packageName': status['packageName']},
|
||||
);
|
||||
if (response == 0 && context.mounted) {
|
||||
cleanInstall = true;
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: I18nText('okButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return cleanInstall ? 10 : 1;
|
||||
}
|
||||
|
||||
void exportPatchedFile(String appName, String version) {
|
||||
try {
|
||||
if (outFile != null) {
|
||||
final String newName = _getFileName(appName, version);
|
||||
FlutterFileDialog.saveFile(
|
||||
params: SaveFileDialogParams(
|
||||
sourceFilePath: outFile!.path,
|
||||
fileName: newName,
|
||||
mimeTypesFilter: ['application/vnd.android.package-archive'],
|
||||
),
|
||||
);
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (versions.isNotEmpty) {
|
||||
final entries = versions.entries.toList()
|
||||
..sort((a, b) => a.value.compareTo(b.value));
|
||||
versions
|
||||
..clear()
|
||||
..addEntries(entries);
|
||||
versions.removeWhere((key, value) => value != versions.values.last);
|
||||
return (versions.keys.toList()
|
||||
..sort()).last;
|
||||
|
||||
void sharePatchedFile(String appName, String version) {
|
||||
try {
|
||||
if (outFile != null) {
|
||||
final String newName = _getFileName(appName, version);
|
||||
final int lastSeparator = outFile!.path.lastIndexOf('/');
|
||||
final String newPath =
|
||||
outFile!.path.substring(0, lastSeparator + 1) + newName;
|
||||
final File shareFile = outFile!.copySync(newPath);
|
||||
Share.shareXFiles([XFile(shareFile.path)]);
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}}
|
||||
|
||||
String _getFileName(String appName, String version) {
|
||||
final String patchVersion = _managerAPI.patchesVersion!;
|
||||
final String prefix = appName.toLowerCase().replaceAll(' ', '-');
|
||||
final String newName =
|
||||
'$prefix-revanced_v$version-patches_$patchVersion.apk';
|
||||
return newName;
|
||||
}
|
||||
|
||||
Future<void> exportPatcherLog(String logs) async {
|
||||
final Directory appCache = await getTemporaryDirectory();
|
||||
final Directory logDir = Directory('${appCache.path}/logs');
|
||||
logDir.createSync();
|
||||
final String dateTime = DateTime.now()
|
||||
.toIso8601String()
|
||||
.replaceAll('-', '')
|
||||
.replaceAll(':', '')
|
||||
.replaceAll('T', '')
|
||||
.replaceAll('.', '');
|
||||
final String fileName = 'revanced-manager_patcher_$dateTime.txt';
|
||||
final File log = File('${logDir.path}/$fileName');
|
||||
log.writeAsStringSync(logs);
|
||||
FlutterFileDialog.saveFile(
|
||||
params: SaveFileDialogParams(
|
||||
sourceFilePath: log.path,
|
||||
fileName: fileName,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String getSuggestedVersion(String packageName) {
|
||||
final Map<String, int> versions = {};
|
||||
for (final Patch patch in _patches) {
|
||||
final Package? package = patch.compatiblePackages.firstWhereOrNull(
|
||||
(pack) => pack.name == packageName,
|
||||
);
|
||||
if (package != null) {
|
||||
for (final String version in package.versions) {
|
||||
versions.update(
|
||||
version,
|
||||
(value) => versions[version]! + 1,
|
||||
ifAbsent: () => 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (versions.isNotEmpty) {
|
||||
final entries = versions.entries.toList()
|
||||
..sort((a, b) => a.value.compareTo(b.value));
|
||||
versions
|
||||
..clear()
|
||||
..addEntries(entries);
|
||||
versions.removeWhere((key, value) => value != versions.values.last);
|
||||
return (versions.keys.toList()..sort()).last;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
enum InstallStatus {
|
||||
mountNoRoot(1),
|
||||
mountVersionMismatch(1.1),
|
||||
mountMissingInstallation(1.2),
|
||||
|
||||
statusFailureBlocked(2),
|
||||
installFailedVerificationFailure(3.1),
|
||||
statusFailureInvalid(4),
|
||||
installFailedVersionDowngrade(4.1),
|
||||
statusFailureConflict(5),
|
||||
statusFailureStorage(6),
|
||||
statusFailureIncompatible(7),
|
||||
statusFailureTimeout(8);
|
||||
|
||||
const InstallStatus(this.statusCode);
|
||||
final double statusCode;
|
||||
|
||||
static String byCode(num code) {
|
||||
try {
|
||||
return InstallStatus.values
|
||||
.firstWhere((flag) => flag.statusCode == code)
|
||||
.status;
|
||||
} catch (e) {
|
||||
return 'status_unknown';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension InstallStatusExtension on InstallStatus {
|
||||
String get status {
|
||||
switch (this) {
|
||||
case InstallStatus.mountNoRoot:
|
||||
return 'mount_no_root';
|
||||
case InstallStatus.mountVersionMismatch:
|
||||
return 'mount_version_mismatch';
|
||||
case InstallStatus.mountMissingInstallation:
|
||||
return 'mount_missing_installation';
|
||||
case InstallStatus.statusFailureBlocked:
|
||||
return 'status_failure_blocked';
|
||||
case InstallStatus.installFailedVerificationFailure:
|
||||
return 'install_failed_verification_failure';
|
||||
case InstallStatus.statusFailureInvalid:
|
||||
return 'status_failure_invalid';
|
||||
case InstallStatus.installFailedVersionDowngrade:
|
||||
return 'install_failed_version_downgrade';
|
||||
case InstallStatus.statusFailureConflict:
|
||||
return 'status_failure_conflict';
|
||||
case InstallStatus.statusFailureStorage:
|
||||
return 'status_failure_storage';
|
||||
case InstallStatus.statusFailureIncompatible:
|
||||
return 'status_failure_incompatible';
|
||||
case InstallStatus.statusFailureTimeout:
|
||||
return 'status_failure_timeout';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/services/download_manager.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
import 'package:timeago/timeago.dart';
|
||||
|
||||
@@ -32,7 +33,7 @@ class RevancedAPI {
|
||||
final response = await _dio.get('/contributors');
|
||||
final List<dynamic> repositories = response.data['repositories'];
|
||||
for (final Map<String, dynamic> repo in repositories) {
|
||||
final String name = repo['name'];
|
||||
final String name = repo['name'].toLowerCase();
|
||||
contributors[name] = repo['contributors'];
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
@@ -48,13 +49,16 @@ class RevancedAPI {
|
||||
String extension,
|
||||
String repoName,
|
||||
) {
|
||||
if (!locator<ManagerAPI>().getDownloadConsent()) {
|
||||
return Future(() => null);
|
||||
}
|
||||
return getToolsLock.synchronized(() async {
|
||||
try {
|
||||
final response = await _dio.get('/tools');
|
||||
final List<dynamic> tools = response.data['tools'];
|
||||
return tools.firstWhereOrNull(
|
||||
(t) =>
|
||||
t['repository'] == repoName &&
|
||||
(t['repository'] as String).toLowerCase() == repoName.toLowerCase() &&
|
||||
(t['name'] as String).endsWith(extension),
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
|
||||
@@ -2,10 +2,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:root/root.dart';
|
||||
|
||||
class RootAPI {
|
||||
// TODO(ponces): remove in the future, keep it for now during migration.
|
||||
final String _revancedOldDirPath = '/data/local/tmp/revanced-manager';
|
||||
final String _revancedDirPath = '/data/adb/revanced';
|
||||
final String _postFsDataDirPath = '/data/adb/post-fs-data.d';
|
||||
final String _serviceDDirPath = '/data/adb/service.d';
|
||||
|
||||
Future<bool> isRooted() async {
|
||||
@@ -43,21 +40,22 @@ class RootAPI {
|
||||
String filePath,
|
||||
) async {
|
||||
try {
|
||||
final StringBuffer commands = StringBuffer();
|
||||
if (permissions.isNotEmpty) {
|
||||
await Root.exec(
|
||||
cmd: 'chmod $permissions "$filePath"',
|
||||
);
|
||||
commands.writeln('chmod $permissions $filePath');
|
||||
}
|
||||
|
||||
if (ownerGroup.isNotEmpty) {
|
||||
await Root.exec(
|
||||
cmd: 'chown $ownerGroup "$filePath"',
|
||||
);
|
||||
commands.writeln('chown $ownerGroup $filePath');
|
||||
}
|
||||
|
||||
if (seLinux.isNotEmpty) {
|
||||
await Root.exec(
|
||||
cmd: 'chcon $seLinux "$filePath"',
|
||||
);
|
||||
commands.writeln('chcon $seLinux $filePath');
|
||||
}
|
||||
|
||||
await Root.exec(
|
||||
cmd: commands.toString(),
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
@@ -75,18 +73,7 @@ class RootAPI {
|
||||
Future<List<String>> getInstalledApps() async {
|
||||
final List<String> apps = List.empty(growable: true);
|
||||
try {
|
||||
String? res = await Root.exec(
|
||||
cmd: 'ls "$_revancedDirPath"',
|
||||
);
|
||||
if (res != null) {
|
||||
final List<String> list = res.split('\n');
|
||||
list.removeWhere((pack) => pack.isEmpty);
|
||||
apps.addAll(list.map((pack) => pack.trim()).toList());
|
||||
}
|
||||
// TODO(ponces): remove in the future, keep it for now during migration.
|
||||
res = await Root.exec(
|
||||
cmd: 'ls "$_revancedOldDirPath"',
|
||||
);
|
||||
final String? res = await Root.exec(cmd: 'ls $_revancedDirPath');
|
||||
if (res != null) {
|
||||
final List<String> list = res.split('\n');
|
||||
list.removeWhere((pack) => pack.isEmpty);
|
||||
@@ -100,49 +87,36 @@ class RootAPI {
|
||||
return apps;
|
||||
}
|
||||
|
||||
Future<void> deleteApp(String packageName, String originalFilePath) async {
|
||||
Future<void> uninstall(String packageName) async {
|
||||
await Root.exec(
|
||||
cmd: 'am force-stop "$packageName"',
|
||||
);
|
||||
await Root.exec(
|
||||
cmd: 'su -mm -c "umount -l $originalFilePath"',
|
||||
);
|
||||
// TODO(ponces): remove in the future, keep it for now during migration.
|
||||
await Root.exec(
|
||||
cmd: 'rm -rf "$_revancedOldDirPath/$packageName"',
|
||||
);
|
||||
await Root.exec(
|
||||
cmd: 'rm -rf "$_revancedDirPath/$packageName"',
|
||||
);
|
||||
await Root.exec(
|
||||
cmd: 'rm -rf "$_serviceDDirPath/$packageName.sh"',
|
||||
);
|
||||
await Root.exec(
|
||||
cmd: 'rm -rf "$_postFsDataDirPath/$packageName.sh"',
|
||||
cmd: '''
|
||||
grep $packageName /proc/mounts | while read -r line; do echo \$line | cut -d " " -f 2 | sed "s/apk.*/apk/" | xargs -r umount -l; done
|
||||
rm -rf $_revancedDirPath/$packageName $_serviceDDirPath/$packageName.sh
|
||||
''',
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> installApp(
|
||||
Future<void> removeOrphanedFiles() async {
|
||||
await Root.exec(
|
||||
cmd: 'find $_revancedDirPath -type f -name original.apk -delete',
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> install(
|
||||
String packageName,
|
||||
String originalFilePath,
|
||||
String patchedFilePath,
|
||||
) async {
|
||||
try {
|
||||
await deleteApp(packageName, originalFilePath);
|
||||
await Root.exec(
|
||||
cmd: 'mkdir -p "$_revancedDirPath/$packageName"',
|
||||
);
|
||||
await setPermissions(
|
||||
'0755',
|
||||
'shell:shell',
|
||||
'',
|
||||
'$_revancedDirPath/$packageName',
|
||||
);
|
||||
await saveOriginalFilePath(packageName, originalFilePath);
|
||||
await installPatchedApk(packageName, patchedFilePath);
|
||||
await installServiceDScript(packageName);
|
||||
await installPostFsDataScript(packageName);
|
||||
await installApk(packageName, patchedFilePath);
|
||||
await mountApk(packageName, originalFilePath);
|
||||
await runMountScript(packageName);
|
||||
return true;
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
@@ -154,38 +128,48 @@ class RootAPI {
|
||||
|
||||
Future<void> installServiceDScript(String packageName) async {
|
||||
await Root.exec(
|
||||
cmd: 'mkdir -p "$_serviceDDirPath"',
|
||||
cmd: 'mkdir -p $_serviceDDirPath',
|
||||
);
|
||||
final String content = '#!/system/bin/sh\n'
|
||||
'while [ "\$(getprop sys.boot_completed | tr -d \'"\'"\'\\\\r\'"\'"\')" != "1" ]; do sleep 3; done\n'
|
||||
'base_path=$_revancedDirPath/$packageName/base.apk\n'
|
||||
'stock_path=\$(pm path $packageName | grep base | sed \'"\'"\'s/package://g\'"\'"\')\n'
|
||||
r'[ ! -z $stock_path ] && mount -o bind $base_path $stock_path';
|
||||
final String mountScript = '''
|
||||
#!/system/bin/sh
|
||||
# Mount using Magisk mirror, if available.
|
||||
MAGISKTMP="\$( magisk --path )" || MAGISKTMP=/sbin
|
||||
MIRROR="\$MAGISKTMP/.magisk/mirror"
|
||||
if [ ! -f \$MIRROR ]; then
|
||||
MIRROR=""
|
||||
fi
|
||||
|
||||
until [ "\$(getprop sys.boot_completed)" = 1 ]; do sleep 3; done
|
||||
until [ -d "/sdcard/Android" ]; do sleep 1; done
|
||||
|
||||
# Unmount any existing installation to prevent multiple unnecessary mounts.
|
||||
grep $packageName /proc/mounts | while read -r line; do echo \$line | cut -d " " -f 2 | sed "s/apk.*/apk/" | xargs -r umount -l; done
|
||||
|
||||
base_path=$_revancedDirPath/$packageName/base.apk
|
||||
stock_path=\$(pm path $packageName | grep base | sed "s/package://g" )
|
||||
|
||||
chcon u:object_r:apk_data_file:s0 \$base_path
|
||||
mount -o bind \$MIRROR\$base_path \$stock_path
|
||||
|
||||
# Kill the app to force it to restart the mounted APK in case it is already running
|
||||
am force-stop $packageName
|
||||
'''
|
||||
.trimMultilineString();
|
||||
final String scriptFilePath = '$_serviceDDirPath/$packageName.sh';
|
||||
await Root.exec(
|
||||
cmd: 'echo \'$content\' > "$scriptFilePath"',
|
||||
cmd: 'echo \'$mountScript\' > "$scriptFilePath"',
|
||||
);
|
||||
await setPermissions('0744', '', '', scriptFilePath);
|
||||
}
|
||||
|
||||
Future<void> installPostFsDataScript(String packageName) async {
|
||||
await Root.exec(
|
||||
cmd: 'mkdir -p "$_postFsDataDirPath"',
|
||||
);
|
||||
final String content = '#!/system/bin/sh\n'
|
||||
'stock_path=\$(pm path $packageName | grep base | sed \'"\'"\'s/package://g\'"\'"\')\n'
|
||||
r'[ ! -z $stock_path ] && umount -l $stock_path';
|
||||
final String scriptFilePath = '$_postFsDataDirPath/$packageName.sh';
|
||||
await Root.exec(
|
||||
cmd: 'echo \'$content\' > "$scriptFilePath"',
|
||||
);
|
||||
await setPermissions('0744', '', '', scriptFilePath);
|
||||
}
|
||||
|
||||
Future<void> installApk(String packageName, String patchedFilePath) async {
|
||||
Future<void> installPatchedApk(
|
||||
String packageName, String patchedFilePath,) async {
|
||||
final String newPatchedFilePath = '$_revancedDirPath/$packageName/base.apk';
|
||||
await Root.exec(
|
||||
cmd: 'cp "$patchedFilePath" "$newPatchedFilePath"',
|
||||
cmd: '''
|
||||
mkdir -p $_revancedDirPath/$packageName
|
||||
cp "$patchedFilePath" $newPatchedFilePath
|
||||
''',
|
||||
);
|
||||
await setPermissions(
|
||||
'0644',
|
||||
@@ -195,50 +179,10 @@ class RootAPI {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> mountApk(String packageName, String originalFilePath) async {
|
||||
final String newPatchedFilePath = '$_revancedDirPath/$packageName/base.apk';
|
||||
await Root.exec(
|
||||
cmd: 'am force-stop "$packageName"',
|
||||
);
|
||||
await Root.exec(
|
||||
cmd: 'su -mm -c "umount -l $originalFilePath"',
|
||||
);
|
||||
await Root.exec(
|
||||
cmd: 'su -mm -c "mount -o bind $newPatchedFilePath $originalFilePath"',
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> isMounted(String packageName) async {
|
||||
final String? res = await Root.exec(
|
||||
cmd: 'cat /proc/mounts | grep $packageName',
|
||||
);
|
||||
return res != null && res.isNotEmpty;
|
||||
}
|
||||
|
||||
Future<void> saveOriginalFilePath(
|
||||
Future<void> runMountScript(
|
||||
String packageName,
|
||||
String originalFilePath,
|
||||
) async {
|
||||
final String originalRootPath =
|
||||
'$_revancedDirPath/$packageName/original.apk';
|
||||
await Root.exec(
|
||||
cmd: 'mkdir -p "$_revancedDirPath/$packageName"',
|
||||
);
|
||||
await setPermissions(
|
||||
'0755',
|
||||
'shell:shell',
|
||||
'',
|
||||
'$_revancedDirPath/$packageName',
|
||||
);
|
||||
await Root.exec(
|
||||
cmd: 'cp "$originalFilePath" "$originalRootPath"',
|
||||
);
|
||||
await setPermissions(
|
||||
'0644',
|
||||
'shell:shell',
|
||||
'u:object_r:apk_data_file:s0',
|
||||
originalFilePath,
|
||||
);
|
||||
await Root.exec(cmd: '.$_serviceDDirPath/$packageName.sh');
|
||||
}
|
||||
|
||||
Future<bool> fileExists(String path) async {
|
||||
@@ -255,3 +199,10 @@ class RootAPI {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove leading spaces manually until
|
||||
// https://github.com/dart-lang/language/issues/559 is closed
|
||||
extension StringExtension on String {
|
||||
String trimMultilineString() =>
|
||||
split('\n').map((line) => line.trim()).join('\n').trim();
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
var lightCustomColorScheme = ColorScheme.fromSeed(
|
||||
seedColor: Colors.blue,
|
||||
primary: const Color(0xff1B73E8),
|
||||
seedColor: Colors.purple,
|
||||
primary: const Color(0xFF4C51C0),
|
||||
);
|
||||
|
||||
var lightCustomTheme = ThemeData(
|
||||
@@ -21,10 +21,10 @@ var lightCustomTheme = ThemeData(
|
||||
);
|
||||
|
||||
var darkCustomColorScheme = ColorScheme.fromSeed(
|
||||
seedColor: Colors.blue,
|
||||
seedColor: Colors.purple,
|
||||
brightness: Brightness.dark,
|
||||
primary: const Color(0xffA5CAFF),
|
||||
surface: const Color(0xff1B1A1D),
|
||||
primary: const Color(0xFFBFC1FF),
|
||||
surface: const Color(0xFF131316),
|
||||
);
|
||||
|
||||
var darkCustomTheme = ThemeData(
|
||||
@@ -38,7 +38,7 @@ var darkCustomTheme = ThemeData(
|
||||
),
|
||||
),
|
||||
),
|
||||
canvasColor: const Color(0xff1B1A1D),
|
||||
scaffoldBackgroundColor: const Color(0xff1B1A1D),
|
||||
canvasColor: const Color(0xFF131316),
|
||||
scaffoldBackgroundColor: const Color(0xFF131316),
|
||||
textTheme: GoogleFonts.robotoTextTheme(ThemeData.dark().textTheme),
|
||||
);
|
||||
|
||||
@@ -12,11 +12,11 @@ import 'package:stacked_services/stacked_services.dart';
|
||||
|
||||
class DynamicThemeBuilder extends StatefulWidget {
|
||||
const DynamicThemeBuilder({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.home,
|
||||
required this.localizationsDelegates,
|
||||
}) : super(key: key);
|
||||
});
|
||||
final String title;
|
||||
final Widget home;
|
||||
final Iterable<LocalizationsDelegate> localizationsDelegates;
|
||||
@@ -25,7 +25,8 @@ class DynamicThemeBuilder extends StatefulWidget {
|
||||
State<DynamicThemeBuilder> createState() => _DynamicThemeBuilderState();
|
||||
}
|
||||
|
||||
class _DynamicThemeBuilderState extends State<DynamicThemeBuilder> with WidgetsBindingObserver {
|
||||
class _DynamicThemeBuilderState extends State<DynamicThemeBuilder>
|
||||
with WidgetsBindingObserver {
|
||||
Brightness brightness = PlatformDispatcher.instance.platformBrightness;
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
|
||||
@@ -43,8 +44,9 @@ class _DynamicThemeBuilderState extends State<DynamicThemeBuilder> with WidgetsB
|
||||
if (_managerAPI.getThemeMode() < 2) {
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
SystemUiOverlayStyle(
|
||||
systemNavigationBarIconBrightness:
|
||||
brightness == Brightness.light ? Brightness.dark : Brightness.light,
|
||||
systemNavigationBarIconBrightness: brightness == Brightness.light
|
||||
? Brightness.dark
|
||||
: Brightness.light,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -83,24 +85,31 @@ class _DynamicThemeBuilderState extends State<DynamicThemeBuilder> with WidgetsB
|
||||
return DynamicTheme(
|
||||
themeCollection: ThemeCollection(
|
||||
themes: {
|
||||
0: brightness == Brightness.light ? lightCustomTheme : darkCustomTheme,
|
||||
1: brightness == Brightness.light ? lightDynamicTheme : darkDynamicTheme,
|
||||
0: brightness == Brightness.light
|
||||
? lightCustomTheme
|
||||
: darkCustomTheme,
|
||||
1: brightness == Brightness.light
|
||||
? lightDynamicTheme
|
||||
: darkDynamicTheme,
|
||||
2: lightCustomTheme,
|
||||
3: lightDynamicTheme,
|
||||
4: darkCustomTheme,
|
||||
5: darkDynamicTheme,
|
||||
},
|
||||
fallbackTheme: PlatformDispatcher.instance.platformBrightness == Brightness.light ? lightCustomTheme : darkCustomTheme,
|
||||
fallbackTheme: PlatformDispatcher.instance.platformBrightness ==
|
||||
Brightness.light
|
||||
? lightCustomTheme
|
||||
: darkCustomTheme,
|
||||
),
|
||||
builder: (context, theme) => MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: widget.title,
|
||||
navigatorKey: StackedService.navigatorKey,
|
||||
onGenerateRoute: StackedRouter().onGenerateRoute,
|
||||
theme: theme,
|
||||
home: widget.home,
|
||||
localizationsDelegates: widget.localizationsDelegates,
|
||||
),
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: widget.title,
|
||||
navigatorKey: StackedService.navigatorKey,
|
||||
onGenerateRoute: StackedRouter().onGenerateRoute,
|
||||
theme: theme,
|
||||
home: widget.home,
|
||||
localizationsDelegates: widget.localizationsDelegates,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -4,11 +4,12 @@ import 'package:revanced_manager/ui/views/app_selector/app_selector_viewmodel.da
|
||||
import 'package:revanced_manager/ui/widgets/appSelectorView/app_skeleton_loader.dart';
|
||||
import 'package:revanced_manager/ui/widgets/appSelectorView/installed_app_item.dart';
|
||||
import 'package:revanced_manager/ui/widgets/appSelectorView/not_installed_app_item.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_floating_action_button_extended.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/search_bar.dart';
|
||||
import 'package:stacked/stacked.dart' hide SkeletonLoader;
|
||||
|
||||
class AppSelectorView extends StatefulWidget {
|
||||
const AppSelectorView({Key? key}) : super(key: key);
|
||||
const AppSelectorView({super.key});
|
||||
|
||||
@override
|
||||
State<AppSelectorView> createState() => _AppSelectorViewState();
|
||||
@@ -23,12 +24,11 @@ class _AppSelectorViewState extends State<AppSelectorView> {
|
||||
onViewModelReady: (model) => model.initialize(),
|
||||
viewModelBuilder: () => AppSelectorViewModel(),
|
||||
builder: (context, model, child) => Scaffold(
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
floatingActionButton: HapticFloatingActionButtonExtended(
|
||||
label: I18nText('appSelectorView.storageButton'),
|
||||
icon: const Icon(Icons.sd_storage),
|
||||
onPressed: () {
|
||||
model.selectAppFromStorage(context);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
body: CustomScrollView(
|
||||
@@ -41,8 +41,8 @@ class _AppSelectorViewState extends State<AppSelectorView> {
|
||||
),
|
||||
titleTextStyle: TextStyle(
|
||||
fontSize: 22.0,
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: Icon(
|
||||
Icons.arrow_back,
|
||||
@@ -95,9 +95,7 @@ class _AppSelectorViewState extends State<AppSelectorView> {
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
...model
|
||||
.getFilteredApps(_query)
|
||||
.map(
|
||||
...model.getFilteredApps(_query).map(
|
||||
(app) => InstalledAppItem(
|
||||
name: app.appName,
|
||||
pkgName: app.packageName,
|
||||
@@ -113,12 +111,13 @@ class _AppSelectorViewState extends State<AppSelectorView> {
|
||||
context,
|
||||
app.packageName,
|
||||
),
|
||||
onLinkTap: () =>
|
||||
model.searchSuggestedVersionOnWeb(
|
||||
packageName: app.packageName,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
...model
|
||||
.getFilteredAppsNames(_query)
|
||||
.map(
|
||||
),
|
||||
...model.getFilteredAppsNames(_query).map(
|
||||
(app) => NotInstalledAppItem(
|
||||
name: app,
|
||||
patchesCount: model.patchesCount(app),
|
||||
@@ -127,9 +126,12 @@ class _AppSelectorViewState extends State<AppSelectorView> {
|
||||
onTap: () {
|
||||
model.showDownloadToast();
|
||||
},
|
||||
onLinkTap: () =>
|
||||
model.searchSuggestedVersionOnWeb(
|
||||
packageName: app,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(height: 70.0),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
||||
import 'package:device_apps/device_apps.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
|
||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
@@ -12,7 +13,6 @@ import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/services/patcher_api.dart';
|
||||
import 'package:revanced_manager/services/toast.dart';
|
||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
@@ -70,7 +70,54 @@ class AppSelectorViewModel extends BaseViewModel {
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> selectApp(ApplicationWithIcon application) async {
|
||||
Future<void> searchSuggestedVersionOnWeb({
|
||||
required String packageName,
|
||||
}) async {
|
||||
final String suggestedVersion = getSuggestedVersion(packageName);
|
||||
|
||||
if (suggestedVersion.isNotEmpty) {
|
||||
await openDefaultBrowser('$packageName apk version $suggestedVersion');
|
||||
} else {
|
||||
await openDefaultBrowser('$packageName apk');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> openDefaultBrowser(String query) async {
|
||||
if (Platform.isAndroid) {
|
||||
try {
|
||||
const platform = MethodChannel('app.revanced.manager.flutter/browser');
|
||||
await platform.invokeMethod('openBrowser', {'query': query});
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw 'Platform not supported';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> selectApp(
|
||||
BuildContext context,
|
||||
ApplicationWithIcon application, [
|
||||
bool isFromStorage = false,
|
||||
]) async {
|
||||
final String suggestedVersion =
|
||||
getSuggestedVersion(application.packageName);
|
||||
if (application.versionName != suggestedVersion &&
|
||||
suggestedVersion.isNotEmpty) {
|
||||
_managerAPI.suggestedAppVersionSelected = false;
|
||||
if (_managerAPI.isRequireSuggestedAppVersionEnabled() &&
|
||||
context.mounted) {
|
||||
return showRequireSuggestedAppVersionDialog(
|
||||
context,
|
||||
application.versionName!,
|
||||
suggestedVersion,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_managerAPI.suggestedAppVersionSelected = true;
|
||||
}
|
||||
locator<PatcherViewModel>().selectedApp = PatchedApplication(
|
||||
name: application.appName,
|
||||
packageName: application.packageName,
|
||||
@@ -78,8 +125,12 @@ class AppSelectorViewModel extends BaseViewModel {
|
||||
apkFilePath: application.apkFilePath,
|
||||
icon: application.icon,
|
||||
patchDate: DateTime.now(),
|
||||
isFromStorage: isFromStorage,
|
||||
);
|
||||
await locator<PatcherViewModel>().loadLastSelectedPatches();
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> canSelectInstalled(
|
||||
@@ -89,27 +140,63 @@ class AppSelectorViewModel extends BaseViewModel {
|
||||
final app =
|
||||
await DeviceApps.getApp(packageName, true) as ApplicationWithIcon?;
|
||||
if (app != null) {
|
||||
if (await checkSplitApk(packageName) && !isRooted) {
|
||||
final bool isSplitApk = await checkSplitApk(packageName);
|
||||
if (isRooted || !isSplitApk) {
|
||||
if (context.mounted) {
|
||||
await selectApp(context, app);
|
||||
}
|
||||
final List<Option> requiredNullOptions = getNullRequiredOptions(
|
||||
locator<PatcherViewModel>().selectedPatches,
|
||||
packageName,
|
||||
);
|
||||
if (requiredNullOptions.isNotEmpty) {
|
||||
locator<PatcherViewModel>().showRequiredOptionDialog();
|
||||
}
|
||||
} else {
|
||||
if (context.mounted) {
|
||||
return showSelectFromStorageDialog(context);
|
||||
}
|
||||
} else if (!await checkSplitApk(packageName) || isRooted) {
|
||||
await selectApp(app);
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
final List<Option> requiredNullOptions = getNullRequiredOptions(locator<PatcherViewModel>().selectedPatches, packageName);
|
||||
if(requiredNullOptions.isNotEmpty){
|
||||
locator<PatcherViewModel>().showRequiredOptionDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future showRequireSuggestedAppVersionDialog(
|
||||
BuildContext context,
|
||||
String selectedVersion,
|
||||
String suggestedVersion,
|
||||
) async {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('warning'),
|
||||
content: I18nText(
|
||||
'appSelectorView.requireSuggestedAppVersionDialogText',
|
||||
translationParams: {
|
||||
'suggested': suggestedVersion,
|
||||
'selected': selectedVersion,
|
||||
},
|
||||
child: const Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: I18nText('okButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future showSelectFromStorageDialog(BuildContext context) async {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => SimpleDialog(
|
||||
builder: (innerContext) => SimpleDialog(
|
||||
alignment: Alignment.center,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
|
||||
@@ -118,7 +205,7 @@ class AppSelectorViewModel extends BaseViewModel {
|
||||
Icon(
|
||||
Icons.block,
|
||||
size: 28,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
color: Theme.of(innerContext).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
I18nText(
|
||||
@@ -144,14 +231,12 @@ class AppSelectorViewModel extends BaseViewModel {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
CustomMaterialButton(
|
||||
onPressed: () => selectAppFromStorage(context).then(
|
||||
(_) {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
label: Row(
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(innerContext);
|
||||
await selectAppFromStorage(context);
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.sd_card),
|
||||
@@ -161,12 +246,11 @@ class AppSelectorViewModel extends BaseViewModel {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(innerContext);
|
||||
},
|
||||
label: Row(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(width: 10),
|
||||
@@ -203,17 +287,8 @@ class AppSelectorViewModel extends BaseViewModel {
|
||||
apkFile.path,
|
||||
true,
|
||||
) as ApplicationWithIcon?;
|
||||
if (application != null) {
|
||||
locator<PatcherViewModel>().selectedApp = PatchedApplication(
|
||||
name: application.appName,
|
||||
packageName: application.packageName,
|
||||
version: application.versionName!,
|
||||
apkFilePath: result,
|
||||
icon: application.icon,
|
||||
patchDate: DateTime.now(),
|
||||
isFromStorage: true,
|
||||
);
|
||||
locator<PatcherViewModel>().loadLastSelectedPatches();
|
||||
if (application != null && context.mounted) {
|
||||
await selectApp(context, application, true);
|
||||
}
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class ContributorsView extends StatelessWidget {
|
||||
const ContributorsView({Key? key}) : super(key: key);
|
||||
const ContributorsView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -14,9 +14,9 @@ class ContributorsViewModel extends BaseViewModel {
|
||||
final Map<String, List<dynamic>> contributors =
|
||||
await _managerAPI.getContributors();
|
||||
patcherContributors = contributors[_managerAPI.defaultPatcherRepo] ?? [];
|
||||
patchesContributors = contributors[_managerAPI.getPatchesRepo()] ?? [];
|
||||
patchesContributors = contributors[_managerAPI.getPatchesRepo().toLowerCase()] ?? [];
|
||||
integrationsContributors =
|
||||
contributors[_managerAPI.getIntegrationsRepo()] ?? [];
|
||||
contributors[_managerAPI.getIntegrationsRepo().toLowerCase()] ?? [];
|
||||
cliContributors = contributors[_managerAPI.defaultCliRepo] ?? [];
|
||||
managerContributors = contributors[_managerAPI.defaultManagerRepo] ?? [];
|
||||
notifyListeners();
|
||||
|
||||
@@ -9,7 +9,7 @@ import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class HomeView extends StatelessWidget {
|
||||
const HomeView({Key? key}) : super(key: key);
|
||||
const HomeView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -8,7 +8,6 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:install_plugin/install_plugin.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/app/app.router.dart';
|
||||
@@ -20,8 +19,8 @@ import 'package:revanced_manager/services/revanced_api.dart';
|
||||
import 'package:revanced_manager/services/toast.dart';
|
||||
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/homeView/update_confirmation_dialog.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||
import 'package:revanced_manager/ui/widgets/homeView/update_confirmation_sheet.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_checkbox_list_tile.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
|
||||
@@ -36,15 +35,32 @@ class HomeViewModel extends BaseViewModel {
|
||||
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||
bool showUpdatableApps = false;
|
||||
List<PatchedApplication> patchedInstalledApps = [];
|
||||
String _currentManagerVersion = '';
|
||||
String _currentPatchesVersion = '';
|
||||
String? _latestManagerVersion = '';
|
||||
File? downloadedApk;
|
||||
|
||||
Future<void> initialize(BuildContext context) async {
|
||||
_latestManagerVersion = await _managerAPI.getLatestManagerVersion();
|
||||
if (!_managerAPI.getPatchesConsent()) {
|
||||
await showPatchesConsent(context);
|
||||
_managerAPI.rePatchedSavedApps().then((_) => _getPatchedApps());
|
||||
_currentManagerVersion = await _managerAPI.getCurrentManagerVersion();
|
||||
if (!_managerAPI.getDownloadConsent()) {
|
||||
await showDownloadConsent(context);
|
||||
await forceRefresh(context);
|
||||
return;
|
||||
}
|
||||
_latestManagerVersion = await _managerAPI.getLatestManagerVersion();
|
||||
_currentPatchesVersion = await _managerAPI.getCurrentPatchesVersion();
|
||||
if (_managerAPI.showUpdateDialog() && await hasManagerUpdates()) {
|
||||
showUpdateDialog(context, false);
|
||||
}
|
||||
if (!_managerAPI.isPatchesAutoUpdate() &&
|
||||
_managerAPI.showUpdateDialog() &&
|
||||
await hasPatchesUpdates()) {
|
||||
showUpdateDialog(context, true);
|
||||
}
|
||||
|
||||
await _patcherAPI.initialize();
|
||||
|
||||
await flutterLocalNotificationsPlugin.initialize(
|
||||
const InitializationSettings(
|
||||
android: AndroidInitializationSettings('ic_notification'),
|
||||
@@ -54,7 +70,7 @@ class HomeViewModel extends BaseViewModel {
|
||||
_toast.showBottom('homeView.installingMessage');
|
||||
final File? managerApk = await _managerAPI.downloadManager();
|
||||
if (managerApk != null) {
|
||||
await InstallPlugin.installApk(managerApk.path);
|
||||
await _patcherAPI.installApk(context, managerApk.path);
|
||||
} else {
|
||||
_toast.showBottom('homeView.errorDownloadMessage');
|
||||
}
|
||||
@@ -65,24 +81,24 @@ class HomeViewModel extends BaseViewModel {
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>()
|
||||
?.requestNotificationsPermission();
|
||||
final bool isConnected = await Connectivity().checkConnectivity() !=
|
||||
ConnectivityResult.none;
|
||||
|
||||
final bool isConnected =
|
||||
await Connectivity().checkConnectivity() != ConnectivityResult.none;
|
||||
if (!isConnected) {
|
||||
_toast.showBottom('homeView.noConnection');
|
||||
}
|
||||
|
||||
final NotificationAppLaunchDetails? notificationAppLaunchDetails =
|
||||
await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
|
||||
if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) {
|
||||
_toast.showBottom('homeView.installingMessage');
|
||||
final File? managerApk = await _managerAPI.downloadManager();
|
||||
if (managerApk != null) {
|
||||
await InstallPlugin.installApk(managerApk.path);
|
||||
await _patcherAPI.installApk(context, managerApk.path);
|
||||
} else {
|
||||
_toast.showBottom('homeView.errorDownloadMessage');
|
||||
}
|
||||
}
|
||||
|
||||
_managerAPI.reAssessSavedApps().then((_) => _getPatchedApps());
|
||||
}
|
||||
|
||||
void navigateToAppInfo(PatchedApplication app) {
|
||||
@@ -111,17 +127,10 @@ class HomeViewModel extends BaseViewModel {
|
||||
}
|
||||
|
||||
Future<bool> hasManagerUpdates() async {
|
||||
String currentVersion = await _managerAPI.getCurrentManagerVersion();
|
||||
|
||||
// add v to current version
|
||||
if (!currentVersion.startsWith('v')) {
|
||||
currentVersion = 'v$currentVersion';
|
||||
}
|
||||
|
||||
_latestManagerVersion =
|
||||
await _managerAPI.getLatestManagerVersion() ?? currentVersion;
|
||||
await _managerAPI.getLatestManagerVersion() ?? _currentManagerVersion;
|
||||
|
||||
if (_latestManagerVersion != currentVersion) {
|
||||
if (_latestManagerVersion != _currentManagerVersion) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -129,13 +138,12 @@ class HomeViewModel extends BaseViewModel {
|
||||
|
||||
Future<bool> hasPatchesUpdates() async {
|
||||
final String? latestVersion = await _managerAPI.getLatestPatchesVersion();
|
||||
final String currentVersion = await _managerAPI.getCurrentPatchesVersion();
|
||||
if (latestVersion != null) {
|
||||
try {
|
||||
final int latestVersionInt =
|
||||
int.parse(latestVersion.replaceAll(RegExp('[^0-9]'), ''));
|
||||
final int currentVersionInt =
|
||||
int.parse(currentVersion.replaceAll(RegExp('[^0-9]'), ''));
|
||||
int.parse(_currentPatchesVersion.replaceAll(RegExp('[^0-9]'), ''));
|
||||
return latestVersionInt > currentVersionInt;
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
@@ -163,22 +171,98 @@ class HomeViewModel extends BaseViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showPatchesConsent(BuildContext context) async {
|
||||
Future<void> showDownloadConsent(BuildContext context) async {
|
||||
final ValueNotifier<bool> autoUpdate = ValueNotifier(true);
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Download ReVanced Patches?'),
|
||||
builder: (context) => PopScope(
|
||||
canPop: false,
|
||||
child: AlertDialog(
|
||||
title: I18nText('homeView.downloadConsentDialogTitle'),
|
||||
content: ValueListenableBuilder(
|
||||
valueListenable: autoUpdate,
|
||||
builder: (context, value, child) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
I18nText(
|
||||
'homeView.downloadConsentDialogText',
|
||||
child: Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: I18nText(
|
||||
'homeView.downloadConsentDialogText2',
|
||||
translationParams: {
|
||||
'url': _managerAPI.defaultApiUrl.split('/')[2],
|
||||
},
|
||||
child: Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
_managerAPI.setDownloadConsent(false);
|
||||
SystemNavigator.pop();
|
||||
},
|
||||
child: I18nText('quitButton'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
_managerAPI.setDownloadConsent(true);
|
||||
_managerAPI.setPatchesAutoUpdate(autoUpdate.value);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: I18nText('okButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void showUpdateDialog(BuildContext context, bool isPatches) {
|
||||
final ValueNotifier<bool> noShow =
|
||||
ValueNotifier(!_managerAPI.showUpdateDialog());
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (innerContext) => AlertDialog(
|
||||
title: I18nText('homeView.updateDialogTitle'),
|
||||
content: ValueListenableBuilder(
|
||||
valueListenable: autoUpdate,
|
||||
valueListenable: noShow,
|
||||
builder: (context, value, child) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
I18nText(
|
||||
'homeView.patchesConsentDialogText',
|
||||
'homeView.updateDialogText',
|
||||
translationParams: {
|
||||
'file': isPatches ? 'ReVanced Patches' : 'ReVanced Manager',
|
||||
'version': isPatches
|
||||
? _currentPatchesVersion
|
||||
: _currentManagerVersion,
|
||||
},
|
||||
child: Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
@@ -188,34 +272,18 @@ class HomeViewModel extends BaseViewModel {
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: I18nText(
|
||||
'homeView.patchesConsentDialogText2',
|
||||
translationParams: {
|
||||
'url': _managerAPI.defaultApiUrl.split('/')[2],
|
||||
},
|
||||
child: Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
CheckboxListTile(
|
||||
const SizedBox(height: 10),
|
||||
HapticCheckboxListTile(
|
||||
value: value,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: I18nText(
|
||||
'homeView.patchesConsentDialogText3',
|
||||
'noShowAgain',
|
||||
),
|
||||
subtitle: I18nText(
|
||||
'homeView.patchesConsentDialogText3Sub',
|
||||
'homeView.changeLaterSubtitle',
|
||||
),
|
||||
onChanged: (selected) {
|
||||
autoUpdate.value = selected!;
|
||||
noShow.value = selected!;
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -223,21 +291,20 @@ class HomeViewModel extends BaseViewModel {
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await _managerAPI.setPatchesConsent(false);
|
||||
SystemNavigator.pop();
|
||||
_managerAPI.setShowUpdateDialog(!noShow.value);
|
||||
Navigator.pop(innerContext);
|
||||
},
|
||||
label: I18nText('quitButton'),
|
||||
child: I18nText('dismissButton'), // Decide later
|
||||
),
|
||||
CustomMaterialButton(
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
await _managerAPI.setPatchesConsent(true);
|
||||
await _managerAPI.setPatchesAutoUpdate(autoUpdate.value);
|
||||
Navigator.of(context).pop();
|
||||
_managerAPI.setShowUpdateDialog(!noShow.value);
|
||||
Navigator.pop(innerContext);
|
||||
await showUpdateConfirmationDialog(context, isPatches);
|
||||
},
|
||||
label: I18nText('okButton'),
|
||||
child: I18nText('showUpdateButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -269,119 +336,91 @@ class HomeViewModel extends BaseViewModel {
|
||||
builder: (context) => ValueListenableBuilder(
|
||||
valueListenable: downloaded,
|
||||
builder: (context, value, child) {
|
||||
return SimpleDialog(
|
||||
contentPadding: const EdgeInsets.all(16.0),
|
||||
return AlertDialog(
|
||||
title: I18nText(
|
||||
!value
|
||||
? 'homeView.downloadingMessage'
|
||||
: 'homeView.downloadedMessage',
|
||||
child: Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Row(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!value)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.new_releases_outlined,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
StreamBuilder<double>(
|
||||
initialData: 0.0,
|
||||
stream: _revancedAPI.managerUpdateProgress.stream,
|
||||
builder: (context, snapshot) {
|
||||
return LinearProgressIndicator(
|
||||
value: snapshot.data! * 0.01,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
Text(
|
||||
'$_latestManagerVersion',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
const SizedBox(height: 16.0),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: FilledButton(
|
||||
onPressed: () {
|
||||
_revancedAPI.disposeManagerUpdateProgress();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: I18nText('cancelButton'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
if (!value)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
StreamBuilder<double>(
|
||||
initialData: 0.0,
|
||||
stream: _revancedAPI.managerUpdateProgress.stream,
|
||||
builder: (context, snapshot) {
|
||||
return LinearProgressIndicator(
|
||||
value: snapshot.data! * 0.01,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: CustomMaterialButton(
|
||||
label: I18nText('cancelButton'),
|
||||
onPressed: () {
|
||||
_revancedAPI.disposeManagerUpdateProgress();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
if (value)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
I18nText(
|
||||
'homeView.installUpdate',
|
||||
child: Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (value)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
I18nText(
|
||||
'homeView.installUpdate',
|
||||
child: Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: I18nText('cancelButton'),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('cancelButton'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: FilledButton(
|
||||
onPressed: () async {
|
||||
await _patcherAPI.installApk(
|
||||
context,
|
||||
downloadedApk!.path,
|
||||
);
|
||||
},
|
||||
child: I18nText('updateButton'),
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: CustomMaterialButton(
|
||||
label: I18nText('updateButton'),
|
||||
onPressed: () async {
|
||||
await InstallPlugin.installApk(
|
||||
downloadedApk!.path,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -415,7 +454,7 @@ class HomeViewModel extends BaseViewModel {
|
||||
// UILocalNotificationDateInterpretation.absoluteTime,
|
||||
// );
|
||||
_toast.showBottom('homeView.installingMessage');
|
||||
await InstallPlugin.installApk(managerApk.path);
|
||||
await _patcherAPI.installApk(context, managerApk.path);
|
||||
} else {
|
||||
_toast.showBottom('homeView.errorDownloadMessage');
|
||||
}
|
||||
@@ -433,16 +472,18 @@ class HomeViewModel extends BaseViewModel {
|
||||
|
||||
Future<void> showUpdateConfirmationDialog(
|
||||
BuildContext parentContext,
|
||||
bool isPatches,
|
||||
) {
|
||||
bool isPatches, [
|
||||
bool changelog = false,
|
||||
]) {
|
||||
return showModalBottomSheet(
|
||||
context: parentContext,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24.0)),
|
||||
),
|
||||
builder: (context) => UpdateConfirmationDialog(
|
||||
builder: (context) => UpdateConfirmationSheet(
|
||||
isPatches: isPatches,
|
||||
changelog: changelog,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,24 +5,25 @@ 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/shared/custom_card.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_floating_action_button_extended.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class InstallerView extends StatelessWidget {
|
||||
const InstallerView({Key? key}) : super(key: key);
|
||||
const InstallerView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<InstallerViewModel>.reactive(
|
||||
onViewModelReady: (model) => model.initialize(context),
|
||||
viewModelBuilder: () => InstallerViewModel(),
|
||||
builder: (context, model, child) => WillPopScope(
|
||||
builder: (context, model, child) => PopScope(
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
bottom: model.isPatching,
|
||||
child: Scaffold(
|
||||
floatingActionButton: Visibility(
|
||||
visible: !model.isPatching && !model.hasErrors,
|
||||
child: FloatingActionButton.extended(
|
||||
child: HapticFloatingActionButtonExtended(
|
||||
label: I18nText(
|
||||
model.isInstalled
|
||||
? 'installerView.openButton'
|
||||
@@ -111,7 +112,7 @@ class InstallerView extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
onWillPop: () => model.onWillPop(context),
|
||||
onPopInvoked: (bool didPop) => model.onWillPop(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/services/patcher_api.dart';
|
||||
import 'package:revanced_manager/services/root_api.dart';
|
||||
import 'package:revanced_manager/services/toast.dart';
|
||||
import 'package:revanced_manager/ui/views/home/home_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/about_info.dart';
|
||||
import 'package:screenshot_callback/screenshot_callback.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
@@ -104,7 +104,7 @@ class InstallerViewModel extends BaseViewModel {
|
||||
isPatching = true;
|
||||
isInstalled = false;
|
||||
hasErrors = false;
|
||||
} else if (value == 1.0) {
|
||||
} else if (value == .85) {
|
||||
isPatching = false;
|
||||
hasErrors = false;
|
||||
await _managerAPI.savePatches(
|
||||
@@ -115,6 +115,7 @@ class InstallerViewModel extends BaseViewModel {
|
||||
} else if (value == -100.0) {
|
||||
isPatching = false;
|
||||
hasErrors = true;
|
||||
progress = 0.0;
|
||||
}
|
||||
if (header.isNotEmpty) {
|
||||
headerLogs = header;
|
||||
@@ -127,10 +128,10 @@ class InstallerViewModel extends BaseViewModel {
|
||||
if (logs[logs.length - 1] == '\n') {
|
||||
logs = logs.substring(0, logs.length - 1);
|
||||
}
|
||||
Future.delayed(const Duration(milliseconds: 500)).then((value) {
|
||||
Future.delayed(const Duration(milliseconds: 100)).then((value) {
|
||||
scrollController.animateTo(
|
||||
scrollController.position.maxScrollExtent,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
duration: const Duration(milliseconds: 100),
|
||||
curve: Curves.fastOutSlowIn,
|
||||
);
|
||||
});
|
||||
@@ -179,32 +180,103 @@ class InstallerViewModel extends BaseViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
void _trimLogs(List<String> logLines, String keyword, String? newString) {
|
||||
final lineCount = logLines.where((line) => line.endsWith(keyword)).length;
|
||||
final index = logLines.indexWhere((line) => line.endsWith(keyword));
|
||||
if (newString != null && lineCount > 0) {
|
||||
logLines.insert(
|
||||
index,
|
||||
newString.replaceAll('{lineCount}', lineCount.toString()),
|
||||
);
|
||||
}
|
||||
logLines.removeWhere((lines) => lines.endsWith(keyword));
|
||||
}
|
||||
|
||||
dynamic _getPatchOptionValue(String patchName, Option option) {
|
||||
final Option? savedOption =
|
||||
_managerAPI.getPatchOption(_app.packageName, patchName, option.key);
|
||||
if (savedOption != null) {
|
||||
return savedOption.value;
|
||||
} else {
|
||||
return option.value;
|
||||
}
|
||||
}
|
||||
|
||||
String _formatPatches(List<Patch> patches, String noneString) {
|
||||
return patches.isEmpty
|
||||
? noneString
|
||||
: patches.map((p) {
|
||||
final optionsChanged = p.options
|
||||
.where((o) => _getPatchOptionValue(p.name, o) != o.value)
|
||||
.toList();
|
||||
return p.name +
|
||||
(optionsChanged.isEmpty
|
||||
? ''
|
||||
: ' [${optionsChanged.map((o) => '${o.title}: ${_getPatchOptionValue(p.name, o)}').join(", ")}]');
|
||||
}).join(', ');
|
||||
}
|
||||
|
||||
String _getSuggestedVersion(String packageName) {
|
||||
String suggestedVersion = _patcherAPI.getSuggestedVersion(_app.packageName);
|
||||
if (suggestedVersion.isEmpty) {
|
||||
suggestedVersion = 'Any';
|
||||
}
|
||||
return suggestedVersion;
|
||||
}
|
||||
|
||||
Future<void> copyLogs() async {
|
||||
final info = await AboutInfo.getInfo();
|
||||
|
||||
// Trim out extra lines
|
||||
final logsTrimmed = logs.split('\n');
|
||||
_trimLogs(logsTrimmed, 'succeeded', 'Applied {lineCount} patches');
|
||||
_trimLogs(logsTrimmed, '.dex', 'Compiled {lineCount} dex files');
|
||||
|
||||
// Get patches added / removed
|
||||
final defaultPatches = _patcherAPI
|
||||
.getFilteredPatches(_app.packageName)
|
||||
.where((p) => !p.excluded)
|
||||
.toList();
|
||||
final appliedPatchesNames = _patches.map((p) => p.name).toList();
|
||||
|
||||
final patchesAdded = _patches.where((p) => p.excluded).toList();
|
||||
final patchesRemoved = defaultPatches
|
||||
.where((p) => !appliedPatchesNames.contains(p.name))
|
||||
.map((p) => p.name)
|
||||
.toList();
|
||||
final patchesOptionsChanged = defaultPatches
|
||||
.where(
|
||||
(p) =>
|
||||
appliedPatchesNames.contains(p.name) &&
|
||||
p.options.any((o) => _getPatchOptionValue(p.name, o) != o.value),
|
||||
)
|
||||
.toList();
|
||||
|
||||
// Add Info
|
||||
final formattedLogs = [
|
||||
'- Device Info',
|
||||
'ReVanced Manager: ${info['version']}',
|
||||
'Build: ${info['flavor']}',
|
||||
'Model: ${info['model']}',
|
||||
'Android version: ${info['androidVersion']}',
|
||||
'Supported architectures: ${info['supportedArch'].join(", ")}',
|
||||
'Root permissions: ${isRooted ? 'Yes' : 'No'}',
|
||||
|
||||
'Root permissions: ${isRooted ? 'Yes' : 'No'}', //
|
||||
|
||||
'\n- Patch Info',
|
||||
'App: ${_app.packageName} v${_app.version}',
|
||||
'App: ${_app.packageName} v${_app.version} (Suggested: ${_getSuggestedVersion(_app.packageName)})',
|
||||
'Patches version: ${_managerAPI.patchesVersion}',
|
||||
'Patches: ${_patches.map((p) => p.name + (p.options.isEmpty ? '' : ' [${p.options.map((o) => '${o.title}: ${o.value}').join(", ")}]')).toList().join(", ")}',
|
||||
|
||||
'Patches added: ${_formatPatches(patchesAdded, 'Default')}',
|
||||
'Patches removed: ${patchesRemoved.isEmpty ? 'None' : patchesRemoved.join(', ')}',
|
||||
'Default patch options changed: ${_formatPatches(patchesOptionsChanged, 'None')}', //
|
||||
|
||||
'\n- Settings',
|
||||
'Allow changing patch selection: ${_managerAPI.isPatchesChangeEnabled()}',
|
||||
'Version compatibility check: ${_managerAPI.isVersionCompatibilityCheckEnabled()}',
|
||||
'Show universal patches: ${_managerAPI.areUniversalPatchesEnabled()}',
|
||||
'Patches source: ${_managerAPI.getPatchesRepo()}',
|
||||
'Integration source: ${_managerAPI.getIntegrationsRepo()}',
|
||||
|
||||
'Integration source: ${_managerAPI.getIntegrationsRepo()}', //
|
||||
|
||||
'\n- Logs',
|
||||
logs,
|
||||
logsTrimmed.join('\n'),
|
||||
];
|
||||
|
||||
Clipboard.setData(ClipboardData(text: formattedLogs.join('\n')));
|
||||
@@ -218,26 +290,24 @@ class InstallerViewModel extends BaseViewModel {
|
||||
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'),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: I18nText('noButton'),
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('yesButton'),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
copyLogs();
|
||||
showPopupScreenshotWarning = true;
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: I18nText('yesButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -250,11 +320,10 @@ class InstallerViewModel extends BaseViewModel {
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => AlertDialog(
|
||||
builder: (innerContext) => 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(
|
||||
@@ -302,31 +371,68 @@ class InstallerViewModel extends BaseViewModel {
|
||||
installType.value = selected!;
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: I18nText(
|
||||
'installerView.warning',
|
||||
child: Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
CustomMaterialButton(
|
||||
label: I18nText('cancelButton'),
|
||||
isFilled: false,
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(innerContext).pop();
|
||||
},
|
||||
child: I18nText('cancelButton'),
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('installerView.installButton'),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(innerContext).pop();
|
||||
installResult(context, installType.value == 1);
|
||||
},
|
||||
child: I18nText('installerView.installButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
installResult(context, false);
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (innerContext) => AlertDialog(
|
||||
title: I18nText(
|
||||
'warning',
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
content: I18nText('installerView.warning'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(innerContext).pop();
|
||||
},
|
||||
child: I18nText('cancelButton'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(innerContext).pop();
|
||||
installResult(context, false);
|
||||
},
|
||||
child: I18nText('installerView.installButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,6 +441,7 @@ class InstallerViewModel extends BaseViewModel {
|
||||
isCanceled = true;
|
||||
update(0.5, 'Canceling...', 'Canceling patching process');
|
||||
await _patcherAPI.stopPatcher();
|
||||
await WakelockPlus.disable();
|
||||
update(-100.0, 'Canceled...', 'Press back to exit');
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
@@ -346,15 +453,16 @@ class InstallerViewModel extends BaseViewModel {
|
||||
Future<void> installResult(BuildContext context, bool installAsRoot) async {
|
||||
try {
|
||||
_app.isRooted = installAsRoot;
|
||||
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) {
|
||||
if (headerLogs != 'Installing...') {
|
||||
update(
|
||||
.85,
|
||||
'Installing...',
|
||||
_app.isRooted ? 'Mounting patched app' : 'Installing patched app',
|
||||
);
|
||||
}
|
||||
final int response = await _patcherAPI.installPatchedFile(context, _app);
|
||||
if (response == 0) {
|
||||
isInstalled = true;
|
||||
_app.isFromStorage = false;
|
||||
_app.patchDate = DateTime.now();
|
||||
_app.appliedPatches = _patches.map((p) => p.name).toList();
|
||||
@@ -369,10 +477,23 @@ class InstallerViewModel extends BaseViewModel {
|
||||
}
|
||||
|
||||
await _managerAPI.savePatchedApp(_app);
|
||||
await locator<HomeViewModel>().initialize(context);
|
||||
|
||||
update(1.0, 'Installed!', 'Installed!');
|
||||
update(1.0, 'Installed', 'Installed');
|
||||
} else if (response == 3) {
|
||||
update(
|
||||
.85,
|
||||
'Installation canceled',
|
||||
'Installation canceled',
|
||||
);
|
||||
} else if (response == 10) {
|
||||
installResult(context, installAsRoot);
|
||||
} else {
|
||||
// TODO(aabed): Show error message.
|
||||
update(
|
||||
.85,
|
||||
'Installation failed',
|
||||
'Installation failed',
|
||||
);
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
|
||||
@@ -6,21 +6,17 @@ import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class NavigationView extends StatelessWidget {
|
||||
const NavigationView({Key? key}) : super(key: key);
|
||||
const NavigationView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<NavigationViewModel>.reactive(
|
||||
onViewModelReady: (model) => model.initialize(context),
|
||||
viewModelBuilder: () => locator<NavigationViewModel>(),
|
||||
builder: (context, model, child) => WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (model.currentIndex == 0) {
|
||||
return true;
|
||||
} else {
|
||||
model.setIndex(0);
|
||||
return false;
|
||||
}
|
||||
builder: (context, model, child) => PopScope(
|
||||
canPop: model.currentIndex == 0,
|
||||
onPopInvoked: (bool didPop) => {
|
||||
if (!didPop) model.setIndex(0),
|
||||
},
|
||||
child: Scaffold(
|
||||
body: PageTransitionSwitcher(
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:revanced_manager/models/patch.dart';
|
||||
import 'package:revanced_manager/ui/views/patch_options/patch_options_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_options_fields.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_floating_action_button_extended.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class PatchOptionsView extends StatelessWidget {
|
||||
@@ -18,7 +18,7 @@ class PatchOptionsView extends StatelessWidget {
|
||||
builder: (context, model, child) => GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: Scaffold(
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
floatingActionButton: HapticFloatingActionButtonExtended(
|
||||
onPressed: () async {
|
||||
final bool saved = model.saveOptions(context);
|
||||
if (saved && context.mounted) {
|
||||
@@ -82,8 +82,7 @@ class PatchOptionsView extends StatelessWidget {
|
||||
model.modifyOptions(value, option);
|
||||
},
|
||||
)
|
||||
else if (option.valueType ==
|
||||
'StringArray' ||
|
||||
else if (option.valueType == 'StringArray' ||
|
||||
option.valueType == 'IntArray' ||
|
||||
option.valueType == 'LongArray')
|
||||
IntStringLongListPatchOption(
|
||||
@@ -104,11 +103,11 @@ class PatchOptionsView extends StatelessWidget {
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
CustomMaterialButton(
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
model.showAddOptionDialog(context);
|
||||
},
|
||||
label: Row(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.add),
|
||||
|
||||
@@ -6,7 +6,6 @@ import 'package:revanced_manager/services/manager_api.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/widgets/shared/custom_card.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class PatchOptionsViewModel extends BaseViewModel {
|
||||
@@ -32,17 +31,15 @@ class PatchOptionsViewModel extends BaseViewModel {
|
||||
if (savedOptions.isNotEmpty) {
|
||||
visibleOptions = [
|
||||
...savedOptions,
|
||||
...options
|
||||
.where(
|
||||
(option) =>
|
||||
option.required &&
|
||||
!savedOptions.any((sOption) => sOption.key == option.key),
|
||||
)
|
||||
.toList(),
|
||||
...options.where(
|
||||
(option) =>
|
||||
option.required &&
|
||||
!savedOptions.any((sOption) => sOption.key == option.key),
|
||||
),
|
||||
];
|
||||
} else {
|
||||
visibleOptions = [
|
||||
...options.where((option) => option.required).toList(),
|
||||
...options.where((option) => option.required),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -136,7 +133,6 @@ class PatchOptionsViewModel extends BaseViewModel {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -154,11 +150,11 @@ class PatchOptionsViewModel extends BaseViewModel {
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
CustomMaterialButton(
|
||||
label: I18nText('cancelButton'),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: I18nText('cancelButton'),
|
||||
),
|
||||
],
|
||||
contentPadding: const EdgeInsets.all(8),
|
||||
@@ -188,8 +184,9 @@ class PatchOptionsViewModel extends BaseViewModel {
|
||||
children: [
|
||||
Text(
|
||||
e.title,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
@@ -227,14 +224,9 @@ Future<void> showRequiredOptionNullDialog(
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
title: I18nText('notice'),
|
||||
actions: [
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText(
|
||||
'patchOptionsView.deselectPatch',
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
if (managerAPI.isPatchesChangeEnabled()) {
|
||||
locator<PatcherViewModel>()
|
||||
@@ -256,12 +248,13 @@ Future<void> showRequiredOptionNullDialog(
|
||||
PatchesSelectorViewModel().showPatchesChangeDialog(context);
|
||||
}
|
||||
},
|
||||
child: I18nText('patchOptionsView.deselectPatch'),
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('okButton'),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: I18nText('okButton'),
|
||||
),
|
||||
],
|
||||
content: I18nText(
|
||||
|
||||
@@ -6,10 +6,11 @@ import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/patcherView/app_selector_card.dart';
|
||||
import 'package:revanced_manager/ui/widgets/patcherView/patch_selector_card.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_floating_action_button_extended.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class PatcherView extends StatelessWidget {
|
||||
const PatcherView({Key? key}) : super(key: key);
|
||||
const PatcherView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -19,10 +20,10 @@ class PatcherView extends StatelessWidget {
|
||||
builder: (context, model, child) => Scaffold(
|
||||
floatingActionButton: Visibility(
|
||||
visible: model.showPatchButton(),
|
||||
child: FloatingActionButton.extended(
|
||||
child: HapticFloatingActionButtonExtended(
|
||||
label: I18nText('patcherView.patchButton'),
|
||||
icon: const Icon(Icons.build),
|
||||
onPressed: () async{
|
||||
onPressed: () async {
|
||||
if (model.checkRequiredPatchOption(context)) {
|
||||
final bool proceed = model.showRemovedPatchesDialog(context);
|
||||
if (proceed && context.mounted) {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
@@ -9,7 +13,6 @@ import 'package:revanced_manager/models/patch.dart';
|
||||
import 'package:revanced_manager/models/patched_application.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/services/patcher_api.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||
import 'package:revanced_manager/utils/about_info.dart';
|
||||
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
@@ -20,6 +23,7 @@ class PatcherViewModel extends BaseViewModel {
|
||||
final NavigationService _navigationService = locator<NavigationService>();
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
||||
Set<String> savedPatchNames = {};
|
||||
PatchedApplication? selectedApp;
|
||||
BuildContext? ctx;
|
||||
List<Patch> selectedPatches = [];
|
||||
@@ -51,25 +55,23 @@ class PatcherViewModel extends BaseViewModel {
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('notice'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText(
|
||||
'patcherView.removedPatchesWarningDialogText',
|
||||
translationParams: {'patches': removedPatches.join('\n')},
|
||||
),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('noButton'),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: I18nText('noButton'),
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('yesButton'),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
showArmv7WarningDialog(context);
|
||||
},
|
||||
child: I18nText('yesButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -93,22 +95,20 @@ class PatcherViewModel extends BaseViewModel {
|
||||
context: context ?? ctx,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('notice'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText('patcherView.requiredOptionDialogText'),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('cancelButton'),
|
||||
TextButton(
|
||||
onPressed: () => {
|
||||
Navigator.of(context).pop(),
|
||||
},
|
||||
child: I18nText('cancelButton'),
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('okButton'),
|
||||
FilledButton(
|
||||
onPressed: () => {
|
||||
Navigator.pop(context),
|
||||
navigateToPatchesSelector(),
|
||||
},
|
||||
child: I18nText('okButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -126,20 +126,18 @@ class PatcherViewModel extends BaseViewModel {
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('warning'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText('patcherView.armv7WarningDialogText'),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
label: I18nText('noButton'),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: I18nText('noButton'),
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('yesButton'),
|
||||
isFilled: false,
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
navigateToInstaller();
|
||||
},
|
||||
child: I18nText('yesButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -150,31 +148,46 @@ class PatcherViewModel extends BaseViewModel {
|
||||
}
|
||||
|
||||
String getAppSelectionString() {
|
||||
String text = '${selectedApp!.name} (${selectedApp!.packageName})';
|
||||
if (text.length > 32) {
|
||||
text = '${text.substring(0, 32)}...)';
|
||||
}
|
||||
return text;
|
||||
return '${selectedApp!.name} ${selectedApp!.version}';
|
||||
}
|
||||
|
||||
Future<void> queryVersion(String suggestedVersion) async {
|
||||
await openDefaultBrowser(
|
||||
'${selectedApp!.packageName} apk version $suggestedVersion',
|
||||
);
|
||||
}
|
||||
|
||||
String getSuggestedVersionString(BuildContext context) {
|
||||
String suggestedVersion =
|
||||
_patcherAPI.getSuggestedVersion(selectedApp!.packageName);
|
||||
if (suggestedVersion.isEmpty) {
|
||||
suggestedVersion = FlutterI18n.translate(
|
||||
context,
|
||||
'appSelectorCard.allVersions',
|
||||
);
|
||||
return _patcherAPI.getSuggestedVersion(selectedApp!.packageName);
|
||||
}
|
||||
|
||||
Future<void> openDefaultBrowser(String query) async {
|
||||
if (Platform.isAndroid) {
|
||||
try {
|
||||
const platform = MethodChannel('app.revanced.manager.flutter/browser');
|
||||
await platform.invokeMethod('openBrowser', {'query': query});
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
suggestedVersion = 'v$suggestedVersion';
|
||||
throw 'Platform not supported';
|
||||
}
|
||||
}
|
||||
|
||||
bool isPatchNew(Patch patch) {
|
||||
if (savedPatchNames.isEmpty) {
|
||||
savedPatchNames = _managerAPI
|
||||
.getSavedPatches(selectedApp!.packageName)
|
||||
.map((p) => p.name)
|
||||
.toSet();
|
||||
}
|
||||
if (savedPatchNames.isEmpty) {
|
||||
return false;
|
||||
} else {
|
||||
return !savedPatchNames.contains(patch.name);
|
||||
}
|
||||
return '${FlutterI18n.translate(
|
||||
context,
|
||||
'appSelectorCard.currentVersion',
|
||||
)}: v${selectedApp!.version}\n${FlutterI18n.translate(
|
||||
context,
|
||||
'appSelectorCard.suggestedVersion',
|
||||
)}: $suggestedVersion';
|
||||
}
|
||||
|
||||
Future<void> loadLastSelectedPatches() async {
|
||||
@@ -199,13 +212,24 @@ class PatcherViewModel extends BaseViewModel {
|
||||
.selectedPatches
|
||||
.removeWhere((patch) => patch.compatiblePackages.isEmpty);
|
||||
}
|
||||
this.selectedPatches.addAll(
|
||||
patches.where(
|
||||
(patch) =>
|
||||
isPatchNew(patch) &&
|
||||
!patch.excluded &&
|
||||
!this.selectedPatches.contains(patch),
|
||||
),
|
||||
);
|
||||
final usedPatches = _managerAPI.getUsedPatches(selectedApp!.packageName);
|
||||
for (final patch in usedPatches) {
|
||||
if (!patches.any((p) => p.name == patch.name)) {
|
||||
removedPatches.add('• ${patch.name}');
|
||||
for (final option in patch.options) {
|
||||
_managerAPI.clearPatchOption(
|
||||
selectedApp!.packageName, patch.name, option.key);
|
||||
selectedApp!.packageName,
|
||||
patch.name,
|
||||
option.key,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,12 @@ import 'package:flutter_i18n/flutter_i18n.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_floating_action_button_extended.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/search_bar.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class PatchesSelectorView extends StatefulWidget {
|
||||
const PatchesSelectorView({Key? key}) : super(key: key);
|
||||
const PatchesSelectorView({super.key});
|
||||
|
||||
@override
|
||||
State<PatchesSelectorView> createState() => _PatchesSelectorViewState();
|
||||
@@ -36,7 +37,7 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
||||
builder: (context, model, child) => Scaffold(
|
||||
floatingActionButton: Visibility(
|
||||
visible: model.patches.isNotEmpty,
|
||||
child: FloatingActionButton.extended(
|
||||
child: HapticFloatingActionButtonExtended(
|
||||
label: Row(
|
||||
children: <Widget>[
|
||||
I18nText('patchesSelectorView.doneButton'),
|
||||
@@ -180,11 +181,16 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (model.getQueriedPatches(_query).any((patch) => model.isPatchNew(patch)))
|
||||
if (model
|
||||
.getQueriedPatches(_query)
|
||||
.any((patch) => model.isPatchNew(patch)))
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
model.getPatchCategory(context, 'patchesSelectorView.newPatches'),
|
||||
model.getPatchCategory(
|
||||
context,
|
||||
'patchesSelectorView.newPatches',
|
||||
),
|
||||
...model.getQueriedPatches(_query).map((patch) {
|
||||
if (model.isPatchNew(patch)) {
|
||||
return model.getPatchItem(context, patch);
|
||||
@@ -192,26 +198,40 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
||||
return Container();
|
||||
}
|
||||
}),
|
||||
if (model.getQueriedPatches(_query).any((patch) => !model.isPatchNew(patch) && patch.compatiblePackages.isNotEmpty))
|
||||
model.getPatchCategory(context, 'patchesSelectorView.patches'),
|
||||
if (model.getQueriedPatches(_query).any(
|
||||
(patch) =>
|
||||
!model.isPatchNew(patch) &&
|
||||
patch.compatiblePackages.isNotEmpty,
|
||||
))
|
||||
model.getPatchCategory(
|
||||
context,
|
||||
'patchesSelectorView.patches',
|
||||
),
|
||||
],
|
||||
),
|
||||
...model.getQueriedPatches(_query).map(
|
||||
(patch) {
|
||||
if (patch.compatiblePackages.isNotEmpty && !model.isPatchNew(patch)) {
|
||||
if (patch.compatiblePackages.isNotEmpty &&
|
||||
!model.isPatchNew(patch)) {
|
||||
return model.getPatchItem(context, patch);
|
||||
} else {
|
||||
return Container();
|
||||
}
|
||||
},
|
||||
),
|
||||
if (model.getQueriedPatches(_query).any((patch) => patch.compatiblePackages.isEmpty))
|
||||
if (model
|
||||
.getQueriedPatches(_query)
|
||||
.any((patch) => patch.compatiblePackages.isEmpty))
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
model.getPatchCategory(context, 'patchesSelectorView.universalPatches'),
|
||||
model.getPatchCategory(
|
||||
context,
|
||||
'patchesSelectorView.universalPatches',
|
||||
),
|
||||
...model.getQueriedPatches(_query).map((patch) {
|
||||
if (patch.compatiblePackages.isEmpty && !model.isPatchNew(patch)) {
|
||||
if (patch.compatiblePackages.isEmpty &&
|
||||
!model.isPatchNew(patch)) {
|
||||
return model.getPatchItem(context, patch);
|
||||
} else {
|
||||
return Container();
|
||||
|
||||
@@ -10,7 +10,6 @@ import 'package:revanced_manager/services/patcher_api.dart';
|
||||
import 'package:revanced_manager/services/toast.dart';
|
||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_item.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
@@ -25,6 +24,7 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
||||
locator<PatcherViewModel>().selectedPatches;
|
||||
PatchedApplication? selectedApp = locator<PatcherViewModel>().selectedApp;
|
||||
String? patchesVersion = '';
|
||||
|
||||
bool isDefaultPatchesRepo() {
|
||||
return _managerAPI.getPatchesRepo() == 'revanced/revanced-patches';
|
||||
}
|
||||
@@ -48,6 +48,7 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
||||
});
|
||||
currentSelection.clear();
|
||||
currentSelection.addAll(selectedPatches);
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -92,7 +93,6 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('notice'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText(
|
||||
'patchesSelectorView.setRequiredOption',
|
||||
translationParams: {
|
||||
@@ -100,11 +100,11 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
||||
},
|
||||
),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
label: I18nText('okButton'),
|
||||
FilledButton(
|
||||
onPressed: () => {
|
||||
Navigator.of(context).pop(),
|
||||
},
|
||||
child: I18nText('okButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -128,7 +128,6 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
title: I18nText('warning'),
|
||||
content: I18nText(
|
||||
'patchItem.patchesChangeWarningDialogText',
|
||||
@@ -141,18 +140,17 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('okButton'),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: I18nText('okButton'),
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('patchItem.patchesChangeWarningDialogButton'),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context)
|
||||
..pop()
|
||||
..pop();
|
||||
},
|
||||
child: I18nText('patchItem.patchesChangeWarningDialogButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -186,10 +184,6 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
||||
void selectPatches() {
|
||||
locator<PatcherViewModel>().selectedPatches = selectedPatches;
|
||||
saveSelectedPatches();
|
||||
if (_managerAPI.ctx != null) {
|
||||
Navigator.pop(_managerAPI.ctx!);
|
||||
_managerAPI.ctx = null;
|
||||
}
|
||||
locator<PatcherViewModel>().notifyListeners();
|
||||
}
|
||||
|
||||
@@ -281,14 +275,7 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
||||
}
|
||||
|
||||
bool isPatchNew(Patch patch) {
|
||||
final List<Patch> savedPatches =
|
||||
_managerAPI.getSavedPatches(selectedApp!.packageName);
|
||||
if (savedPatches.isEmpty) {
|
||||
return false;
|
||||
} else {
|
||||
return !savedPatches
|
||||
.any((p) => p.getSimpleName() == patch.getSimpleName());
|
||||
}
|
||||
return locator<PatcherViewModel>().isPatchNew(patch);
|
||||
}
|
||||
|
||||
List<String> getSupportedVersions(Patch patch) {
|
||||
|
||||
@@ -4,15 +4,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/services/toast.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class SManageApiUrl extends BaseViewModel {
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final Toast _toast = locator<Toast>();
|
||||
|
||||
final TextEditingController _apiUrlController = TextEditingController();
|
||||
|
||||
@@ -33,43 +29,47 @@ class SManageApiUrl extends BaseViewModel {
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
CustomTextField(
|
||||
leadingIcon: Icon(
|
||||
Icons.api_outlined,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
inputController: _apiUrlController,
|
||||
label: I18nText('settingsView.selectApiURL'),
|
||||
hint: apiUrl,
|
||||
TextField(
|
||||
controller: _apiUrlController,
|
||||
autocorrect: false,
|
||||
onChanged: (value) => notifyListeners(),
|
||||
decoration: InputDecoration(
|
||||
icon: Icon(
|
||||
Icons.api_outlined,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: FlutterI18n.translate(
|
||||
context,
|
||||
'settingsView.selectApiURL',
|
||||
),
|
||||
hintText: apiUrl,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('cancelButton'),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_apiUrlController.clear();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: I18nText('cancelButton'),
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('okButton'),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
String apiUrl = _apiUrlController.text;
|
||||
if (!apiUrl.startsWith('https')) {
|
||||
apiUrl = 'https://$apiUrl';
|
||||
}
|
||||
_managerAPI.setApiUrl(apiUrl);
|
||||
_toast.showBottom('settingsView.restartAppForChanges');
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: I18nText('okButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -81,23 +81,20 @@ class SManageApiUrl extends BaseViewModel {
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('settingsView.sourcesResetDialogTitle'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText('settingsView.apiURLResetDialogText'),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('noButton'),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: I18nText('noButton'),
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('yesButton'),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
_managerAPI.setApiUrl('');
|
||||
_toast.showBottom('settingsView.restartAppForChanges');
|
||||
Navigator.of(context)
|
||||
..pop()
|
||||
..pop();
|
||||
},
|
||||
child: I18nText('yesButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -4,9 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class SManageKeystorePassword extends BaseViewModel {
|
||||
@@ -33,35 +31,40 @@ class SManageKeystorePassword extends BaseViewModel {
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
CustomTextField(
|
||||
inputController: _keystorePasswordController,
|
||||
label: I18nText('settingsView.selectKeystorePassword'),
|
||||
hint: '',
|
||||
TextField(
|
||||
controller: _keystorePasswordController,
|
||||
autocorrect: false,
|
||||
obscureText: true,
|
||||
onChanged: (value) => notifyListeners(),
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: FlutterI18n.translate(
|
||||
context,
|
||||
'settingsView.selectKeystorePassword',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('cancelButton'),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_keystorePasswordController.clear();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: I18nText('cancelButton'),
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('okButton'),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
final String passwd = _keystorePasswordController.text;
|
||||
_managerAPI.setKeystorePassword(passwd);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: I18nText('okButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -5,9 +5,7 @@ import 'package:flutter_i18n/flutter_i18n.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/services/toast.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class SManageSources extends BaseViewModel {
|
||||
@@ -43,63 +41,105 @@ class SManageSources extends BaseViewModel {
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
CustomTextField(
|
||||
leadingIcon: const Icon(
|
||||
Icons.extension_outlined,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
inputController: _hostSourceController,
|
||||
label: I18nText('settingsView.hostRepositoryLabel'),
|
||||
hint: hostRepository,
|
||||
/*
|
||||
API for accessing the specified repositories
|
||||
If default is used, will use the ReVanced API
|
||||
*/
|
||||
TextField(
|
||||
controller: _hostSourceController,
|
||||
autocorrect: false,
|
||||
onChanged: (value) => notifyListeners(),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
CustomTextField(
|
||||
leadingIcon: Icon(
|
||||
Icons.extension_outlined,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
decoration: InputDecoration(
|
||||
icon: Icon(
|
||||
Icons.rocket_launch_outlined,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: FlutterI18n.translate(
|
||||
context,
|
||||
'settingsView.hostRepositoryLabel',
|
||||
),
|
||||
hintText: hostRepository,
|
||||
),
|
||||
inputController: _orgPatSourceController,
|
||||
label: I18nText('settingsView.orgPatchesLabel'),
|
||||
hint: patchesRepo.split('/')[0],
|
||||
onChanged: (value) => notifyListeners(),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
CustomTextField(
|
||||
leadingIcon: const Icon(
|
||||
Icons.extension_outlined,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
inputController: _patSourceController,
|
||||
label: I18nText('settingsView.sourcesPatchesLabel'),
|
||||
hint: patchesRepo.split('/')[1],
|
||||
// Patches owner's name
|
||||
TextField(
|
||||
controller: _orgPatSourceController,
|
||||
autocorrect: false,
|
||||
onChanged: (value) => notifyListeners(),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
CustomTextField(
|
||||
leadingIcon: Icon(
|
||||
Icons.merge_outlined,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
decoration: InputDecoration(
|
||||
icon: Icon(
|
||||
Icons.extension_outlined,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: FlutterI18n.translate(
|
||||
context,
|
||||
'settingsView.orgPatchesLabel',
|
||||
),
|
||||
hintText: patchesRepo.split('/')[0],
|
||||
),
|
||||
inputController: _orgIntSourceController,
|
||||
label: I18nText('settingsView.orgIntegrationsLabel'),
|
||||
hint: integrationsRepo.split('/')[0],
|
||||
onChanged: (value) => notifyListeners(),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
CustomTextField(
|
||||
leadingIcon: const Icon(
|
||||
Icons.merge_outlined,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
inputController: _intSourceController,
|
||||
label: I18nText('settingsView.sourcesIntegrationsLabel'),
|
||||
hint: integrationsRepo.split('/')[1],
|
||||
// Patches repository's name
|
||||
TextField(
|
||||
controller: _patSourceController,
|
||||
autocorrect: false,
|
||||
onChanged: (value) => notifyListeners(),
|
||||
decoration: InputDecoration(
|
||||
icon: const Icon(
|
||||
Icons.extension_outlined,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: FlutterI18n.translate(
|
||||
context,
|
||||
'settingsView.sourcesPatchesLabel',
|
||||
),
|
||||
hintText: patchesRepo.split('/')[1],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// Integrations owner's name
|
||||
TextField(
|
||||
controller: _orgIntSourceController,
|
||||
autocorrect: false,
|
||||
onChanged: (value) => notifyListeners(),
|
||||
decoration: InputDecoration(
|
||||
icon: Icon(
|
||||
Icons.merge_outlined,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: FlutterI18n.translate(
|
||||
context,
|
||||
'settingsView.orgIntegrationsLabel',
|
||||
),
|
||||
hintText: integrationsRepo.split('/')[0],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// Integrations repository's name
|
||||
TextField(
|
||||
controller: _intSourceController,
|
||||
autocorrect: false,
|
||||
onChanged: (value) => notifyListeners(),
|
||||
decoration: InputDecoration(
|
||||
icon: const Icon(
|
||||
Icons.merge_outlined,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: FlutterI18n.translate(
|
||||
context,
|
||||
'settingsView.sourcesIntegrationsLabel',
|
||||
),
|
||||
hintText: integrationsRepo.split('/')[1],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
I18nText('settingsView.sourcesUpdateNote'),
|
||||
@@ -107,9 +147,7 @@ class SManageSources extends BaseViewModel {
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('cancelButton'),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_orgPatSourceController.clear();
|
||||
_patSourceController.clear();
|
||||
@@ -117,9 +155,9 @@ class SManageSources extends BaseViewModel {
|
||||
_intSourceController.clear();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: I18nText('cancelButton'),
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('okButton'),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
_managerAPI.setRepoUrl(_hostSourceController.text.trim());
|
||||
_managerAPI.setPatchesRepo(
|
||||
@@ -133,6 +171,7 @@ class SManageSources extends BaseViewModel {
|
||||
_toast.showBottom('settingsView.restartAppForChanges');
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: I18nText('okButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -144,16 +183,13 @@ class SManageSources extends BaseViewModel {
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('settingsView.sourcesResetDialogTitle'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText('settingsView.sourcesResetDialogText'),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('noButton'),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: I18nText('noButton'),
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('yesButton'),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
_managerAPI.setRepoUrl('');
|
||||
_managerAPI.setPatchesRepo('');
|
||||
@@ -165,6 +201,7 @@ class SManageSources extends BaseViewModel {
|
||||
..pop()
|
||||
..pop();
|
||||
},
|
||||
child: I18nText('yesButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -51,7 +51,6 @@ class SUpdateLanguage extends BaseViewModel {
|
||||
context: parentContext,
|
||||
builder: (context) => SimpleDialog(
|
||||
title: I18nText('settingsView.languageLabel'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 500,
|
||||
|
||||
@@ -7,7 +7,8 @@ import 'package:flutter_i18n/widgets/I18nText.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_radio_list_tile.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart';
|
||||
|
||||
class SUpdateThemeUI extends StatefulWidget {
|
||||
const SUpdateThemeUI({super.key});
|
||||
@@ -36,14 +37,14 @@ class _SUpdateThemeUIState extends State<SUpdateThemeUI> {
|
||||
),
|
||||
),
|
||||
),
|
||||
trailing: CustomMaterialButton(
|
||||
label: getThemeModeName(),
|
||||
trailing: FilledButton(
|
||||
onPressed: () => {showThemeDialog(context)},
|
||||
child: getThemeModeName(),
|
||||
),
|
||||
onTap: () => {showThemeDialog(context)},
|
||||
),
|
||||
if (managerAPI.isDynamicThemeAvailable)
|
||||
SwitchListTile(
|
||||
HapticSwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: I18nText(
|
||||
'settingsView.dynamicThemeLabel',
|
||||
@@ -122,7 +123,6 @@ class _SUpdateThemeUIState extends State<SUpdateThemeUI> {
|
||||
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,
|
||||
@@ -131,7 +131,7 @@ class _SUpdateThemeUIState extends State<SUpdateThemeUI> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
RadioListTile(
|
||||
HapticRadioListTile(
|
||||
title: I18nText('settingsView.systemThemeLabel'),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
value: 0,
|
||||
@@ -140,7 +140,7 @@ class _SUpdateThemeUIState extends State<SUpdateThemeUI> {
|
||||
newTheme.value = value!;
|
||||
},
|
||||
),
|
||||
RadioListTile(
|
||||
HapticRadioListTile(
|
||||
title: I18nText('settingsView.lightThemeLabel'),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
value: 1,
|
||||
@@ -149,7 +149,7 @@ class _SUpdateThemeUIState extends State<SUpdateThemeUI> {
|
||||
newTheme.value = value!;
|
||||
},
|
||||
),
|
||||
RadioListTile(
|
||||
HapticRadioListTile(
|
||||
title: I18nText('settingsView.darkThemeLabel'),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
value: 2,
|
||||
@@ -164,19 +164,18 @@ class _SUpdateThemeUIState extends State<SUpdateThemeUI> {
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('cancelButton'),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: I18nText('cancelButton'),
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('okButton'),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
setThemeMode(context, newTheme.value);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: I18nText('okButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -13,7 +13,7 @@ import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class SettingsView extends StatelessWidget {
|
||||
const SettingsView({Key? key}) : super(key: key);
|
||||
const SettingsView({super.key});
|
||||
|
||||
static const _settingsDivider =
|
||||
Divider(thickness: 1.0, indent: 20.0, endIndent: 20.0);
|
||||
|
||||
@@ -12,7 +12,6 @@ import 'package:revanced_manager/services/toast.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/widgets/shared/custom_material_button.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
@@ -40,6 +39,15 @@ class SettingsViewModel extends BaseViewModel {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool showUpdateDialog() {
|
||||
return _managerAPI.showUpdateDialog();
|
||||
}
|
||||
|
||||
void setShowUpdateDialog(bool value) {
|
||||
_managerAPI.setShowUpdateDialog(value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool isPatchesChangeEnabled() {
|
||||
return _managerAPI.isPatchesChangeEnabled();
|
||||
}
|
||||
@@ -52,7 +60,6 @@ class SettingsViewModel extends BaseViewModel {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
title: I18nText('warning'),
|
||||
content: I18nText(
|
||||
'settingsView.enablePatchesSelectionWarningText',
|
||||
@@ -65,20 +72,19 @@ class SettingsViewModel extends BaseViewModel {
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('yesButton'),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_managerAPI.setChangingToggleModified(true);
|
||||
_managerAPI.setPatchesChangeEnabled(true);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: I18nText('yesButton'),
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('noButton'),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: I18nText('noButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -87,7 +93,6 @@ class SettingsViewModel extends BaseViewModel {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
title: I18nText('warning'),
|
||||
content: I18nText(
|
||||
'settingsView.disablePatchesSelectionWarningText',
|
||||
@@ -100,21 +105,20 @@ class SettingsViewModel extends BaseViewModel {
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('noButton'),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: I18nText('noButton'),
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('yesButton'),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
_managerAPI.setChangingToggleModified(true);
|
||||
_patchesSelectorViewModel.selectDefaultPatches();
|
||||
_managerAPI.setPatchesChangeEnabled(false);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: I18nText('yesButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -140,6 +144,57 @@ class SettingsViewModel extends BaseViewModel {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool isRequireSuggestedAppVersionEnabled() {
|
||||
return _managerAPI.isRequireSuggestedAppVersionEnabled();
|
||||
}
|
||||
|
||||
Future<void>? showRequireSuggestedAppVersionDialog(
|
||||
BuildContext context,
|
||||
bool value,
|
||||
) {
|
||||
if (!value) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('warning'),
|
||||
content: I18nText(
|
||||
'settingsView.requireSuggestedAppVersionDialogText',
|
||||
child: const Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_managerAPI.enableRequireSuggestedAppVersionStatus(false);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: I18nText('yesButton'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: I18nText('noButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
_managerAPI.enableRequireSuggestedAppVersionStatus(true);
|
||||
|
||||
if (!_managerAPI.suggestedAppVersionSelected) {
|
||||
_patcherViewModel.selectedApp = null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void deleteKeystore() {
|
||||
_managerAPI.deleteKeystore();
|
||||
_toast.showBottom('settingsView.regeneratedKeystore');
|
||||
|
||||
@@ -9,9 +9,9 @@ import 'package:stacked/stacked.dart';
|
||||
|
||||
class AppInfoView extends StatelessWidget {
|
||||
const AppInfoView({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.app,
|
||||
}) : super(key: key);
|
||||
});
|
||||
final PatchedApplication app;
|
||||
|
||||
@override
|
||||
@@ -184,7 +184,7 @@ class AppInfoView extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
I18nText(
|
||||
'appInfoView.unpatchButton',
|
||||
'appInfoView.unmountButton',
|
||||
child: Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
@@ -236,8 +236,8 @@ class AppInfoView extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
subtitle: app.isRooted
|
||||
? I18nText('appInfoView.rootTypeLabel')
|
||||
: I18nText('appInfoView.nonRootTypeLabel'),
|
||||
? I18nText('appInfoView.mountTypeLabel')
|
||||
: I18nText('appInfoView.regularTypeLabel'),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
ListTile(
|
||||
|
||||
@@ -12,7 +12,6 @@ import 'package:revanced_manager/services/toast.dart';
|
||||
import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class AppInfoViewModel extends BaseViewModel {
|
||||
@@ -26,18 +25,17 @@ class AppInfoViewModel extends BaseViewModel {
|
||||
PatchedApplication app,
|
||||
bool onlyUnpatch,
|
||||
) async {
|
||||
bool isUninstalled = true;
|
||||
if (app.isRooted) {
|
||||
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||
if (hasRootPermissions) {
|
||||
await _rootAPI.deleteApp(app.packageName, app.apkFilePath);
|
||||
if (!onlyUnpatch) {
|
||||
await DeviceApps.uninstallApp(app.packageName);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var isUninstalled = onlyUnpatch;
|
||||
|
||||
if (!onlyUnpatch) {
|
||||
// TODO(Someone): Wait for the app to uninstall successfully.
|
||||
isUninstalled = await DeviceApps.uninstallApp(app.packageName);
|
||||
}
|
||||
|
||||
if (isUninstalled && app.isRooted && await _rootAPI.hasRootPermissions()) {
|
||||
await _rootAPI.uninstall(app.packageName);
|
||||
}
|
||||
|
||||
if (isUninstalled) {
|
||||
await _managerAPI.deletePatchedApp(app);
|
||||
locator<HomeViewModel>().initialize(context);
|
||||
@@ -67,12 +65,11 @@ class AppInfoViewModel extends BaseViewModel {
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('appInfoView.rootDialogTitle'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText('appInfoView.rootDialogText'),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
label: I18nText('okButton'),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: I18nText('okButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -83,32 +80,53 @@ class AppInfoViewModel extends BaseViewModel {
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText(
|
||||
'appInfoView.unpatchButton',
|
||||
'appInfoView.unmountButton',
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText(
|
||||
'appInfoView.unpatchDialogText',
|
||||
'appInfoView.unmountDialogText',
|
||||
),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('noButton'),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: I18nText('noButton'),
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('yesButton'),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
uninstallApp(context, app, onlyUnpatch);
|
||||
uninstallApp(context, app, true);
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: I18nText('yesButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
uninstallApp(context, app, onlyUnpatch);
|
||||
Navigator.of(context).pop();
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText(
|
||||
'appInfoView.uninstallButton',
|
||||
),
|
||||
content: I18nText(
|
||||
'appInfoView.uninstallDialogText',
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: I18nText('noButton'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
uninstallApp(context, app, false);
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: I18nText('yesButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,14 +149,13 @@ class AppInfoViewModel extends BaseViewModel {
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('appInfoView.appliedPatchesLabel'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: SingleChildScrollView(
|
||||
child: Text(getAppliedPatchesString(app.appliedPatches)),
|
||||
),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
label: I18nText('okButton'),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: I18nText('okButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
import 'package:skeletons/skeletons.dart';
|
||||
|
||||
class AppSkeletonLoader extends StatelessWidget {
|
||||
const AppSkeletonLoader({Key? key}) : super(key: key);
|
||||
const AppSkeletonLoader({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
|
||||
class InstalledAppItem extends StatefulWidget {
|
||||
const InstalledAppItem({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.pkgName,
|
||||
required this.icon,
|
||||
@@ -13,7 +13,9 @@ class InstalledAppItem extends StatefulWidget {
|
||||
required this.suggestedVersion,
|
||||
required this.installedVersion,
|
||||
this.onTap,
|
||||
}) : super(key: key);
|
||||
this.onLinkTap,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String pkgName;
|
||||
final Uint8List icon;
|
||||
@@ -21,6 +23,7 @@ class InstalledAppItem extends StatefulWidget {
|
||||
final String suggestedVersion;
|
||||
final String installedVersion;
|
||||
final Function()? onTap;
|
||||
final Function()? onLinkTap;
|
||||
|
||||
@override
|
||||
State<InstalledAppItem> createState() => _InstalledAppItemState();
|
||||
@@ -51,39 +54,26 @@ class _InstalledAppItemState extends State<InstalledAppItem> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
widget.name,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.visible,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(widget.pkgName),
|
||||
I18nText(
|
||||
FlutterI18n.translate(
|
||||
context,
|
||||
'installed',
|
||||
translationParams: {
|
||||
'version': 'v${widget.installedVersion}',
|
||||
},
|
||||
),
|
||||
),
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 4,
|
||||
children: [
|
||||
I18nText(
|
||||
'suggested',
|
||||
translationParams: {
|
||||
'version': widget.suggestedVersion.isEmpty
|
||||
? FlutterI18n.translate(
|
||||
context,
|
||||
'appSelectorCard.allVersions',
|
||||
)
|
||||
: 'v${widget.suggestedVersion}',
|
||||
},
|
||||
Text(
|
||||
widget.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.installedVersion,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
widget.patchesCount == 1
|
||||
? '• ${widget.patchesCount} patch'
|
||||
@@ -91,11 +81,58 @@ class _InstalledAppItemState extends State<InstalledAppItem> {
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
widget.pkgName,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
Material(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(8)),
|
||||
child: InkWell(
|
||||
onTap: widget.onLinkTap,
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(8)),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
I18nText(
|
||||
'suggested',
|
||||
translationParams: {
|
||||
'version': widget.suggestedVersion.isEmpty
|
||||
? FlutterI18n.translate(
|
||||
context,
|
||||
'appSelectorCard.anyVersion',
|
||||
)
|
||||
: widget.suggestedVersion,
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
Icons.search,
|
||||
size: 16,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondaryContainer,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -4,16 +4,19 @@ import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
|
||||
class NotInstalledAppItem extends StatefulWidget {
|
||||
const NotInstalledAppItem({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.patchesCount,
|
||||
required this.suggestedVersion,
|
||||
this.onTap,
|
||||
}) : super(key: key);
|
||||
this.onLinkTap,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final int patchesCount;
|
||||
final String suggestedVersion;
|
||||
final Function()? onTap;
|
||||
final Function()? onLinkTap;
|
||||
|
||||
@override
|
||||
State<NotInstalledAppItem> createState() => _NotInstalledAppItem();
|
||||
@@ -30,68 +33,91 @@ class _NotInstalledAppItem extends State<NotInstalledAppItem> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
alignment: Alignment.center,
|
||||
child: const CircleAvatar(
|
||||
backgroundColor: Colors.transparent,
|
||||
child: Icon(
|
||||
Icons.square_rounded,
|
||||
color: Colors.grey,
|
||||
size: 44,
|
||||
size: 48,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
widget.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
I18nText(
|
||||
'appSelectorCard.notInstalled',
|
||||
child: Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
Wrap(
|
||||
children: [
|
||||
I18nText(
|
||||
'suggested',
|
||||
translationParams: {
|
||||
'version': widget.suggestedVersion.isEmpty
|
||||
? FlutterI18n.translate(
|
||||
context,
|
||||
'appSelectorCard.allVersions',
|
||||
)
|
||||
: 'v${widget.suggestedVersion}',
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
widget.patchesCount == 1
|
||||
? '• ${widget.patchesCount} patch'
|
||||
: '• ${widget.patchesCount} patches',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
widget.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
widget.patchesCount == 1
|
||||
? '• ${widget.patchesCount} patch'
|
||||
: '• ${widget.patchesCount} patches',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
Material(
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(8)),
|
||||
child: InkWell(
|
||||
onTap: widget.onLinkTap,
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(8)),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
I18nText(
|
||||
'suggested',
|
||||
translationParams: {
|
||||
'version': widget.suggestedVersion.isEmpty
|
||||
? FlutterI18n.translate(
|
||||
context,
|
||||
'appSelectorCard.anyVersion',
|
||||
)
|
||||
: widget.suggestedVersion,
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
Icons.search,
|
||||
size: 16,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondaryContainer,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -7,10 +7,10 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class ContributorsCard extends StatefulWidget {
|
||||
const ContributorsCard({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.contributors,
|
||||
}) : super(key: key);
|
||||
});
|
||||
final String title;
|
||||
final List<dynamic> contributors;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
|
||||
//ignore: must_be_immutable
|
||||
class InstalledAppsCard extends StatelessWidget {
|
||||
InstalledAppsCard({Key? key}) : super(key: key);
|
||||
InstalledAppsCard({super.key});
|
||||
|
||||
List<PatchedApplication> apps = locator<HomeViewModel>().patchedInstalledApps;
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
|
||||
@@ -3,14 +3,13 @@ import 'package:flutter_i18n/flutter_i18n.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||
|
||||
class LatestCommitCard extends StatefulWidget {
|
||||
const LatestCommitCard({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.model,
|
||||
required this.parentContext,
|
||||
}) : super(key: key);
|
||||
});
|
||||
final HomeViewModel model;
|
||||
final BuildContext parentContext;
|
||||
|
||||
@@ -56,17 +55,15 @@ class _LatestCommitCardState extends State<LatestCommitCard> {
|
||||
FutureBuilder<bool>(
|
||||
future: model.hasManagerUpdates(),
|
||||
initialData: false,
|
||||
builder: (context, snapshot) => Opacity(
|
||||
opacity: snapshot.hasData && snapshot.data! ? 1.0 : 0.25,
|
||||
child: CustomMaterialButton(
|
||||
label: I18nText('updateButton'),
|
||||
onPressed: snapshot.hasData && snapshot.data!
|
||||
? () => widget.model.showUpdateConfirmationDialog(
|
||||
widget.parentContext,
|
||||
false,
|
||||
)
|
||||
: () => {},
|
||||
builder: (context, snapshot) => FilledButton(
|
||||
onPressed: () => widget.model.showUpdateConfirmationDialog(
|
||||
widget.parentContext,
|
||||
false,
|
||||
!snapshot.data!,
|
||||
),
|
||||
child: (snapshot.hasData && !snapshot.data!)
|
||||
? I18nText('showChangelogButton')
|
||||
: I18nText('showUpdateButton'),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -84,7 +81,7 @@ class _LatestCommitCardState extends State<LatestCommitCard> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const Text('Patches'),
|
||||
const Text('ReVanced Patches'),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
@@ -109,19 +106,17 @@ class _LatestCommitCardState extends State<LatestCommitCard> {
|
||||
),
|
||||
),
|
||||
FutureBuilder<bool>(
|
||||
future: locator<HomeViewModel>().hasPatchesUpdates(),
|
||||
future: model.hasPatchesUpdates(),
|
||||
initialData: false,
|
||||
builder: (context, snapshot) => Opacity(
|
||||
opacity: snapshot.hasData && snapshot.data! ? 1.0 : 0.25,
|
||||
child: CustomMaterialButton(
|
||||
label: I18nText('updateButton'),
|
||||
onPressed: snapshot.hasData && snapshot.data!
|
||||
? () => widget.model.showUpdateConfirmationDialog(
|
||||
widget.parentContext,
|
||||
true,
|
||||
)
|
||||
: () => {},
|
||||
builder: (context, snapshot) => FilledButton(
|
||||
onPressed: () => widget.model.showUpdateConfirmationDialog(
|
||||
widget.parentContext,
|
||||
true,
|
||||
!snapshot.data!,
|
||||
),
|
||||
child: (snapshot.hasData && !snapshot.data!)
|
||||
? I18nText('showChangelogButton')
|
||||
: I18nText('showUpdateButton'),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -3,12 +3,12 @@ import 'package:flutter_i18n/flutter_i18n.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||
|
||||
class UpdateConfirmationDialog extends StatelessWidget {
|
||||
const UpdateConfirmationDialog({super.key, required this.isPatches});
|
||||
class UpdateConfirmationSheet extends StatelessWidget {
|
||||
const UpdateConfirmationSheet({super.key, required this.isPatches, this.changelog = false});
|
||||
|
||||
final bool isPatches;
|
||||
final bool changelog;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final HomeViewModel model = locator<HomeViewModel>();
|
||||
@@ -37,6 +37,7 @@ class UpdateConfirmationDialog extends StatelessWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!changelog)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 40.0,
|
||||
@@ -52,8 +53,8 @@ class UpdateConfirmationDialog extends StatelessWidget {
|
||||
children: [
|
||||
I18nText(
|
||||
isPatches
|
||||
? 'homeView.updatePatchesDialogTitle'
|
||||
: 'homeView.updateDialogTitle',
|
||||
? 'homeView.updatePatchesSheetTitle'
|
||||
: 'homeView.updateSheetTitle',
|
||||
child: const Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
@@ -86,15 +87,14 @@ class UpdateConfirmationDialog extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
CustomMaterialButton(
|
||||
isExpanded: true,
|
||||
label: I18nText('updateButton'),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
isPatches
|
||||
? model.updatePatches(context)
|
||||
: model.updateManager(context);
|
||||
},
|
||||
child: I18nText('updateButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -7,20 +7,29 @@ import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
|
||||
class AppSelectorCard extends StatelessWidget {
|
||||
const AppSelectorCard({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.onPressed,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
final Function() onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final vm = locator<PatcherViewModel>();
|
||||
|
||||
|
||||
String? suggestedVersion;
|
||||
if (vm.selectedApp != null) {
|
||||
suggestedVersion = vm.getSuggestedVersionString(context);
|
||||
}
|
||||
|
||||
return CustomCard(
|
||||
onTap: onPressed,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
I18nText(
|
||||
locator<PatcherViewModel>().selectedApp == null
|
||||
vm.selectedApp == null
|
||||
? 'appSelectorCard.widgetTitle'
|
||||
: 'appSelectorCard.widgetTitleSelected',
|
||||
child: const Text(
|
||||
@@ -32,7 +41,7 @@ class AppSelectorCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (locator<PatcherViewModel>().selectedApp == null)
|
||||
if (vm.selectedApp == null)
|
||||
I18nText('appSelectorCard.widgetSubtitle')
|
||||
else
|
||||
Row(
|
||||
@@ -41,9 +50,9 @@ class AppSelectorCard extends StatelessWidget {
|
||||
height: 18.0,
|
||||
child: ClipOval(
|
||||
child: Image.memory(
|
||||
locator<PatcherViewModel>().selectedApp == null
|
||||
vm.selectedApp == null
|
||||
? Uint8List(0)
|
||||
: locator<PatcherViewModel>().selectedApp!.icon,
|
||||
: vm.selectedApp!.icon,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
@@ -51,22 +60,64 @@ class AppSelectorCard extends StatelessWidget {
|
||||
const SizedBox(width: 6),
|
||||
Flexible(
|
||||
child: Text(
|
||||
locator<PatcherViewModel>().getAppSelectionString(),
|
||||
vm.getAppSelectionString(),
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (locator<PatcherViewModel>().selectedApp == null)
|
||||
if (vm.selectedApp == null)
|
||||
Container()
|
||||
else
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
locator<PatcherViewModel>()
|
||||
.getSuggestedVersionString(context),
|
||||
vm.selectedApp!.packageName,
|
||||
),
|
||||
if (suggestedVersion!.isNotEmpty &&
|
||||
suggestedVersion != vm.selectedApp!.version) ...[
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Material(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(8)),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
vm.queryVersion(suggestedVersion!);
|
||||
},
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(8)),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
I18nText(
|
||||
'suggested',
|
||||
translationParams: {
|
||||
'version': suggestedVersion,
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
Icons.search,
|
||||
size: 16,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondaryContainer,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -7,9 +7,9 @@ import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
|
||||
class PatchSelectorCard extends StatelessWidget {
|
||||
const PatchSelectorCard({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.onPressed,
|
||||
}) : super(key: key);
|
||||
});
|
||||
final Function() onPressed;
|
||||
|
||||
@override
|
||||
@@ -58,7 +58,8 @@ class PatchSelectorCard extends StatelessWidget {
|
||||
|
||||
String _getPatchesSelection() {
|
||||
String text = '';
|
||||
final List<Patch> selectedPatches = locator<PatcherViewModel>().selectedPatches;
|
||||
final List<Patch> selectedPatches =
|
||||
locator<PatcherViewModel>().selectedPatches;
|
||||
selectedPatches.sort((a, b) => a.name.compareTo(b.name));
|
||||
for (final Patch p in selectedPatches) {
|
||||
text += '• ${p.getSimpleName()}\n';
|
||||
|
||||
@@ -4,13 +4,13 @@ import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/models/patch.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/services/toast.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/haptics/haptic_checkbox.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_custom_card.dart';
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class PatchItem extends StatefulWidget {
|
||||
PatchItem({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.simpleName,
|
||||
required this.description,
|
||||
@@ -23,7 +23,7 @@ class PatchItem extends StatefulWidget {
|
||||
required this.onChanged,
|
||||
required this.navigateToOptions,
|
||||
required this.isChangeEnabled,
|
||||
}) : super(key: key);
|
||||
});
|
||||
final String name;
|
||||
final String simpleName;
|
||||
final String description;
|
||||
@@ -57,7 +57,7 @@ class _PatchItemState extends State<PatchItem> {
|
||||
widget._managerAPI.isVersionCompatibilityCheckEnabled() == true
|
||||
? 0.5
|
||||
: 1,
|
||||
child: CustomCard(
|
||||
child: HapticCustomCard(
|
||||
padding: EdgeInsets.only(
|
||||
top: 12,
|
||||
bottom: 16,
|
||||
@@ -89,7 +89,7 @@ class _PatchItemState extends State<PatchItem> {
|
||||
children: [
|
||||
Transform.scale(
|
||||
scale: 1.2,
|
||||
child: Checkbox(
|
||||
child: HapticCheckbox(
|
||||
value: widget.isSelected,
|
||||
activeColor: Theme.of(context).colorScheme.primary,
|
||||
checkColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
@@ -216,7 +216,6 @@ class _PatchItemState extends State<PatchItem> {
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('warning'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText(
|
||||
'patchItem.unsupportedDialogText',
|
||||
translationParams: {
|
||||
@@ -226,9 +225,9 @@ class _PatchItemState extends State<PatchItem> {
|
||||
},
|
||||
),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
label: I18nText('okButton'),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: I18nText('okButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -240,14 +239,13 @@ class _PatchItemState extends State<PatchItem> {
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('notice'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText(
|
||||
'patchItem.unsupportedRequiredOption',
|
||||
),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
label: I18nText('okButton'),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: I18nText('okButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -400,7 +400,9 @@ class _TextFieldForPatchOptionState extends State<TextFieldForPatchOption> {
|
||||
final bool isStringOption = widget.optionType.contains('String');
|
||||
final bool isArrayOption = widget.optionType.contains('Array');
|
||||
selectedKey ??= widget.selectedKey;
|
||||
controller.text = !isStringOption && isArrayOption && selectedKey == '' &&
|
||||
controller.text = !isStringOption &&
|
||||
isArrayOption &&
|
||||
selectedKey == '' &&
|
||||
(widget.value != null && widget.value.toString().startsWith('['))
|
||||
? ''
|
||||
: widget.value ?? '';
|
||||
@@ -424,8 +426,11 @@ class _TextFieldForPatchOptionState extends State<TextFieldForPatchOption> {
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: e.key,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondaryContainer,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
@@ -519,7 +524,8 @@ class _TextFieldForPatchOptionState extends State<TextFieldForPatchOption> {
|
||||
}
|
||||
break;
|
||||
case 'patchOptionsView.selectFolder':
|
||||
final DirectoryLocation? result = await FlutterFileDialog.pickDirectory();
|
||||
final DirectoryLocation? result =
|
||||
await FlutterFileDialog.pickDirectory();
|
||||
if (result != null) {
|
||||
controller.text = result.toString();
|
||||
widget.onChanged(controller.text);
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:flutter_i18n/flutter_i18n.dart';
|
||||
import 'package:revanced_manager/utils/about_info.dart';
|
||||
|
||||
class AboutWidget extends StatefulWidget {
|
||||
const AboutWidget({Key? key, this.padding}) : super(key: key);
|
||||
const AboutWidget({super.key, this.padding});
|
||||
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomSwitch extends StatelessWidget {
|
||||
const CustomSwitch({
|
||||
Key? key,
|
||||
required this.onChanged,
|
||||
required this.value,
|
||||
}) : super(key: key);
|
||||
final ValueChanged<bool> onChanged;
|
||||
final bool value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => onChanged(!value),
|
||||
child: SizedBox(
|
||||
height: 25,
|
||||
width: 50,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
AnimatedContainer(
|
||||
height: 25,
|
||||
width: 50,
|
||||
curve: Curves.ease,
|
||||
duration: const Duration(milliseconds: 400),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(25.0),
|
||||
),
|
||||
color: value
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
AnimatedAlign(
|
||||
curve: Curves.ease,
|
||||
duration: const Duration(milliseconds: 400),
|
||||
alignment: !value ? Alignment.centerLeft : Alignment.centerRight,
|
||||
child: Container(
|
||||
height: 20,
|
||||
width: 20,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 3),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: value
|
||||
? Theme.of(context).colorScheme.primaryContainer
|
||||
: Theme.of(context).colorScheme.surface,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12.withOpacity(0.1),
|
||||
spreadRadius: 0.5,
|
||||
blurRadius: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/custom_switch.dart';
|
||||
|
||||
class CustomSwitchTile extends StatelessWidget {
|
||||
const CustomSwitchTile({
|
||||
Key? key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.value,
|
||||
required this.onTap,
|
||||
this.padding,
|
||||
}) : super(key: key);
|
||||
final Widget title;
|
||||
final Widget subtitle;
|
||||
final bool value;
|
||||
final Function(bool) onTap;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
contentPadding: padding ?? EdgeInsets.zero,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
onTap: () => onTap(!value),
|
||||
trailing: CustomSwitch(
|
||||
value: value,
|
||||
onChanged: onTap,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomTextField extends StatelessWidget {
|
||||
const CustomTextField({
|
||||
Key? key,
|
||||
required this.inputController,
|
||||
required this.label,
|
||||
required this.hint,
|
||||
this.leadingIcon,
|
||||
required this.onChanged,
|
||||
}) : super(key: key);
|
||||
final TextEditingController inputController;
|
||||
final Widget label;
|
||||
final String hint;
|
||||
final Widget? leadingIcon;
|
||||
final Function(String)? onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: TextField(
|
||||
controller: inputController,
|
||||
onChanged: onChanged,
|
||||
keyboardType: TextInputType.text,
|
||||
decoration: InputDecoration(
|
||||
icon: leadingIcon,
|
||||
label: label,
|
||||
filled: true,
|
||||
fillColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
hintText: hint,
|
||||
hintStyle: TextStyle(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
floatingLabelStyle: MaterialStateTextStyle.resolveWith(
|
||||
(states) => states.contains(MaterialState.focused)
|
||||
? TextStyle(color: Theme.of(context).colorScheme.primary)
|
||||
: TextStyle(color: Theme.of(context).colorScheme.secondary),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
horizontal: 16.0,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 2.0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,12 @@ 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/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_require_suggested_app_version.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_show_update_dialog.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_universal_patches.dart';
|
||||
import 'package:revanced_manager/ui/widgets/settingsView/settings_version_compatibility_check.dart';
|
||||
|
||||
|
||||
class SAdvancedSection extends StatelessWidget {
|
||||
const SAdvancedSection({super.key});
|
||||
|
||||
@@ -19,7 +20,9 @@ class SAdvancedSection extends StatelessWidget {
|
||||
title: 'settingsView.advancedSectionTitle',
|
||||
children: const <Widget>[
|
||||
SAutoUpdatePatches(),
|
||||
SShowUpdateDialog(),
|
||||
SEnablePatchesSelection(),
|
||||
SRequireSuggestedAppVersion(),
|
||||
SVersionCompatibilityCheck(),
|
||||
SUniversalPatches(),
|
||||
SManageSourcesUI(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_i18n/widgets/I18nText.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart';
|
||||
|
||||
class SAutoUpdatePatches extends StatefulWidget {
|
||||
const SAutoUpdatePatches({super.key});
|
||||
@@ -14,7 +15,7 @@ final _settingsViewModel = SettingsViewModel();
|
||||
class _SAutoUpdatePatchesState extends State<SAutoUpdatePatches> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SwitchListTile(
|
||||
return HapticSwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: I18nText(
|
||||
'settingsView.autoUpdatePatchesLabel',
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_i18n/widgets/I18nText.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart';
|
||||
|
||||
class SEnablePatchesSelection extends StatefulWidget {
|
||||
const SEnablePatchesSelection({super.key});
|
||||
|
||||
@override
|
||||
State<SEnablePatchesSelection> createState() => _SEnablePatchesSelectionState();
|
||||
State<SEnablePatchesSelection> createState() =>
|
||||
_SEnablePatchesSelectionState();
|
||||
}
|
||||
|
||||
final _settingsViewModel = SettingsViewModel();
|
||||
@@ -14,7 +16,7 @@ final _settingsViewModel = SettingsViewModel();
|
||||
class _SEnablePatchesSelectionState extends State<SEnablePatchesSelection> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SwitchListTile(
|
||||
return HapticSwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: I18nText(
|
||||
'settingsView.enablePatchesSelectionLabel',
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:flutter_i18n/widgets/I18nText.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_keystore_password.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/shared/custom_material_button.dart';
|
||||
|
||||
final _settingsViewModel = SettingsViewModel();
|
||||
|
||||
@@ -151,20 +150,18 @@ class SExportSection extends StatelessWidget {
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText(dialogTitle),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText(dialogText),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('noButton'),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: I18nText('noButton'),
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('yesButton'),
|
||||
FilledButton(
|
||||
onPressed: () => {
|
||||
Navigator.of(context).pop(),
|
||||
dialogAction(),
|
||||
},
|
||||
child: I18nText('yesButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -176,20 +173,18 @@ class SExportSection extends StatelessWidget {
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('settingsView.regenerateKeystoreDialogTitle'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText('settingsView.regenerateKeystoreDialogText'),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('noButton'),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: I18nText('noButton'),
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('yesButton'),
|
||||
FilledButton(
|
||||
onPressed: () => {
|
||||
Navigator.of(context).pop(),
|
||||
_settingsViewModel.deleteKeystore(),
|
||||
},
|
||||
child: I18nText('yesButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_i18n/widgets/I18nText.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart';
|
||||
|
||||
class SRequireSuggestedAppVersion extends StatefulWidget {
|
||||
const SRequireSuggestedAppVersion({super.key});
|
||||
|
||||
@override
|
||||
State<SRequireSuggestedAppVersion> createState() =>
|
||||
_SRequireSuggestedAppVersionState();
|
||||
}
|
||||
|
||||
final _settingsViewModel = SettingsViewModel();
|
||||
|
||||
class _SRequireSuggestedAppVersionState
|
||||
extends State<SRequireSuggestedAppVersion> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HapticSwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: I18nText(
|
||||
'settingsView.requireSuggestedAppVersionLabel',
|
||||
child: const Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
subtitle: I18nText('settingsView.requireSuggestedAppVersionHint'),
|
||||
value: _settingsViewModel.isRequireSuggestedAppVersionEnabled(),
|
||||
onChanged: (value) async {
|
||||
await _settingsViewModel.showRequireSuggestedAppVersionDialog(
|
||||
context,
|
||||
value,
|
||||
);
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,10 @@ import 'package:flutter_i18n/flutter_i18n.dart';
|
||||
|
||||
class SettingsSection extends StatelessWidget {
|
||||
const SettingsSection({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.children,
|
||||
}) : super(key: key);
|
||||
});
|
||||
final String title;
|
||||
final List<Widget> children;
|
||||
|
||||
|
||||
39
lib/ui/widgets/settingsView/settings_show_update_dialog.dart
Normal file
39
lib/ui/widgets/settingsView/settings_show_update_dialog.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_i18n/widgets/I18nText.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart';
|
||||
|
||||
class SShowUpdateDialog extends StatefulWidget {
|
||||
const SShowUpdateDialog({super.key});
|
||||
|
||||
@override
|
||||
State<SShowUpdateDialog> createState() => _SShowUpdateDialogState();
|
||||
}
|
||||
|
||||
final _settingsViewModel = SettingsViewModel();
|
||||
|
||||
class _SShowUpdateDialogState extends State<SShowUpdateDialog> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HapticSwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: I18nText(
|
||||
'settingsView.showUpdateDialogLabel',
|
||||
child: const Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
subtitle: I18nText('settingsView.showUpdateDialogHint'),
|
||||
value: _settingsViewModel.showUpdateDialog(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_settingsViewModel.setShowUpdateDialog(value);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,12 @@ import 'package:flutter_i18n/flutter_i18n.dart';
|
||||
|
||||
class SettingsTileDialog extends StatelessWidget {
|
||||
const SettingsTileDialog({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.onTap,
|
||||
this.padding,
|
||||
}) : super(key: key);
|
||||
});
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final Function()? onTap;
|
||||
|
||||
@@ -3,24 +3,23 @@ import 'package:flutter_i18n/widgets/I18nText.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/settings_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart';
|
||||
|
||||
class SUniversalPatches extends StatefulWidget {
|
||||
const SUniversalPatches({super.key});
|
||||
|
||||
@override
|
||||
State<SUniversalPatches> createState() =>
|
||||
_SUniversalPatchesState();
|
||||
State<SUniversalPatches> createState() => _SUniversalPatchesState();
|
||||
}
|
||||
|
||||
final _settingsViewModel = SettingsViewModel();
|
||||
final _patchesSelectorViewModel = PatchesSelectorViewModel();
|
||||
final _patcherViewModel = PatcherViewModel();
|
||||
|
||||
class _SUniversalPatchesState
|
||||
extends State<SUniversalPatches> {
|
||||
class _SUniversalPatchesState extends State<SUniversalPatches> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SwitchListTile(
|
||||
return HapticSwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: I18nText(
|
||||
'settingsView.universalPatchesLabel',
|
||||
|
||||
@@ -3,23 +3,26 @@ import 'package:flutter_i18n/widgets/I18nText.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/settings_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart';
|
||||
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
||||
|
||||
class SVersionCompatibilityCheck extends StatefulWidget {
|
||||
const SVersionCompatibilityCheck({super.key});
|
||||
|
||||
@override
|
||||
State<SVersionCompatibilityCheck> createState() => _SVersionCompatibilityCheckState();
|
||||
State<SVersionCompatibilityCheck> createState() =>
|
||||
_SVersionCompatibilityCheckState();
|
||||
}
|
||||
|
||||
final _settingsViewModel = SettingsViewModel();
|
||||
final _patchesSelectorViewModel = PatchesSelectorViewModel();
|
||||
final _patcherViewModel = PatcherViewModel();
|
||||
|
||||
class _SVersionCompatibilityCheckState extends State<SVersionCompatibilityCheck> {
|
||||
class _SVersionCompatibilityCheckState
|
||||
extends State<SVersionCompatibilityCheck> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SwitchListTile(
|
||||
return HapticSwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
title: I18nText(
|
||||
'settingsView.versionCompatibilityCheckLabel',
|
||||
|
||||
@@ -3,12 +3,12 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class SocialMediaItem extends StatelessWidget {
|
||||
const SocialMediaItem({
|
||||
Key? key,
|
||||
super.key,
|
||||
this.icon,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.url,
|
||||
}) : super(key: key);
|
||||
});
|
||||
final Widget? icon;
|
||||
final Widget title;
|
||||
final Widget? subtitle;
|
||||
|
||||
@@ -8,9 +8,9 @@ import 'package:revanced_manager/ui/widgets/shared/custom_icon.dart';
|
||||
|
||||
class SocialMediaWidget extends StatelessWidget {
|
||||
const SocialMediaWidget({
|
||||
Key? key,
|
||||
super.key,
|
||||
this.padding,
|
||||
}) : super(key: key);
|
||||
});
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
@override
|
||||
|
||||
@@ -3,17 +3,16 @@ import 'dart:typed_data';
|
||||
import 'package:flutter/material.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_material_button.dart';
|
||||
import 'package:timeago/timeago.dart';
|
||||
|
||||
class ApplicationItem extends StatefulWidget {
|
||||
const ApplicationItem({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.name,
|
||||
required this.patchDate,
|
||||
required this.onPressed,
|
||||
}) : super(key: key);
|
||||
});
|
||||
final Uint8List icon;
|
||||
final String name;
|
||||
final DateTime patchDate;
|
||||
@@ -24,7 +23,6 @@ class ApplicationItem extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ApplicationItemState extends State<ApplicationItem> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -81,9 +79,9 @@ class _ApplicationItemState extends State<ApplicationItem> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
CustomMaterialButton(
|
||||
label: I18nText('applicationItem.infoButton'),
|
||||
FilledButton(
|
||||
onPressed: widget.onPressed,
|
||||
child: I18nText('applicationItem.infoButton'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -2,13 +2,13 @@ import 'package:flutter/material.dart';
|
||||
|
||||
class CustomCard extends StatelessWidget {
|
||||
const CustomCard({
|
||||
Key? key,
|
||||
super.key,
|
||||
this.isFilled = true,
|
||||
required this.child,
|
||||
this.onTap,
|
||||
this.padding,
|
||||
this.backgroundColor,
|
||||
}) : super(key: key);
|
||||
});
|
||||
final bool isFilled;
|
||||
final Widget child;
|
||||
final Function()? onTap;
|
||||
|
||||
@@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
|
||||
|
||||
class CustomChip extends StatelessWidget {
|
||||
const CustomChip({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.label,
|
||||
this.isSelected = false,
|
||||
this.onSelected,
|
||||
}) : super(key: key);
|
||||
});
|
||||
final Widget label;
|
||||
final bool isSelected;
|
||||
final Function(bool)? onSelected;
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomMaterialButton extends StatelessWidget {
|
||||
const CustomMaterialButton({
|
||||
Key? key,
|
||||
required this.label,
|
||||
this.isFilled = true,
|
||||
this.isExpanded = false,
|
||||
required this.onPressed,
|
||||
}) : super(key: key);
|
||||
final Widget label;
|
||||
final bool isFilled;
|
||||
final bool isExpanded;
|
||||
final Function()? onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextButton(
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all(
|
||||
isExpanded
|
||||
? const EdgeInsets.symmetric(horizontal: 24, vertical: 12)
|
||||
: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
),
|
||||
shape: MaterialStateProperty.all(
|
||||
StadiumBorder(
|
||||
side: isFilled
|
||||
? BorderSide.none
|
||||
: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
backgroundColor: MaterialStateProperty.all(
|
||||
isFilled ? Theme.of(context).colorScheme.primary : Colors.transparent,
|
||||
),
|
||||
foregroundColor: MaterialStateProperty.all(
|
||||
isFilled
|
||||
? Theme.of(context).colorScheme.surface
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
onPressed: onPressed,
|
||||
child: label,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class TimerButton extends StatefulWidget {
|
||||
TimerButton({
|
||||
Key? key,
|
||||
required this.seconds,
|
||||
required this.isRunning,
|
||||
required this.onTimerEnd,
|
||||
this.label = const Text(''),
|
||||
this.isFilled = true,
|
||||
}) : super(key: key);
|
||||
Widget label;
|
||||
bool isFilled;
|
||||
int seconds;
|
||||
final bool isRunning;
|
||||
final Function()? onTimerEnd;
|
||||
|
||||
@override
|
||||
State<TimerButton> createState() => _TimerButtonState();
|
||||
}
|
||||
|
||||
class _TimerButtonState extends State<TimerButton> {
|
||||
void timer(int seconds) {
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
if (seconds > 0) {
|
||||
setState(() {
|
||||
seconds--;
|
||||
});
|
||||
timer(seconds);
|
||||
} else {
|
||||
widget.onTimerEnd!();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
//decrement seconds
|
||||
if (widget.isRunning) {
|
||||
timer(widget.seconds);
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext build) {
|
||||
return TextButton(
|
||||
style: ButtonStyle(
|
||||
shape: MaterialStateProperty.all(
|
||||
StadiumBorder(
|
||||
side: widget.isFilled
|
||||
? BorderSide.none
|
||||
: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
backgroundColor: MaterialStateProperty.all(
|
||||
widget.isFilled
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Colors.transparent,
|
||||
),
|
||||
foregroundColor: MaterialStateProperty.all(
|
||||
widget.isFilled
|
||||
? Theme.of(context).colorScheme.surface
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
onPressed: widget.isRunning ? null : widget.onTimerEnd,
|
||||
child: Text(
|
||||
widget.isRunning ? '${widget.seconds}' : 'Install',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomPopupMenu extends StatelessWidget {
|
||||
const CustomPopupMenu({
|
||||
Key? key,
|
||||
required this.onSelected,
|
||||
required this.children,
|
||||
}) : super(key: key);
|
||||
final Function(dynamic) onSelected;
|
||||
final Map<int, Widget> children;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(useMaterial3: false),
|
||||
child: PopupMenuButton<int>(
|
||||
icon: Icon(
|
||||
Icons.more_vert,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
onSelected: onSelected,
|
||||
itemBuilder: (context) => children.entries
|
||||
.map(
|
||||
(entry) => PopupMenuItem<int>(
|
||||
padding: const EdgeInsets.all(16.0).copyWith(right: 20),
|
||||
value: entry.key,
|
||||
child: entry.value,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
position: PopupMenuPosition.under,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,13 @@ import 'package:flutter/material.dart';
|
||||
|
||||
class CustomSliverAppBar extends StatelessWidget {
|
||||
const CustomSliverAppBar({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.title,
|
||||
this.actions,
|
||||
this.bottom,
|
||||
this.isMainView = false,
|
||||
this.onBackButtonPressed,
|
||||
}) : super(key: key);
|
||||
});
|
||||
final Widget title;
|
||||
final List<Widget>? actions;
|
||||
final PreferredSizeWidget? bottom;
|
||||
|
||||
32
lib/ui/widgets/shared/haptics/haptic_checkbox.dart
Normal file
32
lib/ui/widgets/shared/haptics/haptic_checkbox.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class HapticCheckbox extends StatelessWidget {
|
||||
const HapticCheckbox({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
this.activeColor,
|
||||
this.checkColor,
|
||||
this.side,
|
||||
});
|
||||
final bool value;
|
||||
final Function(bool?)? onChanged;
|
||||
final Color? activeColor;
|
||||
final Color? checkColor;
|
||||
final BorderSide? side;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Checkbox(
|
||||
value: value,
|
||||
onChanged: (value) => {
|
||||
HapticFeedback.selectionClick(),
|
||||
if (onChanged != null) onChanged!(value),
|
||||
},
|
||||
activeColor: activeColor,
|
||||
checkColor: checkColor,
|
||||
side: side,
|
||||
);
|
||||
}
|
||||
}
|
||||
32
lib/ui/widgets/shared/haptics/haptic_checkbox_list_tile.dart
Normal file
32
lib/ui/widgets/shared/haptics/haptic_checkbox_list_tile.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class HapticCheckboxListTile extends StatelessWidget {
|
||||
const HapticCheckboxListTile({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.contentPadding,
|
||||
});
|
||||
final bool value;
|
||||
final Function(bool?)? onChanged;
|
||||
final Widget? title;
|
||||
final Widget? subtitle;
|
||||
final EdgeInsetsGeometry? contentPadding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CheckboxListTile(
|
||||
contentPadding: contentPadding ?? EdgeInsets.zero,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
value: value,
|
||||
onChanged: (value) => {
|
||||
HapticFeedback.lightImpact(),
|
||||
if (onChanged != null) onChanged!(value),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
33
lib/ui/widgets/shared/haptics/haptic_custom_card.dart
Normal file
33
lib/ui/widgets/shared/haptics/haptic_custom_card.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
|
||||
class HapticCustomCard extends StatelessWidget {
|
||||
const HapticCustomCard({
|
||||
super.key,
|
||||
this.isFilled = true,
|
||||
required this.child,
|
||||
this.onTap,
|
||||
this.padding,
|
||||
this.backgroundColor,
|
||||
});
|
||||
final bool isFilled;
|
||||
final Widget child;
|
||||
final Function()? onTap;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final Color? backgroundColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomCard(
|
||||
isFilled: isFilled,
|
||||
onTap: () => {
|
||||
HapticFeedback.selectionClick(),
|
||||
if (onTap != null) onTap!(),
|
||||
},
|
||||
padding: padding,
|
||||
backgroundColor: backgroundColor,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class HapticFloatingActionButtonExtended extends StatelessWidget {
|
||||
const HapticFloatingActionButtonExtended({
|
||||
super.key,
|
||||
required this.onPressed,
|
||||
required this.label,
|
||||
this.icon,
|
||||
this.elevation,
|
||||
});
|
||||
final Function()? onPressed;
|
||||
final Widget label;
|
||||
final Widget? icon;
|
||||
final double? elevation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: () => {
|
||||
HapticFeedback.lightImpact(),
|
||||
if (onPressed != null) onPressed!(),
|
||||
},
|
||||
label: label,
|
||||
icon: icon,
|
||||
elevation: elevation,
|
||||
);
|
||||
}
|
||||
}
|
||||
38
lib/ui/widgets/shared/haptics/haptic_radio_list_tile.dart
Normal file
38
lib/ui/widgets/shared/haptics/haptic_radio_list_tile.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class HapticRadioListTile extends StatelessWidget {
|
||||
const HapticRadioListTile({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.groupValue,
|
||||
this.subtitle,
|
||||
this.onChanged,
|
||||
this.contentPadding,
|
||||
});
|
||||
final Widget title;
|
||||
final Widget? subtitle;
|
||||
final int value;
|
||||
final Function(int?)? onChanged;
|
||||
final int groupValue;
|
||||
final EdgeInsetsGeometry? contentPadding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RadioListTile(
|
||||
contentPadding: contentPadding ?? EdgeInsets.zero,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: (val) => {
|
||||
if (val == value) {
|
||||
HapticFeedback.lightImpact(),
|
||||
},
|
||||
|
||||
if (onChanged != null) onChanged!(val),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user