mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 21:26:33 +00:00
fix: use glib channel for thread→UI communication (Send safety)
This commit is contained in:
parent
c4315917d8
commit
df231344a8
1 changed files with 51 additions and 92 deletions
141
kiosk/src/ui.rs
141
kiosk/src/ui.rs
|
|
@ -3,7 +3,6 @@ use std::rc::Rc;
|
||||||
|
|
||||||
use gtk4::prelude::*;
|
use gtk4::prelude::*;
|
||||||
use gtk4::{self as gtk, Application, ApplicationWindow, Box as GtkBox, Grid, Label, Orientation, Picture};
|
use gtk4::{self as gtk, Application, ApplicationWindow, Box as GtkBox, Grid, Label, Orientation, Picture};
|
||||||
use gstreamer::prelude::*;
|
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
use crate::bundle::KioskBundle;
|
use crate::bundle::KioskBundle;
|
||||||
|
|
@ -25,7 +24,6 @@ fn activate(app: &Application) {
|
||||||
.fullscreened(true)
|
.fullscreened(true)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Dark background
|
|
||||||
let provider = gtk::CssProvider::new();
|
let provider = gtk::CssProvider::new();
|
||||||
provider.load_from_string("window { background-color: #1a1a2e; }");
|
provider.load_from_string("window { background-color: #1a1a2e; }");
|
||||||
gtk::style_context_add_provider_for_display(
|
gtk::style_context_add_provider_for_display(
|
||||||
|
|
@ -34,17 +32,15 @@ fn activate(app: &Application) {
|
||||||
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||||
);
|
);
|
||||||
|
|
||||||
let container = GtkBox::new(Orientation::Vertical, 0);
|
// Start with logo
|
||||||
container.set_vexpand(true);
|
show_logo(&window);
|
||||||
container.set_hexpand(true);
|
window.present();
|
||||||
window.set_child(Some(&container));
|
|
||||||
|
|
||||||
// Show pairing or camera display
|
// Channel to send results from worker thread to UI
|
||||||
|
let (tx, rx) = gtk::glib::MainContext::channel::<WorkerMsg>(gtk::glib::Priority::DEFAULT);
|
||||||
|
|
||||||
|
// Worker thread — blocking HTTP calls
|
||||||
let server_url = std::env::args().nth(1);
|
let server_url = std::env::args().nth(1);
|
||||||
let container_clone = container.clone();
|
|
||||||
let window_clone = window.clone();
|
|
||||||
|
|
||||||
// Run server interaction in a thread, update UI via idle_add
|
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let server = server::discover_server(server_url.as_deref());
|
let server = server::discover_server(server_url.as_deref());
|
||||||
info!("server: {server}");
|
info!("server: {server}");
|
||||||
|
|
@ -55,90 +51,65 @@ fn activate(app: &Application) {
|
||||||
} else {
|
} else {
|
||||||
let (code, expires) = server::initiate_pairing(&server);
|
let (code, expires) = server::initiate_pairing(&server);
|
||||||
info!("pairing code: {code} (expires {expires})");
|
info!("pairing code: {code} (expires {expires})");
|
||||||
|
let _ = tx.send(WorkerMsg::ShowPairingCode(code.clone()));
|
||||||
// Show pairing code on UI
|
|
||||||
let code_clone = code.clone();
|
|
||||||
gtk::glib::idle_add_once(move || {
|
|
||||||
show_pairing_code(&container_clone, &code_clone);
|
|
||||||
});
|
|
||||||
|
|
||||||
let (name, key) = server::poll_claim(&server, &code);
|
let (name, key) = server::poll_claim(&server, &code);
|
||||||
info!("paired as: {name}");
|
info!("paired as: {name}");
|
||||||
key
|
key
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch bundle
|
|
||||||
let bundle = server::fetch_bundle(&server, &key);
|
let bundle = server::fetch_bundle(&server, &key);
|
||||||
info!("bundle: {} cameras, {} layouts", bundle.cameras.len(), bundle.layouts.len());
|
info!("bundle: {} cameras, {} layouts", bundle.cameras.len(), bundle.layouts.len());
|
||||||
|
let _ = tx.send(WorkerMsg::RenderBundle(bundle));
|
||||||
// Render layout on UI thread
|
|
||||||
let server_clone = server.clone();
|
|
||||||
let key_clone = key.clone();
|
|
||||||
gtk::glib::idle_add_once(move || {
|
|
||||||
render_bundle(&window_clone, bundle);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Heartbeat loop
|
// Heartbeat loop
|
||||||
loop {
|
loop {
|
||||||
std::thread::sleep(std::time::Duration::from_secs(60));
|
std::thread::sleep(std::time::Duration::from_secs(60));
|
||||||
server::heartbeat(&server_clone, &key_clone);
|
server::heartbeat(&server, &key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.present();
|
// Receive messages on UI thread
|
||||||
|
let window_clone = window.clone();
|
||||||
|
rx.attach(None, move |msg| {
|
||||||
|
match msg {
|
||||||
|
WorkerMsg::ShowPairingCode(code) => show_pairing_code(&window_clone, &code),
|
||||||
|
WorkerMsg::RenderBundle(bundle) => render_bundle(&window_clone, bundle),
|
||||||
|
}
|
||||||
|
gtk::glib::ControlFlow::Continue
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_pairing_code(container: &GtkBox, code: &str) {
|
enum WorkerMsg {
|
||||||
// Clear existing children
|
ShowPairingCode(String),
|
||||||
while let Some(child) = container.first_child() {
|
RenderBundle(KioskBundle),
|
||||||
container.remove(&child);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn show_pairing_code(window: &ApplicationWindow, code: &str) {
|
||||||
let vbox = GtkBox::new(Orientation::Vertical, 20);
|
let vbox = GtkBox::new(Orientation::Vertical, 20);
|
||||||
vbox.set_valign(gtk::Align::Center);
|
vbox.set_valign(gtk::Align::Center);
|
||||||
vbox.set_halign(gtk::Align::Center);
|
vbox.set_halign(gtk::Align::Center);
|
||||||
vbox.set_vexpand(true);
|
vbox.set_vexpand(true);
|
||||||
|
|
||||||
let title = Label::new(Some("BetterFrame"));
|
let title = Label::new(Some("BetterFrame"));
|
||||||
|
add_css(&title, ".title { font-size: 24px; color: #888; font-weight: 300; }");
|
||||||
title.add_css_class("title");
|
title.add_css_class("title");
|
||||||
let title_provider = gtk::CssProvider::new();
|
|
||||||
title_provider.load_from_string(".title { font-size: 24px; color: #888; font-weight: 300; }");
|
|
||||||
gtk::style_context_add_provider_for_display(
|
|
||||||
&title.display(),
|
|
||||||
&title_provider,
|
|
||||||
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
|
||||||
);
|
|
||||||
|
|
||||||
let code_label = Label::new(Some(code));
|
let code_label = Label::new(Some(code));
|
||||||
code_label.add_css_class("pairing-code");
|
add_css(&code_label, ".code { font-size: 72px; color: #fff; font-weight: 700; letter-spacing: 12px; font-family: monospace; }");
|
||||||
let code_provider = gtk::CssProvider::new();
|
code_label.add_css_class("code");
|
||||||
code_provider.load_from_string(
|
|
||||||
".pairing-code { font-size: 72px; color: #fff; font-weight: 700; letter-spacing: 12px; font-family: monospace; }",
|
|
||||||
);
|
|
||||||
gtk::style_context_add_provider_for_display(
|
|
||||||
&code_label.display(),
|
|
||||||
&code_provider,
|
|
||||||
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
|
||||||
);
|
|
||||||
|
|
||||||
let hint = Label::new(Some("Enter this code in BetterFrame admin to pair"));
|
let hint = Label::new(Some("Enter this code in BetterFrame admin to pair"));
|
||||||
|
add_css(&hint, ".hint { font-size: 14px; color: #666; }");
|
||||||
hint.add_css_class("hint");
|
hint.add_css_class("hint");
|
||||||
let hint_provider = gtk::CssProvider::new();
|
|
||||||
hint_provider.load_from_string(".hint { font-size: 14px; color: #666; }");
|
|
||||||
gtk::style_context_add_provider_for_display(
|
|
||||||
&hint.display(),
|
|
||||||
&hint_provider,
|
|
||||||
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
|
||||||
);
|
|
||||||
|
|
||||||
vbox.append(&title);
|
vbox.append(&title);
|
||||||
vbox.append(&code_label);
|
vbox.append(&code_label);
|
||||||
vbox.append(&hint);
|
vbox.append(&hint);
|
||||||
container.append(&vbox);
|
window.set_child(Some(&vbox));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_bundle(window: &ApplicationWindow, bundle: KioskBundle) {
|
fn render_bundle(window: &ApplicationWindow, bundle: KioskBundle) {
|
||||||
// Find default layout
|
|
||||||
let layout = bundle.layouts.iter()
|
let layout = bundle.layouts.iter()
|
||||||
.find(|l| l.is_default)
|
.find(|l| l.is_default)
|
||||||
.or_else(|| bundle.layouts.first());
|
.or_else(|| bundle.layouts.first());
|
||||||
|
|
@ -164,14 +135,12 @@ fn render_bundle(window: &ApplicationWindow, bundle: KioskBundle) {
|
||||||
grid.set_vexpand(true);
|
grid.set_vexpand(true);
|
||||||
grid.set_hexpand(true);
|
grid.set_hexpand(true);
|
||||||
|
|
||||||
// Map cameras by ID for quick lookup
|
|
||||||
let cam_map: std::collections::HashMap<u32, &crate::bundle::BundleCamera> =
|
let cam_map: std::collections::HashMap<u32, &crate::bundle::BundleCamera> =
|
||||||
bundle.cameras.iter().map(|c| (c.id, c)).collect();
|
bundle.cameras.iter().map(|c| (c.id, c)).collect();
|
||||||
|
|
||||||
let pipelines: Rc<RefCell<Vec<gstreamer::Pipeline>>> = Rc::new(RefCell::new(Vec::new()));
|
let pipelines: Rc<RefCell<Vec<gstreamer::Pipeline>>> = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
|
||||||
for cell in &layout.cells {
|
for cell in &layout.cells {
|
||||||
// Find region in template
|
|
||||||
let region = template.regions.iter().find(|r| r.name == cell.region_name);
|
let region = template.regions.iter().find(|r| r.name == cell.region_name);
|
||||||
let Some(region) = region else {
|
let Some(region) = region else {
|
||||||
warn!("region '{}' not found in template", cell.region_name);
|
warn!("region '{}' not found in template", cell.region_name);
|
||||||
|
|
@ -194,33 +163,31 @@ fn render_bundle(window: &ApplicationWindow, bundle: KioskBundle) {
|
||||||
pipelines.borrow_mut().push(pipe);
|
pipelines.borrow_mut().push(pipe);
|
||||||
picture.upcast()
|
picture.upcast()
|
||||||
}
|
}
|
||||||
None => placeholder_label(&format!("{} (pipeline error)", cam.name)),
|
None => placeholder(&format!("{} (pipeline error)", cam.name)),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
placeholder_label(&format!("{} (no stream)", cam.name))
|
placeholder(&format!("{} (no stream)", cam.name))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
placeholder_label("Unknown camera")
|
placeholder("Unknown camera")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
placeholder_label("No camera assigned")
|
placeholder("No camera assigned")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"html" => {
|
"html" => {
|
||||||
let html = cell.html_content.as_deref().unwrap_or("");
|
let html = cell.html_content.as_deref().unwrap_or("HTML");
|
||||||
// For HTML cells, show a label placeholder (WebKit integration later)
|
let label = Label::new(Some(&html.chars().take(100).collect::<String>()));
|
||||||
let label = Label::new(Some("HTML Content"));
|
add_css(&label, "label { color: #888; background-color: #111; }");
|
||||||
label.set_markup(&format!("<span color='#888'>{}</span>",
|
|
||||||
gtk::glib::markup_escape_text(html).chars().take(100).collect::<String>()));
|
|
||||||
label.set_vexpand(true);
|
label.set_vexpand(true);
|
||||||
label.set_hexpand(true);
|
label.set_hexpand(true);
|
||||||
label.upcast()
|
label.upcast()
|
||||||
}
|
}
|
||||||
"web" => {
|
"web" => {
|
||||||
let url = cell.web_url.as_deref().unwrap_or("about:blank");
|
let url = cell.web_url.as_deref().unwrap_or("about:blank");
|
||||||
placeholder_label(&format!("Web: {url}"))
|
placeholder(&format!("Web: {url}"))
|
||||||
}
|
}
|
||||||
_ => placeholder_label("Unknown content"),
|
_ => placeholder("Unknown content"),
|
||||||
};
|
};
|
||||||
|
|
||||||
grid.attach(
|
grid.attach(
|
||||||
|
|
@ -232,17 +199,11 @@ fn render_bundle(window: &ApplicationWindow, bundle: KioskBundle) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill empty regions with dark placeholders
|
// Fill empty regions
|
||||||
for region in &template.regions {
|
for region in &template.regions {
|
||||||
if !layout.cells.iter().any(|c| c.region_name == region.name) {
|
if !layout.cells.iter().any(|c| c.region_name == region.name) {
|
||||||
let empty = GtkBox::new(Orientation::Vertical, 0);
|
let empty = GtkBox::new(Orientation::Vertical, 0);
|
||||||
let empty_provider = gtk::CssProvider::new();
|
add_css(&empty, "box { background-color: #111; }");
|
||||||
empty_provider.load_from_string("box { background-color: #111; }");
|
|
||||||
gtk::style_context_add_provider_for_display(
|
|
||||||
&empty.display(),
|
|
||||||
&empty_provider,
|
|
||||||
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
|
||||||
);
|
|
||||||
empty.set_vexpand(true);
|
empty.set_vexpand(true);
|
||||||
empty.set_hexpand(true);
|
empty.set_hexpand(true);
|
||||||
grid.attach(
|
grid.attach(
|
||||||
|
|
@ -257,9 +218,9 @@ fn render_bundle(window: &ApplicationWindow, bundle: KioskBundle) {
|
||||||
|
|
||||||
window.set_child(Some(&grid));
|
window.set_child(Some(&grid));
|
||||||
|
|
||||||
// Store pipelines so they don't get dropped
|
let pipelines_ref = pipelines.clone();
|
||||||
window.connect_destroy(move |_| {
|
window.connect_destroy(move |_| {
|
||||||
for pipe in pipelines.borrow().iter() {
|
for pipe in pipelines_ref.borrow().iter() {
|
||||||
pipeline::stop(pipe);
|
pipeline::stop(pipe);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -267,29 +228,27 @@ fn render_bundle(window: &ApplicationWindow, bundle: KioskBundle) {
|
||||||
|
|
||||||
fn show_logo(window: &ApplicationWindow) {
|
fn show_logo(window: &ApplicationWindow) {
|
||||||
let label = Label::new(Some("BetterFrame"));
|
let label = Label::new(Some("BetterFrame"));
|
||||||
let provider = gtk::CssProvider::new();
|
add_css(&label, "label { font-size: 48px; color: #fff; font-weight: 300; }");
|
||||||
provider.load_from_string("label { font-size: 48px; color: #fff; font-weight: 300; }");
|
|
||||||
gtk::style_context_add_provider_for_display(
|
|
||||||
&label.display(),
|
|
||||||
&provider,
|
|
||||||
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
|
||||||
);
|
|
||||||
label.set_valign(gtk::Align::Center);
|
label.set_valign(gtk::Align::Center);
|
||||||
label.set_halign(gtk::Align::Center);
|
label.set_halign(gtk::Align::Center);
|
||||||
label.set_vexpand(true);
|
label.set_vexpand(true);
|
||||||
window.set_child(Some(&label));
|
window.set_child(Some(&label));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn placeholder_label(text: &str) -> gtk::Widget {
|
fn placeholder(text: &str) -> gtk::Widget {
|
||||||
let label = Label::new(Some(text));
|
let label = Label::new(Some(text));
|
||||||
|
add_css(&label, "label { color: #666; font-size: 14px; background-color: #111; }");
|
||||||
label.set_vexpand(true);
|
label.set_vexpand(true);
|
||||||
label.set_hexpand(true);
|
label.set_hexpand(true);
|
||||||
|
label.upcast()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_css(widget: &impl IsA<gtk::Widget>, css: &str) {
|
||||||
let provider = gtk::CssProvider::new();
|
let provider = gtk::CssProvider::new();
|
||||||
provider.load_from_string("label { color: #666; font-size: 14px; background-color: #111; }");
|
provider.load_from_string(css);
|
||||||
gtk::style_context_add_provider_for_display(
|
gtk::style_context_add_provider_for_display(
|
||||||
&label.display(),
|
&widget.display(),
|
||||||
&provider,
|
&provider,
|
||||||
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||||
);
|
);
|
||||||
label.upcast()
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue