forked from globuzma/mentalaradio
Compare commits
7 commits
9954154abc
...
a8ab7694a9
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a8ab7694a9 | ||
![]() |
8c836ef181 | ||
![]() |
c7bdedd732 | ||
![]() |
2535a320aa | ||
![]() |
1b1f272a5d | ||
![]() |
560ea833b1 | ||
e2d735a6e5 |
10 changed files with 399 additions and 55 deletions
|
@ -32,3 +32,8 @@ go build server.go
|
|||
```
|
||||
|
||||
Lorsque vous lancerez le serveur avec `./server` le programme viendra chercher tous les mp3 à la racine du dossier **music** et l'UI compilée devra être dans le dossier **dist**. Vous pouvez simplement copier le dossier dist produit par `npm run build` au même endroit que l'executable `server`.
|
||||
|
||||
# Crédits
|
||||
|
||||
Le compteur de personnes connectées utilise un artwork de Keith Haring.
|
||||
La police d'écriture utilisée pour le titre est [Momentz](https://www.dafont.com/momentz.font) de [Khurasan Studio](https://khurasanstudio.com/).
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/menthealeau.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Radio menthe à l'eau 🍹</title>
|
||||
<meta name="robots" content="noindex nofollow" />
|
||||
|
|
79
public/menthealeau.svg
Normal file
79
public/menthealeau.svg
Normal file
|
@ -0,0 +1,79 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="593.60071"
|
||||
height="667.54163"
|
||||
viewBox="-32 -32 593.60071 667.54161"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs6" />
|
||||
<g
|
||||
filter="url(#a)"
|
||||
id="g6"
|
||||
transform="translate(-27.199546,-90.924425)">
|
||||
<defs
|
||||
id="defs3">
|
||||
<filter
|
||||
id="a"
|
||||
x="-0.0082758516"
|
||||
y="-0.0073401053"
|
||||
width="1.0234482"
|
||||
height="1.020797"
|
||||
filterUnits="objectBoundingBox"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood
|
||||
flood-opacity="0"
|
||||
result="BackgroundImageFix"
|
||||
id="feFlood1" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
id="feColorMatrix1" />
|
||||
<feOffset
|
||||
dx="4"
|
||||
dy="4"
|
||||
id="feOffset1" />
|
||||
<feGaussianBlur
|
||||
stdDeviation="2"
|
||||
id="feGaussianBlur1" />
|
||||
<feColorMatrix
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"
|
||||
id="feColorMatrix2" />
|
||||
<feBlend
|
||||
in2="BackgroundImageFix"
|
||||
result="filter_265b8c7a-1d1a-80dd-8005-dbe7b8ea37ba"
|
||||
id="feBlend2"
|
||||
mode="normal" />
|
||||
<feBlend
|
||||
in="SourceGraphic"
|
||||
in2="filter_265b8c7a-1d1a-80dd-8005-dbe7b8ea37ba"
|
||||
result="shape"
|
||||
id="feBlend3"
|
||||
mode="normal" />
|
||||
</filter>
|
||||
</defs>
|
||||
<path
|
||||
d="M 74,79.666 229.489,63.764 c 0,0 14.498,-1.017 21.131,7.26 6.285,7.842 6.769,26.2 6.769,26.2 l 30.51,307.195 -31.228,2.19 -27.145,-297.568 c 0,0 -0.528,-10.723 -5.478,-14.186 -3.887,-2.72 -20.662,-0.569 -20.662,-0.569 l -125.462,13.73 z"
|
||||
style="fill:#ffb700;fill-opacity:1"
|
||||
class="fills"
|
||||
id="path3" />
|
||||
<path
|
||||
d="m 372.205,271.622 c 17.68,-76.098 73.026,-129.905 83.018,-126.83 9.993,3.074 4.613,88.396 -2.306,112.225 -0.26,0.896 -2.697,5.717 2.306,3.075 24.106,-12.731 119.029,7.399 124.528,20.754 5.38,13.067 -77.638,43.045 -121.837,37.28 -1.876,-0.245 -2.12,0.267 0,1.922 16.878,13.175 25.751,101.463 16.527,106.075 -11.277,5.638 -83.787,-61.493 -86.093,-84.553 -2.306,-23.06 -13.068,-20.754 -13.837,-24.597 -0.768,-3.843 -4.227,-39.971 -2.306,-45.351 z m 0,0"
|
||||
style="fill:#20a541;fill-opacity:1"
|
||||
class="fills"
|
||||
id="path4" />
|
||||
<path
|
||||
d="m 140.831,223.964 c 0,0 -164.766,396.433 -137.862,412.575 26.904,16.142 150.663,96.851 179.104,78.403 C 210.515,696.494 421.904,338.296 406.53,302.938 391.156,267.579 195.141,169.19 170.543,186.869 c -24.598,17.679 -37.666,57.65 -29.712,37.095 z m 0,0"
|
||||
style="fill:#ffffff;fill-opacity:1"
|
||||
class="fills"
|
||||
id="path5" />
|
||||
<path
|
||||
d="m 150.129,258.548 c 0,0 -146.245,349.224 -123.795,362.705 22.45,13.481 125.72,80.886 149.453,65.479 23.732,-15.407 200.125,-314.558 187.297,-344.088 -4.739,-10.908 -58.204,8.205 -89.271,-9.529 -27.411,-15.648 -7.611,-66.952 -33.822,-76.867 -24.502,-9.268 -67.572,-15.038 -73.826,-10.539 -20.526,14.765 -16.036,12.839 -16.036,12.839 z"
|
||||
style="fill:#3aff94;fill-opacity:1"
|
||||
class="fills"
|
||||
id="path6" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.2 KiB |
131
server.go
131
server.go
|
@ -37,12 +37,21 @@ type ConnectionPool struct {
|
|||
}
|
||||
|
||||
type MetadataConnection struct {
|
||||
metadataSent bool
|
||||
metadataSent bool
|
||||
}
|
||||
|
||||
type MetadataConnectionPool struct {
|
||||
MetadataConnectionMap map[*MetadataConnection]struct{}
|
||||
mu sync.Mutex
|
||||
MetadataConnectionMap map[*MetadataConnection]struct{}
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type PeopleConnection struct {
|
||||
numberSent int
|
||||
}
|
||||
|
||||
type PeopleConnectionPool struct {
|
||||
PeopleConnectionMap map[*PeopleConnection]struct{}
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// Audio connection pool
|
||||
|
@ -103,6 +112,24 @@ func (cp *MetadataConnectionPool) Broadcast() {
|
|||
}
|
||||
}
|
||||
|
||||
// People connection pool
|
||||
func (cp *PeopleConnectionPool) AddConnection(connection *PeopleConnection) {
|
||||
defer cp.mu.Unlock()
|
||||
cp.mu.Lock()
|
||||
cp.PeopleConnectionMap[connection] = struct{}{}
|
||||
}
|
||||
|
||||
func (cp *PeopleConnectionPool) DeleteConnection(connection *PeopleConnection) {
|
||||
defer cp.mu.Unlock()
|
||||
cp.mu.Lock()
|
||||
delete(cp.PeopleConnectionMap, connection)
|
||||
}
|
||||
|
||||
func NewPeopleConnectionPool() *PeopleConnectionPool {
|
||||
peopleConnectionMap := make(map[*PeopleConnection]struct{})
|
||||
return &PeopleConnectionPool{PeopleConnectionMap: peopleConnectionMap}
|
||||
}
|
||||
|
||||
func getFileDelay(mp3FilePath string) int64 {
|
||||
t := 0.0
|
||||
size := 0
|
||||
|
@ -136,13 +163,31 @@ func getFileDelay(mp3FilePath string) int64 {
|
|||
return delayVal
|
||||
}
|
||||
|
||||
func streamFolder(connectionPool *ConnectionPool, metadataConnectionPool *MetadataConnectionPool, list []string) {
|
||||
func streamFolder(connectionPool *ConnectionPool, metadataConnectionPool *MetadataConnectionPool, folder string) {
|
||||
|
||||
buffer := make([]byte, BUFFERSIZE)
|
||||
|
||||
for _, music := range list {
|
||||
music_files := []string{}
|
||||
|
||||
file, err := os.Open(filepath.Join("./music/", music))
|
||||
entries, err := os.ReadDir(folder)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, e := range entries {
|
||||
if filepath.Ext(e.Name()) == ".mp3" {
|
||||
music_files = append(music_files, e.Name())
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("%d Music file found.", len(music_files))
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
rand.Shuffle(len(music_files), func(i, j int) { music_files[i], music_files[j] = music_files[j], music_files[i] })
|
||||
|
||||
for _, music := range music_files {
|
||||
|
||||
file, err := os.Open(filepath.Join(folder, music))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -182,7 +227,7 @@ func streamFolder(connectionPool *ConnectionPool, metadataConnectionPool *Metada
|
|||
// clear() is a new builtin function introduced in go 1.21. Just reinitialize the buffer if on a lower version.
|
||||
clear(buffer)
|
||||
tempfile := bytes.NewReader(ctn)
|
||||
delay := getFileDelay(filepath.Join("./music/", music))
|
||||
delay := getFileDelay(filepath.Join(folder, music))
|
||||
ticker := time.NewTicker(time.Millisecond * time.Duration(delay))
|
||||
|
||||
// Send one buffer in advance to avoid client choking
|
||||
|
@ -204,37 +249,24 @@ func streamFolder(connectionPool *ConnectionPool, metadataConnectionPool *Metada
|
|||
|
||||
}
|
||||
|
||||
streamFolder(connectionPool, metadataConnectionPool, folder)
|
||||
|
||||
}
|
||||
|
||||
func main() {
|
||||
connPool := NewConnectionPool()
|
||||
metadataConnPool := NewMetadataConnectionPool()
|
||||
peopleConnPool := NewPeopleConnectionPool()
|
||||
|
||||
music_files := []string{}
|
||||
|
||||
entries, err := os.ReadDir("./music")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, e := range entries {
|
||||
if filepath.Ext(e.Name()) == ".mp3" {
|
||||
music_files = append(music_files, e.Name())
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("%d Music file found.", len(music_files))
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
rand.Shuffle(len(music_files), func(i, j int) { music_files[i], music_files[j] = music_files[j], music_files[i] })
|
||||
|
||||
go streamFolder(connPool, metadataConnPool, music_files)
|
||||
go streamFolder(connPool, metadataConnPool, "./music")
|
||||
|
||||
http.Handle("/", http.FileServer(http.Dir("./dist")))
|
||||
|
||||
http.HandleFunc("/stream", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Add("Content-Type", "audio/mp3")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Add("Connection", "keep-alive")
|
||||
|
||||
flusher, ok := w.(http.Flusher)
|
||||
|
@ -268,6 +300,13 @@ func main() {
|
|||
|
||||
connection := &MetadataConnection{metadataSent: false}
|
||||
metadataConnPool.AddConnection(connection)
|
||||
|
||||
go func(done <-chan struct{}) {
|
||||
<-done
|
||||
metadataConnPool.DeleteConnection(connection)
|
||||
log.Printf("%s's connection to the metadata stream has been closed\n", r.Host)
|
||||
}(r.Context().Done())
|
||||
|
||||
log.Printf("%s has connected to the metadata stream\n", r.Host)
|
||||
// Simulate sending events (you can replace this with real data)
|
||||
for {
|
||||
|
@ -279,9 +318,13 @@ func main() {
|
|||
|
||||
finalData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Print(err)
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprintf(w, "data: %s\n\n", fmt.Sprintf("%s", finalData)); err != nil {
|
||||
log.Printf("Error on metadata stream : %s", err)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "data: %s\n\n", fmt.Sprintf("%s", finalData))
|
||||
w.(http.Flusher).Flush()
|
||||
connection.metadataSent = true
|
||||
}
|
||||
|
@ -289,6 +332,40 @@ func main() {
|
|||
}
|
||||
})
|
||||
|
||||
http.HandleFunc("/listeners", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Expose-Headers", "Content-Type")
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
|
||||
connection := &PeopleConnection{numberSent: 0}
|
||||
peopleConnPool.AddConnection(connection)
|
||||
|
||||
go func(done <-chan struct{}) {
|
||||
<-done
|
||||
peopleConnPool.DeleteConnection(connection)
|
||||
log.Printf("%s's connection to the listeners stream has been closed\n", r.Host)
|
||||
}(r.Context().Done())
|
||||
|
||||
log.Printf("%s has connected to the listeners stream\n", r.Host)
|
||||
for {
|
||||
if(connection.numberSent != len(connPool.ConnectionMap)) {
|
||||
connection.numberSent = len(connPool.ConnectionMap)
|
||||
// n, err := fmt.Fprintf(w, "data: %s\n\n", fmt.Sprintf("%d", connection.numberSent))
|
||||
_, err := w.Write([]byte(fmt.Sprintf("data: %d\n\n", connection.numberSent)))
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error on listeners stream : %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
time.Sleep(time.Second * 1)
|
||||
}
|
||||
})
|
||||
|
||||
log.Println("Listening on port 8080...")
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
|
||||
|
|
117
src/App.svelte
117
src/App.svelte
|
@ -1,10 +1,18 @@
|
|||
<script>
|
||||
import { basePath } from './lib/router.tsx'
|
||||
import menthealeauLogo from './assets/menthealeau.svg'
|
||||
import dancing_1 from './assets/d1.svg'
|
||||
import dancing_2 from './assets/d2.svg'
|
||||
import dancing_3 from './assets/d3.svg'
|
||||
import dancing_4 from './assets/d4.svg'
|
||||
import dancing_5 from './assets/d5.svg'
|
||||
|
||||
let dancing_guys = [dancing_1, dancing_2, dancing_3, dancing_4, dancing_5]
|
||||
|
||||
let isPlaying = $state(false)
|
||||
let title = $state("")
|
||||
let url = $state("")
|
||||
let numberOfListeners = $state(0)
|
||||
let imageData = $state("")
|
||||
let loadingSound = $state(false)
|
||||
let firstTimePlaying = $state(true)
|
||||
|
@ -14,14 +22,24 @@
|
|||
const eventSource = new EventSource(basePath + '/metadata')
|
||||
eventSource.onmessage = function(event) {
|
||||
let receivedData = JSON.parse(event.data)
|
||||
console.log(receivedData)
|
||||
title = receivedData.title
|
||||
url = receivedData.url
|
||||
imageData = receivedData.artwork
|
||||
}
|
||||
|
||||
const listenerSource = new EventSource(basePath + '/listeners')
|
||||
listenerSource.onmessage = function(event) {
|
||||
let receivedData = event.data
|
||||
numberOfListeners = parseInt(receivedData)
|
||||
}
|
||||
|
||||
function getRandomInt(min, max) {
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
async function buttonClick() {
|
||||
console.log(audioPlayer.duration)
|
||||
if(isPlaying) {
|
||||
audioPlayer.pause()
|
||||
isPlaying = false
|
||||
|
@ -54,46 +72,91 @@
|
|||
</div>
|
||||
<img class="w-96 -mb-48 md:w-xl md:-mr-56 md:-mt-64 md:-mb-80" src={menthealeauLogo} alt="Logo d'une menthe à l'eau" />
|
||||
</div>
|
||||
<div class="p-4 bg-[#f97095] rounded-md border-white border-5 drop-shadow-md flex flex-col gap-4 max-w-96 w-full">
|
||||
<h1 class="font-[Momentz] text-white font-bold text-4xl">Menthe à l'eau</h1>
|
||||
<p class="text-white font-[Inter] font-bold">Créée à l'origine pour être un énorme dump de musique en tout genre. Cette playlist est contre toute attente devenue un énorme dump de musique en tout genre. Enjoy the radio. 📻</p>
|
||||
{#if imageData == ""}
|
||||
<div class="block w-full pb-[26%] pt-[26%] flex flex-col items-center justify-center bg-[#ff99b4] rounded-md">
|
||||
<svg class="mr-3 -ml-1 size-5 animate-spin text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
|
||||
<div class="flex flex-row md:items-center gap-2 max-w-96 w-full">
|
||||
<div class="p-4 bg-[#f97095] rounded-md border-white border-5 drop-shadow-md flex flex-col gap-4 max-w-96 w-full">
|
||||
<h1 class="font-[Momentz] text-white font-bold text-4xl">Menthe à l'eau</h1>
|
||||
<p class="text-white font-[Inter] font-bold">Créée à l'origine pour être un énorme dump de musique en tout genre. Cette playlist est contre toute attente devenue un énorme dump de musique en tout genre. Enjoy the radio. 📻</p>
|
||||
{#if imageData == ""}
|
||||
<div class="block w-full pb-[26%] pt-[26%] flex flex-col items-center justify-center bg-[#ff99b4] rounded-md">
|
||||
<svg class="mr-3 -ml-1 size-5 animate-spin text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
|
||||
</div>
|
||||
{:else}
|
||||
<a href={url} target="_blank">
|
||||
<img class="w-full rounded" src={imageData} alt="current song artwork" />
|
||||
</a>
|
||||
{/if}
|
||||
<span class="text-white font-[Inter] font-bold w-full text-ellipsis truncate">{#if title == ""}Loading...{:else}{title}{/if}</span>
|
||||
<audio bind:this={audioPlayer}>
|
||||
<source src={basePath + "/stream"} type="audio/mp3">
|
||||
</audio>
|
||||
<div class="flex flex-row w-full mt-16 -mb-24 flex-wrap">
|
||||
{#each {length: numberOfListeners} as _,i}
|
||||
<img src={dancing_guys[getRandomInt(0,4)]} class="max-h-6 max-w-6 animate-bounce">
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<a href={url} target="_blank">
|
||||
<img class="w-full rounded" src={imageData} alt="current song artwork" />
|
||||
</a>
|
||||
{/if}
|
||||
<span class="text-white font-[Inter] font-bold w-full text-ellipsis truncate">{#if title == ""}Loading...{:else}{title}{/if}</span>
|
||||
<audio bind:this={audioPlayer}>
|
||||
<source src={basePath + "/stream"} type="audio/mp3">
|
||||
</audio>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<div class="bg-[#ff99b4] w-32 rounded-full -mb-12">
|
||||
{#if loadingSound}
|
||||
<svg class="mr-3 -ml-1 w-full animate-spin text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
|
||||
{:else}
|
||||
{#if isPlaying}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" onclick={buttonClick} class="text-white w-full" viewBox="0 0 24 24"><path fill="currentColor" d="M6 16V8q0-.825.588-1.412T8 6h8q.825 0 1.413.588T18 8v8q0 .825-.587 1.413T16 18H8q-.825 0-1.412-.587T6 16"/></svg>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<div class="bg-[#ff99b4] w-32 rounded-full -mb-12 z-20">
|
||||
{#if loadingSound}
|
||||
<svg class="mr-3 -ml-1 w-full animate-spin text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" onclick={buttonClick} class="text-white w-full" viewBox="0 0 24 24"><path fill="currentColor" d="M8 17.175V6.825q0-.425.3-.713t.7-.287q.125 0 .263.037t.262.113l8.15 5.175q.225.15.338.375t.112.475t-.112.475t-.338.375l-8.15 5.175q-.125.075-.262.113T9 18.175q-.4 0-.7-.288t-.3-.712"/></svg>
|
||||
{#if isPlaying}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" onclick={buttonClick} class="text-white w-full cursor-pointer" viewBox="0 0 24 24"><path fill="currentColor" d="M6 16V8q0-.825.588-1.412T8 6h8q.825 0 1.413.588T18 8v8q0 .825-.587 1.413T16 18H8q-.825 0-1.412-.587T6 16"/></svg>
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" onclick={buttonClick} class="text-white w-full cursor-pointer" viewBox="0 0 24 24"><path fill="currentColor" d="M8 17.175V6.825q0-.425.3-.713t.7-.287q.125 0 .263.037t.262.113l8.15 5.175q.225.15.338.375t.112.475t-.112.475t-.338.375l-8.15 5.175q-.125.075-.262.113T9 18.175q-.4 0-.7-.288t-.3-.712"/></svg>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col bg-white rounded-md items-center justify-center h-fit mt-12 pt-4 pb-1 gap-2">
|
||||
<input id="volume-slider" type="range" value="100" style="" onchange={(e) => audioPlayer.volume = parseInt(e.target.value) / 100}/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 text-[#ff99b4]" viewBox="0 0 24 24"><path fill="currentColor" d="M9 15H6q-.425 0-.712-.288T5 14v-4q0-.425.288-.712T6 9h3l3.3-3.3q.475-.475 1.088-.213t.612.938v11.15q0 .675-.612.938T12.3 18.3zm9.5-3q0 1.05-.475 1.988t-1.25 1.537q-.25.15-.512.013T16 15.1V8.85q0-.3.263-.437t.512.012q.775.625 1.25 1.575t.475 2"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-fit h-fit flex mt-12 mb-24 md:mb-12 flex-col visible md:w-0 md:h-0 md:invisible items-end gap-4">
|
||||
<span class="speech-bubble arrow-left">Et t'écoutes quoi comme musique ?</span>
|
||||
<span class="speech-bubble arrow-right">Oh ... De tout.</span>
|
||||
</div>
|
||||
<div class="w-full opacity-50">
|
||||
<span>Créé par Zuma · <a href="https://git.shenanigans.cc/globuzma/mentalaradio">Source</a></span>
|
||||
<span>Créé par Zuma · <a class="underline" href="https://git.shenanigans.cc/globuzma/mentalaradio">Source</a></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#volume-slider {
|
||||
border-radius:5px;
|
||||
width: 5px;
|
||||
height: 9rem;
|
||||
background: #d3d3d3;
|
||||
-webkit-appearance: none;
|
||||
outline: none;
|
||||
writing-mode: vertical-lr;
|
||||
direction: rtl;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#volume-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: none;
|
||||
outline: none;
|
||||
border-radius: 50%;
|
||||
background: #ff99b4;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#volume-slider::-moz-range-thumb {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: none;
|
||||
outline: none;
|
||||
border-radius: 50%;
|
||||
background: #ff99b4;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.speech-bubble {
|
||||
width: fit-content;
|
||||
padding: 1rem;
|
||||
|
|
24
src/assets/d1.svg
Normal file
24
src/assets/d1.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 12 KiB |
24
src/assets/d2.svg
Normal file
24
src/assets/d2.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 12 KiB |
24
src/assets/d3.svg
Normal file
24
src/assets/d3.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 12 KiB |
24
src/assets/d4.svg
Normal file
24
src/assets/d4.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 14 KiB |
24
src/assets/d5.svg
Normal file
24
src/assets/d5.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 14 KiB |
Loading…
Add table
Add a link
Reference in a new issue