use dioxus::prelude::*;
use hooks::use_player_controller::PlayerController;
fn spotify_active(ctrl: &PlayerController) -> bool {
let cfg = ctrl.config.read();
cfg.server
.as_ref()
.is_some_and(|s| s.service == config::MusicService::Spotify && s.access_token.is_some())
}
#[component]
fn DeviceRow(
icon: &'static str,
name: String,
subtitle: Option<String>,
chosen: bool,
onclick: EventHandler<MouseEvent>,
) -> Element {
rsx! {
div {
class: if chosen {
"w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left cursor-pointer transition-colors"
} else {
"w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left cursor-pointer transition-colors hover:bg-white/5"
},
style: if chosen {
"background: color-mix(in oklab, var(--color-indigo-500) 12%, transparent);"
} else {
""
},
onclick: move |evt| onclick.call(evt),
div {
class: "w-10 h-10 rounded-md flex items-center justify-center shrink-0",
style: if chosen {
"background: color-mix(in oklab, var(--color-indigo-500) 18%, transparent); color: var(--color-indigo-500);"
} else {
"background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.6);"
},
i { class: "{icon} text-sm" }
}
div { class: "flex-1 min-w-0 flex flex-col justify-center gap-0.5",
span {
class: "text-sm truncate",
style: if chosen { "color: var(--color-indigo-500);" } else { "color: #ffffff;" },
"{name}"
}
if let Some(subtitle) = subtitle {
span {
class: "text-xs truncate",
style: if chosen {
"color: color-mix(in oklab, var(--color-indigo-500) 70%, transparent);"
} else {
"color: rgba(255,255,255,0.5);"
},
"{subtitle}"
}
}
}
if chosen {
i {
class: "fa-solid fa-volume-high text-xs shrink-0",
style: "color: var(--color-indigo-500);",
}
}
}
}
}
#[component]
pub fn SpotifyDevicesButton(
#[props(default = false)] compact: bool,
mut is_rightbar_open: Signal<bool>,
mut is_devices_open: Signal<bool>,
) -> Element {
let ctrl = use_context::<PlayerController>();
if !spotify_active(&ctrl) {
return rsx! {};
}
let override_active = ctrl.spotify_device_override.read().is_some();
rsx! {
button {
class: match (compact, override_active) {
(true, true) => "w-7 h-7 flex items-center justify-center text-indigo-400 hover:text-white transition-colors",
(true, false) => "w-7 h-7 flex items-center justify-center text-slate-500 hover:text-white transition-colors",
(false, true) => "text-indigo-400 hover:text-white",
(false, false) => "text-slate-400 hover:text-white",
},
title: i18n::t("spotify_play_on").to_string(),
onclick: move |_| {
let now = !*is_devices_open.peek();
if now {
is_rightbar_open.set(false);
}
is_devices_open.set(now);
},
i { class: if compact { "fa-solid fa-display text-[10px]" } else { "fa-solid fa-display text-xs" } }
}
}
}
#[component]
pub fn SpotifyDevicesPanel(
mut is_devices_open: Signal<bool>,
is_rightbar_open: Signal<bool>,
) -> Element {
let mut ctrl = use_context::<PlayerController>();
let mut devices = use_signal(Vec::<::server::spotify::api::ConnectDevice>::new);
use_effect(move || {
if *is_rightbar_open.read() {
is_devices_open.set(false);
}
});
use_effect(move || {
if !*is_devices_open.read() {
return;
}
let Some(access) = ctrl.spotify_access_token() else {
return;
};
let sdk_device = ctrl.spotify_device.peek().clone();
let mut ctrl = ctrl;
spawn(async move {
let list = match ::server::spotify::api::devices(&access).await {
Ok(list) => list,
Err(e) => {
tracing::warn!(error = %e, "spotify device list refresh failed");
return;
}
};
if let Some(active) = list
.iter()
.find(|d| d.is_active && Some(&d.id) != sdk_device.as_ref())
{
ctrl.spotify_adopt_external(active.id.clone());
}
devices.set(list);
});
});
if !*is_devices_open.read() || !spotify_active(&ctrl) {
return rsx! {};
}
let sdk_device = ctrl.spotify_device.read().clone();
let selected = ctrl.spotify_device_override.read().clone();
rsx! {
div {
id: "spotify-devices-root",
class: "bg-black/40 border-l border-white/5 flex flex-col h-full flex-shrink-0 z-10",
style: "width: 320px; min-width: 320px;",
div {
class: "flex items-center justify-between px-4 py-4 border-b border-white/10",
span {
class: "text-[10px] font-medium uppercase tracking-wider text-white",
"{i18n::t(\"spotify_play_on\")}"
}
button {
class: "text-white/40 hover:text-white",
onclick: move |_| is_devices_open.set(false),
i { class: "fa-solid fa-xmark text-sm" }
}
}
div { class: "flex-1 overflow-y-auto px-2 py-2 flex flex-col gap-0.5",
DeviceRow {
icon: "fa-solid fa-music",
name: i18n::t("spotify_this_app").to_string(),
subtitle: None,
chosen: selected.is_none(),
onclick: move |_| {
ctrl.spotify_select_device(None);
},
}
for d in devices.read().iter().filter(|d| Some(&d.id) != sdk_device.as_ref()).cloned() {
{
let id = d.id.clone();
let chosen = selected.as_deref() == Some(d.id.as_str());
let icon = match d.kind.as_str() {
"Smartphone" => "fa-solid fa-mobile-screen",
"Speaker" => "fa-solid fa-volume-high",
_ => "fa-solid fa-computer",
};
rsx! {
DeviceRow {
key: "{d.id}",
icon,
name: d.name.clone(),
subtitle: (!d.kind.is_empty()).then(|| d.kind.clone()),
chosen,
onclick: move |_| {
ctrl.spotify_select_device(Some(id.clone()));
},
}
}
}
}
}
}
}
}