From fb21405bf917ad18058bff1a65952000e581ed8f Mon Sep 17 00:00:00 2001 From: qier222 Date: Sat, 19 Mar 2022 17:03:29 +0800 Subject: [PATCH] feat: updates --- .gitignore | 1 + package.json | 5 +- packages/main/cache.ts | 267 ++++++++++++++++++ packages/main/database.ts | 187 +++++------- packages/main/index.ts | 8 +- packages/main/preload.ts | 17 ++ packages/main/server.ts | 67 ++++- packages/renderer/src/api/album.ts | 2 +- packages/renderer/src/api/artist.ts | 4 +- packages/renderer/src/api/playlist.ts | 2 +- packages/renderer/src/api/track.ts | 2 +- packages/renderer/src/api/user.ts | 4 +- packages/renderer/src/api/yesplaymusic.ts | 29 ++ packages/renderer/src/components/CoverRow.tsx | 3 +- packages/renderer/src/components/Player.tsx | 13 +- packages/renderer/src/components/Sidebar.tsx | 4 +- packages/renderer/src/components/Slider.tsx | 4 +- packages/renderer/src/components/Slider2.tsx | 61 ---- .../renderer/src/components/TracksAlbum.tsx | 5 +- .../renderer/src/components/TracksList.tsx | 18 +- packages/renderer/src/hooks/useAlbum.ts | 11 +- .../renderer/src/hooks/useArtistAlbums.ts | 12 +- packages/renderer/src/hooks/usePlaylist.ts | 11 +- packages/renderer/src/hooks/useTracks.ts | 15 +- packages/renderer/src/hooks/useUser.ts | 6 +- .../src/hooks/useUserLikedSongsIDs.ts | 12 +- .../renderer/src/hooks/useUserPlaylists.ts | 12 +- packages/renderer/src/pages/Home.tsx | 13 +- packages/renderer/src/utils/common.ts | 5 +- packages/renderer/src/utils/player.ts | 16 +- packages/renderer/vite.config.ts | 5 + pnpm-lock.yaml | 234 +++++++-------- prettier.config.js | 5 - 33 files changed, 699 insertions(+), 361 deletions(-) create mode 100644 packages/main/cache.ts create mode 100644 packages/main/preload.ts create mode 100644 packages/renderer/src/api/yesplaymusic.ts delete mode 100644 packages/renderer/src/components/Slider2.tsx diff --git a/.gitignore b/.gitignore index 631832f..dc7f310 100644 --- a/.gitignore +++ b/.gitignore @@ -81,6 +81,7 @@ typings/ # ---- dist **/.tmp +/tmp release .DS_Store dist-ssr diff --git a/package.json b/package.json index 3c5d78c..80b9adb 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/main/cache.ts b/packages/main/cache.ts new file mode 100644 index 0000000..b1a02fb --- /dev/null +++ b/packages/main/cache.ts @@ -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 +}) diff --git a/packages/main/database.ts b/packages/main/database.ts index 3db1510..05a24c0 100644 --- a/packages/main/database.ts +++ b/packages/main/database.ts @@ -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 - } - } -} diff --git a/packages/main/index.ts b/packages/main/index.ts index 2dd6f61..a63dacb 100644 --- a/packages/main/index.ts +++ b/packages/main/index.ts @@ -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) ) } }) diff --git a/packages/main/preload.ts b/packages/main/preload.ts new file mode 100644 index 0000000..8dbf322 --- /dev/null +++ b/packages/main/preload.ts @@ -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')}`) diff --git a/packages/main/server.ts b/packages/main/server.ts index 71435f1..5e3eaac 100644 --- a/packages/main/server.ts +++ b/packages/main/server.ts @@ -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}`) }) diff --git a/packages/renderer/src/api/album.ts b/packages/renderer/src/api/album.ts index fd7d3e7..e580704 100644 --- a/packages/renderer/src/api/album.ts +++ b/packages/renderer/src/api/album.ts @@ -8,7 +8,7 @@ export enum AlbumApiNames { export interface FetchAlbumParams { id: number } -interface FetchAlbumResponse { +export interface FetchAlbumResponse { code: number resourceState: boolean album: Album diff --git a/packages/renderer/src/api/artist.ts b/packages/renderer/src/api/artist.ts index 6e688ba..0d6a296 100644 --- a/packages/renderer/src/api/artist.ts +++ b/packages/renderer/src/api/artist.ts @@ -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 diff --git a/packages/renderer/src/api/playlist.ts b/packages/renderer/src/api/playlist.ts index 691ea12..ef8f4ab 100644 --- a/packages/renderer/src/api/playlist.ts +++ b/packages/renderer/src/api/playlist.ts @@ -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 diff --git a/packages/renderer/src/api/track.ts b/packages/renderer/src/api/track.ts index a8bf001..b4df467 100644 --- a/packages/renderer/src/api/track.ts +++ b/packages/renderer/src/api/track.ts @@ -33,7 +33,7 @@ export interface FetchAudioSourceParams { id: number br?: number // bitrate, default 999000,320000 = 320kbps } -interface FetchAudioSourceResponse { +export interface FetchAudioSourceResponse { code: number data: { br: number diff --git a/packages/renderer/src/api/user.ts b/packages/renderer/src/api/user.ts index 2f22e73..e4dca67 100644 --- a/packages/renderer/src/api/user.ts +++ b/packages/renderer/src/api/user.ts @@ -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[] diff --git a/packages/renderer/src/api/yesplaymusic.ts b/packages/renderer/src/api/yesplaymusic.ts new file mode 100644 index 0000000..e2269fd --- /dev/null +++ b/packages/renderer/src/api/yesplaymusic.ts @@ -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, + }, + }) +} diff --git a/packages/renderer/src/components/CoverRow.tsx b/packages/renderer/src/components/CoverRow.tsx index 8bd1485..1956618 100644 --- a/packages/renderer/src/components/CoverRow.tsx +++ b/packages/renderer/src/components/CoverRow.tsx @@ -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) => { diff --git a/packages/renderer/src/components/Player.tsx b/packages/renderer/src/components/Player.tsx index c269c6e..74d81e1 100644 --- a/packages/renderer/src/components/Player.tsx +++ b/packages/renderer/src/components/Player.tsx @@ -87,7 +87,11 @@ const MediaControls = () => { > 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 = () => { { player.progress = value }} diff --git a/packages/renderer/src/components/Sidebar.tsx b/packages/renderer/src/components/Sidebar.tsx index 636702c..0a6f853 100644 --- a/packages/renderer/src/components/Sidebar.tsx +++ b/packages/renderer/src/components/Sidebar.tsx @@ -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 = () => {
{playlists?.playlist?.map(playlist => ( 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', diff --git a/packages/renderer/src/components/Slider.tsx b/packages/renderer/src/components/Slider.tsx index eb4da4a..5c75b25 100644 --- a/packages/renderer/src/components/Slider.tsx +++ b/packages/renderer/src/components/Slider.tsx @@ -13,8 +13,6 @@ const Slider = ({ onlyCallOnChangeAfterDragEnded?: boolean orientation?: 'horizontal' | 'vertical' }) => { - console.log('[Slider.tsx] rendering') - const sliderRef = useRef(null) const [isDragging, setIsDragging] = useState(false) const [draggingValue, setDraggingValue] = useState(value) @@ -133,7 +131,7 @@ const Slider = ({
diff --git a/packages/renderer/src/components/Slider2.tsx b/packages/renderer/src/components/Slider2.tsx deleted file mode 100644 index 294447c..0000000 --- a/packages/renderer/src/components/Slider2.tsx +++ /dev/null @@ -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 ( -
setIsHover(true)} - onMouseLeave={() => setIsHover(false)} - > -
-
- -
-
-
- - setValue(Number(e.target.value))} - className='absolute h-[2px] w-full appearance-none opacity-0' - /> -
- ) -} - -export default Slider diff --git a/packages/renderer/src/components/TracksAlbum.tsx b/packages/renderer/src/components/TracksAlbum.tsx index 1ea52fd..e239f2c 100644 --- a/packages/renderer/src/components/TracksAlbum.tsx +++ b/packages/renderer/src/components/TracksAlbum.tsx @@ -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}) )} diff --git a/packages/renderer/src/components/TracksList.tsx b/packages/renderer/src/components/TracksList.tsx index 67b5d88..d3efe3b 100644 --- a/packages/renderer/src/components/TracksList.tsx +++ b/packages/renderer/src/components/TracksList.tsx @@ -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(
{isSkeleton ? ( @@ -98,9 +101,8 @@ const Track = memo( prefetchAlbum({ id: track.al.id })} className={classNames( - 'hover:underline', + 'hover:underline', isPlaying && 'text-brand-500' )} > @@ -137,7 +139,9 @@ const Track = memo(
{formatDuration(track.dt, 'en', 'hh:mm:ss')} diff --git a/packages/renderer/src/hooks/useAlbum.ts b/packages/renderer/src/hooks/useAlbum.ts index 08abbee..7b70ba5 100644 --- a/packages/renderer/src/hooks/useAlbum.ts +++ b/packages/renderer/src/hooks/useAlbum.ts @@ -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, + }, + }), } ) } diff --git a/packages/renderer/src/hooks/useArtistAlbums.ts b/packages/renderer/src/hooks/useArtistAlbums.ts index 7454623..15bf1be 100644 --- a/packages/renderer/src/hooks/useArtistAlbums.ts +++ b/packages/renderer/src/hooks/useArtistAlbums.ts @@ -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, + }, + }), } ) } diff --git a/packages/renderer/src/hooks/usePlaylist.ts b/packages/renderer/src/hooks/usePlaylist.ts index 131968c..c7fd13d 100644 --- a/packages/renderer/src/hooks/usePlaylist.ts +++ b/packages/renderer/src/hooks/usePlaylist.ts @@ -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, + }, + }), } ) } diff --git a/packages/renderer/src/hooks/useTracks.ts b/packages/renderer/src/hooks/useTracks.ts index 474dad1..c5fee77 100644 --- a/packages/renderer/src/hooks/useTracks.ts +++ b/packages/renderer/src/hooks/useTracks.ts @@ -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小时缓存 } ) } diff --git a/packages/renderer/src/hooks/useUser.ts b/packages/renderer/src/hooks/useUser.ts index ae367e0..c07138d 100644 --- a/packages/renderer/src/hooks/useUser.ts +++ b/packages/renderer/src/hooks/useUser.ts @@ -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', + }), }) } diff --git a/packages/renderer/src/hooks/useUserLikedSongsIDs.ts b/packages/renderer/src/hooks/useUserLikedSongsIDs.ts index 2aabc4e..479bf09 100644 --- a/packages/renderer/src/hooks/useUserLikedSongsIDs.ts +++ b/packages/renderer/src/hooks/useUserLikedSongsIDs.ts @@ -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, + }, + }), } ) } diff --git a/packages/renderer/src/hooks/useUserPlaylists.ts b/packages/renderer/src/hooks/useUserPlaylists.ts index 788a193..1a60e6e 100644 --- a/packages/renderer/src/hooks/useUserPlaylists.ts +++ b/packages/renderer/src/hooks/useUserPlaylists.ts @@ -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, + }, + }), } ) } diff --git a/packages/renderer/src/pages/Home.tsx b/packages/renderer/src/pages/Home.tsx index 363ed8e..ac902c7 100644 --- a/packages/renderer/src/pages/Home.tsx +++ b/packages/renderer/src/pages/Home.tsx @@ -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 (
diff --git a/packages/renderer/src/utils/common.ts b/packages/renderer/src/utils/common.ts index ced77ed..aaa5651 100644 --- a/packages/renderer/src/utils/common.ts +++ b/packages/renderer/src/utils/common.ts @@ -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 = { diff --git a/packages/renderer/src/utils/player.ts b/packages/renderer/src/utils/player.ts index f3fca60..d77330b 100644 --- a/packages/renderer/src/utils/player.ts +++ b/packages/renderer/src/utils/player.ts @@ -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() } /** diff --git a/packages/renderer/vite.config.ts b/packages/renderer/vite.config.ts index ba57ae7..fe4ad61 100644 --- a/packages/renderer/vite.config.ts +++ b/packages/renderer/vite.config.ts @@ -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/, ''), + }, }, }, }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4129111..d0ec83e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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'} diff --git a/prettier.config.js b/prettier.config.js index a48fdbf..8d662fa 100644 --- a/prettier.config.js +++ b/prettier.config.js @@ -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, }