dashboard: per-player settings, storage usage; drop Config tab
- Remove the separate Config tab (3 tabs now); each player's settings (content type, mount point, music folder, fat order, sync command) are shown in a PLAYER SETTINGS card on the dashboard - Show mounted storage on the device card: total/free space (statvfs) plus a usage gauge - Add library + sync-pipeline cards and full-width SYNC PATH chain - Update keybar/help/README for 3 tabs
This commit is contained in:
Generated
+1
@@ -258,6 +258,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"dirs",
|
||||
"libc",
|
||||
"ratatui",
|
||||
"serde",
|
||||
"toml",
|
||||
|
||||
@@ -12,6 +12,7 @@ serde = { version = "1", features = ["derive"] }
|
||||
chrono = "0.4"
|
||||
unicode-normalization = "0.1"
|
||||
dirs = "6"
|
||||
libc = "0.2"
|
||||
anyhow = "1"
|
||||
|
||||
[profile.release]
|
||||
|
||||
@@ -13,12 +13,12 @@ living dashboard instead of a batch CLI.
|
||||
bars.
|
||||
- **Devices sidebar** — live mount status (green dot = mounted), firmware tags,
|
||||
last-sync age per device.
|
||||
- **4 tabs** — Dashboard (device + library cards, source-chain diagram), Sync
|
||||
(live step pipeline + progress gauge), Logs (color-coded, follow mode),
|
||||
Config (syntax-coloured TOML).
|
||||
- **Live sync engine** — streams `rclone` and `rsync` output, parses progress,
|
||||
NFC-aware diffing for macOS→FAT32/Android devices, optional `fatsort`
|
||||
finalize for Mlove devices.
|
||||
- **3 tabs** — Dashboard (per-player overview + settings, storage usage when
|
||||
mounted, library + sync pipeline), Sync (live step pipeline + progress
|
||||
gauge), Logs (color-coded, follow mode).
|
||||
- **Live sync engine** — streams `rclone` and `rsync` output, parses progress
|
||||
(byte-weighted via `--info=progress2`), NFC-aware diffing for
|
||||
macOS→FAT32/Android devices, optional `fatsort` finalize for Mlove devices.
|
||||
- **Abort-safe** — `a` kills the running child process cleanly.
|
||||
- Reads the existing `~/.config/dap-sync/config.toml` (compatible with
|
||||
`dap-sync`), persists last-sync state to `~/.local/share/dap-sync/sync-state.toml`.
|
||||
@@ -39,7 +39,7 @@ cargo build --release
|
||||
| `↑`/`↓` `j`/`k` | move selection |
|
||||
| `s` / `Enter`| sync selected device |
|
||||
| `a` | abort running sync |
|
||||
| `Tab` / `1-4`| switch tab |
|
||||
| `Tab` / `1-3`| switch tab |
|
||||
| `r` | refresh device mounts |
|
||||
| `f` | toggle log follow |
|
||||
| `u` / `d` | scroll logs |
|
||||
|
||||
+1
-5
@@ -16,17 +16,15 @@ pub enum Tab {
|
||||
Dashboard = 0,
|
||||
Sync = 1,
|
||||
Logs = 2,
|
||||
Config = 3,
|
||||
}
|
||||
|
||||
impl Tab {
|
||||
pub const ALL: [Tab; 4] = [Tab::Dashboard, Tab::Sync, Tab::Logs, Tab::Config];
|
||||
pub const ALL: [Tab; 3] = [Tab::Dashboard, Tab::Sync, Tab::Logs];
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Tab::Dashboard => "Dashboard",
|
||||
Tab::Sync => "Sync",
|
||||
Tab::Logs => "Logs",
|
||||
Tab::Config => "Config",
|
||||
}
|
||||
}
|
||||
pub fn number(&self) -> usize {
|
||||
@@ -34,7 +32,6 @@ impl Tab {
|
||||
Tab::Dashboard => 1,
|
||||
Tab::Sync => 2,
|
||||
Tab::Logs => 3,
|
||||
Tab::Config => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -196,7 +193,6 @@ impl App {
|
||||
KeyCode::Char('1') => self.tab = Tab::Dashboard,
|
||||
KeyCode::Char('2') => self.tab = Tab::Sync,
|
||||
KeyCode::Char('3') => self.tab = Tab::Logs,
|
||||
KeyCode::Char('4') => self.tab = Tab::Config,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,6 @@ pub struct Config {
|
||||
pub storagebox_hoerspiele_source: String,
|
||||
pub storagebox_excludes: Vec<String>,
|
||||
pub devices: Vec<Device>,
|
||||
pub raw_text: String,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -258,7 +257,6 @@ pub fn load(config_path: Option<String>) -> Result<Config> {
|
||||
.map(|v| v.clone())
|
||||
.unwrap_or_else(|| DEFAULT_EXCLUDES.iter().map(|s| s.to_string()).collect()),
|
||||
devices,
|
||||
raw_text: text,
|
||||
}
|
||||
} else {
|
||||
Config {
|
||||
@@ -270,7 +268,6 @@ pub fn load(config_path: Option<String>) -> Result<Config> {
|
||||
storagebox_hoerspiele_source: DEFAULT_STORAGEBOX_HOERSPIELE_SOURCE.to_string(),
|
||||
storagebox_excludes: DEFAULT_EXCLUDES.iter().map(|s| s.to_string()).collect(),
|
||||
devices: Vec::new(),
|
||||
raw_text: "# no config file found\n".to_string(),
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
use std::path::Path;
|
||||
|
||||
/// Total and available bytes on a mounted filesystem (statvfs).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct DiskStats {
|
||||
pub total: u64,
|
||||
pub available: u64,
|
||||
}
|
||||
|
||||
pub fn disk_stats(path: &Path) -> Option<DiskStats> {
|
||||
let c = std::ffi::CString::new(path.to_str()?).ok()?;
|
||||
let mut s: libc::statvfs = unsafe { std::mem::zeroed() };
|
||||
if unsafe { libc::statvfs(c.as_ptr(), &mut s) } != 0 {
|
||||
return None;
|
||||
}
|
||||
let frsize = (s.f_frsize.max(1)) as u64;
|
||||
Some(DiskStats {
|
||||
total: s.f_blocks as u64 * frsize,
|
||||
available: s.f_bavail as u64 * frsize,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn fmt_bytes(n: u64) -> String {
|
||||
const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
|
||||
let mut v = n as f64;
|
||||
let mut u = 0;
|
||||
while v >= 1024.0 && u < 4 {
|
||||
v /= 1024.0;
|
||||
u += 1;
|
||||
}
|
||||
if u == 0 {
|
||||
format!("{v:.0} {}", UNITS[u])
|
||||
} else {
|
||||
format!("{v:.1} {}", UNITS[u])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::fmt_bytes;
|
||||
|
||||
#[test]
|
||||
fn formats_bytes() {
|
||||
assert_eq!(fmt_bytes(0), "0 B");
|
||||
assert_eq!(fmt_bytes(512), "512 B");
|
||||
assert_eq!(fmt_bytes(1024), "1.0 KiB");
|
||||
assert_eq!(fmt_bytes(137438953472), "128.0 GiB");
|
||||
assert_eq!(fmt_bytes(1_099_511_627_776), "1.0 TiB");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
mod app;
|
||||
mod config;
|
||||
mod disk;
|
||||
mod engine;
|
||||
mod progress;
|
||||
mod ui;
|
||||
|
||||
@@ -353,7 +353,7 @@ pub fn draw_keys(f: &mut Frame, area: Rect, _app: &App) {
|
||||
("↑↓/jk", "nav"),
|
||||
("s", "sync"),
|
||||
("a", "abort"),
|
||||
("Tab/1-4", "tab"),
|
||||
("Tab/1-3", "tab"),
|
||||
("r", "refresh"),
|
||||
("f", "follow"),
|
||||
("?", "help"),
|
||||
|
||||
+1
-4
@@ -56,7 +56,6 @@ fn draw_main(f: &mut Frame, area: Rect, app: &App) {
|
||||
crate::app::Tab::Dashboard => tabs::dashboard::draw(f, content, app),
|
||||
crate::app::Tab::Sync => tabs::sync::draw(f, content, app),
|
||||
crate::app::Tab::Logs => tabs::logs::draw(f, content, app),
|
||||
crate::app::Tab::Config => tabs::config_view::draw(f, content, app),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,15 +81,13 @@ fn draw_help(f: &mut Frame) {
|
||||
" ↑↓ / j k move selection",
|
||||
" s / Enter sync selected device",
|
||||
" a abort running sync",
|
||||
" Tab / 1-4 switch tab",
|
||||
" Tab / 1-3 switch tab",
|
||||
" r refresh devices",
|
||||
" f toggle log follow",
|
||||
" u / d scroll logs",
|
||||
" x clear logs",
|
||||
" ? / h this help",
|
||||
" q / Esc quit",
|
||||
"",
|
||||
" ← → (todo: device nav)",
|
||||
];
|
||||
let mut spans = Vec::new();
|
||||
for (i, line) in lines.iter().enumerate() {
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
use crate::app::App;
|
||||
use crate::ui::theme;
|
||||
use ratatui::Frame;
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Borders, BorderType, Paragraph};
|
||||
|
||||
pub fn draw(f: &mut Frame, area: Rect, app: &App) {
|
||||
let [head, body] = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]).areas(area);
|
||||
|
||||
let path = app.config.config_file.display().to_string();
|
||||
let head_line = Line::from(vec![
|
||||
Span::styled(" config", Style::default().fg(theme::ACCENT).add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" "),
|
||||
Span::styled(path.clone(), Style::default().fg(theme::DIM)),
|
||||
]);
|
||||
f.render_widget(
|
||||
Paragraph::new(head_line).style(Style::default().bg(theme::BG)),
|
||||
head,
|
||||
);
|
||||
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(Style::default().fg(theme::DIM))
|
||||
.style(Style::default().bg(theme::PANEL_BG));
|
||||
let inner = block.inner(body);
|
||||
f.render_widget(block, body);
|
||||
|
||||
let lines: Vec<Line> = app
|
||||
.config
|
||||
.raw_text
|
||||
.lines()
|
||||
.map(syntax_line)
|
||||
.collect();
|
||||
|
||||
let content_h = inner.height.saturating_sub(1) as usize;
|
||||
let offset = lines.len().saturating_sub(content_h);
|
||||
|
||||
f.render_widget(
|
||||
Paragraph::new(lines)
|
||||
.scroll((offset as u16, 0))
|
||||
.style(Style::default().bg(theme::PANEL_BG)),
|
||||
inner,
|
||||
);
|
||||
}
|
||||
|
||||
fn syntax_line(raw: &str) -> Line<'static> {
|
||||
let line = raw;
|
||||
let trimmed = line.trim_start();
|
||||
|
||||
if trimmed.is_empty() {
|
||||
return Line::raw("");
|
||||
}
|
||||
if trimmed.starts_with('#') {
|
||||
return Line::from(Span::styled(line.to_string(), Style::default().fg(theme::DIM)));
|
||||
}
|
||||
if trimmed.starts_with('[') {
|
||||
return Line::from(Span::styled(
|
||||
line.to_string(),
|
||||
Style::default().fg(theme::MAGENTA).add_modifier(Modifier::BOLD),
|
||||
));
|
||||
}
|
||||
if let Some(idx) = line.find('=') {
|
||||
let key = line[..idx].trim_end();
|
||||
let value = line[idx + 1..].trim_start();
|
||||
let value_style = if value.starts_with('"') {
|
||||
Style::default().fg(theme::GREEN)
|
||||
} else if value.starts_with('[') {
|
||||
Style::default().fg(theme::CYAN)
|
||||
} else if value.starts_with("true") || value.starts_with("false") {
|
||||
Style::default().fg(theme::YELLOW)
|
||||
} else {
|
||||
Style::default().fg(theme::ORANGE)
|
||||
};
|
||||
return Line::from(vec![
|
||||
Span::styled(format!("{key}"), Style::default().fg(theme::ACCENT)),
|
||||
Span::styled(" = ", Style::default().fg(theme::DIM)),
|
||||
Span::styled(value.to_string(), value_style),
|
||||
]);
|
||||
}
|
||||
Line::from(Span::styled(line.to_string(), Style::default().fg(theme::FG)))
|
||||
}
|
||||
+180
-49
@@ -1,4 +1,6 @@
|
||||
use crate::app::{relative_ago, App};
|
||||
use crate::config::Firmware;
|
||||
use crate::disk::{disk_stats, fmt_bytes};
|
||||
use crate::ui::theme;
|
||||
use crate::ui::truncate;
|
||||
use ratatui::Frame;
|
||||
@@ -21,25 +23,18 @@ fn card(title: &str) -> Block<'static> {
|
||||
|
||||
fn kv<'a>(label: &str, value: String, value_style: Style) -> Line<'a> {
|
||||
Line::from(vec![
|
||||
Span::styled(
|
||||
format!(" {label:<12}"),
|
||||
Style::default().fg(theme::DIM),
|
||||
),
|
||||
Span::styled(format!(" {label:<14}"), Style::default().fg(theme::DIM)),
|
||||
Span::styled(value, value_style),
|
||||
])
|
||||
}
|
||||
|
||||
fn firmware_badge(fw: &str) -> Span<'static> {
|
||||
let color = match fw {
|
||||
"rockbox" => theme::MAGENTA,
|
||||
"android" => theme::CYAN,
|
||||
"mlove" => theme::ORANGE,
|
||||
_ => theme::GREEN,
|
||||
};
|
||||
Span::styled(
|
||||
format!(" [{fw}] "),
|
||||
Style::default().fg(color).add_modifier(Modifier::BOLD),
|
||||
)
|
||||
fn firmware_color(fw: &Firmware) -> ratatui::style::Color {
|
||||
match fw {
|
||||
Firmware::Rockbox => theme::MAGENTA,
|
||||
Firmware::Android => theme::CYAN,
|
||||
Firmware::Mlove => theme::ORANGE,
|
||||
Firmware::Apple => theme::GREEN,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw(f: &mut Frame, area: Rect, app: &App) {
|
||||
@@ -48,21 +43,41 @@ pub fn draw(f: &mut Frame, area: Rect, app: &App) {
|
||||
return;
|
||||
};
|
||||
|
||||
let [top, chain] = Layout::vertical([Constraint::Min(8), Constraint::Length(4)]).areas(area);
|
||||
let [left, right] =
|
||||
Layout::horizontal([Constraint::Percentage(55), Constraint::Percentage(45)]).areas(top);
|
||||
let [row1, row2, chain] = Layout::vertical([
|
||||
Constraint::Min(9),
|
||||
Constraint::Min(8),
|
||||
Constraint::Length(4),
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
// ── device card ──
|
||||
let dev_block = card("DEVICE");
|
||||
let inner = dev_block.inner(left);
|
||||
f.render_widget(dev_block, left);
|
||||
let [left, right] =
|
||||
Layout::horizontal([Constraint::Percentage(46), Constraint::Percentage(54)]).areas(row1);
|
||||
draw_device_card(f, left, app, device);
|
||||
draw_settings_card(f, right, device);
|
||||
|
||||
let [lib, pipe] =
|
||||
Layout::horizontal([Constraint::Percentage(52), Constraint::Percentage(48)]).areas(row2);
|
||||
draw_library_card(f, lib, app);
|
||||
draw_pipeline_card(f, pipe, device);
|
||||
|
||||
draw_chain(f, chain, app, device);
|
||||
}
|
||||
|
||||
fn draw_device_card(f: &mut Frame, area: Rect, app: &App, device: &crate::config::Device) {
|
||||
let block = card("DEVICE");
|
||||
let inner = block.inner(area);
|
||||
f.render_widget(block, area);
|
||||
|
||||
let name = truncate(&device.name, inner.width.saturating_sub(6) as usize);
|
||||
let fw = device.firmware.label().to_string();
|
||||
let fw_color = firmware_color(&device.firmware);
|
||||
|
||||
let status = if device.mounted {
|
||||
Span::styled("● mounted", Style::default().fg(theme::GREEN))
|
||||
} else {
|
||||
Span::styled("○ not mounted", Style::default().fg(theme::RED))
|
||||
};
|
||||
let fw = device.firmware.label().to_string();
|
||||
|
||||
let last = match device.last_sync {
|
||||
Some(dt) => {
|
||||
if device.last_status == "ok" {
|
||||
@@ -74,36 +89,127 @@ pub fn draw(f: &mut Frame, area: Rect, app: &App) {
|
||||
None => "never".to_string(),
|
||||
};
|
||||
|
||||
let lines = vec![
|
||||
let mut lines: Vec<Line> = vec![
|
||||
Line::from(vec![
|
||||
Span::raw(" "),
|
||||
Span::styled(name, Style::default().fg(theme::FG).add_modifier(Modifier::BOLD)),
|
||||
Span::styled(
|
||||
truncate(&device.name, inner.width.saturating_sub(4) as usize),
|
||||
Style::default().fg(theme::FG).add_modifier(Modifier::BOLD),
|
||||
format!(" [{fw}] "),
|
||||
Style::default().fg(fw_color).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
firmware_badge(&fw),
|
||||
]),
|
||||
Line::raw(""),
|
||||
Line::from(vec![
|
||||
Span::styled(" status", Style::default().fg(theme::DIM)),
|
||||
Span::raw(" "),
|
||||
status.clone(),
|
||||
Span::raw(" "),
|
||||
status,
|
||||
]),
|
||||
kv("firmware", fw.clone(), Style::default().fg(theme::FG)),
|
||||
kv("content", device.content_type.clone(), Style::default().fg(theme::FG)),
|
||||
kv("mount", device.mount_str(), Style::default().fg(theme::CYAN)),
|
||||
kv("folder", device.music_folder.clone(), Style::default().fg(theme::CYAN)),
|
||||
kv("last sync", last, Style::default().fg(theme::YELLOW)),
|
||||
];
|
||||
|
||||
// mounted storage usage (total / available)
|
||||
let storage = if device.mounted {
|
||||
device.mount_point.as_ref().and_then(|p| disk_stats(p))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(stats) = storage {
|
||||
lines.push(Line::raw(""));
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(" storage", Style::default().fg(theme::DIM)),
|
||||
Span::raw(" "),
|
||||
Span::styled(
|
||||
format!(
|
||||
"{} total · {} free",
|
||||
fmt_bytes(stats.total),
|
||||
fmt_bytes(stats.available)
|
||||
),
|
||||
Style::default().fg(theme::CYAN),
|
||||
),
|
||||
]));
|
||||
}
|
||||
let _ = app;
|
||||
f.render_widget(Paragraph::new(lines).style(Style::default().bg(theme::PANEL_BG)), inner);
|
||||
|
||||
// ── library card ──
|
||||
let lib_block = card("LIBRARY");
|
||||
let inner = lib_block.inner(right);
|
||||
f.render_widget(lib_block, right);
|
||||
// storage usage gauge underneath
|
||||
if let Some(stats) = storage {
|
||||
let used = stats.total.saturating_sub(stats.available);
|
||||
let pct = if stats.total > 0 {
|
||||
(used as f64 / stats.total as f64 * 100.0) as u16
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let gauge_area = Rect {
|
||||
y: inner.y + inner.height.saturating_sub(1),
|
||||
x: inner.x,
|
||||
width: inner.width,
|
||||
height: 1,
|
||||
};
|
||||
f.render_widget(
|
||||
ratatui::widgets::Gauge::default()
|
||||
.gauge_style(Style::default().fg(theme::ACCENT).bg(theme::BG_ALT))
|
||||
.ratio((pct as f64) / 100.0)
|
||||
.label(Span::styled(
|
||||
format!(" {pct}% used "),
|
||||
Style::default().fg(theme::FG).add_modifier(Modifier::BOLD),
|
||||
)),
|
||||
gauge_area,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_settings_card(f: &mut Frame, area: Rect, device: &crate::config::Device) {
|
||||
let block = card("PLAYER SETTINGS");
|
||||
let inner = block.inner(area);
|
||||
f.render_widget(block, area);
|
||||
|
||||
let mut lines: Vec<Line> = vec![Line::raw("")];
|
||||
lines.push(kv(
|
||||
"content type",
|
||||
device.content_type.clone(),
|
||||
Style::default().fg(theme::FG),
|
||||
));
|
||||
lines.push(kv(
|
||||
"mount point",
|
||||
device.mount_str(),
|
||||
Style::default().fg(theme::CYAN),
|
||||
));
|
||||
lines.push(kv(
|
||||
"music folder",
|
||||
device.music_folder.clone(),
|
||||
Style::default().fg(theme::CYAN),
|
||||
));
|
||||
lines.push(kv(
|
||||
"fat order",
|
||||
if device.preserve_fat_order { "yes".into() } else { "no".into() },
|
||||
Style::default().fg(if device.preserve_fat_order { theme::YELLOW } else { theme::DIM }),
|
||||
));
|
||||
if device.firmware == Firmware::Apple {
|
||||
lines.push(kv(
|
||||
"sync command",
|
||||
truncate(&device.sync_command, inner.width.saturating_sub(18) as usize),
|
||||
Style::default().fg(theme::MAGENTA),
|
||||
));
|
||||
}
|
||||
lines.push(Line::raw(""));
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(
|
||||
" edit ~/.config/dap-sync/config.toml",
|
||||
Style::default().fg(theme::DIM),
|
||||
),
|
||||
]));
|
||||
f.render_widget(Paragraph::new(lines).style(Style::default().bg(theme::PANEL_BG)), inner);
|
||||
}
|
||||
|
||||
fn draw_library_card(f: &mut Frame, area: Rect, app: &App) {
|
||||
let block = card("LIBRARY");
|
||||
let inner = block.inner(area);
|
||||
f.render_widget(block, area);
|
||||
|
||||
let excludes = app.config.storagebox_excludes.len();
|
||||
let lib_lines = vec![
|
||||
let lines = vec![
|
||||
Line::raw(""),
|
||||
kv(
|
||||
"music",
|
||||
app.config.local_music_dir.display().to_string(),
|
||||
@@ -137,13 +243,41 @@ pub fn draw(f: &mut Frame, area: Rect, app: &App) {
|
||||
Style::default().fg(theme::DIM),
|
||||
),
|
||||
];
|
||||
f.render_widget(Paragraph::new(lib_lines).style(Style::default().bg(theme::PANEL_BG)), inner);
|
||||
|
||||
// ── source chain ──
|
||||
draw_chain(f, chain, app, device.name.clone());
|
||||
f.render_widget(Paragraph::new(lines).style(Style::default().bg(theme::PANEL_BG)), inner);
|
||||
}
|
||||
|
||||
fn draw_chain(f: &mut Frame, area: Rect, app: &App, device_name: String) {
|
||||
fn draw_pipeline_card(f: &mut Frame, area: Rect, device: &crate::config::Device) {
|
||||
let block = card("SYNC PIPELINE");
|
||||
let inner = block.inner(area);
|
||||
f.render_widget(block, area);
|
||||
|
||||
let steps: Vec<(&str, &str)> = vec![
|
||||
("1", "mirror StorageBox → local"),
|
||||
("2", "NFC-aware diff"),
|
||||
("3", "rsync copy"),
|
||||
("4", if device.firmware == Firmware::Mlove {
|
||||
"eject + fatsort"
|
||||
} else {
|
||||
"eject"
|
||||
}),
|
||||
];
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
for (n, label) in steps {
|
||||
lines.push(Line::from(vec![
|
||||
Span::raw(" "),
|
||||
Span::styled(
|
||||
format!(" {n} "),
|
||||
Style::default().fg(theme::BG).bg(theme::ACCENT).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::styled(label, Style::default().fg(theme::FG)),
|
||||
]));
|
||||
lines.push(Line::raw(""));
|
||||
}
|
||||
f.render_widget(Paragraph::new(lines).style(Style::default().bg(theme::PANEL_BG)), inner);
|
||||
}
|
||||
|
||||
fn draw_chain(f: &mut Frame, area: Rect, app: &App, device: &crate::config::Device) {
|
||||
let block = card("SYNC PATH");
|
||||
let inner = block.inner(area);
|
||||
f.render_widget(block, area);
|
||||
@@ -157,7 +291,7 @@ fn draw_chain(f: &mut Frame, area: Rect, app: &App, device_name: String) {
|
||||
|
||||
let src = truncate(&app.config.storagebox_source, seg1.max(3));
|
||||
let local = truncate(&app.config.local_music_dir.display().to_string(), seg2.max(3));
|
||||
let device = truncate(&device_name, seg3.max(3));
|
||||
let device_name = truncate(&device.name, seg3.max(3));
|
||||
|
||||
let mut line = vec![
|
||||
Span::raw(" "),
|
||||
@@ -165,7 +299,7 @@ fn draw_chain(f: &mut Frame, area: Rect, app: &App, device_name: String) {
|
||||
arrow("rclone"),
|
||||
chip(&local),
|
||||
arrow("rsync"),
|
||||
chip(&device),
|
||||
chip(&device_name),
|
||||
];
|
||||
let used: usize = line.iter().map(|s| s.width()).sum();
|
||||
if used < inner.width as usize {
|
||||
@@ -185,10 +319,7 @@ fn chip(text: &str) -> Span<'static> {
|
||||
}
|
||||
|
||||
fn arrow(label: &str) -> Span<'static> {
|
||||
Span::styled(
|
||||
format!(" ─{label}▶ "),
|
||||
Style::default().fg(theme::CYAN),
|
||||
)
|
||||
Span::styled(format!(" ─{label}▶ "), Style::default().fg(theme::CYAN))
|
||||
}
|
||||
|
||||
fn draw_empty(f: &mut Frame, area: Rect) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod config_view;
|
||||
pub mod dashboard;
|
||||
pub mod logs;
|
||||
pub mod sync;
|
||||
|
||||
Reference in New Issue
Block a user