Reworked statistics

This commit is contained in:
Ultradesu
2026-07-26 01:41:16 +03:00
parent 17eb6a4fee
commit ada18a4583
14 changed files with 957 additions and 216 deletions
+203 -29
View File
@@ -11,8 +11,8 @@ use crate::app::state::{AppState, DevicePresenceSection, FedRow, settings_rows};
pub fn draw(frame: &mut Frame, area: Rect, state: &AppState) {
let block = Block::bordered()
.title(" Settings ")
.title_style(theme::header())
.border_style(theme::dim());
.title_style(theme::header_for(state))
.border_style(theme::border_for(state));
let inner = block.inner(area);
frame.render_widget(block, area);
@@ -68,7 +68,7 @@ fn draw_settings_rows(frame: &mut Frame, area: Rect, state: &AppState) {
let mut y = area.y;
let mut cursor = 0usize;
draw_section(frame, area, &mut y, "Federation");
draw_section(frame, area, state, &mut y, "Federation");
for row in FedRow::ALL {
let (label, value) = match row {
FedRow::Toggle => ("Federation", on_off(settings.enabled).to_string()),
@@ -98,6 +98,7 @@ fn draw_settings_rows(frame: &mut Frame, area: Rect, state: &AppState) {
draw_row(
frame,
area,
state,
&mut y,
cursor,
state.settings_cursor,
@@ -109,12 +110,13 @@ fn draw_settings_rows(frame: &mut Frame, area: Rect, state: &AppState) {
y = y.saturating_add(1);
let connected_devices_enabled = state.connected_devices_enabled();
draw_section(frame, area, &mut y, "Connected Devices");
draw_section(frame, area, state, &mut y, "Connected Devices");
let disabled_value = "enable federation first".to_string();
let devices = state.federation.devices.as_ref();
draw_row_enabled(
frame,
area,
state,
&mut y,
cursor,
state.settings_cursor,
@@ -132,6 +134,7 @@ fn draw_settings_rows(frame: &mut Frame, area: Rect, state: &AppState) {
draw_row_enabled(
frame,
area,
state,
&mut y,
cursor,
state.settings_cursor,
@@ -147,6 +150,7 @@ fn draw_settings_rows(frame: &mut Frame, area: Rect, state: &AppState) {
draw_row_enabled(
frame,
area,
state,
&mut y,
cursor,
state.settings_cursor,
@@ -162,6 +166,7 @@ fn draw_settings_rows(frame: &mut Frame, area: Rect, state: &AppState) {
draw_row_enabled(
frame,
area,
state,
&mut y,
cursor,
state.settings_cursor,
@@ -178,6 +183,22 @@ fn draw_settings_rows(frame: &mut Frame, area: Rect, state: &AppState) {
connected_devices_enabled,
);
cursor += 1;
draw_row_enabled(
frame,
area,
state,
&mut y,
cursor,
state.settings_cursor,
"Leave device group",
if connected_devices_enabled {
"".to_string()
} else {
disabled_value.clone()
},
connected_devices_enabled,
);
cursor += 1;
if let Some(status) = devices {
let now = crate::app::state::unix_time_ms();
let mut current_section = None;
@@ -219,6 +240,7 @@ fn draw_settings_rows(frame: &mut Frame, area: Rect, state: &AppState) {
draw_row_enabled(
frame,
area,
state,
&mut y,
cursor,
state.settings_cursor,
@@ -231,10 +253,11 @@ fn draw_settings_rows(frame: &mut Frame, area: Rect, state: &AppState) {
}
y = y.saturating_add(1);
draw_section(frame, area, &mut y, "Visualizations");
draw_section(frame, area, state, &mut y, "Visualizations");
draw_row(
frame,
area,
state,
&mut y,
cursor,
state.settings_cursor,
@@ -266,6 +289,7 @@ fn draw_settings_rows(frame: &mut Frame, area: Rect, state: &AppState) {
draw_row(
frame,
area,
state,
&mut y,
cursor,
state.settings_cursor,
@@ -278,6 +302,7 @@ fn draw_settings_rows(frame: &mut Frame, area: Rect, state: &AppState) {
draw_row(
frame,
area,
state,
&mut y,
cursor,
state.settings_cursor,
@@ -290,6 +315,7 @@ fn draw_settings_rows(frame: &mut Frame, area: Rect, state: &AppState) {
draw_row(
frame,
area,
state,
&mut y,
cursor,
state.settings_cursor,
@@ -303,6 +329,7 @@ fn draw_settings_rows(frame: &mut Frame, area: Rect, state: &AppState) {
draw_row(
frame,
area,
state,
&mut y,
cursor,
state.settings_cursor,
@@ -311,7 +338,7 @@ fn draw_settings_rows(frame: &mut Frame, area: Rect, state: &AppState) {
);
}
fn draw_section(frame: &mut Frame, area: Rect, y: &mut u16, title: &'static str) {
fn draw_section(frame: &mut Frame, area: Rect, state: &AppState, y: &mut u16, title: &'static str) {
if *y >= area.y + area.height {
return;
}
@@ -321,7 +348,10 @@ fn draw_section(frame: &mut Frame, area: Rect, y: &mut u16, title: &'static str)
width: area.width,
height: 1,
};
frame.render_widget(Paragraph::new(Line::styled(title, theme::header())), rect);
frame.render_widget(
Paragraph::new(Line::styled(title, theme::header_for(state))),
rect,
);
*y = (*y).saturating_add(1);
}
@@ -348,18 +378,20 @@ fn draw_subsection(frame: &mut Frame, area: Rect, y: &mut u16, title: &'static s
fn draw_row(
frame: &mut Frame,
area: Rect,
state: &AppState,
y: &mut u16,
row_index: usize,
cursor: usize,
label: &str,
value: String,
) {
draw_row_enabled(frame, area, y, row_index, cursor, label, value, true);
draw_row_enabled(frame, area, state, y, row_index, cursor, label, value, true);
}
fn draw_row_enabled(
frame: &mut Frame,
area: Rect,
state: &AppState,
y: &mut u16,
row_index: usize,
cursor: usize,
@@ -383,7 +415,7 @@ fn draw_row_enabled(
Span::styled(
marker,
if enabled {
theme::accent()
theme::accent_for(state)
} else {
theme::dim()
},
@@ -393,7 +425,7 @@ fn draw_row_enabled(
if !enabled {
theme::dim()
} else if selected {
theme::accent()
theme::accent_for(state)
} else {
ratatui::style::Style::default()
},
@@ -444,7 +476,7 @@ fn draw_status(frame: &mut Frame, area: Rect, state: &AppState) {
return;
}
if area.height < 18 || area.width < 36 {
if area.height < 15 || area.width < 36 {
frame.render_widget(
Paragraph::new(compact_status_lines(state))
.wrap(ratatui::widgets::Wrap { trim: false }),
@@ -453,40 +485,128 @@ fn draw_status(frame: &mut Frame, area: Rect, state: &AppState) {
return;
}
let [node_area, _, transport_area, _, devices_area, _] = Layout::vertical([
Constraint::Length(5),
if area.width >= 60 && area.height >= 15 {
let [top_area, _, bottom_area] = Layout::vertical([
Constraint::Length(7),
Constraint::Length(1),
Constraint::Length(7),
])
.areas(area);
let [status_area, _, local_area] = Layout::horizontal([
Constraint::Percentage(50),
Constraint::Length(1),
Constraint::Percentage(50),
])
.areas(top_area);
let [transport_area, _, devices_area] = Layout::horizontal([
Constraint::Percentage(50),
Constraint::Length(1),
Constraint::Percentage(50),
])
.areas(bottom_area);
draw_summary_card(
frame,
status_area,
state,
" Status ",
node_summary_lines(state),
);
draw_summary_card(
frame,
local_area,
state,
" Local Data ",
local_data_summary_lines(state),
);
draw_summary_card(
frame,
transport_area,
state,
" Iroh Transport ",
transport_summary_lines(state),
);
draw_summary_card(
frame,
devices_area,
state,
" Connected Devices ",
device_summary_lines(state),
);
return;
}
if area.height < 31 {
frame.render_widget(
Paragraph::new(compact_status_lines(state))
.wrap(ratatui::widgets::Wrap { trim: false }),
area,
);
return;
}
let [
node_area,
_,
transport_area,
_,
devices_area,
_,
local_area,
_,
] = Layout::vertical([
Constraint::Length(7),
Constraint::Length(1),
Constraint::Length(5),
Constraint::Length(7),
Constraint::Length(1),
Constraint::Length(5),
Constraint::Length(7),
Constraint::Length(1),
Constraint::Length(7),
Constraint::Min(0),
])
.areas(area);
draw_summary_card(frame, node_area, " Status ", node_summary_lines(state));
draw_summary_card(
frame,
node_area,
state,
" Status ",
node_summary_lines(state),
);
draw_summary_card(
frame,
transport_area,
state,
" Iroh Transport ",
transport_summary_lines(state),
);
draw_summary_card(
frame,
devices_area,
state,
" Connected Devices ",
device_summary_lines(state),
);
draw_summary_card(
frame,
local_area,
state,
" Local Data ",
local_data_summary_lines(state),
);
}
fn compact_status_lines(state: &AppState) -> Vec<Line<'static>> {
let mut lines = Vec::new();
lines.push(Line::styled("Status", theme::header()));
lines.push(Line::styled("Status", theme::header_for(state)));
lines.extend(node_summary_lines(state).into_iter().take(2));
lines.push(Line::default());
lines.push(Line::styled("Iroh Transport", theme::header()));
lines.push(Line::styled("Local Data", theme::header_for(state)));
lines.extend(local_data_summary_lines(state).into_iter().take(3));
lines.push(Line::default());
lines.push(Line::styled("Iroh Transport", theme::header_for(state)));
lines.extend(transport_summary_lines(state).into_iter().take(2));
lines.push(Line::default());
lines.push(Line::styled("Connected Devices", theme::header()));
lines.push(Line::styled("Connected Devices", theme::header_for(state)));
lines.extend(device_summary_lines(state).into_iter().take(2));
lines
}
@@ -494,6 +614,7 @@ fn compact_status_lines(state: &AppState) -> Vec<Line<'static>> {
fn draw_summary_card(
frame: &mut Frame,
area: Rect,
state: &AppState,
title: &'static str,
lines: Vec<Line<'static>>,
) {
@@ -502,8 +623,8 @@ fn draw_summary_card(
}
let block = Block::bordered()
.title(title)
.title_style(theme::header())
.border_style(theme::dim());
.title_style(theme::header_for(state))
.border_style(theme::border_for(state));
let inner = block.inner(area);
frame.render_widget(block, area);
frame.render_widget(Paragraph::new(lines), inner);
@@ -626,6 +747,51 @@ fn transport_summary_lines(state: &AppState) -> Vec<Line<'static>> {
]
}
fn local_data_summary_lines(state: &AppState) -> Vec<Line<'static>> {
match &state.local_library_stats {
None | Some(crate::app::state::Loadable::Loading) => vec![
summary_line("Counts", "loading".to_string()),
summary_line("Media", "loading".to_string()),
summary_line("SQLite", "loading".to_string()),
summary_line("Total", "loading".to_string()),
],
Some(crate::app::state::Loadable::Failed(message)) => vec![
summary_line("Counts", "unavailable".to_string()),
summary_line("Problem", first_line(message)),
],
Some(crate::app::state::Loadable::Ready(stats)) => {
let media = if stats.tracks_without_size > 0 {
format!(
"{} · {} unknown",
short_bytes_label(stats.audio_bytes),
stats.tracks_without_size
)
} else {
short_bytes_label(stats.audio_bytes)
};
vec![
summary_line(
"Counts",
format!(
"{} artists / {} releases / {} tracks",
stats.artist_count, stats.release_count, stats.track_count
),
),
summary_line("Media", media),
summary_line("SQLite", short_bytes_label(stats.database_bytes)),
summary_line(
"Total",
format!(
"{} (covers {})",
short_bytes_label(stats.total_bytes()),
short_bytes_label(stats.cover_bytes)
),
),
]
}
}
}
fn device_summary_lines(state: &AppState) -> Vec<Line<'static>> {
if !state.connected_devices_enabled() {
return vec![
@@ -718,7 +884,7 @@ pub(super) fn status_detail_sections(
}
fn status_detail_status_lines(state: &AppState, status_cursor: usize) -> Vec<Line<'static>> {
let mut lines: Vec<Line> = vec![Line::styled("Status", theme::header())];
let mut lines: Vec<Line> = vec![Line::styled("Status", theme::header_for(state))];
match &state.federation.status {
None => lines.push(Line::styled("loading…", theme::dim())),
Some(status) if !status.running => {
@@ -741,6 +907,7 @@ fn status_detail_status_lines(state: &AppState, status_cursor: usize) -> Vec<Lin
short_id(&status.endpoint_id),
short_id(&status.dht_node_id)
),
state,
status_cursor == 0,
));
lines.push(status_line(
@@ -764,6 +931,7 @@ fn status_detail_status_lines(state: &AppState, status_cursor: usize) -> Vec<Lin
lines.push(status_action_line(
"Peer IDs",
peers.join(", "),
state,
status_cursor == 1,
));
}
@@ -795,19 +963,24 @@ fn status_detail_status_lines(state: &AppState, status_cursor: usize) -> Vec<Lin
if let Some(error) = &status.last_error {
lines.push(status_line("Error", first_line(error)));
}
push_transport_summary_status(&mut lines, status);
push_transport_summary_status(&mut lines, state, status);
}
}
lines
}
fn status_action_line(label: &str, value: String, selected: bool) -> Line<'static> {
fn status_action_line(
label: &str,
value: String,
state: &AppState,
selected: bool,
) -> Line<'static> {
let marker = if selected { "" } else { " " };
Line::from(vec![
Span::styled(
format!("{marker} {label:<16}"),
if selected {
theme::accent()
theme::accent_for(state)
} else {
theme::dim()
},
@@ -819,10 +992,11 @@ fn status_action_line(label: &str, value: String, selected: bool) -> Line<'stati
fn push_transport_summary_status(
lines: &mut Vec<Line<'static>>,
state: &AppState,
status: &crate::federation::FedStatus,
) {
lines.push(Line::default());
lines.push(Line::styled("Iroh Transport", theme::header()));
lines.push(Line::styled("Iroh Transport", theme::header_for(state)));
let transport = &status.transport;
let runtime_total = transport
.runtime_tx_bytes
@@ -934,7 +1108,7 @@ pub(super) fn status_detail_transport_logs(state: &AppState) -> Vec<Line<'static
}
fn status_detail_device_lines(state: &AppState) -> Vec<Line<'static>> {
let mut lines = vec![Line::styled("Connected Devices", theme::header())];
let mut lines = vec![Line::styled("Connected Devices", theme::header_for(state))];
match &state.federation.devices {
None => lines.push(Line::styled("loading…", theme::dim())),
Some(status) => {
@@ -976,7 +1150,7 @@ fn status_detail_device_lines(state: &AppState) -> Vec<Line<'static>> {
lines.push(status_line("Error", first_line(last_error)));
}
lines.push(Line::default());
lines.push(Line::styled("Device List", theme::header()));
lines.push(Line::styled("Device List", theme::header_for(state)));
if status.devices.is_empty() {
lines.push(status_line("Devices", "none recorded".to_string()));
} else {
@@ -1047,7 +1221,7 @@ fn push_device_detail_compact(
format!("v{}", device.client_version)
};
lines.push(Line::from(vec![
Span::styled(format!("{icon} "), theme::accent()),
Span::styled(format!("{icon} "), theme::accent_for(state)),
Span::raw(crate::app::state::device_display_name(device)),
Span::styled(
format!(" · {} · {}", version, badges.join(", ")),