2026-06-10 16:11:09 +01:00
|
|
|
use ratatui::Frame;
|
|
|
|
|
use ratatui::layout::{Alignment, Rect};
|
|
|
|
|
use ratatui::style::{Color, Style};
|
|
|
|
|
use ratatui::text::{Line, Span};
|
|
|
|
|
use ratatui::widgets::{Block, Paragraph};
|
|
|
|
|
|
2026-07-25 01:23:29 +03:00
|
|
|
use super::{loading_line, theme, track_row_with_like_marker};
|
|
|
|
|
use crate::app::state::{AppState, LIKES_PLAYLIST_ID, Loadable, TrackSelectionScope};
|
2026-06-10 16:11:09 +01:00
|
|
|
use crate::app::update::playlist_tracks;
|
|
|
|
|
|
|
|
|
|
pub fn draw(frame: &mut Frame, area: Rect, state: &AppState) {
|
|
|
|
|
match state.playlists.opened {
|
|
|
|
|
Some(opened) => draw_opened(frame, area, state, opened.id, opened.cursor),
|
|
|
|
|
None => draw_list(frame, area, state),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-26 01:41:16 +03:00
|
|
|
fn bordered(frame: &mut Frame, area: Rect, state: &AppState, title: String) -> Rect {
|
2026-06-10 16:11:09 +01:00
|
|
|
let block = Block::bordered()
|
|
|
|
|
.title(title)
|
2026-07-26 01:41:16 +03:00
|
|
|
.title_style(theme::header_for(state))
|
|
|
|
|
.border_style(theme::border_for(state));
|
2026-06-10 16:11:09 +01:00
|
|
|
let inner = block.inner(area);
|
|
|
|
|
frame.render_widget(block, area);
|
|
|
|
|
inner
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn centered_line(frame: &mut Frame, area: Rect, line: Line) {
|
|
|
|
|
if area.height == 0 {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-06-10 23:30:03 +01:00
|
|
|
let middle = Rect {
|
|
|
|
|
y: area.y + area.height / 2,
|
|
|
|
|
height: 1,
|
|
|
|
|
..area
|
|
|
|
|
};
|
2026-06-10 16:11:09 +01:00
|
|
|
frame.render_widget(Paragraph::new(line).alignment(Alignment::Center), middle);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn draw_list(frame: &mut Frame, area: Rect, state: &AppState) {
|
2026-07-27 14:13:56 +01:00
|
|
|
let inner = bordered(
|
|
|
|
|
frame,
|
|
|
|
|
area,
|
|
|
|
|
state,
|
|
|
|
|
format!(
|
|
|
|
|
" Playlists · Mode: {} ",
|
|
|
|
|
state.global.filters.source_mode.label()
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-06-10 16:11:09 +01:00
|
|
|
let selected = state.playlists.selected;
|
|
|
|
|
|
|
|
|
|
let list = match &state.playlists.list {
|
|
|
|
|
Some(Loadable::Ready(list)) => list,
|
|
|
|
|
Some(Loadable::Failed(error)) => {
|
|
|
|
|
return centered_line(
|
|
|
|
|
frame,
|
|
|
|
|
inner,
|
|
|
|
|
Line::styled(error.clone(), Style::new().fg(Color::Red)),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
_ => {
|
2026-07-25 01:23:29 +03:00
|
|
|
return centered_line(frame, inner, loading_line(state, "loading playlists…"));
|
2026-06-10 16:11:09 +01:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
if list.is_empty() {
|
|
|
|
|
return centered_line(frame, inner, Line::styled("no playlists yet", theme::dim()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let visible = usize::from(inner.height.max(1));
|
|
|
|
|
let first = selected
|
|
|
|
|
.saturating_sub(visible / 2)
|
|
|
|
|
.min(list.len().saturating_sub(visible));
|
|
|
|
|
for (index, playlist) in list.iter().enumerate().skip(first).take(visible) {
|
|
|
|
|
let row = Rect {
|
|
|
|
|
x: inner.x,
|
|
|
|
|
y: inner.y + (index - first) as u16,
|
|
|
|
|
width: inner.width,
|
|
|
|
|
height: 1,
|
|
|
|
|
};
|
|
|
|
|
let marker = if playlist.kind == "likes" {
|
2026-07-26 01:41:16 +03:00
|
|
|
Span::styled("♥ ", theme::accent_for(state))
|
2026-06-10 16:11:09 +01:00
|
|
|
} else {
|
|
|
|
|
Span::raw(" ")
|
|
|
|
|
};
|
|
|
|
|
frame.render_widget(
|
2026-07-16 20:29:51 +03:00
|
|
|
Paragraph::new(Line::from(vec![marker, Span::raw(playlist.title.clone())])),
|
2026-06-10 16:11:09 +01:00
|
|
|
row,
|
|
|
|
|
);
|
|
|
|
|
frame.render_widget(
|
|
|
|
|
Paragraph::new(Line::styled(
|
|
|
|
|
format!("{} trk", playlist.track_count),
|
|
|
|
|
theme::dim(),
|
|
|
|
|
))
|
|
|
|
|
.alignment(Alignment::Right),
|
|
|
|
|
row,
|
|
|
|
|
);
|
|
|
|
|
if index == selected {
|
2026-07-26 01:41:16 +03:00
|
|
|
frame
|
|
|
|
|
.buffer_mut()
|
|
|
|
|
.set_style(row, theme::tab_active_for(state));
|
2026-06-10 16:11:09 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn draw_opened(frame: &mut Frame, area: Rect, state: &AppState, id: i64, cursor: usize) {
|
|
|
|
|
let loadable = state.playlist_views.get(&id);
|
|
|
|
|
let title = match loadable {
|
2026-07-27 14:13:56 +01:00
|
|
|
Some(Loadable::Ready(detail)) => format!(
|
|
|
|
|
" Playlists ▸ {} · Mode: {} ",
|
|
|
|
|
detail.title,
|
|
|
|
|
state.global.filters.source_mode.label()
|
|
|
|
|
),
|
|
|
|
|
_ => format!(
|
|
|
|
|
" Playlists ▸ … · Mode: {} ",
|
|
|
|
|
state.global.filters.source_mode.label()
|
|
|
|
|
),
|
2026-06-10 16:11:09 +01:00
|
|
|
};
|
2026-07-26 01:41:16 +03:00
|
|
|
let inner = bordered(frame, area, state, title);
|
2026-06-10 16:11:09 +01:00
|
|
|
|
|
|
|
|
if let Some(Loadable::Failed(error)) = loadable {
|
|
|
|
|
return centered_line(
|
|
|
|
|
frame,
|
|
|
|
|
inner,
|
|
|
|
|
Line::styled(error.clone(), Style::new().fg(Color::Red)),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
let Some(tracks) = playlist_tracks(state, id) else {
|
2026-07-25 01:23:29 +03:00
|
|
|
return centered_line(frame, inner, loading_line(state, "loading…"));
|
2026-06-10 16:11:09 +01:00
|
|
|
};
|
|
|
|
|
if tracks.is_empty() {
|
2026-06-10 23:30:03 +01:00
|
|
|
return centered_line(
|
|
|
|
|
frame,
|
|
|
|
|
inner,
|
|
|
|
|
Line::styled("no tracks here yet", theme::dim()),
|
|
|
|
|
);
|
2026-06-10 16:11:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let visible = usize::from(inner.height.max(1));
|
|
|
|
|
let first = cursor
|
|
|
|
|
.saturating_sub(visible / 2)
|
|
|
|
|
.min(tracks.len().saturating_sub(visible));
|
|
|
|
|
for (index, track) in tracks.iter().enumerate().skip(first).take(visible) {
|
|
|
|
|
let row = Rect {
|
|
|
|
|
x: inner.x,
|
|
|
|
|
y: inner.y + (index - first) as u16,
|
|
|
|
|
width: inner.width,
|
|
|
|
|
height: 1,
|
|
|
|
|
};
|
2026-07-25 01:23:29 +03:00
|
|
|
track_row_with_like_marker(
|
2026-06-10 23:30:03 +01:00
|
|
|
frame,
|
|
|
|
|
row,
|
|
|
|
|
state,
|
|
|
|
|
track,
|
|
|
|
|
(index + 1).to_string(),
|
|
|
|
|
index == cursor,
|
2026-06-15 12:28:34 +01:00
|
|
|
state
|
|
|
|
|
.track_selection
|
|
|
|
|
.contains(&TrackSelectionScope::Playlist(id), index),
|
2026-07-25 01:23:29 +03:00
|
|
|
id != LIKES_PLAYLIST_ID,
|
2026-06-10 23:30:03 +01:00
|
|
|
);
|
2026-06-10 16:11:09 +01:00
|
|
|
}
|
|
|
|
|
}
|