Compare commits

...

98 Commits

Author SHA1 Message Date
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
6bd218277d build: Bump version to v0.0.9 2022-09-17 19:40:44 +01:00
83ad7605c4 fix: Minor improvements on App Info view 2022-09-17 19:30:04 +01:00
5fbc8ff7a0 feat: Retrieve unsaved apps from manager's root folder and known packages from user's apps list 2022-09-17 19:29:46 +01:00
bb4b59eee6 Disable permission_handler package for now 2022-09-17 17:42:29 +01:00
4cfb620d63 build: Bump version to v0.0.8. 2022-09-17 20:58:19 +05:30
55739a9c78 fix: background should be more opaque. 2022-09-17 20:56:44 +05:30
482599bb4e fix: Do not import dart:html 2022-09-17 14:59:23 +01:00
3aaf49fee0 fix: Only init foreground service if user allowed IGNORE_BATTERY_OPTIMIZATIONS permission 2022-09-17 14:55:00 +01:00
0424ee235c build: Bump patcher dependency version 2022-09-17 14:51:09 +01:00
a178afce99 fix: Fix typo on menu options visibility 2022-09-17 14:47:03 +01:00
ef3685c817 fix: Add Unpatch option on App Info view 2022-09-17 14:45:43 +01:00
e74fce8574 Merge branch 'flutter' of github.com:revanced/revanced-manager into flutter 2022-09-17 14:42:58 +01:00
73e4ae1416 build: restrict manager to android 8. 2022-09-17 16:14:24 +03:00
bcf3b36b13 feat: tweak UI 2022-09-17 16:13:41 +03:00
9c3626c8ed fix: Request permissions a single time at boot 2022-09-16 17:29:32 +01:00
a3dca8c142 fix: Add build mode to about widget 2022-09-16 16:39:57 +01:00
ef0c59f693 fix: Surround isRooted with a try/catch just to be safe 2022-09-16 16:39:37 +01:00
31756884b6 build: Bump version to v0.0.7. 2022-09-16 03:48:23 +05:30
0a2a495ab0 feat: custom animated progressbar. 2022-09-16 03:46:46 +05:30
8300cc4071 feat: dropdown for changelogs. 2022-09-16 00:49:11 +05:30
9f58757caf feat: Apply dark/light mode to system navigation bar too 2022-09-15 13:27:48 +01:00
5d296038b7 fix: Increase API's maxAge to 1 day 2022-09-15 12:33:19 +01:00
9f85450c6c build: Bump version to v0.0.6 2022-09-15 09:14:43 +01:00
68 changed files with 2110 additions and 1317 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

@ -44,7 +44,7 @@ android {
defaultConfig {
applicationId "app.revanced.manager.flutter"
minSdkVersion 21
minSdkVersion 26
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
@ -71,7 +71,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// ReVanced
implementation "app.revanced:revanced-patcher:4.4.0"
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"

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,37 +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"
"doneButton": "Done",
"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",
@ -85,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",
@ -102,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"
},
@ -120,8 +137,8 @@
"openButton": "Open",
"uninstallButton": "Uninstall",
"patchButton": "Patch",
"uninstallDialogTitle": "Uninstall",
"uninstallDialogText": "Are you sure you want to uninstall this app?",
"unpatchButton": "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.",
"packageNameLabel": "Package Name",

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

@ -9,11 +9,10 @@ import 'package:revanced_manager/models/patch.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'));
final DioCacheManager _dioCacheManager = DioCacheManager(CacheConfig());
final Options _cacheOptions = buildCacheOptions(
const Duration(hours: 1),
const Duration(days: 1),
maxStale: const Duration(days: 7),
);
final Map<String, String> repoAppPath = {
@ -37,7 +36,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 +54,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);
}
}
@ -165,46 +182,116 @@ class ManagerAPI {
return packageInfo.version;
}
Future<void> reAssessSavedApps() async {
List<PatchedApplication> patchedApps = getPatchedApps();
Future<List<PatchedApplication>> getAppsToRemove(
List<PatchedApplication> patchedApps,
) async {
List<PatchedApplication> toRemove = [];
for (PatchedApplication app in patchedApps) {
bool isRemove = await isAppUninstalled(app);
if (isRemove) {
toRemove.add(app);
} else {
app.hasUpdates = await hasAppUpdates(app.packageName, app.patchDate);
app.changelog = await getAppChangelog(app.packageName, app.patchDate);
if (!app.hasUpdates) {
String? currentInstalledVersion =
(await DeviceApps.getApp(app.packageName))?.versionName;
if (currentInstalledVersion != null) {
String currentSavedVersion = app.version;
int currentInstalledVersionInt = int.parse(
currentInstalledVersion.replaceAll(RegExp('[^0-9]'), ''));
int currentSavedVersionInt =
int.parse(currentSavedVersion.replaceAll(RegExp('[^0-9]'), ''));
if (currentInstalledVersionInt > currentSavedVersionInt) {
app.hasUpdates = true;
}
}
}
return toRemove;
}
Future<List<PatchedApplication>> getUnsavedApps(
List<PatchedApplication> patchedApps,
) async {
List<PatchedApplication> unsavedApps = [];
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,
),
);
}
}
}
}
List<Application> userApps = await DeviceApps.getInstalledApplications(
includeSystemApps: false,
includeAppIcons: false,
);
for (Application app in userApps) {
if (app.packageName.startsWith('app.revanced') &&
!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(
name: application.appName,
packageName: application.packageName,
version: application.versionName!,
apkFilePath: application.apkFilePath,
icon: application.icon,
patchDate: DateTime.now(),
isRooted: false,
),
);
}
}
}
return unsavedApps;
}
Future<void> reAssessSavedApps() async {
List<PatchedApplication> patchedApps = getPatchedApps();
List<PatchedApplication> unsavedApps = await getUnsavedApps(patchedApps);
patchedApps.addAll(unsavedApps);
List<PatchedApplication> toRemove = await getAppsToRemove(patchedApps);
patchedApps.removeWhere((a) => toRemove.contains(a));
for (PatchedApplication app in patchedApps) {
app.hasUpdates = await hasAppUpdates(app.packageName, app.patchDate);
app.changelog = await getAppChangelog(app.packageName, app.patchDate);
if (!app.hasUpdates) {
String? currentInstalledVersion =
(await DeviceApps.getApp(app.packageName))?.versionName;
if (currentInstalledVersion != null) {
String currentSavedVersion = app.version;
int currentInstalledVersionInt = int.parse(
currentInstalledVersion.replaceAll(RegExp('[^0-9]'), ''));
int currentSavedVersionInt =
int.parse(currentSavedVersion.replaceAll(RegExp('[^0-9]'), ''));
if (currentInstalledVersionInt > currentSavedVersionInt) {
app.hasUpdates = true;
}
}
}
}
await setPatchedApps(patchedApps);
}
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 {

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();
}
@ -84,19 +89,25 @@ class PatcherAPI {
Future<bool> needsIntegrations(List<Patch> selectedPatches) async {
return selectedPatches.any(
(patch) => patch.dependencies.contains('integrations'),
(patch) => patch.dependencies.any(
(dep) => dep.contains('integrations'),
),
);
}
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 +210,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 +230,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

@ -9,15 +9,15 @@ import 'package:timeago/timeago.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(hours: 1),
const Duration(days: 1),
maxStale: const Duration(days: 7),
);
void initialize() {
Future<void> initialize(String apiUrl) async {
_dio = Dio(BaseOptions(baseUrl: apiUrl));
_dio.interceptors.add(_dioCacheManager.interceptor);
}
@ -28,10 +28,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 +42,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 +55,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 +68,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

@ -6,8 +6,16 @@ class RootAPI {
final String _serviceDDirPath = '/data/adb/service.d';
Future<bool> hasRootPermissions() async {
bool? isRooted = await Root.isRooted();
return isRooted != null && isRooted;
try {
bool? isRooted = await Root.isRootAvailable();
if (isRooted != null && isRooted) {
isRooted = await Root.isRooted();
return isRooted != null && isRooted;
}
return false;
} on Exception {
return false;
}
}
Future<void> setPermissions(
@ -55,15 +63,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

@ -4,6 +4,7 @@ import 'package:google_fonts/google_fonts.dart';
var lightCustomColorScheme = ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.light,
primary: const Color(0xff1B73E8),
);
var lightCustomTheme = ThemeData(
@ -23,8 +24,8 @@ var lightCustomTheme = ThemeData(
var darkCustomColorScheme = ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.dark,
primary: const Color(0xff7792BA),
surface: const Color(0xff0A0D11),
primary: const Color(0xffA5CAFF),
surface: const Color(0xff1B1A1D),
);
var darkCustomTheme = ThemeData(
@ -38,8 +39,8 @@ var darkCustomTheme = ThemeData(
),
),
),
canvasColor: const Color(0xff0A0D11),
scaffoldBackgroundColor: const Color(0xff0A0D11),
toggleableActiveColor: const Color(0xff7792BA),
canvasColor: const Color(0xff1B1A1D),
scaffoldBackgroundColor: const Color(0xff1B1A1D),
toggleableActiveColor: const Color(0xffA5CAFF),
textTheme: GoogleFonts.robotoTextTheme(ThemeData.dark().textTheme),
);

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,8 @@ 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';
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
@ -28,9 +29,10 @@ class InstallerView extends StatelessWidget {
color: Theme.of(context).textTheme.headline6!.color,
),
),
onBackButtonPressed: () => model.onWillPop(context),
actions: <Widget>[
Visibility(
visible: !model.isPatching && model.hasErrors,
visible: !model.isPatching && !model.hasErrors,
child: CustomPopupMenu(
onSelected: (value) => model.onMenuSelection(value),
children: {
@ -57,14 +59,9 @@ class InstallerView extends StatelessWidget {
),
],
bottom: PreferredSize(
preferredSize: const Size(double.infinity, 1.0),
child: LinearProgressIndicator(
color: Theme.of(context).colorScheme.primary,
backgroundColor:
Theme.of(context).colorScheme.primaryContainer,
value: model.progress,
),
),
preferredSize: const Size(double.infinity, 1.0),
child:
GradientProgressIndicator(progress: model.progress!)),
),
SliverPadding(
padding: const EdgeInsets.all(20.0),
@ -115,7 +112,10 @@ class InstallerView extends StatelessWidget {
label:
I18nText('installerView.installRootButton'),
isExpanded: true,
onPressed: () => model.installResult(true),
onPressed: () => model.installResult(
context,
true,
),
),
),
Visibility(
@ -129,7 +129,10 @@ class InstallerView extends StatelessWidget {
child: CustomMaterialButton(
label: I18nText('installerView.installButton'),
isExpanded: true,
onPressed: () => model.installResult(false),
onPressed: () => model.installResult(
context,
false,
),
),
),
],
@ -141,13 +144,7 @@ class InstallerView extends StatelessWidget {
],
),
),
onWillPop: () async {
if (!model.isPatching) {
model.cleanPatcher();
Navigator.of(context).pop();
}
return false;
},
onWillPop: () => model.onWillPop(context),
),
);
}

View File

@ -3,18 +3,22 @@ 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: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/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 Toast _toast = locator<Toast>();
final PatchedApplication _app = locator<PatcherViewModel>().selectedApp!;
final List<Patch> _patches = locator<PatcherViewModel>().selectedPatches;
static const _installerChannel = MethodChannel(
@ -29,29 +33,30 @@ class InstallerViewModel extends BaseViewModel {
bool hasErrors = false;
Future<void> initialize(BuildContext context) async {
try {
await FlutterBackground.initialize(
androidConfig: FlutterBackgroundAndroidConfig(
notificationTitle: FlutterI18n.translate(
context,
'installerView.notificationTitle',
if (await Permission.ignoreBatteryOptimizations.isGranted) {
try {
FlutterBackground.initialize(
androidConfig: FlutterBackgroundAndroidConfig(
notificationTitle: FlutterI18n.translate(
context,
'installerView.notificationTitle',
),
notificationText: FlutterI18n.translate(
context,
'installerView.notificationText',
),
notificationImportance: AndroidNotificationImportance.Default,
notificationIcon: const AndroidResource(
name: 'ic_notification',
defType: 'drawable',
),
),
notificationText: FlutterI18n.translate(
context,
'installerView.notificationText',
),
notificationImportance: AndroidNotificationImportance.Default,
notificationIcon: const AndroidResource(
name: 'ic_notification',
defType: 'drawable',
),
),
);
await FlutterBackground.enableBackgroundExecution();
await Wakelock.enable();
} on Exception {
// ignore
).then((value) => FlutterBackground.enableBackgroundExecution());
} on Exception {
// ignore
}
}
await Wakelock.enable();
await handlePlatformChannelMethods();
await runPatcher();
}
@ -73,14 +78,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;
@ -90,6 +101,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,
@ -112,44 +126,76 @@ class InstallerViewModel extends BaseViewModel {
_patches,
);
} catch (e) {
hasErrors = true;
update(-1.0, 'Aborting...', 'An error occurred! Aborting\nError: $e');
}
} else {
hasErrors = true;
update(-1.0, 'Aborting...', 'No app or patches selected! Aborting');
}
try {
await FlutterBackground.disableBackgroundExecution();
await Wakelock.disable();
} on Exception {
// ignore
}
isPatching = false;
}
void installResult(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.',
update(
-100.0,
'Aborting...',
'An error occurred! Aborting\nError:\n$e',
);
}
await _managerAPI.savePatchedApp(_app);
} else {
update(-100.0, 'Aborting...', 'No app or patches selected! Aborting');
}
if (FlutterBackground.isBackgroundExecutionEnabled) {
try {
FlutterBackground.disableBackgroundExecution();
} on Exception {
// ignore
}
}
await Wakelock.disable();
}
void installResult(BuildContext context, bool installAsRoot) async {
_app.isRooted = installAsRoot;
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);
}
}
}
@ -182,4 +228,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

@ -42,6 +42,7 @@ class NavigationView extends StatelessWidget {
context,
'navigationView.dashboardTab',
),
tooltip: '',
),
NavigationDestination(
icon: model.isIndexSelected(1)
@ -51,6 +52,7 @@ class NavigationView extends StatelessWidget {
context,
'navigationView.patcherTab',
),
tooltip: '',
),
NavigationDestination(
icon: model.isIndexSelected(2)
@ -60,6 +62,7 @@ class NavigationView extends StatelessWidget {
context,
'navigationView.settingsTab',
),
tooltip: '',
),
],
),

View File

@ -1,9 +1,12 @@
// ignore_for_file: use_build_context_synchronously
import 'package:dynamic_themes/dynamic_themes.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:injectable/injectable.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';
@ -13,19 +16,32 @@ 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('useDarkTheme') == null) {
if (MediaQuery.of(context).platformBrightness == Brightness.light) {
await prefs.setBool('useDarkTheme', false);
DynamicTheme.of(context)!.setTheme(0);
} else {
await prefs.setBool('useDarkTheme', true);
DynamicTheme.of(context)!.setTheme(1);
}
if (prefs.getBool('permissionsRequested') == null) {
await prefs.setBool('permissionsRequested', true);
RootAPI().hasRootPermissions().then(
(value) => Permission.requestInstallPackages.request().then(
(value) => Permission.ignoreBatteryOptimizations.request(),
),
);
}
RootAPI().hasRootPermissions();
Permission.requestInstallPackages.request();
Permission.ignoreBatteryOptimizations.request();
if (prefs.getBool('useDarkTheme') == null) {
bool isDark =
MediaQuery.of(context).platformBrightness != Brightness.light;
await prefs.setBool('useDarkTheme', isDark);
await DynamicTheme.of(context)!.setTheme(isDark ? 1 : 0);
}
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
systemNavigationBarColor: Colors.transparent,
systemNavigationBarIconBrightness:
DynamicTheme.of(context)!.theme.brightness == Brightness.light
? Brightness.dark
: Brightness.light,
),
);
}
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

@ -7,7 +7,7 @@ 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/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';
@ -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,137 +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
? Center(
child: CircularProgressIndicator(
color: Theme.of(context).colorScheme.primary,
),
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

@ -3,13 +3,14 @@ import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:dynamic_themes/dynamic_themes.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:logcat/logcat.dart';
import 'package: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';
@ -26,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();
@ -50,9 +52,9 @@ class SettingsViewModel extends BaseViewModel {
await _managerAPI.setUseDynamicTheme(value);
int currentTheme = DynamicTheme.of(context)!.themeId;
if (currentTheme.isEven) {
DynamicTheme.of(context)!.setTheme(value ? 2 : 0);
await DynamicTheme.of(context)!.setTheme(value ? 2 : 0);
} else {
DynamicTheme.of(context)!.setTheme(value ? 3 : 1);
await DynamicTheme.of(context)!.setTheme(value ? 3 : 1);
}
notifyListeners();
}
@ -65,10 +67,16 @@ class SettingsViewModel extends BaseViewModel {
await _managerAPI.setUseDarkTheme(value);
int currentTheme = DynamicTheme.of(context)!.themeId;
if (currentTheme < 2) {
DynamicTheme.of(context)!.setTheme(value ? 1 : 0);
await DynamicTheme.of(context)!.setTheme(value ? 1 : 0);
} else {
DynamicTheme.of(context)!.setTheme(value ? 3 : 2);
await DynamicTheme.of(context)!.setTheme(value ? 3 : 2);
}
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
systemNavigationBarIconBrightness:
value ? Brightness.light : Brightness.dark,
),
);
notifyListeners();
}
@ -193,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,
@ -203,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('');
@ -220,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,106 +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,
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,
),
const SizedBox(height: 10),
I18nText(
'appInfoView.openButton',
child: Text(
'',
style: TextStyle(
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,
),
),
),
],
),
),
),
],
),
),
VerticalDivider(
color: Theme.of(context).canvasColor,
),
InkWell(
onTap: () =>
model.showUninstallAlertDialog(context, app),
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,
),
),
),
],
),
),
],
),
],
),
),
),
),
const SizedBox(height: 20),
ListTile(
contentPadding: EdgeInsets.zero,
contentPadding:
const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'appInfoView.packageNameLabel',
child: const Text(
@ -175,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(
@ -192,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(
@ -213,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,16 +20,26 @@ class AppInfoViewModel extends BaseViewModel {
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
final RootAPI _rootAPI = RootAPI();
Future<void> uninstallApp(PatchedApplication app) 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) {
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);
}
}
@ -40,53 +51,60 @@ class AppInfoViewModel extends BaseViewModel {
locator<NavigationViewModel>().setIndex(1);
}
Future<void> showUninstallAlertDialog(
Future<void> showUninstallDialog(
BuildContext context,
PatchedApplication app,
bool onlyUnpatch,
) async {
if (app.isRooted) {
bool hasRootPermissions = await _rootAPI.hasRootPermissions();
if (!hasRootPermissions) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('appInfoView.rootDialogTitle'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('appInfoView.rootDialogText'),
actions: <Widget>[
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () => Navigator.of(context).pop(),
)
],
),
);
}
} else {
bool hasRootPermissions = await _rootAPI.hasRootPermissions();
if (app.isRooted && !hasRootPermissions) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('appInfoView.uninstallDialogTitle'),
title: I18nText('appInfoView.rootDialogTitle'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('appInfoView.uninstallDialogText'),
content: I18nText('appInfoView.rootDialogText'),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('cancelButton'),
onPressed: () => Navigator.of(context).pop(),
),
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () {
uninstallApp(app);
locator<HomeViewModel>().initialize(context);
Navigator.of(context).pop();
Navigator.of(context).pop();
},
onPressed: () => Navigator.of(context).pop(),
)
],
),
);
} else {
if (onlyUnpatch) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText(
'appInfoView.unpatchButton',
),
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 {
@ -71,7 +71,7 @@ class _LatestCommitCardState extends State<LatestCommitCard> {
future: locator<HomeViewModel>().hasManagerUpdates(),
initialData: false,
builder: (context, snapshot) => Opacity(
opacity: snapshot.hasData && snapshot.data! ? 1.0 : 0.5,
opacity: snapshot.hasData && snapshot.data! ? 1.0 : 0.25,
child: CustomMaterialButton(
isExpanded: false,
label: I18nText('latestCommitCard.updateButton'),

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

@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
class GradientProgressIndicator extends StatefulWidget {
final double? progress;
const GradientProgressIndicator({required this.progress, super.key});
@override
State<GradientProgressIndicator> createState() =>
_GradientProgressIndicatorState();
}
class _GradientProgressIndicatorState extends State<GradientProgressIndicator> {
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.centerLeft,
child: AnimatedContainer(
duration: const Duration(milliseconds: 500),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.primary,
Theme.of(context).colorScheme.secondary,
],
),
),
height: 5,
width: MediaQuery.of(context).size.width * widget.progress!,
),
);
}
}

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,98 @@ 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,
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),
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 +147,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(
@ -61,6 +62,13 @@ class _AboutWidgetState extends State<AboutWidget> {
fontWeight: FontWeight.w300,
),
),
Text(
'Build: ${snapshot.data!['flavor']}',
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w300,
),
),
Text(
'Model: ${snapshot.data!['model']}',
style: const TextStyle(
@ -83,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,12 +1,12 @@
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';
class ApplicationItem extends StatelessWidget {
class ApplicationItem extends StatefulWidget {
final Uint8List icon;
final String name;
final DateTime patchDate;
@ -24,53 +24,104 @@ class ApplicationItem extends StatelessWidget {
required this.onPressed,
}) : super(key: key);
@override
State<ApplicationItem> createState() => _ApplicationItemState();
}
class _ApplicationItemState extends State<ApplicationItem>
with TickerProviderStateMixin {
late AnimationController _animationController;
@override
initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
ExpandableController expController = ExpandableController();
return ExpandablePanel(
controller: expController,
theme: const ExpandableThemeData(
inkWellBorderRadius: BorderRadius.all(Radius.circular(16)),
tapBodyToCollapse: false,
tapBodyToExpand: false,
tapHeaderToExpand: false,
hasIcon: false,
animationDuration: Duration(milliseconds: 450),
),
header: CustomCard(
child: Row(
children: <Widget>[
SizedBox(
width: 60,
child: Image.memory(icon, height: 39, width: 39),
),
const SizedBox(width: 4),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
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 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: 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,
),
),
Text(format(patchDate)),
],
),
const Spacer(),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
CustomMaterialButton(
label: isUpdatableApp
? I18nText('applicationItem.patchButton')
: I18nText('applicationItem.infoButton'),
onPressed: onPressed,
),
],
),
],
],
),
],
),
),
),
collapsed: const Text(''),
collapsed: const SizedBox(),
expanded: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
padding: const EdgeInsets.all(16.0).copyWith(top: 0.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
@ -82,7 +133,7 @@ class ApplicationItem extends StatelessWidget {
),
),
const SizedBox(height: 4),
Text('\u2022 ${changelog.join('\n\u2022 ')}'),
Text('\u2022 ${widget.changelog.join('\n\u2022 ')}'),
],
),
),

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
: 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

@ -22,7 +22,7 @@ class DashboardChip extends StatelessWidget {
color: isSelected
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
fontWeight: FontWeight.w500,
),
backgroundColor: Colors.transparent,
selectedColor: Theme.of(context).colorScheme.secondaryContainer,

View File

@ -0,0 +1,129 @@
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,
);
}
}
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: 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

@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:device_info_plus/device_info_plus.dart';
@ -7,6 +8,7 @@ class AboutInfo {
final info = await DeviceInfoPlugin().androidInfo;
return {
'version': packageInfo.version,
'flavor': kReleaseMode ? 'release' : 'debug',
'model': info.model,
'androidVersion': info.version.release,
'arch': info.supportedAbis.first

View File

@ -4,7 +4,7 @@ homepage: https://github.com/revanced/revanced-manager
publish_to: 'none'
version: 0.0.5+5
version: 0.0.21+21
environment:
sdk: ">=2.17.5 <3.0.0"
@ -54,7 +54,10 @@ dependencies:
path_provider: ^2.0.11
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 +66,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 +79,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);
});
}