ADMIN: added releases and artists management form
Build and Publish / Build and Publish Docker Image (push) Successful in 2m50s
Build and Publish / Build and Publish Docker Image (push) Successful in 2m50s
This commit is contained in:
@@ -11,7 +11,32 @@
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<pre class="info-modal-body" x-text="$store.info.modal.body"></pre>
|
||||
<div class="info-modal-body">
|
||||
<template x-if="$store.info.modal.rows && $store.info.modal.rows.length">
|
||||
<table class="info-table">
|
||||
<tbody>
|
||||
<template x-for="(row, idx) in $store.info.modal.rows" :key="row.label + '-' + idx">
|
||||
<tr>
|
||||
<th x-text="row.label"></th>
|
||||
<td>
|
||||
<template x-if="row.links && row.links.length">
|
||||
<div class="info-link-list">
|
||||
<template x-for="link in row.links" :key="link.type + '-' + link.id + '-' + link.label">
|
||||
<button class="info-link" type="button" @click="$store.info.navigate(link)" x-text="link.label"></button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!row.links || !row.links.length">
|
||||
<span x-text="row.value"></span>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
<pre class="info-modal-plain" x-show="!$store.info.modal.rows || !$store.info.modal.rows.length" x-text="$store.info.modal.body"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -4,11 +4,14 @@ const T = {
|
||||
info: "{{ t.player_info }}",
|
||||
noDetails: "{{ t.player_no_details }}",
|
||||
trackInfoTitle: "{{ t.player_track_info }}",
|
||||
releaseInfoTitle: "{{ t.player_release_info }}",
|
||||
loadingHistory: "{{ t.player_loading_history }}",
|
||||
failedLoadHistory: "{{ t.player_failed_load_history }}",
|
||||
totalPlays: "{{ t.player_total_plays }}",
|
||||
unknown: "{{ t.player_unknown }}",
|
||||
unknownSize: "{{ t.player_unknown_size }}",
|
||||
title: "{{ t.player_title }}",
|
||||
release: "{{ t.player_release }}",
|
||||
unknownRelease: "{{ t.player_unknown_release }}",
|
||||
unknownTrack: "{{ t.player_unknown_track }}",
|
||||
unknownAudio: "{{ t.player_unknown_audio }}",
|
||||
@@ -124,14 +127,33 @@ document.addEventListener('alpine:init', () => {
|
||||
Alpine.store('info', {
|
||||
modal: null,
|
||||
open(title, body) {
|
||||
if (Array.isArray(body)) {
|
||||
this.openRows(title, body);
|
||||
return;
|
||||
}
|
||||
this.modal = {
|
||||
title: title || T.info,
|
||||
body: body || T.noDetails,
|
||||
rows: null,
|
||||
};
|
||||
},
|
||||
openRows(title, rows) {
|
||||
this.modal = {
|
||||
title: title || T.info,
|
||||
body: '',
|
||||
rows: (rows || []).filter(row => row && ((row.value !== undefined && row.value !== null && row.value !== '') || (row.links && row.links.length))),
|
||||
};
|
||||
},
|
||||
close() {
|
||||
this.modal = null;
|
||||
},
|
||||
navigate(link) {
|
||||
if (!link || !link.id) return;
|
||||
this.close();
|
||||
const library = Alpine.store('library');
|
||||
if (link.type === 'release') library.openRelease(link.id);
|
||||
if (link.type === 'artist') library.openArtist(link.id);
|
||||
},
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -892,7 +914,7 @@ document.addEventListener('alpine:init', () => {
|
||||
},
|
||||
|
||||
openTrackInfo(track) {
|
||||
Alpine.store('info').open(T.trackInfoTitle, this.trackInfo(track));
|
||||
Alpine.store('info').openRows(T.trackInfoTitle, this.trackInfoRows(track));
|
||||
},
|
||||
|
||||
uploadersInfo(uploaders) {
|
||||
@@ -915,6 +937,31 @@ document.addEventListener('alpine:init', () => {
|
||||
return lines.join('\n');
|
||||
},
|
||||
|
||||
openReleaseInfo(release) {
|
||||
Alpine.store('info').openRows(T.releaseInfoTitle, this.releaseInfoRows(release));
|
||||
},
|
||||
|
||||
infoLinks(items, type) {
|
||||
const seen = new Set();
|
||||
return (items || [])
|
||||
.filter(item => item && item.id && !seen.has(Number(item.id)) && seen.add(Number(item.id)))
|
||||
.map(item => ({ type, id: item.id, label: item.label || item.name || item.title || String(item.id) }));
|
||||
},
|
||||
|
||||
releaseInfoRows(release) {
|
||||
if (!release) return [];
|
||||
const rows = [
|
||||
{ label: T.release, value: release.title || T.unknownRelease },
|
||||
{ label: T.type, value: release.release_type || T.unknown },
|
||||
{ label: T.year, value: release.year || T.unknown },
|
||||
{ label: T.tracks, value: release.track_count || release.tracks?.length || 0 },
|
||||
{ label: T.uploaders, value: this.uploadersInfo(release.uploaders || []) },
|
||||
];
|
||||
const artistLinks = this.infoLinks(release.artists || [], 'artist');
|
||||
if (artistLinks.length) rows.splice(1, 0, { label: T.artists, links: artistLinks });
|
||||
return rows;
|
||||
},
|
||||
|
||||
trackInfo(track) {
|
||||
if (!track) return '';
|
||||
const artists = this.trackArtistLinks(track).map(artist => artist.label).join(', ') || T.unknown;
|
||||
@@ -945,6 +992,40 @@ document.addEventListener('alpine:init', () => {
|
||||
return lines.join('\n');
|
||||
},
|
||||
|
||||
trackInfoRows(track) {
|
||||
if (!track) return [];
|
||||
const artistLinks = this.infoLinks(this.trackArtistLinks(track), 'artist');
|
||||
const releaseLinks = track.release_id
|
||||
? [{ type: 'release', id: track.release_id, label: track.release_title || T.unknownRelease }]
|
||||
: [];
|
||||
const audio = [
|
||||
track.audio_format || null,
|
||||
track.audio_bitrate ? `${track.audio_bitrate} kbps` : null,
|
||||
track.audio_sample_rate ? `${track.audio_sample_rate} Hz` : null,
|
||||
track.audio_bit_depth ? `${track.audio_bit_depth}-bit` : null,
|
||||
].filter(Boolean).join(' · ') || T.unknownAudio;
|
||||
const rows = [
|
||||
{ label: T.title, value: track.title || T.unknownTrack },
|
||||
{ label: T.release, links: releaseLinks },
|
||||
{ label: T.artists, links: artistLinks },
|
||||
{ label: T.releaseYear, value: track.release_year || T.unknown },
|
||||
{ label: T.duration, value: formatTime(track.duration_seconds) },
|
||||
{ label: T.audio, value: audio },
|
||||
{ label: T.size, value: this.bytes(track.file_size_bytes) },
|
||||
{ label: T.uploader, value: track.uploader_name || 'UFO' },
|
||||
];
|
||||
if (track.lastfm_rating != null || track.lastfm_listeners != null || track.lastfm_playcount != null) {
|
||||
const rating = Number(track.lastfm_rating || 0);
|
||||
rows.push({ label: T.lastfmRating, value: Number.isFinite(rating) ? Math.round(rating) : T.unknown });
|
||||
rows.push({ label: T.lastfmListeners, value: new Intl.NumberFormat().format(track.lastfm_listeners || 0) });
|
||||
rows.push({ label: T.lastfmPlaycount, value: new Intl.NumberFormat().format(track.lastfm_playcount || 0) });
|
||||
if (track.lastfm_updated_at) rows.push({ label: T.lastfmUpdated, value: track.lastfm_updated_at });
|
||||
} else {
|
||||
rows.push({ label: T.lastfmRating, value: T.lastfmNotLoaded });
|
||||
}
|
||||
return rows;
|
||||
},
|
||||
|
||||
async openRelease(id, options = {}) {
|
||||
this._beginNavigation('#release/' + id, options);
|
||||
this.searchQuery = '';
|
||||
|
||||
@@ -391,7 +391,7 @@
|
||||
<template x-if="!release.cover_url">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="4"/></svg>
|
||||
</template>
|
||||
<button class="card-info-btn" @click.stop="$store.info.open('{{ t.player_release_info }}', $store.library.releaseInfo(release))" :title="$store.library.releaseInfo(release)" aria-label="{{ t.player_release_info }}">
|
||||
<button class="card-info-btn" @click.stop="$store.library.openReleaseInfo(release)" :title="$store.library.releaseInfo(release)" aria-label="{{ t.player_release_info }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -562,7 +562,7 @@
|
||||
<template x-if="!release.cover_url">
|
||||
<span class="placeholder-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="4"/></svg></span>
|
||||
</template>
|
||||
<button class="card-info-btn" @click.stop="$store.info.open('{{ t.player_release_info }}', $store.library.releaseInfo(release))" :title="$store.library.releaseInfo(release)" aria-label="{{ t.player_release_info }}">
|
||||
<button class="card-info-btn" @click.stop="$store.library.openReleaseInfo(release)" :title="$store.library.releaseInfo(release)" aria-label="{{ t.player_release_info }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
||||
</button>
|
||||
<button class="card-enqueue-btn" @click.stop="$store.library.enqueueRelease(release.id)" title="{{ t.player_add_to_queue }}">
|
||||
@@ -690,7 +690,7 @@
|
||||
<div class="release-year" x-text="$store.library.currentRelease.year || ''"></div>
|
||||
<div class="release-actions">
|
||||
<button class="release-action-btn secondary"
|
||||
@click.stop="$store.info.open('{{ t.player_release_info }}', $store.library.releaseInfo($store.library.currentRelease))"
|
||||
@click.stop="$store.library.openReleaseInfo($store.library.currentRelease)"
|
||||
:title="$store.library.releaseInfo($store.library.currentRelease)"
|
||||
aria-label="{{ t.player_release_info }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
||||
@@ -928,7 +928,16 @@
|
||||
</template>
|
||||
</div>
|
||||
<div class="player-track-info">
|
||||
<div class="player-track-title" x-text="$store.player.currentTrack.title"></div>
|
||||
<div class="player-track-title-row">
|
||||
<div class="player-track-title" x-text="$store.player.currentTrack.title"></div>
|
||||
<button class="like-btn player-current-like"
|
||||
:class="{ liked: $store.likes.has($store.player.currentTrack.id) }"
|
||||
@click.stop="$store.likes.toggle($store.player.currentTrack.id)"
|
||||
title="{{ t.player_like }}"
|
||||
aria-label="{{ t.player_like }}">
|
||||
<svg viewBox="0 0 24 24" :fill="$store.likes.has($store.player.currentTrack.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>
|
||||
</div>
|
||||
<div class="player-track-artist">
|
||||
<template x-for="(artist, artistIdx) in $store.library.trackArtistLinks($store.player.currentTrack)" :key="artist.label + '-' + artist.id + '-' + artistIdx">
|
||||
<span>
|
||||
|
||||
@@ -1079,12 +1079,28 @@ button.user-stat:hover {
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.player-track-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.player-track-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.player-current-like {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 4px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.player-track-artist {
|
||||
@@ -1763,7 +1779,7 @@ button.user-stat:hover {
|
||||
.modal-playlist-item svg { width: 16px; height: 16px; flex-shrink: 0; }
|
||||
|
||||
.info-modal {
|
||||
max-width: min(520px, calc(100vw - 24px));
|
||||
max-width: min(620px, calc(100vw - 24px));
|
||||
}
|
||||
|
||||
.info-modal-head {
|
||||
@@ -1785,14 +1801,72 @@ button.user-stat:hover {
|
||||
border-radius: 8px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-secondary);
|
||||
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
white-space: pre-wrap;
|
||||
overflow: auto;
|
||||
max-height: min(58dvh, 520px);
|
||||
}
|
||||
|
||||
.info-modal-plain {
|
||||
margin: 0;
|
||||
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.info-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.info-table th,
|
||||
.info-table td {
|
||||
padding: 9px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.info-table tr:last-child th,
|
||||
.info-table tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.info-table th {
|
||||
width: 34%;
|
||||
padding-right: 18px;
|
||||
color: var(--text-subdued);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.info-table td {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.info-link-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.info-link {
|
||||
border: 1px solid rgba(29, 185, 84, 0.36);
|
||||
border-radius: 999px;
|
||||
background: rgba(29, 185, 84, 0.1);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
padding: 3px 8px;
|
||||
font: inherit;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.info-link:hover {
|
||||
border-color: rgba(29, 185, 84, 0.7);
|
||||
background: rgba(29, 185, 84, 0.18);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
@@ -2921,6 +2995,22 @@ button.user-stat:hover {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.info-table th,
|
||||
.info-table td {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.info-table th {
|
||||
padding-bottom: 2px;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.info-table td {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.torrent-modal {
|
||||
width: 100vw;
|
||||
max-width: none;
|
||||
|
||||
Reference in New Issue
Block a user