Compare commits

...

256 Commits

Author SHA1 Message Date
semantic-release-bot
7104d6d6dd chore(release): 1.19.0-dev.2 [skip ci]
# [1.19.0-dev.2](https://github.com/ReVanced/revanced-manager/compare/v1.19.0-dev.1...v1.19.0-dev.2) (2024-01-22)

### Bug Fixes

* Use correct version code & name ([#1647](https://github.com/ReVanced/revanced-manager/issues/1647)) ([d933997](d933997c89))
2024-01-22 22:34:38 +00:00
Benjamin
d933997c89 fix: Use correct version code & name (#1647) 2024-01-23 05:27:33 +07:00
semantic-release-bot
bea99bb4c6 chore(release): 1.19.0-dev.1 [skip ci]
# [1.19.0-dev.1](https://github.com/ReVanced/revanced-manager/compare/v1.18.0...v1.19.0-dev.1) (2024-01-22)

### Bug Fixes

* added a trailing comma ([975180b](975180b075))
* Allow mounting without Magisk ([3f96608](3f96608398))
* fix redundant buttons on dialog ([079c0de](079c0defaf))
* Incorrect strings and logics ([#1619](https://github.com/ReVanced/revanced-manager/issues/1619)) ([4f22e88](4f22e88e42))
* **Keystore Password:** textfield title display ([8e52abd](8e52abda9a))
* Mount script causes build to fail ([#1613](https://github.com/ReVanced/revanced-manager/issues/1613)) ([f3c78c2](f3c78c2c24))
* **Patch Option:** Set text colour on dropdown menu ([acb1e24](acb1e2434b))
* **Release CI:** truncate the "v" from version ([8595099](85950991ab))
* use lowercase repo names ([#1626](https://github.com/ReVanced/revanced-manager/issues/1626)) ([edd8602](edd86024b9))

### Features

* Add API migration code ([#1615](https://github.com/ReVanced/revanced-manager/issues/1615)) ([28ae276](28ae2766f0))
* add haptic feedback ([#1459](https://github.com/ReVanced/revanced-manager/issues/1459)) ([7911459](7911459817))
* Improve app selector and patcher UI ([#1616](https://github.com/ReVanced/revanced-manager/issues/1616)) ([efb2d5e](efb2d5ef32))
* Improve Split APK warning readability ([#1625](https://github.com/ReVanced/revanced-manager/issues/1625)) ([6fd740f](6fd740f8c0))
* Simplify settings strings ([#1618](https://github.com/ReVanced/revanced-manager/issues/1618)) ([0d45fe4](0d45fe4a97))
* use native TextField ([9ed43ef](9ed43efe5d))

### Performance Improvements

* Load patched apps as soon as possible ([c94eb7a](c94eb7a48e))
2024-01-22 11:41:51 +00:00
Pun Butrach
f38a593434 ci(Release Build): Use correct syntax in getting the version 2024-01-22 18:34:35 +07:00
Pun Butrach
85950991ab fix(Release CI): truncate the "v" from version
ci: cleanup

force push soon™️
2024-01-22 18:30:16 +07:00
Osama Tahir
6fd740f8c0 feat: Improve Split APK warning readability (#1625)
Co-authored-by: Pun Butrach <pun.butrach@gmail.com>
2024-01-22 12:20:09 +01:00
Benjamin
ec2c2d8ccb ci: Build pre-release versions (#1606) 2024-01-22 09:29:57 +01:00
oSumAtrIX
efb2d5ef32 feat: Improve app selector and patcher UI (#1616) 2024-01-22 09:20:17 +01:00
validcube
acb1e2434b fix(Patch Option): Set text colour on dropdown menu
Signed-off-by: validcube <pun.butrach@gmail.com>
2024-01-21 20:36:23 +07:00
validcube
8e52abda9a fix(Keystore Password): textfield title display
Signed-off-by: validcube <pun.butrach@gmail.com>
2024-01-21 16:58:00 +07:00
validcube
9764326242 chore(patch_item): Remove import directive
Signed-off-by: validcube <pun.butrach@gmail.com>
2024-01-21 15:15:37 +07:00
validcube
97c33d6c54 chore: Remove deprecated lint rules
Signed-off-by: validcube <pun.butrach@gmail.com>
2024-01-21 15:10:03 +07:00
Pun Butrach
30d5f3ad3f docs(security): Prepare for v1.19.0 2024-01-21 15:02:52 +07:00
validcube
079c0defaf fix: fix redundant buttons on dialog
Signed-off-by: validcube <pun.butrach@gmail.com>
2024-01-21 14:45:25 +07:00
Benjamin
cf1afddb9e chore: remove unused dependencies (#1603) 2024-01-21 14:07:30 +07:00
Pun Butrach
b07b9351c4 docs(security): init (#1599)
Co-authored-by: Ax333l <main@axelen.xyz>
2024-01-21 13:59:57 +07:00
Benjamin
7911459817 feat: add haptic feedback (#1459)
Co-authored-by: Ushie <ushiekane@gmail.com>
Co-authored-by: Pun Butrach <pun.butrach@gmail.com>
2024-01-14 14:29:24 -08:00
Benjamin
ef9b1d5c2d refactor: Replace deprecated WillPopScope (#1604) 2024-01-13 19:34:04 -08:00
Benjamin Halko
fd2780624a chore: remove unused import 2024-01-12 09:49:26 -08:00
aAbed
4f22e88e42 fix: Incorrect strings and logics (#1619) 2024-01-12 05:15:37 +07:00
oSumAtrIX
0d45fe4a97 feat: Simplify settings strings (#1618) 2024-01-09 20:23:54 +01:00
oSumAtrIX
28ae2766f0 feat: Add API migration code (#1615) 2024-01-09 20:23:26 +01:00
Benjamin
edd86024b9 fix: use lowercase repo names (#1626) 2024-01-09 05:07:18 +07:00
Benjamin
f3c78c2c24 fix: Mount script causes build to fail (#1613) 2024-01-01 00:32:57 +03:00
Ushie
3f96608398 fix: Allow mounting without Magisk 2023-12-31 03:32:04 +03:00
validcube
9ed43efe5d feat: use native TextField
Signed-off-by: validcube <pun.butrach@gmail.com>
2023-12-30 14:37:46 +07:00
Benjamin Halko
975180b075 fix: added a trailing comma 2023-12-28 13:17:25 -08:00
oSumAtrIX
c94eb7a48e perf: Load patched apps as soon as possible 2023-12-24 00:12:33 +01:00
oSumAtrIX
85c8006977 build: Bump version to v1.18.0 2023-12-23 22:20:32 +01:00
oSumAtrIX
850bdc4a4d fix: Reset progress if patch was cancelled 2023-12-23 22:19:56 +01:00
oSumAtrIX
ca95e32164 chore: Ignore autogenerated project file 2023-12-23 22:14:44 +01:00
oSumAtrIX
a141ec85b0 fix: Correct update message 2023-12-23 22:11:38 +01:00
oSumAtrIX
1298a96b0e fix: Update the progress properly until installation finishes successfully 2023-12-23 22:10:22 +01:00
oSumAtrIX
1928b150ad fix: Use correct installation type labels 2023-12-23 22:10:22 +01:00
oSumAtrIX
7426f5484d fix: Update home screen after installation 2023-12-23 21:58:53 +01:00
oSumAtrIX
11a8f313b0 feat: Clarify non root installation button 2023-12-23 21:46:58 +01:00
oSumAtrIX
69b6ef07a1 feat: Improve dialog text clarity 2023-12-23 21:36:47 +01:00
oSumAtrIX
7a1ba9dabf fix: Add missing confirmation dialog 2023-12-23 21:11:24 +01:00
oSumAtrIX
48a739c94e refactor: Simplify uninstallApp code 2023-12-23 20:53:51 +01:00
oSumAtrIX
9680f0cf12 refactor: Remove unnecessary quotation marks 2023-12-23 20:53:31 +01:00
oSumAtrIX
5b1c89a0c5 perf: Reduce amount of shell commands 2023-12-23 20:52:52 +01:00
oSumAtrIX
8a1ab478a3 fix: Prevent crash by escaping string correctly 2023-12-23 20:52:52 +01:00
Benjamin
2ae8d49526 fix: exported logs patch selection (#1535)
Co-authored-by: validcube <pun.butrach@gmail.com>
2023-12-23 11:29:36 +07:00
validcube
a0b673c138 refactor: apply suggestions from analyser
Signed-off-by: validcube <pun.butrach@gmail.com>
2023-12-23 10:47:12 +07:00
validcube
c56c445fb7 fix: migration latest changes to native buttons
Signed-off-by: validcube <pun.butrach@gmail.com>
2023-12-23 10:36:59 +07:00
aAbed
c23275f2fe feat: Improve installation robustness (#1528)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
Co-authored-by: Ushie <ushiekane@gmail.com>
Co-authored-by: Dhruvan Bhalara <53393418+dhruvanbhalara@users.noreply.github.com>
Co-authored-by: Pun Butrach <pun.butrach@gmail.com>
2023-12-23 10:16:28 +07:00
Pun Butrach
8b28a33b73 ci(security): resolve arbitrary code execution 2023-12-22 20:39:21 +07:00
Pun Butrach
f8d086a743 feat: dialogs correctly follows Material 3 specifications (#1560)
Signed-off-by: validcube <pun.butrach@gmail.com>
2023-12-22 20:34:03 +07:00
Radon Rosborough
c06d15de5f docs: Improve troubleshooting solutions (#1543)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-12-22 05:50:10 +01:00
validcube
dcaf1f54e4 build(gradle): bump Gradle to v8.5
Signed-off-by: validcube <pun.butrach@gmail.com>
2023-12-17 20:16:46 +07:00
festry0
aa91abb022 docs(readme): Correct grammar mistake (#1569) 2023-12-16 10:40:50 +07:00
Ushie
67d204eb02 build: Bump version to v1.17.3 2023-12-12 08:22:10 +03:00
aAbed
18e680b298 fix: Unable to unselect new patches 2023-12-12 10:37:35 +05:45
Benjamin Halko
6ef1b072e8 build: Bump version to v1.17.2 2023-12-11 18:27:19 -08:00
Benjamin
ca9ef544ce chore: merge dev to main (#1545) 2023-12-11 18:23:08 -08:00
oSumAtrIX
7831a3438d fix: Include new patches that are used by default 2023-12-12 02:32:57 +01:00
oSumAtrIX
1fad90441c perf: Use hashset for fast comparison 2023-12-12 02:32:45 +01:00
validcube
7f26c5bd45 revert: "feat: improve predictive back (#1487)"
This reverts commit 06ff36c836.

Signed-off-by: validcube <pun.butrach@gmail.com>
2023-12-11 19:02:13 +07:00
Pun Butrach
637641cf54 feat: output suggested version into patch log (#1557)
Signed-off-by: validcube <pun.butrach@gmail.com>
2023-12-11 11:26:52 +07:00
Pun Butrach
d9a316abbb docs(contributing): update branding 2023-12-09 11:49:19 +07:00
Pun Butrach
093cfa5269 docs(contributing): use correct semantic 2023-12-09 11:48:20 +07:00
Pun Butrach
61a025de4d docs: correct styling issue 2023-12-09 11:47:26 +07:00
Dhruvan Bhalara
7df1ae7ed8 feat: append patch version to the APK name while sharing/exporting (#1547) 2023-12-05 12:58:32 +05:45
Pun Butrach
eb6e75b156 ci: use migrate setup-java to Node 20 (#1542) 2023-12-05 09:43:04 +07:00
validcube
8480b3ac3d chore: update repository's owner name 2023-12-02 17:39:02 +07:00
validcube
6339a31fec build(dart-dependency): bump FlutterToast to v8.2.4 2023-12-02 17:35:35 +07:00
validcube
71b5bb3f8f build(dart-dependency): prefer immutable for dependency using git 2023-12-02 17:33:42 +07:00
Ushie
a9878dbbdf chore: Merge dev to main (#1544) 2023-12-02 03:46:17 +03:00
Ushie
b1fb9dd7d3 build: Bump version to v1.17.1 2023-12-02 03:44:51 +03:00
Dhruvan Bhalara
de51fbd7be fix: Incorrect duplicate filename handling when exporting files (#1541) 2023-12-01 20:31:46 +05:45
Ax333l
c9412a97d0 build(dependency): Bump patcher to 19.1.0 2023-12-01 13:22:38 +01:00
Ushie
63c29bdd75 chore: Merge dev to main (#1529) 2023-11-27 11:06:43 +03:00
Ushie
bace26063d build: Bump version to v1.17.0 2023-11-27 11:05:56 +03:00
Benjamin
7931eb97b9 feat: updated logs (#1526) 2023-11-25 17:06:18 -08:00
aAbed
9df89c7b74 fix: Disable wakelock when patching is canceled (#1514) 2023-11-22 16:21:27 +03:00
aAbed
78978276c4 fix(App Selector): Unable to select APK from storage when asked to (#1513) 2023-11-22 16:20:51 +03:00
Ushie
011eddbbc5 chore: Merge dev to main (#1478) 2023-11-21 17:37:40 +03:00
Ushie
4e9f3fe1dc build: Bump version to v1.16.0 2023-11-21 17:36:24 +03:00
Aditya Arora
9bd48c19ff feat: ability to search query for suggested version (#1151)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
Co-authored-by: Ushie <ushiekane@gmail.com>
2023-11-21 17:33:21 +03:00
Pun Butrach
c0516c3665 ci(build): Improve pull request build (#1305)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-11-20 14:50:44 +07:00
Pun Butrach
06ff36c836 feat: improve predictive back (#1487)
Signed-off-by: validcube <pun.butrach@gmail.com>
2023-11-20 14:49:39 +07:00
Pun Butrach
98c16eb1dc docs(readme): remove redundant feature information 2023-11-20 07:34:08 +07:00
KAZI MMT
35d80840e5 fix: Remove incorrect punctuation (#1499) 2023-11-19 05:38:34 +01:00
validcube
ad3a778fb9 refactor: apply lint from analyzer 2023-11-12 10:02:40 +05:45
Pun Butrach
d25d1efe9c fix(build): allow profile variant to compile
fix backported from #1305
2023-11-12 10:02:10 +05:45
Pun Butrach
de58dff423 docs: improve readme (#1484) 2023-11-12 10:02:07 +05:45
Pun Butrach
ec26e4d8d1 feat: update color scheme (#1483) 2023-11-12 09:59:55 +05:45
validcube
c24da41505 refactor: replace deprecated command 2023-11-12 09:59:50 +05:45
Pun Butrach
f785185e1c chore: update label for template 2023-11-11 13:19:01 +07:00
aAbed
5ed3ed9a2d fix: patcher logs hiding behind navigation bar (#1476) 2023-11-11 12:00:21 +07:00
oSumAtrIX
a3adace60e build: Update Gradle to v8.4 2023-11-08 17:22:06 +01:00
oSumAtrIX
3db4de09e2 chore: Remove unnecessary project files 2023-11-08 17:21:02 +01:00
aAbed
70b2ee0a84 feat: Disable selection of un-suggested app version by default (#1471)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-11-08 03:38:54 +01:00
aAbed
e7d82850c9 fix: Log saved patch option values (#1420) 2023-11-07 23:37:59 +01:00
oSumAtrIX
e6a8f4e6dc build: Bump version to v1.15.1 2023-11-04 22:41:48 +01:00
oSumAtrIX
494e268bc5 fix: Use correct method name for string replacement 2023-11-04 22:41:27 +01:00
oSumAtrIX
e1c6f65b7e chore: Merge branch dev to main (#1446) 2023-11-04 22:10:14 +01:00
oSumAtrIX
89075c5588 build: Bump version to v1.15.0 2023-11-04 22:08:20 +01:00
Benjamin
c7fa9b8ce7 chore: upgrade dependencies (#1404) 2023-11-04 22:08:20 +01:00
oSumAtrIX
cb70082d31 docs: Remove unneeded steps to build from source 2023-11-04 22:08:19 +01:00
Mr. X
48b9ac8f5b docs: use correct directory path (#1440) 2023-11-04 22:08:19 +01:00
aAbed
0be568bbbd fix: Hide empty patches category labels (#1439) 2023-11-04 22:08:19 +01:00
aAbed
ba44fa620f build: Bump dependencies to support patch option values (#1431)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-11-04 22:08:19 +01:00
Ushie
dde402afbf chore: merge dev to main (#1437) 2023-10-26 17:22:07 +03:00
Ushie
bbe5142ca9 build: bump version to v1.14.2 2023-10-26 17:21:25 +03:00
aAbed
e74ffac5b0 fix: unable to use custom API (#1435) 2023-10-26 17:20:34 +03:00
Ushie
cfb8980e3a chore: merge dev to main (#1425) 2023-10-25 11:42:54 +03:00
Ushie
e06e1bdcbe build: bump version to v1.14.1 2023-10-25 11:42:19 +03:00
Ushie
d60ced2f61 fix: remove codeblock from log export
Fixes: #1416
2023-10-25 11:40:30 +03:00
aAbed
e68689828e fix: Material You toggle not updating sometimes (#1421) 2023-10-25 11:37:03 +03:00
validcube
ba932758c8 build: bump version to v1.14.0 2023-10-21 23:00:51 +07:00
Pun Butrach
ee43fa6311 chore: merge dev to main (#1399) 2023-10-21 22:58:58 +07:00
Pun Butrach
ad6b164d51 feat: root status in export patch log (#1407)
Co-authored-by: Ushie <ushiekane@gmail.com>
2023-10-21 17:46:51 +07:00
validcube
4a5510acb2 refactor: remove the remaining of semantic release
Removal reason: not maintained
2023-10-21 15:37:23 +07:00
validcube
970dbc4428 refactor(social): use correct github organisation name
the correct name is "ReVanced" not "revanced", we changed for a while now.
2023-10-21 15:33:36 +07:00
validcube
f8f37325eb refactor(social): change Twitter to X 2023-10-21 15:32:13 +07:00
Benjamin
bb999019ef feat: show patch options in error log (#1394) 2023-10-17 13:21:59 +07:00
KobeW50
533b6a155a feat: clarify "Version compatibility check" description (#1397) 2023-10-17 13:20:46 +07:00
KobeW50
4cdc92388c refactor: fix patch log order to be consistent with settings order (#1398) 2023-10-17 13:19:18 +07:00
Pun Butrach
ccc6be1e71 refactor(accessibility): improve patch options (#1369)
Co-authored-by: Palm <palmpasuthorn@gmail.com>
2023-10-16 16:47:59 +07:00
Ushie
b355778a92 chore: merge dev to main (#1388) 2023-10-15 20:19:54 +03:00
Ushie
6a12e8f37a build: bump version to v1.13.1 2023-10-15 20:19:23 +03:00
Ushie
59adb91f5f fix(settings): inverted version compatibility switch 2023-10-15 20:16:58 +03:00
KobeW50
53677e2f39 fix: typo in reset patch selection dialog (#1387) 2023-10-15 20:14:19 +03:00
validcube
1c74f43b22 build: bump version to 1.13.0 2023-10-15 18:22:42 +07:00
Pun Butrach
b4801970e8 chore: merge dev to main (#1384)
ReVanced Manager 1.13.0!!
2023-10-15 18:18:09 +07:00
Pun Butrach
7a3a6b512f docs(building): use new highlight format 2023-10-15 18:04:19 +07:00
Pun Butrach
72ea33b6de fix(patch_selector): correct popup menu style
fix: #1372
2023-10-15 17:46:44 +07:00
Pun Butrach
d97192e0ee refactor(patch_options): disable card tap (#1368) 2023-10-15 16:56:43 +07:00
KobeW50
196d9fe4d2 refactor: reorganize and rename settings (#1307)
Co-authored-by: Pun Butrach <pun.butrach@gmail.com>
Co-authored-by: Ushie <ushiekane@gmail.com>
2023-10-15 16:56:02 +07:00
Benjamin
e960fcc303 feat: add user agent (#1380)
Co-authored-by: Pun Butrach <pun.butrach@gmail.com>
2023-10-15 16:51:31 +07:00
Benjamin
f334da95ff feat: Remove full external storage access (#1381) 2023-10-15 16:44:11 +07:00
aAbed
5d5f311e36 fix(settings-view): list items jumping on scroll up (#1375) 2023-10-15 16:17:28 +07:00
aAbed
d577e97758 fix(app-bar): title not hiding completely (#1376) 2023-10-15 16:16:55 +07:00
aAbed
2dc92e26d3 fix(patches-selector): ignore punctuation marks when searching for patch (#1377) 2023-10-14 19:43:18 +07:00
oSumAtrIX
f4994a36a3 chore: Merge branch dev to main (#1366) 2023-10-13 00:22:02 +02:00
oSumAtrIX
7a785a8163 build: Bump version to v1.12.1 2023-10-13 00:14:14 +02:00
oSumAtrIX
6ad0d860c7 fix: Load patches from older versions of ReVanced Manager correctly 2023-10-13 00:12:27 +02:00
oSumAtrIX
38a2fa55df chore: Merge branch dev to main (#1350) 2023-10-12 21:58:29 +02:00
oSumAtrIX
a21b170b52 build: Bump version to v1.12.0 2023-10-12 21:55:06 +02:00
oSumAtrIX
44265b2362 chore: Setup Crowdin configuration file 2023-10-12 18:51:16 +02:00
oSumAtrIX
069193342b docs: use correct alert syntax 2023-10-12 14:25:13 +02:00
oSumAtrIX
54e9a56cda docs: Add options setting 2023-10-12 14:22:20 +02:00
oSumAtrIX
39bc9227dc docs: Improve readability 2023-10-12 14:20:44 +02:00
aAbed
ac636670c3 feat: Add patch options (#1354) 2023-10-12 02:00:39 +02:00
MD Danish Ansari
2abadc73e4 fix: selected patches order (#1345) 2023-10-10 18:54:42 +07:00
KobeW50
377368f6bf docs: reflect the latest changes of ReVanced Manager (#1349) 2023-10-10 01:07:58 +07:00
Sagar
4085c10bfc fix: search with package name (#1344) 2023-10-09 11:22:15 +07:00
oSumAtrIX
657ba11e7e chore: Merge branch dev to main (#1340) 2023-10-07 02:04:26 +02:00
oSumAtrIX
a9ae45fe63 build: Bump version to v1.11.2 2023-10-07 02:01:10 +02:00
oSumAtrIX
61bb39b46f build: Bump dependencies to improve merging integrations speed 2023-10-07 02:01:10 +02:00
Benjamin Halko
2ad106f7d7 fix(export-settings): remove boolean workaround 2023-10-07 02:01:09 +02:00
Benjamin
8fd4fe0e55 feat(patcher): improve logs (#1299)
Co-authored-by: Ushie <ushiekane@gmail.com>
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-10-07 02:01:09 +02:00
oSumAtrIX
b1c9aedac3 build: Bump version to v1.11.1 2023-10-05 17:45:56 +02:00
oSumAtrIX
a80415be02 build: Bump dependencies 2023-10-05 17:45:56 +02:00
oSumAtrIX
d9acd0d74b chore: Merge branch dev to main (#1329) 2023-10-05 01:35:08 +02:00
oSumAtrIX
7ae09159ba build: Bump version to v1.11.0 2023-10-05 01:23:48 +02:00
oSumAtrIX
a709abd80c perf: Do not load patches twice (#1328) 2023-10-04 21:58:25 +02:00
Benjamin Halko
cd07f39b69 fix: reset patches after patching 2023-10-04 12:16:56 -07:00
Benjamin Halko
f7c11d07a8 fix(export-settings): export patches as json object 2023-10-04 11:33:13 -07:00
oSumAtrIX
b07439d402 fix: Reload patches 2023-10-04 19:38:34 +02:00
oSumAtrIX
d8eadc2a2d feat: Use simpler wording 2023-10-04 19:01:17 +02:00
oSumAtrIX
3a88d4d3e6 build: Bump dependencies 2023-10-04 18:39:24 +02:00
oSumAtrIX
012110f008 perf: Do not load patches twice 2023-10-04 18:39:24 +02:00
oSumAtrIX
4de274bf62 feat: Export settings migration activity (#1308) 2023-10-01 19:12:31 +02:00
oSumAtrIX
76b89baee3 build: Bump dependencies (#1311) 2023-10-01 19:03:26 +02:00
oSumAtrIX
697ae92031 Apply suggestions from code review [skip ci] 2023-10-01 19:02:49 +02:00
oSumAtrIX
c87f92b346 feat: Adjust install dialog labels 2023-10-01 06:21:03 +02:00
oSumAtrIX
6961bb7fd0 fix: Do not delete cached downloads 2023-10-01 04:48:24 +02:00
oSumAtrIX
6e26130744 chore: Add todo 2023-09-30 22:15:46 +02:00
oSumAtrIX
123a375a27 refactor: Remove unused strings 2023-09-30 22:14:51 +02:00
oSumAtrIX
2b4b3ca0a5 fix: Retrieve app information from patched app 2023-09-30 21:40:03 +02:00
oSumAtrIX
c4a795418f fix: Move installation log to correct place 2023-09-30 21:13:32 +02:00
oSumAtrIX
91837ebade feat: Remove original package name in app info view 2023-09-30 21:07:26 +02:00
oSumAtrIX
0492e910ea fix: Fill the preferred action 2023-09-30 20:11:53 +02:00
oSumAtrIX
36c86e22b1 fix: Load installed apps 2023-09-30 20:03:09 +02:00
oSumAtrIX
6bdc0c7bb2 feat: Simplify label 2023-09-30 19:58:45 +02:00
aAbed
1e8d8f749a fix: do not ask for patches consent before initializing model 2023-09-30 14:57:48 +05:45
oSumAtrIX
2e8e3b0d1e fix: Do not hardcode any predefined packages 2023-09-29 20:12:39 +02:00
oSumAtrIX
15b8613d3c feat: Only log relevant records 2023-09-29 19:40:22 +02:00
oSumAtrIX
8ce266bc94 perf: Reduce amount of network requests 2023-09-29 18:39:07 +02:00
oSumAtrIX
8661d72e45 feat: Simplify label 2023-09-29 17:00:34 +02:00
oSumAtrIX
62505f2543 build: Bump dependencies 2023-09-28 17:36:10 +02:00
Pun Butrach
37986c58ec docs(building): correct path to gradle.properties 2023-09-28 21:45:41 +07:00
Benjamin Halko
2968d96fe9 remove log import 2023-09-27 14:42:11 -07:00
Benjamin Halko
e7c8d0e78c use same fingerprint 2023-09-27 14:36:39 -07:00
Benjamin Halko
83cbb34a5b use revanced fingerprint 2023-09-27 14:27:38 -07:00
Benjamin Halko
7559c7b67e verify fingerprint of calling app 2023-09-27 14:17:29 -07:00
Benjamin Halko
02822f4b38 remove user interaction 2023-09-27 13:47:59 -07:00
Benjamin Halko
96736afb94 make bars transparent 2023-09-27 12:32:19 -07:00
Benjamin Halko
72ae132fcd make export settings activity transparent 2023-09-27 12:21:27 -07:00
Benjamin Halko
2250e1bcab convert Booleans to Ints 2023-09-26 16:46:49 -07:00
Benjamin Halko
d9d5b746c3 Added ExportSettingsActivity 2023-09-26 16:21:46 -07:00
Benjamin Halko
f1ea306291 change booleans to numbers 2023-09-25 14:42:40 -07:00
Benjamin Halko
378d62395a remove newlines from base64 output 2023-09-25 10:15:56 -07:00
Benjamin Halko
99c92069b9 export saved patches and keystore 2023-09-25 08:43:10 -07:00
Benjamin Halko
2a89ef797f feat: share settings 2023-09-24 16:56:25 -07:00
Ushie
5838550188 build: bump version to v1.10.3 2023-09-23 13:10:46 +03:00
Ushie
e0e01ae3ee chore: merge dev to main (#1300) 2023-09-23 13:10:08 +03:00
Benjamin
0983ba8a0f fix: search bar overflow (#1301) 2023-09-23 00:51:25 +03:00
Ushie
0bfa776ce7 fix: npe when loading patch bundle on android 8 2023-09-23 00:24:17 +03:00
Pun Butrach
d2b09936d1 chore: merge dev to main (#1295) 2023-09-22 22:08:13 +07:00
Pun Butrach
68e9f0f7c1 build: bump version 1.10.2
!!
2023-09-22 22:05:31 +07:00
Benjamin
c3d345de80 fix: force disable material you on Android 11 and below (#1293) 2023-09-22 21:05:13 +07:00
Pun Butrach
385c0e246a build: use correct version code
The user won't notice it :shhh: we're fine, continue as normal.
2023-09-21 19:32:10 +07:00
Benjamin Halko
5ead49a5b7 build: bump version to v1.10.1 2023-09-20 20:06:19 -07:00
Benjamin
c0760b1347 chore: merge dev to main (#1289) 2023-09-20 20:04:10 -07:00
Benjamin Halko
e01b323aee fix: make entire theme item clickable 2023-09-20 20:03:47 -07:00
Benjamin Halko
6f4866ef63 fix: default theme not following system (#1288) 2023-09-20 20:03:15 -07:00
Benjamin Halko
1b6d72661c build: bump version to v1.10.0 2023-09-20 17:40:23 -07:00
Benjamin
c59d4aea81 chore: merge dev to main (#1239) 2023-09-20 17:35:01 -07:00
Benjamin
6260a80738 feat(settings - appearance): add system option (#1279)
Closes #1260
2023-09-21 03:25:23 +03:00
Benjamin
e75d3c8273 fix(install-type): update padding and enable radio list scrolling (#1287)
fix(install-type): update padding and enable radio list scrolling
2023-09-20 17:24:37 -07:00
Benjamin Halko
b7acb475e9 fix: update install type dialog padding 2023-09-20 16:57:13 -07:00
Benjamin
42b6bbff7c fix: update youtube link (#1286) 2023-09-21 02:16:55 +03:00
aAbed
4b8542b35b fix: load patches via PatchBundle (#1242) 2023-09-21 01:35:32 +03:00
Julienraptor01
9ad1d6cbfb fix(custom-sources): ignore casing when checking if default repo is being used (#1281) 2023-09-21 00:42:29 +03:00
KobeW50
4cdd9acd73 docs(readme): add documentation and minor fixes (#1264)
Co-authored-by: Pun Butrach <pun.butrach@gmail.com>
2023-09-16 17:38:06 +07:00
Benjamin
f4b0a695d6 fix: improve app list loading speed (#1166) 2023-09-15 22:58:15 +07:00
Benjamin
b525ea1ba4 ci: remove analyze workflow (#1262)
Code style is not enforced so analysis is not needed.
2023-09-12 23:40:00 +07:00
Pun Butrach
c1fc2c4766 ci: bump actions/checkout to v4 2023-09-10 14:43:58 +07:00
Ushie
5c733932c7 ci(pr-build): revert "sign apk with keystore (#1231)"
This reverts commit 8bf08ff464, as it fails for PRs originating from forks
2023-09-08 01:14:13 +02:00
Ushie
d1218616ec docs(readme): minor improvements 2023-09-08 01:14:13 +02:00
aAbed
2bf6a03d56 fix: back button closing the app from any page 2023-09-08 01:14:13 +02:00
Pun Butrach
b6ee63c1ea ci(pr-build): sign apk with keystore (#1231) 2023-09-08 01:14:13 +02:00
oSumAtrIX
6d08efdcd7 chore: fix issue template 2023-09-08 01:13:42 +02:00
Ushie
a0a43a5651 build: bump version to v1.9.5 2023-09-08 01:13:42 +02:00
Ushie
3af2f5b032 chore: merge dev to main (#1177) 2023-09-04 04:15:12 +03:00
Ushie
8f54b226b4 refactor(patches-selector): improve universal patches header 2023-09-03 21:14:09 +03:00
Ushie
9f64011b26 fix: npe when patching on android 8 2023-09-03 20:54:42 +03:00
Pun Butrach
c5fc54e721 fix(installer): open the patched app after install (#1233) 2023-09-03 21:48:28 +07:00
Jay Gajjar
fc8a4fc5b6 fix: Don't use 'BuildContext's across async gaps. (#1148) 2023-09-03 01:47:20 +03:00
Benjamin
6f9ab232ae chore: ignore the root .gradle folder (#1160) 2023-09-02 09:29:49 -07:00
aAbed
8cb96f1e45 fix: permissions handling at first launch 2023-08-31 19:34:12 +05:45
Pun Butrach
5733acb77a build(dependency): update patcher to v14.2.2
https://github.com/ReVanced/revanced-patcher/releases/tag/v14.2.2
2023-08-31 20:35:24 +07:00
oSumAtrIX
e49bcb2a69 chore: simplify issue templates (#1165) 2023-08-31 17:36:29 +07:00
Ushie
42e41c399f fix: hide install button on error 2023-08-28 19:05:30 +03:00
Ushie
166a3180d3 build: correct version code 2023-08-28 03:20:15 +03:00
Ushie
3bf4982f23 chore: merge dev to main (#1163) 2023-08-28 02:47:47 +03:00
oSumAtrIX
f4e1cccfac build: bump version to v1.9.4 2023-08-28 01:44:46 +02:00
oSumAtrIX
7911a8f49e fix: properly log messages and progress 2023-08-28 01:44:46 +02:00
oSumAtrIX
64a96fc3ce fix: close before returning 2023-08-28 01:44:46 +02:00
BenjaminHalko
8e2cfbddc5 fix: ignore the root .gradle folder 2023-08-27 14:05:39 -07:00
oSumAtrIX
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
aAbed
e45a7824c1 build: bump version to v1.9.3 2023-08-27 13:27:53 +07:00
aAbed
5d72c48a76 chore: merge dev to main (#1157) 2023-08-27 13:27:01 +07:00
aAbed
d6169c6fa2 fix: broken settings page 2023-08-27 11:55:21 +05:45
Ushie
9df6d52e2d build: bump version to v1.9.2 2023-08-27 05:40:50 +03:00
Ushie
239de8e923 chore: merge dev to main (#1156) 2023-08-27 05:40:13 +03:00
Ushie
7d553a87f3 build: revert patcher to v11.0.4 2023-08-27 05:39:24 +03:00
Ushie
557b42bc56 build: bump version to 1.9.1 2023-08-27 03:58:30 +03:00
Ushie
8423914748 chore: merge dev to main (#1155) 2023-08-27 03:57:57 +03:00
Ushie
07dce23794 build: bump patcher to v14.2.0 2023-08-27 03:55:56 +03:00
Ushie
18fd0552db build: bump version to v1.9.0 2023-08-27 02:23:36 +03:00
Ushie
d537d48f8e chore: merge dev to main (#1125) 2023-08-27 02:21:48 +03:00
Ushie
b456512bbb build: bump patcher to v14.1.0 (#1153)
Co-authored-by: aAbed <aabedhkhan@gmail.com>
2023-08-27 02:21:16 +03:00
121 changed files with 11862 additions and 10237 deletions

View File

@@ -1,5 +1,5 @@
name: 🐞 Bug report name: 🐞 Bug report
description: Report a very clearly broken issue. description: Create a new bug report.
title: 'bug: <title>' title: 'bug: <title>'
labels: [bug] labels: [bug]
body: body:
@@ -8,53 +8,20 @@ body:
value: | value: |
# ReVanced Manager bug report # ReVanced Manager bug report
Important to note that your issue may have already been reported before. Please check for existing issues [here](https://github.com/revanced/revanced-manager/labels/bug). Please check for existing issues [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
- type: dropdown
attributes:
label: Type
options:
- Error while running the manager
- Error at runtime
- Cosmetic
- Other
validations:
required: true
- type: textarea - type: textarea
attributes: attributes:
label: Bug description label: Bug description
description: How did you find the bug? Any additional details that might help? description: |
- Describe your bug in detail
- Add steps to reproduce the bug if possible (Step 1. Download some files. Step 2. ...)
- Add images and videos if possible
- List selected patches if applicable
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Steps to reproduce label: Version of ReVanced Manager and version & name of application you tried to patch
description: Add the steps to reproduce this bug, including your environment.
placeholder: Step 1. Download some files. Step 2. ...
validations:
required: true
- type: textarea
attributes:
label: Android version
description: Android version used.
validations:
required: true
- type: textarea
attributes:
label: Manager version
description: Manager version used.
validations:
required: true
- type: textarea
attributes:
label: Target package name
description: App you tried to patch.
validations:
required: true
- type: textarea
attributes:
label: Target package version.
description: Version of the app you tried to patch.
validations: validations:
required: true required: true
- type: dropdown - type: dropdown
@@ -64,57 +31,31 @@ body:
- Non-root - Non-root
- Root - Root
validations: validations:
required: true required: false
- type: textarea - type: textarea
attributes: attributes:
label: Patches selected. label: Device logs
description: Patches you selected for the app. description: Export logs in ReVanced Manager settings.
validations:
required: true
- type: textarea
attributes:
label: Device logs (exported using Manager settings).
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so there is no need for backticks.
render: shell render: shell
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Installer logs (exported using Installer menu option) [unneeded if the issue is not during patching]. label: Patcher logs
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so there is no need for backticks. description: Export logs in "Patcher" screen.
render: shell render: shell
validations: validations:
required: false required: false
- type: textarea
attributes:
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:
required: false
- type: textarea
attributes:
label: Solution
description: If applicable, add a possible solution.
validations:
required: false
- type: textarea
attributes:
label: Additional context
description: Add additional context here.
validations:
required: false
- type: checkboxes - type: checkboxes
id: acknowledgments
attributes: attributes:
label: Acknowledgments label: Acknowledgements
description: Your issue will be closed if you haven't done these steps. description: Your issue will be closed if you don't follow the checklist below!
options: options:
- label: I have searched the existing issues; this is new and no duplicate or related to another open issue. - label: This request is not a duplicate of an existing issue.
required: true required: true
- label: I have written a short but informative title. - label: I have chosen an appropriate title.
required: true required: true
- label: I properly filled out all of the requested information in this issue. - label: All requested information has been provided properly.
required: true required: true
- label: The issue is solely related to ReVanced Manager and not caused by patches. - label: The issue is solely related to the ReVanced Manager
required: true required: true

View File

@@ -1,52 +1,42 @@
name: ⭐ Feature request name: ⭐ Feature request
description: Create a detailed feature request. description: Create a new feature request.
title: 'feat: <title>' title: 'feat: <title>'
labels: [feature-request] labels: 'feature request'
body: body:
- type: dropdown - type: markdown
attributes: attributes:
label: Type value: |
options: # ReVanced Manager feature request
- Functionality
- Cosmetic Please check for existing feature requests [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
- Other
validations:
required: true
- type: textarea - type: textarea
attributes: attributes:
label: Issue label: Feature description
description: What is the current problem. Why does it require a feature request? description: Describe your feature in detail.
validations:
required: true
- type: textarea
attributes:
label: Feature
description: Describe your feature in detail. How does it solve the issue?
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Motivation label: Motivation
description: Why should your feature should be considered? description: Explain why the lack of it is a problem.
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Additional context label: Additional context
description: Add additional context here. description: In case there is something else you want to add.
validations: validations:
required: false required: false
- type: checkboxes - type: checkboxes
id: acknowledgements
attributes: attributes:
label: Acknowledgements label: Acknowledgements
description: Your issue will be closed if you haven't done these steps. description: Your issue will be closed if you don't follow the checklist below!
options: options:
- label: I have searched the existing issues and this is a new and no duplicate or related to another open issue. - label: This request is not a duplicate of an existing issue.
required: true required: true
- label: I have written a short but informative title. - label: I have chosen an appropriate title.
required: true required: true
- label: I filled out all of the requested information in this issue properly. - label: All requested information has been provided properly.
required: true required: true
- label: The issue is related solely to the ReVanced Manager - label: The issue is solely related to the ReVanced Manager
required: true required: true

View File

@@ -1,38 +0,0 @@
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,45 +1,121 @@
name: PR Build name: PR Build
on: on:
pull_request: workflow_dispatch:
paths: inputs:
- ".github/workflows/pr-build.yml" # Flutter
- "android/**" flutter-branch:
- "assets/**" description: Flutter branch
- "lib/**" type: choice
default: 'stable'
options:
- stable
- beta
- dev
- master
flutter-cache:
description: Cache
type: boolean
default: true
# Application configuration
app-flavour:
description: App flavour
default: 'release'
type: choice
options:
- release
- debug
- profile
# Pull Request
pr-number:
description: PR number (No hashtag)
required: true
run-name: "Build PR ${{ inputs.pr-number }}"
jobs: jobs:
build: build:
name: Build name: Build the application
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps: steps:
- name: Setup
env:
GH_TOKEN: ${{ github.token }}
run: |
gh repo clone "${{ github.repository }}"
cd revanced-manager
gh repo set-default "${{ github.repository }}"
gh pr checkout "${{ inputs.pr-number }}"
echo "DATETIME=$( TZ='UTC+0' date --rfc-email )" >> $GITHUB_ENV
echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
# Make sure the release step uses its own credentials:
# https://github.com/cycjimmy/semantic-release-action#private-packages
persist-credentials: false persist-credentials: false
fetch-depth: 0
- name: Setup JDK - name: Setup JDK
uses: actions/setup-java@v3 uses: actions/setup-java@v4
with: with:
java-version: '11' java-version: '17'
distribution: 'zulu' distribution: 'zulu'
cache: gradle
- name: Setup Flutter - name: Setup Flutter
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: 'stable' channel: ${{ inputs.flutter-branch }}
cache: true cache: ${{ inputs.flutter-cache }}
- name: Install Flutter dependencies - name: Install Flutter dependencies
run: flutter pub get run: flutter pub get
- name: Generate files with Builder - name: Generate files with Builder
run: flutter packages pub run build_runner build --delete-conflicting-outputs run: dart run build_runner build --delete-conflicting-outputs
- name: Build with Flutter - name: Build with Flutter
env: continue-on-error: true
id: flutter-build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: flutter build apk --debug run: |
flutter build apk --"${{ inputs.app-flavour }}";
- name: Prepare to comment
run: |
if [[ "${{ steps.flutter-build.outcome }}" == "success" ]]; then
echo "MESSAGE=✅ ReVanced Manager ${{ env.COMMIT_HASH }} build succeeded." >> $GITHUB_ENV
else
echo "MESSAGE=🚫 ReVanced Manager ${{ env.COMMIT_HASH }} build failed." >> $GITHUB_ENV
fi
- name: "Comment to Pull Request #${{ inputs.pr-number }}"
uses: thollander/actions-comment-pull-request@v2
with:
GITHUB_TOKEN: ${{ github.token }}
pr_number: ${{ inputs.pr-number }}
mode: recreate
message: |
## ⚒️ ReVanced PR Build workflow
${{ env.MESSAGE }}
You can see more details in run [${{ github.run_id }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})!
### ⚙️ Overview
- App flavor: ${{ inputs.app-flavour }}
- Branch: ${{ inputs.flutter-branch }}
- Start time: ${{ env.DATETIME }}
- name: Upload build - name: Upload build
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: revanced-manager if-no-files-found: error
path: build/app/outputs/flutter-apk/app-debug.apk name: revanced-manager-(${{ env.COMMIT_HASH }}-${{ inputs.pr-number }}-${{ inputs.app-flavour }})-${{ inputs.flutter-branch }}
path: |
build/app/outputs/flutter-apk/app-${{ inputs.app-flavour }}.apk
build/app/outputs/flutter-apk/app-${{ inputs.app-flavour }}.apk.sha1

View File

@@ -1,36 +1,61 @@
name: "Release Build" name: "Release Build"
on: on:
workflow_dispatch:
push: push:
tags: branches:
- "v*" - main
- dev
jobs: jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - name: Set up JDK
- name: Set up JDK 11 uses: actions/setup-java@v4
uses: actions/setup-java@v3
with: with:
java-version: "11" java-version: "17"
distribution: "zulu" distribution: "zulu"
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
with: with:
channel: "stable" channel: "stable"
- name: Set up Flutter
run: flutter pub get - name: Cache Node modules
- name: Generate files with Builder uses: actions/cache@v3
run: flutter packages pub run build_runner build --delete-conflicting-outputs with:
- name: Build with Flutter path: |
node_modules
key: npm-${{ hashFiles('package-lock.json') }}
- name: Setup semantic-release
run: npm install
- name: Get version code
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }} run: |
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} npx semantic-release --extends ./.releaserc.getversion.json --no-ci --dry-run
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }} echo "RELEASE_VERSION=v$(cat .VERSION)" >> $GITHUB_ENV
- name: Bump pubspec version
run: |
VERSION=$(echo "${RELEASE_VERSION}" | sed 's/v//')
IFS='.' read -r -a nums <<< "${VERSION/-dev/}.0"
VERSIONCODE=$((nums[0] * 100000000 + nums[1] * 100000 + nums[2] * 100 + nums[3]))
sed -i "/^version/c\\version: $VERSION+$VERSIONCODE" pubspec.yaml
- name: Set up Flutter
run: flutter pub get
- name: Generate files with Builder
run: dart run build_runner build --delete-conflicting-outputs
- name: Build with Flutter
run: flutter build apk run: flutter build apk
- name: Sign APK - name: Sign APK
id: sign_apk id: sign_apk
uses: ilharp/sign-android-release@v1 uses: ilharp/sign-android-release@v1
@@ -40,11 +65,11 @@ jobs:
keyStorePassword: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }} keyStorePassword: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
keyAlias: ${{ secrets.SIGNING_KEY_ALIAS }} keyAlias: ${{ secrets.SIGNING_KEY_ALIAS }}
keyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }} keyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
- name: Add version to APK
- name: Rename APK
run: mv ${{steps.sign_apk.outputs.signedFile}} revanced-manager-${{ env.RELEASE_VERSION }}.apk run: mv ${{steps.sign_apk.outputs.signedFile}} revanced-manager-${{ env.RELEASE_VERSION }}.apk
- name: Publish release APK - name: Publish release APK
uses: "marvinpinto/action-automatic-releases@latest" env:
with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
repo_token: "${{ secrets.GITHUB_TOKEN }}" run: npx semantic-release --extends ./.releaserc.release.json
prerelease: false
files: revanced-manager-${{ env.RELEASE_VERSION }}.apk

2
.gitignore vendored
View File

@@ -9,6 +9,7 @@
.buildlog/ .buildlog/
.history .history
.svn/ .svn/
local.properties
# IntelliJ related # IntelliJ related
*.iml *.iml
@@ -58,6 +59,7 @@ unlinked.ds
unlinked_spec.ds unlinked_spec.ds
# Android related # Android related
.gradle/
**/android/**/gradle-wrapper.jar **/android/**/gradle-wrapper.jar
**/android/.gradle **/android/.gradle
**/android/captures/ **/android/captures/

View File

@@ -1,75 +0,0 @@
{
"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
}
]
]
}

View File

@@ -0,0 +1,24 @@
{
"branches": [
"main",
{
"name": "dev",
"prerelease": true
}
],
"plugins": [
[
"@semantic-release/commit-analyzer", {
"releaseRules": [
{ "type": "build", "scope": "Needs bump", "release": "patch" }
]
}
],
[
"@semantic-release/exec",
{
"verifyReleaseCmd": "echo ${nextRelease.version} > .VERSION"
}
]
]
}

46
.releaserc.release.json Normal file
View File

@@ -0,0 +1,46 @@
{
"branches": [
"main",
{
"name": "dev",
"prerelease": true
}
],
"plugins": [
[
"@semantic-release/commit-analyzer", {
"releaseRules": [
{ "type": "build", "scope": "Needs bump", "release": "patch" }
]
}
],
"@semantic-release/changelog",
"@semantic-release/release-notes-generator",
[
"@semantic-release/git",
{
"assets": [
"pubspec.yaml"
]
}
],
[
"@semantic-release/github",
{
"assets": [
{
"path": "revanced-manager-v*.apk"
}
],
"successComment": false
}
],
[
"@saithodev/semantic-release-backmerge",
{
"backmergeBranches": [{"from": "main", "to": "dev"}],
"clearWorkspace": true
}
]
]
}

View File

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

View File

@@ -1 +0,0 @@

103
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,103 @@
<p align="center">
<picture>
<source
width="256px"
media="(prefers-color-scheme: dark)"
srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="assets/revanced-headline/revanced-headline-vertical-light.svg"
>
</picture>
<br>
<a href="https://revanced.app/">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo-round.svg" />
<img height="24px" src="assets/revanced-logo/revanced-logo-round.svg" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://github.com/ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="http://revanced.app/discord">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://reddit.com/r/revancedapp">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://t.me/app_revanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://x.com/revancedapp">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://www.youtube.com/@ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</picture>
</a>
<br>
<br>
Continuing the legacy of Vanced
</p>
# 👋 Contribution guidelines
This document describes how to contribute to ReVanced Manager.
## 📖 Resources to help you get started
* The [documentation](/docs/README.md) provides steps to build ReVanced Manager from source
* Our [backlog](https://github.com/orgs/ReVanced/projects/12) is where we keep track of what we're working on
* [Issues](https://github.com/ReVanced/revanced-manager/issues) are where we keep track of bugs and feature requests
## 🙏 Submitting a feature request
Features can be requested by opening an issue using the
[feature request issue template](https://github.com/ReVanced/revanced-manager/issues/new?assignees=&labels=feature-request&projects=&template=feature-issue.yml&title=feat%3A+%3Ctitle%3E).
> [!NOTE]
> We may reject your request at the discretion of ReVanced Manager's maintainers,
> please provide good motivation for a request to be accepted.
## 🐞 Submitting a bug report
If you encounter a bug while using the ReVanced Manager app, open an issue using the
[bug report issue template](https://github.com/ReVanced/revanced-manager/issues/new?assignees=&labels=bug&projects=&template=bug-issue.yml&title=bug%3A+%3Ctitle%3E).
## 📝 How to contribute
> [!TIP]
> We recommend that you discuss your changes with
> the maintainers of ReVanced Manager before contributing.
> This will help you determine whether your change is acceptable.
1. Fork the repository and create a new branch based off the `dev` branch
2. Commit your changes
3. Open a pull request to the `dev` branch and reference issues that your pull request closes
4. The maintainers of ReVanced Manager will review and provide suggestions.
Once your pull request is approved and merged, it will be included in the next release of ReVanced Manager
## 🤚 I want to contribute but don't know how to code
Even if you don't know how to code, you can still contribute by
translating ReVanced Manager on [Crowdin](https://translate.revanced.app/).
❤️ Thank you for considering contributing to ReVanced Manager.

106
README.md
View File

@@ -1,31 +1,97 @@
<p align="center">
<picture>
<source
width="256px"
media="(prefers-color-scheme: dark)"
srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="assets/revanced-headline/revanced-headline-vertical-light.svg"
>
</picture>
<br>
<a href="https://revanced.app/">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo-round.svg" />
<img height="24px" src="assets/revanced-logo/revanced-logo-round.svg" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://github.com/ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="http://revanced.app/discord">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://reddit.com/r/revancedapp">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://t.me/app_revanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://x.com/revancedapp">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://www.youtube.com/@ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</picture>
</a>
<br>
<br>
Continuing the legacy of Vanced
</p>
# 💊 ReVanced Manager # 💊 ReVanced Manager
The official ReVanced Manager based on Flutter. [![GitHub last commit](https://img.shields.io/github/last-commit/ReVanced/revanced-manager)](https://github.com/ReVanced/revanced-manager/commits "")
[![GitHub commit activity](https://img.shields.io/github/commit-activity/w/ReVanced/revanced-manager)](https://github.com/ReVanced/revanced-manager/commits "")
ReVanced Manager is an Android application that uses ReVanced Patcher to add, remove, and modify existing functionalities in Android applications.
## 💪 Features
Some of the features we provide are:
* 📱 **Portable**: ReVanced Patcher that fit in your pocket;
* 🤗 **Intuitive UI**: Help you manage your patched applications with easy-to-use interface;
* 🛠️ **Customization**: Patch with third-party sources;
* ✨ And a **lot more!**
## 🔽 Download ## 🔽 Download
To download latest Manager, go [here](https://github.com/revanced/revanced-manager/releases/latest) and install the provided APK file.
## 📝 Prerequisites You can get ReVanced Manager by downloading from [ReVanced site](https://revanced.app/download) or [GitHub releases](https://github.com/ReVanced/revanced-manager/releases).
1. Android 8 or higher
2. Does not work on some armv7 devices
## 🔴 Issues ## 📚 Everything else
For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose).
## 💭 Discussion ### 📄 Documentation
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.
Documentation on how to use the application is available [here](/docs/README.md).
## 🌐 Translation ### 👋 Contributing
[![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) Thank you for considering contributing to ReVanced Manager, you can find the contribution guidelines [here](/CONTRIBUTING.md).
## 🛠️ Building Manager from source ### 🔴 Issues
1. Setup flutter environment for your [platform](https://docs.flutter.dev/get-started/install)
2. Clone the repository locally For suggestions and bug reports, open an issue [here](https://github.com/ReVanced/revanced-manager/issues/choose).
3. Add your github token in gradle.properties like [this](/docs/4_building.md)
4. Open the project in terminal ## ⚖️ License
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) ReVanced Manager adopts the [GNU General Public License 3.0](/LICENSE), [tl;dr](https://www.tldrlegal.com/license/gnu-general-public-license-v3-gpl-3): You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build & install instructions.
7. To build release apk run `flutter build apk`

78
SECURITY.md Normal file
View File

@@ -0,0 +1,78 @@
<p align="center">
<picture>
<source
width="256px"
media="(prefers-color-scheme: dark)"
srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="assets/revanced-headline/revanced-headline-vertical-light.svg"
>
</picture>
<br>
<a href="https://revanced.app/">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo-round.svg" />
<img height="24px" src="assets/revanced-logo/revanced-logo-round.svg" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://github.com/ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="http://revanced.app/discord">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://reddit.com/r/revancedapp">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://t.me/app_revanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://x.com/revancedapp">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://www.youtube.com/@ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</picture>
</a>
<br>
<br>
Continuing the legacy of Vanced
</p>
# 🔒 Security Policy
This document describes how to report security vulnerabilities for ReVanced Manager.
## 🚨 Reporting a Vulnerability
Please open an issue in our [advisory tracker](https://github.com/ReVanced/revanced-manager/security/advisories/new) or reach out privately to us on [Discord](https://discord.gg/revanced).
If a vulnerability is confirmed and accepted, you can join our [Discord](https://discord.gg/revanced) server to receive a special contributor role.
### ⏳ Supported Versions
| Version | Branch | Supported |
| ------- | ------------|------------------- |
| v1.19.0 | main | :white_check_mark: |
| latest | dev | :white_check_mark: |
| latest | compose-dev | :white_check_mark: |

View File

@@ -21,7 +21,6 @@ linter:
- always_declare_return_types - always_declare_return_types
- require_trailing_commas - require_trailing_commas
- always_put_control_body_on_new_line - always_put_control_body_on_new_line
- always_require_non_null_named_parameters
- always_use_package_imports # we do this commonly - always_use_package_imports # we do this commonly
- annotate_overrides - annotate_overrides
- avoid_bool_literals_in_conditional_expressions - avoid_bool_literals_in_conditional_expressions
@@ -40,8 +39,6 @@ linter:
- avoid_relative_lib_imports - avoid_relative_lib_imports
- avoid_renaming_method_parameters - avoid_renaming_method_parameters
- avoid_return_types_on_setters - avoid_return_types_on_setters
- avoid_returning_null
- avoid_returning_null_for_future
- avoid_returning_null_for_void - avoid_returning_null_for_void
- avoid_setters_without_getters - avoid_setters_without_getters
- avoid_shadowing_type_parameters - avoid_shadowing_type_parameters
@@ -126,6 +123,7 @@ linter:
- slash_for_doc_comments - slash_for_doc_comments
- sort_child_properties_last - sort_child_properties_last
- sort_constructors_first - sort_constructors_first
- sort_pub_dependencies
- sort_unnamed_constructors_first - sort_unnamed_constructors_first
- test_types_in_equals - test_types_in_equals
- throw_in_finally - throw_in_finally

View File

@@ -1,3 +1,9 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties() def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties') def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) { if (localPropertiesFile.exists()) {
@@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) {
} }
} }
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode') def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) { if (flutterVersionCode == null) {
flutterVersionCode = '1' flutterVersionCode = '1'
@@ -21,37 +22,31 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0' flutterVersionName = '1.0'
} }
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android { android {
compileSdkVersion flutter.compileSdkVersion compileSdk 34
ndkVersion flutter.ndkVersion ndkVersion flutter.ndkVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_11 sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_17
} }
kotlinOptions { kotlinOptions {
jvmTarget = '11' jvmTarget = '17'
} }
sourceSets { sourceSets {
main.java.srcDirs += 'src/main/kotlin' main.java.srcDirs += 'src/main/kotlin'
} }
defaultConfig { defaultConfig {
applicationId "app.revanced.manager.flutter" applicationId "app.revanced.manager.flutter"
minSdkVersion 26 minSdk 26
targetSdkVersion 33 targetSdk 34
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
} }
buildTypes { buildTypes {
release { release {
shrinkResources false
minifyEnabled false
resValue "string", "app_name", "ReVanced Manager" resValue "string", "app_name", "ReVanced Manager"
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
ndk { ndk {
@@ -68,11 +63,32 @@ android {
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64' abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
} }
} }
profile {
shrinkResources false
minifyEnabled false
resValue "string", "app_name", "ReVanced Manager Profile"
applicationIdSuffix ".profile"
signingConfig signingConfigs.debug
ndk {
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
}
}
}
packagingOptions {
jniLibs {
useLegacyPackaging true
excludes += ['/prebuilt/**']
}
resources {
excludes += ['/prebuilt/**']
}
} }
packagingOptions { namespace 'app.revanced.manager.flutter'
exclude '/prebuilt/**' }
}
kotlin {
jvmToolchain(17)
} }
flutter { flutter {
@@ -83,10 +99,9 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// ReVanced // ReVanced
implementation "app.revanced:revanced-patcher:11.0.4" implementation "app.revanced:revanced-patcher:19.1.0"
// Signing & aligning // Signing & aligning
implementation("org.bouncycastle:bcpkix-jdk15on:1.70") implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
implementation("com.android.tools.build:apksig:7.2.2") implementation("com.android.tools.build:apksig:7.2.2")
} }

View File

@@ -1,4 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android">
package="app.revanced.manager.flutter">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
</manifest> </manifest>

View File

@@ -1,8 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android">
package="app.revanced.manager.flutter">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <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_MEDIA_AUDIO" />
@@ -24,9 +23,7 @@
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:largeHeap="true" android:largeHeap="true"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true">
android:extractNativeLibs="true"
android:enableOnBackInvokedCallback="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@@ -36,13 +33,17 @@
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<meta-data <meta-data
android:name="io.flutter.embedding.android.NormalTheme" android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"/> android:resource="@style/NormalTheme" />
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".ExportSettingsActivity"
android:exported="true">
</activity>
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
@@ -55,5 +56,22 @@
android:name="android.support.FILE_PROVIDER_PATHS" android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" /> android:resource="@xml/file_paths" />
</provider> </provider>
<receiver
android:name=".utils.packageInstaller.InstallerReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="APP_INSTALL_ACTION" />
</intent-filter>
</receiver>
<receiver
android:name=".utils.packageInstaller.UninstallerReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="APP_UNINSTALL_ACTION" />
</intent-filter>
</receiver>
</application> </application>
</manifest> </manifest>

View File

@@ -0,0 +1,83 @@
package app.revanced.manager.flutter
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Base64
import org.json.JSONObject
import java.io.ByteArrayInputStream
import java.io.File
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.MessageDigest
class ExportSettingsActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val callingPackageName = getCallingPackage()!!
if (getFingerprint(callingPackageName) == getFingerprint(getPackageName())) {
// Create JSON Object
val json = JSONObject()
// Default Data
json.put("keystorePassword", "s3cur3p@ssw0rd")
// Load Shared Preferences
val sharedPreferences = getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
val allEntries: Map<String, *> = sharedPreferences.getAll()
for ((key, value) in allEntries.entries) {
json.put(key.replace("flutter.", ""), value)
}
// Load keystore
val keystoreFile = File(getExternalFilesDir(null), "/revanced-manager.keystore")
if (keystoreFile.exists()) {
val keystoreBytes = keystoreFile.readBytes()
val keystoreBase64 = Base64.encodeToString(keystoreBytes, Base64.DEFAULT)
json.put("keystore", keystoreBase64)
}
// Load saved patches
val storedPatchesFile = File(filesDir.parentFile.absolutePath, "/app_flutter/selected-patches.json")
if (storedPatchesFile.exists()) {
val patchesBytes = storedPatchesFile.readBytes()
val patches = String(patchesBytes, Charsets.UTF_8)
json.put("patches", JSONObject(patches))
}
// Send data back
val resultIntent = Intent()
resultIntent.putExtra("data", json.toString())
setResult(Activity.RESULT_OK, resultIntent)
finish()
} else {
val resultIntent = Intent()
setResult(Activity.RESULT_CANCELED)
finish()
}
}
fun getFingerprint(packageName: String): String {
// Get the signature of the app that matches the package name
val packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
val signature = packageInfo.signatures[0]
// Get the raw certificate data
val rawCert = signature.toByteArray()
// Generate an X509Certificate from the data
val certFactory = CertificateFactory.getInstance("X509")
val x509Cert = certFactory.generateCertificate(ByteArrayInputStream(rawCert)) as X509Certificate
// Get the SHA256 fingerprint
val fingerprint = MessageDigest.getInstance("SHA256").digest(x509Cert.encoded).joinToString("") {
"%02x".format(it)
}
return fingerprint
}
}

View File

@@ -1,28 +1,39 @@
package app.revanced.manager.flutter package app.revanced.manager.flutter
import android.app.PendingIntent
import android.app.SearchManager
import android.content.Intent
import android.content.pm.PackageInstaller
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import androidx.annotation.NonNull
import app.revanced.manager.flutter.utils.Aapt import app.revanced.manager.flutter.utils.Aapt
import app.revanced.manager.flutter.utils.aligning.ZipAligner import app.revanced.manager.flutter.utils.aligning.ZipAligner
import app.revanced.manager.flutter.utils.packageInstaller.InstallerReceiver
import app.revanced.manager.flutter.utils.packageInstaller.UninstallerReceiver
import app.revanced.manager.flutter.utils.signing.Signer import app.revanced.manager.flutter.utils.signing.Signer
import app.revanced.manager.flutter.utils.zip.ZipFile import app.revanced.manager.flutter.utils.zip.ZipFile
import app.revanced.manager.flutter.utils.zip.structures.ZipEntry import app.revanced.manager.flutter.utils.zip.structures.ZipEntry
import app.revanced.patcher.PatchBundleLoader
import app.revanced.patcher.PatchSet
import app.revanced.patcher.Patcher import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherOptions import app.revanced.patcher.PatcherOptions
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages import app.revanced.patcher.patch.PatchResult
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.logging.Logger
import app.revanced.patcher.util.patch.PatchBundle
import dalvik.system.DexClassLoader
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.runBlocking
import org.json.JSONArray
import org.json.JSONObject
import java.io.File 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() { class MainActivity : FlutterActivity() {
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
@@ -30,31 +41,53 @@ class MainActivity : FlutterActivity() {
private var cancel: Boolean = false private var cancel: Boolean = false
private var stopResult: MethodChannel.Result? = null private var stopResult: MethodChannel.Result? = null
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { private lateinit var patches: PatchSet
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(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 openBrowserChannel = "app.revanced.manager.flutter/browser"
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, openBrowserChannel).setMethodCallHandler { call, result ->
if (call.method == "openBrowser") {
val searchQuery = call.argument<String>("query")
openBrowser(searchQuery)
result.success(null)
} else {
result.notImplemented()
}
}
val mainChannel =
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, patcherChannel)
this.installerChannel =
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, installerChannel)
mainChannel.setMethodCallHandler { call, result -> mainChannel.setMethodCallHandler { call, result ->
when (call.method) { when (call.method) {
"runPatcher" -> { "runPatcher" -> {
val patchBundleFilePath = call.argument<String>("patchBundleFilePath")
val originalFilePath = call.argument<String>("originalFilePath") val originalFilePath = call.argument<String>("originalFilePath")
val inputFilePath = call.argument<String>("inputFilePath") val inputFilePath = call.argument<String>("inputFilePath")
val patchedFilePath = call.argument<String>("patchedFilePath") val patchedFilePath = call.argument<String>("patchedFilePath")
val outFilePath = call.argument<String>("outFilePath") val outFilePath = call.argument<String>("outFilePath")
val integrationsPath = call.argument<String>("integrationsPath") val integrationsPath = call.argument<String>("integrationsPath")
val selectedPatches = call.argument<List<String>>("selectedPatches") val selectedPatches = call.argument<List<String>>("selectedPatches")
val options = call.argument<Map<String, Map<String, Any>>>("options")
val cacheDirPath = call.argument<String>("cacheDirPath") val cacheDirPath = call.argument<String>("cacheDirPath")
val keyStoreFilePath = call.argument<String>("keyStoreFilePath") val keyStoreFilePath = call.argument<String>("keyStoreFilePath")
val keystorePassword = call.argument<String>("keystorePassword") val keystorePassword = call.argument<String>("keystorePassword")
if (patchBundleFilePath != null && if (
originalFilePath != null && originalFilePath != null &&
inputFilePath != null && inputFilePath != null &&
patchedFilePath != null && patchedFilePath != null &&
outFilePath != null && outFilePath != null &&
integrationsPath != null && integrationsPath != null &&
selectedPatches != null && selectedPatches != null &&
options != null &&
cacheDirPath != null && cacheDirPath != null &&
keyStoreFilePath != null && keyStoreFilePath != null &&
keystorePassword != null keystorePassword != null
@@ -62,39 +95,136 @@ class MainActivity : FlutterActivity() {
cancel = false cancel = false
runPatcher( runPatcher(
result, result,
patchBundleFilePath,
originalFilePath, originalFilePath,
inputFilePath, inputFilePath,
patchedFilePath, patchedFilePath,
outFilePath, outFilePath,
integrationsPath, integrationsPath,
selectedPatches, selectedPatches,
options,
cacheDirPath, cacheDirPath,
keyStoreFilePath, keyStoreFilePath,
keystorePassword keystorePassword
) )
} else { } else result.notImplemented()
result.notImplemented()
}
} }
"stopPatcher" -> { "stopPatcher" -> {
cancel = true cancel = true
stopResult = result stopResult = result
} }
"getPatches" -> {
val patchBundleFilePath = call.argument<String>("patchBundleFilePath")!!
val cacheDirPath = call.argument<String>("cacheDirPath")!!
try {
val patchBundleFile = File(patchBundleFilePath)
patchBundleFile.setWritable(false)
patches = PatchBundleLoader.Dex(
patchBundleFile,
optimizedDexDirectory = File(cacheDirPath)
)
} catch (ex: Exception) {
return@setMethodCallHandler result.notImplemented()
} catch (err: Error) {
return@setMethodCallHandler result.notImplemented()
}
JSONArray().apply {
patches.forEach {
JSONObject().apply {
put("name", it.name)
put("description", it.description)
put("excluded", !it.use)
put("compatiblePackages", JSONArray().apply {
it.compatiblePackages?.forEach { compatiblePackage ->
val compatiblePackageJson = JSONObject().apply {
put("name", compatiblePackage.name)
put(
"versions",
JSONArray().apply {
compatiblePackage.versions?.forEach { version ->
put(version)
}
})
}
put(compatiblePackageJson)
}
})
put("options", JSONArray().apply {
it.options.values.forEach { option ->
JSONObject().apply {
put("key", option.key)
put("title", option.title)
put("description", option.description)
put("required", option.required)
fun JSONObject.putValue(
value: Any?,
key: String = "value"
) = if (value is Array<*>) put(
key,
JSONArray().apply {
value.forEach { put(it) }
})
else put(key, value)
putValue(option.default)
option.values?.let { values ->
put("values",
JSONObject().apply {
values.forEach { (key, value) ->
putValue(value, key)
}
})
} ?: put("values", null)
put("valueType", option.valueType)
}.let(::put)
}
})
}.let(::put)
}
}.toString().let(result::success)
}
"installApk" -> {
val apkPath = call.argument<String>("apkPath")!!
PackageInstallerManager.result = result
installApk(apkPath)
}
"uninstallApp" -> {
val packageName = call.argument<String>("packageName")!!
uninstallApp(packageName)
PackageInstallerManager.result = result
}
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
} }
private fun openBrowser(query: String?) {
val intent = Intent(Intent.ACTION_WEB_SEARCH).apply {
putExtra(SearchManager.QUERY, query)
}
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
}
}
@OptIn(InternalCoroutinesApi::class)
private fun runPatcher( private fun runPatcher(
result: MethodChannel.Result, result: MethodChannel.Result,
patchBundleFilePath: String,
originalFilePath: String, originalFilePath: String,
inputFilePath: String, inputFilePath: String,
patchedFilePath: String, patchedFilePath: String,
outFilePath: String, outFilePath: String,
integrationsPath: String, integrationsPath: String,
selectedPatches: List<String>, selectedPatches: List<String>,
options: Map<String, Map<String, Any>>,
cacheDirPath: String, cacheDirPath: String,
keyStoreFilePath: String, keyStoreFilePath: String,
keystorePassword: String keystorePassword: String
@@ -105,179 +235,146 @@ class MainActivity : FlutterActivity() {
val outFile = File(outFilePath) val outFile = File(outFilePath)
val integrations = File(integrationsPath) val integrations = File(integrationsPath)
val keyStoreFile = File(keyStoreFilePath) val keyStoreFile = File(keyStoreFilePath)
val cacheDir = File(cacheDirPath)
Thread { Thread {
try { fun updateProgress(progress: Double, header: String, log: String) {
handler.post { handler.post {
installerChannel.invokeMethod( installerChannel.invokeMethod(
"update", "update",
mapOf( mapOf(
"progress" to 0.1, "progress" to progress,
"header" to "", "header" to header,
"log" to "Copying original apk" "log" to log
) )
) )
} }
}
if(cancel) { fun postStop() = handler.post { stopResult!!.success(null) }
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) {
if (record.loggerName?.startsWith("app.revanced") != true || cancel) return
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 return@Thread
} }
originalFile.copyTo(inputFile, true) originalFile.copyTo(inputFile, true)
handler.post { if (cancel) {
installerChannel.invokeMethod( postStop()
"update", return@Thread
mapOf( }
"progress" to 0.2,
"header" to "Unpacking apk...", updateProgress(0.05, "Reading APK...", "Reading APK")
"log" to "Unpacking input apk"
) val patcher = Patcher(
PatcherOptions(
inputFile,
cacheDir,
Aapt.binary(applicationContext).absolutePath,
cacheDir.path,
true // TODO: Add option to disable this
) )
} )
if(cancel) { if (cancel) {
handler.post { stopResult!!.success(null) } postStop()
return@Thread return@Thread
} }
val patcher = updateProgress(0.1, "Loading patches...", "Loading patches")
Patcher(
PatcherOptions(
inputFile,
cacheDirPath,
Aapt.binary(applicationContext).absolutePath,
cacheDirPath,
logger = ManagerLogger()
)
)
if(cancel) { val patches = patches.filter { patch ->
handler.post { stopResult!!.success(null) } val isCompatible = patch.compatiblePackages?.any {
return@Thread it.name == patcher.context.packageMetadata.packageName
} } ?: false
handler.post { val compatibleOrUniversal =
installerChannel.invokeMethod( isCompatible || patch.compatiblePackages.isNullOrEmpty()
"update",
mapOf("progress" to 0.3, "header" to "", "log" to "")
)
}
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 0.4,
"header" to "Merging integrations...",
"log" to "Merging integrations"
)
)
}
if(cancel) { compatibleOrUniversal && selectedPatches.any { it == patch.name }
handler.post { stopResult!!.success(null) } }.onEach { patch ->
return@Thread options[patch.name]?.forEach { (key, value) ->
} patch.options[key] = value
patcher.addIntegrations(listOf(integrations)) {}
if(cancel) {
handler.post { stopResult!!.success(null) }
return@Thread
}
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 0.5,
"header" to "Applying patches...",
"log" to ""
)
)
}
if(cancel) {
handler.post { stopResult!!.success(null) }
return@Thread
}
val patches = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE) {
PatchBundle.Dex(
patchBundleFilePath,
DexClassLoader(
patchBundleFilePath,
cacheDirPath,
null,
javaClass.classLoader
)
).loadPatches().filter { patch ->
(patch.compatiblePackages?.any { it.name == patcher.context.packageMetadata.packageName } == true || patch.compatiblePackages.isNullOrEmpty()) &&
selectedPatches.any { it == patch.patchName }
}
} else {
TODO("VERSION.SDK_INT < CUPCAKE")
}
if(cancel) {
handler.post { stopResult!!.success(null) }
return@Thread
}
patcher.addPatches(patches)
patcher.executePatches().forEach { (patch, res) ->
if (res.isSuccess) {
val msg = "Applied $patch"
handler.post {
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 0.5,
"header" to "",
"log" to msg
)
)
}
if(cancel) {
handler.post { stopResult!!.success(null) }
return@Thread
}
return@forEach
}
val msg =
"Failed to apply $patch: " + "${res.exceptionOrNull()!!.message ?: res.exceptionOrNull()!!.cause!!::class.simpleName}"
handler.post {
installerChannel.invokeMethod(
"update",
mapOf("progress" to 0.5, "header" to "", "log" to msg)
)
}
if(cancel) {
handler.post { stopResult!!.success(null) }
return@Thread
} }
} }
handler.post { if (cancel) {
installerChannel.invokeMethod( postStop()
"update",
mapOf(
"progress" to 0.7,
"header" to "Repacking apk...",
"log" to "Repacking patched apk"
)
)
}
if(cancel) {
handler.post { stopResult!!.success(null) }
return@Thread return@Thread
} }
val res = patcher.save()
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(FlowCollector { patchResult: PatchResult ->
if (cancel) {
handler.post { stopResult!!.success(null) }
this.cancel()
this@apply.close()
return@FlowCollector
}
val msg = patchResult.exception?.let {
val writer = StringWriter()
it.printStackTrace(PrintWriter(writer))
"${patchResult.patch.name} failed: $writer"
} ?: run {
"${patchResult.patch.name} succeeded"
}
updateProgress(progress, "", msg)
progress += progressStep
})
}
}
if (cancel) {
postStop()
patcher.close()
return@Thread
}
updateProgress(0.75, "Building...", "")
val res = patcher.get()
patcher.close()
ZipFile(patchedFile).use { file -> ZipFile(patchedFile).use { file ->
res.dexFiles.forEach { res.dexFiles.forEach {
if(cancel) { if (cancel) {
handler.post { stopResult!!.success(null) } postStop()
return@Thread return@Thread
} }
file.addEntryCompressData( file.addEntryCompressData(
@@ -296,90 +393,75 @@ class MainActivity : FlutterActivity() {
ZipAligner::getEntryAlignment ZipAligner::getEntryAlignment
) )
} }
if(cancel) {
handler.post { stopResult!!.success(null) } if (cancel) {
postStop()
return@Thread return@Thread
} }
handler.post {
installerChannel.invokeMethod( updateProgress(0.8, "Signing...", "Signing APK")
"update",
mapOf(
"progress" to 0.9,
"header" to "Signing apk...",
"log" to ""
)
)
}
try { try {
Signer("ReVanced", keystorePassword).signApk( Signer("ReVanced", keystorePassword)
patchedFile, .signApk(patchedFile, outFile, keyStoreFile)
outFile,
keyStoreFile
)
} catch (e: Exception) { } catch (e: Exception) {
//log to console print("Error signing APK: ${e.message}")
print("Error signing apk: ${e.message}")
e.printStackTrace() e.printStackTrace()
} }
handler.post { updateProgress(.85, "Patched", "Patched APK")
installerChannel.invokeMethod(
"update",
mapOf(
"progress" to 1.0,
"header" to "Finished!",
"log" to "Finished!"
)
)
}
} catch (ex: Throwable) { } catch (ex: Throwable) {
val stack = ex.stackTraceToString() if (!cancel) {
handler.post { val stack = ex.stackTraceToString()
installerChannel.invokeMethod( updateProgress(
"update", -100.0,
mapOf( "Failed",
"progress" to -100.0, "An error occurred:\n$stack"
"header" to "Aborted...",
"log" to "An error occurred! Aborted\nError:\n$stack"
)
) )
} }
} }
handler.post { result.success(null) } handler.post { result.success(null) }
}.start() }.start()
} }
inner class ManagerLogger : Logger { private fun installApk(apkPath: String) {
override fun error(msg: String) { val packageInstaller: PackageInstaller = applicationContext.packageManager.packageInstaller
handler.post { val sessionParams = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
installerChannel val sessionId: Int = packageInstaller.createSession(sessionParams)
.invokeMethod( val session: PackageInstaller.Session = packageInstaller.openSession(sessionId)
"update", session.use { activeSession ->
mapOf("progress" to -1.0, "header" to "", "log" to msg) val sessionOutputStream = activeSession.openWrite(applicationContext.packageName, 0, -1)
) sessionOutputStream.use { outputStream ->
val apkFile = File(apkPath)
apkFile.inputStream().use { inputStream ->
inputStream.copyTo(outputStream)
}
} }
} }
val receiverIntent = Intent(applicationContext, InstallerReceiver::class.java).apply {
override fun warn(msg: String) { action = "APP_INSTALL_ACTION"
handler.post {
installerChannel.invokeMethod(
"update",
mapOf("progress" to -1.0, "header" to "", "log" to msg)
)
}
} }
val receiverPendingIntent = PendingIntent.getBroadcast(context, sessionId, receiverIntent, PackageInstallerManager.flags)
session.commit(receiverPendingIntent.intentSender)
session.close()
}
override fun info(msg: String) { private fun uninstallApp(packageName: String) {
handler.post { val packageInstaller: PackageInstaller = applicationContext.packageManager.packageInstaller
installerChannel.invokeMethod( val receiverIntent = Intent(applicationContext, UninstallerReceiver::class.java).apply {
"update", action = "APP_UNINSTALL_ACTION"
mapOf("progress" to -1.0, "header" to "", "log" to msg)
)
}
} }
val receiverPendingIntent = PendingIntent.getBroadcast(context, 0, receiverIntent, PackageInstallerManager.flags)
packageInstaller.uninstall(packageName, receiverPendingIntent.intentSender)
}
override fun trace(_msg: String) { /* unused */ object PackageInstallerManager {
var result: MethodChannel.Result? = null
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
} }
} }
} }

View File

@@ -0,0 +1,32 @@
package app.revanced.manager.flutter.utils.packageInstaller
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import app.revanced.manager.flutter.MainActivity
class InstallerReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
if (confirmationIntent != null) {
context.startActivity(confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
}
}
else -> {
val packageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
val otherPackageName = intent.getStringExtra(PackageInstaller.EXTRA_OTHER_PACKAGE_NAME)
MainActivity.PackageInstallerManager.result!!.success(mapOf(
"status" to status,
"packageName" to packageName,
"message" to message,
"otherPackageName" to otherPackageName
))
}
}
}
}

View File

@@ -0,0 +1,24 @@
package app.revanced.manager.flutter.utils.packageInstaller
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import app.revanced.manager.flutter.MainActivity
class UninstallerReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
if (confirmationIntent != null) {
context.startActivity(confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
}
}
else -> {
MainActivity.PackageInstallerManager.result!!.success(status)
}
}
}
}

View File

@@ -1,4 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android">
package="app.revanced.manager.flutter">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
</manifest> </manifest>

View File

@@ -1,12 +1,12 @@
buildscript { buildscript {
ext.kotlin_version = '1.7.10' ext.kotlin_version = '1.9.10'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.1.3' classpath 'com.android.tools.build:gradle:8.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
} }
} }
@@ -16,11 +16,7 @@ allprojects {
google() google()
mavenCentral() mavenCentral()
maven { maven {
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher") url 'https://jitpack.io'
credentials {
username = (project.findProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR")) as String
password = (project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN")) as String
}
} }
mavenLocal() mavenLocal()
} }
@@ -29,6 +25,8 @@ allprojects {
rootProject.buildDir = '../build' rootProject.buildDir = '../build'
subprojects { subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}" project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app') project.evaluationDependsOn(':app')
} }

View File

@@ -4,3 +4,6 @@ org.gradle.daemon=true
org.gradle.caching=true org.gradle.caching=true
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false

View File

@@ -1,7 +1,8 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=a01b6587e15fe7ed120a0ee299c25982a1eee045abd6a9dd5e216b2f628ef9ac distributionSha256Sum=9d926787066a081739e8200858338b4a69e837c3a821a33aca9db09dd4a41026
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@@ -1,11 +1,20 @@
include ':app' pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}
settings.ext.flutterSdkPath = flutterSdkPath()
def localPropertiesFile = new File(rootProject.projectDir, "local.properties") includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
def properties = new Properties()
assert localPropertiesFile.exists() plugins {
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false
}
}
def flutterSdkPath = properties.getProperty("flutter.sdk") include ":app"
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle"

View File

@@ -1,6 +1,7 @@
{ {
"okButton": "OK", "okButton": "OK",
"cancelButton": "Cancel", "cancelButton": "Cancel",
"dismissButton": "Dismiss",
"quitButton": "Quit", "quitButton": "Quit",
"updateButton": "Update", "updateButton": "Update",
"enabledLabel": "Enabled", "enabledLabel": "Enabled",
@@ -10,9 +11,11 @@
"yesButton": "Yes", "yesButton": "Yes",
"noButton": "No", "noButton": "No",
"warning": "Warning", "warning": "Warning",
"options": "Options",
"notice": "Notice", "notice": "Notice",
"noShowAgain": "Don't show this again", "noShowAgain": "Don't show this again",
"new": "New", "add": "Add",
"remove": "Remove",
"navigationView": { "navigationView": {
"dashboardTab": "Dashboard", "dashboardTab": "Dashboard",
"patcherTab": "Patcher", "patcherTab": "Patcher",
@@ -23,13 +26,13 @@
"widgetTitle": "Dashboard", "widgetTitle": "Dashboard",
"updatesSubtitle": "Updates", "updatesSubtitle": "Updates",
"patchedSubtitle": "Patched applications", "patchedSubtitle": "Patched apps",
"noUpdates": "No updates available", "noUpdates": "No updates available",
"WIP": "Work in progress...", "WIP": "Work in progress...",
"noInstallations": "No patched applications installed", "noInstallations": "No patched apps installed",
"installUpdate": "Continue to install the update?", "installUpdate": "Continue to install the update?",
"updateDialogTitle": "Update Manager", "updateDialogTitle": "Update Manager",
@@ -56,9 +59,7 @@
"updatesDisabled": "Updating a patched app is currently disabled. Repatch the app again." "updatesDisabled": "Updating a patched app is currently disabled. Repatch the app again."
}, },
"applicationItem": { "applicationItem": {
"patchButton": "Patch", "infoButton": "Info"
"infoButton": "Info",
"changelogLabel": "Changelog"
}, },
"latestCommitCard": { "latestCommitCard": {
"loadingLabel": "Loading...", "loadingLabel": "Loading...",
@@ -71,22 +72,21 @@
"widgetTitle": "Patcher", "widgetTitle": "Patcher",
"patchButton": "Patch", "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. Continue anyways?",
"armv7WarningDialogText": "Patching on ARMv7 devices is not yet supported and might fail. Proceed anyways?",
"splitApkWarningDialogText": "Patching a split APK is not yet supported and might fail. Proceed anyways?", "removedPatchesWarningDialogText": "The following patches have been removed since the last time you used them.\n\n{patches}\n\nContinue anyways?",
"removedPatchesWarningDialogText": "The following patches have been removed since the last time you used them.\n\n{patches}\n\nProceed anyways?" "requiredOptionDialogText" : "Some patch options have to be set."
}, },
"appSelectorCard": { "appSelectorCard": {
"widgetTitle": "Select an application", "widgetTitle": "Select an app",
"widgetTitleSelected": "Selected application", "widgetTitleSelected": "Selected app",
"widgetSubtitle": "No application selected", "widgetSubtitle": "No app selected",
"noAppsLabel": "No applications found", "noAppsLabel": "No applications found",
"notInstalled":"App not installed",
"currentVersion": "Current", "currentVersion": "Current",
"suggestedVersion": "Suggested", "suggestedVersion": "Suggested",
"allVersions": "All versions" "anyVersion": "Any version"
}, },
"patchSelectorCard": { "patchSelectorCard": {
"widgetTitle": "Select patches", "widgetTitle": "Select patches",
@@ -100,23 +100,27 @@
"widgetSubtitle": "We are online!" "widgetSubtitle": "We are online!"
}, },
"appSelectorView": { "appSelectorView": {
"viewTitle": "Select an application", "viewTitle": "Select an app",
"searchBarHint": "Search applications", "searchBarHint": "Search app",
"storageButton": "Storage", "storageButton": "Storage",
"selectFromStorageButton": "Select from storage", "selectFromStorageButton": "Select from storage",
"errorMessage": "Unable to use selected application", "errorMessage": "Unable to use selected application",
"downloadToast": "Download function is not available yet", "downloadToast": "Download function is not available yet",
"requireSuggestedAppVersionDialogText": "The version of the app you have selected does not match the suggested version which can lead to unexpected issues. Please use the suggested version.\n\nSelected version: {selected}\nSuggested version: {suggested}\n\nTo continue anyway, disable \"Require suggested app version\" in the settings.",
"featureNotAvailable": "Feature not implemented", "featureNotAvailable": "Feature not implemented",
"featureNotAvailableText": "This application is a split APK and cannot be selected. Unfortunately, this feature is only available for rooted users at the moment. However, you can still install the application by selecting its APK files from your device's storage instead" "featureNotAvailableText": "This app is a split APK and can only be patched and installed reliably by mounting with root permissions. However, you can patch and install a full APK by selecting it from storage."
}, },
"patchesSelectorView": { "patchesSelectorView": {
"viewTitle": "Select patches", "viewTitle": "Select patches",
"searchBarHint": "Search patches", "searchBarHint": "Search patches",
"universalPatches": "Universal patches", "universalPatches": "Universal patches",
"newPatches": "New patches",
"patches": "Patches",
"doneButton": "Done", "doneButton": "Done",
@@ -126,46 +130,60 @@
"none": "None", "none": "None",
"noneTooltip": "Deselect all patches", "noneTooltip": "Deselect all patches",
"loadPatchesSelection": "Load patches selection", "loadPatchesSelection": "Load patch selection",
"noSavedPatches": "No saved patches for the selected app.\nPress Done to save current selection.", "noSavedPatches": "No saved patch selection for the selected app.\nPress Done to save the current selection.",
"noPatchesFound": "No patches found for the selected app", "noPatchesFound": "No patches found for the selected app",
"setRequiredOption": "Some patches require options to be set:\n\n{patches}\n\nPlease set them before continuing."
},
"patchOptionsView": {
"customValue": "Custom value",
"resetOptionsTooltip": "Reset patch options",
"viewTitle": "Patch options",
"saveOptions": "Save",
"selectAllPatchesWarningContent": "You are about to select all patches, that includes non-suggested patches and can cause unwanted behavior." "addOptions": "Add options",
"deselectPatch": "Deselect patch",
"tooltip": "More input options",
"selectFilePath": "Select file path",
"selectFolder": "Select folder",
"selectOption": "Select option",
"requiredOption": "This option is required",
"unsupportedOption": "This option is not supported",
"requiredOptionNull": "The following options have to be set:\n\n{options}"
}, },
"patchItem": { "patchItem": {
"unsupportedDialogText": "Selecting this patch may result in patching errors.\n\nApp version: {packageVersion}\nSupported versions:\n{supportedVersions}", "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.", "unsupportedPatchVersion": "Patch is not supported for this app version.",
"unsupportedRequiredOption": "This patch contains a required option that is not supported by this app",
"newPatchDialogText": "This is a new patch that has been added since the last time you have patched this app.", "patchesChangeWarningDialogText": "It is recommended to use the default patch selection and options. Changing them may result in unexpected issues.\n\nYou'll need to turn on \"Allow changing patch selection\" in settings before changing any patch selection.",
"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" "patchesChangeWarningDialogButton": "Use default selection"
}, },
"installerView": { "installerView": {
"widgetTitle": "Installer", "widgetTitle": "Installer",
"installType": "Select install type", "installType": "Select install type",
"installTypeDescription": "Select the installation type to proceed with.", "installTypeDescription": "Select the installation type to continue with.",
"installButton": "Install", "installButton": "Install",
"installRootType": "Root", "installRootType": "Mount",
"installNonRootType": "Non-root", "installNonRootType": "Regular",
"installRecommendedType": "Recommended",
"warning": "Disable auto updates for the patched app to avoid unexpected issues.",
"pressBackAgain": "Press back again to cancel", "pressBackAgain": "Press back again to cancel",
"openButton": "Open", "openButton": "Open",
"shareButton": "Share file", "shareButton": "Share file",
"notificationTitle": "ReVanced Manager is patching", "notificationTitle": "ReVanced Manager is patching",
"notificationText": "Tap to return to the installer", "notificationText": "Tap to return to the installer",
"exportApkButtonTooltip": "Export patched APK", "exportApkButtonTooltip": "Export patched APK",
"exportLogButtonTooltip": "Export log", "exportLogButtonTooltip": "Export log",
"installErrorDialogTitle": "Error", "screenshotDetected": "A screenshot has been detected. If you are trying to share the log, please share a text copy instead.\n\nCopy log to clipboard?",
"installErrorDialogText1": "Root install is not possible with the current patches selection.\nRepatch your app or choose non-root install.", "copiedToClipboard": "Copied log to clipboard",
"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..." "noExit": "Installer is still running, cannot exit..."
}, },
"settingsView": { "settingsView": {
@@ -173,13 +191,14 @@
"appearanceSectionTitle": "Appearance", "appearanceSectionTitle": "Appearance",
"teamSectionTitle": "Team", "teamSectionTitle": "Team",
"infoSectionTitle": "Info", "debugSectionTitle": "Debugging",
"advancedSectionTitle": "Advanced", "advancedSectionTitle": "Advanced",
"exportSectionTitle": "Import & export", "exportSectionTitle": "Import & export",
"logsSectionTitle": "Logs",
"darkThemeLabel": "Dark mode", "themeModeLabel": "App theme",
"darkThemeHint": "Welcome to the dark side", "systemThemeLabel": "System",
"lightThemeLabel": "Light",
"darkThemeLabel": "Dark",
"dynamicThemeLabel": "Material You", "dynamicThemeLabel": "Material You",
"dynamicThemeHint": "Enjoy an experience closer to your device", "dynamicThemeHint": "Enjoy an experience closer to your device",
@@ -188,39 +207,43 @@
"englishOption": "English", "englishOption": "English",
"sourcesLabel": "Sources", "sourcesLabel": "Sources",
"sourcesLabelHint": "Configure your custom sources", "sourcesLabelHint": "Configure the source of patches and integrations",
"sourcesIntegrationsLabel": "Integrations source", "sourcesIntegrationsLabel": "Integrations source",
"sourcesResetDialogTitle": "Reset", "sourcesResetDialogTitle": "Reset",
"sourcesResetDialogText": "Are you sure you want to reset custom sources to their default values?", "sourcesResetDialogText": "Are you sure you want to reset your sources to their default values?",
"apiURLResetDialogText": "Are you sure you want to reset API URL to its default value?", "apiURLResetDialogText": "Are you sure you want to reset your 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.", "sourcesUpdateNote": "Note: Patches will be updated to the latest version automatically.\n\nThis will reveal your IP address to the server.",
"apiURLLabel": "API URL", "apiURLLabel": "API URL",
"apiURLHint": "Configure your custom API URL", "apiURLHint": "Configure the URL of the API to use",
"selectApiURL": "API URL", "selectApiURL": "API URL",
"hostRepositoryLabel": "Repository API", "hostRepositoryLabel": "Repository API",
"orgPatchesLabel": "Patches organization", "orgPatchesLabel": "Patches organization",
"sourcesPatchesLabel": "Patches source", "sourcesPatchesLabel": "Patches source",
"orgIntegrationsLabel": "Integrations organization", "orgIntegrationsLabel": "Integrations organization",
"contributorsLabel": "Contributors", "contributorsLabel": "Contributors",
"contributorsHint": "A list of contributors of ReVanced", "contributorsHint": "A list of contributors of ReVanced",
"logsLabel": "Logs", "logsLabel": "Share logs",
"logsHint": "Share Manager's logs", "logsHint": "Share ReVanced Manager logs",
"enablePatchesSelectionLabel": "Enable changing selection", "enablePatchesSelectionLabel": "Allow changing patch selection",
"enablePatchesSelectionHint": "Enable changing the selection of patches.", "enablePatchesSelectionHint": "Do not prevent selecting or deselecting patches",
"enablePatchesSelectionWarningText": "Changing the default selection of patches may cause unexpected issues.\n\nEnable anyways?", "enablePatchesSelectionWarningText": "Changing the selection of patches may cause unexpected issues.\n\nEnable anyways?",
"disablePatchesSelectionWarningText": "You are about to disable changing the selection of patches.\nThe default selection of patches will be restored.\n\nDisable anyways?", "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", "autoUpdatePatchesLabel": "Auto update patches",
"autoUpdatePatchesHint": "Automatically update ReVanced Patches to the latest version", "autoUpdatePatchesHint": "Automatically update patches to the latest version",
"experimentalUniversalPatchesLabel": "Experimental universal patches support", "universalPatchesLabel": "Show universal patches",
"experimentalUniversalPatchesHint": "Display all applications to use with universal patches, loading list of apps may be slower", "universalPatchesHint": "Display all apps and universal patches (may slow down the app list)",
"experimentalPatchesLabel": "Experimental patches support",
"experimentalPatchesHint": "Enable usage of unsupported patches in any app version", "versionCompatibilityCheckLabel": "Version compatibility check",
"enabledExperimentalPatches": "Experimental patches support enabled", "versionCompatibilityCheckHint": "Prevent selecting patches that are not compatible with the selected app version",
"requireSuggestedAppVersionLabel": "Require suggested app version",
"requireSuggestedAppVersionHint": "Prevent selecting an app with a version that is not the suggested",
"requireSuggestedAppVersionDialogText": "Selecting an app that is not the suggested version may cause unexpected issues.\n\nDo you want to proceed anyways?",
"aboutLabel": "About", "aboutLabel": "About",
"snackbarMessage": "Copied to clipboard", "snackbarMessage": "Copied to clipboard",
@@ -230,69 +253,74 @@
"deleteTempDirHint": "Delete unused temporary files", "deleteTempDirHint": "Delete unused temporary files",
"deletedTempDir": "Temporary files deleted", "deletedTempDir": "Temporary files deleted",
"exportPatchesLabel": "Export patches selection", "exportPatchesLabel": "Export patch selection",
"exportPatchesHint": "Export patches selection to a JSON file", "exportPatchesHint": "Export patch selection to a JSON file",
"exportedPatches": "Patches selection exported", "exportedPatches": "Patch selection exported",
"noExportFileFound": "No patches selection to export", "noExportFileFound": "No patch selection to export",
"importPatchesLabel": "Import patches selection", "importPatchesLabel": "Import patch selection",
"importPatchesHint": "Import patches selection from a JSON file", "importPatchesHint": "Import patch selection from a JSON file",
"importedPatches": "Patches selection imported", "importedPatches": "Patch selection imported",
"resetStoredPatchesLabel": "Reset patches", "resetStoredPatchesLabel": "Reset patch selection",
"resetStoredPatchesHint": "Reset the stored patches selection", "resetStoredPatchesHint": "Reset the stored patch selection",
"resetStoredPatchesDialogTitle": "Reset patch selection?",
"resetStoredPatchesDialogText": "The default selection of patches will be restored.",
"resetStoredPatches": "Patch selection has been reset",
"resetStoredPatchesDialogTitle": "Reset patches selection?", "resetStoredOptionsLabel": "Reset patch options",
"resetStoredPatchesDialogText": "Resetting patches selection will remove all selected patches.", "resetStoredOptionsHint": "Reset all patch options",
"resetStoredPatches": "Patches selection has been reset", "resetStoredOptionsDialogTitle": "Reset patch options?",
"resetStoredOptionsDialogText": "Resetting patch options will remove all saved options.",
"resetStoredOptions": "Options have been reset",
"deleteLogsLabel": "Delete logs", "deleteLogsLabel": "Clear logs",
"deleteLogsHint": "Delete collected manager logs", "deleteLogsHint": "Delete collected ReVanced Manager logs",
"deletedLogs": "Logs deleted", "deletedLogs": "Logs deleted",
"regenerateKeystoreLabel": "Regenerate keystore", "regenerateKeystoreLabel": "Regenerate keystore",
"regenerateKeystoreHint": "Regenerate the keystore used to sign the app", "regenerateKeystoreHint": "Regenerate the keystore used to sign apps",
"regenerateKeystoreDialogTitle": "Regenerate keystore?", "regenerateKeystoreDialogTitle": "Regenerate keystore?",
"regenerateKeystoreDialogText": "Patched apps signed with the old keystore will no longer be able to update.", "regenerateKeystoreDialogText": "Patched apps signed with the old keystore will no longer be able to be updated.",
"regeneratedKeystore": "Keystore regenerated", "regeneratedKeystore": "Keystore regenerated",
"exportKeystoreLabel": "Export keystore", "exportKeystoreLabel": "Export keystore",
"exportKeystoreHint": "Export keystore used to sign apps", "exportKeystoreHint": "Export the keystore used to sign apps",
"exportedKeystore": "Keystore exported", "exportedKeystore": "Keystore exported",
"noKeystoreExportFileFound": "No keystore to export", "noKeystoreExportFileFound": "No keystore to export",
"importKeystoreLabel": "Import keystore", "importKeystoreLabel": "Import keystore",
"importKeystoreHint": "Import keystore used to sign apps", "importKeystoreHint": "Import a keystore used to sign apps",
"importedKeystore": "Keystore imported", "importedKeystore": "Keystore imported",
"selectKeystorePassword": "Keystore Password", "selectKeystorePassword": "Keystore password",
"selectKeystorePasswordHint": "Select keystore password used to sign the apk", "selectKeystorePasswordHint": "Select keystore password used to sign apps",
"jsonSelectorErrorMessage": "Unable to use selected JSON file", "jsonSelectorErrorMessage": "Unable to use selected JSON file",
"keystoreSelectorErrorMessage": "Unable to use selected KEYSTORE file" "keystoreSelectorErrorMessage": "Unable to use selected keystore file"
}, },
"appInfoView": { "appInfoView": {
"widgetTitle": "App info", "widgetTitle": "App info",
"openButton": "Open", "openButton": "Open",
"uninstallButton": "Uninstall", "uninstallButton": "Uninstall",
"unpatchButton": "Unpatch", "unmountButton": "Unmount",
"rootDialogTitle": "Error", "rootDialogTitle": "Error",
"unpatchDialogText": "Are you sure you want to unpatch this app?", "unmountDialogText": "Are you sure you want to unmount this app?",
"uninstallDialogText": "Are you sure you want to uninstall this app?",
"rootDialogText": "App was installed with superuser permissions, but currently ReVanced Manager has no permissions.\nPlease grant superuser permissions first.", "rootDialogText": "App was installed with superuser permissions, but currently ReVanced Manager has no permissions.\nPlease grant superuser permissions first.",
"packageNameLabel": "Package name", "packageNameLabel": "Package name",
"originalPackageNameLabel": "Original package name",
"installTypeLabel": "Installation type", "installTypeLabel": "Installation type",
"rootTypeLabel": "Root", "mountTypeLabel": "Mount",
"nonRootTypeLabel": "Non-root", "regularTypeLabel": "Regular",
"patchedDateLabel": "Patched date", "patchedDateLabel": "Patched date",
"appliedPatchesLabel": "Applied patches", "appliedPatchesLabel": "Applied patches",
"patchedDateHint": "{date} at {time}", "patchedDateHint": "{date} at {time}",
"appliedPatchesHint": "{quantity} applied patches", "appliedPatchesHint": "{quantity} applied patches",
"updateNotImplemented": "This feature has not been implemented yet" "updateNotImplemented": "This feature has not been implemented yet"
}, },
"contributorsView": { "contributorsView": {
@@ -302,5 +330,34 @@
"integrationsContributors": "Integrations contributors", "integrationsContributors": "Integrations contributors",
"cliContributors": "CLI contributors", "cliContributors": "CLI contributors",
"managerContributors": "Manager contributors" "managerContributors": "Manager contributors"
},
"installErrorDialog": {
"mount_version_mismatch": "Version mismatch",
"mount_no_root": "No root access",
"mount_missing_installation": "Installation not found",
"status_failure_blocked": "Installation blocked",
"install_failed_verification_failure": "Verification failed",
"status_failure_invalid": "Installation invalid",
"install_failed_version_downgrade": "Can't downgrade",
"status_failure_conflict": "Installation conflict",
"status_failure_storage": "Installation storage issue",
"status_failure_incompatible": "Installation incompatible",
"status_failure_timeout": "Installation timeout",
"status_unknown": "Installation failed",
"mount_version_mismatch_description": "The installation failed due to the installed app being a different version than the patched app.\n\nInstall the version of the app you are mounting and try again.",
"mount_no_root_description": "The installation failed due to root access not being granted.\n\nGrant root access to ReVanced Manager and try again.",
"mount_missing_installation_description": "The installation failed due to the unpatched app not being installed on this device in order to mount over it.\n\nInstall the unpatched app before mounting and try again.",
"status_failure_timeout_description": "The installation took too long to finish.\n\nWould you like to try again?",
"status_failure_storage_description": "The installation failed due to insufficient storage.\n\nFree up some space and try again.",
"status_failure_invalid_description": "The installation failed due to the patched app being invalid.\n\nUninstall the app and try again?",
"status_failure_incompatible_description": "The app is incompatible with this device.\n\nContact the developer of the app and ask for support.",
"status_failure_conflict_description": "The installation was prevented by an existing installation of the app.\n\nUninstall the installed app and try again?",
"status_failure_blocked_description": "The installation was blocked by {packageName}.\n\nAdjust your security settings and try again.",
"install_failed_verification_failure_description": "The installation failed due to a verification issue.\n\nAdjust your security settings and try again.",
"install_failed_version_downgrade_description": "The installation failed due to the patched app being a lower version than the installed app.\n\nUninstall the app and try again?",
"status_unknown_description": "The installation failed due to an unknown reason. Please try again."
} }
} }

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 800 800" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><g id="Logo"><g id="Ring"><circle id="Ring-Background" serif:id="Ring Background" cx="400" cy="400" r="400" style="fill:#1b1b1b;"/><path id="Ring1" serif:id="Ring" d="M400,0c220.766,0 400,179.234 400,400c-0,220.766 -179.234,400 -400,400c-220.766,-0 -400,-179.234 -400,-400c0,-220.766 179.234,-400 400,-400Zm-0,36c200.897,-0 364,163.103 364,364c0,200.897 -163.103,364 -364,364c-200.897,0 -364,-163.103 -364,-364c-0,-200.897 163.103,-364 364,-364Z" style="fill:url(#_Linear1);"/></g><g id="Shape"><path id="V-Shape" serif:id="V Shape" d="M538.74,269.872c1.481,-3.382 1.157,-7.283 -0.863,-10.373c-2.021,-3.091 -5.464,-4.954 -9.156,-4.954c-5.148,0 -10.435,0 -14.165,0c-3.1,0 -5.907,1.834 -7.153,4.672c-12.468,28.396 -78.273,178.273 -100.25,228.328c-1.246,2.838 -4.053,4.671 -7.154,4.671c-3.1,0 -5.907,-1.833 -7.153,-4.671c-21.977,-50.055 -87.782,-199.932 -100.25,-228.328c-1.246,-2.838 -4.053,-4.672 -7.153,-4.672c-3.73,0 -9.017,0 -14.164,0c-3.693,0 -7.135,1.863 -9.156,4.954c-2.02,3.09 -2.344,6.991 -0.863,10.373c23.557,53.766 101.872,232.519 117.871,269.034c1.743,3.979 5.674,6.549 10.018,6.549c6.293,-0 15.408,-0 21.701,-0c4.344,-0 8.275,-2.57 10.018,-6.549c15.999,-36.515 94.315,-215.268 117.872,-269.034Z" style="fill:#fff;"/><path id="Diamond" d="M408.119,395.312c-1.675,2.901 -4.77,4.688 -8.119,4.688c-3.349,-0 -6.444,-1.787 -8.119,-4.688c-16.997,-29.44 -56.156,-97.264 -73.153,-126.704c-1.675,-2.901 -1.675,-6.474 0,-9.375c1.675,-2.901 4.77,-4.688 8.119,-4.688c33.995,0 112.311,0 146.306,0c3.349,0 6.444,1.787 8.119,4.688c1.675,2.901 1.675,6.474 -0,9.375c-16.997,29.44 -56.156,97.264 -73.153,126.704Z" style="fill:url(#_Linear2);"/></g></g><defs><linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(4.89859e-14,800,-800,4.89859e-14,400.001,3.31681e-10)"><stop offset="0" style="stop-color:#f04e98;stop-opacity:1"/><stop offset="0.5" style="stop-color:#5f65d4;stop-opacity:1"/><stop offset="1" style="stop-color:#4e98f0;stop-opacity:1"/></linearGradient><linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.77155e-14,289.317,-282.535,1.73003e-14,400,254.545)"><stop offset="0" style="stop-color:#f04e98;stop-opacity:1"/><stop offset="0.5" style="stop-color:#5f65d4;stop-opacity:1"/><stop offset="1" style="stop-color:#4e98f0;stop-opacity:1"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

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

View File

@@ -7,16 +7,27 @@ The following pages will guide you through using ReVanced Manager to patch apps.
1. Navigate to the **Patcher** tab from the bottom navigation bar 1. Navigate to the **Patcher** tab from the bottom navigation bar
2. Tap on the **Select an app** card 2. Tap on the **Select an app** card
3. Choose an app to patch[^1] 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. > [!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].
> [!NOTE]
> Some patches have options that can or must be configured by tapping on ⚙️ icon next to the patch name.
>[!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 cancel patching or 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 option in the bottom left corner.
[^1]: Non-root users may be prompted to select an APK from storage, in which case you must 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. [^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. [^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.

View File

@@ -2,33 +2,46 @@
ReVanced Manager has settings that can be configured to your liking. ReVanced Manager has settings that can be configured to your liking.
## Essential settings ## 🎛️ Essential settings
- ### 🔗 API URL - ### 🪛 Allow changing patch selection
Specify the URL of the API to use. This is used to fetch ReVanced Patches and update ReVanced Manager. Allows the user to change the patch selection from the default selection.
- ### 🔍 Version compatibility check
Constrains patches to supported app versions. Disable this to patch any version of an app.
> [!WARNING]
> Disabling this may cause issues if the patches are not compatible with the app version.
- ### 🧑‍🔬 Show universal patches
Reveals patches which can be applied to any app.
> [!WARNING]
> These patches may not work on all apps.
- ### 🧬 Sources - ### 🧬 Sources
Override the API and change the source of ReVanced Patches. Override the API and download patches from a different source.
- ### 🧪 Experimental ReVanced Patches support - ### 🔗 API URL
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. API to use to fetch updates and patches from.
- ### 🧑‍🔬 Experimental universal support - ### 💾 Imports & Exports
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. You can import, export or reset the following settings:
- ### 🔑 Export, import or delete keystore - 🔑 Keystore
- 📄 Patch selection
- ⚙️ Patch options
Manage the keystore used to sign patched apps. > [!NOTE]
> This is particularly useful if you want to backup or reset your settings.
- ### 📄 Export, import or reset ReVanced Patches selection - ### ❓ About
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. View information about your device and ReVanced Manager. This includes the version of ReVanced Manager and supported architectures of your device.
@@ -36,4 +49,4 @@ ReVanced Manager has settings that can be configured to your liking.
The next page will bring you back to the usage page. The next page will bring you back to the usage page.
Continue: [🛠️ Usage](2_usage.md) Continue: [🛠️ Usage](2_usage.md)

View File

@@ -5,21 +5,19 @@ In case you encounter any issues while using ReVanced Manager, please refer to t
- 💉 Patching fails with an error - 💉 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. 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. - 🚫 App not installed as package conflicts with an existing package
An existing installation of the app you're trying to patch conflicts with the patched app (i.e., signature mismatch or downgrade). Uninstall the existing app before installing the patched app.
- ❗️ Error code `135`, `139` or `1` when patching the app - ❗️ Error code `135`, `139` or `1` when patching the app
Your device is not supported. Refer to the [Prerequisites](0_prerequisites.md) page for supported devices. You may be trying to patch a split APK[^1]. This may not work under certain circumstances. In such a case, patch a full APK.
Your device may otherwise be unsupported. Please look at the [Prerequisites](0_prerequisites.md) page for supported devices.
Alternatively, you can use [ReVanced CLI](https://github.com/revanced/revanced-cli) to patch the app. 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 - 🚨 Patched app crashes on launch
Select the **Default** button when choosing patches. Select the **Default** button when choosing patches.
@@ -29,3 +27,5 @@ In case you encounter any issues while using ReVanced Manager, please refer to t
The next page will teach you how to build ReVanced Manager from source. The next page will teach you how to build ReVanced Manager from source.
Continue: [🔨 Building from source](4_building.md) Continue: [🔨 Building from source](4_building.md)
[^1]: https://developer.android.com/guide/app-bundle/app-bundle-format

View File

@@ -2,38 +2,31 @@
This page will guide you through building ReVanced Manager 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) 1\. Setup the Flutter environment for your [platform](https://docs.flutter.dev/get-started/install)
2. Clone the repository 2\. Clone the repository
```sh ```sh
git clone https://github.com/revanced/revanced-manager.git && cd revanced-manager git clone https://github.com/revanced/revanced-manager.git && cd revanced-manager
``` ```
3\. Get dependencies
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 ```sh
flutter pub get flutter pub get
``` ```
6. Delete conflicting outputs 4\. Delete conflicting outputs
> [!TIP]
> Must be run every time you sync your local repository with the remote repository.
```sh ```sh
flutter packages pub run build_runner build --delete-conflicting-outputs dart run build_runner build --delete-conflicting-outputs
``` ```
> **Note**: Must be run every time you sync your local repository with the remote repository.
7. Build the APK
5\. Build the APK
```sh ```sh
flutter build apk flutter build apk

BIN
fonts/custom-icons.ttf Normal file

Binary file not shown.

View File

@@ -1,3 +1,4 @@
import 'package:revanced_manager/services/download_manager.dart';
import 'package:revanced_manager/services/github_api.dart'; import 'package:revanced_manager/services/github_api.dart';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/patcher_api.dart'; import 'package:revanced_manager/services/patcher_api.dart';
@@ -9,6 +10,8 @@ import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
import 'package:revanced_manager/ui/views/installer/installer_view.dart'; import 'package:revanced_manager/ui/views/installer/installer_view.dart';
import 'package:revanced_manager/ui/views/navigation/navigation_view.dart'; import 'package:revanced_manager/ui/views/navigation/navigation_view.dart';
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart'; import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
import 'package:revanced_manager/ui/views/patch_options/patch_options_view.dart';
import 'package:revanced_manager/ui/views/patch_options/patch_options_viewmodel.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_view.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_view.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_view.dart'; import 'package:revanced_manager/ui/views/patches_selector/patches_selector_view.dart';
@@ -23,6 +26,7 @@ import 'package:stacked_services/stacked_services.dart';
MaterialRoute(page: PatcherView), MaterialRoute(page: PatcherView),
MaterialRoute(page: AppSelectorView), MaterialRoute(page: AppSelectorView),
MaterialRoute(page: PatchesSelectorView), MaterialRoute(page: PatchesSelectorView),
MaterialRoute(page: PatchOptionsView),
MaterialRoute(page: InstallerView), MaterialRoute(page: InstallerView),
MaterialRoute(page: SettingsView), MaterialRoute(page: SettingsView),
MaterialRoute(page: ContributorsView), MaterialRoute(page: ContributorsView),
@@ -32,11 +36,13 @@ import 'package:stacked_services/stacked_services.dart';
LazySingleton(classType: NavigationViewModel), LazySingleton(classType: NavigationViewModel),
LazySingleton(classType: HomeViewModel), LazySingleton(classType: HomeViewModel),
LazySingleton(classType: PatcherViewModel), LazySingleton(classType: PatcherViewModel),
LazySingleton(classType: PatchOptionsViewModel),
LazySingleton(classType: NavigationService), LazySingleton(classType: NavigationService),
LazySingleton(classType: ManagerAPI), LazySingleton(classType: ManagerAPI),
LazySingleton(classType: PatcherAPI), LazySingleton(classType: PatcherAPI),
LazySingleton(classType: RevancedAPI), LazySingleton(classType: RevancedAPI),
LazySingleton(classType: GithubAPI), LazySingleton(classType: GithubAPI),
LazySingleton(classType: DownloadManager),
LazySingleton(classType: Toast), LazySingleton(classType: Toast),
], ],
) )

View File

@@ -4,33 +4,41 @@ import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/download_manager.dart';
import 'package:revanced_manager/services/github_api.dart'; import 'package:revanced_manager/services/github_api.dart';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/revanced_api.dart'; import 'package:revanced_manager/services/revanced_api.dart';
import 'package:revanced_manager/services/root_api.dart';
import 'package:revanced_manager/ui/theme/dynamic_theme_builder.dart'; import 'package:revanced_manager/ui/theme/dynamic_theme_builder.dart';
import 'package:revanced_manager/ui/views/navigation/navigation_view.dart'; import 'package:revanced_manager/ui/views/navigation/navigation_view.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:stacked_themes/stacked_themes.dart';
import 'package:timezone/data/latest.dart' as tz; import 'package:timezone/data/latest.dart' as tz;
late SharedPreferences prefs; late SharedPreferences prefs;
Future main() async { Future main() async {
await ThemeManager.initialise();
await setupLocator(); await setupLocator();
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await locator<ManagerAPI>().initialize(); await locator<ManagerAPI>().initialize();
await locator<DownloadManager>().initialize();
final String apiUrl = locator<ManagerAPI>().getApiUrl(); final String apiUrl = locator<ManagerAPI>().getApiUrl();
await locator<RevancedAPI>().initialize(apiUrl); await locator<RevancedAPI>().initialize(apiUrl);
final String repoUrl = locator<ManagerAPI>().getRepoUrl(); final String repoUrl = locator<ManagerAPI>().getRepoUrl();
locator<GithubAPI>().initialize(repoUrl); locator<GithubAPI>().initialize(repoUrl);
tz.initializeTimeZones(); tz.initializeTimeZones();
// TODO(aAbed): remove in the future, keep it for now during migration.
final rootAPI = RootAPI();
if (await rootAPI.hasRootPermissions()) {
await rootAPI.removeOrphanedFiles();
}
prefs = await SharedPreferences.getInstance(); prefs = await SharedPreferences.getInstance();
runApp(const MyApp()); runApp(const MyApp());
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key); const MyApp({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@@ -1,5 +1,4 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:revanced_manager/utils/string.dart';
part 'patch.g.dart'; part 'patch.g.dart';
@@ -9,26 +8,32 @@ class Patch {
required this.name, required this.name,
required this.description, required this.description,
required this.excluded, required this.excluded,
required this.dependencies,
required this.compatiblePackages, required this.compatiblePackages,
required this.options,
}); });
factory Patch.fromJson(Map<String, dynamic> json) => _$PatchFromJson(json); factory Patch.fromJson(Map<String, dynamic> json) {
_migrateV16ToV17(json);
return _$PatchFromJson(json);
}
static void _migrateV16ToV17(Map<String, dynamic> json) {
if (json['options'] == null) {
json['options'] = [];
}
}
final String name; final String name;
final String description; final String? description;
final bool excluded; final bool excluded;
final List<String> dependencies;
final List<Package> compatiblePackages; final List<Package> compatiblePackages;
final List<Option> options;
Map<String, dynamic> toJson() => _$PatchToJson(this); Map<String, dynamic> toJson() => _$PatchToJson(this);
String getSimpleName() { String getSimpleName() {
return name return name;
.replaceAll('-', ' ')
.split('-')
.join(' ')
.toTitleCase()
.replaceFirst('Microg', 'MicroG');
} }
} }
@@ -41,8 +46,50 @@ class Package {
factory Package.fromJson(Map<String, dynamic> json) => factory Package.fromJson(Map<String, dynamic> json) =>
_$PackageFromJson(json); _$PackageFromJson(json);
final String name; final String name;
final List<String> versions; final List<String> versions;
Map toJson() => _$PackageToJson(this); Map toJson() => _$PackageToJson(this);
} }
@JsonSerializable()
class Option {
Option({
required this.key,
required this.title,
required this.description,
required this.value,
required this.values,
required this.required,
required this.valueType,
});
factory Option.fromJson(Map<String, dynamic> json) {
_migrateV17ToV19(json);
return _$OptionFromJson(json);
}
static void _migrateV17ToV19(Map<String, dynamic> json) {
if (json['valueType'] == null) {
final type = json['optionClassType'];
if (type is String) {
json['valueType'] =
type.replaceAll('PatchOption', '').replaceAll('List', 'Array');
json['optionClassType'] = null;
}
}
}
final String key;
final String title;
final String description;
final dynamic value;
final Map<String, dynamic>? values;
final bool required;
final String valueType;
Map toJson() => _$OptionToJson(this);
}

View File

@@ -9,23 +9,19 @@ class PatchedApplication {
PatchedApplication({ PatchedApplication({
required this.name, required this.name,
required this.packageName, required this.packageName,
required this.originalPackageName,
required this.version, required this.version,
required this.apkFilePath, required this.apkFilePath,
required this.icon, required this.icon,
required this.patchDate, required this.patchDate,
this.isRooted = false, this.isRooted = false,
this.isFromStorage = false, this.isFromStorage = false,
this.hasUpdates = false,
this.appliedPatches = const [], this.appliedPatches = const [],
this.changelog = const [],
}); });
factory PatchedApplication.fromJson(Map<String, dynamic> json) => factory PatchedApplication.fromJson(Map<String, dynamic> json) =>
_$PatchedApplicationFromJson(json); _$PatchedApplicationFromJson(json);
String name; String name;
String packageName; String packageName;
String originalPackageName;
String version; String version;
final String apkFilePath; final String apkFilePath;
@JsonKey( @JsonKey(
@@ -36,9 +32,7 @@ class PatchedApplication {
DateTime patchDate; DateTime patchDate;
bool isRooted; bool isRooted;
bool isFromStorage; bool isFromStorage;
bool hasUpdates;
List<String> appliedPatches; List<String> appliedPatches;
List<String> changelog;
Map<String, dynamic> toJson() => _$PatchedApplicationToJson(this); Map<String, dynamic> toJson() => _$PatchedApplicationToJson(this);

View File

@@ -0,0 +1,75 @@
import 'package:dio/dio.dart';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_cache_manager/file.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/services/manager_api.dart';
@lazySingleton
class DownloadManager {
final ManagerAPI _managerAPI = locator<ManagerAPI>();
late final String _userAgent;
final _cacheOptions = CacheOptions(
store: MemCacheStore(),
maxStale: const Duration(days: 1),
priority: CachePriority.high,
);
Future<void> initialize() async {
_userAgent =
'ReVanced-Manager/${await _managerAPI.getCurrentManagerVersion()}';
}
Dio initDio(String url) {
var dio = Dio();
try {
dio = Dio(
BaseOptions(
baseUrl: url,
headers: {
'User-Agent': _userAgent,
},
),
);
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
return dio;
}
Future<void> clearAllCache() async {
try {
await _cacheOptions.store!.clean();
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
Future<File> getSingleFile(String url) async {
return DefaultCacheManager().getSingleFile(
url,
headers: {
'User-Agent': _userAgent,
},
);
}
Stream<FileResponse> getFileStream(String url) {
return DefaultCacheManager().getFileStream(
url,
withProgress: true,
headers: {
'User-Agent': _userAgent,
},
);
}
}

View File

@@ -1,61 +1,24 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/patch.dart'; import 'package:revanced_manager/services/download_manager.dart';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.dart';
@lazySingleton @lazySingleton
class GithubAPI { class GithubAPI {
late Dio _dio = Dio(); late final Dio _dio;
late final ManagerAPI _managerAPI = locator<ManagerAPI>(); late final ManagerAPI _managerAPI = locator<ManagerAPI>();
late final DownloadManager _downloadManager = locator<DownloadManager>();
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',
'com.twitter.android': 'twitter',
'com.reddit.frontpage': 'reddit',
'com.zhiliaoapp.musically': 'tiktok',
'de.dwd.warnapp': 'warnwetter',
'com.garzotto.pflotsh.ecmwf_a': 'ecmwf',
'com.spotify.music': 'spotify',
};
Future<void> initialize(String repoUrl) async { Future<void> initialize(String repoUrl) async {
try { _dio = _downloadManager.initDio(repoUrl);
_dio = Dio(
BaseOptions(
baseUrl: repoUrl,
),
);
_dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
} }
Future<void> clearAllCache() async { Future<void> clearAllCache() async {
try { await _downloadManager.clearAllCache();
await _cacheOptions.store!.clean();
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
} }
Future<Map<String, dynamic>?> getLatestRelease( Future<Map<String, dynamic>?> getLatestRelease(
@@ -117,7 +80,7 @@ class GithubAPI {
final Map<String, dynamic> releases = response.data[0]; final Map<String, dynamic> releases = response.data[0];
int updates = 0; int updates = 0;
final String currentVersion = final String currentVersion =
await ManagerAPI().getCurrentManagerVersion(); await _managerAPI.getCurrentManagerVersion();
while (response.data[updates]['tag_name'] != 'v$currentVersion') { while (response.data[updates]['tag_name'] != 'v$currentVersion') {
updates++; updates++;
} }
@@ -142,38 +105,6 @@ class GithubAPI {
} }
} }
Future<List<String>> getCommits(
String packageName,
String repoName,
DateTime since,
) async {
final String path =
'src/main/kotlin/app/revanced/patches/${repoAppPath[packageName]}';
try {
final response = await _dio.get(
'/repos/$repoName/commits',
queryParameters: {
'path': path,
'since': since.toIso8601String(),
},
);
final List<dynamic> commits = response.data;
return commits
.map(
(commit) => commit['commit']['message'].split('\n')[0] +
' - ' +
commit['commit']['author']['name'] +
'\n' as String,
)
.toList();
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
return [];
}
Future<File?> getLatestReleaseFile( Future<File?> getLatestReleaseFile(
String extension, String extension,
String repoName, String repoName,
@@ -186,7 +117,7 @@ class GithubAPI {
(asset) => (asset['name'] as String).endsWith(extension), (asset) => (asset['name'] as String).endsWith(extension),
); );
if (asset != null) { if (asset != null) {
return await DefaultCacheManager().getSingleFile( return await _downloadManager.getSingleFile(
asset['browser_download_url'], asset['browser_download_url'],
); );
} }
@@ -207,7 +138,7 @@ class GithubAPI {
) async { ) async {
try { try {
if (url.isNotEmpty) { if (url.isNotEmpty) {
return await DefaultCacheManager().getSingleFile( return await _downloadManager.getSingleFile(
url, url,
); );
} }
@@ -222,12 +153,10 @@ class GithubAPI {
final String downloadUrl = asset['browser_download_url']; final String downloadUrl = asset['browser_download_url'];
if (extension == '.apk') { if (extension == '.apk') {
_managerAPI.setIntegrationsDownloadURL(downloadUrl); _managerAPI.setIntegrationsDownloadURL(downloadUrl);
} else if (extension == '.json') {
_managerAPI.setPatchesDownloadURL(downloadUrl, false);
} else { } else {
_managerAPI.setPatchesDownloadURL(downloadUrl, true); _managerAPI.setPatchesDownloadURL(downloadUrl);
} }
return await DefaultCacheManager().getSingleFile( return await _downloadManager.getSingleFile(
downloadUrl, downloadUrl,
); );
} }
@@ -239,30 +168,4 @@ class GithubAPI {
} }
return null; return null;
} }
Future<List<Patch>> getPatches(
String repoName,
String version,
String url,
) async {
List<Patch> patches = [];
try {
final File? f = await getPatchesReleaseFile(
'.json',
repoName,
version,
url,
);
if (f != null) {
final List<dynamic> list = jsonDecode(f.readAsStringSync());
patches = list.map((patch) => Patch.fromJson(patch)).toList();
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
return patches;
}
} }

View File

@@ -1,6 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:device_apps/device_apps.dart'; import 'package:device_apps/device_apps.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart'; import 'package:flutter_i18n/widgets/I18nText.dart';
@@ -11,9 +12,11 @@ import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/patch.dart'; import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/models/patched_application.dart'; import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/github_api.dart'; import 'package:revanced_manager/services/github_api.dart';
import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/services/revanced_api.dart'; import 'package:revanced_manager/services/revanced_api.dart';
import 'package:revanced_manager/services/root_api.dart'; import 'package:revanced_manager/services/root_api.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_checkbox_list_tile.dart';
import 'package:revanced_manager/utils/check_for_supported_patch.dart'; import 'package:revanced_manager/utils/check_for_supported_patch.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:timeago/timeago.dart'; import 'package:timeago/timeago.dart';
@@ -22,11 +25,19 @@ import 'package:timeago/timeago.dart';
class ManagerAPI { class ManagerAPI {
final RevancedAPI _revancedAPI = locator<RevancedAPI>(); final RevancedAPI _revancedAPI = locator<RevancedAPI>();
final GithubAPI _githubAPI = locator<GithubAPI>(); final GithubAPI _githubAPI = locator<GithubAPI>();
final Toast _toast = locator<Toast>();
final RootAPI _rootAPI = RootAPI(); final RootAPI _rootAPI = RootAPI();
final String patcherRepo = 'revanced-patcher'; final String patcherRepo = 'revanced-patcher';
final String cliRepo = 'revanced-cli'; final String cliRepo = 'revanced-cli';
late SharedPreferences _prefs; late SharedPreferences _prefs;
List<Patch> patches = [];
List<Option> modifiedOptions = [];
List<Option> options = [];
Patch? selectedPatch;
BuildContext? ctx;
bool isRooted = false; bool isRooted = false;
bool suggestedAppVersionSelected = true;
bool isDynamicThemeAvailable = false;
String storedPatchesFile = '/selected-patches.json'; String storedPatchesFile = '/selected-patches.json';
String keystoreFile = String keystoreFile =
'/sdcard/Android/data/app.revanced.manager.flutter/files/revanced-manager.keystore'; '/sdcard/Android/data/app.revanced.manager.flutter/files/revanced-manager.keystore';
@@ -40,19 +51,38 @@ class ManagerAPI {
String defaultManagerRepo = 'revanced/revanced-manager'; String defaultManagerRepo = 'revanced/revanced-manager';
String? patchesVersion = ''; String? patchesVersion = '';
String? integrationsVersion = ''; String? integrationsVersion = '';
bool isDefaultPatchesRepo() { bool isDefaultPatchesRepo() {
return getPatchesRepo() == 'revanced/revanced-patches'; return getPatchesRepo().toLowerCase() == defaultPatchesRepo;
} }
bool isDefaultIntegrationsRepo() { bool isDefaultIntegrationsRepo() {
return getIntegrationsRepo() == 'revanced/revanced-integrations'; return getIntegrationsRepo().toLowerCase() ==
defaultIntegrationsRepo;
} }
Future<void> initialize() async { Future<void> initialize() async {
_prefs = await SharedPreferences.getInstance(); _prefs = await SharedPreferences.getInstance();
isRooted = await _rootAPI.isRooted(); isRooted = await _rootAPI.isRooted();
isDynamicThemeAvailable =
(await getSdkVersion()) >= 31; // ANDROID_12_SDK_VERSION = 31
storedPatchesFile = storedPatchesFile =
(await getApplicationDocumentsDirectory()).path + storedPatchesFile; (await getApplicationDocumentsDirectory()).path + storedPatchesFile;
// Migrate to new API URL if not done yet as the old one is sunset.
final bool hasMigrated = _prefs.getBool('migratedToNewApiUrl') ?? false;
if (!hasMigrated) {
final String apiUrl = getApiUrl().toLowerCase();
if (apiUrl.contains('releases.revanced.app')) {
await setApiUrl(''); // Reset to default.
_prefs.setBool('migratedToNewApiUrl', true);
}
}
}
Future<int> getSdkVersion() async {
final AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo;
return info.version.sdkInt;
} }
String getApiUrl() { String getApiUrl() {
@@ -63,9 +93,9 @@ class ManagerAPI {
if (url.isEmpty || url == ' ') { if (url.isEmpty || url == ' ') {
url = defaultApiUrl; url = defaultApiUrl;
} }
await _revancedAPI.initialize(url);
await _revancedAPI.clearAllCache(); await _revancedAPI.clearAllCache();
await _prefs.setString('apiUrl', url); await _prefs.setString('apiUrl', url);
_toast.showBottom('settingsView.restartAppForChanges');
} }
String getRepoUrl() { String getRepoUrl() {
@@ -79,12 +109,12 @@ class ManagerAPI {
await _prefs.setString('repoUrl', url); await _prefs.setString('repoUrl', url);
} }
String getPatchesDownloadURL(bool bundle) { String getPatchesDownloadURL() {
return _prefs.getString('patchesDownloadURL-$bundle') ?? ''; return _prefs.getString('patchesDownloadURL') ?? '';
} }
Future<void> setPatchesDownloadURL(String value, bool bundle) async { Future<void> setPatchesDownloadURL(String value) async {
await _prefs.setString('patchesDownloadURL-$bundle', value); await _prefs.setString('patchesDownloadURL', value);
} }
String getPatchesRepo() { String getPatchesRepo() {
@@ -111,17 +141,6 @@ class ManagerAPI {
} }
bool isPatchesChangeEnabled() { bool isPatchesChangeEnabled() {
if (getPatchedApps().isNotEmpty && !isChangingToggleModified()) {
for (final apps in getPatchedApps()) {
if (getSavedPatches(apps.originalPackageName)
.indexWhere((patch) => patch.excluded) !=
-1) {
setPatchesChangeWarning(false);
setPatchesChangeEnabled(true);
break;
}
}
}
return _prefs.getBool('patchesChangeEnabled') ?? false; return _prefs.getBool('patchesChangeEnabled') ?? false;
} }
@@ -189,6 +208,29 @@ class ManagerAPI {
await _prefs.setStringList('usedPatches-$packageName', patchesJson); await _prefs.setStringList('usedPatches-$packageName', patchesJson);
} }
Option? getPatchOption(String packageName, String patchName, String key) {
final String? optionJson =
_prefs.getString('patchOption-$packageName-$patchName-$key');
if (optionJson != null) {
final Option option = Option.fromJson(jsonDecode(optionJson));
return option;
} else {
return null;
}
}
void setPatchOption(Option option, String patchName, String packageName) {
final String optionJson = jsonEncode(option.toJson());
_prefs.setString(
'patchOption-$packageName-$patchName-${option.key}',
optionJson,
);
}
void clearPatchOption(String packageName, String patchName, String key) {
_prefs.remove('patchOption-$packageName-$patchName-$key');
}
String getIntegrationsRepo() { String getIntegrationsRepo() {
return _prefs.getString('integrationsRepo') ?? defaultIntegrationsRepo; return _prefs.getString('integrationsRepo') ?? defaultIntegrationsRepo;
} }
@@ -208,12 +250,12 @@ class ManagerAPI {
await _prefs.setBool('useDynamicTheme', value); await _prefs.setBool('useDynamicTheme', value);
} }
bool getUseDarkTheme() { int getThemeMode() {
return _prefs.getBool('useDarkTheme') ?? false; return _prefs.getInt('themeMode') ?? 2;
} }
Future<void> setUseDarkTheme(bool value) async { Future<void> setThemeMode(int value) async {
await _prefs.setBool('useDarkTheme', value); await _prefs.setInt('themeMode', value);
} }
bool areUniversalPatchesEnabled() { bool areUniversalPatchesEnabled() {
@@ -224,12 +266,20 @@ class ManagerAPI {
await _prefs.setBool('universalPatchesEnabled', value); await _prefs.setBool('universalPatchesEnabled', value);
} }
bool areExperimentalPatchesEnabled() { bool isVersionCompatibilityCheckEnabled() {
return _prefs.getBool('experimentalPatchesEnabled') ?? false; return _prefs.getBool('versionCompatibilityCheckEnabled') ?? true;
} }
Future<void> enableExperimentalPatchesStatus(bool value) async { Future<void> enableVersionCompatibilityCheckStatus(bool value) async {
await _prefs.setBool('experimentalPatchesEnabled', value); await _prefs.setBool('versionCompatibilityCheckEnabled', value);
}
bool isRequireSuggestedAppVersionEnabled() {
return _prefs.getBool('requireSuggestedAppVersionEnabled') ?? true;
}
Future<void> enableRequireSuggestedAppVersionStatus(bool value) async {
await _prefs.setBool('requireSuggestedAppVersionEnabled', value);
} }
Future<void> setKeystorePassword(String password) async { Future<void> setKeystorePassword(String password) async {
@@ -311,28 +361,45 @@ class ManagerAPI {
} }
Future<List<Patch>> getPatches() async { Future<List<Patch>> getPatches() async {
try { if (patches.isNotEmpty) {
final String repoName = getPatchesRepo(); return patches;
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 [];
} }
final File? patchBundleFile = await downloadPatches();
final Directory appCache = await getTemporaryDirectory();
Directory('${appCache.path}/cache').createSync();
final Directory workDir =
Directory('${appCache.path}/cache').createTempSync('tmp-');
final Directory cacheDir = Directory('${workDir.path}/cache');
cacheDir.createSync();
if (patchBundleFile != null) {
try {
final String patchesJson = await PatcherAPI.patcherChannel.invokeMethod(
'getPatches',
{
'patchBundleFilePath': patchBundleFile.path,
'cacheDirPath': cacheDir.path,
},
);
final List<dynamic> patchesJsonList = jsonDecode(patchesJson);
patches = patchesJsonList
.map((patchJson) => Patch.fromJson(patchJson))
.toList();
return patches;
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
}
return List.empty();
} }
Future<File?> downloadPatches() async { Future<File?> downloadPatches() async {
try { try {
final String repoName = getPatchesRepo(); final String repoName = getPatchesRepo();
final String currentVersion = await getCurrentPatchesVersion(); final String currentVersion = await getCurrentPatchesVersion();
final String url = getPatchesDownloadURL(true); final String url = getPatchesDownloadURL();
return await _githubAPI.getPatchesReleaseFile( return await _githubAPI.getPatchesReleaseFile(
'.jar', '.jar',
repoName, repoName,
@@ -458,8 +525,7 @@ class ManagerAPI {
Future<void> setCurrentPatchesVersion(String version) async { Future<void> setCurrentPatchesVersion(String version) async {
await _prefs.setString('patchesVersion', version); await _prefs.setString('patchesVersion', version);
await setPatchesDownloadURL('', false); await setPatchesDownloadURL('');
await setPatchesDownloadURL('', true);
await downloadPatches(); await downloadPatches();
} }
@@ -495,62 +561,33 @@ class ManagerAPI {
return toRemove; return toRemove;
} }
Future<List<PatchedApplication>> getUnsavedApps( Future<List<PatchedApplication>> getMountedApps() async {
List<PatchedApplication> patchedApps, final List<PatchedApplication> mountedApps = [];
) async {
final List<PatchedApplication> unsavedApps = [];
final bool hasRootPermissions = await _rootAPI.hasRootPermissions(); final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
if (hasRootPermissions) { if (hasRootPermissions) {
final List<String> installedApps = await _rootAPI.getInstalledApps(); final List<String> installedApps = await _rootAPI.getInstalledApps();
for (final String packageName in installedApps) { for (final String packageName in installedApps) {
if (!patchedApps.any((app) => app.packageName == packageName)) {
final ApplicationWithIcon? application = await DeviceApps.getApp(
packageName,
true,
) as ApplicationWithIcon?;
if (application != null) {
unsavedApps.add(
PatchedApplication(
name: application.appName,
packageName: application.packageName,
originalPackageName: application.packageName,
version: application.versionName!,
apkFilePath: application.apkFilePath,
icon: application.icon,
patchDate: DateTime.now(),
isRooted: true,
),
);
}
}
}
}
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)) {
final ApplicationWithIcon? application = await DeviceApps.getApp( final ApplicationWithIcon? application = await DeviceApps.getApp(
app.packageName, packageName,
true, true,
) as ApplicationWithIcon?; ) as ApplicationWithIcon?;
if (application != null) { if (application != null) {
unsavedApps.add( mountedApps.add(
PatchedApplication( PatchedApplication(
name: application.appName, name: application.appName,
packageName: application.packageName, packageName: application.packageName,
originalPackageName: application.packageName,
version: application.versionName!, version: application.versionName!,
apkFilePath: application.apkFilePath, apkFilePath: application.apkFilePath,
icon: application.icon, icon: application.icon,
patchDate: DateTime.now(), patchDate: DateTime.now(),
isRooted: true,
), ),
); );
} }
} }
} }
return unsavedApps;
return mountedApps;
} }
Future<void> showPatchesChangeWarningDialog(BuildContext context) { Future<void> showPatchesChangeWarningDialog(BuildContext context) {
@@ -559,10 +596,9 @@ class ManagerAPI {
return showDialog( return showDialog(
barrierDismissible: false, barrierDismissible: false,
context: context, context: context,
builder: (context) => WillPopScope( builder: (context) => PopScope(
onWillPop: () async => false, canPop: false,
child: AlertDialog( child: AlertDialog(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
title: I18nText('warning'), title: I18nText('warning'),
content: ValueListenableBuilder( content: ValueListenableBuilder(
valueListenable: noShow, valueListenable: noShow,
@@ -582,7 +618,7 @@ class ManagerAPI {
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
CheckboxListTile( HapticCheckboxListTile(
value: value, value: value,
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: I18nText( title: I18nText(
@@ -597,12 +633,12 @@ class ManagerAPI {
}, },
), ),
actions: [ actions: [
CustomMaterialButton( FilledButton(
label: I18nText('okButton'),
onPressed: () { onPressed: () {
setPatchesChangeWarning(noShow.value); setPatchesChangeWarning(noShow.value);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: I18nText('okButton'),
), ),
], ],
), ),
@@ -610,36 +646,22 @@ class ManagerAPI {
); );
} }
Future<void> reAssessSavedApps() async { Future<void> rePatchedSavedApps() async {
final List<PatchedApplication> patchedApps = getPatchedApps(); final List<PatchedApplication> patchedApps = getPatchedApps();
final List<PatchedApplication> unsavedApps =
await getUnsavedApps(patchedApps); // Remove apps that are not installed anymore.
patchedApps.addAll(unsavedApps);
final List<PatchedApplication> toRemove = final List<PatchedApplication> toRemove =
await getAppsToRemove(patchedApps); await getAppsToRemove(patchedApps);
patchedApps.removeWhere((a) => toRemove.contains(a)); patchedApps.removeWhere((a) => toRemove.contains(a));
for (final PatchedApplication app in patchedApps) {
app.hasUpdates = // Determine all apps that are installed by mounting.
await hasAppUpdates(app.originalPackageName, app.patchDate); final List<PatchedApplication> mountedApps = await getMountedApps();
app.changelog = mountedApps.removeWhere(
await getAppChangelog(app.originalPackageName, app.patchDate); (app) => patchedApps
if (!app.hasUpdates) { .any((patchedApp) => patchedApp.packageName == app.packageName),
final String? currentInstalledVersion = );
(await DeviceApps.getApp(app.packageName))?.versionName; patchedApps.addAll(mountedApps);
if (currentInstalledVersion != null) {
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;
}
}
}
}
await setPatchedApps(patchedApps); await setPatchedApps(patchedApps);
} }
@@ -656,37 +678,6 @@ class ManagerAPI {
return !existsNonRoot; return !existsNonRoot;
} }
Future<bool> hasAppUpdates(
String packageName,
DateTime patchDate,
) async {
final List<String> commits = await _githubAPI.getCommits(
packageName,
getPatchesRepo(),
patchDate,
);
return commits.isNotEmpty;
}
Future<List<String>> getAppChangelog(
String packageName,
DateTime patchDate,
) async {
List<String> newCommits = await _githubAPI.getCommits(
packageName,
getPatchesRepo(),
patchDate,
);
if (newCommits.isEmpty) {
newCommits = await _githubAPI.getCommits(
packageName,
getPatchesRepo(),
patchDate,
);
}
return newCommits;
}
Future<bool> isSplitApk(PatchedApplication patchedApp) async { Future<bool> isSplitApk(PatchedApplication patchedApp) async {
Application? app; Application? app;
if (patchedApp.isFromStorage) { if (patchedApp.isFromStorage) {
@@ -715,7 +706,7 @@ class ManagerAPI {
Future<List<String>> getDefaultPatches() async { Future<List<String>> getDefaultPatches() async {
final List<Patch> patches = await getPatches(); final List<Patch> patches = await getPatches();
final List<String> defaultPatches = []; final List<String> defaultPatches = [];
if (areExperimentalPatchesEnabled() == false) { if (isVersionCompatibilityCheckEnabled() == true) {
defaultPatches.addAll( defaultPatches.addAll(
patches patches
.where( .where(
@@ -752,8 +743,18 @@ class ManagerAPI {
return jsonDecode(string); return jsonDecode(string);
} }
void resetAllOptions() {
_prefs.getKeys().where((key) => key.startsWith('patchOption-')).forEach(
(key) {
_prefs.remove(key);
},
);
}
Future<void> resetLastSelectedPatches() async { Future<void> resetLastSelectedPatches() async {
final File selectedPatchesFile = File(storedPatchesFile); final File selectedPatchesFile = File(storedPatchesFile);
selectedPatchesFile.deleteSync(); if (selectedPatchesFile.existsSync()) {
selectedPatchesFile.deleteSync();
}
} }
} }

View File

@@ -1,19 +1,20 @@
import 'dart:io'; import 'dart:io';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:cr_file_saver/file_saver.dart';
import 'package:device_apps/device_apps.dart'; import 'package:device_apps/device_apps.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:install_plugin/install_plugin.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/patch.dart'; import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/models/patched_application.dart'; import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/root_api.dart'; import 'package:revanced_manager/services/root_api.dart';
import 'package:share_extend/share_extend.dart'; import 'package:share_plus/share_plus.dart';
@lazySingleton @lazySingleton
class PatcherAPI { class PatcherAPI {
@@ -25,12 +26,13 @@ class PatcherAPI {
late Directory _tmpDir; late Directory _tmpDir;
late File _keyStoreFile; late File _keyStoreFile;
List<Patch> _patches = []; List<Patch> _patches = [];
List<Patch> _universalPatches = [];
List<String> _compatiblePackages = [];
Map filteredPatches = <String, List<Patch>>{}; Map filteredPatches = <String, List<Patch>>{};
File? _outFile; File? outFile;
Future<void> initialize() async { Future<void> initialize() async {
await _loadPatches(); await loadPatches();
await _managerAPI.downloadPatches();
await _managerAPI.downloadIntegrations(); await _managerAPI.downloadIntegrations();
final Directory appCache = await getTemporaryDirectory(); final Directory appCache = await getTemporaryDirectory();
_dataDir = await getExternalStorageDirectory() ?? appCache; _dataDir = await getExternalStorageDirectory() ?? appCache;
@@ -45,7 +47,23 @@ class PatcherAPI {
} }
} }
Future<void> _loadPatches() async { List<String> getCompatiblePackages() {
final List<String> compatiblePackages = [];
for (final Patch patch in _patches) {
for (final Package package in patch.compatiblePackages) {
if (!compatiblePackages.contains(package.name)) {
compatiblePackages.add(package.name);
}
}
}
return compatiblePackages;
}
List<Patch> getUniversalPatches() {
return _patches.where((patch) => patch.compatiblePackages.isEmpty).toList();
}
Future<void> loadPatches() async {
try { try {
if (_patches.isEmpty) { if (_patches.isEmpty) {
_patches = await _managerAPI.getPatches(); _patches = await _managerAPI.getPatches();
@@ -56,6 +74,9 @@ class PatcherAPI {
} }
_patches = List.empty(); _patches = List.empty();
} }
_compatiblePackages = getCompatiblePackages();
_universalPatches = getUniversalPatches();
} }
Future<List<ApplicationWithIcon>> getFilteredInstalledApps( Future<List<ApplicationWithIcon>> getFilteredInstalledApps(
@@ -63,48 +84,42 @@ class PatcherAPI {
) async { ) async {
final List<ApplicationWithIcon> filteredApps = []; final List<ApplicationWithIcon> filteredApps = [];
final bool allAppsIncluded = final bool allAppsIncluded =
_patches.any((patch) => patch.compatiblePackages.isEmpty) && _universalPatches.isNotEmpty && showUniversalPatches;
showUniversalPatches;
if (allAppsIncluded) { if (allAppsIncluded) {
final allPackages = await DeviceApps.getInstalledApplications( final appList = await DeviceApps.getInstalledApplications(
includeAppIcons: true, includeAppIcons: true,
onlyAppsWithLaunchIntent: true, onlyAppsWithLaunchIntent: true,
); );
for (final pkg in allPackages) {
if (!filteredApps.any((app) => app.packageName == pkg.packageName)) { for (final app in appList) {
final appInfo = await DeviceApps.getApp( filteredApps.add(app as ApplicationWithIcon);
pkg.packageName,
true,
) as ApplicationWithIcon?;
if (appInfo != null) {
filteredApps.add(appInfo);
}
}
} }
} }
for (final Patch patch in _patches) { for (final packageName in _compatiblePackages) {
for (final Package package in patch.compatiblePackages) { try {
try { if (!filteredApps.any((app) => app.packageName == packageName)) {
if (!filteredApps.any((app) => app.packageName == package.name)) { final ApplicationWithIcon? app = await DeviceApps.getApp(
final ApplicationWithIcon? app = await DeviceApps.getApp( packageName,
package.name, true,
true, ) as ApplicationWithIcon?;
) as ApplicationWithIcon?; if (app != null) {
if (app != null) { filteredApps.add(app);
filteredApps.add(app);
}
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
} }
} }
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
} }
} }
return filteredApps; return filteredApps;
} }
List<Patch> getFilteredPatches(String packageName) { List<Patch> getFilteredPatches(String packageName) {
if (!_compatiblePackages.contains(packageName)) {
return _universalPatches;
}
final List<Patch> patches = _patches final List<Patch> patches = _patches
.where( .where(
(patch) => (patch) =>
@@ -132,69 +147,49 @@ class PatcherAPI {
.toList(); .toList();
} }
Future<bool> needsResourcePatching(
List<Patch> selectedPatches,
) async {
return selectedPatches.any(
(patch) => patch.dependencies.any(
(dep) => dep.contains('resource-'),
),
);
}
Future<bool> needsSettingsPatch(List<Patch> selectedPatches) async {
return selectedPatches.any(
(patch) => patch.dependencies.any(
(dep) => dep.contains('settings'),
),
);
}
Future<void> runPatcher( Future<void> runPatcher(
String packageName, String packageName,
String apkFilePath, String apkFilePath,
List<Patch> selectedPatches, List<Patch> selectedPatches,
) async { ) async {
final bool includeSettings = await needsSettingsPatch(selectedPatches); final File? integrationsFile = await _managerAPI.downloadIntegrations();
if (includeSettings) { final Map<String, Map<String, dynamic>> options = {};
try { for (final patch in selectedPatches) {
final Patch? settingsPatch = _patches.firstWhereOrNull( if (patch.options.isNotEmpty) {
(patch) => final Map<String, dynamic> patchOptions = {};
patch.name.contains('settings') && for (final option in patch.options) {
patch.compatiblePackages.any((pack) => pack.name == packageName), final patchOption =
); _managerAPI.getPatchOption(packageName, patch.name, option.key);
if (settingsPatch != null) { if (patchOption != null) {
selectedPatches.add(settingsPatch); patchOptions[patchOption.key] = patchOption.value;
} }
} on Exception catch (e) {
if (kDebugMode) {
print(e);
} }
options[patch.name] = patchOptions;
} }
} }
final File? patchBundleFile = await _managerAPI.downloadPatches();
final File? integrationsFile = await _managerAPI.downloadIntegrations(); if (integrationsFile != null) {
if (patchBundleFile != null) {
_dataDir.createSync(); _dataDir.createSync();
_tmpDir.createSync(); _tmpDir.createSync();
final Directory workDir = _tmpDir.createTempSync('tmp-'); final Directory workDir = _tmpDir.createTempSync('tmp-');
final File inputFile = File('${workDir.path}/base.apk'); final File inputFile = File('${workDir.path}/base.apk');
final File patchedFile = File('${workDir.path}/patched.apk'); final File patchedFile = File('${workDir.path}/patched.apk');
_outFile = File('${workDir.path}/out.apk'); outFile = File('${workDir.path}/out.apk');
final Directory cacheDir = Directory('${workDir.path}/cache'); final Directory cacheDir = Directory('${workDir.path}/cache');
cacheDir.createSync(); cacheDir.createSync();
final String originalFilePath = apkFilePath; final String originalFilePath = apkFilePath;
try { try {
await patcherChannel.invokeMethod( await patcherChannel.invokeMethod(
'runPatcher', 'runPatcher',
{ {
'patchBundleFilePath': patchBundleFile.path,
'originalFilePath': originalFilePath, 'originalFilePath': originalFilePath,
'inputFilePath': inputFile.path, 'inputFilePath': inputFile.path,
'patchedFilePath': patchedFile.path, 'patchedFilePath': patchedFile.path,
'outFilePath': _outFile!.path, 'outFilePath': outFile!.path,
'integrationsPath': integrationsFile!.path, 'integrationsPath': integrationsFile.path,
'selectedPatches': selectedPatches.map((p) => p.name).toList(), 'selectedPatches': selectedPatches.map((p) => p.name).toList(),
'options': options,
'cacheDirPath': cacheDir.path, 'cacheDirPath': cacheDir.path,
'keyStoreFilePath': _keyStoreFile.path, 'keyStoreFilePath': _keyStoreFile.path,
'keystorePassword': _managerAPI.getKeystorePassword(), 'keystorePassword': _managerAPI.getKeystorePassword(),
@@ -218,40 +213,165 @@ class PatcherAPI {
} }
} }
Future<bool> installPatchedFile(PatchedApplication patchedApp) async { Future<int> installPatchedFile(
if (_outFile != null) { BuildContext context,
PatchedApplication patchedApp,
) async {
if (outFile != null) {
_managerAPI.ctx = context;
try { try {
if (patchedApp.isRooted) { if (patchedApp.isRooted) {
final bool hasRootPermissions = await _rootAPI.hasRootPermissions(); final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
if (hasRootPermissions) { final packageVersion = await DeviceApps.getApp(patchedApp.packageName)
return _rootAPI.installApp( .then((app) => app?.versionName);
if (!hasRootPermissions) {
installErrorDialog(1);
} else if (packageVersion == null) {
installErrorDialog(1.2);
} else if (packageVersion == patchedApp.version) {
return await _rootAPI.install(
patchedApp.packageName, patchedApp.packageName,
patchedApp.apkFilePath, patchedApp.apkFilePath,
_outFile!.path, outFile!.path,
); )
? 0
: 1;
} else {
installErrorDialog(1.1);
} }
} else { } else {
final install = await InstallPlugin.installApk(_outFile!.path); if (await _rootAPI.hasRootPermissions()) {
return install['isSuccess']; await _rootAPI.uninstall(patchedApp.packageName);
}
if (context.mounted) {
return await installApk(
context,
outFile!.path,
);
}
} }
} on Exception catch (e) { } on Exception catch (e) {
if (kDebugMode) { if (kDebugMode) {
print(e); print(e);
} }
return false;
} }
} }
return false; return 1;
}
Future<int> installApk(
BuildContext context,
String apkPath,
) async {
try {
final status = await patcherChannel.invokeMethod('installApk', {
'apkPath': apkPath,
});
final int statusCode = status['status'];
final String message = status['message'];
final bool hasExtra =
message.contains('INSTALL_FAILED_VERIFICATION_FAILURE') ||
message.contains('INSTALL_FAILED_VERSION_DOWNGRADE');
if (statusCode == 0 || (statusCode == 3 && !hasExtra)) {
return statusCode;
} else {
_managerAPI.ctx = context;
return await installErrorDialog(
statusCode,
status,
hasExtra,
);
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return 3;
}
}
Future<int> installErrorDialog(
num statusCode, [
status,
bool hasExtra = false,
]) async {
final String statusValue = InstallStatus.byCode(
hasExtra ? double.parse('$statusCode.1') : statusCode,
);
bool cleanInstall = false;
final bool isFixable = statusCode == 4 || statusCode == 5;
await showDialog(
context: _managerAPI.ctx!,
builder: (context) => AlertDialog(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
title: I18nText('installErrorDialog.$statusValue'),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
I18nText(
'installErrorDialog.${statusValue}_description',
translationParams: statusCode == 2
? {
'packageName': status['otherPackageName'],
}
: null,
),
],
),
actions: (status == null)
? <Widget>[
FilledButton(
onPressed: () async {
Navigator.pop(context);
},
child: I18nText('okButton'),
),
]
: <Widget>[
if (!isFixable)
FilledButton(
onPressed: () {
Navigator.pop(context);
},
child: I18nText('cancelButton'),
)
else
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: I18nText('cancelButton'),
),
if (isFixable)
FilledButton(
onPressed: () async {
final int response = await patcherChannel.invokeMethod(
'uninstallApp',
{'packageName': status['packageName']},
);
if (response == 0 && context.mounted) {
cleanInstall = true;
Navigator.pop(context);
}
},
child: I18nText('okButton'),
),
],
),
);
return cleanInstall ? 10 : 1;
} }
void exportPatchedFile(String appName, String version) { void exportPatchedFile(String appName, String version) {
try { try {
if (_outFile != null) { if (outFile != null) {
final String newName = _getFileName(appName, version); final String newName = _getFileName(appName, version);
CRFileSaver.saveFileWithDialog( FlutterFileDialog.saveFile(
SaveFileDialogParams( params: SaveFileDialogParams(
sourceFilePath: _outFile!.path, sourceFilePath: outFile!.path,
destinationFileName: newName, fileName: newName,
mimeTypesFilter: ['application/vnd.android.package-archive'],
), ),
); );
} }
@@ -264,13 +384,13 @@ class PatcherAPI {
void sharePatchedFile(String appName, String version) { void sharePatchedFile(String appName, String version) {
try { try {
if (_outFile != null) { if (outFile != null) {
final String newName = _getFileName(appName, version); final String newName = _getFileName(appName, version);
final int lastSeparator = _outFile!.path.lastIndexOf('/'); final int lastSeparator = outFile!.path.lastIndexOf('/');
final String newPath = final String newPath =
_outFile!.path.substring(0, lastSeparator + 1) + newName; outFile!.path.substring(0, lastSeparator + 1) + newName;
final File shareFile = _outFile!.copySync(newPath); final File shareFile = outFile!.copySync(newPath);
ShareExtend.share(shareFile.path, 'file'); Share.shareXFiles([XFile(shareFile.path)]);
} }
} on Exception catch (e) { } on Exception catch (e) {
if (kDebugMode) { if (kDebugMode) {
@@ -280,8 +400,10 @@ class PatcherAPI {
} }
String _getFileName(String appName, String version) { String _getFileName(String appName, String version) {
final String patchVersion = _managerAPI.patchesVersion!;
final String prefix = appName.toLowerCase().replaceAll(' ', '-'); final String prefix = appName.toLowerCase().replaceAll(' ', '-');
final String newName = '$prefix-revanced_v$version.apk'; final String newName =
'$prefix-revanced_v$version-patches_$patchVersion.apk';
return newName; return newName;
} }
@@ -295,13 +417,13 @@ class PatcherAPI {
.replaceAll(':', '') .replaceAll(':', '')
.replaceAll('T', '') .replaceAll('T', '')
.replaceAll('.', ''); .replaceAll('.', '');
final String fileName = 'revanced-manager_patcher_$dateTime.log'; final String fileName = 'revanced-manager_patcher_$dateTime.txt';
final File log = File('${logDir.path}/$fileName'); final File log = File('${logDir.path}/$fileName');
log.writeAsStringSync(logs); log.writeAsStringSync(logs);
CRFileSaver.saveFileWithDialog( FlutterFileDialog.saveFile(
SaveFileDialogParams( params: SaveFileDialogParams(
sourceFilePath: log.path, sourceFilePath: log.path,
destinationFileName: fileName, fileName: fileName,
), ),
); );
} }
@@ -334,3 +456,60 @@ class PatcherAPI {
return ''; return '';
} }
} }
enum InstallStatus {
mountNoRoot(1),
mountVersionMismatch(1.1),
mountMissingInstallation(1.2),
statusFailureBlocked(2),
installFailedVerificationFailure(3.1),
statusFailureInvalid(4),
installFailedVersionDowngrade(4.1),
statusFailureConflict(5),
statusFailureStorage(6),
statusFailureIncompatible(7),
statusFailureTimeout(8);
const InstallStatus(this.statusCode);
final double statusCode;
static String byCode(num code) {
try {
return InstallStatus.values
.firstWhere((flag) => flag.statusCode == code)
.status;
} catch (e) {
return 'status_unknown';
}
}
}
extension InstallStatusExtension on InstallStatus {
String get status {
switch (this) {
case InstallStatus.mountNoRoot:
return 'mount_no_root';
case InstallStatus.mountVersionMismatch:
return 'mount_version_mismatch';
case InstallStatus.mountMissingInstallation:
return 'mount_missing_installation';
case InstallStatus.statusFailureBlocked:
return 'status_failure_blocked';
case InstallStatus.installFailedVerificationFailure:
return 'install_failed_verification_failure';
case InstallStatus.statusFailureInvalid:
return 'status_failure_invalid';
case InstallStatus.installFailedVersionDowngrade:
return 'install_failed_version_downgrade';
case InstallStatus.statusFailureConflict:
return 'status_failure_conflict';
case InstallStatus.statusFailureStorage:
return 'status_failure_storage';
case InstallStatus.statusFailureIncompatible:
return 'status_failure_incompatible';
case InstallStatus.statusFailureTimeout:
return 'status_failure_timeout';
}
}
}

View File

@@ -3,46 +3,27 @@ import 'dart:io';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/download_manager.dart';
import 'package:synchronized/synchronized.dart';
import 'package:timeago/timeago.dart'; import 'package:timeago/timeago.dart';
@lazySingleton @lazySingleton
class RevancedAPI { class RevancedAPI {
late Dio _dio = Dio(); late final Dio _dio;
late final DownloadManager _downloadManager = locator<DownloadManager>();
final _cacheOptions = CacheOptions( final Lock getToolsLock = Lock();
store: MemCacheStore(),
maxStale: const Duration(days: 1),
priority: CachePriority.high,
);
Future<void> initialize(String apiUrl) async { Future<void> initialize(String repoUrl) async {
try { _dio = _downloadManager.initDio(repoUrl);
_dio = Dio(
BaseOptions(
baseUrl: apiUrl,
),
);
_dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
} }
Future<void> clearAllCache() async { Future<void> clearAllCache() async {
try { await _downloadManager.clearAllCache();
await _cacheOptions.store!.clean();
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
}
} }
Future<Map<String, List<dynamic>>> getContributors() async { Future<Map<String, List<dynamic>>> getContributors() async {
@@ -51,7 +32,7 @@ class RevancedAPI {
final response = await _dio.get('/contributors'); final response = await _dio.get('/contributors');
final List<dynamic> repositories = response.data['repositories']; final List<dynamic> repositories = response.data['repositories'];
for (final Map<String, dynamic> repo in repositories) { for (final Map<String, dynamic> repo in repositories) {
final String name = repo['name']; final String name = repo['name'].toLowerCase();
contributors[name] = repo['contributors']; contributors[name] = repo['contributors'];
} }
} on Exception catch (e) { } on Exception catch (e) {
@@ -66,21 +47,23 @@ class RevancedAPI {
Future<Map<String, dynamic>?> _getLatestRelease( Future<Map<String, dynamic>?> _getLatestRelease(
String extension, String extension,
String repoName, String repoName,
) async { ) {
try { return getToolsLock.synchronized(() async {
final response = await _dio.get('/tools'); try {
final List<dynamic> tools = response.data['tools']; final response = await _dio.get('/tools');
return tools.firstWhereOrNull( final List<dynamic> tools = response.data['tools'];
(t) => return tools.firstWhereOrNull(
t['repository'] == repoName && (t) =>
(t['name'] as String).endsWith(extension), (t['repository'] as String).toLowerCase() == repoName.toLowerCase() &&
); (t['name'] as String).endsWith(extension),
} on Exception catch (e) { );
if (kDebugMode) { } on Exception catch (e) {
print(e); if (kDebugMode) {
print(e);
}
return null;
} }
return null; });
}
} }
Future<String?> getLatestReleaseVersion( Future<String?> getLatestReleaseVersion(
@@ -115,7 +98,7 @@ class RevancedAPI {
); );
if (release != null) { if (release != null) {
final String url = release['browser_download_url']; final String url = release['browser_download_url'];
return await DefaultCacheManager().getSingleFile(url); return await _downloadManager.getSingleFile(url);
} }
} on Exception catch (e) { } on Exception catch (e) {
if (kDebugMode) { if (kDebugMode) {
@@ -147,9 +130,8 @@ class RevancedAPI {
'revanced/revanced-manager', 'revanced/revanced-manager',
); );
File? outputFile; File? outputFile;
await for (final result in DefaultCacheManager().getFileStream( await for (final result in _downloadManager.getFileStream(
release!['browser_download_url'] as String, release!['browser_download_url'] as String,
withProgress: true,
)) { )) {
if (result is DownloadProgress) { if (result is DownloadProgress) {
final totalSize = result.totalSize ?? 10000000; final totalSize = result.totalSize ?? 10000000;

View File

@@ -2,10 +2,10 @@ import 'package:flutter/foundation.dart';
import 'package:root/root.dart'; import 'package:root/root.dart';
class RootAPI { class RootAPI {
// TODO(ponces): remove in the future, keep it for now during migration. // TODO(aAbed): 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 _postFsDataDirPath = '/data/adb/post-fs-data.d';
final String _revancedDirPath = '/data/adb/revanced';
final String _serviceDDirPath = '/data/adb/service.d'; final String _serviceDDirPath = '/data/adb/service.d';
Future<bool> isRooted() async { Future<bool> isRooted() async {
@@ -43,21 +43,22 @@ class RootAPI {
String filePath, String filePath,
) async { ) async {
try { try {
final StringBuffer commands = StringBuffer();
if (permissions.isNotEmpty) { if (permissions.isNotEmpty) {
await Root.exec( commands.writeln('chmod $permissions $filePath');
cmd: 'chmod $permissions "$filePath"',
);
} }
if (ownerGroup.isNotEmpty) { if (ownerGroup.isNotEmpty) {
await Root.exec( commands.writeln('chown $ownerGroup $filePath');
cmd: 'chown $ownerGroup "$filePath"',
);
} }
if (seLinux.isNotEmpty) { if (seLinux.isNotEmpty) {
await Root.exec( commands.writeln('chcon $seLinux $filePath');
cmd: 'chcon $seLinux "$filePath"',
);
} }
await Root.exec(
cmd: commands.toString(),
);
} on Exception catch (e) { } on Exception catch (e) {
if (kDebugMode) { if (kDebugMode) {
print(e); print(e);
@@ -75,18 +76,7 @@ class RootAPI {
Future<List<String>> getInstalledApps() async { Future<List<String>> getInstalledApps() async {
final List<String> apps = List.empty(growable: true); final List<String> apps = List.empty(growable: true);
try { try {
String? res = await Root.exec( final String? res = await Root.exec(cmd: 'ls $_revancedDirPath');
cmd: 'ls "$_revancedDirPath"',
);
if (res != null) {
final List<String> list = res.split('\n');
list.removeWhere((pack) => pack.isEmpty);
apps.addAll(list.map((pack) => pack.trim()).toList());
}
// TODO(ponces): remove in the future, keep it for now during migration.
res = await Root.exec(
cmd: 'ls "$_revancedOldDirPath"',
);
if (res != null) { if (res != null) {
final List<String> list = res.split('\n'); final List<String> list = res.split('\n');
list.removeWhere((pack) => pack.isEmpty); list.removeWhere((pack) => pack.isEmpty);
@@ -100,49 +90,45 @@ class RootAPI {
return apps; return apps;
} }
Future<void> deleteApp(String packageName, String originalFilePath) async { Future<void> uninstall(String packageName) async {
await Root.exec( await Root.exec(
cmd: 'am force-stop "$packageName"', cmd: '''
); grep $packageName /proc/mounts | while read -r line; do echo \$line | cut -d " " -f 2 | sed "s/apk.*/apk/" | xargs -r umount -l; done
await Root.exec( rm -rf $_revancedDirPath/$packageName $_serviceDDirPath/$packageName.sh
cmd: 'su -mm -c "umount -l $originalFilePath"', ''',
);
// TODO(ponces): remove in the future, keep it for now during migration.
await Root.exec(
cmd: 'rm -rf "$_revancedOldDirPath/$packageName"',
);
await Root.exec(
cmd: 'rm -rf "$_revancedDirPath/$packageName"',
);
await Root.exec(
cmd: 'rm -rf "$_serviceDDirPath/$packageName.sh"',
);
await Root.exec(
cmd: 'rm -rf "$_postFsDataDirPath/$packageName.sh"',
); );
} }
Future<bool> installApp( // TODO(aAbed): remove in the future, keep it for now during migration.
Future<void> removeOrphanedFiles() async {
await Root.exec(
cmd: '''
find $_revancedDirPath -type f -name original.apk -delete
for file in "$_serviceDDirPath"/*; do
filename=\$(basename "\$file")
if [ -f "$_postFsDataDirPath/\$filename" ]; then
rm "$_postFsDataDirPath/\$filename"
fi
done
''',
);
}
Future<bool> install(
String packageName, String packageName,
String originalFilePath, String originalFilePath,
String patchedFilePath, String patchedFilePath,
) async { ) async {
try { try {
await deleteApp(packageName, originalFilePath);
await Root.exec(
cmd: 'mkdir -p "$_revancedDirPath/$packageName"',
);
await setPermissions( await setPermissions(
'0755', '0755',
'shell:shell', 'shell:shell',
'', '',
'$_revancedDirPath/$packageName', '$_revancedDirPath/$packageName',
); );
await saveOriginalFilePath(packageName, originalFilePath); await installPatchedApk(packageName, patchedFilePath);
await installServiceDScript(packageName); await installServiceDScript(packageName);
await installPostFsDataScript(packageName); await runMountScript(packageName);
await installApk(packageName, patchedFilePath);
await mountApk(packageName, originalFilePath);
return true; return true;
} on Exception catch (e) { } on Exception catch (e) {
if (kDebugMode) { if (kDebugMode) {
@@ -154,38 +140,48 @@ class RootAPI {
Future<void> installServiceDScript(String packageName) async { Future<void> installServiceDScript(String packageName) async {
await Root.exec( await Root.exec(
cmd: 'mkdir -p "$_serviceDDirPath"', cmd: 'mkdir -p $_serviceDDirPath',
); );
final String content = '#!/system/bin/sh\n' final String mountScript = '''
'while [ "\$(getprop sys.boot_completed | tr -d \'"\'"\'\\\\r\'"\'"\')" != "1" ]; do sleep 3; done\n' #!/system/bin/sh
'base_path=$_revancedDirPath/$packageName/base.apk\n' # Mount using Magisk mirror, if available.
'stock_path=\$(pm path $packageName | grep base | sed \'"\'"\'s/package://g\'"\'"\')\n' MAGISKTMP="\$( magisk --path )" || MAGISKTMP=/sbin
r'[ ! -z $stock_path ] && mount -o bind $base_path $stock_path'; MIRROR="\$MAGISKTMP/.magisk/mirror"
if [ ! -f \$MIRROR ]; then
MIRROR=""
fi
until [ "\$(getprop sys.boot_completed)" = 1 ]; do sleep 3; done
until [ -d "/sdcard/Android" ]; do sleep 1; done
# Unmount any existing installation to prevent multiple unnecessary mounts.
grep $packageName /proc/mounts | while read -r line; do echo \$line | cut -d " " -f 2 | sed "s/apk.*/apk/" | xargs -r umount -l; done
base_path=$_revancedDirPath/$packageName/base.apk
stock_path=\$(pm path $packageName | grep base | sed "s/package://g" )
chcon u:object_r:apk_data_file:s0 \$base_path
mount -o bind \$MIRROR\$base_path \$stock_path
# Kill the app to force it to restart the mounted APK in case it is already running
am force-stop $packageName
'''
.trimMultilineString();
final String scriptFilePath = '$_serviceDDirPath/$packageName.sh'; final String scriptFilePath = '$_serviceDDirPath/$packageName.sh';
await Root.exec( await Root.exec(
cmd: 'echo \'$content\' > "$scriptFilePath"', cmd: 'echo \'$mountScript\' > "$scriptFilePath"',
); );
await setPermissions('0744', '', '', scriptFilePath); await setPermissions('0744', '', '', scriptFilePath);
} }
Future<void> installPostFsDataScript(String packageName) async { Future<void> installPatchedApk(
await Root.exec( String packageName, String patchedFilePath,) async {
cmd: 'mkdir -p "$_postFsDataDirPath"',
);
final String content = '#!/system/bin/sh\n'
'stock_path=\$(pm path $packageName | grep base | sed \'"\'"\'s/package://g\'"\'"\')\n'
r'[ ! -z $stock_path ] && umount -l $stock_path';
final String scriptFilePath = '$_postFsDataDirPath/$packageName.sh';
await Root.exec(
cmd: 'echo \'$content\' > "$scriptFilePath"',
);
await setPermissions('0744', '', '', scriptFilePath);
}
Future<void> installApk(String packageName, String patchedFilePath) async {
final String newPatchedFilePath = '$_revancedDirPath/$packageName/base.apk'; final String newPatchedFilePath = '$_revancedDirPath/$packageName/base.apk';
await Root.exec( await Root.exec(
cmd: 'cp "$patchedFilePath" "$newPatchedFilePath"', cmd: '''
mkdir -p $_revancedDirPath/$packageName
cp "$patchedFilePath" $newPatchedFilePath
''',
); );
await setPermissions( await setPermissions(
'0644', '0644',
@@ -195,50 +191,10 @@ class RootAPI {
); );
} }
Future<void> mountApk(String packageName, String originalFilePath) async { Future<void> runMountScript(
final String newPatchedFilePath = '$_revancedDirPath/$packageName/base.apk';
await Root.exec(
cmd: 'am force-stop "$packageName"',
);
await Root.exec(
cmd: 'su -mm -c "umount -l $originalFilePath"',
);
await Root.exec(
cmd: 'su -mm -c "mount -o bind $newPatchedFilePath $originalFilePath"',
);
}
Future<bool> isMounted(String packageName) async {
final String? res = await Root.exec(
cmd: 'cat /proc/mounts | grep $packageName',
);
return res != null && res.isNotEmpty;
}
Future<void> saveOriginalFilePath(
String packageName, String packageName,
String originalFilePath,
) async { ) async {
final String originalRootPath = await Root.exec(cmd: '.$_serviceDDirPath/$packageName.sh');
'$_revancedDirPath/$packageName/original.apk';
await Root.exec(
cmd: 'mkdir -p "$_revancedDirPath/$packageName"',
);
await setPermissions(
'0755',
'shell:shell',
'',
'$_revancedDirPath/$packageName',
);
await Root.exec(
cmd: 'cp "$originalFilePath" "$originalRootPath"',
);
await setPermissions(
'0644',
'shell:shell',
'u:object_r:apk_data_file:s0',
originalFilePath,
);
} }
Future<bool> fileExists(String path) async { Future<bool> fileExists(String path) async {
@@ -255,3 +211,10 @@ class RootAPI {
} }
} }
} }
// Remove leading spaces manually until
// https://github.com/dart-lang/language/issues/559 is closed
extension StringExtension on String {
String trimMultilineString() =>
split('\n').map((line) => line.trim()).join('\n').trim();
}

View File

@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
var lightCustomColorScheme = ColorScheme.fromSeed( var lightCustomColorScheme = ColorScheme.fromSeed(
seedColor: Colors.blue, seedColor: Colors.purple,
primary: const Color(0xff1B73E8), primary: const Color(0xFF4C51C0),
); );
var lightCustomTheme = ThemeData( var lightCustomTheme = ThemeData(
@@ -21,10 +21,10 @@ var lightCustomTheme = ThemeData(
); );
var darkCustomColorScheme = ColorScheme.fromSeed( var darkCustomColorScheme = ColorScheme.fromSeed(
seedColor: Colors.blue, seedColor: Colors.purple,
brightness: Brightness.dark, brightness: Brightness.dark,
primary: const Color(0xffA5CAFF), primary: const Color(0xFFBFC1FF),
surface: const Color(0xff1B1A1D), surface: const Color(0xFF131316),
); );
var darkCustomTheme = ThemeData( var darkCustomTheme = ThemeData(
@@ -38,7 +38,7 @@ var darkCustomTheme = ThemeData(
), ),
), ),
), ),
canvasColor: const Color(0xff1B1A1D), canvasColor: const Color(0xFF131316),
scaffoldBackgroundColor: const Color(0xff1B1A1D), scaffoldBackgroundColor: const Color(0xFF131316),
textTheme: GoogleFonts.robotoTextTheme(ThemeData.dark().textTheme), textTheme: GoogleFonts.robotoTextTheme(ThemeData.dark().textTheme),
); );

View File

@@ -1,22 +1,57 @@
import 'dart:ui';
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:dynamic_themes/dynamic_themes.dart'; import 'package:dynamic_themes/dynamic_themes.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/app/app.router.dart'; import 'package:revanced_manager/app/app.router.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/theme.dart'; import 'package:revanced_manager/theme.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
class DynamicThemeBuilder extends StatelessWidget { class DynamicThemeBuilder extends StatefulWidget {
const DynamicThemeBuilder({ const DynamicThemeBuilder({
Key? key, super.key,
required this.title, required this.title,
required this.home, required this.home,
required this.localizationsDelegates, required this.localizationsDelegates,
}) : super(key: key); });
final String title; final String title;
final Widget home; final Widget home;
final Iterable<LocalizationsDelegate> localizationsDelegates; final Iterable<LocalizationsDelegate> localizationsDelegates;
@override
State<DynamicThemeBuilder> createState() => _DynamicThemeBuilderState();
}
class _DynamicThemeBuilderState extends State<DynamicThemeBuilder>
with WidgetsBindingObserver {
Brightness brightness = PlatformDispatcher.instance.platformBrightness;
final ManagerAPI _managerAPI = locator<ManagerAPI>();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangePlatformBrightness() {
setState(() {
brightness = PlatformDispatcher.instance.platformBrightness;
});
if (_managerAPI.getThemeMode() < 2) {
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
systemNavigationBarIconBrightness: brightness == Brightness.light
? Brightness.dark
: Brightness.light,
),
);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DynamicColorBuilder( return DynamicColorBuilder(
@@ -50,24 +85,39 @@ class DynamicThemeBuilder extends StatelessWidget {
return DynamicTheme( return DynamicTheme(
themeCollection: ThemeCollection( themeCollection: ThemeCollection(
themes: { themes: {
0: lightCustomTheme, 0: brightness == Brightness.light
1: darkCustomTheme, ? lightCustomTheme
2: lightDynamicTheme, : darkCustomTheme,
3: darkDynamicTheme, 1: brightness == Brightness.light
? lightDynamicTheme
: darkDynamicTheme,
2: lightCustomTheme,
3: lightDynamicTheme,
4: darkCustomTheme,
5: darkDynamicTheme,
}, },
fallbackTheme: lightCustomTheme, fallbackTheme: PlatformDispatcher.instance.platformBrightness ==
Brightness.light
? lightCustomTheme
: darkCustomTheme,
), ),
builder: (context, theme) => MaterialApp( builder: (context, theme) => MaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
title: title, title: widget.title,
navigatorKey: StackedService.navigatorKey, navigatorKey: StackedService.navigatorKey,
onGenerateRoute: StackedRouter().onGenerateRoute, onGenerateRoute: StackedRouter().onGenerateRoute,
theme: theme, theme: theme,
home: home, home: widget.home,
localizationsDelegates: localizationsDelegates, localizationsDelegates: widget.localizationsDelegates,
), ),
); );
}, },
); );
} }
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
} }

View File

@@ -4,11 +4,12 @@ import 'package:revanced_manager/ui/views/app_selector/app_selector_viewmodel.da
import 'package:revanced_manager/ui/widgets/appSelectorView/app_skeleton_loader.dart'; import 'package:revanced_manager/ui/widgets/appSelectorView/app_skeleton_loader.dart';
import 'package:revanced_manager/ui/widgets/appSelectorView/installed_app_item.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/appSelectorView/not_installed_app_item.dart';
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_floating_action_button_extended.dart';
import 'package:revanced_manager/ui/widgets/shared/search_bar.dart'; import 'package:revanced_manager/ui/widgets/shared/search_bar.dart';
import 'package:stacked/stacked.dart' hide SkeletonLoader; import 'package:stacked/stacked.dart' hide SkeletonLoader;
class AppSelectorView extends StatefulWidget { class AppSelectorView extends StatefulWidget {
const AppSelectorView({Key? key}) : super(key: key); const AppSelectorView({super.key});
@override @override
State<AppSelectorView> createState() => _AppSelectorViewState(); State<AppSelectorView> createState() => _AppSelectorViewState();
@@ -23,13 +24,11 @@ class _AppSelectorViewState extends State<AppSelectorView> {
onViewModelReady: (model) => model.initialize(), onViewModelReady: (model) => model.initialize(),
viewModelBuilder: () => AppSelectorViewModel(), viewModelBuilder: () => AppSelectorViewModel(),
builder: (context, model, child) => Scaffold( builder: (context, model, child) => Scaffold(
resizeToAvoidBottomInset: false, floatingActionButton: HapticFloatingActionButtonExtended(
floatingActionButton: FloatingActionButton.extended(
label: I18nText('appSelectorView.storageButton'), label: I18nText('appSelectorView.storageButton'),
icon: const Icon(Icons.sd_storage), icon: const Icon(Icons.sd_storage),
onPressed: () { onPressed: () {
model.selectAppFromStorage(context); model.selectAppFromStorage(context);
Navigator.of(context).pop();
}, },
), ),
body: CustomScrollView( body: CustomScrollView(
@@ -39,12 +38,10 @@ class _AppSelectorViewState extends State<AppSelectorView> {
floating: true, floating: true,
title: I18nText( title: I18nText(
'appSelectorView.viewTitle', 'appSelectorView.viewTitle',
child: Text( ),
'', titleTextStyle: TextStyle(
style: TextStyle( fontSize: 22.0,
color: Theme.of(context).textTheme.titleLarge!.color, color: Theme.of(context).textTheme.titleLarge!.color,
),
),
), ),
leading: IconButton( leading: IconButton(
icon: Icon( icon: Icon(
@@ -54,7 +51,7 @@ class _AppSelectorViewState extends State<AppSelectorView> {
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
), ),
bottom: PreferredSize( bottom: PreferredSize(
preferredSize: const Size.fromHeight(64.0), preferredSize: const Size.fromHeight(66.0),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 8.0, vertical: 8.0,
@@ -98,9 +95,7 @@ class _AppSelectorViewState extends State<AppSelectorView> {
), ),
child: Column( child: Column(
children: [ children: [
...model ...model.getFilteredApps(_query).map(
.getFilteredApps(_query)
.map(
(app) => InstalledAppItem( (app) => InstalledAppItem(
name: app.appName, name: app.appName,
pkgName: app.packageName, pkgName: app.packageName,
@@ -116,12 +111,13 @@ class _AppSelectorViewState extends State<AppSelectorView> {
context, context,
app.packageName, app.packageName,
), ),
onLinkTap: () =>
model.searchSuggestedVersionOnWeb(
packageName: app.packageName,
),
), ),
) ),
.toList(), ...model.getFilteredAppsNames(_query).map(
...model
.getFilteredAppsNames(_query)
.map(
(app) => NotInstalledAppItem( (app) => NotInstalledAppItem(
name: app, name: app,
patchesCount: model.patchesCount(app), patchesCount: model.patchesCount(app),
@@ -130,9 +126,12 @@ class _AppSelectorViewState extends State<AppSelectorView> {
onTap: () { onTap: () {
model.showDownloadToast(); model.showDownloadToast();
}, },
onLinkTap: () =>
model.searchSuggestedVersionOnWeb(
packageName: app,
),
), ),
) ),
.toList(),
const SizedBox(height: 70.0), const SizedBox(height: 70.0),
], ],
), ),

View File

@@ -1,9 +1,10 @@
import 'dart:io'; import 'dart:io';
import 'package:device_apps/device_apps.dart'; import 'package:device_apps/device_apps.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/patch.dart'; import 'package:revanced_manager/models/patch.dart';
@@ -12,7 +13,7 @@ import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/patcher_api.dart'; import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/services/toast.dart'; import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; import 'package:revanced_manager/utils/check_for_supported_patch.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
class AppSelectorViewModel extends BaseViewModel { class AppSelectorViewModel extends BaseViewModel {
@@ -69,17 +70,67 @@ class AppSelectorViewModel extends BaseViewModel {
return true; return true;
} }
Future<void> selectApp(ApplicationWithIcon application) async { Future<void> searchSuggestedVersionOnWeb({
required String packageName,
}) async {
final String suggestedVersion = getSuggestedVersion(packageName);
if (suggestedVersion.isNotEmpty) {
await openDefaultBrowser('$packageName apk version $suggestedVersion');
} else {
await openDefaultBrowser('$packageName apk');
}
}
Future<void> openDefaultBrowser(String query) async {
if (Platform.isAndroid) {
try {
const platform = MethodChannel('app.revanced.manager.flutter/browser');
await platform.invokeMethod('openBrowser', {'query': query});
} catch (e) {
if (kDebugMode) {
print(e);
}
}
} else {
throw 'Platform not supported';
}
}
Future<void> selectApp(
BuildContext context,
ApplicationWithIcon application, [
bool isFromStorage = false,
]) async {
final String suggestedVersion =
getSuggestedVersion(application.packageName);
if (application.versionName != suggestedVersion &&
suggestedVersion.isNotEmpty) {
_managerAPI.suggestedAppVersionSelected = false;
if (_managerAPI.isRequireSuggestedAppVersionEnabled() &&
context.mounted) {
return showRequireSuggestedAppVersionDialog(
context,
application.versionName!,
suggestedVersion,
);
}
} else {
_managerAPI.suggestedAppVersionSelected = true;
}
locator<PatcherViewModel>().selectedApp = PatchedApplication( locator<PatcherViewModel>().selectedApp = PatchedApplication(
name: application.appName, name: application.appName,
packageName: application.packageName, packageName: application.packageName,
originalPackageName: application.packageName,
version: application.versionName!, version: application.versionName!,
apkFilePath: application.apkFilePath, apkFilePath: application.apkFilePath,
icon: application.icon, icon: application.icon,
patchDate: DateTime.now(), patchDate: DateTime.now(),
isFromStorage: isFromStorage,
); );
locator<PatcherViewModel>().loadLastSelectedPatches(); await locator<PatcherViewModel>().loadLastSelectedPatches();
if (context.mounted) {
Navigator.pop(context);
}
} }
Future<void> canSelectInstalled( Future<void> canSelectInstalled(
@@ -89,19 +140,63 @@ class AppSelectorViewModel extends BaseViewModel {
final app = final app =
await DeviceApps.getApp(packageName, true) as ApplicationWithIcon?; await DeviceApps.getApp(packageName, true) as ApplicationWithIcon?;
if (app != null) { if (app != null) {
if (await checkSplitApk(packageName) && !isRooted) { final bool isSplitApk = await checkSplitApk(packageName);
return showSelectFromStorageDialog(context); if (isRooted || !isSplitApk) {
} else if (!await checkSplitApk(packageName) || isRooted) { if (context.mounted) {
selectApp(app); await selectApp(context, app);
Navigator.pop(context); }
final List<Option> requiredNullOptions = getNullRequiredOptions(
locator<PatcherViewModel>().selectedPatches,
packageName,
);
if (requiredNullOptions.isNotEmpty) {
locator<PatcherViewModel>().showRequiredOptionDialog();
}
} else {
if (context.mounted) {
return showSelectFromStorageDialog(context);
}
} }
} }
} }
Future showRequireSuggestedAppVersionDialog(
BuildContext context,
String selectedVersion,
String suggestedVersion,
) async {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('warning'),
content: I18nText(
'appSelectorView.requireSuggestedAppVersionDialogText',
translationParams: {
'suggested': suggestedVersion,
'selected': selectedVersion,
},
child: const Text(
'',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
actions: [
FilledButton(
onPressed: () => Navigator.of(context).pop(),
child: I18nText('okButton'),
),
],
),
);
}
Future showSelectFromStorageDialog(BuildContext context) async { Future showSelectFromStorageDialog(BuildContext context) async {
return showDialog( return showDialog(
context: context, context: context,
builder: (context) => SimpleDialog( builder: (innerContext) => SimpleDialog(
alignment: Alignment.center, alignment: Alignment.center,
contentPadding: contentPadding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 20), const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
@@ -110,7 +205,7 @@ class AppSelectorViewModel extends BaseViewModel {
Icon( Icon(
Icons.block, Icons.block,
size: 28, size: 28,
color: Theme.of(context).colorScheme.primary, color: Theme.of(innerContext).colorScheme.primary,
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
I18nText( I18nText(
@@ -136,14 +231,12 @@ class AppSelectorViewModel extends BaseViewModel {
), ),
), ),
const SizedBox(height: 30), const SizedBox(height: 30),
CustomMaterialButton( FilledButton(
onPressed: () => selectAppFromStorage(context).then( onPressed: () async {
(_) { Navigator.pop(innerContext);
Navigator.pop(context); await selectAppFromStorage(context);
Navigator.pop(context); },
}, child: Row(
),
label: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Icon(Icons.sd_card), const Icon(Icons.sd_card),
@@ -153,12 +246,11 @@ class AppSelectorViewModel extends BaseViewModel {
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
CustomMaterialButton( TextButton(
isFilled: false,
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(innerContext);
}, },
label: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const SizedBox(width: 10), const SizedBox(width: 10),
@@ -173,13 +265,14 @@ class AppSelectorViewModel extends BaseViewModel {
Future<void> selectAppFromStorage(BuildContext context) async { Future<void> selectAppFromStorage(BuildContext context) async {
try { try {
final FilePickerResult? result = await FilePicker.platform.pickFiles( final String? result = await FlutterFileDialog.pickFile(
type: FileType.custom, params: const OpenFileDialogParams(
allowedExtensions: ['apk'], fileExtensionsFilter: ['apk'],
),
); );
if (result != null && result.files.single.path != null) { if (result != null) {
final File apkFile = File(result.files.single.path!); final File apkFile = File(result);
final List<String> pathSplit = result.files.single.path!.split('/'); final List<String> pathSplit = result.split('/');
pathSplit.removeLast(); pathSplit.removeLast();
final Directory filePickerCacheDir = Directory(pathSplit.join('/')); final Directory filePickerCacheDir = Directory(pathSplit.join('/'));
final Iterable<File> deletableFiles = final Iterable<File> deletableFiles =
@@ -194,18 +287,8 @@ class AppSelectorViewModel extends BaseViewModel {
apkFile.path, apkFile.path,
true, true,
) as ApplicationWithIcon?; ) as ApplicationWithIcon?;
if (application != null) { if (application != null && context.mounted) {
locator<PatcherViewModel>().selectedApp = PatchedApplication( await selectApp(context, application, true);
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>().loadLastSelectedPatches();
} }
} }
} on Exception catch (e) { } on Exception catch (e) {
@@ -222,7 +305,8 @@ class AppSelectorViewModel extends BaseViewModel {
(app) => (app) =>
query.isEmpty || query.isEmpty ||
query.length < 2 || query.length < 2 ||
app.appName.toLowerCase().contains(query.toLowerCase()), app.appName.toLowerCase().contains(query.toLowerCase()) ||
app.packageName.toLowerCase().contains(query.toLowerCase()),
) )
.toList(); .toList();
} }

View File

@@ -7,7 +7,7 @@ import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
class ContributorsView extends StatelessWidget { class ContributorsView extends StatelessWidget {
const ContributorsView({Key? key}) : super(key: key); const ContributorsView({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@@ -14,9 +14,9 @@ class ContributorsViewModel extends BaseViewModel {
final Map<String, List<dynamic>> contributors = final Map<String, List<dynamic>> contributors =
await _managerAPI.getContributors(); await _managerAPI.getContributors();
patcherContributors = contributors[_managerAPI.defaultPatcherRepo] ?? []; patcherContributors = contributors[_managerAPI.defaultPatcherRepo] ?? [];
patchesContributors = contributors[_managerAPI.getPatchesRepo()] ?? []; patchesContributors = contributors[_managerAPI.getPatchesRepo().toLowerCase()] ?? [];
integrationsContributors = integrationsContributors =
contributors[_managerAPI.getIntegrationsRepo()] ?? []; contributors[_managerAPI.getIntegrationsRepo().toLowerCase()] ?? [];
cliContributors = contributors[_managerAPI.defaultCliRepo] ?? []; cliContributors = contributors[_managerAPI.defaultCliRepo] ?? [];
managerContributors = contributors[_managerAPI.defaultManagerRepo] ?? []; managerContributors = contributors[_managerAPI.defaultManagerRepo] ?? [];
notifyListeners(); notifyListeners();

View File

@@ -9,7 +9,7 @@ import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
class HomeView extends StatelessWidget { class HomeView extends StatelessWidget {
const HomeView({Key? key}) : super(key: key); const HomeView({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@@ -1,14 +1,13 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:cross_connectivity/cross_connectivity.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:install_plugin/install_plugin.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/app/app.router.dart'; import 'package:revanced_manager/app/app.router.dart';
@@ -21,7 +20,7 @@ import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart'; import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/homeView/update_confirmation_dialog.dart'; import 'package:revanced_manager/ui/widgets/homeView/update_confirmation_dialog.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_checkbox_list_tile.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
@@ -34,19 +33,21 @@ class HomeViewModel extends BaseViewModel {
final RevancedAPI _revancedAPI = locator<RevancedAPI>(); final RevancedAPI _revancedAPI = locator<RevancedAPI>();
final Toast _toast = locator<Toast>(); final Toast _toast = locator<Toast>();
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
DateTime? _lastUpdate;
bool showUpdatableApps = false; bool showUpdatableApps = false;
List<PatchedApplication> patchedInstalledApps = []; List<PatchedApplication> patchedInstalledApps = [];
List<PatchedApplication> patchedUpdatableApps = [];
String? _latestManagerVersion = ''; String? _latestManagerVersion = '';
File? downloadedApk; File? downloadedApk;
Future<void> initialize(BuildContext context) async { Future<void> initialize(BuildContext context) async {
_managerAPI.rePatchedSavedApps().then((_) => _getPatchedApps());
_latestManagerVersion = await _managerAPI.getLatestManagerVersion(); _latestManagerVersion = await _managerAPI.getLatestManagerVersion();
if (!_managerAPI.getPatchesConsent()) { if (!_managerAPI.getPatchesConsent()) {
await showPatchesConsent(context); await showPatchesConsent(context);
} }
await _patcherAPI.initialize(); await _patcherAPI.initialize();
await flutterLocalNotificationsPlugin.initialize( await flutterLocalNotificationsPlugin.initialize(
const InitializationSettings( const InitializationSettings(
android: AndroidInitializationSettings('ic_notification'), android: AndroidInitializationSettings('ic_notification'),
@@ -56,7 +57,7 @@ class HomeViewModel extends BaseViewModel {
_toast.showBottom('homeView.installingMessage'); _toast.showBottom('homeView.installingMessage');
final File? managerApk = await _managerAPI.downloadManager(); final File? managerApk = await _managerAPI.downloadManager();
if (managerApk != null) { if (managerApk != null) {
await InstallPlugin.installApk(managerApk.path); await _patcherAPI.installApk(context, managerApk.path);
} else { } else {
_toast.showBottom('homeView.errorDownloadMessage'); _toast.showBottom('homeView.errorDownloadMessage');
} }
@@ -66,24 +67,25 @@ class HomeViewModel extends BaseViewModel {
flutterLocalNotificationsPlugin flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation< .resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>() AndroidFlutterLocalNotificationsPlugin>()
?.requestPermission(); ?.requestNotificationsPermission();
final bool isConnected = await Connectivity().checkConnection();
final bool isConnected =
await Connectivity().checkConnectivity() != ConnectivityResult.none;
if (!isConnected) { if (!isConnected) {
_toast.showBottom('homeView.noConnection'); _toast.showBottom('homeView.noConnection');
} }
final NotificationAppLaunchDetails? notificationAppLaunchDetails = final NotificationAppLaunchDetails? notificationAppLaunchDetails =
await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) { if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) {
_toast.showBottom('homeView.installingMessage'); _toast.showBottom('homeView.installingMessage');
final File? managerApk = await _managerAPI.downloadManager(); final File? managerApk = await _managerAPI.downloadManager();
if (managerApk != null) { if (managerApk != null) {
await InstallPlugin.installApk(managerApk.path); await _patcherAPI.installApk(context, managerApk.path);
} else { } else {
_toast.showBottom('homeView.errorDownloadMessage'); _toast.showBottom('homeView.errorDownloadMessage');
} }
} }
_getPatchedApps();
_managerAPI.reAssessSavedApps().then((_) => _getPatchedApps());
} }
void navigateToAppInfo(PatchedApplication app) { void navigateToAppInfo(PatchedApplication app) {
@@ -108,10 +110,6 @@ class HomeViewModel extends BaseViewModel {
void _getPatchedApps() { void _getPatchedApps() {
patchedInstalledApps = _managerAPI.getPatchedApps().toList(); patchedInstalledApps = _managerAPI.getPatchedApps().toList();
patchedUpdatableApps = _managerAPI
.getPatchedApps()
.where((app) => app.hasUpdates == true)
.toList();
notifyListeners(); notifyListeners();
} }
@@ -210,7 +208,7 @@ class HomeViewModel extends BaseViewModel {
), ),
), ),
), ),
CheckboxListTile( HapticCheckboxListTile(
value: value, value: value,
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: I18nText( title: I18nText(
@@ -228,21 +226,20 @@ class HomeViewModel extends BaseViewModel {
}, },
), ),
actions: [ actions: [
CustomMaterialButton( TextButton(
isFilled: false,
onPressed: () async { onPressed: () async {
await _managerAPI.setPatchesConsent(false); await _managerAPI.setPatchesConsent(false);
SystemNavigator.pop(); SystemNavigator.pop();
}, },
label: I18nText('quitButton'), child: I18nText('quitButton'),
), ),
CustomMaterialButton( FilledButton(
onPressed: () async { onPressed: () async {
await _managerAPI.setPatchesConsent(true); await _managerAPI.setPatchesConsent(true);
await _managerAPI.setPatchesAutoUpdate(autoUpdate.value); await _managerAPI.setPatchesAutoUpdate(autoUpdate.value);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
label: I18nText('okButton'), child: I18nText('okButton'),
), ),
], ],
), ),
@@ -275,6 +272,7 @@ class HomeViewModel extends BaseViewModel {
valueListenable: downloaded, valueListenable: downloaded,
builder: (context, value, child) { builder: (context, value, child) {
return SimpleDialog( return SimpleDialog(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
contentPadding: const EdgeInsets.all(16.0), contentPadding: const EdgeInsets.all(16.0),
title: I18nText( title: I18nText(
!value !value
@@ -329,12 +327,12 @@ class HomeViewModel extends BaseViewModel {
const SizedBox(height: 16.0), const SizedBox(height: 16.0),
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: CustomMaterialButton( child: FilledButton(
label: I18nText('cancelButton'),
onPressed: () { onPressed: () {
_revancedAPI.disposeManagerUpdateProgress(); _revancedAPI.disposeManagerUpdateProgress();
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: I18nText('cancelButton'),
), ),
), ),
], ],
@@ -360,24 +358,24 @@ class HomeViewModel extends BaseViewModel {
children: [ children: [
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: CustomMaterialButton( child: TextButton(
isFilled: false,
label: I18nText('cancelButton'),
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: I18nText('cancelButton'),
), ),
), ),
const SizedBox(width: 8.0), const SizedBox(width: 8.0),
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: CustomMaterialButton( child: FilledButton(
label: I18nText('updateButton'),
onPressed: () async { onPressed: () async {
await InstallPlugin.installApk( await _patcherAPI.installApk(
context,
downloadedApk!.path, downloadedApk!.path,
); );
}, },
child: I18nText('updateButton'),
), ),
), ),
], ],
@@ -420,7 +418,7 @@ class HomeViewModel extends BaseViewModel {
// UILocalNotificationDateInterpretation.absoluteTime, // UILocalNotificationDateInterpretation.absoluteTime,
// ); // );
_toast.showBottom('homeView.installingMessage'); _toast.showBottom('homeView.installingMessage');
await InstallPlugin.installApk(managerApk.path); await _patcherAPI.installApk(context, managerApk.path);
} else { } else {
_toast.showBottom('homeView.errorDownloadMessage'); _toast.showBottom('homeView.errorDownloadMessage');
} }
@@ -469,11 +467,7 @@ class HomeViewModel extends BaseViewModel {
} }
Future<void> forceRefresh(BuildContext context) async { Future<void> forceRefresh(BuildContext context) async {
await Future.delayed(const Duration(seconds: 1)); _managerAPI.clearAllData();
if (_lastUpdate == null ||
_lastUpdate!.difference(DateTime.now()).inSeconds > 2) {
_managerAPI.clearAllData();
}
_toast.showBottom('homeView.refreshSuccess'); _toast.showBottom('homeView.refreshSuccess');
initialize(context); initialize(context);
} }

View File

@@ -5,27 +5,40 @@ import 'package:revanced_manager/ui/views/installer/installer_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/installerView/gradient_progress_indicator.dart'; import 'package:revanced_manager/ui/widgets/installerView/gradient_progress_indicator.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_floating_action_button_extended.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
class InstallerView extends StatelessWidget { class InstallerView extends StatelessWidget {
const InstallerView({Key? key}) : super(key: key); const InstallerView({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ViewModelBuilder<InstallerViewModel>.reactive( return ViewModelBuilder<InstallerViewModel>.reactive(
onViewModelReady: (model) => model.initialize(context), onViewModelReady: (model) => model.initialize(context),
viewModelBuilder: () => InstallerViewModel(), viewModelBuilder: () => InstallerViewModel(),
builder: (context, model, child) => WillPopScope( builder: (context, model, child) => PopScope(
child: SafeArea( child: SafeArea(
top: false, top: false,
bottom: false, bottom: model.isPatching,
child: Scaffold( child: Scaffold(
floatingActionButton: Visibility( floatingActionButton: Visibility(
visible: !model.isPatching, visible: !model.isPatching && !model.hasErrors,
child: FloatingActionButton.extended( child: HapticFloatingActionButtonExtended(
label: I18nText('installerView.installButton'), label: I18nText(
icon: const Icon(Icons.file_download_outlined), model.isInstalled
onPressed: () => model.installTypeDialog(context), ? 'installerView.openButton'
: 'installerView.installButton',
),
icon: model.isInstalled
? const Icon(Icons.open_in_new)
: const Icon(Icons.file_download_outlined),
onPressed: model.isInstalled
? () => {
model.openApp(),
model.cleanPatcher(),
Navigator.of(context).pop(),
}
: () => model.installTypeDialog(context),
elevation: 0, elevation: 0,
), ),
), ),
@@ -99,7 +112,7 @@ class InstallerView extends StatelessWidget {
), ),
), ),
), ),
onWillPop: () => model.onWillPop(context), onPopInvoked: (bool didPop) => model.onWillPop(context),
), ),
); );
} }

View File

@@ -13,10 +13,12 @@ import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/patcher_api.dart'; import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/services/root_api.dart'; import 'package:revanced_manager/services/root_api.dart';
import 'package:revanced_manager/services/toast.dart'; import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; import 'package:revanced_manager/utils/about_info.dart';
import 'package:screenshot_callback/screenshot_callback.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:wakelock/wakelock.dart'; import 'package:wakelock_plus/wakelock_plus.dart';
class InstallerViewModel extends BaseViewModel { class InstallerViewModel extends BaseViewModel {
final ManagerAPI _managerAPI = locator<ManagerAPI>(); final ManagerAPI _managerAPI = locator<ManagerAPI>();
@@ -29,6 +31,7 @@ class InstallerViewModel extends BaseViewModel {
'app.revanced.manager.flutter/installer', 'app.revanced.manager.flutter/installer',
); );
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
final ScreenshotCallback screenshotCallback = ScreenshotCallback();
double? progress = 0.0; double? progress = 0.0;
String logs = ''; String logs = '';
String headerLogs = ''; String headerLogs = '';
@@ -38,6 +41,7 @@ class InstallerViewModel extends BaseViewModel {
bool hasErrors = false; bool hasErrors = false;
bool isCanceled = false; bool isCanceled = false;
bool cancel = false; bool cancel = false;
bool showPopupScreenshotWarning = true;
Future<void> initialize(BuildContext context) async { Future<void> initialize(BuildContext context) async {
isRooted = await _rootAPI.isRooted(); isRooted = await _rootAPI.isRooted();
@@ -64,7 +68,13 @@ class InstallerViewModel extends BaseViewModel {
} // ignore } // ignore
} }
} }
await Wakelock.enable(); screenshotCallback.addListener(() {
if (showPopupScreenshotWarning) {
showPopupScreenshotWarning = false;
screenshotDetected(context);
}
});
await WakelockPlus.enable();
await handlePlatformChannelMethods(); await handlePlatformChannelMethods();
await runPatcher(); await runPatcher();
} }
@@ -94,7 +104,7 @@ class InstallerViewModel extends BaseViewModel {
isPatching = true; isPatching = true;
isInstalled = false; isInstalled = false;
hasErrors = false; hasErrors = false;
} else if (value == 1.0) { } else if (value == .85) {
isPatching = false; isPatching = false;
hasErrors = false; hasErrors = false;
await _managerAPI.savePatches( await _managerAPI.savePatches(
@@ -105,6 +115,7 @@ class InstallerViewModel extends BaseViewModel {
} else if (value == -100.0) { } else if (value == -100.0) {
isPatching = false; isPatching = false;
hasErrors = true; hasErrors = true;
progress = 0.0;
} }
if (header.isNotEmpty) { if (header.isNotEmpty) {
headerLogs = header; headerLogs = header;
@@ -117,10 +128,10 @@ class InstallerViewModel extends BaseViewModel {
if (logs[logs.length - 1] == '\n') { if (logs[logs.length - 1] == '\n') {
logs = logs.substring(0, logs.length - 1); logs = logs.substring(0, logs.length - 1);
} }
Future.delayed(const Duration(milliseconds: 500)).then((value) { Future.delayed(const Duration(milliseconds: 100)).then((value) {
scrollController.animateTo( scrollController.animateTo(
scrollController.position.maxScrollExtent, scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 100),
curve: Curves.fastOutSlowIn, curve: Curves.fastOutSlowIn,
); );
}); });
@@ -130,28 +141,28 @@ class InstallerViewModel extends BaseViewModel {
Future<void> runPatcher() async { Future<void> runPatcher() async {
try { try {
update(0.0, 'Initializing...', 'Initializing installer'); await _patcherAPI.runPatcher(
if (_patches.isNotEmpty) { _app.packageName,
try { _app.apkFilePath,
update(0.1, '', 'Creating working directory'); _patches,
await _patcherAPI.runPatcher( );
_app.packageName, } on Exception catch (e) {
_app.apkFilePath, update(
_patches, -100.0,
); 'Failed...',
} on Exception catch (e) { 'Something went wrong:\n$e',
update( );
-100.0, if (kDebugMode) {
'Aborted...', print(e);
'An error occurred! Aborted\nError:\n$e',
);
if (kDebugMode) {
print(e);
}
}
} else {
update(-100.0, 'Aborted...', 'No app or patches selected! Aborted');
} }
}
// Necessary to reset the state of patches so that they
// can be reloaded again.
_managerAPI.patches.clear();
await _patcherAPI.loadPatches();
try {
if (FlutterBackground.isBackgroundExecutionEnabled) { if (FlutterBackground.isBackgroundExecutionEnabled) {
try { try {
FlutterBackground.disableBackgroundExecution(); FlutterBackground.disableBackgroundExecution();
@@ -161,7 +172,7 @@ class InstallerViewModel extends BaseViewModel {
} // ignore } // ignore
} }
} }
await Wakelock.disable(); await WakelockPlus.disable();
} on Exception catch (e) { } on Exception catch (e) {
if (kDebugMode) { if (kDebugMode) {
print(e); print(e);
@@ -169,95 +180,269 @@ class InstallerViewModel extends BaseViewModel {
} }
} }
void _trimLogs(List<String> logLines, String keyword, String? newString) {
final lineCount = logLines.where((line) => line.endsWith(keyword)).length;
final index = logLines.indexWhere((line) => line.endsWith(keyword));
if (newString != null && lineCount > 0) {
logLines.insert(
index,
newString.replaceAll('{lineCount}', lineCount.toString()),
);
}
logLines.removeWhere((lines) => lines.endsWith(keyword));
}
dynamic _getPatchOptionValue(String patchName, Option option) {
final Option? savedOption =
_managerAPI.getPatchOption(_app.packageName, patchName, option.key);
if (savedOption != null) {
return savedOption.value;
} else {
return option.value;
}
}
String _formatPatches(List<Patch> patches, String noneString) {
return patches.isEmpty
? noneString
: patches.map((p) {
final optionsChanged = p.options
.where((o) => _getPatchOptionValue(p.name, o) != o.value)
.toList();
return p.name +
(optionsChanged.isEmpty
? ''
: ' [${optionsChanged.map((o) => '${o.title}: ${_getPatchOptionValue(p.name, o)}').join(", ")}]');
}).join(', ');
}
String _getSuggestedVersion(String packageName) {
String suggestedVersion = _patcherAPI.getSuggestedVersion(_app.packageName);
if (suggestedVersion.isEmpty) {
suggestedVersion = 'Any';
}
return suggestedVersion;
}
Future<void> copyLogs() async {
final info = await AboutInfo.getInfo();
// Trim out extra lines
final logsTrimmed = logs.split('\n');
_trimLogs(logsTrimmed, 'succeeded', 'Applied {lineCount} patches');
_trimLogs(logsTrimmed, '.dex', 'Compiled {lineCount} dex files');
// Get patches added / removed
final defaultPatches = _patcherAPI
.getFilteredPatches(_app.packageName)
.where((p) => !p.excluded)
.toList();
final appliedPatchesNames = _patches.map((p) => p.name).toList();
final patchesAdded = _patches.where((p) => p.excluded).toList();
final patchesRemoved = defaultPatches
.where((p) => !appliedPatchesNames.contains(p.name))
.map((p) => p.name)
.toList();
final patchesOptionsChanged = defaultPatches
.where(
(p) =>
appliedPatchesNames.contains(p.name) &&
p.options.any((o) => _getPatchOptionValue(p.name, o) != o.value),
)
.toList();
// Add Info
final formattedLogs = [
'- Device Info',
'ReVanced Manager: ${info['version']}',
'Model: ${info['model']}',
'Android version: ${info['androidVersion']}',
'Supported architectures: ${info['supportedArch'].join(", ")}',
'Root permissions: ${isRooted ? 'Yes' : 'No'}', //
'\n- Patch Info',
'App: ${_app.packageName} v${_app.version} (Suggested: ${_getSuggestedVersion(_app.packageName)})',
'Patches version: ${_managerAPI.patchesVersion}',
'Patches added: ${_formatPatches(patchesAdded, 'Default')}',
'Patches removed: ${patchesRemoved.isEmpty ? 'None' : patchesRemoved.join(', ')}',
'Default patch options changed: ${_formatPatches(patchesOptionsChanged, 'None')}', //
'\n- Settings',
'Allow changing patch selection: ${_managerAPI.isPatchesChangeEnabled()}',
'Version compatibility check: ${_managerAPI.isVersionCompatibilityCheckEnabled()}',
'Show universal patches: ${_managerAPI.areUniversalPatchesEnabled()}',
'Patches source: ${_managerAPI.getPatchesRepo()}',
'Integration source: ${_managerAPI.getIntegrationsRepo()}', //
'\n- Logs',
logsTrimmed.join('\n'),
];
Clipboard.setData(ClipboardData(text: formattedLogs.join('\n')));
_toast.showBottom('installerView.copiedToClipboard');
}
Future<void> screenshotDetected(BuildContext context) async {
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText(
'warning',
),
icon: const Icon(Icons.warning),
content: SingleChildScrollView(
child: I18nText('installerView.screenshotDetected'),
),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: I18nText('noButton'),
),
FilledButton(
onPressed: () {
copyLogs();
showPopupScreenshotWarning = true;
Navigator.of(context).pop();
},
child: I18nText('yesButton'),
),
],
),
);
}
Future<void> installTypeDialog(BuildContext context) async { Future<void> installTypeDialog(BuildContext context) async {
final ValueNotifier<int> installType = ValueNotifier(0); final ValueNotifier<int> installType = ValueNotifier(0);
if (isRooted) { if (isRooted) {
await showDialog( await showDialog(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
builder: (context) => AlertDialog( builder: (innerContext) => AlertDialog(
title: I18nText( title: I18nText(
'installerView.installType', 'installerView.installType',
), ),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
icon: const Icon(Icons.file_download_outlined), icon: const Icon(Icons.file_download_outlined),
contentPadding: const EdgeInsets.symmetric(vertical: 16), contentPadding: const EdgeInsets.symmetric(vertical: 16),
content: ValueListenableBuilder( content: SingleChildScrollView(
valueListenable: installType, child: ValueListenableBuilder(
builder: (context, value, child) { valueListenable: installType,
return Column( builder: (context, value, child) {
mainAxisSize: MainAxisSize.min, return Column(
crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min,
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Padding( children: [
padding: const EdgeInsets.symmetric( Padding(
horizontal: 20, padding: const EdgeInsets.symmetric(
vertical: 10, horizontal: 20,
), vertical: 10,
child: I18nText( ),
'installerView.installTypeDescription', child: I18nText(
child: Text( 'installerView.installTypeDescription',
'', child: Text(
style: TextStyle( '',
fontSize: 16, style: TextStyle(
fontWeight: FontWeight.w500, fontSize: 16,
color: Theme.of(context).colorScheme.secondary, fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.secondary,
),
), ),
), ),
), ),
), RadioListTile(
RadioListTile( title: I18nText('installerView.installNonRootType'),
title: I18nText('installerView.installNonRootType'), contentPadding:
subtitle: I18nText('installerView.installRecommendedType'), const EdgeInsets.symmetric(horizontal: 16),
contentPadding: const EdgeInsets.symmetric(horizontal: 10), value: 0,
value: 0, groupValue: value,
groupValue: value, onChanged: (selected) {
onChanged: (selected) { installType.value = selected!;
installType.value = selected!; },
}, ),
), RadioListTile(
RadioListTile( title: I18nText('installerView.installRootType'),
title: I18nText('installerView.installRootType'), contentPadding:
contentPadding: const EdgeInsets.symmetric(horizontal: 10), const EdgeInsets.symmetric(horizontal: 16),
value: 1, value: 1,
groupValue: value, groupValue: value,
onChanged: (selected) { onChanged: (selected) {
installType.value = selected!; installType.value = selected!;
}, },
), ),
], Padding(
); padding: const EdgeInsets.symmetric(horizontal: 16),
}, child: I18nText(
), 'installerView.warning',
actions: [ child: Text(
CustomMaterialButton( '',
label: I18nText('cancelButton'), style: TextStyle(
isFilled: false, fontWeight: FontWeight.w500,
onPressed: () { color: Theme.of(context).colorScheme.error,
Navigator.of(context).pop(); ),
),
),
),
],
);
}, },
), ),
CustomMaterialButton( ),
label: I18nText('installerView.installButton'), actions: [
TextButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(innerContext).pop();
},
child: I18nText('cancelButton'),
),
FilledButton(
onPressed: () {
Navigator.of(innerContext).pop();
installResult(context, installType.value == 1); installResult(context, installType.value == 1);
}, },
child: I18nText('installerView.installButton'),
), ),
], ],
), ),
); );
} else { } else {
installResult(context, false); await showDialog(
context: context,
barrierDismissible: false,
builder: (innerContext) => AlertDialog(
title: I18nText(
'warning',
),
contentPadding: const EdgeInsets.all(16),
content: I18nText('installerView.warning'),
actions: [
TextButton(
onPressed: () {
Navigator.of(innerContext).pop();
},
child: I18nText('cancelButton'),
),
FilledButton(
onPressed: () {
Navigator.of(innerContext).pop();
installResult(context, false);
},
child: I18nText('installerView.installButton'),
),
],
),
);
} }
} }
Future<void> stopPatcher() async { Future<void> stopPatcher() async {
try { try {
isCanceled = true; isCanceled = true;
update(0.5, 'Aborting...', 'Canceling patching process'); update(0.5, 'Canceling...', 'Canceling patching process');
await _patcherAPI.stopPatcher(); await _patcherAPI.stopPatcher();
update(-100.0, 'Aborted...', 'Press back to exit'); await WakelockPlus.disable();
update(-100.0, 'Canceled...', 'Press back to exit');
} on Exception catch (e) { } on Exception catch (e) {
if (kDebugMode) { if (kDebugMode) {
print(e); print(e);
@@ -268,56 +453,47 @@ class InstallerViewModel extends BaseViewModel {
Future<void> installResult(BuildContext context, bool installAsRoot) async { Future<void> installResult(BuildContext context, bool installAsRoot) async {
try { try {
_app.isRooted = installAsRoot; _app.isRooted = installAsRoot;
final bool hasMicroG = if (headerLogs != 'Installing...') {
_patches.any((p) => p.name.endsWith('MicroG support')); update(
final bool rootMicroG = installAsRoot && hasMicroG; .85,
final bool rootFromStorage = installAsRoot && _app.isFromStorage; 'Installing...',
final bool ytWithoutRootMicroG = _app.isRooted ? 'Mounting patched app' : 'Installing patched app',
!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(),
),
],
),
); );
}
final int response = await _patcherAPI.installPatchedFile(context, _app);
if (response == 0) {
isInstalled = true;
_app.isFromStorage = false;
_app.patchDate = DateTime.now();
_app.appliedPatches = _patches.map((p) => p.name).toList();
// In case a patch changed the app name or package name,
// update the app info.
final app =
await DeviceApps.getAppFromStorage(_patcherAPI.outFile!.path);
if (app != null) {
_app.name = app.appName;
_app.packageName = app.packageName;
}
await _managerAPI.savePatchedApp(_app);
await locator<HomeViewModel>().initialize(context);
update(1.0, 'Installed', 'Installed');
} else if (response == 3) {
update(
.85,
'Installation canceled',
'Installation canceled',
);
} else if (response == 10) {
installResult(context, installAsRoot);
} else { } else {
update( update(
1.0, .85,
'Installing...', 'Installation failed',
_app.isRooted 'Installation failed',
? '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) { } on Exception catch (e) {
if (kDebugMode) { if (kDebugMode) {
@@ -336,10 +512,6 @@ class InstallerViewModel extends BaseViewModel {
} }
} }
void exportLog() {
_patcherAPI.exportPatcherLog(logs);
}
Future<void> cleanPatcher() async { Future<void> cleanPatcher() async {
try { try {
_patcherAPI.cleanPatcher(); _patcherAPI.cleanPatcher();
@@ -363,7 +535,7 @@ class InstallerViewModel extends BaseViewModel {
exportResult(); exportResult();
break; break;
case 1: case 1:
exportLog(); copyLogs();
break; break;
} }
} }
@@ -385,6 +557,7 @@ class InstallerViewModel extends BaseViewModel {
} else { } else {
_patcherAPI.cleanPatcher(); _patcherAPI.cleanPatcher();
} }
screenshotCallback.dispose();
Navigator.of(context).pop(); Navigator.of(context).pop();
return true; return true;
} }

View File

@@ -6,21 +6,17 @@ import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
class NavigationView extends StatelessWidget { class NavigationView extends StatelessWidget {
const NavigationView({Key? key}) : super(key: key); const NavigationView({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ViewModelBuilder<NavigationViewModel>.reactive( return ViewModelBuilder<NavigationViewModel>.reactive(
onViewModelReady: (model) => model.initialize(context), onViewModelReady: (model) => model.initialize(context),
viewModelBuilder: () => locator<NavigationViewModel>(), viewModelBuilder: () => locator<NavigationViewModel>(),
builder: (context, model, child) => WillPopScope( builder: (context, model, child) => PopScope(
onWillPop: () async { canPop: model.currentIndex == 0,
if (model.currentIndex == 0) { onPopInvoked: (bool didPop) => {
return true; if (!didPop) model.setIndex(0),
} else {
model.setIndex(0);
return false;
}
}, },
child: Scaffold( child: Scaffold(
body: PageTransitionSwitcher( body: PageTransitionSwitcher(

View File

@@ -1,4 +1,5 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'package:device_info_plus/device_info_plus.dart';
import 'package:dynamic_themes/dynamic_themes.dart'; import 'package:dynamic_themes/dynamic_themes.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@@ -18,25 +19,34 @@ class NavigationViewModel extends IndexTrackingViewModel {
Future<void> initialize(BuildContext context) async { Future<void> initialize(BuildContext context) async {
locator<Toast>().initialize(context); locator<Toast>().initialize(context);
final SharedPreferences prefs = await SharedPreferences.getInstance(); final SharedPreferences prefs = await SharedPreferences.getInstance();
requestManageExternalStorage();
if (prefs.getBool('permissionsRequested') == null) { if (prefs.getBool('permissionsRequested') == null) {
await Permission.storage.request(); await Permission.storage.request();
await Permission.manageExternalStorage.request();
await prefs.setBool('permissionsRequested', true); await prefs.setBool('permissionsRequested', true);
RootAPI().hasRootPermissions().then( await RootAPI().hasRootPermissions().then(
(value) => Permission.requestInstallPackages.request().then( (value) => Permission.requestInstallPackages.request().then(
(value) => Permission.ignoreBatteryOptimizations.request(), (value) => Permission.ignoreBatteryOptimizations.request(),
), ),
); );
} }
if (prefs.getBool('useDarkTheme') == null) { final dynamicTheme = DynamicTheme.of(context)!;
final bool isDark = if (prefs.getInt('themeMode') == null) {
MediaQuery.platformBrightnessOf(context) != Brightness.light; await prefs.setInt('themeMode', 0);
await prefs.setBool('useDarkTheme', isDark); await dynamicTheme.setTheme(0);
await DynamicTheme.of(context)!.setTheme(isDark ? 1 : 0);
} }
// Force disable Material You on Android 11 and below
if (dynamicTheme.themeId.isOdd) {
const int android12SdkVersion = 31;
final AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo;
if (info.version.sdkInt < android12SdkVersion) {
await prefs.setInt('themeMode', 0);
await prefs.setBool('useDynamicTheme', false);
await dynamicTheme.setTheme(0);
}
}
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle( SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle( SystemUiOverlayStyle(
@@ -49,17 +59,6 @@ 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) { Widget getViewForIndex(int index) {
switch (index) { switch (index) {
case 0: case 0:

View File

@@ -0,0 +1,132 @@
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/ui/views/patch_options/patch_options_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_options_fields.dart';
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_floating_action_button_extended.dart';
import 'package:stacked/stacked.dart';
class PatchOptionsView extends StatelessWidget {
const PatchOptionsView({super.key});
@override
Widget build(BuildContext context) {
return ViewModelBuilder<PatchOptionsViewModel>.reactive(
onViewModelReady: (model) => model.initialize(),
viewModelBuilder: () => PatchOptionsViewModel(),
builder: (context, model, child) => GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
floatingActionButton: HapticFloatingActionButtonExtended(
onPressed: () async {
final bool saved = model.saveOptions(context);
if (saved && context.mounted) {
Navigator.pop(context);
}
},
label: I18nText('patchOptionsView.saveOptions'),
icon: const Icon(Icons.save),
),
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
title: I18nText(
'patchOptionsView.viewTitle',
child: Text(
'',
style: GoogleFonts.inter(
color: Theme.of(context).textTheme.titleLarge!.color,
),
),
),
actions: [
IconButton(
onPressed: () {
model.resetOptions();
},
icon: const Icon(
Icons.history,
),
tooltip: FlutterI18n.translate(
context,
'patchOptionsView.resetOptionsTooltip',
),
),
],
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
for (final Option option in model.visibleOptions)
if (option.valueType == 'String' ||
option.valueType == 'Int')
IntAndStringPatchOption(
patchOption: option,
removeOption: (option) {
model.removeOption(option);
},
onChanged: (value, option) {
model.modifyOptions(value, option);
},
)
else if (option.valueType == 'Boolean')
BooleanPatchOption(
patchOption: option,
removeOption: (option) {
model.removeOption(option);
},
onChanged: (value, option) {
model.modifyOptions(value, option);
},
)
else if (option.valueType == 'StringArray' ||
option.valueType == 'IntArray' ||
option.valueType == 'LongArray')
IntStringLongListPatchOption(
patchOption: option,
removeOption: (option) {
model.removeOption(option);
},
onChanged: (value, option) {
model.modifyOptions(value, option);
},
)
else
UnsupportedPatchOption(
patchOption: option,
),
if (model.visibleOptions.length !=
model.options.length) ...[
const SizedBox(
height: 8,
),
FilledButton(
onPressed: () {
model.showAddOptionDialog(context);
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.add),
I18nText('patchOptionsView.addOptions'),
],
),
),
],
const SizedBox(
height: 80,
),
],
),
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,268 @@
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/services/manager_api.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
import 'package:stacked/stacked.dart';
class PatchOptionsViewModel extends BaseViewModel {
final ManagerAPI _managerAPI = locator<ManagerAPI>();
final String selectedApp =
locator<PatcherViewModel>().selectedApp!.packageName;
List<Option> options = [];
List<Option> savedOptions = [];
List<Option> visibleOptions = [];
Future<void> initialize() async {
options = getDefaultOptions();
for (final Option option in options) {
final Option? savedOption = _managerAPI.getPatchOption(
selectedApp,
_managerAPI.selectedPatch!.name,
option.key,
);
if (savedOption != null) {
savedOptions.add(savedOption);
}
}
if (savedOptions.isNotEmpty) {
visibleOptions = [
...savedOptions,
...options.where(
(option) =>
option.required &&
!savedOptions.any((sOption) => sOption.key == option.key),
),
];
} else {
visibleOptions = [
...options.where((option) => option.required),
];
}
}
void addOption(Option option) {
visibleOptions.add(option);
notifyListeners();
}
void removeOption(Option option) {
visibleOptions.removeWhere((vOption) => vOption.key == option.key);
notifyListeners();
}
bool saveOptions(BuildContext context) {
final List<Option> requiredNullOptions = [];
for (final Option option in options) {
if (!visibleOptions.any((vOption) => vOption.key == option.key)) {
_managerAPI.clearPatchOption(
selectedApp,
_managerAPI.selectedPatch!.name,
option.key,
);
}
}
for (final Option option in visibleOptions) {
if (option.required && option.value == null) {
requiredNullOptions.add(option);
} else {
_managerAPI.setPatchOption(
option,
_managerAPI.selectedPatch!.name,
selectedApp,
);
}
}
if (requiredNullOptions.isNotEmpty) {
showRequiredOptionNullDialog(
context,
requiredNullOptions,
_managerAPI,
selectedApp,
);
return false;
}
return true;
}
void modifyOptions(dynamic value, Option option) {
final Option modifiedOption = Option(
title: option.title,
description: option.description,
values: option.values,
valueType: option.valueType,
value: value,
required: option.required,
key: option.key,
);
visibleOptions[visibleOptions
.indexWhere((vOption) => vOption.key == option.key)] = modifiedOption;
_managerAPI.modifiedOptions
.removeWhere((mOption) => mOption.key == option.key);
_managerAPI.modifiedOptions.add(modifiedOption);
}
List<Option> getDefaultOptions() {
final List<Option> defaultOptions = [];
for (final option in _managerAPI.options) {
final Option defaultOption = Option(
title: option.title,
description: option.description,
values: option.values,
valueType: option.valueType,
value: option.value is List ? option.value.toList() : option.value,
required: option.required,
key: option.key,
);
defaultOptions.add(defaultOption);
}
return defaultOptions;
}
void resetOptions() {
_managerAPI.modifiedOptions.clear();
visibleOptions =
getDefaultOptions().where((option) => option.required).toList();
notifyListeners();
}
Future<void> showAddOptionDialog(BuildContext context) async {
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
I18nText(
'patchOptionsView.addOptions',
),
Text(
'',
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
),
],
),
actions: [
FilledButton(
onPressed: () {
Navigator.of(context).pop();
},
child: I18nText('cancelButton'),
),
],
contentPadding: const EdgeInsets.all(8),
content: Wrap(
spacing: 14,
runSpacing: 14,
children: options
.where(
(option) =>
!visibleOptions.any((vOption) => vOption.key == option.key),
)
.map((e) {
return CustomCard(
padding: const EdgeInsets.all(4),
backgroundColor: Theme.of(context).colorScheme.surface,
onTap: () {
addOption(e);
Navigator.pop(context);
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
e.title,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 4),
Text(
e.description,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
),
],
),
),
);
}).toList(),
),
),
);
}
}
Future<void> showRequiredOptionNullDialog(
BuildContext context,
List<Option> options,
ManagerAPI managerAPI,
String selectedApp,
) async {
final List<String> optionsTitles = [];
for (final option in options) {
optionsTitles.add('${option.title}');
}
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('notice'),
actions: [
TextButton(
onPressed: () async {
if (managerAPI.isPatchesChangeEnabled()) {
locator<PatcherViewModel>()
.selectedPatches
.remove(managerAPI.selectedPatch);
locator<PatcherViewModel>().notifyListeners();
for (final option in options) {
managerAPI.clearPatchOption(
selectedApp,
managerAPI.selectedPatch!.name,
option.key,
);
}
Navigator.of(context)
..pop()
..pop()
..pop();
} else {
PatchesSelectorViewModel().showPatchesChangeDialog(context);
}
},
child: I18nText('patchOptionsView.deselectPatch'),
),
FilledButton(
onPressed: () {
Navigator.of(context).pop();
},
child: I18nText('okButton'),
),
],
content: I18nText(
'patchOptionsView.requiredOptionNull',
translationParams: {
'options': optionsTitles.join('\n'),
},
),
),
);
}

View File

@@ -6,10 +6,11 @@ import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/patcherView/app_selector_card.dart'; import 'package:revanced_manager/ui/widgets/patcherView/app_selector_card.dart';
import 'package:revanced_manager/ui/widgets/patcherView/patch_selector_card.dart'; import 'package:revanced_manager/ui/widgets/patcherView/patch_selector_card.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_floating_action_button_extended.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
class PatcherView extends StatelessWidget { class PatcherView extends StatelessWidget {
const PatcherView({Key? key}) : super(key: key); const PatcherView({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -19,10 +20,17 @@ class PatcherView extends StatelessWidget {
builder: (context, model, child) => Scaffold( builder: (context, model, child) => Scaffold(
floatingActionButton: Visibility( floatingActionButton: Visibility(
visible: model.showPatchButton(), visible: model.showPatchButton(),
child: FloatingActionButton.extended( child: HapticFloatingActionButtonExtended(
label: I18nText('patcherView.patchButton'), label: I18nText('patcherView.patchButton'),
icon: const Icon(Icons.build), icon: const Icon(Icons.build),
onPressed: () => model.showRemovedPatchesDialog(context), onPressed: () async {
if (model.checkRequiredPatchOption(context)) {
final bool proceed = model.showRemovedPatchesDialog(context);
if (proceed && context.mounted) {
model.showArmv7WarningDialog(context);
}
}
},
), ),
), ),
body: CustomScrollView( body: CustomScrollView(
@@ -45,7 +53,10 @@ class PatcherView extends StatelessWidget {
delegate: SliverChildListDelegate.fixed( delegate: SliverChildListDelegate.fixed(
<Widget>[ <Widget>[
AppSelectorCard( AppSelectorCard(
onPressed: () => model.navigateToAppSelector(), onPressed: () => {
model.navigateToAppSelector(),
model.ctx = context,
},
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Opacity( Opacity(

View File

@@ -1,6 +1,10 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
@@ -9,7 +13,6 @@ import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/models/patched_application.dart'; import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/patcher_api.dart'; import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/utils/about_info.dart'; import 'package:revanced_manager/utils/about_info.dart';
import 'package:revanced_manager/utils/check_for_supported_patch.dart'; import 'package:revanced_manager/utils/check_for_supported_patch.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
@@ -20,7 +23,9 @@ class PatcherViewModel extends BaseViewModel {
final NavigationService _navigationService = locator<NavigationService>(); final NavigationService _navigationService = locator<NavigationService>();
final ManagerAPI _managerAPI = locator<ManagerAPI>(); final ManagerAPI _managerAPI = locator<ManagerAPI>();
final PatcherAPI _patcherAPI = locator<PatcherAPI>(); final PatcherAPI _patcherAPI = locator<PatcherAPI>();
Set<String> savedPatchNames = {};
PatchedApplication? selectedApp; PatchedApplication? selectedApp;
BuildContext? ctx;
List<Patch> selectedPatches = []; List<Patch> selectedPatches = [];
List<String> removedPatches = []; List<String> removedPatches = [];
@@ -44,79 +49,70 @@ class PatcherViewModel extends BaseViewModel {
return selectedApp == null; return selectedApp == null;
} }
Future<bool> isValidPatchConfig() async { bool showRemovedPatchesDialog(BuildContext context) {
final bool needsResourcePatching = await _patcherAPI.needsResourcePatching(
selectedPatches,
);
if (needsResourcePatching && selectedApp != null) {
final bool isSplit = await _managerAPI.isSplitApk(selectedApp!);
return !isSplit;
}
return true;
}
Future<void> showPatchConfirmationDialog(BuildContext context) async {
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) { if (removedPatches.isNotEmpty) {
return showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: I18nText('notice'), title: I18nText('notice'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText( content: I18nText(
'patcherView.removedPatchesWarningDialogText', 'patcherView.removedPatchesWarningDialogText',
translationParams: {'patches': removedPatches.join('\n')}, translationParams: {'patches': removedPatches.join('\n')},
), ),
actions: <Widget>[ actions: <Widget>[
CustomMaterialButton( TextButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(),
),
CustomMaterialButton(
label: I18nText('yesButton'),
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
navigateToInstaller();
}, },
child: I18nText('noButton'),
),
FilledButton(
onPressed: () {
Navigator.of(context).pop();
showArmv7WarningDialog(context);
},
child: I18nText('yesButton'),
), ),
], ],
), ),
); );
} else { return false;
showArmv7WarningDialog(context);
} }
return true;
}
bool checkRequiredPatchOption(BuildContext context) {
if (getNullRequiredOptions(selectedPatches, selectedApp!.packageName)
.isNotEmpty) {
showRequiredOptionDialog(context);
return false;
}
return true;
}
void showRequiredOptionDialog([context]) {
showDialog(
context: context ?? ctx,
builder: (context) => AlertDialog(
title: I18nText('notice'),
content: I18nText('patcherView.requiredOptionDialogText'),
actions: <Widget>[
TextButton(
onPressed: () => {
Navigator.of(context).pop(),
},
child: I18nText('cancelButton'),
),
FilledButton(
onPressed: () => {
Navigator.pop(context),
navigateToPatchesSelector(),
},
child: I18nText('okButton'),
),
],
),
);
} }
Future<void> showArmv7WarningDialog(BuildContext context) async { Future<void> showArmv7WarningDialog(BuildContext context) async {
@@ -130,20 +126,18 @@ class PatcherViewModel extends BaseViewModel {
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: I18nText('warning'), title: I18nText('warning'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('patcherView.armv7WarningDialogText'), content: I18nText('patcherView.armv7WarningDialogText'),
actions: <Widget>[ actions: <Widget>[
CustomMaterialButton( FilledButton(
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: I18nText('noButton'),
), ),
CustomMaterialButton( TextButton(
label: I18nText('yesButton'),
isFilled: false,
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
navigateToInstaller(); navigateToInstaller();
}, },
child: I18nText('yesButton'),
), ),
], ],
), ),
@@ -154,40 +148,55 @@ class PatcherViewModel extends BaseViewModel {
} }
String getAppSelectionString() { String getAppSelectionString() {
String text = '${selectedApp!.name} (${selectedApp!.packageName})'; return '${selectedApp!.name} ${selectedApp!.version}';
if (text.length > 32) { }
text = '${text.substring(0, 32)}...)';
} Future<void> queryVersion(String suggestedVersion) async {
return text; await openDefaultBrowser(
'${selectedApp!.packageName} apk version $suggestedVersion',
);
} }
String getSuggestedVersionString(BuildContext context) { String getSuggestedVersionString(BuildContext context) {
String suggestedVersion = return _patcherAPI.getSuggestedVersion(selectedApp!.packageName);
_patcherAPI.getSuggestedVersion(selectedApp!.packageName); }
if (suggestedVersion.isEmpty) {
suggestedVersion = FlutterI18n.translate( Future<void> openDefaultBrowser(String query) async {
context, if (Platform.isAndroid) {
'appSelectorCard.allVersions', try {
); const platform = MethodChannel('app.revanced.manager.flutter/browser');
await platform.invokeMethod('openBrowser', {'query': query});
} catch (e) {
if (kDebugMode) {
print(e);
}
}
} else { } else {
suggestedVersion = 'v$suggestedVersion'; throw 'Platform not supported';
}
}
bool isPatchNew(Patch patch) {
if (savedPatchNames.isEmpty) {
savedPatchNames = _managerAPI
.getSavedPatches(selectedApp!.packageName)
.map((p) => p.name)
.toSet();
}
if (savedPatchNames.isEmpty) {
return false;
} else {
return !savedPatchNames.contains(patch.name);
} }
return '${FlutterI18n.translate(
context,
'appSelectorCard.currentVersion',
)}: v${selectedApp!.version}\n${FlutterI18n.translate(
context,
'appSelectorCard.suggestedVersion',
)}: $suggestedVersion';
} }
Future<void> loadLastSelectedPatches() async { Future<void> loadLastSelectedPatches() async {
this.selectedPatches.clear(); this.selectedPatches.clear();
removedPatches.clear(); removedPatches.clear();
final List<String> selectedPatches = final List<String> selectedPatches =
await _managerAPI.getSelectedPatches(selectedApp!.originalPackageName); await _managerAPI.getSelectedPatches(selectedApp!.packageName);
final List<Patch> patches = final List<Patch> patches =
_patcherAPI.getFilteredPatches(selectedApp!.originalPackageName); _patcherAPI.getFilteredPatches(selectedApp!.packageName);
this this
.selectedPatches .selectedPatches
.addAll(patches.where((patch) => selectedPatches.contains(patch.name))); .addAll(patches.where((patch) => selectedPatches.contains(patch.name)));
@@ -195,7 +204,7 @@ class PatcherViewModel extends BaseViewModel {
this.selectedPatches.clear(); this.selectedPatches.clear();
this.selectedPatches.addAll(patches.where((patch) => !patch.excluded)); this.selectedPatches.addAll(patches.where((patch) => !patch.excluded));
} }
if (!_managerAPI.areExperimentalPatchesEnabled()) { if (_managerAPI.isVersionCompatibilityCheckEnabled()) {
this.selectedPatches.removeWhere((patch) => !isPatchSupported(patch)); this.selectedPatches.removeWhere((patch) => !isPatchSupported(patch));
} }
if (!_managerAPI.areUniversalPatchesEnabled()) { if (!_managerAPI.areUniversalPatchesEnabled()) {
@@ -203,10 +212,25 @@ class PatcherViewModel extends BaseViewModel {
.selectedPatches .selectedPatches
.removeWhere((patch) => patch.compatiblePackages.isEmpty); .removeWhere((patch) => patch.compatiblePackages.isEmpty);
} }
final usedPatches = _managerAPI.getUsedPatches(selectedApp!.originalPackageName); this.selectedPatches.addAll(
for (final patch in usedPatches){ patches.where(
if (!patches.any((p) => p.name == patch.name)){ (patch) =>
removedPatches.add('\u2022 ${patch.name}'); isPatchNew(patch) &&
!patch.excluded &&
!this.selectedPatches.contains(patch),
),
);
final usedPatches = _managerAPI.getUsedPatches(selectedApp!.packageName);
for (final patch in usedPatches) {
if (!patches.any((p) => p.name == patch.name)) {
removedPatches.add('${patch.name}');
for (final option in patch.options) {
_managerAPI.clearPatchOption(
selectedApp!.packageName,
patch.name,
option.key,
);
}
} }
} }
notifyListeners(); notifyListeners();

View File

@@ -3,14 +3,12 @@ import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/manager_api.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/views/patches_selector/patches_selector_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_item.dart'; import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_floating_action_button_extended.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/ui/widgets/shared/search_bar.dart';
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
class PatchesSelectorView extends StatefulWidget { class PatchesSelectorView extends StatefulWidget {
const PatchesSelectorView({Key? key}) : super(key: key); const PatchesSelectorView({super.key});
@override @override
State<PatchesSelectorView> createState() => _PatchesSelectorViewState(); State<PatchesSelectorView> createState() => _PatchesSelectorViewState();
@@ -37,10 +35,9 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
onViewModelReady: (model) => model.initialize(), onViewModelReady: (model) => model.initialize(),
viewModelBuilder: () => PatchesSelectorViewModel(), viewModelBuilder: () => PatchesSelectorViewModel(),
builder: (context, model, child) => Scaffold( builder: (context, model, child) => Scaffold(
resizeToAvoidBottomInset: false,
floatingActionButton: Visibility( floatingActionButton: Visibility(
visible: model.patches.isNotEmpty, visible: model.patches.isNotEmpty,
child: FloatingActionButton.extended( child: HapticFloatingActionButtonExtended(
label: Row( label: Row(
children: <Widget>[ children: <Widget>[
I18nText('patchesSelectorView.doneButton'), I18nText('patchesSelectorView.doneButton'),
@@ -49,8 +46,10 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
), ),
icon: const Icon(Icons.check), icon: const Icon(Icons.check),
onPressed: () { onPressed: () {
model.selectPatches(); if (!model.areRequiredOptionsNull(context)) {
Navigator.of(context).pop(); model.selectPatches();
Navigator.of(context).pop();
}
}, },
), ),
), ),
@@ -61,60 +60,54 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
floating: true, floating: true,
title: I18nText( title: I18nText(
'patchesSelectorView.viewTitle', 'patchesSelectorView.viewTitle',
child: Text( ),
'', titleTextStyle: TextStyle(
style: TextStyle( fontSize: 22.0,
color: Theme.of(context).textTheme.titleLarge!.color, color: Theme.of(context).textTheme.titleLarge!.color,
),
),
), ),
leading: IconButton( leading: IconButton(
icon: Icon( icon: Icon(
Icons.arrow_back, Icons.arrow_back,
color: Theme.of(context).textTheme.titleLarge!.color, color: Theme.of(context).textTheme.titleLarge!.color,
), ),
onPressed: () => Navigator.of(context).pop(), onPressed: () {
model.resetSelection();
Navigator.of(context).pop();
},
), ),
actions: [ actions: [
FittedBox( Container(
fit: BoxFit.scaleDown, margin: const EdgeInsets.symmetric(vertical: 12),
child: Container( padding: const EdgeInsets.symmetric(horizontal: 6),
margin: const EdgeInsets.only(top: 12, bottom: 12), decoration: BoxDecoration(
padding: color:
const EdgeInsets.symmetric(horizontal: 6, vertical: 6), Theme.of(context).colorScheme.tertiary.withOpacity(0.5),
decoration: BoxDecoration( borderRadius: BorderRadius.circular(6),
color: Theme.of(context) ),
.colorScheme alignment: Alignment.center,
.tertiary child: Text(
.withOpacity(0.5), model.patchesVersion!,
borderRadius: BorderRadius.circular(6), style: TextStyle(
), color: Theme.of(context).textTheme.titleLarge!.color,
child: Text(
model.patchesVersion!,
style: TextStyle(
color: Theme.of(context).textTheme.titleLarge!.color,
),
), ),
), ),
), ),
CustomPopupMenu( PopupMenuButton(
onSelected: (value) => onSelected: (value) {
{model.onMenuSelection(value, context)}, model.onMenuSelection(value, context);
children: { },
0: I18nText( itemBuilder: (BuildContext context) => <PopupMenuEntry>[
'patchesSelectorView.loadPatchesSelection', PopupMenuItem(
child: const Text( value: 0,
'', child: I18nText(
style: TextStyle( 'patchesSelectorView.loadPatchesSelection',
fontWeight: FontWeight.bold,
),
), ),
), ),
}, ],
), ),
], ],
bottom: PreferredSize( bottom: PreferredSize(
preferredSize: const Size.fromHeight(64.0), preferredSize: const Size.fromHeight(66.0),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 8.0, vertical: 8.0,
@@ -188,63 +181,58 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
), ),
], ],
), ),
if (model
.getQueriedPatches(_query)
.any((patch) => model.isPatchNew(patch)))
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
model.getPatchCategory(
context,
'patchesSelectorView.newPatches',
),
...model.getQueriedPatches(_query).map((patch) {
if (model.isPatchNew(patch)) {
return model.getPatchItem(context, patch);
} else {
return Container();
}
}),
if (model.getQueriedPatches(_query).any(
(patch) =>
!model.isPatchNew(patch) &&
patch.compatiblePackages.isNotEmpty,
))
model.getPatchCategory(
context,
'patchesSelectorView.patches',
),
],
),
...model.getQueriedPatches(_query).map( ...model.getQueriedPatches(_query).map(
(patch) { (patch) {
if (patch.compatiblePackages.isNotEmpty) { if (patch.compatiblePackages.isNotEmpty &&
return PatchItem( !model.isPatchNew(patch)) {
name: patch.name, return model.getPatchItem(context, patch);
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 { } else {
return Container(); return Container();
} }
}, },
), ),
if (_managerAPI.areUniversalPatchesEnabled()) if (model
.getQueriedPatches(_query)
.any((patch) => patch.compatiblePackages.isEmpty))
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Padding( model.getPatchCategory(
padding: const EdgeInsets.symmetric( context,
vertical: 10.0, 'patchesSelectorView.universalPatches',
),
child: I18nText(
'patchesSelectorView.universalPatches',
),
), ),
...model.getQueriedPatches(_query).map((patch) { ...model.getQueriedPatches(_query).map((patch) {
if (patch.compatiblePackages.isEmpty) { if (patch.compatiblePackages.isEmpty &&
return PatchItem( !model.isPatchNew(patch)) {
name: patch.name, return model.getPatchItem(context, patch);
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,
),
);
} else { } else {
return Container(); return Container();
} }

View File

@@ -2,24 +2,29 @@ import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart'; import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/app/app.locator.dart'; 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/patch.dart';
import 'package:revanced_manager/models/patched_application.dart'; import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/patcher_api.dart'; import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/services/toast.dart'; import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_item.dart';
import 'package:revanced_manager/utils/check_for_supported_patch.dart'; import 'package:revanced_manager/utils/check_for_supported_patch.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
class PatchesSelectorViewModel extends BaseViewModel { class PatchesSelectorViewModel extends BaseViewModel {
final PatcherAPI _patcherAPI = locator<PatcherAPI>(); final PatcherAPI _patcherAPI = locator<PatcherAPI>();
final ManagerAPI _managerAPI = locator<ManagerAPI>(); final ManagerAPI _managerAPI = locator<ManagerAPI>();
final NavigationService _navigationService = locator<NavigationService>();
final List<Patch> patches = []; final List<Patch> patches = [];
final List<Patch> currentSelection = [];
final List<Patch> selectedPatches = final List<Patch> selectedPatches =
locator<PatcherViewModel>().selectedPatches; locator<PatcherViewModel>().selectedPatches;
PatchedApplication? selectedApp = locator<PatcherViewModel>().selectedApp; PatchedApplication? selectedApp = locator<PatcherViewModel>().selectedApp;
String? patchesVersion = ''; String? patchesVersion = '';
bool isDefaultPatchesRepo() { bool isDefaultPatchesRepo() {
return _managerAPI.getPatchesRepo() == 'revanced/revanced-patches'; return _managerAPI.getPatchesRepo() == 'revanced/revanced-patches';
} }
@@ -28,17 +33,22 @@ class PatchesSelectorViewModel extends BaseViewModel {
getPatchesVersion().whenComplete(() => notifyListeners()); getPatchesVersion().whenComplete(() => notifyListeners());
patches.addAll( patches.addAll(
_patcherAPI.getFilteredPatches( _patcherAPI.getFilteredPatches(
selectedApp!.originalPackageName, selectedApp!.packageName,
), ),
); );
final List<Option> requiredNullOptions =
getNullRequiredOptions(patches, selectedApp!.packageName);
patches.sort((a, b) { patches.sort((a, b) {
if (isPatchNew(a, selectedApp!.packageName) == if (b.options.any((option) => requiredNullOptions.contains(option)) &&
isPatchNew(b, selectedApp!.packageName)) { a.options.isEmpty) {
return a.name.compareTo(b.name); return 1;
} else { } else {
return isPatchNew(b, selectedApp!.packageName) ? 1 : -1; return a.name.compareTo(b.name);
} }
}); });
currentSelection.clear();
currentSelection.addAll(selectedPatches);
notifyListeners(); notifyListeners();
} }
@@ -48,6 +58,59 @@ class PatchesSelectorViewModel extends BaseViewModel {
); );
} }
void navigateToPatchOptions(List<Option> setOptions, Patch patch) {
_managerAPI.options = setOptions;
_managerAPI.selectedPatch = patch;
_managerAPI.modifiedOptions.clear();
_navigationService.navigateToPatchOptionsView();
}
bool areRequiredOptionsNull(BuildContext context) {
final List<String> patchesWithNullRequiredOptions = [];
final List<Option> requiredNullOptions =
getNullRequiredOptions(selectedPatches, selectedApp!.packageName);
if (requiredNullOptions.isNotEmpty) {
for (final patch in selectedPatches) {
for (final patchOption in patch.options) {
if (requiredNullOptions.contains(patchOption)) {
patchesWithNullRequiredOptions.add(patch.name);
break;
}
}
}
showSetRequiredOption(context, patchesWithNullRequiredOptions);
return true;
}
return false;
}
Future<void> showSetRequiredOption(
BuildContext context,
List<String> patches,
) async {
return showDialog(
barrierDismissible: false,
context: context,
builder: (context) => AlertDialog(
title: I18nText('notice'),
content: I18nText(
'patchesSelectorView.setRequiredOption',
translationParams: {
'patches': patches.map((patch) => '$patch').join('\n'),
},
),
actions: <Widget>[
FilledButton(
onPressed: () => {
Navigator.of(context).pop(),
},
child: I18nText('okButton'),
),
],
),
);
}
void selectPatch(Patch patch, bool isSelected, BuildContext context) { void selectPatch(Patch patch, bool isSelected, BuildContext context) {
if (_managerAPI.isPatchesChangeEnabled()) { if (_managerAPI.isPatchesChangeEnabled()) {
if (isSelected && !selectedPatches.contains(patch)) { if (isSelected && !selectedPatches.contains(patch)) {
@@ -65,7 +128,6 @@ class PatchesSelectorViewModel extends BaseViewModel {
return showDialog( return showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
title: I18nText('warning'), title: I18nText('warning'),
content: I18nText( content: I18nText(
'patchItem.patchesChangeWarningDialogText', 'patchItem.patchesChangeWarningDialogText',
@@ -78,18 +140,17 @@ class PatchesSelectorViewModel extends BaseViewModel {
), ),
), ),
actions: [ actions: [
CustomMaterialButton( TextButton(
isFilled: false,
label: I18nText('okButton'),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: I18nText('okButton'),
), ),
CustomMaterialButton( FilledButton(
label: I18nText('patchItem.patchesChangeWarningDialogButton'),
onPressed: () { onPressed: () {
Navigator.of(context) Navigator.of(context)
..pop() ..pop()
..pop(); ..pop();
}, },
child: I18nText('patchItem.patchesChangeWarningDialogButton'),
), ),
], ],
), ),
@@ -98,16 +159,16 @@ class PatchesSelectorViewModel extends BaseViewModel {
void selectDefaultPatches() { void selectDefaultPatches() {
selectedPatches.clear(); selectedPatches.clear();
if (locator<PatcherViewModel>().selectedApp?.originalPackageName != null) { if (locator<PatcherViewModel>().selectedApp?.packageName != null) {
selectedPatches.addAll( selectedPatches.addAll(
_patcherAPI _patcherAPI
.getFilteredPatches( .getFilteredPatches(
locator<PatcherViewModel>().selectedApp!.originalPackageName, locator<PatcherViewModel>().selectedApp!.packageName,
) )
.where( .where(
(element) => (element) =>
!element.excluded && !element.excluded &&
(_managerAPI.areExperimentalPatchesEnabled() || (!_managerAPI.isVersionCompatibilityCheckEnabled() ||
isPatchSupported(element)), isPatchSupported(element)),
), ),
); );
@@ -126,6 +187,12 @@ class PatchesSelectorViewModel extends BaseViewModel {
locator<PatcherViewModel>().notifyListeners(); locator<PatcherViewModel>().notifyListeners();
} }
void resetSelection() {
selectedPatches.clear();
selectedPatches.addAll(currentSelection);
notifyListeners();
}
Future<void> getPatchesVersion() async { Future<void> getPatchesVersion() async {
patchesVersion = await _managerAPI.getCurrentPatchesVersion(); patchesVersion = await _managerAPI.getCurrentPatchesVersion();
} }
@@ -137,7 +204,10 @@ class PatchesSelectorViewModel extends BaseViewModel {
query.isEmpty || query.isEmpty ||
query.length < 2 || query.length < 2 ||
patch.name.toLowerCase().contains(query.toLowerCase()) || patch.name.toLowerCase().contains(query.toLowerCase()) ||
patch.getSimpleName().toLowerCase().contains(query.toLowerCase()), patch.name
.replaceAll(RegExp(r'[^\w\s]+'), '')
.toLowerCase()
.contains(query.toLowerCase()),
) )
.toList(); .toList();
if (_managerAPI.areUniversalPatchesEnabled()) { if (_managerAPI.areUniversalPatchesEnabled()) {
@@ -149,18 +219,63 @@ class PatchesSelectorViewModel extends BaseViewModel {
} }
} }
Widget getPatchItem(BuildContext context, Patch patch) {
return PatchItem(
name: patch.name,
simpleName: patch.getSimpleName(),
description: patch.description ?? '',
packageVersion: getAppInfo().version,
supportedPackageVersions: getSupportedVersions(patch),
isUnsupported: !isPatchSupported(patch),
isChangeEnabled: _managerAPI.isPatchesChangeEnabled(),
hasUnsupportedPatchOption: hasUnsupportedRequiredOption(
patch.options,
patch,
),
options: patch.options,
isSelected: isSelected(patch),
navigateToOptions: (options) => navigateToPatchOptions(
options,
patch,
),
onChanged: (value) => selectPatch(
patch,
value,
context,
),
);
}
Widget getPatchCategory(BuildContext context, String category) {
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 10.0,
),
child: Container(
padding: const EdgeInsets.only(
top: 10.0,
bottom: 10.0,
left: 5.0,
),
child: I18nText(
category,
child: Text(
'',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
),
),
),
);
}
PatchedApplication getAppInfo() { PatchedApplication getAppInfo() {
return locator<PatcherViewModel>().selectedApp!; return locator<PatcherViewModel>().selectedApp!;
} }
bool isPatchNew(Patch patch, String packageName) { bool isPatchNew(Patch patch) {
final List<Patch> savedPatches = _managerAPI.getSavedPatches(packageName); return locator<PatcherViewModel>().isPatchNew(patch);
if (savedPatches.isEmpty) {
return false;
} else {
return !savedPatches
.any((p) => p.getSimpleName() == patch.getSimpleName());
}
} }
List<String> getSupportedVersions(Patch patch) { List<String> getSupportedVersions(Patch patch) {
@@ -187,7 +302,7 @@ class PatchesSelectorViewModel extends BaseViewModel {
final List<String> selectedPatches = final List<String> selectedPatches =
this.selectedPatches.map((patch) => patch.name).toList(); this.selectedPatches.map((patch) => patch.name).toList();
await _managerAPI.setSelectedPatches( await _managerAPI.setSelectedPatches(
locator<PatcherViewModel>().selectedApp!.originalPackageName, locator<PatcherViewModel>().selectedApp!.packageName,
selectedPatches, selectedPatches,
); );
} }
@@ -195,14 +310,14 @@ class PatchesSelectorViewModel extends BaseViewModel {
Future<void> loadSelectedPatches(BuildContext context) async { Future<void> loadSelectedPatches(BuildContext context) async {
if (_managerAPI.isPatchesChangeEnabled()) { if (_managerAPI.isPatchesChangeEnabled()) {
final List<String> selectedPatches = await _managerAPI.getSelectedPatches( final List<String> selectedPatches = await _managerAPI.getSelectedPatches(
locator<PatcherViewModel>().selectedApp!.originalPackageName, locator<PatcherViewModel>().selectedApp!.packageName,
); );
if (selectedPatches.isNotEmpty) { if (selectedPatches.isNotEmpty) {
this.selectedPatches.clear(); this.selectedPatches.clear();
this.selectedPatches.addAll( this.selectedPatches.addAll(
patches.where((patch) => selectedPatches.contains(patch.name)), patches.where((patch) => selectedPatches.contains(patch.name)),
); );
if (!_managerAPI.areExperimentalPatchesEnabled()) { if (_managerAPI.isVersionCompatibilityCheckEnabled()) {
this.selectedPatches.removeWhere((patch) => !isPatchSupported(patch)); this.selectedPatches.removeWhere((patch) => !isPatchSupported(patch));
} }
} else { } else {

View File

@@ -4,15 +4,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/manager_api.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/settingsView/settings_tile_dialog.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
class SManageApiUrl extends BaseViewModel { class SManageApiUrl extends BaseViewModel {
final ManagerAPI _managerAPI = locator<ManagerAPI>(); final ManagerAPI _managerAPI = locator<ManagerAPI>();
final Toast _toast = locator<Toast>();
final TextEditingController _apiUrlController = TextEditingController(); final TextEditingController _apiUrlController = TextEditingController();
@@ -33,34 +29,38 @@ class SManageApiUrl extends BaseViewModel {
), ),
], ],
), ),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: SingleChildScrollView( content: SingleChildScrollView(
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
CustomTextField( TextField(
leadingIcon: Icon( controller: _apiUrlController,
Icons.api_outlined, autocorrect: false,
color: Theme.of(context).colorScheme.secondary,
),
inputController: _apiUrlController,
label: I18nText('settingsView.selectApiURL'),
hint: apiUrl,
onChanged: (value) => notifyListeners(), onChanged: (value) => notifyListeners(),
decoration: InputDecoration(
icon: Icon(
Icons.api_outlined,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
border: const OutlineInputBorder(),
labelText: FlutterI18n.translate(
context,
'settingsView.selectApiURL',
),
hintText: apiUrl,
),
), ),
], ],
), ),
), ),
actions: <Widget>[ actions: <Widget>[
CustomMaterialButton( TextButton(
isFilled: false,
label: I18nText('cancelButton'),
onPressed: () { onPressed: () {
_apiUrlController.clear(); _apiUrlController.clear();
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: I18nText('cancelButton'),
), ),
CustomMaterialButton( FilledButton(
label: I18nText('okButton'),
onPressed: () { onPressed: () {
String apiUrl = _apiUrlController.text; String apiUrl = _apiUrlController.text;
if (!apiUrl.startsWith('https')) { if (!apiUrl.startsWith('https')) {
@@ -69,6 +69,7 @@ class SManageApiUrl extends BaseViewModel {
_managerAPI.setApiUrl(apiUrl); _managerAPI.setApiUrl(apiUrl);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: I18nText('okButton'),
), ),
], ],
), ),
@@ -80,23 +81,20 @@ class SManageApiUrl extends BaseViewModel {
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: I18nText('settingsView.sourcesResetDialogTitle'), title: I18nText('settingsView.sourcesResetDialogTitle'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('settingsView.apiURLResetDialogText'), content: I18nText('settingsView.apiURLResetDialogText'),
actions: <Widget>[ actions: <Widget>[
CustomMaterialButton( TextButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: I18nText('noButton'),
), ),
CustomMaterialButton( FilledButton(
label: I18nText('yesButton'),
onPressed: () { onPressed: () {
_managerAPI.setApiUrl(''); _managerAPI.setApiUrl('');
_toast.showBottom('settingsView.restartAppForChanges');
Navigator.of(context) Navigator.of(context)
..pop() ..pop()
..pop(); ..pop();
}, },
child: I18nText('yesButton'),
), ),
], ],
), ),

View File

@@ -4,9 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/manager_api.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/settingsView/settings_tile_dialog.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
class SManageKeystorePassword extends BaseViewModel { class SManageKeystorePassword extends BaseViewModel {
@@ -33,35 +31,40 @@ class SManageKeystorePassword extends BaseViewModel {
), ),
], ],
), ),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: SingleChildScrollView( content: SingleChildScrollView(
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
CustomTextField( TextField(
inputController: _keystorePasswordController, controller: _keystorePasswordController,
label: I18nText('settingsView.selectKeystorePassword'), autocorrect: false,
hint: '', obscureText: true,
onChanged: (value) => notifyListeners(), onChanged: (value) => notifyListeners(),
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: FlutterI18n.translate(
context,
'settingsView.selectKeystorePassword',
),
),
), ),
], ],
), ),
), ),
actions: <Widget>[ actions: <Widget>[
CustomMaterialButton( TextButton(
isFilled: false,
label: I18nText('cancelButton'),
onPressed: () { onPressed: () {
_keystorePasswordController.clear(); _keystorePasswordController.clear();
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: I18nText('cancelButton'),
), ),
CustomMaterialButton( FilledButton(
label: I18nText('okButton'),
onPressed: () { onPressed: () {
final String passwd = _keystorePasswordController.text; final String passwd = _keystorePasswordController.text;
_managerAPI.setKeystorePassword(passwd); _managerAPI.setKeystorePassword(passwd);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: I18nText('okButton'),
), ),
], ],
), ),

View File

@@ -5,9 +5,7 @@ import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/toast.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/settingsView/settings_tile_dialog.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
class SManageSources extends BaseViewModel { class SManageSources extends BaseViewModel {
@@ -43,63 +41,105 @@ class SManageSources extends BaseViewModel {
), ),
], ],
), ),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: SingleChildScrollView( content: SingleChildScrollView(
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
CustomTextField( /*
leadingIcon: const Icon( API for accessing the specified repositories
Icons.extension_outlined, If default is used, will use the ReVanced API
color: Colors.transparent, */
), TextField(
inputController: _hostSourceController, controller: _hostSourceController,
label: I18nText('settingsView.hostRepositoryLabel'), autocorrect: false,
hint: hostRepository,
onChanged: (value) => notifyListeners(), onChanged: (value) => notifyListeners(),
), decoration: InputDecoration(
const SizedBox(height: 20), icon: Icon(
CustomTextField( Icons.rocket_launch_outlined,
leadingIcon: Icon( color: Theme.of(context).colorScheme.onSurfaceVariant,
Icons.extension_outlined, ),
color: Theme.of(context).colorScheme.secondary, border: const OutlineInputBorder(),
labelText: FlutterI18n.translate(
context,
'settingsView.hostRepositoryLabel',
),
hintText: hostRepository,
), ),
inputController: _orgPatSourceController,
label: I18nText('settingsView.orgPatchesLabel'),
hint: patchesRepo.split('/')[0],
onChanged: (value) => notifyListeners(),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
CustomTextField( // Patches owner's name
leadingIcon: const Icon( TextField(
Icons.extension_outlined, controller: _orgPatSourceController,
color: Colors.transparent, autocorrect: false,
),
inputController: _patSourceController,
label: I18nText('settingsView.sourcesPatchesLabel'),
hint: patchesRepo.split('/')[1],
onChanged: (value) => notifyListeners(), onChanged: (value) => notifyListeners(),
), decoration: InputDecoration(
const SizedBox(height: 20), icon: Icon(
CustomTextField( Icons.extension_outlined,
leadingIcon: Icon( color: Theme.of(context).colorScheme.onSurfaceVariant,
Icons.merge_outlined, ),
color: Theme.of(context).colorScheme.secondary, border: const OutlineInputBorder(),
labelText: FlutterI18n.translate(
context,
'settingsView.orgPatchesLabel',
),
hintText: patchesRepo.split('/')[0],
), ),
inputController: _orgIntSourceController,
label: I18nText('settingsView.orgIntegrationsLabel'),
hint: integrationsRepo.split('/')[0],
onChanged: (value) => notifyListeners(),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
CustomTextField( // Patches repository's name
leadingIcon: const Icon( TextField(
Icons.merge_outlined, controller: _patSourceController,
color: Colors.transparent, autocorrect: false,
),
inputController: _intSourceController,
label: I18nText('settingsView.sourcesIntegrationsLabel'),
hint: integrationsRepo.split('/')[1],
onChanged: (value) => notifyListeners(), onChanged: (value) => notifyListeners(),
decoration: InputDecoration(
icon: const Icon(
Icons.extension_outlined,
color: Colors.transparent,
),
border: const OutlineInputBorder(),
labelText: FlutterI18n.translate(
context,
'settingsView.sourcesPatchesLabel',
),
hintText: patchesRepo.split('/')[1],
),
),
const SizedBox(height: 8),
// Integrations owner's name
TextField(
controller: _orgIntSourceController,
autocorrect: false,
onChanged: (value) => notifyListeners(),
decoration: InputDecoration(
icon: Icon(
Icons.merge_outlined,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
border: const OutlineInputBorder(),
labelText: FlutterI18n.translate(
context,
'settingsView.orgIntegrationsLabel',
),
hintText: integrationsRepo.split('/')[0],
),
),
const SizedBox(height: 8),
// Integrations repository's name
TextField(
controller: _intSourceController,
autocorrect: false,
onChanged: (value) => notifyListeners(),
decoration: InputDecoration(
icon: const Icon(
Icons.merge_outlined,
color: Colors.transparent,
),
border: const OutlineInputBorder(),
labelText: FlutterI18n.translate(
context,
'settingsView.sourcesIntegrationsLabel',
),
hintText: integrationsRepo.split('/')[1],
),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
I18nText('settingsView.sourcesUpdateNote'), I18nText('settingsView.sourcesUpdateNote'),
@@ -107,9 +147,7 @@ class SManageSources extends BaseViewModel {
), ),
), ),
actions: <Widget>[ actions: <Widget>[
CustomMaterialButton( TextButton(
isFilled: false,
label: I18nText('cancelButton'),
onPressed: () { onPressed: () {
_orgPatSourceController.clear(); _orgPatSourceController.clear();
_patSourceController.clear(); _patSourceController.clear();
@@ -117,9 +155,9 @@ class SManageSources extends BaseViewModel {
_intSourceController.clear(); _intSourceController.clear();
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: I18nText('cancelButton'),
), ),
CustomMaterialButton( FilledButton(
label: I18nText('okButton'),
onPressed: () { onPressed: () {
_managerAPI.setRepoUrl(_hostSourceController.text.trim()); _managerAPI.setRepoUrl(_hostSourceController.text.trim());
_managerAPI.setPatchesRepo( _managerAPI.setPatchesRepo(
@@ -133,6 +171,7 @@ class SManageSources extends BaseViewModel {
_toast.showBottom('settingsView.restartAppForChanges'); _toast.showBottom('settingsView.restartAppForChanges');
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: I18nText('okButton'),
), ),
], ],
), ),
@@ -144,16 +183,13 @@ class SManageSources extends BaseViewModel {
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: I18nText('settingsView.sourcesResetDialogTitle'), title: I18nText('settingsView.sourcesResetDialogTitle'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('settingsView.sourcesResetDialogText'), content: I18nText('settingsView.sourcesResetDialogText'),
actions: <Widget>[ actions: <Widget>[
CustomMaterialButton( TextButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: I18nText('noButton'),
), ),
CustomMaterialButton( FilledButton(
label: I18nText('yesButton'),
onPressed: () { onPressed: () {
_managerAPI.setRepoUrl(''); _managerAPI.setRepoUrl('');
_managerAPI.setPatchesRepo(''); _managerAPI.setPatchesRepo('');
@@ -165,6 +201,7 @@ class SManageSources extends BaseViewModel {
..pop() ..pop()
..pop(); ..pop();
}, },
child: I18nText('yesButton'),
), ),
], ],
), ),

View File

@@ -51,7 +51,6 @@ class SUpdateLanguage extends BaseViewModel {
context: parentContext, context: parentContext,
builder: (context) => SimpleDialog( builder: (context) => SimpleDialog(
title: I18nText('settingsView.languageLabel'), title: I18nText('settingsView.languageLabel'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
children: [ children: [
SizedBox( SizedBox(
height: 500, height: 500,

View File

@@ -6,67 +6,29 @@ import 'package:flutter/services.dart';
import 'package:flutter_i18n/widgets/I18nText.dart'; import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/manager_api.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:revanced_manager/ui/widgets/settingsView/settings_section.dart';
import 'package:stacked/stacked.dart'; import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_radio_list_tile.dart';
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart';
final _settingViewModel = SettingsViewModel(); class SUpdateThemeUI extends StatefulWidget {
const SUpdateThemeUI({super.key});
// ignore: constant_identifier_names @override
const int ANDROID_12_SDK_VERSION = 31; State<SUpdateThemeUI> createState() => _SUpdateThemeUIState();
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 { class _SUpdateThemeUIState extends State<SUpdateThemeUI> {
const SUpdateThemeUI({super.key}); final ManagerAPI managerAPI = locator<ManagerAPI>();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SettingsSection( return SettingsSection(
title: 'settingsView.appearanceSectionTitle', title: 'settingsView.appearanceSectionTitle',
children: <Widget>[ children: <Widget>[
SwitchListTile( ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText( title: I18nText(
'settingsView.darkThemeLabel', 'settingsView.themeModeLabel',
child: const Text( child: const Text(
'', '',
style: TextStyle( style: TextStyle(
@@ -75,42 +37,148 @@ class SUpdateThemeUI extends StatelessWidget {
), ),
), ),
), ),
subtitle: I18nText('settingsView.darkThemeHint'), trailing: FilledButton(
value: SUpdateTheme().getDarkThemeStatus(), onPressed: () => {showThemeDialog(context)},
onChanged: (value) => SUpdateTheme().setUseDarkTheme( child: getThemeModeName(),
context,
value,
), ),
onTap: () => {showThemeDialog(context)},
), ),
FutureBuilder<int>( if (managerAPI.isDynamicThemeAvailable)
future: _settingViewModel.getSdkVersion(), HapticSwitchListTile(
builder: (context, snapshot) => Visibility( contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
visible: title: I18nText(
snapshot.hasData && snapshot.data! >= ANDROID_12_SDK_VERSION, 'settingsView.dynamicThemeLabel',
child: SwitchListTile( child: const Text(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), '',
title: I18nText( style: TextStyle(
'settingsView.dynamicThemeLabel', fontSize: 20,
child: const Text( fontWeight: FontWeight.w500,
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
), ),
), ),
subtitle: I18nText('settingsView.dynamicThemeHint'),
value: _settingViewModel.sUpdateTheme.getDynamicThemeStatus(),
onChanged: (value) => {
_settingViewModel.sUpdateTheme.setUseDynamicTheme(
context,
value,
),
},
), ),
subtitle: I18nText('settingsView.dynamicThemeHint'),
value: getDynamicThemeStatus(),
onChanged: (value) => {
setUseDynamicTheme(
context,
value,
),
},
), ),
),
], ],
); );
} }
bool getDynamicThemeStatus() {
return managerAPI.getUseDynamicTheme();
}
Future<void> setUseDynamicTheme(BuildContext context, bool value) async {
await managerAPI.setUseDynamicTheme(value);
final int currentTheme = (DynamicTheme.of(context)!.themeId ~/ 2) * 2;
await DynamicTheme.of(context)!.setTheme(currentTheme + (value ? 1 : 0));
setState(() {});
}
int getThemeMode() {
return managerAPI.getThemeMode();
}
Future<void> setThemeMode(BuildContext context, int value) async {
await managerAPI.setThemeMode(value);
final bool isDynamicTheme = DynamicTheme.of(context)!.themeId.isEven;
await DynamicTheme.of(context)!
.setTheme(value * 2 + (isDynamicTheme ? 0 : 1));
final bool isLight = value != 2 &&
(value == 1 ||
DynamicTheme.of(context)!.theme.brightness == Brightness.light);
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
systemNavigationBarIconBrightness:
isLight ? Brightness.dark : Brightness.light,
),
);
setState(() {});
}
I18nText getThemeModeName() {
switch (getThemeMode()) {
case 0:
return I18nText('settingsView.systemThemeLabel');
case 1:
return I18nText('settingsView.lightThemeLabel');
case 2:
return I18nText('settingsView.darkThemeLabel');
default:
return I18nText('settingsView.systemThemeLabel');
}
}
Future<void> showThemeDialog(BuildContext context) async {
final ValueNotifier<int> newTheme = ValueNotifier(getThemeMode());
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('settingsView.themeModeLabel'),
icon: const Icon(Icons.palette),
contentPadding: const EdgeInsets.symmetric(vertical: 16),
content: SingleChildScrollView(
child: ValueListenableBuilder(
valueListenable: newTheme,
builder: (context, value, child) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
HapticRadioListTile(
title: I18nText('settingsView.systemThemeLabel'),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
value: 0,
groupValue: value,
onChanged: (value) {
newTheme.value = value!;
},
),
HapticRadioListTile(
title: I18nText('settingsView.lightThemeLabel'),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
value: 1,
groupValue: value,
onChanged: (value) {
newTheme.value = value!;
},
),
HapticRadioListTile(
title: I18nText('settingsView.darkThemeLabel'),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
value: 2,
groupValue: value,
onChanged: (value) {
newTheme.value = value!;
},
),
],
);
},
),
),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: I18nText('cancelButton'),
),
FilledButton(
onPressed: () {
setThemeMode(context, newTheme.value);
Navigator.of(context).pop();
},
child: I18nText('okButton'),
),
],
),
);
}
} }

View File

@@ -6,14 +6,14 @@ 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/settingsFragment/settings_update_theme.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_advanced_section.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_advanced_section.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_debug_section.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_export_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/settingsView/settings_team_section.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
class SettingsView extends StatelessWidget { class SettingsView extends StatelessWidget {
const SettingsView({Key? key}) : super(key: key); const SettingsView({super.key});
static const _settingsDivider = static const _settingsDivider =
Divider(thickness: 1.0, indent: 20.0, endIndent: 20.0); Divider(thickness: 1.0, indent: 20.0, endIndent: 20.0);
@@ -40,16 +40,24 @@ class SettingsView extends StatelessWidget {
SliverList( SliverList(
delegate: SliverChildListDelegate.fixed( delegate: SliverChildListDelegate.fixed(
<Widget>[ <Widget>[
SUpdateThemeUI(), ListView(
// SUpdateLanguageUI(), padding: EdgeInsets.zero,
// _settingsDivider, shrinkWrap: true,
STeamSection(), physics: NeverScrollableScrollPhysics(),
_settingsDivider, children: const [
SAdvancedSection(), SUpdateThemeUI(),
_settingsDivider, // _settingsDivider,
SExportSection(), // SUpdateLanguageUI(),
_settingsDivider, _settingsDivider,
SInfoSection(), SAdvancedSection(),
_settingsDivider,
SExportSection(),
_settingsDivider,
STeamSection(),
_settingsDivider,
SDebugSection(),
],
),
], ],
), ),
), ),

View File

@@ -1,9 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:cr_file_saver/file_saver.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:logcat/logcat.dart'; import 'package:logcat/logcat.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@@ -14,9 +12,7 @@ import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.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/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_language.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_update_theme.dart'; import 'package:share_plus/share_plus.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/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
@@ -29,7 +25,6 @@ class SettingsViewModel extends BaseViewModel {
final Toast _toast = locator<Toast>(); final Toast _toast = locator<Toast>();
final SUpdateLanguage sUpdateLanguage = SUpdateLanguage(); final SUpdateLanguage sUpdateLanguage = SUpdateLanguage();
final SUpdateTheme sUpdateTheme = SUpdateTheme();
void navigateToContributors() { void navigateToContributors() {
_navigationService.navigateTo(Routes.contributorsView); _navigationService.navigateTo(Routes.contributorsView);
@@ -56,7 +51,6 @@ class SettingsViewModel extends BaseViewModel {
return showDialog( return showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
title: I18nText('warning'), title: I18nText('warning'),
content: I18nText( content: I18nText(
'settingsView.enablePatchesSelectionWarningText', 'settingsView.enablePatchesSelectionWarningText',
@@ -69,20 +63,19 @@ class SettingsViewModel extends BaseViewModel {
), ),
), ),
actions: [ actions: [
CustomMaterialButton( TextButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () {
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('yesButton'),
onPressed: () { onPressed: () {
_managerAPI.setChangingToggleModified(true); _managerAPI.setChangingToggleModified(true);
_managerAPI.setPatchesChangeEnabled(true); _managerAPI.setPatchesChangeEnabled(true);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: I18nText('yesButton'),
),
FilledButton(
onPressed: () {
Navigator.of(context).pop();
},
child: I18nText('noButton'),
), ),
], ],
), ),
@@ -91,7 +84,6 @@ class SettingsViewModel extends BaseViewModel {
return showDialog( return showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
title: I18nText('warning'), title: I18nText('warning'),
content: I18nText( content: I18nText(
'settingsView.disablePatchesSelectionWarningText', 'settingsView.disablePatchesSelectionWarningText',
@@ -104,21 +96,20 @@ class SettingsViewModel extends BaseViewModel {
), ),
), ),
actions: [ actions: [
CustomMaterialButton( TextButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: I18nText('noButton'),
), ),
CustomMaterialButton( FilledButton(
label: I18nText('yesButton'),
onPressed: () { onPressed: () {
_managerAPI.setChangingToggleModified(true); _managerAPI.setChangingToggleModified(true);
_patchesSelectorViewModel.selectDefaultPatches(); _patchesSelectorViewModel.selectDefaultPatches();
_managerAPI.setPatchesChangeEnabled(false); _managerAPI.setPatchesChangeEnabled(false);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: I18nText('yesButton'),
), ),
], ],
), ),
@@ -135,15 +126,66 @@ class SettingsViewModel extends BaseViewModel {
notifyListeners(); notifyListeners();
} }
bool areExperimentalPatchesEnabled() { bool isVersionCompatibilityCheckEnabled() {
return _managerAPI.areExperimentalPatchesEnabled(); return _managerAPI.isVersionCompatibilityCheckEnabled();
} }
void useExperimentalPatches(bool value) { void useVersionCompatibilityCheck(bool value) {
_managerAPI.enableExperimentalPatchesStatus(value); _managerAPI.enableVersionCompatibilityCheckStatus(value);
notifyListeners(); notifyListeners();
} }
bool isRequireSuggestedAppVersionEnabled() {
return _managerAPI.isRequireSuggestedAppVersionEnabled();
}
Future<void>? showRequireSuggestedAppVersionDialog(
BuildContext context,
bool value,
) {
if (!value) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('warning'),
content: I18nText(
'settingsView.requireSuggestedAppVersionDialogText',
child: const Text(
'',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
actions: [
TextButton(
onPressed: () {
_managerAPI.enableRequireSuggestedAppVersionStatus(false);
Navigator.of(context).pop();
},
child: I18nText('yesButton'),
),
FilledButton(
onPressed: () {
Navigator.of(context).pop();
},
child: I18nText('noButton'),
),
],
),
);
} else {
_managerAPI.enableRequireSuggestedAppVersionStatus(true);
if (!_managerAPI.suggestedAppVersionSelected) {
_patcherViewModel.selectedApp = null;
}
return null;
}
}
void deleteKeystore() { void deleteKeystore() {
_managerAPI.deleteKeystore(); _managerAPI.deleteKeystore();
_toast.showBottom('settingsView.regeneratedKeystore'); _toast.showBottom('settingsView.regeneratedKeystore');
@@ -162,10 +204,10 @@ class SettingsViewModel extends BaseViewModel {
if (outFile.existsSync()) { if (outFile.existsSync()) {
final String dateTime = final String dateTime =
DateTime.now().toString().replaceAll(' ', '_').split('.').first; DateTime.now().toString().replaceAll(' ', '_').split('.').first;
await CRFileSaver.saveFileWithDialog( await FlutterFileDialog.saveFile(
SaveFileDialogParams( params: SaveFileDialogParams(
sourceFilePath: outFile.path, sourceFilePath: outFile.path,
destinationFileName: 'selected_patches_$dateTime.json', fileName: 'selected_patches_$dateTime.json',
), ),
); );
_toast.showBottom('settingsView.exportedPatches'); _toast.showBottom('settingsView.exportedPatches');
@@ -182,12 +224,13 @@ class SettingsViewModel extends BaseViewModel {
Future<void> importPatches(BuildContext context) async { Future<void> importPatches(BuildContext context) async {
if (isPatchesChangeEnabled()) { if (isPatchesChangeEnabled()) {
try { try {
final FilePickerResult? result = await FilePicker.platform.pickFiles( final String? result = await FlutterFileDialog.pickFile(
type: FileType.custom, params: const OpenFileDialogParams(
allowedExtensions: ['json'], fileExtensionsFilter: ['json'],
),
); );
if (result != null && result.files.single.path != null) { if (result != null) {
final File inFile = File(result.files.single.path!); final File inFile = File(result);
inFile.copySync(_managerAPI.storedPatchesFile); inFile.copySync(_managerAPI.storedPatchesFile);
inFile.delete(); inFile.delete();
if (_patcherViewModel.selectedApp != null) { if (_patcherViewModel.selectedApp != null) {
@@ -212,10 +255,10 @@ class SettingsViewModel extends BaseViewModel {
if (outFile.existsSync()) { if (outFile.existsSync()) {
final String dateTime = final String dateTime =
DateTime.now().toString().replaceAll(' ', '_').split('.').first; DateTime.now().toString().replaceAll(' ', '_').split('.').first;
await CRFileSaver.saveFileWithDialog( await FlutterFileDialog.saveFile(
SaveFileDialogParams( params: SaveFileDialogParams(
sourceFilePath: outFile.path, sourceFilePath: outFile.path,
destinationFileName: 'keystore_$dateTime.keystore', fileName: 'keystore_$dateTime.keystore',
), ),
); );
_toast.showBottom('settingsView.exportedKeystore'); _toast.showBottom('settingsView.exportedKeystore');
@@ -231,9 +274,9 @@ class SettingsViewModel extends BaseViewModel {
Future<void> importKeystore() async { Future<void> importKeystore() async {
try { try {
final FilePickerResult? result = await FilePicker.platform.pickFiles(); final String? result = await FlutterFileDialog.pickFile();
if (result != null && result.files.single.path != null) { if (result != null) {
final File inFile = File(result.files.single.path!); final File inFile = File(result);
inFile.copySync(_managerAPI.keystoreFile); inFile.copySync(_managerAPI.keystoreFile);
_toast.showBottom('settingsView.importedKeystore'); _toast.showBottom('settingsView.importedKeystore');
@@ -246,16 +289,16 @@ class SettingsViewModel extends BaseViewModel {
} }
} }
void resetAllOptions() {
_managerAPI.resetAllOptions();
_toast.showBottom('settingsView.resetStoredOptions');
}
void resetSelectedPatches() { void resetSelectedPatches() {
_managerAPI.resetLastSelectedPatches(); _managerAPI.resetLastSelectedPatches();
_toast.showBottom('settingsView.resetStoredPatches'); _toast.showBottom('settingsView.resetStoredPatches');
} }
Future<int> getSdkVersion() async {
final AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo;
return info.version.sdkInt;
}
Future<void> deleteLogs() async { Future<void> deleteLogs() async {
final Directory appCacheDir = await getTemporaryDirectory(); final Directory appCacheDir = await getTemporaryDirectory();
final Directory logsDir = Directory('${appCacheDir.path}/logs'); final Directory logsDir = Directory('${appCacheDir.path}/logs');
@@ -279,6 +322,6 @@ class SettingsViewModel extends BaseViewModel {
File('${logDir.path}/revanced-manager_logcat_$dateTime.log'); File('${logDir.path}/revanced-manager_logcat_$dateTime.log');
final String logs = await Logcat.execute(); final String logs = await Logcat.execute();
logcat.writeAsStringSync(logs); logcat.writeAsStringSync(logs);
ShareExtend.share(logcat.path, 'file'); await Share.shareXFiles([XFile(logcat.path)]);
} }
} }

View File

@@ -9,9 +9,9 @@ import 'package:stacked/stacked.dart';
class AppInfoView extends StatelessWidget { class AppInfoView extends StatelessWidget {
const AppInfoView({ const AppInfoView({
Key? key, super.key,
required this.app, required this.app,
}) : super(key: key); });
final PatchedApplication app; final PatchedApplication app;
@override @override
@@ -184,7 +184,7 @@ class AppInfoView extends StatelessWidget {
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
I18nText( I18nText(
'appInfoView.unpatchButton', 'appInfoView.unmountButton',
child: Text( child: Text(
'', '',
style: TextStyle( style: TextStyle(
@@ -222,22 +222,6 @@ class AppInfoView extends StatelessWidget {
subtitle: Text(app.packageName), subtitle: Text(app.packageName),
), ),
const SizedBox(height: 4), 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( ListTile(
contentPadding: contentPadding:
const EdgeInsets.symmetric(horizontal: 20.0), const EdgeInsets.symmetric(horizontal: 20.0),
@@ -252,8 +236,8 @@ class AppInfoView extends StatelessWidget {
), ),
), ),
subtitle: app.isRooted subtitle: app.isRooted
? I18nText('appInfoView.rootTypeLabel') ? I18nText('appInfoView.mountTypeLabel')
: I18nText('appInfoView.nonRootTypeLabel'), : I18nText('appInfoView.regularTypeLabel'),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
ListTile( ListTile(

View File

@@ -12,8 +12,6 @@ import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/home/home_viewmodel.dart'; import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart'; import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/utils/string.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
class AppInfoViewModel extends BaseViewModel { class AppInfoViewModel extends BaseViewModel {
@@ -27,18 +25,17 @@ class AppInfoViewModel extends BaseViewModel {
PatchedApplication app, PatchedApplication app,
bool onlyUnpatch, bool onlyUnpatch,
) async { ) async {
bool isUninstalled = true; var isUninstalled = onlyUnpatch;
if (app.isRooted) {
final bool hasRootPermissions = await _rootAPI.hasRootPermissions(); if (!onlyUnpatch) {
if (hasRootPermissions) { // TODO(Someone): Wait for the app to uninstall successfully.
await _rootAPI.deleteApp(app.packageName, app.apkFilePath);
if (!onlyUnpatch) {
await DeviceApps.uninstallApp(app.packageName);
}
}
} else {
isUninstalled = await DeviceApps.uninstallApp(app.packageName); isUninstalled = await DeviceApps.uninstallApp(app.packageName);
} }
if (isUninstalled && app.isRooted && await _rootAPI.hasRootPermissions()) {
await _rootAPI.uninstall(app.packageName);
}
if (isUninstalled) { if (isUninstalled) {
await _managerAPI.deletePatchedApp(app); await _managerAPI.deletePatchedApp(app);
locator<HomeViewModel>().initialize(context); locator<HomeViewModel>().initialize(context);
@@ -68,12 +65,11 @@ class AppInfoViewModel extends BaseViewModel {
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: I18nText('appInfoView.rootDialogTitle'), title: I18nText('appInfoView.rootDialogTitle'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('appInfoView.rootDialogText'), content: I18nText('appInfoView.rootDialogText'),
actions: <Widget>[ actions: <Widget>[
CustomMaterialButton( FilledButton(
label: I18nText('okButton'),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: I18nText('okButton'),
), ),
], ],
), ),
@@ -84,32 +80,53 @@ class AppInfoViewModel extends BaseViewModel {
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: I18nText( title: I18nText(
'appInfoView.unpatchButton', 'appInfoView.unmountButton',
), ),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText( content: I18nText(
'appInfoView.unpatchDialogText', 'appInfoView.unmountDialogText',
), ),
actions: <Widget>[ actions: <Widget>[
CustomMaterialButton( TextButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: I18nText('noButton'),
), ),
CustomMaterialButton( FilledButton(
label: I18nText('yesButton'),
onPressed: () { onPressed: () {
uninstallApp(context, app, onlyUnpatch); uninstallApp(context, app, true);
Navigator.of(context).pop(); Navigator.of(context).pop();
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: I18nText('yesButton'),
), ),
], ],
), ),
); );
} else { } else {
uninstallApp(context, app, onlyUnpatch); return showDialog(
Navigator.of(context).pop(); context: context,
builder: (context) => AlertDialog(
title: I18nText(
'appInfoView.uninstallButton',
),
content: I18nText(
'appInfoView.uninstallDialogText',
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: I18nText('noButton'),
),
FilledButton(
onPressed: () {
uninstallApp(context, app, false);
Navigator.of(context).pop();
Navigator.of(context).pop();
},
child: I18nText('yesButton'),
),
],
),
);
} }
} }
} }
@@ -132,14 +149,13 @@ class AppInfoViewModel extends BaseViewModel {
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: I18nText('appInfoView.appliedPatchesLabel'), title: I18nText('appInfoView.appliedPatchesLabel'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: SingleChildScrollView( content: SingleChildScrollView(
child: Text(getAppliedPatchesString(app.appliedPatches)), child: Text(getAppliedPatchesString(app.appliedPatches)),
), ),
actions: <Widget>[ actions: <Widget>[
CustomMaterialButton( FilledButton(
label: I18nText('okButton'),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: I18nText('okButton'),
), ),
], ],
), ),
@@ -147,17 +163,7 @@ class AppInfoViewModel extends BaseViewModel {
} }
String getAppliedPatchesString(List<String> appliedPatches) { String getAppliedPatchesString(List<String> appliedPatches) {
final List<String> names = appliedPatches return '${appliedPatches.join('\n')}';
.map(
(p) => p
.replaceAll('-', ' ')
.split('-')
.join(' ')
.toTitleCase()
.replaceFirst('Microg', 'MicroG'),
)
.toList();
return '\u2022 ${names.join('\n\u2022 ')}';
} }
void openApp(PatchedApplication app) { void openApp(PatchedApplication app) {

View File

@@ -3,7 +3,7 @@ import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
import 'package:skeletons/skeletons.dart'; import 'package:skeletons/skeletons.dart';
class AppSkeletonLoader extends StatelessWidget { class AppSkeletonLoader extends StatelessWidget {
const AppSkeletonLoader({Key? key}) : super(key: key); const AppSkeletonLoader({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@@ -5,7 +5,7 @@ import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
class InstalledAppItem extends StatefulWidget { class InstalledAppItem extends StatefulWidget {
const InstalledAppItem({ const InstalledAppItem({
Key? key, super.key,
required this.name, required this.name,
required this.pkgName, required this.pkgName,
required this.icon, required this.icon,
@@ -13,7 +13,9 @@ class InstalledAppItem extends StatefulWidget {
required this.suggestedVersion, required this.suggestedVersion,
required this.installedVersion, required this.installedVersion,
this.onTap, this.onTap,
}) : super(key: key); this.onLinkTap,
});
final String name; final String name;
final String pkgName; final String pkgName;
final Uint8List icon; final Uint8List icon;
@@ -21,6 +23,7 @@ class InstalledAppItem extends StatefulWidget {
final String suggestedVersion; final String suggestedVersion;
final String installedVersion; final String installedVersion;
final Function()? onTap; final Function()? onTap;
final Function()? onLinkTap;
@override @override
State<InstalledAppItem> createState() => _InstalledAppItemState(); State<InstalledAppItem> createState() => _InstalledAppItemState();
@@ -51,39 +54,26 @@ class _InstalledAppItemState extends State<InstalledAppItem> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text(
widget.name,
maxLines: 2,
overflow: TextOverflow.visible,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
Text(widget.pkgName),
I18nText(
FlutterI18n.translate(
context,
'installed',
translationParams: {
'version': 'v${widget.installedVersion}',
},
),
),
Wrap( Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 4,
children: [ children: [
I18nText( Text(
'suggested', widget.name,
translationParams: { maxLines: 1,
'version': widget.suggestedVersion.isEmpty overflow: TextOverflow.ellipsis,
? FlutterI18n.translate( style: const TextStyle(
context, fontSize: 16,
'appSelectorCard.allVersions', ),
) ),
: 'v${widget.suggestedVersion}', Text(
}, widget.installedVersion,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
),
), ),
const SizedBox(width: 4),
Text( Text(
widget.patchesCount == 1 widget.patchesCount == 1
? '${widget.patchesCount} patch' ? '${widget.patchesCount} patch'
@@ -91,11 +81,58 @@ class _InstalledAppItemState extends State<InstalledAppItem> {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.secondary, color: Theme.of(context).colorScheme.secondary,
), ),
), ),
], ],
), ),
Text(
widget.pkgName,
),
const SizedBox(height: 4),
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Material(
color: Theme.of(context).colorScheme.secondaryContainer,
borderRadius:
const BorderRadius.all(Radius.circular(8)),
child: InkWell(
onTap: widget.onLinkTap,
borderRadius:
const BorderRadius.all(Radius.circular(8)),
child: Container(
padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
I18nText(
'suggested',
translationParams: {
'version': widget.suggestedVersion.isEmpty
? FlutterI18n.translate(
context,
'appSelectorCard.anyVersion',
)
: widget.suggestedVersion,
},
),
const SizedBox(width: 4),
Icon(
Icons.search,
size: 16,
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
),
],
),
),
),
),
],
),
], ],
), ),
), ),

View File

@@ -4,16 +4,19 @@ import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
class NotInstalledAppItem extends StatefulWidget { class NotInstalledAppItem extends StatefulWidget {
const NotInstalledAppItem({ const NotInstalledAppItem({
Key? key, super.key,
required this.name, required this.name,
required this.patchesCount, required this.patchesCount,
required this.suggestedVersion, required this.suggestedVersion,
this.onTap, this.onTap,
}) : super(key: key); this.onLinkTap,
});
final String name; final String name;
final int patchesCount; final int patchesCount;
final String suggestedVersion; final String suggestedVersion;
final Function()? onTap; final Function()? onTap;
final Function()? onLinkTap;
@override @override
State<NotInstalledAppItem> createState() => _NotInstalledAppItem(); State<NotInstalledAppItem> createState() => _NotInstalledAppItem();
@@ -30,68 +33,91 @@ class _NotInstalledAppItem extends State<NotInstalledAppItem> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
Container( Container(
width: 48,
height: 48, height: 48,
padding: const EdgeInsets.symmetric(vertical: 4.0),
alignment: Alignment.center, alignment: Alignment.center,
child: const CircleAvatar( child: const CircleAvatar(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
child: Icon( child: Icon(
Icons.square_rounded, Icons.square_rounded,
color: Colors.grey, color: Colors.grey,
size: 44, size: 48,
), ),
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text( Wrap(
widget.name, crossAxisAlignment: WrapCrossAlignment.center,
style: const TextStyle( spacing: 4,
fontSize: 16, children: [
fontWeight: FontWeight.w500, Text(
), widget.name,
), style: const TextStyle(
const SizedBox(height: 4), fontSize: 16,
I18nText( ),
'appSelectorCard.notInstalled',
child: Text(
'',
style: TextStyle(
color: Theme.of(context).textTheme.titleLarge!.color,
),
),
),
Wrap(
children: [
I18nText(
'suggested',
translationParams: {
'version': widget.suggestedVersion.isEmpty
? FlutterI18n.translate(
context,
'appSelectorCard.allVersions',
)
: 'v${widget.suggestedVersion}',
},
),
const SizedBox(width: 4),
Text(
widget.patchesCount == 1
? '${widget.patchesCount} patch'
: '${widget.patchesCount} patches',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
), ),
), Text(
], widget.patchesCount == 1
), ? '${widget.patchesCount} patch'
], : '${widget.patchesCount} patches',
), maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.secondary,
),
),
],
),
const SizedBox(height: 4),
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Material(
color:
Theme.of(context).colorScheme.secondaryContainer,
borderRadius:
const BorderRadius.all(Radius.circular(8)),
child: InkWell(
onTap: widget.onLinkTap,
borderRadius:
const BorderRadius.all(Radius.circular(8)),
child: Container(
padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
I18nText(
'suggested',
translationParams: {
'version': widget.suggestedVersion.isEmpty
? FlutterI18n.translate(
context,
'appSelectorCard.anyVersion',
)
: widget.suggestedVersion,
},
),
const SizedBox(width: 4),
Icon(
Icons.search,
size: 16,
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
),
],
),
),
),
),
],
),
]),
), ),
], ],
), ),

View File

@@ -1,16 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/file.dart'; import 'package:flutter_cache_manager/file.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/services/download_manager.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class ContributorsCard extends StatefulWidget { class ContributorsCard extends StatefulWidget {
const ContributorsCard({ const ContributorsCard({
Key? key, super.key,
required this.title, required this.title,
required this.contributors, required this.contributors,
}) : super(key: key); });
final String title; final String title;
final List<dynamic> contributors; final List<dynamic> contributors;
@@ -58,7 +58,7 @@ class _ContributorsCardState extends State<ContributorsCard> {
mode: LaunchMode.externalApplication, mode: LaunchMode.externalApplication,
), ),
child: FutureBuilder<File?>( child: FutureBuilder<File?>(
future: DefaultCacheManager().getSingleFile( future: DownloadManager().getSingleFile(
widget.contributors[index]['avatar_url'], widget.contributors[index]['avatar_url'],
), ),
builder: (context, snapshot) => snapshot.hasData builder: (context, snapshot) => snapshot.hasData

View File

@@ -10,7 +10,7 @@ import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
//ignore: must_be_immutable //ignore: must_be_immutable
class InstalledAppsCard extends StatelessWidget { class InstalledAppsCard extends StatelessWidget {
InstalledAppsCard({Key? key}) : super(key: key); InstalledAppsCard({super.key});
List<PatchedApplication> apps = locator<HomeViewModel>().patchedInstalledApps; List<PatchedApplication> apps = locator<HomeViewModel>().patchedInstalledApps;
final ManagerAPI _managerAPI = locator<ManagerAPI>(); final ManagerAPI _managerAPI = locator<ManagerAPI>();
@@ -79,8 +79,6 @@ class InstalledAppsCard extends StatelessWidget {
icon: app.icon, icon: app.icon,
name: app.name, name: app.name,
patchDate: app.patchDate, patchDate: app.patchDate,
changelog: app.changelog,
isUpdatableApp: false,
onPressed: () => onPressed: () =>
locator<HomeViewModel>().navigateToAppInfo(app), locator<HomeViewModel>().navigateToAppInfo(app),
), ),

View File

@@ -3,14 +3,13 @@ import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/ui/views/home/home_viewmodel.dart'; import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
class LatestCommitCard extends StatefulWidget { class LatestCommitCard extends StatefulWidget {
const LatestCommitCard({ const LatestCommitCard({
Key? key, super.key,
required this.model, required this.model,
required this.parentContext, required this.parentContext,
}) : super(key: key); });
final HomeViewModel model; final HomeViewModel model;
final BuildContext parentContext; final BuildContext parentContext;
@@ -58,14 +57,14 @@ class _LatestCommitCardState extends State<LatestCommitCard> {
initialData: false, initialData: false,
builder: (context, snapshot) => Opacity( builder: (context, snapshot) => Opacity(
opacity: snapshot.hasData && snapshot.data! ? 1.0 : 0.25, opacity: snapshot.hasData && snapshot.data! ? 1.0 : 0.25,
child: CustomMaterialButton( child: FilledButton(
label: I18nText('updateButton'),
onPressed: snapshot.hasData && snapshot.data! onPressed: snapshot.hasData && snapshot.data!
? () => widget.model.showUpdateConfirmationDialog( ? () => widget.model.showUpdateConfirmationDialog(
widget.parentContext, widget.parentContext,
false, false,
) )
: () => {}, : () => {},
child: I18nText('updateButton'),
), ),
), ),
), ),
@@ -75,7 +74,7 @@ class _LatestCommitCardState extends State<LatestCommitCard> {
const SizedBox(height: 16), const SizedBox(height: 16),
// ReVanced Patches // Patches
CustomCard( CustomCard(
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -84,7 +83,7 @@ class _LatestCommitCardState extends State<LatestCommitCard> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
const Text('ReVanced Patches'), const Text('Patches'),
const SizedBox(height: 4), const SizedBox(height: 4),
Row( Row(
children: <Widget>[ children: <Widget>[
@@ -113,14 +112,14 @@ class _LatestCommitCardState extends State<LatestCommitCard> {
initialData: false, initialData: false,
builder: (context, snapshot) => Opacity( builder: (context, snapshot) => Opacity(
opacity: snapshot.hasData && snapshot.data! ? 1.0 : 0.25, opacity: snapshot.hasData && snapshot.data! ? 1.0 : 0.25,
child: CustomMaterialButton( child: FilledButton(
label: I18nText('updateButton'),
onPressed: snapshot.hasData && snapshot.data! onPressed: snapshot.hasData && snapshot.data!
? () => widget.model.showUpdateConfirmationDialog( ? () => widget.model.showUpdateConfirmationDialog(
widget.parentContext, widget.parentContext,
true, true,
) )
: () => {}, : () => {},
child: I18nText('updateButton'),
), ),
), ),
), ),

View File

@@ -3,7 +3,6 @@ import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/ui/views/home/home_viewmodel.dart'; import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
class UpdateConfirmationDialog extends StatelessWidget { class UpdateConfirmationDialog extends StatelessWidget {
const UpdateConfirmationDialog({super.key, required this.isPatches}); const UpdateConfirmationDialog({super.key, required this.isPatches});
@@ -86,15 +85,14 @@ class UpdateConfirmationDialog extends StatelessWidget {
], ],
), ),
), ),
CustomMaterialButton( FilledButton(
isExpanded: true,
label: I18nText('updateButton'),
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
isPatches isPatches
? model.updatePatches(context) ? model.updatePatches(context)
: model.updateManager(context); : model.updateManager(context);
}, },
child: I18nText('updateButton'),
), ),
], ],
), ),

View File

@@ -7,20 +7,29 @@ import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
class AppSelectorCard extends StatelessWidget { class AppSelectorCard extends StatelessWidget {
const AppSelectorCard({ const AppSelectorCard({
Key? key, super.key,
required this.onPressed, required this.onPressed,
}) : super(key: key); });
final Function() onPressed; final Function() onPressed;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final vm = locator<PatcherViewModel>();
String? suggestedVersion;
if (vm.selectedApp != null) {
suggestedVersion = vm.getSuggestedVersionString(context);
}
return CustomCard( return CustomCard(
onTap: onPressed, onTap: onPressed,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
I18nText( I18nText(
locator<PatcherViewModel>().selectedApp == null vm.selectedApp == null
? 'appSelectorCard.widgetTitle' ? 'appSelectorCard.widgetTitle'
: 'appSelectorCard.widgetTitleSelected', : 'appSelectorCard.widgetTitleSelected',
child: const Text( child: const Text(
@@ -32,7 +41,7 @@ class AppSelectorCard extends StatelessWidget {
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
if (locator<PatcherViewModel>().selectedApp == null) if (vm.selectedApp == null)
I18nText('appSelectorCard.widgetSubtitle') I18nText('appSelectorCard.widgetSubtitle')
else else
Row( Row(
@@ -41,9 +50,9 @@ class AppSelectorCard extends StatelessWidget {
height: 18.0, height: 18.0,
child: ClipOval( child: ClipOval(
child: Image.memory( child: Image.memory(
locator<PatcherViewModel>().selectedApp == null vm.selectedApp == null
? Uint8List(0) ? Uint8List(0)
: locator<PatcherViewModel>().selectedApp!.icon, : vm.selectedApp!.icon,
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
), ),
@@ -51,22 +60,64 @@ class AppSelectorCard extends StatelessWidget {
const SizedBox(width: 6), const SizedBox(width: 6),
Flexible( Flexible(
child: Text( child: Text(
locator<PatcherViewModel>().getAppSelectionString(), vm.getAppSelectionString(),
style: const TextStyle(fontWeight: FontWeight.w600), style: const TextStyle(fontWeight: FontWeight.w600),
), ),
), ),
], ],
), ),
if (locator<PatcherViewModel>().selectedApp == null) if (vm.selectedApp == null)
Container() Container()
else else
Column( Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
locator<PatcherViewModel>() vm.selectedApp!.packageName,
.getSuggestedVersionString(context),
), ),
if (suggestedVersion!.isNotEmpty &&
suggestedVersion != vm.selectedApp!.version) ...[
const SizedBox(height: 4),
Row(
children: [
Material(
color: Theme.of(context).colorScheme.secondaryContainer,
borderRadius:
const BorderRadius.all(Radius.circular(8)),
child: InkWell(
onTap: () {
vm.queryVersion(suggestedVersion!);
},
borderRadius:
const BorderRadius.all(Radius.circular(8)),
child: Container(
padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
I18nText(
'suggested',
translationParams: {
'version': suggestedVersion,
},
),
const SizedBox(width: 4),
Icon(
Icons.search,
size: 16,
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
),
],
),
),
),
),
],
),
]
], ],
), ),
], ],

View File

@@ -7,9 +7,9 @@ import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
class PatchSelectorCard extends StatelessWidget { class PatchSelectorCard extends StatelessWidget {
const PatchSelectorCard({ const PatchSelectorCard({
Key? key, super.key,
required this.onPressed, required this.onPressed,
}) : super(key: key); });
final Function() onPressed; final Function() onPressed;
@override @override
@@ -58,8 +58,11 @@ class PatchSelectorCard extends StatelessWidget {
String _getPatchesSelection() { String _getPatchesSelection() {
String text = ''; String text = '';
for (final Patch p in locator<PatcherViewModel>().selectedPatches) { final List<Patch> selectedPatches =
text += '\u2022 ${p.getSimpleName()}\n'; locator<PatcherViewModel>().selectedPatches;
selectedPatches.sort((a, b) => a.name.compareTo(b.name));
for (final Patch p in selectedPatches) {
text += '${p.getSimpleName()}\n';
} }
return text.substring(0, text.length - 1); return text.substring(0, text.length - 1);
} }

View File

@@ -1,38 +1,41 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/toast.dart'; import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_checkbox.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_custom_card.dart';
// ignore: must_be_immutable // ignore: must_be_immutable
class PatchItem extends StatefulWidget { class PatchItem extends StatefulWidget {
PatchItem({ PatchItem({
Key? key, super.key,
required this.name, required this.name,
required this.simpleName, required this.simpleName,
required this.description, required this.description,
required this.packageVersion, required this.packageVersion,
required this.supportedPackageVersions, required this.supportedPackageVersions,
required this.isUnsupported, required this.isUnsupported,
required this.isNew, required this.hasUnsupportedPatchOption,
required this.options,
required this.isSelected, required this.isSelected,
required this.onChanged, required this.onChanged,
required this.navigateToOptions,
required this.isChangeEnabled, required this.isChangeEnabled,
this.child, });
}) : super(key: key);
final String name; final String name;
final String simpleName; final String simpleName;
final String description; final String description;
final String packageVersion; final String packageVersion;
final List<String> supportedPackageVersions; final List<String> supportedPackageVersions;
final bool isUnsupported; final bool isUnsupported;
final bool isNew; final bool hasUnsupportedPatchOption;
final List<Option> options;
bool isSelected; bool isSelected;
final Function(bool) onChanged; final Function(bool) onChanged;
final void Function(List<Option>) navigateToOptions;
final bool isChangeEnabled; final bool isChangeEnabled;
final Widget? child;
final toast = locator<Toast>(); final toast = locator<Toast>();
final _managerAPI = locator<ManagerAPI>(); final _managerAPI = locator<ManagerAPI>();
@@ -45,157 +48,162 @@ class _PatchItemState extends State<PatchItem> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
widget.isSelected = widget.isSelected && widget.isSelected = widget.isSelected &&
(!widget.isUnsupported || (!widget.isUnsupported ||
widget._managerAPI.areExperimentalPatchesEnabled()); !widget._managerAPI.isVersionCompatibilityCheckEnabled()) &&
!widget.hasUnsupportedPatchOption;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0), padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Opacity( child: Opacity(
opacity: widget.isUnsupported && opacity: widget.isUnsupported &&
widget._managerAPI.areExperimentalPatchesEnabled() == false widget._managerAPI.isVersionCompatibilityCheckEnabled() == true
? 0.5 ? 0.5
: 1, : 1,
child: CustomCard( child: HapticCustomCard(
padding: EdgeInsets.only(
top: 12,
bottom: 16,
left: 8.0,
right: widget.options.isNotEmpty ? 4.0 : 8.0,
),
onTap: () { onTap: () {
setState(() { if (widget.isUnsupported &&
if (widget.isUnsupported && widget._managerAPI.isVersionCompatibilityCheckEnabled()) {
!widget._managerAPI.areExperimentalPatchesEnabled()) { widget.isSelected = false;
widget.isSelected = false; widget.toast.showBottom('patchItem.unsupportedPatchVersion');
widget.toast.showBottom('patchItem.unsupportedPatchVersion'); } else if (widget.isChangeEnabled) {
} else if (widget.isChangeEnabled) { if (!widget.isSelected) {
widget.isSelected = !widget.isSelected; if (widget.hasUnsupportedPatchOption) {
_showUnsupportedRequiredOptionDialog();
return;
}
} }
}); widget.isSelected = !widget.isSelected;
if (!widget.isUnsupported || widget._managerAPI.areExperimentalPatchesEnabled()) { setState(() {});
}
if (!widget.isUnsupported ||
!widget._managerAPI.isVersionCompatibilityCheckEnabled()) {
widget.onChanged(widget.isSelected); widget.onChanged(widget.isSelected);
} }
}, },
child: Column( child: Row(
children: <Widget>[ crossAxisAlignment: CrossAxisAlignment.start,
Row( children: [
mainAxisAlignment: MainAxisAlignment.spaceBetween, Transform.scale(
children: <Widget>[ scale: 1.2,
Flexible( child: HapticCheckbox(
child: Column( value: widget.isSelected,
crossAxisAlignment: CrossAxisAlignment.start, activeColor: Theme.of(context).colorScheme.primary,
children: <Widget>[ checkColor: Theme.of(context).colorScheme.secondaryContainer,
Row( side: BorderSide(
crossAxisAlignment: CrossAxisAlignment.end, width: 2.0,
children: <Widget>[ color: Theme.of(context).colorScheme.primary,
Expanded(
child: Text(
widget.simpleName,
maxLines: 2,
overflow: TextOverflow.visible,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
],
),
const SizedBox(height: 4),
Text(
widget.description,
softWrap: true,
overflow: TextOverflow.visible,
style: TextStyle(
fontSize: 14,
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
),
),
],
),
), ),
Transform.scale( onChanged: (newValue) {
scale: 1.2, if (widget.isUnsupported &&
child: Checkbox( widget._managerAPI
value: widget.isSelected, .isVersionCompatibilityCheckEnabled()) {
activeColor: Theme.of(context).colorScheme.primary, widget.isSelected = false;
checkColor: widget.toast.showBottom(
Theme.of(context).colorScheme.secondaryContainer, 'patchItem.unsupportedPatchVersion',
side: BorderSide( );
width: 2.0, } else if (widget.isChangeEnabled) {
color: Theme.of(context).colorScheme.primary, if (!widget.isSelected) {
), if (widget.hasUnsupportedPatchOption) {
onChanged: (newValue) { _showUnsupportedRequiredOptionDialog();
setState(() { return;
if (widget.isUnsupported &&
!widget._managerAPI
.areExperimentalPatchesEnabled()) {
widget.isSelected = false;
widget.toast.showBottom(
'patchItem.unsupportedPatchVersion',
);
} else if (widget.isChangeEnabled) {
widget.isSelected = newValue!;
}
});
if (!widget.isUnsupported || widget._managerAPI.areExperimentalPatchesEnabled()) {
widget.onChanged(widget.isSelected);
} }
}, }
), widget.isSelected = newValue!;
setState(() {});
}
if (!widget.isUnsupported ||
!widget._managerAPI
.isVersionCompatibilityCheckEnabled()) {
widget.onChanged(widget.isSelected);
}
},
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(top: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.simpleName,
maxLines: 2,
overflow: TextOverflow.visible,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
Text(
widget.description,
softWrap: true,
overflow: TextOverflow.visible,
style: TextStyle(
fontSize: 14,
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
),
),
if (widget.description.isNotEmpty)
Align(
alignment: Alignment.topLeft,
child: Wrap(
spacing: 8,
runSpacing: 4,
children: [
if (widget.isUnsupported &&
!widget._managerAPI
.isVersionCompatibilityCheckEnabled())
Padding(
padding: const EdgeInsets.only(top: 8),
child: TextButton.icon(
label: I18nText('warning'),
icon: const Icon(
Icons.warning_amber_outlined,
size: 20.0,
),
onPressed: () =>
_showUnsupportedWarningDialog(),
style: ButtonStyle(
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(8),
side: BorderSide(
color: Theme.of(context)
.colorScheme
.secondary,
),
),
),
backgroundColor:
MaterialStateProperty.all(
Colors.transparent,
),
foregroundColor:
MaterialStateProperty.all(
Theme.of(context).colorScheme.secondary,
),
),
),
),
],
),
),
],
), ),
], ),
), ),
Row( if (widget.options.isNotEmpty)
children: [ IconButton(
if (widget.isUnsupported && icon: const Icon(Icons.settings_outlined),
widget._managerAPI.areExperimentalPatchesEnabled()) onPressed: () => widget.navigateToOptions(widget.options),
Padding( ),
padding: const EdgeInsets.only(top: 8, right: 8),
child: TextButton.icon(
label: I18nText('warning'),
icon: const Icon(Icons.warning, size: 20.0),
onPressed: () => _showUnsupportedWarningDialog(),
style: ButtonStyle(
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(
color: Theme.of(context).colorScheme.secondary,
),
),
),
backgroundColor: MaterialStateProperty.all(
Colors.transparent,
),
foregroundColor: MaterialStateProperty.all(
Theme.of(context).colorScheme.secondary,
),
),
),
),
if (widget.isNew)
Padding(
padding: const EdgeInsets.only(top: 8),
child: TextButton.icon(
label: I18nText('new'),
icon: const Icon(Icons.star, size: 20.0),
onPressed: () => _showNewPatchDialog(),
style: ButtonStyle(
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(
color: Theme.of(context).colorScheme.secondary,
),
),
),
backgroundColor: MaterialStateProperty.all(
Colors.transparent,
),
foregroundColor: MaterialStateProperty.all(
Theme.of(context).colorScheme.secondary,
),
),
),
),
],
),
widget.child ?? const SizedBox(),
], ],
), ),
), ),
@@ -208,38 +216,36 @@ class _PatchItemState extends State<PatchItem> {
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: I18nText('warning'), title: I18nText('warning'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText( content: I18nText(
'patchItem.unsupportedDialogText', 'patchItem.unsupportedDialogText',
translationParams: { translationParams: {
'packageVersion': widget.packageVersion, 'packageVersion': widget.packageVersion,
'supportedVersions': 'supportedVersions':
'\u2022 ${widget.supportedPackageVersions.reversed.join('\n\u2022 ')}', ' ${widget.supportedPackageVersions.reversed.join('\n ')}',
}, },
), ),
actions: <Widget>[ actions: <Widget>[
CustomMaterialButton( FilledButton(
label: I18nText('okButton'),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: I18nText('okButton'),
), ),
], ],
), ),
); );
} }
Future<void> _showNewPatchDialog() { Future<void> _showUnsupportedRequiredOptionDialog() {
return showDialog( return showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: I18nText('patchItem.newPatch'), title: I18nText('notice'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText( content: I18nText(
'patchItem.newPatchDialogText', 'patchItem.unsupportedRequiredOption',
), ),
actions: <Widget>[ actions: <Widget>[
CustomMaterialButton( FilledButton(
label: I18nText('okButton'),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: I18nText('okButton'),
), ),
], ],
), ),

View File

@@ -1,73 +1,552 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
class OptionsTextField extends StatelessWidget { class BooleanPatchOption extends StatelessWidget {
const OptionsTextField({Key? key, required this.hint}) : super(key: key); const BooleanPatchOption({
final String hint; super.key,
required this.patchOption,
required this.removeOption,
required this.onChanged,
});
final Option patchOption;
final void Function(Option option) removeOption;
final void Function(dynamic value, Option option) onChanged;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final size = MediaQuery.sizeOf(context); final ValueNotifier patchOptionValue = ValueNotifier(patchOption.value);
final sHeight = size.height; return PatchOption(
final sWidth = size.width; widget: Align(
return Container( alignment: Alignment.bottomLeft,
margin: const EdgeInsets.only(top: 12, bottom: 6), child: ValueListenableBuilder(
padding: EdgeInsets.zero, valueListenable: patchOptionValue,
child: TextField( builder: (context, value, child) {
decoration: InputDecoration( return Switch(
constraints: BoxConstraints( value: value ?? false,
maxHeight: sHeight * 0.05, onChanged: (bool value) {
maxWidth: sWidth * 1, patchOptionValue.value = value;
onChanged(value, patchOption);
},
);
},
),
),
patchOption: patchOption,
removeOption: (Option option) {
removeOption(option);
},
);
}
}
class IntAndStringPatchOption extends StatelessWidget {
const IntAndStringPatchOption({
super.key,
required this.patchOption,
required this.removeOption,
required this.onChanged,
});
final Option patchOption;
final void Function(Option option) removeOption;
final void Function(dynamic value, Option option) onChanged;
@override
Widget build(BuildContext context) {
final ValueNotifier patchOptionValue = ValueNotifier(patchOption.value);
String getKey() {
if (patchOption.value != null && patchOption.values != null) {
final List values = patchOption.values!.entries
.where((e) => e.value == patchOption.value)
.toList();
if (values.isNotEmpty) {
return values.first.key;
}
}
return '';
}
return PatchOption(
widget: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFieldForPatchOption(
value: patchOption.value,
values: patchOption.values,
optionType: patchOption.valueType,
selectedKey: getKey(),
onChanged: (value) {
patchOptionValue.value = value;
onChanged(value, patchOption);
},
), ),
border: const OutlineInputBorder(), ValueListenableBuilder(
labelText: hint, valueListenable: patchOptionValue,
builder: (context, value, child) {
if (patchOption.required && value == null) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
I18nText(
'patchOptionsView.requiredOption',
child: Text(
'',
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
),
],
);
} else {
return const SizedBox();
}
},
),
],
),
patchOption: patchOption,
removeOption: (Option option) {
removeOption(option);
},
);
}
}
class IntStringLongListPatchOption extends StatelessWidget {
const IntStringLongListPatchOption({
super.key,
required this.patchOption,
required this.removeOption,
required this.onChanged,
});
final Option patchOption;
final void Function(Option option) removeOption;
final void Function(dynamic value, Option option) onChanged;
@override
Widget build(BuildContext context) {
final List<dynamic> values = List.from(patchOption.value ?? []);
final ValueNotifier patchOptionValue = ValueNotifier(values);
final String type = patchOption.valueType;
String getKey(dynamic value) {
if (value != null && patchOption.values != null) {
final List values = patchOption.values!.entries
.where((e) => e.value.toString() == value)
.toList();
if (values.isNotEmpty) {
return values.first.key;
}
}
return '';
}
bool isCustomValue() {
if (values.length == 1 && patchOption.values != null) {
if (getKey(values[0]) != '') {
return false;
}
}
return true;
}
bool isTextFieldVisible = isCustomValue();
return PatchOption(
widget: ValueListenableBuilder(
valueListenable: patchOptionValue,
builder: (context, value, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListView.builder(
shrinkWrap: true,
itemCount: value.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
final e = values[index];
return TextFieldForPatchOption(
value: e.toString(),
values: patchOption.values,
optionType: type,
selectedKey: value.length > 1 ? '' : getKey(e),
showDropdown: index == 0,
onChanged: (newValue) {
if (newValue is List) {
values.clear();
isTextFieldVisible = false;
values.add(newValue.toString());
} else {
isTextFieldVisible = true;
if (values.length == 1 &&
values[0].toString().startsWith('[') &&
type.contains('Array')) {
values.clear();
values.addAll(patchOption.value);
} else {
values[index] = type == 'StringArray'
? newValue
: type == 'IntArray'
? int.parse(
newValue.toString().isEmpty
? '0'
: newValue.toString(),
)
: num.parse(
newValue.toString().isEmpty
? '0'
: newValue.toString(),
);
}
}
patchOptionValue.value = List.from(values);
onChanged(values, patchOption);
},
removeValue: () {
patchOptionValue.value = List.from(patchOptionValue.value)
..removeAt(index);
values.removeAt(index);
onChanged(values, patchOption);
},
);
},
),
if (isTextFieldVisible) ...[
const SizedBox(height: 4),
Align(
alignment: Alignment.centerLeft,
child: TextButton(
onPressed: () {
if (type == 'StringArray') {
patchOptionValue.value =
List.from(patchOptionValue.value)..add('');
values.add('');
} else {
patchOptionValue.value =
List.from(patchOptionValue.value)..add(0);
values.add(0);
}
onChanged(values, patchOption);
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.add, size: 20),
I18nText(
'add',
child: const Text(
'',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
),
],
],
);
},
),
patchOption: patchOption,
removeOption: (Option option) {
removeOption(option);
},
);
}
}
class UnsupportedPatchOption extends StatelessWidget {
const UnsupportedPatchOption({super.key, required this.patchOption});
final Option patchOption;
@override
Widget build(BuildContext context) {
return PatchOption(
widget: Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: I18nText(
'patchOptionsView.unsupportedOption',
child: const Text(
'',
style: TextStyle(
fontSize: 16,
),
),
),
),
),
patchOption: patchOption,
removeOption: (_) {},
);
}
}
class PatchOption extends StatelessWidget {
const PatchOption({
super.key,
required this.widget,
required this.patchOption,
required this.removeOption,
});
final Widget widget;
final Option patchOption;
final void Function(Option option) removeOption;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: CustomCard(
child: Row(
children: [
Expanded(
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
patchOption.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
patchOption.description,
style: TextStyle(
fontSize: 14,
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
),
),
],
),
),
if (!patchOption.required)
IconButton(
onPressed: () => removeOption(patchOption),
icon: const Icon(Icons.delete),
),
],
),
const SizedBox(height: 4),
widget,
],
),
),
],
), ),
), ),
); );
} }
} }
class OptionsFilePicker extends StatelessWidget { class TextFieldForPatchOption extends StatefulWidget {
const OptionsFilePicker({Key? key, required this.optionName}) const TextFieldForPatchOption({
: super(key: key); super.key,
final String optionName; required this.value,
required this.values,
this.removeValue,
required this.onChanged,
required this.optionType,
required this.selectedKey,
this.showDropdown = true,
});
final String? value;
final Map<String, dynamic>? values;
final String optionType;
final String selectedKey;
final bool showDropdown;
final void Function()? removeValue;
final void Function(dynamic value) onChanged;
@override
State<TextFieldForPatchOption> createState() =>
_TextFieldForPatchOptionState();
}
class _TextFieldForPatchOptionState extends State<TextFieldForPatchOption> {
final TextEditingController controller = TextEditingController();
String? selectedKey;
String? defaultValue;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( final bool isStringOption = widget.optionType.contains('String');
padding: const EdgeInsets.symmetric(horizontal: 4.0), final bool isArrayOption = widget.optionType.contains('Array');
child: Row( selectedKey ??= widget.selectedKey;
mainAxisAlignment: MainAxisAlignment.spaceBetween, controller.text = !isStringOption &&
children: <Widget>[ isArrayOption &&
I18nText( selectedKey == '' &&
optionName, (widget.value != null && widget.value.toString().startsWith('['))
child: Text( ? ''
'', : widget.value ?? '';
style: GoogleFonts.inter( defaultValue ??= controller.text;
fontSize: 16, return Column(
fontWeight: FontWeight.w500, crossAxisAlignment: CrossAxisAlignment.start,
), children: [
if (widget.showDropdown && (widget.values?.isNotEmpty ?? false))
DropdownButton<String>(
style: const TextStyle(
fontSize: 16,
), ),
), borderRadius: BorderRadius.circular(4),
ElevatedButton( dropdownColor: Theme.of(context).colorScheme.secondaryContainer,
style: ButtonStyle( isExpanded: true,
backgroundColor: MaterialStateProperty.all( value: selectedKey,
Theme.of(context).colorScheme.primary, items: widget.values!.entries
.map(
(e) => DropdownMenuItem(
value: e.key,
child: RichText(
text: TextSpan(
text: e.key,
style: TextStyle(
fontSize: 16,
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
),
children: [
TextSpan(
text: ' ${e.value}',
style: TextStyle(
fontSize: 14,
color: Theme.of(context)
.colorScheme
.onSecondaryContainer
.withOpacity(0.6),
),
),
],
),
),
),
)
.toList()
..add(
DropdownMenuItem(
value: '',
child: I18nText(
'patchOptionsView.customValue',
child: const Text(
'',
style: TextStyle(
fontSize: 16,
),
),
),
),
), ),
), onChanged: (value) {
onPressed: () { if (value == '') {
// pick files controller.text = defaultValue!;
widget.onChanged(controller.text);
} else {
controller.text = widget.values![value].toString();
widget.onChanged(
isArrayOption ? widget.values![value] : controller.text,
);
}
setState(() {
selectedKey = value;
});
}, },
child: Text( ),
'Select File', if (selectedKey == '')
style: TextStyle( TextFormField(
color: Theme.of(context).textTheme.bodyLarge?.color, inputFormatters: [
if (widget.optionType.contains('Int'))
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
if (widget.optionType.contains('Long'))
FilteringTextInputFormatter.allow(RegExp(r'^[0-9]*\.?[0-9]*')),
],
controller: controller,
keyboardType:
isStringOption ? TextInputType.text : TextInputType.number,
decoration: InputDecoration(
suffixIcon: PopupMenuButton(
tooltip: FlutterI18n.translate(
context,
'patchOptionsView.tooltip',
),
itemBuilder: (BuildContext context) {
return [
if (isArrayOption)
PopupMenuItem(
value: 'remove',
child: I18nText('remove'),
),
if (isStringOption) ...[
PopupMenuItem(
value: 'patchOptionsView.selectFilePath',
child: I18nText('patchOptionsView.selectFilePath'),
),
PopupMenuItem(
value: 'patchOptionsView.selectFolder',
child: I18nText('patchOptionsView.selectFolder'),
),
],
];
},
onSelected: (String selection) async {
switch (selection) {
case 'patchOptionsView.selectFilePath':
final String? result = await FlutterFileDialog.pickFile();
if (result != null) {
controller.text = result;
widget.onChanged(controller.text);
}
break;
case 'patchOptionsView.selectFolder':
final DirectoryLocation? result =
await FlutterFileDialog.pickDirectory();
if (result != null) {
controller.text = result.toString();
widget.onChanged(controller.text);
}
break;
case 'remove':
widget.removeValue!();
break;
}
},
),
hintStyle: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSecondaryContainer,
), ),
), ),
onChanged: (String value) {
widget.onChanged(value);
},
), ),
], ],
),
); );
} }
} }

View File

@@ -4,7 +4,7 @@ import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/utils/about_info.dart'; import 'package:revanced_manager/utils/about_info.dart';
class AboutWidget extends StatefulWidget { class AboutWidget extends StatefulWidget {
const AboutWidget({Key? key, this.padding}) : super(key: key); const AboutWidget({super.key, this.padding});
final EdgeInsetsGeometry? padding; final EdgeInsetsGeometry? padding;

View File

@@ -1,63 +0,0 @@
import 'package:flutter/material.dart';
class CustomSwitch extends StatelessWidget {
const CustomSwitch({
Key? key,
required this.onChanged,
required this.value,
}) : super(key: key);
final ValueChanged<bool> onChanged;
final bool value;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => onChanged(!value),
child: SizedBox(
height: 25,
width: 50,
child: Stack(
children: <Widget>[
AnimatedContainer(
height: 25,
width: 50,
curve: Curves.ease,
duration: const Duration(milliseconds: 400),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.circular(25.0),
),
color: value
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.secondary,
),
),
AnimatedAlign(
curve: Curves.ease,
duration: const Duration(milliseconds: 400),
alignment: !value ? Alignment.centerLeft : Alignment.centerRight,
child: Container(
height: 20,
width: 20,
margin: const EdgeInsets.symmetric(horizontal: 3),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: value
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surface,
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(0.1),
spreadRadius: 0.5,
blurRadius: 1,
),
],
),
),
),
],
),
),
);
}
}

View File

@@ -1,32 +0,0 @@
import 'package:flutter/material.dart';
import 'package:revanced_manager/ui/widgets/settingsView/custom_switch.dart';
class CustomSwitchTile extends StatelessWidget {
const CustomSwitchTile({
Key? key,
required this.title,
required this.subtitle,
required this.value,
required this.onTap,
this.padding,
}) : super(key: key);
final Widget title;
final Widget subtitle;
final bool value;
final Function(bool) onTap;
final EdgeInsetsGeometry? padding;
@override
Widget build(BuildContext context) {
return ListTile(
contentPadding: padding ?? EdgeInsets.zero,
title: title,
subtitle: subtitle,
onTap: () => onTap(!value),
trailing: CustomSwitch(
value: value,
onChanged: onTap,
),
);
}
}

View File

@@ -1,73 +0,0 @@
import 'package:flutter/material.dart';
class CustomTextField extends StatelessWidget {
const CustomTextField({
Key? key,
required this.inputController,
required this.label,
required this.hint,
this.leadingIcon,
required this.onChanged,
}) : super(key: key);
final TextEditingController inputController;
final Widget label;
final String hint;
final Widget? leadingIcon;
final Function(String)? onChanged;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 4.0),
child: TextField(
controller: inputController,
onChanged: onChanged,
keyboardType: TextInputType.text,
decoration: InputDecoration(
icon: leadingIcon,
label: label,
filled: true,
fillColor: Theme.of(context).colorScheme.secondaryContainer,
hintText: hint,
hintStyle: TextStyle(
color: Theme.of(context).colorScheme.secondary,
),
floatingLabelStyle: MaterialStateTextStyle.resolveWith(
(states) => states.contains(MaterialState.focused)
? TextStyle(color: Theme.of(context).colorScheme.primary)
: TextStyle(color: Theme.of(context).colorScheme.secondary),
),
contentPadding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
border: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
borderRadius: BorderRadius.circular(10),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2.0,
),
borderRadius: BorderRadius.circular(10),
),
errorBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.error,
),
borderRadius: BorderRadius.circular(10),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
borderRadius: BorderRadius.circular(10),
),
),
),
);
}
}

View File

@@ -1,18 +1,14 @@
// ignore_for_file: prefer_const_constructors // ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_api_url.dart'; import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_api_url.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_sources.dart'; import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_sources.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_enable_patches_selection.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_auto_update_patches.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_auto_update_patches.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_experimental_patches.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_enable_patches_selection.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_experimental_universal_patches.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_require_suggested_app_version.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_universal_patches.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_version_compatibility_check.dart';
final _settingsViewModel = SettingsViewModel();
class SAdvancedSection extends StatelessWidget { class SAdvancedSection extends StatelessWidget {
const SAdvancedSection({super.key}); const SAdvancedSection({super.key});
@@ -21,85 +17,15 @@ class SAdvancedSection extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SettingsSection( return SettingsSection(
title: 'settingsView.advancedSectionTitle', title: 'settingsView.advancedSectionTitle',
children: <Widget>[ children: const <Widget>[
SManageApiUrlUI(),
SManageSourcesUI(),
// SManageKeystorePasswordUI(),
SAutoUpdatePatches(), SAutoUpdatePatches(),
SEnablePatchesSelection(), SEnablePatchesSelection(),
SExperimentalUniversalPatches(), SRequireSuggestedAppVersion(),
SExperimentalPatches(), SVersionCompatibilityCheck(),
ListTile( SUniversalPatches(),
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), SManageSourcesUI(),
title: I18nText( SManageApiUrlUI(),
'settingsView.regenerateKeystoreLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.regenerateKeystoreHint'),
onTap: () => _showDeleteKeystoreDialog(context),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.deleteTempDirLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.deleteTempDirHint'),
onTap: () => _settingsViewModel.deleteTempDir(),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.deleteLogsLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.deleteLogsHint'),
onTap: () => _settingsViewModel.deleteLogs(),
),
], ],
); );
} }
Future<void> _showDeleteKeystoreDialog(context) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('settingsView.regenerateKeystoreDialogTitle'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('settingsView.regenerateKeystoreDialogText'),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(),
),
CustomMaterialButton(
label: I18nText('yesButton'),
onPressed: () => {
Navigator.of(context).pop(),
_settingsViewModel.deleteKeystore(),
},
),
],
),
);
}
} }

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart'; import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart';
class SAutoUpdatePatches extends StatefulWidget { class SAutoUpdatePatches extends StatefulWidget {
const SAutoUpdatePatches({super.key}); const SAutoUpdatePatches({super.key});
@@ -14,7 +15,7 @@ final _settingsViewModel = SettingsViewModel();
class _SAutoUpdatePatchesState extends State<SAutoUpdatePatches> { class _SAutoUpdatePatchesState extends State<SAutoUpdatePatches> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SwitchListTile( return HapticSwitchListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText( title: I18nText(
'settingsView.autoUpdatePatchesLabel', 'settingsView.autoUpdatePatchesLabel',

View File

@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/about_widget.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
final _settingsViewModel = SettingsViewModel();
class SDebugSection extends StatelessWidget {
const SDebugSection({super.key});
@override
Widget build(BuildContext context) {
return SettingsSection(
title: 'settingsView.debugSectionTitle',
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: () => _settingsViewModel.exportLogcatLogs(),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.deleteLogsLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.deleteLogsHint'),
onTap: () => _settingsViewModel.deleteLogs(),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.deleteTempDirLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.deleteTempDirHint'),
onTap: () => _settingsViewModel.deleteTempDir(),
),
const AboutWidget(
padding: EdgeInsets.symmetric(horizontal: 20.0),
),
],
);
}
}

View File

@@ -1,12 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart'; import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart';
class SEnablePatchesSelection extends StatefulWidget { class SEnablePatchesSelection extends StatefulWidget {
const SEnablePatchesSelection({super.key}); const SEnablePatchesSelection({super.key});
@override @override
State<SEnablePatchesSelection> createState() => _SEnablePatchesSelectionState(); State<SEnablePatchesSelection> createState() =>
_SEnablePatchesSelectionState();
} }
final _settingsViewModel = SettingsViewModel(); final _settingsViewModel = SettingsViewModel();
@@ -14,7 +16,7 @@ final _settingsViewModel = SettingsViewModel();
class _SEnablePatchesSelectionState extends State<SEnablePatchesSelection> { class _SEnablePatchesSelectionState extends State<SEnablePatchesSelection> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SwitchListTile( return HapticSwitchListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText( title: I18nText(
'settingsView.enablePatchesSelectionLabel', 'settingsView.enablePatchesSelectionLabel',

View File

@@ -3,7 +3,6 @@ import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_keystore_password.dart'; import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_keystore_password.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
final _settingsViewModel = SettingsViewModel(); final _settingsViewModel = SettingsViewModel();
@@ -45,6 +44,46 @@ class SExportSection extends StatelessWidget {
subtitle: I18nText('settingsView.importPatchesHint'), subtitle: I18nText('settingsView.importPatchesHint'),
onTap: () => _settingsViewModel.importPatches(context), onTap: () => _settingsViewModel.importPatches(context),
), ),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.resetStoredPatchesLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.resetStoredPatchesHint'),
onTap: () => _showResetDialog(
context,
'settingsView.resetStoredPatchesDialogTitle',
'settingsView.resetStoredPatchesDialogText',
_settingsViewModel.resetSelectedPatches,
),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.resetStoredOptionsLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.resetStoredOptionsHint'),
onTap: () => _showResetDialog(
context,
'settingsView.resetStoredOptionsDialogTitle',
'settingsView.resetStoredOptionsDialogText',
_settingsViewModel.resetAllOptions,
),
),
ListTile( ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText( title: I18nText(
@@ -84,7 +123,7 @@ class SExportSection extends StatelessWidget {
ListTile( ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText( title: I18nText(
'settingsView.resetStoredPatchesLabel', 'settingsView.regenerateKeystoreLabel',
child: const Text( child: const Text(
'', '',
style: TextStyle( style: TextStyle(
@@ -93,34 +132,59 @@ class SExportSection extends StatelessWidget {
), ),
), ),
), ),
subtitle: I18nText('settingsView.resetStoredPatchesHint'), subtitle: I18nText('settingsView.regenerateKeystoreHint'),
onTap: () => _showResetStoredPatchesDialog(context), onTap: () => _showDeleteKeystoreDialog(context),
), ),
// SManageKeystorePasswordUI(),
], ],
); );
} }
Future<void> _showResetStoredPatchesDialog(context) { Future<void> _showResetDialog(
context,
dialogTitle,
dialogText,
dialogAction,
) {
return showDialog( return showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: I18nText('settingsView.resetStoredPatchesDialogTitle'), title: I18nText(dialogTitle),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer, content: I18nText(dialogText),
content: I18nText(
'settingsView.resetStoredPatchesDialogText',
),
actions: <Widget>[ actions: <Widget>[
CustomMaterialButton( TextButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: I18nText('noButton'),
), ),
CustomMaterialButton( FilledButton(
label: I18nText('yesButton'),
onPressed: () => { onPressed: () => {
Navigator.of(context).pop(), Navigator.of(context).pop(),
_settingsViewModel.resetSelectedPatches(), dialogAction(),
}, },
child: I18nText('yesButton'),
),
],
),
);
}
Future<void> _showDeleteKeystoreDialog(context) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('settingsView.regenerateKeystoreDialogTitle'),
content: I18nText('settingsView.regenerateKeystoreDialogText'),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: I18nText('noButton'),
),
FilledButton(
onPressed: () => {
Navigator.of(context).pop(),
_settingsViewModel.deleteKeystore(),
},
child: I18nText('yesButton'),
), ),
], ],
), ),

View File

@@ -1,38 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/about_widget.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
final _settingsViewModel = SettingsViewModel();
class SInfoSection extends StatelessWidget {
const SInfoSection({super.key});
@override
Widget build(BuildContext context) {
return 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: () => _settingsViewModel.exportLogcatLogs(),
),
const AboutWidget(
padding: EdgeInsets.symmetric(horizontal: 20.0),
),
],
);
}
}

View File

@@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart';
class SRequireSuggestedAppVersion extends StatefulWidget {
const SRequireSuggestedAppVersion({super.key});
@override
State<SRequireSuggestedAppVersion> createState() =>
_SRequireSuggestedAppVersionState();
}
final _settingsViewModel = SettingsViewModel();
class _SRequireSuggestedAppVersionState
extends State<SRequireSuggestedAppVersion> {
@override
Widget build(BuildContext context) {
return HapticSwitchListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.requireSuggestedAppVersionLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.requireSuggestedAppVersionHint'),
value: _settingsViewModel.isRequireSuggestedAppVersionEnabled(),
onChanged: (value) async {
await _settingsViewModel.showRequireSuggestedAppVersionDialog(
context,
value,
);
setState(() {});
},
);
}
}

View File

@@ -3,10 +3,10 @@ import 'package:flutter_i18n/flutter_i18n.dart';
class SettingsSection extends StatelessWidget { class SettingsSection extends StatelessWidget {
const SettingsSection({ const SettingsSection({
Key? key, super.key,
required this.title, required this.title,
required this.children, required this.children,
}) : super(key: key); });
final String title; final String title;
final List<Widget> children; final List<Widget> children;

View File

@@ -3,12 +3,12 @@ import 'package:flutter_i18n/flutter_i18n.dart';
class SettingsTileDialog extends StatelessWidget { class SettingsTileDialog extends StatelessWidget {
const SettingsTileDialog({ const SettingsTileDialog({
Key? key, super.key,
required this.title, required this.title,
required this.subtitle, required this.subtitle,
required this.onTap, required this.onTap,
this.padding, this.padding,
}) : super(key: key); });
final String title; final String title;
final String subtitle; final String subtitle;
final Function()? onTap; final Function()? onTap;

View File

@@ -3,27 +3,26 @@ import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart'; import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart';
class SExperimentalUniversalPatches extends StatefulWidget { class SUniversalPatches extends StatefulWidget {
const SExperimentalUniversalPatches({super.key}); const SUniversalPatches({super.key});
@override @override
State<SExperimentalUniversalPatches> createState() => State<SUniversalPatches> createState() => _SUniversalPatchesState();
_SExperimentalUniversalPatchesState();
} }
final _settingsViewModel = SettingsViewModel(); final _settingsViewModel = SettingsViewModel();
final _patchesSelectorViewModel = PatchesSelectorViewModel(); final _patchesSelectorViewModel = PatchesSelectorViewModel();
final _patcherViewModel = PatcherViewModel(); final _patcherViewModel = PatcherViewModel();
class _SExperimentalUniversalPatchesState class _SUniversalPatchesState extends State<SUniversalPatches> {
extends State<SExperimentalUniversalPatches> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SwitchListTile( return HapticSwitchListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText( title: I18nText(
'settingsView.experimentalUniversalPatchesLabel', 'settingsView.universalPatchesLabel',
child: const Text( child: const Text(
'', '',
style: TextStyle( style: TextStyle(
@@ -32,7 +31,7 @@ class _SExperimentalUniversalPatchesState
), ),
), ),
), ),
subtitle: I18nText('settingsView.experimentalUniversalPatchesHint'), subtitle: I18nText('settingsView.universalPatchesHint'),
value: _settingsViewModel.areUniversalPatchesEnabled(), value: _settingsViewModel.areUniversalPatchesEnabled(),
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {

View File

@@ -3,26 +3,29 @@ import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.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/patches_selector/patches_selector_viewmodel.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_switch_list_tile.dart';
import 'package:revanced_manager/utils/check_for_supported_patch.dart'; import 'package:revanced_manager/utils/check_for_supported_patch.dart';
class SExperimentalPatches extends StatefulWidget { class SVersionCompatibilityCheck extends StatefulWidget {
const SExperimentalPatches({super.key}); const SVersionCompatibilityCheck({super.key});
@override @override
State<SExperimentalPatches> createState() => _SExperimentalPatchesState(); State<SVersionCompatibilityCheck> createState() =>
_SVersionCompatibilityCheckState();
} }
final _settingsViewModel = SettingsViewModel(); final _settingsViewModel = SettingsViewModel();
final _patchesSelectorViewModel = PatchesSelectorViewModel(); final _patchesSelectorViewModel = PatchesSelectorViewModel();
final _patcherViewModel = PatcherViewModel(); final _patcherViewModel = PatcherViewModel();
class _SExperimentalPatchesState extends State<SExperimentalPatches> { class _SVersionCompatibilityCheckState
extends State<SVersionCompatibilityCheck> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SwitchListTile( return HapticSwitchListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText( title: I18nText(
'settingsView.experimentalPatchesLabel', 'settingsView.versionCompatibilityCheckLabel',
child: const Text( child: const Text(
'', '',
style: TextStyle( style: TextStyle(
@@ -31,11 +34,11 @@ class _SExperimentalPatchesState extends State<SExperimentalPatches> {
), ),
), ),
), ),
subtitle: I18nText('settingsView.experimentalPatchesHint'), subtitle: I18nText('settingsView.versionCompatibilityCheckHint'),
value: _settingsViewModel.areExperimentalPatchesEnabled(), value: _settingsViewModel.isVersionCompatibilityCheckEnabled(),
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
_settingsViewModel.useExperimentalPatches(value); _settingsViewModel.useVersionCompatibilityCheck(value);
}); });
if (!value) { if (!value) {
_patcherViewModel.selectedPatches _patcherViewModel.selectedPatches

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