From f340a90117b042d5828e1f4ca521548e0b75a3bd Mon Sep 17 00:00:00 2001 From: qier222 Date: Tue, 14 Jun 2022 23:23:34 +0800 Subject: [PATCH] feat: updates --- packages/web/IpcRendererReact.tsx | 2 +- packages/web/api/hooks/useLyric.ts | 1 + packages/web/api/playlist.ts | 1 - .../web/assets/images/topbar-background.png | Bin 0 -> 1519 bytes packages/web/components/New/ArtistRow.tsx | 5 +- packages/web/components/New/CoverRow.tsx | 81 ++++++---- .../web/components/New/CoverRowVirtual.tsx | 128 +++++++++++++++ packages/web/components/New/Image.tsx | 77 ++++++--- packages/web/components/New/Layout.tsx | 2 + packages/web/components/New/LayoutMobile.tsx | 5 +- packages/web/components/New/Login/Login.tsx | 80 +++++++++ packages/web/components/New/Login/index.tsx | 3 + packages/web/components/New/MenuBar.tsx | 12 +- .../web/components/New/PlayLikedSongsCard.tsx | 93 ----------- packages/web/components/New/PlayingNext.tsx | 2 +- packages/web/components/New/Router.tsx | 2 + packages/web/components/New/Tabs.tsx | 4 +- .../web/components/New/Topbar/SearchBox.tsx | 2 +- .../components/New/Topbar/TopbarDesktop.tsx | 8 +- .../web/components/New/TrackListHeader.tsx | 42 ++--- packages/web/package.json | 1 + packages/web/pages/New/Browse.tsx | 94 +++++++++++ packages/web/pages/New/Discover.tsx | 2 +- packages/web/pages/New/My.tsx | 115 ------------- packages/web/pages/New/My/Collections.tsx | 58 +++++++ packages/web/pages/New/My/My.tsx | 19 +++ .../web/pages/New/My/PlayLikedSongsCard.tsx | 152 ++++++++++++++++++ .../web/pages/New/My/RecentlyListened.tsx | 42 +++++ packages/web/pages/New/My/index.tsx | 3 + packages/web/styles/global.css | 16 +- packages/web/utils/player.ts | 6 +- packages/web/vite.config.ts | 12 +- pnpm-lock.yaml | 28 +++- vercel.json | 6 +- 34 files changed, 781 insertions(+), 323 deletions(-) create mode 100644 packages/web/assets/images/topbar-background.png create mode 100644 packages/web/components/New/CoverRowVirtual.tsx create mode 100644 packages/web/components/New/Login/Login.tsx create mode 100644 packages/web/components/New/Login/index.tsx delete mode 100644 packages/web/components/New/PlayLikedSongsCard.tsx create mode 100644 packages/web/pages/New/Browse.tsx delete mode 100644 packages/web/pages/New/My.tsx create mode 100644 packages/web/pages/New/My/Collections.tsx create mode 100644 packages/web/pages/New/My/My.tsx create mode 100644 packages/web/pages/New/My/PlayLikedSongsCard.tsx create mode 100644 packages/web/pages/New/My/RecentlyListened.tsx create mode 100644 packages/web/pages/New/My/index.tsx diff --git a/packages/web/IpcRendererReact.tsx b/packages/web/IpcRendererReact.tsx index ad101f6..1226b13 100644 --- a/packages/web/IpcRendererReact.tsx +++ b/packages/web/IpcRendererReact.tsx @@ -45,7 +45,7 @@ const IpcRendererReact = () => { window.ipcRenderer?.send(playing ? IpcChannels.Play : IpcChannels.Pause) setIsPlaying(playing) - }, [state]) + }, [isPlaying, state]) useEffectOnce(() => { // 用于显示 windows taskbar buttons diff --git a/packages/web/api/hooks/useLyric.ts b/packages/web/api/hooks/useLyric.ts index 54c6011..9092065 100644 --- a/packages/web/api/hooks/useLyric.ts +++ b/packages/web/api/hooks/useLyric.ts @@ -18,6 +18,7 @@ export default function useLyric(params: FetchLyricParams) { { enabled: !!params.id && params.id !== 0, refetchInterval: false, + refetchOnWindowFocus: false, staleTime: Infinity, initialData: (): FetchLyricResponse | undefined => window.ipcRenderer?.sendSync(IpcChannels.GetApiCacheSync, { diff --git a/packages/web/api/playlist.ts b/packages/web/api/playlist.ts index d8d2977..2c8b921 100644 --- a/packages/web/api/playlist.ts +++ b/packages/web/api/playlist.ts @@ -39,7 +39,6 @@ export function fetchRecommendedPlaylists( } // 每日推荐歌单(需要登录) - export function fetchDailyRecommendPlaylists(): Promise { return request({ url: '/recommend/resource', diff --git a/packages/web/assets/images/topbar-background.png b/packages/web/assets/images/topbar-background.png new file mode 100644 index 0000000000000000000000000000000000000000..ab7cbd64860b71f915fcd55067b0f7021c7834be GIT binary patch literal 1519 zcmVox2PfNDofM+ zCzpyCrdEy5Z`OY{j^G>A*3Y@2EauEk{Q0w0h)9j$lm8ZVv9?pIB2@ch zBa=qI!i5qbk#xN;b@+RO3GlI<9{PL35QEhp#1l-ts5L2M2o&<{-ygZwoVl14UK z-H3VVY#lWz{N6gIAuKb!#T3)n5sje9NmDI^FL0$!r%PoC=w2gY41+)>HW|AU{5Cde ztjwgUMh~<`F)8`t5JIgnG^r?BgJB)=g&A^9VUl4=3r7q^-Av_9S}_B-BHg&IvI?Zx z5GQs^lu}M4-`Al@yx^o&Nd{b-)Xk8FPC4ZzxxMl2vzywS4_OUS(&ZSEtwZY5uw-jD zCRyVLHH?|$D{@eWbi{bsGiU;_M4ol^(EA`nFd)F6={6t=XheziQEHuyLpQD-o_z?8 zT*Mfbnbtknuu`Xh7-m)fMW_G6MAQmtA9^O$b?xVTS;A*y42QMF2{;0aa~;x1YlDO{ zAtFWrCB%>32k5tLUV|1O7D$x({q?)O0CB27g9|exNS1uZSUD4YnVC6hR@<+@n@EDi z2S}I|?g9HG3{ldBwu>gs&`W2sU3^o%yfesx5J`l@Q4P(~(h|3!UDtWZBnj~zr~AML zk#D#-*iLFjW`I8%Cz7+hD48Tq*AOS2fDbWiK$R?6srG(2v-p|T$%j^)Zkh(>^EnaH zH%yCj#t{&n1mSbT*-DF|mQI=@5Mr-cmzmhXca6Pyga5P+Q7MU5OQtn%68xze%&@$? z3~lR`T70h5Gl!DIP#xoM`YBg{1xZ^~>)C&1bWj!Sxz+^+qH(BALU= z%8EAy8F-U=Z&4&*hPSsjmX2eOiRR+Ne06noPT@zQR-S2ek_pf2&Dz>p_FLQh zCMFQu5kKGB+M=-|I$|KH#z{4gkB>xXFk`#z?QO!Fhld9vbd8(n{I7O)cA~?NI37tu zhsniK@9*!~92$|Cw6J9QVQMg*8SugE@9%eRkw@I5?(Xi+Gu+?MvNN$lZxV*d+A+Y3r#9W`BpVu;X zKUrToJ3Fgu{HNC>CN8q#ti8Cn;D$u9GAfa&wyqAO^#HYpDc)-rLTy#pu>U|Q{04FX VZIWKJZZQA=002ovPDHLkV1nm3&kO(n literal 0 HcmV?d00001 diff --git a/packages/web/components/New/ArtistRow.tsx b/packages/web/components/New/ArtistRow.tsx index 7dbdcce..9d322bd 100644 --- a/packages/web/components/New/ArtistRow.tsx +++ b/packages/web/components/New/ArtistRow.tsx @@ -1,5 +1,6 @@ import { resizeImage } from '@/web/utils/common' import { css, cx } from '@emotion/css' +import { memo } from 'react' import Image from './Image' const Artist = ({ artist }: { artist: Artist }) => { @@ -82,4 +83,6 @@ const ArtistRow = ({ ) } -export default ArtistRow +const memoizedArtistRow = memo(ArtistRow) +memoizedArtistRow.displayName = 'ArtistRow' +export default memoizedArtistRow diff --git a/packages/web/components/New/CoverRow.tsx b/packages/web/components/New/CoverRow.tsx index 0de43b1..635af5c 100644 --- a/packages/web/components/New/CoverRow.tsx +++ b/packages/web/components/New/CoverRow.tsx @@ -4,6 +4,48 @@ import { useNavigate } from 'react-router-dom' import Image from './Image' import { prefetchAlbum } from '@/web/api/hooks/useAlbum' import { prefetchPlaylist } from '@/web/api/hooks/usePlaylist' +import { memo, useCallback } from 'react' + +const Album = ({ album }: { album: Album }) => { + const navigate = useNavigate() + const goTo = () => { + console.log('dsada') + navigate(`/album/${album.id}`) + } + const prefetch = () => { + prefetchAlbum({ id: album.id }) + } + + return ( + + ) +} + +const Playlist = ({ playlist }: { playlist: Playlist }) => { + const navigate = useNavigate() + const goTo = useCallback(() => { + navigate(`/playlist/${playlist.id}`) + }, [navigate, playlist.id]) + const prefetch = useCallback(() => { + prefetchPlaylist({ id: playlist.id }) + }, [playlist.id]) + + return ( + + ) +} const CoverRow = ({ albums, @@ -16,18 +58,6 @@ const CoverRow = ({ albums?: Album[] playlists?: Playlist[] }) => { - const navigate = useNavigate() - - const goTo = (id: number) => { - if (albums) navigate(`/album/${id}`) - if (playlists) navigate(`/playlist/${id}`) - } - - const prefetch = (id: number) => { - if (albums) prefetchAlbum({ id }) - if (playlists) prefetchPlaylist({ id }) - } - return (
{/* Title */} @@ -38,33 +68,18 @@ const CoverRow = ({ )} {/* Items */} -
+
{albums?.map(album => ( - goTo(album.id)} - key={album.id} - alt={album.name} - src={resizeImage(album?.picUrl || '', 'md')} - className='aspect-square rounded-24' - onMouseOver={() => prefetch(album.id)} - /> + ))} {playlists?.map(playlist => ( - goTo(playlist.id)} - key={playlist.id} - alt={playlist.name} - src={resizeImage( - playlist.coverImgUrl || playlist?.picUrl || '', - 'md' - )} - className='aspect-square rounded-24' - onMouseOver={() => prefetch(playlist.id)} - /> + ))}
) } -export default CoverRow +const memoizedCoverRow = memo(CoverRow) +memoizedCoverRow.displayName = 'CoverRow' +export default memoizedCoverRow diff --git a/packages/web/components/New/CoverRowVirtual.tsx b/packages/web/components/New/CoverRowVirtual.tsx new file mode 100644 index 0000000..d29f1b6 --- /dev/null +++ b/packages/web/components/New/CoverRowVirtual.tsx @@ -0,0 +1,128 @@ +import { resizeImage } from '@/web/utils/common' +import { cx } from '@emotion/css' +import { useNavigate } from 'react-router-dom' +import Image from './Image' +import { prefetchAlbum } from '@/web/api/hooks/useAlbum' +import { prefetchPlaylist } from '@/web/api/hooks/usePlaylist' +import { useVirtualizer } from '@tanstack/react-virtual' +import { CSSProperties, useRef } from 'react' + +const CoverRow = ({ + albums, + playlists, + title, + className, + containerClassName, + containerStyle, +}: { + title?: string + className?: string + albums?: Album[] + playlists?: Playlist[] + containerClassName?: string + containerStyle?: CSSProperties +}) => { + const navigate = useNavigate() + + const goTo = (id: number) => { + if (albums) navigate(`/album/${id}`) + if (playlists) navigate(`/playlist/${id}`) + } + + const prefetch = (id: number) => { + if (albums) prefetchAlbum({ id }) + if (playlists) prefetchPlaylist({ id }) + } + + // The scrollable element for your list + const parentRef = useRef(null) + + type Item = Album | Playlist + const items: Item[] = albums || playlists || [] + const rows = items.reduce((rows: Item[][], item: Item, index: number) => { + const rowIndex = Math.floor(index / 4) + if (rows.length < rowIndex + 1) { + rows.push([item]) + } else { + rows[rowIndex].push(item) + } + return rows + }, []) + + // The virtualizer + const rowVirtualizer = useVirtualizer({ + count: rows.length, + getScrollElement: () => parentRef.current, + overscan: 5, + estimateSize: () => { + const containerWidth = parentRef.current?.clientWidth + console.log(parentRef.current?.clientWidth) + if (!containerWidth) { + return 192 + } + const gridGapY = 24 + const gridGapX = 40 + const gridColumns = 4 + console.log( + (containerWidth - (gridColumns - 1) * gridGapX) / gridColumns + gridGapY + ) + return ( + (containerWidth - (gridColumns - 1) * gridGapX) / gridColumns + gridGapY + ) + }, + }) + + return ( +
+ {/* Title */} + {title && ( +

+ {title} +

+ )} + +
+
+ {rowVirtualizer.getVirtualItems().map((row: any) => ( +
+ {rows[row.index].map((item: Item) => ( + goTo(item.id)} + key={item.id} + alt={item.name} + src={resizeImage( + item?.picUrl || + (item as Playlist)?.coverImgUrl || + item?.picUrl || + '', + 'md' + )} + className='aspect-square rounded-24' + onMouseOver={() => prefetch(item.id)} + /> + ))} +
+ ))} +
+
+
+ ) +} + +export default CoverRow diff --git a/packages/web/components/New/Image.tsx b/packages/web/components/New/Image.tsx index d82df0f..c8d5371 100644 --- a/packages/web/components/New/Image.tsx +++ b/packages/web/components/New/Image.tsx @@ -4,45 +4,46 @@ import { useEffect, useState } from 'react' import { ease } from '@/web/utils/const' import useIsMobile from '@/web/hooks/useIsMobile' -const Image = ({ +type Props = { + src?: string + srcSet?: string + sizes?: string + className?: string + lazyLoad?: boolean + placeholder?: 'artist' | 'album' | 'playlist' | 'podcast' | 'blank' | false + onClick?: (e: React.MouseEvent) => void + onMouseOver?: (e: React.MouseEvent) => void + animation?: boolean +} + +const ImageDesktop = ({ src, srcSet, className, - alt, lazyLoad = true, sizes, placeholder = 'blank', onClick, onMouseOver, animation = true, -}: { - src?: string - srcSet?: string - sizes?: string - className?: string - alt: string - lazyLoad?: boolean - placeholder?: 'artist' | 'album' | 'playlist' | 'podcast' | 'blank' | false - onClick?: (e: React.MouseEvent) => void - onMouseOver?: (e: React.MouseEvent) => void - animation?: boolean -}) => { - const [loaded, setLoaded] = useState(false) +}: Props) => { const [error, setError] = useState(false) const animate = useAnimation() + const placeholderAnimate = useAnimation() const isMobile = useIsMobile() const isAnimate = animation && !isMobile useEffect(() => setError(false), [src]) const onLoad = async () => { - setLoaded(true) - if (isAnimate) animate.start({ opacity: 1 }) + if (isAnimate) { + animate.start({ opacity: 1 }) + placeholderAnimate.start({ opacity: 0 }) + } } const onError = () => { setError(true) } - const hidden = error || !loaded const transition = { duration: 0.6, ease } const motionProps = isAnimate ? { @@ -54,6 +55,7 @@ const Image = ({ : {} const placeholderMotionProps = isAnimate ? { + animate: placeholderAnimate, initial: { opacity: 1 }, exit: { opacity: 0 }, transition, @@ -62,6 +64,8 @@ const Image = ({ return (
{/* Placeholder / Error fallback */} - {hidden && placeholder && ( + {placeholder && ( { + const { src, className, srcSet, sizes, lazyLoad, onClick, onMouseOver } = + props + return ( +
+ {src && ( + + )} +
+ ) +} + +const Image = (props: Props) => { + const isMobile = useIsMobile() + return isMobile ? : +} + export default Image diff --git a/packages/web/components/New/Layout.tsx b/packages/web/components/New/Layout.tsx index 26208ef..9fce077 100644 --- a/packages/web/components/New/Layout.tsx +++ b/packages/web/components/New/Layout.tsx @@ -5,6 +5,7 @@ import Topbar from '@/web/components/New/Topbar/TopbarDesktop' import { css, cx } from '@emotion/css' import { player } from '@/web/store' import { useSnapshot } from 'valtio' +import Login from './Login' const Layout = () => { const playerSnapshot = useSnapshot(player) @@ -36,6 +37,7 @@ const Layout = () => {
+ {showPlayer && }
) diff --git a/packages/web/components/New/LayoutMobile.tsx b/packages/web/components/New/LayoutMobile.tsx index 1d02051..413fdfa 100644 --- a/packages/web/components/New/LayoutMobile.tsx +++ b/packages/web/components/New/LayoutMobile.tsx @@ -7,13 +7,14 @@ import Router from '@/web/components/New/Router' import MenuBar from './MenuBar' import TopbarMobile from './Topbar/TopbarMobile' import { isIOS, isPWA, isSafari } from '@/web/utils/common' +import Login from './Login' const LayoutMobile = () => { const playerSnapshot = useSnapshot(player) const showPlayer = !!playerSnapshot.track return ( -
+
@@ -48,6 +49,8 @@ const LayoutMobile = () => {
+ + {/* Notch background */} {isIOS && isSafari && isPWA && (
{ + const [image, setImage] = useState('') + + useEffect(() => { + if (text) { + qrCode + .toString(text, { + margin: 0, + color: { light: '#ffffff00' }, + type: 'svg', + }) + .then(image => { + setImage(image) + console.log(image) + }) + } + }, [text]) + + const encodedImage = useMemo(() => encodeURIComponent(image), [image]) + + return ( + + ) +} + +const Login = () => { + return
+ return ( +
+
+
+
+ Log in with NetEase QR +
+ +
+ +
+ +
+
+
or
+
+
+ +
+ +
+
+ + {/* Close */} +
+
+
+ ) +} + +export default Login diff --git a/packages/web/components/New/Login/index.tsx b/packages/web/components/New/Login/index.tsx new file mode 100644 index 0000000..393a6df --- /dev/null +++ b/packages/web/components/New/Login/index.tsx @@ -0,0 +1,3 @@ +import Login from './Login' + +export default Login diff --git a/packages/web/components/New/MenuBar.tsx b/packages/web/components/New/MenuBar.tsx index 254752b..f505ec8 100644 --- a/packages/web/components/New/MenuBar.tsx +++ b/packages/web/components/New/MenuBar.tsx @@ -78,9 +78,12 @@ const TabName = () => { } const Tabs = () => { + const location = useLocation() const navigate = useNavigate() const controls = useAnimation() - const [active, setActive] = useState(tabs[0].path) + const [active, setActive] = useState( + location.pathname || tabs[0].path + ) const animate = async (path: string) => { await controls.start((p: string) => @@ -96,7 +99,12 @@ const Tabs = () => { key={tab.name} animate={controls} transition={{ ease, duration: 0.18 }} - onMouseDown={() => animate(tab.path)} + onMouseDown={() => { + if ('vibrate' in navigator) { + navigator.vibrate(20) + } + animate(tab.path) + }} onClick={() => { setActive(tab.path) navigate(tab.path) diff --git a/packages/web/components/New/PlayLikedSongsCard.tsx b/packages/web/components/New/PlayLikedSongsCard.tsx deleted file mode 100644 index 5f8680b..0000000 --- a/packages/web/components/New/PlayLikedSongsCard.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import useLyric from '@/web/api/hooks/useLyric' -import usePlaylist from '@/web/api/hooks/usePlaylist' -import useUserPlaylists from '@/web/api/hooks/useUserPlaylists' -import { player } from '@/web/store' -import { sample, chunk } from 'lodash-es' -import { css, cx } from '@emotion/css' -import { useState, useEffect, useMemo, useCallback } from 'react' -import toast from 'react-hot-toast' -import { useNavigate } from 'react-router-dom' - -const PlayLikedSongsCard = () => { - const navigate = useNavigate() - - const { data: playlists } = useUserPlaylists() - - const { data: likedSongsPlaylist } = usePlaylist({ - id: playlists?.playlist?.[0].id ?? 0, - }) - - // Lyric - const [trackID, setTrackID] = useState(0) - - useEffect(() => { - if (trackID === 0) { - setTrackID( - sample(likedSongsPlaylist?.playlist.trackIds?.map(t => t.id) ?? []) ?? 0 - ) - } - }, [likedSongsPlaylist?.playlist.trackIds, trackID]) - - const { data: lyric } = useLyric({ - id: trackID, - }) - - const lyricLines = useMemo(() => { - return ( - sample( - chunk( - lyric?.lrc.lyric - ?.split('\n') - ?.map(l => l.split(']').pop()?.trim()) - ?.filter( - l => - l && - !l.includes('作词') && - !l.includes('作曲') && - !l.includes('纯音乐,请欣赏') - ), - 4 - ) - ) ?? [] - ) - }, [lyric]) - - const handlePlay = useCallback( - (e: React.MouseEvent) => { - e.stopPropagation() - if (!likedSongsPlaylist?.playlist.id) { - toast('无法播放歌单') - return - } - player.playPlaylist(likedSongsPlaylist.playlist.id) - }, - [likedSongsPlaylist?.playlist.id] - ) - - return ( -
-
- {lyricLines.map((line, index) => ( -
{line}
- ))} -
-
- -
-
- ) -} - -export default PlayLikedSongsCard diff --git a/packages/web/components/New/PlayingNext.tsx b/packages/web/components/New/PlayingNext.tsx index 75b3279..feab834 100644 --- a/packages/web/components/New/PlayingNext.tsx +++ b/packages/web/components/New/PlayingNext.tsx @@ -112,7 +112,7 @@ const TrackList = ({ className }: { className?: string }) => { count: tracks.length, getScrollElement: () => parentRef.current, estimateSize: () => 76, - overscan: 5, + overscan: 10, }) return ( diff --git a/packages/web/components/New/Router.tsx b/packages/web/components/New/Router.tsx index 91a74fa..7dd5ae4 100644 --- a/packages/web/components/New/Router.tsx +++ b/packages/web/components/New/Router.tsx @@ -9,6 +9,7 @@ import React, { ReactNode, Suspense } from 'react' const My = React.lazy(() => import('@/web/pages/New/My')) const Discover = React.lazy(() => import('@/web/pages/New/Discover')) +const Browse = React.lazy(() => import('@/web/pages/New/Browse')) const Album = React.lazy(() => import('@/web/pages/New/Album')) const Playlist = React.lazy(() => import('@/web/pages/New/Playlist')) @@ -69,6 +70,7 @@ const Router = () => { )} /> )} /> + )} /> )} /> )} /> )} /> diff --git a/packages/web/components/New/Tabs.tsx b/packages/web/components/New/Tabs.tsx index a78d2e9..1f42d3c 100644 --- a/packages/web/components/New/Tabs.tsx +++ b/packages/web/components/New/Tabs.tsx @@ -20,10 +20,10 @@ const Tabs = ({
onChange(tab.id)} > diff --git a/packages/web/components/New/Topbar/SearchBox.tsx b/packages/web/components/New/Topbar/SearchBox.tsx index f56a8c8..8c6ea5b 100644 --- a/packages/web/components/New/Topbar/SearchBox.tsx +++ b/packages/web/components/New/Topbar/SearchBox.tsx @@ -20,7 +20,7 @@ const SearchBox = () => { className={cx( 'flex-shrink bg-transparent font-medium placeholder:text-neutral-500 dark:text-neutral-200', css` - @media (max-width: 360px) { + @media (max-width: 420px) { width: 142px; } ` diff --git a/packages/web/components/New/Topbar/TopbarDesktop.tsx b/packages/web/components/New/Topbar/TopbarDesktop.tsx index 5a6d300..7ece523 100644 --- a/packages/web/components/New/Topbar/TopbarDesktop.tsx +++ b/packages/web/components/New/Topbar/TopbarDesktop.tsx @@ -4,6 +4,7 @@ import Avatar from './Avatar' import SearchBox from './SearchBox' import SettingsButton from './SettingsButton' import NavigationButtons from './NavigationButtons' +import topbarBackground from '@/web/assets/images/topbar-background.png' const TopbarDesktop = () => { const location = useLocation() @@ -11,13 +12,16 @@ const TopbarDesktop = () => { return (
{/* Left Part */} diff --git a/packages/web/components/New/TrackListHeader.tsx b/packages/web/components/New/TrackListHeader.tsx index fbe4620..ea51786 100644 --- a/packages/web/components/New/TrackListHeader.tsx +++ b/packages/web/components/New/TrackListHeader.tsx @@ -12,7 +12,6 @@ import Image from './Image' import useIsMobile from '@/web/hooks/useIsMobile' import { memo, useEffect, useMemo, useRef } from 'react' import Hls from 'hls.js' -import Plyr, { APITypes, PlyrProps, PlyrInstance } from 'plyr-react' import useVideoCover from '@/web/hooks/useVideoCover' import { motion } from 'framer-motion' import { ease } from '@/web/utils/const' @@ -26,42 +25,20 @@ injectGlobal` ` const VideoCover = ({ source }: { source?: string }) => { - const ref = useRef(null) - useEffect(() => { - const loadVideo = async () => { - if (!source || !Hls.isSupported()) return - const video = document.getElementById('plyr') as HTMLVideoElement - const hls = new Hls() - hls.loadSource(source) - hls.attachMedia(video) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - ref.current!.plyr.media = video + const ref = useRef(null) + const hls = useRef(new Hls()) - hls.on(Hls.Events.MANIFEST_PARSED, () => { - // eslint-disable-next-line @typescript-eslint/no-extra-semi - ;(ref.current!.plyr as PlyrInstance).play() - }) + useEffect(() => { + if (source && Hls.isSupported()) { + const video = document.querySelector('#video-cover') as HTMLVideoElement + hls.current.loadSource(source) + hls.current.attachMedia(video) } - loadVideo() - }) + }, [source]) return (
- +
) } @@ -82,7 +59,6 @@ const Cover = memo( Cover {videoCover && ( diff --git a/packages/web/package.json b/packages/web/package.json index b84229c..2f1e3c8 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -26,6 +26,7 @@ "@sentry/react": "^6.19.7", "@sentry/tracing": "^6.19.7", "@tanstack/react-virtual": "3.0.0-beta.2", + "ahooks": "^3.4.1", "axios": "^0.27.2", "color.js": "^1.2.0", "colord": "^2.9.2", diff --git a/packages/web/pages/New/Browse.tsx b/packages/web/pages/New/Browse.tsx new file mode 100644 index 0000000..8e55658 --- /dev/null +++ b/packages/web/pages/New/Browse.tsx @@ -0,0 +1,94 @@ +import Tabs from '@/web/components/New/Tabs' +import { + fetchDailyRecommendPlaylists, + fetchRecommendedPlaylists, +} from '@/web/api/playlist' +import { PlaylistApiNames } from '@/shared/api/Playlists' +import { useState } from 'react' +import { useQuery } from 'react-query' +import CoverRowVirtual from '@/web/components/New/CoverRowVirtual' +import PageTransition from '@/web/components/New/PageTransition' +import { playerWidth, topbarHeight } from '@/web/utils/const' +import { cx, css } from '@emotion/css' +import CoverRow from '@/web/components/New/CoverRow' +import topbarBackground from '@/web/assets/images/topbar-background.png' + +const reactQueryOptions = { + refetchOnWindowFocus: false, + refetchInterval: 1000 * 60 * 60, // 1 hour +} + +const Recommend = () => { + const { data: dailyRecommendPlaylists } = useQuery( + PlaylistApiNames.FetchDailyRecommendPlaylists, + () => fetchDailyRecommendPlaylists(), + reactQueryOptions + ) + const { data: recommendedPlaylists } = useQuery( + [PlaylistApiNames.FetchRecommendedPlaylists, { limit: 200 }], + () => fetchRecommendedPlaylists({ limit: 200 }), + reactQueryOptions + ) + const playlists = [ + ...(dailyRecommendPlaylists?.recommend || []), + ...(recommendedPlaylists?.result || []), + ] + + // return ( + // + // ) + + return +} + +const All = () => { + return
+} + +const categories = [ + { id: 'recommend', name: 'Recommend', component: }, + { id: 'all', name: 'All', component: }, + { id: 'featured', name: 'Featured', component: }, + { id: 'official', name: 'Official', component: }, + { id: 'charts', name: 'Charts', component: }, +] +const categoriesKeys = categories.map(c => c.id) +type Key = typeof categoriesKeys[number] + +const Browse = () => { + const [active, setActive] = useState('recommend') + + return ( + + {/* Topbar background */} + + + setActive(category)} + className='sticky top-0 z-10 px-2.5 lg:px-0' + /> + +
+ {categories.find(c => c.id === active)?.component} +
+
+ ) +} + +export default Browse diff --git a/packages/web/pages/New/Discover.tsx b/packages/web/pages/New/Discover.tsx index 40227de..62034c8 100644 --- a/packages/web/pages/New/Discover.tsx +++ b/packages/web/pages/New/Discover.tsx @@ -102,7 +102,7 @@ const Discover = () => { return ( -
+
diff --git a/packages/web/pages/New/My.tsx b/packages/web/pages/New/My.tsx deleted file mode 100644 index a567c14..0000000 --- a/packages/web/pages/New/My.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { css, cx } from '@emotion/css' -import PlayLikedSongsCard from '@/web/components/New/PlayLikedSongsCard' -import PageTransition from '@/web/components/New/PageTransition' -import useUserArtists from '@/web/api/hooks/useUserArtists' -import ArtistRow from '@/web/components/New/ArtistRow' -import Tabs from '@/web/components/New/Tabs' -import { useMemo, useState } from 'react' -import CoverRow from '@/web/components/New/CoverRow' -import useUserPlaylists from '@/web/api/hooks/useUserPlaylists' -import useUserAlbums from '@/web/api/hooks/useUserAlbums' -import useUserListenedRecords from '@/web/api/hooks/useUserListenedRecords' -import useArtists from '@/web/api/hooks/useArtists' - -const tabs = [ - { - id: 'playlists', - name: 'Playlists', - }, - { - id: 'albums', - name: 'Albums', - }, - { - id: 'artists', - name: 'Artists', - }, - { - id: 'videos', - name: 'Videos', - }, -] - -const Albums = () => { - const { data: albums } = useUserAlbums() - - return -} - -const Playlists = () => { - const { data: playlists } = useUserPlaylists() - return ( - - ) -} - -const Collections = () => { - // const { data: artists } = useUserArtists() - const [selectedTab, setSelectedTab] = useState(tabs[0].id) - - return ( -
- setSelectedTab(id)} - className='px-2.5 lg:px-0' - /> - {selectedTab === 'albums' && } - {selectedTab === 'playlists' && } -
- ) -} - -const RecentlyListened = () => { - const { data: listenedRecords } = useUserListenedRecords({ type: 'week' }) - const recentListenedArtistsIDs = useMemo(() => { - const artists: { - id: number - playCount: number - }[] = [] - listenedRecords?.weekData?.forEach(record => { - const artist = record.song.ar[0] - const index = artists.findIndex(a => a.id === artist.id) - if (index === -1) { - artists.push({ - id: artist.id, - playCount: record.playCount, - }) - } else { - artists[index].playCount += record.playCount - } - }) - - return artists - .sort((a, b) => b.playCount - a.playCount) - .slice(0, 5) - .map(artist => artist.id) - }, [listenedRecords]) - const { data: recentListenedArtists } = useArtists(recentListenedArtistsIDs) - - return ( - a.artist)} - placeholderRow={1} - title='RECENTLY LISTENED' - /> - ) -} - -const My = () => { - return ( - -
- - - -
-
- ) -} - -export default My diff --git a/packages/web/pages/New/My/Collections.tsx b/packages/web/pages/New/My/Collections.tsx new file mode 100644 index 0000000..71c40ef --- /dev/null +++ b/packages/web/pages/New/My/Collections.tsx @@ -0,0 +1,58 @@ +import { css, cx } from '@emotion/css' +import useUserArtists from '@/web/api/hooks/useUserArtists' +import Tabs from '@/web/components/New/Tabs' +import { useMemo, useState } from 'react' +import CoverRow from '@/web/components/New/CoverRow' +import useUserPlaylists from '@/web/api/hooks/useUserPlaylists' +import useUserAlbums from '@/web/api/hooks/useUserAlbums' + +const tabs = [ + { + id: 'playlists', + name: 'Playlists', + }, + { + id: 'albums', + name: 'Albums', + }, + { + id: 'artists', + name: 'Artists', + }, + { + id: 'videos', + name: 'Videos', + }, +] + +const Albums = () => { + const { data: albums } = useUserAlbums() + + return +} + +const Playlists = () => { + const { data: playlists } = useUserPlaylists() + const p = useMemo(() => playlists?.playlist?.slice(1), [playlists]) + return +} + +const Collections = () => { + // const { data: artists } = useUserArtists() + const [selectedTab, setSelectedTab] = useState(tabs[0].id) + + return ( +
+ setSelectedTab(id)} + className='px-2.5 lg:px-0' + /> + {selectedTab === 'albums' && } + {selectedTab === 'playlists' && } +
+ ) +} + +export default Collections diff --git a/packages/web/pages/New/My/My.tsx b/packages/web/pages/New/My/My.tsx new file mode 100644 index 0000000..ff79e2a --- /dev/null +++ b/packages/web/pages/New/My/My.tsx @@ -0,0 +1,19 @@ +import { css, cx } from '@emotion/css' +import PlayLikedSongsCard from './PlayLikedSongsCard' +import PageTransition from '@/web/components/New/PageTransition' +import RecentlyListened from './RecentlyListened' +import Collections from './Collections' + +const My = () => { + return ( + +
+ + + +
+
+ ) +} + +export default My diff --git a/packages/web/pages/New/My/PlayLikedSongsCard.tsx b/packages/web/pages/New/My/PlayLikedSongsCard.tsx new file mode 100644 index 0000000..0f7fd6a --- /dev/null +++ b/packages/web/pages/New/My/PlayLikedSongsCard.tsx @@ -0,0 +1,152 @@ +import useLyric from '@/web/api/hooks/useLyric' +import usePlaylist from '@/web/api/hooks/usePlaylist' +import useUserPlaylists from '@/web/api/hooks/useUserPlaylists' +import { player } from '@/web/store' +import { sample, chunk, sampleSize } from 'lodash-es' +import { css, cx } from '@emotion/css' +import { useState, useEffect, useMemo, useCallback, memo } from 'react' +import toast from 'react-hot-toast' +import { useNavigate } from 'react-router-dom' +import Icon from '@/web/components/Icon' +import { lyricParser } from '@/web/utils/lyric' +import Image from '@/web/components/New/Image' +import { resizeImage } from '@/web/utils/common' +import { breakpoint as bp } from '@/web/utils/const' + +const Lyrics = ({ tracksIDs }: { tracksIDs: number[] }) => { + const [id, setId] = useState(0) + + useEffect(() => { + if (id === 0) { + setId(sample(tracksIDs) || 0) + } + }, [id, tracksIDs]) + + const { data: lyric } = useLyric({ id }) + + const lyricLines = useMemo(() => { + if (!lyric?.lrc?.lyric) return [] + + const parsedLyrics = lyricParser(lyric) + + const lines = parsedLyrics.lyric.map(line => line.content) + + return sample(chunk(lines, 4)) ?? [] + }, [lyric]) + + return ( +
+ {lyricLines.map((line, index) => ( +
{line}
+ ))} +
+ ) +} + +const Covers = memo(({ tracks }: { tracks: Track[] }) => { + return ( +
+ {tracks.map(track => ( + + ))} +
+ ) +}) +Covers.displayName = 'Covers' + +const PlayLikedSongsCard = () => { + const navigate = useNavigate() + + const { data: playlists } = useUserPlaylists() + + const { data: likedSongsPlaylist } = usePlaylist({ + id: playlists?.playlist?.[0].id ?? 0, + }) + + const handlePlay = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation() + if (!likedSongsPlaylist?.playlist.id) { + toast('无法播放歌单') + return + } + player.playPlaylist(likedSongsPlaylist.playlist.id) + }, + [likedSongsPlaylist?.playlist.id] + ) + + const [sampledTracks, setSampledTracks] = useState([]) + useEffect(() => { + const tracks = likedSongsPlaylist?.playlist?.tracks + if (!sampledTracks.length && tracks?.length) { + setSampledTracks(sampleSize(tracks, 3)) + } + }, [likedSongsPlaylist?.playlist?.tracks, sampledTracks]) + + return ( +
+ {/* Lyrics and Covers */} +
+ t.id)} /> + +
+ + {/* Buttons */} +
+ + +
+
+ ) +} + +export default PlayLikedSongsCard diff --git a/packages/web/pages/New/My/RecentlyListened.tsx b/packages/web/pages/New/My/RecentlyListened.tsx new file mode 100644 index 0000000..5008450 --- /dev/null +++ b/packages/web/pages/New/My/RecentlyListened.tsx @@ -0,0 +1,42 @@ +import useUserListenedRecords from '@/web/api/hooks/useUserListenedRecords' +import useArtists from '@/web/api/hooks/useArtists' +import { useMemo } from 'react' +import ArtistRow from '@/web/components/New/ArtistRow' + +const RecentlyListened = () => { + const { data: listenedRecords } = useUserListenedRecords({ type: 'week' }) + const recentListenedArtistsIDs = useMemo(() => { + const artists: { + id: number + playCount: number + }[] = [] + listenedRecords?.weekData?.forEach(record => { + const artist = record.song.ar[0] + const index = artists.findIndex(a => a.id === artist.id) + if (index === -1) { + artists.push({ + id: artist.id, + playCount: record.playCount, + }) + } else { + artists[index].playCount += record.playCount + } + }) + + return artists + .sort((a, b) => b.playCount - a.playCount) + .slice(0, 5) + .map(artist => artist.id) + }, [listenedRecords]) + const { data: recentListenedArtists } = useArtists(recentListenedArtistsIDs) + const artist = useMemo( + () => recentListenedArtists?.map(a => a.artist), + [recentListenedArtists] + ) + + return ( + + ) +} + +export default RecentlyListened diff --git a/packages/web/pages/New/My/index.tsx b/packages/web/pages/New/My/index.tsx new file mode 100644 index 0000000..56c0101 --- /dev/null +++ b/packages/web/pages/New/My/index.tsx @@ -0,0 +1,3 @@ +import My from './My' + +export default My diff --git a/packages/web/styles/global.css b/packages/web/styles/global.css index f0db938..c090227 100644 --- a/packages/web/styles/global.css +++ b/packages/web/styles/global.css @@ -59,6 +59,14 @@ word-break: break-all; -webkit-line-clamp: 3; } + + .line-clamp-4 { + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; + word-break: break-all; + -webkit-line-clamp: 4; + } } @font-face { @@ -107,9 +115,12 @@ input { } body { + overscroll-behavior: contain; } html { + background-color: black; + min-height: calc(100% + env(safe-area-inset-top)); } button, @@ -135,8 +146,3 @@ a { .no-scrollbar::-webkit-scrollbar { display: none; } - -html { - background-color: black; - min-height: calc(100% + env(safe-area-inset-top)); -} diff --git a/packages/web/utils/player.ts b/packages/web/utils/player.ts index 35fa59c..ed64a0e 100644 --- a/packages/web/utils/player.ts +++ b/packages/web/utils/player.ts @@ -215,8 +215,12 @@ export class Player { */ private async _fetchAudioSource(trackID: TrackID) { const response = await fetchAudioSourceWithReactQuery({ id: trackID }) + let audio = response.data?.[0]?.url + if (audio && audio.includes('126.net')) { + audio = audio.replace('http://', 'https://') + } return { - audio: response.data?.[0]?.url, + audio, id: trackID, } } diff --git a/packages/web/vite.config.ts b/packages/web/vite.config.ts index c8a6b50..7dd9297 100644 --- a/packages/web/vite.config.ts +++ b/packages/web/vite.config.ts @@ -14,10 +14,15 @@ const IS_ELECTRON = process.env.IS_ELECTRON * @see https://vitejs.dev/config/ */ export default defineConfig({ + clearScreen: IS_ELECTRON ? false : true, mode: process.env.NODE_ENV, root: './', base: '/', - clearScreen: IS_ELECTRON ? false : true, + resolve: { + alias: { + '@': join(__dirname, '../'), + }, + }, plugins: [ react(), @@ -75,11 +80,6 @@ export default defineConfig({ ], }, }, - resolve: { - alias: { - '@': join(__dirname, '../'), - }, - }, server: { port: Number(process.env['ELECTRON_WEB_SERVER_PORT'] || 42710), strictPort: IS_ELECTRON ? true : false, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8a3ec9c..0bd3194 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -130,6 +130,7 @@ importers: '@typescript-eslint/parser': ^5.27.0 '@vitejs/plugin-react': ^1.3.1 '@vitest/ui': ^0.12.10 + ahooks: ^3.4.1 autoprefixer: ^10.4.5 axios: ^0.27.2 c8: ^7.11.3 @@ -176,6 +177,7 @@ importers: '@sentry/react': 6.19.7_react@18.1.0 '@sentry/tracing': 6.19.7 '@tanstack/react-virtual': 3.0.0-beta.2 + ahooks: 3.4.1_react@18.1.0 axios: 0.27.2 color.js: 1.2.0 colord: 2.9.2 @@ -7549,6 +7551,27 @@ packages: indent-string: 4.0.0 dev: true + /ahooks-v3-count/1.0.0: + resolution: {integrity: sha512-V7uUvAwnimu6eh/PED4mCDjE7tokeZQLKlxg9lCTMPhN+NjsSbtdacByVlR1oluXQzD3MOw55wylDmQo4+S9ZQ==} + dev: false + + /ahooks/3.4.1_react@18.1.0: + resolution: {integrity: sha512-PMxCDO6JsFdNrAyN3cW1J/2qt/vy2EJ/9KhxGOxj41hJhQddjgaBJjZKf/FrrnZmL+3yGPioZtbC4C7q7ru3yA==} + engines: {node: '>=8.0.0'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@types/js-cookie': 2.2.7 + ahooks-v3-count: 1.0.0 + dayjs: 1.11.2 + intersection-observer: 0.12.0 + js-cookie: 2.2.1 + lodash: 4.17.21 + react: 18.1.0 + resize-observer-polyfill: 1.5.1 + screenfull: 5.2.0 + dev: false + /airbnb-js-shims/2.2.1: resolution: {integrity: sha512-wJNXPH66U2xjgo1Zwyjf9EydvJ2Si94+vSdk6EERcBfB2VZkeltpqIats0cqIZMLCXP3zcyaUKGYQeIBT6XjsQ==} dependencies: @@ -12937,6 +12960,10 @@ packages: engines: {node: '>= 0.10'} dev: true + /intersection-observer/0.12.0: + resolution: {integrity: sha512-2Vkz8z46Dv401zTWudDGwO7KiGHNDkMv417T5ItcNYfmvHR/1qCTVBO9vwH8zZmQ0WkA/1ARwpysR9bsnop4NQ==} + dev: false + /ip/1.1.8: resolution: {integrity: sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==} @@ -13947,7 +13974,6 @@ packages: /lodash/4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - dev: true /log-symbols/4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} diff --git a/vercel.json b/vercel.json index 3d26fe2..95c1861 100644 --- a/vercel.json +++ b/vercel.json @@ -7,6 +7,10 @@ "source": "/netease/:match*", "destination": "http://168.138.40.199:12835/:match*" }, - {"source": "/(.*)", "destination": "/"} + { + "source": "/yesplaymusic/:match*", + "destination": "http://168.138.40.199:51324/yesplaymusic/:match*" + }, + { "source": "/(.*)", "destination": "/" } ] }