418 lines
12 KiB
HTML
418 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Touchscreen Jukebox</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<!-- Bootstrap CSS -->
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<!-- Font Awesome -->
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
|
<link rel="stylesheet" href="https://unpkg.com/kioskboard@2.3.0/dist/kioskboard-2.3.0.min.css" />
|
|
<style>
|
|
body, html {
|
|
height: 100%;
|
|
overflow: hidden; /* Prevent normal scrolling */
|
|
}
|
|
body {
|
|
transform: scale(0.8);
|
|
transform-origin: top left;
|
|
width: 125%; /* 1920 * 0.7 */
|
|
height: 125%;
|
|
}
|
|
|
|
.top-bar, .bottom-bar {
|
|
height: 120px; /* ~3x normal nav */
|
|
}
|
|
.main-body {
|
|
height: calc(100% - 240px); /* subtract top+bottom bar */
|
|
display: flex;
|
|
overflow-x: auto;
|
|
overflow-y: hidden;
|
|
scroll-snap-type: x mandatory;
|
|
|
|
}
|
|
.jukebox-card {
|
|
flex: 0 0 40%;
|
|
margin: 5rem 1rem;
|
|
scroll-snap-align: start;
|
|
display: flex;
|
|
scroll-margin-left:1rem;
|
|
flex-direction: column;
|
|
}
|
|
.jukebox-card .card-body{
|
|
flex:1;
|
|
display:flex;
|
|
flex-direction:column;
|
|
overflow:hidden;
|
|
}
|
|
.artist-card {
|
|
flex: 0 0 50%;
|
|
margin: 5rem 1rem;
|
|
scroll-snap-align: start;
|
|
background:none;
|
|
scroll-margin-left:1rem;
|
|
border:none;
|
|
}
|
|
.song-list {
|
|
flex:1;
|
|
overflow-y: auto;
|
|
}
|
|
.bottom-bar .nav {
|
|
justify-content: center;
|
|
}
|
|
.bottom-bar .nav-link {
|
|
text-align: center;
|
|
color: white;
|
|
border: 1px solid white;
|
|
border-radius:0px;
|
|
}
|
|
.bottom-bar .nav-link i {
|
|
display: block;
|
|
font-size: 1.5rem;
|
|
}
|
|
.bg-dark{
|
|
background-color:#191919 !important;
|
|
}
|
|
.card{
|
|
border-radius:0px;
|
|
}
|
|
.main-body{
|
|
|
|
background: #000dff;
|
|
background:
|
|
linear-gradient(350deg, rgba(0, 13, 255, .5) 0%, rgba(255, 0, 157, .5) 80%),
|
|
linear-gradient(to right, rgba(0,0,0,0) 40vh, rgba(0,0,0,1) 75vh),
|
|
url("<?php echo $artist['images'][0]['url']; ?>") no-repeat;
|
|
background-size: contain;
|
|
background-blend-mode: multiply, destination-in, normal; /* control blending */
|
|
}
|
|
.credits{
|
|
border: 2px solid magenta;
|
|
border-radius: 50%;
|
|
font-size: 3em;
|
|
width:80px;
|
|
text-align: center;
|
|
font-weight: light;
|
|
}
|
|
.card.bg-dark,
|
|
.modal-content.bg-dark{
|
|
background-color: rgba(25, 25, 25, 0.8) !important;
|
|
}
|
|
.card > .card-header,
|
|
.modal-content > .modal-header{
|
|
background: #191919 !important;
|
|
}
|
|
li.bg-dark{
|
|
background:none!important;
|
|
}
|
|
.fs-1{
|
|
font-size: 5em !important;
|
|
}
|
|
.btn{
|
|
border-radius:0;
|
|
background: rgba(0,0,0,.8);
|
|
}
|
|
.modal-content{
|
|
border-radius:0;
|
|
}
|
|
.modal > .btn{
|
|
background: blue !important;
|
|
}
|
|
input.form-control{
|
|
border-radius:0;
|
|
}
|
|
/* make sure the container hides overflow */
|
|
#titlebox {
|
|
overflow: hidden;
|
|
position: relative;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* when the text needs scrolling */
|
|
#titlebox.scrolling .text {
|
|
display: inline-block; /* creates spacing for the loop */
|
|
animation: marquee 10s linear infinite;
|
|
}
|
|
|
|
/* pause at the start before moving */
|
|
@keyframes marquee {
|
|
0% { transform: translateX(0); }
|
|
10% { transform: translateX(0); } /* pause for 10% of duration */
|
|
100% { transform: translateX(-100%); }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-dark text-light">
|
|
|
|
<!-- Top Bar -->
|
|
<div class="top-bar bg-dark d-flex justify-content-between align-items-center px-4">
|
|
<!-- Now Playing -->
|
|
<div class="d-flex align-items-center m-0">
|
|
<img src="https://placehold.co/120" id="nowPlayingArt" width="120px" class="me-3" alt="Now Playing">
|
|
<div>
|
|
<div class="fw-bold text-primary">Now Playing</div>
|
|
<div class="fw-bold" id="nowPlayingTrack"></div>
|
|
<div class="text-light fw-light" id="nowPlayingArtist"></div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
|
|
<input type="text" id="search" class="form-control text-light bg-dark js-kioskboard-input" data-kioskboard-type="keyboard" data-kioskboard-placement="bottom" data-kioskboard-specialcharacters="false">
|
|
</div>
|
|
<!-- Credits & Prices -->
|
|
<div class="text-end d-flex align-items-center">
|
|
<div class="credits fw-lighter me-3" id="creditDisplay">0</div>
|
|
<div class="vr me-3"></div>
|
|
<div>
|
|
<div class="text-light">2 for $1.00</div>
|
|
<div class="text-light">13 for $5.00</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Body -->
|
|
<div class="main-body">
|
|
<!-- Example Card -->
|
|
|
|
|
|
<!-- Add more cards horizontally -->
|
|
<div class="card jukebox-card bg-dark text-light">
|
|
<div class="card-header">Playlists (12)</div>
|
|
<div class="card-body d-flex align-items-center justify-content-center">
|
|
<p>Playlist content goes here</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bottom Bar -->
|
|
<div class="bottom-bar bg-dark">
|
|
<ul class="nav nav-pills h-100">
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="#"><i class="fa fa-home"></i>Home</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="#"><i class="fa fa-search"></i>Search</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="#"><i class="fa fa-list"></i>Playlists</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="#" data-bs-toggle="modal" data-bs-target="#exampleModal"><i class="fa fa-heart"></i>Favorites</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="#" onclick="loadArtist()"><i class="fa fa-star"></i>Top Tracks</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="#"><i class="fa fa-ellipsis-h"></i>More+</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
|
|
<!-- Modal -->
|
|
<div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
<div class="modal-content bg-dark">
|
|
<div class="modal-body d-flex">
|
|
<div class="flex-shrink-1 m-5">
|
|
<img id="trackModalArt" src="https://placehold.co/250" width="250px"><br><br><span class="fs-small"><small>© 2011 COLUMBIA / LEGACY</small></span>
|
|
</div>
|
|
<div class="flex-grow-1 m-5 text-nowrap overflow-hidden">
|
|
<div id="titlebox" class="overflow-hidden text-nowrap">
|
|
<h2 class="fw-bold text" id="trackModalTitle"></h2>
|
|
</div>
|
|
<span id="trackModalArtist"></span> ><br>
|
|
<span class="fw-bold" id="trackModalAlbum"></span>
|
|
<hr>
|
|
<div class="d-flex mt-2">
|
|
<button class="btn flex-grow-1 w-50 btn-lg btn-primary" id="trackModalNextButton" onclick="queueNext(this,3)">Play Next<br><span class="badge rounded-pill text-bg-primary">3 Credits</span></button>
|
|
<button class="btn flex-grow-1 w-50 btn-lg btn-secondary" id="trackModalQueueButton" onclick="queueSong(this,2)">Queue Song<br><span class="badge rounded-pill text-bg-secondary">2 Credits</span></button>
|
|
</div>
|
|
<div id="msg">
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer d-flex flex-column align-items-center">
|
|
<button type="button" class="btn btn-secondary text-uppercase" id="trackModalArtistButton">More from this artist ></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bootstrap JS -->
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script src="https://unpkg.com/kioskboard@2.3.0/dist/kioskboard-2.3.0.min.js"></script>
|
|
<script src="./jsonws.js"></script>
|
|
<script src="./credits.js"></script>
|
|
<script src="./queue.js"></script>
|
|
<script src="./nowplaying.js"></script>
|
|
<script src="./cards.js"></script>
|
|
<script src="./templates.js"></script>
|
|
<script src="./viewManager.js"></script>
|
|
<script>
|
|
|
|
ViewManager.load(Templates.test);
|
|
|
|
//init WebSocket
|
|
const addr = "kiosk";
|
|
const client = new JsonRpcWsClient({
|
|
url: ("ws://" + addr + ':6680/mopidy/ws/'),
|
|
reconnect: true,
|
|
requestTimeout: 15000
|
|
});
|
|
|
|
var artistid = '6zFYqv1mOsgBRQbae3JJ9e';
|
|
|
|
var modalEl = document.getElementById("exampleModal");
|
|
var myModal = new bootstrap.Modal(modalEl);
|
|
|
|
function loadArtist(){
|
|
client
|
|
.call("core.library.browse",{uri:"spotify:artist:" + artistid})
|
|
.then(resultz => {
|
|
ViewManager.load(Templates.artist,resultz)
|
|
})
|
|
}
|
|
|
|
function loadTrack(el){
|
|
var uri = el.dataset.uri;
|
|
client.call("core.library.lookup",{"uris":[uri]}).then(trackRes =>{
|
|
var track = trackRes[uri][0];
|
|
console.dir("track: " + JSON.stringify(track));
|
|
const trackEl = document.getElementById("trackModalTitle");
|
|
const artistEl = document.getElementById("trackModalArtist");
|
|
const albumEl = document.getElementById("trackModalAlbum");
|
|
const buttonEl = document.getElementById("trackModalArtistButton");
|
|
|
|
if (trackEl) trackEl.textContent = track.name;
|
|
if (artistEl) artistEl.textContent = track.artists[0].name;
|
|
if (albumEl) artistEl.textContent = track.album.name;
|
|
|
|
|
|
client.call("core.library.get_images",{ uris: [track.album.uri] }).then(images => {
|
|
|
|
// Pick the first (largest?) image
|
|
const artUri = images[track.album.uri][0].uri;
|
|
const artEl = document.getElementById("trackModalArt");
|
|
if (artEl) artEl.src = artUri;
|
|
|
|
})
|
|
});
|
|
|
|
myModal.show();
|
|
}
|
|
|
|
client.on('notification', (notif) => {
|
|
console.log('JSON-RPC notification:', notif);
|
|
});
|
|
|
|
client.on('response', (r) => {
|
|
console.log('response event:', r);
|
|
});
|
|
|
|
client.on('message', (msg) => {
|
|
console.log('got plain JSON:', msg);
|
|
});
|
|
|
|
client.on('raw', (txt) => {
|
|
console.log('raw text:', txt);
|
|
});
|
|
|
|
client.on('error', (e) => console.error('ws error', e));
|
|
client.on('close', (ev) => console.log('closed', ev));
|
|
client.on('reconnect_scheduled', delay => console.log('reconnect in', delay, 'ms'));
|
|
|
|
// Start:
|
|
client.connect();
|
|
|
|
// Example: call after 2s (demonstrates queueing if not ready yet)
|
|
setTimeout(() => {
|
|
client.call('core.playback.get_time_position')
|
|
.then(pos => console.log('position', pos))
|
|
.catch(err => console.error('pos err', err));
|
|
}, 2000);
|
|
|
|
</script>
|
|
<script>
|
|
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const modal = document.getElementById("exampleModal");
|
|
const box = document.getElementById("titlebox");
|
|
const text = box.querySelector(".text");
|
|
|
|
function applyScrolling() {
|
|
box.classList.remove("scrolling");
|
|
text.style.paddingRight = "0px";
|
|
|
|
if (text.scrollWidth > box.clientWidth) {
|
|
const overflow = text.scrollWidth - box.clientWidth;
|
|
text.style.paddingRight = overflow + "px"; // dynamic gap
|
|
box.classList.add("scrolling");
|
|
}
|
|
}
|
|
|
|
// Recalculate once modal is fully visible
|
|
modal.addEventListener("shown.bs.modal", applyScrolling);
|
|
|
|
// Optional: recalc on window resize
|
|
window.addEventListener("resize", function () {
|
|
if (modal.classList.contains("show")) {
|
|
applyScrolling();
|
|
}
|
|
});
|
|
});
|
|
|
|
var usKeyboard = [
|
|
{
|
|
"0": "Q",
|
|
"1": "W",
|
|
"2": "E",
|
|
"3": "R",
|
|
"4": "T",
|
|
"5": "Y",
|
|
"6": "U",
|
|
"7": "I",
|
|
"8": "O",
|
|
"9": "P"
|
|
},
|
|
{
|
|
"0": "A",
|
|
"1": "S",
|
|
"2": "D",
|
|
"3": "F",
|
|
"4": "G",
|
|
"5": "H",
|
|
"6": "J",
|
|
"7": "K",
|
|
"8": "L"
|
|
},
|
|
{
|
|
"0": "Z",
|
|
"1": "X",
|
|
"2": "C",
|
|
"3": "V",
|
|
"4": "B",
|
|
"5": "N",
|
|
"6": "M"
|
|
}
|
|
];
|
|
|
|
|
|
KioskBoard.run('.js-kioskboard-input', {
|
|
keysArrayOfObjects:usKeyboard,
|
|
theme:"dark",
|
|
cssAnimations: true,
|
|
keysEnterText: 'Search',
|
|
keysAllowSpacebar: true,
|
|
allowRealKeyboard: true,
|
|
allowMobileKeyboard: true,
|
|
// ...init options
|
|
});
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|