Compare commits

...

89 Commits

Author SHA1 Message Date
6b66c7bbd0 build: Bump version to v0.0.23 2022-09-24 12:16:02 +01:00
f398b6863a Merge branch 'flutter' of github.com:revanced/revanced-manager into flutter 2022-09-24 12:12:24 +01:00
4d82ff3011 fix: Prevent content from being overlapped by system navigation bar 2022-09-24 12:06:36 +01:00
2a0ea78d7f feat: Adds support for patching on x86 and x64 devices
Introduces new aapt2 binaries from revanced/aapt2
2022-09-24 12:05:36 +01:00
4722880647 feat: better patch version background. 2022-09-24 13:47:09 +05:30
32fabcfa3f fix: nav overlapping install buttons. 2022-09-24 12:53:46 +05:30
e0c46e4268 fix: Show "Share log" menu option even if patching has errors 2022-09-23 17:22:39 +01:00
d84230fa22 feat: Merge integrations if a patch or any of its dependencies need them 2022-09-23 17:20:19 +01:00
9561153bfb feat: Hide "Install as Root" button if user does not have root access at all 2022-09-23 15:47:30 +01:00
c8c35ca801 build: Bump version to v0.0.22 2022-09-23 15:34:18 +01:00
cf99069804 fix: Handle minor lint errors 2022-09-23 15:33:27 +01:00
6abb761724 refactor: Move some logic to ManagerAPI and PatcherAPI 2022-09-23 15:31:24 +01:00
4609ed9eba fix: Make sure we are getting app from from storage when selection is from storage 2022-09-23 15:21:58 +01:00
c0f743df89 feat: HTTP2 support for api requests. (#290) 2022-09-23 19:06:06 +05:30
f00f910973 Merge branch 'flutter' of https://github.com/revanced/revanced-manager into flutter 2022-09-23 14:58:19 +05:30
2bc486dcec build: Bump version to v0.0.21. 2022-09-23 14:58:07 +05:30
148981da18 refactor: removed dropdown for branch. 2022-09-23 14:57:05 +05:30
d8df377427 feat: warning dialog when selecting all patches. 2022-09-23 14:53:21 +05:30
efe1306aac fix: reverse versions list to have recents first. 2022-09-23 13:35:47 +05:30
ead308740f refactor: move custom button to shared dir. 2022-09-23 13:00:30 +05:30
fa0f250e27 build: bump patcher dependency version to v5.0.1 2022-09-23 07:45:44 +02:00
185d883b65 feat: acknowledgements section for template feature-issue 2022-09-23 07:33:30 +02:00
9d8b2e1a35 feat: acknowledgements section for template feature-issue 2022-09-23 07:32:44 +02:00
a6a7ff5040 build: Bump version to v0.0.19 2022-09-23 00:39:44 +01:00
1028ec8e2a fix: Prevent Manager from crashing when a patcher exception occurs and print its stack trace on installer's log 2022-09-23 00:38:42 +01:00
f9242c1958 build: Bump version to v0.0.18 2022-09-22 14:04:59 +01:00
f9865166b0 fix: TikTok integrations and settings patches should be merged if needed 2022-09-22 14:02:07 +01:00
f0f934f6a1 fix: Prevent printing of denied root permission exceptions for unrooted users 2022-09-22 14:01:05 +01:00
3d25655851 fix: Uninstall button should always be shown 2022-09-22 09:46:28 +01:00
733190e76c build: bump version to v0.0.17+17 2022-09-21 05:19:26 +02:00
ab312dec43 build: bump patcher dependency version to v5.0.0 2022-09-21 04:16:19 +02:00
70159b8cd8 Update Crowdin configuration file 2022-09-20 19:17:29 +02:00
97060913a2 Update Crowdin configuration file 2022-09-20 19:07:41 +02:00
4a0c5f9935 Update Crowdin configuration file 2022-09-20 12:06:23 +02:00
f643a31455 Merge branch 'flutter' of github.com:revanced/revanced-manager into flutter 2022-09-20 10:05:06 +01:00
03b87a50ea fix: Use a proper app name when MicroG is applied 2022-09-20 10:04:51 +01:00
4631982d42 fix: Use original packageName to get compatible patches 2022-09-20 10:04:02 +01:00
4c4b694db9 chore: update English language capitalizations and format (#228)
* chore: update English language capitalizations and format

* chore: revert capitalization changes as requested
2022-09-20 12:27:20 +05:30
fd099fb632 build: Bump version to v0.0.16 2022-09-20 02:38:55 +01:00
e374e03355 fix: Readd padding between Application Items 2022-09-20 02:38:28 +01:00
d0293f5412 build: Bump version to v0.0.15 2022-09-20 01:54:41 +01:00
6f72c2ebec fix: Fix unfixed fixes which needed to be fixed (better now??) 2022-09-20 01:07:36 +01:00
07407933d9 fix: Don't select patches by default based on their excluded flag 2022-09-20 00:59:14 +01:00
2f4726ea68 fix: Add toast service to simplify creation of toasts 2022-09-20 00:49:31 +01:00
207e94de5a fix: Fix Update Manager's button 2022-09-20 00:03:37 +01:00
7231d44e02 fix: Push Custom Sources tile to Advanced section and reorder a bit 2022-09-19 23:31:21 +01:00
bd5b38d88e fix: Add rounded borders on Unpatch button inkwell as well 2022-09-19 23:25:39 +01:00
530dd78752 fix: make inkwells visible (#205) 2022-09-19 17:55:44 +01:00
efcf455b24 Merge branch 'flutter' of github.com:revanced/revanced-manager into flutter 2022-09-19 17:40:11 +01:00
bed2cf76d5 feat: Prevent exiting installer on new back button as well and show why exiting is not possible during patching 2022-09-19 17:40:06 +01:00
5c8079ae95 fix: warn about no armv7 support. 2022-09-19 21:34:52 +05:30
2a2bb8212f fix: Add back button on app bars of secondary views 2022-09-19 16:24:31 +01:00
3e6e94c098 fix: disable creating blank issues. 2022-09-19 18:23:33 +05:30
ce100d97fd refactor: add dropdown for feature template. 2022-09-19 18:22:51 +05:30
743a661c10 refactor: use dropdown for branch. 2022-09-19 18:10:59 +05:30
2b7287c04c feat: CustomScrollView in app and patches selector views (#208) 2022-09-19 12:57:40 +01:00
9bdccb3c9d refactor: more strict bug issue template. 2022-09-19 17:17:48 +05:30
b8171253c7 fix: Readd divider mistakenly removed in #194 2022-09-19 09:56:51 +01:00
d6dde3e23b fix: (settings) remove padding from inkwells (#194) 2022-09-19 09:39:25 +01:00
fc5414c788 fix: delete tests (#196) 2022-09-19 09:13:40 +01:00
ed9038ffe8 build: Bump version to v0.0.14 2022-09-19 01:47:51 +01:00
269a71d336 fix: Improve foreground service init and disable 2022-09-19 01:46:12 +01:00
9405334a7d fix: Fix multiple APK shares 2022-09-19 01:45:24 +01:00
2f14746124 feat: Add install error dialogs to prevent faulty installations 2022-09-19 01:44:27 +01:00
6c2ceed91f fix: Improve API URL handling and force init after setting a new config 2022-09-19 00:28:26 +01:00
c333fa3c33 fix: Prefer Yes/No instead of OK/Cancel on a few dialogs 2022-09-18 22:53:04 +01:00
79d0396630 fix: No need for an uninstall confirmation dialog as OS already handles this 2022-09-18 22:47:24 +01:00
110c3326bd fix: navigation bar color (#197) 2022-09-18 22:20:08 +01:00
9c5b0b9c14 fix: Readd permission_handler with a proper fix 2022-09-18 22:13:29 +01:00
c5ad337daa feat: Custom api endpoints. (#216)
Co-authored-by: Alberto Ponces <ponces26@gmail.com>
2022-09-18 23:12:30 +05:30
a9f64e449b refactor: basic prerequisites in readme. 2022-09-18 23:01:12 +05:30
48f0bc625d feat: animate switching updates on HomeView (#209) 2022-09-18 16:49:18 +01:00
4c6b93320f build: Bump bump version to v0.0.13 2022-09-18 12:22:39 +05:30
2f3bb6cfe4 refactor: better wording. 2022-09-18 12:18:47 +05:30
0e1fa1a5d6 build: bump patcher dependency 2022-09-18 08:38:03 +02:00
38fb3444e4 build: Bump version to v0.0.12 2022-09-18 05:07:12 +01:00
f10b5aebac fix: Decrease app name space a bit to prevent overflowing 2022-09-18 05:06:51 +01:00
5a3884e159 fix: Fix uninstall apps 2022-09-18 05:06:23 +01:00
41ac2b0df8 build: Bump version to v0.0.11 2022-09-18 04:47:23 +01:00
52e7d76c9d fix: Fix spacing of App Info buttons 2022-09-18 04:46:21 +01:00
3aa80cacc0 fix: Fix duplicate entries on non-root installations 2022-09-18 04:44:44 +01:00
dd52c379b4 fix: Show updatable app item only on updatable apps listing 2022-09-18 04:12:33 +01:00
5d073bddf2 fix: Minor i18n typo 2022-09-18 03:41:16 +01:00
7d09169c06 build: Bump version to v0.0.10 2022-09-18 03:35:32 +01:00
ce6f11f3f2 feat: Show recommendation version when possible 2022-09-18 03:14:48 +01:00
d0fb6ac3c0 fix: Ignore empty results on root installed apps listing 2022-09-18 02:41:11 +01:00
ae801a2918 fix: Fix apps reassess on root 2022-09-18 01:54:25 +01:00
257fd46e27 fix: Improve quality of launcher and splash icons 2022-09-18 01:23:21 +01:00
bb96c3ed63 fix: Make sure aapt lib gets extracted to native lib dir during install 2022-09-17 22:54:38 +01:00
70 changed files with 2064 additions and 1451 deletions

View File

@ -20,12 +20,6 @@ body:
- Other
validations:
required: true
- type: textarea
attributes:
label: Branch
description: Which github branch did you use (Flutter or Compose)?
validations:
required: true
- type: textarea
attributes:
label: Bug description
@ -41,11 +35,56 @@ body:
required: true
- type: textarea
attributes:
label: Relevant log output
label: Android version
description: Android version used.
validations:
required: true
- type: textarea
attributes:
label: Manager version
description: Manager version used.
validations:
required: true
- type: textarea
attributes:
label: Target package name
description: App you tried to patch.
validations:
required: true
- type: textarea
attributes:
label: Target package version.
description: Version of the app you tried to patch.
validations:
required: true
- type: dropdown
attributes:
label: Installation type
options:
- Non-root
- Root
validations:
required: true
- type: textarea
attributes:
label: Patches selected.
description: Patches you selected for the app.
validations:
required: true
- type: textarea
attributes:
label: Device logs (exported using Manager settings).
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so 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.
render: shell
validations:
required: false
- type: textarea
attributes:
label: Screenshots or videos
@ -65,3 +104,17 @@ body:
description: Add additional context here.
validations:
required: false
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
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.
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.
required: true
- label: The issue is related solely to the ReVanced Manager
required: true

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1 @@
blank_issues_enabled: false

View File

@ -12,12 +12,6 @@ body:
- Other
validations:
required: true
- type: textarea
attributes:
label: Branch
description: Which github branch did you use (Flutter or Compose)?
validations:
required: true
- type: textarea
attributes:
label: Issue
@ -41,4 +35,18 @@ body:
label: Additional context
description: Add additional context here.
validations:
required: false
required: false
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
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.
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.
required: true
- label: The issue is related solely to the ReVanced Manager
required: true

View File

@ -12,4 +12,9 @@ For suggestions and bug reports, open an issue [here](https://github.com/revance
If you wish to discuss the Manager, a thread has been made under the [#chat](https://discord.com/channels/952946952348270622/1002922226443632761) channel in the Discord server, please note that this thread may be temporary and may be removed in the future.
## ⚠️ Disclaimer
*Please note that even though we're releasing the Manager, it is an ALPHA version. Meaning there's a big chance that the Manager might not work at all for you.*
*Please note that even though we're releasing the Manager, it is an ALPHA version. Meaning there's a big chance that the Manager might not work at all for you.*
## Prerequisites
1. Android 8 or higher.
2. Does not work on armv7
3. For YouTube and YouTube Music - Vanced MicroG(Only for non-root).

View File

@ -71,7 +71,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// ReVanced
implementation "app.revanced:revanced-patcher:4.4.1"
implementation "app.revanced:revanced-patcher:5.0.1"
// Signing & aligning
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")

View File

@ -1,21 +1,23 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
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.QUERY_ALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<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" />
<application
android:label="ReVanced Manager"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:largeHeap="true">
android:largeHeap="true"
android:extractNativeLibs="true">
<activity
android:name=".MainActivity"
android:exported="true"

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -74,7 +74,6 @@ class MainActivity : FlutterActivity() {
result.notImplemented()
}
}
else -> result.notImplemented()
}
}
@ -101,161 +100,174 @@ class MainActivity : FlutterActivity() {
val integrations = File(integrationsPath)
val keyStoreFile = File(keyStoreFilePath)
val patches = DexPatchBundle(
patchBundleFilePath,
DexClassLoader(
patchBundleFilePath,
cacheDirPath,
null,
javaClass.classLoader
)
).loadPatches().filter { patch -> selectedPatches.any { it == patch.patchName } }
Thread {
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 0.1,
"header" to "",
"log" to "Copying original apk"
)
)
}
originalFile.copyTo(inputFile, true)
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 0.2,
"header" to "Unpacking apk...",
"log" to "Unpacking input apk"
)
)
}
val patcher =
Patcher(
PatcherOptions(
inputFile,
try {
val patches = DexPatchBundle(
patchBundleFilePath,
DexClassLoader(
patchBundleFilePath,
cacheDirPath,
resourcePatching,
Aapt.binary(applicationContext).absolutePath,
cacheDirPath,
logger = ManagerLogger()
null,
javaClass.classLoader
)
)
handler.post {
installerChannel.invokeMethod(
"update",
mapOf("progress" to 0.3, "header" to "", "log" to "")
)
}
if (mergeIntegrations) {
).loadPatches().filter { patch -> selectedPatches.any { it == patch.patchName } }
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 0.4,
"header" to "Merging integrations...",
"log" to "Merging integrations"
"progress" to 0.1,
"header" to "",
"log" to "Copying original apk"
)
)
}
patcher.addFiles(listOf(integrations)) {}
}
originalFile.copyTo(inputFile, true)
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 0.5,
"header" to "Applying patches...",
"log" to ""
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 0.2,
"header" to "Unpacking apk...",
"log" to "Unpacking input apk"
)
)
)
}
patcher.addPatches(patches)
patcher.applyPatches().forEach { (patch, res) ->
if (res.isSuccess) {
val msg = "[success] $patch"
}
val patcher =
Patcher(
PatcherOptions(
inputFile,
cacheDirPath,
resourcePatching,
Aapt.binary(applicationContext).absolutePath,
cacheDirPath,
logger = ManagerLogger()
)
)
handler.post {
installerChannel.invokeMethod(
"update",
mapOf("progress" to 0.3, "header" to "", "log" to "")
)
}
if (mergeIntegrations) {
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 0.5,
"header" to "",
"log" to msg
"progress" to 0.4,
"header" to "Merging integrations...",
"log" to "Merging integrations"
)
)
}
return@forEach
patcher.addFiles(listOf(integrations)) {}
}
val msg = "[error] $patch:" + res.exceptionOrNull()!!
handler.post {
installerChannel.invokeMethod(
"update",
mapOf("progress" to 0.5, "header" to "", "log" to msg)
mapOf(
"progress" to 0.5,
"header" to "Applying patches...",
"log" to ""
)
)
}
}
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 0.7,
"header" to "Repacking apk...",
"log" to "Repacking patched apk"
)
)
}
val res = patcher.save()
ZipFile(patchedFile).use { file ->
res.dexFiles.forEach {
file.addEntryCompressData(
ZipEntry.createWithName(it.name),
it.stream.readBytes()
patcher.addPatches(patches)
patcher.applyPatches().forEach { (patch, res) ->
if (res.isSuccess) {
val msg = "[success] $patch"
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 0.5,
"header" to "",
"log" to msg
)
)
}
return@forEach
}
val msg = "[error] $patch:" + res.exceptionOrNull()!!
handler.post {
installerChannel.invokeMethod(
"update",
mapOf("progress" to 0.5, "header" to "", "log" to msg)
)
}
}
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 0.7,
"header" to "Repacking apk...",
"log" to "Repacking patched apk"
)
)
}
res.resourceFile?.let {
val res = patcher.save()
ZipFile(patchedFile).use { file ->
res.dexFiles.forEach {
file.addEntryCompressData(
ZipEntry.createWithName(it.name),
it.stream.readBytes()
)
}
res.resourceFile?.let {
file.copyEntriesFromFileAligned(
ZipFile(it),
ZipAligner::getEntryAlignment
)
}
file.copyEntriesFromFileAligned(
ZipFile(it),
ZipFile(inputFile),
ZipAligner::getEntryAlignment
)
}
file.copyEntriesFromFileAligned(
ZipFile(inputFile),
ZipAligner::getEntryAlignment
)
}
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 0.9,
"header" to "Signing apk...",
"log" to ""
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 0.9,
"header" to "Signing apk...",
"log" to ""
)
)
)
}
Signer("ReVanced", "s3cur3p@ssw0rd").signApk(patchedFile, outFile, keyStoreFile)
}
Signer("ReVanced", "s3cur3p@ssw0rd").signApk(patchedFile, outFile, keyStoreFile)
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 1.0,
"header" to "Finished!",
"log" to "Finished!"
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 1.0,
"header" to "Finished!",
"log" to "Finished!"
)
)
)
}
} catch (ex: Throwable) {
val stack = ex.stackTraceToString()
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to -100.0,
"header" to "Aborting...",
"log" to "An error occurred! Aborting\nError:\n$stack"
)
)
}
}
handler.post { result.success(null) }
}
.start()
}.start()
}
inner class ManagerLogger : Logger {

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_launcher_round</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_launcher_round</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -3,8 +3,8 @@
"cancelButton": "Cancel",
"enabledLabel": "Enabled",
"disabledLabel": "Disabled",
"yesLabel": "Yes",
"noLabel": "No",
"yesButton": "Yes",
"noButton": "No",
"navigationView": {
"dashboardTab": "Dashboard",
"patcherTab": "Patcher",
@ -14,18 +14,18 @@
"widgetTitle": "Dashboard",
"updatesSubtitle": "Updates",
"patchedSubtitle": "Patched Applications",
"updatesAvailable": "Updates Available",
"updatesAvailable": "Updates available",
"noUpdates": "No updates available",
"noInstallations": "No patched applications installed",
"installed": "Installed",
"updateDialogTitle": "Update",
"updateDialogText": "Are you sure you want to download and update Manager?",
"notificationTitle": "ReVanced Manager was updated!",
"updateDialogTitle": "Update Manager",
"updateDialogText": "Are you sure you want to download and update ReVanced Manager?",
"notificationTitle": "ReVanced Manager was updated",
"notificationText": "Tap to open the app",
"downloadingMessage": "Downloading update!",
"installingMessage": "Installing update... Hang on!",
"errorDownloadMessage": "Unable to download update!",
"errorInstallMessage": "Unable to download update!",
"downloadingMessage": "Downloading update...",
"installingMessage": "Installing update...",
"errorDownloadMessage": "Unable to download update",
"errorInstallMessage": "Unable to install update",
"noConnection": "No internet connection"
},
"applicationItem": {
@ -34,7 +34,7 @@
"changelogLabel": "Changelog"
},
"latestCommitCard": {
"loadingLabel": "Loading",
"loadingLabel": "Loading...",
"timeagoLabel": "{time} ago",
"patcherLabel": "Patcher: ",
"managerLabel": "Manager: ",
@ -44,38 +44,45 @@
"widgetTitle": "Patcher",
"patchButton": "Patch",
"patchDialogTitle": "Warning",
"patchDialogText": "You have selected a resource patch and a split APK installation was detected so patching errors can occur.\nAre you sure you want to proceed with patching a split base APK?"
"patchDialogText": "You have selected a resource patch and a split APK installation was detected so patching errors may occur.\nAre you sure you want to proceed with patching a split base APK?"
},
"appSelectorCard": {
"widgetTitle": "Select application",
"widgetTitleSelected": "Selected application",
"widgetSubtitle": "No application selected.",
"noAppsLabel": "No applications found."
"widgetSubtitle": "No application selected",
"noAppsLabel": "No applications found.",
"currentVersion": "Current",
"recommendedVersion": "Recommended",
"anyVersion": "any"
},
"patchSelectorCard": {
"widgetTitle": "Select patches",
"widgetTitleSelected": "Selected patches",
"widgetSubtitle": "Select an application first.",
"widgetEmptySubtitle": "No patches selected."
"widgetSubtitle": "Select an application first",
"widgetEmptySubtitle": "No patches selected"
},
"socialMediaCard": {
"widgetTitle": "Social Media",
"widgetTitle": "Socials",
"widgetSubtitle": "We are online!"
},
"appSelectorView": {
"viewTitle": "Select application",
"searchBarHint": "Search applications",
"storageButton": "Storage",
"errorMessage": "Unable to use selected application."
},
"patchesSelectorView": {
"viewTitle": "Select patches",
"searchBarHint": "Search patches",
"doneButton": "Done",
"noPatchesFound": "No patches found for the selected app."
"noPatchesFound": "No patches found for the selected app",
"selectAllPatchesWarningTitle": "Warning",
"selectAllPatchesWarningContent": "You are about to select all patches, that includes unrecommended patches and can cause unwanted behavior."
},
"patchItem": {
"unsupportedWarningButton": "Unsupported version",
"unsupportedDialogTitle": "Warning",
"unsupportedDialogText": "Selecting this patch may or may not result in patching errors.\n\nApp version: {packageVersion}\nCurrent supported versions:\n{supportedVersions}"
"unsupportedDialogText": "Selecting this patch may result in patching errors.\n\nApp version: {packageVersion}\nCurrent supported versions:\n{supportedVersions}"
},
"installerView": {
"widgetTitle": "Installer",
@ -86,14 +93,19 @@
"notificationTitle": "ReVanced Manager is patching",
"notificationText": "Tap to return to the installer",
"shareApkMenuOption": "Share APK",
"shareLogMenuOption": "Share log"
"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.",
"installErrorDialogText3": "Root install is not possible as the original APK was selected from storage.\nSelect an installed app or choose non-root install.",
"noExit": "Installer is still running..."
},
"settingsView": {
"widgetTitle": "Settings",
"appearanceSectionTitle": "Appearance",
"patcherSectionTitle": "Patcher",
"teamSectionTitle": "Team",
"infoSectionTitle": "Info",
"advancedSectionTitle": "Advanced",
"darkThemeLabel": "Dark Mode",
"darkThemeHint": "Welcome to the Dark Side",
"dynamicThemeLabel": "Material You",
@ -103,16 +115,20 @@
"frenchOption": "French",
"sourcesLabel": "Sources",
"sourcesLabelHint": "Configure your custom sources",
"orgPatchesLabel" : "Patches Org",
"orgPatchesLabel" : "Patches Organization",
"sourcesPatchesLabel" : "Patches Source",
"orgIntegrationsLabel": "Integrations Org",
"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 device debug logs",
"apiURLLabel": "API URL",
"apiURLHint": "Configure your custom API URL",
"selectApiURL": "Select URL",
"aboutLabel": "About",
"snackbarMessage": "Copied to clipboard"
},
@ -122,9 +138,6 @@
"uninstallButton": "Uninstall",
"patchButton": "Patch",
"unpatchButton": "Unpatch",
"uninstallDialogTitle": "Uninstall",
"uninstallDialogText": "Are you sure you want to uninstall this app?",
"unpatchDialogTitle": "Unpatch",
"unpatchDialogText": "Are you sure you want to unpatch this app?",
"rootDialogTitle": "Error",
"rootDialogText": "App was installed with root mode enabled but currently root mode is disabled.\nPlease enable root mode first.",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

3
crowdin.yml Normal file
View File

@ -0,0 +1,3 @@
files:
- source: /assets/i18n/en.json
translation: /assets/i18n/%two_letters_code%.json

View File

@ -2,6 +2,7 @@ import 'package:revanced_manager/services/github_api.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/services/revanced_api.dart';
import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/app_selector/app_selector_view.dart';
import 'package:revanced_manager/ui/views/contributors/contributors_view.dart';
import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
@ -36,6 +37,7 @@ import 'package:stacked_services/stacked_services.dart';
LazySingleton(classType: PatcherAPI),
LazySingleton(classType: RevancedAPI),
LazySingleton(classType: GithubAPI),
LazySingleton(classType: Toast),
],
)
class AppSetup {}

View File

@ -9,15 +9,18 @@ import 'package:revanced_manager/services/revanced_api.dart';
import 'package:revanced_manager/ui/theme/dynamic_theme_builder.dart';
import 'package:revanced_manager/ui/views/navigation/navigation_view.dart';
import 'package:stacked_themes/stacked_themes.dart';
import 'package:timezone/data/latest.dart' as tz;
Future main() async {
await ThemeManager.initialise();
await setupLocator();
WidgetsFlutterBinding.ensureInitialized();
await locator<ManagerAPI>().initialize();
await locator<PatcherAPI>().initialize();
locator<RevancedAPI>().initialize();
String apiUrl = locator<ManagerAPI>().getApiUrl();
await locator<RevancedAPI>().initialize(apiUrl);
locator<GithubAPI>().initialize();
await locator<PatcherAPI>().initialize();
tz.initializeTimeZones();
runApp(const MyApp());
}

View File

@ -17,6 +17,7 @@ class PatchedApplication {
Uint8List icon;
DateTime patchDate;
bool isRooted;
bool isFromStorage;
bool hasUpdates;
List<String> appliedPatches;
List<String> changelog;
@ -29,6 +30,7 @@ class PatchedApplication {
required this.icon,
required this.patchDate,
this.isRooted = false,
this.isFromStorage = false,
this.hasUpdates = false,
this.appliedPatches = const [],
this.changelog = const [],

View File

@ -6,11 +6,18 @@ import 'package:dio_http_cache_lts/dio_http_cache_lts.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:injectable/injectable.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:dio_http2_adapter/dio_http2_adapter.dart';
@lazySingleton
class GithubAPI {
final String apiUrl = 'https://api.github.com';
final Dio _dio = Dio();
final Dio _dio = Dio(
BaseOptions(baseUrl: 'https://api.github.com'),
)..httpClientAdapter = Http2Adapter(
ConnectionManager(
idleTimeout: 10000,
onClientCreate: (_, config) => config.onBadCertificate = (_) => true,
),
);
final DioCacheManager _dioCacheManager = DioCacheManager(CacheConfig());
final Options _cacheOptions = buildCacheOptions(
const Duration(days: 1),
@ -37,7 +44,7 @@ class GithubAPI {
Future<Map<String, dynamic>?> _getLatestRelease(String repoName) async {
try {
var response = await _dio.get(
'$apiUrl/repos/$repoName/releases/latest',
'/repos/$repoName/releases/latest',
options: _cacheOptions,
);
return response.data;
@ -55,7 +62,7 @@ class GithubAPI {
'src/main/kotlin/app/revanced/patches/${repoAppPath[packageName]}';
try {
var response = await _dio.get(
'$apiUrl/repos/$repoName/commits',
'/repos/$repoName/commits',
queryParameters: {
'path': path,
'per_page': 3,

View File

@ -19,6 +19,7 @@ class ManagerAPI {
final String patcherRepo = 'revanced-patcher';
final String cliRepo = 'revanced-cli';
late SharedPreferences _prefs;
String defaultApiUrl = 'https://revanced-releases-api.afterst0rm.xyz';
String defaultPatcherRepo = 'revanced/revanced-patcher';
String defaultPatchesRepo = 'revanced/revanced-patches';
String defaultIntegrationsRepo = 'revanced/revanced-integrations';
@ -29,6 +30,19 @@ class ManagerAPI {
_prefs = await SharedPreferences.getInstance();
}
String getApiUrl() {
return _prefs.getString('apiUrl') ?? defaultApiUrl;
}
Future<void> setApiUrl(String url) async {
if (url.isEmpty || url == ' ') {
url = defaultApiUrl;
}
await _revancedAPI.initialize(url);
await _revancedAPI.clearAllCache();
await _prefs.setString('apiUrl', url);
}
String getPatchesRepo() {
return _prefs.getString('patchesRepo') ?? defaultPatchesRepo;
}
@ -83,8 +97,10 @@ class ManagerAPI {
Future<void> savePatchedApp(PatchedApplication app) async {
List<PatchedApplication> patchedApps = getPatchedApps();
patchedApps.removeWhere((a) => a.packageName == app.packageName);
ApplicationWithIcon? installed =
await DeviceApps.getApp(app.packageName, true) as ApplicationWithIcon?;
ApplicationWithIcon? installed = await DeviceApps.getApp(
app.packageName,
true,
) as ApplicationWithIcon?;
if (installed != null) {
app.name = installed.appName;
app.version = installed.versionName!;
@ -110,10 +126,11 @@ class ManagerAPI {
}
Future<List<Patch>> getPatches() async {
if (getPatchesRepo() == defaultPatchesRepo) {
String repoName = getPatchesRepo();
if (repoName == defaultPatchesRepo) {
return await _revancedAPI.getPatches();
} else {
return await _githubAPI.getPatches(getPatchesRepo());
return await _githubAPI.getPatches(repoName);
}
}
@ -182,23 +199,28 @@ class ManagerAPI {
List<PatchedApplication> patchedApps,
) async {
List<PatchedApplication> unsavedApps = [];
List<String> installedApps = await _rootAPI.getInstalledApps();
for (String packageName in installedApps) {
if (!patchedApps.any((app) => app.packageName == packageName)) {
ApplicationWithIcon? application =
await DeviceApps.getApp(packageName, true) as ApplicationWithIcon?;
if (application != null) {
unsavedApps.add(
PatchedApplication(
name: application.appName,
packageName: application.packageName,
version: application.versionName!,
apkFilePath: application.apkFilePath,
icon: application.icon,
patchDate: DateTime.now(),
isRooted: true,
),
);
bool hasRootPermissions = await _rootAPI.hasRootPermissions();
if (hasRootPermissions) {
List<String> installedApps = await _rootAPI.getInstalledApps();
for (String packageName in installedApps) {
if (!patchedApps.any((app) => app.packageName == packageName)) {
ApplicationWithIcon? application = await DeviceApps.getApp(
packageName,
true,
) as ApplicationWithIcon?;
if (application != null) {
unsavedApps.add(
PatchedApplication(
name: application.appName,
packageName: application.packageName,
version: application.versionName!,
apkFilePath: application.apkFilePath,
icon: application.icon,
patchDate: DateTime.now(),
isRooted: true,
),
);
}
}
}
}
@ -208,10 +230,12 @@ class ManagerAPI {
);
for (Application app in userApps) {
if (app.packageName.startsWith('app.revanced') &&
!app.packageName.startsWith('app.revanced.manager.')) {
ApplicationWithIcon? application =
await DeviceApps.getApp(app.packageName, true)
as ApplicationWithIcon?;
!app.packageName.startsWith('app.revanced.manager.') &&
!patchedApps.any((uapp) => uapp.packageName == app.packageName)) {
ApplicationWithIcon? application = await DeviceApps.getApp(
app.packageName,
true,
) as ApplicationWithIcon?;
if (application != null) {
unsavedApps.add(
PatchedApplication(
@ -227,7 +251,6 @@ class ManagerAPI {
}
}
}
return unsavedApps;
}
@ -260,14 +283,15 @@ class ManagerAPI {
Future<bool> isAppUninstalled(PatchedApplication app) async {
bool existsRoot = false;
bool existsNonRoot = await DeviceApps.isAppInstalled(app.packageName);
if (app.isRooted) {
bool hasRootPermissions = await _rootAPI.hasRootPermissions();
if (hasRootPermissions) {
existsRoot = await _rootAPI.isAppInstalled(app.packageName);
}
return !existsRoot || !existsNonRoot;
}
bool existsNonRoot = await DeviceApps.isAppInstalled(app.packageName);
return !existsRoot && !existsNonRoot;
return !existsNonRoot;
}
Future<bool> hasAppUpdates(String packageName, DateTime patchDate) async {
@ -295,4 +319,14 @@ class ManagerAPI {
}
return newCommits;
}
Future<bool> isSplitApk(PatchedApplication patchedApp) async {
Application? app;
if (patchedApp.isFromStorage) {
app = await DeviceApps.getAppFromStorage(patchedApp.apkFilePath);
} else {
app = await DeviceApps.getApp(patchedApp.packageName);
}
return app != null && app.isSplit;
}
}

View File

@ -53,9 +53,10 @@ class PatcherAPI {
for (Package package in patch.compatiblePackages) {
try {
if (!filteredApps.any((app) => app.packageName == package.name)) {
ApplicationWithIcon? app =
await DeviceApps.getApp(package.name, true)
as ApplicationWithIcon?;
ApplicationWithIcon? app = await DeviceApps.getApp(
package.name,
true,
) as ApplicationWithIcon?;
if (app != null) {
filteredApps.add(app);
}
@ -69,10 +70,14 @@ class PatcherAPI {
}
Future<List<Patch>> getFilteredPatches(String packageName) async {
String newPackageName = packageName.replaceFirst(
'app.revanced.',
'com.google.',
);
return _patches
.where((patch) =>
!patch.name.contains('settings') &&
patch.compatiblePackages.any((pack) => pack.name == packageName))
patch.compatiblePackages.any((pack) => pack.name == newPackageName))
.toList();
}
@ -82,21 +87,38 @@ class PatcherAPI {
.toList();
}
bool dependencyNeedsIntegrations(String name) {
return name.contains('integrations') ||
_patches.any(
(patch) =>
patch.name == name &&
(patch.dependencies.any(
(dep) => dependencyNeedsIntegrations(dep),
)),
);
}
Future<bool> needsIntegrations(List<Patch> selectedPatches) async {
return selectedPatches.any(
(patch) => patch.dependencies.contains('integrations'),
(patch) => patch.dependencies.any(
(dep) => dependencyNeedsIntegrations(dep),
),
);
}
Future<bool> needsResourcePatching(List<Patch> selectedPatches) async {
return selectedPatches.any(
(patch) => patch.dependencies.any((dep) => dep.contains('resource-')),
(patch) => patch.dependencies.any(
(dep) => dep.contains('resource-'),
),
);
}
Future<bool> needsSettingsPatch(List<Patch> selectedPatches) async {
return selectedPatches.any(
(patch) => patch.dependencies.contains('settings'),
(patch) => patch.dependencies.any(
(dep) => dep.contains('settings'),
),
);
}
@ -199,10 +221,9 @@ class PatcherAPI {
String prefix = appName.toLowerCase().replaceAll(' ', '-');
String newName = '$prefix-revanced_v$version.apk';
int lastSeparator = _outFile!.path.lastIndexOf('/');
File share = _outFile!.renameSync(
_outFile!.path.substring(0, lastSeparator + 1) + newName,
);
ShareExtend.share(share.path, 'file');
String newPath = _outFile!.path.substring(0, lastSeparator + 1) + newName;
File shareFile = _outFile!.copySync(newPath);
ShareExtend.share(shareFile.path, 'file');
}
}
@ -220,4 +241,32 @@ class PatcherAPI {
log.writeAsStringSync(logs);
ShareExtend.share(log.path, 'file');
}
String getRecommendedVersion(String packageName) {
Map<String, int> versions = {};
for (Patch patch in _patches) {
Package? package = patch.compatiblePackages.firstWhereOrNull(
(pack) => pack.name == packageName,
);
if (package != null) {
for (String version in package.versions) {
versions.update(
version,
(value) => versions[version]! + 1,
ifAbsent: () => 1,
);
}
}
}
if (versions.isNotEmpty) {
var entries = versions.entries.toList()
..sort((a, b) => a.value.compareTo(b.value));
versions
..clear()
..addEntries(entries);
versions.removeWhere((key, value) => value != versions.values.last);
return (versions.keys.toList()..sort()).last;
}
return '';
}
}

View File

@ -6,18 +6,27 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:injectable/injectable.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:timeago/timeago.dart';
import 'package:dio_http2_adapter/dio_http2_adapter.dart';
@lazySingleton
class RevancedAPI {
final String apiUrl = 'https://revanced-releases-api.afterst0rm.xyz';
final Dio _dio = Dio();
late Dio _dio = Dio();
final DioCacheManager _dioCacheManager = DioCacheManager(CacheConfig());
final Options _cacheOptions = buildCacheOptions(
const Duration(days: 1),
maxStale: const Duration(days: 7),
);
void initialize() {
Future<void> initialize(String apiUrl) async {
_dio = Dio(BaseOptions(
baseUrl: apiUrl,
))
..httpClientAdapter = Http2Adapter(
ConnectionManager(
idleTimeout: 10000,
onClientCreate: (_, config) => config.onBadCertificate = (_) => true,
),
);
_dio.interceptors.add(_dioCacheManager.interceptor);
}
@ -28,10 +37,7 @@ class RevancedAPI {
Future<Map<String, List<dynamic>>> getContributors() async {
Map<String, List<dynamic>> contributors = {};
try {
var response = await _dio.get(
'$apiUrl/contributors',
options: _cacheOptions,
);
var response = await _dio.get('/contributors', options: _cacheOptions);
List<dynamic> repositories = response.data['repositories'];
for (Map<String, dynamic> repo in repositories) {
String name = repo['name'];
@ -45,7 +51,7 @@ class RevancedAPI {
Future<List<Patch>> getPatches() async {
try {
var response = await _dio.get('$apiUrl/patches', options: _cacheOptions);
var response = await _dio.get('/patches', options: _cacheOptions);
List<dynamic> patches = response.data;
return patches.map((patch) => Patch.fromJson(patch)).toList();
} on Exception {
@ -58,7 +64,7 @@ class RevancedAPI {
String repoName,
) async {
try {
var response = await _dio.get('$apiUrl/tools', options: _cacheOptions);
var response = await _dio.get('/tools', options: _cacheOptions);
List<dynamic> tools = response.data['tools'];
return tools.firstWhereOrNull(
(t) =>
@ -71,10 +77,14 @@ class RevancedAPI {
}
Future<String?> getLatestReleaseVersion(
String extension, String repoName) async {
String extension,
String repoName,
) async {
try {
Map<String, dynamic>? release =
await _getLatestRelease(extension, repoName);
Map<String, dynamic>? release = await _getLatestRelease(
extension,
repoName,
);
if (release != null) {
return release['version'];
}

View File

@ -5,10 +5,23 @@ class RootAPI {
final String _postFsDataDirPath = '/data/adb/post-fs-data.d';
final String _serviceDDirPath = '/data/adb/service.d';
Future<bool> isRooted() async {
try {
bool? isRooted = await Root.isRootAvailable();
return isRooted != null && isRooted;
} on Exception {
return false;
}
}
Future<bool> hasRootPermissions() async {
try {
bool? isRooted = await Root.isRooted();
return isRooted != null && isRooted;
bool? isRooted = await Root.isRootAvailable();
if (isRooted != null && isRooted) {
isRooted = await Root.isRooted();
return isRooted != null && isRooted;
}
return false;
} on Exception {
return false;
}
@ -59,15 +72,8 @@ class RootAPI {
);
if (res != null) {
List<String> apps = res.split('\n');
List<String> toRemove = [];
for (String packageName in apps) {
bool isInstalled = await isAppInstalled(packageName);
if (!isInstalled) {
toRemove.add(packageName);
}
}
apps.removeWhere((a) => toRemove.contains(a));
return apps;
apps.removeWhere((pack) => pack.isEmpty);
return apps.map((pack) => pack.trim()).toList();
}
} on Exception {
return List.empty();

23
lib/services/toast.dart Normal file
View File

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:fluttertoast/fluttertoast.dart' as t;
class Toast {
final t.FToast _fToast = t.FToast();
late BuildContext buildContext;
void initialize(BuildContext context) {
_fToast.init(context);
}
void show(String text) {
t.Fluttertoast.showToast(
msg: FlutterI18n.translate(
_fToast.context!,
text,
),
toastLength: t.Toast.LENGTH_LONG,
gravity: t.ToastGravity.CENTER,
);
}
}

View File

@ -31,53 +31,77 @@ class _AppSelectorViewState extends State<AppSelectorView> {
Navigator.of(context).pop();
},
),
body: SafeArea(
child: Padding(
padding:
const EdgeInsets.symmetric(vertical: 4.0, horizontal: 12.0),
child: Column(
children: <Widget>[
SearchBar(
showSelectIcon: false,
hintText: FlutterI18n.translate(
context,
'appSelectorView.searchBarHint',
body: CustomScrollView(
slivers: [
SliverAppBar(
pinned: true,
floating: true,
snap: false,
title: I18nText(
'appSelectorView.viewTitle',
child: Text(
'',
style: TextStyle(
color: Theme.of(context).textTheme.headline6!.color,
),
onQueryChanged: (searchQuery) {
setState(() {
_query = searchQuery;
});
},
),
const SizedBox(height: 12),
Expanded(
child: model.noApps
? Center(
child: I18nText('appSelectorCard.noAppsLabel'),
)
: model.apps.isEmpty
? const AppSkeletonLoader()
: ListView(
padding: const EdgeInsets.only(bottom: 80),
children: model
.getFilteredApps(_query)
.map((app) => InkWell(
onTap: () {
model.selectApp(app);
Navigator.of(context).pop();
},
child: InstalledAppItem(
name: app.appName,
pkgName: app.packageName,
icon: app.icon,
),
))
.toList(),
),
),
leading: IconButton(
icon: Icon(
Icons.arrow_back,
color: Theme.of(context).textTheme.headline6!.color,
),
],
onPressed: () => Navigator.of(context).pop(),
),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(64.0),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 12.0,
),
child: SearchBar(
showSelectIcon: false,
hintText: FlutterI18n.translate(
context,
'appSelectorView.searchBarHint',
),
onQueryChanged: (searchQuery) {
setState(() {
_query = searchQuery;
});
},
),
),
),
),
),
SliverToBoxAdapter(
child: model.noApps
? Center(
child: I18nText('appSelectorCard.noAppsLabel'),
)
: model.apps.isEmpty
? const AppSkeletonLoader()
: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0)
.copyWith(bottom: 80),
child: Column(
children: model
.getFilteredApps(_query)
.map((app) => InstalledAppItem(
name: app.appName,
pkgName: app.packageName,
icon: app.icon,
onTap: () {
model.selectApp(app);
Navigator.of(context).pop();
},
))
.toList(),
),
),
),
],
),
),
);

View File

@ -2,16 +2,16 @@ import 'dart:io';
import 'package:device_apps/device_apps.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:stacked/stacked.dart';
class AppSelectorViewModel extends BaseViewModel {
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
final Toast _toast = locator<Toast>();
final List<ApplicationWithIcon> apps = [];
bool noApps = false;
@ -43,9 +43,10 @@ class AppSelectorViewModel extends BaseViewModel {
);
if (result != null && result.files.single.path != null) {
File apkFile = File(result.files.single.path!);
ApplicationWithIcon? application =
await DeviceApps.getAppFromStorage(apkFile.path, true)
as ApplicationWithIcon?;
ApplicationWithIcon? application = await DeviceApps.getAppFromStorage(
apkFile.path,
true,
) as ApplicationWithIcon?;
if (application != null) {
locator<PatcherViewModel>().selectedApp = PatchedApplication(
name: application.appName,
@ -54,20 +55,14 @@ class AppSelectorViewModel extends BaseViewModel {
apkFilePath: result.files.single.path!,
icon: application.icon,
patchDate: DateTime.now(),
isFromStorage: true,
);
locator<PatcherViewModel>().selectedPatches.clear();
locator<PatcherViewModel>().notifyListeners();
}
}
} on Exception {
Fluttertoast.showToast(
msg: FlutterI18n.translate(
context,
'appSelectorView.errorMessage',
),
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.CENTER,
);
_toast.show('appSelectorView.errorMessage');
}
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:animations/animations.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/homeView/available_updates_card.dart';
@ -27,6 +28,7 @@ class HomeView extends StatelessWidget {
child: CustomScrollView(
slivers: <Widget>[
CustomSliverAppBar(
isMainView: true,
title: I18nText(
'homeView.widgetTitle',
child: Text(
@ -83,9 +85,26 @@ class HomeView extends StatelessWidget {
],
),
const SizedBox(height: 14),
model.showUpdatableApps
? AvailableUpdatesCard()
: InstalledAppsCard(),
PageTransitionSwitcher(
transitionBuilder:
(child, primaryAnimation, secondaryAnimation) {
return FadeThroughTransition(
animation: primaryAnimation,
secondaryAnimation: secondaryAnimation,
fillColor: Colors.transparent,
child: child,
);
},
layoutBuilder: (entries) {
return Stack(
alignment: Alignment.topCenter,
children: entries,
);
},
child: model.showUpdatableApps
? AvailableUpdatesCard()
: InstalledAppsCard(),
),
],
),
),

View File

@ -6,24 +6,26 @@ import 'package:device_apps/device_apps.dart';
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:injectable/injectable.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/app/app.router.dart';
import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/installerView/custom_material_button.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:timezone/timezone.dart' as tz;
@lazySingleton
class HomeViewModel extends BaseViewModel {
final NavigationService _navigationService = locator<NavigationService>();
final ManagerAPI _managerAPI = locator<ManagerAPI>();
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
final Toast _toast = locator<Toast>();
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
DateTime? _lastUpdate;
bool showUpdatableApps = true;
@ -38,16 +40,13 @@ class HomeViewModel extends BaseViewModel {
onSelectNotification: (p) =>
DeviceApps.openApp('app.revanced.manager.flutter'),
);
flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.requestPermission();
bool isConnected = await Connectivity().checkConnection();
if (!isConnected) {
Fluttertoast.showToast(
msg: FlutterI18n.translate(
context,
'homeView.noConnection',
),
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.CENTER,
);
_toast.show('homeView.noConnection');
}
_getPatchedApps();
_managerAPI.reAssessSavedApps().then((_) => _getPatchedApps());
@ -74,7 +73,10 @@ class HomeViewModel extends BaseViewModel {
}
void _getPatchedApps() {
patchedInstalledApps = _managerAPI.getPatchedApps().toList();
patchedInstalledApps = _managerAPI
.getPatchedApps()
.where((app) => app.hasUpdates == false)
.toList();
patchedUpdatableApps = _managerAPI
.getPatchedApps()
.where((app) => app.hasUpdates == true)
@ -99,69 +101,48 @@ class HomeViewModel extends BaseViewModel {
return false;
}
void updateManager(BuildContext context) async {
Fluttertoast.showToast(
msg: FlutterI18n.translate(
context,
'homeView.downloadingMessage',
),
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.CENTER,
);
File? managerApk = await _managerAPI.downloadManager();
if (managerApk != null) {
flutterLocalNotificationsPlugin.show(
0,
FlutterI18n.translate(
context,
'homeView.notificationTitle',
),
FlutterI18n.translate(
context,
'homeView.notificationText',
),
const NotificationDetails(
android: AndroidNotificationDetails(
'revanced_manager_channel',
'ReVanced Manager Channel',
),
),
);
try {
Fluttertoast.showToast(
msg: FlutterI18n.translate(
Future<void> updateManager(BuildContext context) async {
try {
_toast.show('homeView.downloadingMessage');
File? managerApk = await _managerAPI.downloadManager();
if (managerApk != null) {
await flutterLocalNotificationsPlugin.zonedSchedule(
0,
FlutterI18n.translate(
context,
'homeView.installingMessage',
'homeView.notificationTitle',
),
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.CENTER,
FlutterI18n.translate(
context,
'homeView.notificationText',
),
tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)),
const NotificationDetails(
android: AndroidNotificationDetails(
'revanced_manager_channel',
'ReVanced Manager Channel',
importance: Importance.max,
priority: Priority.high,
ticker: 'ticker',
),
),
androidAllowWhileIdle: true,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
);
_toast.show('homeView.installingMessage');
await AppInstaller.installApk(managerApk.path);
} on Exception {
Fluttertoast.showToast(
msg: FlutterI18n.translate(
context,
'homeView.errorInstallMessage',
),
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.CENTER,
);
} else {
_toast.show('homeView.errorDownloadMessage');
}
} else {
Fluttertoast.showToast(
msg: FlutterI18n.translate(
context,
'homeView.errorDownloadMessage',
),
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.CENTER,
);
} on Exception {
_toast.show('homeView.errorInstallMessage');
}
}
Future<void> showUpdateConfirmationDialog(BuildContext context) async {
Future<void> showUpdateConfirmationDialog(BuildContext parentContext) async {
return showDialog(
context: context,
context: parentContext,
builder: (context) => AlertDialog(
title: I18nText('homeView.updateDialogTitle'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
@ -169,14 +150,14 @@ class HomeViewModel extends BaseViewModel {
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('cancelButton'),
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(),
),
CustomMaterialButton(
label: I18nText('okButton'),
label: I18nText('yesButton'),
onPressed: () {
Navigator.of(context).pop();
updateManager(context);
updateManager(parentContext);
},
)
],

View File

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

View File

@ -1,21 +1,27 @@
// ignore_for_file: use_build_context_synchronously
import 'package:device_apps/device_apps.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_background/flutter_background.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
//import 'package:permission_handler/permission_handler.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/services/root_api.dart';
import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:stacked/stacked.dart';
import 'package:wakelock/wakelock.dart';
class InstallerViewModel extends BaseViewModel {
final ManagerAPI _managerAPI = locator<ManagerAPI>();
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
final RootAPI _rootAPI = RootAPI();
final Toast _toast = locator<Toast>();
final PatchedApplication _app = locator<PatcherViewModel>().selectedApp!;
final List<Patch> _patches = locator<PatcherViewModel>().selectedPatches;
static const _installerChannel = MethodChannel(
@ -25,14 +31,16 @@ class InstallerViewModel extends BaseViewModel {
double? progress = 0.0;
String logs = '';
String headerLogs = '';
bool isRooted = false;
bool isPatching = true;
bool isInstalled = false;
bool hasErrors = false;
Future<void> initialize(BuildContext context) async {
if (true /*await Permission.ignoreBatteryOptimizations.isGranted*/) {
isRooted = await _rootAPI.isRooted();
if (await Permission.ignoreBatteryOptimizations.isGranted) {
try {
await FlutterBackground.initialize(
FlutterBackground.initialize(
androidConfig: FlutterBackgroundAndroidConfig(
notificationTitle: FlutterI18n.translate(
context,
@ -48,8 +56,7 @@ class InstallerViewModel extends BaseViewModel {
defType: 'drawable',
),
),
);
await FlutterBackground.enableBackgroundExecution();
).then((value) => FlutterBackground.enableBackgroundExecution());
} on Exception {
// ignore
}
@ -76,14 +83,20 @@ class InstallerViewModel extends BaseViewModel {
}
void update(double value, String header, String log) {
if (value > 0) {
if (value >= 0.0) {
progress = value;
}
isPatching = progress == 1.0 ? false : true;
if (progress == 0.0) {
if (value == 0.0) {
logs = '';
isPatching = true;
isInstalled = false;
hasErrors = false;
} else if (value == 1.0) {
isPatching = false;
hasErrors = false;
} else if (value == -100.0) {
isPatching = false;
hasErrors = true;
}
if (header.isNotEmpty) {
headerLogs = header;
@ -93,6 +106,9 @@ class InstallerViewModel extends BaseViewModel {
logs += '\n';
}
logs += log;
if (logs[logs.length - 1] == '\n') {
logs = logs.substring(0, logs.length - 1);
}
Future.delayed(const Duration(milliseconds: 500)).then((value) {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
@ -115,46 +131,76 @@ class InstallerViewModel extends BaseViewModel {
_patches,
);
} catch (e) {
hasErrors = true;
update(-1.0, 'Aborting...', 'An error occurred! Aborting\nError: $e');
update(
-100.0,
'Aborting...',
'An error occurred! Aborting\nError:\n$e',
);
}
} else {
hasErrors = true;
update(-1.0, 'Aborting...', 'No app or patches selected! Aborting');
update(-100.0, 'Aborting...', 'No app or patches selected! Aborting');
}
if (true /*await Permission.ignoreBatteryOptimizations.isGranted*/) {
if (FlutterBackground.isBackgroundExecutionEnabled) {
try {
await FlutterBackground.disableBackgroundExecution();
FlutterBackground.disableBackgroundExecution();
} on Exception {
// ignore
}
}
await Wakelock.disable();
isPatching = false;
}
void installResult(bool installAsRoot) async {
void installResult(BuildContext context, bool installAsRoot) async {
_app.isRooted = installAsRoot;
update(
1.0,
'Installing...',
_app.isRooted
? 'Installing patched file using root method'
: 'Installing patched file using nonroot method',
);
isInstalled = await _patcherAPI.installPatchedFile(_app);
if (isInstalled) {
update(1.0, 'Installed!', 'Installed!');
_app.patchDate = DateTime.now();
_app.appliedPatches = _patches.map((p) => p.name).toList();
bool hasMicroG = _patches.any((p) => p.name.endsWith('microg-support'));
if (hasMicroG) {
_app.packageName = _app.packageName.replaceFirst(
'com.google.',
'app.revanced.',
);
bool hasMicroG = _patches.any((p) => p.name.endsWith('microg-support'));
bool rootMicroG = installAsRoot && hasMicroG;
bool rootFromStorage = installAsRoot && _app.isFromStorage;
bool ytWithoutRootMicroG =
!installAsRoot && !hasMicroG && _app.packageName.contains('youtube');
if (rootMicroG || rootFromStorage || ytWithoutRootMicroG) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('installerView.installErrorDialogTitle'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText(
rootMicroG
? 'installerView.installErrorDialogText1'
: rootFromStorage
? 'installerView.installErrorDialogText3'
: 'installerView.installErrorDialogText2',
),
actions: <Widget>[
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () => Navigator.of(context).pop(),
)
],
),
);
} else {
update(
1.0,
'Installing...',
_app.isRooted
? 'Installing patched file using root method'
: 'Installing patched file using nonroot method',
);
isInstalled = await _patcherAPI.installPatchedFile(_app);
if (isInstalled) {
update(1.0, 'Installed!', 'Installed!');
_app.isFromStorage = false;
_app.patchDate = DateTime.now();
_app.appliedPatches = _patches.map((p) => p.name).toList();
if (hasMicroG) {
_app.name += ' ReVanced';
_app.packageName = _app.packageName.replaceFirst(
'com.google.',
'app.revanced.',
);
}
await _managerAPI.savePatchedApp(_app);
}
await _managerAPI.savePatchedApp(_app);
}
}
@ -187,4 +233,14 @@ class InstallerViewModel extends BaseViewModel {
break;
}
}
Future<bool> onWillPop(BuildContext context) async {
if (isPatching) {
_toast.show('installerView.noExit');
return false;
}
cleanPatcher();
Navigator.of(context).pop();
return true;
}
}

View File

@ -3,8 +3,10 @@ import 'package:dynamic_themes/dynamic_themes.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:injectable/injectable.dart';
//import 'package:permission_handler/permission_handler.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/root_api.dart';
import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/home/home_view.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_view.dart';
import 'package:revanced_manager/ui/views/settings/settings_view.dart';
@ -14,7 +16,16 @@ import 'package:stacked/stacked.dart';
@lazySingleton
class NavigationViewModel extends IndexTrackingViewModel {
void initialize(BuildContext context) async {
locator<Toast>().initialize(context);
SharedPreferences prefs = await SharedPreferences.getInstance();
if (prefs.getBool('permissionsRequested') == null) {
await prefs.setBool('permissionsRequested', true);
RootAPI().hasRootPermissions().then(
(value) => Permission.requestInstallPackages.request().then(
(value) => Permission.ignoreBatteryOptimizations.request(),
),
);
}
if (prefs.getBool('useDarkTheme') == null) {
bool isDark =
MediaQuery.of(context).platformBrightness != Brightness.light;
@ -23,20 +34,13 @@ class NavigationViewModel extends IndexTrackingViewModel {
}
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
systemNavigationBarColor:
DynamicTheme.of(context)!.theme.colorScheme.surface,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarIconBrightness:
DynamicTheme.of(context)!.theme.brightness == Brightness.light
? Brightness.dark
: Brightness.light,
),
);
//if (prefs.getBool('permissionsRequested') == null) {
//await prefs.setBool('permissionsRequested', true);
RootAPI().hasRootPermissions();
//Permission.requestInstallPackages.request();
//Permission.ignoreBatteryOptimizations.request();
//}
}
Widget getViewForIndex(int index) {

View File

@ -28,6 +28,7 @@ class PatcherView extends StatelessWidget {
body: CustomScrollView(
slivers: <Widget>[
CustomSliverAppBar(
isMainView: true,
title: I18nText(
'patcherView.widgetTitle',
child: Text(

View File

@ -1,4 +1,3 @@
import 'package:device_apps/device_apps.dart';
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:injectable/injectable.dart';
@ -6,14 +5,16 @@ import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/app/app.router.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/ui/widgets/installerView/custom_material_button.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
@lazySingleton
class PatcherViewModel extends BaseViewModel {
final NavigationService _navigationService = locator<NavigationService>();
final ManagerAPI _managerAPI = locator<ManagerAPI>();
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
PatchedApplication? selectedApp;
List<Patch> selectedPatches = [];
@ -39,13 +40,12 @@ class PatcherViewModel extends BaseViewModel {
}
Future<bool> isValidPatchConfig() async {
bool needsResourcePatching =
await _patcherAPI.needsResourcePatching(selectedPatches);
bool needsResourcePatching = await _patcherAPI.needsResourcePatching(
selectedPatches,
);
if (needsResourcePatching && selectedApp != null) {
Application? app = await DeviceApps.getApp(selectedApp!.packageName);
if (app != null && app.isSplit) {
return false;
}
bool isSplit = await _managerAPI.isSplitApk(selectedApp!);
return !isSplit;
}
return true;
}
@ -64,11 +64,11 @@ class PatcherViewModel extends BaseViewModel {
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('cancelButton'),
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(),
),
CustomMaterialButton(
label: I18nText('okButton'),
label: I18nText('yesButton'),
onPressed: () {
Navigator.of(context).pop();
navigateToInstaller();
@ -79,4 +79,32 @@ class PatcherViewModel extends BaseViewModel {
);
}
}
String getAppSelectionString() {
String text = '${selectedApp!.name} (${selectedApp!.packageName})';
if (text.length > 32) {
text = '${text.substring(0, 32)}...)';
}
return text;
}
String getRecommendedVersionString(BuildContext context) {
String recommendedVersion =
_patcherAPI.getRecommendedVersion(selectedApp!.packageName);
if (recommendedVersion.isEmpty) {
recommendedVersion = FlutterI18n.translate(
context,
'appSelectorCard.anyVersion',
);
} else {
recommendedVersion = 'v$recommendedVersion';
}
return '${FlutterI18n.translate(
context,
'appSelectorCard.currentVersion',
)}: v${selectedApp!.version}\n${FlutterI18n.translate(
context,
'appSelectorCard.recommendedVersion',
)}: $recommendedVersion';
}
}

View File

@ -33,144 +33,175 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
},
),
),
body: SafeArea(
child: Padding(
padding:
const EdgeInsets.symmetric(vertical: 4.0, horizontal: 12.0),
child: Column(
children: <Widget>[
SearchBar(
showSelectIcon: true,
hintText: FlutterI18n.translate(
context,
'patchesSelectorView.searchBarHint',
body: CustomScrollView(
slivers: [
SliverAppBar(
pinned: true,
floating: true,
snap: false,
title: I18nText(
'patchesSelectorView.viewTitle',
child: Text(
'',
style: TextStyle(
color: Theme.of(context).textTheme.headline6!.color,
),
onQueryChanged: (searchQuery) {
setState(() {
_query = searchQuery;
});
},
onSelectAll: (value) => model.selectAllPatches(value),
),
const SizedBox(height: 12),
Expanded(
child: model.patches.isEmpty
? Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: I18nText(
'patchesSelectorView.noPatchesFound',
child: Text(
'',
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
leading: IconButton(
icon: Icon(
Icons.arrow_back,
color: Theme.of(context).textTheme.headline6!.color,
),
onPressed: () => Navigator.of(context).pop(),
),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(64.0),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 12.0,
),
child: SearchBar(
showSelectIcon: true,
hintText: FlutterI18n.translate(
context,
'patchesSelectorView.searchBarHint',
),
onQueryChanged: (searchQuery) {
setState(() {
_query = searchQuery;
});
},
onSelectAll: (value) {
if (value) {
model.selectAllPatcherWarning(context);
}
model.selectAllPatches(value);
},
),
),
),
),
SliverToBoxAdapter(
child: model.patches.isEmpty
? Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: I18nText(
'patchesSelectorView.noPatchesFound',
child: Text(
'',
style: Theme.of(context).textTheme.bodyMedium,
),
)
: ListView(
padding: const EdgeInsets.only(bottom: 80),
children: model
.getQueriedPatches(_query)
.map(
(patch) => PatchItem(
name: patch.name,
simpleName: patch.getSimpleName(),
version: patch.version,
description: patch.description,
packageVersion: model.getAppVersion(),
supportedPackageVersions:
model.getSupportedVersions(patch),
isUnsupported: !model.isPatchSupported(patch),
isSelected: model.isSelected(patch),
onChanged: (value) =>
model.selectPatch(patch, value),
),
/* TODO: Enable this and make use of new Patch Options implementation
patch.hasOptions ? ExpandablePanel(
controller: expController,
theme: const ExpandableThemeData(
hasIcon: false,
tapBodyToExpand: true,
tapBodyToCollapse: true,
tapHeaderToExpand: true,
),
header: Column(
children: <Widget>[
GestureDetector(
onLongPress: () =>
expController.toggle(),
child: PatchItem(
name: patch.name,
simpleName: patch.getSimpleName(),
description: patch.description,
version: patch.version,
packageVersion:
model.getAppVersion(),
supportedPackageVersions: model
.getSupportedVersions(patch),
isUnsupported: !model
.isPatchSupported(patch),
isSelected:
model.isSelected(patch),
onChanged: (value) => model
.selectPatch(patch, value),
child: const Padding(
padding: EdgeInsets.symmetric(
vertical: 8.0,
),
child: Text(
'Long press for additional options.',
),
),
),
)
: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0)
.copyWith(bottom: 80),
child: Column(
children: model
.getQueriedPatches(_query)
.map(
(patch) => PatchItem(
name: patch.name,
simpleName: patch.getSimpleName(),
version: patch.version,
description: patch.description,
packageVersion: model.getAppVersion(),
supportedPackageVersions:
model.getSupportedVersions(patch),
isUnsupported: !model.isPatchSupported(patch),
isSelected: model.isSelected(patch),
onChanged: (value) =>
model.selectPatch(patch, value),
),
/* TODO: Enable this and make use of new Patch Options implementation
patch.hasOptions ? ExpandablePanel(
controller: expController,
theme: const ExpandableThemeData(
hasIcon: false,
tapBodyToExpand: true,
tapBodyToCollapse: true,
tapHeaderToExpand: true,
),
header: Column(
children: <Widget>[
GestureDetector(
onLongPress: () =>
expController.toggle(),
child: PatchItem(
name: patch.name,
simpleName: patch.getSimpleName(),
description: patch.description,
version: patch.version,
packageVersion:
model.getAppVersion(),
supportedPackageVersions: model
.getSupportedVersions(patch),
isUnsupported: !model
.isPatchSupported(patch),
isSelected:
model.isSelected(patch),
onChanged: (value) => model
.selectPatch(patch, value),
child: const Padding(
padding: EdgeInsets.symmetric(
vertical: 8.0,
),
child: Text(
'Long press for additional options.',
),
),
),
],
),
],
),
expanded: Padding(
padding: const EdgeInsets.symmetric(
vertical: 10.0,
horizontal: 10,
),
expanded: Padding(
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 10.0,
horizontal: 10,
vertical: 8,
horizontal: 8,
),
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.tertiary
.withOpacity(0.1),
borderRadius:
BorderRadius.circular(12),
),
child: Column(
children: <Widget>[
Text(
'Patch options',
style: GoogleFonts.inter(
fontSize: 18,
fontWeight: FontWeight.w600,
),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.tertiary
.withOpacity(0.1),
borderRadius:
BorderRadius.circular(12),
),
child: Column(
children: <Widget>[
Text(
'Patch options',
style: GoogleFonts.inter(
fontSize: 18,
fontWeight: FontWeight.w600,
),
const OptionsTextField(
hint: 'App name'),
const OptionsFilePicker(
optionName: 'Choose a logo',
),
],
),
),
const OptionsTextField(
hint: 'App name'),
const OptionsFilePicker(
optionName: 'Choose a logo',
),
],
),
),
collapsed: Container(),
) */
)
.toList(),
),
),
],
),
collapsed: Container(),
) */
)
.toList(),
),
),
),
),
],
),
),
);

View File

@ -1,10 +1,13 @@
import 'package:collection/collection.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:stacked/stacked.dart';
import 'package:flutter/material.dart';
class PatchesSelectorViewModel extends BaseViewModel {
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
@ -17,13 +20,6 @@ class PatchesSelectorViewModel extends BaseViewModel {
locator<PatcherViewModel>().selectedApp!.packageName,
));
patches.sort((a, b) => a.name.compareTo(b.name));
if (selectedPatches.isEmpty) {
for (Patch patch in patches) {
if (!patch.excluded && isPatchSupported(patch)) {
selectedPatches.add(patch);
}
}
}
notifyListeners();
}
@ -42,6 +38,23 @@ class PatchesSelectorViewModel extends BaseViewModel {
notifyListeners();
}
Future<void> selectAllPatcherWarning(BuildContext context) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('patchesSelectorView.selectAllPatchesWarningTitle'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('patchesSelectorView.selectAllPatchesWarningContent'),
actions: <Widget>[
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () => Navigator.of(context).pop(),
)
],
),
);
}
void selectAllPatches(bool isSelected) {
selectedPatches.clear();
if (isSelected) {

View File

@ -13,6 +13,9 @@ import 'package:stacked/stacked.dart';
class SettingsView extends StatelessWidget {
const SettingsView({Key? key}) : super(key: key);
static const _settingsDivider =
Divider(thickness: 1.0, indent: 20.0, endIndent: 20.0);
@override
Widget build(BuildContext context) {
return ViewModelBuilder<SettingsViewModel>.reactive(
@ -21,6 +24,7 @@ class SettingsView extends StatelessWidget {
body: CustomScrollView(
slivers: <Widget>[
CustomSliverAppBar(
isMainView: true,
title: I18nText(
'settingsView.widgetTitle',
child: Text(
@ -31,122 +35,135 @@ class SettingsView extends StatelessWidget {
),
),
),
SliverPadding(
padding: const EdgeInsets.all(20.0),
sliver: SliverList(
delegate: SliverChildListDelegate.fixed(
<Widget>[
SettingsSection(
title: 'settingsView.appearanceSectionTitle',
children: <Widget>[
CustomSwitchTile(
title: I18nText(
'settingsView.darkThemeLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
SliverList(
delegate: SliverChildListDelegate.fixed(
<Widget>[
SettingsSection(
title: 'settingsView.appearanceSectionTitle',
children: <Widget>[
CustomSwitchTile(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.darkThemeLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
subtitle: I18nText('settingsView.darkThemeHint'),
value: model.getDarkThemeStatus(),
onTap: (value) => model.setUseDarkTheme(
context,
value,
),
),
FutureBuilder<int>(
future: model.getSdkVersion(),
builder: (context, snapshot) => Visibility(
visible: snapshot.hasData &&
snapshot.data! >= ANDROID_12_SDK_VERSION,
child: CustomSwitchTile(
title: I18nText(
'settingsView.dynamicThemeLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
subtitle: I18nText('settingsView.darkThemeHint'),
value: model.getDarkThemeStatus(),
onTap: (value) => model.setUseDarkTheme(
context,
value,
),
),
FutureBuilder<int>(
future: model.getSdkVersion(),
builder: (context, snapshot) => Visibility(
visible: snapshot.hasData &&
snapshot.data! >= ANDROID_12_SDK_VERSION,
child: CustomSwitchTile(
padding:
const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.dynamicThemeLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
subtitle:
I18nText('settingsView.dynamicThemeHint'),
value: model.getDynamicThemeStatus(),
onTap: (value) => model.setUseDynamicTheme(
context,
value,
),
),
subtitle: I18nText('settingsView.dynamicThemeHint'),
value: model.getDynamicThemeStatus(),
onTap: (value) => model.setUseDynamicTheme(
context,
value,
),
),
),
],
),
SettingsTileDialog(
title: 'settingsView.languageLabel',
subtitle: 'English',
onTap: () => model.showLanguagesDialog(context),
),
const Divider(thickness: 1.0),
SettingsSection(
title: 'settingsView.patcherSectionTitle',
children: <Widget>[
SettingsTileDialog(
title: 'settingsView.sourcesLabel',
subtitle: 'settingsView.sourcesLabelHint',
onTap: () => model.showSourcesDialog(context),
),
],
),
const Divider(thickness: 1.0),
SettingsSection(
title: 'settingsView.teamSectionTitle',
children: <Widget>[
ListTile(
contentPadding: EdgeInsets.zero,
title: I18nText(
'settingsView.contributorsLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
],
),
SettingsTileDialog(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
title: 'settingsView.languageLabel',
subtitle: 'English',
onTap: () => model.showLanguagesDialog(context),
),
_settingsDivider,
SettingsSection(
title: 'settingsView.teamSectionTitle',
children: <Widget>[
ListTile(
contentPadding:
const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.contributorsLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
subtitle: I18nText('settingsView.contributorsHint'),
onTap: () => model.navigateToContributors(),
),
const SocialMediaWidget(),
],
),
const Divider(thickness: 1.0),
SettingsSection(
title: 'settingsView.infoSectionTitle',
children: <Widget>[
ListTile(
contentPadding: EdgeInsets.zero,
title: I18nText(
'settingsView.logsLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
subtitle: I18nText('settingsView.contributorsHint'),
onTap: () => model.navigateToContributors(),
),
const SocialMediaWidget(
padding: EdgeInsets.symmetric(horizontal: 20.0),
),
],
),
_settingsDivider,
SettingsSection(
title: 'settingsView.advancedSectionTitle',
children: <Widget>[
SettingsTileDialog(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
title: 'settingsView.apiURLLabel',
subtitle: 'settingsView.apiURLHint',
onTap: () => model.showApiUrlDialog(context),
),
SettingsTileDialog(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
title: 'settingsView.sourcesLabel',
subtitle: 'settingsView.sourcesLabelHint',
onTap: () => model.showSourcesDialog(context),
),
],
),
_settingsDivider,
SettingsSection(
title: 'settingsView.infoSectionTitle',
children: <Widget>[
ListTile(
contentPadding:
const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.logsLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
subtitle: I18nText('settingsView.logsHint'),
onTap: () => model.exportLogcatLogs(),
),
const AboutWidget(),
],
),
],
),
subtitle: I18nText('settingsView.logsHint'),
onTap: () => model.exportLogcatLogs(),
),
const AboutWidget(
padding: EdgeInsets.symmetric(horizontal: 20.0),
),
],
),
],
),
),
],

View File

@ -10,7 +10,7 @@ import 'package:path_provider/path_provider.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/app/app.router.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/ui/widgets/installerView/custom_material_button.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.dart';
import 'package:share_extend/share_extend.dart';
import 'package:stacked/stacked.dart';
@ -27,6 +27,7 @@ class SettingsViewModel extends BaseViewModel {
final TextEditingController _patSourceController = TextEditingController();
final TextEditingController _orgIntSourceController = TextEditingController();
final TextEditingController _intSourceController = TextEditingController();
final TextEditingController _apiUrlController = TextEditingController();
void setLanguage(String language) {
notifyListeners();
@ -55,12 +56,6 @@ class SettingsViewModel extends BaseViewModel {
} else {
await DynamicTheme.of(context)!.setTheme(value ? 3 : 1);
}
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
systemNavigationBarColor:
DynamicTheme.of(context)!.theme.colorScheme.surface,
),
);
notifyListeners();
}
@ -78,8 +73,6 @@ class SettingsViewModel extends BaseViewModel {
}
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
systemNavigationBarColor:
DynamicTheme.of(context)!.theme.colorScheme.surface,
systemNavigationBarIconBrightness:
value ? Brightness.light : Brightness.dark,
),
@ -208,6 +201,65 @@ class SettingsViewModel extends BaseViewModel {
);
}
Future<void> showApiUrlDialog(BuildContext context) async {
String apiUrl = _managerAPI.getApiUrl();
_apiUrlController.text = apiUrl.replaceAll('https://', '');
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: <Widget>[
I18nText('settingsView.apiURLLabel'),
const Spacer(),
IconButton(
icon: const Icon(Icons.manage_history_outlined),
onPressed: () => showApiUrlResetDialog(context),
color: Theme.of(context).colorScheme.secondary,
)
],
),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: SingleChildScrollView(
child: Column(
children: <Widget>[
CustomTextField(
leadingIcon: Icon(
Icons.api_outlined,
color: Theme.of(context).colorScheme.secondary,
),
inputController: _apiUrlController,
label: I18nText('settingsView.selectApiURL'),
hint: apiUrl.split('/')[0],
onChanged: (value) => notifyListeners(),
),
],
),
),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('cancelButton'),
onPressed: () {
_apiUrlController.clear();
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () {
String apiUrl = _apiUrlController.text;
if (!apiUrl.startsWith('https')) {
apiUrl = 'https://$apiUrl';
}
_managerAPI.setApiUrl(apiUrl);
Navigator.of(context).pop();
},
)
],
),
);
}
Future<void> showResetConfirmationDialog(BuildContext context) async {
return showDialog(
context: context,
@ -218,11 +270,11 @@ class SettingsViewModel extends BaseViewModel {
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('cancelButton'),
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(),
),
CustomMaterialButton(
label: I18nText('okButton'),
label: I18nText('yesButton'),
onPressed: () {
_managerAPI.setPatchesRepo('');
_managerAPI.setIntegrationsRepo('');
@ -235,6 +287,32 @@ class SettingsViewModel extends BaseViewModel {
);
}
Future<void> showApiUrlResetDialog(BuildContext context) async {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('settingsView.sourcesResetDialogTitle'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('settingsView.apiURLResetDialogText'),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(),
),
CustomMaterialButton(
label: I18nText('yesButton'),
onPressed: () {
_managerAPI.setApiUrl('');
Navigator.of(context).pop();
Navigator.of(context).pop();
},
)
],
),
);
}
Future<int> getSdkVersion() async {
AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo;
return info.version.sdkInt ?? -1;

View File

@ -34,7 +34,7 @@ class AppInfoView extends StatelessWidget {
),
),
SliverPadding(
padding: const EdgeInsets.all(20.0),
padding: const EdgeInsets.symmetric(vertical: 20.0),
sliver: SliverList(
delegate: SliverChildListDelegate.fixed(
<Widget>[
@ -61,148 +61,192 @@ class AppInfoView extends StatelessWidget {
style: Theme.of(context).textTheme.subtitle1,
),
const SizedBox(height: 20),
CustomCard(
child: IntrinsicHeight(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
InkWell(
onTap: () => model.openApp(app),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.open_in_new_outlined,
color:
Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 10),
I18nText(
'appInfoView.openButton',
child: Text(
'',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
VerticalDivider(
color: Theme.of(context).canvasColor,
),
InkWell(
onTap: () => model.showUninstallAlertDialog(
context,
app,
false,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.delete_outline,
color:
Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 10),
I18nText(
'appInfoView.uninstallButton',
child: Text(
'',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
VerticalDivider(
color: Theme.of(context).canvasColor,
),
InkWell(
onTap: () {
model.navigateToPatcher(app);
Navigator.of(context).pop();
},
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.build_outlined,
color:
Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 10),
I18nText(
'appInfoView.patchButton',
child: Text(
'',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
Visibility(
visible: app.isRooted,
child: VerticalDivider(
color: Theme.of(context).canvasColor,
),
),
Visibility(
visible: app.isRooted,
child: InkWell(
onTap: () => model.showUninstallAlertDialog(
context,
app,
true,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.settings_backup_restore_outlined,
color:
Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 10),
I18nText(
'appInfoView.unpatchButton',
child: Text(
'',
style: TextStyle(
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: CustomCard(
padding: EdgeInsets.zero,
child: SizedBox(
height: 94.0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
child: Material(
type: MaterialType.transparency,
child: InkWell(
borderRadius: BorderRadius.circular(16.0),
onTap: () => model.openApp(app),
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.open_in_new_outlined,
color: Theme.of(context)
.colorScheme
.primary,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
I18nText(
'appInfoView.openButton',
child: Text(
'',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
fontWeight: FontWeight.bold,
),
),
),
],
),
],
),
),
),
),
],
VerticalDivider(
color: Theme.of(context).canvasColor,
indent: 12.0,
endIndent: 12.0,
width: 1.0,
),
Expanded(
child: Material(
type: MaterialType.transparency,
child: InkWell(
borderRadius: BorderRadius.circular(16.0),
onTap: () => model.showUninstallDialog(
context,
app,
false,
),
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.delete_outline,
color: Theme.of(context)
.colorScheme
.primary,
),
const SizedBox(height: 10),
I18nText(
'appInfoView.uninstallButton',
child: Text(
'',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
),
),
VerticalDivider(
color: Theme.of(context).canvasColor,
indent: 12.0,
endIndent: 12.0,
width: 1.0,
),
Expanded(
child: Material(
type: MaterialType.transparency,
child: InkWell(
borderRadius: BorderRadius.circular(16.0),
onTap: () {
model.navigateToPatcher(app);
Navigator.of(context).pop();
},
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.build_outlined,
color: Theme.of(context)
.colorScheme
.primary,
),
const SizedBox(height: 10),
I18nText(
'appInfoView.patchButton',
child: Text(
'',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
),
),
if (app.isRooted)
VerticalDivider(
color: Theme.of(context).canvasColor,
indent: 12.0,
endIndent: 12.0,
width: 1.0,
),
if (app.isRooted)
Expanded(
child: Material(
type: MaterialType.transparency,
child: InkWell(
borderRadius: BorderRadius.circular(16.0),
onTap: () => model.showUninstallDialog(
context,
app,
true,
),
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons
.settings_backup_restore_outlined,
color: Theme.of(context)
.colorScheme
.primary,
),
const SizedBox(height: 10),
I18nText(
'appInfoView.unpatchButton',
child: Text(
'',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
),
),
],
),
),
),
),
const SizedBox(height: 20),
ListTile(
contentPadding: EdgeInsets.zero,
contentPadding:
const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'appInfoView.packageNameLabel',
child: const Text(
@ -217,7 +261,8 @@ class AppInfoView extends StatelessWidget {
),
const SizedBox(height: 4),
ListTile(
contentPadding: EdgeInsets.zero,
contentPadding:
const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'appInfoView.installTypeLabel',
child: const Text(
@ -234,7 +279,8 @@ class AppInfoView extends StatelessWidget {
),
const SizedBox(height: 4),
ListTile(
contentPadding: EdgeInsets.zero,
contentPadding:
const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'appInfoView.patchedDateLabel',
child: const Text(
@ -255,7 +301,8 @@ class AppInfoView extends StatelessWidget {
),
const SizedBox(height: 4),
ListTile(
contentPadding: EdgeInsets.zero,
contentPadding:
const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'appInfoView.appliedPatchesLabel',
child: const Text(

View File

@ -1,3 +1,4 @@
// ignore_for_file: use_build_context_synchronously
import 'package:device_apps/device_apps.dart';
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
@ -10,7 +11,7 @@ import 'package:revanced_manager/services/root_api.dart';
import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/installerView/custom_material_button.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/utils/string.dart';
import 'package:stacked/stacked.dart';
@ -19,19 +20,26 @@ class AppInfoViewModel extends BaseViewModel {
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
final RootAPI _rootAPI = RootAPI();
Future<void> uninstallApp(PatchedApplication app, bool onlyUnpatch) async {
Future<void> uninstallApp(
BuildContext context,
PatchedApplication app,
bool onlyUnpatch,
) async {
bool isUninstalled = true;
if (app.isRooted) {
bool hasRootPermissions = await _rootAPI.hasRootPermissions();
if (hasRootPermissions) {
_rootAPI.deleteApp(app.packageName, app.apkFilePath);
_managerAPI.deletePatchedApp(app);
await _rootAPI.deleteApp(app.packageName, app.apkFilePath);
if (!onlyUnpatch) {
DeviceApps.uninstallApp(app.packageName);
await DeviceApps.uninstallApp(app.packageName);
}
}
} else {
DeviceApps.uninstallApp(app.packageName);
_managerAPI.deletePatchedApp(app);
isUninstalled = await DeviceApps.uninstallApp(app.packageName);
}
if (isUninstalled) {
await _managerAPI.deletePatchedApp(app);
locator<HomeViewModel>().initialize(context);
}
}
@ -43,7 +51,7 @@ class AppInfoViewModel extends BaseViewModel {
locator<NavigationViewModel>().setIndex(1);
}
Future<void> showUninstallAlertDialog(
Future<void> showUninstallDialog(
BuildContext context,
PatchedApplication app,
bool onlyUnpatch,
@ -65,38 +73,38 @@ class AppInfoViewModel extends BaseViewModel {
),
);
} else {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText(
onlyUnpatch
? 'appInfoView.unpatchDialogTitle'
: 'appInfoView.uninstallDialogTitle',
),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText(
onlyUnpatch
? 'appInfoView.unpatchDialogText'
: 'appInfoView.uninstallDialogText',
),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('cancelButton'),
onPressed: () => Navigator.of(context).pop(),
if (onlyUnpatch) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText(
'appInfoView.unpatchButton',
),
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () {
uninstallApp(app, onlyUnpatch);
locator<HomeViewModel>().initialize(context);
Navigator.of(context).pop();
Navigator.of(context).pop();
},
)
],
),
);
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText(
'appInfoView.unpatchDialogText',
),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(),
),
CustomMaterialButton(
label: I18nText('yesButton'),
onPressed: () {
uninstallApp(context, app, onlyUnpatch);
Navigator.of(context).pop();
Navigator.of(context).pop();
},
)
],
),
);
} else {
uninstallApp(context, app, onlyUnpatch);
Navigator.of(context).pop();
}
}
}

View File

@ -10,6 +10,7 @@ class AppSkeletonLoader extends StatelessWidget {
return Skeleton(
isLoading: true,
skeleton: ListView.builder(
shrinkWrap: true,
itemCount: 7,
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8.0),

View File

@ -6,12 +6,14 @@ class InstalledAppItem extends StatefulWidget {
final String name;
final String pkgName;
final Uint8List icon;
final Function()? onTap;
const InstalledAppItem({
Key? key,
required this.name,
required this.pkgName,
required this.icon,
this.onTap,
}) : super(key: key);
@override
@ -24,6 +26,7 @@ class _InstalledAppItemState extends State<InstalledAppItem> {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: CustomCard(
onTap: widget.onTap,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/installerView/custom_material_button.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
class LatestCommitCard extends StatefulWidget {

View File

@ -1,49 +0,0 @@
import 'package:flutter/material.dart';
class CustomMaterialButton extends StatelessWidget {
final Widget label;
final bool isFilled;
final bool isExpanded;
final Function()? onPressed;
const CustomMaterialButton({
Key? key,
required this.label,
this.isFilled = true,
this.isExpanded = false,
required this.onPressed,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return TextButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(
isExpanded
? const EdgeInsets.symmetric(horizontal: 24, vertical: 12)
: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
shape: MaterialStateProperty.all(
StadiumBorder(
side: isFilled
? BorderSide.none
: BorderSide(
width: 1,
color: Theme.of(context).colorScheme.primary,
),
),
),
backgroundColor: MaterialStateProperty.all(
isFilled ? Theme.of(context).colorScheme.primary : Colors.transparent,
),
foregroundColor: MaterialStateProperty.all(
isFilled
? Theme.of(context).colorScheme.surface
: Theme.of(context).colorScheme.primary,
),
),
onPressed: onPressed,
child: label,
);
}
}

View File

@ -15,53 +15,60 @@ class AppSelectorCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
return CustomCard(
onTap: onPressed,
child: CustomCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
I18nText(
locator<PatcherViewModel>().selectedApp == null
? 'appSelectorCard.widgetTitle'
: 'appSelectorCard.widgetTitleSelected',
child: const Text(
'',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
I18nText(
locator<PatcherViewModel>().selectedApp == null
? 'appSelectorCard.widgetTitle'
: 'appSelectorCard.widgetTitleSelected',
child: const Text(
'',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 10),
locator<PatcherViewModel>().selectedApp == null
? I18nText('appSelectorCard.widgetSubtitle')
: Row(
children: <Widget>[
SizedBox(
height: 16.0,
child: ClipOval(
child: Image.memory(
locator<PatcherViewModel>().selectedApp == null
? Uint8List(0)
: locator<PatcherViewModel>().selectedApp!.icon,
fit: BoxFit.cover,
),
),
const SizedBox(height: 10),
locator<PatcherViewModel>().selectedApp == null
? I18nText('appSelectorCard.widgetSubtitle')
: Row(
children: <Widget>[
SizedBox(
height: 18.0,
child: ClipOval(
child: Image.memory(
locator<PatcherViewModel>().selectedApp == null
? Uint8List(0)
: locator<PatcherViewModel>().selectedApp!.icon,
fit: BoxFit.cover,
),
),
const SizedBox(width: 4),
Text(_getAppSelection()),
],
),
],
),
),
const SizedBox(width: 6),
Text(locator<PatcherViewModel>().getAppSelectionString()),
],
),
locator<PatcherViewModel>().selectedApp == null
? Container()
: Column(
children: [
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.only(left: 20),
child: Text(
locator<PatcherViewModel>()
.getRecommendedVersionString(context),
style: const TextStyle(fontStyle: FontStyle.italic),
),
),
],
),
],
),
);
}
String _getAppSelection() {
String name = locator<PatcherViewModel>().selectedApp!.name;
String version = locator<PatcherViewModel>().selectedApp!.version;
return '$name (v$version)';
}
}

View File

@ -15,32 +15,30 @@ class PatchSelectorCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
return CustomCard(
onTap: onPressed,
child: CustomCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
I18nText(
locator<PatcherViewModel>().selectedPatches.isEmpty
? 'patchSelectorCard.widgetTitle'
: 'patchSelectorCard.widgetTitleSelected',
child: const Text(
'',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
I18nText(
locator<PatcherViewModel>().selectedPatches.isEmpty
? 'patchSelectorCard.widgetTitle'
: 'patchSelectorCard.widgetTitleSelected',
child: const Text(
'',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 10),
locator<PatcherViewModel>().selectedApp == null
? I18nText('patchSelectorCard.widgetSubtitle')
: locator<PatcherViewModel>().selectedPatches.isEmpty
? I18nText('patchSelectorCard.widgetEmptySubtitle')
: Text(_getPatchesSelection()),
],
),
),
const SizedBox(height: 10),
locator<PatcherViewModel>().selectedApp == null
? I18nText('patchSelectorCard.widgetSubtitle')
: locator<PatcherViewModel>().selectedPatches.isEmpty
? I18nText('patchSelectorCard.widgetEmptySubtitle')
: Text(_getPatchesSelection()),
],
),
);
}

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/ui/widgets/installerView/custom_material_button.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
// ignore: must_be_immutable
@ -39,102 +39,113 @@ class _PatchItemState extends State<PatchItem> {
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: InkWell(
borderRadius: BorderRadius.circular(16),
child: CustomCard(
onTap: () {
setState(() => widget.isSelected = !widget.isSelected);
widget.onChanged(widget.isSelected);
},
child: CustomCard(
child: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Text(
widget.simpleName,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
child: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Text(
widget.simpleName,
maxLines: 2,
overflow: TextOverflow.visible,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
const SizedBox(width: 4),
Text(widget.version)
],
),
const SizedBox(height: 4),
Text(
widget.description,
softWrap: true,
maxLines: 3,
overflow: TextOverflow.visible,
style: const TextStyle(fontSize: 14),
),
],
),
),
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,
),
const SizedBox(width: 4),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 2,
),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.background
.withOpacity(0.5),
borderRadius: BorderRadius.circular(6),
),
child: Text(widget.version),
)
],
),
onChanged: (newValue) {
setState(() => widget.isSelected = newValue!);
widget.onChanged(widget.isSelected);
},
const SizedBox(height: 4),
Text(
widget.description,
softWrap: true,
maxLines: 3,
overflow: TextOverflow.visible,
style: const TextStyle(fontSize: 14),
),
],
),
),
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,
),
)
],
),
widget.isUnsupported
? Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 8),
child: TextButton.icon(
label:
I18nText('patchItem.unsupportedWarningButton'),
icon: const Icon(Icons.warning),
onPressed: () => _showUnsupportedWarningDialog(),
style: ButtonStyle(
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
width: 1,
color:
Theme.of(context).colorScheme.secondary,
),
onChanged: (newValue) {
setState(() => widget.isSelected = newValue!);
widget.onChanged(widget.isSelected);
},
),
)
],
),
widget.isUnsupported
? Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 8),
child: TextButton.icon(
label: I18nText('patchItem.unsupportedWarningButton'),
icon: const Icon(Icons.warning),
onPressed: () => _showUnsupportedWarningDialog(),
style: ButtonStyle(
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
width: 1,
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,
),
),
),
],
)
: Container(),
widget.child ?? const SizedBox(),
],
),
),
],
)
: Container(),
widget.child ?? const SizedBox(),
],
),
),
);
@ -151,7 +162,7 @@ class _PatchItemState extends State<PatchItem> {
translationParams: {
'packageVersion': widget.packageVersion,
'supportedVersions':
'\u2022 ${widget.supportedPackageVersions.join('\n\u2022 ')}',
'\u2022 ${widget.supportedPackageVersions.reversed.join('\n\u2022 ')}',
},
),
actions: <Widget>[

View File

@ -4,7 +4,9 @@ import 'package:revanced_manager/utils/about_info.dart';
import 'package:flutter/services.dart';
class AboutWidget extends StatefulWidget {
const AboutWidget({Key? key}) : super(key: key);
const AboutWidget({Key? key, this.padding}) : super(key: key);
final EdgeInsetsGeometry? padding;
@override
State<AboutWidget> createState() => _AboutWidgetState();
@ -13,28 +15,15 @@ class AboutWidget extends StatefulWidget {
class _AboutWidgetState extends State<AboutWidget> {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
I18nText(
'settingsView.aboutLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(height: 4),
FutureBuilder<Map<String, dynamic>>(
future: AboutInfo.getInfo(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return GestureDetector(
onLongPress: () {
return FutureBuilder<Map<String, dynamic>>(
future: AboutInfo.getInfo(),
builder: (context, snapshot) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: ListTile(
contentPadding: widget.padding ?? EdgeInsets.zero,
onLongPress: snapshot.hasData
? () {
Clipboard.setData(
ClipboardData(
text: 'Version: ${snapshot.data!['version']}\n'
@ -50,8 +39,20 @@ class _AboutWidgetState extends State<AboutWidget> {
Theme.of(context).colorScheme.secondary,
),
);
},
child: Column(
}
: null,
title: I18nText(
'settingsView.aboutLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: snapshot.hasData
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
@ -90,15 +91,11 @@ class _AboutWidgetState extends State<AboutWidget> {
),
),
],
),
);
} else {
return Container();
}
},
)
: const SizedBox(),
),
],
),
);
},
);
}
}

View File

@ -6,6 +6,7 @@ class CustomSwitchTile extends StatelessWidget {
final Widget subtitle;
final bool value;
final Function(bool) onTap;
final EdgeInsetsGeometry? padding;
const CustomSwitchTile({
Key? key,
@ -13,14 +14,16 @@ class CustomSwitchTile extends StatelessWidget {
required this.subtitle,
required this.value,
required this.onTap,
this.padding,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListTile(
contentPadding: EdgeInsets.zero,
contentPadding: padding ?? EdgeInsets.zero,
title: title,
subtitle: subtitle,
onTap: () => onTap(!value),
trailing: CustomSwitch(
value: value,
onChanged: onTap,

View File

@ -17,7 +17,7 @@ class SettingsSection extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
padding: const EdgeInsets.only(top: 16.0, bottom: 10.0),
padding: const EdgeInsets.only(top: 16.0, bottom: 10.0, left: 20.0),
child: I18nText(
title,
child: Text(

View File

@ -5,18 +5,20 @@ class SettingsTileDialog extends StatelessWidget {
final String title;
final String subtitle;
final Function()? onTap;
final EdgeInsetsGeometry? padding;
const SettingsTileDialog({
Key? key,
required this.title,
required this.subtitle,
required this.onTap,
this.padding,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListTile(
contentPadding: EdgeInsets.zero,
contentPadding: padding ?? EdgeInsets.zero,
title: I18nText(
title,
child: const Text(

View File

@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
class SocialMediaItem extends StatelessWidget {
final Widget? icon;
final Widget title;
final Widget? subtitle;
final String? url;
const SocialMediaItem({
Key? key,
this.icon,
required this.title,
this.subtitle,
this.url,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListTile(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.0)),
contentPadding: EdgeInsets.zero,
leading: SizedBox(
width: 48.0,
child: Center(
child: icon,
),
),
title: DefaultTextStyle(
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
child: title,
),
subtitle: subtitle != null
? DefaultTextStyle(
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.primary,
),
child: subtitle!,
)
: null,
onTap: () => url != null
? launchUrl(
Uri.parse(url!),
mode: LaunchMode.externalApplication,
)
: null,
);
}
}

View File

@ -2,11 +2,16 @@ import 'package:expandable/expandable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:revanced_manager/ui/widgets/settingsView/social_media_item.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
import 'package:url_launcher/url_launcher.dart';
class SocialMediaWidget extends StatelessWidget {
const SocialMediaWidget({Key? key}) : super(key: key);
final EdgeInsetsGeometry? padding;
const SocialMediaWidget({
Key? key,
this.padding,
}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -14,11 +19,13 @@ class SocialMediaWidget extends StatelessWidget {
theme: ExpandableThemeData(
hasIcon: true,
iconColor: Theme.of(context).iconTheme.color,
iconPadding: const EdgeInsets.symmetric(vertical: 16.0),
iconPadding: const EdgeInsets.symmetric(vertical: 16.0)
.add(padding ?? EdgeInsets.zero)
.resolve(Directionality.of(context)),
animationDuration: const Duration(milliseconds: 400),
),
header: ListTile(
contentPadding: EdgeInsets.zero,
contentPadding: padding ?? EdgeInsets.zero,
title: I18nText(
'socialMediaCard.widgetTitle',
child: const Text(
@ -31,169 +38,52 @@ class SocialMediaWidget extends StatelessWidget {
),
subtitle: I18nText('socialMediaCard.widgetSubtitle'),
),
expanded: CustomCard(
child: Column(
children: <Widget>[
ListTile(
contentPadding: EdgeInsets.zero,
leading: Padding(
padding: const EdgeInsets.all(8.0),
child: FaIcon(
FontAwesomeIcons.github,
color: Theme.of(context).colorScheme.secondary,
),
expanded: Padding(
padding: padding ?? EdgeInsets.zero,
child: CustomCard(
child: Column(
children: const <Widget>[
SocialMediaItem(
icon: FaIcon(FontAwesomeIcons.github),
title: Text('GitHub'),
subtitle: Text('github.com/revanced'),
url: 'https://github.com/revanced',
),
title: Text(
'GitHub',
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
),
SocialMediaItem(
icon: FaIcon(FontAwesomeIcons.discord),
title: Text('Discord'),
subtitle: Text('discord.gg/revanced'),
url: 'https://discord.gg/rF2YcEjcrT',
),
subtitle: Text(
'github.com/revanced',
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
),
SocialMediaItem(
icon: FaIcon(FontAwesomeIcons.telegram),
title: Text('Telegram'),
subtitle: Text('t.me/app_revanced'),
url: 'https://t.me/app_revanced',
),
onTap: () => launchUrl(
Uri.parse('https://github.com/revanced'),
mode: LaunchMode.externalApplication,
SocialMediaItem(
icon: FaIcon(FontAwesomeIcons.reddit),
title: Text('Reddit'),
subtitle: Text('r/revancedapp'),
url: 'https://reddit.com/r/revancedapp',
),
),
ListTile(
contentPadding: EdgeInsets.zero,
leading: Padding(
padding: const EdgeInsets.all(8.0).copyWith(left: 5),
child: FaIcon(
FontAwesomeIcons.discord,
color: Theme.of(context).colorScheme.secondary,
),
SocialMediaItem(
icon: FaIcon(FontAwesomeIcons.twitter),
title: Text('Twitter'),
subtitle: Text('@revancedapp'),
url: 'https://twitter.com/revancedapp',
),
title: Text(
'Discord',
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
),
SocialMediaItem(
icon: FaIcon(FontAwesomeIcons.youtube),
title: Text('YouTube'),
subtitle: Text('youtube.com/revanced'),
url: 'https://youtube.com/revanced',
),
subtitle: Text(
'discord.gg/revanced',
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
),
),
onTap: () => launchUrl(
Uri.parse('https://discord.gg/rF2YcEjcrT'),
mode: LaunchMode.externalApplication,
),
),
ListTile(
contentPadding: EdgeInsets.zero,
leading: Padding(
padding: const EdgeInsets.all(8.0),
child: FaIcon(
FontAwesomeIcons.telegram,
color: Theme.of(context).colorScheme.secondary,
),
),
title: Text(
'Telegram',
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
),
),
subtitle: Text(
't.me/app_revanced',
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
),
),
onTap: () => launchUrl(
Uri.parse('https://t.me/app_revanced'),
mode: LaunchMode.externalApplication,
),
),
ListTile(
contentPadding: EdgeInsets.zero,
leading: Padding(
padding: const EdgeInsets.all(8.0),
child: FaIcon(
FontAwesomeIcons.reddit,
color: Theme.of(context).colorScheme.secondary,
),
),
title: Text(
'Reddit',
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
),
),
subtitle: Text(
'r/revancedapp',
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
),
),
onTap: () => launchUrl(
Uri.parse('https://reddit.com/r/revancedapp'),
mode: LaunchMode.externalApplication,
),
),
ListTile(
contentPadding: EdgeInsets.zero,
leading: Padding(
padding: const EdgeInsets.all(8.0),
child: FaIcon(
FontAwesomeIcons.twitter,
color: Theme.of(context).colorScheme.secondary,
),
),
title: Text(
'Twitter',
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
),
),
subtitle: Text(
'@revancedapp',
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
),
),
onTap: () => launchUrl(
Uri.parse('https://twitter.com/revancedapp'),
mode: LaunchMode.externalApplication,
),
),
ListTile(
contentPadding: EdgeInsets.zero,
leading: Padding(
padding: const EdgeInsets.all(8.0),
child: FaIcon(
FontAwesomeIcons.youtube,
color: Theme.of(context).colorScheme.secondary,
),
),
title: Text(
'YouTube',
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
),
),
subtitle: Text(
'youtube.com/revanced',
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
),
),
onTap: () => launchUrl(
Uri.parse('https://youtube.com/revanced'),
mode: LaunchMode.externalApplication,
),
),
],
],
),
),
),
collapsed: Container(),
collapsed: const SizedBox(),
);
}
}

View File

@ -1,7 +1,7 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/ui/widgets/installerView/custom_material_button.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
import 'package:expandable/expandable.dart';
import 'package:timeago/timeago.dart';
@ -60,67 +60,68 @@ class _ApplicationItemState extends State<ApplicationItem>
hasIcon: false,
animationDuration: Duration(milliseconds: 450),
),
header: CustomCard(
child: Row(
children: <Widget>[
SizedBox(
width: 40,
child: Image.memory(widget.icon, height: 40, width: 40),
),
const SizedBox(width: 4),
Padding(
padding: const EdgeInsets.only(left: 15.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
widget.name.length > 10
? '${widget.name.substring(0, 10)}...'
: widget.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
Text(format(widget.patchDate)),
],
header: Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: CustomCard(
onTap: () {
expController.toggle();
_animationController.isCompleted
? _animationController.reverse()
: _animationController.forward();
},
child: Row(
children: <Widget>[
SizedBox(
width: 40,
child: Image.memory(widget.icon, height: 40, width: 40),
),
),
const Spacer(),
Padding(
padding: const EdgeInsets.only(right: 5.0),
child: RotationTransition(
const SizedBox(width: 4),
Padding(
padding: const EdgeInsets.only(left: 15.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
widget.name.length > 9
? '${widget.name.substring(0, 9)}...'
: widget.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
Text(format(widget.patchDate)),
],
),
),
const Spacer(),
RotationTransition(
turns:
Tween(begin: 0.0, end: 0.50).animate(_animationController),
child: IconButton(
onPressed: () {
expController.toggle();
_animationController.isCompleted
? _animationController.reverse()
: _animationController.forward();
},
icon: const Icon(Icons.arrow_drop_down),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.arrow_drop_down),
),
),
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
CustomMaterialButton(
label: widget.isUpdatableApp
? I18nText('applicationItem.patchButton')
: I18nText('applicationItem.infoButton'),
onPressed: widget.onPressed,
),
],
),
],
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
CustomMaterialButton(
label: widget.isUpdatableApp
? I18nText('applicationItem.patchButton')
: I18nText('applicationItem.infoButton'),
onPressed: widget.onPressed,
),
],
),
],
),
),
),
collapsed: const Text(''),
collapsed: const SizedBox(),
expanded: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0),
padding: const EdgeInsets.all(16.0).copyWith(top: 0.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[

View File

@ -3,30 +3,33 @@ import 'package:flutter/material.dart';
class CustomCard extends StatelessWidget {
final bool isFilled;
final Widget child;
final Function()? onTap;
final EdgeInsetsGeometry? padding;
const CustomCard({
Key? key,
this.isFilled = true,
required this.child,
this.onTap,
this.padding,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
return Material(
type: isFilled ? MaterialType.card : MaterialType.transparency,
color: isFilled
? Theme.of(context).colorScheme.secondaryContainer.withOpacity(0.4)
: Colors.transparent,
borderRadius: BorderRadius.circular(16),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(16),
color: isFilled
? Theme.of(context).colorScheme.secondaryContainer.withOpacity(0.40)
: Colors.transparent,
border: isFilled
? null
: Border.all(
width: 1,
color: Theme.of(context).colorScheme.secondary,
),
child: Padding(
padding: padding ?? const EdgeInsets.all(20.0),
child: child,
),
),
padding: const EdgeInsets.all(20),
child: child,
);
}
}

View File

@ -0,0 +1,130 @@
import 'package:flutter/material.dart';
class CustomMaterialButton extends StatelessWidget {
final Widget label;
final bool isFilled;
final bool isExpanded;
final Function()? onPressed;
const CustomMaterialButton({
Key? key,
required this.label,
this.isFilled = true,
this.isExpanded = false,
required this.onPressed,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return TextButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(
isExpanded
? const EdgeInsets.symmetric(horizontal: 24, vertical: 12)
: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
shape: MaterialStateProperty.all(
StadiumBorder(
side: isFilled
? BorderSide.none
: BorderSide(
width: 1,
color: Theme.of(context).colorScheme.primary,
),
),
),
backgroundColor: MaterialStateProperty.all(
isFilled ? Theme.of(context).colorScheme.primary : Colors.transparent,
),
foregroundColor: MaterialStateProperty.all(
isFilled
? Theme.of(context).colorScheme.surface
: Theme.of(context).colorScheme.primary,
),
),
onPressed: onPressed,
child: label,
);
}
}
// ignore: must_be_immutable
class TimerButton extends StatefulWidget {
Widget label;
bool isFilled;
int seconds;
final bool isRunning;
final Function()? onTimerEnd;
TimerButton({
Key? key,
required this.seconds,
required this.isRunning,
required this.onTimerEnd,
this.label = const Text(''),
this.isFilled = true,
}) : super(key: key);
@override
State<TimerButton> createState() => _TimerButtonState();
}
class _TimerButtonState extends State<TimerButton> {
void timer(int seconds) {
Future.delayed(const Duration(seconds: 1), () {
if (seconds > 0) {
setState(() {
seconds--;
});
timer(seconds);
} else {
widget.onTimerEnd!();
}
});
}
@override
void initState() {
//decrement seconds
if (widget.isRunning) {
timer(widget.seconds);
}
super.initState();
}
@override
Widget build(BuildContext build) {
return TextButton(
style: ButtonStyle(
shape: MaterialStateProperty.all(
StadiumBorder(
side: widget.isFilled
? BorderSide.none
: BorderSide(
width: 1,
color: Theme.of(context).colorScheme.primary,
),
),
),
backgroundColor: MaterialStateProperty.all(
widget.isFilled
? Theme.of(context).colorScheme.primary
: Colors.transparent,
),
foregroundColor: MaterialStateProperty.all(
widget.isFilled
? Theme.of(context).colorScheme.surface
: Theme.of(context).colorScheme.primary,
),
),
onPressed: widget.isRunning ? null : widget.onTimerEnd,
child: Text(
widget.isRunning ? '${widget.seconds}' : 'Install',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
);
}
}

View File

@ -4,12 +4,16 @@ class CustomSliverAppBar extends StatelessWidget {
final Widget title;
final List<Widget>? actions;
final PreferredSizeWidget? bottom;
final bool isMainView;
final Function()? onBackButtonPressed;
const CustomSliverAppBar({
Key? key,
required this.title,
this.actions,
this.bottom,
this.isMainView = false,
this.onBackButtonPressed,
}) : super(key: key);
@override
@ -19,16 +23,29 @@ class CustomSliverAppBar extends StatelessWidget {
snap: false,
floating: false,
expandedHeight: 100.0,
automaticallyImplyLeading: false,
automaticallyImplyLeading: !isMainView,
flexibleSpace: FlexibleSpaceBar(
titlePadding: EdgeInsets.only(
bottom: bottom != null ? 16.0 : 14.0,
left: isMainView ? 20.0 : 55.0,
),
title: title,
),
leading: isMainView
? null
: IconButton(
icon: Icon(
Icons.arrow_back,
color: Theme.of(context).textTheme.headline6!.color,
),
onPressed:
onBackButtonPressed ?? () => Navigator.of(context).pop(),
),
backgroundColor: MaterialStateColor.resolveWith(
(states) => states.contains(MaterialState.scrolledUnder)
? Theme.of(context).colorScheme.surface
: Theme.of(context).canvasColor,
),
flexibleSpace: FlexibleSpaceBar(
titlePadding: const EdgeInsets.only(bottom: 16.0, left: 20.0),
title: title,
),
actions: actions,
bottom: bottom,
);

View File

@ -21,7 +21,7 @@ class SearchBar extends StatefulWidget {
class _SearchBarState extends State<SearchBar> {
final TextEditingController _textController = TextEditingController();
bool _toggleSelectAll = true;
bool _toggleSelectAll = false;
@override
Widget build(BuildContext context) {

View File

@ -4,7 +4,7 @@ homepage: https://github.com/revanced/revanced-manager
publish_to: 'none'
version: 0.0.9+9
version: 0.0.23+23
environment:
sdk: ">=2.17.5 <3.0.0"
@ -20,6 +20,7 @@ dependencies:
ref: revanced-manager
device_info_plus: ^4.1.2
dio: ^4.0.6
dio_http2_adapter: ^2.0.0
dio_http_cache_lts: ^0.4.1
dynamic_color: ^1.5.4
dynamic_themes: ^1.1.0
@ -52,9 +53,12 @@ dependencies:
ref: feature/nullSafe
package_info_plus: ^1.4.3+1
path_provider: ^2.0.11
#permission_handler: ^10.0.0
permission_handler: ^10.0.0
pull_to_refresh: ^2.0.0
root: ^2.0.2
root:
git:
url: https://github.com/gokul1630/root
ref: main
share_extend: ^2.0.0
shared_preferences: ^2.0.15
skeletons: ^0.0.3
@ -63,6 +67,7 @@ dependencies:
stacked_services: ^0.9.3
stacked_themes: ^0.3.9
timeago: ^3.2.2
timezone: ^0.8.0
url_launcher: ^6.1.5
wakelock: ^0.6.2
@ -75,13 +80,6 @@ dev_dependencies:
injectable_generator: ^1.5.4
json_serializable: ^6.3.1
flutter_icons:
android: true
ios: false
image_path: "assets/images/ic_launcher_round.png"
adaptive_icon_background: "#1B1B1B"
adaptive_icon_foreground: "assets/images/ic_launcher.png"
flutter:
uses-material-design: true
assets:

View File

@ -1,30 +0,0 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:revanced_manager/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}