diff --git a/docs-site/content/docs/fedora-kde.md b/docs-site/content/docs/fedora-kde.md deleted file mode 100644 index a5eb4fe..0000000 --- a/docs-site/content/docs/fedora-kde.md +++ /dev/null @@ -1,165 +0,0 @@ ---- -title: Fedora — KDE Plasma -description: Reproducible punktfunk host setup on Fedora KDE (KWin) via the RPM. ---- - -Set up a punktfunk host on **Fedora KDE** (the KDE Plasma spin). The host runs as an RPM-managed -systemd service and uses KWin to create per-client virtual displays, captured zero-copy -(dmabuf → CUDA → NVENC) on NVIDIA. - -> Validated live on **Fedora 44 KDE Plasma** with an RTX 4090: KWin virtual output + full -> zero-copy capture. Everything below is the reproducible flow — paste it on a fresh box. - -> New here? Read [Security & Safe Use](/docs/security) first — a streaming host is remote control of -> the machine, so keep it on a trusted LAN or VPN and require pairing. - -The setup has three parts: **NVIDIA driver** → **host RPM** → **KWin streaming session**. - -## 1. NVIDIA driver (RPM Fusion akmod) - -Enable RPM Fusion (free + nonfree), then install the akmod driver + CUDA. RPM Fusion's nonfree -NVIDIA repo is sometimes pre-enabled on the KDE spin; the full free/nonfree repos below are still -needed (they carry the NVENC ffmpeg in the next step). - -```sh -sudo dnf install \ - https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm \ - https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm -sudo dnf install akmod-nvidia xorg-x11-drv-nvidia-cuda -``` - -**NVENC ffmpeg.** Fedora ships `ffmpeg-free`, which is built **without** NVENC — the host can't -encode with it. Swap to RPM Fusion's ffmpeg: - -```sh -sudo dnf install --allowerasing ffmpeg ffmpeg-libs -ffmpeg -hide_banner -encoders | grep nvenc # expect hevc_nvenc / av1_nvenc / h264_nvenc -``` - -**Secure Boot.** If `mokutil --sb-state` says *enabled*, the akmod module is signed with a -locally-generated key that must be enrolled once: - -```sh -sudo akmods --force # build + sign the module -sudo mokutil --import /etc/pki/akmods/certs/public_key.der # set a one-time password -sudo reboot -``` - -On the next boot a blue **MOK Manager** screen appears **on the machine's console** (not over -SSH): *Enroll MOK → Continue → Yes → (the password) → Reboot*. Then verify: - -```sh -nvidia-smi # driver loads -ffmpeg -hide_banner -encoders | grep nvenc -``` - -(Or disable Secure Boot in firmware to skip the MOK step — fine for a dedicated test box.) - -## 2. Install the host (RPM) - -The host is published to the self-hosted Gitea RPM registry, in a per-Fedora-release group (an RPM -is soname-coupled to its base, so Fedora 44 has its own `fedora-44` group). Add the repo and -install: - -```sh -sudo tee /etc/yum.repos.d/punktfunk.repo >/dev/null <<'REPO' -[punktfunk] -name=punktfunk -baseurl=https://git.unom.io/api/packages/unom/rpm/fedora-44 -enabled=1 -# Packages are GPG-signed (gpgcheck=1) AND the repo metadata is Gitea-signed (repo_gpgcheck=1). -gpgcheck=1 -repo_gpgcheck=1 -gpgkey=https://git.unom.io/api/packages/unom/rpm/repository.key - https://git.unom.io/api/packages/unom/generic/punktfunk-keys/1/RPM-GPG-KEY-punktfunk -REPO - -sudo dnf install punktfunk -sudo usermod -aG input "$USER" # /dev/uinput access for virtual gamepads (re-login to apply) -``` - -Updates later are just `sudo dnf upgrade punktfunk`. The package ships the systemd user units, the -udev rule, the UDP socket-buffer sysctl tuning, and example configs. - -> No matching `fedora-NN` group for your release yet? Build one with the same toolchain CI uses — -> `docker build --build-arg FEDORA_VERSION=NN -f ci/fedora-rpm.Dockerfile -t pf-rpm ci` then run -> `packaging/rpm/build-rpm.sh` inside it — or build from source (appendix below). - -## 3. KWin streaming session - -KWin's virtual-output capture uses its **privileged** `zkde_screencast` protocol, which an -*interactive* Plasma session will not hand to an external client. So the host streams from a -**dedicated headless KWin session** (`kwin --virtual` launched with -`KWIN_WAYLAND_NO_PERMISSION_CHECKS=1`) — shipped as `punktfunk-kde-session.service`. This also -makes the box a self-contained appliance: it streams at boot with no graphical login. - -```sh -# KWin appliance config (ships with the package): -mkdir -p ~/.config/punktfunk -cp /usr/share/punktfunk/host.env.kde ~/.config/punktfunk/host.env - -# Start the headless KWin session + the host, and start user units at boot without a login: -systemctl --user daemon-reload -systemctl --user enable --now punktfunk-kde-session punktfunk-host -sudo loginctl enable-linger "$USER" -``` - -Check it came up: - -```sh -systemctl --user status punktfunk-host # active -journalctl --user -u punktfunk-host -f # watch a client connect -``` - -The host now listens on `9777` (native punktfunk/1) + the GameStream ports, and advertises over -mDNS. It requires **PIN pairing** by default (secure on a LAN); pair once from your client. - -### Web console - -The console (status, paired devices, arm pairing) ships as `punktfunk-web` — enable it, then open -`http://:47992`: - -```sh -systemctl --user enable --now punktfunk-web -``` - -#### Console login password - -The console is password-protected. On first start `punktfunk-web-init` generates a random login -password and saves it to `~/.config/punktfunk/web-password` (as `PUNKTFUNK_UI_PASSWORD=…`). Read it -back at any time — from the init service's journal, or straight from the file: - -```sh -journalctl --user -u punktfunk-web-init | sed -n 's/.*password generated: //p' -sed -n 's/^PUNKTFUNK_UI_PASSWORD=//p' ~/.config/punktfunk/web-password -``` - -To set your own password, edit that file (`PUNKTFUNK_UI_PASSWORD=`) and restart the -console: `systemctl --user restart punktfunk-web`. Forgot it? This is the recovery path linked from -the console login screen — see [Forgot your Password?](/docs/forgot-password). - -## 4. Connect a client - -From any [client](/docs/clients) — `punktfunk-client --discover` finds the host on the LAN. On -first connect, complete the PIN pairing — **arm it from the host's web console / mgmt API**, which -makes the host display a 4-digit PIN to type into the client. (Pairing is required by default; pass -`serve --open` only if you deliberately want to disable the requirement.) See -[Clients](/docs/clients) and [Running as a Service](/docs/running-as-a-service). - -## Appendix — build from source - -If there's no RPM for your Fedora release and you don't want to build one, compile the host -directly (no clean updates / no packaged units — you wire those up by hand): - -```sh -sudo dnf install gcc gcc-c++ make cmake clang clang-devel nasm git \ - pipewire-devel wayland-devel wayland-protocols-devel libxkbcommon-devel opus-devel \ - libdrm-devel mesa-libgbm-devel mesa-libEGL-devel mesa-libGLES-devel libva-devel \ - ffmpeg-devel libei-devel -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -git clone https://git.unom.io/unom/punktfunk.git && cd punktfunk -cargo build --release -p punktfunk-host -``` - -Then write `~/.config/punktfunk/host.env` (as in `/usr/share/punktfunk/host.env.kde`, but the host -binary is `target/release/punktfunk-host`) and run it inside the KWin session from step 3. diff --git a/docs-site/content/docs/ubuntu-gnome.md b/docs-site/content/docs/ubuntu-gnome.md deleted file mode 100644 index f415b6c..0000000 --- a/docs-site/content/docs/ubuntu-gnome.md +++ /dev/null @@ -1,172 +0,0 @@ ---- -title: Ubuntu — GNOME -description: Set up a punktfunk host on Ubuntu with the GNOME desktop (Mutter). ---- - -Set up a punktfunk host on **Ubuntu** (Desktop or Server) running **GNOME**. The host uses GNOME's -Mutter compositor to create a per-client virtual display. Tested on Ubuntu 24.04+ and GNOME 48+. - -> New to this? Skim [Requirements](/docs/requirements) first, and read -> [Security & Safe Use](/docs/security) — a streaming host is remote control of the machine, so keep it -> on a trusted LAN or VPN and require pairing. - -## 1. NVIDIA driver - -Install the recommended NVIDIA driver: - -```sh -sudo ubuntu-drivers install # or: sudo apt install nvidia-driver- -``` - -Then make sure the **GL/EGL userspace** is present — GNOME on NVIDIA needs it, and the base driver -package doesn't always pull it in. Install the `libnvidia-gl` package matching your driver version: - -```sh -sudo apt install libnvidia-gl- # e.g. libnvidia-gl-550 -``` - -Reboot, then confirm the driver and KMS modeset: - -```sh -nvidia-smi -cat /sys/module/nvidia_drm/parameters/modeset # should print Y -``` - -If modeset is not `Y`: - -```sh -echo 'options nvidia-drm modeset=1' | sudo tee /etc/modprobe.d/nvidia-drm.conf -sudo update-initramfs -u && sudo reboot -``` - -> **Secure Boot:** on a machine with Secure Boot **enabled**, the NVIDIA kernel module won't load -> until you enrol its signing key. If `nvidia-smi` reports it can't talk to the driver, run -> `sudo mokutil --import /var/lib/shim-signed/mok/MOK.der` (set a one-time password), reboot, and -> choose **Enrol MOK** at the blue screen. Or disable Secure Boot in firmware. - -## 2. Install the host (apt) - -`punktfunk-host` is published as a `.deb` to the public Gitea apt registry, so the box installs and -updates with plain `apt`. The registry is public — no auth needed, just trust its signing key: - -```sh -sudo install -d -m 0755 /etc/apt/keyrings -curl -fsSL https://git.unom.io/api/packages/unom/debian/repository.key \ - | sudo tee /etc/apt/keyrings/punktfunk.asc >/dev/null - -echo "deb [signed-by=/etc/apt/keyrings/punktfunk.asc] https://git.unom.io/api/packages/unom/debian stable main" \ - | sudo tee /etc/apt/sources.list.d/punktfunk.list - -sudo apt update -sudo apt install punktfunk-host -``` - -`punktfunk-host` `Recommends` the browser console (`punktfunk-web`), so apt pulls it in by default. -The desktop *client* (`punktfunk-client`) is a separate package for the machine you stream *to* — not -installed on a host. The NVIDIA driver is **not** a dependency — you installed it out of band in -step 1. Later updates are just `sudo apt update && sudo apt upgrade`. - -## 3. Configure - -The package ships the systemd **user** unit, the `/dev/uinput` udev rule, the socket-buffer sysctl -tuning, and an example config. As the desktop user, grant gamepad access and write the GNOME config: - -```sh -sudo usermod -aG input "$USER" # /dev/uinput for virtual gamepads (re-login to apply) -mkdir -p ~/.config/punktfunk -cat > ~/.config/punktfunk/host.env <<'ENV' -WAYLAND_DISPLAY=wayland-0 -XDG_CURRENT_DESKTOP=GNOME -PUNKTFUNK_COMPOSITOR=mutter -PUNKTFUNK_VIDEO_SOURCE=virtual -PUNKTFUNK_ZEROCOPY=1 -PUNKTFUNK_INPUT_BACKEND=libei -ENV -``` - -See the [Configuration reference](/docs/configuration) for every option. - -## 4. Run - -Start the host as a user service from **inside your GNOME session** (so it can reach Mutter): - -```sh -systemctl --user enable --now punktfunk-host -journalctl --user -u punktfunk-host -f # watch it come up + print its fingerprint -``` - -The host listens on UDP `9777` (native punktfunk/1) plus the GameStream ports, and advertises itself -over mDNS. It requires **PIN pairing** by default (secure on a LAN) — arm pairing from the web -console (next step) and pair once from your [client](/docs/clients). - -### Web console - -The console (status, paired devices, arm pairing) ships as `punktfunk-web`: - -```sh -systemctl --user enable --now punktfunk-web -# read the auto-generated login password, then open http://:47992 -journalctl --user -u punktfunk-web-init | sed -n 's/.*password generated: //p' -``` - -#### Console login password - -The console is password-protected. On first start `punktfunk-web-init` generates a random login -password and saves it to `~/.config/punktfunk/web-password` (as `PUNKTFUNK_UI_PASSWORD=…`). Read it -back at any time — from the init service's journal, or straight from the file: - -```sh -journalctl --user -u punktfunk-web-init | sed -n 's/.*password generated: //p' -sed -n 's/^PUNKTFUNK_UI_PASSWORD=//p' ~/.config/punktfunk/web-password -``` - -To set your own password, edit that file (`PUNKTFUNK_UI_PASSWORD=`) and restart the -console: `systemctl --user restart punktfunk-web`. Forgot it? This is the recovery path linked from -the console login screen — see [Forgot your Password?](/docs/forgot-password). - -To run the host automatically at boot — including on a **headless** machine with no monitor — see -[Running as a Service](/docs/running-as-a-service). - -## Troubleshooting - -- **gnome-shell fails to start / "GPU … not supported by EGL":** the NVIDIA GL/EGL userspace is - missing. Install `libnvidia-gl-` (step 1) and confirm - `/usr/share/glvnd/egl_vendor.d/10_nvidia.json` exists. -- **Capture fails with "Session creation inhibited":** a **locked** GNOME session blocks screen - capture. On a headless/always-on host, disable the lock — see - [Running as a Service](/docs/running-as-a-service). -- More in [Troubleshooting](/docs/troubleshooting). - -## Appendix — build from source - -If the apt registry doesn't have a build for your release, or you want to track `main` directly, -compile the host yourself (no clean updates / no packaged units — you wire those up by hand). - -Install the build toolchain and runtime libraries: - -```sh -sudo apt install build-essential pkg-config cmake clang libclang-dev nasm git curl \ - pipewire pipewire-pulse wireplumber libpipewire-0.3-dev libspa-0.2-dev \ - libwayland-dev wayland-protocols libxkbcommon-dev libopus-dev \ - libdrm-dev libgbm-dev libegl-dev libgles-dev mesa-common-dev libva-dev \ - ffmpeg libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libavfilter-dev libavdevice-dev \ - libnvidia-egl-wayland1 libnvidia-egl-gbm1 libei-dev -``` - -Install Rust if you don't have it, then build: - -```sh -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -git clone https://git.unom.io/unom/punktfunk.git && cd punktfunk -cargo build --release -p punktfunk-host -``` - -The host binary lands at `target/release/punktfunk-host`. Write `~/.config/punktfunk/host.env` as in -step 3, then run it inside your GNOME session: - -```sh -cargo run --release -p punktfunk-host -- serve --gamestream -``` - -(The native plane is always on; `--gamestream` adds the Moonlight-compat surface this guide's -GameStream ports refer to — trusted LAN only. Drop it for a secure native-only host.) diff --git a/docs-site/content/docs/ubuntu-kde.md b/docs-site/content/docs/ubuntu-kde.md deleted file mode 100644 index eef5481..0000000 --- a/docs-site/content/docs/ubuntu-kde.md +++ /dev/null @@ -1,128 +0,0 @@ ---- -title: Ubuntu — KDE Plasma -description: Set up a punktfunk host on Ubuntu with KDE Plasma (KWin). ---- - -Set up a punktfunk host on **Ubuntu** running **KDE Plasma**. The host uses KDE's KWin compositor to -create a per-client virtual display. Needs **KWin 6.5.6 or newer**. - -> New to this? Skim [Requirements](/docs/requirements) first, and read -> [Security & Safe Use](/docs/security) — a streaming host is remote control of the machine, so keep it -> on a trusted LAN or VPN and require pairing. - -## 1. NVIDIA driver - -Identical to the GNOME guide — follow **step 1** of -[Ubuntu — GNOME](/docs/ubuntu-gnome#1-nvidia-driver): install the NVIDIA driver **and** the -`libnvidia-gl-` userspace, enable `nvidia-drm modeset=1`, reboot, and verify with -`nvidia-smi`. - -## 2. Install the host (apt) - -The host is published as a `.deb` to the public Gitea apt registry — install and update with plain -`apt`. Trust the repo's signing key, add the repo, and install: - -```sh -sudo install -d -m 0755 /etc/apt/keyrings -curl -fsSL https://git.unom.io/api/packages/unom/debian/repository.key \ - | sudo tee /etc/apt/keyrings/punktfunk.asc >/dev/null - -echo "deb [signed-by=/etc/apt/keyrings/punktfunk.asc] https://git.unom.io/api/packages/unom/debian stable main" \ - | sudo tee /etc/apt/sources.list.d/punktfunk.list - -sudo apt update -sudo apt install punktfunk-host -``` - -This also pulls the web console (`punktfunk-web`) via `Recommends` (the pairing/status UI). The -desktop *client* — `punktfunk-client`, for the machine you stream *to* — is a separate package, not -needed on a host. The NVIDIA driver stays out of band (step 1). Updates later are just -`sudo apt update && sudo apt upgrade`. - -## 3. Configure - -The package ships the systemd **user** unit, the udev rule, and the sysctl tuning. As the desktop -user, grant gamepad access and write the KDE config: - -```sh -sudo usermod -aG input "$USER" # /dev/uinput for virtual gamepads (re-login to apply) -mkdir -p ~/.config/punktfunk -cat > ~/.config/punktfunk/host.env <<'ENV' -WAYLAND_DISPLAY=wayland-0 -XDG_CURRENT_DESKTOP=KDE -PUNKTFUNK_COMPOSITOR=kwin -PUNKTFUNK_VIDEO_SOURCE=virtual -PUNKTFUNK_ZEROCOPY=1 -PUNKTFUNK_INPUT_BACKEND=libei -ENV -``` - -> Make sure you're on a **KDE Wayland** session (not X11) — the picker on the login screen. The -> virtual-display path is Wayland-only. See the [Configuration reference](/docs/configuration) for -> every option. - -## 4. Run - -Start the host as a user service from **inside your Plasma session**: - -```sh -systemctl --user enable --now punktfunk-host -journalctl --user -u punktfunk-host -f # watch it come up + print its fingerprint -``` - -The host listens on UDP `9777` (native punktfunk/1) plus the GameStream ports and advertises over -mDNS. It requires **PIN pairing** by default — arm pairing from the web console and pair once from -your [client](/docs/clients). - -### Web console - -```sh -systemctl --user enable --now punktfunk-web -# read the auto-generated login password, then open http://:47992 -journalctl --user -u punktfunk-web-init | sed -n 's/.*password generated: //p' -``` - -#### Console login password - -The console is password-protected. On first start `punktfunk-web-init` generates a random login -password and saves it to `~/.config/punktfunk/web-password` (as `PUNKTFUNK_UI_PASSWORD=…`). Read it -back at any time — from the init service's journal, or straight from the file: - -```sh -journalctl --user -u punktfunk-web-init | sed -n 's/.*password generated: //p' -sed -n 's/^PUNKTFUNK_UI_PASSWORD=//p' ~/.config/punktfunk/web-password -``` - -To set your own password, edit that file (`PUNKTFUNK_UI_PASSWORD=`) and restart the -console: `systemctl --user restart punktfunk-web`. Forgot it? This is the recovery path linked from -the console login screen — see [Forgot your Password?](/docs/forgot-password). - -To run it at boot — including fully **headless**, with KWin brought up automatically and no login — -see [Running as a Service](/docs/running-as-a-service); the headless appliance is built around KDE. - -## Troubleshooting - -- **KWin too old:** virtual outputs need KWin **≥ 6.5.6**. Check with `kwin_wayland --version`. -- **No picture / capture fails:** confirm you're on a Wayland session and the NVIDIA GL userspace is - installed (`libnvidia-gl-`). More in [Troubleshooting](/docs/troubleshooting). - -## Appendix — build from source - -If the apt registry has no build for your release, compile the host yourself (no clean updates / no -packaged units). Install the build toolchain and runtime libraries — the same `apt` line as the -[GNOME build-from-source appendix](/docs/ubuntu-gnome#appendix--build-from-source) — then: - -```sh -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -git clone https://git.unom.io/unom/punktfunk.git && cd punktfunk -cargo build --release -p punktfunk-host -``` - -Write `~/.config/punktfunk/host.env` as in step 3, then run it inside your Plasma session: - -```sh -cargo run --release -p punktfunk-host -- serve --gamestream -``` - -(The native plane is always on; `--gamestream` adds the Moonlight-compat surface this guide's -GameStream ports refer to — trusted LAN only. Drop it for a secure native-only host.) diff --git a/web/messages/de.json b/web/messages/de.json index 35e4e54..b86843d 100644 --- a/web/messages/de.json +++ b/web/messages/de.json @@ -9,6 +9,7 @@ "nav_pairing": "Kopplung", "nav_library": "Bibliothek", "nav_settings": "Einstellungen", + "nav_more": "Mehr", "status_title": "Live-Status", "status_video": "Video", "status_audio": "Audio", @@ -87,7 +88,7 @@ "display_preset_current": "Aktiv", "display_preset_soon": "in Kürze", "display_keep_alive_help": "„Aus“ baut die Anzeige sofort beim Trennen ab. Halte sie (und bei gamescope ihr Spiel) am Leben, damit ein schnelles Wiederverbinden sofort fortsetzt, statt neu aufzubauen.", - "display_topology_help": "Was mit den physischen Monitoren des Hosts während des Streamings geschieht.", + "display_topology_help": "Wie sich die gestreamte Anzeige in den Desktop des Hosts einfügt. Erweitern: ein zusätzlicher Bildschirm neben deinen Monitoren. Primär: die gestreamte Anzeige wird der primäre Ausgang, deine Monitore bleiben. Exklusiv: die gestreamte Anzeige ist der einzige Ausgang — physische Monitore werden beim Streamen deaktiviert und danach wiederhergestellt. Auf einem Host ohne Monitor legt es nur fest, ob die gestreamte Anzeige primär ist.", "display_conflict": "Wenn ein weiterer Client verbindet", "display_conflict_help": "Was passiert, wenn ein zweiter Client verbindet, während bereits gestreamt wird, und eine andere Auflösung anfragt.", "display_conflict_separate": "Eigene Anzeige", @@ -95,7 +96,7 @@ "display_conflict_join": "Ansicht teilen", "display_conflict_reject": "Besetzt — ablehnen", "display_identity": "Client-Identität", - "display_identity_help": "Jedem Client eine stabile Anzeige geben, damit der Desktop seine Monitor-Einstellungen merkt (z. B. Skalierung).", + "display_identity_help": "Ob die gestreamte Anzeige eine stabile Client-Identität trägt, sodass der Desktop des Hosts die Monitor-Einstellungen dieses Clients (Skalierung, Auflösung) merkt und beim erneuten Verbinden wieder anwendet. Geteilt: eine Identität für alle. Pro Client: jedes Gerät eigene. Pro Client + Auflösung: separate Einstellungen je Gerät und Auflösung.", "display_identity_shared": "Geteilt", "display_identity_per_client": "Pro Client", "display_identity_per_client_mode": "Pro Client + Auflösung", diff --git a/web/messages/en.json b/web/messages/en.json index a88e977..6d222fd 100644 --- a/web/messages/en.json +++ b/web/messages/en.json @@ -9,6 +9,7 @@ "nav_pairing": "Pairing", "nav_library": "Library", "nav_settings": "Settings", + "nav_more": "More", "status_title": "Live status", "status_video": "Video", "status_audio": "Audio", @@ -87,7 +88,7 @@ "display_preset_current": "Active", "display_preset_soon": "coming soon", "display_keep_alive_help": "Off tears the display down as soon as the client disconnects. Keep it alive (and, on gamescope, its game) so a quick reconnect resumes instantly instead of rebuilding.", - "display_topology_help": "What happens to the host's physical monitors while streaming.", + "display_topology_help": "How the streamed display fits into the host's desktop. Extend: an extra screen alongside your monitors. Primary: the streamed display becomes the primary output, your monitors kept. Exclusive: the streamed display is the sole output — physical monitors are disabled while streaming and restored after. On a headless host it just sets whether the streamed display is the primary.", "display_conflict": "When another client connects", "display_conflict_help": "What happens if a second client connects while one is already streaming and asks for a different resolution.", "display_conflict_separate": "Own display", @@ -95,7 +96,7 @@ "display_conflict_join": "Share view", "display_conflict_reject": "Busy — reject", "display_identity": "Per-client identity", - "display_identity_help": "Give each client a stable display so the desktop remembers its per-monitor settings (e.g. scaling).", + "display_identity_help": "Whether the streamed display carries a stable per-client identity, so the host's desktop remembers that client's per-monitor settings (scaling, resolution) and reapplies them when it reconnects. Shared: one identity for everyone. Per client: each device keeps its own. Per client + resolution: a device keeps separate settings per resolution it connects at.", "display_identity_shared": "Shared", "display_identity_per_client": "Per client", "display_identity_per_client_mode": "Per client + resolution", diff --git a/web/src/components/app-shell.tsx b/web/src/components/app-shell.tsx index 4499f5f..b769371 100644 --- a/web/src/components/app-shell.tsx +++ b/web/src/components/app-shell.tsx @@ -1,16 +1,17 @@ -import { Link } from "@tanstack/react-router"; +import { Link, useRouterState } from "@tanstack/react-router"; import { Activity, GaugeCircle, KeyRound, LibraryBig, MonitorPlay, + MoreHorizontal, ScrollText, Server, Settings, } from "lucide-react"; import { motion, stagger } from "motion/react"; -import type { ReactNode } from "react"; +import { type ReactNode, useState } from "react"; import { BrandMark } from "@/components/brand-mark"; import { Wordmark } from "@/components/wordmark"; import { changeLocale, type Locale, locales, useLocale } from "@/lib/i18n"; @@ -30,6 +31,11 @@ const NAV = [ { to: "/settings", icon: Settings, label: () => m.nav_settings() }, ] as const; +// On phones a flat 8-tab bar is too cramped, so the first four are pinned and the rest live behind a +// "More" tab that opens a sheet above the bar. Keep it ≤ 5 slots including "More". +const MOBILE_PRIMARY = NAV.slice(0, 4); +const MOBILE_OVERFLOW = NAV.slice(4); + export function AppShell({ children }: { children: ReactNode }) { // Read the locale so the whole shell re-renders on a language switch. useLocale(); @@ -108,29 +114,88 @@ export function AppShell({ children }: { children: ReactNode }) { - {/* Mobile bottom tab bar (< sm): the primary navigation on phones. */} + + + ); +} + +/** Mobile bottom navigation (< sm): four pinned tabs + a "More" tab whose sheet holds the rest. */ +function MobileNav() { + const [moreOpen, setMoreOpen] = useState(false); + const pathname = useRouterState({ select: (s) => s.location.pathname }); + // Highlight "More" when the current route lives in the overflow. + const overflowActive = MOBILE_OVERFLOW.some( + (n) => pathname === n.to || pathname.startsWith(`${n.to}/`), + ); + // Fixed two-line-tall label box so a 1- or 2-line label (labels vary by locale) keeps every icon + // at the same height. + const tab = + "flex flex-1 flex-col items-center justify-center gap-1 px-0.5 py-2 text-muted-foreground transition-colors"; + const lbl = + "flex h-7 w-full items-center justify-center text-center text-[10px] leading-tight"; + return ( + <> + {/* Tap-outside backdrop, under the bar (z-50) but over the page. */} + {moreOpen && ( + + - + ); } diff --git a/web/src/sections/Displays/DisplayCard.tsx b/web/src/sections/Displays/DisplayCard.tsx index e6862ba..f5d7e4c 100644 --- a/web/src/sections/Displays/DisplayCard.tsx +++ b/web/src/sections/Displays/DisplayCard.tsx @@ -67,7 +67,7 @@ export const DisplaySection: FC = () => { {m.display_config_title()} -

{m.host_displays_help()}

+

{m.host_displays_help()}

{q.data && draft && ( {/* One-click presets — a 2-up grid so each has room to breathe */} -
- +
+
{PRESET_ORDER.map((id) => { const p = presets.find((x) => x.id === id); @@ -341,7 +341,7 @@ const DisplayForm: FC<{ {`${effective.max_displays}×`}
-

{m.display_pending_note()}

+

{m.display_pending_note()}

{error &&

{error}

}
); @@ -357,7 +357,7 @@ const Field: FC<{ label: string; help?: string; children: ReactNode }> = ({
{children} - {help &&

{help}

} + {help &&

{help}

}
);