From a7cfe419b440d271266c85ae6a2e0050c0d4b67a Mon Sep 17 00:00:00 2001 From: Joe Ardent Date: Fri, 1 Aug 2025 14:55:17 -0700 Subject: [PATCH] able to accept or reject upload requests from the tui --- src/app/mod.rs | 51 +++++++++++++++++++++++++++++++++++--- src/app/widgets.rs | 61 ++++++++++++++++++++++++++++++++++------------ src/discovery.rs | 6 ++--- src/transfer.rs | 6 +++++ 4 files changed, 102 insertions(+), 22 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index f5a7c1b..fb8903b 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -4,11 +4,10 @@ use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, KeyEventKind}; use futures::{FutureExt, StreamExt}; use joecalsend::{ Config, JoecalState, JoecalUploadRequest, Listeners, TransferEvent, UploadDialog, - error::{LocalSendError, Result}, - models::Device, + error::Result, models::Device, }; use julid::Julid; -use log::{LevelFilter, debug, error, info}; +use log::{LevelFilter, debug, error, info, warn}; use ratatui::{DefaultTerminal, Frame, widgets::TableState}; use tokio::{ sync::mpsc::{UnboundedReceiver, unbounded_channel}, @@ -147,6 +146,15 @@ impl App { KeyCode::Char('q') => self.exit(), _ => {} }, + CurrentScreen::Receiving => match key_event.code { + KeyCode::Up => self.upload_state.select_previous(), + KeyCode::Down => self.upload_state.select_next(), + KeyCode::Char('a') => self.accept(), + KeyCode::Char('d') => self.deny(), + KeyCode::Esc => self.pop(), + KeyCode::Char('q') => self.exit(), + _ => {} + }, _ => match key_event.code { KeyCode::Char('q') => self.exit(), KeyCode::Char('s') => self.send(), @@ -158,6 +166,43 @@ impl App { } } + fn accept(&mut self) { + let Some(idx) = self.upload_state.selected() else { + return; + }; + // keys are sorted, so we can use the table selection index + let keys: Vec<_> = self.uploads.keys().collect(); + let Some(key) = keys.get(idx) else { + warn!("could not get id from selection index {idx}"); + return; + }; + let Some(req) = self.uploads.get(key) else { + return; + }; + if let Err(e) = req.tx.send(UploadDialog::UploadConfirm) { + error!("got error sending upload confirmation: {e:?}"); + }; + } + + fn deny(&mut self) { + let Some(idx) = self.upload_state.selected() else { + return; + }; + // keys are sorted, so we can use the table selection index + let keys: Vec<_> = self.uploads.keys().cloned().collect(); + let Some(key) = keys.get(idx) else { + warn!("could not get id from selection index {idx}"); + return; + }; + let Some(req) = self.uploads.get(key).cloned() else { + return; + }; + if let Err(e) = req.tx.send(UploadDialog::UploadDeny) { + error!("got error sending upload confirmation: {e:?}"); + }; + self.uploads.remove(key); + } + fn draw(&mut self, frame: &mut Frame) { frame.render_widget(self, frame.area()); } diff --git a/src/app/widgets.rs b/src/app/widgets.rs index fd04340..a21b659 100644 --- a/src/app/widgets.rs +++ b/src/app/widgets.rs @@ -5,7 +5,7 @@ use log::LevelFilter; use ratatui::{ buffer::Buffer, layout::{Constraint, Layout, Margin, Rect}, - style::{Style, Stylize}, + style::{Color, Style, Stylize}, symbols::border, text::{Line, Text, ToLine}, widgets::{Block, Borders, List, ListItem, Padding, Paragraph, Row, Table, TableState, Widget}, @@ -42,6 +42,23 @@ static LOGGING_MENU: LazyLock = LazyLock::new(|| { ]) }); +static UPLOADS_MENU: LazyLock = LazyLock::new(|| { + Line::from(vec![ + " Select Previous ".into(), + "".blue().bold(), + " Select Next ".into(), + "".blue().bold(), + " Approve Selection ".into(), + "".blue().bold(), + " Deny Selection ".into(), + "".blue().bold(), + " Previous Screen ".into(), + "".blue().bold(), + " Quit ".into(), + "".blue().bold(), + ]) +}); + impl Widget for &mut App { fn render(self, area: Rect, buf: &mut Buffer) { let main_layout = @@ -59,6 +76,7 @@ impl Widget for &mut App { let header_margin = Margin::new(1, 2); let mode = self.screen.last().unwrap(); + let ups: Vec<_> = self.uploads.values().collect(); match mode { CurrentScreen::Main => { main_page(*mode, &MAIN_MENU, area, buf); @@ -66,7 +84,6 @@ impl Widget for &mut App { let peers = PeersWidget { peers: &self.peers }; peers.render(footer_right.inner(footer_margin), buf); NetworkInfoWidget.render(footer_left.inner(footer_margin), buf); - let ups: Vec<_> = self.uploads.values().collect(); uploads( &ups, &mut self.upload_state, @@ -79,7 +96,13 @@ impl Widget for &mut App { logger(area.inner(Margin::new(2, 4)), buf); } CurrentScreen::Receiving => { - main_page(*mode, &MAIN_MENU, area, buf); + main_page(*mode, &UPLOADS_MENU, area, buf); + uploads( + &ups, + &mut self.upload_state, + area.inner(Margin::new(2, 4)), + buf, + ); } _ => { main_page(*mode, &MAIN_MENU, area, buf); @@ -97,11 +120,7 @@ fn logger(area: Rect, buf: &mut Buffer) { .output_target(true) .output_file(false) .output_line(false) - .block( - Block::bordered() - .border_set(border::THICK) - .title(title.centered()), - ) + .block(Block::bordered().title(title.centered())) .style(Style::default()) .state(&TuiWidgetState::new().set_default_display_level(LevelFilter::Debug)); logger.render(area, buf); @@ -130,9 +149,8 @@ fn uploads( buf: &mut Buffer, ) { let title = Line::from(" Upload Requests ").bold(); - let block = Block::bordered() - .title(title.centered()) - .border_set(border::THICK); + let block = Block::bordered().title(title.centered()); + let mut rows = Vec::new(); for &req in requests { let src = req.alias.to_line().left_aligned(); @@ -150,16 +168,27 @@ fn uploads( let size = Line::from(format!("{size}")).centered(); rows.push(Row::new([src, size, files])); } + + if state.selected().is_none() && !rows.is_empty() { + state.select(Some(0)); + } else if rows.is_empty() { + state.select(None); + }; + let widths = [ Constraint::Max(20), Constraint::Max(15), Constraint::Min(50), ]; - let table = Table::new(rows, widths).block(block).header(Row::new([ - "Sender".bold().into_left_aligned_line(), - "Bytes".bold().into_centered_line(), - "Files".bold().into_centered_line(), - ])); + + let table = Table::new(rows, widths) + .block(block) + .header(Row::new([ + "Sender".bold().into_left_aligned_line(), + "Bytes".bold().into_centered_line(), + "Files".bold().into_centered_line(), + ])) + .row_highlight_style(Style::new().bg(Color::Rgb(99, 99, 99))); ratatui::widgets::StatefulWidget::render(table, area, buf, state); } diff --git a/src/discovery.rs b/src/discovery.rs index cde4d42..a99486f 100644 --- a/src/discovery.rs +++ b/src/discovery.rs @@ -8,7 +8,7 @@ use axum::{ Json, extract::{ConnectInfo, State}, }; -use log::{debug, error, warn}; +use log::{debug, error, trace, warn}; use tokio::net::UdpSocket; use crate::{Config, JoecalState, RunningState, models::Device}; @@ -19,7 +19,7 @@ impl JoecalState { socket: Option, config: &Config, ) -> crate::error::Result<()> { - debug!("announcing"); + trace!("announcing"); announce_http(&self.device, socket, self.client.clone()).await?; announce_multicast(&self.device, config.multicast_addr, self.socket.clone()).await?; Ok(()) @@ -44,7 +44,7 @@ impl JoecalState { } }, r = self.socket.recv_from(&mut buf) => { - debug!("received multicast datagram"); + trace!("received multicast datagram"); match r { Ok((size, src)) => { let received_msg = String::from_utf8_lossy(&buf[..size]); diff --git a/src/transfer.rs b/src/transfer.rs index 3057dd7..bfa9d21 100644 --- a/src/transfer.rs +++ b/src/transfer.rs @@ -321,6 +321,12 @@ pub async fn register_upload( .into_response(); } + if let Ok(id) = Julid::from_str(session_id) + && let Err(e) = state.transfer_event_tx.send(TransferEvent::Received(id)) + { + error!("got error sending upload received event: {e:?}"); + }; + StatusCode::OK.into_response() }