Added network catalog slices

This commit is contained in:
Ultradesu
2026-07-25 22:40:27 +03:00
parent b725ca8d05
commit 788bc2e01f
14 changed files with 878 additions and 152 deletions
+39 -7
View File
@@ -12,7 +12,7 @@ use crate::app::state::{
fed_release_groups, release_groups,
};
use crate::art::cache_key;
use crate::library::models::{ArtistCard, ReleaseCard, SearchResults};
use crate::library::models::{ArtistCard, Availability, ReleaseCard, SearchResults};
const TILE_MARQUEE_STEP_MS: u128 = 250;
const TILE_MARQUEE_PAUSE_STEPS: u128 = 4;
@@ -83,13 +83,14 @@ fn draw_art(frame: &mut Frame, area: Rect, art_state: Option<&ArtState>) {
/// Bordered tile with artwork, a title line and a dim meta line. The
/// selected tile gets a thick accent border and an inverted (filled)
/// caption so it stands out in a large grid; the artwork stays untouched.
fn draw_tile(
fn draw_tile_with_availability(
frame: &mut Frame,
tile: Rect,
art_state: Option<&ArtState>,
title: &str,
meta: &str,
selected: bool,
availability: Option<Availability>,
) {
let block = if selected {
Block::bordered()
@@ -106,6 +107,9 @@ fn draw_tile(
..inner
};
draw_art(frame, art_area, art_state);
if let Some(availability) = availability {
draw_availability_badge(frame, art_area, availability);
}
if inner.height > ART_CELL_HEIGHT {
let name_area = Rect {
@@ -137,6 +141,27 @@ fn draw_tile(
}
}
fn draw_availability_badge(frame: &mut Frame, area: Rect, availability: Availability) {
if area.width < 2 || area.height == 0 {
return;
}
let (label, style) = match availability {
Availability::Local => ("", Style::new().fg(Color::Green)),
Availability::Mixed => ("", Style::new().fg(Color::Yellow)),
Availability::Remote => ("", theme::accent()),
};
let badge = Rect {
x: area.x + area.width.saturating_sub(2),
y: area.y,
width: 2,
height: 1,
};
frame.render_widget(
Paragraph::new(Line::styled(label, style)).alignment(Alignment::Right),
badge,
);
}
fn tile_title(title: &str, width: u16, selected: bool) -> String {
let width = usize::from(width);
if width == 0 {
@@ -272,9 +297,13 @@ fn scroll_offset(items: &[PlanItem], cursor_item: Option<usize>, viewport: u16)
fn draw_grid(frame: &mut Frame, area: Rect, state: &AppState) {
let global = &state.global;
let title = if global.total > 0 {
format!(" Library — {} artists ", global.total)
format!(
" Library — {} artists · {} ",
global.total,
global.filters.source_mode.label()
)
} else {
" Library ".to_string()
format!(" Library · {} ", global.filters.source_mode.label())
};
let mut title_spans = vec![Span::styled(title, theme::tab_active())];
if global.filters.is_active() {
@@ -323,13 +352,14 @@ fn draw_grid_tiles(frame: &mut Frame, inner: Rect, state: &AppState) {
width: TILE_WIDTH,
height: TILE_HEIGHT,
};
draw_tile(
draw_tile_with_availability(
frame,
tile,
tile_art(state, artist.image_path.as_ref()),
&artist.name,
&artist_tile_meta(artist),
index == global.selected,
Some(artist.availability),
);
}
}
@@ -557,13 +587,14 @@ fn draw_artist(frame: &mut Frame, area: Rect, state: &AppState, id: i64, cursor:
if tile.width < 3 {
break;
}
draw_tile(
draw_tile_with_availability(
frame,
tile,
tile_art(state, release.cover_path.as_ref()),
&release.title,
&release_tile_meta(release),
cursor == tracks + position,
Some(release.availability),
);
}
}
@@ -1055,13 +1086,14 @@ fn draw_fed_artist(frame: &mut Frame, area: Rect, state: &AppState, cursor: usiz
if let Some(year) = release.year {
meta = format!("{meta} · {year}");
}
draw_tile(
draw_tile_with_availability(
frame,
tile,
tile_art(state, release.cover_path.as_ref()),
&release.title,
&meta,
cursor == *position,
Some(Availability::Remote),
);
}
}
+30 -16
View File
@@ -371,7 +371,7 @@ fn clip_cells(text: &str, max_width: usize) -> String {
}
fn draw_library_filters(frame: &mut Frame, state: &AppState, cursor: usize) {
let area = centered(frame.area(), 46, 6);
let area = centered(frame.area(), 54, 9);
let block = Block::bordered()
.title(" Library filters ")
.title_style(theme::header())
@@ -381,33 +381,47 @@ fn draw_library_filters(frame: &mut Frame, state: &AppState, cursor: usize) {
frame.render_widget(block, area);
let [list_area, _, footer] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(4),
Constraint::Length(1),
Constraint::Length(1),
])
.areas(inner);
let mut rows: Vec<Line<'static>> = Vec::new();
let checked = if state.global.filters.hide_featured_only {
"[x]"
} else {
"[ ]"
};
let row = Rect {
height: 1,
..list_area
};
frame.render_widget(
Paragraph::new(Line::from(vec![
Span::styled(format!("{checked} "), theme::accent()),
Span::raw("Hide featured only"),
])),
row,
);
if cursor == 0 {
frame.buffer_mut().set_style(row, theme::tab_active());
rows.push(Line::from(vec![
Span::styled(format!("{checked} "), theme::accent()),
Span::raw("Hide featured only"),
]));
for mode in crate::config::settings::LibrarySourceMode::ALL {
let marker = if state.global.filters.source_mode == mode {
"(*)"
} else {
"( )"
};
rows.push(Line::from(vec![
Span::styled(format!("{marker} "), theme::accent()),
Span::raw(mode.label()),
Span::styled(format!(" {}", mode.description()), theme::dim()),
]));
}
for (index, line) in rows.into_iter().enumerate() {
let row = Rect {
y: list_area.y + index as u16,
height: 1,
..list_area
};
frame.render_widget(Paragraph::new(line), row);
if cursor == index {
frame.buffer_mut().set_style(row, theme::tab_active());
}
}
frame.render_widget(
Paragraph::new(Line::styled("space/enter toggle · esc close", theme::dim()))
Paragraph::new(Line::styled("space/enter select · esc close", theme::dim()))
.alignment(Alignment::Center),
footer,
);