The bottom nav rendered under the Android navigation bar, and full-screen
playback controls spilled into unusable screen edges. It looked device-specific
(Motorola bad, Fairphone fine) but every device was equally unpadded — only the
intrusion differed: a tall opaque 3-button bar swallows the nav, a thin
translucent gesture pill overlaps harmlessly.
None of the app's safe-area handling was ever active, for two independent
reasons:
1. app.html had no `viewport-fit=cover`, so every `env(safe-area-inset-*)`
resolved to 0px — the padding in app.css and BottomUi was a no-op.
2. Android WebView maps only the *display cutout* into `env()`; the status bar
and navigation bar are never reported. With enableEdgeToEdge() and
targetSdk 36 (enforced from 35, opt-out ignored from 36) the WebView always
spans them, so CSS could not learn about them by any route.
WindowInsetsBridge now reads `systemBars() | displayCutout()` and publishes
`--jt-inset-*` CSS custom properties, both pushed on every inset change
(rotation, nav-mode switch, PiP) and pullable via `AndroidInsets.get()` — the
pull is required because the first inset pass lands before the document exists
and a page load wipes the pushed inline style. app.css folds them with `env()`
via `max()` into `--safe-*`, the only thing components may pad from.
Exactly one element owns each edge: the shell takes top/left/right, BottomUi
takes bottom (inside its surface box, so the colour extends behind the gesture
bar), and shellReservesBottomInset hands bottom back to the shell on routes with
no bottom UI. The full-screen players inset their control layers only, leaving
video and artwork edge-to-edge.
The theme's `fitsSystemWindows=true` claimed the opposite of what actually
happened — overridden at runtime, ignored at this target SDK — and is removed.
Also converts six nested `h-screen`/`min-h-screen` boxes to `h-full`: the shell
is `h-screen` *and* inset-padded, so its content box is `100vh - safe-top` and
any nested 100vh box overflows by exactly the inset (the library column would
have clipped its own BottomUi). A test guards against reintroduction.
97 lines
3.4 KiB
Svelte
97 lines
3.4 KiB
Svelte
<script lang="ts">
|
|
import { sessions, selectedSession } from "$lib/stores";
|
|
import SessionsList from "$lib/components/sessions/SessionsList.svelte";
|
|
import RemoteControls from "$lib/components/sessions/RemoteControls.svelte";
|
|
|
|
function handleSelectSession(sessionId: string) {
|
|
// Toggle selection - if clicking the same session, deselect it
|
|
if ($sessions.selectedSessionId === sessionId) {
|
|
sessions.selectSession(null);
|
|
} else {
|
|
sessions.selectSession(sessionId);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="min-h-full bg-[var(--color-background)] p-4 md:p-8">
|
|
<div class="max-w-7xl mx-auto">
|
|
<!-- Page Header -->
|
|
<header class="mb-8">
|
|
<h1 class="text-3xl font-bold text-white mb-2">Remote Sessions</h1>
|
|
<p class="text-gray-400">
|
|
Control playback on other Jellyfin clients
|
|
</p>
|
|
</header>
|
|
|
|
<!-- Main Content -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<!-- Sessions List Column -->
|
|
<div class="order-1">
|
|
<SessionsList onSelectSession={handleSelectSession} />
|
|
</div>
|
|
|
|
<!-- Remote Controls Column -->
|
|
<div class="order-2">
|
|
{#if $selectedSession}
|
|
<div class="sticky top-4">
|
|
<RemoteControls session={$selectedSession} />
|
|
</div>
|
|
{:else}
|
|
<!-- Placeholder when no session selected -->
|
|
<div class="flex flex-col items-center justify-center p-12 rounded-lg bg-[var(--color-surface)] text-center">
|
|
<svg class="w-20 h-20 text-gray-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"
|
|
/>
|
|
</svg>
|
|
<h3 class="text-lg font-medium text-gray-400 mb-2">Select a Session</h3>
|
|
<p class="text-sm text-gray-500 max-w-sm">
|
|
Choose a session from the list to control playback on that device
|
|
</p>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Help Text -->
|
|
<div class="mt-8 p-4 rounded-lg bg-[var(--color-surface)] border border-[var(--color-jellyfin)]/20">
|
|
<h3 class="text-sm font-semibold text-white mb-2">How to use Remote Sessions</h3>
|
|
<ul class="text-sm text-gray-400 space-y-1 list-disc list-inside">
|
|
<li>Start playing media on another Jellyfin client (TV, web browser, mobile app)</li>
|
|
<li>The session will appear in the list above automatically</li>
|
|
<li>Click on a session to select it and view playback controls</li>
|
|
<li>Use the controls to play, pause, skip tracks, adjust volume, and seek</li>
|
|
<li>Sessions update automatically - click refresh for immediate updates</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
/* Custom scrollbar for better appearance */
|
|
:global(html) {
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--color-jellyfin) var(--color-surface);
|
|
}
|
|
|
|
:global(::-webkit-scrollbar) {
|
|
width: 8px;
|
|
}
|
|
|
|
:global(::-webkit-scrollbar-track) {
|
|
background: var(--color-surface);
|
|
}
|
|
|
|
:global(::-webkit-scrollbar-thumb) {
|
|
background: var(--color-jellyfin);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
:global(::-webkit-scrollbar-thumb:hover) {
|
|
background: var(--color-jellyfin-hover);
|
|
}
|
|
</style>
|