use std::collections::HashSet;
use crate::NavigationController;
use crate::constants::{COLUMNS_NORMAL, COLUMNS_NORMAL_ALBUM};
use crate::header::Header;
use crate::reorder_buttons::ReorderButtons;
use crate::showcase::{self, ShowcaseProps};
use crate::track_row::TrackRow;
use config::AppConfig;
use dioxus::prelude::*;
use hooks::use_player_controller::PlayerController;
#[component]
pub fn ShowcaseNormal(props: ShowcaseProps) -> Element {
let mut ctrl = use_context::<PlayerController>();
let config = use_context::<Signal<AppConfig>>();
let _nav_ctrl = use_context::<NavigationController>();
let total_seconds: u64 = props.tracks.iter().map(|t| t.duration).sum();
let duration_min = total_seconds / 60;
let cover_for = hooks::use_db_queries::use_cover_resolver(80);
let offline_tracks = config.read().offline_tracks.clone();
let sort_state = use_signal(|| None);
let indexed_tracks: Vec<_> = props
.tracks
.iter()
.cloned()
.enumerate()
.map(|(idx, track)| (track, idx))
.collect();
let sorted_track_pairs = showcase::sorted_track_pairs(&indexed_tracks, *sort_state.read());
let sorted_tracks: Vec<_> = sorted_track_pairs
.iter()
.map(|(track, _)| track.clone())
.collect();
let sorted_tracks_arc = std::sync::Arc::new(sorted_tracks.clone());
let has_multiple_discs = sorted_tracks
.iter()
.filter_map(|t| t.disc_number)
.collect::<HashSet<_>>()
.len()
> 1;
let mut last_disc = None;
let mut last_disc_size = 0;
let currently_playing_path = {
let idx = *ctrl.current_queue_index.read();
ctrl.get_track_at(idx).map(|track| track.id.clone())
};
let current_song_title = ctrl.current_song_title.read().clone();
let current_song_artist = ctrl.current_song_artist.read().clone();
let current_song_album = ctrl.current_song_album.read().clone();
let current_song_duration = *ctrl.current_song_duration.read();
let tracks_for_play_all = sorted_tracks.clone();
let selected_queue_tracks: Vec<_> = sorted_tracks
.iter()
.filter(|track| props.selected_tracks.contains(&track.id))
.cloned()
.collect();
let selected_queue_tracks_arc = std::sync::Arc::new(selected_queue_tracks.clone());
let all_downloaded = !props.tracks.is_empty()
&& props.tracks.iter().all(|t| {
let p = t.id.uid();
let id = p.split(':').nth(1).unwrap_or(&p);
if let Some(path_str) = offline_tracks.get(id) {
std::path::Path::new(path_str).exists()
} else {
false
}
});
let columns = if props.is_album {
COLUMNS_NORMAL_ALBUM
} else {
COLUMNS_NORMAL
};
let column_gap = if cfg!(target_os = "android") {
"0.5rem"
} else {
"1.5rem"
};
let scroll_stat = use_signal(|| 0.0_f64);
let container_height = use_signal(|| 0.0_f64);
const ITEM_HEIGHT: f64 = 56.0;
let scroll_info = crate::virtual_scroll::use_virtual_scroll(
*scroll_stat.read(),
*container_height.read(),
sorted_track_pairs.len(),
ITEM_HEIGHT,
);
rsx! {
div {
class: "select-none flex-1 min-h-0 flex flex-col w-full",
div {
class: if cfg!(target_os = "android") { "flex flex-col items-center text-center gap-4 mb-6 shrink-0" } else { "flex flex-col md:flex-row items-end gap-8 mb-12 shrink-0" },
div { class: if cfg!(target_os = "android") { "w-44 h-44 rounded-xl bg-stone-800 overflow-hidden relative flex-shrink-0" } else { "w-64 h-64 rounded-xl bg-stone-800 overflow-hidden relative flex-shrink-0" },
if let Some(url) = &props.cover_url {
img { src: "{url.as_ref()}", class: "w-full h-full object-cover" }
} else {
div { class: "w-full h-full flex flex-col items-center justify-center text-white/20",
i { class: "fa-solid fa-music text-6xl mb-4" }
}
}
if props.on_cover_click.is_some() {
div {
class: "absolute inset-0 bg-black/50 opacity-0 hover:opacity-100 transition-opacity flex items-center justify-center cursor-pointer rounded-xl",
onclick: move |_| {
if let Some(ref h) = props.on_cover_click {
h.call(());
}
},
i { class: "fa-solid fa-camera text-white text-3xl" }
}
}
}
div { class: "flex-1",
if !props.description.is_empty() {
if let Some(on_description_click) = props.on_description_click {
button {
class: "text-sm font-bold text-white/60 mb-2 text-left cursor-pointer hover:underline hover:text-white transition-colors",
onclick: move |_| on_description_click.call(()),
"{props.description}"
}
} else {
h5 { class: "text-sm font-bold text-white/60 mb-2", "{props.description}" }
}
}
h1 { class: if cfg!(target_os = "android") { "text-3xl font-bold text-white mb-3" } else { "text-5xl md:text-7xl font-bold text-white mb-6" }, "{props.name}" }
div { class: if cfg!(target_os = "android") { "flex items-center justify-center gap-4 text-slate-400" } else { "flex items-center gap-6 text-slate-400" },
{
let count = props.tracks.len();
let song_text = i18n::t_with("showcase_song_count", &[("count", count.to_string())]);
rsx! {
p { "{song_text}" }
}
}
span { "•" }
p { "{duration_min} {i18n::t(\"min\")}" }
}
}
div { class: "flex items-center gap-4",
if !props.tracks.is_empty() {
button {
class: format!("w-14 h-14 rounded-full flex items-center justify-center {}", if *ctrl.shuffle.read() { "text-white" } else { "text-slate-400 hover:text-white" }),
title: if *ctrl.shuffle.read() {
i18n::t("shuffle_on").to_string()
} else {
i18n::t("shuffle_off").to_string()
},
onclick: move |_| ctrl.toggle_shuffle(),
i { class: "fa-solid fa-shuffle text-xl ml-1" }
}
button {
class: "w-14 h-14 rounded-full bg-indigo-500 hover:bg-indigo-400 text-black flex items-center justify-center transition-transform hover:scale-105",
onclick: move |_| {
let is_shuffle = *ctrl.shuffle.peek();
if is_shuffle {
ctrl.play_queue_shuffled(tracks_for_play_all.clone());
} else {
ctrl.play_queue_linear(tracks_for_play_all.clone());
}
},
i { class: "fa-solid fa-play text-xl ml-1" }
}
if props.on_download_all.is_some() || props.on_delete_all.is_some() {
button {
class: "w-12 h-12 rounded-full border border-white/20 hover:border-white/40 text-white/70 hover:text-white flex items-center justify-center transition-colors",
title: if all_downloaded { "Remove downloads" } else { "Download all for offline playback" },
disabled: props.is_downloading_all,
onclick: move |_| {
if all_downloaded {
if let Some(ref h) = props.on_delete_all { h.call(()); }
} else {
if let Some(ref h) = props.on_download_all { h.call(()); }
}
},
if props.is_downloading_all {
i { class: "fa-solid fa-spinner fa-spin" }
} else if all_downloaded {
i { class: "fa-solid fa-trash" }
} else {
i { class: "fa-solid fa-download" }
}
}
}
}
if let Some(actions) = props.actions {
{actions}
}
}
}
div { class: "flex-1 min-h-0 flex flex-col w-full",
if props.tracks.is_empty() {
div { class: "py-12 flex flex-col items-center justify-center text-slate-600",
i { class: "fa-regular fa-folder-open text-4xl mb-4" }
p { class: "text-lg", "{i18n::t(\"no_songs_here\")}" }
}
} else {
div { class: "shrink-0",
Header {
is_modern: false,
is_album: props.is_album,
is_selection_mode: props.is_selection_mode,
on_select_all: props.on_select_all,
all_selected: props.all_selected,
sort_state: sort_state,
is_reorderable: props.is_reorderable
}
}
div { class: "flex-1 min-h-0 w-full flex flex-col overflow-hidden",
crate::virtual_scroll::VirtualScrollView {
id: "normal-showcase-scroll".to_string(),
class: "flex-1 min-h-0 overflow-y-auto pb-20".to_string(),
scroll_stat,
container_height,
item_height: ITEM_HEIGHT,
saved_scroll: 0.0,
top_pad: scroll_info.top_pad,
bottom_pad: scroll_info.bottom_pad,
for (display_idx, (track, idx)) in sorted_track_pairs.iter().enumerate().skip(scroll_info.start_index).take(scroll_info.items_to_render) {
{
let idx = *idx;
let cover_url = cover_for(track);
let is_selected = props.selected_tracks.contains(&track.id);
let matches_current_path = currently_playing_path.as_ref() == Some(&track.id);
let matches_current_metadata = currently_playing_path.is_none()
&& !current_song_title.is_empty()
&& track.title == current_song_title
&& track.artist == current_song_artist
&& track.album == current_song_album
&& track.duration == current_song_duration;
let is_currently_playing: bool = matches_current_path || matches_current_metadata;
let track_count = props.tracks.len();
let can_move_up = props.is_reorderable && idx > 0;
let can_move_down = props.is_reorderable && idx + 1 < track_count;
let path_str = track.id.uid();
let item_id_str: String = path_str.split(':').nth(1).unwrap_or(&path_str).to_string();
let is_downloaded = if let Some(path_str) = offline_tracks.get(&item_id_str) {
std::path::Path::new(path_str).exists()
} else {
false
};
let is_downloading = false;
let play_queue = std::sync::Arc::clone(&sorted_tracks_arc);
let mut is_new_disc = false;
if track.disc_number != last_disc && sort_state.peek().is_none() && props.is_album {
last_disc = track.disc_number;
is_new_disc = true;
last_disc_size = display_idx;
}
rsx! {
div {
key: "{track.id.uid()}",
class: "contents",
div {
class: "flex items-center group",
if has_multiple_discs && props.is_album && is_new_disc && sort_state.peek().is_none() {
div {
class: "flex-1 min-w-0",
div {
class: "grid items-center p-2 rounded-lg hover:bg-white/5 group transition-colors relative select-none",
style: format!("grid-template-columns: {columns}; column-gap: {column_gap};"),
i { class: "fa-solid fa-compact-disc text-center" }
p { "Disc {track.disc_number.unwrap_or(1)}" }
}
}
}
}
div {
class: "flex items-center group",
div { class: "flex-1 min-w-0",
TrackRow {
track: track.clone(),
on_start_radio: crate::track_row::radio_handler(track.clone()),
cover_url: cover_url,
is_menu_open: props.active_track.as_ref() == Some(&track.id),
is_album: props.is_album,
is_selection_mode: props.is_selection_mode,
is_selected: is_selected,
is_downloaded: is_downloaded,
is_downloading: is_downloading,
is_currently_playing,
selected_queue_tracks: (*selected_queue_tracks_arc).clone(),
row_num: Some(display_idx + 1 - last_disc_size),
on_select: move |selected| {
if let Some(handler) = &props.on_select {
handler.call((idx, selected));
}
},
on_long_press: move |_| {
if let Some(handler) = &props.on_long_press {
handler.call(idx);
}
},
on_click_menu: move |_| {
if let Some(handler) = &props.on_click_menu {
handler.call(idx);
}
},
on_add_to_playlist: move |_| {
if let Some(handler) = &props.on_add_to_playlist {
handler.call(idx);
}
},
on_queue: move |_| {
if let Some(handler) = &props.on_queue {
handler.call(idx);
}
},
on_close_menu: move |_| {
if let Some(handler) = &props.on_close_menu {
handler.call(());
}
},
on_delete: move |_| {
if let Some(handler) = &props.on_delete_track {
handler.call(idx);
}
},
on_remove_from_playlist: move |_| {
if let Some(handler) = &props.on_remove_from_playlist {
handler.call(idx);
}
},
on_view_metadata: props.on_view_metadata.map(|h| EventHandler::new(move |_| h.call(idx))),
on_download: move |_| {
if let Some(handler) = &props.on_download_track {
handler.call(idx);
}
},
on_play: move |_| {
ctrl.queue.set((*play_queue).clone());
ctrl.play_track(display_idx);
}
}
}
if props.is_reorderable && !props.is_selection_mode {
ReorderButtons {
can_move_up,
can_move_down,
on_move_up: move |_| props.on_move_up.call(idx),
on_move_down: move |_| props.on_move_down.call(idx),
}
}
}
}
}
}
}
}
}
}
}
}
}
}