2026-05-26 12:55:11 +03:00
|
|
|
<!-- 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>
|
2026-05-26 14:47:10 +03:00
|
|
|
<button class="mobile-list-action" @click="$store.info.close()" title="{{ t.player_close }}">
|
2026-05-26 12:55:11 +03:00
|
|
|
<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>
|
|
|
|
|
<pre class="info-modal-body" x-text="$store.info.modal.body"></pre>
|
|
|
|
|
</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">
|
2026-05-26 14:47:10 +03:00
|
|
|
<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 }}"
|
2026-05-26 12:55:11 +03:00
|
|
|
@keydown.enter="$store.playlists.submitModal()" x-init="$nextTick(() => $el.focus())">
|
|
|
|
|
<div class="modal-footer">
|
2026-05-26 14:47:10 +03:00
|
|
|
<button class="modal-btn modal-btn-ghost" @click="$store.playlists.modal = null">{{ t.player_cancel }}</button>
|
2026-05-26 12:55:11 +03:00
|
|
|
<button class="modal-btn modal-btn-primary" @click="$store.playlists.submitModal()"
|
2026-05-26 14:47:10 +03:00
|
|
|
x-text="$store.playlists.modal.mode === 'create' ? '{{ t.player_create }}' : '{{ t.player_save }}'"></button>
|
2026-05-26 12:55:11 +03:00
|
|
|
</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">
|
2026-05-26 14:47:10 +03:00
|
|
|
<h3>{{ t.player_add_to_playlist }}</h3>
|
2026-05-26 12:55:11 +03:00
|
|
|
<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">
|
2026-05-26 14:47:10 +03:00
|
|
|
<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>
|
2026-05-26 12:55:11 +03:00
|
|
|
</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>
|
2026-05-26 14:47:10 +03:00
|
|
|
<h3>{{ t.player_torrent_manager }}</h3>
|
2026-05-26 12:55:11 +03:00
|
|
|
<p class="torrent-message" style="margin:4px 0 0"
|
|
|
|
|
:class="{ error: $store.torrents.error }"
|
|
|
|
|
x-text="$store.torrents.message"></p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="torrent-client-status">
|
|
|
|
|
<span class="torrent-status-pill"
|
|
|
|
|
:class="{ active: $store.torrents.activeCount() > 0 }"
|
|
|
|
|
x-text="$store.torrents.clientSummary()"></span>
|
2026-05-26 14:47:10 +03:00
|
|
|
<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>
|
2026-05-26 12:55:11 +03:00
|
|
|
<span class="torrent-status-pill"
|
2026-05-26 16:21:21 +03:00
|
|
|
x-text="$store.torrents.sessions.length + ' ' + T.saved"></span>
|
2026-05-26 12:55:11 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="torrent-manager-layout">
|
|
|
|
|
<aside class="torrent-manager-sidebar">
|
|
|
|
|
<div class="torrent-manager-title">
|
2026-05-26 14:47:10 +03:00
|
|
|
<span>{{ t.player_saved_torrents }}</span>
|
2026-05-26 12:55:11 +03:00
|
|
|
<button class="modal-btn modal-btn-ghost" style="padding:4px 8px"
|
|
|
|
|
@click="$store.torrents.loadSessions()"
|
2026-05-26 14:47:10 +03:00
|
|
|
:disabled="$store.torrents.loading">{{ t.player_refresh }}</button>
|
2026-05-26 12:55:11 +03:00
|
|
|
</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">
|
2026-05-26 14:47:10 +03:00
|
|
|
<p>{{ t.player_no_saved_torrents }}</p>
|
2026-05-26 12:55:11 +03:00
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<template x-for="job in $store.torrents.sessions" :key="job.id">
|
|
|
|
|
<div class="torrent-session-row"
|
2026-05-26 16:21:21 +03:00
|
|
|
:class="{ active: $store.torrents.workspaceMode === 'session' && $store.torrents.previewData && $store.torrents.previewData.id === job.id }"
|
2026-05-26 12:55:11 +03:00
|
|
|
@click="$store.torrents.openSession(job.id)">
|
2026-05-26 14:47:10 +03:00
|
|
|
<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>
|
2026-05-26 12:55:11 +03:00
|
|
|
<div class="torrent-session-meta" x-text="$store.torrents.sessionMeta(job)"></div>
|
2026-05-26 14:47:10 +03:00
|
|
|
<div class="torrent-session-progress">
|
|
|
|
|
<div class="torrent-session-progress-bar"
|
|
|
|
|
:style="'width:' + $store.torrents.progressValue(job) + '%'"></div>
|
|
|
|
|
</div>
|
2026-05-26 12:55:11 +03:00
|
|
|
</div>
|
|
|
|
|
<button class="torrent-session-remove"
|
2026-05-26 14:47:10 +03:00
|
|
|
@click.stop="$store.torrents.removeSession(job.id)">{{ t.player_delete }}</button>
|
2026-05-26 12:55:11 +03:00
|
|
|
</div>
|
|
|
|
|
</template>
|
2026-05-26 16:21:21 +03:00
|
|
|
<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_add_torrent }}</span>
|
|
|
|
|
</button>
|
2026-05-26 12:55:11 +03:00
|
|
|
</div>
|
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
<section class="torrent-workspace">
|
2026-05-26 16:21:21 +03:00
|
|
|
<template x-if="$store.torrents.workspaceMode === 'empty'">
|
|
|
|
|
<div class="empty-state torrent-workspace-empty">
|
|
|
|
|
<p x-text="T.chooseSavedOrAddTorrent"></p>
|
2026-05-26 12:55:11 +03:00
|
|
|
</div>
|
2026-05-26 16:21:21 +03:00
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<template x-if="$store.torrents.isImporting()">
|
|
|
|
|
<div class="torrent-import-panel">
|
|
|
|
|
<div class="torrent-modal-grid">
|
|
|
|
|
<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>
|
|
|
|
|
<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>
|
|
|
|
|
<div class="torrent-actions">
|
|
|
|
|
<button class="modal-btn modal-btn-primary" @click="$store.torrents.preview()" :disabled="$store.torrents.loading">
|
|
|
|
|
{{ t.player_preview_content }}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2026-05-26 12:55:11 +03:00
|
|
|
</div>
|
2026-05-26 16:21:21 +03:00
|
|
|
</template>
|
2026-05-26 12:55:11 +03:00
|
|
|
|
|
|
|
|
<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>
|
2026-05-26 14:47:10 +03:00
|
|
|
<span x-text="$store.torrents.progressValue($store.torrents.currentJob).toFixed(1) + '%'"></span>
|
2026-05-26 12:55:11 +03:00
|
|
|
</div>
|
|
|
|
|
<div class="torrent-progress-track">
|
|
|
|
|
<div class="torrent-progress-bar"
|
2026-05-26 14:47:10 +03:00
|
|
|
:style="'width:' + $store.torrents.progressValue($store.torrents.currentJob) + '%'"></div>
|
2026-05-26 12:55:11 +03:00
|
|
|
</div>
|
2026-05-26 16:21:21 +03:00
|
|
|
<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>
|
2026-05-26 12:55:11 +03:00
|
|
|
</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"
|
2026-05-26 14:47:10 +03:00
|
|
|
x-text="$store.torrents.previewData.files.length + ' {{ t.player_files_count }} - ' + $store.torrents.bytes($store.torrents.previewData.total_size)"></div>
|
2026-05-26 12:55:11 +03:00
|
|
|
</div>
|
2026-05-26 14:47:10 +03:00
|
|
|
<button class="modal-btn"
|
2026-05-26 16:21:21 +03:00
|
|
|
:class="$store.torrents.actionButtonClass()"
|
|
|
|
|
@click="$store.torrents.toggleDownloadAction()"
|
|
|
|
|
:disabled="$store.torrents.actionButtonDisabled()">
|
|
|
|
|
<span x-text="$store.torrents.actionButtonText()"></span>
|
2026-05-26 12:55:11 +03:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="torrent-tree-toolbar">
|
|
|
|
|
<div class="torrent-selected-summary"
|
2026-05-26 14:47:10 +03:00
|
|
|
x-text="$store.torrents.selected.size + ' {{ t.player_selected }} - ' + $store.torrents.bytes($store.torrents.selectedBytes())"></div>
|
2026-05-26 12:55:11 +03:00
|
|
|
<div class="torrent-actions" style="margin-top:0">
|
2026-05-26 14:47:10 +03:00
|
|
|
<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>
|
2026-05-26 12:55:11 +03:00
|
|
|
</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>
|
|
|
|
|
</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">
|
2026-05-26 14:47:10 +03:00
|
|
|
<h3>{{ t.player_play_history }}</h3>
|
2026-05-26 12:55:11 +03:00
|
|
|
<p class="torrent-message" :class="{ error: $store.history.error }"
|
|
|
|
|
x-text="$store.history.message"></p>
|
|
|
|
|
<div class="history-list">
|
|
|
|
|
<template x-if="!$store.history.loading && $store.history.items.length === 0">
|
|
|
|
|
<div class="empty-state" style="padding:32px 16px">
|
2026-05-26 14:47:10 +03:00
|
|
|
<p>{{ t.player_no_plays_yet }}</p>
|
2026-05-26 12:55:11 +03:00
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<template x-for="item in $store.history.items" :key="item.id">
|
|
|
|
|
<div class="history-row">
|
|
|
|
|
<div style="min-width:0">
|
|
|
|
|
<div class="history-title" x-text="item.track_title"></div>
|
2026-05-26 14:47:10 +03:00
|
|
|
<div class="history-release" x-text="item.release_title || '{{ t.player_unknown_release }}'"></div>
|
2026-05-26 12:55:11 +03:00
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div class="history-date" x-text="$store.history.date(item.played_at)"></div>
|
|
|
|
|
<div class="history-duration" x-text="$store.history.duration(item.duration_listened)"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</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">
|
2026-05-26 14:47:10 +03:00
|
|
|
{{ t.player_previous }}
|
2026-05-26 12:55:11 +03:00
|
|
|
</button>
|
|
|
|
|
<span class="history-release"
|
2026-05-26 14:47:10 +03:00
|
|
|
x-text="'{{ t.player_page }} ' + $store.history.page + ' {{ t.player_of }} ' + $store.history.totalPages()"></span>
|
2026-05-26 12:55:11 +03:00
|
|
|
<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()">
|
2026-05-26 14:47:10 +03:00
|
|
|
{{ t.player_next }}
|
2026-05-26 12:55:11 +03:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|