mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2026-01-27 13:01:02 +00:00
Compare commits
437 Commits
feat/toolt
...
feat/trans
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78ab8b4ee1 | ||
|
|
7615453eec | ||
|
|
4c0b6b02e9 | ||
|
|
fe84b22b6f | ||
|
|
1b21f5d4ab | ||
|
|
72b1db9a2f | ||
|
|
2805ac6540 | ||
|
|
b16931ca79 | ||
|
|
dfeca09d00 | ||
|
|
44c06e2197 | ||
|
|
df31b39cc8 | ||
|
|
25d82e869c | ||
|
|
9d9a0e81db | ||
|
|
ffa42099e3 | ||
|
|
11dd6e4064 | ||
|
|
35fb59b31d | ||
|
|
18a4df9af9 | ||
|
|
bd69b45a69 | ||
|
|
0d26df03f4 | ||
|
|
c436a7a100 | ||
|
|
dbb6c01e89 | ||
|
|
e0d529c2df | ||
|
|
0300da9eac | ||
|
|
4e5057ecad | ||
|
|
1ef5c1c5c5 | ||
|
|
39f52c1242 | ||
|
|
0c62837454 | ||
|
|
3d75ffe6a7 | ||
|
|
d188d5a410 | ||
|
|
b43ac8a1af | ||
|
|
2ff70728b4 | ||
|
|
e5097b5ecd | ||
|
|
d5671db3a7 | ||
|
|
a314ba209d | ||
|
|
12d92ba811 | ||
|
|
ffd08c737c | ||
|
|
e1b768c467 | ||
|
|
df6ecd27dd | ||
|
|
2571cb8c11 | ||
|
|
c327857823 | ||
|
|
03160ccef1 | ||
|
|
08cec674bb | ||
|
|
95fb97ec31 | ||
|
|
4753873866 | ||
|
|
ca322f2da8 | ||
|
|
d758964742 | ||
|
|
a592e0b302 | ||
|
|
aeab639b2b | ||
|
|
4016ae732c | ||
|
|
a1cb18f9c8 | ||
|
|
8d9c2439dd | ||
|
|
c1e90a45cd | ||
|
|
775c2d5cde | ||
|
|
de27b4b109 | ||
|
|
b58a11f9f1 | ||
|
|
2297e94cb8 | ||
|
|
a84ab244c7 | ||
|
|
2cdd6d1843 | ||
|
|
3b82767a89 | ||
|
|
af8e2b44c0 | ||
|
|
a8820a4daf | ||
|
|
e1278452b9 | ||
|
|
d4d2056585 | ||
|
|
55524f7284 | ||
|
|
cd2dbcc841 | ||
|
|
9fdca5a0af | ||
|
|
05444d8824 | ||
|
|
59d233e15c | ||
|
|
1fb94b711f | ||
|
|
0096169af8 | ||
|
|
7c410fef45 | ||
|
|
a91ff60533 | ||
|
|
0ce49582f1 | ||
|
|
f32ffbb6f2 | ||
|
|
3851983a44 | ||
|
|
e97b19d2b6 | ||
|
|
24c4cd3f99 | ||
|
|
f2ea00757a | ||
|
|
51132731a4 | ||
|
|
bf311664d5 | ||
|
|
581f7922ed | ||
|
|
83d33e87e3 | ||
|
|
537e0b5bed | ||
|
|
663cf2d6b8 | ||
|
|
0030c7a788 | ||
|
|
eba92e2644 | ||
|
|
2671e68004 | ||
|
|
6e08e9dc39 | ||
|
|
3710675ac0 | ||
|
|
244a8f4e17 | ||
|
|
015a2df457 | ||
|
|
8686c058ce | ||
|
|
5c21ec6eb5 | ||
|
|
f5e1e0b065 | ||
|
|
bb5d414abb | ||
|
|
bd07435d4b | ||
|
|
54c56f1d81 | ||
|
|
5133f02ad6 | ||
|
|
9f7eaa2123 | ||
|
|
9a82b78528 | ||
|
|
cedc6ad49f | ||
|
|
4d95a29a1c | ||
|
|
6f6296b8cd | ||
|
|
f3f8bc4ec2 | ||
|
|
55e7ebf4fc | ||
|
|
26778f57e6 | ||
|
|
7007010f14 | ||
|
|
8b3c4eb91c | ||
|
|
a53a8ba627 | ||
|
|
f07f75a55f | ||
|
|
2697077fc8 | ||
|
|
dc51d6134d | ||
|
|
a9a21fd15a | ||
|
|
cd142a70d3 | ||
|
|
ac0625892d | ||
|
|
7f5a291c99 | ||
|
|
93f4a5bb7c | ||
|
|
2de16e18e8 | ||
|
|
c73fdfdd2d | ||
|
|
bdfb5a9462 | ||
|
|
ca918eea46 | ||
|
|
8dc4e5b89e | ||
|
|
1a54313c1d | ||
|
|
a3604579fe | ||
|
|
3a63e42df9 | ||
|
|
66432764cf | ||
|
|
7ef496b98c | ||
|
|
9863c5161a | ||
|
|
7438f45903 | ||
|
|
5662863a64 | ||
|
|
426b28932f | ||
|
|
83eeeae801 | ||
|
|
4de534094a | ||
|
|
ca38737783 | ||
|
|
7e858a244c | ||
|
|
dbe5c44ec3 | ||
|
|
a5c8a23f9f | ||
|
|
f92cb9e191 | ||
|
|
e55566d3df | ||
|
|
76e0c95187 | ||
|
|
fedaedfda1 | ||
|
|
3bd4f0d8f3 | ||
|
|
43b3743213 | ||
|
|
089f200fe6 | ||
|
|
a3f31ea657 | ||
|
|
a907528a20 | ||
|
|
fa86c1a0bb | ||
|
|
ea29d0f00c | ||
|
|
b93ecc0db2 | ||
|
|
55be7a6a9c | ||
|
|
95efff8b66 | ||
|
|
44f8b1fb6b | ||
|
|
e4f19b0c25 | ||
|
|
3c23d573bf | ||
|
|
61f1ee0627 | ||
|
|
df52a7bdef | ||
|
|
113a74d270 | ||
|
|
15b47f9bb6 | ||
|
|
e2623d6d79 | ||
|
|
79a513f94b | ||
|
|
cc771817cb | ||
|
|
deea682651 | ||
|
|
7dae562819 | ||
|
|
8d939a6669 | ||
|
|
8419f75d59 | ||
|
|
d2119d3643 | ||
|
|
9a01273c43 | ||
|
|
c22e5b4051 | ||
|
|
f6ca4e9555 | ||
|
|
bf049c3c1a | ||
|
|
6ace71b739 | ||
|
|
d889677b29 | ||
|
|
5f2376919b | ||
|
|
5c434137d3 | ||
|
|
7bf89887e4 | ||
|
|
5901372523 | ||
|
|
537b7084e0 | ||
|
|
45d64117bf | ||
|
|
6c3a99a492 | ||
|
|
ea007adecd | ||
|
|
72cd8ebca8 | ||
|
|
7ab191ed2b | ||
|
|
3a05150fa3 | ||
|
|
e5b414e277 | ||
|
|
d3d4c27f6d | ||
|
|
36a1c3f368 | ||
|
|
2a63a6163a | ||
|
|
02ea5c6d4a | ||
|
|
9562d80bfd | ||
|
|
49f851022d | ||
|
|
b18c678354 | ||
|
|
189c993ada | ||
|
|
69c119d545 | ||
|
|
feb0ca4cf3 | ||
|
|
0383bd74f7 | ||
|
|
60fdec9804 | ||
|
|
483be5d722 | ||
|
|
e44d3fdee4 | ||
|
|
b59a16191a | ||
|
|
f4d6c60b9e | ||
|
|
154f036fe9 | ||
|
|
d3417adbeb | ||
|
|
16c4290f05 | ||
|
|
d23d673c47 | ||
|
|
b80f94b77b | ||
|
|
5f4c9584a9 | ||
|
|
9a085f4091 | ||
|
|
8d7886f7eb | ||
|
|
c21076f2fb | ||
|
|
05fe058151 | ||
|
|
752310fe94 | ||
|
|
75500bbc43 | ||
|
|
84013ffca2 | ||
|
|
9da798dac1 | ||
|
|
d622208b17 | ||
|
|
c5cb18a7ea | ||
|
|
0d73e0cd32 | ||
|
|
9d961f6a52 | ||
|
|
7df3350acb | ||
|
|
858dd99ffa | ||
|
|
757e132321 | ||
|
|
413fe980a8 | ||
|
|
1aba6bc874 | ||
|
|
b5c1f6d732 | ||
|
|
ea50e65ab1 | ||
|
|
e1647fdef0 | ||
|
|
0b362e3393 | ||
|
|
159461356e | ||
|
|
b1206cb663 | ||
|
|
b5558ea3ff | ||
|
|
dcaa38c882 | ||
|
|
429b428f67 | ||
|
|
a12cae7299 | ||
|
|
88e860cf01 | ||
|
|
0616666d5e | ||
|
|
2700d45e4d | ||
|
|
c3a32a1142 | ||
|
|
afb0f80de5 | ||
|
|
4aaa88e353 | ||
|
|
359095a6f8 | ||
|
|
e1c3d61ec6 | ||
|
|
72b7d24b33 | ||
|
|
0ccc2c13ac | ||
|
|
fa414ce6ee | ||
|
|
3f362b605f | ||
|
|
a2f9e2f1da | ||
|
|
3af26e7065 | ||
|
|
d5bdc293f3 | ||
|
|
336eed3a95 | ||
|
|
59b4c0b2d2 | ||
|
|
0934b24591 | ||
|
|
bb2164e1a9 | ||
|
|
2a3590ddd2 | ||
|
|
ceb7623794 | ||
|
|
f183b6d8a6 | ||
|
|
a6174eee8f | ||
|
|
f8aafa0503 | ||
|
|
5e35893883 | ||
|
|
90db765c9a | ||
|
|
e61c98ca59 | ||
|
|
c003c3c324 | ||
|
|
5a3a619d16 | ||
|
|
89addf3f78 | ||
|
|
34cf848baa | ||
|
|
a9171e17bd | ||
|
|
2e64022229 | ||
|
|
8aa70d350e | ||
|
|
ca93524be0 | ||
|
|
7fb1e27617 | ||
|
|
1dd6738964 | ||
|
|
4924eaef80 | ||
|
|
56896d6197 | ||
|
|
ee0f342456 | ||
|
|
90edf1ddd0 | ||
|
|
b69a369d4e | ||
|
|
663d21c6af | ||
|
|
dadc5462e3 | ||
|
|
3aea6cbaec | ||
|
|
8425b6fa81 | ||
|
|
c62f772284 | ||
|
|
ad775f3059 | ||
|
|
c199801fb7 | ||
|
|
2ffcaec724 | ||
|
|
08eb995220 | ||
|
|
bf1d628944 | ||
|
|
f6fb534e04 | ||
|
|
686eb40cb0 | ||
|
|
b03f7b18a0 | ||
|
|
e833bf4ad1 | ||
|
|
805d440450 | ||
|
|
e997255cf3 | ||
|
|
cfcabf6ef1 | ||
|
|
7af6c79076 | ||
|
|
a17c2de228 | ||
|
|
3bb071d80d | ||
|
|
9950d85779 | ||
|
|
e7ec239783 | ||
|
|
3246269937 | ||
|
|
85692f8596 | ||
|
|
c879faf2eb | ||
|
|
933a4a3220 | ||
|
|
a269a39aa4 | ||
|
|
cff9c5ed09 | ||
|
|
a811df9547 | ||
|
|
83cdaaee18 | ||
|
|
5b3e9e595c | ||
|
|
97aa8c06c8 | ||
|
|
933e69e21e | ||
|
|
dc73462ac4 | ||
|
|
ad903533f8 | ||
|
|
62e934c403 | ||
|
|
6561e4c97c | ||
|
|
ac98ef834a | ||
|
|
a246863a89 | ||
|
|
39b5b87c40 | ||
|
|
35595fb4bc | ||
|
|
02fb47d3b5 | ||
|
|
eef50888bb | ||
|
|
9b3974414c | ||
|
|
88deadcf08 | ||
|
|
bad847d89b | ||
|
|
3f446f8236 | ||
|
|
31cbab209c | ||
|
|
b14285b2c8 | ||
|
|
8e28f06e26 | ||
|
|
5b46c58af9 | ||
|
|
dccf86163a | ||
|
|
8767f0e99c | ||
|
|
0a1acd24e3 | ||
|
|
a55160e7c6 | ||
|
|
6436483206 | ||
|
|
409c888d52 | ||
|
|
858b0ec5b4 | ||
|
|
8c9fe6989f | ||
|
|
17d1874381 | ||
|
|
e7802ed3d7 | ||
|
|
f547bb7ab1 | ||
|
|
bfcd1fb977 | ||
|
|
1ab1e4682f | ||
|
|
a854221969 | ||
|
|
ab42b946ef | ||
|
|
8ae4e850da | ||
|
|
4d04ae088c | ||
|
|
5c17a78e46 | ||
|
|
3492dc4e83 | ||
|
|
39b08e5201 | ||
|
|
18527999b5 | ||
|
|
fd520bba70 | ||
|
|
59b894dce4 | ||
|
|
6f6476e851 | ||
|
|
37c1754098 | ||
|
|
1f1a480d51 | ||
|
|
6cf51d71c5 | ||
|
|
06ef9dee44 | ||
|
|
49b4bbbf0b | ||
|
|
fc90bbc27c | ||
|
|
86e42449eb | ||
|
|
45142841d5 | ||
|
|
3d59ee51ac | ||
|
|
6dbcd6293e | ||
|
|
a008cf5dd1 | ||
|
|
23162f6233 | ||
|
|
8540d30196 | ||
|
|
f06da2ba56 | ||
|
|
9e1ebb3902 | ||
|
|
2aec6e1e55 | ||
|
|
6134ed78b4 | ||
|
|
e3cb056858 | ||
|
|
eb2270673d | ||
|
|
db070b125b | ||
|
|
a510d59e64 | ||
|
|
0264308b6d | ||
|
|
2ac3d5c483 | ||
|
|
3f54381d30 | ||
|
|
059a72b9dd | ||
|
|
64496bfbe7 | ||
|
|
b07fd2321d | ||
|
|
1efccda3f5 | ||
|
|
b9231b4de0 | ||
|
|
1a164ebe30 | ||
|
|
8d53180d86 | ||
|
|
62bccd1504 | ||
|
|
969ddb7bef | ||
|
|
8b6d32dd7b | ||
|
|
fd0ec6c6a7 | ||
|
|
de1ef23824 | ||
|
|
f30333e753 | ||
|
|
d84e6a3ffc | ||
|
|
5ec97f4a85 | ||
|
|
8c40119609 | ||
|
|
5b6ae800fd | ||
|
|
064a54eaf0 | ||
|
|
41268ca80b | ||
|
|
f0d9def3dd | ||
|
|
44e5dad6e9 | ||
|
|
83eb88170a | ||
|
|
e7599e1386 | ||
|
|
14888f9da7 | ||
|
|
9675a2777b | ||
|
|
d71a4bf3c3 | ||
|
|
57548641e7 | ||
|
|
553af83139 | ||
|
|
1d6b34a39f | ||
|
|
948a6d1440 | ||
|
|
0dd036574f | ||
|
|
4d201f17f2 | ||
|
|
da32ff954a | ||
|
|
70e49aaaa3 | ||
|
|
4e43938f7f | ||
|
|
f620a887ad | ||
|
|
8fa4d1d26d | ||
|
|
925be5a168 | ||
|
|
caeabfc91b | ||
|
|
5c2f9d91a6 | ||
|
|
55e871aa7d | ||
|
|
b7d53cfca8 | ||
|
|
85a03a6472 | ||
|
|
373cc4bbb1 | ||
|
|
a127b959ea | ||
|
|
476a6e5771 | ||
|
|
037a11aeb8 | ||
|
|
45a54d1608 | ||
|
|
ca85b38bdc | ||
|
|
60a5a11c71 | ||
|
|
7fc6ec5c2c | ||
|
|
7fa7b9d53a | ||
|
|
045a5483f1 | ||
|
|
40dd81eba3 | ||
|
|
9f99fcf808 | ||
|
|
1b11a0ad7a | ||
|
|
8f60bf44e5 | ||
|
|
3b459160d7 | ||
|
|
c97e4adace | ||
|
|
875ce6439d | ||
|
|
c45d579406 | ||
|
|
b6b8f4e069 | ||
|
|
21ceadaf7f |
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,3 +1,7 @@
|
|||||||
|
name: ⭐ Feature request
|
||||||
|
description: Create a detailed request for a new feature.
|
||||||
|
title: 'feat: '
|
||||||
|
labels: ['Feature request']
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
2
.github/workflows/build_pull_request.yml
vendored
2
.github/workflows/build_pull_request.yml
vendored
@@ -13,8 +13,6 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Cache Gradle
|
- name: Cache Gradle
|
||||||
uses: burrunan/gradle-cache-action@v1
|
uses: burrunan/gradle-cache-action@v1
|
||||||
|
|||||||
43
.github/workflows/pull_strings.yml
vendored
Normal file
43
.github/workflows/pull_strings.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
name: Pull strings
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 0 * * 0"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pull:
|
||||||
|
name: Pull strings
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
ref: dev
|
||||||
|
clean: true
|
||||||
|
|
||||||
|
- name: Pull strings
|
||||||
|
uses: crowdin/github-action@v2
|
||||||
|
with:
|
||||||
|
config: crowdin.yml
|
||||||
|
upload_sources: false
|
||||||
|
download_translations: true
|
||||||
|
skip_ref_checkout: true
|
||||||
|
localization_branch_name: feat/translations
|
||||||
|
create_pull_request: false
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||||
|
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||||
|
|
||||||
|
- name: Open pull request
|
||||||
|
if: github.event_name == 'workflow_dispatch'
|
||||||
|
uses: repo-sync/pull-request@v2
|
||||||
|
with:
|
||||||
|
source_branch: feat/translations
|
||||||
|
destination_branch: dev
|
||||||
|
pr_title: "chore: Sync translations"
|
||||||
|
pr_body: "Sync translations from [crowdin.com/project/revanced](https://crowdin.com/project/revanced)"
|
||||||
26
.github/workflows/push_strings.yml
vendored
Normal file
26
.github/workflows/push_strings.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
name: Push strings
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
paths:
|
||||||
|
- app/src/main/res/values/strings.xml
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
push:
|
||||||
|
name: Push strings
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Push strings
|
||||||
|
uses: crowdin/github-action@v2
|
||||||
|
with:
|
||||||
|
config: crowdin.yml
|
||||||
|
upload_sources: true
|
||||||
|
env:
|
||||||
|
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||||
|
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||||
24
.github/workflows/release.yml
vendored
24
.github/workflows/release.yml
vendored
@@ -12,14 +12,13 @@ jobs:
|
|||||||
name: Release
|
name: Release
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
packages: write
|
||||||
id-token: write
|
id-token: write
|
||||||
attestations: write
|
attestations: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
@@ -28,10 +27,11 @@ jobs:
|
|||||||
java-version: '17'
|
java-version: '17'
|
||||||
|
|
||||||
- name: Cache Gradle
|
- name: Cache Gradle
|
||||||
uses: burrunan/gradle-cache-action@v1
|
uses: burrunan/gradle-cache-action@v3
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
|
GITHUB_ACTOR: ${{ github.actor }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: ./gradlew assembleRelease
|
run: ./gradlew assembleRelease
|
||||||
|
|
||||||
@@ -55,18 +55,26 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "${{ secrets.KEYSTORE }}" | base64 --decode > "app/keystore.jks"
|
echo "${{ secrets.KEYSTORE }}" | base64 --decode > "app/keystore.jks"
|
||||||
|
|
||||||
- name: Semantic Release
|
- name: Release API
|
||||||
uses: cycjimmy/semantic-release-action@v4
|
run: npx multi-semantic-release --tag-format 'api@${version}' --ignore-packages app
|
||||||
id: semantic
|
|
||||||
env:
|
env:
|
||||||
|
GITHUB_ACTOR: ${{ github.actor }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
id: release
|
||||||
|
run: |
|
||||||
|
echo "NEW_TAG=$(npx multi-semantic-release --tag-format 'v${version}' --ignore-packages api | tee | grep 'Created tag ' | sed -E 's/.*Created tag ([^ ]+).*/\1/')" >> $GITHUB_OUTPUT
|
||||||
|
env:
|
||||||
|
GITHUB_ACTOR: ${{ github.actor }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||||
KEYSTORE_ENTRY_ALIAS: ${{ secrets.KEYSTORE_ENTRY_ALIAS }}
|
KEYSTORE_ENTRY_ALIAS: ${{ secrets.KEYSTORE_ENTRY_ALIAS }}
|
||||||
KEYSTORE_ENTRY_PASSWORD: ${{ secrets.KEYSTORE_ENTRY_PASSWORD }}
|
KEYSTORE_ENTRY_PASSWORD: ${{ secrets.KEYSTORE_ENTRY_PASSWORD }}
|
||||||
|
|
||||||
- name: Attest
|
- name: Attest
|
||||||
if: steps.semantic.outputs.new_release_published == 'true'
|
if: steps.release.outputs.NEW_TAG != ''
|
||||||
uses: actions/attest-build-provenance@v2
|
uses: actions/attest-build-provenance@v2
|
||||||
with:
|
with:
|
||||||
subject-name: 'ReVanced Manager ${{ steps.release.outputs.new_release_git_tag }}'
|
subject-name: 'ReVanced Manager ${{ steps.release.outputs.NEW_TAG }}'
|
||||||
subject-path: app/build/outputs/apk/release/revanced-manager*.apk
|
subject-path: app/build/outputs/apk/release/revanced-manager*.apk
|
||||||
|
|||||||
8
adsfund.json
Normal file
8
adsfund.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"info": "This is verification file for ads.fund project",
|
||||||
|
"project": {
|
||||||
|
"name": "ReVanced Manager",
|
||||||
|
"walletAddress": "0x7ab4091e00363654bf84B34151225742cd92FCE5",
|
||||||
|
"tokenAddress": "0xadf954bc6f509b3a32fb5e97ed4ba6c000e37155"
|
||||||
|
}
|
||||||
|
}
|
||||||
39
api/.releaserc
Normal file
39
api/.releaserc
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"branches": [
|
||||||
|
"main",
|
||||||
|
{
|
||||||
|
"name": "dev",
|
||||||
|
"prerelease": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"@semantic-release/commit-analyzer",
|
||||||
|
{
|
||||||
|
"releaseRules": [
|
||||||
|
{ "type": "build", "scope": "Needs bump", "release": "patch" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@semantic-release/release-notes-generator",
|
||||||
|
"@semantic-release/changelog",
|
||||||
|
"gradle-semantic-release-plugin",
|
||||||
|
[
|
||||||
|
"@semantic-release/git",
|
||||||
|
{
|
||||||
|
"assets": [
|
||||||
|
"CHANGELOG.md",
|
||||||
|
"gradle.properties"
|
||||||
|
],
|
||||||
|
"message": "chore: Release API v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@saithodev/semantic-release-backmerge",
|
||||||
|
{
|
||||||
|
"backmergeBranches": [{"from": "main", "to": "dev"}],
|
||||||
|
"clearWorkspace": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -24,14 +24,6 @@ public final class app/revanced/manager/plugin/downloader/DownloadUrl : android/
|
|||||||
public final fun writeToParcel (Landroid/os/Parcel;I)V
|
public final fun writeToParcel (Landroid/os/Parcel;I)V
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/plugin/downloader/DownloadUrl$Creator : android/os/Parcelable$Creator {
|
|
||||||
public fun <init> ()V
|
|
||||||
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/DownloadUrl;
|
|
||||||
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
|
|
||||||
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/DownloadUrl;
|
|
||||||
public synthetic fun newArray (I)[Ljava/lang/Object;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final class app/revanced/manager/plugin/downloader/Downloader {
|
public final class app/revanced/manager/plugin/downloader/Downloader {
|
||||||
public static final field $stable I
|
public static final field $stable I
|
||||||
}
|
}
|
||||||
@@ -85,14 +77,6 @@ public final class app/revanced/manager/plugin/downloader/Package : android/os/P
|
|||||||
public final fun writeToParcel (Landroid/os/Parcel;I)V
|
public final fun writeToParcel (Landroid/os/Parcel;I)V
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/plugin/downloader/Package$Creator : android/os/Parcelable$Creator {
|
|
||||||
public fun <init> ()V
|
|
||||||
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/Package;
|
|
||||||
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
|
|
||||||
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/Package;
|
|
||||||
public synthetic fun newArray (I)[Ljava/lang/Object;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract interface annotation class app/revanced/manager/plugin/downloader/PluginHostApi : java/lang/annotation/Annotation {
|
public abstract interface annotation class app/revanced/manager/plugin/downloader/PluginHostApi : java/lang/annotation/Annotation {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,14 +143,6 @@ public abstract class app/revanced/manager/plugin/downloader/webview/IWebViewEve
|
|||||||
public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z
|
public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters$Creator : android/os/Parcelable$Creator {
|
|
||||||
public fun <init> ()V
|
|
||||||
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters;
|
|
||||||
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
|
|
||||||
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters;
|
|
||||||
public synthetic fun newArray (I)[Ljava/lang/Object;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract interface class app/revanced/manager/plugin/downloader/webview/WebViewCallbackScope : app/revanced/manager/plugin/downloader/Scope {
|
public abstract interface class app/revanced/manager/plugin/downloader/webview/WebViewCallbackScope : app/revanced/manager/plugin/downloader/Scope {
|
||||||
public abstract fun finish (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
public abstract fun finish (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||||
public abstract fun load (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
public abstract fun load (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import java.io.IOException
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.library)
|
alias(libs.plugins.android.library)
|
||||||
@@ -19,46 +19,16 @@ dependencies {
|
|||||||
implementation(libs.appcompat)
|
implementation(libs.appcompat)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun String.runCommand(): String {
|
kotlin {
|
||||||
val process = ProcessBuilder(split("\\s".toRegex()))
|
jvmToolchain(17)
|
||||||
.redirectErrorStream(true)
|
compilerOptions {
|
||||||
.directory(rootDir)
|
jvmTarget = JvmTarget.JVM_17
|
||||||
.start()
|
|
||||||
|
|
||||||
val output = StringBuilder()
|
|
||||||
val reader = process.inputStream.bufferedReader()
|
|
||||||
|
|
||||||
val thread = Thread {
|
|
||||||
reader.forEachLine {
|
|
||||||
output.appendLine(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
if (!process.waitFor(10, TimeUnit.SECONDS)) {
|
|
||||||
process.destroy()
|
|
||||||
throw IOException("Command timed out: $this")
|
|
||||||
}
|
|
||||||
|
|
||||||
thread.join()
|
|
||||||
return output.toString().trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
val projectPath: String = projectDir.relativeTo(rootDir).path
|
|
||||||
val lastTag = "git describe --tags --abbrev=0".runCommand()
|
|
||||||
val hasChangesInThisModule = "git diff --name-only $lastTag..HEAD".runCommand().lineSequence().any {
|
|
||||||
it.startsWith(projectPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.matching { it.name.startsWith("publish") }.configureEach {
|
|
||||||
onlyIf {
|
|
||||||
hasChangesInThisModule
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "app.revanced.manager.plugin.downloader"
|
namespace = "app.revanced.manager.plugin.downloader"
|
||||||
compileSdk = 35
|
compileSdk = 36
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
@@ -81,10 +51,6 @@ android {
|
|||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "17"
|
|
||||||
}
|
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
aidl = true
|
aidl = true
|
||||||
}
|
}
|
||||||
@@ -147,4 +113,4 @@ publishing {
|
|||||||
signing {
|
signing {
|
||||||
useGpgCmd()
|
useGpgCmd()
|
||||||
sign(publishing.publications["Api"])
|
sign(publishing.publications["Api"])
|
||||||
}
|
}
|
||||||
|
|||||||
1
api/gradlew
vendored
Symbolic link
1
api/gradlew
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../gradlew
|
||||||
11
api/package.json
Normal file
11
api/package.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "api",
|
||||||
|
"private": false,
|
||||||
|
"devDependencies": {
|
||||||
|
"@anolilab/multi-semantic-release": "^1.1.10",
|
||||||
|
"@saithodev/semantic-release-backmerge": "^4.0.1",
|
||||||
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
|
"@semantic-release/git": "^10.0.1",
|
||||||
|
"gradle-semantic-release-plugin": "^1.10.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,8 @@
|
|||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
[
|
[
|
||||||
"@semantic-release/commit-analyzer", {
|
"@semantic-release/commit-analyzer",
|
||||||
|
{
|
||||||
"releaseRules": [
|
"releaseRules": [
|
||||||
{ "type": "build", "scope": "Needs bump", "release": "patch" }
|
{ "type": "build", "scope": "Needs bump", "release": "patch" }
|
||||||
]
|
]
|
||||||
@@ -22,7 +23,7 @@
|
|||||||
{
|
{
|
||||||
"assets": [
|
"assets": [
|
||||||
"CHANGELOG.md",
|
"CHANGELOG.md",
|
||||||
"gradle.properties",
|
"gradle.properties"
|
||||||
],
|
],
|
||||||
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
||||||
}
|
}
|
||||||
@@ -32,17 +33,17 @@
|
|||||||
{
|
{
|
||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"path": "app/build/outputs/apk/release/revanced-manager*.apk?(.asc)"
|
"path": "build/outputs/apk/release/revanced-manager*.apk?(.asc)"
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
successComment: false
|
"successComment": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"@saithodev/semantic-release-backmerge",
|
"@saithodev/semantic-release-backmerge",
|
||||||
{
|
{
|
||||||
backmergeBranches: [{"from": "main", "to": "dev"}],
|
"backmergeBranches": [{"from": "main", "to": "dev"}],
|
||||||
clearWorkspace: true
|
"clearWorkspace": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
413
app/CHANGELOG.md
Normal file
413
app/CHANGELOG.md
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
# app [1.26.0-dev.20](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.19...v1.26.0-dev.20) (2026-01-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Save FAB freaking out in select patches screen ([4c0b6b0](https://github.com/ReVanced/revanced-manager/commit/4c0b6b02e95a8d6f655bcf5c25493b1f9a4a4dcd))
|
||||||
|
|
||||||
|
# app [1.26.0-dev.19](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.18...v1.26.0-dev.19) (2026-01-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **locales:** use buildconfig instead of generating kt file ([72b1db9](https://github.com/ReVanced/revanced-manager/commit/72b1db9a2f33ab5d5fffd8ba83c05901eff19bea))
|
||||||
|
|
||||||
|
# app [1.26.0-dev.18](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.17...v1.26.0-dev.18) (2026-01-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Prevent trailing comma when no locales are generated ([b16931c](https://github.com/ReVanced/revanced-manager/commit/b16931ca79d5ce4d17c75f6dd3bf6f976b8ff7be))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Add language settings ([#2913](https://github.com/ReVanced/revanced-manager/issues/2913)) ([df31b39](https://github.com/ReVanced/revanced-manager/commit/df31b39cc8c1fbf00bc3301468e8e7e4b283caf2))
|
||||||
|
|
||||||
|
# app [1.26.0-dev.17](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.16...v1.26.0-dev.17) (2026-01-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* allow updating patches on metered networks ([9d9a0e8](https://github.com/ReVanced/revanced-manager/commit/9d9a0e81dbc9e73e6e3181f6bea9cabb69e49ea8))
|
||||||
|
|
||||||
|
# app [1.26.0-dev.16](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.15...v1.26.0-dev.16) (2025-12-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Show patches as individual steps in patcher screen ([#2889](https://github.com/ReVanced/revanced-manager/issues/2889)) ([11dd6e4](https://github.com/ReVanced/revanced-manager/commit/11dd6e4064099427a8c9bc6f225a19412e5c70e2))
|
||||||
|
|
||||||
|
# app [1.26.0-dev.15](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.14...v1.26.0-dev.15) (2025-12-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* install dialog getting stuck ([#2900](https://github.com/ReVanced/revanced-manager/issues/2900)) ([18a4df9](https://github.com/ReVanced/revanced-manager/commit/18a4df9af9cac120fdb8e4ff7aadd2e2a8d5c1a6))
|
||||||
|
|
||||||
|
# app [1.26.0-dev.14](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.13...v1.26.0-dev.14) (2025-12-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Update selected patch count when SelectionState changes ([#2896](https://github.com/ReVanced/revanced-manager/issues/2896)) ([0d26df0](https://github.com/ReVanced/revanced-manager/commit/0d26df03f463195dae550240c7f652680763079c))
|
||||||
|
|
||||||
|
# app [1.26.0-dev.13](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.12...v1.26.0-dev.13) (2025-12-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Make patcher screen design more consistent with inspiration ([#2805](https://github.com/ReVanced/revanced-manager/issues/2805)) ([dbb6c01](https://github.com/ReVanced/revanced-manager/commit/dbb6c01e89a5e710185ff4304de0ac9e19bed053))
|
||||||
|
|
||||||
|
# app [1.26.0-dev.12](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.11...v1.26.0-dev.12) (2025-12-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Improve trust plugin dialog design ([#2420](https://github.com/ReVanced/revanced-manager/issues/2420)) ([0300da9](https://github.com/ReVanced/revanced-manager/commit/0300da9eac6c0fc29dbbb66622c0d52f4cf68934))
|
||||||
|
|
||||||
|
# app [1.26.0-dev.11](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.10...v1.26.0-dev.11) (2025-10-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Add pure black theme ([#2824](https://github.com/ReVanced/revanced-manager/issues/2824)) ([3d75ffe](https://github.com/ReVanced/revanced-manager/commit/3d75ffe6a7a39efdebe13dbd07c937c1de409ead))
|
||||||
|
|
||||||
|
# app [1.26.0-dev.10](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.9...v1.26.0-dev.10) (2025-10-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* prevent back presses during installation ([2ff7072](https://github.com/ReVanced/revanced-manager/commit/2ff70728b490b92f212a82dcf599bc0c23f589e7))
|
||||||
|
|
||||||
|
# app [1.26.0-dev.9](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.8...v1.26.0-dev.9) (2025-10-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Instantly re-fetch patch bundle on pre-release preference update ([d5671db](https://github.com/ReVanced/revanced-manager/commit/d5671db3a77541c07bbbb4c3baca02f3ba0703f2)), closes [#2784](https://github.com/ReVanced/revanced-manager/issues/2784)
|
||||||
|
|
||||||
|
# app [1.26.0-dev.8](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.7...v1.26.0-dev.8) (2025-10-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Offcenter loading indicator in AppSelector ([12d92ba](https://github.com/ReVanced/revanced-manager/commit/12d92ba8110f5d1ac78aeecfa575444b5c53f561))
|
||||||
|
|
||||||
|
# app [1.26.0-dev.7](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.6...v1.26.0-dev.7) (2025-10-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Improve consistency between pre-release toggles ([e1b768c](https://github.com/ReVanced/revanced-manager/commit/e1b768c4679ecae8bff8007bdab56ff6544b12b6))
|
||||||
|
|
||||||
|
# app [1.26.0-dev.6](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.5...v1.26.0-dev.6) (2025-10-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Broken version comparison ([c327857](https://github.com/ReVanced/revanced-manager/commit/c3278578237dcddd9e7ab79ee80a02fdeef9604d))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Open contributor's GitHub profile when clicked ([#2775](https://github.com/ReVanced/revanced-manager/issues/2775)) ([2571cb8](https://github.com/ReVanced/revanced-manager/commit/2571cb8c1108e9c1ed84950f17692c09d66e0556))
|
||||||
|
|
||||||
|
# app [1.26.0-dev.5](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.4...v1.26.0-dev.5) (2025-10-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Toggle to use pre-release versions of ReVanced Patches ([08cec67](https://github.com/ReVanced/revanced-manager/commit/08cec674bbbe5297090ac5ee6039569975fbe9e7))
|
||||||
|
|
||||||
|
# app [1.26.0-dev.4](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.3...v1.26.0-dev.4) (2025-10-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add newlines to debug logs ([4753873](https://github.com/ReVanced/revanced-manager/commit/4753873866b575e2dcb160020df63f63862c8f33))
|
||||||
|
|
||||||
|
# app [1.26.0-dev.3](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.2...v1.26.0-dev.3) (2025-10-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Toggle to use pre-release versions of ReVanced Manager ([#2773](https://github.com/ReVanced/revanced-manager/issues/2773)) ([d758964](https://github.com/ReVanced/revanced-manager/commit/d7589647426b3d3438161a2f0b59bf4f154ac34b))
|
||||||
|
|
||||||
|
# app [1.26.0-dev.2](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.1...v1.26.0-dev.2) (2025-10-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Migration of keystore, by fixing mislabeling of alias as cn ([#2769](https://github.com/ReVanced/revanced-manager/issues/2769)) ([aeab639](https://github.com/ReVanced/revanced-manager/commit/aeab639b2b09e8bbd2478cfbf5a518586405c0f7))
|
||||||
|
|
||||||
|
# app [1.26.0-dev.1](https://github.com/ReVanced/revanced-manager/compare/v1.25.1...v1.26.0-dev.1) (2025-10-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* `ExtendedFloatingActionButton` not accessible by screen readers ([#2080](https://github.com/ReVanced/revanced-manager/issues/2080)) ([e4f19b0](https://github.com/ReVanced/revanced-manager/commit/e4f19b0c251e818cce59e11362a29dc8f657e065))
|
||||||
|
* add bounds checks in patch selector ([483be5d](https://github.com/ReVanced/revanced-manager/commit/483be5d722db2be2595f6f6dd0c537a6c8487daf))
|
||||||
|
* Add missing header for "Updates" settings ([#2642](https://github.com/ReVanced/revanced-manager/issues/2642)) ([d4d2056](https://github.com/ReVanced/revanced-manager/commit/d4d2056585ccd4a0456318448dc822c0f40c9c50))
|
||||||
|
* Allow different app version when downloading via plugin if setting is off ([#2579](https://github.com/ReVanced/revanced-manager/issues/2579)) ([59d233e](https://github.com/ReVanced/revanced-manager/commit/59d233e15c885104900c7d4129fb4839c4da81e0))
|
||||||
|
* always use default patch selection if customization is disabled ([cc77181](https://github.com/ReVanced/revanced-manager/commit/cc771817cba3dfd8f704cb7ecc9089ad7911c6ce))
|
||||||
|
* android icon not loading in app selector ([deea682](https://github.com/ReVanced/revanced-manager/commit/deea68265157da65ef98986d751e2551797522e0))
|
||||||
|
* automatically focus search views ([d23d673](https://github.com/ReVanced/revanced-manager/commit/d23d673c4703cdfa3be3a292873bbb37bea30ac7))
|
||||||
|
* available updates dialog list item color ([1a54313](https://github.com/ReVanced/revanced-manager/commit/1a54313c1dc4efbb8b274201a79e28661a7ecf64))
|
||||||
|
* Broken header padding in `AlertDialogExtended` when using an Icon ([8d939a6](https://github.com/ReVanced/revanced-manager/commit/8d939a6669909a44382fc7404276f2eeefcf728d))
|
||||||
|
* broken logo in about page on release builds ([ad775f3](https://github.com/ReVanced/revanced-manager/commit/ad775f3059345dd93ff2baf6d018c2beecc413df))
|
||||||
|
* buildfile syntax ([#66](https://github.com/ReVanced/revanced-manager/issues/66)) ([5c17a78](https://github.com/ReVanced/revanced-manager/commit/5c17a78e46db586642d53362267472fbbd47ae8c))
|
||||||
|
* bundles not loading on Android 14 ([56896d6](https://github.com/ReVanced/revanced-manager/commit/56896d6197baa836bcd4a499ea2cee487e3d07c8))
|
||||||
|
* Change the title in the Update screen from "Updates" to "Update" ([5f23769](https://github.com/ReVanced/revanced-manager/commit/5f2376919bd036987eba8188e3a1a2ff53ef6793)), closes [#1960](https://github.com/ReVanced/revanced-manager/issues/1960)
|
||||||
|
* cleanup advanced settings screen ([02ea5c6](https://github.com/ReVanced/revanced-manager/commit/02ea5c6d4a2e6baa7c034b614deb6e4232cf6d0b))
|
||||||
|
* **Compose:** Adjusted universal patches safeguard and warnings ([#2550](https://github.com/ReVanced/revanced-manager/issues/2550)) ([663cf2d](https://github.com/ReVanced/revanced-manager/commit/663cf2d6b86c276c6bb236af8e05a4f69df9eba0))
|
||||||
|
* contributors screen fix ([#1256](https://github.com/ReVanced/revanced-manager/issues/1256)) ([dc73462](https://github.com/ReVanced/revanced-manager/commit/dc73462ac41bd5f1813358eb5e2265a3e2e7c0f9))
|
||||||
|
* contributors screen repository name ([426b289](https://github.com/ReVanced/revanced-manager/commit/426b28932fe37a6d7412685819ffc8e26b69d31c))
|
||||||
|
* Correct preference description ([#2619](https://github.com/ReVanced/revanced-manager/issues/2619)) ([0096169](https://github.com/ReVanced/revanced-manager/commit/0096169af8f9e2db6c22b8e88f0dfe1cab1260be))
|
||||||
|
* Correctly display universal patches warning ([#2570](https://github.com/ReVanced/revanced-manager/issues/2570)) ([24c4cd3](https://github.com/ReVanced/revanced-manager/commit/24c4cd3f991953dd00b5bf5e7c3ec965315a9528))
|
||||||
|
* correctly patch apk files ([c5cb18a](https://github.com/ReVanced/revanced-manager/commit/c5cb18a7eab838ea096577780335a29b9771b43d))
|
||||||
|
* crash caused by compose inlining bug ([05fe058](https://github.com/ReVanced/revanced-manager/commit/05fe0581516a373cc26dd559d3fc7f21fcf16f3f))
|
||||||
|
* crash when removing used bundles ([189c993](https://github.com/ReVanced/revanced-manager/commit/189c993ada6406db6f8c48c4051c5bd9fac98e2b))
|
||||||
|
* delete temporary files ([#1341](https://github.com/ReVanced/revanced-manager/issues/1341)) ([b03f7b1](https://github.com/ReVanced/revanced-manager/commit/b03f7b18a029465142d08fe1ed68e92c81586a5f))
|
||||||
|
* disable `WebView` history ([#1278](https://github.com/ReVanced/revanced-manager/issues/1278)) ([a811df9](https://github.com/ReVanced/revanced-manager/commit/a811df9547da33fc61397cb33ba5fd35ee470ff9))
|
||||||
|
* display version from manifest ([#2634](https://github.com/ReVanced/revanced-manager/issues/2634)) ([1fb94b7](https://github.com/ReVanced/revanced-manager/commit/1fb94b711fdbbbca7d9baaa90c53faf208fc4d0d))
|
||||||
|
* Do not poll battery optimization status ([#2491](https://github.com/ReVanced/revanced-manager/issues/2491)) ([26778f5](https://github.com/ReVanced/revanced-manager/commit/26778f57e6dd185d9aed1086aa03659a2e91d1a9))
|
||||||
|
* don't store app list in parcel ([e7802ed](https://github.com/ReVanced/revanced-manager/commit/e7802ed3d714cbe6e29409d27989c65d4d7ce6a5))
|
||||||
|
* dont crash when the bundle cannot be downloaded ([4d201f1](https://github.com/ReVanced/revanced-manager/commit/4d201f17f2ce01aad6adb456a49c3f03526c5ad3))
|
||||||
|
* **downloader:** versions not loading correctly ([16c4290](https://github.com/ReVanced/revanced-manager/commit/16c4290f05d94cbe53e68cb98307d7be1bfce7af))
|
||||||
|
* handle edge-to-edge properly in fullscreen dialogs ([eba92e2](https://github.com/ReVanced/revanced-manager/commit/eba92e2644663b10e7e17f2cf955afefe260d769))
|
||||||
|
* handle exceptions when checking for bundle updates ([1dd6738](https://github.com/ReVanced/revanced-manager/commit/1dd673896454710094e83789abb585c106ee6bcb))
|
||||||
|
* Handle open source licenses page crash ([#2569](https://github.com/ReVanced/revanced-manager/issues/2569)) ([f2ea007](https://github.com/ReVanced/revanced-manager/commit/f2ea00757a76ed8758bc0d4df54843c89483c986))
|
||||||
|
* hide patch button ([#1284](https://github.com/ReVanced/revanced-manager/issues/1284)) ([dadc546](https://github.com/ReVanced/revanced-manager/commit/dadc5462e352e91cf971395def91d693677701bc))
|
||||||
|
* Ignore long click when already in delete mode ([6f6296b](https://github.com/ReVanced/revanced-manager/commit/6f6296b8cde56d5fc73e00ef671ca7ab431455f4)), closes [#2503](https://github.com/ReVanced/revanced-manager/issues/2503)
|
||||||
|
* import bundles on another thread ([0383bd7](https://github.com/ReVanced/revanced-manager/commit/0383bd74f73a3523d539c44cdf38b0e857c16bdc))
|
||||||
|
* import export screen UX ([69c119d](https://github.com/ReVanced/revanced-manager/commit/69c119d545ac811c605124173e5cbc97a9064c79))
|
||||||
|
* Improve background running notification ([#2614](https://github.com/ReVanced/revanced-manager/issues/2614)) ([05444d8](https://github.com/ReVanced/revanced-manager/commit/05444d8824a429c7e554d0597f8997e670936a63))
|
||||||
|
* improve bundle page strings ([2a63a61](https://github.com/ReVanced/revanced-manager/commit/2a63a6163a8d2e6ee649cb22099b426ed605de8f))
|
||||||
|
* improve keystore import error handling and show toast ([cd142a7](https://github.com/ReVanced/revanced-manager/commit/cd142a70d3f210161d3c1f20d2cb82a70432469f))
|
||||||
|
* Inconsistent padding for battery optimisation warning ([6c3a99a](https://github.com/ReVanced/revanced-manager/commit/6c3a99a4921ab4438a038ad4c4bccd0326fdd565))
|
||||||
|
* **installer:** make the correct column scrollable ([64496bf](https://github.com/ReVanced/revanced-manager/commit/64496bfbe77a9a44f5535fd5f12eee803ac7c26a))
|
||||||
|
* **installer:** progress tracking ([f547bb7](https://github.com/ReVanced/revanced-manager/commit/f547bb7ab1b7149d7290729527714168a2561b23))
|
||||||
|
* **installer:** properly track worker state ([#32](https://github.com/ReVanced/revanced-manager/issues/32)) ([de1ef23](https://github.com/ReVanced/revanced-manager/commit/de1ef23824227796c8583242e624f83d9dae5af3))
|
||||||
|
* **installer:** save step incorrectly being marked as completed ([0264308](https://github.com/ReVanced/revanced-manager/commit/0264308b6dad051db80da6f130e8d28d86b38f04))
|
||||||
|
* **installer:** sign and install on threads ([3d59ee5](https://github.com/ReVanced/revanced-manager/commit/3d59ee51acc5a6ebb17f68c0462d17d7ecb0f07c))
|
||||||
|
* jvm signature clash error ([ee0f342](https://github.com/ReVanced/revanced-manager/commit/ee0f34245636027d55bd5bdfce4d6a5e6c3b3dcd))
|
||||||
|
* library info not being embedded ([8c9fe69](https://github.com/ReVanced/revanced-manager/commit/8c9fe6989fc6d05afd53baa877f1e6dffc067b50))
|
||||||
|
* load patch bundles earlier ([a2f9e2f](https://github.com/ReVanced/revanced-manager/commit/a2f9e2f1da961a13b2b20e2812593031c9339b88))
|
||||||
|
* Match "Installation incompatible" dialog message with Flutter Manager ([#2231](https://github.com/ReVanced/revanced-manager/issues/2231)) ([fedaedf](https://github.com/ReVanced/revanced-manager/commit/fedaedfda112260144b0b9b0776509ddb3438046))
|
||||||
|
* minify crash on building release ([#1245](https://github.com/ReVanced/revanced-manager/issues/1245)) ([6561e4c](https://github.com/ReVanced/revanced-manager/commit/6561e4c97c19134b22b72e19fad3884f99327b9a))
|
||||||
|
* more android 34 fixes ([7fb1e27](https://github.com/ReVanced/revanced-manager/commit/7fb1e27617b69803b3d4463993b2290877502545))
|
||||||
|
* move battery warning to dashboard ([3a05150](https://github.com/ReVanced/revanced-manager/commit/3a05150fa33f119ecdf436f8508862ef81c327a0))
|
||||||
|
* Move temporary files outside of the cache directory ([#2122](https://github.com/ReVanced/revanced-manager/issues/2122)) ([b93ecc0](https://github.com/ReVanced/revanced-manager/commit/b93ecc0db20339393e1296c44ce4b1dbd837b577))
|
||||||
|
* Offset badge ([c73fdfd](https://github.com/ReVanced/revanced-manager/commit/c73fdfdd2d3a1b8552d9c26df575b3019346596d))
|
||||||
|
* only perform haptics on events ([e55566d](https://github.com/ReVanced/revanced-manager/commit/e55566d3df25480260922f0418b4bbee5d7b7a07))
|
||||||
|
* option state crash ([#1456](https://github.com/ReVanced/revanced-manager/issues/1456)) ([f183b6d](https://github.com/ReVanced/revanced-manager/commit/f183b6d8a6b139fe3e84d5ea3a9658ef900453bc))
|
||||||
|
* parcel error for nullable types ([336eed3](https://github.com/ReVanced/revanced-manager/commit/336eed3a95111ebbe456321f5986e6875ded354e))
|
||||||
|
* pass worker inputs without serialization ([#44](https://github.com/ReVanced/revanced-manager/issues/44)) ([059a72b](https://github.com/ReVanced/revanced-manager/commit/059a72b9dd9103d2b3704daa7dbb13ad83971460))
|
||||||
|
* patch count remaining at zero when using process runtime ([#2542](https://github.com/ReVanced/revanced-manager/issues/2542)) ([f5e1e0b](https://github.com/ReVanced/revanced-manager/commit/f5e1e0b0659e5775dd460b8dfc15427eb0175139))
|
||||||
|
* patch options reset button being broken ([e1647fd](https://github.com/ReVanced/revanced-manager/commit/e1647fdef0c9f68e171a2d15e2b6e744da6bbaf5))
|
||||||
|
* Patch process cancelation dialog conditions ([#2554](https://github.com/ReVanced/revanced-manager/issues/2554)) ([e97b19d](https://github.com/ReVanced/revanced-manager/commit/e97b19d2b65dbfc49ed062b123c363e412b9bf8e))
|
||||||
|
* Patch selection screen padding ([#2533](https://github.com/ReVanced/revanced-manager/issues/2533)) ([cd2dbcc](https://github.com/ReVanced/revanced-manager/commit/cd2dbcc841e56dac99230ea6501af87c43e9c572))
|
||||||
|
* **patcher:** add notification and wakelock to worker; chore: add app icon ([8b6d32d](https://github.com/ReVanced/revanced-manager/commit/8b6d32dd7b3ca4c694414a55a1b6202b62636530))
|
||||||
|
* patches not being reloaded ([dccf861](https://github.com/ReVanced/revanced-manager/commit/dccf86163af34341e3e451df9f24356c7294ae1e))
|
||||||
|
* **patches selector:** copy the selected patches list ([70e49aa](https://github.com/ReVanced/revanced-manager/commit/70e49aaaa3a42510cb9ced2209c90cd1da98391d))
|
||||||
|
* perform selected app operations in the correct order ([34cf848](https://github.com/ReVanced/revanced-manager/commit/34cf848baaaa2504d162c515a95240d45bd7092a))
|
||||||
|
* permission error when using installed app ([8767f0e](https://github.com/ReVanced/revanced-manager/commit/8767f0e99c6de5bbb0a690ced40f6e9a486f0828))
|
||||||
|
* Playback Switch's Haptic Feedback ([#2639](https://github.com/ReVanced/revanced-manager/issues/2639)) ([9fdca5a](https://github.com/ReVanced/revanced-manager/commit/9fdca5a0afd6be8a24e2ec09eec0000b0b9cd179))
|
||||||
|
* process death resilience and account for android 11 bug ([#2355](https://github.com/ReVanced/revanced-manager/issues/2355)) ([83eeeae](https://github.com/ReVanced/revanced-manager/commit/83eeeae801827800a0787e9e753c72d2a24d7970))
|
||||||
|
* progress bar not updating ([dcaa38c](https://github.com/ReVanced/revanced-manager/commit/dcaa38c8824f54da7a833c354b247f309d1c9871))
|
||||||
|
* release builds not working properly ([6f6476e](https://github.com/ReVanced/revanced-manager/commit/6f6476e85158cad4e2497e9f72b73c4dc948f0bc))
|
||||||
|
* remove battery optimization notification if user grants the permission ([9863c51](https://github.com/ReVanced/revanced-manager/commit/9863c5161a1bc16941a323e654f80f8cb0122f9f))
|
||||||
|
* remove the unique constraint for patch bundle names ([ea29d0f](https://github.com/ReVanced/revanced-manager/commit/ea29d0f00c3b3b2c137c4849e6c445a6bf9a180f))
|
||||||
|
* Remove unnecessary screen padding ([8419f75](https://github.com/ReVanced/revanced-manager/commit/8419f75d597dd198aa1029fae2109646c5874078)), closes [#2062](https://github.com/ReVanced/revanced-manager/issues/2062)
|
||||||
|
* remove unused function preventing compilation ([2297e94](https://github.com/ReVanced/revanced-manager/commit/2297e94cb81a9a22ea032d8e247769774ca85087))
|
||||||
|
* Reset cached theme on theme change to avoid broken colors ([#2527](https://github.com/ReVanced/revanced-manager/issues/2527)) ([9a82b78](https://github.com/ReVanced/revanced-manager/commit/9a82b785280954973cafc5e6dccb3c90fdb5ef49))
|
||||||
|
* run blocking IO operations in the correct context ([969ddb7](https://github.com/ReVanced/revanced-manager/commit/969ddb7bef321d7aa2a682b8128b1f755f35c28b))
|
||||||
|
* run props flow on correct dispatcher ([#2035](https://github.com/ReVanced/revanced-manager/issues/2035)) ([d3d4c27](https://github.com/ReVanced/revanced-manager/commit/d3d4c27f6d7affceef233a0138ee6c985c7f56bc))
|
||||||
|
* Screen turns off while patching due to wrong WakeLock ([#2147](https://github.com/ReVanced/revanced-manager/issues/2147)) ([4de5340](https://github.com/ReVanced/revanced-manager/commit/4de534094adc0665021d3ba129a648d896718568))
|
||||||
|
* scrolling in patch selector ([154f036](https://github.com/ReVanced/revanced-manager/commit/154f036fe956096bca983fe9d6654ccca38fd8ac))
|
||||||
|
* Selected patch count ([#2559](https://github.com/ReVanced/revanced-manager/issues/2559)) ([a91ff60](https://github.com/ReVanced/revanced-manager/commit/a91ff60533b44629ea60e8cd6acceeb80b0253b7))
|
||||||
|
* serialization not working ([4d04ae0](https://github.com/ReVanced/revanced-manager/commit/4d04ae088c406d84936120cb753cd1f11fb8a8c2))
|
||||||
|
* show available and selected patches in patch selector screen ([61f1ee0](https://github.com/ReVanced/revanced-manager/commit/61f1ee0627d6cbb6b9a4d226eb6c2f9e0b8c6453))
|
||||||
|
* show install button when installation has been cancelled ([93f4a5b](https://github.com/ReVanced/revanced-manager/commit/93f4a5bb7c912ca77bb04e414432922c89d3e2c0))
|
||||||
|
* Show selection warning also on patch option ([#2643](https://github.com/ReVanced/revanced-manager/issues/2643)) ([3b82767](https://github.com/ReVanced/revanced-manager/commit/3b82767a897eeca1dda1d8343f1db4207050e960))
|
||||||
|
* sources screen being misaligned during transitions ([2ac3d5c](https://github.com/ReVanced/revanced-manager/commit/2ac3d5c483d5cc4776681ed3f900550a4e45f616))
|
||||||
|
* specify `multithreadingDexFileWriter` in `PatcherOptions` ([#1402](https://github.com/ReVanced/revanced-manager/issues/1402)) ([3f362b6](https://github.com/ReVanced/revanced-manager/commit/3f362b605fbce3ea72e7c95b7e0bc614443c7d44))
|
||||||
|
* Support patching on ARMv7 by updating AAPT2 ([#2084](https://github.com/ReVanced/revanced-manager/issues/2084)) ([15b47f9](https://github.com/ReVanced/revanced-manager/commit/15b47f9bb6cd6bb0360fda6ac641cd4c75542287))
|
||||||
|
* Transparent status on fullscreen dialog ([#2654](https://github.com/ReVanced/revanced-manager/issues/2654)) ([a8820a4](https://github.com/ReVanced/revanced-manager/commit/a8820a4daf71704f6945b8f794495fe8a8d7589e))
|
||||||
|
* Turn off filters by default ([#2079](https://github.com/ReVanced/revanced-manager/issues/2079)) ([44f8b1f](https://github.com/ReVanced/revanced-manager/commit/44f8b1fb6bffed5866ada356910119465320a9a8))
|
||||||
|
* typo in string name `import_keystore_description` ([#1273](https://github.com/ReVanced/revanced-manager/issues/1273)) ([933e69e](https://github.com/ReVanced/revanced-manager/commit/933e69e21e97fede2183a26dd1645a6eb96c4509))
|
||||||
|
* **ui:** make entire patches view button selectable ([#1271](https://github.com/ReVanced/revanced-manager/issues/1271)) ([83cdaae](https://github.com/ReVanced/revanced-manager/commit/83cdaaee183ff1b6d905977df38fe4e47f7d5973))
|
||||||
|
* Updates popup shows incorrect names ([#1283](https://github.com/ReVanced/revanced-manager/issues/1283)) ([c879faf](https://github.com/ReVanced/revanced-manager/commit/c879faf2eb338476c6abd9f104922b0d49f95cd6))
|
||||||
|
* Use `compatible` rather than `support` when referring to patch compatibility ([#2422](https://github.com/ReVanced/revanced-manager/issues/2422)) ([8b3c4eb](https://github.com/ReVanced/revanced-manager/commit/8b3c4eb91c491a0971e2ccf7d46012437eca5c25))
|
||||||
|
* use correct `getViewModel` ([5b6ae80](https://github.com/ReVanced/revanced-manager/commit/5b6ae800fdfc93ef5058b21b3e48daac2a4e1358))
|
||||||
|
* use correct classes to determine option type ([e833bf4](https://github.com/ReVanced/revanced-manager/commit/e833bf4ad14811bb6880ae2d97055e4ce0de222f))
|
||||||
|
* use correct directory ([9e1ebb3](https://github.com/ReVanced/revanced-manager/commit/9e1ebb390244dcb9af03a9164a32386481ec5691))
|
||||||
|
* Use FAB instead of ListItem to patch in App Overview ([6ace71b](https://github.com/ReVanced/revanced-manager/commit/6ace71b739302466274ce9b46f5f7dd6ab9da05d)), closes [#1995](https://github.com/ReVanced/revanced-manager/issues/1995)
|
||||||
|
* use proper update icon ([b59a161](https://github.com/ReVanced/revanced-manager/commit/b59a16191a61c64275137c4a6145fd30d68aa480))
|
||||||
|
* use ReVanced ring logo in about section ([#1302](https://github.com/ReVanced/revanced-manager/issues/1302)) ([933a4a3](https://github.com/ReVanced/revanced-manager/commit/933a4a32203425e745e05615217a8d0975c2e959))
|
||||||
|
* Use the correct icon in API URL dialog ([c22e5b4](https://github.com/ReVanced/revanced-manager/commit/c22e5b4051515e0f02828a2b30f6af19b48ba55f)), closes [#1972](https://github.com/ReVanced/revanced-manager/issues/1972)
|
||||||
|
* use upsert when modifying installed apps ([90edf1d](https://github.com/ReVanced/revanced-manager/commit/90edf1ddd0de29b299855810402a31828d989d04))
|
||||||
|
* **VersionSelector:** use correct LazyColumn item key ([413fe98](https://github.com/ReVanced/revanced-manager/commit/413fe980a8c0b45e3924c98b2fbd1a3e9b579528))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **about screen:** complete about screen ([1d6b34a](https://github.com/ReVanced/revanced-manager/commit/1d6b34a39f76e8e733649f7fcfeb20eb1009a39a))
|
||||||
|
* Add `isScrollingUp` support for ScrollState ([bf049c3](https://github.com/ReVanced/revanced-manager/commit/bf049c3c1ac12a60c5c6226b5c3fec7f72caa7db))
|
||||||
|
* add ability to share debug logs ([feb0ca4](https://github.com/ReVanced/revanced-manager/commit/feb0ca4cf315e5d332f36039fbb989b3cfb9cf58))
|
||||||
|
* add checkboxes to the downloaded apps page ([ca93524](https://github.com/ReVanced/revanced-manager/commit/ca93524be0b37f38b860d8512c81d2898b2860af))
|
||||||
|
* Add confirm dialogs when toggling dangerous settings ([#2072](https://github.com/ReVanced/revanced-manager/issues/2072)) ([6643276](https://github.com/ReVanced/revanced-manager/commit/66432764cfe8192f4cf8e599a592f27c675f25ec))
|
||||||
|
* Add confirmation dialog to "Reset" options ([#2576](https://github.com/ReVanced/revanced-manager/issues/2576)) ([f32ffbb](https://github.com/ReVanced/revanced-manager/commit/f32ffbb6f2224f886af14205721fb2372f396de2))
|
||||||
|
* Add downloader plugin system ([#2041](https://github.com/ReVanced/revanced-manager/issues/2041)) ([ca38737](https://github.com/ReVanced/revanced-manager/commit/ca3873778307612b93af3273ffe4821c6a5e398d))
|
||||||
|
* add external process runtime ([#1799](https://github.com/ReVanced/revanced-manager/issues/1799)) ([0d73e0c](https://github.com/ReVanced/revanced-manager/commit/0d73e0cd32b6af3526c226ce4695c7e905f65b15))
|
||||||
|
* Add haptic feedback ([#1457](https://github.com/ReVanced/revanced-manager/issues/1457)) ([76e0c95](https://github.com/ReVanced/revanced-manager/commit/76e0c9518746620cd2723a99c310f92f5b3fd996))
|
||||||
|
* Add installer status dialog ([#1473](https://github.com/ReVanced/revanced-manager/issues/1473)) ([43b3743](https://github.com/ReVanced/revanced-manager/commit/43b37432138d7cd8a507efad80827d6f3bdcdf08))
|
||||||
|
* add network checks for features that require it ([f3f8bc4](https://github.com/ReVanced/revanced-manager/commit/f3f8bc4ec2f593ade91324d78f9ce83f60ef65cc))
|
||||||
|
* add patch bundle info screen ([#55](https://github.com/ReVanced/revanced-manager/issues/55)) ([8ae4e85](https://github.com/ReVanced/revanced-manager/commit/8ae4e850dae9cf4df14afe90048ca0b0a48389ac))
|
||||||
|
* add patches selector bottom sheet ([#1360](https://github.com/ReVanced/revanced-manager/issues/1360)) ([f6fb534](https://github.com/ReVanced/revanced-manager/commit/f6fb534e04777b4f0ec2ff2b13768c724c68c028))
|
||||||
|
* add required options screen ([#2378](https://github.com/ReVanced/revanced-manager/issues/2378)) ([3a63e42](https://github.com/ReVanced/revanced-manager/commit/3a63e42df9ce50069a573d98cf44a8abec03b639))
|
||||||
|
* Add reset button to custom API ([#2076](https://github.com/ReVanced/revanced-manager/issues/2076)) ([df52a7b](https://github.com/ReVanced/revanced-manager/commit/df52a7bdef05e1c9f034ae067c3dd183fb8fdffd)), closes [#2051](https://github.com/ReVanced/revanced-manager/issues/2051)
|
||||||
|
* Add sensitivity to `isScrollingUp` ([f6ca4e9](https://github.com/ReVanced/revanced-manager/commit/f6ca4e95551193c8d21afd09872d9bbe6c80c0e8))
|
||||||
|
* add social links ([#1294](https://github.com/ReVanced/revanced-manager/issues/1294)) ([7df3350](https://github.com/ReVanced/revanced-manager/commit/7df3350acb4aae957e2a7c0d2f30faf6cae6ab85))
|
||||||
|
* add toast feedback to the bundle update button ([ea50e65](https://github.com/ReVanced/revanced-manager/commit/ea50e65ab1d626152bdd40c1893cd408b7271472))
|
||||||
|
* add user agent ([#1382](https://github.com/ReVanced/revanced-manager/issues/1382)) ([3aea6cb](https://github.com/ReVanced/revanced-manager/commit/3aea6cbaecc9db103e9a3925b3c4a531de6c5f0e))
|
||||||
|
* advanced settings page with device info ([#51](https://github.com/ReVanced/revanced-manager/issues/51)) ([86e4244](https://github.com/ReVanced/revanced-manager/commit/86e42449eb553417726b95f79f6edd7f526f6d44))
|
||||||
|
* allow bundles to use classes from other bundles ([#1951](https://github.com/ReVanced/revanced-manager/issues/1951)) ([af8e2b4](https://github.com/ReVanced/revanced-manager/commit/af8e2b44c027d978046a0e7926f1425f0348b098))
|
||||||
|
* allow user to save logs ([a008cf5](https://github.com/ReVanced/revanced-manager/commit/a008cf5dd143fafb1f642cd037db29393716f7d5))
|
||||||
|
* animate the arrow button ([db070b1](https://github.com/ReVanced/revanced-manager/commit/db070b125bf08ff251450259045755e6469c2d5e))
|
||||||
|
* app downloader ([#43](https://github.com/ReVanced/revanced-manager/issues/43)) ([1f1a480](https://github.com/ReVanced/revanced-manager/commit/1f1a480d51edb310934523024c52e0c19b066662))
|
||||||
|
* app selector screen ([373cc4b](https://github.com/ReVanced/revanced-manager/commit/373cc4bbb1a8194bf9475d0a13e1c154cd87480b))
|
||||||
|
* **app-selector:** show patchable installed apps first ([#1496](https://github.com/ReVanced/revanced-manager/issues/1496)) ([afb0f80](https://github.com/ReVanced/revanced-manager/commit/afb0f80de5a73c213f77bfde761ea1ea0886abef))
|
||||||
|
* armv7 warning ([2ffcaec](https://github.com/ReVanced/revanced-manager/commit/2ffcaec724d5a13b816e04813d45cde75681eb69))
|
||||||
|
* Automatic language detection ([#2032](https://github.com/ReVanced/revanced-manager/issues/2032)) ([36a1c3f](https://github.com/ReVanced/revanced-manager/commit/36a1c3f36807500fbe820bf4142fef159b138c7d))
|
||||||
|
* backend ([45a54d1](https://github.com/ReVanced/revanced-manager/commit/45a54d1608a77547e06748867d63a452224727b6))
|
||||||
|
* better installer ui ([#29](https://github.com/ReVanced/revanced-manager/issues/29)) ([14888f9](https://github.com/ReVanced/revanced-manager/commit/14888f9da71ecf1c50d770123d1e8dd09aa6c8b1))
|
||||||
|
* **bundles tab:** add BackHandler ([a9171e1](https://github.com/ReVanced/revanced-manager/commit/a9171e17bd628601f1e074a7fcdf74c15cb73709))
|
||||||
|
* Change "Update" to "Show" in Update Available notification ([5c43413](https://github.com/ReVanced/revanced-manager/commit/5c434137d332aabaaca236b6f9616d7727d0b3d2)), closes [#1959](https://github.com/ReVanced/revanced-manager/issues/1959)
|
||||||
|
* change appID and name of debug builds ([5b3e9e5](https://github.com/ReVanced/revanced-manager/commit/5b3e9e595cded277c051cc669d9f29bcb6ce5d18))
|
||||||
|
* **Changelogs:** overall improvement ([#1429](https://github.com/ReVanced/revanced-manager/issues/1429)) ([2a3590d](https://github.com/ReVanced/revanced-manager/commit/2a3590ddd2cc74b746a3f632a93970bfa23cf384))
|
||||||
|
* check for updates on startup ([#1462](https://github.com/ReVanced/revanced-manager/issues/1462)) ([bb2164e](https://github.com/ReVanced/revanced-manager/commit/bb2164e1a95a698b1b0f69e725af5e0e1e45b868))
|
||||||
|
* check if the version being used is the recommended version ([#1675](https://github.com/ReVanced/revanced-manager/issues/1675)) ([9d961f6](https://github.com/ReVanced/revanced-manager/commit/9d961f6a52d15ed6116afc78c7008460347da69a))
|
||||||
|
* Collapse ExtendedFAB on scroll ([#1630](https://github.com/ReVanced/revanced-manager/issues/1630)) ([b5c1f6d](https://github.com/ReVanced/revanced-manager/commit/b5c1f6d732b65c1c9becb7962c51a70a840dea73))
|
||||||
|
* **Compose:** Add confirmation dialog on multiple operations ([#2529](https://github.com/ReVanced/revanced-manager/issues/2529)) ([2671e68](https://github.com/ReVanced/revanced-manager/commit/2671e68004269deebdedaee38a6692b2302ca732))
|
||||||
|
* **Compose:** hide developer settings ([#2551](https://github.com/ReVanced/revanced-manager/issues/2551)) ([0030c7a](https://github.com/ReVanced/revanced-manager/commit/0030c7a7885feee0578ee1423ee2aefc6a0e2c2c))
|
||||||
|
* **Compose:** Improve patches selector tab by adding the bundle version ([#2545](https://github.com/ReVanced/revanced-manager/issues/2545)) ([3710675](https://github.com/ReVanced/revanced-manager/commit/3710675ac0ca77cecfb172b4cf148f41a762bf06))
|
||||||
|
* **Compose:** Move developer options to top level ([#2528](https://github.com/ReVanced/revanced-manager/issues/2528)) ([cedc6ad](https://github.com/ReVanced/revanced-manager/commit/cedc6ad49f23d778a52a8846f9e384fd2106e074))
|
||||||
|
* contributors screen ([#42](https://github.com/ReVanced/revanced-manager/issues/42)) ([3f54381](https://github.com/ReVanced/revanced-manager/commit/3f54381d307fd71296be18e97a1ab870f1cdc297))
|
||||||
|
* **Contributors Screen:** implement design from Figma ([#1465](https://github.com/ReVanced/revanced-manager/issues/1465)) ([d5bdc29](https://github.com/ReVanced/revanced-manager/commit/d5bdc293f308e2a283d744afdc1aed6a165f7166))
|
||||||
|
* Dashboard Screen ([#18](https://github.com/ReVanced/revanced-manager/issues/18)) ([a127b95](https://github.com/ReVanced/revanced-manager/commit/a127b959ead5a9c83a0c4f7e7840aeeb68362c0d))
|
||||||
|
* disable filter chips when there are no patches ([fd520bb](https://github.com/ReVanced/revanced-manager/commit/fd520bba700bae9d8eae745ce23a95b07b7f7d34))
|
||||||
|
* dont ask for root on launch ([9562d80](https://github.com/ReVanced/revanced-manager/commit/9562d80bfdc785fe5ed512a15cfd7c0e09091acc))
|
||||||
|
* download apps in patcher screen ([#73](https://github.com/ReVanced/revanced-manager/issues/73)) ([a854221](https://github.com/ReVanced/revanced-manager/commit/a854221969c363712a0b3de84607092709db291f))
|
||||||
|
* experimental patches setting ([b07fd23](https://github.com/ReVanced/revanced-manager/commit/b07fd2321dd0aecce556f341e2b18f930baa58fd))
|
||||||
|
* filter options for patches ([62bccd1](https://github.com/ReVanced/revanced-manager/commit/62bccd150441747e5cd6de71de304e416922bdda))
|
||||||
|
* finish implementing the sources system ([#70](https://github.com/ReVanced/revanced-manager/issues/70)) ([858b0ec](https://github.com/ReVanced/revanced-manager/commit/858b0ec5b456043fa61b681bbbd195fd9c30a6f0))
|
||||||
|
* get bundle information from jar manifest ([#2027](https://github.com/ReVanced/revanced-manager/issues/2027)) ([60fdec9](https://github.com/ReVanced/revanced-manager/commit/60fdec9804c763ef9308a7a56d245401dbd35d7c))
|
||||||
|
* hide tabs when 1 bundle is used ([41268ca](https://github.com/ReVanced/revanced-manager/commit/41268ca80b71f68dbf9523fa7bac34feeec7d011))
|
||||||
|
* hide unfinished pages in release mode ([c199801](https://github.com/ReVanced/revanced-manager/commit/c199801fb7f91306538391177d240cf1121964d2))
|
||||||
|
* Highlight links in Markdown ([7bf8988](https://github.com/ReVanced/revanced-manager/commit/7bf89887e420a402b30da4796ba3648147f00394)), closes [#1962](https://github.com/ReVanced/revanced-manager/issues/1962)
|
||||||
|
* implement DI ([7fa7b9d](https://github.com/ReVanced/revanced-manager/commit/7fa7b9d53a3217c7e1e4c70a524fd68ae170c832))
|
||||||
|
* implement more patch option types ([#2015](https://github.com/ReVanced/revanced-manager/issues/2015)) ([b18c678](https://github.com/ReVanced/revanced-manager/commit/b18c6783547e910fa2dbd3d7edcc5fe329e6d921))
|
||||||
|
* implement navigation ([7fc6ec5](https://github.com/ReVanced/revanced-manager/commit/7fc6ec5c2cf8eb9ebfc3dda01cdfd80962be1f8f))
|
||||||
|
* implement Submit Issue button ([#1276](https://github.com/ReVanced/revanced-manager/issues/1276)) ([a269a39](https://github.com/ReVanced/revanced-manager/commit/a269a39aa4a34b94aef4e1e85126c571e96be575))
|
||||||
|
* improve accessibility ([#64](https://github.com/ReVanced/revanced-manager/issues/64)) ([39b08e5](https://github.com/ReVanced/revanced-manager/commit/39b08e5201d2cec6bdb67f9386120a7a40c9ccc6))
|
||||||
|
* Improve APK file name formatting on save ([#2421](https://github.com/ReVanced/revanced-manager/issues/2421)) ([a53a8ba](https://github.com/ReVanced/revanced-manager/commit/a53a8ba62734daf9bd80ab79265241a4a22f489c))
|
||||||
|
* improve bundle dialog UI ([409c888](https://github.com/ReVanced/revanced-manager/commit/409c888d523f398505daaaff9d2490dc5a863680))
|
||||||
|
* Improve bundle info screen design ([#2548](https://github.com/ReVanced/revanced-manager/issues/2548)) ([55524f7](https://github.com/ReVanced/revanced-manager/commit/55524f7284a44bbf8e8c782eedd7fc06d54944cf))
|
||||||
|
* Improve custom API URL dialog ([#2033](https://github.com/ReVanced/revanced-manager/issues/2033)) ([7dae562](https://github.com/ReVanced/revanced-manager/commit/7dae56281994942577bac7bf50c59e805672d0e1))
|
||||||
|
* Improve device information in debugging section ([d889677](https://github.com/ReVanced/revanced-manager/commit/d889677b29aeb4a49a025da98060265e88876ddf)), closes [#1977](https://github.com/ReVanced/revanced-manager/issues/1977)
|
||||||
|
* Improve initial update popup wording ([5901372](https://github.com/ReVanced/revanced-manager/commit/5901372523643eef5a605256662c8e1f0a9f2263)), closes [#1956](https://github.com/ReVanced/revanced-manager/issues/1956)
|
||||||
|
* improve keystore UI and UX ([#52](https://github.com/ReVanced/revanced-manager/issues/52)) ([49b4bbb](https://github.com/ReVanced/revanced-manager/commit/49b4bbbf0ba84b006a1694ca95662cf224a84b0f))
|
||||||
|
* Improve patch bundle screen ([#2070](https://github.com/ReVanced/revanced-manager/issues/2070)) ([a907528](https://github.com/ReVanced/revanced-manager/commit/a907528a2096d8de9778efa8f85e0cdc1d7c2b80))
|
||||||
|
* improve patcher screen labels ([f4d6c60](https://github.com/ReVanced/revanced-manager/commit/f4d6c60b9ec4c76e8e3fa233f79e062b802860e5))
|
||||||
|
* improve patcher UI ([#1494](https://github.com/ReVanced/revanced-manager/issues/1494)) ([429b428](https://github.com/ReVanced/revanced-manager/commit/429b428f673dd949289baaf27ed2e08970db83ae))
|
||||||
|
* Improve Settings order ([#2060](https://github.com/ReVanced/revanced-manager/issues/2060)) ([fa86c1a](https://github.com/ReVanced/revanced-manager/commit/fa86c1a0bb039a86e0649eae30c7b33620f98dbe))
|
||||||
|
* improve the safeguards ([#2038](https://github.com/ReVanced/revanced-manager/issues/2038)) ([e5b414e](https://github.com/ReVanced/revanced-manager/commit/e5b414e277341967c7b5a5f071ddac1fdfdb8e63))
|
||||||
|
* Improve unsupported patch warnings ([#2066](https://github.com/ReVanced/revanced-manager/issues/2066)) ([3c23d57](https://github.com/ReVanced/revanced-manager/commit/3c23d573bf3998304cad4485016004a871cf1636)), closes [#2052](https://github.com/ReVanced/revanced-manager/issues/2052)
|
||||||
|
* Improve update screen design ([#2487](https://github.com/ReVanced/revanced-manager/issues/2487)) ([7007010](https://github.com/ReVanced/revanced-manager/commit/7007010f14239452e565736fe7cee7666a682ffb))
|
||||||
|
* Improve update setting tile titles ([e2623d6](https://github.com/ReVanced/revanced-manager/commit/e2623d6d79b3b87e9ba29016e42f1d645b2f9e19)), closes [#1968](https://github.com/ReVanced/revanced-manager/issues/1968)
|
||||||
|
* improve UX for failed or missing bundles ([49f8510](https://github.com/ReVanced/revanced-manager/commit/49f851022db72b110c8597aa1c711461c1b01882))
|
||||||
|
* improved compose stability ([8c40119](https://github.com/ReVanced/revanced-manager/commit/8c40119609c650d1f012d810a4117e84fbe2da52))
|
||||||
|
* improved dashboard screen ([5c2f9d9](https://github.com/ReVanced/revanced-manager/commit/5c2f9d91a6e803d9b3705e2b3aa84176353ba963))
|
||||||
|
* in-app updater ([#25](https://github.com/ReVanced/revanced-manager/issues/25)) ([d71a4bf](https://github.com/ReVanced/revanced-manager/commit/d71a4bf3c3457a02578bb8ad3c7615b074f6e3f1))
|
||||||
|
* **installer:** adjust arrow icon size ([e997255](https://github.com/ReVanced/revanced-manager/commit/e997255cf3c3c5ba777da07752217f99e01dd789))
|
||||||
|
* **installer:** adjust step icon size and alignment ([cfcabf6](https://github.com/ReVanced/revanced-manager/commit/cfcabf6ef1c212f2627d5d02f4d59981bdc276ca))
|
||||||
|
* **installer:** apk signing and installation ([da32ff9](https://github.com/ReVanced/revanced-manager/commit/da32ff954a84cf8ff321bbbf71cc5b544d6e6be9))
|
||||||
|
* **installer:** sign apk in patcher worker ([c003c3c](https://github.com/ReVanced/revanced-manager/commit/c003c3c3245f5a663a0371d4e9df71777ba728b9))
|
||||||
|
* **Installer:** use BottomAppBar ([#1428](https://github.com/ReVanced/revanced-manager/issues/1428)) ([ceb7623](https://github.com/ReVanced/revanced-manager/commit/ceb762379461443e7e62c37511df1c84a6068bb4))
|
||||||
|
* integrate revanced patcher ([#22](https://github.com/ReVanced/revanced-manager/issues/22)) ([caeabfc](https://github.com/ReVanced/revanced-manager/commit/caeabfc91b2aa7e3de9e6a31859049d4b2d37388))
|
||||||
|
* keystore import/export ([#30](https://github.com/ReVanced/revanced-manager/issues/30)) ([fd0ec6c](https://github.com/ReVanced/revanced-manager/commit/fd0ec6c6a7fc8488db859056a95ebe0455e2843b))
|
||||||
|
* **koin:** use the android logger ([f30333e](https://github.com/ReVanced/revanced-manager/commit/f30333e75338dd2c1ef891723ecb834fc1eb10f7))
|
||||||
|
* licenses screen ([#47](https://github.com/ReVanced/revanced-manager/issues/47)) ([e3cb056](https://github.com/ReVanced/revanced-manager/commit/e3cb056858ea8917162c1a421a7a8d03ddaa08e2))
|
||||||
|
* make bundles selectable ([#1237](https://github.com/ReVanced/revanced-manager/issues/1237)) ([a246863](https://github.com/ReVanced/revanced-manager/commit/a246863a89fe8781feaf2a45fcb7ea991d26028f))
|
||||||
|
* Make patch bundles list scrollable ([#2322](https://github.com/ReVanced/revanced-manager/issues/2322)) ([a5c8a23](https://github.com/ReVanced/revanced-manager/commit/a5c8a23f9ffb36543d45b46bb5f01c5dea56bf90))
|
||||||
|
* more info for the select from application screen ([#81](https://github.com/ReVanced/revanced-manager/issues/81)) ([3f446f8](https://github.com/ReVanced/revanced-manager/commit/3f446f8236101755a9d51a2aa759f70a0bd429da))
|
||||||
|
* move plugin api to another repository ([55e7ebf](https://github.com/ReVanced/revanced-manager/commit/55e7ebf4fc5adf8800430ad4aa2579cb6210290d))
|
||||||
|
* Move safeguards above patcher preference group ([9f7eaa2](https://github.com/ReVanced/revanced-manager/commit/9f7eaa212339f2093050087dc7ab0b8237356939))
|
||||||
|
* move update to notification card ([#1917](https://github.com/ReVanced/revanced-manager/issues/1917)) ([b80f94b](https://github.com/ReVanced/revanced-manager/commit/b80f94b77bba89e31608cdb302dab0619bf7c5cc))
|
||||||
|
* **NotificationCard:** rewrite & consistent usage ([#1426](https://github.com/ReVanced/revanced-manager/issues/1426)) ([f8aafa0](https://github.com/ReVanced/revanced-manager/commit/f8aafa050328423b3168a7943f566fce58100cb0))
|
||||||
|
* Open the app-specific manage all files permission dialog ([#2148](https://github.com/ReVanced/revanced-manager/issues/2148)) ([a3f31ea](https://github.com/ReVanced/revanced-manager/commit/a3f31ea65788a43ce57d548e8240e5b1fe3005d0))
|
||||||
|
* Order bundles by number of patches ([bb5d414](https://github.com/ReVanced/revanced-manager/commit/bb5d414abb4f294aa88d795486836a99ade2b388))
|
||||||
|
* patch bundle sources system ([#24](https://github.com/ReVanced/revanced-manager/issues/24)) ([9675a27](https://github.com/ReVanced/revanced-manager/commit/9675a2777b364e5ede0d44b92eb7e551d4f7b3d6))
|
||||||
|
* patch options ([#45](https://github.com/ReVanced/revanced-manager/issues/45)) ([8540d30](https://github.com/ReVanced/revanced-manager/commit/8540d301962669e3d79ca345c852f5b01df641a4))
|
||||||
|
* patch options UI ([#80](https://github.com/ReVanced/revanced-manager/issues/80)) ([0a1acd2](https://github.com/ReVanced/revanced-manager/commit/0a1acd24e3f0d06fde412b8eeecd923d92ee64a9))
|
||||||
|
* **patch-selector:** default patches selection ([#1272](https://github.com/ReVanced/revanced-manager/issues/1272)) ([a17c2de](https://github.com/ReVanced/revanced-manager/commit/a17c2de228cccb4a0bb0ca7497720011bec131fc))
|
||||||
|
* **patch-selector:** remove TODO about an unplanned feature ([4924eae](https://github.com/ReVanced/revanced-manager/commit/4924eaef800c429f2a59b8a15fd48fae0292810c))
|
||||||
|
* **patcher:** Improve installation ([#2185](https://github.com/ReVanced/revanced-manager/issues/2185)) ([3bd4f0d](https://github.com/ReVanced/revanced-manager/commit/3bd4f0d8f3f60d079d4647d42592b10a15f0dae8))
|
||||||
|
* patches selector screen ([55e871a](https://github.com/ReVanced/revanced-manager/commit/55e871aa7d27885e44ef33faab1bb4ae33e7a460))
|
||||||
|
* Progressive AlertDialog for adding bundles ([9a01273](https://github.com/ReVanced/revanced-manager/commit/9a01273c43bd6bcdb0cdfd26c5a467cd3193e5d7)), closes [#1992](https://github.com/ReVanced/revanced-manager/issues/1992)
|
||||||
|
* ProGuard ([d84e6a3](https://github.com/ReVanced/revanced-manager/commit/d84e6a3ffc20d018b2edeb505de20a920785ba5c))
|
||||||
|
* Purple default theme ([#1601](https://github.com/ReVanced/revanced-manager/issues/1601)) ([0616666](https://github.com/ReVanced/revanced-manager/commit/0616666d5ef9b53bef5fd630b1b1a47088097d37))
|
||||||
|
* Redesign the patches screen ([#2381](https://github.com/ReVanced/revanced-manager/issues/2381)) ([8dc4e5b](https://github.com/ReVanced/revanced-manager/commit/8dc4e5b89ee4d36263c8b4187650691b68484688))
|
||||||
|
* remember patch options ([#1449](https://github.com/ReVanced/revanced-manager/issues/1449)) ([90db765](https://github.com/ReVanced/revanced-manager/commit/90db765c9aa014495775a34927904dedf5fef1e3))
|
||||||
|
* remove dead help icons ([3bb071d](https://github.com/ReVanced/revanced-manager/commit/3bb071d80d319d4943b0d4c3048f232f3eb9f5cf))
|
||||||
|
* Remove tag from changelog ([d2119d3](https://github.com/ReVanced/revanced-manager/commit/d2119d36430198151140b469192f76f781df6dd3))
|
||||||
|
* Rename "Patch bundle" to "Patches" ([#2541](https://github.com/ReVanced/revanced-manager/issues/2541)) ([2cdd6d1](https://github.com/ReVanced/revanced-manager/commit/2cdd6d1843f1e49c7c720f8859e11d6a30c0eea6))
|
||||||
|
* rename debug build to `ReVanced Manager (dev)` ([d3417ad](https://github.com/ReVanced/revanced-manager/commit/d3417adbeba0a8e06d3494a2fd108f735f73632c))
|
||||||
|
* rename main bundle to `Default` ([e44d3fd](https://github.com/ReVanced/revanced-manager/commit/e44d3fdee444d915e3e8b8143e55f1353980aad2))
|
||||||
|
* rename package to `app.revanced.manager` ([5ec97f4](https://github.com/ReVanced/revanced-manager/commit/5ec97f4a852a07d0e554bbe1eacc379179ac089e))
|
||||||
|
* Rename strings ([e127845](https://github.com/ReVanced/revanced-manager/commit/e1278452b9c73479cdfb0eb0703db1552b158633))
|
||||||
|
* rename ViewModels for consistency ([064a54e](https://github.com/ReVanced/revanced-manager/commit/064a54eaf0675a1cc9d21f3e1071160deb25c201))
|
||||||
|
* Reorder Import & Export settings ([#2403](https://github.com/ReVanced/revanced-manager/issues/2403)) ([2697077](https://github.com/ReVanced/revanced-manager/commit/2697077fc88bb795027303558c9d52448a4daded))
|
||||||
|
* ReVanced theme colors ([59b894d](https://github.com/ReVanced/revanced-manager/commit/59b894dce4b99c51151a4cccd03a998ceec31778))
|
||||||
|
* revert to blue theme colors ([5f4c958](https://github.com/ReVanced/revanced-manager/commit/5f4c9584a94a1edd1eeaa0b9ecfcd9b281b7cccc))
|
||||||
|
* root installation ([#1243](https://github.com/ReVanced/revanced-manager/issues/1243)) ([62e934c](https://github.com/ReVanced/revanced-manager/commit/62e934c4032096bed36201510fc55304ba48de68))
|
||||||
|
* save patch options and selected patches in bundle ([#50](https://github.com/ReVanced/revanced-manager/issues/50)) ([23162f6](https://github.com/ReVanced/revanced-manager/commit/23162f6233fa6a176514b35feff731f8f28b4d4b))
|
||||||
|
* save patch selection using room db ([#38](https://github.com/ReVanced/revanced-manager/issues/38)) ([1efccda](https://github.com/ReVanced/revanced-manager/commit/1efccda3f55d964fae3bee9ee1f0bd260bb1cc74))
|
||||||
|
* Screen slide transition ([#2396](https://github.com/ReVanced/revanced-manager/issues/2396)) ([2de16e1](https://github.com/ReVanced/revanced-manager/commit/2de16e18e8ba5e84149b377f225693ea35fa2385))
|
||||||
|
* Scrollbars ([#1479](https://github.com/ReVanced/revanced-manager/issues/1479)) ([b5558ea](https://github.com/ReVanced/revanced-manager/commit/b5558ea3ffef40f96b271f8dfe3a5cf95328781e))
|
||||||
|
* Select bundle type before adding bundle ([#1490](https://github.com/ReVanced/revanced-manager/issues/1490)) ([88e860c](https://github.com/ReVanced/revanced-manager/commit/88e860cf0132aed23a3cfd3d9d12e472aa895718))
|
||||||
|
* selected app info page ([#1395](https://github.com/ReVanced/revanced-manager/issues/1395)) ([b69a369](https://github.com/ReVanced/revanced-manager/commit/b69a369d4e304c8a4c8a8db052309b485171e353))
|
||||||
|
* Set app ownership when installing apps ([#2558](https://github.com/ReVanced/revanced-manager/issues/2558)) ([7c410fe](https://github.com/ReVanced/revanced-manager/commit/7c410fef4512087657e3978d5be049c422b25456))
|
||||||
|
* settings migration (compose) ([#1309](https://github.com/ReVanced/revanced-manager/issues/1309)) ([bf1d628](https://github.com/ReVanced/revanced-manager/commit/bf1d628944cb5a439d0bda7c49d820a5fa7576b3))
|
||||||
|
* settings screen ([b7d53cf](https://github.com/ReVanced/revanced-manager/commit/b7d53cfca84d7239bed9189e265a03fd44dc2e45))
|
||||||
|
* **settings screen:** add battery optimization notification ([5754864](https://github.com/ReVanced/revanced-manager/commit/57548641e7ecd06decfc926cb860674ce7443d7a))
|
||||||
|
* **settings screen:** match typography from figma ([948a6d1](https://github.com/ReVanced/revanced-manager/commit/948a6d14404e067907c9e84576cfeba76134aaf6))
|
||||||
|
* **settings:** move experimental patches option to advanced ([805d440](https://github.com/ReVanced/revanced-manager/commit/805d440450d821a26d3ef90a4f97cd796635057d))
|
||||||
|
* **Settings:** use SettingsListItem consistently and overall improvements ([#1427](https://github.com/ReVanced/revanced-manager/issues/1427)) ([5e35893](https://github.com/ReVanced/revanced-manager/commit/5e35893883fa109d74b028478e60b51f97a2e12d))
|
||||||
|
* show installed app in version selector ([1ab1e46](https://github.com/ReVanced/revanced-manager/commit/1ab1e4682ffbfe16c02c438ad833adbfdec58b33))
|
||||||
|
* Show manager update dialog ([#2069](https://github.com/ReVanced/revanced-manager/issues/2069)) ([113a74d](https://github.com/ReVanced/revanced-manager/commit/113a74d270c1c222d4d06049b4edda8f27724a20)), closes [#1963](https://github.com/ReVanced/revanced-manager/issues/1963) [#1958](https://github.com/ReVanced/revanced-manager/issues/1958)
|
||||||
|
* show stacktrace in installer ui ([#36](https://github.com/ReVanced/revanced-manager/issues/36)) ([8d53180](https://github.com/ReVanced/revanced-manager/commit/8d53180d86e6e9d9c8a4056a5fde0603f17e3157))
|
||||||
|
* show toast when no patches are selected ([8aa70d3](https://github.com/ReVanced/revanced-manager/commit/8aa70d350e07aae8b4a22b6bc6fb90c0f6227acd))
|
||||||
|
* splash screen ([60a5a11](https://github.com/ReVanced/revanced-manager/commit/60a5a11c71634aeda414c2ed85f7706ba3deefe1))
|
||||||
|
* store patched apps ([#79](https://github.com/ReVanced/revanced-manager/issues/79)) ([b14285b](https://github.com/ReVanced/revanced-manager/commit/b14285b2c83e60376ad42fa6ea508257cd04d47d))
|
||||||
|
* switch to androidx.navigation ([#2362](https://github.com/ReVanced/revanced-manager/issues/2362)) ([7438f45](https://github.com/ReVanced/revanced-manager/commit/7438f45903ec6ed3436a895d4c32d34d41b00010))
|
||||||
|
* switch to Preferences DataStore ([#60](https://github.com/ReVanced/revanced-manager/issues/60)) ([1852799](https://github.com/ReVanced/revanced-manager/commit/18527999b5f8752faf36c145276d51e2e095c8ee))
|
||||||
|
* switch to revanced api v4 ([7e858a2](https://github.com/ReVanced/revanced-manager/commit/7e858a244cc4038bdb029c4418278700f6a6490f))
|
||||||
|
* switch to the new api ([#75](https://github.com/ReVanced/revanced-manager/issues/75)) ([a55160e](https://github.com/ReVanced/revanced-manager/commit/a55160e7c619ec5541de72fa80f079c9bc94d2d5))
|
||||||
|
* TopAppBar scroll behavior ([#2397](https://github.com/ReVanced/revanced-manager/issues/2397)) ([dc51d61](https://github.com/ReVanced/revanced-manager/commit/dc51d6134dae0fdc415f66e2716c6bffa35dfdb5))
|
||||||
|
* **Update Screen:** changelogs & handle states ([#1464](https://github.com/ReVanced/revanced-manager/issues/1464)) ([3af26e7](https://github.com/ReVanced/revanced-manager/commit/3af26e706571339a3c69688098a51616549c58a8))
|
||||||
|
* **update screen:** complete main update screen ([553af83](https://github.com/ReVanced/revanced-manager/commit/553af831393d7276088ceb0b0a854ec654f72def))
|
||||||
|
* updater changelogs ([#48](https://github.com/ReVanced/revanced-manager/issues/48)) ([6dbcd62](https://github.com/ReVanced/revanced-manager/commit/6dbcd6293e94d8d20cccc401b0edeb1d7047553e))
|
||||||
|
* updater UI and code improvements ([#1597](https://github.com/ReVanced/revanced-manager/issues/1597)) ([a12cae7](https://github.com/ReVanced/revanced-manager/commit/a12cae72998d85138dcf29c0e5d430359e338d5e))
|
||||||
|
* Use "Debug" and "Debug signed" for build names respectively ([5133f02](https://github.com/ReVanced/revanced-manager/commit/5133f02ad61b85af28608c7180b7a2accb4811ab))
|
||||||
|
* Use correct casing in module description ([59b4c0b](https://github.com/ReVanced/revanced-manager/commit/59b4c0b2d2e426dfe66b5a01d219b57bb0df5b8b))
|
||||||
|
* use revanced api for changelogs ([686eb40](https://github.com/ReVanced/revanced-manager/commit/686eb40cb0f8b8d785732dd2bc82d17b5a4fd042))
|
||||||
|
* Use simpler strings ([83d33e8](https://github.com/ReVanced/revanced-manager/commit/83d33e87e3f89cb3efce63dcabcde6478f69b8e7))
|
||||||
|
* View bundle patches ([#2065](https://github.com/ReVanced/revanced-manager/issues/2065)) ([089f200](https://github.com/ReVanced/revanced-manager/commit/089f200fe6ff59020a87883a47ef20a0c4c08565))
|
||||||
|
|
||||||
|
|
||||||
|
### Reverts
|
||||||
|
|
||||||
|
* downgrade Kotlin to 1.8.21 ([fc90bbc](https://github.com/ReVanced/revanced-manager/commit/fc90bbc27ce765e0b55bb5ac9132e58f46aee9aa))
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import com.mikepenz.aboutlibraries.plugin.DuplicateMode
|
||||||
|
import com.mikepenz.aboutlibraries.plugin.DuplicateRule
|
||||||
|
import io.github.z4kn4fein.semver.toVersion
|
||||||
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
@@ -8,6 +12,7 @@ plugins {
|
|||||||
alias(libs.plugins.compose.compiler)
|
alias(libs.plugins.compose.compiler)
|
||||||
alias(libs.plugins.devtools)
|
alias(libs.plugins.devtools)
|
||||||
alias(libs.plugins.about.libraries)
|
alias(libs.plugins.about.libraries)
|
||||||
|
alias(libs.plugins.about.libraries.android)
|
||||||
signing
|
signing
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +85,8 @@ dependencies {
|
|||||||
implementation(libs.koin.workmanager)
|
implementation(libs.koin.workmanager)
|
||||||
|
|
||||||
// Licenses
|
// Licenses
|
||||||
implementation(libs.about.libraries)
|
implementation(libs.about.libraries.core)
|
||||||
|
implementation(libs.about.libraries.m3)
|
||||||
|
|
||||||
// Ktor
|
// Ktor
|
||||||
implementation(libs.ktor.core)
|
implementation(libs.ktor.core)
|
||||||
@@ -107,27 +113,60 @@ dependencies {
|
|||||||
|
|
||||||
// Compose Icons
|
// Compose Icons
|
||||||
implementation(libs.compose.icons.fontawesome)
|
implementation(libs.compose.icons.fontawesome)
|
||||||
|
|
||||||
|
// Ackpine
|
||||||
|
implementation(libs.ackpine.core)
|
||||||
|
implementation(libs.ackpine.ktx)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
// Semantic versioning string parser
|
||||||
|
classpath(libs.semver.parser)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "app.revanced.manager"
|
namespace = "app.revanced.manager"
|
||||||
compileSdk = 35
|
compileSdk = 36
|
||||||
buildToolsVersion = "35.0.1"
|
buildToolsVersion = "35.0.1"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "app.revanced.manager"
|
applicationId = "app.revanced.manager"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 1
|
|
||||||
versionName = "0.0.1"
|
val versionStr = if (version == "unspecified") "1.0.0" else version.toString()
|
||||||
|
versionName = versionStr
|
||||||
|
versionCode = with(versionStr.toVersion()) {
|
||||||
|
major * 10_000_000 +
|
||||||
|
minor * 10_000 +
|
||||||
|
patch * 100 +
|
||||||
|
(preRelease?.substringAfterLast('.')?.toInt() ?: 99)
|
||||||
|
}
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|
||||||
|
val resDir = file("src/main/res")
|
||||||
|
val locales = resDir.listFiles()
|
||||||
|
.orEmpty()
|
||||||
|
//noinspection WrongGradleMethod
|
||||||
|
.filter { it.isDirectory && it.name.matches(Regex("values-[a-z]{2}(-r[A-Z]{2})?")) }
|
||||||
|
//noinspection WrongGradleMethod
|
||||||
|
.map { it.name.removePrefix("values-").replace("-r", "-") }
|
||||||
|
.sorted()
|
||||||
|
//noinspection WrongGradleMethod
|
||||||
|
.joinToString(prefix = "{", separator = ",", postfix = "}") { "\"$it\"" }
|
||||||
|
|
||||||
|
buildConfigField("String[]", "SUPPORTED_LOCALES", locales)
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
applicationIdSuffix = ".debug"
|
applicationIdSuffix = ".debug"
|
||||||
resValue("string", "app_name", "ReVanced Manager (Debug)")
|
resValue("string", "app_name", "ReVanced Manager (Debug)")
|
||||||
isPseudoLocalesEnabled = true
|
|
||||||
|
|
||||||
buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L")
|
buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L")
|
||||||
}
|
}
|
||||||
@@ -199,20 +238,14 @@ android {
|
|||||||
arg("room.schemaLocation", "$projectDir/schemas")
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "17"
|
|
||||||
}
|
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
aidl = true
|
aidl = true
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
androidResources {
|
||||||
androidResources {
|
generateLocaleConfig = true
|
||||||
generateLocaleConfig = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
@@ -225,6 +258,18 @@ android {
|
|||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvmToolchain(17)
|
jvmToolchain(17)
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget = JvmTarget.JVM_17
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aboutLibraries {
|
||||||
|
library {
|
||||||
|
// Enable the duplication mode, allows to merge, or link dependencies which relate
|
||||||
|
duplicationMode = DuplicateMode.MERGE
|
||||||
|
// Configure the duplication rule, to match "duplicates" with
|
||||||
|
duplicationRule = DuplicateRule.EXACT
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
|
|||||||
1
app/gradle.properties
Normal file
1
app/gradle.properties
Normal file
@@ -0,0 +1 @@
|
|||||||
|
version = 1.26.0-dev.20
|
||||||
1
app/gradlew
vendored
Symbolic link
1
app/gradlew
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../gradlew
|
||||||
11
app/package.json
Normal file
11
app/package.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "app",
|
||||||
|
"private": false,
|
||||||
|
"devDependencies": {
|
||||||
|
"@anolilab/multi-semantic-release": "^1.1.10",
|
||||||
|
"@saithodev/semantic-release-backmerge": "^4.0.1",
|
||||||
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
|
"@semantic-release/git": "^10.0.1",
|
||||||
|
"gradle-semantic-release-plugin": "^1.10.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
67
app/proguard-rules.pro
vendored
67
app/proguard-rules.pro
vendored
@@ -1,63 +1,14 @@
|
|||||||
# Add project specific ProGuard rules here.
|
|
||||||
# You can control the set of applied configuration files using the
|
|
||||||
# proguardFiles setting in build.gradle.kts.kts.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
-dontobfuscate
|
-dontobfuscate
|
||||||
|
|
||||||
# Required for serialization to work properly
|
-keep class app.revanced.manager.patcher.runtime.process.* { *; }
|
||||||
-if @kotlinx.serialization.Serializable class **
|
-keep class app.revanced.manager.plugin.** { *; }
|
||||||
-keepclassmembers class <1> {
|
-keep class app.revanced.patcher.** { *; }
|
||||||
static <1>$Companion Companion;
|
-keep class com.android.tools.smali.** { *; }
|
||||||
}
|
-keep class kotlin.** { *; }
|
||||||
-if @kotlinx.serialization.Serializable class ** {
|
-keepnames class com.android.apksig.internal.** { *; }
|
||||||
static **$* *;
|
-keepnames class org.xmlpull.** { *; }
|
||||||
}
|
|
||||||
-keepclassmembers class <2>$<3> {
|
|
||||||
kotlinx.serialization.KSerializer serializer(...);
|
|
||||||
}
|
|
||||||
-if @kotlinx.serialization.Serializable class ** {
|
|
||||||
public static ** INSTANCE;
|
|
||||||
}
|
|
||||||
-keepclassmembers class <1> {
|
|
||||||
public static <1> INSTANCE;
|
|
||||||
kotlinx.serialization.KSerializer serializer(...);
|
|
||||||
}
|
|
||||||
|
|
||||||
# This required for the process runtime.
|
-dontwarn com.google.j2objc.annotations.*
|
||||||
-keep class app.revanced.manager.patcher.runtime.process.* {
|
|
||||||
*;
|
|
||||||
}
|
|
||||||
# Required for the patcher to function correctly
|
|
||||||
-keep class app.revanced.patcher.** {
|
|
||||||
*;
|
|
||||||
}
|
|
||||||
-keep class brut.** {
|
|
||||||
*;
|
|
||||||
}
|
|
||||||
-keep class org.xmlpull.** {
|
|
||||||
*;
|
|
||||||
}
|
|
||||||
-keep class kotlin.** {
|
|
||||||
*;
|
|
||||||
}
|
|
||||||
-keep class org.jf.** {
|
|
||||||
*;
|
|
||||||
}
|
|
||||||
-keep class com.android.** {
|
|
||||||
*;
|
|
||||||
}
|
|
||||||
-keep class app.revanced.manager.plugin.** {
|
|
||||||
*;
|
|
||||||
}
|
|
||||||
|
|
||||||
-dontwarn com.google.auto.value.**
|
|
||||||
-dontwarn java.awt.**
|
-dontwarn java.awt.**
|
||||||
-dontwarn javax.**
|
-dontwarn javax.**
|
||||||
-dontwarn org.slf4j.**
|
-dontwarn org.slf4j.**
|
||||||
-dontwarn it.skrape.fetcher.*
|
|
||||||
-dontwarn com.google.j2objc.annotations.*
|
|
||||||
|
|
||||||
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
|
|
||||||
@@ -51,9 +51,6 @@
|
|||||||
|
|
||||||
<activity android:name=".plugin.downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" />
|
<activity android:name=".plugin.downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" />
|
||||||
|
|
||||||
<service android:name=".service.InstallService" />
|
|
||||||
<service android:name=".service.UninstallService" />
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||||
android:foregroundServiceType="specialUse"
|
android:foregroundServiceType="specialUse"
|
||||||
@@ -75,5 +72,15 @@
|
|||||||
android:value="androidx.startup"
|
android:value="androidx.startup"
|
||||||
tools:node="remove" />
|
tools:node="remove" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.startup.InitializationProvider"
|
||||||
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
|
android:exported="false"
|
||||||
|
tools:node="merge">
|
||||||
|
<meta-data
|
||||||
|
android:name="ru.solrudev.ackpine.AckpineInitializer"
|
||||||
|
tools:node="remove" />
|
||||||
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// ProgressEventParcel.aidl
|
||||||
|
package app.revanced.manager.patcher;
|
||||||
|
|
||||||
|
parcelable ProgressEventParcel;
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
// IPatcherEvents.aidl
|
// IPatcherEvents.aidl
|
||||||
package app.revanced.manager.patcher.runtime.process;
|
package app.revanced.manager.patcher.runtime.process;
|
||||||
|
|
||||||
|
import app.revanced.manager.patcher.ProgressEventParcel;
|
||||||
|
|
||||||
// Interface for sending events back to the main app process.
|
// Interface for sending events back to the main app process.
|
||||||
oneway interface IPatcherEvents {
|
oneway interface IPatcherEvents {
|
||||||
void log(String level, String msg);
|
void log(String level, String msg);
|
||||||
void patchSucceeded();
|
void event(in ProgressEventParcel event);
|
||||||
void progress(String name, String state, String msg);
|
|
||||||
// The patching process has ended. The exceptionStackTrace is null if it finished successfully.
|
// The patching process has ended. The exceptionStackTrace is null if it finished successfully.
|
||||||
void finished(String exceptionStackTrace);
|
void finished(String exceptionStackTrace);
|
||||||
}
|
}
|
||||||
@@ -3,11 +3,11 @@ package app.revanced.manager
|
|||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.animation.slideInHorizontally
|
import androidx.compose.animation.slideInHorizontally
|
||||||
import androidx.compose.animation.slideOutHorizontally
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
@@ -59,11 +59,10 @@ import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
|
|||||||
import app.revanced.manager.util.EventEffect
|
import app.revanced.manager.util.EventEffect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import org.koin.androidx.compose.navigation.koinNavViewModel
|
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
|
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -81,6 +80,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
)
|
)
|
||||||
val theme by vm.prefs.theme.getAsState()
|
val theme by vm.prefs.theme.getAsState()
|
||||||
val dynamicColor by vm.prefs.dynamicColor.getAsState()
|
val dynamicColor by vm.prefs.dynamicColor.getAsState()
|
||||||
|
val pureBlackTheme by vm.prefs.pureBlackTheme.getAsState()
|
||||||
|
|
||||||
EventEffect(vm.legacyImportActivityFlow) {
|
EventEffect(vm.legacyImportActivityFlow) {
|
||||||
try {
|
try {
|
||||||
@@ -91,7 +91,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
ReVancedManagerTheme(
|
ReVancedManagerTheme(
|
||||||
darkTheme = theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK,
|
darkTheme = theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK,
|
||||||
dynamicColor = dynamicColor
|
dynamicColor = dynamicColor,
|
||||||
|
pureBlackTheme = pureBlackTheme
|
||||||
) {
|
) {
|
||||||
ReVancedManager(vm)
|
ReVancedManager(vm)
|
||||||
}
|
}
|
||||||
@@ -183,7 +184,7 @@ private fun ReVancedManager(vm: MainViewModel) {
|
|||||||
val data =
|
val data =
|
||||||
parentBackStackEntry.getComplexArg<SelectedApplicationInfo.ViewModelParams>()
|
parentBackStackEntry.getComplexArg<SelectedApplicationInfo.ViewModelParams>()
|
||||||
val viewModel =
|
val viewModel =
|
||||||
koinNavViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) {
|
koinViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) {
|
||||||
parametersOf(data)
|
parametersOf(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,7 +225,7 @@ private fun ReVancedManager(vm: MainViewModel) {
|
|||||||
composable<SelectedApplicationInfo.PatchesSelector> {
|
composable<SelectedApplicationInfo.PatchesSelector> {
|
||||||
val data =
|
val data =
|
||||||
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
||||||
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
val selectedAppInfoVm = koinViewModel<SelectedAppInfoViewModel>(
|
||||||
viewModelStoreOwner = navController.navGraphEntry(it)
|
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -241,7 +242,7 @@ private fun ReVancedManager(vm: MainViewModel) {
|
|||||||
composable<SelectedApplicationInfo.RequiredOptions> {
|
composable<SelectedApplicationInfo.RequiredOptions> {
|
||||||
val data =
|
val data =
|
||||||
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
||||||
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
val selectedAppInfoVm = koinViewModel<SelectedAppInfoViewModel>(
|
||||||
viewModelStoreOwner = navController.navGraphEntry(it)
|
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ class ManagerApplication : Application() {
|
|||||||
workerModule,
|
workerModule,
|
||||||
viewModelModule,
|
viewModelModule,
|
||||||
databaseModule,
|
databaseModule,
|
||||||
rootModule
|
rootModule,
|
||||||
|
ackpineModule
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,5 +15,5 @@ class NetworkInfo(app: Application) {
|
|||||||
/**
|
/**
|
||||||
* Returns true if it is safe to download large files.
|
* Returns true if it is safe to download large files.
|
||||||
*/
|
*/
|
||||||
fun isSafe() = isConnected() && isUnmetered()
|
fun isSafe(ignoreMetered: Boolean) = isConnected() && (ignoreMetered || isUnmetered())
|
||||||
}
|
}
|
||||||
19
app/src/main/java/app/revanced/manager/di/AckpineModule.kt
Normal file
19
app/src/main/java/app/revanced/manager/di/AckpineModule.kt
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package app.revanced.manager.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.dsl.module
|
||||||
|
import ru.solrudev.ackpine.installer.PackageInstaller
|
||||||
|
import ru.solrudev.ackpine.uninstaller.PackageUninstaller
|
||||||
|
|
||||||
|
val ackpineModule = module {
|
||||||
|
fun provideInstaller(context: Context) = PackageInstaller.getInstance(context)
|
||||||
|
fun provideUninstaller(context: Context) = PackageUninstaller.getInstance(context)
|
||||||
|
|
||||||
|
single {
|
||||||
|
provideInstaller(androidContext())
|
||||||
|
}
|
||||||
|
single {
|
||||||
|
provideUninstaller(androidContext())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package app.revanced.manager.di
|
package app.revanced.manager.di
|
||||||
|
|
||||||
import app.revanced.manager.ui.viewmodel.*
|
import app.revanced.manager.ui.viewmodel.*
|
||||||
import org.koin.androidx.viewmodel.dsl.viewModelOf
|
import org.koin.core.module.dsl.*
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val viewModelModule = module {
|
val viewModelModule = module {
|
||||||
|
|||||||
@@ -27,25 +27,25 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
|
|||||||
private val keystorePath =
|
private val keystorePath =
|
||||||
app.getDir("signing", Context.MODE_PRIVATE).resolve("manager.keystore")
|
app.getDir("signing", Context.MODE_PRIVATE).resolve("manager.keystore")
|
||||||
|
|
||||||
private suspend fun updatePrefs(cn: String, pass: String) = prefs.edit {
|
private suspend fun updatePrefs(alias: String, pass: String) = prefs.edit {
|
||||||
prefs.keystoreCommonName.value = cn
|
prefs.keystoreAlias.value = alias
|
||||||
prefs.keystorePass.value = pass
|
prefs.keystorePass.value = pass
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun signingDetails(path: File = keystorePath) = ApkUtils.KeyStoreDetails(
|
private suspend fun signingDetails(path: File = keystorePath) = ApkUtils.KeyStoreDetails(
|
||||||
keyStore = path,
|
keyStore = path,
|
||||||
keyStorePassword = null,
|
keyStorePassword = null,
|
||||||
alias = prefs.keystoreCommonName.get(),
|
alias = prefs.keystoreAlias.get(),
|
||||||
password = prefs.keystorePass.get()
|
password = prefs.keystorePass.get()
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun sign(input: File, output: File) = withContext(Dispatchers.Default) {
|
suspend fun sign(input: File, output: File) = withContext(Dispatchers.Default) {
|
||||||
ApkUtils.signApk(input, output, prefs.keystoreCommonName.get(), signingDetails())
|
ApkUtils.signApk(input, output, prefs.keystoreAlias.get(), signingDetails())
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun regenerate() = withContext(Dispatchers.Default) {
|
suspend fun regenerate() = withContext(Dispatchers.Default) {
|
||||||
val keyCertPair = ApkSigner.newPrivateKeyCertificatePair(
|
val keyCertPair = ApkSigner.newPrivateKeyCertificatePair(
|
||||||
prefs.keystoreCommonName.get(),
|
prefs.keystoreAlias.get(),
|
||||||
eightYearsFromNow
|
eightYearsFromNow
|
||||||
)
|
)
|
||||||
val ks = ApkSigner.newKeyStore(
|
val ks = ApkSigner.newKeyStore(
|
||||||
@@ -64,13 +64,13 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
|
|||||||
updatePrefs(DEFAULT, DEFAULT)
|
updatePrefs(DEFAULT, DEFAULT)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun import(cn: String, pass: String, keystore: InputStream): Boolean {
|
suspend fun import(alias: String, pass: String, keystore: InputStream): Boolean {
|
||||||
val keystoreData = withContext(Dispatchers.IO) { keystore.readBytes() }
|
val keystoreData = withContext(Dispatchers.IO) { keystore.readBytes() }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val ks = ApkSigner.readKeyStore(ByteArrayInputStream(keystoreData), null)
|
val ks = ApkSigner.readKeyStore(ByteArrayInputStream(keystoreData), null)
|
||||||
|
|
||||||
ApkSigner.readPrivateKeyCertificatePair(ks, cn, pass)
|
ApkSigner.readPrivateKeyCertificatePair(ks, alias, pass)
|
||||||
} catch (_: UnrecoverableKeyException) {
|
} catch (_: UnrecoverableKeyException) {
|
||||||
return false
|
return false
|
||||||
} catch (_: IllegalArgumentException) {
|
} catch (_: IllegalArgumentException) {
|
||||||
@@ -81,7 +81,7 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
|
|||||||
Files.write(keystorePath.toPath(), keystoreData)
|
Files.write(keystorePath.toPath(), keystoreData)
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePrefs(cn, pass)
|
updatePrefs(alias, pass)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class PreferencesManager(
|
|||||||
context: Context
|
context: Context
|
||||||
) : BasePreferencesManager(context, "settings") {
|
) : BasePreferencesManager(context, "settings") {
|
||||||
val dynamicColor = booleanPreference("dynamic_color", true)
|
val dynamicColor = booleanPreference("dynamic_color", true)
|
||||||
|
val pureBlackTheme = booleanPreference("pure_black_theme", false)
|
||||||
val theme = enumPreference("theme", Theme.SYSTEM)
|
val theme = enumPreference("theme", Theme.SYSTEM)
|
||||||
|
|
||||||
val api = stringPreference("api_url", "https://api.revanced.app")
|
val api = stringPreference("api_url", "https://api.revanced.app")
|
||||||
@@ -16,12 +17,14 @@ class PreferencesManager(
|
|||||||
val useProcessRuntime = booleanPreference("use_process_runtime", false)
|
val useProcessRuntime = booleanPreference("use_process_runtime", false)
|
||||||
val patcherProcessMemoryLimit = intPreference("process_runtime_memory_limit", 700)
|
val patcherProcessMemoryLimit = intPreference("process_runtime_memory_limit", 700)
|
||||||
|
|
||||||
val keystoreCommonName = stringPreference("keystore_cn", KeystoreManager.DEFAULT)
|
val keystoreAlias = stringPreference("keystore_alias", KeystoreManager.DEFAULT)
|
||||||
val keystorePass = stringPreference("keystore_pass", KeystoreManager.DEFAULT)
|
val keystorePass = stringPreference("keystore_pass", KeystoreManager.DEFAULT)
|
||||||
|
|
||||||
val firstLaunch = booleanPreference("first_launch", true)
|
val firstLaunch = booleanPreference("first_launch", true)
|
||||||
val managerAutoUpdates = booleanPreference("manager_auto_updates", false)
|
val managerAutoUpdates = booleanPreference("manager_auto_updates", false)
|
||||||
val showManagerUpdateDialogOnLaunch = booleanPreference("show_manager_update_dialog_on_launch", true)
|
val showManagerUpdateDialogOnLaunch = booleanPreference("show_manager_update_dialog_on_launch", true)
|
||||||
|
val useManagerPrereleases = booleanPreference("manager_prereleases", false)
|
||||||
|
val usePatchesPrereleases = booleanPreference("patches_prereleases", false)
|
||||||
|
|
||||||
val disablePatchVersionCompatCheck = booleanPreference("disable_patch_version_compatibility_check", false)
|
val disablePatchVersionCompatCheck = booleanPreference("disable_patch_version_compatibility_check", false)
|
||||||
val disableSelectionWarning = booleanPreference("disable_selection_warning", false)
|
val disableSelectionWarning = booleanPreference("disable_selection_warning", false)
|
||||||
@@ -31,4 +34,6 @@ class PreferencesManager(
|
|||||||
val acknowledgedDownloaderPlugins = stringSetPreference("acknowledged_downloader_plugins", emptySet())
|
val acknowledgedDownloaderPlugins = stringSetPreference("acknowledged_downloader_plugins", emptySet())
|
||||||
|
|
||||||
val showDeveloperSettings = booleanPreference("show_developer_settings", context.isDebuggable)
|
val showDeveloperSettings = booleanPreference("show_developer_settings", context.isDebuggable)
|
||||||
|
|
||||||
|
val allowMeteredNetworks = booleanPreference("allow_metered_networks", false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -286,28 +286,29 @@ class PatchBundleRepository(
|
|||||||
State(sources.toPersistentMap(), info.toPersistentMap())
|
State(sources.toPersistentMap(), info.toPersistentMap())
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun createLocal(createStream: suspend () -> InputStream) = dispatchAction("Add bundle") {
|
suspend fun createLocal(createStream: suspend () -> InputStream) =
|
||||||
with(createEntity("", SourceInfo.Local).load() as LocalPatchBundle) {
|
dispatchAction("Add bundle") {
|
||||||
try {
|
with(createEntity("", SourceInfo.Local).load() as LocalPatchBundle) {
|
||||||
createStream().use { patches -> replace(patches) }
|
try {
|
||||||
} catch (e: Exception) {
|
createStream().use { patches -> replace(patches) }
|
||||||
if (e is CancellationException) throw e
|
} catch (e: Exception) {
|
||||||
Log.e(tag, "Got exception while importing bundle", e)
|
if (e is CancellationException) throw e
|
||||||
withContext(Dispatchers.Main) {
|
Log.e(tag, "Got exception while importing bundle", e)
|
||||||
app.toast(app.getString(R.string.patches_replace_fail, e.simpleMessage()))
|
withContext(Dispatchers.Main) {
|
||||||
|
app.toast(app.getString(R.string.patches_replace_fail, e.simpleMessage()))
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteLocalFile()
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteLocalFile()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
doReload()
|
doReload()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun createRemote(url: String, autoUpdate: Boolean) =
|
suspend fun createRemote(url: String, autoUpdate: Boolean) =
|
||||||
dispatchAction("Add bundle ($url)") { state ->
|
dispatchAction("Add bundle ($url)") { state ->
|
||||||
val src = createEntity("", SourceInfo.from(url), autoUpdate).load() as RemotePatchBundle
|
val src = createEntity("", SourceInfo.from(url), autoUpdate).load() as RemotePatchBundle
|
||||||
update(src)
|
update(src, force = true)
|
||||||
state.copy(sources = state.sources.put(src.uid, src))
|
state.copy(sources = state.sources.put(src.uid, src))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,32 +330,38 @@ class PatchBundleRepository(
|
|||||||
state.copy(sources = state.sources.put(uid, newSrc))
|
state.copy(sources = state.sources.put(uid, newSrc))
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun update(vararg sources: RemotePatchBundle, showToast: Boolean = false) {
|
suspend fun update(
|
||||||
|
vararg sources: RemotePatchBundle,
|
||||||
|
showToast: Boolean = false,
|
||||||
|
force: Boolean = false
|
||||||
|
) {
|
||||||
val uids = sources.map { it.uid }.toSet()
|
val uids = sources.map { it.uid }.toSet()
|
||||||
store.dispatch(Update(showToast = showToast) { it.uid in uids })
|
store.dispatch(Update(showToast = showToast, force = force) { it.uid in uids })
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun redownloadRemoteBundles() = store.dispatch(Update(force = true))
|
suspend fun redownloadRemoteBundles() = store.dispatch(Update(force = true, redownload = true))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates all bundles that should be automatically updated.
|
* Updates all bundles that should be automatically updated.
|
||||||
*/
|
*/
|
||||||
suspend fun updateCheck() = store.dispatch(Update { it.autoUpdate })
|
suspend fun updateCheck() =
|
||||||
|
store.dispatch(Update(force = prefs.allowMeteredNetworks.get()) { it.autoUpdate })
|
||||||
|
|
||||||
private inner class Update(
|
private inner class Update(
|
||||||
private val force: Boolean = false,
|
private val force: Boolean = false,
|
||||||
|
private val redownload: Boolean = false,
|
||||||
private val showToast: Boolean = false,
|
private val showToast: Boolean = false,
|
||||||
private val predicate: (bundle: RemotePatchBundle) -> Boolean = { true },
|
private val predicate: (bundle: RemotePatchBundle) -> Boolean = { true },
|
||||||
) : Action<State> {
|
) : Action<State> {
|
||||||
private suspend fun toast(@StringRes id: Int, vararg args: Any?) =
|
private suspend fun toast(@StringRes id: Int, vararg args: Any?) =
|
||||||
withContext(Dispatchers.Main) { app.toast(app.getString(id, *args)) }
|
withContext(Dispatchers.Main) { app.toast(app.getString(id, *args)) }
|
||||||
|
|
||||||
override fun toString() = if (force) "Redownload remote bundles" else "Update check"
|
override fun toString() = if (redownload) "Redownload remote bundles" else "Update check"
|
||||||
|
|
||||||
override suspend fun ActionContext.execute(
|
override suspend fun ActionContext.execute(
|
||||||
current: State
|
current: State
|
||||||
) = coroutineScope {
|
) = coroutineScope {
|
||||||
if (!networkInfo.isSafe()) {
|
if (!networkInfo.isSafe(force)) {
|
||||||
Log.d(tag, "Skipping update check because the network is down or metered.")
|
Log.d(tag, "Skipping update check because the network is down or metered.")
|
||||||
return@coroutineScope current
|
return@coroutineScope current
|
||||||
}
|
}
|
||||||
@@ -367,7 +374,7 @@ class PatchBundleRepository(
|
|||||||
Log.d(tag, "Updating patch bundle: ${it.name}")
|
Log.d(tag, "Updating patch bundle: ${it.name}")
|
||||||
|
|
||||||
val newVersion = with(it) {
|
val newVersion = with(it) {
|
||||||
if (force) downloadLatest() else update()
|
if (redownload) downloadLatest() else update()
|
||||||
} ?: return@async null
|
} ?: return@async null
|
||||||
|
|
||||||
it to newVersion
|
it to newVersion
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package app.revanced.manager.network.api
|
package app.revanced.manager.network.api
|
||||||
|
|
||||||
import android.os.Build
|
import app.revanced.manager.BuildConfig
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
import app.revanced.manager.network.dto.ReVancedAsset
|
import app.revanced.manager.network.dto.ReVancedAsset
|
||||||
import app.revanced.manager.network.dto.ReVancedGitRepository
|
import app.revanced.manager.network.dto.ReVancedGitRepository
|
||||||
@@ -30,11 +30,12 @@ class ReVancedAPI(
|
|||||||
private suspend inline fun <reified T> request(route: String) = request<T>(apiUrl(), route)
|
private suspend inline fun <reified T> request(route: String) = request<T>(apiUrl(), route)
|
||||||
|
|
||||||
suspend fun getAppUpdate() =
|
suspend fun getAppUpdate() =
|
||||||
getLatestAppInfo().getOrThrow().takeIf { it.version != Build.VERSION.RELEASE }
|
getLatestAppInfo().getOrThrow().takeIf { it.version.removePrefix("v") != BuildConfig.VERSION_NAME }
|
||||||
|
|
||||||
suspend fun getLatestAppInfo() = request<ReVancedAsset>("manager")
|
suspend fun getLatestAppInfo() =
|
||||||
|
request<ReVancedAsset>("manager?prerelease=${prefs.useManagerPrereleases.get()}")
|
||||||
|
|
||||||
suspend fun getPatchesUpdate() = request<ReVancedAsset>("patches")
|
suspend fun getPatchesUpdate() = request<ReVancedAsset>("patches?prerelease=${prefs.usePatchesPrereleases.get()}")
|
||||||
|
|
||||||
suspend fun getContributors() = request<List<ReVancedGitRepository>>("contributors")
|
suspend fun getContributors() = request<List<ReVancedGitRepository>>("contributors")
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,11 @@ import io.ktor.http.isSuccess
|
|||||||
import io.ktor.utils.io.ByteReadChannel
|
import io.ktor.utils.io.ByteReadChannel
|
||||||
import io.ktor.utils.io.core.isNotEmpty
|
import io.ktor.utils.io.core.isNotEmpty
|
||||||
import io.ktor.utils.io.core.readBytes
|
import io.ktor.utils.io.core.readBytes
|
||||||
|
import io.ktor.utils.io.exhausted
|
||||||
|
import io.ktor.utils.io.readRemaining
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.io.asSink
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
@@ -69,14 +72,12 @@ class HttpService(
|
|||||||
) {
|
) {
|
||||||
http.prepareGet(builder).execute { httpResponse ->
|
http.prepareGet(builder).execute { httpResponse ->
|
||||||
if (httpResponse.status.isSuccess()) {
|
if (httpResponse.status.isSuccess()) {
|
||||||
val channel: ByteReadChannel = httpResponse.body()
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
while (!channel.isClosedForRead) {
|
val channel: ByteReadChannel = httpResponse.body()
|
||||||
|
val sink = outputStream.asSink()
|
||||||
|
while (!channel.exhausted()) {
|
||||||
val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
|
val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
|
||||||
while (packet.isNotEmpty) {
|
packet.transferTo(sink)
|
||||||
val bytes = packet.readBytes()
|
|
||||||
outputStream.write(bytes)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package app.revanced.manager.patcher
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
sealed class ProgressEvent : Parcelable {
|
||||||
|
abstract val stepId: StepId?
|
||||||
|
|
||||||
|
data class Started(override val stepId: StepId) : ProgressEvent()
|
||||||
|
|
||||||
|
data class Progress(
|
||||||
|
override val stepId: StepId,
|
||||||
|
val current: Long? = null,
|
||||||
|
val total: Long? = null,
|
||||||
|
val message: String? = null,
|
||||||
|
) : ProgressEvent()
|
||||||
|
|
||||||
|
data class Completed(
|
||||||
|
override val stepId: StepId,
|
||||||
|
) : ProgressEvent()
|
||||||
|
|
||||||
|
data class Failed(
|
||||||
|
override val stepId: StepId?,
|
||||||
|
val error: RemoteError,
|
||||||
|
) : ProgressEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parcelable wrapper for [ProgressEvent].
|
||||||
|
*
|
||||||
|
* Required because AIDL does not support sealed classes.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class ProgressEventParcel(val event: ProgressEvent) : Parcelable
|
||||||
|
|
||||||
|
fun ProgressEventParcel.toEvent(): ProgressEvent = event
|
||||||
|
fun ProgressEvent.toParcel(): ProgressEventParcel = ProgressEventParcel(this)
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
sealed class StepId : Parcelable {
|
||||||
|
data object DownloadAPK : StepId()
|
||||||
|
data object LoadPatches : StepId()
|
||||||
|
data object ReadAPK : StepId()
|
||||||
|
data object ExecutePatches : StepId()
|
||||||
|
data class ExecutePatch(val index: Int) : StepId()
|
||||||
|
data object WriteAPK : StepId()
|
||||||
|
data object SignAPK : StepId()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class RemoteError(
|
||||||
|
val type: String,
|
||||||
|
val message: String?,
|
||||||
|
val stackTrace: String,
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
fun Exception.toRemoteError() = RemoteError(
|
||||||
|
type = this::class.java.name,
|
||||||
|
message = this.message,
|
||||||
|
stackTrace = this.stackTraceToString(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
inline fun <T> runStep(
|
||||||
|
stepId: StepId,
|
||||||
|
onEvent: (ProgressEvent) -> Unit,
|
||||||
|
block: () -> T,
|
||||||
|
): T = try {
|
||||||
|
onEvent(ProgressEvent.Started(stepId))
|
||||||
|
val value = block()
|
||||||
|
onEvent(ProgressEvent.Completed(stepId))
|
||||||
|
value
|
||||||
|
} catch (error: Exception) {
|
||||||
|
onEvent(ProgressEvent.Failed(stepId, error.toRemoteError()))
|
||||||
|
throw error
|
||||||
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
package app.revanced.manager.patcher
|
package app.revanced.manager.patcher
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import app.revanced.library.ApkUtils.applyTo
|
import app.revanced.library.ApkUtils.applyTo
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.patcher.Session.Companion.component1
|
||||||
|
import app.revanced.manager.patcher.Session.Companion.component2
|
||||||
import app.revanced.manager.patcher.logger.Logger
|
import app.revanced.manager.patcher.logger.Logger
|
||||||
import app.revanced.manager.ui.model.State
|
|
||||||
import app.revanced.patcher.Patcher
|
import app.revanced.patcher.Patcher
|
||||||
import app.revanced.patcher.PatcherConfig
|
import app.revanced.patcher.PatcherConfig
|
||||||
import app.revanced.patcher.patch.Patch
|
import app.revanced.patcher.patch.Patch
|
||||||
@@ -22,15 +21,10 @@ class Session(
|
|||||||
cacheDir: String,
|
cacheDir: String,
|
||||||
frameworkDir: String,
|
frameworkDir: String,
|
||||||
aaptPath: String,
|
aaptPath: String,
|
||||||
private val androidContext: Context,
|
|
||||||
private val logger: Logger,
|
private val logger: Logger,
|
||||||
private val input: File,
|
private val input: File,
|
||||||
private val onPatchCompleted: suspend () -> Unit,
|
private val onEvent: (ProgressEvent) -> Unit,
|
||||||
private val onProgress: (name: String?, state: State?, message: String?) -> Unit
|
|
||||||
) : Closeable {
|
) : Closeable {
|
||||||
private fun updateProgress(name: String? = null, state: State? = null, message: String? = null) =
|
|
||||||
onProgress(name, state, message)
|
|
||||||
|
|
||||||
private val tempDir = File(cacheDir).resolve("patcher").also { it.mkdirs() }
|
private val tempDir = File(cacheDir).resolve("patcher").also { it.mkdirs() }
|
||||||
private val patcher = Patcher(
|
private val patcher = Patcher(
|
||||||
PatcherConfig(
|
PatcherConfig(
|
||||||
@@ -42,86 +36,68 @@ class Session(
|
|||||||
)
|
)
|
||||||
|
|
||||||
private suspend fun Patcher.applyPatchesVerbose(selectedPatches: PatchList) {
|
private suspend fun Patcher.applyPatchesVerbose(selectedPatches: PatchList) {
|
||||||
var nextPatchIndex = 0
|
|
||||||
|
|
||||||
updateProgress(
|
|
||||||
name = androidContext.getString(R.string.executing_patch, selectedPatches[nextPatchIndex]),
|
|
||||||
state = State.RUNNING
|
|
||||||
)
|
|
||||||
|
|
||||||
this().collect { (patch, exception) ->
|
this().collect { (patch, exception) ->
|
||||||
if (patch !in selectedPatches) return@collect
|
val index = selectedPatches.indexOf(patch)
|
||||||
|
if (index == -1) return@collect
|
||||||
|
|
||||||
if (exception != null) {
|
if (exception != null) {
|
||||||
updateProgress(
|
onEvent(
|
||||||
name = androidContext.getString(R.string.failed_to_execute_patch, patch.name),
|
ProgressEvent.Failed(
|
||||||
state = State.FAILED,
|
StepId.ExecutePatch(index),
|
||||||
message = exception.stackTraceToString()
|
exception.toRemoteError(),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.error("${patch.name} failed:")
|
logger.error("${patch.name} failed:")
|
||||||
logger.error(exception.stackTraceToString())
|
logger.error(exception.stackTraceToString())
|
||||||
throw exception
|
throw exception
|
||||||
}
|
}
|
||||||
|
|
||||||
nextPatchIndex++
|
onEvent(
|
||||||
|
ProgressEvent.Completed(
|
||||||
onPatchCompleted()
|
StepId.ExecutePatch(index),
|
||||||
|
|
||||||
selectedPatches.getOrNull(nextPatchIndex)?.let { nextPatch ->
|
|
||||||
updateProgress(
|
|
||||||
name = androidContext.getString(R.string.executing_patch, nextPatch.name)
|
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
|
|
||||||
logger.info("${patch.name} succeeded")
|
logger.info("${patch.name} succeeded")
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProgress(
|
|
||||||
state = State.COMPLETED,
|
|
||||||
name = androidContext.resources.getQuantityString(
|
|
||||||
R.plurals.patches_executed,
|
|
||||||
selectedPatches.size,
|
|
||||||
selectedPatches.size
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun run(output: File, selectedPatches: PatchList) {
|
suspend fun run(output: File, selectedPatches: PatchList) {
|
||||||
updateProgress(state = State.COMPLETED) // Unpacking
|
runStep(StepId.ExecutePatches, onEvent) {
|
||||||
|
java.util.logging.Logger.getLogger("").apply {
|
||||||
|
handlers.forEach {
|
||||||
|
it.close()
|
||||||
|
removeHandler(it)
|
||||||
|
}
|
||||||
|
|
||||||
java.util.logging.Logger.getLogger("").apply {
|
addHandler(logger.handler)
|
||||||
handlers.forEach {
|
|
||||||
it.close()
|
|
||||||
removeHandler(it)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addHandler(logger.handler)
|
with(patcher) {
|
||||||
|
logger.info("Merging integrations")
|
||||||
|
this += selectedPatches.toSet()
|
||||||
|
|
||||||
|
logger.info("Applying patches...")
|
||||||
|
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
with(patcher) {
|
runStep(StepId.WriteAPK, onEvent) {
|
||||||
logger.info("Merging integrations")
|
logger.info("Writing patched files...")
|
||||||
this += selectedPatches.toSet()
|
val result = patcher.get()
|
||||||
|
|
||||||
logger.info("Applying patches...")
|
val patched = tempDir.resolve("result.apk")
|
||||||
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
|
withContext(Dispatchers.IO) {
|
||||||
|
Files.copy(input.toPath(), patched.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||||
|
}
|
||||||
|
result.applyTo(patched)
|
||||||
|
|
||||||
|
logger.info("Patched apk saved to $patched")
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
Files.move(patched.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Writing patched files...")
|
|
||||||
val result = patcher.get()
|
|
||||||
|
|
||||||
val patched = tempDir.resolve("result.apk")
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
Files.copy(input.toPath(), patched.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
|
||||||
}
|
|
||||||
result.applyTo(patched)
|
|
||||||
|
|
||||||
logger.info("Patched apk saved to $patched")
|
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
Files.move(patched.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
|
||||||
}
|
|
||||||
updateProgress(state = State.COMPLETED) // Saving
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package app.revanced.manager.patcher.runtime
|
package app.revanced.manager.patcher.runtime
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import app.revanced.manager.patcher.ProgressEvent
|
||||||
import app.revanced.manager.patcher.Session
|
import app.revanced.manager.patcher.Session
|
||||||
|
import app.revanced.manager.patcher.StepId
|
||||||
import app.revanced.manager.patcher.logger.Logger
|
import app.revanced.manager.patcher.logger.Logger
|
||||||
import app.revanced.manager.patcher.patch.PatchBundle
|
import app.revanced.manager.patcher.patch.PatchBundle
|
||||||
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
import app.revanced.manager.patcher.runStep
|
||||||
import app.revanced.manager.ui.model.State
|
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -13,7 +14,7 @@ import java.io.File
|
|||||||
/**
|
/**
|
||||||
* Simple [Runtime] implementation that runs the patcher using coroutines.
|
* Simple [Runtime] implementation that runs the patcher using coroutines.
|
||||||
*/
|
*/
|
||||||
class CoroutineRuntime(private val context: Context) : Runtime(context) {
|
class CoroutineRuntime(context: Context) : Runtime(context) {
|
||||||
override suspend fun execute(
|
override suspend fun execute(
|
||||||
inputFile: String,
|
inputFile: String,
|
||||||
outputFile: String,
|
outputFile: String,
|
||||||
@@ -21,47 +22,50 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) {
|
|||||||
selectedPatches: PatchSelection,
|
selectedPatches: PatchSelection,
|
||||||
options: Options,
|
options: Options,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
onPatchCompleted: suspend () -> Unit,
|
onEvent: (ProgressEvent) -> Unit,
|
||||||
onProgress: ProgressEventHandler,
|
|
||||||
) {
|
) {
|
||||||
val selectedBundles = selectedPatches.keys
|
val patchList = runStep(StepId.LoadPatches, onEvent) {
|
||||||
val bundles = bundles()
|
val selectedBundles = selectedPatches.keys
|
||||||
val uids = bundles.entries.associate { (key, value) -> value to key }
|
val bundles = bundles()
|
||||||
|
val uids = bundles.entries.associate { (key, value) -> value to key }
|
||||||
|
|
||||||
val allPatches =
|
val allPatches =
|
||||||
PatchBundle.Loader.patches(bundles.values, packageName)
|
PatchBundle.Loader.patches(bundles.values, packageName)
|
||||||
.mapKeys { (b, _) -> uids[b]!! }
|
.mapKeys { (b, _) -> uids[b]!! }
|
||||||
.filterKeys { it in selectedBundles }
|
.filterKeys { it in selectedBundles }
|
||||||
|
|
||||||
val patchList = selectedPatches.flatMap { (bundle, selected) ->
|
val patchList = selectedPatches.flatMap { (bundle, selected) ->
|
||||||
allPatches[bundle]?.filter { it.name in selected }
|
allPatches[bundle]?.filter { it.name in selected }
|
||||||
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
|
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set all patch options.
|
// Set all patch options.
|
||||||
options.forEach { (bundle, bundlePatchOptions) ->
|
options.forEach { (bundle, bundlePatchOptions) ->
|
||||||
val patches = allPatches[bundle] ?: return@forEach
|
val patches = allPatches[bundle] ?: return@forEach
|
||||||
bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
|
bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
|
||||||
val patchOptions = patches.single { it.name == patchName }.options
|
val patchOptions = patches.single { it.name == patchName }.options
|
||||||
configuredPatchOptions.forEach { (key, value) ->
|
configuredPatchOptions.forEach { (key, value) ->
|
||||||
patchOptions[key] = value
|
patchOptions[key] = value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
patchList
|
||||||
}
|
}
|
||||||
|
|
||||||
onProgress(null, State.COMPLETED, null) // Loading patches
|
val session = runStep(StepId.ReadAPK, onEvent) {
|
||||||
|
Session(
|
||||||
|
cacheDir,
|
||||||
|
frameworkPath,
|
||||||
|
aaptPath,
|
||||||
|
logger,
|
||||||
|
File(inputFile),
|
||||||
|
onEvent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Session(
|
session.use { s ->
|
||||||
cacheDir,
|
s.run(
|
||||||
frameworkPath,
|
|
||||||
aaptPath,
|
|
||||||
context,
|
|
||||||
logger,
|
|
||||||
File(inputFile),
|
|
||||||
onPatchCompleted = onPatchCompleted,
|
|
||||||
onProgress
|
|
||||||
).use { session ->
|
|
||||||
session.run(
|
|
||||||
File(outputFile),
|
File(outputFile),
|
||||||
patchList
|
patchList
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ import app.revanced.manager.BuildConfig
|
|||||||
import app.revanced.manager.patcher.runtime.process.IPatcherEvents
|
import app.revanced.manager.patcher.runtime.process.IPatcherEvents
|
||||||
import app.revanced.manager.patcher.runtime.process.IPatcherProcess
|
import app.revanced.manager.patcher.runtime.process.IPatcherProcess
|
||||||
import app.revanced.manager.patcher.LibraryResolver
|
import app.revanced.manager.patcher.LibraryResolver
|
||||||
|
import app.revanced.manager.patcher.ProgressEvent
|
||||||
|
import app.revanced.manager.patcher.ProgressEventParcel
|
||||||
import app.revanced.manager.patcher.logger.Logger
|
import app.revanced.manager.patcher.logger.Logger
|
||||||
import app.revanced.manager.patcher.runtime.process.Parameters
|
import app.revanced.manager.patcher.runtime.process.Parameters
|
||||||
import app.revanced.manager.patcher.runtime.process.PatchConfiguration
|
import app.revanced.manager.patcher.runtime.process.PatchConfiguration
|
||||||
import app.revanced.manager.patcher.runtime.process.PatcherProcess
|
import app.revanced.manager.patcher.runtime.process.PatcherProcess
|
||||||
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
import app.revanced.manager.patcher.toEvent
|
||||||
import app.revanced.manager.ui.model.State
|
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
@@ -66,8 +67,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
|||||||
selectedPatches: PatchSelection,
|
selectedPatches: PatchSelection,
|
||||||
options: Options,
|
options: Options,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
onPatchCompleted: suspend () -> Unit,
|
onEvent: (ProgressEvent) -> Unit,
|
||||||
onProgress: ProgressEventHandler,
|
|
||||||
) = coroutineScope {
|
) = coroutineScope {
|
||||||
// Get the location of our own Apk.
|
// Get the location of our own Apk.
|
||||||
val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo!!.sourceDir
|
val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo!!.sourceDir
|
||||||
@@ -111,7 +111,6 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val patching = CompletableDeferred<Unit>()
|
val patching = CompletableDeferred<Unit>()
|
||||||
val scope = this
|
|
||||||
|
|
||||||
launch(Dispatchers.IO) {
|
launch(Dispatchers.IO) {
|
||||||
val binder = awaitBinderConnection()
|
val binder = awaitBinderConnection()
|
||||||
@@ -124,13 +123,10 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
|||||||
val eventHandler = object : IPatcherEvents.Stub() {
|
val eventHandler = object : IPatcherEvents.Stub() {
|
||||||
override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg)
|
override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg)
|
||||||
|
|
||||||
override fun patchSucceeded() {
|
override fun event(event: ProgressEventParcel?) {
|
||||||
scope.launch { onPatchCompleted() }
|
event?.let { onEvent(it.toEvent()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun progress(name: String?, state: String?, msg: String?) =
|
|
||||||
onProgress(name, state?.let { enumValueOf<State>(it) }, msg)
|
|
||||||
|
|
||||||
override fun finished(exceptionStackTrace: String?) {
|
override fun finished(exceptionStackTrace: String?) {
|
||||||
binder.exit()
|
binder.exit()
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import android.content.Context
|
|||||||
import app.revanced.manager.data.platform.Filesystem
|
import app.revanced.manager.data.platform.Filesystem
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
|
import app.revanced.manager.patcher.ProgressEvent
|
||||||
import app.revanced.manager.patcher.aapt.Aapt
|
import app.revanced.manager.patcher.aapt.Aapt
|
||||||
import app.revanced.manager.patcher.logger.Logger
|
import app.revanced.manager.patcher.logger.Logger
|
||||||
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
@@ -34,7 +34,6 @@ sealed class Runtime(context: Context) : KoinComponent {
|
|||||||
selectedPatches: PatchSelection,
|
selectedPatches: PatchSelection,
|
||||||
options: Options,
|
options: Options,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
onPatchCompleted: suspend () -> Unit,
|
onEvent: (ProgressEvent) -> Unit,
|
||||||
onProgress: ProgressEventHandler,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -8,12 +8,15 @@ import android.os.Build
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import app.revanced.manager.BuildConfig
|
import app.revanced.manager.BuildConfig
|
||||||
|
import app.revanced.manager.patcher.ProgressEvent
|
||||||
import app.revanced.manager.patcher.Session
|
import app.revanced.manager.patcher.Session
|
||||||
|
import app.revanced.manager.patcher.StepId
|
||||||
import app.revanced.manager.patcher.logger.LogLevel
|
import app.revanced.manager.patcher.logger.LogLevel
|
||||||
import app.revanced.manager.patcher.logger.Logger
|
import app.revanced.manager.patcher.logger.Logger
|
||||||
import app.revanced.manager.patcher.patch.PatchBundle
|
import app.revanced.manager.patcher.patch.PatchBundle
|
||||||
|
import app.revanced.manager.patcher.runStep
|
||||||
import app.revanced.manager.patcher.runtime.ProcessRuntime
|
import app.revanced.manager.patcher.runtime.ProcessRuntime
|
||||||
import app.revanced.manager.ui.model.State
|
import app.revanced.manager.patcher.toParcel
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -24,7 +27,7 @@ import kotlin.system.exitProcess
|
|||||||
/**
|
/**
|
||||||
* The main class that runs inside the runner process launched by [ProcessRuntime].
|
* The main class that runs inside the runner process launched by [ProcessRuntime].
|
||||||
*/
|
*/
|
||||||
class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
class PatcherProcess() : IPatcherProcess.Stub() {
|
||||||
private var eventBinder: IPatcherEvents? = null
|
private var eventBinder: IPatcherEvents? = null
|
||||||
|
|
||||||
private val scope =
|
private val scope =
|
||||||
@@ -46,6 +49,8 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
|||||||
override fun exit() = exitProcess(0)
|
override fun exit() = exitProcess(0)
|
||||||
|
|
||||||
override fun start(parameters: Parameters, events: IPatcherEvents) {
|
override fun start(parameters: Parameters, events: IPatcherEvents) {
|
||||||
|
fun onEvent(event: ProgressEvent) = events.event(event.toParcel())
|
||||||
|
|
||||||
eventBinder = events
|
eventBinder = events
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -56,38 +61,42 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
|||||||
|
|
||||||
logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB")
|
logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB")
|
||||||
|
|
||||||
val allPatches = PatchBundle.Loader.patches(parameters.configurations.map { it.bundle }, parameters.packageName)
|
val patchList = runStep(StepId.LoadPatches, ::onEvent) {
|
||||||
val patchList = parameters.configurations.flatMap { config ->
|
val allPatches = PatchBundle.Loader.patches(
|
||||||
val patches = (allPatches[config.bundle] ?: return@flatMap emptyList())
|
parameters.configurations.map { it.bundle },
|
||||||
|
parameters.packageName
|
||||||
|
)
|
||||||
|
|
||||||
|
parameters.configurations.flatMap { config ->
|
||||||
|
val patches = (allPatches[config.bundle] ?: return@flatMap emptyList())
|
||||||
.filter { it.name in config.patches }
|
.filter { it.name in config.patches }
|
||||||
.associateBy { it.name }
|
.associateBy { it.name }
|
||||||
|
|
||||||
config.options.forEach { (patchName, opts) ->
|
config.options.forEach { (patchName, opts) ->
|
||||||
val patchOptions = patches[patchName]?.options
|
val patchOptions = patches[patchName]?.options
|
||||||
?: throw Exception("Patch with name $patchName does not exist.")
|
?: throw Exception("Patch with name $patchName does not exist.")
|
||||||
|
|
||||||
opts.forEach { (key, value) ->
|
opts.forEach { (key, value) ->
|
||||||
patchOptions[key] = value
|
patchOptions[key] = value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
patches.values
|
patches.values
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
events.progress(null, State.COMPLETED.name, null) // Loading patches
|
val session = runStep(StepId.ReadAPK, ::onEvent) {
|
||||||
|
Session(
|
||||||
|
cacheDir = parameters.cacheDir,
|
||||||
|
aaptPath = parameters.aaptPath,
|
||||||
|
frameworkDir = parameters.frameworkDir,
|
||||||
|
logger = logger,
|
||||||
|
input = File(parameters.inputFile),
|
||||||
|
onEvent = ::onEvent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Session(
|
session.use {
|
||||||
cacheDir = parameters.cacheDir,
|
|
||||||
aaptPath = parameters.aaptPath,
|
|
||||||
frameworkDir = parameters.frameworkDir,
|
|
||||||
androidContext = context,
|
|
||||||
logger = logger,
|
|
||||||
input = File(parameters.inputFile),
|
|
||||||
onPatchCompleted = { events.patchSucceeded() },
|
|
||||||
onProgress = { name, state, message ->
|
|
||||||
events.progress(name, state?.name, message)
|
|
||||||
}
|
|
||||||
).use {
|
|
||||||
it.run(File(parameters.outputFile), patchList)
|
it.run(File(parameters.outputFile), patchList)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +128,7 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val ipcInterface = PatcherProcess(appContext)
|
val ipcInterface = PatcherProcess()
|
||||||
|
|
||||||
appContext.sendBroadcast(Intent().apply {
|
appContext.sendBroadcast(Intent().apply {
|
||||||
action = ProcessRuntime.CONNECT_TO_APP_ACTION
|
action = ProcessRuntime.CONNECT_TO_APP_ACTION
|
||||||
|
|||||||
@@ -29,14 +29,17 @@ import app.revanced.manager.domain.repository.InstalledAppRepository
|
|||||||
import app.revanced.manager.domain.worker.Worker
|
import app.revanced.manager.domain.worker.Worker
|
||||||
import app.revanced.manager.domain.worker.WorkerRepository
|
import app.revanced.manager.domain.worker.WorkerRepository
|
||||||
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||||
|
import app.revanced.manager.patcher.ProgressEvent
|
||||||
|
import app.revanced.manager.patcher.StepId
|
||||||
import app.revanced.manager.patcher.logger.Logger
|
import app.revanced.manager.patcher.logger.Logger
|
||||||
|
import app.revanced.manager.patcher.runStep
|
||||||
import app.revanced.manager.patcher.runtime.CoroutineRuntime
|
import app.revanced.manager.patcher.runtime.CoroutineRuntime
|
||||||
import app.revanced.manager.patcher.runtime.ProcessRuntime
|
import app.revanced.manager.patcher.runtime.ProcessRuntime
|
||||||
|
import app.revanced.manager.patcher.toRemoteError
|
||||||
import app.revanced.manager.plugin.downloader.GetScope
|
import app.revanced.manager.plugin.downloader.GetScope
|
||||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||||
import app.revanced.manager.plugin.downloader.UserInteractionException
|
import app.revanced.manager.plugin.downloader.UserInteractionException
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
import app.revanced.manager.ui.model.State
|
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
@@ -48,8 +51,6 @@ import org.koin.core.component.KoinComponent
|
|||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
typealias ProgressEventHandler = (name: String?, state: State?, message: String?) -> Unit
|
|
||||||
|
|
||||||
@OptIn(PluginHostApi::class)
|
@OptIn(PluginHostApi::class)
|
||||||
class PatcherWorker(
|
class PatcherWorker(
|
||||||
context: Context,
|
context: Context,
|
||||||
@@ -71,11 +72,9 @@ class PatcherWorker(
|
|||||||
val selectedPatches: PatchSelection,
|
val selectedPatches: PatchSelection,
|
||||||
val options: Options,
|
val options: Options,
|
||||||
val logger: Logger,
|
val logger: Logger,
|
||||||
val onDownloadProgress: suspend (Pair<Long, Long?>?) -> Unit,
|
|
||||||
val onPatchCompleted: suspend () -> Unit,
|
|
||||||
val handleStartActivityRequest: suspend (LoadedDownloaderPlugin, Intent) -> ActivityResult,
|
val handleStartActivityRequest: suspend (LoadedDownloaderPlugin, Intent) -> ActivityResult,
|
||||||
val setInputFile: suspend (File) -> Unit,
|
val setInputFile: suspend (File) -> Unit,
|
||||||
val onProgress: ProgressEventHandler
|
val onEvent: (ProgressEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
val packageName get() = input.packageName
|
val packageName get() = input.packageName
|
||||||
}
|
}
|
||||||
@@ -140,10 +139,6 @@ class PatcherWorker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun runPatcher(args: Args): Result {
|
private suspend fun runPatcher(args: Args): Result {
|
||||||
|
|
||||||
fun updateProgress(name: String? = null, state: State? = null, message: String? = null) =
|
|
||||||
args.onProgress(name, state, message)
|
|
||||||
|
|
||||||
val patchedApk = fs.tempDir.resolve("patched.apk")
|
val patchedApk = fs.tempDir.resolve("patched.apk")
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
@@ -163,51 +158,65 @@ class PatcherWorker(
|
|||||||
args.input.version,
|
args.input.version,
|
||||||
prefs.suggestedVersionSafeguard.get(),
|
prefs.suggestedVersionSafeguard.get(),
|
||||||
!prefs.disablePatchVersionCompatCheck.get(),
|
!prefs.disablePatchVersionCompatCheck.get(),
|
||||||
onDownload = args.onDownloadProgress
|
onDownload = { progress ->
|
||||||
).also {
|
args.onEvent(
|
||||||
args.setInputFile(it)
|
ProgressEvent.Progress(
|
||||||
updateProgress(state = State.COMPLETED) // Download APK
|
stepId = StepId.DownloadAPK,
|
||||||
}
|
current = progress.first,
|
||||||
|
total = progress.second
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
).also { args.setInputFile(it) }
|
||||||
|
|
||||||
val inputFile = when (val selectedApp = args.input) {
|
val inputFile = when (val selectedApp = args.input) {
|
||||||
is SelectedApp.Download -> {
|
is SelectedApp.Download -> {
|
||||||
val (plugin, data) = downloaderPluginRepository.unwrapParceledData(selectedApp.data)
|
runStep(StepId.DownloadAPK, args.onEvent) {
|
||||||
|
val (plugin, data) = downloaderPluginRepository.unwrapParceledData(
|
||||||
|
selectedApp.data
|
||||||
|
)
|
||||||
|
|
||||||
download(plugin, data)
|
download(plugin, data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is SelectedApp.Search -> {
|
is SelectedApp.Search -> {
|
||||||
downloaderPluginRepository.loadedPluginsFlow.first()
|
runStep(StepId.DownloadAPK, args.onEvent) {
|
||||||
.firstNotNullOfOrNull { plugin ->
|
downloaderPluginRepository.loadedPluginsFlow.first()
|
||||||
try {
|
.firstNotNullOfOrNull { plugin ->
|
||||||
val getScope = object : GetScope {
|
try {
|
||||||
override val pluginPackageName = plugin.packageName
|
val getScope = object : GetScope {
|
||||||
override val hostPackageName = applicationContext.packageName
|
override val pluginPackageName = plugin.packageName
|
||||||
override suspend fun requestStartActivity(intent: Intent): Intent? {
|
override val hostPackageName =
|
||||||
val result = args.handleStartActivityRequest(plugin, intent)
|
applicationContext.packageName
|
||||||
return when (result.resultCode) {
|
|
||||||
Activity.RESULT_OK -> result.data
|
override suspend fun requestStartActivity(intent: Intent): Intent? {
|
||||||
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
|
val result =
|
||||||
else -> throw UserInteractionException.Activity.NotCompleted(
|
args.handleStartActivityRequest(plugin, intent)
|
||||||
result.resultCode,
|
return when (result.resultCode) {
|
||||||
result.data
|
Activity.RESULT_OK -> result.data
|
||||||
)
|
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
|
||||||
|
else -> throw UserInteractionException.Activity.NotCompleted(
|
||||||
|
result.resultCode,
|
||||||
|
result.data
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
withContext(Dispatchers.IO) {
|
||||||
withContext(Dispatchers.IO) {
|
plugin.get(
|
||||||
plugin.get(
|
getScope,
|
||||||
getScope,
|
selectedApp.packageName,
|
||||||
selectedApp.packageName,
|
selectedApp.version
|
||||||
selectedApp.version
|
)
|
||||||
)
|
}?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
|
||||||
}?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
|
} catch (e: UserInteractionException.Activity.NotCompleted) {
|
||||||
} catch (e: UserInteractionException.Activity.NotCompleted) {
|
throw e
|
||||||
throw e
|
} catch (_: UserInteractionException) {
|
||||||
} catch (_: UserInteractionException) {
|
null
|
||||||
null
|
}?.let { (data, _) -> download(plugin, data) }
|
||||||
}?.let { (data, _) -> download(plugin, data) }
|
} ?: throw Exception("App is not available.")
|
||||||
} ?: throw Exception("App is not available.")
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) }
|
is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) }
|
||||||
@@ -227,12 +236,12 @@ class PatcherWorker(
|
|||||||
args.selectedPatches,
|
args.selectedPatches,
|
||||||
args.options,
|
args.options,
|
||||||
args.logger,
|
args.logger,
|
||||||
args.onPatchCompleted,
|
args.onEvent,
|
||||||
args.onProgress
|
|
||||||
)
|
)
|
||||||
|
|
||||||
keystoreManager.sign(patchedApk, File(args.output))
|
runStep(StepId.SignAPK, args.onEvent) {
|
||||||
updateProgress(state = State.COMPLETED) // Signing
|
keystoreManager.sign(patchedApk, File(args.output))
|
||||||
|
}
|
||||||
|
|
||||||
Log.i(tag, "Patching succeeded".logFmt())
|
Log.i(tag, "Patching succeeded".logFmt())
|
||||||
Result.success()
|
Result.success()
|
||||||
@@ -241,11 +250,11 @@ class PatcherWorker(
|
|||||||
tag,
|
tag,
|
||||||
"An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt()
|
"An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt()
|
||||||
)
|
)
|
||||||
updateProgress(state = State.FAILED, message = e.originalStackTrace)
|
args.onEvent(ProgressEvent.Failed(null, e.toRemoteError())) // Fallback if exception doesn't occur within step
|
||||||
Result.failure()
|
Result.failure()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(tag, "An exception occurred while patching".logFmt(), e)
|
Log.e(tag, "An exception occurred while patching".logFmt(), e)
|
||||||
updateProgress(state = State.FAILED, message = e.stackTraceToString())
|
args.onEvent(ProgressEvent.Failed(null, e.toRemoteError())) // Fallback if exception doesn't occur within step
|
||||||
Result.failure()
|
Result.failure()
|
||||||
} finally {
|
} finally {
|
||||||
patchedApk.delete()
|
patchedApk.delete()
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
package app.revanced.manager.service
|
|
||||||
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.IBinder
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
class InstallService : Service() {
|
|
||||||
|
|
||||||
override fun onStartCommand(
|
|
||||||
intent: Intent, flags: Int, startId: Int
|
|
||||||
): Int {
|
|
||||||
val extraStatus = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)
|
|
||||||
val extraStatusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
|
||||||
val extraPackageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
|
|
||||||
when (extraStatus) {
|
|
||||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
|
||||||
startActivity(if (Build.VERSION.SDK_INT >= 33) {
|
|
||||||
intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
|
|
||||||
} else {
|
|
||||||
intent.getParcelableExtra(Intent.EXTRA_INTENT)
|
|
||||||
}.apply {
|
|
||||||
this?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
sendBroadcast(Intent().apply {
|
|
||||||
action = APP_INSTALL_ACTION
|
|
||||||
`package` = packageName
|
|
||||||
putExtra(EXTRA_INSTALL_STATUS, extraStatus)
|
|
||||||
putExtra(EXTRA_INSTALL_STATUS_MESSAGE, extraStatusMessage)
|
|
||||||
putExtra(EXTRA_PACKAGE_NAME, extraPackageName)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stopSelf()
|
|
||||||
return START_NOT_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? = null
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val APP_INSTALL_ACTION = "APP_INSTALL_ACTION"
|
|
||||||
|
|
||||||
const val EXTRA_INSTALL_STATUS = "EXTRA_INSTALL_STATUS"
|
|
||||||
const val EXTRA_INSTALL_STATUS_MESSAGE = "EXTRA_INSTALL_STATUS_MESSAGE"
|
|
||||||
const val EXTRA_PACKAGE_NAME = "EXTRA_PACKAGE_NAME"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import io.github.fornewid.placeholder.material3.placeholder
|
import com.eygraber.compose.placeholder.material3.placeholder
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppIcon(
|
fun AppIcon(
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import io.github.fornewid.placeholder.material3.placeholder
|
import com.eygraber.compose.placeholder.material3.placeholder
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -21,7 +22,6 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -69,10 +69,7 @@ fun AppTopBar(
|
|||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
if (onBackClick != null) {
|
if (onBackClick != null) {
|
||||||
TooltipIconButton(
|
IconButton(onClick = onBackClick) {
|
||||||
onClick = onBackClick,
|
|
||||||
tooltip = stringResource(R.string.back),
|
|
||||||
) {
|
|
||||||
backIcon()
|
backIcon()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,10 +108,7 @@ fun AppTopBar(
|
|||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
if (onBackClick != null) {
|
if (onBackClick != null) {
|
||||||
TooltipIconButton(
|
IconButton(onClick = onBackClick) {
|
||||||
onClick = onBackClick,
|
|
||||||
tooltip = stringResource(R.string.back),
|
|
||||||
) {
|
|
||||||
backIcon()
|
backIcon()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import androidx.compose.animation.core.animateFloatAsState
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ArrowButton(
|
fun ArrowButton(
|
||||||
@@ -27,11 +27,7 @@ fun ArrowButton(
|
|||||||
)
|
)
|
||||||
|
|
||||||
onClick?.let {
|
onClick?.let {
|
||||||
TooltipIconButton(
|
IconButton(onClick = it) {
|
||||||
modifier = Modifier,
|
|
||||||
onClick = it,
|
|
||||||
tooltip = stringResource(description),
|
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.KeyboardArrowUp,
|
imageVector = Icons.Filled.KeyboardArrowUp,
|
||||||
contentDescription = stringResource(description),
|
contentDescription = stringResource(description),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|||||||
import androidx.compose.material.icons.outlined.Share
|
import androidx.compose.material.icons.outlined.Share
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -17,7 +18,6 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.bundle.BundleTopBar
|
import app.revanced.manager.ui.component.bundle.BundleTopBar
|
||||||
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -39,8 +39,7 @@ fun ExceptionViewerDialog(text: String, onDismiss: () -> Unit) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
TooltipIconButton(
|
IconButton(
|
||||||
modifier = Modifier,
|
|
||||||
onClick = {
|
onClick = {
|
||||||
val sendIntent: Intent = Intent().apply {
|
val sendIntent: Intent = Intent().apply {
|
||||||
action = Intent.ACTION_SEND
|
action = Intent.ACTION_SEND
|
||||||
@@ -53,8 +52,7 @@ fun ExceptionViewerDialog(text: String, onDismiss: () -> Unit) {
|
|||||||
|
|
||||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||||
context.startActivity(shareIntent)
|
context.startActivity(shareIntent)
|
||||||
},
|
}
|
||||||
tooltip = stringResource(R.string.share),
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.Share,
|
Icons.Outlined.Share,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.revanced.manager.ui.component
|
package app.revanced.manager.ui.component
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.pm.PackageInstaller
|
import android.content.pm.PackageInstaller
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
@@ -79,7 +80,7 @@ private fun installerStatusDialogButton(
|
|||||||
enum class DialogKind(
|
enum class DialogKind(
|
||||||
val flag: Int,
|
val flag: Int,
|
||||||
val title: Int,
|
val title: Int,
|
||||||
@StringRes val contentStringResId: Int,
|
@param:StringRes val contentStringResId: Int,
|
||||||
val icon: ImageVector = Icons.Outlined.ErrorOutline,
|
val icon: ImageVector = Icons.Outlined.ErrorOutline,
|
||||||
val confirmButton: InstallerStatusDialogButton = installerStatusDialogButton(R.string.ok),
|
val confirmButton: InstallerStatusDialogButton = installerStatusDialogButton(R.string.ok),
|
||||||
val dismissButton: InstallerStatusDialogButton? = null,
|
val dismissButton: InstallerStatusDialogButton? = null,
|
||||||
@@ -133,10 +134,8 @@ enum class DialogKind(
|
|||||||
title = R.string.installation_storage_issue_dialog_title,
|
title = R.string.installation_storage_issue_dialog_title,
|
||||||
contentStringResId = R.string.installation_storage_issue_description,
|
contentStringResId = R.string.installation_storage_issue_description,
|
||||||
),
|
),
|
||||||
|
|
||||||
@RequiresApi(34)
|
|
||||||
FAILURE_TIMEOUT(
|
FAILURE_TIMEOUT(
|
||||||
flag = PackageInstaller.STATUS_FAILURE_TIMEOUT,
|
flag = @SuppressLint("InlinedApi") PackageInstaller.STATUS_FAILURE_TIMEOUT,
|
||||||
title = R.string.installation_timeout_dialog_title,
|
title = R.string.installation_timeout_dialog_title,
|
||||||
contentStringResId = R.string.installation_timeout_description,
|
contentStringResId = R.string.installation_timeout_description,
|
||||||
confirmButton = installerStatusDialogButton(R.string.install_app) { model ->
|
confirmButton = installerStatusDialogButton(R.string.install_app) { model ->
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ fun LoadingIndicator(
|
|||||||
progress: () -> Float? = { null },
|
progress: () -> Float? = { null },
|
||||||
color: Color = ProgressIndicatorDefaults.circularColor,
|
color: Color = ProgressIndicatorDefaults.circularColor,
|
||||||
strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth,
|
strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth,
|
||||||
trackColor: Color = ProgressIndicatorDefaults.circularTrackColor,
|
trackColor: Color = ProgressIndicatorDefaults.circularIndeterminateTrackColor,
|
||||||
strokeCap: StrokeCap = ProgressIndicatorDefaults.CircularDeterminateStrokeCap
|
strokeCap: StrokeCap = ProgressIndicatorDefaults.CircularDeterminateStrokeCap
|
||||||
) {
|
) {
|
||||||
progress()?.let {
|
progress()?.let {
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ fun Markdown(
|
|||||||
colors = markdownColor(
|
colors = markdownColor(
|
||||||
text = MaterialTheme.colorScheme.onSurfaceVariant,
|
text = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
codeBackground = MaterialTheme.colorScheme.secondaryContainer,
|
codeBackground = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
codeText = MaterialTheme.colorScheme.onSecondaryContainer,
|
|
||||||
linkText = MaterialTheme.colorScheme.primary
|
|
||||||
),
|
),
|
||||||
typography = markdownTypography(
|
typography = markdownTypography(
|
||||||
h1 = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.Bold),
|
h1 = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.Bold),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import androidx.compose.material.icons.outlined.Close
|
|||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -24,7 +25,6 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NotificationCard(
|
fun NotificationCard(
|
||||||
@@ -138,10 +138,7 @@ fun NotificationCard(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (onDismiss != null) {
|
if (onDismiss != null) {
|
||||||
TooltipIconButton(
|
IconButton(onClick = onDismiss) {
|
||||||
onClick = onDismiss,
|
|
||||||
tooltip = stringResource(R.string.close),
|
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Close,
|
imageVector = Icons.Outlined.Close,
|
||||||
contentDescription = stringResource(R.string.close),
|
contentDescription = stringResource(R.string.close),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.outlined.Visibility
|
import androidx.compose.material.icons.outlined.Visibility
|
||||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -18,7 +19,6 @@ import androidx.compose.ui.text.input.KeyboardType
|
|||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PasswordField(modifier: Modifier = Modifier, value: String, onValueChange: (String) -> Unit, label: @Composable (() -> Unit)? = null, placeholder: @Composable (() -> Unit)? = null) {
|
fun PasswordField(modifier: Modifier = Modifier, value: String, onValueChange: (String) -> Unit, label: @Composable (() -> Unit)? = null, placeholder: @Composable (() -> Unit)? = null) {
|
||||||
@@ -33,15 +33,9 @@ fun PasswordField(modifier: Modifier = Modifier, value: String, onValueChange: (
|
|||||||
label = label,
|
label = label,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
TooltipIconButton(
|
IconButton(onClick = {
|
||||||
modifier = Modifier,
|
visible = !visible
|
||||||
onClick = {
|
}) {
|
||||||
visible = !visible
|
|
||||||
},
|
|
||||||
tooltip = if (visible) stringResource(R.string.show_password_field) else stringResource(
|
|
||||||
R.string.hide_password_field
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
val (icon, description) = remember(visible) {
|
val (icon, description) = remember(visible) {
|
||||||
if (visible) Icons.Outlined.VisibilityOff to R.string.hide_password_field else Icons.Outlined.Visibility to R.string.show_password_field
|
if (visible) Icons.Outlined.VisibilityOff to R.string.hide_password_field else Icons.Outlined.Visibility to R.string.show_password_field
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ fun SearchBar(
|
|||||||
) {
|
) {
|
||||||
val colors = SearchBarColors(
|
val colors = SearchBarColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
dividerColor = MaterialTheme.colorScheme.outline
|
dividerColor = MaterialTheme.colorScheme.outline,
|
||||||
|
inputFieldColors = SearchBarDefaults.inputFieldColors()
|
||||||
)
|
)
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SearchBar
|
import androidx.compose.material3.SearchBar
|
||||||
import androidx.compose.material3.SearchBarColors
|
import androidx.compose.material3.SearchBarColors
|
||||||
@@ -18,7 +19,6 @@ import androidx.compose.ui.focus.focusRequester
|
|||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -31,7 +31,8 @@ fun SearchView(
|
|||||||
) {
|
) {
|
||||||
val colors = SearchBarColors(
|
val colors = SearchBarColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
dividerColor = MaterialTheme.colorScheme.outline
|
dividerColor = MaterialTheme.colorScheme.outline,
|
||||||
|
inputFieldColors = SearchBarDefaults.inputFieldColors()
|
||||||
)
|
)
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
@@ -48,10 +49,7 @@ fun SearchView(
|
|||||||
onExpandedChange = onActiveChange,
|
onExpandedChange = onActiveChange,
|
||||||
placeholder = placeholder,
|
placeholder = placeholder,
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
TooltipIconButton(
|
IconButton(onClick = { onActiveChange(false) }) {
|
||||||
tooltip = stringResource(R.string.back),
|
|
||||||
onClick = { onActiveChange(false) }
|
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
Icons.AutoMirrored.Filled.ArrowBack,
|
Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
stringResource(R.string.back)
|
stringResource(R.string.back)
|
||||||
|
|||||||
@@ -45,11 +45,11 @@ import app.revanced.manager.domain.bundles.LocalPatchBundle
|
|||||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
|
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
|
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
|
||||||
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||||
import app.revanced.manager.ui.component.ExceptionViewerDialog
|
import app.revanced.manager.ui.component.ExceptionViewerDialog
|
||||||
import app.revanced.manager.ui.component.FullscreenDialog
|
import app.revanced.manager.ui.component.FullscreenDialog
|
||||||
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
|
|
||||||
import app.revanced.manager.ui.component.TextInputDialog
|
import app.revanced.manager.ui.component.TextInputDialog
|
||||||
import app.revanced.manager.ui.component.haptics.HapticSwitch
|
import app.revanced.manager.ui.component.haptics.HapticSwitch
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -66,12 +66,14 @@ fun BundleInformationDialog(
|
|||||||
) {
|
) {
|
||||||
val bundleRepo = koinInject<PatchBundleRepository>()
|
val bundleRepo = koinInject<PatchBundleRepository>()
|
||||||
val networkInfo = koinInject<NetworkInfo>()
|
val networkInfo = koinInject<NetworkInfo>()
|
||||||
|
val prefs = koinInject<PreferencesManager>()
|
||||||
val hasNetwork = remember { networkInfo.isConnected() }
|
val hasNetwork = remember { networkInfo.isConnected() }
|
||||||
val composableScope = rememberCoroutineScope()
|
val composableScope = rememberCoroutineScope()
|
||||||
var viewCurrentBundlePatches by remember { mutableStateOf(false) }
|
var viewCurrentBundlePatches by remember { mutableStateOf(false) }
|
||||||
val isLocal = src is LocalPatchBundle
|
val isLocal = src is LocalPatchBundle
|
||||||
val bundleManifestAttributes = src.patchBundle?.manifestAttributes
|
val bundleManifestAttributes = src.patchBundle?.manifestAttributes
|
||||||
val (autoUpdate, endpoint) = src.asRemoteOrNull?.let { it.autoUpdate to it.endpoint } ?: (null to null)
|
val (autoUpdate, endpoint) = src.asRemoteOrNull?.let { it.autoUpdate to it.endpoint }
|
||||||
|
?: (null to null)
|
||||||
|
|
||||||
fun onAutoUpdateChange(new: Boolean) = composableScope.launch {
|
fun onAutoUpdateChange(new: Boolean) = composableScope.launch {
|
||||||
with(bundleRepo) {
|
with(bundleRepo) {
|
||||||
@@ -104,11 +106,7 @@ fun BundleInformationDialog(
|
|||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
if (!src.isDefault) {
|
if (!src.isDefault) {
|
||||||
TooltipIconButton(
|
IconButton(onClick = onDeleteRequest) {
|
||||||
modifier = Modifier,
|
|
||||||
onClick = onDeleteRequest,
|
|
||||||
tooltip = stringResource(R.string.delete),
|
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.DeleteOutline,
|
Icons.Outlined.DeleteOutline,
|
||||||
stringResource(R.string.delete)
|
stringResource(R.string.delete)
|
||||||
@@ -116,11 +114,7 @@ fun BundleInformationDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isLocal && hasNetwork) {
|
if (!isLocal && hasNetwork) {
|
||||||
TooltipIconButton(
|
IconButton(onClick = onUpdate) {
|
||||||
modifier = Modifier,
|
|
||||||
onClick = onUpdate,
|
|
||||||
tooltip = stringResource(R.string.refresh),
|
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.Update,
|
Icons.Outlined.Update,
|
||||||
stringResource(R.string.refresh)
|
stringResource(R.string.refresh)
|
||||||
@@ -182,6 +176,34 @@ fun BundleInformationDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (src.isDefault) {
|
||||||
|
val useBundlePrerelease by prefs.usePatchesPrereleases.getAsState()
|
||||||
|
|
||||||
|
BundleListItem(
|
||||||
|
headlineText = stringResource(R.string.patches_prereleases),
|
||||||
|
supportingText = stringResource(R.string.patches_prereleases_description, src.name),
|
||||||
|
trailingContent = {
|
||||||
|
HapticSwitch(
|
||||||
|
checked = useBundlePrerelease,
|
||||||
|
onCheckedChange = {
|
||||||
|
composableScope.launch {
|
||||||
|
prefs.usePatchesPrereleases.update(
|
||||||
|
it
|
||||||
|
)
|
||||||
|
onUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
composableScope.launch {
|
||||||
|
prefs.usePatchesPrereleases.update(!useBundlePrerelease)
|
||||||
|
onUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
endpoint?.takeUnless { src.isDefault }?.let { url ->
|
endpoint?.takeUnless { src.isDefault }?.let { url ->
|
||||||
var showUrlInputDialog by rememberSaveable {
|
var showUrlInputDialog by rememberSaveable {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package app.revanced.manager.ui.component.bundle
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
@@ -9,11 +10,7 @@ import androidx.compose.material3.TopAppBarDefaults
|
|||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import app.revanced.manager.R
|
|
||||||
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -36,10 +33,7 @@ fun BundleTopBar(
|
|||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
if (onBackClick != null) {
|
if (onBackClick != null) {
|
||||||
TooltipIconButton(
|
IconButton(onClick = onBackClick) {
|
||||||
tooltip = stringResource(R.string.back),
|
|
||||||
onClick = onBackClick
|
|
||||||
) {
|
|
||||||
backIcon()
|
backIcon()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,7 +193,6 @@ private fun ImportBundleStep(
|
|||||||
},
|
},
|
||||||
supportingContent = { Text(stringResource(if (patchBundle != null) R.string.file_field_set else R.string.file_field_not_set)) },
|
supportingContent = { Text(stringResource(if (patchBundle != null) R.string.file_field_set else R.string.file_field_not_set)) },
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
// TODO: Determine if this button should be [TooltipWrap]'ped
|
|
||||||
IconButton(onClick = launchPatchActivity) {
|
IconButton(onClick = launchPatchActivity) {
|
||||||
Icon(imageVector = Icons.Default.Topic, contentDescription = null)
|
Icon(imageVector = Icons.Default.Topic, contentDescription = null)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
package app.revanced.manager.ui.component.haptics
|
|
||||||
|
|
||||||
import android.view.HapticFeedbackConstants
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.material3.FloatingActionButton
|
|
||||||
import androidx.compose.material3.FloatingActionButtonDefaults
|
|
||||||
import androidx.compose.material3.FloatingActionButtonElevation
|
|
||||||
import androidx.compose.material3.SmallFloatingActionButton
|
|
||||||
import androidx.compose.material3.contentColorFor
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.Shape
|
|
||||||
import app.revanced.manager.util.withHapticFeedback
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun HapticSmallFloatingActionButton (
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
shape: Shape = FloatingActionButtonDefaults.smallShape,
|
|
||||||
containerColor: Color = FloatingActionButtonDefaults.containerColor,
|
|
||||||
contentColor: Color = contentColorFor(containerColor),
|
|
||||||
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
|
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
|
||||||
content: @Composable () -> Unit,
|
|
||||||
) {
|
|
||||||
SmallFloatingActionButton(
|
|
||||||
onClick = onClick.withHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY),
|
|
||||||
modifier = modifier,
|
|
||||||
shape = shape,
|
|
||||||
containerColor = containerColor,
|
|
||||||
contentColor = contentColor,
|
|
||||||
elevation = elevation,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
content = content
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package app.revanced.manager.ui.component.patcher
|
package app.revanced.manager.ui.component.patcher
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Cancel
|
import androidx.compose.material.icons.filled.Cancel
|
||||||
import androidx.compose.material.icons.filled.CheckCircle
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
@@ -21,6 +20,7 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -39,11 +39,9 @@ import androidx.compose.ui.unit.dp
|
|||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.ArrowButton
|
import app.revanced.manager.ui.component.ArrowButton
|
||||||
import app.revanced.manager.ui.component.LoadingIndicator
|
import app.revanced.manager.ui.component.LoadingIndicator
|
||||||
import app.revanced.manager.ui.model.ProgressKey
|
|
||||||
import app.revanced.manager.ui.model.State
|
import app.revanced.manager.ui.model.State
|
||||||
import app.revanced.manager.ui.model.Step
|
|
||||||
import app.revanced.manager.ui.model.StepCategory
|
import app.revanced.manager.ui.model.StepCategory
|
||||||
import app.revanced.manager.ui.model.StepProgressProvider
|
import app.revanced.manager.ui.model.Step
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
|
|
||||||
@@ -52,21 +50,10 @@ import kotlin.math.floor
|
|||||||
fun Steps(
|
fun Steps(
|
||||||
category: StepCategory,
|
category: StepCategory,
|
||||||
steps: List<Step>,
|
steps: List<Step>,
|
||||||
stepCount: Pair<Int, Int>? = null,
|
isExpanded: Boolean = false,
|
||||||
stepProgressProvider: StepProgressProvider
|
onExpand: () -> Unit,
|
||||||
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
var expanded by rememberSaveable { mutableStateOf(true) }
|
|
||||||
|
|
||||||
val categoryColor by animateColorAsState(
|
|
||||||
if (expanded) MaterialTheme.colorScheme.surfaceContainerHigh else Color.Transparent,
|
|
||||||
label = "category"
|
|
||||||
)
|
|
||||||
|
|
||||||
val cardColor by animateColorAsState(
|
|
||||||
if (expanded) MaterialTheme.colorScheme.surfaceContainer else Color.Transparent,
|
|
||||||
label = "card"
|
|
||||||
)
|
|
||||||
|
|
||||||
val state = remember(steps) {
|
val state = remember(steps) {
|
||||||
when {
|
when {
|
||||||
steps.all { it.state == State.COMPLETED } -> State.COMPLETED
|
steps.all { it.state == State.COMPLETED } -> State.COMPLETED
|
||||||
@@ -76,62 +63,69 @@ fun Steps(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val filteredSteps = remember(steps) {
|
||||||
|
val failedCount = steps.count { it.state == State.FAILED }
|
||||||
|
|
||||||
|
steps.filter { step ->
|
||||||
|
// Show hidden steps if it's the only failed step.
|
||||||
|
!step.hide || (step.state == State.FAILED && failedCount == 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(state) {
|
||||||
|
if (state == State.RUNNING || state == State.FAILED)
|
||||||
|
onExpand()
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(MaterialTheme.shapes.large)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(cardColor)
|
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clickable(true, onClick = onClick)
|
||||||
.clickable { expanded = !expanded }
|
.fillMaxWidth()
|
||||||
.background(categoryColor)
|
.padding(20.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
StepIcon(state = state, size = 24.dp)
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
modifier = Modifier.padding(16.dp)
|
|
||||||
) {
|
|
||||||
StepIcon(state = state, size = 24.dp)
|
|
||||||
|
|
||||||
Text(stringResource(category.displayName))
|
Text(stringResource(category.displayName))
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
val stepProgress = remember(stepCount, steps) {
|
Text(
|
||||||
stepCount?.let { (current, total) -> "$current/$total" }
|
text = "${filteredSteps.count { it.state == State.COMPLETED }}/${filteredSteps.size}",
|
||||||
?: "${steps.count { it.state == State.COMPLETED }}/${steps.size}"
|
style = MaterialTheme.typography.labelSmall
|
||||||
}
|
)
|
||||||
|
|
||||||
Text(
|
ArrowButton(modifier = Modifier.size(24.dp), expanded = isExpanded, onClick = null)
|
||||||
text = stepProgress,
|
|
||||||
style = MaterialTheme.typography.labelSmall
|
|
||||||
)
|
|
||||||
|
|
||||||
ArrowButton(modifier = Modifier.size(24.dp), expanded = expanded, onClick = null)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimatedVisibility(visible = expanded) {
|
AnimatedVisibility(visible = isExpanded) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier
|
||||||
|
.background(MaterialTheme.colorScheme.background.copy(0.6f))
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 10.dp)
|
||||||
) {
|
) {
|
||||||
steps.forEach { step ->
|
filteredSteps.forEachIndexed { index, step ->
|
||||||
val (progress, progressText) = when (step.progressKey) {
|
val (progress, progressText) = step.progress?.let { (current, total) ->
|
||||||
null -> null
|
if (total != null) current.toFloat() / total.toFloat() to "${current.megaBytes}/${total.megaBytes} MB"
|
||||||
ProgressKey.DOWNLOAD -> stepProgressProvider.downloadProgress?.let { (downloaded, total) ->
|
else null to "${current.megaBytes} MB"
|
||||||
if (total != null) downloaded.toFloat() / total.toFloat() to "${downloaded.megaBytes}/${total.megaBytes} MB"
|
|
||||||
else null to "${downloaded.megaBytes} MB"
|
|
||||||
}
|
|
||||||
} ?: (null to null)
|
} ?: (null to null)
|
||||||
|
|
||||||
SubStep(
|
SubStep(
|
||||||
name = step.name,
|
name = step.title,
|
||||||
state = step.state,
|
state = step.state,
|
||||||
message = step.message,
|
message = step.message,
|
||||||
progress = progress,
|
progress = progress,
|
||||||
progressText = progressText
|
progressText = progressText,
|
||||||
|
isFirst = index == 0,
|
||||||
|
isLast = index == filteredSteps.lastIndex,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,7 +139,9 @@ fun SubStep(
|
|||||||
state: State,
|
state: State,
|
||||||
message: String? = null,
|
message: String? = null,
|
||||||
progress: Float? = null,
|
progress: Float? = null,
|
||||||
progressText: String? = null
|
progressText: String? = null,
|
||||||
|
isFirst: Boolean = false,
|
||||||
|
isLast: Boolean = false,
|
||||||
) {
|
) {
|
||||||
var messageExpanded by rememberSaveable { mutableStateOf(true) }
|
var messageExpanded by rememberSaveable { mutableStateOf(true) }
|
||||||
|
|
||||||
@@ -156,22 +152,22 @@ fun SubStep(
|
|||||||
clickable { messageExpanded = !messageExpanded }
|
clickable { messageExpanded = !messageExpanded }
|
||||||
else this
|
else this
|
||||||
}
|
}
|
||||||
|
.padding(top = if (isFirst) 10.dp else 8.dp, bottom = if (isLast) 20.dp else 8.dp)
|
||||||
|
.padding(horizontal = 20.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
||||||
) {
|
) {
|
||||||
Box(
|
StepIcon(
|
||||||
modifier = Modifier.size(24.dp),
|
size = 18.dp,
|
||||||
contentAlignment = Alignment.Center
|
state = state,
|
||||||
) {
|
progress = progress,
|
||||||
StepIcon(state, progress, size = 20.dp)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = name,
|
text = name,
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.weight(1f, true),
|
modifier = Modifier.weight(1f, true),
|
||||||
@@ -201,7 +197,7 @@ fun SubStep(
|
|||||||
text = message.orEmpty(),
|
text = message.orEmpty(),
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
modifier = Modifier.padding(horizontal = 52.dp, vertical = 8.dp)
|
modifier = Modifier.padding(horizontal = 36.dp, vertical = 8.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,40 +207,44 @@ fun SubStep(
|
|||||||
fun StepIcon(state: State, progress: Float? = null, size: Dp) {
|
fun StepIcon(state: State, progress: Float? = null, size: Dp) {
|
||||||
val strokeWidth = Dp(floor(size.value / 10) + 1)
|
val strokeWidth = Dp(floor(size.value / 10) + 1)
|
||||||
|
|
||||||
when (state) {
|
Crossfade(targetState = state, label = "State CrossFade") { state ->
|
||||||
State.COMPLETED -> Icon(
|
when (state) {
|
||||||
Icons.Filled.CheckCircle,
|
State.COMPLETED -> Icon(
|
||||||
contentDescription = stringResource(R.string.step_completed),
|
Icons.Filled.CheckCircle,
|
||||||
tint = MaterialTheme.colorScheme.surfaceTint,
|
contentDescription = stringResource(R.string.step_completed),
|
||||||
modifier = Modifier.size(size)
|
tint = Color(0xFF59B463),
|
||||||
)
|
modifier = Modifier.size(size)
|
||||||
|
|
||||||
State.FAILED -> Icon(
|
|
||||||
Icons.Filled.Cancel,
|
|
||||||
contentDescription = stringResource(R.string.step_failed),
|
|
||||||
tint = MaterialTheme.colorScheme.error,
|
|
||||||
modifier = Modifier.size(size)
|
|
||||||
)
|
|
||||||
|
|
||||||
State.WAITING -> Icon(
|
|
||||||
Icons.Outlined.Circle,
|
|
||||||
contentDescription = stringResource(R.string.step_waiting),
|
|
||||||
tint = MaterialTheme.colorScheme.surfaceVariant,
|
|
||||||
modifier = Modifier.size(size)
|
|
||||||
)
|
|
||||||
|
|
||||||
State.RUNNING ->
|
|
||||||
LoadingIndicator(
|
|
||||||
modifier = stringResource(R.string.step_running).let { description ->
|
|
||||||
Modifier
|
|
||||||
.size(size)
|
|
||||||
.semantics {
|
|
||||||
contentDescription = description
|
|
||||||
}
|
|
||||||
},
|
|
||||||
progress = { progress },
|
|
||||||
strokeWidth = strokeWidth
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
State.FAILED -> Icon(
|
||||||
|
Icons.Filled.Cancel,
|
||||||
|
contentDescription = stringResource(R.string.step_failed),
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.size(size)
|
||||||
|
)
|
||||||
|
|
||||||
|
State.WAITING -> Icon(
|
||||||
|
Icons.Outlined.Circle,
|
||||||
|
contentDescription = stringResource(R.string.step_waiting),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface.copy(.2f),
|
||||||
|
modifier = Modifier.size(size)
|
||||||
|
)
|
||||||
|
|
||||||
|
State.RUNNING -> {
|
||||||
|
LoadingIndicator(
|
||||||
|
modifier = stringResource(R.string.step_running).let { description ->
|
||||||
|
Modifier
|
||||||
|
.size(size)
|
||||||
|
.semantics {
|
||||||
|
contentDescription = description
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
progress = { progress },
|
||||||
|
strokeWidth = strokeWidth
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ import app.revanced.manager.ui.component.LongInputDialog
|
|||||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||||
import app.revanced.manager.ui.component.haptics.HapticRadioButton
|
import app.revanced.manager.ui.component.haptics.HapticRadioButton
|
||||||
import app.revanced.manager.ui.component.haptics.HapticSwitch
|
import app.revanced.manager.ui.component.haptics.HapticSwitch
|
||||||
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
|
|
||||||
import app.revanced.manager.util.isScrollingUp
|
import app.revanced.manager.util.isScrollingUp
|
||||||
import app.revanced.manager.util.mutableStateSetOf
|
import app.revanced.manager.util.mutableStateSetOf
|
||||||
import app.revanced.manager.util.saver.snapshotStateListSaver
|
import app.revanced.manager.util.saver.snapshotStateListSaver
|
||||||
@@ -124,11 +123,7 @@ private interface OptionEditor<T : Any> {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ListItemTrailingContent(scope: OptionEditorScope<T>) {
|
fun ListItemTrailingContent(scope: OptionEditorScope<T>) {
|
||||||
TooltipIconButton(
|
IconButton(onClick = { scope.checkSafeguard { clickAction(scope) } }) {
|
||||||
modifier = Modifier,
|
|
||||||
tooltip = stringResource(R.string.edit),
|
|
||||||
onClick = { scope.checkSafeguard { clickAction(scope) } }
|
|
||||||
) {
|
|
||||||
Icon(Icons.Outlined.Edit, stringResource(R.string.edit))
|
Icon(Icons.Outlined.Edit, stringResource(R.string.edit))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,9 +270,7 @@ private object StringOptionEditor : OptionEditor<String> {
|
|||||||
},
|
},
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
var showDropdownMenu by rememberSaveable { mutableStateOf(false) }
|
var showDropdownMenu by rememberSaveable { mutableStateOf(false) }
|
||||||
TooltipIconButton(
|
IconButton(
|
||||||
modifier = Modifier,
|
|
||||||
tooltip = stringResource(R.string.string_option_menu_description),
|
|
||||||
onClick = { showDropdownMenu = true }
|
onClick = { showDropdownMenu = true }
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -587,9 +580,7 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
|
|||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
if (deleteMode) {
|
if (deleteMode) {
|
||||||
TooltipIconButton(
|
IconButton(
|
||||||
modifier = Modifier,
|
|
||||||
tooltip = stringResource(R.string.select_deselect_all),
|
|
||||||
onClick = {
|
onClick = {
|
||||||
if (items.size == deletionTargets.size) deletionTargets.clear()
|
if (items.size == deletionTargets.size) deletionTargets.clear()
|
||||||
else deletionTargets.addAll(items.map { it.key })
|
else deletionTargets.addAll(items.map { it.key })
|
||||||
@@ -600,9 +591,7 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
|
|||||||
stringResource(R.string.select_deselect_all)
|
stringResource(R.string.select_deselect_all)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
TooltipIconButton(
|
IconButton(
|
||||||
modifier = Modifier,
|
|
||||||
tooltip = stringResource(R.string.delete),
|
|
||||||
onClick = {
|
onClick = {
|
||||||
items.removeIf { it.key in deletionTargets }
|
items.removeIf { it.key in deletionTargets }
|
||||||
deletionTargets.clear()
|
deletionTargets.clear()
|
||||||
@@ -615,15 +604,8 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
TooltipIconButton(
|
IconButton(onClick = items::clear) {
|
||||||
modifier = Modifier,
|
Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
|
||||||
tooltip = stringResource(R.string.reset),
|
|
||||||
onClick = items::clear
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Outlined.Restore,
|
|
||||||
stringResource(R.string.reset)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -691,10 +673,9 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
|
|||||||
),
|
),
|
||||||
tonalElevation = if (deleteMode && item.key in deletionTargets) 8.dp else 0.dp,
|
tonalElevation = if (deleteMode && item.key in deletionTargets) 8.dp else 0.dp,
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
TooltipIconButton(
|
IconButton(
|
||||||
modifier = Modifier.draggableHandle(interactionSource = interactionSource),
|
modifier = Modifier.draggableHandle(interactionSource = interactionSource),
|
||||||
tooltip = stringResource(R.string.delete),
|
onClick = {},
|
||||||
onClick = { }
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.DragHandle,
|
Icons.Filled.DragHandle,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.compose.foundation.clickable
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Edit
|
import androidx.compose.material.icons.outlined.Edit
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -16,7 +17,6 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.domain.manager.base.Preference
|
import app.revanced.manager.domain.manager.base.Preference
|
||||||
import app.revanced.manager.ui.component.IntInputDialog
|
import app.revanced.manager.ui.component.IntInputDialog
|
||||||
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -65,14 +65,10 @@ fun IntegerItem(
|
|||||||
headlineContent = stringResource(headline),
|
headlineContent = stringResource(headline),
|
||||||
supportingContent = stringResource(description),
|
supportingContent = stringResource(description),
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
TooltipIconButton(
|
IconButton(onClick = { dialogOpen = true }) {
|
||||||
modifier = modifier,
|
|
||||||
onClick = { dialogOpen = true },
|
|
||||||
tooltip = stringResource(R.string.edit),
|
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Edit,
|
Icons.Outlined.Edit,
|
||||||
contentDescription = stringResource(R.string.edit),
|
contentDescription = stringResource(R.string.edit)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
package app.revanced.manager.ui.component.tooltip
|
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.FloatingActionButtonDefaults
|
|
||||||
import androidx.compose.material3.FloatingActionButtonElevation
|
|
||||||
import androidx.compose.material3.TooltipDefaults
|
|
||||||
import androidx.compose.material3.contentColorFor
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.Shape
|
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
|
||||||
import androidx.compose.ui.window.PopupPositionProvider
|
|
||||||
import app.revanced.manager.ui.component.haptics.HapticFloatingActionButton
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [HapticFloatingActionButton] with tooltip-specific params.
|
|
||||||
*
|
|
||||||
* @param tooltip [String] text to show in a tooltip.
|
|
||||||
* @param positionProvider [PopupPositionProvider] Anchor point for the tooltip.
|
|
||||||
* @param haptic Whether to perform haptic feedback when the tooltip shown.
|
|
||||||
* @param hapticFeedbackType The type of haptic feedback to perform.
|
|
||||||
*
|
|
||||||
* @see [HapticFloatingActionButton]
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun TooltipFloatingActionButton(
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
shape: Shape = FloatingActionButtonDefaults.shape,
|
|
||||||
containerColor: Color = FloatingActionButtonDefaults.containerColor,
|
|
||||||
contentColor: Color = contentColorFor(containerColor),
|
|
||||||
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
|
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
|
||||||
tooltip: String,
|
|
||||||
positionProvider: PopupPositionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
|
||||||
haptic: Boolean = true,
|
|
||||||
hapticFeedbackType: HapticFeedbackType = HapticFeedbackType.LongPress,
|
|
||||||
content: @Composable (() -> Unit)
|
|
||||||
) {
|
|
||||||
TooltipWrap(
|
|
||||||
tooltip = tooltip,
|
|
||||||
positionProvider = positionProvider,
|
|
||||||
haptic = haptic,
|
|
||||||
hapticFeedbackType = hapticFeedbackType,
|
|
||||||
) {
|
|
||||||
HapticFloatingActionButton(
|
|
||||||
onClick = onClick,
|
|
||||||
modifier = modifier,
|
|
||||||
shape = shape,
|
|
||||||
containerColor = containerColor,
|
|
||||||
contentColor = contentColor,
|
|
||||||
elevation = elevation,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
content = content,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [HapticFloatingActionButton] with tooltip-specific params.
|
|
||||||
*
|
|
||||||
* @param tooltip [Int] or `id` string resource to show in a tooltip.
|
|
||||||
* @param positionProvider [PopupPositionProvider] Anchor point for the tooltip.
|
|
||||||
* @param haptic Whether to perform haptic feedback when the tooltip shown.
|
|
||||||
* @param hapticFeedbackType The type of haptic feedback to perform.
|
|
||||||
*
|
|
||||||
* @see [HapticFloatingActionButton]
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun TooltipFloatingActionButton(
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
shape: Shape = FloatingActionButtonDefaults.shape,
|
|
||||||
containerColor: Color = FloatingActionButtonDefaults.containerColor,
|
|
||||||
contentColor: Color = contentColorFor(containerColor),
|
|
||||||
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
|
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
|
||||||
@StringRes tooltip: Int,
|
|
||||||
positionProvider: PopupPositionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
|
||||||
haptic: Boolean = true,
|
|
||||||
hapticFeedbackType: HapticFeedbackType = HapticFeedbackType.LongPress,
|
|
||||||
content: @Composable (() -> Unit)
|
|
||||||
) {
|
|
||||||
TooltipWrap(
|
|
||||||
tooltip = tooltip,
|
|
||||||
positionProvider = positionProvider,
|
|
||||||
haptic = haptic,
|
|
||||||
hapticFeedbackType = hapticFeedbackType,
|
|
||||||
) {
|
|
||||||
HapticFloatingActionButton(
|
|
||||||
onClick = onClick,
|
|
||||||
modifier = modifier,
|
|
||||||
shape = shape,
|
|
||||||
containerColor = containerColor,
|
|
||||||
contentColor = contentColor,
|
|
||||||
elevation = elevation,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
content = content,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
package app.revanced.manager.ui.component.tooltip
|
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.IconButtonColors
|
|
||||||
import androidx.compose.material3.IconButtonDefaults
|
|
||||||
import androidx.compose.material3.TooltipDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
|
||||||
import androidx.compose.ui.window.PopupPositionProvider
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [IconButton] with tooltip-specific params.
|
|
||||||
*
|
|
||||||
* @param tooltip [String] text to show in a tooltip.
|
|
||||||
* @param positionProvider [PopupPositionProvider] Anchor point for the tooltip.
|
|
||||||
* @param haptic Whether to perform haptic feedback when the tooltip shown.
|
|
||||||
* @param hapticFeedbackType The type of haptic feedback to perform.
|
|
||||||
*
|
|
||||||
* @see [IconButton]
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun TooltipIconButton(
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
enabled: Boolean = true,
|
|
||||||
colors: IconButtonColors = IconButtonDefaults.iconButtonColors(),
|
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
|
||||||
tooltip: String,
|
|
||||||
positionProvider: PopupPositionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
|
||||||
haptic: Boolean = true,
|
|
||||||
hapticFeedbackType: HapticFeedbackType = HapticFeedbackType.LongPress,
|
|
||||||
content: @Composable (() -> Unit),
|
|
||||||
) {
|
|
||||||
TooltipWrap(
|
|
||||||
tooltip = tooltip,
|
|
||||||
positionProvider = positionProvider,
|
|
||||||
haptic = haptic,
|
|
||||||
hapticFeedbackType = hapticFeedbackType,
|
|
||||||
) {
|
|
||||||
IconButton(
|
|
||||||
onClick = onClick,
|
|
||||||
modifier = modifier,
|
|
||||||
enabled = enabled,
|
|
||||||
colors = colors,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
content = content,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [IconButton] with tooltip-specific params.
|
|
||||||
*
|
|
||||||
* @param tooltip [Int] or `id` string resource to show in a tooltip.
|
|
||||||
* @param positionProvider [PopupPositionProvider] Anchor point for the tooltip.
|
|
||||||
* @param haptic Whether to perform haptic feedback when the tooltip shown.
|
|
||||||
* @param hapticFeedbackType The type of haptic feedback to perform.
|
|
||||||
*
|
|
||||||
* @see [IconButton]
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun TooltipIconButton(
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
enabled: Boolean = true,
|
|
||||||
colors: IconButtonColors = IconButtonDefaults.iconButtonColors(),
|
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
|
||||||
@StringRes tooltip: Int,
|
|
||||||
positionProvider: PopupPositionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
|
||||||
haptic: Boolean = true,
|
|
||||||
hapticFeedbackType: HapticFeedbackType = HapticFeedbackType.LongPress,
|
|
||||||
content: @Composable (() -> Unit),
|
|
||||||
) {
|
|
||||||
TooltipWrap(
|
|
||||||
tooltip = tooltip,
|
|
||||||
positionProvider = positionProvider,
|
|
||||||
haptic = haptic,
|
|
||||||
hapticFeedbackType = hapticFeedbackType,
|
|
||||||
) {
|
|
||||||
IconButton(
|
|
||||||
onClick = onClick,
|
|
||||||
modifier = modifier,
|
|
||||||
enabled = enabled,
|
|
||||||
colors = colors,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
content = content,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
package app.revanced.manager.ui.component.tooltip
|
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.FloatingActionButtonDefaults
|
|
||||||
import androidx.compose.material3.FloatingActionButtonElevation
|
|
||||||
import androidx.compose.material3.TooltipDefaults
|
|
||||||
import androidx.compose.material3.contentColorFor
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.Shape
|
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
|
||||||
import androidx.compose.ui.window.PopupPositionProvider
|
|
||||||
import app.revanced.manager.ui.component.haptics.HapticSmallFloatingActionButton
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [HapticSmallFloatingActionButton] with tooltip-specific params.
|
|
||||||
*
|
|
||||||
* @param tooltip [String] text to show in a tooltip.
|
|
||||||
* @param positionProvider [PopupPositionProvider] Anchor point for the tooltip.
|
|
||||||
* @param haptic Whether to perform haptic feedback when the tooltip shown.
|
|
||||||
* @param hapticFeedbackType The type of haptic feedback to perform.
|
|
||||||
*
|
|
||||||
* @see [HapticSmallFloatingActionButton]
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun TooltipSmallFloatingActionButton(
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
shape: Shape = FloatingActionButtonDefaults.smallShape,
|
|
||||||
containerColor: Color = FloatingActionButtonDefaults.containerColor,
|
|
||||||
contentColor: Color = contentColorFor(containerColor),
|
|
||||||
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
|
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
|
||||||
tooltip: String,
|
|
||||||
positionProvider: PopupPositionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
|
||||||
haptic: Boolean = true,
|
|
||||||
hapticFeedbackType: HapticFeedbackType = HapticFeedbackType.LongPress,
|
|
||||||
content: @Composable (() -> Unit)
|
|
||||||
) {
|
|
||||||
TooltipWrap(
|
|
||||||
tooltip = tooltip,
|
|
||||||
positionProvider = positionProvider,
|
|
||||||
haptic = haptic,
|
|
||||||
hapticFeedbackType = hapticFeedbackType,
|
|
||||||
) {
|
|
||||||
HapticSmallFloatingActionButton(
|
|
||||||
onClick = onClick,
|
|
||||||
modifier = modifier,
|
|
||||||
shape = shape,
|
|
||||||
containerColor = containerColor,
|
|
||||||
contentColor = contentColor,
|
|
||||||
elevation = elevation,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
content = content,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [HapticSmallFloatingActionButton] with tooltip-specific params.
|
|
||||||
*
|
|
||||||
* @param tooltip [Int] or `id` string resource to show in a tooltip.
|
|
||||||
* @param positionProvider [PopupPositionProvider] Anchor point for the tooltip.
|
|
||||||
* @param haptic Whether to perform haptic feedback when the tooltip shown.
|
|
||||||
* @param hapticFeedbackType The type of haptic feedback to perform.
|
|
||||||
*
|
|
||||||
* @see [HapticSmallFloatingActionButton]
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun TooltipSmallFloatingActionButton(
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
shape: Shape = FloatingActionButtonDefaults.smallShape,
|
|
||||||
containerColor: Color = FloatingActionButtonDefaults.containerColor,
|
|
||||||
contentColor: Color = contentColorFor(containerColor),
|
|
||||||
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
|
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
|
||||||
@StringRes tooltip: Int,
|
|
||||||
positionProvider: PopupPositionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
|
||||||
haptic: Boolean = true,
|
|
||||||
hapticFeedbackType: HapticFeedbackType = HapticFeedbackType.LongPress,
|
|
||||||
content: @Composable (() -> Unit)
|
|
||||||
) {
|
|
||||||
TooltipWrap(
|
|
||||||
tooltip = tooltip,
|
|
||||||
positionProvider = positionProvider,
|
|
||||||
haptic = haptic,
|
|
||||||
hapticFeedbackType = hapticFeedbackType,
|
|
||||||
) {
|
|
||||||
HapticSmallFloatingActionButton(
|
|
||||||
onClick = onClick,
|
|
||||||
modifier = modifier,
|
|
||||||
shape = shape,
|
|
||||||
containerColor = containerColor,
|
|
||||||
contentColor = contentColor,
|
|
||||||
elevation = elevation,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
content = content,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
package app.revanced.manager.ui.component.tooltip
|
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.PlainTooltip
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TooltipBox
|
|
||||||
import androidx.compose.material3.TooltipDefaults
|
|
||||||
import androidx.compose.material3.rememberTooltipState
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.window.PopupPositionProvider
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps a composable with a tooltip.
|
|
||||||
*
|
|
||||||
* @param modifier the [Modifier] to applied to Tooltip.
|
|
||||||
* @param tooltip [String] text to show in a tooltip.
|
|
||||||
* @param positionProvider [PopupPositionProvider] Anchor point for the tooltip.
|
|
||||||
* @param content The composable UI to wrapped with.
|
|
||||||
* @param haptic Whether to perform haptic feedback when the tooltip shown.
|
|
||||||
* @param hapticFeedbackType The type of haptic feedback to perform.
|
|
||||||
*
|
|
||||||
* @see [TooltipBox]
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
fun TooltipWrap(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
tooltip: String,
|
|
||||||
positionProvider: PopupPositionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
|
||||||
haptic: Boolean = true,
|
|
||||||
hapticFeedbackType: HapticFeedbackType = HapticFeedbackType.LongPress,
|
|
||||||
content: @Composable () -> Unit
|
|
||||||
) {
|
|
||||||
val tooltipState = rememberTooltipState()
|
|
||||||
val localHaptic = LocalHapticFeedback.current
|
|
||||||
|
|
||||||
LaunchedEffect(tooltipState.isVisible) {
|
|
||||||
if (tooltipState.isVisible && haptic) {
|
|
||||||
localHaptic.performHapticFeedback(hapticFeedbackType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TooltipBox(
|
|
||||||
modifier = modifier,
|
|
||||||
positionProvider = positionProvider,
|
|
||||||
tooltip = { PlainTooltip { Text(tooltip) } },
|
|
||||||
state = tooltipState,
|
|
||||||
content = content,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps a composable with a tooltip.
|
|
||||||
*
|
|
||||||
* @param modifier the [Modifier] to applied to tooltip.
|
|
||||||
* @param tooltip [Int] or `id` string resource to show in a tooltip.
|
|
||||||
* @param positionProvider [PopupPositionProvider] Anchor point for the tooltip.
|
|
||||||
* @param content The composable UI to wrapped with.
|
|
||||||
* @param haptic Whether to perform haptic feedback when the tooltip shown.
|
|
||||||
* @param hapticFeedbackType The type of haptic feedback to perform.
|
|
||||||
*
|
|
||||||
* @see [TooltipBox]
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
fun TooltipWrap(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
@StringRes tooltip: Int,
|
|
||||||
positionProvider: PopupPositionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
|
||||||
haptic: Boolean = true,
|
|
||||||
hapticFeedbackType: HapticFeedbackType = HapticFeedbackType.LongPress,
|
|
||||||
content: @Composable () -> Unit
|
|
||||||
) {
|
|
||||||
val tooltipState = rememberTooltipState()
|
|
||||||
val localHaptic = LocalHapticFeedback.current
|
|
||||||
|
|
||||||
LaunchedEffect(tooltipState.isVisible) {
|
|
||||||
if (tooltipState.isVisible && haptic) {
|
|
||||||
localHaptic.performHapticFeedback(hapticFeedbackType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TooltipBox(
|
|
||||||
modifier = modifier,
|
|
||||||
positionProvider = positionProvider,
|
|
||||||
tooltip = { PlainTooltip { Text(stringResource(tooltip)) } },
|
|
||||||
state = tooltipState,
|
|
||||||
content = content,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -3,9 +3,10 @@ package app.revanced.manager.ui.model
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.patcher.StepId
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
enum class StepCategory(@StringRes val displayName: Int) {
|
enum class StepCategory(@param:StringRes val displayName: Int) {
|
||||||
PREPARING(R.string.patcher_step_group_preparing),
|
PREPARING(R.string.patcher_step_group_preparing),
|
||||||
PATCHING(R.string.patcher_step_group_patching),
|
PATCHING(R.string.patcher_step_group_patching),
|
||||||
SAVING(R.string.patcher_step_group_saving)
|
SAVING(R.string.patcher_step_group_saving)
|
||||||
@@ -15,19 +16,20 @@ enum class State {
|
|||||||
WAITING, RUNNING, FAILED, COMPLETED
|
WAITING, RUNNING, FAILED, COMPLETED
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class ProgressKey {
|
|
||||||
DOWNLOAD
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StepProgressProvider {
|
|
||||||
val downloadProgress: Pair<Long, Long?>?
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Step(
|
data class Step(
|
||||||
val name: String,
|
val id: StepId,
|
||||||
|
val title: String,
|
||||||
val category: StepCategory,
|
val category: StepCategory,
|
||||||
val state: State = State.WAITING,
|
val state: State = State.WAITING,
|
||||||
val message: String? = null,
|
val message: String? = null,
|
||||||
val progressKey: ProgressKey? = null
|
val progress: Pair<Long, Long?>? = null,
|
||||||
) : Parcelable
|
val hide: Boolean = false,
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
|
||||||
|
fun Step.withState(
|
||||||
|
state: State = this.state,
|
||||||
|
message: String? = this.message,
|
||||||
|
progress: Pair<Long, Long?>? = this.progress
|
||||||
|
) = copy(state = state, message = message, progress = progress)
|
||||||
@@ -16,6 +16,7 @@ import androidx.compose.material.icons.outlined.Search
|
|||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
@@ -43,7 +44,6 @@ import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
|||||||
import app.revanced.manager.ui.component.LoadingIndicator
|
import app.revanced.manager.ui.component.LoadingIndicator
|
||||||
import app.revanced.manager.ui.component.NonSuggestedVersionDialog
|
import app.revanced.manager.ui.component.NonSuggestedVersionDialog
|
||||||
import app.revanced.manager.ui.component.SearchView
|
import app.revanced.manager.ui.component.SearchView
|
||||||
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
|
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
import app.revanced.manager.ui.viewmodel.AppSelectorViewModel
|
import app.revanced.manager.ui.viewmodel.AppSelectorViewModel
|
||||||
import app.revanced.manager.util.APK_MIMETYPE
|
import app.revanced.manager.util.APK_MIMETYPE
|
||||||
@@ -162,14 +162,8 @@ fun AppSelectorScreen(
|
|||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick,
|
||||||
actions = {
|
actions = {
|
||||||
TooltipIconButton(
|
IconButton(onClick = { search = true }) {
|
||||||
tooltip = stringResource(R.string.search_patches),
|
Icon(Icons.Outlined.Search, stringResource(R.string.search))
|
||||||
onClick = { search = true }
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Outlined.Search,
|
|
||||||
stringResource(R.string.search)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -240,7 +234,13 @@ fun AppSelectorScreen(
|
|||||||
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
item { LoadingIndicator() }
|
item {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillParentMaxSize(), contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
LoadingIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.TabRow
|
import androidx.compose.material3.SecondaryTabRow
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
@@ -53,6 +53,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalResources
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
@@ -66,9 +67,8 @@ import app.revanced.manager.ui.component.NotificationCard
|
|||||||
import app.revanced.manager.ui.component.ConfirmDialog
|
import app.revanced.manager.ui.component.ConfirmDialog
|
||||||
import app.revanced.manager.ui.component.bundle.BundleTopBar
|
import app.revanced.manager.ui.component.bundle.BundleTopBar
|
||||||
import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog
|
import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog
|
||||||
|
import app.revanced.manager.ui.component.haptics.HapticFloatingActionButton
|
||||||
import app.revanced.manager.ui.component.haptics.HapticTab
|
import app.revanced.manager.ui.component.haptics.HapticTab
|
||||||
import app.revanced.manager.ui.component.tooltip.TooltipFloatingActionButton
|
|
||||||
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
|
|
||||||
import app.revanced.manager.ui.viewmodel.DashboardViewModel
|
import app.revanced.manager.ui.viewmodel.DashboardViewModel
|
||||||
import app.revanced.manager.util.RequestInstallAppsContract
|
import app.revanced.manager.util.RequestInstallAppsContract
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
@@ -101,6 +101,7 @@ fun DashboardScreen(
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
val androidContext = LocalContext.current
|
val androidContext = LocalContext.current
|
||||||
|
val resources = LocalResources.current
|
||||||
val composableScope = rememberCoroutineScope()
|
val composableScope = rememberCoroutineScope()
|
||||||
val pagerState = rememberPagerState(
|
val pagerState = rememberPagerState(
|
||||||
initialPage = DashboardPage.DASHBOARD.ordinal,
|
initialPage = DashboardPage.DASHBOARD.ordinal,
|
||||||
@@ -182,20 +183,18 @@ fun DashboardScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
TooltipIconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
showDeleteConfirmationDialog = true
|
showDeleteConfirmationDialog = true
|
||||||
},
|
}
|
||||||
tooltip = stringResource(R.string.delete),
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.DeleteOutline,
|
Icons.Outlined.DeleteOutline,
|
||||||
stringResource(R.string.delete)
|
stringResource(R.string.delete)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
TooltipIconButton(
|
IconButton(
|
||||||
onClick = vm::updateSources,
|
onClick = vm::updateSources
|
||||||
tooltip = stringResource(R.string.refresh)
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.Refresh,
|
Icons.Outlined.Refresh,
|
||||||
@@ -209,9 +208,8 @@ fun DashboardScreen(
|
|||||||
title = stringResource(R.string.app_name),
|
title = stringResource(R.string.app_name),
|
||||||
actions = {
|
actions = {
|
||||||
if (!vm.updatedManagerVersion.isNullOrEmpty()) {
|
if (!vm.updatedManagerVersion.isNullOrEmpty()) {
|
||||||
TooltipIconButton(
|
IconButton(
|
||||||
onClick = onUpdateClick,
|
onClick = onUpdateClick,
|
||||||
tooltip = stringResource(R.string.update),
|
|
||||||
) {
|
) {
|
||||||
BadgedBox(
|
BadgedBox(
|
||||||
badge = {
|
badge = {
|
||||||
@@ -222,17 +220,8 @@ fun DashboardScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TooltipIconButton(
|
IconButton(onClick = onSettingsClick) {
|
||||||
onClick = onSettingsClick,
|
Icon(Icons.Outlined.Settings, stringResource(R.string.settings))
|
||||||
tooltip = stringResource(R.string.settings),
|
|
||||||
) {
|
|
||||||
BadgedBox(
|
|
||||||
badge = {
|
|
||||||
Badge(modifier = Modifier.size(6.dp))
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Icon(Icons.Outlined.Settings, stringResource(R.string.settings))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
applyContainerColor = true
|
applyContainerColor = true
|
||||||
@@ -240,25 +229,24 @@ fun DashboardScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
TooltipFloatingActionButton(
|
HapticFloatingActionButton(
|
||||||
tooltip = stringResource(R.string.add),
|
|
||||||
onClick = {
|
onClick = {
|
||||||
vm.cancelSourceSelection()
|
vm.cancelSourceSelection()
|
||||||
|
|
||||||
when (pagerState.currentPage) {
|
when (pagerState.currentPage) {
|
||||||
DashboardPage.DASHBOARD.ordinal -> {
|
DashboardPage.DASHBOARD.ordinal -> {
|
||||||
if (availablePatches < 1) {
|
if (availablePatches < 1) {
|
||||||
androidContext.toast(androidContext.getString(R.string.no_patch_found))
|
androidContext.toast(resources.getString(R.string.no_patch_found))
|
||||||
composableScope.launch {
|
composableScope.launch {
|
||||||
pagerState.animateScrollToPage(
|
pagerState.animateScrollToPage(
|
||||||
DashboardPage.BUNDLES.ordinal
|
DashboardPage.BUNDLES.ordinal
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return@TooltipFloatingActionButton
|
return@HapticFloatingActionButton
|
||||||
}
|
}
|
||||||
if (vm.android11BugActive) {
|
if (vm.android11BugActive) {
|
||||||
showAndroid11Dialog = true
|
showAndroid11Dialog = true
|
||||||
return@TooltipFloatingActionButton
|
return@HapticFloatingActionButton
|
||||||
}
|
}
|
||||||
|
|
||||||
onAppSelectorClick()
|
onAppSelectorClick()
|
||||||
@@ -273,7 +261,7 @@ fun DashboardScreen(
|
|||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(Modifier.padding(paddingValues)) {
|
Column(Modifier.padding(paddingValues)) {
|
||||||
TabRow(
|
SecondaryTabRow(
|
||||||
selectedTabIndex = pagerState.currentPage,
|
selectedTabIndex = pagerState.currentPage,
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import androidx.compose.material3.AlertDialog
|
|||||||
import androidx.compose.material3.BottomAppBar
|
import androidx.compose.material3.BottomAppBar
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
@@ -39,6 +40,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalResources
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
@@ -50,11 +52,11 @@ import app.revanced.manager.ui.component.InstallerStatusDialog
|
|||||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||||
import app.revanced.manager.ui.component.patcher.InstallPickerDialog
|
import app.revanced.manager.ui.component.patcher.InstallPickerDialog
|
||||||
import app.revanced.manager.ui.component.patcher.Steps
|
import app.revanced.manager.ui.component.patcher.Steps
|
||||||
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
|
|
||||||
import app.revanced.manager.ui.model.StepCategory
|
import app.revanced.manager.ui.model.StepCategory
|
||||||
import app.revanced.manager.ui.viewmodel.PatcherViewModel
|
import app.revanced.manager.ui.viewmodel.PatcherViewModel
|
||||||
import app.revanced.manager.util.APK_MIMETYPE
|
import app.revanced.manager.util.APK_MIMETYPE
|
||||||
import app.revanced.manager.util.EventEffect
|
import app.revanced.manager.util.EventEffect
|
||||||
|
import app.revanced.manager.util.toast
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -68,6 +70,7 @@ fun PatcherScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val resources = LocalResources.current
|
||||||
val exportApkLauncher =
|
val exportApkLauncher =
|
||||||
rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), viewModel::export)
|
rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), viewModel::export)
|
||||||
|
|
||||||
@@ -76,18 +79,17 @@ fun PatcherScreen(
|
|||||||
var showInstallPicker by rememberSaveable { mutableStateOf(false) }
|
var showInstallPicker by rememberSaveable { mutableStateOf(false) }
|
||||||
var showDismissConfirmationDialog by rememberSaveable { mutableStateOf(false) }
|
var showDismissConfirmationDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
fun onPageBack() {
|
fun onPageBack() = when {
|
||||||
if(patcherSucceeded == null)
|
patcherSucceeded == null -> showDismissConfirmationDialog = true
|
||||||
showDismissConfirmationDialog = true
|
viewModel.isInstalling -> context.toast(resources.getString(R.string.patcher_install_in_progress))
|
||||||
else
|
else -> onLeave()
|
||||||
onLeave()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BackHandler(onBack = ::onPageBack)
|
BackHandler(onBack = ::onPageBack)
|
||||||
|
|
||||||
val steps by remember {
|
val steps by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
viewModel.steps.groupBy { it.category }
|
viewModel.steps.groupBy { it.category }.toList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,17 +166,15 @@ fun PatcherScreen(
|
|||||||
bottomBar = {
|
bottomBar = {
|
||||||
BottomAppBar(
|
BottomAppBar(
|
||||||
actions = {
|
actions = {
|
||||||
TooltipIconButton(
|
IconButton(
|
||||||
onClick = { exportApkLauncher.launch("${viewModel.packageName}_${viewModel.version}_revanced_patched.apk") },
|
onClick = { exportApkLauncher.launch("${viewModel.packageName}_${viewModel.version}_revanced_patched.apk") },
|
||||||
enabled = patcherSucceeded == true,
|
enabled = patcherSucceeded == true
|
||||||
tooltip = stringResource(R.string.save_apk),
|
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk))
|
Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk))
|
||||||
}
|
}
|
||||||
TooltipIconButton(
|
IconButton(
|
||||||
onClick = { viewModel.exportLogs(context) },
|
onClick = { viewModel.exportLogs(context) },
|
||||||
enabled = patcherSucceeded != null,
|
enabled = patcherSucceeded != null
|
||||||
tooltip = stringResource(R.string.save_logs),
|
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Outlined.PostAdd, stringResource(id = R.string.save_logs))
|
Icon(Icons.Outlined.PostAdd, stringResource(id = R.string.save_logs))
|
||||||
}
|
}
|
||||||
@@ -215,6 +215,12 @@ fun PatcherScreen(
|
|||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
|
var expandedCategory by rememberSaveable { mutableStateOf<StepCategory?>(null) }
|
||||||
|
|
||||||
|
val expandCategory: (StepCategory?) -> Unit = { category ->
|
||||||
|
expandedCategory = category
|
||||||
|
}
|
||||||
|
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
progress = { viewModel.progress },
|
progress = { viewModel.progress },
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
@@ -226,14 +232,17 @@ fun PatcherScreen(
|
|||||||
contentPadding = PaddingValues(16.dp)
|
contentPadding = PaddingValues(16.dp)
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
items = steps.toList(),
|
items = steps,
|
||||||
key = { it.first }
|
key = { it.first }
|
||||||
) { (category, steps) ->
|
) { (category, steps) ->
|
||||||
Steps(
|
Steps(
|
||||||
category = category,
|
category = category,
|
||||||
steps = steps,
|
steps = steps,
|
||||||
stepCount = if (category == StepCategory.PATCHING) viewModel.patchesProgress else null,
|
isExpanded = expandedCategory == category,
|
||||||
stepProgressProvider = viewModel
|
onExpand = { expandCategory(category) },
|
||||||
|
onClick = {
|
||||||
|
expandCategory(if (expandedCategory == category) null else category)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ import androidx.compose.material3.ListItem
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.ScrollableTabRow
|
import androidx.compose.material3.SecondaryScrollableTabRow
|
||||||
|
import androidx.compose.material3.SmallFloatingActionButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
@@ -48,10 +49,12 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
@@ -72,9 +75,6 @@ import app.revanced.manager.ui.component.haptics.HapticCheckbox
|
|||||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||||
import app.revanced.manager.ui.component.haptics.HapticTab
|
import app.revanced.manager.ui.component.haptics.HapticTab
|
||||||
import app.revanced.manager.ui.component.patches.OptionItem
|
import app.revanced.manager.ui.component.patches.OptionItem
|
||||||
import app.revanced.manager.ui.component.tooltip.TooltipFloatingActionButton
|
|
||||||
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
|
|
||||||
import app.revanced.manager.ui.component.tooltip.TooltipSmallFloatingActionButton
|
|
||||||
import app.revanced.manager.ui.component.patches.SelectionWarningDialog
|
import app.revanced.manager.ui.component.patches.SelectionWarningDialog
|
||||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
||||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_INCOMPATIBLE
|
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_INCOMPATIBLE
|
||||||
@@ -83,9 +83,11 @@ import app.revanced.manager.util.Options
|
|||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
import app.revanced.manager.util.isScrollingUp
|
import app.revanced.manager.util.isScrollingUp
|
||||||
import app.revanced.manager.util.transparentListItemColors
|
import app.revanced.manager.util.transparentListItemColors
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.flow.sample
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class, FlowPreview::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PatchesSelectorScreen(
|
fun PatchesSelectorScreen(
|
||||||
onSave: (PatchSelection?, Options) -> Unit,
|
onSave: (PatchSelection?, Options) -> Unit,
|
||||||
@@ -233,7 +235,8 @@ fun PatchesSelectorScreen(
|
|||||||
viewModel.selectionWarningEnabled -> showSelectionWarning = true
|
viewModel.selectionWarningEnabled -> showSelectionWarning = true
|
||||||
|
|
||||||
// Show universal warning if universal patch is selected and the toggle is off
|
// Show universal warning if universal patch is selected and the toggle is off
|
||||||
patch.compatiblePackages == null && viewModel.universalPatchWarningEnabled -> showUniversalWarning = true
|
patch.compatiblePackages == null && viewModel.universalPatchWarningEnabled -> showUniversalWarning =
|
||||||
|
true
|
||||||
|
|
||||||
// Toggle the patch otherwise
|
// Toggle the patch otherwise
|
||||||
else -> viewModel.togglePatch(uid, patch)
|
else -> viewModel.togglePatch(uid, patch)
|
||||||
@@ -261,15 +264,14 @@ fun PatchesSelectorScreen(
|
|||||||
animationSpec = tween(durationMillis = 400, easing = EaseInOut),
|
animationSpec = tween(durationMillis = 400, easing = EaseInOut),
|
||||||
label = "SearchBar back button"
|
label = "SearchBar back button"
|
||||||
)
|
)
|
||||||
TooltipIconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (searchExpanded) {
|
if (searchExpanded) {
|
||||||
setSearchExpanded(false)
|
setSearchExpanded(false)
|
||||||
} else {
|
} else {
|
||||||
onBackClick()
|
onBackClick()
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
tooltip = stringResource(R.string.back),
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.rotate(rotation),
|
modifier = Modifier.rotate(rotation),
|
||||||
@@ -285,10 +287,9 @@ fun PatchesSelectorScreen(
|
|||||||
transitionSpec = { fadeIn() togetherWith fadeOut() }
|
transitionSpec = { fadeIn() togetherWith fadeOut() }
|
||||||
) { searchExpanded ->
|
) { searchExpanded ->
|
||||||
if (searchExpanded) {
|
if (searchExpanded) {
|
||||||
TooltipIconButton(
|
IconButton(
|
||||||
onClick = { setQuery("") },
|
onClick = { setQuery("") },
|
||||||
enabled = query.isNotEmpty(),
|
enabled = query.isNotEmpty()
|
||||||
tooltip = stringResource(R.string.clear),
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Close,
|
imageVector = Icons.Filled.Close,
|
||||||
@@ -296,10 +297,7 @@ fun PatchesSelectorScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
TooltipIconButton(
|
IconButton(onClick = { showBottomSheet = true }) {
|
||||||
onClick = { showBottomSheet = true },
|
|
||||||
tooltip = stringResource(R.string.more),
|
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.FilterList,
|
imageVector = Icons.Outlined.FilterList,
|
||||||
contentDescription = stringResource(R.string.more)
|
contentDescription = stringResource(R.string.more)
|
||||||
@@ -361,13 +359,27 @@ fun PatchesSelectorScreen(
|
|||||||
horizontalAlignment = Alignment.End,
|
horizontalAlignment = Alignment.End,
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
TooltipSmallFloatingActionButton(
|
SmallFloatingActionButton(
|
||||||
tooltip = stringResource(R.string.reset),
|
|
||||||
onClick = viewModel::reset,
|
onClick = viewModel::reset,
|
||||||
containerColor = MaterialTheme.colorScheme.tertiaryContainer
|
containerColor = MaterialTheme.colorScheme.tertiaryContainer
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
|
Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isScrollingUp =
|
||||||
|
patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp()
|
||||||
|
val expanded by produceState(true, isScrollingUp) {
|
||||||
|
val state = isScrollingUp ?: return@produceState
|
||||||
|
value = state.value
|
||||||
|
|
||||||
|
// Use snapshotFlow and sample to prevent the value from changing too often.
|
||||||
|
snapshotFlow { state.value }
|
||||||
|
.sample(333L)
|
||||||
|
.collect {
|
||||||
|
value = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
HapticExtendedFloatingActionButton(
|
HapticExtendedFloatingActionButton(
|
||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
@@ -383,8 +395,7 @@ fun PatchesSelectorScreen(
|
|||||||
contentDescription = stringResource(R.string.save)
|
contentDescription = stringResource(R.string.save)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp
|
expanded = expanded,
|
||||||
?: true,
|
|
||||||
onClick = {
|
onClick = {
|
||||||
onSave(viewModel.getCustomSelection(), viewModel.getOptions())
|
onSave(viewModel.getCustomSelection(), viewModel.getOptions())
|
||||||
}
|
}
|
||||||
@@ -400,7 +411,7 @@ fun PatchesSelectorScreen(
|
|||||||
.padding(top = 16.dp)
|
.padding(top = 16.dp)
|
||||||
) {
|
) {
|
||||||
if (bundles.size > 1) {
|
if (bundles.size > 1) {
|
||||||
ScrollableTabRow(
|
SecondaryScrollableTabRow(
|
||||||
selectedTabIndex = pagerState.currentPage,
|
selectedTabIndex = pagerState.currentPage,
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
||||||
) {
|
) {
|
||||||
@@ -514,7 +525,6 @@ private fun PatchItem(
|
|||||||
supportingContent = patch.description?.let { { Text(it) } },
|
supportingContent = patch.description?.let { { Text(it) } },
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
if (patch.options?.isNotEmpty() == true) {
|
if (patch.options?.isNotEmpty() == true) {
|
||||||
// TODO: Determine if this button should be [TooltipWrap]
|
|
||||||
IconButton(onClick = onOptionsDialog, enabled = compatible) {
|
IconButton(onClick = onOptionsDialog, enabled = compatible) {
|
||||||
Icon(Icons.Outlined.Settings, null)
|
Icon(Icons.Outlined.Settings, null)
|
||||||
}
|
}
|
||||||
@@ -538,10 +548,7 @@ fun ListHeader(
|
|||||||
},
|
},
|
||||||
trailingContent = onHelpClick?.let {
|
trailingContent = onHelpClick?.let {
|
||||||
{
|
{
|
||||||
TooltipIconButton(
|
IconButton(onClick = it) {
|
||||||
tooltip = stringResource(R.string.help),
|
|
||||||
onClick = it
|
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
Icons.AutoMirrored.Outlined.HelpOutline,
|
Icons.AutoMirrored.Outlined.HelpOutline,
|
||||||
stringResource(R.string.help)
|
stringResource(R.string.help)
|
||||||
@@ -621,10 +628,7 @@ private fun OptionsDialog(
|
|||||||
title = patch.name,
|
title = patch.name,
|
||||||
onBackClick = onDismissRequest,
|
onBackClick = onDismissRequest,
|
||||||
actions = {
|
actions = {
|
||||||
TooltipIconButton(
|
IconButton(onClick = reset) {
|
||||||
tooltip = stringResource(R.string.reset),
|
|
||||||
onClick = reset
|
|
||||||
) {
|
|
||||||
Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
|
Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.ScrollableTabRow
|
import androidx.compose.material3.SecondaryScrollableTabRow
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
@@ -106,7 +106,7 @@ fun RequiredOptionsScreen(
|
|||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
) {
|
) {
|
||||||
if (list.isEmpty()) return@Column
|
if (list.isEmpty()) return@Column
|
||||||
else if (list.size > 1) ScrollableTabRow(
|
else if (list.size > 1) SecondaryScrollableTabRow(
|
||||||
selectedTabIndex = pagerState.currentPage,
|
selectedTabIndex = pagerState.currentPage,
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -25,12 +25,14 @@ import androidx.compose.material3.TextButton
|
|||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalResources
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
@@ -67,6 +69,7 @@ fun SelectedAppInfoScreen(
|
|||||||
vm: SelectedAppInfoViewModel
|
vm: SelectedAppInfoViewModel
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val resources = LocalResources.current
|
||||||
val networkInfo = koinInject<NetworkInfo>()
|
val networkInfo = koinInject<NetworkInfo>()
|
||||||
val networkConnected = remember { networkInfo.isConnected() }
|
val networkConnected = remember { networkInfo.isConnected() }
|
||||||
val networkMetered = remember { !networkInfo.isUnmetered() }
|
val networkMetered = remember { !networkInfo.isUnmetered() }
|
||||||
@@ -76,12 +79,12 @@ fun SelectedAppInfoScreen(
|
|||||||
val bundles by vm.bundleInfoFlow.collectAsStateWithLifecycle(emptyList())
|
val bundles by vm.bundleInfoFlow.collectAsStateWithLifecycle(emptyList())
|
||||||
|
|
||||||
val allowIncompatiblePatches by vm.prefs.disablePatchVersionCompatCheck.getAsState()
|
val allowIncompatiblePatches by vm.prefs.disablePatchVersionCompatCheck.getAsState()
|
||||||
val patches = remember(bundles, allowIncompatiblePatches) {
|
val patches by remember {
|
||||||
vm.getPatches(bundles, allowIncompatiblePatches)
|
derivedStateOf {
|
||||||
}
|
vm.getPatches(bundles, allowIncompatiblePatches)
|
||||||
val selectedPatchCount = remember(patches) {
|
}
|
||||||
patches.values.sumOf { it.size }
|
|
||||||
}
|
}
|
||||||
|
val selectedPatchCount = patches.values.sumOf { it.size }
|
||||||
|
|
||||||
val launcher = rememberLauncherForActivityResult(
|
val launcher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.StartActivityForResult(),
|
contract = ActivityResultContracts.StartActivityForResult(),
|
||||||
@@ -117,7 +120,7 @@ fun SelectedAppInfoScreen(
|
|||||||
},
|
},
|
||||||
onClick = patchClick@{
|
onClick = patchClick@{
|
||||||
if (selectedPatchCount == 0) {
|
if (selectedPatchCount == 0) {
|
||||||
context.toast(context.getString(R.string.no_patches_selected))
|
context.toast(resources.getString(R.string.no_patches_selected))
|
||||||
|
|
||||||
return@patchClick
|
return@patchClick
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ import app.revanced.manager.ui.model.navigation.Settings
|
|||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
|
|
||||||
private data class Section(
|
private data class Section(
|
||||||
@StringRes val name: Int,
|
@param:StringRes val name: Int,
|
||||||
@StringRes val description: Int,
|
@param:StringRes val description: Int,
|
||||||
val image: ImageVector,
|
val image: ImageVector,
|
||||||
val destination: Settings.Destination,
|
val destination: Settings.Destination,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.revanced.manager.ui.screen.settings
|
package app.revanced.manager.ui.screen.settings
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
@@ -19,6 +20,7 @@ import androidx.compose.material.icons.outlined.MailOutline
|
|||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FilledTonalButton
|
import androidx.compose.material3.FilledTonalButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedCard
|
import androidx.compose.material3.OutlinedCard
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
@@ -39,6 +41,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalResources
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.hideFromAccessibility
|
import androidx.compose.ui.semantics.hideFromAccessibility
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
@@ -49,7 +52,6 @@ import app.revanced.manager.network.dto.ReVancedSocial
|
|||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||||
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
|
|
||||||
import app.revanced.manager.ui.model.navigation.Settings
|
import app.revanced.manager.ui.model.navigation.Settings
|
||||||
import app.revanced.manager.ui.viewmodel.AboutViewModel
|
import app.revanced.manager.ui.viewmodel.AboutViewModel
|
||||||
import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.DEVELOPER_OPTIONS_TAPS
|
import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.DEVELOPER_OPTIONS_TAPS
|
||||||
@@ -67,8 +69,9 @@ fun AboutSettingsScreen(
|
|||||||
viewModel: AboutViewModel = koinViewModel()
|
viewModel: AboutViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val resources = LocalResources.current
|
||||||
// painterResource() is broken on release builds for some reason.
|
// painterResource() is broken on release builds for some reason.
|
||||||
val icon = rememberDrawablePainter(drawable = remember {
|
val icon = rememberDrawablePainter(drawable = remember(resources) {
|
||||||
AppCompatResources.getDrawable(context, R.drawable.ic_logo_ring)
|
AppCompatResources.getDrawable(context, R.drawable.ic_logo_ring)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -76,7 +79,7 @@ fun AboutSettingsScreen(
|
|||||||
viewModel.socials.partition(ReVancedSocial::preferred)
|
viewModel.socials.partition(ReVancedSocial::preferred)
|
||||||
}
|
}
|
||||||
|
|
||||||
val preferredSocialButtons = remember(preferredSocials, viewModel.donate, viewModel.contact) {
|
val preferredSocialButtons = remember(resources, preferredSocials, viewModel.donate, viewModel.contact) {
|
||||||
preferredSocials.map {
|
preferredSocials.map {
|
||||||
Triple(
|
Triple(
|
||||||
getSocialIcon(it.name),
|
getSocialIcon(it.name),
|
||||||
@@ -89,7 +92,7 @@ fun AboutSettingsScreen(
|
|||||||
viewModel.donate?.let {
|
viewModel.donate?.let {
|
||||||
Triple(
|
Triple(
|
||||||
Icons.Outlined.FavoriteBorder,
|
Icons.Outlined.FavoriteBorder,
|
||||||
context.getString(R.string.donate),
|
resources.getString(R.string.donate),
|
||||||
third = {
|
third = {
|
||||||
context.openUrl(it)
|
context.openUrl(it)
|
||||||
}
|
}
|
||||||
@@ -98,7 +101,7 @@ fun AboutSettingsScreen(
|
|||||||
viewModel.contact?.let {
|
viewModel.contact?.let {
|
||||||
Triple(
|
Triple(
|
||||||
Icons.Outlined.MailOutline,
|
Icons.Outlined.MailOutline,
|
||||||
context.getString(R.string.contact),
|
resources.getString(R.string.contact),
|
||||||
third = {
|
third = {
|
||||||
context.openUrl("mailto:$it")
|
context.openUrl("mailto:$it")
|
||||||
}
|
}
|
||||||
@@ -131,7 +134,7 @@ fun AboutSettingsScreen(
|
|||||||
stringResource(R.string.contributors_description),
|
stringResource(R.string.contributors_description),
|
||||||
third = nav@{
|
third = nav@{
|
||||||
if (!viewModel.isConnected) {
|
if (!viewModel.isConnected) {
|
||||||
context.toast(context.getString(R.string.no_network_toast))
|
context.toast(resources.getString(R.string.no_network_toast))
|
||||||
return@nav
|
return@nav
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +156,7 @@ fun AboutSettingsScreen(
|
|||||||
LaunchedEffect(developerTaps) {
|
LaunchedEffect(developerTaps) {
|
||||||
if (developerTaps == 0) return@LaunchedEffect
|
if (developerTaps == 0) return@LaunchedEffect
|
||||||
if (showDeveloperSettings) {
|
if (showDeveloperSettings) {
|
||||||
snackbarHostState.showSnackbar(context.getString(R.string.developer_options_already_enabled))
|
snackbarHostState.showSnackbar(resources.getString(R.string.developer_options_already_enabled))
|
||||||
developerTaps = 0
|
developerTaps = 0
|
||||||
return@LaunchedEffect
|
return@LaunchedEffect
|
||||||
}
|
}
|
||||||
@@ -161,7 +164,7 @@ fun AboutSettingsScreen(
|
|||||||
val remaining = DEVELOPER_OPTIONS_TAPS - developerTaps
|
val remaining = DEVELOPER_OPTIONS_TAPS - developerTaps
|
||||||
if (remaining > 0) {
|
if (remaining > 0) {
|
||||||
snackbarHostState.showSnackbar(
|
snackbarHostState.showSnackbar(
|
||||||
context.getString(
|
resources.getString(
|
||||||
R.string.developer_options_taps,
|
R.string.developer_options_taps,
|
||||||
remaining
|
remaining
|
||||||
),
|
),
|
||||||
@@ -169,7 +172,7 @@ fun AboutSettingsScreen(
|
|||||||
)
|
)
|
||||||
} else if (remaining == 0) {
|
} else if (remaining == 0) {
|
||||||
viewModel.showDeveloperSettings.update(true)
|
viewModel.showDeveloperSettings.update(true)
|
||||||
snackbarHostState.showSnackbar(context.getString(R.string.developer_options_enabled))
|
snackbarHostState.showSnackbar(resources.getString(R.string.developer_options_enabled))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset the counter
|
// Reset the counter
|
||||||
@@ -252,10 +255,9 @@ fun AboutSettingsScreen(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)
|
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)
|
||||||
) {
|
) {
|
||||||
socialButtons.forEach { (icon, text, onClick) ->
|
socialButtons.forEach { (icon, text, onClick) ->
|
||||||
TooltipIconButton(
|
IconButton(
|
||||||
|
onClick = onClick,
|
||||||
modifier = Modifier.padding(end = 8.dp),
|
modifier = Modifier.padding(end = 8.dp),
|
||||||
tooltip = text,
|
|
||||||
onClick = onClick
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
icon,
|
icon,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import androidx.compose.material.icons.outlined.Restore
|
|||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
@@ -37,6 +38,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalResources
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -51,7 +53,6 @@ import app.revanced.manager.ui.component.settings.BooleanItem
|
|||||||
import app.revanced.manager.ui.component.settings.IntegerItem
|
import app.revanced.manager.ui.component.settings.IntegerItem
|
||||||
import app.revanced.manager.ui.component.settings.SafeguardBooleanItem
|
import app.revanced.manager.ui.component.settings.SafeguardBooleanItem
|
||||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||||
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
|
|
||||||
import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel
|
import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
import app.revanced.manager.util.withHapticFeedback
|
import app.revanced.manager.util.withHapticFeedback
|
||||||
@@ -64,9 +65,10 @@ fun AdvancedSettingsScreen(
|
|||||||
viewModel: AdvancedSettingsViewModel = koinViewModel()
|
viewModel: AdvancedSettingsViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val memoryLimit = remember {
|
val resources = LocalResources.current
|
||||||
|
val memoryLimit = remember(resources) {
|
||||||
val activityManager = context.getSystemService<ActivityManager>()!!
|
val activityManager = context.getSystemService<ActivityManager>()!!
|
||||||
context.getString(
|
resources.getString(
|
||||||
R.string.device_memory_limit_format,
|
R.string.device_memory_limit_format,
|
||||||
activityManager.memoryClass,
|
activityManager.memoryClass,
|
||||||
activityManager.largeMemoryClass
|
activityManager.largeMemoryClass
|
||||||
@@ -183,7 +185,7 @@ fun AdvancedSettingsScreen(
|
|||||||
ClipData.newPlainText("Device Information", deviceContent)
|
ClipData.newPlainText("Device Information", deviceContent)
|
||||||
)
|
)
|
||||||
|
|
||||||
context.toast(context.getString(R.string.toast_copied_to_clipboard))
|
context.toast(resources.getString(R.string.toast_copied_to_clipboard))
|
||||||
}.withHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
}.withHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||||
),
|
),
|
||||||
headlineContent = stringResource(R.string.about_device),
|
headlineContent = stringResource(R.string.about_device),
|
||||||
@@ -243,11 +245,7 @@ private fun APIUrlDialog(currentUrl: String, defaultUrl: String, onSubmit: (Stri
|
|||||||
onValueChange = { url = it },
|
onValueChange = { url = it },
|
||||||
label = { Text(stringResource(R.string.api_url)) },
|
label = { Text(stringResource(R.string.api_url)) },
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
TooltipIconButton(
|
IconButton(onClick = { url = defaultUrl }) {
|
||||||
modifier = Modifier,
|
|
||||||
tooltip = stringResource(R.string.api_url_dialog_reset),
|
|
||||||
onClick = { url = defaultUrl }
|
|
||||||
) {
|
|
||||||
Icon(Icons.Outlined.Restore, stringResource(R.string.api_url_dialog_reset))
|
Icon(Icons.Outlined.Restore, stringResource(R.string.api_url_dialog_reset))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package app.revanced.manager.ui.screen.settings
|
package app.revanced.manager.ui.screen.settings
|
||||||
|
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
@@ -34,6 +35,8 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
import androidx.compose.ui.platform.UriHandler
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
@@ -57,6 +60,7 @@ fun ContributorSettingsScreen(
|
|||||||
) {
|
) {
|
||||||
val repositories = viewModel.repositories
|
val repositories = viewModel.repositories
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
val uriHandler = LocalUriHandler.current
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -93,7 +97,8 @@ fun ContributorSettingsScreen(
|
|||||||
) {
|
) {
|
||||||
ContributorsCard(
|
ContributorsCard(
|
||||||
title = it.name,
|
title = it.name,
|
||||||
contributors = it.contributors
|
contributors = it.contributors,
|
||||||
|
uriHandler = uriHandler
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,7 +120,8 @@ fun ContributorsCard(
|
|||||||
title: String,
|
title: String,
|
||||||
contributors: List<ReVancedContributor>,
|
contributors: List<ReVancedContributor>,
|
||||||
itemsPerPage: Int = 12,
|
itemsPerPage: Int = 12,
|
||||||
numberOfRows: Int = 2
|
numberOfRows: Int = 2,
|
||||||
|
uriHandler: UriHandler
|
||||||
) {
|
) {
|
||||||
val itemsPerRow = (itemsPerPage / numberOfRows)
|
val itemsPerRow = (itemsPerPage / numberOfRows)
|
||||||
|
|
||||||
@@ -172,7 +178,11 @@ fun ContributorsCard(
|
|||||||
contributorsByPage[page].forEach {
|
contributorsByPage[page].forEach {
|
||||||
if (itemSize > 100.dp) {
|
if (itemSize > 100.dp) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.width(itemSize - 1.dp), // we delete 1.dp to account for not-so divisible numbers
|
modifier = Modifier
|
||||||
|
.width(itemSize - 1.dp)
|
||||||
|
.clickable {
|
||||||
|
uriHandler.openUri("https://github.com/${it.username}")
|
||||||
|
}, // we delete 1.dp to account for not-so divisible numbers
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
@@ -203,6 +213,9 @@ fun ContributorsCard(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(size = (itemSize - 1.dp).coerceAtMost(50.dp)) // we delete 1.dp to account for not-so divisible numbers
|
.size(size = (itemSize - 1.dp).coerceAtMost(50.dp)) // we delete 1.dp to account for not-so divisible numbers
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
|
.clickable {
|
||||||
|
uriHandler.openUri("https://github.com/${it.username}")
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package app.revanced.manager.ui.screen.settings
|
|||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -9,12 +11,14 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.outlined.Delete
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
import androidx.compose.material.icons.outlined.Search
|
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedCard
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
@@ -29,6 +33,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -37,14 +42,12 @@ import app.revanced.manager.R
|
|||||||
import app.revanced.manager.network.downloader.DownloaderPluginState
|
import app.revanced.manager.network.downloader.DownloaderPluginState
|
||||||
import app.revanced.manager.ui.component.AppLabel
|
import app.revanced.manager.ui.component.AppLabel
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
|
import app.revanced.manager.ui.component.ConfirmDialog
|
||||||
import app.revanced.manager.ui.component.ExceptionViewerDialog
|
import app.revanced.manager.ui.component.ExceptionViewerDialog
|
||||||
import app.revanced.manager.ui.component.GroupHeader
|
import app.revanced.manager.ui.component.GroupHeader
|
||||||
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
||||||
import app.revanced.manager.ui.component.ConfirmDialog
|
|
||||||
import app.revanced.manager.ui.component.tooltip.TooltipWrap
|
|
||||||
import app.revanced.manager.ui.component.haptics.HapticCheckbox
|
import app.revanced.manager.ui.component.haptics.HapticCheckbox
|
||||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||||
import app.revanced.manager.ui.component.tooltip.TooltipIconButton
|
|
||||||
import app.revanced.manager.ui.viewmodel.DownloadsViewModel
|
import app.revanced.manager.ui.viewmodel.DownloadsViewModel
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
@@ -55,6 +58,7 @@ fun DownloadsSettingsScreen(
|
|||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
viewModel: DownloadsViewModel = koinViewModel()
|
viewModel: DownloadsViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList())
|
val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList())
|
||||||
val pluginStates by viewModel.downloaderPluginStates.collectAsStateWithLifecycle()
|
val pluginStates by viewModel.downloaderPluginStates.collectAsStateWithLifecycle()
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
@@ -78,10 +82,7 @@ fun DownloadsSettingsScreen(
|
|||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick,
|
||||||
actions = {
|
actions = {
|
||||||
if (viewModel.appSelection.isNotEmpty()) {
|
if (viewModel.appSelection.isNotEmpty()) {
|
||||||
TooltipIconButton(
|
IconButton(onClick = { viewModel.deleteApps() }) {
|
||||||
tooltip = stringResource(R.string.delete),
|
|
||||||
onClick = { showDeleteConfirmationDialog = true }
|
|
||||||
) {
|
|
||||||
Icon(Icons.Default.Delete, stringResource(R.string.delete))
|
Icon(Icons.Default.Delete, stringResource(R.string.delete))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,6 +128,11 @@ fun DownloadsSettingsScreen(
|
|||||||
.digest(androidSignature.toByteArray())
|
.digest(androidSignature.toByteArray())
|
||||||
hash.toHexString(format = HexFormat.UpperCase)
|
hash.toHexString(format = HexFormat.UpperCase)
|
||||||
}
|
}
|
||||||
|
val appName = remember {
|
||||||
|
packageInfo.applicationInfo?.loadLabel(context.packageManager)
|
||||||
|
?.toString()
|
||||||
|
?: packageName
|
||||||
|
}
|
||||||
|
|
||||||
when (state) {
|
when (state) {
|
||||||
is DownloaderPluginState.Loaded -> TrustDialog(
|
is DownloaderPluginState.Loaded -> TrustDialog(
|
||||||
@@ -136,6 +142,8 @@ fun DownloadsSettingsScreen(
|
|||||||
packageName,
|
packageName,
|
||||||
signature
|
signature
|
||||||
),
|
),
|
||||||
|
pluginName = appName,
|
||||||
|
signature = signature,
|
||||||
onDismiss = ::dismiss,
|
onDismiss = ::dismiss,
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
viewModel.revokePluginTrust(packageName)
|
viewModel.revokePluginTrust(packageName)
|
||||||
@@ -153,10 +161,10 @@ fun DownloadsSettingsScreen(
|
|||||||
is DownloaderPluginState.Untrusted -> TrustDialog(
|
is DownloaderPluginState.Untrusted -> TrustDialog(
|
||||||
title = R.string.downloader_plugin_trust_dialog_title,
|
title = R.string.downloader_plugin_trust_dialog_title,
|
||||||
body = stringResource(
|
body = stringResource(
|
||||||
R.string.downloader_plugin_trust_dialog_body,
|
R.string.downloader_plugin_trust_dialog_body
|
||||||
packageName,
|
|
||||||
signature
|
|
||||||
),
|
),
|
||||||
|
pluginName = appName,
|
||||||
|
signature = signature,
|
||||||
onDismiss = ::dismiss,
|
onDismiss = ::dismiss,
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
viewModel.trustPlugin(packageName)
|
viewModel.trustPlugin(packageName)
|
||||||
@@ -232,6 +240,8 @@ fun DownloadsSettingsScreen(
|
|||||||
private fun TrustDialog(
|
private fun TrustDialog(
|
||||||
@StringRes title: Int,
|
@StringRes title: Int,
|
||||||
body: String,
|
body: String,
|
||||||
|
pluginName: String,
|
||||||
|
signature: String,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onConfirm: () -> Unit
|
onConfirm: () -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -244,10 +254,39 @@ private fun TrustDialog(
|
|||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = onDismiss) {
|
TextButton(onClick = onDismiss) {
|
||||||
Text(stringResource(R.string.dismiss))
|
Text(stringResource(R.string.cancel))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
title = { Text(stringResource(title)) },
|
title = { Text(stringResource(title)) },
|
||||||
text = { Text(body) }
|
text = {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
Text(body)
|
||||||
|
Card {
|
||||||
|
Column(
|
||||||
|
Modifier.padding(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(
|
||||||
|
R.string.downloader_plugin_trust_dialog_plugin,
|
||||||
|
pluginName
|
||||||
|
),
|
||||||
|
)
|
||||||
|
OutlinedCard(
|
||||||
|
colors = CardDefaults.outlinedCardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(
|
||||||
|
R.string.downloader_plugin_trust_dialog_signature,
|
||||||
|
signature.chunked(2).joinToString(" ")
|
||||||
|
), modifier = Modifier.padding(12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
package app.revanced.manager.ui.screen.settings
|
package app.revanced.manager.ui.screen.settings
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FilledTonalButton
|
import androidx.compose.material3.FilledTonalButton
|
||||||
@@ -18,6 +21,7 @@ import androidx.compose.material3.rememberTopAppBarState
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -37,6 +41,7 @@ import app.revanced.manager.ui.theme.Theme
|
|||||||
import app.revanced.manager.ui.viewmodel.GeneralSettingsViewModel
|
import app.revanced.manager.ui.viewmodel.GeneralSettingsViewModel
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -47,6 +52,7 @@ fun GeneralSettingsScreen(
|
|||||||
val prefs = viewModel.prefs
|
val prefs = viewModel.prefs
|
||||||
val coroutineScope = viewModel.viewModelScope
|
val coroutineScope = viewModel.viewModelScope
|
||||||
var showThemePicker by rememberSaveable { mutableStateOf(false) }
|
var showThemePicker by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var showLanguagePicker by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
if (showThemePicker) {
|
if (showThemePicker) {
|
||||||
ThemePicker(
|
ThemePicker(
|
||||||
@@ -54,6 +60,17 @@ fun GeneralSettingsScreen(
|
|||||||
onConfirm = { viewModel.setTheme(it) }
|
onConfirm = { viewModel.setTheme(it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showLanguagePicker) {
|
||||||
|
LanguagePicker(
|
||||||
|
supportedLocales = viewModel.getSupportedLocales(),
|
||||||
|
currentLocale = viewModel.getCurrentLocale(),
|
||||||
|
onDismiss = { showLanguagePicker = false },
|
||||||
|
onConfirm = { viewModel.setLocale(it) },
|
||||||
|
getDisplayName = { viewModel.getLocaleDisplayName(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -73,6 +90,24 @@ fun GeneralSettingsScreen(
|
|||||||
) {
|
) {
|
||||||
GroupHeader(stringResource(R.string.appearance))
|
GroupHeader(stringResource(R.string.appearance))
|
||||||
|
|
||||||
|
val currentLocale = viewModel.getCurrentLocale()
|
||||||
|
val currentLanguageDisplay = remember(currentLocale) {
|
||||||
|
currentLocale?.let { viewModel.getLocaleDisplayName(it) }
|
||||||
|
}
|
||||||
|
SettingsListItem(
|
||||||
|
modifier = Modifier.clickable { showLanguagePicker = true },
|
||||||
|
headlineContent = stringResource(R.string.language),
|
||||||
|
supportingContent = stringResource(R.string.language_description),
|
||||||
|
trailingContent = {
|
||||||
|
FilledTonalButton(onClick = { showLanguagePicker = true }) {
|
||||||
|
Text(
|
||||||
|
currentLanguageDisplay
|
||||||
|
?: stringResource(R.string.language_system_default)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
val theme by prefs.theme.getAsState()
|
val theme by prefs.theme.getAsState()
|
||||||
SettingsListItem(
|
SettingsListItem(
|
||||||
modifier = Modifier.clickable { showThemePicker = true },
|
modifier = Modifier.clickable { showThemePicker = true },
|
||||||
@@ -96,6 +131,22 @@ fun GeneralSettingsScreen(
|
|||||||
description = R.string.dynamic_color_description
|
description = R.string.dynamic_color_description
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
AnimatedVisibility(theme != Theme.LIGHT) {
|
||||||
|
BooleanItem(
|
||||||
|
preference = prefs.pureBlackTheme,
|
||||||
|
coroutineScope = coroutineScope,
|
||||||
|
headline = R.string.pure_black_theme,
|
||||||
|
description = R.string.pure_black_theme_description
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupHeader(stringResource(R.string.networking))
|
||||||
|
BooleanItem(
|
||||||
|
preference = prefs.allowMeteredNetworks,
|
||||||
|
coroutineScope = coroutineScope,
|
||||||
|
headline = R.string.allow_metered_networks,
|
||||||
|
description = R.string.allow_metered_networks_description
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,4 +190,64 @@ private fun ThemePicker(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LanguagePicker(
|
||||||
|
supportedLocales: List<Locale>,
|
||||||
|
currentLocale: Locale?,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onConfirm: (Locale?) -> Unit,
|
||||||
|
getDisplayName: (Locale) -> String
|
||||||
|
) {
|
||||||
|
var selectedLocale by remember { mutableStateOf(currentLocale) }
|
||||||
|
val systemDefaultString = stringResource(R.string.language_system_default)
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text(stringResource(R.string.language)) },
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { selectedLocale = null },
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
HapticRadioButton(
|
||||||
|
selected = selectedLocale == null,
|
||||||
|
onClick = { selectedLocale = null }
|
||||||
|
)
|
||||||
|
Text(systemDefaultString)
|
||||||
|
}
|
||||||
|
|
||||||
|
supportedLocales.forEach { locale ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { selectedLocale = locale },
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
HapticRadioButton(
|
||||||
|
selected = selectedLocale == locale,
|
||||||
|
onClick = { selectedLocale = locale }
|
||||||
|
)
|
||||||
|
Text(getDisplayName(locale))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
onConfirm(selectedLocale)
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.apply))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -36,6 +36,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalResources
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -64,6 +65,7 @@ fun ImportExportSettingsScreen(
|
|||||||
vm: ImportExportViewModel = koinViewModel()
|
vm: ImportExportViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val resources = LocalResources.current
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
var selectorDialog by rememberSaveable { mutableStateOf<(@Composable () -> Unit)?>(null) }
|
var selectorDialog by rememberSaveable { mutableStateOf<(@Composable () -> Unit)?>(null) }
|
||||||
|
|
||||||
@@ -104,11 +106,11 @@ fun ImportExportSettingsScreen(
|
|||||||
if (vm.showCredentialsDialog) {
|
if (vm.showCredentialsDialog) {
|
||||||
KeystoreCredentialsDialog(
|
KeystoreCredentialsDialog(
|
||||||
onDismissRequest = vm::cancelKeystoreImport,
|
onDismissRequest = vm::cancelKeystoreImport,
|
||||||
onSubmit = { cn, pass ->
|
onSubmit = { alias, pass ->
|
||||||
vm.viewModelScope.launch {
|
vm.viewModelScope.launch {
|
||||||
uiSafe(context, R.string.failed_to_import_keystore, "Failed to import keystore") {
|
uiSafe(context, R.string.failed_to_import_keystore, "Failed to import keystore") {
|
||||||
val result = vm.tryKeystoreImport(cn, pass)
|
val result = vm.tryKeystoreImport(alias, pass)
|
||||||
if (!result) context.toast(context.getString(R.string.import_keystore_wrong_credentials))
|
if (!result) context.toast(resources.getString(R.string.import_keystore_wrong_credentials))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,7 +168,7 @@ fun ImportExportSettingsScreen(
|
|||||||
GroupItem(
|
GroupItem(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!vm.canExport()) {
|
if (!vm.canExport()) {
|
||||||
context.toast(context.getString(R.string.export_keystore_unavailable))
|
context.toast(resources.getString(R.string.export_keystore_unavailable))
|
||||||
return@GroupItem
|
return@GroupItem
|
||||||
}
|
}
|
||||||
exportKeystoreLauncher.launch("Manager.keystore")
|
exportKeystoreLauncher.launch("Manager.keystore")
|
||||||
@@ -382,7 +384,7 @@ fun KeystoreCredentialsDialog(
|
|||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onSubmit: (String, String) -> Unit
|
onSubmit: (String, String) -> Unit
|
||||||
) {
|
) {
|
||||||
var cn by rememberSaveable { mutableStateOf("") }
|
var alias by rememberSaveable { mutableStateOf("") }
|
||||||
var pass by rememberSaveable { mutableStateOf("") }
|
var pass by rememberSaveable { mutableStateOf("") }
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
@@ -390,7 +392,7 @@ fun KeystoreCredentialsDialog(
|
|||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
onSubmit(cn, pass)
|
onSubmit(alias, pass)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.import_keystore_dialog_button))
|
Text(stringResource(R.string.import_keystore_dialog_button))
|
||||||
@@ -422,8 +424,8 @@ fun KeystoreCredentialsDialog(
|
|||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = cn,
|
value = alias,
|
||||||
onValueChange = { cn = it },
|
onValueChange = { alias = it },
|
||||||
label = { Text(stringResource(R.string.import_keystore_dialog_alias_field)) }
|
label = { Text(stringResource(R.string.import_keystore_dialog_alias_field)) }
|
||||||
)
|
)
|
||||||
PasswordField(
|
PasswordField(
|
||||||
|
|||||||
@@ -7,15 +7,18 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
|||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.AppScaffold
|
import app.revanced.manager.ui.component.AppScaffold
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
import app.revanced.manager.ui.component.Scrollbar
|
import app.revanced.manager.ui.component.Scrollbar
|
||||||
import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer
|
|
||||||
import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
|
import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
|
||||||
import com.mikepenz.aboutlibraries.ui.compose.libraryColors
|
import com.mikepenz.aboutlibraries.ui.compose.android.produceLibraries
|
||||||
|
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
||||||
|
import com.mikepenz.aboutlibraries.ui.compose.m3.chipColors
|
||||||
|
import com.mikepenz.aboutlibraries.ui.compose.m3.libraryColors
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -33,16 +36,23 @@ fun LicensesSettingsScreen(
|
|||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(modifier = Modifier.padding(paddingValues)) {
|
Column(modifier = Modifier.padding(paddingValues)) {
|
||||||
val lazyListState = rememberLazyListState()
|
val lazyListState = rememberLazyListState()
|
||||||
|
val libraries by produceLibraries(R.raw.aboutlibraries)
|
||||||
|
val chipColors = LibraryDefaults.chipColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
)
|
||||||
|
|
||||||
LibrariesContainer(
|
LibrariesContainer(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
|
libraries = libraries,
|
||||||
lazyListState = lazyListState,
|
lazyListState = lazyListState,
|
||||||
colors = LibraryDefaults.libraryColors(
|
colors = LibraryDefaults.libraryColors(
|
||||||
backgroundColor = MaterialTheme.colorScheme.background,
|
libraryBackgroundColor = MaterialTheme.colorScheme.background,
|
||||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
libraryContentColor = MaterialTheme.colorScheme.onBackground,
|
||||||
badgeBackgroundColor = MaterialTheme.colorScheme.primary,
|
versionChipColors = chipColors,
|
||||||
badgeContentColor = MaterialTheme.colorScheme.onPrimary,
|
licenseChipColors = chipColors,
|
||||||
|
fundingChipColors = chipColors,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Scrollbar(lazyListState = lazyListState, modifier = Modifier.padding(paddingValues))
|
Scrollbar(lazyListState = lazyListState, modifier = Modifier.padding(paddingValues))
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalResources
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
@@ -33,6 +34,7 @@ fun UpdatesSettingsScreen(
|
|||||||
vm: UpdatesSettingsViewModel = koinViewModel(),
|
vm: UpdatesSettingsViewModel = koinViewModel(),
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val resources = LocalResources.current
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
|
||||||
@@ -57,7 +59,7 @@ fun UpdatesSettingsScreen(
|
|||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
if (!vm.isConnected) {
|
if (!vm.isConnected) {
|
||||||
context.toast(context.getString(R.string.no_network_toast))
|
context.toast(resources.getString(R.string.no_network_toast))
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
if (vm.checkForUpdates()) onUpdateClick()
|
if (vm.checkForUpdates()) onUpdateClick()
|
||||||
@@ -70,7 +72,7 @@ fun UpdatesSettingsScreen(
|
|||||||
SettingsListItem(
|
SettingsListItem(
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
if (!vm.isConnected) {
|
if (!vm.isConnected) {
|
||||||
context.toast(context.getString(R.string.no_network_toast))
|
context.toast(resources.getString(R.string.no_network_toast))
|
||||||
return@clickable
|
return@clickable
|
||||||
}
|
}
|
||||||
onChangelogClick()
|
onChangelogClick()
|
||||||
@@ -92,6 +94,12 @@ fun UpdatesSettingsScreen(
|
|||||||
headline = R.string.show_manager_update_dialog_on_launch,
|
headline = R.string.show_manager_update_dialog_on_launch,
|
||||||
description = R.string.show_manager_update_dialog_on_launch_description
|
description = R.string.show_manager_update_dialog_on_launch_description
|
||||||
)
|
)
|
||||||
|
|
||||||
|
BooleanItem(
|
||||||
|
preference = vm.useManagerPrereleases,
|
||||||
|
headline = R.string.manager_prereleases,
|
||||||
|
description = R.string.manager_prereleases_description
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,6 +80,7 @@ private val LightColorScheme = lightColorScheme(
|
|||||||
fun ReVancedManagerTheme(
|
fun ReVancedManagerTheme(
|
||||||
darkTheme: Boolean,
|
darkTheme: Boolean,
|
||||||
dynamicColor: Boolean,
|
dynamicColor: Boolean,
|
||||||
|
pureBlackTheme: Boolean,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val colorScheme = when {
|
val colorScheme = when {
|
||||||
@@ -93,6 +94,10 @@ fun ReVancedManagerTheme(
|
|||||||
|
|
||||||
darkTheme -> DarkColorScheme
|
darkTheme -> DarkColorScheme
|
||||||
else -> LightColorScheme
|
else -> LightColorScheme
|
||||||
|
}.let {
|
||||||
|
if (darkTheme && pureBlackTheme)
|
||||||
|
it.copy(background = Color.Black, surface = Color.Black)
|
||||||
|
else it
|
||||||
}
|
}
|
||||||
|
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class AdvancedSettingsViewModel(
|
|||||||
app.contentResolver.openOutputStream(target)!!.bufferedWriter().use { writer ->
|
app.contentResolver.openOutputStream(target)!!.bufferedWriter().use { writer ->
|
||||||
val consumer = Redirect.Consume { flow ->
|
val consumer = Redirect.Consume { flow ->
|
||||||
flow.onEach {
|
flow.onEach {
|
||||||
writer.write(it)
|
writer.write("${it}\n")
|
||||||
}.flowOn(Dispatchers.IO).collect()
|
}.flowOn(Dispatchers.IO).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ class BundleListViewModel : ViewModel(), KoinComponent {
|
|||||||
patchBundleRepository.update(
|
patchBundleRepository.update(
|
||||||
*getSelectedSources().filterIsInstance<RemotePatchBundle>().toTypedArray(),
|
*getSelectedSources().filterIsInstance<RemotePatchBundle>().toTypedArray(),
|
||||||
showToast = true,
|
showToast = true,
|
||||||
|
force = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,7 +66,7 @@ class BundleListViewModel : ViewModel(), KoinComponent {
|
|||||||
fun update(src: PatchBundleSource) = viewModelScope.launch {
|
fun update(src: PatchBundleSource) = viewModelScope.launch {
|
||||||
if (src !is RemotePatchBundle) return@launch
|
if (src !is RemotePatchBundle) return@launch
|
||||||
|
|
||||||
patchBundleRepository.update(src, showToast = true)
|
patchBundleRepository.update(src, showToast = true, force = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Event {
|
enum class Event {
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
import app.revanced.manager.ui.theme.Theme
|
import app.revanced.manager.ui.theme.Theme
|
||||||
|
import app.revanced.manager.util.SupportedLocales
|
||||||
import app.revanced.manager.util.resetListItemColorsCached
|
import app.revanced.manager.util.resetListItemColorsCached
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class GeneralSettingsViewModel(
|
class GeneralSettingsViewModel(
|
||||||
|
private val app: Application,
|
||||||
val prefs: PreferencesManager
|
val prefs: PreferencesManager
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
fun setTheme(theme: Theme) = viewModelScope.launch {
|
fun setTheme(theme: Theme) = viewModelScope.launch {
|
||||||
prefs.theme.update(theme)
|
prefs.theme.update(theme)
|
||||||
resetListItemColorsCached()
|
resetListItemColorsCached()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getSupportedLocales() = SupportedLocales.getSupportedLocales(app)
|
||||||
|
fun getCurrentLocale() = SupportedLocales.getCurrentLocale()
|
||||||
|
fun setLocale(locale: Locale?) = SupportedLocales.setLocale(locale)
|
||||||
|
fun getLocaleDisplayName(locale: Locale) = SupportedLocales.getDisplayName(locale)
|
||||||
}
|
}
|
||||||
@@ -36,8 +36,8 @@ import kotlin.io.path.deleteExisting
|
|||||||
import kotlin.io.path.inputStream
|
import kotlin.io.path.inputStream
|
||||||
|
|
||||||
sealed class ResetDialogState(
|
sealed class ResetDialogState(
|
||||||
@StringRes val titleResId: Int,
|
@param:StringRes val titleResId: Int,
|
||||||
@StringRes val descriptionResId: Int,
|
@param:StringRes val descriptionResId: Int,
|
||||||
val onConfirm: () -> Unit,
|
val onConfirm: () -> Unit,
|
||||||
val dialogOptionName: String? = null
|
val dialogOptionName: String? = null
|
||||||
) {
|
) {
|
||||||
@@ -154,12 +154,12 @@ class ImportExportViewModel(
|
|||||||
keystoreImportPath = null
|
keystoreImportPath = null
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun tryKeystoreImport(cn: String, pass: String) =
|
suspend fun tryKeystoreImport(alias: String, pass: String) =
|
||||||
tryKeystoreImport(cn, pass, keystoreImportPath!!)
|
tryKeystoreImport(alias, pass, keystoreImportPath!!)
|
||||||
|
|
||||||
private suspend fun tryKeystoreImport(cn: String, pass: String, path: Path): Boolean {
|
private suspend fun tryKeystoreImport(alias: String, pass: String, path: Path): Boolean {
|
||||||
path.inputStream().use { stream ->
|
path.inputStream().use { stream ->
|
||||||
if (keystoreManager.import(cn, pass, stream)) {
|
if (keystoreManager.import(alias, pass, stream)) {
|
||||||
app.toast(app.getString(R.string.import_keystore_success))
|
app.toast(app.getString(R.string.import_keystore_success))
|
||||||
cancelKeystoreImport()
|
cancelKeystoreImport()
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
@@ -19,7 +13,6 @@ import app.revanced.manager.data.room.apps.installed.InstallType
|
|||||||
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
||||||
import app.revanced.manager.domain.installer.RootInstaller
|
import app.revanced.manager.domain.installer.RootInstaller
|
||||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||||
import app.revanced.manager.service.UninstallService
|
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
import app.revanced.manager.util.simpleMessage
|
import app.revanced.manager.util.simpleMessage
|
||||||
@@ -30,6 +23,8 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
import ru.solrudev.ackpine.session.Session
|
||||||
|
import ru.solrudev.ackpine.uninstaller.UninstallFailure
|
||||||
|
|
||||||
class InstalledAppInfoViewModel(
|
class InstalledAppInfoViewModel(
|
||||||
packageName: String
|
packageName: String
|
||||||
@@ -87,51 +82,28 @@ class InstalledAppInfoViewModel(
|
|||||||
|
|
||||||
fun uninstall() {
|
fun uninstall() {
|
||||||
val app = installedApp ?: return
|
val app = installedApp ?: return
|
||||||
when (app.installType) {
|
viewModelScope.launch {
|
||||||
InstallType.DEFAULT -> pm.uninstallPackage(app.currentPackageName)
|
when (app.installType) {
|
||||||
|
InstallType.DEFAULT -> {
|
||||||
InstallType.MOUNT -> viewModelScope.launch {
|
when (val result = pm.uninstallPackage(app.currentPackageName)) {
|
||||||
rootInstaller.uninstall(app.currentPackageName)
|
is Session.State.Failed<UninstallFailure> -> {
|
||||||
installedAppRepository.delete(app)
|
val msg = result.failure.message.orEmpty()
|
||||||
onBackClick()
|
context.toast(
|
||||||
}
|
this@InstalledAppInfoViewModel.context.getString(
|
||||||
}
|
R.string.uninstall_app_fail,
|
||||||
}
|
msg
|
||||||
|
)
|
||||||
private val uninstallBroadcastReceiver = object : BroadcastReceiver() {
|
)
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
return@launch
|
||||||
when (intent?.action) {
|
|
||||||
UninstallService.APP_UNINSTALL_ACTION -> {
|
|
||||||
val extraStatus =
|
|
||||||
intent.getIntExtra(UninstallService.EXTRA_UNINSTALL_STATUS, -999)
|
|
||||||
val extraStatusMessage =
|
|
||||||
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
|
|
||||||
|
|
||||||
if (extraStatus == PackageInstaller.STATUS_SUCCESS) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
installedApp?.let {
|
|
||||||
installedAppRepository.delete(it)
|
|
||||||
}
|
|
||||||
onBackClick()
|
|
||||||
}
|
}
|
||||||
} else if (extraStatus != PackageInstaller.STATUS_FAILURE_ABORTED) {
|
Session.State.Succeeded -> {}
|
||||||
this@InstalledAppInfoViewModel.context.toast(this@InstalledAppInfoViewModel.context.getString(R.string.uninstall_app_fail, extraStatusMessage))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}.also {
|
|
||||||
ContextCompat.registerReceiver(
|
|
||||||
context,
|
|
||||||
it,
|
|
||||||
IntentFilter(UninstallService.APP_UNINSTALL_ACTION),
|
|
||||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared() {
|
InstallType.MOUNT -> rootInstaller.uninstall(app.currentPackageName)
|
||||||
super.onCleared()
|
}
|
||||||
context.unregisterReceiver(uninstallBroadcastReceiver)
|
installedAppRepository.delete(app)
|
||||||
|
onBackClick()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,6 +120,10 @@ class MainViewModel(
|
|||||||
settings.useDynamicTheme?.let { dynamicColor ->
|
settings.useDynamicTheme?.let { dynamicColor ->
|
||||||
prefs.dynamicColor.update(dynamicColor)
|
prefs.dynamicColor.update(dynamicColor)
|
||||||
}
|
}
|
||||||
|
settings.usePrereleases?.let { prereleases ->
|
||||||
|
prefs.useManagerPrereleases.update(prereleases)
|
||||||
|
prefs.usePatchesPrereleases.update(prereleases)
|
||||||
|
}
|
||||||
settings.apiUrl?.let { api ->
|
settings.apiUrl?.let { api ->
|
||||||
prefs.api.update(api.removeSuffix("/"))
|
prefs.api.update(api.removeSuffix("/"))
|
||||||
}
|
}
|
||||||
@@ -143,7 +147,7 @@ class MainViewModel(
|
|||||||
settings.keystore?.let { keystore ->
|
settings.keystore?.let { keystore ->
|
||||||
val keystoreBytes = Base64.decode(keystore, Base64.DEFAULT)
|
val keystoreBytes = Base64.decode(keystore, Base64.DEFAULT)
|
||||||
keystoreManager.import(
|
keystoreManager.import(
|
||||||
"ReVanced",
|
"alias",
|
||||||
settings.keystorePassword,
|
settings.keystorePassword,
|
||||||
keystoreBytes.inputStream()
|
keystoreBytes.inputStream()
|
||||||
)
|
)
|
||||||
@@ -159,6 +163,7 @@ class MainViewModel(
|
|||||||
val keystorePassword: String,
|
val keystorePassword: String,
|
||||||
val themeMode: Int? = null,
|
val themeMode: Int? = null,
|
||||||
val useDynamicTheme: Boolean? = null,
|
val useDynamicTheme: Boolean? = null,
|
||||||
|
val usePrereleases: Boolean? = null,
|
||||||
val apiUrl: String? = null,
|
val apiUrl: String? = null,
|
||||||
val experimentalPatchesEnabled: Boolean? = null,
|
val experimentalPatchesEnabled: Boolean? = null,
|
||||||
val patchesAutoUpdate: Boolean? = null,
|
val patchesAutoUpdate: Boolean? = null,
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.pm.PackageInstaller as AndroidPackageInstaller
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.ParcelUuid
|
import android.os.ParcelUuid
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -16,7 +14,6 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.saveable.autoSaver
|
import androidx.compose.runtime.saveable.autoSaver
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.toMutableStateList
|
import androidx.compose.runtime.toMutableStateList
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.map
|
import androidx.lifecycle.map
|
||||||
@@ -32,32 +29,35 @@ import app.revanced.manager.data.room.apps.installed.InstalledApp
|
|||||||
import app.revanced.manager.domain.installer.RootInstaller
|
import app.revanced.manager.domain.installer.RootInstaller
|
||||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||||
import app.revanced.manager.domain.worker.WorkerRepository
|
import app.revanced.manager.domain.worker.WorkerRepository
|
||||||
|
import app.revanced.manager.patcher.ProgressEvent
|
||||||
|
import app.revanced.manager.patcher.StepId
|
||||||
import app.revanced.manager.patcher.logger.LogLevel
|
import app.revanced.manager.patcher.logger.LogLevel
|
||||||
import app.revanced.manager.patcher.logger.Logger
|
import app.revanced.manager.patcher.logger.Logger
|
||||||
import app.revanced.manager.patcher.worker.PatcherWorker
|
import app.revanced.manager.patcher.worker.PatcherWorker
|
||||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||||
import app.revanced.manager.plugin.downloader.UserInteractionException
|
import app.revanced.manager.plugin.downloader.UserInteractionException
|
||||||
import app.revanced.manager.service.InstallService
|
|
||||||
import app.revanced.manager.service.UninstallService
|
|
||||||
import app.revanced.manager.ui.model.InstallerModel
|
import app.revanced.manager.ui.model.InstallerModel
|
||||||
import app.revanced.manager.ui.model.ProgressKey
|
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
import app.revanced.manager.ui.model.State
|
import app.revanced.manager.ui.model.State
|
||||||
import app.revanced.manager.ui.model.Step
|
|
||||||
import app.revanced.manager.ui.model.StepCategory
|
import app.revanced.manager.ui.model.StepCategory
|
||||||
import app.revanced.manager.ui.model.StepProgressProvider
|
import app.revanced.manager.ui.model.Step
|
||||||
import app.revanced.manager.ui.model.navigation.Patcher
|
import app.revanced.manager.ui.model.navigation.Patcher
|
||||||
|
import app.revanced.manager.ui.model.withState
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
|
import app.revanced.manager.util.PatchSelection
|
||||||
|
import app.revanced.manager.util.asCode
|
||||||
import app.revanced.manager.util.saveableVar
|
import app.revanced.manager.util.saveableVar
|
||||||
import app.revanced.manager.util.saver.snapshotStateListSaver
|
import app.revanced.manager.util.saver.snapshotStateListSaver
|
||||||
import app.revanced.manager.util.simpleMessage
|
|
||||||
import app.revanced.manager.util.tag
|
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
import app.revanced.manager.util.uiSafe
|
import app.revanced.manager.util.uiSafe
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.NonCancellable
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -66,6 +66,15 @@ import kotlinx.coroutines.withContext
|
|||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
import ru.solrudev.ackpine.installer.InstallFailure
|
||||||
|
import ru.solrudev.ackpine.installer.PackageInstaller
|
||||||
|
import ru.solrudev.ackpine.installer.createSession
|
||||||
|
import ru.solrudev.ackpine.installer.getSession
|
||||||
|
import ru.solrudev.ackpine.session.ProgressSession
|
||||||
|
import ru.solrudev.ackpine.session.Session
|
||||||
|
import ru.solrudev.ackpine.session.await
|
||||||
|
import ru.solrudev.ackpine.session.parameters.Confirmation
|
||||||
|
import ru.solrudev.ackpine.uninstaller.UninstallFailure
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
@@ -73,7 +82,7 @@ import java.time.Duration
|
|||||||
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
|
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
|
||||||
class PatcherViewModel(
|
class PatcherViewModel(
|
||||||
private val input: Patcher.ViewModelParams
|
private val input: Patcher.ViewModelParams
|
||||||
) : ViewModel(), KoinComponent, StepProgressProvider, InstallerModel {
|
) : ViewModel(), KoinComponent, InstallerModel {
|
||||||
private val app: Application by inject()
|
private val app: Application by inject()
|
||||||
private val fs: Filesystem by inject()
|
private val fs: Filesystem by inject()
|
||||||
private val pm: PM by inject()
|
private val pm: PM by inject()
|
||||||
@@ -81,6 +90,7 @@ class PatcherViewModel(
|
|||||||
private val installedAppRepository: InstalledAppRepository by inject()
|
private val installedAppRepository: InstalledAppRepository by inject()
|
||||||
private val rootInstaller: RootInstaller by inject()
|
private val rootInstaller: RootInstaller by inject()
|
||||||
private val savedStateHandle: SavedStateHandle = get()
|
private val savedStateHandle: SavedStateHandle = get()
|
||||||
|
private val ackpineInstaller: PackageInstaller = get()
|
||||||
|
|
||||||
private var installedApp: InstalledApp? = null
|
private var installedApp: InstalledApp? = null
|
||||||
private val selectedApp = input.selectedApp
|
private val selectedApp = input.selectedApp
|
||||||
@@ -95,7 +105,6 @@ class PatcherViewModel(
|
|||||||
mutableStateOf<String?>(null)
|
mutableStateOf<String?>(null)
|
||||||
}
|
}
|
||||||
private set
|
private set
|
||||||
private var ongoingPmSession: Boolean by savedStateHandle.saveableVar { false }
|
|
||||||
var packageInstallerStatus: Int? by savedStateHandle.saveable(
|
var packageInstallerStatus: Int? by savedStateHandle.saveable(
|
||||||
key = "packageInstallerStatus",
|
key = "packageInstallerStatus",
|
||||||
stateSaver = autoSaver()
|
stateSaver = autoSaver()
|
||||||
@@ -104,7 +113,7 @@ class PatcherViewModel(
|
|||||||
}
|
}
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var isInstalling by mutableStateOf(ongoingPmSession)
|
var isInstalling by mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
private var currentActivityRequest: Pair<CompletableDeferred<Boolean>, String>? by mutableStateOf(
|
private var currentActivityRequest: Pair<CompletableDeferred<Boolean>, String>? by mutableStateOf(
|
||||||
@@ -123,6 +132,18 @@ class PatcherViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This coroutine scope is used to await installations.
|
||||||
|
* It should not be cancelled on system-initiated process death since that would cancel the installation process.
|
||||||
|
*/
|
||||||
|
private val installerCoroutineScope = CoroutineScope(Dispatchers.Main)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the package name of the Apk we are trying to install.
|
||||||
|
*/
|
||||||
|
private var installerPkgName: String by savedStateHandle.saveableVar { "" }
|
||||||
|
private var installerSessionId: ParcelUuid? by savedStateHandle.saveableVar()
|
||||||
|
|
||||||
private var inputFile: File? by savedStateHandle.saveableVar()
|
private var inputFile: File? by savedStateHandle.saveableVar()
|
||||||
private val outputFile = tempDir.resolve("output.apk")
|
private val outputFile = tempDir.resolve("output.apk")
|
||||||
|
|
||||||
@@ -138,35 +159,15 @@ class PatcherViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val patchCount = input.selectedPatches.values.sumOf { it.size }
|
|
||||||
private var completedPatchCount by savedStateHandle.saveable {
|
|
||||||
// SavedStateHandle.saveable only supports the boxed version.
|
|
||||||
@Suppress("AutoboxingStateCreation") mutableStateOf(
|
|
||||||
0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val patchesProgress get() = completedPatchCount to patchCount
|
|
||||||
override var downloadProgress by savedStateHandle.saveable(
|
|
||||||
key = "downloadProgress",
|
|
||||||
stateSaver = autoSaver()
|
|
||||||
) {
|
|
||||||
mutableStateOf<Pair<Long, Long?>?>(null)
|
|
||||||
}
|
|
||||||
private set
|
|
||||||
val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) {
|
val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) {
|
||||||
generateSteps(
|
generateSteps(app, input.selectedApp, input.selectedPatches).toMutableStateList()
|
||||||
app,
|
|
||||||
input.selectedApp
|
|
||||||
).toMutableStateList()
|
|
||||||
}
|
}
|
||||||
private var currentStepIndex = 0
|
|
||||||
|
|
||||||
val progress by derivedStateOf {
|
val progress by derivedStateOf {
|
||||||
val current = steps.count {
|
val steps = steps.filter { it.id != StepId.ExecutePatches }
|
||||||
it.state == State.COMPLETED && it.category != StepCategory.PATCHING
|
|
||||||
} + completedPatchCount
|
|
||||||
|
|
||||||
val total = steps.size - 1 + patchCount
|
val current = steps.count { it.state == State.COMPLETED }
|
||||||
|
val total = steps.size
|
||||||
|
|
||||||
current.toFloat() / total.toFloat()
|
current.toFloat() / total.toFloat()
|
||||||
}
|
}
|
||||||
@@ -174,67 +175,46 @@ class PatcherViewModel(
|
|||||||
private val workManager = WorkManager.getInstance(app)
|
private val workManager = WorkManager.getInstance(app)
|
||||||
|
|
||||||
private val patcherWorkerId by savedStateHandle.saveable<ParcelUuid> {
|
private val patcherWorkerId by savedStateHandle.saveable<ParcelUuid> {
|
||||||
ParcelUuid(workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
ParcelUuid(
|
||||||
"patching", PatcherWorker.Args(
|
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
||||||
input.selectedApp,
|
"patching", PatcherWorker.Args(
|
||||||
outputFile.path,
|
input.selectedApp,
|
||||||
input.selectedPatches,
|
outputFile.path,
|
||||||
input.options,
|
input.selectedPatches,
|
||||||
logger,
|
input.options,
|
||||||
onDownloadProgress = {
|
logger,
|
||||||
withContext(Dispatchers.Main) {
|
setInputFile = { withContext(Dispatchers.Main) { inputFile = it } },
|
||||||
downloadProgress = it
|
handleStartActivityRequest = { plugin, intent ->
|
||||||
}
|
withContext(Dispatchers.Main) {
|
||||||
},
|
if (currentActivityRequest != null) throw Exception("Another request is already pending.")
|
||||||
onPatchCompleted = { withContext(Dispatchers.Main) { completedPatchCount += 1 } },
|
|
||||||
setInputFile = { withContext(Dispatchers.Main) { inputFile = it } },
|
|
||||||
handleStartActivityRequest = { plugin, intent ->
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
if (currentActivityRequest != null) throw Exception("Another request is already pending.")
|
|
||||||
try {
|
|
||||||
// Wait for the dialog interaction.
|
|
||||||
val accepted = with(CompletableDeferred<Boolean>()) {
|
|
||||||
currentActivityRequest = this to plugin.name
|
|
||||||
|
|
||||||
await()
|
|
||||||
}
|
|
||||||
if (!accepted) throw UserInteractionException.RequestDenied()
|
|
||||||
|
|
||||||
// Launch the activity and wait for the result.
|
|
||||||
try {
|
try {
|
||||||
with(CompletableDeferred<ActivityResult>()) {
|
// Wait for the dialog interaction.
|
||||||
launchedActivity = this
|
val accepted = with(CompletableDeferred<Boolean>()) {
|
||||||
launchActivityChannel.send(intent)
|
currentActivityRequest = this to plugin.name
|
||||||
|
|
||||||
await()
|
await()
|
||||||
}
|
}
|
||||||
|
if (!accepted) throw UserInteractionException.RequestDenied()
|
||||||
|
|
||||||
|
// Launch the activity and wait for the result.
|
||||||
|
try {
|
||||||
|
with(CompletableDeferred<ActivityResult>()) {
|
||||||
|
launchedActivity = this
|
||||||
|
launchActivityChannel.send(intent)
|
||||||
|
await()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
launchedActivity = null
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
launchedActivity = null
|
currentActivityRequest = null
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
currentActivityRequest = null
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
onEvent = ::handleProgressEvent,
|
||||||
onProgress = { name, state, message ->
|
)
|
||||||
viewModelScope.launch {
|
|
||||||
steps[currentStepIndex] = steps[currentStepIndex].run {
|
|
||||||
copy(
|
|
||||||
name = name ?: this.name,
|
|
||||||
state = state ?: this.state,
|
|
||||||
message = message ?: this.message
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == State.COMPLETED && currentStepIndex != steps.lastIndex) {
|
|
||||||
currentStepIndex++
|
|
||||||
|
|
||||||
steps[currentStepIndex] =
|
|
||||||
steps[currentStepIndex].copy(state = State.RUNNING)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
))
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val patcherSucceeded =
|
val patcherSucceeded =
|
||||||
@@ -246,64 +226,26 @@ class PatcherViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val installerBroadcastReceiver = object : BroadcastReceiver() {
|
init {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
// TODO: detect system-initiated process death during the patching process.
|
||||||
when (intent?.action) {
|
|
||||||
InstallService.APP_INSTALL_ACTION -> {
|
|
||||||
val pmStatus = intent.getIntExtra(
|
|
||||||
InstallService.EXTRA_INSTALL_STATUS,
|
|
||||||
PackageInstaller.STATUS_FAILURE
|
|
||||||
)
|
|
||||||
|
|
||||||
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
|
installerSessionId?.uuid?.let { id ->
|
||||||
?.let(logger::trace)
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
|
isInstalling = true
|
||||||
app.toast(app.getString(R.string.install_app_success))
|
uiSafe(app, R.string.install_app_fail, "Failed to install") {
|
||||||
installedPackageName =
|
// The process was killed during installation. Await the session again.
|
||||||
intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME)
|
withContext(Dispatchers.IO) {
|
||||||
viewModelScope.launch {
|
ackpineInstaller.getSession(id)
|
||||||
installedAppRepository.addOrUpdate(
|
}?.let {
|
||||||
installedPackageName!!,
|
awaitInstallation(it)
|
||||||
packageName,
|
|
||||||
input.selectedApp.version
|
|
||||||
?: pm.getPackageInfo(outputFile)?.versionName!!,
|
|
||||||
InstallType.DEFAULT,
|
|
||||||
input.selectedPatches
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else packageInstallerStatus = pmStatus
|
}
|
||||||
|
} finally {
|
||||||
isInstalling = false
|
isInstalling = false
|
||||||
}
|
}
|
||||||
|
|
||||||
UninstallService.APP_UNINSTALL_ACTION -> {
|
|
||||||
val pmStatus = intent.getIntExtra(
|
|
||||||
UninstallService.EXTRA_UNINSTALL_STATUS,
|
|
||||||
PackageInstaller.STATUS_FAILURE
|
|
||||||
)
|
|
||||||
|
|
||||||
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
|
|
||||||
?.let(logger::trace)
|
|
||||||
|
|
||||||
if (pmStatus != PackageInstaller.STATUS_SUCCESS)
|
|
||||||
packageInstallerStatus = pmStatus
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
// TODO: detect system-initiated process death during the patching process.
|
|
||||||
ContextCompat.registerReceiver(
|
|
||||||
app,
|
|
||||||
installerBroadcastReceiver,
|
|
||||||
IntentFilter().apply {
|
|
||||||
addAction(InstallService.APP_INSTALL_ACTION)
|
|
||||||
addAction(UninstallService.APP_UNINSTALL_ACTION)
|
|
||||||
},
|
|
||||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
|
||||||
)
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
installedApp = installedAppRepository.get(packageName)
|
installedApp = installedAppRepository.get(packageName)
|
||||||
@@ -313,7 +255,6 @@ class PatcherViewModel(
|
|||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
app.unregisterReceiver(installerBroadcastReceiver)
|
|
||||||
workManager.cancelWorkById(patcherWorkerId.uuid)
|
workManager.cancelWorkById(patcherWorkerId.uuid)
|
||||||
|
|
||||||
if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) {
|
if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) {
|
||||||
@@ -327,7 +268,37 @@ class PatcherViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleProgressEvent(event: ProgressEvent) = viewModelScope.launch {
|
||||||
|
val stepIndex = steps.indexOfFirst {
|
||||||
|
event.stepId?.let { id -> id == it.id }
|
||||||
|
?: (it.state == State.RUNNING || it.state == State.WAITING)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stepIndex != -1) steps[stepIndex] = steps[stepIndex].run {
|
||||||
|
when (event) {
|
||||||
|
is ProgressEvent.Started -> withState(State.RUNNING)
|
||||||
|
|
||||||
|
is ProgressEvent.Progress -> withState(
|
||||||
|
message = event.message ?: message,
|
||||||
|
progress = event.current?.let { event.current to event.total } ?: progress
|
||||||
|
)
|
||||||
|
|
||||||
|
is ProgressEvent.Completed -> withState(State.COMPLETED, progress = null)
|
||||||
|
|
||||||
|
is ProgressEvent.Failed -> {
|
||||||
|
if (event.stepId == null && steps.any { it.state == State.FAILED }) return@launch
|
||||||
|
withState(
|
||||||
|
State.FAILED,
|
||||||
|
message = event.error.stackTrace,
|
||||||
|
progress = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onBack() {
|
fun onBack() {
|
||||||
|
installerCoroutineScope.cancel()
|
||||||
// tempDir cannot be deleted inside onCleared because it gets called on system-initiated process death.
|
// tempDir cannot be deleted inside onCleared because it gets called on system-initiated process death.
|
||||||
tempDir.deleteRecursively()
|
tempDir.deleteRecursively()
|
||||||
}
|
}
|
||||||
@@ -372,44 +343,93 @@ class PatcherViewModel(
|
|||||||
|
|
||||||
fun open() = installedPackageName?.let(pm::launch)
|
fun open() = installedPackageName?.let(pm::launch)
|
||||||
|
|
||||||
fun install(installType: InstallType) = viewModelScope.launch {
|
private suspend fun startInstallation(file: File, packageName: String) {
|
||||||
var pmInstallStarted = false
|
val session = withContext(Dispatchers.IO) {
|
||||||
try {
|
ackpineInstaller.createSession(Uri.fromFile(file)) {
|
||||||
isInstalling = true
|
confirmation = Confirmation.IMMEDIATE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
installerPkgName = packageName
|
||||||
|
}
|
||||||
|
awaitInstallation(session)
|
||||||
|
}
|
||||||
|
|
||||||
val currentPackageInfo = pm.getPackageInfo(outputFile)
|
private suspend fun awaitInstallation(session: ProgressSession<InstallFailure>) = withContext(
|
||||||
?: throw Exception("Failed to load application info")
|
Dispatchers.Main
|
||||||
|
) {
|
||||||
// If the app is currently installed
|
val result = installerCoroutineScope.async {
|
||||||
val existingPackageInfo = pm.getPackageInfo(currentPackageInfo.packageName)
|
try {
|
||||||
if (existingPackageInfo != null) {
|
installerSessionId = ParcelUuid(session.id)
|
||||||
// Check if the app version is less than the installed version
|
withContext(Dispatchers.IO) {
|
||||||
if (pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(existingPackageInfo)) {
|
session.await()
|
||||||
// Exit if the selected app version is less than the installed version
|
|
||||||
packageInstallerStatus = PackageInstaller.STATUS_FAILURE_CONFLICT
|
|
||||||
return@launch
|
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
installerSessionId = null
|
||||||
|
}
|
||||||
|
}.await()
|
||||||
|
|
||||||
|
when (result) {
|
||||||
|
is Session.State.Failed<InstallFailure> -> {
|
||||||
|
result.failure.message?.let(logger::trace)
|
||||||
|
packageInstallerStatus = result.failure.asCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
when (installType) {
|
Session.State.Succeeded -> {
|
||||||
InstallType.DEFAULT -> {
|
app.toast(app.getString(R.string.install_app_success))
|
||||||
// Check if the app is mounted as root
|
installedPackageName = installerPkgName
|
||||||
// If it is, unmount it first, silently
|
installedAppRepository.addOrUpdate(
|
||||||
if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) {
|
installerPkgName,
|
||||||
rootInstaller.unmount(packageName)
|
packageName,
|
||||||
}
|
input.selectedApp.version
|
||||||
|
?: withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile)?.versionName!! },
|
||||||
|
InstallType.DEFAULT,
|
||||||
|
input.selectedPatches
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Install regularly
|
fun install(installType: InstallType) = viewModelScope.launch {
|
||||||
pm.installApp(listOf(outputFile))
|
isInstalling = true
|
||||||
pmInstallStarted = true
|
var needsRootUninstall = false
|
||||||
|
try {
|
||||||
|
uiSafe(app, R.string.install_app_fail, "Failed to install") {
|
||||||
|
val currentPackageInfo =
|
||||||
|
withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile) }
|
||||||
|
?: throw Exception("Failed to load application info")
|
||||||
|
|
||||||
|
// If the app is currently installed
|
||||||
|
val existingPackageInfo =
|
||||||
|
withContext(Dispatchers.IO) { pm.getPackageInfo(currentPackageInfo.packageName) }
|
||||||
|
if (existingPackageInfo != null) {
|
||||||
|
// Check if the app version is less than the installed version
|
||||||
|
if (
|
||||||
|
pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(
|
||||||
|
existingPackageInfo
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// Exit if the selected app version is less than the installed version
|
||||||
|
packageInstallerStatus = AndroidPackageInstaller.STATUS_FAILURE_CONFLICT
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
InstallType.MOUNT -> {
|
when (installType) {
|
||||||
try {
|
InstallType.DEFAULT -> {
|
||||||
val packageInfo = pm.getPackageInfo(outputFile)
|
// Check if the app is mounted as root
|
||||||
?: throw Exception("Failed to load application info")
|
// If it is, unmount it first, silently
|
||||||
|
if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) {
|
||||||
|
rootInstaller.unmount(packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install regularly
|
||||||
|
startInstallation(outputFile, currentPackageInfo.packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
InstallType.MOUNT -> {
|
||||||
val label = with(pm) {
|
val label = with(pm) {
|
||||||
packageInfo.label()
|
currentPackageInfo.label()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for base APK, first check if the app is already installed
|
// Check for base APK, first check if the app is already installed
|
||||||
@@ -417,15 +437,17 @@ class PatcherViewModel(
|
|||||||
// If the app is not installed, check if the output file is a base apk
|
// If the app is not installed, check if the output file is a base apk
|
||||||
if (currentPackageInfo.splitNames.isNotEmpty()) {
|
if (currentPackageInfo.splitNames.isNotEmpty()) {
|
||||||
// Exit if there is no base APK package
|
// Exit if there is no base APK package
|
||||||
packageInstallerStatus = PackageInstaller.STATUS_FAILURE_INVALID
|
packageInstallerStatus =
|
||||||
|
AndroidPackageInstaller.STATUS_FAILURE_INVALID
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val inputVersion = input.selectedApp.version
|
val inputVersion = input.selectedApp.version
|
||||||
?: inputFile?.let(pm::getPackageInfo)?.versionName
|
?: withContext(Dispatchers.IO) { inputFile?.let(pm::getPackageInfo)?.versionName }
|
||||||
?: throw Exception("Failed to determine input APK version")
|
?: throw Exception("Failed to determine input APK version")
|
||||||
|
|
||||||
|
needsRootUninstall = true
|
||||||
// Install as root
|
// Install as root
|
||||||
rootInstaller.install(
|
rootInstaller.install(
|
||||||
outputFile,
|
outputFile,
|
||||||
@@ -436,7 +458,7 @@ class PatcherViewModel(
|
|||||||
)
|
)
|
||||||
|
|
||||||
installedAppRepository.addOrUpdate(
|
installedAppRepository.addOrUpdate(
|
||||||
packageInfo.packageName,
|
currentPackageInfo.packageName,
|
||||||
packageName,
|
packageName,
|
||||||
inputVersion,
|
inputVersion,
|
||||||
InstallType.MOUNT,
|
InstallType.MOUNT,
|
||||||
@@ -448,21 +470,20 @@ class PatcherViewModel(
|
|||||||
installedPackageName = packageName
|
installedPackageName = packageName
|
||||||
|
|
||||||
app.toast(app.getString(R.string.install_app_success))
|
app.toast(app.getString(R.string.install_app_success))
|
||||||
} catch (e: Exception) {
|
needsRootUninstall = false
|
||||||
Log.e(tag, "Failed to install as root", e)
|
|
||||||
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
|
|
||||||
try {
|
|
||||||
rootInstaller.uninstall(packageName)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(tag, "Failed to install", e)
|
|
||||||
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
|
|
||||||
} finally {
|
} finally {
|
||||||
if (!pmInstallStarted) isInstalling = false
|
isInstalling = false
|
||||||
|
if (needsRootUninstall) {
|
||||||
|
try {
|
||||||
|
withContext(NonCancellable) {
|
||||||
|
rootInstaller.uninstall(packageName)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,12 +494,27 @@ class PatcherViewModel(
|
|||||||
|
|
||||||
override fun reinstall() {
|
override fun reinstall() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") {
|
try {
|
||||||
pm.getPackageInfo(outputFile)?.packageName?.let { pm.uninstallPackage(it) }
|
|
||||||
?: throw Exception("Failed to load application info")
|
|
||||||
|
|
||||||
pm.installApp(listOf(outputFile))
|
|
||||||
isInstalling = true
|
isInstalling = true
|
||||||
|
uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") {
|
||||||
|
val pkgName = withContext(Dispatchers.IO) {
|
||||||
|
pm.getPackageInfo(outputFile)?.packageName
|
||||||
|
?: throw Exception("Failed to load application info")
|
||||||
|
}
|
||||||
|
|
||||||
|
when (val result = pm.uninstallPackage(pkgName)) {
|
||||||
|
is Session.State.Failed<UninstallFailure> -> {
|
||||||
|
result.failure.message?.let(logger::trace)
|
||||||
|
packageInstallerStatus = result.failure.asCode()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
Session.State.Succeeded -> {}
|
||||||
|
}
|
||||||
|
startInstallation(outputFile, pkgName)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isInstalling = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -497,34 +533,66 @@ class PatcherViewModel(
|
|||||||
LogLevel.ERROR -> Log.e(TAG, msg)
|
LogLevel.ERROR -> Log.e(TAG, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun generateSteps(context: Context, selectedApp: SelectedApp): List<Step> {
|
fun generateSteps(
|
||||||
val needsDownload =
|
context: Context,
|
||||||
selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search
|
selectedApp: SelectedApp,
|
||||||
|
selectedPatches: PatchSelection
|
||||||
|
): List<Step> = buildList {
|
||||||
|
if (selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search)
|
||||||
|
add(
|
||||||
|
Step(
|
||||||
|
StepId.DownloadAPK,
|
||||||
|
context.getString(R.string.download_apk),
|
||||||
|
StepCategory.PREPARING
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return listOfNotNull(
|
add(
|
||||||
Step(
|
|
||||||
context.getString(R.string.download_apk),
|
|
||||||
StepCategory.PREPARING,
|
|
||||||
state = State.RUNNING,
|
|
||||||
progressKey = ProgressKey.DOWNLOAD,
|
|
||||||
).takeIf { needsDownload },
|
|
||||||
Step(
|
Step(
|
||||||
|
StepId.LoadPatches,
|
||||||
context.getString(R.string.patcher_step_load_patches),
|
context.getString(R.string.patcher_step_load_patches),
|
||||||
StepCategory.PREPARING,
|
StepCategory.PREPARING
|
||||||
state = if (needsDownload) State.WAITING else State.RUNNING,
|
)
|
||||||
),
|
)
|
||||||
|
add(
|
||||||
Step(
|
Step(
|
||||||
|
StepId.ReadAPK,
|
||||||
context.getString(R.string.patcher_step_unpack),
|
context.getString(R.string.patcher_step_unpack),
|
||||||
StepCategory.PREPARING
|
StepCategory.PREPARING
|
||||||
),
|
)
|
||||||
|
)
|
||||||
|
add(
|
||||||
Step(
|
Step(
|
||||||
|
StepId.ExecutePatches,
|
||||||
context.getString(R.string.execute_patches),
|
context.getString(R.string.execute_patches),
|
||||||
StepCategory.PATCHING
|
StepCategory.PATCHING,
|
||||||
),
|
hide = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
Step(context.getString(R.string.patcher_step_write_patched), StepCategory.SAVING),
|
selectedPatches.values.asSequence().flatten().sorted().forEachIndexed { index, name ->
|
||||||
Step(context.getString(R.string.patcher_step_sign_apk), StepCategory.SAVING)
|
add(
|
||||||
|
Step(
|
||||||
|
StepId.ExecutePatch(index),
|
||||||
|
name,
|
||||||
|
StepCategory.PATCHING
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
add(
|
||||||
|
Step(
|
||||||
|
StepId.WriteAPK,
|
||||||
|
context.getString(R.string.patcher_step_write_patched),
|
||||||
|
StepCategory.SAVING
|
||||||
|
)
|
||||||
|
)
|
||||||
|
add(
|
||||||
|
Step(
|
||||||
|
StepId.SignAPK,
|
||||||
|
context.getString(R.string.patcher_step_sign_apk),
|
||||||
|
StepCategory.SAVING
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import android.os.Parcelable
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -129,8 +128,6 @@ class SelectedAppInfoViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var options: Options by savedStateHandle.saveable {
|
var options: Options by savedStateHandle.saveable {
|
||||||
val state = mutableStateOf<Options>(emptyMap())
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (!persistConfiguration) return@launch // TODO: save options for patched apps.
|
if (!persistConfiguration) return@launch // TODO: save options for patched apps.
|
||||||
val bundlePatches = bundleInfoFlow.first()
|
val bundlePatches = bundleInfoFlow.first()
|
||||||
@@ -141,7 +138,7 @@ class SelectedAppInfoViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state
|
mutableStateOf(emptyMap())
|
||||||
}
|
}
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@@ -149,8 +146,6 @@ class SelectedAppInfoViewModel(
|
|||||||
if (input.patches != null)
|
if (input.patches != null)
|
||||||
return@saveable mutableStateOf(SelectionState.Customized(input.patches))
|
return@saveable mutableStateOf(SelectionState.Customized(input.patches))
|
||||||
|
|
||||||
val selection: MutableState<SelectionState> = mutableStateOf(SelectionState.Default)
|
|
||||||
|
|
||||||
// Try to get the previous selection if customization is enabled.
|
// Try to get the previous selection if customization is enabled.
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (!prefs.disableSelectionWarning.get()) return@launch
|
if (!prefs.disableSelectionWarning.get()) return@launch
|
||||||
@@ -160,7 +155,7 @@ class SelectedAppInfoViewModel(
|
|||||||
selectionState = SelectionState.Customized(previous)
|
selectionState = SelectionState.Customized(previous)
|
||||||
}
|
}
|
||||||
|
|
||||||
selection
|
mutableStateOf(SelectionState.Default)
|
||||||
}
|
}
|
||||||
|
|
||||||
var showSourceSelector by mutableStateOf(false)
|
var showSourceSelector by mutableStateOf(false)
|
||||||
@@ -311,7 +306,7 @@ class SelectedAppInfoViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Error(@StringRes val resourceId: Int) {
|
enum class Error(@param:StringRes val resourceId: Int) {
|
||||||
NoPlugins(R.string.downloader_no_plugins_available)
|
NoPlugins(R.string.downloader_no_plugins_available)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.BroadcastReceiver
|
import android.net.Uri
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableLongStateOf
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
@@ -21,8 +16,6 @@ import app.revanced.manager.data.platform.NetworkInfo
|
|||||||
import app.revanced.manager.network.api.ReVancedAPI
|
import app.revanced.manager.network.api.ReVancedAPI
|
||||||
import app.revanced.manager.network.dto.ReVancedAsset
|
import app.revanced.manager.network.dto.ReVancedAsset
|
||||||
import app.revanced.manager.network.service.HttpService
|
import app.revanced.manager.network.service.HttpService
|
||||||
import app.revanced.manager.service.InstallService
|
|
||||||
import app.revanced.manager.util.PM
|
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
import app.revanced.manager.util.uiSafe
|
import app.revanced.manager.util.uiSafe
|
||||||
import io.ktor.client.plugins.onDownload
|
import io.ktor.client.plugins.onDownload
|
||||||
@@ -31,7 +24,14 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.get
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
import ru.solrudev.ackpine.installer.InstallFailure
|
||||||
|
import ru.solrudev.ackpine.installer.PackageInstaller
|
||||||
|
import ru.solrudev.ackpine.installer.createSession
|
||||||
|
import ru.solrudev.ackpine.session.Session
|
||||||
|
import ru.solrudev.ackpine.session.await
|
||||||
|
import ru.solrudev.ackpine.session.parameters.Confirmation
|
||||||
|
|
||||||
class UpdateViewModel(
|
class UpdateViewModel(
|
||||||
private val downloadOnScreenEntry: Boolean
|
private val downloadOnScreenEntry: Boolean
|
||||||
@@ -39,10 +39,11 @@ class UpdateViewModel(
|
|||||||
private val app: Application by inject()
|
private val app: Application by inject()
|
||||||
private val reVancedAPI: ReVancedAPI by inject()
|
private val reVancedAPI: ReVancedAPI by inject()
|
||||||
private val http: HttpService by inject()
|
private val http: HttpService by inject()
|
||||||
private val pm: PM by inject()
|
|
||||||
private val networkInfo: NetworkInfo by inject()
|
private val networkInfo: NetworkInfo by inject()
|
||||||
private val fs: Filesystem by inject()
|
private val fs: Filesystem by inject()
|
||||||
|
private val ackpineInstaller: PackageInstaller = get()
|
||||||
|
|
||||||
|
// TODO: save state to handle process death.
|
||||||
var downloadedSize by mutableLongStateOf(0L)
|
var downloadedSize by mutableLongStateOf(0L)
|
||||||
private set
|
private set
|
||||||
var totalSize by mutableLongStateOf(0L)
|
var totalSize by mutableLongStateOf(0L)
|
||||||
@@ -62,14 +63,17 @@ class UpdateViewModel(
|
|||||||
private set
|
private set
|
||||||
|
|
||||||
private val location = fs.tempDir.resolve("updater.apk")
|
private val location = fs.tempDir.resolve("updater.apk")
|
||||||
private val job = viewModelScope.launch {
|
|
||||||
uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") {
|
|
||||||
releaseInfo = reVancedAPI.getAppUpdate() ?: throw Exception("No update available")
|
|
||||||
|
|
||||||
if (downloadOnScreenEntry) {
|
init {
|
||||||
downloadUpdate()
|
viewModelScope.launch {
|
||||||
} else {
|
uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") {
|
||||||
state = State.CAN_DOWNLOAD
|
releaseInfo = reVancedAPI.getAppUpdate() ?: throw Exception("No update available")
|
||||||
|
|
||||||
|
if (downloadOnScreenEntry) {
|
||||||
|
downloadUpdate()
|
||||||
|
} else {
|
||||||
|
state = State.CAN_DOWNLOAD
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,7 +82,7 @@ class UpdateViewModel(
|
|||||||
uiSafe(app, R.string.failed_to_download_update, "Failed to download update") {
|
uiSafe(app, R.string.failed_to_download_update, "Failed to download update") {
|
||||||
val release = releaseInfo!!
|
val release = releaseInfo!!
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
if (!networkInfo.isSafe() && !ignoreInternetCheck) {
|
if (!networkInfo.isSafe(false) && !ignoreInternetCheck) {
|
||||||
showInternetCheckDialog = true
|
showInternetCheckDialog = true
|
||||||
} else {
|
} else {
|
||||||
state = State.DOWNLOADING
|
state = State.DOWNLOADING
|
||||||
@@ -86,8 +90,10 @@ class UpdateViewModel(
|
|||||||
http.download(location) {
|
http.download(location) {
|
||||||
url(release.downloadUrl)
|
url(release.downloadUrl)
|
||||||
onDownload { bytesSentTotal, contentLength ->
|
onDownload { bytesSentTotal, contentLength ->
|
||||||
downloadedSize = bytesSentTotal
|
withContext(Dispatchers.Main) {
|
||||||
totalSize = contentLength
|
downloadedSize = bytesSentTotal
|
||||||
|
contentLength?.let { totalSize = it }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
installUpdate()
|
installUpdate()
|
||||||
@@ -98,50 +104,36 @@ class UpdateViewModel(
|
|||||||
|
|
||||||
fun installUpdate() = viewModelScope.launch {
|
fun installUpdate() = viewModelScope.launch {
|
||||||
state = State.INSTALLING
|
state = State.INSTALLING
|
||||||
|
val result = withContext(Dispatchers.IO) {
|
||||||
|
ackpineInstaller.createSession(Uri.fromFile(location)) {
|
||||||
|
confirmation = Confirmation.IMMEDIATE
|
||||||
|
}.await()
|
||||||
|
}
|
||||||
|
|
||||||
pm.installApp(listOf(location))
|
when (result) {
|
||||||
}
|
is Session.State.Failed<InstallFailure> -> when (val failure = result.failure) {
|
||||||
|
is InstallFailure.Aborted -> state = State.CAN_INSTALL
|
||||||
private val installBroadcastReceiver = object : BroadcastReceiver() {
|
else -> {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
val msg = failure.message.orEmpty()
|
||||||
intent?.let {
|
app.toast(app.getString(R.string.install_app_fail, msg))
|
||||||
val pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999)
|
installError = msg
|
||||||
val extra =
|
state = State.FAILED
|
||||||
intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!!
|
|
||||||
|
|
||||||
when(pmStatus) {
|
|
||||||
PackageInstaller.STATUS_SUCCESS -> {
|
|
||||||
app.toast(app.getString(R.string.install_app_success))
|
|
||||||
state = State.SUCCESS
|
|
||||||
}
|
|
||||||
PackageInstaller.STATUS_FAILURE_ABORTED -> {
|
|
||||||
state = State.CAN_INSTALL
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
app.toast(app.getString(R.string.install_app_fail, extra))
|
|
||||||
installError = extra
|
|
||||||
state = State.FAILED
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Session.State.Succeeded -> {
|
||||||
|
app.toast(app.getString(R.string.install_app_success))
|
||||||
|
state = State.SUCCESS
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
|
||||||
ContextCompat.registerReceiver(app, installBroadcastReceiver, IntentFilter().apply {
|
|
||||||
addAction(InstallService.APP_INSTALL_ACTION)
|
|
||||||
}, ContextCompat.RECEIVER_NOT_EXPORTED)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
app.unregisterReceiver(installBroadcastReceiver)
|
|
||||||
|
|
||||||
job.cancel()
|
|
||||||
location.delete()
|
location.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class State(@StringRes val title: Int) {
|
enum class State(@param:StringRes val title: Int) {
|
||||||
CAN_DOWNLOAD(R.string.update_available),
|
CAN_DOWNLOAD(R.string.update_available),
|
||||||
DOWNLOADING(R.string.downloading_manager_update),
|
DOWNLOADING(R.string.downloading_manager_update),
|
||||||
CAN_INSTALL(R.string.ready_to_install_update),
|
CAN_INSTALL(R.string.ready_to_install_update),
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ class UpdatesSettingsViewModel(
|
|||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val managerAutoUpdates = prefs.managerAutoUpdates
|
val managerAutoUpdates = prefs.managerAutoUpdates
|
||||||
val showManagerUpdateDialogOnLaunch = prefs.showManagerUpdateDialogOnLaunch
|
val showManagerUpdateDialogOnLaunch = prefs.showManagerUpdateDialogOnLaunch
|
||||||
|
val useManagerPrereleases = prefs.useManagerPrereleases
|
||||||
|
|
||||||
|
|
||||||
val isConnected: Boolean
|
val isConnected: Boolean
|
||||||
get() = network.isConnected()
|
get() = network.isConnected()
|
||||||
|
|||||||
30
app/src/main/java/app/revanced/manager/util/Ackpine.kt
Normal file
30
app/src/main/java/app/revanced/manager/util/Ackpine.kt
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package app.revanced.manager.util
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
|
import ru.solrudev.ackpine.installer.InstallFailure
|
||||||
|
import ru.solrudev.ackpine.uninstaller.UninstallFailure
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an Ackpine installation failure into a PM status code
|
||||||
|
*/
|
||||||
|
fun InstallFailure.asCode() = when (this) {
|
||||||
|
is InstallFailure.Aborted -> PackageInstaller.STATUS_FAILURE_ABORTED
|
||||||
|
is InstallFailure.Blocked -> PackageInstaller.STATUS_FAILURE_BLOCKED
|
||||||
|
is InstallFailure.Conflict -> PackageInstaller.STATUS_FAILURE_CONFLICT
|
||||||
|
is InstallFailure.Incompatible -> PackageInstaller.STATUS_FAILURE_INCOMPATIBLE
|
||||||
|
is InstallFailure.Invalid -> PackageInstaller.STATUS_FAILURE_INVALID
|
||||||
|
is InstallFailure.Storage -> PackageInstaller.STATUS_FAILURE_STORAGE
|
||||||
|
is InstallFailure.Timeout -> @SuppressLint("InlinedApi") PackageInstaller.STATUS_FAILURE_TIMEOUT
|
||||||
|
else -> PackageInstaller.STATUS_FAILURE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an Ackpine uninstallation failure into a PM status code
|
||||||
|
*/
|
||||||
|
fun UninstallFailure.asCode() = when (this) {
|
||||||
|
is UninstallFailure.Aborted -> PackageInstaller.STATUS_FAILURE_ABORTED
|
||||||
|
is UninstallFailure.Blocked -> PackageInstaller.STATUS_FAILURE_BLOCKED
|
||||||
|
is UninstallFailure.Conflict -> PackageInstaller.STATUS_FAILURE_CONFLICT
|
||||||
|
else -> PackageInstaller.STATUS_FAILURE
|
||||||
|
}
|
||||||
@@ -2,11 +2,8 @@ package app.revanced.manager.util
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.pm.PackageManager.PackageInfoFlags
|
import android.content.pm.PackageManager.PackageInfoFlags
|
||||||
import android.content.pm.PackageManager.NameNotFoundException
|
import android.content.pm.PackageManager.NameNotFoundException
|
||||||
@@ -16,8 +13,6 @@ import android.os.Build
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
import app.revanced.manager.service.InstallService
|
|
||||||
import app.revanced.manager.service.UninstallService
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
@@ -25,10 +20,13 @@ import kotlinx.coroutines.flow.flowOn
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import ru.solrudev.ackpine.session.await
|
||||||
|
import ru.solrudev.ackpine.session.parameters.Confirmation
|
||||||
|
import ru.solrudev.ackpine.uninstaller.PackageUninstaller
|
||||||
|
import ru.solrudev.ackpine.uninstaller.createSession
|
||||||
|
import ru.solrudev.ackpine.uninstaller.parameters.UninstallParametersDsl
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable
|
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class AppInfo(
|
data class AppInfo(
|
||||||
@@ -40,7 +38,8 @@ data class AppInfo(
|
|||||||
@SuppressLint("QueryPermissionsNeeded")
|
@SuppressLint("QueryPermissionsNeeded")
|
||||||
class PM(
|
class PM(
|
||||||
private val app: Application,
|
private val app: Application,
|
||||||
patchBundleRepository: PatchBundleRepository
|
patchBundleRepository: PatchBundleRepository,
|
||||||
|
private val uninstaller: PackageUninstaller
|
||||||
) {
|
) {
|
||||||
private val scope = CoroutineScope(Dispatchers.IO)
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
@@ -145,17 +144,11 @@ class PM(
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun installApp(apks: List<File>) = withContext(Dispatchers.IO) {
|
suspend fun uninstallPackage(pkg: String, config: UninstallParametersDsl.() -> Unit = {}) = withContext(Dispatchers.IO) {
|
||||||
val packageInstaller = app.packageManager.packageInstaller
|
uninstaller.createSession(pkg) {
|
||||||
packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session ->
|
confirmation = Confirmation.IMMEDIATE
|
||||||
apks.forEach { apk -> session.writeApk(apk) }
|
config()
|
||||||
session.commit(app.installIntentSender)
|
}.await()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun uninstallPackage(pkg: String) {
|
|
||||||
val packageInstaller = app.packageManager.packageInstaller
|
|
||||||
packageInstaller.uninstall(pkg, app.uninstallIntentSender)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launch(pkg: String) = app.packageManager.getLaunchIntentForPackage(pkg)?.let {
|
fun launch(pkg: String) = app.packageManager.getLaunchIntentForPackage(pkg)?.let {
|
||||||
@@ -164,44 +157,4 @@ class PM(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun canInstallPackages() = app.packageManager.canRequestPackageInstalls()
|
fun canInstallPackages() = app.packageManager.canRequestPackageInstalls()
|
||||||
|
|
||||||
private fun PackageInstaller.Session.writeApk(apk: File) {
|
|
||||||
apk.inputStream().use { inputStream ->
|
|
||||||
openWrite(apk.name, 0, apk.length()).use { outputStream ->
|
|
||||||
inputStream.copyTo(outputStream, byteArraySize)
|
|
||||||
fsync(outputStream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val intentFlags
|
|
||||||
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
|
||||||
PendingIntent.FLAG_MUTABLE
|
|
||||||
else
|
|
||||||
0
|
|
||||||
|
|
||||||
private val sessionParams
|
|
||||||
get() = PackageInstaller.SessionParams(
|
|
||||||
PackageInstaller.SessionParams.MODE_FULL_INSTALL
|
|
||||||
).apply {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
||||||
setRequestUpdateOwnership(true)
|
|
||||||
setInstallReason(PackageManager.INSTALL_REASON_USER)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val Context.installIntentSender
|
|
||||||
get() = PendingIntent.getService(
|
|
||||||
this,
|
|
||||||
0,
|
|
||||||
Intent(this, InstallService::class.java),
|
|
||||||
intentFlags
|
|
||||||
).intentSender
|
|
||||||
|
|
||||||
private val Context.uninstallIntentSender
|
|
||||||
get() = PendingIntent.getService(
|
|
||||||
this,
|
|
||||||
0,
|
|
||||||
Intent(this, UninstallService::class.java),
|
|
||||||
intentFlags
|
|
||||||
).intentSender
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package app.revanced.manager.util
|
||||||
|
|
||||||
|
import android.app.LocaleConfig
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.LocaleList
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.core.os.LocaleListCompat
|
||||||
|
import app.revanced.manager.BuildConfig
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
object SupportedLocales {
|
||||||
|
fun getSupportedLocales(context: Context): List<Locale> {
|
||||||
|
var result: List<Locale>? = null
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) result = runCatching {
|
||||||
|
LocaleConfig(context).supportedLocales?.toList()
|
||||||
|
}.getOrNull()
|
||||||
|
|
||||||
|
return result ?: generated
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCurrentLocale(): Locale? =
|
||||||
|
AppCompatDelegate.getApplicationLocales().takeIf { !it.isEmpty }?.get(0)
|
||||||
|
|
||||||
|
fun setLocale(locale: Locale?) = AppCompatDelegate.setApplicationLocales(
|
||||||
|
locale?.let { LocaleListCompat.create(it) } ?: LocaleListCompat.getEmptyLocaleList()
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getDisplayName(locale: Locale) =
|
||||||
|
locale.getDisplayName(locale).replaceFirstChar { it.uppercase(locale) }
|
||||||
|
|
||||||
|
private fun LocaleList.toList() = (0 until size()).map { get(it) }
|
||||||
|
|
||||||
|
private val generated by lazy {
|
||||||
|
listOf(
|
||||||
|
Locale.ENGLISH,
|
||||||
|
*BuildConfig.SUPPORTED_LOCALES.map(Locale::forLanguageTag).toTypedArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,12 +15,10 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -33,6 +31,7 @@ import androidx.lifecycle.SavedStateHandle
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -42,7 +41,6 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.datetime.Clock
|
|
||||||
import kotlinx.datetime.LocalDateTime
|
import kotlinx.datetime.LocalDateTime
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import kotlinx.datetime.format.MonthNames
|
import kotlinx.datetime.format.MonthNames
|
||||||
@@ -50,9 +48,11 @@ import kotlinx.datetime.format.char
|
|||||||
import kotlinx.datetime.toInstant
|
import kotlinx.datetime.toInstant
|
||||||
import kotlinx.datetime.toLocalDateTime
|
import kotlinx.datetime.toLocalDateTime
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import kotlin.math.abs
|
||||||
import kotlin.properties.PropertyDelegateProvider
|
import kotlin.properties.PropertyDelegateProvider
|
||||||
import kotlin.properties.ReadWriteProperty
|
import kotlin.properties.ReadWriteProperty
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
|
import kotlin.time.Clock
|
||||||
|
|
||||||
typealias PatchSelection = Map<Int, Set<String>>
|
typealias PatchSelection = Map<Int, Set<String>>
|
||||||
typealias Options = Map<Int, Map<String, Map<String, Any?>>>
|
typealias Options = Map<Int, Map<String, Map<String, Any?>>>
|
||||||
@@ -82,6 +82,8 @@ fun Context.toast(string: String, duration: Int = Toast.LENGTH_SHORT) {
|
|||||||
inline fun uiSafe(context: Context, @StringRes toastMsg: Int, logMsg: String, block: () -> Unit) {
|
inline fun uiSafe(context: Context, @StringRes toastMsg: Int, logMsg: String, block: () -> Unit) {
|
||||||
try {
|
try {
|
||||||
block()
|
block()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (error: Exception) {
|
} catch (error: Exception) {
|
||||||
// You can only toast on the main thread.
|
// You can only toast on the main thread.
|
||||||
GlobalScope.launch(Dispatchers.Main) {
|
GlobalScope.launch(Dispatchers.Main) {
|
||||||
@@ -166,7 +168,7 @@ fun LocalDateTime.relativeTime(context: Context): String {
|
|||||||
else -> LocalDateTime.Format {
|
else -> LocalDateTime.Format {
|
||||||
monthName(MonthNames.ENGLISH_ABBREVIATED)
|
monthName(MonthNames.ENGLISH_ABBREVIATED)
|
||||||
char(' ')
|
char(' ')
|
||||||
dayOfMonth()
|
day()
|
||||||
if (now.toLocalDateTime(TimeZone.UTC).year != this@relativeTime.year) {
|
if (now.toLocalDateTime(TimeZone.UTC).year != this@relativeTime.year) {
|
||||||
chars(", ")
|
chars(", ")
|
||||||
year()
|
year()
|
||||||
@@ -193,7 +195,12 @@ val transparentListItemColors
|
|||||||
.also { transparentListItemColorsCached = it }
|
.also { transparentListItemColorsCached = it }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun <T> EventEffect(flow: Flow<T>, vararg keys: Any?, state: Lifecycle.State = Lifecycle.State.STARTED, block: suspend (T) -> Unit) {
|
fun <T> EventEffect(
|
||||||
|
flow: Flow<T>,
|
||||||
|
vararg keys: Any?,
|
||||||
|
state: Lifecycle.State = Lifecycle.State.STARTED,
|
||||||
|
block: suspend (T) -> Unit
|
||||||
|
) {
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
val currentBlock by rememberUpdatedState(block)
|
val currentBlock by rememberUpdatedState(block)
|
||||||
|
|
||||||
@@ -209,40 +216,36 @@ fun <T> EventEffect(flow: Flow<T>, vararg keys: Any?, state: Lifecycle.State = L
|
|||||||
const val isScrollingUpSensitivity = 10
|
const val isScrollingUpSensitivity = 10
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LazyListState.isScrollingUp(): State<Boolean> {
|
fun LazyListState.isScrollingUp() = produceState(true, this) {
|
||||||
return remember(this) {
|
var previousIndex = firstVisibleItemIndex
|
||||||
var previousIndex by mutableIntStateOf(firstVisibleItemIndex)
|
var previousScrollOffset = firstVisibleItemScrollOffset
|
||||||
var previousScrollOffset by mutableIntStateOf(firstVisibleItemScrollOffset)
|
|
||||||
|
|
||||||
derivedStateOf {
|
snapshotFlow {
|
||||||
val indexChanged = previousIndex != firstVisibleItemIndex
|
firstVisibleItemIndex to firstVisibleItemScrollOffset
|
||||||
val offsetChanged =
|
}.collect { (index, scrollOffset) ->
|
||||||
kotlin.math.abs(previousScrollOffset - firstVisibleItemScrollOffset) > isScrollingUpSensitivity
|
val indexChanged = previousIndex != index
|
||||||
|
val offsetChanged = abs(previousScrollOffset - scrollOffset) > isScrollingUpSensitivity
|
||||||
|
|
||||||
if (indexChanged) {
|
value = when {
|
||||||
previousIndex > firstVisibleItemIndex
|
indexChanged -> previousIndex > index
|
||||||
} else if (offsetChanged) {
|
offsetChanged -> previousScrollOffset > scrollOffset
|
||||||
previousScrollOffset > firstVisibleItemScrollOffset
|
else -> value
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}.also {
|
|
||||||
previousIndex = firstVisibleItemIndex
|
|
||||||
previousScrollOffset = firstVisibleItemScrollOffset
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
previousIndex = index
|
||||||
|
previousScrollOffset = scrollOffset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: support sensitivity
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ScrollState.isScrollingUp(): State<Boolean> {
|
fun ScrollState.isScrollingUp() = produceState(true, this) {
|
||||||
return remember(this) {
|
var previousScrollOffset = this@isScrollingUp.value
|
||||||
var previousScrollOffset by mutableIntStateOf(value)
|
|
||||||
derivedStateOf {
|
snapshotFlow { this@isScrollingUp.value }.collect { scrollOffset ->
|
||||||
(previousScrollOffset >= value).also {
|
if (abs(previousScrollOffset - scrollOffset) > isScrollingUpSensitivity) {
|
||||||
previousScrollOffset = value
|
value = previousScrollOffset >= scrollOffset
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
previousScrollOffset = scrollOffset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,4 +3,4 @@
|
|||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
16
app/src/main/res/values-af-rZA/strings.xml
Normal file
16
app/src/main/res/values-af-rZA/strings.xml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Strings with new lines must be raw strings, where the string is wrapped in double quotes and new lines are regular line breaks and not \n
|
||||||
|
Raw strings still require escaping embedded double quotes, but single quote characters can be escaped or used as-is.
|
||||||
|
|
||||||
|
Raw strings are required because Crowdin AI translations regularly gets confused and
|
||||||
|
replace \n with an encoded new line character.
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
<string name="summary_key">First \'item\' text\nSecond \"item\" text</string>
|
||||||
|
Good:
|
||||||
|
<string name="summary_key">"First 'item' text
|
||||||
|
Second \"item\" text"</string>
|
||||||
|
|
||||||
|
-->
|
||||||
|
<resources></resources>
|
||||||
16
app/src/main/res/values-am-rET/strings.xml
Normal file
16
app/src/main/res/values-am-rET/strings.xml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Strings with new lines must be raw strings, where the string is wrapped in double quotes and new lines are regular line breaks and not \n
|
||||||
|
Raw strings still require escaping embedded double quotes, but single quote characters can be escaped or used as-is.
|
||||||
|
|
||||||
|
Raw strings are required because Crowdin AI translations regularly gets confused and
|
||||||
|
replace \n with an encoded new line character.
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
<string name="summary_key">First \'item\' text\nSecond \"item\" text</string>
|
||||||
|
Good:
|
||||||
|
<string name="summary_key">"First 'item' text
|
||||||
|
Second \"item\" text"</string>
|
||||||
|
|
||||||
|
-->
|
||||||
|
<resources></resources>
|
||||||
460
app/src/main/res/values-ar-rSA/strings.xml
Normal file
460
app/src/main/res/values-ar-rSA/strings.xml
Normal file
@@ -0,0 +1,460 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Strings with new lines must be raw strings, where the string is wrapped in double quotes and new lines are regular line breaks and not \n
|
||||||
|
Raw strings still require escaping embedded double quotes, but single quote characters can be escaped or used as-is.
|
||||||
|
|
||||||
|
Raw strings are required because Crowdin AI translations regularly gets confused and
|
||||||
|
replace \n with an encoded new line character.
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
<string name="summary_key">First \'item\' text\nSecond \"item\" text</string>
|
||||||
|
Good:
|
||||||
|
<string name="summary_key">"First 'item' text
|
||||||
|
Second \"item\" text"</string>
|
||||||
|
|
||||||
|
-->
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">ReVanced Manager</string>
|
||||||
|
<string name="patcher">اختبار التعديل</string>
|
||||||
|
<string name="patches">التعديلات</string>
|
||||||
|
<string name="cli">CLI</string>
|
||||||
|
<string name="manager">المدير</string>
|
||||||
|
<string name="plugin_host_permission_label">مضيف المكونات الإضافية ReVanced Manager</string>
|
||||||
|
<string name="plugin_host_permission_description">يُستخدم للتحكم في الوصول إلى مكونات ReVanced Manager الإضافية. فقط ReVanced Manager يمتلك هذا.</string>
|
||||||
|
<string name="toast_copied_to_clipboard">نُسخ!</string>
|
||||||
|
<string name="copy_to_clipboard">نسخ إلى الحافظة</string>
|
||||||
|
<string name="dashboard">لوحة التحكم</string>
|
||||||
|
<string name="settings">الإعدادات</string>
|
||||||
|
<string name="select_app">حدد تطبيقًا</string>
|
||||||
|
<string name="patches_count_selected">تم تحديد %1$d/%2$d</string>
|
||||||
|
<string name="new_downloader_plugins_notification">تتوفر مكونات إضافية جديدة للتنزيل. انقر هنا لتهيئتها.</string>
|
||||||
|
<string name="unsupported_architecture_warning">التعديل على بنية هذا الجهاز غير مدعوم ومن المرجح أن يفشل.</string>
|
||||||
|
<string name="import_">استيراد</string>
|
||||||
|
<string name="import_patches">استيراد التعديلات</string>
|
||||||
|
<string name="file_field_set">محدد</string>
|
||||||
|
<string name="file_field_not_set">غير محدد</string>
|
||||||
|
<string name="field_not_set">غير معين</string>
|
||||||
|
<string name="patches_missing">مفقود</string>
|
||||||
|
<string name="patches_error">خطأ</string>
|
||||||
|
<string name="patches_error_description">تعذر تحميل التعديلات. انقر لعرض الخطأ</string>
|
||||||
|
<string name="patches_not_downloaded">لم يتم تنزيل التعديلات. انقر هنا لتنزيلها</string>
|
||||||
|
<string name="patches_name_default">التعديلات</string>
|
||||||
|
<string name="patches_name_fallback">غير مُسمى</string>
|
||||||
|
<string name="android_11_bug_dialog_title">خطأ Android 11</string>
|
||||||
|
<string name="android_11_bug_dialog_description">يجب منح إذن تثبيت التطبيق مسبقًا لتجنب خطأ في نظام Android 11 سيؤثر سلبًا على تجربة المستخدم.</string>
|
||||||
|
<string name="no_network_toast">لا يوجد اتصال بالإنترنت متاح</string>
|
||||||
|
<string name="selected_app_meta_any_version">أي إصدار متاح</string>
|
||||||
|
<string name="app_source_dialog_title">حدد المصدر</string>
|
||||||
|
<string name="app_source_dialog_option_auto">تلقائي</string>
|
||||||
|
<string name="app_source_dialog_option_auto_description">استخدم جميع أدوات التنزيل المتاحة لتنزيل التطبيق</string>
|
||||||
|
<string name="app_source_dialog_option_auto_unavailable">لا توجد مكونات إضافية متاحة</string>
|
||||||
|
<string name="app_source_dialog_option_installed_no_root">لا يمكن تعديل التطبيقات المثبتة مرة أخرى بدون صلاحيات Root</string>
|
||||||
|
<string name="app_source_dialog_option_installed_version_not_suggested">الإصدار %s لا يطابق الإصدار الموصى به</string>
|
||||||
|
<string name="patch_item_description">بدء تعديل التطبيق</string>
|
||||||
|
<string name="patch_selector_item">حدد التعديلات</string>
|
||||||
|
<string name="patch_selector_item_description">تم تحديد %d تعديل</string>
|
||||||
|
<string name="no_patches_selected">لم يتم تحديد أي تعديلات</string>
|
||||||
|
<string name="network_unavailable_warning">جهازك غير متصل بالإنترنت. سيفشل التنزيل لاحقًا.</string>
|
||||||
|
<string name="network_metered_warning">أنت حاليًا تستخدم اتصالًا محدودًا. قد يتم تطبيق رسوم بيانات من مزود الخدمة الخاص بك.</string>
|
||||||
|
<string name="apk_source_selector_item">حدد مصدر APK</string>
|
||||||
|
<string name="apk_source_auto">استخدام جميع أدوات تنزيل APK</string>
|
||||||
|
<string name="apk_source_downloader">استخدام %s</string>
|
||||||
|
<string name="apk_source_installed">استخدام APK المثبت</string>
|
||||||
|
<string name="apk_source_local">استخدام ملف APK محلي</string>
|
||||||
|
<string name="legacy_import_failed">تعذر استيراد الإعدادات القديمة</string>
|
||||||
|
<string name="auto_updates_dialog_title">تهيئة التحديثات</string>
|
||||||
|
<string name="auto_updates_dialog_description">هل تريد من ReVanced Manager التحقق بشكل دوري من التحديثات للمكونات التالية؟</string>
|
||||||
|
<string name="auto_updates_dialog_manager">ReVanced Manager</string>
|
||||||
|
<string name="auto_updates_dialog_patches">تعديلات ReVanced</string>
|
||||||
|
<string name="auto_updates_dialog_note">يمكن تغيير هذه الإعدادات لاحقًا.</string>
|
||||||
|
<string name="general">عام</string>
|
||||||
|
<string name="general_description">اللغة، المظهر، اللون الديناميكي</string>
|
||||||
|
<string name="updates">التحديثات</string>
|
||||||
|
<string name="updates_description">التحقق من التحديثات وعرض سجل التغييرات</string>
|
||||||
|
<string name="downloads">التنزيلات</string>
|
||||||
|
<string name="downloads_description">مكونات التنزيل الإضافية والتطبيقات التي تم تنزيلها</string>
|
||||||
|
<string name="import_export">استيراد وتصدير</string>
|
||||||
|
<string name="import_export_description">مخزن المفاتيح، خيارات التعديل والتحديد</string>
|
||||||
|
<string name="advanced">إعدادات متقدمة</string>
|
||||||
|
<string name="advanced_description">عنوان URL للـ API، حد الذاكرة، تصحيح الأخطاء</string>
|
||||||
|
<string name="about">لمحة</string>
|
||||||
|
<string name="opensource_licenses">تراخيص المصدر المفتوح</string>
|
||||||
|
<string name="opensource_licenses_description">عرض جميع المكتبات المستخدمة لإنشاء هذا التطبيق</string>
|
||||||
|
<string name="contributors">المساهمون</string>
|
||||||
|
<string name="contributors_description">عرض المساهمين في ReVanced</string>
|
||||||
|
<string name="dynamic_color">لون ديناميكي</string>
|
||||||
|
<string name="dynamic_color_description">تكييف الألوان مع خلفية الشاشة</string>
|
||||||
|
<string name="pure_black_theme">مظهر أسود نقي</string>
|
||||||
|
<string name="pure_black_theme_description">استخدام خلفيات سوداء نقية للمظهر الداكن</string>
|
||||||
|
<string name="theme">المظهر</string>
|
||||||
|
<string name="theme_description">اختر بين المظهر الفاتح أو الداكن</string>
|
||||||
|
<string name="language">اللغة</string>
|
||||||
|
<string name="language_description">اختر لغة عرض التطبيق</string>
|
||||||
|
<string name="language_system_default">افتراضي النظام</string>
|
||||||
|
<string name="safeguards">الضمانات</string>
|
||||||
|
<string name="patch_compat_check">تعطيل التحقق من توافق الإصدار</string>
|
||||||
|
<string name="patch_compat_check_description">لا تقيد التعديلات بإصدارات التطبيقات المتوافقة</string>
|
||||||
|
<string name="patch_compat_check_confirmation">"قد يؤدي اختيار تعديلات غير متوافقة إلى تطبيق معطّل.
|
||||||
|
|
||||||
|
هل تريد المتابعة على أي حال؟"</string>
|
||||||
|
<string name="suggested_version_safeguard">يتطلب إصدار التطبيق الموصى به</string>
|
||||||
|
<string name="suggested_version_safeguard_description">فرض اختيار إصدار التطبيق الموصى به</string>
|
||||||
|
<string name="suggested_version_safeguard_confirmation">"قد يؤدي اختيار تطبيق ليس بالإصدار الموصى به إلى مشاكل غير متوقعة.
|
||||||
|
|
||||||
|
هل تريد المتابعة على أي حال؟"</string>
|
||||||
|
<string name="patch_selection_safeguard">السماح بتغيير تحديد التعديل والخيارات</string>
|
||||||
|
<string name="patch_selection_safeguard_description">لا تمنع تحديد أو إلغاء تحديد التعديلات وتخصيص الخيارات</string>
|
||||||
|
<string name="patch_selection_safeguard_confirmation">"قد يؤدي تغيير تحديد التعديلات إلى مشاكل غير متوقعة.
|
||||||
|
|
||||||
|
هل تريد التمكين على أي حال؟"</string>
|
||||||
|
<string name="universal_patches_safeguard">السماح باستخدام التعديلات الشاملة</string>
|
||||||
|
<string name="universal_patches_safeguard_description">لا تمنع استخدام التعديلات الشاملة</string>
|
||||||
|
<string name="universal_patches_safeguard_confirmation">"التعديلات الشاملة ليست مختبرة جيدًا مثل تلك التي تستهدف تطبيقات محددة.
|
||||||
|
|
||||||
|
هل تريد التمكين على أي حال؟"</string>
|
||||||
|
<string name="import_keystore">استيراد مخزن المفاتيح</string>
|
||||||
|
<string name="import_keystore_description">استيراد مخزن مفاتيح مخصص</string>
|
||||||
|
<string name="import_keystore_dialog_title">أدخل بيانات اعتماد مخزن المفاتيح</string>
|
||||||
|
<string name="import_keystore_dialog_description">ستحتاج إلى إدخال بيانات اعتماد مخزن المفاتيح لاستيراده.</string>
|
||||||
|
<string name="import_keystore_dialog_alias_field">اسم المستخدم (الاسم المستعار)</string>
|
||||||
|
<string name="import_keystore_dialog_password_field">كلمة المرور</string>
|
||||||
|
<string name="import_keystore_dialog_button">استيراد</string>
|
||||||
|
<string name="import_keystore_wrong_credentials">بيانات اعتماد مخزن المفاتيح خاطئة</string>
|
||||||
|
<string name="import_keystore_success">تم استيراد مخزن المفاتيح</string>
|
||||||
|
<string name="export_keystore">تصدير مخزن المفاتيح</string>
|
||||||
|
<string name="export_keystore_description">تصدير مخزن المفاتيح الحالي</string>
|
||||||
|
<string name="export_keystore_unavailable">لا يوجد مخزن مفاتيح للتصدير</string>
|
||||||
|
<string name="export_keystore_success">تم تصدير مخزن المفاتيح</string>
|
||||||
|
<string name="regenerate_keystore">إعادة إنشاء مخزن المفاتيح</string>
|
||||||
|
<string name="regenerate_keystore_description">إنشاء مخزن مفاتيح جديد</string>
|
||||||
|
<string name="regenerate_keystore_dialog_description">"أنت على وشك إعادة إنشاء مخزن المفاتيح الذي سيستخدمه المدير أثناء عملية التعديل.
|
||||||
|
|
||||||
|
لن تتمكن من تحديث التطبيقات المثبتة مسبقًا من هذا المصدر."</string>
|
||||||
|
<string name="regenerate_keystore_success">تم استبدال مخزن المفاتيح بنجاح</string>
|
||||||
|
<string name="import_patch_selection">استيراد التعديل المحدد</string>
|
||||||
|
<string name="import_patch_selection_description">استيراد التعديل المحدد من ملف JSON</string>
|
||||||
|
<string name="import_patch_selection_fail">تعذر استيراد التعديل المحدد: %s</string>
|
||||||
|
<string name="import_patch_selection_success">تم استيراد التعديل المحدد</string>
|
||||||
|
<string name="export_patch_selection">تصدير التعديل المحدد</string>
|
||||||
|
<string name="export_patch_selection_description">تصدير التعديل المحدد إلى ملف JSON</string>
|
||||||
|
<string name="export_patch_selection_fail">تعذر تصدير التعديل المحدد: %s</string>
|
||||||
|
<string name="export_patch_selection_success">تم تصدير التعديل المحدد</string>
|
||||||
|
<string name="reset_patch_selection">إعادة تعيين التعديل المحدد</string>
|
||||||
|
<string name="reset_patch_selection_description">إعادة تعيين التعديل المحدد المخزن</string>
|
||||||
|
<string name="reset_patch_options">إعادة تعيين خيارات التعديل</string>
|
||||||
|
<string name="reset_patch_options_description">إعادة تعيين خيارات التعديل المخزنة</string>
|
||||||
|
<string name="reset_patch_selection_success">تم إعادة تعيين التعديل المحدد</string>
|
||||||
|
<string name="patch_selection_reset_all">إعادة تعيين التعديل المحدد عمومًا</string>
|
||||||
|
<string name="patch_selection_reset_all_dialog_description">أنت على وشك إعادة تعيين جميع التعديلات المحددة. ستحتاج إلى تحديد كل تعديل يدويًا مرة أخرى.</string>
|
||||||
|
<string name="patch_selection_reset_all_description">يعيد تعيين جميع التعديلات المحددة</string>
|
||||||
|
<string name="patch_selection_reset_package">إعادة تعيين التعديل المحدد للتطبيق</string>
|
||||||
|
<string name="patch_selection_reset_package_dialog_description">أنت على وشك إعادة تعيين التعديل المحدد للتطبيق \"%s\". سيتعين عليك تحديد كل تعديل يدويًا مرة أخرى.</string>
|
||||||
|
<string name="patch_selection_reset_package_description">يعيد تعيين التعديل المحدد لتطبيق واحد</string>
|
||||||
|
<string name="patch_selection_reset_patches">إعادة تعيين التعديل المحدد (فردي)</string>
|
||||||
|
<string name="patch_selection_reset_patches_dialog_description">أنت على وشك إعادة تعيين التعديل المحدد لـ \"%s\". سيتعين عليك تحديد كل تعديل يدويًا مرة أخرى.</string>
|
||||||
|
<string name="patch_selection_reset_patches_description">يعيد تعيين التعديل المحدد لمجموعة محددة من التعديلات</string>
|
||||||
|
<string name="patch_options_reset_package">إعادة تعيين خيارات التعديل للتطبيق</string>
|
||||||
|
<string name="patch_options_reset_package_dialog_description">أنت على وشك إعادة تعيين خيارات التعديل للتطبيق \"%s\". سيتعين عليك إعادة تطبيق كل خيار مرة أخرى.</string>
|
||||||
|
<string name="patch_options_reset_package_description">يعيد تعيين خيارات التعديل لتطبيق واحد</string>
|
||||||
|
<string name="patch_options_reset_patches">إعادة تعيين خيارات التعديل (فردي)</string>
|
||||||
|
<string name="patch_options_reset_patches_dialog_description">أنت على وشك إعادة تعيين خيارات التعديل لـ \"%s\". سيتعين عليك إعادة تطبيق كل خيار مرة أخرى.</string>
|
||||||
|
<string name="patch_options_reset_patches_description">يعيد تعيين خيارات التعديل لمجموعة محددة من التعديلات</string>
|
||||||
|
<string name="patch_options_reset_all">إعادة تعيين خيارات التعديل عمومًا</string>
|
||||||
|
<string name="patch_options_reset_all_dialog_description">أنت على وشك إعادة تعيين جميع خيارات التعديل. سيتعين عليك إعادة تطبيق كل خيار مرة أخرى.</string>
|
||||||
|
<string name="patch_options_reset_all_description">يعيد تعيين جميع خيارات التعديل</string>
|
||||||
|
<string name="downloader_plugins">المكونات الإضافية</string>
|
||||||
|
<string name="downloader_plugin_state_trusted">موثوق به</string>
|
||||||
|
<string name="downloader_plugin_state_failed">فشل التحميل. انقر للمزيد من التفاصيل.</string>
|
||||||
|
<string name="downloader_plugin_state_untrusted">غير موثوق به</string>
|
||||||
|
<string name="downloader_plugin_trust_dialog_title">هل تثق في المكون الإضافي؟</string>
|
||||||
|
<string name="downloader_plugin_revoke_trust_dialog_title">إلغاء الثقة؟</string>
|
||||||
|
<string name="downloader_plugin_trust_dialog_body">ستسمح المتابعة لهذا المكون الإضافي بالتشغيل على نظامك.\n\nقم بتمكين هذا المكون الإضافي فقط إذا كنت تثق به. يمكن للمكونات الإضافية تنفيذ تعليمات برمجية عشوائية وقد تعرض جهازك للخطر.</string>
|
||||||
|
<string name="downloader_plugin_trust_dialog_signature">التوقيع:\n\n%s</string>
|
||||||
|
<string name="downloader_plugin_trust_dialog_plugin">المكون الإضافي:\n%s</string>
|
||||||
|
<string name="downloader_plugin_delete_apps_title">حذف التطبيقات المحددة</string>
|
||||||
|
<string name="downloader_plugin_delete_apps_description">هل أنت متأكد أنك تريد حذف التطبيقات المحددة؟</string>
|
||||||
|
<string name="downloader_settings_no_apps">لم يتم العثور على تطبيقات تم تنزيلها.</string>
|
||||||
|
<string name="search_apps">البحث عن تطبيقات…</string>
|
||||||
|
<string name="loading_body">جارٍ التحميل…</string>
|
||||||
|
<string name="downloading_patches">جارٍ تنزيل التعديلات…</string>
|
||||||
|
<string name="options">الخيارات</string>
|
||||||
|
<string name="ok">موافق</string>
|
||||||
|
<string name="yes">نعم</string>
|
||||||
|
<string name="no">لا</string>
|
||||||
|
<string name="edit">تحرير</string>
|
||||||
|
<string name="dialog_input_placeholder">القيمة</string>
|
||||||
|
<string name="reset">إعادة التعيين</string>
|
||||||
|
<string name="share">مشاركة</string>
|
||||||
|
<string name="patch">تعديل</string>
|
||||||
|
<string name="select_from_storage">اختر من التخزين</string>
|
||||||
|
<string name="select_from_storage_description">اختر ملف APK من التخزين باستخدام منتقي الملفات</string>
|
||||||
|
<string name="suggested_version_info">الإصدار الموصى به: %s</string>
|
||||||
|
<string name="type_anything">اكتب أي شيء للمتابعة</string>
|
||||||
|
<string name="search">البحث عن التعديلات…</string>
|
||||||
|
<string name="apply">تطبيق</string>
|
||||||
|
<string name="help">مساعدة</string>
|
||||||
|
<string name="back">رجوع</string>
|
||||||
|
<string name="warning">تحذير</string>
|
||||||
|
<string name="add">إضافة</string>
|
||||||
|
<string name="close">إغلاق</string>
|
||||||
|
<string name="clear">مسح</string>
|
||||||
|
<string name="system">النظام</string>
|
||||||
|
<string name="light">فاتح</string>
|
||||||
|
<string name="dark">داكن</string>
|
||||||
|
<string name="appearance">المظهر</string>
|
||||||
|
<string name="networking">الشبكة</string>
|
||||||
|
<string name="allow_metered_networks">السماح بالشبكات ذات البيانات المحدودة</string>
|
||||||
|
<string name="allow_metered_networks_description">يسمح بالتحديثات التلقائية على الشبكات ذات البيانات المحدودة.\n قد يستمر التطبيق في التحذير بشأن الشبكات ذات البيانات المحدودة للعمليات اليدوية.</string>
|
||||||
|
<string name="downloaded_apps">التطبيقات التي تم تنزيلها</string>
|
||||||
|
<string name="process_runtime">تشغيل أداة التعديل في عملية أخرى (تجريبي)</string>
|
||||||
|
<string name="process_runtime_description">هذا أسرع ويسمح لـ أداة التعديل باستخدام ذاكرة أكبر</string>
|
||||||
|
<string name="process_runtime_memory_limit">حد ذاكرة عملية أداة التعديل</string>
|
||||||
|
<string name="process_runtime_memory_limit_description">الحد الأقصى للذاكرة التي يمكن أن تستخدمها عملية أداة التعديل (بالميغابايت)</string>
|
||||||
|
<string name="debug_logs_export">تصدير سجلات تصحيح الأخطاء</string>
|
||||||
|
<string name="debug_logs_export_read_failed">فشل قراءة السجلات (رمز الخروج %d)</string>
|
||||||
|
<string name="debug_logs_export_failed">فشل تصدير السجلات</string>
|
||||||
|
<string name="debug_logs_export_success">السجلات المصدرة</string>
|
||||||
|
<string name="api_url">عنوان URL لواجهة برمجة التطبيقات</string>
|
||||||
|
<string name="api_url_description">واجهة برمجة التطبيقات المستخدمة لتنزيل الملفات الضرورية</string>
|
||||||
|
<string name="api_url_dialog_title">تغيير عنوان API</string>
|
||||||
|
<string name="api_url_dialog_description">تغيير عنوان URL لواجهة برمجة التطبيقات لـ ReVanced Manager. يستخدم ReVanced Manager واجهة برمجة التطبيقات لتنزيل التعديلات والتحديثات.</string>
|
||||||
|
<string name="api_url_dialog_warning">يتصل ReVanced Manager بواجهة برمجة التطبيقات لتنزيل التعديلات والتحديثات. تأكد من أنك تثق بها.</string>
|
||||||
|
<string name="api_url_dialog_save">تعيين</string>
|
||||||
|
<string name="api_url_dialog_reset">إعادة تعيين عنوان URL لواجهة برمجة التطبيقات</string>
|
||||||
|
<string name="device">الجهاز</string>
|
||||||
|
<string name="device_android_version">إصدار Android</string>
|
||||||
|
<string name="device_model">الموديل</string>
|
||||||
|
<string name="device_architectures">معماريات وحدة المعالجة المركزية</string>
|
||||||
|
<string name="device_memory_limit">حدود الذاكرة</string>
|
||||||
|
<string name="device_memory_limit_format">%1$d ميغابايت (عادي) - %2$d ميغابايت (كبير)</string>
|
||||||
|
<string name="patches_force_download">فرض تنزيل جميع التعديلات</string>
|
||||||
|
<string name="patches_reset">إعادة تعيين التعديلات</string>
|
||||||
|
<string name="patching">تعديل</string>
|
||||||
|
<string name="signing">توقيع</string>
|
||||||
|
<string name="storage">التخزين</string>
|
||||||
|
<string name="no_patch_found">لا يمكن العثور على أي تعديل. تحقق من التعديلات الخاصة بك</string>
|
||||||
|
<string name="tab_apps">التطبيقات</string>
|
||||||
|
<string name="tab_patches">التعديلات</string>
|
||||||
|
<string name="delete">حذف</string>
|
||||||
|
<string name="refresh">تحديث</string>
|
||||||
|
<string name="continue_anyways">متابعة على أي حال</string>
|
||||||
|
<string name="download_another_version">تنزيل إصدار آخر</string>
|
||||||
|
<string name="download_app">تنزيل التطبيق</string>
|
||||||
|
<string name="download_apk">تنزيل ملف APK</string>
|
||||||
|
<string name="patches_download_fail">فشل تنزيل التعديلات: %s</string>
|
||||||
|
<string name="patches_replace_fail">فشل استيراد التعديلات: %s</string>
|
||||||
|
<string name="no_patched_apps_found">لم يتم العثور على تطبيقات معدلة</string>
|
||||||
|
<string name="tap_on_patches">اضغط على التعديلات للحصول على مزيد من المعلومات عنها</string>
|
||||||
|
<string name="patches_selected">تم تحديد %s</string>
|
||||||
|
<string name="incompatible_patches">تعديلات غير متوافقة</string>
|
||||||
|
<string name="universal_patches">تعديلات شاملة</string>
|
||||||
|
<string name="patch_selection_reset_toast">تم إعادة تعيين تحديد التعديل والخيارات إلى الإعدادات الافتراضية الموصى بها</string>
|
||||||
|
<string name="patch_options_reset_toast">تم إعادة تعيين خيارات التعديل</string>
|
||||||
|
<string name="non_suggested_version_warning_title">إصدار غير موصى به</string>
|
||||||
|
<string name="non_suggested_version_warning_description">"إصدار التطبيق الذي حددته لا يتطابق مع الإصدار الموصى به.\nالرجاء استخدام الإصدار الموصى به: %s\n\nللمتابعة على أي حال، قم بتعطيل \"يتطلب إصدار التطبيق الموصى به\" في الإعدادات المتقدمة."</string>
|
||||||
|
<string name="selection_warning_title">هل تريد التوقف عن استخدام الإعدادات الافتراضية؟</string>
|
||||||
|
<string name="selection_warning_description">"يوصى باستخدام تحديد التعديل والخيارات الافتراضية. قد يؤدي تغييرها إلى مشاكل غير متوقعة.\n\nتحتاج إلى تشغيل \"السماح بتغيير تحديد التعديل والخيارات\" في الإعدادات المتقدمة قبل تبديل التعديلات."</string>
|
||||||
|
<string name="universal_patch_warning_description">"التعديلات الشاملة لها استخدام أكثر عمومية ولا تعمل بنفس الموثوقية مثل التعديلات التي تستهدف تطبيقات معينة. قد تواجه مشاكل أثناء استخدامها.\n\nتحتاج إلى تشغيل \"السماح باستخدام التعديلات الشاملة\" في الإعدادات المتقدمة قبل استخدام التعديلات الشاملة."</string>
|
||||||
|
<string name="this_version">هذا الإصدار</string>
|
||||||
|
<string name="universal">أي تطبيق</string>
|
||||||
|
<string name="search_patches">البحث عن تعديلات</string>
|
||||||
|
<string name="app_version_not_compatible">"هذا التعديل غير متوافق مع إصدار التطبيق المحدد (%1$s)\n\nإنه متوافق فقط مع الإصدار (الإصدارات) التالية: %2$s"</string>
|
||||||
|
<string name="continue_with_version">هل تريد المتابعة بهذا الإصدار؟</string>
|
||||||
|
<string name="version_not_compatible">ليست كل التعديلات متوافقة مع هذا الإصدار (%s). هل تريد المتابعة على أي حال؟</string>
|
||||||
|
<string name="download_application">هل تريد تنزيل التطبيق؟</string>
|
||||||
|
<string name="app_not_installed">التطبيق الذي حددته غير مثبت. هل تريد تنزيله؟</string>
|
||||||
|
<string name="failed_to_load_apk">فشل تحميل APK</string>
|
||||||
|
<string name="loading">جارٍ التحميل…</string>
|
||||||
|
<string name="not_installed">غير مثبت</string>
|
||||||
|
<string name="installed">مثبت</string>
|
||||||
|
<string name="app_info">معلومات التطبيق</string>
|
||||||
|
<string name="uninstall">إلغاء التثبيت</string>
|
||||||
|
<string name="unpatch">إلغاء التعديل</string>
|
||||||
|
<string name="repatch">إعادة التعديل</string>
|
||||||
|
<string name="install_type">نوع التثبيت</string>
|
||||||
|
<string name="package_name">اسم الحزمة</string>
|
||||||
|
<string name="original_package_name">اسم الحزمة الأصلي</string>
|
||||||
|
<string name="applied_patches">التعديلات المطبقة</string>
|
||||||
|
<string name="view_applied_patches">عرض التعديلات المطبقة</string>
|
||||||
|
<string name="default_install">الافتراضي</string>
|
||||||
|
<string name="mount_install">تركيب</string>
|
||||||
|
<string name="mounted">مركّب</string>
|
||||||
|
<string name="not_mounted">غير مركّب</string>
|
||||||
|
<string name="mount">تركيب</string>
|
||||||
|
<string name="unmount">فك تركيب</string>
|
||||||
|
<string name="failed_to_mount">فشل تركيب: %s</string>
|
||||||
|
<string name="failed_to_unmount">فشل فك تركيب: %s</string>
|
||||||
|
<string name="unpatch_app">إلغاء تعديل التطبيق؟</string>
|
||||||
|
<string name="unpatch_description">هل أنت متأكد أنك تريد إلغاء تعديل هذا التطبيق؟</string>
|
||||||
|
<string name="downloader_invalid_version">لم تجلب أداة التنزيل الإصدار الصحيح</string>
|
||||||
|
<string name="downloader_app_not_found">لم تجد أداة التنزيل التطبيق</string>
|
||||||
|
<string name="downloader_error">خطأ في أداة التنزيل: %s</string>
|
||||||
|
<string name="downloader_no_plugins_installed">لم يتم تثبيت أي أداة تنزيل.</string>
|
||||||
|
<string name="downloader_no_plugins_available">يوجد أدوات تنزيل مثبتة ولكن لا يوجد أي منها موثوق به. تحقق من إعداداتك.</string>
|
||||||
|
<string name="already_patched">تم تعديله بالفعل</string>
|
||||||
|
<string name="patch_selector_sheet_filter_title">تصفية</string>
|
||||||
|
<string name="patch_selector_sheet_filter_compat_title">التوافق</string>
|
||||||
|
<string name="string_option_menu_description">المزيد من الخيارات</string>
|
||||||
|
<string name="option_preset_custom_value">قيمة مخصصة</string>
|
||||||
|
<string name="path_selector">تحديد من التخزين</string>
|
||||||
|
<string name="path_selector_parent_dir">المجلد السابق</string>
|
||||||
|
<string name="path_selector_dirs">المجلدات</string>
|
||||||
|
<string name="path_selector_files">الملفات</string>
|
||||||
|
<string name="show_password_field">عرض كلمة المرور</string>
|
||||||
|
<string name="hide_password_field">إخفاء كلمة المرور</string>
|
||||||
|
<string name="installer">أداة التثبيت</string>
|
||||||
|
<string name="install_app">تثبيت</string>
|
||||||
|
<string name="install_app_success">تم تثبيت التطبيق</string>
|
||||||
|
<string name="install_app_fail">فشل تثبيت التطبيق: %s</string>
|
||||||
|
<string name="reinstall_app_fail">فشل إعادة تثبيت التطبيق: %s</string>
|
||||||
|
<string name="uninstall_app_fail">فشل إلغاء تثبيت التطبيق: %s</string>
|
||||||
|
<string name="open_app">فتح</string>
|
||||||
|
<string name="save_apk">حفظ APK</string>
|
||||||
|
<string name="save_apk_success">تم حفظ APK</string>
|
||||||
|
<string name="sign_fail">فشل توقيع APK: %s</string>
|
||||||
|
<string name="save_logs">حفظ السجلات</string>
|
||||||
|
<string name="plugin_activity_dialog_body">مطلوب تفاعل المستخدم للمتابعة مع هذه الإضافة.</string>
|
||||||
|
<string name="select_install_type">تحديد نوع التثبيت</string>
|
||||||
|
<string name="patcher_step_group_preparing">جارٍ التحضير</string>
|
||||||
|
<string name="patcher_step_load_patches">تحميل التعديلات</string>
|
||||||
|
<string name="patcher_step_unpack">قراءة ملف APK</string>
|
||||||
|
<string name="patcher_step_group_patching">جارٍ التعديل</string>
|
||||||
|
<string name="patcher_step_group_saving">جارٍ الحفظ</string>
|
||||||
|
<string name="patcher_step_write_patched">كتابة ملف APK المعدل</string>
|
||||||
|
<string name="patcher_step_sign_apk">توقيع ملف APK المعدل</string>
|
||||||
|
<string name="patcher_notification_title">التعديل قيد التقدم…</string>
|
||||||
|
<string name="patcher_notification_text">انقر للعودة إلى أداة التعديل</string>
|
||||||
|
<string name="patcher_stop_confirm_title">إيقاف أداة التعديل</string>
|
||||||
|
<string name="patcher_stop_confirm_description">هل أنت متأكد أنك تريد إيقاف عملية التعديل؟</string>
|
||||||
|
<string name="patcher_install_in_progress">التثبيت قيد التقدم. يرجى الانتظار</string>
|
||||||
|
<string name="execute_patches">تنفيذ التعديلات</string>
|
||||||
|
<string name="executing_patch">تنفيذ %s</string>
|
||||||
|
<string name="failed_to_execute_patch">فشل تنفيذ %s</string>
|
||||||
|
<string name="step_completed">مكتمل</string>
|
||||||
|
<string name="step_failed">فشل</string>
|
||||||
|
<string name="step_running">قيد التشغيل</string>
|
||||||
|
<string name="step_waiting">انتظار</string>
|
||||||
|
<string name="expand_content">توسيع</string>
|
||||||
|
<string name="collapse_content">طي</string>
|
||||||
|
<string name="drag_handle">إعادة ترتيب</string>
|
||||||
|
<string name="more">المزيد</string>
|
||||||
|
<string name="less">أقل</string>
|
||||||
|
<string name="continue_">متابعة</string>
|
||||||
|
<string name="dismiss">تجاهل</string>
|
||||||
|
<string name="permanent_dismiss">لا تظهر هذا مرة أخرى</string>
|
||||||
|
<string name="donate">تبرع</string>
|
||||||
|
<string name="website">الموقع الإلكتروني</string>
|
||||||
|
<string name="github">GitHub</string>
|
||||||
|
<string name="contact">اتصال</string>
|
||||||
|
<string name="version">الإصدار</string>
|
||||||
|
<string name="submit_feedback">إرسال مشكلة أو ملاحظات</string>
|
||||||
|
<string name="submit_feedback_description">ساعدنا في تحسين هذا التطبيق</string>
|
||||||
|
<string name="developer_options">خيارات المطور</string>
|
||||||
|
<string name="developer_options_description">خيارات لتصحيح الأخطاء</string>
|
||||||
|
<string name="patches_update_success">تم التحديث بنجاح</string>
|
||||||
|
<string name="patches_update_unavailable">لا يوجد تحديث متاح</string>
|
||||||
|
<string name="view_patches">عرض التعديلات</string>
|
||||||
|
<string name="patches_view_any_version">أي إصدار</string>
|
||||||
|
<string name="patches_view_any_package">أي حزمة</string>
|
||||||
|
<string name="patches_delete_single_dialog_description">هل أنت متأكد أنك تريد حذف \"%s\"؟</string>
|
||||||
|
<string name="patches_delete_multiple_dialog_description">هل أنت متأكد أنك تريد حذف التعديلات المحددة؟</string>
|
||||||
|
<string name="about_revanced_manager">حول ReVanced Manager</string>
|
||||||
|
<string name="revanced_manager_description">ReVanced Manager هو تطبيق Android يستخدم ReVanced Patcher لتعديل تطبيقات Android. يتيح لك تنزيل التطبيقات وتعديلها باستخدام تعديلات مخصصة، وإدارة عملية التعديل.</string>
|
||||||
|
<string name="developer_options_taps">%d نقرة متبقية</string>
|
||||||
|
<string name="developer_options_enabled">تم تمكين خيارات المطور</string>
|
||||||
|
<string name="developer_options_already_enabled">خيارات المطور ممكّنة بالفعل</string>
|
||||||
|
<string name="update_available">يتوفر تحديث</string>
|
||||||
|
<string name="current_version">الإصدار الحالي: %s</string>
|
||||||
|
<string name="new_version">الإصدار الجديد: %s</string>
|
||||||
|
<string name="ready_to_install_update">جاهز لتثبيت التحديث</string>
|
||||||
|
<string name="update_completed">تم تثبيت التحديث</string>
|
||||||
|
<string name="install_update_manager_failed">فشل تثبيت التحديث</string>
|
||||||
|
<string name="manual_update_check">التحقق من وجود تحديثات</string>
|
||||||
|
<string name="manual_update_check_description">التحقق اليدوي من توفر تحديثات</string>
|
||||||
|
<string name="update_checking_manager">التحقق من التحديثات عند التشغيل</string>
|
||||||
|
<string name="update_checking_manager_description">التحقق من وجود إصدارات جديدة من ReVanced Manager عند بدء التطبيق</string>
|
||||||
|
<string name="manager_prereleases">استخدام إصدارات ما قبل الإطلاق</string>
|
||||||
|
<string name="manager_prereleases_description">استخدام إصدارات ما قبل الإطلاق من ReVanced Manager</string>
|
||||||
|
<string name="changelog">عرض سجل التغييرات</string>
|
||||||
|
<string name="changelog_loading">جارٍ تحميل سجل التغييرات</string>
|
||||||
|
<string name="changelog_download_fail">فشل تنزيل سجل التغييرات: %s</string>
|
||||||
|
<string name="changelog_description">اطلع على آخر التغييرات في هذا التحديث</string>
|
||||||
|
<string name="battery_optimization_notification">يجب إيقاف تحسينات البطارية لكي يعمل ReVanced Manager بشكل صحيح في الخلفية. انقر هنا لإيقاف التحسينات.</string>
|
||||||
|
<string name="installing_manager_update">جارٍ تثبيت التحديث…</string>
|
||||||
|
<string name="downloading_manager_update">جارٍ تنزيل التحديث…</string>
|
||||||
|
<string name="download_manager_failed">فشل تنزيل التحديث: %s</string>
|
||||||
|
<string name="cancel">إلغاء</string>
|
||||||
|
<string name="save">حفظ</string>
|
||||||
|
<string name="save_with_count">حفظ (%1$s)</string>
|
||||||
|
<string name="update">تحديث</string>
|
||||||
|
<string name="empty">فارغ</string>
|
||||||
|
<string name="installing_message">"انقر على <b>تحديث</b> عندما يطلب ذلك.
|
||||||
|
سيتم إغلاق ReVanced Manager عند التحديث."</string>
|
||||||
|
<string name="no_changelogs_found">لم يتم العثور على سجلات التغييرات</string>
|
||||||
|
<string name="just_now">الآن</string>
|
||||||
|
<string name="minutes_ago">منذ %s دقيقة</string>
|
||||||
|
<string name="hours_ago">منذ %s ساعة</string>
|
||||||
|
<string name="days_ago">منذ %s يوم</string>
|
||||||
|
<string name="invalid_date">تاريخ غير صالح</string>
|
||||||
|
<string name="disable_battery_optimization">تعطيل تحسين البطارية</string>
|
||||||
|
<string name="input_dialog_value_invalid">قيمة غير صالحة</string>
|
||||||
|
<string name="option_required">هذا الخيار مطلوب</string>
|
||||||
|
<string name="required_options_screen">الخيارات المطلوبة</string>
|
||||||
|
<string name="failed_to_check_updates">فشل التحقق من توفر تحديثات: %s</string>
|
||||||
|
<string name="no_update_available">لا يوجد تحديث متاح</string>
|
||||||
|
<string name="update_check">جارٍ التحقق من توفر تحديثات…</string>
|
||||||
|
<string name="dismiss_temporary">ليس الآن</string>
|
||||||
|
<string name="update_available_dialog_description">يتوفر إصدار جديد من ReVanced Manager (%s).</string>
|
||||||
|
<string name="failed_to_download_update">فشل تنزيل التحديث: %s</string>
|
||||||
|
<string name="download">تنزيل</string>
|
||||||
|
<string name="download_confirmation_metered">"أنت متصل حالياً بشبكة محدودة البيانات، وقد يتم تطبيق رسوم بيانات من مزود الخدمة الخاص بك.
|
||||||
|
|
||||||
|
هل ما زلت تريد المتابعة؟"</string>
|
||||||
|
<string name="download_update_confirmation">تنزيل التحديث؟</string>
|
||||||
|
<string name="no_contributors_found">لم يتم العثور على مساهمين</string>
|
||||||
|
<string name="select">تحديد</string>
|
||||||
|
<string name="select_deselect_all">تحديد أو إلغاء تحديد الكل</string>
|
||||||
|
<string name="select_patches_type_dialog_description">إضافة تعديلات جديدة من عنوان URL أو الملفات المحلية</string>
|
||||||
|
<string name="local_patches_description">إضافة تعديلات من التخزين المحلي.</string>
|
||||||
|
<string name="remote_patches_description">إضافة تعديلات من عنوان URL. يمكن للتعديلات التحديث تلقائيًا.</string>
|
||||||
|
<string name="recommended">مُوصَى به</string>
|
||||||
|
<string name="installation_failed_dialog_title">فشل التثبيت</string>
|
||||||
|
<string name="installation_cancelled_dialog_title">تم إلغاء التثبيت</string>
|
||||||
|
<string name="installation_blocked_dialog_title">تم حظر التثبيت</string>
|
||||||
|
<string name="installation_conflict_dialog_title">تعارض التثبيت</string>
|
||||||
|
<string name="installation_incompatible_dialog_title">تثبيت غير متوافق</string>
|
||||||
|
<string name="installation_invalid_dialog_title">تثبيت غير صالح</string>
|
||||||
|
<string name="installation_storage_issue_dialog_title">مساحة تخزين غير كافية</string>
|
||||||
|
<string name="installation_timeout_dialog_title">انتهت مهلة التثبيت</string>
|
||||||
|
<string name="installation_failed_description">فشل التثبيت لسبب غير معروف. هل تريد المحاولة مرة أخرى؟</string>
|
||||||
|
<string name="installation_aborted_description">تم إلغاء التثبيت يدوياً. هل تريد المحاولة مرة أخرى؟</string>
|
||||||
|
<string name="installation_blocked_description">تم حظر التثبيت. راجع إعدادات أمان جهازك وحاول مرة أخرى.</string>
|
||||||
|
<string name="installation_conflict_description">تم منع التثبيت بسبب وجود تثبيت سابق للتطبيق. هل تريد إلغاء تثبيت التطبيق المثبت والمحاولة مرة أخرى؟</string>
|
||||||
|
<string name="installation_incompatible_description">التطبيق غير متوافق مع هذا الجهاز. استخدم ملف APK متوافق مع هذا الجهاز وحاول مرة أخرى.</string>
|
||||||
|
<string name="installation_invalid_description">التطبيق غير صالح. هل تريد إلغاء تثبيت التطبيق والمحاولة مرة أخرى؟</string>
|
||||||
|
<string name="installation_storage_issue_description">لا يمكن تثبيت التطبيق بسبب عدم كفاية مساحة التخزين. قم بتحرير بعض المساحة وحاول مرة أخرى.</string>
|
||||||
|
<string name="installation_timeout_description">استغرق التثبيت وقتاً طويلاً جداً. هل تريد المحاولة مرة أخرى؟</string>
|
||||||
|
<string name="reinstall">إعادة التثبيت</string>
|
||||||
|
<string name="show">عرض</string>
|
||||||
|
<string name="debugging">تصحيح الأخطاء</string>
|
||||||
|
<string name="about_device">حول الجهاز</string>
|
||||||
|
<string name="enter_url">أدخل عنوان URL</string>
|
||||||
|
<string name="next">التالي</string>
|
||||||
|
<string name="auto_update">تحديث تلقائي</string>
|
||||||
|
<string name="add_patches">إضافة تعديلات</string>
|
||||||
|
<string name="auto_update_description">التحديث تلقائيًا عند توفر إصدار جديد</string>
|
||||||
|
<string name="patches_prereleases">استخدام إصدارات ما قبل الإطلاق</string>
|
||||||
|
<string name="patches_prereleases_description">استخدام إصدارات ما قبل الإطلاق من %s</string>
|
||||||
|
<string name="patches_url">عنوان URL للتعديلات</string>
|
||||||
|
<string name="incompatible_patches_dialog">"هذه التعديلات غير متوافقة مع إصدار التطبيق المحدد (%1$s).
|
||||||
|
|
||||||
|
انقر على التعديلات لمشاهدة المزيد من التفاصيل."</string>
|
||||||
|
<string name="incompatible_patch">تعديل غير متوافق</string>
|
||||||
|
<string name="any_version">أيها</string>
|
||||||
|
<string name="never_show_again">لا تعرض مرة أخرى أبدًا</string>
|
||||||
|
<string name="show_manager_update_dialog_on_launch">عرض رسالة التحديث عند التشغيل</string>
|
||||||
|
<string name="show_manager_update_dialog_on_launch_description">عرض إشعار منبثق عند توفر تحديث جديد عند بدء التشغيل</string>
|
||||||
|
<string name="failed_to_import_keystore">فشل استيراد مخزن المفاتيح</string>
|
||||||
|
<string name="export">تصدير</string>
|
||||||
|
<string name="confirm">تأكيد</string>
|
||||||
|
</resources>
|
||||||
16
app/src/main/res/values-as-rIN/strings.xml
Normal file
16
app/src/main/res/values-as-rIN/strings.xml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Strings with new lines must be raw strings, where the string is wrapped in double quotes and new lines are regular line breaks and not \n
|
||||||
|
Raw strings still require escaping embedded double quotes, but single quote characters can be escaped or used as-is.
|
||||||
|
|
||||||
|
Raw strings are required because Crowdin AI translations regularly gets confused and
|
||||||
|
replace \n with an encoded new line character.
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
<string name="summary_key">First \'item\' text\nSecond \"item\" text</string>
|
||||||
|
Good:
|
||||||
|
<string name="summary_key">"First 'item' text
|
||||||
|
Second \"item\" text"</string>
|
||||||
|
|
||||||
|
-->
|
||||||
|
<resources></resources>
|
||||||
455
app/src/main/res/values-az-rAZ/strings.xml
Normal file
455
app/src/main/res/values-az-rAZ/strings.xml
Normal file
@@ -0,0 +1,455 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Strings with new lines must be raw strings, where the string is wrapped in double quotes and new lines are regular line breaks and not \n
|
||||||
|
Raw strings still require escaping embedded double quotes, but single quote characters can be escaped or used as-is.
|
||||||
|
|
||||||
|
Raw strings are required because Crowdin AI translations regularly gets confused and
|
||||||
|
replace \n with an encoded new line character.
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
<string name="summary_key">First \'item\' text\nSecond \"item\" text</string>
|
||||||
|
Good:
|
||||||
|
<string name="summary_key">"First 'item' text
|
||||||
|
Second \"item\" text"</string>
|
||||||
|
|
||||||
|
-->
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">ReVanced Manager</string>
|
||||||
|
<string name="patcher">Patcher sınağı</string>
|
||||||
|
<string name="patches">Yamalar</string>
|
||||||
|
<string name="cli">CLI</string>
|
||||||
|
<string name="manager">Menecer</string>
|
||||||
|
<string name="plugin_host_permission_label">ReVanced Manager plagin hostu</string>
|
||||||
|
<string name="plugin_host_permission_description">ReVanced Manager plaginlərinə girişi idarə etmək üçün istifadə olunur. Buna yalnız ReVanced Manager malikdir.</string>
|
||||||
|
<string name="toast_copied_to_clipboard">Kopyalandı!</string>
|
||||||
|
<string name="copy_to_clipboard">Buferə kopyala</string>
|
||||||
|
<string name="dashboard">İdarə Paneli</string>
|
||||||
|
<string name="settings">Ayarlar</string>
|
||||||
|
<string name="select_app">Tətbiq seçin</string>
|
||||||
|
<string name="patches_count_selected">%1$d/%2$d seçildi</string>
|
||||||
|
<string name="new_downloader_plugins_notification">Yeni yükləyici plaginlər mövcuddur. Onları konfiqurasiya etmək üçün bura klikləyin.</string>
|
||||||
|
<string name="unsupported_architecture_warning">Bu cihaz arxitekturasında yamaqlama dəstəklənmir və çox güman ki uğursuz olacaq.</string>
|
||||||
|
<string name="import_">İdxal</string>
|
||||||
|
<string name="import_patches">Yamaqları idxal et</string>
|
||||||
|
<string name="file_field_set">Seçildi</string>
|
||||||
|
<string name="file_field_not_set">Seçilməyib</string>
|
||||||
|
<string name="field_not_set">Təyin edilməyib</string>
|
||||||
|
<string name="patches_missing">Əskik</string>
|
||||||
|
<string name="patches_error">Xəta</string>
|
||||||
|
<string name="patches_error_description">Yamaqlar yüklənə bilmədi. Xətanı görmək üçün klikləyin</string>
|
||||||
|
<string name="patches_not_downloaded">Yamaqlar yüklənməyib. Yükləmək üçün bura klikləyin</string>
|
||||||
|
<string name="patches_name_default">Yamaqlar</string>
|
||||||
|
<string name="patches_name_fallback">Adsız</string>
|
||||||
|
<string name="android_11_bug_dialog_title">Android 11 səhvi</string>
|
||||||
|
<string name="android_11_bug_dialog_description">Tətbiqin quraşdırılması icazəsi, Android 11 sistemində istifadəçi təcrübəsinə mənfi təsir göstərəcək bir səhvin qarşısını almaq üçün əvvəlcədən verilməlidir.</string>
|
||||||
|
<string name="no_network_toast">İnternet bağlantısı yoxdur</string>
|
||||||
|
<string name="selected_app_meta_any_version">Hər hansı mövcud versiya</string>
|
||||||
|
<string name="app_source_dialog_title">Mənbə seçin</string>
|
||||||
|
<string name="app_source_dialog_option_auto">Avtomatik</string>
|
||||||
|
<string name="app_source_dialog_option_auto_description">Tətbiqi yükləmək üçün bütün mövcud yükləyicilərdən istifadə edin</string>
|
||||||
|
<string name="app_source_dialog_option_auto_unavailable">Heç bir plagin mövcud deyil</string>
|
||||||
|
<string name="app_source_dialog_option_installed_no_root">Quraşdırılmış tətbiqlər root icazəsi olmadan yenidən yamaqlana bilməz</string>
|
||||||
|
<string name="app_source_dialog_option_installed_version_not_suggested">Versiya %s təklif olunan versiyaya uyğun gəlmir</string>
|
||||||
|
<string name="patch_item_description">Tətbiqi yamaqlamağa başlayın</string>
|
||||||
|
<string name="patch_selector_item">Yamaqları seçin</string>
|
||||||
|
<string name="patch_selector_item_description">%d yamaq seçildi</string>
|
||||||
|
<string name="no_patches_selected">Heç bir yamaq seçilməyib</string>
|
||||||
|
<string name="network_unavailable_warning">Cihazınız internetə qoşulmayıb. Yükləmə daha sonra uğursuz olacaq.</string>
|
||||||
|
<string name="network_metered_warning">Hazırda ölçülü bir bağlantıdasınız. Xidmət təminatçınızdan məlumat haqları tətbiq oluna bilər.</string>
|
||||||
|
<string name="apk_source_selector_item">APK mənbəsini seçin</string>
|
||||||
|
<string name="apk_source_auto">Bütün APK yükləyicilərindən istifadə olunur</string>
|
||||||
|
<string name="apk_source_downloader">%s istifadə olunur</string>
|
||||||
|
<string name="apk_source_installed">Quraşdırılmış APK istifadə olunur</string>
|
||||||
|
<string name="apk_source_local">Yerli APK faylı istifadə olunur</string>
|
||||||
|
<string name="legacy_import_failed">Köhnə parametrlər idxal edilə bilmədi</string>
|
||||||
|
<string name="auto_updates_dialog_title">Yeniləmələri konfiqurasiya edin</string>
|
||||||
|
<string name="auto_updates_dialog_description">ReVanced Manager-in aşağıdakı komponentlər üçün vaxtaşırı yeniləmələri yoxlamasını istəyirsinizmi?</string>
|
||||||
|
<string name="auto_updates_dialog_manager">ReVanced Manager</string>
|
||||||
|
<string name="auto_updates_dialog_patches">ReVanced Yamaqları</string>
|
||||||
|
<string name="auto_updates_dialog_note">Bu parametrlər daha sonra dəyişdirilə bilər.</string>
|
||||||
|
<string name="general">Ümumi</string>
|
||||||
|
<string name="general_description">Dil, mövzu, dinamik rəng</string>
|
||||||
|
<string name="updates">Yeniləmələr</string>
|
||||||
|
<string name="updates_description">Yeniləmələri yoxlayın və dəyişiklik qeydlərinə baxın</string>
|
||||||
|
<string name="downloads">Yükləmələr</string>
|
||||||
|
<string name="downloads_description">Yükləyici plaginlər və yüklənmiş tətbiqlər</string>
|
||||||
|
<string name="import_export">İdxal & ixrac</string>
|
||||||
|
<string name="import_export_description">Açar anbarı, yamaq seçimləri və seçimi</string>
|
||||||
|
<string name="advanced">Qabaqcıl</string>
|
||||||
|
<string name="advanced_description">API URL, yaddaş limiti, sazlama</string>
|
||||||
|
<string name="about">Haqqında</string>
|
||||||
|
<string name="opensource_licenses">Açıq mənbə lisenziyaları</string>
|
||||||
|
<string name="opensource_licenses_description">Bu tətbiqi yaratmaq üçün istifadə olunan bütün kitabxanaları göstər</string>
|
||||||
|
<string name="contributors">İştirakçılar</string>
|
||||||
|
<string name="contributors_description">ReVanced iştirakçılarına baxın</string>
|
||||||
|
<string name="dynamic_color">Dinamik rəng</string>
|
||||||
|
<string name="dynamic_color_description">Rəngləri divar kağızına uyğunlaşdırın</string>
|
||||||
|
<string name="pure_black_theme">Saf qara mövzu</string>
|
||||||
|
<string name="pure_black_theme_description">Qaranlıq mövzu üçün saf qara fonlardan istifadə edin</string>
|
||||||
|
<string name="theme">Mövzu</string>
|
||||||
|
<string name="theme_description">Açıq və ya qaranlıq mövzu arasında seçim edin</string>
|
||||||
|
<string name="language">Dil</string>
|
||||||
|
<string name="language_description">Tətbiqin göstərilmə dilini seçin</string>
|
||||||
|
<string name="language_system_default">Sistem standartı</string>
|
||||||
|
<string name="safeguards">Mühafizələr</string>
|
||||||
|
<string name="patch_compat_check">Versiya uyğunluq yoxlamasını söndür</string>
|
||||||
|
<string name="patch_compat_check_description">Yamaqları uyğun tətbiq versiyalarına məhdudlaşdırmayın</string>
|
||||||
|
<string name="patch_compat_check_confirmation">"Uyğun olmayan yamaqların seçilməsi tətbiqin pozulmasına səbəb ola bilər.
|
||||||
|
|
||||||
|
Yenə də davam etmək istəyirsiniz?"</string>
|
||||||
|
<string name="suggested_version_safeguard">Təklif olunan tətbiq versiyasını tələb et</string>
|
||||||
|
<string name="suggested_version_safeguard_description">Təklif olunan tətbiq versiyasının seçimini tətbiq edin</string>
|
||||||
|
<string name="suggested_version_safeguard_confirmation">"Təklif olunan versiya olmayan bir tətbiqin seçilməsi gözlənilməz problemlərə səbəb ola bilər.
|
||||||
|
|
||||||
|
Yenə də davam etmək istəyirsiniz?"</string>
|
||||||
|
<string name="patch_selection_safeguard">Yamaq seçimini və seçimlərini dəyişməyə icazə ver</string>
|
||||||
|
<string name="patch_selection_safeguard_description">Yamaqları seçməyə və ya seçimi ləğv etməyə, həmçinin seçimlərin fərdiləşdirilməsinə mane olmayın</string>
|
||||||
|
<string name="patch_selection_safeguard_confirmation">"Yamaqların seçimini dəyişmək gözlənilməz problemlərə səbəb ola bilər.
|
||||||
|
|
||||||
|
Yenə də aktiv edilsin?"</string>
|
||||||
|
<string name="universal_patches_safeguard">Universal yamaqlardan istifadə etməyə icazə ver</string>
|
||||||
|
<string name="universal_patches_safeguard_description">Universal yamaqlardan istifadə etməyə mane olmayın</string>
|
||||||
|
<string name="universal_patches_safeguard_confirmation">"Universal yamaqlar, xüsusi tətbiqləri hədəfləyənlər qədər yaxşı sınaqdan keçirilməyib.
|
||||||
|
|
||||||
|
Yenə də aktiv edilsin?"</string>
|
||||||
|
<string name="import_keystore">Açar anbarını idxal et</string>
|
||||||
|
<string name="import_keystore_description">Fərdi açar anbarını idxal et</string>
|
||||||
|
<string name="import_keystore_dialog_title">Açar anbarının etimadnaməsini daxil edin</string>
|
||||||
|
<string name="import_keystore_dialog_description">Onu idxal etmək üçün açar anbarının etimadnaməsini daxil etməlisiniz.</string>
|
||||||
|
<string name="import_keystore_dialog_alias_field">İstifadəçi adı (Ləqəb)</string>
|
||||||
|
<string name="import_keystore_dialog_password_field">Parol</string>
|
||||||
|
<string name="import_keystore_dialog_button">İdxal</string>
|
||||||
|
<string name="import_keystore_wrong_credentials">Səhv açar anbarı etimadnaməsi</string>
|
||||||
|
<string name="import_keystore_success">İdxal edilmiş açar anbarı</string>
|
||||||
|
<string name="export_keystore">Açar anbarını ixrac et</string>
|
||||||
|
<string name="export_keystore_description">Cari açar anbarını ixrac et</string>
|
||||||
|
<string name="export_keystore_unavailable">İxrac ediləcək açar anbarı yoxdur</string>
|
||||||
|
<string name="export_keystore_success">İxrac edilmiş açar anbarı</string>
|
||||||
|
<string name="regenerate_keystore">Açar anbarını yenidən yarat</string>
|
||||||
|
<string name="regenerate_keystore_description">Yeni açar anbarı yarat</string>
|
||||||
|
<string name="regenerate_keystore_dialog_description">"Yamaqlama prosesi zamanı menecerin istifadə edəcəyi açar anbarını yenidən yaratmaq üzrəsiniz.
|
||||||
|
|
||||||
|
Bu mənbədən əvvəlcədən quraşdırılmış tətbiqləri yeniləyə bilməyəcəksiniz."</string>
|
||||||
|
<string name="regenerate_keystore_success">Açar anbarı uğurla dəyişdirildi</string>
|
||||||
|
<string name="import_patch_selection">Yamaq seçimini idxal et</string>
|
||||||
|
<string name="import_patch_selection_description">JSON faylından yamaq seçimini idxal et</string>
|
||||||
|
<string name="import_patch_selection_fail">Yamaq seçimi idxal edilə bilmədi: %s</string>
|
||||||
|
<string name="import_patch_selection_success">İdxal edilmiş yamaq seçimi</string>
|
||||||
|
<string name="export_patch_selection">Yamaq seçimini ixrac et</string>
|
||||||
|
<string name="export_patch_selection_description">Yamaq seçimini JSON faylına ixrac et</string>
|
||||||
|
<string name="export_patch_selection_fail">Yamaq seçimi ixrac edilə bilmədi: %s</string>
|
||||||
|
<string name="export_patch_selection_success">İxrac edilmiş yamaq seçimi</string>
|
||||||
|
<string name="reset_patch_selection">Yamaq seçimini sıfırla</string>
|
||||||
|
<string name="reset_patch_selection_description">Saxlanmış yamaq seçimini sıfırla</string>
|
||||||
|
<string name="reset_patch_options">Yamaq seçimlərini sıfırla</string>
|
||||||
|
<string name="reset_patch_options_description">Saxlanmış yamaq seçimlərini sıfırla</string>
|
||||||
|
<string name="reset_patch_selection_success">Yamaq seçimi sıfırlandı</string>
|
||||||
|
<string name="patch_selection_reset_all">Yamaq seçimini qlobal olaraq sıfırla</string>
|
||||||
|
<string name="patch_selection_reset_all_dialog_description">Bütün yamaq seçimlərini sıfırlamaq üzrəsiniz. Hər bir yamağı yenidən əl ilə seçməli olacaqsınız.</string>
|
||||||
|
<string name="patch_selection_reset_all_description">Bütün yamaq seçimlərini sıfırlayır</string>
|
||||||
|
<string name="patch_selection_reset_package">Tətbiq üçün yamaq seçimini sıfırla</string>
|
||||||
|
<string name="patch_selection_reset_package_dialog_description">Siz \"%s\" tətbiqi üçün yamaq seçimini sıfırlamaq üzrəsiniz. Hər bir yamağı yenidən əl ilə seçməli olacaqsınız.</string>
|
||||||
|
<string name="patch_selection_reset_package_description">Tək bir tətbiq üçün yamaq seçimini sıfırlayır</string>
|
||||||
|
<string name="patch_selection_reset_patches">Yamaq seçimini sıfırla (tək)</string>
|
||||||
|
<string name="patch_selection_reset_patches_dialog_description">\"%s\" üçün yamaq seçimini sıfırlamaq üzrəsiniz. Hər bir yamağı yenidən əl ilə seçməli olacaqsınız.</string>
|
||||||
|
<string name="patch_selection_reset_patches_description">Müəyyən yamaq kolleksiyası üçün yamaq seçimini sıfırlayır</string>
|
||||||
|
<string name="patch_options_reset_package">Tətbiq üçün yamaq seçimlərini sıfırla</string>
|
||||||
|
<string name="patch_options_reset_package_dialog_description">\"%s\" tətbiqi üçün yamaq seçimlərini sıfırlamaq üzrəsiniz. Hər bir seçimi yenidən tətbiq etməli olacaqsınız.</string>
|
||||||
|
<string name="patch_options_reset_package_description">Tək bir tətbiq üçün yamaq seçimlərini sıfırlayır</string>
|
||||||
|
<string name="patch_options_reset_patches">Yamaq seçimlərini sıfırla (tək)</string>
|
||||||
|
<string name="patch_options_reset_patches_dialog_description">\"%s\" üçün yamaq seçimlərini sıfırlamaq üzrəsiniz. Hər bir seçimi yenidən tətbiq etməli olacaqsınız.</string>
|
||||||
|
<string name="patch_options_reset_patches_description">Müəyyən yamaq kolleksiyası üçün yamaq seçimlərini sıfırlayır</string>
|
||||||
|
<string name="patch_options_reset_all">Yamaq seçimlərini qlobal olaraq sıfırla</string>
|
||||||
|
<string name="patch_options_reset_all_dialog_description">Bütün yamaq seçimlərini sıfırlamaq üzrəsiniz. Hər bir seçimi yenidən tətbiq etməli olacaqsınız.</string>
|
||||||
|
<string name="patch_options_reset_all_description">Bütün yamaq seçimlərini sıfırlayır</string>
|
||||||
|
<string name="downloader_plugins">Plaginlər</string>
|
||||||
|
<string name="downloader_plugin_state_trusted">Etibarlı</string>
|
||||||
|
<string name="downloader_plugin_state_failed">Yüklənə bilmədi. Daha çox təfərrüat üçün klikləyin</string>
|
||||||
|
<string name="downloader_plugin_state_untrusted">Etibarsız</string>
|
||||||
|
<string name="downloader_plugin_trust_dialog_title">Plaginə güvənilsin?</string>
|
||||||
|
<string name="downloader_plugin_revoke_trust_dialog_title">Güvən ləğv edilsin?</string>
|
||||||
|
<string name="downloader_plugin_trust_dialog_body">Davam etmək bu plaginin sisteminizdə işləməsinə icazə verəcək.\n\nYalnız bu plagini ona güvənirsinizsə aktiv edin. Plaginlər istənilən kodu icra edə bilər və cihazınızı təhlükəyə sala bilər.</string>
|
||||||
|
<string name="downloader_plugin_trust_dialog_signature">İmza:\n\n%s</string>
|
||||||
|
<string name="downloader_plugin_trust_dialog_plugin">Plagin:\n%s</string>
|
||||||
|
<string name="downloader_plugin_delete_apps_title">Seçilmiş tətbiqləri sil</string>
|
||||||
|
<string name="downloader_plugin_delete_apps_description">Seçilmiş tətbiqləri silmək istədiyinizə əminsiniz?</string>
|
||||||
|
<string name="downloader_settings_no_apps">Yüklənmiş tətbiq tapılmadı.</string>
|
||||||
|
<string name="search_apps">Tətbiqləri axtar…</string>
|
||||||
|
<string name="loading_body">Yüklənir…</string>
|
||||||
|
<string name="downloading_patches">Yamaqlar yüklənir…</string>
|
||||||
|
<string name="options">Seçimlər</string>
|
||||||
|
<string name="ok">OK</string>
|
||||||
|
<string name="yes">Bəli</string>
|
||||||
|
<string name="no">Xeyr</string>
|
||||||
|
<string name="edit">Redaktə et</string>
|
||||||
|
<string name="dialog_input_placeholder">Dəyər</string>
|
||||||
|
<string name="reset">Sıfırla</string>
|
||||||
|
<string name="share">Paylaş</string>
|
||||||
|
<string name="patch">Yamaq</string>
|
||||||
|
<string name="select_from_storage">Yaddaşdan seç</string>
|
||||||
|
<string name="select_from_storage_description">Fayl seçicidən istifadə edərək yaddaşdan bir APK faylı seçin</string>
|
||||||
|
<string name="suggested_version_info">Tövsiyə olunan versiya: %s</string>
|
||||||
|
<string name="type_anything">Davam etmək üçün hər hansı bir şey yazın</string>
|
||||||
|
<string name="search">Yamaqları axtar…</string>
|
||||||
|
<string name="apply">Tətbiq et</string>
|
||||||
|
<string name="help">Kömək</string>
|
||||||
|
<string name="back">Geri</string>
|
||||||
|
<string name="warning">Xəbərdarlıq</string>
|
||||||
|
<string name="add">Əlavə et</string>
|
||||||
|
<string name="close">Bağla</string>
|
||||||
|
<string name="clear">Təmizlə</string>
|
||||||
|
<string name="system">Sistem</string>
|
||||||
|
<string name="light">Açıq</string>
|
||||||
|
<string name="dark">Tünd</string>
|
||||||
|
<string name="appearance">Görünüş</string>
|
||||||
|
<string name="networking">Şəbəkə</string>
|
||||||
|
<string name="allow_metered_networks">Ölçülmüş şəbəkələrə icazə ver</string>
|
||||||
|
<string name="allow_metered_networks_description">Ölçülmüş şəbəkələrdə avtomatik yeniləmələrə icazə verir.\n Tətbiq əl əməliyyatları üçün hələ də ölçülmüş şəbəkələr haqqında xəbərdarlıq edə bilər.</string>
|
||||||
|
<string name="downloaded_apps">Yüklənmiş tətbiqlər</string>
|
||||||
|
<string name="process_runtime">Yamaqlayıcını başqa prosesdə işlət (eksperimental)</string>
|
||||||
|
<string name="process_runtime_description">Bu daha sürətli və Yamaqlayıcının daha çox yaddaş istifadə etməsinə imkan verir</string>
|
||||||
|
<string name="process_runtime_memory_limit">Yamaqlayıcı prosesinin yaddaş limiti</string>
|
||||||
|
<string name="process_runtime_memory_limit_description">Yamaqlayıcı prosesinin istifadə edə biləcəyi maksimum yaddaş miqdarı (meqabaytlarda)</string>
|
||||||
|
<string name="debug_logs_export">Bərpa qeydlərini ixrac et</string>
|
||||||
|
<string name="debug_logs_export_read_failed">Qeydləri oxumaq alınmadı (çıxış kodu %d)</string>
|
||||||
|
<string name="debug_logs_export_failed">Qeydləri ixrac etmək alınmadı</string>
|
||||||
|
<string name="debug_logs_export_success">İxrac edilmiş qeydlər</string>
|
||||||
|
<string name="api_url">API URL</string>
|
||||||
|
<string name="api_url_description">Lazımi faylları yükləmək üçün istifadə olunan API</string>
|
||||||
|
<string name="api_url_dialog_title">API URL-ni dəyişdir</string>
|
||||||
|
<string name="api_url_dialog_description">ReVanced Manager-ın API URL-ni dəyişdirin. ReVanced Manager yamaqları və yeniləmələri yükləmək üçün API-dən istifadə edir.</string>
|
||||||
|
<string name="api_url_dialog_warning">ReVanced Manager yamaqları və yeniləmələri yükləmək üçün API-yə qoşulur. Ona güvəndiyinizə əmin olun.</string>
|
||||||
|
<string name="api_url_dialog_save">Təyin et</string>
|
||||||
|
<string name="api_url_dialog_reset">API URL-ni sıfırla</string>
|
||||||
|
<string name="device">Cihaz</string>
|
||||||
|
<string name="device_android_version">Android versiyası</string>
|
||||||
|
<string name="device_model">Model</string>
|
||||||
|
<string name="device_architectures">CPU Arxitekturaları</string>
|
||||||
|
<string name="device_memory_limit">Yaddaş limitləri</string>
|
||||||
|
<string name="device_memory_limit_format">%1$dMB (Normal) - %2$dMB (Böyük)</string>
|
||||||
|
<string name="patches_force_download">Bütün yamaqları yükləməyə məcbur et</string>
|
||||||
|
<string name="patches_reset">Yamaqları sıfırla</string>
|
||||||
|
<string name="patching">Yamaqlanır</string>
|
||||||
|
<string name="signing">İmzalanır</string>
|
||||||
|
<string name="storage">Yaddaş</string>
|
||||||
|
<string name="no_patch_found">Yamaq tapılmadı. Yamaqlarınızı yoxlayın</string>
|
||||||
|
<string name="tab_apps">Tətbiqlər</string>
|
||||||
|
<string name="tab_patches">Yamaqlar</string>
|
||||||
|
<string name="delete">Sil</string>
|
||||||
|
<string name="refresh">Yenilə</string>
|
||||||
|
<string name="continue_anyways">Hər halda davam et</string>
|
||||||
|
<string name="download_another_version">Başqa versiyanı yüklə</string>
|
||||||
|
<string name="download_app">Tətbiqi yüklə</string>
|
||||||
|
<string name="download_apk">APK faylını yüklə</string>
|
||||||
|
<string name="patches_download_fail">Yamaqlar yüklənə bilmədi: %s</string>
|
||||||
|
<string name="patches_replace_fail">Yamaqları idxal etmək alınmadı: %s</string>
|
||||||
|
<string name="no_patched_apps_found">Yamaqlanmış tətbiq tapılmadı</string>
|
||||||
|
<string name="tap_on_patches">Yamaqlar haqqında daha çox məlumat almaq üçün onların üzərinə toxunun</string>
|
||||||
|
<string name="patches_selected">%s seçildi</string>
|
||||||
|
<string name="incompatible_patches">Uyğun olmayan yamaqlar</string>
|
||||||
|
<string name="universal_patches">Universal yamaqlar</string>
|
||||||
|
<string name="patch_selection_reset_toast">Yamaq seçimi və seçimləri tövsiyə olunan standartlara qaytarıldı</string>
|
||||||
|
<string name="patch_options_reset_toast">Yamaq seçimləri sıfırlandı</string>
|
||||||
|
<string name="non_suggested_version_warning_title">Tövsiyə olunmayan versiya</string>
|
||||||
|
<string name="non_suggested_version_warning_description">"Seçdiyiniz tətbiqin versiyası tövsiyə olunan versiya ilə uyğun gəlmir.\nZəhmət olmasa tövsiyə olunan versiyadan istifadə edin: %s\n\nHər halda davam etmək üçün, qabaqcıl parametrlərdə \"Tövsiyə olunan tətbiq versiyasını tələb et\" seçimini söndürün."</string>
|
||||||
|
<string name="selection_warning_title">Standartlardan istifadəni dayandırılsın?</string>
|
||||||
|
<string name="selection_warning_description">"Varsayılan yamaq seçimi və seçimlərindən istifadə etmək tövsiyə olunur. Onları dəyişdirmək gözlənilməz problemlərə səbəb ola bilər.\n\nYamaqları dəyişdirməzdən əvvəl qabaqcıl parametrlərdə \"Yamaq seçimini və seçimlərini dəyişdirməyə icazə ver\" seçimini aktiv etməlisiniz."</string>
|
||||||
|
<string name="universal_patch_warning_description">"Universal yamaqların daha ümumi istifadəsi var və xüsusi tətbiqləri hədəfləyən yamaqlar qədər etibarlı işləmir. Onlardan istifadə edərkən problemlərlə qarşılaşa bilərsiniz.\n\nUniversal yamaqlardan istifadə etməzdən əvvəl qabaqcıl parametrlərdə \"Universal yamaqlardan istifadəyə icazə ver\" seçimini aktiv etməlisiniz."</string>
|
||||||
|
<string name="this_version">Bu versiya</string>
|
||||||
|
<string name="universal">Hər hansı tətbiq</string>
|
||||||
|
<string name="search_patches">Yamaqları axtar</string>
|
||||||
|
<string name="app_version_not_compatible">"Bu yamaq seçilmiş tətbiq versiyası (%1$s) ilə uyğun gəlmir.\n\nYalnız aşağıdakı versiya(lar) ilə uyğundur: %2$s"</string>
|
||||||
|
<string name="continue_with_version">Bu versiya ilə davam edilsin?</string>
|
||||||
|
<string name="version_not_compatible">Bütün yamaqlar bu versiya (%s) ilə uyğun gəlmir. Hər halda davam etmək istəyirsiniz?</string>
|
||||||
|
<string name="download_application">Tətbiq yüklənsin?</string>
|
||||||
|
<string name="app_not_installed">Seçdiyiniz tətbiq quraşdırılmayıb. Onu yükləmək istəyirsiniz?</string>
|
||||||
|
<string name="failed_to_load_apk">APK yüklənə bilmədi</string>
|
||||||
|
<string name="loading">Yüklənir…</string>
|
||||||
|
<string name="not_installed">Quraşdırılmayıb</string>
|
||||||
|
<string name="installed">Quraşdırılıb</string>
|
||||||
|
<string name="app_info">Tətbiq məlumatı</string>
|
||||||
|
<string name="uninstall">Sil</string>
|
||||||
|
<string name="unpatch">Yamağı ləğv et</string>
|
||||||
|
<string name="repatch">Yenidən yamaqla</string>
|
||||||
|
<string name="install_type">Quraşdırma növü</string>
|
||||||
|
<string name="package_name">Paket adı</string>
|
||||||
|
<string name="original_package_name">Orijinal paket adı</string>
|
||||||
|
<string name="applied_patches">Tətbiq edilmiş yamalar</string>
|
||||||
|
<string name="view_applied_patches">Tətbiq edilmiş yamalara bax</string>
|
||||||
|
<string name="default_install">İlkin</string>
|
||||||
|
<string name="mount_install">Quraşdır</string>
|
||||||
|
<string name="mounted">Quraşdırılıb</string>
|
||||||
|
<string name="not_mounted">Quraşdırılmayıb</string>
|
||||||
|
<string name="mount">Quraşdır</string>
|
||||||
|
<string name="unmount">Ayır</string>
|
||||||
|
<string name="failed_to_mount">Quraşdırmaq alınmadı: %s</string>
|
||||||
|
<string name="failed_to_unmount">Ayırmaq alınmadı: %s</string>
|
||||||
|
<string name="unpatch_app">Tətbiqi yamasız et?</string>
|
||||||
|
<string name="unpatch_description">Bu tətbiqi yamasız etmək istədiyinizə əminsiniz?</string>
|
||||||
|
<string name="downloader_invalid_version">Yükləyici düzgün versiyanı əldə etmədi</string>
|
||||||
|
<string name="downloader_app_not_found">Yükləyici tətbiqi tapmadı</string>
|
||||||
|
<string name="downloader_error">Yükləyici xətası: %s</string>
|
||||||
|
<string name="downloader_no_plugins_installed">Heç bir yükləyici quraşdırılmayıb.</string>
|
||||||
|
<string name="downloader_no_plugins_available">Yükləyicilər quraşdırılıb, lakin heç biri etibarlı deyil. Parametrlərinizi yoxlayın.</string>
|
||||||
|
<string name="already_patched">Artıq yamalanıb</string>
|
||||||
|
<string name="patch_selector_sheet_filter_title">Filtr</string>
|
||||||
|
<string name="patch_selector_sheet_filter_compat_title">Uyğunluq</string>
|
||||||
|
<string name="string_option_menu_description">Daha çox seçim</string>
|
||||||
|
<string name="option_preset_custom_value">Fərdi dəyər</string>
|
||||||
|
<string name="path_selector">Saxlama yerindən seç</string>
|
||||||
|
<string name="path_selector_parent_dir">Əvvəlki qovluq</string>
|
||||||
|
<string name="path_selector_dirs">Qovluqlar</string>
|
||||||
|
<string name="path_selector_files">Fayllar</string>
|
||||||
|
<string name="show_password_field">Parolu göstər</string>
|
||||||
|
<string name="hide_password_field">Parolu gizlət</string>
|
||||||
|
<string name="installer">Quraşdırıcı</string>
|
||||||
|
<string name="install_app">Quraşdır</string>
|
||||||
|
<string name="install_app_success">Tətbiq quraşdırıldı</string>
|
||||||
|
<string name="install_app_fail">Tətbiqi quraşdırmaq alınmadı: %s</string>
|
||||||
|
<string name="reinstall_app_fail">Tətbiqi yenidən quraşdırmaq alınmadı: %s</string>
|
||||||
|
<string name="uninstall_app_fail">Tətbiqi silmək alınmadı: %s</string>
|
||||||
|
<string name="open_app">Aç</string>
|
||||||
|
<string name="save_apk">APK-ni Saxla</string>
|
||||||
|
<string name="save_apk_success">APK Saxlandı</string>
|
||||||
|
<string name="sign_fail">APK-ni imzalamaq alınmadı: %s</string>
|
||||||
|
<string name="save_logs">Qeydləri saxla</string>
|
||||||
|
<string name="plugin_activity_dialog_body">Bu plaginlə davam etmək üçün istifadəçi qarşılıqlı əlaqəsi tələb olunur.</string>
|
||||||
|
<string name="select_install_type">Quraşdırma növünü seç</string>
|
||||||
|
<string name="patcher_step_group_preparing">Hazırlanır</string>
|
||||||
|
<string name="patcher_step_load_patches">Yamaları yüklə</string>
|
||||||
|
<string name="patcher_step_unpack">APK faylını oxu</string>
|
||||||
|
<string name="patcher_step_group_patching">Yamalanır</string>
|
||||||
|
<string name="patcher_step_group_saving">Saxlanılır</string>
|
||||||
|
<string name="patcher_step_write_patched">Yamalanmış APK faylını yaz</string>
|
||||||
|
<string name="patcher_step_sign_apk">Yamalanmış APK faylını imzala</string>
|
||||||
|
<string name="patcher_notification_title">Yamalanma davam edir…</string>
|
||||||
|
<string name="patcher_notification_text">Yamalayıcıya qayıtmaq üçün toxun</string>
|
||||||
|
<string name="patcher_stop_confirm_title">Yamalayıcını dayandır</string>
|
||||||
|
<string name="patcher_stop_confirm_description">Yamalanma prosesini dayandırmaq istədiyinizə əminsiniz?</string>
|
||||||
|
<string name="patcher_install_in_progress">Quraşdırma davam edir. Zəhmət olmasa gözləyin</string>
|
||||||
|
<string name="execute_patches">Yamaları icra et</string>
|
||||||
|
<string name="executing_patch">%s icra et</string>
|
||||||
|
<string name="failed_to_execute_patch">%s icra etmək alınmadı</string>
|
||||||
|
<string name="step_completed">tamamlandı</string>
|
||||||
|
<string name="step_failed">uğursuz oldu</string>
|
||||||
|
<string name="step_running">işləyir</string>
|
||||||
|
<string name="step_waiting">gözləyir</string>
|
||||||
|
<string name="expand_content">genişləndir</string>
|
||||||
|
<string name="collapse_content">yığışdır</string>
|
||||||
|
<string name="drag_handle">yenidən sırala</string>
|
||||||
|
<string name="more">Daha çox</string>
|
||||||
|
<string name="less">Daha az</string>
|
||||||
|
<string name="continue_">Davam et</string>
|
||||||
|
<string name="dismiss">Rədd et</string>
|
||||||
|
<string name="permanent_dismiss">Bunu bir daha göstərmə</string>
|
||||||
|
<string name="donate">İanə et</string>
|
||||||
|
<string name="website">Vebsayt</string>
|
||||||
|
<string name="github">GitHub</string>
|
||||||
|
<string name="contact">Əlaqə</string>
|
||||||
|
<string name="version">Versiya</string>
|
||||||
|
<string name="submit_feedback">Problemi və ya rəyi göndər</string>
|
||||||
|
<string name="submit_feedback_description">Bu tətbiqi təkmilləşdirməyə kömək edin</string>
|
||||||
|
<string name="developer_options">Tərtibatçı seçimləri</string>
|
||||||
|
<string name="developer_options_description">Problemləri aradan qaldırmaq üçün seçimlər</string>
|
||||||
|
<string name="patches_update_success">Yeniləmə uğurlu oldu</string>
|
||||||
|
<string name="patches_update_unavailable">Yeniləmə yoxdur</string>
|
||||||
|
<string name="view_patches">Yamalara bax</string>
|
||||||
|
<string name="patches_view_any_version">İstənilən versiya</string>
|
||||||
|
<string name="patches_view_any_package">İstənilən paket</string>
|
||||||
|
<string name="patches_delete_single_dialog_description">\"%s\" silmək istədiyinizə əminsiniz?</string>
|
||||||
|
<string name="patches_delete_multiple_dialog_description">Seçilmiş yamaları silmək istədiyinizə əminsiniz?</string>
|
||||||
|
<string name="about_revanced_manager">ReVanced Manager haqqında</string>
|
||||||
|
<string name="revanced_manager_description">ReVanced Manager, Android tətbiqlərini yamalamaq üçün ReVanced Patcher istifadə edən bir Android tətbiqidir. O, xüsusi yamalarla tətbiqləri yükləməyə və yamalamağa, həmçinin yamalanma prosesini idarə etməyə imkan verir.</string>
|
||||||
|
<string name="developer_options_taps">%d toxunuş qalıb</string>
|
||||||
|
<string name="developer_options_enabled">Tərtibatçı seçimləri aktivləşdirildi</string>
|
||||||
|
<string name="developer_options_already_enabled">Tərtibatçı seçimləri artıq aktivləşdirilib</string>
|
||||||
|
<string name="update_available">Yeniləmə mövcuddur</string>
|
||||||
|
<string name="current_version">Cari versiya: %s</string>
|
||||||
|
<string name="new_version">Yeni versiya: %s</string>
|
||||||
|
<string name="ready_to_install_update">Yeniləməni quraşdırmağa hazırdır</string>
|
||||||
|
<string name="update_completed">Yeniləmə quraşdırıldı</string>
|
||||||
|
<string name="install_update_manager_failed">Yeniləməni quraşdırmaq alınmadı</string>
|
||||||
|
<string name="manual_update_check">Yeniləmələri yoxla</string>
|
||||||
|
<string name="manual_update_check_description">Yeniləmələri əl ilə yoxla</string>
|
||||||
|
<string name="update_checking_manager">Başlayarkən yeniləmələri yoxla</string>
|
||||||
|
<string name="update_checking_manager_description">Tətbiq başlayanda ReVanced Manager-in yeni versiyalarını yoxla</string>
|
||||||
|
<string name="manager_prereleases">İlkin buraxılışları istifadə et</string>
|
||||||
|
<string name="manager_prereleases_description">ReVanced Manager-in ilkin versiyalarından istifadə et</string>
|
||||||
|
<string name="changelog">Dəyişiklik qeydlərinə bax</string>
|
||||||
|
<string name="changelog_loading">Dəyişiklik qeydi yüklənir</string>
|
||||||
|
<string name="changelog_download_fail">Dəyişiklik qeydini yükləmək alınmadı: %s</string>
|
||||||
|
<string name="changelog_description">Bu yeniləmədəki son dəyişikliklərə bax</string>
|
||||||
|
<string name="battery_optimization_notification">ReVanced Manager-in arxa planda düzgün işləməsi üçün batareya optimizasiyası söndürülməlidir. Optimizasiyaları söndürmək üçün buraya klikləyin.</string>
|
||||||
|
<string name="installing_manager_update">Yeniləmə quraşdırılır…</string>
|
||||||
|
<string name="downloading_manager_update">Yeniləmə yüklənir…</string>
|
||||||
|
<string name="download_manager_failed">Yeniləməni yükləmək alınmadı: %s</string>
|
||||||
|
<string name="cancel">Ləğv et</string>
|
||||||
|
<string name="save">Saxla</string>
|
||||||
|
<string name="save_with_count">Saxla (%1$s)</string>
|
||||||
|
<string name="update">Yenilə</string>
|
||||||
|
<string name="empty">Boş</string>
|
||||||
|
<string name="installing_message">"Xahiş edildikdə <b>Yenilə</b> düyməsinə toxunun.\nReVanced Manager yenilənərkən bağlanacaq."</string>
|
||||||
|
<string name="no_changelogs_found">Dəyişiklik qeydləri tapılmadı</string>
|
||||||
|
<string name="just_now">İndi</string>
|
||||||
|
<string name="minutes_ago">%s dəq əvvəl</string>
|
||||||
|
<string name="hours_ago">%s saat əvvəl</string>
|
||||||
|
<string name="days_ago">%s gün əvvəl</string>
|
||||||
|
<string name="invalid_date">Yanlış tarix</string>
|
||||||
|
<string name="disable_battery_optimization">Batareya optimizasiyasını söndür</string>
|
||||||
|
<string name="input_dialog_value_invalid">Yanlış dəyər</string>
|
||||||
|
<string name="option_required">Bu seçim tələb olunur</string>
|
||||||
|
<string name="required_options_screen">Tələb olunan seçimlər</string>
|
||||||
|
<string name="failed_to_check_updates">Yeniləmələri yoxlamaq alınmadı: %s</string>
|
||||||
|
<string name="no_update_available">Yeniləmə mövcud deyil</string>
|
||||||
|
<string name="update_check">Yeniləmələr yoxlanılır…</string>
|
||||||
|
<string name="dismiss_temporary">İndi yox</string>
|
||||||
|
<string name="update_available_dialog_description">ReVanced Manager-in (%s) yeni versiyası mövcuddur.</string>
|
||||||
|
<string name="failed_to_download_update">Yeniləməni yükləmək alınmadı: %s</string>
|
||||||
|
<string name="download">Yüklə</string>
|
||||||
|
<string name="download_confirmation_metered">"Hazırda ölçülü bağlantıdasınız və xidmət provayderiniz tərəfindən məlumat haqqları tətbiq oluna bilər.\n\nDavam etmək istəyirsiniz?"</string>
|
||||||
|
<string name="download_update_confirmation">Yeniləməni yüklənsin?</string>
|
||||||
|
<string name="no_contributors_found">Heç bir töhfəçi tapılmadı</string>
|
||||||
|
<string name="select">Seç</string>
|
||||||
|
<string name="select_deselect_all">Hamısını seç və ya seçimi ləğv et</string>
|
||||||
|
<string name="select_patches_type_dialog_description">URL-dən və ya yerli fayllardan yeni yamalar əlavə et</string>
|
||||||
|
<string name="local_patches_description">Yerli saxlama yerindən yamalar əlavə et.</string>
|
||||||
|
<string name="remote_patches_description">URL-dən yamalar əlavə et. Yamalar avtomatik olaraq yenilənə bilər.</string>
|
||||||
|
<string name="recommended">Tövsiyə olunur</string>
|
||||||
|
<string name="installation_failed_dialog_title">Quraşdırma uğursuz oldu</string>
|
||||||
|
<string name="installation_cancelled_dialog_title">Quraşdırma ləğv edildi</string>
|
||||||
|
<string name="installation_blocked_dialog_title">Quraşdırma bloklandı</string>
|
||||||
|
<string name="installation_conflict_dialog_title">Quraşdırma konflikti</string>
|
||||||
|
<string name="installation_incompatible_dialog_title">Quraşdırma uyğun deyil</string>
|
||||||
|
<string name="installation_invalid_dialog_title">Quraşdırma etibarsızdır</string>
|
||||||
|
<string name="installation_storage_issue_dialog_title">Kifayət qədər yaddaş yoxdur</string>
|
||||||
|
<string name="installation_timeout_dialog_title">Quraşdırmanın vaxtı bitdi</string>
|
||||||
|
<string name="installation_failed_description">Quraşdırma naməlum səbəbdən uğursuz oldu. Yenidən cəhd edin?</string>
|
||||||
|
<string name="installation_aborted_description">Quraşdırma əl ilə ləğv edildi. Yenidən cəhd edin?</string>
|
||||||
|
<string name="installation_blocked_description">Quraşdırma bloklandı. Cihazınızın təhlükəsizlik parametrlərini nəzərdən keçirin və yenidən cəhd edin.</string>
|
||||||
|
<string name="installation_conflict_description">Quraşdırma tətbiqin mövcud quraşdırılması tərəfindən dayandırıldı. Quraşdırılmış tətbiqi silin və yenidən cəhd edin?</string>
|
||||||
|
<string name="installation_incompatible_description">Tətbiq bu cihazla uyğun deyil. Bu cihazla uyğun gələn bir APK istifadə edin və yenidən cəhd edin.</string>
|
||||||
|
<string name="installation_invalid_description">Tətbiq etibarsızdır. Tətbiqi silin və yenidən cəhd edin?</string>
|
||||||
|
<string name="installation_storage_issue_description">Kifayət qədər yaddaş olmadığı üçün tətbiq quraşdırıla bilmədi. Bir qədər yer boşaldın və yenidən cəhd edin.</string>
|
||||||
|
<string name="installation_timeout_description">Quraşdırma çox uzun çəkdi. Yenidən cəhd edin?</string>
|
||||||
|
<string name="reinstall">Yenidən quraşdır</string>
|
||||||
|
<string name="show">Göstər</string>
|
||||||
|
<string name="debugging">Hata ayıklama</string>
|
||||||
|
<string name="about_device">Cihaz haqqında</string>
|
||||||
|
<string name="enter_url">URL daxil et</string>
|
||||||
|
<string name="next">Növbəti</string>
|
||||||
|
<string name="auto_update">Avtomatik yeniləmə</string>
|
||||||
|
<string name="add_patches">Yamalar əlavə et</string>
|
||||||
|
<string name="auto_update_description">Yeni versiya mövcud olduqda avtomatik yenilə</string>
|
||||||
|
<string name="patches_prereleases">İlkin buraxılışları istifadə et</string>
|
||||||
|
<string name="patches_prereleases_description">%s-in ilkin versiyalarından istifadə et</string>
|
||||||
|
<string name="patches_url">Yamaların URL-i</string>
|
||||||
|
<string name="incompatible_patches_dialog">"Bu yamalar seçilmiş tətbiq versiyası (%1$s) ilə uyğun deyil.\n\nDaha çox məlumat üçün yamalara klikləyin."</string>
|
||||||
|
<string name="incompatible_patch">Uyğun olmayan yama</string>
|
||||||
|
<string name="any_version">İstənilən</string>
|
||||||
|
<string name="never_show_again">Bir daha göstərmə</string>
|
||||||
|
<string name="show_manager_update_dialog_on_launch">Başlayarkən yeniləmə mesajını göstər</string>
|
||||||
|
<string name="show_manager_update_dialog_on_launch_description">Başlayarkən yeni yeniləmə mövcud olduqda açılan bildirişi göstər</string>
|
||||||
|
<string name="failed_to_import_keystore">Açar anbarını idxal etmək alınmadı</string>
|
||||||
|
<string name="export">İxrac et</string>
|
||||||
|
<string name="confirm">Təsdiqlə</string>
|
||||||
|
</resources>
|
||||||
460
app/src/main/res/values-be-rBY/strings.xml
Normal file
460
app/src/main/res/values-be-rBY/strings.xml
Normal file
@@ -0,0 +1,460 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Strings with new lines must be raw strings, where the string is wrapped in double quotes and new lines are regular line breaks and not \n
|
||||||
|
Raw strings still require escaping embedded double quotes, but single quote characters can be escaped or used as-is.
|
||||||
|
|
||||||
|
Raw strings are required because Crowdin AI translations regularly gets confused and
|
||||||
|
replace \n with an encoded new line character.
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
<string name="summary_key">First \'item\' text\nSecond \"item\" text</string>
|
||||||
|
Good:
|
||||||
|
<string name="summary_key">"First 'item' text
|
||||||
|
Second \"item\" text"</string>
|
||||||
|
|
||||||
|
-->
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">ReVanced Manager</string>
|
||||||
|
<string name="patcher">Тэст патчара</string>
|
||||||
|
<string name="patches">Патчы</string>
|
||||||
|
<string name="cli">CLI</string>
|
||||||
|
<string name="manager">Менеджар</string>
|
||||||
|
<string name="plugin_host_permission_label">Хост плагінаў ReVanced Manager</string>
|
||||||
|
<string name="plugin_host_permission_description">Выкарыстоўваецца для кіравання доступам да плагінаў ReVanced Manager. Толькі ReVanced Manager мае гэта.</string>
|
||||||
|
<string name="toast_copied_to_clipboard">Скапіявана!</string>
|
||||||
|
<string name="copy_to_clipboard">Скапіяваць у буфер абмену</string>
|
||||||
|
<string name="dashboard">Панэль кіравання</string>
|
||||||
|
<string name="settings">Налады</string>
|
||||||
|
<string name="select_app">Выберыце праграму</string>
|
||||||
|
<string name="patches_count_selected">%1$d/%2$d выбрана</string>
|
||||||
|
<string name="new_downloader_plugins_notification">Даступны новыя плагіны для загрузкі. Націсніце тут, каб наладзіць іх.</string>
|
||||||
|
<string name="unsupported_architecture_warning">Патчынг на гэтай архітэктуры прылады не падтрымліваецца і, хутчэй за ўсё, не атрымаецца.</string>
|
||||||
|
<string name="import_">Імпарт</string>
|
||||||
|
<string name="import_patches">Імпартаваць патчы</string>
|
||||||
|
<string name="file_field_set">Выбрана</string>
|
||||||
|
<string name="file_field_not_set">Не выбрана</string>
|
||||||
|
<string name="field_not_set">Не ўсталявана</string>
|
||||||
|
<string name="patches_missing">Адсутнічае</string>
|
||||||
|
<string name="patches_error">Памылка</string>
|
||||||
|
<string name="patches_error_description">Патчы не ўдалося загрузіць. Націсніце, каб праглядзець памылку</string>
|
||||||
|
<string name="patches_not_downloaded">Патчы не былі загружаны. Націсніце тут, каб загрузіць іх</string>
|
||||||
|
<string name="patches_name_default">Патчы</string>
|
||||||
|
<string name="patches_name_fallback">Без назвы</string>
|
||||||
|
<string name="android_11_bug_dialog_title">Памылка Android 11</string>
|
||||||
|
<string name="android_11_bug_dialog_description">Дазвол на ўстаноўку праграм павінен быць выдадзены загадзя, каб пазбегнуць памылкі ў сістэме Android 11, якая негатыўна паўплывае на зручнасць карыстання.</string>
|
||||||
|
<string name="no_network_toast">Няма падключэння да інтэрнэту</string>
|
||||||
|
<string name="selected_app_meta_any_version">Любая даступная версія</string>
|
||||||
|
<string name="app_source_dialog_title">Выберыце крыніцу</string>
|
||||||
|
<string name="app_source_dialog_option_auto">Аўтаматычна</string>
|
||||||
|
<string name="app_source_dialog_option_auto_description">Выкарыстоўваць усе даступныя загрузчыкі для загрузкі праграмы</string>
|
||||||
|
<string name="app_source_dialog_option_auto_unavailable">Няма даступных плагінаў</string>
|
||||||
|
<string name="app_source_dialog_option_installed_no_root">Усталяваныя праграмы не могуць быць зноў прапатчаны без root-доступу</string>
|
||||||
|
<string name="app_source_dialog_option_installed_version_not_suggested">Версія %s не адпавядае прапанаванай версіі</string>
|
||||||
|
<string name="patch_item_description">Пачаць патчынг праграмы</string>
|
||||||
|
<string name="patch_selector_item">Выбраць патчы</string>
|
||||||
|
<string name="patch_selector_item_description">%d патчаў выбрана</string>
|
||||||
|
<string name="no_patches_selected">Няма выбраных патчаў</string>
|
||||||
|
<string name="network_unavailable_warning">Ваша прылада не падключана да інтэрнэту. Загрузка не ўдасца пазней.</string>
|
||||||
|
<string name="network_metered_warning">Вы зараз карыстаецеся тарыфікуемым падключэннем. Можа спаганяцца плата за перадачу даных ад вашага пастаўшчыка паслуг.</string>
|
||||||
|
<string name="apk_source_selector_item">Выбраць крыніцу APK</string>
|
||||||
|
<string name="apk_source_auto">Выкарыстоўваюцца ўсе загрузчыкі APK</string>
|
||||||
|
<string name="apk_source_downloader">Выкарыстоўваецца %s</string>
|
||||||
|
<string name="apk_source_installed">Выкарыстоўваецца ўсталяваны APK</string>
|
||||||
|
<string name="apk_source_local">Выкарыстоўваецца лакальны файл APK</string>
|
||||||
|
<string name="legacy_import_failed">Не ўдалося імпартаваць старыя налады</string>
|
||||||
|
<string name="auto_updates_dialog_title">Наладзіць абнаўленні</string>
|
||||||
|
<string name="auto_updates_dialog_description">Ці хочаце вы, каб ReVanced Manager перыядычна правяраў наяўнасць абнаўленняў для наступных кампанентаў?</string>
|
||||||
|
<string name="auto_updates_dialog_manager">ReVanced Manager</string>
|
||||||
|
<string name="auto_updates_dialog_patches">Патчы ReVanced</string>
|
||||||
|
<string name="auto_updates_dialog_note">Гэтыя налады можна змяніць пазней.</string>
|
||||||
|
<string name="general">Агульныя</string>
|
||||||
|
<string name="general_description">Мова, тэма, дынамічны колер</string>
|
||||||
|
<string name="updates">Абнаўленні</string>
|
||||||
|
<string name="updates_description">Правяраць наяўнасць абнаўленняў і праглядаць спісы змен</string>
|
||||||
|
<string name="downloads">Загрузкі</string>
|
||||||
|
<string name="downloads_description">Плагіны для загрузкі і загружаныя праграмы</string>
|
||||||
|
<string name="import_export">Імпарт & экспарт</string>
|
||||||
|
<string name="import_export_description">Сховішча ключоў, параметры і выбар патчаў</string>
|
||||||
|
<string name="advanced">Пашыраныя</string>
|
||||||
|
<string name="advanced_description">URL API, ліміт памяці, адладка</string>
|
||||||
|
<string name="about">Пра праграму</string>
|
||||||
|
<string name="opensource_licenses">Ліцэнзіі з адкрытым зыходным кодам</string>
|
||||||
|
<string name="opensource_licenses_description">Прагледзець усе бібліятэкі, выкарыстаныя для стварэння гэтай праграмы</string>
|
||||||
|
<string name="contributors">Удзельнікі</string>
|
||||||
|
<string name="contributors_description">Прагледзець удзельнікаў ReVanced</string>
|
||||||
|
<string name="dynamic_color">Дынамічны колер</string>
|
||||||
|
<string name="dynamic_color_description">Адаптаваць колеры да шпалер</string>
|
||||||
|
<string name="pure_black_theme">Чыста чорная тэма</string>
|
||||||
|
<string name="pure_black_theme_description">Выкарыстоўваць чыста чорныя фоны для цёмнай тэмы</string>
|
||||||
|
<string name="theme">Тэма</string>
|
||||||
|
<string name="theme_description">Выбраць паміж светлай ці цёмнай тэмай</string>
|
||||||
|
<string name="language">Мова</string>
|
||||||
|
<string name="language_description">Выберыце мову адлюстравання праграмы</string>
|
||||||
|
<string name="language_system_default">Сістэмны па змаўчанні</string>
|
||||||
|
<string name="safeguards">Абаронныя меры</string>
|
||||||
|
<string name="patch_compat_check">Адключыць праверку сумяшчальнасці версій</string>
|
||||||
|
<string name="patch_compat_check_description">Не абмяжоўваць патчы сумяшчальнымі версіямі праграм</string>
|
||||||
|
<string name="patch_compat_check_confirmation">"Выбар несумяшчальных патчаў можа прывесці да няспраўнасці праграмы.
|
||||||
|
|
||||||
|
Ці хочаце вы працягнуць усё роўна?"</string>
|
||||||
|
<string name="suggested_version_safeguard">Патрабаваць рэкамендаваную версію праграмы</string>
|
||||||
|
<string name="suggested_version_safeguard_description">Прымусіць выбар рэкамендаванай версіі праграмы</string>
|
||||||
|
<string name="suggested_version_safeguard_confirmation">"Выбар праграмы, якая не з'яўляецца рэкамендаванай версіяй, можа выклікаць нечаканыя праблемы.
|
||||||
|
|
||||||
|
Ці хочаце вы працягнуць усё роўна?"</string>
|
||||||
|
<string name="patch_selection_safeguard">Дазволіць змяненне выбару і параметраў патчаў</string>
|
||||||
|
<string name="patch_selection_safeguard_description">Не перашкаджаць выбару або адмене выбару патчаў і наладзе параметраў</string>
|
||||||
|
<string name="patch_selection_safeguard_confirmation">"Змяненне выбару патчаў можа выклікаць нечаканыя праблемы.
|
||||||
|
|
||||||
|
Уключыць усё роўна?"</string>
|
||||||
|
<string name="universal_patches_safeguard">Дазволіць выкарыстоўваць універсальныя патчы</string>
|
||||||
|
<string name="universal_patches_safeguard_description">Не перашкаджаць выкарыстанню ўніверсальных патчаў</string>
|
||||||
|
<string name="universal_patches_safeguard_confirmation">"Універсальныя патчы не так добра пратэставаны, як тыя, што прызначаны для пэўных праграм.
|
||||||
|
|
||||||
|
Уключыць усё роўна?"</string>
|
||||||
|
<string name="import_keystore">Імпартаваць сховішча ключоў</string>
|
||||||
|
<string name="import_keystore_description">Імпартаваць карыстальніцкае сховішча ключоў</string>
|
||||||
|
<string name="import_keystore_dialog_title">Увядзіце ўліковыя даныя сховішча ключоў</string>
|
||||||
|
<string name="import_keystore_dialog_description">Вам трэба будзе ўвесці ўліковыя даныя сховішча ключоў, каб імпартаваць яго.</string>
|
||||||
|
<string name="import_keystore_dialog_alias_field">Імя карыстальніка (Псеўданім)</string>
|
||||||
|
<string name="import_keystore_dialog_password_field">Пароль</string>
|
||||||
|
<string name="import_keystore_dialog_button">Імпартаваць</string>
|
||||||
|
<string name="import_keystore_wrong_credentials">Няправільныя ўліковыя даныя сховішча ключоў</string>
|
||||||
|
<string name="import_keystore_success">Імпартаванае сховішча ключоў</string>
|
||||||
|
<string name="export_keystore">Экспартаваць сховішча ключоў</string>
|
||||||
|
<string name="export_keystore_description">Экспартаваць бягучае сховішча ключоў</string>
|
||||||
|
<string name="export_keystore_unavailable">Няма сховішча ключоў для экспарту</string>
|
||||||
|
<string name="export_keystore_success">Экспартаванае сховішча ключоў</string>
|
||||||
|
<string name="regenerate_keystore">Аднавіць сховішча ключоў</string>
|
||||||
|
<string name="regenerate_keystore_description">Стварыць новае сховішча ключоў</string>
|
||||||
|
<string name="regenerate_keystore_dialog_description">"Вы збіраецеся аднавіць сховішча ключоў, якое менеджэр будзе выкарыстоўваць падчас працэсу патчынгу.
|
||||||
|
|
||||||
|
Вы не зможаце абнавіць раней усталяваныя праграмы з гэтай крыніцы."</string>
|
||||||
|
<string name="regenerate_keystore_success">Сховішча ключоў было паспяхова заменена</string>
|
||||||
|
<string name="import_patch_selection">Імпартаваць выбар патчаў</string>
|
||||||
|
<string name="import_patch_selection_description">Імпартаваць выбар патчаў з файла JSON</string>
|
||||||
|
<string name="import_patch_selection_fail">Не ўдалося імпартаваць выбар патчаў: %s</string>
|
||||||
|
<string name="import_patch_selection_success">Імпартаваны выбар патчаў</string>
|
||||||
|
<string name="export_patch_selection">Экспартаваць выбар патчаў</string>
|
||||||
|
<string name="export_patch_selection_description">Экспартаваць выбар патчаў у файл JSON</string>
|
||||||
|
<string name="export_patch_selection_fail">Не ўдалося экспартаваць выбар патчаў: %s</string>
|
||||||
|
<string name="export_patch_selection_success">Экспартаваны выбар патчаў</string>
|
||||||
|
<string name="reset_patch_selection">Скінуць выбар патчаў</string>
|
||||||
|
<string name="reset_patch_selection_description">Скінуць захаваны выбар патчаў</string>
|
||||||
|
<string name="reset_patch_options">Скінуць параметры патчаў</string>
|
||||||
|
<string name="reset_patch_options_description">Скінуць захаваныя параметры патчаў</string>
|
||||||
|
<string name="reset_patch_selection_success">Выбар патчаў скінуты</string>
|
||||||
|
<string name="patch_selection_reset_all">Скінуць выбар патчаў глабальна</string>
|
||||||
|
<string name="patch_selection_reset_all_dialog_description">Вы збіраецеся скінуць усе выбраныя патчы. Вам трэба будзе ўручную зноў выбраць кожны патч.</string>
|
||||||
|
<string name="patch_selection_reset_all_description">Скідвае ўсе выбраныя патчы</string>
|
||||||
|
<string name="patch_selection_reset_package">Скінуць выбар патчаў для праграмы</string>
|
||||||
|
<string name="patch_selection_reset_package_dialog_description">Вы збіраецеся скінуць выбар патчаў для праграмы \"%s\". Вам прыйдзецца ўручную зноў выбраць кожны патч.</string>
|
||||||
|
<string name="patch_selection_reset_package_description">Скідвае выбар патчаў для адной праграмы</string>
|
||||||
|
<string name="patch_selection_reset_patches">Скінуць выбар патчаў (адзін)</string>
|
||||||
|
<string name="patch_selection_reset_patches_dialog_description">Вы збіраецеся скінуць выбар патчаў для \"%s\". Вам прыйдзецца выбіраць кожны патч уручную нанова.</string>
|
||||||
|
<string name="patch_selection_reset_patches_description">Скідвае выбар патчаў для пэўнай калекцыі патчаў</string>
|
||||||
|
<string name="patch_options_reset_package">Скінуць параметры патчаў для праграмы</string>
|
||||||
|
<string name="patch_options_reset_package_dialog_description">Вы збіраецеся скінуць параметры патчаў для праграмы \"%s\". Вам прыйдзецца паўторна ўжываць кожны параметр нанова.</string>
|
||||||
|
<string name="patch_options_reset_package_description">Скідвае параметры патчаў для адной праграмы</string>
|
||||||
|
<string name="patch_options_reset_patches">Скінуць параметры патчаў (адзін)</string>
|
||||||
|
<string name="patch_options_reset_patches_dialog_description">Вы збіраецеся скінуць параметры патчаў для \"%s\". Вам прыйдзецца паўторна ўжываць кожны параметр нанова.</string>
|
||||||
|
<string name="patch_options_reset_patches_description">Скідвае параметры патчаў для пэўнай калекцыі патчаў</string>
|
||||||
|
<string name="patch_options_reset_all">Скінуць параметры патчаў глабальна</string>
|
||||||
|
<string name="patch_options_reset_all_dialog_description">Вы збіраецеся скінуць усе параметры патчаў. Вам прыйдзецца паўторна ўжываць кожны параметр нанова.</string>
|
||||||
|
<string name="patch_options_reset_all_description">Скідвае ўсе параметры патчаў</string>
|
||||||
|
<string name="downloader_plugins">Убудовы</string>
|
||||||
|
<string name="downloader_plugin_state_trusted">Надзейны</string>
|
||||||
|
<string name="downloader_plugin_state_failed">Не атрымалася загрузіць. Націсніце для атрымання падрабязнасцей</string>
|
||||||
|
<string name="downloader_plugin_state_untrusted">Ненадзейны</string>
|
||||||
|
<string name="downloader_plugin_trust_dialog_title">Давяраць убудове?</string>
|
||||||
|
<string name="downloader_plugin_revoke_trust_dialog_title">Адазваць давер?</string>
|
||||||
|
<string name="downloader_plugin_trust_dialog_body">Працяг дазволіць гэтаму ўбудове працаваць у вашай сістэме.\n\nУключайце гэты ўбудова толькі ў тым выпадку, калі вы яму давяраеце. Убудовы могуць выконваць адвольны код і могуць скампраметаваць вашу прыладу.</string>
|
||||||
|
<string name="downloader_plugin_trust_dialog_signature">Подпіс:\n\n%s</string>
|
||||||
|
<string name="downloader_plugin_trust_dialog_plugin">Убудова:\n%s</string>
|
||||||
|
<string name="downloader_plugin_delete_apps_title">Выдаліць выбраныя праграмы</string>
|
||||||
|
<string name="downloader_plugin_delete_apps_description">Вы ўпэўнены, што жадаеце выдаліць выбраныя праграмы?</string>
|
||||||
|
<string name="downloader_settings_no_apps">Не знойдзена загружаных праграм.</string>
|
||||||
|
<string name="search_apps">Пошук праграм…</string>
|
||||||
|
<string name="loading_body">Загрузка…</string>
|
||||||
|
<string name="downloading_patches">Спампоўка патчаў…</string>
|
||||||
|
<string name="options">Параметры</string>
|
||||||
|
<string name="ok">ОК</string>
|
||||||
|
<string name="yes">Так</string>
|
||||||
|
<string name="no">Не</string>
|
||||||
|
<string name="edit">Рэдагаваць</string>
|
||||||
|
<string name="dialog_input_placeholder">Значэнне</string>
|
||||||
|
<string name="reset">Скінуць</string>
|
||||||
|
<string name="share">Падзяліцца</string>
|
||||||
|
<string name="patch">Патч</string>
|
||||||
|
<string name="select_from_storage">Выбраць са сховішча</string>
|
||||||
|
<string name="select_from_storage_description">Выберыце файл APK са сховішча з дапамогай файлавага мэнэджара</string>
|
||||||
|
<string name="suggested_version_info">Рэкамендаваная версія: %s</string>
|
||||||
|
<string name="type_anything">Набярыце што-небудзь для працягу</string>
|
||||||
|
<string name="search">Пошук патчаў…</string>
|
||||||
|
<string name="apply">Ужыць</string>
|
||||||
|
<string name="help">Дапамога</string>
|
||||||
|
<string name="back">Назад</string>
|
||||||
|
<string name="warning">Папярэджанне</string>
|
||||||
|
<string name="add">Дадаць</string>
|
||||||
|
<string name="close">Зачыніць</string>
|
||||||
|
<string name="clear">Ачысціць</string>
|
||||||
|
<string name="system">Сістэма</string>
|
||||||
|
<string name="light">Светлая</string>
|
||||||
|
<string name="dark">Цёмная</string>
|
||||||
|
<string name="appearance">Знешні выгляд</string>
|
||||||
|
<string name="networking">Сетка</string>
|
||||||
|
<string name="allow_metered_networks">Дазволіць лімітаваныя сеткі</string>
|
||||||
|
<string name="allow_metered_networks_description">Дазваляе аўтаматычныя абнаўленні ў лімітаваных сетках.\n Праграма ўсё яшчэ можа папярэджваць аб лімітаваных сетках для ручных аперацый.</string>
|
||||||
|
<string name="downloaded_apps">Спампаваныя праграмы</string>
|
||||||
|
<string name="process_runtime">Запусціць Patcher у іншым працэсе (эксперыментальна)</string>
|
||||||
|
<string name="process_runtime_description">Гэта хутчэй і дазваляе Patcher выкарыстоўваць больш памяці</string>
|
||||||
|
<string name="process_runtime_memory_limit">Ліміт памяці працэсу Patcher</string>
|
||||||
|
<string name="process_runtime_memory_limit_description">Максімальны аб\'ём памяці, які можа выкарыстоўваць працэс Patcher (у мегабайтах)</string>
|
||||||
|
<string name="debug_logs_export">Экспартаваць адладачныя журналы</string>
|
||||||
|
<string name="debug_logs_export_read_failed">Не атрымалася прачытаць журналы (код выхаду %d)</string>
|
||||||
|
<string name="debug_logs_export_failed">Не атрымалася экспартаваць журналы</string>
|
||||||
|
<string name="debug_logs_export_success">Экспартаваныя журналы</string>
|
||||||
|
<string name="api_url">URL-адрас API</string>
|
||||||
|
<string name="api_url_description">API, які выкарыстоўваецца для спампоўкі неабходных файлаў</string>
|
||||||
|
<string name="api_url_dialog_title">Змяніць URL-адрас API</string>
|
||||||
|
<string name="api_url_dialog_description">Змяніць URL-адрас API ReVanced Manager. ReVanced Manager выкарыстоўвае API для спампоўкі патчаў і абнаўленняў.</string>
|
||||||
|
<string name="api_url_dialog_warning">ReVanced Manager падключаецца да API для спампоўкі патчаў і абнаўленняў. Пераканайцеся, што вы давяраеце яму.</string>
|
||||||
|
<string name="api_url_dialog_save">Усталяваць</string>
|
||||||
|
<string name="api_url_dialog_reset">Скінуць URL-адрас API</string>
|
||||||
|
<string name="device">Прылада</string>
|
||||||
|
<string name="device_android_version">Версія Android</string>
|
||||||
|
<string name="device_model">Мадэль</string>
|
||||||
|
<string name="device_architectures">Архітэктуры ЦП</string>
|
||||||
|
<string name="device_memory_limit">Ліміты памяці</string>
|
||||||
|
<string name="device_memory_limit_format">%1$dМБ (Звычайная) - %2$dМБ (Вялікая)</string>
|
||||||
|
<string name="patches_force_download">Прымусова спампаваць усе патчы</string>
|
||||||
|
<string name="patches_reset">Скінуць патчы</string>
|
||||||
|
<string name="patching">Патчынг</string>
|
||||||
|
<string name="signing">Падпісанне</string>
|
||||||
|
<string name="storage">Сховішча</string>
|
||||||
|
<string name="no_patch_found">Патчы не знойдзены. Праверце свае патчы</string>
|
||||||
|
<string name="tab_apps">Праграмы</string>
|
||||||
|
<string name="tab_patches">Патчы</string>
|
||||||
|
<string name="delete">Выдаліць</string>
|
||||||
|
<string name="refresh">Абнавіць</string>
|
||||||
|
<string name="continue_anyways">Усё роўна працягнуць</string>
|
||||||
|
<string name="download_another_version">Спампаваць іншую версію</string>
|
||||||
|
<string name="download_app">Спампаваць праграму</string>
|
||||||
|
<string name="download_apk">Спампаваць файл APK</string>
|
||||||
|
<string name="patches_download_fail">Не атрымалася спампаваць патчы: %s</string>
|
||||||
|
<string name="patches_replace_fail">Не атрымалася імпартаваць патчы: %s</string>
|
||||||
|
<string name="no_patched_apps_found">Не знойдзена залатанных праграм</string>
|
||||||
|
<string name="tap_on_patches">Націсніце на патчы, каб атрымаць больш інфармацыі пра іх</string>
|
||||||
|
<string name="patches_selected">Выбрана: %s</string>
|
||||||
|
<string name="incompatible_patches">Несумяшчальныя патчы</string>
|
||||||
|
<string name="universal_patches">Універсальныя патчы</string>
|
||||||
|
<string name="patch_selection_reset_toast">Выбар патчаў і параметры былі скінуты да рэкамендаваных значэнняў па змаўчанні</string>
|
||||||
|
<string name="patch_options_reset_toast">Параметры патчаў былі скінуты</string>
|
||||||
|
<string name="non_suggested_version_warning_title">Не рэкамендаваная версія</string>
|
||||||
|
<string name="non_suggested_version_warning_description">"Выбраная вамі версія праграмы не супадае з рэкамендаванай версіяй.\nКалі ласка, выкарыстоўвайце рэкамендаваную версію: %s\n\nКаб усё роўна працягнуць, адключыце \"Патрабаваць рэкамендаваную версію праграмы\" у пашыраных наладах."</string>
|
||||||
|
<string name="selection_warning_title">Спыніць выкарыстанне значэнняў па змаўчанні?</string>
|
||||||
|
<string name="selection_warning_description">"Рэкамендуецца выкарыстоўваць выбар патчаў і параметры па змаўчанні. Іх змяненне можа прывесці да нечаканых праблем.\n\nВам трэба ўключыць \"Дазволіць змяненне выбару патчаў і параметраў\" у пашыраных наладах перад пераключэннем патчаў."</string>
|
||||||
|
<string name="universal_patch_warning_description">"Універсальныя патчы маюць больш абагульненае выкарыстанне і працуюць не так надзейна, як патчы, арыентаваныя на пэўныя праграмы. Вы можаце сутыкнуцца з праблемамі пры іх выкарыстанні.\n\nВам трэба ўключыць \"Дазволіць выкарыстанне ўніверсальных патчаў\" у пашыраных наладах перад выкарыстаннем універсальных патчаў."</string>
|
||||||
|
<string name="this_version">Гэтая версія</string>
|
||||||
|
<string name="universal">Любая праграма</string>
|
||||||
|
<string name="search_patches">Пошук патчаў</string>
|
||||||
|
<string name="app_version_not_compatible">"Гэты патч несумяшчальны з выбранай версіяй праграмы (%1$s)\n\nЁн сумяшчальны толькі з наступнымі версіямі: %2$s"</string>
|
||||||
|
<string name="continue_with_version">Працягнуць з гэтай версіяй?</string>
|
||||||
|
<string name="version_not_compatible">Не ўсе патчы сумяшчальныя з гэтай версіяй (%s). Вы ўсё роўна хочаце працягнуць?</string>
|
||||||
|
<string name="download_application">Спампаваць праграму?</string>
|
||||||
|
<string name="app_not_installed">Выбраная вамі праграма не ўсталявана. Вы хочаце яе спампаваць?</string>
|
||||||
|
<string name="failed_to_load_apk">Не атрымалася загрузіць APK</string>
|
||||||
|
<string name="loading">Загрузка…</string>
|
||||||
|
<string name="not_installed">Не ўсталявана</string>
|
||||||
|
<string name="installed">Усталявана</string>
|
||||||
|
<string name="app_info">Інфармацыя аб праграме</string>
|
||||||
|
<string name="uninstall">Выдаліць</string>
|
||||||
|
<string name="unpatch">Распатчаваць</string>
|
||||||
|
<string name="repatch">Перапатчаваць</string>
|
||||||
|
<string name="install_type">Тып усталёўкі</string>
|
||||||
|
<string name="package_name">Назва пакета</string>
|
||||||
|
<string name="original_package_name">Арыгінальная назва пакета</string>
|
||||||
|
<string name="applied_patches">Ужытыя патчы</string>
|
||||||
|
<string name="view_applied_patches">Праглядзець ужытыя патчы</string>
|
||||||
|
<string name="default_install">Па змаўчанні</string>
|
||||||
|
<string name="mount_install">Змантаваць</string>
|
||||||
|
<string name="mounted">Змантавана</string>
|
||||||
|
<string name="not_mounted">Не змантавана</string>
|
||||||
|
<string name="mount">Змантаваць</string>
|
||||||
|
<string name="unmount">Дэмантаваць</string>
|
||||||
|
<string name="failed_to_mount">Не ўдалося змантаваць: %s</string>
|
||||||
|
<string name="failed_to_unmount">Не ўдалося дэмантаваць: %s</string>
|
||||||
|
<string name="unpatch_app">Выдаліць патчы з праграмы?</string>
|
||||||
|
<string name="unpatch_description">Вы ўпэўненыя, што хочаце выдаліць патчы з гэтай праграмы?</string>
|
||||||
|
<string name="downloader_invalid_version">Спампоўшчык не атрымаў правільную версію</string>
|
||||||
|
<string name="downloader_app_not_found">Спампоўшчык не знайшоў праграму</string>
|
||||||
|
<string name="downloader_error">Памылка спампоўшчыка: %s</string>
|
||||||
|
<string name="downloader_no_plugins_installed">Не ўсталяваны спампоўшчык.</string>
|
||||||
|
<string name="downloader_no_plugins_available">Ёсць усталяваныя спампоўшчыкі, але ніводзін з іх не з\'яўляецца давераным. Праверце свае налады.</string>
|
||||||
|
<string name="already_patched">Ужо адпачана</string>
|
||||||
|
<string name="patch_selector_sheet_filter_title">Фільтр</string>
|
||||||
|
<string name="patch_selector_sheet_filter_compat_title">Сумяшчальнасць</string>
|
||||||
|
<string name="string_option_menu_description">Больш опцый</string>
|
||||||
|
<string name="option_preset_custom_value">Карыстальніцкае значэнне</string>
|
||||||
|
<string name="path_selector">Выбраць са сховішча</string>
|
||||||
|
<string name="path_selector_parent_dir">Папярэдні каталог</string>
|
||||||
|
<string name="path_selector_dirs">Каталогі</string>
|
||||||
|
<string name="path_selector_files">Файлы</string>
|
||||||
|
<string name="show_password_field">Паказаць пароль</string>
|
||||||
|
<string name="hide_password_field">Схаваць пароль</string>
|
||||||
|
<string name="installer">Усталёўшчык</string>
|
||||||
|
<string name="install_app">Усталяваць</string>
|
||||||
|
<string name="install_app_success">Праграма ўсталявана</string>
|
||||||
|
<string name="install_app_fail">Не ўдалося ўсталяваць праграму: %s</string>
|
||||||
|
<string name="reinstall_app_fail">Не ўдалося пераўсталяваць праграму: %s</string>
|
||||||
|
<string name="uninstall_app_fail">Не ўдалося выдаліць праграму: %s</string>
|
||||||
|
<string name="open_app">Адкрыць</string>
|
||||||
|
<string name="save_apk">Захаваць APK</string>
|
||||||
|
<string name="save_apk_success">APK захаваны</string>
|
||||||
|
<string name="sign_fail">Не ўдалося падпісаць APK: %s</string>
|
||||||
|
<string name="save_logs">Захаваць логі</string>
|
||||||
|
<string name="plugin_activity_dialog_body">Для працягу працы з гэтым плагінам патрабуецца ўзаемадзеянне карыстальніка.</string>
|
||||||
|
<string name="select_install_type">Выберыце тып устаноўкі</string>
|
||||||
|
<string name="patcher_step_group_preparing">Падрыхтоўка</string>
|
||||||
|
<string name="patcher_step_load_patches">Загрузка патчаў</string>
|
||||||
|
<string name="patcher_step_unpack">Чытанне файла APK</string>
|
||||||
|
<string name="patcher_step_group_patching">Патчынг</string>
|
||||||
|
<string name="patcher_step_group_saving">Захаванне</string>
|
||||||
|
<string name="patcher_step_write_patched">Запіс адпачанага файла APK</string>
|
||||||
|
<string name="patcher_step_sign_apk">Падпісанне адпачанага файла APK</string>
|
||||||
|
<string name="patcher_notification_title">Патчынг у працэсе…</string>
|
||||||
|
<string name="patcher_notification_text">Націсніце, каб вярнуцца да патчара</string>
|
||||||
|
<string name="patcher_stop_confirm_title">Спыніць патчар</string>
|
||||||
|
<string name="patcher_stop_confirm_description">Вы ўпэўненыя, што хочаце спыніць працэс патчынгу?</string>
|
||||||
|
<string name="patcher_install_in_progress">Устаноўка ў працэсе. Калі ласка, пачакайце</string>
|
||||||
|
<string name="execute_patches">Выканаць патчы</string>
|
||||||
|
<string name="executing_patch">Выканаць %s</string>
|
||||||
|
<string name="failed_to_execute_patch">Не ўдалося выканаць %s</string>
|
||||||
|
<string name="step_completed">завершана</string>
|
||||||
|
<string name="step_failed">не ўдалося</string>
|
||||||
|
<string name="step_running">выконваецца</string>
|
||||||
|
<string name="step_waiting">чаканне</string>
|
||||||
|
<string name="expand_content">разгарнуць</string>
|
||||||
|
<string name="collapse_content">згарнуць</string>
|
||||||
|
<string name="drag_handle">змяніць парадак</string>
|
||||||
|
<string name="more">Больш</string>
|
||||||
|
<string name="less">Менш</string>
|
||||||
|
<string name="continue_">Працягнуць</string>
|
||||||
|
<string name="dismiss">Адхіліць</string>
|
||||||
|
<string name="permanent_dismiss">Не паказваць гэта зноў</string>
|
||||||
|
<string name="donate">Ахвяраваць</string>
|
||||||
|
<string name="website">Вэб-сайт</string>
|
||||||
|
<string name="github">GitHub</string>
|
||||||
|
<string name="contact">Кантакт</string>
|
||||||
|
<string name="version">Версія</string>
|
||||||
|
<string name="submit_feedback">Адправіць праблему або водгук</string>
|
||||||
|
<string name="submit_feedback_description">Дапамажыце нам палепшыць гэтую праграму</string>
|
||||||
|
<string name="developer_options">Параметры распрацоўшчыка</string>
|
||||||
|
<string name="developer_options_description">Параметры для адладкі праблем</string>
|
||||||
|
<string name="patches_update_success">Абнаўленне паспяховае</string>
|
||||||
|
<string name="patches_update_unavailable">Абнаўлення няма</string>
|
||||||
|
<string name="view_patches">Праглядзець патчы</string>
|
||||||
|
<string name="patches_view_any_version">Любая версія</string>
|
||||||
|
<string name="patches_view_any_package">Любы пакет</string>
|
||||||
|
<string name="patches_delete_single_dialog_description">Вы ўпэўненыя, што хочаце выдаліць \"%s\"?</string>
|
||||||
|
<string name="patches_delete_multiple_dialog_description">Вы ўпэўненыя, што хочаце выдаліць выбраныя патчы?</string>
|
||||||
|
<string name="about_revanced_manager">Пра ReVanced Manager</string>
|
||||||
|
<string name="revanced_manager_description">ReVanced Manager — гэта праграма для Android, якая выкарыстоўвае ReVanced Patcher для патчынгу праграм Android. Яна дазваляе спампоўваць і патчыць праграмы з дапамогай карыстальніцкіх патчаў, а таксама кіраваць працэсам патчынгу.</string>
|
||||||
|
<string name="developer_options_taps">засталося %d націскаў</string>
|
||||||
|
<string name="developer_options_enabled">Параметры распрацоўшчыка ўключаны</string>
|
||||||
|
<string name="developer_options_already_enabled">Параметры распрацоўшчыка ўжо ўключаны</string>
|
||||||
|
<string name="update_available">Даступна абнаўленне</string>
|
||||||
|
<string name="current_version">Бягучая версія: %s</string>
|
||||||
|
<string name="new_version">Новая версія: %s</string>
|
||||||
|
<string name="ready_to_install_update">Гатова да ўстаноўкі абнаўлення</string>
|
||||||
|
<string name="update_completed">Абнаўленне ўсталявана</string>
|
||||||
|
<string name="install_update_manager_failed">Не ўдалося ўсталяваць абнаўленне</string>
|
||||||
|
<string name="manual_update_check">Праверыць наяўнасць абнаўленняў</string>
|
||||||
|
<string name="manual_update_check_description">Праверыць абнаўленні ўручную</string>
|
||||||
|
<string name="update_checking_manager">Праверыць наяўнасць абнаўленняў пры запуску</string>
|
||||||
|
<string name="update_checking_manager_description">Праверыць наяўнасць новых версій ReVanced Manager пры запуску праграмы</string>
|
||||||
|
<string name="manager_prereleases">Выкарыстоўваць папярэднія версіі</string>
|
||||||
|
<string name="manager_prereleases_description">Выкарыстоўваць папярэднія версіі ReVanced Manager</string>
|
||||||
|
<string name="changelog">Праглядзець змены</string>
|
||||||
|
<string name="changelog_loading">Загрузка змяненняў</string>
|
||||||
|
<string name="changelog_download_fail">Не ўдалося спампаваць змены: %s</string>
|
||||||
|
<string name="changelog_description">Праверце апошнія змены ў гэтым абнаўленні</string>
|
||||||
|
<string name="battery_optimization_notification">Аптымізацыі батарэі павінны быць адключаны, каб ReVanced Manager правільна працаваў у фонавым рэжыме. Націсніце тут, каб адключыць аптымізацыі.</string>
|
||||||
|
<string name="installing_manager_update">Устаноўка абнаўлення…</string>
|
||||||
|
<string name="downloading_manager_update">Спампоўка абнаўлення…</string>
|
||||||
|
<string name="download_manager_failed">Не ўдалося спампаваць абнаўленне: %s</string>
|
||||||
|
<string name="cancel">Адмена</string>
|
||||||
|
<string name="save">Захаваць</string>
|
||||||
|
<string name="save_with_count">Захаваць (%1$s)</string>
|
||||||
|
<string name="update">Абнавіць</string>
|
||||||
|
<string name="empty">Пуста</string>
|
||||||
|
<string name="installing_message">"Націсніце на <b>Абнавіць</b>, калі будзе прапанавана.
|
||||||
|
ReVanced Manager закрыецца падчас абнаўлення."</string>
|
||||||
|
<string name="no_changelogs_found">Зменаў не знойдзена</string>
|
||||||
|
<string name="just_now">Толькі што</string>
|
||||||
|
<string name="minutes_ago">%s хв. таму</string>
|
||||||
|
<string name="hours_ago">%s гадз. таму</string>
|
||||||
|
<string name="days_ago">%s дн. таму</string>
|
||||||
|
<string name="invalid_date">Недапушчальная дата</string>
|
||||||
|
<string name="disable_battery_optimization">Адключыць аптымізацыю батарэі</string>
|
||||||
|
<string name="input_dialog_value_invalid">Недапушчальнае значэнне</string>
|
||||||
|
<string name="option_required">Гэты параметр абавязковы</string>
|
||||||
|
<string name="required_options_screen">Абавязковыя параметры</string>
|
||||||
|
<string name="failed_to_check_updates">Не ўдалося праверыць наяўнасць абнаўленняў: %s</string>
|
||||||
|
<string name="no_update_available">Абнаўлення няма</string>
|
||||||
|
<string name="update_check">Праверка наяўнасці абнаўленняў…</string>
|
||||||
|
<string name="dismiss_temporary">Не зараз</string>
|
||||||
|
<string name="update_available_dialog_description">Даступна новая версія ReVanced Manager (%s).</string>
|
||||||
|
<string name="failed_to_download_update">Не ўдалося спампаваць абнаўленне: %s</string>
|
||||||
|
<string name="download">Спампаваць</string>
|
||||||
|
<string name="download_confirmation_metered">"Вы зараз карыстаецеся тарыфным злучэннем, і можа спаганяцца плата за перадачу дадзеных ад вашага правайдэра.
|
||||||
|
|
||||||
|
Вы ўсё яшчэ хочаце працягнуць?"</string>
|
||||||
|
<string name="download_update_confirmation">Спампаваць абнаўленне?</string>
|
||||||
|
<string name="no_contributors_found">Удзельнікаў не знойдзена</string>
|
||||||
|
<string name="select">Выбраць</string>
|
||||||
|
<string name="select_deselect_all">Выбраць або адмяніць выбар усяго</string>
|
||||||
|
<string name="select_patches_type_dialog_description">Дадаць новыя патчы з URL або лакальных файлаў</string>
|
||||||
|
<string name="local_patches_description">Дадаць патчы з лакальнага сховішча.</string>
|
||||||
|
<string name="remote_patches_description">Дадаць патчы з URL. Патчы могуць аўтаматычна абнаўляцца.</string>
|
||||||
|
<string name="recommended">Рэкамендавана</string>
|
||||||
|
<string name="installation_failed_dialog_title">Не ўдалося ўсталяваць</string>
|
||||||
|
<string name="installation_cancelled_dialog_title">Устаноўка адменена</string>
|
||||||
|
<string name="installation_blocked_dialog_title">Устаноўка заблакавана</string>
|
||||||
|
<string name="installation_conflict_dialog_title">Канфлікт устаноўкі</string>
|
||||||
|
<string name="installation_incompatible_dialog_title">Несумяшчальная ўстаноўка</string>
|
||||||
|
<string name="installation_invalid_dialog_title">Несапраўдная ўстаноўка</string>
|
||||||
|
<string name="installation_storage_issue_dialog_title">Недастаткова месца для захоўвання</string>
|
||||||
|
<string name="installation_timeout_dialog_title">Час устаноўкі скончыўся</string>
|
||||||
|
<string name="installation_failed_description">Устаноўка не ўдалася па невядомай прычыне. Паспрабаваць яшчэ раз?</string>
|
||||||
|
<string name="installation_aborted_description">Устаноўка была адменена ўручную. Паспрабаваць яшчэ раз?</string>
|
||||||
|
<string name="installation_blocked_description">Устаноўка была заблакавана. Праверце налады бяспекі прылады і паспрабуйце яшчэ раз.</string>
|
||||||
|
<string name="installation_conflict_description">Устаноўка была прадухілена існуючай устаноўкай праграмы. Выдаліць усталяваную праграму і паспрабаваць яшчэ раз?</string>
|
||||||
|
<string name="installation_incompatible_description">Праграма несумяшчальная з гэтай прыладай. Выкарыстоўвайце APK, які сумяшчальны з гэтай прыладай, і паспрабуйце яшчоў раз.</string>
|
||||||
|
<string name="installation_invalid_description">Праграма несапраўдная. Выдаліць праграму і паспрабаваць яшчэ раз?</string>
|
||||||
|
<string name="installation_storage_issue_description">Праграму не ўдалося ўсталяваць з-за недастатковага месца для захоўвання. Вызваліце месца і паспрабуйце яшчэ раз.</string>
|
||||||
|
<string name="installation_timeout_description">Устаноўка заняла занадта шмат часу. Паспрабаваць яшчэ раз?</string>
|
||||||
|
<string name="reinstall">Пераўсталяваць</string>
|
||||||
|
<string name="show">Паказаць</string>
|
||||||
|
<string name="debugging">Адладка</string>
|
||||||
|
<string name="about_device">Пра прыладу</string>
|
||||||
|
<string name="enter_url">Увядзіце URL</string>
|
||||||
|
<string name="next">Далей</string>
|
||||||
|
<string name="auto_update">Аўтаматычнае абнаўленне</string>
|
||||||
|
<string name="add_patches">Дадаць патчы</string>
|
||||||
|
<string name="auto_update_description">Аўтаматычна абнаўляць, калі даступная новая версія</string>
|
||||||
|
<string name="patches_prereleases">Выкарыстоўваць папярэднія версіі</string>
|
||||||
|
<string name="patches_prereleases_description">Выкарыстоўваць папярэднія версіі %s</string>
|
||||||
|
<string name="patches_url">URL патчаў</string>
|
||||||
|
<string name="incompatible_patches_dialog">"Гэтыя патчы несумяшчальныя з абранай версіяй праграмы (%1$s).
|
||||||
|
|
||||||
|
Націсніце на патчы, каб убачыць больш падрабязную інфармацыю."</string>
|
||||||
|
<string name="incompatible_patch">Несумяшчальны патч</string>
|
||||||
|
<string name="any_version">Любы</string>
|
||||||
|
<string name="never_show_again">Ніколі не паказваць зноў</string>
|
||||||
|
<string name="show_manager_update_dialog_on_launch">Паказваць паведамленне аб абнаўленні пры запуску</string>
|
||||||
|
<string name="show_manager_update_dialog_on_launch_description">Паказваць усплывальнае апавяшчэнне кожны раз, калі пры запуску даступна новае абнаўленне</string>
|
||||||
|
<string name="failed_to_import_keystore">Не ўдалося імпартаваць сховішча ключоў</string>
|
||||||
|
<string name="export">Экспарт</string>
|
||||||
|
<string name="confirm">Пацвердзіць</string>
|
||||||
|
</resources>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user