Skip to content

Translations update from Hosted Weblate - #290

Merged
ravindu644 merged 1 commit into
ravindu644:mainfrom
weblate:weblate-droidspaces-strings
Aug 24, 2026
Merged

Translations update from Hosted Weblate#290
ravindu644 merged 1 commit into
ravindu644:mainfrom
weblate:weblate-droidspaces-strings

Conversation

@weblate

@weblate weblate commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Translations update from Hosted Weblate for Droidspaces/Strings.

It also includes following components:

Current translation status:

Weblate translation status

Currently translated at 100.0% (553 of 553 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 98.3% (544 of 553 strings)

Co-authored-by: Jung Chi Wang <snowwolf725@gmail.com>
Co-authored-by: MonkiFantasy <execvp@qq.com>
Translate-URL: https://hosted.weblate.org/projects/droidspaces/strings/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/droidspaces/strings/zh_Hant/
Translation: Droidspaces/Strings
@weblate
weblate force-pushed the weblate-droidspaces-strings branch from 9e1b210 to 391bb45 Compare August 24, 2026 04:34
@ravindu644
ravindu644 merged commit dfb6eca into ravindu644:main Aug 24, 2026
omrxm18 added a commit to omrxm18/Droidspaces-OSS that referenced this pull request Aug 25, 2026
* Korean Resource Activation  (#269)

* 한국어 추가 드가자

* 어라 리소스가 있네

* 흠

* 멍청하다

* plurals.xml 업데이트

* app: drag to reorder upstream interfaces in container config

Selected upstream interfaces could previously only be added or removed,
not reordered, even though order matters (first entry is the primary
upstream, the rest are fallbacks). Reuses the same drag-to-reorder
pattern as the Auto Boot Priority screen: a LazyColumn driven by
sh.calvin.reorderable's live onMove callback, so the reordered list
propagates immediately as items cross during the drag rather than only
after a settle animation.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* fix: reinstall host network rules when netd flushes them

Android's netd rebuilds its entire netfilter and policy-routing state
whenever it restarts, and a system_server crash ("soft reboot") is enough
to trigger it.  It flushes the iptables built-in chains and the FIB rule
table wholesale, taking our rules with them, which silently strands every
running container.

The container keeps running and the host's own connectivity comes back,
so the failure looks like a Droidspaces bug: with the policy rules gone
nothing consults the main table for 172.28.0.0/16, so host traffic to a
container falls through to "from all unreachable" and SSH into the
container hangs, while the missing MASQUERADE and FORWARD ACCEPT rules
leave the container with no WAN.  Only restarting the container fixed it.

Setup installed these rules exactly once and assumed they would survive.
Make the route monitor reconcile them instead:

- Split the rule installation out of setup into install_policy_rules()
  and install_netfilter_rules(), each the single source of truth for its
  half, called both at container start and from the monitor.  This also
  collapses the bridge/bridgeless branching in setup_veth_host_side()
  into one call.
- Re-assert the three policy rules on every monitor cycle.  They are
  cheap netlink round-trips and ds_nl_add_rule4() maps EEXIST to success,
  so this is a silent no-op when nothing is wrong.
- Gate the netfilter set behind ds_ipt_host_rules_present(), a new
  fork-free probe that checks INPUT/FORWARD ACCEPT and MASQUERADE in two
  table reads.  ds_ipt_ensure_mss_clamp() and the port-forward helpers
  shell out to the iptables binary, so they must not run every cycle.
  Probing every rule rather than one canary matters because netd is not
  the only thing that removes them: firewall apps rewrite the filter
  chains while leaving nat's MASQUERADE untouched.
- Snapshot the topology and port-forward list in setup_veth_host_side()
  so the monitor thread can rebuild the rules without reading cfg, which
  the reboot path rewrites in place.  ds_ipt_add_portforwards() now takes
  the port-forward array directly instead of the whole config.

Recovery is automatic within one 1.5s heartbeat, or immediately on the
netlink event, and every helper is idempotent so no rules are duplicated.
Verified on a device by killing system_server and by deleting each rule
individually.

Fixes: https://github.com/ravindu644/Droidspaces-OSS/issues/273
Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* fix: detect MSS clamp and port-forward removal in the rule probe

ds_ipt_host_rules_present() only covered the four connectivity rules, so
a flush that spared them went unnoticed: deleting the mangle MSS clamp,
or flushing just nat PREROUTING, left the container reachable but with a
silent MTU blackhole or dead --port mappings until it was restarted.

Both are detectable from the entry blob without forking, so extend the
probe to cover them:

- mangle POSTROUTING is checked for a TCPMSS target.  An unreadable
  mangle table deliberately leaves the verdict untouched instead of
  failing the probe - MSS clamping is an MTU guard rather than
  connectivity, and bailing out there would stop us reconciling the
  rules that do matter.
- nat PREROUTING is checked for a DNAT target, but only when the
  container actually has --port mappings.

Both match on target name, since the --dport selector lives in an xt
match payload we do not parse.  They therefore prove that MSS clamping
and port forwarding are still installed, not that every individual
mapping is: a single mapping deleted on its own is still not detected,
while anything that removes them as a group now is.

Reinstalling port forwards on a partially flushed table also had to be
made safe.  Those rules go in through the iptables binary, which has no
idempotency check, so re-adding them stacked a second copy of every
survivor - and removal only issues one -D per recorded rule, so the
extras leaked on container stop.  ds_ipt_add_portforwards() now drops
whatever the state file recorded before reinstalling; with no state file
(first install) that is a no-op.

Verified on a device: deleting the MSS clamp alone and flushing nat
PREROUTING alone both self-heal within one heartbeat, three consecutive
full flushes leave exactly one copy of every rule, and a forwarded port
still reaches the container afterwards.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* formatted src/seccomp.c -s

* src: app: re-ordered korean locale in the locales_config.xml

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* Translated using Weblate (Chinese (Simplified Han script)) (#274)

Currently translated at 100.0% (544 of 544 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (544 of 544 strings)



Translate-URL: https://hosted.weblate.org/projects/droidspaces/strings/zh_Hans/
Translation: Droidspaces/Strings

Co-authored-by: seriaTvT <63958697+seriaTvT@users.noreply.github.com>
Co-authored-by: 月有陰晴圓缺 <moonleeeaf@petalmail.com>

* droidspaces: bump v6.5.0

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* feat: add terminal dark mode toggle in Settings (#275)

* Add terminal dark mode toggle in settings

Add a 'Terminal Dark Mode' switch in the Appearance settings section that
renders only the terminal page in dark colors (dark background, light
foreground/cursor, dark virtual keys bar) while the rest of the app keeps
the light theme. The preference is independent of the app-wide theme.

* Fix terminal dark mode: set TerminalView background color

The termux renderer only paints cell backgrounds; the full-screen default
background comes from the View itself. Set the TerminalView background to
dark when terminal dark mode is enabled, otherwise the terminal area stays
white while only the foreground/cursor colors change.

* Use classic termux white-on-black colors for terminal dark mode

Change the terminal dark mode foreground/background to pure white (#FFFFFF)
on pure black (#000000), matching termux's default scheme instead of the
previous grayish text (#E2E2E6) on dark gray background (#121212).

* Fix unresolved Color.White/Black references for cloud build

Use Compose's Color.White / Color.Black constants instead of the
Android android.graphics.Color.WHITE/BLACK uppercase aliases.

* fix: only apply background overrides in terminal dark mode (#276)

* Add terminal dark mode toggle in settings

Add a 'Terminal Dark Mode' switch in the Appearance settings section that
renders only the terminal page in dark colors (dark background, light
foreground/cursor, dark virtual keys bar) while the rest of the app keeps
the light theme. The preference is independent of the app-wide theme.

* Fix terminal dark mode: set TerminalView background color

The termux renderer only paints cell backgrounds; the full-screen default
background comes from the View itself. Set the TerminalView background to
dark when terminal dark mode is enabled, otherwise the terminal area stays
white while only the foreground/cursor colors change.

* Use classic termux white-on-black colors for terminal dark mode

Change the terminal dark mode foreground/background to pure white (#FFFFFF)
on pure black (#000000), matching termux's default scheme instead of the
previous grayish text (#E2E2E6) on dark gray background (#121212).

* Fix unresolved Color.White/Black references for cloud build

Use Compose's Color.White / Color.Black constants instead of the
Android android.graphics.Color.WHITE/BLACK uppercase aliases.

* fix: only apply background overrides in terminal dark mode

Light mode now keeps the pre-PR behavior: the Activity background shows
through the TerminalView and the default background color is left untouched.
The full-screen background paint and the default-background override only
apply when "Terminal Dark Mode" is enabled.

* garaho: activate IME for 12-key terminal input (#280)

12-key hardware keypads can depend on the active IME to translate
numeric key events into composed text. Droidspaces currently suppresses
showSoftInput() whenever Android reports a hardware keyboard, causing
12-key devices to deliver only raw numeric key events to TerminalView.

Allow soft-input activation for KEYBOARD_12KEY and activate the IME
after the terminal receives focus.

This keeps the existing behavior for full hardware keyboards while
allowing 12-key devices to use their IME-backed text input path.

* app: fix: accept NixOS rootfs tarballs in validator

The rootfs validator required bin, sbin, etc and usr at the top level of
the tarball, which NixOS images do not have: /bin, /sbin and /usr are
created by the activation script on first boot, so installing a NixOS
image aborted with "Not a Linux rootfs - missing top-level dirs: bin
usr".

Accept a top-level nix/ store on its own, the same marker
post_extract_fixes.sh already uses to detect NixOS and skip its fixes.

The check is tracked with an ok flag instead of printing OK from the
matching rule, because awk still runs the END block after exit; a bare
print/exit in the nix branch emitted both OK and the MISSING line and
fell through to the "corrupt archive" case.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: refactor: route the last three dialogs through DialogFooterRow

DT-5 introduced DialogFooterRow but three dialogs kept their own copy of the
same two-Surface cancel/confirm row: the bind-mount destination dialog in
ContainerConfigForm, and SparseSizeDialog and UninstallConfirmationDialog in
ContainersScreen. Each was the same ~28 lines with different labels, so a
change to the footer's look had to be made in four places.

Ported from VizXtreme's #277 on the ui branch, which is otherwise dropped.

One deliberate difference: the uninstall dialog drew its confirm label Bold
and its cancel label SemiBold. DialogFooterRow applies one weight to both, so
both are SemiBold now; the destructive emphasis is carried by the error
colour rather than the font weight, and adding a second weight parameter for
a single caller is not worth the API.

Co-authored-by: VizXtreme <sk06pandit@gmail.com>
Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: refactor: round progress bar ends with strokeCap, not a clip

Both progress bars rounded their ends by clipping the whole composable to a
RoundedCornerShape. LinearProgressIndicator takes a strokeCap for exactly
this, and it rounds the track and the indicator separately rather than
shaving the corners off the composable's bounds.

Ported from VizXtreme's #277 on the ui branch, which is otherwise dropped.

Co-authored-by: VizXtreme <sk06pandit@gmail.com>
Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: fix: block the resize dialog's Continue on the current image size

The size field opens pre-filled with the image's existing size, and Continue
was enabled for any value in 4..512 - including that one. Tapping it ran a
resize to the size the image already was: no progress, no change, no error,
just a no-op that looked like a broken button.

Continue is now disabled when the entered size equals the image's current
size, and the field explains why instead of showing the range hint, which
was misleading for a value that is inside the range.

The guard is passed in as currentSize and is null for a migrate: that path
shares this dialog but has no image yet, so its default size is a legitimate
choice rather than a no-op.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: fix: centre the rootfs repo sheet's loading and empty states

The sheet skips its partially-expanded state, so it is always full height,
but its column only filled the width. Every state below the header was laid
out at its own natural height and stacked at the top, leaving the spinner
sitting near the title with most of the sheet empty beneath it. The column
fills the sheet now and the content area takes the height the header and
the navigation-bar spacer leave behind, so the loading state, the load
error and the no-search-results state all centre in the real empty area
instead of inside a hardcoded 240dp box.

FullScreenLoading had its own version of the same problem: the message sat
in a wrap-content column, so a message long enough to wrap made the column
as wide as the text while the spinner stayed centred within it, leaving the
two out of line. The text fills the column now, so both share one centre
line, with horizontal padding to keep a long message off the edges. Its
style also drops from bodyLarge to bodyMedium - it reads as a status line
under a spinner, not as body copy. That part affects the init service list,
the unit detail screen and the override editor too, which is the point: the
repo sheet is meant to look exactly like the services fetch.

Fetching the distro list previously showed a bare medium spinner with no
label, so a slow repo was indistinguishable from a stuck sheet. It now uses
FullScreenLoading with a message naming what is being fetched.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: fix: wrap the Panel usage chip instead of squeezing it off screen

The CPU and RAM readings sat in a plain Row. A Row measures each child
against whatever width the previous child left behind, so once the two no
longer fit side by side - a watch, a small phone, or simply a locale whose
labels are longer - CPU took what it needed and RAM was handed a sliver.
Its text then wrapped inside that sliver into a tall column of fragments,
which is what stretched the chip's height and pushed the card past the
bottom of the screen.

FlowRow moves RAM onto a second line when the line is full, which is what
the layout wanted in the first place. Same component the upstream interface
list already uses for this.

ContainerDetailsScreen shows the same two readings but lays them out with
SyncedGrid, so it never had the bug and is untouched.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* Translated using Weblate (Czech) (#278)

Currently translated at 100.0% (548 of 548 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (548 of 548 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (548 of 548 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (546 of 546 strings)





Translate-URL: https://hosted.weblate.org/projects/droidspaces/strings/cs/
Translate-URL: https://hosted.weblate.org/projects/droidspaces/strings/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/droidspaces/strings/zh_Hant/
Translation: Droidspaces/Strings

Co-authored-by: Jung Chi Wang <snowwolf725@gmail.com>
Co-authored-by: Michal Várady <miko.vaji@gmail.com>
Co-authored-by: MonkiFantasy <execvp@qq.com>

* app: Add UIJ permissions and battery exemption helper (#282)



on new source downloadmanager delegates foreground-visible downloads to jobscheduler as user-initiated jobs (uij / uidt).
because droidspaces was missing android.permission.RUN_USER_INITIATED_JOBS and FOREGROUND_SERVICE_DATA_SYNC in its androidmanifest.xml, jobscheduler treated the job as an unprivileged background job.when jobscheduler started downloadjobservice, params.getnetwork() returned null.
in realsystemfacade.java, getnetwork(params) returned params.getnetwork() directly with no fallback.
downloadthread.java then immediately threw stoprequestexception(status_waiting_for_network, "no network associated with requesting uid") and kept rescheduling the job in a loop (status 190), causing rootfs repo downloads to stay stuck at 0%.

Change-Id: I11f2b38936a1772d8338f675e04843903126414d

* feat(ui): UI improvements - session tabs, dialog buttons, animations, and terminal dark mode fix (#283)

- update button radius to 14dp for Copy login, Manage, and Terminal buttons
- add common button outline and even padding to dialog footer buttons
- update popup dialog buttons to use translucent container fill and visible accent outline
- wrap dialog footer buttons in outer segment Surface container with common outline
- refactor terminal screen (centered title, double outline tabs, long press confirmation, slide animations)
- update terminal tab double-outline segment container shape and add bidirectional slide animation
- separate double-outline card per session tab and polished 48dp bidirectional slide animation
- fix terminal session tabs and animation consistency
- separate outline per session tab, match button radius and padding
- restyle session tabs to match Start button pattern
- double outline session tabs matching Start button pattern
- fix white flicker on tab switch when terminal is dark in light mode

* docs: sync Chinese documentation terminology (#285)

* Translated using Weblate (Bengali) (#284)

Currently translated at 10.9% (60 of 548 strings)

Translated using Weblate (Bengali)

Currently translated at 33.3% (1 of 3 strings)

Added translation using Weblate (Bengali)

Added translation using Weblate (Bengali)



Translate-URL: https://hosted.weblate.org/projects/droidspaces/plurals/bn/
Translate-URL: https://hosted.weblate.org/projects/droidspaces/strings/bn/
Translation: Droidspaces/Plurals
Translation: Droidspaces/Strings

Co-authored-by: ShoyebOP <starshoyeb@gmail.com>

* app: revert: restore terminal session tab design

Bring back the plain tab strip with its inline close button and drop the
long-press close confirmation dialog added in c3a77851 -- the restyled
double-outline tabs took up too much space on screen. The centered title,
slide animation and terminal dark mode background fix from that commit
are kept.

Also drop the now-unused confirmContentColor parameter from
DialogFooterRow and its call sites, clearing a compiler warning.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* fix: fail closed in ds_peer_in_pidns peer namespace guard

ds_peer_in_pidns() is the host-net container guard in ds_peer_authorized():
it must only authorize peers proven to share our PID namespace. It returned
1 ("fail open, never wrongly deny") whenever membership could not be
determined -- peer_pid <= 0, or readlink of /proc/<pid>/ns/pid failing.

That is a root container escape. SO_PEERCRED remembers the connecting pid
forever; a peer can hand off its connection and get reaped, so the recorded
pid is dead and readlink ENOENTs. The old code then failed open and
authorized the caller as uid 0, after which the daemon re-execs droidspaces
as root on the host. A burst of connections recycles dead pids faster than
the kernel, reliably landing several checks on ENOENT.

Invert both branches to fail closed: a pid that does not translate into our
namespace (<= 0) and any readlink failure now deny. Only a positively
confirmed same-namespace peer is authorized.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: revert: restore terminal top bar and tab switch animation

Drop the remaining terminal UI changes from c3a77851: the centered top app
bar goes back to the leading-aligned TopAppBar, and the directional slide
on tab switch goes back to a plain fade. This also removes the tab index
tracking state (currentTabIndex/previousTabIndex/isMovingForward) that only
existed to feed the slide direction.

The terminal dark mode background fix from that commit is kept.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* docs: add AGENTS.md contributor contract and rewrite CONTRIBUTING

Nothing in this repository told a contributor, human or agent, what already
exists here. That is how we ended up with a 700 line duplicate of the container
config form, three copies of the init system screen, one bottom bar pasted seven
times, and a class of host root command injection caused by every class
hand-rolling its own Shell.cmd string.

AGENTS.md is the short form: build commands, commit rules, style rules, and the
security choke points that must never be bypassed. CLAUDE.md is a symlink to it,
since Claude Code does not read AGENTS.md natively.

CONTRIBUTING.md keeps the platform rules that were already there, the 3.10 kernel
floor, is_android() guards, SoC coverage, PR requirements, and adds a reuse
inventory of every shared component and helper in both halves of the project,
built from a full sweep of Android/app/ and src/. It also lists the duplicates
that still exist today, so contributors extend the shared version instead of
adding a third copy.

New rules recorded in both files:

  - no em-dashes anywhere
  - no ASCII banner comments, no ----- and no =====
  - comments explain why, not what
  - ten lines that work beat a hundred that do the same thing
  - make format before committing C changes
  - sign off every commit
  - no Co-Authored-By trailer for AI agents

CONTRIBUTING_CN.md is rewritten to mirror the new English file.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* refactor: drop banner comments from the shared header

The 29 dash-rule banners that split droidspace.h into one section per .c file
become plain one-line comments. The section labels are what made the header
navigable, so they stay; only the rules around them go.

Also reattaches the "Container configuration" comment to struct ds_config. It
had drifted 40 lines up and was sitting on the port-forwarding section, which
the banners were hiding.

No prototype or macro changes. make native is clean.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* refactor: drop banner comments and em-dashes from the core runtime

utils.c alone carried 22 dash-rule banners, including five nested inside
show_container_usage. Label-only banners collapse to a one-line comment; the
ones carrying real explanation keep their prose and its hanging indentation,
losing only the rules. check.c had a variant with a stray + on every line and
an unclosed opening rule, now a plain block comment.

Also replaces the em-dashes in user-facing strings: the welcome banner, the
--help header, the requirements check header, and two boot abort messages,
which now read "Boot aborted: ..." instead.

No logic changes. make format is clean and make native builds.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* refactor: drop banner comments from the container runtime

Same treatment as the core files across container.c, mount.c, cgroup.c, pid.c,
seccomp.c, virtualize.c, monitor.c and terminal.c.

container.c had one banner with the rule in the middle rather than at the end,
splitting the LXC-style session setup title from the vhangup explanation below
it. That is now one block comment with the title on the first line.

No logic changes. make native builds.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* net: drop banner comments and em-dashes

The networking layer carried the most of them, 80 banners across dhcp.c,
iptables.c, netlink.c and network.c. Label-only ones collapse to a single line,
the ones explaining a protocol detail or a kernel quirk keep their prose.

One label in netlink.c was long enough that clang-format wrapped its closer onto
its own line, so it is reworded to fit.

Also replaces the em-dashes in four [IPT] retry log lines, which now read
"EAGAIN (attempt remaining=%d), refetching ..." and "failed, giving up".

No logic changes. make native builds.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* refactor: drop banner comments from the Android backend

android.c used the same dash-rule blocks as the rest of the tree. The three
graphics and audio daemons used a different shape, a single line reading
/* ---- label ---- */, which becomes /* label */.

No logic changes. make native builds.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: refactor: drop em-dashes and banner comments

Replaces every em-dash in the Kotlin sources, 47 of them across 29 files, almost
all in comments and KDoc. Most become a comma; the numbered list in
GatewaySettingsSection becomes 1. 2. 3., and five that turned into comma splices
are reworded. The two user-facing validation fallbacks now read "Name too long,
max N characters".

Also removes the dash-rule banners in LocaleHelper and PreferencesManager, the
only two in the app.

Comments only, no behaviour change. ./build.sh is green.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* docs: drop em-dashes from the English documentation

Four occurrences across the networking guide and the supported devices table.
The Chinese translations still use the full-width dash, which is correct Chinese
punctuation and is Weblate-managed, so they are left alone.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: refactor: drop references to the deleted findings files

Twenty comments still pointed at FINDINGS_APP_VULN.md and
FINDINGS_APP_DUCT_TAPES.md by finding id (V1, V2, V7, V10, V11, V12, V16, DT-4,
DT-5, DT-6, DT-9). Both files were removed from the repo after the July refactor
landed, so the ids resolve to nothing a reader can open.

Every one of those comments already explains its own reasoning, why the service
name is allow-listed, why the validator fails closed, why the session client is
swapped on dispose, so only the dead pointer goes. The one exception is the
chmod failure branch in ContainerInstaller, whose comment was just the pointer;
it now says what failing closed means there.

Comments only, no behaviour change. ./build.sh is green.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: refactor: drop the last findings reference from the manifest resources

network_security_config.xml still pointed at FINDINGS_APP_VULN V13. It sits
outside the Kotlin sources so the previous pass missed it. The comment already
explains why cleartext is blocked app-wide, so only the pointer goes.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: fix: list directories without parsing ls output

The picker inferred entry type from the suffix ls -F appends, which cannot
answer the only question it needs answered. ls -F marks a symlink with @
even when it points at a directory, so the entry rendered as a file and
could not be opened, which made /sdcard and /storage/self/primary
unreachable. The suffix stripping also ate a real trailing *@=|> from a
filename, and without -a hidden directories were invisible entirely.

Ask the shell instead. [ -d ] follows symlinks, so a link to a directory
is reported as one, and a d/ or f/ prefix survives any character a name
can legally contain. Still one root round-trip per directory.

Also fix the flicker when typing a path into the search box. Two
LaunchedEffects were writing the item list: the currentPath one listed the
directory, and the searchQuery one set currentPath (retriggering that) and
then listed it a second time itself, so the later of two concurrent
listings won. The search effect now only sets currentPath. Its existence
probe was also running on every keystroke, so a debounce goes in front of
it; LaunchedEffect already cancels the previous run per keystroke.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: feat: probe filesystem capability for a storage path

A container rootfs cannot go just anywhere. A directory rootfs stores
ownership, modes and symlinks directly, so it needs a filesystem that
implements the Linux permission model; FUSE, sdcardfs, exFAT and NTFS
cannot represent one. A sparse image carries its own ext4 inside a single
file, so it only needs the filesystem underneath to hold that file, which
FAT32 cannot do past 4GB. Nothing in the app could tell these apart.

StorageChecker.probe() answers it from one read of /proc/mounts, taking
the longest mount point that prefixes the target. Longest-prefix is what
makes it correct for a subdirectory: matching the path itself only works
when the path is a mount point, so a nested directory would report as
neither noexec nor read-only. It also keeps /storage/emulated/0/Android/data
(f2fs) apart from /storage/emulated/0 (fuse) directly above it.

Options are split on commas rather than substring-matched, so an ext4
volume mounted errors=remount-ro is not mistaken for read-only.

nodev is deliberately not checked. setup_dev() in src/mount.c mounts
<rootfs>/dev as its own devtmpfs, so device nodes never land on the
filesystem holding the rootfs, and Android mounts /data itself nodev.
Checking it would refuse directory mode at the default install location.

FAT32 is refused outright rather than clamped below its 4GB file limit. A
container that cannot grow past 4GB is not worth the extra branch, and
reformatting is the honest answer.

getFreeSpaceGB and hasSufficientSpace take a path instead of assuming
/data, so the space check runs against the volume actually chosen.

Kernel interfaces and RAM-backed mounts are refused too: tmpfs, ramfs,
rootfs, devtmpfs, devpts, proc, sysfs and the rest of the pseudo
filesystems are either not storage at all or vanish on reboot. tmpfs gets
its own message because it is the one people actually land on, root's
/storage being an empty tmpfs that looks like a real destination.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: feat: install a container rootfs to a chosen directory

Containers were pinned to /data/local/Droidspaces/Containers, so a large
SD card or USB-OTG drive could not be used at all.

The backend already supported this. config.c parses rootfs_path= as any
absolute path, ds_config_validate access()-checks it, and start_rootfs()
writes the config both next to the rootfs and mirrored into the workspace.
The app was the only thing standing in the way: installContainer() rebuilt
rootfsPath from the container name instead of using the one already
present on the ContainerInfo it was handed, so whatever the caller
resolved was discarded. It now honours that path, and getRootfsPath and
getSparseImagePath take an optional base directory to produce it.

Only the rootfs moves. The container directory under CONTAINERS_BASE_PATH
still holds the config, .env and pidfile, so a container stays listed and
inspectable while its storage is unplugged.

The wizard's existing storage step gains the destination, because the
filesystem is what decides the rest of that screen: StorageChecker.probe()
disables directory mode on a volume that cannot represent one and blocks
Next entirely on a volume that cannot hold a container, quoting the reason.
Choosing the folder reuses FilePickerDialog, which already browses from /
through a root shell and so reaches /mnt/media_rw and /data/media without
needing the Storage Access Framework.

installContainer re-runs the same probe before extracting rather than
trusting the wizard, since it is also the path a non-wizard caller takes.
It fails closed.

Every path in the install chain now goes through
ContainerCommandBuilder.quote(). These were interpolated into shell
strings with literal double quotes, which was survivable while they all
derived from a validated container name, and is a root command injection
the moment the destination comes from a picker.

Migrate and Resize are hidden for a container whose rootfs is outside its
container directory: sparsemgr.sh derives <baseDir>/rootfs itself and is
handed the container directory, so it would operate on the wrong path.
Repointing it belongs with the move-container work.

A destination with less than the 4GB minimum free is refused, in the
wizard and again in the installer, where it used to be a warning that let
the install proceed and fail during extraction. Free space that cannot be
determined at all is not treated as insufficient: blocking there would
break installs on a device where both stat and df fail.

Uninstall now removes the rootfs as well. It only ever deleted the
container directory, so a container installed elsewhere left its rootfs
behind. The path is re-read from container.config rather than taken from
the in-memory listing, because the backend rewrites that file on every
start. It deletes only an entry named exactly rootfs or rootfs.img, so a
truncated or hand-edited path cannot turn this into an rm -rf of something
else, and the folder above it goes with a plain rmdir that fails
harmlessly when another container still lives there.

The storage layout and the plumbing shape come from zen's PR #279: only
the rootfs relocates while the config, .env and pidfile stay under
CONTAINERS_BASE_PATH, destinations are namespaced as <baseDir>/<name>/,
the path helpers take an optional base directory, and the installer
decides an external destination by comparing the rootfs parent against
the container directory. The rest of that PR was reimplemented.

Co-authored-by: zen <rishi.gothic@gmail.com>
Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: fix: point the storage step's Next button at the summary

The button on the storage step read "Next: Configuration" while navigating
to the installation summary. It was copied from the container name step,
which does go to Configuration, and never updated.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: fix: align the wizard step headings with each other

ContainerConfigForm places its heading at top = 8.dp, so "Container
Options" on the Configuration step sat 16dp higher than the headings on
every other step, which all used 24.dp. Moving between steps shifted the
title up and down.

The storage step also applied its padding outside verticalScroll rather
than inside it, unlike the other three, so its padding did not scroll with
the content. All four now use the same chain as the form: scroll, then
horizontal, then top.

The summary step keeps its bottom padding so the last card still clears
the bottom bar.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: refactor: drop unreferenced string resources

Ten resources had no reference left anywhere: not from Kotlin, not from a
layout, the manifest, a theme or another string. Most predate this branch;
sparse_image_configuration was orphaned by renaming the wizard's storage
step.

Gone: clear, procd_detected, procd_services_placeholder_desc,
repo_error_network, repo_no_custom, sparse_image_configuration, tcp, udp,
unknown_error, and the container_count plural. tcp and udp looked live at
a glance, but PortForwardingList builds its dropdown from raw
listOf("tcp", "udp") literals and never reads the resources.

Removed from all 29 locales, not just values/, so Weblate stops asking
translators for strings the app can no longer reach.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: refactor: drop the container name length limit

Container names were capped at 17 characters. The constant's own comment
gave the derivation: 63, minus the default container directory, minus
"/rootfs.img". It existed because Android's mount is toybox, whose losetup
calls xstrncpy(lo_file_name, path, LO_NAME_SIZE) and aborts rather than
truncating, so a loop-mounted image path over 63 characters fails.

That cap only ever described the default install location, and custom
storage paths made it wrong in both directions: a shorter destination
leaves the budget unused, a longer one overflows despite a legal name.
Capping the name was always treating the symptom.

Measured on an SM-A166P, kernel 5.15.178, toybox 0.8.9-android: a 64MB
ext4 image mounts at a path of 63 characters and fails at 64 with
"losetup: '...' > 64 bytes". The kernel itself has no such limit, and
neither does BusyBox; the loop device takes a file descriptor, and
lo_file_name is only what losetup -a prints.

The real fix is to stop routing through toybox, which the next commits do.

error_container_name_too_long goes with it, from all locales.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* fix: drop sparsemgr's loop path length check

migrate refused outright when the image path exceeded 64 bytes. The
threshold was off by one to begin with: toybox errors when
strlen(path) + 1 > 64, so 64 characters already fails and only 63 pass,
which is what the on-device measurement shows.

More to the point the check does not belong here. The limit is toybox's,
not the kernel's, and _mount already falls back to BusyBox, which
truncates the name instead of aborting. Once BusyBox can actually claim a
loop device on Android, which the next commit fixes, no length limit
applies at all.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* fix: let busybox loop-mount on Android

Bumps the busybox submodule to d14464f1 and rebuilds the shipped binaries
for all four architectures.

busybox mount -o loop never worked on Android, at any path length:
get_next_free_loop() asks /dev/loop-control for a free device and spells
the answer as /dev/loopN, but Android keeps loop nodes under /dev/block,
so the open ENOENTs. Every "busybox mount || mount" chain in the app was
silently falling through to toybox, which refuses a backing path over 63
characters because its losetup aborts rather than truncating the name.

That fallthrough is the whole reason container names were capped at 17
characters and sparsemgr refused to migrate a deep path.

Measured on an SM-A166P, kernel 5.15.178, with a 64MB ext4 image:

  shipped busybox   len=40   FAIL: can't setup loop device
  shipped busybox   len=63   FAIL: can't setup loop device
  shipped busybox   len=80   FAIL: can't setup loop device
  patched busybox   len=40   mounted
  patched busybox   len=63   mounted
  patched busybox   len=64   mounted
  patched busybox   len=80   mounted
  patched busybox   len=120  mounted
  toybox            len=63   mounted
  toybox            len=64   FAIL: losetup: '...' > 64 bytes

Unmount releases the loop device with no leak. The rebuild also moves the
shipped binaries from 1.37.0 to 1.39.0, matching the submodule.

The submodule bump also carries a droidspaces.config fix: applet trimming
had turned CONFIG_XZCAT off, which broke every .tar.xz install at rootfs
validation once the binaries were rebuilt. The last shipped binaries
predated that config change, which is why it had gone unnoticed.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* fix: prefer busybox for loop mounts in the shell helpers

sparsemgr's _mount tried toybox first and busybox second, and
export_container.sh called bare mount with no fallback at all even though
it already resolves $BUSYBOX. Now that busybox can actually claim a loop
device, it is the implementation without the 63-character backing path
limit, so it goes first in both.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: refactor: read the rootfs path from container.config

A container's rootfs can live anywhere now, but its container.config
always sits under /data/local/Droidspaces/Containers/<name>/, and that
file's rootfs_path is the only authoritative record of where the data
actually is. Anything rebuilding that path from the container name is
guessing, and guesses wrong the moment a custom location is used.

Export already read it, in export_container.sh. Uninstall was taught to.
The two sparse operations were not:

  migrate passed -d getContainerDirectory(name), and sparsemgr derives
  <dir>/rootfs and <dir>/rootfs.img from it, so it operated on the
  container directory rather than wherever the rootfs really was.

  resize passed -i getSparseImagePath(name), rebuilding the image path
  from the name and ignoring the one on the ContainerInfo entirely.

Both now read ContainerManager.readRootfsPath(), a single accessor that
loads the config off disk rather than trusting an in-memory listing the
backend may have rewritten since. Resize passes the path straight through;
migrate passes its parent, which is what sparsemgr wants and which still
equals the container directory for a default-location container.

Both refuse rather than guess when the config is unreadable, and when the
rootfs is not named rootfs or rootfs.img, so a hand-edited config cannot
send sparsemgr at a path that does not exist. After a migrate the config
records <parent>/rootfs.img, which is what sparsemgr created, instead of
appending ".img" to the old string.

Migrate and resize are visible again for containers outside the container
directory; the guard hiding them was a stopgap for exactly this bug.

Every path in these commands now goes through ContainerCommandBuilder
.quote(), export's included.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* fix: prefer busybox consistently for loop image tooling

The previous commit made the mount itself try busybox first, because
Android's mount is toybox and its losetup aborts rather than truncating
when a backing path exceeds 63 characters. The operations around that
mount were left on the system tools, which is inconsistent in a way that
can matter: whatever attaches a loop device should be what detaches it.

sparsemgr's _umount and _is_mounted now match _mount's order.
export_container.sh's cleanup trap was calling bare umount and mountpoint
even though it had just started mounting through busybox; it resolves
$BUSYBOX after the trap is installed, so the guards fall through to the
system tools when it is still empty.

truncate goes the same way, in sparsemgr and in SparseImageInstaller.
Both implementations are a plain ftruncate and produce a byte-identical
sparse file, verified at 8G with matching apparent size and zero blocks
allocated, so preferring the binary we ship costs nothing and removes one
more per-device variable.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* fix: drop sparsemgr's busybox mkfs fallback

_mkfs_ext4 fell back to "$BB" mkfs.ext4 and "$BB" mke2fs -t ext4 when
neither system tool was present. Neither could ever run: busybox has no
mkfs.ext4 applet at all, and its mke2fs rejects -t outright.

Being dead is the only reason it was harmless. Busybox's mke2fs builds a
bare ext2:

  busybox mke2fs   dir_index filetype sparse_super
  system mkfs.ext4 has_journal ext_attr ... extent 64bit metadata_csum

No ext_attr means no extended attributes, so a rootfs formatted that way
could hold neither SELinux labels nor file capabilities. No has_journal
means a power cut during use corrupts it. The ext4 driver mounts ext2
quite happily, so this would not have failed loudly; it would have
produced a container that was subtly wrong. It also silently truncates
the label to 15 characters.

The requirements precheck counted the same busybox applet toward
has_mkfs, which would have reported "mkfs.ext4: ok" on a device where
formatting then failed, so that goes too.

This is the one place where preferring busybox is wrong, which is why it
is a separate commit from the ordering change before it.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: fix: let Reinstall Backend run more than once

performInstallation() returned early when isInstallSuccess was already
set. That flag latches on the first successful run and AppStateViewModel
is shared across the nav graph, so it outlives the installation screen.
Every Reinstall Backend after the first therefore did nothing: the guard
returned immediately and the screen re-rendered the previous run's
completed state, Continue button and all, without copying a single file.

The guard is meant to stop a second install running concurrently with the
first, not to stop one ever happening again. The screen is popped when it
is left, so arriving there always means someone asked for an install.

Clearing the state up front rather than letting each field be overwritten
fixes a second case: after a failed install, installErrorMessage survived
into the retry, and since both the bottom bar and the status icon key off
errorMessage being non-null, a retry that succeeded still rendered as a
failure.

isInstalling is now set before the suspending backend status check rather
than after it, closing the window where two entries in quick succession
could both get past the guard.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* fix: treat command locks from a previous boot as stale

External command locks stored only the CLI PID and trusted
kill(pid, 0) == 0 as proof the holder was still running. A lock left
behind by a reboot mid-command survives on persistent storage, and
after boot Android almost always hands that PID to an unrelated
long-lived process, so every later command on the container fails
with "Cannot acquire lock: held by process N" until the file is
deleted by hand. Reported against v6.4.5, where PID 4806 was reused
by com.android.systemui after a reboot (PR #286).

A lock holder is always a CLI process, so no lock can legitimately
outlive a boot. Stamp locks as "<pid> <boot-id>" and accept only
that exact format with a matching boot ID and a live PID. Anything
else, including pid-only locks from older versions, is removed and
the command proceeds, since a bare PID cannot be told apart from
post-reboot PID reuse. Stale lock cleanup is logged with the [DEBUG]
tag, so it reaches the container log file but not the terminal.
Older binaries atoi() the new format and still read the leading PID.

Co-authored-by: UmiSlat <275275677+UmiSlat@users.noreply.github.com>
Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* fix: make restart honor every CLI flag exactly like start

Since fc2dbda8, restart reloads the workspace config between its stop
and start, because main's argument scheme had loaded the booted
snapshot while the container was alive and that snapshot is gone after
the stop. The reload silently clobbered every flag the CLI override
pass had applied: restart --hw-access -f came up with hardware access
disabled and in the background, and -B binds vanished the same way.
start was unaffected only because it never reloads.

The architecture's invariant is that CLI overrides are applied after
any disk load; restart inverted it because the override pass was
welded inline into main(). Extract it into ds_apply_cli_overrides()
and re-run it right after restart's reload. Daemon mode is covered
for free since workers re-exec the full binary with the client's
argv; the socketd bridge passes NULL since its config arrives over
the wire with no CLI overrides to re-apply.

The extraction keeps every case byte-identical except where a second
run over the same argv demanded it: -B now tokenizes a strdup'd copy
instead of writing NULs into argv, -n validates into a scratch buffer
since the discovery pass already captured the names, and -u becomes a
no-op for the same reason. long_options moves to file scope so the
helper can see it.

Three more gaps separated restart from start, all closed here. --reset
moved inside ds_apply_cli_overrides: a pre-scan wipes the config
before the other flags land, so restart resets and re-applies exactly
like start instead of surviving by accident of the eager workspace
save; main's separate reset block and its dead cli_net_mode plumbing
are gone. --conf skips restart's reload entirely: an explicit config
file never read the booted snapshot, so cfg already holds that file
plus the overrides, where the old reload overwrote them with the
workspace copy and then wrote that back over the user's own file. And
the by-name reload now starts from a clean slate via
ds_config_reset_defaults, split out of apply_reset_config without its
trailing save: ds_config_load only overlays keys present in the file,
so loading into the lived-in snapshot cfg left stale values behind
for every conditionally-written key and unioned port_forwards and
upstream_ifaces across the two loads. The reset also frees env vars,
closing a stale-environment hazard.

The result is one pipeline for both commands: load, reset if asked,
apply CLI overrides, save, boot. Persistence was already shared:
start_rootfs mirrors the effective config to the workspace
unconditionally before the fork, and restart ends in start_rootfs.

The enforce_nat_safety argv rescan stays as belt and braces.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* docs: Community-Supported-Devices: update milanf, add fogo and dre to roster (#287)

* Fix Motorola Moto G Stylus 5G (2022) details

Updated Motorola Moto G Stylus 5G (2022) entry with new kernel version and corrected formatting.

Signed-off-by: Chara <bionichuman07@gmail.com>

* Droidspaces: Docs: Community-Supported-Devices: add fogo (XT2417) and dre (DE2117/DE2118) to roster

Additionally, update milanf (XT2115) to reflect my new username

Signed-off-by: Chara <bionichuman07@gmail.com>

---------

Signed-off-by: Chara <bionichuman07@gmail.com>
Co-authored-by: Garry Strait (Chara) <92406354+GarryStraitYT@users.noreply.github.com>

* Docs: community-supported-devices: Move SM-A155F to proper section (see #225) (#288)

Additionally, I added a new contribution guideline which will hopefully prevent further "throw it anywhere" contributions.
Using the table structure improves readability and reduces maintenance costs. Throwing it anywhere makes the documentation sloppy and hard to read. The sole reason for me making the table structure was because I could not find where to put my device at in the list. The table structure prevents this.

Signed-off-by: Chara <bionichuman07@gmail.com>

* docs: add Volla Phone Quintus (algiz) to community-supported devices (#289)

Co-authored-by: blue-boy-questions <blue-boy-questions@users.noreply.github.com>

* Translated using Weblate (Chinese (Traditional Han script)) (#290)

Currently translated at 100.0% (553 of 553 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 98.3% (544 of 553 strings)



Translate-URL: https://hosted.weblate.org/projects/droidspaces/strings/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/droidspaces/strings/zh_Hant/
Translation: Droidspaces/Strings

Co-authored-by: Jung Chi Wang <snowwolf725@gmail.com>
Co-authored-by: MonkiFantasy <execvp@qq.com>

* docs: add a YAGNI gate to AGENTS.md

The file already told agents to prefer a small diff and to reuse what exists,
but nothing told them to ask whether the code needs to exist at all. Add an
ordered list they work down before writing anything: skip speculative work,
grep for it here, reach for libc or the kernel, then an installed dependency,
then one line, then the smallest thing that works.

Two guards on it. The list shortens the solution, not the reading, so trace
the flow first and fix a bug at the shared caller rather than at the one path
the report names. And nothing on the list applies to input validation at a
trust boundary, a fail-closed check, or error handling that loses state.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: revert: restore the tighter wrapper radius on action pills

The tinted Surface that wraps a row of action buttons was drawn at a 12.dp
radius while the buttons inside it sit at 16.dp. The mismatch is the design,
the buttons are meant to read as sitting proud of the wrapper rather than
nesting flush inside it.

2a782aa4 (#236) and b528c28c (#264) each read this as a corner radius bug and
set the wrapper to 20.dp, on the container card and on the init system service
rows respectively. Put both back to 12.dp. DialogFooterRow, added later in
c3a77851 (#283), copied the 20.dp version and never carried the original look,
so it gets the same treatment and the dialog footers now match the rest.

Each of the three wrappers carries a comment saying the two radii are not
supposed to agree, so the next pass over this code has the reasoning in front
of it.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* docs: write down the app's design language

The Compose app has a real visual language, but nothing recorded it, so every
contributor re-derived it from whichever screen they happened to open. That is
how the action pill radius got "fixed" twice by someone reading a deliberate
choice as a bug, and how border alpha ended up spread across six values.

DESIGN.md documents what the app already does rather than what it should
ideally do. Every value in it was counted out of the tree, and where the app
disagreed with itself the value most of it already uses won. So the language is
flat with no elevation anywhere, depth comes from a surface step plus a 1dp
border, Surface is the primitive over Material components, and colour is always
derived from the scheme so dynamic colour, the six palettes and AMOLED all keep
working.

It deliberately does not become a second component catalogue. A spec reads as
instructions for rebuilding the thing it describes, which is the opposite of
what this repo needs, so it opens by pointing at the CONTRIBUTING.md reuse
inventory and only covers values.

DESIGN-TODO.md is the backlog: roughly forty places that do not follow it yet,
each with a file, a current value and a target, ranked with the touch target
violation first because that one is correctness rather than taste.

Also drop the instruction to take corner radii from ShapeUtils. It is
referenced nowhere outside its own file and three of its six values contradict
the app, including a button radius that would reproduce the exact mismatch
DESIGN.md warns about.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: fix: give the container card logs button a real touch target

The logs IconButton was pinned to 32.dp, which is also what Compose then
uses for the touch target, so it sat below the 48.dp accessibility floor.
Dropping the size override lets Material give it the default box with a
48.dp minimum target.

The header Row it lives in was fixed at 32.dp and would have clipped that,
so the row moves to 48.dp. The card header grows by 16.dp as a result.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: refactor: delete the unused PercentCircle component

Never had a call site. The repo has no test or preview sources, so nothing
was referencing it out of view either.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: refactor: drop the empty DialogUtils object

Its entire body was the comment "Use ShapeUtils for consistency", and no
call site ever referenced it. The file keeps ProgressDialog and
ErrorLogsDialog, which are the parts anyone actually uses.

Also drops two imports the file was making of its own package.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: refactor: delete the legacy colour constants

Eleven of the twelve values in Color.kt were marked "kept for backward
compatibility" and referenced nowhere. Every colour in the app comes from
the scheme built in Theme.kt, so keeping a second parallel set of literals
around only invited someone to use one.

AMOLED_BLACK stays, Theme.kt uses it in seventeen places.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: refactor: flatten AmoledColorCache into two functions

The object claimed to pre-compute and cache AMOLED blends. createAmoledScheme
cached nothing at all, and the one real memo in createStaticAmoledScheme sat
underneath DroidspacesTheme's own remember(), which already keys on the
palette. So it saved rebuilding one ColorScheme across an Activity restart,
which is not worth a process-global mutable static.

fastBlend goes with it. It differed from blend only in preserving the
receiver's alpha, and every receiver it was called with is opaque, so the two
produced identical output. AMOLED_BLEND_RATIO was never referenced, and the
comments promising a lighter 0.3f blend for surfaceVariant described
something the code did not do.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: refactor: one source for the init system status colours

The six status hues were written out as hex in four files: statusColorFor()
in InitServiceScreen, and again in the filter chip legends of the systemd,
OpenRC and procd screens. Four copies of one palette, and the chips could
drift from the pills they describe.

The chips now call statusColorFor(), which each chip maps onto one to one,
so the legend and the card can no longer disagree.

The hues stay as they are rather than becoming theme roles. Six of these
states appear in the legend row at the same time, and primary and tertiary
move with dynamic colour and the palette picker, so running and abnormal
would land on nearly the same colour under some palettes. That reasoning is
now a comment on the function.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: fix: use tertiary for the update available accent

The status card reached for a literal amber. tertiary is what the rest of
the app already uses for the attention state, and unlike a literal it
follows dynamic colour, the palettes and AMOLED.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: docs: say why the virtual keys background is a literal

It looked like theme drift. It is not: the key strip sits against the
terminal's own black background, which is independent of the app theme, so
a surface role would give a light strip under a dark terminal whenever the
app is in light mode.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: fix: settle every dialog shell at 24dp

Twelve dialogs were already 24. TerminalDialog and the terminal's user
picker sat at 28, ProgressDialog and ErrorLogsDialog took 28 from
ShapeUtils, and AboutDialog used 20, the card radius. All four move to 24.

ShapeUtils goes with them. Nothing outside its own file ever referenced it,
and three of its six values contradicted what the app actually draws: a
16dp card against the real 20, a 28dp dialog against the real 24, and a
12dp button against the real 16. That last one is the wrapper radius from
the action pill, so anyone who had followed it would have produced exactly
the mismatch DESIGN.md warns about.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: fix: bring buttons and text fields onto one radius

Seventeen sites used a 14dp radius, mostly the inline buttons on the
container detail screen and the fields inside dialogs, against 16dp
everywhere else. They move to 16.

Two of the 14dp sites were not buttons. The repo source banner is a card,
so it takes the 20dp card radius. The 28dp avatar box in settings was
clipped at 14dp, which is a circle drawn the long way round, so it says
CircleShape now.

Also 6dp to 8dp on the one badge that had its own value, 20dp to 16dp on
the DsDropdown field so it stops disagreeing with the plain text fields
beside it in the same form, and 12dp to 16dp on the language picker rows to
match the equivalent rows in the terminal user picker.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: fix: shape the four buttons that rendered as full pills

These Material buttons never passed a shape, so they fell back to the M3
default of a fully rounded pill. Nothing else in the app is shaped that
way, which made the installation progress screen and the kernel
requirements screen look like they came from a different app.

They take the 16dp action button radius.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: fix: put the drifted border and state alphas back on the table

The container detail screen drew its card borders at 0.5f while every other
screen drew the same kind of card at 0.35f. Four surfaces, now 0.35f.

SettingsCard and ToggleCard dimmed disabled content to 0.5f. Material's
disabled content alpha is 0.38f and the rest of the app already uses it.

DsDropdown had its own copy of the shared text field colours, identical
except for a 0.35f unfocused border where DsTextFieldDefaults uses 0.5f, so
a dropdown sat next to a plain field with a lighter outline. It calls the
shared defaults now.

Two one-off text alphas in the rootfs sheet, 0.55f and 0.75f, move onto the
0.6f and 0.7f pair the app uses for tertiary and secondary text.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: refactor: drop the cancelBorderAlpha parameter

An alpha exposed as component API. Four of the eleven call sites passed
0.35f to override a 0.4f default, which is a difference nobody can see and
a decision no caller should be making. The border is fixed at 0.35f, the
same value the footer's own wrapper uses.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: fix: one style for every screen title

Screen titles were in four weights: Black on settings, Bold on the init
service screen, SemiBold on requirements and container details, and six
screens passed a bare Text with no style at all, so they fell back to
whatever TopAppBar defaults to. They are all titleLarge Bold now.

The three centred bars go back to a plain TopAppBar. A centred title is
squeezed between the navigation icon and the actions, which is the worst
place to put the long unit names those particular screens display.

Two screens keep a smaller style, with a comment saying why: the unit
detail title is a systemd unit name, which is machine text and stays mono,
and the override editor title carries a unit name inside a formatted
string. Both would only ellipsize sooner at titleLarge.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: refactor: import the theme mono font instead of redeclaring it

Three screens declared their own JetBrainsMono family. The one in
InitServiceScreen even shadowed the theme's under the same name, and the
other two were the same font under two other names. All three drop their
copy and import ui/theme/Type.kt.

The theme family also carries the italic and bold-italic faces the local
copies left out, so styled log output can now render them.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: fix: stop bypassing the type scale

Two overrides on the container detail screen restated the size their own
style already sets, 11sp on labelSmall and 12sp on bodySmall, so they did
nothing but hide the style behind a copy().

The bottom navigation dropped unselected labels to 10sp, below the 11sp
floor of the scale.

The service name on init system rows picked its size from its own length,
13sp past 25 characters and 16sp under. It already has maxLines and an
ellipsis, so this only moved where the truncation happened while making one
row's text a different size from its neighbour's.

Button labels on the container card and the help card title were Bold where
the shared components use SemiBold.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: refactor: share one SectionHeader

Four screens had grown their own section heading and none of them agreed.
The config form and the storage screen used titleMedium, settings used
titleSmall Bold in four places and titleSmall SemiBold in two more, and the
unit detail card had a fourth copy. The storage screen's version even
carried a comment saying it existed to match the config form, which is a
copy admitting to being one.

SectionHeader is titleSmall Bold primary, the value DESIGN.md documents.
Spacing stays at the call site through the modifier, because a header in a
form needs room above it and one at the top of a settings group does not,
and that is the only thing the four versions actually varied on.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: fix: snap the spacing outliers back onto the scale

Cards in a list sit 16.dp apart on most screens. The container detail
screen used 12, the rootfs sheet 10 and the auto boot list 8, so the same
kind of list looked denser or looser depending on where you were.

Card inner padding is 16. HelpCard used 20, and the container card used a
vertical 14 that is not on the scale at all.

Icon sizes: the seven 13.dp icons beside small text move to 16, and the
five 22.dp leading icons move to 20. Both were a couple of dp off values
the app already uses everywhere else, which is exactly the kind of
difference that is invisible in isolation and obvious in a row.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: refactor: reuse StatusPill and share the menu theming

The init service card drew its own status pill: same tint, same border, same
6.dp dot, same labelSmall Black with the same letter spacing, all of it
already in StatusPill. It calls the component now, which is pixel identical.

The dropdown surface override was the other verbatim copy. DsDropdown and the
overflow menu on service rows both computed the same luminance check, forced
the same opaque surface and drew the same 20.dp border. That block is now
DsMenuTheme plus a dsMenuBorder modifier.

The overflow menu stays a DropdownMenu rather than becoming a DsDropdown. It
is a list of fire-and-forget actions with their own icons, not a select field
with a selected value, so only the surface was ever shared.

Signed-off-by: ravindu644 <droidcasts@protonmail.com>

* app: refactor: use DialogFooterRow in the last two dialogs

Thirteen dialogs already used it. The language picker built its own pair of
48.dp Surfaces at a 14.dp radius, and the terminal user picker used a
right-aligned TextButton and Button. Three footer styles in one app.

Both change appearan…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants