Commit inicial: G Radio Remoto (gr-client)
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.
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
// 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();
|
||||
}
|
||||
Reference in New Issue
Block a user