mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2025-05-18 23:16:50 +08:00
Compare commits
64 Commits
Author | SHA1 | Date | |
---|---|---|---|
29db947e25 | |||
fd43ac7581 | |||
e49c19b3cd | |||
06f0e59967 | |||
c5fc5ee93b | |||
159c85bd1f | |||
2460acf0f4 | |||
6495687841 | |||
d229ccb36c | |||
4d6a57ddcf | |||
d161d55aaf | |||
768ad0c9bc | |||
29323d4e20 | |||
630b22e193 | |||
79116f9e67 | |||
eb58475259 | |||
a879ac30fb | |||
c5b0621323 | |||
0462815014 | |||
e64318c947 | |||
b784482788 | |||
2834e8b348 | |||
b23dfd4289 | |||
217d525cb2 | |||
85b166cbda | |||
9a57f8b858 | |||
3bfdc932c2 | |||
48c878af21 | |||
490a7a58fc | |||
567b1a3ace | |||
6a45db8a38 | |||
b272988929 | |||
9828857570 | |||
ecb54d8e44 | |||
344717b021 | |||
da6cf585c0 | |||
9e93177afd | |||
16318efb01 | |||
e83e13b6d4 | |||
7c2c695d88 | |||
c7b227529d | |||
14f49e9d30 | |||
1875c4ee73 | |||
76c68baa1f | |||
68876a4414 | |||
26d7e5b60f | |||
e78af6ae99 | |||
6fe05cd86e | |||
4100d7a391 | |||
c1cc812ea4 | |||
9663e3f0f4 | |||
941f618153 | |||
716a30bf7b | |||
d051ae576b | |||
35e99cb014 | |||
af054fba49 | |||
58d837d641 | |||
6cc1bd21cd | |||
cebfa7c8ae | |||
e2d7ab8f8f | |||
ec77987fcd | |||
b161608d02 | |||
5e7458ff1c | |||
ed06aaa1f5 |
20
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
20
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
@ -29,7 +29,7 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Add the steps to reproduce this bug including your environment.
|
||||
description: Add the steps to reproduce this bug, including your environment.
|
||||
placeholder: Step 1. Download some files. Step 2. ...
|
||||
validations:
|
||||
required: true
|
||||
@ -74,20 +74,20 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Device logs (exported using Manager settings).
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so there is no need for backticks.
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Installer logs (exported using Installer menu option) [unneeded if issue is not during patching].
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
label: Installer logs (exported using Installer menu option) [unneeded if the issue is not during patching].
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so there is no need for backticks.
|
||||
render: shell
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Screenshots or videos
|
||||
label: Screenshots or video
|
||||
description: Add screenshots or videos that show the bug here.
|
||||
placeholder: Drag and drop the screenshots/videos into this box.
|
||||
validations:
|
||||
@ -105,16 +105,16 @@ body:
|
||||
validations:
|
||||
required: false
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
id: acknowledgments
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
label: Acknowledgments
|
||||
description: Your issue will be closed if you haven't done these steps.
|
||||
options:
|
||||
- label: I have searched the existing issues and this is a new and no duplicate or related to another open issue.
|
||||
- label: I have searched the existing issues; this is new and no duplicate or related to another open issue.
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: I filled out all of the requested information in this issue properly.
|
||||
- label: I properly filled out all of the requested information in this issue.
|
||||
required: true
|
||||
- label: The issue is related solely to the ReVanced Manager and not related to patching errors or patches
|
||||
- label: The issue is solely related to ReVanced Manager and not caused by patches.
|
||||
required: true
|
||||
|
11
.github/workflows/analyze.yml
vendored
11
.github/workflows/analyze.yml
vendored
@ -1,14 +1,25 @@
|
||||
name: Analyze Code
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "dev" ]
|
||||
paths:
|
||||
- "**.dart"
|
||||
- ".github/workflows/analyze.yml"
|
||||
pull_request:
|
||||
branches: [ "main", "dev" ]
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
paths:
|
||||
- "**.dart"
|
||||
- ".github/workflows/analyze.yml"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: "Static analysis & format check"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
2
.github/workflows/pull-request-build.yml
vendored
2
.github/workflows/pull-request-build.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
||||
- name: Setup JDK
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
java-version: '11'
|
||||
distribution: 'zulu'
|
||||
cache: 'gradle'
|
||||
- name: Setup Flutter
|
||||
|
4
.github/workflows/release-build.yml
vendored
4
.github/workflows/release-build.yml
vendored
@ -12,10 +12,10 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set env
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
- name: Set up JDK 12
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: "12"
|
||||
java-version: "11"
|
||||
distribution: "zulu"
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
||||
- name: Setup JDK
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
java-version: '11'
|
||||
distribution: 'zulu'
|
||||
cache: 'gradle'
|
||||
- name: Setup Node.js
|
||||
|
@ -24,7 +24,7 @@ If you wish to translate ReVanced Manager, we're accepting translations on [Crow
|
||||
## 🛠️ Building Manager from source
|
||||
1. Setup flutter environment for your [platform](https://docs.flutter.dev/get-started/install)
|
||||
2. Clone the repository locally
|
||||
3. Add your github token in gradle.properties like [this](https://github.com/revanced/revanced-manager/blob/docs/docs/5_building-from-source.md)
|
||||
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)
|
||||
|
@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
compileSdkVersion flutter.compileSdkVersion
|
||||
ndkVersion flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
@ -71,7 +71,7 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
|
||||
// ReVanced
|
||||
implementation "app.revanced:revanced-patcher:11.0.1"
|
||||
implementation "app.revanced:revanced-patcher:11.0.4"
|
||||
|
||||
// Signing & aligning
|
||||
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
|
||||
|
@ -2,23 +2,29 @@
|
||||
package="app.revanced.manager.flutter">
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<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" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
<application
|
||||
android:label="ReVanced Manager"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:largeHeap="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:extractNativeLibs="true"
|
||||
android:enableOnBackInvokedCallback="true">
|
||||
<activity
|
||||
|
@ -1,3 +1,6 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
org.gradle.jvmargs=-Xmx1536M -XX:+UseParallelGC
|
||||
org.gradle.parallel=true
|
||||
org.gradle.daemon=true
|
||||
org.gradle.caching=true
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
@ -1,7 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
|
||||
distributionSha256Sum=6147605a23b4eff6c334927a86ff3508cb5d6722cd624c97ded4c2e8640f1f87
|
||||
distributionSha256Sum=a01b6587e15fe7ed120a0ee299c25982a1eee045abd6a9dd5e216b2f628ef9ac
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip
|
||||
networkTimeout=10000
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
@ -55,8 +55,7 @@
|
||||
"widgetSubtitle": "لم يتم اختيار أي تطبيق",
|
||||
"noAppsLabel": "لم يتم العثور على تطبيقات",
|
||||
"currentVersion": "الحالي",
|
||||
"recommendedVersion": "موصى به",
|
||||
"anyVersion": "أي"
|
||||
"recommendedVersion": "موصى به"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "اختر التعديلات",
|
||||
|
@ -54,8 +54,7 @@
|
||||
"widgetSubtitle": "Heç bir tətbiq seçilməyib",
|
||||
"noAppsLabel": "Heç bir tətbiq tapılmadı",
|
||||
"currentVersion": "Hazırkı",
|
||||
"recommendedVersion": "Tövsiyyə edilən",
|
||||
"anyVersion": "istənilən"
|
||||
"recommendedVersion": "Tövsiyyə edilən"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Yamaqları seçin",
|
||||
|
@ -55,8 +55,7 @@
|
||||
"widgetSubtitle": "Няма избрано приложение",
|
||||
"noAppsLabel": "Няма намерени приложения",
|
||||
"currentVersion": "Текуща",
|
||||
"recommendedVersion": "Препоръчана",
|
||||
"anyVersion": "всяка"
|
||||
"recommendedVersion": "Препоръчана"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Изберете модификации",
|
||||
|
@ -54,8 +54,7 @@
|
||||
"widgetSubtitle": "কোনো অ্যাপ্লিকেশন নির্বাচন করা হয়নি",
|
||||
"noAppsLabel": "কোন অ্যাপ্লিকেশন পাওয়া যায়নি",
|
||||
"currentVersion": "বর্তমান",
|
||||
"recommendedVersion": "প্রস্তাবিত",
|
||||
"anyVersion": "যেকোনো সংস্করণ"
|
||||
"recommendedVersion": "প্রস্তাবিত"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "প্যাচ নির্বাচন করুন",
|
||||
|
@ -54,8 +54,7 @@
|
||||
"widgetSubtitle": "Nebyla vybrána žádná aplikace",
|
||||
"noAppsLabel": "Nebyly nalezeny žádné aplikace",
|
||||
"currentVersion": "Aktuální",
|
||||
"recommendedVersion": "Doporučeno",
|
||||
"anyVersion": "jakákoliv"
|
||||
"recommendedVersion": "Doporučeno"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Vybrat záplaty",
|
||||
|
@ -55,8 +55,7 @@
|
||||
"widgetSubtitle": "Ingen applikation valgt",
|
||||
"noAppsLabel": "Ingen applikationer fundet",
|
||||
"currentVersion": "Nuværende",
|
||||
"recommendedVersion": "Anbefalet",
|
||||
"anyVersion": "enhver"
|
||||
"recommendedVersion": "Anbefalet"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Vælg patches",
|
||||
|
@ -55,8 +55,7 @@
|
||||
"widgetSubtitle": "Keine App ausgewählt",
|
||||
"noAppsLabel": "Keine Apps gefunden",
|
||||
"currentVersion": "Aktuell",
|
||||
"recommendedVersion": "Empfohlen",
|
||||
"anyVersion": "beliebig"
|
||||
"recommendedVersion": "Empfohlen"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Patches auswählen",
|
||||
|
@ -55,8 +55,7 @@
|
||||
"widgetSubtitle": "Δεν έχει επιλεχθεί εφαρμογή",
|
||||
"noAppsLabel": "Δεν βρέθηκαν εφαρμογές",
|
||||
"currentVersion": "Τρέχουσα",
|
||||
"recommendedVersion": "Προτεινόμενη",
|
||||
"anyVersion": "οποιαδήποτε"
|
||||
"recommendedVersion": "Προτεινόμενη"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Επιλέξτε τροποποιήσεις",
|
||||
|
@ -17,21 +17,33 @@
|
||||
"homeView": {
|
||||
"refreshSuccess": "Refreshed successfully",
|
||||
"widgetTitle": "Dashboard",
|
||||
|
||||
"updatesSubtitle": "Updates",
|
||||
"patchedSubtitle": "Patched applications",
|
||||
|
||||
"updatesAvailable": "Updates available",
|
||||
"noUpdates": "No updates available",
|
||||
|
||||
"WIP": "Work in progress...",
|
||||
|
||||
"noInstallations": "No patched applications installed",
|
||||
"installed": "Installed",
|
||||
"installUpdate": "Continue to install the update?",
|
||||
|
||||
"updateDialogTitle": "Update Manager",
|
||||
"updateChangelogTitle": "Changelog",
|
||||
|
||||
"notificationTitle": "Update downloaded",
|
||||
"notificationText": "Tap to install the update",
|
||||
|
||||
"downloadingMessage": "Downloading update...",
|
||||
"downloadedMessage": "Update downloaded!",
|
||||
|
||||
"installingMessage": "Installing update...",
|
||||
|
||||
"errorDownloadMessage": "Unable to download update",
|
||||
"errorInstallMessage": "Unable to install update",
|
||||
|
||||
"noConnection": "No internet connection",
|
||||
"updatesDisabled": "Updating a patched app is currently disabled. Repatch the app again."
|
||||
},
|
||||
@ -50,21 +62,27 @@
|
||||
"patcherView": {
|
||||
"widgetTitle": "Patcher",
|
||||
"patchButton": "Patch",
|
||||
|
||||
"patchDialogText": "You have selected a resource patch and a split APK installation has been detected, so patching errors may occur.\nAre you sure you want to proceed?",
|
||||
"armv7WarningDialogText": "Patching on ARMv7 devices is not yet supported and might fail. Proceed anyways?"
|
||||
"armv7WarningDialogText": "Patching on ARMv7 devices is not yet supported and might fail. Proceed anyways?",
|
||||
"splitApkWarningDialogText": "Patching a split APK is not yet supported and might fail. Proceed anyways?"
|
||||
},
|
||||
"appSelectorCard": {
|
||||
"widgetTitle": "Select an application",
|
||||
"widgetTitleSelected": "Selected application",
|
||||
"widgetSubtitle": "No application selected",
|
||||
|
||||
"noAppsLabel": "No applications found",
|
||||
"notInstalled":"App not installed",
|
||||
|
||||
"currentVersion": "Current",
|
||||
"suggestedVersion": "Suggested",
|
||||
"anyVersion": "any"
|
||||
"allVersions": "All versions"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Select patches",
|
||||
"widgetTitleSelected": "Selected patches",
|
||||
|
||||
"widgetSubtitle": "Select an application first",
|
||||
"widgetEmptySubtitle": "No patches selected"
|
||||
},
|
||||
@ -75,24 +93,33 @@
|
||||
"appSelectorView": {
|
||||
"viewTitle": "Select an application",
|
||||
"searchBarHint": "Search applications",
|
||||
"selectFromStorageButton": "Select from storage",
|
||||
|
||||
"storageButton": "Storage",
|
||||
"selectFromStorageButton": "Select from storage",
|
||||
|
||||
"errorMessage": "Unable to use selected application",
|
||||
|
||||
"downloadToast": "Download function is not available yet",
|
||||
|
||||
"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"
|
||||
},
|
||||
"patchesSelectorView": {
|
||||
"viewTitle": "Select patches",
|
||||
"searchBarHint": "Search patches",
|
||||
|
||||
"doneButton": "Done",
|
||||
|
||||
"default": "Default",
|
||||
"defaultTooltip": "Select all default patches",
|
||||
|
||||
"none": "None",
|
||||
"noneTooltip": "Deselect all patches",
|
||||
|
||||
"loadPatchesSelection": "Load patches selection",
|
||||
"noSavedPatches": "No saved patches for the selected app.\nPress Done to save current selection.",
|
||||
"noPatchesFound": "No patches found for the selected app",
|
||||
|
||||
"selectAllPatchesWarningContent": "You are about to select all patches, that includes non-suggested patches and can cause unwanted behavior."
|
||||
},
|
||||
"patchItem": {
|
||||
@ -105,11 +132,14 @@
|
||||
"installRootButton": "Install as Root",
|
||||
"openButton": "Open",
|
||||
"shareButton": "Share file",
|
||||
|
||||
"notificationTitle": "ReVanced Manager is patching",
|
||||
"notificationText": "Tap to return to the installer",
|
||||
|
||||
"shareApkMenuOption": "Share APK",
|
||||
"exportApkMenuOption": "Export APK",
|
||||
"shareLogMenuOption": "Share log",
|
||||
|
||||
"installErrorDialogTitle": "Error",
|
||||
"installErrorDialogText1": "Root install is not possible with the current patches selection.\nRepatch your app or choose non-root install.",
|
||||
"installErrorDialogText2": "Non-root install is not possible with the current patches selection.\nRepatch your app or choose root install if you have your device rooted.",
|
||||
@ -118,74 +148,95 @@
|
||||
},
|
||||
"settingsView": {
|
||||
"widgetTitle": "Settings",
|
||||
|
||||
"appearanceSectionTitle": "Appearance",
|
||||
"teamSectionTitle": "Team",
|
||||
"infoSectionTitle": "Info",
|
||||
"advancedSectionTitle": "Advanced",
|
||||
"exportSectionTitle": "Import & export",
|
||||
"logsSectionTitle": "Logs",
|
||||
|
||||
"darkThemeLabel": "Dark mode",
|
||||
"darkThemeHint": "Welcome to the dark side",
|
||||
|
||||
"dynamicThemeLabel": "Material You",
|
||||
"dynamicThemeHint": "Enjoy an experience closer to your device",
|
||||
|
||||
"languageLabel": "Language",
|
||||
"englishOption": "English",
|
||||
|
||||
"sourcesLabel": "Sources",
|
||||
"sourcesLabelHint": "Configure your custom sources",
|
||||
"hostRepositoryLabel": "Repository API",
|
||||
"orgPatchesLabel": "Patches organization",
|
||||
"sourcesPatchesLabel": "Patches source",
|
||||
"orgIntegrationsLabel": "Integrations organization",
|
||||
"sourcesIntegrationsLabel": "Integrations source",
|
||||
"sourcesResetDialogTitle": "Reset",
|
||||
"sourcesResetDialogText": "Are you sure you want to reset custom sources to their default values?",
|
||||
"apiURLResetDialogText": "Are you sure you want to reset API URL to its default value?",
|
||||
"contributorsLabel": "Contributors",
|
||||
"contributorsHint": "A list of contributors of ReVanced",
|
||||
"logsLabel": "Logs",
|
||||
"logsHint": "Share Manager's logs",
|
||||
|
||||
"apiURLLabel": "API URL",
|
||||
"apiURLHint": "Configure your custom API URL",
|
||||
"selectApiURL": "API URL",
|
||||
"hostRepositoryLabel": "Repository API",
|
||||
"orgPatchesLabel": "Patches organization",
|
||||
"sourcesPatchesLabel": "Patches source",
|
||||
"orgIntegrationsLabel": "Integrations organization",
|
||||
|
||||
"contributorsLabel": "Contributors",
|
||||
"contributorsHint": "A list of contributors of ReVanced",
|
||||
|
||||
"logsLabel": "Logs",
|
||||
"logsHint": "Share Manager's logs",
|
||||
|
||||
"experimentalUniversalPatchesLabel": "Experimental universal patches support",
|
||||
"experimentalUniversalPatchesHint": "Display all applications to use with universal patches, loading list of apps may be slower",
|
||||
"experimentalPatchesLabel": "Experimental patches support",
|
||||
"experimentalPatchesHint": "Enable usage of unsupported patches in any app version",
|
||||
"enabledExperimentalPatches": "Experimental patches support enabled",
|
||||
"exportSectionTitle": "Import & export",
|
||||
|
||||
"aboutLabel": "About",
|
||||
"snackbarMessage": "Copied to clipboard",
|
||||
"restartAppForChanges": "Restart the app to apply changes",
|
||||
"deleteKeystoreLabel": "Delete keystore",
|
||||
"deleteKeystoreHint": "Delete the keystore used to sign the app",
|
||||
"deleteKeystoreDialogText": "Are you sure you want to delete the keystore used to sign patched applications?",
|
||||
"deletedKeystore": "Keystore deleted",
|
||||
|
||||
"deleteTempDirLabel": "Delete temporary files",
|
||||
"deleteTempDirHint": "Delete unused temporary files",
|
||||
"deletedTempDir": "Temporary files deleted",
|
||||
|
||||
"exportPatchesLabel": "Export patches selection",
|
||||
"exportPatchesHint": "Export patches selection to a JSON file",
|
||||
"exportedPatches": "Patches selection exported",
|
||||
"noExportFileFound": "No patches selection to export",
|
||||
|
||||
"importPatchesLabel": "Import patches selection",
|
||||
"importPatchesHint": "Import patches selection from a JSON file",
|
||||
"importedPatches": "Patches selection imported",
|
||||
|
||||
"resetStoredPatchesLabel": "Reset patches",
|
||||
"resetStoredPatchesHint": "Reset the stored patches selection",
|
||||
"resetStoredPatchesDialogText": "Are you sure you want to reset the stored patches selection?",
|
||||
"resetStoredPatches": "Patches selection has been reset",
|
||||
"jsonSelectorErrorMessage": "Unable to use selected JSON file",
|
||||
|
||||
"deleteLogsLabel": "Delete logs",
|
||||
"deleteLogsHint": "Delete collected manager logs",
|
||||
"deletedLogs": "Logs deleted",
|
||||
|
||||
"deleteKeystoreLabel": "Delete keystore",
|
||||
"deleteKeystoreHint": "Delete the keystore used to sign the app",
|
||||
"deleteKeystoreDialogText": "Are you sure you want to delete the keystore used to sign patched applications?",
|
||||
"deletedKeystore": "Keystore deleted",
|
||||
|
||||
"exportKeystoreLabel": "Export keystore",
|
||||
"exportKeystoreHint": "Export keystore used to sign apps",
|
||||
"exportedKeystore": "Keystore exported",
|
||||
"noKeystoreExportFileFound": "No keystore to export",
|
||||
|
||||
"importKeystoreLabel": "Import keystore",
|
||||
"importKeystoreHint": "Import keystore used to sign apps",
|
||||
"importedKeystore": "Keystore imported",
|
||||
"keystoreSelectorErrorMessage": "Unable to use selected KEYSTORE file",
|
||||
|
||||
"selectKeystorePassword": "Keystore Password",
|
||||
"selectKeystorePasswordHint": "Select keystore password used to sign the apk"
|
||||
"selectKeystorePasswordHint": "Select keystore password used to sign the apk",
|
||||
|
||||
"jsonSelectorErrorMessage": "Unable to use selected JSON file",
|
||||
"keystoreSelectorErrorMessage": "Unable to use selected KEYSTORE file"
|
||||
},
|
||||
"appInfoView": {
|
||||
"widgetTitle": "App info",
|
||||
@ -193,18 +244,22 @@
|
||||
"uninstallButton": "Uninstall",
|
||||
"patchButton": "Patch",
|
||||
"unpatchButton": "Unpatch",
|
||||
"unpatchDialogText": "Are you sure you want to unpatch this app?",
|
||||
"rootDialogTitle": "Error",
|
||||
|
||||
"unpatchDialogText": "Are you sure you want to unpatch this app?",
|
||||
"rootDialogText": "App was installed with superuser permissions, but currently ReVanced Manager has no permissions.\nPlease grant superuser permissions first.",
|
||||
|
||||
"packageNameLabel": "Package name",
|
||||
"originalPackageNameLabel": "Original package name",
|
||||
"installTypeLabel": "Installation type",
|
||||
"rootTypeLabel": "Root",
|
||||
"nonRootTypeLabel": "Non-root",
|
||||
"patchedDateLabel": "Patched date",
|
||||
"patchedDateHint": "{date} at {time}",
|
||||
"appliedPatchesLabel": "Applied patches",
|
||||
|
||||
"patchedDateHint": "{date} at {time}",
|
||||
"appliedPatchesHint": "{quantity} applied patches",
|
||||
|
||||
"updateNotImplemented": "This feature has not been implemented yet"
|
||||
},
|
||||
"contributorsView": {
|
||||
|
@ -55,8 +55,7 @@
|
||||
"widgetSubtitle": "Ninguna aplicación seleccionada",
|
||||
"noAppsLabel": "No se encontraron aplicaciones",
|
||||
"currentVersion": "Versión actual",
|
||||
"recommendedVersion": "Versión recomendada",
|
||||
"anyVersion": "cualquiera"
|
||||
"recommendedVersion": "Versión recomendada"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Seleccionar parches",
|
||||
|
@ -54,8 +54,7 @@
|
||||
"widgetSubtitle": "Ninguna aplicación seleccionada",
|
||||
"noAppsLabel": "No se encontraron aplicaciones",
|
||||
"currentVersion": "Versión actual",
|
||||
"recommendedVersion": "Versión recomendada",
|
||||
"anyVersion": "Cualquier versión"
|
||||
"recommendedVersion": "Versión recomendada"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Seleccionar parches",
|
||||
|
@ -54,8 +54,7 @@
|
||||
"widgetSubtitle": "Sovellusta ei ole valittu",
|
||||
"noAppsLabel": "Sovelluksia ei löytynyt",
|
||||
"currentVersion": "Nykyinen",
|
||||
"recommendedVersion": "Suositeltu",
|
||||
"anyVersion": "mikä tahansa"
|
||||
"recommendedVersion": "Suositeltu"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Valitse paikkaukset",
|
||||
|
@ -55,8 +55,7 @@
|
||||
"widgetSubtitle": "Aucune application sélectionnée",
|
||||
"noAppsLabel": "Aucune application trouvée",
|
||||
"currentVersion": "Version actuelle",
|
||||
"recommendedVersion": "Version recommandée",
|
||||
"anyVersion": "n'importe laquelle"
|
||||
"recommendedVersion": "Version recommandée"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Sélectionner les patchs",
|
||||
|
@ -54,8 +54,7 @@
|
||||
"widgetSubtitle": "कोई ऐप्लकैशन चुना हुआ नहीं",
|
||||
"noAppsLabel": "कोई एप्लिकेशन नहीं मिला।",
|
||||
"currentVersion": "वर्तमान",
|
||||
"recommendedVersion": "अनुशंसित",
|
||||
"anyVersion": "कोई"
|
||||
"recommendedVersion": "अनुशंसित"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "पैच चुने",
|
||||
|
@ -54,8 +54,7 @@
|
||||
"widgetSubtitle": "Nincs kiválasztott alkalmazás",
|
||||
"noAppsLabel": "Nem találhatóak alkalmazások",
|
||||
"currentVersion": "Jelenlegi",
|
||||
"recommendedVersion": "Ajánlott",
|
||||
"anyVersion": "bármely"
|
||||
"recommendedVersion": "Ajánlott"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Patchek kiválasztása",
|
||||
|
@ -55,8 +55,7 @@
|
||||
"widgetSubtitle": "Tidak ada aplikasi yang dipilih",
|
||||
"noAppsLabel": "Aplikasi tidak ditemukan",
|
||||
"currentVersion": "Saat ini",
|
||||
"recommendedVersion": "Rekomendasi",
|
||||
"anyVersion": "semua"
|
||||
"recommendedVersion": "Rekomendasi"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Pilih Modifikasi",
|
||||
|
@ -55,8 +55,7 @@
|
||||
"widgetSubtitle": "Nessuna applicazione selezionata",
|
||||
"noAppsLabel": "Nessuna applicazione trovata",
|
||||
"currentVersion": "Attuale",
|
||||
"recommendedVersion": "Consigliata",
|
||||
"anyVersion": "qualsiasi"
|
||||
"recommendedVersion": "Consigliata"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Seleziona patch",
|
||||
|
@ -54,8 +54,7 @@
|
||||
"widgetSubtitle": "선택된 앱 없음",
|
||||
"noAppsLabel": "앱이 발견되지 않음",
|
||||
"currentVersion": "현재 버전",
|
||||
"recommendedVersion": "권장 버전",
|
||||
"anyVersion": "모든 버전"
|
||||
"recommendedVersion": "권장 버전"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "패치를 선택하세요",
|
||||
|
@ -55,8 +55,7 @@
|
||||
"widgetSubtitle": "Nepasirinkta programa",
|
||||
"noAppsLabel": "Nerastos jokios programos",
|
||||
"currentVersion": "Dabartinė",
|
||||
"recommendedVersion": "Rekomenduojama",
|
||||
"anyVersion": "bet kokia"
|
||||
"recommendedVersion": "Rekomenduojama"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Pasirinkite modifikacijas",
|
||||
|
@ -54,8 +54,7 @@
|
||||
"widgetSubtitle": "Tiada aplikasi dipilih",
|
||||
"noAppsLabel": "Tiada aplikasi ditemui",
|
||||
"currentVersion": "Semasa",
|
||||
"recommendedVersion": "Disyorkan",
|
||||
"anyVersion": "mana-mana"
|
||||
"recommendedVersion": "Disyorkan"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Pilih modifikasi",
|
||||
|
@ -55,8 +55,7 @@
|
||||
"widgetSubtitle": "Nie wybrano aplikacji",
|
||||
"noAppsLabel": "Nie znaleziono aplikacji",
|
||||
"currentVersion": "Aktualna",
|
||||
"recommendedVersion": "Zalecana",
|
||||
"anyVersion": "dowolna"
|
||||
"recommendedVersion": "Zalecana"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Wybierz łatki",
|
||||
|
@ -54,8 +54,7 @@
|
||||
"widgetSubtitle": "Nenhum aplicativo selecionado",
|
||||
"noAppsLabel": "Nenhum app foi encontrado",
|
||||
"currentVersion": "Atual",
|
||||
"recommendedVersion": "Recomendado",
|
||||
"anyVersion": "todos"
|
||||
"recommendedVersion": "Recomendado"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Selecionar patches",
|
||||
|
@ -55,8 +55,7 @@
|
||||
"widgetSubtitle": "Nenhuma aplicação selecionada",
|
||||
"noAppsLabel": "Não foram encontradas aplicações",
|
||||
"currentVersion": "Atual",
|
||||
"recommendedVersion": "Recomendada",
|
||||
"anyVersion": "qualquer"
|
||||
"recommendedVersion": "Recomendada"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Selecionar modificações",
|
||||
|
@ -55,8 +55,7 @@
|
||||
"widgetSubtitle": "Nicio aplicație selectată",
|
||||
"noAppsLabel": "Nu s-au găsit aplicații",
|
||||
"currentVersion": "Actual",
|
||||
"recommendedVersion": "Recomandat",
|
||||
"anyVersion": "oricare"
|
||||
"recommendedVersion": "Recomandat"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Selectați patch-urile",
|
||||
|
@ -55,8 +55,7 @@
|
||||
"widgetSubtitle": "Не выбрано приложение",
|
||||
"noAppsLabel": "Приложения не найдены",
|
||||
"currentVersion": "Текущая",
|
||||
"recommendedVersion": "Рекомендуемая",
|
||||
"anyVersion": "любая"
|
||||
"recommendedVersion": "Рекомендуемая"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Выберите патчи",
|
||||
|
@ -55,8 +55,7 @@
|
||||
"widgetSubtitle": "Nie je vybraná žiadna aplikácia",
|
||||
"noAppsLabel": "Nebola nájdená žiadna aplikácia",
|
||||
"currentVersion": "Aktuálne",
|
||||
"recommendedVersion": "Odporúčané",
|
||||
"anyVersion": "akákoľvek"
|
||||
"recommendedVersion": "Odporúčané"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Vyberte záplaty",
|
||||
|
@ -54,8 +54,7 @@
|
||||
"widgetSubtitle": "Nijedna aplikacija nije izabrana",
|
||||
"noAppsLabel": "Nijedna aplikacija nije pronađena",
|
||||
"currentVersion": "Trenutna",
|
||||
"recommendedVersion": "Preporučena",
|
||||
"anyVersion": "bilo koja"
|
||||
"recommendedVersion": "Preporučena"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Izaberite pečeve",
|
||||
|
@ -54,8 +54,7 @@
|
||||
"widgetSubtitle": "Ниједна апликација није изабрана",
|
||||
"noAppsLabel": "Ниједна апликација није пронађена",
|
||||
"currentVersion": "Тренутна",
|
||||
"recommendedVersion": "Препоручена",
|
||||
"anyVersion": "било која"
|
||||
"recommendedVersion": "Препоручена"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Изаберите печеве",
|
||||
|
@ -54,8 +54,7 @@
|
||||
"widgetSubtitle": "Ingen app vald",
|
||||
"noAppsLabel": "Inga applikationer hittades",
|
||||
"currentVersion": "Nuvarande",
|
||||
"recommendedVersion": "Rekommenderad",
|
||||
"anyVersion": "alla"
|
||||
"recommendedVersion": "Rekommenderad"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Välj patchar",
|
||||
|
@ -55,8 +55,7 @@
|
||||
"widgetSubtitle": "Hakuna programu iliyochaguliwa",
|
||||
"noAppsLabel": "Hakuna programu zilizopatikana",
|
||||
"currentVersion": "Sasa",
|
||||
"recommendedVersion": "Imependekezwa",
|
||||
"anyVersion": "yoyote"
|
||||
"recommendedVersion": "Imependekezwa"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Chagua viraka",
|
||||
|
@ -55,8 +55,7 @@
|
||||
"widgetSubtitle": "Hiçbir uygulama seçilmedi",
|
||||
"noAppsLabel": "Hiçbir uygulama bulunamadı",
|
||||
"currentVersion": "Şu anki",
|
||||
"recommendedVersion": "Önerilen",
|
||||
"anyVersion": "herhangi"
|
||||
"recommendedVersion": "Önerilen"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Yamaları Seçin",
|
||||
|
@ -54,8 +54,7 @@
|
||||
"widgetSubtitle": "کوئی ایپلیکیشن منتخب نہیں",
|
||||
"noAppsLabel": "کوئی ایپلیکیشن نہیں ملی",
|
||||
"currentVersion": "موجودہ",
|
||||
"recommendedVersion": "تجویز کردہ",
|
||||
"anyVersion": "کوئی بھی"
|
||||
"recommendedVersion": "تجویز کردہ"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "پیچز منتخب کریں",
|
||||
|
@ -55,8 +55,7 @@
|
||||
"widgetSubtitle": "Chưa chọn ứng dụng nào",
|
||||
"noAppsLabel": "Không tìm thấy ứng dụng nào",
|
||||
"currentVersion": "Hiện tại",
|
||||
"recommendedVersion": "Đề xuất",
|
||||
"anyVersion": "bất kỳ"
|
||||
"recommendedVersion": "Đề xuất"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "Chọn bản vá",
|
||||
|
@ -54,8 +54,7 @@
|
||||
"widgetSubtitle": "未选择应用程序",
|
||||
"noAppsLabel": "未发现应用程序。",
|
||||
"currentVersion": "当前",
|
||||
"recommendedVersion": "推荐",
|
||||
"anyVersion": "任意"
|
||||
"recommendedVersion": "推荐"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "选择补丁",
|
||||
|
@ -54,8 +54,7 @@
|
||||
"widgetSubtitle": "沒有選擇應用程式",
|
||||
"noAppsLabel": "找不到應用程式",
|
||||
"currentVersion": "目前版本",
|
||||
"recommendedVersion": "推薦版本",
|
||||
"anyVersion": "任何版本"
|
||||
"recommendedVersion": "推薦版本"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "選擇補丁",
|
||||
|
@ -55,8 +55,7 @@
|
||||
"widgetSubtitle": "未選取應用程式",
|
||||
"noAppsLabel": "找不到應用程式",
|
||||
"currentVersion": "目前",
|
||||
"recommendedVersion": "建議",
|
||||
"anyVersion": "任何"
|
||||
"recommendedVersion": "建議"
|
||||
},
|
||||
"patchSelectorCard": {
|
||||
"widgetTitle": "選取修補檔",
|
||||
|
@ -11,6 +11,6 @@ In order to use ReVanced Manager, certain requirements must be met.
|
||||
|
||||
## ⏭️ What's next
|
||||
|
||||
The next page will guide you through patching an application.
|
||||
The next page will guide you through patching an app.
|
||||
|
||||
Continue: [⬇️ Installation](1_installation.md)
|
||||
|
@ -4,11 +4,11 @@ In order to use ReVanced on your Android device, ReVanced Manager must be instal
|
||||
|
||||
## 🪜 Installation steps
|
||||
|
||||
- Download the latest version of ReVanced Manager from [here](https://github.com/revanced/revanced-manager/releases/latest)
|
||||
- Install ReVanced Manager
|
||||
1. Download the latest version of ReVanced Manager from [here](https://github.com/revanced/revanced-manager/releases/latest)
|
||||
2. Install ReVanced Manager
|
||||
|
||||
## ⏭️ What's next
|
||||
|
||||
The next page will guide you through patching an application.
|
||||
The next page will guide you through using ReVanced Manager.
|
||||
|
||||
Continue: [🪛 Usage](2_usage.md)
|
||||
|
@ -1,26 +1,24 @@
|
||||
# 🧩 Patching applications
|
||||
# 🧩 Patching apps
|
||||
|
||||
The following pages will guide you through using ReVanced Manager to patch applications.
|
||||
The following pages will guide you through using ReVanced Manager to patch apps.
|
||||
|
||||
## 🪜 Steps to patch applications
|
||||
## 🪜 Steps to patch apps
|
||||
|
||||
1. Navigate to the **Patcher** tab from the bottom navigation bar
|
||||
2. Tap on the **Select an application** card
|
||||
3. Choose an application to patch[^1]
|
||||
> **Note**: The application version suggested by ReVanced Manager is visible in each application's card. This is important in step 5. below, as most patches are compatible with this version.
|
||||
2. Tap on the **Select an app** card
|
||||
3. Choose an app to patch[^1]
|
||||
> **Note**: The suggested version is visible in each app's card.
|
||||
4. Tap on the **Select patches** card and select the patches you want to apply[^2]
|
||||
> **Warning**: If you see a warning icon next to a patch, it means that the patch is not compatible with the application version you selected in step 3. above. In this case, you should either go back and select a different application version or deselect the patch.
|
||||
5. Tap on the **Done** button to confirm your selection
|
||||
6. Tap on the **Patch** button to start patching
|
||||
> **Warning**: The patching process can take anywhere from 2 to 5 minutes depending on your device. Refrain from exiting ReVanced Manager while the process is still running, otherwise the patching time will increase significantly.
|
||||
7. After patching is complete, tap on the **Install** button to install the patched application
|
||||
> **Note**: If you have root access on your device, you may have the option to mount the patched application on top of the original application.[^4]
|
||||
> Optionally, you may tap on the three vertical dots in the top right to export the patched application to storage.
|
||||
> **Warning**: If you see a warning you can click on it for more information.
|
||||
5. Tap on the **Done** then **Patch** button
|
||||
> **Warning**: The patching process may take ~5 minutes. Exiting the app may increase the time it takes to patch.
|
||||
6. Tap on the **Install** button
|
||||
> **Note**: If you are rooted, you can mount the patched app on top of the original app.[^3]
|
||||
> Optionally, you may export the patched app to storage using the options in the top right corner.
|
||||
|
||||
[^1]: You may be prompted to select an application from storage in case you have no root access on your device. In this case, select the APK file from your device storage.[^3]
|
||||
[^2]: It is suggested to use the default set of patches by tapping on the **Default** chip above the list of patches.
|
||||
[^3]: You can obtain `.apk` files from sites such as [APKMirror](https://www.apkmirror.com/).
|
||||
[^4]: Mounting the patched application on top of the original application will only work if the installed application version matches the version of the application selected in step 3. above.
|
||||
[^1]: Non-root users may be prompted to select an APK from storage, in which case you have to source the APK file yourself. ReVanced does not provide any APK files.
|
||||
[^2]: It is suggested to use the default set of patches by tapping on the **Default** button above the list of patches.
|
||||
[^3]: Mounting the patched app on top of the original app will only work if the installed app version matches the version of the app selected in step 3. above.
|
||||
|
||||
## ⏭️ What's next
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
# 🧰 Managing patched applications
|
||||
# 🧰 Managing patched apps
|
||||
|
||||
After patching an application, you may want to manage it. This page will guide you through managing patched applications.
|
||||
After patching an app, you may want to manage it. This page will guide you through managing patched apps.
|
||||
|
||||
## 🪜 Steps to manage patched applications
|
||||
## 🪜 Steps to manage patched apps
|
||||
|
||||
1. Tap on the **Dashboard** tab in the bottom navigation bar
|
||||
2. Select the **Installed** chip
|
||||
3. Tap on the **Info** button for the application you want to manage
|
||||
3. Tap on the **Info** button for the app you want to manage
|
||||
4. Choose one of the options from the menu
|
||||
|
||||
## ⏭️ What's next
|
||||
|
@ -14,15 +14,15 @@ ReVanced Manager has settings that can be configured to your liking.
|
||||
|
||||
- ### 🧪 Experimental ReVanced Patches support
|
||||
|
||||
Lift application version constraints from ReVanced Patches. This allows you to patch any version of an application, even if the patch is not explicitly compatible with it.
|
||||
Lift app version constraints from ReVanced Patches. This allows you to patch any version of an app, even if the patch is not explicitly compatible with it.
|
||||
|
||||
- ### 🧑🔬 Experimental universal support
|
||||
|
||||
This will show or hide ReVanced Patches, which are not meant for any application in particular but rather for all applications but may not work on all applications.
|
||||
This will show or hide ReVanced Patches, which are not meant for any app in particular but rather for all apps but may not work on all apps.
|
||||
|
||||
- ### 🔑 Export, import or delete keystore
|
||||
|
||||
Manage the keystore used to sign patched applications.
|
||||
Manage the keystore used to sign patched apps.
|
||||
|
||||
- ### 📄 Export, import or reset ReVanced Patches selection
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
# 🛠️ Usage
|
||||
|
||||
The following pages will guide you through using ReVanced Manager to patch applications, manage patched applications, and update ReVanced Manager.
|
||||
The following pages will guide you through using ReVanced Manager to patch apps, manage patched apps, and update ReVanced Manager.
|
||||
|
||||
## 📖 Table of contents
|
||||
|
||||
1. [🧩 Patching applications](2_1_patching.md)
|
||||
2. [🧰 Managing patched applications](2_2_managing.md)
|
||||
1. [🧩 Patching apps](2_1_patching.md)
|
||||
2. [🧰 Managing patched apps](2_2_managing.md)
|
||||
3. [🔄 Updating ReVanced Manager](2_3_updating.md)
|
||||
4. [⚙️ Configuring ReVanced Manager](2_4_settings.md)
|
||||
|
||||
|
@ -2,23 +2,27 @@
|
||||
|
||||
In case you encounter any issues while using ReVanced Manager, please refer to this page for possible solutions.
|
||||
|
||||
- ### 💥 App not installed as package conflicts with an existing package
|
||||
- 💉 Patching fails with an error
|
||||
|
||||
An existing installation of the application you're trying to patch is conflicting with the patched application. Uninstall the existing application before installing the patched application.
|
||||
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
|
||||
|
||||
- ### ❗️Error code `135`, `139` or `1` when patching the application
|
||||
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.
|
||||
|
||||
- ❗️ 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.
|
||||
|
||||
Alternatively, you can use [ReVanced CLI](https://github.com/revanced/revanced-cli) to patch the application.
|
||||
Alternatively, you can use [ReVanced CLI](https://github.com/revanced/revanced-cli) to patch the app.
|
||||
|
||||
- ### 🚫 Non-root install is not possible with the current patches selection
|
||||
- 🚫 Non-root install is not possible with the current patches selection
|
||||
|
||||
Select the **Default** chip when choosing patches.
|
||||
Select the **Default** button when choosing patches.
|
||||
|
||||
- ### 🚨 Patched application crashes on launch
|
||||
- 🚨 Patched app crashes on launch
|
||||
|
||||
Select the **Default** chip when choosing patches.
|
||||
Select the **Default** button when choosing patches.
|
||||
|
||||
## ⏭️ What's next
|
||||
|
||||
|
@ -7,9 +7,15 @@ This documentation explains how to use [ReVanced Manager](https://github.com/rev
|
||||
0. [💼 Prerequisites](0_prerequisites.md)
|
||||
1. [⬇️ Installation](1_installation.md)
|
||||
2. [🛠️ Usage](2_usage.md)
|
||||
1. [🧩 Patching applications](2_1_patching.md)
|
||||
2. [🧰 Managing patched applications](2_2_managing.md)
|
||||
1. [🧩 Patching apps](2_1_patching.md)
|
||||
2. [🧰 Managing patched apps](2_2_managing.md)
|
||||
3. [🔄 Updating ReVanced Manager](2_3_updating.md)
|
||||
4. [⚙️ Configuring ReVanced Manager](2_4_settings.md)
|
||||
3. [🛟 Troubleshooting](3_troubleshooting.md)
|
||||
4. [🛠 Building from source](4_building.md)
|
||||
|
||||
## ⏭️ Start here
|
||||
|
||||
The next page will tell you about the prerequisites for using ReVanced Manager.
|
||||
|
||||
Continue: [💼 Prerequisites](0_prerequisites.md)
|
||||
|
@ -1 +1 @@
|
||||
Patch your favourite apps, right on your device.
|
||||
Patch your favorite apps, right on your device.
|
||||
|
@ -1,6 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
|
||||
@ -8,6 +7,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:revanced_manager/models/patch.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
|
||||
@lazySingleton
|
||||
class GithubAPI {
|
||||
@ -72,6 +72,41 @@ class GithubAPI {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> getLatestManagerRelease(
|
||||
String repoName,
|
||||
) async {
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
'/repos/$repoName/releases',
|
||||
);
|
||||
final Map<String, dynamic> releases = response.data[0];
|
||||
int updates = 0;
|
||||
final String currentVersion =
|
||||
await ManagerAPI().getCurrentManagerVersion();
|
||||
while (response.data[updates]['tag_name'] != 'v$currentVersion') {
|
||||
updates++;
|
||||
}
|
||||
for (int i = 1; i < updates; i++) {
|
||||
releases.update(
|
||||
'body',
|
||||
(value) =>
|
||||
value +
|
||||
'\n' +
|
||||
'# ' +
|
||||
response.data[i]['tag_name'] +
|
||||
'\n' +
|
||||
response.data[i]['body'],
|
||||
);
|
||||
}
|
||||
return releases;
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<String>> getCommits(
|
||||
String packageName,
|
||||
String repoName,
|
||||
|
@ -1,12 +1,12 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:app_installer/app_installer.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:cr_file_saver/file_saver.dart';
|
||||
import 'package:device_apps/device_apps.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.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';
|
||||
@ -144,30 +144,23 @@ class PatcherAPI {
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> getOriginalFilePath(
|
||||
String packageName,
|
||||
String originalFilePath,
|
||||
) async {
|
||||
Future<String> getOriginalFilePath(String packageName) async {
|
||||
try {
|
||||
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||
if (hasRootPermissions) {
|
||||
originalFilePath = await _rootAPI.getOriginalFilePath(
|
||||
packageName,
|
||||
originalFilePath,
|
||||
);
|
||||
return await _rootAPI.getOriginalFilePath(packageName);
|
||||
}
|
||||
return originalFilePath;
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return originalFilePath;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
Future<void> runPatcher(
|
||||
String packageName,
|
||||
String originalFilePath,
|
||||
String apkFilePath,
|
||||
List<Patch> selectedPatches,
|
||||
) async {
|
||||
final bool includeSettings = await needsSettingsPatch(selectedPatches);
|
||||
@ -198,15 +191,16 @@ class PatcherAPI {
|
||||
_outFile = File('${workDir.path}/out.apk');
|
||||
final Directory cacheDir = Directory('${workDir.path}/cache');
|
||||
cacheDir.createSync();
|
||||
String originalFilePath = await getOriginalFilePath(packageName);
|
||||
if (originalFilePath.isEmpty) {
|
||||
originalFilePath = apkFilePath;
|
||||
}
|
||||
try {
|
||||
await patcherChannel.invokeMethod(
|
||||
'runPatcher',
|
||||
{
|
||||
'patchBundleFilePath': patchBundleFile.path,
|
||||
'originalFilePath': await getOriginalFilePath(
|
||||
packageName,
|
||||
originalFilePath,
|
||||
),
|
||||
'originalFilePath': originalFilePath,
|
||||
'inputFilePath': inputFile.path,
|
||||
'patchedFilePath': patchedFile.path,
|
||||
'outFilePath': _outFile!.path,
|
||||
@ -238,10 +232,8 @@ class PatcherAPI {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await AppInstaller.installApk(_outFile!.path);
|
||||
return await DeviceApps.isAppInstalled(
|
||||
patchedApp.packageName,
|
||||
);
|
||||
final install = await InstallPlugin.installApk(_outFile!.path);
|
||||
return install['isSuccess'];
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
|
@ -140,7 +140,8 @@ class RevancedAPI {
|
||||
return null;
|
||||
}
|
||||
|
||||
StreamController<double> managerUpdateProgress = StreamController<double>();
|
||||
StreamController<double> managerUpdateProgress =
|
||||
StreamController<double>.broadcast();
|
||||
|
||||
void updateManagerDownloadProgress(int progress) {
|
||||
managerUpdateProgress.add(progress.toDouble());
|
||||
@ -170,6 +171,7 @@ class RevancedAPI {
|
||||
|
||||
updateManagerDownloadProgress(progress);
|
||||
} else if (result is FileInfo) {
|
||||
disposeManagerUpdateProgress();
|
||||
// The download is complete; convert the FileInfo object to a File object
|
||||
outputFile = File(result.file.path);
|
||||
}
|
||||
|
@ -2,7 +2,9 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:root/root.dart';
|
||||
|
||||
class RootAPI {
|
||||
final String _managerDirPath = '/data/local/tmp/revanced-manager';
|
||||
// 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';
|
||||
|
||||
@ -40,55 +42,62 @@ class RootAPI {
|
||||
seLinux,
|
||||
String filePath,
|
||||
) async {
|
||||
if (permissions.isNotEmpty) {
|
||||
await Root.exec(
|
||||
cmd: 'chmod $permissions "$filePath"',
|
||||
);
|
||||
}
|
||||
if (ownerGroup.isNotEmpty) {
|
||||
await Root.exec(
|
||||
cmd: 'chown $ownerGroup "$filePath"',
|
||||
);
|
||||
}
|
||||
if (seLinux.isNotEmpty) {
|
||||
await Root.exec(
|
||||
cmd: 'chcon $seLinux "$filePath"',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> isAppInstalled(String packageName) async {
|
||||
if (packageName.isNotEmpty) {
|
||||
String? res = await Root.exec(
|
||||
cmd: 'ls "$_managerDirPath/$packageName"',
|
||||
);
|
||||
if (res != null && res.isNotEmpty) {
|
||||
res = await Root.exec(
|
||||
cmd: 'ls "$_serviceDDirPath/$packageName.sh"',
|
||||
);
|
||||
return res != null && res.isNotEmpty;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<List<String>> getInstalledApps() async {
|
||||
try {
|
||||
final String? res = await Root.exec(
|
||||
cmd: 'ls "$_managerDirPath"',
|
||||
);
|
||||
if (res != null) {
|
||||
final List<String> apps = res.split('\n');
|
||||
apps.removeWhere((pack) => pack.isEmpty);
|
||||
return apps.map((pack) => pack.trim()).toList();
|
||||
if (permissions.isNotEmpty) {
|
||||
await Root.exec(
|
||||
cmd: 'chmod $permissions "$filePath"',
|
||||
);
|
||||
}
|
||||
if (ownerGroup.isNotEmpty) {
|
||||
await Root.exec(
|
||||
cmd: 'chown $ownerGroup "$filePath"',
|
||||
);
|
||||
}
|
||||
if (seLinux.isNotEmpty) {
|
||||
await Root.exec(
|
||||
cmd: 'chcon $seLinux "$filePath"',
|
||||
);
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return List.empty();
|
||||
}
|
||||
return List.empty();
|
||||
}
|
||||
|
||||
Future<bool> isAppInstalled(String packageName) async {
|
||||
if (packageName.isNotEmpty) {
|
||||
return fileExists('$_serviceDDirPath/$packageName.sh');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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"',
|
||||
);
|
||||
if (res != null) {
|
||||
final List<String> list = res.split('\n');
|
||||
list.removeWhere((pack) => pack.isEmpty);
|
||||
apps.addAll(list.map((pack) => pack.trim()).toList());
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
return apps;
|
||||
}
|
||||
|
||||
Future<void> deleteApp(String packageName, String originalFilePath) async {
|
||||
@ -98,8 +107,12 @@ class RootAPI {
|
||||
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 "$_managerDirPath/$packageName"',
|
||||
cmd: 'rm -rf "$_revancedOldDirPath/$packageName"',
|
||||
);
|
||||
await Root.exec(
|
||||
cmd: 'rm -rf "$_revancedDirPath/$packageName"',
|
||||
);
|
||||
await Root.exec(
|
||||
cmd: 'rm -rf "$_serviceDDirPath/$packageName.sh"',
|
||||
@ -117,13 +130,13 @@ class RootAPI {
|
||||
try {
|
||||
await deleteApp(packageName, originalFilePath);
|
||||
await Root.exec(
|
||||
cmd: 'mkdir -p "$_managerDirPath/$packageName"',
|
||||
cmd: 'mkdir -p "$_revancedDirPath/$packageName"',
|
||||
);
|
||||
await setPermissions(
|
||||
'0755',
|
||||
'shell:shell',
|
||||
'',
|
||||
'$_managerDirPath/$packageName',
|
||||
'$_revancedDirPath/$packageName',
|
||||
);
|
||||
await saveOriginalFilePath(packageName, originalFilePath);
|
||||
await installServiceDScript(packageName);
|
||||
@ -142,7 +155,7 @@ class RootAPI {
|
||||
Future<void> installServiceDScript(String packageName) async {
|
||||
final String content = '#!/system/bin/sh\n'
|
||||
'while [ "\$(getprop sys.boot_completed | tr -d \'"\'"\'\\\\r\'"\'"\')" != "1" ]; do sleep 3; done\n'
|
||||
'base_path=$_managerDirPath/$packageName/base.apk\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 scriptFilePath = '$_serviceDDirPath/$packageName.sh';
|
||||
@ -164,7 +177,7 @@ class RootAPI {
|
||||
}
|
||||
|
||||
Future<void> installApk(String packageName, String patchedFilePath) async {
|
||||
final String newPatchedFilePath = '$_managerDirPath/$packageName/base.apk';
|
||||
final String newPatchedFilePath = '$_revancedDirPath/$packageName/base.apk';
|
||||
await Root.exec(
|
||||
cmd: 'cp "$patchedFilePath" "$newPatchedFilePath"',
|
||||
);
|
||||
@ -177,7 +190,7 @@ class RootAPI {
|
||||
}
|
||||
|
||||
Future<void> mountApk(String packageName, String originalFilePath) async {
|
||||
final String newPatchedFilePath = '$_managerDirPath/$packageName/base.apk';
|
||||
final String newPatchedFilePath = '$_revancedDirPath/$packageName/base.apk';
|
||||
await Root.exec(
|
||||
cmd: 'am force-stop "$packageName"',
|
||||
);
|
||||
@ -196,21 +209,18 @@ class RootAPI {
|
||||
return res != null && res.isNotEmpty;
|
||||
}
|
||||
|
||||
Future<String> getOriginalFilePath(
|
||||
String packageName,
|
||||
String originalFilePath,
|
||||
) async {
|
||||
Future<String> getOriginalFilePath(String packageName) async {
|
||||
final String originalPath = '$_revancedDirPath/$packageName/original.apk';
|
||||
final String oldOrigPath = '$_revancedOldDirPath/$packageName/original.apk';
|
||||
final bool isInstalled = await isAppInstalled(packageName);
|
||||
if (isInstalled && await isMounted(packageName)) {
|
||||
originalFilePath = '$_managerDirPath/$packageName/original.apk';
|
||||
await setPermissions(
|
||||
'0644',
|
||||
'shell:shell',
|
||||
'u:object_r:apk_data_file:s0',
|
||||
originalFilePath,
|
||||
);
|
||||
if (await fileExists(originalPath)) {
|
||||
return originalPath;
|
||||
} else if (await fileExists(oldOrigPath)) {
|
||||
return oldOrigPath;
|
||||
}
|
||||
}
|
||||
return originalFilePath;
|
||||
return '';
|
||||
}
|
||||
|
||||
Future<void> saveOriginalFilePath(
|
||||
@ -218,15 +228,15 @@ class RootAPI {
|
||||
String originalFilePath,
|
||||
) async {
|
||||
final String originalRootPath =
|
||||
'$_managerDirPath/$packageName/original.apk';
|
||||
'$_revancedDirPath/$packageName/original.apk';
|
||||
await Root.exec(
|
||||
cmd: 'mkdir -p "$_managerDirPath/$packageName"',
|
||||
cmd: 'mkdir -p "$_revancedDirPath/$packageName"',
|
||||
);
|
||||
await setPermissions(
|
||||
'0755',
|
||||
'shell:shell',
|
||||
'',
|
||||
'$_managerDirPath/$packageName',
|
||||
'$_revancedDirPath/$packageName',
|
||||
);
|
||||
await Root.exec(
|
||||
cmd: 'cp "$originalFilePath" "$originalRootPath"',
|
||||
@ -238,4 +248,18 @@ class RootAPI {
|
||||
originalFilePath,
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> fileExists(String path) async {
|
||||
try {
|
||||
final String? res = await Root.exec(
|
||||
cmd: 'ls $path',
|
||||
);
|
||||
return res != null && res.isNotEmpty;
|
||||
} on Exception catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ class _AppSelectorViewState extends State<AppSelectorView> {
|
||||
),
|
||||
),
|
||||
)
|
||||
: model.apps.isEmpty
|
||||
: model.allApps.isEmpty
|
||||
? const AppSkeletonLoader()
|
||||
: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0)
|
||||
@ -113,7 +113,9 @@ class _AppSelectorViewState extends State<AppSelectorView> {
|
||||
),
|
||||
installedVersion: app.versionName!,
|
||||
onTap: () => model.canSelectInstalled(
|
||||
context, app.packageName),
|
||||
context,
|
||||
app.packageName,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
|
@ -45,9 +45,7 @@ class AppSelectorViewModel extends BaseViewModel {
|
||||
.length
|
||||
.compareTo(_patcherAPI.getFilteredPatches(a.packageName).length),
|
||||
);
|
||||
noApps = apps.isEmpty;
|
||||
getAllApps();
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@ -57,7 +55,7 @@ class AppSelectorViewModel extends BaseViewModel {
|
||||
.toSet()
|
||||
.where((name) => !apps.any((app) => app.packageName == name))
|
||||
.toList();
|
||||
|
||||
noApps = allApps.isEmpty;
|
||||
return allApps;
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,13 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:app_installer/app_installer.dart';
|
||||
import 'package:cross_connectivity/cross_connectivity.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.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';
|
||||
@ -39,6 +38,7 @@ class HomeViewModel extends BaseViewModel {
|
||||
List<PatchedApplication> patchedInstalledApps = [];
|
||||
List<PatchedApplication> patchedUpdatableApps = [];
|
||||
String? _latestManagerVersion = '';
|
||||
File? downloadedApk;
|
||||
|
||||
Future<void> initialize(BuildContext context) async {
|
||||
_latestManagerVersion = await _managerAPI.getLatestManagerVersion();
|
||||
@ -51,7 +51,7 @@ class HomeViewModel extends BaseViewModel {
|
||||
_toast.showBottom('homeView.installingMessage');
|
||||
final File? managerApk = await _managerAPI.downloadManager();
|
||||
if (managerApk != null) {
|
||||
await AppInstaller.installApk(managerApk.path);
|
||||
await InstallPlugin.installApk(managerApk.path);
|
||||
} else {
|
||||
_toast.showBottom('homeView.errorDownloadMessage');
|
||||
}
|
||||
@ -72,7 +72,7 @@ class HomeViewModel extends BaseViewModel {
|
||||
_toast.showBottom('homeView.installingMessage');
|
||||
final File? managerApk = await _managerAPI.downloadManager();
|
||||
if (managerApk != null) {
|
||||
await AppInstaller.installApk(managerApk.path);
|
||||
await InstallPlugin.installApk(managerApk.path);
|
||||
} else {
|
||||
_toast.showBottom('homeView.errorDownloadMessage');
|
||||
}
|
||||
@ -112,13 +112,15 @@ class HomeViewModel extends BaseViewModel {
|
||||
|
||||
Future<bool> hasManagerUpdates() async {
|
||||
String currentVersion = await _managerAPI.getCurrentManagerVersion();
|
||||
_latestManagerVersion = await _managerAPI.getLatestManagerVersion();
|
||||
|
||||
// add v to current version
|
||||
if (!currentVersion.startsWith('v')) {
|
||||
currentVersion = 'v$currentVersion';
|
||||
}
|
||||
|
||||
_latestManagerVersion =
|
||||
await _managerAPI.getLatestManagerVersion() ?? currentVersion;
|
||||
|
||||
if (_latestManagerVersion != currentVersion) {
|
||||
return true;
|
||||
}
|
||||
@ -162,74 +164,135 @@ class HomeViewModel extends BaseViewModel {
|
||||
}
|
||||
|
||||
Future<void> updateManager(BuildContext context) async {
|
||||
final ValueNotifier<bool> downloaded = ValueNotifier(false);
|
||||
try {
|
||||
_toast.showBottom('homeView.downloadingMessage');
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => SimpleDialog(
|
||||
contentPadding: const EdgeInsets.all(16.0),
|
||||
title: I18nText(
|
||||
'homeView.downloadingMessage',
|
||||
child: Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.new_releases_outlined,
|
||||
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),
|
||||
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();
|
||||
},
|
||||
builder: (context) => ValueListenableBuilder(
|
||||
valueListenable: downloaded,
|
||||
builder: (context, value, child) {
|
||||
return SimpleDialog(
|
||||
contentPadding: const EdgeInsets.all(16.0),
|
||||
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(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.new_releases_outlined,
|
||||
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),
|
||||
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: 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: CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('cancelButton'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: CustomMaterialButton(
|
||||
label: I18nText('updateButton'),
|
||||
onPressed: () async {
|
||||
await InstallPlugin.installApk(
|
||||
downloadedApk!.path,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
final File? managerApk = await downloadManager();
|
||||
if (managerApk != null) {
|
||||
downloaded.value = true;
|
||||
downloadedApk = managerApk;
|
||||
// await flutterLocalNotificationsPlugin.zonedSchedule(
|
||||
// 0,
|
||||
// FlutterI18n.translate(
|
||||
@ -255,7 +318,7 @@ class HomeViewModel extends BaseViewModel {
|
||||
// UILocalNotificationDateInterpretation.absoluteTime,
|
||||
// );
|
||||
_toast.showBottom('homeView.installingMessage');
|
||||
await AppInstaller.installApk(managerApk.path);
|
||||
await InstallPlugin.installApk(managerApk.path);
|
||||
} else {
|
||||
_toast.showBottom('homeView.errorDownloadMessage');
|
||||
}
|
||||
@ -285,7 +348,7 @@ class HomeViewModel extends BaseViewModel {
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> getLatestManagerRelease() {
|
||||
return _githubAPI.getLatestRelease(_managerAPI.defaultManagerRepo);
|
||||
return _githubAPI.getLatestManagerRelease(_managerAPI.defaultManagerRepo);
|
||||
}
|
||||
|
||||
Future<String?> getLatestPatcherReleaseTime() {
|
||||
|
@ -166,7 +166,7 @@ class InstallerViewModel extends BaseViewModel {
|
||||
try {
|
||||
_app.isRooted = installAsRoot;
|
||||
final bool hasMicroG =
|
||||
_patches.any((p) => p.name.endsWith('microg-support'));
|
||||
_patches.any((p) => p.name.endsWith('MicroG support'));
|
||||
final bool rootMicroG = installAsRoot && hasMicroG;
|
||||
final bool rootFromStorage = installAsRoot && _app.isFromStorage;
|
||||
final bool ytWithoutRootMicroG =
|
||||
|
@ -13,55 +13,68 @@ class NavigationView extends StatelessWidget {
|
||||
return ViewModelBuilder<NavigationViewModel>.reactive(
|
||||
onViewModelReady: (model) => model.initialize(context),
|
||||
viewModelBuilder: () => locator<NavigationViewModel>(),
|
||||
builder: (context, model, child) => Scaffold(
|
||||
body: PageTransitionSwitcher(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return FadeThroughTransition(
|
||||
animation: animation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
fillColor: Theme.of(context).colorScheme.surface,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: model.getViewForIndex(model.currentIndex),
|
||||
),
|
||||
bottomNavigationBar: NavigationBar(
|
||||
onDestinationSelected: model.setIndex,
|
||||
selectedIndex: model.currentIndex,
|
||||
destinations: <Widget>[
|
||||
NavigationDestination(
|
||||
icon: model.isIndexSelected(0)
|
||||
? const Icon(Icons.dashboard)
|
||||
: const Icon(Icons.dashboard_outlined),
|
||||
label: FlutterI18n.translate(
|
||||
context,
|
||||
'navigationView.dashboardTab',
|
||||
builder: (context, model, child) => WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (model.currentIndex == 0) {
|
||||
return true;
|
||||
} else {
|
||||
model.setIndex(0);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
body: PageTransitionSwitcher(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return FadeThroughTransition(
|
||||
animation: animation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
fillColor: Theme.of(context).colorScheme.surface,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: model.getViewForIndex(model.currentIndex),
|
||||
),
|
||||
bottomNavigationBar: NavigationBar(
|
||||
onDestinationSelected: model.setIndex,
|
||||
selectedIndex: model.currentIndex,
|
||||
destinations: <Widget>[
|
||||
NavigationDestination(
|
||||
icon: model.isIndexSelected(0)
|
||||
? const Icon(Icons.dashboard)
|
||||
: const Icon(Icons.dashboard_outlined),
|
||||
label: FlutterI18n.translate(
|
||||
context,
|
||||
'navigationView.dashboardTab',
|
||||
),
|
||||
tooltip: '',
|
||||
),
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: model.isIndexSelected(1)
|
||||
? const Icon(Icons.build)
|
||||
: const Icon(Icons.build_outlined),
|
||||
label: FlutterI18n.translate(
|
||||
context,
|
||||
'navigationView.patcherTab',
|
||||
NavigationDestination(
|
||||
icon: model.isIndexSelected(1)
|
||||
? const Icon(Icons.build)
|
||||
: const Icon(Icons.build_outlined),
|
||||
label: FlutterI18n.translate(
|
||||
context,
|
||||
'navigationView.patcherTab',
|
||||
),
|
||||
tooltip: '',
|
||||
),
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: model.isIndexSelected(2)
|
||||
? const Icon(Icons.settings)
|
||||
: const Icon(Icons.settings_outlined),
|
||||
label: FlutterI18n.translate(
|
||||
context,
|
||||
'navigationView.settingsTab',
|
||||
NavigationDestination(
|
||||
icon: model.isIndexSelected(2)
|
||||
? const Icon(Icons.settings)
|
||||
: const Icon(Icons.settings_outlined),
|
||||
label: FlutterI18n.translate(
|
||||
context,
|
||||
'navigationView.settingsTab',
|
||||
),
|
||||
tooltip: '',
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -64,7 +64,7 @@ class PatcherViewModel extends BaseViewModel {
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('warning'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText('patcherView.armv7WarningDialogText'),
|
||||
content: I18nText('patcherView.splitApkWarningDialogText'),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
label: I18nText('noButton'),
|
||||
@ -133,7 +133,7 @@ class PatcherViewModel extends BaseViewModel {
|
||||
if (suggestedVersion.isEmpty) {
|
||||
suggestedVersion = FlutterI18n.translate(
|
||||
context,
|
||||
'appSelectorCard.anyVersion',
|
||||
'appSelectorCard.allVersions',
|
||||
);
|
||||
} else {
|
||||
suggestedVersion = 'v$suggestedVersion';
|
||||
|
@ -93,7 +93,9 @@ class SManageApiUrl extends BaseViewModel {
|
||||
onPressed: () {
|
||||
_managerAPI.setApiUrl('');
|
||||
_toast.showBottom('settingsView.restartAppForChanges');
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context)
|
||||
..pop()
|
||||
..pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
|
@ -17,8 +17,7 @@ import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
|
||||
class SettingsViewModel extends BaseViewModel {
|
||||
final NavigationService _navigationService =
|
||||
locator<NavigationService>();
|
||||
final NavigationService _navigationService = locator<NavigationService>();
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
final Toast _toast = locator<Toast>();
|
||||
|
||||
@ -63,11 +62,8 @@ class SettingsViewModel extends BaseViewModel {
|
||||
try {
|
||||
final File outFile = File(_managerAPI.storedPatchesFile);
|
||||
if (outFile.existsSync()) {
|
||||
final String dateTime = DateTime.now()
|
||||
.toString()
|
||||
.replaceAll(' ', '_')
|
||||
.split('.')
|
||||
.first;
|
||||
final String dateTime =
|
||||
DateTime.now().toString().replaceAll(' ', '_').split('.').first;
|
||||
await CRFileSaver.saveFileWithDialog(
|
||||
SaveFileDialogParams(
|
||||
sourceFilePath: outFile.path,
|
||||
@ -87,8 +83,7 @@ class SettingsViewModel extends BaseViewModel {
|
||||
|
||||
Future<void> importPatches() async {
|
||||
try {
|
||||
final FilePickerResult? result =
|
||||
await FilePicker.platform.pickFiles(
|
||||
final FilePickerResult? result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['json'],
|
||||
);
|
||||
@ -109,7 +104,7 @@ class SettingsViewModel extends BaseViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> exportKeystore() async {
|
||||
Future<void> exportKeystore() async {
|
||||
try {
|
||||
final File outFile = File(_managerAPI.keystoreFile);
|
||||
if (outFile.existsSync()) {
|
||||
@ -138,7 +133,7 @@ class SettingsViewModel extends BaseViewModel {
|
||||
if (result != null && result.files.single.path != null) {
|
||||
final File inFile = File(result.files.single.path!);
|
||||
inFile.copySync(_managerAPI.keystoreFile);
|
||||
|
||||
|
||||
_toast.showBottom('settingsView.importedKeystore');
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
import 'package:skeletons/skeletons.dart';
|
||||
|
||||
class AppSkeletonLoader extends StatelessWidget {
|
||||
@ -7,59 +8,67 @@ class AppSkeletonLoader extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
return Skeleton(
|
||||
isLoading: true,
|
||||
skeleton: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: 7,
|
||||
itemBuilder: (context, index) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8.0),
|
||||
child: SkeletonItem(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
SkeletonAvatar(
|
||||
style: SkeletonAvatarStyle(
|
||||
width: screenWidth * 0.15,
|
||||
height: screenWidth * 0.15,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: 7,
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder: (context, index) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12.0),
|
||||
child: CustomCard(
|
||||
child: Row(
|
||||
children: [
|
||||
SkeletonAvatar(
|
||||
style: SkeletonAvatarStyle(
|
||||
width: screenWidth * 0.10,
|
||||
height: screenWidth * 0.10,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
width: screenWidth * 0.4,
|
||||
child: SkeletonLine(
|
||||
style: SkeletonLineStyle(
|
||||
height: 20,
|
||||
width: screenWidth * 0.4,
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
color: Colors.white,
|
||||
height: 34,
|
||||
width: screenWidth * 0.4,
|
||||
child: SkeletonParagraph(
|
||||
style: const SkeletonParagraphStyle(
|
||||
lines: 1,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: screenWidth * 0.6,
|
||||
child: SkeletonLine(
|
||||
style: SkeletonLineStyle(
|
||||
height: 15,
|
||||
width: screenWidth * 0.6,
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
color: Colors.white,
|
||||
height: 34,
|
||||
width: screenWidth * 0.6,
|
||||
child: SkeletonParagraph(
|
||||
style: const SkeletonParagraphStyle(
|
||||
lines: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
SizedBox(
|
||||
width: screenWidth * 0.5,
|
||||
child: SkeletonLine(
|
||||
style: SkeletonLineStyle(
|
||||
height: 15,
|
||||
width: screenWidth * 0.5,
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const Center(
|
||||
child: Text('Content'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
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';
|
||||
@ -52,46 +51,49 @@ class _InstalledAppItemState extends State<InstalledAppItem> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
widget.name,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.visible,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
I18nText(
|
||||
widget.installedVersion.isEmpty
|
||||
? 'All versions'
|
||||
: 'v${widget.installedVersion}',
|
||||
),
|
||||
],
|
||||
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}'
|
||||
},
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
I18nText(
|
||||
FlutterI18n.translate(
|
||||
context,
|
||||
'suggested',
|
||||
translationParams: {
|
||||
'version': widget.suggestedVersion.isEmpty
|
||||
? 'All versions'
|
||||
: 'v${widget.suggestedVersion}'
|
||||
},
|
||||
),
|
||||
'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',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.patchesCount == 1
|
||||
? '• ${widget.patchesCount} patch'
|
||||
: '• ${widget.patchesCount} patches',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
|
||||
class NotInstalledAppItem extends StatefulWidget {
|
||||
@ -54,20 +55,39 @@ class _NotInstalledAppItem extends State<NotInstalledAppItem> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text('App not installed.'),
|
||||
const SizedBox(height: 4),
|
||||
I18nText(
|
||||
'appSelectorCard.notInstalled',
|
||||
child: Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Suggested: ${widget.suggestedVersion.isEmpty ? 'All versions' : 'v${widget.suggestedVersion}'}',
|
||||
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',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.patchesCount == 1
|
||||
? '• ${widget.patchesCount} patch'
|
||||
: '• ${widget.patchesCount} patches',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -1,61 +1,96 @@
|
||||
import 'package:device_apps/device_apps.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||
import 'package:revanced_manager/app/app.locator.dart';
|
||||
import 'package:revanced_manager/models/patched_application.dart';
|
||||
import 'package:revanced_manager/services/manager_api.dart';
|
||||
import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/application_item.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
|
||||
//ignore: must_be_immutable
|
||||
class InstalledAppsCard extends StatelessWidget {
|
||||
InstalledAppsCard({Key? key}) : super(key: key);
|
||||
|
||||
final List<PatchedApplication> apps =
|
||||
locator<HomeViewModel>().patchedInstalledApps;
|
||||
List<PatchedApplication> apps = locator<HomeViewModel>().patchedInstalledApps;
|
||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||
List<PatchedApplication> patchedApps = [];
|
||||
|
||||
Future _getApps() async {
|
||||
if (apps.isNotEmpty) {
|
||||
patchedApps = [...apps];
|
||||
for (final element in apps) {
|
||||
await DeviceApps.getApp(element.packageName).then((value) {
|
||||
if (element.version != value?.versionName) {
|
||||
patchedApps.remove(element);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (apps.length != patchedApps.length) {
|
||||
await _managerAPI.setPatchedApps(patchedApps);
|
||||
apps.clear();
|
||||
apps = [...patchedApps];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return apps.isEmpty
|
||||
? CustomCard(
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
size: 40,
|
||||
Icons.file_download_off,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
I18nText(
|
||||
'homeView.noInstallations',
|
||||
child: Text(
|
||||
'',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
return FutureBuilder(
|
||||
future: _getApps(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return apps.isEmpty
|
||||
? CustomCard(
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
size: 40,
|
||||
Icons.file_download_off,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
I18nText(
|
||||
'homeView.noInstallations',
|
||||
child: Text(
|
||||
'',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: apps
|
||||
.map(
|
||||
(app) => ApplicationItem(
|
||||
icon: app.icon,
|
||||
name: app.name,
|
||||
patchDate: app.patchDate,
|
||||
changelog: app.changelog,
|
||||
isUpdatableApp: false,
|
||||
onPressed: () =>
|
||||
locator<HomeViewModel>().navigateToAppInfo(app),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
: ListView(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: apps
|
||||
.map(
|
||||
(app) => ApplicationItem(
|
||||
icon: app.icon,
|
||||
name: app.name,
|
||||
patchDate: app.patchDate,
|
||||
changelog: app.changelog,
|
||||
isUpdatableApp: false,
|
||||
onPressed: () =>
|
||||
locator<HomeViewModel>().navigateToAppInfo(app),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
} else {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -45,8 +45,7 @@ class UpdateConfirmationDialog extends StatelessWidget {
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
I18nText(
|
||||
'homeView.updateDialogTitle',
|
||||
@ -63,14 +62,12 @@ class UpdateConfirmationDialog extends StatelessWidget {
|
||||
children: [
|
||||
Icon(
|
||||
Icons.new_releases_outlined,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondary,
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
Text(
|
||||
snapshot.data!['tag_name'] ??
|
||||
'Unknown',
|
||||
snapshot.data!['tag_name'] ?? 'Unknown',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
@ -96,8 +93,7 @@ class UpdateConfirmationDialog extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 24.0, bottom: 12.0),
|
||||
padding: const EdgeInsets.only(left: 24.0, bottom: 12.0),
|
||||
child: I18nText(
|
||||
'homeView.updateChangelogTitle',
|
||||
child: Text(
|
||||
@ -113,12 +109,9 @@ class UpdateConfirmationDialog extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin:
|
||||
const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondaryContainer,
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
child: Markdown(
|
||||
|
@ -3,6 +3,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/settings_experimental_patches.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
||||
|
||||
@ -41,137 +42,148 @@ class PatchItem extends StatefulWidget {
|
||||
class _PatchItemState extends State<PatchItem> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
widget.isSelected = widget.isSelected &&
|
||||
(!widget.isUnsupported ||
|
||||
widget._managerAPI.areExperimentalPatchesEnabled());
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: CustomCard(
|
||||
backgroundColor: widget.isUnsupported &&
|
||||
child: Opacity(
|
||||
opacity: widget.isUnsupported &&
|
||||
widget._managerAPI.areExperimentalPatchesEnabled() == false
|
||||
? Theme.of(context).colorScheme.brightness == Brightness.light
|
||||
? Colors.grey[400]
|
||||
: Colors.grey[700]
|
||||
: null,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (widget.isUnsupported &&
|
||||
!widget._managerAPI.areExperimentalPatchesEnabled()) {
|
||||
widget.isSelected = false;
|
||||
widget.toast.showBottom('patchItem.unsupportedPatchVersion');
|
||||
} else {
|
||||
widget.isSelected = !widget.isSelected;
|
||||
}
|
||||
});
|
||||
widget.onChanged(widget.isSelected);
|
||||
},
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.simpleName,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.visible,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
? 0.5
|
||||
: 1,
|
||||
child: CustomCard(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (widget.isUnsupported &&
|
||||
!widget._managerAPI.areExperimentalPatchesEnabled()) {
|
||||
widget.isSelected = false;
|
||||
widget.toast.showBottom('patchItem.unsupportedPatchVersion');
|
||||
} else {
|
||||
widget.isSelected = !widget.isSelected;
|
||||
}
|
||||
});
|
||||
widget.onChanged(widget.isSelected);
|
||||
},
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.simpleName,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.visible,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 1),
|
||||
Text(
|
||||
widget.version,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.description,
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.visible,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Transform.scale(
|
||||
scale: 1.2,
|
||||
child: Checkbox(
|
||||
value: widget.isSelected,
|
||||
activeColor: Theme.of(context).colorScheme.primary,
|
||||
checkColor:
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
side: BorderSide(
|
||||
width: 2.0,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onChanged: (newValue) {
|
||||
setState(() {
|
||||
if (widget.isUnsupported &&
|
||||
!widget._managerAPI
|
||||
.areExperimentalPatchesEnabled()) {
|
||||
widget.isSelected = false;
|
||||
widget.toast.showBottom(
|
||||
'patchItem.unsupportedPatchVersion',
|
||||
);
|
||||
} else {
|
||||
widget.isSelected = newValue!;
|
||||
}
|
||||
if (widget.isUnsupported &&
|
||||
widget.isSelected &&
|
||||
!selectedUnsupportedPatches
|
||||
.contains(widget.name)) {
|
||||
selectedUnsupportedPatches.add(widget.name);
|
||||
}
|
||||
});
|
||||
widget.onChanged(widget.isSelected);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
if (widget.isUnsupported &&
|
||||
widget._managerAPI.areExperimentalPatchesEnabled())
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: TextButton.icon(
|
||||
label: I18nText('warning'),
|
||||
icon: const Icon(Icons.warning, size: 20.0),
|
||||
onPressed: () => _showUnsupportedWarningDialog(),
|
||||
style: ButtonStyle(
|
||||
shape: MaterialStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 1),
|
||||
Text(
|
||||
widget.version,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.description,
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.visible,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Transform.scale(
|
||||
scale: 1.2,
|
||||
child: Checkbox(
|
||||
value: widget.isSelected,
|
||||
activeColor: Theme.of(context).colorScheme.primary,
|
||||
checkColor:
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
side: BorderSide(
|
||||
width: 2.0,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onChanged: (newValue) {
|
||||
setState(() {
|
||||
if (widget.isUnsupported &&
|
||||
!widget._managerAPI
|
||||
.areExperimentalPatchesEnabled()) {
|
||||
widget.isSelected = false;
|
||||
widget.toast
|
||||
.showBottom('patchItem.unsupportedPatchVersion');
|
||||
} else {
|
||||
widget.isSelected = newValue!;
|
||||
}
|
||||
});
|
||||
widget.onChanged(widget.isSelected);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
if (widget.isUnsupported)
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: TextButton.icon(
|
||||
label: I18nText('warning'),
|
||||
icon: const Icon(Icons.warning, size: 20.0),
|
||||
onPressed: () => _showUnsupportedWarningDialog(),
|
||||
style: ButtonStyle(
|
||||
shape: MaterialStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
backgroundColor: MaterialStateProperty.all(
|
||||
Colors.transparent,
|
||||
),
|
||||
foregroundColor: MaterialStateProperty.all(
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
backgroundColor: MaterialStateProperty.all(
|
||||
Colors.transparent,
|
||||
),
|
||||
foregroundColor: MaterialStateProperty.all(
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
Container(),
|
||||
widget.child ?? const SizedBox(),
|
||||
],
|
||||
],
|
||||
)
|
||||
else
|
||||
Container(),
|
||||
widget.child ?? const SizedBox(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_i18n/widgets/I18nText.dart';
|
||||
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
|
||||
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
|
||||
|
||||
class SExperimentalPatches extends StatefulWidget {
|
||||
@ -10,6 +11,7 @@ class SExperimentalPatches extends StatefulWidget {
|
||||
}
|
||||
|
||||
final _settingsViewModel = SettingsViewModel();
|
||||
final List<String> selectedUnsupportedPatches = [];
|
||||
|
||||
class _SExperimentalPatchesState extends State<SExperimentalPatches> {
|
||||
@override
|
||||
@ -32,6 +34,14 @@ class _SExperimentalPatchesState extends State<SExperimentalPatches> {
|
||||
setState(() {
|
||||
_settingsViewModel.useExperimentalPatches(value);
|
||||
});
|
||||
if (!value) {
|
||||
for (final patch in selectedUnsupportedPatches) {
|
||||
PatchesSelectorViewModel()
|
||||
.selectedPatches
|
||||
.removeWhere((element) => patch == element.name);
|
||||
}
|
||||
selectedUnsupportedPatches.clear();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ 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();
|
||||
|
||||
@ -91,9 +92,36 @@ class SExportSection extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
subtitle: I18nText('settingsView.resetStoredPatchesHint'),
|
||||
onTap: () => _settingsViewModel.resetSelectedPatches(),
|
||||
onTap: () => _showResetStoredPatchesDialog(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showResetStoredPatchesDialog(context) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: I18nText('warning'),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
content: I18nText(
|
||||
'settingsView.resetStoredPatchesDialogText',
|
||||
),
|
||||
actions: <Widget>[
|
||||
CustomMaterialButton(
|
||||
isFilled: false,
|
||||
label: I18nText('noButton'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
CustomMaterialButton(
|
||||
label: I18nText('yesButton'),
|
||||
onPressed: () => {
|
||||
Navigator.of(context).pop(),
|
||||
_settingsViewModel.resetSelectedPatches()
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
10
pubspec.yaml
10
pubspec.yaml
@ -1,17 +1,16 @@
|
||||
name: revanced_manager
|
||||
description: The official ReVanced Manager.
|
||||
description: Patch your favourite apps, right on your device.
|
||||
homepage: https://github.com/revanced/revanced-manager
|
||||
|
||||
publish_to: 'none'
|
||||
|
||||
version: 1.3.5+100300005
|
||||
version: 1.4.1+100400001
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
animations: ^2.0.7
|
||||
app_installer: ^1.1.0
|
||||
collection: ^1.17.0
|
||||
cross_connectivity: ^3.0.5
|
||||
cr_file_saver:
|
||||
@ -59,8 +58,8 @@ dependencies:
|
||||
pull_to_refresh: ^2.0.0
|
||||
root:
|
||||
git:
|
||||
url: https://github.com/gokul1630/root
|
||||
ref: main
|
||||
url: https://github.com/EvadeMaster/root
|
||||
ref: 82803aa40f63cddff81c3e4d27ce8ce3e7c83f60
|
||||
share_extend: ^2.0.0
|
||||
shared_preferences: ^2.1.0
|
||||
skeletons: ^0.0.3
|
||||
@ -75,6 +74,7 @@ dependencies:
|
||||
flutter_dotenv: ^5.0.2
|
||||
flutter_markdown: ^0.6.14
|
||||
dio_cache_interceptor: ^3.4.0
|
||||
install_plugin: ^2.1.0
|
||||
|
||||
dev_dependencies:
|
||||
json_serializable: ^6.6.1
|
||||
|
Reference in New Issue
Block a user