feat: updates

This commit is contained in:
qier222 2022-03-19 17:03:29 +08:00
parent 08abf8229f
commit fb21405bf9
No known key found for this signature in database
GPG key ID: 9C85007ED905F14D
33 changed files with 699 additions and 361 deletions

1
.gitignore vendored
View file

@ -81,6 +81,7 @@ typings/
# ----
dist
**/.tmp
/tmp
release
.DS_Store
dist-ssr

View file

@ -30,9 +30,9 @@
"realm": "^10.13.0"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^3.2.0",
"@types/cookie-parser": "^1.4.2",
"@types/express": "^4.17.13",
"@types/express-fileupload": "^1.2.2",
"@types/howler": "^2.2.6",
"@types/js-cookie": "^3.0.1",
"@types/lodash-es": "^4.17.6",
@ -60,10 +60,13 @@
"eslint": "^8.11.0",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.3.0",
"express-fileupload": "^1.3.1",
"fast-folder-size": "^1.6.1",
"howler": "^2.2.3",
"js-cookie": "^3.0.1",
"lodash-es": "^4.17.21",
"md5": "^2.3.0",
"music-metadata": "^7.12.2",
"postcss": "^8.4.12",
"prettier": "2.5.1",
"prettier-plugin-tailwindcss": "^0.1.8",

267
packages/main/cache.ts Normal file
View file

@ -0,0 +1,267 @@
import { db, ModelNames, realm } from './database'
import type { FetchTracksResponse } from '../renderer/src/api/track'
import { app, ipcMain } from 'electron'
import { Request, Response } from 'express'
import logger from './logger'
import fs from 'fs'
import * as musicMetadata from 'music-metadata'
export async function setCache(api: string, data: any, query: any) {
switch (api) {
case 'user/account':
case 'personalized':
case 'likelist': {
if (!data) return
db.set(ModelNames.ACCOUNT_DATA, api, data)
break
}
case 'user/playlist': {
if (!data.playlist) return
db.set(ModelNames.USER_PLAYLISTS, Number(query.uid), data)
break
}
case 'song/detail': {
if (!data.songs) return
const tracks = (data as FetchTracksResponse).songs
db.batchSet(
ModelNames.TRACK,
tracks.map(t => ({
id: t.id,
json: JSON.stringify(t),
updateAt: Date.now(),
}))
)
break
}
case 'album': {
if (!data.album) return
db.set(ModelNames.ALBUM, Number(data.album.id), data)
break
}
case 'playlist/detail': {
if (!data.playlist) return
db.set(ModelNames.PLAYLIST, Number(data.playlist.id), data)
break
}
case 'artist/album': {
if (!data.hotAlbums) return
db.set(ModelNames.ARTIST_ALBUMS, Number(data.artist.id), data)
break
}
}
}
/**
* Check if the cache is expired
* @param updateAt from database, milliseconds
* @param staleTime minutes
*/
const isCacheExpired = (updateAt: number, staleTime: number) => {
return Date.now() - updateAt > staleTime * 1000 * 60
}
export function getCache(
api: string,
query: any,
checkIsExpired: boolean = false
): any {
switch (api) {
case 'user/account':
case 'personalized':
case 'likelist': {
const data = db.get(ModelNames.ACCOUNT_DATA, api) as any
if (data?.json) return JSON.parse(data.json)
break
}
case 'user/playlist': {
if (!query.uid) return
const userPlaylists = db.get(
ModelNames.USER_PLAYLISTS,
Number(query?.uid)
) as any
if (userPlaylists?.json) return JSON.parse(userPlaylists.json)
break
}
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 = ids.map(id => {
const track = tracksRaw.find(t => t.id === Number(id)) as any
return JSON.parse(track.json)
})
return {
code: 200,
songs: tracks,
privileges: {},
}
}
case 'album': {
if (!query?.id) return
const album = db.get(ModelNames.ALBUM, Number(query?.id)) as any
if (checkIsExpired && isCacheExpired(album?.updateAt, 24 * 60)) return
if (album?.json) return JSON.parse(album.json)
break
}
case 'playlist/detail': {
if (!query?.id) return
const playlist = db.get(ModelNames.PLAYLIST, Number(query?.id)) as any
if (checkIsExpired && isCacheExpired(playlist?.updateAt, 10)) return
if (playlist?.json) return JSON.parse(playlist.json)
break
}
case 'artist/album': {
if (!query?.id) return
const artistAlbums = db.get(
ModelNames.ARTIST_ALBUMS,
Number(query?.id)
) as any
if (checkIsExpired && isCacheExpired(artistAlbums?.updateAt, 30)) return
if (artistAlbums?.json) return JSON.parse(artistAlbums.json)
break
}
}
}
export async function getCacheForExpress(api: string, req: Request) {
// Get track detail cache
if (api === 'song/detail') {
const cache = getCache(api, req.query)
if (cache) {
logger.info(`[cache] Cache hit for ${req.path}`)
return cache
}
}
// Get audio cache if API is song/detail
if (api === 'song/url') {
const cache = db.get(ModelNames.AUDIO, Number(req.query.id)) as any
if (!cache) return
const audioFileName = `${cache.id}-${cache.br}.${cache.type}`
const isAudioFileExists = fs.existsSync(
`${app.getPath('userData')}/audio_cache/${audioFileName}`
)
if (!isAudioFileExists) return
logger.info(`[cache] Audio cache hit for ${req.path}`)
return {
data: [
{
source: cache.source,
id: cache.id,
url: `http://127.0.0.1:42710/yesplaymusic/audio/${audioFileName}`,
br: cache.br,
size: 0,
md5: '',
code: 200,
expi: 0,
type: cache.type,
gain: 0,
fee: 8,
uf: null,
payed: 0,
flag: 4,
canExtend: false,
freeTrialInfo: null,
level: 'standard',
encodeType: cache.type,
freeTrialPrivilege: {
resConsumable: false,
userConsumable: false,
listenType: null,
},
freeTimeTrialPrivilege: {
resConsumable: false,
userConsumable: false,
type: 0,
remainTime: 0,
},
urlSource: 0,
},
],
code: 200,
}
}
}
export function getAudioCache(fileName: string, res: Response) {
if (!fileName) {
return res.status(400).send({ error: 'No filename provided' })
}
const id = Number(fileName.split('-')[0])
try {
const path = `${app.getPath('userData')}/audio_cache/${fileName}`
const audio = fs.readFileSync(path)
if (audio.byteLength === 0) {
db.delete(ModelNames.AUDIO, Number(id))
fs.unlinkSync(path)
return res.status(404).send({ error: 'Audio not found' })
}
res.send(audio)
} catch (error) {
res.status(500).send({ error })
}
}
// Cache audio info local folder
export async function cacheAudio(
buffer: Buffer,
{ id, source }: { id: number; source: string }
) {
const path = `${app.getPath('userData')}/audio_cache`
try {
fs.statSync(path)
} catch (e) {
fs.mkdirSync(path)
}
const meta = await musicMetadata.parseBuffer(buffer)
const br = meta.format.bitrate
const type = {
'MPEG 1 Layer 3': 'mp3',
'Ogg Vorbis': 'ogg',
AAC: 'm4a',
FLAC: 'flac',
unknown: 'unknown',
}[meta.format.codec ?? 'unknown']
await fs.writeFile(`${path}/${id}-${br}.${type}`, buffer, error => {
if (error) {
return logger.error(`[cache] cacheAudio failed: ${error}`)
}
logger.info(`Audio file ${id}-${br}.${type} cached!`)
realm.write(() => {
realm.create(
ModelNames.AUDIO,
{
id: Number(id),
type,
br,
source,
updateAt: Date.now(),
},
'modified'
)
})
logger.info(`[cache] cacheAudio ${id}-${br}.${type}`)
})
}
ipcMain.on('getApiCacheSync', (event, args) => {
const { api, query } = args
const data = getCache(api, query, false)
event.returnValue = data
})

View file

@ -1,132 +1,97 @@
import Realm from 'realm'
import type { FetchTracksResponse } from '../renderer/src/api/track'
import type { FetchAlbumResponse } from '../renderer/src/api/album'
import path from 'path'
import { app } from 'electron'
enum ModelNames {
export enum ModelNames {
ACCOUNT_DATA = 'AccountData',
TRACK = 'Track',
ALBUM = 'Album',
ARTIST = 'Artist',
PLAYLIST = 'Playlist',
ARTIST_ALBUMS = 'ArtistAlbums',
USER_PLAYLISTS = 'UserPlaylists',
AUDIO = 'Audio',
}
const universalProperties = {
id: 'int',
json: 'string',
updateAt: 'int',
export enum AudioSources {
NETEASE = 'netease',
KUWO = 'kuwo',
QQ = 'qq',
KUGOU = 'kugou',
YOUTUBE = 'youtube',
MIGU = 'migu',
JOOX = 'joox',
BILIBILI = 'bilibili',
}
const TrackSchema = {
name: ModelNames.TRACK,
properties: universalProperties,
const RegularSchemas = [
ModelNames.USER_PLAYLISTS,
ModelNames.ARTIST_ALBUMS,
ModelNames.PLAYLIST,
ModelNames.ALBUM,
ModelNames.TRACK,
].map(name => ({
primaryKey: 'id',
}
name,
properties: {
id: 'int',
json: 'string',
updateAt: 'int',
},
}))
const AlbumSchema = {
name: ModelNames.ALBUM,
properties: universalProperties,
primaryKey: 'id',
}
const PlaylistSchema = {
name: ModelNames.PLAYLIST,
properties: universalProperties,
primaryKey: 'id',
}
const realm = new Realm({
path: './.tmp/db.realm',
schema: [TrackSchema, AlbumSchema, PlaylistSchema],
export const realm = new Realm({
path: path.resolve(app.getPath('userData'), './api_cache/db.realm'),
schema: [
...RegularSchemas,
{
name: ModelNames.ACCOUNT_DATA,
properties: {
id: 'string',
json: 'string',
updateAt: 'int',
},
primaryKey: 'id',
},
{
name: ModelNames.AUDIO,
properties: {
id: 'int',
br: 'int',
type: 'string',
source: 'string',
updateAt: 'int',
},
primaryKey: 'id',
},
],
})
export const database = {
get: (model: ModelNames, key: number) => {
export const db = {
get: (model: ModelNames, key: number | string) => {
return realm.objectForPrimaryKey(model, key)
},
set: (model: ModelNames, key: number, value: any) => {
realm.create(
model,
{
id: key,
updateAt: Date.now(),
json: JSON.stringify(value),
},
'modified'
)
set: (model: ModelNames, key: number | string, value: any) => {
realm.write(() => {
realm.create(
model,
{
id: key,
updateAt: Date.now(),
json: JSON.stringify(value),
},
'modified'
)
})
},
batchSet: (model: ModelNames, items: any[]) => {
realm.write(() => {
items.forEach(item => {
realm.create(model, item, 'modified')
})
})
},
delete: (model: ModelNames, key: number) => {
realm.delete(realm.objectForPrimaryKey(model, key))
},
}
export async function setTracks(data: FetchTracksResponse) {
if (!data.songs) return
const tracks = data.songs
realm.write(() => {
tracks.forEach(track => {
database.set(ModelNames.TRACK, track.id, track)
})
})
}
export async function setCache(api: string, data: any) {
switch (api) {
case 'song_detail': {
setTracks(data)
return
}
case 'album': {
if (!data.album) return
realm.write(() => {
database.set(ModelNames.ALBUM, Number(data.album.id), data)
})
return
}
case 'playlist_detail': {
if (!data.playlist) return
realm.write(() => {
database.set(ModelNames.PLAYLIST, Number(data.playlist.id), data)
})
return
}
}
}
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: {},
}
}
case 'album': {
if (!query?.id) return
const album = realm.objectForPrimaryKey(
ModelNames.ALBUM,
Number(query?.id)
)?.json
if (album) return JSON.parse(album)
return
}
case 'playlist_detail': {
if (!query?.id) return
const playlist = realm.objectForPrimaryKey(
ModelNames.PLAYLIST,
Number(query?.id)
)?.json
if (playlist) return JSON.parse(playlist)
return
}
}
}

View file

@ -1,3 +1,4 @@
import './preload' // must be first
import {
BrowserWindow,
BrowserWindowConstructorOptions,
@ -6,7 +7,7 @@ import {
} from 'electron'
import Store from 'electron-store'
import { release } from 'os'
import { join } from 'path'
import path, { join } from 'path'
import logger from './logger'
import './server'
import './database'
@ -96,6 +97,7 @@ async function createWindow() {
}
app.whenReady().then(async () => {
logger.info('[index] app ready')
createWindow()
// Install devtool extension
@ -107,10 +109,10 @@ app.whenReady().then(async () => {
// 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)
logger.info('An error occurred: ', err)
)
installExtension(REDUX_DEVTOOLS.id).catch(err =>
console.log('An error occurred: ', err)
logger.info('An error occurred: ', err)
)
}
})

17
packages/main/preload.ts Normal file
View file

@ -0,0 +1,17 @@
import logger from './logger'
import path from 'path'
import { app } from 'electron'
import fs from 'fs'
const isDev = !app.isPackaged
if (isDev) {
const devUserDataPath = path.resolve(process.cwd(), './tmp/userData')
try {
fs.statSync(devUserDataPath)
} catch (e) {
fs.mkdirSync(devUserDataPath)
}
app.setPath('appData', devUserDataPath)
}
logger.info(`[index] userData path: ${app.getPath('userData')}`)

View file

@ -2,27 +2,32 @@ 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'
import {
setCache,
getCacheForExpress,
cacheAudio,
getAudioCache,
} from './cache'
import fileUpload from 'express-fileupload'
// 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)
app.use(fileUpload())
Object.entries(neteaseApi).forEach(([name, handler]) => {
if (['serveNcmApi', 'getModulesDefinitions'].includes(name)) return
name = pathCase(name)
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)
}
const cache = await getCacheForExpress(name, req)
if (cache) return res.json(cache)
// Request netease api
try {
@ -30,18 +35,54 @@ Object.entries(neteaseApi).forEach(([name, handler]) => {
...req.query,
cookie: `MUSIC_U=${req.cookies['MUSIC_U']}`,
})
res.send(result.body)
setCache(name, result.body)
setCache(name, result.body, req.query)
return res.send(result.body)
} catch (error) {
res.status(500).send(error)
return res.status(500).send(error)
}
}
const neteasePath = `/netease/${pathCase(name)}`
app.get(neteasePath, wrappedHandler)
app.post(neteasePath, wrappedHandler)
app.get(`/netease/${name}`, wrappedHandler)
app.post(`/netease/${name}`, wrappedHandler)
})
// Cache audio
app.get(
'/yesplaymusic/audio/:filename',
async (req: Request, res: Response) => {
getAudioCache(req.params.filename, res)
}
)
app.post('/yesplaymusic/audio/:id', async (req: Request, res: Response) => {
const id = Number(req.params.id)
const { url } = req.query
if (isNaN(id)) {
return res.status(400).send({ error: 'Invalid param id' })
}
if (!url) {
return res.status(400).send({ error: 'Invalid query url' })
}
if (!req.files || Object.keys(req.files).length === 0 || !req.files.file) {
return res.status(400).send('No audio were uploaded.')
}
if ('length' in req.files.file) {
return res.status(400).send('Only can upload one audio at a time.')
}
try {
await cacheAudio(req.files.file.data, {
id: id,
source: 'netease',
})
res.status(200).send('Audio cached!')
} catch (error) {
res.status(500).send({ error })
}
})
const port = Number(process.env['ELECTRON_DEV_NETEASE_API_PORT'] ?? 3000)
app.listen(port, () => {
logger.info(`[server] API server listening on port ${port}`)
})

View file

@ -8,7 +8,7 @@ export enum AlbumApiNames {
export interface FetchAlbumParams {
id: number
}
interface FetchAlbumResponse {
export interface FetchAlbumResponse {
code: number
resourceState: boolean
album: Album

View file

@ -9,7 +9,7 @@ export enum ArtistApiNames {
export interface FetchArtistParams {
id: number
}
interface FetchArtistResponse {
export interface FetchArtistResponse {
code: number
more: boolean
artist: Artist
@ -34,7 +34,7 @@ export interface FetchArtistAlbumsParams {
limit?: number // default: 50
offset?: number // default: 0
}
interface FetchArtistAlbumsResponse {
export interface FetchArtistAlbumsResponse {
code: number
hotAlbums: Album[]
more: boolean

View file

@ -10,7 +10,7 @@ export interface FetchPlaylistParams {
id: number
s?: number // 歌单最近的 s 个收藏者
}
interface FetchPlaylistResponse {
export interface FetchPlaylistResponse {
code: number
playlist: Playlist
privileges: unknown // TODO: unknown type

View file

@ -33,7 +33,7 @@ export interface FetchAudioSourceParams {
id: number
br?: number // bitrate, default 999000320000 = 320kbps
}
interface FetchAudioSourceResponse {
export interface FetchAudioSourceResponse {
code: number
data: {
br: number

View file

@ -97,7 +97,7 @@ export interface FetchUserPlaylistsParams {
offset: number
limit?: number // default 30
}
interface FetchUserPlaylistsResponse {
export interface FetchUserPlaylistsResponse {
code: number
more: false
version: string
@ -116,7 +116,7 @@ export function fetchUserPlaylists(
export interface FetchUserLikedSongsIDsParams {
uid: number
}
interface FetchUserLikedSongsIDsResponse {
export interface FetchUserLikedSongsIDsResponse {
code: number
checkPoint: number
ids: number[]

View file

@ -0,0 +1,29 @@
import axios, { AxiosInstance } from 'axios'
const baseURL = String(
import.meta.env.DEV ? '/yesplaymusic' : `http://127.0.0.1:42710/yesplaymusic`
)
const request: AxiosInstance = axios.create({
baseURL,
withCredentials: true,
timeout: 15000,
})
export async function cacheAudio(id: number, audio: string) {
const file = await axios.get(audio, { responseType: 'arraybuffer' })
if (file.status !== 200) return
const formData = new FormData()
const blob = new Blob([file.data], { type: 'multipart/form-data' })
formData.append('file', blob)
request.post(`/audio/${id}`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
params: {
url: audio,
},
})
}

View file

@ -3,7 +3,7 @@ import Skeleton from '@/components/Skeleton'
import SvgIcon from '@/components/SvgIcon'
import { prefetchAlbum } from '@/hooks/useAlbum'
import { prefetchPlaylist } from '@/hooks/usePlaylist'
import { formatDate, resizeImage } from '@/utils/common'
import { formatDate, resizeImage, scrollToTop } from '@/utils/common'
export enum Subtitle {
COPYWRITER = 'copywriter',
@ -111,6 +111,7 @@ const CoverRow = ({
if (playlists) navigate(`/playlist/${id}`)
if (artists) navigate(`/artist/${id}`)
if (navigateCallback) navigateCallback()
scrollToTop()
}
const prefetch = (id: number) => {

View file

@ -87,7 +87,11 @@ const MediaControls = () => {
>
<SvgIcon
className='h-[1.5rem] w-[1.5rem] '
name={state === PlayerState.PLAYING ? 'pause' : 'play'}
name={
[PlayerState.PLAYING, PlayerState.LOADING].includes(state)
? 'pause'
: 'play'
}
/>
</IconButton>
<IconButton onClick={() => track && player.nextTrack()} disabled={!track}>
@ -125,6 +129,7 @@ const Progress = () => {
() => playerSnapshot.progress,
[playerSnapshot.progress]
)
const state = useMemo(() => playerSnapshot.state, [playerSnapshot.state])
const track = useMemo(() => playerSnapshot.track, [playerSnapshot.track])
return (
@ -133,7 +138,11 @@ const Progress = () => {
<Slider
min={0}
max={(track.dt ?? 0) / 1000}
value={progress}
value={
state === PlayerState.PLAYING || state === PlayerState.PAUSED
? progress
: 0
}
onChange={value => {
player.progress = value
}}

View file

@ -1,9 +1,9 @@
import { NavLink } from 'react-router-dom'
import SvgIcon from '@/components/SvgIcon'
import { prefetchPlaylist } from '@/hooks/usePlaylist'
import useUser from '@/hooks/useUser'
import useUserPlaylists from '@/hooks/useUserPlaylists'
import { scrollToTop } from '@/utils/common'
import { prefetchPlaylist } from '@/hooks/usePlaylist'
interface Tab {
name: string
@ -70,10 +70,10 @@ const Playlists = () => {
<div className='mb-16 overflow-auto pb-2'>
{playlists?.playlist?.map(playlist => (
<NavLink
onMouseOver={() => prefetchPlaylist({ id: playlist.id })}
key={playlist.id}
onClick={() => scrollToTop()}
to={`/playlist/${playlist.id}`}
onMouseOver={() => prefetchPlaylist({ id: playlist.id })}
className={({ isActive }: { isActive: boolean }) =>
classNames(
'btn-hover-animation line-clamp-1 my-px mx-3 flex cursor-default items-center rounded-lg px-3 py-[0.38rem] text-sm text-black opacity-70 transition-colors duration-200 after:scale-[0.97] after:bg-black/[.06] dark:text-white dark:after:bg-white/20',

View file

@ -13,8 +13,6 @@ const Slider = ({
onlyCallOnChangeAfterDragEnded?: boolean
orientation?: 'horizontal' | 'vertical'
}) => {
console.log('[Slider.tsx] rendering')
const sliderRef = useRef<HTMLInputElement>(null)
const [isDragging, setIsDragging] = useState(false)
const [draggingValue, setDraggingValue] = useState(value)
@ -133,7 +131,7 @@ const Slider = ({
<div
className={classNames(
'absolute h-[2px] group-hover:bg-brand-500',
isDragging ? 'bg-brand-500' : 'bg-gray-300 dark:bg-gray-400'
isDragging ? 'bg-brand-500' : 'bg-gray-300 dark:bg-gray-600'
)}
style={usedTrackStyle}
></div>

View file

@ -1,61 +0,0 @@
import style from './Slider.module.scss'
const Slider = () => {
const [value, setValue] = useState(50)
const thumbStyle = useMemo(
() => ({
left: `${value}%`,
transform: `translate(-${value}%, -9px)`,
}),
[value]
)
const usedTrackStyle = useMemo(
() => ({
width: `${value}%`,
}),
[value]
)
const onDragging = false
const [isHover, setIsHover] = useState(false)
return (
<div
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
>
<div className='absolute h-[2px] w-full bg-gray-500 bg-opacity-10'></div>
<div
className={classNames(
'absolute h-[2px]',
onDragging || isHover ? 'bg-brand-500' : 'bg-gray-500'
)}
style={usedTrackStyle}
></div>
<div
className={classNames(
'absolute flex h-5 w-5 items-center justify-center rounded-full bg-brand-500 bg-opacity-20',
!onDragging && !isHover && 'opacity-0'
)}
style={thumbStyle}
>
<div className='absolute h-2 w-2 rounded-full bg-brand-500'></div>
</div>
<input
type='range'
min='0'
max='100'
value={value}
onChange={e => setValue(Number(e.target.value))}
className='absolute h-[2px] w-full appearance-none opacity-0'
/>
</div>
)
}
export default Slider

View file

@ -71,7 +71,7 @@ const Track = memo(
!isSkeleton && {
'btn-hover-animation after:bg-gray-100 dark:after:bg-white/[.08]':
!isHighlight,
'bg-brand-100 dark:bg-gray-800': isHighlight,
'bg-brand-50 dark:bg-gray-800': isHighlight,
}
)}
>
@ -122,7 +122,8 @@ const Track = memo(
className={classNames(
'ml-1',
isHighlight ? 'text-brand-500/[.8]' : 'text-gray-400'
)}>
)}
>
({subtitle})
</span>
)}

View file

@ -3,7 +3,6 @@ import { NavLink } from 'react-router-dom'
import ArtistInline from '@/components/ArtistsInline'
import Skeleton from '@/components/Skeleton'
import SvgIcon from '@/components/SvgIcon'
import { prefetchAlbum } from '@/hooks/useAlbum'
import useUser from '@/hooks/useUser'
import useUserLikedSongsIDs from '@/hooks/useUserLikedSongsIDs'
import { formatDuration, resizeImage } from '@/utils/common'
@ -31,8 +30,10 @@ const Track = memo(
className={classNames(
'group grid w-full rounded-xl after:scale-[.98] after:rounded-xl dark:after:bg-white/[.08]',
'grid-cols-12 p-2 pr-4',
!isSkeleton && !isPlaying && 'btn-hover-animation after:bg-gray-100 dark:after:bg-white/[.08]',
!isSkeleton && isPlaying && 'bg-brand-100 dark:bg-gray-800'
!isSkeleton &&
!isPlaying &&
'btn-hover-animation after:bg-gray-100 dark:after:bg-white/[.08]',
!isSkeleton && isPlaying && 'bg-brand-50 dark:bg-gray-800'
)}
>
{/* Track info */}
@ -78,7 +79,9 @@ const Track = memo(
<div
className={classNames(
'text-sm',
isPlaying ? 'text-brand-500' : 'text-gray-600 dark:text-gray-400'
isPlaying
? 'text-brand-500'
: 'text-gray-600 dark:text-gray-400'
)}
>
{isSkeleton ? (
@ -98,7 +101,6 @@ const Track = memo(
<Fragment>
<NavLink
to={`/album/${track.al.id}`}
onMouseOver={() => prefetchAlbum({ id: track.al.id })}
className={classNames(
'hover:underline',
isPlaying && 'text-brand-500'
@ -137,7 +139,9 @@ const Track = memo(
<div
className={classNames(
'min-w-[2.5rem] text-right',
isPlaying ? 'text-brand-500' : 'text-gray-600 dark:text-gray-400'
isPlaying
? 'text-brand-500'
: 'text-gray-600 dark:text-gray-400'
)}
>
{formatDuration(track.dt, 'en', 'hh:mm:ss')}

View file

@ -1,6 +1,6 @@
import { fetchAlbum } from '@/api/album'
import { AlbumApiNames } from '@/api/album'
import type { FetchAlbumParams } from '@/api/album'
import type { FetchAlbumParams, FetchAlbumResponse } from '@/api/album'
import reactQueryClient from '@/utils/reactQueryClient'
const fetch = async (params: FetchAlbumParams, noCache?: boolean) => {
@ -17,7 +17,14 @@ export default function useAlbum(params: FetchAlbumParams, noCache?: boolean) {
() => fetch(params, noCache),
{
enabled: !!params.id,
staleTime: Infinity,
staleTime: 24 * 60 * 60 * 1000, // 24 hours
placeholderData: (): FetchAlbumResponse =>
window.ipcRenderer.sendSync('getApiCacheSync', {
api: 'album',
query: {
id: params.id,
},
}),
}
)
}

View file

@ -1,6 +1,9 @@
import { fetchArtistAlbums } from '@/api/artist'
import { ArtistApiNames } from '@/api/artist'
import type { FetchArtistAlbumsParams } from '@/api/artist'
import type {
FetchArtistAlbumsParams,
FetchArtistAlbumsResponse,
} from '@/api/artist'
export default function useUserAlbums(params: FetchArtistAlbumsParams) {
return useQuery(
@ -12,6 +15,13 @@ export default function useUserAlbums(params: FetchArtistAlbumsParams) {
{
enabled: !!params.id && params.id !== 0,
staleTime: 3600000,
placeholderData: (): FetchArtistAlbumsResponse =>
window.ipcRenderer.sendSync('getApiCacheSync', {
api: 'artist/album',
query: {
id: params.id,
},
}),
}
)
}

View file

@ -1,6 +1,6 @@
import { fetchPlaylist } from '@/api/playlist'
import { PlaylistApiNames } from '@/api/playlist'
import type { FetchPlaylistParams } from '@/api/playlist'
import type { FetchPlaylistParams, FetchPlaylistResponse } from '@/api/playlist'
import reactQueryClient from '@/utils/reactQueryClient'
const fetch = (params: FetchPlaylistParams, noCache?: boolean) => {
@ -16,7 +16,14 @@ export default function usePlaylist(
() => fetch(params, noCache),
{
enabled: !!(params.id && params.id > 0 && !isNaN(Number(params.id))),
staleTime: 3600000,
staleTime: 60 * 60 * 1000, // 1 hour
placeholderData: (): FetchPlaylistResponse | undefined =>
window.ipcRenderer.sendSync('getApiCacheSync', {
api: 'playlist/detail',
query: {
id: params.id,
},
}),
}
)
}

View file

@ -1,5 +1,9 @@
import { TrackApiNames, fetchAudioSource, fetchTracks } from '@/api/track'
import type { FetchAudioSourceParams, FetchTracksParams } from '@/api/track'
import type {
FetchAudioSourceParams,
FetchTracksParams,
FetchTracksResponse,
} from '@/api/track'
import reactQueryClient from '@/utils/reactQueryClient'
export default function useTracks(params: FetchTracksParams) {
@ -12,6 +16,13 @@ export default function useTracks(params: FetchTracksParams) {
enabled: params.ids.length !== 0,
refetchInterval: false,
staleTime: Infinity,
initialData: (): FetchTracksResponse | undefined =>
window.ipcRenderer.sendSync('getApiCacheSync', {
api: 'song/detail',
query: {
ids: params.ids.join(','),
},
}),
}
)
}
@ -37,7 +48,7 @@ export function fetchAudioSourceWithReactQuery(params: FetchAudioSourceParams) {
},
{
retry: 3,
staleTime: 1200000,
staleTime: 0, // TODO: Web版1小时缓存
}
)
}

View file

@ -1,8 +1,12 @@
import { fetchUserAccount } from '@/api/user'
import { fetchUserAccount, fetchUserAccountResponse } from '@/api/user'
import { UserApiNames } from '@/api/user'
export default function useUser() {
return useQuery(UserApiNames.FETCH_USER_ACCOUNT, fetchUserAccount, {
refetchOnWindowFocus: true,
placeholderData: (): fetchUserAccountResponse | undefined =>
window.ipcRenderer.sendSync('getApiCacheSync', {
api: 'user/account',
}),
})
}

View file

@ -1,4 +1,7 @@
import type { FetchUserLikedSongsIDsParams } from '@/api/user'
import type {
FetchUserLikedSongsIDsParams,
FetchUserLikedSongsIDsResponse,
} from '@/api/user'
import { UserApiNames, fetchUserLikedSongsIDs } from '@/api/user'
export default function useUserLikedSongsIDs(
@ -10,6 +13,13 @@ export default function useUserLikedSongsIDs(
{
enabled: !!(params.uid && params.uid !== 0),
refetchOnWindowFocus: true,
placeholderData: (): FetchUserLikedSongsIDsResponse | undefined =>
window.ipcRenderer.sendSync('getApiCacheSync', {
api: 'likelist',
query: {
uid: params.uid,
},
}),
}
)
}

View file

@ -1,4 +1,7 @@
import type { FetchUserPlaylistsParams } from '@/api/user'
import type {
FetchUserPlaylistsParams,
FetchUserPlaylistsResponse,
} from '@/api/user'
import { UserApiNames, fetchUserPlaylists } from '@/api/user'
export default function useUserPlaylists(params: FetchUserPlaylistsParams) {
@ -14,6 +17,13 @@ export default function useUserPlaylists(params: FetchUserPlaylistsParams) {
params.uid !== 0 &&
params.offset !== undefined
),
placeholderData: (): FetchUserPlaylistsResponse =>
window.ipcRenderer.sendSync('getApiCacheSync', {
api: 'user/playlist',
query: {
uid: params.uid,
},
}),
}
)
}

View file

@ -7,9 +7,16 @@ export default function Home() {
const {
data: recommendedPlaylists,
isLoading: isLoadingRecommendedPlaylists,
} = useQuery(PlaylistApiNames.FETCH_RECOMMENDED_PLAYLISTS, () => {
return fetchRecommendedPlaylists({})
})
} = useQuery(
PlaylistApiNames.FETCH_RECOMMENDED_PLAYLISTS,
() => {
return fetchRecommendedPlaylists({})
},
{
placeholderData: () =>
window.ipcRenderer.sendSync('getApiCacheSync', { api: 'personalized' }),
}
)
return (
<div>

View file

@ -20,7 +20,10 @@ export function resizeImage(
if (!Object.keys(sizeMap).includes(size)) {
console.error(`Invalid cover size: ${size}`)
}
return `${url}?param=${sizeMap[size]}y${sizeMap[size]}`
return `${url}?param=${sizeMap[size]}y${sizeMap[size]}`.replace(
'http://',
'https://'
)
}
export const storage = {

View file

@ -3,6 +3,7 @@ import {
fetchAudioSourceWithReactQuery,
fetchTracksWithReactQuery,
} from '@/hooks/useTracks'
import { cacheAudio } from '@/api/yesplaymusic'
type TrackID = number
enum TrackListSourceType {
@ -19,9 +20,10 @@ export enum Mode {
}
export enum State {
INITIALIZING = 'initializing',
READY = 'ready',
PLAYING = 'playing',
PAUSED = 'paused',
LOADED = 'loaded',
LOADING = 'loading',
}
export enum RepeatMode {
OFF = 'off',
@ -107,8 +109,7 @@ export class Player {
private _setupProgressInterval() {
this._progressInterval = setInterval(() => {
this._progress = _howler.seek()
console.log(this.progress)
if (this.state === State.PLAYING) this._progress = _howler.seek()
}, 1000)
}
@ -116,6 +117,7 @@ export class Player {
* Fetch track details from Netease based on this.trackID
*/
private async _fetchTrack(trackID: TrackID) {
this.state = State.LOADING
const response = await fetchTracksWithReactQuery({ ids: [trackID] })
if (response.songs.length) {
return response.songs[0]
@ -161,6 +163,8 @@ export class Player {
})
_howler = howler
this.play()
this.state = State.PLAYING
_howler.once('load', () => this._cacheAudio(this.trackID, audio))
if (!this._progressInterval) {
this._setupProgressInterval()
@ -177,6 +181,11 @@ export class Player {
}
}
private _cacheAudio(id: number, audio: string) {
if (audio.includes('yesplaymusic')) return
cacheAudio(id, audio)
}
/**
* Play current track
* @param {boolean} fade fade in
@ -184,6 +193,7 @@ export class Player {
play() {
_howler.play()
this.state = State.PLAYING
this._progress = _howler.seek()
}
/**

View file

@ -84,6 +84,11 @@ export default defineConfig({
changeOrigin: true,
rewrite: path => path.replace(/^\/netease/, ''),
},
'/yesplaymusic/': {
target: `http://127.0.0.1:${process.env.ELECTRON_DEV_NETEASE_API_PORT}/yesplaymusic`,
changeOrigin: true,
rewrite: path => path.replace(/^\/yesplaymusic/, ''),
},
},
},
})

234
pnpm-lock.yaml generated
View file

@ -1,9 +1,9 @@
lockfileVersion: 5.3
specifiers:
'@trivago/prettier-plugin-sort-imports': ^3.2.0
'@types/cookie-parser': ^1.4.2
'@types/express': ^4.17.13
'@types/express-fileupload': ^1.2.2
'@types/howler': ^2.2.6
'@types/js-cookie': ^3.0.1
'@types/lodash-es': ^4.17.6
@ -19,6 +19,7 @@ specifiers:
ansi-styles: ^6.1.0
autoprefixer: ^10.4.4
axios: ^0.26.1
body-parser: ^1.19.2
change-case: ^4.1.2
classnames: ^2.3.1
color.js: ^1.2.0
@ -37,10 +38,13 @@ specifiers:
eslint-plugin-react: ^7.29.4
eslint-plugin-react-hooks: ^4.3.0
express: ^4.17.3
express-fileupload: ^1.3.1
fast-folder-size: ^1.6.1
howler: ^2.2.3
js-cookie: ^3.0.1
lodash-es: ^4.17.21
md5: ^2.3.0
music-metadata: ^7.12.2
postcss: ^8.4.12
prettier: 2.5.1
prettier-plugin-tailwindcss: ^0.1.8
@ -74,9 +78,9 @@ dependencies:
realm: 10.13.0
devDependencies:
'@trivago/prettier-plugin-sort-imports': 3.2.0_prettier@2.5.1
'@types/cookie-parser': 1.4.2
'@types/express': 4.17.13
'@types/express-fileupload': 1.2.2
'@types/howler': 2.2.6
'@types/js-cookie': 3.0.1
'@types/lodash-es': 4.17.6
@ -91,6 +95,7 @@ devDependencies:
ansi-styles: 6.1.0
autoprefixer: 10.4.4_postcss@8.4.12
axios: 0.26.1
body-parser: 1.19.2
classnames: 2.3.1
color.js: 1.2.0
colord: 2.9.2
@ -104,10 +109,13 @@ devDependencies:
eslint: 8.11.0
eslint-plugin-react: 7.29.4_eslint@8.11.0
eslint-plugin-react-hooks: 4.3.0_eslint@8.11.0
express-fileupload: 1.3.1
fast-folder-size: 1.6.1
howler: 2.2.3
js-cookie: 3.0.1
lodash-es: 4.17.21
md5: 2.3.0
music-metadata: 7.12.2
postcss: 8.4.12
prettier: 2.5.1
prettier-plugin-tailwindcss: 0.1.8_prettier@2.5.1
@ -159,30 +167,6 @@ packages:
engines: {node: '>=6.9.0'}
dev: true
/@babel/core/7.13.10:
resolution: {integrity: sha512-bfIYcT0BdKeAZrovpMqX2Mx5NrgAckGbwT982AkdS5GNfn3KMGiprlBAtmBcFZRUmpaufS6WZFP8trvx8ptFDw==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/code-frame': 7.16.7
'@babel/generator': 7.17.3
'@babel/helper-compilation-targets': 7.16.7_@babel+core@7.13.10
'@babel/helper-module-transforms': 7.17.6
'@babel/helpers': 7.17.2
'@babel/parser': 7.17.3
'@babel/template': 7.16.7
'@babel/traverse': 7.17.3
'@babel/types': 7.17.0
convert-source-map: 1.8.0
debug: 4.3.3
gensync: 1.0.0-beta.2
json5: 2.2.0
lodash: 4.17.21
semver: 6.3.0
source-map: 0.5.7
transitivePeerDependencies:
- supports-color
dev: true
/@babel/core/7.17.2:
resolution: {integrity: sha512-R3VH5G42VSDolRHyUO4V2cfag8WHcZyxdq5Z/m8Xyb92lW/Erm/6kM+XtRFGf3Mulre3mveni2NHfEUws8wSvw==}
engines: {node: '>=6.9.0'}
@ -206,14 +190,6 @@ packages:
- supports-color
dev: true
/@babel/generator/7.13.9:
resolution: {integrity: sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw==}
dependencies:
'@babel/types': 7.17.0
jsesc: 2.5.2
source-map: 0.5.7
dev: true
/@babel/generator/7.17.3:
resolution: {integrity: sha512-+R6Dctil/MgUsZsZAkYgK+ADNSZzJRRy0TvY65T71z/CR854xHQ1EweBYXdfT+HNeN7w0cSJJEzgxZMv40pxsg==}
engines: {node: '>=6.9.0'}
@ -230,19 +206,6 @@ packages:
'@babel/types': 7.17.0
dev: true
/@babel/helper-compilation-targets/7.16.7_@babel+core@7.13.10:
resolution: {integrity: sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
'@babel/compat-data': 7.17.0
'@babel/core': 7.13.10
'@babel/helper-validator-option': 7.16.7
browserslist: 4.19.1
semver: 6.3.0
dev: true
/@babel/helper-compilation-targets/7.16.7_@babel+core@7.17.2:
resolution: {integrity: sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA==}
engines: {node: '>=6.9.0'}
@ -358,12 +321,6 @@ packages:
js-tokens: 4.0.0
dev: true
/@babel/parser/7.14.6:
resolution: {integrity: sha512-oG0ej7efjEXxb4UgE+klVx+3j4MVo+A2vCzm7OUN4CLo6WhQ+vSOD2yJ8m7B+DghObxtLxt3EfgMWpq+AsWehQ==}
engines: {node: '>=6.0.0'}
hasBin: true
dev: true
/@babel/parser/7.17.3:
resolution: {integrity: sha512-7yJPvPV+ESz2IUTPbOL+YkIGyCqOyNIzdguKQuJGnH7bg1WTIifuM21YqokFt/THWh1AkCRn9IgoykTRCBVpzA==}
engines: {node: '>=6.0.0'}
@ -440,22 +397,6 @@ packages:
'@babel/types': 7.17.0
dev: true
/@babel/traverse/7.13.0:
resolution: {integrity: sha512-xys5xi5JEhzC3RzEmSGrs/b3pJW/o87SypZ+G/PhaE7uqVQNv/jlmVIBXuoh5atqQ434LfXV+sf23Oxj0bchJQ==}
dependencies:
'@babel/code-frame': 7.16.7
'@babel/generator': 7.17.3
'@babel/helper-function-name': 7.16.7
'@babel/helper-split-export-declaration': 7.16.7
'@babel/parser': 7.17.3
'@babel/types': 7.17.0
debug: 4.3.3
globals: 11.12.0
lodash: 4.17.21
transitivePeerDependencies:
- supports-color
dev: true
/@babel/traverse/7.17.3:
resolution: {integrity: sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==}
engines: {node: '>=6.9.0'}
@ -474,14 +415,6 @@ packages:
- supports-color
dev: true
/@babel/types/7.13.0:
resolution: {integrity: sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA==}
dependencies:
'@babel/helper-validator-identifier': 7.16.7
lodash: 4.17.21
to-fast-properties: 2.0.0
dev: true
/@babel/types/7.17.0:
resolution: {integrity: sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==}
engines: {node: '>=6.9.0'}
@ -643,7 +576,6 @@ packages:
/@tokenizer/token/0.3.0:
resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
dev: false
/@tootallnate/once/1.1.2:
resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==}
@ -655,23 +587,6 @@ packages:
engines: {node: '>= 10'}
dev: true
/@trivago/prettier-plugin-sort-imports/3.2.0_prettier@2.5.1:
resolution: {integrity: sha512-DnwLe+z8t/dZX5xBbYZV1+C5STkyK/P6SSq3Nk6NXlJZsgvDZX2eN4ND7bMFgGV/NL/YChWzcNf6ziGba1ktQQ==}
peerDependencies:
prettier: 2.x
dependencies:
'@babel/core': 7.13.10
'@babel/generator': 7.13.9
'@babel/parser': 7.14.6
'@babel/traverse': 7.13.0
'@babel/types': 7.13.0
javascript-natural-sort: 0.7.1
lodash: 4.17.21
prettier: 2.5.1
transitivePeerDependencies:
- supports-color
dev: true
/@trysound/sax/0.2.0:
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
engines: {node: '>=10.13.0'}
@ -684,6 +599,12 @@ packages:
'@types/node': 14.18.10
dev: true
/@types/busboy/0.3.2:
resolution: {integrity: sha512-iEvdm9Z9KdSs/ozuh1Z7ZsXrOl8F4M/CLMXPZHr3QuJ4d6Bjn+HBMC5EMKpwpAo8oi8iK9GZfFoHaIMrrZgwVw==}
dependencies:
'@types/node': 14.18.10
dev: true
/@types/connect/3.4.35:
resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
dependencies:
@ -702,6 +623,13 @@ packages:
'@types/ms': 0.7.31
dev: true
/@types/express-fileupload/1.2.2:
resolution: {integrity: sha512-sWU1EVFfLsdAginKVrkwTRbRPnbn7dawxEFEBgaRDcpNFCUuksZtASaAKEhqwEIg6fSdeTyI6dIUGl3thhrypg==}
dependencies:
'@types/busboy': 0.3.2
'@types/express': 4.17.13
dev: true
/@types/express-serve-static-core/4.17.28:
resolution: {integrity: sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==}
dependencies:
@ -1456,6 +1384,13 @@ packages:
engines: {node: '>=8'}
dev: true
/binary/0.3.0:
resolution: {integrity: sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=}
dependencies:
buffers: 0.1.1
chainsaw: 0.1.0
dev: true
/bindings/1.5.0:
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
dependencies:
@ -1476,6 +1411,10 @@ packages:
bluebird: 3.7.2
dev: true
/bluebird/3.4.7:
resolution: {integrity: sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=}
dev: true
/bluebird/3.7.2:
resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
dev: true
@ -1494,7 +1433,6 @@ packages:
qs: 6.9.7
raw-body: 2.4.3
type-is: 1.6.18
dev: false
/boolbase/1.0.0:
resolution: {integrity: sha1-aN/1++YMUes3cl6p4+0xDcwed24=}
@ -1619,12 +1557,22 @@ packages:
/buffer-from/1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
/buffer-indexof-polyfill/1.0.2:
resolution: {integrity: sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==}
engines: {node: '>=0.10'}
dev: true
/buffer/5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
/buffers/0.1.1:
resolution: {integrity: sha1-skV5w77U1tOWru5tmorn9Ugqt7s=}
engines: {node: '>=0.2.0'}
dev: true
/builder-util-runtime/8.9.2:
resolution: {integrity: sha512-rhuKm5vh7E0aAmT6i8aoSfEjxzdYEFX7zDApK+eNgOhjofnWb74d9SRJv0H/8nsgOkos0TZ4zxW0P8J4N7xQ2A==}
engines: {node: '>=12.0.0'}
@ -1664,12 +1612,10 @@ packages:
engines: {node: '>=4.5.0'}
dependencies:
dicer: 0.3.0
dev: false
/bytes/3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
dev: false
/cache-base/1.0.1:
resolution: {integrity: sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==}
@ -1751,6 +1697,12 @@ packages:
resolution: {integrity: sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=}
dev: false
/chainsaw/0.1.0:
resolution: {integrity: sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=}
dependencies:
traverse: 0.3.9
dev: true
/chalk/1.1.3:
resolution: {integrity: sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=}
engines: {node: '>=0.10.0'}
@ -2054,7 +2006,6 @@ packages:
/content-type/1.0.4:
resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==}
engines: {node: '>= 0.6'}
dev: false
/convert-source-map/1.8.0:
resolution: {integrity: sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==}
@ -2342,7 +2293,6 @@ packages:
/depd/1.1.2:
resolution: {integrity: sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=}
engines: {node: '>= 0.6'}
dev: false
/destroy/1.0.4:
resolution: {integrity: sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=}
@ -2372,7 +2322,6 @@ packages:
engines: {node: '>=4.5.0'}
dependencies:
streamsearch: 0.1.2
dev: false
/didyoumean/1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
@ -2536,6 +2485,12 @@ packages:
engines: {node: '>=10'}
dev: true
/duplexer2/0.1.4:
resolution: {integrity: sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=}
dependencies:
readable-stream: 2.3.7
dev: true
/duplexer3/0.1.4:
resolution: {integrity: sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=}
dev: true
@ -2549,7 +2504,6 @@ packages:
/ee-first/1.1.1:
resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=}
dev: false
/ejs/3.1.6:
resolution: {integrity: sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw==}
@ -3166,7 +3120,6 @@ packages:
engines: {node: '>=12.0.0'}
dependencies:
busboy: 0.3.1
dev: false
/express/4.17.3:
resolution: {integrity: sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==}
@ -3259,6 +3212,14 @@ packages:
/fast-deep-equal/3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
/fast-folder-size/1.6.1:
resolution: {integrity: sha512-F3tRpfkAzb7TT2JNKaJUglyuRjRa+jelQD94s9OSqkfEeytLmupCqQiD+H2KoIXGtp4pB5m4zNmv5m2Ktcr+LA==}
hasBin: true
requiresBuild: true
dependencies:
unzipper: 0.10.11
dev: true
/fast-glob/3.2.11:
resolution: {integrity: sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==}
engines: {node: '>=8.6.0'}
@ -3310,7 +3271,6 @@ packages:
readable-web-to-node-stream: 3.0.2
strtok3: 6.3.0
token-types: 4.2.0
dev: false
/file-uri-to-path/1.0.0:
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
@ -3504,6 +3464,16 @@ packages:
dev: true
optional: true
/fstream/1.0.12:
resolution: {integrity: sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==}
engines: {node: '>=0.6'}
dependencies:
graceful-fs: 4.2.9
inherits: 2.0.4
mkdirp: 0.5.5
rimraf: 2.7.1
dev: true
/ftp/0.3.10:
resolution: {integrity: sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0=}
engines: {node: '>=0.8.0'}
@ -3878,7 +3848,6 @@ packages:
setprototypeof: 1.2.0
statuses: 1.5.0
toidentifier: 1.0.1
dev: false
/http-proxy-agent/4.0.1:
resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==}
@ -3951,7 +3920,6 @@ packages:
engines: {node: '>=0.10.0'}
dependencies:
safer-buffer: 2.1.2
dev: false
/iconv-lite/0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
@ -4349,10 +4317,6 @@ packages:
minimatch: 3.1.2
dev: true
/javascript-natural-sort/0.7.1:
resolution: {integrity: sha1-+eIwPUUH9tdDVac2ZNFED7Wg71k=}
dev: true
/js-base64/2.6.4:
resolution: {integrity: sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==}
dev: true
@ -4547,6 +4511,10 @@ packages:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
dev: true
/listenercount/1.0.1:
resolution: {integrity: sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=}
dev: true
/loader-utils/1.4.0:
resolution: {integrity: sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==}
engines: {node: '>=4.0.0'}
@ -4674,12 +4642,10 @@ packages:
/media-typer/0.3.0:
resolution: {integrity: sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=}
engines: {node: '>= 0.6'}
dev: false
/media-typer/1.1.0:
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
engines: {node: '>= 0.8'}
dev: false
/merge-descriptors/1.0.1:
resolution: {integrity: sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=}
@ -4852,7 +4818,6 @@ packages:
token-types: 4.2.0
transitivePeerDependencies:
- supports-color
dev: false
/nano-css/5.3.4_react-dom@17.0.2+react@17.0.2:
resolution: {integrity: sha512-wfcviJB6NOxDIDfr7RFn/GlaN7I/Bhe4d39ZRCJ3xvZX60LVe2qZ+rDqM49nm4YT81gAjzS+ZklhKP/Gnfnubg==}
@ -5103,7 +5068,6 @@ packages:
engines: {node: '>= 0.8'}
dependencies:
ee-first: 1.1.1
dev: false
/once/1.4.0:
resolution: {integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=}
@ -5303,7 +5267,6 @@ packages:
/peek-readable/4.1.0:
resolution: {integrity: sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==}
engines: {node: '>=8'}
dev: false
/pend/1.2.0:
resolution: {integrity: sha1-elfrVQpng/kRUzH89GY9XI4AelA=}
@ -5592,7 +5555,6 @@ packages:
/qs/6.9.7:
resolution: {integrity: sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==}
engines: {node: '>=0.6'}
dev: false
/query-string/4.3.4:
resolution: {integrity: sha1-u7aTucqRXCMlFbIosaArYJBD2+s=}
@ -5628,7 +5590,6 @@ packages:
http-errors: 1.8.1
iconv-lite: 0.4.24
unpipe: 1.0.0
dev: false
/rc/1.2.8:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
@ -5798,7 +5759,6 @@ packages:
engines: {node: '>=8'}
dependencies:
readable-stream: 3.6.0
dev: false
/readdirp/3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
@ -5992,6 +5952,13 @@ packages:
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
dev: true
/rimraf/2.7.1:
resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
hasBin: true
dependencies:
glob: 7.2.0
dev: true
/rimraf/3.0.2:
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
hasBin: true
@ -6190,9 +6157,12 @@ packages:
split-string: 3.1.0
dev: true
/setimmediate/1.0.5:
resolution: {integrity: sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=}
dev: true
/setprototypeof/1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
dev: false
/shebang-command/2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
@ -6427,7 +6397,6 @@ packages:
/statuses/1.5.0:
resolution: {integrity: sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=}
engines: {node: '>= 0.6'}
dev: false
/stream-counter/1.0.0:
resolution: {integrity: sha1-kc8lac5NxQYf6816yyY5SloRR1E=}
@ -6437,7 +6406,6 @@ packages:
/streamsearch/0.1.2:
resolution: {integrity: sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=}
engines: {node: '>=0.8.0'}
dev: false
/strict-uri-encode/1.1.0:
resolution: {integrity: sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=}
@ -6524,7 +6492,6 @@ packages:
dependencies:
'@tokenizer/token': 0.3.0
peek-readable: 4.1.0
dev: false
/stylis/4.0.13:
resolution: {integrity: sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==}
@ -6763,7 +6730,6 @@ packages:
/toidentifier/1.0.1:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
dev: false
/token-types/4.2.0:
resolution: {integrity: sha512-P0rrp4wUpefLncNamWIef62J0v0kQR/GfDVji9WKY7GDCWy5YbVSrKUTam07iWPZQGy0zWNOfstYTykMmPNR7w==}
@ -6771,7 +6737,6 @@ packages:
dependencies:
'@tokenizer/token': 0.3.0
ieee754: 1.2.1
dev: false
/tough-cookie/2.5.0:
resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==}
@ -6785,6 +6750,10 @@ packages:
resolution: {integrity: sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=}
dev: false
/traverse/0.3.9:
resolution: {integrity: sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=}
dev: true
/traverse/0.6.6:
resolution: {integrity: sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=}
dev: true
@ -6866,7 +6835,6 @@ packages:
dependencies:
media-typer: 0.3.0
mime-types: 2.1.34
dev: false
/typedarray-to-buffer/3.1.5:
resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==}
@ -6933,7 +6901,6 @@ packages:
/unpipe/1.0.0:
resolution: {integrity: sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=}
engines: {node: '>= 0.8'}
dev: false
/unplugin-auto-import/0.6.6_rollup@2.70.1+vite@2.8.6:
resolution: {integrity: sha512-x3YxAI9ePoumXOakuS5YJlFkSyAkl5vJlaFZSJhSp75nH5gg8LpqQ/0Gz1/CG/JRRv+xaE1CZpEV161AqFGjEg==}
@ -6996,6 +6963,21 @@ packages:
yaku: 0.16.7
dev: true
/unzipper/0.10.11:
resolution: {integrity: sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==}
dependencies:
big-integer: 1.6.51
binary: 0.3.0
bluebird: 3.4.7
buffer-indexof-polyfill: 1.0.2
duplexer2: 0.1.4
fstream: 1.0.12
graceful-fs: 4.2.9
listenercount: 1.0.1
readable-stream: 2.3.7
setimmediate: 1.0.5
dev: true
/update-notifier/5.1.0:
resolution: {integrity: sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==}
engines: {node: '>=10'}

View file

@ -14,9 +14,4 @@ module.exports = {
// Tailwind CSS
plugins: [require('prettier-plugin-tailwindcss')],
tailwindConfig: './tailwind.config.js',
// Sort import order
importOrder: ['^@/(.*)$', '^[./]'],
importOrderSeparation: false,
importOrderSortSpecifiers: true,
}