= gen.tracks.iter().map(|t| t.track_id).collect();
match api::save_playlist(&name, None, &track_ids).await {
Ok(_) => {
message.set(Some(format!("Saved playlist: {name}")));
refresh.emit(());
active_tab.set("saved".to_string());
}
Err(e) => error.set(Some(e.0)),
}
}
});
})
};
// --- Tab bar ---
let tab_bar = {
let active = (*active_tab).clone();
let tab = active_tab.clone();
let editing = editing_playlist.clone();
html! {
{ "Saved Playlists" }
{ "Generate" }
if let Some(ref detail) = *editing {
{ format!("Edit: {}", detail.playlist.name) }
}
}
};
// --- Tab 1: Saved Playlists ---
let saved_tab = {
let active_tab = active_tab.clone();
let editing_playlist = editing_playlist.clone();
let edit_name = edit_name.clone();
let saved_playlists = saved_playlists.clone();
let refresh_playlists = refresh_playlists.clone();
let saved_list = if let Some(ref playlists) = *saved_playlists {
if playlists.is_empty() {
html! { { "No saved playlists." }
}
} else {
html! {
{ "Name" }
{ "Tracks" }
{ "Created" }
{ for playlists.iter().map(|p| {
let id = p.id;
let refresh = refresh_playlists.clone();
let editing = editing_playlist.clone();
let tab = active_tab.clone();
let en = edit_name.clone();
html! {
{ &p.name }
{ p.track_count }
{ &p.created_at }
}
})}
}
}
} else {
html! { { "Loading..." }
}
};
html! {
{ "Generate" }
{
// Extract the new playlist id and open it for editing
if let Some(id) = resp.get("id").and_then(|v| v.as_i64()) {
let id = id as i32;
edit_name.set("New Playlist".to_string());
if let Ok(detail) = api::get_playlist(id).await {
editing.set(Some(detail));
tab.set("edit".to_string());
}
refresh.emit(());
}
}
Err(e) => error.set(Some(e.0)),
}
});
})
}}>{ "New" }
{ saved_list }
}
};
// --- Tab 2: Generate ---
let generate_tab = {
let strategy_inputs = {
let seed_input_c = seed_input.clone();
let seeds_c = seeds.clone();
let query = (*seed_input_c).to_lowercase();
let filtered: Vec<_> = (*all_artists)
.iter()
.filter(|a| {
if query.is_empty() {
return true;
}
let name_lower = a.name.to_lowercase();
let mut chars = query.chars();
let mut current = chars.next();
for c in name_lower.chars() {
if current == Some(c) {
current = chars.next();
}
}
current.is_none()
})
.filter(|a| !seeds_c.contains(&a.name))
.take(15)
.cloned()
.collect();
html! {
}
};
let track_list = if let Some(ref gen) = *generated {
html! {
}
} else {
html! {}
};
html! {
{ "Generate Playlist" }
{ "Build a playlist from similar artists in your library using Last.fm data." }
{ strategy_inputs }
{ format!("Count: {}", *count) }
{ if *loading { "Generating..." } else { "Generate" } }
{ track_list }
}
};
// --- Tab 3: Edit ---
let edit_tab = {
let editing = editing_playlist.clone();
let active_tab_c = active_tab.clone();
let edit_name_c = edit_name.clone();
let refresh = refresh_playlists.clone();
let error_c = error.clone();
let message_c = message.clone();
let track_search_input_c = track_search_input.clone();
let track_search_focused_c = track_search_focused.clone();
let all_tracks_c = all_tracks.clone();
let dragging_index_c = dragging_index.clone();
if let Some(ref detail) = *editing {
let playlist_id = detail.playlist.id;
let tracks = detail.tracks.clone();
// Filter tracks for add-track search
let search_query = (*track_search_input_c).to_lowercase();
let existing_ids: Vec = tracks.iter().map(|t| t.id).collect();
let filtered_tracks: Vec<_> = if search_query.is_empty() {
vec![]
} else {
(*all_tracks_c)
.iter()
.filter(|t| {
if existing_ids.contains(&t.id) {
return false;
}
let title_lower = t.title.as_deref().unwrap_or("").to_lowercase();
let artist_lower = t.artist.as_deref().unwrap_or("").to_lowercase();
// Subsequence match on title or artist
let matches_field = |field: &str| {
let mut chars = search_query.chars();
let mut current = chars.next();
for c in field.chars() {
if current == Some(c) {
current = chars.next();
}
}
current.is_none()
};
matches_field(&title_lower) || matches_field(&artist_lower)
})
.take(15)
.cloned()
.collect()
};
html! {
{ "Back to Playlists" }
{ format!("Tracks ({})", tracks.len()) }
{ "#" }
{ "Title" }
{ "Artist" }
{ "Album" }
{ for tracks.iter().enumerate().map(|(i, t)| {
let track_id = t.id;
let editing = editing.clone();
let dragging = dragging_index_c.clone();
let idx = i;
let ondragstart = {
let dragging = dragging.clone();
Callback::from(move |e: DragEvent| {
dragging.set(Some(idx));
if let Some(dt) = e.data_transfer() {
let _ = dt.set_data("text/plain", &idx.to_string());
}
})
};
let ondragover = Callback::from(move |e: DragEvent| {
e.prevent_default();
});
let ondrop = {
let editing = editing.clone();
let dragging = dragging.clone();
Callback::from(move |e: DragEvent| {
e.prevent_default();
let target_idx = idx;
if let Some(source_idx) = *dragging {
if source_idx != target_idx {
let editing_inner = editing.clone();
let dragging_inner = dragging.clone();
// Reorder locally
if let Some(ref detail) = *editing {
let mut new_tracks = detail.tracks.clone();
let item = new_tracks.remove(source_idx);
new_tracks.insert(target_idx, item);
let track_ids: Vec = new_tracks.iter().map(|t| t.id).collect();
let new_detail = PlaylistDetail {
playlist: detail.playlist.clone(),
tracks: new_tracks,
};
editing_inner.set(Some(new_detail));
dragging_inner.set(None);
// Call reorder API
wasm_bindgen_futures::spawn_local(async move {
let _ = api::reorder_playlist_tracks(playlist_id, &track_ids).await;
});
}
}
}
dragging.set(None);
})
};
let on_remove = {
let editing = editing.clone();
Callback::from(move |_: MouseEvent| {
let editing = editing.clone();
wasm_bindgen_futures::spawn_local(async move {
let _ = api::remove_track_from_playlist(playlist_id, track_id).await;
// Refresh the playlist
if let Ok(detail) = api::get_playlist(playlist_id).await {
editing.set(Some(detail));
}
});
})
};
html! {
{ "\u{2261}" }
{ i + 1 }
{ t.title.as_deref().unwrap_or("Unknown") }
{ t.artist.as_deref().unwrap_or("Unknown") }
{ t.album.as_deref().unwrap_or("") }
{ "\u{00d7}" }
}
})}
// Add track section
{ "Add Track" }
if *track_search_focused_c && !filtered_tracks.is_empty() {
{ for filtered_tracks.iter().map(|t| {
let tid = t.id;
let title = t.title.clone().unwrap_or_else(|| "Unknown".to_string());
let artist = t.artist.clone().unwrap_or_else(|| "Unknown".to_string());
let editing = editing.clone();
let tsi = track_search_input_c.clone();
html! {
{ format!("{} - {}", title, artist) }
}
})}
}
}
} else {
html! { { "No playlist selected for editing." }
}
}
};
html! {
if let Some(ref msg) = *message {
{ msg }
}
if let Some(ref err) = *error {
{ err }
}
{ tab_bar }
if *active_tab == "saved" {
{ saved_tab }
}
if *active_tab == "generate" {
{ generate_tab }
}
if *active_tab == "edit" {
{ edit_tab }
}
}
}