Add song player with multiple songs and options page.

This commit is contained in:
天クマ 2026-02-08 19:32:57 -03:00
commit dcc31cc85a
40 changed files with 940 additions and 70 deletions

View file

@ -46,7 +46,11 @@ module.exports = {
eyeofnemesisProjectDesctiption: "Eye of Nemesis is a plugin that allows server admins to write policies that will deny or allow (black/whitelist) players to do specific things based on the value of nodes.",
jamfishProjectDesctiption: "Native music player for Android devices that connects to Jellyfin media servers. The code is based on Gelli's archived repository, which is based on an old version of Phonograph.",
pestoProjectDesctiption: "Multi-platform customizable client for wikis written in Python using PySide6 (QT).",
itemeconomyProjectDesctiption: "This PaperMC plugin integrates with VaultUnlocked to provide a unique, item-based economy system for your Minecraft server. Instead of relying solely on virtual balances, players use in-game items as physical currency, adding a layer of immersion and realism to your economy."
itemeconomyProjectDesctiption: "This PaperMC plugin integrates with VaultUnlocked to provide a unique, item-based economy system for your Minecraft server. Instead of relying solely on virtual balances, players use in-game items as physical currency, adding a layer of immersion and realism to your economy.",
by: "by",
back: "back",
hideBackground: "Hide background",
options: "Options"
},
pt: {
home: "início",
@ -80,6 +84,10 @@ module.exports = {
eyeofnemesisProjectDesctiption: "Eye of Nemesis é um plugin que permite aos administradores de servidores escrever políticas que negarão ou permitirão (lista negra/branca) que os jogadores façam coisas específicas com base no valor dos nós.",
jamfishProjectDesctiption: "Reprodutor de música nativo para dispositivos Android que se conecta a servidores de mídia Jellyfin. O código é baseado no repositório arquivado do Gelli, que por sua vez se baseia em uma versão antiga do Phonograph.",
pestoProjectDesctiption: "Cliente personalizável multiplataforma para wikis escrito em Python usando PySide6 (QT).",
itemeconomyProjectDesctiption: "Este plugin PaperMC integra-se ao VaultUnlocked para fornecer um sistema de economia único baseado em itens para o seu servidor Minecraft. Em vez de depender apenas de saldos virtuais, os jogadores usam itens do jogo como moeda física, adicionando uma camada de imersão e realismo à sua economia."
itemeconomyProjectDesctiption: "Este plugin PaperMC integra-se ao VaultUnlocked para fornecer um sistema de economia único baseado em itens para o seu servidor Minecraft. Em vez de depender apenas de saldos virtuais, os jogadores usam itens do jogo como moeda física, adicionando uma camada de imersão e realismo à sua economia.",
by: "por",
back: "voltar",
hideBackground: "Esconder imagem de fundo",
options: "Options"
}
};

View file

@ -2,9 +2,19 @@
<div>
<h1>{{ title or "Adrian Victor" }}</h1>
<a id="headerSubtitle"><i>{{ subtitle or "Fanasy is not a crime, find your castle in the sky." }}</i></a>
<script>
const headeri18n =
{
by: "{{ i18n[langKey].by | safe }}",
options: "{{ i18n[langKey].options | safe }}",
hideBackground: "{{ i18n[langKey].hideBackground | safe }}",
back: "{{ i18n[langKey].back | safe }}"
}
</script>
</div>
<div id="linksHelper">
<div id="soundDiv" data-title="tenkuma - Velkommen" data-source="Velkommen.mp3"></div>
<div id="music">
</div>
<ul id="headerLinks">
<a href="/{{ langKey }}/">{{ i18n[langKey].home }}</a>
<a href="/{{ langKey }}/blog/">blog</a>

View file

@ -24,9 +24,19 @@
<div>
<h1>Adrian Victor:Blog</h1>
<a id="headerSubtitle"><i>Fanasy is not a crime, find your castle in the sky.</i></a>
<script>
const headeri18n =
{
by: "by",
options: "Options",
hideBackground: "Hide background",
back: "back"
}
</script>
</div>
<div id="linksHelper">
<div id="soundDiv" data-title="tenkuma - Velkommen" data-source="Velkommen.mp3"></div>
<div id="music">
</div>
<ul id="headerLinks">
<a href="/en/">home</a>
<a href="/en/blog/">blog</a>

View file

@ -24,9 +24,19 @@
<div>
<h1>Adrian Victor</h1>
<a id="headerSubtitle"><i>Fanasy is not a crime, find your castle in the sky.</i></a>
<script>
const headeri18n =
{
by: "by",
options: "Options",
hideBackground: "Hide background",
back: "back"
}
</script>
</div>
<div id="linksHelper">
<div id="soundDiv" data-title="tenkuma - Velkommen" data-source="Velkommen.mp3"></div>
<div id="music">
</div>
<ul id="headerLinks">
<a href="/en/">home</a>
<a href="/en/blog/">blog</a>

View file

@ -24,9 +24,19 @@
<div>
<h1>Adrian Victor</h1>
<a id="headerSubtitle"><i>Fanasy is not a crime, find your castle in the sky.</i></a>
<script>
const headeri18n =
{
by: "by",
options: "Options",
hideBackground: "Hide background",
back: "back"
}
</script>
</div>
<div id="linksHelper">
<div id="soundDiv" data-title="tenkuma - Velkommen" data-source="Velkommen.mp3"></div>
<div id="music">
</div>
<ul id="headerLinks">
<a href="/en/">home</a>
<a href="/en/blog/">blog</a>

View file

@ -24,9 +24,19 @@
<div>
<h1>Adrian Victor:Blog</h1>
<a id="headerSubtitle"><i>Fanasy is not a crime, find your castle in the sky.</i></a>
<script>
const headeri18n =
{
by: "by",
options: "Options",
hideBackground: "Hide background",
back: "back"
}
</script>
</div>
<div id="linksHelper">
<div id="soundDiv" data-title="tenkuma - Velkommen" data-source="Velkommen.mp3"></div>
<div id="music">
</div>
<ul id="headerLinks">
<a href="/en/">home</a>
<a href="/en/blog/">blog</a>

View file

@ -24,9 +24,19 @@
<div>
<h1>Adrian Victor:Blog</h1>
<a id="headerSubtitle"><i>Fanasy is not a crime, find your castle in the sky.</i></a>
<script>
const headeri18n =
{
by: "by",
options: "Options",
hideBackground: "Hide background",
back: "back"
}
</script>
</div>
<div id="linksHelper">
<div id="soundDiv" data-title="tenkuma - Velkommen" data-source="Velkommen.mp3"></div>
<div id="music">
</div>
<ul id="headerLinks">
<a href="/en/">home</a>
<a href="/en/blog/">blog</a>

View file

@ -24,9 +24,19 @@
<div>
<h1>Adrian Victor:Blog</h1>
<a id="headerSubtitle"><i>Fanasy is not a crime, find your castle in the sky.</i></a>
<script>
const headeri18n =
{
by: "por",
options: "Opções",
hideBackground: "Esconder imagem de fundo",
back: "voltar"
}
</script>
</div>
<div id="linksHelper">
<div id="soundDiv" data-title="tenkuma - Velkommen" data-source="Velkommen.mp3"></div>
<div id="music">
</div>
<ul id="headerLinks">
<a href="/pt/">início</a>
<a href="/pt/blog/">blog</a>

View file

@ -24,9 +24,19 @@
<div>
<h1>Adrian Victor:Blog</h1>
<a id="headerSubtitle"><i>Fanasy is not a crime, find your castle in the sky.</i></a>
<script>
const headeri18n =
{
by: "por",
options: "Opções",
hideBackground: "Esconder imagem de fundo",
back: "voltar"
}
</script>
</div>
<div id="linksHelper">
<div id="soundDiv" data-title="tenkuma - Velkommen" data-source="Velkommen.mp3"></div>
<div id="music">
</div>
<ul id="headerLinks">
<a href="/pt/">início</a>
<a href="/pt/blog/">blog</a>

View file

@ -24,9 +24,19 @@
<div>
<h1>Adrian Victor:Blog</h1>
<a id="headerSubtitle"><i>Fanasy is not a crime, find your castle in the sky.</i></a>
<script>
const headeri18n =
{
by: "por",
options: "Opções",
hideBackground: "Esconder imagem de fundo",
back: "voltar"
}
</script>
</div>
<div id="linksHelper">
<div id="soundDiv" data-title="tenkuma - Velkommen" data-source="Velkommen.mp3"></div>
<div id="music">
</div>
<ul id="headerLinks">
<a href="/pt/">início</a>
<a href="/pt/blog/">blog</a>

View file

@ -24,9 +24,19 @@
<div>
<h1>Adrian Victor</h1>
<a id="headerSubtitle"><i>Fanasy is not a crime, find your castle in the sky.</i></a>
<script>
const headeri18n =
{
by: "por",
options: "Opções",
hideBackground: "Esconder imagem de fundo",
back: "voltar"
}
</script>
</div>
<div id="linksHelper">
<div id="soundDiv" data-title="tenkuma - Velkommen" data-source="Velkommen.mp3"></div>
<div id="music">
</div>
<ul id="headerLinks">
<a href="/pt/">início</a>
<a href="/pt/blog/">blog</a>

View file

@ -24,9 +24,19 @@
<div>
<h1>Adrian Victor:Trabalhos</h1>
<a id="headerSubtitle"><i>Fanasy is not a crime, find your castle in the sky.</i></a>
<script>
const headeri18n =
{
by: "por",
options: "Opções",
hideBackground: "Esconder imagem de fundo",
back: "voltar"
}
</script>
</div>
<div id="linksHelper">
<div id="soundDiv" data-title="tenkuma - Velkommen" data-source="Velkommen.mp3"></div>
<div id="music">
</div>
<ul id="headerLinks">
<a href="/pt/">início</a>
<a href="/pt/blog/">blog</a>

View file

@ -24,9 +24,19 @@
<div>
<h1>Adrian Victor:Escola</h1>
<a id="headerSubtitle"><i>Fanasy is not a crime, find your castle in the sky.</i></a>
<script>
const headeri18n =
{
by: "por",
options: "Opções",
hideBackground: "Esconder imagem de fundo",
back: "voltar"
}
</script>
</div>
<div id="linksHelper">
<div id="soundDiv" data-title="tenkuma - Velkommen" data-source="Velkommen.mp3"></div>
<div id="music">
</div>
<ul id="headerLinks">
<a href="/pt/">início</a>
<a href="/pt/blog/">blog</a>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

36
docs/static/images/gears.svg vendored Normal file
View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="800px" height="800px" viewBox="0 0 30.998 30.998"
xml:space="preserve">
<g>
<g>
<path d="M18.73,16.686l-1.713-0.205c-0.176-0.654-0.433-1.271-0.763-1.844l1.063-1.354c0.164-0.209,0.145-0.531-0.043-0.718
l-1.766-1.767c-0.187-0.187-0.509-0.206-0.717-0.044l-1.356,1.067c-0.571-0.33-1.188-0.587-1.841-0.761L11.39,9.345
c-0.031-0.262-0.273-0.477-0.537-0.477H8.354c-0.264,0-0.506,0.215-0.537,0.477l-0.206,1.714
c-0.653,0.174-1.271,0.432-1.842,0.762l-1.357-1.065c-0.207-0.163-0.53-0.145-0.716,0.042l-1.767,1.769
c-0.187,0.187-0.206,0.509-0.043,0.717l1.065,1.354c-0.331,0.572-0.586,1.19-0.761,1.844l-1.713,0.205
C0.215,16.718,0,16.959,0,17.225v2.498c0,0.265,0.215,0.506,0.477,0.536l1.714,0.207c0.175,0.651,0.431,1.269,0.761,1.841
l-1.064,1.354c-0.163,0.21-0.144,0.532,0.043,0.719l1.765,1.767c0.186,0.188,0.509,0.207,0.716,0.045l1.357-1.068
c0.571,0.33,1.189,0.588,1.842,0.762L7.817,27.6c0.031,0.262,0.273,0.477,0.537,0.477h2.499c0.264,0,0.506-0.215,0.537-0.477
l0.206-1.715c0.653-0.174,1.271-0.432,1.843-0.762l1.356,1.064c0.208,0.162,0.53,0.145,0.716-0.041l1.767-1.77
c0.187-0.188,0.207-0.51,0.043-0.717l-1.065-1.354c0.331-0.572,0.586-1.19,0.761-1.844l1.715-0.205
c0.263-0.031,0.477-0.271,0.477-0.537v-2.498C19.209,16.957,18.994,16.718,18.73,16.686z M9.605,23.271
c-2.651,0-4.801-2.148-4.801-4.801c0-2.652,2.15-4.802,4.801-4.802c2.652,0,4.801,2.149,4.801,4.802
C14.407,21.123,12.257,23.271,9.605,23.271z"/>
<path d="M30.641,8.804L29.35,8.651c-0.132-0.492-0.324-0.959-0.574-1.39l0.803-1.02c0.123-0.155,0.107-0.399-0.033-0.54
l-1.33-1.329c-0.14-0.142-0.383-0.156-0.54-0.033l-1.021,0.803c-0.43-0.249-0.896-0.441-1.385-0.571l-0.156-1.29
c-0.022-0.198-0.205-0.359-0.402-0.359H22.83c-0.199,0-0.381,0.161-0.404,0.359l-0.154,1.29c-0.492,0.13-0.957,0.323-1.388,0.572
l-1.021-0.802c-0.156-0.122-0.399-0.107-0.539,0.031l-1.331,1.332c-0.142,0.141-0.155,0.383-0.032,0.539l0.803,1.021
c-0.25,0.43-0.441,0.896-0.574,1.388L16.9,8.806c-0.198,0.023-0.359,0.206-0.359,0.405v1.881c0,0.197,0.162,0.381,0.359,0.402
l1.289,0.157c0.133,0.49,0.326,0.955,0.574,1.386l-0.803,1.02c-0.122,0.156-0.107,0.4,0.033,0.54l1.328,1.329
c0.141,0.143,0.383,0.156,0.539,0.033l1.021-0.804c0.43,0.249,0.896,0.442,1.387,0.572l0.155,1.292
c0.022,0.195,0.206,0.356,0.404,0.356h1.881c0.198,0,0.38-0.161,0.403-0.356l0.154-1.292c0.492-0.13,0.957-0.323,1.387-0.572
l1.021,0.802c0.157,0.123,0.399,0.109,0.54-0.031l1.33-1.332c0.141-0.139,0.155-0.382,0.032-0.538l-0.802-1.021
c0.25-0.429,0.44-0.895,0.572-1.386l1.291-0.154c0.198-0.025,0.359-0.205,0.359-0.405V9.21
C30.999,9.009,30.837,8.829,30.641,8.804z M23.771,13.764c-1.998,0-3.615-1.618-3.615-3.614c0-1.997,1.619-3.614,3.615-3.614
c1.994,0,3.613,1.617,3.613,3.614C27.384,12.145,25.766,13.764,23.771,13.764z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

BIN
docs/static/images/songs/pg.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
docs/static/images/songs/velkommen.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

BIN
docs/static/images/songs/winds.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

201
docs/static/main.css vendored
View file

@ -31,6 +31,22 @@ li {
list-style-type: none;
}
select {
background-color: transparent;
color: white;
border: none;
font-size: 1em;
}
#music {
padding: 0;
display: flex;
gap: .4em;
inline-size: fit-content;
height: 1.4em;
margin-top: auto;
}
header {
display: flex;
border-bottom: thick solid rgba(255, 255, 255, 0.1);
@ -64,6 +80,7 @@ header div {
mask-repeat: no-repeat;
mask-size: 100% 100%;
user-select: none;
transition: .4s;
}
header ul {
@ -96,7 +113,7 @@ header ul:hover {
}
main {
margin-bottom: 4em;
margin-bottom: 2em;
}
h1, h2, h3 {
@ -143,7 +160,6 @@ textarea, input, button {
}
#sound {
height: 1.5em;
filter: invert();
}
@ -153,20 +169,9 @@ textarea, input, button {
#linksHelper {
margin: auto 1em auto auto;
padding-top: 0;
}
#soundDiv {
margin-left: auto;
inline-size: fit-content;
padding: 0;
padding-top: 1rem;
display: flex;
gap: 10px;
}
#soundDiv p {
margin: 0 auto 0 auto;
flex-direction: column;
gap: .2em;
}
#headerLinks {
@ -456,6 +461,163 @@ div.hs.selected {
-webkit-box-orient: vertical
}
aside.metromenu {
z-index: 2;
position: fixed;
top: 0;
right: 0;
width: 30vw;
background-color: black;
height: 100vh;
transition: .2s;
transition-timing-function: cubic-bezier(0.1, 0.2, 0.3, 0.955);
padding: 2em;
}
aside.metromenu.closed {
transform: translateX(100%);
}
aside.metromenu h2 {
font-size: xx-large;
}
aside.metromenu p {
margin-bottom: .4em;
}
aside.metromenu .optionsToggle {
margin-bottom: 1em;
}
.optionsToggle {
cursor: pointer;
}
aside.metromenu .optionsToggle {
opacity: .6;
width: fit-content;
}
aside.metromenu #content {
display: flex;
flex-direction: column;
gap: 1em;
}
.checkbox {
display: flex;
}
.checkbox p {
flex-grow: 1;
}
input[type="checkbox"] {
border: thick solid white;
}
input[type="range"] {
width: 100%;
border: none;
padding: 0;
}
input[type="range"]::-webkit-slider-thumb, input[type="range"]::-moz-range-thumb {
background-color: black;
transition: .2s;
border-radius: 0;
border: medium solid white;
height: 1.2em;
}
input[type="range"]:hover::-webkit-slider-thumb, input[type="range"]:hover::-moz-range-thumb {
height: 2em;
background-color: white;
border-width: thin;
}
input[type="range"]::-webkit-slider-runnable-track, input[type="range"]::-moz-range-track {
background-color: white;
height: 1em;
}
#songDrawer {
display: flex;
height: 10em;
overflow-x: auto;
overflow-y: hidden;
gap: .2em;
}
.drawerSong {
position: relative;
overflow: hidden;
}
.drawerSong img {
transition: .4s;
width: 5em;
height: 100%;
object-fit: cover;
opacity: .4;
filter: grayscale(1);
}
.drawerSong p {
position: absolute;
top: 0;
left: 0;
display: inline;
}
.drawerSong:hover img, .drawerSong.selected img {
opacity: 1;
overflow: hidden;
width: 10em;
}
.drawerSong.selected img {
filter: none;
}
.playlistTitle {
background-color: white;
color: black;
}
#playlist {
transition: .2s;
max-height: 10em;
overflow: auto;
border: medium solid white;
}
#playlist p {
cursor: pointer;
padding: 0 .4em;
}
#playlist p:first-child {
padding-bottom: .2em;
}
.playingSong {
font-size: larger;
}
.hidden {
display: none;
}
.invisible {
opacity: 0;
}
.bg.invisible {
opacity: 0!important;
}
@keyframes ellipsis-loader {
0%, 25% {
transform: translateX(0);
@ -508,6 +670,7 @@ div.hs.selected {
#headerLinks {
text-align: center;
width: 100%;
}
#homeTitle {
@ -571,12 +734,20 @@ div.hs.selected {
.hsProjectHeaderIcon {
margin: auto;
}
aside.metromenu {
width: 50%;
}
}
@media screen and (max-width: 800px) {
#homeSquares {
width: 60vw;
}
aside.metromenu {
width: 100%;
}
}
@media screen and (max-width: 720px) {

BIN
docs/static/music/PG2.mp3 vendored Normal file

Binary file not shown.

BIN
docs/static/music/dreamscape.mp3 vendored Normal file

Binary file not shown.

BIN
docs/static/music/skychat.mp3 vendored Normal file

Binary file not shown.

View file

@ -1,25 +1,156 @@
const toggle = document.querySelector('#soundDiv')
toggle.innerHTML = `<img src="/static/images/sound-on.png" id="sound" onclick="toggleAudio()"><p>${toggle.getAttribute("data-title")}</p>`
// This script handles the playback of music in the header's miniplayer ;)
const body = document.querySelector("body");
document.getElementById("music").innerHTML = `
<img src="/static/images/gears.svg" class="optionsToggle invertedc">
<img src="/static/images/sound-on.png" id="sound">
<select name="song" id="songSelection"></select>
`
const songs = [
{ file: "Velkommen.mp3", name: 'Velkommen', artwork: "velkommen.jpg" },
{ file: "PG2.mp3", name: 'Frugal APE', artwork: "pg.jpg" },
{ file: "dreamscape.mp3", name: 'Dreamscape', artwork: "winds.png" },
{ file: "skychat.mp3", name: 'Skychat', artwork: "winds.png" }
];
// Options page
const optionsAside = document.createElement("aside");
optionsAside.classList.add("closed");
optionsAside.classList.add("metromenu");
{
const back = document.createElement("p");
back.textContent = headeri18n.back;
back.classList.add("optionsToggle");
const title = document.createElement("h2");
title.textContent = headeri18n.options;
optionsAside.appendChild(title);
optionsAside.appendChild(back);
const content = document.createElement("div");
content.innerHTML = `
<div id="content">
<div id="songDrawer"></div>
<div>
<p class="playingSong"></p>
<p>${headeri18n.by} tenkuma</p>
</div>
<div id="playlist"></div>
<div>
<p>Volume</p>
<input id="volume" oninput="setVolume(this.value / 100)" type="range" min="0" max="100"></input>
</div>
<div class="checkbox">
<p>${headeri18n.hideBackground}</p>
<input id="background" type="checkbox"></input>
</div>
</div>
`
optionsAside.appendChild(content);
}
body.appendChild(optionsAside);
const audio = new Audio(`/static/${toggle.getAttribute("data-source")}`);
const toggleIMG = document.querySelector('#sound');
toggleIMG.addEventListener('click', () => {
toggleAudio();
})
const savedTime = localStorage.getItem("audioTime");
const wasPlaying = localStorage.getItem("audioPlaying") === 'true'
const hideBG = document.querySelector("input#background");
if (localStorage.getItem("bgHidden") === "true") hideBG.checked = true, toggleBG();
hideBG.addEventListener("click", () => {
toggleBG();
})
if (savedTime) audio.currentTime = parseFloat(savedTime);
if (wasPlaying) {
play();
} else {
stop();
function toggleBG() {
const bg = document.querySelector(".bg");
bg.classList.toggle("invisible");
localStorage.setItem("bgHidden", bg.classList.contains("invisible"))
}
const songsDrawer = document.querySelector("#songDrawer");
const drawerSongs = [];
const playlist = document.querySelector("#playlist");
const expandButton = document.createElement('p');
expandButton.textContent = "Playlist";
expandButton.classList.add("playlistTitle");
playlist.appendChild(expandButton);
songs.forEach(song => {
const songElement = document.createElement("div");
songElement.classList.add("drawerSong");
songElement.dataset.song = song.file;
const songImage = document.createElement("img");
songImage.src = `/static/images/songs/${song.artwork}`;
songElement.appendChild(songImage);
songElement.addEventListener('click', () => {
changeSong(song.file);
});
drawerSongs.push(songElement);
songsDrawer.appendChild(songElement);
// Playlist
const playlistEntry = document.createElement("p");
playlistEntry.textContent = song.name;
playlistEntry.addEventListener('click', () => {
changeSong(song.file);
})
playlist.appendChild(playlistEntry);
})
const audioSelect = document.getElementById("songSelection");
songs.forEach(song => {
const songOption = document.createElement("option");
songOption.value = song.file;
songOption.textContent = song.name;
audioSelect.appendChild(songOption);
});
const playingSongLabel = document.querySelector(".playingSong");
function updatePlayingLabel(label) {
drawerSongs.forEach(sng => {
sng.classList.remove("selected");
if (sng.dataset.song == label) {
sng.classList.add("selected");
}
});
const songString = songs.find(item => item.file === label).name;
playingSongLabel.textContent = songString;
}
const savedSong = localStorage.getItem("song");
if (savedSong) {
audioSelect.value = savedSong;
updatePlayingLabel(savedSong);
} else {
audioSelect.value = songs[0].file;
updatePlayingLabel(songs[0].file);
}
const optionsButton = document.querySelectorAll(".optionsToggle");
optionsButton.forEach(button => {
button.addEventListener('click', () => {
optionsAside.classList.toggle('closed');
});
});
// Create the audio object using the current select value
let audio = new Audio(`/static/music/${audioSelect.value}`);
const savedTime = localStorage.getItem("audioTime");
const savedVolume = localStorage.getItem("volume");
const wasPlaying = localStorage.getItem("audioPlaying") === 'true';
function play() {
audio.volume = 0.8;
audio.volume = localStorage.getItem("volume");
audio.play();
localStorage.setItem("audioPlaying", "true")
toggleIMG.src = "/static/images/sound-on.png"
console.log(`[Music Player] playing ${audioSelect.value}`)
}
function stop() {
@ -28,6 +159,11 @@ function stop() {
toggleIMG.src = "/static/images/sound-off.png"
}
function setVolume(volume) {
audio.volume = volume;
localStorage.setItem("volume", volume);
}
function toggleAudio() {
if (!audio.paused) {
stop();
@ -37,6 +173,34 @@ function toggleAudio() {
}
window.addEventListener("beforeunload", () => {
localStorage.setItem("audioTime", audio.currentTime);
localStorage.setItem("audioPlaying", !audio.paused);
});
localStorage.setItem("audioTime", audio.currentTime);
localStorage.setItem("audioPlaying", !audio.paused);
});
function changeSong(song) {
const wasPlaying = !audio.paused;
stop();
localStorage.removeItem("audioTime");
audio = new Audio(`/static/music/${song}`);
if (savedVolume) setVolume(savedVolume);
console.log(`[Music Player] changing song to ${song}`)
localStorage.setItem("song", song);
updatePlayingLabel(song);
if (wasPlaying) play();
}
// hooking into the options menu 'change' event to update the song
audioSelect.addEventListener('change', () => {
changeSong(audioSelect.value);
})
// Set initial playback state and volume based on saved preferences
if (savedTime) audio.currentTime = parseFloat(savedTime);
if (savedVolume) document.getElementById("volume").value = savedVolume * 100;
if (wasPlaying) {
play();
} else {
stop();
}

Binary file not shown.

36
static/images/gears.svg Normal file
View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="800px" height="800px" viewBox="0 0 30.998 30.998"
xml:space="preserve">
<g>
<g>
<path d="M18.73,16.686l-1.713-0.205c-0.176-0.654-0.433-1.271-0.763-1.844l1.063-1.354c0.164-0.209,0.145-0.531-0.043-0.718
l-1.766-1.767c-0.187-0.187-0.509-0.206-0.717-0.044l-1.356,1.067c-0.571-0.33-1.188-0.587-1.841-0.761L11.39,9.345
c-0.031-0.262-0.273-0.477-0.537-0.477H8.354c-0.264,0-0.506,0.215-0.537,0.477l-0.206,1.714
c-0.653,0.174-1.271,0.432-1.842,0.762l-1.357-1.065c-0.207-0.163-0.53-0.145-0.716,0.042l-1.767,1.769
c-0.187,0.187-0.206,0.509-0.043,0.717l1.065,1.354c-0.331,0.572-0.586,1.19-0.761,1.844l-1.713,0.205
C0.215,16.718,0,16.959,0,17.225v2.498c0,0.265,0.215,0.506,0.477,0.536l1.714,0.207c0.175,0.651,0.431,1.269,0.761,1.841
l-1.064,1.354c-0.163,0.21-0.144,0.532,0.043,0.719l1.765,1.767c0.186,0.188,0.509,0.207,0.716,0.045l1.357-1.068
c0.571,0.33,1.189,0.588,1.842,0.762L7.817,27.6c0.031,0.262,0.273,0.477,0.537,0.477h2.499c0.264,0,0.506-0.215,0.537-0.477
l0.206-1.715c0.653-0.174,1.271-0.432,1.843-0.762l1.356,1.064c0.208,0.162,0.53,0.145,0.716-0.041l1.767-1.77
c0.187-0.188,0.207-0.51,0.043-0.717l-1.065-1.354c0.331-0.572,0.586-1.19,0.761-1.844l1.715-0.205
c0.263-0.031,0.477-0.271,0.477-0.537v-2.498C19.209,16.957,18.994,16.718,18.73,16.686z M9.605,23.271
c-2.651,0-4.801-2.148-4.801-4.801c0-2.652,2.15-4.802,4.801-4.802c2.652,0,4.801,2.149,4.801,4.802
C14.407,21.123,12.257,23.271,9.605,23.271z"/>
<path d="M30.641,8.804L29.35,8.651c-0.132-0.492-0.324-0.959-0.574-1.39l0.803-1.02c0.123-0.155,0.107-0.399-0.033-0.54
l-1.33-1.329c-0.14-0.142-0.383-0.156-0.54-0.033l-1.021,0.803c-0.43-0.249-0.896-0.441-1.385-0.571l-0.156-1.29
c-0.022-0.198-0.205-0.359-0.402-0.359H22.83c-0.199,0-0.381,0.161-0.404,0.359l-0.154,1.29c-0.492,0.13-0.957,0.323-1.388,0.572
l-1.021-0.802c-0.156-0.122-0.399-0.107-0.539,0.031l-1.331,1.332c-0.142,0.141-0.155,0.383-0.032,0.539l0.803,1.021
c-0.25,0.43-0.441,0.896-0.574,1.388L16.9,8.806c-0.198,0.023-0.359,0.206-0.359,0.405v1.881c0,0.197,0.162,0.381,0.359,0.402
l1.289,0.157c0.133,0.49,0.326,0.955,0.574,1.386l-0.803,1.02c-0.122,0.156-0.107,0.4,0.033,0.54l1.328,1.329
c0.141,0.143,0.383,0.156,0.539,0.033l1.021-0.804c0.43,0.249,0.896,0.442,1.387,0.572l0.155,1.292
c0.022,0.195,0.206,0.356,0.404,0.356h1.881c0.198,0,0.38-0.161,0.403-0.356l0.154-1.292c0.492-0.13,0.957-0.323,1.387-0.572
l1.021,0.802c0.157,0.123,0.399,0.109,0.54-0.031l1.33-1.332c0.141-0.139,0.155-0.382,0.032-0.538l-0.802-1.021
c0.25-0.429,0.44-0.895,0.572-1.386l1.291-0.154c0.198-0.025,0.359-0.205,0.359-0.405V9.21
C30.999,9.009,30.837,8.829,30.641,8.804z M23.771,13.764c-1.998,0-3.615-1.618-3.615-3.614c0-1.997,1.619-3.614,3.615-3.614
c1.994,0,3.613,1.617,3.613,3.614C27.384,12.145,25.766,13.764,23.771,13.764z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
static/images/songs/pg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 KiB

View file

@ -31,6 +31,22 @@ li {
list-style-type: none;
}
select {
background-color: transparent;
color: white;
border: none;
font-size: 1em;
}
#music {
padding: 0;
display: flex;
gap: .4em;
inline-size: fit-content;
height: 1.4em;
margin-top: auto;
}
header {
display: flex;
border-bottom: thick solid rgba(255, 255, 255, 0.1);
@ -64,6 +80,7 @@ header div {
mask-repeat: no-repeat;
mask-size: 100% 100%;
user-select: none;
transition: .4s;
}
header ul {
@ -96,7 +113,7 @@ header ul:hover {
}
main {
margin-bottom: 4em;
margin-bottom: 2em;
}
h1, h2, h3 {
@ -143,7 +160,6 @@ textarea, input, button {
}
#sound {
height: 1.5em;
filter: invert();
}
@ -153,20 +169,9 @@ textarea, input, button {
#linksHelper {
margin: auto 1em auto auto;
padding-top: 0;
}
#soundDiv {
margin-left: auto;
inline-size: fit-content;
padding: 0;
padding-top: 1rem;
display: flex;
gap: 10px;
}
#soundDiv p {
margin: 0 auto 0 auto;
flex-direction: column;
gap: .2em;
}
#headerLinks {
@ -456,6 +461,163 @@ div.hs.selected {
-webkit-box-orient: vertical
}
aside.metromenu {
z-index: 2;
position: fixed;
top: 0;
right: 0;
width: 30vw;
background-color: black;
height: 100vh;
transition: .2s;
transition-timing-function: cubic-bezier(0.1, 0.2, 0.3, 0.955);
padding: 2em;
}
aside.metromenu.closed {
transform: translateX(100%);
}
aside.metromenu h2 {
font-size: xx-large;
}
aside.metromenu p {
margin-bottom: .4em;
}
aside.metromenu .optionsToggle {
margin-bottom: 1em;
}
.optionsToggle {
cursor: pointer;
}
aside.metromenu .optionsToggle {
opacity: .6;
width: fit-content;
}
aside.metromenu #content {
display: flex;
flex-direction: column;
gap: 1em;
}
.checkbox {
display: flex;
}
.checkbox p {
flex-grow: 1;
}
input[type="checkbox"] {
border: thick solid white;
}
input[type="range"] {
width: 100%;
border: none;
padding: 0;
}
input[type="range"]::-webkit-slider-thumb, input[type="range"]::-moz-range-thumb {
background-color: black;
transition: .2s;
border-radius: 0;
border: medium solid white;
height: 1.2em;
}
input[type="range"]:hover::-webkit-slider-thumb, input[type="range"]:hover::-moz-range-thumb {
height: 2em;
background-color: white;
border-width: thin;
}
input[type="range"]::-webkit-slider-runnable-track, input[type="range"]::-moz-range-track {
background-color: white;
height: 1em;
}
#songDrawer {
display: flex;
height: 10em;
overflow-x: auto;
overflow-y: hidden;
gap: .2em;
}
.drawerSong {
position: relative;
overflow: hidden;
}
.drawerSong img {
transition: .4s;
width: 5em;
height: 100%;
object-fit: cover;
opacity: .4;
filter: grayscale(1);
}
.drawerSong p {
position: absolute;
top: 0;
left: 0;
display: inline;
}
.drawerSong:hover img, .drawerSong.selected img {
opacity: 1;
overflow: hidden;
width: 10em;
}
.drawerSong.selected img {
filter: none;
}
.playlistTitle {
background-color: white;
color: black;
}
#playlist {
transition: .2s;
max-height: 10em;
overflow: auto;
border: medium solid white;
}
#playlist p {
cursor: pointer;
padding: 0 .4em;
}
#playlist p:first-child {
padding-bottom: .2em;
}
.playingSong {
font-size: larger;
}
.hidden {
display: none;
}
.invisible {
opacity: 0;
}
.bg.invisible {
opacity: 0!important;
}
@keyframes ellipsis-loader {
0%, 25% {
transform: translateX(0);
@ -508,6 +670,7 @@ div.hs.selected {
#headerLinks {
text-align: center;
width: 100%;
}
#homeTitle {
@ -571,12 +734,20 @@ div.hs.selected {
.hsProjectHeaderIcon {
margin: auto;
}
aside.metromenu {
width: 50%;
}
}
@media screen and (max-width: 800px) {
#homeSquares {
width: 60vw;
}
aside.metromenu {
width: 100%;
}
}
@media screen and (max-width: 720px) {

BIN
static/music/PG2.mp3 Normal file

Binary file not shown.

BIN
static/music/dreamscape.mp3 Normal file

Binary file not shown.

BIN
static/music/skychat.mp3 Normal file

Binary file not shown.

View file

@ -1,25 +1,156 @@
const toggle = document.querySelector('#soundDiv')
toggle.innerHTML = `<img src="/static/images/sound-on.png" id="sound" onclick="toggleAudio()"><p>${toggle.getAttribute("data-title")}</p>`
// This script handles the playback of music in the header's miniplayer ;)
const body = document.querySelector("body");
document.getElementById("music").innerHTML = `
<img src="/static/images/gears.svg" class="optionsToggle invertedc">
<img src="/static/images/sound-on.png" id="sound">
<select name="song" id="songSelection"></select>
`
const songs = [
{ file: "Velkommen.mp3", name: 'Velkommen', artwork: "velkommen.jpg" },
{ file: "PG2.mp3", name: 'Frugal APE', artwork: "pg.jpg" },
{ file: "dreamscape.mp3", name: 'Dreamscape', artwork: "winds.png" },
{ file: "skychat.mp3", name: 'Skychat', artwork: "winds.png" }
];
// Options page
const optionsAside = document.createElement("aside");
optionsAside.classList.add("closed");
optionsAside.classList.add("metromenu");
{
const back = document.createElement("p");
back.textContent = headeri18n.back;
back.classList.add("optionsToggle");
const title = document.createElement("h2");
title.textContent = headeri18n.options;
optionsAside.appendChild(title);
optionsAside.appendChild(back);
const content = document.createElement("div");
content.innerHTML = `
<div id="content">
<div id="songDrawer"></div>
<div>
<p class="playingSong"></p>
<p>${headeri18n.by} tenkuma</p>
</div>
<div id="playlist"></div>
<div>
<p>Volume</p>
<input id="volume" oninput="setVolume(this.value / 100)" type="range" min="0" max="100"></input>
</div>
<div class="checkbox">
<p>${headeri18n.hideBackground}</p>
<input id="background" type="checkbox"></input>
</div>
</div>
`
optionsAside.appendChild(content);
}
body.appendChild(optionsAside);
const audio = new Audio(`/static/${toggle.getAttribute("data-source")}`);
const toggleIMG = document.querySelector('#sound');
toggleIMG.addEventListener('click', () => {
toggleAudio();
})
const savedTime = localStorage.getItem("audioTime");
const wasPlaying = localStorage.getItem("audioPlaying") === 'true'
const hideBG = document.querySelector("input#background");
if (localStorage.getItem("bgHidden") === "true") hideBG.checked = true, toggleBG();
hideBG.addEventListener("click", () => {
toggleBG();
})
if (savedTime) audio.currentTime = parseFloat(savedTime);
if (wasPlaying) {
play();
} else {
stop();
function toggleBG() {
const bg = document.querySelector(".bg");
bg.classList.toggle("invisible");
localStorage.setItem("bgHidden", bg.classList.contains("invisible"))
}
const songsDrawer = document.querySelector("#songDrawer");
const drawerSongs = [];
const playlist = document.querySelector("#playlist");
const expandButton = document.createElement('p');
expandButton.textContent = "Playlist";
expandButton.classList.add("playlistTitle");
playlist.appendChild(expandButton);
songs.forEach(song => {
const songElement = document.createElement("div");
songElement.classList.add("drawerSong");
songElement.dataset.song = song.file;
const songImage = document.createElement("img");
songImage.src = `/static/images/songs/${song.artwork}`;
songElement.appendChild(songImage);
songElement.addEventListener('click', () => {
changeSong(song.file);
});
drawerSongs.push(songElement);
songsDrawer.appendChild(songElement);
// Playlist
const playlistEntry = document.createElement("p");
playlistEntry.textContent = song.name;
playlistEntry.addEventListener('click', () => {
changeSong(song.file);
})
playlist.appendChild(playlistEntry);
})
const audioSelect = document.getElementById("songSelection");
songs.forEach(song => {
const songOption = document.createElement("option");
songOption.value = song.file;
songOption.textContent = song.name;
audioSelect.appendChild(songOption);
});
const playingSongLabel = document.querySelector(".playingSong");
function updatePlayingLabel(label) {
drawerSongs.forEach(sng => {
sng.classList.remove("selected");
if (sng.dataset.song == label) {
sng.classList.add("selected");
}
});
const songString = songs.find(item => item.file === label).name;
playingSongLabel.textContent = songString;
}
const savedSong = localStorage.getItem("song");
if (savedSong) {
audioSelect.value = savedSong;
updatePlayingLabel(savedSong);
} else {
audioSelect.value = songs[0].file;
updatePlayingLabel(songs[0].file);
}
const optionsButton = document.querySelectorAll(".optionsToggle");
optionsButton.forEach(button => {
button.addEventListener('click', () => {
optionsAside.classList.toggle('closed');
});
});
// Create the audio object using the current select value
let audio = new Audio(`/static/music/${audioSelect.value}`);
const savedTime = localStorage.getItem("audioTime");
const savedVolume = localStorage.getItem("volume");
const wasPlaying = localStorage.getItem("audioPlaying") === 'true';
function play() {
audio.volume = 0.8;
audio.volume = localStorage.getItem("volume");
audio.play();
localStorage.setItem("audioPlaying", "true")
toggleIMG.src = "/static/images/sound-on.png"
console.log(`[Music Player] playing ${audioSelect.value}`)
}
function stop() {
@ -28,6 +159,11 @@ function stop() {
toggleIMG.src = "/static/images/sound-off.png"
}
function setVolume(volume) {
audio.volume = volume;
localStorage.setItem("volume", volume);
}
function toggleAudio() {
if (!audio.paused) {
stop();
@ -37,6 +173,34 @@ function toggleAudio() {
}
window.addEventListener("beforeunload", () => {
localStorage.setItem("audioTime", audio.currentTime);
localStorage.setItem("audioPlaying", !audio.paused);
});
localStorage.setItem("audioTime", audio.currentTime);
localStorage.setItem("audioPlaying", !audio.paused);
});
function changeSong(song) {
const wasPlaying = !audio.paused;
stop();
localStorage.removeItem("audioTime");
audio = new Audio(`/static/music/${song}`);
if (savedVolume) setVolume(savedVolume);
console.log(`[Music Player] changing song to ${song}`)
localStorage.setItem("song", song);
updatePlayingLabel(song);
if (wasPlaying) play();
}
// hooking into the options menu 'change' event to update the song
audioSelect.addEventListener('change', () => {
changeSong(audioSelect.value);
})
// Set initial playback state and volume based on saved preferences
if (savedTime) audio.currentTime = parseFloat(savedTime);
if (savedVolume) document.getElementById("volume").value = savedVolume * 100;
if (wasPlaying) {
play();
} else {
stop();
}