DUNGEONS-34: implement Webka local streaming app

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
2026-09-06 16:23:09 +05:00
parent 5540b9a1f4
commit 7c737c1011
36 changed files with 21596 additions and 0 deletions

88
frontend/pages/watch.vue Normal file
View File

@@ -0,0 +1,88 @@
<script setup lang="ts">
import {
Maximize2,
Minimize2,
LogOut,
Radio,
Wifi,
RotateCw,
} from "lucide-vue-next";
const { t } = useI18n();
useHead({ title: computed(() => `${t("nav.watch")} · Webka`) });
const session = useSessionStore();
const { remote, state, error, resolution, start, stop } = useStream("viewer");
const player = ref<HTMLElement>(),
full = ref(false),
actionError = ref("");
onMounted(() => {
void start();
document.addEventListener("fullscreenchange", changed);
});
onBeforeUnmount(() =>
document.removeEventListener("fullscreenchange", changed),
);
function changed() {
full.value = !!document.fullscreenElement;
}
async function fullscreen() {
try {
if (document.fullscreenElement) await document.exitFullscreen();
else await player.value?.requestFullscreen();
} catch {
actionError.value = t("errors.fullscreen");
}
}
async function disconnect() {
stop();
await session.logout();
}
</script>
<template>
<div ref="player" class="watch-page">
<StreamVideo v-if="remote" :stream="remote" />
<div v-else class="watch-empty">
<Radio :size="44" :stroke-width="1.2" />
<div class="eyebrow">{{ $t(`stream.${state}`) }}</div>
<h1>
{{
$t(
state === "waiting"
? "watch.waitingTitle"
: "watch.connectingTitle",
)
}}
</h1>
<p>
{{
$t(state === "waiting" ? "watch.waitingHint" : "watch.connectingHint")
}}
</p>
<GlassButton v-if="state === 'offline'" @click="start"
><RotateCw :size="18" />{{ $t("watch.reconnect") }}</GlassButton
>
</div>
<p v-if="error || actionError" class="error watch-error" role="alert">
{{ error || actionError }}
</p>
<GlassCard class="watch-controls"
><span class="watch-state" role="status"
><span class="status-dot" :class="{ live: state === 'live' }" />{{
$t(`stream.${state}`)
}}</span
><span class="watch-quality"
><Wifi :size="17" />{{ resolution || $t("quality.auto") }}</span
><GlassButton
class="icon-button"
:aria-label="$t('watch.fullscreen')"
@click="fullscreen"
><Minimize2 v-if="full" :size="20" /><Maximize2
v-else
:size="20" /></GlassButton
><GlassButton @click="disconnect"
><LogOut :size="18" /><span>{{
$t("watch.disconnect")
}}</span></GlassButton
><LanguageSwitcher v-if="full"
/></GlassCard>
</div>
</template>