620bd374de
- Remove all sample/mock data (artists, albums, tracks, queue, history, calendar) - Delete input module (vim.rs, leader.rs) and related UI (which_key, cmdline) - Add mouse support: click tabs, click list items, scroll wheel navigation - Show real disk space via nix::statvfs instead of hardcoded value - Simplify topbar/statusbar by removing mode display and key hints - Hide album/track sections when no artist is selected
207 lines
6.1 KiB
Rust
207 lines
6.1 KiB
Rust
//! Calendar view - upcoming releases.
|
|
|
|
use ratatui::{
|
|
Frame,
|
|
layout::{Constraint, Layout, Rect},
|
|
style::{Modifier, Style},
|
|
text::{Line, Span},
|
|
widgets::Paragraph,
|
|
};
|
|
|
|
use crate::data::CalendarEntry;
|
|
use crate::theme;
|
|
use crate::ui::pane::Pane;
|
|
|
|
struct CalendarCell {
|
|
day: u8,
|
|
dim: bool,
|
|
is_today: bool,
|
|
events: Vec<CalendarEntry>,
|
|
}
|
|
|
|
pub fn render_calendar(frame: &mut Frame, area: Rect, calendar: &[CalendarEntry]) {
|
|
let meta = "upcoming releases · May 2026";
|
|
|
|
let footer = Line::from(vec![
|
|
Span::styled("[h/l]", Style::default().fg(theme::GRAY)),
|
|
Span::styled(" month · ", Style::default().fg(theme::FG2)),
|
|
Span::styled("[Enter]", Style::default().fg(theme::GRAY)),
|
|
Span::styled(" details", Style::default().fg(theme::FG2)),
|
|
Span::raw(" "),
|
|
Span::styled(
|
|
format!("{} upcoming", calendar.len()),
|
|
Style::default().fg(theme::GRAY),
|
|
),
|
|
]);
|
|
|
|
let pane = Pane::new("Calendar")
|
|
.meta(meta)
|
|
.focused(true)
|
|
.footer(footer);
|
|
|
|
let block = pane.build_block();
|
|
let inner = block.inner(area);
|
|
frame.render_widget(block, area);
|
|
|
|
// Build calendar grid for May 2026
|
|
let today_day = 8u8; // May 8, 2026
|
|
let year = 2026u16;
|
|
let month = 5u8;
|
|
|
|
// May 2026 starts on Friday (day_of_week = 5)
|
|
let start_dow = 5u8;
|
|
let days_in_month = 31u8;
|
|
let days_in_prev = 30u8; // April has 30 days
|
|
|
|
let mut cells: Vec<CalendarCell> = Vec::new();
|
|
|
|
// Previous month days
|
|
for i in 0..start_dow {
|
|
cells.push(CalendarCell {
|
|
day: days_in_prev - start_dow + i + 1,
|
|
dim: true,
|
|
is_today: false,
|
|
events: Vec::new(),
|
|
});
|
|
}
|
|
|
|
// Current month days
|
|
for d in 1..=days_in_month {
|
|
let events: Vec<CalendarEntry> = calendar
|
|
.iter()
|
|
.filter(|e| {
|
|
if let Some(day_str) = e.date.split('-').nth(2) {
|
|
if let Ok(day) = day_str.parse::<u8>() {
|
|
let month_match = e.date.contains(&format!("{:04}-{:02}", year, month));
|
|
return month_match && day == d;
|
|
}
|
|
}
|
|
false
|
|
})
|
|
.cloned()
|
|
.collect();
|
|
|
|
cells.push(CalendarCell {
|
|
day: d,
|
|
dim: false,
|
|
is_today: d == today_day,
|
|
events,
|
|
});
|
|
}
|
|
|
|
// Next month days to fill 6 weeks (42 cells)
|
|
let mut next_day = 1u8;
|
|
while cells.len() < 42 {
|
|
cells.push(CalendarCell {
|
|
day: next_day,
|
|
dim: true,
|
|
is_today: false,
|
|
events: Vec::new(),
|
|
});
|
|
next_day += 1;
|
|
}
|
|
|
|
// Layout: header + day-of-week + 6 rows of days
|
|
let chunks = Layout::vertical([
|
|
Constraint::Length(1), // Month header
|
|
Constraint::Length(1), // Day of week labels
|
|
Constraint::Fill(1), // Calendar grid
|
|
])
|
|
.split(inner);
|
|
|
|
// Month header: "◀ May 2026 ▶"
|
|
let header = Line::from(vec![
|
|
Span::styled("◀ ", Style::default().fg(theme::GRAY)),
|
|
Span::styled(
|
|
"May 2026",
|
|
Style::default()
|
|
.fg(theme::YELLOW)
|
|
.add_modifier(Modifier::BOLD),
|
|
),
|
|
Span::styled(" ▶", Style::default().fg(theme::GRAY)),
|
|
]);
|
|
frame.render_widget(
|
|
Paragraph::new(header).alignment(ratatui::layout::Alignment::Center),
|
|
chunks[0],
|
|
);
|
|
|
|
// Day of week labels
|
|
let dow_labels = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
|
|
let cell_width = chunks[2].width / 7;
|
|
let dow_spans: Vec<Span> = dow_labels
|
|
.iter()
|
|
.map(|&d| {
|
|
let pad = cell_width.saturating_sub(3) as usize;
|
|
Span::styled(
|
|
format!("{}{}", d, " ".repeat(pad)),
|
|
Style::default().fg(theme::GRAY),
|
|
)
|
|
})
|
|
.collect();
|
|
frame.render_widget(Paragraph::new(Line::from(dow_spans)), chunks[1]);
|
|
|
|
// Calendar grid - 6 rows x 7 cols
|
|
let row_height = chunks[2].height / 6;
|
|
let grid_rows = Layout::vertical(vec![Constraint::Length(row_height); 6]).split(chunks[2]);
|
|
|
|
for (row_idx, row_area) in grid_rows.iter().enumerate() {
|
|
let col_areas = Layout::horizontal(vec![Constraint::Ratio(1, 7); 7]).split(*row_area);
|
|
|
|
for (col_idx, col_area) in col_areas.iter().enumerate() {
|
|
let cell_idx = row_idx * 7 + col_idx;
|
|
if cell_idx < cells.len() {
|
|
render_calendar_cell(frame, *col_area, &cells[cell_idx]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn render_calendar_cell(frame: &mut Frame, area: Rect, cell: &CalendarCell) {
|
|
if area.height == 0 || area.width == 0 {
|
|
return;
|
|
}
|
|
|
|
let day_style = if cell.dim {
|
|
Style::default().fg(theme::BG4)
|
|
} else if cell.is_today {
|
|
Style::default().fg(theme::FG1)
|
|
} else {
|
|
Style::default().fg(theme::FG2)
|
|
};
|
|
|
|
let mut lines: Vec<Line> = Vec::new();
|
|
|
|
// First line: day number with optional today marker
|
|
let day_line = if cell.is_today {
|
|
Line::from(vec![
|
|
Span::styled(format!("{}", cell.day), day_style),
|
|
Span::styled(" ●", Style::default().fg(theme::ORANGE)),
|
|
])
|
|
} else {
|
|
Line::from(Span::styled(format!("{}", cell.day), day_style))
|
|
};
|
|
lines.push(day_line);
|
|
|
|
// Event lines
|
|
let max_events = (area.height.saturating_sub(1)) as usize;
|
|
for event in cell.events.iter().take(max_events) {
|
|
let event_style = match event.status.as_str() {
|
|
"announced" => Style::default().fg(theme::YELLOW),
|
|
"monitored" => Style::default().fg(theme::GREEN),
|
|
_ => Style::default().fg(theme::FG2),
|
|
};
|
|
|
|
let mut display = event.artist.clone();
|
|
let max_len = area.width.saturating_sub(1) as usize;
|
|
if display.len() > max_len {
|
|
display.truncate(max_len.saturating_sub(1));
|
|
display.push('…');
|
|
}
|
|
|
|
lines.push(Line::from(Span::styled(display, event_style)));
|
|
}
|
|
|
|
let para = Paragraph::new(lines);
|
|
frame.render_widget(para, area);
|
|
}
|