mirror of
https://github.com/0x192/universal-android-debloater.git
synced 2026-10-21 13:52:12 +00:00
Compare commits
235 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11f27c671c | ||
|
|
4ef967e26c | ||
|
|
1ef952200a | ||
|
|
05a1625c8f | ||
|
|
fd75b23e89 | ||
|
|
3699d155d8 | ||
|
|
7fbec57072 | ||
|
|
68092ff987 | ||
|
|
69235a9aa5 | ||
|
|
588e310882 | ||
|
|
12c71c908a | ||
|
|
04ba4d6cb1 | ||
|
|
a076ab525c | ||
|
|
2b31c66d67 | ||
|
|
bf12bf6a5b | ||
|
|
fa1669b509 | ||
|
|
8d302a98ec | ||
|
|
e467fc7366 | ||
|
|
5fa6fea353 | ||
|
|
28642075fd | ||
|
|
d150bb9bea | ||
|
|
cc6406e1f8 | ||
|
|
9f01eed2b5 | ||
|
|
cce2503039 | ||
|
|
768a1226f4 | ||
|
|
328e1c81c7 | ||
|
|
f6263d663f | ||
|
|
a73e0e5577 | ||
|
|
749820ca86 | ||
|
|
8bc3eee321 | ||
|
|
0e7e99a7eb | ||
|
|
49712b0ad3 | ||
|
|
ae5e25377d | ||
|
|
d104054074 | ||
|
|
b04d235edf | ||
|
|
e233914de0 | ||
|
|
44fe67fe6e | ||
|
|
967fe3144a | ||
|
|
83afd5f928 | ||
|
|
761fb07f56 | ||
|
|
259a8540a0 | ||
|
|
fbb374d4c3 | ||
|
|
10924c74aa | ||
|
|
5ea61774cb | ||
|
|
e372c7c532 | ||
|
|
dac6e5bfa8 | ||
|
|
2a442f97fa | ||
|
|
d329f341db | ||
|
|
368b9631ef | ||
|
|
783835bd53 | ||
|
|
bf6ea4b8e7 | ||
|
|
7213c8eda4 | ||
|
|
6f01c65feb | ||
|
|
93671088ba | ||
|
|
940b62b479 | ||
|
|
70f508ec84 | ||
|
|
bcca8046c4 | ||
|
|
9ec80c480f | ||
|
|
d74f935821 | ||
|
|
074004f5ab | ||
|
|
ee53de6f0a | ||
|
|
f66558ab54 | ||
|
|
5ada75909c | ||
|
|
e8e0343318 | ||
|
|
8c3db8f077 | ||
|
|
3412c42425 | ||
|
|
da3a76b6e3 | ||
|
|
3dd913ada5 | ||
|
|
732b908d32 | ||
|
|
01c237133d | ||
|
|
8be2771933 | ||
|
|
9153dfe015 | ||
|
|
87f3906e14 | ||
|
|
5e4106f6d1 | ||
|
|
f003025863 | ||
|
|
33c782dcd9 | ||
|
|
490065dff6 | ||
|
|
6c5a0c4498 | ||
|
|
befeb0e5c2 | ||
|
|
06c76433c5 | ||
|
|
5dc7093c5c | ||
|
|
41e437a4e9 | ||
|
|
686e41281d | ||
|
|
475da371eb | ||
|
|
c7db8fc78f | ||
|
|
ea23902fa9 | ||
|
|
b2ecc0ea81 | ||
|
|
9b460add41 | ||
|
|
56a96d766c | ||
|
|
ab10511513 | ||
|
|
f67e139128 | ||
|
|
e094d07017 | ||
|
|
b525e61a55 | ||
|
|
36af0d557c | ||
|
|
646f98ee4b | ||
|
|
c752fe2b85 | ||
|
|
c3524d2b4e | ||
|
|
bdb33fe99d | ||
|
|
eb1e867db2 | ||
|
|
77e344270b | ||
|
|
5d22149f7c | ||
|
|
2e8bc6d35f | ||
|
|
36e1b06470 | ||
|
|
9b564d3825 | ||
|
|
7788f3cf83 | ||
|
|
20ed855533 | ||
|
|
f47705d3b9 | ||
|
|
21732dae00 | ||
|
|
3493718e9f | ||
|
|
712539c23b | ||
|
|
45b16e221e | ||
|
|
f3a6636772 | ||
|
|
11cffd75c4 | ||
|
|
f8f4e7d1a4 | ||
|
|
75c80c5f15 | ||
|
|
7ad8a152a1 | ||
|
|
e1afa78540 | ||
|
|
faaa6f554a | ||
|
|
ffcd65c893 | ||
|
|
88db53a17e | ||
|
|
433abb623a | ||
|
|
fe7a622a11 | ||
|
|
437731e958 | ||
|
|
4efd168a4b | ||
|
|
7a948672bd | ||
|
|
af0ed70fbc | ||
|
|
4f2ae6c06c | ||
|
|
773cd6b6e2 | ||
|
|
a86d3852d8 | ||
|
|
28f5da1778 | ||
|
|
b3c23ce5db | ||
|
|
fac0df1779 | ||
|
|
4a526fe2a1 | ||
|
|
fd2a12ad49 | ||
|
|
f1b3ac40df | ||
|
|
7f93631cf2 | ||
|
|
c654f25427 | ||
|
|
5584856aec | ||
|
|
4261d03fd5 | ||
|
|
faed75db1c | ||
|
|
3ba356f9b5 | ||
|
|
c85a082ef0 | ||
|
|
e765855430 | ||
|
|
4f9a1d7547 | ||
|
|
aa86db344d | ||
|
|
93950f3ce0 | ||
|
|
bd3b0699e2 | ||
|
|
6a23503b7e | ||
|
|
a76dda2571 | ||
|
|
7014c1b65f | ||
|
|
d064b9493f | ||
|
|
9089123b98 | ||
|
|
1df2bd1abc | ||
|
|
136a5bac86 | ||
|
|
18cefe3e80 | ||
|
|
077bf466d6 | ||
|
|
8089d6bce6 | ||
|
|
f166b76468 | ||
|
|
4d24549bcc | ||
|
|
4e040a16ba | ||
|
|
2b0f424a45 | ||
|
|
fb3cb732d0 | ||
|
|
2c69392748 | ||
|
|
0993c51c7e | ||
|
|
1b4516177e | ||
|
|
ee557b2624 | ||
|
|
4659eb7b0f | ||
|
|
8424ea431e | ||
|
|
b31568b277 | ||
|
|
c604d86466 | ||
|
|
28e97e6a0d | ||
|
|
b8fd936de2 | ||
|
|
af3bb6d779 | ||
|
|
083eb42f84 | ||
|
|
e4cdf9f205 | ||
|
|
e89c9ae4a3 | ||
|
|
64c99db00d | ||
|
|
ed4b753bfd | ||
|
|
bf45971239 | ||
|
|
49a38c8da8 | ||
|
|
2ac4a43dd8 | ||
|
|
280447c0ea | ||
|
|
9407eaefce | ||
|
|
f70a7a6062 | ||
|
|
c27f355191 | ||
|
|
00168a8b0f | ||
|
|
c638efe1f0 | ||
|
|
577fdff474 | ||
|
|
17f025f020 | ||
|
|
6a2de27d42 | ||
|
|
cc2455b9f2 | ||
|
|
c188479f88 | ||
|
|
d840934dba | ||
|
|
cbae57bca5 | ||
|
|
72c7bbdd28 | ||
|
|
ed246bf9bf | ||
|
|
04f9dd17f5 | ||
|
|
0bb93c1fae | ||
|
|
18cbba2b13 | ||
|
|
eda0882451 | ||
|
|
608c05a408 | ||
|
|
c93e722d27 | ||
|
|
72ba0ab02b | ||
|
|
8831628015 | ||
|
|
c5e45d0053 | ||
|
|
39da91a33d | ||
|
|
c789cef26f | ||
|
|
855df3458c | ||
|
|
68e00fdafc | ||
|
|
b5f1132929 | ||
|
|
069e14cdc3 | ||
|
|
c087dec8bb | ||
|
|
a129de5085 | ||
|
|
6f4ccd752d | ||
|
|
173be4df5c | ||
|
|
2b97541a21 | ||
|
|
b03260a684 | ||
|
|
28647f38cb | ||
|
|
77915446ce | ||
|
|
5a14ab8dc3 | ||
|
|
edf7dd6c47 | ||
|
|
18c9499e93 | ||
|
|
b26f6b60de | ||
|
|
52cd522732 | ||
|
|
e49df41534 | ||
|
|
1dc0c02f51 | ||
|
|
4a60606711 | ||
|
|
af4d35fe57 | ||
|
|
98d511fadf | ||
|
|
71e05e3434 | ||
|
|
acb99b2120 | ||
|
|
69c3d8beb9 | ||
|
|
de7871a525 | ||
|
|
e87d0e362e | ||
|
|
35c9f25eaf |
47 changed files with 26394 additions and 10282 deletions
|
|
@ -1,12 +1,6 @@
|
|||
[target.x86_64-pc-windows-gnu]
|
||||
linker = "/usr/bin/x86_64-w64-mingw32-gcc"
|
||||
ar = "/usr/bin/x86_64-w64-mingw32-ar"
|
||||
|
||||
[target.i686-pc-windows-gnu]
|
||||
linker = "/usr/bin/i686-w64-mingw32-gcc"
|
||||
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
|
|
|||
34
.github/ISSUE_TEMPLATE/add-new-package-s-.md
vendored
Normal file
34
.github/ISSUE_TEMPLATE/add-new-package-s-.md
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
name: Add new package(s)
|
||||
about: You want to add new apps in the debloat list
|
||||
title: ""
|
||||
labels: package::addition
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Your phone model:**
|
||||
|
||||
**Packages:**
|
||||
|
||||
```
|
||||
com.this.is.a.bad.application
|
||||
com.this.is.another.bad.application
|
||||
...
|
||||
```
|
||||
|
||||
- [ ] **I removed all those packages on my phone**
|
||||
If not why. Leave the brackets blank and explain why.
|
||||
|
||||
## Document each package the best you can
|
||||
|
||||
**List**: `Google`|`Misc`|`OEM` (manufacturer)|`AOSP`|`Pending`|`Carrier` (isp).
|
||||
|
||||
**Removal**: `Recommended`, `Advanced`, `Expert` (this can break important features),
|
||||
or `Unsafe` (this can bootloop the phone or break extremely important features).
|
||||
|
||||
### \<package name\>
|
||||
|
||||
**List**: \<list\>
|
||||
**Removal**: \<recommendation list\>
|
||||
|
||||
> Description. Link to its Playstore page if it exists.
|
||||
19
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
19
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: You have an issue with the UAD software itself
|
||||
title: ""
|
||||
labels: bug
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**You have a solution?**
|
||||
What to do to fix the issue.
|
||||
|
||||
**UAD log**
|
||||
Upload the logfile generated by UAD.
|
||||
18
.github/ISSUE_TEMPLATE/debloat-issue-report.md
vendored
Normal file
18
.github/ISSUE_TEMPLATE/debloat-issue-report.md
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
name: Debloat issue report
|
||||
about: Your phone has unexpected issues after debloating
|
||||
title: ""
|
||||
labels: package::breakage
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Your phone model**:
|
||||
|
||||
**Describe the issue**
|
||||
A clear and concise description of what the problem is.
|
||||
|
||||
**You have a solution?**
|
||||
What to do to fix the issue.
|
||||
|
||||
**UAD log**
|
||||
Upload the logfile generated by UAD. It would be difficult to help you without it.
|
||||
10
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: You want a new feature
|
||||
title: ""
|
||||
labels: enhancement
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Describe the feature you want**
|
||||
A clear description of what you want to happen.
|
||||
37
.github/ISSUE_TEMPLATE/update-apps-description-or-recommendation.md
vendored
Normal file
37
.github/ISSUE_TEMPLATE/update-apps-description-or-recommendation.md
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
name: Update apps description or recommendation
|
||||
about: You want to improve/update a description/recommendation
|
||||
title: ""
|
||||
labels: package::documentation
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Your phone model**:
|
||||
|
||||
**Packages documentation to update:**
|
||||
|
||||
```
|
||||
com.this.is.a.application
|
||||
com.this.is.another.application
|
||||
...
|
||||
```
|
||||
|
||||
## Documentation Change
|
||||
|
||||
**List**: `Google`|`Misc`|`OEM` (manufacturer)|`AOSP`|`Pending`|`Carrier` (isp).
|
||||
**Removal**: `Recommended`, `Advanced`, `Expert` (this can break important features),
|
||||
or `Unsafe` (this can bootloop the phone or break extremely important features).
|
||||
|
||||
### \<package name\>
|
||||
|
||||
**List**: \<current list\> :arrow_right: \<proposed list\>
|
||||
**Removal**: \<current recommendation list\>
|
||||
:arrow_right: \<proposed recommendation list\>
|
||||
|
||||
### Current description
|
||||
|
||||
> Current description
|
||||
|
||||
### Proposed description
|
||||
|
||||
> Proposed description
|
||||
17
.github/release.yml
vendored
Normal file
17
.github/release.yml
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- package::documentation
|
||||
- package::addition
|
||||
- package::breakage
|
||||
- package::bootloop
|
||||
categories:
|
||||
- title: Added
|
||||
labels:
|
||||
- feature
|
||||
- title: Changed
|
||||
labels:
|
||||
- enhancement
|
||||
- title: Fixed
|
||||
labels:
|
||||
- bug
|
||||
65
.github/workflows/build.yml
vendored
65
.github/workflows/build.yml
vendored
|
|
@ -1,65 +0,0 @@
|
|||
name: Build
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Building ${{ matrix.build_target }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
build_target: [linux, macos, windows]
|
||||
include:
|
||||
- build_target: linux
|
||||
os: ubuntu-latest
|
||||
- build_target: macos
|
||||
os: macos-latest
|
||||
- build_target: windows
|
||||
os: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
override: true
|
||||
target: ${{ matrix.target }}
|
||||
profile: minimal
|
||||
- name: Install Linux dependencies
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get -qq update
|
||||
sudo apt-get install -y libxkbcommon-dev lld
|
||||
- uses: actions/checkout@v2
|
||||
- name: Cargo build (WGPU feature)
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --release
|
||||
- name: Renaming
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: mv target/release/uad_gui.exe target/release/uad_gui-${{ matrix.build_target }}.exe
|
||||
- name: Renaming
|
||||
if: matrix.os != 'windows-latest'
|
||||
run: mv target/release/uad_gui target/release/uad_gui-${{ matrix.build_target }}
|
||||
- name: Cargo build (Glow feature)
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --release --features glow
|
||||
- name: Renaming
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: mv target/release/uad_gui.exe target/release/uad_gui-${{ matrix.build_target }}-opengl.exe
|
||||
- name: Renaming
|
||||
if: matrix.os != 'windows-latest'
|
||||
run: mv target/release/uad_gui target/release/uad_gui-${{ matrix.build_target }}-opengl
|
||||
- name: Create pre-release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body_path: ${{ github.workspace }}/CHANGELOG.md
|
||||
files: target/release/uad_gui-*
|
||||
prerelease: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
65
.github/workflows/build_artifacts.yml
vendored
Normal file
65
.github/workflows/build_artifacts.yml
vendored
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
name: Build artifacts
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Building ${{ matrix.build_target }} [${{ matrix.graphics }}] [${{ matrix.update_feature }}]
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
build_target: [linux, macos, windows]
|
||||
graphics: [glow, wgpu]
|
||||
update_feature: [self-update, no-self-update]
|
||||
exclude:
|
||||
- build_target: windows
|
||||
update_feature: no-self-update
|
||||
include:
|
||||
- build_target: linux
|
||||
os: ubuntu-latest
|
||||
- build_target: macos
|
||||
os: macos-latest
|
||||
- build_target: windows
|
||||
os: windows-latest
|
||||
- graphics: glow
|
||||
renderer: "-opengl"
|
||||
- graphics: wgpu
|
||||
renderer: "" # Vulkan but we don't want this in the binary filename
|
||||
- update_feature: self-update
|
||||
update_name: "" # we don't want this in the binary filename
|
||||
- update_feature: no-self-update
|
||||
update_name: "-noseflupdate"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: rui314/setup-mold@v1 # faster linker
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target
|
||||
key: ${{ runner.os }}-release-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: ${{ runner.OS }}-release-
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
- name: Building
|
||||
run: cargo build --release --no-default-features --features ${{ matrix.graphics }},${{ matrix.update_feature }}
|
||||
- name: Creating ./bin directory
|
||||
run: mkdir -p bin
|
||||
- name: Renaming binaries [Windows]
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: mv target/release/uad_gui.exe bin/uad_gui-${{ matrix.build_target }}${{ matrix.renderer }}.exe
|
||||
- name: Renaming binaries [Others]
|
||||
if: matrix.os != 'windows-latest'
|
||||
run: mv target/release/uad_gui bin/uad_gui${{ matrix.update_name }}-${{ matrix.build_target }}${{ matrix.renderer }}
|
||||
- name: Tarball Linux/MacOS binary
|
||||
if: matrix.os != 'windows-latest'
|
||||
run: tar -czf bin/uad_gui${{ matrix.update_name }}-${{ matrix.build_target }}${{ matrix.renderer }}{.tar.gz,}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: uad_gui${{ matrix.update_name }}-${{ matrix.build_target }}${{ matrix.renderer }}
|
||||
path: bin/uad_gui-*
|
||||
138
.github/workflows/ci.yml
vendored
138
.github/workflows/ci.yml
vendored
|
|
@ -2,106 +2,56 @@ name: Continuous Integration
|
|||
on:
|
||||
push:
|
||||
paths:
|
||||
- '**.rs'
|
||||
- 'Cargo.lock'
|
||||
- 'Cargo.toml'
|
||||
- "**.rs"
|
||||
- "Cargo.lock"
|
||||
- "Cargo.toml"
|
||||
- "**.json"
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.rs'
|
||||
- 'Cargo.lock'
|
||||
- 'Cargo.toml'
|
||||
- "**.rs"
|
||||
- "Cargo.lock"
|
||||
- "Cargo.toml"
|
||||
- "**.json"
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check
|
||||
runs-on: ubuntu-latest
|
||||
lint:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
lint: [check, test, clippy, fmt]
|
||||
exclude: # https://github.com/community/community/discussions/7835
|
||||
- os: windows-latest
|
||||
lint: clippy
|
||||
- os: windows-latest
|
||||
lint: fmt
|
||||
- os: macOS-latest
|
||||
lint: clippy
|
||||
- os: macOS-latest
|
||||
lint: fmt
|
||||
include:
|
||||
- lint: check
|
||||
args: " --all-features"
|
||||
- lint: test
|
||||
args: ""
|
||||
- lint: clippy
|
||||
args: " --all --all-features -- -D warnings"
|
||||
- lint: fmt
|
||||
args: " --all -- --check"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt -qq update
|
||||
sudo apt install -y lld
|
||||
- uses: actions/cache@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: rui314/setup-mold@v1 # faster linker
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target
|
||||
key: ${{ runner.os }}-check-${{ hashFiles('**/Cargo.lock') }}
|
||||
- uses: actions-rs/toolchain@v1
|
||||
key: ${{ runner.os }}-${{ matrix.lint }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: ${{ runner.OS }}-${{ matrix.lint }}-
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: nightly
|
||||
override: true
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --all-features
|
||||
|
||||
test:
|
||||
name: Test Suite
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt -qq update
|
||||
sudo apt install -y libxkbcommon-dev lld
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-test-${{ hashFiles('**/Cargo.lock') }}
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: nightly
|
||||
override: true
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
|
||||
fmt:
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: nightly
|
||||
override: true
|
||||
- run: rustup component add rustfmt
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
clippy:
|
||||
name: Clippy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt -qq update
|
||||
sudo apt install -y lld
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-clippy-${{ hashFiles('**/Cargo.lock') }}
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: nightly
|
||||
override: true
|
||||
- run: rustup component add clippy
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --all -- -D warnings
|
||||
components: clippy,rustfmt
|
||||
- run: cargo ${{ matrix.lint }}${{ matrix.args }}
|
||||
|
|
|
|||
50
.github/workflows/release.yml
vendored
Normal file
50
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
name: Build and release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "**.rs"
|
||||
- "Cargo.lock"
|
||||
- "Cargo.toml"
|
||||
tags-ignore:
|
||||
- dev-build
|
||||
|
||||
env:
|
||||
dev_tag: dev-build
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./.github/workflows/build_artifacts.yml
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Downloads artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: bin
|
||||
- name: Create pre-release
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body_path: ${{ github.workspace }}/CHANGELOG.md
|
||||
files: bin/*/uad_gui-*
|
||||
prerelease: true
|
||||
# - name: Update dev-build tag
|
||||
# if: ${{ github.event_name == 'push' }}
|
||||
# run: |
|
||||
# git tag -d ${{ env.dev_tag }} || true
|
||||
# git push origin :refs/tags/${{ env.dev_tag }} || true
|
||||
# git tag ${{ env.dev_tag }}
|
||||
# git push origin ${{ env.dev_tag }}
|
||||
# - name: Create dev-build release
|
||||
# if: ${{ github.event_name == 'push' }}
|
||||
# uses: softprops/action-gh-release@v1
|
||||
# with:
|
||||
# generate_release_notes: true
|
||||
# files: bin/*/uad_gui-*
|
||||
# prerelease: true
|
||||
# tag_name: ${{ env.dev_tag }}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
|||
debug/
|
||||
target/
|
||||
*.log
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
image: rustlang/rust:nightly-bullseye
|
||||
|
||||
default:
|
||||
before_script:
|
||||
- rustc --version
|
||||
- cargo --version
|
||||
|
||||
stages:
|
||||
- build
|
||||
|
||||
i686-pc-windows-gnu:
|
||||
stage: build
|
||||
|
||||
before_script:
|
||||
- apt update -yqq && apt install -yqq mingw-w64
|
||||
- rustup target add i686-pc-windows-gnu
|
||||
|
||||
script:
|
||||
- cargo build --features glow --release --target i686-pc-windows-gnu
|
||||
- mv target/i686-pc-windows-gnu/release/uad_gui.exe target/i686-pc-windows-gnu/release/uad_gui-opengl.exe
|
||||
|
||||
artifacts:
|
||||
paths:
|
||||
- target/i686-pc-windows-gnu/release/uad_gui-opengl.exe
|
||||
|
||||
x86_64-pc-windows-gnu:
|
||||
stage: build
|
||||
|
||||
before_script:
|
||||
- apt update -yqq && apt install -yqq mingw-w64
|
||||
- rustup target add x86_64-pc-windows-gnu
|
||||
|
||||
script:
|
||||
- cargo build --features glow --release --target x86_64-pc-windows-gnu
|
||||
- mv target/x86_64-pc-windows-gnu/release/uad_gui.exe target/x86_64-pc-windows-gnu/release/uad_gui-opengl.exe
|
||||
- cargo build --features wgpu --release --target x86_64-pc-windows-gnu
|
||||
|
||||
artifacts:
|
||||
paths:
|
||||
- target/x86_64-pc-windows-gnu/release/uad_gui.exe
|
||||
- target/x86_64-pc-windows-gnu/release/uad_gui-opengl.exe
|
||||
|
||||
x86_64-unknown-linux-gnu:
|
||||
stage: build
|
||||
|
||||
before_script:
|
||||
- apt update -yqq && apt install -yqq libxkbcommon-dev lld
|
||||
|
||||
script:
|
||||
- cargo build --features glow --release --target x86_64-unknown-linux-gnu
|
||||
- mv target/x86_64-unknown-linux-gnu/release/uad_gui target/x86_64-unknown-linux-gnu/release/uad_gui-opengl
|
||||
- cargo build --features wgpu --release --target x86_64-unknown-linux-gnu
|
||||
|
||||
artifacts:
|
||||
paths:
|
||||
- target/x86_64-unknown-linux-gnu/release/uad_gui
|
||||
- target/x86_64-unknown-linux-gnu/release/uad_gui-opengl
|
||||
140
CHANGELOG.md
140
CHANGELOG.md
|
|
@ -5,60 +5,166 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
The sections should follow the order `Packaging`, `Added`, `Changed`, `Fixed`
|
||||
The sections should follow the order `Apps`, `Added`, `Changed`, `Fixed`, `Packaging`
|
||||
and `Removed`.
|
||||
|
||||
## [Unreleased]
|
||||
## [0.6] - Unreleased
|
||||
|
||||
**WARNING: Settings specification has changed. Previous user settings will be erased**.
|
||||
|
||||
### Added
|
||||
- [[#374](https://github.com/0x192/universal-android-debloater/pull/374)] **Device-specific persistent configuration:** Some settings are now device-specific which means you can maintain different settings across several devices.
|
||||
|
||||
- [[#447](https://github.com/0x192/universal-android-debloater/pull/447)] **Backup/Restore the state of a device:** Quick and easy way to save the state of all the system apps on a device and restore it.
|
||||
|
||||
- [[#450](https://github.com/0x192/universal-android-debloater/pull/450)] **Warn the user when a work profile is detected:** Displays a warning message when switching to a work profile user and displays unavailable work profile users in the settings.
|
||||
|
||||
### Changed
|
||||
- [[#374](https://github.com/0x192/universal-android-debloater/pull/374)] ALL settings are now persistent.
|
||||
|
||||
### Fixed
|
||||
- [[#448](https://github.com/0x192/universal-android-debloater/pull/448)] UAD crash when interacting with work profiles on recent phones.
|
||||
|
||||
### Removed
|
||||
- The `Export current selection` button and the unintuitive auto import selection (see https://github.com/0x192/universal-android-debloater/issues/192) have been replaced by the new backup/restore system.
|
||||
|
||||
## [0.5.1] - 2022-07-03
|
||||
|
||||
Since `0.5.0`, all changes related to apps are available to users without downloading a new version of UAD as the software directly download the json debloat list from Github. These changes can be tracked in commits with `[Pkg]` in their name. [See the commits](https://github.com/0x192/universal-android-debloater/commits/main)
|
||||
|
||||
### Added
|
||||
- [[#209](https://github.com/0x192/universal-android-debloater/issues/209)] Persistent highlighting when you click on a package
|
||||
|
||||
### Changed
|
||||
- `neededBy` and `dependencies` field can now list multiple packages (feature not visible in the UI yet)
|
||||
|
||||
### Fixed
|
||||
- [[#286](https://github.com/0x192/universal-android-debloater/issues/286)] UAD stuck on "Downloading UAD lists. Please wait" screen
|
||||
|
||||
## Packaging
|
||||
- [[#256](https://github.com/0x192/universal-android-debloater/issues/256)] Fixed typo in the release name of the noseflupdate variation
|
||||
- Bump dependencies
|
||||
|
||||
## [0.5.0] - 2022-04-03
|
||||
|
||||
### Apps
|
||||
|
||||
- [[#115](https://github.com/0x192/universal-android-debloater/issues/115)] Added `com.tblenovo.lenovotips` to the recommended list.
|
||||
- [[#120](https://github.com/0x192/universal-android-debloater/pull/120)] Move Google keyboard to `Advanced` list (Default keyboards should not be in the `Recommended` list)
|
||||
- [[#169](https://github.com/0x192/universal-android-debloater/issues/154) Move `com.android.htmlviewer` to the `Expert` list. Removing it bootloop the device on MIUI 12.5.4+.
|
||||
|
||||
Huge thanks to [@KarlRamstedt](https://github.com/KarlRamstedt) for their help in this major debloat list update:
|
||||
- [[#122](https://github.com/0x192/universal-android-debloater/pull/122)] Added a bunch of new packages
|
||||
- [[#122](https://github.com/0x192/universal-android-debloater/pull/122)] A lot of description updates and fixes
|
||||
- [[#122](https://github.com/0x192/universal-android-debloater/pull/122) | [#138](https://github.com/0x192/universal-android-debloater/pull/138)] Big revision of the recommendations according to more consistent criteria ([see the wiki](https://github.com/0x192/universal-android-debloater/wiki/FAQ#how-are-the-recommendations-chosen))
|
||||
|
||||
### Added
|
||||
- [[#68](https://github.com/0x192/universal-android-debloater/issue/68)] **Unselect all button**: Let's you unselect all the packages you see on screen (i.e in the current filtered list).
|
||||
- [[#119](https://github.com/0x192/universal-android-debloater/issue/119)] **Reboot button**: Let's you quickly reboot the currently selected device.
|
||||
- [[#110](https://github.com/0x192/universal-android-debloater/pull/110)] **Remote `uad_lists.json` download**: The debloat list is now directly fetched from the main branch of this repo when you launch UAD. This means there is no longer the need to release a new version of UAD for updating the debloat lists! :rocket:
|
||||
- [[#121](https://github.com/0x192/universal-android-debloater/pull/121)] :arrows_counterclockwise: **UAD self-update**: UAD will now check at launch if there is a new version of itself and enable you to perform the update directly from the app! :rocket:
|
||||
|
||||
### Changed
|
||||
- [[#165](https://github.com/0x192/universal-android-debloater/issues/165)] UAD now tries every 500ms (for 1min) to initiate an ADB connection until a device is found during the `FindingPhones` loading state.
|
||||
- All the init process was reworked and a status message is displayed at each stage (`DownloadingList`, `FindingPhones`,`LoadingPackages`,`UpdatingUad` `Ready`) so you know what is happening.
|
||||
- Minor UI changes
|
||||
|
||||
### Packaging
|
||||
- Add a `no-self-update` build for MacOS and Linux. Useful if UAD is distributed into repositories. The update process will then be managed by a package manager.
|
||||
- MacOS builds are now also be released as a compressed tarball (like for Linux). You won't need to manually add the executable permission anymore. ([more info](https://github.com/actions/upload-artifact/issues/38))
|
||||
- Bump dependencies
|
||||
|
||||
|
||||
## [0.4.1] - 2022-01-31
|
||||
|
||||
### Fixed
|
||||
- Selection counter never decreasing.
|
||||
|
||||
## [0.4] - 2022-01-30
|
||||
|
||||
### Apps
|
||||
- [[#92](https://github.com/0x192/universal-android-debloater/pull/92)] Added 3 Fairphone packages + 7 Qualcomm packages (thanks [@VeH-c](https://github.com/VeH-c))
|
||||
- [[#87](https://github.com/0x192/universal-android-debloater/pull/87)] Added 2 Unihertz packages (thanks [@rar0ch](https://github.com/rar0ch))
|
||||
- [[#52](https://github.com/0x192/universal-android-debloater/issues/52)] Added `uk.co.ee.myee` to the debloat lists (thanks [@lawson58](https://github.com/lawson85)).
|
||||
- [[#58](https://github.com/0x192/universal-android-debloater/issues/52)] Added `android` to the debloat lists with the tag `Unsafe`.
|
||||
- Added 2 new Xiaomi packages to the `Recommended` list.
|
||||
- Multiple package description improvement (thanks [@jonas-ott](https://github.com/jonas-ott) and [@felurx](https://github.com/felurx) for the help)
|
||||
- Review of the package lists recommendations. The `Recommended` debloat list is now safer (less likely to remove something you'd want to keep).
|
||||
|
||||
### Added
|
||||
- [[#49](https://github.com/0x192/universal-android-debloater/issues/49)] Multi-device support: You are now able to select a device among the list of all ADB connected devices/emulators.
|
||||
- [[#44](https://github.com/0x192/universal-android-debloater/issues/44)] Persistent settings: Settings (only `theme` for now) are saved to a config file. Its location follows [the standards of the different OS](https://github.com/dirs-dev/dirs-rs#example).
|
||||
- Links to the Github page, wiki, github issues and logfiles in the `About` page.
|
||||
|
||||
### Changed
|
||||
- [[#65](https://github.com/0x192/universal-android-debloater/issues/65)] ADB commands now run in parallel and asynchronously! This means no more UI freeze when performing long/many actions! :rocket:
|
||||
- UI now updates itself in real time when performing ADB actions (thanks to async & multithreading). Before, it waited for the end of all actions.
|
||||
- Logfiles are now located in a more conventional place: [cache_dir](https://docs.rs/dirs/latest/dirs/).
|
||||
- Previous logs are no longer overwritten. The logger now only appends to the current logfile of the day (UAD_%Y%m%d.log).
|
||||
- Each new day the logger will create a new file on UAD launch.
|
||||
- [[#78](https://github.com/0x192/universal-android-debloater/issues/78)] Disable mode is now only available on Android 6+ because the disable ADB commands do not work without root on older devices. The setting will be greyed-out for those devices.
|
||||
- Minor light theme update
|
||||
|
||||
|
||||
### Fixed
|
||||
- [[#50](https://github.com/0x192/universal-android-debloater/issues/50)] Resync button flipping theme back to `Lupin`.
|
||||
- [Regression ([048e7f](https://github.com/0x192/universal-android-debloater/commit/048e7fc8fd6d44b0e8ba933c289249366254a9cc))] Weird disabled/greyed action button with older devices (< Android 8.0). Package could be selected but no action was performed.
|
||||
- [[#78](https://github.com/0x192/universal-android-debloater/issues/78)] Packages not being actually uninstalled on older devices (< Android 6.0). Without root we can only use `pm block`/`pm unblock` for Android KitKit (4.4) and `pm hide`/`pm unhide` on Android Lollipop (5.x).
|
||||
|
||||
### Packaging
|
||||
- For Arch-based users, UAD is now available in the AUR: `universal-android-debloater-bin` (binary) and `universal-android-debloater` (from source)
|
||||
- Bump dependencies
|
||||
|
||||
|
||||
## [0.3] - 2021-10-10
|
||||
|
||||
### Added
|
||||
- Multi-user support (#16): You can now debloat/restore apps for any user of the phone (not only the primary user 0).
|
||||
- [[#16](https://github.com/0x192/universal-android-debloater/issues/16)] Multi-user support: You can now debloat/restore apps for any user of the phone (not only the primary user 0).
|
||||
- `Multi user mode` setting (default to `on` for Android 5+) allowing to remove packages for all users ([a work profile is another user](https://developer.android.com/work/managed-profiles)) instead of only the selected user.
|
||||
- User switcher (picklist).
|
||||
- New themes! (#11) : light, dark and lupin. Lupin theme is now the new default theme. Themes can be changed from the settings.
|
||||
- Description field scrollbar: you can now scroll long descriptions (#40).
|
||||
- [[#11](https://github.com/0x192/universal-android-debloater/issues/11)] New themes: light, dark and lupin. Lupin theme is now the new default theme. Themes can be changed from the settings.
|
||||
- [[#40](https://github.com/0x192/universal-android-debloater/issues/40)] Description field scrollbar: you can now scroll long descriptions.
|
||||
|
||||
## Fixed
|
||||
### Fixed
|
||||
- [Regression] Unsafe packages can be deleted without enabling `expert mode`.
|
||||
- The refresh button doesn't update settings when a (new) phone is connected.
|
||||
- [Regression] Restore buttons are disabled when connecting an Android 8.0 phone.
|
||||
- Refresh icon does not appear (#17)
|
||||
- [[#17](https://github.com/0x192/universal-android-debloater/issues/17)] Refresh icon does not appear.
|
||||
|
||||
## [0.2.2] - 2021-09-30
|
||||
|
||||
## Fixed
|
||||
### Fixed
|
||||
- Crash when connecting a LG device (#33)
|
||||
|
||||
## [0.2.1] - 2021-09-28
|
||||
|
||||
## Added
|
||||
### Added
|
||||
- Software version in the navigation panel
|
||||
|
||||
## Packaging
|
||||
### Packaging
|
||||
- `wgpu` renderer is not the default renderer (you don't need to add `--features wgpu` if you want to build UAD with `wgpu`)
|
||||
|
||||
### Fixed
|
||||
- Exported selection not found (#35)
|
||||
- [[#35](https://github.com/0x192/universal-android-debloater/issues/35)] Exported selection not found
|
||||
|
||||
## [0.2] - 2021-09-26
|
||||
|
||||
### Added
|
||||
- UAD now comes with a logger. Debug information will be written to a `uad.log` file (Warning level log in *stdout*) (#2)
|
||||
- Support for older phone (< Android 8.0) (#15):
|
||||
- [[#2](https://github.com/0x192/universal-android-debloater/issues/2)] UAD now comes with a logger. Debug information will be written to a `uad.log` file (Warning level log in *stdout*)
|
||||
- [[#15](https://github.com/0x192/universal-android-debloater/issues/15)] Support for older phone (< Android 8.0):
|
||||
- Disable mode in settings: clear and disable packages instead of uninstalling them (default for old phones because you can't restore uninstalled packages)
|
||||
- Export your selection in the `uad_exported_selection.txt` file. Packages from this file (if found in the current directory) will be automatically selected upon the start of UAD (or after a refresh) (#8)
|
||||
- [[#8](https://github.com/0x192/universal-android-debloater/issues/8)] Export your selection in the `uad_exported_selection.txt` file. Packages from this file (if found in the current directory) will be automatically selected upon the start of UAD (or after a refresh).
|
||||
|
||||
### Changed
|
||||
- UAD will no longer crash at start if it doesn't find ADB but will display a useful error message (#25)
|
||||
- Better handling of ADB errors (#3)
|
||||
- [[#25](https://github.com/0x192/universal-android-debloater/issues/25)] UAD will no longer crash at start if it doesn't find ADB but will display a useful error message
|
||||
- [[#3](https://github.com/0x192/universal-android-debloater/issues/3)] Better handling of ADB errors
|
||||
- Updated dependencies (compatibility with [Iced](https://github.com/iced-rs/iced) main branch latest commit)
|
||||
- Cleanup and refactoring of the code
|
||||
- Performance improvement
|
||||
- Various UI/UX improvement
|
||||
- The `Debloat/Restore selection` button has been split in 2 buttons: `removing` and `restoring`
|
||||
|
||||
## Packaging
|
||||
### Packaging
|
||||
- Added an alternative build that uses [OpenGL](https://fr.wikipedia.org/wiki/OpenGL) (instead of [Vulkan](https://fr.wikipedia.org/wiki/Vulkan_(API))) for compatibility with older computers. If you encouter some visual glitches with the default Vulkan build you should try the OpenGL build.
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
2304
Cargo.lock
generated
2304
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
33
Cargo.toml
33
Cargo.toml
|
|
@ -1,34 +1,43 @@
|
|||
cargo-features = ["strip"] # nightly
|
||||
|
||||
[package]
|
||||
name = "uad_gui"
|
||||
description = "A cross-platform GUI debloater for android devices"
|
||||
version = "0.3.0"
|
||||
version = "0.5.1"
|
||||
authors = ["w1nst0n"]
|
||||
license = "GPL-3.0"
|
||||
homepage = "https://gitlab.com/W1nst0n/universal-android-debloater-rs"
|
||||
repository = "https://gitlab.com/W1nst0n/universal-android-debloater-rs"
|
||||
homepage = "https://github.com/0x192/universal-android-debloater"
|
||||
repository = "https://github.com/0x192/universal-android-debloater"
|
||||
readme = "README.md"
|
||||
keywords = ["debloater", "android", "adb", "privacy", "bloatware"]
|
||||
categories = ["gui"]
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
default = ["wgpu"]
|
||||
wgpu = [] # iced/wgpu is default
|
||||
glow = ["iced/glow"] # OpenGL support
|
||||
default = ["wgpu", "self-update"]
|
||||
wgpu = [] # Iced/wgpu is default
|
||||
glow = ["iced/glow"] # OpenGL support
|
||||
self-update = ["flate2", "tar"]
|
||||
no-self-update = []
|
||||
|
||||
[dependencies]
|
||||
iced = { git = "https://github.com/hecrj/iced.git" }
|
||||
iced = { git = "https://github.com/iced-rs/iced.git" }
|
||||
iced_native = { git = "https://github.com/iced-rs/iced.git" }
|
||||
serde = { version = "^1.0", features = ["derive"] }
|
||||
serde_json = "^1.0"
|
||||
static_init = "^1.0"
|
||||
fern = { version = "^0.6", features = ["colored"] }
|
||||
fern = { version = "^0", features = ["colored"] }
|
||||
chrono = { version = "^0.4", default-features = false, features = ["std", "clock"] }
|
||||
log = "^0.4"
|
||||
chrono = "^0.4"
|
||||
regex = "^1.5"
|
||||
toml = "^0"
|
||||
dirs = "^5.0.0"
|
||||
ureq = { version = "*", features = ["json"] }
|
||||
retry = { version = "^2.0.0" }
|
||||
|
||||
[target.'cfg(not(target_os = "windows"))'.dependencies]
|
||||
flate2 = { version = "^1", optional = true }
|
||||
tar = { version = "^0.4", optional = true }
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
lto = true
|
||||
strip = "symbols"
|
||||
strip = "symbols"
|
||||
|
|
|
|||
4
LICENSE
4
LICENSE
|
|
@ -632,7 +632,7 @@ state the exclusion of warranty; and each file should have at least
|
|||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
Universal Android Debloater GUI
|
||||
Copyright (C) 2021 W1nst0n
|
||||
Copyright (C) 2021 W1nst0n
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
|
|||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) 2021 W1nst0n
|
||||
Universal Android Debloater GUI Copyright (C) 2021 W1nst0n
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
|
|
|||
236
README.md
236
README.md
|
|
@ -1,133 +1,179 @@
|
|||
# Universal Android Debloater GUI
|
||||
**DISCLAIMER**: Use it at your own risk. I am not responsible for anything that could happen to your phone.
|
||||
|
||||

|
||||
**DISCLAIMER**: Use at your own risk. I am not responsible for anything that
|
||||
could happen to your phone.
|
||||
|
||||
**This software is still in a early stage of developpement. Check out the issues, and feel free to contribute!**
|
||||
<img src="/resources/screenshots/v0.5.0.png" width="850" alt="uad_screenshot">
|
||||
|
||||
**This software is still in an early stage of development. Check out the issues, and feel free to contribute!**
|
||||
|
||||
## Summary
|
||||
This is a complete rewrite in Rust of the [UAD project](https://gitlab.com/W1nst0n/universal-android-debloater) which aims at improving privacy and battery performance by removing unnecessary and obscure system apps. This can also contribute to improve security by reducing [the attack surface](https://en.wikipedia.org/wiki/Attack_surface).
|
||||
|
||||
Packages are as well documented as possible in order to provide a better understanding of what you can delete or not. The worse thing which could happen is removing an essential system package needed during boot causing then an unfortunate bootloop. After about 5 failed system boots, the phone will automatically reboot in recovery mode and you'll have to perform a FACTORY RESET. So make a backup!
|
||||
This is a complete rewrite in Rust of the [UAD project](https://gitlab.com/W1nst0n/universal-android-debloater),
|
||||
which aims to improve privacy and battery performance by removing unnecessary
|
||||
and obscure system apps.
|
||||
This can also contribute to improve security by reducing [the attack surface](https://en.wikipedia.org/wiki/Attack_surface).
|
||||
|
||||
In any case, you can NOT brick your device with this software! That's the main thing, right?
|
||||
Packages are as well documented as possible in order to provide a better
|
||||
understanding of what you can delete or not. The worst issue that could happen
|
||||
is removing an essential system package needed during boot causing then an unfortunate
|
||||
bootloop. After about 5 failed system boots, the phone will automatically reboot
|
||||
in recovery mode, and you'll have to perform a FACTORY RESET. Make a backup first!
|
||||
|
||||
## Features
|
||||
* [X] Uninstall/Disable and Restore/Enable system packages
|
||||
* [X] Multi-user support (e.g apps in work profiles)
|
||||
* [X] Export/Import your selection in `uad_exported_selection.txt`
|
||||
* [X] Logging of all your action in the `uad.log` file
|
||||
In any case, you **CANNOT** brick your device with this software!
|
||||
That's the main point, right?
|
||||
|
||||
NB : System apps cannot really be uninstalled without root (see the [FAQ](https://gitlab.com/W1nst0n/universal-android-debloater-rs/-/wikis/FAQ))
|
||||
## Features
|
||||
|
||||
## Universal debloat lists
|
||||
* [X] GFAM (Google/Facebook/Amazon/Microsoft)
|
||||
* [X] AOSP
|
||||
* [X] Manufacturers (OEM)
|
||||
* [X] Mobile carriers
|
||||
* [X] Qualcomm / Mediatek / Miscellaneous
|
||||
- [x] Uninstall/Disable and Restore/Enable system packages
|
||||
- [x] Multi-user support (e.g. apps in work profiles)
|
||||
- [x] Export/Import your selection in `uad_exported_selection.txt`
|
||||
- [x] Multi-device support: you can connect multiple phones at the same time
|
||||
- [x] All your actions are logged, so you never forget what you've done
|
||||
|
||||
NB : System apps cannot truly be uninstalled without root (see the [FAQ](https://github.com/0x192/universal-android-debloater/wiki/FAQ))
|
||||
|
||||
## Universal Debloat Lists
|
||||
|
||||
- [x] GFAM (Google/Facebook/Amazon/Microsoft)
|
||||
- [x] AOSP
|
||||
- [x] Manufacturers (OEM)
|
||||
- [x] Mobile carriers
|
||||
- [x] Qualcomm / Mediatek / Miscellaneous
|
||||
|
||||
## Manufacturers debloat lists
|
||||
* [ ] Archos
|
||||
* [X] Asus
|
||||
* [ ] Blackberry
|
||||
* [ ] Gionee
|
||||
* [X] LG
|
||||
* [X] Google
|
||||
* [ ] HTC
|
||||
* [X] Huawei
|
||||
* [X] Motorola
|
||||
* [X] Nokia
|
||||
* [X] OnePlus
|
||||
* [X] Oppo
|
||||
* [X] Samsung
|
||||
* [X] Sony
|
||||
* [ ] TCL
|
||||
* [ ] Wiko
|
||||
* [X] Xiaomi
|
||||
* [ ] ZTE
|
||||
|
||||
- [ ] Archos
|
||||
- [x] Asus
|
||||
- [ ] Blackberry
|
||||
- [ ] Gionee
|
||||
- [x] LG
|
||||
- [x] Google
|
||||
- [ ] iQOO
|
||||
- [x] Fairphone
|
||||
- [ ] HTC
|
||||
- [x] Huawei
|
||||
- [x] Motorola
|
||||
- [x] Nokia
|
||||
- [x] OnePlus
|
||||
- [x] Oppo
|
||||
- [x] Realme
|
||||
- [x] Samsung
|
||||
- [x] Sony
|
||||
- [x] Tecno
|
||||
- [ ] TCL
|
||||
- [x] Unihertz
|
||||
- [x] Vivo/iQOO
|
||||
- [ ] Wiko
|
||||
- [x] Xiaomi
|
||||
- [x] ZTE
|
||||
|
||||
## Mobile carriers debloat lists
|
||||
| Country | Carriers |
|
||||
|-----------------|-----------------------------------|
|
||||
| France | Orange, SFR, Free, Bouyges/Sosh |
|
||||
| USA | T-Mobile, Verizon, Sprint, AT&T |
|
||||
| Germany | Telekom |
|
||||
|
||||
| Country | Carriers |
|
||||
| ------- | ------------------------------- |
|
||||
| France | Orange, SFR, Free, Bouygues |
|
||||
| USA | T-Mobile, Verizon, Sprint, AT&T |
|
||||
| Germany | Telekom |
|
||||
| UK | EE |
|
||||
|
||||
## How to use it
|
||||
|
||||
## How to use it
|
||||
- **Read the [FAQ](https://github.com/0x192/universal-android-debloater/wiki/FAQ)!**
|
||||
- **Do a proper backup of your data! You can never be too careful!**
|
||||
- Enable *Developer Options* on your smartphone.
|
||||
- Turn on *USB Debugging* from the developper panel.
|
||||
- From the settings, disconnect from any OEM accounts (when you delete OEM account package it could lock you on the lockscreen because the phone can't associate your identity anymore)
|
||||
- Install ADB:
|
||||
<p>
|
||||
<details>
|
||||
<summary>LINUX</summary>
|
||||
- Enable _Developer Options_ on your smartphone.
|
||||
- Turn on _USB Debugging_ from the developer panel.
|
||||
- From the settings, disconnect from any OEM accounts (when you delete an OEM
|
||||
account package it could lock you on the lockscreen because the phone can't
|
||||
associate your identity anymore)
|
||||
- Install ADB (see the intructions by clicking on your OS below):
|
||||
<p>
|
||||
<details>
|
||||
<summary>LINUX</summary>
|
||||
|
||||
- Install *Android platform tools* on your PC :
|
||||
- Install _Android platform tools_ on your PC :
|
||||
|
||||
Debian Base:
|
||||
```bash
|
||||
$ sudo apt install android-sdk-platform-tools
|
||||
```
|
||||
Arch-Linux Base:
|
||||
```bash
|
||||
$ sudo pacman -S android-tools
|
||||
```
|
||||
Red Hat Base:
|
||||
```bash
|
||||
$ sudo yum install android-tools
|
||||
```
|
||||
OpenSUSE Base:
|
||||
```bash
|
||||
$ sudo zypper install android-tools
|
||||
```
|
||||
</details>
|
||||
</p>
|
||||
Debian Base:
|
||||
|
||||
<p>
|
||||
<details>
|
||||
<summary>MAC OS</summary>
|
||||
```bash
|
||||
sudo apt install android-sdk-platform-tools
|
||||
```
|
||||
|
||||
- Install [Homebrew](https://brew.sh/)
|
||||
- Install *Android platform tools*
|
||||
Arch-Linux Base:
|
||||
|
||||
```bash
|
||||
$ brew install android-platform-tools
|
||||
```
|
||||
</details>
|
||||
</p>
|
||||
```bash
|
||||
sudo pacman -S android-tools
|
||||
```
|
||||
|
||||
<p>
|
||||
<details>
|
||||
<summary>WINDOWS</summary>
|
||||
Red Hat Base:
|
||||
|
||||
- Download [android platform tools](https://dl.google.com/android/repository/platform-tools-latest-windows.zip) and unzip it somewhere. [Add the folder to your PATH](https://www.architectryan.com/2018/03/17/add-to-the-path-on-windows-10/).
|
||||
```bash
|
||||
sudo yum install android-tools
|
||||
```
|
||||
|
||||
- [Install USB drivers of your device](https://developer.android.com/studio/run/oem-usb#Drivers)
|
||||
- Check your device is detected:
|
||||
```batch
|
||||
> adb devices
|
||||
```
|
||||
</details>
|
||||
</p>
|
||||
OpenSUSE Base:
|
||||
|
||||
```bash
|
||||
sudo zypper install android-tools
|
||||
```
|
||||
|
||||
- Download the latest stable release of UAD GUI [here](https://github.com/0x192/universal-android-debloater/releases).
|
||||
</details>
|
||||
</p>
|
||||
|
||||
**NOTE:** Chinese phones users may need to use the AOSP list for removing some stock apps because those chinese manufacturers (especially Xiaomi and Huawei) have been using the name of AOSP packages for their own (modified & closed-source) apps.
|
||||
<p>
|
||||
<details>
|
||||
<summary>MAC OS</summary>
|
||||
|
||||
**IMPORTANT NOTE:** You will have to run this software whenever your OEM push an update to your phone as some *uninstalled* system apps could be reinstalled.
|
||||
- Install [Homebrew](https://brew.sh/)
|
||||
- Install _Android platform tools_
|
||||
|
||||
```bash
|
||||
brew install android-platform-tools
|
||||
```
|
||||
|
||||
</details>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<details>
|
||||
<summary>WINDOWS</summary>
|
||||
|
||||
- Download [android platform tools](https://dl.google.com/android/repository/platform-tools-latest-windows.zip)
|
||||
and unzip it somewhere.
|
||||
- [Add the android platform tools to your PATH](https://www.architectryan.com/2018/03/17/add-to-the-path-on-windows-10/)
|
||||
**OR** make sure to launch UAD from the same directory.
|
||||
|
||||
- [Install USB drivers for your device](https://developer.android.com/studio/run/oem-usb#Drivers)
|
||||
- Check your device is detected:
|
||||
|
||||
```bash
|
||||
adb devices
|
||||
```
|
||||
|
||||
</details>
|
||||
</p>
|
||||
|
||||
- Download the latest release of UAD GUI for your Operating System [here](https://github.com/0x192/universal-android-debloater/releases).
|
||||
Take the `opengl` version only if the default version (with a Vulkan backend)
|
||||
doesn't launch.
|
||||
|
||||
**NOTE:** Chinese phones users may need to use the AOSP list for removing some stock
|
||||
apps because those Chinese manufacturers (especially Xiaomi and Huawei) have been
|
||||
using the name of AOSP packages for their own (modified & closed-source) apps.
|
||||
|
||||
**IMPORTANT NOTE:** You will have to run this software whenever your OEM pushes
|
||||
an update to your phone as some _uninstalled_ system apps could be reinstalled.
|
||||
|
||||
## How to contribute
|
||||
|
||||
Hey-hey-hey! Don't go away so fast! This is a community project. That means I need you! I'm sure you want to make this project better anyway.
|
||||
Hey-hey-hey! Don't go away so fast! This is a community project.
|
||||
That means I need you! I'm sure you want to make this project better anyway.
|
||||
|
||||
==> [How to contribute](https://github.com/0x192/universal-android-debloater/wiki)
|
||||
|
||||
## Special thanks
|
||||
|
||||
- [@mawilms](https://github.com/mawilms) for his LotRO plugin manager ([Lembas](https://github.com/mawilms/lembas)) which helped me a lot to understand how to use the [Iced](https://github.com/hecrj/iced) GUI library.
|
||||
- [@casperstorm](https://github.com/casperstorm) for the UI/UX inspiration.
|
||||
- [@mawilms](https://github.com/mawilms) for his LotRO plugin manager ([Lembas](https://github.com/mawilms/lembas))
|
||||
which helped me a lot to understand how to use the [Iced](https://github.com/hecrj/iced)
|
||||
GUI library.
|
||||
- [@casperstorm](https://github.com/casperstorm) for the UI/UX inspiration.
|
||||
|
|
|
|||
16
build.rs
16
build.rs
|
|
@ -1,16 +0,0 @@
|
|||
use std::env::var;
|
||||
|
||||
fn main() {
|
||||
let target = var("TARGET").unwrap();
|
||||
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
|
||||
if target.contains("windows") && !target.contains("msvc") {
|
||||
println!("cargo:rerun-if-env-changed=WINAPI_NO_BUNDLED_LIBRARIES");
|
||||
println!("cargo:rerun-if-env-changed=WINAPI_STATIC_NOBUNDLE");
|
||||
|
||||
println!("cargo:rustc-link-lib=static=gcc");
|
||||
println!("cargo:rustc-link-lib=static=stdc++");
|
||||
println!("cargo:rustc-link-lib=static=winpthread");
|
||||
}
|
||||
}
|
||||
10621
resources/assets/icons.json
Normal file
10621
resources/assets/icons.json
Normal file
File diff suppressed because one or more lines are too long
BIN
resources/assets/icons.ttf
Normal file
BIN
resources/assets/icons.ttf
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load diff
BIN
resources/screenshots/v0.5.0.png
Normal file
BIN
resources/screenshots/v0.5.0.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 309 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 208 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
86
src/core/config.rs
Normal file
86
src/core/config.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
use crate::core::sync::{get_android_sdk, User};
|
||||
use crate::core::utils::DisplayablePath;
|
||||
use crate::gui::views::settings::Settings;
|
||||
use crate::CONFIG_DIR;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use static_init::dynamic;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Config {
|
||||
pub general: GeneralSettings,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
|
||||
pub devices: Vec<DeviceSettings>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct GeneralSettings {
|
||||
pub theme: String,
|
||||
pub expert_mode: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct BackupSettings {
|
||||
pub backups: Vec<DisplayablePath>,
|
||||
pub selected: Option<DisplayablePath>,
|
||||
pub users: Vec<User>,
|
||||
pub selected_user: Option<User>,
|
||||
pub backup_state: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct DeviceSettings {
|
||||
pub device_id: String,
|
||||
pub disable_mode: bool,
|
||||
pub multi_user_mode: bool,
|
||||
#[serde(skip)]
|
||||
pub backup: BackupSettings,
|
||||
}
|
||||
|
||||
impl Default for DeviceSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
device_id: String::new(),
|
||||
multi_user_mode: get_android_sdk() > 21,
|
||||
disable_mode: false,
|
||||
backup: BackupSettings::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[dynamic]
|
||||
static CONFIG_FILE: PathBuf = CONFIG_DIR.join("config.toml");
|
||||
|
||||
impl Config {
|
||||
pub fn save_changes(settings: &Settings, device_id: &String) {
|
||||
let mut config = Self::load_configuration_file();
|
||||
if let Some(device) = config
|
||||
.devices
|
||||
.iter_mut()
|
||||
.find(|x| x.device_id == *device_id)
|
||||
{
|
||||
*device = settings.device.clone();
|
||||
} else {
|
||||
debug!("config: New device settings saved");
|
||||
config.devices.push(settings.device.clone());
|
||||
}
|
||||
config.general = settings.general.clone();
|
||||
let toml = toml::to_string(&config).unwrap();
|
||||
fs::write(&*CONFIG_FILE, toml).expect("Could not write config file to disk!");
|
||||
}
|
||||
|
||||
pub fn load_configuration_file() -> Self {
|
||||
match fs::read_to_string(&*CONFIG_FILE) {
|
||||
Ok(s) => match toml::from_str(&s) {
|
||||
Ok(config) => return config,
|
||||
Err(e) => error!("Invalid config file: `{}`", e),
|
||||
},
|
||||
Err(e) => error!("Failed to read config file: `{}`", e),
|
||||
}
|
||||
error!("Restoring default config file");
|
||||
let toml = toml::to_string(&Self::default()).unwrap();
|
||||
fs::write(&*CONFIG_FILE, toml).expect("Could not write config file to disk!");
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
pub mod config;
|
||||
pub mod save;
|
||||
pub mod sync;
|
||||
pub mod theme;
|
||||
pub mod uad_lists;
|
||||
pub mod update;
|
||||
pub mod utils;
|
||||
|
|
|
|||
179
src/core/save.rs
Normal file
179
src/core/save.rs
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
use crate::core::config::DeviceSettings;
|
||||
use crate::core::sync::{apply_pkg_state_commands, CorePackage, Phone, User};
|
||||
use crate::core::utils::DisplayablePath;
|
||||
use crate::gui::widgets::package_row::PackageRow;
|
||||
use crate::CACHE_DIR;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use static_init::dynamic;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[dynamic]
|
||||
pub static BACKUP_DIR: PathBuf = CACHE_DIR.join("backups");
|
||||
|
||||
#[derive(Default, Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
|
||||
struct PhoneBackup {
|
||||
device_id: String,
|
||||
users: Vec<UserBackup>,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
|
||||
struct UserBackup {
|
||||
id: u16,
|
||||
packages: Vec<CorePackage>,
|
||||
}
|
||||
|
||||
// Backup all `Uninstalled` and `Disabled` packages
|
||||
pub async fn backup_phone(
|
||||
users: Vec<User>,
|
||||
device_id: String,
|
||||
phone_packages: Vec<Vec<PackageRow>>,
|
||||
) -> Result<(), String> {
|
||||
let mut backup = PhoneBackup {
|
||||
device_id: device_id.clone(),
|
||||
..PhoneBackup::default()
|
||||
};
|
||||
|
||||
for u in users {
|
||||
let mut user_backup = UserBackup {
|
||||
id: u.id,
|
||||
..UserBackup::default()
|
||||
};
|
||||
|
||||
for p in phone_packages[u.index].clone() {
|
||||
user_backup.packages.push(CorePackage {
|
||||
name: p.name.clone(),
|
||||
state: p.state,
|
||||
});
|
||||
}
|
||||
backup.users.push(user_backup);
|
||||
}
|
||||
|
||||
match serde_json::to_string_pretty(&backup) {
|
||||
Ok(json) => {
|
||||
let backup_path = &*BACKUP_DIR.join(device_id);
|
||||
|
||||
if let Err(e) = fs::create_dir_all(backup_path) {
|
||||
error!("BACKUP: could not create backup dir: {}", e);
|
||||
return Err(e.to_string());
|
||||
};
|
||||
|
||||
let backup_filename =
|
||||
format!("{}.json", chrono::Local::now().format("%Y-%m-%d_%H-%M-%S"));
|
||||
|
||||
match fs::write(backup_path.join(backup_filename), json) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list_available_backups(dir: &Path) -> Vec<DisplayablePath> {
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
match fs::read_dir(dir) {
|
||||
Ok(files) => files
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| DisplayablePath { path: e.path() })
|
||||
.collect::<Vec<_>>(),
|
||||
Err(_) => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list_available_backup_user(backup: DisplayablePath) -> Vec<User> {
|
||||
match fs::read_to_string(backup.path) {
|
||||
Ok(data) => {
|
||||
let phone_backup: PhoneBackup =
|
||||
serde_json::from_str(&data).expect("Unable to parse backup file");
|
||||
|
||||
let mut users = vec![];
|
||||
for u in phone_backup.users {
|
||||
users.push(User {
|
||||
id: u.id,
|
||||
index: 0,
|
||||
protected: false,
|
||||
});
|
||||
}
|
||||
users
|
||||
}
|
||||
Err(e) => {
|
||||
error!("[BACKUP]: Selected backup file not found: {}", e);
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BackupPackage {
|
||||
pub index: usize,
|
||||
pub commands: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn restore_backup(
|
||||
selected_device: &Phone,
|
||||
packages: &[Vec<PackageRow>],
|
||||
settings: &DeviceSettings,
|
||||
) -> Result<Vec<BackupPackage>, String> {
|
||||
match fs::read_to_string(
|
||||
settings
|
||||
.backup
|
||||
.selected
|
||||
.as_ref()
|
||||
.ok_or("field should be Some type")?
|
||||
.path
|
||||
.clone(),
|
||||
) {
|
||||
Ok(data) => {
|
||||
let phone_backup: PhoneBackup =
|
||||
serde_json::from_str(&data).expect("Unable to parse backup file");
|
||||
|
||||
let mut commands = vec![];
|
||||
for u in phone_backup.users {
|
||||
let index = match selected_device.user_list.iter().find(|x| x.id == u.id) {
|
||||
Some(i) => i.index,
|
||||
None => return Err(format!("user {} doesn't exist", u.id)),
|
||||
};
|
||||
|
||||
for (i, backup_package) in u.packages.iter().enumerate() {
|
||||
let package: CorePackage;
|
||||
match packages[index]
|
||||
.iter()
|
||||
.find(|x| x.name == backup_package.name)
|
||||
{
|
||||
Some(p) => package = p.into(),
|
||||
None => {
|
||||
return Err(format!(
|
||||
"{} not found for user {}",
|
||||
backup_package.name, u.id
|
||||
))
|
||||
}
|
||||
}
|
||||
let p_commands = apply_pkg_state_commands(
|
||||
&package,
|
||||
backup_package.state,
|
||||
&settings
|
||||
.backup
|
||||
.selected_user
|
||||
.ok_or("field should be Some type")?,
|
||||
selected_device,
|
||||
);
|
||||
if !p_commands.is_empty() {
|
||||
commands.push(BackupPackage {
|
||||
index: i,
|
||||
commands: p_commands,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if !commands.is_empty() {
|
||||
commands.push(BackupPackage {
|
||||
index: 0,
|
||||
commands: vec![],
|
||||
});
|
||||
}
|
||||
Ok(commands)
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
426
src/core/sync.rs
426
src/core/sync.rs
|
|
@ -1,99 +1,163 @@
|
|||
use crate::core::uad_lists::PackageState;
|
||||
use crate::gui::views::settings::Settings;
|
||||
use crate::gui::views::list::PackageInfo;
|
||||
use crate::gui::widgets::package_row::PackageRow;
|
||||
use regex::Regex;
|
||||
use retry::{delay::Fixed, retry, OperationResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use static_init::dynamic;
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::process::Command;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[dynamic]
|
||||
static RE: Regex = Regex::new(r"\n(\S+)\s+device").unwrap();
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Phone {
|
||||
pub model: String,
|
||||
pub android_sdk: u8,
|
||||
pub user_list: Vec<User>,
|
||||
pub adb_id: String,
|
||||
}
|
||||
|
||||
impl Default for Phone {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
model: get_phone_brand(),
|
||||
android_sdk: get_android_sdk(),
|
||||
user_list: get_user_list(),
|
||||
model: "fetching devices...".to_string(),
|
||||
android_sdk: 0,
|
||||
user_list: vec![],
|
||||
adb_id: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
|
||||
impl std::fmt::Display for Phone {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.model)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Copy)]
|
||||
pub struct User {
|
||||
pub id: u16,
|
||||
pub index: usize,
|
||||
pub protected: bool,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for User {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", format!("user {}", self.id),)
|
||||
write!(f, "user {}", self.id)
|
||||
}
|
||||
}
|
||||
pub fn adb_shell_command(args: &str) -> Result<String, String> {
|
||||
|
||||
pub fn adb_shell_command(shell: bool, args: &str) -> Result<String, String> {
|
||||
let adb_command = if shell {
|
||||
vec!["shell", args]
|
||||
} else {
|
||||
vec![args]
|
||||
};
|
||||
|
||||
let mut command = Command::new("adb");
|
||||
command.args(adb_command);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let output = Command::new("adb")
|
||||
.args(&["shell", args])
|
||||
.creation_flags(0x08000000) // do not open a cmd window
|
||||
.output();
|
||||
let command = command.creation_flags(0x08000000); // do not open a cmd window
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let output = Command::new("adb").args(&["shell", args]).output();
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||
let output = Command::new("adb").args(&["shell", args]).output();
|
||||
|
||||
match output {
|
||||
match command.output() {
|
||||
Err(e) => {
|
||||
error!("ADB: {}", e);
|
||||
Err("ADB was not found".to_string())
|
||||
}
|
||||
Ok(o) => {
|
||||
if !o.status.success() {
|
||||
let stdout = String::from_utf8(o.stdout).unwrap().trim_end().to_string();
|
||||
let stderr = String::from_utf8(o.stderr).unwrap().trim_end().to_string();
|
||||
if o.status.success() {
|
||||
Ok(String::from_utf8(o.stdout)
|
||||
.map_err(|e| e.to_string())?
|
||||
.trim_end()
|
||||
.to_string())
|
||||
} else {
|
||||
let stdout = String::from_utf8(o.stdout)
|
||||
.map_err(|e| e.to_string())?
|
||||
.trim_end()
|
||||
.to_string();
|
||||
let stderr = String::from_utf8(o.stderr)
|
||||
.map_err(|e| e.to_string())?
|
||||
.trim_end()
|
||||
.to_string();
|
||||
|
||||
// ADB does really weird things. Some errors are not redirected to stderr
|
||||
let err = if stdout.is_empty() { stderr } else { stdout };
|
||||
Err(err)
|
||||
} else {
|
||||
Ok(String::from_utf8(o.stdout).unwrap().trim_end().to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list_all_system_packages(user_id: &Option<&User>) -> String {
|
||||
let action = match user_id {
|
||||
Some(user_id) => format!("pm list packages -s -u --user {}", user_id.id),
|
||||
None => "pm list packages -s -u".to_string(),
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CommandType {
|
||||
PackageManager(PackageInfo),
|
||||
Shell,
|
||||
}
|
||||
pub async fn perform_adb_commands(
|
||||
action: String,
|
||||
command_type: CommandType,
|
||||
) -> Result<CommandType, ()> {
|
||||
let label = match command_type {
|
||||
CommandType::PackageManager(ref p) => p.removal.to_string(),
|
||||
CommandType::Shell => "Shell".to_string(),
|
||||
};
|
||||
|
||||
adb_shell_command(&action)
|
||||
.unwrap_or_else(|_| "".to_string())
|
||||
match adb_shell_command(true, &action) {
|
||||
Ok(o) => {
|
||||
// On old devices, adb commands can return the '0' exit code even if there
|
||||
// is an error. On Android 4.4, ADB doesn't check if the package exists.
|
||||
// It does not return any error if you try to `pm block` a non-existent package.
|
||||
// Some commands are even killed by ADB before finishing and UAD can't catch
|
||||
// the output.
|
||||
if ["Error", "Failure"].iter().any(|&e| o.contains(e)) {
|
||||
error!("[{}] {} -> {}", label, action, o);
|
||||
Err(())
|
||||
} else {
|
||||
info!("[{}] {} -> {}", label, action, o);
|
||||
Ok(command_type)
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
if !err.contains("[not installed for") {
|
||||
error!("[{}] {} -> {}", label, action, err);
|
||||
}
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
pub fn user_flag(user_id: Option<&User>) -> String {
|
||||
match user_id {
|
||||
Some(user_id) => format!(" --user {}", user_id.id),
|
||||
None => "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list_all_system_packages(user_id: Option<&User>) -> String {
|
||||
let action = format!("pm list packages -s -u{}", user_flag(user_id));
|
||||
|
||||
adb_shell_command(true, &action)
|
||||
.unwrap_or_else(|_| String::new())
|
||||
.replace("package:", "")
|
||||
}
|
||||
|
||||
pub fn hashset_system_packages(state: PackageState, user_id: &Option<&User>) -> HashSet<String> {
|
||||
let user = match user_id {
|
||||
Some(user_id) => format!(" --user {}", user_id.id),
|
||||
None => "".to_string(),
|
||||
};
|
||||
|
||||
pub fn hashset_system_packages(state: PackageState, user_id: Option<&User>) -> HashSet<String> {
|
||||
let user = user_flag(user_id);
|
||||
let action = match state {
|
||||
PackageState::Enabled => format!("pm list packages -s -e{}", user),
|
||||
PackageState::Disabled => format!("pm list package -s -d{}", user),
|
||||
_ => "".to_string(), // You probably don't need to use this function for anything else
|
||||
PackageState::Enabled => format!("pm list packages -s -e{user}"),
|
||||
PackageState::Disabled => format!("pm list package -s -d{user}"),
|
||||
_ => String::new(), // You probably don't need to use this function for anything else
|
||||
};
|
||||
|
||||
adb_shell_command(&action)
|
||||
adb_shell_command(true, &action)
|
||||
.unwrap_or_default()
|
||||
.replace("package:", "")
|
||||
.lines()
|
||||
|
|
@ -101,162 +165,172 @@ pub fn hashset_system_packages(state: PackageState, user_id: &Option<&User>) ->
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub fn action_handler(
|
||||
selected_user: &User,
|
||||
package: &PackageRow,
|
||||
phone: &Phone,
|
||||
settings: &Settings,
|
||||
) -> Result<bool, bool> {
|
||||
let actions: Vec<String> = match package.state {
|
||||
PackageState::Enabled => match settings.disable_mode {
|
||||
true => {
|
||||
if phone.android_sdk < 21 {
|
||||
vec![
|
||||
format!("am force-stop {}", package.name),
|
||||
format!("pm disable-user {}", package.name),
|
||||
format!("pm clear {}", package.name),
|
||||
]
|
||||
} else if settings.multi_user_mode {
|
||||
phone
|
||||
.user_list
|
||||
.iter()
|
||||
.flat_map(|u| {
|
||||
[
|
||||
format!("am force-stop --user {} {}", u.id, package.name),
|
||||
format!("pm disable-user --user {} {}", u.id, package.name),
|
||||
format!("pm clear --user {} {}", u.id, package.name),
|
||||
]
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
vec![
|
||||
format!("am force-stop --user {} {}", selected_user.id, package.name),
|
||||
format!(
|
||||
"pm disable-user --user {} {}",
|
||||
selected_user.id, package.name
|
||||
),
|
||||
format!("pm clear --user {} {}", selected_user.id, package.name),
|
||||
]
|
||||
}
|
||||
}
|
||||
false => {
|
||||
if phone.android_sdk < 21 {
|
||||
vec![format!("pm uninstall {}", package.name)]
|
||||
} else if settings.multi_user_mode {
|
||||
phone
|
||||
.user_list
|
||||
.iter()
|
||||
.map(|u| format!("pm uninstall --user {} {}", u.id, package.name))
|
||||
.collect()
|
||||
} else {
|
||||
vec![format!(
|
||||
"pm uninstall --user {} {}",
|
||||
selected_user.id, package.name
|
||||
)]
|
||||
}
|
||||
}
|
||||
},
|
||||
PackageState::Uninstalled => {
|
||||
if phone.android_sdk < 21 {
|
||||
Vec::new() // action already prevented by the GUI
|
||||
} else if settings.multi_user_mode {
|
||||
phone
|
||||
.user_list
|
||||
.iter()
|
||||
.map(|u| {
|
||||
format!(
|
||||
"cmd package install-existing --user {} {}",
|
||||
u.id, package.name
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
vec![format!(
|
||||
"cmd package install-existing --user {} {}",
|
||||
selected_user.id, package.name
|
||||
)]
|
||||
}
|
||||
}
|
||||
PackageState::Disabled => {
|
||||
if phone.android_sdk < 21 {
|
||||
vec![format!("pm enable {}", package.name)]
|
||||
} else if settings.multi_user_mode {
|
||||
phone
|
||||
.user_list
|
||||
.iter()
|
||||
.map(|u| format!("pm enable --user {} {}", u.id, package.name))
|
||||
.collect()
|
||||
} else {
|
||||
vec![format!(
|
||||
"pm enable --user {} {}",
|
||||
selected_user.id, package.name
|
||||
)]
|
||||
}
|
||||
}
|
||||
PackageState::All => vec![], // This can't happen (like... never)
|
||||
};
|
||||
// Minimum information for processing adb commands
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
|
||||
pub struct CorePackage {
|
||||
pub name: String,
|
||||
pub state: PackageState,
|
||||
}
|
||||
|
||||
for action in actions {
|
||||
match adb_shell_command(&action) {
|
||||
Ok(_) => {
|
||||
info!("[{}] {}", package.removal, action);
|
||||
}
|
||||
Err(err) => {
|
||||
if err.contains("[not installed for") {
|
||||
} else {
|
||||
error!("[{}] {} -> {}", package.removal, action, err);
|
||||
return Err(false);
|
||||
}
|
||||
}
|
||||
impl From<&mut PackageRow> for CorePackage {
|
||||
fn from(pr: &mut PackageRow) -> Self {
|
||||
Self {
|
||||
name: pr.name.clone(),
|
||||
state: pr.state,
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
impl From<PackageRow> for CorePackage {
|
||||
fn from(pr: PackageRow) -> Self {
|
||||
Self {
|
||||
name: pr.name.clone(),
|
||||
state: pr.state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&PackageRow> for CorePackage {
|
||||
fn from(pr: &PackageRow) -> Self {
|
||||
Self {
|
||||
name: pr.name.clone(),
|
||||
state: pr.state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_pkg_state_commands(
|
||||
package: &CorePackage,
|
||||
wanted_state: PackageState,
|
||||
selected_user: &User,
|
||||
phone: &Phone,
|
||||
) -> Vec<String> {
|
||||
// https://github.com/0x192/universal-android-debloater/wiki/ADB-reference
|
||||
// ALWAYS PUT THE COMMAND THAT CHANGES THE PACKAGE STATE FIRST!
|
||||
let commands = match wanted_state {
|
||||
PackageState::Enabled => {
|
||||
match package.state {
|
||||
PackageState::Disabled => match phone.android_sdk {
|
||||
i if i >= 23 => vec!["pm enable"],
|
||||
_ => vec!["pm enable"],
|
||||
},
|
||||
PackageState::Uninstalled => match phone.android_sdk {
|
||||
i if i >= 23 => vec!["cmd package install-existing"],
|
||||
21 | 22 => vec!["pm unhide"],
|
||||
19 | 20 => vec!["pm unblock", "pm clear"],
|
||||
_ => vec![], // Impossible action already prevented by the GUI
|
||||
},
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
PackageState::Disabled => match package.state {
|
||||
PackageState::Uninstalled | PackageState::Enabled => match phone.android_sdk {
|
||||
sdk if sdk >= 23 => vec!["pm disable-user", "am force-stop", "pm clear"],
|
||||
_ => vec![],
|
||||
},
|
||||
_ => vec![],
|
||||
},
|
||||
PackageState::Uninstalled => match package.state {
|
||||
PackageState::Enabled | PackageState::Disabled => match phone.android_sdk {
|
||||
sdk if sdk >= 23 => vec!["pm uninstall"], // > Android Marshmallow (6.0)
|
||||
21 | 22 => vec!["pm hide", "pm clear"], // Android Lollipop (5.x)
|
||||
19 | 20 => vec!["pm block", "pm clear"], // Android KitKat (4.4/4.4W)
|
||||
_ => vec!["pm uninstall"], // Disable mode is unavailable on older devices because the specific ADB commands need root
|
||||
},
|
||||
_ => vec![],
|
||||
},
|
||||
PackageState::All => vec![],
|
||||
};
|
||||
if phone.android_sdk < 21 {
|
||||
request_builder(&commands, &package.name, None)
|
||||
} else {
|
||||
request_builder(&commands, &package.name, Some(selected_user))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_builder(commands: &[&str], package: &str, user: Option<&User>) -> Vec<String> {
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
match user {
|
||||
Some(u) => commands
|
||||
.iter()
|
||||
.map(|c| format!("{} --user {} {}", c, u.id, package))
|
||||
.collect(),
|
||||
None => commands.iter().map(|c| format!("{c} {package}")).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_phone_model() -> String {
|
||||
match adb_shell_command("getprop ro.product.model") {
|
||||
Ok(model) => model,
|
||||
Err(err) => {
|
||||
println!("ERROR: {}", err);
|
||||
if err.contains("adb: no devices/emulators found") {
|
||||
"no devices/emulators found".to_string()
|
||||
} else {
|
||||
err
|
||||
}
|
||||
adb_shell_command(true, "getprop ro.product.model").unwrap_or_else(|err| {
|
||||
println!("ERROR: {err}");
|
||||
if err.contains("adb: no devices/emulators found") {
|
||||
"no devices/emulators found".to_string()
|
||||
} else {
|
||||
err
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_android_sdk() -> u8 {
|
||||
match adb_shell_command("getprop ro.build.version.sdk") {
|
||||
Ok(sdk) => sdk.parse().unwrap(),
|
||||
Err(_) => 0,
|
||||
}
|
||||
adb_shell_command(true, "getprop ro.build.version.sdk").map_or(0, |sdk| sdk.parse().unwrap())
|
||||
}
|
||||
|
||||
pub fn get_phone_brand() -> String {
|
||||
format!(
|
||||
"{} {}",
|
||||
adb_shell_command("getprop ro.product.brand")
|
||||
.unwrap_or_else(|_| "".to_string())
|
||||
.trim(),
|
||||
adb_shell_command(true, "getprop ro.product.brand")
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_default(),
|
||||
get_phone_model()
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_protected_user(user_id: &str) -> bool {
|
||||
adb_shell_command(true, &format!("pm list packages --user {user_id}")).is_err()
|
||||
}
|
||||
|
||||
pub fn get_user_list() -> Vec<User> {
|
||||
#[dynamic]
|
||||
static RE: Regex = Regex::new(r"\{([0-9]+)").unwrap();
|
||||
|
||||
match adb_shell_command("pm list users") {
|
||||
Ok(users) => RE
|
||||
.find_iter(&users)
|
||||
.enumerate()
|
||||
.map(|(i, u)| User {
|
||||
id: u.as_str()[1..].parse().unwrap(),
|
||||
index: i,
|
||||
})
|
||||
.collect(),
|
||||
Err(_) => vec![],
|
||||
}
|
||||
adb_shell_command(true, "pm list users")
|
||||
.map(|users| {
|
||||
RE.find_iter(&users)
|
||||
.enumerate()
|
||||
.map(|(i, u)| User {
|
||||
id: u.as_str()[1..].parse().unwrap(),
|
||||
index: i,
|
||||
protected: is_protected_user(&u.as_str()[1..]),
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
// getprop ro.serialno
|
||||
pub async fn get_devices_list() -> Vec<Phone> {
|
||||
retry(
|
||||
Fixed::from_millis(500).take(120),
|
||||
|| match adb_shell_command(false, "devices") {
|
||||
Ok(devices) => {
|
||||
let mut device_list: Vec<Phone> = vec![];
|
||||
if !RE.is_match(&devices) {
|
||||
return OperationResult::Retry(vec![]);
|
||||
}
|
||||
for device in RE.captures_iter(&devices) {
|
||||
env::set_var("ANDROID_SERIAL", &device[1]);
|
||||
device_list.push(Phone {
|
||||
model: get_phone_brand(),
|
||||
android_sdk: get_android_sdk(),
|
||||
user_list: get_user_list(),
|
||||
adb_id: device[1].to_string(),
|
||||
});
|
||||
}
|
||||
OperationResult::Ok(device_list)
|
||||
}
|
||||
Err(err) => {
|
||||
error!("get_device_list() -> {}", err);
|
||||
let test: Vec<Phone> = vec![];
|
||||
OperationResult::Retry(test)
|
||||
}
|
||||
},
|
||||
)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
use iced::Color;
|
||||
use std::cmp::Ordering;
|
||||
use iced::{color, Color};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Theme {
|
||||
pub name: String,
|
||||
pub palette: ColorPalette,
|
||||
#[derive(Default, Debug, PartialEq, Eq, Copy, Clone)]
|
||||
pub enum Theme {
|
||||
#[default]
|
||||
Lupin,
|
||||
Dark,
|
||||
Light,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
|
|
@ -37,101 +38,65 @@ pub struct ColorPalette {
|
|||
}
|
||||
|
||||
impl Theme {
|
||||
pub fn all() -> Vec<Theme> {
|
||||
vec![Theme::dark(), Theme::light(), Theme::lupin()]
|
||||
}
|
||||
|
||||
pub fn dark() -> Theme {
|
||||
Theme {
|
||||
name: "Dark".to_string(),
|
||||
palette: ColorPalette {
|
||||
pub const ALL: [Self; 3] = [Self::Lupin, Self::Dark, Self::Light];
|
||||
pub fn palette(self) -> ColorPalette {
|
||||
match self {
|
||||
Self::Dark => ColorPalette {
|
||||
base: BaseColors {
|
||||
background: hex_to_color("#111111").unwrap(),
|
||||
foreground: hex_to_color("#1C1C1C").unwrap(),
|
||||
background: color!(0x111111),
|
||||
foreground: color!(0x1C1C1C),
|
||||
},
|
||||
normal: NormalColors {
|
||||
primary: hex_to_color("#3f2b56").unwrap(),
|
||||
secondary: hex_to_color("#386e50").unwrap(),
|
||||
surface: hex_to_color("#828282").unwrap(),
|
||||
error: hex_to_color("#992B2B").unwrap(),
|
||||
primary: color!(0x5E4266),
|
||||
secondary: color!(0x386e50),
|
||||
surface: color!(0x828282),
|
||||
error: color!(0x992B2B),
|
||||
},
|
||||
bright: BrightColors {
|
||||
primary: hex_to_color("#BA84FC").unwrap(),
|
||||
secondary: hex_to_color("#49eb7a").unwrap(),
|
||||
surface: hex_to_color("#E0E0E0").unwrap(),
|
||||
error: hex_to_color("#C13047").unwrap(),
|
||||
primary: color!(0xBA84FC),
|
||||
secondary: color!(0x49eb7a),
|
||||
surface: color!(0xE0E0E0),
|
||||
error: color!(0xC13047),
|
||||
},
|
||||
},
|
||||
Self::Light => ColorPalette {
|
||||
base: BaseColors {
|
||||
background: color!(0xEEEEEE),
|
||||
foreground: color!(0xE0E0E0),
|
||||
},
|
||||
normal: NormalColors {
|
||||
primary: color!(0x230F08),
|
||||
secondary: color!(0xF9D659),
|
||||
surface: color!(0x818181),
|
||||
error: color!(0x992B2B),
|
||||
},
|
||||
bright: BrightColors {
|
||||
primary: color!(0x673AB7),
|
||||
secondary: color!(0x3797A4),
|
||||
surface: color!(0x000000),
|
||||
error: color!(0xC13047),
|
||||
},
|
||||
},
|
||||
Self::Lupin => ColorPalette {
|
||||
base: BaseColors {
|
||||
background: color!(0x282a36),
|
||||
foreground: color!(0x353746),
|
||||
},
|
||||
normal: NormalColors {
|
||||
primary: color!(0x58406F),
|
||||
secondary: color!(0x386e50),
|
||||
surface: color!(0xa2a4a3),
|
||||
error: color!(0xA13034),
|
||||
},
|
||||
bright: BrightColors {
|
||||
primary: color!(0xbd94f9),
|
||||
secondary: color!(0x49eb7a),
|
||||
surface: color!(0xf4f8f3),
|
||||
error: color!(0xE63E6D),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn light() -> Theme {
|
||||
Theme {
|
||||
name: "Light".to_string(),
|
||||
palette: ColorPalette {
|
||||
base: BaseColors {
|
||||
background: hex_to_color("#EEEEEE").unwrap(),
|
||||
foreground: hex_to_color("#E0E0E0").unwrap(),
|
||||
},
|
||||
normal: NormalColors {
|
||||
primary: hex_to_color("#673AB7").unwrap(),
|
||||
secondary: hex_to_color("#F9D659").unwrap(),
|
||||
surface: hex_to_color("#818181").unwrap(),
|
||||
error: hex_to_color("#992B2B").unwrap(),
|
||||
},
|
||||
bright: BrightColors {
|
||||
primary: hex_to_color("#673AB7").unwrap(),
|
||||
secondary: hex_to_color("#3797A4").unwrap(),
|
||||
surface: hex_to_color("#000000").unwrap(),
|
||||
error: hex_to_color("#C13047").unwrap(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lupin() -> Theme {
|
||||
Theme {
|
||||
name: "Lupin".to_string(),
|
||||
palette: ColorPalette {
|
||||
base: BaseColors {
|
||||
background: hex_to_color("#282a36").unwrap(),
|
||||
foreground: hex_to_color("#353746").unwrap(),
|
||||
},
|
||||
normal: NormalColors {
|
||||
primary: hex_to_color("#483e61").unwrap(),
|
||||
secondary: hex_to_color("#386e50").unwrap(),
|
||||
surface: hex_to_color("#a2a4a3").unwrap(),
|
||||
error: hex_to_color("#A13034").unwrap(),
|
||||
},
|
||||
bright: BrightColors {
|
||||
primary: hex_to_color("#bd94f9").unwrap(),
|
||||
secondary: hex_to_color("#49eb7a").unwrap(),
|
||||
surface: hex_to_color("#f4f8f3").unwrap(),
|
||||
error: hex_to_color("#E63E6D").unwrap(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Theme {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.name == other.name
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Theme {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.name.cmp(&other.name))
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Theme {}
|
||||
|
||||
impl Ord for Theme {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.name.cmp(&other.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Theme {
|
||||
|
|
@ -139,33 +104,11 @@ impl std::fmt::Display for Theme {
|
|||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self.name.as_str() {
|
||||
"Dark" => "Dark",
|
||||
"Light" => "Light",
|
||||
"Lupin" => "Lupin",
|
||||
_ => "Unknown theme",
|
||||
match self {
|
||||
Self::Dark => "Dark",
|
||||
Self::Light => "Light",
|
||||
Self::Lupin => "Lupin",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn hex_to_color(hex: &str) -> Option<Color> {
|
||||
if hex.len() == 7 {
|
||||
let hash = &hex[0..1];
|
||||
let r = u8::from_str_radix(&hex[1..3], 16);
|
||||
let g = u8::from_str_radix(&hex[3..5], 16);
|
||||
let b = u8::from_str_radix(&hex[5..7], 16);
|
||||
|
||||
return match (hash, r, g, b) {
|
||||
("#", Ok(r), Ok(g), Ok(b)) => Some(Color {
|
||||
r: r as f32 / 255.0,
|
||||
g: g as f32 / 255.0,
|
||||
b: b as f32 / 255.0,
|
||||
a: 1.0,
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,27 @@
|
|||
use serde::Deserialize;
|
||||
use crate::core::utils::{format_diff_time_from_now, last_modified_date};
|
||||
use crate::CACHE_DIR;
|
||||
use retry::{delay::Fixed, retry, OperationResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
//use std::fs;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, PartialEq, Hash, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Package {
|
||||
id: String,
|
||||
pub list: UadList,
|
||||
pub description: Option<String>,
|
||||
dependencies: Option<String>,
|
||||
needed_by: Option<String>,
|
||||
labels: Option<Vec<String>>,
|
||||
pub description: String,
|
||||
dependencies: Vec<String>,
|
||||
needed_by: Vec<String>,
|
||||
labels: Vec<String>,
|
||||
pub removal: Removal,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Default, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum UadList {
|
||||
#[default]
|
||||
All,
|
||||
Aosp,
|
||||
Carrier,
|
||||
|
|
@ -27,22 +32,36 @@ pub enum UadList {
|
|||
Unlisted,
|
||||
}
|
||||
|
||||
impl Default for UadList {
|
||||
fn default() -> UadList {
|
||||
UadList::All
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum UadListState {
|
||||
#[default]
|
||||
Downloading,
|
||||
Done,
|
||||
Failed,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for UadListState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let date = last_modified_date(CACHE_DIR.join("uad_lists.json"));
|
||||
let s = match self {
|
||||
Self::Downloading => "Checking updates...".to_string(),
|
||||
Self::Done => format!("Done (last was {})", format_diff_time_from_now(date)),
|
||||
Self::Failed => "Failed to check update!".to_string(),
|
||||
};
|
||||
write!(f, "{s}")
|
||||
}
|
||||
}
|
||||
|
||||
impl UadList {
|
||||
pub const ALL: [UadList; 8] = [
|
||||
UadList::All,
|
||||
UadList::Aosp,
|
||||
UadList::Carrier,
|
||||
UadList::Google,
|
||||
UadList::Misc,
|
||||
UadList::Oem,
|
||||
UadList::Pending,
|
||||
UadList::Unlisted,
|
||||
pub const ALL: [Self; 8] = [
|
||||
Self::All,
|
||||
Self::Aosp,
|
||||
Self::Carrier,
|
||||
Self::Google,
|
||||
Self::Misc,
|
||||
Self::Oem,
|
||||
Self::Pending,
|
||||
Self::Unlisted,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -52,40 +71,30 @@ impl std::fmt::Display for UadList {
|
|||
f,
|
||||
"{}",
|
||||
match self {
|
||||
UadList::All => "All lists",
|
||||
UadList::Aosp => "aosp",
|
||||
UadList::Carrier => "carrier",
|
||||
UadList::Google => "google",
|
||||
UadList::Misc => "misc",
|
||||
UadList::Oem => "oem",
|
||||
UadList::Pending => "pending",
|
||||
UadList::Unlisted => "unlisted",
|
||||
Self::All => "All lists",
|
||||
Self::Aosp => "aosp",
|
||||
Self::Carrier => "carrier",
|
||||
Self::Google => "google",
|
||||
Self::Misc => "misc",
|
||||
Self::Oem => "oem",
|
||||
Self::Pending => "pending",
|
||||
Self::Unlisted => "unlisted",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PackageState {
|
||||
All,
|
||||
#[default]
|
||||
Enabled,
|
||||
Uninstalled,
|
||||
Disabled,
|
||||
}
|
||||
|
||||
impl Default for PackageState {
|
||||
fn default() -> PackageState {
|
||||
PackageState::Enabled
|
||||
}
|
||||
}
|
||||
|
||||
impl PackageState {
|
||||
pub const ALL: [PackageState; 4] = [
|
||||
PackageState::All,
|
||||
PackageState::Enabled,
|
||||
PackageState::Uninstalled,
|
||||
PackageState::Disabled,
|
||||
];
|
||||
pub const ALL: [Self; 4] = [Self::All, Self::Enabled, Self::Uninstalled, Self::Disabled];
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PackageState {
|
||||
|
|
@ -94,10 +103,10 @@ impl std::fmt::Display for PackageState {
|
|||
f,
|
||||
"{}",
|
||||
match self {
|
||||
PackageState::All => "All packages",
|
||||
PackageState::Enabled => "Enabled",
|
||||
PackageState::Uninstalled => "Uninstalled",
|
||||
PackageState::Disabled => "Disabled",
|
||||
Self::All => "All packages",
|
||||
Self::Enabled => "Enabled",
|
||||
Self::Uninstalled => "Uninstalled",
|
||||
Self::Disabled => "Disabled",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -108,26 +117,26 @@ pub trait Opposite {
|
|||
}
|
||||
|
||||
impl Opposite for PackageState {
|
||||
fn opposite(&self, disable: bool) -> PackageState {
|
||||
fn opposite(&self, disable: bool) -> Self {
|
||||
match self {
|
||||
PackageState::Enabled => {
|
||||
Self::Enabled => {
|
||||
if disable {
|
||||
PackageState::Disabled
|
||||
Self::Disabled
|
||||
} else {
|
||||
PackageState::Uninstalled
|
||||
Self::Uninstalled
|
||||
}
|
||||
}
|
||||
PackageState::Uninstalled => PackageState::Enabled,
|
||||
PackageState::Disabled => PackageState::Enabled,
|
||||
PackageState::All => PackageState::All,
|
||||
Self::Uninstalled | Self::Disabled => Self::Enabled,
|
||||
Self::All => Self::All,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bad names. To be changed!
|
||||
#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Default, Debug, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Removal {
|
||||
All,
|
||||
#[default]
|
||||
Recommended,
|
||||
Advanced,
|
||||
Expert,
|
||||
|
|
@ -135,20 +144,14 @@ pub enum Removal {
|
|||
Unlisted,
|
||||
}
|
||||
|
||||
impl Default for Removal {
|
||||
fn default() -> Removal {
|
||||
Removal::Recommended
|
||||
}
|
||||
}
|
||||
|
||||
impl Removal {
|
||||
pub const ALL: [Removal; 6] = [
|
||||
Removal::All,
|
||||
Removal::Recommended,
|
||||
Removal::Advanced,
|
||||
Removal::Expert,
|
||||
Removal::Unsafe,
|
||||
Removal::Unlisted,
|
||||
pub const ALL: [Self; 6] = [
|
||||
Self::All,
|
||||
Self::Recommended,
|
||||
Self::Advanced,
|
||||
Self::Expert,
|
||||
Self::Unsafe,
|
||||
Self::Unlisted,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -158,29 +161,79 @@ impl std::fmt::Display for Removal {
|
|||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Removal::All => "All",
|
||||
Removal::Recommended => "Recommended",
|
||||
Removal::Advanced => "Advanced",
|
||||
Removal::Expert => "Expert",
|
||||
Removal::Unsafe => "Unsafe",
|
||||
Removal::Unlisted => "Unlisted",
|
||||
Self::All => "All",
|
||||
Self::Recommended => "Recommended",
|
||||
Self::Advanced => "Advanced",
|
||||
Self::Expert => "Expert",
|
||||
Self::Unsafe => "Unsafe",
|
||||
Self::Unlisted => "Unlisted",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_debloat_lists() -> HashMap<String, Package> {
|
||||
const DATA: &str = include_str!("../../ressources/assets/uad_lists.json");
|
||||
let mut package_lists = HashMap::new();
|
||||
//let data = fs::read_to_string("ressources/assets/uad_lists.json").expect("Unable to read file");
|
||||
type PackageHashMap = HashMap<String, Package>;
|
||||
pub fn load_debloat_lists(remote: bool) -> (Result<PackageHashMap, PackageHashMap>, bool) {
|
||||
let cached_uad_lists: PathBuf = CACHE_DIR.join("uad_lists.json");
|
||||
let mut error = false;
|
||||
let list: Vec<Package> = if remote {
|
||||
retry(Fixed::from_millis(1000).take(60), || {
|
||||
match ureq::get(
|
||||
"https://raw.githubusercontent.com/0x192/universal-android-debloater/\
|
||||
main/resources/assets/uad_lists.json",
|
||||
)
|
||||
.call()
|
||||
{
|
||||
Ok(data) => {
|
||||
let text = data.into_string().expect("response should be Ok type");
|
||||
fs::write(cached_uad_lists.clone(), &text).expect("Unable to write file");
|
||||
let list = serde_json::from_str(&text).expect("Unable to parse");
|
||||
OperationResult::Ok(list)
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Could not load remote debloat list: {}", e);
|
||||
error = true;
|
||||
OperationResult::Retry(Vec::<Package>::new())
|
||||
}
|
||||
}
|
||||
})
|
||||
.map_or_else(|_| get_local_lists(), |list| list)
|
||||
} else {
|
||||
warn!("Could not load remote debloat list");
|
||||
get_local_lists()
|
||||
};
|
||||
|
||||
// TODO: Do it without intermediary Vec?
|
||||
let list: Vec<Package> = serde_json::from_str(DATA).expect("Unable to parse");
|
||||
|
||||
let mut package_lists = HashMap::new();
|
||||
for p in list {
|
||||
let name = p.id.clone();
|
||||
package_lists.insert(name, p);
|
||||
}
|
||||
|
||||
package_lists
|
||||
if error {
|
||||
(Err(package_lists), remote)
|
||||
} else {
|
||||
(Ok(package_lists), remote)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_local_lists() -> Vec<Package> {
|
||||
const DATA: &str = include_str!("../../resources/assets/uad_lists.json");
|
||||
let cached_uad_lists = CACHE_DIR.join("uad_lists.json");
|
||||
|
||||
if Path::new(&cached_uad_lists).exists() {
|
||||
let data = fs::read_to_string(cached_uad_lists).unwrap();
|
||||
serde_json::from_str(&data).expect("Unable to parse")
|
||||
} else {
|
||||
serde_json::from_str(DATA).expect("Unable to parse")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_parse_json() {
|
||||
const DATA: &str = include_str!("../../resources/assets/uad_lists.json");
|
||||
let _: Vec<Package> = serde_json::from_str(DATA).expect("Unable to parse");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
299
src/core/update.rs
Normal file
299
src/core/update.rs
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
use {
|
||||
retry::{delay::Fibonacci, retry, OperationResult},
|
||||
std::fs,
|
||||
std::io,
|
||||
std::io::copy,
|
||||
std::path::Path,
|
||||
std::path::PathBuf,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct Release {
|
||||
pub tag_name: String,
|
||||
pub assets: Vec<ReleaseAsset>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ReleaseAsset {
|
||||
pub name: String,
|
||||
#[serde(rename = "browser_download_url")]
|
||||
pub download_url: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct SelfUpdateState {
|
||||
pub latest_release: Option<Release>,
|
||||
pub status: SelfUpdateStatus,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, PartialEq, Eq, Clone)]
|
||||
pub enum SelfUpdateStatus {
|
||||
Updating,
|
||||
#[default]
|
||||
Checking,
|
||||
Done,
|
||||
Failed,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SelfUpdateStatus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let s = match self {
|
||||
Self::Checking => "Checking updates...",
|
||||
Self::Updating => "Updating...",
|
||||
Self::Failed => "Failed to check update!",
|
||||
Self::Done => "Done",
|
||||
};
|
||||
write!(f, "{s}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Download a file from the internet
|
||||
#[cfg(feature = "self-update")]
|
||||
pub async fn download_file<T: ToString + Send>(url: T, dest_file: PathBuf) -> Result<(), String> {
|
||||
let url = url.to_string();
|
||||
debug!("downloading file from {}", &url);
|
||||
|
||||
match ureq::get(&url).call() {
|
||||
Ok(res) => {
|
||||
let mut file = fs::File::create(dest_file).map_err(|e| e.to_string())?;
|
||||
|
||||
if let Err(e) = copy(&mut res.into_reader(), &mut file) {
|
||||
return Err(e.to_string());
|
||||
}
|
||||
}
|
||||
Err(e) => return Err(e.to_string()),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Downloads the latest release file that matches `bin_name`, renames the current
|
||||
/// executable to a temp path, renames the new version as the original file name,
|
||||
/// then returns both the original file name (new version) and temp path (old version)
|
||||
#[cfg(feature = "self-update")]
|
||||
pub async fn download_update_to_temp_file(
|
||||
bin_name: String,
|
||||
release: Release,
|
||||
) -> Result<(PathBuf, PathBuf), ()> {
|
||||
let current_bin_path = std::env::current_exe().map_err(|_| ())?;
|
||||
|
||||
// Path to download the new version to
|
||||
let download_path = current_bin_path
|
||||
.parent()
|
||||
.ok_or(())?
|
||||
.join(format!("tmp_{bin_name}"));
|
||||
|
||||
// Path to temporarily force rename current process to, se we can then
|
||||
// rename `download_path` to `current_bin_path` and then launch new version
|
||||
// cleanly as `current_bin_path`
|
||||
let tmp_path = current_bin_path
|
||||
.parent()
|
||||
.ok_or(())?
|
||||
.join(format!("tmp2_{bin_name}"));
|
||||
|
||||
// MacOS and Linux release are gziped tarball
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let asset_name = format!("{bin_name}.tar.gz");
|
||||
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|a| a.name == asset_name)
|
||||
.cloned()
|
||||
.ok_or(())?;
|
||||
|
||||
let archive_path = current_bin_path.parent().ok_or(())?.join(&asset_name);
|
||||
|
||||
if let Err(e) = download_file(asset.download_url, archive_path.clone()).await {
|
||||
error!("Couldn't download UAD update: {}", e);
|
||||
return Err(());
|
||||
}
|
||||
|
||||
if extract_binary_from_tar(&archive_path, &download_path).is_err() {
|
||||
error!("Couldn't extract UAD release tarball");
|
||||
return Err(());
|
||||
}
|
||||
|
||||
std::fs::remove_file(&archive_path).map_err(|_| ())?;
|
||||
}
|
||||
|
||||
// For Windows we download the new binary directly
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|a| a.name == bin_name)
|
||||
.cloned()
|
||||
.ok_or(())?;
|
||||
|
||||
if let Err(e) = download_file(asset.download_url, download_path.clone()).await {
|
||||
error!("Couldn't download UAD update: {}", e);
|
||||
return Err(());
|
||||
}
|
||||
}
|
||||
|
||||
// Make the file executable
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let mut permissions = fs::metadata(&download_path).map_err(|_| ())?.permissions();
|
||||
permissions.set_mode(0o755);
|
||||
if let Err(e) = fs::set_permissions(&download_path, permissions) {
|
||||
error!("[SelfUpdate] Couldn't set permission to temp file: {}", e);
|
||||
return Err(());
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = rename(¤t_bin_path, &tmp_path) {
|
||||
error!("[SelfUpdate] Couldn't rename binary path: {}", e);
|
||||
return Err(());
|
||||
}
|
||||
if let Err(e) = rename(&download_path, ¤t_bin_path) {
|
||||
error!("[SelfUpdate] Couldn't rename binary path: {}", e);
|
||||
return Err(());
|
||||
}
|
||||
|
||||
Ok((current_bin_path, tmp_path))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "self-update"))]
|
||||
pub fn get_latest_release() -> Result<Option<Release>, ()> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
// UAD only has pre-releases so we can't use
|
||||
// https://api.github.com/repos/0x192/universal-android-debloater/releases/latest
|
||||
// to only get the latest release
|
||||
#[cfg(feature = "self-update")]
|
||||
pub fn get_latest_release() -> Result<Option<Release>, ()> {
|
||||
debug!("Checking for UAD update");
|
||||
|
||||
match ureq::get("https://api.github.com/repos/0x192/universal-android-debloater/releases")
|
||||
.call()
|
||||
{
|
||||
Ok(res) => {
|
||||
let release: Release = serde_json::from_value(
|
||||
res.into_json::<serde_json::Value>()
|
||||
.map_err(|_| ())?
|
||||
.get(0)
|
||||
.ok_or(())?
|
||||
.clone(),
|
||||
)
|
||||
.map_err(|_| ())?;
|
||||
if release.tag_name.as_str() != "dev-build"
|
||||
&& release.tag_name.as_str() > env!("CARGO_PKG_VERSION")
|
||||
{
|
||||
Ok(Some(release))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
debug!("Failed to check UAD update");
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts the binary from a `tar.gz` archive to `temp_file` path
|
||||
#[cfg(feature = "self-update")]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn extract_binary_from_tar(archive_path: &Path, temp_file: &Path) -> io::Result<()> {
|
||||
use flate2::read::GzDecoder;
|
||||
use std::fs::File;
|
||||
use tar::Archive;
|
||||
let mut archive = Archive::new(GzDecoder::new(File::open(archive_path)?));
|
||||
|
||||
let mut temp_file = File::create(temp_file)?;
|
||||
|
||||
for file in archive.entries()? {
|
||||
let mut file = file?;
|
||||
|
||||
let path = file.path()?;
|
||||
if path.to_str().is_some() {
|
||||
io::copy(&mut file, &mut temp_file)?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(io::ErrorKind::NotFound.into())
|
||||
}
|
||||
|
||||
/// Hardcoded binary names for each compilation target
|
||||
/// that gets published to the Github Release
|
||||
#[cfg(feature = "self-update")]
|
||||
pub const fn bin_name() -> &'static str {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
"uad_gui.exe"
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
"uad_gui-macos"
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||
{
|
||||
"uad_gui-linux"
|
||||
}
|
||||
}
|
||||
|
||||
/// Rename a file or directory to a new name, retrying if the operation fails because of permissions
|
||||
///
|
||||
/// Will retry for ~30 seconds with longer and longer delays between each, to allow for virus scan
|
||||
/// and other automated operations to complete.
|
||||
#[cfg(feature = "self-update")]
|
||||
pub fn rename<F, T>(from: F, to: T) -> Result<(), String>
|
||||
where
|
||||
F: AsRef<Path>,
|
||||
T: AsRef<Path>,
|
||||
{
|
||||
// 21 Fibonacci steps starting at 1 ms is ~28 seconds total
|
||||
// See https://github.com/rust-lang/rustup/pull/1873 where this was used by Rustup to work around
|
||||
// virus scanning file locks
|
||||
let from = from.as_ref();
|
||||
let to = to.as_ref();
|
||||
|
||||
retry(Fibonacci::from_millis(1).take(21), || {
|
||||
match fs::rename(from, to) {
|
||||
Ok(_) => OperationResult::Ok(()),
|
||||
Err(e) => match e.kind() {
|
||||
io::ErrorKind::PermissionDenied => OperationResult::Retry(e),
|
||||
_ => OperationResult::Err(e),
|
||||
},
|
||||
}
|
||||
})
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Remove a file, retrying if the operation fails because of permissions
|
||||
///
|
||||
/// Will retry for ~30 seconds with longer and longer delays between each, to allow for virus scan
|
||||
/// and other automated operations to complete.
|
||||
#[cfg(feature = "self-update")]
|
||||
pub fn remove_file<P>(path: P) -> Result<(), String>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
// 21 Fibonacci steps starting at 1 ms is ~28 seconds total
|
||||
// See https://github.com/rust-lang/rustup/pull/1873 where this was used by Rustup to work around
|
||||
// virus scanning file locks
|
||||
let path = path.as_ref();
|
||||
|
||||
retry(
|
||||
Fibonacci::from_millis(1).take(21),
|
||||
|| match fs::remove_file(path) {
|
||||
Ok(_) => OperationResult::Ok(()),
|
||||
Err(e) => match e.kind() {
|
||||
io::ErrorKind::PermissionDenied => OperationResult::Retry(e),
|
||||
_ => OperationResult::Err(e),
|
||||
},
|
||||
},
|
||||
)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
use crate::core::sync::{hashset_system_packages, list_all_system_packages, User};
|
||||
use crate::core::theme::Theme;
|
||||
use crate::core::uad_lists::{Package, PackageState, Removal, UadList};
|
||||
use crate::gui::views::list::Selection;
|
||||
use crate::gui::widgets::package_row::PackageRow;
|
||||
use crate::gui::ICONS;
|
||||
|
||||
use iced::{alignment, Length, Text};
|
||||
use chrono::offset::Utc;
|
||||
use chrono::DateTime;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::io::{self, prelude::*, BufReader};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::{fmt, fs};
|
||||
|
||||
pub fn fetch_packages(
|
||||
uad_lists: &'static HashMap<String, Package>,
|
||||
user_id: &Option<&User>,
|
||||
uad_lists: &HashMap<String, Package>,
|
||||
user_id: Option<&User>,
|
||||
) -> Vec<PackageRow> {
|
||||
let all_system_packages = list_all_system_packages(user_id); // installed and uninstalled packages
|
||||
let enabled_system_packages = hashset_system_packages(PackageState::Enabled, user_id);
|
||||
|
|
@ -24,14 +24,14 @@ pub fn fetch_packages(
|
|||
|
||||
for p_name in all_system_packages.lines() {
|
||||
state = PackageState::Uninstalled;
|
||||
description = "[No description]";
|
||||
description = "[No description] : CONTRIBUTION WELCOMED";
|
||||
uad_list = UadList::Unlisted;
|
||||
removal = Removal::Unlisted;
|
||||
|
||||
if uad_lists.contains_key(p_name) {
|
||||
description = match &uad_lists.get(p_name).unwrap().description {
|
||||
Some(descr) => descr,
|
||||
None => "[No description]",
|
||||
description = &uad_lists.get(p_name).unwrap().description;
|
||||
if description.is_empty() {
|
||||
description = "[No description] : CONTRIBUTION WELCOMED";
|
||||
};
|
||||
uad_list = uad_lists.get(p_name).unwrap().list;
|
||||
removal = uad_lists.get(p_name).unwrap().removal;
|
||||
|
|
@ -43,85 +43,94 @@ pub fn fetch_packages(
|
|||
state = PackageState::Disabled;
|
||||
}
|
||||
|
||||
let package_row = PackageRow::new(p_name, state, description, uad_list, removal, false);
|
||||
let package_row =
|
||||
PackageRow::new(p_name, state, description, uad_list, removal, false, false);
|
||||
user_package.push(package_row);
|
||||
}
|
||||
user_package.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
||||
user_package
|
||||
}
|
||||
|
||||
pub fn update_selection_count(selection: &mut Selection, p_state: PackageState, add: bool) {
|
||||
match p_state {
|
||||
PackageState::Enabled => {
|
||||
if add {
|
||||
selection.enabled += 1
|
||||
} else {
|
||||
selection.enabled -= 1
|
||||
};
|
||||
}
|
||||
PackageState::Disabled => {
|
||||
if add {
|
||||
selection.disabled += 1
|
||||
} else {
|
||||
selection.disabled -= 1
|
||||
};
|
||||
}
|
||||
PackageState::Uninstalled => {
|
||||
if add {
|
||||
selection.uninstalled += 1
|
||||
} else {
|
||||
selection.uninstalled -= 1
|
||||
};
|
||||
}
|
||||
PackageState::All => {}
|
||||
};
|
||||
}
|
||||
|
||||
pub async fn export_selection(packages: Vec<PackageRow>) -> Result<bool, String> {
|
||||
let selected = packages
|
||||
.iter()
|
||||
.filter(|p| p.selected)
|
||||
.map(|p| p.name.clone())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
|
||||
match fs::write("uad_exported_selection.txt", selected) {
|
||||
Ok(_) => Ok(true),
|
||||
Err(err) => Err(err.to_string()),
|
||||
pub fn string_to_theme(theme: &str) -> Theme {
|
||||
match theme {
|
||||
"Dark" => Theme::Dark,
|
||||
"Light" => Theme::Light,
|
||||
"Lupin" => Theme::Lupin,
|
||||
_ => Theme::Lupin,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_collect)] // false positive: https://github.com/rust-lang/rust-clippy/issues/6164
|
||||
pub fn import_selection(
|
||||
packages: &mut Vec<PackageRow>,
|
||||
selection: &mut Selection,
|
||||
) -> io::Result<()> {
|
||||
let file = fs::File::open("uad_exported_selection.txt")?;
|
||||
let reader = BufReader::new(file);
|
||||
let imported_selection: Vec<String> = reader
|
||||
.lines()
|
||||
.map(|l| l.expect("Could not parse line"))
|
||||
.collect();
|
||||
pub fn setup_uad_dir(dir: Option<PathBuf>) -> PathBuf {
|
||||
let dir = dir.unwrap().join("uad");
|
||||
fs::create_dir_all(&dir).expect("Can't create cache directory");
|
||||
dir
|
||||
}
|
||||
|
||||
*selection = Selection::default(); // should already be empty normally
|
||||
pub fn open_url(dir: PathBuf) {
|
||||
#[cfg(target_os = "windows")]
|
||||
let output = Command::new("explorer").args([dir]).output();
|
||||
|
||||
for (i, p) in packages.iter_mut().enumerate() {
|
||||
if imported_selection.contains(&p.name) {
|
||||
p.selected = true;
|
||||
selection.selected_packages.push(i);
|
||||
update_selection_count(selection, p.state, true);
|
||||
#[cfg(target_os = "macos")]
|
||||
let output = Command::new("open").args([dir]).output();
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||
let output = Command::new("xdg-open").args([dir]).output();
|
||||
|
||||
match output {
|
||||
Ok(o) => {
|
||||
if !o.status.success() {
|
||||
let stderr = String::from_utf8(o.stderr).unwrap().trim_end().to_string();
|
||||
error!("Can't open the following URL: {}", stderr);
|
||||
}
|
||||
}
|
||||
Err(e) => error!("Failed to run command to open the file explorer: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
pub fn last_modified_date(file: PathBuf) -> DateTime<Utc> {
|
||||
fs::metadata(file).map_or_else(|_| Utc::now(), |metadata| match metadata.modified() {
|
||||
Ok(time) => time.into(),
|
||||
Err(_) => Utc::now(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn format_diff_time_from_now(date: DateTime<Utc>) -> String {
|
||||
let now: DateTime<Utc> = Utc::now();
|
||||
let last_update = now - date;
|
||||
if last_update.num_days() == 0 {
|
||||
if last_update.num_hours() == 0 {
|
||||
last_update.num_minutes().to_string() + " min(s) ago"
|
||||
} else {
|
||||
p.selected = false;
|
||||
last_update.num_hours().to_string() + " hour(s) ago"
|
||||
}
|
||||
} else {
|
||||
last_update.num_days().to_string() + " day(s) ago"
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn icon(unicode: char) -> Text {
|
||||
Text::new(&unicode.to_string())
|
||||
.font(ICONS)
|
||||
.width(Length::Units(17))
|
||||
.horizontal_alignment(alignment::Horizontal::Center)
|
||||
.size(17)
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DisplayablePath {
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl fmt::Display for DisplayablePath {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let stem = self.path.file_stem().map_or_else(
|
||||
|| {
|
||||
error!("[PATH STEM]: No file stem found");
|
||||
"[File steam not found]".to_string()
|
||||
},
|
||||
|p| match p.to_os_string().into_string() {
|
||||
Ok(stem) => stem,
|
||||
Err(e) => {
|
||||
error!("[PATH ENCODING]: {:?}", e);
|
||||
"[PATH ENCODING ERROR]".to_string()
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
write!(f, "{stem}")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
391
src/gui/mod.rs
391
src/gui/mod.rs
|
|
@ -2,47 +2,51 @@ pub mod style;
|
|||
pub mod views;
|
||||
pub mod widgets;
|
||||
|
||||
pub use crate::core::sync::Phone;
|
||||
pub use crate::core::uad_lists::Package;
|
||||
use crate::core::utils::icon;
|
||||
pub use views::about::About as AboutView;
|
||||
pub use views::list::{List as AppsView, Message as AppsMessage};
|
||||
pub use views::settings::{Message as SettingsMessage, Settings as SettingsView};
|
||||
use crate::core::sync::{get_devices_list, perform_adb_commands, CommandType, Phone};
|
||||
use crate::core::theme::Theme;
|
||||
use crate::core::uad_lists::UadListState;
|
||||
use crate::core::update::{get_latest_release, Release, SelfUpdateState, SelfUpdateStatus};
|
||||
use crate::core::utils::string_to_theme;
|
||||
|
||||
use views::about::{About as AboutView, Message as AboutMessage};
|
||||
use views::list::{List as AppsView, LoadingState as ListLoadingState, Message as AppsMessage};
|
||||
use views::settings::{Message as SettingsMessage, Settings as SettingsView};
|
||||
use widgets::navigation_menu::nav_menu;
|
||||
|
||||
use iced::widget::column;
|
||||
use iced::{
|
||||
button, window::Settings as Window, Alignment, Application, Button, Column, Command, Container,
|
||||
Element, Font, Length, Row, Settings, Space, Text,
|
||||
window::Settings as Window, Alignment, Application, Command, Element, Length, Renderer,
|
||||
Settings,
|
||||
};
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
pub const ICONS: Font = Font::External {
|
||||
name: "Icons",
|
||||
bytes: include_bytes!("../../ressources/assets/icons.ttf"),
|
||||
};
|
||||
#[cfg(feature = "self-update")]
|
||||
use crate::core::update::{bin_name, download_update_to_temp_file, remove_file};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum View {
|
||||
#[derive(Default, Debug, Clone)]
|
||||
enum View {
|
||||
#[default]
|
||||
List,
|
||||
About,
|
||||
Settings,
|
||||
}
|
||||
|
||||
impl Default for View {
|
||||
fn default() -> Self {
|
||||
Self::List
|
||||
}
|
||||
#[derive(Default, Clone)]
|
||||
pub struct UpdateState {
|
||||
self_update: SelfUpdateState,
|
||||
uad_list: UadListState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Default, Clone)]
|
||||
pub struct UadGui {
|
||||
phone: Phone,
|
||||
view: View,
|
||||
apps_view: AppsView,
|
||||
about_view: AboutView,
|
||||
settings_view: SettingsView,
|
||||
about_btn: button::State,
|
||||
settings_btn: button::State,
|
||||
apps_btn: button::State,
|
||||
apps_refresh_btn: button::State,
|
||||
devices_list: Vec<Phone>,
|
||||
selected_device: Option<Phone>, // index of devices_list
|
||||
update_state: UpdateState,
|
||||
nb_running_async_adb_commands: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -50,15 +54,21 @@ pub enum Message {
|
|||
// Navigation Panel
|
||||
AboutPressed,
|
||||
SettingsPressed,
|
||||
AppsRefreshPress,
|
||||
AppsPress,
|
||||
|
||||
DeviceSelected(Phone),
|
||||
AboutAction(AboutMessage),
|
||||
AppsAction(AppsMessage),
|
||||
SettingsAction(SettingsMessage),
|
||||
Init(AppsMessage),
|
||||
RefreshButtonPressed,
|
||||
RebootButtonPressed,
|
||||
LoadDevices(Vec<Phone>),
|
||||
_NewReleaseDownloaded(Result<(PathBuf, PathBuf), ()>),
|
||||
GetLatestRelease(Result<Option<Release>, ()>),
|
||||
Nothing,
|
||||
}
|
||||
|
||||
impl Application for UadGui {
|
||||
type Theme = Theme;
|
||||
type Executor = iced::executor::Default;
|
||||
type Message = Message;
|
||||
type Flags = ();
|
||||
|
|
@ -66,34 +76,45 @@ impl Application for UadGui {
|
|||
fn new(_flags: ()) -> (Self, Command<Message>) {
|
||||
(
|
||||
Self::default(),
|
||||
Command::perform(Self::load_phone_packages(), Message::Init),
|
||||
Command::batch([
|
||||
Command::perform(get_devices_list(), Message::LoadDevices),
|
||||
Command::perform(
|
||||
async move { get_latest_release() },
|
||||
Message::GetLatestRelease,
|
||||
),
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
String::from("UadGui")
|
||||
fn theme(&self) -> Theme {
|
||||
string_to_theme(&self.settings_view.general.theme)
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Command<Message> {
|
||||
match message {
|
||||
Message::Init(_) => {
|
||||
info!(
|
||||
"ANDROID_SDK: {} | PHONE: {}",
|
||||
self.phone.android_sdk, self.phone.model
|
||||
);
|
||||
Command::perform(Self::load_phone_packages(), Message::AppsAction)
|
||||
}
|
||||
Message::AppsRefreshPress => {
|
||||
self.phone = Phone::default();
|
||||
self.settings_view = SettingsView::default();
|
||||
info!("{:-^65}", "-");
|
||||
info!(
|
||||
"ANDROID_SDK: {} | PHONE: {}",
|
||||
self.phone.android_sdk, self.phone.model
|
||||
);
|
||||
self.apps_view = AppsView::default();
|
||||
self.view = View::List;
|
||||
Command::perform(Self::load_phone_packages(), Message::AppsAction)
|
||||
fn title(&self) -> String {
|
||||
String::from("Universal Android Debloater")
|
||||
}
|
||||
fn update(&mut self, msg: Message) -> Command<Message> {
|
||||
match msg {
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
Message::LoadDevices(devices_list) => {
|
||||
self.selected_device = match &self.selected_device {
|
||||
Some(s_device) => {
|
||||
// Try to reload last selected phone
|
||||
devices_list
|
||||
.iter()
|
||||
.find(|phone| phone.adb_id == s_device.adb_id)
|
||||
.cloned()
|
||||
}
|
||||
None => devices_list.first().cloned(),
|
||||
};
|
||||
self.devices_list = devices_list;
|
||||
|
||||
#[allow(unused_must_use)]
|
||||
{
|
||||
self.update(Message::SettingsAction(SettingsMessage::LoadDeviceSettings));
|
||||
}
|
||||
|
||||
self.update(Message::AppsAction(AppsMessage::LoadUadList(true)))
|
||||
}
|
||||
Message::AppsPress => {
|
||||
self.view = View::List;
|
||||
|
|
@ -101,100 +122,246 @@ impl Application for UadGui {
|
|||
}
|
||||
Message::AboutPressed => {
|
||||
self.view = View::About;
|
||||
Command::none()
|
||||
self.update_state.self_update = SelfUpdateState::default();
|
||||
Command::perform(
|
||||
async move { get_latest_release() },
|
||||
Message::GetLatestRelease,
|
||||
)
|
||||
}
|
||||
Message::SettingsPressed => {
|
||||
self.view = View::Settings;
|
||||
Command::none()
|
||||
}
|
||||
Message::RefreshButtonPressed => {
|
||||
self.apps_view = AppsView::default();
|
||||
Command::perform(get_devices_list(), Message::LoadDevices)
|
||||
}
|
||||
Message::RebootButtonPressed => {
|
||||
self.apps_view = AppsView::default();
|
||||
self.selected_device = None;
|
||||
self.devices_list = vec![];
|
||||
Command::perform(
|
||||
perform_adb_commands("reboot".to_string(), CommandType::Shell),
|
||||
|_| Message::Nothing,
|
||||
)
|
||||
}
|
||||
Message::AppsAction(msg) => self
|
||||
.apps_view
|
||||
.update(&self.settings_view, &mut self.phone, msg)
|
||||
.update(
|
||||
&mut self.settings_view,
|
||||
&mut self.selected_device.clone().unwrap_or_default(),
|
||||
&mut self.update_state.uad_list,
|
||||
msg,
|
||||
)
|
||||
.map(Message::AppsAction),
|
||||
Message::SettingsAction(msg) => {
|
||||
self.settings_view.update(msg);
|
||||
match msg {
|
||||
SettingsMessage::RestoringDevice(ref output) => {
|
||||
self.nb_running_async_adb_commands -= 1;
|
||||
self.view = View::List;
|
||||
|
||||
#[allow(unused_must_use)]
|
||||
{
|
||||
self.apps_view.update(
|
||||
&mut self.settings_view,
|
||||
&mut self.selected_device.clone().unwrap_or_default(),
|
||||
&mut self.update_state.uad_list,
|
||||
AppsMessage::RestoringDevice(output.clone()),
|
||||
);
|
||||
}
|
||||
if self.nb_running_async_adb_commands == 0 {
|
||||
return self.update(Message::RefreshButtonPressed);
|
||||
}
|
||||
}
|
||||
SettingsMessage::MultiUserMode(toggled) => {
|
||||
if toggled {
|
||||
for user in self.apps_view.phone_packages.clone() {
|
||||
for (i, _) in
|
||||
user.iter().enumerate().filter(|&(_, pkg)| pkg.selected)
|
||||
{
|
||||
for u in self
|
||||
.selected_device
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.user_list
|
||||
.iter()
|
||||
.filter(|&u| !u.protected)
|
||||
{
|
||||
self.apps_view.phone_packages[u.index][i].selected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
self.settings_view
|
||||
.update(
|
||||
&self.selected_device.clone().unwrap_or_default(),
|
||||
&self.apps_view.phone_packages,
|
||||
&mut self.nb_running_async_adb_commands,
|
||||
msg,
|
||||
)
|
||||
.map(Message::SettingsAction)
|
||||
}
|
||||
Message::AboutAction(msg) => {
|
||||
self.about_view.update(msg.clone());
|
||||
|
||||
match msg {
|
||||
AboutMessage::UpdateUadLists => {
|
||||
self.update_state.uad_list = UadListState::Downloading;
|
||||
self.apps_view.loading_state =
|
||||
ListLoadingState::DownloadingList(String::new());
|
||||
self.update(Message::AppsAction(AppsMessage::LoadUadList(true)))
|
||||
}
|
||||
AboutMessage::DoSelfUpdate => {
|
||||
#[cfg(feature = "self-update")]
|
||||
if self.update_state.self_update.latest_release.is_some() {
|
||||
self.update_state.self_update.status = SelfUpdateStatus::Updating;
|
||||
self.apps_view.loading_state =
|
||||
ListLoadingState::_UpdatingUad(String::new());
|
||||
let bin_name = bin_name().to_owned();
|
||||
let release = self
|
||||
.update_state
|
||||
.self_update
|
||||
.latest_release
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.clone();
|
||||
Command::perform(
|
||||
download_update_to_temp_file(bin_name, release),
|
||||
Message::_NewReleaseDownloaded,
|
||||
)
|
||||
} else {
|
||||
Command::none()
|
||||
}
|
||||
#[cfg(not(feature = "self-update"))]
|
||||
Command::none()
|
||||
}
|
||||
AboutMessage::UrlPressed(_) => Command::none(),
|
||||
}
|
||||
}
|
||||
Message::DeviceSelected(s_device) => {
|
||||
self.selected_device = Some(s_device.clone());
|
||||
self.view = View::List;
|
||||
env::set_var("ANDROID_SERIAL", s_device.adb_id);
|
||||
info!("{:-^65}", "-");
|
||||
info!(
|
||||
"ANDROID_SDK: {} | DEVICE: {}",
|
||||
s_device.android_sdk, s_device.model
|
||||
);
|
||||
info!("{:-^65}", "-");
|
||||
self.apps_view.loading_state = ListLoadingState::FindingPhones(String::new());
|
||||
|
||||
#[allow(unused_must_use)]
|
||||
{
|
||||
self.update(Message::SettingsAction(SettingsMessage::LoadDeviceSettings));
|
||||
}
|
||||
self.update(Message::AppsAction(AppsMessage::LoadPhonePackages((
|
||||
self.apps_view.uad_lists.clone(),
|
||||
UadListState::Done,
|
||||
))))
|
||||
}
|
||||
Message::_NewReleaseDownloaded(res) => {
|
||||
debug!("UAD update has been download!");
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
if let Ok((relaunch_path, cleanup_path)) = res {
|
||||
// Remove first arg, which is path to binary. We don't use this first
|
||||
// arg as binary path because it's not reliable, per the docs.
|
||||
let mut args = std::env::args();
|
||||
args.next();
|
||||
let mut args: Vec<_> = args.collect();
|
||||
|
||||
// Remove the `--self-update-temp` arg from args if it exists,
|
||||
// since we need to pass it cleanly. Otherwise new process will
|
||||
// fail during arg parsing.
|
||||
if let Some(idx) = args.iter().position(|a| a == "--self-update-temp") {
|
||||
args.remove(idx);
|
||||
// Remove path passed after this arg
|
||||
args.remove(idx);
|
||||
}
|
||||
|
||||
match std::process::Command::new(relaunch_path)
|
||||
.args(args)
|
||||
.arg("--self-update-temp")
|
||||
.arg(&cleanup_path)
|
||||
.spawn()
|
||||
{
|
||||
Ok(_) => {
|
||||
if let Err(e) = remove_file(cleanup_path) {
|
||||
error!("Could not remove temp update file: {}", e);
|
||||
}
|
||||
std::process::exit(0)
|
||||
}
|
||||
Err(error) => {
|
||||
if let Err(e) = remove_file(cleanup_path) {
|
||||
error!("Could not remove temp update file: {}", e);
|
||||
}
|
||||
error!("Failed to update UAD: {}", error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("Failed to update UAD!");
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
Message::GetLatestRelease(release) => {
|
||||
match release {
|
||||
Ok(r) => {
|
||||
self.update_state.self_update.status = SelfUpdateStatus::Done;
|
||||
self.update_state.self_update.latest_release = r;
|
||||
}
|
||||
Err(_) => self.update_state.self_update.status = SelfUpdateStatus::Failed,
|
||||
};
|
||||
Command::none()
|
||||
}
|
||||
Message::Nothing => Command::none(),
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&mut self) -> Element<Message> {
|
||||
let apps_btn = Button::new(&mut self.apps_btn, Text::new("Apps"))
|
||||
.on_press(Message::AppsPress)
|
||||
.padding(5)
|
||||
.style(style::PrimaryButton(self.settings_view.theme.palette));
|
||||
|
||||
let apps_refresh_btn = Button::new(&mut self.apps_refresh_btn, refresh_icon())
|
||||
.on_press(Message::AppsRefreshPress)
|
||||
.padding(5)
|
||||
.style(style::RefreshButton(self.settings_view.theme.palette));
|
||||
|
||||
let uad_version = Text::new(env!("CARGO_PKG_VERSION"));
|
||||
|
||||
let about_btn = Button::new(&mut self.about_btn, Text::new("About"))
|
||||
.on_press(Message::AboutPressed)
|
||||
.padding(5)
|
||||
.style(style::PrimaryButton(self.settings_view.theme.palette));
|
||||
|
||||
let settings_btn = Button::new(&mut self.settings_btn, Text::new("Settings"))
|
||||
.on_press(Message::SettingsPressed)
|
||||
.padding(5)
|
||||
.style(style::PrimaryButton(self.settings_view.theme.palette));
|
||||
|
||||
let row = Row::new()
|
||||
.width(Length::Fill)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(10)
|
||||
.push(apps_refresh_btn)
|
||||
.push(Text::new("Device: ".to_string() + &self.phone.model))
|
||||
.push(Space::new(Length::Fill, Length::Shrink))
|
||||
.push(uad_version)
|
||||
.push(apps_btn)
|
||||
.push(about_btn)
|
||||
.push(settings_btn);
|
||||
|
||||
let navigation_container = Container::new(row)
|
||||
.width(Length::Fill)
|
||||
.padding(10)
|
||||
.style(style::NavigationContainer(self.settings_view.theme.palette));
|
||||
fn view(&self) -> Element<Self::Message, Renderer<Self::Theme>> {
|
||||
let navigation_container = nav_menu(
|
||||
&self.devices_list,
|
||||
self.selected_device.clone(),
|
||||
&self.apps_view,
|
||||
&self.update_state.self_update,
|
||||
);
|
||||
|
||||
let selected_device = self.selected_device.clone().unwrap_or_default();
|
||||
let main_container = match self.view {
|
||||
View::List => self
|
||||
.apps_view
|
||||
.view(&self.settings_view, &self.phone)
|
||||
.view(&self.settings_view, &selected_device)
|
||||
.map(Message::AppsAction),
|
||||
View::About => self.about_view.view(&self.settings_view),
|
||||
View::Settings => self.settings_view.view().map(Message::SettingsAction),
|
||||
View::About => self
|
||||
.about_view
|
||||
.view(&self.update_state)
|
||||
.map(Message::AboutAction),
|
||||
View::Settings => self
|
||||
.settings_view
|
||||
.view(&selected_device)
|
||||
.map(Message::SettingsAction),
|
||||
};
|
||||
|
||||
Column::new()
|
||||
column![navigation_container, main_container]
|
||||
.width(Length::Fill)
|
||||
.push(navigation_container)
|
||||
.push(main_container)
|
||||
.align_items(Alignment::Center)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl UadGui {
|
||||
pub fn start() {
|
||||
let settings: Settings<()> = Settings {
|
||||
pub fn start() -> iced::Result {
|
||||
Self::run(Settings {
|
||||
window: Window {
|
||||
size: (1050, 800),
|
||||
resizable: true,
|
||||
decorations: true,
|
||||
..iced::window::Settings::default()
|
||||
},
|
||||
default_text_size: 17,
|
||||
..iced::Settings::default()
|
||||
};
|
||||
Self::run(settings).unwrap_err();
|
||||
}
|
||||
|
||||
pub async fn load_phone_packages() -> AppsMessage {
|
||||
AppsMessage::LoadPackages
|
||||
default_text_size: 17.0,
|
||||
..Settings::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_icon() -> Text {
|
||||
icon('\u{E900}')
|
||||
}
|
||||
|
|
|
|||
668
src/gui/style.rs
668
src/gui/style.rs
|
|
@ -1,395 +1,479 @@
|
|||
use crate::core::theme::ColorPalette;
|
||||
use iced::{button, checkbox, container, pick_list, scrollable, text_input, Background, Color};
|
||||
use crate::core::theme::Theme;
|
||||
use iced::overlay::menu;
|
||||
use iced::widget::{
|
||||
button, checkbox, container, pick_list, radio, rule, scrollable, text, text_input,
|
||||
};
|
||||
use iced::{application, Background, Color};
|
||||
|
||||
pub struct Content(pub ColorPalette);
|
||||
impl container::StyleSheet for Content {
|
||||
fn style(&self) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(self.0.base.background)),
|
||||
text_color: Some(self.0.bright.surface),
|
||||
..container::Style::default()
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
pub enum Application {
|
||||
#[default]
|
||||
Default,
|
||||
}
|
||||
|
||||
impl application::StyleSheet for Theme {
|
||||
type Style = Application;
|
||||
|
||||
fn appearance(&self, _style: &Self::Style) -> application::Appearance {
|
||||
application::Appearance {
|
||||
background_color: self.palette().base.background,
|
||||
text_color: self.palette().bright.surface,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NavigationContainer(pub ColorPalette);
|
||||
impl container::StyleSheet for NavigationContainer {
|
||||
fn style(&self) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(self.0.base.foreground)),
|
||||
text_color: Some(self.0.bright.surface),
|
||||
..container::Style::default()
|
||||
}
|
||||
}
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
pub enum Container {
|
||||
#[default]
|
||||
Invisible,
|
||||
Frame,
|
||||
BorderedFrame,
|
||||
Tooltip,
|
||||
Background,
|
||||
}
|
||||
|
||||
pub struct PrimaryButton(pub ColorPalette);
|
||||
impl button::StyleSheet for PrimaryButton {
|
||||
fn active(&self) -> button::Style {
|
||||
button::Style {
|
||||
border_color: Color {
|
||||
a: 0.5,
|
||||
..self.0.bright.primary
|
||||
impl container::StyleSheet for Theme {
|
||||
type Style = Container;
|
||||
|
||||
fn appearance(&self, style: &Self::Style) -> container::Appearance {
|
||||
match style {
|
||||
Container::Invisible => container::Appearance::default(),
|
||||
Container::Frame => container::Appearance {
|
||||
background: Some(Background::Color(self.palette().base.foreground)),
|
||||
text_color: Some(self.palette().bright.surface),
|
||||
border_radius: 5.0,
|
||||
..container::Appearance::default()
|
||||
},
|
||||
Container::BorderedFrame => container::Appearance {
|
||||
background: Some(Background::Color(self.palette().base.foreground)),
|
||||
text_color: Some(self.palette().bright.surface),
|
||||
border_radius: 5.0,
|
||||
border_width: 1.0,
|
||||
border_color: self.palette().normal.error,
|
||||
},
|
||||
Container::Tooltip => container::Appearance {
|
||||
background: Some(Background::Color(self.palette().base.foreground)),
|
||||
text_color: Some(self.palette().bright.surface),
|
||||
border_radius: 8.0,
|
||||
border_width: 1.0,
|
||||
border_color: self.palette().normal.primary,
|
||||
},
|
||||
|
||||
Container::Background => container::Appearance {
|
||||
background: Some(Background::Color(self.palette().base.background)),
|
||||
text_color: Some(self.palette().bright.surface),
|
||||
border_radius: 5.0,
|
||||
..container::Appearance::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Button {
|
||||
#[default]
|
||||
Primary,
|
||||
Unavailable,
|
||||
SelfUpdate,
|
||||
Refresh,
|
||||
UninstallPackage,
|
||||
RestorePackage,
|
||||
NormalPackage,
|
||||
SelectedPackage,
|
||||
}
|
||||
|
||||
impl button::StyleSheet for Theme {
|
||||
type Style = Button;
|
||||
|
||||
fn active(&self, style: &Self::Style) -> button::Appearance {
|
||||
let p = self.palette();
|
||||
|
||||
let appearance = button::Appearance {
|
||||
border_width: 1.0,
|
||||
border_radius: 2.0,
|
||||
text_color: self.0.bright.primary,
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
..button::Appearance::default()
|
||||
};
|
||||
|
||||
fn hovered(&self) -> button::Style {
|
||||
button::Style {
|
||||
background: Some(Background::Color(Color {
|
||||
a: 0.25,
|
||||
..self.0.normal.surface
|
||||
})),
|
||||
text_color: self.0.bright.primary,
|
||||
..self.active()
|
||||
}
|
||||
}
|
||||
}
|
||||
let active_appearance = |bg: Option<Color>, mc| button::Appearance {
|
||||
background: Some(Background::Color(bg.unwrap_or(p.base.foreground))),
|
||||
border_color: Color { a: 0.5, ..mc },
|
||||
text_color: mc,
|
||||
..appearance
|
||||
};
|
||||
|
||||
pub struct RefreshButton(pub ColorPalette);
|
||||
impl button::StyleSheet for RefreshButton {
|
||||
fn active(&self) -> button::Style {
|
||||
button::Style {
|
||||
border_color: Color {
|
||||
a: 0.5,
|
||||
..self.0.bright.primary
|
||||
match style {
|
||||
Button::Primary | Button::SelfUpdate | Button::Refresh => {
|
||||
active_appearance(None, p.bright.primary)
|
||||
}
|
||||
Button::RestorePackage => active_appearance(None, p.bright.secondary),
|
||||
Button::NormalPackage => button::Appearance {
|
||||
background: Some(Background::Color(p.base.foreground)),
|
||||
text_color: p.bright.surface,
|
||||
border_radius: 5.0,
|
||||
border_width: 0.0,
|
||||
border_color: p.base.background,
|
||||
..appearance
|
||||
},
|
||||
border_width: 1.0,
|
||||
border_radius: 2.0,
|
||||
text_color: self.0.bright.primary,
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self) -> button::Style {
|
||||
button::Style {
|
||||
background: Some(Background::Color(Color {
|
||||
a: 0.25,
|
||||
..self.0.normal.surface
|
||||
})),
|
||||
text_color: self.0.bright.primary,
|
||||
..self.active()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum PackageButton {
|
||||
Uninstall(ColorPalette),
|
||||
Restore(ColorPalette),
|
||||
}
|
||||
|
||||
impl button::StyleSheet for PackageButton {
|
||||
fn active(&self) -> button::Style {
|
||||
match self {
|
||||
Self::Uninstall(palette) => button::Style {
|
||||
border_color: Color {
|
||||
a: 0.5,
|
||||
..palette.bright.error
|
||||
},
|
||||
border_width: 1.0,
|
||||
border_radius: 2.0,
|
||||
text_color: palette.bright.error,
|
||||
..button::Style::default()
|
||||
},
|
||||
Self::Restore(palette) => button::Style {
|
||||
border_color: Color {
|
||||
a: 0.5,
|
||||
..palette.bright.secondary
|
||||
},
|
||||
border_width: 1.0,
|
||||
border_radius: 2.0,
|
||||
text_color: palette.bright.secondary,
|
||||
..button::Style::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self) -> button::Style {
|
||||
match self {
|
||||
Self::Restore(palette) => button::Style {
|
||||
Button::SelectedPackage => button::Appearance {
|
||||
background: Some(Background::Color(Color {
|
||||
a: 0.25,
|
||||
..palette.normal.primary
|
||||
..p.normal.primary
|
||||
})),
|
||||
text_color: palette.bright.primary,
|
||||
..self.active()
|
||||
},
|
||||
Self::Uninstall(palette) => button::Style {
|
||||
background: Some(Background::Color(Color {
|
||||
a: 0.25,
|
||||
..palette.normal.primary
|
||||
})),
|
||||
text_color: palette.bright.primary,
|
||||
..self.active()
|
||||
text_color: p.bright.primary,
|
||||
border_radius: 5.0,
|
||||
border_width: 0.0,
|
||||
border_color: p.normal.primary,
|
||||
..appearance
|
||||
},
|
||||
Button::Unavailable | Button::UninstallPackage => {
|
||||
active_appearance(None, p.bright.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn disabled(&self) -> button::Style {
|
||||
match self {
|
||||
Self::Restore(palette) => button::Style {
|
||||
background: Some(Background::Color(Color {
|
||||
a: 0.05,
|
||||
..palette.normal.primary
|
||||
})),
|
||||
text_color: Color {
|
||||
a: 0.50,
|
||||
..palette.bright.primary
|
||||
},
|
||||
..self.active()
|
||||
},
|
||||
Self::Uninstall(palette) => button::Style {
|
||||
background: Some(Background::Color(Color {
|
||||
a: 0.01,
|
||||
..palette.bright.error
|
||||
})),
|
||||
text_color: Color {
|
||||
a: 0.50,
|
||||
..palette.bright.error
|
||||
},
|
||||
..self.active()
|
||||
fn hovered(&self, style: &Self::Style) -> button::Appearance {
|
||||
let active = self.active(style);
|
||||
let p = self.palette();
|
||||
|
||||
let hover_appearance = |bg, tc: Option<Color>| button::Appearance {
|
||||
background: Some(Background::Color(Color { a: 0.25, ..bg })),
|
||||
text_color: tc.unwrap_or(bg),
|
||||
..active
|
||||
};
|
||||
|
||||
match style {
|
||||
Button::Primary | Button::SelfUpdate | Button::Refresh => {
|
||||
hover_appearance(p.bright.primary, None)
|
||||
}
|
||||
Button::NormalPackage => hover_appearance(p.normal.primary, Some(p.bright.surface)),
|
||||
Button::SelectedPackage => hover_appearance(p.normal.primary, None),
|
||||
Button::RestorePackage => hover_appearance(p.bright.secondary, None),
|
||||
Button::Unavailable | Button::UninstallPackage => {
|
||||
hover_appearance(p.bright.error, None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pressed(&self, style: &Self::Style) -> button::Appearance {
|
||||
self.active(style)
|
||||
}
|
||||
|
||||
fn disabled(&self, style: &Self::Style) -> button::Appearance {
|
||||
let active = self.active(style);
|
||||
let p = self.palette();
|
||||
|
||||
let disabled_appearance = |bg, tc: Option<Color>| button::Appearance {
|
||||
background: Some(Background::Color(Color { a: 0.05, ..bg })),
|
||||
text_color: Color {
|
||||
a: 0.50,
|
||||
..tc.unwrap_or(bg)
|
||||
},
|
||||
..active
|
||||
};
|
||||
|
||||
match style {
|
||||
Button::RestorePackage => disabled_appearance(p.normal.primary, Some(p.bright.primary)),
|
||||
Button::UninstallPackage => disabled_appearance(p.bright.error, None),
|
||||
Button::Primary => disabled_appearance(p.bright.primary, Some(p.bright.primary)),
|
||||
_ => active,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PackageRow(pub ColorPalette);
|
||||
impl button::StyleSheet for PackageRow {
|
||||
fn active(&self) -> button::Style {
|
||||
button::Style {
|
||||
background: Some(Background::Color(self.0.base.foreground)),
|
||||
text_color: self.0.bright.surface,
|
||||
border_radius: 5.0,
|
||||
border_width: 0.0,
|
||||
border_color: self.0.base.background,
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
fn hovered(&self) -> button::Style {
|
||||
button::Style {
|
||||
background: Some(Background::Color(Color {
|
||||
a: 0.25,
|
||||
..self.0.normal.primary
|
||||
})),
|
||||
text_color: self.0.bright.primary,
|
||||
..self.active()
|
||||
}
|
||||
}
|
||||
fn pressed(&self) -> button::Style {
|
||||
button::Style {
|
||||
background: Some(Background::Color(Color::from_rgb(0.35, 0.43, 0.46))),
|
||||
text_color: self.0.bright.primary,
|
||||
..self.active()
|
||||
}
|
||||
}
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
pub enum Scrollable {
|
||||
#[default]
|
||||
Description,
|
||||
Packages,
|
||||
}
|
||||
|
||||
pub struct Description(pub ColorPalette);
|
||||
impl container::StyleSheet for Description {
|
||||
fn style(&self) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(self.0.base.foreground)),
|
||||
text_color: Some(self.0.bright.surface),
|
||||
border_radius: 5.0,
|
||||
border_width: 0.0,
|
||||
border_color: self.0.base.background,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl scrollable::StyleSheet for Theme {
|
||||
type Style = Scrollable;
|
||||
|
||||
pub struct DescriptionScrollable(pub ColorPalette);
|
||||
impl scrollable::StyleSheet for DescriptionScrollable {
|
||||
fn active(&self) -> scrollable::Scrollbar {
|
||||
scrollable::Scrollbar {
|
||||
background: Some(Background::Color(self.0.base.foreground)),
|
||||
fn active(&self, style: &Self::Style) -> scrollable::Scrollbar {
|
||||
let from_appearance = |c: Color| scrollable::Scrollbar {
|
||||
background: Some(Background::Color(Color::TRANSPARENT)),
|
||||
border_radius: 5.0,
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
scroller: scrollable::Scroller {
|
||||
color: self.0.base.foreground,
|
||||
border_radius: 0.0,
|
||||
border_width: 0.0,
|
||||
color: c,
|
||||
border_radius: 5.0,
|
||||
border_width: 1.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
};
|
||||
|
||||
match style {
|
||||
Scrollable::Description => from_appearance(self.palette().normal.surface),
|
||||
Scrollable::Packages => from_appearance(self.palette().base.foreground),
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self) -> scrollable::Scrollbar {
|
||||
let active = self.active();
|
||||
|
||||
fn hovered(&self, style: &Self::Style, _mouse_over_scrollbar: bool) -> scrollable::Scrollbar {
|
||||
scrollable::Scrollbar {
|
||||
scroller: scrollable::Scroller { ..active.scroller },
|
||||
..active
|
||||
scroller: self.active(style).scroller,
|
||||
..self.active(style)
|
||||
}
|
||||
}
|
||||
|
||||
fn dragging(&self) -> scrollable::Scrollbar {
|
||||
let hovered = self.hovered();
|
||||
fn dragging(&self, style: &Self::Style) -> scrollable::Scrollbar {
|
||||
let hovered = self.hovered(style, true);
|
||||
scrollable::Scrollbar {
|
||||
scroller: scrollable::Scroller { ..hovered.scroller },
|
||||
scroller: hovered.scroller,
|
||||
..hovered
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Scrollable(pub ColorPalette);
|
||||
impl scrollable::StyleSheet for Scrollable {
|
||||
fn active(&self) -> scrollable::Scrollbar {
|
||||
scrollable::Scrollbar {
|
||||
background: Some(Background::Color(self.0.base.background)),
|
||||
border_radius: 0.0,
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
scroller: scrollable::Scroller {
|
||||
color: self.0.base.foreground,
|
||||
border_radius: 2.0,
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self) -> scrollable::Scrollbar {
|
||||
let active = self.active();
|
||||
|
||||
scrollable::Scrollbar {
|
||||
scroller: scrollable::Scroller { ..active.scroller },
|
||||
..active
|
||||
}
|
||||
}
|
||||
|
||||
fn dragging(&self) -> scrollable::Scrollbar {
|
||||
let hovered = self.hovered();
|
||||
scrollable::Scrollbar {
|
||||
scroller: scrollable::Scroller { ..hovered.scroller },
|
||||
..hovered
|
||||
}
|
||||
}
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
pub enum CheckBox {
|
||||
#[default]
|
||||
PackageEnabled,
|
||||
PackageDisabled,
|
||||
SettingsEnabled,
|
||||
SettingsDisabled,
|
||||
}
|
||||
|
||||
pub enum SelectionCheckBox {
|
||||
Enabled(ColorPalette),
|
||||
Disabled(ColorPalette),
|
||||
}
|
||||
impl checkbox::StyleSheet for Theme {
|
||||
type Style = CheckBox;
|
||||
|
||||
impl checkbox::StyleSheet for SelectionCheckBox {
|
||||
fn active(&self, _is_checked: bool) -> checkbox::Style {
|
||||
match self {
|
||||
Self::Enabled(palette) => checkbox::Style {
|
||||
background: Background::Color(palette.base.background),
|
||||
checkmark_color: palette.bright.primary,
|
||||
fn active(&self, style: &Self::Style, _is_checked: bool) -> checkbox::Appearance {
|
||||
match style {
|
||||
CheckBox::PackageEnabled => checkbox::Appearance {
|
||||
background: Background::Color(self.palette().base.background),
|
||||
icon_color: self.palette().bright.primary,
|
||||
border_radius: 5.0,
|
||||
border_width: 1.0,
|
||||
border_color: palette.base.background,
|
||||
border_color: self.palette().base.background,
|
||||
text_color: Some(self.palette().bright.surface),
|
||||
},
|
||||
Self::Disabled(palette) => checkbox::Style {
|
||||
background: Background::Color(palette.base.foreground),
|
||||
checkmark_color: palette.bright.primary,
|
||||
CheckBox::PackageDisabled => checkbox::Appearance {
|
||||
background: Background::Color(Color {
|
||||
a: 0.55,
|
||||
..self.palette().base.background
|
||||
}),
|
||||
icon_color: self.palette().bright.primary,
|
||||
border_radius: 5.0,
|
||||
border_width: 1.0,
|
||||
border_color: palette.normal.primary,
|
||||
border_color: self.palette().normal.primary,
|
||||
text_color: Some(self.palette().normal.primary),
|
||||
},
|
||||
CheckBox::SettingsEnabled => checkbox::Appearance {
|
||||
background: Background::Color(self.palette().base.background),
|
||||
icon_color: self.palette().bright.primary,
|
||||
border_radius: 5.0,
|
||||
border_width: 1.0,
|
||||
border_color: self.palette().bright.primary,
|
||||
text_color: Some(self.palette().bright.surface),
|
||||
},
|
||||
CheckBox::SettingsDisabled => checkbox::Appearance {
|
||||
background: Background::Color(self.palette().base.foreground),
|
||||
icon_color: self.palette().bright.primary,
|
||||
border_radius: 5.0,
|
||||
border_width: 1.0,
|
||||
border_color: self.palette().normal.primary,
|
||||
text_color: Some(self.palette().bright.surface),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self, is_checked: bool) -> checkbox::Style {
|
||||
match self {
|
||||
Self::Enabled(palette) => checkbox::Style {
|
||||
background: Background::Color(palette.base.foreground),
|
||||
checkmark_color: palette.bright.primary,
|
||||
border_radius: 5.0,
|
||||
border_width: 2.0,
|
||||
border_color: palette.normal.primary,
|
||||
},
|
||||
fn hovered(&self, style: &Self::Style, is_checked: bool) -> checkbox::Appearance {
|
||||
let from_appearance = || checkbox::Appearance {
|
||||
background: Background::Color(self.palette().base.foreground),
|
||||
icon_color: self.palette().bright.primary,
|
||||
border_radius: 5.0,
|
||||
border_width: 2.0,
|
||||
border_color: self.palette().bright.primary,
|
||||
text_color: Some(self.palette().bright.surface),
|
||||
};
|
||||
|
||||
Self::Disabled(_) => checkbox::Style {
|
||||
..self.active(is_checked)
|
||||
},
|
||||
match style {
|
||||
CheckBox::PackageEnabled | CheckBox::SettingsEnabled => from_appearance(),
|
||||
CheckBox::PackageDisabled | CheckBox::SettingsDisabled => {
|
||||
self.active(style, is_checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SearchInput(pub ColorPalette);
|
||||
impl text_input::StyleSheet for SearchInput {
|
||||
fn active(&self) -> text_input::Style {
|
||||
text_input::Style {
|
||||
background: Background::Color(self.0.base.foreground),
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
pub enum TextInput {
|
||||
#[default]
|
||||
Default,
|
||||
}
|
||||
|
||||
impl text_input::StyleSheet for Theme {
|
||||
type Style = TextInput;
|
||||
|
||||
fn active(&self, _style: &Self::Style) -> text_input::Appearance {
|
||||
text_input::Appearance {
|
||||
background: Background::Color(self.palette().base.foreground),
|
||||
border_radius: 5.0,
|
||||
border_width: 0.0,
|
||||
border_color: self.0.base.foreground,
|
||||
border_color: self.palette().base.foreground,
|
||||
}
|
||||
}
|
||||
|
||||
fn focused(&self) -> text_input::Style {
|
||||
text_input::Style {
|
||||
background: Background::Color(self.0.base.foreground),
|
||||
fn focused(&self, _style: &Self::Style) -> text_input::Appearance {
|
||||
text_input::Appearance {
|
||||
background: Background::Color(self.palette().base.foreground),
|
||||
border_radius: 2.0,
|
||||
border_width: 1.0,
|
||||
border_color: Color {
|
||||
a: 0.5,
|
||||
..self.0.normal.primary
|
||||
..self.palette().normal.primary
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn placeholder_color(&self) -> Color {
|
||||
self.0.normal.surface
|
||||
fn placeholder_color(&self, _style: &Self::Style) -> Color {
|
||||
self.palette().normal.surface
|
||||
}
|
||||
|
||||
fn value_color(&self) -> Color {
|
||||
self.0.bright.primary
|
||||
fn value_color(&self, _style: &Self::Style) -> Color {
|
||||
self.palette().bright.primary
|
||||
}
|
||||
|
||||
fn selection_color(&self) -> Color {
|
||||
self.0.bright.secondary
|
||||
fn selection_color(&self, _style: &Self::Style) -> Color {
|
||||
self.palette().normal.primary
|
||||
}
|
||||
|
||||
/// Produces the style of an hovered text input.
|
||||
fn hovered(&self) -> text_input::Style {
|
||||
self.focused()
|
||||
fn hovered(&self, style: &Self::Style) -> text_input::Appearance {
|
||||
self.focused(style)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PickList(pub ColorPalette);
|
||||
impl pick_list::StyleSheet for PickList {
|
||||
fn menu(&self) -> pick_list::Menu {
|
||||
pick_list::Menu {
|
||||
text_color: self.0.bright.surface,
|
||||
background: Background::Color(self.0.base.foreground),
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
pub enum PickList {
|
||||
#[default]
|
||||
Default,
|
||||
}
|
||||
|
||||
impl menu::StyleSheet for Theme {
|
||||
type Style = ();
|
||||
|
||||
fn appearance(&self, _style: &Self::Style) -> menu::Appearance {
|
||||
let p = self.palette();
|
||||
|
||||
menu::Appearance {
|
||||
text_color: p.bright.surface,
|
||||
background: p.base.background.into(),
|
||||
border_width: 1.0,
|
||||
border_color: self.0.base.background,
|
||||
selected_background: Background::Color(Color {
|
||||
a: 0.15,
|
||||
..self.0.normal.primary
|
||||
}),
|
||||
selected_text_color: self.0.bright.primary,
|
||||
border_radius: 2.0,
|
||||
border_color: p.base.background,
|
||||
selected_text_color: p.bright.surface,
|
||||
selected_background: p.normal.primary.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn active(&self) -> pick_list::Style {
|
||||
pick_list::Style {
|
||||
text_color: self.0.bright.surface,
|
||||
background: self.0.base.background.into(),
|
||||
impl pick_list::StyleSheet for Theme {
|
||||
type Style = ();
|
||||
|
||||
fn active(&self, _style: &()) -> pick_list::Appearance {
|
||||
pick_list::Appearance {
|
||||
text_color: self.palette().bright.surface,
|
||||
background: self.palette().base.background.into(),
|
||||
border_width: 1.0,
|
||||
border_color: Color {
|
||||
a: 0.5,
|
||||
..self.0.normal.primary
|
||||
..self.palette().normal.primary
|
||||
},
|
||||
border_radius: 2.0,
|
||||
icon_size: 0.5,
|
||||
placeholder_color: self.0.bright.surface,
|
||||
handle_color: self.palette().bright.surface,
|
||||
placeholder_color: self.palette().bright.surface,
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self) -> pick_list::Style {
|
||||
let active = self.active();
|
||||
pick_list::Style {
|
||||
text_color: self.0.bright.primary,
|
||||
fn hovered(&self, style: &()) -> pick_list::Appearance {
|
||||
let active = self.active(style);
|
||||
pick_list::Appearance {
|
||||
border_color: self.palette().normal.primary,
|
||||
..active
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy)]
|
||||
pub enum Text {
|
||||
#[default]
|
||||
Default,
|
||||
Ok,
|
||||
Danger,
|
||||
Commentary,
|
||||
Color(Color),
|
||||
}
|
||||
|
||||
impl From<Color> for Text {
|
||||
fn from(color: Color) -> Self {
|
||||
Self::Color(color)
|
||||
}
|
||||
}
|
||||
|
||||
impl text::StyleSheet for Theme {
|
||||
type Style = Text;
|
||||
|
||||
fn appearance(&self, style: Self::Style) -> text::Appearance {
|
||||
match style {
|
||||
Text::Default => text::Appearance::default(),
|
||||
Text::Ok => text::Appearance {
|
||||
color: Some(self.palette().bright.secondary),
|
||||
},
|
||||
Text::Danger => text::Appearance {
|
||||
color: Some(self.palette().bright.error),
|
||||
},
|
||||
Text::Commentary => text::Appearance {
|
||||
color: Some(self.palette().normal.surface),
|
||||
},
|
||||
Text::Color(c) => text::Appearance { color: Some(c) },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl radio::StyleSheet for Theme {
|
||||
type Style = ();
|
||||
|
||||
fn active(&self, _style: &Self::Style, _is_selected: bool) -> radio::Appearance {
|
||||
radio::Appearance {
|
||||
background: Color::TRANSPARENT.into(),
|
||||
dot_color: self.palette().bright.primary,
|
||||
border_width: 1.0,
|
||||
border_color: self.palette().bright.primary,
|
||||
text_color: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self, style: &Self::Style, _is_selected: bool) -> radio::Appearance {
|
||||
let active = self.active(style, true);
|
||||
|
||||
radio::Appearance {
|
||||
dot_color: self.palette().bright.primary,
|
||||
border_color: self.palette().bright.primary,
|
||||
border_width: 2.0,
|
||||
..active
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy)]
|
||||
pub enum Rule {
|
||||
#[default]
|
||||
Default,
|
||||
}
|
||||
|
||||
impl rule::StyleSheet for Theme {
|
||||
type Style = Rule;
|
||||
|
||||
fn appearance(&self, style: &Self::Style) -> rule::Appearance {
|
||||
match style {
|
||||
Rule::Default => rule::Appearance {
|
||||
color: self.palette().bright.surface,
|
||||
width: 2,
|
||||
radius: 2.0,
|
||||
fill_mode: rule::FillMode::Full,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,145 @@
|
|||
use crate::gui::style;
|
||||
use crate::gui::views::settings::Settings;
|
||||
use crate::gui::Message;
|
||||
use iced::{Alignment, Column, Container, Element, Length, Text};
|
||||
use crate::core::theme::Theme;
|
||||
use crate::core::utils::{last_modified_date, open_url};
|
||||
use crate::gui::{style, UpdateState};
|
||||
use crate::CACHE_DIR;
|
||||
use iced::widget::{button, column, container, row, text, Space};
|
||||
use iced::{Alignment, Element, Length, Renderer};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
use crate::core::update::SelfUpdateStatus;
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct About {}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
UrlPressed(PathBuf),
|
||||
UpdateUadLists,
|
||||
DoSelfUpdate,
|
||||
}
|
||||
|
||||
impl About {
|
||||
pub fn view(&self, settings: &Settings) -> Element<Message> {
|
||||
let about_text = Text::new(
|
||||
"Welcome to UadGui, a completely free project that helps you remove pre-installed apps on your Android device.",
|
||||
pub fn update(&mut self, msg: Message) {
|
||||
if let Message::UrlPressed(url) = msg {
|
||||
open_url(url);
|
||||
}
|
||||
// other events are handled by UadGui update()
|
||||
}
|
||||
pub fn view(&self, update_state: &UpdateState) -> Element<Message, Renderer<Theme>> {
|
||||
let about_text = text(
|
||||
"Universal Android Debloater (UAD) is a Free and Open-Source community project aiming at simplifying \
|
||||
the removal of pre-installed apps on any Android device.",
|
||||
);
|
||||
|
||||
let content = Column::new()
|
||||
let descr_container = container(about_text)
|
||||
.width(Length::Fill)
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
.push(about_text);
|
||||
.padding(25)
|
||||
.style(style::Container::Frame);
|
||||
|
||||
Container::new(content)
|
||||
let date = last_modified_date(CACHE_DIR.join("uad_lists.json"));
|
||||
let uad_list_text = text(format!("Documentation: v{}", date.format("%Y%m%d"))).width(250);
|
||||
let last_update_text = text(update_state.uad_list.to_string());
|
||||
let uad_lists_btn = button("Update")
|
||||
.on_press(Message::UpdateUadLists)
|
||||
.padding(5)
|
||||
.style(style::Button::Primary);
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
let self_update_btn = button("Update")
|
||||
.on_press(Message::DoSelfUpdate)
|
||||
.padding(5)
|
||||
.style(style::Button::Primary);
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
let uad_version_text =
|
||||
text(format!("UAD version: v{}", env!("CARGO_PKG_VERSION"))).width(250);
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
#[rustfmt::skip]
|
||||
let self_update_text = update_state.self_update.latest_release.as_ref().map_or_else(||
|
||||
if update_state.self_update.status == SelfUpdateStatus::Done {
|
||||
"(No update available)".to_string()
|
||||
} else {
|
||||
update_state.self_update.status.to_string()
|
||||
}, |r| if update_state.self_update.status == SelfUpdateStatus::Updating {
|
||||
update_state.self_update.status.to_string()
|
||||
} else {
|
||||
format!("(v{} available)", r.tag_name)
|
||||
});
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
let last_self_update_text = text(self_update_text).style(style::Text::Default);
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
let self_update_row = row![uad_version_text, self_update_btn, last_self_update_text,]
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(10)
|
||||
.width(550);
|
||||
|
||||
let uad_list_row = row![uad_list_text, uad_lists_btn, last_update_text,]
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(10)
|
||||
.width(550);
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
let update_column = column![uad_list_row, self_update_row]
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(10);
|
||||
|
||||
#[cfg(not(feature = "self-update"))]
|
||||
let update_column = column![uad_list_row]
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(10);
|
||||
|
||||
let update_container = container(update_column)
|
||||
.width(Length::Fill)
|
||||
.center_x()
|
||||
.padding(10)
|
||||
.style(style::Container::Frame);
|
||||
|
||||
let website_btn = button("Github page")
|
||||
.on_press(Message::UrlPressed(PathBuf::from(
|
||||
"https://github.com/0x192/universal-android-debloater",
|
||||
)))
|
||||
.padding(5)
|
||||
.style(style::Button::Primary);
|
||||
|
||||
let issue_btn = button("Have an issue?")
|
||||
.on_press(Message::UrlPressed(PathBuf::from(
|
||||
"https://github.com/0x192/universal-android-debloater/issues",
|
||||
)))
|
||||
.padding(5)
|
||||
.style(style::Button::Primary);
|
||||
|
||||
let log_btn = button("Locate the logfiles")
|
||||
.on_press(Message::UrlPressed(CACHE_DIR.to_path_buf()))
|
||||
.padding(5)
|
||||
.style(style::Button::Primary);
|
||||
|
||||
let wiki_btn = button("Wiki")
|
||||
.on_press(Message::UrlPressed(PathBuf::from(
|
||||
"https://github.com/0x192/universal-android-debloater/wiki",
|
||||
)))
|
||||
.padding(5)
|
||||
.style(style::Button::Primary);
|
||||
|
||||
let row = row![website_btn, wiki_btn, issue_btn, log_btn,].spacing(20);
|
||||
|
||||
let content = column![
|
||||
Space::new(Length::Fill, Length::Shrink),
|
||||
descr_container,
|
||||
update_container,
|
||||
row,
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.spacing(20)
|
||||
.align_items(Alignment::Center);
|
||||
|
||||
container(content)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.padding(10)
|
||||
.style(style::Content(settings.theme.palette))
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,134 +1,427 @@
|
|||
use crate::core::sync::get_android_sdk;
|
||||
use crate::core::config::{BackupSettings, Config, DeviceSettings, GeneralSettings};
|
||||
use crate::core::save::{
|
||||
backup_phone, list_available_backup_user, list_available_backups, restore_backup, BACKUP_DIR,
|
||||
};
|
||||
use crate::core::sync::{get_android_sdk, perform_adb_commands, CommandType, Phone};
|
||||
use crate::core::theme::Theme;
|
||||
use crate::core::utils::{open_url, string_to_theme, DisplayablePath};
|
||||
use crate::gui::style;
|
||||
use iced::{pick_list, Checkbox, Column, Container, Element, Length, PickList, Space, Text};
|
||||
use crate::gui::views::list::PackageInfo;
|
||||
use crate::gui::widgets::package_row::PackageRow;
|
||||
|
||||
use iced::widget::{button, checkbox, column, container, pick_list, radio, row, text, Space};
|
||||
use iced::{alignment, Alignment, Command, Element, Length, Renderer};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Settings {
|
||||
pub expert_mode: bool,
|
||||
pub disable_mode: bool,
|
||||
pub multi_user_mode: bool,
|
||||
pub theme: Theme,
|
||||
theme_picklist: pick_list::State<Theme>,
|
||||
pub general: GeneralSettings,
|
||||
pub device: DeviceSettings,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
expert_mode: false,
|
||||
disable_mode: get_android_sdk() < 26,
|
||||
multi_user_mode: get_android_sdk() > 21,
|
||||
theme: Theme::lupin(),
|
||||
theme_picklist: pick_list::State::default(),
|
||||
general: Config::load_configuration_file().general,
|
||||
device: DeviceSettings::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
LoadDeviceSettings,
|
||||
ExpertMode(bool),
|
||||
DisableMode(bool),
|
||||
MultiUserMode(bool),
|
||||
ApplyTheme(Theme),
|
||||
UrlPressed(PathBuf),
|
||||
BackupSelected(DisplayablePath),
|
||||
BackupDevice,
|
||||
RestoreDevice,
|
||||
RestoringDevice(Result<CommandType, ()>),
|
||||
DeviceBackedUp(Result<(), String>),
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn update(&mut self, msg: Message) {
|
||||
pub fn update(
|
||||
&mut self,
|
||||
phone: &Phone,
|
||||
packages: &[Vec<PackageRow>],
|
||||
nb_running_async_adb_commands: &mut u32,
|
||||
msg: Message,
|
||||
) -> Command<Message> {
|
||||
match msg {
|
||||
Message::ExpertMode(toggled) => {
|
||||
info!(
|
||||
"Expert mode {}",
|
||||
if toggled { "enabled" } else { "disabled" }
|
||||
);
|
||||
self.expert_mode = toggled;
|
||||
self.general.expert_mode = toggled;
|
||||
debug!("Config change: {:?}", self);
|
||||
Config::save_changes(self, &phone.adb_id);
|
||||
Command::none()
|
||||
}
|
||||
Message::DisableMode(toggled) => {
|
||||
info!(
|
||||
"Disable mode {}",
|
||||
if toggled { "enabled" } else { "disabled" }
|
||||
);
|
||||
self.disable_mode = toggled;
|
||||
if phone.android_sdk >= 23 {
|
||||
self.device.disable_mode = toggled;
|
||||
debug!("Config change: {:?}", self);
|
||||
Config::save_changes(self, &phone.adb_id);
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
Message::MultiUserMode(toggled) => {
|
||||
info!(
|
||||
"Multi-user mode {}",
|
||||
if toggled { "enabled" } else { "disabled" }
|
||||
);
|
||||
self.multi_user_mode = toggled;
|
||||
self.device.multi_user_mode = toggled;
|
||||
debug!("Config change: {:?}", self);
|
||||
Config::save_changes(self, &phone.adb_id);
|
||||
Command::none()
|
||||
}
|
||||
Message::ApplyTheme(theme) => {
|
||||
self.theme = theme;
|
||||
self.general.theme = theme.to_string();
|
||||
debug!("Config change: {:?}", self);
|
||||
Config::save_changes(self, &phone.adb_id);
|
||||
Command::none()
|
||||
}
|
||||
Message::UrlPressed(url) => {
|
||||
open_url(url);
|
||||
Command::none()
|
||||
}
|
||||
Message::LoadDeviceSettings => {
|
||||
let backups = list_available_backups(&BACKUP_DIR.join(phone.adb_id.clone()));
|
||||
match Config::load_configuration_file()
|
||||
.devices
|
||||
.iter()
|
||||
.find(|d| d.device_id == phone.adb_id)
|
||||
{
|
||||
Some(device) => {
|
||||
self.device = device.clone();
|
||||
self.device.backup = BackupSettings {
|
||||
backups: backups.clone(),
|
||||
selected: backups.first().cloned(),
|
||||
users: phone.user_list.clone(),
|
||||
selected_user: phone.user_list.first().copied(),
|
||||
backup_state: String::new(),
|
||||
};
|
||||
}
|
||||
None => {
|
||||
self.device = DeviceSettings {
|
||||
device_id: phone.adb_id.clone(),
|
||||
multi_user_mode: phone.android_sdk > 21,
|
||||
disable_mode: false,
|
||||
backup: BackupSettings {
|
||||
backups: backups.clone(),
|
||||
selected: backups.first().cloned(),
|
||||
users: phone.user_list.clone(),
|
||||
selected_user: phone.user_list.first().copied(),
|
||||
backup_state: String::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
Command::none()
|
||||
}
|
||||
Message::BackupSelected(d_path) => {
|
||||
self.device.backup.selected = Some(d_path.clone());
|
||||
self.device.backup.users = list_available_backup_user(d_path);
|
||||
Command::none()
|
||||
}
|
||||
Message::BackupDevice => Command::perform(
|
||||
backup_phone(
|
||||
phone.user_list.clone(),
|
||||
self.device.device_id.clone(),
|
||||
packages.to_vec(),
|
||||
),
|
||||
Message::DeviceBackedUp,
|
||||
),
|
||||
Message::DeviceBackedUp(_) => {
|
||||
info!("[BACKUP] Backup successfully created");
|
||||
self.device.backup.backups =
|
||||
list_available_backups(&BACKUP_DIR.join(phone.adb_id.clone()));
|
||||
self.device.backup.selected = self.device.backup.backups.first().cloned();
|
||||
Command::none()
|
||||
}
|
||||
Message::RestoreDevice => match restore_backup(phone, packages, &self.device) {
|
||||
Ok(r_packages) => {
|
||||
let mut commands = vec![];
|
||||
*nb_running_async_adb_commands = 0;
|
||||
for p in &r_packages {
|
||||
let p_info = PackageInfo {
|
||||
i_user: 0,
|
||||
index: p.index,
|
||||
removal: "RESTORE".to_string(),
|
||||
};
|
||||
for command in p.commands.clone() {
|
||||
*nb_running_async_adb_commands += 1;
|
||||
commands.push(Command::perform(
|
||||
perform_adb_commands(
|
||||
command,
|
||||
CommandType::PackageManager(p_info.clone()),
|
||||
),
|
||||
Message::RestoringDevice,
|
||||
));
|
||||
}
|
||||
}
|
||||
if r_packages.is_empty() {
|
||||
if get_android_sdk() == 0 {
|
||||
self.device.backup.backup_state = "Device is not connected".to_string();
|
||||
} else {
|
||||
self.device.backup.backup_state =
|
||||
"Device state is already restored".to_string();
|
||||
}
|
||||
}
|
||||
info!(
|
||||
"[RESTORE] Restoring backup {}",
|
||||
self.device.backup.selected.as_ref().unwrap()
|
||||
);
|
||||
Command::batch(commands)
|
||||
}
|
||||
Err(e) => {
|
||||
self.device.backup.backup_state = e.to_string();
|
||||
error!("{} - {}", self.device.backup.selected.as_ref().unwrap(), e);
|
||||
Command::none()
|
||||
}
|
||||
},
|
||||
// Trigger an action in mod.rs (Message::SettingsAction(msg))
|
||||
Message::RestoringDevice(_) => Command::none(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(&mut self) -> Element<Message> {
|
||||
let general_category_text = Text::new("General").size(25);
|
||||
pub fn view(&self, phone: &Phone) -> Element<Message, Renderer<Theme>> {
|
||||
let radio_btn_theme = Theme::ALL
|
||||
.iter()
|
||||
.fold(row![].spacing(10), |column, option| {
|
||||
column.push(
|
||||
radio(
|
||||
format!("{}", option.clone()),
|
||||
*option,
|
||||
Some(string_to_theme(&self.general.theme)),
|
||||
Message::ApplyTheme,
|
||||
)
|
||||
.size(23),
|
||||
)
|
||||
});
|
||||
let theme_ctn = container(radio_btn_theme)
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Shrink)
|
||||
.style(style::Container::Frame);
|
||||
|
||||
let theme_picklist = PickList::new(
|
||||
&mut self.theme_picklist,
|
||||
Theme::all(),
|
||||
Some(self.theme.clone()),
|
||||
Message::ApplyTheme,
|
||||
let expert_mode_checkbox = checkbox(
|
||||
"Allow to uninstall packages marked as \"unsafe\" (I KNOW WHAT I AM DOING)",
|
||||
self.general.expert_mode,
|
||||
Message::ExpertMode,
|
||||
)
|
||||
.style(style::PickList(self.theme.palette));
|
||||
|
||||
let uad_category_text = Text::new("UAD").size(25);
|
||||
.style(style::CheckBox::SettingsEnabled);
|
||||
|
||||
let expert_mode_descr =
|
||||
Text::new("Most of unsafe packages are known to bootloop the device if removed.")
|
||||
.size(15)
|
||||
.color(self.theme.palette.normal.surface);
|
||||
text("Most of unsafe packages are known to bootloop the device if removed.")
|
||||
.style(style::Text::Commentary)
|
||||
.size(15);
|
||||
|
||||
let expert_mode_checkbox = Checkbox::new(
|
||||
self.expert_mode,
|
||||
"Allow to uninstall packages marked as \"unsafe\" (I KNOW WHAT I AM DOING)",
|
||||
Message::ExpertMode,
|
||||
);
|
||||
|
||||
let disable_mode_descr = Text::new("Default mode on older phone (< Android 8.0) where uninstalled packages can't be restored.")
|
||||
.size(15)
|
||||
.color(self.theme.palette.normal.surface);
|
||||
|
||||
let disable_mode_checkbox = Checkbox::new(
|
||||
self.disable_mode,
|
||||
"Clear and disable packages instead of uninstalling them",
|
||||
Message::DisableMode,
|
||||
);
|
||||
|
||||
let multi_user_mode_descr =
|
||||
Text::new("Disabling this setting will typically prevent affecting your work profile")
|
||||
.size(15)
|
||||
.color(self.theme.palette.normal.surface);
|
||||
|
||||
let multi_user_mode_checkbox = Checkbox::new(
|
||||
self.multi_user_mode,
|
||||
"Affect all the users of the phone (not only the selected user)",
|
||||
Message::MultiUserMode,
|
||||
);
|
||||
|
||||
let content = Column::new()
|
||||
let general_ctn = container(column![expert_mode_checkbox, expert_mode_descr].spacing(10))
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.spacing(10)
|
||||
.push(general_category_text)
|
||||
.push(Text::new("Theme"))
|
||||
.push(theme_picklist)
|
||||
.push(Space::new(Length::Fill, Length::Shrink))
|
||||
.push(uad_category_text)
|
||||
.push(expert_mode_checkbox)
|
||||
.push(expert_mode_descr)
|
||||
.push(Space::new(Length::Fill, Length::Shrink))
|
||||
.push(disable_mode_checkbox)
|
||||
.push(disable_mode_descr)
|
||||
.push(Space::new(Length::Fill, Length::Shrink))
|
||||
.push(multi_user_mode_checkbox)
|
||||
.push(multi_user_mode_descr);
|
||||
.height(Length::Shrink)
|
||||
.style(style::Container::Frame);
|
||||
|
||||
Container::new(content)
|
||||
let warning_ctn = container(
|
||||
row![
|
||||
text("The following settings only affect the currently selected device :")
|
||||
.style(style::Text::Danger),
|
||||
text(phone.model.clone()),
|
||||
Space::new(Length::Fill, Length::Shrink),
|
||||
text(phone.adb_id.clone()).style(style::Text::Commentary)
|
||||
]
|
||||
.spacing(7),
|
||||
)
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.style(style::Container::BorderedFrame);
|
||||
|
||||
let multi_user_mode_descr = row![
|
||||
text("This will not affect the following protected work profile users: ")
|
||||
.size(15)
|
||||
.style(style::Text::Commentary),
|
||||
text(
|
||||
phone
|
||||
.user_list
|
||||
.iter()
|
||||
.filter(|&u| u.protected)
|
||||
.map(|u| u.id.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
)
|
||||
.size(15)
|
||||
.style(style::Text::Danger)
|
||||
];
|
||||
|
||||
let multi_user_mode_checkbox = checkbox(
|
||||
"Affect all the users of the device (not only the selected user)",
|
||||
self.device.multi_user_mode,
|
||||
Message::MultiUserMode,
|
||||
)
|
||||
.style(style::CheckBox::SettingsEnabled);
|
||||
|
||||
let disable_checkbox_style = if phone.android_sdk >= 23 {
|
||||
style::CheckBox::SettingsEnabled
|
||||
} else {
|
||||
style::CheckBox::SettingsDisabled
|
||||
};
|
||||
|
||||
let disable_mode_descr =
|
||||
text("In some cases, it can be better to disable a package instead of uninstalling it")
|
||||
.style(style::Text::Commentary)
|
||||
.size(15);
|
||||
|
||||
let unavailable_btn = button(text("Unavailable").size(13))
|
||||
.on_press(Message::UrlPressed(PathBuf::from(
|
||||
"https://github.com/0x192/universal-android-debloater/wiki/FAQ#\
|
||||
why-is-the-disable-mode-setting-not-available-for-my-device",
|
||||
)))
|
||||
.height(22)
|
||||
.style(style::Button::Unavailable);
|
||||
|
||||
// Disabling package without root isn't really possible before Android Oreo (8.0)
|
||||
// see https://github.com/0x192/universal-android-debloater/wiki/ADB-reference
|
||||
let disable_mode_checkbox = checkbox(
|
||||
"Clear and disable packages instead of uninstalling them",
|
||||
self.device.disable_mode,
|
||||
Message::DisableMode,
|
||||
)
|
||||
.style(disable_checkbox_style);
|
||||
|
||||
let disable_setting_row = if phone.android_sdk >= 23 {
|
||||
row![
|
||||
disable_mode_checkbox,
|
||||
Space::new(Length::Fill, Length::Shrink),
|
||||
]
|
||||
.width(Length::Fill)
|
||||
} else {
|
||||
row![
|
||||
disable_mode_checkbox,
|
||||
Space::new(Length::Fill, Length::Shrink),
|
||||
unavailable_btn,
|
||||
]
|
||||
.width(Length::Fill)
|
||||
};
|
||||
|
||||
let device_specific_ctn = container(
|
||||
column![
|
||||
multi_user_mode_checkbox,
|
||||
multi_user_mode_descr,
|
||||
disable_setting_row,
|
||||
disable_mode_descr,
|
||||
]
|
||||
.spacing(10),
|
||||
)
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Shrink)
|
||||
.style(style::Container::Frame);
|
||||
|
||||
let backup_pick_list = pick_list(
|
||||
self.device.backup.backups.clone(),
|
||||
self.device.backup.selected.clone(),
|
||||
Message::BackupSelected,
|
||||
)
|
||||
.padding(6);
|
||||
|
||||
let backup_btn = button(text("Backup").horizontal_alignment(alignment::Horizontal::Center))
|
||||
.padding(5)
|
||||
.on_press(Message::BackupDevice)
|
||||
.style(style::Button::Primary)
|
||||
.width(77);
|
||||
|
||||
let restore_btn = |enabled| {
|
||||
if enabled {
|
||||
button(text("Restore").horizontal_alignment(alignment::Horizontal::Center))
|
||||
.padding(5)
|
||||
.on_press(Message::RestoreDevice)
|
||||
.width(77)
|
||||
} else {
|
||||
button(text("No backup").horizontal_alignment(alignment::Horizontal::Center))
|
||||
.padding(5)
|
||||
.width(77)
|
||||
}
|
||||
};
|
||||
|
||||
let locate_backup_btn = if self.device.backup.backups.is_empty() {
|
||||
button("Open backup directory")
|
||||
.padding(5)
|
||||
.style(style::Button::Primary)
|
||||
} else {
|
||||
button("Open backup directory")
|
||||
.on_press(Message::UrlPressed(BACKUP_DIR.join(phone.adb_id.clone())))
|
||||
.padding(5)
|
||||
.style(style::Button::Primary)
|
||||
};
|
||||
|
||||
let backup_row = row![
|
||||
backup_btn,
|
||||
"Backup the current state of the phone",
|
||||
Space::new(Length::Fill, Length::Shrink),
|
||||
locate_backup_btn,
|
||||
]
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center);
|
||||
|
||||
let restore_row = if self.device.backup.backups.is_empty() {
|
||||
row![restore_btn(false), "Restore the state of the device",]
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
} else {
|
||||
row![
|
||||
restore_btn(true),
|
||||
"Restore the state of the device",
|
||||
Space::new(Length::Fill, Length::Shrink),
|
||||
text(self.device.backup.backup_state.clone()).style(style::Text::Danger),
|
||||
backup_pick_list,
|
||||
]
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
};
|
||||
|
||||
let backup_restore_ctn = container(column![backup_row, restore_row].spacing(10))
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Shrink)
|
||||
.style(style::Container::Frame);
|
||||
|
||||
let no_device_ctn = || {
|
||||
container(text("No device detected").style(style::Text::Danger))
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.style(style::Container::BorderedFrame)
|
||||
};
|
||||
|
||||
let content = if phone.adb_id.clone().is_empty() {
|
||||
column![
|
||||
text("Theme").size(25),
|
||||
theme_ctn,
|
||||
text("General").size(25),
|
||||
general_ctn,
|
||||
text("Current device").size(25),
|
||||
no_device_ctn(),
|
||||
text("Backup / Restore").size(25),
|
||||
no_device_ctn(),
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.spacing(20)
|
||||
} else {
|
||||
column![
|
||||
text("Theme").size(25),
|
||||
theme_ctn,
|
||||
text("General").size(25),
|
||||
general_ctn,
|
||||
text("Current device").size(25),
|
||||
warning_ctn,
|
||||
device_specific_ctn,
|
||||
backup_restore_ctn,
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.spacing(20)
|
||||
};
|
||||
|
||||
container(content)
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.style(style::Content(self.theme.palette))
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1,3 @@
|
|||
pub mod modal;
|
||||
pub mod navigation_menu;
|
||||
pub mod package_row;
|
||||
|
|
|
|||
283
src/gui/widgets/modal.rs
Normal file
283
src/gui/widgets/modal.rs
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
use iced_native::alignment::Alignment;
|
||||
use iced_native::widget::{self, Tree};
|
||||
use iced_native::{
|
||||
event, layout, mouse, overlay, renderer, Clipboard, Color, Element, Event, Layout, Length,
|
||||
Point, Rectangle, Shell, Size, Widget,
|
||||
};
|
||||
|
||||
/// A widget that centers a modal element over some base element
|
||||
pub struct Modal<'a, Message, Renderer> {
|
||||
base: Element<'a, Message, Renderer>,
|
||||
modal: Element<'a, Message, Renderer>,
|
||||
on_blur: Option<Message>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Modal<'a, Message, Renderer> {
|
||||
/// Returns a new [`Modal`]
|
||||
pub fn new(
|
||||
base: impl Into<Element<'a, Message, Renderer>>,
|
||||
modal: impl Into<Element<'a, Message, Renderer>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
base: base.into(),
|
||||
modal: modal.into(),
|
||||
on_blur: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::missing_const_for_fn)]
|
||||
/// Sets the message that will be produces when the background
|
||||
/// of the [`Modal`] is pressed
|
||||
pub fn on_blur(self, on_blur: Message) -> Self {
|
||||
Self {
|
||||
on_blur: Some(on_blur),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer> for Modal<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_native::Renderer,
|
||||
Message: Clone,
|
||||
{
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
vec![Tree::new(&self.base), Tree::new(&self.modal)]
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut Tree) {
|
||||
tree.diff_children(&[&self.base, &self.modal]);
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
self.base.as_widget().width()
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.base.as_widget().height()
|
||||
}
|
||||
|
||||
fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node {
|
||||
self.base.as_widget().layout(renderer, limits)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
state: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
self.base.as_widget_mut().on_event(
|
||||
&mut state.children[0],
|
||||
event,
|
||||
layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
state: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &<Renderer as iced_native::Renderer>::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.base.as_widget().draw(
|
||||
&state.children[0],
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
state: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
_renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'b, Message, Renderer>> {
|
||||
Some(overlay::Element::new(
|
||||
layout.position(),
|
||||
Box::new(Overlay {
|
||||
content: &mut self.modal,
|
||||
tree: &mut state.children[1],
|
||||
size: layout.bounds().size(),
|
||||
on_blur: self.on_blur.clone(),
|
||||
}),
|
||||
))
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
state: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.base.as_widget().mouse_interaction(
|
||||
&state.children[0],
|
||||
layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
state: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<Message>,
|
||||
) {
|
||||
self.base
|
||||
.as_widget()
|
||||
.operate(&mut state.children[0], layout, renderer, operation);
|
||||
}
|
||||
}
|
||||
|
||||
struct Overlay<'a, 'b, Message, Renderer> {
|
||||
content: &'b mut Element<'a, Message, Renderer>,
|
||||
tree: &'b mut Tree,
|
||||
size: Size,
|
||||
on_blur: Option<Message>,
|
||||
}
|
||||
|
||||
impl<'a, 'b, Message, Renderer> overlay::Overlay<Message, Renderer>
|
||||
for Overlay<'a, 'b, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_native::Renderer,
|
||||
Message: Clone,
|
||||
{
|
||||
fn layout(&self, renderer: &Renderer, _bounds: Size, position: Point) -> layout::Node {
|
||||
let limits = layout::Limits::new(Size::ZERO, self.size)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill);
|
||||
|
||||
let mut child = self.content.as_widget().layout(renderer, &limits);
|
||||
child.align(Alignment::Center, Alignment::Center, limits.max());
|
||||
|
||||
let mut node = layout::Node::with_children(self.size, vec![child]);
|
||||
node.move_to(position);
|
||||
|
||||
node
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
let content_bounds = layout.children().next().unwrap().bounds();
|
||||
|
||||
#[allow(clippy::equatable_if_let)]
|
||||
if let Some(message) = self.on_blur.as_ref() {
|
||||
if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) = &event {
|
||||
if !content_bounds.contains(cursor_position) {
|
||||
shell.publish(message.clone());
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.content.as_widget_mut().on_event(
|
||||
self.tree,
|
||||
event,
|
||||
layout.children().next().unwrap(),
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
) {
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: layout.bounds(),
|
||||
border_radius: renderer::BorderRadius::from(0.0),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
Color {
|
||||
a: 0.80,
|
||||
..Color::BLACK
|
||||
},
|
||||
);
|
||||
|
||||
self.content.as_widget().draw(
|
||||
self.tree,
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout.children().next().unwrap(),
|
||||
cursor_position,
|
||||
&layout.bounds(),
|
||||
);
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&mut self,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<Message>,
|
||||
) {
|
||||
self.content.as_widget().operate(
|
||||
self.tree,
|
||||
layout.children().next().unwrap(),
|
||||
renderer,
|
||||
operation,
|
||||
);
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.content.as_widget().mouse_interaction(
|
||||
self.tree,
|
||||
layout.children().next().unwrap(),
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Modal<'a, Message, Renderer>> for Element<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: 'a + iced_native::Renderer,
|
||||
Message: 'a + Clone,
|
||||
{
|
||||
fn from(modal: Modal<'a, Message, Renderer>) -> Self {
|
||||
Element::new(modal)
|
||||
}
|
||||
}
|
||||
113
src/gui/widgets/navigation_menu.rs
Normal file
113
src/gui/widgets/navigation_menu.rs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
pub use crate::core::sync::Phone;
|
||||
use crate::core::theme::Theme;
|
||||
use crate::core::update::{SelfUpdateState, SelfUpdateStatus};
|
||||
pub use crate::gui::views::about::Message as AboutMessage;
|
||||
pub use crate::gui::views::list::{List as AppsView, LoadingState as ListLoadingState};
|
||||
use crate::gui::{style, Message};
|
||||
use iced::widget::{button, container, pick_list, row, text, Space, Text};
|
||||
use iced::{alignment, Alignment, Element, Font, Length, Renderer};
|
||||
|
||||
pub const ICONS: Font = Font::External {
|
||||
name: "Icons",
|
||||
bytes: include_bytes!("../../../resources/assets/icons.ttf"),
|
||||
};
|
||||
|
||||
pub fn nav_menu<'a>(
|
||||
device_list: &'a Vec<Phone>,
|
||||
selected_device: Option<Phone>,
|
||||
apps_view: &AppsView,
|
||||
self_update_state: &SelfUpdateState,
|
||||
) -> Element<'a, Message, Renderer<Theme>> {
|
||||
let apps_refresh_btn = button(
|
||||
Text::new("\u{E900}")
|
||||
.font(ICONS)
|
||||
.width(17)
|
||||
.horizontal_alignment(alignment::Horizontal::Center)
|
||||
.size(17),
|
||||
)
|
||||
.on_press(Message::RefreshButtonPressed)
|
||||
.padding(5)
|
||||
.style(style::Button::Refresh);
|
||||
|
||||
let reboot_btn = button("Reboot")
|
||||
.on_press(Message::RebootButtonPressed)
|
||||
.padding(5)
|
||||
.style(style::Button::Refresh);
|
||||
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
let uad_version_text = if let Some(r) = &self_update_state.latest_release {
|
||||
if self_update_state.status == SelfUpdateStatus::Updating {
|
||||
Text::new("Updating please wait...")
|
||||
} else {
|
||||
Text::new(format!(
|
||||
"New UAD version available {} -> {}",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
r.tag_name
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Text::new(env!("CARGO_PKG_VERSION"))
|
||||
};
|
||||
|
||||
let apps_btn = if self_update_state.latest_release.is_some() {
|
||||
button("Update")
|
||||
.on_press(Message::AboutAction(AboutMessage::DoSelfUpdate))
|
||||
.padding(5)
|
||||
.style(style::Button::SelfUpdate)
|
||||
} else {
|
||||
button("Apps")
|
||||
.on_press(Message::AppsPress)
|
||||
.padding(5)
|
||||
.style(style::Button::Primary)
|
||||
};
|
||||
|
||||
let about_btn = button("About")
|
||||
.on_press(Message::AboutPressed)
|
||||
.padding(5)
|
||||
.style(style::Button::Primary);
|
||||
|
||||
let settings_btn = button("Settings")
|
||||
.on_press(Message::SettingsPressed)
|
||||
.padding(5)
|
||||
.style(style::Button::Primary);
|
||||
|
||||
let device_list_text = match apps_view.loading_state {
|
||||
ListLoadingState::FindingPhones(_) => text("finding connected phone..."),
|
||||
_ => text("no devices/emulators found"),
|
||||
};
|
||||
|
||||
let row = match selected_device {
|
||||
Some(phone) => row![
|
||||
apps_refresh_btn,
|
||||
reboot_btn,
|
||||
pick_list(device_list, Some(phone), Message::DeviceSelected,),
|
||||
Space::new(Length::Fill, Length::Shrink),
|
||||
uad_version_text,
|
||||
apps_btn,
|
||||
about_btn,
|
||||
settings_btn,
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(10),
|
||||
None => row![
|
||||
reboot_btn,
|
||||
apps_refresh_btn,
|
||||
device_list_text,
|
||||
Space::new(Length::Fill, Length::Shrink),
|
||||
uad_version_text,
|
||||
apps_btn,
|
||||
about_btn,
|
||||
settings_btn,
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(10),
|
||||
};
|
||||
|
||||
container(row)
|
||||
.width(Length::Fill)
|
||||
.padding(10)
|
||||
.style(style::Container::Frame)
|
||||
.into()
|
||||
}
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
use crate::core::sync::Phone;
|
||||
use crate::core::theme::Theme;
|
||||
use crate::core::uad_lists::{PackageState, Removal, UadList};
|
||||
|
||||
use crate::gui::style;
|
||||
use crate::gui::views::settings::Settings;
|
||||
|
||||
use iced::{
|
||||
alignment, button, Alignment, Button, Checkbox, Command, Element, Length, Row, Space, Text,
|
||||
};
|
||||
use iced::widget::{button, checkbox, row, text, Space};
|
||||
use iced::{alignment, Alignment, Command, Element, Length, Renderer};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PackageRow {
|
||||
|
|
@ -15,9 +14,8 @@ pub struct PackageRow {
|
|||
pub description: String,
|
||||
pub uad_list: UadList,
|
||||
pub removal: Removal,
|
||||
package_btn_state: button::State,
|
||||
action_btn_state: button::State,
|
||||
pub selected: bool,
|
||||
pub current: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
|
@ -35,6 +33,7 @@ impl PackageRow {
|
|||
uad_list: UadList,
|
||||
removal: Removal,
|
||||
selected: bool,
|
||||
current: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
|
|
@ -42,19 +41,18 @@ impl PackageRow {
|
|||
description: description.to_string(),
|
||||
uad_list,
|
||||
removal,
|
||||
package_btn_state: button::State::default(),
|
||||
action_btn_state: button::State::default(),
|
||||
selected,
|
||||
current,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, _message: Message) -> Command<Message> {
|
||||
pub fn update(&mut self, _message: &Message) -> Command<Message> {
|
||||
Command::none()
|
||||
}
|
||||
|
||||
pub fn view(&mut self, settings: &Settings, phone: &Phone) -> Element<Message> {
|
||||
//let trash_svg = format!("{}/ressources/assets/trash.svg", env!("CARGO_MANIFEST_DIR"));
|
||||
//let restore_svg = format!("{}/ressources/assets/rotate.svg", env!("CARGO_MANIFEST_DIR"));
|
||||
pub fn view(&self, settings: &Settings, _phone: &Phone) -> Element<Message, Renderer<Theme>> {
|
||||
//let trash_svg = format!("{}/resources/assets/trash.svg", env!("CARGO_MANIFEST_DIR"));
|
||||
//let restore_svg = format!("{}/resources/assets/rotate.svg", env!("CARGO_MANIFEST_DIR"));
|
||||
let button_style;
|
||||
let action_text;
|
||||
let action_btn;
|
||||
|
|
@ -62,66 +60,72 @@ impl PackageRow {
|
|||
|
||||
match self.state {
|
||||
PackageState::Enabled => {
|
||||
action_text = if settings.disable_mode {
|
||||
action_text = if settings.device.disable_mode {
|
||||
"Disable"
|
||||
} else {
|
||||
"Uninstall"
|
||||
};
|
||||
button_style = style::PackageButton::Uninstall(settings.theme.palette);
|
||||
button_style = style::Button::UninstallPackage;
|
||||
}
|
||||
PackageState::Disabled => {
|
||||
action_text = "Enable";
|
||||
button_style = style::PackageButton::Restore(settings.theme.palette);
|
||||
button_style = style::Button::RestorePackage;
|
||||
}
|
||||
PackageState::Uninstalled => {
|
||||
action_text = "Restore";
|
||||
button_style = style::PackageButton::Restore(settings.theme.palette);
|
||||
button_style = style::Button::RestorePackage;
|
||||
}
|
||||
PackageState::All => {
|
||||
action_text = "Error";
|
||||
button_style = style::PackageButton::Restore(settings.theme.palette);
|
||||
warn!("Incredible! Something impossible happenned!");
|
||||
button_style = style::Button::RestorePackage;
|
||||
warn!("Incredible! Something impossible happened!");
|
||||
}
|
||||
}
|
||||
|
||||
if (self.state != PackageState::Uninstalled && settings.expert_mode)
|
||||
|| (self.removal != Removal::Unsafe && phone.android_sdk >= 26)
|
||||
// Disable any removal action for unsafe packages if expert_mode is disabled
|
||||
if self.removal != Removal::Unsafe
|
||||
|| self.state != PackageState::Enabled
|
||||
|| settings.general.expert_mode
|
||||
{
|
||||
selection_checkbox = Checkbox::new(self.selected, "", Message::ToggleSelection)
|
||||
.style(style::SelectionCheckBox::Enabled(settings.theme.palette));
|
||||
selection_checkbox = checkbox("", self.selected, Message::ToggleSelection)
|
||||
.style(style::CheckBox::PackageEnabled);
|
||||
|
||||
action_btn = Button::new(
|
||||
&mut self.action_btn_state,
|
||||
Text::new(action_text).horizontal_alignment(alignment::Horizontal::Center),
|
||||
action_btn = button(
|
||||
text(action_text)
|
||||
.horizontal_alignment(alignment::Horizontal::Center)
|
||||
.width(100),
|
||||
)
|
||||
.on_press(Message::ActionPressed);
|
||||
} else {
|
||||
selection_checkbox = Checkbox::new(self.selected, "", Message::ToggleSelection)
|
||||
.style(style::SelectionCheckBox::Disabled(settings.theme.palette));
|
||||
selection_checkbox = checkbox("", self.selected, Message::ToggleSelection)
|
||||
.style(style::CheckBox::PackageDisabled);
|
||||
|
||||
action_btn = Button::new(
|
||||
&mut self.action_btn_state,
|
||||
Text::new(action_text).horizontal_alignment(alignment::Horizontal::Center),
|
||||
action_btn = button(
|
||||
text(action_text)
|
||||
.horizontal_alignment(alignment::Horizontal::Center)
|
||||
.width(100),
|
||||
);
|
||||
}
|
||||
|
||||
Row::new()
|
||||
.push(
|
||||
Button::new(
|
||||
&mut self.package_btn_state,
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.push(selection_checkbox)
|
||||
.push(Text::new(&self.name).width(Length::FillPortion(8)))
|
||||
.push(action_btn.width(Length::FillPortion(1)).style(button_style)),
|
||||
)
|
||||
.padding(8)
|
||||
.style(style::PackageRow(settings.theme.palette))
|
||||
.width(Length::Fill)
|
||||
.on_press(Message::PackagePressed),
|
||||
row![
|
||||
button(
|
||||
row![
|
||||
selection_checkbox,
|
||||
text(&self.name).width(Length::FillPortion(8)),
|
||||
action_btn.style(button_style)
|
||||
]
|
||||
.align_items(Alignment::Center)
|
||||
)
|
||||
.push(Space::with_width(Length::Units(15)))
|
||||
.align_items(Alignment::Center)
|
||||
.into()
|
||||
.padding(8)
|
||||
.style(if self.current {
|
||||
style::Button::SelectedPackage
|
||||
} else {
|
||||
style::Button::NormalPackage
|
||||
})
|
||||
.width(Length::Fill)
|
||||
.on_press(Message::PackagePressed),
|
||||
Space::with_width(15)
|
||||
]
|
||||
.align_items(Alignment::Center)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
22
src/main.rs
22
src/main.rs
|
|
@ -1,21 +1,29 @@
|
|||
#![windows_subsystem = "windows"]
|
||||
#![feature(drain_filter)]
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use crate::core::utils::setup_uad_dir;
|
||||
use fern::{
|
||||
colors::{Color, ColoredLevelConfig},
|
||||
FormatCallback,
|
||||
};
|
||||
use log::Record;
|
||||
use static_init::dynamic;
|
||||
use std::path::PathBuf;
|
||||
use std::{fmt::Arguments, fs::OpenOptions};
|
||||
|
||||
mod core;
|
||||
mod gui;
|
||||
|
||||
fn main() {
|
||||
#[dynamic]
|
||||
static CONFIG_DIR: PathBuf = setup_uad_dir(dirs::config_dir());
|
||||
|
||||
#[dynamic]
|
||||
static CACHE_DIR: PathBuf = setup_uad_dir(dirs::cache_dir());
|
||||
|
||||
fn main() -> iced::Result {
|
||||
setup_logger().expect("setup logging");
|
||||
gui::UadGui::start();
|
||||
gui::UadGui::start()
|
||||
}
|
||||
|
||||
pub fn setup_logger() -> Result<(), fern::InitError> {
|
||||
|
|
@ -34,7 +42,7 @@ pub fn setup_logger() -> Result<(), fern::InitError> {
|
|||
record.file().unwrap_or("?"),
|
||||
record.line().map(|l| l.to_string()).unwrap_or_default(),
|
||||
message
|
||||
))
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -42,9 +50,9 @@ pub fn setup_logger() -> Result<(), fern::InitError> {
|
|||
let log_file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.append(false)
|
||||
.truncate(true)
|
||||
.open("uad.log")?;
|
||||
.append(true)
|
||||
.truncate(false)
|
||||
.open(CACHE_DIR.join(format!("UAD_{}.log", chrono::Local::now().format("%Y%m%d"))))?;
|
||||
|
||||
let file_dispatcher = fern::Dispatch::new()
|
||||
.format(make_formatter(false))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue