able to accept or reject upload requests from the tui

This commit is contained in:
Joe Ardent 2025-08-01 14:55:17 -07:00
parent f9efd37d00
commit a7cfe419b4
4 changed files with 102 additions and 22 deletions

View file

@ -4,11 +4,10 @@ use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, KeyEventKind};
use futures::{FutureExt, StreamExt}; use futures::{FutureExt, StreamExt};
use joecalsend::{ use joecalsend::{
Config, JoecalState, JoecalUploadRequest, Listeners, TransferEvent, UploadDialog, Config, JoecalState, JoecalUploadRequest, Listeners, TransferEvent, UploadDialog,
error::{LocalSendError, Result}, error::Result, models::Device,
models::Device,
}; };
use julid::Julid; use julid::Julid;
use log::{LevelFilter, debug, error, info}; use log::{LevelFilter, debug, error, info, warn};
use ratatui::{DefaultTerminal, Frame, widgets::TableState}; use ratatui::{DefaultTerminal, Frame, widgets::TableState};
use tokio::{ use tokio::{
sync::mpsc::{UnboundedReceiver, unbounded_channel}, sync::mpsc::{UnboundedReceiver, unbounded_channel},
@ -147,6 +146,15 @@ impl App {
KeyCode::Char('q') => self.exit(), 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 { _ => match key_event.code {
KeyCode::Char('q') => self.exit(), KeyCode::Char('q') => self.exit(),
KeyCode::Char('s') => self.send(), 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) { fn draw(&mut self, frame: &mut Frame) {
frame.render_widget(self, frame.area()); frame.render_widget(self, frame.area());
} }

View file

@ -5,7 +5,7 @@ use log::LevelFilter;
use ratatui::{ use ratatui::{
buffer::Buffer, buffer::Buffer,
layout::{Constraint, Layout, Margin, Rect}, layout::{Constraint, Layout, Margin, Rect},
style::{Style, Stylize}, style::{Color, Style, Stylize},
symbols::border, symbols::border,
text::{Line, Text, ToLine}, text::{Line, Text, ToLine},
widgets::{Block, Borders, List, ListItem, Padding, Paragraph, Row, Table, TableState, Widget}, widgets::{Block, Borders, List, ListItem, Padding, Paragraph, Row, Table, TableState, Widget},
@ -42,6 +42,23 @@ static LOGGING_MENU: LazyLock<Line> = LazyLock::new(|| {
]) ])
}); });
static UPLOADS_MENU: LazyLock<Line> = LazyLock::new(|| {
Line::from(vec![
" Select Previous ".into(),
"<UP>".blue().bold(),
" Select Next ".into(),
"<DOWN>".blue().bold(),
" Approve Selection ".into(),
"<A>".blue().bold(),
" Deny Selection ".into(),
"<D>".blue().bold(),
" Previous Screen ".into(),
"<ESC>".blue().bold(),
" Quit ".into(),
"<Q>".blue().bold(),
])
});
impl Widget for &mut App { impl Widget for &mut App {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let main_layout = let main_layout =
@ -59,6 +76,7 @@ impl Widget for &mut App {
let header_margin = Margin::new(1, 2); let header_margin = Margin::new(1, 2);
let mode = self.screen.last().unwrap(); let mode = self.screen.last().unwrap();
let ups: Vec<_> = self.uploads.values().collect();
match mode { match mode {
CurrentScreen::Main => { CurrentScreen::Main => {
main_page(*mode, &MAIN_MENU, area, buf); main_page(*mode, &MAIN_MENU, area, buf);
@ -66,7 +84,6 @@ impl Widget for &mut App {
let peers = PeersWidget { peers: &self.peers }; let peers = PeersWidget { peers: &self.peers };
peers.render(footer_right.inner(footer_margin), buf); peers.render(footer_right.inner(footer_margin), buf);
NetworkInfoWidget.render(footer_left.inner(footer_margin), buf); NetworkInfoWidget.render(footer_left.inner(footer_margin), buf);
let ups: Vec<_> = self.uploads.values().collect();
uploads( uploads(
&ups, &ups,
&mut self.upload_state, &mut self.upload_state,
@ -79,7 +96,13 @@ impl Widget for &mut App {
logger(area.inner(Margin::new(2, 4)), buf); logger(area.inner(Margin::new(2, 4)), buf);
} }
CurrentScreen::Receiving => { 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); main_page(*mode, &MAIN_MENU, area, buf);
@ -97,11 +120,7 @@ fn logger(area: Rect, buf: &mut Buffer) {
.output_target(true) .output_target(true)
.output_file(false) .output_file(false)
.output_line(false) .output_line(false)
.block( .block(Block::bordered().title(title.centered()))
Block::bordered()
.border_set(border::THICK)
.title(title.centered()),
)
.style(Style::default()) .style(Style::default())
.state(&TuiWidgetState::new().set_default_display_level(LevelFilter::Debug)); .state(&TuiWidgetState::new().set_default_display_level(LevelFilter::Debug));
logger.render(area, buf); logger.render(area, buf);
@ -130,9 +149,8 @@ fn uploads(
buf: &mut Buffer, buf: &mut Buffer,
) { ) {
let title = Line::from(" Upload Requests ").bold(); let title = Line::from(" Upload Requests ").bold();
let block = Block::bordered() let block = Block::bordered().title(title.centered());
.title(title.centered())
.border_set(border::THICK);
let mut rows = Vec::new(); let mut rows = Vec::new();
for &req in requests { for &req in requests {
let src = req.alias.to_line().left_aligned(); let src = req.alias.to_line().left_aligned();
@ -150,16 +168,27 @@ fn uploads(
let size = Line::from(format!("{size}")).centered(); let size = Line::from(format!("{size}")).centered();
rows.push(Row::new([src, size, files])); 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 = [ let widths = [
Constraint::Max(20), Constraint::Max(20),
Constraint::Max(15), Constraint::Max(15),
Constraint::Min(50), Constraint::Min(50),
]; ];
let table = Table::new(rows, widths).block(block).header(Row::new([
"Sender".bold().into_left_aligned_line(), let table = Table::new(rows, widths)
"Bytes".bold().into_centered_line(), .block(block)
"Files".bold().into_centered_line(), .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); ratatui::widgets::StatefulWidget::render(table, area, buf, state);
} }

View file

@ -8,7 +8,7 @@ use axum::{
Json, Json,
extract::{ConnectInfo, State}, extract::{ConnectInfo, State},
}; };
use log::{debug, error, warn}; use log::{debug, error, trace, warn};
use tokio::net::UdpSocket; use tokio::net::UdpSocket;
use crate::{Config, JoecalState, RunningState, models::Device}; use crate::{Config, JoecalState, RunningState, models::Device};
@ -19,7 +19,7 @@ impl JoecalState {
socket: Option<SocketAddr>, socket: Option<SocketAddr>,
config: &Config, config: &Config,
) -> crate::error::Result<()> { ) -> crate::error::Result<()> {
debug!("announcing"); trace!("announcing");
announce_http(&self.device, socket, self.client.clone()).await?; announce_http(&self.device, socket, self.client.clone()).await?;
announce_multicast(&self.device, config.multicast_addr, self.socket.clone()).await?; announce_multicast(&self.device, config.multicast_addr, self.socket.clone()).await?;
Ok(()) Ok(())
@ -44,7 +44,7 @@ impl JoecalState {
} }
}, },
r = self.socket.recv_from(&mut buf) => { r = self.socket.recv_from(&mut buf) => {
debug!("received multicast datagram"); trace!("received multicast datagram");
match r { match r {
Ok((size, src)) => { Ok((size, src)) => {
let received_msg = String::from_utf8_lossy(&buf[..size]); let received_msg = String::from_utf8_lossy(&buf[..size]);

View file

@ -321,6 +321,12 @@ pub async fn register_upload(
.into_response(); .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() StatusCode::OK.into_response()
} }