Create initial interface app

This commit is contained in:
Nettika 2025-10-09 13:54:29 -07:00
parent 5210c0a5e6
commit 2e1cd7dc16
9 changed files with 3100 additions and 6 deletions

1
app/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

2762
app/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

12
app/Cargo.toml Normal file
View file

@ -0,0 +1,12 @@
[package]
name = "jukebox"
version = "0.1.0"
edition = "2024"
[dependencies]
base64 = "0.22.1"
maud = "0.27.0"
serde = "1.0.228"
serde_json = "1.0.145"
tao = { version = "0.34.3", default-features = false, features = ["rwh_06"] }
wry = { version = "0.53.3", default-features = false, features = ["os-webview", "protocol"] }

19
app/src/app.js Normal file
View file

@ -0,0 +1,19 @@
function removePlaying() {
for (const el of document.getElementsByClassName("playing")) {
el.classList.add("fadeout");
el.addEventListener("transitionend", () => el.remove());
}
}
function showPlaying({ cover, title, artist, album }) {
removePlaying();
const container = document.createElement("div");
container.classList.add("playing");
container.innerHTML = `
<img class="cover" src="${cover}">
<span class="title">${title}</span>
<span class="artist">${artist}</span>
<span class="album">${album}</span>
`;
document.body.appendChild(container);
}

134
app/src/main.rs Normal file
View file

@ -0,0 +1,134 @@
use std::{thread, time::Duration};
use base64::prelude::*;
use maud::{Markup, PreEscaped, html};
use serde::Serialize;
use tao::{
event::Event,
event_loop::{ControlFlow, EventLoopBuilder},
platform::unix::WindowExtUnix,
window::{Fullscreen, WindowBuilder},
};
use wry::{WebViewBuilder, WebViewBuilderExtUnix};
static STYLESHEET: &str = include_str!("stylesheet.css");
static SCRIPT: &str = include_str!("app.js");
static RALEWAY: &[u8] = include_bytes!("Raleway.ttf");
static RALEWAY_ITALIC: &[u8] = include_bytes!("Raleway-Italic.ttf");
static ALBUM_COVER_1: &[u8] = include_bytes!("album01.jpg");
static ALBUM_COVER_2: &[u8] = include_bytes!("album02.jpg");
#[derive(Debug, Serialize)]
#[serde(untagged)]
enum JukeboxEvent {
ShowPlaying {
cover: String,
title: String,
artist: String,
album: String,
},
RemovePlaying,
}
impl JukeboxEvent {
fn get_invocation_name(&self) -> &'static str {
match self {
JukeboxEvent::ShowPlaying { .. } => "showPlaying",
JukeboxEvent::RemovePlaying => "removePlaying",
}
}
fn get_invocation(&self) -> String {
return format!(
"{}({});",
self.get_invocation_name(),
serde_json::to_string(&self).unwrap()
);
}
}
fn main() {
let event_loop = EventLoopBuilder::<JukeboxEvent>::with_user_event().build();
let proxy = event_loop.create_proxy();
let window = WindowBuilder::new()
.with_fullscreen(Some(Fullscreen::Borderless(None)))
.build(&event_loop)
.unwrap();
let vbox = window.default_vbox().unwrap();
let webview = WebViewBuilder::new()
.with_html(webapp())
.with_focused(true)
.build_gtk(vbox)
.unwrap();
thread::spawn(move || {
thread::sleep(Duration::from_secs(2));
loop {
proxy
.send_event(JukeboxEvent::ShowPlaying {
cover: build_data_url("image/jpeg", ALBUM_COVER_1),
title: String::from("The Great Gig in the Sky"),
artist: String::from("Pink Floyd"),
album: String::from("The Dark Side of the Moon"),
})
.unwrap();
thread::sleep(Duration::from_secs(3));
proxy.send_event(JukeboxEvent::RemovePlaying).unwrap();
thread::sleep(Duration::from_secs(3));
proxy
.send_event(JukeboxEvent::ShowPlaying {
cover: build_data_url("image/jpeg", ALBUM_COVER_2),
title: String::from("Boulevard of Broken Dreams"),
artist: String::from("Green Day"),
album: String::from("American Idiot"),
})
.unwrap();
thread::sleep(Duration::from_secs(3));
}
});
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::UserEvent(jukebox_event) => {
let js = jukebox_event.get_invocation();
webview.evaluate_script(&js).unwrap();
}
_ => {}
}
});
}
fn build_data_url(mimetype: &str, input: impl AsRef<[u8]>) -> String {
format!(
"data:{};base64,{}",
mimetype,
BASE64_STANDARD.encode(&input)
)
}
fn webapp() -> Markup {
let b64 = build_data_url("font/ttf;charset=utf-8", RALEWAY);
let b64_italic = build_data_url("font/ttf;charset=utf-8", RALEWAY_ITALIC);
let fontface_defs = format!(
r#"
@font-face {{
font-family: 'Raleway';
src: url('{b64}') format('truetype');
font-style: normal;
}}
@font-face {{
font-family: 'Raleway';
src: url('{b64_italic}') format('truetype');
font-style: italic;
}}
"#
);
html! {
style { (PreEscaped(fontface_defs)) }
style { (PreEscaped(STYLESHEET)) }
script { (PreEscaped(SCRIPT)) }
}
}

71
app/src/stylesheet.css Normal file
View file

@ -0,0 +1,71 @@
:root {
background-color: #456798;
color: white;
font-family: 'Raleway';
user-select: none;
overflow: hidden;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
@keyframes show-playing {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes remove-playing {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(-20px);
}
}
.playing {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
gap: 0.4rem;
align-items: center;
justify-content: center;
animation: show-playing 0.3s ease-out forwards;
&.fadeout {
animation: remove-playing 0.3s ease-out forwards;
}
span {
text-shadow: 4px 4px 4px #0000001a;
}
.cover {
width: 400px;
border-radius: 4px;
box-shadow: 8px 8px 8px #0000003a;
margin-bottom: 0.6rem;
}
.title {
font-size: 2rem;
font-weight: 450;
}
.artist {
font-size: 1.2rem;
}
.album {
font-style: italic;
}
}