use std::collections::HashSet;
use std::sync::LazyLock;
use askama::Template;
use axum::Extension;
use axum::extract::{Path, Query, State};
use axum::response::Response;
use koan_core::auth::Role;
use koan_core::db::queries;
use super::{UiState, events, open, patch};
use crate::auth::AuthUser;
use crate::html::{Html, Render};
use crate::share::blocking;
use crate::subsonic::EntityKind;
pub(super) struct Hearts {
pub tracks: HashSet<i64>,
pub albums: HashSet<i64>,
pub artists: HashSet<i64>,
}
impl Hearts {
pub fn load(conn: &rusqlite::Connection, user: &AuthUser) -> Option<Self> {
if !user.role.has_permission(Role::User) {
return None;
}
Some(Self {
tracks: queries::load_favourites(conn, user.user_id).ok()?,
albums: queries::favourite_album_id_set(conn, user.user_id).ok()?,
artists: queries::favourite_artist_id_set(conn, user.user_id).ok()?,
})
}
pub fn track(&self, id: i64) -> Html {
heart(Kind::Song, id, self.tracks.contains(&id), false)
}
pub fn tile(&self, id: i64) -> Html {
heart(Kind::Album, id, self.albums.contains(&id), true)
}
pub fn album(&self, id: i64) -> Html {
heart(Kind::Album, id, self.albums.contains(&id), false)
}
pub fn artist(&self, id: i64) -> Html {
heart(Kind::Artist, id, self.artists.contains(&id), false)
}
pub fn mark(&self) -> &'static Html {
static MARK: LazyLock<Html> = LazyLock::new(|| Mark.html());
&MARK
}
}
#[derive(Clone, Copy, PartialEq, Eq, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub(super) enum Kind {
Song,
Album,
Artist,
}
impl Kind {
fn as_str(self) -> &'static str {
match self {
Kind::Song => "song",
Kind::Album => "album",
Kind::Artist => "artist",
}
}
fn entity(self) -> EntityKind {
match self {
Kind::Song => EntityKind::Song,
Kind::Album => EntityKind::Album,
Kind::Artist => EntityKind::Artist,
}
}
fn noun(self) -> &'static str {
match self {
Kind::Song => "track",
Kind::Album => "record",
Kind::Artist => "artist",
}
}
}
#[derive(Template)]
#[template(path = "favourite/heart.html")]
struct Heart {
kind: &'static str,
id: i64,
on: bool,
label: String,
tile: bool,
}
#[derive(Template)]
#[template(path = "favourite/mark.html")]
struct Mark;
fn label(kind: Kind, on: bool) -> String {
let verb = if on { "Unfavourite" } else { "Favourite" };
format!("{verb} this {}", kind.noun())
}
fn heart(kind: Kind, id: i64, on: bool, tile: bool) -> Html {
Heart {
kind: kind.as_str(),
id,
on,
label: label(kind, on),
tile,
}
.html()
}
#[derive(serde::Deserialize)]
pub(super) struct On {
on: u8,
}
pub(super) async fn toggle(
State(s): State<UiState>,
Extension(user): Extension<AuthUser>,
Path((kind, id)): Path<(Kind, i64)>,
Query(On { on }): Query<On>,
) -> Response {
let on = on != 0;
let selector = format!("[data-fav=\"{}-{id}\"]", kind.as_str());
if !user.role.has_permission(Role::User) {
return events(Vec::new());
}
let done = blocking(move || {
let db = open(&s.pool)?;
Some(crate::subsonic::favourite(
&db,
user.user_id,
&user.username,
kind.entity(),
id,
on,
))
})
.await;
match done {
Some(Ok(())) => events(vec![patch(
&heart(kind, id, on, false),
Some((&selector, "outer")),
)]),
Some(Err(e)) => {
log::info!("web UI: favourite {} {id}: {e}", kind.as_str());
events(Vec::new())
}
None => events(Vec::new()),
}
}