Add code for fal.ai integration #12
14
.gitignore
vendored
14
.gitignore
vendored
@@ -4,8 +4,7 @@
|
|||||||
### Linux ###
|
### Linux ###
|
||||||
*~
|
*~
|
||||||
|
|
||||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
# temporary files which can be created if a process still has a handle open of a deleted file .fuse_hidden*
|
||||||
.fuse_hidden*
|
|
||||||
|
|
||||||
# KDE directory preferences
|
# KDE directory preferences
|
||||||
.directory
|
.directory
|
||||||
@@ -249,4 +248,13 @@ $RECYCLE.BIN/
|
|||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/python,windows,macos,linux
|
# End of https://www.toptal.com/developers/gitignore/api/python,windows,macos,linux
|
||||||
|
|
||||||
*.mp3
|
0.mp3
|
||||||
|
1.mp3
|
||||||
|
2.mp3
|
||||||
|
3.mp3
|
||||||
|
4.mp3
|
||||||
|
5.mp3
|
||||||
|
6.mp3
|
||||||
|
7.mp3
|
||||||
|
8.mp3
|
||||||
|
9.mp3
|
||||||
|
@@ -38,3 +38,7 @@ INFERENCE_SERVER_WS_URL=ws://2.3.1.4:1938 fastapi run server.py --port ${ANY_POR
|
|||||||
|
|
||||||
if you have any feature idea, feel free to use the issue tracker to let me know!
|
if you have any feature idea, feel free to use the issue tracker to let me know!
|
||||||
|
|
||||||
|
## credits:
|
||||||
|
|
||||||
|
- cats by [@PixElthen](https://x.com/pixelthen)
|
||||||
|
|
||||||
|
36
server.py
36
server.py
@@ -3,10 +3,11 @@ import os
|
|||||||
|
|
||||||
import websocket
|
import websocket
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from logger import log_info, log_warn
|
from logger import log_info, log_warn
|
||||||
|
from websocket_connection_manager import WebSocketConnectionManager
|
||||||
|
|
||||||
# the index of the current audio track from 0 to 9
|
# the index of the current audio track from 0 to 9
|
||||||
current_index = -1
|
current_index = -1
|
||||||
@@ -15,6 +16,8 @@ t = None
|
|||||||
# websocket connection to the inference server
|
# websocket connection to the inference server
|
||||||
ws = None
|
ws = None
|
||||||
ws_url = ""
|
ws_url = ""
|
||||||
|
ws_connection_manager = WebSocketConnectionManager()
|
||||||
|
active_listeners = set()
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
@@ -83,7 +86,6 @@ def advance():
|
|||||||
current_index = 0
|
current_index = 0
|
||||||
else:
|
else:
|
||||||
current_index = current_index + 1
|
current_index = current_index + 1
|
||||||
|
|
||||||
threading.Thread(target=generate_new_audio).start()
|
threading.Thread(target=generate_new_audio).start()
|
||||||
|
|
||||||
t = threading.Timer(60, advance)
|
t = threading.Timer(60, advance)
|
||||||
@@ -98,4 +100,34 @@ def get_current_audio():
|
|||||||
return FileResponse(f"{current_index}.mp3")
|
return FileResponse(f"{current_index}.mp3")
|
||||||
|
|
||||||
|
|
||||||
|
@app.websocket("/ws")
|
||||||
|
async def ws_endpoint(ws: WebSocket):
|
||||||
|
await ws_connection_manager.connect(ws)
|
||||||
|
|
||||||
|
addr = ""
|
||||||
|
if ws.client:
|
||||||
|
addr, _ = ws.client
|
||||||
|
else:
|
||||||
|
await ws.close()
|
||||||
|
ws_connection_manager.disconnect(ws)
|
||||||
|
|
||||||
|
await ws_connection_manager.broadcast(f"{len(active_listeners)}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
msg = await ws.receive_text()
|
||||||
|
|
||||||
|
if msg == "playing":
|
||||||
|
active_listeners.add(addr)
|
||||||
|
await ws_connection_manager.broadcast(f"{len(active_listeners)}")
|
||||||
|
elif msg == "paused":
|
||||||
|
active_listeners.discard(addr)
|
||||||
|
await ws_connection_manager.broadcast(f"{len(active_listeners)}")
|
||||||
|
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
active_listeners.discard(addr)
|
||||||
|
ws_connection_manager.disconnect(ws)
|
||||||
|
await ws_connection_manager.broadcast(f"{len(active_listeners)}")
|
||||||
|
|
||||||
|
|
||||||
app.mount("/", StaticFiles(directory="web", html=True), name="web")
|
app.mount("/", StaticFiles(directory="web", html=True), name="web")
|
||||||
|
BIN
web/audio/achievement-unlocked.mp3
Normal file
BIN
web/audio/achievement-unlocked.mp3
Normal file
Binary file not shown.
BIN
web/audio/cat-meow.mp3
Normal file
BIN
web/audio/cat-meow.mp3
Normal file
Binary file not shown.
BIN
web/favicon.png
Normal file
BIN
web/favicon.png
Normal file
Binary file not shown.
Before Width: | Height: | Size: 939 B After Width: | Height: | Size: 939 B |
BIN
web/images/heart.png
Normal file
BIN
web/images/heart.png
Normal file
Binary file not shown.
Before Width: | Height: | Size: 492 B After Width: | Height: | Size: 492 B |
@@ -1,9 +1,11 @@
|
|||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
|
||||||
<title>infinifi</title>
|
<title>infinifi</title>
|
||||||
|
|
||||||
|
<link rel="icon" type="image/png" href="./favicon.png">
|
||||||
<link rel="preload" href="./style.css" as="style">
|
<link rel="preload" href="./style.css" as="style">
|
||||||
<link rel="modulepreload" href="./bg.js">
|
<link rel="modulepreload" href="./bg.js">
|
||||||
<link rel="modulepreload" href="./script.js">
|
<link rel="modulepreload" href="./script.js">
|
||||||
@@ -11,13 +13,14 @@
|
|||||||
<link rel="preload" href="./images/eeping-cat.png" as="image">
|
<link rel="preload" href="./images/eeping-cat.png" as="image">
|
||||||
<link rel="stylesheet" href="./style.css">
|
<link rel="stylesheet" href="./style.css">
|
||||||
|
|
||||||
|
<meta name="description" content="infinite lo-fi music in the background">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
<meta name="twitter:card" content="summary">
|
<meta name="twitter:card" content="summary">
|
||||||
<meta name="twitter:site" content="@kennethnym">
|
<meta name="twitter:site" content="@kennethnym">
|
||||||
<meta name="twitter:title" content="infinifi">
|
<meta name="twitter:title" content="infinifi">
|
||||||
<meta name="twitter:description" content="infinite lo-fi beats in the background">
|
<meta name="twitter:description" content="infinite lo-fi beats in the background">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<main>
|
<main>
|
||||||
@@ -26,24 +29,38 @@
|
|||||||
<div class="button-container">
|
<div class="button-container">
|
||||||
<button id="play-btn" class="button">play</button>
|
<button id="play-btn" class="button">play</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="volume-slider-container">
|
|
||||||
<output id="current-volume-label" for="volume-slider">100%</output>
|
<div class="status-bar">
|
||||||
<input id="volume-slider" type="range" min="0" max="100" step="1" />
|
<p id="listener-count">0 person tuned in</p>
|
||||||
|
<div id="volume-slider-container">
|
||||||
|
<output id="current-volume-label" for="volume-slider">100%</output>
|
||||||
|
<input id="volume-slider" type="range" min="0" max="100" step="1" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
<img class="cat" src="./images/cat-0.png">
|
|
||||||
<img class="eeping-cat" src="./images/eeping-cat.png">
|
<aside role="alert" id="notification">
|
||||||
|
<p id="notification-title">a little chatty</p>
|
||||||
|
<p id="notification-body">make milo meow 100 times</p>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<img id="cat" src="./images/cat-0.png">
|
||||||
|
<img id="eeping-cat" src="./images/eeping-cat.png">
|
||||||
|
<img id="heart" src="./images/heart.png">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<span>made by kennethnym <3 · </span>
|
<span>made by kennethnym <3 · </span>
|
||||||
<a target="_blank" href="https://github.com/kennethnym/infinifi">github</a>
|
<a target="_blank" rel="noopener noreferrer" href="https://github.com/kennethnym/infinifi">github</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<canvas id="bg"></canvas>
|
<canvas id="bg"></canvas>
|
||||||
|
|
||||||
<audio id="click-audio" preload="auto" src="./audio/click.wav"></audio>
|
<audio id="click-audio" preload="auto" src="./audio/click.wav"></audio>
|
||||||
<audio id="click-release-audio" preload="auto" src="./audio/click-release.wav"></audio>
|
<audio id="click-release-audio" preload="auto" src="./audio/click-release.wav"></audio>
|
||||||
|
<audio id="meow-audio" preload="auto" src="./audio/cat-meow.mp3"></audio>
|
||||||
|
<audio id="achievement-unlocked-audio" preload="auto" src="./audio/achievement-unlocked.mp3"></audio>
|
||||||
|
|
||||||
<script type="module" src="./bg.js"></script>
|
<script type="module" src="./bg.js"></script>
|
||||||
<script type="module" src="./script.js"></script>
|
<script type="module" src="./script.js"></script>
|
||||||
|
176
web/script.js
176
web/script.js
@@ -1,19 +1,35 @@
|
|||||||
const playBtn = document.getElementById("play-btn");
|
|
||||||
const catImg = document.getElementsByClassName("cat")[0];
|
|
||||||
const volumeSlider = document.getElementById("volume-slider");
|
|
||||||
const currentVolumeLabel = document.getElementById("current-volume-label");
|
|
||||||
const clickAudio = document.getElementById("click-audio");
|
|
||||||
const clickReleaseAudio = document.getElementById("click-release-audio");
|
|
||||||
|
|
||||||
const CROSSFADE_DURATION_MS = 5000;
|
const CROSSFADE_DURATION_MS = 5000;
|
||||||
const CROSSFADE_INTERVAL_MS = 20;
|
const CROSSFADE_INTERVAL_MS = 20;
|
||||||
const AUDIO_DURATION_MS = 60000;
|
const AUDIO_DURATION_MS = 60000;
|
||||||
|
const SAVE_VOLUME_TIMEOUT_MS = 200;
|
||||||
|
|
||||||
|
const ACHIEVEMENT_A_LITTLE_CHATTY = "a-little-chatty";
|
||||||
|
|
||||||
|
const playBtn = document.getElementById("play-btn");
|
||||||
|
const catImg = document.getElementById("cat");
|
||||||
|
const heartImg = document.getElementById("heart");
|
||||||
|
const volumeSlider = document.getElementById("volume-slider");
|
||||||
|
const currentVolumeLabel = document.getElementById("current-volume-label");
|
||||||
|
const listenerCountLabel = document.getElementById("listener-count");
|
||||||
|
const notificationContainer = document.getElementById("notification");
|
||||||
|
const notificationTitle = document.getElementById("notification-title");
|
||||||
|
const notificationBody = document.getElementById("notification-body");
|
||||||
|
|
||||||
|
const clickAudio = document.getElementById("click-audio");
|
||||||
|
const clickReleaseAudio = document.getElementById("click-release-audio");
|
||||||
|
const meowAudio = document.getElementById("meow-audio");
|
||||||
|
const achievementUnlockedAudio = document.getElementById(
|
||||||
|
"achievement-unlocked-audio",
|
||||||
|
);
|
||||||
|
|
||||||
let isPlaying = false;
|
let isPlaying = false;
|
||||||
let isFading = false;
|
let isFading = false;
|
||||||
let currentAudio;
|
let currentAudio;
|
||||||
let maxVolume = 100;
|
let maxVolume = 100;
|
||||||
let currentVolume = 0;
|
let currentVolume = 0;
|
||||||
|
let saveVolumeTimeout = null;
|
||||||
|
let meowCount = 0;
|
||||||
|
let ws = connectToWebSocket();
|
||||||
|
|
||||||
function playAudio() {
|
function playAudio() {
|
||||||
// add a random query parameter at the end to prevent browser caching
|
// add a random query parameter at the end to prevent browser caching
|
||||||
@@ -21,11 +37,17 @@ function playAudio() {
|
|||||||
currentAudio.onplay = () => {
|
currentAudio.onplay = () => {
|
||||||
isPlaying = true;
|
isPlaying = true;
|
||||||
playBtn.innerText = "pause";
|
playBtn.innerText = "pause";
|
||||||
|
if (ws) {
|
||||||
|
ws.send("playing");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
currentAudio.onpause = () => {
|
currentAudio.onpause = () => {
|
||||||
isPlaying = false;
|
isPlaying = false;
|
||||||
currentVolume = 0;
|
currentVolume = 0;
|
||||||
playBtn.innerText = "play";
|
playBtn.innerText = "play";
|
||||||
|
if (ws) {
|
||||||
|
ws.send("paused");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
currentAudio.onended = () => {
|
currentAudio.onended = () => {
|
||||||
currentVolume = 0;
|
currentVolume = 0;
|
||||||
@@ -121,6 +143,92 @@ function enableSpaceBarControl() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function connectToWebSocket() {
|
||||||
|
const ws = new WebSocket(
|
||||||
|
`${location.protocol === "https:" ? "wss:" : "ws:"}//${location.host}/ws`,
|
||||||
|
);
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
console.log(event.data);
|
||||||
|
|
||||||
|
if (typeof event.data !== "string") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const listenerCountStr = event.data;
|
||||||
|
const listenerCount = Number.parseInt(listenerCountStr);
|
||||||
|
if (Number.isNaN(listenerCount)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listenerCount <= 1) {
|
||||||
|
listenerCountLabel.innerText = `${listenerCount} person tuned in`;
|
||||||
|
} else {
|
||||||
|
listenerCountLabel.innerText = `${listenerCount} ppl tuned in`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return ws;
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeVolume(volume) {
|
||||||
|
maxVolume = volume;
|
||||||
|
const v = maxVolume / 100;
|
||||||
|
|
||||||
|
currentVolumeLabel.textContent = `${maxVolume}%`;
|
||||||
|
|
||||||
|
if (!isFading && currentAudio) {
|
||||||
|
currentAudio.volume = v;
|
||||||
|
currentVolume = maxVolume;
|
||||||
|
}
|
||||||
|
|
||||||
|
clickAudio.volume = v;
|
||||||
|
clickReleaseAudio.volume = v;
|
||||||
|
meowAudio.volume = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadInitialVolume() {
|
||||||
|
const savedVolume = localStorage.getItem("volume");
|
||||||
|
let volume = 100;
|
||||||
|
if (savedVolume) {
|
||||||
|
volume = Number.parseInt(savedVolume);
|
||||||
|
if (Number.isNaN(volume)) {
|
||||||
|
volume = 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
volumeSlider.value = volume;
|
||||||
|
changeVolume(volume);
|
||||||
|
|
||||||
|
document.getElementById("volume-slider-container").style.display = "flex";
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMeowCount() {
|
||||||
|
const lastMeowCount = localStorage.getItem("meowCount");
|
||||||
|
if (!lastMeowCount) {
|
||||||
|
meowCount = 0;
|
||||||
|
} else {
|
||||||
|
const n = Number.parseInt(lastMeowCount);
|
||||||
|
if (Number.isNaN(n)) {
|
||||||
|
meowCount = 0;
|
||||||
|
} else {
|
||||||
|
meowCount += n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNotification(title, content, duration) {
|
||||||
|
notificationTitle.innerText = title;
|
||||||
|
notificationBody.innerText = content;
|
||||||
|
notificationContainer.style.display = "block";
|
||||||
|
notificationContainer.style.animation = "0.5s linear 0s notification-fade-in";
|
||||||
|
setTimeout(() => {
|
||||||
|
notificationContainer.style.animation =
|
||||||
|
"0.5s linear 0s notification-fade-out";
|
||||||
|
setTimeout(() => {
|
||||||
|
notificationContainer.style.display = "none";
|
||||||
|
}, 450);
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
|
||||||
playBtn.onmousedown = () => {
|
playBtn.onmousedown = () => {
|
||||||
clickAudio.play();
|
clickAudio.play();
|
||||||
document.addEventListener(
|
document.addEventListener(
|
||||||
@@ -132,6 +240,10 @@ playBtn.onmousedown = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
catImg.onmousedown = () => {
|
||||||
|
meowAudio.play();
|
||||||
|
};
|
||||||
|
|
||||||
playBtn.onclick = () => {
|
playBtn.onclick = () => {
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
pauseAudio();
|
pauseAudio();
|
||||||
@@ -141,16 +253,50 @@ playBtn.onclick = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
volumeSlider.oninput = () => {
|
volumeSlider.oninput = () => {
|
||||||
maxVolume = volumeSlider.value;
|
const volume = volumeSlider.value;
|
||||||
currentVolumeLabel.textContent = `${maxVolume}%`;
|
changeVolume(volume);
|
||||||
if (!isFading && currentAudio) {
|
if (saveVolumeTimeout) {
|
||||||
currentAudio.volume = maxVolume / 100;
|
clearTimeout(saveVolumeTimeout);
|
||||||
currentVolume = maxVolume;
|
}
|
||||||
|
saveVolumeTimeout = setTimeout(() => {
|
||||||
|
localStorage.setItem("volume", `${volume}`);
|
||||||
|
}, SAVE_VOLUME_TIMEOUT_MS);
|
||||||
|
};
|
||||||
|
volumeSlider.value = 100;
|
||||||
|
|
||||||
|
meowAudio.onplay = () => {
|
||||||
|
heartImg.style.display = "block";
|
||||||
|
heartImg.style.animation = "1s linear 0s heart-animation";
|
||||||
|
setTimeout(() => {
|
||||||
|
heartImg.style.display = "none";
|
||||||
|
heartImg.style.animation = "";
|
||||||
|
}, 900);
|
||||||
|
|
||||||
|
meowCount += 1;
|
||||||
|
localStorage.setItem("meowCount", `${meowCount}`);
|
||||||
|
|
||||||
|
if (meowCount === 100) {
|
||||||
|
showNotification("a little chatty", "make milo meow 100 times", 5000);
|
||||||
|
achievementUnlockedAudio.play();
|
||||||
|
localStorage.setItem(
|
||||||
|
"achievements",
|
||||||
|
JSON.stringify([ACHIEVEMENT_A_LITTLE_CHATTY]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
clickAudio.volume = volumeSlider.value / 100;
|
|
||||||
clickReleaseAudio.volume = volumeSlider.value / 100;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
volumeSlider.value = 100;
|
// don't wanna jumpscare ppl
|
||||||
|
achievementUnlockedAudio.volume = 0.05;
|
||||||
|
|
||||||
|
window.addEventListener("offline", () => {
|
||||||
|
ws = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("online", () => {
|
||||||
|
ws = connectToWebSocket();
|
||||||
|
});
|
||||||
|
|
||||||
|
loadMeowCount();
|
||||||
|
loadInitialVolume();
|
||||||
animateCat();
|
animateCat();
|
||||||
enableSpaceBarControl();
|
enableSpaceBarControl();
|
||||||
|
161
web/style.css
161
web/style.css
@@ -1,6 +1,7 @@
|
|||||||
:root {
|
:root {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
--text: #4c4f69;
|
--text: #4c4f69;
|
||||||
|
--surface0: #ccd0da;
|
||||||
--surface1: #bcc0cc;
|
--surface1: #bcc0cc;
|
||||||
--base: #eff1f5;
|
--base: #eff1f5;
|
||||||
--lavender: #7287fd;
|
--lavender: #7287fd;
|
||||||
@@ -31,6 +32,7 @@ body {
|
|||||||
body {
|
body {
|
||||||
width: calc(100% - 4rem - 2px);
|
width: calc(100% - 4rem - 2px);
|
||||||
height: calc(100vh - 6rem - 2px);
|
height: calc(100vh - 6rem - 2px);
|
||||||
|
height: calc(100svh - 6rem - 2px);
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: var(--base);
|
background-color: var(--base);
|
||||||
@@ -40,6 +42,7 @@ body {
|
|||||||
body {
|
body {
|
||||||
width: calc(100% - 8rem - 2px);
|
width: calc(100% - 8rem - 2px);
|
||||||
height: calc(100vh - 10rem - 2px);
|
height: calc(100vh - 10rem - 2px);
|
||||||
|
height: calc(100svh - 10rem - 2px);
|
||||||
padding: 4rem;
|
padding: 4rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,32 +111,31 @@ a {
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-bar {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: -10;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 2rem;
|
||||||
|
z-index: 0;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar > #listener-count {
|
||||||
|
margin: 0;
|
||||||
|
opacity: 0.8;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cat {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0px;
|
|
||||||
right: 2rem;
|
|
||||||
image-rendering: pixelated;
|
|
||||||
height: 30px;
|
|
||||||
}
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.cat {
|
|
||||||
right: 4rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.eeping-cat {
|
|
||||||
position: absolute;
|
|
||||||
image-rendering: pixelated;
|
|
||||||
top: -16px;
|
|
||||||
right: 50%;
|
|
||||||
height: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
main {
|
main {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
@@ -151,22 +153,6 @@ a {
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.volume-slider-container {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
margin: 1rem;
|
|
||||||
display: flex;
|
|
||||||
justify-content: start;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.volume-slider-container > output {
|
|
||||||
color: var(--text);
|
|
||||||
margin-right: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (-webkit-min-device-pixel-ratio: 0) {
|
@media screen and (-webkit-min-device-pixel-ratio: 0) {
|
||||||
input[type="range"] {
|
input[type="range"] {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -291,3 +277,104 @@ input[type="range"]::-ms-fill-upper {
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#cat {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
right: 2rem;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
#cat {
|
||||||
|
right: 4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#heart {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 24px;
|
||||||
|
right: 2rem;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
#heart {
|
||||||
|
right: 4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes heart-animation {
|
||||||
|
0% {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: translate(0, -24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(0, -30px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#eeping-cat {
|
||||||
|
position: absolute;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
top: -16px;
|
||||||
|
right: 50%;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#volume-slider-container {
|
||||||
|
display: none;
|
||||||
|
justify-content: start;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#volume-slider-container > output {
|
||||||
|
color: var(--text);
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside#notification {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
background: var(--surface0);
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
margin: 2rem;
|
||||||
|
border: 2px solid var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
aside#notification > p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside#notification > #notification-title {
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes notification-fade-in {
|
||||||
|
0% {
|
||||||
|
display: block;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes notification-fade-out {
|
||||||
|
0% {
|
||||||
|
opactiy: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
25
websocket_connection_manager.py
Normal file
25
websocket_connection_manager.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import asyncio
|
||||||
|
from fastapi import WebSocket
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketConnectionManager:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.__active_connections: list[WebSocket] = []
|
||||||
|
|
||||||
|
async def connect(self, ws: WebSocket):
|
||||||
|
await ws.accept()
|
||||||
|
self.__active_connections.append(ws)
|
||||||
|
|
||||||
|
def disconnect(self, ws: WebSocket):
|
||||||
|
self.__active_connections.remove(ws)
|
||||||
|
|
||||||
|
async def send_text(self, ws: WebSocket, msg: str):
|
||||||
|
try:
|
||||||
|
await ws.send_text(msg)
|
||||||
|
except:
|
||||||
|
self.__active_connections.remove(ws)
|
||||||
|
|
||||||
|
async def broadcast(self, msg: str):
|
||||||
|
await asyncio.gather(
|
||||||
|
*[self.send_text(conn, msg) for conn in self.__active_connections]
|
||||||
|
)
|
Reference in New Issue
Block a user