Files
furumusic/templates/player/shell.html
T
ab e1a4b6267f
Build and Publish / Build and Publish Docker Image (push) Successful in 3m5s
PLAYER: Added generated playlists feature
2026-05-29 17:04:30 +03:00

1238 lines
95 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<div class="app-layout"
x-data
@keydown.window.space="if (!['INPUT','TEXTAREA','SELECT'].includes(document.activeElement?.tagName)) { $event.preventDefault(); $store.player.toggle(); }"
@keydown.window.arrow-left="if (!['INPUT','TEXTAREA','SELECT'].includes(document.activeElement?.tagName)) { $event.preventDefault(); $store.player.seekRelative(-5); }"
@keydown.window.arrow-right="if (!['INPUT','TEXTAREA','SELECT'].includes(document.activeElement?.tagName)) { $event.preventDefault(); $store.player.seekRelative(5); }"
@keydown.window="if ((e=$event).ctrlKey && e.key==='k') { e.preventDefault(); document.getElementById('search-input')?.focus(); } else if (e.key==='/' && !['INPUT','TEXTAREA','SELECT'].includes(document.activeElement?.tagName)) { e.preventDefault(); document.getElementById('search-input')?.focus(); }"
>
<div class="main-content">
<!-- Left Sidebar -->
<div class="sidebar-left">
<div class="user-widget" x-show="$store.user.profile" x-cloak>
<div class="user-widget-main">
<div class="user-avatar" x-text="$store.user.initials()"></div>
<div style="min-width:0">
<div class="user-name" x-text="$store.user.profile?.name || ''"></div>
<div class="user-role" x-text="$store.user.profile?.role || ''"></div>
</div>
<button class="user-logout-btn" @click="$store.user.logout()" title="{{ t.player_log_out }}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"/>
<polyline points="16 17 21 12 16 7"/>
<line x1="21" y1="12" x2="9" y2="12"/>
</svg>
</button>
</div>
<div class="user-stats">
<button class="user-stat" @click="$store.history.open()">
<span class="user-stat-value" x-text="$store.user.format($store.user.profile?.stats?.plays)"></span>
<span class="user-stat-label">{{ t.player_plays_count }}</span>
</button>
<div class="user-stat">
<span class="user-stat-value" x-text="$store.user.format($store.user.profile?.stats?.liked_tracks)"></span>
<span class="user-stat-label">{{ t.player_likes_count }}</span>
</div>
<div class="user-stat">
<span class="user-stat-value" x-text="$store.user.duration($store.user.profile?.stats?.listened_minutes)"></span>
<span class="user-stat-label">{{ t.player_listened }}</span>
</div>
</div>
<button class="lastfm-profile-action"
:class="$store.user.lastfmClass()"
:disabled="$store.user.lastfmBusy || !$store.user.lastfm?.configured"
@click="$store.user.handleLastfm()"
:title="$store.user.lastfmLabel()"
:aria-label="$store.user.lastfmLabel()">
<span class="lastfm-dot"></span>
<span class="lastfm-profile-text">
<span class="lastfm-profile-brand">{{ t.player_lastfm_profile }}</span>
<span class="lastfm-profile-separator">·</span>
<span class="lastfm-profile-status" x-text="$store.user.lastfmStatusLabel()"></span>
</span>
</button>
</div>
<div class="sidebar-header">
<h2>{{ t.player_library }}</h2>
</div>
<div class="sidebar-nav">
<div class="sidebar-nav-item"
:class="{ active: $store.library.view === 'artists' }"
@click="$store.library.goArtists()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="4"/><path d="M20 21a8 8 0 10-16 0"/></svg>
{{ t.player_artists }}
</div>
</div>
<div class="sidebar-section">
<div class="sidebar-section-title">
{{ t.player_following }}
<span x-show="$store.follows.artists.length > 0"
x-text="'(' + $store.follows.artists.length + ')'"></span>
</div>
<template x-if="$store.follows.artists.length === 0">
<div class="following-empty">{{ t.player_no_followed_artists }}</div>
</template>
<div class="following-list" x-show="$store.follows.artists.length > 0" x-cloak>
<template x-for="artist in $store.follows.artists" :key="artist.id">
<div class="following-artist"
:class="{ active: $store.library.currentArtist && $store.library.currentArtist.id === artist.id }"
@click="$store.library.openArtist(artist.id)">
<div class="following-avatar">
<template x-if="artist.image_url">
<img :src="artist.image_url" :alt="artist.name" loading="lazy">
</template>
<template x-if="!artist.image_url">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="12" cy="8" r="4"/><path d="M20 21a8 8 0 10-16 0"/></svg>
</template>
</div>
<div class="following-name" x-text="artist.name"></div>
</div>
</template>
</div>
</div>
<div class="playlist-list">
<template x-for="pl in $store.playlists.regularList()" :key="pl.id">
<div class="playlist-item-row">
<div class="playlist-item" @click="$store.library.openPlaylist(pl.id)">
<template x-if="pl.kind === 'likes'">
<span style="display:flex;align-items:center;gap:6px">
<svg viewBox="0 0 24 24" fill="var(--accent)" stroke="none" width="14" height="14"><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>
<span x-text="$store.playlists.displayTitle(pl)"></span>
</span>
</template>
<template x-if="pl.kind !== 'likes'">
<span x-text="$store.playlists.displayTitle(pl)"></span>
</template>
<span class="playlist-count" x-text="pl.track_count + ' {{ t.player_tracks_count }}'"></span>
</div>
<template x-if="pl.is_own && pl.kind === 'user'">
<div class="playlist-item-actions">
<button class="playlist-action-btn" @click.stop="$store.playlists.startRename(pl)" title="{{ t.player_rename }}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
</button>
<button class="playlist-action-btn" @click.stop="$store.playlists.deletePlaylist(pl.id)" title="{{ t.player_delete }}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
</button>
</div>
</template>
</div>
</template>
<button class="sidebar-create-btn" @click="$store.playlists.showCreate()">
<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>
{{ t.player_new_playlist }}
</button>
<template x-if="$store.playlists.publishedList().length > 0">
<div class="playlist-public-section">
<div class="sidebar-section-title playlist-subtitle">{{ t.player_published_playlists }}</div>
<template x-for="pl in $store.playlists.publishedList()" :key="'published-' + pl.id">
<div class="playlist-item-row">
<div class="playlist-item playlist-item-public" @click="$store.library.openPlaylist(pl.id)">
<div class="playlist-title-line">
<span class="playlist-title-text" x-text="$store.playlists.displayTitle(pl)"></span>
<span class="playlist-public-badge">{{ t.player_public }}</span>
</div>
<div class="playlist-meta-line">
<span class="playlist-owner" x-show="pl.owner_name" x-text="'{{ t.player_by }} ' + pl.owner_name"></span>
<span x-show="pl.owner_name">&middot;</span>
<span x-text="pl.track_count + ' {{ t.player_tracks_count }}'"></span>
</div>
</div>
</div>
</template>
</div>
</template>
</div>
<div class="sidebar-bottom">
<a href="/admin/">{{ t.player_admin_panel }}</a>
</div>
</div>
<template x-if="$store.mobile.libraryOpen">
<div class="mobile-library-backdrop" @click.self="$store.mobile.closeLibrary()" x-cloak>
<aside class="mobile-library-drawer">
<div class="mobile-drawer-head">
<div>
<div class="mobile-drawer-title">{{ t.player_library }}</div>
<div class="playlist-count">{{ t.player_playlists }} / {{ t.player_following }}</div>
</div>
<button class="mobile-list-action" @click="$store.mobile.closeLibrary()" title="{{ t.player_close }}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
<div class="mobile-drawer-body">
<div class="mobile-drawer-section">
<div class="sidebar-nav-item"
:class="{ active: $store.library.view === 'artists' }"
@click="$store.library.goArtists(); $store.mobile.closeLibrary()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="4"/><path d="M20 21a8 8 0 10-16 0"/></svg>
{{ t.player_artists }}
</div>
</div>
<div class="mobile-drawer-section">
<div class="sidebar-section-title">
{{ t.player_following }}
<span x-show="$store.follows.artists.length > 0"
x-text="'(' + $store.follows.artists.length + ')'"></span>
</div>
<template x-if="$store.follows.artists.length === 0">
<div class="following-empty">{{ t.player_no_followed_artists }}</div>
</template>
<div class="following-list" x-show="$store.follows.artists.length > 0" x-cloak>
<template x-for="artist in $store.follows.artists" :key="'mobile-follow-' + artist.id">
<div class="mobile-list-row">
<div class="following-artist"
:class="{ active: $store.library.currentArtist && $store.library.currentArtist.id === artist.id }"
@click="$store.library.openArtist(artist.id); $store.mobile.closeLibrary()">
<div class="following-avatar">
<template x-if="artist.image_url">
<img :src="artist.image_url" :alt="artist.name" loading="lazy">
</template>
<template x-if="!artist.image_url">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="12" cy="8" r="4"/><path d="M20 21a8 8 0 10-16 0"/></svg>
</template>
</div>
<div class="following-name" x-text="artist.name"></div>
</div>
<button class="mobile-list-action"
@click.stop="$store.follows.toggle(artist.id)"
title="{{ t.player_unfollow_artist }}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M16 21v-2a4 4 0 00-4-4H6a4 4 0 00-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<line x1="17" y1="11" x2="23" y2="11"/>
</svg>
</button>
</div>
</template>
</div>
</div>
<div class="mobile-drawer-section">
<div class="sidebar-section-title">{{ t.player_playlists }}</div>
<template x-for="pl in $store.playlists.regularList()" :key="'mobile-playlist-' + pl.id">
<div class="playlist-item-row">
<div class="playlist-item" @click="$store.library.openPlaylist(pl.id); $store.mobile.closeLibrary()">
<template x-if="pl.kind === 'likes'">
<span style="display:flex;align-items:center;gap:6px">
<svg viewBox="0 0 24 24" fill="var(--accent)" stroke="none" width="14" height="14"><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>
<span x-text="$store.playlists.displayTitle(pl)"></span>
</span>
</template>
<template x-if="pl.kind !== 'likes'">
<span x-text="$store.playlists.displayTitle(pl)"></span>
</template>
<span class="playlist-count" x-text="pl.track_count + ' {{ t.player_tracks_count }}'"></span>
</div>
<template x-if="pl.is_own && pl.kind === 'user'">
<div class="playlist-item-actions">
<button class="playlist-action-btn" @click.stop="$store.mobile.closeLibrary(); $store.playlists.startRename(pl)" title="{{ t.player_rename }}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
</button>
<button class="playlist-action-btn" @click.stop="$store.playlists.deletePlaylist(pl.id)" title="{{ t.player_delete }}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
</button>
</div>
</template>
</div>
</template>
<button class="sidebar-create-btn" @click="$store.mobile.closeLibrary(); $store.playlists.showCreate()">
<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>
{{ t.player_new_playlist }}
</button>
<template x-if="$store.playlists.publishedList().length > 0">
<div class="playlist-public-section">
<div class="sidebar-section-title playlist-subtitle">{{ t.player_published_playlists }}</div>
<template x-for="pl in $store.playlists.publishedList()" :key="'mobile-published-' + pl.id">
<div class="playlist-item-row">
<div class="playlist-item playlist-item-public" @click="$store.library.openPlaylist(pl.id); $store.mobile.closeLibrary()">
<div class="playlist-title-line">
<span class="playlist-title-text" x-text="$store.playlists.displayTitle(pl)"></span>
<span class="playlist-public-badge">{{ t.player_public }}</span>
</div>
<div class="playlist-meta-line">
<span class="playlist-owner" x-show="pl.owner_name" x-text="'{{ t.player_by }} ' + pl.owner_name"></span>
<span x-show="pl.owner_name">&middot;</span>
<span x-text="pl.track_count + ' {{ t.player_tracks_count }}'"></span>
</div>
</div>
</div>
</template>
</div>
</template>
</div>
</div>
</aside>
</div>
</template>
<!-- Center Content -->
<div class="center-content" id="center-scroll">
<!-- Search / account bar -->
<div class="content-topbar" @click.outside="$store.user.menuOpen = false">
<button class="mobile-library-btn"
@click="$store.user.menuOpen = false; $store.mobile.toggleLibrary()"
title="{{ t.player_library }}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 19.5A2.5 2.5 0 016.5 17H20"/>
<path d="M4 4.5A2.5 2.5 0 016.5 2H20v20H6.5A2.5 2.5 0 014 19.5z"/>
</svg>
</button>
<div class="connection-alert"
x-show="$store.connection.disconnected"
x-cloak
:title="$store.connection.message()"
role="status"
aria-live="polite">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M2 8.82a15 15 0 0120 0"/>
<path d="M5 12.86a10 10 0 0114 0"/>
<path d="M8.5 16.43a5 5 0 017 0"/>
<line x1="2" y1="2" x2="22" y2="22"/>
</svg>
<span class="connection-alert-text">{{ t.player_connection_lost }}</span>
</div>
<div class="search-bar">
<span class="search-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span>
<input id="search-input" type="text" placeholder="{{ t.player_search_placeholder }}"
x-model="$store.library.searchQuery"
@input.debounce.300ms="$store.library.search($store.library.searchQuery)"
@keydown.escape="$store.library.clearSearch(); $el.blur()">
<template x-if="$store.library.searchQuery">
<button class="search-clear" @click="$store.library.clearSearch()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</template>
<template x-if="!$store.library.searchQuery">
<span class="search-shortcut">Ctrl+K</span>
</template>
</div>
<button class="torrent-import-btn"
@click="$store.torrents.open()"
title="{{ t.player_import_torrent }}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
</button>
<button class="mobile-account-chip"
x-show="$store.user.profile"
x-cloak
@click="$store.mobile.closeLibrary(); $store.user.menuOpen = !$store.user.menuOpen"
:title="$store.user.profile?.name || 'Account'">
<span class="user-avatar" x-text="$store.user.initials()"></span>
<span class="mobile-account-name" x-text="$store.user.profile?.name || ''"></span>
</button>
<div class="mobile-account-popover"
x-show="$store.user.menuOpen && $store.user.profile"
x-cloak>
<div class="user-widget-main">
<span class="user-avatar" x-text="$store.user.initials()"></span>
<div style="min-width:0">
<div class="user-name" x-text="$store.user.profile?.name || ''"></div>
<div class="user-role" x-text="$store.user.profile?.role || ''"></div>
</div>
</div>
<div class="user-stats">
<button class="user-stat" @click="$store.history.open(); $store.user.menuOpen = false">
<span class="user-stat-value" x-text="$store.user.format($store.user.profile?.stats?.plays)"></span>
<span class="user-stat-label">{{ t.player_plays_count }}</span>
</button>
<div class="user-stat">
<span class="user-stat-value" x-text="$store.user.format($store.user.profile?.stats?.liked_tracks)"></span>
<span class="user-stat-label">{{ t.player_likes_count }}</span>
</div>
<div class="user-stat">
<span class="user-stat-value" x-text="$store.user.duration($store.user.profile?.stats?.listened_minutes)"></span>
<span class="user-stat-label">{{ t.player_listened }}</span>
</div>
</div>
<button class="lastfm-profile-action"
:class="$store.user.lastfmClass()"
:disabled="$store.user.lastfmBusy || !$store.user.lastfm?.configured"
@click="$store.user.handleLastfm()"
:title="$store.user.lastfmLabel()"
:aria-label="$store.user.lastfmLabel()">
<span class="lastfm-dot"></span>
<span class="lastfm-profile-text">
<span class="lastfm-profile-brand">{{ t.player_lastfm_profile }}</span>
<span class="lastfm-profile-separator">·</span>
<span class="lastfm-profile-status" x-text="$store.user.lastfmStatusLabel()"></span>
</span>
</button>
<button class="modal-btn modal-btn-primary mobile-account-logout"
@click="$store.user.logout()">
{{ t.player_log_out }}
</button>
</div>
</div>
<!-- Search Results -->
<template x-if="$store.library.view === 'search'">
<div>
<template x-if="$store.library.searchLoading">
<div class="loading-spinner"><div class="spinner"></div></div>
</template>
<template x-if="!$store.library.searchLoading && $store.library.searchResults">
<div>
<template x-if="$store.library.searchResults.artists.length === 0 && $store.library.searchResults.releases.length === 0 && $store.library.searchResults.tracks.length === 0">
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<p>{{ t.player_no_results }}</p>
</div>
</template>
<!-- Artists section -->
<template x-if="$store.library.searchResults.artists.length > 0">
<div class="search-section">
<h2 class="search-section-title">{{ t.player_artists }}</h2>
<div class="search-artists-row">
<template x-for="artist in $store.library.searchResults.artists" :key="artist.id">
<div class="search-artist-card" @click="$store.library.openArtist(artist.id)">
<div class="search-artist-img">
<template x-if="artist.image_url">
<img :src="artist.image_url" :alt="artist.name" loading="lazy">
</template>
<template x-if="!artist.image_url">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="8" r="4"/><path d="M20 21a8 8 0 10-16 0"/></svg>
</template>
<button class="artist-follow-card-btn"
:class="{ followed: $store.follows.has(artist.id) }"
@click.stop="$store.follows.toggle(artist.id)"
:title="$store.follows.has(artist.id) ? '{{ t.player_unfollow_artist }}' : '{{ t.player_follow_artist }}'">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M16 21v-2a4 4 0 00-4-4H6a4 4 0 00-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path x-show="!$store.follows.has(artist.id)" d="M19 8v6M16 11h6"/>
<path x-show="$store.follows.has(artist.id)" d="M16 11l2 2 4-5"/>
</svg>
</button>
</div>
<div class="search-artist-name" x-text="artist.name"></div>
</div>
</template>
</div>
</div>
</template>
<!-- Releases section -->
<template x-if="$store.library.searchResults.releases.length > 0">
<div class="search-section">
<h2 class="search-section-title">{{ t.player_releases }}</h2>
<div class="search-releases-row">
<template x-for="release in $store.library.searchResults.releases" :key="release.id">
<div class="search-release-card" @click="$store.library.openRelease(release.id)" style="position:relative">
<div class="search-release-cover" style="position:relative">
<template x-if="release.cover_url">
<img :src="release.cover_url" :alt="release.title" loading="lazy">
</template>
<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.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>
<div class="card-title" x-text="release.title"></div>
<div class="card-subtitle">
<span x-text="release.year || ''"></span>
<span x-text="release.release_type"></span>
</div>
</div>
</template>
</div>
</div>
</template>
<!-- Tracks section -->
<template x-if="$store.library.searchResults.tracks.length > 0">
<div class="search-section">
<h2 class="search-section-title">{{ t.player_tracks }}</h2>
<div class="track-list-header">
<span>#</span>
<span>{{ t.player_title }}</span>
<span></span>
<span></span>
<span style="text-align:right">{{ t.player_duration }}</span>
</div>
<template x-for="(track, idx) in $store.library.searchResults.tracks" :key="track.id">
<div class="track-row"
:class="{ playing: $store.player.currentTrack && $store.player.currentTrack.id === track.id }"
@dblclick="$store.library.playSearchTrack(idx)">
<span class="track-num" x-text="idx + 1"></span>
<div class="track-info">
<div class="track-title" x-text="track.title"></div>
<div class="track-artists-inline">
<template x-for="(artist, artistIdx) in $store.library.trackArtistLinks(track)" :key="artist.label + '-' + artist.id + '-' + artistIdx">
<span>
<template x-if="artistIdx > 0"><span>, </span></template>
<a class="artist-link" @click.stop="$store.library.openArtist(artist.id)" x-text="artist.label"></a>
</span>
</template>
</div>
</div>
<span></span>
<div class="track-actions">
<button class="track-action-btn info-btn popularity-info-btn"
:class="{ 'has-popularity': $store.library.hasPopularity(track), 'no-popularity': !$store.library.hasPopularity(track) }"
:style="$store.library.popularityStyle(track)"
@click.stop="$store.library.openTrackInfo(track)"
:title="$store.library.trackInfoTitle(track)"
aria-label="{{ t.player_track_info }}">
<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="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 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 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 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>
<span class="track-duration" x-text="formatTime(track.duration_seconds)"></span>
</div>
</template>
</div>
</template>
</div>
</template>
</div>
</template>
<!-- Artists Grid -->
<template x-if="$store.library.view === 'artists'">
<div>
<h1 class="section-title">{{ t.player_artists }}</h1>
<div class="card-grid">
<template x-for="artist in $store.library.artists" :key="artist.id">
<div class="card" @click="$store.library.openArtist(artist.id)">
<div class="card-img">
<template x-if="artist.image_url">
<img :src="artist.image_url" :alt="artist.name" loading="lazy">
</template>
<template x-if="!artist.image_url">
<span class="placeholder-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="8" r="4"/><path d="M20 21a8 8 0 10-16 0"/></svg></span>
</template>
<button class="artist-follow-card-btn"
:class="{ followed: $store.follows.has(artist.id) }"
@click.stop="$store.follows.toggle(artist.id)"
:title="$store.follows.has(artist.id) ? '{{ t.player_unfollow_artist }}' : '{{ t.player_follow_artist }}'">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M16 21v-2a4 4 0 00-4-4H6a4 4 0 00-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path x-show="!$store.follows.has(artist.id)" d="M19 8v6M16 11h6"/>
<path x-show="$store.follows.has(artist.id)" d="M16 11l2 2 4-5"/>
</svg>
</button>
</div>
<div class="card-title" x-text="artist.name"></div>
<div class="card-subtitle" x-text="artist.release_count + ' {{ t.player_releases_count }} · ' + artist.track_count + ' {{ t.player_tracks_count }}'"></div>
</div>
</template>
</div>
<template x-if="$store.library.loading">
<div class="loading-spinner"><div class="spinner"></div></div>
</template>
<div id="artist-sentinel" style="height:1px"></div>
</div>
</template>
<!-- Artist Detail -->
<template x-if="$store.library.view === 'artist_detail' && $store.library.currentArtist">
<div>
<div class="breadcrumb">
<a @click="$store.library.goArtists()">{{ t.player_artists }}</a>
<span>/</span>
<span x-text="$store.library.currentArtist.name"></span>
</div>
<div class="artist-header">
<div class="artist-img">
<template x-if="$store.library.currentArtist.image_url">
<img :src="$store.library.currentArtist.image_url" :alt="$store.library.currentArtist.name">
</template>
<template x-if="!$store.library.currentArtist.image_url">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="8" r="4"/><path d="M20 21a8 8 0 10-16 0"/></svg>
</template>
</div>
<div>
<div class="artist-name" x-text="$store.library.currentArtist.name"></div>
<div class="artist-stats">
<span x-text="$store.library.currentArtist.releases.length + ' {{ t.player_releases_count }}'"></span>
<span></span>
<span x-text="$store.library.currentArtist.total_track_count + ' {{ t.player_tracks_count }}'"></span>
<span></span>
<span x-text="$store.library.currentArtist.total_play_count + ' {{ t.player_plays_count }}'"></span>
</div>
<div class="release-actions artist-actions">
<button class="release-action-btn primary artist-listen-action"
:disabled="!($store.library.currentArtist.top_tracks && $store.library.currentArtist.top_tracks.length)"
@click="$store.library.playArtistTopTracks()"
title="{{ t.player_listen_artist }}">
<svg viewBox="0 0 24 24" fill="currentColor" stroke="none">
<path d="M8 5v14l11-7z"/>
</svg>
<span>{{ t.player_listen }}</span>
</button>
<button class="release-action-btn secondary artist-follow-action"
:class="{ followed: $store.follows.has($store.library.currentArtist.id) }"
@click="$store.follows.toggle($store.library.currentArtist.id)"
:title="$store.follows.has($store.library.currentArtist.id) ? '{{ t.player_unfollow_artist }}' : '{{ t.player_follow_artist }}'">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M16 21v-2a4 4 0 00-4-4H6a4 4 0 00-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path x-show="!$store.follows.has($store.library.currentArtist.id)" d="M19 8v6M16 11h6"/>
<path x-show="$store.follows.has($store.library.currentArtist.id)" d="M16 11l2 2 4-5"/>
</svg>
<span x-text="$store.follows.has($store.library.currentArtist.id) ? '{{ t.player_followed }}' : '{{ t.player_follow }}'"></span>
</button>
</div>
</div>
</div>
<template x-for="group in $store.library.artistReleaseGroups()" :key="group.type">
<section class="artist-release-group">
<h2 class="artist-release-group-title" x-text="group.label"></h2>
<div class="card-grid">
<template x-for="release in group.releases" :key="release.id">
<div class="card" @click="$store.library.openRelease(release.id)">
<div class="card-img">
<template x-if="release.cover_url">
<img :src="release.cover_url" :alt="release.title" loading="lazy">
</template>
<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.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 }}">
<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>
<button class="card-play-btn" @click.stop="$store.library.playRelease(release.id)">
<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
</button>
</div>
<div class="card-title" x-text="release.title"></div>
<div class="card-subtitle">
<span x-text="release.year || ''"></span>
<span x-text="release.track_count + ' {{ t.player_tracks_count }}'"></span>
</div>
</div>
</template>
</div>
</section>
</template>
<template x-if="$store.library.currentArtist.featured_tracks && $store.library.currentArtist.featured_tracks.length > 0">
<section class="artist-release-group">
<h2 class="artist-release-group-title">{{ t.player_appears_on }}</h2>
<div class="track-list-header">
<span>#</span>
<span>{{ t.player_title }}</span>
<span></span>
<span></span>
<span style="text-align:right">{{ t.player_duration }}</span>
</div>
<template x-for="(track, idx) in $store.library.currentArtist.featured_tracks" :key="track.id">
<div class="track-row"
:class="{ playing: $store.player.currentTrack && $store.player.currentTrack.id === track.id }"
@dblclick="$store.queue.playRelease($store.library.currentArtist.featured_tracks, idx)">
<span class="track-num" x-text="idx + 1"></span>
<div class="track-info">
<div class="track-title">
<span x-text="track.title"></span>
<span style="color:var(--text-subdued)"> · </span>
<a class="artist-link" @click.stop="$store.library.openRelease(track.release_id)" x-text="track.release_title"></a>
</div>
<div class="track-artists-inline">
<template x-for="(artist, artistIdx) in $store.library.trackArtistLinks(track)" :key="artist.label + '-' + artist.id + '-' + artistIdx">
<span>
<template x-if="artistIdx > 0"><span>, </span></template>
<a class="artist-link" @click.stop="$store.library.openArtist(artist.id)" x-text="artist.label"></a>
</span>
</template>
</div>
</div>
<span></span>
<div class="track-actions">
<button class="track-action-btn info-btn popularity-info-btn"
:class="{ 'has-popularity': $store.library.hasPopularity(track), 'no-popularity': !$store.library.hasPopularity(track) }"
:style="$store.library.popularityStyle(track)"
@click.stop="$store.library.openTrackInfo(track)"
:title="$store.library.trackInfoTitle(track)"
aria-label="{{ t.player_track_info }}">
<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="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 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 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 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>
<span class="track-duration" x-text="formatTime(track.duration_seconds)"></span>
</div>
</template>
</section>
</template>
</div>
</template>
<!-- Release Detail -->
<template x-if="$store.library.view === 'release_detail' && $store.library.currentRelease">
<div>
<div class="breadcrumb">
<a @click="$store.library.goArtists()">{{ t.player_artists }}</a>
<span>/</span>
<template x-if="$store.library.currentRelease.artists.length > 0">
<a @click="$store.library.openArtist($store.library.currentRelease.artists[0].id)" x-text="$store.library.currentRelease.artists[0].name"></a>
</template>
<span>/</span>
<span x-text="$store.library.currentRelease.title"></span>
</div>
<div class="release-header">
<div class="release-cover">
<template x-if="$store.library.currentRelease.cover_url">
<img :src="$store.library.currentRelease.cover_url" :alt="$store.library.currentRelease.title">
</template>
<template x-if="!$store.library.currentRelease.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>
</div>
<div class="release-meta">
<div class="release-type" x-text="$store.library.currentRelease.release_type"></div>
<div class="release-title-row">
<div class="release-title" x-text="$store.library.currentRelease.title"></div>
<button class="like-btn like-btn-lg release-title-like"
:class="{ liked: $store.likes.isReleaseLiked($store.library.currentRelease) }"
@click.stop="$store.likes.toggleRelease($store.library.currentRelease.id)"
title="{{ t.player_like }}">
<svg viewBox="0 0 24 24" :fill="$store.likes.isReleaseLiked($store.library.currentRelease) ? '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="release-artists">
<template x-for="(artist, artistIdx) in $store.library.currentRelease.artists" :key="artist.id">
<span>
<template x-if="artistIdx > 0"><span>, </span></template>
<a class="artist-link" @click="$store.library.openArtist(artist.id)" x-text="artist.name"></a>
</span>
</template>
</div>
<div class="release-year" x-text="$store.library.currentRelease.year || ''"></div>
<div class="release-actions">
<button class="release-action-btn secondary"
@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>
{{ t.player_info }}
</button>
<button class="release-action-btn primary" @click="$store.queue.playRelease($store.library.currentRelease.tracks, 0)">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
{{ t.player_play }}
</button>
<button class="release-action-btn secondary" @click="$store.queue.addToEnd($store.library.currentRelease.tracks)" title="{{ t.player_add_to_end_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>
{{ t.player_queue }}
</button>
<button class="release-action-btn secondary" @click="$store.queue.addNextInQueue($store.library.currentRelease.tracks)" 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>
{{ t.player_next }}
</button>
</div>
</div>
</div>
<!-- Track list -->
<div class="track-list-header">
<span>#</span>
<span>{{ t.player_title }}</span>
<span></span>
<span></span>
<span style="text-align:right">{{ t.player_duration }}</span>
</div>
<template x-for="(track, idx) in $store.library.currentRelease.tracks" :key="track.id">
<div class="track-row"
:class="{ playing: $store.player.currentTrack && $store.player.currentTrack.id === track.id }"
@dblclick="$store.queue.playRelease([track], 0)">
<span class="track-num" x-text="track.track_number || (idx + 1)"></span>
<div class="track-info">
<div class="track-title" x-text="track.title"></div>
<div class="track-artists-inline">
<template x-for="(artist, artistIdx) in $store.library.trackArtistLinks(track)" :key="artist.label + '-' + artist.id + '-' + artistIdx">
<span>
<template x-if="artistIdx > 0"><span>, </span></template>
<a class="artist-link" @click.stop="$store.library.openArtist(artist.id)" x-text="artist.label"></a>
</span>
</template>
</div>
</div>
<span></span>
<div class="track-actions">
<button class="track-action-btn info-btn popularity-info-btn"
:class="{ 'has-popularity': $store.library.hasPopularity(track), 'no-popularity': !$store.library.hasPopularity(track) }"
:style="$store.library.popularityStyle(track)"
@click.stop="$store.library.openTrackInfo(track)"
:title="$store.library.trackInfoTitle(track)"
aria-label="{{ t.player_track_info }}">
<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="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 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 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 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>
<span class="track-duration" x-text="formatTime(track.duration_seconds)"></span>
</div>
</template>
</div>
</template>
<!-- Playlist Detail -->
<template x-if="$store.library.view === 'playlist_detail' && $store.library.currentPlaylist">
<div>
<div class="breadcrumb">
<a @click="$store.library.goArtists()">{{ t.player_library }}</a>
<span>/</span>
<span x-text="$store.playlists.displayTitle($store.library.currentPlaylist)"></span>
</div>
<h1 class="section-title" x-text="$store.playlists.displayTitle($store.library.currentPlaylist)"></h1>
<div class="playlist-detail-meta"
x-show="$store.library.currentPlaylist.owner_name || $store.library.currentPlaylist.is_public">
<span x-show="$store.library.currentPlaylist.owner_name"
x-text="'{{ t.player_by }} ' + $store.library.currentPlaylist.owner_name"></span>
<span x-show="$store.library.currentPlaylist.owner_name && $store.library.currentPlaylist.is_public">&middot;</span>
<span class="playlist-public-badge"
x-show="$store.library.currentPlaylist.is_public">{{ t.player_published }}</span>
</div>
<template x-if="$store.library.currentPlaylist.description">
<p style="color:var(--text-subdued);margin-bottom:16px" x-text="$store.library.currentPlaylist.description"></p>
</template>
<div class="track-list-header">
<span>#</span>
<span>{{ t.player_title }}</span>
<span></span>
<span></span>
<span style="text-align:right">{{ t.player_duration }}</span>
</div>
<template x-for="(track, idx) in $store.library.currentPlaylist.tracks" :key="track.id">
<div class="track-row"
:class="{ playing: $store.player.currentTrack && $store.player.currentTrack.id === track.id }"
@dblclick="$store.queue.playRelease($store.library.currentPlaylist.tracks, idx)">
<span class="track-num" x-text="idx + 1"></span>
<div class="track-info">
<div class="track-title" x-text="track.title"></div>
<div class="track-artists-inline">
<template x-for="(artist, artistIdx) in $store.library.trackArtistLinks(track)" :key="artist.label + '-' + artist.id + '-' + artistIdx">
<span>
<template x-if="artistIdx > 0"><span>, </span></template>
<a class="artist-link" @click.stop="$store.library.openArtist(artist.id)" x-text="artist.label"></a>
</span>
</template>
</div>
</div>
<span></span>
<div class="track-actions">
<button class="track-action-btn info-btn popularity-info-btn"
:class="{ 'has-popularity': $store.library.hasPopularity(track), 'no-popularity': !$store.library.hasPopularity(track) }"
:style="$store.library.popularityStyle(track)"
@click.stop="$store.library.openTrackInfo(track)"
:title="$store.library.trackInfoTitle(track)"
aria-label="{{ t.player_track_info }}">
<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="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 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 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 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>
<span class="track-duration" x-text="formatTime(track.duration_seconds)"></span>
</div>
</template>
</div>
</template>
</div>
<!-- Queue Panel -->
<div class="queue-backdrop"
x-show="$store.queue.visible"
x-cloak
@click="$store.queue.visible = false"></div>
<div class="queue-panel" :class="{ hidden: !$store.queue.visible }">
<div class="queue-header">
<h3>{{ t.player_queue }}</h3>
<button class="queue-clear-btn" @click="$store.queue.clear()">{{ t.player_clear }}</button>
</div>
<div class="queue-tracks">
<template x-if="$store.queue.tracks.length === 0">
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
<p>{{ t.player_queue_empty }}</p>
</div>
</template>
<template x-for="(track, idx) in $store.queue.tracks" :key="idx + '-' + track.id">
<div class="queue-track"
:data-queue-index="idx"
:class="{ active: idx === $store.queue.currentIndex, dragging: $store.queue._dragIdx === idx, 'foreign-jam-track': $store.queue.isForeignJamTrack(track) }"
:style="$store.queue.isForeignJamTrack(track) ? $store.queue.contributorStyle(track) : ''"
@click="$store.queue.playFromIndex(idx)"
draggable="true"
@dragstart="$store.queue._dragIdx = idx; $event.dataTransfer.effectAllowed = 'move'"
@dragend="$store.queue._dragIdx = null; document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'))"
@dragover.prevent="$event.dataTransfer.dropEffect = 'move'; $event.currentTarget.classList.add('drag-over')"
@dragleave="$event.currentTarget.classList.remove('drag-over')"
@drop.prevent="$event.currentTarget.classList.remove('drag-over'); if ($store.queue._dragIdx !== null) { $store.queue.moveTrack($store.queue._dragIdx, idx); $store.queue._dragIdx = null; }">
<div class="queue-drag-handle"
@mousedown.stop
@click.stop
@pointerdown.stop="$store.queue.startPointerReorder($event, idx)">
<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="9" cy="6" r="1.5"/><circle cx="15" cy="6" r="1.5"/><circle cx="9" cy="12" r="1.5"/><circle cx="15" cy="12" r="1.5"/><circle cx="9" cy="18" r="1.5"/><circle cx="15" cy="18" r="1.5"/></svg>
</div>
<div class="queue-track-cover">
<template x-if="track.cover_url">
<img :src="track.cover_url" :alt="track.title" loading="lazy">
</template>
<template x-if="!track.cover_url">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
</template>
</div>
<div class="queue-track-info">
<div class="queue-track-title" x-text="track.title"></div>
<div class="queue-track-artist">
<template x-for="(artist, artistIdx) in $store.library.trackArtistLinks(track)" :key="artist.label + '-' + artist.id + '-' + artistIdx">
<span>
<template x-if="artistIdx > 0"><span>, </span></template>
<a class="artist-link" @click.stop="$store.library.openArtist(artist.id)" x-text="artist.label"></a>
</span>
</template>
</div>
</div>
<div class="queue-track-actions">
<button class="queue-track-remove info-btn popularity-info-btn"
:class="{ 'has-popularity': $store.library.hasPopularity(track), 'no-popularity': !$store.library.hasPopularity(track) }"
:style="$store.library.popularityStyle(track)"
@click.stop="$store.library.openTrackInfo(track)"
:title="$store.library.trackInfoTitle(track)"
aria-label="{{ t.player_track_info }}">
<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="queue-track-remove" @click.stop="$store.queue.remove(idx)" title="{{ t.player_remove }}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
</div>
</template>
</div>
</div>
</div>
<!-- Player Bar -->
<div class="player-bar"
:class="{ 'mobile-expanded': $store.mobile.playerExpanded, 'mobile-dragging': $store.mobile.playerDragging }"
:style="$store.mobile.playerDragStyle()"
@click.capture="$store.mobile.handlePlayerClick($event)"
@pointerdown="$store.mobile.startPlayerDrag($event)">
<button class="mobile-player-collapse-btn" type="button" @click.stop="$store.mobile.closePlayerFullscreen()" title="{{ t.player_close }}" aria-label="{{ t.player_close }}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4">
<path d="M6 9l6 6 6-6"/>
</svg>
</button>
<div class="player-now-playing">
<template x-if="$store.player.currentTrack">
<div style="display:flex;align-items:center;gap:12px;overflow:hidden">
<div class="player-cover"
@click.stop="$store.mobile.openPlayerFullscreen()">
<template x-if="$store.player.currentTrack.cover_url">
<img :src="$store.player.currentTrack.cover_url" :alt="$store.player.currentTrack.title">
</template>
<template x-if="!$store.player.currentTrack.cover_url">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
</template>
</div>
<div class="player-track-info">
<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>
<template x-if="artistIdx > 0"><span>, </span></template>
<a class="artist-link" @click.stop="$store.library.openArtist(artist.id)" x-text="artist.label"></a>
</span>
</template>
<template x-if="$store.player.currentTrack.release_year">
<span class="player-release-year" x-text="' · ' + $store.player.currentTrack.release_year"></span>
</template>
</div>
<div class="player-track-release" x-show="$store.player.currentTrack.release_title">
<a class="artist-link" @click.stop="$store.player.currentTrack.release_id && $store.library.openRelease($store.player.currentTrack.release_id)" x-text="$store.player.currentTrack.release_title"></a>
</div>
</div>
</div>
</template>
</div>
<div class="player-controls">
<div class="player-buttons">
<button class="player-btn" :class="{ active: $store.player.shuffle }" @click="$store.player.toggleShuffle()" title="{{ t.player_shuffle }}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></svg>
</button>
<button class="player-btn" @click="$store.player.prev()" title="{{ t.player_previous }}">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg>
</button>
<button class="player-btn player-btn-play" @click="$store.player.toggle()">
<template x-if="!$store.player.isPlaying">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
</template>
<template x-if="$store.player.isPlaying">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 4h4v16H6zM14 4h4v16h-4z"/></svg>
</template>
</button>
<button class="player-btn" @click="$store.player.next()" title="{{ t.player_next }}">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg>
</button>
<button class="player-btn" :class="{ active: $store.player.repeatMode !== 'off' }" @click="$store.player.cycleRepeat()" title="{{ t.player_repeat }}">
<template x-if="$store.player.repeatMode !== 'one'">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 014-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 01-4 4H3"/></svg>
</template>
<template x-if="$store.player.repeatMode === 'one'">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 014-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 01-4 4H3"/><text x="12" y="14" font-size="8" fill="currentColor" text-anchor="middle" font-weight="bold">1</text></svg>
</template>
</button>
</div>
<div class="player-timeline">
<span class="player-time" x-text="formatTime($store.player.currentTime)"></span>
<div class="progress-bar" @click="$store.player.seekFromClick($event)">
<div class="progress-bar-fill" :style="'width:' + $store.player.progress + '%'">
<div class="progress-bar-thumb"></div>
</div>
</div>
<div class="player-progress-strip-times"
x-text="'-' + formatTime(Math.max(0, $store.player.duration - $store.player.currentTime)) + ' / ' + formatTime($store.player.duration)"></div>
<span class="player-time" x-text="formatTime($store.player.duration)"></span>
</div>
<div class="player-version-chip">v{{ t.app_version() }}</div>
</div>
<div class="player-right">
<div class="volume-control">
<button class="volume-btn" @click="$store.player.toggleMute()">
<template x-if="$store.player.volume === 0">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/></svg>
</template>
<template x-if="$store.player.volume > 0 && $store.player.volume < 0.5">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M15.54 8.46a5 5 0 010 7.07"/></svg>
</template>
<template x-if="$store.player.volume >= 0.5">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 010 14.14M15.54 8.46a5 5 0 010 7.07"/></svg>
</template>
</button>
<div class="volume-slider"
@pointerdown.prevent="$store.player.startVolumeDrag($event)"
aria-label="{{ t.player_volume }}">
<div class="volume-slider-fill" :style="'width:' + $store.player.volumeSliderPercent() + '%'">
<div class="volume-slider-thumb"></div>
</div>
</div>
</div>
<button class="queue-toggle-btn" :class="{ active: $store.queue.visible }" @click="$store.queue.visible = !$store.queue.visible" title="{{ t.player_queue }}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
</button>
<div class="device-picker" @click.outside="$store.devices.open = false">
<button class="queue-toggle-btn device-toggle-btn"
:class="{ active: $store.devices.isActive() }"
@click="$store.devices.toggle()"
:title="$store.devices.activeLabel()"
aria-label="Devices">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="12" rx="2"/>
<path d="M8 20h8"/>
<path d="M12 16v4"/>
</svg>
<span class="jam-member-squares"
x-show="$store.devices.activeJamMembers().length > 0"
x-cloak>
<template x-for="member in $store.devices.activeJamMembers().slice(0, 8)" :key="'device-jam-member-' + member.user_id">
<span class="jam-member-square" :style="$store.devices.userColorStyle(member.user_id, member.name)"></span>
</template>
</span>
</button>
<div class="device-popover" x-show="$store.devices.open" x-transition x-cloak>
<template x-for="device in $store.devices.devices" :key="device.id">
<button class="device-row"
:class="{ active: device.is_active, 'current-device': device.is_current }"
@click="$store.devices.select(device.id)">
<span class="device-row-icon">
<template x-if="device.kind === 'phone'">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="7" y="2" width="10" height="20" rx="2"/>
<path d="M11 18h2"/>
</svg>
</template>
<template x-if="device.kind !== 'phone'">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="12" rx="2"/>
<path d="M8 20h8"/>
<path d="M12 16v4"/>
</svg>
</template>
</span>
<span class="device-row-main">
<span class="device-row-name" x-text="device.name"></span>
<span class="device-row-current" x-show="device.is_current">This device</span>
</span>
<span class="device-row-check" x-show="device.is_active">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4">
<polyline points="20 6 9 17 4 12"/>
</svg>
</span>
</button>
</template>
<template x-if="$store.devices.jams.length > 0">
<div class="device-section-label jam-section-label">Jams</div>
</template>
<template x-for="jam in $store.devices.jams" :key="jam.id">
<button class="device-row jam-row"
:class="{ active: jam.is_active, pending: jam.is_pending }"
@click="$store.devices.handleJamRowClick(jam)">
<span class="device-row-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="8" cy="8" r="3"/>
<circle cx="16" cy="8" r="3"/>
<path d="M4 20v-1a4 4 0 014-4h0a4 4 0 014 4v1"/>
<path d="M12 20v-1a4 4 0 014-4h0a4 4 0 014 4v1"/>
</svg>
</span>
<span class="device-row-main">
<span class="device-row-name" x-text="jam.name"></span>
<span class="device-row-current"
x-text="!jam.host_device_online ? 'Host offline' : (jam.is_pending ? 'Invite pending' : (jam.is_owner ? 'Your Jam' : 'Shared queue'))"></span>
</span>
<span class="device-row-check" x-show="jam.is_active">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4">
<polyline points="20 6 9 17 4 12"/>
</svg>
</span>
</button>
</template>
<button class="device-row start-jam-row" x-show="!$store.devices.hasJoinedJam()" @click="$store.devices.openJamPanel()">
<span class="device-row-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 5v14"/>
<path d="M5 12h14"/>
</svg>
</span>
<span class="device-row-main">
<span class="device-row-name">Start Jam</span>
<span class="device-row-current">Invite listeners</span>
</span>
</button>
<div class="jam-create-panel" x-show="$store.devices.jamPanelOpen" x-transition x-cloak>
<div class="jam-panel-title" x-text="$store.devices.jamPanelMode === 'manage' ? 'Invite listeners' : 'Start Jam'"></div>
<div class="jam-selected-users" x-show="$store.devices.jamSelectedUsers.length > 0">
<template x-for="user in $store.devices.jamSelectedUsers" :key="user.id">
<button class="jam-user-chip" @click="$store.devices.removeJamInvitee(user.id)">
<span x-text="user.display_name || user.username"></span>
<span aria-hidden="true">×</span>
</button>
</template>
</div>
<input class="jam-user-search"
type="search"
placeholder="Search users"
x-model="$store.devices.jamQuery"
@input="$store.devices.queueJamSearch()">
<div class="jam-search-results" x-show="$store.devices.jamUsers.length > 0">
<template x-for="user in $store.devices.jamUsers" :key="user.id">
<button class="jam-search-row" @click="$store.devices.addJamInvitee(user)">
<span x-text="user.display_name || user.username"></span>
<small x-text="user.email || user.username"></small>
</button>
</template>
</div>
<div class="jam-panel-actions">
<button class="jam-create-btn"
:disabled="$store.devices.jamSelectedUsers.length === 0 && $store.devices.jamPanelMode === 'manage'"
@click="$store.devices.submitJamPanel()"
x-text="$store.devices.jamPanelMode === 'manage' ? 'Invite' : 'Create Jam'"></button>
<button class="jam-leave-btn"
x-show="$store.devices.jamPanelMode === 'manage'"
@click="$store.devices.leaveJam($store.devices.jamPanelJamId)">Leave</button>
</div>
</div>
</div>
</div>
</div>
<div class="mobile-expanded-queue">
<div class="mobile-expanded-queue-title">{{ t.player_queue }}</div>
<template x-if="$store.queue.upcoming().length === 0">
<div class="mobile-expanded-queue-empty">{{ t.player_queue_empty }}</div>
</template>
<template x-for="(track, idx) in $store.queue.upcoming()" :key="'mobile-expanded-queue-' + track.id + '-' + idx">
<button class="mobile-expanded-queue-row"
:class="{ 'foreign-jam-track': $store.queue.isForeignJamTrack(track) }"
:style="$store.queue.isForeignJamTrack(track) ? $store.queue.contributorStyle(track) : ''"
type="button"
@click="$store.queue.playFromIndex($store.queue.currentIndex + idx + 1)">
<div class="mobile-expanded-queue-cover">
<template x-if="track.cover_url">
<img :src="track.cover_url" :alt="track.title" loading="lazy">
</template>
<template x-if="!track.cover_url">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
</template>
</div>
<div class="mobile-expanded-queue-info">
<div class="mobile-expanded-queue-name" x-text="track.title"></div>
<div class="mobile-expanded-queue-artist">
<template x-for="(artist, artistIdx) in $store.library.trackArtistLinks(track)" :key="artist.label + '-' + artist.id + '-' + artistIdx">
<span>
<template x-if="artistIdx > 0"><span>, </span></template>
<span x-text="artist.label"></span>
</span>
</template>
</div>
</div>
<span class="mobile-expanded-queue-time" x-text="formatTime(track.duration_seconds)"></span>
</button>
</template>
</div>
</div>