technically 1.0
This commit is contained in:
commit
34981d292f
32
alt.html
Normal file
32
alt.html
Normal file
|
@ -0,0 +1,32 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>wormTuner</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<link rel="icon" href="/favicon.png" type="image/png" sizes="128x128">
|
||||
<script type="text/javascript" src="radio.js"></script>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<meta property="og:site_name" content="wormTuner">
|
||||
<meta property="og:title" content="wormTuner">
|
||||
<meta property="og:description" content="Worm-crafted radio waves, straight to your computer!">
|
||||
</head>
|
||||
<body>
|
||||
<center>
|
||||
<img src="banner.png" class='banner' width=384>
|
||||
<div class='player'>
|
||||
<div class='textainer'>
|
||||
<div class='station'></div>
|
||||
<div class='track'></div>
|
||||
<div class='genre'></div>
|
||||
</div>
|
||||
<video controls=1 height=62 width=370></video>
|
||||
</div>
|
||||
</center>
|
||||
<center class='section list'>
|
||||
<div><p>Click on a station to tune in!</p></div>
|
||||
<div class='broadcasts'>
|
||||
|
||||
</div>
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
166
audiovis.js
Normal file
166
audiovis.js
Normal file
|
@ -0,0 +1,166 @@
|
|||
/* wormTune Audio Visualizer */
|
||||
|
||||
var modes = ["None", "Bars","Scope","Spectrogram","Laser Rain"];
|
||||
var drawRequest = 0;
|
||||
var renderInverval;
|
||||
|
||||
function createVisualizer(player, context) {
|
||||
var audioSrc = context.createMediaElementSource(player);
|
||||
analyser = context.createAnalyser();
|
||||
|
||||
var canvas = document.querySelector("canvas.vis");
|
||||
|
||||
audioSrc.connect(analyser);
|
||||
analyser.connect(context.destination);
|
||||
|
||||
setVisualizer(canvas, analyser, 0);
|
||||
|
||||
return analyser;
|
||||
};
|
||||
|
||||
function setVisualizer(canvas, analyser, mode = 0) {
|
||||
if (!analyser) return;
|
||||
clearInterval(renderInverval);
|
||||
var ctx = canvas.getContext("2d",{antialias: false,alpha: false});
|
||||
|
||||
var WIDTH = canvas.width;
|
||||
var HEIGHT = canvas.height;
|
||||
|
||||
// Set the "defaults"
|
||||
analyser.smoothingTimeConstant = 0.8;
|
||||
analyser.fftSize = 2048;
|
||||
|
||||
if (mode != -1) canvas.height = 96;
|
||||
|
||||
window.cancelAnimationFrame(drawRequest);
|
||||
if (mode == -1) { // VIDEO MODE
|
||||
canvas.height = canvas.width / 4 * 3;
|
||||
renderInverval = window.setInterval(() => {
|
||||
ctx.drawImage(audioPlayer, 0, 0, WIDTH, HEIGHT);
|
||||
}, 1000 / 24);
|
||||
} else if (mode == 0 || mode > modes.length - 1) {
|
||||
function draw() {
|
||||
drawRequest = window.requestAnimationFrame(draw);
|
||||
|
||||
ctx.clearRect(0,0,WIDTH,HEIGHT);
|
||||
|
||||
ctx.font = "16px Pixio";
|
||||
ctx.fillStyle = "#FFF";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText("Click here to cycle visualizations.", WIDTH/2,HEIGHT/2);
|
||||
}
|
||||
draw();
|
||||
} else if (mode == 1) { // BAR
|
||||
analyser.fftSize = 256;
|
||||
var bufferLength = analyser.frequencyBinCount;
|
||||
var dataArray = new Uint8Array(bufferLength)
|
||||
|
||||
var barWidth = (WIDTH / bufferLength) * 2.5;
|
||||
var barHeight;
|
||||
|
||||
function draw() {
|
||||
ctx.clearRect(0, 0, WIDTH, HEIGHT);
|
||||
drawRequest = window.requestAnimationFrame(draw);
|
||||
|
||||
analyser.getByteFrequencyData(dataArray);
|
||||
ctx.fillStyle = "#FFF";
|
||||
|
||||
var x = 0;
|
||||
|
||||
ctx.beginPath();
|
||||
for (var i = 0; i < bufferLength; i++) {
|
||||
barHeight = (dataArray[i] / 256) * HEIGHT;
|
||||
ctx.fillRect(x, HEIGHT - barHeight, barWidth, barHeight);
|
||||
x += barWidth + 1;
|
||||
}
|
||||
ctx.closePath();
|
||||
}
|
||||
draw();
|
||||
} else if (mode == 2) { // SCOPE
|
||||
analyser.smoothingTimeConstant = 0;
|
||||
var dataArray = new Uint8Array(analyser.fftSize)
|
||||
|
||||
function draw() {
|
||||
ctx.clearRect(0, 0, WIDTH, HEIGHT);
|
||||
drawRequest = window.requestAnimationFrame(draw);
|
||||
|
||||
analyser.getByteTimeDomainData(dataArray);
|
||||
const step = WIDTH / dataArray.length;
|
||||
|
||||
ctx.strokeStyle = "#0F0";
|
||||
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i < dataArray.length; i += 2) {
|
||||
const percent = dataArray[i] / 256;
|
||||
const x = i * step;
|
||||
const y = HEIGHT * percent;
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
draw();
|
||||
} else if (mode == 3) { // SPECTROGRAM
|
||||
analyser.smoothingTimeConstant = 0;
|
||||
|
||||
var tempCanvas = document.createElement("canvas"),
|
||||
tempCtx = tempCanvas.getContext("2d");
|
||||
tempCanvas.width = WIDTH;
|
||||
tempCanvas.height = HEIGHT;
|
||||
|
||||
analyser.fftSize = Math.pow(2, Math.ceil(Math.log(WIDTH)/Math.log(2)));
|
||||
|
||||
var bufferLength = analyser.frequencyBinCount;
|
||||
var dataArray = new Uint8Array(bufferLength);
|
||||
|
||||
var start = new Date();
|
||||
//ctx.clearRect(0,0,WIDTH,HEIGHT);
|
||||
function draw() {
|
||||
drawRequest = window.requestAnimationFrame(draw);
|
||||
|
||||
analyser.getByteFrequencyData(dataArray);
|
||||
var barHeight = WIDTH/analyser.fftSize;
|
||||
|
||||
var now = new Date();
|
||||
if (now < new Date(start.getTime() + 20)) { return; }
|
||||
start = now;
|
||||
|
||||
tempCtx.drawImage(ctx.canvas, 0, 0, WIDTH, HEIGHT);
|
||||
|
||||
for (var i = 0; i < dataArray.length; i++) {
|
||||
var value = dataArray[i];
|
||||
ctx.fillStyle = 'rgb('+((value > 190) ? 255 : value)+', '+ ((value > 220) ? 255 : value-100) +', 0)';
|
||||
ctx.fillRect(WIDTH - 1, HEIGHT - i*barHeight, 1, barHeight);
|
||||
}
|
||||
ctx.translate(-1, 0);
|
||||
ctx.drawImage(tempCanvas, 0, 0, WIDTH, HEIGHT, 0, 0, WIDTH, HEIGHT);
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
}
|
||||
draw();
|
||||
} else if (mode == 4) { // RUNWAY
|
||||
analyser.smoothingTimeConstant = 0.2;
|
||||
|
||||
var bufferLength = analyser.frequencyBinCount;
|
||||
var dataArray = new Uint8Array(bufferLength);
|
||||
|
||||
//ctx.clearRect(0,0,WIDTH,HEIGHT);
|
||||
function draw() {
|
||||
drawRequest = window.requestAnimationFrame(draw);
|
||||
|
||||
analyser.getByteFrequencyData(dataArray);
|
||||
|
||||
ctx.clearRect(0,0,WIDTH,HEIGHT);
|
||||
for (var i = 0; i < dataArray.length; i++) {
|
||||
if (dataArray[i] == 0) continue;
|
||||
ctx.fillStyle = 'rgb('+dataArray[i]/4+','+dataArray[i]/2+','+dataArray[i]+')';
|
||||
ctx.fillRect((WIDTH / 2)+i, 0, 1, HEIGHT);
|
||||
ctx.fillRect((WIDTH / 2)-(i+1), 0, 1, HEIGHT);
|
||||
}
|
||||
// You are probably wondering how I got here, huh? Questioning my very sanity. Or maybe you completely understand, and as a web-developer, very well know the experience of bending the knees for Chrome in order to get something working that already works everywhere else. Since I've explained it, I think regardless, you understand now.
|
||||
ctx.font = "16px Pixio";
|
||||
ctx.fillStyle = "#4080FF0A";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText("0x4655434B204348524F4D45",WIDTH/2,HEIGHT/2);
|
||||
}
|
||||
draw();
|
||||
}
|
||||
}
|
BIN
banner.png
Normal file
BIN
banner.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
40
index.html
Normal file
40
index.html
Normal file
|
@ -0,0 +1,40 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>wormTuner</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<link rel="icon" href="/favicon.png" type="image/png" sizes="128x128">
|
||||
<script type="text/javascript" src="audiovis.js"></script>
|
||||
<script type="text/javascript" src="radio.js"></script>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<meta property="og:site_name" content="wormTuner">
|
||||
<meta property="og:title" content="wormTuner">
|
||||
<meta property="og:description" content="Worm-crafted radio waves, straight to your computer!">
|
||||
</head>
|
||||
<body>
|
||||
<center>
|
||||
<img src="banner.png" class='banner' width=384>
|
||||
<div class='player'>
|
||||
<div class='textainer'>
|
||||
<div class='station'></div>
|
||||
<div class='track'></div>
|
||||
<div class='genre'></div>
|
||||
</div>
|
||||
<div class="vismode hidden">0/4</div>
|
||||
<canvas class='vis' height=96 width=370><a>Test</a></canvas>
|
||||
<div class='vol hidden'>VOL [====================] 100%</div>
|
||||
<div class="about">
|
||||
<a class='name' href=""></a>
|
||||
<div style="flex-grow:1;"></div>
|
||||
<a class='vers'></a>
|
||||
</div>
|
||||
</div>
|
||||
</center>
|
||||
<center class='section list'>
|
||||
<div><p>Click on a station to tune in! If you are having trouble, try the <a href='alt.html'>Alternate player</a>.</p></div>
|
||||
<div class='broadcasts'>
|
||||
|
||||
</div>
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
310
radio.js
Normal file
310
radio.js
Normal file
|
@ -0,0 +1,310 @@
|
|||
/* == wormTuner == */
|
||||
/* The Icecast Radio JS Tuner */
|
||||
|
||||
const nameJs = "wormTuner";
|
||||
const version = "0.3.1";
|
||||
|
||||
const statusJson = "/status-json.xsl";
|
||||
|
||||
// 'mimetype': 'icon-path'
|
||||
const mimeIcons = {
|
||||
// 'audio/aac':'/mime/aac.png',
|
||||
};
|
||||
|
||||
const options = {
|
||||
'json-timer': 2500, // Time (in ms) for each stream update
|
||||
'visualizers': true, // whether to support visualizers
|
||||
'video-support': true, // whether to support video streams
|
||||
'character-overflow': 38, // character count needed to put in marquee.
|
||||
'replace-url': false, // fellow lazies please stand up
|
||||
'url-replacement': ['',''],
|
||||
'crossorigin': true
|
||||
};
|
||||
|
||||
var ajax = new XMLHttpRequest();
|
||||
var sources = [];
|
||||
|
||||
var isFancy = true;
|
||||
var castContainer;
|
||||
var currentStation = "";
|
||||
|
||||
var audioPlayer = document.createElement('video');
|
||||
var audioContext;
|
||||
var audioAnalyser;
|
||||
var visualizer;
|
||||
var visMode = 0;
|
||||
|
||||
var videoMode = false;
|
||||
|
||||
// Timers
|
||||
var volTimer;
|
||||
var visTimer;
|
||||
|
||||
function updateJSON() {
|
||||
ajax.open('GET', statusJson, true);
|
||||
ajax.send();
|
||||
}
|
||||
|
||||
function processListenURL(url) {
|
||||
if (!options['replace-url'] || !url) return url;
|
||||
// Add Date.now() to prevent browser caching. Cache-Control isn't reliable IME
|
||||
return url.replace(options['url-replacement'][0], options['url-replacement'][1])+"?"+Date.now();
|
||||
}
|
||||
|
||||
function setSource(source) {
|
||||
if (!source) return;
|
||||
currentStation = source.listenurl.substr(23);
|
||||
window.location.hash = "#"+currentStation;
|
||||
|
||||
if (options['video-support'] && source.server_type.substr(0, source.server_type.indexOf('/')) == 'video') {
|
||||
visMode = -1;
|
||||
} else if (visMode < 0) visMode = 0;
|
||||
|
||||
audioPlayer.src = processListenURL(source.listenurl);
|
||||
audioPlayer.type = source.server_type;
|
||||
|
||||
if (isFancy) {
|
||||
if (!audioContext || !audioAnalyser) {
|
||||
audioContext = new AudioContext();
|
||||
audioAnalyser = createVisualizer(audioPlayer, audioContext);
|
||||
}
|
||||
setVisualizer(visualizer, audioAnalyser, visMode);
|
||||
} else audioPlayer = document.querySelector("video");
|
||||
|
||||
audioPlayer.play();
|
||||
document.body.scrollTop = 0;
|
||||
updateJSON();
|
||||
}
|
||||
|
||||
function handleVolume(event) {
|
||||
event.preventDefault();
|
||||
var volMeter = document.querySelector('.vol');
|
||||
volMeter.classList.remove('hidden');
|
||||
var volInc = 0.05;
|
||||
|
||||
if (event.deltaY < 0) {
|
||||
if (audioPlayer.volume+volInc > 1) audioPlayer.volume=1;
|
||||
else audioPlayer.volume+=volInc;
|
||||
}
|
||||
else if (event.deltaY > 0) {
|
||||
if (audioPlayer.volume-volInc < 0) audioPlayer.volume=0;
|
||||
else audioPlayer.volume-=volInc;
|
||||
}
|
||||
|
||||
volMeter.innerText = "VOL ["+'='.repeat(((audioPlayer.volume / 1) * 20)).padEnd(20, ' ') + "] "+(Math.round(audioPlayer.volume * 100)+'').padStart(3, ' ')+"%";
|
||||
|
||||
window.clearTimeout(volTimer)
|
||||
volTimer = setTimeout(function(){
|
||||
volMeter.classList.add('hidden');
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function switchMode() {
|
||||
if (videoMode) return;
|
||||
if (!isFancy) return;
|
||||
if (visMode == modes.length - 1) // - 1 if we count empty.
|
||||
visMode = 0;
|
||||
else visMode++;
|
||||
|
||||
var visCounter = document.querySelector('.vismode');
|
||||
visCounter.classList.remove('hidden');
|
||||
|
||||
visCounter.innerText = modes[visMode]+' '+visMode+'/'+(modes.length - 1);
|
||||
|
||||
setVisualizer(visualizer, audioAnalyser, visMode);
|
||||
|
||||
window.clearTimeout(visTimer)
|
||||
visTimer = setTimeout(function(){
|
||||
visCounter.classList.add('hidden');
|
||||
}, 5000);
|
||||
|
||||
audioPlayer.play();
|
||||
}
|
||||
|
||||
function newEntry(source, i) {
|
||||
var entryContainer = document.createElement('div');
|
||||
entryContainer.classList.add('station');
|
||||
|
||||
entryContainer.name = source.listenurl.substr(23);
|
||||
|
||||
var titlebar = document.createElement('div'),
|
||||
infoBar = document.createElement('div'),
|
||||
descBox = document.createElement('div');
|
||||
|
||||
titlebar.classList.add('titlebar');
|
||||
infoBar.classList.add('infobar');
|
||||
descBox.classList.add('descbox');
|
||||
|
||||
var titleTxt = document.createElement('b'),
|
||||
listenerBox = document.createElement('div');
|
||||
|
||||
titleTxt.innerText = source.server_name;
|
||||
titlebar.appendChild(titleTxt);
|
||||
|
||||
var stationLineHeight = 24;
|
||||
var grower = document.createElement('div');
|
||||
grower.style.flexGrow = "1";
|
||||
titlebar.appendChild(grower);
|
||||
|
||||
var listenerImg = document.createElement('img'),
|
||||
listenerTxt = document.createElement('a');
|
||||
|
||||
|
||||
listenerImg.src = "users.png";
|
||||
listenerImg.width = listenerImg.height = stationLineHeight;
|
||||
listenerTxt.innerText = source.listeners;
|
||||
|
||||
listenerBox.title = source.listeners+" listener"+(source.listeners != 1 ? 's' : '')+" (peak "+source.listener_peak+")";
|
||||
listenerBox.appendChild(listenerImg);
|
||||
listenerBox.appendChild(listenerTxt);
|
||||
titlebar.appendChild(listenerBox);
|
||||
|
||||
entryContainer.appendChild(titlebar);
|
||||
|
||||
entryContainer.appendChild(document.createElement('hr'));
|
||||
|
||||
var bitrateTxt = document.createElement('a'),
|
||||
urlTxt = document.createElement('a'),
|
||||
urlImg = document.createElement('img'),
|
||||
dirTxt = document.createElement('a'),
|
||||
dirImg = document.createElement('img');
|
||||
|
||||
bitrateTxt.classList.add('bitrate');
|
||||
listenerBox.classList.add('listeners');
|
||||
|
||||
if (mimeIcons[source.server_type]) {
|
||||
var formatImg = document.createElement('img');
|
||||
formatImg.classList.add('format');
|
||||
formatImg.height = stationLineHeight;
|
||||
formatImg.alt = formatImg.title = source.server_type.substr(source.server_type.indexOf('/') + 1);
|
||||
formatImg.src = mimeIcons[source.server_type];
|
||||
infoBar.appendChild(formatImg);
|
||||
} else {
|
||||
var formatTxt = document.createElement('a');
|
||||
formatTxt.classList.add('format');
|
||||
formatTxt.innerText = formatTxt.title = source.server_type.substr(source.server_type.indexOf('/') + 1).toUpperCase();
|
||||
infoBar.appendChild(formatTxt);
|
||||
}
|
||||
|
||||
var btr = source.bitrate;
|
||||
if (!source.bitrate && source.audio_bitrate) btr = source.audio_bitrate / 1000;
|
||||
else if (!source.audio_bitrate && !source.bitrate) btr = "???";
|
||||
bitrateTxt.innerHTML = btr + "<font size='1'>kbps</font>";
|
||||
infoBar.appendChild(bitrateTxt);
|
||||
|
||||
if (source.server_url) {
|
||||
urlTxt.href = source.server_url;
|
||||
urlTxt.target = "_blank";
|
||||
urlImg.src = "url.png";
|
||||
urlImg.height = stationLineHeight;
|
||||
urlTxt.appendChild(urlImg);
|
||||
infoBar.appendChild(urlTxt);
|
||||
}
|
||||
|
||||
dirTxt.href = processListenURL(source.listenurl);
|
||||
dirTxt.target = "_blank";
|
||||
dirImg.src = "dir.png";
|
||||
dirImg.height = stationLineHeight;
|
||||
dirTxt.appendChild(dirImg);
|
||||
infoBar.appendChild(dirTxt);
|
||||
|
||||
entryContainer.appendChild(infoBar);
|
||||
|
||||
var descTxt = document.createElement('p');
|
||||
descTxt.innerHTML = source.server_description + " <i>(Genre: "+source.genre+")</i>";
|
||||
descBox.appendChild(descTxt);
|
||||
|
||||
entryContainer.appendChild(descBox);
|
||||
|
||||
if (source.listenurl.substr(23) == currentStation) entryContainer.classList.add("selected");
|
||||
entryContainer.onclick = function() {
|
||||
setSource(sources[i]);
|
||||
this.classList.add("selected");
|
||||
}
|
||||
|
||||
return entryContainer;
|
||||
}
|
||||
|
||||
function setMaybeOverflow(elem, txt) {
|
||||
if (!txt) return;
|
||||
var limit = options['character-overflow'];
|
||||
if (elem.innerText == txt) return;
|
||||
if (txt.length > limit)
|
||||
elem.innerHTML = "<marquee>"+txt+"</marquee>";
|
||||
else if (txt.length <= limit)
|
||||
elem.innerHTML = txt;
|
||||
|
||||
}
|
||||
|
||||
function updateStreamInfo(source) {
|
||||
var stationName = document.querySelector(".textainer .station"),
|
||||
title = document.querySelector(".textainer .track"),
|
||||
genre = document.querySelector(".textainer .genre");
|
||||
if (source) {
|
||||
setMaybeOverflow(stationName,source.server_name);
|
||||
if (source.artist)
|
||||
setMaybeOverflow(title,source.artist+" - "+source.title);
|
||||
else
|
||||
setMaybeOverflow(title,source.title);
|
||||
setMaybeOverflow(genre,source.genre);
|
||||
} else stationName.innerText = title.innerText = genre.innerText = "";
|
||||
}
|
||||
|
||||
ajax.onload = function() {
|
||||
// Clear Container for new stuff.
|
||||
while(castContainer.childElementCount > 0) {
|
||||
castContainer.removeChild(castContainer.lastChild);
|
||||
}
|
||||
|
||||
if (ajax.status == 200) { // OK
|
||||
if (ajax.response.icestats.source) {
|
||||
if (ajax.response.icestats.source.length > 0) sources = ajax.response.icestats.source;
|
||||
else sources = [ ajax.response.icestats.source ];
|
||||
} else sources = [];
|
||||
var index = -1;
|
||||
for (let i = 0; i < sources.length; i++) {
|
||||
castContainer.appendChild(newEntry(sources[i], i));
|
||||
if (sources[i].listenurl.substr(23) == currentStation) index = i;
|
||||
}
|
||||
if (index != -1) updateStreamInfo(sources[index]);
|
||||
if (audioPlayer.paused && currentStation.length > 0)
|
||||
setSource(sources[index]);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', (event) => {
|
||||
castContainer = document.querySelector(".broadcasts");
|
||||
ajax.responseType = 'json';
|
||||
updateJSON();
|
||||
|
||||
visualizer = document.querySelector('canvas.vis');
|
||||
isFancy = isFancy && (visualizer != null);
|
||||
|
||||
// Detect Visualizer and initialize events
|
||||
if (isFancy) {
|
||||
visualizer.addEventListener('click', (event) => {
|
||||
switchMode();
|
||||
});
|
||||
visualizer.addEventListener('wheel', handleVolume);
|
||||
}
|
||||
|
||||
// #my-stream.ogg -> listenurl: /my-stream.ogg
|
||||
if (window.location.hash.length > 2) currentStation = window.location.hash.substr(1);
|
||||
|
||||
// Handle name and versioning
|
||||
var _nameElem = document.querySelector('.about .name');
|
||||
if (_nameElem) _nameElem.innerText = nameJs;
|
||||
var _versElem = document.querySelector('.about .vers');
|
||||
if (_versElem) _versElem.innerText = 'v'+version;
|
||||
|
||||
setInterval(function() {
|
||||
updateJSON();
|
||||
}, options['json-timer']);
|
||||
|
||||
if (options['crossorigin']) audioPlayer.setAttribute('crossorigin','anonymous');
|
||||
audioPlayer.onended = function () {
|
||||
currentStation = "";
|
||||
console.log("Stream over");
|
||||
updateStreamInfo(null);
|
||||
};
|
||||
});
|
129
style.css
Normal file
129
style.css
Normal file
|
@ -0,0 +1,129 @@
|
|||
:root {
|
||||
--background: #b3a99e;
|
||||
--primary: #f3dca9;
|
||||
--border: #f2e5c8;
|
||||
|
||||
--station-primary: #d0d0d0;
|
||||
--station-border: #a4a4a4;
|
||||
|
||||
--station-selected: #d0dadf;
|
||||
--station-selected-border: #0088ff;
|
||||
|
||||
--about-color: #b9a77f;
|
||||
--about-indent: #000000AA;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
background-repeat: repeat;
|
||||
}
|
||||
|
||||
canvas.vis {
|
||||
border: 2px inset var(--border);
|
||||
background: black;
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
.player {
|
||||
background: var(--primary);
|
||||
border: 2px outset var(--border);
|
||||
width: 384px;
|
||||
}
|
||||
|
||||
.textainer {
|
||||
display: flex;
|
||||
flex-direction:column;
|
||||
line-height: 24px;
|
||||
text-align: left;
|
||||
flex-grow: 1;
|
||||
padding: 2px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
}
|
||||
.textainer .station { font-weight: bold; }
|
||||
.textainer .genre { font-style: italic; display: none; }
|
||||
.textainer > div { white-space: nowrap; }
|
||||
.textainer > div.overflow {
|
||||
-moz-animation: scroll-left 2s linear infinite;
|
||||
-webkit-animation: scroll-left 2s linear infinite;
|
||||
animation: scroll-left 20s linear infinite;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.broadcasts .station {
|
||||
background: var(--station-primary);
|
||||
color: black;
|
||||
border: 1px outset var(--station-border);
|
||||
padding: 4px;
|
||||
margin: 16px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
width: 360px;
|
||||
display: inline-block;
|
||||
vertical-align:top;
|
||||
}
|
||||
|
||||
.broadcasts .station.selected {
|
||||
outline: 2px solid var(--station-selected-border);
|
||||
background: var(--station-selected);
|
||||
}
|
||||
|
||||
.broadcasts .station hr {
|
||||
margin: 2px 0px;
|
||||
border: 1px solid #a4a4a4;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.infobar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.broadcasts .station .listeners img {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.list {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.vol,
|
||||
.vismode {
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
height: 0;
|
||||
text-shadow: 0px 0px 5px black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.vol {
|
||||
top: -32px;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.vismode {
|
||||
top: 8px;
|
||||
text-align: right;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
.vol.hidden,
|
||||
.vismode.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.about {
|
||||
display: flex;
|
||||
margin: -8px 0px 0px 0px;
|
||||
text-shadow: -1px -1px 0px var(--about-indent);
|
||||
padding: 4px 8px;
|
||||
}
|
||||
.about, .about a, .about a:link, .about:visited {color: var(--about-color);text-decoration: none;}
|
Reference in New Issue
Block a user