Reworked statistics

This commit is contained in:
Ultradesu
2026-07-26 00:45:53 +03:00
parent 76f67372a1
commit 17eb6a4fee
8 changed files with 867 additions and 259 deletions
+4
View File
@@ -147,12 +147,16 @@ pub enum AppEvent {
},
/// This peer's connection ticket, requested from the Federation tab.
FedTicket(Result<String, String>),
/// Immediate library publish finished.
FedSyncFinished(String),
/// Fresh personal-device sync status snapshot for Settings.
DeviceSyncStatus(crate::devices::DeviceSyncStatus),
/// Invite link for pairing another device.
DeviceInvite(Result<String, String>),
/// Result of `:connect frid://i/...`.
DeviceConnectResult(Result<String, String>),
/// Manual trusted-device sync finished.
DeviceSyncFinished(String),
/// Incoming pairing request that passed the invite-secret check.
DevicePairingRequest(crate::devices::PendingPairing),
/// Trusted device playback state, delivered by personal-device sync.
+23 -52
View File
@@ -7,9 +7,9 @@ pub(crate) mod popup;
pub mod state;
pub mod update;
use std::io::{self, Write as _};
use std::io;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::process::Command;
use std::sync::Arc;
use std::time::Duration;
@@ -1397,6 +1397,8 @@ fn perform_effect(state: &mut AppState, runtime: &mut Runtime, effect: Effect) {
}
Effect::FedApplySettings => fed_apply_settings(state, runtime),
Effect::FedSyncNow => {
state.federation.publishing = true;
state.status_message = Some("publishing library…".to_string());
let fed = Arc::clone(&runtime.federation);
let tx = runtime.event_tx.clone();
tokio::spawn(async move {
@@ -1405,7 +1407,7 @@ fn perform_effect(state: &mut AppState, runtime: &mut Runtime, effect: Effect) {
Err(err) => format!("federation sync failed: {err:#}"),
};
let _ = tx.send(AppEvent::FederationStatus(fed.status().await));
let _ = tx.send(AppEvent::StatusMessage(message));
let _ = tx.send(AppEvent::FedSyncFinished(message));
});
}
Effect::FedShowTicket => {
@@ -1438,6 +1440,8 @@ fn perform_effect(state: &mut AppState, runtime: &mut Runtime, effect: Effect) {
}
Effect::DeviceConnectInvite(invite) => device_connect(runtime, invite),
Effect::DeviceSyncNow => {
state.federation.device_syncing = true;
state.status_message = Some("syncing devices…".to_string());
let fed = Arc::clone(&runtime.federation);
let devices = Arc::clone(&runtime.devices);
let tx = runtime.event_tx.clone();
@@ -1447,7 +1451,7 @@ fn perform_effect(state: &mut AppState, runtime: &mut Runtime, effect: Effect) {
Err(err) => format!("devices: {err:#}"),
};
let _ = tx.send(AppEvent::DeviceSyncStatus(devices.status()));
let _ = tx.send(AppEvent::StatusMessage(message));
let _ = tx.send(AppEvent::DeviceSyncFinished(message));
});
}
Effect::DeviceSetName(name) => {
@@ -1667,45 +1671,6 @@ fn shell_quote(path: &Path) -> String {
format!("'{}'", value.replace('\'', "'\\''"))
}
fn copy_text_to_clipboard(text: &str) -> bool {
let command: &[&str] = if cfg!(target_os = "macos") {
&["pbcopy"]
} else if cfg!(target_os = "windows") {
&["clip"]
} else {
&["wl-copy"]
};
let Some((program, args)) = command.split_first() else {
return false;
};
let mut child = match Command::new(program)
.args(args)
.stdin(Stdio::piped())
.spawn()
{
Ok(child) => child,
Err(_) if !cfg!(target_os = "macos") && !cfg!(target_os = "windows") => {
match Command::new("xclip")
.args(["-selection", "clipboard"])
.stdin(Stdio::piped())
.spawn()
{
Ok(child) => child,
Err(_) => return false,
}
}
Err(_) => return false,
};
let Some(mut stdin) = child.stdin.take() else {
return false;
};
if stdin.write_all(text.as_bytes()).is_err() {
return false;
}
drop(stdin);
child.wait().is_ok_and(|status| status.success())
}
/// Start playing `queue[queue_pos]`: open the local file in a background
/// task and hand the reader to the audio thread.
fn play_current(state: &mut AppState, runtime: &mut Runtime) {
@@ -2826,18 +2791,22 @@ fn handle_app_event(state: &mut AppState, runtime: &mut Runtime, event: AppEvent
}
clamp_settings_cursor(state);
}
AppEvent::FedSyncFinished(message) => {
state.federation.publishing = false;
state.status_message = Some(message);
}
AppEvent::DeviceSyncFinished(message) => {
state.federation.device_syncing = false;
state.status_message = Some(message);
}
AppEvent::DeviceInvite(result) => match result {
Ok(invite) => {
let copied = copy_text_to_clipboard(&invite);
state.popup = Some(state::Popup::FedText {
state.popup = Some(state::Popup::FedCopyText {
title: "Device invite".to_string(),
text: invite,
help: "Use this invite on another client within 10 minutes to pair it with this device group.".to_string(),
});
state.status_message = Some(if copied {
"device invite copied to clipboard".to_string()
} else {
"device invite generated".to_string()
});
state.status_message = Some("device invite generated".to_string());
}
Err(message) => state.status_message = Some(format!("device invite: {message}")),
},
@@ -3105,9 +3074,11 @@ fn handle_app_event(state: &mut AppState, runtime: &mut Runtime, event: AppEvent
}
AppEvent::FedTicket(result) => match result {
Ok(ticket) => {
state.popup = Some(state::Popup::FedText {
title: "Federation ticket (share with a peer)".to_string(),
state.popup = Some(state::Popup::FedCopyText {
title: "Connection ticket".to_string(),
text: ticket,
help: "Copy this ticket and paste it into Connect to a peer on another client."
.to_string(),
});
}
Err(message) => state.status_message = Some(message),
+183 -9
View File
@@ -13,7 +13,7 @@ use crate::app::Runtime;
use crate::app::event::AppEvent;
use crate::app::state::{
self, AppState, DeleteTarget, DevicePresenceSection, EditField, EditTarget, FedInputField,
Loadable, Popup, addable_playlists,
FederationStatusPopupState, Loadable, Popup, StatusDetailFocus, addable_playlists,
};
use crate::library::models::{ReleaseEdit, TrackEdit, TrackItem};
@@ -136,8 +136,40 @@ pub fn handle_key(state: &mut AppState, runtime: &mut Runtime, key: KeyEvent) {
KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => {}
_ => state.popup = Some(Popup::FedText { title, text }),
},
Popup::FederationStatusDetails { scroll } => {
handle_federation_status_details(state, scroll, key);
Popup::FedCopyText { title, text, help } => match key.code {
KeyCode::Esc | KeyCode::Char('q') => {}
KeyCode::Enter | KeyCode::Char('c') => match copy_to_clipboard(&text) {
Ok(()) => state.status_message = Some("copied to clipboard".to_string()),
Err(err) => {
state.status_message = Some(format!("copy failed: {err}"));
state.popup = Some(Popup::FedCopyText { title, text, help });
}
},
_ => state.popup = Some(Popup::FedCopyText { title, text, help }),
},
Popup::FederationStatusDetails {
focus,
status_cursor,
devices_scroll,
logs_scroll,
} => handle_federation_status_details(
state,
FederationStatusPopupState {
focus,
status_cursor,
devices_scroll,
logs_scroll,
},
key,
),
Popup::FederationStatusText {
parent,
title,
text,
scroll,
} => handle_federation_status_child(state, parent, title, text, scroll, key),
Popup::FederationStatusLog { parent, scroll } => {
handle_federation_status_log(state, parent, scroll, key);
}
Popup::DevicePairing {
request_id,
@@ -166,20 +198,162 @@ pub fn handle_key(state: &mut AppState, runtime: &mut Runtime, key: KeyEvent) {
}
}
fn handle_federation_status_details(state: &mut AppState, scroll: usize, key: KeyEvent) {
fn handle_federation_status_details(
state: &mut AppState,
mut parent: FederationStatusPopupState,
key: KeyEvent,
) {
let action_count = status_detail_action_count(state);
match key.code {
KeyCode::Esc | KeyCode::Char('q') => return,
KeyCode::Left | KeyCode::Char('h') => parent.focus = parent.focus.previous(),
KeyCode::Right | KeyCode::Char('l') | KeyCode::Tab => parent.focus = parent.focus.next(),
KeyCode::Up | KeyCode::Char('k') => match parent.focus {
StatusDetailFocus::Status => {
parent.status_cursor = parent.status_cursor.saturating_sub(1)
}
StatusDetailFocus::Devices => {
parent.devices_scroll = parent.devices_scroll.saturating_sub(1)
}
StatusDetailFocus::Logs => {
if action_count > 0 {
parent.focus = StatusDetailFocus::Status;
parent.status_cursor = action_count - 1;
}
}
},
KeyCode::Down | KeyCode::Char('j') => match parent.focus {
StatusDetailFocus::Status => {
if parent.status_cursor + 1 < action_count {
parent.status_cursor += 1;
} else {
parent.focus = StatusDetailFocus::Logs;
}
}
StatusDetailFocus::Devices => {
parent.devices_scroll = parent.devices_scroll.saturating_add(1)
}
StatusDetailFocus::Logs => {}
},
KeyCode::PageUp => match parent.focus {
StatusDetailFocus::Status => parent.status_cursor = 0,
StatusDetailFocus::Devices => {
parent.devices_scroll = parent.devices_scroll.saturating_sub(8)
}
StatusDetailFocus::Logs => {}
},
KeyCode::PageDown => match parent.focus {
StatusDetailFocus::Status => {
parent.status_cursor = action_count.saturating_sub(1);
parent.focus = StatusDetailFocus::Logs;
}
StatusDetailFocus::Devices => {
parent.devices_scroll = parent.devices_scroll.saturating_add(8)
}
StatusDetailFocus::Logs => {}
},
KeyCode::Enter if parent.focus == StatusDetailFocus::Status => {
if let Some((title, text)) = status_detail_action_text(state, parent.status_cursor) {
state.popup = Some(Popup::FederationStatusText {
parent,
title,
text,
scroll: 0,
});
return;
}
}
KeyCode::Enter if parent.focus == StatusDetailFocus::Logs => {
state.popup = Some(Popup::FederationStatusLog { parent, scroll: 0 });
return;
}
_ => {}
}
parent.status_cursor = parent.status_cursor.min(action_count.saturating_sub(1));
state.popup = Some(parent.into());
}
fn handle_federation_status_child(
state: &mut AppState,
parent: FederationStatusPopupState,
title: String,
text: String,
scroll: usize,
key: KeyEvent,
) {
let next_scroll = match key.code {
KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => return,
KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => {
state.popup = Some(parent.into());
return;
}
KeyCode::Up | KeyCode::Char('k') => scroll.saturating_sub(1),
KeyCode::Down | KeyCode::Char('j') => scroll + 1,
KeyCode::PageUp => scroll.saturating_sub(8),
KeyCode::PageDown => scroll + 8,
KeyCode::Down | KeyCode::Char('j') => scroll.saturating_add(1),
KeyCode::PageUp => scroll.saturating_sub(10),
KeyCode::PageDown => scroll.saturating_add(10),
_ => scroll,
};
state.popup = Some(Popup::FederationStatusDetails {
state.popup = Some(Popup::FederationStatusText {
parent,
title,
text,
scroll: next_scroll,
});
}
fn handle_federation_status_log(
state: &mut AppState,
parent: FederationStatusPopupState,
scroll: usize,
key: KeyEvent,
) {
let next_scroll = match key.code {
KeyCode::Esc | KeyCode::Char('q') => {
state.popup = Some(parent.into());
return;
}
KeyCode::Up | KeyCode::Char('k') => scroll.saturating_sub(1),
KeyCode::Down | KeyCode::Char('j') => scroll.saturating_add(1),
KeyCode::PageUp => scroll.saturating_sub(10),
KeyCode::PageDown => scroll.saturating_add(10),
_ => scroll,
};
state.popup = Some(Popup::FederationStatusLog {
parent,
scroll: next_scroll,
});
}
fn status_detail_action_count(state: &AppState) -> usize {
state
.federation
.status
.as_ref()
.filter(|status| status.running)
.map(|status| 1 + usize::from(!status.connected_peers.is_empty()))
.unwrap_or(0)
}
fn status_detail_action_text(state: &AppState, cursor: usize) -> Option<(String, String)> {
let status = state
.federation
.status
.as_ref()
.filter(|status| status.running)?;
match cursor {
0 => Some((
"Endpoint IDs".to_string(),
format!(
"Endpoint ID\n{}\n\nDHT node ID\n{}",
status.endpoint_id, status.dht_node_id
),
)),
1 if !status.connected_peers.is_empty() => {
Some(("Peer IDs".to_string(), status.connected_peers.join("\n")))
}
_ => None,
}
}
fn handle_connected_devices(
state: &mut AppState,
runtime: &mut Runtime,
+87 -1
View File
@@ -711,6 +711,12 @@ pub enum Popup {
},
/// Wrapped read-only text (this peer's connection ticket).
FedText { title: String, text: String },
/// Wrapped text that can be copied to the system clipboard.
FedCopyText {
title: String,
text: String,
help: String,
},
/// Incoming trusted-device pairing request.
DevicePairing {
request_id: String,
@@ -725,7 +731,68 @@ pub enum Popup {
/// Connected playback devices and their current role/status.
ConnectedDevices { cursor: usize },
/// Full federation, transport and device status details.
FederationStatusDetails { scroll: usize },
FederationStatusDetails {
focus: StatusDetailFocus,
status_cursor: usize,
devices_scroll: usize,
logs_scroll: usize,
},
/// Full text opened from the full federation status dashboard.
FederationStatusText {
parent: FederationStatusPopupState,
title: String,
text: String,
scroll: usize,
},
/// Full connection log opened from the full federation status dashboard.
FederationStatusLog {
parent: FederationStatusPopupState,
scroll: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FederationStatusPopupState {
pub focus: StatusDetailFocus,
pub status_cursor: usize,
pub devices_scroll: usize,
pub logs_scroll: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StatusDetailFocus {
Status,
Devices,
Logs,
}
impl From<FederationStatusPopupState> for Popup {
fn from(value: FederationStatusPopupState) -> Self {
Popup::FederationStatusDetails {
focus: value.focus,
status_cursor: value.status_cursor,
devices_scroll: value.devices_scroll,
logs_scroll: value.logs_scroll,
}
}
}
impl StatusDetailFocus {
pub fn next(self) -> Self {
match self {
StatusDetailFocus::Status => StatusDetailFocus::Devices,
StatusDetailFocus::Devices => StatusDetailFocus::Logs,
StatusDetailFocus::Logs => StatusDetailFocus::Status,
}
}
pub fn previous(self) -> Self {
match self {
StatusDetailFocus::Status => StatusDetailFocus::Logs,
StatusDetailFocus::Devices => StatusDetailFocus::Status,
StatusDetailFocus::Logs => StatusDetailFocus::Devices,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -745,6 +812,23 @@ impl FedInputField {
FedInputField::ConnectInvite => "Connect device (paste frid://i invite)",
}
}
pub fn help(self) -> &'static str {
match self {
FedInputField::NetworkId => {
"A unique network id. It must match exactly on every client that should see and connect to the same peers."
}
FedInputField::ConnectTicket => {
"Paste a connection ticket generated by another client to connect to that peer directly."
}
FedInputField::DeviceName => {
"A friendly nickname for this device, used only to make connected-device management easier."
}
FedInputField::ConnectInvite => {
"Paste a frid:// invite generated by another client to add this device to its sync group."
}
}
}
}
/// Rows of the federation block inside Settings, in display order.
@@ -944,6 +1028,8 @@ pub struct FederationTab {
pub settings: crate::federation::FedSettings,
pub status: Option<crate::federation::FedStatus>,
pub devices: Option<crate::devices::DeviceSyncStatus>,
pub publishing: bool,
pub device_syncing: bool,
}
/// Playlists eligible as add-targets (the virtual Likes playlist is managed
+6 -1
View File
@@ -2579,7 +2579,12 @@ fn federation_select(state: &mut AppState) -> Option<Effect> {
None
}
SettingsRow::StatusDetails => {
state.popup = Some(Popup::FederationStatusDetails { scroll: 0 });
state.popup = Some(Popup::FederationStatusDetails {
focus: super::state::StatusDetailFocus::Status,
status_cursor: 0,
devices_scroll: 0,
logs_scroll: 0,
});
None
}
SettingsRow::DeviceName => {