feat: updates

This commit is contained in:
qier222 2022-03-17 14:45:04 +08:00
parent 950f72d4e8
commit f54d2ded5c
No known key found for this signature in database
GPG key ID: 9C85007ED905F14D
26 changed files with 361 additions and 166 deletions

View file

@ -3,7 +3,7 @@
* @see https://www.electron.build/configuration/configuration * @see https://www.electron.build/configuration/configuration
*/ */
module.exports = { module.exports = {
appId: 'yesplaymusic', appId: 'com.qier222.yesplaymusic',
productName: 'YesPlayMusic', productName: 'YesPlayMusic',
copyright: 'Copyright © 2022 ${author}', copyright: 'Copyright © 2022 ${author}',
asar: true, asar: true,
@ -43,4 +43,19 @@ module.exports = {
target: ['AppImage'], target: ['AppImage'],
artifactName: '${productName}-${version}-Installer.${ext}', artifactName: '${productName}-${version}-Installer.${ext}',
}, },
files: [
'**/*',
'!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}',
'!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}',
'!**/node_modules/*.d.ts',
'!**/node_modules/.bin',
'!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}',
'!.editorconfig',
'!**/._*',
'!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,.gitignore,.gitattributes}',
'!**/{__pycache__,thumbs.db,.flowconfig,.idea,.vs,.nyc_output}',
'!**/{appveyor.yml,.travis.yml,circle.yml}',
'!**/{npm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json}',
'!**/node_modules/realm/react-native/**/*',
],
} }

3
.gitignore vendored
View file

@ -86,3 +86,6 @@ release
dist-ssr dist-ssr
*.local *.local
.vscode/settings.json .vscode/settings.json
bundle-stats-main.html
bundle-stats-preload.html
bundle-stats-renderer.html

View file

@ -11,7 +11,7 @@
"main": "dist/main/index.cjs", "main": "dist/main/index.cjs",
"scripts": { "scripts": {
"dev": "node scripts/watch.mjs", "dev": "node scripts/watch.mjs",
"build": "npm run typecheck && node scripts/build.mjs && electron-builder --config .electron-builder.config.js", "build": "npm run typecheck && node scripts/build.mjs && electron-builder --config .electron-builder.config.js --dir",
"typecheck": "tsc --noEmit --project packages/renderer/tsconfig.json", "typecheck": "tsc --noEmit --project packages/renderer/tsconfig.json",
"debug": "cross-env-shell NODE_ENV=debug \"npm run typecheck && node scripts/build.mjs && vite ./packages/renderer\"", "debug": "cross-env-shell NODE_ENV=debug \"npm run typecheck && node scripts/build.mjs && vite ./packages/renderer\"",
"eslint": "eslint --ext .ts,.js ./", "eslint": "eslint --ext .ts,.js ./",
@ -26,11 +26,12 @@
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"electron-log": "^4.4.6", "electron-log": "^4.4.6",
"electron-store": "^8.0.1", "electron-store": "^8.0.1",
"express": "^4.17.3", "realm": "^10.13.0",
"realm": "^10.13.0" "express": "^4.17.3"
}, },
"devDependencies": { "devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^3.2.0", "@trivago/prettier-plugin-sort-imports": "^3.2.0",
"@types/cookie-parser": "^1.4.2",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/howler": "^2.2.6", "@types/howler": "^2.2.6",
"@types/js-cookie": "^3.0.1", "@types/js-cookie": "^3.0.1",

82
packages/main/database.ts Normal file
View file

@ -0,0 +1,82 @@
import Realm from 'realm'
import type { FetchTracksResponse } from '../renderer/src/api/track'
enum ModelNames {
TRACK = 'Track',
}
const TrackSchema = {
name: ModelNames.TRACK,
properties: {
id: 'int',
json: 'string',
updateAt: 'int',
},
primaryKey: 'id',
}
const realm = new Realm({
path: './dist/db.realm',
schema: [TrackSchema],
})
export const database = {
get: (model: ModelNames, key: number) => {
return realm.objectForPrimaryKey(model, key)
},
set: (model: ModelNames, key: number, value: any) => {
realm.create(
model,
{
id: key,
updateAt: ~~(Date.now() / 1000),
json: JSON.stringify(value),
},
'modified'
)
},
delete: (model: ModelNames, key: number) => {
realm.delete(realm.objectForPrimaryKey(model, key))
},
}
export function setTracks(data: FetchTracksResponse) {
const tracks = data.songs
if (!data.songs) return
const write = async () =>
realm.write(() => {
tracks.forEach(track => {
database.set(ModelNames.TRACK, track.id, track)
})
})
write()
}
export async function setCache(api: string, data: any) {
switch (api) {
case 'song_detail':
setTracks(data)
}
}
export function getCache(api: string, query: any) {
switch (api) {
case 'song_detail': {
const ids: string[] = query.ids.split(',')
const idsQuery = ids.map(id => `id = ${id}`).join(' OR ')
const tracksRaw = realm
.objects(ModelNames.TRACK)
.filtered(`(${idsQuery})`)
if (tracksRaw.length !== ids.length) {
return
}
const tracks = tracksRaw.map(track => JSON.parse(track.json))
return {
code: 200,
songs: tracks,
privileges: {},
}
}
}
}

View file

@ -4,16 +4,12 @@ import {
app, app,
shell, shell,
} from 'electron' } from 'electron'
import installExtension, {
REACT_DEVELOPER_TOOLS,
REDUX_DEVTOOLS,
} from 'electron-devtools-installer'
import Store from 'electron-store' import Store from 'electron-store'
import { release } from 'os' import { release } from 'os'
import { join } from 'path' import { join } from 'path'
import Realm from 'realm'
import logger from './logger' import logger from './logger'
import './server' import './server'
import './database'
const isWindows = process.platform === 'win32' const isWindows = process.platform === 'win32'
const isMac = process.platform === 'darwin' const isMac = process.platform === 'darwin'
@ -104,6 +100,12 @@ app.whenReady().then(async () => {
// Install devtool extension // Install devtool extension
if (isDev) { if (isDev) {
const {
default: installExtension,
REACT_DEVELOPER_TOOLS,
REDUX_DEVTOOLS,
// eslint-disable-next-line @typescript-eslint/no-var-requires
} = require('electron-devtools-installer')
installExtension(REACT_DEVELOPER_TOOLS.id).catch(err => installExtension(REACT_DEVELOPER_TOOLS.id).catch(err =>
console.log('An error occurred: ', err) console.log('An error occurred: ', err)
) )

View file

@ -2,42 +2,44 @@ import { pathCase } from 'change-case'
import cookieParser from 'cookie-parser' import cookieParser from 'cookie-parser'
import express, { Request, Response } from 'express' import express, { Request, Response } from 'express'
import logger from './logger' import logger from './logger'
import { getCache, setCache } from './database'
const neteaseApi = require('NeteaseCloudMusicApi') // eslint-disable-next-line @typescript-eslint/no-var-requires
const neteaseApi = require('NeteaseCloudMusicApi') as (params: any) => any[]
const app = express() const app = express()
app.use(cookieParser()) app.use(cookieParser())
const port = Number(process.env['ELECTRON_DEV_NETEASE_API_PORT'] ?? 3000) const port = Number(process.env['ELECTRON_DEV_NETEASE_API_PORT'] ?? 3000)
Object.entries(neteaseApi).forEach(([name, handler]) => { Object.entries(neteaseApi).forEach(([name, handler]) => {
if (['serveNcmApi', 'getModulesDefinitions'].includes(name)) { if (['serveNcmApi', 'getModulesDefinitions'].includes(name)) return
return
}
const wrappedHandler = async (req: Request, res: Response) => { const wrappedHandler = async (req: Request, res: Response) => {
logger.info(`[server] Handling request: ${req.path}`) logger.info(`[server] Handling request: ${req.path}`)
// Get from cache
const cacheResult = getCache(name, req.query)
if (cacheResult) {
logger.info(`[server] Cache hit for ${req.path}`)
return res.json(cacheResult)
}
// Request netease api
try { try {
const result = await handler({ const result = await handler({
...req.query, ...req.query,
// cookie:
// 'MUSIC_U=1239b6c1217d8cd240df9c8fa15e99a62f9aaac86baa7a8aa3166acbad267cd8a237494327fc3ec043124f3fcebe94e446b14e3f0c3f8af9fe5c85647582a507',
// cookie: req.headers.cookie,
cookie: `MUSIC_U=${req.cookies['MUSIC_U']}`, cookie: `MUSIC_U=${req.cookies['MUSIC_U']}`,
}) })
res.send(result.body) res.send(result.body)
setCache(name, result.body)
} catch (error) { } catch (error) {
res.status(500).send(error) res.status(500).send(error)
} }
} }
app.get( const neteasePath = `/netease/${pathCase(name)}`
`/netease/${pathCase(name)}`, app.get(neteasePath, wrappedHandler)
async (req: Request, res: Response) => await wrappedHandler(req, res) app.post(neteasePath, wrappedHandler)
)
app.post(
`/netease/${pathCase(name)}`,
async (req: Request, res: Response) => await wrappedHandler(req, res)
)
}) })
app.listen(port, () => { app.listen(port, () => {

View file

@ -1,6 +1,7 @@
import dotenv from 'dotenv' import dotenv from 'dotenv'
import { builtinModules } from 'module' import { builtinModules } from 'module'
import path from 'path' import path from 'path'
import { visualizer } from 'rollup-plugin-visualizer'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import pkg from '../../package.json' import pkg from '../../package.json'
import esm2cjs from '../../scripts/vite-plugin-esm2cjs' import esm2cjs from '../../scripts/vite-plugin-esm2cjs'
@ -27,6 +28,13 @@ export default defineConfig({
...builtinModules, ...builtinModules,
...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.dependencies || {}),
], ],
plugins: [
visualizer({
filename: './bundle-stats-main.html',
gzipSize: true,
projectRoot: 'packages/main',
}),
],
}, },
}, },
}) })

View file

@ -3,6 +3,7 @@ import { builtinModules } from 'module'
import path from 'path' import path from 'path'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import pkg from '../../package.json' import pkg from '../../package.json'
import { visualizer } from 'rollup-plugin-visualizer'
dotenv.config({ dotenv.config({
path: path.resolve(process.cwd(), '.env'), path: path.resolve(process.cwd(), '.env'),
@ -25,6 +26,13 @@ export default defineConfig({
...builtinModules, ...builtinModules,
...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.dependencies || {}),
], ],
plugins: [
visualizer({
filename: './bundle-stats-preload.html',
gzipSize: true,
projectRoot: 'packages/preload',
}),
],
}, },
}, },
}) })

View file

@ -9,7 +9,7 @@ export enum TrackApiNames {
export interface FetchTracksParams { export interface FetchTracksParams {
ids: number[] ids: number[]
} }
interface FetchTracksResponse { export interface FetchTracksResponse {
code: number code: number
songs: Track[] songs: Track[]
privileges: { privileges: {

View file

@ -12,7 +12,7 @@ export enum UserApiNames {
* - uid : 用户 id * - uid : 用户 id
* @param {number} uid * @param {number} uid
*/ */
export function userDetail(uid) { export function userDetail(uid: number) {
return request({ return request({
url: '/user/detail', url: '/user/detail',
method: 'get', method: 'get',
@ -160,68 +160,68 @@ export function dailySignin(type = 0) {
* @param {number} params.limit * @param {number} params.limit
* @param {number=} params.offset * @param {number=} params.offset
*/ */
export function likedAlbums(params) { // export function likedAlbums(params) {
return request({ // return request({
url: '/album/sublist', // url: '/album/sublist',
method: 'get', // method: 'get',
params: { // params: {
limit: params.limit, // limit: params.limit,
timestamp: new Date().getTime(), // timestamp: new Date().getTime(),
}, // },
}) // })
} // }
/** /**
* *
* 说明 : 调用此接口可获取到用户收藏的歌手 * 说明 : 调用此接口可获取到用户收藏的歌手
*/ */
export function likedArtists(params) { // export function likedArtists(params) {
return request({ // return request({
url: '/artist/sublist', // url: '/artist/sublist',
method: 'get', // method: 'get',
params: { // params: {
limit: params.limit, // limit: params.limit,
timestamp: new Date().getTime(), // timestamp: new Date().getTime(),
}, // },
}) // })
} // }
/** /**
* MV * MV
* 说明 : 调用此接口可获取到用户收藏的MV * 说明 : 调用此接口可获取到用户收藏的MV
*/ */
export function likedMVs(params) { // export function likedMVs(params) {
return request({ // return request({
url: '/mv/sublist', // url: '/mv/sublist',
method: 'get', // method: 'get',
params: { // params: {
limit: params.limit, // limit: params.limit,
timestamp: new Date().getTime(), // timestamp: new Date().getTime(),
}, // },
}) // })
} // }
/** /**
* *
*/ */
export function uploadSong(file) { // export function uploadSong(file) {
let formData = new FormData() // let formData = new FormData()
formData.append('songFile', file) // formData.append('songFile', file)
return request({ // return request({
url: '/cloud', // url: '/cloud',
method: 'post', // method: 'post',
params: { // params: {
timestamp: new Date().getTime(), // timestamp: new Date().getTime(),
}, // },
data: formData, // data: formData,
headers: { // headers: {
'Content-Type': 'multipart/form-data', // 'Content-Type': 'multipart/form-data',
}, // },
timeout: 200000, // timeout: 200000,
}).catch(error => { // }).catch(error => {
alert(`上传失败Error: ${error}`) // alert(`上传失败Error: ${error}`)
}) // })
} // }
/** /**
* *
@ -232,40 +232,40 @@ export function uploadSong(file) {
* @param {number} params.limit * @param {number} params.limit
* @param {number=} params.offset * @param {number=} params.offset
*/ */
export function cloudDisk(params = {}) { // export function cloudDisk(params = {}) {
params.timestamp = new Date().getTime() // params.timestamp = new Date().getTime()
return request({ // return request({
url: '/user/cloud', // url: '/user/cloud',
method: 'get', // method: 'get',
params, // params,
}) // })
} // }
/** /**
* *
*/ */
export function cloudDiskTrackDetail(id) { // export function cloudDiskTrackDetail(id) {
return request({ // return request({
url: '/user/cloud/detail', // url: '/user/cloud/detail',
method: 'get', // method: 'get',
params: { // params: {
timestamp: new Date().getTime(), // timestamp: new Date().getTime(),
id, // id,
}, // },
}) // })
} // }
/** /**
* *
* @param {Array} id * @param {Array} id
*/ */
export function cloudDiskTrackDelete(id) { // export function cloudDiskTrackDelete(id) {
return request({ // return request({
url: '/user/cloud/del', // url: '/user/cloud/del',
method: 'get', // method: 'get',
params: { // params: {
timestamp: new Date().getTime(), // timestamp: new Date().getTime(),
id, // id,
}, // },
}) // })
} // }

View file

@ -36,11 +36,11 @@ const Button = ({
{ {
'px-4 py-1.5': shape === Shape.Default, 'px-4 py-1.5': shape === Shape.Default,
'px-3 py-1.5': shape === Shape.Square, 'px-3 py-1.5': shape === Shape.Square,
'bg-brand-100 dark:bg-brand-700': color === Color.Primary, 'bg-brand-100 dark:bg-brand-600': color === Color.Primary,
'text-brand-500 dark:text-white': iconColor === Color.Primary, 'text-brand-500 dark:text-white': iconColor === Color.Primary,
'bg-gray-100 dark:bg-gray-700': color === Color.Gray, 'bg-gray-100 dark:bg-gray-700': color === Color.Gray,
'text-gray-900 dark:text-gray-400': iconColor === Color.Gray, 'text-gray-900 dark:text-gray-400': iconColor === Color.Gray,
'animate-pulse bg-gray-100 text-transparent dark:bg-gray-800': 'animate-pulse bg-gray-100 !text-transparent dark:bg-gray-800':
isSkelton, isSkelton,
} }
)} )}

View file

@ -38,11 +38,14 @@ const getSubtitleText = (
subtitle: Subtitle subtitle: Subtitle
) => { ) => {
const nickname = 'creator' in item ? item.creator.nickname : 'someone' const nickname = 'creator' in item ? item.creator.nickname : 'someone'
const artist = 'artist' in item ? item.artist.name : 'unknown'
const copywriter = 'copywriter' in item ? item.copywriter : 'unknown'
const releaseYear = const releaseYear =
'publishTime' in item ('publishTime' in item &&
? formatDate(item.publishTime ?? 0, 'en', 'YYYY') formatDate(item.publishTime ?? 0, 'en', 'YYYY')) ||
: 'unknown' 'unknown'
const types = {
const type = {
playlist: 'playlist', playlist: 'playlist',
album: 'Album', album: 'Album',
: 'Album', : 'Album',
@ -50,16 +53,17 @@ const getSubtitleText = (
'EP/Single': 'EP', 'EP/Single': 'EP',
EP: 'EP', EP: 'EP',
unknown: 'unknown', unknown: 'unknown',
} : 'Collection',
const type = 'type' in item ? item.type || 'unknown' : 'unknown' }[('type' in item && typeof item.type !== 'number' && item.type) || 'unknown']
const artist = 'artist' in item ? item.artist.name : 'unknown'
const table = { const table = {
[Subtitle.CREATOR]: `by ${nickname}`, [Subtitle.CREATOR]: `by ${nickname}`,
[Subtitle.TYPE_RELEASE_YEAR]: `${types[type]} · ${releaseYear}`, [Subtitle.TYPE_RELEASE_YEAR]: `${type} · ${releaseYear}`,
[Subtitle.ARTIST]: artist, [Subtitle.ARTIST]: artist,
[Subtitle.COPYWRITER]: copywriter,
} }
return table[subtitle] ?? item[subtitle]
return table[subtitle]
} }
const getImageUrl = (item: Album | Playlist | Artist) => { const getImageUrl = (item: Album | Playlist | Artist) => {
@ -162,7 +166,7 @@ const CoverRow = ({
)} )}
<span <span
onClick={() => goTo(item.id)} onClick={() => goTo(item.id)}
className="decoration-gray-600 decoration-2 hover:underline dark:text-white" className="decoration-gray-600 decoration-2 hover:underline dark:text-white dark:decoration-gray-200"
> >
{item.name} {item.name}
</span> </span>

View file

@ -18,7 +18,7 @@ const IconButton = ({
className, className,
'relative transform cursor-default p-2 transition duration-200', 'relative transform cursor-default p-2 transition duration-200',
!disabled && !disabled &&
'btn-pressed-animation btn-hover-animation after:bg-black/[.06]', 'btn-pressed-animation btn-hover-animation after:bg-black/[.06] dark:after:bg-white/10',
disabled && 'opacity-30' disabled && 'opacity-30'
)} )}
> >

View file

@ -49,7 +49,7 @@ const PlayingTrack = () => {
<div className="flex flex-col justify-center leading-tight"> <div className="flex flex-col justify-center leading-tight">
<div <div
onClick={toTrackListSource} onClick={toTrackListSource}
className="line-clamp-1 font-semibold text-black decoration-gray-600 decoration-2 hover:underline dark:text-white" className="line-clamp-1 font-semibold text-black decoration-gray-600 decoration-2 hover:underline dark:text-white dark:decoration-gray-300"
> >
{track?.name} {track?.name}
</div> </div>
@ -58,7 +58,7 @@ const PlayingTrack = () => {
</div> </div>
</div> </div>
<IconButton> <IconButton onClick={() => toast('Work in progress')}>
<SvgIcon <SvgIcon
className="h-4 w-4 text-black dark:text-white" className="h-4 w-4 text-black dark:text-white"
name="heart-outline" name="heart-outline"
@ -100,19 +100,19 @@ const MediaControls = () => {
const Others = () => { const Others = () => {
return ( return (
<div className="flex items-center justify-end gap-2 pr-2 text-black dark:text-white"> <div className="flex items-center justify-end gap-2 pr-2 text-black dark:text-white">
<IconButton> <IconButton onClick={() => toast('Work in progress')}>
<SvgIcon className="h-4 w-4" name="playlist" /> <SvgIcon className="h-4 w-4" name="playlist" />
</IconButton> </IconButton>
<IconButton> <IconButton onClick={() => toast('Work in progress')}>
<SvgIcon className="h-4 w-4" name="repeat" /> <SvgIcon className="h-4 w-4" name="repeat" />
</IconButton> </IconButton>
<IconButton> <IconButton onClick={() => toast('Work in progress')}>
<SvgIcon className="h-4 w-4" name="shuffle" /> <SvgIcon className="h-4 w-4" name="shuffle" />
</IconButton> </IconButton>
<IconButton> <IconButton onClick={() => toast('Work in progress')}>
<SvgIcon className="h-4 w-4" name="volume" /> <SvgIcon className="h-4 w-4" name="volume" />
</IconButton> </IconButton>
<IconButton> <IconButton onClick={() => toast('Work in progress')}>
<SvgIcon className="h-4 w-4" name="chevron-up" /> <SvgIcon className="h-4 w-4" name="chevron-up" />
</IconButton> </IconButton>
</div> </div>
@ -149,7 +149,7 @@ const Progress = () => {
const Player = () => { const Player = () => {
return ( return (
<div className="fixed bottom-0 left-0 right-0 grid h-16 grid-cols-3 grid-rows-1 bg-white bg-opacity-[.86] py-2.5 px-5 backdrop-blur-xl backdrop-saturate-[1.8] dark:bg-[#222]"> <div className="fixed bottom-0 left-0 right-0 grid h-16 grid-cols-3 grid-rows-1 bg-white bg-opacity-[.86] py-2.5 px-5 backdrop-blur-xl backdrop-saturate-[1.8] dark:bg-[#222] dark:bg-opacity-[.86]">
<Progress /> <Progress />
<PlayingTrack /> <PlayingTrack />

View file

@ -52,7 +52,7 @@ const PrimaryTabs = () => {
</NavLink> </NavLink>
))} ))}
<div className="mx-5 my-2 h-px bg-black opacity-5 dark:bg-white dark:opacity-20"></div> <div className="mx-5 my-2 h-px bg-black opacity-5 dark:bg-white dark:opacity-10"></div>
</div> </div>
) )
} }
@ -65,7 +65,7 @@ const Playlists = () => {
}) })
return ( return (
<div className="overflow-auto pb-[4.6rem]"> <div className="mb-16 overflow-auto pb-2">
{playlists?.playlist?.map(playlist => ( {playlists?.playlist?.map(playlist => (
<NavLink <NavLink
key={playlist.id} key={playlist.id}
@ -89,7 +89,7 @@ const Sidebar = () => {
return ( return (
<div <div
id="sidebar" id="sidebar"
className="grid h-screen max-w-sm grid-rows-[12rem_auto] border-r border-gray-300/10 bg-gray-50 bg-opacity-[.85] dark:bg-black dark:bg-opacity-70" className="grid h-screen max-w-sm grid-rows-[12rem_auto] border-r border-gray-300/10 bg-gray-50 bg-opacity-[.85] dark:border-gray-500/10 dark:bg-gray-900 dark:bg-opacity-80"
> >
<PrimaryTabs /> <PrimaryTabs />
<Playlists /> <Playlists />

View file

@ -80,7 +80,7 @@ const Topbar = () => {
className={classNames( className={classNames(
'app-region-drag sticky top-0 z-30 flex h-16 min-h-[4rem] w-full cursor-default items-center justify-between px-8 transition duration-300', 'app-region-drag sticky top-0 z-30 flex h-16 min-h-[4rem] w-full cursor-default items-center justify-between px-8 transition duration-300',
!scroll.arrivedState.top && !scroll.arrivedState.top &&
'bg-white bg-opacity-[.86] backdrop-blur-xl backdrop-saturate-[1.8] dark:bg-[#222]' 'bg-white bg-opacity-[.86] backdrop-blur-xl backdrop-saturate-[1.8] dark:bg-[#222] dark:bg-opacity-[.86]'
)} )}
> >
<div className="flex gap-2"> <div className="flex gap-2">

View file

@ -36,9 +36,9 @@ const useScroll = (
useEffect(() => { useEffect(() => {
if (!ref) return if (!ref) return
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => { const handleScroll = (e: Event) => {
if (!e.currentTarget && !e.target) return if (!e.target) return
const target = e.currentTarget || e.target const target = e.target as HTMLElement
const arrivedState: ArrivedState = { const arrivedState: ArrivedState = {
left: target.scrollLeft <= 0 + (offset?.left || 0), left: target.scrollLeft <= 0 + (offset?.left || 0),

View file

@ -9,8 +9,53 @@ import TracksAlbum from '@/components/TracksAlbum'
import useAlbum from '@/hooks/useAlbum' import useAlbum from '@/hooks/useAlbum'
import useArtistAlbums from '@/hooks/useArtistAlbums' import useArtistAlbums from '@/hooks/useArtistAlbums'
import { player } from '@/store' import { player } from '@/store'
import { State as PlayerState } from '@/utils/player'
import { formatDate, formatDuration, resizeImage } from '@/utils/common' import { formatDate, formatDuration, resizeImage } from '@/utils/common'
const PlayButton = ({
album,
handlePlay,
isLoading,
}: {
album: Album | undefined
isLoading: boolean
handlePlay: () => void
}) => {
const playerSnapshot = useSnapshot(player)
const isPlaying = useMemo(
() => playerSnapshot.state === PlayerState.PLAYING,
[playerSnapshot.state]
)
const isThisAlbumPlaying = useMemo(
() =>
playerSnapshot.trackListSource?.type === 'album' &&
playerSnapshot.trackListSource?.id === album?.id,
[playerSnapshot.trackListSource, album?.id]
)
const wrappedHandlePlay = () => {
if (isPlaying && isThisAlbumPlaying) {
player.pause()
return
}
if (!isPlaying && isThisAlbumPlaying) {
player.play()
return
}
handlePlay()
}
return (
<Button onClick={wrappedHandlePlay} isSkelton={isLoading}>
<SvgIcon
name={isPlaying && isThisAlbumPlaying ? 'pause' : 'play'}
className="mr-2 h-4 w-4"
/>
{isPlaying && isThisAlbumPlaying ? '暂停' : '播放'}
</Button>
)
}
const Header = ({ const Header = ({
album, album,
isLoading, isLoading,
@ -37,11 +82,11 @@ const Header = ({
<Fragment> <Fragment>
<img <img
src={coverUrl} src={coverUrl}
className="absolute top-[-50%] w-full blur-[100px]" className="absolute -top-full w-full blur-[100px]"
/> />
<img <img
src={coverUrl} src={coverUrl}
className="absolute top-[-50%] w-full blur-[100px]" className="absolute -top-full w-full blur-[100px]"
/> />
</Fragment> </Fragment>
)} )}
@ -81,17 +126,18 @@ const Header = ({
{/* Info */} {/* Info */}
<div className="z-10 flex h-full flex-col justify-between"> <div className="z-10 flex h-full flex-col justify-between">
{/* Name */} {/* Name */}
{!isLoading && ( {isLoading ? (
<Skeleton className="w-3/4 text-6xl">PLACEHOLDER</Skeleton>
) : (
<div className="text-6xl font-bold dark:text-white"> <div className="text-6xl font-bold dark:text-white">
{album?.name} {album?.name}
</div> </div>
)} )}
{isLoading && (
<Skeleton className="w-3/4 text-6xl">PLACEHOLDER</Skeleton>
)}
{/* Artist */} {/* Artist */}
{!isLoading && ( {isLoading ? (
<Skeleton className="mt-5 w-64 text-lg">PLACEHOLDER</Skeleton>
) : (
<div className="mt-5 text-lg font-medium text-gray-800 dark:text-gray-300"> <div className="mt-5 text-lg font-medium text-gray-800 dark:text-gray-300">
Album by{' '} Album by{' '}
<NavLink <NavLink
@ -102,43 +148,39 @@ const Header = ({
</NavLink> </NavLink>
</div> </div>
)} )}
{isLoading && (
<Skeleton className="mt-5 w-64 text-lg">PLACEHOLDER</Skeleton>
)}
{/* Release date & track count & album duration */} {/* Release date & track count & album duration */}
{!isLoading && ( {isLoading ? (
<Skeleton className="w-72 translate-y-px text-sm">
PLACEHOLDER
</Skeleton>
) : (
<div className="text-sm font-thin text-gray-500 dark:text-gray-400"> <div className="text-sm font-thin text-gray-500 dark:text-gray-400">
{dayjs(album?.publishTime || 0).year()} · {album?.size} Songs,{' '} {dayjs(album?.publishTime || 0).year()} · {album?.size} Songs,{' '}
{albumDuration} {albumDuration}
</div> </div>
)} )}
{isLoading && (
<Skeleton className="w-72 translate-y-px text-sm">
PLACEHOLDER
</Skeleton>
)}
{/* Description */} {/* Description */}
{!isLoading && ( {isLoading ? (
<Skeleton className="mt-5 min-h-[2.5rem] w-1/2 text-sm">
PLACEHOLDER
</Skeleton>
) : (
<div className="line-clamp-2 mt-5 min-h-[2.5rem] text-sm text-gray-500 dark:text-gray-400"> <div className="line-clamp-2 mt-5 min-h-[2.5rem] text-sm text-gray-500 dark:text-gray-400">
{album?.description} {album?.description}
</div> </div>
)} )}
{isLoading && (
<Skeleton className="mt-5 min-h-[2.5rem] w-1/2 text-sm">
PLACEHOLDER
</Skeleton>
)}
{/* Buttons */} {/* Buttons */}
<div className="mt-5 flex gap-4"> <div className="mt-5 flex gap-4">
<Button onClick={() => handlePlay()} isSkelton={isLoading}> <PlayButton {...{ album, handlePlay, isLoading }} />
<SvgIcon name="play" className="mr-2 h-4 w-4" />
PLAY
</Button>
<Button color={ButtonColor.Gray} isSkelton={isLoading}> <Button
color={ButtonColor.Gray}
isSkelton={isLoading}
onClick={() => toast('Work in progress')}
>
<SvgIcon name="heart" className="h-4 w-4" /> <SvgIcon name="heart" className="h-4 w-4" />
</Button> </Button>
@ -146,6 +188,7 @@ const Header = ({
color={ButtonColor.Gray} color={ButtonColor.Gray}
iconColor={ButtonColor.Gray} iconColor={ButtonColor.Gray}
isSkelton={isLoading} isSkelton={isLoading}
onClick={() => toast('Work in progress')}
> >
<SvgIcon name="more" className="h-4 w-4" /> <SvgIcon name="more" className="h-4 w-4" />
</Button> </Button>

View file

@ -189,7 +189,7 @@ const LoginWithEmail = () => {
<Fragment> <Fragment>
<EmailInput {...{ email, setEmail }} /> <EmailInput {...{ email, setEmail }} />
<PasswordInput {...{ password, setPassword }} /> <PasswordInput {...{ password, setPassword }} />
<LoginButton /> <LoginButton onClick={() => toast('Work in progress')} disabled={true} />
</Fragment> </Fragment>
) )
} }

View file

@ -115,7 +115,11 @@ const Header = memo(
PLAY PLAY
</Button> </Button>
<Button color={ButtonColor.Gray} isSkelton={isLoading}> <Button
color={ButtonColor.Gray}
isSkelton={isLoading}
onClick={() => toast('Work in progress')}
>
<SvgIcon name="heart" className="h-4 w-4" /> <SvgIcon name="heart" className="h-4 w-4" />
</Button> </Button>
@ -123,6 +127,7 @@ const Header = memo(
color={ButtonColor.Gray} color={ButtonColor.Gray}
iconColor={ButtonColor.Gray} iconColor={ButtonColor.Gray}
isSkelton={isLoading} isSkelton={isLoading}
onClick={() => toast('Work in progress')}
> >
<SvgIcon name="more" className="h-4 w-4" /> <SvgIcon name="more" className="h-4 w-4" />
</Button> </Button>

View file

@ -44,7 +44,7 @@ export class Player {
repeatMode: RepeatMode = RepeatMode.OFF repeatMode: RepeatMode = RepeatMode.OFF
constructor() { constructor() {
window.player = this //
} }
/** /**

View file

@ -3,14 +3,13 @@ import dotenv from 'dotenv'
import { builtinModules } from 'module' import { builtinModules } from 'module'
import path, { join } from 'path' import path, { join } from 'path'
import AutoImport from 'unplugin-auto-import/vite' import AutoImport from 'unplugin-auto-import/vite'
import { Plugin, defineConfig } from 'vite' import { defineConfig, Plugin } from 'vite'
import resolve from 'vite-plugin-resolve' import resolve from 'vite-plugin-resolve'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import { visualizer } from 'rollup-plugin-visualizer'
dotenv.config({ path: path.resolve(process.cwd(), '.env') }) dotenv.config({ path: path.resolve(process.cwd(), '.env') })
console.log(join(__dirname, '../../.eslintrc.js'))
/** /**
* @see https://vitejs.dev/config/ * @see https://vitejs.dev/config/
*/ */
@ -60,6 +59,16 @@ export default defineConfig({
build: { build: {
sourcemap: process.env.NODE_ENV === 'debug', sourcemap: process.env.NODE_ENV === 'debug',
outDir: '../../dist/renderer', outDir: '../../dist/renderer',
rollupOptions: {
plugins: [
visualizer({
filename: './bundle-stats-renderer.html',
gzipSize: true,
projectRoot: 'packages/renderer',
template: 'treemap',
}),
],
},
}, },
resolve: { resolve: {
alias: { alias: {

View file

@ -3,7 +3,7 @@ module.exports = {
tabWidth: 2, tabWidth: 2,
useTabs: false, useTabs: false,
semi: false, semi: false,
jsxBracketSameLine: false, bracketSameLine: false,
arrowParens: 'avoid', arrowParens: 'avoid',
endOfLine: 'lf', endOfLine: 'lf',
bracketSpacing: true, bracketSpacing: true,

View file

@ -86,9 +86,14 @@ const color = (hex, text) => {
// bootstrap // bootstrap
logPrefix(color('#eab308', '[vite] ')) logPrefix(color('#eab308', '[vite] '))
console.log('building renderer')
const server = await createServer({ const server = await createServer({
configFile: 'packages/renderer/vite.config.ts', configFile: 'packages/renderer/vite.config.ts',
}) })
await server.listen() await server.listen()
console.log('building preload')
await watchPreload(server) await watchPreload(server)
console.log('building main')
await watchMain(server) await watchMain(server)

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const colors = require('tailwindcss/colors') const colors = require('tailwindcss/colors')
module.exports = { module.exports = {

View file

@ -565,6 +565,13 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/cookie-parser@^1.4.2":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@types/cookie-parser/-/cookie-parser-1.4.2.tgz#e4d5c5ffda82b80672a88a4281aaceefb1bd9df5"
integrity sha512-uwcY8m6SDQqciHsqcKDGbo10GdasYsPCYkH3hVegj9qAah6pX5HivOnOuI3WYmyQMnOATV39zv/Ybs0bC/6iVg==
dependencies:
"@types/express" "*"
"@types/debug@^4.1.6": "@types/debug@^4.1.6":
version "4.1.7" version "4.1.7"
resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz" resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz"
@ -581,7 +588,7 @@
"@types/qs" "*" "@types/qs" "*"
"@types/range-parser" "*" "@types/range-parser" "*"
"@types/express@^4.17.13": "@types/express@*", "@types/express@^4.17.13":
version "4.17.13" version "4.17.13"
resolved "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz" resolved "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz"
integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==
@ -5934,7 +5941,7 @@ roarr@^2.15.3:
rollup-plugin-visualizer@^5.6.0: rollup-plugin-visualizer@^5.6.0:
version "5.6.0" version "5.6.0"
resolved "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.6.0.tgz" resolved "https://registry.yarnpkg.com/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.6.0.tgz#06aa7cf3fd504a29d404335700f2a3f28ebb33f3"
integrity sha512-CKcc8GTUZjC+LsMytU8ocRr/cGZIfMR7+mdy4YnlyetlmIl/dM8BMnOEpD4JPIGt+ZVW7Db9ZtSsbgyeBH3uTA== integrity sha512-CKcc8GTUZjC+LsMytU8ocRr/cGZIfMR7+mdy4YnlyetlmIl/dM8BMnOEpD4JPIGt+ZVW7Db9ZtSsbgyeBH3uTA==
dependencies: dependencies:
nanoid "^3.1.32" nanoid "^3.1.32"