From ffd2b390f0cb57f866dbc69e2fff657ab32b7b3d Mon Sep 17 00:00:00 2001 From: DiheChen Date: Mon, 9 Nov 2026 21:31:18 +0800 Subject: [PATCH] feat: save songs to my playlists from the player, queue and history --- web/src/app/main.tsx | 3 +- web/src/features/history/history-page.tsx | 2 + web/src/features/playback/playback.css | 2 +- web/src/features/playback/player-bar.tsx | 18 +- .../features/playlists/account-settings.ts | 10 ++ .../playlists/create-playlist-form.tsx | 41 +++++ web/src/features/playlists/index.ts | 3 + web/src/features/playlists/playlist-cover.tsx | 26 +++ .../features/playlists/playlist-sort.test.ts | 69 ++++++++ web/src/features/playlists/playlist-sort.ts | 35 ++++ web/src/features/playlists/playlists.css | 24 +++ web/src/features/playlists/save-label.ts | 5 + .../features/playlists/save-to-playlist.tsx | 165 ++++++++++++++++++ web/src/features/queue/queue-page.tsx | 30 ++-- 14 files changed, 419 insertions(+), 14 deletions(-) create mode 100644 web/src/features/playlists/account-settings.ts create mode 100644 web/src/features/playlists/create-playlist-form.tsx create mode 100644 web/src/features/playlists/playlist-cover.tsx create mode 100644 web/src/features/playlists/playlist-sort.test.ts create mode 100644 web/src/features/playlists/playlist-sort.ts create mode 100644 web/src/features/playlists/playlists.css create mode 100644 web/src/features/playlists/save-label.ts create mode 100644 web/src/features/playlists/save-to-playlist.tsx diff --git a/web/src/app/main.tsx b/web/src/app/main.tsx index 1fb0b4c..996fec4 100644 --- a/web/src/app/main.tsx +++ b/web/src/app/main.tsx @@ -5,7 +5,7 @@ import { chatStore } from '@/features/chat' import { historyStore } from '@/features/history' import { lyricsStore } from '@/features/lyrics' import { playerStore, reminderStore, volumeStore } from '@/features/playback' -import { playlistsStore } from '@/features/playlists' +import { playlistsStore, setAccountSettingsOpener } from '@/features/playlists' import { roomEntryStore } from '@/features/rooms' import { searchStore } from '@/features/search' import { appearanceStore, performanceStore, profileStore } from '@/features/settings' @@ -28,6 +28,7 @@ volumeStore.start() brandStore.start() profileStore.start() playlistsStore.start() +setAccountSettingsOpener(() => navigationStore.open('settings')) playerStore.start() reminderStore.start() historyStore.start() diff --git a/web/src/features/history/history-page.tsx b/web/src/features/history/history-page.tsx index 245d0c6..c0dcf39 100644 --- a/web/src/features/history/history-page.tsx +++ b/web/src/features/history/history-page.tsx @@ -2,6 +2,7 @@ import type { PlayRecord } from './records' import { CirclePlus, Download, History, Search, Trash2 } from 'lucide-react' import { useState } from 'react' import { roomStore, sameTrack } from '@/core/room' +import { SaveToPlaylistButton } from '@/features/playlists' import { enqueue, sourceName } from '@/features/queue' import { bookTitle } from '@/shared/lib/book-title' import { formatClock } from '@/shared/lib/time' @@ -46,6 +47,7 @@ function HistoryRow({ record, compact, connected, playing }: { record: PlayRecor await enqueue(record.track) }} /> + (null) return (
) } diff --git a/web/src/features/playlists/account-settings.ts b/web/src/features/playlists/account-settings.ts new file mode 100644 index 0000000..0905973 --- /dev/null +++ b/web/src/features/playlists/account-settings.ts @@ -0,0 +1,10 @@ +// 游客在保存对话框里点“去账号设置”时打开账号设置。设置页在哪里、怎样打开由应用壳决定,启动时设置。 +let open = (): void => {} + +export function openAccountSettings(): void { + open() +} + +export function setAccountSettingsOpener(opener: () => void): void { + open = opener +} diff --git a/web/src/features/playlists/create-playlist-form.tsx b/web/src/features/playlists/create-playlist-form.tsx new file mode 100644 index 0000000..8acbe54 --- /dev/null +++ b/web/src/features/playlists/create-playlist-form.tsx @@ -0,0 +1,41 @@ +import type { MyPlaylist } from '@/core/realtime' +import { useState } from 'react' +import { playlistsStore } from './playlists-store' + +export function CreatePlaylistForm({ onCreated, onCancel }: { onCreated: (playlist: MyPlaylist) => void, onCancel: () => void }) { + const [name, setName] = useState('') + const [saving, setSaving] = useState(false) + return ( +
{ + if (event.key !== 'Escape' || event.nativeEvent.isComposing) + return + // 拦下对话框的 Esc:在“保存到歌单”里,Esc 只回到列表。 + event.preventDefault() + onCancel() + }} + onSubmit={async (event) => { + event.preventDefault() + if (saving) + return + setSaving(true) + const playlist = await playlistsStore.create(name) + if (playlist) + onCreated(playlist) + else + setSaving(false) + }} + > + +
+ + +
+
+ ) +} diff --git a/web/src/features/playlists/index.ts b/web/src/features/playlists/index.ts index 277bca5..f544eac 100644 --- a/web/src/features/playlists/index.ts +++ b/web/src/features/playlists/index.ts @@ -1 +1,4 @@ +export { setAccountSettingsOpener } from './account-settings' export { playlistsStore } from './playlists-store' +export { saveToPlaylistLabel } from './save-label' +export { SaveToPlaylistButton, SaveToPlaylistDialog } from './save-to-playlist' diff --git a/web/src/features/playlists/playlist-cover.tsx b/web/src/features/playlists/playlist-cover.tsx new file mode 100644 index 0000000..f967936 --- /dev/null +++ b/web/src/features/playlists/playlist-cover.tsx @@ -0,0 +1,26 @@ +import { Music2, Star } from 'lucide-react' +import { useState } from 'react' + +export function FavoritesMark({ size = 14 }: { size?: number }) { + return