3a9240b82c
Add track, release, and queue sharing with post-login redirects; support shared playlist links and highlighted shared tracks. Add local synchronized playback for jams, constrain HTTP metrics to known routes, and refine mobile player controls/layout.
754 lines
65 KiB
HTML
754 lines
65 KiB
HTML
<!-- Info Modal -->
|
|
<template x-if="$store.info.modal">
|
|
<div class="modal-overlay" @click.self="$store.info.close()">
|
|
<div class="modal-box info-modal">
|
|
<div class="info-modal-head">
|
|
<h3 x-text="$store.info.modal.title"></h3>
|
|
<button class="mobile-list-action" @click="$store.info.close()" 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="info-actions" x-show="$store.info.modal.actions && $store.info.modal.actions.length">
|
|
<template x-for="(action, idx) in $store.info.modal.actions" :key="action.label + '-' + idx">
|
|
<button class="info-action-btn" type="button" @click.stop.prevent="$store.info.runAction(idx)" :disabled="action.busy === true">
|
|
<span x-text="action.busy ? '{{ t.player_resolving }}' : action.label"></span>
|
|
</button>
|
|
</template>
|
|
</div>
|
|
<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>
|
|
|
|
<!-- Create / Rename Playlist Modal -->
|
|
<template x-if="$store.playlists.modal">
|
|
<div class="modal-overlay" @click.self="$store.playlists.modal = null">
|
|
<div class="modal-box">
|
|
<h3 x-text="$store.playlists.modal.mode === 'create' ? '{{ t.player_new_playlist }}' : '{{ t.player_rename_playlist }}'"></h3>
|
|
<input type="text" x-model="$store.playlists.modal.title" placeholder="{{ t.player_playlist_name }}"
|
|
@keydown.enter="$store.playlists.submitModal()" x-init="$nextTick(() => $el.focus())">
|
|
<div class="modal-footer">
|
|
<button class="modal-btn modal-btn-ghost" @click="$store.playlists.modal = null">{{ t.player_cancel }}</button>
|
|
<button class="modal-btn modal-btn-primary" @click="$store.playlists.submitModal()"
|
|
x-text="$store.playlists.modal.mode === 'create' ? '{{ t.player_create }}' : '{{ t.player_save }}'"></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Add to Playlist Modal -->
|
|
<template x-if="$store.playlists.picker">
|
|
<div class="modal-overlay" @click.self="$store.playlists.picker = null">
|
|
<div class="modal-box">
|
|
<h3>{{ t.player_add_to_playlist }}</h3>
|
|
<div class="modal-playlist-list">
|
|
<template x-for="pl in $store.playlists.list.filter(p => p.kind === 'user' && p.is_own)" :key="pl.id">
|
|
<div class="modal-playlist-item" @click="$store.playlists.addToPicked(pl.id)">
|
|
<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>
|
|
<span x-text="pl.title"></span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="modal-btn modal-btn-ghost" @click="$store.playlists.picker = null">{{ t.player_cancel }}</button>
|
|
<button class="modal-btn modal-btn-primary" @click="$store.playlists.picker = null; $store.playlists.showCreate()">{{ t.player_new_playlist }}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Torrent Import Modal -->
|
|
<template x-if="$store.torrents.modal">
|
|
<div class="modal-overlay" @click.self="$store.torrents.close()">
|
|
<div class="modal-box torrent-modal">
|
|
<div class="torrent-modal-head">
|
|
<div>
|
|
<h3>{{ t.player_torrent_manager }}</h3>
|
|
<p class="torrent-message" style="margin:4px 0 0"
|
|
:class="{ error: $store.torrents.error }"
|
|
x-text="$store.torrents.message"></p>
|
|
</div>
|
|
<button class="torrent-modal-close"
|
|
@click="$store.torrents.close()"
|
|
title="{{ t.player_close }}"
|
|
aria-label="{{ t.player_close }}">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
|
|
<line x1="18" y1="6" x2="6" y2="18"/>
|
|
<line x1="6" y1="6" x2="18" y2="18"/>
|
|
</svg>
|
|
</button>
|
|
<div class="torrent-client-status">
|
|
<span class="torrent-status-pill"
|
|
:class="{ active: $store.torrents.activeCount() > 0 }"
|
|
x-text="$store.torrents.clientSummary()"></span>
|
|
<span class="torrent-status-pill torrent-agent-pill"
|
|
:class="{ active: $store.torrents.agentBusy() }">
|
|
<span class="torrent-agent-dot"></span>
|
|
<span x-text="$store.torrents.agentSummary()"></span>
|
|
</span>
|
|
<span class="torrent-status-pill"
|
|
x-text="$store.torrents.sessions.length + ' ' + T.saved"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="torrent-tabs">
|
|
<button class="torrent-tab-btn"
|
|
:class="{ active: $store.torrents.activeTab === 'import' }"
|
|
@click="$store.torrents.showImportTab()">{{ t.player_import }}</button>
|
|
<button class="torrent-tab-btn"
|
|
:class="{ active: $store.torrents.activeTab === 'uploads' }"
|
|
@click="$store.torrents.showUploadsTab()">
|
|
<span>{{ t.player_my_uploads }}</span>
|
|
<span class="torrent-tab-count"
|
|
x-show="$store.torrents.uploadPendingTotal + $store.torrents.uploadQueuedTotal > 0"
|
|
x-text="$store.torrents.uploadPendingTotal + $store.torrents.uploadQueuedTotal"></span>
|
|
</button>
|
|
</div>
|
|
|
|
<template x-if="$store.torrents.activeTab === 'import'">
|
|
<div class="torrent-manager-layout">
|
|
<aside class="torrent-manager-sidebar">
|
|
<div class="torrent-manager-title">
|
|
<span>{{ t.player_saved_torrents }}</span>
|
|
<button class="modal-btn modal-btn-ghost" style="padding:4px 8px"
|
|
@click="$store.torrents.loadSessions()"
|
|
:disabled="$store.torrents.loading">{{ t.player_refresh }}</button>
|
|
</div>
|
|
<div class="torrent-session-list">
|
|
<template x-if="!$store.torrents.loadingSessions && $store.torrents.sessions.length === 0">
|
|
<div class="empty-state" style="padding:28px 12px">
|
|
<p>{{ t.player_no_saved_torrents }}</p>
|
|
</div>
|
|
</template>
|
|
<template x-for="job in $store.torrents.sessions" :key="job.id">
|
|
<div class="torrent-session-row"
|
|
:class="{ active: $store.torrents.workspaceMode === 'session' && $store.torrents.previewData && $store.torrents.previewData.id === job.id }"
|
|
@click="$store.torrents.openSession(job.id)">
|
|
<div class="torrent-session-main">
|
|
<div class="torrent-session-topline">
|
|
<div class="torrent-session-name" x-text="job.name"></div>
|
|
<span class="torrent-status-badge"
|
|
:class="$store.torrents.statusBadgeClass(job)"
|
|
x-text="$store.torrents.statusLabel(job)"></span>
|
|
</div>
|
|
<div class="torrent-session-meta" x-text="$store.torrents.sessionMeta(job)"></div>
|
|
<div class="torrent-session-progress">
|
|
<div class="torrent-session-progress-bar"
|
|
:style="'width:' + $store.torrents.progressValue(job) + '%'"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<button type="button"
|
|
class="torrent-session-row torrent-session-add"
|
|
:class="{ active: $store.torrents.isImporting() }"
|
|
@click="$store.torrents.addNew()"
|
|
:disabled="$store.torrents.loading">
|
|
<span class="torrent-session-add-icon">+</span>
|
|
<span>{{ t.player_upload }}</span>
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
|
|
<section class="torrent-workspace">
|
|
<template x-if="$store.torrents.workspaceMode === 'empty'">
|
|
<div class="empty-state torrent-workspace-empty">
|
|
<p x-text="T.chooseSavedOrAddTorrent"></p>
|
|
</div>
|
|
</template>
|
|
|
|
<template x-if="$store.torrents.isImporting()">
|
|
<div class="torrent-import-panel">
|
|
<div class="torrent-modal-grid">
|
|
<div>
|
|
<label for="local-file-input">{{ t.player_local_files }}</label>
|
|
<input id="local-file-input" type="file" multiple accept="audio/*,.mp3,.flac,.wav,.m4a,.ogg,.opus,.aac"
|
|
@change="$store.torrents.setLocalFiles($event.target.files)">
|
|
<div class="torrent-upload-summary" x-text="$store.torrents.localUploadSummary()"></div>
|
|
</div>
|
|
<div>
|
|
<label for="torrent-magnet-input">{{ t.player_magnet_link }}</label>
|
|
<input id="torrent-magnet-input" type="text"
|
|
x-model="$store.torrents.magnet"
|
|
placeholder="magnet:?xt=urn:btih:...">
|
|
</div>
|
|
<div>
|
|
<label for="torrent-file-input">{{ t.player_torrent_file }}</label>
|
|
<input id="torrent-file-input" type="file" accept=".torrent,application/x-bittorrent"
|
|
@change="$store.torrents.file = $event.target.files[0] || null">
|
|
</div>
|
|
</div>
|
|
<div class="torrent-upload-progress"
|
|
x-show="$store.torrents.uploadProgress > 0 || ($store.torrents.localFiles.length > 0 && $store.torrents.loading)">
|
|
<div class="torrent-progress-head">
|
|
<span x-text="$store.torrents.uploadProgress >= 100 ? T.uploadComplete : T.uploadingFiles"></span>
|
|
<span x-text="$store.torrents.uploadProgressText"></span>
|
|
</div>
|
|
<div class="torrent-progress-track">
|
|
<div class="torrent-progress-bar"
|
|
:style="'width:' + $store.torrents.uploadProgress + '%'"></div>
|
|
</div>
|
|
</div>
|
|
<div class="torrent-actions">
|
|
<button class="modal-btn modal-btn-primary" @click="$store.torrents.preview()" :disabled="$store.torrents.loading">
|
|
{{ t.player_upload_content }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template x-if="$store.torrents.currentJob">
|
|
<div class="torrent-progress-card">
|
|
<div class="torrent-progress-head">
|
|
<span x-text="$store.torrents.statusText($store.torrents.currentJob)"></span>
|
|
<span x-text="$store.torrents.progressValue($store.torrents.currentJob).toFixed(1) + '%'"></span>
|
|
</div>
|
|
<div class="torrent-progress-track">
|
|
<div class="torrent-progress-bar"
|
|
:style="'width:' + $store.torrents.progressValue($store.torrents.currentJob) + '%'"></div>
|
|
</div>
|
|
<div class="torrent-progress-details"
|
|
:class="{ completed: $store.torrents.isCompleted($store.torrents.currentJob) }">
|
|
<span class="torrent-progress-metric">
|
|
<span class="torrent-progress-label"
|
|
x-text="$store.torrents.isCompleted($store.torrents.currentJob) ? T.size : T.downloaded"></span>
|
|
<span class="torrent-progress-value"
|
|
x-text="$store.torrents.progressDetailText($store.torrents.currentJob)"></span>
|
|
</span>
|
|
<span class="torrent-progress-metric"
|
|
x-show="!$store.torrents.isCompleted($store.torrents.currentJob)">
|
|
<span class="torrent-progress-label" x-text="T.speed"></span>
|
|
<span class="torrent-progress-value"
|
|
x-text="$store.torrents.speedText($store.torrents.currentJob)"></span>
|
|
</span>
|
|
<span class="torrent-progress-metric"
|
|
x-show="!$store.torrents.isCompleted($store.torrents.currentJob)">
|
|
<span class="torrent-progress-label" x-text="T.peers"></span>
|
|
<span class="torrent-progress-value"
|
|
x-text="$store.torrents.peerText($store.torrents.currentJob)"></span>
|
|
</span>
|
|
<span class="torrent-progress-metric"
|
|
x-show="!$store.torrents.isCompleted($store.torrents.currentJob) && $store.torrents.etaText($store.torrents.currentJob)">
|
|
<span class="torrent-progress-label" x-text="T.eta"></span>
|
|
<span class="torrent-progress-value"
|
|
x-text="$store.torrents.etaText($store.torrents.currentJob)"></span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template x-if="$store.torrents.previewData">
|
|
<div class="torrent-preview-panel">
|
|
<div class="torrent-preview-head">
|
|
<div style="min-width:0">
|
|
<div class="torrent-preview-title" x-text="$store.torrents.previewData.name"></div>
|
|
<div class="torrent-preview-meta"
|
|
x-text="$store.torrents.previewData.files.length + ' {{ t.player_files_count }} - ' + $store.torrents.bytes($store.torrents.previewData.total_size)"></div>
|
|
</div>
|
|
<div class="torrent-preview-actions">
|
|
<button class="modal-btn"
|
|
:class="$store.torrents.actionButtonClass()"
|
|
@click="$store.torrents.toggleDownloadAction()"
|
|
:disabled="$store.torrents.actionButtonDisabled()">
|
|
<span x-text="$store.torrents.actionButtonText()"></span>
|
|
</button>
|
|
<button class="modal-btn modal-btn-danger"
|
|
@click="$store.torrents.removeSession($store.torrents.previewData.id)"
|
|
:disabled="$store.torrents.loading">
|
|
{{ t.player_delete }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="torrent-tree-toolbar">
|
|
<div class="torrent-selected-summary"
|
|
x-text="$store.torrents.selected.size + ' {{ t.player_selected }} - ' + $store.torrents.bytes($store.torrents.selectedBytes())"></div>
|
|
<div class="torrent-actions" style="margin-top:0">
|
|
<button class="modal-btn modal-btn-ghost" @click="$store.torrents.expandAll(true)">{{ t.player_expand_all }}</button>
|
|
<button class="modal-btn modal-btn-ghost" @click="$store.torrents.expandAll(false)">{{ t.player_collapse }}</button>
|
|
</div>
|
|
</div>
|
|
<div class="torrent-file-tree">
|
|
<template x-for="node in $store.torrents.visibleNodes()" :key="node.key">
|
|
<div class="torrent-tree-row" :style="'--indent:' + $store.torrents.rowIndent(node) + 'px'">
|
|
<button class="torrent-tree-toggle"
|
|
:class="{ expanded: $store.torrents.expanded.has(node.key) }"
|
|
@click="$store.torrents.toggleExpand(node)"
|
|
:style="node.type === 'folder' ? '' : 'visibility:hidden'">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polyline points="9 18 15 12 9 6"/>
|
|
</svg>
|
|
</button>
|
|
<button class="torrent-tree-check"
|
|
:class="$store.torrents.nodeCheckClass(node)"
|
|
@click="$store.torrents.toggleNode(node)">
|
|
<template x-if="$store.torrents.nodeState(node) === 'checked'">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
|
|
<polyline points="20 6 9 17 4 12"/>
|
|
</svg>
|
|
</template>
|
|
<template x-if="$store.torrents.nodeState(node) === 'partial'">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
|
|
<line x1="5" y1="12" x2="19" y2="12"/>
|
|
</svg>
|
|
</template>
|
|
</button>
|
|
<div class="torrent-tree-label" :title="node.name">
|
|
<template x-if="node.type === 'folder'">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M3 7a2 2 0 012-2h5l2 2h7a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"/>
|
|
</svg>
|
|
</template>
|
|
<template x-if="node.type === 'file'">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/>
|
|
<polyline points="14 2 14 8 20 8"/>
|
|
</svg>
|
|
</template>
|
|
<span class="torrent-file-name" x-text="node.name"></span>
|
|
</div>
|
|
<span class="torrent-file-size" x-text="$store.torrents.bytes(node.size)"></span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</section>
|
|
</div>
|
|
</template>
|
|
|
|
<template x-if="$store.torrents.activeTab === 'uploads'">
|
|
<section class="upload-manager-panel">
|
|
<div class="upload-manager-head">
|
|
<div>
|
|
<h4>{{ t.player_my_uploaded_tracks }}</h4>
|
|
<p x-text="$store.torrents.uploadSummary()"></p>
|
|
</div>
|
|
<button class="modal-btn modal-btn-ghost"
|
|
@click="$store.torrents.loadUploads()"
|
|
:disabled="$store.torrents.uploadHasEditorOpen()">{{ t.player_refresh }}</button>
|
|
</div>
|
|
|
|
<template x-if="$store.torrents.uploadLoaded && $store.torrents.uploadTracks.length === 0 && $store.torrents.uploadPending.length === 0 && $store.torrents.uploadQueued.length === 0">
|
|
<div class="empty-state torrent-workspace-empty">
|
|
<p>{{ t.player_no_uploaded_tracks }}</p>
|
|
</div>
|
|
</template>
|
|
|
|
<div class="upload-manager-grid">
|
|
<aside class="upload-review-column">
|
|
<div class="upload-panel-card">
|
|
<div class="upload-panel-title">{{ t.player_needs_approval }}</div>
|
|
<p class="upload-panel-subtitle" x-text="$store.torrents.uploadPendingTotal + ' {{ t.player_pending_or_failed }}'"></p>
|
|
<template x-if="$store.torrents.uploadPending.length === 0">
|
|
<div class="upload-mini-empty">{{ t.player_no_tracks_need_approval }}</div>
|
|
</template>
|
|
<div class="upload-review-list">
|
|
<template x-for="item in $store.torrents.uploadPending" :key="item.id">
|
|
<button class="upload-review-row" :class="{ active: $store.torrents.uploadReviewEditId === item.id, failed: item.status === 'failed' }" @click="$store.torrents.editUploadReview(item)">
|
|
<span class="torrent-status-badge" :class="'status-' + item.status" x-text="$store.torrents.uploadStatusLabel(item.status)"></span>
|
|
<span class="upload-review-name" x-text="item.filename"></span>
|
|
<span class="upload-review-error" x-show="item.error_message" x-text="item.error_message"></span>
|
|
</button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
<template x-if="$store.torrents.uploadQueuedTotal > 0">
|
|
<div class="upload-panel-card upload-queue-panel">
|
|
<div class="upload-panel-title upload-panel-title-row">
|
|
<span>{{ t.player_queued_processing }}</span>
|
|
<div class="upload-queue-nav" x-show="$store.torrents.uploadQueued.length > $store.torrents.uploadQueuePageSize">
|
|
<span class="upload-queue-range" x-text="$store.torrents.uploadQueueRangeText()"></span>
|
|
<button type="button"
|
|
class="upload-queue-nav-btn"
|
|
@click="$store.torrents.uploadQueuePrev()"
|
|
:disabled="!$store.torrents.uploadQueueCanPrev()"
|
|
title="{{ t.player_previous }}">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4"><polyline points="15 18 9 12 15 6"/></svg>
|
|
</button>
|
|
<button type="button"
|
|
class="upload-queue-nav-btn"
|
|
@click="$store.torrents.uploadQueueNext()"
|
|
:disabled="!$store.torrents.uploadQueueCanNext()"
|
|
title="{{ t.player_next }}">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4"><polyline points="9 18 15 12 9 6"/></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<template x-for="item in $store.torrents.compactQueuedUploads()" :key="item.id">
|
|
<div class="upload-queue-row" :title="$store.torrents.uploadQueueTooltip(item)">
|
|
<span class="torrent-status-badge" :class="'status-' + item.status" x-text="$store.torrents.uploadStatusLabel(item.status)"></span>
|
|
<span class="upload-queue-name" x-text="item.filename"></span>
|
|
</div>
|
|
</template>
|
|
<div class="upload-mini-empty" x-show="$store.torrents.uploadQueuedTotal > $store.torrents.uploadQueued.length" x-text="$store.torrents.uploadQueueOverflowText()"></div>
|
|
</div>
|
|
</template>
|
|
</aside>
|
|
<section class="upload-library-column">
|
|
<div class="upload-bulk-bar" x-show="$store.torrents.uploadSelectedCount() > 0">
|
|
<div class="upload-bulk-title" x-text="$store.torrents.uploadSelectedCount() + ' {{ t.player_selected }}'"></div>
|
|
<input type="text" placeholder="{{ t.player_artists }}" x-model="$store.torrents.uploadBulkDraft.artists">
|
|
<input type="text" placeholder="{{ t.player_featured }}" x-model="$store.torrents.uploadBulkDraft.featured_artists">
|
|
<input type="text" placeholder="{{ t.player_album }}" x-model="$store.torrents.uploadBulkDraft.release_title">
|
|
<input type="number" placeholder="{{ t.player_year }}" x-model="$store.torrents.uploadBulkDraft.release_year">
|
|
<select x-model="$store.torrents.uploadBulkDraft.release_type">
|
|
<option value="">{{ t.player_type_unchanged }}</option><option value="album">{{ t.player_release_type_album }}</option><option value="single">{{ t.player_release_type_single }}</option><option value="ep">{{ t.player_release_type_ep }}</option><option value="compilation">{{ t.player_release_type_compilation }}</option><option value="mixtape">{{ t.player_release_type_mixtape }}</option><option value="live">{{ t.player_release_type_live }}</option><option value="soundtrack">{{ t.player_release_type_soundtrack }}</option><option value="remix">{{ t.player_release_type_remix }}</option><option value="demo">{{ t.player_release_type_demo }}</option>
|
|
</select>
|
|
<select x-model="$store.torrents.uploadBulkDraft.hidden">
|
|
<option value="">{{ t.player_visibility_unchanged }}</option><option value="false">{{ t.player_visible }}</option><option value="true">{{ t.player_hidden }}</option>
|
|
</select>
|
|
<button class="modal-btn modal-btn-primary" @click="$store.torrents.saveUploadBulkEdit()" :disabled="$store.torrents.uploadBulkSaving">{{ t.player_apply }}</button>
|
|
<button class="modal-btn modal-btn-ghost" @click="$store.torrents.clearUploadSelection()">{{ t.player_clear }}</button>
|
|
</div>
|
|
<div class="upload-release-tree">
|
|
<template x-for="group in $store.torrents.uploadArtistGroups()" :key="group.key">
|
|
<section class="upload-artist-group">
|
|
<div class="upload-artist-row">
|
|
<div class="upload-artist-name" x-text="group.name"></div>
|
|
<div class="upload-artist-meta" x-text="group.releases.length + ' {{ t.player_releases_count }} - ' + group.trackCount + ' {{ t.player_tracks_count }}'"></div>
|
|
</div>
|
|
<template x-for="release in group.releases" :key="release.id">
|
|
<div class="upload-release-node" :class="{ hidden: release.is_hidden }">
|
|
<div class="upload-release-row">
|
|
<button class="torrent-tree-check" :class="$store.torrents.uploadReleaseSelectionState(release)" @click="$store.torrents.toggleUploadReleaseSelection(release)">
|
|
<template x-if="$store.torrents.uploadReleaseSelectionState(release) === 'checked'"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg></template>
|
|
<template x-if="$store.torrents.uploadReleaseSelectionState(release) === 'partial'"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><line x1="5" y1="12" x2="19" y2="12"/></svg></template>
|
|
</button>
|
|
<button class="torrent-tree-toggle" :class="{ expanded: $store.torrents.uploadReleaseExpanded(release.id) }" @click="$store.torrents.toggleUploadRelease(release.id)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg></button>
|
|
<div class="upload-release-main">
|
|
<div class="upload-release-title"><span x-text="release.title"></span><span class="upload-hidden-pill" x-show="release.is_hidden">{{ t.player_hidden }}</span></div>
|
|
<div class="upload-track-meta"><span x-text="$store.torrents.uploadReleaseArtistsText(release)"></span><span>-</span><span x-text="release.year || T.noYear"></span><span>-</span><span x-text="release.tracks.length + ' {{ t.player_tracks_count }}'"></span></div>
|
|
</div>
|
|
<button class="modal-btn modal-btn-ghost" @click="$store.torrents.editUploadRelease(release)">{{ t.player_edit_release }}</button>
|
|
</div>
|
|
<div class="upload-track-children" x-show="$store.torrents.uploadReleaseExpanded(release.id)">
|
|
<template x-for="item in release.tracks" :key="item.track.id">
|
|
<div class="upload-tree-track" :class="{ hidden: item.is_hidden, selected: $store.torrents.selectedUploadTracks.has(item.track.id) }">
|
|
<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>
|
|
<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">{{ t.player_hidden }}</span></div>
|
|
<div class="upload-track-meta"><span x-text="$store.torrents.uploadArtistsText(item)"></span><span x-show="$store.torrents.uploadFeaturedArtistsText(item)" x-text="T.featuredShort"></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 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)">{{ t.player_edit }}</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</section>
|
|
</template>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<template x-if="$store.torrents.uploadHasEditorOpen()">
|
|
<div class="upload-editor-backdrop" @click.self="$store.torrents.closeUploadEditor()">
|
|
<aside class="upload-editor-drawer">
|
|
<div class="upload-editor-head">
|
|
<div>
|
|
<div class="upload-panel-title" x-text="$store.torrents.uploadEditorKicker()"></div>
|
|
<h4 x-text="$store.torrents.uploadEditorTitle()"></h4>
|
|
</div>
|
|
<button class="track-action-btn" @click="$store.torrents.closeUploadEditor()" title="{{ t.player_close }}">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4">
|
|
<path d="M18 6L6 18M6 6l12 12"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<template x-if="$store.torrents.uploadReviewDraft">
|
|
<div class="upload-editor-form">
|
|
<label class="upload-field upload-field-half"><span>{{ t.player_title }}</span><input type="text" x-model="$store.torrents.uploadReviewDraft.title"></label>
|
|
<label class="upload-field upload-field-half"><span>{{ t.player_artist }}</span><input type="text" x-model="$store.torrents.uploadReviewDraft.artist"></label>
|
|
<label class="upload-field upload-field-half"><span>{{ t.player_album }}</span><input type="text" x-model="$store.torrents.uploadReviewDraft.album"></label>
|
|
<label class="upload-field upload-field-half"><span>{{ t.player_featured }}</span><input type="text" x-model="$store.torrents.uploadReviewDraft.featured_artists" placeholder="{{ t.player_artists_placeholder }}"></label>
|
|
<label class="upload-field upload-field-compact"><span>{{ t.player_year }}</span><input type="number" x-model="$store.torrents.uploadReviewDraft.year"></label>
|
|
<label class="upload-field upload-field-compact"><span>{{ t.player_track_number }}</span><input type="number" min="1" x-model="$store.torrents.uploadReviewDraft.track_number"></label>
|
|
<label class="upload-field upload-field-compact"><span>{{ t.player_genre }}</span><input type="text" x-model="$store.torrents.uploadReviewDraft.genre"></label>
|
|
<label class="upload-field upload-field-compact">
|
|
<span>{{ t.player_type }}</span>
|
|
<select x-model="$store.torrents.uploadReviewDraft.release_type">
|
|
<option value="album">{{ t.player_release_type_album }}</option><option value="single">{{ t.player_release_type_single }}</option><option value="ep">{{ t.player_release_type_ep }}</option><option value="compilation">{{ t.player_release_type_compilation }}</option><option value="mixtape">{{ t.player_release_type_mixtape }}</option><option value="live">{{ t.player_release_type_live }}</option><option value="soundtrack">{{ t.player_release_type_soundtrack }}</option><option value="remix">{{ t.player_release_type_remix }}</option><option value="demo">{{ t.player_release_type_demo }}</option>
|
|
</select>
|
|
</label>
|
|
<label class="upload-field upload-field-wide"><span>{{ t.player_notes }}</span><textarea rows="4" x-model="$store.torrents.uploadReviewDraft.notes"></textarea></label>
|
|
<div class="upload-editor-actions">
|
|
<button class="modal-btn modal-btn-danger" @click="$store.torrents.deleteUploadReview()" :disabled="$store.torrents.uploadReviewSavingId === $store.torrents.uploadReviewEditId">{{ t.player_delete_review }}</button>
|
|
<button class="modal-btn modal-btn-primary" @click="$store.torrents.approveUploadReview()" :disabled="$store.torrents.uploadReviewSavingId === $store.torrents.uploadReviewEditId">{{ t.player_approve }}</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template x-if="$store.torrents.uploadReleaseDraft">
|
|
<div class="upload-editor-form">
|
|
<label class="upload-field upload-field-wide"><span>{{ t.player_album }}</span><input type="text" x-model="$store.torrents.uploadReleaseDraft.title"></label>
|
|
<label class="upload-field upload-field-wide"><span>{{ t.player_album_artists }}</span><input type="text" x-model="$store.torrents.uploadReleaseDraft.artists"></label>
|
|
<label class="upload-field upload-field-half"><span>{{ t.player_year }}</span><input type="number" x-model="$store.torrents.uploadReleaseDraft.year"></label>
|
|
<label class="upload-field upload-field-half"><span>{{ t.player_type }}</span><select x-model="$store.torrents.uploadReleaseDraft.release_type"><option value="album">{{ t.player_release_type_album }}</option><option value="single">{{ t.player_release_type_single }}</option><option value="ep">{{ t.player_release_type_ep }}</option><option value="compilation">{{ t.player_release_type_compilation }}</option><option value="mixtape">{{ t.player_release_type_mixtape }}</option><option value="live">{{ t.player_release_type_live }}</option><option value="soundtrack">{{ t.player_release_type_soundtrack }}</option><option value="remix">{{ t.player_release_type_remix }}</option><option value="demo">{{ t.player_release_type_demo }}</option></select></label>
|
|
<label class="upload-field upload-field-toggle"><input type="checkbox" x-model="$store.torrents.uploadReleaseDraft.is_hidden"><span>{{ t.player_hidden }}</span></label>
|
|
<div class="upload-editor-actions">
|
|
<button class="modal-btn modal-btn-primary" @click="$store.torrents.saveUploadReleaseEdit()" :disabled="$store.torrents.uploadReleaseSavingId === $store.torrents.uploadReleaseEditId">{{ t.player_save_release }}</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template x-if="$store.torrents.uploadDraft">
|
|
<div class="upload-editor-form">
|
|
<label class="upload-field upload-field-wide"><span>{{ t.player_title }}</span><input type="text" x-model="$store.torrents.uploadDraft.title"></label>
|
|
<label class="upload-field upload-field-half"><span>{{ t.player_artists }}</span><input type="text" x-model="$store.torrents.uploadDraft.artists"></label>
|
|
<label class="upload-field upload-field-half"><span>{{ t.player_featured }}</span><input type="text" x-model="$store.torrents.uploadDraft.featured_artists"></label>
|
|
<label class="upload-field upload-field-half"><span>{{ t.player_album }}</span><input type="text" x-model="$store.torrents.uploadDraft.release_title"></label>
|
|
<label class="upload-field upload-field-half"><span>{{ t.player_type }}</span><select x-model="$store.torrents.uploadDraft.release_type"><option value="album">{{ t.player_release_type_album }}</option><option value="single">{{ t.player_release_type_single }}</option><option value="ep">{{ t.player_release_type_ep }}</option><option value="compilation">{{ t.player_release_type_compilation }}</option><option value="mixtape">{{ t.player_release_type_mixtape }}</option><option value="live">{{ t.player_release_type_live }}</option><option value="soundtrack">{{ t.player_release_type_soundtrack }}</option><option value="remix">{{ t.player_release_type_remix }}</option><option value="demo">{{ t.player_release_type_demo }}</option></select></label>
|
|
<label class="upload-field upload-field-compact"><span>{{ t.player_year }}</span><input type="number" x-model="$store.torrents.uploadDraft.release_year"></label>
|
|
<label class="upload-field upload-field-compact"><span>{{ t.player_track_number }}</span><input type="number" min="1" x-model="$store.torrents.uploadDraft.track_number"></label>
|
|
<label class="upload-field upload-field-compact"><span>{{ t.player_disc_number }}</span><input type="number" min="1" x-model="$store.torrents.uploadDraft.disc_number"></label>
|
|
<label class="upload-field upload-field-toggle"><input type="checkbox" x-model="$store.torrents.uploadDraft.is_hidden"><span>{{ t.player_hidden }}</span></label>
|
|
<div class="upload-editor-actions">
|
|
<button class="modal-btn modal-btn-primary" @click="$store.torrents.saveUploadEdit()" :disabled="$store.torrents.uploadSavingId === $store.torrents.uploadEditId">{{ t.player_save_track }}</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</aside>
|
|
</div>
|
|
</template>
|
|
|
|
<div class="upload-track-list" x-show="false">
|
|
<template x-for="item in $store.torrents.uploadTracks" :key="item.track.id">
|
|
<div class="upload-track-card" :class="{ hidden: item.is_hidden }">
|
|
<template x-if="$store.torrents.uploadEditId !== item.track.id">
|
|
<div class="upload-track-display">
|
|
<div class="upload-track-main">
|
|
<div class="upload-track-title">
|
|
<span x-text="item.track.title"></span>
|
|
<span class="upload-hidden-pill" x-show="item.is_hidden">{{ t.player_hidden }}</span>
|
|
</div>
|
|
<div class="upload-track-meta">
|
|
<span x-text="$store.torrents.uploadArtistsText(item)"></span>
|
|
<span>·</span>
|
|
<span x-text="item.track.release_title"></span>
|
|
<span x-show="item.track.track_number">·</span>
|
|
<span x-show="item.track.track_number" x-text="'#' + item.track.track_number"></span>
|
|
</div>
|
|
</div>
|
|
<div class="upload-track-actions">
|
|
<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)">{{ t.player_edit }}</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template x-if="$store.torrents.uploadEditId === item.track.id">
|
|
<div class="upload-edit-form">
|
|
<label>
|
|
<span>{{ t.player_title }}</span>
|
|
<input type="text" x-model="$store.torrents.uploadDraft.title">
|
|
</label>
|
|
<label>
|
|
<span>{{ t.player_artists }}</span>
|
|
<input type="text" x-model="$store.torrents.uploadDraft.artists" placeholder="{{ t.player_artist_featured_placeholder }}">
|
|
</label>
|
|
<label>
|
|
<span>{{ t.player_release }}</span>
|
|
<input type="text" x-model="$store.torrents.uploadDraft.release_title">
|
|
</label>
|
|
<label>
|
|
<span>{{ t.player_type }}</span>
|
|
<select x-model="$store.torrents.uploadDraft.release_type">
|
|
<option value="album">{{ t.player_release_type_album }}</option>
|
|
<option value="single">{{ t.player_release_type_single }}</option>
|
|
<option value="ep">{{ t.player_release_type_ep }}</option>
|
|
<option value="compilation">{{ t.player_release_type_compilation }}</option>
|
|
<option value="mixtape">{{ t.player_release_type_mixtape }}</option>
|
|
<option value="live">{{ t.player_release_type_live }}</option>
|
|
<option value="soundtrack">{{ t.player_release_type_soundtrack }}</option>
|
|
<option value="remix">{{ t.player_release_type_remix }}</option>
|
|
<option value="demo">{{ t.player_release_type_demo }}</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
<span>{{ t.player_year }}</span>
|
|
<input type="number" x-model="$store.torrents.uploadDraft.release_year">
|
|
</label>
|
|
<label>
|
|
<span>{{ t.player_track_number }}</span>
|
|
<input type="number" min="1" x-model="$store.torrents.uploadDraft.track_number">
|
|
</label>
|
|
<label>
|
|
<span>{{ t.player_disc_number }}</span>
|
|
<input type="number" min="1" x-model="$store.torrents.uploadDraft.disc_number">
|
|
</label>
|
|
<label class="upload-hidden-toggle">
|
|
<input type="checkbox" x-model="$store.torrents.uploadDraft.is_hidden">
|
|
<span>{{ t.player_hidden }}</span>
|
|
</label>
|
|
<div class="upload-edit-actions">
|
|
<button class="modal-btn modal-btn-primary"
|
|
@click="$store.torrents.saveUploadEdit()"
|
|
:disabled="$store.torrents.uploadSavingId === item.track.id">{{ t.player_save }}</button>
|
|
<button class="modal-btn modal-btn-ghost" @click="$store.torrents.cancelUploadEdit()">{{ t.player_cancel }}</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Play History Modal -->
|
|
<template x-if="$store.history.modal">
|
|
<div class="modal-overlay" @click.self="$store.history.close()">
|
|
<div class="modal-box history-modal">
|
|
<div class="history-head">
|
|
<div>
|
|
<h3>{{ t.player_play_history }}</h3>
|
|
<p class="torrent-message" :class="{ error: $store.history.error }"
|
|
x-text="$store.history.message"></p>
|
|
</div>
|
|
<button class="mobile-list-action" @click="$store.history.close()" 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="history-list">
|
|
<template x-if="!$store.history.loading && $store.history.items.length === 0">
|
|
<div class="empty-state" style="padding:32px 16px">
|
|
<p>{{ t.player_no_plays_yet }}</p>
|
|
</div>
|
|
</template>
|
|
<template x-if="$store.history.items.length > 0">
|
|
<div class="history-table-head">
|
|
<span></span>
|
|
<span>{{ t.player_title }}</span>
|
|
<span>{{ t.player_played_at }}</span>
|
|
<span></span>
|
|
<span style="text-align:right">{{ t.player_listened }}</span>
|
|
</div>
|
|
</template>
|
|
<template x-for="(item, idx) in $store.history.items" :key="item.id">
|
|
<div class="history-row track-row"
|
|
:class="{ playing: $store.player.currentTrack && item.track && $store.player.currentTrack.id === item.track.id }"
|
|
@dblclick="$store.history.playFrom(idx)">
|
|
<button class="history-cover"
|
|
@click.stop="$store.history.playFrom(idx)"
|
|
:title="item.track?.title || item.track_title">
|
|
<template x-if="item.track && item.track.cover_url">
|
|
<img :src="item.track.cover_url" :alt="item.track.title" loading="lazy">
|
|
</template>
|
|
<template x-if="!item.track || !item.track.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>
|
|
<div class="track-info">
|
|
<div class="track-title" x-text="item.track?.title || item.track_title"></div>
|
|
<div class="track-artists-inline">
|
|
<template x-for="(artist, artistIdx) in $store.library.trackArtistLinks(item.track)" :key="artist.label + '-' + artist.id + '-' + artistIdx">
|
|
<span>
|
|
<template x-if="artistIdx > 0"><span>, </span></template>
|
|
<a class="artist-link" @click.stop="$store.history.close(); $store.library.openArtist(artist.id)" x-text="artist.label"></a>
|
|
</span>
|
|
</template>
|
|
<template x-if="!item.track || !$store.library.trackArtistLinks(item.track).length">
|
|
<span x-text="item.release_title || '{{ t.player_unknown_release }}'"></span>
|
|
</template>
|
|
</div>
|
|
<div class="history-release-line">
|
|
<a class="artist-link"
|
|
x-show="item.track && item.track.release_id"
|
|
@click.stop="$store.history.close(); $store.library.openRelease(item.track.release_id)"
|
|
x-text="item.track?.release_title || item.release_title || '{{ t.player_unknown_release }}'"></a>
|
|
</div>
|
|
</div>
|
|
<div class="history-date" x-text="$store.history.date(item.played_at)"></div>
|
|
<div class="track-actions">
|
|
<button class="track-action-btn info-btn popularity-info-btn"
|
|
:class="{ 'has-popularity': $store.library.hasPopularity(item.track), 'no-popularity': !$store.library.hasPopularity(item.track) }"
|
|
:style="$store.library.popularityStyle(item.track)"
|
|
@click.stop="$store.library.openTrackInfo(item.track)"
|
|
:title="$store.library.trackInfoTitle(item.track)"
|
|
aria-label="{{ t.player_track_info }}">
|
|
<span x-show="$store.library.hasPopularity(item.track)" x-text="$store.library.popularityLabel(item.track)"></span>
|
|
<span x-show="!$store.library.hasPopularity(item.track)" class="info-letter">i</span>
|
|
</button>
|
|
<button class="like-btn" :class="{ liked: $store.likes.has(item.track_id) }" @click.stop="$store.likes.toggle(item.track_id)" title="{{ t.player_like }}">
|
|
<svg viewBox="0 0 24 24" :fill="$store.likes.has(item.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([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.stop="$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="track-action-btn track-share-btn" @click.stop="$store.sharing.copyTrack(item.track || item.track_id, $event.currentTarget)" title="{{ t.player_share_track }}" aria-label="{{ t.player_share_track }}">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="M8.6 10.6l6.8-3.9M8.6 13.4l6.8 3.9"/></svg>
|
|
</button>
|
|
<button class="track-action-btn playlist-add-btn" @click.stop="$store.playlists.showPicker([item.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="history-duration" x-text="$store.history.duration(item.duration_listened)"></span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
<div class="history-pager">
|
|
<button class="modal-btn modal-btn-ghost"
|
|
@click="$store.history.load($store.history.page - 1)"
|
|
:disabled="$store.history.loading || $store.history.page <= 1">
|
|
{{ t.player_previous }}
|
|
</button>
|
|
<span class="history-release"
|
|
x-text="'{{ t.player_page }} ' + $store.history.page + ' {{ t.player_of }} ' + $store.history.totalPages()"></span>
|
|
<button class="modal-btn modal-btn-primary"
|
|
@click="$store.history.load($store.history.page + 1)"
|
|
:disabled="$store.history.loading || $store.history.page >= $store.history.totalPages()">
|
|
{{ t.player_next }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|