Create initial interface app
This commit is contained in:
parent
5210c0a5e6
commit
2e1cd7dc16
9 changed files with 3100 additions and 6 deletions
1
app/.gitignore
vendored
Normal file
1
app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/target
|
||||||
2762
app/Cargo.lock
generated
Normal file
2762
app/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
12
app/Cargo.toml
Normal file
12
app/Cargo.toml
Normal 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
19
app/src/app.js
Normal 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
134
app/src/main.rs
Normal 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
71
app/src/stylesheet.css
Normal 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
61
flake.lock
generated
Normal 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
40
flake.nix
Normal 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
|
||||||
|
];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
6
justfile
6
justfile
|
|
@ -1,6 +0,0 @@
|
||||||
mod models
|
|
||||||
|
|
||||||
default: build
|
|
||||||
|
|
||||||
build:
|
|
||||||
@just models/build
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue