feat: P7 在线播放(播放队列、媒体控制)与自建歌单
- 播放器:统一 PlayItem 队列,顺序 / 列表循环 / 单曲循环 / 随机,滚动歌词含翻译,Media Session,队列与进度存 localStorage - 在线播放音质可在设置中选择;曲库已有的歌优先播放本地文件 - 自建歌单:混排在线曲目与本地文件,拖动排序,一键下载曲库缺失曲目 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import type { QualityKey, TrackSummary } from '../lib/api.ts';
|
||||
import { bestQuality, formatDuration, isGold, QUALITY_BADGE, STAGE_LABEL } from '../lib/format.ts';
|
||||
import { isPlaying, player, playOnline } from '../lib/player.ts';
|
||||
import { isPlaying, player, playTracks } from '../lib/player.ts';
|
||||
import { inputFromTrack, pickPlaylist } from '../lib/playlists.ts';
|
||||
import { activeByTrack, defaultQualityFor, download, isDownloaded, progress, trackKey } from '../lib/store.ts';
|
||||
import CoverArt from './CoverArt.vue';
|
||||
import Icon from './Icon.vue';
|
||||
@@ -70,6 +71,17 @@ function downloadAll() {
|
||||
download(props.tracks, quality.value, props.group);
|
||||
}
|
||||
|
||||
const picked = () => (selected.value.size ? props.tracks.filter((t) => selected.value.has(t.id)) : props.tracks);
|
||||
|
||||
function playSelected() {
|
||||
playTracks(picked());
|
||||
}
|
||||
|
||||
/** group 形如「专辑 · 名称」,取名称作为新歌单的默认名 */
|
||||
function addSelected() {
|
||||
pickPlaylist(picked().map(inputFromTrack), props.group?.split(' · ').slice(1).join(' · ') ?? '');
|
||||
}
|
||||
|
||||
interface RowState {
|
||||
kind: string;
|
||||
text: string;
|
||||
@@ -115,6 +127,12 @@ const best = (t: TrackSummary) => bestQuality(t.qualities);
|
||||
</template>
|
||||
</span>
|
||||
<div class="spacer" />
|
||||
<button class="btn" type="button" :disabled="!tracks.length" :title="selected.size ? '播放所选' : '播放全部'" @click="playSelected">
|
||||
<Icon name="play" :size="14" /> {{ selected.size ? '播放所选' : '播放全部' }}
|
||||
</button>
|
||||
<button class="btn" type="button" :disabled="!tracks.length" :title="selected.size ? '所选加入歌单' : '全部加入歌单'" @click="addSelected">
|
||||
<Icon name="listPlus" :size="16" /> 加入歌单
|
||||
</button>
|
||||
<QualityPicker v-model="quality" :source="source" />
|
||||
<button v-if="selected.size" class="btn primary" type="button" @click="downloadSelected">
|
||||
<Icon name="download" :size="16" /> 下载所选
|
||||
@@ -130,18 +148,18 @@ const best = (t: TrackSummary) => bestQuality(t.qualities);
|
||||
:key="`${t.source}-${t.id}-${i}`"
|
||||
class="row"
|
||||
:class="[t.source, { selected: selected.has(t.id), current: player.current?.key === `${t.source}:${t.id}` }]"
|
||||
@dblclick="playOnline(t)"
|
||||
@dblclick="playTracks(tracks, i)"
|
||||
>
|
||||
<button type="button" class="check" :class="{ on: selected.has(t.id) }" @click="toggleRow(i, $event)" :aria-label="`选择 ${t.name}`">
|
||||
<Icon v-if="selected.has(t.id)" name="check" :size="13" />
|
||||
</button>
|
||||
<span class="num">
|
||||
<span class="mono faint idx">{{ number(t, i) }}</span>
|
||||
<button type="button" class="play" @click="playOnline(t)" :aria-label="`试听 ${t.name}`">
|
||||
<button type="button" class="play" @click="playTracks(tracks, i)" :aria-label="`播放 ${t.name}`">
|
||||
<Icon :name="isPlaying(`${t.source}:${t.id}`) ? 'pause' : 'play'" :size="16" />
|
||||
</button>
|
||||
</span>
|
||||
<CoverArt v-if="showCover" :src="t.cover" :size="40" />
|
||||
<button v-if="showCover" type="button" class="cover-btn" tabindex="-1" @click="playTracks(tracks, i)"><CoverArt :src="t.cover" :size="40" /></button>
|
||||
<div class="title">
|
||||
<div class="name">
|
||||
{{ t.name }}
|
||||
@@ -175,9 +193,14 @@ const best = (t: TrackSummary) => bestQuality(t.qualities);
|
||||
</template>
|
||||
</span>
|
||||
<span class="dur mono faint">{{ formatDuration(t.duration) }}</span>
|
||||
<button type="button" class="dl" title="下载" @click="download([t], quality, group)">
|
||||
<Icon name="download" :size="17" />
|
||||
</button>
|
||||
<span class="acts">
|
||||
<button type="button" class="dl add" title="加入歌单" @click="pickPlaylist([inputFromTrack(t)])">
|
||||
<Icon name="listPlus" :size="17" />
|
||||
</button>
|
||||
<button type="button" class="dl" title="下载" @click="download([t], quality, group)">
|
||||
<Icon name="download" :size="17" />
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
@@ -254,11 +277,14 @@ const best = (t: TrackSummary) => bestQuality(t.qualities);
|
||||
.row {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 18px 30px auto minmax(0, 1fr) auto 96px 44px 32px;
|
||||
grid-template-columns: 18px 30px auto minmax(0, 1fr) auto 96px 44px 64px;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 9px 10px 9px 12px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
/* 长列表只渲染视口附近的行 */
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: auto 59px;
|
||||
transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
|
||||
}
|
||||
|
||||
@@ -295,7 +321,13 @@ const best = (t: TrackSummary) => bestQuality(t.qualities);
|
||||
}
|
||||
|
||||
.no-cover .row {
|
||||
grid-template-columns: 18px 30px minmax(0, 1fr) auto 96px 44px 32px;
|
||||
grid-template-columns: 18px 30px minmax(0, 1fr) auto 96px 44px 64px;
|
||||
}
|
||||
|
||||
.cover-btn {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.num {
|
||||
@@ -451,6 +483,21 @@ const best = (t: TrackSummary) => bestQuality(t.qualities);
|
||||
background: var(--accent-soft);
|
||||
}
|
||||
|
||||
.acts {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.add {
|
||||
opacity: 0;
|
||||
transition: color var(--dur) var(--ease), background var(--dur) var(--ease), opacity var(--dur) var(--ease);
|
||||
}
|
||||
|
||||
.row:hover .add,
|
||||
.add:focus-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.toolbar {
|
||||
flex-wrap: wrap;
|
||||
@@ -467,7 +514,8 @@ const best = (t: TrackSummary) => bestQuality(t.qualities);
|
||||
|
||||
.num,
|
||||
.badges,
|
||||
.dur {
|
||||
.dur,
|
||||
.add {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user