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
View file

@ -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
View file

@ -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 0100 **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 0100 privacy score and AF 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
View 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.

View file

@ -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 0100 privacy score with an AF 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
View file

@ -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)
}

View file

@ -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

View file

@ -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
}

View file

@ -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{

View file

@ -1 +1 @@
5044e767932c804d924f539d25c7ab46
bfb47127747332de1e5119ef153cca76

View file

@ -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}

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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>
)
}

View file

@ -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} />}

View file

@ -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>
)

View file

@ -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>
)
}

View file

@ -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 && (

View file

@ -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>
)}

View file

@ -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>

View file

@ -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>
)

View file

@ -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')
}

View file

@ -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[] {

View file

@ -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?.()
}

View file

@ -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 }
)
}

View file

@ -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

View file

@ -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'

View file

@ -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

View file

@ -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>

View file

@ -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;
}

View file

@ -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
View file

@ -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
View file

@ -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=

View file

@ -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>"

View file

@ -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
}

View file

@ -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()

View file

@ -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 }

View file

@ -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")
}

View file

@ -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 }