@ -0,0 +1,8 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM nginx:1.22.0-alpine
|
||||
WORKDIR /var/www/
|
||||
COPY nginx/nginx.conf /etc/nginx/nginx.conf
|
||||
RUN mkdir -p /var/www/website
|
||||
COPY src /var/www/website
|
||||
|
||||
EXPOSE 80
|
||||
@ -0,0 +1,12 @@
|
||||
login:
|
||||
docker login gitea.bjornmossa.net
|
||||
|
||||
start:
|
||||
npx live-server ./src
|
||||
|
||||
build:
|
||||
docker build . -t gitea.bjornmossa.net/radioiceberg/website
|
||||
|
||||
push: login
|
||||
docker push gitea.bjornmossa.net/radioiceberg/website
|
||||
|
||||
@ -0,0 +1,50 @@
|
||||
include /etc/nginx/modules_enabled/*.conf;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
server {
|
||||
listen 80;
|
||||
server_name radioiceberg.net www.radioiceberg.net localhost 127.0.0.1;
|
||||
rewrite ^/(.*) https://radioiceberg.net/$1 permanent;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
include /etc/nginx/mime.types;
|
||||
server_name radioiceberg.net www.radioiceberg.net localhost 127.0.0.1;
|
||||
|
||||
#SSL
|
||||
#ssl_certificate /etc/letsencrypt/live/radioiceberg.net/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/radioiceberg.net/privkey.pem;
|
||||
ssl_trusted_certificate /etc/letsencrypt/live/radioiceberg.net/chain.pem;
|
||||
|
||||
location / {
|
||||
root /var/www/website;
|
||||
index index.html;
|
||||
}
|
||||
|
||||
location /stream {
|
||||
proxy_read_timeout 3000;
|
||||
proxy_connect_timeout 3000;
|
||||
proxy_redirect off;
|
||||
proxy_pass http://icecast:8000/iceberg.ogg;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Server $host;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
}
|
||||
|
||||
location /meta {
|
||||
proxy_pass http://metadata:7000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_connect_timeout 1d;
|
||||
proxy_send_timeout 1d;
|
||||
proxy_read_timeout 1d;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 273 B |
|
After Width: | Height: | Size: 471 B |
|
After Width: | Height: | Size: 15 KiB |
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "Radio Iceberg",
|
||||
"short_name": "Iceberg",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ded8d8",
|
||||
"background_color": "#252525",
|
||||
"display": "standalone"
|
||||
}
|
||||
|
After Width: | Height: | Size: 47 KiB |
@ -0,0 +1,94 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Radio . Iceberg</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta
|
||||
property="og:image"
|
||||
content="http://radioiceberg.net/images/social-preview.jpg"
|
||||
/>
|
||||
<meta
|
||||
property="og:title"
|
||||
content="Звуки севера, пронзающие пустоту космоса."
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Наверху нихуя, внизу дохуя. Независимое интернет-радио."
|
||||
/>
|
||||
<meta property="og:type" content="music.radio_station" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Наверху нихуя, внизу дохуя. Независимое интернет-радио."
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="icons/apple-touch-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="icons/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="icons/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="manifest" href="icons/site.webmanifest" />
|
||||
<link rel="stylesheet" type="text/css" href="styles.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Radio . Iceberg</h1>
|
||||
<button class="play-stream">
|
||||
<svg viewBox="0 0 2000 500">
|
||||
<defs></defs>
|
||||
|
||||
<polygon
|
||||
points="0,0, 2000,250, 2000,250, 0,500"
|
||||
fill="var(--color-primary-1)"
|
||||
>
|
||||
<animate
|
||||
id="play-button-animation"
|
||||
attributeName="points"
|
||||
dur="1.25s"
|
||||
fill="freeze"
|
||||
from="0,0, 2000,250, 2000,250, 0,500"
|
||||
to="0,0, 2000,250, 2000,250, 0,500"
|
||||
restart="always"
|
||||
/>
|
||||
</polygon>
|
||||
</svg>
|
||||
<audio controls preload="none" src="/stream" />
|
||||
</button>
|
||||
<div class="now-playing">
|
||||
<div class="now-playing_title">now playing:</div>
|
||||
<div class="now-playing_artist">We don't know'</div>
|
||||
<div class="now-playing_song">yet...</div>
|
||||
</div>
|
||||
<div class="icebergs">
|
||||
<svg viewBox="0 0 100 100">
|
||||
<polygon
|
||||
id="iceberg1"
|
||||
points="0,0, 100,0, 100,100, 0,100"
|
||||
fill="var(--color-primary-1)"
|
||||
></polygon>
|
||||
<polygon
|
||||
id="iceberg2"
|
||||
points="5,5, 95,5, 95,95, 5,95"
|
||||
fill="var(--color-primary-2)"
|
||||
></polygon>
|
||||
<polygon
|
||||
id="iceberg3"
|
||||
points="10,10, 90,10, 90,90, 10,90"
|
||||
fill="var(--color-complement-1)"
|
||||
></polygon>
|
||||
</svg>
|
||||
</div>
|
||||
<script src="main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,115 @@
|
||||
const STOP_STATE_POINTS = "650,0, 1350,0, 1350,500, 650,500";
|
||||
const PLAY_STATE_POINTS = "0,0, 2000,250, 2000,250, 0,500";
|
||||
|
||||
const getAudio = () => document.querySelector("audio");
|
||||
const getPlayButton = () => document.querySelector(".play-stream");
|
||||
const getPlayButtonAnimation = () =>
|
||||
document.getElementById("play-button-animation");
|
||||
|
||||
const hideAudio = () => {
|
||||
const a = getAudio();
|
||||
a.hidden = true;
|
||||
};
|
||||
|
||||
const playAudio = () => {
|
||||
getAudio()?.play();
|
||||
};
|
||||
|
||||
const pauseAudio = () => {
|
||||
getAudio()?.pause();
|
||||
};
|
||||
|
||||
const setButtonPlayState = () => {
|
||||
const animationNode = getPlayButtonAnimation();
|
||||
animationNode.setAttribute("from", PLAY_STATE_POINTS);
|
||||
animationNode.setAttribute("to", STOP_STATE_POINTS);
|
||||
animationNode.beginElement();
|
||||
};
|
||||
|
||||
const setButtonStopState = () => {
|
||||
const animationNode = getPlayButtonAnimation();
|
||||
animationNode.setAttribute("from", STOP_STATE_POINTS);
|
||||
animationNode.setAttribute("to", PLAY_STATE_POINTS);
|
||||
animationNode.beginElement();
|
||||
};
|
||||
|
||||
const startIcebergsAnimation = () => {
|
||||
[1, 2, 3].map((i) => {
|
||||
const polygon = document.getElementById(`iceberg${i}`);
|
||||
polygon.classList.add("active");
|
||||
});
|
||||
};
|
||||
|
||||
const stopIcebergsAnimation = () => {
|
||||
[1, 2, 3].map((i) => {
|
||||
const polygon = document.getElementById(`iceberg${i}`);
|
||||
polygon.classList.remove("active");
|
||||
});
|
||||
};
|
||||
|
||||
const handleAudioPlay = () => {
|
||||
playAudio();
|
||||
setButtonPlayState();
|
||||
startIcebergsAnimation();
|
||||
};
|
||||
|
||||
const handleAudioStop = () => {
|
||||
pauseAudio();
|
||||
setButtonStopState();
|
||||
stopIcebergsAnimation();
|
||||
};
|
||||
|
||||
const addPlayerInteractions = () => {
|
||||
const playerButton = getPlayButton();
|
||||
const audio = getAudio();
|
||||
|
||||
playerButton.addEventListener("click", () => {
|
||||
audio.paused ? handleAudioPlay() : handleAudioStop();
|
||||
});
|
||||
|
||||
audio.addEventListener("timeupdate", () => {
|
||||
window.requestAnimationFrame(() => {
|
||||
document.documentElement.style.setProperty(
|
||||
"--time",
|
||||
`${audio.currentTime * 100}deg`
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
async function connectToServer() {
|
||||
const ws = new WebSocket(`ws://${location.host}/meta`);
|
||||
ws.addEventListener("message", changeMetadata);
|
||||
}
|
||||
|
||||
const changeMetadata = (metadataEvent) => {
|
||||
const metaObject = JSON.parse(metadataEvent.data);
|
||||
const { artist, title } = metaObject;
|
||||
const artistBlock = document.querySelector(".now-playing_artist");
|
||||
const titleBlock = document.querySelector(".now-playing_song");
|
||||
|
||||
if (artist) {
|
||||
artistBlock.innerHTML = artist;
|
||||
} else {
|
||||
artistBlock.innerHTML = "";
|
||||
}
|
||||
|
||||
if (title) {
|
||||
titleBlock.innerHTML = title;
|
||||
document.title = `${title} on Radioiceberg`;
|
||||
} else {
|
||||
titleBlock.innerHTML = "";
|
||||
}
|
||||
|
||||
if (artist && title) {
|
||||
document.title = `${artist} - ${title} on Radioiceberg`;
|
||||
}
|
||||
};
|
||||
|
||||
const main = () => {
|
||||
hideAudio();
|
||||
addPlayerInteractions();
|
||||
connectToServer();
|
||||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", main);
|
||||
@ -0,0 +1,10 @@
|
||||
const getAudio = () => document.querySelector("audio");
|
||||
const getPlayButton = () => document.querySelector(".play-stream");
|
||||
const getPlayButtonAnimation = () =>
|
||||
document.getElementById("play-button-animation");
|
||||
|
||||
export {
|
||||
getAudio,
|
||||
getPlayButton,
|
||||
getPlayButtonAnimation
|
||||
};
|
||||
@ -0,0 +1,14 @@
|
||||
import { getAudio } from "./dom.js";
|
||||
|
||||
const hideAudio = () => {
|
||||
const a = getAudio();
|
||||
a.hidden = true;
|
||||
};
|
||||
|
||||
const playAudio = () => {
|
||||
getAudio()?.play();
|
||||
};
|
||||
|
||||
const pauseAudio = () => {
|
||||
getAudio()?.pause();
|
||||
};
|
||||
@ -0,0 +1,70 @@
|
||||
@import url("styles/global.css");
|
||||
@import url("styles/system.css");
|
||||
|
||||
body {
|
||||
background-color: var(--color-primary-0);
|
||||
color: var(--color-primary-1);
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
line-height: 1.75;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5 {
|
||||
margin: 3rem 0 1.38rem;
|
||||
font-family: "Lato", sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
font-size: 5.063rem;
|
||||
}
|
||||
|
||||
.play-stream {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
width: 300px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.now-playing {
|
||||
font-family: "Lato Light Italic";
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.icebergs {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
rotate: -45deg;
|
||||
translate: 10% 50%;
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
#iceberg1,
|
||||
#iceberg2,
|
||||
#iceberg3 {
|
||||
transition: transform 2s ease-in-out;
|
||||
transform: translate(0);
|
||||
}
|
||||
|
||||
#iceberg1.active {
|
||||
transform: translate(0, calc(100px * sin(var(--time))));
|
||||
}
|
||||
|
||||
#iceberg2.active {
|
||||
transform: translate(0, calc(50px * cos(var(--time))));
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
html {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
/* Typography */
|
||||
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
src: url("fonts/lato-regular-webfont.woff2") format("woff2"),
|
||||
url("fonts/lato-regular-webfont.woff") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Lato Light Italic";
|
||||
src: url("fonts/lato-lightitalic.woff2") format("woff2"),
|
||||
url("fonts/lato-lightitalic.woff") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* Colors */
|
||||
|
||||
:root {
|
||||
--color-primary-0: #252525;
|
||||
--color-primary-1: #ded8d8;
|
||||
--color-primary-2: #7d7d7d;
|
||||
--color-primary-3: #010101;
|
||||
--color-primary-4: #0e0404;
|
||||
|
||||
--color-complement-0: #1e1e1e;
|
||||
--color-complement-1: #adb2ad;
|
||||
--color-complement-2: #646464;
|
||||
--color-complement-3: #010101;
|
||||
--color-complement-4: #030b03;
|
||||
|
||||
--time: 0;
|
||||
--lol: calc(100px * sin(var(--time)));
|
||||
}
|
||||