commit 76
This commit is contained in:
parent
b90f897123
commit
18dc17d6d5
46 changed files with 2032 additions and 2096 deletions
3
ui/.gitignore
vendored
3
ui/.gitignore
vendored
|
@ -1,4 +1,5 @@
|
||||||
.idea/
|
.idea/
|
||||||
node_modules/
|
node_modules/
|
||||||
*.env
|
|
||||||
dist/
|
dist/
|
||||||
|
src/generated-types
|
||||||
|
*.env
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
18
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
2016
ui/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -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(() => {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
48
ui/src/global.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")}`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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>
|
|
|
@ -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>
|
|
25
ui/src/pages/for/audio-operator.vue
Executable file
25
ui/src/pages/for/audio-operator.vue
Executable 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>
|
63
ui/src/pages/for/show-operator.vue
Executable file
63
ui/src/pages/for/show-operator.vue
Executable 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>
|
71
ui/src/pages/for/spot-operator.vue
Normal file
71
ui/src/pages/for/spot-operator.vue
Normal 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
13
ui/src/pages/index.vue
Normal 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>
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
304
ui/src/state.ts
304
ui/src/state.ts
|
@ -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 }
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 })
|
||||||
}
|
}
|
||||||
|
|
3
ui/src/vite-env.d.ts
vendored
3
ui/src/vite-env.d.ts
vendored
|
@ -1,3 +0,0 @@
|
||||||
/// <reference types="vite-plugin-pages/client"/>
|
|
||||||
/// <reference types="unplugin-icons/types/vue"/>
|
|
||||||
/// <reference types="vite/client"/>
|
|
|
@ -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
44
ui/unocss.config.ts
Executable 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()
|
||||||
|
]
|
||||||
|
})
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
|
||||||
]
|
|
||||||
})
|
|
Loading…
Add table
Reference in a new issue