From 92953dec743667dcfe6309215df11ff56f87d968 Mon Sep 17 00:00:00 2001 From: Steve-xmh <39523898+Steve-xmh@users.noreply.github.com> Date: Fri, 1 Nov 2024 15:00:32 +0800 Subject: [PATCH] =?UTF-8?q?player:=20=E6=9B=B4=E6=96=B0=E4=BA=86=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=AD=8C=E6=9B=B2=E5=8D=A1?= =?UTF-8?q?=E7=89=87=E6=9A=82=E6=97=A0=E5=8F=AF=E4=BA=A4=E4=BA=92=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E4=B8=BA=E6=92=AD=E6=94=BE=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=8F=90=E4=BE=9B=E4=BA=86=E5=85=B6=E4=BB=96=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=20ws-protocol:=20=E5=A2=9E=E5=8A=A0=E4=BA=86=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E4=BC=A0=E9=80=92=20TTML=20=E6=AD=8C=E8=AF=8D=E7=9A=84?= =?UTF-8?q?=E6=8A=A5=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/player/package.json | 1 + .../components/AppContainer/index.module.css | 2 +- .../src/components/NowPlayingBar/index.tsx | 4 +- .../index.module.css | 0 .../src/components/NowPlaylistCard/index.tsx | 151 +++++++++ .../src/components/PlaylistCard/index.tsx | 215 ++++-------- .../src/components/PlaylistSongCard/index.tsx | 129 +++++++ .../player/src/components/SongCard/index.tsx | 43 +++ packages/player/src/pages/main/index.tsx | 36 +- packages/player/src/pages/playlist/index.tsx | 142 +------- .../player/src/pages/search/index.module.css | 27 ++ packages/player/src/pages/search/index.tsx | 314 ++++++++++++++++++ packages/player/src/router.tsx | 48 ++- packages/player/src/utils/use-song-cover.ts | 23 +- packages/player/vite.config.ts | 9 +- packages/ws-protocol/src/lib.rs | 2 + pnpm-lock.yaml | 10 + 17 files changed, 822 insertions(+), 334 deletions(-) rename packages/player/src/components/{PlaylistCard => NowPlaylistCard}/index.module.css (100%) create mode 100644 packages/player/src/components/NowPlaylistCard/index.tsx create mode 100644 packages/player/src/components/PlaylistSongCard/index.tsx create mode 100644 packages/player/src/components/SongCard/index.tsx create mode 100644 packages/player/src/pages/search/index.module.css diff --git a/packages/player/package.json b/packages/player/package.json index a5b52cc1c..cd1b60ffb 100644 --- a/packages/player/package.json +++ b/packages/player/package.json @@ -20,6 +20,7 @@ "@applemusic-like-lyrics/react-full": "workspace:^", "@applemusic-like-lyrics/ws-protocol": "workspace:^", "@dnd-kit/core": "^6.1.0", + "@iconify/icons-fluent": "^1.2.38", "@iconify/icons-ic": "^1.2.13", "@iconify/react": "^5.0.2", "@monaco-editor/react": "^4.6.0", diff --git a/packages/player/src/components/AppContainer/index.module.css b/packages/player/src/components/AppContainer/index.module.css index 17ff92b07..7ee084ea8 100644 --- a/packages/player/src/components/AppContainer/index.module.css +++ b/packages/player/src/components/AppContainer/index.module.css @@ -37,7 +37,7 @@ grid-column: 3 / 4; min-width: 0; min-height: 0; - overflow: hidden; + overflow: hidden auto; } .playbar { diff --git a/packages/player/src/components/NowPlayingBar/index.tsx b/packages/player/src/components/NowPlayingBar/index.tsx index 1edb36ecf..eba78278e 100644 --- a/packages/player/src/components/NowPlayingBar/index.tsx +++ b/packages/player/src/components/NowPlayingBar/index.tsx @@ -31,7 +31,7 @@ import { hideNowPlayingBarAtom, playlistCardOpenedAtom, } from "../../states/index.ts"; -import { PlaylistCard } from "../PlaylistCard/index.tsx"; +import { NowPlaylistCard } from "../NowPlaylistCard/index.tsx"; import styles from "./index.module.css"; export const NowPlayingBar: FC = () => { @@ -88,7 +88,7 @@ export const NowPlayingBar: FC = () => { right="0" bottom="calc(var(--amll-player-playbar-bottom) + var(--space-3))" > - + )} +> = ({ songData, className, index, ...props }) => { + const playlistIndex = useAtomValue(currentPlaylistMusicIndexAtom); + const [songId, setSongId] = useState(""); + const lastSongId = useRef(""); + + useLayoutEffect(() => { + if (songData.type === "local") { + const newSongId = md5(songData.filePath); + if (lastSongId.current !== newSongId) { + console.log("newSongId", lastSongId.current, "->", newSongId); + setSongId(newSongId); + } + lastSongId.current = newSongId; + } + }, [songData]); + + const [curSongInfo, setCurSongInfo] = useState(); + const songInfo = useLiveQuery(() => db.songs.get(songId), [songId]); + + useLayoutEffect(() => { + if (songInfo) setCurSongInfo(songInfo); + }, [songInfo]); + + const name = useMemo(() => { + if (curSongInfo?.songName) return curSongInfo?.songName; + if (songData.type === "local") return songData.filePath; + return ""; + }, [songData, curSongInfo]); + + const artists = useMemo(() => { + if (curSongInfo) return curSongInfo?.songArtists ?? ""; + return ""; + }, [curSongInfo]); + + const [cover, setCover] = useState(""); + + useLayoutEffect(() => { + if (curSongInfo?.cover) { + const newUri = URL.createObjectURL(curSongInfo.cover); + setCover(newUri); + return () => { + URL.revokeObjectURL(newUri); + }; + } + }, [curSongInfo]); + + return ( +
{ + emitAudioThread("jumpToSong", { + songIndex: index, + }); + }} + {...props} + > + } src={cover} /> +
+
{name}
+
{artists}
+
+ {playlistIndex === index && } +
+ ); +}; + +export const NowPlaylistCard: FC = (props) => { + const playlist = useAtomValue(currentPlaylistAtom); + const playlistIndex = useAtomValue(currentPlaylistMusicIndexAtom); + const playlistRef = useRef(); + const playlistContainerRef = useRef(null); + + useEffect(() => { + if (playlistRef.current) { + playlistRef.current.scrollToIndex({ + index: playlistIndex, + }); + } + }, [playlistIndex]); + + return ( + + + 当前播放列表 + + + + {(songData, index) => ( + + )} + + + + ); +}; diff --git a/packages/player/src/components/PlaylistCard/index.tsx b/packages/player/src/components/PlaylistCard/index.tsx index 6e8ce25f4..0627914ab 100644 --- a/packages/player/src/components/PlaylistCard/index.tsx +++ b/packages/player/src/components/PlaylistCard/index.tsx @@ -1,151 +1,76 @@ -import { PlayIcon } from "@radix-ui/react-icons"; -import { Avatar, Box, Flex, type FlexProps, Inset } from "@radix-ui/themes"; -import classNames from "classnames"; -import { useLiveQuery } from "dexie-react-hooks"; -import { useAtomValue } from "jotai"; -import md5 from "md5"; -import { - type FC, - type HTMLProps, - useEffect, - useLayoutEffect, - useMemo, - useRef, - useState, -} from "react"; +import { Card, ContextMenu, Flex, Text } from "@radix-ui/themes"; +import { type PropsWithChildren, forwardRef, useMemo } from "react"; import { Trans } from "react-i18next"; -import { ViewportList, type ViewportListRef } from "react-viewport-list"; -import { type Song, db } from "../../dexie.ts"; -import { - currentPlaylistAtom, - currentPlaylistMusicIndexAtom, -} from "../../states/index.ts"; -import { type SongData, emitAudioThread } from "../../utils/player.ts"; -import styles from "./index.module.css"; +import { Link } from "react-router-dom"; +import { type Playlist, db } from "../../dexie.ts"; +import { PlaylistCover } from "../PlaylistCover/index.tsx"; -// TODO: 会产生闪烁更新,需要检查修正 -const PlaylistSongItem: FC< - { - songData: SongData; - index: number; - } & HTMLProps -> = ({ songData, className, index, ...props }) => { - const playlistIndex = useAtomValue(currentPlaylistMusicIndexAtom); - const [songId, setSongId] = useState(""); - const lastSongId = useRef(""); - - useLayoutEffect(() => { - if (songData.type === "local") { - const newSongId = md5(songData.filePath); - if (lastSongId.current !== newSongId) { - console.log("newSongId", lastSongId.current, "->", newSongId); - setSongId(newSongId); - } - lastSongId.current = newSongId; - } - }, [songData]); - - const [curSongInfo, setCurSongInfo] = useState(); - const songInfo = useLiveQuery(() => db.songs.get(songId), [songId]); - - useLayoutEffect(() => { - if (songInfo) setCurSongInfo(songInfo); - }, [songInfo]); - - const name = useMemo(() => { - if (curSongInfo?.songName) return curSongInfo?.songName; - if (songData.type === "local") return songData.filePath; - return ""; - }, [songData, curSongInfo]); - - const artists = useMemo(() => { - if (curSongInfo) return curSongInfo?.songArtists ?? ""; - return ""; - }, [curSongInfo]); - - const [cover, setCover] = useState(""); - - useLayoutEffect(() => { - if (curSongInfo?.cover) { - const newUri = URL.createObjectURL(curSongInfo.cover); - setCover(newUri); - return () => { - URL.revokeObjectURL(newUri); - }; - } - }, [curSongInfo]); - - return ( -
{ - emitAudioThread("jumpToSong", { - songIndex: index, - }); - }} - {...props} - > - } src={cover} /> -
-
{name}
-
{artists}
-
- {playlistIndex === index && } -
- ); -}; - -export const PlaylistCard: FC = (props) => { - const playlist = useAtomValue(currentPlaylistAtom); - const playlistIndex = useAtomValue(currentPlaylistMusicIndexAtom); - const playlistRef = useRef(); - const playlistContainerRef = useRef(null); - - useEffect(() => { - if (playlistRef.current) { - playlistRef.current.scrollToIndex({ - index: playlistIndex, - }); - } - }, [playlistIndex]); +export const PlaylistCard = forwardRef< + HTMLDivElement, + PropsWithChildren<{ + playlist: Playlist; + }> +>(({ playlist, children }, ref) => { + const songAmount = playlist.songIds.length; + const createTime = useMemo(() => { + const today = new Date(); + const createTime = new Date(playlist.createTime); + if (today.toDateString() === createTime.toDateString()) + return createTime.toLocaleTimeString(); + return createTime.toLocaleDateString(); + }, [playlist.createTime]); return ( - - - 当前播放列表 - - - + + + + + + + {playlist.name} + + + + {{ songAmount }} 首歌曲 + +
-
+ + 创建于 {{ createTime }} + +
+
+
+ {children} +
+ +
+
+ + {}}> + 播放此列表 + + {}}> + + 以乱序播放此列表 + + + + db.playlists.delete(playlist.id)} > - {(songData, index) => ( - - )} -
-
-
+ 删除 + + + ); -}; +}); diff --git a/packages/player/src/components/PlaylistSongCard/index.tsx b/packages/player/src/components/PlaylistSongCard/index.tsx new file mode 100644 index 000000000..8933839ab --- /dev/null +++ b/packages/player/src/components/PlaylistSongCard/index.tsx @@ -0,0 +1,129 @@ +import { toDuration } from "@applemusic-like-lyrics/react-full"; +import { HamburgerMenuIcon, PlayIcon } from "@radix-ui/react-icons"; +import { + Avatar, + Box, + Card, + DropdownMenu, + Flex, + IconButton, + Skeleton, + Text, +} from "@radix-ui/themes"; +import { useLiveQuery } from "dexie-react-hooks"; +import type { Loadable } from "jotai/vanilla/utils/loadable"; +import { type CSSProperties, forwardRef } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { type Song, db } from "../../dexie.ts"; +import { router } from "../../router.tsx"; +import { useSongCover } from "../../utils/use-song-cover.ts"; + +export const PlaylistSongCard = forwardRef< + HTMLDivElement, + { + songId: string; + songIndex: number; + onPlayList: (songIndex: number) => void; + onDeleteSong: (songId: string) => void; + style?: CSSProperties; + } +>(({ songId, songIndex, onPlayList, onDeleteSong, style }, ref) => { + const song: Loadable = useLiveQuery( + () => + db.songs.get(songId).then((data) => { + if (!data) { + return { + state: "hasError", + error: new Error(`未找到歌曲 ID ${songId}`), + }; + } + return { + state: "hasData", + data: data, + }; + }), + [songId], + { + state: "loading", + }, + ); + const songImgUrl = useSongCover( + song.state === "hasData" ? song.data : undefined, + ); + const { t } = useTranslation(); + const navigate = useNavigate(); + + return ( + onPlayList(songIndex)} + > + + + + } src={songImgUrl} /> + + + {song.state === "hasData" && + (song.data.songName || + song.data.filePath || + t( + "page.playlist.music.unknownSongName", + "未知歌曲 ID {id}", + { + id: songId, + }, + ))} + + + {song.state === "hasData" && (song.data.songArtists || "")} + + + + {song.state === "hasData" && + (song.data.duration ? toDuration(song.data.duration) : "")} + + onPlayList(songIndex)}> + + + + + router.navigate(`/song/${songId}`)} + > + + + + + onPlayList(songIndex)}> + + 播放音乐 + + + navigate(`/song/${songId}`)}> + + 编辑歌曲覆盖信息 + + + + onDeleteSong(songId)} + > + + 从歌单中删除 + + + + + + + + + ); +}); diff --git a/packages/player/src/components/SongCard/index.tsx b/packages/player/src/components/SongCard/index.tsx new file mode 100644 index 000000000..7d0f5892c --- /dev/null +++ b/packages/player/src/components/SongCard/index.tsx @@ -0,0 +1,43 @@ +import { toDuration } from "@applemusic-like-lyrics/react-full"; +import { Avatar, Box, Card, Flex, Text } from "@radix-ui/themes"; +import { type CSSProperties, type PropsWithChildren, forwardRef } from "react"; +import { useTranslation } from "react-i18next"; +import type { Song } from "../../dexie.ts"; +import { useSongCover } from "../../utils/use-song-cover.ts"; + +export const SongCard = forwardRef< + HTMLDivElement, + PropsWithChildren<{ + song: Song; + style?: CSSProperties; + }> +>(({ song, style, children }, ref) => { + const songImgUrl = useSongCover(song); + const { t } = useTranslation(); + + return ( + + {}}> + + } src={songImgUrl} /> + + + {song.songName || + song.filePath || + t("page.playlist.music.unknownSongName", "未知歌曲 ID {id}", { + id: song.id, + })} + + + {song.songArtists || ""} + + + + {song.duration ? toDuration(song.duration) : ""} + + {children} + + + + ); +}); diff --git a/packages/player/src/pages/main/index.tsx b/packages/player/src/pages/main/index.tsx index 4d76e5caf..c044525c9 100644 --- a/packages/player/src/pages/main/index.tsx +++ b/packages/player/src/pages/main/index.tsx @@ -1,9 +1,7 @@ -import { HamburgerMenuIcon } from "@radix-ui/react-icons"; +import { HamburgerMenuIcon, MagnifyingGlassIcon } from "@radix-ui/react-icons"; import { Badge, Box, - Card, - ContextMenu, DropdownMenu, Flex, Heading, @@ -20,7 +18,7 @@ import { ViewportList } from "react-viewport-list"; import { ExtensionInjectPoint } from "../../components/ExtensionInjectPoint/index.tsx"; import { NewPlaylistButton } from "../../components/NewPlaylistButton/index.tsx"; import { PageContainer } from "../../components/PageContainer/index.tsx"; -import { PlaylistCover } from "../../components/PlaylistCover/index.tsx"; +import { PlaylistCard } from "../../components/PlaylistCard/index.tsx"; import { db } from "../../dexie.ts"; import { router } from "../../router.tsx"; import { updateInfoAtom } from "../../states/updater.ts"; @@ -56,6 +54,11 @@ export const Component: FC = () => { + + + + + @@ -114,30 +117,7 @@ export const Component: FC = () => { ref={viewportRef} > - {(v) => ( - - - - - - - {v.name} - - - - - - db.playlists.delete(v.id)} - > - - 删除 - - - - - )} + {(v) => } ) diff --git a/packages/player/src/pages/playlist/index.tsx b/packages/player/src/pages/playlist/index.tsx index 2e0194c92..cc8764c7a 100644 --- a/packages/player/src/pages/playlist/index.tsx +++ b/packages/player/src/pages/playlist/index.tsx @@ -1,22 +1,17 @@ import { ArrowLeftIcon, - HamburgerMenuIcon, Pencil1Icon, PlayIcon, PlusIcon, } from "@radix-ui/react-icons"; import { - Avatar, Box, Button, - Card, Container, ContextMenu, - DropdownMenu, Flex, Heading, IconButton, - Skeleton, Text, TextField, } from "@radix-ui/themes"; @@ -27,24 +22,15 @@ import { platform } from "@tauri-apps/plugin-os"; import { useLiveQuery } from "dexie-react-hooks"; import { motion, useMotionTemplate, useScroll } from "framer-motion"; import md5 from "md5"; -import { - type CSSProperties, - type FC, - forwardRef, - useCallback, - useMemo, - useRef, - useState, -} from "react"; +import { type FC, useCallback, useMemo, useRef, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; -import { useNavigate, useParams } from "react-router-dom"; +import { useParams } from "react-router-dom"; import { toast } from "react-toastify"; import { ViewportList } from "react-viewport-list"; import { PlaylistCover } from "../../components/PlaylistCover/index.tsx"; +import { PlaylistSongCard } from "../../components/PlaylistSongCard/index.tsx"; import { type Song, db } from "../../dexie.ts"; -import { router } from "../../router.tsx"; import { emitAudioThread, readLocalMusicMetadata } from "../../utils/player.ts"; -import { useSongCover } from "../../utils/use-song-cover.ts"; import styles from "./index.module.css"; export type Loadable = @@ -60,126 +46,6 @@ export type Loadable = data: Awaited; }; -function toDuration(duration: number) { - const isRemainTime = duration < 0; - - const d = Math.abs(duration | 0); - const sec = d % 60; - const min = Math.floor((d - sec) / 60); - const secText = "0".repeat(2 - sec.toString().length) + sec; - - return `${isRemainTime ? "-" : ""}${min}:${secText}`; -} - -export const SongCard = forwardRef< - HTMLDivElement, - { - songId: string; - songIndex: number; - onPlayList: (songIndex: number) => void; - onDeleteSong: (songId: string) => void; - style?: CSSProperties; - } ->(({ songId, songIndex, onPlayList, onDeleteSong, style }, ref) => { - const song: Loadable = useLiveQuery( - () => - db.songs.get(songId).then((data) => { - if (!data) { - return { - state: "hasError", - error: new Error(`未找到歌曲 ID ${songId}`), - }; - } - return { - state: "hasData", - data: data, - }; - }), - [songId], - { - state: "loading", - }, - ); - const songImgUrl = useSongCover( - song.state === "hasData" ? song.data : undefined, - ); - const { t } = useTranslation(); - const navigate = useNavigate(); - - return ( - onPlayList(songIndex)} - > - - - - } src={songImgUrl} /> - - - {song.state === "hasData" && - (song.data.songName || - song.data.filePath || - t( - "page.playlist.music.unknownSongName", - "未知歌曲 ID {id}", - { - id: songId, - }, - ))} - - - {song.state === "hasData" && (song.data.songArtists || "")} - - - - {song.state === "hasData" && - (song.data.duration ? toDuration(song.data.duration) : "")} - - onPlayList(songIndex)}> - - - - - router.navigate(`/song/${songId}`)} - > - - - - - onPlayList(songIndex)}> - - 播放音乐 - - - navigate(`/song/${songId}`)}> - - 编辑歌曲覆盖信息 - - - - onDeleteSong(songId)} - > - - 从歌单中删除 - - - - - - - - - ); -}); - const EditablePlaylistName: FC<{ playlistName: string; onPlaylistNameChange: (newName: string) => void; @@ -602,7 +468,7 @@ export const Component: FC = () => { viewportRef={playlistViewRef} > {(songId, index) => ( - +> = ({ label, ...props }) => { + return ( + + ); +}; + +interface Filter { + filterType: string; + keyword: string; + regexp: RegExp; +} + +const filtersAtom = atom([] as Filter[]); +const keywordAtom = atom(""); + +export const Component: FC = () => { + const [filters, setFilters] = useAtom(filtersAtom); + const [keyword, setKeyword] = useAtom(keywordAtom); + const trimmedKeyword = keyword.trim(); + const { t } = useTranslation(); + const inputRef = useRef(null); + + const songsSearchResult = useLiveQuery( + async () => + db.songs + .filter((song) => { + for (const filter of filters) { + switch (filter.filterType) { + case "songName": + if (!filter.regexp.test(song.songName)) return false; + break; + case "artistName": + if (!filter.regexp.test(song.songArtists)) return false; + break; + case "albumName": + if (!filter.regexp.test(song.songAlbum)) return false; + break; + case "lyricContent": + if (!filter.regexp.test(song.lyric)) return false; + break; + default: + break; + } + } + return true; + }) + .distinct() + .limit(20) + .toArray(), + [filters], + ); + + const playlistsSearchResult = useLiveQuery( + async () => + db.playlists + .filter((playlist) => { + for (const filter of filters) { + switch (filter.filterType) { + case "playlistName": + if (!filter.regexp.test(playlist.name)) return false; + break; + default: + break; + } + } + return true; + }) + .distinct() + .limit(20) + .toArray(), + [filters], + ); + + const addFilter = useCallback( + (filterType: string) => { + setFilters((prev) => [ + ...prev, + { + filterType, + keyword: trimmedKeyword, + regexp: new RegExp(trimmedKeyword, "i"), + }, + ]); + setKeyword(""); + inputRef.current?.focus(); + }, + [trimmedKeyword, setFilters, setKeyword], + ); + + return ( + + + + + + setKeyword(evt.target.value)} + onKeyUp={(evt) => { + if (evt.key === "Enter" && trimmedKeyword !== "") { + addFilter("songName"); + } else if (evt.key === "Backspace" && keyword === "") { + setFilters((prev) => prev.slice(0, -1)); + } + }} + ref={inputRef} + > + + + {filters.map(({ filterType, keyword }, i) => ( + + ))} + + + {trimmedKeyword.length > 0 && ( + + + addFilter("songName")} + /> + addFilter("artistName")} + /> + addFilter("albumName")} + /> + addFilter("playlistName")} + /> + addFilter("lyricContent")} + /> + + + )} + {filters.length === 0 ? ( + + 搜索歌曲、歌手、专辑、歌词、播放列表等信息 + + ) : ( + <> + {!songsSearchResult && ( + + + 搜索歌曲中 + + )} + {songsSearchResult && songsSearchResult.length > 0 && ( + <> + + 搜索到 {songsSearchResult.length} 首歌曲 + + + {songsSearchResult.map((song) => ( + + ))} + + )} + {songsSearchResult?.length === 0 && ( + + 无歌曲结果 + + )} + {!playlistsSearchResult && ( + + + 搜索播放列表中 + + )} + {playlistsSearchResult?.length === 0 && 无播放列表结果} + {playlistsSearchResult && playlistsSearchResult.length > 0 && ( + <> + + 搜索到 {playlistsSearchResult.length} 个播放列表 + + + {playlistsSearchResult.map((playlist) => ( + + ))} + + )} + + )} + + + ); +}; + +Component.displayName = "SearchPage"; + +export default Component; diff --git a/packages/player/src/router.tsx b/packages/player/src/router.tsx index e158e99f3..de535de45 100644 --- a/packages/player/src/router.tsx +++ b/packages/player/src/router.tsx @@ -10,20 +10,50 @@ export const router = createBrowserRouter( <> import("./pages/main")} + lazy={() => import("./pages/main/index.tsx")} + errorElement={} + /> + import("./pages/settings/index.tsx")} + errorElement={} + /> + import("./pages/search/index.tsx")} + errorElement={} + /> + import("./pages/playlist")} + errorElement={} + /> + import("./pages/song/index.tsx")} errorElement={} /> - import("./pages/settings")} /> - import("./pages/playlist")} /> - import("./pages/song")} /> import("./pages/amll-dev/mg-edit")} + lazy={() => import("./pages/amll-dev/mg-edit.tsx")} + errorElement={} + /> + import("./pages/amll-dev/index.tsx")} + errorElement={} /> - import("./pages/amll-dev")} /> - - import("./pages/ws/recv")} /> - import("./pages/ws/send")} /> + }> + import("./pages/ws/recv.tsx")} + errorElement={} + /> + import("./pages/ws/send.tsx")} + errorElement={} + /> , ), diff --git a/packages/player/src/utils/use-song-cover.ts b/packages/player/src/utils/use-song-cover.ts index 6f6e8fd01..7de72cd22 100644 --- a/packages/player/src/utils/use-song-cover.ts +++ b/packages/player/src/utils/use-song-cover.ts @@ -1,15 +1,18 @@ -import { useEffect, useMemo } from "react"; +import { useLayoutEffect, useState } from "react"; import type { Song } from "../dexie.ts"; export const useSongCover = (song?: Song) => { - const songImgUrl = useMemo( - () => (song?.cover ? URL.createObjectURL(song.cover) : ""), - [song], - ); - useEffect(() => { - return () => { - if (songImgUrl.length > 0) URL.revokeObjectURL(songImgUrl); - }; - }, [songImgUrl]); + const [songImgUrl, setSongImgUrl] = useState(""); + + useLayoutEffect(() => { + if (song?.cover) { + const newUri = URL.createObjectURL(song.cover); + setSongImgUrl(newUri); + return () => { + URL.revokeObjectURL(newUri); + }; + } + }, [song?.cover]); + return songImgUrl; }; diff --git a/packages/player/vite.config.ts b/packages/player/vite.config.ts index 81a193a5b..8185b68ea 100644 --- a/packages/player/vite.config.ts +++ b/packages/player/vite.config.ts @@ -1,5 +1,7 @@ import react from "@vitejs/plugin-react"; import { execSync } from "child_process"; +import jotaiDebugLabel from "jotai/babel/plugin-debug-label"; +import jotaiReactRefresh from "jotai/babel/plugin-react-refresh"; import { resolve } from "path"; import { type Plugin, defineConfig } from "vite"; import i18nextLoader from "vite-plugin-i18next-loader"; @@ -84,9 +86,14 @@ export default defineConfig(async () => ({ rollupOptions: { shimMissingExports: true, }, + sourcemap: "inline", }, plugins: [ - react(), + react({ + babel: { + plugins: [jotaiDebugLabel, jotaiReactRefresh], + }, + }), wasm(), topLevelAwait(), svgr({ diff --git a/packages/ws-protocol/src/lib.rs b/packages/ws-protocol/src/lib.rs index d3ddb41ce..15383773a 100644 --- a/packages/ws-protocol/src/lib.rs +++ b/packages/ws-protocol/src/lib.rs @@ -131,6 +131,8 @@ pub enum Body { BackwardSong, #[brw(magic(18u16))] SetVolume { volume: f64 }, + #[brw(magic(19u16))] + SetLyricLineTTML { data: NullString }, } pub fn parse_body(body: &[u8]) -> anyhow::Result { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d9603e49..5588eb8a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -342,6 +342,9 @@ importers: '@dnd-kit/core': specifier: ^6.1.0 version: 6.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@iconify/icons-fluent': + specifier: ^1.2.38 + version: 1.2.38 '@iconify/icons-ic': specifier: ^1.2.13 version: 1.2.13 @@ -1344,6 +1347,9 @@ packages: resolution: {integrity: sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==} engines: {node: '>=6.9.0'} + '@iconify/icons-fluent@1.2.38': + resolution: {integrity: sha512-nwCCUD3GLl/0xUDFnNIFYDhOr00mUTBToDBZaspFeXAaCnfHklMRtf0uHevBjKgGVqwlaV5c8zer7aNjwyRBGQ==} + '@iconify/icons-ic@1.2.13': resolution: {integrity: sha512-TphrhwOvgd7CTmUhz6jgXF+SPnMtvTm03bZsAYbny2kwq4zlkhr9e16YEyPGMvKhjtTqNooA3iZ9Wa+pZ8moXQ==} @@ -7871,6 +7877,10 @@ snapshots: '@hutson/parse-repository-url@3.0.2': {} + '@iconify/icons-fluent@1.2.38': + dependencies: + '@iconify/types': 2.0.0 + '@iconify/icons-ic@1.2.13': dependencies: '@iconify/types': 2.0.0