Include UI (moira)
This commit is contained in:
parent
02a03731ea
commit
d1d326cfa8
42 changed files with 3666 additions and 0 deletions
|
@ -12,6 +12,7 @@ import de.moritzruth.theaterdsl.value.Color
|
||||||
import de.moritzruth.theaterdsl.value.degrees
|
import de.moritzruth.theaterdsl.value.degrees
|
||||||
import de.moritzruth.theaterdsl.value.percent
|
import de.moritzruth.theaterdsl.value.percent
|
||||||
import kotlinx.collections.immutable.persistentSetOf
|
import kotlinx.collections.immutable.persistentSetOf
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
val bar = StairvilleTriLedBar(DmxAddress(400u))
|
val bar = StairvilleTriLedBar(DmxAddress(400u))
|
||||||
|
@ -315,10 +316,291 @@ val show = createShow {
|
||||||
trigger = StepCue.Custom("Vorhang auf, Musik Ende")
|
trigger = StepCue.Custom("Vorhang auf, Musik Ende")
|
||||||
|
|
||||||
onRun {
|
onRun {
|
||||||
|
// irgendwann Musik
|
||||||
spotRight.brightness.static(100.percent)
|
spotRight.brightness.static(100.percent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scene("III.7") {
|
||||||
|
step {
|
||||||
|
trigger = StepCue.Stub
|
||||||
|
|
||||||
|
onRun {
|
||||||
|
// Musik wird hektischer
|
||||||
|
FrontLights.all.forEach { it.brightness.fade(100.percent, 30.seconds) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scene("III.8") {
|
||||||
|
step {
|
||||||
|
trigger = StepCue.MusicEnd
|
||||||
|
|
||||||
|
onRun {
|
||||||
|
FrontLights.all.forEach { it.brightness.static(50.percent) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step {
|
||||||
|
trigger = StepCue.Custom("Auftritt Paula")
|
||||||
|
|
||||||
|
onRun {
|
||||||
|
(FrontLights.right + FrontLights.left).forEach { it.brightness.fade(0.percent, 5.seconds) }
|
||||||
|
// kühle Farben
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step {
|
||||||
|
trigger = StepCue.Custom("Auftritt weitere Schüler")
|
||||||
|
|
||||||
|
onRun {
|
||||||
|
(FrontLights.right + FrontLights.left).forEach { it.brightness.fade(50.percent, 40.seconds) }
|
||||||
|
// buntere Farben
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step {
|
||||||
|
trigger = StepCue.MusicEnd
|
||||||
|
|
||||||
|
onRun {
|
||||||
|
FrontLights.all.forEach { it.brightness.off() }
|
||||||
|
// Vorhang zu, Pause
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RnR entfällt
|
||||||
|
|
||||||
|
scene("IV.1") {
|
||||||
|
step {
|
||||||
|
trigger = StepCue.MusicStart("Pause", 69.seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
step {
|
||||||
|
trigger = StepCue.MusicEnd
|
||||||
|
|
||||||
|
onRun {
|
||||||
|
// Badezimmer-Licht, zweigeteilt
|
||||||
|
FrontLights.right.forEach { it.brightness.fade(75.percent, 8.seconds) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step {
|
||||||
|
trigger = StepCue.Stub
|
||||||
|
|
||||||
|
onRun {
|
||||||
|
// Badezimmer-Licht, zweigeteilt
|
||||||
|
FrontLights.right.forEach { it.brightness.fade(0.percent, 3.seconds) }
|
||||||
|
FrontLights.left.forEach { it.brightness.fade(75.percent, 3.seconds) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step {
|
||||||
|
trigger = StepCue.MusicStart("Schau mich mit anderen Augen an", 69.seconds)
|
||||||
|
|
||||||
|
onRun {
|
||||||
|
// beide beleuchten
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step {
|
||||||
|
trigger = StepCue.MusicEnd
|
||||||
|
|
||||||
|
onRun {
|
||||||
|
// Umbau
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scene("IV.2") {
|
||||||
|
step {
|
||||||
|
trigger = StepCue.Stub
|
||||||
|
|
||||||
|
onRun {
|
||||||
|
// vor dem Vorhang
|
||||||
|
FrontLights.center.forEach { it.brightness.fade(75.percent, 5.seconds) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step {
|
||||||
|
trigger = StepCue.MusicStart("Und jetzt", 69.seconds)
|
||||||
|
|
||||||
|
onRun {
|
||||||
|
// wütend, entgeistert
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scene("IV.3") {
|
||||||
|
step {
|
||||||
|
trigger = StepCue.Stub
|
||||||
|
|
||||||
|
onRun {
|
||||||
|
FrontLights.all.forEach { it.brightness.fade(75.percent, 8.seconds) }
|
||||||
|
// warmes Licht
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scene("IV.4") {
|
||||||
|
step {
|
||||||
|
trigger = StepCue.Custom("Auftritt David")
|
||||||
|
|
||||||
|
onRun {
|
||||||
|
// Vorhang bleibt auf
|
||||||
|
FrontLights.all.forEach { it.brightness.fade(50.percent, 8.seconds) }
|
||||||
|
spotRight.brightness.fade(100.percent, 4.seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step {
|
||||||
|
trigger = StepCue.MusicStart("Wenn Du da bist (Duett)", 69.seconds)
|
||||||
|
|
||||||
|
onRun {
|
||||||
|
(FrontLights.right + FrontLights.center).forEach { it.brightness.fade(0.percent, 8.seconds) }
|
||||||
|
spotRight.brightness.fade(100.percent, 10.seconds)
|
||||||
|
// romantisch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step {
|
||||||
|
trigger = StepCue.MusicEnd
|
||||||
|
|
||||||
|
onRun {
|
||||||
|
FrontLights.left.forEach { it.brightness.fade(0.percent, 2.seconds) }
|
||||||
|
spotRight.brightness.fade(0.percent, 2.seconds)
|
||||||
|
// romantisch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scene("IV.5") {
|
||||||
|
step {
|
||||||
|
trigger = StepCue.Stub
|
||||||
|
|
||||||
|
onRun {
|
||||||
|
(FrontLights.left + FrontLights.center).forEach { it.brightness.fade(75.percent, 8.seconds) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step {
|
||||||
|
trigger = StepCue.MusicStart("Richy", 69.seconds)
|
||||||
|
|
||||||
|
onRun {
|
||||||
|
// bunt, verspielt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scene("IV.6") {
|
||||||
|
step {
|
||||||
|
trigger = StepCue.MusicEnd
|
||||||
|
|
||||||
|
onRun {
|
||||||
|
// Vorhang zu, rechts davor
|
||||||
|
// rotes Licht
|
||||||
|
|
||||||
|
spotRight.brightness.fade(100.percent, 10.seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step {
|
||||||
|
trigger = StepCue.Custom("Störung, Ruf")
|
||||||
|
|
||||||
|
onRun {
|
||||||
|
FrontLights.left.forEach { it.brightness.static(100.percent) }
|
||||||
|
spotLeft.brightness.static(100.percent)
|
||||||
|
// romantisches Licht schlagartig aus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scene("IV.7") {
|
||||||
|
step {
|
||||||
|
trigger = StepCue.Stub
|
||||||
|
|
||||||
|
onRun {
|
||||||
|
// Vorhang auf, Spot immer noch auf Paula
|
||||||
|
spotLeft.brightness.fade(0.percent, 8.seconds)
|
||||||
|
FrontLights.all.forEach { it.brightness.fade(75.percent, 5.seconds) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step {
|
||||||
|
trigger = StepCue.MusicStart("Paulas Monolog", 69.seconds)
|
||||||
|
|
||||||
|
onRun {
|
||||||
|
FrontLights.all.forEach { it.brightness.fade(20.percent, 15.seconds) }
|
||||||
|
// blaues Licht
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step {
|
||||||
|
trigger = StepCue.MusicEnd
|
||||||
|
|
||||||
|
onRun {
|
||||||
|
// Vorhang zu
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scene("IV.8") {
|
||||||
|
step {
|
||||||
|
trigger = StepCue.MusicStart("Angstballet", 69.seconds)
|
||||||
|
|
||||||
|
onRun {
|
||||||
|
// passend zur Musik
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step {
|
||||||
|
trigger = StepCue.MusicEnd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scene("IV.9") {
|
||||||
|
step {
|
||||||
|
trigger = StepCue.Stub
|
||||||
|
|
||||||
|
onRun {
|
||||||
|
spotRight.brightness.fade(100.percent, 5.seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step {
|
||||||
|
trigger = StepCue.Custom("Bühne erreicht")
|
||||||
|
|
||||||
|
onRun {
|
||||||
|
// Vorhang auf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scene("IV.10") {
|
||||||
|
step {
|
||||||
|
trigger = StepCue.Stub
|
||||||
|
|
||||||
|
onRun {
|
||||||
|
// Paula steht mit dem Rücken zum Publikum in der Mitte der Bühne, relativ weit hinten
|
||||||
|
// Sie wird von oben weiß beleuchtet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step {
|
||||||
|
trigger = StepCue.Custom("Bühne erreicht")
|
||||||
|
|
||||||
|
onRun {
|
||||||
|
// Vorhang auf
|
||||||
|
bar.color.static(Color(40.degrees, 50.percent))
|
||||||
|
|
||||||
|
val steps = mutableListOf(75.percent)
|
||||||
|
steps.addAll((0..30).map { 0.percent })
|
||||||
|
|
||||||
|
bar.brightness.steps(steps, 100.milliseconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,4 +23,5 @@ suspend fun main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun test() {
|
fun test() {
|
||||||
|
|
||||||
}
|
}
|
|
@ -13,6 +13,7 @@ import io.ktor.serialization.kotlinx.json.*
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.cio.*
|
import io.ktor.server.cio.*
|
||||||
import io.ktor.server.engine.*
|
import io.ktor.server.engine.*
|
||||||
|
import io.ktor.server.http.content.*
|
||||||
import io.ktor.server.plugins.contentnegotiation.*
|
import io.ktor.server.plugins.contentnegotiation.*
|
||||||
import io.ktor.server.request.*
|
import io.ktor.server.request.*
|
||||||
import io.ktor.server.response.*
|
import io.ktor.server.response.*
|
||||||
|
@ -225,6 +226,10 @@ private fun CoroutineScope.startWebsocketServer(context: ShowContext) = launch(D
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
staticResources("/", "ui") {
|
||||||
|
enableAutoHeadResponse()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.start(wait = true)
|
}.start(wait = true)
|
||||||
}
|
}
|
4
ui/.gitignore
vendored
Executable file
4
ui/.gitignore
vendored
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
.idea/
|
||||||
|
node_modules/
|
||||||
|
*.env
|
||||||
|
dist/
|
1
ui/.nvmrc
Normal file
1
ui/.nvmrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
18
|
201
ui/LICENSE
Executable file
201
ui/LICENSE
Executable file
|
@ -0,0 +1,201 @@
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
13
ui/index.html
Executable file
13
ui/index.html
Executable file
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Moira</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<link href="./node_modules/@fontsource-variable/manrope/index.css" rel="stylesheet"/>
|
||||||
|
<script type="module" src="./src/main.ts"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
31
ui/package.json
Executable file
31
ui/package.json
Executable file
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"name": "moira",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "Moritz Ruth <dev@moritzruth.de>",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --port 3000 --host",
|
||||||
|
"build": "vite build",
|
||||||
|
"start": "vite preview --port 3000 --host"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@iconify-json/ph": "^1.1.5",
|
||||||
|
"@types/lodash-es": "^4.17.7",
|
||||||
|
"@vitejs/plugin-vue": "^4.2.3",
|
||||||
|
"typescript": "^5.0.4",
|
||||||
|
"unplugin-icons": "^0.16.1",
|
||||||
|
"vite": "^4.3.8",
|
||||||
|
"vite-plugin-pages": "^0.30.1",
|
||||||
|
"vite-plugin-windicss": "^1.9.0",
|
||||||
|
"windicss": "^3.5.6"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fontsource-variable/manrope": "^5.0.0",
|
||||||
|
"@vueuse/core": "^10.1.2",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"reconnecting-websocket": "^4.4.0",
|
||||||
|
"sass": "^1.62.1",
|
||||||
|
"vue": "^3.3.4",
|
||||||
|
"vue-router": "^4.2.1"
|
||||||
|
}
|
||||||
|
}
|
1430
ui/pnpm-lock.yaml
generated
Executable file
1430
ui/pnpm-lock.yaml
generated
Executable file
File diff suppressed because it is too large
Load diff
17
ui/server/package.json
Executable file
17
ui/server/package.json
Executable file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsx watch src/main.ts"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^18.0.1",
|
||||||
|
"@types/ws": "^8.5.3",
|
||||||
|
"tsx": "^3.7.1",
|
||||||
|
"typescript": "^4.7.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bufferutil": "^4.0.6",
|
||||||
|
"utf-8-validate": "^5.0.9",
|
||||||
|
"ws": "^8.8.0"
|
||||||
|
}
|
||||||
|
}
|
344
ui/server/pnpm-lock.yaml
generated
Executable file
344
ui/server/pnpm-lock.yaml
generated
Executable file
|
@ -0,0 +1,344 @@
|
||||||
|
lockfileVersion: 5.4
|
||||||
|
|
||||||
|
specifiers:
|
||||||
|
'@types/node': ^18.0.1
|
||||||
|
'@types/ws': ^8.5.3
|
||||||
|
bufferutil: ^4.0.6
|
||||||
|
tsx: ^3.7.1
|
||||||
|
typescript: ^4.7.4
|
||||||
|
utf-8-validate: ^5.0.9
|
||||||
|
ws: ^8.8.0
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
bufferutil: 4.0.6
|
||||||
|
utf-8-validate: 5.0.9
|
||||||
|
ws: 8.8.0_22kvxa7zeyivx4jp72v2w3pkvy
|
||||||
|
|
||||||
|
devDependencies:
|
||||||
|
'@types/node': 18.0.1
|
||||||
|
'@types/ws': 8.5.3
|
||||||
|
tsx: 3.7.1
|
||||||
|
typescript: 4.7.4
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
/@esbuild-kit/cjs-loader/2.3.0:
|
||||||
|
resolution: {integrity: sha512-KInrVt8wlKLhWy7+y4a+E+0uBJoWgdx6Xupy+rrF4MFHA/dEt22ACvvChOZSyiqtQieYPtbPkVYSjbC7mOrFVw==}
|
||||||
|
dependencies:
|
||||||
|
'@esbuild-kit/core-utils': 2.0.2
|
||||||
|
get-tsconfig: 4.1.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@esbuild-kit/core-utils/2.0.2:
|
||||||
|
resolution: {integrity: sha512-clNYQUsqtc36pzW5EufMsahcbLG45EaW3YDyf0DlaS0eCMkDXpxIlHwPC0rndUwG6Ytk9sMSD5k1qHbwYEC/OQ==}
|
||||||
|
dependencies:
|
||||||
|
esbuild: 0.14.48
|
||||||
|
source-map-support: 0.5.21
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@esbuild-kit/esm-loader/2.4.0:
|
||||||
|
resolution: {integrity: sha512-zS720jXh06nfg5yAzm6oob4sWN9VTP2E1SonhFgEb6zCBswa4S8fOQ/4Bksz1flDgn56NPqoTTDn2XmWRyMG9Q==}
|
||||||
|
dependencies:
|
||||||
|
'@esbuild-kit/core-utils': 2.0.2
|
||||||
|
get-tsconfig: 4.1.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/node/18.0.1:
|
||||||
|
resolution: {integrity: sha512-CmR8+Tsy95hhwtZBKJBs0/FFq4XX7sDZHlGGf+0q+BRZfMbOTkzkj0AFAuTyXbObDIoanaBBW0+KEW+m3N16Wg==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/ws/8.5.3:
|
||||||
|
resolution: {integrity: sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 18.0.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/buffer-from/1.1.2:
|
||||||
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/bufferutil/4.0.6:
|
||||||
|
resolution: {integrity: sha512-jduaYOYtnio4aIAyc6UbvPCVcgq7nYpVnucyxr6eCYg/Woad9Hf/oxxBRDnGGjPfjUm6j5O/uBWhIu4iLebFaw==}
|
||||||
|
engines: {node: '>=6.14.2'}
|
||||||
|
requiresBuild: true
|
||||||
|
dependencies:
|
||||||
|
node-gyp-build: 4.5.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/esbuild-android-64/0.14.48:
|
||||||
|
resolution: {integrity: sha512-3aMjboap/kqwCUpGWIjsk20TtxVoKck8/4Tu19rubh7t5Ra0Yrpg30Mt1QXXlipOazrEceGeWurXKeFJgkPOUg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [android]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-android-arm64/0.14.48:
|
||||||
|
resolution: {integrity: sha512-vptI3K0wGALiDq+EvRuZotZrJqkYkN5282iAfcffjI5lmGG9G1ta/CIVauhY42MBXwEgDJkweiDcDMRLzBZC4g==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-darwin-64/0.14.48:
|
||||||
|
resolution: {integrity: sha512-gGQZa4+hab2Va/Zww94YbshLuWteyKGD3+EsVon8EWTWhnHFRm5N9NbALNbwi/7hQ/hM1Zm4FuHg+k6BLsl5UA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-darwin-arm64/0.14.48:
|
||||||
|
resolution: {integrity: sha512-bFjnNEXjhZT+IZ8RvRGNJthLWNHV5JkCtuOFOnjvo5pC0sk2/QVk0Qc06g2PV3J0TcU6kaPC3RN9yy9w2PSLEA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-freebsd-64/0.14.48:
|
||||||
|
resolution: {integrity: sha512-1NOlwRxmOsnPcWOGTB10JKAkYSb2nue0oM1AfHWunW/mv3wERfJmnYlGzL3UAOIUXZqW8GeA2mv+QGwq7DToqA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-freebsd-arm64/0.14.48:
|
||||||
|
resolution: {integrity: sha512-gXqKdO8wabVcYtluAbikDH2jhXp+Klq5oCD5qbVyUG6tFiGhrC9oczKq3vIrrtwcxDQqK6+HDYK8Zrd4bCA9Gw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [freebsd]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-linux-32/0.14.48:
|
||||||
|
resolution: {integrity: sha512-ghGyDfS289z/LReZQUuuKq9KlTiTspxL8SITBFQFAFRA/IkIvDpnZnCAKTCjGXAmUqroMQfKJXMxyjJA69c/nQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [linux]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-linux-64/0.14.48:
|
||||||
|
resolution: {integrity: sha512-vni3p/gppLMVZLghI7oMqbOZdGmLbbKR23XFARKnszCIBpEMEDxOMNIKPmMItQrmH/iJrL1z8Jt2nynY0bE1ug==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-linux-arm/0.14.48:
|
||||||
|
resolution: {integrity: sha512-+VfSV7Akh1XUiDNXgqgY1cUP1i2vjI+BmlyXRfVz5AfV3jbpde8JTs5Q9sYgaoq5cWfuKfoZB/QkGOI+QcL1Tw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-linux-arm64/0.14.48:
|
||||||
|
resolution: {integrity: sha512-3CFsOlpoxlKPRevEHq8aAntgYGYkE1N9yRYAcPyng/p4Wyx0tPR5SBYsxLKcgPB9mR8chHEhtWYz6EZ+H199Zw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-linux-mips64le/0.14.48:
|
||||||
|
resolution: {integrity: sha512-cs0uOiRlPp6ymknDnjajCgvDMSsLw5mST2UXh+ZIrXTj2Ifyf2aAP3Iw4DiqgnyYLV2O/v/yWBJx+WfmKEpNLA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [mips64el]
|
||||||
|
os: [linux]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-linux-ppc64le/0.14.48:
|
||||||
|
resolution: {integrity: sha512-+2F0vJMkuI0Wie/wcSPDCqXvSFEELH7Jubxb7mpWrA/4NpT+/byjxDz0gG6R1WJoeDefcrMfpBx4GFNN1JQorQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-linux-riscv64/0.14.48:
|
||||||
|
resolution: {integrity: sha512-BmaK/GfEE+5F2/QDrIXteFGKnVHGxlnK9MjdVKMTfvtmudjY3k2t8NtlY4qemKSizc+QwyombGWTBDc76rxePA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-linux-s390x/0.14.48:
|
||||||
|
resolution: {integrity: sha512-tndw/0B9jiCL+KWKo0TSMaUm5UWBLsfCKVdbfMlb3d5LeV9WbijZ8Ordia8SAYv38VSJWOEt6eDCdOx8LqkC4g==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-netbsd-64/0.14.48:
|
||||||
|
resolution: {integrity: sha512-V9hgXfwf/T901Lr1wkOfoevtyNkrxmMcRHyticybBUHookznipMOHoF41Al68QBsqBxnITCEpjjd4yAos7z9Tw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [netbsd]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-openbsd-64/0.14.48:
|
||||||
|
resolution: {integrity: sha512-+IHf4JcbnnBl4T52egorXMatil/za0awqzg2Vy6FBgPcBpisDWT2sVz/tNdrK9kAqj+GZG/jZdrOkj7wsrNTKA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [openbsd]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-sunos-64/0.14.48:
|
||||||
|
resolution: {integrity: sha512-77m8bsr5wOpOWbGi9KSqDphcq6dFeJyun8TA+12JW/GAjyfTwVtOnN8DOt6DSPUfEV+ltVMNqtXUeTeMAxl5KA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [sunos]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-windows-32/0.14.48:
|
||||||
|
resolution: {integrity: sha512-EPgRuTPP8vK9maxpTGDe5lSoIBHGKO/AuxDncg5O3NkrPeLNdvvK8oywB0zGaAZXxYWfNNSHskvvDgmfVTguhg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-windows-64/0.14.48:
|
||||||
|
resolution: {integrity: sha512-YmpXjdT1q0b8ictSdGwH3M8VCoqPpK1/UArze3X199w6u8hUx3V8BhAi1WjbsfDYRBanVVtduAhh2sirImtAvA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-windows-arm64/0.14.48:
|
||||||
|
resolution: {integrity: sha512-HHaOMCsCXp0rz5BT2crTka6MPWVno121NKApsGs/OIW5QC0ggC69YMGs1aJct9/9FSUF4A1xNE/cLvgB5svR4g==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/esbuild/0.14.48:
|
||||||
|
resolution: {integrity: sha512-w6N1Yn5MtqK2U1/WZTX9ZqUVb8IOLZkZ5AdHkT6x3cHDMVsYWC7WPdiLmx19w3i4Rwzy5LqsEMtVihG3e4rFzA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
hasBin: true
|
||||||
|
requiresBuild: true
|
||||||
|
optionalDependencies:
|
||||||
|
esbuild-android-64: 0.14.48
|
||||||
|
esbuild-android-arm64: 0.14.48
|
||||||
|
esbuild-darwin-64: 0.14.48
|
||||||
|
esbuild-darwin-arm64: 0.14.48
|
||||||
|
esbuild-freebsd-64: 0.14.48
|
||||||
|
esbuild-freebsd-arm64: 0.14.48
|
||||||
|
esbuild-linux-32: 0.14.48
|
||||||
|
esbuild-linux-64: 0.14.48
|
||||||
|
esbuild-linux-arm: 0.14.48
|
||||||
|
esbuild-linux-arm64: 0.14.48
|
||||||
|
esbuild-linux-mips64le: 0.14.48
|
||||||
|
esbuild-linux-ppc64le: 0.14.48
|
||||||
|
esbuild-linux-riscv64: 0.14.48
|
||||||
|
esbuild-linux-s390x: 0.14.48
|
||||||
|
esbuild-netbsd-64: 0.14.48
|
||||||
|
esbuild-openbsd-64: 0.14.48
|
||||||
|
esbuild-sunos-64: 0.14.48
|
||||||
|
esbuild-windows-32: 0.14.48
|
||||||
|
esbuild-windows-64: 0.14.48
|
||||||
|
esbuild-windows-arm64: 0.14.48
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/fsevents/2.3.2:
|
||||||
|
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||||
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
|
os: [darwin]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/get-tsconfig/4.1.0:
|
||||||
|
resolution: {integrity: sha512-bhshxJhpfmeQ8x4fAvDqJV2VfGp5TfHdLpmBpNZZhMoVyfIrOippBW4mayC3DT9Sxuhcyl56Efw61qL28hG4EQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/node-gyp-build/4.5.0:
|
||||||
|
resolution: {integrity: sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==}
|
||||||
|
hasBin: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/source-map-support/0.5.21:
|
||||||
|
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
|
||||||
|
dependencies:
|
||||||
|
buffer-from: 1.1.2
|
||||||
|
source-map: 0.6.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/source-map/0.6.1:
|
||||||
|
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/tsx/3.7.1:
|
||||||
|
resolution: {integrity: sha512-dwl1GBdkwVQ9zRxTmETGi+ck8pewNm2QXh+HK6jHxdHmeCjfCL+Db3b4VX/dOMDSS2hle1j5LzQoo8OpVXu6XQ==}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
'@esbuild-kit/cjs-loader': 2.3.0
|
||||||
|
'@esbuild-kit/core-utils': 2.0.2
|
||||||
|
'@esbuild-kit/esm-loader': 2.4.0
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/typescript/4.7.4:
|
||||||
|
resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==}
|
||||||
|
engines: {node: '>=4.2.0'}
|
||||||
|
hasBin: true
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/utf-8-validate/5.0.9:
|
||||||
|
resolution: {integrity: sha512-Yek7dAy0v3Kl0orwMlvi7TPtiCNrdfHNd7Gcc/pLq4BLXqfAmd0J7OWMizUQnTTJsyjKn02mU7anqwfmUP4J8Q==}
|
||||||
|
engines: {node: '>=6.14.2'}
|
||||||
|
requiresBuild: true
|
||||||
|
dependencies:
|
||||||
|
node-gyp-build: 4.5.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/ws/8.8.0_22kvxa7zeyivx4jp72v2w3pkvy:
|
||||||
|
resolution: {integrity: sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
bufferutil: ^4.0.1
|
||||||
|
utf-8-validate: ^5.0.2
|
||||||
|
peerDependenciesMeta:
|
||||||
|
bufferutil:
|
||||||
|
optional: true
|
||||||
|
utf-8-validate:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
bufferutil: 4.0.6
|
||||||
|
utf-8-validate: 5.0.9
|
||||||
|
dev: false
|
46
ui/server/src/main.ts
Executable file
46
ui/server/src/main.ts
Executable file
|
@ -0,0 +1,46 @@
|
||||||
|
import { WebSocketServer } from "ws"
|
||||||
|
|
||||||
|
const server = new WebSocketServer({
|
||||||
|
clientTracking: true,
|
||||||
|
host: "0.0.0.0",
|
||||||
|
port: 8000
|
||||||
|
})
|
||||||
|
|
||||||
|
let state = {
|
||||||
|
music: null,
|
||||||
|
message: "",
|
||||||
|
position: {
|
||||||
|
scene: 0,
|
||||||
|
step: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server.on("connection", (client, request) => {
|
||||||
|
const address = request.connection.remoteAddress
|
||||||
|
console.log(`Connected: ${address}`)
|
||||||
|
client.send(JSON.stringify({
|
||||||
|
state,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}))
|
||||||
|
|
||||||
|
client.on("message", rawData => {
|
||||||
|
state = JSON.parse(rawData.toString())
|
||||||
|
|
||||||
|
console.log("Update: ", state)
|
||||||
|
|
||||||
|
server.clients.forEach(c => {
|
||||||
|
if (c !== client) {
|
||||||
|
c.send(JSON.stringify({
|
||||||
|
state,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
client.on("close", () => {
|
||||||
|
console.log(`Disconnected: ${address}`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log("Listening on ws://0.0.0.0:8000")
|
52
ui/src/App.vue
Executable file
52
ui/src/App.vue
Executable file
|
@ -0,0 +1,52 @@
|
||||||
|
<template>
|
||||||
|
<div id="app" class="bg-black text-white">
|
||||||
|
<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="text-s1">Created by Moritz Ruth</div>
|
||||||
|
</div>
|
||||||
|
<router-view v-else/>
|
||||||
|
<TimeDisplay/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
html, body, #app {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: "Manrope Variable", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: rgb(255 255 255 / 10%);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgb(255 255 255 / 20%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { connect } from "./syncing"
|
||||||
|
import { ref } from "vue"
|
||||||
|
import TimeDisplay from "./components/TimeDisplay.vue"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "App",
|
||||||
|
components: { TimeDisplay },
|
||||||
|
setup() {
|
||||||
|
const isConnecting = ref(true)
|
||||||
|
connect().then(() => {
|
||||||
|
isConnecting.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
isConnecting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
23
ui/src/components/ActorsOnStageBox.vue
Executable file
23
ui/src/components/ActorsOnStageBox.vue
Executable file
|
@ -0,0 +1,23 @@
|
||||||
|
<template>
|
||||||
|
<div class="p-2 space-y-8">
|
||||||
|
<div>
|
||||||
|
<div class="pb-2 font-bold text-gray-400 text-2 tracking-wider uppercase">
|
||||||
|
auf der Bühne
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<EntrancesList :entrances="current.step.actorsOnStage"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import EntrancesList from "./EntrancesList.vue"
|
||||||
|
import { current, getNextValidPosition, getStep, parseStringWithDetails, ShowPosition, state, Step, show } from "../state"
|
||||||
|
import { computed } from "vue"
|
||||||
|
import { intersection } from "lodash-es"
|
||||||
|
</script>
|
81
ui/src/components/CueBox.vue
Executable file
81
ui/src/components/CueBox.vue
Executable file
|
@ -0,0 +1,81 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex items-start space-x-2">
|
||||||
|
<component :is="icon" class="text-4 mt-0.5 flex-shrink-0"/>
|
||||||
|
<div class="text-4" :class="singleLine && 'truncate'">
|
||||||
|
{{ text }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import MusicNoteIcon from "virtual:icons/ph/music-note"
|
||||||
|
import ChatCircleTextIcon from "virtual:icons/ph/chat-circle-TEXT"
|
||||||
|
import StopIcon from "virtual:icons/ph/stop"
|
||||||
|
import HeadlightsIcon from "virtual:icons/ph/headlights"
|
||||||
|
import WarningIcon from "virtual:icons/ph/warning"
|
||||||
|
import ArrowsOutLineHorizontalIcon from "virtual:icons/ph/arrows-out-line-horizontal"
|
||||||
|
import ArrowsInLineHorizontalIcon from "virtual:icons/ph/arrows-in-line-horizontal"
|
||||||
|
import DotFillIcon from "virtual:icons/ph/dot-fill"
|
||||||
|
import { computed } from "vue"
|
||||||
|
import { START_STEP, Step } from "../state"
|
||||||
|
import { formatSeconds } from "../helpers"
|
||||||
|
import { isEqual } from "lodash-es"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
step: Step,
|
||||||
|
singleLine?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const icon = computed(() => {
|
||||||
|
const cue = props.step.cue
|
||||||
|
|
||||||
|
if (isEqual(props.step.position, START_STEP.position)) {
|
||||||
|
return DotFillIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (cue.type) {
|
||||||
|
case "CURTAIN":
|
||||||
|
return cue.state === "closed"
|
||||||
|
? ArrowsInLineHorizontalIcon
|
||||||
|
: ArrowsOutLineHorizontalIcon
|
||||||
|
|
||||||
|
case "LIGHTS": return HeadlightsIcon
|
||||||
|
case "TEXT": return ChatCircleTextIcon
|
||||||
|
case "MUSIC_START": return MusicNoteIcon
|
||||||
|
case "MUSIC_END": return StopIcon
|
||||||
|
case "CUSTOM": return WarningIcon
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const text = computed(() => {
|
||||||
|
const cue = props.step.cue
|
||||||
|
|
||||||
|
switch (cue.type) {
|
||||||
|
case "CURTAIN":
|
||||||
|
if (cue.state === "open") {
|
||||||
|
return cue.whileMoving ? "Der Vorhang öffnet sich" : "Der Vorhang ist geöffnet"
|
||||||
|
} else {
|
||||||
|
return cue.whileMoving ? "Der Vorhang schließt sich" : "Der Vorhang ist geschlossen"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "LIGHTS":
|
||||||
|
if (cue.state === "on") {
|
||||||
|
return cue.whileFading ? "Das Licht geht an" : "Das Licht ist an"
|
||||||
|
} else {
|
||||||
|
return cue.whileFading ? "Das Licht geht aus" : "Das Licht ist aus"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "TEXT":
|
||||||
|
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_END": return "Ende der Musik"
|
||||||
|
case "CUSTOM": return cue.text
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
54
ui/src/components/EntrancesList.vue
Executable file
54
ui/src/components/EntrancesList.vue
Executable file
|
@ -0,0 +1,54 @@
|
||||||
|
<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>
|
46
ui/src/components/MessageDisplay.vue
Executable file
46
ui/src/components/MessageDisplay.vue
Executable file
|
@ -0,0 +1,46 @@
|
||||||
|
<template>
|
||||||
|
<div class="overflow-hidden text-6 box p-4" :data-blinking="isBlinking">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.box {
|
||||||
|
&[data-blinking="true"] {
|
||||||
|
animation: alternate infinite 500ms ease-in-out pulse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
from {
|
||||||
|
@apply bg-red-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
@apply bg-transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { state } from "../state"
|
||||||
|
import { toRef, watch } from "vue"
|
||||||
|
import { autoResetRef } from "@vueuse/core"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "MessageDisplay",
|
||||||
|
setup() {
|
||||||
|
const message = toRef(state, "message")
|
||||||
|
const isBlinking = autoResetRef(false, 10 * 1000)
|
||||||
|
|
||||||
|
watch(message, () => {
|
||||||
|
isBlinking.value = message.value !== ""
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
message,
|
||||||
|
isBlinking
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
23
ui/src/components/MessageEdit.vue
Executable file
23
ui/src/components/MessageEdit.vue
Executable file
|
@ -0,0 +1,23 @@
|
||||||
|
<template>
|
||||||
|
<div class="h-full max-h-60">
|
||||||
|
<textarea
|
||||||
|
:value="state.message"
|
||||||
|
id="message-box"
|
||||||
|
:class="$style.area"
|
||||||
|
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"
|
||||||
|
placeholder="Nachricht an alle"
|
||||||
|
@input="e => setMessage(e.target.value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style module>
|
||||||
|
.area {
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { state } from "../state"
|
||||||
|
import { setMessage } from "../syncing"
|
||||||
|
</script>
|
35
ui/src/components/MotionsList.vue
Executable file
35
ui/src/components/MotionsList.vue
Executable file
|
@ -0,0 +1,35 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col space-y-5" :class="scrollable ? 'overflow-y-auto' : 'overflow-hidden'">
|
||||||
|
<div
|
||||||
|
v-for="(scene, sceneIndex) in scenes"
|
||||||
|
:key="sceneIndex"
|
||||||
|
>
|
||||||
|
<div class="text-gray-400 pl-3">
|
||||||
|
{{ scene.name }}
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<template v-for="step in scene.steps" :key="step.position">
|
||||||
|
<MotionsListStep
|
||||||
|
v-if="step.actorEntrances.length > 0 || step.actorExits.length > 0"
|
||||||
|
:step="step"
|
||||||
|
:center-current="Boolean(centerCurrent)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="h-[50%] flex-shrink-0"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { show } from "../state"
|
||||||
|
import { computed } from "vue"
|
||||||
|
import MotionsListStep from "./MotionsListStep.vue"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
centerCurrent?: boolean
|
||||||
|
scrollable?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const scenes = computed(() => show.value.acts.flatMap(a => a.scenes))
|
||||||
|
</script>
|
7
ui/src/components/MotionsListScene.vue
Normal file
7
ui/src/components/MotionsListScene.vue
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
78
ui/src/components/MotionsListStep.vue
Normal file
78
ui/src/components/MotionsListStep.vue
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
<template>
|
||||||
|
<div class="transition p-3" :class="isActive && 'bg-green-800'">
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<div class="flex-grow">
|
||||||
|
<CueBox :step="step"/>
|
||||||
|
<div class="py-2 pl-8 space-y-2 text-6">
|
||||||
|
<div class="flex flex-col space-y-1">
|
||||||
|
<div
|
||||||
|
v-for="motion in step.actorEntrances"
|
||||||
|
:key="motion"
|
||||||
|
class="flex items-center"
|
||||||
|
>
|
||||||
|
<CaretDoubleRightIcon/>
|
||||||
|
<div class="pl-2">
|
||||||
|
<span class="font-bold">
|
||||||
|
{{ parseStringWithDetails(motion).main }}
|
||||||
|
</span>
|
||||||
|
<span class="pl-1.5">
|
||||||
|
{{ parseStringWithDetails(motion).details }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="motion in step.actorExits"
|
||||||
|
:key="motion"
|
||||||
|
class="flex items-center"
|
||||||
|
>
|
||||||
|
<CaretDoubleLeftIcon/>
|
||||||
|
<div class="pl-2">
|
||||||
|
<span class="font-bold">
|
||||||
|
{{ parseStringWithDetails(motion).main }}
|
||||||
|
</span>
|
||||||
|
<span class="pl-1.5">
|
||||||
|
{{ parseStringWithDetails(motion).details }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="step.hasChangedProps" class="flex space-x-2 pt-0.5">
|
||||||
|
<BarricadeIcon class="mt-0.5"/>
|
||||||
|
<span>Umbau</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { parseStringWithDetails, START_STEP, state, Step } from "../state"
|
||||||
|
import CueBox from "./CueBox.vue"
|
||||||
|
import CaretDoubleRightIcon from "virtual:icons/ph/caret-double-right"
|
||||||
|
import CaretDoubleLeftIcon from "virtual:icons/ph/caret-double-left"
|
||||||
|
import BarricadeIcon from "virtual:icons/ph/barricade"
|
||||||
|
import { isEqual } from "lodash-es"
|
||||||
|
import { toRef, computed, watchEffect } from "vue"
|
||||||
|
import { useCurrentElement } from "@vueuse/core"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
step: Step,
|
||||||
|
centerCurrent: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const position = toRef(state, "position")
|
||||||
|
const element = useCurrentElement()
|
||||||
|
const isActive = computed(() => isEqual(props.step.position, position.value))
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
const p = props.step.position
|
||||||
|
if (isActive.value || (p.act + p.scene + p.step === 0 && isEqual(START_STEP.position, position.value))) {
|
||||||
|
element.value?.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: props.centerCurrent ? "center" : "start",
|
||||||
|
inline: "nearest"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
73
ui/src/components/MusicProgressBar.vue
Executable file
73
ui/src/components/MusicProgressBar.vue
Executable file
|
@ -0,0 +1,73 @@
|
||||||
|
<template>
|
||||||
|
<div class="relative">
|
||||||
|
<div
|
||||||
|
class="bg-green-700 h-full transition-all"
|
||||||
|
:style="{ width: (progress * 100) + '%' }"
|
||||||
|
:class="barClass"
|
||||||
|
></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'"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{{ lastMusic?.title ?? "" }}
|
||||||
|
</div>
|
||||||
|
<div class="tabular-nums">
|
||||||
|
{{ formatSeconds(remainingSeconds) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style module>
|
||||||
|
.pulse {
|
||||||
|
animation: alternate infinite 500ms ease-in-out pulse;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
from {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { state, current } from "../state"
|
||||||
|
import { computed, useCssModule } from "vue"
|
||||||
|
import { avoidNull, formatSeconds } from "../helpers"
|
||||||
|
import { syncedTime } from "../syncing"
|
||||||
|
|
||||||
|
const music = computed(() => current.activeMusic)
|
||||||
|
const lastMusic = avoidNull(music)
|
||||||
|
|
||||||
|
const styles = useCssModule()
|
||||||
|
|
||||||
|
const deltaInSeconds = computed(() => {
|
||||||
|
if (music.value === null) return 0
|
||||||
|
return (syncedTime.value - state.musicStartTime) / 1000
|
||||||
|
})
|
||||||
|
|
||||||
|
const progress = computed(() => {
|
||||||
|
if (music.value === null) return 0
|
||||||
|
return Math.min(1, deltaInSeconds.value / (music.value.duration / 1000))
|
||||||
|
})
|
||||||
|
|
||||||
|
const remainingSeconds = computed(() => {
|
||||||
|
if (lastMusic.value === null) return 0
|
||||||
|
return Math.max(0, (lastMusic.value.duration / 1000) - deltaInSeconds.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const barClass = computed(() => {
|
||||||
|
if (lastMusic.value === null) return "bg-gray-700"
|
||||||
|
|
||||||
|
if (remainingSeconds.value < 5) return styles.pulse + " bg-red-600"
|
||||||
|
if (remainingSeconds.value < 10) return "bg-red-600"
|
||||||
|
if (remainingSeconds.value < 20) return "bg-orange-600"
|
||||||
|
|
||||||
|
return "bg-blue-700"
|
||||||
|
})
|
||||||
|
</script>
|
73
ui/src/components/PropBox.vue
Executable file
73
ui/src/components/PropBox.vue
Executable file
|
@ -0,0 +1,73 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center justify-center py-4"
|
||||||
|
:class="$style.box"
|
||||||
|
:data-blinking="isBlinking"
|
||||||
|
>
|
||||||
|
<div class="text-s1 tracking-wide text-gray-500">
|
||||||
|
{{ positionName }}
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow w-full">
|
||||||
|
<transition name="fade" mode="out-in">
|
||||||
|
<div :key="prop" class="flex flex-col items-center justify-center">
|
||||||
|
<template v-if="prop !== null">
|
||||||
|
<div class="font-bold text-3 text-center">
|
||||||
|
{{ parseStringWithDetails(prop).main }}
|
||||||
|
</div>
|
||||||
|
<div v-if="parseStringWithDetails(prop).details" class="text-center px-2">
|
||||||
|
{{ parseStringWithDetails(prop).details }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style module>
|
||||||
|
.box {
|
||||||
|
&[data-blinking="true"] {
|
||||||
|
animation: alternate infinite 1000ms ease-in-out pulse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
from {
|
||||||
|
@apply bg-red-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
@apply bg-transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { toRef, watch } from "vue"
|
||||||
|
import { autoResetRef } from "@vueuse/core"
|
||||||
|
import { parseStringWithDetails } from "../state"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
prop: string | null
|
||||||
|
positionName: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const prop = toRef(props, "prop")
|
||||||
|
const isBlinking = autoResetRef(false, 20 * 1000)
|
||||||
|
|
||||||
|
watch(prop, () => {
|
||||||
|
isBlinking.value = true
|
||||||
|
})
|
||||||
|
</script>
|
39
ui/src/components/StageTopDownView.vue
Executable file
39
ui/src/components/StageTopDownView.vue
Executable file
|
@ -0,0 +1,39 @@
|
||||||
|
<template>
|
||||||
|
<div class="bg-dark-800 p-5 flex flex-col">
|
||||||
|
<div :class="$style.row">
|
||||||
|
<div class="text-s1 text-center tracking-wide text-gray-500 pb-3">
|
||||||
|
Publikum
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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 position-name="Mitte der Vorbühne" :prop="current.step.props.PROSCENIUM_CENTER"/>
|
||||||
|
<PropBox position-name="Linke der Vorbühne" :prop="current.step.props.PROSCENIUM_LEFT"/>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.row" class="flex-grow h-0">
|
||||||
|
<PropBox position-name="Rechts" :prop="current.step.props.PROSCENIUM_RIGHT"/>
|
||||||
|
<PropBox position-name="Mitte" :prop="current.step.props.PROSCENIUM_CENTER"/>
|
||||||
|
<PropBox position-name="Links" :prop="current.step.props.PROSCENIUM_LEFT"/>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.row" class="border-t border-dark-300 py-3 h-20">
|
||||||
|
<div/>
|
||||||
|
<PropBox position-name="Rückwand" :prop="current.step.props.BACKDROP"/>
|
||||||
|
<div/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style module>
|
||||||
|
.row {
|
||||||
|
@apply flex justify-between items-center gap-x-10;
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
@apply flex-shrink-0 flex-grow w-1 h-full;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { current } from "../state"
|
||||||
|
import PropBox from "./PropBox.vue"
|
||||||
|
</script>
|
16
ui/src/components/StepSelection.vue
Executable file
16
ui/src/components/StepSelection.vue
Executable file
|
@ -0,0 +1,16 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex-grow overflow-y-auto bg-dark-800 flex flex-col pt-2">
|
||||||
|
<StepSelectionStep :step="START_STEP"/>
|
||||||
|
<StepSelectionAct v-for="act in show.acts" :key="act.name" :act="act"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { show, START_STEP } from "../state"
|
||||||
|
import StepSelectionAct from "./StepSelectionAct.vue"
|
||||||
|
import StepSelectionStep from "./StepSelectionStep.vue"
|
||||||
|
</script>
|
18
ui/src/components/StepSelectionAct.vue
Normal file
18
ui/src/components/StepSelectionAct.vue
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<StepSelectionScene v-for="scene in act.scenes" :key="scene" :scene="scene"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style module>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import StepSelectionScene from "./StepSelectionScene.vue"
|
||||||
|
import { Act } from "../state"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
act: Act
|
||||||
|
}>()
|
||||||
|
</script>
|
26
ui/src/components/StepSelectionScene.vue
Normal file
26
ui/src/components/StepSelectionScene.vue
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="border-b border-dark-300 transition"
|
||||||
|
:class="scene === current.scene ? 'bg-green-900' : ''"
|
||||||
|
>
|
||||||
|
<div class="pb-1 pt-4 px-4 text-4 font-bold">
|
||||||
|
{{ scene.name }}
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col pb-2">
|
||||||
|
<StepSelectionStep v-for="step in scene.steps" :key="step.position" :step="step"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style module>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { current, Scene } from "../state"
|
||||||
|
import StepSelectionStep from "./StepSelectionStep.vue"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
scene: Scene
|
||||||
|
}>()
|
||||||
|
</script>
|
46
ui/src/components/StepSelectionStep.vue
Normal file
46
ui/src/components/StepSelectionStep.vue
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="px-4 py-1 flex items-center justify-between space-x-2 transition "
|
||||||
|
:class="isActive ? 'bg-green-700' : ''"
|
||||||
|
>
|
||||||
|
<CueBox :step="step"/>
|
||||||
|
<button
|
||||||
|
class="flex items-center text-4"
|
||||||
|
@click="goToPosition(step.position)"
|
||||||
|
>
|
||||||
|
<KeyReturnIcon/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style module>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { state, Step } from "../state"
|
||||||
|
import { goToPosition } from "../syncing"
|
||||||
|
import KeyReturnIcon from "virtual:icons/ph/key-return"
|
||||||
|
import CueBox from "./CueBox.vue"
|
||||||
|
import { useCurrentElement } from "@vueuse/core"
|
||||||
|
import { computed, toRef, watchEffect } from "vue"
|
||||||
|
import { isEqual } from "lodash-es"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
step: Step
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const position = toRef(state, "position")
|
||||||
|
const element = useCurrentElement()
|
||||||
|
const isActive = computed(() => isEqual(props.step.position, position.value))
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (isActive.value) {
|
||||||
|
element.value?.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "center",
|
||||||
|
inline: "nearest"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
21
ui/src/components/TimeDisplay.vue
Executable file
21
ui/src/components/TimeDisplay.vue
Executable file
|
@ -0,0 +1,21 @@
|
||||||
|
<template>
|
||||||
|
<div class="absolute right-3 top-2 text-5 pointer-events-none flex items-center gap-1">
|
||||||
|
<ClockIcon/>
|
||||||
|
<span class="font-bold tabular-nums tracking-tighter">{{ format.format(syncedTime) }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { syncedTime } from "../syncing"
|
||||||
|
import ClockIcon from "virtual:icons/ph/clock-bold"
|
||||||
|
|
||||||
|
const format = new Intl.DateTimeFormat("de", {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit"
|
||||||
|
})
|
||||||
|
</script>
|
28
ui/src/helpers.ts
Executable file
28
ui/src/helpers.ts
Executable file
|
@ -0,0 +1,28 @@
|
||||||
|
import { Ref, ComputedRef, computed, UnwrapRef, ref, watchEffect } from "vue"
|
||||||
|
|
||||||
|
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
|
||||||
|
else if (object.value === undefined) return undefined
|
||||||
|
|
||||||
|
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(() => {
|
||||||
|
if (object.value === null) return null
|
||||||
|
return access(object.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
export function avoidNull<T>(originRef: Ref<UnwrapRef<T> | null>): ComputedRef<UnwrapRef<T> | null> {
|
||||||
|
const nullAvoidingRef = ref<T | null>(null)
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (originRef.value !== null) nullAvoidingRef.value = originRef.value
|
||||||
|
})
|
||||||
|
|
||||||
|
return computed(() => nullAvoidingRef.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatSeconds(seconds: number) {
|
||||||
|
const duration = new Date(seconds * 1000)
|
||||||
|
return `${duration.getMinutes().toFixed()}:${duration.getSeconds().toFixed().padStart(2, '0')}`
|
||||||
|
}
|
22
ui/src/main.ts
Executable file
22
ui/src/main.ts
Executable file
|
@ -0,0 +1,22 @@
|
||||||
|
import "virtual:windi.css"
|
||||||
|
import { createApp } from "vue"
|
||||||
|
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router"
|
||||||
|
import App from "./App.vue"
|
||||||
|
import originalRoutes from "~pages"
|
||||||
|
|
||||||
|
const routes = originalRoutes.map(route => {
|
||||||
|
if (typeof route.component !== "function") return route
|
||||||
|
return {
|
||||||
|
...route,
|
||||||
|
props: false
|
||||||
|
}
|
||||||
|
}) as RouteRecordRaw[]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
routes,
|
||||||
|
history: createWebHistory()
|
||||||
|
})
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
app.use(router)
|
||||||
|
app.mount("#app")
|
35
ui/src/pages/control-m.vue
Executable file
35
ui/src/pages/control-m.vue
Executable file
|
@ -0,0 +1,35 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col overflow-hidden">
|
||||||
|
<h1 class="font-800 text-9 px-4 pt-10 pb-0">
|
||||||
|
{{ current.scene.name }}
|
||||||
|
</h1>
|
||||||
|
<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"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<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"
|
||||||
|
|
||||||
|
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>
|
35
ui/src/pages/control.vue
Executable file
35
ui/src/pages/control.vue
Executable file
|
@ -0,0 +1,35 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<h1 class="font-800 text-9 p-10 pb-0">
|
||||||
|
{{ 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"/>
|
||||||
|
<ActorsOnStageBox class="h-1/2"/>
|
||||||
|
</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"
|
||||||
|
|
||||||
|
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>
|
29
ui/src/pages/kiosk/stage.vue
Executable file
29
ui/src/pages/kiosk/stage.vue
Executable file
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<h1 class="font-800 text-9 p-4 pb-0">
|
||||||
|
{{ 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"/>
|
||||||
|
<div class="w-4/7 flex flex-col space-y-4">
|
||||||
|
<StageTopDownView class="h-full"/>
|
||||||
|
<MessageDisplay class="h-30"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<MusicProgressBar class="h-15"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import MusicProgressBar from "../../components/MusicProgressBar.vue"
|
||||||
|
import MessageBox from "../../components/MessageEdit.vue"
|
||||||
|
import ActorsOnStageBox from "../../components/ActorsOnStageBox.vue"
|
||||||
|
import { current } from "../../state"
|
||||||
|
import MotionsList from "../../components/MotionsList.vue"
|
||||||
|
import StageTopDownView from "../../components/StageTopDownView.vue"
|
||||||
|
import MessageDisplay from "../../components/MessageDisplay.vue"
|
||||||
|
</script>
|
23
ui/src/pages/microphones.vue
Executable file
23
ui/src/pages/microphones.vue
Executable file
|
@ -0,0 +1,23 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<h1 class="font-800 text-9 p-4 pb-0">
|
||||||
|
{{ 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-6"/>
|
||||||
|
<MessageEdit class="h-40"/>
|
||||||
|
</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"
|
||||||
|
</script>
|
40
ui/src/pages/rein.vue
Executable file
40
ui/src/pages/rein.vue
Executable file
|
@ -0,0 +1,40 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<h1 class="font-800 text-9 p-4 pb-0">
|
||||||
|
{{ 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">
|
||||||
|
<StageTopDownView class="h-full"/>
|
||||||
|
<MessageEdit class="h-30"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<MusicProgressBar class="h-10"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MusicProgressBar from "../components/MusicProgressBar.vue"
|
||||||
|
import MessageBox from "../components/MessageEdit.vue"
|
||||||
|
import ActorsOnStageBox from "../components/ActorsOnStageBox.vue"
|
||||||
|
import { current } from "../state"
|
||||||
|
import MotionsList from "../components/MotionsList.vue"
|
||||||
|
import StageTopDownView from "../components/StageTopDownView.vue"
|
||||||
|
import MessageDisplay from "../components/MessageDisplay.vue"
|
||||||
|
import MessageEdit from "../components/MessageEdit.vue"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "ReinPage",
|
||||||
|
components: { MessageEdit, MessageDisplay, StageTopDownView, MotionsList, ActorsOnStageBox, MessageBox, MusicProgressBar },
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
current: current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
218
ui/src/state.ts
Executable file
218
ui/src/state.ts
Executable file
|
@ -0,0 +1,218 @@
|
||||||
|
import { reactive, shallowRef } from "vue"
|
||||||
|
import { reactiveComputed } from "@vueuse/core"
|
||||||
|
|
||||||
|
export interface Act {
|
||||||
|
name: string
|
||||||
|
scenes: Scene[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Scene {
|
||||||
|
name: string
|
||||||
|
steps: Step[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Step {
|
||||||
|
position: ShowPosition
|
||||||
|
cue: StepCue
|
||||||
|
actorEntrances: string[]
|
||||||
|
actorExits: string[]
|
||||||
|
actorsOnStage: string[]
|
||||||
|
props: PropMap
|
||||||
|
hasChangedProps: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StepCue = {
|
||||||
|
type: "TEXT",
|
||||||
|
speaker: string
|
||||||
|
text: string
|
||||||
|
clarification?: string
|
||||||
|
} | {
|
||||||
|
type: "MUSIC_START",
|
||||||
|
title: string
|
||||||
|
duration: number
|
||||||
|
} | {
|
||||||
|
type: "MUSIC_END"
|
||||||
|
} | {
|
||||||
|
type: "CURTAIN",
|
||||||
|
state: "open" | "closed"
|
||||||
|
whileMoving: boolean
|
||||||
|
} | {
|
||||||
|
type: "LIGHTS"
|
||||||
|
state: "on" | "off"
|
||||||
|
whileFading: boolean
|
||||||
|
} | {
|
||||||
|
type: "CUSTOM"
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PropMap = Record<PropPosition, string | null>
|
||||||
|
export type PropPosition =
|
||||||
|
| "PROSCENIUM_LEFT"
|
||||||
|
| "PROSCENIUM_CENTER"
|
||||||
|
| "PROSCENIUM_RIGHT"
|
||||||
|
| "CENTER"
|
||||||
|
| "LEFT"
|
||||||
|
| "RIGHT"
|
||||||
|
| "BACKDROP"
|
||||||
|
|
||||||
|
export interface ShowState {
|
||||||
|
position: ShowPosition
|
||||||
|
message: string
|
||||||
|
activeMusic: ShowMusic | null
|
||||||
|
musicStartTime: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShowMusic {
|
||||||
|
title: string
|
||||||
|
duration: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShowPosition {
|
||||||
|
act: number
|
||||||
|
scene: number
|
||||||
|
step: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Show {
|
||||||
|
acts: Act[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const START_STEP: Step = {
|
||||||
|
position: { act: -1, scene: 0, step: 0 },
|
||||||
|
actorsOnStage: [],
|
||||||
|
cue: {
|
||||||
|
type: "CUSTOM",
|
||||||
|
text: "Start"
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
BACKDROP: null,
|
||||||
|
LEFT: null,
|
||||||
|
CENTER: null,
|
||||||
|
RIGHT: null,
|
||||||
|
PROSCENIUM_LEFT: null,
|
||||||
|
PROSCENIUM_CENTER: null,
|
||||||
|
PROSCENIUM_RIGHT: null
|
||||||
|
},
|
||||||
|
hasChangedProps: false,
|
||||||
|
actorEntrances: [],
|
||||||
|
actorExits: []
|
||||||
|
}
|
||||||
|
|
||||||
|
const START_SCENE: Scene = {
|
||||||
|
name: "Start",
|
||||||
|
steps: [START_STEP]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const show = shallowRef<Show>({
|
||||||
|
acts: []
|
||||||
|
})
|
||||||
|
|
||||||
|
export const state = reactive<ShowState>({
|
||||||
|
position: START_STEP.position,
|
||||||
|
message: "",
|
||||||
|
activeMusic: null,
|
||||||
|
musicStartTime: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
export function getStep(position: ShowPosition) {
|
||||||
|
if (position.act === -1) return START_STEP
|
||||||
|
return getScene(position).steps[position.step]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getScene(position: ShowPosition) {
|
||||||
|
if (position.act === -1) return START_SCENE
|
||||||
|
return show.value.acts[position.act].scenes[position.scene]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActiveMusicAt(position: ShowPosition): ShowMusic | null {
|
||||||
|
let activeMusic: ShowMusic | null = null
|
||||||
|
|
||||||
|
for (let actIndex = 0; actIndex < show.value.acts.length; actIndex++) {
|
||||||
|
const scenes = show.value.acts[actIndex].scenes
|
||||||
|
|
||||||
|
for (let sceneIndex = 0; sceneIndex < scenes.length; sceneIndex++) {
|
||||||
|
const scene = scenes[sceneIndex]
|
||||||
|
|
||||||
|
for (let stepIndex = 0; stepIndex < scene.steps.length; stepIndex++) {
|
||||||
|
const step = scene.steps[stepIndex]
|
||||||
|
|
||||||
|
// SONG
|
||||||
|
if (step.cue.type === "MUSIC_START") {
|
||||||
|
activeMusic = {
|
||||||
|
title: step.cue.title,
|
||||||
|
duration: step.cue.duration
|
||||||
|
}
|
||||||
|
} else if (step.cue.type === "MUSIC_END") {
|
||||||
|
activeMusic = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sceneIndex == position.scene && stepIndex == position.step) return activeMusic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const current = reactiveComputed<{
|
||||||
|
scene: Scene
|
||||||
|
step: Step
|
||||||
|
activeMusic: ShowMusic | null
|
||||||
|
}>(() => ({
|
||||||
|
scene: getScene(state.position),
|
||||||
|
step: getStep(state.position),
|
||||||
|
activeMusic: state.activeMusic
|
||||||
|
}))
|
||||||
|
|
||||||
|
export function parseStringWithDetails(string: string) {
|
||||||
|
const parts = string.split(" / ")
|
||||||
|
if (parts.length === 1) return { main: parts[0], details: "" }
|
||||||
|
return { main: parts[0], details: parts.slice(1).join(" / ") }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNextValidPosition(start: ShowPosition): ShowPosition | null {
|
||||||
|
if (start.act === START_STEP.position.act) return { act: 0, scene: 0, step: 0 }
|
||||||
|
const acts = show.value.acts
|
||||||
|
let { act, scene, step } = start
|
||||||
|
|
||||||
|
step++
|
||||||
|
|
||||||
|
if (step >= acts[start.act].scenes[start.scene].steps.length) {
|
||||||
|
step = 0
|
||||||
|
scene++
|
||||||
|
|
||||||
|
if (scene >= acts[start.act].scenes.length) {
|
||||||
|
scene = 0
|
||||||
|
act++
|
||||||
|
|
||||||
|
if (act >= acts.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { act, scene, step }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPreviousValidPosition(start: ShowPosition): ShowPosition | null {
|
||||||
|
let { act, scene, step } = start
|
||||||
|
step--
|
||||||
|
|
||||||
|
if (step < 0) {
|
||||||
|
scene--
|
||||||
|
|
||||||
|
if (scene < 0) {
|
||||||
|
act--
|
||||||
|
|
||||||
|
if (act < 0) {
|
||||||
|
return START_STEP.position
|
||||||
|
}
|
||||||
|
|
||||||
|
scene = show.value.acts[act].scenes.length - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
step = show.value.acts[act].scenes[scene].steps.length - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return { act, scene, step }
|
||||||
|
}
|
58
ui/src/syncing.ts
Executable file
58
ui/src/syncing.ts
Executable file
|
@ -0,0 +1,58 @@
|
||||||
|
import ReconnectingWebSocket from "reconnecting-websocket"
|
||||||
|
import { show, ShowPosition, state } from "./state"
|
||||||
|
import { ref, toRaw } from "vue"
|
||||||
|
|
||||||
|
let socket: ReconnectingWebSocket | null = null
|
||||||
|
let timeDifference = 0
|
||||||
|
|
||||||
|
export const getSyncedTime = () => Date.now() - timeDifference
|
||||||
|
export const syncedTime = ref(Date.now())
|
||||||
|
|
||||||
|
const setSyncedTimeRef = () => {
|
||||||
|
syncedTime.value = getSyncedTime()
|
||||||
|
requestAnimationFrame(setSyncedTimeRef)
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(setSyncedTimeRef)
|
||||||
|
|
||||||
|
export const connect = () => new Promise<void>(resolve => {
|
||||||
|
if (socket !== null) return
|
||||||
|
|
||||||
|
const url = new URL(window.location.toString())
|
||||||
|
url.protocol = "ws"
|
||||||
|
url.pathname = "/api/ws"
|
||||||
|
socket = new ReconnectingWebSocket(url.toString(), [], {
|
||||||
|
reconnectionDelayGrowFactor: 1,
|
||||||
|
minReconnectionDelay: 1000
|
||||||
|
})
|
||||||
|
|
||||||
|
let isFirstMessage = true
|
||||||
|
|
||||||
|
socket.addEventListener("open", () => {
|
||||||
|
isFirstMessage = true
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.addEventListener("message", event => {
|
||||||
|
const data = JSON.parse(event.data as string)
|
||||||
|
|
||||||
|
if (isFirstMessage) {
|
||||||
|
isFirstMessage = false
|
||||||
|
show.value = data
|
||||||
|
console.log("Show:", data)
|
||||||
|
} else {
|
||||||
|
Object.assign(state, data.state)
|
||||||
|
timeDifference = Date.now() - data.timestamp
|
||||||
|
console.log("New state:", data.state)
|
||||||
|
console.log("New time difference:", timeDifference)
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function goToPosition(position: ShowPosition) {
|
||||||
|
await fetch("/api/go", { method: "POST", body: JSON.stringify(position), headers: { "Content-Type": "application/json" } })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setMessage(message: string) {
|
||||||
|
await fetch("/api/message", { method: "POST", body: message })
|
||||||
|
}
|
3
ui/src/vite-env.d.ts
vendored
Executable file
3
ui/src/vite-env.d.ts
vendored
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
/// <reference types="vite-plugin-pages/client"/>
|
||||||
|
/// <reference types="unplugin-icons/types/vue"/>
|
||||||
|
/// <reference types="vite/client"/>
|
20
ui/tsconfig.json
Executable file
20
ui/tsconfig.json
Executable file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"module": "ESNext",
|
||||||
|
"target": "ES2021",
|
||||||
|
"lib": ["DOM", "ESNext"],
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": false,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"downlevelIteration": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"node_modules",
|
||||||
|
"src/**/*"
|
||||||
|
]
|
||||||
|
}
|
25
ui/vite.config.ts
Executable file
25
ui/vite.config.ts
Executable file
|
@ -0,0 +1,25 @@
|
||||||
|
import { defineConfig, splitVendorChunkPlugin } from "vite"
|
||||||
|
import vuePlugin from "@vitejs/plugin-vue"
|
||||||
|
import windicssPlugin from "vite-plugin-windicss"
|
||||||
|
import pagesPlugin from "vite-plugin-pages"
|
||||||
|
import iconsPlugin from "unplugin-icons/vite"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
splitVendorChunkPlugin(),
|
||||||
|
vuePlugin(),
|
||||||
|
pagesPlugin({
|
||||||
|
syncIndex: false
|
||||||
|
}),
|
||||||
|
windicssPlugin(),
|
||||||
|
iconsPlugin()
|
||||||
|
],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
"/api": {
|
||||||
|
target: "http://localhost:8000",
|
||||||
|
ws: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
44
ui/windi.config.ts
Executable file
44
ui/windi.config.ts
Executable file
|
@ -0,0 +1,44 @@
|
||||||
|
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