From 8f4c3d8e5b3e04a2d16265b376ac96a28d3b5a61 Mon Sep 17 00:00:00 2001 From: qier222 Date: Sun, 12 Jun 2022 15:29:14 +0800 Subject: [PATCH] feat: updates --- packages/electron/main/appleMusic.ts | 84 ++++++++++ packages/electron/main/cache.ts | 21 ++- packages/electron/main/db.ts | 9 +- packages/electron/main/ipcMain.ts | 27 +++ packages/electron/main/rendererPreload.ts | 1 + packages/electron/migrations/init.sql | 6 +- packages/electron/package.json | 3 +- packages/shared/CacheAPIs.ts | 11 +- packages/shared/IpcChannels.ts | 6 + packages/web/components/New/Image.tsx | 41 +++-- packages/web/components/New/Main.tsx | 1 + packages/web/components/New/PlayingNext.tsx | 99 ++++++++--- .../web/components/New/TrackListHeader.tsx | 158 ++++++++++++++---- packages/web/global.d.ts | 4 + packages/web/hooks/useVideoCover.ts | 43 +++++ packages/web/index.html | 3 +- packages/web/main.tsx | 16 +- packages/web/package.json | 4 + packages/web/pages/New/Album.tsx | 1 + packages/web/pages/New/My.tsx | 5 +- packages/web/utils/common.ts | 4 +- packages/web/utils/const.ts | 6 + packages/web/vite.config.ts | 5 + pnpm-lock.yaml | 107 +++++++++++- 24 files changed, 572 insertions(+), 93 deletions(-) create mode 100644 packages/electron/main/appleMusic.ts create mode 100644 packages/web/hooks/useVideoCover.ts diff --git a/packages/electron/main/appleMusic.ts b/packages/electron/main/appleMusic.ts new file mode 100644 index 0000000..73d2585 --- /dev/null +++ b/packages/electron/main/appleMusic.ts @@ -0,0 +1,84 @@ +import { logger } from '@sentry/utils' +import axios from 'axios' + +// 'https://mvod.itunes.apple.com/itunes-assets/HLSMusic116/v4/de/52/95/de52957b-fcf1-ae96-b114-0445cb8c41d4/P359420813_default.m3u8' + +const searchAlbum = async ( + keyword: string +): Promise< + | { + id: string + href: string + attributes: { + artistName: string + url: string + name: string + editorialNotes?: { + standard: string + short: string + } + } + } + | undefined +> => { + const r = await axios.get( + `https://amp-api.music.apple.com/v1/catalog/cn/search`, + { + params: { + term: keyword, + l: 'zh-cn', + platform: 'web', + types: 'albums', + limit: 1, + }, + headers: { + authorization: 'Bearer xxxxxx', // required token + }, + } + ) + + return r.data?.results?.albums?.data?.[0] +} + +export const getCoverVideo = async ({ + name, + artists, +}: { + name: string + artists: string[] +}): Promise => { + const keyword = `${artists.join(' ')} ${name}` + logger.debug(`[appleMusic] getCoverVideo: ${keyword}`) + const album = await searchAlbum(keyword).catch(e => { + console.log(e) + logger.debug('[appleMusic] Search album error', e) + }) + + const url = album?.attributes.url + + if (!url) { + logger.info('[appleMusic] no url') + return + } + + let { data: html } = await axios.get(url) + if (!html) return + + const regex = + /', '') + html = JSON.parse(html) + const data = JSON.parse(html[Object.keys(html)[1]]) + const m3u8 = + data?.d?.[0]?.attributes?.editorialVideo?.motionSquareVideo1x1?.video + + logger.debug(`[appleMusic] ${m3u8}`) + + return m3u8 +} diff --git a/packages/electron/main/cache.ts b/packages/electron/main/cache.ts index 1b07081..d1156aa 100644 --- a/packages/electron/main/cache.ts +++ b/packages/electron/main/cache.ts @@ -31,8 +31,9 @@ class Cache { break } case APIs.Track: { - if (!data.songs) return - const tracks = (data as FetchTracksResponse).songs.map(t => ({ + const res = data as FetchTracksResponse + if (!res.songs) return + const tracks = res.songs.map(t => ({ id: t.id, json: JSON.stringify(t), updatedAt: Date.now(), @@ -106,6 +107,16 @@ class Cache { db.upsert(Tables.CoverColor, { id: data.id, color: data.color, + queriedAt: Date.now(), + }) + break + } + case APIs.VideoCover: { + if (!data.id) return + db.upsert(Tables.VideoCover, { + id: data.id, + url: data.url || 'no', + queriedAt: Date.now(), }) } } @@ -196,6 +207,10 @@ class Cache { if (isNaN(Number(params?.id))) return return db.find(Tables.CoverColor, params.id)?.color } + case APIs.VideoCover: { + if (isNaN(Number(params?.id))) return + return db.find(Tables.VideoCover, params.id)?.url + } } } @@ -278,7 +293,7 @@ class Cache { br, type: type as TablesStructures[Tables.Audio]['type'], source, - updatedAt: Date.now(), + queriedAt: Date.now(), }) log.info(`[cache] cacheAudio ${id}-${br}.${type}`) diff --git a/packages/electron/main/db.ts b/packages/electron/main/db.ts index 366566a..90aec2b 100644 --- a/packages/electron/main/db.ts +++ b/packages/electron/main/db.ts @@ -18,6 +18,7 @@ export const enum Tables { AccountData = 'AccountData', CoverColor = 'CoverColor', AppData = 'AppData', + VideoCover = 'VideoCover', } interface CommonTableStructure { id: number @@ -50,16 +51,22 @@ export interface TablesStructures { | 'qq' | 'bilibili' | 'joox' - updatedAt: number + queriedAt: number } [Tables.CoverColor]: { id: number color: string + queriedAt: number } [Tables.AppData]: { id: 'appVersion' | 'skippedVersion' value: string } + [Tables.VideoCover]: { + id: number + url: string + queriedAt: number + } } type TableNames = keyof TablesStructures diff --git a/packages/electron/main/ipcMain.ts b/packages/electron/main/ipcMain.ts index d9e737c..dc191f4 100644 --- a/packages/electron/main/ipcMain.ts +++ b/packages/electron/main/ipcMain.ts @@ -12,6 +12,7 @@ import { Thumbar } from './windowsTaskbar' import fastFolderSize from 'fast-folder-size' import path from 'path' import prettyBytes from 'pretty-bytes' +import { getCoverVideo } from './appleMusic' const on = ( channel: T, @@ -20,6 +21,16 @@ const on = ( ipcMain.on(channel, listener) } +const handle = ( + channel: T, + listener: ( + event: Electron.IpcMainInvokeEvent, + params: IpcChannelsParams[T] + ) => void +) => { + return ipcMain.handle(channel, listener) +} + export function initIpcMain( win: BrowserWindow | null, tray: YPMTray | null, @@ -143,6 +154,22 @@ function initOtherIpcMain() { ) }) + /** + * 缓存动态专辑封面 + */ + on(IpcChannels.SetVideoCover, (event, args) => { + const { id, url } = args + cache.set(APIs.VideoCover, { id, url }) + }) + + /** + * 获取动态专辑封面 + */ + on(IpcChannels.GetVideoCover, (event, args) => { + const { id } = args + event.returnValue = cache.get(APIs.VideoCover, { id }) + }) + /** * 导出tables到json文件,方便查看table大小(dev环境) */ diff --git a/packages/electron/main/rendererPreload.ts b/packages/electron/main/rendererPreload.ts index d920c51..c0dc083 100644 --- a/packages/electron/main/rendererPreload.ts +++ b/packages/electron/main/rendererPreload.ts @@ -11,6 +11,7 @@ contextBridge.exposeInMainWorld('log', log) contextBridge.exposeInMainWorld('ipcRenderer', { sendSync: ipcRenderer.sendSync, + invoke: ipcRenderer.invoke, send: ipcRenderer.send, on: ( channel: IpcChannels, diff --git a/packages/electron/migrations/init.sql b/packages/electron/migrations/init.sql index 08906b2..da49040 100644 --- a/packages/electron/migrations/init.sql +++ b/packages/electron/migrations/init.sql @@ -2,9 +2,11 @@ CREATE TABLE IF NOT EXISTS "AccountData" ("id" text NOT NULL,"json" text NOT NUL CREATE TABLE IF NOT EXISTS "Album" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id)); CREATE TABLE IF NOT EXISTS "ArtistAlbum" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id)); CREATE TABLE IF NOT EXISTS "Artist" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id)); -CREATE TABLE IF NOT EXISTS "Audio" ("id" integer NOT NULL,"br" int NOT NULL,"type" text NOT NULL,"source" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id)); CREATE TABLE IF NOT EXISTS "Lyric" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" integer NOT NULL, PRIMARY KEY (id)); CREATE TABLE IF NOT EXISTS "Playlist" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id)); CREATE TABLE IF NOT EXISTS "Track" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id)); -CREATE TABLE IF NOT EXISTS "CoverColor" ("id" integer NOT NULL,"color" text NOT NULL, PRIMARY KEY (id)); + CREATE TABLE IF NOT EXISTS "AppData" ("id" text NOT NULL,"value" text, PRIMARY KEY (id)); +CREATE TABLE IF NOT EXISTS "CoverColor" ("id" integer NOT NULL,"color" text NOT NULL, "queriedAt" int NOT NULL, PRIMARY KEY (id)); +CREATE TABLE IF NOT EXISTS "Audio" ("id" integer NOT NULL,"br" int NOT NULL,"type" text NOT NULL,"source" text NOT NULL,"updatedAt" int NOT NULL, "queriedAt" int NOT NULL, PRIMARY KEY (id)); +CREATE TABLE IF NOT EXISTS "VideoCover" ("id" integer NOT NULL,"url" text NOT NULL,"updatedAt" int NOT NULL, "queriedAt" int NOT NULL, PRIMARY KEY (id)); diff --git a/packages/electron/package.json b/packages/electron/package.json index 65e6b51..0d52d34 100644 --- a/packages/electron/package.json +++ b/packages/electron/package.json @@ -9,7 +9,7 @@ "dev": "node scripts/build.main.mjs --watch", "build": "node scripts/build.main.mjs", "pack": "electron-builder build -c .electron-builder.config.js", - "test:types": "tsc --noEmit --project src/main/tsconfig.json", + "test:types": "tsc --noEmit --project ./tsconfig.json", "lint": "eslint --ext .ts,.js ./", "format": "prettier --write './**/*.{ts,js,tsx,jsx}'" }, @@ -29,6 +29,7 @@ "electron-store": "^8.0.1", "express": "^4.18.1", "fast-folder-size": "^1.7.0", + "m3u8-parser": "^4.7.1", "pretty-bytes": "^6.0.0" }, "devDependencies": { diff --git a/packages/shared/CacheAPIs.ts b/packages/shared/CacheAPIs.ts index 9b14e4c..e358f5a 100644 --- a/packages/shared/CacheAPIs.ts +++ b/packages/shared/CacheAPIs.ts @@ -21,7 +21,6 @@ export const enum APIs { Album = 'album', Artist = 'artists', ArtistAlbum = 'artist/album', - CoverColor = 'cover_color', Likelist = 'likelist', Lyric = 'lyric', Personalized = 'personalized', @@ -33,13 +32,16 @@ export const enum APIs { UserAlbums = 'album/sublist', UserArtists = 'artist/sublist', UserPlaylist = 'user/playlist', + + // not netease api + CoverColor = 'cover_color', + VideoCover = 'video_cover', } export interface APIsParams { [APIs.Album]: { id: number } [APIs.Artist]: { id: number } [APIs.ArtistAlbum]: { id: number } - [APIs.CoverColor]: { id: number } [APIs.Likelist]: void [APIs.Lyric]: { id: number } [APIs.Personalized]: void @@ -51,13 +53,14 @@ export interface APIsParams { [APIs.UserAlbums]: void [APIs.UserArtists]: void [APIs.UserPlaylist]: void + [APIs.CoverColor]: { id: number } + [APIs.VideoCover]: { id: number } } export interface APIsResponse { [APIs.Album]: FetchAlbumResponse [APIs.Artist]: FetchArtistResponse [APIs.ArtistAlbum]: FetchArtistAlbumsResponse - [APIs.CoverColor]: string | undefined [APIs.Likelist]: FetchUserLikedTracksIDsResponse [APIs.Lyric]: FetchLyricResponse [APIs.Personalized]: FetchRecommendedPlaylistsResponse @@ -69,4 +72,6 @@ export interface APIsResponse { [APIs.UserAlbums]: FetchUserAlbumsResponse [APIs.UserArtists]: FetchUserArtistsResponse [APIs.UserPlaylist]: FetchUserPlaylistsResponse + [APIs.CoverColor]: string | undefined + [APIs.VideoCover]: string | undefined } diff --git a/packages/shared/IpcChannels.ts b/packages/shared/IpcChannels.ts index 2f72e37..d6130bb 100644 --- a/packages/shared/IpcChannels.ts +++ b/packages/shared/IpcChannels.ts @@ -23,6 +23,8 @@ export const enum IpcChannels { SyncSettings = 'SyncSettings', GetAudioCacheSize = 'GetAudioCacheSize', ResetWindowSize = 'ResetWindowSize', + GetVideoCover = 'GetVideoCover', + SetVideoCover = 'SetVideoCover', } // ipcMain.on params @@ -58,6 +60,8 @@ export interface IpcChannelsParams { [IpcChannels.SyncSettings]: Store['settings'] [IpcChannels.GetAudioCacheSize]: void [IpcChannels.ResetWindowSize]: void + [IpcChannels.GetVideoCover]: { id: number } + [IpcChannels.SetVideoCover]: { id: number; url: string } } // ipcRenderer.on params @@ -79,4 +83,6 @@ export interface IpcChannelsReturns { [IpcChannels.Like]: void [IpcChannels.Repeat]: RepeatMode [IpcChannels.GetAudioCacheSize]: void + [IpcChannels.GetVideoCover]: string | undefined + [IpcChannels.SetVideoCover]: void } diff --git a/packages/web/components/New/Image.tsx b/packages/web/components/New/Image.tsx index bfe9db1..d82df0f 100644 --- a/packages/web/components/New/Image.tsx +++ b/packages/web/components/New/Image.tsx @@ -22,7 +22,7 @@ const Image = ({ className?: string alt: string lazyLoad?: boolean - placeholder?: 'artist' | 'album' | 'playlist' | 'podcast' | 'blank' | null + placeholder?: 'artist' | 'album' | 'playlist' | 'podcast' | 'blank' | false onClick?: (e: React.MouseEvent) => void onMouseOver?: (e: React.MouseEvent) => void animation?: boolean @@ -48,6 +48,7 @@ const Image = ({ ? { animate, initial: { opacity: 0 }, + exit: { opacity: 0 }, transition, } : {} @@ -60,22 +61,30 @@ const Image = ({ : {} return ( -
+
{/* Image */} - + + + {/* Placeholder / Error fallback */} diff --git a/packages/web/components/New/Main.tsx b/packages/web/components/New/Main.tsx index e106a3f..6168614 100644 --- a/packages/web/components/New/Main.tsx +++ b/packages/web/components/New/Main.tsx @@ -4,6 +4,7 @@ import Router from './Router' const Main = () => { return (
{ return ( @@ -46,13 +50,13 @@ const Track = ({ return ( { if (e.detail === 2 && track?.id) player.playTrack(track.id) }} @@ -62,6 +66,8 @@ const Track = ({ alt='Cover' className='mr-4 aspect-square h-14 w-14 flex-shrink-0 rounded-12' src={resizeImage(track?.al?.picUrl || '', 'sm')} + animation={false} + placeholder={false} /> {/* Track info */} @@ -93,41 +99,82 @@ const Track = ({ ) } -const PlayingNext = ({ className }: { className?: string }) => { +const TrackList = ({ className }: { className?: string }) => { const { trackList, trackIndex, state } = useSnapshot(player) - const { data: tracks } = useTracks({ ids: trackList }) + const { data: tracksRaw } = useTracks({ ids: trackList }) + const tracks = tracksRaw?.songs || [] + const parentRef = useRef(null) + const { height } = useWindowSize() + + const listHeight = height - topbarHeight - playerWidth - 24 - 20 // 24是封面与底部间距,20是list与封面间距 + + const rowVirtualizer = useVirtualizer({ + count: tracks.length, + getScrollElement: () => parentRef.current, + estimateSize: () => 76, + overscan: 5, + }) + return ( <> -
-
- - - {tracks?.songs?.map((track, index) => ( +
+ {rowVirtualizer.getVirtualItems().map((row: any) => ( +
- ))} - - {(tracks?.songs?.length || 0) >= 4 && ( -
- )} - - +
+ ))} +
+ + {/* 底部渐变遮罩 */} +
+ + ) +} + +const PlayingNext = ({ className }: { className?: string }) => { + return ( + <> +
+ ) } diff --git a/packages/web/components/New/TrackListHeader.tsx b/packages/web/components/New/TrackListHeader.tsx index e9b8558..fbe4620 100644 --- a/packages/web/components/New/TrackListHeader.tsx +++ b/packages/web/components/New/TrackListHeader.tsx @@ -1,10 +1,134 @@ -import { formatDate, formatDuration, resizeImage } from '@/web/utils/common' +import { + formatDate, + formatDuration, + isIOS, + isSafari, + resizeImage, +} from '@/web/utils/common' import { css, cx } from '@emotion/css' import Icon from '@/web/components/Icon' import dayjs from 'dayjs' -import { useMemo } from 'react' 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' +import { injectGlobal } from '@emotion/css' + +injectGlobal` + .plyr__video-wrapper, + .plyr--video { + background-color: transparent !important; + } +` + +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 + + hls.on(Hls.Events.MANIFEST_PARSED, () => { + // eslint-disable-next-line @typescript-eslint/no-extra-semi + ;(ref.current!.plyr as PlyrInstance).play() + }) + } + loadVideo() + }) + + return ( +
+ +
+ ) +} + +const Cover = memo( + ({ album, playlist }: { album?: Album; playlist?: Playlist }) => { + const isMobile = useIsMobile() + const { data: videoCover } = useVideoCover({ + id: album?.id, + name: album?.name, + artist: album?.artist.name, + }) + const cover = album?.picUrl || playlist?.coverImgUrl || '' + + return ( + <> +
+ Cover + + {videoCover && ( + + {isSafari ? ( + + ) : ( + + )} + + )} +
+ + {/* Blur bg */} + {!isMobile && ( + + )} + + ) + } +) +Cover.displayName = 'Cover' const TrackListHeader = ({ album, @@ -15,18 +139,16 @@ const TrackListHeader = ({ playlist?: Playlist onPlay: () => void }) => { + const isMobile = useIsMobile() const albumDuration = useMemo(() => { const duration = album?.songs?.reduce((acc, cur) => acc + cur.dt, 0) || 0 return formatDuration(duration, 'en', 'hh[hr] mm[min]') }, [album?.songs]) - const isMobile = useIsMobile() - - const cover = album?.picUrl || playlist?.coverImgUrl || '' return (
- {/* Cover */} - Cover - - {/* Blur bg */} - {!isMobile && ( - - )} +
diff --git a/packages/web/global.d.ts b/packages/web/global.d.ts index 8fc1bc6..d7799f3 100644 --- a/packages/web/global.d.ts +++ b/packages/web/global.d.ts @@ -10,6 +10,10 @@ declare global { channel: T, params?: IpcChannelsParams[T] ) => IpcChannelsReturns[T] + invoke: ( + channel: T, + params?: IpcChannelsParams[T] + ) => Promise send: ( channel: T, params?: IpcChannelsParams[T] diff --git a/packages/web/hooks/useVideoCover.ts b/packages/web/hooks/useVideoCover.ts new file mode 100644 index 0000000..6c2c2a8 --- /dev/null +++ b/packages/web/hooks/useVideoCover.ts @@ -0,0 +1,43 @@ +import { IpcChannels } from '@/shared/IpcChannels' +import axios from 'axios' +import { useQuery } from 'react-query' + +export default function useVideoCover(props: { + id?: number + name?: string + artist?: string +}) { + const { id, name, artist } = props + return useQuery( + ['useVideoCover', props], + async () => { + if (!id || !name || !artist) return + + const fromCache = window.ipcRenderer?.sendSync( + IpcChannels.GetVideoCover, + { + id, + } + ) + if (fromCache) { + return fromCache === 'no' ? undefined : fromCache + } + + const fromRemote = await axios.get('/yesplaymusic/video-cover', { + params: props, + }) + window.ipcRenderer?.send(IpcChannels.SetVideoCover, { + id, + url: fromRemote.data.url || '', + }) + if (fromRemote?.data?.url) { + return fromRemote.data.url + } + }, + { + enabled: !!id && !!name && !!artist, + refetchOnWindowFocus: false, + refetchInterval: false, + } + ) +} diff --git a/packages/web/index.html b/packages/web/index.html index 1c1cf5b..5245877 100644 --- a/packages/web/index.html +++ b/packages/web/index.html @@ -5,7 +5,8 @@ - + YesPlayMusic diff --git a/packages/web/main.tsx b/packages/web/main.tsx index 52ad3ab..2f33c31 100644 --- a/packages/web/main.tsx +++ b/packages/web/main.tsx @@ -14,6 +14,7 @@ import ReactGA from 'react-ga4' import { ipcRenderer } from './ipcRenderer' import { QueryClientProvider } from 'react-query' import reactQueryClient from '@/web/utils/reactQueryClient' +import ReactDOM from 'react-dom' ReactGA.initialize('G-KMJJCFZDKF') @@ -34,12 +35,23 @@ ipcRenderer() const container = document.getElementById('root') as HTMLElement const root = ReactDOMClient.createRoot(container) -root.render( +// root.render( +// +// +// +// +// +// +// +// ) + +ReactDOM.render( - + , + document.getElementById('root') ) diff --git a/packages/web/package.json b/packages/web/package.json index 39c2dd7..b84229c 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -25,15 +25,19 @@ "@emotion/css": "^11.9.0", "@sentry/react": "^6.19.7", "@sentry/tracing": "^6.19.7", + "@tanstack/react-virtual": "3.0.0-beta.2", "axios": "^0.27.2", "color.js": "^1.2.0", "colord": "^2.9.2", "dayjs": "^1.11.1", "framer-motion": "^6.3.4", + "hls.js": "^1.1.5", "howler": "^2.2.3", "js-cookie": "^3.0.1", "lodash-es": "^4.17.21", "md5": "^2.3.0", + "plyr": "^3.7.2", + "plyr-react": "^5.0.2", "qrcode": "^1.5.0", "react": "^18.1.0", "react-dom": "^18.1.0", diff --git a/packages/web/pages/New/Album.tsx b/packages/web/pages/New/Album.tsx index aed7cfe..815832e 100644 --- a/packages/web/pages/New/Album.tsx +++ b/packages/web/pages/New/Album.tsx @@ -11,6 +11,7 @@ import useArtistAlbums from '@/web/api/hooks/useArtistAlbums' import { css, cx } from '@emotion/css' import CoverRow from '@/web/components/New/CoverRow' import { useMemo } from 'react' +import 'plyr-react/plyr.css' const MoreByArtist = ({ album }: { album?: Album }) => { const { data: albums } = useArtistAlbums({ diff --git a/packages/web/pages/New/My.tsx b/packages/web/pages/New/My.tsx index b15e085..a567c14 100644 --- a/packages/web/pages/New/My.tsx +++ b/packages/web/pages/New/My.tsx @@ -39,7 +39,10 @@ const Albums = () => { const Playlists = () => { const { data: playlists } = useUserPlaylists() return ( - + ) } diff --git a/packages/web/utils/common.ts b/packages/web/utils/common.ts index 3e1d583..c0daab0 100644 --- a/packages/web/utils/common.ts +++ b/packages/web/utils/common.ts @@ -160,7 +160,9 @@ export async function calcCoverColor(coverUrl: string) { } export const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) -export const isSafari = /Safari/.test(navigator.userAgent) +export const isSafari = /^((?!chrome|android).)*safari/i.test( + navigator.userAgent +) export const isPWA = (navigator as any).standalone || window.matchMedia('(display-mode: standalone)').matches diff --git a/packages/web/utils/const.ts b/packages/web/utils/const.ts index aa24372..4cbccd9 100644 --- a/packages/web/utils/const.ts +++ b/packages/web/utils/const.ts @@ -1,4 +1,7 @@ +// 动画曲线 export const ease: [number, number, number, number] = [0.4, 0, 0.2, 1] + +// 屏幕断点 export const breakpoint = { sm: '@media (min-width: 640px)', md: '@media (min-width: 768px)', @@ -6,3 +9,6 @@ export const breakpoint = { xl: '@media (min-width: 1280px)', '2xl': '@media (min-width: 1536px)', } + +export const topbarHeight = 132 // 桌面端顶栏高度 (px) +export const playerWidth = 318 // 桌面端播放器宽度 (px) diff --git a/packages/web/vite.config.ts b/packages/web/vite.config.ts index c613d87..c8a6b50 100644 --- a/packages/web/vite.config.ts +++ b/packages/web/vite.config.ts @@ -91,6 +91,11 @@ export default defineConfig({ changeOrigin: true, rewrite: path => (IS_ELECTRON ? path : path.replace(/^\/netease/, '')), }, + '/yesplaymusic/video-cover': { + target: `http://168.138.40.199:51324`, + // target: `http://127.0.0.1:51324`, + changeOrigin: true, + }, '/yesplaymusic/': { target: `http://127.0.0.1:${ process.env.ELECTRON_DEV_NETEASE_API_PORT || 3000 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd0c285..8a3ec9c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,6 +48,7 @@ importers: express: ^4.18.1 express-fileupload: ^1.4.0 fast-folder-size: ^1.7.0 + m3u8-parser: ^4.7.1 minimist: ^1.2.6 music-metadata: ^7.12.3 NeteaseCloudMusicApi: ^4.6.2 @@ -70,6 +71,7 @@ importers: electron-store: 8.0.1 express: 4.18.1 fast-folder-size: 1.7.0 + m3u8-parser: 4.7.1 NeteaseCloudMusicApi: 4.6.2 pretty-bytes: 6.0.0 devDependencies: @@ -115,6 +117,7 @@ importers: '@storybook/builder-vite': ^0.1.35 '@storybook/react': ^6.5.5 '@storybook/testing-library': ^0.0.11 + '@tanstack/react-virtual': 3.0.0-beta.2 '@testing-library/react': ^13.3.0 '@types/howler': ^2.2.7 '@types/js-cookie': ^3.0.2 @@ -138,12 +141,15 @@ importers: eslint-plugin-react: ^7.30.0 eslint-plugin-react-hooks: ^4.5.0 framer-motion: ^6.3.4 + hls.js: ^1.1.5 howler: ^2.2.3 js-cookie: ^3.0.1 jsdom: ^19.0.0 lodash-es: ^4.17.21 md5: ^2.3.0 open-cli: ^7.0.1 + plyr: ^3.7.2 + plyr-react: ^5.0.2 postcss: ^8.4.14 prettier: '*' prettier-plugin-tailwindcss: ^0.1.11 @@ -169,15 +175,19 @@ importers: '@emotion/css': 11.9.0 '@sentry/react': 6.19.7_react@18.1.0 '@sentry/tracing': 6.19.7 + '@tanstack/react-virtual': 3.0.0-beta.2 axios: 0.27.2 color.js: 1.2.0 colord: 2.9.2 dayjs: 1.11.2 framer-motion: 6.3.10_ef5jwxihqo6n7gxfmzogljlgcm + hls.js: 1.1.5 howler: 2.2.3 js-cookie: 3.0.1 lodash-es: 4.17.21 md5: 2.3.0 + plyr: 3.7.2 + plyr-react: 5.0.2_react@18.1.0 qrcode: 1.5.0 react: 18.1.0 react-dom: 18.1.0_react@18.1.0 @@ -4527,6 +4537,10 @@ packages: resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==} dev: true + /@reach/observe-rect/1.2.0: + resolution: {integrity: sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==} + dev: false + /@rollup/plugin-babel/5.3.1_4kojsos35jimftt7mhjohcqk6y: resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} engines: {node: '>= 10.0.0'} @@ -6386,6 +6400,13 @@ packages: defer-to-connect: 2.0.1 dev: true + /@tanstack/react-virtual/3.0.0-beta.2: + resolution: {integrity: sha512-pwA9URTHYXX/2PgIISoMcf1P77hxf5oI3L/IDQ19Q1xuAc76o2R2CwHv6vvl5fDhwVj5klOfBxJvuT61Lhy9/w==} + engines: {node: '>=12'} + dependencies: + '@reach/observe-rect': 1.2.0 + dev: false + /@testing-library/dom/8.13.0: resolution: {integrity: sha512-9VHgfIatKNXQNaZTtLnalIy0jNZzY35a4S3oi08YAt9Hv1VsfZ/DfA45lM8D/UhtHBGJ4/lGwp0PZkVndRkoOQ==} engines: {node: '>=12'} @@ -7119,6 +7140,15 @@ packages: '@unblockneteasemusic/rust-napi-win32-x64-msvc': 0.3.0 dev: false + /@videojs/vhs-utils/3.0.5: + resolution: {integrity: sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==} + engines: {node: '>=8', npm: '>=5'} + dependencies: + '@babel/runtime': 7.18.3 + global: 4.4.0 + url-toolkit: 2.2.5 + dev: false + /@vitejs/plugin-react/1.3.2: resolution: {integrity: sha512-aurBNmMo0kz1O4qRoY+FM4epSA39y3ShWGuqfLRA/3z0oEJAdtoSfgA3aO98/PCCHAqMaduLxIxErWrVKIFzXA==} engines: {node: '>=12.0.0'} @@ -9424,7 +9454,6 @@ packages: /core-js/3.22.7: resolution: {integrity: sha512-Jt8SReuDKVNZnZEzyEQT5eK6T2RRCXkfTq7Lo09kpm+fHjgGewSbNjV+Wt4yZMhPDdzz2x1ulI5z/w4nxpBseg==} requiresBuild: true - dev: true /core-util-is/1.0.2: resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} @@ -9698,6 +9727,10 @@ packages: dev: true optional: true + /custom-event-polyfill/1.0.7: + resolution: {integrity: sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==} + dev: false + /cyclist/1.0.1: resolution: {integrity: sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==} dev: true @@ -10109,7 +10142,6 @@ packages: /dom-walk/0.1.2: resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} - dev: true /domain-browser/1.2.0: resolution: {integrity: sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==} @@ -12259,7 +12291,6 @@ packages: dependencies: min-document: 2.19.0 process: 0.11.10 - dev: true /globals/11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} @@ -12578,6 +12609,10 @@ packages: dependencies: '@babel/runtime': 7.18.3 + /hls.js/1.1.5: + resolution: {integrity: sha512-mQX5TSNtJEzGo5HPpvcQgCu+BWoKDQM6YYtg/KbgWkmVAcqOCvSTi0SuqG2ZJLXxIzdnFcKU2z7Mrw/YQWhPOA==} + dev: false + /hmac-drbg/1.0.1: resolution: {integrity: sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=} dependencies: @@ -13861,6 +13896,10 @@ packages: json5: 2.2.1 dev: true + /loadjs/4.2.0: + resolution: {integrity: sha512-AgQGZisAlTPbTEzrHPb6q+NYBMD+DP9uvGSIjSUM5uG+0jG15cb8axWpxuOIqrmQjn6scaaH8JwloiP27b2KXA==} + dev: false + /local-pkg/0.4.1: resolution: {integrity: sha512-lL87ytIGP2FU5PWwNDo0w3WhIo2gopIAxPg9RxDYF7m4rr5ahuZxP22xnJHIvaLTe4Z9P6uKKY2UHiwyB4pcrw==} engines: {node: '>=14'} @@ -14001,6 +14040,14 @@ packages: readable-stream: 3.6.0 dev: true + /m3u8-parser/4.7.1: + resolution: {integrity: sha512-pbrQwiMiq+MmI9bl7UjtPT3AK603PV9bogNlr83uC+X9IoxqL5E4k7kU7fMQ0dpRgxgeSMygqUa0IMLQNXLBNA==} + dependencies: + '@babel/runtime': 7.18.3 + '@videojs/vhs-utils': 3.0.5 + global: 4.4.0 + dev: false + /magic-string/0.25.9: resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} dependencies: @@ -14357,10 +14404,9 @@ packages: engines: {node: '>=10'} /min-document/2.19.0: - resolution: {integrity: sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=} + resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==} dependencies: dom-walk: 0.1.2 - dev: true /min-indent/1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} @@ -15472,6 +15518,30 @@ packages: xmlbuilder: 9.0.7 dev: true + /plyr-react/5.0.2_react@18.1.0: + resolution: {integrity: sha512-CksykyesFtmPoslasOVIplYZkduJ2OQ/q3QNUdktjy8Ds4Rhxw9u57jKBjSLdpyhENp/Yu+lDC7lOHc1o9iPUQ==} + engines: {node: '>=12.7.0'} + peerDependencies: + react: '>=16.8' + peerDependenciesMeta: + react: + optional: true + dependencies: + plyr: 3.7.2 + react: 18.1.0 + react-aptor: 2.0.0-alpha.1_react@18.1.0 + dev: false + + /plyr/3.7.2: + resolution: {integrity: sha512-I0ZC/OI4oJ0iWG9s2rrnO0YFO6aLyrPiQBq9kum0FqITYljwTPBbYL3TZZu8UJQJUq7tUWN18Q7ACwNCkGKABQ==} + dependencies: + core-js: 3.22.7 + custom-event-polyfill: 1.0.7 + loadjs: 4.2.0 + rangetouch: 2.0.1 + url-polyfill: 1.1.12 + dev: false + /pngjs/5.0.0: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} engines: {node: '>=10.13.0'} @@ -15793,9 +15863,8 @@ packages: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} /process/0.11.10: - resolution: {integrity: sha1-czIwDoQBYb2j5podHZGn1LwW8YI=} + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} - dev: true /progress/2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} @@ -16020,6 +16089,10 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} + /rangetouch/2.0.1: + resolution: {integrity: sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA==} + dev: false + /raw-body/2.5.1: resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} engines: {node: '>= 0.8'} @@ -16049,6 +16122,18 @@ packages: minimist: 1.2.6 strip-json-comments: 2.0.1 + /react-aptor/2.0.0-alpha.1_react@18.1.0: + resolution: {integrity: sha512-FbvxQKsZMUZcLr2WdrQEmxH0kifsN4N+v6YdL1g3At03zouJCEcPXv+o+bhP3Ci3ya4QPvNHK/bpbrCzuKWOMw==} + engines: {node: '>=12.7.0'} + peerDependencies: + react: '>=16.8' + peerDependenciesMeta: + react: + optional: true + dependencies: + react: 18.1.0 + dev: false + /react-docgen-typescript/2.2.2_typescript@4.7.3: resolution: {integrity: sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==} peerDependencies: @@ -18653,6 +18738,14 @@ packages: prepend-http: 2.0.0 dev: true + /url-polyfill/1.1.12: + resolution: {integrity: sha512-mYFmBHCapZjtcNHW0MDq9967t+z4Dmg5CJ0KqysK3+ZbyoNOWQHksGCTWwDhxGXllkWlOc10Xfko6v4a3ucM6A==} + dev: false + + /url-toolkit/2.2.5: + resolution: {integrity: sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==} + dev: false + /url/0.11.0: resolution: {integrity: sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=} dependencies: