Cliente remoto GTK4 para G Radio Player: se conecta al servidor TCP embebido en radio-player para controlar el reproductor y editar programación (pautaje, parrilla, botonera, buscador, reportes) desde otra máquina de la LAN.
211 lines
6.9 KiB
Rust
211 lines
6.9 KiB
Rust
// panel_botonera.rs — Botonera remota
|
|
|
|
use gtk4::prelude::*;
|
|
use gtk4::{
|
|
Align, Box as GtkBox, Button, Entry, FlowBox, Label, Orientation, ScrolledWindow, Separator,
|
|
};
|
|
use std::cell::RefCell;
|
|
use std::rc::Rc;
|
|
|
|
use crate::conexion::Conexion;
|
|
use crate::protocol::{BotonRemoto, MsgCliente};
|
|
|
|
#[derive(Clone)]
|
|
pub struct PanelBotonera {
|
|
pub root: GtkBox,
|
|
flow: FlowBox,
|
|
conexion: Rc<RefCell<Option<Conexion>>>,
|
|
botones: Rc<RefCell<Vec<BotonRemoto>>>,
|
|
}
|
|
|
|
impl PanelBotonera {
|
|
pub fn nuevo(conexion: &Rc<RefCell<Option<Conexion>>>) -> Self {
|
|
let root = GtkBox::new(Orientation::Vertical, 4);
|
|
root.set_margin_top(6);
|
|
root.set_margin_bottom(6);
|
|
root.set_margin_start(6);
|
|
root.set_margin_end(6);
|
|
|
|
let toolbar = GtkBox::new(Orientation::Horizontal, 6);
|
|
let btn_cargar = Button::with_label("⟳ Cargar");
|
|
let btn_guardar = Button::with_label("💾 Guardar");
|
|
btn_guardar.add_css_class("suggested-action");
|
|
toolbar.append(&btn_cargar);
|
|
toolbar.append(&btn_guardar);
|
|
root.append(&toolbar);
|
|
root.append(&Separator::new(Orientation::Horizontal));
|
|
|
|
let lbl = Label::new(Some(
|
|
"Clic → reproducir en servidor. Clic derecho → editar botón.",
|
|
));
|
|
lbl.add_css_class("dim-label");
|
|
lbl.set_margin_top(8);
|
|
lbl.set_margin_bottom(8);
|
|
root.append(&lbl);
|
|
|
|
let flow = FlowBox::new();
|
|
flow.set_max_children_per_line(8);
|
|
flow.set_min_children_per_line(4);
|
|
flow.set_selection_mode(gtk4::SelectionMode::None);
|
|
flow.set_homogeneous(true);
|
|
flow.set_row_spacing(4);
|
|
flow.set_column_spacing(4);
|
|
flow.set_margin_top(8);
|
|
flow.set_margin_bottom(8);
|
|
flow.set_margin_start(8);
|
|
flow.set_margin_end(8);
|
|
|
|
let scroll = ScrolledWindow::new();
|
|
scroll.set_vexpand(true);
|
|
scroll.set_child(Some(&flow));
|
|
root.append(&scroll);
|
|
|
|
let botones: Rc<RefCell<Vec<BotonRemoto>>> = Rc::new(RefCell::new(Vec::new()));
|
|
|
|
{
|
|
let conexion2 = conexion.clone();
|
|
btn_cargar.connect_clicked(move |_| {
|
|
if let Some(ref con) = *conexion2.borrow() {
|
|
con.enviar(MsgCliente::ObtenerBotonera);
|
|
}
|
|
});
|
|
}
|
|
{
|
|
let botones2 = botones.clone();
|
|
let conexion2 = conexion.clone();
|
|
btn_guardar.connect_clicked(move |_| {
|
|
let lista = botones2.borrow().clone();
|
|
if let Some(ref con) = *conexion2.borrow() {
|
|
con.enviar(MsgCliente::GuardarBotonera { botones: lista });
|
|
}
|
|
});
|
|
}
|
|
|
|
PanelBotonera { root, flow, conexion: conexion.clone(), botones }
|
|
}
|
|
|
|
pub fn cargar_botones(&self, botones: &[BotonRemoto]) {
|
|
while let Some(child) = self.flow.first_child() {
|
|
self.flow.remove(&child);
|
|
}
|
|
*self.botones.borrow_mut() = botones.to_vec();
|
|
|
|
for boton in botones {
|
|
let btn = Button::with_label(&boton.etiqueta);
|
|
btn.set_size_request(90, 60);
|
|
btn.add_css_class("botonera-btn");
|
|
|
|
let ruta = boton.ruta.clone();
|
|
let conexion2 = self.conexion.clone();
|
|
btn.connect_clicked(move |_| {
|
|
if let Some(ref con) = *conexion2.borrow() {
|
|
con.enviar(MsgCliente::ReproducirAhora { ruta: ruta.clone() });
|
|
}
|
|
});
|
|
|
|
// Clic derecho → editar
|
|
let gc = gtk4::GestureClick::new();
|
|
gc.set_button(3);
|
|
let btn2 = btn.clone();
|
|
let indice = boton.indice;
|
|
let botones2 = self.botones.clone();
|
|
gc.connect_pressed(move |_gc, _n, _x, _y| {
|
|
let padre: Option<gtk4::Window> = btn2
|
|
.root()
|
|
.and_then(|r| r.downcast::<gtk4::Window>().ok());
|
|
abrir_dialogo_editar_boton(
|
|
padre.as_ref(),
|
|
&btn2.label().unwrap_or_default(),
|
|
{
|
|
let b = botones2.borrow();
|
|
b.iter().find(|b| b.indice == indice)
|
|
.map(|b| b.ruta.clone())
|
|
.unwrap_or_default()
|
|
},
|
|
{
|
|
let btn3 = btn2.clone();
|
|
let b2 = botones2.clone();
|
|
move |nueva_lbl, nueva_ruta| {
|
|
btn3.set_label(&nueva_lbl);
|
|
if let Some(b) = b2.borrow_mut().iter_mut().find(|b| b.indice == indice) {
|
|
b.etiqueta = nueva_lbl;
|
|
b.ruta = nueva_ruta;
|
|
}
|
|
}
|
|
},
|
|
);
|
|
});
|
|
btn.add_controller(gc);
|
|
|
|
self.flow.insert(&btn, -1);
|
|
}
|
|
}
|
|
|
|
pub fn set_activo(&self, activo: bool) {
|
|
self.flow.set_sensitive(activo);
|
|
}
|
|
}
|
|
|
|
fn abrir_dialogo_editar_boton<F: Fn(String, String) + 'static>(
|
|
padre: Option<>k4::Window>,
|
|
etiqueta_actual: &str,
|
|
ruta_actual: String,
|
|
on_accept: F,
|
|
) {
|
|
let win = gtk4::Window::new();
|
|
win.set_title(Some("Editar botón"));
|
|
win.set_modal(true);
|
|
win.set_resizable(false);
|
|
if let Some(p) = padre { win.set_transient_for(Some(p)); }
|
|
|
|
let vbox = GtkBox::new(Orientation::Vertical, 8);
|
|
vbox.set_margin_top(12);
|
|
vbox.set_margin_bottom(12);
|
|
vbox.set_margin_start(16);
|
|
vbox.set_margin_end(16);
|
|
|
|
let f1 = GtkBox::new(Orientation::Horizontal, 8);
|
|
f1.append(&Label::new(Some("Etiqueta:")));
|
|
let e_lbl = Entry::new();
|
|
e_lbl.set_text(etiqueta_actual);
|
|
e_lbl.set_hexpand(true);
|
|
f1.append(&e_lbl);
|
|
vbox.append(&f1);
|
|
|
|
let f2 = GtkBox::new(Orientation::Horizontal, 8);
|
|
f2.append(&Label::new(Some("Ruta:")));
|
|
let e_ruta = Entry::new();
|
|
e_ruta.set_text(&ruta_actual);
|
|
e_ruta.set_hexpand(true);
|
|
f2.append(&e_ruta);
|
|
vbox.append(&f2);
|
|
|
|
let fila_btn = GtkBox::new(Orientation::Horizontal, 8);
|
|
fila_btn.set_halign(Align::End);
|
|
fila_btn.set_margin_top(8);
|
|
let btn_cancel = Button::with_label("Cancelar");
|
|
let btn_ok = Button::with_label("Aceptar");
|
|
btn_ok.add_css_class("suggested-action");
|
|
fila_btn.append(&btn_cancel);
|
|
fila_btn.append(&btn_ok);
|
|
vbox.append(&fila_btn);
|
|
|
|
win.set_child(Some(&vbox));
|
|
let on_accept = Rc::new(on_accept);
|
|
{
|
|
let win2 = win.clone();
|
|
let e_lbl2 = e_lbl.clone();
|
|
let e_ruta2 = e_ruta.clone();
|
|
let oa2 = on_accept.clone();
|
|
btn_ok.connect_clicked(move |_| {
|
|
on_accept(e_lbl2.text().to_string(), e_ruta2.text().to_string());
|
|
win2.close();
|
|
});
|
|
}
|
|
{
|
|
let win2 = win.clone();
|
|
btn_cancel.connect_clicked(move |_| { win2.close(); });
|
|
}
|
|
win.present();
|
|
}
|