@ -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)));
|
||||||
|
}
|
||||||