mirror of
https://github.com/qier222/YesPlayMusic.git
synced 2025-02-01 04:10:44 +08:00
feat: updates
This commit is contained in:
parent
950f72d4e8
commit
f54d2ded5c
|
@ -3,7 +3,7 @@
|
|||
* @see https://www.electron.build/configuration/configuration
|
||||
*/
|
||||
module.exports = {
|
||||
appId: 'yesplaymusic',
|
||||
appId: 'com.qier222.yesplaymusic',
|
||||
productName: 'YesPlayMusic',
|
||||
copyright: 'Copyright © 2022 ${author}',
|
||||
asar: true,
|
||||
|
@ -43,4 +43,19 @@ module.exports = {
|
|||
target: ['AppImage'],
|
||||
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
3
.gitignore
vendored
|
@ -86,3 +86,6 @@ release
|
|||
dist-ssr
|
||||
*.local
|
||||
.vscode/settings.json
|
||||
bundle-stats-main.html
|
||||
bundle-stats-preload.html
|
||||
bundle-stats-renderer.html
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
"main": "dist/main/index.cjs",
|
||||
"scripts": {
|
||||
"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",
|
||||
"debug": "cross-env-shell NODE_ENV=debug \"npm run typecheck && node scripts/build.mjs && vite ./packages/renderer\"",
|
||||
"eslint": "eslint --ext .ts,.js ./",
|
||||
|
@ -26,11 +26,12 @@
|
|||
"cookie-parser": "^1.4.6",
|
||||
"electron-log": "^4.4.6",
|
||||
"electron-store": "^8.0.1",
|
||||
"express": "^4.17.3",
|
||||
"realm": "^10.13.0"
|
||||
"realm": "^10.13.0",
|
||||
"express": "^4.17.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@trivago/prettier-plugin-sort-imports": "^3.2.0",
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/howler": "^2.2.6",
|
||||
"@types/js-cookie": "^3.0.1",
|
||||
|
|
82
packages/main/database.ts
Normal file
82
packages/main/database.ts
Normal 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: {},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,16 +4,12 @@ import {
|
|||
app,
|
||||
shell,
|
||||
} from 'electron'
|
||||
import installExtension, {
|
||||
REACT_DEVELOPER_TOOLS,
|
||||
REDUX_DEVTOOLS,
|
||||
} from 'electron-devtools-installer'
|
||||
import Store from 'electron-store'
|
||||
import { release } from 'os'
|
||||
import { join } from 'path'
|
||||
import Realm from 'realm'
|
||||
import logger from './logger'
|
||||
import './server'
|
||||
import './database'
|
||||
|
||||
const isWindows = process.platform === 'win32'
|
||||
const isMac = process.platform === 'darwin'
|
||||
|
@ -104,6 +100,12 @@ app.whenReady().then(async () => {
|
|||
|
||||
// Install devtool extension
|
||||
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 =>
|
||||
console.log('An error occurred: ', err)
|
||||
)
|
||||
|
|
|
@ -2,42 +2,44 @@ import { pathCase } from 'change-case'
|
|||
import cookieParser from 'cookie-parser'
|
||||
import express, { Request, Response } from 'express'
|
||||
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()
|
||||
app.use(cookieParser())
|
||||
const port = Number(process.env['ELECTRON_DEV_NETEASE_API_PORT'] ?? 3000)
|
||||
|
||||
Object.entries(neteaseApi).forEach(([name, handler]) => {
|
||||
if (['serveNcmApi', 'getModulesDefinitions'].includes(name)) {
|
||||
return
|
||||
}
|
||||
if (['serveNcmApi', 'getModulesDefinitions'].includes(name)) return
|
||||
|
||||
const wrappedHandler = async (req: Request, res: Response) => {
|
||||
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 {
|
||||
const result = await handler({
|
||||
...req.query,
|
||||
// cookie:
|
||||
// 'MUSIC_U=1239b6c1217d8cd240df9c8fa15e99a62f9aaac86baa7a8aa3166acbad267cd8a237494327fc3ec043124f3fcebe94e446b14e3f0c3f8af9fe5c85647582a507',
|
||||
// cookie: req.headers.cookie,
|
||||
cookie: `MUSIC_U=${req.cookies['MUSIC_U']}`,
|
||||
})
|
||||
res.send(result.body)
|
||||
setCache(name, result.body)
|
||||
} catch (error) {
|
||||
res.status(500).send(error)
|
||||
}
|
||||
}
|
||||
|
||||
app.get(
|
||||
`/netease/${pathCase(name)}`,
|
||||
async (req: Request, res: Response) => await wrappedHandler(req, res)
|
||||
)
|
||||
app.post(
|
||||
`/netease/${pathCase(name)}`,
|
||||
async (req: Request, res: Response) => await wrappedHandler(req, res)
|
||||
)
|
||||
const neteasePath = `/netease/${pathCase(name)}`
|
||||
app.get(neteasePath, wrappedHandler)
|
||||
app.post(neteasePath, wrappedHandler)
|
||||
})
|
||||
|
||||
app.listen(port, () => {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import dotenv from 'dotenv'
|
||||
import { builtinModules } from 'module'
|
||||
import path from 'path'
|
||||
import { visualizer } from 'rollup-plugin-visualizer'
|
||||
import { defineConfig } from 'vite'
|
||||
import pkg from '../../package.json'
|
||||
import esm2cjs from '../../scripts/vite-plugin-esm2cjs'
|
||||
|
@ -27,6 +28,13 @@ export default defineConfig({
|
|||
...builtinModules,
|
||||
...Object.keys(pkg.dependencies || {}),
|
||||
],
|
||||
plugins: [
|
||||
visualizer({
|
||||
filename: './bundle-stats-main.html',
|
||||
gzipSize: true,
|
||||
projectRoot: 'packages/main',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
@ -3,6 +3,7 @@ import { builtinModules } from 'module'
|
|||
import path from 'path'
|
||||
import { defineConfig } from 'vite'
|
||||
import pkg from '../../package.json'
|
||||
import { visualizer } from 'rollup-plugin-visualizer'
|
||||
|
||||
dotenv.config({
|
||||
path: path.resolve(process.cwd(), '.env'),
|
||||
|
@ -25,6 +26,13 @@ export default defineConfig({
|
|||
...builtinModules,
|
||||
...Object.keys(pkg.dependencies || {}),
|
||||
],
|
||||
plugins: [
|
||||
visualizer({
|
||||
filename: './bundle-stats-preload.html',
|
||||
gzipSize: true,
|
||||
projectRoot: 'packages/preload',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
@ -9,7 +9,7 @@ export enum TrackApiNames {
|
|||
export interface FetchTracksParams {
|
||||
ids: number[]
|
||||
}
|
||||
interface FetchTracksResponse {
|
||||
export interface FetchTracksResponse {
|
||||
code: number
|
||||
songs: Track[]
|
||||
privileges: {
|
||||
|
|
|
@ -12,7 +12,7 @@ export enum UserApiNames {
|
|||
* - uid : 用户 id
|
||||
* @param {number} uid
|
||||
*/
|
||||
export function userDetail(uid) {
|
||||
export function userDetail(uid: number) {
|
||||
return request({
|
||||
url: '/user/detail',
|
||||
method: 'get',
|
||||
|
@ -160,68 +160,68 @@ export function dailySignin(type = 0) {
|
|||
* @param {number} params.limit
|
||||
* @param {number=} params.offset
|
||||
*/
|
||||
export function likedAlbums(params) {
|
||||
return request({
|
||||
url: '/album/sublist',
|
||||
method: 'get',
|
||||
params: {
|
||||
limit: params.limit,
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
})
|
||||
}
|
||||
// export function likedAlbums(params) {
|
||||
// return request({
|
||||
// url: '/album/sublist',
|
||||
// method: 'get',
|
||||
// params: {
|
||||
// limit: params.limit,
|
||||
// timestamp: new Date().getTime(),
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
|
||||
/**
|
||||
* 获取收藏的歌手(需要登录)
|
||||
* 说明 : 调用此接口可获取到用户收藏的歌手
|
||||
*/
|
||||
export function likedArtists(params) {
|
||||
return request({
|
||||
url: '/artist/sublist',
|
||||
method: 'get',
|
||||
params: {
|
||||
limit: params.limit,
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
})
|
||||
}
|
||||
// export function likedArtists(params) {
|
||||
// return request({
|
||||
// url: '/artist/sublist',
|
||||
// method: 'get',
|
||||
// params: {
|
||||
// limit: params.limit,
|
||||
// timestamp: new Date().getTime(),
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
|
||||
/**
|
||||
* 获取收藏的MV(需要登录)
|
||||
* 说明 : 调用此接口可获取到用户收藏的MV
|
||||
*/
|
||||
export function likedMVs(params) {
|
||||
return request({
|
||||
url: '/mv/sublist',
|
||||
method: 'get',
|
||||
params: {
|
||||
limit: params.limit,
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
})
|
||||
}
|
||||
// export function likedMVs(params) {
|
||||
// return request({
|
||||
// url: '/mv/sublist',
|
||||
// method: 'get',
|
||||
// params: {
|
||||
// limit: params.limit,
|
||||
// timestamp: new Date().getTime(),
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
|
||||
/**
|
||||
* 上传歌曲到云盘(需要登录)
|
||||
*/
|
||||
export function uploadSong(file) {
|
||||
let formData = new FormData()
|
||||
formData.append('songFile', file)
|
||||
return request({
|
||||
url: '/cloud',
|
||||
method: 'post',
|
||||
params: {
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
timeout: 200000,
|
||||
}).catch(error => {
|
||||
alert(`上传失败,Error: ${error}`)
|
||||
})
|
||||
}
|
||||
// export function uploadSong(file) {
|
||||
// let formData = new FormData()
|
||||
// formData.append('songFile', file)
|
||||
// return request({
|
||||
// url: '/cloud',
|
||||
// method: 'post',
|
||||
// params: {
|
||||
// timestamp: new Date().getTime(),
|
||||
// },
|
||||
// data: formData,
|
||||
// headers: {
|
||||
// 'Content-Type': 'multipart/form-data',
|
||||
// },
|
||||
// timeout: 200000,
|
||||
// }).catch(error => {
|
||||
// alert(`上传失败,Error: ${error}`)
|
||||
// })
|
||||
// }
|
||||
|
||||
/**
|
||||
* 获取云盘歌曲(需要登录)
|
||||
|
@ -232,40 +232,40 @@ export function uploadSong(file) {
|
|||
* @param {number} params.limit
|
||||
* @param {number=} params.offset
|
||||
*/
|
||||
export function cloudDisk(params = {}) {
|
||||
params.timestamp = new Date().getTime()
|
||||
return request({
|
||||
url: '/user/cloud',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
// export function cloudDisk(params = {}) {
|
||||
// params.timestamp = new Date().getTime()
|
||||
// return request({
|
||||
// url: '/user/cloud',
|
||||
// method: 'get',
|
||||
// params,
|
||||
// })
|
||||
// }
|
||||
|
||||
/**
|
||||
* 获取云盘歌曲详情(需要登录)
|
||||
*/
|
||||
export function cloudDiskTrackDetail(id) {
|
||||
return request({
|
||||
url: '/user/cloud/detail',
|
||||
method: 'get',
|
||||
params: {
|
||||
timestamp: new Date().getTime(),
|
||||
id,
|
||||
},
|
||||
})
|
||||
}
|
||||
// export function cloudDiskTrackDetail(id) {
|
||||
// return request({
|
||||
// url: '/user/cloud/detail',
|
||||
// method: 'get',
|
||||
// params: {
|
||||
// timestamp: new Date().getTime(),
|
||||
// id,
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
|
||||
/**
|
||||
* 删除云盘歌曲(需要登录)
|
||||
* @param {Array} id
|
||||
*/
|
||||
export function cloudDiskTrackDelete(id) {
|
||||
return request({
|
||||
url: '/user/cloud/del',
|
||||
method: 'get',
|
||||
params: {
|
||||
timestamp: new Date().getTime(),
|
||||
id,
|
||||
},
|
||||
})
|
||||
}
|
||||
// export function cloudDiskTrackDelete(id) {
|
||||
// return request({
|
||||
// url: '/user/cloud/del',
|
||||
// method: 'get',
|
||||
// params: {
|
||||
// timestamp: new Date().getTime(),
|
||||
// id,
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
|
|
|
@ -36,11 +36,11 @@ const Button = ({
|
|||
{
|
||||
'px-4 py-1.5': shape === Shape.Default,
|
||||
'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,
|
||||
'bg-gray-100 dark:bg-gray-700': color === 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,
|
||||
}
|
||||
)}
|
||||
|
|
|
@ -38,11 +38,14 @@ const getSubtitleText = (
|
|||
subtitle: Subtitle
|
||||
) => {
|
||||
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 =
|
||||
'publishTime' in item
|
||||
? formatDate(item.publishTime ?? 0, 'en', 'YYYY')
|
||||
: 'unknown'
|
||||
const types = {
|
||||
('publishTime' in item &&
|
||||
formatDate(item.publishTime ?? 0, 'en', 'YYYY')) ||
|
||||
'unknown'
|
||||
|
||||
const type = {
|
||||
playlist: 'playlist',
|
||||
album: 'Album',
|
||||
专辑: 'Album',
|
||||
|
@ -50,16 +53,17 @@ const getSubtitleText = (
|
|||
'EP/Single': 'EP',
|
||||
EP: 'EP',
|
||||
unknown: 'unknown',
|
||||
}
|
||||
const type = 'type' in item ? item.type || 'unknown' : 'unknown'
|
||||
const artist = 'artist' in item ? item.artist.name : 'unknown'
|
||||
精选集: 'Collection',
|
||||
}[('type' in item && typeof item.type !== 'number' && item.type) || 'unknown']
|
||||
|
||||
const table = {
|
||||
[Subtitle.CREATOR]: `by ${nickname}`,
|
||||
[Subtitle.TYPE_RELEASE_YEAR]: `${types[type]} · ${releaseYear}`,
|
||||
[Subtitle.TYPE_RELEASE_YEAR]: `${type} · ${releaseYear}`,
|
||||
[Subtitle.ARTIST]: artist,
|
||||
[Subtitle.COPYWRITER]: copywriter,
|
||||
}
|
||||
return table[subtitle] ?? item[subtitle]
|
||||
|
||||
return table[subtitle]
|
||||
}
|
||||
|
||||
const getImageUrl = (item: Album | Playlist | Artist) => {
|
||||
|
@ -162,7 +166,7 @@ const CoverRow = ({
|
|||
)}
|
||||
<span
|
||||
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}
|
||||
</span>
|
||||
|
|
|
@ -18,7 +18,7 @@ const IconButton = ({
|
|||
className,
|
||||
'relative transform cursor-default p-2 transition duration-200',
|
||||
!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'
|
||||
)}
|
||||
>
|
||||
|
|
|
@ -49,7 +49,7 @@ const PlayingTrack = () => {
|
|||
<div className="flex flex-col justify-center leading-tight">
|
||||
<div
|
||||
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}
|
||||
</div>
|
||||
|
@ -58,7 +58,7 @@ const PlayingTrack = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<IconButton>
|
||||
<IconButton onClick={() => toast('Work in progress')}>
|
||||
<SvgIcon
|
||||
className="h-4 w-4 text-black dark:text-white"
|
||||
name="heart-outline"
|
||||
|
@ -100,19 +100,19 @@ const MediaControls = () => {
|
|||
const Others = () => {
|
||||
return (
|
||||
<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" />
|
||||
</IconButton>
|
||||
<IconButton>
|
||||
<IconButton onClick={() => toast('Work in progress')}>
|
||||
<SvgIcon className="h-4 w-4" name="repeat" />
|
||||
</IconButton>
|
||||
<IconButton>
|
||||
<IconButton onClick={() => toast('Work in progress')}>
|
||||
<SvgIcon className="h-4 w-4" name="shuffle" />
|
||||
</IconButton>
|
||||
<IconButton>
|
||||
<IconButton onClick={() => toast('Work in progress')}>
|
||||
<SvgIcon className="h-4 w-4" name="volume" />
|
||||
</IconButton>
|
||||
<IconButton>
|
||||
<IconButton onClick={() => toast('Work in progress')}>
|
||||
<SvgIcon className="h-4 w-4" name="chevron-up" />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
@ -149,7 +149,7 @@ const Progress = () => {
|
|||
|
||||
const Player = () => {
|
||||
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 />
|
||||
|
||||
<PlayingTrack />
|
||||
|
|
|
@ -52,7 +52,7 @@ const PrimaryTabs = () => {
|
|||
</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>
|
||||
)
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ const Playlists = () => {
|
|||
})
|
||||
|
||||
return (
|
||||
<div className="overflow-auto pb-[4.6rem]">
|
||||
<div className="mb-16 overflow-auto pb-2">
|
||||
{playlists?.playlist?.map(playlist => (
|
||||
<NavLink
|
||||
key={playlist.id}
|
||||
|
@ -89,7 +89,7 @@ const Sidebar = () => {
|
|||
return (
|
||||
<div
|
||||
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 />
|
||||
<Playlists />
|
||||
|
|
|
@ -80,7 +80,7 @@ const Topbar = () => {
|
|||
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',
|
||||
!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">
|
||||
|
|
|
@ -36,9 +36,9 @@ const useScroll = (
|
|||
|
||||
useEffect(() => {
|
||||
if (!ref) return
|
||||
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||
if (!e.currentTarget && !e.target) return
|
||||
const target = e.currentTarget || e.target
|
||||
const handleScroll = (e: Event) => {
|
||||
if (!e.target) return
|
||||
const target = e.target as HTMLElement
|
||||
|
||||
const arrivedState: ArrivedState = {
|
||||
left: target.scrollLeft <= 0 + (offset?.left || 0),
|
||||
|
|
|
@ -9,8 +9,53 @@ import TracksAlbum from '@/components/TracksAlbum'
|
|||
import useAlbum from '@/hooks/useAlbum'
|
||||
import useArtistAlbums from '@/hooks/useArtistAlbums'
|
||||
import { player } from '@/store'
|
||||
import { State as PlayerState } from '@/utils/player'
|
||||
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 = ({
|
||||
album,
|
||||
isLoading,
|
||||
|
@ -37,11 +82,11 @@ const Header = ({
|
|||
<Fragment>
|
||||
<img
|
||||
src={coverUrl}
|
||||
className="absolute top-[-50%] w-full blur-[100px]"
|
||||
className="absolute -top-full w-full blur-[100px]"
|
||||
/>
|
||||
<img
|
||||
src={coverUrl}
|
||||
className="absolute top-[-50%] w-full blur-[100px]"
|
||||
className="absolute -top-full w-full blur-[100px]"
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
|
@ -81,17 +126,18 @@ const Header = ({
|
|||
{/* Info */}
|
||||
<div className="z-10 flex h-full flex-col justify-between">
|
||||
{/* Name */}
|
||||
{!isLoading && (
|
||||
{isLoading ? (
|
||||
<Skeleton className="w-3/4 text-6xl">PLACEHOLDER</Skeleton>
|
||||
) : (
|
||||
<div className="text-6xl font-bold dark:text-white">
|
||||
{album?.name}
|
||||
</div>
|
||||
)}
|
||||
{isLoading && (
|
||||
<Skeleton className="w-3/4 text-6xl">PLACEHOLDER</Skeleton>
|
||||
)}
|
||||
|
||||
{/* 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">
|
||||
Album by{' '}
|
||||
<NavLink
|
||||
|
@ -102,43 +148,39 @@ const Header = ({
|
|||
</NavLink>
|
||||
</div>
|
||||
)}
|
||||
{isLoading && (
|
||||
<Skeleton className="mt-5 w-64 text-lg">PLACEHOLDER</Skeleton>
|
||||
)}
|
||||
|
||||
{/* 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">
|
||||
{dayjs(album?.publishTime || 0).year()} · {album?.size} Songs,{' '}
|
||||
{albumDuration}
|
||||
</div>
|
||||
)}
|
||||
{isLoading && (
|
||||
<Skeleton className="w-72 translate-y-px text-sm">
|
||||
PLACEHOLDER
|
||||
</Skeleton>
|
||||
)}
|
||||
|
||||
{/* 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">
|
||||
{album?.description}
|
||||
</div>
|
||||
)}
|
||||
{isLoading && (
|
||||
<Skeleton className="mt-5 min-h-[2.5rem] w-1/2 text-sm">
|
||||
PLACEHOLDER
|
||||
</Skeleton>
|
||||
)}
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="mt-5 flex gap-4">
|
||||
<Button onClick={() => handlePlay()} isSkelton={isLoading}>
|
||||
<SvgIcon name="play" className="mr-2 h-4 w-4" />
|
||||
PLAY
|
||||
</Button>
|
||||
<PlayButton {...{ album, handlePlay, isLoading }} />
|
||||
|
||||
<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" />
|
||||
</Button>
|
||||
|
||||
|
@ -146,6 +188,7 @@ const Header = ({
|
|||
color={ButtonColor.Gray}
|
||||
iconColor={ButtonColor.Gray}
|
||||
isSkelton={isLoading}
|
||||
onClick={() => toast('Work in progress')}
|
||||
>
|
||||
<SvgIcon name="more" className="h-4 w-4" />
|
||||
</Button>
|
||||
|
|
|
@ -189,7 +189,7 @@ const LoginWithEmail = () => {
|
|||
<Fragment>
|
||||
<EmailInput {...{ email, setEmail }} />
|
||||
<PasswordInput {...{ password, setPassword }} />
|
||||
<LoginButton />
|
||||
<LoginButton onClick={() => toast('Work in progress')} disabled={true} />
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -115,7 +115,11 @@ const Header = memo(
|
|||
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" />
|
||||
</Button>
|
||||
|
||||
|
@ -123,6 +127,7 @@ const Header = memo(
|
|||
color={ButtonColor.Gray}
|
||||
iconColor={ButtonColor.Gray}
|
||||
isSkelton={isLoading}
|
||||
onClick={() => toast('Work in progress')}
|
||||
>
|
||||
<SvgIcon name="more" className="h-4 w-4" />
|
||||
</Button>
|
||||
|
|
|
@ -44,7 +44,7 @@ export class Player {
|
|||
repeatMode: RepeatMode = RepeatMode.OFF
|
||||
|
||||
constructor() {
|
||||
window.player = this
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,14 +3,13 @@ import dotenv from 'dotenv'
|
|||
import { builtinModules } from 'module'
|
||||
import path, { join } from 'path'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import { Plugin, defineConfig } from 'vite'
|
||||
import { defineConfig, Plugin } from 'vite'
|
||||
import resolve from 'vite-plugin-resolve'
|
||||
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
|
||||
import { visualizer } from 'rollup-plugin-visualizer'
|
||||
|
||||
dotenv.config({ path: path.resolve(process.cwd(), '.env') })
|
||||
|
||||
console.log(join(__dirname, '../../.eslintrc.js'))
|
||||
|
||||
/**
|
||||
* @see https://vitejs.dev/config/
|
||||
*/
|
||||
|
@ -60,6 +59,16 @@ export default defineConfig({
|
|||
build: {
|
||||
sourcemap: process.env.NODE_ENV === 'debug',
|
||||
outDir: '../../dist/renderer',
|
||||
rollupOptions: {
|
||||
plugins: [
|
||||
visualizer({
|
||||
filename: './bundle-stats-renderer.html',
|
||||
gzipSize: true,
|
||||
projectRoot: 'packages/renderer',
|
||||
template: 'treemap',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
|
|
@ -3,7 +3,7 @@ module.exports = {
|
|||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
semi: false,
|
||||
jsxBracketSameLine: false,
|
||||
bracketSameLine: false,
|
||||
arrowParens: 'avoid',
|
||||
endOfLine: 'lf',
|
||||
bracketSpacing: true,
|
||||
|
|
|
@ -86,9 +86,14 @@ const color = (hex, text) => {
|
|||
|
||||
// bootstrap
|
||||
logPrefix(color('#eab308', '[vite] '))
|
||||
console.log('building renderer')
|
||||
const server = await createServer({
|
||||
configFile: 'packages/renderer/vite.config.ts',
|
||||
})
|
||||
await server.listen()
|
||||
|
||||
console.log('building preload')
|
||||
await watchPreload(server)
|
||||
|
||||
console.log('building main')
|
||||
await watchMain(server)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const colors = require('tailwindcss/colors')
|
||||
|
||||
module.exports = {
|
||||
|
|
11
yarn.lock
11
yarn.lock
|
@ -565,6 +565,13 @@
|
|||
dependencies:
|
||||
"@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":
|
||||
version "4.1.7"
|
||||
resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz"
|
||||
|
@ -581,7 +588,7 @@
|
|||
"@types/qs" "*"
|
||||
"@types/range-parser" "*"
|
||||
|
||||
"@types/express@^4.17.13":
|
||||
"@types/express@*", "@types/express@^4.17.13":
|
||||
version "4.17.13"
|
||||
resolved "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz"
|
||||
integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==
|
||||
|
@ -5934,7 +5941,7 @@ roarr@^2.15.3:
|
|||
|
||||
rollup-plugin-visualizer@^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==
|
||||
dependencies:
|
||||
nanoid "^3.1.32"
|
||||
|
|
Loading…
Reference in New Issue
Block a user