use crate::NavigationController;
use config::PlayerBarPosition;
use dioxus::prelude::*;
use hooks::use_player_controller::{LoopMode, PlayerController};
use player::player::Player;
use crate::shared::{fmt_time, toggle_favorite};
#[component]
pub fn BottombarModern(
mut config: Signal<config::AppConfig>,
mut player: Signal<Player>,
mut is_playing: Signal<bool>,
mut is_fullscreen: Signal<bool>,
mut current_song_duration: Signal<u64>,
mut current_song_progress: Signal<u64>,
queue: Signal<Vec<reader::models::Track>>,
mut current_queue_index: Signal<usize>,
mut current_song_title: Signal<String>,
mut current_song_artist: Signal<String>,
mut current_song_cover_url: Signal<String>,
mut volume: Signal<f32>,
mut persisted_volume: Signal<f32>,
mut is_rightbar_open: Signal<bool>,
) -> Element {
let mut is_dragging = use_signal(|| false);
let mut drag_progress = use_signal(|| 0u64);
let initial_volume = *volume.read();
let mut is_muted = use_signal(move || initial_volume <= f32::EPSILON);
let mut volume_before_mute = use_signal(move || {
if initial_volume > f32::EPSILON {
initial_volume
} else {
0.5f32
}
});
let display_progress = if *is_dragging.read() {
*drag_progress.read()
} else {
*current_song_progress.read()
};
let progress_percent = if *current_song_duration.read() > 0 {
(display_progress as f64 / *current_song_duration.read() as f64) * 100.0
} else {
0.0
};
let volume_percent = *volume.read() * 100.0;
let mut ctrl = use_context::<PlayerController>();
let active_source = use_context::<Signal<::server::source::ActiveSource>>();
let nav_ctrl = use_context::<NavigationController>();
let fav_track = use_memo(move || ctrl.current_track_snapshot.read().clone());
let is_fav = hooks::use_db_queries::use_track_is_favorite(fav_track);
let crate::CompactMode(mut compact_mode) = use_context::<crate::CompactMode>();
if cfg!(target_os = "android") {
let pct = if *current_song_duration.read() > 0 {
(*current_song_progress.read() as f64 / *current_song_duration.read() as f64) * 100.0
} else {
0.0
};
let cover = current_song_cover_url.read().clone();
let fav = is_fav();
return rsx! {
div {
class: "shrink-0 h-[68px] bg-black/85 backdrop-blur-2xl border-t border-white/10 flex items-center px-3 gap-3 relative overflow-hidden mb-[env(safe-area-inset-bottom)]",
onclick: move |_| is_fullscreen.set(true),
div { class: "absolute top-0 left-0 h-[2px] bg-white/10 w-full",
div { class: "h-full bg-white/80 transition-all duration-300", style: "width: {pct}%" }
}
div { class: "w-11 h-11 bg-white/5 rounded shrink-0 overflow-hidden flex items-center justify-center",
if cover.is_empty() {
i { class: "fa-solid fa-music text-white/20" }
} else {
img { src: "{cover}", class: "w-full h-full object-cover" }
}
}
div { class: "flex-1 min-w-0 flex flex-col justify-center gap-0.5",
span { class: "text-[13px] font-semibold text-white/90 truncate leading-tight", "{current_song_title}" }
span { class: "text-[11px] text-slate-400 truncate leading-tight", "{current_song_artist}" }
}
div { class: "flex items-center gap-0.5 pr-1",
button {
class: if fav { "w-10 h-10 flex items-center justify-center text-red-400 active:scale-90 transition-transform" } else { "w-10 h-10 flex items-center justify-center text-slate-400 active:scale-90 transition-transform" },
onclick: move |evt| { evt.stop_propagation(); toggle_favorite(ctrl.current_track_snapshot.read().clone()); },
i { class: if fav { "fa-solid fa-heart text-sm" } else { "fa-regular fa-heart text-sm" } }
}
button {
class: "w-11 h-11 flex items-center justify-center text-white text-xl active:scale-90 transition-transform",
onclick: move |evt| { evt.stop_propagation(); ctrl.toggle(); },
i { class: if *is_playing.read() { "fa-solid fa-pause" } else { "fa-solid fa-play ml-1" } }
}
button {
class: "w-11 h-11 flex items-center justify-center text-white text-lg active:scale-90 transition-transform",
onclick: move |evt| { evt.stop_propagation(); ctrl.play_next(); },
i { class: "fa-solid fa-forward-step" }
}
}
}
};
}
let current_track_snapshot = ctrl.current_track_snapshot.read().clone();
let is_favorite = is_fav();
let heart_class = if is_favorite {
"text-red-400 hover:text-red-300 transition-colors"
} else {
"text-slate-500 hover:text-red-400 transition-colors"
};
let heart_icon = if is_favorite {
"fa-solid fa-heart"
} else {
"fa-regular fa-heart"
};
let position = config.read().player_bar_position;
let border_class = match position {
PlayerBarPosition::Bottom => "border-t border-white/5",
PlayerBarPosition::Top => "border-b border-white/5",
};
let is_radio = *current_song_duration.read() == u64::MAX;
rsx! {
div {
class: "h-16 bg-black/70 backdrop-blur-xl {border_class} px-4 flex items-center gap-3 select-none shrink-0",
div {
class: "flex items-center gap-2 shrink-0",
button {
class: format!("w-7 h-7 flex items-center justify-center transition-all active:scale-90 {}",
if *ctrl.shuffle.read() { "text-white" } else { "text-slate-500 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-[11px]" }
}
button {
class: "w-7 h-7 flex items-center justify-center text-slate-400 hover:text-white transition-all active:scale-90",
onclick: move |_| ctrl.play_prev(),
i { class: "fa-solid fa-backward-step text-sm" }
}
button {
class: "w-8 h-8 flex items-center justify-center rounded-full bg-white/10 hover:bg-white/20 text-white transition-all active:scale-95",
onclick: move |_| ctrl.toggle(),
i { class: if *is_playing.read() { "fa-solid fa-pause text-xs" } else { "fa-solid fa-play text-xs ml-0.5" } }
}
button {
class: "w-7 h-7 flex items-center justify-center text-slate-400 hover:text-white transition-all active:scale-90",
onclick: move |_| ctrl.play_next(),
i { class: "fa-solid fa-forward-step text-sm" }
}
button {
class: format!("w-7 h-7 flex items-center justify-center transition-all active:scale-90 relative {}",
match *ctrl.loop_mode.read() {
LoopMode::None => "text-slate-500 hover:text-white",
_ => "text-white",
}
),
title: match *ctrl.loop_mode.read() {
LoopMode::None => i18n::t("repeat_off").to_string(),
LoopMode::Queue => i18n::t("repeat_queue").to_string(),
LoopMode::Track => i18n::t("repeat_track").to_string(),
},
onclick: move |_| ctrl.toggle_loop(),
i { class: "fa-solid fa-repeat text-[11px]" }
if let LoopMode::Track = *ctrl.loop_mode.read() {
span { class: "absolute -bottom-1.5 left-1/2 -translate-x-1/2 text-[8px] font-bold leading-none", "1" }
}
}
}
div { class: "w-px h-5 bg-white/10 shrink-0" }
div {
class: "w-11 h-11 rounded overflow-hidden bg-white/5 shrink-0 flex items-center justify-center",
if current_song_cover_url.read().is_empty() {
i { class: "fa-solid fa-music text-white/20 text-xs" }
} else {
img { src: "{current_song_cover_url}", class: "w-full h-full object-cover" }
}
}
div {
class: "flex flex-col flex-1 min-w-0 justify-center gap-0.5",
div {
class: "flex items-baseline gap-1.5 min-w-0",
span {
class: "text-xs font-semibold text-white/90 truncate hover:underline cursor-pointer shrink-0 max-w-[40%]",
onclick: move |_| {
let album_id = current_track_snapshot
.as_ref()
.map(|track| track.album_id.clone())
.unwrap_or_default();
nav_ctrl.navigate_to_album(album_id);
},
"{current_song_title}"
}
span { class: "text-white/20 text-[10px] shrink-0", "—" }
span {
class: "text-[11px] text-slate-400 truncate min-w-0 cursor-pointer hover:underline hover:text-slate-300",
onclick: move |_| {
let artist = current_song_artist.read().clone();
nav_ctrl.navigate_to_artist(artist);
},
"{current_song_artist}"
}
}
div {
class: "flex items-center gap-1.5 w-full",
span { class: "text-[9px] text-slate-600 font-mono shrink-0 w-7 text-right", "{fmt_time(display_progress)}" }
div {
class: format!("flex-1 h-[3px] bg-white/10 rounded-full relative {}", if is_radio { "" } else { "group cursor-pointer" }),
div {
class: "absolute top-0 left-0 h-full bg-white/60 group-hover:bg-white rounded-full transition-colors pointer-events-none",
style: "width: {progress_percent}%",
}
div {
class: "absolute top-1/2 -translate-y-1/2 w-2.5 h-2.5 bg-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none -translate-x-1/2",
style: "left: {progress_percent}%",
}
input {
r#type: "range",
min: "0",
max: "{*current_song_duration.read()}",
value: "{display_progress}",
class: format!("absolute top-0 left-0 w-full h-full opacity-0 z-10 {}", if is_radio { "pointer-events-none" } else { "cursor-pointer" }),
disabled: is_radio,
onchange: move |evt| {
if let Ok(val) = evt.value().parse::<f64>().map(|v| v as u64) {
player.write().seek(std::time::Duration::from_secs(val));
current_song_progress.set(val);
drag_progress.set(val);
is_dragging.set(false);
}
},
oninput: move |evt| {
if let Ok(val) = evt.value().parse::<f64>().map(|v| v as u64) {
is_dragging.set(true);
drag_progress.set(val);
}
}
}
}
span { class: "text-[9px] text-slate-600 font-mono shrink-0 w-7", "{fmt_time(*current_song_duration.read())}" }
}
}
div { class: "w-px h-5 bg-white/10 shrink-0" }
div {
class: "flex items-center gap-2 shrink-0",
button {
class: "{heart_class} w-7 h-7 flex items-center justify-center",
title: if is_favorite { i18n::t("remove_from_favorites").to_string() } else { i18n::t("add_to_favorites").to_string() },
onclick: move |_| toggle_favorite(ctrl.current_track_snapshot.read().clone()),
i { class: "{heart_icon} text-xs" }
}
div {
class: "flex items-center gap-1.5",
button {
class: "w-6 h-6 flex items-center justify-center text-slate-500 hover:text-white transition-colors",
onclick: move |_| {
let muted = *is_muted.read();
if muted {
let vol = *volume_before_mute.read();
player.write().set_volume(vol);
volume.set(vol);
persisted_volume.set(vol);
is_muted.set(false);
} else {
volume_before_mute.set(*volume.read());
player.write().set_volume(0.0);
volume.set(0.0);
persisted_volume.set(0.0);
is_muted.set(true);
}
},
i { class: if *is_muted.read() { "fa-solid fa-volume-xmark text-[10px]" } else { "fa-solid fa-volume-high text-[10px]" } }
}
div {
class: "w-20 h-[3px] bg-white/10 rounded-full group/vol cursor-pointer relative",
onwheel: move |evt| {
evt.stop_propagation();
let dy = evt.delta().strip_units().y;
if dy.abs() < f64::EPSILON {
return;
}
let step = config.read().volume_scroll_step.max(0.0);
let dir = if dy < 0.0 { 1.0 } else { -1.0 };
let current = *volume.read();
let new_val = (current + dir * step).clamp(0.0, 1.0);
player.write().set_volume(new_val);
volume.set(new_val);
persisted_volume.set(new_val);
is_muted.set(new_val <= f32::EPSILON);
if new_val > f32::EPSILON {
volume_before_mute.set(new_val);
}
},
div {
class: "absolute top-0 left-0 h-full bg-white/60 group-hover/vol:bg-white rounded-full transition-colors pointer-events-none",
style: "width: {volume_percent}%",
}
div {
class: "absolute top-1/2 -translate-y-1/2 w-2.5 h-2.5 bg-white rounded-full opacity-0 group-hover/vol:opacity-100 transition-opacity pointer-events-none -translate-x-1/2",
style: "left: {volume_percent}%",
}
input {
r#type: "range",
min: "0",
max: "1",
step: "0.01",
value: "{*volume.read()}",
class: "absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer z-10",
onchange: move |evt| {
if let Ok(val) = evt.value().parse::<f32>() {
persisted_volume.set(val);
is_muted.set(val == 0.0);
}
},
oninput: move |evt| {
if let Ok(val) = evt.value().parse::<f32>() {
player.write().set_volume(val);
volume.set(val);
is_muted.set(val == 0.0);
if val > f32::EPSILON {
volume_before_mute.set(val);
}
}
}
}
}
}
button {
class: "w-7 h-7 flex items-center justify-center text-slate-500 hover:text-white transition-colors",
onclick: move |_| { let c = *is_rightbar_open.read(); is_rightbar_open.set(!c); },
i { class: "fa-solid fa-list text-[10px]" }
}
button {
class: "w-7 h-7 flex items-center justify-center text-slate-500 hover:text-white transition-colors",
title: i18n::t("share_musicbrainz").to_string(),
onclick: move |_| {
if let Some(t) = ctrl.current_track_snapshot.read().clone() {
let src = active_source.peek().clone();
crate::track_row::share_track(t, src);
}
},
i { class: "fa-solid fa-share-nodes text-[10px]" }
}
if cfg!(all(not(target_arch = "wasm32"), not(target_os = "android"))) {
button {
class: "w-7 h-7 flex items-center justify-center text-slate-500 hover:text-white transition-colors",
title: i18n::t("mini_player").to_string(),
onclick: move |_| { let c = *compact_mode.read(); compact_mode.set(!c); },
i { class: "fa-solid fa-compress text-[10px]" }
}
}
button {
class: "w-7 h-7 flex items-center justify-center text-slate-500 hover:text-white transition-colors",
onclick: move |_| is_fullscreen.set(true),
i { class: "fa-solid fa-up-right-and-down-left-from-center text-[10px]" }
}
}
}
}
}