Compare commits

..

1 commit

Author SHA1 Message Date
d881463411 notification support 2024-10-13 01:49:19 +02:00
15 changed files with 1016 additions and 982 deletions

View file

@ -1,7 +1,20 @@
# Syng web client # foundation-vue
This is the web client for [Syng](https://github.com/christofsteel/syng) This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development ### Compile and Hot-Reload for Development

1645
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "syng-web", "name": "syng-web",
"version": "2.0.4", "version": "2.0.0",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
@ -11,6 +11,7 @@
"@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-brands-svg-icons": "^6.2.1", "@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1",
"@fortawesome/free-regular-svg-icons": "^6.2.1",
"@fortawesome/vue-fontawesome": "^3.0.2", "@fortawesome/vue-fontawesome": "^3.0.2",
"foundation-sites": "^6.7.5", "foundation-sites": "^6.7.5",
"socket.io-client": "^4.5.3", "socket.io-client": "^4.5.3",

View file

@ -28,17 +28,31 @@ const state = ref({
'secret': null, 'secret': null,
'current_entry': null, 'current_entry': null,
'current_name': null, 'current_name': null,
'last_notification_uuid': null,
'searching': false, 'searching': false,
'last_msg': "", 'last_msg': "",
'join_msg': null, 'join_msg': null,
'uid': null, 'uid': null,
'double_entry': {'artist': null, 'title': null, 'reason': null}, 'double_entry': {'artist': null, 'title': null, 'reason': null},
'waiting_room_policy': null, 'waiting_room_policy': null,
'config': {}, 'notify_me': [],
'kiosk': false 'config': {}
}) })
onMounted(() => { onMounted(() => {
if (!window.Notification) {
console.log('Browser does not support notifications.');
} else {
if (Notification.permission !== 'granted') {
Notification.requestPermission().then(
p => {
if (p !== 'granted') {
console.log('User blocked notifications.');
}
}
).catch(err => console.error(err))
}
}
window.addEventListener("resize", (e) => { state.value.is_small = (e.target.innerWidth < 768) }); window.addEventListener("resize", (e) => { state.value.is_small = (e.target.innerWidth < 768) });
$(document).foundation(); $(document).foundation();
if(localStorage.name && localStorage.name != "null"){ state.value.name = localStorage.name } if(localStorage.name && localStorage.name != "null"){ state.value.name = localStorage.name }
@ -48,6 +62,7 @@ onMounted(() => {
} }
if(localStorage.secret){ state.value.secret = localStorage.secret } if(localStorage.secret){ state.value.secret = localStorage.secret }
if(localStorage.uid){ state.value.uid = localStorage.uid } if(localStorage.uid){ state.value.uid = localStorage.uid }
if(localStorage.notify_me) { state.value.notify_me = JSON.parse(localStorage.notify_me); }
if(state.value.server && state.value.room && state.value.name) { if(state.value.server && state.value.room && state.value.name) {
connect() connect()
} }
@ -59,7 +74,7 @@ function emptyLocalStorageAndLogout() {
localStorage.removeItem('name') localStorage.removeItem('name')
localStorage.removeItem('room') localStorage.removeItem('room')
localStorage.removeItem('uid') localStorage.removeItem('uid')
state.socket.disconnect() state.value.socket.disconnect()
state.value.joined = false state.value.joined = false
} }
@ -71,10 +86,9 @@ function updateName(evt) { evt.target.textContent = state.value.name;}
function setServer(server) { state.value.server = server } function setServer(server) { state.value.server = server }
function setSearchTerm(searchTerm) { state.value.search.searchTerm = searchTerm } function setSearchTerm(searchTerm) { state.value.search.searchTerm = searchTerm }
function update_config(config) { function update_config(config) {
state.socket.emit("update_config", {"config": config}) state.value.socket.emit("update_config", {"config": config})
close_config() close_config()
} }
function setKiosk(kiosk) { state.value.kiosk = kiosk }
function search() { function search() {
var yt_checker = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/; var yt_checker = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;
@ -82,18 +96,14 @@ function search() {
append({"ident": state.value.search.searchTerm, "source": "youtube"}) append({"ident": state.value.search.searchTerm, "source": "youtube"})
} else { } else {
state.value.searching = true state.value.searching = true
state.socket.emit("search", {"query": state.value.search.searchTerm }) state.value.socket.emit("search", {"query": state.value.search.searchTerm })
} }
} }
function show_config() { function show_config() {
state.socket.emit("show_config"); state.value.socket.emit("show_config");
} }
function waitingRoomToQueue(uuid) {
state.socket.emit("waiting-room-to-queue", {"uuid": uuid})
}
function append(entry) { function append(entry) {
checked_append_with_name(entry, state.value.name) checked_append_with_name(entry, state.value.name)
} }
@ -104,39 +114,39 @@ function checked_append_with_name(entry, name) {
$("#getusername").foundation("open"); $("#getusername").foundation("open");
} else { } else {
$("#getusername").foundation("close"); $("#getusername").foundation("close");
raw_append(entry.ident, name, entry.source, entry.title, entry.artist, state.value.uid); raw_append(entry.ident, name, entry.source, state.value.uid);
} }
} }
function append_anyway(entry) { function append_anyway(ident, name, source, uid) {
$("#getusername").foundation("close"); $("#getusername").foundation("close");
$("#alreadyqueued").foundation("close"); $("#alreadyqueued").foundation("close");
state.value.current_name = null; state.value.current_name = null;
state.value.current_entry = null; state.value.current_entry = null;
state.value.double_entry = {'artist': null, 'title': null, 'reason': null}; state.value.double_entry = {'artist': null, 'title': null, 'reason': null};
state.socket.emit("append-anyway", {"ident": entry.ident, "performer": entry.performer, "source": entry.source, "title": entry.title, "artist": entry.artist, "uid": null }); state.value.socket.emit("append-anyway", {"ident": ident, "performer": name, "source": source, "uid": uid });
$("#queue-tab-title").click(); $("#queue-tab-title").click();
} }
function raw_append(ident, name, source, title, artist, uid) { function raw_append(ident, name, source, uid) {
$("#getusername").foundation("close"); $("#getusername").foundation("close");
$("#alreadyqueued").foundation("close"); $("#alreadyqueued").foundation("close");
state.value.current_name = null; state.value.current_name = null;
state.value.current_entry = null; state.value.current_entry = null;
state.value.double_entry = {'artist': null, 'title': null, 'reason': null}; state.value.double_entry = {'artist': null, 'title': null, 'reason': null};
state.socket.emit("append", {"ident": ident, "performer": name, "source": source, "title":title, "artist": artist,"uid": uid }); state.value.socket.emit("append", {"ident": ident, "performer": name, "source": source, "uid": uid });
$("#queue-tab-title").click(); $("#queue-tab-title").click();
} }
function wait_append(entry) { function wait_append(ident, name, source, uid) {
$("#getusername").foundation("close"); $("#getusername").foundation("close");
$("#alreadyqueued").foundation("close"); $("#alreadyqueued").foundation("close");
state.socket.emit("waiting-room-append", {"ident": entry.ident, "performer": entry.performer, "source": entry.source, "title": entry.title, "artist": entry.artist, "uid": null });
state.value.current_name = null; state.value.current_name = null;
state.value.current_entry = null; state.value.current_entry = null;
state.value.socket.emit("waiting-room-append", {"ident": ident, "performer": name, "source": source, "uid": uid });
$("#queue-tab-title").click(); $("#queue-tab-title").click();
} }
@ -164,66 +174,89 @@ function connect() {
state.value.uid = Math.random(); state.value.uid = Math.random();
} }
} }
registerSocketEvents(state.socket) registerSocketEvents(state.value.socket)
} }
function skipCurrent() { function notify_disable(uuid) {
state.socket.emit("skip-current") let notify_me = JSON.parse(localStorage.notify_me)
let idx = notify_me.indexOf(uuid);
notify_me.splice(idx, 1);
localStorage.setItem("notify_me", JSON.stringify(notify_me))
state.value.notify_me = notify_me
} }
function moveUp(uuid) { function notify_enable(uuid) {
state.socket.emit("move-up", {"uuid": uuid}) let notify_me = JSON.parse(localStorage.notify_me)
} notify_me.push(uuid)
localStorage.setItem("notify_me", JSON.stringify(notify_me))
function moveTo(data) { state.value.notify_me = notify_me
state.socket.emit("move-to", data)
}
function skip(uuid) {
state.socket.emit("skip", {"uuid": uuid})
} }
function registerSocketEvents() { function registerSocketEvents() {
state.socket = io(state.value.server) state.value.socket = io(state.value.server)
state.socket.on("search-results", (results) => { state.value.socket.on("search-results", (results) => {
state.value.searching = false state.value.searching = false
state.value.search.searchResults = results.results state.value.search.searchResults = results.results
}) })
state.socket.on("connect", () => { joinRoom() }) state.value.socket.on("connect", () => { joinRoom() })
state.socket.io.on("reconnect", () => { joinRoom() }) state.value.socket.io.on("reconnect", () => { joinRoom() })
state.socket.on("state", (val) => { state.value.socket.on("state", (val) => {
if(!localStorage.notify_me) {
localStorage.setItem("notify_me", "[]");
state.value.notify_me = []
}
var new_notify = [] // Somehow Array.filter does not work
state.value.notify_me.forEach((uuid) => {
for(const entry of val.queue) {
if(entry.uuid === uuid) {
new_notify.push(uuid)
};
};
});
state.value.notify_me = new_notify;
localStorage.setItem("notify_me", JSON.stringify(state.value.notify_me))
if (val.queue.length > 1 &&
val.queue[1].uuid != state.value.last_notification_uuid &&
state.value.notify_me.indexOf(val.queue[1].uuid) !== -1 &&
Notification.permission === 'granted') {
state.value.last_notification_uuid = val.queue[1].uuid;
new Notification("Next Up for " + val.queue[1].performer + ":", {body: val.queue[1].artist + " - " + val.queue[1].title})
}
state.value.queue=val.queue state.value.queue=val.queue
state.value.recent=val.recent state.value.recent=val.recent
state.value.waiting_room = val.waiting_room state.value.waiting_room = val.waiting_room
state.value.waiting_room_policy = val.config.waiting_room_policy state.value.waiting_room_policy = val.config.waiting_room_policy
}) })
state.socket.on("config", (response) => { state.value.socket.on("config", (response) => {
state.value.config=response state.value.config=response
$("#config").foundation("open") $("#config").foundation("open")
}) })
state.socket.on("update_config", (response) => { state.value.socket.on("update_config", (response) => {
console.log(response) console.log(response)
state.value.waiting_room_policy = response["waiting_room_policy"] state.value.waiting_room_policy = response["waiting_room_policy"]
console.log(state) console.log(state)
}) })
state.socket.on("msg", (response) => { state.value.socket.on("msg", (response) => {
state.value.last_msg = response.msg state.value.last_msg = response.msg
$("#msg").foundation("open") $("#msg").foundation("open")
}) })
state.socket.on("ask_for_waitingroom", (response) => { state.value.socket.on("ask_for_waitingroom", (response) => {
state.value.double_entry = response.old_entry; state.value.double_entry = response.old_entry;
state.value.current_entry = response.current_entry; state.value.current_entry = response.current_entry;
$("#alreadyqueued").foundation("open"); $("#alreadyqueued").foundation("open");
}) })
state.socket.on("err", (response) => { state.value.socket.on("err", (response) => {
console.log(response) console.log(response)
switch(response.type) { switch(response.type) {
case "QUEUE_FULL": case "QUEUE_FULL":
@ -253,7 +286,7 @@ function registerSocketEvents() {
function joinRoom() { function joinRoom() {
console.log("Joining room " + state.value.room) console.log("Joining room " + state.value.room)
state.socket.emit("register-web", {"room": state.value.room}, (response) => { state.value.socket.emit("register-web", {"room": state.value.room}, (response) => {
if(response === true) { if(response === true) {
localStorage.name = state.value.name localStorage.name = state.value.name
localStorage.server = state.value.server localStorage.server = state.value.server
@ -263,17 +296,19 @@ function joinRoom() {
state.value.joined = true state.value.joined = true
router.push({name: "room", params: {room: state.value.room}}) router.push({name: "room", params: {room: state.value.room}})
if (state.value.secret) { if (state.value.secret) {
state.socket.emit("register-admin", {"secret": state.value.secret}, (response) => { state.value.socket.emit("register-admin", {"secret": state.value.secret}, (response) => {
state.value.admin = response state.value.admin = response
}) })
} }
} else { } else {
state.value.join_msg = "<strong>No such room!</strong> <br/>" + state.value.join_msg = "<strong>No such room!</strong> <br/>" +
"Please use the correct room code your organizer provided you.<br/>" + "Please use the correct room code your organizer provided you.<br/>" +
"To host your own syng powered karaoke events, please download and " + "To host your own syng powered karaoke parties, please download and " +
"install <a href='https://github.com/christofsteel/syng' target='_blank'>Syng</a>" "install <a href='https://git.k-fortytwo.de/christofsteel/syng2.git' " +
"target='_blank'>Syng</a> and run it with <pre>syng-client " +
state.value.server + "</pre>"
if(state.value.joined) { if(state.value.joined) {
state.socket.disconnect() state.value.socket.disconnect()
setTimeout(() => connect(), 2000) setTimeout(() => connect(), 2000)
} }
} }
@ -283,18 +318,15 @@ function joinRoom() {
<template> <template>
<div class="page"> <div class="page">
<div class="row" :id="state.kiosk ? 'main-content-kiosk' : 'main-content'"> <div class="row" id="main-content">
<MobileLayout <MobileLayout
v-show="state.is_small" v-show="state.is_small"
:state="state" :state="state"
@update:searchTerm="setSearchTerm" @update:searchTerm="setSearchTerm"
@search="search" @search="search"
@append="append" @append="append"
@skip="skip" @notify_disable="notify_disable"
@skipCurrent="skipCurrent" @notify_enable="notify_enable"
@moveUp="moveUp"
@moveTo="moveTo"
@waitingRoomToQueue="waitingRoomToQueue"
/> />
<DesktopLayout <DesktopLayout
v-show="!state.is_small" v-show="!state.is_small"
@ -302,11 +334,8 @@ function joinRoom() {
@update:searchTerm="setSearchTerm" @update:searchTerm="setSearchTerm"
@search="search" @search="search"
@append="append" @append="append"
@skip="skip" @notify_disable="notify_disable"
@skipCurrent="skipCurrent" @notify_enable="notify_enable"
@moveUp="moveUp"
@moveTo="moveTo"
@waitingRoomToQueue="waitingRoomToQueue"
/> />
<WelcomeReveal <WelcomeReveal
v-if="!state.joined" v-if="!state.joined"
@ -315,13 +344,11 @@ function joinRoom() {
:joinMsg="state.join_msg" :joinMsg="state.join_msg"
:name="state.name" :name="state.name"
:secret="state.secret" :secret="state.secret"
:kiosk="state.kiosk"
@connect="connect" @connect="connect"
@update:room="setRoomCode" @update:room="setRoomCode"
@update:name="setName" @update:name="setName"
@update:server="setServer" @update:server="setServer"
@update:secret="setSecret" @update:secret="setSecret"
@update:kiosk="setKiosk"
/> />
<GetUserName <GetUserName
:current_name="state.current_name" :current_name="state.current_name"
@ -331,12 +358,11 @@ function joinRoom() {
@close_name="close_name" @close_name="close_name"
/> />
<AlreadyQueued <AlreadyQueued
@append="append_anyway" @append="append_anyway(state.current_entry.ident, state.name ? state.name : state.current_name, state.current_entry.source, state.uid)"
@wait="wait_append" @wait="(uid) => wait_append(state.current_entry.ident, state.name ? state.name : state.current_name, state.current_entry.source, null)"
@cancel="close_already_queued" @cancel="close_already_queued"
:waiting_room_policy="state.waiting_room_policy" :waiting_room_policy="state.waiting_room_policy"
:double_entry="state.double_entry" :double_entry="state.double_entry"
:current_entry="state.current_entry"
/> />
<div class="reveal" id="msg" data-reveal> <div class="reveal" id="msg" data-reveal>
{{ state.last_msg }} {{ state.last_msg }}
@ -348,7 +374,7 @@ function joinRoom() {
<Footer <Footer
:name="state.name" :name="state.name"
:admin="state.admin" :admin="state.admin"
:kiosk="state.kiosk" :show_name="state.show_name"
@update:name="setName" @update:name="setName"
@logout="emptyLocalStorageAndLogout" @logout="emptyLocalStorageAndLogout"
@config="show_config" @config="show_config"
@ -371,15 +397,10 @@ function joinRoom() {
height: 100%; height: 100%;
position: relative; position: relative;
} }
#main-content { #main-content {
height: calc(100vh - 50px); height: calc(100vh - 50px);
max-height: 100vh; max-height: 100vh;
max-width: 100vw; max-width: 100vw;
} }
#main-content-kiosk {
height: 100vh;
max-height: 100vh;
max-width: 100vw;
}
</style> </style>

View file

@ -1,6 +1,6 @@
<script setup> <script setup>
const emits = defineEmits(["append", "wait", "cancel"]); const emits = defineEmits(["append", "wait", "cancel"]);
const props = defineProps(["double_entry", "current_entry", "waiting_room_policy"]); const props = defineProps(["double_entry", "waiting_room_policy"]);
</script> </script>
<template> <template>
<div class="reveal" id="alreadyqueued" data-reveal > <div class="reveal" id="alreadyqueued" data-reveal >
@ -10,20 +10,21 @@ const props = defineProps(["double_entry", "current_entry", "waiting_room_policy
</p> </p>
<p>To give everyone a chance to sing, you are limited to one song in the active queue, but you can add yourself to the <b>waiting room</b>. <p>To give everyone a chance to sing, you are limited to one song in the active queue, but you can add yourself to the <b>waiting room</b>.
Songs in the waiting room will be added to the queue, once your old song leaves the queue.</p> Songs in the waiting room will be added to the queue, once your old song leaves the queue.</p>
<p>If you think this is an error, you can</p> <p>If you think this is an error, you can
<ul> <ul>
<li>choose another name, or</li> <li>choose another name, or</li>
<li>add it to the waiting room and talk to the organizers for manual insertion into the queue.</li> <li>add it to the waiting room and talk to the organizers for manual insertion into the queue.</li>
</ul> </ul>
</p>
<div class="grid-x"> <div class="grid-x">
<div class="cell medium-6 small-12 btn"> <div class="cell medium-6 small-12 btn">
<button class="button expanded" @click="$emit('wait', current_entry)">Add to waiting room</button> <button class="button expanded" @click="$emit('wait', double_entry.reason == 'uid' ? double_entry.uid : null)">Add to waiting room</button>
</div> </div>
<div class="cell medium-6 small-12 btn"> <div class="cell medium-6 small-12 btn">
<button class="button expanded alert" @click="$emit('cancel')">Cancel</button> <button class="button expanded alert" @click="$emit('cancel')">Cancel</button>
</div> </div>
<div v-show="waiting_room_policy && waiting_room_policy.toLowerCase() == 'optional'" class="cell medium-12 small-12 btn"> <div v-show="waiting_room_policy && waiting_room_policy.toLowerCase() == 'optional'" class="cell medium-12 small-12 btn">
<button class="button expanded" @click="$emit('append', current_entry)">Append Anyway</button> <button class="button expanded" @click="$emit('append')">Append Anyway</button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -4,21 +4,24 @@ import QueueDesktop from './QueueDesktop.vue'
import RecentDesktop from './RecentDesktop.vue' import RecentDesktop from './RecentDesktop.vue'
const props = defineProps(['state']); const props = defineProps(['state']);
const emit = defineEmits(['update:searchTerm', 'search', 'append', 'skip', 'skipCurrent', 'moveUp', 'waitingRoomToQueue', 'moveTo']) const emit = defineEmits(['update:searchTerm', 'search', 'append', 'skip', 'skipCurrent', 'moveUp', 'waitingRoomToQueue', 'notify_disable', 'notify_enable'])
</script> </script>
<template> <template>
<div class="splitter"> <div class="splitter">
<SearchDesktop :search="state.search" :searching="state.searching" @update:searchTerm="(val) => $emit('update:searchTerm', val)" @search="$emit('search')" @append="(entry) => $emit('append', entry)" /> <SearchDesktop :socket="state.socket" :search="state.search" :searching="state.searching" @update:searchTerm="(val) => $emit('update:searchTerm', val)" @search="$emit('search')" @append="(entry) => $emit('append', entry)" />
<QueueDesktop <QueueDesktop
:socket="state.socket"
:queue="state.queue" :queue="state.queue"
:waiting_room="state.waiting_room" :waiting_room="state.waiting_room"
:waiting_room_policy="state.waiting_room_policy" :waiting_room_policy="state.waiting_room_policy"
:admin="state.admin" :admin="state.admin"
:notify_me="state.notify_me"
@notify_enable="(uuid) => $emit('notify_enable', uuid)"
@notify_disable="(uuid) => $emit('notify_disable', uuid)"
@skip="(uuid) => $emit('skip', uuid)" @skip="(uuid) => $emit('skip', uuid)"
@moveUp="(uuid) => $emit('moveUp', uuid)" @moveUp="(uuid) => $emit('moveUp', uuid)"
@moveTo="(data) => $emit('moveTo', data)"
@skipCurrent="$emit('skipCurrent')" @skipCurrent="$emit('skipCurrent')"
@waitingRoomToQueue="(uuid) => $emit('waitingRoomToQueue', uuid)" @waitingRoomToQueue="(uuid) => $emit('waitingRoomToQueue', uuid)"
/> />

View file

@ -1,13 +1,13 @@
<script setup> <script setup>
import { computed } from 'vue' import { computed } from 'vue'
const props = defineProps(['admin', 'entry', 'current', 'firstStartedAt', 'offset', 'currentTime', 'waitingRoom']) const props = defineProps(['socket', 'admin', 'entry', 'current', 'firstStartedAt', 'offset', 'currentTime', 'waitingRoom', 'isQueue', 'notify_me'])
const emits = defineEmits(['skip', 'skipCurrent', 'moveUp', 'waitingRoomToQueue', 'moveTo']) const emits = defineEmits(['moveUp', 'waitingRoomToQueue', 'notify_enable', 'notify_disable'])
function skip() { function skip() {
if(props.current) { if(props.current) {
emits("skipCurrent") props.socket.emit("skip-current")
} else { } else {
emits("skip", props.entry.uuid) props.socket.emit("skip", {"uuid": props.entry.uuid})
} }
} }
@ -16,9 +16,6 @@ const eta = computed(() =>{
let startTime = new Date(props.firstStartedAt * 1000) let startTime = new Date(props.firstStartedAt * 1000)
let playBackSeconds = (now - startTime) / 1000 let playBackSeconds = (now - startTime) / 1000
let etaSeconds = Math.round(props.offset - playBackSeconds) let etaSeconds = Math.round(props.offset - playBackSeconds)
if (etaSeconds < 0) {
etaSeconds = 0
}
if (isNaN(etaSeconds)) { if (isNaN(etaSeconds)) {
return null return null
@ -27,60 +24,18 @@ const eta = computed(() =>{
} }
}) })
const dragging = (e) => {
const data = {
uuid: props.entry.uuid,
index: $(e.target.closest('li')).index()}
console.log(data)
e.dataTransfer.clearData()
e.dataTransfer.setData('application/json', JSON.stringify(data))
e.dataTransfer.effectAllowed = 'move'
}
const dragend = (e) => {
e.preventDefault()
e.target.closest('li').classList.remove('draggedoverBottom')
e.target.closest('li').classList.remove('draggedoverTop')
}
const dropped = (e) => {
e.preventDefault()
e.target.closest('li').classList.remove('draggedoverBottom')
e.target.closest('li').classList.remove('draggedoverTop')
const drop_index = $(e.target.closest('li')).index()
const element = JSON.parse(e.dataTransfer.getData('application/json'))
if (element.index < drop_index) {
emits('moveTo', {"uuid": element.uuid , "target": drop_index + 1})
} else {
emits('moveTo', {"uuid": element.uuid , "target": drop_index})
}
}
const dragover = (e) => {
e.preventDefault()
e.dataTransfer.dropEffect = 'move'
const element = JSON.parse(e.dataTransfer.getData('application/json'))
const index = $(e.target.closest('li')).index()
if (index < element.index) {
e.target.closest('li').classList.add('draggedoverTop')
} else {
e.target.closest('li').classList.add('draggedoverBottom')
}
}
const dragleave = (e) => {
e.preventDefault()
e.target.closest('li').classList.remove('draggedoverTop')
e.target.closest('li').classList.remove('draggedoverBottom')
}
</script> </script>
<template> <template>
<li :class="[{ current: current }, {waitingRoom: waitingRoom}]" :draggable="admin" @dragstart="dragging" @dragend="dragend" @dragover="dragover" @dragleave="dragleave" @drop="dropped"> <li :class="[{ current: current }, {waitingRoom: waitingRoom}]">
<div class="grid-x"> <div class="grid-x">
<div class="cell" :class="{'small-9': admin}"> <div class="cell" :class="{'small-9': admin}">
<span class="artist">{{ entry.artist }}</span> <span class="artist">{{ entry.artist }}</span>
<span class="title">{{ entry.title }}</span><br /> <span class="title">{{ entry.title }}</span><br />
<span v-if="!current && !waitingRoom && isQueue" class="notify">
<font-awesome-icon :icon="['far', 'bell']" v-if="notify_me.indexOf(entry.uuid) === -1" @click="$emit('notify_enable', entry.uuid)"/>
<font-awesome-icon :icon="['fas', 'bell']" v-if="notify_me.indexOf(entry.uuid) !== -1" @click="$emit('notify_disable', entry.uuid)"/>
</span>
<span class="performer">{{ entry.performer }}</span> <span class="performer">{{ entry.performer }}</span>
<span v-if="!current && !waitingRoom" class="eta">{{ eta }}</span> <span v-if="!current && !waitingRoom" class="eta">{{ eta }}</span>
</div> </div>
@ -91,15 +46,15 @@ const dragleave = (e) => {
<button <button
class="button success fright" class="button success fright"
v-if="waitingRoom" v-if="waitingRoom"
@click="$emit('waitingRoomToQueue', entry.uuid)" > @click="socket.emit('waiting-room-to-queue', {'uuid': entry.uuid})" >
<font-awesome-icon icon="fa-solid fa-arrows-up-to-line" /> <font-awesome-icon icon="fa-solid fa-arrows-up-to-line" />
</button> </button>
<!-- <button --> <button
<!-- class="button warning fright" --> class="button warning fright"
<!-- v-if="!current && !waitingRoom" --> v-if="!current && !waitingRoom"
<!-- @click="$emit('moveUp', entry.uuid)" > --> @click="socket.emit('move-up', {'uuid': entry.uuid})" >
<!-- <font-awesome-icon icon="fa-solid fa-arrow-up" /> --> <font-awesome-icon icon="fa-solid fa-arrow-up" />
<!-- </button> --> </button>
</div> </div>
</div> </div>
@ -111,6 +66,11 @@ const dragleave = (e) => {
background-color: #008000 !important; background-color: #008000 !important;
} }
.notify {
display: inline;
margin-right: 10px;
}
.current::before, #large-current::before{ .current::before, #large-current::before{
content: "Now Playing"; content: "Now Playing";
text-align: center; text-align: center;
@ -125,12 +85,4 @@ const dragleave = (e) => {
content: "in "; content: "in ";
} }
.draggedoverTop {
border-top: 2px solid #008000;
}
.draggedoverBottom {
border-bottom: 2px solid #008000;
}
</style> </style>

View file

@ -1,9 +1,9 @@
<script setup> <script setup>
const props = defineProps(['name', 'admin', 'kiosk']) const props = defineProps(['name', 'admin', 'show_name'])
const emits = defineEmits(['update:name', 'logout', 'config']) const emits = defineEmits(['update:name', 'logout', 'config'])
</script> </script>
<template> <template>
<footer v-if="!kiosk"> <footer>
Name: <span Name: <span
class="userName" class="userName"
@keyup.enter="(evt) => evt.target.blur()" @keyup.enter="(evt) => evt.target.blur()"

View file

@ -5,7 +5,7 @@ import RecentTab from './RecentTab.vue'
import TabHeader from './TabHeader.vue' import TabHeader from './TabHeader.vue'
const props = defineProps(['state']); const props = defineProps(['state']);
const emit = defineEmits(['update:searchTerm', 'search', 'append', 'skip', 'skipCurrent', 'moveUp', 'waitingRoomToQueue', 'moveTo']) const emit = defineEmits(['update:searchTerm', 'search', 'append', 'skip', 'skipCurrent', 'moveUp', 'waitingRoomToQueue', 'notify_enable', 'notify_disable'])
</script> </script>
@ -18,15 +18,18 @@ const emit = defineEmits(['update:searchTerm', 'search', 'append', 'skip', 'skip
<TabHeader link="#recent-list" icon="fa-history" /> <TabHeader link="#recent-list" icon="fa-history" />
</div> </div>
<div class="tabs-container" data-tabs-content="main-tab"> <div class="tabs-container" data-tabs-content="main-tab">
<SearchTab :search="state.search" :searching="state.searching" @update:searchTerm="(val) => $emit('update:searchTerm', val)" @search="$emit('search')" @append="(entry) => $emit('append', entry)"/> <SearchTab :socket="state.socket" :search="state.search" :searching="state.searching" @update:searchTerm="(val) => $emit('update:searchTerm', val)" @search="$emit('search')" @append="(entry) => $emit('append', entry)"/>
<QueueTab <QueueTab
:socket="state.socket"
:queue="state.queue" :queue="state.queue"
:admin="state.admin" :admin="state.admin"
:waiting_room="state.waiting_room" :waiting_room="state.waiting_room"
:waiting_room_policy="state.waiting_room_policy" :waiting_room_enabled="state.waiting_room_enabled"
:notify_me="state.notify_me"
@notify_disable="(uuid) => $emit('notify_disable', uuid)"
@notify_enable="(uuid) => $emit('notify_enable', uuid)"
@skip="(uuid) => $emit('skip', uuid)" @skip="(uuid) => $emit('skip', uuid)"
@moveUp="(uuid) => $emit('moveUp', uuid)" @moveUp="(uuid) => $emit('moveUp', uuid)"
@moveTo="(data) => $emit('moveTo', data)"
@skipCurrent="$emit('skipCurrent')" @skipCurrent="$emit('skipCurrent')"
@waitingRoomToQueue="(uuid) => $emit('waitingRoomToQueue', uuid)" @waitingRoomToQueue="(uuid) => $emit('waitingRoomToQueue', uuid)"
/> />

View file

@ -1,21 +1,24 @@
<script setup> <script setup>
import QueueInner from './QueueInner.vue' import QueueInner from './QueueInner.vue'
const props = defineProps(['queue', 'waiting_room', 'admin', 'waiting_room_policy']); const props = defineProps(['socket','queue', 'waiting_room', 'admin', 'waiting_room_policy', 'notify_me']);
const emits = defineEmits(['skip', 'skipCurrent', 'moveUp', 'waitingRoomToQueue', 'moveTo']) const emits = defineEmits(['skip', 'skipCurrent', 'moveUp', 'waitingRoomToQueue', 'notify_disable', 'notify_enable'])
</script> </script>
<template> <template>
<div class="comp-column"> <div class="comp-column">
<div class="header">Queue</div> <div class="header">Queue</div>
<QueueInner <QueueInner
:socket="socket"
:queue="queue" :queue="queue"
:admin="admin" :admin="admin"
:waiting_room="waiting_room" :waiting_room="waiting_room"
:waiting_room_policy="waiting_room_policy" :waiting_room_policy="waiting_room_policy"
:notify_me="notify_me"
@notify_enable="(uuid) => $emit('notify_enable', uuid)"
@notify_disable="(uuid) => $emit('notify_disable', uuid)"
@skip="(uuid) => $emit('skip', uuid)" @skip="(uuid) => $emit('skip', uuid)"
@moveUp="(uuid) => $emit('moveUp', uuid)" @moveUp="(uuid) => $emit('moveUp', uuid)"
@moveTo="(data) => $emit('moveTo', data)"
@skipCurrent="$emit('skipCurrent')" @skipCurrent="$emit('skipCurrent')"
@waitingRoomToQueue="(uuid) => $emit('waitingRoomToQueue', uuid)" @waitingRoomToQueue="(uuid) => $emit('waitingRoomToQueue', uuid)"
/> />

View file

@ -2,8 +2,8 @@
import { onMounted, reactive } from 'vue' import { onMounted, reactive } from 'vue'
import Entry from './Entry.vue' import Entry from './Entry.vue'
const props = defineProps(['queue', 'waiting_room', 'admin', 'waiting_room_policy']); const props = defineProps(['socket', 'queue', 'waiting_room', 'admin', 'waiting_room_policy', 'notify_me']);
const emits = defineEmits(['skip', 'skipCurrent', 'moveUp', 'waitingRoomToQueue', 'moveTo']) const emits = defineEmits(['skip', 'skipCurrent', 'moveUp', 'waitingRoomToQueue', 'notify_disable', 'notify_enable'])
let currentTime = reactive({time: Date.now()}) let currentTime = reactive({time: Date.now()})
@ -23,27 +23,28 @@ function offset(index) {
} }
return _offset return _offset
} }
</script> </script>
<template> <template>
<div class="vsplit"> <div class="vsplit">
<div id="queue-list-wrapper" class="results"> <div id="queue-list-wrapper" class="results">
<ul id="queue" class="vertical menu" @drop="dropHandler"> <ul id="queue" class="vertical menu">
<Entry <Entry
v-for="(entry, index) in queue" v-for="(entry, index) in queue"
:socket="socket"
:entry="entry" :entry="entry"
:current="index == 0" :current="index == 0"
:isQueue="true"
:admin="admin" :admin="admin"
:notify_me="notify_me"
:firstStartedAt="queue[0].started_at" :firstStartedAt="queue[0].started_at"
:currentTime="currentTime.time" :currentTime="currentTime.time"
:offset="offset(index)" :offset="offset(index)"
@notify_enable="(uuid) => $emit('notify_enable', uuid)"
@notify_disable="(uuid) => $emit('notify_disable', uuid)"
@skip="(uuid) => $emit('skip', uuid)" @skip="(uuid) => $emit('skip', uuid)"
@skipCurrent="$emit('skipCurrent')" @skipCurrent="$emit('skipCurrent')"
@moveUp="(uuid) => $emit('moveUp', uuid)" @moveUp="(uuid) => $emit('moveUp', uuid)"
@moveTo="(data) => $emit('moveTo', data)"
/> />
</ul> </ul>
<div v-show="waiting_room_policy" class="header">Waiting room</div> <div v-show="waiting_room_policy" class="header">Waiting room</div>

View file

@ -1,22 +1,25 @@
<script setup> <script setup>
import QueueInner from './QueueInner.vue' import QueueInner from './QueueInner.vue'
const props = defineProps(['queue', 'waiting_room', 'admin', 'waiting_room_policy']); const props = defineProps(['socket','queue', 'waiting_room', 'admin', 'waiting_room_policy', 'notify_me']);
const emits = defineEmits(['skip', 'skipCurrent', 'moveUp', 'waitingRoomToQueue', 'moveTo']) const emits = defineEmits(['skip', 'skipCurrent', 'moveUp', 'waitingRoomToQueue', 'notify_enable', 'notify_disable'])
</script> </script>
<template> <template>
<div class="tabs-panel" id="queue-list"> <div class="tabs-panel" id="queue-list">
<QueueInner <QueueInner
:socket="socket"
:queue="queue" :queue="queue"
:admin="admin" :admin="admin"
:waiting_room="waiting_room" :waiting_room="waiting_room"
:waiting_room_policy="waiting_room_policy" :waiting_room_policy="waiting_room_policy"
:notify_me="notify_me"
@notify_disable="(uuid) => $emit('notify_disable', uuid)"
@notify_enable="(uuid) => $emit('notify_enable', uuid)"
@skip="(uuid) => $emit('skip', uuid)" @skip="(uuid) => $emit('skip', uuid)"
@moveUp="(uuid) => $emit('moveUp', uuid)" @moveUp="(uuid) => $emit('moveUp', uuid)"
@skipCurrent="$emit('skipCurrent')" @skipCurrent="$emit('skipCurrent')"
@waitingRoomToQueue="(uuid) => $emit('waitingRoomToQueue', uuid)" @waitingRoomToQueue="(uuid) => $emit('waitingRoomToQueue', uuid)"
@moveTo="(data) => $emit('moveTo', data)"
/> />
</div> </div>
</template> </template>

View file

@ -2,14 +2,14 @@
import SearchBar from './SearchBar.vue' import SearchBar from './SearchBar.vue'
import SearchResults from './SearchResults.vue' import SearchResults from './SearchResults.vue'
const props = defineProps(['search', 'searching']); const props = defineProps(['search', 'searching', 'socket']);
const emit = defineEmits(['update:searchTerm', 'search', 'append']) const emit = defineEmits(['update:searchTerm', 'search', 'append'])
</script> </script>
<template> <template>
<div class="tabs-panel is-active" id="simplesearch"> <div class="tabs-panel is-active" id="simplesearch">
<div class="vsplit"> <div class="vsplit">
<SearchBar :searchTerm="search.searchTerm" @update:searchTerm="(val) => $emit('update:searchTerm', val)" @search="$emit('search')" @append="(entry) => $emit('append', entry)" /> <SearchBar :socket="socket" :searchTerm="search.searchTerm" @update:searchTerm="(val) => $emit('update:searchTerm', val)" @search="$emit('search')" @append="(entry) => $emit('append', entry)" />
<SearchResults :searchResults="search.searchResults" :searching="searching" @append="(entry) => $emit('append', entry)" /> <SearchResults :searchResults="search.searchResults" :searching="searching" @append="(entry) => $emit('append', entry)" />
</div> </div>
</div> </div>

View file

@ -2,8 +2,8 @@
import { onMounted, onBeforeUnmount } from 'vue' import { onMounted, onBeforeUnmount } from 'vue'
import $ from 'jquery' import $ from 'jquery'
const emits = defineEmits(['connect', 'update:room', 'update:name', 'update:server', 'update:secret', 'update:kiosk']) const emits = defineEmits(['connect', 'update:room', 'update:name', 'update:server', 'update:secret'])
const props = defineProps(['room', 'server', 'secret', 'name', 'joinMsg', 'kiosk']) const props = defineProps(['room', 'server', 'secret', 'name', 'joinMsg'])
onMounted(() => { onMounted(() => {
$(document).foundation(); $(document).foundation();
@ -54,11 +54,6 @@ onBeforeUnmount(() => {
<input type="password" @input="$emit('update:secret', $event.target.value)" :value="secret" placeholder="Leave free, if not admin"> <input type="password" @input="$emit('update:secret', $event.target.value)" :value="secret" placeholder="Leave free, if not admin">
</label> </label>
</div> </div>
<div class="medium-12 cell">
<label>Kiosk mode
<input type="checkbox" id="kiosk" @change="$emit('update:kiosk', $event.target.checked)">
</label>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -6,7 +6,8 @@ import App from './App.vue'
import Main from './Main.vue' import Main from './Main.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { faMagnifyingGlass, faList, faChair, faArrowUp, faHistory, faPlus, faStepForward, faTimes, faArrowsUpToLine } from '@fortawesome/free-solid-svg-icons' import { faMagnifyingGlass, faList, faChair, faArrowUp, faHistory, faPlus, faStepForward, faTimes, faArrowsUpToLine, faBell } from '@fortawesome/free-solid-svg-icons'
import { faBell as farBell } from '@fortawesome/free-regular-svg-icons'
import { faYoutube } from '@fortawesome/free-brands-svg-icons' import { faYoutube } from '@fortawesome/free-brands-svg-icons'
import 'foundation-sites/dist/css/foundation.min.css' import 'foundation-sites/dist/css/foundation.min.css'
@ -21,6 +22,8 @@ library.add(faTimes)
library.add(faArrowUp) library.add(faArrowUp)
library.add(faChair) library.add(faChair)
library.add(faArrowsUpToLine) library.add(faArrowsUpToLine)
library.add(faBell)
library.add(farBell)
window.jQuery = jquery; window.jQuery = jquery;
window.$ = jquery; window.$ = jquery;