able to accept or reject upload requests from the tui
This commit is contained in:
parent
f9efd37d00
commit
a7cfe419b4
4 changed files with 102 additions and 22 deletions
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]);
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue