feat. OSD Lyrics | 增加桌面歌词功能 (#685)

* Add OSD Lyrics

* tidy files

* fix OSDLyrics: last line of lyrics not showing, performance

* tidy files

* make user can resize the lyrics window

* Fix bug of initial window size

* Fix: 1. auto resize osdlyrics window after packaging; 2. lyric parser problem with %;

* tidy files
This commit is contained in:
Shi Liang 2021-05-21 17:15:30 +08:00 committed by GitHub
parent d464e30d83
commit 85592142af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 405 additions and 5 deletions

239
public/osdlyrics.html Normal file
View File

@ -0,0 +1,239 @@
<!DOCTYPE html>
<html lang="en">
<html>
<head>
<style>
body {
font-family: Barlow, -apple-system, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, WenQuanYi Micro Hei, sans-serif;
font-weight: 600;
text-align: center;
font-size: 28px;
user-select: none;
margin: 2px;
padding: 0px;
-webkit-user-select: none;
-webkit-app-region: drag;
}
div {
display: block;
padding: 9px;
border-radius: 6px;
background: rgba(128, 128, 128, 0.800);
}
</style>
<title>OSD Lyrics</title>
<script>
const electron = process && process.versions && process.versions.hasOwnProperty('electron') ? window.require('electron') : null;
const ipcRenderer = electron ? electron.ipcRenderer : null;
const player_api = window.location.origin + "/player";
const lyrics_api = window.location.origin + "/api/lyric?id=";
const min_update_interval = 50;
const max_update_interval = 500;
song_id = null;
lyrics = null;
current_line = null;
current_index = 0;
next_time = 0;
function lyric_parser(lrc) {
if (!lrc) {
return null;
}
lines = lrc
.split('\n')
.map(nx => {
//nx = decodeURIComponent(x);
text = nx.split(']');
lrc = text[1];
if (!lrc) {
return null;
}
_time = text[0].split('[')[1];
if (!_time) {
return null;
}
time = _time.split(':')
min = Number(time[0]);
sec = Number(time[1]);
if (lrc.length < 1) {
return [min * 60 + sec, " "]
} else {
return [min * 60 + sec, lrc];
}
})
.filter(x => Boolean(x));
if (lines.length > 0) {
return lines;
} else {
return null;
}
}
function update_window_size() {
if (!ipcRenderer) {
return;
}
offsetHeight = document.getElementById('line1').offsetHeight + 4;
if (offsetHeight == window.innerHeight) {
return;
}
ipcRenderer.send('resizeOSDLyrics', offsetHeight);
}
function search_line(progress) {
for (i = 1; i < lyrics.length; i++) {
if (progress < lyrics[i][0]) {
current_index = i - 1;
next_time = lyrics[i][0];
return;
}
}
current_index = lyrics.length - 1;
return;
}
function translation_combine(lrc, tr) {
if (!lrc) {
lyrics = null;
return;
}
if (!tr) {
lyrics = lrc.map(x => [x[0], x[1], null]);
return;
}
lyrics = new Array(lrc.length);
lrc_length = lrc.length;
t_length = tr.length;
for (i = 0, j = 0; i < lrc_length;) {
if (j >= t_length) {
lyrics[i] = [lrc[i][0], lrc[i][1], null];
i++;
} else {
lrc_time = lrc[i][0];
tr_time = tr[j][0];
if (lrc_time == tr_time) {
lyrics[i] = [lrc_time, lrc[i][1], tr[j][1]];
i++; j++;
} else if (lrc_time < tr_time) {
lyrics[i] = [lrc_time, lrc[i][1], null];
i++;
} else if (lrc_time > tr_time) {
while (lrc_time > tr[j][0] && j < t_length) {
j++;
}
} else {
j++;
}
}
}
}
function update_display() {
document.getElementById("line1").innerHTML = current_line;
update_window_size();
}
function update_line(progress) {
next_update = max_update_interval;
if (!lyrics) {
current_line = "纯音乐,请欣赏";
} else {
lyrics_length = lyrics.length;
if (progress > lyrics[current_index][0]) {
next_index = current_index + 1;
if (next_index < lyrics_length) {
if (progress < lyrics[next_index][0]) {
next_update = (next_time - progress) * 1000;
} else {
next_next_index = next_index + 1;
if (next_next_index < lyrics_length) {
if (progress < lyrics[next_next_index][0]) {
current_index = next_index;
current = lyrics[next_index];
current_line = current[2] ? (current[1] + "<br>" + current[2]) : current[1];
next_time = lyrics[next_next_index][0];
next_update = (next_time - progress) * 1000;
} else {
search_line(progress);
current = lyrics[current_index];
current_line = current[2] ? (current[1] + "<br>" + current[2]) : current[1];
next_update = (next_time - progress) * 1000;
}
} else {
// next line is last line
current_index = next_index;
current = lyrics[next_index];
current_line = current[2] ? (current[1] + "<br>" + current[2]) : current[1];
next_update = max_update_interval;
}
}
} else {
// last line
next_update = max_update_interval;
}
} else {
search_line(progress);
current = lyrics[current_index];
current_line = current[2] ? (current[1] + "<br>" + current[2]) : current[1];
next_update = (next_time - progress) * 1000;
}
}
if (next_update > max_update_interval) {
interval = parseInt(next_update / max_update_interval);
next_update = next_update / interval;
} else if (next_update < min_update_interval) {
next_update = min_update_interval;
}
setTimeout(update_lyrics, next_update);
update_display();
}
function get_lyrics(_song_id) {
var url = lyrics_api + _song_id;
fetch(url)
.then(
(_lyric) => {
_lyric
.text()
.then(
(text) => {
result = JSON.parse(text);
if (!result.lrc) {
lyrics = null;
update_line(_progress);
return;
}
_lyrics = lyric_parser(result.lrc.lyric);
_translation = lyric_parser(result.tlyric.lyric);
translation_combine(_lyrics, _translation);
current_index = 0;
update_line(_progress);
})
})
}
function update_lyrics() {
fetch(player_api)
.then(
(player) => {
player
.text()
.then((text) => {
_status = JSON.parse(text);
_song_id = _status.currentTrack.id;
_progress = _status.progress;
if (_song_id !== song_id) {
song_id = _song_id;
get_lyrics(_song_id);
} else {
update_line(_progress);
}
})
});
}
function init_lyrics() {
update_window_size();
update_lyrics();
}
</script>
</head>
<body onload="init_lyrics()">
<div id="line1">YesPlayMusic</div>
</body>
</html>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" focusable="false" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px" viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<path fill="currentColor" d="M15,0C6.7,0,0,6.7,0,15s6.7,15,15,15s15-6.7,15-15S23.3,0,15,0z M11.9,10.6c0.4,0,0.7,0.1,1,0.1s0.6,0,0.9,0.1
c0.3,0,0.6,0,1,0c0.3,0,0.7,0,1.2,0c0.5,0,0.9,0,1.2,0c0.3,0,0.6,0,0.9,0c0.3,0,0.5,0,0.7,0c0.2,0,0.5-0.1,0.8-0.1v1.7l0,0l0,0l0,0
c-0.3,0-0.5,0-0.8-0.1s-0.5,0-0.7,0c-0.3,0-0.6,0-0.9,0c-0.3,0-0.8,0-1.2,0c-0.5,0-0.9,0-1.2,0c-0.3,0-0.7,0-1,0c-0.3,0-0.6,0-0.9,0
c-0.3,0-0.6,0-0.9,0.1L11.9,10.6L11.9,10.6z M9.1,6C9.4,6.4,9.7,6.9,10,7.3s0.6,1,0.9,1.5l-1.7,1l0,0C8.9,9.2,8.6,8.7,8.4,8.2
C8.1,7.7,7.8,7.3,7.5,6.9L9.1,6z M9.9,21.6c-0.1,0.1-0.3,0.3-0.4,0.4c-0.1,0.1-0.2,0.2-0.3,0.4c-0.1,0.1-0.2,0.3-0.3,0.4
c-0.1,0.1-0.2,0.3-0.3,0.5c-0.2-0.3-0.4-0.6-0.6-0.9c-0.2-0.3-0.4-0.5-0.6-0.7c0.3-0.3,0.6-0.7,0.7-1.2C8.3,20,8.3,19.5,8.3,19v-5.9
c-0.6,0-1.1,0-1.5,0c-0.3,0-0.6,0-0.9,0.1v-1.8c0.3,0.1,0.6,0.1,0.9,0.1c0.3,0,0.7,0,1.1,0c0.4,0,0.8,0,1.2,0c0.3,0,0.7-0.1,0.9-0.1
c-0.1,0.3-0.1,0.7-0.1,1.1c0,0.4,0,0.9,0,1.3v5.8c0.3-0.3,0.6-0.6,0.9-0.9c0.3-0.3,0.5-0.6,0.8-0.9c0.1,0.4,0.1,0.7,0.2,0.9
c0.1,0.2,0.2,0.4,0.4,0.6L9.9,21.6L9.9,21.6z M14.3,19.5v1.8h-1.7c0.1-0.4,0.2-0.8,0.2-1.3c0-0.5,0-0.9,0-1.4v-2.8
c0-0.4,0-0.8,0-1.2c0-0.4-0.1-0.7-0.1-0.9c0.2,0,0.3,0,0.5,0.1c0.2,0,0.4,0,0.6,0c0.2,0,0.5,0,0.8,0c0.3,0,0.7,0,1.1,0h1.1
c0.3,0,0.6,0,0.8,0c0.2,0,0.4,0,0.5,0c0.1,0,0.3,0,0.4-0.1c-0.1,0.2-0.1,0.5-0.1,0.9c0,0.4,0,0.8,0,1.2v2.5c0,0.1,0,0.3,0,0.5
c0,0.2,0,0.4,0,0.6c0,0.2,0,0.4,0.1,0.6c0,0.2,0,0.4,0.1,0.6h-1.7v-1L14.3,19.5L14.3,19.5z M22.6,7.5c0,0.1,0,0.3,0,0.5
c0,0.2,0,0.4,0,0.6v12.7c0,0.4,0,0.7-0.1,1c-0.1,0.3-0.2,0.5-0.4,0.6c-0.2,0.2-0.5,0.3-1,0.4c-0.4,0.1-1,0.2-1.8,0.3
c0-0.3-0.1-0.7-0.2-1.1c-0.1-0.4-0.4-0.8-0.7-1.1c0.5,0.1,1,0.1,1.3,0.1c0.3,0,0.6,0,0.7,0c0.2,0,0.3-0.1,0.3-0.2
c0.1-0.1,0.1-0.2,0.1-0.4V8.7h-4.1c-0.5,0-0.9,0-1.2,0c-0.4,0-0.7,0-1.1,0c-0.3,0-0.7,0-1,0.1c-0.3,0-0.6,0.1-1,0.1V7.2
c0.3,0,0.6,0,0.9,0.1c0.3,0,0.6,0,0.9,0c0.3,0,0.7,0,1.2,0H17c0.8,0,1.4,0,2,0c0.5,0,1,0,1.4,0c0.4,0,0.8,0,1.1,0
c0.3,0,0.7-0.1,1.1-0.1l0,0C22.6,7.3,22.6,7.4,22.6,7.5z"/>
<path fill="currentColor" d="M14.3,15h2.8v3.4h-2.8L14.3,15L14.3,15z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -25,6 +25,7 @@ import Store from 'electron-store';
class Background { class Background {
constructor() { constructor() {
this.window = null; this.window = null;
this.osdlyrics = null;
this.tray = null; this.tray = null;
this.store = new Store({ this.store = new Store({
windowWidth: { windowWidth: {
@ -152,6 +153,71 @@ class Background {
} }
} }
createOSDWindow() {
this.osdlyrics = new BrowserWindow({
x: this.store.get('osdlyrics.x-pos') || 0,
y: this.store.get('osdlyrics.y-pos') || 0,
width: this.store.get('osdlyrics.width') || 840,
height: this.store.get('osdlyrics.height') || 110,
title: 'OSD Lyrics',
transparent: true,
frame: false,
webPreferences: {
webSecurity: false,
nodeIntegration: true,
enableRemoteModule: true,
contextIsolation: false,
},
});
this.osdlyrics.setAlwaysOnTop(true, 'screen');
if (process.env.WEBPACK_DEV_SERVER_URL) {
// Load the url of the dev server if in development mode
this.osdlyrics.loadURL(
process.env.WEBPACK_DEV_SERVER_URL + '/osdlyrics.html'
);
if (!process.env.IS_TEST) this.osdlyrics.webContents.openDevTools();
} else {
this.osdlyrics.loadURL('http://localhost:27232/osdlyrics.html');
}
}
initOSDLyrics() {
const osdState = this.store.get('osdlyrics.show') || false;
if (osdState) {
this.showOSDLyrics();
}
}
toggleOSDLyrics() {
const osdState = this.store.get('osdlyrics.show') || false;
if (osdState) {
this.hideOSDLyrics();
} else {
this.showOSDLyrics();
}
}
showOSDLyrics() {
this.store.set('osdlyrics.show', true);
if (!this.osdlyrics) {
this.createOSDWindow();
this.handleOSDEvents();
}
}
hideOSDLyrics() {
this.store.set('osdlyrics.show', false);
if (this.osdlyrics) {
this.osdlyrics.close();
}
}
resizeOSDLyrics(height) {
const width = this.store.get('osdlyrics.width') || 840;
this.osdlyrics.setSize(width, height);
}
checkForUpdates() { checkForUpdates() {
autoUpdater.checkForUpdatesAndNotify(); autoUpdater.checkForUpdatesAndNotify();
@ -179,6 +245,30 @@ class Background {
}); });
} }
handleOSDEvents() {
this.osdlyrics.once('ready-to-show', () => {
console.log('OSD ready-to-show event');
this.osdlyrics.show();
});
this.osdlyrics.on('closed', e => {
console.log('OSD close event');
this.osdlyrics = null;
});
this.osdlyrics.on('resized', () => {
let { height, width } = this.osdlyrics.getBounds();
this.store.set('osdlyrics.width', width);
this.store.set('osdlyrics.height', height);
});
this.osdlyrics.on('moved', () => {
var pos = this.osdlyrics.getPosition();
this.store.set('osdlyrics.x-pos', pos[0]);
this.store.set('osdlyrics.y-pos', pos[1]);
});
}
handleWindowEvents() { handleWindowEvents() {
this.window.once('ready-to-show', () => { this.window.once('ready-to-show', () => {
console.log('windows ready-to-show event'); console.log('windows ready-to-show event');
@ -253,8 +343,17 @@ class Background {
this.createWindow(); this.createWindow();
this.handleWindowEvents(); this.handleWindowEvents();
this.initOSDLyrics();
// init ipcMain // init ipcMain
initIpcMain(this.window, this.store); initIpcMain(
this.window,
{
resizeOSDLyrics: height => this.resizeOSDLyrics(height),
toggleOSDLyrics: () => this.toggleOSDLyrics(),
},
this.store
);
// set proxy // set proxy
const proxyRules = this.store.get('proxy'); const proxyRules = this.store.get('proxy');
@ -268,7 +367,13 @@ class Background {
this.checkForUpdates(); this.checkForUpdates();
// create menu // create menu
createMenu(this.window); createMenu(this.window, {
openDevTools: () => {
if (this.osdlyrics) {
this.osdlyrics.webContents.openDevTools();
}
},
});
// create tray // create tray
if ( if (

View File

@ -95,6 +95,15 @@
<div class="right-control-buttons"> <div class="right-control-buttons">
<div class="blank"></div> <div class="blank"></div>
<div class="container" @click.stop> <div class="container" @click.stop>
<button-icon
:title="$t('player.osdlyrics')"
:class="{
active: osdState,
disabled: !osdState,
}"
@click.native="toggleOSDLyrics"
><svg-icon icon-class="osd-lyrics"
/></button-icon>
<button-icon <button-icon
:title="$t('player.nextUp')" :title="$t('player.nextUp')"
:class="{ :class="{
@ -168,6 +177,10 @@
</template> </template>
<script> <script>
const electron =
process.env.IS_ELECTRON === true ? window.require('electron') : null;
const ipcRenderer =
process.env.IS_ELECTRON === true ? electron.ipcRenderer : null;
import { mapState, mapMutations, mapActions } from 'vuex'; import { mapState, mapMutations, mapActions } from 'vuex';
import '@/assets/css/slider.css'; import '@/assets/css/slider.css';
@ -201,10 +214,18 @@ export default {
? '音源来自酷我音乐' ? '音源来自酷我音乐'
: ''; : '';
}, },
osdState() {
return true; //this.$store.osdlyrics.show || false;
},
}, },
methods: { methods: {
...mapMutations(['toggleLyrics']), ...mapMutations(['toggleLyrics']),
...mapActions(['showToast', 'likeATrack']), ...mapActions(['showToast', 'likeATrack']),
toggleOSDLyrics() {
if (ipcRenderer) {
ipcRenderer.send('toggleOSDLyrics');
}
},
goToNextTracksPage() { goToNextTracksPage() {
if (this.player.isPersonalFM) return; if (this.player.isPersonalFM) return;
this.$route.name === 'next' this.$route.name === 'next'

View File

@ -4,7 +4,7 @@ import { registerGlobalShortcut } from '@/electron/globalShortcut';
const client = require('discord-rich-presence')('818936529484906596'); const client = require('discord-rich-presence')('818936529484906596');
export function initIpcMain(win, store) { export function initIpcMain(win, lrc, store) {
ipcMain.on('unblock-music', (event, track) => { ipcMain.on('unblock-music', (event, track) => {
// 兼容 unblockneteasemusic 所使用的 api 字段 // 兼容 unblockneteasemusic 所使用的 api 字段
track.alias = track.alia || []; track.alias = track.alia || [];
@ -122,4 +122,12 @@ export function initIpcMain(win, store) {
win.webContents.session.setProxy({}); win.webContents.session.setProxy({});
store.set('proxy', ''); store.set('proxy', '');
}); });
ipcMain.on('resizeOSDLyrics', (event, arg) => {
lrc.resizeOSDLyrics(arg);
});
ipcMain.on('toggleOSDLyrics', () => {
lrc.toggleOSDLyrics();
});
} }

View File

@ -4,7 +4,7 @@ const { app, Menu } = require('electron');
const isMac = process.platform === 'darwin'; const isMac = process.platform === 'darwin';
export function createMenu(win) { export function createMenu(win, lrc) {
let menu = null; let menu = null;
const template = [ const template = [
...(isMac ...(isMac
@ -181,6 +181,7 @@ export function createMenu(win) {
accelerator: 'F12', accelerator: 'F12',
click: () => { click: () => {
win.webContents.openDevTools(); win.webContents.openDevTools();
lrc.openDevTools();
}, },
}, },
], ],

View File

@ -100,6 +100,7 @@ export default {
pause: 'Pause', pause: 'Pause',
mute: 'Mute', mute: 'Mute',
nextUp: 'Next Up', nextUp: 'Next Up',
osdLyrics: 'OSD Lyrics',
}, },
modal: { modal: {
close: 'Close', close: 'Close',

View File

@ -100,6 +100,7 @@ export default {
pause: 'Durdur', pause: 'Durdur',
mute: 'Sesi kapat', mute: 'Sesi kapat',
nextUp: 'Sıradaki', nextUp: 'Sıradaki',
osdLyrics: 'masaüstü şarkı sözleri',
}, },
modal: { modal: {
close: 'Kapat', close: 'Kapat',

View File

@ -101,6 +101,7 @@ export default {
pause: '暂停', pause: '暂停',
mute: '静音', mute: '静音',
nextUp: '播放列表', nextUp: '播放列表',
osdLyrics: '桌面歌词',
}, },
modal: { modal: {
close: '关闭', close: '关闭',

View File

@ -13,7 +13,7 @@ export function parseLyric(lrc) {
const lyrics = lrc.split('\n'); const lyrics = lrc.split('\n');
const lrcObj = []; const lrcObj = [];
for (let i = 0; i < lyrics.length; i++) { for (let i = 0; i < lyrics.length; i++) {
const lyric = decodeURIComponent(lyrics[i]); const lyric = lyrics[i];
const timeReg = /\[\d*:\d*((\.|:)\d*)*\]/g; const timeReg = /\[\d*:\d*((\.|:)\d*)*\]/g;
const timeRegExpArr = lyric.match(timeReg); const timeRegExpArr = lyric.match(timeReg);
if (!timeRegExpArr) continue; if (!timeRegExpArr) continue;