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 name: Release
env:
YARN_INSTALL_NOPT: yarn add --ignore-platform --ignore-optional
on: on:
push: push:
branches: branches:
@ -39,9 +42,41 @@ jobs:
- name: Install Snapcraft (on Ubuntu) - name: Install Snapcraft (on Ubuntu)
uses: samuelmeuli/action-snapcraft@v1 uses: samuelmeuli/action-snapcraft@v1
if: startsWith(matrix.os, 'ubuntu') if: startsWith(matrix.os, 'ubuntu')
# with: with:
# Disable since the Snapcraft token is currently not working snapcraft_token: ${{ secrets.snapcraft_token }}
# 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 - name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1.6.0 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", "target": "ES6",
"module": "commonjs", "module": "commonjs",
"allowSyntheticDefaultImports": true "allowSyntheticDefaultImports": true,
"jsx": "preserve"
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules"] "exclude": ["node_modules"]

View file

@ -1,6 +1,6 @@
{ {
"name": "yesplaymusic", "name": "yesplaymusic",
"version": "0.4.4", "version": "0.4.5",
"private": true, "private": true,
"description": "A third party music player for Netease Music", "description": "A third party music player for Netease Music",
"author": "qier222<qier222@outlook.com>", "author": "qier222<qier222@outlook.com>",
@ -23,12 +23,12 @@
}, },
"main": "background.js", "main": "background.js",
"dependencies": { "dependencies": {
"@unblockneteasemusic/server": "v0.27.0-rc.6", "@unblockneteasemusic/rust-napi": "^0.3.0-pre.1",
"NeteaseCloudMusicApi": "^4.5.2", "NeteaseCloudMusicApi": "^4.5.2",
"axios": "^0.21.0", "axios": "^0.26.1",
"change-case": "^4.1.2", "change-case": "^4.1.2",
"cli-color": "^2.0.0", "cli-color": "^2.0.0",
"color": "^3.1.3", "color": "^4.2.3",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"crypto-js": "^4.0.0", "crypto-js": "^4.0.0",
"dayjs": "^1.8.36", "dayjs": "^1.8.36",
@ -36,14 +36,14 @@
"discord-rich-presence": "^0.0.8", "discord-rich-presence": "^0.0.8",
"electron": "^13.6.7", "electron": "^13.6.7",
"electron-builder": "^23.0.0", "electron-builder": "^23.0.0",
"electron-context-menu": "^2.3.0", "electron-context-menu": "^3.1.2",
"electron-debug": "^3.1.0", "electron-debug": "^3.1.0",
"electron-devtools-installer": "^3.2", "electron-devtools-installer": "^3.2",
"electron-icon-builder": "^1.0.2", "electron-icon-builder": "^2.0.1",
"electron-is-dev": "^1.2.0", "electron-is-dev": "^2.0.0",
"electron-log": "^4.3.0", "electron-log": "^4.3.0",
"electron-store": "^6.0.1", "electron-store": "^8.0.1",
"electron-updater": "^4.3.5", "electron-updater": "^5.0.1",
"express": "^4.17.1", "express": "^4.17.1",
"express-fileupload": "^1.2.0", "express-fileupload": "^1.2.0",
"express-http-proxy": "^1.6.2", "express-http-proxy": "^1.6.2",
@ -62,12 +62,12 @@
"prettier": "2.5.1", "prettier": "2.5.1",
"qrcode": "^1.4.4", "qrcode": "^1.4.4",
"register-service-worker": "^1.7.1", "register-service-worker": "^1.7.1",
"svg-sprite-loader": "^5.0.0", "svg-sprite-loader": "^6.0.11",
"tunnel": "^0.0.6", "tunnel": "^0.0.6",
"vscode-codicons": "^0.0.17", "vscode-codicons": "^0.0.17",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-analytics": "^5.22.1",
"vue-clipboard2": "^0.3.1", "vue-clipboard2": "^0.3.1",
"vue-gtag": "1",
"vue-i18n": "^8.22.0", "vue-i18n": "^8.22.0",
"vue-router": "^3.4.3", "vue-router": "^3.4.3",
"vue-slider-component": "^3.2.5", "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 * @param {string} id - 音乐的 id例如 id=405998841,33894312
*/ */
export function getMP3(id) { export function getMP3(id) {
let br = const getBr = () => {
store.state.settings?.musicQuality !== undefined // 当返回的 quality >= 400000时就会优先返回 hi-res
? store.state.settings.musicQuality const quality = store.state.settings?.musicQuality ?? '320000';
: 320000; return quality === 'flac' ? '350000' : quality;
};
return request({ return request({
url: '/song/url', url: '/song/url',
method: 'get', method: 'get',
params: { params: {
id, id,
br, br: getBr(),
}, },
}); });
} }

View file

@ -7,6 +7,7 @@ import {
dialog, dialog,
globalShortcut, globalShortcut,
nativeTheme, nativeTheme,
screen,
} from 'electron'; } from 'electron';
import { import {
isWindows, isWindows,
@ -201,8 +202,42 @@ class Background {
}; };
if (this.store.get('window.x') && this.store.get('window.y')) { if (this.store.get('window.x') && this.store.get('window.y')) {
options.x = this.store.get('window.x'); let x = this.store.get('window.x');
options.y = this.store.get('window.y'); 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); this.window = new BrowserWindow(options);
@ -261,6 +296,7 @@ class Background {
this.window.once('ready-to-show', () => { this.window.once('ready-to-show', () => {
log('window ready-to-show event'); log('window ready-to-show event');
this.window.show(); this.window.show();
this.store.set('window', this.window.getBounds());
}); });
this.window.on('close', e => { this.window.on('close', e => {
@ -296,6 +332,14 @@ class Background {
this.store.set('window', this.window.getBounds()); 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) { this.window.webContents.on('new-window', function (e, url) {
e.preventDefault(); e.preventDefault();
log('open url'); log('open url');

View file

@ -2,11 +2,13 @@
<span class="artist-in-line"> <span class="artist-in-line">
{{ computedPrefix }} {{ computedPrefix }}
<span v-for="(ar, index) in filteredArtists" :key="index"> <span v-for="(ar, index) in filteredArtists" :key="index">
<router-link v-if="ar.id !== 0" :to="`/artist/${ar.id}`"> <router-link v-if="ar.id !== 0" :to="`/artist/${ar.id}`">{{
{{ ar.name }} ar.name
</router-link> }}</router-link>
<span v-else>{{ ar.name }}</span> <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>
</span> </span>
</template> </template>
@ -40,4 +42,12 @@ export default {
}; };
</script> </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); box-shadow: 0 6px 12px -4px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(0, 0, 0, 0.06); border: 1px solid rgba(0, 0, 0, 0.06);
backdrop-filter: blur(12px); backdrop-filter: blur(12px);
border-radius: 8px; border-radius: 12px;
box-sizing: border-box; box-sizing: border-box;
padding: 6px; padding: 6px;
z-index: 1000; z-index: 1000;
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
transition: background 125ms ease-out, opacity 125ms ease-out,
transform 125ms ease-out;
&:focus { &:focus {
outline: none; outline: none;
@ -94,8 +96,9 @@ export default {
[data-theme='dark'] { [data-theme='dark'] {
.menu { .menu {
background: rgba(36, 36, 36, 0.78); 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); border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 0 6px rgba(255, 255, 255, 0.08);
} }
.menu .item:hover { .menu .item:hover {
color: var(--color-text); color: var(--color-text);
@ -112,7 +115,7 @@ export default {
font-weight: 600; font-weight: 600;
font-size: 14px; font-size: 14px;
padding: 10px 14px; padding: 10px 14px;
border-radius: 7px; border-radius: 8px;
cursor: default; cursor: default;
color: var(--color-text); color: var(--color-text);
display: flex; display: flex;
@ -120,6 +123,11 @@ export default {
&:hover { &:hover {
color: var(--color-primary); color: var(--color-primary);
background: var(--color-primary-bg-for-transparent); 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 { .svg-icon {
@ -149,7 +157,7 @@ hr {
border-radius: 4px; border-radius: 4px;
} }
.info { .info {
margin-left: 8px; margin-left: 10px;
} }
.title { .title {
font-size: 16px; font-size: 16px;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,10 @@
<div class="track-list"> <div class="track-list">
<ContextMenu ref="menu"> <ContextMenu ref="menu">
<div v-show="type !== 'cloudDisk'" class="item-info"> <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="info">
<div class="title">{{ rightClickedTrackComputed.name }}</div> <div class="title">{{ rightClickedTrackComputed.name }}</div>
<div class="subtitle">{{ rightClickedTrackComputed.ar[0].name }}</div> <div class="subtitle">{{ rightClickedTrackComputed.ar[0].name }}</div>
@ -46,6 +49,9 @@
@click="addTrackToPlaylist" @click="addTrackToPlaylist"
>{{ $t('contextMenu.addToPlaylist') }}</div >{{ $t('contextMenu.addToPlaylist') }}</div
> >
<div v-show="type !== 'cloudDisk'" class="item" @click="copyLink">{{
$t('contextMenu.copyUrl')
}}</div>
<div <div
v-if="extraContextMenuItem.includes('removeTrackFromCloudDisk')" v-if="extraContextMenuItem.includes('removeTrackFromCloudDisk')"
class="item" 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() { removeTrackFromQueue() {
this.$store.state.player.removeTrackFromQueue( this.$store.state.player.removeTrackFromQueue(
this.rightClickedTrackIndex this.rightClickedTrackIndex

View file

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

View file

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

View file

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

View file

@ -178,6 +178,34 @@ export default {
exit: 'Exit', exit: 'Exit',
minimizeToTray: 'Minimize to tray', 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: { contextMenu: {
play: 'Play', play: 'Play',

View file

@ -172,6 +172,34 @@ export default {
exit: 'Exit', exit: 'Exit',
minimizeToTray: 'Küçült', 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: { contextMenu: {
play: 'Oynat', play: 'Oynat',

View file

@ -179,6 +179,33 @@ export default {
exit: '退出', exit: '退出',
minimizeToTray: '最小化到托盘', 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: { contextMenu: {
play: '播放', play: '播放',

View file

@ -176,6 +176,33 @@ export default {
exit: '退出', exit: '退出',
minimizeToTray: '最小化到工作列角落', 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: { contextMenu: {
play: '播放', play: '播放',

View file

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

View file

@ -22,6 +22,10 @@ export default {
artists: [], artists: [],
mvs: [], mvs: [],
cloudDisk: [], cloudDisk: [],
playHistory: {
weekData: [],
allData: [],
},
}, },
contextMenu: { contextMenu: {
clickObjectID: 0, 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 { getAlbum } from '@/api/album';
import { getPlaylistDetail, intelligencePlaylist } from '@/api/playlist';
import { getArtist } from '@/api/artist'; 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 store from '@/store';
import { isAccountLoggedIn } from '@/utils/auth'; import { isAccountLoggedIn } from '@/utils/auth';
import { trackUpdateNowPlaying, trackScrobble } from '@/api/lastfm'; import { cacheTrackSource, getTrackSource } from '@/utils/db';
import { isCreateMpris, isCreateTray } from '@/utils/platform'; 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; const PLAY_PAUSE_FADE_DURATION = 200;
@ -34,14 +35,14 @@ function setTitle(track) {
? `${track.name} · ${track.ar[0].name} - YesPlayMusic` ? `${track.name} · ${track.ar[0].name} - YesPlayMusic`
: 'YesPlayMusic'; : 'YesPlayMusic';
if (isCreateTray) { if (isCreateTray) {
ipcRenderer.send('updateTrayTooltip', document.title); ipcRenderer?.send('updateTrayTooltip', document.title);
} }
store.commit('updateTitle', document.title); store.commit('updateTitle', document.title);
} }
function setTrayLikeState(isLiked) { function setTrayLikeState(isLiked) {
if (isCreateTray) { if (isCreateTray) {
ipcRenderer.send('updateTrayLikeState', isLiked); ipcRenderer?.send('updateTrayLikeState', isLiked);
} }
} }
@ -69,7 +70,9 @@ export default class {
this._playNextList = []; // 当这个list不为空时会优先播放这个list的歌 this._playNextList = []; // 当这个list不为空时会优先播放这个list的歌
this._isPersonalFM = false; // 是否是私人FM模式 this._isPersonalFM = false; // 是否是私人FM模式
this._personalFMTrack = { id: 0 }; // 私人FM当前歌曲 this._personalFMTrack = { id: 0 }; // 私人FM当前歌曲
this._personalFMNextTrack = { id: 0 }; // 私人FM下一首歌曲信息为了快速加载下一首 this._personalFMNextTrack = {
id: 0,
}; // 私人FM下一首歌曲信息为了快速加载下一首
/** /**
* The blob records for cleanup. * The blob records for cleanup.
@ -192,8 +195,6 @@ export default class {
_init() { _init() {
this._loadSelfFromLocalStorage(); this._loadSelfFromLocalStorage();
Howler.autoUnlock = false;
Howler.usingWebAudio = true;
Howler.volume(this.volume); Howler.volume(this.volume);
if (this._enabled) { if (this._enabled) {
@ -222,18 +223,19 @@ export default class {
_setPlaying(isPlaying) { _setPlaying(isPlaying) {
this._playing = isPlaying; this._playing = isPlaying;
if (isCreateTray) { if (isCreateTray) {
ipcRenderer.send('updateTrayPlayState', this._playing); ipcRenderer?.send('updateTrayPlayState', this._playing);
} }
} }
_setIntervals() { _setIntervals() {
// 同步播放进度 // 同步播放进度
// TODO: 如果 _progress 在别的地方被改变了这个定时器会覆盖之前改变的值是bug // TODO: 如果 _progress 在别的地方被改变了,
// 这个定时器会覆盖之前改变的值是bug
setInterval(() => { setInterval(() => {
if (this._howler === null) return; if (this._howler === null) return;
this._progress = this._howler.seek(); this._progress = this._howler.seek();
localStorage.setItem('playerCurrentTrackTime', this._progress); localStorage.setItem('playerCurrentTrackTime', this._progress);
if (isCreateMpris) { if (isCreateMpris) {
ipcRenderer.send('playerCurrentTrackTime', this._progress); ipcRenderer?.send('playerCurrentTrackTime', this._progress);
} }
}, 1000); }, 1000);
} }
@ -313,6 +315,7 @@ export default class {
this._howler = new Howl({ this._howler = new Howl({
src: [source], src: [source],
html5: true, html5: true,
preload: true,
format: ['mp3', 'flac'], format: ['mp3', 'flac'],
onend: () => { onend: () => {
this._nextTrackCallback(); this._nextTrackCallback();
@ -327,25 +330,27 @@ export default class {
} }
this.setOutputDevice(); 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) { _getAudioSourceFromCache(id) {
return getTrackSource(id).then(t => { return getTrackSource(id).then(t => {
if (!t) return null; if (!t) return null;
return this._getAudioSourceBlobURL(t.source);
// 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;
}); });
} }
_getAudioSourceFromNetease(track) { _getAudioSourceFromNetease(track) {
@ -368,22 +373,72 @@ export default class {
} }
async _getAudioSourceFromUnblockMusic(track) { async _getAudioSourceFromUnblockMusic(track) {
console.debug(`[debug][Player.js] _getAudioSourceFromUnblockMusic`); console.debug(`[debug][Player.js] _getAudioSourceFromUnblockMusic`);
if ( if (
process.env.IS_ELECTRON !== true || process.env.IS_ELECTRON !== true ||
store.state.settings.enableUnblockNeteaseMusic === false store.state.settings.enableUnblockNeteaseMusic === false
) { ) {
return null; 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', 'unblock-music',
store.state.settings.unmSource,
track, 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字样换成真正的来源比如酷我咪咕等 if (store.state.settings.automaticallyCacheSongs && retrieveSongInfo?.url) {
cacheTrackSource(track, source.url, 128000, 'unblockMusic'); // 对于来自 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) { _getAudioSource(track) {
return this._getAudioSourceFromCache(String(track.id)) return this._getAudioSourceFromCache(String(track.id))
@ -489,6 +544,11 @@ export default class {
artist: artists.join(','), artist: artists.join(','),
album: track.al.name, album: track.al.name,
artwork: [ artwork: [
{
src: track.al.picUrl + '?param=224y224',
type: 'image/jpg',
sizes: '224x224',
},
{ {
src: track.al.picUrl + '?param=512y512', src: track.al.picUrl + '?param=512y512',
type: 'image/jpg', type: 'image/jpg',
@ -501,7 +561,7 @@ export default class {
navigator.mediaSession.metadata = new window.MediaMetadata(metadata); navigator.mediaSession.metadata = new window.MediaMetadata(metadata);
if (isCreateMpris) { if (isCreateMpris) {
ipcRenderer.send('metadata', metadata); ipcRenderer?.send('metadata', metadata);
} }
} }
_updateMediaSessionPositionState() { _updateMediaSessionPositionState() {
@ -557,7 +617,7 @@ export default class {
} }
let copyTrack = { ...track }; let copyTrack = { ...track };
copyTrack.dt -= seekTime * 1000; copyTrack.dt -= seekTime * 1000;
ipcRenderer.send('playDiscordPresence', copyTrack); ipcRenderer?.send('playDiscordPresence', copyTrack);
} }
_pauseDiscordPresence(track) { _pauseDiscordPresence(track) {
if ( if (
@ -566,7 +626,7 @@ export default class {
) { ) {
return null; return null;
} }
ipcRenderer.send('pauseDiscordPresence', track); ipcRenderer?.send('pauseDiscordPresence', track);
} }
currentTrackID() { currentTrackID() {
@ -807,7 +867,7 @@ export default class {
sendSelfToIpcMain() { sendSelfToIpcMain() {
if (process.env.IS_ELECTRON !== true) return false; if (process.env.IS_ELECTRON !== true) return false;
let liked = store.state.liked.songs.includes(this.currentTrack.id); let liked = store.state.liked.songs.includes(this.currentTrack.id);
ipcRenderer.send('player', { ipcRenderer?.send('player', {
playing: this.playing, playing: this.playing,
likedCurrentTrack: liked, likedCurrentTrack: liked,
}); });
@ -823,13 +883,13 @@ export default class {
this.repeatMode = 'on'; this.repeatMode = 'on';
} }
if (isCreateMpris) { if (isCreateMpris) {
ipcRenderer.send('switchRepeatMode', this.repeatMode); ipcRenderer?.send('switchRepeatMode', this.repeatMode);
} }
} }
switchShuffle() { switchShuffle() {
this.shuffle = !this.shuffle; this.shuffle = !this.shuffle;
if (isCreateMpris) { if (isCreateMpris) {
ipcRenderer.send('switchShuffle', this.shuffle); ipcRenderer?.send('switchShuffle', this.shuffle);
} }
} }
switchReversed() { 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 isLinux = process.platform === 'linux';
export const isDevelopment = process.env.NODE_ENV === 'development'; export const isDevelopment = process.env.NODE_ENV === 'development';
export const isCreateTray = export const isCreateTray = isWindows || isLinux || isDevelopment;
process.env.IS_ELECTRON && (isWindows || isLinux || isDevelopment);
export const isCreateMpris = isLinux; 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 router from '@/router';
import { doLogout, getCookie } from '@/utils/auth';
import axios from 'axios';
let baseURL = ''; let baseURL = '';
// Web 和 Electron 跑在不同端口避免同时启动时冲突 // Web 和 Electron 跑在不同端口避免同时启动时冲突
@ -34,6 +34,10 @@ service.interceptors.request.use(function (config) {
config.params.realIP = '211.161.244.70'; 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; const proxy = JSON.parse(localStorage.getItem('settings')).proxyConfig;
if (['HTTP', 'HTTPS'].includes(proxy.protocol)) { if (['HTTP', 'HTTPS'].includes(proxy.protocol)) {
config.params.proxy = `${proxy.protocol}://${proxy.server}:${proxy.port}`; config.params.proxy = `${proxy.protocol}://${proxy.server}:${proxy.port}`;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -139,9 +139,12 @@
<div v-if="isLikeSongsPage" class="user-info"> <div v-if="isLikeSongsPage" class="user-info">
<h1> <h1>
<img class="avatar" :src="data.user.avatarUrl | resizeImage" />{{ <img
data.user.nickname class="avatar"
}}{{ $t('library.sLikedSongs') }} :src="data.user.avatarUrl | resizeImage"
loading="lazy"
/>
{{ data.user.nickname }}{{ $t('library.sLikedSongs') }}
</h1> </h1>
<div class="search-box-likepage" @click="searchInPlaylist()"> <div class="search-box-likepage" @click="searchInPlaylist()">
<div class="container" :class="{ active: inputFocus }"> <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]', symbolId: 'icon-[name]',
}) })
.end(); .end();
config.module
.rule('napi')
.test(/\.node$/)
.use('node-loader')
.loader('node-loader')
.end();
// LimitChunkCountPlugin 可以通过合并块来对块进行后期处理。用以解决 chunk 包太多的问题 // LimitChunkCountPlugin 可以通过合并块来对块进行后期处理。用以解决 chunk 包太多的问题
config.plugin('chunkPlugin').use(webpack.optimize.LimitChunkCountPlugin, [ config.plugin('chunkPlugin').use(webpack.optimize.LimitChunkCountPlugin, [
{ {
@ -69,10 +76,7 @@ module.exports = {
// electron-builder的配置文件 // electron-builder的配置文件
electronBuilder: { electronBuilder: {
nodeIntegration: true, nodeIntegration: true,
externals: [ externals: ['@unblockneteasemusic/rust-napi'],
'@unblockneteasemusic/server',
'@unblockneteasemusic/server/src/consts',
],
builderOptions: { builderOptions: {
productName: 'YesPlayMusic', productName: 'YesPlayMusic',
copyright: 'Copyright © YesPlayMusic', copyright: 'Copyright © YesPlayMusic',

1747
yarn.lock

File diff suppressed because it is too large Load diff