Compare commits
No commits in common. "main" and "v1.2.0" have entirely different histories.
55 changed files with 144 additions and 2749 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -38,10 +38,6 @@ ATK_*.log
|
|||
*.tmp
|
||||
/tmp/
|
||||
|
||||
# Raw font source dump (bundled fonts already live in frontend/src/assets/fonts/)
|
||||
/DotGothic16,Geist_Pixel,Gugi,Orbitron,Playfair_Display,etc/
|
||||
/DotGothic16,Geist_Pixel,Gugi,Orbitron,Playfair_Display,etc.zip
|
||||
|
||||
# ── PROPRIETARY — Logcat visual map. NEVER publish to GitHub. ──
|
||||
# (Builds into the shipped binary as obfuscated JS + native Go; source stays private.)
|
||||
/proprietary/
|
||||
|
|
|
|||
101
README.md
101
README.md
|
|
@ -143,72 +143,34 @@ sudo apt install scrcpy
|
|||
| 🖥️ **Screen Mirror** | Live mirror and full control via scrcpy; detachable and recordable |
|
||||
| 📊 **Dashboard** | Device info, wireless ADB, reboot controls |
|
||||
| 📁 **File Explorer** | Browse the device and your computer, push and pull, image viewer |
|
||||
| 📦 **Package Manager** | Install, uninstall, enable, disable, restore, pull APK, plus **privileged removal of protected system apps without root**. Correctly shows packages disabled/uninstalled by other tools (e.g. Canta/Shizuku) instead of hiding them, with a state filter (Enabled/Disabled/Uninstalled) |
|
||||
| 📲 **APK Installer** | Batch-install APKs from a folder or hand-picked files, with live per-file progress and select all/none |
|
||||
| 🧹 **Debloater** | 5,362 packages across Samsung, Xiaomi, Google, and 11 more OEMs |
|
||||
| 💻 **Shell Terminal** | adb shell and host, command library, export session |
|
||||
| 📡 **Live Logcat + System Map** | Real-time log streaming, plus a live, interactive map of system behaviour across subsystems |
|
||||
| 🕵️ **App Inspector** | Permissions, components, certs, SSL-pinning check, plus a 0–100 **privacy score** from a DEX tracker/ad-SDK scan |
|
||||
| 🚀 **Intent Lab** | List an app's launchable activities from `dumpsys` and start them with one click, plus a free-form implicit-intent launcher — reach hidden settings screens |
|
||||
| 📦 **Package Manager** | Install, uninstall, enable, disable, pull APK, plus **privileged removal of protected system apps without root** |
|
||||
| 🔎 **APK Audit** | Static APK security audit: perms, trackers, certs, rule findings |
|
||||
| 🧹 **Debloater** | 5,362 packages across Samsung, Xiaomi, Google, and 11 more OEMs |
|
||||
| 📡 **Live Logcat + System Map** | Real-time log streaming, plus a live, interactive map of system behaviour across subsystems |
|
||||
| 🕵️ **App Inspector** | Permissions, components, certs, SSL-pinning check |
|
||||
| 🔐 **Certificate Manager** | Install and remove user CAs for HTTPS interception |
|
||||
| 💾 **Device Backup** | `adb backup` with app selection and restore |
|
||||
| 🎚️ **Prop Editor** | Read and write all 300+ system properties |
|
||||
| 💻 **Shell Terminal** | adb shell and host, command library, export session |
|
||||
| 🧰 **Utilities** | 631 one-click commands across 50+ categories |
|
||||
| ⚡ **Flasher** | Fastboot, live-boot, Magisk root, firmware download |
|
||||
| 💿 **GSI Loader** | Boot a Generic System Image via **DSU** (temporary, no unlock/wipe) or a danger-gated **permanent fastboot flash**, with a Treble/ABI/VNDK compatibility pre-check |
|
||||
|
||||
> [!TIP]
|
||||
> Hide any module you don't use from **Settings → Sidebar Features**. 26 theme
|
||||
> palettes — Dark, Catppuccin Frappé/Latte/Mocha/Macchiato, Dracula, Gruvbox
|
||||
> Material, Nord, Tokyo Night, Solarized, Rosé Pine, Everforest, and more —
|
||||
> plus a custom accent/text colour, 8 bundled display fonts, adjustable font
|
||||
> size, and sidebar position (left, top, bottom) are all configurable too. A
|
||||
> system tray icon (Linux) lets ATK keep running in the background instead of
|
||||
> quitting.
|
||||
> Hide any module you don't use from **Settings → Sidebar Features**. Theme
|
||||
> (Dark, Catppuccin Frappé, Latte) and sidebar position (left, top, bottom) are
|
||||
> configurable too.
|
||||
|
||||
---
|
||||
|
||||
## ✨ What's new in v1.3.0
|
||||
## ✨ What's new
|
||||
|
||||
- 🖥️ **System tray** (Linux): closing the window now offers **Minimize to
|
||||
Tray** instead of only Quit, with a tray menu to show/hide the window or
|
||||
quit for good.
|
||||
- 📲 **APK Installer**: a new module to batch-install APKs from a folder or
|
||||
hand-picked files, with live per-file progress and select all/none.
|
||||
- 📦 **Package visibility fix**: Packages and App Inspector were silently
|
||||
hiding any package disabled/uninstalled by tools like Canta or Shizuku
|
||||
(`pm uninstall --user 0` doesn't fully remove it, just hides it from a plain
|
||||
package list). They now show correctly, with a state filter
|
||||
(Enabled/Disabled/Uninstalled) and per-row Restore/Enable/Disable/Uninstall.
|
||||
- 🎨 **6 more theme palettes** — Nord, Tokyo Night, Solarized Dark/Light, Rosé
|
||||
Pine, Everforest (26 total) — plus a custom text colour, 8 bundled display
|
||||
fonts, and an adjustable base font size, all in Settings → Appearance.
|
||||
- 🖱️ **Sidebar polish**: a horizontal (top/bottom) sidebar now shrinks label
|
||||
text progressively as the window narrows instead of showing a scrollbar,
|
||||
and a touchpad-triggered stuck-scrollbar-drag bug is fixed.
|
||||
|
||||
<details>
|
||||
<summary>Previous release (v1.2.0)</summary>
|
||||
|
||||
- 💿 **GSI Loader**: boot a Generic System Image via **DSU** (temporary guest OS)
|
||||
or a danger-gated **permanent fastboot flash**, with a built-in compatibility
|
||||
pre-check.
|
||||
- 🚀 **Intent Lab**: list and launch an app's exported activities from `dumpsys`,
|
||||
plus a free-form implicit-intent launcher, to reach hidden settings screens.
|
||||
- 🕵️ **Privacy & Tracker Scanner**: a 0–100 privacy score and A–F grade for any
|
||||
app, from an offline DEX tracker/ad-SDK scan, built into App Inspector.
|
||||
- 🔒 **App Lock**: password-protect the app and gate destructive actions
|
||||
(flashing, permanent GSI install) behind a separate danger-unlock.
|
||||
- 🎨 **~14 new theme palettes** — Dracula, Catppuccin Mocha/Macchiato, Gruvbox
|
||||
Material, C64, Adventure Time, and more.
|
||||
- 🧹 **Debloater**: unmatched device packages now show as **Unknown** instead of
|
||||
disappearing from the list.
|
||||
- 🔐 **Security**: fixed a path-injection issue in the local file viewer, and
|
||||
bumped several dependencies with known CVEs (`x/image`, `x/net`, `vite`,
|
||||
`postcss`, `esbuild`).
|
||||
|
||||
</details>
|
||||
- 📡 **Live System Map**: turn logcat into a live, interactive map of system behaviour across subsystems *(see below)*.
|
||||
- 🧹 **Debloater database grew from 2,157 to 5,362 packages**, with a **privileged uninstall of protected system apps without root** and a one-click **restore**.
|
||||
- 🧰 **Utilities expanded to 631 one-click commands** across 50+ categories.
|
||||
- 🎨 **Themes**: Dark, Catppuccin **Frappé**, and **Latte**. Dismissible safety banners.
|
||||
- 🔎 **APK Audit** exports to **JSON, CSV, SARIF**, with an in-app APK explorer.
|
||||
- 📦 **Smarter package ops**: combined *Disable + Uninstall*, a *disabled* badge, and verify-then-escalate so removals stick.
|
||||
- 🔌 **Offline-capable UI**: fonts are self-hosted, with no runtime CDN fetches.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -292,28 +254,6 @@ Browse factory images: https://developers.google.com/android/images
|
|||
|
||||
---
|
||||
|
||||
## 💿 GSI Loader
|
||||
|
||||
Boot a Generic System Image (GSI) two different ways, each with its own risk
|
||||
profile.
|
||||
|
||||
| Mode | What it does |
|
||||
|---|---|
|
||||
| **DSU (Temporary)** | Loads the GSI as a guest OS via Android's Dynamic System Updates. No bootloader unlock, no data wipe — reboot to return to your normal system |
|
||||
| **GSI Flasher (Permanent)** | Fastboot-flashes the GSI to the system partition. Destructive: wipes userdata, needs an unlocked bootloader. Danger-gated behind App Lock |
|
||||
|
||||
Both modes run a **compatibility pre-check** first — Treble enablement, CPU ABI,
|
||||
VNDK isolation, and Android version — so you know whether a GSI is even viable
|
||||
on the connected device before you commit to either path. You supply the GSI
|
||||
image file (ATK doesn't fetch these — see Android Flash Tool / ci.android.com).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The permanent flash path wipes user data and requires an unlocked bootloader.
|
||||
> It's gated behind App Lock's danger-unlock, same as other destructive flash
|
||||
> operations.
|
||||
|
||||
---
|
||||
|
||||
## 🔎 APK Audit
|
||||
|
||||
A static security audit of any APK, whether a browsed file or an app pulled off
|
||||
|
|
@ -355,7 +295,6 @@ Coverage: Samsung · Xiaomi · OnePlus/Oppo · Huawei · Sony · Motorola · LG
|
|||
> - **No bundled binaries.** ATK resolves `adb`, `fastboot`, and `scrcpy` from your PATH. Settings shows each binary's path and SHA-256 to verify against Google's published checksums.
|
||||
> - **No host shell string-building.** Host commands use `exec.Command(binary, args…)` (direct `execve`, no shell). Paths sent to the *device* shell are quoted, so filenames with spaces or special characters stay safe.
|
||||
> - **Input validation.** ATK validates package names, partitions, IPs, and remote paths. Fastboot flash uses a partition allowlist. Destructive flash and bootloader actions confirm first.
|
||||
> - **App Lock.** Optionally password-protect the app itself, with a separate danger-unlock required for destructive actions (permanent flashing, permanent GSI install, wiping).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -364,12 +303,12 @@ Coverage: Samsung · Xiaomi · OnePlus/Oppo · Huawei · Sony · Motorola · LG
|
|||
**Prerequisites on Ubuntu/Debian**
|
||||
```bash
|
||||
sudo apt install -y build-essential pkg-config libgtk-3-dev \
|
||||
libwebkit2gtk-4.1-dev adb fastboot
|
||||
libwebkit2gtk-4.1-dev libayatana-appindicator3-dev adb fastboot
|
||||
|
||||
# Go 1.25+ (required by go.mod)
|
||||
wget https://go.dev/dl/go1.25.0.linux-amd64.tar.gz
|
||||
# Go 1.23
|
||||
wget https://go.dev/dl/go1.23.0.linux-amd64.tar.gz
|
||||
sudo rm -rf /usr/local/go
|
||||
sudo tar -C /usr/local -xzf go1.25.0.linux-amd64.tar.gz
|
||||
sudo tar -C /usr/local -xzf go1.23.0.linux-amd64.tar.gz
|
||||
echo 'export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
|
||||
|
|
|
|||
45
RELEASE_NOTES_v1.1.0.md
Normal file
45
RELEASE_NOTES_v1.1.0.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# ATK v1.1.0 — the Live System Map release
|
||||
|
||||
The all-in-one, OEM-style Android toolkit for power users, security researchers,
|
||||
and bug hunters — now with a **first-of-its-kind real-time debugging engine**.
|
||||
|
||||
## ⭐ Headline — Live System Map
|
||||
Turn the raw logcat firehose into a **live, interactive map of what your phone is
|
||||
actually doing**. A real-time engine unifies system-level telemetry from many
|
||||
subsystems into a single live relational model — processes, services, tags and
|
||||
components become nodes; launches, crashes, ANRs, kills, signals and
|
||||
graphics/audio events become edges; every event flows source → destination.
|
||||
|
||||
- 🌐 Multiple render modes — crisp 2D graph, neon flow view, 3D hierarchical tree
|
||||
- 🧩 Layouts — force-directed, hub boxes, radial-by-importance, geometric
|
||||
- 🌊 Trackable flows — follow individual events between subsystems
|
||||
- 🚨 Auto-surfacing of crashes / ANRs / errors + your own keyword watch-rules
|
||||
- 🎯 Focus tools — isolate a node, watchlist, severity/kind filters, timeline, baseline diff
|
||||
- 🎥 Capture & export the packet stream for offline analysis
|
||||
|
||||
## ✨ What's new
|
||||
- 📡 **Live System Map** — the flagship real-time visualization (above).
|
||||
- 🧹 **Debloater database 2,157 → 5,362 packages** (Samsung, Xiaomi, Google + 11 more OEMs).
|
||||
- 🔓 **Privileged uninstall of protected system apps — without root**, plus one-click **restore**.
|
||||
- 🧰 **Utilities expanded to 631 one-click commands** across 50+ categories.
|
||||
- 🔎 **APK Audit** — static security audit (perms, trackers, certs, CWE/MASVS rule findings) with an in-app APK explorer and **JSON · CSV · SARIF** export.
|
||||
- 🎨 **Themes** — Dark, Catppuccin **Frappé** & **Latte**; configurable sidebar; dismissible safety banners.
|
||||
- 📦 **Smarter package ops** — combined *Disable + Uninstall*, a *disabled* badge, and verify-then-escalate so removals actually stick.
|
||||
- 🔌 **Fully offline-capable UI** — self-hosted fonts, no runtime CDN fetches.
|
||||
- 🖼️ New app icon.
|
||||
|
||||
## 📦 Install (Linux)
|
||||
**Debian / Ubuntu:**
|
||||
```bash
|
||||
sudo dpkg -i atk_1.1.0_amd64.deb
|
||||
```
|
||||
Requirements: `adb`, `fastboot`, `libgtk-3-0`, `libwebkit2gtk-4.1-0`
|
||||
(`scrcpy` only needed for the Screen Mirror module).
|
||||
|
||||
Other distros: build from source — see the README.
|
||||
|
||||
> ⚠️ Linux only. No bundled binaries — ATK uses *your* system `adb`/`fastboot`/`scrcpy`.
|
||||
|
||||
## 🙏 Credits
|
||||
Built on the open-source community — scrcpy, Magisk, Universal Android Debloater,
|
||||
Wails, Lucide, and more. Full attributions and licenses are in the README. GPL-3.0.
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
# ATK v1.2.0
|
||||
|
||||
Six new tools, a big theming pass, and a security cleanup.
|
||||
|
||||
## ✨ What's new
|
||||
- 💿 **GSI Loader** — boot a Generic System Image two ways: **DSU** (temporary
|
||||
guest OS, no unlock/wipe, reboot to reclean) or a **permanent fastboot flash**
|
||||
(danger-gated, requires unlocked bootloader). Built-in compatibility pre-check
|
||||
(Treble, ABI, VNDK isolation) before either path.
|
||||
- 🚀 **Intent Lab** — list an app's launchable (exported) activities straight from
|
||||
`dumpsys` and start them with one click, plus a free-form implicit-intent
|
||||
launcher. Reach hidden settings menus and internal screens that never show up
|
||||
on the launcher.
|
||||
- 📥 **Firmware** — download and SHA-verify firmware images for a device
|
||||
codename, right from the app.
|
||||
- 📱 **Screen Mirror** — scrcpy integration: start/stop mirroring and grab
|
||||
screenshots without leaving ATK.
|
||||
- 🧙 **Magisk integration** — install Magisk, extract/patch boot images, and
|
||||
manage installed modules (list, toggle, remove) in one panel.
|
||||
- 🕵️ **Privacy & Tracker Scanner** — scans an app's DEX bytecode for known
|
||||
tracker/analytics/ad SDK signatures, cross-references dangerous permissions,
|
||||
and derives a 0–100 privacy score with an A–F grade. Fully offline, no
|
||||
network calls.
|
||||
- 🔒 **App Lock** — password-protect the app and gate destructive actions
|
||||
(flashing, permanent GSI install, etc.) behind a separate danger-unlock.
|
||||
- 🔓 **Privileged uninstall, no root** — removes protected system apps via a
|
||||
bundled Android helper, with one-click restore.
|
||||
- 🎨 **~14 new theme palettes** (Dracula, Catppuccin Mocha/Macchiato,
|
||||
Gruvbox Material, C64, Adventure Time, and more), on top of the existing
|
||||
Dark/Frappé/Latte.
|
||||
- 🧹 **Debloater**: device packages with no UAD database match now show as
|
||||
**Unknown/Uncategorized** instead of silently disappearing from the list.
|
||||
- 🔎 **APK Audit**: entry viewer now has syntax-highlighted code display instead
|
||||
of a plain text dump.
|
||||
|
||||
## 🔐 Security
|
||||
- Fixed a path-injection issue in the local file viewer (CodeQL
|
||||
`go/path-injection`) — the resolved path is now validated as a real, regular
|
||||
file before being served.
|
||||
- Dependency bumps: `golang.org/x/image` and `golang.org/x/net` (DoS fixes),
|
||||
`vite`, `postcss`, and `esbuild` (dev-tooling only, not shipped in the
|
||||
built app).
|
||||
|
||||
## 📦 Install (Linux)
|
||||
**Debian / Ubuntu:**
|
||||
```bash
|
||||
sudo dpkg -i atk_1.2.0_amd64.deb
|
||||
```
|
||||
Requirements: `adb`, `fastboot`, `libgtk-3-0`, `libwebkit2gtk-4.1-0`
|
||||
(`scrcpy` only needed for the Screen Mirror module).
|
||||
|
||||
Other distros: build from source — see the README.
|
||||
|
||||
> ⚠️ Linux only. No bundled binaries — ATK uses *your* system `adb`/`fastboot`/`scrcpy`.
|
||||
|
||||
## 🙏 Credits
|
||||
Built on the open-source community — scrcpy, Magisk, Universal Android Debloater,
|
||||
Wails, Lucide, and more. Full attributions and licenses are in the README. GPL-3.0.
|
||||
64
app.go
64
app.go
|
|
@ -60,11 +60,6 @@ type FileEntry struct {
|
|||
type PackageInfo struct {
|
||||
PackageName string `json:"packageName"`
|
||||
IsEnabled bool `json:"isEnabled"`
|
||||
// IsInstalled is false when the package is present on the system image but
|
||||
// uninstalled for user 0 (e.g. via `pm uninstall --user 0`, which is what
|
||||
// Canta/Shizuku do to "remove" a system app). Such packages are still on
|
||||
// disk and show up in `pm list packages -u`, but not in the plain listing.
|
||||
IsInstalled bool `json:"isInstalled"`
|
||||
}
|
||||
|
||||
// AdbConfig holds user-configurable ADB settings
|
||||
|
|
@ -89,10 +84,6 @@ type App struct {
|
|||
// app-lock "require password for destructive actions" session window
|
||||
dangerMu sync.Mutex
|
||||
dangerUntil time.Time
|
||||
|
||||
// window visibility, tracked for the tray's show/hide toggle
|
||||
windowMu sync.Mutex
|
||||
windowShown bool
|
||||
}
|
||||
|
||||
// NewApp creates a new App instance
|
||||
|
|
@ -106,10 +97,8 @@ func NewApp() *App {
|
|||
// Startup is called when the app starts
|
||||
func (a *App) Startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
a.windowShown = true
|
||||
// Frameless windows can open off-centre on some WMs; centre on launch.
|
||||
runtime.WindowCenter(ctx)
|
||||
initTray(a)
|
||||
}
|
||||
|
||||
// Shutdown is called when the app is closing — tidy up spawned child processes
|
||||
|
|
@ -122,57 +111,4 @@ func (a *App) Shutdown(ctx context.Context) {
|
|||
if !detached {
|
||||
a.StopScrcpy()
|
||||
}
|
||||
closeTray()
|
||||
}
|
||||
|
||||
// ShowWindow restores the main window - used by the tray's "Show ATK" /
|
||||
// left-click action and by the frontend's "Minimize to tray" quit dialog.
|
||||
func (a *App) ShowWindow() {
|
||||
a.windowMu.Lock()
|
||||
a.windowShown = true
|
||||
a.windowMu.Unlock()
|
||||
runtime.WindowShow(a.ctx)
|
||||
runtime.WindowUnminimise(a.ctx)
|
||||
}
|
||||
|
||||
// HideWindow parks the window (app keeps running, reachable from the tray).
|
||||
func (a *App) HideWindow() {
|
||||
a.windowMu.Lock()
|
||||
a.windowShown = false
|
||||
a.windowMu.Unlock()
|
||||
runtime.WindowHide(a.ctx)
|
||||
}
|
||||
|
||||
// ToggleWindow shows the window if hidden, hides it if shown - the tray's
|
||||
// left-click / "Show ATK" menu action.
|
||||
func (a *App) ToggleWindow() {
|
||||
a.windowMu.Lock()
|
||||
shown := a.windowShown
|
||||
a.windowMu.Unlock()
|
||||
if shown {
|
||||
a.HideWindow()
|
||||
} else {
|
||||
a.ShowWindow()
|
||||
}
|
||||
}
|
||||
|
||||
// TrayAvailable reports whether a real system tray icon was registered (e.g.
|
||||
// false on Linux if no StatusNotifierWatcher/AppIndicator host is running).
|
||||
// The frontend uses this to decide whether "Minimize to tray" is even a
|
||||
// sensible option to offer in the close-confirmation dialog.
|
||||
func (a *App) TrayAvailable() bool {
|
||||
return trayAvailable()
|
||||
}
|
||||
|
||||
// QuitApp fully quits ATK (not just the window) - used by both the tray's
|
||||
// "Quit ATK" menu item and the frontend's close-confirmation dialog.
|
||||
func (a *App) QuitApp() {
|
||||
runtime.Quit(a.ctx)
|
||||
}
|
||||
|
||||
// OpenURL opens a link in the user's default system browser. A bare <a
|
||||
// target="_blank"> isn't reliable inside the embedded webview - this is
|
||||
// Wails' supported way to hand off to the OS.
|
||||
func (a *App) OpenURL(url string) {
|
||||
runtime.BrowserOpenURL(a.ctx, url)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10c1.28,0 2.5,-0.2 3.68,-0.68l-0.36,-1.9C14.34,19.79 13.19,20 12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8s8,3.59 8,8v0.9c0,0.61 -0.6,1.32 -1.5,1.32s-1.5,-0.71 -1.5,-1.32V8h-2v0.68C14.42,8.24 13.51,8 12.5,8C10.02,8 8,10.02 8,12.5s2.02,4.5 4.5,4.5c1.14,0 2.17,-0.42 2.96,-1.11c0.55,0.66 1.42,1.11 2.29,1.11c1.66,0 3.25,-1.34 3.25,-3.32V12C21,6.48 16.52,2 12,2zM12.5,15c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5S13.88,15 12.5,15z" fill="#bebebe"/></svg>
|
||||
|
Before Width: | Height: | Size: 600 B |
Binary file not shown.
|
Before Width: | Height: | Size: 696 B |
Binary file not shown.
|
Before Width: | Height: | Size: 411 B |
Binary file not shown.
|
Before Width: | Height: | Size: 396 B |
|
|
@ -1,118 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// ApkFileInfo describes one APK found in a chosen folder, for the batch
|
||||
// installer's checklist.
|
||||
type ApkFileInfo struct {
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
// ListApksInFolder scans a folder (non-recursive) for .apk files.
|
||||
func (a *App) ListApksInFolder(folderPath string) ([]ApkFileInfo, error) {
|
||||
entries, err := os.ReadDir(folderPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read folder: %w", err)
|
||||
}
|
||||
|
||||
var apks []ApkFileInfo
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.EqualFold(filepath.Ext(e.Name()), ".apk") {
|
||||
continue
|
||||
}
|
||||
info, err := e.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
apks = append(apks, ApkFileInfo{
|
||||
Path: filepath.Join(folderPath, e.Name()),
|
||||
Name: e.Name(),
|
||||
Size: info.Size(),
|
||||
})
|
||||
}
|
||||
sort.Slice(apks, func(i, j int) bool { return apks[i].Name < apks[j].Name })
|
||||
return apks, nil
|
||||
}
|
||||
|
||||
// StatApkFiles builds ApkFileInfo entries for explicitly chosen APK paths
|
||||
// (from the multi-file picker), so manual selection and folder scans feed the
|
||||
// same checklist shape. Unreadable paths are silently skipped.
|
||||
func (a *App) StatApkFiles(paths []string) ([]ApkFileInfo, error) {
|
||||
out := make([]ApkFileInfo, 0, len(paths))
|
||||
for _, p := range paths {
|
||||
info, err := os.Stat(p)
|
||||
if err != nil || info.IsDir() {
|
||||
continue
|
||||
}
|
||||
out = append(out, ApkFileInfo{Path: p, Name: info.Name(), Size: info.Size()})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// InstallApksWithProgress installs each APK in order (adb install -r),
|
||||
// emitting apkinstall:progress events so the UI can show a live per-file
|
||||
// checklist rather than one opaque spinner. Cancellable via CancelOperation.
|
||||
func (a *App) InstallApksWithProgress(paths []string) (string, error) {
|
||||
if len(paths) == 0 {
|
||||
return "", fmt.Errorf("no APKs selected")
|
||||
}
|
||||
|
||||
ctx, cancel := a.beginCancellableOp(0)
|
||||
defer cancel()
|
||||
|
||||
total := len(paths)
|
||||
var ok, fail int
|
||||
var details strings.Builder
|
||||
|
||||
for i, p := range paths {
|
||||
name := baseName(p)
|
||||
// Keyed by full path (not just fileName) - two selected APKs from
|
||||
// different folders can share a filename, and the path is what's
|
||||
// actually unique in the frontend's checklist.
|
||||
runtime.EventsEmit(a.ctx, "apkinstall:progress", map[string]interface{}{
|
||||
"current": i + 1, "total": total, "path": p, "fileName": name, "status": "installing",
|
||||
})
|
||||
|
||||
output, err := a.runCommandContext(ctx, "adb", "install", "-r", p)
|
||||
status, msg := "success", ""
|
||||
switch {
|
||||
case err != nil:
|
||||
status = "failed"
|
||||
msg = err.Error()
|
||||
case strings.Contains(output, "Failure"):
|
||||
status = "failed"
|
||||
msg = strings.TrimSpace(output)
|
||||
}
|
||||
|
||||
if status == "success" {
|
||||
ok++
|
||||
} else {
|
||||
fail++
|
||||
details.WriteString(fmt.Sprintf("• %s: %s\n", name, msg))
|
||||
}
|
||||
runtime.EventsEmit(a.ctx, "apkinstall:progress", map[string]interface{}{
|
||||
"current": i + 1, "total": total, "path": p, "fileName": name, "status": status, "message": msg,
|
||||
})
|
||||
|
||||
if strings.Contains(msg, "cancelled") {
|
||||
break
|
||||
}
|
||||
}
|
||||
runtime.EventsEmit(a.ctx, "apkinstall:done", nil)
|
||||
|
||||
summary := fmt.Sprintf("Installed %d of %d APK(s).", ok, total)
|
||||
if fail > 0 {
|
||||
summary += fmt.Sprintf(" Failed: %d\n%s", fail, details.String())
|
||||
}
|
||||
return summary, nil
|
||||
}
|
||||
|
|
@ -78,33 +78,6 @@ func (a *App) SelectDirectoryForPull() (string, error) {
|
|||
return path, err
|
||||
}
|
||||
|
||||
// SelectApkFolder opens a native directory picker for a folder of APKs to
|
||||
// batch-install.
|
||||
func (a *App) SelectApkFolder() (string, error) {
|
||||
path, err := zenity.SelectFile(
|
||||
zenity.Title("Select a folder of APKs"),
|
||||
zenity.Directory(),
|
||||
)
|
||||
if err == zenity.ErrCanceled {
|
||||
return "", nil
|
||||
}
|
||||
return path, err
|
||||
}
|
||||
|
||||
// SelectMultipleApkFiles opens a native multi-file picker filtered to APKs.
|
||||
func (a *App) SelectMultipleApkFiles() ([]string, error) {
|
||||
paths, err := zenity.SelectFileMultiple(
|
||||
zenity.Title("Select APKs to install"),
|
||||
zenity.FileFilters{
|
||||
{Name: "APK files", Patterns: []string{"*.apk"}, CaseFold: true},
|
||||
},
|
||||
)
|
||||
if err == zenity.ErrCanceled {
|
||||
return nil, nil
|
||||
}
|
||||
return paths, err
|
||||
}
|
||||
|
||||
// SelectFileWithFilter opens a file picker with custom file type filters.
|
||||
func (a *App) SelectFileWithFilter(title string, patterns []string) (string, error) {
|
||||
filters := []zenity.FileFilter{
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
5044e767932c804d924f539d25c7ab46
|
||||
bfb47127747332de1e5119ef153cca76
|
||||
|
|
@ -5,12 +5,10 @@ import TitleBar from './components/layout/TitleBar'
|
|||
import DismissibleBanner from './components/DismissibleBanner'
|
||||
import LockGate from './components/LockGate'
|
||||
import DangerGate from './components/DangerGate'
|
||||
import QuitGate from './components/QuitGate'
|
||||
import ViewDashboard from './components/views/ViewDashboard'
|
||||
import ViewFiles from './components/views/ViewFiles'
|
||||
import ViewScreenMirror from './components/views/ViewScreenMirror'
|
||||
import ViewPackages from './components/views/ViewPackages'
|
||||
import ViewApkInstaller from './components/views/ViewApkInstaller'
|
||||
import ViewDebloater from './components/views/ViewDebloater'
|
||||
import ViewShell from './components/views/ViewShell'
|
||||
import ViewLogcat from './components/views/ViewLogcat'
|
||||
|
|
@ -57,7 +55,6 @@ export default function App() {
|
|||
case 'files': return <ViewFiles />
|
||||
case 'mirror': return <ViewScreenMirror />
|
||||
case 'packages': return <ViewPackages />
|
||||
case 'apkinstaller': return <ViewApkInstaller />
|
||||
case 'debloater': return <ViewDebloater />
|
||||
case 'shell': return <ViewShell />
|
||||
case 'logcat': return <ViewLogcat />
|
||||
|
|
@ -91,7 +88,6 @@ export default function App() {
|
|||
return (
|
||||
<div className="flex flex-col h-full bg-bg-base overflow-hidden rounded-[10px]">
|
||||
<DangerGate />
|
||||
<QuitGate />
|
||||
<TitleBar />
|
||||
<div className={`flex-1 flex overflow-hidden ${sidebarPos === 'left' ? 'flex-row' : 'flex-col'}`}>
|
||||
{sidebarPos !== 'bottom' && sidebar}
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,93 +0,0 @@
|
|||
Copyright 2020 The DotGothic16 Project Authors (https://github.com/fontworks-fonts/DotGothic16)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
Copyright 2026 The Geist Project Authors (https://github.com/vercel/geist-font)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
Copyright (c) 2017 by TAE System & Typefaces Co.. All rights reserved.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
Copyright 2018 The Orbitron Project Authors (https://github.com/theleagueof/orbitron), with Reserved Font Name: "Orbitron"
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
Copyright 2017 The Playfair Display Project Authors (https://github.com/clauseggers/Playfair-Display), with Reserved Font Name "Playfair Display"
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
Copyright 2012 The Press Start 2P Project Authors (cody@zone38.net), with Reserved Font Name "Press Start 2P".
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
Copyright 2020 The Space Grotesk Project Authors (https://github.com/floriankarsten/space-grotesk)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
Copyright 2011, The VT323 Project Authors (peter.hull@oikoi.com)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { LogOut, Minus, X } from 'lucide-react'
|
||||
import { HideWindow, QuitApp, TrayAvailable } from '../lib/wails'
|
||||
import { _registerQuitHost } from '../lib/quitgate'
|
||||
|
||||
// Modal shown when the title bar's close button is clicked. Mirrors
|
||||
// Frequency's close dialog: offer Minimize-to-tray (default) alongside Quit,
|
||||
// rather than one silently winning. "Minimize to tray" only shows up if a
|
||||
// tray icon actually registered (TrayAvailable) - otherwise there'd be no way
|
||||
// back to the window.
|
||||
export default function QuitGate() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [trayOk, setTrayOk] = useState(false)
|
||||
|
||||
useEffect(() => _registerQuitHost(() => {
|
||||
TrayAvailable().then(setTrayOk).catch(() => setTrayOk(false))
|
||||
setOpen(true)
|
||||
}), [])
|
||||
|
||||
if (!open) return null
|
||||
|
||||
const close = () => setOpen(false)
|
||||
const minimize = () => { close(); HideWindow() }
|
||||
const quit = () => { close(); QuitApp() }
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
||||
onMouseDown={e => { if (e.target === e.currentTarget) close() }}
|
||||
>
|
||||
<div className="card p-5 w-80 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm font-medium text-text-primary">Close ATK?</p>
|
||||
<button onClick={close} className="btn-ghost text-xs p-1"><X size={14} /></button>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted">
|
||||
{trayOk
|
||||
? 'ATK can keep running in the background and stay reachable from the tray, or quit completely.'
|
||||
: 'No tray icon is available on this system, so closing quits ATK completely.'}
|
||||
</p>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<button onClick={close} className="btn-ghost text-xs">Cancel</button>
|
||||
{trayOk && (
|
||||
<button onClick={minimize} className="btn-ghost text-xs flex items-center gap-1.5">
|
||||
<Minus size={12} /> Minimize to tray
|
||||
</button>
|
||||
)}
|
||||
<button onClick={quit} className="btn-danger text-xs flex items-center gap-1.5">
|
||||
<LogOut size={12} /> Quit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import {
|
||||
LayoutDashboard, FolderOpen, Package, PackagePlus, Terminal,
|
||||
LayoutDashboard, FolderOpen, Package, Terminal,
|
||||
Zap, Wrench, Settings, Shield,
|
||||
ScrollText, Search, Lock, Archive, SlidersHorizontal, ScanSearch, MonitorSmartphone, Rocket, HardDriveDownload
|
||||
} from 'lucide-react'
|
||||
|
|
@ -22,7 +22,6 @@ const navItems: NavItem[] = [
|
|||
{ view: 'files', icon: <FolderOpen size={17} />, label: 'Files' },
|
||||
{ view: 'mirror', icon: <MonitorSmartphone size={17} />, label: 'Screen Mirror' },
|
||||
{ view: 'packages', icon: <Package size={17} />, label: 'Packages' },
|
||||
{ view: 'apkinstaller', icon: <PackagePlus size={17} />, label: 'APK Installer' },
|
||||
{ view: 'debloater', icon: <Shield size={17} />, label: 'Debloater' },
|
||||
{ view: 'shell', icon: <Terminal size={17} />, label: 'Shell' },
|
||||
{ view: 'logcat', icon: <ScrollText size={17} />, label: 'Logcat', dividerBefore: true },
|
||||
|
|
@ -37,10 +36,6 @@ const navItems: NavItem[] = [
|
|||
{ view: 'gsiloader', icon: <HardDriveDownload size={17} />, label: 'GSI Loader' },
|
||||
]
|
||||
|
||||
// Floor for progressive label shrinking in the horizontal sidebar - below
|
||||
// this, text becomes illegible, so labels hide outright instead.
|
||||
const MIN_LABEL_SCALE = 0.65
|
||||
|
||||
interface DragProps {
|
||||
onDragStart: (e: React.DragEvent) => void
|
||||
onDragOver: (e: React.DragEvent) => void
|
||||
|
|
@ -56,19 +51,6 @@ export default function Sidebar({ activeView, onViewChange, position, showLabels
|
|||
const [hidden, setHidden] = useState<string[]>(getHiddenViews())
|
||||
useEffect(() => onHiddenViewsChange(setHidden), [])
|
||||
|
||||
// When horizontal (top/bottom sidebar), labels can make the row wider than
|
||||
// the window - rather than forcing a horizontal scrollbar, shrink label text
|
||||
// progressively as space gets tight (labelScale 1 -> MIN_LABEL_SCALE), and
|
||||
// only hide labels outright once even minimum-size text wouldn't fit.
|
||||
// `measureRef` is an offscreen clone always rendered WITH full-size labels
|
||||
// at natural width; comparing its scrollWidth against the real nav's
|
||||
// clientWidth (which itself doesn't change as labelScale changes) avoids
|
||||
// the flip-flop you'd get measuring the visible, already-shrunk row.
|
||||
const navRef = useRef<HTMLElement>(null)
|
||||
const measureRef = useRef<HTMLDivElement>(null)
|
||||
const [labelScale, setLabelScale] = useState(1)
|
||||
const effectiveShowLabels = showLabels && (!horizontal || labelScale > 0)
|
||||
|
||||
// Drag-to-reorder (dock style). Saved order first, then any new defaults.
|
||||
const [order, setOrder] = useState<string[]>(getNavOrder())
|
||||
const dragRef = useRef<string | null>(null)
|
||||
|
|
@ -88,21 +70,6 @@ export default function Sidebar({ activeView, onViewChange, position, showLabels
|
|||
|
||||
const visibleItems = ordered.filter(i => !hidden.includes(i.view))
|
||||
|
||||
useEffect(() => {
|
||||
if (!horizontal || !showLabels) { setLabelScale(1); return }
|
||||
const nav = navRef.current
|
||||
const measure = measureRef.current
|
||||
if (!nav || !measure) return
|
||||
const check = () => {
|
||||
const ratio = measure.scrollWidth > 0 ? nav.clientWidth / measure.scrollWidth : 1
|
||||
setLabelScale(ratio >= 1 ? 1 : ratio >= MIN_LABEL_SCALE ? ratio : 0)
|
||||
}
|
||||
check()
|
||||
const ro = new ResizeObserver(check)
|
||||
ro.observe(nav)
|
||||
return () => ro.disconnect()
|
||||
}, [horizontal, showLabels, visibleItems])
|
||||
|
||||
const handleDrop = (target: string) => {
|
||||
const from = dragRef.current
|
||||
dragRef.current = null
|
||||
|
|
@ -122,8 +89,8 @@ export default function Sidebar({ activeView, onViewChange, position, showLabels
|
|||
position === 'left' ? 'border-r' : position === 'top' ? 'border-b' : 'border-t'
|
||||
|
||||
const asideCls = horizontal
|
||||
? `${effectiveShowLabels ? 'h-[68px]' : 'h-[52px]'} w-full flex flex-row items-center bg-bg-surface ${edgeBorder} border-bg-border shrink-0`
|
||||
: `${effectiveShowLabels ? 'w-[84px]' : 'w-[52px]'} flex flex-col bg-bg-surface ${edgeBorder} border-bg-border shrink-0`
|
||||
? `${showLabels ? 'h-[68px]' : 'h-[52px]'} w-full flex flex-row items-center bg-bg-surface ${edgeBorder} border-bg-border shrink-0`
|
||||
: `${showLabels ? 'w-[84px]' : 'w-[52px]'} flex flex-col bg-bg-surface ${edgeBorder} border-bg-border shrink-0`
|
||||
|
||||
const navCls = horizontal
|
||||
? 'flex-1 flex flex-row items-center justify-center gap-0.5 px-1 overflow-x-auto'
|
||||
|
|
@ -135,19 +102,13 @@ export default function Sidebar({ activeView, onViewChange, position, showLabels
|
|||
? 'px-1 h-full flex items-center border-l border-bg-border shrink-0'
|
||||
: 'p-1 pb-1.5 border-t border-bg-border shrink-0'
|
||||
|
||||
const btnSizing = !effectiveShowLabels
|
||||
const btnSizing = !showLabels
|
||||
? 'w-8 h-8'
|
||||
: horizontal
|
||||
? 'flex-col gap-1 px-2 py-1.5 h-full justify-center'
|
||||
? 'flex-col gap-1 px-2 py-1.5 min-w-[3.25rem] h-full justify-center'
|
||||
: 'flex-col gap-1 px-1 py-1.5 w-full'
|
||||
|
||||
const labelCls = `leading-tight text-center ${horizontal ? 'whitespace-nowrap' : ''}`
|
||||
|
||||
// Horizontal-only: button width and label font-size both track labelScale,
|
||||
// so shrinking text actually reclaims row space instead of just looking smaller.
|
||||
const btnStyle: React.CSSProperties | undefined =
|
||||
horizontal && effectiveShowLabels ? { minWidth: `${3.25 * labelScale}rem` } : undefined
|
||||
const labelStyle: React.CSSProperties = { fontSize: `${10 * labelScale}px` }
|
||||
const labelCls = `text-[10px] leading-tight text-center ${horizontal ? 'whitespace-nowrap' : ''}`
|
||||
|
||||
const renderButton = (view: View | 'settings', icon: React.ReactNode, label: string, drag?: DragProps) => {
|
||||
const active = activeView === view
|
||||
|
|
@ -161,7 +122,6 @@ export default function Sidebar({ activeView, onViewChange, position, showLabels
|
|||
onDragEnd={drag?.onDragEnd}
|
||||
onClick={() => onViewChange(view as View)}
|
||||
title={label}
|
||||
style={btnStyle}
|
||||
className={`
|
||||
flex items-center justify-center rounded transition-all duration-150 relative
|
||||
${btnSizing}
|
||||
|
|
@ -174,7 +134,7 @@ export default function Sidebar({ activeView, onViewChange, position, showLabels
|
|||
`}
|
||||
>
|
||||
{icon}
|
||||
{effectiveShowLabels && <span className={labelCls} style={labelStyle}>{label}</span>}
|
||||
{showLabels && <span className={labelCls}>{label}</span>}
|
||||
{active && (
|
||||
horizontal
|
||||
? <span className="absolute bottom-0 left-1/2 -translate-x-1/2 h-0.5 w-5 bg-accent-green rounded-t" />
|
||||
|
|
@ -186,25 +146,7 @@ export default function Sidebar({ activeView, onViewChange, position, showLabels
|
|||
|
||||
return (
|
||||
<aside className={asideCls}>
|
||||
{horizontal && showLabels && (
|
||||
<div ref={measureRef} aria-hidden="true" className="fixed -top-[9999px] left-0 flex flex-row items-center gap-0.5 px-1 pointer-events-none">
|
||||
{visibleItems.map(({ view, icon, label, dividerBefore }, idx) => (
|
||||
<div key={view} className="flex items-center">
|
||||
{dividerBefore && idx > 0 && <div className={dividerCls} />}
|
||||
<div className="flex flex-col items-center justify-center gap-1 px-2 py-1.5 min-w-[3.25rem] h-[68px]">
|
||||
{icon}
|
||||
<span className="text-[10px] leading-tight text-center whitespace-nowrap">{label}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="w-px h-7 bg-bg-border mx-1" />
|
||||
<div className="flex flex-col items-center justify-center gap-1 px-2 py-1.5 min-w-[3.25rem] h-[68px]">
|
||||
<Settings size={17} />
|
||||
<span className="text-[10px] leading-tight text-center whitespace-nowrap">Settings</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<nav ref={navRef} className={navCls}>
|
||||
<nav className={navCls}>
|
||||
{visibleItems.map(({ view, icon, label, dividerBefore }, idx) => (
|
||||
<div key={view} className={horizontal ? 'flex items-center' : undefined}>
|
||||
{dividerBefore && idx > 0 && <div className={dividerCls} />}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
// the drag region via Wails' `--wails-draggable:drag` CSS hint, plus macOS-style
|
||||
// traffic-light controls tinted in Catppuccin pastels. No app name by design.
|
||||
|
||||
import { requestQuitConfirm } from '../../lib/quitgate'
|
||||
|
||||
// Runtime is injected by Wails on window['runtime'] (same access pattern as
|
||||
// ViewLogcat.tsx); guarded with ?. so a browser dev session won't crash.
|
||||
const rt = () => (window as any)['runtime']
|
||||
|
|
@ -33,7 +31,7 @@ export default function TitleBar() {
|
|||
<div className="flex items-center gap-2 ml-auto" style={{ '--wails-draggable': 'no-drag' } as React.CSSProperties}>
|
||||
<TrafficLight color="#e5c890" hover="hover:brightness-110" title="Minimise" onClick={() => rt()?.WindowMinimise?.()} />
|
||||
<TrafficLight color="#a6d189" hover="hover:brightness-110" title="Maximise" onClick={() => rt()?.WindowToggleMaximise?.()} />
|
||||
<TrafficLight color="#e78284" hover="hover:brightness-110" title="Close" onClick={requestQuitConfirm} />
|
||||
<TrafficLight color="#e78284" hover="hover:brightness-110" title="Close" onClick={() => rt()?.Quit?.()} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,251 +0,0 @@
|
|||
import { useState, useEffect, useMemo, useCallback } from 'react'
|
||||
import {
|
||||
PackagePlus, FolderOpen, FilePlus, Trash2, Search, Download,
|
||||
CheckCircle2, XCircle, Loader2, X, Ban
|
||||
} from 'lucide-react'
|
||||
import {
|
||||
SelectApkFolder, SelectMultipleApkFiles, ListApksInFolder, StatApkFiles,
|
||||
InstallApksWithProgress, CancelOperation
|
||||
} from '../../lib/wails'
|
||||
import { notify } from '../../lib/notify'
|
||||
import type { ApkFileInfo } from '../../lib/types'
|
||||
|
||||
const rt = () => (window as any)['runtime']
|
||||
|
||||
type RowStatus = 'idle' | 'pending' | 'installing' | 'success' | 'failed'
|
||||
interface Progress { status: RowStatus; message?: string }
|
||||
|
||||
function formatSize(bytes: number): string {
|
||||
if (bytes >= 1048576) return `${(bytes / 1048576).toFixed(1)} MB`
|
||||
if (bytes >= 1024) return `${(bytes / 1024).toFixed(0)} KB`
|
||||
return `${bytes} B`
|
||||
}
|
||||
|
||||
export default function ViewApkInstaller() {
|
||||
const [apks, setApks] = useState<ApkFileInfo[]>([])
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set())
|
||||
const [progress, setProgress] = useState<Record<string, Progress>>({})
|
||||
const [installing, setInstalling] = useState(false)
|
||||
const [search, setSearch] = useState('')
|
||||
|
||||
// Live per-file install status, driven by backend apkinstall:* events.
|
||||
useEffect(() => {
|
||||
const onProgress = (p: { current: number; total: number; path: string; status: RowStatus; message?: string }) => {
|
||||
setProgress(prev => ({ ...prev, [p.path]: { status: p.status, message: p.message } }))
|
||||
}
|
||||
const onDone = () => setInstalling(false)
|
||||
const off1 = rt()?.EventsOn?.('apkinstall:progress', onProgress)
|
||||
const off2 = rt()?.EventsOn?.('apkinstall:done', onDone)
|
||||
return () => { off1?.(); off2?.() }
|
||||
}, [])
|
||||
|
||||
const mergeApks = useCallback((incoming: ApkFileInfo[]) => {
|
||||
if (incoming.length === 0) return
|
||||
setApks(prev => {
|
||||
const byPath = new Map(prev.map(a => [a.path, a]))
|
||||
for (const a of incoming) byPath.set(a.path, a)
|
||||
return [...byPath.values()].sort((a, b) => a.name.localeCompare(b.name))
|
||||
})
|
||||
setSelected(prev => {
|
||||
const next = new Set(prev)
|
||||
for (const a of incoming) next.add(a.path)
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
const addFolder = async () => {
|
||||
try {
|
||||
const folder = await SelectApkFolder()
|
||||
if (!folder) return
|
||||
const found = await ListApksInFolder(folder)
|
||||
if (!found || found.length === 0) { notify.error('No APKs found in that folder'); return }
|
||||
mergeApks(found)
|
||||
} catch (e: any) {
|
||||
notify.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
const addFiles = async () => {
|
||||
try {
|
||||
const paths = await SelectMultipleApkFiles()
|
||||
if (!paths || paths.length === 0) return
|
||||
const found = await StatApkFiles(paths)
|
||||
mergeApks(found || [])
|
||||
} catch (e: any) {
|
||||
notify.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
const removeOne = (path: string) => {
|
||||
setApks(prev => prev.filter(a => a.path !== path))
|
||||
setSelected(prev => { const next = new Set(prev); next.delete(path); return next })
|
||||
setProgress(prev => { const { [path]: _drop, ...rest } = prev; return rest })
|
||||
}
|
||||
|
||||
const clearAll = () => { setApks([]); setSelected(new Set()); setProgress({}) }
|
||||
|
||||
const filtered = useMemo(() =>
|
||||
apks.filter(a => a.name.toLowerCase().includes(search.toLowerCase())),
|
||||
[apks, search]
|
||||
)
|
||||
|
||||
const toggleSelect = (path: string) => setSelected(prev => {
|
||||
const next = new Set(prev)
|
||||
next.has(path) ? next.delete(path) : next.add(path)
|
||||
return next
|
||||
})
|
||||
|
||||
const selectAll = () => {
|
||||
if (selected.size === filtered.length) {
|
||||
setSelected(new Set())
|
||||
} else {
|
||||
setSelected(new Set(filtered.map(a => a.path)))
|
||||
}
|
||||
}
|
||||
|
||||
const install = async () => {
|
||||
if (selected.size === 0) { notify.error('Select at least one APK'); return }
|
||||
const targets = apks.filter(a => selected.has(a.path))
|
||||
setInstalling(true)
|
||||
const reset: Record<string, Progress> = {}
|
||||
for (const t of targets) reset[t.path] = { status: 'pending' }
|
||||
setProgress(prev => ({ ...prev, ...reset }))
|
||||
try {
|
||||
const summary = await InstallApksWithProgress(targets.map(t => t.path))
|
||||
notify.success(summary)
|
||||
} catch (e: any) {
|
||||
notify.error(e)
|
||||
} finally {
|
||||
setInstalling(false)
|
||||
}
|
||||
}
|
||||
|
||||
const cancel = async () => {
|
||||
try { await CancelOperation() } catch { /* best effort */ }
|
||||
}
|
||||
|
||||
const statusIcon = (status: RowStatus | undefined) => {
|
||||
switch (status) {
|
||||
case 'installing': return <Loader2 size={13} className="animate-spin text-accent-green" />
|
||||
case 'success': return <CheckCircle2 size={13} className="text-accent-green" />
|
||||
case 'failed': return <XCircle size={13} className="text-danger" />
|
||||
case 'pending': return <span className="w-[13px] h-[13px] rounded-full border border-text-muted inline-block" />
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Toolbar */}
|
||||
<div className="border-b border-bg-border px-4 py-2 flex items-center gap-2 shrink-0 flex-wrap">
|
||||
<PackagePlus size={14} className="text-accent-green shrink-0" />
|
||||
<span className="text-xs text-text-secondary">APK Installer</span>
|
||||
|
||||
<button onClick={addFolder} disabled={installing} className="btn-ghost text-xs">
|
||||
<FolderOpen size={13} /> Add Folder
|
||||
</button>
|
||||
<button onClick={addFiles} disabled={installing} className="btn-ghost text-xs">
|
||||
<FilePlus size={13} /> Add APKs
|
||||
</button>
|
||||
{apks.length > 0 && (
|
||||
<button onClick={clearAll} disabled={installing} className="btn-ghost text-xs">
|
||||
<Trash2 size={13} /> Clear
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="relative flex-1 min-w-[160px]">
|
||||
<Search size={13} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-muted" />
|
||||
<input
|
||||
className="input pl-8 text-xs"
|
||||
placeholder="Filter list..."
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-px h-5 bg-bg-border" />
|
||||
|
||||
{installing ? (
|
||||
<button onClick={cancel} className="btn-danger text-xs">
|
||||
<Ban size={13} /> Cancel
|
||||
</button>
|
||||
) : (
|
||||
<button onClick={install} disabled={selected.size === 0} className="btn-primary text-xs">
|
||||
<Download size={13} /> Install Selected ({selected.size})
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* List header */}
|
||||
{apks.length > 0 && (
|
||||
<div className="grid grid-cols-[24px_20px_1fr_80px_60px] gap-2 px-4 py-1.5 border-b border-bg-border text-text-muted text-xs shrink-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filtered.length > 0 && selected.size === filtered.length}
|
||||
onChange={selectAll}
|
||||
disabled={installing}
|
||||
className="accent-accent-green"
|
||||
/>
|
||||
<span />
|
||||
<span>APK</span>
|
||||
<span>Size</span>
|
||||
<span></span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* List */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
{apks.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center h-full gap-3 text-text-muted">
|
||||
<PackagePlus size={28} className="opacity-30" />
|
||||
<p className="text-sm">Add a folder of APKs, or pick files individually</p>
|
||||
</div>
|
||||
)}
|
||||
{filtered.map(a => {
|
||||
const prog = progress[a.path]
|
||||
return (
|
||||
<div
|
||||
key={a.path}
|
||||
className={`
|
||||
grid grid-cols-[24px_20px_1fr_80px_60px] gap-2 px-4 py-2
|
||||
border-b border-bg-border/50 items-center text-xs
|
||||
hover:bg-bg-raised transition-colors
|
||||
${selected.has(a.path) ? 'bg-accent-green/5' : ''}
|
||||
`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected.has(a.path)}
|
||||
onChange={() => toggleSelect(a.path)}
|
||||
disabled={installing}
|
||||
className="accent-accent-green"
|
||||
/>
|
||||
<span title={prog?.message}>{statusIcon(prog?.status)}</span>
|
||||
<div className="min-w-0">
|
||||
<p className="mono text-text-secondary truncate">{a.name}</p>
|
||||
{prog?.status === 'failed' && prog.message && (
|
||||
<p className="text-danger text-[11px] truncate" title={prog.message}>{prog.message}</p>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-text-muted">{formatSize(a.size)}</span>
|
||||
<button
|
||||
onClick={() => removeOne(a.path)}
|
||||
disabled={installing}
|
||||
className="btn-ghost text-xs py-0.5 px-1.5 justify-self-start"
|
||||
title="Remove from list"
|
||||
>
|
||||
<X size={12} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Status bar */}
|
||||
<div className="border-t border-bg-border px-4 py-1.5 flex items-center justify-between text-xs text-text-muted shrink-0">
|
||||
<span>{filtered.length} APK(s){search ? ` matching "${search}"` : ''}</span>
|
||||
{selected.size > 0 && <span>{selected.size} selected</span>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -10,11 +10,8 @@ const GRADE_COLOR: Record<string, string> = {
|
|||
A: 'text-accent-green', B: 'text-accent-green', C: 'text-warn', D: 'text-warn', F: 'text-danger',
|
||||
}
|
||||
|
||||
type StateFilter = 'all' | 'enabled' | 'disabled' | 'uninstalled'
|
||||
|
||||
export default function ViewAppInspect() {
|
||||
const [search, setSearch] = useState('')
|
||||
const [stateFilter, setStateFilter] = useState<StateFilter>('all')
|
||||
const [packages, setPackages] = useState<PackageInfo[]>([])
|
||||
const [pkgsLoaded, setPkgsLoaded] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
|
@ -104,16 +101,9 @@ export default function ViewAppInspect() {
|
|||
}
|
||||
}
|
||||
|
||||
const filtered = packages
|
||||
.filter(p => p.packageName.toLowerCase().includes(search.toLowerCase()))
|
||||
.filter(p => {
|
||||
switch (stateFilter) {
|
||||
case 'enabled': return p.isInstalled && p.isEnabled
|
||||
case 'disabled': return p.isInstalled && !p.isEnabled
|
||||
case 'uninstalled': return !p.isInstalled
|
||||
default: return true
|
||||
}
|
||||
})
|
||||
const filtered = packages.filter(p =>
|
||||
p.packageName.toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
|
||||
const tabs = [
|
||||
{ id: 'overview', label: 'Overview', icon: <Package size={12} /> },
|
||||
|
|
@ -145,17 +135,6 @@ export default function ViewAppInspect() {
|
|||
<button onClick={() => inspect(search)} disabled={!search || loading} className="btn-primary text-xs w-full justify-center">
|
||||
{loading ? 'Inspecting...' : 'Inspect'}
|
||||
</button>
|
||||
<select
|
||||
className="input text-xs w-full"
|
||||
value={stateFilter}
|
||||
onChange={e => setStateFilter(e.target.value as StateFilter)}
|
||||
title="Filter by state"
|
||||
>
|
||||
<option value="all">All states</option>
|
||||
<option value="enabled">Enabled</option>
|
||||
<option value="disabled">Disabled</option>
|
||||
<option value="uninstalled">Uninstalled</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto">
|
||||
|
|
@ -166,9 +145,7 @@ export default function ViewAppInspect() {
|
|||
className="w-full text-left px-3 py-2 text-xs text-text-secondary hover:bg-bg-raised hover:text-text-primary transition-colors border-b border-bg-border/30"
|
||||
>
|
||||
<p className="truncate mono">{p.packageName}</p>
|
||||
<p className={!p.isInstalled ? 'text-text-muted' : p.isEnabled ? 'text-accent-green' : 'text-danger'}>
|
||||
{!p.isInstalled ? 'uninstalled' : p.isEnabled ? 'enabled' : 'disabled'}
|
||||
</p>
|
||||
<p className={p.isEnabled ? 'text-accent-green' : 'text-danger'}>{p.isEnabled ? 'enabled' : 'disabled'}</p>
|
||||
</button>
|
||||
))}
|
||||
{!pkgsLoaded && (
|
||||
|
|
|
|||
|
|
@ -31,14 +31,7 @@ interface Row {
|
|||
category: string
|
||||
deps?: string[]
|
||||
neededBy?: string[]
|
||||
// Physically present on the system image (pm list packages -u), regardless
|
||||
// of per-user install state. False only for packages that have never been
|
||||
// seen on this device at all (UAD-database-only entries).
|
||||
onDevice: boolean
|
||||
// onDevice but uninstalled for user 0 - e.g. via Canta/Shizuku, which runs
|
||||
// `pm uninstall --user 0`. The APK is still on disk; it just isn't active
|
||||
// for this user until restored.
|
||||
isUninstalledForUser: boolean
|
||||
isInstalled: boolean
|
||||
isDisabled: boolean
|
||||
}
|
||||
|
||||
|
|
@ -63,8 +56,7 @@ export default function ViewDebloater() {
|
|||
const [mfrFilter, setMfrFilter] = useState('all')
|
||||
const [openCats, setOpenCats] = useState<Set<string>>(new Set())
|
||||
const [operating, setOperating] = useState(false)
|
||||
const [stateFilter, setStateFilter] = useState<'installed' | 'enabled' | 'disabled' | 'uninstalled' | 'notinstalled' | 'all'>('installed')
|
||||
const [uninstalledForUser, setUninstalledForUser] = useState<Set<string>>(new Set())
|
||||
const [stateFilter, setStateFilter] = useState<'installed' | 'enabled' | 'disabled' | 'notinstalled' | 'all'>('installed')
|
||||
|
||||
// pkg -> UAD database entry (with its category). Built once; first match wins.
|
||||
const dbIndex = useMemo(() => {
|
||||
|
|
@ -75,7 +67,7 @@ export default function ViewDebloater() {
|
|||
m.set(p.pkg, {
|
||||
pkg: p.pkg, label: p.label, description: p.description, safety: p.safety,
|
||||
category: cat.name, deps: p.deps, neededBy: p.neededBy,
|
||||
onDevice: false, isUninstalledForUser: false, isDisabled: false,
|
||||
isInstalled: false, isDisabled: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -87,16 +79,12 @@ export default function ViewDebloater() {
|
|||
setLoading(true)
|
||||
setInstalled(new Set())
|
||||
setDisabled(new Set())
|
||||
setUninstalledForUser(new Set())
|
||||
setSelected(new Set())
|
||||
try {
|
||||
// 'all' now includes packages uninstalled-for-user (pm list packages -u) -
|
||||
// still physically on the device, just not active for user 0.
|
||||
const pkgs = await ListPackages('all')
|
||||
const names = new Set<string>((pkgs || []).map((p: PackageInfo) => p.packageName))
|
||||
setInstalled(names)
|
||||
setDisabled(new Set<string>((pkgs || []).filter((p: PackageInfo) => p.isInstalled && !p.isEnabled).map((p: PackageInfo) => p.packageName)))
|
||||
setUninstalledForUser(new Set<string>((pkgs || []).filter((p: PackageInfo) => !p.isInstalled).map((p: PackageInfo) => p.packageName)))
|
||||
setDisabled(new Set<string>((pkgs || []).filter((p: PackageInfo) => !p.isEnabled).map((p: PackageInfo) => p.packageName)))
|
||||
// Auto-open every category that has at least one package on the device.
|
||||
const withInstalled = new Set<string>()
|
||||
names.forEach(name => withInstalled.add(dbIndex.get(name)?.category ?? UNCATEGORIZED))
|
||||
|
|
@ -119,23 +107,22 @@ export default function ViewDebloater() {
|
|||
const rows: Row[] = []
|
||||
installed.forEach(name => {
|
||||
const e = dbIndex.get(name)
|
||||
const isUninstalledForUser = uninstalledForUser.has(name)
|
||||
if (e) {
|
||||
rows.push({ ...e, onDevice: true, isUninstalledForUser, isDisabled: disabled.has(name) })
|
||||
rows.push({ ...e, isInstalled: true, isDisabled: disabled.has(name) })
|
||||
} else {
|
||||
rows.push({
|
||||
pkg: name, label: shortLabel(name),
|
||||
description: 'Not in the debloat database — likely an OEM, carrier, or region-specific package. Safety unknown; research before removing.',
|
||||
safety: 'unknown', category: UNCATEGORIZED,
|
||||
onDevice: true, isUninstalledForUser, isDisabled: disabled.has(name),
|
||||
isInstalled: true, isDisabled: disabled.has(name),
|
||||
})
|
||||
}
|
||||
})
|
||||
dbIndex.forEach((e, pkg) => {
|
||||
if (!installed.has(pkg)) rows.push({ ...e, onDevice: false, isUninstalledForUser: false, isDisabled: false })
|
||||
if (!installed.has(pkg)) rows.push({ ...e, isInstalled: false, isDisabled: false })
|
||||
})
|
||||
return rows
|
||||
}, [installed, disabled, uninstalledForUser, dbIndex])
|
||||
}, [installed, disabled, dbIndex])
|
||||
|
||||
const visibleCategories = useMemo(() => {
|
||||
const q = search.toLowerCase()
|
||||
|
|
@ -144,11 +131,10 @@ export default function ViewDebloater() {
|
|||
if (mfrFilter !== 'all' && r.category !== mfrFilter) continue
|
||||
if (safetyFilter !== 'all' && r.safety !== safetyFilter) continue
|
||||
switch (stateFilter) {
|
||||
case 'installed': if (!r.onDevice) continue; break
|
||||
case 'enabled': if (!r.onDevice || r.isUninstalledForUser || r.isDisabled) continue; break
|
||||
case 'installed': if (!r.isInstalled) continue; break
|
||||
case 'enabled': if (!r.isInstalled || r.isDisabled) continue; break
|
||||
case 'disabled': if (!r.isDisabled) continue; break
|
||||
case 'uninstalled': if (!r.isUninstalledForUser) continue; break
|
||||
case 'notinstalled': if (r.onDevice) continue; break
|
||||
case 'notinstalled': if (r.isInstalled) continue; break
|
||||
// 'all' → no state restriction
|
||||
}
|
||||
if (q && !(r.pkg.toLowerCase().includes(q) || r.label.toLowerCase().includes(q) || r.description.toLowerCase().includes(q))) continue
|
||||
|
|
@ -257,8 +243,7 @@ export default function ViewDebloater() {
|
|||
<option value="installed">On device</option>
|
||||
<option value="enabled">Enabled</option>
|
||||
<option value="disabled">Disabled</option>
|
||||
<option value="uninstalled">Uninstalled (this user)</option>
|
||||
<option value="notinstalled">Not on device</option>
|
||||
<option value="notinstalled">Not installed</option>
|
||||
<option value="all">All</option>
|
||||
</select>
|
||||
|
||||
|
|
@ -347,7 +332,7 @@ export default function ViewDebloater() {
|
|||
)}
|
||||
|
||||
{!loading && visibleCategories.map(cat => {
|
||||
const installedCount = cat.packages.filter(p => p.onDevice).length
|
||||
const installedCount = cat.packages.filter(p => p.isInstalled).length
|
||||
const isOpen = openCats.has(cat.name)
|
||||
|
||||
return (
|
||||
|
|
@ -373,8 +358,7 @@ export default function ViewDebloater() {
|
|||
|
||||
{/* Packages */}
|
||||
{isOpen && cat.packages.map(p => {
|
||||
const isInst = p.onDevice
|
||||
const isUninstalledForUser = p.isUninstalledForUser
|
||||
const isInst = p.isInstalled
|
||||
const isDisabled = p.isDisabled
|
||||
const isSel = selected.has(p.pkg)
|
||||
const safety = SAFETY_CONFIG[p.safety]
|
||||
|
|
@ -404,8 +388,7 @@ export default function ViewDebloater() {
|
|||
{safety.icon} {safety.label}
|
||||
</span>
|
||||
{!isInst && <span className="badge-gray text-xs">not on device</span>}
|
||||
{isInst && isUninstalledForUser && <span className="badge-gray text-xs" title="Present on the system image but uninstalled for this user (e.g. via Canta/Shizuku)">uninstalled</span>}
|
||||
{isInst && !isUninstalledForUser && isDisabled && <span className="badge-yellow text-xs">disabled</span>}
|
||||
{isInst && isDisabled && <span className="badge-yellow text-xs">disabled</span>}
|
||||
{p.deps && p.deps.length > 0 && (
|
||||
<span className="badge-gray text-xs" title={`Depends on: ${p.deps.join(', ')}`}>has deps</span>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,20 @@
|
|||
import { useState, useCallback, useMemo } from 'react'
|
||||
import { Package, RefreshCw, Search, Download, Trash2, Power, PowerOff, RotateCcw, X } from 'lucide-react'
|
||||
import { Package, RefreshCw, Search, Download, Trash2, Power, PowerOff, X } from 'lucide-react'
|
||||
import {
|
||||
ListPackages, UninstallMultiplePackages, DisableMultiplePackages,
|
||||
EnableMultiplePackages, RestoreMultiplePackages, PullApk, ClearData, ForceStopPackage,
|
||||
SelectFileForInstall, InstallPackage,
|
||||
UninstallPackage, DisablePackage, EnablePackage, RestorePackage
|
||||
EnableMultiplePackages, PullApk, ClearData, ForceStopPackage,
|
||||
SelectFileForInstall, InstallPackage
|
||||
} from '../../lib/wails'
|
||||
import { notify } from '../../lib/notify'
|
||||
import { ensureDangerUnlocked } from '../../lib/applock'
|
||||
import type { PackageInfo } from '../../lib/types'
|
||||
|
||||
type Filter = 'all' | 'user' | 'system'
|
||||
type StateFilter = 'all' | 'enabled' | 'disabled' | 'uninstalled'
|
||||
|
||||
export default function ViewPackages() {
|
||||
const [packages, setPackages] = useState<PackageInfo[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [filter, setFilter] = useState<Filter>('user')
|
||||
const [stateFilter, setStateFilter] = useState<StateFilter>('all')
|
||||
const [search, setSearch] = useState('')
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set())
|
||||
const [activeCtx, setActiveCtx] = useState<string | null>(null)
|
||||
|
|
@ -39,16 +36,8 @@ export default function ViewPackages() {
|
|||
const filtered = useMemo(() =>
|
||||
packages
|
||||
.filter(p => p.packageName.toLowerCase().includes(search.toLowerCase()))
|
||||
.filter(p => {
|
||||
switch (stateFilter) {
|
||||
case 'enabled': return p.isInstalled && p.isEnabled
|
||||
case 'disabled': return p.isInstalled && !p.isEnabled
|
||||
case 'uninstalled': return !p.isInstalled
|
||||
default: return true
|
||||
}
|
||||
})
|
||||
.sort((a, b) => a.packageName.localeCompare(b.packageName)),
|
||||
[packages, search, stateFilter]
|
||||
[packages, search]
|
||||
)
|
||||
|
||||
const toggleSelect = (pkg: string) => {
|
||||
|
|
@ -131,67 +120,6 @@ export default function ViewPackages() {
|
|||
setActiveCtx(null)
|
||||
}
|
||||
|
||||
// Single-package state changes, mirroring the batch toolbar actions but
|
||||
// reachable per-row from the "..." menu without needing to select first.
|
||||
const handleUninstall = async (pkg: string) => {
|
||||
setActiveCtx(null)
|
||||
if (!confirm(`Uninstall ${pkg} for the current user?`)) return
|
||||
if (!(await ensureDangerUnlocked())) return
|
||||
const id = notify.loading(`Uninstalling ${pkg}...`)
|
||||
try {
|
||||
const out = await UninstallPackage(pkg)
|
||||
notify.dismiss(id)
|
||||
notify.success(out)
|
||||
load(filter)
|
||||
} catch (e: any) {
|
||||
notify.dismiss(id)
|
||||
notify.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDisable = async (pkg: string) => {
|
||||
setActiveCtx(null)
|
||||
if (!(await ensureDangerUnlocked())) return
|
||||
const id = notify.loading(`Disabling ${pkg}...`)
|
||||
try {
|
||||
const out = await DisablePackage(pkg)
|
||||
notify.dismiss(id)
|
||||
notify.success(out)
|
||||
load(filter)
|
||||
} catch (e: any) {
|
||||
notify.dismiss(id)
|
||||
notify.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEnable = async (pkg: string) => {
|
||||
setActiveCtx(null)
|
||||
const id = notify.loading(`Enabling ${pkg}...`)
|
||||
try {
|
||||
const out = await EnablePackage(pkg)
|
||||
notify.dismiss(id)
|
||||
notify.success(out)
|
||||
load(filter)
|
||||
} catch (e: any) {
|
||||
notify.dismiss(id)
|
||||
notify.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRestore = async (pkg: string) => {
|
||||
setActiveCtx(null)
|
||||
const id = notify.loading(`Restoring ${pkg}...`)
|
||||
try {
|
||||
const out = await RestorePackage(pkg)
|
||||
notify.dismiss(id)
|
||||
notify.success(out)
|
||||
load(filter)
|
||||
} catch (e: any) {
|
||||
notify.dismiss(id)
|
||||
notify.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Toolbar */}
|
||||
|
|
@ -226,18 +154,6 @@ export default function ViewPackages() {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<select
|
||||
className="input text-xs"
|
||||
value={stateFilter}
|
||||
onChange={e => setStateFilter(e.target.value as StateFilter)}
|
||||
title="Filter by state"
|
||||
>
|
||||
<option value="all">All states</option>
|
||||
<option value="enabled">Enabled</option>
|
||||
<option value="disabled">Disabled</option>
|
||||
<option value="uninstalled">Uninstalled</option>
|
||||
</select>
|
||||
|
||||
<div className="w-px h-5 bg-bg-border" />
|
||||
|
||||
<button onClick={handleInstall} className="btn-primary text-xs">
|
||||
|
|
@ -255,9 +171,6 @@ export default function ViewPackages() {
|
|||
<button onClick={() => batchOp('Enabling', EnableMultiplePackages)} className="btn-ghost text-xs">
|
||||
<Power size={13} /> Enable ({selected.size})
|
||||
</button>
|
||||
<button onClick={() => batchOp('Restoring', RestoreMultiplePackages)} className="btn-ghost text-xs text-accent-green">
|
||||
<RotateCcw size={13} /> Restore ({selected.size})
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -306,13 +219,9 @@ export default function ViewPackages() {
|
|||
/>
|
||||
<span className="mono text-text-secondary truncate">{pkg.packageName}</span>
|
||||
<span>
|
||||
{!pkg.isInstalled ? (
|
||||
<span className="badge-gray" title="Present on the system image but uninstalled for this user (e.g. via Canta/Shizuku)">uninstalled</span>
|
||||
) : (
|
||||
<span className={pkg.isEnabled ? 'badge-green' : 'badge-red'}>
|
||||
{pkg.isEnabled ? 'enabled' : 'disabled'}
|
||||
</span>
|
||||
)}
|
||||
<span className={pkg.isEnabled ? 'badge-green' : 'badge-red'}>
|
||||
{pkg.isEnabled ? 'enabled' : 'disabled'}
|
||||
</span>
|
||||
</span>
|
||||
<div className="relative">
|
||||
<button
|
||||
|
|
@ -323,42 +232,15 @@ export default function ViewPackages() {
|
|||
</button>
|
||||
{activeCtx === pkg.packageName && (
|
||||
<div className="absolute right-0 top-full mt-1 bg-bg-raised border border-bg-border rounded shadow-xl z-10 min-w-[160px]">
|
||||
{!pkg.isInstalled && (
|
||||
<>
|
||||
<button onClick={() => handleRestore(pkg.packageName)} className="flex items-center gap-2 w-full px-3 py-2 text-xs text-accent-green hover:bg-bg-surface">
|
||||
<RotateCcw size={12} /> Restore
|
||||
</button>
|
||||
<button onClick={() => handleDisable(pkg.packageName)} className="flex items-center gap-2 w-full px-3 py-2 text-xs text-warn hover:bg-bg-surface">
|
||||
<PowerOff size={12} /> Disable
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{pkg.isInstalled && (
|
||||
<>
|
||||
<button onClick={() => handlePullApk(pkg.packageName)} className="flex items-center gap-2 w-full px-3 py-2 text-xs text-text-secondary hover:text-text-primary hover:bg-bg-surface">
|
||||
<Download size={12} /> Pull APK
|
||||
</button>
|
||||
<button onClick={() => handleClearData(pkg.packageName)} className="flex items-center gap-2 w-full px-3 py-2 text-xs text-warn hover:bg-bg-surface">
|
||||
<X size={12} /> Clear Data
|
||||
</button>
|
||||
<button onClick={() => handleForceStop(pkg.packageName)} className="flex items-center gap-2 w-full px-3 py-2 text-xs text-danger hover:bg-bg-surface">
|
||||
<PowerOff size={12} /> Force Stop
|
||||
</button>
|
||||
<div className="h-px bg-bg-border my-1" />
|
||||
{pkg.isEnabled ? (
|
||||
<button onClick={() => handleDisable(pkg.packageName)} className="flex items-center gap-2 w-full px-3 py-2 text-xs text-warn hover:bg-bg-surface">
|
||||
<PowerOff size={12} /> Disable
|
||||
</button>
|
||||
) : (
|
||||
<button onClick={() => handleEnable(pkg.packageName)} className="flex items-center gap-2 w-full px-3 py-2 text-xs text-accent-green hover:bg-bg-surface">
|
||||
<Power size={12} /> Enable
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => handleUninstall(pkg.packageName)} className="flex items-center gap-2 w-full px-3 py-2 text-xs text-danger hover:bg-bg-surface">
|
||||
<Trash2 size={12} /> Uninstall
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button onClick={() => handlePullApk(pkg.packageName)} className="flex items-center gap-2 w-full px-3 py-2 text-xs text-text-secondary hover:text-text-primary hover:bg-bg-surface">
|
||||
<Download size={12} /> Pull APK
|
||||
</button>
|
||||
<button onClick={() => handleClearData(pkg.packageName)} className="flex items-center gap-2 w-full px-3 py-2 text-xs text-warn hover:bg-bg-surface">
|
||||
<X size={12} /> Clear Data
|
||||
</button>
|
||||
<button onClick={() => handleForceStop(pkg.packageName)} className="flex items-center gap-2 w-full px-3 py-2 text-xs text-danger hover:bg-bg-surface">
|
||||
<PowerOff size={12} /> Force Stop
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,10 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { Shield, RefreshCw, Check, AlertTriangle, Palette, Lock, ChevronDown, ChevronRight } from 'lucide-react'
|
||||
import { GetBinaryInfo, SetAdbPath, SetFastbootPath, AppLockStatus, SetAppPassword, DisableAppLock, SetRequireForDanger, OpenURL } from '../../lib/wails'
|
||||
import { Shield, RefreshCw, Check, AlertTriangle, Palette, Lock } from 'lucide-react'
|
||||
import { GetBinaryInfo, SetAdbPath, SetFastbootPath, AppLockStatus, SetAppPassword, DisableAppLock, SetRequireForDanger } from '../../lib/wails'
|
||||
import { notify } from '../../lib/notify'
|
||||
import { refreshAppLockStatus } from '../../lib/applock'
|
||||
import { applyTheme, getTheme, THEMES, type Theme } from '../../lib/theme'
|
||||
import {
|
||||
getCustomAccent, setCustomAccent, getCustomTextColor, setCustomTextColor,
|
||||
getCustomFont, setCustomFont, FONT_OPTIONS,
|
||||
getCustomFontSize, setCustomFontSize, FONT_SIZE_MIN, FONT_SIZE_MAX, FONT_SIZE_DEFAULT
|
||||
} from '../../lib/appearance'
|
||||
import { getCustomAccent, setCustomAccent, getCustomFont, setCustomFont, FONT_OPTIONS } from '../../lib/appearance'
|
||||
import { getSidebarPosition, setSidebarPosition, SIDEBAR_POSITIONS, getSidebarLabels, setSidebarLabels, type SidebarPosition } from '../../lib/layout'
|
||||
import { getRootTools, setRootTools, getHiddenViews, setHiddenViews, TOGGLEABLE_VIEWS, getMuteNoDevice, setMuteNoDevice } from '../../lib/featureflags'
|
||||
import { resetDismissed } from '../../lib/dismissible'
|
||||
|
|
@ -22,18 +18,13 @@ export default function ViewSettings() {
|
|||
const [sidebarPos, setSidebarPos] = useState<SidebarPosition>(getSidebarPosition())
|
||||
const [sidebarLabels, setSidebarLabelsState] = useState<boolean>(getSidebarLabels())
|
||||
const [rootTools, setRootToolsState] = useState<boolean>(getRootTools())
|
||||
const [aboutOpen, setAboutOpen] = useState(false)
|
||||
|
||||
const [customAccent, setCustomAccentState] = useState<string>(getCustomAccent())
|
||||
const [customTextColor, setCustomTextColorState] = useState<string>(getCustomTextColor())
|
||||
const [customFont, setCustomFontState] = useState<string>(getCustomFont())
|
||||
const [customFontSize, setCustomFontSizeState] = useState<number>(getCustomFontSize())
|
||||
|
||||
const changeTheme = (t: Theme) => { setTheme(t); applyTheme(t) }
|
||||
const changeAccent = (hex: string | null) => { setCustomAccentState(hex || ''); setCustomAccent(hex) }
|
||||
const changeTextColor = (hex: string | null) => { setCustomTextColorState(hex || ''); setCustomTextColor(hex) }
|
||||
const changeFont = (id: string) => { setCustomFontState(id); setCustomFont(id || null) }
|
||||
const changeFontSize = (px: number) => { setCustomFontSizeState(px); setCustomFontSize(px) }
|
||||
const changeSidebarPos = (p: SidebarPosition) => { setSidebarPos(p); setSidebarPosition(p) }
|
||||
const changeSidebarLabels = (on: boolean) => { setSidebarLabelsState(on); setSidebarLabels(on) }
|
||||
const changeRootTools = (on: boolean) => { setRootToolsState(on); setRootTools(on) }
|
||||
|
|
@ -169,8 +160,8 @@ export default function ViewSettings() {
|
|||
))}
|
||||
</div>
|
||||
|
||||
{/* Custom accent/text colour + font/size — system-wide overrides on top of the theme */}
|
||||
<div className="pt-1 grid grid-cols-4 gap-4">
|
||||
{/* Custom accent colour + font — system-wide overrides on top of the theme */}
|
||||
<div className="pt-1 grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-xs text-text-muted mb-1.5">Custom accent colour (overrides the theme accent everywhere)</p>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -187,22 +178,6 @@ export default function ViewSettings() {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-text-muted mb-1.5">Custom text colour (overrides the theme's primary text colour)</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="color"
|
||||
value={customTextColor || '#c6d0f5'}
|
||||
onChange={e => changeTextColor(e.target.value)}
|
||||
className="h-8 w-12 rounded border border-bg-border bg-bg-raised cursor-pointer p-0.5"
|
||||
title="Pick a custom text colour"
|
||||
/>
|
||||
<span className="mono text-xs text-text-secondary">{customTextColor || 'theme default'}</span>
|
||||
{customTextColor && (
|
||||
<button onClick={() => changeTextColor(null)} className="btn-ghost text-xs ml-auto">Reset</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-text-muted mb-1.5">Font (applied app-wide)</p>
|
||||
<select
|
||||
|
|
@ -213,23 +188,6 @@ export default function ViewSettings() {
|
|||
{FONT_OPTIONS.map(f => <option key={f.id} value={f.id}>{f.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-text-muted mb-1.5">Font size ({customFontSize || FONT_SIZE_DEFAULT}px)</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="range"
|
||||
min={FONT_SIZE_MIN}
|
||||
max={FONT_SIZE_MAX}
|
||||
value={customFontSize || FONT_SIZE_DEFAULT}
|
||||
onChange={e => changeFontSize(Number(e.target.value))}
|
||||
className="flex-1 accent-accent-green"
|
||||
title="Adjust the app-wide base font size"
|
||||
/>
|
||||
{customFontSize > 0 && (
|
||||
<button onClick={() => changeFontSize(0)} className="btn-ghost text-xs">Reset</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-text-muted pt-1">Sidebar position. Applies instantly and is remembered.</p>
|
||||
|
|
@ -511,26 +469,12 @@ export default function ViewSettings() {
|
|||
|
||||
{/* About */}
|
||||
<div className="card p-4 space-y-2">
|
||||
<button
|
||||
onClick={() => setAboutOpen(o => !o)}
|
||||
className="flex items-center gap-2 w-full text-left"
|
||||
>
|
||||
{aboutOpen ? <ChevronDown size={13} className="text-text-muted" /> : <ChevronRight size={13} className="text-text-muted" />}
|
||||
<p className="section-title">About</p>
|
||||
</button>
|
||||
{aboutOpen && (
|
||||
<div className="text-xs text-text-muted space-y-1 pl-5">
|
||||
<p>ATK (Android Toolkit) — an all-in-one ADB GUI for Android power users</p>
|
||||
<p>All commands use discrete argument passing — no shell string building, no injection vectors</p>
|
||||
<p>Built with Wails v2 (Go + React) · github.com/jegly/ATK</p>
|
||||
<p>
|
||||
Jesse Li-Yates ·{' '}
|
||||
<button onClick={() => OpenURL('https://www.jegly.xyz')} className="text-accent-green hover:underline">
|
||||
www.jegly.xyz
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<p className="section-title">About</p>
|
||||
<div className="text-xs text-text-muted space-y-1">
|
||||
<p>ATK (Android Toolkit) — an all-in-one ADB GUI for Android power users and bug hunters</p>
|
||||
<p>All commands use discrete argument passing — no shell string building, no injection vectors</p>
|
||||
<p>Built with Wails v2 (Go + React) · github.com/jegly/ATK</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,33 +7,16 @@
|
|||
// removes the inline prop so the theme's own value shows through again.
|
||||
|
||||
const ACCENT_KEY = 'atk-custom-accent'
|
||||
const TEXT_COLOR_KEY = 'atk-custom-text-color'
|
||||
const FONT_KEY = 'atk-custom-font'
|
||||
const FONT_SIZE_KEY = 'atk-custom-font-size'
|
||||
|
||||
// Base UI font size in px. The slider in Settings ranges over these bounds;
|
||||
// FONT_SIZE_DEFAULT matches global.css's hardcoded fallback (var(--app-font-size, 14px)).
|
||||
export const FONT_SIZE_MIN = 12
|
||||
export const FONT_SIZE_MAX = 18
|
||||
export const FONT_SIZE_DEFAULT = 14
|
||||
|
||||
// Built-in font choices. '' = use the theme/app default. The @fontsource
|
||||
// families and the 8 self-hosted display fonts (styles/fonts.css) are
|
||||
// bundled; the "system-*" entries are generics that always resolve.
|
||||
// Built-in font choices. '' = use the theme/app default. The two @fontsource
|
||||
// families are bundled; the rest are system generics that always resolve.
|
||||
export const FONT_OPTIONS: { id: string; label: string; stack: string }[] = [
|
||||
{ id: '', label: 'Default (IBM Plex Sans)', stack: '' },
|
||||
{ id: 'jetbrains', label: 'JetBrains Mono', stack: "'JetBrains Mono', monospace" },
|
||||
{ id: 'system-sans', label: 'System Sans', stack: 'system-ui, sans-serif' },
|
||||
{ id: 'system-serif', label: 'System Serif', stack: 'Georgia, \'Times New Roman\', serif' },
|
||||
{ id: 'system-mono', label: 'System Monospace', stack: 'ui-monospace, \'Cascadia Code\', \'Courier New\', monospace' },
|
||||
{ id: 'dotgothic16', label: 'DotGothic16', stack: "'DotGothic16', sans-serif" },
|
||||
{ id: 'geist-pixel', label: 'Geist Pixel', stack: "'Geist Pixel', monospace" },
|
||||
{ id: 'gugi', label: 'Gugi', stack: "'Gugi', sans-serif" },
|
||||
{ id: 'orbitron', label: 'Orbitron', stack: "'Orbitron', sans-serif" },
|
||||
{ id: 'playfair-display', label: 'Playfair Display', stack: "'Playfair Display', serif" },
|
||||
{ id: 'press-start-2p', label: 'Press Start 2P', stack: "'Press Start 2P', monospace" },
|
||||
{ id: 'space-grotesk', label: 'Space Grotesk', stack: "'Space Grotesk', sans-serif" },
|
||||
{ id: 'vt323', label: 'VT323', stack: "'VT323', monospace" },
|
||||
]
|
||||
|
||||
export function hexToChannels(hex: string): string | null {
|
||||
|
|
@ -49,39 +32,20 @@ function darkenChannels(ch: string, f: number): string {
|
|||
}
|
||||
|
||||
export function getCustomAccent(): string { return localStorage.getItem(ACCENT_KEY) || '' }
|
||||
export function getCustomTextColor(): string { return localStorage.getItem(TEXT_COLOR_KEY) || '' }
|
||||
export function getCustomFont(): string { return localStorage.getItem(FONT_KEY) || '' }
|
||||
|
||||
// Returns 0 when unset (theme default applies via the CSS fallback).
|
||||
export function getCustomFontSize(): number {
|
||||
const v = parseInt(localStorage.getItem(FONT_SIZE_KEY) || '', 10)
|
||||
return Number.isFinite(v) && v >= FONT_SIZE_MIN && v <= FONT_SIZE_MAX ? v : 0
|
||||
}
|
||||
|
||||
export function setCustomAccent(hex: string | null): void {
|
||||
if (hex && hexToChannels(hex)) localStorage.setItem(ACCENT_KEY, hex)
|
||||
else localStorage.removeItem(ACCENT_KEY)
|
||||
applyAppearance()
|
||||
}
|
||||
|
||||
export function setCustomTextColor(hex: string | null): void {
|
||||
if (hex && hexToChannels(hex)) localStorage.setItem(TEXT_COLOR_KEY, hex)
|
||||
else localStorage.removeItem(TEXT_COLOR_KEY)
|
||||
applyAppearance()
|
||||
}
|
||||
|
||||
export function setCustomFont(id: string | null): void {
|
||||
if (id) localStorage.setItem(FONT_KEY, id)
|
||||
else localStorage.removeItem(FONT_KEY)
|
||||
applyAppearance()
|
||||
}
|
||||
|
||||
export function setCustomFontSize(px: number | null): void {
|
||||
if (px && px >= FONT_SIZE_MIN && px <= FONT_SIZE_MAX) localStorage.setItem(FONT_SIZE_KEY, String(px))
|
||||
else localStorage.removeItem(FONT_SIZE_KEY)
|
||||
applyAppearance()
|
||||
}
|
||||
|
||||
// applyAppearance (re)applies the stored overrides. Call on boot and after a change.
|
||||
export function applyAppearance(): void {
|
||||
const root = document.documentElement
|
||||
|
|
@ -96,20 +60,8 @@ export function applyAppearance(): void {
|
|||
root.style.removeProperty('--accent-dim')
|
||||
}
|
||||
|
||||
const textColor = getCustomTextColor()
|
||||
const tch = textColor ? hexToChannels(textColor) : null
|
||||
if (tch) {
|
||||
root.style.setProperty('--text-primary', tch)
|
||||
} else {
|
||||
root.style.removeProperty('--text-primary')
|
||||
}
|
||||
|
||||
const fontId = getCustomFont()
|
||||
const stack = FONT_OPTIONS.find(f => f.id === fontId)?.stack
|
||||
if (stack) root.style.setProperty('--app-font', stack)
|
||||
else root.style.removeProperty('--app-font')
|
||||
|
||||
const fontSize = getCustomFontSize()
|
||||
if (fontSize) root.style.setProperty('--app-font-size', `${fontSize}px`)
|
||||
else root.style.removeProperty('--app-font-size')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,19 +32,16 @@ export const TOGGLEABLE_VIEWS: { view: string; label: string }[] = [
|
|||
{ view: 'files', label: 'Files' },
|
||||
{ view: 'mirror', label: 'Screen Mirror' },
|
||||
{ view: 'packages', label: 'Packages' },
|
||||
{ view: 'apkinstaller', label: 'APK Installer' },
|
||||
{ view: 'debloater', label: 'Debloater' },
|
||||
{ view: 'shell', label: 'Shell' },
|
||||
{ view: 'logcat', label: 'Logcat' },
|
||||
{ view: 'appinspect', label: 'App Inspector' },
|
||||
{ view: 'intentlab', label: 'Intent Lab' },
|
||||
{ view: 'apkaudit', label: 'APK Audit' },
|
||||
{ view: 'certs', label: 'Certificates' },
|
||||
{ view: 'backup', label: 'Backup' },
|
||||
{ view: 'props', label: 'Prop Editor' },
|
||||
{ view: 'utilities', label: 'Utilities' },
|
||||
{ view: 'flasher', label: 'Flasher' },
|
||||
{ view: 'gsiloader', label: 'GSI Loader' },
|
||||
]
|
||||
|
||||
export function getHiddenViews(): string[] {
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
// Trigger for the close-confirmation dialog (QuitGate.tsx). The title bar's
|
||||
// close button calls requestQuitConfirm() instead of quitting directly, so
|
||||
// the user picks Minimize-to-tray vs Quit rather than the window just vanishing.
|
||||
let host: (() => void) | null = null
|
||||
|
||||
export function _registerQuitHost(fn: () => void): () => void {
|
||||
host = fn
|
||||
return () => { host = null }
|
||||
}
|
||||
|
||||
export function requestQuitConfirm() {
|
||||
host?.()
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
// Works around a known Chromium/WebView2 bug where a native scrollbar-thumb
|
||||
// drag gets stuck "captured" after a click - typically triggered by precision
|
||||
// touchpad drivers that emit mousemove without a matching mouseup right after
|
||||
// the click. Symptom: click a scrollbar once, then any further pointer motion
|
||||
// keeps dragging it as if the button were still held down. Long lists (the
|
||||
// Packages / Debloater views) are where this is actually noticed.
|
||||
//
|
||||
// Only acts if we saw a mousedown with no matching mouseup yet - `e.buttons
|
||||
// === 0` on a subsequent mousemove then means the OS-reported button state
|
||||
// disagrees with what we're tracking, i.e. the real mouseup happened but
|
||||
// wasn't delivered as an event. That's the stuck-drag signature; a synthetic
|
||||
// mouseup nudges the browser's internal scrollbar-drag state to release.
|
||||
export function installScrollbarDragFix() {
|
||||
let mouseIsDown = false
|
||||
|
||||
window.addEventListener('mousedown', () => { mouseIsDown = true }, { capture: true })
|
||||
window.addEventListener('mouseup', () => { mouseIsDown = false }, { capture: true })
|
||||
|
||||
window.addEventListener(
|
||||
'mousemove',
|
||||
(e: MouseEvent) => {
|
||||
if (mouseIsDown && e.buttons === 0) {
|
||||
mouseIsDown = false
|
||||
window.dispatchEvent(
|
||||
new MouseEvent('mouseup', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
{ capture: true }
|
||||
)
|
||||
}
|
||||
|
|
@ -36,13 +36,6 @@ export const THEMES: ThemeDef[] = [
|
|||
{ id: 'ocean', label: 'Ocean', hint: 'Muted blue-grey', swatch: ['#2b303b', '#343d46', '#8fa1b3', '#c0c5ce'] },
|
||||
{ id: 'kokuban', label: 'Kokuban', hint: 'Chalkboard green', swatch: ['#1f3526', '#274030', '#f2e9c8', '#f0f0e8'] },
|
||||
{ id: 'mono-cyan', label: 'Mono Cyan', hint: 'Monochrome cyan glow', swatch: ['#081414', '#0e1f1f', '#00d0d0', '#c8f0f0'] },
|
||||
// More additions
|
||||
{ id: 'nord', label: 'Nord', hint: 'Arctic blue-grey with frost green', swatch: ['#2e3440', '#3b4252', '#a3be8c', '#eceff4'] },
|
||||
{ id: 'tokyo-night', label: 'Tokyo Night', hint: 'Neon purple & blue on midnight', swatch: ['#1a1b26', '#16161e', '#9ece6a', '#c0caf5'] },
|
||||
{ id: 'solarized-dark', label: 'Solarized Dark', hint: 'Low-contrast teal & olive', swatch: ['#002b36', '#073642', '#859900', '#93a1a1'] },
|
||||
{ id: 'solarized-light', label: 'Solarized Light', hint: 'Low-contrast teal & olive, light', swatch: ['#fdf6e3', '#eee8d5', '#859900', '#586e75'] },
|
||||
{ id: 'rose-pine', label: 'Rosé Pine', hint: 'Elegant muted purple & rose', swatch: ['#191724', '#1f1d2e', '#9ccfd8', '#e0def4'] },
|
||||
{ id: 'everforest', label: 'Everforest', hint: 'Warm forest green, easy on the eyes', swatch: ['#2d353b', '#343f44', '#a7c080', '#d3c6aa'] },
|
||||
]
|
||||
|
||||
export type Theme = string
|
||||
|
|
|
|||
|
|
@ -37,15 +37,6 @@ export interface FileEntry {
|
|||
export interface PackageInfo {
|
||||
packageName: string
|
||||
isEnabled: boolean
|
||||
// False when the package is present on the system image but uninstalled for
|
||||
// user 0 (e.g. via Canta/Shizuku, which runs `pm uninstall --user 0`).
|
||||
isInstalled: boolean
|
||||
}
|
||||
|
||||
export interface ApkFileInfo {
|
||||
path: string
|
||||
name: string
|
||||
size: number
|
||||
}
|
||||
|
||||
// Relationship kinds mined (in Go) from a log line for the visual map.
|
||||
|
|
@ -260,7 +251,6 @@ export type View =
|
|||
| 'files'
|
||||
| 'mirror'
|
||||
| 'packages'
|
||||
| 'apkinstaller'
|
||||
| 'debloater'
|
||||
| 'shell'
|
||||
| 'logcat'
|
||||
|
|
|
|||
|
|
@ -17,16 +17,6 @@ export const GetBinaryInfo = () => window['go']['main']['App']['GetBinaryInfo'](
|
|||
// @ts-ignore
|
||||
export const CheckSystemRequirements = () => window['go']['main']['App']['CheckSystemRequirements']()
|
||||
// @ts-ignore
|
||||
export const ShowWindow = () => window['go']['main']['App']['ShowWindow']()
|
||||
// @ts-ignore
|
||||
export const HideWindow = () => window['go']['main']['App']['HideWindow']()
|
||||
// @ts-ignore
|
||||
export const QuitApp = () => window['go']['main']['App']['QuitApp']()
|
||||
// @ts-ignore
|
||||
export const TrayAvailable = () => window['go']['main']['App']['TrayAvailable']()
|
||||
// @ts-ignore
|
||||
export const OpenURL = (url: string) => window['go']['main']['App']['OpenURL'](url)
|
||||
// @ts-ignore
|
||||
export const SetAdbPath = (path: string) => window['go']['main']['App']['SetAdbPath'](path)
|
||||
// @ts-ignore
|
||||
export const SetFastbootPath = (path: string) => window['go']['main']['App']['SetFastbootPath'](path)
|
||||
|
|
@ -109,21 +99,9 @@ export const EnableMultiplePackages = (pkgs: string[]) => window['go']['main']['
|
|||
// @ts-ignore
|
||||
export const RestoreMultiplePackages = (pkgs: string[]) => window['go']['main']['App']['RestoreMultiplePackages'](pkgs)
|
||||
// @ts-ignore
|
||||
export const RestorePackage = (pkg: string) => window['go']['main']['App']['RestorePackage'](pkg)
|
||||
// @ts-ignore
|
||||
export const SelectFileForInstall = () => window['go']['main']['App']['SelectFileForInstall']()
|
||||
// @ts-ignore
|
||||
export const SideloadPackage = (path: string) => window['go']['main']['App']['SideloadPackage'](path)
|
||||
// @ts-ignore
|
||||
export const SelectApkFolder = () => window['go']['main']['App']['SelectApkFolder']()
|
||||
// @ts-ignore
|
||||
export const SelectMultipleApkFiles = () => window['go']['main']['App']['SelectMultipleApkFiles']()
|
||||
// @ts-ignore
|
||||
export const ListApksInFolder = (folder: string) => window['go']['main']['App']['ListApksInFolder'](folder)
|
||||
// @ts-ignore
|
||||
export const StatApkFiles = (paths: string[]) => window['go']['main']['App']['StatApkFiles'](paths)
|
||||
// @ts-ignore
|
||||
export const InstallApksWithProgress = (paths: string[]) => window['go']['main']['App']['InstallApksWithProgress'](paths)
|
||||
|
||||
// Shell
|
||||
// @ts-ignore
|
||||
|
|
|
|||
|
|
@ -7,16 +7,13 @@ import '@fontsource/ibm-plex-sans/500.css'
|
|||
import '@fontsource/ibm-plex-sans/600.css'
|
||||
import '@fontsource/jetbrains-mono/400.css'
|
||||
import '@fontsource/jetbrains-mono/500.css'
|
||||
import './styles/fonts.css'
|
||||
import './styles/global.css'
|
||||
import { applyTheme, getTheme } from './lib/theme'
|
||||
import { applyAppearance } from './lib/appearance'
|
||||
import { installScrollbarDragFix } from './lib/scrollfix'
|
||||
|
||||
// Apply the saved theme + custom accent/font before first paint to avoid a flash.
|
||||
applyTheme(getTheme())
|
||||
applyAppearance()
|
||||
installScrollbarDragFix()
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
/* Self-hosted display/decorative fonts (bundled, OFL-licensed - see
|
||||
src/assets/fonts/licenses/). Selectable app-wide from Settings via
|
||||
lib/appearance.ts FONT_OPTIONS. Variable-font files cover their whole
|
||||
weight range through a single @font-face with a weight range. */
|
||||
|
||||
@font-face {
|
||||
font-family: 'DotGothic16';
|
||||
src: url('../assets/fonts/DotGothic16-Regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Geist Pixel';
|
||||
src: url('../assets/fonts/GeistPixel-Regular-VariableFont_ELSH.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Gugi';
|
||||
src: url('../assets/fonts/Gugi-Regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Orbitron';
|
||||
src: url('../assets/fonts/Orbitron-VariableFont_wght.ttf') format('truetype');
|
||||
font-weight: 400 900;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Playfair Display';
|
||||
src: url('../assets/fonts/PlayfairDisplay-VariableFont_wght.ttf') format('truetype');
|
||||
font-weight: 400 900;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Press Start 2P';
|
||||
src: url('../assets/fonts/PressStart2P-Regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
src: url('../assets/fonts/SpaceGrotesk-VariableFont_wght.ttf') format('truetype');
|
||||
font-weight: 300 700;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'VT323';
|
||||
src: url('../assets/fonts/VT323-Regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
}
|
||||
|
|
@ -357,109 +357,6 @@
|
|||
--scrollbar-hover: 79 136 136;
|
||||
}
|
||||
|
||||
/* Nord — arctic blue-grey with frost green */
|
||||
:root[data-theme="nord"] {
|
||||
--bg-base: 46 52 64;
|
||||
--bg-surface: 59 66 82;
|
||||
--bg-raised: 67 76 94;
|
||||
--bg-border: 76 86 106;
|
||||
--accent-green: 163 190 140;
|
||||
--accent-dim: 130 152 112;
|
||||
--accent-muted: 33 38 28;
|
||||
--text-primary: 236 239 244;
|
||||
--text-secondary: 216 222 233;
|
||||
--text-muted: 76 86 106;
|
||||
--danger: 191 97 106;
|
||||
--warn: 235 203 139;
|
||||
--scrollbar-hover: 126 133 148;
|
||||
}
|
||||
|
||||
/* Tokyo Night — neon purple & blue on midnight */
|
||||
:root[data-theme="tokyo-night"] {
|
||||
--bg-base: 26 27 38;
|
||||
--bg-surface: 22 22 30;
|
||||
--bg-raised: 36 40 59;
|
||||
--bg-border: 41 46 66;
|
||||
--accent-green: 158 206 106;
|
||||
--accent-dim: 126 165 85;
|
||||
--accent-muted: 32 41 21;
|
||||
--text-primary: 192 202 245;
|
||||
--text-secondary: 169 177 214;
|
||||
--text-muted: 86 95 137;
|
||||
--danger: 247 118 142;
|
||||
--warn: 224 175 104;
|
||||
--scrollbar-hover: 101 105 119;
|
||||
}
|
||||
|
||||
/* Solarized Dark — low-contrast teal & olive */
|
||||
:root[data-theme="solarized-dark"] {
|
||||
--bg-base: 0 43 54;
|
||||
--bg-surface: 7 54 66;
|
||||
--bg-raised: 10 61 74;
|
||||
--bg-border: 88 110 117;
|
||||
--accent-green: 133 153 0;
|
||||
--accent-dim: 106 122 0;
|
||||
--accent-muted: 27 31 0;
|
||||
--text-primary: 147 161 161;
|
||||
--text-secondary: 131 148 150;
|
||||
--text-muted: 88 110 117;
|
||||
--danger: 220 50 47;
|
||||
--warn: 181 137 0;
|
||||
--scrollbar-hover: 135 151 156;
|
||||
}
|
||||
|
||||
/* Solarized Light — low-contrast teal & olive, light */
|
||||
:root[data-theme="solarized-light"] {
|
||||
--bg-base: 253 246 227;
|
||||
--bg-surface: 238 232 213;
|
||||
--bg-raised: 229 221 195;
|
||||
--bg-border: 201 189 151;
|
||||
--accent-green: 133 153 0;
|
||||
--accent-dim: 106 122 0;
|
||||
--accent-muted: 233 237 209;
|
||||
--text-primary: 88 110 117;
|
||||
--text-secondary: 101 123 131;
|
||||
--text-muted: 147 161 161;
|
||||
--danger: 220 50 47;
|
||||
--warn: 181 137 0;
|
||||
--scrollbar-hover: 185 174 139;
|
||||
}
|
||||
:root[data-theme="solarized-light"] body { color-scheme: light; }
|
||||
|
||||
/* Rosé Pine — elegant muted purple & rose */
|
||||
:root[data-theme="rose-pine"] {
|
||||
--bg-base: 25 23 36;
|
||||
--bg-surface: 31 29 46;
|
||||
--bg-raised: 38 35 58;
|
||||
--bg-border: 64 61 82;
|
||||
--accent-green: 156 207 216;
|
||||
--accent-dim: 125 166 173;
|
||||
--accent-muted: 31 41 43;
|
||||
--text-primary: 224 222 244;
|
||||
--text-secondary: 144 140 170;
|
||||
--text-muted: 110 106 134;
|
||||
--danger: 235 111 146;
|
||||
--warn: 246 193 119;
|
||||
--scrollbar-hover: 117 115 130;
|
||||
}
|
||||
|
||||
/* Everforest — warm forest green, easy on the eyes */
|
||||
:root[data-theme="everforest"] {
|
||||
--bg-base: 45 53 59;
|
||||
--bg-surface: 52 63 68;
|
||||
--bg-raised: 61 72 77;
|
||||
--bg-border: 71 82 88;
|
||||
--accent-green: 167 192 128;
|
||||
--accent-dim: 134 154 102;
|
||||
--accent-muted: 33 38 26;
|
||||
--text-primary: 211 198 170;
|
||||
--text-secondary: 157 169 160;
|
||||
--text-muted: 122 132 120;
|
||||
--danger: 230 126 128;
|
||||
--warn: 219 188 127;
|
||||
--scrollbar-hover: 123 130 135;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
|
|
@ -500,10 +397,10 @@
|
|||
background: transparent;
|
||||
color-scheme: dark;
|
||||
color: rgb(var(--text-primary));
|
||||
/* --app-font/--app-font-size are optional system-wide overrides set from
|
||||
Settings (see lib/appearance.ts); fall back to the bundled UI font/14px. */
|
||||
/* --app-font is an optional system-wide override set from Settings (see
|
||||
lib/appearance.ts); falls back to the bundled UI font. */
|
||||
font-family: var(--app-font, 'IBM Plex Sans', sans-serif);
|
||||
font-size: var(--app-font-size, 14px);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
/* Content is selectable/copyable; interactive chrome opts out below. */
|
||||
|
|
@ -556,18 +453,17 @@
|
|||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgb(var(--bg-border));
|
||||
border-radius: 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover,
|
||||
::-webkit-scrollbar-thumb:active {
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgb(var(--scrollbar-hover));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
4
go.mod
4
go.mod
|
|
@ -5,11 +5,10 @@ go 1.25.0
|
|||
require (
|
||||
github.com/avast/apkparser v0.0.0-20260423123151-7fcaee440f68
|
||||
github.com/avast/apkverifier v0.0.0-20260410045523-e2781ccbddc8
|
||||
github.com/godbus/dbus/v5 v5.1.0
|
||||
github.com/ncruces/zenity v0.10.14
|
||||
github.com/ulikunitz/xz v0.5.15
|
||||
github.com/wailsapp/wails/v2 v2.12.0
|
||||
golang.org/x/crypto v0.52.0
|
||||
golang.org/x/crypto v0.51.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
@ -18,6 +17,7 @@ require (
|
|||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -84,8 +84,8 @@ github.com/wailsapp/wails/v2 v2.12.0 h1:BHO/kLNWFHYjCzucxbzAYZWUjub1Tvb4cSguQozH
|
|||
github.com/wailsapp/wails/v2 v2.12.0/go.mod h1:mo1bzK1DEJrobt7YrBjgxvb5Sihb1mhAY09hppbibQg=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
|
||||
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
name: "atk"
|
||||
arch: "amd64"
|
||||
platform: "linux"
|
||||
version: "1.3.0"
|
||||
version: "1.2.0"
|
||||
section: "utils"
|
||||
priority: "optional"
|
||||
maintainer: "jegly <https://github.com/jegly>"
|
||||
|
|
|
|||
|
|
@ -35,22 +35,20 @@ func (a *App) ListPackages(filterType string) ([]PackageInfo, error) {
|
|||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var allPkgs, installedPkgs, disabledPkgs []string
|
||||
var allPkgs, disabledPkgs []string
|
||||
var errAll error
|
||||
|
||||
wg.Add(3)
|
||||
wg.Add(2)
|
||||
|
||||
// Membership = the UNFILTERED superset for this category, via `-u`. Every
|
||||
// package that's ever been on the system image shows up here, INCLUDING
|
||||
// ones uninstalled for user 0 (Canta/Shizuku's "uninstall" of a system app
|
||||
// is really `pm uninstall --user 0`, which leaves the APK on disk but drops
|
||||
// it from a plain `pm list packages`). The old approach queried without
|
||||
// `-u`, which is why per-user-uninstalled system apps were invisible in
|
||||
// Packages / Debloater / the App Inspector picker even though they were
|
||||
// still physically present on the device.
|
||||
// Membership = the UNFILTERED list for this category. Every installed package
|
||||
// shows up here regardless of state. The old approach unioned `-e` (enabled)
|
||||
// and `-d` (disabled), but a ROM parks some apps in states (e.g.
|
||||
// DISABLED_UNTIL_USED) that neither filter reports, so system apps silently
|
||||
// went missing from App Inspector / APK Audit. Plain `pm list packages` is a
|
||||
// strict superset, so nothing is dropped.
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
output, err := a.runAdbShell(buildArgs("-u")...)
|
||||
output, err := a.runAdbShell(buildArgs()...)
|
||||
if err != nil {
|
||||
errAll = err
|
||||
return
|
||||
|
|
@ -58,19 +56,6 @@ func (a *App) ListPackages(filterType string) ([]PackageInfo, error) {
|
|||
allPkgs = parse(output)
|
||||
}()
|
||||
|
||||
// Without `-u`: only packages actually installed for user 0 right now. Used
|
||||
// to tell "uninstalled for user" apart from enabled/disabled — `-d`/`-e`
|
||||
// alone can't do that, since a per-user-uninstalled package appears in
|
||||
// neither.
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
output, err := a.runAdbShell(buildArgs()...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
installedPkgs = parse(output)
|
||||
}()
|
||||
|
||||
// `-d` is used only to flag the disabled badge. Some ROMs restrict it; that's
|
||||
// non-fatal — it just means nothing gets marked disabled.
|
||||
go func() {
|
||||
|
|
@ -88,10 +73,6 @@ func (a *App) ListPackages(filterType string) ([]PackageInfo, error) {
|
|||
return nil, fmt.Errorf("failed to list packages: %w", errAll)
|
||||
}
|
||||
|
||||
installed := make(map[string]bool, len(installedPkgs))
|
||||
for _, p := range installedPkgs {
|
||||
installed[p] = true
|
||||
}
|
||||
disabled := make(map[string]bool, len(disabledPkgs))
|
||||
for _, p := range disabledPkgs {
|
||||
disabled[p] = true
|
||||
|
|
@ -104,14 +85,7 @@ func (a *App) ListPackages(filterType string) ([]PackageInfo, error) {
|
|||
continue
|
||||
}
|
||||
seen[p] = true
|
||||
isInstalled := installed[p]
|
||||
// A package not installed for the user can't run either way, so it's
|
||||
// never reported as enabled regardless of its internal enabled flag.
|
||||
packages = append(packages, PackageInfo{
|
||||
PackageName: p,
|
||||
IsInstalled: isInstalled,
|
||||
IsEnabled: isInstalled && !disabled[p],
|
||||
})
|
||||
packages = append(packages, PackageInfo{PackageName: p, IsEnabled: !disabled[p]})
|
||||
}
|
||||
return packages, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,298 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generates the ATK tray icon assets from a hand-specified vector path (no
|
||||
image libraries available offline - this rasterizes and encodes PNG/ICO from
|
||||
scratch using only the stdlib). Re-run after editing PATH_D or COLORS.
|
||||
|
||||
Outputs into assets/:
|
||||
tray_icon.png 32x32 RGBA, ATK green - Windows/Linux tray icon
|
||||
tray_icon_template.png 32x32 RGBA, black silhouette - macOS template icon
|
||||
tray_icon.ico 16 + 32px, ATK green - Windows .ico container
|
||||
tray_symbolic.svg vector, single-fill - Linux icon-theme install
|
||||
"""
|
||||
import re
|
||||
import struct
|
||||
import zlib
|
||||
import os
|
||||
|
||||
# Material Design "alternate_email" (@) glyph, 24x24 viewBox. Single source of
|
||||
# truth for every rasterized/vector output below.
|
||||
PATH_D = (
|
||||
"M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10c1.28,0 2.5,-0.2 3.68,-0.68l-0.36,-1.9"
|
||||
"C14.34,19.79 13.19,20 12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8s8,3.59 8,8v0.9"
|
||||
"c0,0.61 -0.6,1.32 -1.5,1.32s-1.5,-0.71 -1.5,-1.32V8h-2v0.68"
|
||||
"C14.42,8.24 13.51,8 12.5,8C10.02,8 8,10.02 8,12.5s2.02,4.5 4.5,4.5"
|
||||
"c1.14,0 2.17,-0.42 2.96,-1.11c0.55,0.66 1.42,1.11 2.29,1.11"
|
||||
"c1.66,0 3.25,-1.34 3.25,-3.32V12C21,6.48 16.52,2 12,2z"
|
||||
"M12.5,15c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5"
|
||||
"S13.88,15 12.5,15z"
|
||||
)
|
||||
VIEWBOX = 24
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
ASSETS = os.path.join(ROOT, "assets")
|
||||
|
||||
|
||||
def tokenize(d):
|
||||
return re.findall(r"[MmLlHhVvCcSsZz]|-?\d*\.?\d+(?:[eE]-?\d+)?", d)
|
||||
|
||||
|
||||
def flatten_cubic(p0, p1, p2, p3, segs=20):
|
||||
pts = []
|
||||
for i in range(1, segs + 1):
|
||||
t = i / segs
|
||||
mt = 1 - t
|
||||
x = mt**3 * p0[0] + 3 * mt**2 * t * p1[0] + 3 * mt * t**2 * p2[0] + t**3 * p3[0]
|
||||
y = mt**3 * p0[1] + 3 * mt**2 * t * p1[1] + 3 * mt * t**2 * p2[1] + t**3 * p3[1]
|
||||
pts.append((x, y))
|
||||
return pts
|
||||
|
||||
|
||||
def parse_path(d):
|
||||
"""Returns a flat list of (x1,y1,x2,y2) edges (line segments), flattening
|
||||
beziers, across every subpath - enough for a nonzero-winding scanline fill."""
|
||||
tokens = tokenize(d)
|
||||
i = 0
|
||||
cmd = None
|
||||
cx = cy = 0.0
|
||||
start_x = start_y = 0.0
|
||||
last_ctrl = None
|
||||
edges = []
|
||||
|
||||
def line_to(nx, ny):
|
||||
edges.append((cx, cy, nx, ny))
|
||||
|
||||
while i < len(tokens):
|
||||
tok = tokens[i]
|
||||
if tok in "MmLlHhVvCcSsZz":
|
||||
cmd = tok
|
||||
i += 1
|
||||
|
||||
if cmd in ("M", "m"):
|
||||
x, y = float(tokens[i]), float(tokens[i + 1])
|
||||
i += 2
|
||||
if cmd == "m":
|
||||
x += cx
|
||||
y += cy
|
||||
cx, cy = x, y
|
||||
start_x, start_y = x, y
|
||||
cmd = "L" if cmd == "M" else "l" # subsequent pairs are implicit lineto
|
||||
elif cmd in ("L", "l"):
|
||||
x, y = float(tokens[i]), float(tokens[i + 1])
|
||||
i += 2
|
||||
if cmd == "l":
|
||||
x += cx
|
||||
y += cy
|
||||
line_to(x, y)
|
||||
cx, cy = x, y
|
||||
elif cmd in ("H", "h"):
|
||||
x = float(tokens[i])
|
||||
i += 1
|
||||
if cmd == "h":
|
||||
x += cx
|
||||
line_to(x, cy)
|
||||
cx = x
|
||||
elif cmd in ("V", "v"):
|
||||
y = float(tokens[i])
|
||||
i += 1
|
||||
if cmd == "v":
|
||||
y += cy
|
||||
line_to(cx, y)
|
||||
cy = y
|
||||
elif cmd in ("C", "c"):
|
||||
x1, y1, x2, y2, x, y = (float(tokens[i + k]) for k in range(6))
|
||||
i += 6
|
||||
if cmd == "c":
|
||||
x1 += cx; y1 += cy; x2 += cx; y2 += cy; x += cx; y += cy
|
||||
for px, py in flatten_cubic((cx, cy), (x1, y1), (x2, y2), (x, y)):
|
||||
edges.append((cx, cy, px, py))
|
||||
cx, cy = px, py
|
||||
last_ctrl = (x2, y2)
|
||||
elif cmd in ("S", "s"):
|
||||
x2, y2, x, y = (float(tokens[i + k]) for k in range(4))
|
||||
i += 4
|
||||
if cmd == "s":
|
||||
x2 += cx; y2 += cy; x += cx; y += cy
|
||||
if last_ctrl:
|
||||
x1, y1 = 2 * cx - last_ctrl[0], 2 * cy - last_ctrl[1]
|
||||
else:
|
||||
x1, y1 = cx, cy
|
||||
for px, py in flatten_cubic((cx, cy), (x1, y1), (x2, y2), (x, y)):
|
||||
edges.append((cx, cy, px, py))
|
||||
cx, cy = px, py
|
||||
last_ctrl = (x2, y2)
|
||||
elif cmd in ("Z", "z"):
|
||||
line_to(start_x, start_y)
|
||||
cx, cy = start_x, start_y
|
||||
else:
|
||||
raise ValueError(f"unhandled path command {cmd!r}")
|
||||
return edges
|
||||
|
||||
|
||||
def winding_at(x, y, edges):
|
||||
w = 0
|
||||
for x1, y1, x2, y2 in edges:
|
||||
if y1 == y2:
|
||||
continue
|
||||
if (y1 <= y < y2) or (y2 <= y < y1):
|
||||
t = (y - y1) / (y2 - y1)
|
||||
xi = x1 + t * (x2 - x1)
|
||||
if xi > x:
|
||||
w += 1 if y2 > y1 else -1
|
||||
return w
|
||||
|
||||
|
||||
def rasterize(edges, size, supersample=4):
|
||||
"""Nonzero-winding scanline fill -> size x size alpha coverage (0..255),
|
||||
antialiased via `supersample`x supersampling + box downsample."""
|
||||
hi = size * supersample
|
||||
scale = hi / VIEWBOX
|
||||
scaled = [(x1 * scale, y1 * scale, x2 * scale, y2 * scale) for x1, y1, x2, y2 in edges]
|
||||
coverage = [[0] * hi for _ in range(hi)]
|
||||
for y in range(hi):
|
||||
fy = y + 0.5
|
||||
# crossings for this scanline, sorted, with winding deltas
|
||||
xs = []
|
||||
for x1, y1, x2, y2 in scaled:
|
||||
if y1 == y2:
|
||||
continue
|
||||
if (y1 <= fy < y2) or (y2 <= fy < y1):
|
||||
t = (fy - y1) / (y2 - y1)
|
||||
xi = x1 + t * (x2 - x1)
|
||||
xs.append((xi, 1 if y2 > y1 else -1))
|
||||
xs.sort()
|
||||
w = 0
|
||||
row = coverage[y]
|
||||
for idx in range(len(xs)):
|
||||
xi, d = xs[idx]
|
||||
prev_w = w
|
||||
w += d
|
||||
if prev_w == 0 and w != 0:
|
||||
span_start = xi
|
||||
elif prev_w != 0 and w == 0:
|
||||
x0 = max(0, int(round(span_start)))
|
||||
x1_ = min(hi, int(round(xi)))
|
||||
for px in range(x0, x1_):
|
||||
row[px] = 1
|
||||
# box downsample
|
||||
out = [[0] * size for _ in range(size)]
|
||||
for y in range(size):
|
||||
for x in range(size):
|
||||
s = 0
|
||||
for dy in range(supersample):
|
||||
row = coverage[y * supersample + dy]
|
||||
for dx in range(supersample):
|
||||
s += row[x * supersample + dx]
|
||||
out[y][x] = round(255 * s / (supersample * supersample))
|
||||
return out
|
||||
|
||||
|
||||
def to_rgba(coverage, size, rgb):
|
||||
r, g, b = rgb
|
||||
buf = bytearray(size * size * 4)
|
||||
for y in range(size):
|
||||
for x in range(size):
|
||||
a = coverage[y][x]
|
||||
o = (y * size + x) * 4
|
||||
buf[o] = r
|
||||
buf[o + 1] = g
|
||||
buf[o + 2] = b
|
||||
buf[o + 3] = a
|
||||
return bytes(buf)
|
||||
|
||||
|
||||
def write_png(path, size, rgba):
|
||||
def chunk(tag, data):
|
||||
return struct.pack(">I", len(data)) + tag + data + struct.pack(
|
||||
">I", zlib.crc32(tag + data) & 0xFFFFFFFF
|
||||
)
|
||||
|
||||
sig = b"\x89PNG\r\n\x1a\n"
|
||||
ihdr = struct.pack(">IIBBBBB", size, size, 8, 6, 0, 0, 0)
|
||||
raw = bytearray()
|
||||
stride = size * 4
|
||||
for y in range(size):
|
||||
raw.append(0)
|
||||
raw.extend(rgba[y * stride:(y + 1) * stride])
|
||||
idat = zlib.compress(bytes(raw), 9)
|
||||
with open(path, "wb") as f:
|
||||
f.write(sig)
|
||||
f.write(chunk(b"IHDR", ihdr))
|
||||
f.write(chunk(b"IDAT", idat))
|
||||
f.write(chunk(b"IEND", b""))
|
||||
|
||||
|
||||
def write_ico(path, frames):
|
||||
"""frames: list of (size, png_bytes)"""
|
||||
count = len(frames)
|
||||
header = struct.pack("<HHH", 0, 1, count)
|
||||
entries = b""
|
||||
blob = b""
|
||||
offset = 6 + count * 16
|
||||
for size, png_bytes in frames:
|
||||
wb = size if size < 256 else 0
|
||||
hb = size if size < 256 else 0
|
||||
entries += struct.pack("<BBBBHHII", wb, hb, 0, 0, 1, 32, len(png_bytes), offset)
|
||||
blob += png_bytes
|
||||
offset += len(png_bytes)
|
||||
with open(path, "wb") as f:
|
||||
f.write(header)
|
||||
f.write(entries)
|
||||
f.write(blob)
|
||||
|
||||
|
||||
def main():
|
||||
edges = parse_path(PATH_D)
|
||||
os.makedirs(ASSETS, exist_ok=True)
|
||||
|
||||
green = (0, 255, 136) # ATK accent green #00ff88
|
||||
black = (0, 0, 0)
|
||||
|
||||
sizes = [16, 32]
|
||||
green_pngs = {}
|
||||
for size in sizes:
|
||||
cov = rasterize(edges, size)
|
||||
rgba = to_rgba(cov, size, green)
|
||||
green_pngs[size] = rgba
|
||||
write_png(os.path.join(ASSETS, f"_tray_preview_{size}.png"), size, rgba)
|
||||
|
||||
# Primary tray icon (32px, colored) for Windows/Linux.
|
||||
write_png(os.path.join(ASSETS, "tray_icon.png"), 32, green_pngs[32])
|
||||
|
||||
# macOS template icon: solid black silhouette, alpha only. macOS recolors
|
||||
# this automatically for the light/dark menu bar.
|
||||
cov32 = rasterize(edges, 32)
|
||||
tmpl_rgba = to_rgba(cov32, 32, black)
|
||||
write_png(os.path.join(ASSETS, "tray_icon_template.png"), 32, tmpl_rgba)
|
||||
|
||||
# Windows .ico, 16 + 32px, PNG-compressed frames (Vista+ format).
|
||||
ico_frames = []
|
||||
for size in sizes:
|
||||
tmp_path = os.path.join(ASSETS, f"_ico_tmp_{size}.png")
|
||||
write_png(tmp_path, size, green_pngs[size])
|
||||
with open(tmp_path, "rb") as f:
|
||||
ico_frames.append((size, f.read()))
|
||||
os.remove(tmp_path)
|
||||
write_ico(os.path.join(ASSETS, "tray_icon.ico"), ico_frames)
|
||||
|
||||
# Linux tray icon: a StatusNotifierItem IconName resolved via an app-owned
|
||||
# IconThemePath (not the user's ~/.local/share/icons - no cache to
|
||||
# invalidate). Convention and fill colour (#bebebe) copied directly from
|
||||
# /opt/frequency/icons/frequency-tray-symbolic.svg, a working reference on
|
||||
# this exact machine: GNOME Shell recolors any such named symbolic icon to
|
||||
# monochrome at render time regardless of source colour, so the exact
|
||||
# shade doesn't matter - what matters is shipping a NAME the shell resolves
|
||||
# itself rather than raw pixmap bytes (which is what breaks on GNOME's
|
||||
# AppIndicator extension per Cascade's README).
|
||||
svg = (
|
||||
f'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" '
|
||||
f'viewBox="0 0 {VIEWBOX} {VIEWBOX}"><path d="{PATH_D}" fill="#bebebe"/></svg>\n'
|
||||
)
|
||||
with open(os.path.join(ASSETS, "atk-tray-symbolic.svg"), "w") as f:
|
||||
f.write(svg)
|
||||
|
||||
print("Wrote tray_icon.png, tray_icon_template.png, tray_icon.ico, atk-tray-symbolic.svg")
|
||||
print("Preview files: assets/_tray_preview_16.png, assets/_tray_preview_32.png (delete after checking)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package main
|
||||
|
||||
// No tray implementation on macOS yet - only verified on Linux (see
|
||||
// tray_linux.go) against this project's actual dev/test machine. ATK runs
|
||||
// fine without it; the frontend's close dialog just won't offer "Minimize to
|
||||
// tray" here (TrayAvailable() is false).
|
||||
|
||||
func initTray(a *App) {}
|
||||
func closeTray() {}
|
||||
func trayAvailable() bool { return false }
|
||||
339
tray_linux.go
339
tray_linux.go
|
|
@ -1,339 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"github.com/godbus/dbus/v5/introspect"
|
||||
)
|
||||
|
||||
// Linux tray, implemented as a minimal StatusNotifierItem (+ dbusmenu) server
|
||||
// directly over D-Bus. Ported from a working reference on this same machine:
|
||||
// Frequency's gui.py `_Tray` class (/opt/frequency/gui.py). GTK/Wails have no
|
||||
// portable tray API, and the common Go systray wrappers pull in libappindicator
|
||||
// or ship raw icon pixmap bytes - the latter is exactly what breaks GNOME's
|
||||
// AppIndicator extension (crashes with "can't access property 'clone'",
|
||||
// documented in Cascade's README). Using an IconName resolved via our own
|
||||
// IconThemePath (not the user's ~/.local/share/icons - nothing to cache-
|
||||
// invalidate) sidesteps that entirely, same as both reference apps do it.
|
||||
|
||||
//go:embed assets/atk-tray-symbolic.svg
|
||||
var trayIconSVG []byte
|
||||
|
||||
const (
|
||||
trayIconName = "atk-tray-symbolic"
|
||||
sniPath = dbus.ObjectPath("/StatusNotifierItem")
|
||||
menuPath = dbus.ObjectPath("/MenuBar")
|
||||
)
|
||||
|
||||
var (
|
||||
trayMu sync.Mutex
|
||||
trayState *linuxTray
|
||||
)
|
||||
|
||||
type linuxTray struct {
|
||||
app *App
|
||||
conn *dbus.Conn
|
||||
iconDir string
|
||||
ok bool
|
||||
}
|
||||
|
||||
func initTray(a *App) {
|
||||
trayMu.Lock()
|
||||
defer trayMu.Unlock()
|
||||
if trayState != nil {
|
||||
return
|
||||
}
|
||||
t := &linuxTray{app: a}
|
||||
if err := t.start(); err != nil {
|
||||
// Non-fatal: no session bus, no StatusNotifierWatcher (extension not
|
||||
// installed/enabled), etc. ATK just runs without a tray icon.
|
||||
t.ok = false
|
||||
}
|
||||
trayState = t
|
||||
}
|
||||
|
||||
func closeTray() {
|
||||
trayMu.Lock()
|
||||
defer trayMu.Unlock()
|
||||
if trayState == nil || trayState.conn == nil {
|
||||
return
|
||||
}
|
||||
_ = trayState.conn.Close()
|
||||
trayState = nil
|
||||
}
|
||||
|
||||
func trayAvailable() bool {
|
||||
trayMu.Lock()
|
||||
defer trayMu.Unlock()
|
||||
return trayState != nil && trayState.ok
|
||||
}
|
||||
|
||||
func (t *linuxTray) start() error {
|
||||
if err := t.writeIcon(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn, err := dbus.ConnectSessionBus()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.conn = conn
|
||||
|
||||
if err := t.exportSNI(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := t.exportMenu(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name := fmt.Sprintf("org.kde.StatusNotifierItem-%d-1", os.Getpid())
|
||||
reply, err := conn.RequestName(name, dbus.NameFlagDoNotQueue)
|
||||
if err != nil || reply != dbus.RequestNameReplyPrimaryOwner {
|
||||
return fmt.Errorf("could not own tray bus name")
|
||||
}
|
||||
|
||||
watcher := conn.Object("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher")
|
||||
call := watcher.Call("org.kde.StatusNotifierWatcher.RegisterStatusNotifierItem", 0, name)
|
||||
if call.Err != nil {
|
||||
// No watcher running (AppIndicator extension off, or a DE without one).
|
||||
return call.Err
|
||||
}
|
||||
t.ok = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeIcon materialises the embedded SVG to an app-owned directory and
|
||||
// advertises that directory via IconThemePath, rather than installing into
|
||||
// the user's shared ~/.local/share/icons (no icon-cache/gtk-update step
|
||||
// needed - the same approach Frequency uses via its bundled icons/ dir).
|
||||
func (t *linuxTray) writeIcon() error {
|
||||
cacheDir, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
cacheDir = os.TempDir()
|
||||
}
|
||||
dir := filepath.Join(cacheDir, "atk", "tray-icons")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
path := filepath.Join(dir, trayIconName+".svg")
|
||||
if err := os.WriteFile(path, trayIconSVG, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
t.iconDir = dir
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- org.kde.StatusNotifierItem -------------------------------------------
|
||||
|
||||
const sniIntrospectXML = `
|
||||
<interface name="org.kde.StatusNotifierItem">
|
||||
<property name="Category" type="s" access="read"/>
|
||||
<property name="Id" type="s" access="read"/>
|
||||
<property name="Title" type="s" access="read"/>
|
||||
<property name="Status" type="s" access="read"/>
|
||||
<property name="IconName" type="s" access="read"/>
|
||||
<property name="IconThemePath" type="s" access="read"/>
|
||||
<property name="Menu" type="o" access="read"/>
|
||||
<property name="ItemIsMenu" type="b" access="read"/>
|
||||
<method name="Activate"><arg type="i" direction="in"/><arg type="i" direction="in"/></method>
|
||||
<method name="SecondaryActivate"><arg type="i" direction="in"/><arg type="i" direction="in"/></method>
|
||||
<method name="ContextMenu"><arg type="i" direction="in"/><arg type="i" direction="in"/></method>
|
||||
<method name="Scroll"><arg type="i" direction="in"/><arg type="s" direction="in"/></method>
|
||||
</interface>`
|
||||
|
||||
type sniHandler struct{ t *linuxTray }
|
||||
|
||||
func (h *sniHandler) Activate(x, y int32) *dbus.Error {
|
||||
h.t.app.ToggleWindow()
|
||||
return nil
|
||||
}
|
||||
func (h *sniHandler) SecondaryActivate(x, y int32) *dbus.Error {
|
||||
h.t.app.ToggleWindow()
|
||||
return nil
|
||||
}
|
||||
func (h *sniHandler) ContextMenu(x, y int32) *dbus.Error { return nil }
|
||||
func (h *sniHandler) Scroll(delta int32, orientation string) *dbus.Error { return nil }
|
||||
|
||||
func (h *sniHandler) sniProps() map[string]dbus.Variant {
|
||||
return map[string]dbus.Variant{
|
||||
"Category": dbus.MakeVariant("ApplicationStatus"),
|
||||
"Id": dbus.MakeVariant("atk"),
|
||||
"Title": dbus.MakeVariant("ATK"),
|
||||
"Status": dbus.MakeVariant("Active"),
|
||||
"IconName": dbus.MakeVariant(trayIconName),
|
||||
"IconThemePath": dbus.MakeVariant(h.t.iconDir),
|
||||
"Menu": dbus.MakeVariant(menuPath),
|
||||
"ItemIsMenu": dbus.MakeVariant(false),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *sniHandler) Get(iface, propName string) (dbus.Variant, *dbus.Error) {
|
||||
v, ok := h.sniProps()[propName]
|
||||
if !ok {
|
||||
return dbus.Variant{}, dbus.NewError("org.freedesktop.DBus.Error.UnknownProperty", nil)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
func (h *sniHandler) GetAll(iface string) (map[string]dbus.Variant, *dbus.Error) {
|
||||
return h.sniProps(), nil
|
||||
}
|
||||
func (h *sniHandler) Set(iface, propName string, value dbus.Variant) *dbus.Error {
|
||||
return dbus.NewError("org.freedesktop.DBus.Error.PropertyReadOnly", nil)
|
||||
}
|
||||
|
||||
func (t *linuxTray) exportSNI() error {
|
||||
h := &sniHandler{t: t}
|
||||
if err := t.conn.Export(h, sniPath, "org.kde.StatusNotifierItem"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := t.conn.Export(h, sniPath, "org.freedesktop.DBus.Properties"); err != nil {
|
||||
return err
|
||||
}
|
||||
node := `<node>` + sniIntrospectXML + `</node>`
|
||||
return t.conn.Export(introspect.Introspectable(node), sniPath, "org.freedesktop.DBus.Introspectable")
|
||||
}
|
||||
|
||||
// --- com.canonical.dbusmenu -------------------------------------------
|
||||
|
||||
const menuIntrospectXML = `
|
||||
<interface name="com.canonical.dbusmenu">
|
||||
<property name="Version" type="u" access="read"/>
|
||||
<property name="Status" type="s" access="read"/>
|
||||
<method name="GetLayout">
|
||||
<arg type="i" direction="in"/><arg type="i" direction="in"/><arg type="as" direction="in"/>
|
||||
<arg type="u" direction="out"/><arg type="(ia{sv}av)" direction="out"/>
|
||||
</method>
|
||||
<method name="GetGroupProperties">
|
||||
<arg type="ai" direction="in"/><arg type="as" direction="in"/>
|
||||
<arg type="a(ia{sv})" direction="out"/>
|
||||
</method>
|
||||
<method name="Event">
|
||||
<arg type="i" direction="in"/><arg type="s" direction="in"/>
|
||||
<arg type="v" direction="in"/><arg type="u" direction="in"/>
|
||||
</method>
|
||||
<method name="AboutToShow"><arg type="i" direction="in"/><arg type="b" direction="out"/></method>
|
||||
</interface>`
|
||||
|
||||
// menuNode mirrors dbusmenu's recursive (ia{sv}av) layout struct: id,
|
||||
// properties, children (each child a variant wrapping another menuNode).
|
||||
type menuNode struct {
|
||||
ID int32
|
||||
Props map[string]dbus.Variant
|
||||
Children []dbus.Variant
|
||||
}
|
||||
|
||||
type groupProps struct {
|
||||
ID int32
|
||||
Props map[string]dbus.Variant
|
||||
}
|
||||
|
||||
// menuItem ids: 0 is reserved for the menu root (GetLayout's implicit parent),
|
||||
// so item/separator ids start at 1 and must all stay distinct from it and
|
||||
// from each other.
|
||||
type menuItemDef struct {
|
||||
id int32
|
||||
label string // empty => separator
|
||||
}
|
||||
|
||||
func menuItems() []menuItemDef {
|
||||
return []menuItemDef{
|
||||
{1, "Show / Hide ATK"},
|
||||
{2, ""}, // separator
|
||||
{3, "Quit ATK"},
|
||||
}
|
||||
}
|
||||
|
||||
func menuItemProps(item menuItemDef) map[string]dbus.Variant {
|
||||
if item.label == "" {
|
||||
return map[string]dbus.Variant{"type": dbus.MakeVariant("separator")}
|
||||
}
|
||||
return map[string]dbus.Variant{
|
||||
"label": dbus.MakeVariant(item.label),
|
||||
"enabled": dbus.MakeVariant(true),
|
||||
}
|
||||
}
|
||||
|
||||
type menuHandler struct{ t *linuxTray }
|
||||
|
||||
func (h *menuHandler) GetLayout(parentID, recursionDepth int32, propertyNames []string) (uint32, menuNode, *dbus.Error) {
|
||||
var children []dbus.Variant
|
||||
for _, item := range menuItems() {
|
||||
children = append(children, dbus.MakeVariant(menuNode{
|
||||
ID: item.id,
|
||||
Props: menuItemProps(item),
|
||||
Children: nil,
|
||||
}))
|
||||
}
|
||||
root := menuNode{
|
||||
ID: 0,
|
||||
Props: map[string]dbus.Variant{"children-display": dbus.MakeVariant("submenu")},
|
||||
Children: children,
|
||||
}
|
||||
return 1, root, nil
|
||||
}
|
||||
|
||||
func (h *menuHandler) GetGroupProperties(ids []int32, propertyNames []string) ([]groupProps, *dbus.Error) {
|
||||
wanted := make(map[int32]bool, len(ids))
|
||||
for _, id := range ids {
|
||||
wanted[id] = true
|
||||
}
|
||||
var out []groupProps
|
||||
for _, item := range menuItems() {
|
||||
if wanted[item.id] {
|
||||
out = append(out, groupProps{ID: item.id, Props: menuItemProps(item)})
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (h *menuHandler) Event(id int32, eventID string, data dbus.Variant, timestamp uint32) *dbus.Error {
|
||||
if eventID != "clicked" {
|
||||
return nil
|
||||
}
|
||||
switch id {
|
||||
case 1:
|
||||
h.t.app.ToggleWindow()
|
||||
case 3:
|
||||
h.t.app.QuitApp()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *menuHandler) AboutToShow(id int32) (bool, *dbus.Error) { return false, nil }
|
||||
|
||||
func (h *menuHandler) menuProps() map[string]dbus.Variant {
|
||||
return map[string]dbus.Variant{
|
||||
"Version": dbus.MakeVariant(uint32(3)),
|
||||
"Status": dbus.MakeVariant("normal"),
|
||||
}
|
||||
}
|
||||
func (h *menuHandler) Get(iface, propName string) (dbus.Variant, *dbus.Error) {
|
||||
v, ok := h.menuProps()[propName]
|
||||
if !ok {
|
||||
return dbus.Variant{}, dbus.NewError("org.freedesktop.DBus.Error.UnknownProperty", nil)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
func (h *menuHandler) GetAll(iface string) (map[string]dbus.Variant, *dbus.Error) {
|
||||
return h.menuProps(), nil
|
||||
}
|
||||
func (h *menuHandler) Set(iface, propName string, value dbus.Variant) *dbus.Error {
|
||||
return dbus.NewError("org.freedesktop.DBus.Error.PropertyReadOnly", nil)
|
||||
}
|
||||
|
||||
func (t *linuxTray) exportMenu() error {
|
||||
h := &menuHandler{t: t}
|
||||
if err := t.conn.Export(h, menuPath, "com.canonical.dbusmenu"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := t.conn.Export(h, menuPath, "org.freedesktop.DBus.Properties"); err != nil {
|
||||
return err
|
||||
}
|
||||
node := `<node>` + menuIntrospectXML + `</node>`
|
||||
return t.conn.Export(introspect.Introspectable(node), menuPath, "org.freedesktop.DBus.Introspectable")
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package main
|
||||
|
||||
// No tray implementation on Windows yet - only verified on Linux (see
|
||||
// tray_linux.go) against this project's actual dev/test machine. ATK runs
|
||||
// fine without it; the frontend's close dialog just won't offer "Minimize to
|
||||
// tray" here (TrayAvailable() is false).
|
||||
|
||||
func initTray(a *App) {}
|
||||
func closeTray() {}
|
||||
func trayAvailable() bool { return false }
|
||||
Loading…
Add table
Add a link
Reference in a new issue