Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ddba0b974 | ||
|
|
3d7c44e3b2 |
@@ -2,6 +2,8 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- **Video on the kiosk stops tearing.** The kiosk's display had no vertical sync at all, so fast motion — IndeedHub films especially — showed horizontal tearing lines. The display driver now syncs every frame to the panel (no extra hardware needed, existing kiosks pick it up with this update), and on machines with a GPU, video decoding moves off the CPU onto the video hardware — smoother playback that also leaves more headroom for audio, not less.
|
||||
- **The Back button finally does what you expect.** Pressing Back — the mouse's side button on a kiosk, a swipe on a phone, the toolbar button in any browser — used to navigate the screen underneath an open window, or leave the dashboard entirely. Back now closes the topmost open window first, one per press, exactly like a native app; closing a window yourself never leaves a phantom entry that makes you press Back twice.
|
||||
- **No more bare IP addresses in your update or app-registry settings.** The update mirrors and the app registry each listed the same server twice — once by its proper name, once as a raw `http://146…` address left over from before the domain existed. The raw-address entries are retired: new nodes never see them, and existing nodes clean them out of their saved lists automatically on the next read. Everything now goes through the named, TLS-protected origin — which was always the same machine.
|
||||
- **The phone companion app downloads over the proper domain.** The download QR pointed at a raw address over plain HTTP; it now points at the same file on the https domain. Scanning it gets you an encrypted download from a named server.
|
||||
- **The Receive window now tells you when the money is on its way.** Previously it showed a QR code and left you to check elsewhere whether anything happened. Now, the moment the sender's transaction is broadcast, the QR gives way to a clock: the amount, the transaction ID (tap to copy), and a note that the funds arrive on their own — with a single Done button. If you keep the window open, the clock becomes a green check at the first confirmation. Verified live on a real node: payment detected within seconds of broadcast.
|
||||
|
||||
@@ -408,6 +408,10 @@ DOCKERFILE_HEAD
|
||||
xorg \
|
||||
xdotool \
|
||||
chromium \
|
||||
mesa-va-drivers \
|
||||
intel-media-va-driver \
|
||||
i965-va-driver \
|
||||
vainfo \
|
||||
pipewire \
|
||||
pipewire-pulse \
|
||||
pipewire-alsa \
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
#!/bin/bash
|
||||
|
||||
# TearFree BEFORE X starts: bare Xorg with the stock modesetting driver has
|
||||
# no vsync and no compositor, so video page-flips land mid-scanout — visible
|
||||
# tearing on every kiosk (reported 2026-08-11, "really bad" on IndeedHub
|
||||
# playback). The modesetting driver's TearFree option double-buffers the
|
||||
# scanout at the driver level: no compositor needed, one frame of latency,
|
||||
# no interaction with the 2026-06-28 choppy-audio GPU decisions. Written
|
||||
# here (idempotently) rather than baked into the image so existing kiosk
|
||||
# nodes pick it up through the launcher's own OTA path (bootstrap.rs
|
||||
# reinstalls this script on every node).
|
||||
XORG_CONF_DIR=/etc/X11/xorg.conf.d
|
||||
XORG_TEARFREE="$XORG_CONF_DIR/20-archipelago-kiosk-tearfree.conf"
|
||||
mkdir -p "$XORG_CONF_DIR"
|
||||
if [ ! -f "$XORG_TEARFREE" ] || ! grep -q TearFree "$XORG_TEARFREE"; then
|
||||
cat > "$XORG_TEARFREE" <<'EOF'
|
||||
# Written by archipelago-kiosk-launcher — vsynced scanout for kiosk video.
|
||||
Section "Device"
|
||||
Identifier "Archipelago Kiosk GPU"
|
||||
Driver "modesetting"
|
||||
Option "TearFree" "true"
|
||||
EndSection
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Start a dedicated X server for the attached kiosk display.
|
||||
/usr/bin/Xorg :0 vt1 -nolisten tcp -keeptty &
|
||||
XPID=$!
|
||||
@@ -157,8 +180,16 @@ sleep 1
|
||||
# On a GPU-less / headless server (no /dev/dri), disable GPU entirely instead.
|
||||
if [ -e /dev/dri/card0 ] || [ -e /dev/dri/renderD128 ]; then
|
||||
GPU_FLAGS="--in-process-gpu --num-raster-threads=1"
|
||||
# Hardware VIDEO DECODE (VA-API) on GPU hardware. Orthogonal to the
|
||||
# GpuRasterization ban above: decode offload REDUCES the CPU pressure
|
||||
# that caused the choppy-audio incident, it doesn't re-create it.
|
||||
# Falls back silently to software decode when the platform lacks a
|
||||
# va driver — never a black player. IgnoreDriverChecks: older Intel
|
||||
# gens (HD 5500-era kiosks) are wrongly blocklisted upstream.
|
||||
ENABLE_FEATURES="OverlayScrollbar,VaapiVideoDecodeLinuxGL,VaapiIgnoreDriverChecks"
|
||||
else
|
||||
GPU_FLAGS="--disable-gpu --num-raster-threads=1"
|
||||
ENABLE_FEATURES="OverlayScrollbar"
|
||||
fi
|
||||
|
||||
ARCHIPELAGO_UID=$(id -u archipelago)
|
||||
@@ -194,7 +225,7 @@ while true; do
|
||||
--no-first-run \
|
||||
--check-for-update-interval=31536000 \
|
||||
--disable-features=TranslateUI,MetricsReporting,AutofillServerCommunication,PasswordManagerEnabled,GpuRasterization \
|
||||
--enable-features=OverlayScrollbar \
|
||||
--enable-features=$ENABLE_FEATURES \
|
||||
--disable-session-crashed-bubble \
|
||||
--disable-save-password-bubble \
|
||||
--disable-suggestions-service \
|
||||
|
||||
@@ -54,6 +54,7 @@ import { ref, computed, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useModalKeyboard } from '@/composables/useModalKeyboard'
|
||||
import { useBodyScrollLock } from '@/composables/useBodyScrollLock'
|
||||
import { useModalHistory } from '@/composables/useModalHistory'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
show: boolean
|
||||
@@ -105,6 +106,10 @@ function close() {
|
||||
|
||||
useModalKeyboard(modalRef, computed(() => props.show), close)
|
||||
useBodyScrollLock(computed(() => props.show))
|
||||
// Browser/mouse/gesture Back closes the modal instead of navigating the
|
||||
// router out from under it — the native-app behaviour kiosk and mobile
|
||||
// browsers expect (the companion webview already provides it natively).
|
||||
useModalHistory(computed(() => props.show), close)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// Back/forward integration for modals (kiosk, remote browsers, mobile).
|
||||
//
|
||||
// Without this, the browser's Back control (mouse side-button on kiosk,
|
||||
// gesture on mobile, toolbar button in a remote browser) navigates the
|
||||
// ROUTER while a modal is open — at best closing the whole screen under a
|
||||
// dialog, at worst leaving the app. The native-app expectation, and what
|
||||
// the companion webview already provides, is: Back closes the topmost
|
||||
// dialog first.
|
||||
//
|
||||
// Mechanics: opening a modal pushes one history entry (same URL, a depth
|
||||
// marker in state — router keys are preserved by spreading the existing
|
||||
// state). A popstate that lands BELOW our depth means the user pressed
|
||||
// Back over an open modal: close the topmost one. A UI-side close (X,
|
||||
// backdrop, Esc) consumes its own entry with history.back() so Back never
|
||||
// needs pressing twice — guarded by the depth marker so it can never eat
|
||||
// a router entry. One module-level stack serves every BaseModal instance,
|
||||
// so stacked modals close one per Back, top first.
|
||||
import { watch, type Ref } from 'vue'
|
||||
|
||||
type Entry = { close: () => void }
|
||||
|
||||
const stack: Entry[] = []
|
||||
// Set when a popstate initiated the close: the history entry is already
|
||||
// gone, so the close-side cleanup must not call history.back() again.
|
||||
let poppedClose = false
|
||||
let listening = false
|
||||
|
||||
function modalDepth(state: unknown): number {
|
||||
return (state as { __archyModal?: number } | null)?.__archyModal ?? 0
|
||||
}
|
||||
|
||||
function ensureListener() {
|
||||
if (listening || typeof window === 'undefined') return
|
||||
listening = true
|
||||
window.addEventListener('popstate', (e) => {
|
||||
// Landed at a depth below the open-modal count → this Back was aimed
|
||||
// at the topmost modal. One entry per Back press: close exactly one.
|
||||
// (A popstate at or above our depth is someone else's navigation —
|
||||
// e.g. our own cleanup back, or a forward — leave it alone.)
|
||||
if (modalDepth(e.state) < stack.length) {
|
||||
const top = stack[stack.length - 1]
|
||||
if (top) {
|
||||
poppedClose = true
|
||||
top.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** Call from a modal component with its visibility and close trigger. */
|
||||
export function useModalHistory(show: Ref<boolean>, close: () => void) {
|
||||
ensureListener()
|
||||
const entry: Entry = { close }
|
||||
watch(show, (open, was) => {
|
||||
if (open === was) return
|
||||
if (open) {
|
||||
stack.push(entry)
|
||||
try {
|
||||
// Preserve vue-router's own keys in state — clobbering them breaks
|
||||
// its scroll restoration and position tracking.
|
||||
window.history.pushState(
|
||||
{ ...(window.history.state ?? {}), __archyModal: stack.length },
|
||||
'',
|
||||
)
|
||||
} catch { /* history can throw in exotic embeds — modal still works */ }
|
||||
} else {
|
||||
const wasTop = stack[stack.length - 1] === entry
|
||||
const i = stack.indexOf(entry)
|
||||
if (i >= 0) stack.splice(i, 1)
|
||||
if (poppedClose) {
|
||||
poppedClose = false
|
||||
return
|
||||
}
|
||||
// UI-side close of the top modal: consume the entry we pushed, but
|
||||
// only if it is still the current one (a route change after opening
|
||||
// moves history past it — backing out then would eat a real entry).
|
||||
if (wasTop && modalDepth(window.history.state) > stack.length) {
|
||||
try { window.history.back() } catch { /* same guard as above */ }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user