Compare commits

..

447 Commits

Author SHA1 Message Date
166a3180d3 build: correct version code 2023-08-28 03:20:15 +03:00
3bf4982f23 chore: merge dev to main (#1163) 2023-08-28 02:47:47 +03:00
f4e1cccfac build: bump version to v1.9.4 2023-08-28 01:44:46 +02:00
7911a8f49e fix: properly log messages and progress 2023-08-28 01:44:46 +02:00
64a96fc3ce fix: close before returning 2023-08-28 01:44:46 +02:00
45fae3f0fd build: bump ReVanced Patcher back to v14.2.1
This reverts the previous regression with the dependency to ReVanced Patcher.
2023-08-27 22:35:03 +02:00
e45a7824c1 build: bump version to v1.9.3 2023-08-27 13:27:53 +07:00
5d72c48a76 chore: merge dev to main (#1157) 2023-08-27 13:27:01 +07:00
d6169c6fa2 fix: broken settings page 2023-08-27 11:55:21 +05:45
9df6d52e2d build: bump version to v1.9.2 2023-08-27 05:40:50 +03:00
239de8e923 chore: merge dev to main (#1156) 2023-08-27 05:40:13 +03:00
7d553a87f3 build: revert patcher to v11.0.4 2023-08-27 05:39:24 +03:00
557b42bc56 build: bump version to 1.9.1 2023-08-27 03:58:30 +03:00
8423914748 chore: merge dev to main (#1155) 2023-08-27 03:57:57 +03:00
07dce23794 build: bump patcher to v14.2.0 2023-08-27 03:55:56 +03:00
18fd0552db build: bump version to v1.9.0 2023-08-27 02:23:36 +03:00
d537d48f8e chore: merge dev to main (#1125) 2023-08-27 02:21:48 +03:00
b456512bbb build: bump patcher to v14.1.0 (#1153)
Co-authored-by: aAbed <aabedhkhan@gmail.com>
2023-08-27 02:21:16 +03:00
d9953b1473 fix(installer): use correct elevation level 2023-08-24 19:00:50 +07:00
c6ac898390 fix: universal patches not selectable 2023-08-17 18:25:23 +05:45
43f98cec43 build: make variant more identifiable
Release variant will use the "ReVanced Manager" app name, Debug will use "ReVanced Manager Debug"
2023-08-15 21:31:34 +07:00
63175cdec6 ci(build): create debug variant instead of release 2023-08-15 20:55:11 +07:00
6436a1ec61 build: debug apk come with application suffix
Instead of using the same one as Release, app.revanced.manager.flutter - We add .debug to it, yay, co-installation of two variants!
2023-08-15 20:37:43 +07:00
c400619338 feat: disable changing patches selection by default (#1132)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-08-15 16:05:27 +07:00
dcd5ba41cf build: add mavenlocal to repositories 2023-08-14 03:40:05 +03:00
7525e52fab fix(installer): use correct bg colour for dialog 2023-08-12 15:07:04 +07:00
72fd24e624 fix: use i18n translation for installer page 2023-08-12 15:06:17 +07:00
11d8f9fd30 refactor: apply changes according to Dart
trailing commas
2023-08-12 14:53:54 +07:00
4b0c8cecc8 ci(build): update event trigger
This add ".github/workflows/pr-build.yml" to build on every change to the CI Build to check if there's no error when it got modified.

And also remove "fastlane/**" because that's not related to the ReVanced Manager's code
2023-08-12 13:56:18 +07:00
372ce174c9 fix: overlapping issue in application selection page (#1128) 2023-08-11 19:19:44 +03:00
381daff980 fix: exclude x86 aapt2 binary from release builds (#1126) 2023-08-11 19:19:33 +03:00
94acebbebd docs(fixed): replaced some emoji icons, not rendering on chromium based browsers on windows (#1123) 2023-08-11 10:17:46 +07:00
f90f6e81ee feat: patch apps without internet (#1114) 2023-08-11 08:11:19 +07:00
97e37f304b refactor: migrate MediaQuery properties to InheritedModel (#1115)
Co-authored-by: Ushie <ushiekane@gmail.com>
2023-08-10 15:25:07 +03:00
f5e45ead26 ci(build): don't build on every pull request unrelated to application 2023-08-10 19:14:13 +07:00
14f765f4b4 ci(build): don't cache Gradle
Gradle take up so much space in cache that it wouldn't make sense for us to cache it since we are rapidly hitting the cache limit every time.
2023-08-10 19:12:15 +07:00
b77d46b2c2 feat(installer): redesign utility options (#1062)
Co-authored-by: Ushie <ushiekane@gmail.com>
2023-08-10 19:02:30 +07:00
1442916b20 build: bump version to 1.8.0 2023-08-10 03:01:03 +03:00
c6a5f42d23 chore: merge dev to main (#1120) 2023-08-10 03:00:27 +03:00
580d50eb8d fix: use right label for auto patch 2023-08-10 00:05:47 +03:00
9a66b6e50d fix(root-install): create service and postfs directories if they dont exist (#1116)
Fixes #860
2023-08-09 23:57:09 +03:00
acec064cb7 feat(patcher-view): show notice for removed patches that were used previously (#1107) 2023-08-09 22:32:13 +03:00
ea05d13a1f fix(patches-selector): New tag showing for old patches (#1113) 2023-08-09 22:31:37 +03:00
fd741f2ccf build: bump version to v1.7.0 2023-08-08 03:52:03 +03:00
055c52178f chore: merge dev branch to main branch (#1096) 2023-08-08 03:51:05 +03:00
722a5859a5 fix: pair integrations and patches updates (#1102) 2023-08-08 03:30:48 +03:00
1714c3fa86 feat(patches-selector): show New tag for new patches (#1099) 2023-08-07 22:28:09 +07:00
131df28110 docs(managing): update to reflect current changes
The chip was removed because it was not implemented for too long.
2023-08-07 20:13:23 +07:00
1be284f8d8 feat: switch to the new ReVanced API (#1101) 2023-08-07 17:14:16 +07:00
218e53ae75 refactor(analysis): migrate deprecated rule
deprecated since Dart 2.0, yike
2023-08-06 14:41:15 +07:00
264d8d90c4 refactor: apply changes according to analysis
trailing commas, and sort import alphabetically
2023-08-06 14:39:46 +07:00
0b529c2629 fix(patches-selector): separate all universal patches to the bottom (#1092) 2023-08-06 12:12:01 +07:00
5abcc7191f fix: experimental/universal patches being used even when turned off (#1090)
Co-authored-by: Pun Butrach <pun.butrach@gmail.com>
2023-08-06 12:10:57 +07:00
5346f6e1bf fix(patch-item): hide universal patches if not enabled (#1087) 2023-08-05 20:19:36 +07:00
2f471b3de4 fix: use user friendly strings (#1088) 2023-08-05 20:19:03 +07:00
3cf06efddf build: bump version to v1.6.1 2023-08-04 03:33:19 +03:00
1f4f0a3bb7 build: revert patcher to v11.0.4 2023-08-04 03:32:50 +03:00
c07f8eae9b build: bump version to v1.6.0 2023-08-04 00:54:13 +03:00
374eb3d06d feat: abort patching process at any time (#1072) 2023-08-04 00:40:08 +03:00
fe75b75ddc feat: dpi responsive layout (#361) 2023-08-04 00:38:38 +03:00
d3790bf64b refactor: remove unimplemented features (#1061)
Co-authored-by: Ushie <ushiekane@gmail.com>
2023-08-03 23:58:50 +03:00
f905a52988 feat: allow control over patches update (#1063)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-08-03 23:23:56 +03:00
e55f427b05 build: bump patcher version to v12.1.1 2023-08-04 01:53:19 +05:45
0f000fc4d1 build: bump version to v1.5.2 2023-08-04 01:52:24 +05:45
a0339e3c19 chore: merge dev to main (#1077) 2023-08-03 05:24:25 +03:00
9cfa274d81 build: bump version to v1.5.1 2023-08-03 05:23:47 +03:00
8c79f5e371 build: bump patcher to v12.1.0 2023-08-03 05:23:16 +03:00
0d716513d7 fix: keystore password dialog showing up before importing (#1068) 2023-08-03 08:36:39 +07:00
08f6724060 chore: merge dev branch to main branch (#1060) 2023-08-03 02:20:24 +03:00
29daf51e64 build: bump version to 2023-08-03 02:19:14 +03:00
b3b2b01c0f ci(build): use pr event trigger for now
I just heard of cache eviction policy :)
2023-08-01 20:20:27 +07:00
c3212d0308 ci(build): update event trigger
Run on every PR

Run on every Push, ignore main branch, and ignore tag
2023-08-01 19:32:52 +07:00
aaa114ba13 ci(pr-build): reinstate functionality
PR Build was never use because it have to be manually trigger
2023-08-01 19:26:26 +07:00
8ca6418630 refactor(settings): improve clarity of the warning dialogs (#1058) 2023-07-31 21:53:23 +07:00
95632b7f55 chore(deps): update revanced-patcher to v12.0.0 2023-07-30 11:49:01 +07:00
64744b2abf fix: original.apk not found despite existing (#1052) 2023-07-30 10:35:34 +07:00
096b315701 fix(patch-item): remove redundant patch version completely (#1059)
https://github.com/ReVanced/revanced-patches/pull/2709
2023-07-29 16:21:30 +07:00
c27ca08d3a docs: leftovers from 6495687841524cd82ab65990c9c44d53d377db6e 2023-07-29 16:08:54 +07:00
0011222371 chore:fix incorrect version 2023-07-23 14:25:11 +05:30
29db947e25 fix: using non const value in a const variable 2023-07-23 14:04:35 +05:30
fd43ac7581 chore: merge dev branch to main branch (#1032) 2023-07-23 13:53:25 +05:30
e49c19b3cd feat: support older version of android for reddit client patches 2023-07-23 13:51:36 +05:30
Pun
06f0e59967 chore(deps): update libsu to v5.2.0 2023-07-22 13:21:13 +07:00
Pun
c5fc5ee93b build: speed up compilation time
faster build faster build faster build faster build faster build
2023-07-19 18:30:50 +07:00
159c85bd1f fix: close previous dialog when user reset the API URL (#1025) 2023-07-16 21:04:35 +07:00
Pun
2460acf0f4 ci(analyze): don't run when PR is in draft 2023-07-16 17:10:29 +07:00
Pun
6495687841 docs: correct misspelling (EN_US)
This correct spelling to English American, because by default, we use English US.
2023-07-16 16:31:08 +07:00
Pun
d229ccb36c ci(analyze): clarify job name that the job do static analysis & format checking 2023-07-16 16:21:16 +07:00
Pun
4d6a57ddcf ci(analyze): restore run when commit push to dev branch 2023-07-16 16:17:15 +07:00
d161d55aaf fix: patched applications not showing at launch (#1031) 2023-07-16 12:46:01 +07:00
Pun
768ad0c9bc chore: merge dev branch to main branch (#1011)
ReVanced Manager v1.4.1
2023-07-15 20:33:30 +07:00
29323d4e20 build: update app version to v1.4.1 2023-07-15 12:55:59 +07:00
630b22e193 refactor: applies changes from formatter & linter 2023-07-15 12:41:03 +07:00
79116f9e67 fix(patched-applications): non-patched app showing on Installed section (#1022) 2023-07-15 08:11:51 +07:00
eb58475259 fix: showing Installed when it's actually not (#1021) 2023-07-15 08:10:36 +07:00
a879ac30fb fix: prevent unsupported operation exception (#1018) 2023-07-14 17:55:04 +07:00
c5b0621323 fix(navigation-view): back button closing the app from any page (#1019) 2023-07-13 19:25:48 +05:45
0462815014 fix(app-selector): fix text overflow on small screen (#1017) 2023-07-13 00:16:53 +05:45
e64318c947 fix(app-selector): fix text overflow on small screen 2023-07-13 00:14:47 +05:45
b784482788 chore: update libsu to 5.1.0
This should fixes some issues with root
2023-07-11 19:28:59 +07:00
2834e8b348 fix: patchable apps not showing if none of them is installed (#1009) 2023-07-11 17:11:45 +07:00
b23dfd4289 chore: merge dev branch to main branch (#1010) 2023-07-11 01:24:54 +05:30
217d525cb2 build: update app version to v1.4.0 2023-07-11 01:22:57 +05:30
85b166cbda fix: update hardcoded patch name 2023-07-11 01:21:45 +05:30
9a57f8b858 ci(build): use correct JDK version
There's no reasons to go higher than JDK 11 since we compile the application using JDK 11
2023-07-10 21:42:24 +07:00
Pun
3bfdc932c2 chore: update app description
👍👍 Patch your favourite apps, right on your device.
2023-07-10 21:27:20 +07:00
48c878af21 chore: merge dev branch to main branch (#996) 2023-07-10 16:56:50 +03:00
490a7a58fc build: update app version to v1.3.9
ReVanced Manager, 1.3.9 (10030009)

See PR #996
2023-07-10 20:40:40 +07:00
567b1a3ace refactor: improve code readability according to linter 2023-07-10 19:45:50 +07:00
6a45db8a38 refactor: improve code readability according to formatter 2023-07-10 19:36:50 +07:00
Pun
b272988929 build: always compile with the latest SDK supported by Flutter
`targetSdkVersion` is still the same, we want to test the application first before bumping up to latest.
2023-07-10 18:43:07 +07:00
9828857570 fix: fixed typo 2023-07-10 12:16:51 +05:45
ecb54d8e44 docs: improve clarity of speech in issue template (#1005)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-07-09 23:15:20 +02:00
344717b021 fix: app crash after custom source (#1003) 2023-07-08 23:07:19 +05:45
da6cf585c0 fix: app crash after custom source 2023-07-08 22:26:31 +05:45
9e93177afd feat: show warning dialog when resetting stored patches
This prevent user from accidentally resetting stored patches by showing
them warning dialog.
2023-07-08 11:01:10 +07:00
16318efb01 refactor: seperate translation string by context 2023-07-08 10:45:13 +07:00
e83e13b6d4 refactor: fix more problem with analyser
Missing trailing comma in app_selector_view
2023-07-07 22:21:22 +07:00
7c2c695d88 refactor: apply changes from analyser 2023-07-07 22:15:12 +07:00
c7b227529d chore: disable unnecessary tooltip
Remove tooltip in Navigation View, there's no impact to accessibility.

Tested using: Android TalkBack
2023-07-07 22:11:13 +07:00
14f49e9d30 docs: fix description for next page 2023-07-05 15:43:40 +02:00
1875c4ee73 fix: update button being clickable when offline (#987) 2023-07-05 19:59:53 +07:00
76c68baa1f build: update gradle to v7.6.2 2023-07-01 18:21:49 +07:00
68876a4414 build: bump version to v1.3.8 and patcher dep 2023-07-01 14:33:25 +05:30
26d7e5b60f build: bump version to v1.3.7 and patcher dep 2023-07-01 02:46:43 +03:00
e78af6ae99 chore: merge dev to main (#976) 2023-07-01 02:41:57 +03:00
6fe05cd86e feat: show all the unseen changelogs on changelog section (#970) 2023-07-01 02:41:03 +03:00
4100d7a391 fix: different message when trying to patch spilt apk (#973) 2023-07-01 02:40:30 +03:00
c1cc812ea4 build: bump version to v1.3.6 2023-06-24 19:17:59 +03:00
9663e3f0f4 chore: merge dev branch to main branch (#945) 2023-06-24 19:16:22 +03:00
941f618153 fix(app-selector): remove direct use of strings (#944) 2023-06-23 16:39:12 +03:00
716a30bf7b fix: experimental patches stay selected when toggled off (#946) 2023-06-23 16:37:23 +03:00
d051ae576b feat(updater): download successful dialog (#938) 2023-06-23 16:36:36 +03:00
35e99cb014 feat(mount): use /data/adb/revanced again and ensure migration scenario (#948) 2023-06-23 01:03:03 +03:00
af054fba49 docs: improve clarity of footnotes [skip ci] (#950) 2023-06-17 14:48:57 +02:00
58d837d641 feat(app-selector): Improve app item UI to avoid overflow issues (#943) 2023-06-16 10:04:55 +03:00
6cc1bd21cd fix(updater): ability to start again after cancelling (#937) 2023-06-16 10:04:07 +03:00
cebfa7c8ae docs: simplify steps 2023-06-16 05:09:52 +02:00
e2d7ab8f8f chore: fix broken docs link 2023-06-15 16:42:09 +02:00
ec77987fcd feat(app-selector): improve skeleton UI (#939) 2023-06-15 12:57:39 +03:00
b161608d02 feat(patches-selector): improve disabled card UI (#941) 2023-06-15 12:54:49 +03:00
5e7458ff1c fix: import patches selects unsupported patches (#942) 2023-06-14 21:34:11 +07:00
Pun
ed06aaa1f5 docs(readme): fix invalid url for building manager from source
📜 fix invalid URL for building manager from source.

Old: https://github.com/revanced/revanced-manager/blob/docs/docs/5_building-from-source.md
New: https://github.com/revanced/revanced-manager/blob/main/docs/4_building.md
2023-06-14 19:52:10 +07:00
13b7179941 fix: fix not asking for permission 2023-06-12 09:56:49 +05:30
da0d88d86f build: bump patcher 2023-06-12 09:08:29 +05:30
b7347c312a build: bump version to v1.3.4 2023-06-12 08:59:49 +05:30
c876f2f7e3 feat: add permission to manage storage 2023-06-12 08:59:14 +05:30
adad5fd8ff build: bump version to v1.3.3 2023-06-12 07:02:49 +05:30
2aaa7ae8c9 chore: merge dev to main (#935) 2023-06-12 07:01:12 +05:30
1f64ea37bd fix: using wrong string 2023-06-12 06:59:43 +05:30
cac1525da0 build: bump patcher 2023-06-12 06:02:16 +05:30
1ad906fedc refactor: improve code 2023-06-12 06:01:57 +05:30
20ffef39a3 fix: update button being shown as clickable on launch (#932) 2023-06-12 05:55:53 +05:30
185460c054 chore(fastlane): update description (#933)
Co-authored-by: Ushie <ushiekane@gmail.com>
2023-06-12 03:21:33 +03:00
0079e74d77 feat: clarify suggested version in app list (#934)
Co-authored-by: Aunali321 <48486084+Aunali321@users.noreply.github.com>
2023-06-12 05:35:02 +05:30
a8e019482f ci: add workflow to update documentation repository 2023-06-10 23:41:36 +02:00
f01b8e47aa fix: aborting message changed (#928)
Change aborting message from "aborting..." to "aborted..."

----------------------
Co-authored-by: Ushie <github@ushie.dev>
Co-authored-by: Ushie <ushiekane@gmail.com>
2023-06-09 17:34:55 +07:00
Pun
e43dfb7599 docs(troubleshooting): fix markdown styling (#929)
`docs`: `84a9168` - Self-explanatory, fix markdown styling issue
2023-06-09 17:24:31 +07:00
6b8cfe2b49 build: bump version to v1.3.2 2023-06-09 11:42:06 +03:00
7f7b14bae3 fix: use correct variable name for armv7 check 2023-06-09 11:40:54 +03:00
810b02d9fd build: bump version to v1.3.1 2023-06-09 10:45:22 +03:00
62813145b2 chore: merge dev to main (#926) 2023-06-09 01:59:18 +03:00
4877058253 chore(fastlane): add banner 2023-06-09 01:58:02 +03:00
2a9fd3abb8 chore(fastlane): add missing directory 2023-06-09 01:57:49 +03:00
a244f7b598 chore: merge dev to main 2023-06-08 21:31:41 +03:00
5396457ad5 chore: move fastlane to correct directory 2023-06-08 21:30:44 +03:00
0186b6ea61 chore: merge dev to main (#924) 2023-06-07 20:18:21 +03:00
fe
f6e99f7e88 fix: return fetched patch version if non default patch repo is used (#922) 2023-06-07 20:16:25 +03:00
Pun
c24a3828be ci(analyze): don't run when pushed to repository (#920) 2023-06-07 20:15:43 +03:00
Pun
2e38a4567a feat: clarify architecture in about section
This commit changes `Arch` to `Supported Arch(s)`
2023-06-07 20:13:42 +03:00
67c5d67a61 feat: update icons for the new logo
fixed #918
2023-06-07 20:10:14 +03:00
9592dde534 feat: add fastlane for F-Droid (#889)
Co-authored-by: Ushie <ushiekane@gmail.com>
2023-06-07 19:59:17 +03:00
d030b0af70 docs: init (#911)
Co-authored-by: afn <_@afn.lol>
2023-06-07 12:01:36 +02:00
4ccb9ac94d fix: remove redundant "v" in the downloader screen (#895) 2023-05-26 13:54:52 +03:00
8af62b917c build: bump version to 1.3.0 (#892) 2023-05-24 23:33:04 +03:00
311f114132 chore: update v1.3.0 2023-05-24 21:46:00 +02:00
d015bd03f7 chore: merge dev to main (#891) 2023-05-24 22:17:16 +03:00
a61b9de0fa chore(deps): bump revanced-patcher to 9.0.0 2023-05-24 02:02:59 +02:00
ef1b283917 chore(deps): bump revanced-patcher to 8.0.0 (#880) 2023-05-24 01:07:22 +05:30
c677f00105 build: bump version to v1.2.0 2023-05-20 02:30:18 +03:00
8d2f778dfe chore: merge dev to main (#881) 2023-05-20 02:28:58 +03:00
c549d102f6 build: migrate to dart 3 (#871) 2023-05-20 04:07:53 +05:30
39a9ee4e9d chore: remove coreLibraryDesugaring (#873) 2023-05-20 04:05:46 +05:30
a27dc6ad1c feat: migrate to dart3 2023-05-19 14:23:48 +05:45
8ccb75fc8d chore: remove coreLibraryDesugaring 2023-05-17 15:38:03 +09:00
8fc86dbe02 feat: allow selecting installed if app is full apk 2023-05-13 04:54:35 +03:00
359f052608 fix: export keystore not working in some conditions (#862) 2023-05-12 19:36:47 +03:00
4150e2265c fix: use correct version in update download dialog (#859) 2023-05-10 13:23:02 +03:00
b803ce7435 fix: system navigation overlapping UI (#853) 2023-05-10 13:18:51 +03:00
289c6cd7a9 chore: merge dev to main (#857) 2023-05-08 21:46:33 +05:30
31fc7b74c2 build: bump version to v1.1.0 2023-05-08 21:38:19 +05:30
3e565f25be fix(appselector): closing dialog closes app selector 2023-05-08 21:27:07 +05:30
e509be4e21 chore(deps): bump patcher to 7.1.1 2023-05-08 21:19:45 +05:30
170fc537ac fix: fix armv7 dialog shown for x86, x86_64 2023-05-07 04:14:57 +05:30
3fe5882145 feat: remove cronet 2023-05-06 05:39:46 +05:30
a290791410 chore: merge dev to main (#844) 2023-05-06 03:26:14 +05:30
Pun
2ebd38ff68 feat: add checksums verification to Gradle (#813) 2023-05-03 21:38:47 +03:00
cd987a5b19 fix: broken filename when saving files with the same name (#837) 2023-05-03 21:34:30 +03:00
7f1dab7ee1 ci: temporarily restore old release workflow 2023-05-02 17:50:07 +03:00
bed3945aa5 feat: clarify warning and consequences for ARMv7 users (#836) 2023-05-02 16:41:11 +07:00
15a32a18b7 fix: fix broken manager update implmentation 2023-04-30 17:38:08 +07:00
d2e8e7dd5d chore: comply with review changes 2023-04-30 01:03:36 +07:00
ce12ec89c4 ci(release): automatically bump version codes 2023-04-29 20:53:54 +07:00
b286444ad9 fix(i18n): update translation for refreshSucess 2023-04-29 20:43:02 +07:00
941263102d refactor: change name from "recommendedVersion" to "suggestedVersion" 2023-04-29 20:43:02 +07:00
e2ed296dc7 chore(deps): constrain get_it to 7.2.0 2023-04-29 19:46:07 +07:00
cfc866bef2 feat: change continue anyways to cancel 2023-04-29 15:37:30 +03:00
affba669ce feat: add continue anyway button to select from storage dialog (#810) 2023-04-29 15:36:25 +03:00
7230152ab8 chore: fix issues with gitignore [skip ci] 2023-04-29 17:42:19 +07:00
21fee7171f chore: update pubspec version
skip-checks: true
2023-04-29 15:40:25 +07:00
ad17995f28 feat: improve suggested app version text (#822) 2023-04-28 17:41:28 +03:00
ac830cbe7f fix: resized monochrome icon to match the original (#789)
cherry-pick from 6281ae82876c69ff4194a782e4b08b398a1285d6
2023-04-27 18:12:48 +07:00
65da6af3f9 fix: update pubspec version 2023-04-21 14:56:31 +03:00
3d90bf7588 ci: bypass push permission on protected branches with owner PAT 2023-04-21 14:55:29 +03:00
62ef1c88fe feat: resetting source to default dismiss the sources pop-up (#797)
* feat: resetting source to default dismiss the sources pop-up

* chore: format using `dart format`
2023-04-21 00:29:37 +05:30
d6918920b6 chore: add missing translations 2023-04-20 23:53:24 +05:30
cdfb09fbfa feat: warning for armv7 devices 2023-04-20 23:20:30 +05:30
bb681e31c9 feat: disable selecting installed apps for nonroot 2023-04-20 22:25:29 +05:30
c7483936ec docs: remove alpha disclaimer 2023-04-20 05:07:46 +05:30
0a1f2da33d feat: appreciation message for new contributors 2023-04-20 05:04:37 +05:30
f5aafdb7d6 feat: progress bar for manager updates 2023-04-20 04:45:46 +05:30
c9adf1c492 style: sort imports 2023-04-19 01:32:43 +05:30
4c9cb560e3 feat: auto select default patches 2023-04-19 01:21:08 +05:30
f0b028279c fix: open contributor links externally (#791) 2023-04-18 19:46:16 +05:30
197770b68b chore: update dependencies (#772)
* chore: updated some dependencies

* refactor: reimplemented cache interceptor

* Revert "Updated dependencies & migrated breaking changes"

This reverts commit e6743b0d6b2552fdbf1c99d23e158e682362dd5d.

* chore: migrated flutter_local_notifications

* revert: reimplemented cache interceptor
2023-04-18 19:45:29 +05:30
37b583f560 feat: trim extra space when setting custom source (#771) 2023-04-18 17:15:04 +07:00
dca2d4fe12 feat: add option to import/export keystore (#776)
* feat: add option to import/export keystore

* change the order of import/export keystore buttons

* feat: add option to change the keystore password
2023-04-18 15:08:10 +05:30
3b677f8ae3 feat: improve ux (#752)
* feat: restart app toast when changing sources, api url

* fix: potentially fix manager stuck on black screen

* feat: remove select all patches chip

* feat: show all apps and recommended versions

* chore(i18n): remove unused strings

remove unused strings left out in 7e05bca

* feat: select install type before patching

* feat: update patches button (#782)

* feat: update patches button

* feat: show toast when force refresh

* chore: don't translate "ReVanced Manager" and "ReVanced Patches"

* Revert "feat: select install type before patching"

This reverts commit 74e0c09b543fe70b70cd917e6e21ceb0f46f3533.

* feat: rename recommended patches to default patches

* feat: add missing localization

* feat: display restart app toast for resetting source

---------

Co-authored-by: EvadeMaster <93124920+EvadeMaster@users.noreply.github.com>
2023-04-18 13:27:26 +05:30
0b952578d1 ci(release): update semantic-release 2023-04-02 02:37:49 +07:00
054afbbedd feat: confirmation dialog for deleting keystore (#764)
* feat: confirmation dialog for deleting keystore

* refactor(i18n): apply suggestion from code-reviewer

Co-authored-by: Ushie <github@ushie.dev>

* refactor: apply suggestion from code-reviewer

Co-authored-by: Mipirakas <borismichiels@gmail.com>

---------

Co-authored-by: Ushie <github@ushie.dev>
Co-authored-by: Mipirakas <borismichiels@gmail.com>
2023-04-01 21:02:28 +07:00
866a6e4a44 chore(CHANGELOG): reset changelog 2023-03-30 23:29:26 +07:00
8ea7dd478b ci(release): update node dependencies 2023-03-30 23:20:31 +07:00
7839252934 ci(analyze): only run when necessary (#766)
* ci(analyser): only run on lib/`.dart` changes

* ci(analyser): runs on workflow changes

* ci(analyze): run on all dart file changes

---------

Co-authored-by: Palm <palmpasuthorn@gmail.com>
2023-03-29 20:51:08 +03:00
fa4063220f ci: dart analyser (#761)
* ci: dart analyser

* ci(analyser): clarify the dart analysing step

* refactor: ignore generated files

* ci(analyser): apply suggestion from code-reviewer

Co-Authored-By: Palm <palmpasuthorn@gmail.com>

* ci(analyser): apply suggestion from code-reviewer

Co-Authored-By: Palm <palmpasuthorn@gmail.com>

* ci(analyser): apply suggestion from code-reviewer

Co-authored-by: Palm <palmpasuthorn@gmail.com>

---------

Co-authored-by: Palm <palmpasuthorn@gmail.com>
2023-03-28 15:47:34 +07:00
d214a02abd ci(release): fix argument parsing of npm exec 2023-03-26 23:02:57 +07:00
d1c12edd1b ci(release): use appropriate npm commands for ci environment 2023-03-26 23:02:57 +07:00
ded1a44c37 ci(release): fix step not moving artifact to accessible path 2023-03-26 23:02:57 +07:00
790a6cd1e3 ci(release): remove unnecessary variables in flutter build step 2023-03-26 18:57:27 +03:00
a79f883a0f ci: use caching to speed up workflows (#760)
Use caching to speed up workflows: 9-14 minutes (ish) to 6-8 minutes (ish)

Commits:
* ci(release): use GitHub cache

* ci(release): restore signing
2023-03-26 20:02:56 +07:00
d9c5a540a3 ci(release): fix wrong artifact name and ci not uploading build 2023-03-24 01:01:44 +07:00
276f33b9ec ci: use semantic-release (#746) 2023-03-23 17:34:52 +01:00
ded59d2da0 feat(ci): update crowdin workflow to use new main branch 2023-03-20 22:39:29 +03:00
62f7a820d8 feat: remove notice about stale development [skip ci] 2023-03-20 18:03:58 +03:00
7063ffa013 build: bump version to v0.0.57 2023-03-14 21:00:48 +03:00
bf4dc3c095 chore: bump stacked
stacked: ^3.0.0 -> ^3.2.0
stacked_generator: ^0.8.0 -> ^1.0.0
stacked_services: ^0.9.3 -> ^1.0.0
2023-03-14 21:00:28 +03:00
c10e5848bf Merge pull request #707 from revanced/dev
chore: merge branch `dev` to `flutter`
2023-03-14 22:07:35 +07:00
92a3b0d6e0 feat(style): use native switch & chip (#732)
* chore: remove useless themedata

* feat(style): new switch

* feat(style): use native chip components

* chore: remove unused import

* feat(accessibility): set tooltip

* chore: remove unneeded themedata

* chore: fix theme

* feat(i18n): add 3 new strings

* feat(style): correct material 3 theme on nondynamic
2023-03-14 21:53:42 +07:00
b475bd25c8 build: remove env setup step 2023-03-11 23:01:26 +05:30
d318224a6f fix: black screen after resetting custom sources 2023-03-06 18:08:48 +05:30
0074fee865 build: new ci (#731)
New Build CI

Commits:
* buid: re-do ci

* build: ignore tags

* build: get the latest flutter version automatically && formatting

---------

Co-authored-by: EvadeMaster <93124920+EvadeMaster@users.noreply.github.com>
2023-03-05 16:23:51 +07:00
5617535a63 refactor: remove sentry and crowdin (#730)
We no longer use sentry and crowdin.
2023-03-05 16:12:46 +07:00
68ccefc59f revert: "refactor: update deprecated and minor code refactors (#710)"
This reverts commit 6829d3cdeac94fb305b8885570840fe25769b6a6.

Signed-off-by: EvadeMaster <93124920+EvadeMaster@users.noreply.github.com>
2023-03-03 18:06:24 +07:00
6d60541626 chore(deps): meet patcher breaking changes 2023-03-03 03:11:44 +03:00
a635e5b8d0 chore: addFiles -> addIntegrations (#725)
fix #721
2023-03-02 14:51:35 +07:00
48a10440fe ci(build): remove environment on PR build 2023-03-02 11:06:15 +07:00
8e3ba88318 chore: bump gradle 7.5-rc.1 -> 7.6.1 (#717)
* chore: upgrade gradle from 7.5-rc.1 -> 7.6.1

* chore: remove unrelated changes
2023-02-27 19:26:10 +07:00
ab8fccc544 chore(deps): bump patcher version from 6.4.3 -> 7.0.0 2023-02-27 19:19:19 +07:00
8319dc9164 fix: improperly sized monochrome icon (#715) 2023-02-27 19:07:44 +07:00
6829d3cdea refactor: update deprecated and minor code refactors (#710)
Improve code readability & additional refactoring

Commits:
chore: exclude generated from analyzer

refactor: add SharedPreferences to locator

refactor: access shared pref from locator, and code refactor

refactor: remove unwanted `await`

refactor: remove `const` from `CacheConfig`
2023-02-27 19:06:05 +07:00
3ae4d69110 chore: migrate deprecation code && code cleanup (#708)
Fixes all issues in `flutter analyze`.
<Reviewed>

Commits:
* chore: migrate deprecated text style

* chore: migrate `toggleableActiveColor` to individual theme

* chore: don't use 'BuildContext's across async gaps
2023-02-20 16:53:53 +07:00
dc665f227e fix: use high resolution adaptive icons (#675)
Use high-resolution icons instead of the low-resolution ones on ReVanced Manager.

Commits:
* High resolution adaptive icons, built using revanced-logo-no-background.svg

Same icons currently used for ReVanced YouTube.

* tweak background color to match revanced-logo-no-background.svg

* recreated foreground using 'revanced-logo-shape.svg'

* updated full resolution icons, using same ratio as original SVG

* updated icons with gradient border
2023-02-17 19:28:01 +07:00
a83496568f chore: code cleanup (#681)
Improve the readability of the code.

Commits:
* chore: correct typos `fragement` to `fragment`

* chore: put a single newline at end of file.
2023-02-17 18:53:23 +07:00
12d25570af fix: Long patch description truncated (#702)
fix: long patch description truncated (#702)

Allow flexible height
2023-02-14 18:42:24 +07:00
378c947654 chore: bump version to v0.0.56 2023-02-12 13:04:34 +07:00
bd39a3140e feat: fix patch bundle version no longer displayed (#686) 2023-02-12 00:02:30 +07:00
7d3ca3dec1 fix: display patches version on first load (#687) 2023-02-12 00:00:01 +07:00
1cb556c8f8 chore: bump version to 0.0.55 2023-02-11 23:59:53 +07:00
8c8f96de1c build: bump patcher version 2023-02-11 23:59:47 +07:00
318cd87a9a feat(style): use the correct m3 theming (partially) (#680) 2023-02-07 15:46:29 +03:00
5d63d5c2d3 feat: potentially fix apps disappearing when update is available (#674) 2023-01-31 15:42:00 +03:00
7d347fccc6 build: bump version to v0.0.54 2023-01-30 15:57:06 +03:00
a54ca799b9 feat: update rules of analysis_options.yaml. and solved all problems (#665)
* update rules of analysis_options.yaml. and solved all problems

* refactor: fix remaining problems

---------

Co-authored-by: Ushie <ushiekane@gmail.com>
2023-01-30 15:35:06 +03:00
f5bc1a996f feat: remove select all icon from searchbar (#669) 2023-01-30 15:13:25 +03:00
8591bc4d01 chore: update cr_file_saver (#668)
Co-authored-by: Ushie <github@ushie.dev>
2023-01-30 15:11:31 +03:00
40888c07f3 feat: fix overflow text & separate version from patch name (#666)
* chore: Add note

* feat: Fix overflow text & seperate version from patch title

* feat: Fix overflow text & separate version from patch name
2023-01-30 15:07:25 +03:00
1c965c3788 refactor: cleanup remember patches feature (#630) 2023-01-30 15:03:55 +03:00
4df690c2a2 feat: monochrome icon + predictive back gesture (#591)
* Add monochrome icon

* Enable predictive back gesture

* feat: monochrome icon + predictive back gesture
2023-01-30 01:23:56 +03:00
d6abb61e2b ci(build): update workflow actions (#662)
- Bump upload-artifact to v3

- Update crowdin/github-action according to new versioning format [https://github.com/crowdin/github-action/releases/tag/v1.6.0]
2023-01-29 23:56:13 +03:00
3434c862e9 feat: improve note [skip ci] 2023-01-28 07:49:57 +01:00
ea8af926fa chore: update broken documentation link (#659)
* chorus: fix invalid documentation link

* chorus: Update broken documentation link
2023-01-22 16:18:41 +03:00
c3df48174c feat: warn user for selecting all patches (#649) 2023-01-21 16:37:28 +01:00
f1e60f96c4 chore: bump version to 0.0.53 2023-01-18 22:07:00 +01:00
cdd852678b build: bump patcher version 2023-01-18 22:07:00 +01:00
bf518b5467 bump: kotlin gradle plugin 2023-01-18 22:07:00 +01:00
ffd53fab26 feat: clarify acknowledgement label (#608) 2023-01-14 20:23:49 +03:00
5aad7dad35 chore: fix incorrect wording 2023-01-12 21:03:11 +01:00
b1c1a9f4e1 feat: stale development notice [skip ci] 2023-01-07 14:24:43 +01:00
d847a8e0b2 build: bump version to v0.0.52 2022-12-19 01:39:48 +01:00
9668730b5d build: bump patcher version (#610) 2022-12-19 01:35:58 +01:00
dc049cf26a New Crowdin translations by Github Action (#601)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-12-16 22:41:26 +03:00
82c2b2f128 build: bump version to v0.0.51 2022-12-15 21:24:06 +03:00
5f81d65911 feat: sort by amount of patches, display patches count, setting to enable universal patches (#593)
* feat: sort by amount of patches

* feat: display patches count in application card

* feat: setting to enable universal patches
2022-12-15 23:35:45 +05:30
19f990c564 build: bump version to v0.0.50 2022-12-14 14:17:38 +03:00
62467007b2 Revert "feat: display app's patch count in appcard"
This reverts commit 8d4e4ba6c9c30fdade2fcbf79aaaba34c68458ac.
2022-12-14 13:20:17 +03:00
d7624e5e1f Revert "feat: filter apps by patch count"
This reverts commit d78868b4625747b8c30e278d5a6a8fbb49bdc19d.
2022-12-14 13:19:59 +03:00
4505f10e50 build: bump version number 2022-12-14 06:04:42 +01:00
3ce3df5e19 build: bump patcher version 2022-12-14 06:04:10 +01:00
8d4e4ba6c9 feat: display app's patch count in appcard 2022-12-14 02:12:57 +03:00
d78868b462 feat: filter apps by patch count 2022-12-14 01:52:06 +03:00
01a681ad00 build: bump version to v0.0.48 2022-12-12 22:34:39 +03:00
adfeb61eab fix: print patch fail cause if message is null 2022-12-12 22:32:11 +03:00
8c3faac343 build: bump version to v0.0.47 2022-12-12 17:09:32 +03:00
c81acce31c feat: improve patch fail logging 2022-12-12 17:09:32 +03:00
fe629ce77c Merge branch 'flutter' of https://github.com/revanced/revanced-manager into flutter 2022-12-11 18:04:06 +05:30
5c27add2b2 bump:bump version to v0.0.46 2022-12-11 18:04:00 +05:30
ff90dae695 feat: add support for shared patches (#577)
* fix: avoid npe if a patch has empty compatible package.

* feat: support for shared patches

* fix: incorrect bool check and cleanup

Co-authored-by: Aunali321 <aunvakil.aa@gmail.com>
2022-12-11 18:00:44 +05:30
4f8aec6a05 build: bump version to v0.0.45 2022-12-09 17:47:51 +05:30
ba8df57580 Update broken link (#569) 2022-12-09 17:42:09 +05:30
3bab1940c1 Support Gitea repositories (#570) 2022-12-09 17:40:43 +05:30
e300c92215 build: bump version to v0.0.44 2022-12-06 03:15:23 +01:00
99764e25ed build: bump patcher dependency version 2022-12-06 03:14:35 +01:00
b5dcae11a4 Merge branch 'flutter' of https://github.com/revanced/revanced-manager into flutter 2022-11-29 21:16:54 +05:30
f69f475ab9 build: bump version to v0.0.43 2022-11-29 21:16:42 +05:30
061e929705 feat: Changelog in update dialog (#551)
* add update confirmation dialog

* fix loader placement
2022-11-29 21:15:44 +05:30
9e8193a6ac fix: selected patches not being remembered. 2022-11-29 21:12:46 +05:30
1601796c6c build: bump patcher dependency. 2022-11-29 21:03:12 +05:30
a0af0dde0a fix: disable changing languages for now. 2022-11-29 17:45:42 +05:30
c5ba6a238a ci(crowdin): upload sources only 2022-11-29 14:29:30 +03:00
3c3ebe7d9d build: bump version to v0.0.42 2022-11-26 22:14:56 +03:00
b7227bfad7 fix(build): use correct step id 2022-11-26 22:14:42 +03:00
447c16cff1 fix(build): migrate env name to new workflow 2022-11-26 21:17:57 +03:00
47529ae3f5 build: workflows for PRs & all commits 2022-11-26 21:16:29 +03:00
149af96d51 New Crowdin translations by Github Action (#541)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-11-26 16:43:08 +03:00
14e9dd1c04 fix: update workflow actions (#482)
* fix: update workflow actions

* fix: sign apk parameters
2022-11-26 16:34:14 +03:00
ec9ef98981 feat(apiUrl): fix textbox hint (#521) 2022-11-26 16:33:18 +03:00
922f474b59 fix: apply correct patch if patch names are same (#535) 2022-11-24 20:14:04 +03:00
795a1c78df build: bump version to v0.0.41 2022-11-24 00:36:03 +03:00
6ea055d486 feat: use secrets for keys (#528) 2022-11-24 00:27:00 +03:00
b0c4567cb8 feat: use github to get latest patches version if custom sources are used. 2022-11-23 10:03:56 +05:30
6e05120aa5 feat: add chips for patches selection. 2022-11-23 09:48:10 +05:30
c5958f1257 docs: add translation field 2022-11-15 14:20:23 +03:00
0faf86c9e9 refactor: restructure codebase. (#501)
* refactor: separate language selection to own widget.

* feat: separate theme changer to own widget.

* refactor: move Appearance UI to separate class.

* refactor: move language selection UI to separate class.

* refactor: move sources selection to separate file.

* refactor: move sources selection to separate file.

* refactor: split settings sections in separate files.

* refactor: move logging section to separate file.

* fix: show toast on bottom.

* fix: recommended patches not being selected by default.

* fix: patch selection selecting non recommended patches.

* fix: experimental toggle not updating.
2022-11-12 21:25:33 +05:30
ee689922a3 feat: do not update on tracing logs 2022-11-11 16:15:23 +01:00
be77a181ec feat: EXPERIMENTAL language switching. 2022-11-10 23:03:07 +05:30
30376c960f build: bump version to v0.0.40 2022-11-10 12:14:50 +05:30
405147b1c5 fix(i18n): locale capitalization and grammar (#496)
* fix(i18n): locale capitalization and grammar

* fix(i18n): add some more changes requested
2022-11-09 19:07:28 +05:30
d545dfe49b refactor: move exporting patches to another section 2022-11-09 14:20:32 +05:30
c571cf2c53 feat: ability to store and load selected patches (#469)
* feat: ability to store and load selected patches

* fix: I18n

* fix: do not append but truncate file

* fix: use json file, minor fixes

* fix: better ui

* WIP

* feat: load patches selection after app selection

* feat: import/export json file

* fix: reformat code

* fix: rare bug on import feature fixed

* fix: move export/ipmort to settings page & import full json

* fix: minor improvements

* fix: minor code quality improvements

* fix: export filename fix

* fix: select list element istead of removing it
2022-11-09 13:06:04 +05:30
fd5d71e24d fix: delete cached apk files when picking new one (#481) 2022-11-09 13:05:26 +05:30
2c3809d2bc feat: show selected patches count in patches selector view & patch selector card (#466)
* feat: show selected patches count in patches selector view & patch selector card

* fix: add Widget on row

* fix: formatting

* fix: same text style for count
2022-11-09 13:02:39 +05:30
0fc8e7cbc8 feat: crowdin workflow 2022-11-05 01:31:56 +03:00
787e47f634 Update Crowdin configuration file 2022-11-04 23:14:55 +03:00
dc47da75f2 feat: Adding support for Exporting APK (#439)
* feat: Adding Export APK support

* Using cr_file_saver to simplify export
2022-11-02 17:22:40 +05:30
6b999b0a0c build: bump version to v0.0.39 2022-11-02 00:06:29 +03:00
b00d2d16d4 feat: simplify logging (#305)
Co-authored-by: Ushie <ushiekane@gmail.com>
2022-11-02 00:02:51 +03:00
97d4da568b fix: allow tapping on patch card when experimental switch is enabled (#464) 2022-11-02 00:02:33 +03:00
e563049f6a build: bump version to v0.0.38 2022-11-01 19:04:23 +03:00
cc00d0dc08 bump: kotlin gradle plugin (#461) 2022-11-01 19:03:26 +03:00
2a220c3984 fix: custom sources. 2022-11-01 19:03:30 +05:30
1d440d25be fix: dont select all patches if experimental toggle is off. 2022-11-01 15:36:06 +05:30
ba5234e850 feat: experimental settings to allow patch on any app version. 2022-11-01 15:26:15 +05:30
293f7150f1 build: bump version to v0.0.37 2022-10-31 00:59:57 +03:00
41b1cec8d3 feat: move to new API domain 2022-10-31 00:57:12 +03:00
c129c1eeae build: bump version to v0.0.36
totally didnt forget to bump last night
2022-10-20 10:08:33 +03:00
caa9694543 feat: improve explanation of update being unusable 2022-10-20 00:15:48 +03:00
ac79765372 fix: keystore name typo. 2022-10-19 18:55:27 +05:30
39500f054d feat: disable sentry for time being. 2022-10-19 18:53:13 +05:30
f2d5cc91db Merge branch 'flutter' of https://github.com/revanced/revanced-manager into flutter 2022-10-19 16:33:00 +05:30
84a788fd9e feat: toast for disabled updates. 2022-10-19 16:32:54 +05:30
3778bfe1b5 feat(installer): remove restriction of Share APK, closes #410 (#412)
Share APK has a requirement of needing to install the patched app before sharing, this PR removes that as I see no reason for this restriction
2022-10-18 16:16:04 +03:00
63b2d8e0bd build: remove create env step from actions. 2022-10-17 21:51:18 +05:30
e7490b8d75 refactor: disable sentry for now. 2022-10-17 21:39:46 +05:30
2e050d06e8 feat: remove firebase. (#405)
* feat: remove firebase.

* build: remove firebase config.
2022-10-17 20:24:47 +05:30
5fd1154039 build: bump version to 0.0.34 2022-10-17 19:53:46 +05:30
39401a78ec refactor: disable firebase for now. 2022-10-17 19:52:47 +05:30
273aa42b17 refactor: redo the environment system. 2022-10-17 16:48:39 +05:30
603917d21e feat: create env with github actions. 2022-10-17 16:25:22 +05:30
e55cd6a938 feat: add env vars to repo. 2022-10-17 15:06:33 +05:30
2aaed14a3a fix: add firebase options to repo. 2022-10-17 13:59:39 +05:30
511c25163d refactor: delete empty env. 2022-10-17 12:58:26 +05:30
c24e50f3a0 feat: add empty .env config. 2022-10-17 02:09:45 +05:30
2d732288a7 feat: option to delete manager logs. 2022-10-17 01:58:50 +05:30
56e715cd3c build: bump version to v0.0.33 2022-10-17 01:41:11 +05:30
074d8005bc feat: show patch bundle version in patch selector screen. 2022-10-17 01:40:16 +05:30
5b38c9442a fix: increase sleep timer for mount script. 2022-10-17 00:46:48 +05:30
3b8dc66da6 feat: option to delete temporary directory. 2022-10-17 00:41:20 +05:30
f5ebfc92fc feat: decrease time for force refresh of patches. 2022-10-17 00:32:45 +05:30
9de063aced feat: ability to delete keystores. 2022-10-17 00:22:07 +05:30
331691cc9d fix: add env file to repo. 2022-10-16 23:55:09 +05:30
1a97cdf91d refactor: remove useless prints. 2022-10-16 23:54:07 +05:30
fbd4359d61 fix: add missing else block. 2022-10-16 23:43:33 +05:30
f31a60d9bb feat: make firebase crashlytics optional. 2022-10-15 18:29:50 +05:30
79aca0e579 feat: firebase crashlytics for improving manager. 2022-10-15 15:01:31 +05:30
6d35c47b6b feat: make sentry logging optional. 2022-10-15 01:52:10 +05:30
f1261398e9 feat: sentry integration. 2022-10-14 23:35:33 +05:30
007b518503 build: Bump version to v0.0.32 2022-10-12 02:07:23 +05:30
c6edc620c8 feat: cronet lib for non gms device. 2022-10-12 01:52:57 +05:30
3f9d7c9cc0 fix: add back saving patched apps after clearing data. 2022-10-11 20:27:29 +05:30
6c1845e246 feat: add fallback for cronet. 2022-10-11 20:19:50 +05:30
0b2572a730 Bump version to v0.0.31 2022-10-11 02:56:27 +05:30
6fc46d632b feat: make applied patches dialog scrollable. 2022-10-11 02:55:35 +05:30
b2a35813f6 fix: disable update functionality for now. 2022-10-11 02:32:23 +05:30
279b76ad53 fix: don't show previously patched apps after clearing data. 2022-10-11 02:17:13 +05:30
b550016681 fix: use original package name to load patches. 2022-10-10 19:02:42 +05:30
3ab5d12f3e fix: use original package name for changelog. 2022-10-10 18:57:32 +05:30
53bcb8b85f Merge branch 'flutter' of https://github.com/revanced/revanced-manager into flutter 2022-10-10 18:46:02 +05:30
94397dcb4c feat: store original package name of patches apps. 2022-10-10 18:45:58 +05:30
afn
53fbee2d44 refactor: Reorder sections in readme. 2022-10-10 00:21:49 +05:30
e46ad3595d fix: permanent keystore path if available (#375) 2022-10-10 00:05:43 +05:30
f2b03b6e69 Merge branch 'flutter' of https://github.com/revanced/revanced-manager into flutter 2022-10-09 23:01:18 +05:30
0df2a6bdc0 feat: brotli support. 2022-10-09 23:01:10 +05:30
df31e5ccd1 feat: add building from source instructions. 2022-10-09 02:59:50 +05:30
4dfa0dada6 build: bump version to v0.0.30 2022-10-08 22:08:03 +05:30
857a523f84 fix: changelog for youtube. 2022-10-08 22:06:45 +05:30
ceac838706 feat: use new api url. 2022-10-08 19:03:45 +05:30
e8cb6d27fc Merge branch 'flutter' of https://github.com/revanced/revanced-manager into flutter 2022-10-07 18:23:49 +05:30
78428f6bd3 feat: HTTP/3 Support. 2022-10-07 18:23:43 +05:30
da94dfba70 build: Android Studio project build config 2022-10-06 12:40:25 +03:00
8275792f45 chore: bump patcher dependency version to v6.0.0 (#355) 2022-10-06 11:26:20 +02:00
a90923011a feat: allow searching for displayed patch names (#348) 2022-10-03 21:06:12 +03:00
1aa24e2871 fix: print stack trace of patch exceptions (#314) 2022-10-03 22:34:46 +05:30
68ce751745 build: bump version to v0.0.27 2022-10-01 02:15:47 +02:00
74ff64d41a chore: bump patcher dependency version to v5.1.2 2022-10-01 02:15:08 +02:00
6d45ccecc2 build: Bump version to v0.0.27 2022-09-30 21:38:15 +03:00
5418c36716 feat: use patcher for resourcePatch detection (#337)
Co-authored-by: Aunali321 <aunvakil.aa@gmail.com>
2022-09-30 21:36:39 +03:00
ca0657e8f9 feat: decrease cache duration of patches and integrations. 2022-09-28 22:26:54 +05:30
a5511c2a2c feat: update unsupported button title 2022-09-26 23:36:17 +03:00
a346f8857f build: Bump version to v0.0.26 2022-09-26 16:52:04 +03:00
e12532ea4c chore: bump patcher dependency version to v5.1.0 2022-09-26 04:34:23 +02:00
afn
7ecf951bfb fix: tweak card appearances (#296)
* fix: tweak card appearances

* Update patch_selector_card.dart
2022-09-25 18:13:37 +05:30
db18874ea1 fix: now using country code for languages. 2022-09-25 14:38:25 +05:30
18a69776cd feat: en_US.json language asset file 2022-09-25 08:26:08 +02:00
21cadf6450 feat: remove en.json language asset 2022-09-25 08:22:21 +02:00
5ddbe6e252 Update Crowdin configuration file 2022-09-25 08:19:47 +02:00
afn
6d1427e01e fix: move changelog into app item custom card (#294) 2022-09-25 00:19:02 +05:30
6ac901f1d6 build: Bump version to v0.0.24 2022-09-24 13:50:44 +01:00
587ba795bb Revert "fix: Prevent content from being overlapped by system navigation bar"
This reverts commit 4d82ff3011af43f60b0ea694aaed207c6883bd8f.
2022-09-24 13:49:46 +01:00
6b66c7bbd0 build: Bump version to v0.0.23 2022-09-24 12:16:02 +01:00
f398b6863a Merge branch 'flutter' of github.com:revanced/revanced-manager into flutter 2022-09-24 12:12:24 +01:00
4d82ff3011 fix: Prevent content from being overlapped by system navigation bar 2022-09-24 12:06:36 +01:00
2a0ea78d7f feat: Adds support for patching on x86 and x64 devices
Introduces new aapt2 binaries from revanced/aapt2
2022-09-24 12:05:36 +01:00
4722880647 feat: better patch version background. 2022-09-24 13:47:09 +05:30
32fabcfa3f fix: nav overlapping install buttons. 2022-09-24 12:53:46 +05:30
e0c46e4268 fix: Show "Share log" menu option even if patching has errors 2022-09-23 17:22:39 +01:00
d84230fa22 feat: Merge integrations if a patch or any of its dependencies need them 2022-09-23 17:20:19 +01:00
9561153bfb feat: Hide "Install as Root" button if user does not have root access at all 2022-09-23 15:47:30 +01:00
c8c35ca801 build: Bump version to v0.0.22 2022-09-23 15:34:18 +01:00
cf99069804 fix: Handle minor lint errors 2022-09-23 15:33:27 +01:00
6abb761724 refactor: Move some logic to ManagerAPI and PatcherAPI 2022-09-23 15:31:24 +01:00
4609ed9eba fix: Make sure we are getting app from from storage when selection is from storage 2022-09-23 15:21:58 +01:00
c0f743df89 feat: HTTP2 support for api requests. (#290) 2022-09-23 19:06:06 +05:30
f00f910973 Merge branch 'flutter' of https://github.com/revanced/revanced-manager into flutter 2022-09-23 14:58:19 +05:30
2bc486dcec build: Bump version to v0.0.21. 2022-09-23 14:58:07 +05:30
148981da18 refactor: removed dropdown for branch. 2022-09-23 14:57:05 +05:30
d8df377427 feat: warning dialog when selecting all patches. 2022-09-23 14:53:21 +05:30
efe1306aac fix: reverse versions list to have recents first. 2022-09-23 13:35:47 +05:30
ead308740f refactor: move custom button to shared dir. 2022-09-23 13:00:30 +05:30
144 changed files with 13618 additions and 2518 deletions

View File

@ -20,14 +20,6 @@ body:
- Other
validations:
required: true
- type: dropdown
attributes:
label: Manager Branch
options:
- Flutter
- Compose
validations:
required: true
- type: textarea
attributes:
label: Bug description
@ -37,7 +29,7 @@ body:
- type: textarea
attributes:
label: Steps to reproduce
description: Add the steps to reproduce this bug including your environment.
description: Add the steps to reproduce this bug, including your environment.
placeholder: Step 1. Download some files. Step 2. ...
validations:
required: true
@ -82,20 +74,20 @@ body:
- type: textarea
attributes:
label: Device logs (exported using Manager settings).
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so there is no need for backticks.
render: shell
validations:
required: true
- type: textarea
attributes:
label: Installer logs (exported using Installer menu option) [unneeded if issue is not during patching].
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
label: Installer logs (exported using Installer menu option) [unneeded if the issue is not during patching].
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so there is no need for backticks.
render: shell
validations:
required: false
- type: textarea
attributes:
label: Screenshots or videos
label: Screenshots or video
description: Add screenshots or videos that show the bug here.
placeholder: Drag and drop the screenshots/videos into this box.
validations:
@ -113,16 +105,16 @@ body:
validations:
required: false
- type: checkboxes
id: acknowledgements
id: acknowledgments
attributes:
label: Acknowledgements
label: Acknowledgments
description: Your issue will be closed if you haven't done these steps.
options:
- label: I have searched the existing issues and this is a new and no duplicate or related to another open issue.
- label: I have searched the existing issues; this is new and no duplicate or related to another open issue.
required: true
- label: I have written a short but informative title.
required: true
- label: I filled out all of the requested information in this issue properly.
- label: I properly filled out all of the requested information in this issue.
required: true
- label: The issue is related solely to the ReVanced Manager
- label: The issue is solely related to ReVanced Manager and not caused by patches.
required: true

View File

@ -12,14 +12,6 @@ body:
- Other
validations:
required: true
- type: dropdown
attributes:
label: Manager Branch
options:
- Flutter
- Compose
validations:
required: true
- type: textarea
attributes:
label: Issue

2
.github/config.yaml vendored Normal file
View File

@ -0,0 +1,2 @@
firstPRMergeComment: >
Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) if you want to receive a contributor role.

38
.github/workflows/analyze.yml vendored Normal file
View File

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

View File

@ -1,39 +0,0 @@
name: "Debug Build"
on:
push:
branches:
- "flutter-disabled"
tags-ignore:
- "*"
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up JDK 12
uses: actions/setup-java@v1
with:
java-version: '12.x'
- uses: subosito/flutter-action@v1
with:
channel: 'stable'
- name: Set up Flutter
run: flutter pub get
- name: Generate files with Builder
run: flutter packages pub run build_runner build --delete-conflicting-outputs
- name: Build with Flutter
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: flutter build apk --debug
- name: Add version to APK
run: mv build/app/outputs/flutter-apk/app-debug.apk revanced-manager-latest.apk
- name: Publish debug APK
uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
automatic_release_tag: "latest"
prerelease: true
title: "Development Build"
files: revanced-manager-latest.apk

45
.github/workflows/pr-build.yml vendored Normal file
View File

@ -0,0 +1,45 @@
name: PR Build
on:
pull_request:
paths:
- ".github/workflows/pr-build.yml"
- "android/**"
- "assets/**"
- "lib/**"
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
# Make sure the release step uses its own credentials:
# https://github.com/cycjimmy/semantic-release-action#private-packages
persist-credentials: false
fetch-depth: 0
- name: Setup JDK
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'zulu'
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
cache: true
- name: Install Flutter dependencies
run: flutter pub get
- name: Generate files with Builder
run: flutter packages pub run build_runner build --delete-conflicting-outputs
- name: Build with Flutter
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: flutter build apk --debug
- name: Upload build
uses: actions/upload-artifact@v3
with:
name: revanced-manager
path: build/app/outputs/flutter-apk/app-debug.apk

View File

@ -9,16 +9,17 @@ jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v3
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Set up JDK 12
uses: actions/setup-java@v1
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '12.x'
- uses: subosito/flutter-action@v1
java-version: "11"
distribution: "zulu"
- uses: subosito/flutter-action@v2
with:
channel: 'stable'
channel: "stable"
- name: Set up Flutter
run: flutter pub get
- name: Generate files with Builder
@ -32,18 +33,18 @@ jobs:
run: flutter build apk
- name: Sign APK
id: sign_apk
uses: r0adkll/sign-android-release@v1
uses: ilharp/sign-android-release@v1
with:
releaseDirectory: build/app/outputs/apk/release
signingKeyBase64: ${{ secrets.SIGNING_KEYSTORE }}
releaseDir: build/app/outputs/apk/release
signingKey: ${{ secrets.SIGNING_KEYSTORE }}
keyStorePassword: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
alias: ${{ secrets.SIGNING_KEY_ALIAS }}
keyAlias: ${{ secrets.SIGNING_KEY_ALIAS }}
keyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
- name: Add version to APK
run: mv ${{steps.sign_apk.outputs.signedReleaseFile}} revanced-manager-${{ env.RELEASE_VERSION }}.apk
run: mv ${{steps.sign_apk.outputs.signedFile}} revanced-manager-${{ env.RELEASE_VERSION }}.apk
- name: Publish release APK
uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
prerelease: false
files: revanced-manager-${{ env.RELEASE_VERSION }}.apk
files: revanced-manager-${{ env.RELEASE_VERSION }}.apk

View File

@ -0,0 +1,19 @@
name: Update documentation
on:
push:
paths:
- docs/**
jobs:
trigger:
runs-on: ubuntu-latest
name: Dispatch event to documentation repository
if: github.ref == 'refs/heads/main'
steps:
- uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }}
repository: revanced/revanced-documentation
event-type: update-documentation
client-payload: '{"repo": "${{ github.event.repository.name }}", "ref": "${{ github.ref }}"}'

8
.gitignore vendored
View File

@ -134,5 +134,11 @@ app.*.map.json
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
!/dev/ci/**/Gemfile.lock
Firebase related
# Firebase related
.firebase
# Dependency directories
node_modules/
# FVM
.fvm

75
.releaserc Normal file
View File

@ -0,0 +1,75 @@
{
"branches": [
"main",
{
"name": "dev",
"prerelease": true
}
],
"plugins": [
"semantic-release-export-data",
"@semantic-release/commit-analyzer",
[
"@semantic-release/release-notes-generator",
{
"presetConfig": {
"types": [
{
"type": "build",
"section": "Dependency Updates"
},
{
"type": "chore",
"section": "Other Changes",
"hidden": false
},
{
"type": "perf",
"section": "Performance Improvements",
"hidden": false
},
{
"type": "refactor",
"section": "Code Improvements",
"hidden": false
}
]
}
}
],
"@semantic-release/changelog",
"semantic-release-flutter-plugin",
[
"@semantic-release/git",
{
"assets": [
"CHANGELOG.md",
"pubspec.yaml"
]
}
],
[
"@semantic-release/github",
{
"assets": [
{
"path": "build/app/outputs/apk/release/revanced-manager-*.apk"
}
],
"successComment": false
}
],
[
"@saithodev/semantic-release-backmerge",
{
"backmergeBranches": [
{
"from": "main",
"to": "dev"
}
],
"clearWorkspace": true
}
]
]
}

6
.run/main.dart.run.xml Normal file
View File

@ -0,0 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="main.dart" type="FlutterRunConfigurationType" factoryName="Flutter">
<option name="filePath" value="$PROJECT_DIR$/lib/main.dart" />
<method v="2" />
</configuration>
</component>

1
CHANGELOG.md Normal file
View File

@ -0,0 +1 @@

View File

@ -3,18 +3,29 @@
The official ReVanced Manager based on Flutter.
## 🔽 Download
To download the Alpha version of Manager, go [here](https://github.com/revanced/revanced-manager/releases/latest) and install the provided APK file.
To download latest Manager, go [here](https://github.com/revanced/revanced-manager/releases/latest) and install the provided APK file.
## 📝 Prerequisites
1. Android 8 or higher
2. Does not work on some armv7 devices
## 🔴 Issues
For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose).
For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose).
## 💭 Discussion
If you wish to discuss the Manager, a thread has been made under the [#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.
If you wish to discuss the Manager, a thread has been made under the [#development](https://discord.com/channels/952946952348270622/1002922226443632761) channel in the Discord server, please note that this thread may be temporary and may be removed in the future.
## ⚠️ 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.*
## Prerequisites
1. Android 8 or higher.
2. Does not work on armv7
3. For YouTube and YouTube Music - Vanced MicroG(Only for non-root).
## 🌐 Translation
[![Crowdin](https://badges.crowdin.net/revanced/localized.svg)](https://crowdin.com/project/revanced)
If you wish to translate ReVanced Manager, we're accepting translations on [Crowdin](https://translate.revanced.app)
## 🛠️ Building Manager from source
1. Setup flutter environment for your [platform](https://docs.flutter.dev/get-started/install)
2. Clone the repository locally
3. Add your github token in gradle.properties like [this](/docs/4_building.md)
4. Open the project in terminal
5. Run `flutter pub get` in terminal
6. Then `flutter packages pub run build_runner build --delete-conflicting-outputs` (Must be done on each git pull)
7. To build release apk run `flutter build apk`

View File

@ -9,21 +9,155 @@
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
analyzer:
exclude:
- lib/app/app.locator.dart
- lib/app/app.router.dart
- lib/models/patch.g.dart
- lib/models/patched_application.g.dart
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
linter:
rules:
- always_declare_return_types
- require_trailing_commas
- always_put_control_body_on_new_line
- always_require_non_null_named_parameters
- always_use_package_imports # we do this commonly
- annotate_overrides
- avoid_bool_literals_in_conditional_expressions
- avoid_double_and_int_checks
- avoid_empty_else
- avoid_equals_and_hash_code_on_mutable_classes
- avoid_escaping_inner_quotes
- avoid_field_initializers_in_const_classes
- avoid_function_literals_in_foreach_calls
- avoid_implementing_value_types
- avoid_init_to_null
- avoid_js_rounded_ints
- avoid_null_checks_in_equality_operators
- avoid_print
- avoid_redundant_argument_values
- avoid_relative_lib_imports
- avoid_renaming_method_parameters
- avoid_return_types_on_setters
- avoid_returning_null
- avoid_returning_null_for_future
- avoid_returning_null_for_void
- avoid_setters_without_getters
- avoid_shadowing_type_parameters
- avoid_single_cascade_in_expression_statements
- avoid_type_to_string
- avoid_types_as_parameter_names
- avoid_unnecessary_containers
- avoid_void_async
- avoid_web_libraries_in_flutter # we use web libraries in web-specific code, and our tests prevent us from using them elsewhere
- await_only_futures
- camel_case_extensions
- camel_case_types
- cancel_subscriptions
- cast_nullable_to_non_nullable
- close_sinks # not reliable enough
- control_flow_in_finally
- curly_braces_in_flow_control_structures
- depend_on_referenced_packages
- deprecated_consistency
- directives_ordering
- empty_catches
- empty_constructor_bodies
- empty_statements
- eol_at_end_of_file
- exhaustive_cases
- file_names
- flutter_style_todos
- hash_and_equals
- implementation_imports
- collection_methods_unrelated_type
- leading_newlines_in_multiline_strings
- library_names
- library_prefixes
- library_private_types_in_public_api
- missing_whitespace_between_adjacent_strings
- no_adjacent_strings_in_list
- no_duplicate_case_values
- no_logic_in_create_state
- non_constant_identifier_names
- noop_primitive_operations
- null_check_on_nullable_type_parameter
- null_closures
- overridden_fields
- package_api_docs
- package_names
- package_prefixed_library_names
- prefer_adjacent_string_concatenation
- prefer_asserts_in_initializer_lists
- prefer_collection_literals
- prefer_conditional_assignment
- prefer_const_constructors
- prefer_const_constructors_in_immutables
- prefer_const_declarations
- prefer_const_literals_to_create_immutables
- prefer_contains
- prefer_final_fields
- prefer_final_in_for_each
- prefer_final_locals
- prefer_for_elements_to_map_fromIterable
- prefer_foreach
- prefer_function_declarations_over_variables
- prefer_generic_function_type_aliases
- prefer_if_elements_to_conditional_expressions
- prefer_if_null_operators
- prefer_initializing_formals
- prefer_inlined_adds
- prefer_interpolation_to_compose_strings
- prefer_is_empty
- prefer_is_not_empty
- prefer_is_not_operator
- prefer_iterable_whereType
- prefer_mixin # Has false positives, see https://github.com/dart-lang/linter/issues/3018
- prefer_null_aware_method_calls # "call()" is confusing to people new to the language since it's not documented anywhere
- prefer_null_aware_operators
- prefer_single_quotes
- prefer_spread_collections
- prefer_typing_uninitialized_variables
- prefer_void_to_null
- provide_deprecation_message
- recursive_getters
- sized_box_for_whitespace
- slash_for_doc_comments
- sort_child_properties_last
- sort_constructors_first
- sort_unnamed_constructors_first
- test_types_in_equals
- throw_in_finally
- tighten_type_of_initializing_formals
- type_init_formals
- unnecessary_brace_in_string_interps
- unnecessary_const
- unnecessary_getters_setters
- unnecessary_new
- unnecessary_null_aware_assignments
- unnecessary_null_checks
- unnecessary_null_in_if_null_operators
- unnecessary_nullable_for_final_variable_declarations
- unnecessary_overrides
- unnecessary_parenthesis
- unnecessary_statements
- unnecessary_string_escapes
- unnecessary_string_interpolations
- unnecessary_this
- unrelated_type_equality_checks
- unsafe_html
- use_build_context_synchronously
- use_full_hex_values_for_flutter_colors
- use_function_type_syntax_for_parameters
- use_if_null_to_convert_nulls_to_bools
- use_is_even_rather_than_modulo
- use_key_in_widget_constructors
- use_late_for_private_fields_and_variables
- use_named_constants
- use_raw_strings
- use_rethrow_when_possible
- use_setters_to_change_properties
- use_test_throws_matchers
- valid_regexps
- void_checks

3
android/Gemfile Normal file
View File

@ -0,0 +1,3 @@
source "https://rubygems.org"
gem "fastlane"

View File

@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 33
compileSdkVersion flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
compileOptions {
@ -54,7 +54,21 @@ android {
release {
shrinkResources false
minifyEnabled false
resValue "string", "app_name", "ReVanced Manager"
signingConfig signingConfigs.debug
ndk {
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
}
}
debug {
shrinkResources false
minifyEnabled false
resValue "string", "app_name", "ReVanced Manager Debug"
applicationIdSuffix ".debug"
signingConfig signingConfigs.debug
ndk {
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
}
}
}
@ -71,7 +85,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// ReVanced
implementation "app.revanced:revanced-patcher:5.0.1"
implementation "app.revanced:revanced-patcher:14.2.1"
// Signing & aligning
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")

View File

@ -2,22 +2,31 @@
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.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<application
android:label="ReVanced Manager"
android:label="@string/app_name"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:largeHeap="true"
android:extractNativeLibs="true">
android:requestLegacyExternalStorage="true"
android:extractNativeLibs="true"
android:enableOnBackInvokedCallback="true">
<activity
android:name=".MainActivity"
android:exported="true"
@ -28,8 +37,7 @@
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
android:resource="@style/NormalTheme"/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -2,34 +2,47 @@ package app.revanced.manager.flutter
import android.os.Handler
import android.os.Looper
import androidx.annotation.NonNull
import app.revanced.manager.flutter.utils.Aapt
import app.revanced.manager.flutter.utils.aligning.ZipAligner
import app.revanced.manager.flutter.utils.signing.Signer
import app.revanced.manager.flutter.utils.zip.ZipFile
import app.revanced.manager.flutter.utils.zip.structures.ZipEntry
import app.revanced.patcher.PatchBundleLoader
import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherOptions
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.logging.Logger
import app.revanced.patcher.util.patch.impl.DexPatchBundle
import dalvik.system.DexClassLoader
import app.revanced.patcher.patch.PatchResult
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.cancel
import kotlinx.coroutines.runBlocking
import java.io.File
import java.io.PrintWriter
import java.io.StringWriter
import java.util.logging.LogRecord
import java.util.logging.Logger
private const val PATCHER_CHANNEL = "app.revanced.manager.flutter/patcher"
private const val INSTALLER_CHANNEL = "app.revanced.manager.flutter/installer"
class MainActivity : FlutterActivity() {
private val handler = Handler(Looper.getMainLooper())
private lateinit var installerChannel: MethodChannel
private var cancel: Boolean = false
private var stopResult: MethodChannel.Result? = null
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
val mainChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, PATCHER_CHANNEL)
installerChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, INSTALLER_CHANNEL)
val patcherChannel = "app.revanced.manager.flutter/patcher"
val installerChannel = "app.revanced.manager.flutter/installer"
val mainChannel =
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, patcherChannel)
this.installerChannel =
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, installerChannel)
mainChannel.setMethodCallHandler { call, result ->
when (call.method) {
"runPatcher" -> {
@ -41,9 +54,9 @@ class MainActivity : FlutterActivity() {
val integrationsPath = call.argument<String>("integrationsPath")
val selectedPatches = call.argument<List<String>>("selectedPatches")
val cacheDirPath = call.argument<String>("cacheDirPath")
val mergeIntegrations = call.argument<Boolean>("mergeIntegrations")
val resourcePatching = call.argument<Boolean>("resourcePatching")
val keyStoreFilePath = call.argument<String>("keyStoreFilePath")
val keystorePassword = call.argument<String>("keystorePassword")
if (patchBundleFilePath != null &&
originalFilePath != null &&
inputFilePath != null &&
@ -52,10 +65,10 @@ class MainActivity : FlutterActivity() {
integrationsPath != null &&
selectedPatches != null &&
cacheDirPath != null &&
mergeIntegrations != null &&
resourcePatching != null &&
keyStoreFilePath != null
keyStoreFilePath != null &&
keystorePassword != null
) {
cancel = false
runPatcher(
result,
patchBundleFilePath,
@ -66,14 +79,17 @@ class MainActivity : FlutterActivity() {
integrationsPath,
selectedPatches,
cacheDirPath,
mergeIntegrations,
resourcePatching,
keyStoreFilePath
keyStoreFilePath,
keystorePassword
)
} else {
result.notImplemented()
}
} else result.notImplemented()
}
"stopPatcher" -> {
cancel = true
stopResult = result
}
else -> result.notImplemented()
}
}
@ -89,9 +105,8 @@ class MainActivity : FlutterActivity() {
integrationsPath: String,
selectedPatches: List<String>,
cacheDirPath: String,
mergeIntegrations: Boolean,
resourcePatching: Boolean,
keyStoreFilePath: String
keyStoreFilePath: String,
keystorePassword: String
) {
val originalFile = File(originalFilePath)
val inputFile = File(inputFilePath)
@ -99,122 +114,142 @@ class MainActivity : FlutterActivity() {
val outFile = File(outFilePath)
val integrations = File(integrationsPath)
val keyStoreFile = File(keyStoreFilePath)
val cacheDir = File(cacheDirPath)
Thread {
try {
val patches = DexPatchBundle(
patchBundleFilePath,
DexClassLoader(
patchBundleFilePath,
cacheDirPath,
null,
javaClass.classLoader
)
).loadPatches().filter { patch -> selectedPatches.any { it == patch.patchName } }
fun updateProgress(progress: Double, header: String, log: String) {
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 0.1,
"header" to "",
"log" to "Copying original apk"
"progress" to progress,
"header" to header,
"log" to log
)
)
}
}
fun postStop() = handler.post { stopResult!!.success(null) }
// Setup logger
Logger.getLogger("").apply {
handlers.forEach {
it.close()
removeHandler(it)
}
object : java.util.logging.Handler() {
override fun publish(record: LogRecord) =
updateProgress(-1.0, "", record.message)
override fun flush() = Unit
override fun close() = flush()
}.let(::addHandler)
}
try {
updateProgress(0.0, "", "Copying APK")
if (cancel) {
postStop()
return@Thread
}
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,
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.4,
"header" to "Merging integrations...",
"log" to "Merging integrations"
)
)
}
patcher.addFiles(listOf(integrations)) {}
if (cancel) {
postStop()
return@Thread
}
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 0.5,
"header" to "Applying patches...",
"log" to ""
)
updateProgress(0.05, "Reading APK...", "Reading APK")
val patcher = Patcher(
PatcherOptions(
inputFile,
cacheDir,
Aapt.binary(applicationContext).absolutePath,
cacheDir.path,
)
)
if (cancel) {
postStop()
return@Thread
}
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
)
)
updateProgress(0.1, "Loading patches...", "Loading patches")
val patches = PatchBundleLoader.Dex(
File(patchBundleFilePath)
).filter { patch ->
val isCompatible = patch.compatiblePackages?.any {
it.name == patcher.context.packageMetadata.packageName
} ?: false
val compatibleOrUniversal =
isCompatible || patch.compatiblePackages.isNullOrEmpty()
compatibleOrUniversal && selectedPatches.any { it == patch.patchName }
}
if (cancel) {
postStop()
return@Thread
}
updateProgress(0.15, "Executing...", "")
// Update the progress bar every time a patch is executed from 0.15 to 0.7
val totalPatchesCount = patches.size
val progressStep = 0.55 / totalPatchesCount
var progress = 0.15
patcher.apply {
acceptIntegrations(listOf(integrations))
acceptPatches(patches)
runBlocking {
apply(false).collect { patchResult: PatchResult ->
if (cancel) {
handler.post { stopResult!!.success(null) }
this.cancel()
this@apply.close()
return@collect
}
val msg = patchResult.exception?.let {
val writer = StringWriter()
it.printStackTrace(PrintWriter(writer))
"${patchResult.patchName} failed: $writer"
} ?: run {
"${patchResult.patchName} succeeded"
}
updateProgress(progress, "", msg)
progress += progressStep
}
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"
)
)
if (cancel) {
postStop()
patcher.close()
return@Thread
}
val res = patcher.save()
updateProgress(0.8, "Building...", "")
val res = patcher.get()
patcher.close()
ZipFile(patchedFile).use { file ->
res.dexFiles.forEach {
if (cancel) {
postStop()
return@Thread
}
file.addEntryCompressData(
ZipEntry.createWithName(it.name),
it.stream.readBytes()
@ -231,81 +266,35 @@ class MainActivity : FlutterActivity() {
ZipAligner::getEntryAlignment
)
}
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 0.9,
"header" to "Signing apk...",
"log" to ""
)
)
}
Signer("ReVanced", "s3cur3p@ssw0rd").signApk(patchedFile, outFile, keyStoreFile)
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 1.0,
"header" to "Finished!",
"log" to "Finished!"
)
)
if (cancel) {
postStop()
return@Thread
}
updateProgress(0.9, "Signing...", "Signing APK")
try {
Signer("ReVanced", keystorePassword)
.signApk(patchedFile, outFile, keyStoreFile)
} catch (e: Exception) {
print("Error signing APK: ${e.message}")
e.printStackTrace()
}
updateProgress(1.0, "Patched", "Patched")
} 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"
)
if (!cancel) {
val stack = ex.stackTraceToString()
updateProgress(
-100.0,
"Aborted",
"An error occurred:\n$stack"
)
}
}
handler.post { result.success(null) }
}.start()
}
inner class ManagerLogger : Logger {
override fun error(msg: String) {
handler.post {
installerChannel
.invokeMethod(
"update",
mapOf("progress" to -1.0, "header" to "", "log" to msg)
)
}
}
override fun warn(msg: String) {
handler.post {
installerChannel.invokeMethod(
"update",
mapOf("progress" to -1.0, "header" to "", "log" to msg)
)
}
}
override fun info(msg: String) {
handler.post {
installerChannel.invokeMethod(
"update",
mapOf("progress" to -1.0, "header" to "", "log" to msg)
)
}
}
override fun trace(msg: String) {
handler.post {
installerChannel.invokeMethod(
"update",
mapOf("progress" to -1.0, "header" to "", "log" to msg)
)
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 814 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="256"
android:viewportHeight="256">
<group android:scaleX="0.23"
android:scaleY="0.23"
android:translateX="98.56"
android:translateY="98.56">
<path
android:pathData="M253.85,4.9C254.32,3.82 254.22,2.57 253.58,1.58C252.93,0.6 251.83,0 250.64,0C243.29,0 230.47,0 225.95,0C224.96,0 224.06,0.59 223.66,1.5C216.03,18.88 144.1,182.7 130.29,214.16C129.89,215.07 128.99,215.66 128,215.66C127.01,215.66 126.11,215.07 125.71,214.16C111.9,182.7 39.97,18.88 32.34,1.5C31.94,0.59 31.04,0 30.05,0C25.53,0 12.71,0 5.36,0C4.17,0 3.07,0.6 2.42,1.58C1.78,2.57 1.68,3.82 2.15,4.9C16.78,38.3 101.47,231.61 111.24,253.9C111.8,255.18 113.06,256 114.45,256C120.29,256 135.71,256 141.55,256C142.94,256 144.2,255.18 144.76,253.9C154.52,231.61 239.22,38.3 253.85,4.9Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M130.59,131.75C130.06,132.68 129.07,133.25 128,133.25C126.93,133.25 125.94,132.68 125.4,131.75C113.45,111.06 63.88,25.19 51.93,4.5C51.4,3.57 51.4,2.43 51.93,1.5C52.47,0.57 53.46,-0 54.53,-0L201.47,-0C202.54,-0 203.53,0.57 204.06,1.5C204.6,2.43 204.6,3.57 204.06,4.5C192.12,25.19 142.54,111.06 130.59,131.75Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="128"
android:startY="-0"
android:endX="128"
android:endY="254.6"
android:type="linear">
<item android:offset="0" android:color="#FFF04E98"/>
<item android:offset="0.5" android:color="#FF5F65D4"/>
<item android:offset="1" android:color="#FF4E98F0"/>
</gradient>
</aapt:attr>
</path>
</group>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#1B1B1B</color>
</resources>
</resources>

View File

@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.7.10'
ext.kotlin_version = '1.9.0'
repositories {
google()
mavenCentral()
@ -22,6 +22,7 @@ allprojects {
password = (project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN")) as String
}
}
mavenLocal()
}
}
@ -31,6 +32,6 @@ subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@ -1,3 +1,6 @@
org.gradle.jvmargs=-Xmx1536M
org.gradle.jvmargs=-Xmx1536M -XX:+UseParallelGC
org.gradle.parallel=true
org.gradle.daemon=true
org.gradle.caching=true
android.useAndroidX=true
android.enableJetifier=true

View File

@ -1,6 +1,7 @@
#Mon May 09 12:07:41 MSK 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-rc-1-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
distributionSha256Sum=a01b6587e15fe7ed120a0ee299c25982a1eee045abd6a9dd5e216b2f628ef9ac
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -1,159 +0,0 @@
{
"okButton": "OK",
"cancelButton": "Cancel",
"enabledLabel": "Enabled",
"disabledLabel": "Disabled",
"yesButton": "Yes",
"noButton": "No",
"navigationView": {
"dashboardTab": "Dashboard",
"patcherTab": "Patcher",
"settingsTab": "Settings"
},
"homeView": {
"widgetTitle": "Dashboard",
"updatesSubtitle": "Updates",
"patchedSubtitle": "Patched Applications",
"updatesAvailable": "Updates available",
"noUpdates": "No updates available",
"noInstallations": "No patched applications installed",
"installed": "Installed",
"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...",
"errorDownloadMessage": "Unable to download update",
"errorInstallMessage": "Unable to install update",
"noConnection": "No internet connection"
},
"applicationItem": {
"patchButton": "Patch",
"infoButton": "Info",
"changelogLabel": "Changelog"
},
"latestCommitCard": {
"loadingLabel": "Loading...",
"timeagoLabel": "{time} ago",
"patcherLabel": "Patcher: ",
"managerLabel": "Manager: ",
"updateButton": "Update Manager"
},
"patcherView": {
"widgetTitle": "Patcher",
"patchButton": "Patch",
"patchDialogTitle": "Warning",
"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.",
"currentVersion": "Current",
"recommendedVersion": "Recommended",
"anyVersion": "any"
},
"patchSelectorCard": {
"widgetTitle": "Select patches",
"widgetTitleSelected": "Selected patches",
"widgetSubtitle": "Select an application first",
"widgetEmptySubtitle": "No patches selected"
},
"socialMediaCard": {
"widgetTitle": "Socials",
"widgetSubtitle": "We are online!"
},
"appSelectorView": {
"viewTitle": "Select application",
"searchBarHint": "Search applications",
"storageButton": "Storage",
"errorMessage": "Unable to use selected application."
},
"patchesSelectorView": {
"viewTitle": "Select patches",
"searchBarHint": "Search patches",
"doneButton": "Done",
"noPatchesFound": "No patches found for the selected app"
},
"patchItem": {
"unsupportedWarningButton": "Unsupported version",
"unsupportedDialogTitle": "Warning",
"unsupportedDialogText": "Selecting this patch may result in patching errors.\n\nApp version: {packageVersion}\nCurrent supported versions:\n{supportedVersions}"
},
"installerView": {
"widgetTitle": "Installer",
"installButton": "Install",
"installRootButton": "Install as Root",
"openButton": "Open",
"shareButton": "Share file",
"notificationTitle": "ReVanced Manager is patching",
"notificationText": "Tap to return to the installer",
"shareApkMenuOption": "Share APK",
"shareLogMenuOption": "Share log",
"installErrorDialogTitle": "Error",
"installErrorDialogText1": "Root install is not possible with the current patches selection.\nRepatch your app or choose non-root install.",
"installErrorDialogText2": "Non-root install is not possible with the current patches selection.\nRepatch your app or choose root install if you have your device rooted.",
"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",
"teamSectionTitle": "Team",
"infoSectionTitle": "Info",
"advancedSectionTitle": "Advanced",
"darkThemeLabel": "Dark Mode",
"darkThemeHint": "Welcome to the Dark Side",
"dynamicThemeLabel": "Material You",
"dynamicThemeHint": "Enjoy an experience closer to your device",
"languageLabel": "Language",
"englishOption": "English",
"frenchOption": "French",
"sourcesLabel": "Sources",
"sourcesLabelHint": "Configure your custom sources",
"orgPatchesLabel" : "Patches Organization",
"sourcesPatchesLabel" : "Patches Source",
"orgIntegrationsLabel": "Integrations Organization",
"sourcesIntegrationsLabel": "Integrations Source",
"sourcesResetDialogTitle": "Reset",
"sourcesResetDialogText": "Are you sure you want to reset custom sources to their default values?",
"apiURLResetDialogText": "Are you sure you want to reset API URL to its default value?",
"contributorsLabel": "Contributors",
"contributorsHint": "A list of contributors of ReVanced",
"logsLabel": "Logs",
"logsHint": "Share device debug logs",
"apiURLLabel": "API URL",
"apiURLHint": "Configure your custom API URL",
"selectApiURL": "Select URL",
"aboutLabel": "About",
"snackbarMessage": "Copied to clipboard"
},
"appInfoView": {
"widgetTitle": "App Info",
"openButton": "Open",
"uninstallButton": "Uninstall",
"patchButton": "Patch",
"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",
"installTypeLabel": "Installation Type",
"rootTypeLabel": "Root",
"nonRootTypeLabel": "Non-root",
"patchedDateLabel": "Patched Date",
"patchedDateHint": "{date} at {time}",
"appliedPatchesLabel": "Applied Patches",
"appliedPatchesHint": "{quantity} applied patches"
},
"contributorsView": {
"widgetTitle": "Contributors",
"patcherContributors": "Patcher Contributors",
"patchesContributors": "Patches Contributors",
"integrationsContributors": "Integrations Contributors",
"cliContributors": "CLI Contributors",
"managerContributors": "Manager Contributors"
}
}

306
assets/i18n/en_US.json Normal file
View File

@ -0,0 +1,306 @@
{
"okButton": "OK",
"cancelButton": "Cancel",
"quitButton": "Quit",
"updateButton": "Update",
"enabledLabel": "Enabled",
"disabledLabel": "Disabled",
"installed":"Installed: {version}",
"suggested":"Suggested: {version}",
"yesButton": "Yes",
"noButton": "No",
"warning": "Warning",
"notice": "Notice",
"noShowAgain": "Don't show this again",
"new": "New",
"navigationView": {
"dashboardTab": "Dashboard",
"patcherTab": "Patcher",
"settingsTab": "Settings"
},
"homeView": {
"refreshSuccess": "Refreshed successfully",
"widgetTitle": "Dashboard",
"updatesSubtitle": "Updates",
"patchedSubtitle": "Patched applications",
"noUpdates": "No updates available",
"WIP": "Work in progress...",
"noInstallations": "No patched applications installed",
"installUpdate": "Continue to install the update?",
"updateDialogTitle": "Update Manager",
"updatePatchesDialogTitle": "Update ReVanced Patches",
"updateChangelogTitle": "Changelog",
"patchesConsentDialogText": "ReVanced Patches needs to be downloaded.",
"patchesConsentDialogText2": "This will connect you to {url}.",
"patchesConsentDialogText3": "Auto update?",
"patchesConsentDialogText3Sub": "You can change this in settings at a later time.",
"notificationTitle": "Update downloaded",
"notificationText": "Tap to install the update",
"downloadingMessage": "Downloading update...",
"downloadedMessage": "Update downloaded!",
"installingMessage": "Installing update...",
"errorDownloadMessage": "Unable to download update",
"errorInstallMessage": "Unable to install update",
"noConnection": "No internet connection",
"updatesDisabled": "Updating a patched app is currently disabled. Repatch the app again."
},
"applicationItem": {
"patchButton": "Patch",
"infoButton": "Info",
"changelogLabel": "Changelog"
},
"latestCommitCard": {
"loadingLabel": "Loading...",
"timeagoLabel": "{time} ago",
"patcherLabel": "Patcher: ",
"managerLabel": "Manager: ",
"updateButton": "Update Manager"
},
"patcherView": {
"widgetTitle": "Patcher",
"patchButton": "Patch",
"patchDialogText": "You have selected a resource patch and a split APK installation has been detected, so patching errors may occur.\nAre you sure you want to proceed?",
"armv7WarningDialogText": "Patching on ARMv7 devices is not yet supported and might fail. Proceed anyways?",
"splitApkWarningDialogText": "Patching a split APK is not yet supported and might fail. Proceed anyways?",
"removedPatchesWarningDialogText": "The following patches have been removed since the last time you used them.\n\n{patches}\n\nProceed anyways?"
},
"appSelectorCard": {
"widgetTitle": "Select an application",
"widgetTitleSelected": "Selected application",
"widgetSubtitle": "No application selected",
"noAppsLabel": "No applications found",
"notInstalled":"App not installed",
"currentVersion": "Current",
"suggestedVersion": "Suggested",
"allVersions": "All versions"
},
"patchSelectorCard": {
"widgetTitle": "Select patches",
"widgetTitleSelected": "Selected patches",
"widgetSubtitle": "Select an application first",
"widgetEmptySubtitle": "No patches selected"
},
"socialMediaCard": {
"widgetTitle": "Socials",
"widgetSubtitle": "We are online!"
},
"appSelectorView": {
"viewTitle": "Select an application",
"searchBarHint": "Search applications",
"storageButton": "Storage",
"selectFromStorageButton": "Select from storage",
"errorMessage": "Unable to use selected application",
"downloadToast": "Download function is not available yet",
"featureNotAvailable": "Feature not implemented",
"featureNotAvailableText": "This application is a split APK and cannot be selected. Unfortunately, this feature is only available for rooted users at the moment. However, you can still install the application by selecting its APK files from your device's storage instead"
},
"patchesSelectorView": {
"viewTitle": "Select patches",
"searchBarHint": "Search patches",
"universalPatches": "Universal patches",
"doneButton": "Done",
"default": "Default",
"defaultTooltip": "Select all default patches",
"none": "None",
"noneTooltip": "Deselect all patches",
"loadPatchesSelection": "Load patches selection",
"noSavedPatches": "No saved patches for the selected app.\nPress Done to save current selection.",
"noPatchesFound": "No patches found for the selected app",
"selectAllPatchesWarningContent": "You are about to select all patches, that includes non-suggested patches and can cause unwanted behavior."
},
"patchItem": {
"unsupportedDialogText": "Selecting this patch may result in patching errors.\n\nApp version: {packageVersion}\nSupported versions:\n{supportedVersions}",
"unsupportedPatchVersion": "Patch is not supported for this app version. Enable the experimental toggle in settings to proceed.",
"newPatchDialogText": "This is a new patch that has been added since the last time you have patched this app.",
"newPatch": "New patch",
"patchesChangeWarningDialogText": "It is recommended to use the default selection of patches because changing it may cause unexpected issues.\n\nIf you know what you are doing, you can enable \"Enable changing selection\" in the settings.",
"patchesChangeWarningDialogButton": "Use default selection"
},
"installerView": {
"widgetTitle": "Installer",
"installType": "Select install type",
"installTypeDescription": "Select the installation type to proceed with.",
"installButton": "Install",
"installRootType": "Root",
"installNonRootType": "Non-root",
"installRecommendedType": "Recommended",
"pressBackAgain": "Press back again to cancel",
"openButton": "Open",
"shareButton": "Share file",
"notificationTitle": "ReVanced Manager is patching",
"notificationText": "Tap to return to the installer",
"exportApkButtonTooltip": "Export patched APK",
"exportLogButtonTooltip": "Export 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, cannot exit..."
},
"settingsView": {
"widgetTitle": "Settings",
"appearanceSectionTitle": "Appearance",
"teamSectionTitle": "Team",
"infoSectionTitle": "Info",
"advancedSectionTitle": "Advanced",
"exportSectionTitle": "Import & export",
"logsSectionTitle": "Logs",
"darkThemeLabel": "Dark mode",
"darkThemeHint": "Welcome to the dark side",
"dynamicThemeLabel": "Material You",
"dynamicThemeHint": "Enjoy an experience closer to your device",
"languageLabel": "Language",
"englishOption": "English",
"sourcesLabel": "Sources",
"sourcesLabelHint": "Configure your custom sources",
"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?",
"sourcesUpdateNote": "Note: ReVanced Patches will be updated to the latest version automatically.\n\nThis will reveal your IP address to the server.",
"apiURLLabel": "API URL",
"apiURLHint": "Configure your custom API URL",
"selectApiURL": "API URL",
"hostRepositoryLabel": "Repository API",
"orgPatchesLabel": "Patches organization",
"sourcesPatchesLabel": "Patches source",
"orgIntegrationsLabel": "Integrations organization",
"contributorsLabel": "Contributors",
"contributorsHint": "A list of contributors of ReVanced",
"logsLabel": "Logs",
"logsHint": "Share Manager's logs",
"enablePatchesSelectionLabel": "Enable changing selection",
"enablePatchesSelectionHint": "Enable changing the selection of patches.",
"enablePatchesSelectionWarningText": "Changing the default selection of patches may cause unexpected issues.\n\nEnable anyways?",
"disablePatchesSelectionWarningText": "You are about to disable changing the selection of patches.\nThe default selection of patches will be restored.\n\nDisable anyways?",
"autoUpdatePatchesLabel": "Auto update patches",
"autoUpdatePatchesHint": "Automatically update ReVanced Patches to the latest version",
"experimentalUniversalPatchesLabel": "Experimental universal patches support",
"experimentalUniversalPatchesHint": "Display all applications to use with universal patches, loading list of apps may be slower",
"experimentalPatchesLabel": "Experimental patches support",
"experimentalPatchesHint": "Enable usage of unsupported patches in any app version",
"enabledExperimentalPatches": "Experimental patches support enabled",
"aboutLabel": "About",
"snackbarMessage": "Copied to clipboard",
"restartAppForChanges": "Restart the app to apply changes",
"deleteTempDirLabel": "Delete temporary files",
"deleteTempDirHint": "Delete unused temporary files",
"deletedTempDir": "Temporary files deleted",
"exportPatchesLabel": "Export patches selection",
"exportPatchesHint": "Export patches selection to a JSON file",
"exportedPatches": "Patches selection exported",
"noExportFileFound": "No patches selection to export",
"importPatchesLabel": "Import patches selection",
"importPatchesHint": "Import patches selection from a JSON file",
"importedPatches": "Patches selection imported",
"resetStoredPatchesLabel": "Reset patches",
"resetStoredPatchesHint": "Reset the stored patches selection",
"resetStoredPatchesDialogTitle": "Reset patches selection?",
"resetStoredPatchesDialogText": "Resetting patches selection will remove all selected patches.",
"resetStoredPatches": "Patches selection has been reset",
"deleteLogsLabel": "Delete logs",
"deleteLogsHint": "Delete collected manager logs",
"deletedLogs": "Logs deleted",
"regenerateKeystoreLabel": "Regenerate keystore",
"regenerateKeystoreHint": "Regenerate the keystore used to sign the app",
"regenerateKeystoreDialogTitle": "Regenerate keystore?",
"regenerateKeystoreDialogText": "Patched apps signed with the old keystore will no longer be able to update.",
"regeneratedKeystore": "Keystore regenerated",
"exportKeystoreLabel": "Export keystore",
"exportKeystoreHint": "Export keystore used to sign apps",
"exportedKeystore": "Keystore exported",
"noKeystoreExportFileFound": "No keystore to export",
"importKeystoreLabel": "Import keystore",
"importKeystoreHint": "Import keystore used to sign apps",
"importedKeystore": "Keystore imported",
"selectKeystorePassword": "Keystore Password",
"selectKeystorePasswordHint": "Select keystore password used to sign the apk",
"jsonSelectorErrorMessage": "Unable to use selected JSON file",
"keystoreSelectorErrorMessage": "Unable to use selected KEYSTORE file"
},
"appInfoView": {
"widgetTitle": "App info",
"openButton": "Open",
"uninstallButton": "Uninstall",
"unpatchButton": "Unpatch",
"rootDialogTitle": "Error",
"unpatchDialogText": "Are you sure you want to unpatch this app?",
"rootDialogText": "App was installed with superuser permissions, but currently ReVanced Manager has no permissions.\nPlease grant superuser permissions first.",
"packageNameLabel": "Package name",
"originalPackageNameLabel": "Original package name",
"installTypeLabel": "Installation type",
"rootTypeLabel": "Root",
"nonRootTypeLabel": "Non-root",
"patchedDateLabel": "Patched date",
"appliedPatchesLabel": "Applied patches",
"patchedDateHint": "{date} at {time}",
"appliedPatchesHint": "{quantity} applied patches",
"updateNotImplemented": "This feature has not been implemented yet"
},
"contributorsView": {
"widgetTitle": "Contributors",
"patcherContributors": "Patcher contributors",
"patchesContributors": "Patches contributors",
"integrationsContributors": "Integrations contributors",
"cliContributors": "CLI contributors",
"managerContributors": "Manager contributors"
}
}

View File

@ -1,3 +1,9 @@
project_id_env: CROWDIN_PROJECT_ID
api_token_env: CROWDIN_PERSONAL_TOKEN
commit_message: 'chore(i18n): sync translations'
preserve_hierarchy: true
files:
- source: /assets/i18n/en.json
translation: /assets/i18n/%two_letters_code%.json
- source: assets/i18n/en_US.json
translation: assets/i18n/%locale_with_underscore%.json

16
docs/0_prerequisites.md Normal file
View File

@ -0,0 +1,16 @@
# 💼 Prerequisites
In order to use ReVanced Manager, certain requirements must be met.
## 🤝 Requirements
- An Android device running Android 8 or higher
- Any device architecture except ARMv7[^1]
[^1]: This constraint only applies to patches, that require patching APK resources which is why some patches may or may not work on ARMv7 architecture. You can find out, which architectures your device supports here: [⚙️ Configuring ReVanced Manager](2_4_settings.md#%E2%84%B9%EF%B8%8F-about).
## ⏭️ What's next
The next page will guide you through patching an app.
Continue: [⬇️ Installation](1_installation.md)

14
docs/1_installation.md Normal file
View File

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

27
docs/2_1_patching.md Normal file
View File

@ -0,0 +1,27 @@
# 🧩 Patching apps
The following pages will guide you through using ReVanced Manager to patch apps.
## ✅ Steps to patch apps
1. Navigate to the **Patcher** tab from the bottom navigation bar
2. Tap on the **Select an app** card
3. Choose an app to patch[^1]
> **Note**: The suggested version is visible in each app's card.
4. Tap on the **Select patches** card and select the patches you want to apply[^2]
> **Warning**: If you see a warning you can click on it for more information.
5. Tap on the **Done** then **Patch** button
> **Warning**: The patching process may take ~5 minutes. Exiting the app may increase the time it takes to patch.
6. Tap on the **Install** button
> **Note**: If you are rooted, you can mount the patched app on top of the original app.[^3]
> Optionally, you may export the patched app to storage using the options in the top right corner.
[^1]: Non-root users may be prompted to select an APK from storage, in which case you have to source the APK file yourself. ReVanced does not provide any APK files.
[^2]: It is suggested to use the default set of patches by tapping on the **Default** button above the list of patches.
[^3]: Mounting the patched app on top of the original app will only work if the installed app version matches the version of the app selected in step 3. above.
## ⏭️ What's next
The next page will bring you back to the usage page.
Continue: [🛠️ Usage](2_usage.md)

15
docs/2_2_managing.md Normal file
View File

@ -0,0 +1,15 @@
# 🧰 Managing patched apps
After patching an app, you may want to manage it. This page will guide you through managing patched apps.
## ✅ Steps to manage patched apps
1. Tap on the **Dashboard** tab in the bottom navigation bar
2. Tap on the **Info** button for the app you want to manage
3. Choose one of the options from the menu
## ⏭️ What's next
The next page will bring you back to the usage page.
Continue: [🛠️ Usage](2_usage.md)

14
docs/2_3_updating.md Normal file
View File

@ -0,0 +1,14 @@
# 🔄 Updating ReVanced Manager
In order to keep up with the latest features and bug fixes, it is recommended to keep ReVanced Manager up to date.
## ✅ Updating steps
1. Navigate to the **Dashboard** tab from the bottom navigation bar
2. Tap on the **Update** button in the **Updates** section
## ⏭️ What's next
The next page will bring you back to the usage page.
Continue: [🛠️ Usage](2_usage.md)

39
docs/2_4_settings.md Normal file
View File

@ -0,0 +1,39 @@
# ⚙️ Configuring ReVanced Manager
ReVanced Manager has settings that can be configured to your liking.
## ⭐ Essential settings
- ### 🔗 API URL
Specify the URL of the API to use. This is used to fetch ReVanced Patches and update ReVanced Manager.
- ### 🧬 Sources
Override the API and change the source of ReVanced Patches.
- ### 🧪 Experimental ReVanced Patches support
Lift app version constraints from ReVanced Patches. This allows you to patch any version of an app, even if the patch is not explicitly compatible with it.
- ### 🧑‍🔬 Experimental universal support
This will show or hide ReVanced Patches, which are not meant for any app in particular but rather for all apps but may not work on all apps.
- ### 🔑 Export, import or delete keystore
Manage the keystore used to sign patched apps.
- ### 📄 Export, import or reset ReVanced Patches selection
Manage the ReVanced Patches selection. This is useful if you want to share your ReVanced Patches selection with others or reset it to the default selection.
- ### About
View information about your device and ReVanced Manager. This includes the version of ReVanced Manager and supported architectures of your device.
## ⏭️ What's next
The next page will bring you back to the usage page.
Continue: [🛠️ Usage](2_usage.md)

16
docs/2_usage.md Normal file
View File

@ -0,0 +1,16 @@
# 🛠️ Usage
The following pages will guide you through using ReVanced Manager to patch apps, manage patched apps, and update ReVanced Manager.
## 📖 Table of contents
1. [🧩 Patching apps](2_1_patching.md)
2. [🧰 Managing patched apps](2_2_managing.md)
3. [🔄 Updating ReVanced Manager](2_3_updating.md)
4. [⚙️ Configuring ReVanced Manager](2_4_settings.md)
## ⏭️ What's next
The next page will guide you through troubleshooting ReVanced Manager.
Continue: [❔ Troubleshooting](3_troubleshooting.md)

31
docs/3_troubleshooting.md Normal file
View File

@ -0,0 +1,31 @@
# ❔ Troubleshooting
In case you encounter any issues while using ReVanced Manager, please refer to this page for possible solutions.
- 💉 Patching fails with an error
Make sure ReVanced Manager is up to date by following [🔄 Updating ReVanced Manager](2_3_updating.md) and select the **Default** button when choosing patches.
- 💥 App not installed as package conflicts with an existing package
An existing installation of the app you're trying to patch is conflicting with the patched app. Uninstall the existing app before installing the patched app.
- ❗️ Error code `135`, `139` or `1` when patching the app
Your device is not supported. Refer to the [Prerequisites](0_prerequisites.md) page for supported devices.
Alternatively, you can use [ReVanced CLI](https://github.com/revanced/revanced-cli) to patch the app.
- 🚫 Non-root install is not possible with the current patches selection
Select the **Default** button when choosing patches.
- 🚨 Patched app crashes on launch
Select the **Default** button when choosing patches.
## ⏭️ What's next
The next page will teach you how to build ReVanced Manager from source.
Continue: [🔨 Building from source](4_building.md)

40
docs/4_building.md Normal file
View File

@ -0,0 +1,40 @@
# 🛠️ Building from source
This page will guide you through building ReVanced Manager from source.
1. Setup the Flutter environment for your [platform](https://docs.flutter.dev/get-started/install)
2. Clone the repository
```sh
git clone https://github.com/revanced/revanced-manager.git && cd revanced-manager
```
3. Create a GitHub personal access token with the `read:packages` scope [here](https://github.com/settings/tokens/new?scopes=read:packages&description=ReVanced)
4. Add your GitHub username and the token to `~/.gradle/gradle.properties`
```properties
gpr.user = YourUsername
gpr.key = ghp_longrandomkey
```
5. Get dependencies
```sh
flutter pub get
```
6. Delete conflicting outputs
```sh
flutter packages pub run build_runner build --delete-conflicting-outputs
```
> **Note**: Must be run every time you sync your local repository with the remote repository.
7. Build the APK
```sh
flutter build apk
```

21
docs/README.md Normal file
View File

@ -0,0 +1,21 @@
# 💊 ReVanced Manager
This documentation explains how to use [ReVanced Manager](https://github.com/revanced/revanced-manager).
## 📖 Table of contents
0. [💼 Prerequisites](0_prerequisites.md)
1. [⬇️ Installation](1_installation.md)
2. [🛠️ Usage](2_usage.md)
1. [🧩 Patching apps](2_1_patching.md)
2. [🧰 Managing patched apps](2_2_managing.md)
3. [🔄 Updating ReVanced Manager](2_3_updating.md)
4. [⚙️ Configuring ReVanced Manager](2_4_settings.md)
3. [❔ Troubleshooting](3_troubleshooting.md)
4. [🔨 Building from source](4_building.md)
## ⏭️ Start here
The next page will tell you about the prerequisites for using ReVanced Manager.
Continue: [💼 Prerequisites](0_prerequisites.md)

2
fastlane/Appfile Normal file
View File

@ -0,0 +1,2 @@
json_key_file("") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
package_name("app.revanced.manager.flutter") # e.g. com.krausefx.app

38
fastlane/Fastfile Normal file
View File

@ -0,0 +1,38 @@
# This file contains the fastlane.tools configuration
# You can find the documentation at https://docs.fastlane.tools
#
# For a list of all available actions, check out
#
# https://docs.fastlane.tools/actions
#
# For a list of all available plugins, check out
#
# https://docs.fastlane.tools/plugins/available-plugins
#
# Uncomment the line if you want fastlane to automatically update itself
# update_fastlane
default_platform(:android)
platform :android do
desc "Runs all the tests"
lane :test do
gradle(task: "test")
end
desc "Submit a new Beta Build to Crashlytics Beta"
lane :beta do
gradle(task: "clean assembleRelease")
crashlytics
# sh "your_script.sh"
# You can also use other beta testing services here
end
desc "Deploy a new version to the Google Play"
lane :deploy do
gradle(task: "clean assembleRelease")
upload_to_play_store
end
end

48
fastlane/README.md Normal file
View File

@ -0,0 +1,48 @@
fastlane documentation
----
# Installation
Make sure you have the latest version of the Xcode command line tools installed:
```sh
xcode-select --install
```
For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
# Available Actions
## Android
### android test
```sh
[bundle exec] fastlane android test
```
Runs all the tests
### android beta
```sh
[bundle exec] fastlane android beta
```
Submit a new Beta Build to Crashlytics Beta
### android deploy
```sh
[bundle exec] fastlane android deploy
```
Deploy a new version to the Google Play
----
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).

View File

@ -0,0 +1 @@
ReVanced Manager is an Android application that uses ReVanced Patcher to add, remove, and modify existing functionalities in Android applications

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 KiB

View File

@ -0,0 +1 @@
Patch your favorite apps, right on your device.

View File

@ -0,0 +1 @@
ReVanced Manager

20
fastlane/report.xml Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,26 +1,31 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:revanced_manager/app/app.locator.dart';
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/ui/theme/dynamic_theme_builder.dart';
import 'package:revanced_manager/ui/views/navigation/navigation_view.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:stacked_themes/stacked_themes.dart';
import 'package:timezone/data/latest.dart' as tz;
late SharedPreferences prefs;
Future main() async {
await ThemeManager.initialise();
await setupLocator();
WidgetsFlutterBinding.ensureInitialized();
await locator<ManagerAPI>().initialize();
String apiUrl = locator<ManagerAPI>().getApiUrl();
final String apiUrl = locator<ManagerAPI>().getApiUrl();
await locator<RevancedAPI>().initialize(apiUrl);
locator<GithubAPI>().initialize();
await locator<PatcherAPI>().initialize();
final String repoUrl = locator<ManagerAPI>().getRepoUrl();
locator<GithubAPI>().initialize(repoUrl);
tz.initializeTimeZones();
prefs = await SharedPreferences.getInstance();
runApp(const MyApp());
}
@ -29,18 +34,31 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// String rawLocale = prefs.getString('language') ?? 'en_US';
// String replaceLocale = rawLocale.replaceAll('_', '-');
// List<String> localeList = replaceLocale.split('-');
// Locale locale = Locale(localeList[0], localeList[1]);
const Locale locale = Locale('en', 'US');
return DynamicThemeBuilder(
title: 'ReVanced Manager',
home: const NavigationView(),
localizationsDelegates: [
FlutterI18nDelegate(
translationLoader: FileTranslationLoader(
fallbackFile: 'en',
fallbackFile: 'en_US',
forcedLocale: locale,
basePath: 'assets/i18n',
useCountryCode: true,
),
missingTranslationHandler: (key, locale) {
log(
'--> Missing translation: key: $key, languageCode: ${locale?.languageCode}',
);
},
),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate
GlobalWidgetsLocalizations.delegate,
],
);
}

View File

@ -5,23 +5,20 @@ part 'patch.g.dart';
@JsonSerializable()
class Patch {
final String name;
final String description;
final String version;
final bool excluded;
final List<String> dependencies;
final List<Package> compatiblePackages;
Patch({
required this.name,
required this.description,
required this.version,
required this.excluded,
required this.dependencies,
required this.compatiblePackages,
});
factory Patch.fromJson(Map<String, dynamic> json) => _$PatchFromJson(json);
final String name;
final String description;
final bool excluded;
final List<String> dependencies;
final List<Package> compatiblePackages;
Map<String, dynamic> toJson() => _$PatchToJson(this);
@ -37,9 +34,6 @@ class Patch {
@JsonSerializable()
class Package {
final String name;
final List<String> versions;
Package({
required this.name,
required this.versions,
@ -47,6 +41,8 @@ class Package {
factory Package.fromJson(Map<String, dynamic> json) =>
_$PackageFromJson(json);
final String name;
final List<String> versions;
Map toJson() => _$PackageToJson(this);
}

View File

@ -6,8 +6,26 @@ part 'patched_application.g.dart';
@JsonSerializable()
class PatchedApplication {
PatchedApplication({
required this.name,
required this.packageName,
required this.originalPackageName,
required this.version,
required this.apkFilePath,
required this.icon,
required this.patchDate,
this.isRooted = false,
this.isFromStorage = false,
this.hasUpdates = false,
this.appliedPatches = const [],
this.changelog = const [],
});
factory PatchedApplication.fromJson(Map<String, dynamic> json) =>
_$PatchedApplicationFromJson(json);
String name;
String packageName;
String originalPackageName;
String version;
final String apkFilePath;
@JsonKey(
@ -22,23 +40,6 @@ class PatchedApplication {
List<String> appliedPatches;
List<String> changelog;
PatchedApplication({
required this.name,
required this.packageName,
required this.version,
required this.apkFilePath,
required this.icon,
required this.patchDate,
this.isRooted = false,
this.isFromStorage = false,
this.hasUpdates = false,
this.appliedPatches = const [],
this.changelog = const [],
});
factory PatchedApplication.fromJson(Map<String, dynamic> json) =>
_$PatchedApplicationFromJson(json);
Map<String, dynamic> toJson() => _$PatchedApplicationToJson(this);
static Uint8List decodeBase64(String icon) => base64.decode(icon);

View File

@ -2,19 +2,25 @@ import 'dart:convert';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:dio/dio.dart';
import 'package:dio_http_cache_lts/dio_http_cache_lts.dart';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:injectable/injectable.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/services/manager_api.dart';
@lazySingleton
class GithubAPI {
final Dio _dio = Dio(BaseOptions(baseUrl: 'https://api.github.com'));
final DioCacheManager _dioCacheManager = DioCacheManager(CacheConfig());
final Options _cacheOptions = buildCacheOptions(
const Duration(days: 1),
maxStale: const Duration(days: 7),
late Dio _dio = Dio();
late final ManagerAPI _managerAPI = locator<ManagerAPI>();
final _cacheOptions = CacheOptions(
store: MemCacheStore(),
maxStale: const Duration(days: 1),
priority: CachePriority.high,
);
final Map<String, String> repoAppPath = {
'com.google.android.youtube': 'youtube',
'com.google.android.apps.youtube.music': 'music',
@ -23,24 +29,115 @@ class GithubAPI {
'com.zhiliaoapp.musically': 'tiktok',
'de.dwd.warnapp': 'warnwetter',
'com.garzotto.pflotsh.ecmwf_a': 'ecmwf',
'com.spotify.music': 'spotify',
};
void initialize() {
_dio.interceptors.add(_dioCacheManager.interceptor);
Future<void> initialize(String repoUrl) async {
try {
_dio = Dio(
BaseOptions(
baseUrl: repoUrl,
),
);
_dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
Future<void> clearAllCache() async {
await _dioCacheManager.clearAll();
try {
await _cacheOptions.store!.clean();
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
Future<Map<String, dynamic>?> _getLatestRelease(String repoName) async {
Future<Map<String, dynamic>?> getLatestRelease(
String repoName,
) async {
try {
var response = await _dio.get(
'/repos/$repoName/releases/latest',
options: _cacheOptions,
final response = await _dio.get(
'/repos/$repoName/releases',
);
return response.data[0];
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return null;
}
}
Future<Map<String, dynamic>?> getPatchesRelease(
String repoName,
String version,
) async {
try {
final response = await _dio.get(
'/repos/$repoName/releases/tags/$version',
);
return response.data;
} on Exception {
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return null;
}
}
Future<Map<String, dynamic>?> getLatestPatchesRelease(
String repoName,
) async {
try {
final response = await _dio.get(
'/repos/$repoName/releases/latest',
);
return response.data;
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return null;
}
}
Future<Map<String, dynamic>?> getLatestManagerRelease(
String repoName,
) async {
try {
final response = await _dio.get(
'/repos/$repoName/releases',
);
final Map<String, dynamic> releases = response.data[0];
int updates = 0;
final String currentVersion =
await ManagerAPI().getCurrentManagerVersion();
while (response.data[updates]['tag_name'] != 'v$currentVersion') {
updates++;
}
for (int i = 1; i < updates; i++) {
releases.update(
'body',
(value) =>
value +
'\n' +
'# ' +
response.data[i]['tag_name'] +
'\n' +
response.data[i]['body'],
);
}
return releases;
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return null;
}
}
@ -50,33 +147,41 @@ class GithubAPI {
String repoName,
DateTime since,
) async {
String path =
final String path =
'src/main/kotlin/app/revanced/patches/${repoAppPath[packageName]}';
try {
var response = await _dio.get(
final response = await _dio.get(
'/repos/$repoName/commits',
queryParameters: {
'path': path,
'per_page': 3,
'since': since.toIso8601String(),
},
options: _cacheOptions,
);
List<dynamic> commits = response.data;
final List<dynamic> commits = response.data;
return commits
.map((commit) =>
(commit['commit']['message'] as String).split('\n')[0])
.map(
(commit) => commit['commit']['message'].split('\n')[0] +
' - ' +
commit['commit']['author']['name'] +
'\n' as String,
)
.toList();
} on Exception {
return List.empty();
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
return [];
}
Future<File?> getLatestReleaseFile(String extension, String repoName) async {
Future<File?> getLatestReleaseFile(
String extension,
String repoName,
) async {
try {
Map<String, dynamic>? release = await _getLatestRelease(repoName);
final Map<String, dynamic>? release = await getLatestRelease(repoName);
if (release != null) {
Map<String, dynamic>? asset =
final Map<String, dynamic>? asset =
(release['assets'] as List<dynamic>).firstWhereOrNull(
(asset) => (asset['name'] as String).endsWith(extension),
);
@ -86,23 +191,78 @@ class GithubAPI {
);
}
}
} on Exception {
return null;
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
return null;
}
Future<List<Patch>> getPatches(String repoName) async {
Future<File?> getPatchesReleaseFile(
String extension,
String repoName,
String version,
String url,
) async {
try {
if (url.isNotEmpty) {
return await DefaultCacheManager().getSingleFile(
url,
);
}
final Map<String, dynamic>? release =
await getPatchesRelease(repoName, version);
if (release != null) {
final Map<String, dynamic>? asset =
(release['assets'] as List<dynamic>).firstWhereOrNull(
(asset) => (asset['name'] as String).endsWith(extension),
);
if (asset != null) {
final String downloadUrl = asset['browser_download_url'];
if (extension == '.apk') {
_managerAPI.setIntegrationsDownloadURL(downloadUrl);
} else if (extension == '.json') {
_managerAPI.setPatchesDownloadURL(downloadUrl, false);
} else {
_managerAPI.setPatchesDownloadURL(downloadUrl, true);
}
return await DefaultCacheManager().getSingleFile(
downloadUrl,
);
}
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
return null;
}
Future<List<Patch>> getPatches(
String repoName,
String version,
String url,
) async {
List<Patch> patches = [];
try {
File? f = await getLatestReleaseFile('.json', repoName);
final File? f = await getPatchesReleaseFile(
'.json',
repoName,
version,
url,
);
if (f != null) {
List<dynamic> list = jsonDecode(f.readAsStringSync());
final List<dynamic> list = jsonDecode(f.readAsStringSync());
patches = list.map((patch) => Patch.fromJson(patch)).toList();
}
} on Exception {
return List.empty();
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
return patches;
}
}

View File

@ -1,15 +1,22 @@
import 'dart:convert';
import 'dart:io';
import 'package:device_apps/device_apps.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:injectable/injectable.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.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/github_api.dart';
import 'package:revanced_manager/services/revanced_api.dart';
import 'package:revanced_manager/services/root_api.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:timeago/timeago.dart';
@lazySingleton
class ManagerAPI {
@ -19,15 +26,33 @@ class ManagerAPI {
final String patcherRepo = 'revanced-patcher';
final String cliRepo = 'revanced-cli';
late SharedPreferences _prefs;
String defaultApiUrl = 'https://revanced-releases-api.afterst0rm.xyz';
bool isRooted = false;
String storedPatchesFile = '/selected-patches.json';
String keystoreFile =
'/sdcard/Android/data/app.revanced.manager.flutter/files/revanced-manager.keystore';
String defaultKeystorePassword = 's3cur3p@ssw0rd';
String defaultApiUrl = 'https://api.revanced.app/';
String defaultRepoUrl = 'https://api.github.com';
String defaultPatcherRepo = 'revanced/revanced-patcher';
String defaultPatchesRepo = 'revanced/revanced-patches';
String defaultIntegrationsRepo = 'revanced/revanced-integrations';
String defaultCliRepo = 'revanced/revanced-cli';
String defaultManagerRepo = 'revanced/revanced-manager';
String? patchesVersion = '';
String? integrationsVersion = '';
bool isDefaultPatchesRepo() {
return getPatchesRepo() == 'revanced/revanced-patches';
}
bool isDefaultIntegrationsRepo() {
return getIntegrationsRepo() == 'revanced/revanced-integrations';
}
Future<void> initialize() async {
_prefs = await SharedPreferences.getInstance();
isRooted = await _rootAPI.isRooted();
storedPatchesFile =
(await getApplicationDocumentsDirectory()).path + storedPatchesFile;
}
String getApiUrl() {
@ -43,6 +68,25 @@ class ManagerAPI {
await _prefs.setString('apiUrl', url);
}
String getRepoUrl() {
return _prefs.getString('repoUrl') ?? defaultRepoUrl;
}
Future<void> setRepoUrl(String url) async {
if (url.isEmpty || url == ' ') {
url = defaultRepoUrl;
}
await _prefs.setString('repoUrl', url);
}
String getPatchesDownloadURL(bool bundle) {
return _prefs.getString('patchesDownloadURL-$bundle') ?? '';
}
Future<void> setPatchesDownloadURL(String value, bool bundle) async {
await _prefs.setString('patchesDownloadURL-$bundle', value);
}
String getPatchesRepo() {
return _prefs.getString('patchesRepo') ?? defaultPatchesRepo;
}
@ -54,6 +98,86 @@ class ManagerAPI {
await _prefs.setString('patchesRepo', value);
}
bool getPatchesConsent() {
return _prefs.getBool('patchesConsent') ?? false;
}
Future<void> setPatchesConsent(bool consent) async {
await _prefs.setBool('patchesConsent', consent);
}
bool isPatchesAutoUpdate() {
return _prefs.getBool('patchesAutoUpdate') ?? false;
}
bool isPatchesChangeEnabled() {
return _prefs.getBool('patchesChangeEnabled') ?? false;
}
void setPatchesChangeEnabled(bool value) {
_prefs.setBool('patchesChangeEnabled', value);
}
bool showPatchesChangeWarning() {
return _prefs.getBool('showPatchesChangeWarning') ?? true;
}
void setPatchesChangeWarning(bool value) {
_prefs.setBool('showPatchesChangeWarning', !value);
}
bool isChangingToggleModified() {
return _prefs.getBool('isChangingToggleModified') ?? false;
}
void setChangingToggleModified(bool value) {
_prefs.setBool('isChangingToggleModified', value);
}
Future<void> setPatchesAutoUpdate(bool value) async {
await _prefs.setBool('patchesAutoUpdate', value);
}
List<Patch> getSavedPatches(String packageName) {
final List<String> patchesJson =
_prefs.getStringList('savedPatches-$packageName') ?? [];
final List<Patch> patches = patchesJson.map((String patchJson) {
return Patch.fromJson(jsonDecode(patchJson));
}).toList();
return patches;
}
Future<void> savePatches(List<Patch> patches, String packageName) async {
final List<String> patchesJson = patches.map((Patch patch) {
return jsonEncode(patch.toJson());
}).toList();
await _prefs.setStringList('savedPatches-$packageName', patchesJson);
}
String getIntegrationsDownloadURL() {
return _prefs.getString('integrationsDownloadURL') ?? '';
}
Future<void> setIntegrationsDownloadURL(String value) async {
await _prefs.setString('integrationsDownloadURL', value);
}
List<Patch> getUsedPatches(String packageName) {
final List<String> patchesJson =
_prefs.getStringList('usedPatches-$packageName') ?? [];
final List<Patch> patches = patchesJson.map((String patchJson) {
return Patch.fromJson(jsonDecode(patchJson));
}).toList();
return patches;
}
Future<void> setUsedPatches(List<Patch> patches, String packageName) async {
final List<String> patchesJson = patches.map((Patch patch) {
return jsonEncode(patch.toJson());
}).toList();
await _prefs.setStringList('usedPatches-$packageName', patchesJson);
}
String getIntegrationsRepo() {
return _prefs.getString('integrationsRepo') ?? defaultIntegrationsRepo;
}
@ -81,23 +205,67 @@ class ManagerAPI {
await _prefs.setBool('useDarkTheme', value);
}
bool areUniversalPatchesEnabled() {
return _prefs.getBool('universalPatchesEnabled') ?? false;
}
Future<void> enableUniversalPatchesStatus(bool value) async {
await _prefs.setBool('universalPatchesEnabled', value);
}
bool areExperimentalPatchesEnabled() {
return _prefs.getBool('experimentalPatchesEnabled') ?? false;
}
Future<void> enableExperimentalPatchesStatus(bool value) async {
await _prefs.setBool('experimentalPatchesEnabled', value);
}
Future<void> setKeystorePassword(String password) async {
await _prefs.setString('keystorePassword', password);
}
String getKeystorePassword() {
return _prefs.getString('keystorePassword') ?? defaultKeystorePassword;
}
Future<void> deleteTempFolder() async {
final Directory dir = Directory('/data/local/tmp/revanced-manager');
if (await dir.exists()) {
await dir.delete(recursive: true);
}
}
Future<void> deleteKeystore() async {
final File keystore = File(
keystoreFile,
);
if (await keystore.exists()) {
await keystore.delete();
}
}
List<PatchedApplication> getPatchedApps() {
List<String> apps = _prefs.getStringList('patchedApps') ?? [];
final List<String> apps = _prefs.getStringList('patchedApps') ?? [];
return apps.map((a) => PatchedApplication.fromJson(jsonDecode(a))).toList();
}
Future<void> setPatchedApps(List<PatchedApplication> patchedApps) async {
Future<void> setPatchedApps(
List<PatchedApplication> patchedApps,
) async {
if (patchedApps.length > 1) {
patchedApps.sort((a, b) => a.name.compareTo(b.name));
}
await _prefs.setStringList('patchedApps',
patchedApps.map((a) => json.encode(a.toJson())).toList());
await _prefs.setStringList(
'patchedApps',
patchedApps.map((a) => json.encode(a.toJson())).toList(),
);
}
Future<void> savePatchedApp(PatchedApplication app) async {
List<PatchedApplication> patchedApps = getPatchedApps();
final List<PatchedApplication> patchedApps = getPatchedApps();
patchedApps.removeWhere((a) => a.packageName == app.packageName);
ApplicationWithIcon? installed = await DeviceApps.getApp(
final ApplicationWithIcon? installed = await DeviceApps.getApp(
app.packageName,
true,
) as ApplicationWithIcon?;
@ -111,14 +279,20 @@ class ManagerAPI {
}
Future<void> deletePatchedApp(PatchedApplication app) async {
List<PatchedApplication> patchedApps = getPatchedApps();
final List<PatchedApplication> patchedApps = getPatchedApps();
patchedApps.removeWhere((a) => a.packageName == app.packageName);
await setPatchedApps(patchedApps);
}
void clearAllData() {
_revancedAPI.clearAllCache();
_githubAPI.clearAllCache();
Future<void> clearAllData() async {
try {
_revancedAPI.clearAllCache();
_githubAPI.clearAllCache();
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
Future<Map<String, List<dynamic>>> getContributors() async {
@ -126,48 +300,92 @@ class ManagerAPI {
}
Future<List<Patch>> getPatches() async {
String repoName = getPatchesRepo();
if (repoName == defaultPatchesRepo) {
return await _revancedAPI.getPatches();
} else {
return await _githubAPI.getPatches(repoName);
try {
final String repoName = getPatchesRepo();
final String currentVersion = await getCurrentPatchesVersion();
final String url = getPatchesDownloadURL(false);
return await _githubAPI.getPatches(
repoName,
currentVersion,
url,
);
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return [];
}
}
Future<File?> downloadPatches() async {
String repoName = getPatchesRepo();
if (repoName == defaultPatchesRepo) {
return await _revancedAPI.getLatestReleaseFile(
try {
final String repoName = getPatchesRepo();
final String currentVersion = await getCurrentPatchesVersion();
final String url = getPatchesDownloadURL(true);
return await _githubAPI.getPatchesReleaseFile(
'.jar',
defaultPatchesRepo,
repoName,
currentVersion,
url,
);
} else {
return await _githubAPI.getLatestReleaseFile('.jar', repoName);
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return null;
}
}
Future<File?> downloadIntegrations() async {
String repoName = getIntegrationsRepo();
if (repoName == defaultIntegrationsRepo) {
return await _revancedAPI.getLatestReleaseFile(
try {
final String repoName = getIntegrationsRepo();
final String currentVersion = await getCurrentIntegrationsVersion();
final String url = getIntegrationsDownloadURL();
return await _githubAPI.getPatchesReleaseFile(
'.apk',
defaultIntegrationsRepo,
repoName,
currentVersion,
url,
);
} else {
return await _githubAPI.getLatestReleaseFile('.apk', repoName);
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return null;
}
}
Future<File?> downloadManager() async {
return await _revancedAPI.getLatestReleaseFile('.apk', defaultManagerRepo);
return await _revancedAPI.getLatestReleaseFile(
'.apk',
defaultManagerRepo,
);
}
Future<String?> getLatestPatcherReleaseTime() async {
return await _revancedAPI.getLatestReleaseTime('.gz', defaultPatcherRepo);
Future<String?> getLatestPatchesReleaseTime() async {
if (isDefaultPatchesRepo()) {
return await _revancedAPI.getLatestReleaseTime(
'.json',
defaultPatchesRepo,
);
} else {
final release =
await _githubAPI.getLatestPatchesRelease(getPatchesRepo());
if (release != null) {
final DateTime timestamp =
DateTime.parse(release['created_at'] as String);
return format(timestamp, locale: 'en_short');
} else {
return null;
}
}
}
Future<String?> getLatestManagerReleaseTime() async {
return await _revancedAPI.getLatestReleaseTime('.apk', defaultManagerRepo);
return await _revancedAPI.getLatestReleaseTime(
'.apk',
defaultManagerRepo,
);
}
Future<String?> getLatestManagerVersion() async {
@ -177,17 +395,88 @@ class ManagerAPI {
);
}
Future<String?> getLatestIntegrationsVersion() async {
if (isDefaultIntegrationsRepo()) {
return await _revancedAPI.getLatestReleaseVersion(
'.apk',
defaultIntegrationsRepo,
);
} else {
final release = await _githubAPI.getLatestRelease(getIntegrationsRepo());
if (release != null) {
return release['tag_name'];
} else {
return null;
}
}
}
Future<String?> getLatestPatchesVersion() async {
if (isDefaultPatchesRepo()) {
return await _revancedAPI.getLatestReleaseVersion(
'.json',
defaultPatchesRepo,
);
} else {
final release =
await _githubAPI.getLatestPatchesRelease(getPatchesRepo());
if (release != null) {
return release['tag_name'];
} else {
return null;
}
}
}
Future<String> getCurrentManagerVersion() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
return packageInfo.version;
}
Future<String> getCurrentPatchesVersion() async {
patchesVersion = _prefs.getString('patchesVersion') ?? '0.0.0';
if (patchesVersion == '0.0.0' || isPatchesAutoUpdate()) {
final String newPatchesVersion =
await getLatestPatchesVersion() ?? '0.0.0';
if (patchesVersion != newPatchesVersion && newPatchesVersion != '0.0.0') {
await setCurrentPatchesVersion(newPatchesVersion);
}
}
return patchesVersion!;
}
Future<void> setCurrentPatchesVersion(String version) async {
await _prefs.setString('patchesVersion', version);
await setPatchesDownloadURL('', false);
await setPatchesDownloadURL('', true);
await downloadPatches();
}
Future<String> getCurrentIntegrationsVersion() async {
integrationsVersion = _prefs.getString('integrationsVersion') ?? '0.0.0';
if (integrationsVersion == '0.0.0' || isPatchesAutoUpdate()) {
final String newIntegrationsVersion =
await getLatestIntegrationsVersion() ?? '0.0.0';
if (integrationsVersion != newIntegrationsVersion &&
newIntegrationsVersion != '0.0.0') {
await setCurrentIntegrationsVersion(newIntegrationsVersion);
}
}
return integrationsVersion!;
}
Future<void> setCurrentIntegrationsVersion(String version) async {
await _prefs.setString('integrationsVersion', version);
await setIntegrationsDownloadURL('');
await downloadIntegrations();
}
Future<List<PatchedApplication>> getAppsToRemove(
List<PatchedApplication> patchedApps,
) async {
List<PatchedApplication> toRemove = [];
for (PatchedApplication app in patchedApps) {
bool isRemove = await isAppUninstalled(app);
final List<PatchedApplication> toRemove = [];
for (final PatchedApplication app in patchedApps) {
final bool isRemove = await isAppUninstalled(app);
if (isRemove) {
toRemove.add(app);
}
@ -198,13 +487,13 @@ class ManagerAPI {
Future<List<PatchedApplication>> getUnsavedApps(
List<PatchedApplication> patchedApps,
) async {
List<PatchedApplication> unsavedApps = [];
bool hasRootPermissions = await _rootAPI.hasRootPermissions();
final List<PatchedApplication> unsavedApps = [];
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
if (hasRootPermissions) {
List<String> installedApps = await _rootAPI.getInstalledApps();
for (String packageName in installedApps) {
final List<String> installedApps = await _rootAPI.getInstalledApps();
for (final String packageName in installedApps) {
if (!patchedApps.any((app) => app.packageName == packageName)) {
ApplicationWithIcon? application = await DeviceApps.getApp(
final ApplicationWithIcon? application = await DeviceApps.getApp(
packageName,
true,
) as ApplicationWithIcon?;
@ -213,6 +502,7 @@ class ManagerAPI {
PatchedApplication(
name: application.appName,
packageName: application.packageName,
originalPackageName: application.packageName,
version: application.versionName!,
apkFilePath: application.apkFilePath,
icon: application.icon,
@ -224,15 +514,13 @@ class ManagerAPI {
}
}
}
List<Application> userApps = await DeviceApps.getInstalledApplications(
includeSystemApps: false,
includeAppIcons: false,
);
for (Application app in userApps) {
final List<Application> userApps =
await DeviceApps.getInstalledApplications();
for (final Application app in userApps) {
if (app.packageName.startsWith('app.revanced') &&
!app.packageName.startsWith('app.revanced.manager.') &&
!patchedApps.any((uapp) => uapp.packageName == app.packageName)) {
ApplicationWithIcon? application = await DeviceApps.getApp(
final ApplicationWithIcon? application = await DeviceApps.getApp(
app.packageName,
true,
) as ApplicationWithIcon?;
@ -241,11 +529,11 @@ class ManagerAPI {
PatchedApplication(
name: application.appName,
packageName: application.packageName,
originalPackageName: application.packageName,
version: application.versionName!,
apkFilePath: application.apkFilePath,
icon: application.icon,
patchDate: DateTime.now(),
isRooted: false,
),
);
}
@ -254,24 +542,87 @@ class ManagerAPI {
return unsavedApps;
}
Future<void> showPatchesChangeWarningDialog(BuildContext context) {
final ValueNotifier<bool> noShow =
ValueNotifier(!showPatchesChangeWarning());
return showDialog(
barrierDismissible: false,
context: context,
builder: (context) => WillPopScope(
onWillPop: () async => false,
child: AlertDialog(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
title: I18nText('warning'),
content: ValueListenableBuilder(
valueListenable: noShow,
builder: (context, value, child) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
I18nText(
'patchItem.patchesChangeWarningDialogText',
child: const Text(
'',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(height: 8),
CheckboxListTile(
value: value,
contentPadding: EdgeInsets.zero,
title: I18nText(
'noShowAgain',
),
onChanged: (selected) {
noShow.value = selected!;
},
),
],
);
},
),
actions: [
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () {
setPatchesChangeWarning(noShow.value);
Navigator.of(context).pop();
},
),
],
),
),
);
}
Future<void> reAssessSavedApps() async {
List<PatchedApplication> patchedApps = getPatchedApps();
List<PatchedApplication> unsavedApps = await getUnsavedApps(patchedApps);
final List<PatchedApplication> patchedApps = getPatchedApps();
final List<PatchedApplication> unsavedApps =
await getUnsavedApps(patchedApps);
patchedApps.addAll(unsavedApps);
List<PatchedApplication> toRemove = await getAppsToRemove(patchedApps);
final 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);
for (final PatchedApplication app in patchedApps) {
app.hasUpdates =
await hasAppUpdates(app.originalPackageName, app.patchDate);
app.changelog =
await getAppChangelog(app.originalPackageName, app.patchDate);
if (!app.hasUpdates) {
String? currentInstalledVersion =
final 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]'), ''));
final String currentSavedVersion = app.version;
final int currentInstalledVersionInt = int.parse(
currentInstalledVersion.replaceAll(RegExp('[^0-9]'), ''),
);
final int currentSavedVersionInt = int.parse(
currentSavedVersion.replaceAll(RegExp('[^0-9]'), ''),
);
if (currentInstalledVersionInt > currentSavedVersionInt) {
app.hasUpdates = true;
}
@ -283,9 +634,9 @@ class ManagerAPI {
Future<bool> isAppUninstalled(PatchedApplication app) async {
bool existsRoot = false;
bool existsNonRoot = await DeviceApps.isAppInstalled(app.packageName);
final bool existsNonRoot = await DeviceApps.isAppInstalled(app.packageName);
if (app.isRooted) {
bool hasRootPermissions = await _rootAPI.hasRootPermissions();
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
if (hasRootPermissions) {
existsRoot = await _rootAPI.isAppInstalled(app.packageName);
}
@ -294,8 +645,11 @@ class ManagerAPI {
return !existsNonRoot;
}
Future<bool> hasAppUpdates(String packageName, DateTime patchDate) async {
List<String> commits = await _githubAPI.getCommits(
Future<bool> hasAppUpdates(
String packageName,
DateTime patchDate,
) async {
final List<String> commits = await _githubAPI.getCommits(
packageName,
getPatchesRepo(),
patchDate,
@ -304,7 +658,9 @@ class ManagerAPI {
}
Future<List<String>> getAppChangelog(
String packageName, DateTime patchDate) async {
String packageName,
DateTime patchDate,
) async {
List<String> newCommits = await _githubAPI.getCommits(
packageName,
getPatchesRepo(),
@ -314,9 +670,79 @@ class ManagerAPI {
newCommits = await _githubAPI.getCommits(
packageName,
getPatchesRepo(),
DateTime(2022, 3, 20, 21, 06, 01),
patchDate,
);
}
return newCommits;
}
Future<bool> isSplitApk(PatchedApplication patchedApp) async {
Application? app;
if (patchedApp.isFromStorage) {
app = await DeviceApps.getAppFromStorage(patchedApp.apkFilePath);
} else {
app = await DeviceApps.getApp(patchedApp.packageName);
}
return app != null && app.isSplit;
}
Future<void> setSelectedPatches(
String app,
List<String> patches,
) async {
final File selectedPatchesFile = File(storedPatchesFile);
final Map<String, dynamic> patchesMap = await readSelectedPatchesFile();
if (patches.isEmpty) {
patchesMap.remove(app);
} else {
patchesMap[app] = patches;
}
selectedPatchesFile.writeAsString(jsonEncode(patchesMap));
}
// get default patches for app
Future<List<String>> getDefaultPatches() async {
final List<Patch> patches = await getPatches();
final List<String> defaultPatches = [];
if (areExperimentalPatchesEnabled() == false) {
defaultPatches.addAll(
patches
.where(
(element) =>
element.excluded == false && isPatchSupported(element),
)
.map((p) => p.name),
);
} else {
defaultPatches.addAll(
patches
.where((element) => isPatchSupported(element))
.map((p) => p.name),
);
}
return defaultPatches;
}
Future<List<String>> getSelectedPatches(String app) async {
final Map<String, dynamic> patchesMap = await readSelectedPatchesFile();
final List<String> defaultPatches = await getDefaultPatches();
return List.from(patchesMap.putIfAbsent(app, () => defaultPatches));
}
Future<Map<String, dynamic>> readSelectedPatchesFile() async {
final File selectedPatchesFile = File(storedPatchesFile);
if (!selectedPatchesFile.existsSync()) {
return {};
}
final String string = selectedPatchesFile.readAsStringSync();
if (string.trim().isEmpty) {
return {};
}
return jsonDecode(string);
}
Future<void> resetLastSelectedPatches() async {
final File selectedPatchesFile = File(storedPatchesFile);
selectedPatchesFile.deleteSync();
}
}

View File

@ -1,9 +1,12 @@
import 'dart:io';
import 'package:app_installer/app_installer.dart';
import 'package:collection/collection.dart';
import 'package:cr_file_saver/file_saver.dart';
import 'package:device_apps/device_apps.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:injectable/injectable.dart';
import 'package:install_plugin/install_plugin.dart';
import 'package:path_provider/path_provider.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/patch.dart';
@ -18,16 +21,21 @@ class PatcherAPI {
MethodChannel('app.revanced.manager.flutter/patcher');
final ManagerAPI _managerAPI = locator<ManagerAPI>();
final RootAPI _rootAPI = RootAPI();
late Directory _dataDir;
late Directory _tmpDir;
late File _keyStoreFile;
List<Patch> _patches = [];
Map filteredPatches = <String, List<Patch>>{};
File? _outFile;
Future<void> initialize() async {
await _loadPatches();
Directory appCache = await getTemporaryDirectory();
await _managerAPI.downloadPatches();
await _managerAPI.downloadIntegrations();
final Directory appCache = await getTemporaryDirectory();
_dataDir = await getExternalStorageDirectory() ?? appCache;
_tmpDir = Directory('${appCache.path}/patcher');
_keyStoreFile = File('${appCache.path}/revanced-manager.keystore');
_keyStoreFile = File('${_dataDir.path}/revanced-manager.keystore');
cleanPatcher();
}
@ -42,18 +50,43 @@ class PatcherAPI {
if (_patches.isEmpty) {
_patches = await _managerAPI.getPatches();
}
} on Exception {
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
_patches = List.empty();
}
}
Future<List<ApplicationWithIcon>> getFilteredInstalledApps() async {
List<ApplicationWithIcon> filteredApps = [];
for (Patch patch in _patches) {
for (Package package in patch.compatiblePackages) {
Future<List<ApplicationWithIcon>> getFilteredInstalledApps(
bool showUniversalPatches,
) async {
final List<ApplicationWithIcon> filteredApps = [];
final bool allAppsIncluded =
_patches.any((patch) => patch.compatiblePackages.isEmpty) &&
showUniversalPatches;
if (allAppsIncluded) {
final allPackages = await DeviceApps.getInstalledApplications(
includeAppIcons: true,
onlyAppsWithLaunchIntent: true,
);
for (final pkg in allPackages) {
if (!filteredApps.any((app) => app.packageName == pkg.packageName)) {
final appInfo = await DeviceApps.getApp(
pkg.packageName,
true,
) as ApplicationWithIcon?;
if (appInfo != null) {
filteredApps.add(appInfo);
}
}
}
}
for (final Patch patch in _patches) {
for (final Package package in patch.compatiblePackages) {
try {
if (!filteredApps.any((app) => app.packageName == package.name)) {
ApplicationWithIcon? app = await DeviceApps.getApp(
final ApplicationWithIcon? app = await DeviceApps.getApp(
package.name,
true,
) as ApplicationWithIcon?;
@ -61,41 +94,47 @@ class PatcherAPI {
filteredApps.add(app);
}
}
} catch (e) {
continue;
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
}
return filteredApps;
}
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 == newPackageName))
List<Patch> getFilteredPatches(String packageName) {
final List<Patch> patches = _patches
.where(
(patch) =>
patch.compatiblePackages.isEmpty ||
!patch.name.contains('settings') &&
patch.compatiblePackages
.any((pack) => pack.name == packageName),
)
.toList();
if (!_managerAPI.areUniversalPatchesEnabled()) {
filteredPatches[packageName] = patches
.where((patch) => patch.compatiblePackages.isNotEmpty)
.toList();
} else {
filteredPatches[packageName] = patches;
}
return filteredPatches[packageName];
}
Future<List<Patch>> getAppliedPatches(List<String> appliedPatches) async {
Future<List<Patch>> getAppliedPatches(
List<String> appliedPatches,
) async {
return _patches
.where((patch) => appliedPatches.contains(patch.name))
.toList();
}
Future<bool> needsIntegrations(List<Patch> selectedPatches) async {
return selectedPatches.any(
(patch) => patch.dependencies.any(
(dep) => dep.contains('integrations'),
),
);
}
Future<bool> needsResourcePatching(List<Patch> selectedPatches) async {
Future<bool> needsResourcePatching(
List<Patch> selectedPatches,
) async {
return selectedPatches.any(
(patch) => patch.dependencies.any(
(dep) => dep.contains('resource-'),
@ -111,31 +150,15 @@ class PatcherAPI {
);
}
Future<String> getOriginalFilePath(
String packageName,
String originalFilePath,
) async {
bool hasRootPermissions = await _rootAPI.hasRootPermissions();
if (hasRootPermissions) {
originalFilePath = await _rootAPI.getOriginalFilePath(
packageName,
originalFilePath,
);
}
return originalFilePath;
}
Future<void> runPatcher(
String packageName,
String originalFilePath,
String apkFilePath,
List<Patch> selectedPatches,
) async {
bool mergeIntegrations = await needsIntegrations(selectedPatches);
bool resourcePatching = await needsResourcePatching(selectedPatches);
bool includeSettings = await needsSettingsPatch(selectedPatches);
final bool includeSettings = await needsSettingsPatch(selectedPatches);
if (includeSettings) {
try {
Patch? settingsPatch = _patches.firstWhereOrNull(
final Patch? settingsPatch = _patches.firstWhereOrNull(
(patch) =>
patch.name.contains('settings') &&
patch.compatiblePackages.any((pack) => pack.name == packageName),
@ -143,42 +166,55 @@ class PatcherAPI {
if (settingsPatch != null) {
selectedPatches.add(settingsPatch);
}
} catch (e) {
// ignore
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
File? patchBundleFile = await _managerAPI.downloadPatches();
File? integrationsFile;
if (mergeIntegrations) {
integrationsFile = await _managerAPI.downloadIntegrations();
}
final File? patchBundleFile = await _managerAPI.downloadPatches();
final File? integrationsFile = await _managerAPI.downloadIntegrations();
if (patchBundleFile != null) {
_dataDir.createSync();
_tmpDir.createSync();
Directory workDir = _tmpDir.createTempSync('tmp-');
File inputFile = File('${workDir.path}/base.apk');
File patchedFile = File('${workDir.path}/patched.apk');
final Directory workDir = _tmpDir.createTempSync('tmp-');
final File inputFile = File('${workDir.path}/base.apk');
final File patchedFile = File('${workDir.path}/patched.apk');
_outFile = File('${workDir.path}/out.apk');
Directory cacheDir = Directory('${workDir.path}/cache');
final Directory cacheDir = Directory('${workDir.path}/cache');
cacheDir.createSync();
await patcherChannel.invokeMethod(
'runPatcher',
{
'patchBundleFilePath': patchBundleFile.path,
'originalFilePath': await getOriginalFilePath(
packageName,
originalFilePath,
),
'inputFilePath': inputFile.path,
'patchedFilePath': patchedFile.path,
'outFilePath': _outFile!.path,
'integrationsPath': mergeIntegrations ? integrationsFile!.path : '',
'selectedPatches': selectedPatches.map((p) => p.name).toList(),
'cacheDirPath': cacheDir.path,
'mergeIntegrations': mergeIntegrations,
'resourcePatching': resourcePatching,
'keyStoreFilePath': _keyStoreFile.path,
},
);
final String originalFilePath = apkFilePath;
try {
await patcherChannel.invokeMethod(
'runPatcher',
{
'patchBundleFilePath': patchBundleFile.path,
'originalFilePath': originalFilePath,
'inputFilePath': inputFile.path,
'patchedFilePath': patchedFile.path,
'outFilePath': _outFile!.path,
'integrationsPath': integrationsFile!.path,
'selectedPatches': selectedPatches.map((p) => p.name).toList(),
'cacheDirPath': cacheDir.path,
'keyStoreFilePath': _keyStoreFile.path,
'keystorePassword': _managerAPI.getKeystorePassword(),
},
);
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
}
Future<void> stopPatcher() async {
try {
await patcherChannel.invokeMethod('stopPatcher');
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
@ -186,7 +222,7 @@ class PatcherAPI {
if (_outFile != null) {
try {
if (patchedApp.isRooted) {
bool hasRootPermissions = await _rootAPI.hasRootPermissions();
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
if (hasRootPermissions) {
return _rootAPI.installApp(
patchedApp.packageName,
@ -195,50 +231,89 @@ class PatcherAPI {
);
}
} else {
await AppInstaller.installApk(_outFile!.path);
return await DeviceApps.isAppInstalled(patchedApp.packageName);
final install = await InstallPlugin.installApk(_outFile!.path);
return install['isSuccess'];
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
} on Exception {
return false;
}
}
return false;
}
void sharePatchedFile(String appName, String version) {
if (_outFile != null) {
String prefix = appName.toLowerCase().replaceAll(' ', '-');
String newName = '$prefix-revanced_v$version.apk';
int lastSeparator = _outFile!.path.lastIndexOf('/');
String newPath = _outFile!.path.substring(0, lastSeparator + 1) + newName;
File shareFile = _outFile!.copySync(newPath);
ShareExtend.share(shareFile.path, 'file');
void exportPatchedFile(String appName, String version) {
try {
if (_outFile != null) {
final String newName = _getFileName(appName, version);
CRFileSaver.saveFileWithDialog(
SaveFileDialogParams(
sourceFilePath: _outFile!.path,
destinationFileName: newName,
),
);
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
Future<void> sharePatcherLog(String logs) async {
Directory appCache = await getTemporaryDirectory();
Directory logDir = Directory('${appCache.path}/logs');
void sharePatchedFile(String appName, String version) {
try {
if (_outFile != null) {
final String newName = _getFileName(appName, version);
final int lastSeparator = _outFile!.path.lastIndexOf('/');
final String newPath =
_outFile!.path.substring(0, lastSeparator + 1) + newName;
final File shareFile = _outFile!.copySync(newPath);
ShareExtend.share(shareFile.path, 'file');
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
String _getFileName(String appName, String version) {
final String prefix = appName.toLowerCase().replaceAll(' ', '-');
final String newName = '$prefix-revanced_v$version.apk';
return newName;
}
Future<void> exportPatcherLog(String logs) async {
final Directory appCache = await getTemporaryDirectory();
final Directory logDir = Directory('${appCache.path}/logs');
logDir.createSync();
String dateTime = DateTime.now()
final String dateTime = DateTime.now()
.toIso8601String()
.replaceAll('-', '')
.replaceAll(':', '')
.replaceAll('T', '')
.replaceAll('.', '');
File log = File('${logDir.path}/revanced-manager_patcher_$dateTime.log');
final String fileName = 'revanced-manager_patcher_$dateTime.log';
final File log = File('${logDir.path}/$fileName');
log.writeAsStringSync(logs);
ShareExtend.share(log.path, 'file');
CRFileSaver.saveFileWithDialog(
SaveFileDialogParams(
sourceFilePath: log.path,
destinationFileName: fileName,
),
);
}
String getRecommendedVersion(String packageName) {
Map<String, int> versions = {};
for (Patch patch in _patches) {
Package? package = patch.compatiblePackages.firstWhereOrNull(
String getSuggestedVersion(String packageName) {
final Map<String, int> versions = {};
for (final Patch patch in _patches) {
final Package? package = patch.compatiblePackages.firstWhereOrNull(
(pack) => pack.name == packageName,
);
if (package != null) {
for (String version in package.versions) {
for (final String version in package.versions) {
versions.update(
version,
(value) => versions[version]! + 1,
@ -248,7 +323,7 @@ class PatcherAPI {
}
}
if (versions.isNotEmpty) {
var entries = versions.entries.toList()
final entries = versions.entries.toList()
..sort((a, b) => a.value.compareTo(b.value));
versions
..clear()

View File

@ -1,68 +1,84 @@
import 'dart:async';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:dio/dio.dart';
import 'package:dio_http_cache_lts/dio_http_cache_lts.dart';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:injectable/injectable.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:timeago/timeago.dart';
@lazySingleton
class RevancedAPI {
late Dio _dio = Dio();
final DioCacheManager _dioCacheManager = DioCacheManager(CacheConfig());
final Options _cacheOptions = buildCacheOptions(
const Duration(days: 1),
maxStale: const Duration(days: 7),
final _cacheOptions = CacheOptions(
store: MemCacheStore(),
maxStale: const Duration(days: 1),
priority: CachePriority.high,
);
Future<void> initialize(String apiUrl) async {
_dio = Dio(BaseOptions(baseUrl: apiUrl));
_dio.interceptors.add(_dioCacheManager.interceptor);
try {
_dio = Dio(
BaseOptions(
baseUrl: apiUrl,
),
);
_dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
Future<void> clearAllCache() async {
await _dioCacheManager.clearAll();
try {
await _cacheOptions.store!.clean();
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
Future<Map<String, List<dynamic>>> getContributors() async {
Map<String, List<dynamic>> contributors = {};
final Map<String, List<dynamic>> contributors = {};
try {
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'];
final response = await _dio.get('/contributors');
final List<dynamic> repositories = response.data['repositories'];
for (final Map<String, dynamic> repo in repositories) {
final String name = repo['name'];
contributors[name] = repo['contributors'];
}
} on Exception {
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return {};
}
return contributors;
}
Future<List<Patch>> getPatches() async {
try {
var response = await _dio.get('/patches', options: _cacheOptions);
List<dynamic> patches = response.data;
return patches.map((patch) => Patch.fromJson(patch)).toList();
} on Exception {
return List.empty();
}
}
Future<Map<String, dynamic>?> _getLatestRelease(
String extension,
String repoName,
) async {
try {
var response = await _dio.get('/tools', options: _cacheOptions);
List<dynamic> tools = response.data['tools'];
final response = await _dio.get('/tools');
final List<dynamic> tools = response.data['tools'];
return tools.firstWhereOrNull(
(t) =>
t['repository'] == repoName &&
(t['name'] as String).endsWith(extension),
);
} on Exception {
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return null;
}
}
@ -72,49 +88,101 @@ class RevancedAPI {
String repoName,
) async {
try {
Map<String, dynamic>? release = await _getLatestRelease(
final Map<String, dynamic>? release = await _getLatestRelease(
extension,
repoName,
);
if (release != null) {
return release['version'];
}
} on Exception {
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return null;
}
return null;
}
Future<File?> getLatestReleaseFile(String extension, String repoName) async {
Future<File?> getLatestReleaseFile(
String extension,
String repoName,
) async {
try {
Map<String, dynamic>? release = await _getLatestRelease(
final Map<String, dynamic>? release = await _getLatestRelease(
extension,
repoName,
);
if (release != null) {
String url = release['browser_download_url'];
final String url = release['browser_download_url'];
return await DefaultCacheManager().getSingleFile(url);
}
} on Exception {
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return null;
}
return null;
}
StreamController<double> managerUpdateProgress =
StreamController<double>.broadcast();
void updateManagerDownloadProgress(int progress) {
managerUpdateProgress.add(progress.toDouble());
}
Stream<double> getManagerUpdateProgress() {
return managerUpdateProgress.stream;
}
void disposeManagerUpdateProgress() {
managerUpdateProgress.close();
}
Future<File?> downloadManager() async {
final Map<String, dynamic>? release = await _getLatestRelease(
'.apk',
'revanced/revanced-manager',
);
File? outputFile;
await for (final result in DefaultCacheManager().getFileStream(
release!['browser_download_url'] as String,
withProgress: true,
)) {
if (result is DownloadProgress) {
final totalSize = result.totalSize ?? 10000000;
final progress = (result.downloaded / totalSize * 100).round();
updateManagerDownloadProgress(progress);
} else if (result is FileInfo) {
disposeManagerUpdateProgress();
// The download is complete; convert the FileInfo object to a File object
outputFile = File(result.file.path);
}
}
return outputFile;
}
Future<String?> getLatestReleaseTime(
String extension,
String repoName,
) async {
try {
Map<String, dynamic>? release = await _getLatestRelease(
final Map<String, dynamic>? release = await _getLatestRelease(
extension,
repoName,
);
if (release != null) {
DateTime timestamp = DateTime.parse(release['timestamp'] as String);
final DateTime timestamp =
DateTime.parse(release['timestamp'] as String);
return format(timestamp, locale: 'en_short');
}
} on Exception {
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return null;
}
return null;

View File

@ -1,10 +1,25 @@
import 'package:flutter/foundation.dart';
import 'package:root/root.dart';
class RootAPI {
final String _managerDirPath = '/data/local/tmp/revanced-manager';
// TODO(ponces): remove in the future, keep it for now during migration.
final String _revancedOldDirPath = '/data/local/tmp/revanced-manager';
final String _revancedDirPath = '/data/adb/revanced';
final String _postFsDataDirPath = '/data/adb/post-fs-data.d';
final String _serviceDDirPath = '/data/adb/service.d';
Future<bool> isRooted() async {
try {
final bool? isRooted = await Root.isRootAvailable();
return isRooted != null && isRooted;
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return false;
}
}
Future<bool> hasRootPermissions() async {
try {
bool? isRooted = await Root.isRootAvailable();
@ -13,7 +28,10 @@ class RootAPI {
return isRooted != null && isRooted;
}
return false;
} on Exception {
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return false;
}
}
@ -24,52 +42,62 @@ class RootAPI {
seLinux,
String filePath,
) async {
if (permissions.isNotEmpty) {
await Root.exec(
cmd: 'chmod $permissions "$filePath"',
);
}
if (ownerGroup.isNotEmpty) {
await Root.exec(
cmd: 'chown $ownerGroup "$filePath"',
);
}
if (seLinux.isNotEmpty) {
await Root.exec(
cmd: 'chcon $seLinux "$filePath"',
);
try {
if (permissions.isNotEmpty) {
await Root.exec(
cmd: 'chmod $permissions "$filePath"',
);
}
if (ownerGroup.isNotEmpty) {
await Root.exec(
cmd: 'chown $ownerGroup "$filePath"',
);
}
if (seLinux.isNotEmpty) {
await Root.exec(
cmd: 'chcon $seLinux "$filePath"',
);
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
Future<bool> isAppInstalled(String packageName) async {
if (packageName.isNotEmpty) {
String? res = await Root.exec(
cmd: 'ls "$_managerDirPath/$packageName"',
);
if (res != null && res.isNotEmpty) {
res = await Root.exec(
cmd: 'ls "$_serviceDDirPath/$packageName.sh"',
);
return res != null && res.isNotEmpty;
}
return fileExists('$_serviceDDirPath/$packageName.sh');
}
return false;
}
Future<List<String>> getInstalledApps() async {
final List<String> apps = List.empty(growable: true);
try {
String? res = await Root.exec(
cmd: 'ls "$_managerDirPath"',
cmd: 'ls "$_revancedDirPath"',
);
if (res != null) {
List<String> apps = res.split('\n');
apps.removeWhere((pack) => pack.isEmpty);
return apps.map((pack) => pack.trim()).toList();
final List<String> list = res.split('\n');
list.removeWhere((pack) => pack.isEmpty);
apps.addAll(list.map((pack) => pack.trim()).toList());
}
// TODO(ponces): remove in the future, keep it for now during migration.
res = await Root.exec(
cmd: 'ls "$_revancedOldDirPath"',
);
if (res != null) {
final List<String> list = res.split('\n');
list.removeWhere((pack) => pack.isEmpty);
apps.addAll(list.map((pack) => pack.trim()).toList());
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
} on Exception {
return List.empty();
}
return List.empty();
return apps;
}
Future<void> deleteApp(String packageName, String originalFilePath) async {
@ -79,8 +107,12 @@ class RootAPI {
await Root.exec(
cmd: 'su -mm -c "umount -l $originalFilePath"',
);
// TODO(ponces): remove in the future, keep it for now during migration.
await Root.exec(
cmd: 'rm -rf "$_managerDirPath/$packageName"',
cmd: 'rm -rf "$_revancedOldDirPath/$packageName"',
);
await Root.exec(
cmd: 'rm -rf "$_revancedDirPath/$packageName"',
);
await Root.exec(
cmd: 'rm -rf "$_serviceDDirPath/$packageName.sh"',
@ -98,13 +130,13 @@ class RootAPI {
try {
await deleteApp(packageName, originalFilePath);
await Root.exec(
cmd: 'mkdir -p "$_managerDirPath/$packageName"',
cmd: 'mkdir -p "$_revancedDirPath/$packageName"',
);
await setPermissions(
'0755',
'shell:shell',
'',
'$_managerDirPath/$packageName',
'$_revancedDirPath/$packageName',
);
await saveOriginalFilePath(packageName, originalFilePath);
await installServiceDScript(packageName);
@ -112,18 +144,24 @@ class RootAPI {
await installApk(packageName, patchedFilePath);
await mountApk(packageName, originalFilePath);
return true;
} on Exception {
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return false;
}
}
Future<void> installServiceDScript(String packageName) async {
String content = '#!/system/bin/sh\n'
'while [ "\$(getprop sys.boot_completed | tr -d \'"\'"\'\\\\r\'"\'"\')" != "1" ]; do sleep 1; done\n'
'base_path=$_managerDirPath/$packageName/base.apk\n'
await Root.exec(
cmd: 'mkdir -p "$_serviceDDirPath"',
);
final String content = '#!/system/bin/sh\n'
'while [ "\$(getprop sys.boot_completed | tr -d \'"\'"\'\\\\r\'"\'"\')" != "1" ]; do sleep 3; done\n'
'base_path=$_revancedDirPath/$packageName/base.apk\n'
'stock_path=\$(pm path $packageName | grep base | sed \'"\'"\'s/package://g\'"\'"\')\n'
'[ ! -z \$stock_path ] && mount -o bind \$base_path \$stock_path';
String scriptFilePath = '$_serviceDDirPath/$packageName.sh';
r'[ ! -z $stock_path ] && mount -o bind $base_path $stock_path';
final String scriptFilePath = '$_serviceDDirPath/$packageName.sh';
await Root.exec(
cmd: 'echo \'$content\' > "$scriptFilePath"',
);
@ -131,10 +169,13 @@ class RootAPI {
}
Future<void> installPostFsDataScript(String packageName) async {
String content = '#!/system/bin/sh\n'
await Root.exec(
cmd: 'mkdir -p "$_postFsDataDirPath"',
);
final String content = '#!/system/bin/sh\n'
'stock_path=\$(pm path $packageName | grep base | sed \'"\'"\'s/package://g\'"\'"\')\n'
'[ ! -z \$stock_path ] && umount -l \$stock_path';
String scriptFilePath = '$_postFsDataDirPath/$packageName.sh';
r'[ ! -z $stock_path ] && umount -l $stock_path';
final String scriptFilePath = '$_postFsDataDirPath/$packageName.sh';
await Root.exec(
cmd: 'echo \'$content\' > "$scriptFilePath"',
);
@ -142,7 +183,7 @@ class RootAPI {
}
Future<void> installApk(String packageName, String patchedFilePath) async {
String newPatchedFilePath = '$_managerDirPath/$packageName/base.apk';
final String newPatchedFilePath = '$_revancedDirPath/$packageName/base.apk';
await Root.exec(
cmd: 'cp "$patchedFilePath" "$newPatchedFilePath"',
);
@ -155,7 +196,7 @@ class RootAPI {
}
Future<void> mountApk(String packageName, String originalFilePath) async {
String newPatchedFilePath = '$_managerDirPath/$packageName/base.apk';
final String newPatchedFilePath = '$_revancedDirPath/$packageName/base.apk';
await Root.exec(
cmd: 'am force-stop "$packageName"',
);
@ -168,42 +209,26 @@ class RootAPI {
}
Future<bool> isMounted(String packageName) async {
String? res = await Root.exec(
final String? res = await Root.exec(
cmd: 'cat /proc/mounts | grep $packageName',
);
return res != null && res.isNotEmpty;
}
Future<String> getOriginalFilePath(
String packageName,
String originalFilePath,
) async {
bool isInstalled = await isAppInstalled(packageName);
if (isInstalled && await isMounted(packageName)) {
originalFilePath = '$_managerDirPath/$packageName/original.apk';
await setPermissions(
'0644',
'shell:shell',
'u:object_r:apk_data_file:s0',
originalFilePath,
);
}
return originalFilePath;
}
Future<void> saveOriginalFilePath(
String packageName,
String originalFilePath,
) async {
String originalRootPath = '$_managerDirPath/$packageName/original.apk';
final String originalRootPath =
'$_revancedDirPath/$packageName/original.apk';
await Root.exec(
cmd: 'mkdir -p "$_managerDirPath/$packageName"',
cmd: 'mkdir -p "$_revancedDirPath/$packageName"',
);
await setPermissions(
'0755',
'shell:shell',
'',
'$_managerDirPath/$packageName',
'$_revancedDirPath/$packageName',
);
await Root.exec(
cmd: 'cp "$originalFilePath" "$originalRootPath"',
@ -215,4 +240,18 @@ class RootAPI {
originalFilePath,
);
}
Future<bool> fileExists(String path) async {
try {
final String? res = await Root.exec(
cmd: 'ls $path',
);
return res != null && res.isNotEmpty;
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return false;
}
}
}

View File

@ -20,4 +20,15 @@ class Toast {
gravity: t.ToastGravity.CENTER,
);
}
void showBottom(String text) {
t.Fluttertoast.showToast(
msg: FlutterI18n.translate(
_fToast.context!,
text,
),
toastLength: t.Toast.LENGTH_LONG,
gravity: t.ToastGravity.BOTTOM,
);
}
}

View File

@ -3,7 +3,6 @@ import 'package:google_fonts/google_fonts.dart';
var lightCustomColorScheme = ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.light,
primary: const Color(0xff1B73E8),
);
@ -13,7 +12,7 @@ var lightCustomTheme = ThemeData(
navigationBarTheme: NavigationBarThemeData(
labelTextStyle: MaterialStateProperty.all(
TextStyle(
color: lightCustomColorScheme.secondary,
color: lightCustomColorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
@ -34,13 +33,12 @@ var darkCustomTheme = ThemeData(
navigationBarTheme: NavigationBarThemeData(
labelTextStyle: MaterialStateProperty.all(
TextStyle(
color: darkCustomColorScheme.secondary,
color: darkCustomColorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
),
canvasColor: const Color(0xff1B1A1D),
scaffoldBackgroundColor: const Color(0xff1B1A1D),
toggleableActiveColor: const Color(0xffA5CAFF),
textTheme: GoogleFonts.robotoTextTheme(ThemeData.dark().textTheme),
);

View File

@ -7,65 +7,44 @@ import 'package:revanced_manager/theme.dart';
import 'package:stacked_services/stacked_services.dart';
class DynamicThemeBuilder extends StatelessWidget {
final String title;
final Widget home;
final Iterable<LocalizationsDelegate> localizationsDelegates;
const DynamicThemeBuilder({
Key? key,
required this.title,
required this.home,
required this.localizationsDelegates,
}) : super(key: key);
final String title;
final Widget home;
final Iterable<LocalizationsDelegate> localizationsDelegates;
@override
Widget build(BuildContext context) {
return DynamicColorBuilder(
builder: (lightColorScheme, darkColorScheme) {
ThemeData lightDynamicTheme = ThemeData(
final ThemeData lightDynamicTheme = ThemeData(
useMaterial3: true,
canvasColor: lightColorScheme?.background,
navigationBarTheme: NavigationBarThemeData(
backgroundColor: lightColorScheme?.background,
indicatorColor: lightColorScheme?.primary.withAlpha(150),
labelTextStyle: MaterialStateProperty.all(
GoogleFonts.roboto(
color: lightColorScheme?.secondary,
color: lightColorScheme?.onSurface,
fontWeight: FontWeight.w500,
),
),
iconTheme: MaterialStateProperty.all(
IconThemeData(
color: lightColorScheme?.secondary,
),
),
),
scaffoldBackgroundColor: lightColorScheme?.background,
colorScheme: lightColorScheme?.harmonized(),
toggleableActiveColor: lightColorScheme?.primary,
textTheme: GoogleFonts.robotoTextTheme(ThemeData.light().textTheme),
);
ThemeData darkDynamicTheme = ThemeData(
final ThemeData darkDynamicTheme = ThemeData(
useMaterial3: true,
canvasColor: darkColorScheme?.background,
navigationBarTheme: NavigationBarThemeData(
backgroundColor: darkColorScheme?.background,
indicatorColor: darkColorScheme?.primary.withOpacity(0.4),
labelTextStyle: MaterialStateProperty.all(
GoogleFonts.roboto(
color: darkColorScheme?.secondary,
color: darkColorScheme?.onSurface,
fontWeight: FontWeight.w500,
),
),
iconTheme: MaterialStateProperty.all(
IconThemeData(
color: darkColorScheme?.secondary,
),
),
),
scaffoldBackgroundColor: darkColorScheme?.background,
colorScheme: darkColorScheme?.harmonized(),
toggleableActiveColor: darkColorScheme?.primary,
textTheme: GoogleFonts.robotoTextTheme(ThemeData.dark().textTheme),
);
return DynamicTheme(

View File

@ -1,10 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter/material.dart' hide SearchBar;
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/ui/widgets/appSelectorView/installed_app_item.dart';
import 'package:revanced_manager/ui/widgets/shared/search_bar.dart';
import 'package:revanced_manager/ui/widgets/appSelectorView/app_skeleton_loader.dart';
import 'package:stacked/stacked.dart' hide SkeletonLoader;
import 'package:revanced_manager/ui/views/app_selector/app_selector_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/appSelectorView/app_skeleton_loader.dart';
import 'package:revanced_manager/ui/widgets/appSelectorView/installed_app_item.dart';
import 'package:revanced_manager/ui/widgets/appSelectorView/not_installed_app_item.dart';
import 'package:revanced_manager/ui/widgets/shared/search_bar.dart';
import 'package:stacked/stacked.dart' hide SkeletonLoader;
class AppSelectorView extends StatefulWidget {
const AppSelectorView({Key? key}) : super(key: key);
@ -19,7 +20,7 @@ class _AppSelectorViewState extends State<AppSelectorView> {
@override
Widget build(BuildContext context) {
return ViewModelBuilder<AppSelectorViewModel>.reactive(
onModelReady: (model) => model.initialize(),
onViewModelReady: (model) => model.initialize(),
viewModelBuilder: () => AppSelectorViewModel(),
builder: (context, model, child) => Scaffold(
resizeToAvoidBottomInset: false,
@ -36,20 +37,19 @@ class _AppSelectorViewState extends State<AppSelectorView> {
SliverAppBar(
pinned: true,
floating: true,
snap: false,
title: I18nText(
'appSelectorView.viewTitle',
child: Text(
'',
style: TextStyle(
color: Theme.of(context).textTheme.headline6!.color,
color: Theme.of(context).textTheme.titleLarge!.color,
),
),
),
leading: IconButton(
icon: Icon(
Icons.arrow_back,
color: Theme.of(context).textTheme.headline6!.color,
color: Theme.of(context).textTheme.titleLarge!.color,
),
onPressed: () => Navigator.of(context).pop(),
),
@ -61,7 +61,6 @@ class _AppSelectorViewState extends State<AppSelectorView> {
horizontal: 12.0,
),
child: SearchBar(
showSelectIcon: false,
hintText: FlutterI18n.translate(
context,
'appSelectorView.searchBarHint',
@ -78,26 +77,64 @@ class _AppSelectorViewState extends State<AppSelectorView> {
SliverToBoxAdapter(
child: model.noApps
? Center(
child: I18nText('appSelectorCard.noAppsLabel'),
child: I18nText(
'appSelectorCard.noAppsLabel',
child: Text(
'',
style: TextStyle(
color:
Theme.of(context).textTheme.titleLarge!.color,
),
),
),
)
: model.apps.isEmpty
: model.allApps.isEmpty
? const AppSkeletonLoader()
: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0)
.copyWith(bottom: 80),
.copyWith(
bottom:
MediaQuery.viewPaddingOf(context).bottom + 8.0,
),
child: Column(
children: model
.getFilteredApps(_query)
.map((app) => InstalledAppItem(
children: [
...model
.getFilteredApps(_query)
.map(
(app) => InstalledAppItem(
name: app.appName,
pkgName: app.packageName,
icon: app.icon,
patchesCount:
model.patchesCount(app.packageName),
suggestedVersion:
model.getSuggestedVersion(
app.packageName,
),
installedVersion: app.versionName!,
onTap: () => model.canSelectInstalled(
context,
app.packageName,
),
),
)
.toList(),
...model
.getFilteredAppsNames(_query)
.map(
(app) => NotInstalledAppItem(
name: app,
patchesCount: model.patchesCount(app),
suggestedVersion:
model.getSuggestedVersion(app),
onTap: () {
model.selectApp(app);
Navigator.of(context).pop();
model.showDownloadToast();
},
))
.toList(),
),
)
.toList(),
const SizedBox(height: 70.0),
],
),
),
),

View File

@ -1,49 +1,196 @@
import 'dart:io';
import 'package:device_apps/device_apps.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/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';
class AppSelectorViewModel extends BaseViewModel {
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
final ManagerAPI _managerAPI = locator<ManagerAPI>();
final Toast _toast = locator<Toast>();
final List<ApplicationWithIcon> apps = [];
List<String> allApps = [];
bool noApps = false;
bool isRooted = false;
int patchesCount(String packageName) {
return _patcherAPI.getFilteredPatches(packageName).length;
}
List<Patch> patches = [];
Future<void> initialize() async {
apps.addAll(await _patcherAPI.getFilteredInstalledApps());
apps.sort((a, b) => a.appName.compareTo(b.appName));
noApps = apps.isEmpty;
patches = await _managerAPI.getPatches();
isRooted = _managerAPI.isRooted;
apps.addAll(
await _patcherAPI
.getFilteredInstalledApps(_managerAPI.areUniversalPatchesEnabled()),
);
apps.sort(
(a, b) => _patcherAPI
.getFilteredPatches(b.packageName)
.length
.compareTo(_patcherAPI.getFilteredPatches(a.packageName).length),
);
getAllApps();
notifyListeners();
}
void selectApp(ApplicationWithIcon application) async {
List<String> getAllApps() {
allApps = patches
.expand((e) => e.compatiblePackages.map((p) => p.name))
.toSet()
.where((name) => !apps.any((app) => app.packageName == name))
.toList();
noApps = allApps.isEmpty;
return allApps;
}
String getSuggestedVersion(String packageName) {
return _patcherAPI.getSuggestedVersion(packageName);
}
Future<bool> checkSplitApk(String packageName) async {
final app = await DeviceApps.getApp(packageName);
if (app != null) {
return app.isSplit;
}
return true;
}
Future<void> selectApp(ApplicationWithIcon application) async {
locator<PatcherViewModel>().selectedApp = PatchedApplication(
name: application.appName,
packageName: application.packageName,
originalPackageName: application.packageName,
version: application.versionName!,
apkFilePath: application.apkFilePath,
icon: application.icon,
patchDate: DateTime.now(),
);
locator<PatcherViewModel>().selectedPatches.clear();
locator<PatcherViewModel>().notifyListeners();
locator<PatcherViewModel>().loadLastSelectedPatches();
}
Future<void> canSelectInstalled(
BuildContext context,
String packageName,
) async {
final app =
await DeviceApps.getApp(packageName, true) as ApplicationWithIcon?;
if (app != null) {
if (await checkSplitApk(packageName) && !isRooted) {
return showSelectFromStorageDialog(context);
} else if (!await checkSplitApk(packageName) || isRooted) {
selectApp(app);
Navigator.pop(context);
}
}
}
Future showSelectFromStorageDialog(BuildContext context) async {
return showDialog(
context: context,
builder: (context) => SimpleDialog(
alignment: Alignment.center,
contentPadding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
children: [
const SizedBox(height: 10),
Icon(
Icons.block,
size: 28,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 20),
I18nText(
'appSelectorView.featureNotAvailable',
child: const Text(
'',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
wordSpacing: 1.5,
),
),
),
const SizedBox(height: 20),
I18nText(
'appSelectorView.featureNotAvailableText',
child: const Text(
'',
style: TextStyle(
fontSize: 14,
),
),
),
const SizedBox(height: 30),
CustomMaterialButton(
onPressed: () => selectAppFromStorage(context).then(
(_) {
Navigator.pop(context);
Navigator.pop(context);
},
),
label: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.sd_card),
const SizedBox(width: 10),
I18nText('appSelectorView.selectFromStorageButton'),
],
),
),
const SizedBox(height: 10),
CustomMaterialButton(
isFilled: false,
onPressed: () {
Navigator.pop(context);
},
label: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(width: 10),
I18nText('cancelButton'),
],
),
),
],
),
);
}
Future<void> selectAppFromStorage(BuildContext context) async {
try {
FilePickerResult? result = await FilePicker.platform.pickFiles(
final FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['apk'],
);
if (result != null && result.files.single.path != null) {
File apkFile = File(result.files.single.path!);
ApplicationWithIcon? application = await DeviceApps.getAppFromStorage(
final File apkFile = File(result.files.single.path!);
final List<String> pathSplit = result.files.single.path!.split('/');
pathSplit.removeLast();
final Directory filePickerCacheDir = Directory(pathSplit.join('/'));
final Iterable<File> deletableFiles =
(await filePickerCacheDir.list().toList()).whereType<File>();
for (final file in deletableFiles) {
if (file.path != apkFile.path && file.path.endsWith('.apk')) {
file.delete();
}
}
final ApplicationWithIcon? application =
await DeviceApps.getAppFromStorage(
apkFile.path,
true,
) as ApplicationWithIcon?;
@ -51,27 +198,46 @@ class AppSelectorViewModel extends BaseViewModel {
locator<PatcherViewModel>().selectedApp = PatchedApplication(
name: application.appName,
packageName: application.packageName,
originalPackageName: application.packageName,
version: application.versionName!,
apkFilePath: result.files.single.path!,
icon: application.icon,
patchDate: DateTime.now(),
isFromStorage: true,
);
locator<PatcherViewModel>().selectedPatches.clear();
locator<PatcherViewModel>().notifyListeners();
locator<PatcherViewModel>().loadLastSelectedPatches();
}
}
} on Exception {
_toast.show('appSelectorView.errorMessage');
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
_toast.showBottom('appSelectorView.errorMessage');
}
}
List<ApplicationWithIcon> getFilteredApps(String query) {
return apps
.where((app) =>
query.isEmpty ||
query.length < 2 ||
app.appName.toLowerCase().contains(query.toLowerCase()))
.where(
(app) =>
query.isEmpty ||
query.length < 2 ||
app.appName.toLowerCase().contains(query.toLowerCase()),
)
.toList();
}
List<String> getFilteredAppsNames(String query) {
return allApps
.where(
(app) =>
query.isEmpty ||
query.length < 2 ||
app.toLowerCase().contains(query.toLowerCase()),
)
.toList();
}
void showDownloadToast() =>
_toast.showBottom('appSelectorView.downloadToast');
}

View File

@ -13,7 +13,7 @@ class ContributorsView extends StatelessWidget {
Widget build(BuildContext context) {
return ViewModelBuilder<ContributorsViewModel>.reactive(
viewModelBuilder: () => ContributorsViewModel(),
onModelReady: (model) => model.getContributors(),
onViewModelReady: (model) => model.getContributors(),
builder: (context, model, child) => Scaffold(
body: CustomScrollView(
slivers: <Widget>[
@ -23,7 +23,7 @@ class ContributorsView extends StatelessWidget {
child: Text(
'',
style: GoogleFonts.inter(
color: Theme.of(context).textTheme.headline6!.color,
color: Theme.of(context).textTheme.titleLarge!.color,
),
),
),
@ -57,6 +57,7 @@ class ContributorsView extends StatelessWidget {
title: 'contributorsView.managerContributors',
contributors: model.managerContributors,
),
SizedBox(height: MediaQuery.viewPaddingOf(context).bottom),
],
),
),

View File

@ -11,7 +11,7 @@ class ContributorsViewModel extends BaseViewModel {
List<dynamic> managerContributors = [];
Future<void> getContributors() async {
Map<String, List<dynamic>> contributors =
final Map<String, List<dynamic>> contributors =
await _managerAPI.getContributors();
patcherContributors = contributors[_managerAPI.defaultPatcherRepo] ?? [];
patchesContributors = contributors[_managerAPI.getPatchesRepo()] ?? [];

View File

@ -1,13 +1,10 @@
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';
import 'package:revanced_manager/ui/widgets/homeView/installed_apps_card.dart';
import 'package:revanced_manager/ui/widgets/homeView/latest_commit_card.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_chip.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
import 'package:stacked/stacked.dart';
@ -18,12 +15,11 @@ class HomeView extends StatelessWidget {
Widget build(BuildContext context) {
return ViewModelBuilder<HomeViewModel>.reactive(
disposeViewModel: false,
onModelReady: (model) => model.initialize(context),
fireOnViewModelReadyOnce: true,
onViewModelReady: (model) => model.initialize(context),
viewModelBuilder: () => locator<HomeViewModel>(),
builder: (context, model, child) => Scaffold(
body: RefreshIndicator(
color: Theme.of(context).colorScheme.secondary,
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
onRefresh: () => model.forceRefresh(context),
child: CustomScrollView(
slivers: <Widget>[
@ -34,7 +30,7 @@ class HomeView extends StatelessWidget {
child: Text(
'',
style: GoogleFonts.inter(
color: Theme.of(context).textTheme.headline6!.color,
color: Theme.of(context).textTheme.titleLarge!.color,
),
),
),
@ -48,63 +44,21 @@ class HomeView extends StatelessWidget {
'homeView.updatesSubtitle',
child: Text(
'',
style: Theme.of(context).textTheme.headline6!,
style: Theme.of(context).textTheme.titleLarge,
),
),
const SizedBox(height: 10),
LatestCommitCard(
onPressed: () =>
model.showUpdateConfirmationDialog(context),
),
LatestCommitCard(model: model, parentContext: context),
const SizedBox(height: 23),
I18nText(
'homeView.patchedSubtitle',
child: Text(
'',
style: Theme.of(context).textTheme.headline6!,
style: Theme.of(context).textTheme.titleLarge,
),
),
const SizedBox(height: 8),
Row(
children: <Widget>[
DashboardChip(
label: I18nText('homeView.updatesAvailable'),
isSelected: model.showUpdatableApps,
onSelected: (value) {
model.toggleUpdatableApps(true);
},
),
const SizedBox(width: 10),
DashboardChip(
label: I18nText('homeView.installed'),
isSelected: !model.showUpdatableApps,
onSelected: (value) {
model.toggleUpdatableApps(false);
},
)
],
),
const SizedBox(height: 14),
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(),
),
const SizedBox(height: 10),
InstalledAppsCard(),
],
),
),

View File

@ -1,52 +1,86 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:async';
import 'dart:io';
import 'package:app_installer/app_installer.dart';
import 'package:cross_connectivity/cross_connectivity.dart';
import 'package:device_apps/device_apps.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:injectable/injectable.dart';
import 'package:install_plugin/install_plugin.dart';
import 'package:path_provider/path_provider.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/app/app.router.dart';
import 'package:revanced_manager/models/patched_application.dart';
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/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/homeView/update_confirmation_dialog.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 GithubAPI _githubAPI = locator<GithubAPI>();
final RevancedAPI _revancedAPI = locator<RevancedAPI>();
final Toast _toast = locator<Toast>();
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
DateTime? _lastUpdate;
bool showUpdatableApps = true;
bool showUpdatableApps = false;
List<PatchedApplication> patchedInstalledApps = [];
List<PatchedApplication> patchedUpdatableApps = [];
String? _latestManagerVersion = '';
File? downloadedApk;
Future<void> initialize(BuildContext context) async {
_latestManagerVersion = await _managerAPI.getLatestManagerVersion();
if (!_managerAPI.getPatchesConsent()) {
await showPatchesConsent(context);
}
await _patcherAPI.initialize();
await flutterLocalNotificationsPlugin.initialize(
const InitializationSettings(
android: AndroidInitializationSettings('ic_notification'),
),
onSelectNotification: (p) =>
DeviceApps.openApp('app.revanced.manager.flutter'),
onDidReceiveNotificationResponse: (response) async {
if (response.id == 0) {
_toast.showBottom('homeView.installingMessage');
final File? managerApk = await _managerAPI.downloadManager();
if (managerApk != null) {
await InstallPlugin.installApk(managerApk.path);
} else {
_toast.showBottom('homeView.errorDownloadMessage');
}
}
},
);
flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.requestPermission();
bool isConnected = await Connectivity().checkConnection();
final bool isConnected = await Connectivity().checkConnection();
if (!isConnected) {
_toast.show('homeView.noConnection');
_toast.showBottom('homeView.noConnection');
}
final NotificationAppLaunchDetails? notificationAppLaunchDetails =
await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) {
_toast.showBottom('homeView.installingMessage');
final File? managerApk = await _managerAPI.downloadManager();
if (managerApk != null) {
await InstallPlugin.installApk(managerApk.path);
} else {
_toast.showBottom('homeView.errorDownloadMessage');
}
}
_getPatchedApps();
_managerAPI.reAssessSavedApps().then((_) => _getPatchedApps());
@ -64,7 +98,7 @@ class HomeViewModel extends BaseViewModel {
notifyListeners();
}
void navigateToPatcher(PatchedApplication app) async {
Future<void> navigateToPatcher(PatchedApplication app) async {
locator<PatcherViewModel>().selectedApp = app;
locator<PatcherViewModel>().selectedPatches =
await _patcherAPI.getAppliedPatches(app.appliedPatches);
@ -73,10 +107,7 @@ class HomeViewModel extends BaseViewModel {
}
void _getPatchedApps() {
patchedInstalledApps = _managerAPI
.getPatchedApps()
.where((app) => app.hasUpdates == false)
.toList();
patchedInstalledApps = _managerAPI.getPatchedApps().toList();
patchedUpdatableApps = _managerAPI
.getPatchedApps()
.where((app) => app.hasUpdates == true)
@ -85,100 +116,365 @@ class HomeViewModel extends BaseViewModel {
}
Future<bool> hasManagerUpdates() async {
String? latestVersion = await _managerAPI.getLatestManagerVersion();
String currentVersion = await _managerAPI.getCurrentManagerVersion();
// add v to current version
if (!currentVersion.startsWith('v')) {
currentVersion = 'v$currentVersion';
}
_latestManagerVersion =
await _managerAPI.getLatestManagerVersion() ?? currentVersion;
if (_latestManagerVersion != currentVersion) {
return true;
}
return false;
}
Future<bool> hasPatchesUpdates() async {
final String? latestVersion = await _managerAPI.getLatestPatchesVersion();
final String currentVersion = await _managerAPI.getCurrentPatchesVersion();
if (latestVersion != null) {
try {
int latestVersionInt =
final int latestVersionInt =
int.parse(latestVersion.replaceAll(RegExp('[^0-9]'), ''));
int currentVersionInt =
final int currentVersionInt =
int.parse(currentVersion.replaceAll(RegExp('[^0-9]'), ''));
return latestVersionInt > currentVersionInt;
} on Exception {
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return false;
}
}
return false;
}
Future<void> updateManager(BuildContext context) async {
Future<File?> downloadManager() async {
try {
_toast.show('homeView.downloadingMessage');
File? managerApk = await _managerAPI.downloadManager();
if (managerApk != null) {
await flutterLocalNotificationsPlugin.zonedSchedule(
0,
FlutterI18n.translate(
context,
'homeView.notificationTitle',
),
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);
} else {
_toast.show('homeView.errorDownloadMessage');
final response = await _revancedAPI.downloadManager();
final bytes = await response!.readAsBytes();
final tempDir = await getTemporaryDirectory();
final tempFile = File('${tempDir.path}/revanced-manager.apk');
await tempFile.writeAsBytes(bytes);
return tempFile;
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
} on Exception {
_toast.show('homeView.errorInstallMessage');
return null;
}
}
Future<void> showUpdateConfirmationDialog(BuildContext parentContext) async {
return showDialog(
context: parentContext,
Future<void> showPatchesConsent(BuildContext context) async {
final ValueNotifier<bool> autoUpdate = ValueNotifier(true);
await showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: I18nText('homeView.updateDialogTitle'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('homeView.updateDialogText'),
actions: <Widget>[
title: const Text('Download ReVanced Patches?'),
content: ValueListenableBuilder(
valueListenable: autoUpdate,
builder: (context, value, child) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
I18nText(
'homeView.patchesConsentDialogText',
child: Text(
'',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.secondary,
),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: I18nText(
'homeView.patchesConsentDialogText2',
translationParams: {
'url': _managerAPI.defaultApiUrl.split('/')[2],
},
child: Text(
'',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.error,
),
),
),
),
CheckboxListTile(
value: value,
contentPadding: EdgeInsets.zero,
title: I18nText(
'homeView.patchesConsentDialogText3',
),
subtitle: I18nText(
'homeView.patchesConsentDialogText3Sub',
),
onChanged: (selected) {
autoUpdate.value = selected!;
},
),
],
);
},
),
actions: [
CustomMaterialButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(),
onPressed: () async {
await _managerAPI.setPatchesConsent(false);
SystemNavigator.pop();
},
label: I18nText('quitButton'),
),
CustomMaterialButton(
label: I18nText('yesButton'),
onPressed: () {
onPressed: () async {
await _managerAPI.setPatchesConsent(true);
await _managerAPI.setPatchesAutoUpdate(autoUpdate.value);
Navigator.of(context).pop();
updateManager(parentContext);
},
)
label: I18nText('okButton'),
),
],
),
);
}
Future<String?> getLatestPatcherReleaseTime() async {
return _managerAPI.getLatestPatcherReleaseTime();
Future<void> updatePatches(BuildContext context) async {
_toast.showBottom('homeView.downloadingMessage');
final String patchesVersion =
await _managerAPI.getLatestPatchesVersion() ?? '0.0.0';
final String integrationsVersion =
await _managerAPI.getLatestIntegrationsVersion() ?? '0.0.0';
if (patchesVersion != '0.0.0' && integrationsVersion != '0.0.0') {
await _managerAPI.setCurrentPatchesVersion(patchesVersion);
await _managerAPI.setCurrentIntegrationsVersion(integrationsVersion);
_toast.showBottom('homeView.downloadedMessage');
forceRefresh(context);
} else {
_toast.showBottom('homeView.errorDownloadMessage');
}
}
Future<String?> getLatestManagerReleaseTime() async {
Future<void> updateManager(BuildContext context) async {
final ValueNotifier<bool> downloaded = ValueNotifier(false);
try {
_toast.showBottom('homeView.downloadingMessage');
showDialog(
context: context,
builder: (context) => ValueListenableBuilder(
valueListenable: downloaded,
builder: (context, value, child) {
return SimpleDialog(
contentPadding: const EdgeInsets.all(16.0),
title: I18nText(
!value
? 'homeView.downloadingMessage'
: 'homeView.downloadedMessage',
child: Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.secondary,
),
),
),
children: [
Column(
children: [
Row(
children: [
Icon(
Icons.new_releases_outlined,
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(width: 8.0),
Text(
'$_latestManagerVersion',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.secondary,
),
),
],
),
const SizedBox(height: 16.0),
if (!value)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
StreamBuilder<double>(
initialData: 0.0,
stream: _revancedAPI.managerUpdateProgress.stream,
builder: (context, snapshot) {
return LinearProgressIndicator(
value: snapshot.data! * 0.01,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.secondary,
),
);
},
),
const SizedBox(height: 16.0),
Align(
alignment: Alignment.centerRight,
child: CustomMaterialButton(
label: I18nText('cancelButton'),
onPressed: () {
_revancedAPI.disposeManagerUpdateProgress();
Navigator.of(context).pop();
},
),
),
],
),
if (value)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
I18nText(
'homeView.installUpdate',
child: Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.secondary,
),
),
),
const SizedBox(height: 16.0),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Align(
alignment: Alignment.centerRight,
child: CustomMaterialButton(
isFilled: false,
label: I18nText('cancelButton'),
onPressed: () {
Navigator.of(context).pop();
},
),
),
const SizedBox(width: 8.0),
Align(
alignment: Alignment.centerRight,
child: CustomMaterialButton(
label: I18nText('updateButton'),
onPressed: () async {
await InstallPlugin.installApk(
downloadedApk!.path,
);
},
),
),
],
),
],
),
],
),
],
);
},
),
);
final File? managerApk = await downloadManager();
if (managerApk != null) {
downloaded.value = true;
downloadedApk = managerApk;
// await flutterLocalNotificationsPlugin.zonedSchedule(
// 0,
// FlutterI18n.translate(
// context,
// 'homeView.notificationTitle',
// ),
// 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.showBottom('homeView.installingMessage');
await InstallPlugin.installApk(managerApk.path);
} else {
_toast.showBottom('homeView.errorDownloadMessage');
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
_toast.showBottom('homeView.errorInstallMessage');
}
}
void updatesAreDisabled() {
_toast.showBottom('homeView.updatesDisabled');
}
Future<void> showUpdateConfirmationDialog(
BuildContext parentContext,
bool isPatches,
) {
return showModalBottomSheet(
context: parentContext,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24.0)),
),
builder: (context) => UpdateConfirmationDialog(
isPatches: isPatches,
),
);
}
Future<Map<String, dynamic>?> getLatestManagerRelease() {
return _githubAPI.getLatestManagerRelease(_managerAPI.defaultManagerRepo);
}
Future<Map<String, dynamic>?> getLatestPatchesRelease() {
return _githubAPI.getLatestPatchesRelease(_managerAPI.defaultPatchesRepo);
}
Future<String?> getLatestPatchesReleaseTime() {
return _managerAPI.getLatestPatchesReleaseTime();
}
Future<String?> getLatestManagerReleaseTime() {
return _managerAPI.getLatestManagerReleaseTime();
}
Future<void> forceRefresh(BuildContext context) async {
await Future.delayed(const Duration(seconds: 1));
if (_lastUpdate == null ||
_lastUpdate!.difference(DateTime.now()).inSeconds > 60) {
_lastUpdate!.difference(DateTime.now()).inSeconds > 2) {
_managerAPI.clearAllData();
}
_toast.showBottom('homeView.refreshSuccess');
initialize(context);
}
}

View File

@ -2,10 +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/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';
import 'package:stacked/stacked.dart';
@ -15,133 +13,90 @@ class InstallerView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ViewModelBuilder<InstallerViewModel>.reactive(
onModelReady: (model) => model.initialize(context),
onViewModelReady: (model) => model.initialize(context),
viewModelBuilder: () => InstallerViewModel(),
builder: (context, model, child) => WillPopScope(
child: Scaffold(
body: CustomScrollView(
controller: model.scrollController,
slivers: <Widget>[
CustomSliverAppBar(
title: Text(
model.headerLogs,
style: GoogleFonts.inter(
color: Theme.of(context).textTheme.headline6!.color,
),
),
onBackButtonPressed: () => model.onWillPop(context),
actions: <Widget>[
Visibility(
visible: !model.isPatching && !model.hasErrors,
child: CustomPopupMenu(
onSelected: (value) => model.onMenuSelection(value),
children: {
0: I18nText(
'installerView.shareApkMenuOption',
child: const Text(
'',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
child: SafeArea(
top: false,
bottom: false,
child: Scaffold(
floatingActionButton: Visibility(
visible: !model.isPatching,
child: FloatingActionButton.extended(
label: I18nText('installerView.installButton'),
icon: const Icon(Icons.file_download_outlined),
onPressed: () => model.installTypeDialog(context),
elevation: 0,
),
),
floatingActionButtonLocation:
FloatingActionButtonLocation.endContained,
bottomNavigationBar: Visibility(
visible: !model.isPatching,
child: BottomAppBar(
child: Row(
children: <Widget>[
Visibility(
visible: !model.hasErrors,
child: IconButton.filledTonal(
tooltip: FlutterI18n.translate(
context,
'installerView.exportApkButtonTooltip',
),
1: I18nText(
'installerView.shareLogMenuOption',
child: const Text(
'',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
},
icon: const Icon(Icons.save),
onPressed: () => model.onButtonPressed(0),
),
),
IconButton.filledTonal(
tooltip: FlutterI18n.translate(
context,
'installerView.exportLogButtonTooltip',
),
icon: const Icon(Icons.post_add),
onPressed: () => model.onButtonPressed(1),
),
],
),
),
),
body: CustomScrollView(
controller: model.scrollController,
slivers: <Widget>[
CustomSliverAppBar(
title: Text(
model.headerLogs,
style: GoogleFonts.inter(
color: Theme.of(context).textTheme.titleLarge!.color,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
bottom: PreferredSize(
onBackButtonPressed: () => model.onWillPop(context),
bottom: PreferredSize(
preferredSize: const Size(double.infinity, 1.0),
child:
GradientProgressIndicator(progress: model.progress!)),
),
SliverPadding(
padding: const EdgeInsets.all(20.0),
sliver: SliverList(
delegate: SliverChildListDelegate.fixed(
<Widget>[
CustomCard(
child: Text(
model.logs,
style: GoogleFonts.jetBrainsMono(
fontSize: 13,
height: 1.5,
),
),
),
],
child: GradientProgressIndicator(progress: model.progress),
),
),
),
SliverFillRemaining(
hasScrollBody: false,
child: Align(
alignment: Alignment.bottomCenter,
child: Visibility(
visible: !model.isPatching && !model.hasErrors,
child: Padding(
padding: const EdgeInsets.all(20.0).copyWith(top: 0.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Visibility(
visible: model.isInstalled,
child: CustomMaterialButton(
label: I18nText('installerView.openButton'),
isExpanded: true,
onPressed: () {
model.openApp();
model.cleanPatcher();
Navigator.of(context).pop();
},
SliverPadding(
padding: const EdgeInsets.all(20.0),
sliver: SliverList(
delegate: SliverChildListDelegate.fixed(
<Widget>[
CustomCard(
child: Text(
model.logs,
style: GoogleFonts.jetBrainsMono(
fontSize: 13,
height: 1.5,
),
),
Visibility(
visible: !model.isInstalled,
child: CustomMaterialButton(
isFilled: false,
label:
I18nText('installerView.installRootButton'),
isExpanded: true,
onPressed: () => model.installResult(
context,
true,
),
),
),
Visibility(
visible: !model.isInstalled,
child: const SizedBox(
width: 16,
),
),
Visibility(
visible: !model.isInstalled,
child: CustomMaterialButton(
label: I18nText('installerView.installButton'),
isExpanded: true,
onPressed: () => model.installResult(
context,
false,
),
),
),
],
),
),
],
),
),
),
),
],
],
),
),
),
onWillPop: () => model.onWillPop(context),

View File

@ -1,4 +1,6 @@
// ignore_for_file: use_build_context_synchronously
import 'package:device_apps/device_apps.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_background/flutter_background.dart';
@ -9,15 +11,17 @@ import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/services/root_api.dart';
import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/installerView/custom_material_button.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:stacked/stacked.dart';
import 'package:wakelock/wakelock.dart';
class InstallerViewModel extends BaseViewModel {
final ManagerAPI _managerAPI = locator<ManagerAPI>();
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
final RootAPI _rootAPI = RootAPI();
final Toast _toast = locator<Toast>();
final PatchedApplication _app = locator<PatcherViewModel>().selectedApp!;
final List<Patch> _patches = locator<PatcherViewModel>().selectedPatches;
@ -28,11 +32,15 @@ class InstallerViewModel extends BaseViewModel {
double? progress = 0.0;
String logs = '';
String headerLogs = '';
bool isRooted = false;
bool isPatching = true;
bool isInstalled = false;
bool hasErrors = false;
bool isCanceled = false;
bool cancel = false;
Future<void> initialize(BuildContext context) async {
isRooted = await _rootAPI.isRooted();
if (await Permission.ignoreBatteryOptimizations.isGranted) {
try {
FlutterBackground.initialize(
@ -45,15 +53,15 @@ class InstallerViewModel extends BaseViewModel {
context,
'installerView.notificationText',
),
notificationImportance: AndroidNotificationImportance.Default,
notificationIcon: const AndroidResource(
name: 'ic_notification',
defType: 'drawable',
),
),
).then((value) => FlutterBackground.enableBackgroundExecution());
} on Exception {
// ignore
} on Exception catch (e) {
if (kDebugMode) {
print(e);
} // ignore
}
}
await Wakelock.enable();
@ -66,10 +74,10 @@ class InstallerViewModel extends BaseViewModel {
switch (call.method) {
case 'update':
if (call.arguments != null) {
Map<dynamic, dynamic> arguments = call.arguments;
double progress = arguments['progress'];
String header = arguments['header'];
String log = arguments['log'];
final Map<dynamic, dynamic> arguments = call.arguments;
final double progress = arguments['progress'];
final String header = arguments['header'];
final String log = arguments['log'];
update(progress, header, log);
}
break;
@ -77,7 +85,7 @@ class InstallerViewModel extends BaseViewModel {
});
}
void update(double value, String header, String log) {
Future<void> update(double value, String header, String log) async {
if (value >= 0.0) {
progress = value;
}
@ -89,6 +97,11 @@ class InstallerViewModel extends BaseViewModel {
} else if (value == 1.0) {
isPatching = false;
hasErrors = false;
await _managerAPI.savePatches(
_patcherAPI.getFilteredPatches(_app.packageName),
_app.packageName,
);
await _managerAPI.setUsedPatches(_patches, _app.packageName);
} else if (value == -100.0) {
isPatching = false;
hasErrors = true;
@ -116,125 +129,262 @@ class InstallerViewModel extends BaseViewModel {
}
Future<void> runPatcher() async {
update(0.0, 'Initializing...', 'Initializing installer');
if (_patches.isNotEmpty) {
try {
update(0.1, '', 'Creating working directory');
await _patcherAPI.runPatcher(
_app.packageName,
_app.apkFilePath,
_patches,
);
} catch (e) {
update(
-100.0,
'Aborting...',
'An error occurred! Aborting\nError:\n$e',
);
try {
update(0.0, 'Initializing...', 'Initializing installer');
if (_patches.isNotEmpty) {
try {
update(0.1, '', 'Creating working directory');
await _patcherAPI.runPatcher(
_app.packageName,
_app.apkFilePath,
_patches,
);
} on Exception catch (e) {
update(
-100.0,
'Aborted...',
'An error occurred! Aborted\nError:\n$e',
);
if (kDebugMode) {
print(e);
}
}
} else {
update(-100.0, 'Aborted...', 'No app or patches selected! Aborted');
}
} else {
update(-100.0, 'Aborting...', 'No app or patches selected! Aborting');
}
if (FlutterBackground.isBackgroundExecutionEnabled) {
try {
FlutterBackground.disableBackgroundExecution();
} on Exception {
// ignore
if (FlutterBackground.isBackgroundExecutionEnabled) {
try {
FlutterBackground.disableBackgroundExecution();
} on Exception catch (e) {
if (kDebugMode) {
print(e);
} // ignore
}
}
await Wakelock.disable();
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
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(
Future<void> installTypeDialog(BuildContext context) async {
final ValueNotifier<int> installType = ValueNotifier(0);
if (isRooted) {
await showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: I18nText('installerView.installErrorDialogTitle'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText(
rootMicroG
? 'installerView.installErrorDialogText1'
: rootFromStorage
? 'installerView.installErrorDialogText3'
: 'installerView.installErrorDialogText2',
title: I18nText(
'installerView.installType',
),
actions: <Widget>[
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
icon: const Icon(Icons.file_download_outlined),
contentPadding: const EdgeInsets.symmetric(vertical: 16),
content: ValueListenableBuilder(
valueListenable: installType,
builder: (context, value, child) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 10,
),
child: I18nText(
'installerView.installTypeDescription',
child: Text(
'',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.secondary,
),
),
),
),
RadioListTile(
title: I18nText('installerView.installNonRootType'),
subtitle: I18nText('installerView.installRecommendedType'),
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
value: 0,
groupValue: value,
onChanged: (selected) {
installType.value = selected!;
},
),
RadioListTile(
title: I18nText('installerView.installRootType'),
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
value: 1,
groupValue: value,
onChanged: (selected) {
installType.value = selected!;
},
),
],
);
},
),
actions: [
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () => Navigator.of(context).pop(),
)
label: I18nText('cancelButton'),
isFilled: false,
onPressed: () {
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('installerView.installButton'),
onPressed: () {
Navigator.of(context).pop();
installResult(context, installType.value == 1);
},
),
],
),
);
} else {
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);
installResult(context, false);
}
}
Future<void> stopPatcher() async {
try {
isCanceled = true;
update(0.5, 'Aborting...', 'Canceling patching process');
await _patcherAPI.stopPatcher();
update(-100.0, 'Aborted...', 'Press back to exit');
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
void shareResult() {
_patcherAPI.sharePatchedFile(_app.name, _app.version);
Future<void> installResult(BuildContext context, bool installAsRoot) async {
try {
_app.isRooted = installAsRoot;
final bool hasMicroG =
_patches.any((p) => p.name.endsWith('MicroG support'));
final bool rootMicroG = installAsRoot && hasMicroG;
final bool rootFromStorage = installAsRoot && _app.isFromStorage;
final 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);
}
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
void shareLog() {
_patcherAPI.sharePatcherLog(logs);
void exportResult() {
try {
_patcherAPI.exportPatchedFile(_app.name, _app.version);
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
void exportLog() {
_patcherAPI.exportPatcherLog(logs);
}
Future<void> cleanPatcher() async {
_patcherAPI.cleanPatcher();
locator<PatcherViewModel>().selectedApp = null;
locator<PatcherViewModel>().selectedPatches.clear();
locator<PatcherViewModel>().notifyListeners();
try {
_patcherAPI.cleanPatcher();
locator<PatcherViewModel>().selectedApp = null;
locator<PatcherViewModel>().selectedPatches.clear();
locator<PatcherViewModel>().notifyListeners();
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
void openApp() {
DeviceApps.openApp(_app.packageName);
}
void onMenuSelection(int value) {
void onButtonPressed(int value) {
switch (value) {
case 0:
shareResult();
exportResult();
break;
case 1:
shareLog();
exportLog();
break;
}
}
Future<bool> onWillPop(BuildContext context) async {
if (isPatching) {
_toast.show('installerView.noExit');
if (!cancel) {
cancel = true;
_toast.showBottom('installerView.pressBackAgain');
} else if (!isCanceled) {
await stopPatcher();
} else {
_toast.showBottom('installerView.noExit');
}
return false;
}
cleanPatcher();
if (!cancel) {
cleanPatcher();
} else {
_patcherAPI.cleanPatcher();
}
Navigator.of(context).pop();
return true;
}

View File

@ -11,60 +11,70 @@ class NavigationView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ViewModelBuilder<NavigationViewModel>.reactive(
onModelReady: (model) => model.initialize(context),
onViewModelReady: (model) => model.initialize(context),
viewModelBuilder: () => locator<NavigationViewModel>(),
builder: (context, model, child) => Scaffold(
body: PageTransitionSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder: (
Widget child,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
fillColor: Theme.of(context).colorScheme.surface,
child: child,
);
},
child: model.getViewForIndex(model.currentIndex),
),
bottomNavigationBar: NavigationBar(
onDestinationSelected: model.setIndex,
selectedIndex: model.currentIndex,
destinations: <Widget>[
NavigationDestination(
icon: model.isIndexSelected(0)
? const Icon(Icons.dashboard)
: const Icon(Icons.dashboard_outlined),
label: FlutterI18n.translate(
context,
'navigationView.dashboardTab',
builder: (context, model, child) => WillPopScope(
onWillPop: () async {
if (model.currentIndex == 0) {
return true;
} else {
model.setIndex(0);
return false;
}
},
child: Scaffold(
body: PageTransitionSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder: (
Widget child,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
fillColor: Theme.of(context).colorScheme.surface,
child: child,
);
},
child: model.getViewForIndex(model.currentIndex),
),
bottomNavigationBar: NavigationBar(
onDestinationSelected: model.setIndex,
selectedIndex: model.currentIndex,
destinations: <Widget>[
NavigationDestination(
icon: model.isIndexSelected(0)
? const Icon(Icons.dashboard)
: const Icon(Icons.dashboard_outlined),
label: FlutterI18n.translate(
context,
'navigationView.dashboardTab',
),
tooltip: '',
),
tooltip: '',
),
NavigationDestination(
icon: model.isIndexSelected(1)
? const Icon(Icons.build)
: const Icon(Icons.build_outlined),
label: FlutterI18n.translate(
context,
'navigationView.patcherTab',
NavigationDestination(
icon: model.isIndexSelected(1)
? const Icon(Icons.build)
: const Icon(Icons.build_outlined),
label: FlutterI18n.translate(
context,
'navigationView.patcherTab',
),
tooltip: '',
),
tooltip: '',
),
NavigationDestination(
icon: model.isIndexSelected(2)
? const Icon(Icons.settings)
: const Icon(Icons.settings_outlined),
label: FlutterI18n.translate(
context,
'navigationView.settingsTab',
NavigationDestination(
icon: model.isIndexSelected(2)
? const Icon(Icons.settings)
: const Icon(Icons.settings_outlined),
label: FlutterI18n.translate(
context,
'navigationView.settingsTab',
),
tooltip: '',
),
tooltip: '',
),
],
],
),
),
),
);

View File

@ -15,10 +15,14 @@ import 'package:stacked/stacked.dart';
@lazySingleton
class NavigationViewModel extends IndexTrackingViewModel {
void initialize(BuildContext context) async {
Future<void> initialize(BuildContext context) async {
locator<Toast>().initialize(context);
SharedPreferences prefs = await SharedPreferences.getInstance();
final SharedPreferences prefs = await SharedPreferences.getInstance();
requestManageExternalStorage();
if (prefs.getBool('permissionsRequested') == null) {
await Permission.storage.request();
await Permission.manageExternalStorage.request();
await prefs.setBool('permissionsRequested', true);
RootAPI().hasRootPermissions().then(
(value) => Permission.requestInstallPackages.request().then(
@ -26,9 +30,10 @@ class NavigationViewModel extends IndexTrackingViewModel {
),
);
}
if (prefs.getBool('useDarkTheme') == null) {
bool isDark =
MediaQuery.of(context).platformBrightness != Brightness.light;
final bool isDark =
MediaQuery.platformBrightnessOf(context) != Brightness.light;
await prefs.setBool('useDarkTheme', isDark);
await DynamicTheme.of(context)!.setTheme(isDark ? 1 : 0);
}
@ -44,6 +49,17 @@ class NavigationViewModel extends IndexTrackingViewModel {
);
}
Future<void> requestManageExternalStorage() async {
final manageExternalStorageStatus =
await Permission.manageExternalStorage.status;
if (manageExternalStorageStatus.isDenied) {
await Permission.manageExternalStorage.request();
}
if (manageExternalStorageStatus.isPermanentlyDenied) {
await openAppSettings();
}
}
Widget getViewForIndex(int index) {
switch (index) {
case 0:

View File

@ -22,7 +22,7 @@ class PatcherView extends StatelessWidget {
child: FloatingActionButton.extended(
label: I18nText('patcherView.patchButton'),
icon: const Icon(Icons.build),
onPressed: () => model.showPatchConfirmationDialog(context),
onPressed: () => model.showRemovedPatchesDialog(context),
),
),
body: CustomScrollView(
@ -34,7 +34,7 @@ class PatcherView extends StatelessWidget {
child: Text(
'',
style: GoogleFonts.inter(
color: Theme.of(context).textTheme.headline6!.color,
color: Theme.of(context).textTheme.titleLarge!.color,
),
),
),

View File

@ -1,4 +1,5 @@
import 'package:device_apps/device_apps.dart';
// ignore_for_file: use_build_context_synchronously
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:injectable/injectable.dart';
@ -6,17 +7,22 @@ import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/app/app.router.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/ui/widgets/installerView/custom_material_button.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/utils/about_info.dart';
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
@lazySingleton
class PatcherViewModel extends BaseViewModel {
final NavigationService _navigationService = locator<NavigationService>();
final ManagerAPI _managerAPI = locator<ManagerAPI>();
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
PatchedApplication? selectedApp;
List<Patch> selectedPatches = [];
List<String> removedPatches = [];
void navigateToAppSelector() {
_navigationService.navigateTo(Routes.appSelectorView);
@ -39,28 +45,59 @@ class PatcherViewModel extends BaseViewModel {
}
Future<bool> isValidPatchConfig() async {
bool needsResourcePatching =
await _patcherAPI.needsResourcePatching(selectedPatches);
final bool needsResourcePatching = await _patcherAPI.needsResourcePatching(
selectedPatches,
);
if (needsResourcePatching && selectedApp != null) {
Application? app = await DeviceApps.getApp(selectedApp!.packageName);
if (app != null && app.isSplit) {
return false;
}
final bool isSplit = await _managerAPI.isSplitApk(selectedApp!);
return !isSplit;
}
return true;
}
Future<void> showPatchConfirmationDialog(BuildContext context) async {
bool isValid = await isValidPatchConfig();
if (isValid) {
navigateToInstaller();
} else {
final bool isValid = await isValidPatchConfig();
if (context.mounted) {
if (isValid) {
showArmv7WarningDialog(context);
} else {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('warning'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('patcherView.splitApkWarningDialogText'),
actions: <Widget>[
CustomMaterialButton(
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(),
),
CustomMaterialButton(
label: I18nText('yesButton'),
isFilled: false,
onPressed: () {
Navigator.of(context).pop();
showArmv7WarningDialog(context);
},
),
],
),
);
}
}
}
Future<void> showRemovedPatchesDialog(BuildContext context) async {
if (removedPatches.isNotEmpty) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('patcherView.patchDialogTitle'),
title: I18nText('notice'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('patcherView.patchDialogText'),
content: I18nText(
'patcherView.removedPatchesWarningDialogText',
translationParams: {'patches': removedPatches.join('\n')},
),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
@ -73,10 +110,46 @@ class PatcherViewModel extends BaseViewModel {
Navigator.of(context).pop();
navigateToInstaller();
},
)
),
],
),
);
} else {
showArmv7WarningDialog(context);
}
}
Future<void> showArmv7WarningDialog(BuildContext context) async {
final bool armv7 = await AboutInfo.getInfo().then((info) {
final List<String> archs = info['supportedArch'];
final supportedAbis = ['arm64-v8a', 'x86', 'x86_64'];
return !archs.any((arch) => supportedAbis.contains(arch));
});
if (context.mounted && armv7) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('warning'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('patcherView.armv7WarningDialogText'),
actions: <Widget>[
CustomMaterialButton(
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(),
),
CustomMaterialButton(
label: I18nText('yesButton'),
isFilled: false,
onPressed: () {
Navigator.of(context).pop();
navigateToInstaller();
},
),
],
),
);
} else {
navigateToInstaller();
}
}
@ -88,23 +161,54 @@ class PatcherViewModel extends BaseViewModel {
return text;
}
String getRecommendedVersionString(BuildContext context) {
String recommendedVersion =
_patcherAPI.getRecommendedVersion(selectedApp!.packageName);
if (recommendedVersion.isEmpty) {
recommendedVersion = FlutterI18n.translate(
String getSuggestedVersionString(BuildContext context) {
String suggestedVersion =
_patcherAPI.getSuggestedVersion(selectedApp!.packageName);
if (suggestedVersion.isEmpty) {
suggestedVersion = FlutterI18n.translate(
context,
'appSelectorCard.anyVersion',
'appSelectorCard.allVersions',
);
} else {
recommendedVersion = 'v$recommendedVersion';
suggestedVersion = 'v$suggestedVersion';
}
return '${FlutterI18n.translate(
context,
'appSelectorCard.currentVersion',
)}: v${selectedApp!.version}\n${FlutterI18n.translate(
context,
'appSelectorCard.recommendedVersion',
)}: $recommendedVersion';
'appSelectorCard.suggestedVersion',
)}: $suggestedVersion';
}
Future<void> loadLastSelectedPatches() async {
this.selectedPatches.clear();
removedPatches.clear();
final List<String> selectedPatches =
await _managerAPI.getSelectedPatches(selectedApp!.originalPackageName);
final List<Patch> patches =
_patcherAPI.getFilteredPatches(selectedApp!.originalPackageName);
this
.selectedPatches
.addAll(patches.where((patch) => selectedPatches.contains(patch.name)));
if (!_managerAPI.isPatchesChangeEnabled()) {
this.selectedPatches.clear();
this.selectedPatches.addAll(patches.where((patch) => !patch.excluded));
}
if (!_managerAPI.areExperimentalPatchesEnabled()) {
this.selectedPatches.removeWhere((patch) => !isPatchSupported(patch));
}
if (!_managerAPI.areUniversalPatchesEnabled()) {
this
.selectedPatches
.removeWhere((patch) => patch.compatiblePackages.isEmpty);
}
final usedPatches = _managerAPI.getUsedPatches(selectedApp!.originalPackageName);
for (final patch in usedPatches){
if (!patches.any((p) => p.name == patch.name)){
removedPatches.add('\u2022 ${patch.name}');
}
}
notifyListeners();
}
}

View File

@ -1,8 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter/material.dart' hide SearchBar;
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_item.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_popup_menu.dart';
import 'package:revanced_manager/ui/widgets/shared/search_bar.dart';
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
import 'package:stacked/stacked.dart';
class PatchesSelectorView extends StatefulWidget {
@ -14,18 +18,35 @@ class PatchesSelectorView extends StatefulWidget {
class _PatchesSelectorViewState extends State<PatchesSelectorView> {
String _query = '';
final _managerAPI = locator<ManagerAPI>();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (!_managerAPI.isPatchesChangeEnabled() &&
_managerAPI.showPatchesChangeWarning()) {
_managerAPI.showPatchesChangeWarningDialog(context);
}
});
}
@override
Widget build(BuildContext context) {
return ViewModelBuilder<PatchesSelectorViewModel>.reactive(
onModelReady: (model) => model.initialize(),
onViewModelReady: (model) => model.initialize(),
viewModelBuilder: () => PatchesSelectorViewModel(),
builder: (context, model, child) => Scaffold(
resizeToAvoidBottomInset: false,
floatingActionButton: Visibility(
visible: model.patches.isNotEmpty,
child: FloatingActionButton.extended(
label: I18nText('patchesSelectorView.doneButton'),
label: Row(
children: <Widget>[
I18nText('patchesSelectorView.doneButton'),
Text(' (${model.selectedPatches.length})'),
],
),
icon: const Icon(Icons.check),
onPressed: () {
model.selectPatches();
@ -38,23 +59,60 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
SliverAppBar(
pinned: true,
floating: true,
snap: false,
title: I18nText(
'patchesSelectorView.viewTitle',
child: Text(
'',
style: TextStyle(
color: Theme.of(context).textTheme.headline6!.color,
color: Theme.of(context).textTheme.titleLarge!.color,
),
),
),
leading: IconButton(
icon: Icon(
Icons.arrow_back,
color: Theme.of(context).textTheme.headline6!.color,
color: Theme.of(context).textTheme.titleLarge!.color,
),
onPressed: () => Navigator.of(context).pop(),
),
actions: [
FittedBox(
fit: BoxFit.scaleDown,
child: Container(
margin: const EdgeInsets.only(top: 12, bottom: 12),
padding:
const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.tertiary
.withOpacity(0.5),
borderRadius: BorderRadius.circular(6),
),
child: Text(
model.patchesVersion!,
style: TextStyle(
color: Theme.of(context).textTheme.titleLarge!.color,
),
),
),
),
CustomPopupMenu(
onSelected: (value) =>
{model.onMenuSelection(value, context)},
children: {
0: I18nText(
'patchesSelectorView.loadPatchesSelection',
child: const Text(
'',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
},
),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(64.0),
child: Padding(
@ -63,7 +121,6 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
horizontal: 12.0,
),
child: SearchBar(
showSelectIcon: true,
hintText: FlutterI18n.translate(
context,
'patchesSelectorView.searchBarHint',
@ -73,7 +130,6 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
_query = searchQuery;
});
},
onSelectAll: (value) => model.selectAllPatches(value),
),
),
),
@ -93,106 +149,110 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
),
)
: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0)
.copyWith(bottom: 80),
padding:
const EdgeInsets.symmetric(horizontal: 12.0).copyWith(
bottom: MediaQuery.viewPaddingOf(context).bottom + 8.0,
),
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),
children: [
Row(
children: [
ActionChip(
label: I18nText('patchesSelectorView.default'),
tooltip: FlutterI18n.translate(
context,
'patchesSelectorView.defaultTooltip',
),
onPressed: () {
if (_managerAPI.isPatchesChangeEnabled()) {
model.selectDefaultPatches();
} else {
model.showPatchesChangeDialog(context);
}
},
),
/* 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,
const SizedBox(width: 8),
ActionChip(
label: I18nText('patchesSelectorView.none'),
tooltip: FlutterI18n.translate(
context,
'patchesSelectorView.noneTooltip',
),
onPressed: () {
if (_managerAPI.isPatchesChangeEnabled()) {
model.clearPatches();
} else {
model.showPatchesChangeDialog(context);
}
},
),
],
),
...model.getQueriedPatches(_query).map(
(patch) {
if (patch.compatiblePackages.isNotEmpty) {
return PatchItem(
name: patch.name,
simpleName: patch.getSimpleName(),
description: patch.description,
packageVersion: model.getAppInfo().version,
supportedPackageVersions:
model.getSupportedVersions(patch),
isUnsupported: !isPatchSupported(patch),
isChangeEnabled: _managerAPI.isPatchesChangeEnabled(),
isNew: model.isPatchNew(
patch,
model.getAppInfo().packageName,
),
isSelected: model.isSelected(patch),
onChanged: (value) =>
model.selectPatch(patch, value, context),
);
} else {
return Container();
}
},
),
if (_managerAPI.areUniversalPatchesEnabled())
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
vertical: 10.0,
),
child: I18nText(
'patchesSelectorView.universalPatches',
),
),
...model.getQueriedPatches(_query).map((patch) {
if (patch.compatiblePackages.isEmpty) {
return PatchItem(
name: patch.name,
simpleName: patch.getSimpleName(),
description: patch.description,
packageVersion:
model.getAppInfo().version,
supportedPackageVersions:
model.getSupportedVersions(patch),
isUnsupported: !isPatchSupported(patch),
isChangeEnabled: _managerAPI.isPatchesChangeEnabled(),
isNew: false,
isSelected: model.isSelected(patch),
onChanged: (value) => model.selectPatch(
patch,
value,
context,
),
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,
),
),
const OptionsTextField(
hint: 'App name'),
const OptionsFilePicker(
optionName: 'Choose a logo',
),
],
),
),
),
collapsed: Container(),
) */
)
.toList(),
);
} else {
return Container();
}
}),
],
),
const SizedBox(height: 70.0),
],
),
),
),

View File

@ -1,22 +1,44 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/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:revanced_manager/utils/check_for_supported_patch.dart';
import 'package:stacked/stacked.dart';
class PatchesSelectorViewModel extends BaseViewModel {
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
final ManagerAPI _managerAPI = locator<ManagerAPI>();
final List<Patch> patches = [];
final List<Patch> selectedPatches =
locator<PatcherViewModel>().selectedPatches;
PatchedApplication? selectedApp = locator<PatcherViewModel>().selectedApp;
String? patchesVersion = '';
bool isDefaultPatchesRepo() {
return _managerAPI.getPatchesRepo() == 'revanced/revanced-patches';
}
Future<void> initialize() async {
patches.addAll(await _patcherAPI.getFilteredPatches(
locator<PatcherViewModel>().selectedApp!.packageName,
));
patches.sort((a, b) => a.name.compareTo(b.name));
getPatchesVersion().whenComplete(() => notifyListeners());
patches.addAll(
_patcherAPI.getFilteredPatches(
selectedApp!.originalPackageName,
),
);
patches.sort((a, b) {
if (isPatchNew(a, selectedApp!.packageName) ==
isPatchNew(b, selectedApp!.packageName)) {
return a.name.compareTo(b.name);
} else {
return isPatchNew(b, selectedApp!.packageName) ? 1 : -1;
}
});
notifyListeners();
}
@ -26,46 +48,124 @@ class PatchesSelectorViewModel extends BaseViewModel {
);
}
void selectPatch(Patch patch, bool isSelected) {
if (isSelected && !selectedPatches.contains(patch)) {
selectedPatches.add(patch);
void selectPatch(Patch patch, bool isSelected, BuildContext context) {
if (_managerAPI.isPatchesChangeEnabled()) {
if (isSelected && !selectedPatches.contains(patch)) {
selectedPatches.add(patch);
} else {
selectedPatches.remove(patch);
}
notifyListeners();
} else {
selectedPatches.remove(patch);
showPatchesChangeDialog(context);
}
}
Future<void> showPatchesChangeDialog(BuildContext context) async {
return showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
title: I18nText('warning'),
content: I18nText(
'patchItem.patchesChangeWarningDialogText',
child: const Text(
'',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
actions: [
CustomMaterialButton(
isFilled: false,
label: I18nText('okButton'),
onPressed: () => Navigator.of(context).pop(),
),
CustomMaterialButton(
label: I18nText('patchItem.patchesChangeWarningDialogButton'),
onPressed: () {
Navigator.of(context)
..pop()
..pop();
},
),
],
),
);
}
void selectDefaultPatches() {
selectedPatches.clear();
if (locator<PatcherViewModel>().selectedApp?.originalPackageName != null) {
selectedPatches.addAll(
_patcherAPI
.getFilteredPatches(
locator<PatcherViewModel>().selectedApp!.originalPackageName,
)
.where(
(element) =>
!element.excluded &&
(_managerAPI.areExperimentalPatchesEnabled() ||
isPatchSupported(element)),
),
);
}
notifyListeners();
}
void selectAllPatches(bool isSelected) {
void clearPatches() {
selectedPatches.clear();
if (isSelected) {
selectedPatches.addAll(patches);
}
notifyListeners();
}
void selectPatches() {
locator<PatcherViewModel>().selectedPatches = selectedPatches;
saveSelectedPatches();
locator<PatcherViewModel>().notifyListeners();
}
List<Patch> getQueriedPatches(String query) {
return patches
.where((patch) =>
query.isEmpty ||
query.length < 2 ||
patch.name.toLowerCase().contains(
query.toLowerCase(),
))
.toList();
Future<void> getPatchesVersion() async {
patchesVersion = await _managerAPI.getCurrentPatchesVersion();
}
String getAppVersion() {
return locator<PatcherViewModel>().selectedApp!.version;
List<Patch> getQueriedPatches(String query) {
final List<Patch> patch = patches
.where(
(patch) =>
query.isEmpty ||
query.length < 2 ||
patch.name.toLowerCase().contains(query.toLowerCase()) ||
patch.getSimpleName().toLowerCase().contains(query.toLowerCase()),
)
.toList();
if (_managerAPI.areUniversalPatchesEnabled()) {
return patch;
} else {
return patch
.where((patch) => patch.compatiblePackages.isNotEmpty)
.toList();
}
}
PatchedApplication getAppInfo() {
return locator<PatcherViewModel>().selectedApp!;
}
bool isPatchNew(Patch patch, String packageName) {
final List<Patch> savedPatches = _managerAPI.getSavedPatches(packageName);
if (savedPatches.isEmpty) {
return false;
} else {
return !savedPatches
.any((p) => p.getSimpleName() == patch.getSimpleName());
}
}
List<String> getSupportedVersions(Patch patch) {
PatchedApplication app = locator<PatcherViewModel>().selectedApp!;
Package? package = patch.compatiblePackages.firstWhereOrNull(
final PatchedApplication app = locator<PatcherViewModel>().selectedApp!;
final Package? package = patch.compatiblePackages.firstWhereOrNull(
(pack) => pack.name == app.packageName,
);
if (package != null) {
@ -75,10 +175,42 @@ class PatchesSelectorViewModel extends BaseViewModel {
}
}
bool isPatchSupported(Patch patch) {
PatchedApplication app = locator<PatcherViewModel>().selectedApp!;
return patch.compatiblePackages.any((pack) =>
pack.name == app.packageName &&
(pack.versions.isEmpty || pack.versions.contains(app.version)));
void onMenuSelection(value, BuildContext context) {
switch (value) {
case 0:
loadSelectedPatches(context);
break;
}
}
Future<void> saveSelectedPatches() async {
final List<String> selectedPatches =
this.selectedPatches.map((patch) => patch.name).toList();
await _managerAPI.setSelectedPatches(
locator<PatcherViewModel>().selectedApp!.originalPackageName,
selectedPatches,
);
}
Future<void> loadSelectedPatches(BuildContext context) async {
if (_managerAPI.isPatchesChangeEnabled()) {
final List<String> selectedPatches = await _managerAPI.getSelectedPatches(
locator<PatcherViewModel>().selectedApp!.originalPackageName,
);
if (selectedPatches.isNotEmpty) {
this.selectedPatches.clear();
this.selectedPatches.addAll(
patches.where((patch) => selectedPatches.contains(patch.name)),
);
if (!_managerAPI.areExperimentalPatchesEnabled()) {
this.selectedPatches.removeWhere((patch) => !isPatchSupported(patch));
}
} else {
locator<Toast>().showBottom('patchesSelectorView.noSavedPatches');
}
notifyListeners();
} else {
showPatchesChangeDialog(context);
}
}
}

View File

@ -0,0 +1,121 @@
// ignore_for_file: use_build_context_synchronously
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:stacked/stacked.dart';
class SManageApiUrl extends BaseViewModel {
final ManagerAPI _managerAPI = locator<ManagerAPI>();
final Toast _toast = locator<Toast>();
final TextEditingController _apiUrlController = TextEditingController();
Future<void> showApiUrlDialog(BuildContext context) async {
final 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,
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> 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('');
_toast.showBottom('settingsView.restartAppForChanges');
Navigator.of(context)
..pop()
..pop();
},
),
],
),
);
}
}
final sManageApiUrl = SManageApiUrl();
class SManageApiUrlUI extends StatelessWidget {
const SManageApiUrlUI({super.key});
@override
Widget build(BuildContext context) {
return SettingsTileDialog(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
title: 'settingsView.apiURLLabel',
subtitle: 'settingsView.apiURLHint',
onTap: () => sManageApiUrl.showApiUrlDialog(context),
);
}
}

View File

@ -0,0 +1,86 @@
// ignore_for_file: use_build_context_synchronously
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:stacked/stacked.dart';
class SManageKeystorePassword extends BaseViewModel {
final ManagerAPI _managerAPI = locator<ManagerAPI>();
final TextEditingController _keystorePasswordController =
TextEditingController();
Future<void> showKeystoreDialog(BuildContext context) async {
final String keystorePasswordText = _managerAPI.getKeystorePassword();
_keystorePasswordController.text = keystorePasswordText;
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: <Widget>[
I18nText('settingsView.selectKeystorePassword'),
const Spacer(),
IconButton(
icon: const Icon(Icons.manage_history_outlined),
onPressed: () => _keystorePasswordController.text =
_managerAPI.defaultKeystorePassword,
color: Theme.of(context).colorScheme.secondary,
),
],
),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: SingleChildScrollView(
child: Column(
children: <Widget>[
CustomTextField(
inputController: _keystorePasswordController,
label: I18nText('settingsView.selectKeystorePassword'),
hint: '',
onChanged: (value) => notifyListeners(),
),
],
),
),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('cancelButton'),
onPressed: () {
_keystorePasswordController.clear();
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () {
final String passwd = _keystorePasswordController.text;
_managerAPI.setKeystorePassword(passwd);
Navigator.of(context).pop();
},
),
],
),
);
}
}
final sManageKeystorePassword = SManageKeystorePassword();
class SManageKeystorePasswordUI extends StatelessWidget {
const SManageKeystorePasswordUI({super.key});
@override
Widget build(BuildContext context) {
return SettingsTileDialog(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
title: 'settingsView.selectKeystorePassword',
subtitle: 'settingsView.selectKeystorePasswordHint',
onTap: () => sManageKeystorePassword.showKeystoreDialog(context),
);
}
}

View File

@ -0,0 +1,189 @@
// ignore_for_file: use_build_context_synchronously
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:stacked/stacked.dart';
class SManageSources extends BaseViewModel {
final ManagerAPI _managerAPI = locator<ManagerAPI>();
final Toast _toast = locator<Toast>();
final TextEditingController _hostSourceController = TextEditingController();
final TextEditingController _orgPatSourceController = TextEditingController();
final TextEditingController _patSourceController = TextEditingController();
final TextEditingController _orgIntSourceController = TextEditingController();
final TextEditingController _intSourceController = TextEditingController();
Future<void> showSourcesDialog(BuildContext context) async {
final String hostRepository = _managerAPI.getRepoUrl();
final String patchesRepo = _managerAPI.getPatchesRepo();
final String integrationsRepo = _managerAPI.getIntegrationsRepo();
_hostSourceController.text = hostRepository;
_orgPatSourceController.text = patchesRepo.split('/')[0];
_patSourceController.text = patchesRepo.split('/')[1];
_orgIntSourceController.text = integrationsRepo.split('/')[0];
_intSourceController.text = integrationsRepo.split('/')[1];
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: <Widget>[
I18nText('settingsView.sourcesLabel'),
const Spacer(),
IconButton(
icon: const Icon(Icons.manage_history_outlined),
onPressed: () => showResetConfirmationDialog(context),
color: Theme.of(context).colorScheme.secondary,
),
],
),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: SingleChildScrollView(
child: Column(
children: <Widget>[
CustomTextField(
leadingIcon: const Icon(
Icons.extension_outlined,
color: Colors.transparent,
),
inputController: _hostSourceController,
label: I18nText('settingsView.hostRepositoryLabel'),
hint: hostRepository,
onChanged: (value) => notifyListeners(),
),
const SizedBox(height: 20),
CustomTextField(
leadingIcon: Icon(
Icons.extension_outlined,
color: Theme.of(context).colorScheme.secondary,
),
inputController: _orgPatSourceController,
label: I18nText('settingsView.orgPatchesLabel'),
hint: patchesRepo.split('/')[0],
onChanged: (value) => notifyListeners(),
),
const SizedBox(height: 8),
CustomTextField(
leadingIcon: const Icon(
Icons.extension_outlined,
color: Colors.transparent,
),
inputController: _patSourceController,
label: I18nText('settingsView.sourcesPatchesLabel'),
hint: patchesRepo.split('/')[1],
onChanged: (value) => notifyListeners(),
),
const SizedBox(height: 20),
CustomTextField(
leadingIcon: Icon(
Icons.merge_outlined,
color: Theme.of(context).colorScheme.secondary,
),
inputController: _orgIntSourceController,
label: I18nText('settingsView.orgIntegrationsLabel'),
hint: integrationsRepo.split('/')[0],
onChanged: (value) => notifyListeners(),
),
const SizedBox(height: 8),
CustomTextField(
leadingIcon: const Icon(
Icons.merge_outlined,
color: Colors.transparent,
),
inputController: _intSourceController,
label: I18nText('settingsView.sourcesIntegrationsLabel'),
hint: integrationsRepo.split('/')[1],
onChanged: (value) => notifyListeners(),
),
const SizedBox(height: 20),
I18nText('settingsView.sourcesUpdateNote'),
],
),
),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('cancelButton'),
onPressed: () {
_orgPatSourceController.clear();
_patSourceController.clear();
_orgIntSourceController.clear();
_intSourceController.clear();
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () {
_managerAPI.setRepoUrl(_hostSourceController.text.trim());
_managerAPI.setPatchesRepo(
'${_orgPatSourceController.text.trim()}/${_patSourceController.text.trim()}',
);
_managerAPI.setIntegrationsRepo(
'${_orgIntSourceController.text.trim()}/${_intSourceController.text.trim()}',
);
_managerAPI.setCurrentPatchesVersion('0.0.0');
_managerAPI.setCurrentIntegrationsVersion('0.0.0');
_toast.showBottom('settingsView.restartAppForChanges');
Navigator.of(context).pop();
},
),
],
),
);
}
Future<void> showResetConfirmationDialog(BuildContext context) async {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('settingsView.sourcesResetDialogTitle'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('settingsView.sourcesResetDialogText'),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(),
),
CustomMaterialButton(
label: I18nText('yesButton'),
onPressed: () {
_managerAPI.setRepoUrl('');
_managerAPI.setPatchesRepo('');
_managerAPI.setIntegrationsRepo('');
_managerAPI.setCurrentPatchesVersion('0.0.0');
_managerAPI.setCurrentIntegrationsVersion('0.0.0');
_toast.showBottom('settingsView.restartAppForChanges');
Navigator.of(context)
..pop()
..pop();
},
),
],
),
);
}
}
final sManageSources = SManageSources();
class SManageSourcesUI extends StatelessWidget {
const SManageSourcesUI({super.key});
@override
Widget build(BuildContext context) {
return SettingsTileDialog(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
title: 'settingsView.sourcesLabel',
subtitle: 'settingsView.sourcesLabelHint',
onTap: () => sManageSources.showSourcesDialog(context),
);
}
}

View File

@ -0,0 +1,95 @@
// ignore_for_file: use_build_context_synchronously
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/main.dart';
import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:stacked/stacked.dart';
import 'package:timeago/timeago.dart' as timeago;
final _settingViewModel = SettingsViewModel();
class SUpdateLanguage extends BaseViewModel {
final Toast _toast = locator<Toast>();
late SharedPreferences _prefs;
String selectedLanguage = 'English';
String selectedLanguageLocale = prefs.getString('language') ?? 'en_US';
List languages = [];
Future<void> initialize() async {
_prefs = await SharedPreferences.getInstance();
selectedLanguageLocale =
_prefs.getString('language') ?? selectedLanguageLocale;
notifyListeners();
}
Future<void> updateLanguage(BuildContext context, String? value) async {
if (value != null) {
selectedLanguageLocale = value;
_prefs = await SharedPreferences.getInstance();
await _prefs.setString('language', value);
await FlutterI18n.refresh(context, Locale(value));
timeago.setLocaleMessages(value, timeago.EnMessages());
locator<NavigationViewModel>().notifyListeners();
notifyListeners();
}
}
Future<void> initLang() async {
languages.sort((a, b) => a['name'].compareTo(b['name']));
notifyListeners();
}
Future<void> showLanguagesDialog(BuildContext parentContext) {
initLang();
return showDialog(
context: parentContext,
builder: (context) => SimpleDialog(
title: I18nText('settingsView.languageLabel'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
children: [
SizedBox(
height: 500,
child: ListView.builder(
itemCount: languages.length,
itemBuilder: (context, index) {
return RadioListTile<String>(
title: Text(languages[index]['name']),
subtitle: Text(languages[index]['locale']),
value: languages[index]['locale'],
groupValue: selectedLanguageLocale,
onChanged: (value) {
selectedLanguage = languages[index]['name'];
_toast.showBottom('settingsView.restartAppForChanges');
updateLanguage(context, value);
Navigator.pop(context);
},
);
},
),
),
],
),
);
}
}
class SUpdateLanguageUI extends StatelessWidget {
const SUpdateLanguageUI({super.key});
@override
Widget build(BuildContext context) {
return SettingsTileDialog(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
title: 'settingsView.languageLabel',
subtitle: _settingViewModel.sUpdateLanguage.selectedLanguage,
onTap: () =>
_settingViewModel.sUpdateLanguage.showLanguagesDialog(context),
);
}
}

View File

@ -0,0 +1,116 @@
// 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:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
import 'package:stacked/stacked.dart';
final _settingViewModel = SettingsViewModel();
// ignore: constant_identifier_names
const int ANDROID_12_SDK_VERSION = 31;
class SUpdateTheme extends BaseViewModel {
final ManagerAPI _managerAPI = locator<ManagerAPI>();
bool getDynamicThemeStatus() {
return _managerAPI.getUseDynamicTheme();
}
Future<void> setUseDynamicTheme(BuildContext context, bool value) async {
await _managerAPI.setUseDynamicTheme(value);
final int currentTheme = DynamicTheme.of(context)!.themeId;
if (currentTheme.isEven) {
await DynamicTheme.of(context)!.setTheme(value ? 2 : 0);
} else {
await DynamicTheme.of(context)!.setTheme(value ? 3 : 1);
}
notifyListeners();
}
bool getDarkThemeStatus() {
return _managerAPI.getUseDarkTheme();
}
Future<void> setUseDarkTheme(BuildContext context, bool value) async {
await _managerAPI.setUseDarkTheme(value);
final int currentTheme = DynamicTheme.of(context)!.themeId;
if (currentTheme < 2) {
await DynamicTheme.of(context)!.setTheme(value ? 1 : 0);
} else {
await DynamicTheme.of(context)!.setTheme(value ? 3 : 2);
}
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
systemNavigationBarIconBrightness:
value ? Brightness.light : Brightness.dark,
),
);
notifyListeners();
}
}
class SUpdateThemeUI extends StatelessWidget {
const SUpdateThemeUI({super.key});
@override
Widget build(BuildContext context) {
return SettingsSection(
title: 'settingsView.appearanceSectionTitle',
children: <Widget>[
SwitchListTile(
contentPadding: 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: SUpdateTheme().getDarkThemeStatus(),
onChanged: (value) => SUpdateTheme().setUseDarkTheme(
context,
value,
),
),
FutureBuilder<int>(
future: _settingViewModel.getSdkVersion(),
builder: (context, snapshot) => Visibility(
visible:
snapshot.hasData && snapshot.data! >= ANDROID_12_SDK_VERSION,
child: SwitchListTile(
contentPadding: 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: _settingViewModel.sUpdateTheme.getDynamicThemeStatus(),
onChanged: (value) => {
_settingViewModel.sUpdateTheme.setUseDynamicTheme(
context,
value,
),
},
),
),
),
],
);
}
}

View File

@ -1,12 +1,14 @@
// ignore_for_file: prefer_const_constructors
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/settings/settingsFragment/settings_update_theme.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/about_widget.dart';
import 'package:revanced_manager/ui/widgets/settingsView/custom_switch_tile.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
import 'package:revanced_manager/ui/widgets/settingsView/social_media_widget.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_advanced_section.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_export_section.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_info_section.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_team_section.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
import 'package:stacked/stacked.dart';
@ -30,7 +32,7 @@ class SettingsView extends StatelessWidget {
child: Text(
'',
style: GoogleFonts.inter(
color: Theme.of(context).textTheme.headline6!.color,
color: Theme.of(context).textTheme.titleLarge!.color,
),
),
),
@ -38,131 +40,16 @@ class SettingsView extends StatelessWidget {
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(
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,
),
),
),
),
],
),
SettingsTileDialog(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
title: 'settingsView.languageLabel',
subtitle: 'English',
onTap: () => model.showLanguagesDialog(context),
),
SUpdateThemeUI(),
// SUpdateLanguageUI(),
// _settingsDivider,
STeamSection(),
_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(
padding: EdgeInsets.symmetric(horizontal: 20.0),
),
],
),
SAdvancedSection(),
_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),
),
],
),
SExportSection(),
_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(
padding: EdgeInsets.symmetric(horizontal: 20.0),
),
],
),
SInfoSection(),
],
),
),

View File

@ -1,335 +1,283 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:cr_file_saver/file_saver.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:dynamic_themes/dynamic_themes.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.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/settingsView/custom_text_field.dart';
import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_update_language.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_update_theme.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:share_extend/share_extend.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:timeago/timeago.dart';
// ignore: constant_identifier_names
const int ANDROID_12_SDK_VERSION = 31;
class SettingsViewModel extends BaseViewModel {
final NavigationService _navigationService = locator<NavigationService>();
final ManagerAPI _managerAPI = locator<ManagerAPI>();
final TextEditingController _orgPatSourceController = TextEditingController();
final TextEditingController _patSourceController = TextEditingController();
final TextEditingController _orgIntSourceController = TextEditingController();
final TextEditingController _intSourceController = TextEditingController();
final TextEditingController _apiUrlController = TextEditingController();
final PatchesSelectorViewModel _patchesSelectorViewModel =
PatchesSelectorViewModel();
final PatcherViewModel _patcherViewModel = locator<PatcherViewModel>();
final Toast _toast = locator<Toast>();
void setLanguage(String language) {
notifyListeners();
}
final SUpdateLanguage sUpdateLanguage = SUpdateLanguage();
final SUpdateTheme sUpdateTheme = SUpdateTheme();
void navigateToContributors() {
_navigationService.navigateTo(Routes.contributorsView);
}
Future<void> updateLanguage(BuildContext context, String? value) async {
if (value != null) {
await FlutterI18n.refresh(context, Locale(value));
setLocaleMessages(value, EnMessages());
}
bool isPatchesAutoUpdate() {
return _managerAPI.isPatchesAutoUpdate();
}
bool getDynamicThemeStatus() {
return _managerAPI.getUseDynamicTheme();
}
void setUseDynamicTheme(BuildContext context, bool value) async {
await _managerAPI.setUseDynamicTheme(value);
int currentTheme = DynamicTheme.of(context)!.themeId;
if (currentTheme.isEven) {
await DynamicTheme.of(context)!.setTheme(value ? 2 : 0);
} else {
await DynamicTheme.of(context)!.setTheme(value ? 3 : 1);
}
void setPatchesAutoUpdate(bool value) {
_managerAPI.setPatchesAutoUpdate(value);
notifyListeners();
}
bool getDarkThemeStatus() {
return _managerAPI.getUseDarkTheme();
bool isPatchesChangeEnabled() {
return _managerAPI.isPatchesChangeEnabled();
}
void setUseDarkTheme(BuildContext context, bool value) async {
await _managerAPI.setUseDarkTheme(value);
int currentTheme = DynamicTheme.of(context)!.themeId;
if (currentTheme < 2) {
await DynamicTheme.of(context)!.setTheme(value ? 1 : 0);
Future<void> showPatchesChangeEnableDialog(
bool value,
BuildContext context,
) async {
if (value) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
title: I18nText('warning'),
content: I18nText(
'settingsView.enablePatchesSelectionWarningText',
child: const Text(
'',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
actions: [
CustomMaterialButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () {
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('yesButton'),
onPressed: () {
_managerAPI.setChangingToggleModified(true);
_managerAPI.setPatchesChangeEnabled(true);
Navigator.of(context).pop();
},
),
],
),
);
} else {
await DynamicTheme.of(context)!.setTheme(value ? 3 : 2);
return showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
title: I18nText('warning'),
content: I18nText(
'settingsView.disablePatchesSelectionWarningText',
child: const Text(
'',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
actions: [
CustomMaterialButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () {
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('yesButton'),
onPressed: () {
_managerAPI.setChangingToggleModified(true);
_patchesSelectorViewModel.selectDefaultPatches();
_managerAPI.setPatchesChangeEnabled(false);
Navigator.of(context).pop();
},
),
],
),
);
}
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
systemNavigationBarIconBrightness:
value ? Brightness.light : Brightness.dark,
),
);
}
bool areUniversalPatchesEnabled() {
return _managerAPI.areUniversalPatchesEnabled();
}
void showUniversalPatches(bool value) {
_managerAPI.enableUniversalPatchesStatus(value);
notifyListeners();
}
Future<void> showLanguagesDialog(BuildContext context) {
return showDialog(
context: context,
builder: (context) => SimpleDialog(
title: I18nText('settingsView.languageLabel'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
children: <Widget>[
RadioListTile<String>(
title: I18nText('settingsView.englishOption'),
value: 'en',
groupValue: 'en',
onChanged: (value) {
updateLanguage(context, value);
Navigator.of(context).pop();
},
),
],
),
);
bool areExperimentalPatchesEnabled() {
return _managerAPI.areExperimentalPatchesEnabled();
}
Future<void> showSourcesDialog(BuildContext context) async {
String patchesRepo = _managerAPI.getPatchesRepo();
String integrationsRepo = _managerAPI.getIntegrationsRepo();
_orgPatSourceController.text = patchesRepo.split('/')[0];
_patSourceController.text = patchesRepo.split('/')[1];
_orgIntSourceController.text = integrationsRepo.split('/')[0];
_intSourceController.text = integrationsRepo.split('/')[1];
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: <Widget>[
I18nText('settingsView.sourcesLabel'),
const Spacer(),
IconButton(
icon: const Icon(Icons.manage_history_outlined),
onPressed: () => showResetConfirmationDialog(context),
color: Theme.of(context).colorScheme.secondary,
)
],
),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: SingleChildScrollView(
child: Column(
children: <Widget>[
CustomTextField(
leadingIcon: Icon(
Icons.extension_outlined,
color: Theme.of(context).colorScheme.secondary,
),
inputController: _orgPatSourceController,
label: I18nText('settingsView.orgPatchesLabel'),
hint: patchesRepo.split('/')[0],
onChanged: (value) => notifyListeners(),
),
const SizedBox(height: 8),
CustomTextField(
leadingIcon: const Icon(
Icons.extension_outlined,
color: Colors.transparent,
),
inputController: _patSourceController,
label: I18nText('settingsView.sourcesPatchesLabel'),
hint: patchesRepo.split('/')[1],
onChanged: (value) => notifyListeners(),
),
const SizedBox(height: 20),
CustomTextField(
leadingIcon: Icon(
Icons.merge_outlined,
color: Theme.of(context).colorScheme.secondary,
),
inputController: _orgIntSourceController,
label: I18nText('settingsView.orgIntegrationsLabel'),
hint: integrationsRepo.split('/')[0],
onChanged: (value) => notifyListeners(),
),
const SizedBox(height: 8),
CustomTextField(
leadingIcon: const Icon(
Icons.merge_outlined,
color: Colors.transparent,
),
inputController: _intSourceController,
label: I18nText('settingsView.sourcesIntegrationsLabel'),
hint: integrationsRepo.split('/')[1],
onChanged: (value) => notifyListeners(),
),
],
),
),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('cancelButton'),
onPressed: () {
_orgPatSourceController.clear();
_patSourceController.clear();
_orgIntSourceController.clear();
_intSourceController.clear();
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () {
_managerAPI.setPatchesRepo(
'${_orgPatSourceController.text}/${_patSourceController.text}',
);
_managerAPI.setIntegrationsRepo(
'${_orgIntSourceController.text}/${_intSourceController.text}',
);
Navigator.of(context).pop();
},
)
],
),
);
void useExperimentalPatches(bool value) {
_managerAPI.enableExperimentalPatchesStatus(value);
notifyListeners();
}
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();
},
)
],
),
);
void deleteKeystore() {
_managerAPI.deleteKeystore();
_toast.showBottom('settingsView.regeneratedKeystore');
notifyListeners();
}
Future<void> showResetConfirmationDialog(BuildContext context) async {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('settingsView.sourcesResetDialogTitle'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('settingsView.sourcesResetDialogText'),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(),
),
CustomMaterialButton(
label: I18nText('yesButton'),
onPressed: () {
_managerAPI.setPatchesRepo('');
_managerAPI.setIntegrationsRepo('');
Navigator.of(context).pop();
Navigator.of(context).pop();
},
)
],
),
);
void deleteTempDir() {
_managerAPI.deleteTempFolder();
_toast.showBottom('settingsView.deletedTempDir');
notifyListeners();
}
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(),
Future<void> exportPatches() async {
try {
final File outFile = File(_managerAPI.storedPatchesFile);
if (outFile.existsSync()) {
final String dateTime =
DateTime.now().toString().replaceAll(' ', '_').split('.').first;
await CRFileSaver.saveFileWithDialog(
SaveFileDialogParams(
sourceFilePath: outFile.path,
destinationFileName: 'selected_patches_$dateTime.json',
),
CustomMaterialButton(
label: I18nText('yesButton'),
onPressed: () {
_managerAPI.setApiUrl('');
Navigator.of(context).pop();
Navigator.of(context).pop();
},
)
],
),
);
);
_toast.showBottom('settingsView.exportedPatches');
} else {
_toast.showBottom('settingsView.noExportFileFound');
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
Future<void> importPatches(BuildContext context) async {
if (isPatchesChangeEnabled()) {
try {
final FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['json'],
);
if (result != null && result.files.single.path != null) {
final File inFile = File(result.files.single.path!);
inFile.copySync(_managerAPI.storedPatchesFile);
inFile.delete();
if (_patcherViewModel.selectedApp != null) {
_patcherViewModel.loadLastSelectedPatches();
}
_toast.showBottom('settingsView.importedPatches');
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
_toast.showBottom('settingsView.jsonSelectorErrorMessage');
}
} else {
_managerAPI.showPatchesChangeWarningDialog(context);
}
}
Future<void> exportKeystore() async {
try {
final File outFile = File(_managerAPI.keystoreFile);
if (outFile.existsSync()) {
final String dateTime =
DateTime.now().toString().replaceAll(' ', '_').split('.').first;
await CRFileSaver.saveFileWithDialog(
SaveFileDialogParams(
sourceFilePath: outFile.path,
destinationFileName: 'keystore_$dateTime.keystore',
),
);
_toast.showBottom('settingsView.exportedKeystore');
} else {
_toast.showBottom('settingsView.noKeystoreExportFileFound');
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
Future<void> importKeystore() async {
try {
final FilePickerResult? result = await FilePicker.platform.pickFiles();
if (result != null && result.files.single.path != null) {
final File inFile = File(result.files.single.path!);
inFile.copySync(_managerAPI.keystoreFile);
_toast.showBottom('settingsView.importedKeystore');
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
_toast.showBottom('settingsView.keystoreSelectorErrorMessage');
}
}
void resetSelectedPatches() {
_managerAPI.resetLastSelectedPatches();
_toast.showBottom('settingsView.resetStoredPatches');
}
Future<int> getSdkVersion() async {
AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo;
return info.version.sdkInt ?? -1;
final AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo;
return info.version.sdkInt;
}
Future<void> deleteLogs() async {
final Directory appCacheDir = await getTemporaryDirectory();
final Directory logsDir = Directory('${appCacheDir.path}/logs');
if (logsDir.existsSync()) {
logsDir.deleteSync(recursive: true);
}
_toast.showBottom('settingsView.deletedLogs');
}
Future<void> exportLogcatLogs() async {
Directory appCache = await getTemporaryDirectory();
Directory logDir = Directory('${appCache.path}/logs');
final Directory appCache = await getTemporaryDirectory();
final Directory logDir = Directory('${appCache.path}/logs');
logDir.createSync();
String dateTime = DateTime.now()
final String dateTime = DateTime.now()
.toIso8601String()
.replaceAll('-', '')
.replaceAll(':', '')
.replaceAll('T', '')
.replaceAll('.', '');
File logcat = File('${logDir.path}/revanced-manager_logcat_$dateTime.log');
String logs = await Logcat.execute();
final File logcat =
File('${logDir.path}/revanced-manager_logcat_$dateTime.log');
final String logs = await Logcat.execute();
logcat.writeAsStringSync(logs);
ShareExtend.share(logcat.path, 'file');
}

View File

@ -8,12 +8,11 @@ import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
import 'package:stacked/stacked.dart';
class AppInfoView extends StatelessWidget {
final PatchedApplication app;
const AppInfoView({
Key? key,
required this.app,
}) : super(key: key);
final PatchedApplication app;
@override
Widget build(BuildContext context) {
@ -28,7 +27,7 @@ class AppInfoView extends StatelessWidget {
child: Text(
'',
style: GoogleFonts.inter(
color: Theme.of(context).textTheme.headline6!.color,
color: Theme.of(context).textTheme.titleLarge!.color,
),
),
),
@ -52,13 +51,13 @@ class AppInfoView extends StatelessWidget {
Text(
app.name,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headline6,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 4),
Text(
app.version,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.subtitle1,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 20),
Padding(
@ -154,43 +153,6 @@ class AppInfoView extends StatelessWidget {
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,
@ -260,6 +222,22 @@ class AppInfoView extends StatelessWidget {
subtitle: Text(app.packageName),
),
const SizedBox(height: 4),
ListTile(
contentPadding:
const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'appInfoView.originalPackageNameLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: Text(app.originalPackageName),
),
const SizedBox(height: 4),
ListTile(
contentPadding:
const EdgeInsets.symmetric(horizontal: 20.0),

Some files were not shown because too many files have changed in this diff Show More