ADMIN: Added track management
Build and Publish / Build and Publish Docker Image (push) Successful in 3m5s
Build and Publish / Build and Publish Docker Image (push) Successful in 3m5s
This commit is contained in:
Generated
+1
-1
@@ -1418,7 +1418,7 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
||||
|
||||
[[package]]
|
||||
name = "furumusic"
|
||||
version = "0.2.5"
|
||||
version = "0.2.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "furumusic"
|
||||
version = "0.2.6"
|
||||
version = "0.2.7"
|
||||
edition = "2024"
|
||||
description = "Reusable web-app boilerplate: auth, OIDC/SSO, admin panel, user management, i18n, PostgreSQL"
|
||||
|
||||
|
||||
+34
-8
@@ -9,7 +9,7 @@ use cot::response::IntoResponse;
|
||||
use cot::session::Session;
|
||||
use cot::{Body, Template};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use sqlx::{PgPool, Postgres, QueryBuilder};
|
||||
|
||||
use super::BUILD_INFO;
|
||||
@@ -68,9 +68,12 @@ pub(super) struct UpdateLibraryItemRequest {
|
||||
title: String,
|
||||
hidden: bool,
|
||||
release_type: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_optional_stringish")]
|
||||
year: Option<String>,
|
||||
release_id: Option<i64>,
|
||||
#[serde(default, deserialize_with = "deserialize_optional_stringish")]
|
||||
track_number: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_optional_stringish")]
|
||||
disc_number: Option<String>,
|
||||
artist_ids: Option<Vec<i64>>,
|
||||
}
|
||||
@@ -1876,7 +1879,7 @@ async fn fetch_library_item(
|
||||
"releases" => {
|
||||
sqlx::query_as::<_, LibraryItemRow>(
|
||||
"SELECT r.id, r.title::text AS title, \
|
||||
CONCAT(r.release_type::text, COALESCE(' · ' || r.year::text, '')) AS subtitle, \
|
||||
(COALESCE(NULLIF(STRING_AGG(DISTINCT a.name::text, ', '), ''), 'Unknown artist') || COALESCE(' / ' || r.year::text, '')) AS subtitle, \
|
||||
r.is_hidden, COUNT(DISTINCT t.id)::bigint AS primary_count, \
|
||||
COUNT(DISTINCT ra.artist_id)::bigint AS secondary_count, \
|
||||
COUNT(DISTINCT ph.id)::bigint AS tertiary_count, \
|
||||
@@ -1884,6 +1887,7 @@ async fn fetch_library_item(
|
||||
FROM furumusic__release r \
|
||||
LEFT JOIN furumusic__track t ON t.release_id = r.id \
|
||||
LEFT JOIN furumusic__release_artist ra ON ra.release_id = r.id \
|
||||
LEFT JOIN furumusic__artist a ON a.id = ra.artist_id \
|
||||
LEFT JOIN furumusic__play_history ph ON ph.track_id = t.id \
|
||||
WHERE r.id = $1 \
|
||||
GROUP BY r.id",
|
||||
@@ -2055,8 +2059,11 @@ async fn load_artist_options(pool: &PgPool) -> anyhow::Result<Vec<ArtistOptionDt
|
||||
async fn load_release_options(pool: &PgPool) -> anyhow::Result<Vec<ReleaseOptionDto>> {
|
||||
let rows = sqlx::query_as::<_, (i64, String, Option<String>)>(
|
||||
"SELECT r.id, r.title::text AS title, \
|
||||
CONCAT(r.release_type::text, COALESCE(' / ' || r.year::text, '')) AS subtitle \
|
||||
(COALESCE(NULLIF(STRING_AGG(DISTINCT a.name::text, ', '), ''), 'Unknown artist') || COALESCE(' / ' || r.year::text, '')) AS subtitle \
|
||||
FROM furumusic__release r \
|
||||
LEFT JOIN furumusic__release_artist ra ON ra.release_id = r.id \
|
||||
LEFT JOIN furumusic__artist a ON a.id = ra.artist_id \
|
||||
GROUP BY r.id \
|
||||
ORDER BY r.title_sort ASC, r.year NULLS LAST, r.id ASC",
|
||||
)
|
||||
.fetch_all(pool)
|
||||
@@ -2452,7 +2459,7 @@ async fn load_release_items(
|
||||
) -> anyhow::Result<Vec<LibraryItemRow>> {
|
||||
let mut qb = QueryBuilder::<Postgres>::new(
|
||||
"SELECT r.id, r.title::text AS title, \
|
||||
CONCAT(r.release_type::text, COALESCE(' · ' || r.year::text, '')) AS subtitle, \
|
||||
(COALESCE(NULLIF(STRING_AGG(DISTINCT a.name::text, ', '), ''), 'Unknown artist') || COALESCE(' / ' || r.year::text, '')) AS subtitle, \
|
||||
r.is_hidden, COUNT(DISTINCT t.id)::bigint AS primary_count, \
|
||||
COUNT(DISTINCT ra.artist_id)::bigint AS secondary_count, \
|
||||
COUNT(DISTINCT ph.id)::bigint AS tertiary_count, \
|
||||
@@ -2657,10 +2664,12 @@ fn optional_job_time(value: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
fn normalize_library_kind(kind: Option<&str>) -> String {
|
||||
match kind {
|
||||
Some("releases") => "releases",
|
||||
Some("tracks") => "tracks",
|
||||
Some("playlists") => "playlists",
|
||||
let kind = kind.unwrap_or_default().trim().to_ascii_lowercase();
|
||||
match kind.as_str() {
|
||||
"release" | "releases" => "releases",
|
||||
"track" | "tracks" => "tracks",
|
||||
"playlist" | "playlists" => "playlists",
|
||||
"artist" | "artists" => "artists",
|
||||
_ => "artists",
|
||||
}
|
||||
.to_owned()
|
||||
@@ -2696,6 +2705,23 @@ fn parse_optional_admin_i32(value: Option<&str>, min: i32, max: i32) -> Option<i
|
||||
.map(|parsed| parsed.clamp(min, max))
|
||||
}
|
||||
|
||||
fn deserialize_optional_stringish<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let Some(value) = Option::<serde_json::Value>::deserialize(deserializer)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
match value {
|
||||
serde_json::Value::Null => Ok(None),
|
||||
serde_json::Value::String(value) => Ok(Some(value)),
|
||||
serde_json::Value::Number(value) => Ok(Some(value.to_string())),
|
||||
other => Err(serde::de::Error::custom(format!(
|
||||
"expected string, number, or null, got {other}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn now_string() -> String {
|
||||
chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string()
|
||||
}
|
||||
|
||||
+10
-2
@@ -1934,7 +1934,7 @@ tbody tr:hover {
|
||||
<div class="empty" x-show="editorLoading">Loading editor...</div>
|
||||
<div x-show="!editorLoading">
|
||||
<div class="field">
|
||||
<label x-text="isArtistEditor() ? 'Artist name' : 'Title'"></label>
|
||||
<label x-text="isArtistEditor() ? 'Artist name' : (isReleaseEditor() ? 'Release title' : (isTrackEditor() ? 'Track title' : 'Title'))"></label>
|
||||
<input x-model="editorDraft.title" />
|
||||
</div>
|
||||
|
||||
@@ -2829,8 +2829,10 @@ function adminV2() {
|
||||
selectEditorRelease(release = null) {
|
||||
const candidates = this.filteredEditorReleases();
|
||||
release = release || candidates[0];
|
||||
if (release) this.editorDraft.release_id = Number(release.id);
|
||||
if (!release) return false;
|
||||
this.editorDraft.release_id = Number(release.id);
|
||||
this.editorReleaseToAdd = '';
|
||||
return true;
|
||||
},
|
||||
|
||||
setEditorImageFile(event) {
|
||||
@@ -2948,6 +2950,12 @@ function adminV2() {
|
||||
},
|
||||
|
||||
async saveLibraryItem() {
|
||||
if (this.isTrackEditor() && String(this.editorReleaseToAdd || '').trim()) {
|
||||
if (!this.selectEditorRelease()) {
|
||||
this.showToast('Choose a release from search results');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!this.editorCanSave()) return;
|
||||
this.editorSaving = true;
|
||||
try {
|
||||
|
||||
@@ -422,13 +422,13 @@
|
||||
<button class="torrent-tree-check" :class="{ checked: $store.torrents.selectedUploadTracks.has(item.track.id) }" @click="$store.torrents.toggleUploadTrackSelection(item.track.id)">
|
||||
<template x-if="$store.torrents.selectedUploadTracks.has(item.track.id)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg></template>
|
||||
</button>
|
||||
<button class="track-action-btn play-btn" @click="$store.player.play(item.track)" title="{{ t.player_play }}"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg></button>
|
||||
<div class="upload-track-main">
|
||||
<div class="upload-track-title"><span x-text="item.track.track_number ? item.track.track_number + '. ' + item.track.title : item.track.title"></span><span class="upload-hidden-pill" x-show="item.is_hidden">hidden</span></div>
|
||||
<div class="upload-track-meta"><span x-text="$store.torrents.uploadArtistsText(item)"></span><span x-show="$store.torrents.uploadFeaturedArtistsText(item)">feat.</span><span x-show="$store.torrents.uploadFeaturedArtistsText(item)" x-text="$store.torrents.uploadFeaturedArtistsText(item)"></span></div>
|
||||
</div>
|
||||
<div class="upload-track-actions">
|
||||
<button class="track-action-btn" @click="$store.queue.addNextInQueue([item.track])" title="{{ t.player_play_next }}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 6h14M5 12h8M5 18h14"/><path d="M17 10l4 3-4 3" fill="currentColor" stroke="none"/></svg></button>
|
||||
<button class="track-action-btn queue-insert-btn queue-next-btn" @click="$store.queue.addNextInQueue([item.track])" title="{{ t.player_play_next }}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 6h10M5 12h7M5 18h10"/><path d="M17 9l4 3-4 3" fill="currentColor" stroke="none"/></svg></button>
|
||||
<button class="track-action-btn queue-insert-btn queue-end-btn" @click="$store.queue.addToEnd([item.track])" title="{{ t.player_add_to_queue }}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 6h14M5 12h14M5 18h7"/><path d="M17 15l4 3-4 3" fill="currentColor" stroke="none"/></svg></button>
|
||||
<button class="modal-btn modal-btn-ghost" @click="$store.torrents.editUpload(item)">Edit</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -516,11 +516,6 @@
|
||||
<div class="upload-track-card" :class="{ hidden: item.is_hidden }">
|
||||
<template x-if="$store.torrents.uploadEditId !== item.track.id">
|
||||
<div class="upload-track-display">
|
||||
<button class="track-action-btn play-btn"
|
||||
@click="$store.player.play(item.track)"
|
||||
title="{{ t.player_play }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
</button>
|
||||
<div class="upload-track-main">
|
||||
<div class="upload-track-title">
|
||||
<span x-text="item.track.title"></span>
|
||||
@@ -535,8 +530,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="upload-track-actions">
|
||||
<button class="track-action-btn" @click="$store.queue.addNextInQueue([item.track])" title="{{ t.player_play_next }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 6h14M5 12h8M5 18h14"/><path d="M17 10l4 3-4 3" fill="currentColor" stroke="none"/></svg>
|
||||
<button class="track-action-btn queue-insert-btn queue-next-btn" @click="$store.queue.addNextInQueue([item.track])" title="{{ t.player_play_next }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 6h10M5 12h7M5 18h10"/><path d="M17 9l4 3-4 3" fill="currentColor" stroke="none"/></svg>
|
||||
</button>
|
||||
<button class="track-action-btn queue-insert-btn queue-end-btn" @click="$store.queue.addToEnd([item.track])" title="{{ t.player_add_to_queue }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 6h14M5 12h14M5 18h7"/><path d="M17 15l4 3-4 3" fill="currentColor" stroke="none"/></svg>
|
||||
</button>
|
||||
<button class="modal-btn modal-btn-ghost" @click="$store.torrents.editUpload(item)">Edit</button>
|
||||
</div>
|
||||
|
||||
+20
-32
@@ -483,19 +483,16 @@
|
||||
<span x-show="$store.library.hasPopularity(track)" x-text="$store.library.popularityLabel(track)"></span>
|
||||
<span x-show="!$store.library.hasPopularity(track)" class="info-letter">i</span>
|
||||
</button>
|
||||
<button class="track-action-btn play-btn" @click.stop="$store.library.playSearchTrack(idx)" title="{{ t.player_play }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
</button>
|
||||
<button class="like-btn" :class="{ liked: $store.likes.has(track.id) }" @click.stop="$store.likes.toggle(track.id)" title="{{ t.player_like }}">
|
||||
<svg viewBox="0 0 24 24" :fill="$store.likes.has(track.id) ? 'currentColor' : 'none'" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78L12 21.23l8.84-8.84a5.5 5.5 0 000-7.78z"/></svg>
|
||||
</button>
|
||||
<button class="track-action-btn" @click.stop="$store.queue.addNextInQueue([track])" title="{{ t.player_play_next }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 6h14M5 12h8M5 18h14"/><path d="M17 10l4 3-4 3" fill="currentColor" stroke="none"/></svg>
|
||||
<button class="track-action-btn queue-insert-btn queue-next-btn" @click.stop="$store.queue.addNextInQueue([track])" title="{{ t.player_play_next }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 6h10M5 12h7M5 18h10"/><path d="M17 9l4 3-4 3" fill="currentColor" stroke="none"/></svg>
|
||||
</button>
|
||||
<button class="track-action-btn" @click.stop="$store.queue.addToEnd([track])" title="{{ t.player_add_to_queue }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
<button class="track-action-btn queue-insert-btn queue-end-btn" @click.stop="$store.queue.addToEnd([track])" title="{{ t.player_add_to_queue }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 6h14M5 12h14M5 18h7"/><path d="M17 15l4 3-4 3" fill="currentColor" stroke="none"/></svg>
|
||||
</button>
|
||||
<button class="track-action-btn" @click.stop="$store.playlists.showPicker([track.id])" title="{{ t.player_add_to_playlist }}">
|
||||
<button class="track-action-btn playlist-add-btn" @click.stop="$store.playlists.showPicker([track.id])" title="{{ t.player_add_to_playlist }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -663,19 +660,16 @@
|
||||
<span x-show="$store.library.hasPopularity(track)" x-text="$store.library.popularityLabel(track)"></span>
|
||||
<span x-show="!$store.library.hasPopularity(track)" class="info-letter">i</span>
|
||||
</button>
|
||||
<button class="track-action-btn play-btn" @click.stop="$store.queue.playRelease($store.library.currentArtist.featured_tracks, idx)" title="{{ t.player_play }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
</button>
|
||||
<button class="like-btn" :class="{ liked: $store.likes.has(track.id) }" @click.stop="$store.likes.toggle(track.id)" title="{{ t.player_like }}">
|
||||
<svg viewBox="0 0 24 24" :fill="$store.likes.has(track.id) ? 'currentColor' : 'none'" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78L12 21.23l8.84-8.84a5.5 5.5 0 000-7.78z"/></svg>
|
||||
</button>
|
||||
<button class="track-action-btn" @click.stop="$store.queue.addNextInQueue([track])" title="{{ t.player_play_next }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 6h14M5 12h8M5 18h14"/><path d="M17 10l4 3-4 3" fill="currentColor" stroke="none"/></svg>
|
||||
<button class="track-action-btn queue-insert-btn queue-next-btn" @click.stop="$store.queue.addNextInQueue([track])" title="{{ t.player_play_next }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 6h10M5 12h7M5 18h10"/><path d="M17 9l4 3-4 3" fill="currentColor" stroke="none"/></svg>
|
||||
</button>
|
||||
<button class="track-action-btn" @click.stop="$store.queue.addToEnd([track])" title="{{ t.player_add_to_queue }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
<button class="track-action-btn queue-insert-btn queue-end-btn" @click.stop="$store.queue.addToEnd([track])" title="{{ t.player_add_to_queue }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 6h14M5 12h14M5 18h7"/><path d="M17 15l4 3-4 3" fill="currentColor" stroke="none"/></svg>
|
||||
</button>
|
||||
<button class="track-action-btn" @click.stop="$store.playlists.showPicker([track.id])" title="{{ t.player_add_to_playlist }}">
|
||||
<button class="track-action-btn playlist-add-btn" @click.stop="$store.playlists.showPicker([track.id])" title="{{ t.player_add_to_playlist }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -786,19 +780,16 @@
|
||||
<span x-show="$store.library.hasPopularity(track)" x-text="$store.library.popularityLabel(track)"></span>
|
||||
<span x-show="!$store.library.hasPopularity(track)" class="info-letter">i</span>
|
||||
</button>
|
||||
<button class="track-action-btn play-btn" @click.stop="$store.queue.playRelease([track], 0)" title="{{ t.player_play }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
</button>
|
||||
<button class="like-btn" :class="{ liked: $store.likes.has(track.id) }" @click.stop="$store.likes.toggle(track.id)" title="{{ t.player_like }}">
|
||||
<svg viewBox="0 0 24 24" :fill="$store.likes.has(track.id) ? 'currentColor' : 'none'" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78L12 21.23l8.84-8.84a5.5 5.5 0 000-7.78z"/></svg>
|
||||
</button>
|
||||
<button class="track-action-btn" @click.stop="$store.queue.addNextInQueue([track])" title="{{ t.player_play_next }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 6h14M5 12h8M5 18h14"/><path d="M17 10l4 3-4 3" fill="currentColor" stroke="none"/></svg>
|
||||
<button class="track-action-btn queue-insert-btn queue-next-btn" @click.stop="$store.queue.addNextInQueue([track])" title="{{ t.player_play_next }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 6h10M5 12h7M5 18h10"/><path d="M17 9l4 3-4 3" fill="currentColor" stroke="none"/></svg>
|
||||
</button>
|
||||
<button class="track-action-btn" @click.stop="$store.queue.addToEnd([track])" title="{{ t.player_add_to_queue }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
<button class="track-action-btn queue-insert-btn queue-end-btn" @click.stop="$store.queue.addToEnd([track])" title="{{ t.player_add_to_queue }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 6h14M5 12h14M5 18h7"/><path d="M17 15l4 3-4 3" fill="currentColor" stroke="none"/></svg>
|
||||
</button>
|
||||
<button class="track-action-btn" @click.stop="$store.playlists.showPicker([track.id])" title="{{ t.player_add_to_playlist }}">
|
||||
<button class="track-action-btn playlist-add-btn" @click.stop="$store.playlists.showPicker([track.id])" title="{{ t.player_add_to_playlist }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -862,19 +853,16 @@
|
||||
<span x-show="$store.library.hasPopularity(track)" x-text="$store.library.popularityLabel(track)"></span>
|
||||
<span x-show="!$store.library.hasPopularity(track)" class="info-letter">i</span>
|
||||
</button>
|
||||
<button class="track-action-btn play-btn" @click.stop="$store.queue.playRelease($store.library.currentPlaylist.tracks, idx)" title="{{ t.player_play }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
</button>
|
||||
<button class="like-btn" :class="{ liked: $store.likes.has(track.id) }" @click.stop="$store.likes.toggle(track.id)" title="{{ t.player_like }}">
|
||||
<svg viewBox="0 0 24 24" :fill="$store.likes.has(track.id) ? 'currentColor' : 'none'" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78L12 21.23l8.84-8.84a5.5 5.5 0 000-7.78z"/></svg>
|
||||
</button>
|
||||
<button class="track-action-btn" @click.stop="$store.queue.addNextInQueue([track])" title="{{ t.player_play_next }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 6h14M5 12h8M5 18h14"/><path d="M17 10l4 3-4 3" fill="currentColor" stroke="none"/></svg>
|
||||
<button class="track-action-btn queue-insert-btn queue-next-btn" @click.stop="$store.queue.addNextInQueue([track])" title="{{ t.player_play_next }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 6h10M5 12h7M5 18h10"/><path d="M17 9l4 3-4 3" fill="currentColor" stroke="none"/></svg>
|
||||
</button>
|
||||
<button class="track-action-btn" @click.stop="$store.queue.addToEnd([track])" title="{{ t.player_add_to_queue }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
<button class="track-action-btn queue-insert-btn queue-end-btn" @click.stop="$store.queue.addToEnd([track])" title="{{ t.player_add_to_queue }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 6h14M5 12h14M5 18h7"/><path d="M17 15l4 3-4 3" fill="currentColor" stroke="none"/></svg>
|
||||
</button>
|
||||
<button class="track-action-btn" @click.stop="$store.playlists.showPicker([track.id])" title="{{ t.player_add_to_playlist }}">
|
||||
<button class="track-action-btn playlist-add-btn" @click.stop="$store.playlists.showPicker([track.id])" title="{{ t.player_add_to_playlist }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -687,7 +687,7 @@ button.user-stat:hover {
|
||||
|
||||
.track-list-header {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr 1fr 120px 60px;
|
||||
grid-template-columns: 40px minmax(0, 1fr) minmax(0, 1fr) 154px 60px;
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
color: var(--text-subdued);
|
||||
@@ -699,7 +699,7 @@ button.user-stat:hover {
|
||||
|
||||
.track-row {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr 1fr 120px 60px;
|
||||
grid-template-columns: 40px minmax(0, 1fr) minmax(0, 1fr) 154px 60px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
@@ -736,14 +736,18 @@ button.user-stat:hover {
|
||||
}
|
||||
|
||||
.track-album { font-size: 13px; color: var(--text-subdued); }
|
||||
.track-duration { font-size: 13px; color: var(--text-subdued); text-align: right; }
|
||||
.track-duration { font-size: 13px; color: var(--text-subdued); text-align: right; pointer-events: none; }
|
||||
|
||||
/* Track action buttons */
|
||||
.track-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 2px;
|
||||
opacity: 1;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.track-action-btn {
|
||||
@@ -759,9 +763,22 @@ button.user-stat:hover {
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.track-actions .track-action-btn,
|
||||
.track-actions .like-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex: 0 0 28px;
|
||||
}
|
||||
|
||||
.track-actions .popularity-info-btn {
|
||||
width: auto;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.track-action-btn:hover { color: var(--text-primary); background: var(--bg-active); }
|
||||
.track-action-btn.play-btn:hover { color: var(--accent); }
|
||||
.track-action-btn svg { width: 16px; height: 16px; }
|
||||
.track-action-btn.queue-insert-btn svg { width: 17px; height: 17px; }
|
||||
|
||||
.info-btn {
|
||||
color: var(--text-subdued);
|
||||
@@ -3052,7 +3069,7 @@ button.user-stat:hover {
|
||||
|
||||
.upload-tree-track {
|
||||
display: grid;
|
||||
grid-template-columns: 24px 30px minmax(0, 1fr) auto;
|
||||
grid-template-columns: 24px minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 44px;
|
||||
@@ -3151,7 +3168,7 @@ button.user-stat:hover {
|
||||
|
||||
.upload-track-display {
|
||||
display: grid;
|
||||
grid-template-columns: 34px minmax(0, 1fr) auto;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
@@ -3960,7 +3977,7 @@ button.user-stat:hover {
|
||||
}
|
||||
|
||||
.upload-tree-track {
|
||||
grid-template-columns: 24px 30px minmax(0, 1fr);
|
||||
grid-template-columns: 24px minmax(0, 1fr);
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
@@ -3976,7 +3993,7 @@ button.user-stat:hover {
|
||||
}
|
||||
|
||||
.upload-track-display {
|
||||
grid-template-columns: 32px minmax(0, 1fr);
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.upload-track-actions {
|
||||
|
||||
Reference in New Issue
Block a user