This commit is contained in:
qier222 2023-09-04 12:51:56 +08:00
parent 32050e4553
commit 6aee8ae38e
No known key found for this signature in database
41 changed files with 523 additions and 415 deletions

View File

@ -25,10 +25,10 @@
"devDependencies": { "devDependencies": {
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^8.31.0", "eslint": "^8.31.0",
"prettier": "^2.8.1", "prettier": "^2.8.8",
"turbo": "^1.8.3", "turbo": "^1.9.3",
"typescript": "^4.9.5", "typescript": "^4.9.5",
"tsx": "^3.12.1", "tsx": "^3.12.7",
"prettier-plugin-tailwindcss": "^0.2.1" "prettier-plugin-tailwindcss": "^0.2.8"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

@ -13,6 +13,7 @@ import fastFolderSize from 'fast-folder-size'
import path from 'path' import path from 'path'
import prettyBytes from 'pretty-bytes' import prettyBytes from 'pretty-bytes'
import { db, Tables } from './db' import { db, Tables } from './db'
import { promisify } from 'util'
log.info('[electron] ipcMain.ts') log.info('[electron] ipcMain.ts')
@ -137,15 +138,15 @@ function initOtherIpcMain() {
/** /**
* API缓存 * API缓存
*/ */
on(IpcChannels.ClearAPICache, () => { handle(IpcChannels.ClearAPICache, async () => {
// db.truncate(Tables.Track) db.truncate(Tables.Track)
// db.truncate(Tables.Album) db.truncate(Tables.Album)
// db.truncate(Tables.Artist) db.truncate(Tables.Artist)
// db.truncate(Tables.Playlist) db.truncate(Tables.Playlist)
// db.truncate(Tables.ArtistAlbum) db.truncate(Tables.ArtistAlbum)
// db.truncate(Tables.AccountData) db.truncate(Tables.AccountData)
// db.truncate(Tables.Audio) db.truncate(Tables.Audio)
// db.vacuum() db.vacuum()
}) })
/** /**
@ -170,6 +171,31 @@ function initOtherIpcMain() {
} }
}) })
// 获取缓存位置
handle(IpcChannels.GetCachePath, async () => {
return path.join(app.getPath('userData'), './audio_cache')
})
/**
*
*/
handle(IpcChannels.GetAudioCacheSize, async () => {
const fastFolderSizeAsync = promisify(fastFolderSize)
const bytes = await fastFolderSizeAsync(path.join(app.getPath('userData'), './audio_cache'))
return prettyBytes(bytes ?? 0)
})
handle(IpcChannels.ClearAudioCache, async () => {
try {
const audioCachePath = path.join(app.getPath('userData'), './audio_cache')
fs.rmdirSync(audioCachePath, { recursive: true })
fs.mkdirSync(audioCachePath)
return true
} catch (e) {
return false
}
})
/** /**
* *
*/ */
@ -178,17 +204,6 @@ function initOtherIpcMain() {
cache.set(CacheAPIs.CoverColor, { id, color }) cache.set(CacheAPIs.CoverColor, { id, color })
}) })
/**
*
*/
on(IpcChannels.GetAudioCacheSize, event => {
fastFolderSize(path.join(app.getPath('userData'), './audio_cache'), (error, bytes) => {
if (error) throw error
event.returnValue = prettyBytes(bytes ?? 0)
})
})
/** /**
* Apple Music获取专辑信息 * Apple Music获取专辑信息
*/ */
@ -235,30 +250,26 @@ function initOtherIpcMain() {
* tables到json文件便table大小dev环境 * tables到json文件便table大小dev环境
*/ */
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
// on(IpcChannels.DevDbExportJson, () => { on(IpcChannels.DevDbExportJson, () => {
// const tables = [ const tables = [
// Tables.ArtistAlbum, Tables.ArtistAlbum,
// Tables.Playlist, Tables.Playlist,
// Tables.Album, Tables.Album,
// Tables.Track, Tables.Track,
// Tables.Artist, Tables.Artist,
// Tables.Audio, Tables.Audio,
// Tables.AccountData, Tables.AccountData,
// Tables.Lyric, Tables.Lyrics,
// ] ]
// tables.forEach(table => { tables.forEach(table => {
// const data = db.findAll(table) const data = db.findAll(table)
// fs.writeFile( fs.writeFile(`./tmp/${table}.json`, JSON.stringify(data), function (err) {
// `./tmp/${table}.json`, if (err) {
// JSON.stringify(data), return console.log(err)
// function (err) { }
// if (err) { console.log('The file was saved!')
// return console.log(err) })
// } })
// console.log('The file was saved!') })
// }
// )
// })
// })
} }
} }

View File

@ -27,7 +27,7 @@
"@fastify/static": "^6.6.1", "@fastify/static": "^6.6.1",
"@sentry/electron": "^3.0.7", "@sentry/electron": "^3.0.7",
"NeteaseCloudMusicApi": "^4.8.9", "NeteaseCloudMusicApi": "^4.8.9",
"better-sqlite3": "8.1.0", "better-sqlite3": "8.3.0",
"change-case": "^4.1.2", "change-case": "^4.1.2",
"compare-versions": "^4.1.3", "compare-versions": "^4.1.3",
"electron-log": "^4.4.8", "electron-log": "^4.4.8",
@ -39,12 +39,12 @@
"ytdl-core": "^4.11.2" "ytdl-core": "^4.11.2"
}, },
"devDependencies": { "devDependencies": {
"@types/better-sqlite3": "^7.6.3", "@types/better-sqlite3": "^7.6.4",
"@vitest/ui": "^0.20.3", "@vitest/ui": "^0.20.3",
"axios": "^1.3.4", "axios": "^1.3.4",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"electron": "^23.1.4", "electron": "^24.1.3",
"electron-builder": "23.6.0", "electron-builder": "23.6.0",
"electron-devtools-installer": "^3.2.0", "electron-devtools-installer": "^3.2.0",
"electron-rebuild": "^3.2.9", "electron-rebuild": "^3.2.9",

View File

@ -27,6 +27,8 @@ export const enum IpcChannels {
GetAlbumFromAppleMusic = 'GetAlbumFromAppleMusic', GetAlbumFromAppleMusic = 'GetAlbumFromAppleMusic',
GetArtistFromAppleMusic = 'GetArtistFromAppleMusic', GetArtistFromAppleMusic = 'GetArtistFromAppleMusic',
Logout = 'Logout', Logout = 'Logout',
GetCachePath = 'GetCachePath',
ClearAudioCache = 'ClearAudioCache',
} }
// ipcMain.on params // ipcMain.on params
@ -70,6 +72,8 @@ export interface IpcChannelsParams {
} }
[IpcChannels.GetArtistFromAppleMusic]: { id: number; name: string } [IpcChannels.GetArtistFromAppleMusic]: { id: number; name: string }
[IpcChannels.Logout]: void [IpcChannels.Logout]: void
[IpcChannels.GetCachePath]: void
[IpcChannels.ClearAudioCache]: void
} }
// ipcRenderer.on params // ipcRenderer.on params
@ -95,4 +99,6 @@ export interface IpcChannelsReturns {
[IpcChannels.GetAlbumFromAppleMusic]: AppleMusicAlbum | undefined [IpcChannels.GetAlbumFromAppleMusic]: AppleMusicAlbum | undefined
[IpcChannels.GetArtistFromAppleMusic]: AppleMusicArtist | undefined [IpcChannels.GetArtistFromAppleMusic]: AppleMusicArtist | undefined
[IpcChannels.Logout]: void [IpcChannels.Logout]: void
[IpcChannels.GetCachePath]: string
[IpcChannels.ClearAudioCache]: boolean
} }

View File

@ -1,5 +1,5 @@
import { fetchAudioSource, fetchTracks } from '@/web/api/track' import { fetchAudioSource, fetchTracks } from '@/web/api/track'
import type { } from '@/web/api/track' import type {} from '@/web/api/track'
import reactQueryClient from '@/web/utils/reactQueryClient' import reactQueryClient from '@/web/utils/reactQueryClient'
import { IpcChannels } from '@/shared/IpcChannels' import { IpcChannels } from '@/shared/IpcChannels'
import { import {

View File

@ -26,7 +26,7 @@ const Artist = ({ artist }: { artist: Artist }) => {
/> />
<div <div
onClick={to} onClick={to}
className='line-clamp-1 mt-2.5 text-12 font-medium text-neutral-700 dark:text-neutral-600 lg:text-14 lg:font-bold' className='mt-2.5 line-clamp-1 text-12 font-medium text-neutral-700 dark:text-neutral-600 lg:text-14 lg:font-bold'
> >
{artist.name} {artist.name}
</div> </div>
@ -46,7 +46,7 @@ const Placeholder = ({ row }: { row: number }) => {
minWidth: '96px', minWidth: '96px',
}} }}
/> />
<div className='line-clamp-1 mt-2.5 w-1/2 rounded-full text-12 font-medium text-transparent dark:bg-neutral-800 lg:text-14 lg:font-bold'> <div className='mt-2.5 line-clamp-1 w-1/2 rounded-full text-12 font-medium text-transparent dark:bg-neutral-800 lg:text-14 lg:font-bold'>
NAME NAME
</div> </div>
</div> </div>

View File

@ -98,7 +98,7 @@ const MenuItem = ({
></div> ></div>
{/* 增加三角形避免斜着移动到submenu时意外关闭菜单 */} {/* 增加三角形避免斜着移动到submenu时意外关闭菜单 */}
<div className='absolute -right-8 -bottom-6 h-12 w-12 rotate-45'></div> <div className='absolute -bottom-6 -right-8 h-12 w-12 rotate-45'></div>
<div className='absolute -right-8 -top-6 h-12 w-12 rotate-45'></div> <div className='absolute -right-8 -top-6 h-12 w-12 rotate-45'></div>
</> </>
)} )}

View File

@ -56,7 +56,7 @@ const Album = ({
onMouseOver={prefetch} onMouseOver={prefetch}
/> />
{title && ( {title && (
<div className='line-clamp-2 mt-2 text-14 font-medium text-neutral-300'>{title}</div> <div className='mt-2 line-clamp-2 text-14 font-medium text-neutral-300'>{title}</div>
)} )}
{subtitle && <div className='mt-1 text-14 font-medium text-neutral-700'>{subtitle}</div>} {subtitle && <div className='mt-1 text-14 font-medium text-neutral-700'>{subtitle}</div>}
</div> </div>

View File

@ -48,7 +48,7 @@ function DescriptionViewer({
> >
<div className='relative'> <div className='relative'>
{/* Title */} {/* Title */}
<div className='line-clamp-1 absolute -top-8 mx-44 max-w-2xl select-none text-32 font-extrabold text-neutral-100'> <div className='absolute -top-8 mx-44 line-clamp-1 max-w-2xl select-none text-32 font-extrabold text-neutral-100'>
{title} {title}
</div> </div>

View File

@ -33,7 +33,7 @@ const Layout = () => {
{showPlayer && <Player />} {showPlayer && <Player />}
{window.env?.isMac && ( {window.env?.isMac && (
<div className='fixed top-6 left-6 z-30 translate-y-0.5'> <div className='fixed left-6 top-6 z-30 translate-y-0.5'>
<TrafficLight /> <TrafficLight />
</div> </div>
)} )}

View File

@ -53,7 +53,7 @@ const TabName = () => {
return ( return (
<div <div
className={cx( className={cx(
'absolute bottom-8 right-0 left-0 z-10 flex rotate-180 select-none items-center font-bold text-brand-600 dark:text-brand-700', 'absolute bottom-8 left-0 right-0 z-10 flex rotate-180 select-none items-center font-bold text-brand-600 dark:text-brand-700',
css` css`
writing-mode: vertical-rl; writing-mode: vertical-rl;
text-orientation: mixed; text-orientation: mixed;
@ -144,7 +144,7 @@ const MenuBar = () => {
<div <div
className={cx( className={cx(
'app-region-drag relative flex h-full w-full flex-col justify-center', 'app-region-drag relative flex h-full w-full flex-col justify-center',
'lg:fixed lg:left-0 lg:top-0 lg:bottom-0', 'lg:fixed lg:bottom-0 lg:left-0 lg:top-0',
css` css`
${bp.lg} { ${bp.lg} {
width: 104px; width: 104px;

View File

@ -7,7 +7,7 @@ const Progress = () => {
const { track, progress } = useSnapshot(player) const { track, progress } = useSnapshot(player)
return ( return (
<div className='mt-9 mb-4 flex w-full flex-col'> <div className='mb-4 mt-9 flex w-full flex-col'>
<Slider <Slider
min={0} min={0}
max={(track?.dt ?? 100000) / 1000} max={(track?.dt ?? 100000) / 1000}

View File

@ -13,7 +13,7 @@ const Player = () => {
<MotionConfig transition={{ duration: 0.6 }}> <MotionConfig transition={{ duration: 0.6 }}>
<div <div
className={cx( className={cx(
'fixed right-6 bottom-6 flex w-full flex-col justify-between overflow-hidden', 'fixed bottom-6 right-6 flex w-full flex-col justify-between overflow-hidden',
css` css`
width: 318px; width: 318px;
` `

View File

@ -64,7 +64,7 @@ const PlayerMobile = () => {
uiStates.mobileShowPlayingNext = true uiStates.mobileShowPlayingNext = true
}} }}
className={cx( className={cx(
'absolute right-0 left-0 flex justify-center', 'absolute left-0 right-0 flex justify-center',
css` css`
--height: 20px; --height: 20px;
height: var(--height); height: var(--height);
@ -100,7 +100,7 @@ const PlayerMobile = () => {
> >
<div className='flex-shrink-0'> <div className='flex-shrink-0'>
<div className='line-clamp-1 text-14 font-bold text-white'>{track?.name}</div> <div className='line-clamp-1 text-14 font-bold text-white'>{track?.name}</div>
<div className='line-clamp-1 mt-1 text-12 font-bold text-white/60'> <div className='mt-1 line-clamp-1 text-12 font-bold text-white/60'>
{track?.ar?.map(a => a.name).join(', ')} {track?.ar?.map(a => a.name).join(', ')}
</div> </div>
</div> </div>
@ -109,7 +109,7 @@ const PlayerMobile = () => {
<div <div
className={cx( className={cx(
'absolute left-0 top-0 bottom-0 w-3 ', 'absolute bottom-0 left-0 top-0 w-3 ',
css` css`
background: linear-gradient(to right, ${bgColor.to}, transparent); background: linear-gradient(to right, ${bgColor.to}, transparent);
` `
@ -117,7 +117,7 @@ const PlayerMobile = () => {
></div> ></div>
<div <div
className={cx( className={cx(
'absolute right-0 top-0 bottom-0 w-3 bg-red-200', 'absolute bottom-0 right-0 top-0 w-3 bg-red-200',
css` css`
background: linear-gradient(to left, ${bgColor.to}, transparent); background: linear-gradient(to left, ${bgColor.to}, transparent);
` `

View File

@ -36,7 +36,7 @@ const RepeatButton = () => {
)} )}
style={buttonStyle} style={buttonStyle}
> >
<div className='absolute top-1/2 left-1/2 h-2 w-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-white opacity-0 blur group-hover:opacity-100'></div> <div className='absolute left-1/2 top-1/2 h-2 w-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-white opacity-0 blur group-hover:opacity-100'></div>
<Icon name='repeat-1' className='h-7 w-7' /> <Icon name='repeat-1' className='h-7 w-7' />
</motion.button> </motion.button>
) )
@ -61,7 +61,7 @@ const ShuffleButton = () => {
style={buttonStyle} style={buttonStyle}
> >
<Icon name='shuffle' className='h-7 w-7' /> <Icon name='shuffle' className='h-7 w-7' />
<div className='absolute top-1/2 left-1/2 h-2 w-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-white opacity-0 blur group-hover:opacity-100'></div> <div className='absolute left-1/2 top-1/2 h-2 w-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-white opacity-0 blur group-hover:opacity-100'></div>
</motion.button> </motion.button>
) )
} }
@ -71,7 +71,7 @@ const Header = () => {
return ( return (
<div <div
className={cx( className={cx(
'absolute top-0 left-0 z-20 flex w-full items-center justify-between bg-contain bg-repeat-x px-7 pb-6 text-14 font-bold lg:px-0' 'absolute left-0 top-0 z-20 flex w-full items-center justify-between bg-contain bg-repeat-x px-7 pb-6 text-14 font-bold lg:px-0'
)} )}
> >
<div className='flex text-neutral-300'> <div className='flex text-neutral-300'>
@ -134,7 +134,7 @@ const Track = ({
> >
{track?.name} {track?.name}
</div> </div>
<div className='line-clamp-1 mt-1 text-14 font-bold text-neutral-200 dark:text-white/25'> <div className='mt-1 line-clamp-1 text-14 font-bold text-neutral-200 dark:text-white/25'>
{track?.ar.map(a => a.name).join(', ')} {track?.ar.map(a => a.name).join(', ')}
</div> </div>
</div> </div>

View File

@ -22,7 +22,7 @@ function Tabs<T>({
<div <div
key={tab.id as string} key={tab.id as string}
className={cx( className={cx(
'mr-2.5 rounded-12 py-3 px-6 text-16 font-medium backdrop-blur transition duration-500', 'mr-2.5 rounded-12 px-6 py-3 text-16 font-medium backdrop-blur transition duration-500',
value === tab.id value === tab.id
? 'bg-brand-700 text-white' ? 'bg-brand-700 text-white'
: 'dark:bg-white/10 dark:text-white/20 hover:dark:bg-white/20 hover:dark:text-white/40' : 'dark:bg-white/10 dark:text-white/20 hover:dark:bg-white/20 hover:dark:text-white/40'

View File

@ -44,7 +44,7 @@ const TopbarDesktop = () => {
return ( return (
<div <div
className={cx( className={cx(
'app-region-drag fixed top-0 left-0 right-0 z-20 flex items-center justify-between bg-contain pt-11 pb-10 pr-6', 'app-region-drag fixed left-0 right-0 top-0 z-20 flex items-center justify-between bg-contain pb-10 pr-6 pt-11',
css` css`
padding-left: 144px; padding-left: 144px;
` `

View File

@ -74,7 +74,7 @@ const Info = ({
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ duration: 0.3 }} transition={{ duration: 0.3 }}
className='line-clamp-3 mt-6 whitespace-pre-wrap text-14 font-bold transition-colors duration-300 dark:text-white/40 dark:hover:text-white/60' className='mt-6 line-clamp-3 whitespace-pre-wrap text-14 font-bold transition-colors duration-300 dark:text-white/40 dark:hover:text-white/60'
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: description, __html: description,
}} }}

View File

@ -212,7 +212,7 @@ const Controls = ({
visible: { y: 0, opacity: 1 }, visible: { y: 0, opacity: 1 },
}} }}
transition={animationTransition} transition={animationTransition}
className='absolute bottom-5 left-5 flex rounded-20 bg-black/70 py-3 px-5 backdrop-blur-3xl' className='absolute bottom-5 left-5 flex rounded-20 bg-black/70 px-5 py-3 backdrop-blur-3xl'
> >
<button <button
onClick={togglePlay} onClick={togglePlay}

View File

@ -11,7 +11,7 @@ const VideoRow = ({ videos }: { videos: Video[] }) => {
src={video.coverUrl} src={video.coverUrl}
className='aspect-video w-full rounded-24 border border-white/5 object-contain' className='aspect-video w-full rounded-24 border border-white/5 object-contain'
/> />
<div className='line-clamp-2 mt-2 text-12 font-medium text-neutral-600'> <div className='mt-2 line-clamp-2 text-12 font-medium text-neutral-600'>
{video.creator?.at(0)?.userName} - {video.title} {video.creator?.at(0)?.userName} - {video.title}
</div> </div>
</div> </div>

View File

@ -51,7 +51,7 @@ const useHoverLightSpot = (
<motion.div <motion.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
className={cx( className={cx(
'pointer-events-none absolute top-0 left-0 rounded-full transition-opacity duration-400', 'pointer-events-none absolute left-0 top-0 rounded-full transition-opacity duration-400',
css` css`
filter: blur(16px); filter: blur(16px);
background: rgb(255, 255, 255); background: rgb(255, 255, 255);

View File

@ -4,7 +4,7 @@ import zhCN from './locales/zh-cn.json'
import enUS from './locales/en-us.json' import enUS from './locales/en-us.json'
export const supportedLanguages = ['zh-CN', 'en-US'] as const export const supportedLanguages = ['zh-CN', 'en-US'] as const
export type SupportedLanguage = typeof supportedLanguages[number] export type SupportedLanguage = (typeof supportedLanguages)[number]
declare module 'react-i18next' { declare module 'react-i18next' {
interface CustomTypeOptions { interface CustomTypeOptions {

View File

@ -40,14 +40,14 @@
"qrcode": "^1.5.1", "qrcode": "^1.5.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-ga4": "^1.4.1", "react-ga4": "^2.1.0",
"react-hot-toast": "^2.4.0", "react-hot-toast": "^2.4.0",
"react-i18next": "^12.1.5", "react-i18next": "^12.1.5",
"react-router-dom": "^6.6.1", "react-router-dom": "^6.6.1",
"react-use": "^17.4.0", "react-use": "^17.4.0",
"react-use-measure": "^2.1.1", "react-use-measure": "^2.1.1",
"react-virtuoso": "^2.16.6", "react-virtuoso": "^2.16.6",
"valtio": "^1.8.0" "valtio": "^1.10.4"
}, },
"devDependencies": { "devDependencies": {
"@testing-library/react": "^13.3.0", "@testing-library/react": "^13.3.0",
@ -58,7 +58,7 @@
"@types/qrcode": "^1.4.2", "@types/qrcode": "^1.4.2",
"@types/react": "^18.0.15", "@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"@vitejs/plugin-react-swc": "^3.2.0", "@vitejs/plugin-react-swc": "^3.3.0",
"@vitest/ui": "^0.26.3", "@vitest/ui": "^0.26.3",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.13",
"c8": "^7.12.0", "c8": "^7.12.0",
@ -69,10 +69,10 @@
"prettier": "*", "prettier": "*",
"prettier-plugin-tailwindcss": "*", "prettier-plugin-tailwindcss": "*",
"rollup-plugin-visualizer": "^5.9.0", "rollup-plugin-visualizer": "^5.9.0",
"tailwindcss": "^3.2.4", "tailwindcss": "^3.3.2",
"typescript": "*", "typescript": "*",
"vite": "^4.2.0", "vite": "^4.3.3",
"vite-plugin-pwa": "^0.14.4", "vite-plugin-pwa": "^0.14.7",
"vite-plugin-svg-icons": "^2.0.1", "vite-plugin-svg-icons": "^2.0.1",
"vitest": "^0.26.3" "vitest": "^0.26.3"
} }

View File

@ -9,7 +9,7 @@ const Artist = () => {
<div> <div>
<Header /> <Header />
{/* Dividing line */} {/* Dividing line */}
<div className='mt-10 mb-7.5 h-px w-full bg-white/20'></div> <div className='mb-7.5 mt-10 h-px w-full bg-white/20'></div>
<Popular /> <Popular />
<ArtistAlbum /> <ArtistAlbum />
<ArtistVideos /> <ArtistVideos />

View File

@ -57,7 +57,7 @@ const ArtistInfo = ({ artist, isLoading }: { artist?: Artist; isLoading: boolean
(isLoading || isLoadingArtistFromApple ? ( (isLoading || isLoadingArtistFromApple ? (
<div <div
className={cx( className={cx(
'line-clamp-5 mt-6 text-14 font-bold text-transparent', 'mt-6 line-clamp-5 text-14 font-bold text-transparent',
css` css`
min-height: 85px; min-height: 85px;
` `
@ -68,7 +68,7 @@ const ArtistInfo = ({ artist, isLoading }: { artist?: Artist; isLoading: boolean
) : ( ) : (
<div <div
className={cx( className={cx(
'line-clamp-5 mt-6 overflow-hidden whitespace-pre-wrap text-14 font-bold text-white/40 transition-colors duration-500 hover:text-white/60', 'mt-6 line-clamp-5 overflow-hidden whitespace-pre-wrap text-14 font-bold text-white/40 transition-colors duration-500 hover:text-white/60',
css` css`
height: 85px; height: 85px;
` `

View File

@ -42,7 +42,7 @@ const Track = ({
> >
{track?.name} {track?.name}
</div> </div>
<div className='line-clamp-1 mt-1 text-14 font-bold text-neutral-200 dark:text-neutral-700'> <div className='mt-1 line-clamp-1 text-14 font-bold text-neutral-200 dark:text-neutral-700'>
{track?.ar.map(a => a.name).join(', ')} {track?.ar.map(a => a.name).join(', ')}
</div> </div>
</div> </div>

View File

@ -49,7 +49,7 @@ const categories = [
{ id: 'charts', name: 'Charts', component: <Recommend /> }, { id: 'charts', name: 'Charts', component: <Recommend /> },
] ]
const categoriesKeys = categories.map(c => c.id) const categoriesKeys = categories.map(c => c.id)
type Key = typeof categoriesKeys[number] type Key = (typeof categoriesKeys)[number]
const Browse = () => { const Browse = () => {
const [active, setActive] = useState<Key>('recommend') const [active, setActive] = useState<Key>('recommend')
@ -60,7 +60,7 @@ const Browse = () => {
{/* Topbar background */} {/* Topbar background */}
<div <div
className={cx( className={cx(
'pointer-events-none fixed top-0 left-10 z-10 hidden lg:block', 'pointer-events-none fixed left-10 top-0 z-10 hidden lg:block',
css` css`
height: 230px; height: 230px;
` `

View File

@ -22,7 +22,7 @@ import settings from '@/web/states/settings'
import useUser from '@/web/api/hooks/useUser' import useUser from '@/web/api/hooks/useUser'
const collections = ['playlists', 'albums', 'artists', 'videos'] as const const collections = ['playlists', 'albums', 'artists', 'videos'] as const
type Collection = typeof collections[number] type Collection = (typeof collections)[number]
const Albums = () => { const Albums = () => {
const { data: albums } = useUserAlbums() const { data: albums } = useUserAlbums()
@ -114,7 +114,7 @@ const CollectionTabs = ({ showBg }: { showBg: boolean }) => {
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
className={cx( className={cx(
'pointer-events-none absolute right-0 left-0 z-10', 'pointer-events-none absolute left-0 right-0 z-10',
css` css`
height: 230px; height: 230px;
background-repeat: repeat; background-repeat: repeat;
@ -164,7 +164,7 @@ const Collections = () => {
<motion.div layout> <motion.div layout>
<CollectionTabs showBg={isScrollReachBottom} /> <CollectionTabs showBg={isScrollReachBottom} />
<div <div
className={cx('no-scrollbar overflow-y-auto px-2.5 pt-16 pb-16 lg:px-0')} className={cx('no-scrollbar overflow-y-auto px-2.5 pb-16 pt-16 lg:px-0')}
onScroll={onScroll} onScroll={onScroll}
style={{ style={{
height: `calc(100vh - ${topbarHeight}px)`, height: `calc(100vh - ${topbarHeight}px)`,

View File

@ -63,7 +63,7 @@ const Lyrics = ({ tracksIDs }: { tracksIDs: number[] }) => {
const Covers = memo(({ tracks }: { tracks: Track[] }) => { const Covers = memo(({ tracks }: { tracks: Track[] }) => {
const navigate = useNavigate() const navigate = useNavigate()
return ( return (
<div className='mt-6 grid w-full flex-shrink-0 grid-cols-3 gap-2.5 lg:mt-0 lg:ml-8 lg:w-auto'> <div className='mt-6 grid w-full flex-shrink-0 grid-cols-3 gap-2.5 lg:ml-8 lg:mt-0 lg:w-auto'>
{tracks.map(track => ( {tracks.map(track => (
<Image <Image
src={resizeImage(track.al.picUrl || '', 'md')} src={resizeImage(track.al.picUrl || '', 'md')}
@ -127,7 +127,7 @@ const PlayLikedSongsCard = () => {
<div className='flex justify-between'> <div className='flex justify-between'>
<button <button
onClick={handlePlay} onClick={handlePlay}
className='rounded-full bg-brand-700 py-5 px-6 text-16 font-medium text-white' className='rounded-full bg-brand-700 px-6 py-5 text-16 font-medium text-white'
> >
{t`my.playNow`} {t`my.playNow`}
</button> </button>

View File

@ -55,10 +55,10 @@ const Track = ({
{track?.name} {track?.name}
{[1318912, 1310848].includes(track?.mark || 0) && ( {[1318912, 1310848].includes(track?.mark || 0) && (
<Icon name='explicit' className='ml-2 mt-px mr-4 h-3.5 w-3.5 text-white/20' /> <Icon name='explicit' className='ml-2 mr-4 mt-px h-3.5 w-3.5 text-white/20' />
)} )}
</div> </div>
<div className='line-clamp-1 mt-1 text-14 font-bold text-white/30'> <div className='mt-1 line-clamp-1 text-14 font-bold text-white/30'>
{track?.ar.map((a, index) => ( {track?.ar.map((a, index) => (
<Fragment key={a.id}> <Fragment key={a.id}>
{index > 0 && ', '} {index > 0 && ', '}

View File

@ -88,7 +88,7 @@ const Track = ({
> >
{track?.name} {track?.name}
</div> </div>
<div className='line-clamp-1 mt-1 text-14 font-bold text-neutral-200 text-white/30'> <div className='mt-1 line-clamp-1 text-14 font-bold text-neutral-200 text-white/30'>
{track?.ar.map(a => a.name).join(', ')} {track?.ar.map(a => a.name).join(', ')}
</div> </div>
</div> </div>
@ -151,7 +151,7 @@ const Search = () => {
return ( return (
<div> <div>
<div className='mt-6 mb-8 text-4xl font-semibold dark:text-white'> <div className='mb-8 mt-6 text-4xl font-semibold dark:text-white'>
<span className='text-white/40'></span> &quot;{keywords}&quot; <span className='text-white/40'></span> &quot;{keywords}&quot;
</div> </div>

View File

@ -39,7 +39,7 @@ export function Select<T extends string>({
<select <select
onChange={e => onChange(e.target.value as T)} onChange={e => onChange(e.target.value as T)}
value={value} value={value}
className='h-full w-full appearance-none bg-transparent py-1 pr-7 pl-3 focus:outline-none' className='h-full w-full appearance-none bg-transparent py-1 pl-3 pr-7 focus:outline-none'
> >
{options.map(option => ( {options.map(option => (
<option key={option.value} value={option.value}> <option key={option.value} value={option.value}>
@ -71,7 +71,7 @@ export function Input({
<div className='mb-1 text-14 font-medium text-white/30'>Host</div> <div className='mb-1 text-14 font-medium text-white/30'>Host</div>
<div className='inline-block rounded-md bg-neutral-800 font-medium text-neutral-400'> <div className='inline-block rounded-md bg-neutral-800 font-medium text-neutral-400'>
<input <input
className='appearance-none bg-transparent py-1 px-3' className='appearance-none bg-transparent px-3 py-1'
onChange={e => onChange(e.target.value)} onChange={e => onChange(e.target.value)}
{...{ type, value }} {...{ type, value }}
/> />
@ -80,11 +80,20 @@ export function Input({
) )
} }
export function Button({ children, onClick }: { children: React.ReactNode; onClick: () => void }) { export function Button({
disalbed: disabled,
children,
onClick,
}: {
disalbed?: boolean
children: React.ReactNode
onClick: () => void
}) {
return ( return (
<button <button
onClick={onClick} onClick={onClick}
className='rounded-md bg-neutral-800 py-1 px-3 font-medium text-neutral-400 transition-colors duration-300 hover:bg-neutral-700 hover:text-neutral-300' disabled={disabled}
className='rounded-md bg-neutral-800 px-3 py-1 font-medium text-neutral-400 transition-colors duration-300 hover:bg-neutral-700 hover:text-neutral-300 disabled:opacity-10'
> >
{children} {children}
</button> </button>
@ -104,5 +113,9 @@ export function Option({ children }: { children: React.ReactNode }) {
} }
export function OptionText({ children }: { children: React.ReactNode }) { export function OptionText({ children }: { children: React.ReactNode }) {
return <div className='text-16 font-medium text-neutral-400'>{children}</div> return (
<div className='line-clamp-1 flex-shrink-0 text-16 font-medium text-neutral-400'>
{children}
</div>
)
} }

View File

@ -6,12 +6,14 @@ import Slider from '@/web/components/Slider'
import { cx } from '@emotion/css' import { cx } from '@emotion/css'
import player from '@/web/states/player' import player from '@/web/states/player'
import { ceil } from 'lodash' import { ceil } from 'lodash'
import { useMutation, useQuery } from '@tanstack/react-query'
import { IpcChannels } from '@/shared/IpcChannels'
import { useCopyToClipboard } from 'react-use'
function Player() { function Player() {
return ( return (
<div className={cx(`space-y-7`)}> <div className={cx(`space-y-7`)}>
<FindTrackOnYouTube /> <FindTrackOnYouTube />
<VolumeSlider />
</div> </div>
) )
} }
@ -59,31 +61,4 @@ function FindTrackOnYouTube() {
) )
} }
function VolumeSlider() {
const { t } = useTranslation()
const { volume } = useSnapshot(player)
const onChange = (volume: number) => {
player.volume = volume
}
return (
<div>
<BlockTitle>{t(`settings.volume-control`)}</BlockTitle>
<div className='pt-2 pr-1'>
<Slider
value={volume}
min={0}
max={1}
onChange={onChange}
alwaysShowTrack
alwaysShowThumb
/>
</div>
<div className='mt-1 flex justify-between text-14 font-bold text-neutral-100'>
<span>0</span>
<span>{ceil(volume * 100)}</span>
</div>
</div>
)
}
export default Player export default Player

View File

@ -11,7 +11,7 @@ import PageTransition from '@/web/components/PageTransition'
import { ease } from '@/web/utils/const' import { ease } from '@/web/utils/const'
export const categoryIds = ['general', 'appearance', 'player', 'lab', 'about'] as const export const categoryIds = ['general', 'appearance', 'player', 'lab', 'about'] as const
export type Category = typeof categoryIds[number] export type Category = (typeof categoryIds)[number]
const Sidebar = ({ const Sidebar = ({
activeCategory, activeCategory,
@ -42,7 +42,7 @@ const Sidebar = ({
initial={{ y: 11.5 }} initial={{ y: 11.5 }}
animate={indicatorAnimation} animate={indicatorAnimation}
transition={{ type: 'spring', duration: 0.6, bounce: 0.36 }} transition={{ type: 'spring', duration: 0.6, bounce: 0.36 }}
className='absolute top-0 left-3 mr-2 h-4 w-1 rounded-full bg-brand-700' className='absolute left-3 top-0 mr-2 h-4 w-1 rounded-full bg-brand-700'
></motion.div> ></motion.div>
{categories.map(category => ( {categories.map(category => (

View File

@ -16,42 +16,6 @@
.no-drag { .no-drag {
-webkit-user-drag: none; -webkit-user-drag: none;
} }
.line-clamp-1 {
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
word-break: break-all;
-webkit-line-clamp: 1;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
-webkit-line-clamp: 2;
}
.line-clamp-3 {
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
-webkit-line-clamp: 3;
}
.line-clamp-4 {
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
-webkit-line-clamp: 4;
}
.line-clamp-5 {
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
-webkit-line-clamp: 4;
}
} }
@font-face { @font-face {
@ -96,6 +60,9 @@ input {
font-family: Roboto, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Helvetica Neue, font-family: Roboto, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Helvetica Neue,
PingFang SC, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, WenQuanYi Micro Hei, PingFang SC, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, WenQuanYi Micro Hei,
microsoft uighur, sans-serif; microsoft uighur, sans-serif;
-webkit-font-smoothing: antialiased;
text-rendering: optimizelegibility;
-webkit-tap-highlight-color: transparent;
} }
body { body {

View File

@ -1,13 +1,14 @@
/* eslint-disable @typescript-eslint/no-var-requires */ // import colors from "tailwindcss/colors"
const colors = require('tailwindcss/colors') // import pickedColors = "./scripts/pickedColors"
// const pickedColors = require('./scripts/pickedColors.js') import type { Config } from 'tailwindcss'
import containerQueryPlugin from '@tailwindcss/container-queries'
const fontSizeDefault = { const fontSizeDefault = {
lineHeight: '1.2', lineHeight: '1.2',
letterSpacing: '0.02em', letterSpacing: '0.02em',
} }
module.exports = { export default {
content: ['./index.html', './**/*.{vue,js,ts,jsx,tsx}', '!./node_modules/**/*'], content: ['./index.html', './**/*.{vue,js,ts,jsx,tsx}', '!./node_modules/**/*'],
darkMode: 'class', darkMode: 'class',
theme: { theme: {
@ -121,5 +122,5 @@ module.exports = {
}, },
}, },
}, },
plugins: [require('@tailwindcss/container-queries')], plugins: [containerQueryPlugin],
} } satisfies Config

View File

@ -167,8 +167,8 @@ export async function calcCoverColor(coverUrl: string) {
export const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) export const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent)
export const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) export const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
export const isPWA = export const isPWA = () =>
() => (navigator as any).standalone || window.matchMedia('(display-mode: standalone)').matches (navigator as any).standalone || window.matchMedia('(display-mode: standalone)').matches
export const isIosPwa = isIOS && isPWA() && isSafari export const isIosPwa = isIOS && isPWA() && isSafari
export const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)) export const sleep = (ms: number) => new Promise(r => setTimeout(r, ms))

File diff suppressed because it is too large Load Diff

View File

@ -14,5 +14,5 @@ module.exports = {
// Tailwind CSS // Tailwind CSS
plugins: [require('prettier-plugin-tailwindcss')], plugins: [require('prettier-plugin-tailwindcss')],
tailwindConfig: './packages/web/tailwind.config.js', tailwindConfig: './packages/web/tailwind.config.ts',
} }