commit 76

This commit is contained in:
Moritz Ruth 2025-03-13 00:48:21 +01:00
parent b90f897123
commit 18dc17d6d5
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
46 changed files with 2032 additions and 2096 deletions

3
ui/.gitignore vendored
View file

@ -1,4 +1,5 @@
.idea/ .idea/
node_modules/ node_modules/
*.env
dist/ dist/
src/generated-types
*.env

View file

@ -1 +0,0 @@
18

View file

@ -1,13 +1,14 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Moira</title> <title>Janus</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link href="./node_modules/@fontsource-variable/manrope/index.css" rel="stylesheet"/> <link href="./node_modules/@fontsource-variable/manrope/index.css" rel="stylesheet"/>
<script type="module" src="./src/main.ts"></script> <script type="module" src="./src/main.ts"></script>
</head> <link rel="stylesheet" href="src/global.scss"/>
<body> </head>
<div id="app"></div> <body>
</body> <div id="app"></div>
</body>
</html> </html>

View file

@ -1,31 +1,35 @@
{ {
"name": "moira", "name": "janus-ui",
"private": true,
"version": "1.0.0", "version": "1.0.0",
"author": "Moritz Ruth <dev@moritzruth.de>", "author": "Moritz Ruth <dev@moritzruth.de>",
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module",
"scripts": { "scripts": {
"dev": "vite --port 3000 --host", "dev": "vite --port 3000 --host",
"build": "vite build --emptyOutDir --outDir ../src/main/resources/ui" "build": "vite build --emptyOutDir --outDir ../src/main/resources/ui"
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/ph": "^1.1.5", "@iconify-json/ph": "^1.2.2",
"@types/lodash-es": "^4.17.7", "@types/lodash-es": "^4.17.12",
"@vitejs/plugin-vue": "^4.2.3", "@types/node": "^22.13.10",
"typescript": "^5.0.4", "@unocss/preset-wind3": "^66.1.0-beta.3",
"unplugin-icons": "^0.16.1", "unocss": "^66.1.0-beta.3",
"vite": "^4.3.9", "@vitejs/plugin-vue": "^5.2.1",
"vite-plugin-pages": "^0.30.1", "typescript": "^5.8.2",
"vite-plugin-windicss": "^1.9.0", "unplugin-icons": "^22.1.0",
"windicss": "^3.5.6" "unplugin-vue-router": "^0.12.0",
"vite": "^6.2.1"
}, },
"dependencies": { "dependencies": {
"@fontsource-variable/manrope": "^5.0.0", "@fontsource-variable/manrope": "^5.2.5",
"@vueuse/core": "^10.1.2", "@vueuse/core": "^13.0.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"modern-normalize": "^3.0.1",
"reconnecting-websocket": "^4.4.0", "reconnecting-websocket": "^4.4.0",
"sass": "^1.62.1", "sass": "^1.85.1",
"vue": "^3.3.4", "vue": "^3.5.13",
"vue-router": "^4.2.1" "vue-router": "^4.5.0"
}, },
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [

2016
ui/pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,32 +1,39 @@
<template> <template>
<div id="app" class="bg-black text-white"> <div v-if="isConnecting" class="flex flex-col justify-center items-center h-100dvh gap-4">
<div class="flex flex-col justify-center items-center h-full space-y-4" v-if="isConnecting"> <div class="font-bold text-10">Connecting</div>
<div class="font-bold text-10">Connecting...</div> <div class="text-s1">Janus, created by Moritz Ruth</div>
<div class="text-s1">Created by Moritz Ruth</div>
</div>
<router-view v-else/>
<TimeDisplay/>
</div> </div>
<div v-else class="h-100dvh flex flex-col">
<div class="font-black text-2xl md:text-4xl p-4 md:p-8 pb-0 md:pb-0 flex-shrink-0">
{{ current.act === null ? "" : `${current.act.name}` }}{{ current.scene.name }}
</div>
<div class="flex-grow p-4 md:p-8 overflow-hidden">
<router-view/>
</div>
<MusicProgressBar class="h-10 flex-shrink-0"/>
</div>
<TimeDisplay/>
</template> </template>
<style lang="scss"> <style lang="scss">
html, body, #app { .list-move,
width: 100vw; .list-enter-active,
height: 100vh; .list-leave-active {
overflow: hidden; transition: all 0.5s ease;
font-family: "Manrope Variable", sans-serif;
} }
::-webkit-scrollbar { .list-leave-to {
width: 10px; opacity: 0;
transform: translateX(10px);
} }
::-webkit-scrollbar-thumb { .list-enter-from {
background: rgb(255 255 255 / 10%); opacity: 0;
transform: translateX(-10px);
}
&:hover { .list-leave-active {
background: rgb(255 255 255 / 20%); position: absolute;
}
} }
</style> </style>
@ -34,10 +41,17 @@
import { connect } from "./syncing" import { connect } from "./syncing"
import { ref } from "vue" import { ref } from "vue"
import TimeDisplay from "./components/TimeDisplay.vue" import TimeDisplay from "./components/TimeDisplay.vue"
import { current } from "@/state.js"
import MusicProgressBar from "@/components/MusicProgressBar.vue"
export default { export default {
name: "App", name: "App",
components: { TimeDisplay }, computed: {
current() {
return current
}
},
components: { MusicProgressBar, TimeDisplay },
setup() { setup() {
const isConnecting = ref(true) const isConnecting = ref(true)
connect().then(() => { connect().then(() => {

View file

@ -1,13 +1,15 @@
<template> <template>
<div class="p-2 space-y-8"> <div class="overflow-y-auto">
<div> <transition-group name="list" tag="div">
<div class="pb-2 font-bold text-gray-400 text-2 tracking-wider uppercase"> <div v-if="current.step.actorsOnStage.length === 0" key="" class="text-gray-400">Niemand</div>
auf der Bühne <div
v-for="actor in current.step.actorsOnStage"
:key="parseStringWithDetails(actor).main"
class="truncate text-4.5"
>
{{ actor }}
</div> </div>
<div> </transition-group>
<EntrancesList :entrances="current.step.actorsOnStage"/>
</div>
</div>
</div> </div>
</template> </template>
@ -16,8 +18,5 @@
</style> </style>
<script setup lang="ts"> <script setup lang="ts">
import EntrancesList from "./EntrancesList.vue" import { current, parseStringWithDetails } from "@/state"
import { current, getNextValidPosition, getStep, parseStringWithDetails, ShowPosition, state, Step, show } from "../state"
import { computed } from "vue"
import { intersection } from "lodash-es"
</script> </script>

View file

@ -1,23 +1,27 @@
<template> <template>
<button <button
class="px-5 py-2 active:bg-green-800 transition duration-200 font-bold text-5" :class="[$style.root, isActive ? 'bg-green-800' : 'bg-green-600']"
:class="[$style.root, isActive ? 'bg-green-800' : 'bg-green-600']" class="px-5 py-2 active:bg-green-800 transition duration-200 font-bold text-4"
@click="e => emit('click', e)" @click="e => emit('click', e)"
> >
<slot/> <slot/>
</button> </button>
</template> </template>
<style module> <style module>
.root { .root {
user-select: none; user-select: none;
} }
</style> </style>
<script setup lang="ts"> <script lang="ts" setup>
const props = defineProps<{ const props = defineProps<{
isActive?: boolean isActive?: boolean
}>() }>()
const emit = defineEmits(["click"]) const emit = defineEmits<{
click: [MouseEvent]
pointerdown: [PointerEvent]
pointerup: [PointerEvent]
}>()
</script> </script>

View file

@ -1,43 +1,38 @@
<template> <template>
<div <div :class="$style.root" :data-blinking="isBlinking">
class="flex flex-col justify-center py-4" <slot/>
:class="$style.root" </div>
:data-blinking="isBlinking"
>
<slot/>
</div>
</template> </template>
<style module> <style module>
.root[data-blinking="true"] { .root[data-blinking="true"] {
animation: alternate infinite 1000ms ease-in-out pulse; animation: alternate infinite 1000ms ease-in-out pulse;
} }
@keyframes pulse { @keyframes pulse {
from { from {
@apply bg-red-900; @apply bg-red-900;
} }
to { to {
@apply bg-transparent; @apply bg-transparent;
} }
} }
</style> </style>
<script setup lang="ts"> <script lang="ts" setup>
import { toRef, watch } from "vue" import { computed, toRef, watch } from "vue"
import { autoResetRef } from "@vueuse/core" import { autoResetRef } from "@vueuse/core"
import { computed } from "vue"
const props = defineProps<{ const props = defineProps<{
value: unknown value: unknown
blinkSeconds: number blinkSeconds: number
}>() }>()
const value = toRef(props, "value") const value = toRef(props, "value")
const isBlinking = autoResetRef(false, computed(() => props.blinkSeconds * 1000)) const isBlinking = autoResetRef(false, computed(() => props.blinkSeconds * 1000))
watch(value, () => { watch(value, () => {
isBlinking.value = true isBlinking.value = true
}) })
</script> </script>

View file

@ -1,82 +1,83 @@
<template> <template>
<div class="flex items-start space-x-2" :class="textClass ?? 'text-4'"> <div :class="textClass ?? 'text-4'" class="flex items-start gap-2 py-0.5">
<component :is="icon" class="mt-0.5 flex-shrink-0"/> <component :is="icon" class="flex-shrink-0"/>
<div :class="singleLine && 'truncate'"> <div :class="singleLine && 'truncate'">
{{ text }} {{ text }}
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script lang="ts" setup>
import MusicNoteIcon from "virtual:icons/ph/music-note" import MusicNoteIcon from "virtual:icons/ph/music-note"
import ChatCircleTextIcon from "virtual:icons/ph/chat-circle-TEXT" import ChatCircleTextIcon from "virtual:icons/ph/chat-circle-TEXT"
import StopIcon from "virtual:icons/ph/stop" import StopIcon from "virtual:icons/ph/stop"
import HeadlightsIcon from "virtual:icons/ph/headlights"
import WarningIcon from "virtual:icons/ph/warning" import WarningIcon from "virtual:icons/ph/warning"
import ArrowsOutLineHorizontalIcon from "virtual:icons/ph/arrows-out-line-horizontal" import ArrowsOutLineHorizontalIcon from "virtual:icons/ph/arrows-out-line-horizontal"
import ArrowsInLineHorizontalIcon from "virtual:icons/ph/arrows-in-line-horizontal" import ArrowsInLineHorizontalIcon from "virtual:icons/ph/arrows-in-line-horizontal"
import DotFillIcon from "virtual:icons/ph/dot-fill" import DotFillIcon from "virtual:icons/ph/dot-fill"
import { computed } from "vue" import { computed } from "vue"
import { START_STEP, Step } from "../state" import { START_STEP, Step } from "@/state"
import { formatSeconds } from "../helpers" import { formatSeconds } from "@/helpers"
import { isEqual } from "lodash-es" import { isEqual } from "lodash-es"
const props = defineProps<{ const props = defineProps<{
step: Step, step: Step,
singleLine?: boolean singleLine?: boolean
textClass?: string textClass?: string
}>() }>()
const icon = computed(() => { const icon = computed(() => {
const cue = props.step.cue const cue = props.step.cue
if (isEqual(props.step.position, START_STEP.position)) { if (isEqual(props.step.position, START_STEP.position)) {
return DotFillIcon return DotFillIcon
} }
switch (cue.type) { switch (cue.type) {
case "CURTAIN": case "CURTAIN":
return cue.state === "closed" return cue.state === "closed"
? ArrowsInLineHorizontalIcon ? ArrowsInLineHorizontalIcon
: ArrowsOutLineHorizontalIcon : ArrowsOutLineHorizontalIcon
case "LIGHT": return HeadlightsIcon case "TEXT":
case "TEXT": return ChatCircleTextIcon return ChatCircleTextIcon
case "MUSIC_START": return MusicNoteIcon
case "MUSIC_END": return StopIcon case "MUSIC_START":
case "CUSTOM": return WarningIcon return MusicNoteIcon
}
case "MUSIC_END":
return StopIcon
case "CUSTOM":
return WarningIcon
}
}) })
const text = computed(() => { const text = computed(() => {
const cue = props.step.cue const cue = props.step.cue
switch (cue.type) { switch (cue.type) {
case "CURTAIN": case "CURTAIN":
if (cue.state === "open") { if (cue.state === "open") {
return cue.whileMoving ? "Der Vorhang öffnet sich" : "Der Vorhang ist geöffnet" return cue.whileMoving ? "Der Vorhang öffnet sich" : "Der Vorhang ist geöffnet"
} else { } else {
return cue.whileMoving ? "Der Vorhang schließt sich" : "Der Vorhang ist geschlossen" return cue.whileMoving ? "Der Vorhang schließt sich" : "Der Vorhang ist geschlossen"
} }
case "LIGHT": case "TEXT":
if (cue.state === "on") { let suffix = ""
return cue.whileFading ? "Das Licht geht an" : "Das Licht ist an" if (cue.clarification !== undefined) suffix = ` (${cue.clarification})`
} else {
return cue.whileFading ? "Das Licht geht aus" : "Das Licht ist aus"
}
case "TEXT": return `${cue.speaker}: »${cue.text}«${suffix}`
let suffix = ""
if (cue.clarification !== undefined) {
suffix = ` (${cue.clarification})`
}
return `${cue.speaker}: »${cue.text}«${suffix}` case "MUSIC_START":
return `${cue.title} [${formatSeconds(cue.duration / 1000)}]`
case "MUSIC_START": return `${cue.title} [${formatSeconds(cue.duration / 1000)}]` case "MUSIC_END":
case "MUSIC_END": return "Ende der Musik" return "Ende der Musik"
case "CUSTOM": return cue.text
} case "CUSTOM":
return cue.text
}
}) })
</script> </script>

View file

@ -1,7 +1,8 @@
<template> <template>
<div> <Button class="w-20" :is-active="!state.isLightBehindCurtainOn" @click="onClick()">{{
<Button :is-active="!state.isLightBehindCurtainOn" @click="onClick()">{{ state.isLightBehindCurtainOn ? "Umbaulicht ist AN" : "Umbaulicht ist AUS" }}</Button> state.isLightBehindCurtainOn ? "AN" : "AUS"
</div> }}
</Button>
</template> </template>
<style module> <style module>
@ -9,16 +10,16 @@
</style> </style>
<script setup lang="ts"> <script setup lang="ts">
import { state } from "../state" import { state } from "@/state"
import Button from "./Button.vue" import Button from "./Button.vue"
function onClick() { function onClick() {
fetch("/api/lightBehindCurtain", { fetch("/api/lightBehindCurtain", {
method: "POST", method: "POST",
body: JSON.stringify(!state.isLightBehindCurtainOn), body: JSON.stringify(!state.isLightBehindCurtainOn),
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
} }
}) })
} }
</script> </script>

View file

@ -1,54 +0,0 @@
<template>
<transition-group tag="div" name="list">
<div
v-for="actor in entrances"
:key="parseStringWithDetails(actor).main"
class="truncate"
>
<span class="font-bold">
{{ parseStringWithDetails(actor).main }}
</span>
<span class="text-gray-400 pl-2">
{{ parseStringWithDetails(actor).details }}
</span>
</div>
</transition-group>
</template>
<style scoped>
.list-move,
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-leave-to {
opacity: 0;
transform: translateX(10px);
}
.list-enter-from {
opacity: 0;
transform: translateX(-10px);
}
.list-leave-active {
position: absolute;
}
</style>
<script lang="ts">
import { PropType } from "vue"
import { parseStringWithDetails } from "../state"
export default {
name: "EntrancesList",
methods: { parseStringWithDetails },
props: {
entrances: {
type: Array as PropType<string[]>,
required: true
}
}
}
</script>

View file

@ -1,86 +1,85 @@
<template> <template>
<div class="flex flex-wrap items-center gap-3"> <div class="flex items-center gap-1">
<span class="-sm:hidden">Nebel:</span> <Button
<Button v-for="p in buttonPowers"
v-for="p in buttonPowers" :is-active="isActive && p[0] === power"
@pointerdown="e => onButtonActive(e, p[0])" @pointerdown="e => onButtonActive(e, p[0])"
@pointerup="e => onButtonInactive(e)" @pointerup="e => onButtonInactive(e)"
:is-active="isActive && p[0] === power" >
> {{ p[0] * 100 }}%
{{ p[0] * 100 }}% </Button>
</Button> </div>
</div>
</template> </template>
<style module> <style module>
</style> </style>
<script setup lang="ts"> <script lang="ts" setup>
import Button from "./Button.vue" import Button from "./Button.vue"
import { computed, ref, watch } from "vue" import { computed, ref, watch } from "vue"
import { onKeyDown, onKeyUp, useEventListener, useIntervalFn } from "@vueuse/core" import { onKeyDown, onKeyUp, useEventListener, useIntervalFn } from "@vueuse/core"
const activation = ref<"button" | "key" | null>(null) const activation = ref<"button" | "key" | null>(null)
const isActive = computed(() => activation.value !== null) const isActive = computed(() => activation.value !== null)
const power = ref(0) const power = ref(0)
function onButtonActive(e: PointerEvent, p: number) { function onButtonActive(e: PointerEvent, p: number) {
// @ts-ignore // @ts-ignore
e.target.setPointerCapture(e.pointerId) e.target.setPointerCapture(e.pointerId)
activation.value = "button" activation.value = "button"
power.value = p power.value = p
} }
function onButtonInactive(e: PointerEvent) { function onButtonInactive(e: PointerEvent) {
// @ts-ignore // @ts-ignore
e.target.releasePointerCapture(e.pointerId) e.target.releasePointerCapture(e.pointerId)
activation.value = null activation.value = null
power.value = 0 power.value = 0
} }
useEventListener(document.body, "pointerup", () => { useEventListener(document.body, "pointerup", () => {
if (activation.value === "button") { if (activation.value === "button") {
activation.value = null activation.value = null
power.value = 0 power.value = 0
} }
}) })
const buttonPowers: Array<[number, string]> = [[0.1, "7"], [0.25, "8"], [0.5, "9"], [1, "0"]] const buttonPowers: Array<[number, string]> = [[0.1, "7"], [0.25, "8"], [0.5, "9"], [1, "0"]]
for (const p of buttonPowers) { for (const p of buttonPowers) {
onKeyDown(p[1], () => { onKeyDown(p[1], () => {
activation.value = "key" activation.value = "key"
power.value = p[0] power.value = p[0]
}) })
onKeyUp(p[1], () => { onKeyUp(p[1], () => {
if (activation.value === "key" && power.value === p[0]) { if (activation.value === "key" && power.value === p[0]) {
activation.value = null activation.value = null
power.value = 0 power.value = 0
} }
}) })
} }
function send() { function send() {
fetch("/api/fog", { fetch("/api/fog", {
method: "POST", method: "POST",
body: JSON.stringify(power.value), body: JSON.stringify(power.value),
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
} }
}) })
} }
const sendTimer = useIntervalFn(send, 200, { immediate: false }) const sendTimer = useIntervalFn(send, 200, { immediate: false })
watch(power, () => { watch(power, () => {
if (activation.value === null) { if (activation.value === null) {
sendTimer.pause() sendTimer.pause()
send() send()
} else { } else {
send() send()
sendTimer.resume() sendTimer.resume()
} }
}) })
</script> </script>

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="overflow-hidden text-6 box p-4" :data-blinking="isBlinking"> <div :data-blinking="isBlinking" class="overflow-hidden text-6 box p-4">
{{ message }} {{ message }}
</div> </div>
</template> </template>

View file

@ -1,23 +1,19 @@
<template> <template>
<div class="h-full max-h-60"> <div class="max-h-60">
<textarea <textarea
:value="state.message" :value="state.message"
id="message-box" class="h-full w-full border border-dark-200 focus:border-green-500 focus:outline-none transition rounded-lg bg-dark-800 p-4 text-5 resize-none"
:class="$style.area" placeholder="Nachricht an alle"
class="h-full w-full border border-dark-200 focus:border-green-500 focus:outline-none transition rounded-lg bg-dark-800 p-4 text-3" @input="e => setMessage((e.target as HTMLInputElement).value)"
placeholder="Nachricht an alle"
@input="e => setMessage(e.target.value)"
/> />
</div> </div>
</template> </template>
<style module> <style module>
.area {
resize: none;
}
</style> </style>
<script setup lang="ts"> <script setup lang="ts">
import { state } from "../state" import { state } from "@/state"
import { setMessage } from "../syncing" import { setMessage } from "@/syncing"
</script> </script>

View file

@ -1,18 +1,18 @@
<template> <template>
<div class="flex flex-col space-y-5" :class="scrollable ? 'overflow-y-auto' : 'overflow-hidden'"> <div :class="scrollable ? 'overflow-y-auto' : 'overflow-hidden'" class="flex flex-col space-y-5">
<div v-for="(act, actIndex) in show.acts"> <div v-for="(act, actIndex) in show.acts">
<MotionsListAct :act="act" :center-current="Boolean(centerCurrent)"/> <MotionsListAct :act="act" :center-current="Boolean(centerCurrent)"/>
</div> </div>
<div class="h-[50%] flex-shrink-0"/> <div class="h-[50%] flex-shrink-0"/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script lang="ts" setup>
import MotionsListAct from "./MotionsListAct.vue" import MotionsListAct from "./MotionsListAct.vue"
import { show } from "../state" import { show } from "../state"
const props = defineProps<{ const props = defineProps<{
centerCurrent?: boolean centerCurrent?: boolean
scrollable?: boolean scrollable?: boolean
}>() }>()
</script> </script>

View file

@ -1,83 +1,83 @@
<template> <template>
<div class="pt-2"> <div class="pt-2">
<div class="font-bold text-7 flex gap-5 items-center pb-4"> <div class="font-bold text-7 flex gap-5 items-center pb-4">
<span class="flex-grow h-2px bg-white"></span> <span class="flex-grow h-2px bg-white"></span>
<span>{{ act.name }}</span> <span>{{ act.name }}</span>
<span class="flex-grow h-2px bg-white"></span> <span class="flex-grow h-2px bg-white"></span>
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div <div
v-for="(scene, sceneIndex) in scenes" v-for="(scene, sceneIndex) in scenes"
:key="sceneIndex" :key="sceneIndex"
> >
<div class="text-gray-400 pl-3 text-5"> <div class="text-gray-400 pl-3 text-5">
{{ scene.name }} {{ scene.name }}
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
<template v-for="step in scene.steps" :key="step.position"> <template v-for="step in scene.steps" :key="step.position">
<MotionsListStep <MotionsListStep
:step="step" :center-current="centerCurrent"
:more-positions="step.morePositions" :more-positions="step.morePositions"
:center-current="centerCurrent" :step="step"
/> />
</template> </template>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<style module> <style module>
</style> </style>
<script setup lang="ts"> <script lang="ts" setup>
import { Act, Scene, ShowPosition, Step } from "../state" import { Act, Scene, ShowPosition, Step } from "../state"
import { computed } from "vue" import { computed } from "vue"
import MotionsListStep from "./MotionsListStep.vue" import MotionsListStep from "./MotionsListStep.vue"
const props = defineProps<{ const props = defineProps<{
act: Act act: Act
centerCurrent: boolean centerCurrent: boolean
}>() }>()
interface MotionStep extends Step { interface MotionStep extends Step {
morePositions: ShowPosition[] morePositions: ShowPosition[]
} }
interface MotionScene extends Scene { interface MotionScene extends Scene {
steps: MotionStep[] steps: MotionStep[]
} }
const scenes = computed(() => { const scenes = computed(() => {
const all = props.act.scenes const all = props.act.scenes
const result: MotionScene[] = [] const result: MotionScene[] = []
for (const scene of all) { for (const scene of all) {
const steps: MotionStep[] = [] const steps: MotionStep[] = []
let accStep: MotionStep | null = null let accStep: MotionStep | null = null
for (const step of scene.steps) { for (const step of scene.steps) {
if ((step.position.step === 0) || step.actorEntrances.length > 0 || step.actorExits.length > 0) { if ((step.position.step === 0) || step.actorEntrances.length > 0 || step.actorExits.length > 0) {
if (accStep !== null) steps.push(accStep) if (accStep !== null) steps.push(accStep)
accStep = { accStep = {
...step, ...step,
morePositions: [] morePositions: []
} }
} else { } else {
if (accStep !== null) accStep.morePositions.push(step.position) if (accStep !== null) accStep.morePositions.push(step.position)
} }
} }
if (accStep !== null) steps.push(accStep) if (accStep !== null) steps.push(accStep)
if (steps.length > 0) result.push({ if (steps.length > 0) result.push({
...scene, ...scene,
steps: steps steps: steps
}) })
} }
return result return result
}) })
</script> </script>

View file

@ -1,80 +1,80 @@
<template> <template>
<div class="transition p-3" :class="isActive && 'bg-green-800'"> <div :class="isActive && 'bg-green-800'" class="transition p-3">
<div class="flex space-x-2"> <div class="flex space-x-2">
<div class="flex-grow"> <div class="flex-grow">
<CueBox text-class="text-6" :step="step"/> <CueBox :step="step" text-class="text-6"/>
<div v-if="step.actorEntrances.length + step.actorExits.length > 0" class="py-2 pl-8 space-y-2 text-7"> <div v-if="step.actorEntrances.length + step.actorExits.length > 0" class="py-2 pl-8 space-y-2 text-7">
<div class="flex flex-col space-y-1"> <div class="flex flex-col space-y-1">
<div <div
v-for="motion in step.actorEntrances" v-for="motion in step.actorEntrances"
:key="motion" :key="motion"
class="flex items-center" class="flex items-center"
> >
<CaretDoubleRightIcon/> <CaretDoubleRightIcon/>
<div class="pl-2"> <div class="pl-2">
<span class="font-bold"> <span class="font-bold">
{{ parseStringWithDetails(motion).main }} {{ parseStringWithDetails(motion).main }}
</span> </span>
<span class="pl-1.5"> <span class="pl-1.5">
{{ parseStringWithDetails(motion).details }} {{ parseStringWithDetails(motion).details }}
</span> </span>
</div> </div>
</div> </div>
<div <div
v-for="motion in step.actorExits" v-for="motion in step.actorExits"
:key="motion" :key="motion"
class="flex items-center" class="flex items-center"
> >
<CaretDoubleLeftIcon/> <CaretDoubleLeftIcon/>
<div class="pl-2"> <div class="pl-2">
<span class="font-bold"> <span class="font-bold">
{{ parseStringWithDetails(motion).main }} {{ parseStringWithDetails(motion).main }}
</span> </span>
<span class="pl-1.5"> <span class="pl-1.5">
{{ parseStringWithDetails(motion).details }} {{ parseStringWithDetails(motion).details }}
</span> </span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div v-if="step.hasChangedProps" class="flex space-x-2 pt-0.5 text-6"> <div v-if="step.hasChangedProps" class="flex space-x-2 pt-0.5 text-6">
<BarricadeIcon class="mt-0.5"/> <BarricadeIcon class="mt-0.5"/>
<span>Umbau</span> <span>Umbau</span>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script lang="ts" setup>
import { parseStringWithDetails, ShowPosition, START_STEP, state, Step } from "../state" import { parseStringWithDetails, ShowPosition, START_STEP, state, Step } from "../state"
import CueBox from "./CueBox.vue" import CueBox from "./CueBox.vue"
import CaretDoubleRightIcon from "virtual:icons/ph/caret-double-right" import CaretDoubleRightIcon from "virtual:icons/ph/caret-double-right"
import CaretDoubleLeftIcon from "virtual:icons/ph/caret-double-left" import CaretDoubleLeftIcon from "virtual:icons/ph/caret-double-left"
import BarricadeIcon from "virtual:icons/ph/barricade" import BarricadeIcon from "virtual:icons/ph/barricade"
import { isEqual } from "lodash-es" import { isEqual } from "lodash-es"
import { toRef, computed, watchEffect } from "vue" import { computed, toRef, watchEffect } from "vue"
import { useCurrentElement } from "@vueuse/core" import { useCurrentElement } from "@vueuse/core"
const props = defineProps<{ const props = defineProps<{
step: Step, step: Step,
morePositions?: ShowPosition[] morePositions?: ShowPosition[]
centerCurrent: boolean centerCurrent: boolean
}>() }>()
const position = toRef(state, "position") const position = toRef(state, "position")
const element = useCurrentElement() const element = useCurrentElement()
const allPositions = computed(() => [props.step.position, ...(props.morePositions ?? [])]) const allPositions = computed(() => [props.step.position, ...(props.morePositions ?? [])])
const isActive = computed(() => allPositions.value.some(p => isEqual(p, position.value))) const isActive = computed(() => allPositions.value.some(p => isEqual(p, position.value)))
watchEffect(() => { watchEffect(() => {
const p = props.step.position const p = props.step.position
if (isActive.value || (p.act + p.scene + p.step === 0 && isEqual(START_STEP.position, position.value))) { if (isActive.value || (p.act + p.scene + p.step === 0 && isEqual(START_STEP.position, position.value))) {
element.value?.scrollIntoView({ element.value?.scrollIntoView({
behavior: "smooth", behavior: "smooth",
block: props.centerCurrent ? "center" : "start", block: props.centerCurrent ? "center" : "start",
inline: "nearest" inline: "nearest"
}) })
} }
}) })
</script> </script>

View file

@ -1,13 +1,13 @@
<template> <template>
<div class="relative"> <div class="relative">
<div <div
class="bg-green-700 h-full transition-all" :class="barClass"
:style="{ width: (progress * 100) + '%' }" :style="{ width: (progress * 100) + '%' }"
:class="barClass" class="bg-green-700 h-full transition-all"
></div> ></div>
<div <div
class="absolute left-0 top-0 w-full h-full px-4 text-4 text-white font-bold flex items-center transition flex justify-between" :class="music === null ? 'opacity-0' : 'opacity-100'"
:class="music === null ? 'opacity-0' : 'opacity-100'" class="absolute left-0 top-0 w-full h-full px-4 text-4 text-white font-bold flex items-center transition flex justify-between"
> >
<div> <div>
{{ lastMusic?.title ?? "" }} {{ lastMusic?.title ?? "" }}
@ -35,8 +35,8 @@
} }
</style> </style>
<script setup lang="ts"> <script lang="ts" setup>
import { state, current } from "../state" import { current, state } from "../state"
import { computed, useCssModule } from "vue" import { computed, useCssModule } from "vue"
import { avoidNull, formatSeconds } from "../helpers" import { avoidNull, formatSeconds } from "../helpers"
import { syncedTime } from "../syncing" import { syncedTime } from "../syncing"
@ -47,27 +47,27 @@
const styles = useCssModule() const styles = useCssModule()
const deltaInSeconds = computed(() => { const deltaInSeconds = computed(() => {
if (music.value === null) return 0 if (music.value === null) return 0
return (syncedTime.value - state.musicStartTime) / 1000 return (syncedTime.value - state.musicStartTime) / 1000
}) })
const progress = computed(() => { const progress = computed(() => {
if (music.value === null) return 0 if (music.value === null) return 0
return Math.min(1, deltaInSeconds.value / (music.value.duration / 1000)) return Math.min(1, deltaInSeconds.value / (music.value.duration / 1000))
}) })
const remainingSeconds = computed(() => { const remainingSeconds = computed(() => {
if (lastMusic.value === null) return 0 if (lastMusic.value === null) return 0
return Math.max(0, (lastMusic.value.duration / 1000) - deltaInSeconds.value) return Math.max(0, (lastMusic.value.duration / 1000) - deltaInSeconds.value)
}) })
const barClass = computed(() => { const barClass = computed(() => {
if (lastMusic.value === null) return "bg-gray-700" if (lastMusic.value === null) return "bg-gray-700"
if (remainingSeconds.value < 5) return styles.pulse + " bg-red-600" if (remainingSeconds.value < 5) return styles.pulse + " bg-red-600"
if (remainingSeconds.value < 10) return "bg-red-600" if (remainingSeconds.value < 10) return "bg-red-600"
if (remainingSeconds.value < 20) return "bg-orange-600" if (remainingSeconds.value < 20) return "bg-orange-600"
return "bg-blue-700" return "bg-blue-700"
}) })
</script> </script>

View file

@ -1,10 +1,10 @@
<template> <template>
<ChangeBlinkingBox class="items-center" :blink-seconds="20" :value="prop"> <ChangeBlinkingBox :blink-seconds="20" :value="prop" class="items-center">
<div class="text-s1 tracking-wide text-gray-500"> <div class="text-s1 tracking-wide text-gray-500">
{{ positionName }} {{ positionName }}
</div> </div>
<div class="flex-grow w-full"> <div class="flex-grow w-full">
<transition name="fade" mode="out-in"> <transition mode="out-in" name="fade">
<div :key="prop" class="flex flex-col items-center justify-center"> <div :key="prop" class="flex flex-col items-center justify-center">
<template v-if="prop !== null"> <template v-if="prop !== null">
<div class="font-bold text-3 text-center"> <div class="font-bold text-3 text-center">
@ -32,21 +32,21 @@
} }
</style> </style>
<script setup lang="ts"> <script lang="ts" setup>
import { toRef, watch } from "vue" import { toRef, watch } from "vue"
import { autoResetRef } from "@vueuse/core" import { autoResetRef } from "@vueuse/core"
import { parseStringWithDetails } from "../state" import { parseStringWithDetails } from "../state"
import ChangeBlinkingBox from "./ChangeBlinkingBox.vue" import ChangeBlinkingBox from "./ChangeBlinkingBox.vue"
const props = defineProps<{ const props = defineProps<{
prop: string | null prop: string | null
positionName: string positionName: string
}>() }>()
const prop = toRef(props, "prop") const prop = toRef(props, "prop")
const isBlinking = autoResetRef(false, 20 * 1000) const isBlinking = autoResetRef(false, 20 * 1000)
watch(prop, () => { watch(prop, () => {
isBlinking.value = true isBlinking.value = true
}) })
</script> </script>

View file

@ -6,18 +6,18 @@
</div> </div>
</div> </div>
<div :class="$style.row" class="border-t border-b border-dark-300 h-30"> <div :class="$style.row" class="border-t border-b border-dark-300 h-30">
<PropBox position-name="Rechte Vorbühne" :prop="current.step.props.PROSCENIUM_RIGHT"/> <PropBox :prop="current.step.props.PROSCENIUM_RIGHT" position-name="Rechte Vorbühne"/>
<PropBox position-name="Mitte der Vorbühne" :prop="current.step.props.PROSCENIUM_CENTER"/> <PropBox :prop="current.step.props.PROSCENIUM_CENTER" position-name="Mitte der Vorbühne"/>
<PropBox position-name="Linke der Vorbühne" :prop="current.step.props.PROSCENIUM_LEFT"/> <PropBox :prop="current.step.props.PROSCENIUM_LEFT" position-name="Linke der Vorbühne"/>
</div> </div>
<div :class="$style.row" class="flex-grow h-0"> <div :class="$style.row" class="flex-grow h-0">
<PropBox position-name="Rechts" :prop="current.step.props.RIGHT"/> <PropBox :prop="current.step.props.RIGHT" position-name="Rechts"/>
<PropBox position-name="Mitte" :prop="current.step.props.CENTER"/> <PropBox :prop="current.step.props.CENTER" position-name="Mitte"/>
<PropBox position-name="Links" :prop="current.step.props.LEFT"/> <PropBox :prop="current.step.props.LEFT" position-name="Links"/>
</div> </div>
<div :class="$style.row" class="border-t border-dark-300 py-3 h-20"> <div :class="$style.row" class="border-t border-dark-300 py-3 h-20">
<div/> <div/>
<PropBox position-name="Rückwand" :prop="current.step.props.BACKDROP"/> <PropBox :prop="current.step.props.BACKDROP" position-name="Rückwand"/>
<div/> <div/>
</div> </div>
</div> </div>
@ -33,7 +33,7 @@
} }
</style> </style>
<script setup lang="ts"> <script lang="ts" setup>
import { current } from "../state" import { current } from "../state"
import PropBox from "./PropBox.vue" import PropBox from "./PropBox.vue"
</script> </script>

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="flex-grow overflow-y-auto bg-dark-800 flex flex-col pt-2"> <div class="overflow-y-auto bg-dark-800 flex flex-col pt-2">
<StepSelectionStep :step="START_STEP"/> <StepSelectionStep :step="START_STEP"/>
<StepSelectionAct v-for="act in show.acts" :key="act.name" :act="act"/> <StepSelectionAct v-for="act in show.acts" :key="act.name" :act="act"/>
</div> </div>
</template> </template>
@ -9,8 +9,8 @@
</style> </style>
<script setup lang="ts"> <script lang="ts" setup>
import { show, START_STEP } from "../state" import { show, START_STEP } from "@/state"
import StepSelectionAct from "./StepSelectionAct.vue" import StepSelectionAct from "./StepSelectionAct.vue"
import StepSelectionStep from "./StepSelectionStep.vue" import StepSelectionStep from "./StepSelectionStep.vue"
</script> </script>

View file

@ -1,25 +1,25 @@
<template> <template>
<div class="pt-2"> <div class="pt-2">
<div class="font-bold text-7 flex gap-5 items-center"> <div class="font-bold text-7 flex gap-5 items-center">
<span class="flex-grow h-2px bg-white"></span> <span class="flex-grow h-2px bg-white"></span>
<span>{{ act.name }}</span> <span>{{ act.name }}</span>
<span class="flex-grow h-2px bg-white"></span> <span class="flex-grow h-2px bg-white"></span>
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<StepSelectionScene v-for="scene in act.scenes" :key="scene" :scene="scene"/> <StepSelectionScene v-for="(scene, index) in act.scenes" :key="index" :scene="scene"/>
</div> </div>
</div> </div>
</template> </template>
<style module> <style module>
</style> </style>
<script setup lang="ts"> <script lang="ts" setup>
import StepSelectionScene from "./StepSelectionScene.vue" import StepSelectionScene from "./StepSelectionScene.vue"
import { Act } from "../state" import { Act } from "@/state"
const props = defineProps<{ const props = defineProps<{
act: Act act: Act
}>() }>()
</script> </script>

View file

@ -1,26 +1,26 @@
<template> <template>
<div <div
class="not-last:border-b border-dark-300 transition" :class="scene === current.scene ? 'bg-green-900' : ''"
:class="scene === current.scene ? 'bg-green-900' : ''" class="not-last:border-b border-dark-300 transition"
> >
<div class="pb-1 px-4 text-4 font-bold"> <div class="pb-1 px-4 text-4 font-bold">
{{ scene.name }} {{ scene.name }}
</div> </div>
<div class="flex flex-col pb-2"> <div class="flex flex-col pb-2">
<StepSelectionStep v-for="step in scene.steps" :key="step.position" :step="step"/> <StepSelectionStep v-for="step in scene.steps" :key="step.position" :step="step"/>
</div> </div>
</div> </div>
</template> </template>
<style module> <style module>
</style> </style>
<script setup lang="ts"> <script lang="ts" setup>
import { current, Scene } from "../state" import { current, Scene } from "../state"
import StepSelectionStep from "./StepSelectionStep.vue" import StepSelectionStep from "./StepSelectionStep.vue"
const props = defineProps<{ const props = defineProps<{
scene: Scene scene: Scene
}>() }>()
</script> </script>

View file

@ -1,43 +1,43 @@
<template> <template>
<div <div
class="px-4 py-1 flex items-center justify-between space-x-2 transition " :class="isActive ? 'bg-green-700' : ''"
:class="isActive ? 'bg-green-700' : ''" class="px-4 py-1 flex items-center justify-between gap-2 transition"
> >
<CueBox :step="step"/> <CueBox :step="step"/>
<button class="flex items-center text-4" @click="goToPosition(step.position)"> <button class="flex items-center text-4" @click="goToPosition(step.position)">
<KeyReturnIcon/> <KeyReturnIcon/>
</button> </button>
</div> </div>
</template> </template>
<style module> <style module>
</style> </style>
<script setup lang="ts"> <script lang="ts" setup>
import { state, Step } from "../state" import { state, Step } from "@/state"
import { goToPosition } from "../syncing" import { goToPosition } from "@/syncing"
import KeyReturnIcon from "virtual:icons/ph/key-return" import KeyReturnIcon from "virtual:icons/ph/key-return"
import CueBox from "./CueBox.vue" import CueBox from "./CueBox.vue"
import { useCurrentElement } from "@vueuse/core" import { useCurrentElement } from "@vueuse/core"
import { computed, toRef, watchEffect } from "vue" import { computed, toRef, watchEffect } from "vue"
import { isEqual } from "lodash-es" import { isEqual } from "lodash-es"
const props = defineProps<{ const props = defineProps<{
step: Step step: Step
}>() }>()
const position = toRef(state, "position") const position = toRef(state, "position")
const element = useCurrentElement() const element = useCurrentElement<HTMLElement>()
const isActive = computed(() => isEqual(props.step.position, position.value)) const isActive = computed(() => isEqual(props.step.position, position.value))
watchEffect(() => { watchEffect(() => {
if (isActive.value) { if (isActive.value) {
element.value?.scrollIntoView({ element.value?.scrollIntoView({
behavior: "smooth", behavior: "smooth",
block: "center", block: "center",
inline: "nearest" inline: "nearest"
}) })
} }
}) })
</script> </script>

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="absolute right-3 top-2 text-5 pointer-events-none flex items-center gap-1"> <div class="absolute right-3 top-2 text-5 pointer-events-none flex items-center gap-1">
<ClockIcon/> <ClockIcon/>
<span class="font-bold tabular-nums tracking-tighter">{{ format.format(syncedTime) }}</span> <span class="font-bold tabular-nums tracking-tighter">{{ format.format(syncedTime) }}</span>
</div> </div>
</template> </template>
@ -9,7 +9,7 @@
</style> </style>
<script setup lang="ts"> <script lang="ts" setup>
import { syncedTime } from "../syncing" import { syncedTime } from "../syncing"
import ClockIcon from "virtual:icons/ph/clock-bold" import ClockIcon from "virtual:icons/ph/clock-bold"

48
ui/src/global.scss Normal file
View file

@ -0,0 +1,48 @@
@import "modern-normalize/modern-normalize.css";
:root {
color-scheme: dark;
}
html, body, #app {
width: 100dvw;
height: 100dvh;
font-family: "Manrope Variable", sans-serif;
user-select: none;
background: black;
color: white;
}
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-thumb {
background: rgb(255 255 255 / 10%);
&:hover {
background: rgb(255 255 255 / 20%);
}
}
::selection {
background: rgba(255 255 255 / 20%)
}
a {
text-decoration: none;
color: currentColor;
}
button {
background: transparent;
border: none;
color: inherit;
font: inherit;
padding: 0;
&:focus-visible {
outline: solid 2px theme("colors.blue.400");
outline-offset: 1px;
}
}

View file

@ -1,28 +1,28 @@
import { Ref, ComputedRef, computed, UnwrapRef, ref, watchEffect } from "vue" import { computed, ComputedRef, Ref, ref, UnwrapRef, watchEffect } from "vue"
export const computedIfPresent = <T, V>(object: Ref<T>, access: (value: Exclude<T, undefined | null>) => V): ComputedRef<V | undefined | null> => computed(() => { export const computedIfPresent = <T, V>(object: Ref<T>, access: (value: Exclude<T, undefined | null>) => V): ComputedRef<V | undefined | null> => computed(() => {
if (object.value === null) return null if (object.value === null) return null
else if (object.value === undefined) return undefined else if (object.value === undefined) return undefined
return access(object.value as Exclude<T, undefined | null>) return access(object.value as Exclude<T, undefined | null>)
}) })
export const computedOrNull = <T, V>(object: Ref<T | null>, access: (value: T) => V): ComputedRef<V | null> => computed(() => { export const computedOrNull = <T, V>(object: Ref<T | null>, access: (value: T) => V): ComputedRef<V | null> => computed(() => {
if (object.value === null) return null if (object.value === null) return null
return access(object.value) return access(object.value)
}) })
export function avoidNull<T>(originRef: Ref<UnwrapRef<T> | null>): ComputedRef<UnwrapRef<T> | null> { export function avoidNull<T>(originRef: Ref<UnwrapRef<T> | null>): ComputedRef<UnwrapRef<T> | null> {
const nullAvoidingRef = ref<T | null>(null) const nullAvoidingRef = ref<T | null>(null)
watchEffect(() => { watchEffect(() => {
if (originRef.value !== null) nullAvoidingRef.value = originRef.value if (originRef.value !== null) nullAvoidingRef.value = originRef.value
}) })
return computed(() => nullAvoidingRef.value) return computed(() => nullAvoidingRef.value)
} }
export function formatSeconds(seconds: number) { export function formatSeconds(seconds: number) {
const duration = new Date(seconds * 1000) const duration = new Date(seconds * 1000)
return `${duration.getMinutes().toFixed()}:${duration.getSeconds().toFixed().padStart(2, '0')}` return `${duration.getMinutes().toFixed()}:${duration.getSeconds().toFixed().padStart(2, "0")}`
} }

View file

@ -1,22 +1,18 @@
import "virtual:windi.css" import "virtual:uno.css"
import { createApp } from "vue" import { createApp } from "vue"
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router" import { createRouter, createWebHistory } from "vue-router"
import App from "./App.vue" import App from "./App.vue"
import originalRoutes from "~pages" import { handleHotUpdate, routes } from "vue-router/auto-routes"
const routes = originalRoutes.map(route => {
if (typeof route.component !== "function") return route
return {
...route,
props: false
}
}) as RouteRecordRaw[]
const router = createRouter({ const router = createRouter({
routes, routes,
history: createWebHistory() history: createWebHistory()
}) })
const app = createApp(App) const app = createApp(App)
app.use(router) app.use(router)
app.mount("#app") app.mount("#app")
if (import.meta.hot) {
handleHotUpdate(router)
}

View file

@ -1,42 +0,0 @@
<template>
<div class="flex flex-col overflow-hidden">
<div class="flex flex-col space-y-4 p-4 pt-8 flex-grow h-full overflow-hidden">
<div class="flex justify-end">
<button class="px-5 py-3 bg-green-600 font-bold text-5" @click="goNext()">
Next
</button>
</div>
<StepSelection class="h-110"/>
<FogControl/>
<CurtainLightControl/>
</div>
</div>
</template>
<style>
html {
user-select: none;
}
</style>
<script setup lang="ts">
import { current, getNextValidPosition, getPreviousValidPosition, state } from "../state"
import { goToPosition } from "../syncing"
import { onKeyStroke } from "@vueuse/core"
import StepSelection from "../components/StepSelection.vue"
import FogControl from "../components/FogControl.vue"
import CurtainLightControl from "../components/CurtainLightControl.vue"
function goNext() {
const position = getNextValidPosition(state.position)
if (position !== null) goToPosition(position)
}
function goPrevious() {
const position = getPreviousValidPosition(state.position)
if (position !== null) goToPosition(position)
}
onKeyStroke("ArrowLeft", goPrevious)
onKeyStroke("ArrowRight", goNext)
</script>

View file

@ -1,40 +0,0 @@
<template>
<div class="flex flex-col h-full">
<h1 class="font-800 text-9 p-4 pb-0">
{{ current.act === null ? "" : `${current.act.name}`}}{{ current.scene.name }}
</h1>
<div class="h-full flex space-x-4 p-10 pt-8 flex-grow overflow-hidden">
<StepSelection class="w-1/2"/>
<div class="w-1/2 flex flex-col space-y-4">
<MessageEdit class="h-1/2"/>
<div class="text-4">Vorhang: {{ current.step.curtainState === "open" ? "auf" : "geschlossen"}}</div>
<ActorsOnStageBox class="flex-grow"/>
<FogControl/>
<CurtainLightControl/>
</div>
</div>
<MusicProgressBar class="h-10"/>
</div>
</template>
<script setup lang="ts">
import MusicProgressBar from "../components/MusicProgressBar.vue"
import { onKeyStroke } from "@vueuse/core"
import StepSelection from "../components/StepSelection.vue"
import MessageEdit from "../components/MessageEdit.vue"
import ActorsOnStageBox from "../components/ActorsOnStageBox.vue"
import { goToPosition } from "../syncing"
import { current, getNextValidPosition, getPreviousValidPosition, state } from "../state"
import FogControl from "../components/FogControl.vue"
import CurtainLightControl from "../components/CurtainLightControl.vue"
onKeyStroke("ArrowRight", () => {
const position = getNextValidPosition(state.position)
if (position !== null) goToPosition(position)
})
onKeyStroke("ArrowLeft", () => {
const position = getPreviousValidPosition(state.position)
if (position !== null) goToPosition(position)
})
</script>

View file

@ -0,0 +1,25 @@
<template>
<div class="flex flex-col h-full">
<h1 class="font-800 text-9 p-4 pb-0">
{{ current.act === null ? "" : `${current.act.name}` }}{{ current.scene.name }}
</h1>
<div class="h-full flex space-x-4 p-4 pt-8 flex-grow overflow-hidden">
<MotionsList center-current class="w-3/7" scrollable/>
<div class="w-4/7 flex flex-col space-y-4">
<ActorsOnStageBox class="h-full text-7"/>
<MessageEdit class="h-40"/>
<CurtainLightControl/>
</div>
</div>
<MusicProgressBar class="h-10"/>
</div>
</template>
<script lang="ts" setup>
import MusicProgressBar from "../../components/MusicProgressBar.vue"
import MotionsList from "../../components/MotionsList.vue"
import { current } from "../../state"
import ActorsOnStageBox from "../../components/ActorsOnStageBox.vue"
import MessageEdit from "../../components/MessageEdit.vue"
import CurtainLightControl from "../../components/CurtainLightControl.vue"
</script>

View file

@ -0,0 +1,63 @@
<template>
<div class="grid md:grid-cols-2 gap-6 h-full" :class="$style.root">
<StepSelection/>
<div class="flex flex-col gap-5 overflow-y-auto">
<MessageEdit class="h-25 md:h-40"/>
<div class="text-5">
<div class="pb-2 font-bold text-3.5 tracking-wider uppercase">
Vorhang
</div>
<div class="text-4.5">
{{ current.step.curtainState === "open" ? "öffnen" : "schließen" }}
</div>
</div>
<div class="flex-grow overflow-hidden">
<div class="pb-2 font-bold text-3.5 tracking-wider uppercase">
Auf der Bühne
</div>
<ActorsOnStageBox class="flex-grow h-full"/>
</div>
<div>
<div class="pb-2 font-bold text-3.5 tracking-wider uppercase">
Nebel
</div>
<FogControl/>
</div>
<div>
<div class="pb-2 font-bold text-3.5 tracking-wider uppercase">
Umbaulicht
</div>
<CurtainLightControl/>
</div>
</div>
</div>
</template>
<style module lang="scss">
.root {
@screen lt-md {
grid-template-rows: 200px;
}
}
</style>
<script lang="ts" setup>
import { onKeyStroke } from "@vueuse/core"
import StepSelection from "../../components/StepSelection.vue"
import MessageEdit from "../../components/MessageEdit.vue"
import ActorsOnStageBox from "../../components/ActorsOnStageBox.vue"
import { goToPosition } from "@/syncing"
import { current, getNextValidPosition, getPreviousValidPosition, state } from "@/state"
import FogControl from "../../components/FogControl.vue"
import CurtainLightControl from "../../components/CurtainLightControl.vue"
onKeyStroke("ArrowRight", () => {
const position = getNextValidPosition(state.position)
if (position !== null) goToPosition(position)
})
onKeyStroke("ArrowLeft", () => {
const position = getPreviousValidPosition(state.position)
if (position !== null) goToPosition(position)
})
</script>

View file

@ -0,0 +1,71 @@
<template>
<div class="flex flex-col gap-50">
<div class="flex flex-col gap-2">
<div class="text-gray text-2xl">
Aktuelles Ziel
</div>
<ChangeBlinkingBox :blink-seconds="currentTarget === null ? 0 : 10" :value="currentTarget" class="text-7 md:text-10">
{{ currentTarget ?? "Niemand" }}
</ChangeBlinkingBox>
</div>
<div v-if="nextStepWithChange !== null" class="flex flex-col gap-2">
<div class="text-gray text-6">
Nächstes Ziel
<span class="whitespace-nowrap">
[{{
nextStepWithChange.delta === 0
? "in dieser Szene"
: nextStepWithChange.delta === 1
? "in der nächsten Szene"
: `in ${nextStepWithChange.delta} Szenen`
}}]
</span>
</div>
<div class="text-7 md:text-10">{{ nextStepWithChange.target }}</div>
</div>
</div>
</template>
<style module>
</style>
<script lang="ts" setup>
import { useRoute } from "vue-router"
import { computed } from "vue"
import { current, getNextValidPosition, getSceneIndex, getStep, ShowPosition } from "@/state"
import ChangeBlinkingBox from "../../components/ChangeBlinkingBox.vue"
const route = useRoute()
const isLeft = computed(() => route.query.side === "left")
const targetProperty = computed<"leftSpotTarget" | "rightSpotTarget">(() => isLeft.value ? "leftSpotTarget" : "rightSpotTarget")
const currentTarget = computed(() => current.step[targetProperty.value])
interface StepWithChange {
delta: number
position: ShowPosition
target: string
}
const nextStepWithChange = computed<StepWithChange | null>(() => {
let position: ShowPosition | null = getNextValidPosition(current.step.position)
let lastTarget: string | null = currentTarget.value
while (position !== null) {
const step = getStep(position)
if (step[targetProperty.value] !== null && step[targetProperty.value] !== lastTarget) {
return {
position: step.position,
delta: getSceneIndex(step.position) - current.sceneIndex,
target: step[targetProperty.value]!
}
}
lastTarget = step[targetProperty.value]
position = getNextValidPosition(position)
}
return null
})
</script>

13
ui/src/pages/index.vue Normal file
View file

@ -0,0 +1,13 @@
<template>
<div class="flex flex-col gap-4 text-xl">
<router-link to="/mixer">audio operator</router-link>
</div>
</template>
<style module>
</style>
<script lang="ts" setup>
</script>

View file

@ -1,8 +1,8 @@
<template> <template>
<div class="flex flex-col h-full"> <div class="flex flex-col h-full">
<h1 class="font-800 text-9 p-4 pb-0"> <h1 class="font-800 text-9 p-4 pb-0">
{{ current.act === null ? "" : `${current.act.name}`}}{{ current.scene.name }} {{ current.act === null ? "" : `${current.act.name}` }}{{ current.scene.name }}
</h1> </h1>
<div class="h-full flex space-x-4 p-4 pt-8 flex-grow overflow-hidden"> <div class="h-full flex space-x-4 p-4 pt-8 flex-grow overflow-hidden">
<MotionsList center-current class="w-3/7"/> <MotionsList center-current class="w-3/7"/>
<div class="w-4/7 flex flex-col space-y-4"> <div class="w-4/7 flex flex-col space-y-4">
@ -14,7 +14,7 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script lang="ts" setup>
import MusicProgressBar from "../../components/MusicProgressBar.vue" import MusicProgressBar from "../../components/MusicProgressBar.vue"
import { current } from "../../state" import { current } from "../../state"
import MotionsList from "../../components/MotionsList.vue" import MotionsList from "../../components/MotionsList.vue"

View file

@ -1,25 +0,0 @@
<template>
<div class="flex flex-col h-full">
<h1 class="font-800 text-9 p-4 pb-0">
{{ current.act === null ? "" : `${current.act.name}`}}{{ current.scene.name }}
</h1>
<div class="h-full flex space-x-4 p-4 pt-8 flex-grow overflow-hidden">
<MotionsList scrollable center-current class="w-3/7"/>
<div class="w-4/7 flex flex-col space-y-4">
<ActorsOnStageBox class="h-full text-7"/>
<MessageEdit class="h-40"/>
<CurtainLightControl/>
</div>
</div>
<MusicProgressBar class="h-10"/>
</div>
</template>
<script setup lang="ts">
import MusicProgressBar from "../components/MusicProgressBar.vue"
import MotionsList from "../components/MotionsList.vue"
import { current } from "../state"
import ActorsOnStageBox from "../components/ActorsOnStageBox.vue"
import MessageEdit from "../components/MessageEdit.vue"
import CurtainLightControl from "../components/CurtainLightControl.vue"
</script>

View file

@ -1,77 +0,0 @@
<template>
<div class="flex flex-col h-full">
<h1 class="font-800 text-9 p-4 pb-0">
{{ current.act === null ? "" : `${current.act.name}`}}{{ current.scene.name }}
</h1>
<div class="h-full flex flex-col space-y-10 p-7 text-6">
<div class="flex flex-col gap-1">
<div>
Aktuelles Ziel:
</div>
<ChangeBlinkingBox class="h-full min-w-50" :value="currentTarget" :blink-seconds="10">
{{ currentTarget ?? "Niemand" }}
</ChangeBlinkingBox>
</div>
<div v-if="nextStepWithChange !== null" class="h-full flex flex-col space-y-2 text-5">
<div class="flex gap-5 items-center">
<div>Nächstes Ziel:</div>
<div>{{ nextStepWithChange.target }}</div>
</div>
<div>
{{ nextStepWithChange.delta === 0
? "in dieser Szene (rechtzeitig positionieren!)"
: nextStepWithChange.delta === 1
? "in der nächsten Szene"
: `in ${nextStepWithChange.delta} Szenen`
}}
</div>
</div>
</div>
<MusicProgressBar class="h-10"/>
</div>
</template>
<style module>
</style>
<script setup lang="ts">
import { useRoute } from "vue-router"
import { computed } from "vue"
import { current, getNextValidPosition, getSceneIndex, getStep, ShowPosition, START_STEP } from "../state"
import ChangeBlinkingBox from "../components/ChangeBlinkingBox.vue"
import MusicProgressBar from "../components/MusicProgressBar.vue"
const route = useRoute()
const isLeft = computed(() => route.query.side === "left")
const targetProperty = computed<"leftSpotTarget" | "rightSpotTarget">(() => isLeft.value ? "leftSpotTarget" : "rightSpotTarget")
const currentTarget = computed(() => current.step[targetProperty.value])
interface StepWithChange {
delta: number
position: ShowPosition
target: string
}
const nextStepWithChange = computed<StepWithChange | null>(() => {
let position: ShowPosition | null = getNextValidPosition(current.step.position)
let lastTarget: string | null = currentTarget.value
while(position !== null) {
const step = getStep(position)
if (step[targetProperty.value] !== null && step[targetProperty.value] !== lastTarget) {
return {
position: step.position,
delta: getSceneIndex(step.position) - current.sceneIndex,
target: step[targetProperty.value]!
}
}
lastTarget = step[targetProperty.value]
position = getNextValidPosition(position)
}
return null
})
</script>

View file

@ -1,77 +0,0 @@
<template>
<div class="flex flex-col h-full">
<h1 class="font-800 text-9 p-4 pb-0">
{{ current.act === null ? "" : `${current.act.name}`}}{{ current.scene.name }}
</h1>
<div class="h-full flex flex-col space-y-10 p-10 text-10">
<div class="flex gap-5 items-center h-18">
<div>
Aktuelles Ziel:
</div>
<ChangeBlinkingBox class="px-8 h-full min-w-50 items-center" :value="currentTarget" :blink-seconds="10">
{{ currentTarget ?? "Niemand" }}
</ChangeBlinkingBox>
</div>
<div v-if="nextStepWithChange !== null" class="h-full flex flex-col space-y-2 text-7">
<div class="flex gap-5 items-center">
<div>Nächstes Ziel:</div>
<div>{{ nextStepWithChange.target }}</div>
</div>
<div>
{{ nextStepWithChange.delta === 0
? "in dieser Szene (rechtzeitig positionieren!)"
: nextStepWithChange.delta === 1
? "in der nächsten Szene"
: `in ${nextStepWithChange.delta} Szenen`
}}
</div>
</div>
</div>
<MusicProgressBar class="h-10"/>
</div>
</template>
<style module>
</style>
<script setup lang="ts">
import { useRoute } from "vue-router"
import { computed } from "vue"
import { current, getNextValidPosition, getSceneIndex, getStep, ShowPosition, START_STEP } from "../state"
import ChangeBlinkingBox from "../components/ChangeBlinkingBox.vue"
import MusicProgressBar from "../components/MusicProgressBar.vue"
const route = useRoute()
const isLeft = computed(() => route.query.side === "left")
const targetProperty = computed<"leftSpotTarget" | "rightSpotTarget">(() => isLeft.value ? "leftSpotTarget" : "rightSpotTarget")
const currentTarget = computed(() => current.step[targetProperty.value])
interface StepWithChange {
delta: number
position: ShowPosition
target: string
}
const nextStepWithChange = computed<StepWithChange | null>(() => {
let position: ShowPosition | null = getNextValidPosition(current.step.position)
let lastTarget: string | null = currentTarget.value
while(position !== null) {
const step = getStep(position)
if (step[targetProperty.value] !== null && step[targetProperty.value] !== lastTarget) {
return {
position: step.position,
delta: getSceneIndex(step.position) - current.sceneIndex,
target: step[targetProperty.value]!
}
}
lastTarget = step[targetProperty.value]
position = getNextValidPosition(position)
}
return null
})
</script>

View file

@ -2,246 +2,246 @@ import { reactive, shallowRef } from "vue"
import { reactiveComputed } from "@vueuse/core" import { reactiveComputed } from "@vueuse/core"
export interface Act { export interface Act {
name: string name: string
scenes: Scene[] scenes: Scene[]
} }
export interface Scene { export interface Scene {
name: string name: string
steps: Step[] steps: Step[]
} }
export interface Step { export interface Step {
position: ShowPosition position: ShowPosition
cue: StepCue cue: StepCue
actorEntrances: string[] actorEntrances: string[]
actorExits: string[] actorExits: string[]
actorsOnStage: string[] actorsOnStage: string[]
props: PropMap props: PropMap
hasChangedProps: boolean hasChangedProps: boolean
leftSpotTarget: string | null leftSpotTarget: string | null
rightSpotTarget: string | null rightSpotTarget: string | null
curtainState: "open" | "closed" curtainState: "open" | "closed"
} }
export type StepCue = { export type StepCue = {
type: "TEXT", type: "TEXT",
speaker: string speaker: string
text: string text: string
clarification?: string clarification?: string
} | { } | {
type: "MUSIC_START", type: "MUSIC_START",
title: string title: string
duration: number duration: number
} | { } | {
type: "MUSIC_END" type: "MUSIC_END"
} | { } | {
type: "CURTAIN", type: "CURTAIN",
state: "open" | "closed" state: "open" | "closed"
whileMoving: boolean whileMoving: boolean
} | { } | {
type: "LIGHTS" type: "LIGHTS"
state: "on" | "off" state: "on" | "off"
whileFading: boolean whileFading: boolean
} | { } | {
type: "CUSTOM" type: "CUSTOM"
text: string text: string
} }
export type PropMap = Record<PropPosition, string | null> export type PropMap = Record<PropPosition, string | null>
export type PropPosition = export type PropPosition =
| "PROSCENIUM_LEFT" | "PROSCENIUM_LEFT"
| "PROSCENIUM_CENTER" | "PROSCENIUM_CENTER"
| "PROSCENIUM_RIGHT" | "PROSCENIUM_RIGHT"
| "CENTER" | "CENTER"
| "LEFT" | "LEFT"
| "RIGHT" | "RIGHT"
| "BACKDROP" | "BACKDROP"
export interface ShowState { export interface ShowState {
position: ShowPosition position: ShowPosition
message: string message: string
activeMusic: ShowMusic | null activeMusic: ShowMusic | null
musicStartTime: number musicStartTime: number
isLightBehindCurtainOn: boolean isLightBehindCurtainOn: boolean
} }
export interface ShowMusic { export interface ShowMusic {
title: string title: string
duration: number duration: number
} }
export interface ShowPosition { export interface ShowPosition {
act: number act: number
scene: number scene: number
step: number step: number
} }
export interface Show { export interface Show {
acts: Act[] acts: Act[]
} }
export const START_STEP: Step = { export const START_STEP: Step = {
position: { act: -1, scene: 0, step: 0 }, position: { act: -1, scene: 0, step: 0 },
actorsOnStage: [], actorsOnStage: [],
cue: { cue: {
type: "CUSTOM", type: "CUSTOM",
text: "Start" text: "Start"
}, },
props: { props: {
BACKDROP: null, BACKDROP: null,
LEFT: null, LEFT: null,
CENTER: null, CENTER: null,
RIGHT: null, RIGHT: null,
PROSCENIUM_LEFT: null, PROSCENIUM_LEFT: null,
PROSCENIUM_CENTER: null, PROSCENIUM_CENTER: null,
PROSCENIUM_RIGHT: null PROSCENIUM_RIGHT: null
}, },
hasChangedProps: false, hasChangedProps: false,
actorEntrances: [], actorEntrances: [],
actorExits: [], actorExits: [],
leftSpotTarget: null, leftSpotTarget: null,
rightSpotTarget: null, rightSpotTarget: null,
curtainState: "closed" curtainState: "closed"
} }
const START_SCENE: Scene = { const START_SCENE: Scene = {
name: "Start", name: "Start",
steps: [START_STEP] steps: [START_STEP]
} }
export const show = shallowRef<Show>({ export const show = shallowRef<Show>({
acts: [] acts: []
}) })
export const state = reactive<ShowState>({ export const state = reactive<ShowState>({
position: START_STEP.position, position: START_STEP.position,
message: "", message: "",
activeMusic: null, activeMusic: null,
musicStartTime: 0, musicStartTime: 0,
isLightBehindCurtainOn: false isLightBehindCurtainOn: false
}) })
export function getStep(position: ShowPosition) { export function getStep(position: ShowPosition) {
if (position.act === -1) return START_STEP if (position.act === -1) return START_STEP
return getScene(position).steps[position.step] return getScene(position).steps[position.step]
} }
export function getScene(position: ShowPosition) { export function getScene(position: ShowPosition) {
if (position.act === -1) return START_SCENE if (position.act === -1) return START_SCENE
return getAct(position)!.scenes[position.scene] return getAct(position)!.scenes[position.scene]
} }
export function getAct(position: ShowPosition) { export function getAct(position: ShowPosition) {
if (position.act === -1) return null if (position.act === -1) return null
return show.value.acts[position.act] return show.value.acts[position.act]
} }
export function getSceneIndex(position: ShowPosition) { export function getSceneIndex(position: ShowPosition) {
let index = position.scene let index = position.scene
for (let actIndex = 0; actIndex < position.act; actIndex++) { for (let actIndex = 0; actIndex < position.act; actIndex++) {
index += show.value.acts[actIndex]!.scenes.length index += show.value.acts[actIndex]!.scenes.length
} }
return index return index
} }
export function getActiveMusicAt(position: ShowPosition): ShowMusic | null { export function getActiveMusicAt(position: ShowPosition): ShowMusic | null {
let activeMusic: ShowMusic | null = null let activeMusic: ShowMusic | null = null
for (let actIndex = 0; actIndex < show.value.acts.length; actIndex++) { for (let actIndex = 0; actIndex < show.value.acts.length; actIndex++) {
const scenes = show.value.acts[actIndex].scenes const scenes = show.value.acts[actIndex].scenes
for (let sceneIndex = 0; sceneIndex < scenes.length; sceneIndex++) { for (let sceneIndex = 0; sceneIndex < scenes.length; sceneIndex++) {
const scene = scenes[sceneIndex] const scene = scenes[sceneIndex]
for (let stepIndex = 0; stepIndex < scene.steps.length; stepIndex++) { for (let stepIndex = 0; stepIndex < scene.steps.length; stepIndex++) {
const step = scene.steps[stepIndex] const step = scene.steps[stepIndex]
// SONG // SONG
if (step.cue.type === "MUSIC_START") { if (step.cue.type === "MUSIC_START") {
activeMusic = { activeMusic = {
title: step.cue.title, title: step.cue.title,
duration: step.cue.duration duration: step.cue.duration
} }
} else if (step.cue.type === "MUSIC_END") { } else if (step.cue.type === "MUSIC_END") {
activeMusic = null activeMusic = null
}
if (sceneIndex == position.scene && stepIndex == position.step) return activeMusic
}
} }
if (sceneIndex == position.scene && stepIndex == position.step) return activeMusic
}
} }
}
return null return null
} }
export const current = reactiveComputed<{ export const current = reactiveComputed<{
act: Act | null act: Act | null
scene: Scene scene: Scene
sceneIndex: number sceneIndex: number
step: Step step: Step
activeMusic: ShowMusic | null activeMusic: ShowMusic | null
allScenes: Scene[] allScenes: Scene[]
}>(() => ({ }>(() => ({
act: getAct(state.position), act: getAct(state.position),
scene: getScene(state.position), scene: getScene(state.position),
sceneIndex: getSceneIndex(state.position), sceneIndex: getSceneIndex(state.position),
step: getStep(state.position), step: getStep(state.position),
activeMusic: state.activeMusic, activeMusic: state.activeMusic,
allScenes: show.value.acts.flatMap(act => act.scenes) allScenes: show.value.acts.flatMap(act => act.scenes)
})) }))
export function parseStringWithDetails(string: string) { export function parseStringWithDetails(string: string) {
const parts = string.split(" / ") const parts = string.split(" / ")
if (parts.length === 1) return { main: parts[0], details: "" } if (parts.length === 1) return { main: parts[0], details: "" }
return { main: parts[0], details: parts.slice(1).join(" / ") } return { main: parts[0], details: parts.slice(1).join(" / ") }
} }
export function getNextValidPosition(start: ShowPosition): ShowPosition | null { export function getNextValidPosition(start: ShowPosition): ShowPosition | null {
if (start.act === START_STEP.position.act) return { act: 0, scene: 0, step: 0 } if (start.act === START_STEP.position.act) return { act: 0, scene: 0, step: 0 }
const acts = show.value.acts const acts = show.value.acts
let { act, scene, step } = start let { act, scene, step } = start
step++ step++
if (step >= acts[start.act].scenes[start.scene].steps.length) { if (step >= acts[start.act].scenes[start.scene].steps.length) {
step = 0 step = 0
scene++ scene++
if (scene >= acts[start.act].scenes.length) { if (scene >= acts[start.act].scenes.length) {
scene = 0 scene = 0
act++ act++
if (act >= acts.length) { if (act >= acts.length) {
return null return null
} }
}
} }
}
return { act, scene, step } return { act, scene, step }
} }
export function getPreviousValidPosition(start: ShowPosition): ShowPosition | null { export function getPreviousValidPosition(start: ShowPosition): ShowPosition | null {
let { act, scene, step } = start let { act, scene, step } = start
step-- step--
if (step < 0) { if (step < 0) {
scene-- scene--
if (scene < 0) { if (scene < 0) {
act-- act--
if (act < 0) { if (act < 0) {
return START_STEP.position return START_STEP.position
} }
scene = show.value.acts[act].scenes.length - 1 scene = show.value.acts[act].scenes.length - 1
}
step = show.value.acts[act].scenes[scene].steps.length - 1
} }
step = show.value.acts[act].scenes[scene].steps.length - 1 return { act, scene, step }
}
return { act, scene, step }
} }

View file

@ -9,50 +9,50 @@ export const getSyncedTime = () => Date.now() - timeDifference
export const syncedTime = ref(Date.now()) export const syncedTime = ref(Date.now())
const setSyncedTimeRef = () => { const setSyncedTimeRef = () => {
syncedTime.value = getSyncedTime() syncedTime.value = getSyncedTime()
requestAnimationFrame(setSyncedTimeRef) requestAnimationFrame(setSyncedTimeRef)
} }
requestAnimationFrame(setSyncedTimeRef) requestAnimationFrame(setSyncedTimeRef)
export const connect = () => new Promise<void>(resolve => { export const connect = () => new Promise<void>(resolve => {
if (socket !== null) return if (socket !== null) return
const url = new URL(window.location.toString()) const url = new URL(window.location.toString())
url.protocol = "ws" url.protocol = "ws"
url.pathname = "/api/ws" url.pathname = "/api/ws"
socket = new ReconnectingWebSocket(url.toString(), [], { socket = new ReconnectingWebSocket(url.toString(), [], {
reconnectionDelayGrowFactor: 1, reconnectionDelayGrowFactor: 1,
minReconnectionDelay: 1000 minReconnectionDelay: 1000
}) })
let isFirstMessage = true let isFirstMessage = true
socket.addEventListener("open", () => { socket.addEventListener("open", () => {
isFirstMessage = true isFirstMessage = true
}) })
socket.addEventListener("message", event => { socket.addEventListener("message", event => {
const data = JSON.parse(event.data as string) const data = JSON.parse(event.data as string)
if (isFirstMessage) { if (isFirstMessage) {
isFirstMessage = false isFirstMessage = false
show.value = data show.value = data
console.log("Show:", data) console.log("Show:", data)
} else { } else {
Object.assign(state, data.state) Object.assign(state, data.state)
timeDifference = Date.now() - data.timestamp timeDifference = Date.now() - data.timestamp
console.log("New state:", data.state) console.log("New state:", data.state)
console.log("New time difference:", timeDifference) console.log("New time difference:", timeDifference)
resolve() resolve()
} }
}) })
}) })
export async function goToPosition(position: ShowPosition) { export async function goToPosition(position: ShowPosition) {
await fetch("/api/go", { method: "POST", body: JSON.stringify(position), headers: { "Content-Type": "application/json" } }) await fetch("/api/go", { method: "POST", body: JSON.stringify(position), headers: { "Content-Type": "application/json" } })
} }
export async function setMessage(message: string) { export async function setMessage(message: string) {
await fetch("/api/message", { method: "POST", body: message }) await fetch("/api/message", { method: "POST", body: message })
} }

View file

@ -1,3 +0,0 @@
/// <reference types="vite-plugin-pages/client"/>
/// <reference types="unplugin-icons/types/vue"/>
/// <reference types="vite/client"/>

View file

@ -1,20 +1,37 @@
{ {
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "declaration": false,
"module": "ESNext",
"target": "ES2021",
"lib": ["DOM", "ESNext"],
"strict": true,
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": false, "forceConsistentCasingInFileNames": true,
"moduleResolution": "node", "lib": [
"esnext",
"dom",
],
"module": "esnext",
"moduleResolution": "Bundler",
"allowJs": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"strictNullChecks": true, "isolatedModules": true,
"downlevelIteration": true, "rootDir": "src",
"forceConsistentCasingInFileNames": true "skipLibCheck": true,
"sourceMap": true,
"strict": true,
"stripInternal": true,
"noUncheckedIndexedAccess": false,
"target": "esnext",
"types": [
"vite/client",
"unplugin-vue-router/client",
"unplugin-icons/types/vue"
],
"paths": {
"@/*": [
"./src/*"
]
}
}, },
"include": [ "include": [
"node_modules/**/*", "src/**/*.ts",
"src/**/*" "src/**/*.vue",
] ]
} }

44
ui/unocss.config.ts Executable file
View file

@ -0,0 +1,44 @@
import { defineConfig, transformerDirectives } from "unocss"
import { presetWind, colors } from "@unocss/preset-wind3"
const generateValues = (max: number, fn: (step: number) => any) => {
const object: Record<number, any> = {}
for (let i = 1; i <= max; i++) {
object[i] = fn(i)
}
return object
}
export default defineConfig({
presets: [
presetWind({
arbitraryVariants: true,
preflight: true
})
],
theme: {
fontFamily: {
sans: `"Manrope Variable", sans-serif`,
system: "sans-serif"
},
colors: {
black: colors.black,
white: colors.white,
gray: colors.stone,
red: colors.red,
yellow: colors.amber,
orange: colors.orange,
green: colors.green,
blue: colors.blue,
violet: colors.fuchsia,
light: colors.light,
dark: colors.dark,
transparent: colors.transparent
}
},
transformers: [
transformerDirectives()
]
})

View file

@ -1,28 +1,39 @@
import { defineConfig, splitVendorChunkPlugin } from "vite" import { defineConfig } from "vite"
import vuePlugin from "@vitejs/plugin-vue" import vuePlugin from "@vitejs/plugin-vue"
import windicssPlugin from "vite-plugin-windicss"
import pagesPlugin from "vite-plugin-pages"
import iconsPlugin from "unplugin-icons/vite" import iconsPlugin from "unplugin-icons/vite"
import vueRouterPlugin from "unplugin-vue-router/vite"
import unocssPlugin from "unocss/vite"
import { resolve } from "node:path"
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
splitVendorChunkPlugin(), vueRouterPlugin({
vuePlugin(), dts: "./src/generated-types/router.d.ts",
pagesPlugin({ routesFolder: "./src/pages",
importMode: "sync"
}), }),
windicssPlugin(), unocssPlugin(),
iconsPlugin() iconsPlugin(),
vuePlugin()
], ],
resolve: {
alias: {
"@": resolve(__dirname, "./src")
}
},
css: {
preprocessorOptions: {
scss: {
api: "modern-compiler"
}
}
},
server: { server: {
proxy: { proxy: {
"/api": { "/api": {
target: "http://localhost:8000", target: "http://localhost:8000",
ws: true ws: true
} }
} },
}, allowedHosts: true
build: {
reportCompressedSize: false
} }
}) })

View file

@ -1,44 +0,0 @@
import { defineConfig } from "windicss/helpers"
import colors from "windicss/colors"
import lineClampPlugin from "windicss/plugin/line-clamp"
const generateValues = (max: number, fn: (step: number) => any) => {
const object: Record<number, any> = {}
for (let i = 1; i <= max; i++) {
object[i] = fn(i)
}
return object
}
export default defineConfig({
theme: {
colors: {
black: colors.black,
white: colors.white,
gray: colors.stone,
red: colors.red,
yellow: colors.amber,
orange: colors.orange,
green: colors.green,
blue: colors.blue,
violet: colors.fuchsia,
light: colors.light,
dark: colors.dark,
transparent: colors.transparent
},
fontSize: {
...generateValues(30, step => `${step * 0.25}rem`),
4: "1.2rem",
3: "1.1rem",
2: "1rem",
s1: "0.9rem",
s2: "0.8rem",
s3: "0.7rem"
}
},
plugins: [
lineClampPlugin
]
})