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

61
flake.lock generated Normal file
View file

@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1759831965,
"narHash": "sha256-vgPm2xjOmKdZ0xKA6yLXPJpjOtQPHfaZDRtH+47XEBo=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "c9b6fb798541223bbb396d287d16f43520250518",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

40
flake.nix Normal file
View file

@ -0,0 +1,40 @@
{
description = "Jukebox";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { nixpkgs, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system:
let pkgs = nixpkgs.legacyPackages.${system};
in {
# nixosConfigurations.jukebox = nixpkgs.lib.nixosSystem {
# inherit system;
# modules = [ ./system/configuration.nix ];
# };
packages.jukebox = pkgs.rustPlatform.buildRustPackage {
name = "jukebox";
src = ./app;
cargoLock.lockFile = ./app/Cargo.lock;
};
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
atkmm
cairo
cargo
gdk-pixbuf
glib
gtk3
just
openscad-unstable
openssl
pango
pkg-config
rustc
rustPlatform.bindgenHook
webkitgtk_4_1
xdotool
];
};
});
}

View file

@ -1,6 +0,0 @@
mod models
default: build
build:
@just models/build