mirror of
https://github.com/qier222/YesPlayMusic.git
synced 2025-02-16 23:12:45 +08:00
deploy: web: 0.4.5 (#1615)
This commit is contained in:
commit
ff9c8a2d6b
41
.github/workflows/build.yaml
vendored
41
.github/workflows/build.yaml
vendored
|
@ -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
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
},
|
||||
"target": "ES6",
|
||||
"module": "commonjs",
|
||||
"allowSyntheticDefaultImports": true
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
|
|
22
package.json
22
package.json
|
@ -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
7
restyled.yml
Normal file
|
@ -0,0 +1,7 @@
|
|||
commit_template: 'style: with ${restyler.name}'
|
||||
restylers:
|
||||
- prettier
|
||||
- prettier-json
|
||||
- prettier-markdown
|
||||
- prettier-yaml
|
||||
- whitespace
|
|
@ -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(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -25,6 +25,8 @@ export default {
|
|||
this.svgStyle = {
|
||||
height: this.size + 'px',
|
||||
width: this.size + 'px',
|
||||
position: 'relative',
|
||||
left: '-1px',
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) => {
|
||||
// 当窗口最大化时,value为false
|
||||
// 当窗口还原时,value为true
|
||||
this.isShowMaximized = value;
|
||||
this.isMaximized = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -147,6 +147,7 @@ export default {
|
|||
label {
|
||||
font-size: 12px;
|
||||
}
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)"
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
// 当窗口最大化时,value为false
|
||||
// 当窗口还原时,value为true
|
||||
this.isShowMaximized = value;
|
||||
this.isMaximized = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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: '播放',
|
||||
|
|
|
@ -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: '播放',
|
||||
|
|
13
src/main.js
13
src/main.js
|
@ -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 });
|
||||
|
|
|
@ -22,6 +22,10 @@ export default {
|
|||
artists: [],
|
||||
mvs: [],
|
||||
cloudDisk: [],
|
||||
playHistory: {
|
||||
weekData: [],
|
||||
allData: [],
|
||||
},
|
||||
},
|
||||
contextMenu: {
|
||||
clickObjectID: 0,
|
||||
|
|
|
@ -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
67
src/utils/base64.js
Normal 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;
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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}`;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue
Block a user