deploy: web: 0.4.5 (#1615)

This commit is contained in:
pan93412 2022-05-05 00:48:17 +08:00 committed by GitHub
commit ff9c8a2d6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 1716 additions and 1101 deletions

View File

@ -1,5 +1,8 @@
name: Release
env:
YARN_INSTALL_NOPT: yarn add --ignore-platform --ignore-optional
on:
push:
branches:
@ -39,9 +42,41 @@ jobs:
- name: Install Snapcraft (on Ubuntu)
uses: samuelmeuli/action-snapcraft@v1
if: startsWith(matrix.os, 'ubuntu')
# with:
# Disable since the Snapcraft token is currently not working
# snapcraft_token: ${{ secrets.snapcraft_token }}
with:
snapcraft_token: ${{ secrets.snapcraft_token }}
- id: get_unm_version
name: Get the installed UNM version
run: |
yarn --ignore-optional
unm_version=$(node -e "console.log(require('./node_modules/@unblockneteasemusic/rust-napi/package.json').version)")
echo "::set-output name=unmver::${unm_version}"
shell: bash
- name: Install UNM dependencies for Windows
if: runner.os == 'Windows'
run: |
${{ env.YARN_INSTALL_NOPT }} \
@unblockneteasemusic/rust-napi-win32-x64-msvc@${{steps.get_unm_version.outputs.unmver}}
shell: bash
- name: Install UNM dependencies for macOS
if: runner.os == 'macOS'
run: |
${{ env.YARN_INSTALL_NOPT }} \
@unblockneteasemusic/rust-napi-darwin-x64@${{steps.get_unm_version.outputs.unmver}} \
@unblockneteasemusic/rust-napi-darwin-arm64@${{steps.get_unm_version.outputs.unmver}} \
dmg-license
shell: bash
- name: Install UNM dependencies for Linux
if: runner.os == 'Linux'
run: |
${{ env.YARN_INSTALL_NOPT }} \
@unblockneteasemusic/rust-napi-linux-x64-gnu@${{steps.get_unm_version.outputs.unmver}} \
@unblockneteasemusic/rust-napi-linux-arm64-gnu@${{steps.get_unm_version.outputs.unmver}} \
@unblockneteasemusic/rust-napi-linux-arm-gnueabihf@${{steps.get_unm_version.outputs.unmver}}
shell: bash
- name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1.6.0

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
14

View File

@ -7,7 +7,8 @@
},
"target": "ES6",
"module": "commonjs",
"allowSyntheticDefaultImports": true
"allowSyntheticDefaultImports": true,
"jsx": "preserve"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]

View File

@ -1,6 +1,6 @@
{
"name": "yesplaymusic",
"version": "0.4.4",
"version": "0.4.5",
"private": true,
"description": "A third party music player for Netease Music",
"author": "qier222<qier222@outlook.com>",
@ -23,12 +23,12 @@
},
"main": "background.js",
"dependencies": {
"@unblockneteasemusic/server": "v0.27.0-rc.6",
"@unblockneteasemusic/rust-napi": "^0.3.0-pre.1",
"NeteaseCloudMusicApi": "^4.5.2",
"axios": "^0.21.0",
"axios": "^0.26.1",
"change-case": "^4.1.2",
"cli-color": "^2.0.0",
"color": "^3.1.3",
"color": "^4.2.3",
"core-js": "^3.6.5",
"crypto-js": "^4.0.0",
"dayjs": "^1.8.36",
@ -36,14 +36,14 @@
"discord-rich-presence": "^0.0.8",
"electron": "^13.6.7",
"electron-builder": "^23.0.0",
"electron-context-menu": "^2.3.0",
"electron-context-menu": "^3.1.2",
"electron-debug": "^3.1.0",
"electron-devtools-installer": "^3.2",
"electron-icon-builder": "^1.0.2",
"electron-is-dev": "^1.2.0",
"electron-icon-builder": "^2.0.1",
"electron-is-dev": "^2.0.0",
"electron-log": "^4.3.0",
"electron-store": "^6.0.1",
"electron-updater": "^4.3.5",
"electron-store": "^8.0.1",
"electron-updater": "^5.0.1",
"express": "^4.17.1",
"express-fileupload": "^1.2.0",
"express-http-proxy": "^1.6.2",
@ -62,12 +62,12 @@
"prettier": "2.5.1",
"qrcode": "^1.4.4",
"register-service-worker": "^1.7.1",
"svg-sprite-loader": "^5.0.0",
"svg-sprite-loader": "^6.0.11",
"tunnel": "^0.0.6",
"vscode-codicons": "^0.0.17",
"vue": "^2.6.11",
"vue-analytics": "^5.22.1",
"vue-clipboard2": "^0.3.1",
"vue-gtag": "1",
"vue-i18n": "^8.22.0",
"vue-router": "^3.4.3",
"vue-slider-component": "^3.2.5",

7
restyled.yml Normal file
View File

@ -0,0 +1,7 @@
commit_template: 'style: with ${restyler.name}'
restylers:
- prettier
- prettier-json
- prettier-markdown
- prettier-yaml
- whitespace

View File

@ -15,16 +15,18 @@ import {
* @param {string} id - 音乐的 id例如 id=405998841,33894312
*/
export function getMP3(id) {
let br =
store.state.settings?.musicQuality !== undefined
? store.state.settings.musicQuality
: 320000;
const getBr = () => {
// 当返回的 quality >= 400000时就会优先返回 hi-res
const quality = store.state.settings?.musicQuality ?? '320000';
return quality === 'flac' ? '350000' : quality;
};
return request({
url: '/song/url',
method: 'get',
params: {
id,
br,
br: getBr(),
},
});
}

View File

@ -7,6 +7,7 @@ import {
dialog,
globalShortcut,
nativeTheme,
screen,
} from 'electron';
import {
isWindows,
@ -201,8 +202,42 @@ class Background {
};
if (this.store.get('window.x') && this.store.get('window.y')) {
options.x = this.store.get('window.x');
options.y = this.store.get('window.y');
let x = this.store.get('window.x');
let y = this.store.get('window.y');
let displays = screen.getAllDisplays();
let isResetWindiw = false;
if (displays.length === 1) {
let { bounds } = displays[0];
if (
x < bounds.x ||
x > bounds.x + bounds.width - 50 ||
y < bounds.y ||
y > bounds.y + bounds.height - 50
) {
isResetWindiw = true;
}
} else {
isResetWindiw = true;
for (let i = 0; i < displays.length; i++) {
let { bounds } = displays[i];
if (
x > bounds.x &&
x < bounds.x + bounds.width &&
y > bounds.y &&
y < bounds.y - bounds.height
) {
// 检测到APP窗口当前处于一个可用的屏幕里break
isResetWindiw = false;
break;
}
}
}
if (!isResetWindiw) {
options.x = x;
options.y = y;
}
}
this.window = new BrowserWindow(options);
@ -261,6 +296,7 @@ class Background {
this.window.once('ready-to-show', () => {
log('window ready-to-show event');
this.window.show();
this.store.set('window', this.window.getBounds());
});
this.window.on('close', e => {
@ -296,6 +332,14 @@ class Background {
this.store.set('window', this.window.getBounds());
});
this.window.on('maximize', () => {
this.window.webContents.send('isMaximized', true);
});
this.window.on('unmaximize', () => {
this.window.webContents.send('isMaximized', false);
});
this.window.webContents.on('new-window', function (e, url) {
e.preventDefault();
log('open url');

View File

@ -2,11 +2,13 @@
<span class="artist-in-line">
{{ computedPrefix }}
<span v-for="(ar, index) in filteredArtists" :key="index">
<router-link v-if="ar.id !== 0" :to="`/artist/${ar.id}`">
{{ ar.name }}
</router-link>
<router-link v-if="ar.id !== 0" :to="`/artist/${ar.id}`">{{
ar.name
}}</router-link>
<span v-else>{{ ar.name }}</span>
<span v-if="index !== filteredArtists.length - 1">, </span>
<span v-if="index !== filteredArtists.length - 1" class="separator"
>,</span
>
</span>
</span>
</template>
@ -40,4 +42,12 @@ export default {
};
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.separator {
/* make separator distinct enough in long list */
margin-left: 1px;
margin-right: 4px;
position: relative;
top: 0.5px;
}
</style>

View File

@ -80,11 +80,13 @@ export default {
box-shadow: 0 6px 12px -4px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(0, 0, 0, 0.06);
backdrop-filter: blur(12px);
border-radius: 8px;
border-radius: 12px;
box-sizing: border-box;
padding: 6px;
z-index: 1000;
-webkit-app-region: no-drag;
transition: background 125ms ease-out, opacity 125ms ease-out,
transform 125ms ease-out;
&:focus {
outline: none;
@ -94,8 +96,9 @@ export default {
[data-theme='dark'] {
.menu {
background: rgba(36, 36, 36, 0.78);
backdrop-filter: blur(16px) contrast(120%);
backdrop-filter: blur(16px) contrast(120%) brightness(60%);
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 0 6px rgba(255, 255, 255, 0.08);
}
.menu .item:hover {
color: var(--color-text);
@ -112,7 +115,7 @@ export default {
font-weight: 600;
font-size: 14px;
padding: 10px 14px;
border-radius: 7px;
border-radius: 8px;
cursor: default;
color: var(--color-text);
display: flex;
@ -120,6 +123,11 @@ export default {
&:hover {
color: var(--color-primary);
background: var(--color-primary-bg-for-transparent);
transition: opacity 125ms ease-out, transform 125ms ease-out;
}
&:active {
opacity: 0.75;
transform: scale(0.95);
}
.svg-icon {
@ -149,7 +157,7 @@ hr {
border-radius: 4px;
}
.info {
margin-left: 8px;
margin-left: 10px;
}
.title {
font-size: 16px;

View File

@ -16,7 +16,7 @@
><svg-icon icon-class="play" />
</button>
</div>
<img :src="imageUrl" :style="imageStyles" />
<img :src="imageUrl" :style="imageStyles" loading="lazy" />
<transition v-if="coverHover || alwaysShowShadow" name="fade">
<div
v-show="focus || alwaysShowShadow"

View File

@ -1,6 +1,6 @@
<template>
<div class="daily-recommend-card" @click="goToDailyTracks">
<img :src="coverUrl" />
<img :src="coverUrl" loading="lazy" />
<div class="container">
<div class="title-box">
<div class="title">

View File

@ -25,6 +25,8 @@ export default {
this.svgStyle = {
height: this.size + 'px',
width: this.size + 'px',
position: 'relative',
left: '-1px',
};
},
};

View File

@ -1,9 +1,10 @@
<template>
<div class="fm" :style="{ background }" data-theme="dark">
<img :src="nextTrackCover" style="display: none" />
<img :src="nextTrackCover" style="display: none" loading="lazy" />
<img
class="cover"
:src="track.album && track.album.picUrl | resizeImage(512)"
loading="lazy"
@click="goToAlbum"
/>
<div class="right-part">
@ -13,19 +14,20 @@
</div>
<div class="controls">
<div class="buttons">
<button-icon title="不喜欢" @click.native="moveToFMTrash"
><svg-icon id="thumbs-down" icon-class="thumbs-down"
/></button-icon>
<button-icon title="不喜欢" @click.native="moveToFMTrash">
<svg-icon id="thumbs-down" icon-class="thumbs-down" />
</button-icon>
<button-icon
:title="$t(isPlaying ? 'player.pause' : 'player.play')"
class="play"
@click.native="play"
>
<svg-icon :icon-class="isPlaying ? 'pause' : 'play'"
/></button-icon>
<button-icon :title="$t('player.next')" @click.native="next"
><svg-icon icon-class="next" /></button-icon
></div>
<svg-icon :icon-class="isPlaying ? 'pause' : 'play'" />
</button-icon>
<button-icon :title="$t('player.next')" @click.native="next">
<svg-icon icon-class="next" />
</button-icon>
</div>
<div class="card-name"><svg-icon icon-class="fm" />私人FM</div>
</div>
</div>
@ -36,7 +38,7 @@
import ButtonIcon from '@/components/ButtonIcon.vue';
import ArtistsInLine from '@/components/ArtistsInLine.vue';
import { mapState } from 'vuex';
import * as Vibrant from 'node-vibrant';
import * as Vibrant from 'node-vibrant/dist/vibrant.worker.min.js';
import Color from 'color';
export default {

View File

@ -12,8 +12,8 @@
<div
class="button max-restore codicon"
:class="{
'codicon-chrome-restore': !isShowMaximized,
'codicon-chrome-maximize': isShowMaximized,
'codicon-chrome-restore': isMaximized,
'codicon-chrome-maximize': !isMaximized,
}"
@click="windowMaxRestore"
></div>
@ -40,7 +40,7 @@ export default {
name: 'LinuxTitlebar',
data() {
return {
isShowMaximized: true,
isMaximized: false,
};
},
computed: {
@ -49,9 +49,7 @@ export default {
created() {
if (process.env.IS_ELECTRON === true) {
ipcRenderer.on('isMaximized', (_, value) => {
// valuefalse
// valuetrue
this.isShowMaximized = value;
this.isMaximized = value;
});
}
},

View File

@ -39,11 +39,16 @@ export default {
type: Boolean,
default: false,
},
minWidth: {
type: String,
default: 'calc(min(23rem, 100vw))',
},
},
computed: {
modalStyles() {
return {
width: this.width,
minWidth: this.minWidth,
};
},
},

View File

@ -17,7 +17,7 @@
class="playlist"
@click="addTrackToPlaylist(playlist.id)"
>
<img :src="playlist.coverImgUrl | resizeImage(224)" />
<img :src="playlist.coverImgUrl | resizeImage(224)" loading="lazy" />
<div class="info">
<div class="title">{{ playlist.name }}</div>
<div class="track-count">{{ playlist.trackCount }} </div>

View File

@ -147,6 +147,7 @@ export default {
label {
font-size: 12px;
}
user-select: none;
}
}
}

View File

@ -7,7 +7,7 @@
@mouseleave="hoverVideoID = 0"
@click="goToMv(getID(mv))"
>
<img :src="getUrl(mv)" />
<img :src="getUrl(mv)" loading="lazy" />
<transition name="fade">
<div
v-show="hoverVideoID === getID(mv)"

View File

@ -43,7 +43,12 @@
</div>
</div>
</div>
<img class="avatar" :src="avatarUrl" @click="showUserProfileMenu" />
<img
class="avatar"
:src="avatarUrl"
@click="showUserProfileMenu"
loading="lazy"
/>
</div>
</nav>

View File

@ -27,6 +27,7 @@
<div class="container" @click.stop>
<img
:src="currentTrack.al && currentTrack.al.picUrl | resizeImage(224)"
loading="lazy"
@click="goToAlbum"
/>
<div class="track-info" :title="audioSource">

View File

@ -2,7 +2,10 @@
<div class="track-list">
<ContextMenu ref="menu">
<div v-show="type !== 'cloudDisk'" class="item-info">
<img :src="rightClickedTrackComputed.al.picUrl | resizeImage(224)" />
<img
:src="rightClickedTrackComputed.al.picUrl | resizeImage(224)"
loading="lazy"
/>
<div class="info">
<div class="title">{{ rightClickedTrackComputed.name }}</div>
<div class="subtitle">{{ rightClickedTrackComputed.ar[0].name }}</div>
@ -46,6 +49,9 @@
@click="addTrackToPlaylist"
>{{ $t('contextMenu.addToPlaylist') }}</div
>
<div v-show="type !== 'cloudDisk'" class="item" @click="copyLink">{{
$t('contextMenu.copyUrl')
}}</div>
<div
v-if="extraContextMenuItem.includes('removeTrackFromCloudDisk')"
class="item"
@ -265,6 +271,12 @@ export default {
});
}
},
copyLink() {
navigator.clipboard.writeText(
`https://music.163.com/song?id=${this.rightClickedTrack.id}`
);
this.showToast(locale.t('toast.copied'));
},
removeTrackFromQueue() {
this.$store.state.player.removeTrackFromQueue(
this.rightClickedTrackIndex

View File

@ -10,6 +10,7 @@
<img
v-if="!isAlbum"
:src="imgUrl"
loading="lazy"
:class="{ hover: focus }"
@click="goToAlbum"
/>
@ -208,6 +209,7 @@ export default {
methods: {
goToAlbum() {
if (this.track.al.id === 0) return;
this.$router.push({ path: '/album/' + this.track.al.id });
},
playTrack() {
@ -272,7 +274,6 @@ button {
}
.explicit-symbol.before-artist {
margin-right: 2px;
.svg-icon {
margin-bottom: -3px;
}
@ -364,6 +365,11 @@ button {
opacity: 0.88;
color: var(--color-text);
}
.count {
font-weight: bold;
font-size: 22px;
line-height: 22px;
}
}
.track.focus {
@ -425,7 +431,8 @@ button {
}
.title .featured,
.artist,
.explicit-symbol {
.explicit-symbol,
.count {
color: var(--color-primary);
opacity: 0.88;
}

View File

@ -9,8 +9,8 @@
<div
class="button max-restore codicon"
:class="{
'codicon-chrome-restore': !isShowMaximized,
'codicon-chrome-maximize': isShowMaximized,
'codicon-chrome-restore': isMaximized,
'codicon-chrome-maximize': !isMaximized,
}"
@click="windowMaxRestore"
></div>
@ -37,7 +37,7 @@ export default {
name: 'Win32Titlebar',
data() {
return {
isShowMaximized: true,
isMaximized: false,
};
},
computed: {
@ -46,9 +46,7 @@ export default {
created() {
if (process.env.IS_ELECTRON === true) {
ipcRenderer.on('isMaximized', (_, value) => {
// valuefalse
// valuetrue
this.isShowMaximized = value;
this.isMaximized = value;
});
}
},

View File

@ -1,5 +1,5 @@
import { app, dialog, globalShortcut, ipcMain } from 'electron';
import match from '@unblockneteasemusic/server';
import UNM from '@unblockneteasemusic/rust-napi';
import { registerGlobalShortcut } from '@/electron/globalShortcut';
import cloneDeep from 'lodash/cloneDeep';
import shortcuts from '@/utils/shortcuts';
@ -88,10 +88,10 @@ function toBuffer(data) {
}
/**
* Get the file URI from bilivideo.
* Get the file base64 data from bilivideo.
*
* @param {string} url The URL to fetch.
* @returns {Promise<string>} The file URI.
* @returns {Promise<string>} The file base64 data.
*/
async function getBiliVideoFile(url) {
const axios = await import('axios').then(m => m.default);
@ -106,61 +106,97 @@ async function getBiliVideoFile(url) {
const buffer = toBuffer(response.data);
const encodedData = buffer.toString('base64');
return `data:application/octet-stream;base64,${encodedData}`;
return encodedData;
}
/**
* Parse the source string (`a, b`) to source list `['a', 'b']`.
*
* @param {import("@unblockneteasemusic/rust-napi").Executor} executor
* @param {string} sourceString The source string.
* @returns {string[]} The source list.
*/
function parseSourceStringToList(sourceString) {
return sourceString.split(',').map(s => s.trim());
function parseSourceStringToList(executor, sourceString) {
const availableSource = executor.list();
return sourceString
.split(',')
.map(s => s.trim().toLowerCase())
.filter(s => {
const isAvailable = availableSource.includes(s);
if (!isAvailable) {
log(`This source is not one of the supported source: ${s}`);
}
return isAvailable;
});
}
export function initIpcMain(win, store, trayEventEmitter) {
ipcMain.handle('unblock-music', async (_, track, source) => {
// 兼容 unblockneteasemusic 所使用的 api 字段
track.alias = track.alia || [];
track.duration = track.dt || 0;
track.album = track.al || [];
track.artists = track.ar || [];
// WIP: Do not enable logging as it has some issues in non-blocking I/O environment.
// UNM.enableLogging(UNM.LoggingType.ConsoleEnv);
const unmExecutor = new UNM.Executor();
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject('timeout');
}, 5000);
});
ipcMain.handle(
'unblock-music',
/**
*
* @param {*} _
* @param {string | null} sourceListString
* @param {Record<string, any>} ncmTrack
* @param {UNM.Context} context
*/
async (_, sourceListString, ncmTrack, context) => {
// Formt the track input
// FIXME: Figure out the structure of Track
const song = {
id: ncmTrack.id && ncmTrack.id.toString(),
name: ncmTrack.name,
duration: ncmTrack.dt,
album: ncmTrack.al && {
id: ncmTrack.al.id && ncmTrack.al.id.toString(),
name: ncmTrack.al.name,
},
artists: ncmTrack.ar
? ncmTrack.ar.map(({ id, name }) => ({
id: id && id.toString(),
name,
}))
: [],
};
const sourceList =
typeof source === 'string' ? parseSourceStringToList(source) : null;
log(`[UNM] using source: ${sourceList || '<default>'}`);
const sourceList =
typeof sourceListString === 'string'
? parseSourceStringToList(unmExecutor, sourceListString)
: ['migu', 'ytdl', 'bilibili', 'pyncm', 'kugou'];
log(`[UNM] using source: ${sourceList.join(', ')}`);
log(`[UNM] using configuration: ${JSON.stringify(context)}`);
try {
const matchedAudio = await Promise.race([
try {
// TODO: tell users to install yt-dlp.
// we passed "null" to source, to let UNM choose the default source.
match(track.id, sourceList, track),
timeoutPromise,
]);
const matchedAudio = await unmExecutor.search(
sourceList,
song,
context
);
const retrievedSong = await unmExecutor.retrieve(matchedAudio, context);
if (!matchedAudio || !matchedAudio.url) {
throw new Error('no such a song found');
// bilibili's audio file needs some special treatment
if (retrievedSong.url.includes('bilivideo.com')) {
retrievedSong.url = await getBiliVideoFile(retrievedSong.url);
}
log(`respond with retrieve song…`);
log(JSON.stringify(matchedAudio));
return retrievedSong;
} catch (err) {
const errorMessage = err instanceof Error ? `${err.message}` : `${err}`;
log(`UnblockNeteaseMusic failed: ${errorMessage}`);
return null;
}
// bilibili's audio file needs some special treatment
if (matchedAudio.url.includes('bilivideo.com')) {
matchedAudio.url = await getBiliVideoFile(matchedAudio.url);
}
return matchedAudio;
} catch (err) {
const errorMessage = err instanceof Error ? `${err.message}` : `${err}`;
log(`UnblockNeteaseMusic failed: ${errorMessage}`);
return null;
}
});
);
ipcMain.on('close', e => {
if (isMac) {
@ -186,9 +222,7 @@ export function initIpcMain(win, store, trayEventEmitter) {
});
ipcMain.on('maximizeOrUnmaximize', () => {
const isMaximized = win.isMaximized();
isMaximized ? win.unmaximize() : win.maximize();
win.webContents.send('isMaximized', isMaximized);
win.isMaximized() ? win.unmaximize() : win.maximize();
});
ipcMain.on('settings', (event, options) => {

View File

@ -178,6 +178,34 @@ export default {
exit: 'Exit',
minimizeToTray: 'Minimize to tray',
},
unm: {
enable: 'Enable',
audioSource: {
title: 'Audio Sources',
},
enableFlac: {
title: 'Enable FLAC Sources',
desc: 'To take effect, it may be required to clear the cache after enabling this function.',
},
searchMode: {
title: 'Audio Search Mode',
fast: 'Speed Priority',
order: 'Order Priority',
},
cookie: {
joox: 'Cookie for Joox use',
qq: 'Cookie for QQ use',
desc1: 'Click here for the configuration instruction. ',
desc2: 'Leave empty to pick up the default value',
},
ytdl: 'The youtube-dl Executable File for YtDl',
proxy: {
title: 'Proxy Server for UNM',
desc1:
'The proxy server to use for requesting services such as YouTube',
desc2: 'Leave empty to pick up the default value',
},
},
},
contextMenu: {
play: 'Play',

View File

@ -172,6 +172,34 @@ export default {
exit: 'Exit',
minimizeToTray: 'Küçült',
},
unm: {
enable: 'Enable',
audioSource: {
title: 'Audio Sources',
},
enableFlac: {
title: 'Enable FLAC Sources',
desc: 'To take effect, it may be required to clear the cache after enabling this function.',
},
searchMode: {
title: 'Audio Search Mode',
fast: 'Speed Priority',
order: 'Order Priority',
},
cookie: {
joox: 'Cookie for Joox use',
qq: 'Cookie for QQ use',
desc1: 'Click here for the configuration instruction. ',
desc2: 'Leave empty to pick up the default value',
},
ytdl: 'The youtube-dl Executable File for YtDl',
proxy: {
title: 'Proxy Server for UNM',
desc1:
'The proxy server to use for requesting services such as YouTube',
desc2: 'Leave empty to pick up the default value',
},
},
},
contextMenu: {
play: 'Oynat',

View File

@ -179,6 +179,33 @@ export default {
exit: '退出',
minimizeToTray: '最小化到托盘',
},
unm: {
enable: '启用',
audioSource: {
title: '备选音源',
},
enableFlac: {
title: '启用 FLAC',
desc: '启用后需要清除歌曲缓存才能生效',
},
searchMode: {
title: '音源搜索模式',
fast: '速度优先',
order: '顺序优先',
},
cookie: {
joox: 'Joox 引擎的 Cookie',
qq: 'QQ 引擎的 Cookie',
desc1: '设置说明请参见此处',
desc2: ',留空则不进行相关设置',
},
ytdl: 'YtDl 引擎要使用的 youtube-dl 可执行文件',
proxy: {
title: '用于 UNM 的代理服务器',
desc1: '请求如 YouTube 音源服务时要使用的代理服务器',
desc2: '留空则不进行相关设置',
},
},
},
contextMenu: {
play: '播放',

View File

@ -176,6 +176,33 @@ export default {
exit: '退出',
minimizeToTray: '最小化到工作列角落',
},
unm: {
enable: '啟用',
audioSource: {
title: '備選音源',
},
enableFlac: {
title: '啟用 FLAC',
desc: '啟用後需要清除歌曲快取才能生效',
},
searchMode: {
title: '音源搜尋模式',
fast: '速度優先',
order: '順序優先',
},
cookie: {
joox: 'Joox 引擎的 Cookie',
qq: 'QQ 引擎的 Cookie',
desc1: '設定說明請參見此處',
desc2: ',留空則不進行相關設定',
},
ytdl: 'YtDl 引擎要使用的 youtube-dl 執行檔',
proxy: {
title: '用於 UNM 的 Proxy 伺服器',
desc1: '請求如 YouTube 音源服務時要使用的 Proxy 伺服器',
desc2: '留空則不進行相關設定',
},
},
},
contextMenu: {
play: '播放',

View File

@ -1,5 +1,5 @@
import Vue from 'vue';
import VueAnalytics from 'vue-analytics';
import VueGtag from 'vue-gtag';
import App from './App.vue';
import router from './router';
import store from './store';
@ -28,10 +28,13 @@ console.log(
'background:unset;color:unset;'
);
Vue.use(VueAnalytics, {
id: 'UA-180189423-1',
router,
});
Vue.use(
VueGtag,
{
config: { id: 'G-KMJJCFZDKF' },
},
router
);
Vue.config.productionTip = false;
NProgress.configure({ showSpinner: false, trickleSpeed: 100 });

View File

@ -22,6 +22,10 @@ export default {
artists: [],
mvs: [],
cloudDisk: [],
playHistory: {
weekData: [],
allData: [],
},
},
contextMenu: {
clickObjectID: 0,

View File

@ -1,15 +1,16 @@
import { getTrackDetail, scrobble, getMP3 } from '@/api/track';
import shuffle from 'lodash/shuffle';
import { Howler, Howl } from 'howler';
import { cacheTrackSource, getTrackSource } from '@/utils/db';
import { getAlbum } from '@/api/album';
import { getPlaylistDetail, intelligencePlaylist } from '@/api/playlist';
import { getArtist } from '@/api/artist';
import { personalFM, fmTrash } from '@/api/others';
import { trackScrobble, trackUpdateNowPlaying } from '@/api/lastfm';
import { fmTrash, personalFM } from '@/api/others';
import { getPlaylistDetail, intelligencePlaylist } from '@/api/playlist';
import { getMP3, getTrackDetail, scrobble } from '@/api/track';
import store from '@/store';
import { isAccountLoggedIn } from '@/utils/auth';
import { trackUpdateNowPlaying, trackScrobble } from '@/api/lastfm';
import { cacheTrackSource, getTrackSource } from '@/utils/db';
import { isCreateMpris, isCreateTray } from '@/utils/platform';
import { Howl, Howler } from 'howler';
import shuffle from 'lodash/shuffle';
import { decode as base642Buffer } from '@/utils/base64';
const PLAY_PAUSE_FADE_DURATION = 200;
@ -34,14 +35,14 @@ function setTitle(track) {
? `${track.name} · ${track.ar[0].name} - YesPlayMusic`
: 'YesPlayMusic';
if (isCreateTray) {
ipcRenderer.send('updateTrayTooltip', document.title);
ipcRenderer?.send('updateTrayTooltip', document.title);
}
store.commit('updateTitle', document.title);
}
function setTrayLikeState(isLiked) {
if (isCreateTray) {
ipcRenderer.send('updateTrayLikeState', isLiked);
ipcRenderer?.send('updateTrayLikeState', isLiked);
}
}
@ -69,7 +70,9 @@ export default class {
this._playNextList = []; // 当这个list不为空时会优先播放这个list的歌
this._isPersonalFM = false; // 是否是私人FM模式
this._personalFMTrack = { id: 0 }; // 私人FM当前歌曲
this._personalFMNextTrack = { id: 0 }; // 私人FM下一首歌曲信息为了快速加载下一首
this._personalFMNextTrack = {
id: 0,
}; // 私人FM下一首歌曲信息为了快速加载下一首
/**
* The blob records for cleanup.
@ -192,8 +195,6 @@ export default class {
_init() {
this._loadSelfFromLocalStorage();
Howler.autoUnlock = false;
Howler.usingWebAudio = true;
Howler.volume(this.volume);
if (this._enabled) {
@ -222,18 +223,19 @@ export default class {
_setPlaying(isPlaying) {
this._playing = isPlaying;
if (isCreateTray) {
ipcRenderer.send('updateTrayPlayState', this._playing);
ipcRenderer?.send('updateTrayPlayState', this._playing);
}
}
_setIntervals() {
// 同步播放进度
// TODO: 如果 _progress 在别的地方被改变了这个定时器会覆盖之前改变的值是bug
// TODO: 如果 _progress 在别的地方被改变了,
// 这个定时器会覆盖之前改变的值是bug
setInterval(() => {
if (this._howler === null) return;
this._progress = this._howler.seek();
localStorage.setItem('playerCurrentTrackTime', this._progress);
if (isCreateMpris) {
ipcRenderer.send('playerCurrentTrackTime', this._progress);
ipcRenderer?.send('playerCurrentTrackTime', this._progress);
}
}, 1000);
}
@ -313,6 +315,7 @@ export default class {
this._howler = new Howl({
src: [source],
html5: true,
preload: true,
format: ['mp3', 'flac'],
onend: () => {
this._nextTrackCallback();
@ -327,25 +330,27 @@ export default class {
}
this.setOutputDevice();
}
_getAudioSourceBlobURL(data) {
// Create a new object URL.
const source = URL.createObjectURL(new Blob([data]));
// Clean up the previous object URLs since we've created a new one.
// Revoke object URLs can release the memory taken by a Blob,
// which occupied a large proportion of memory.
for (const url in this.createdBlobRecords) {
URL.revokeObjectURL(url);
}
// Then, we replace the createBlobRecords with new one with
// our newly created object URL.
this.createdBlobRecords = [source];
return source;
}
_getAudioSourceFromCache(id) {
return getTrackSource(id).then(t => {
if (!t) return null;
// Create a new object URL.
const source = URL.createObjectURL(new Blob([t.source]));
// Clean up the previous object URLs since we've created a new one.
// Revoke object URLs can release the memory taken by a Blob,
// which occupied a large proportion of memory.
for (const url in this.createdBlobRecords) {
URL.revokeObjectURL(url);
}
// Then, we replace the createBlobRecords with new one with
// our newly created object URL.
this.createdBlobRecords = [source];
return source;
return this._getAudioSourceBlobURL(t.source);
});
}
_getAudioSourceFromNetease(track) {
@ -368,22 +373,72 @@ export default class {
}
async _getAudioSourceFromUnblockMusic(track) {
console.debug(`[debug][Player.js] _getAudioSourceFromUnblockMusic`);
if (
process.env.IS_ELECTRON !== true ||
store.state.settings.enableUnblockNeteaseMusic === false
) {
return null;
}
const source = await ipcRenderer.invoke(
/**
*
* @param {string=} searchMode
* @returns {import("@unblockneteasemusic/rust-napi").SearchMode}
*/
const determineSearchMode = searchMode => {
/**
* FastFirst = 0
* OrderFirst = 1
*/
switch (searchMode) {
case 'fast-first':
return 0;
case 'order-first':
return 1;
default:
return 0;
}
};
/** @type {import("@unblockneteasemusic/rust-napi").RetrievedSongInfo | null} */
const retrieveSongInfo = await ipcRenderer.invoke(
'unblock-music',
store.state.settings.unmSource,
track,
store.state.settings.unmSource
/** @type {import("@unblockneteasemusic/rust-napi").Context} */ ({
enableFlac: store.state.settings.unmEnableFlac || null,
proxyUri: store.state.settings.unmProxyUri || null,
searchMode: determineSearchMode(store.state.settings.unmSearchMode),
config: {
'joox:cookie': store.state.settings.unmJooxCookie || null,
'qq:cookie': store.state.settings.unmQQCookie || null,
'ytdl:exe': store.state.settings.unmYtDlExe || null,
},
})
);
if (store.state.settings.automaticallyCacheSongs && source?.url) {
// TODO: 将unblockMusic字样换成真正的来源比如酷我咪咕等
cacheTrackSource(track, source.url, 128000, 'unblockMusic');
if (store.state.settings.automaticallyCacheSongs && retrieveSongInfo?.url) {
// 对于来自 bilibili 的音源
// retrieveSongInfo.url 是音频数据的base64编码
// 其他音源为实际url
const url =
retrieveSongInfo.source === 'bilibili'
? `data:application/octet-stream;base64,${retrieveSongInfo.url}`
: retrieveSongInfo.url;
cacheTrackSource(track, url, 128000, `unm:${retrieveSongInfo.source}`);
}
return source?.url;
if (!retrieveSongInfo) {
return null;
}
if (retrieveSongInfo.source !== 'bilibili') {
return retrieveSongInfo.url;
}
const buffer = base642Buffer(retrieveSongInfo.url);
return this._getAudioSourceBlobURL(buffer);
}
_getAudioSource(track) {
return this._getAudioSourceFromCache(String(track.id))
@ -489,6 +544,11 @@ export default class {
artist: artists.join(','),
album: track.al.name,
artwork: [
{
src: track.al.picUrl + '?param=224y224',
type: 'image/jpg',
sizes: '224x224',
},
{
src: track.al.picUrl + '?param=512y512',
type: 'image/jpg',
@ -501,7 +561,7 @@ export default class {
navigator.mediaSession.metadata = new window.MediaMetadata(metadata);
if (isCreateMpris) {
ipcRenderer.send('metadata', metadata);
ipcRenderer?.send('metadata', metadata);
}
}
_updateMediaSessionPositionState() {
@ -557,7 +617,7 @@ export default class {
}
let copyTrack = { ...track };
copyTrack.dt -= seekTime * 1000;
ipcRenderer.send('playDiscordPresence', copyTrack);
ipcRenderer?.send('playDiscordPresence', copyTrack);
}
_pauseDiscordPresence(track) {
if (
@ -566,7 +626,7 @@ export default class {
) {
return null;
}
ipcRenderer.send('pauseDiscordPresence', track);
ipcRenderer?.send('pauseDiscordPresence', track);
}
currentTrackID() {
@ -807,7 +867,7 @@ export default class {
sendSelfToIpcMain() {
if (process.env.IS_ELECTRON !== true) return false;
let liked = store.state.liked.songs.includes(this.currentTrack.id);
ipcRenderer.send('player', {
ipcRenderer?.send('player', {
playing: this.playing,
likedCurrentTrack: liked,
});
@ -823,13 +883,13 @@ export default class {
this.repeatMode = 'on';
}
if (isCreateMpris) {
ipcRenderer.send('switchRepeatMode', this.repeatMode);
ipcRenderer?.send('switchRepeatMode', this.repeatMode);
}
}
switchShuffle() {
this.shuffle = !this.shuffle;
if (isCreateMpris) {
ipcRenderer.send('switchShuffle', this.shuffle);
ipcRenderer?.send('switchShuffle', this.shuffle);
}
}
switchReversed() {

67
src/utils/base64.js Normal file
View File

@ -0,0 +1,67 @@
// https://github.com/niklasvh/base64-arraybuffer/blob/master/src/index.ts
// Copyright (c) 2012 Niklas von Hertzen Licensed under the MIT license.
const chars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
// Use a lookup table to find the index.
const lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256);
for (let i = 0; i < chars.length; i++) {
lookup[chars.charCodeAt(i)] = i;
}
export const encode = arraybuffer => {
let bytes = new Uint8Array(arraybuffer),
i,
len = bytes.length,
base64 = '';
for (i = 0; i < len; i += 3) {
base64 += chars[bytes[i] >> 2];
base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
base64 += chars[bytes[i + 2] & 63];
}
if (len % 3 === 2) {
base64 = base64.substring(0, base64.length - 1) + '=';
} else if (len % 3 === 1) {
base64 = base64.substring(0, base64.length - 2) + '==';
}
return base64;
};
export const decode = base64 => {
let bufferLength = base64.length * 0.75,
len = base64.length,
i,
p = 0,
encoded1,
encoded2,
encoded3,
encoded4;
if (base64[base64.length - 1] === '=') {
bufferLength--;
if (base64[base64.length - 2] === '=') {
bufferLength--;
}
}
const arraybuffer = new ArrayBuffer(bufferLength),
bytes = new Uint8Array(arraybuffer);
for (i = 0; i < len; i += 4) {
encoded1 = lookup[base64.charCodeAt(i)];
encoded2 = lookup[base64.charCodeAt(i + 1)];
encoded3 = lookup[base64.charCodeAt(i + 2)];
encoded4 = lookup[base64.charCodeAt(i + 3)];
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
}
return arraybuffer;
};

View File

@ -3,6 +3,5 @@ export const isMac = process.platform === 'darwin';
export const isLinux = process.platform === 'linux';
export const isDevelopment = process.env.NODE_ENV === 'development';
export const isCreateTray =
process.env.IS_ELECTRON && (isWindows || isLinux || isDevelopment);
export const isCreateTray = isWindows || isLinux || isDevelopment;
export const isCreateMpris = isLinux;

View File

@ -1,6 +1,6 @@
import axios from 'axios';
import { getCookie, doLogout } from '@/utils/auth';
import router from '@/router';
import { doLogout, getCookie } from '@/utils/auth';
import axios from 'axios';
let baseURL = '';
// Web 和 Electron 跑在不同端口避免同时启动时冲突
@ -34,6 +34,10 @@ service.interceptors.request.use(function (config) {
config.params.realIP = '211.161.244.70';
}
if (process.env.VUE_APP_REAL_IP) {
config.params.realIP = process.env.VUE_APP_REAL_IP;
}
const proxy = JSON.parse(localStorage.getItem('settings')).proxyConfig;
if (['HTTP', 'HTTPS'].includes(proxy.protocol)) {
config.params.proxy = `${proxy.protocol}://${proxy.server}:${proxy.port}`;

View File

@ -2,7 +2,7 @@
<div v-show="show" class="artist-page">
<div class="artist-info">
<div class="head">
<img :src="artist.img1v1Url | resizeImage(1024)" />
<img :src="artist.img1v1Url | resizeImage(1024)" loading="lazy" />
</div>
<div>
<div class="name">{{ artist.name }}</div>
@ -75,7 +75,7 @@
@mouseleave="mvHover = false"
@click="goToMv(latestMV.id)"
>
<img :src="latestMV.coverUrl" />
<img :src="latestMV.coverUrl" loading="lazy" />
<transition name="fade">
<div
v-show="mvHover"
@ -127,7 +127,7 @@
<div v-if="mvs.length !== 0" id="mvs" class="mvs">
<div class="section-title"
>MVs
<router-link v-show="hasMoreMV" :to="`/artist/${this.artist.id}/mv`">{{
<router-link v-show="hasMoreMV" :to="`/artist/${artist.id}/mv`">{{
$t('home.seeMore')
}}</router-link>
</div>

View File

@ -1,9 +1,11 @@
<template>
<div v-show="show">
<h1>
<img class="avatar" :src="artist.img1v1Url | resizeImage(1024)" />{{
artist.name
}}'s Music Videos
<img
class="avatar"
:src="artist.img1v1Url | resizeImage(1024)"
loading="lazy"
/>{{ artist.name }}'s Music Videos
</h1>
<MvRow :mvs="mvs" subtitle="publishTime" />
<div class="load-more">

View File

@ -1,9 +1,11 @@
<template>
<div v-show="show" ref="library">
<h1>
<img class="avatar" :src="data.user.avatarUrl | resizeImage" />{{
data.user.nickname
}}{{ $t('library.sLibrary') }}
<img
class="avatar"
:src="data.user.avatarUrl | resizeImage"
loading="lazy"
/>{{ data.user.nickname }}{{ $t('library.sLibrary') }}
</h1>
<div class="section-one">
<div class="liked-songs" @click="goToLikedSongsList">
@ -153,10 +155,22 @@
</div>
<div v-show="currentTab === 'playHistory'">
<button class="playHistory-button" @click="playHistoryMode = 'week'">
<button
:class="{
'playHistory-button': true,
'playHistory-button--selected': playHistoryMode === 'week',
}"
@click="playHistoryMode = 'week'"
>
{{ $t('library.playHistory.week') }}
</button>
<button class="playHistory-button" @click="playHistoryMode = 'all'">
<button
:class="{
'playHistory-button': true,
'playHistory-button--selected': playHistoryMode === 'all',
}"
@click="playHistoryMode = 'all'"
>
{{ $t('library.playHistory.all') }}
</button>
<TrackList
@ -255,7 +269,7 @@ export default {
// Pick 3 or fewer lyrics based on the lyric lines.
const lyricsToPick = Math.min(lyricLine.length, 3);
// The upperbound of the lyric line to pick
// The upperBound of the lyric line to pick
const randomUpperBound = lyricLine.length - lyricsToPick;
const startLyricLineIndex = randomNum(0, randomUpperBound - 1);
@ -280,7 +294,8 @@ export default {
playHistoryList() {
if (this.show && this.playHistoryMode === 'week') {
return this.liked.playHistory.weekData;
} else if (this.show && this.playHistoryMode === 'all') {
}
if (this.show && this.playHistoryMode === 'all') {
return this.liked.playHistory.allData;
}
return [];
@ -581,13 +596,29 @@ button.tab-button {
button.playHistory-button {
color: var(--color-text);
border-radius: 8px;
padding: 10px;
padding: 6px 8px;
margin-bottom: 12px;
margin-right: 4px;
transition: 0.2s;
opacity: 0.68;
font-weight: 500;
cursor: pointer;
&:hover {
opacity: 1;
background: var(--color-secondary-bg);
}
&:active {
transform: scale(0.95);
}
}
button.playHistory-button--selected {
color: var(--color-text);
background: var(--color-secondary-bg);
opacity: 1;
font-weight: 700;
&:active {
transform: none;
}
}
</style>

View File

@ -68,8 +68,8 @@
</div>
<div v-show="mode == 'qrCode'">
<div v-show="qrCodeImage" class="qr-code-container">
<img :src="qrCodeImage" />
<div v-show="qrCodeSvg" class="qr-code-container">
<img :src="qrCodeSvg" loading="lazy" />
</div>
<div class="qr-code-info">
{{ qrCodeInformation }}
@ -135,7 +135,7 @@ export default {
smsCode: '',
inputFocus: '',
qrCodeKey: '',
qrCodeImage: '',
qrCodeSvg: '',
qrCodeCheckInterval: null,
qrCodeInformation: '打开网易云音乐APP扫码登录',
};
@ -233,7 +233,7 @@ export default {
return loginQrCodeKey().then(result => {
if (result.code === 200) {
this.qrCodeKey = result.data.unikey;
QRCode.toDataURL(
QRCode.toString(
`https://music.163.com/login?codekey=${this.qrCodeKey}`,
{
width: 192,
@ -242,10 +242,13 @@ export default {
dark: '#335eea',
light: '#00000000',
},
type: 'svg',
}
)
.then(url => {
this.qrCodeImage = url;
.then(svg => {
this.qrCodeSvg = `data:image/svg+xml;utf8,${encodeURIComponent(
svg
)}`;
})
.catch(err => {
console.error(err);

View File

@ -31,7 +31,11 @@
:class="{ active: user.nickname === activeUser.nickname }"
@click="activeUser = user"
>
<img class="head" :src="user.avatarUrl | resizeImage" />
<img
class="head"
:src="user.avatarUrl | resizeImage"
loading="lazy"
/>
<div class="nickname">
{{ user.nickname }}
</div>

View File

@ -34,7 +34,7 @@
<div>
<div class="cover">
<div class="cover-container">
<img :src="imageUrl" />
<img :src="imageUrl" loading="lazy" />
<div
class="shadow"
:style="{ backgroundImage: `url(${imageUrl})` }"
@ -225,7 +225,7 @@ import { formatTrackTime } from '@/utils/common';
import { getLyric } from '@/api/track';
import { lyricParser } from '@/utils/lyrics';
import ButtonIcon from '@/components/ButtonIcon.vue';
import * as Vibrant from 'node-vibrant';
import * as Vibrant from 'node-vibrant/dist/vibrant.worker.min.js';
import Color from 'color';
import { hasListSource, getListSourcePath } from '@/utils/playList';
@ -431,13 +431,13 @@ export default {
},
getCoverColor() {
if (this.settings.lyricsBackground !== true) return;
const cover = this.currentTrack.al?.picUrl + '?param=1024y1024';
const cover = this.currentTrack.al?.picUrl + '?param=256y256';
Vibrant.from(cover, { colorCount: 1 })
.getPalette()
.then(palette => {
const orignColor = Color.rgb(palette.DarkMuted._rgb);
const color = orignColor.darken(0.1).rgb().string();
const color2 = orignColor.lighten(0.28).rotate(-30).rgb().string();
const originColor = Color.rgb(palette.DarkMuted._rgb);
const color = originColor.darken(0.1).rgb().string();
const color2 = originColor.lighten(0.28).rotate(-30).rgb().string();
this.background = `linear-gradient(to top left, ${color}, ${color2})`;
});
},
@ -704,13 +704,13 @@ export default {
span {
opacity: 0.28;
cursor: default;
font-size: 1em;
font-size: 0.9em;
transition: all 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
span.translation {
opacity: 0.2;
font-size: 0.95em;
font-size: 0.825em;
}
}
@ -722,15 +722,18 @@ export default {
margin-top: 0.1em;
}
.highlight {
transform-origin: center left;
transform: scale(1.05);
}
.highlight span {
opacity: 0.98;
display: inline-block;
font-size: 1.25em;
}
.highlight span.translation {
opacity: 0.65;
font-size: 1.1em;
}
}

View File

@ -139,9 +139,12 @@
<div v-if="isLikeSongsPage" class="user-info">
<h1>
<img class="avatar" :src="data.user.avatarUrl | resizeImage" />{{
data.user.nickname
}}{{ $t('library.sLikedSongs') }}
<img
class="avatar"
:src="data.user.avatarUrl | resizeImage"
loading="lazy"
/>
{{ data.user.nickname }}{{ $t('library.sLikedSongs') }}
</h1>
<div class="search-box-likepage" @click="searchInPlaylist()">
<div class="container" :class="{ active: inputFocus }">

File diff suppressed because one or more lines are too long

View File

@ -56,6 +56,13 @@ module.exports = {
symbolId: 'icon-[name]',
})
.end();
config.module
.rule('napi')
.test(/\.node$/)
.use('node-loader')
.loader('node-loader')
.end();
// LimitChunkCountPlugin 可以通过合并块来对块进行后期处理。用以解决 chunk 包太多的问题
config.plugin('chunkPlugin').use(webpack.optimize.LimitChunkCountPlugin, [
{
@ -69,10 +76,7 @@ module.exports = {
// electron-builder的配置文件
electronBuilder: {
nodeIntegration: true,
externals: [
'@unblockneteasemusic/server',
'@unblockneteasemusic/server/src/consts',
],
externals: ['@unblockneteasemusic/rust-napi'],
builderOptions: {
productName: 'YesPlayMusic',
copyright: 'Copyright © YesPlayMusic',

1747
yarn.lock

File diff suppressed because it is too large Load Diff