mirror of
https://github.com/GiriNeko/YesPlayMusic.git
synced 2025-12-16 21:28:06 +00:00
Merge branch 'electron'
# Conflicts: # src/store/actions.js # src/store/mutations.js
This commit is contained in:
commit
7d32e8f3bf
291 changed files with 23897 additions and 161 deletions
18
src/App.vue
18
src/App.vue
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div id="app">
|
||||
<Navbar />
|
||||
<Navbar ref="navbar" />
|
||||
<main>
|
||||
<keep-alive>
|
||||
<router-view v-if="$route.meta.keepAlive"></router-view>
|
||||
|
|
@ -25,6 +25,7 @@
|
|||
import Navbar from "./components/Navbar.vue";
|
||||
import Player from "./components/Player.vue";
|
||||
import GlobalEvents from "vue-global-events";
|
||||
import { ipcRenderer } from "./electron/ipcRenderer";
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
|
|
@ -33,6 +34,16 @@ export default {
|
|||
Player,
|
||||
GlobalEvents,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isElectron: process.env.IS_ELECTRON, // true || undefined
|
||||
};
|
||||
},
|
||||
created() {
|
||||
if (this.isElectron) {
|
||||
ipcRenderer(this);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
play(e) {
|
||||
e.preventDefault();
|
||||
|
|
@ -121,6 +132,11 @@ a {
|
|||
}
|
||||
}
|
||||
|
||||
// for electron
|
||||
body.is-electron::-webkit-scrollbar {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
/* Let's get this party started */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
|
|
|
|||
153
src/background.js
Normal file
153
src/background.js
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
"use strict";
|
||||
import { app, protocol, BrowserWindow, globalShortcut } from "electron";
|
||||
import { createProtocol } from "vue-cli-plugin-electron-builder/lib";
|
||||
import installExtension, { VUEJS_DEVTOOLS } from "electron-devtools-installer";
|
||||
import { startNeteaseMusicApi } from "./electron/services";
|
||||
import { initIpcMain } from "./electron/ipcMain.js";
|
||||
import { createMenu } from "./electron/menu";
|
||||
import { createTouchBar } from "./electron/touchBar";
|
||||
import { createDockMenu } from "./electron/dockMenu";
|
||||
import { createTray } from "./electron/tray.js";
|
||||
// import { autoUpdater } from "electron-updater"
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV !== "production";
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
let win;
|
||||
|
||||
// ipcMain
|
||||
initIpcMain(win);
|
||||
|
||||
// Scheme must be registered before the app is ready
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{ scheme: "app", privileges: { secure: true, standard: true } },
|
||||
]);
|
||||
|
||||
function createWindow() {
|
||||
win = new BrowserWindow({
|
||||
width: 1440,
|
||||
height: 840,
|
||||
titleBarStyle: "hiddenInset",
|
||||
webPreferences: {
|
||||
webSecurity: false,
|
||||
nodeIntegration: true,
|
||||
},
|
||||
});
|
||||
win.setMenuBarVisibility(false);
|
||||
|
||||
if (process.platform !== "darwin") {
|
||||
createTray(win);
|
||||
}
|
||||
|
||||
if (process.env.WEBPACK_DEV_SERVER_URL) {
|
||||
// Load the url of the dev server if in development mode
|
||||
win.loadURL(process.env.WEBPACK_DEV_SERVER_URL);
|
||||
if (!process.env.IS_TEST) win.webContents.openDevTools();
|
||||
} else {
|
||||
createProtocol("app");
|
||||
// Load the index.html when not in development
|
||||
win.loadURL("app://./index.html");
|
||||
}
|
||||
|
||||
win.on("closed", () => {
|
||||
win = null;
|
||||
});
|
||||
return win;
|
||||
}
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on("window-all-closed", () => {
|
||||
// On macOS it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (win === null) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.on("ready", async () => {
|
||||
// start netease music api
|
||||
startNeteaseMusicApi();
|
||||
|
||||
// Install Vue Devtools xtension
|
||||
if (isDevelopment && !process.env.IS_TEST) {
|
||||
try {
|
||||
await installExtension(VUEJS_DEVTOOLS);
|
||||
} catch (e) {
|
||||
console.error("Vue Devtools failed to install:", e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Register shortcut for debug
|
||||
globalShortcut.register("CommandOrControl+K", function () {
|
||||
win.webContents.openDevTools();
|
||||
});
|
||||
|
||||
// create window
|
||||
createWindow();
|
||||
win.once("ready-to-show", () => {
|
||||
win.show();
|
||||
});
|
||||
|
||||
// create menu
|
||||
createMenu(win);
|
||||
|
||||
// create dock menu for macOS
|
||||
app.dock.setMenu(createDockMenu(win));
|
||||
|
||||
// create touchbar
|
||||
win.setTouchBar(createTouchBar(win));
|
||||
|
||||
// autoUpdater.checkForUpdatesAndNotify()
|
||||
});
|
||||
|
||||
// autoUpdater.on("checking-for-update", () => {});
|
||||
// autoUpdater.on("update-available", info => {
|
||||
// console.log(info);
|
||||
// dialog.showMessageBox({
|
||||
// title: "新版本发布",
|
||||
// message: "有新内容更新,稍后将重新为您安装",
|
||||
// buttons: ["确定"],
|
||||
// type: "info",
|
||||
// noLink: true
|
||||
// });
|
||||
// });
|
||||
|
||||
// autoUpdater.on("update-downloaded", info => {
|
||||
// console.log(info);
|
||||
// autoUpdater.quitAndInstall();
|
||||
// });
|
||||
|
||||
// Exit cleanly on request from parent process in development mode.
|
||||
if (isDevelopment) {
|
||||
if (process.platform === "win32") {
|
||||
process.on("message", (data) => {
|
||||
if (data === "graceful-exit") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
process.on("SIGTERM", () => {
|
||||
app.quit();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the app is singleton.
|
||||
function initialize() {
|
||||
const shouldQuit = !app.requestSingleInstanceLock();
|
||||
if (shouldQuit) return app.quit();
|
||||
}
|
||||
|
||||
initialize();
|
||||
|
|
@ -35,6 +35,7 @@
|
|||
<svg-icon icon-class="search" />
|
||||
<div class="input">
|
||||
<input
|
||||
ref="searchInput"
|
||||
:placeholder="inputFocus ? '' : $t('nav.search')"
|
||||
v-model="keywords"
|
||||
@keydown.enter="goToSearchPage"
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
<div class="controls">
|
||||
<div class="playing">
|
||||
<img
|
||||
:src="currentTrack.al.picUrl | resizeImage(224)"
|
||||
:src="currentTrack.al && currentTrack.al.picUrl | resizeImage(224)"
|
||||
@click="goToAlbum"
|
||||
/>
|
||||
<div class="track-info">
|
||||
|
|
@ -141,9 +141,12 @@ export default {
|
|||
oldVolume: 0.5,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
mounted() {
|
||||
setInterval(() => {
|
||||
this.progress = ~~this.howler.seek();
|
||||
// fix 歌曲播放完还设置进度的问题,及 _id 不存在的问题
|
||||
if (this.howler && this.howler._sounds[0]._id) {
|
||||
this.progress = ~~this.howler.seek();
|
||||
}
|
||||
}, 1000);
|
||||
if (isAccountLoggedIn()) {
|
||||
userLikedSongsIDs(this.data.user.userId).then((data) => {
|
||||
|
|
@ -167,9 +170,12 @@ export default {
|
|||
},
|
||||
playing() {
|
||||
if (this.howler.state() === "loading") {
|
||||
this.updatePlayerState({ key: "playing", value: true });
|
||||
return true;
|
||||
}
|
||||
return this.howler.playing();
|
||||
const status = this.howler.playing();
|
||||
this.updatePlayerState({ key: "playing", value: status });
|
||||
return status;
|
||||
},
|
||||
progressMax() {
|
||||
let max = ~~(this.currentTrack.dt / 1000);
|
||||
|
|
|
|||
25
src/electron/dockMenu.js
Normal file
25
src/electron/dockMenu.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
const { Menu } = require("electron");
|
||||
|
||||
export function createDockMenu(win) {
|
||||
return Menu.buildFromTemplate([
|
||||
{
|
||||
label: "Play",
|
||||
click() {
|
||||
win.webContents.send("play");
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Next",
|
||||
click() {
|
||||
win.webContents.send("next");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Previous",
|
||||
click() {
|
||||
win.webContents.send("previous");
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
19
src/electron/ipcMain.js
Normal file
19
src/electron/ipcMain.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { app, ipcMain } from "electron";
|
||||
|
||||
export function initIpcMain(win) {
|
||||
// Make vuex copy for electron.
|
||||
global.vuexCopy = null;
|
||||
// 同步 vuex 状态,由于 player 有循环引用问题,拷贝部分属性
|
||||
ipcMain.on("vuex-state", (e, state) => {
|
||||
global.vuexCopy = state;
|
||||
});
|
||||
|
||||
ipcMain.on("close", () => {
|
||||
win.close();
|
||||
app.quit();
|
||||
});
|
||||
|
||||
ipcMain.on("minimize", () => {
|
||||
win.minimize();
|
||||
});
|
||||
}
|
||||
49
src/electron/ipcRenderer.js
Normal file
49
src/electron/ipcRenderer.js
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
export function ipcRenderer(vueInstance) {
|
||||
const self = vueInstance;
|
||||
// 添加专有的类名
|
||||
document.body.classList.add("is-electron");
|
||||
// ipc message channel
|
||||
const electron = window.require("electron");
|
||||
const ipcRenderer = electron.ipcRenderer;
|
||||
// listens to the main process 'changeRouteTo' event and changes the route from
|
||||
// inside this Vue instance, according to what path the main process requires.
|
||||
// responds to Menu click() events at the main process and changes the route accordingly.
|
||||
ipcRenderer.on("changeRouteTo", (event, path) => {
|
||||
self.$router.push(path);
|
||||
});
|
||||
ipcRenderer.on("search", () => {
|
||||
// 触发数据响应
|
||||
self.$refs.navbar.$refs.searchInput.focus();
|
||||
self.$refs.navbar.inputFocus = true;
|
||||
});
|
||||
ipcRenderer.on("play", () => {
|
||||
self.$refs.player.play();
|
||||
});
|
||||
ipcRenderer.on("next", () => {
|
||||
self.$refs.player.next();
|
||||
});
|
||||
ipcRenderer.on("previous", () => {
|
||||
self.$refs.player.previous();
|
||||
});
|
||||
ipcRenderer.on("increaseVolume", () => {
|
||||
if (self.$refs.player.volume + 0.1 >= 1) {
|
||||
return (self.$refs.player.volume = 1);
|
||||
}
|
||||
self.$refs.player.volume += 0.1;
|
||||
});
|
||||
ipcRenderer.on("decreaseVolume", () => {
|
||||
if (self.$refs.player.volume - 0.1 <= 0) {
|
||||
return (self.$refs.player.volume = 0);
|
||||
}
|
||||
self.$refs.player.volume -= 0.1;
|
||||
});
|
||||
ipcRenderer.on("like", () => {
|
||||
self.$refs.player.likeCurrentSong();
|
||||
});
|
||||
ipcRenderer.on("repeat", () => {
|
||||
self.$refs.player.repeat();
|
||||
});
|
||||
ipcRenderer.on("shuffle", () => {
|
||||
self.$refs.player.shuffle();
|
||||
});
|
||||
}
|
||||
205
src/electron/menu.js
Normal file
205
src/electron/menu.js
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
const { app, Menu } = require("electron");
|
||||
// import { autoUpdater } from "electron-updater"
|
||||
// const version = app.getVersion();
|
||||
|
||||
const isMac = process.platform === "darwin";
|
||||
|
||||
export function createMenu(win) {
|
||||
let menu = null;
|
||||
const template = [
|
||||
...(isMac
|
||||
? [
|
||||
{
|
||||
label: app.name,
|
||||
submenu: [
|
||||
{ role: "about" },
|
||||
{ type: "separator" },
|
||||
{ role: "services" },
|
||||
{ type: "separator" },
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Preferences...",
|
||||
accelerator: (() => (isMac ? "CmdOrCtrl+," : "Ctrl+,"))(),
|
||||
click: () => {
|
||||
win.webContents.send("changeRouteTo", "/settings");
|
||||
},
|
||||
role: "preferences",
|
||||
},
|
||||
{ type: "separator" },
|
||||
{ role: "hide" },
|
||||
{ role: "hideothers" },
|
||||
{ role: "unhide" },
|
||||
{ type: "separator" },
|
||||
{ role: "quit" },
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: "Edit",
|
||||
submenu: [
|
||||
{ role: "undo" },
|
||||
{ role: "redo" },
|
||||
{ type: "separator" },
|
||||
{ role: "cut" },
|
||||
{ role: "copy" },
|
||||
{ role: "paste" },
|
||||
...(isMac
|
||||
? [
|
||||
{ role: "delete" },
|
||||
{ role: "selectAll" },
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Speech",
|
||||
submenu: [{ role: "startspeaking" }, { role: "stopspeaking" }],
|
||||
},
|
||||
]
|
||||
: [{ role: "delete" }, { type: "separator" }, { role: "selectAll" }]),
|
||||
{
|
||||
label: "Search",
|
||||
accelerator: "CmdOrCtrl+F",
|
||||
click: () => {
|
||||
win.webContents.send("search");
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Controls",
|
||||
submenu: [
|
||||
{
|
||||
label: "Play",
|
||||
accelerator: "Space",
|
||||
click: () => {
|
||||
win.webContents.send("play");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Next",
|
||||
accelerator: "CmdOrCtrl+Right",
|
||||
click: () => {
|
||||
win.webContents.send("next");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Previous",
|
||||
accelerator: "CmdOrCtrl+Left",
|
||||
click: () => {
|
||||
win.webContents.send("previous");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Increase Volume",
|
||||
accelerator: "CmdOrCtrl+Up",
|
||||
click: () => {
|
||||
win.webContents.send("increaseVolume");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Decrease Volume",
|
||||
accelerator: "CmdOrCtrl+Down",
|
||||
click: () => {
|
||||
win.webContents.send("decreaseVolume");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Like",
|
||||
accelerator: "CmdOrCtrl+L",
|
||||
click: () => {
|
||||
win.webContents.send("like");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Repeat",
|
||||
accelerator: "Alt+R",
|
||||
click: () => {
|
||||
win.webContents.send("repeat");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Shuffle",
|
||||
accelerator: "Alt+S",
|
||||
click: () => {
|
||||
win.webContents.send("shuffle");
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Window",
|
||||
submenu: [
|
||||
{ role: "minimize" },
|
||||
{ role: "zoom" },
|
||||
{ role: "reload" },
|
||||
{ role: "forcereload" },
|
||||
{ role: "toggledevtools" },
|
||||
{ type: "separator" },
|
||||
{ role: "togglefullscreen" },
|
||||
...(isMac
|
||||
? [
|
||||
{ type: "separator" },
|
||||
{ role: "front" },
|
||||
{ type: "separator" },
|
||||
{
|
||||
role: "window",
|
||||
id: "window",
|
||||
label: "YesPlayMusic",
|
||||
type: "checkbox",
|
||||
checked: true,
|
||||
click: () => {
|
||||
const current = menu.getMenuItemById("window");
|
||||
if (current.checked === false) {
|
||||
win.hide();
|
||||
} else {
|
||||
win.show();
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
: [{ role: "close" }]),
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Help",
|
||||
submenu: [
|
||||
{
|
||||
label: "Github",
|
||||
click: async () => {
|
||||
const { shell } = require("electron");
|
||||
await shell.openExternal("https://github.com/qier222/YesPlayMusic");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Electron",
|
||||
click: async () => {
|
||||
const { shell } = require("electron");
|
||||
await shell.openExternal("https://electronjs.org");
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
// for window
|
||||
// if (process.platform === "win32") {
|
||||
// template.push({
|
||||
// label: "Help",
|
||||
// submenu: [
|
||||
// {
|
||||
// label: `Current version v${version}`,
|
||||
// enabled: false,
|
||||
// },
|
||||
// {
|
||||
// label: "Check for update",
|
||||
// accelerator: "Ctrl+U",
|
||||
// click: (item, focusedWindow) => {
|
||||
// win = focusedWindow;
|
||||
// updateSource = "menu";
|
||||
// autoUpdater.checkForUpdates();
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// });
|
||||
// }
|
||||
menu = Menu.buildFromTemplate(template);
|
||||
Menu.setApplicationMenu(menu);
|
||||
}
|
||||
58
src/electron/services.js
Normal file
58
src/electron/services.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
const express = require("express");
|
||||
const bodyParser = require("body-parser");
|
||||
const cache = require("../../netease_api/util/apicache").middleware;
|
||||
const fileUpload = require("express-fileupload");
|
||||
import routes from "../../netease_api/routes";
|
||||
|
||||
export function startNeteaseMusicApi() {
|
||||
// Integrate API
|
||||
const app = express();
|
||||
|
||||
// CORS & Preflight request
|
||||
app.use((req, res, next) => {
|
||||
if (req.path !== "/" && !req.path.includes(".")) {
|
||||
res.set({
|
||||
"Access-Control-Allow-Credentials": true,
|
||||
"Access-Control-Allow-Origin": req.headers.origin || "*",
|
||||
"Access-Control-Allow-Headers": "X-Requested-With,Content-Type",
|
||||
"Access-Control-Allow-Methods": "PUT,POST,GET,DELETE,OPTIONS",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
});
|
||||
}
|
||||
req.method === "OPTIONS" ? res.status(204).end() : next();
|
||||
});
|
||||
|
||||
// cookie parser
|
||||
app.use((req, res, next) => {
|
||||
req.cookies = {};
|
||||
(req.headers.cookie || "").split(/\s*;\s*/).forEach((pair) => {
|
||||
let crack = pair.indexOf("=");
|
||||
if (crack < 1 || crack == pair.length - 1) return;
|
||||
req.cookies[
|
||||
decodeURIComponent(pair.slice(0, crack)).trim()
|
||||
] = decodeURIComponent(pair.slice(crack + 1)).trim();
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
// body parser
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: false }));
|
||||
|
||||
app.use(fileUpload());
|
||||
|
||||
// cache
|
||||
app.use(cache("2 minutes", (req, res) => res.statusCode === 200));
|
||||
// router
|
||||
|
||||
Object.keys(routes).forEach((route) => {
|
||||
app.use(route, routes[route]);
|
||||
});
|
||||
|
||||
const port = process.env.PORT || 10754;
|
||||
const host = process.env.HOST || "127.0.0.1";
|
||||
|
||||
app.server = app.listen(port, host, () => {
|
||||
console.log(`server running @ http://${host ? host : "localhost"}:${port}`);
|
||||
});
|
||||
}
|
||||
131
src/electron/touchbar.js
Normal file
131
src/electron/touchbar.js
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
const { TouchBar, nativeImage, ipcMain } = require("electron");
|
||||
const path = require("path");
|
||||
|
||||
const {
|
||||
TouchBarButton,
|
||||
TouchBarGroup,
|
||||
TouchBarSpacer,
|
||||
TouchBarSegmentedControl,
|
||||
} = TouchBar;
|
||||
|
||||
function getNativeIcon(name, width = 24, height = 24) {
|
||||
return nativeImage
|
||||
.createFromPath(path.join(__static, "img/icons/touchbar/", name))
|
||||
.resize({
|
||||
width,
|
||||
height,
|
||||
});
|
||||
}
|
||||
|
||||
export function createSegmentedControl(renderer) {
|
||||
const segments = [
|
||||
{
|
||||
icon: getNativeIcon("previous.png"),
|
||||
},
|
||||
{
|
||||
icon: getNativeIcon("play.png", 20, 20),
|
||||
},
|
||||
{
|
||||
icon: getNativeIcon("next.png"),
|
||||
},
|
||||
];
|
||||
const segmentedControl = new TouchBarSegmentedControl({
|
||||
segments,
|
||||
change: (selectedIndex) => {
|
||||
const temp = Object.assign([], segmentedControl.segments);
|
||||
if (selectedIndex === 0) {
|
||||
renderer.send("previous");
|
||||
}
|
||||
if (selectedIndex === 1) {
|
||||
ipcMain.on("vuex-state", (e, { player }) => {
|
||||
const playing = player.playing;
|
||||
if (playing === true) {
|
||||
// To be paused
|
||||
temp[1].icon = getNativeIcon("play.png", 20, 20);
|
||||
segmentedControl.segments = temp;
|
||||
} else {
|
||||
temp[1].icon = getNativeIcon("play.png", 20, 20);
|
||||
segmentedControl.segments = temp;
|
||||
}
|
||||
});
|
||||
renderer.send("play");
|
||||
}
|
||||
if (selectedIndex === 2) {
|
||||
renderer.send("next");
|
||||
}
|
||||
},
|
||||
mode: "buttons",
|
||||
});
|
||||
return segmentedControl;
|
||||
}
|
||||
|
||||
export function createPreferGroup(renderer) {
|
||||
const search = new TouchBarButton({
|
||||
icon: getNativeIcon("search.png", 22, 22),
|
||||
click: () => {
|
||||
renderer.send("search");
|
||||
},
|
||||
});
|
||||
const like = new TouchBarButton({
|
||||
icon: getNativeIcon("like.png"),
|
||||
click: () => {
|
||||
ipcMain.on("vuex-state", (e, { liked, player }) => {
|
||||
const currentTrack = player.currentTrack;
|
||||
if (liked.songs.includes(currentTrack.id)) {
|
||||
like.icon = getNativeIcon("liked.png");
|
||||
} else {
|
||||
like.icon = getNativeIcon("like.png");
|
||||
}
|
||||
});
|
||||
renderer.send("like");
|
||||
},
|
||||
});
|
||||
const repeat = new TouchBarButton({
|
||||
icon: getNativeIcon("repeat.png"),
|
||||
click: () => {
|
||||
ipcMain.on("vuex-state", (e, { player }) => {
|
||||
const repeat = player.repeat;
|
||||
if (repeat === "on") {
|
||||
repeat.icon = getNativeIcon("repeat.png");
|
||||
} else if (repeat === "one") {
|
||||
repeat.icon = getNativeIcon("repeat.png");
|
||||
} else {
|
||||
repeat.icon = getNativeIcon("repeat.png");
|
||||
}
|
||||
});
|
||||
renderer.send("repeat");
|
||||
},
|
||||
});
|
||||
const shuffle = new TouchBarButton({
|
||||
icon: getNativeIcon("shuffle.png"),
|
||||
click: () => {
|
||||
ipcMain.on("vuex-state", (e, { player }) => {
|
||||
const shuffle = player.shuffle;
|
||||
if (shuffle === true) {
|
||||
shuffle.icon = getNativeIcon("shuffle.png");
|
||||
} else {
|
||||
shuffle.icon = getNativeIcon("shuffle.png");
|
||||
}
|
||||
});
|
||||
renderer.send("shuffle");
|
||||
},
|
||||
});
|
||||
return new TouchBar({
|
||||
items: [search, like, repeat, shuffle],
|
||||
});
|
||||
}
|
||||
|
||||
export function createTouchBar(window) {
|
||||
const renderer = window.webContents;
|
||||
const segmentedControl = createSegmentedControl(renderer);
|
||||
const preferGroup = createPreferGroup(renderer);
|
||||
const touchBar = new TouchBar({
|
||||
items: [
|
||||
new TouchBarGroup({ items: preferGroup }),
|
||||
new TouchBarSpacer({ size: "large" }),
|
||||
segmentedControl,
|
||||
new TouchBarSpacer({ size: "large" }),
|
||||
],
|
||||
});
|
||||
return touchBar;
|
||||
}
|
||||
104
src/electron/touchbar1.js
Normal file
104
src/electron/touchbar1.js
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
// 运用 ipdMain 请求用户喜欢的歌手的数据,随机抽几个歌手进行随机
|
||||
const { TouchBar } = require("electron");
|
||||
|
||||
const { TouchBarLabel, TouchBarButton, TouchBarSpacer } = TouchBar;
|
||||
|
||||
let spinning = false;
|
||||
|
||||
// Reel labels
|
||||
const reel1 = new TouchBarLabel();
|
||||
const reel2 = new TouchBarLabel();
|
||||
const reel3 = new TouchBarLabel();
|
||||
|
||||
// Spin result label
|
||||
const result = new TouchBarLabel();
|
||||
|
||||
// Spin button
|
||||
const spin = new TouchBarButton({
|
||||
label: "🎰 Spin",
|
||||
backgroundColor: "#7851A9",
|
||||
click: () => {
|
||||
// Ignore clicks if already spinning
|
||||
if (spinning) {
|
||||
return;
|
||||
}
|
||||
|
||||
spinning = true;
|
||||
result.label = "";
|
||||
|
||||
let timeout = 10;
|
||||
const spinLength = 4 * 1000; // 4 seconds
|
||||
const startTime = Date.now();
|
||||
|
||||
const spinReels = () => {
|
||||
updateReels();
|
||||
|
||||
if (Date.now() - startTime >= spinLength) {
|
||||
finishSpin();
|
||||
} else {
|
||||
// Slow down a bit on each spin
|
||||
timeout *= 1.1;
|
||||
setTimeout(spinReels, timeout);
|
||||
}
|
||||
};
|
||||
|
||||
spinReels();
|
||||
},
|
||||
});
|
||||
|
||||
const getRandomValue = () => {
|
||||
const values = ["🍒", "💎", "7️⃣", "🍊", "🔔", "⭐", "🍇", "🍀"];
|
||||
return values[Math.floor(Math.random() * values.length)];
|
||||
};
|
||||
|
||||
const updateReels = () => {
|
||||
reel1.label = getRandomValue();
|
||||
reel2.label = getRandomValue();
|
||||
reel3.label = getRandomValue();
|
||||
};
|
||||
|
||||
const finishSpin = () => {
|
||||
const uniqueValues = new Set([reel1.label, reel2.label, reel3.label]).size;
|
||||
if (uniqueValues === 1) {
|
||||
// All 3 values are the same
|
||||
result.label = "💰 Jackpot!";
|
||||
result.textColor = "#FDFF00";
|
||||
} else if (uniqueValues === 2) {
|
||||
// 2 values are the same
|
||||
result.label = "😍 Winner!";
|
||||
result.textColor = "#FDFF00";
|
||||
} else {
|
||||
// No values are the same
|
||||
result.label = "🙁 Spin Again";
|
||||
result.textColor = null;
|
||||
}
|
||||
spinning = false;
|
||||
};
|
||||
|
||||
const touchBar = new TouchBar({
|
||||
items: [
|
||||
spin,
|
||||
new TouchBarSpacer({ size: "large" }),
|
||||
reel1,
|
||||
new TouchBarSpacer({ size: "small" }),
|
||||
reel2,
|
||||
new TouchBarSpacer({ size: "small" }),
|
||||
reel3,
|
||||
new TouchBarSpacer({ size: "large" }),
|
||||
result,
|
||||
],
|
||||
});
|
||||
|
||||
// let window
|
||||
|
||||
// app.whenReady().then(() => {
|
||||
// window = new BrowserWindow({
|
||||
// frame: false,
|
||||
// titleBarStyle: 'hiddenInset',
|
||||
// backgroundColor: '#000'
|
||||
// })
|
||||
// window.loadURL('about:blank')
|
||||
// window.setTouchBar(touchBar)
|
||||
// })
|
||||
|
||||
module.exports = touchBar;
|
||||
21
src/electron/tray.js
Normal file
21
src/electron/tray.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import path from "path";
|
||||
import { nativeImage, Tray } from "electron";
|
||||
|
||||
export function createTray(win) {
|
||||
let icon = nativeImage
|
||||
.createFromPath(path.join(__static, "img/icons/menu@88.png"))
|
||||
.resize({
|
||||
height: 20,
|
||||
width: 20,
|
||||
});
|
||||
let tray = new Tray(icon);
|
||||
|
||||
tray.on("click", () => {
|
||||
if (win && win.isVisible()) {
|
||||
win.hide();
|
||||
} else {
|
||||
win.show();
|
||||
}
|
||||
});
|
||||
return tray;
|
||||
}
|
||||
|
|
@ -2,7 +2,10 @@
|
|||
|
||||
import { register } from "register-service-worker";
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
if (
|
||||
process.env.NODE_ENV === "production" &&
|
||||
process.env.IS_ELECTRON === "undefined"
|
||||
) {
|
||||
register(`${process.env.BASE_URL}service-worker.js`, {
|
||||
ready() {
|
||||
console.log(
|
||||
|
|
|
|||
|
|
@ -7,42 +7,46 @@ import initLocalStorage from "./initLocalStorage";
|
|||
import { Howl, Howler } from "howler";
|
||||
import { changeAppearance } from "@/utils/common";
|
||||
import updateApp from "@/utils/updateApp";
|
||||
import pack from "../../package.json";
|
||||
import pkg from "../../package.json";
|
||||
// vuex 自定义插件
|
||||
import { getBroadcastPlugin } from "./plugins/broadcast";
|
||||
import saveToLocalStorage from "./plugins/localStorage";
|
||||
|
||||
if (localStorage.getItem("appVersion") === null) {
|
||||
localStorage.setItem("player", JSON.stringify(initLocalStorage.player));
|
||||
localStorage.setItem("settings", JSON.stringify(initLocalStorage.settings));
|
||||
localStorage.setItem("data", JSON.stringify(initLocalStorage.data));
|
||||
localStorage.setItem("appVersion", pack.version);
|
||||
localStorage.setItem("appVersion", pkg.version);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
updateApp();
|
||||
|
||||
const saveToLocalStorage = (store) => {
|
||||
store.subscribe((mutation, state) => {
|
||||
// console.log(mutation);
|
||||
localStorage.setItem("player", JSON.stringify(state.player));
|
||||
localStorage.setItem("settings", JSON.stringify(state.settings));
|
||||
localStorage.setItem("data", JSON.stringify(state.data));
|
||||
});
|
||||
};
|
||||
|
||||
Vue.use(Vuex);
|
||||
const store = new Vuex.Store({
|
||||
state: state,
|
||||
|
||||
let plugins = [saveToLocalStorage];
|
||||
if (process.env.IS_ELECTRON === true) {
|
||||
let vuexBroadCast = getBroadcastPlugin();
|
||||
plugins.push(vuexBroadCast);
|
||||
}
|
||||
|
||||
const options = {
|
||||
state,
|
||||
mutations,
|
||||
actions,
|
||||
plugins: [saveToLocalStorage],
|
||||
});
|
||||
plugins,
|
||||
};
|
||||
|
||||
const store = new Vuex.Store(options);
|
||||
|
||||
store.state.howler = new Howl({
|
||||
src: [
|
||||
`https://music.163.com/song/media/outer/url?id=${store.state.player.currentTrack.id}`,
|
||||
],
|
||||
html5: true,
|
||||
format: ["mp3"],
|
||||
format: ["webm", "mp3"],
|
||||
});
|
||||
|
||||
Howler.volume(store.state.player.volume);
|
||||
|
||||
if ([undefined, null].includes(store.state.settings.lang)) {
|
||||
|
|
@ -53,6 +57,7 @@ if ([undefined, null].includes(store.state.settings.lang)) {
|
|||
}
|
||||
|
||||
changeAppearance(store.state.settings.appearance);
|
||||
|
||||
window
|
||||
.matchMedia("(prefers-color-scheme: dark)")
|
||||
.addEventListener("change", () => {
|
||||
|
|
|
|||
23
src/store/plugins/broadcast.js
Normal file
23
src/store/plugins/broadcast.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
export function getBroadcastPlugin() {
|
||||
const electron = window.require("electron");
|
||||
const ipcRenderer = electron.ipcRenderer;
|
||||
|
||||
return (store) => {
|
||||
// 第一行初始化第一次的状态
|
||||
ipcRenderer.send("vuex-state", store.state);
|
||||
store.subscribe(
|
||||
(
|
||||
mutation,
|
||||
{ data = "", settings = "", player = {}, contextMenu = {}, liked = {} }
|
||||
) => {
|
||||
const copyState = { data, settings, player, contextMenu, liked };
|
||||
ipcRenderer.send("vuex-state", copyState);
|
||||
}
|
||||
);
|
||||
store.subscribe((mutation, state) => {
|
||||
if (mutation.type === "updateData") {
|
||||
ipcRenderer.send("updateData", state.data);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
8
src/store/plugins/localStorage.js
Normal file
8
src/store/plugins/localStorage.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export default (store) => {
|
||||
store.subscribe((mutation, state) => {
|
||||
// console.log(mutation);
|
||||
localStorage.setItem("player", JSON.stringify(state.player));
|
||||
localStorage.setItem("settings", JSON.stringify(state.settings));
|
||||
localStorage.setItem("data", JSON.stringify(state.data));
|
||||
});
|
||||
};
|
||||
|
|
@ -34,3 +34,19 @@ export function isUsernameLoggedIn() {
|
|||
export function isLooseLoggedIn() {
|
||||
return isAccountLoggedIn() || isUsernameLoggedIn();
|
||||
}
|
||||
|
||||
export function getMusicU(string) {
|
||||
const temp = string.split(";");
|
||||
if (!temp.length) {
|
||||
return undefined;
|
||||
}
|
||||
const MUSIC_U = temp.find((item) => item.includes("MUSIC_U"));
|
||||
if (MUSIC_U) {
|
||||
return MUSIC_U.split("=")[1];
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function setMusicU(key, value) {
|
||||
return Cookies.set(key, value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import axios from "axios";
|
||||
// import axios from "axios";
|
||||
import localforage from "localforage";
|
||||
import { getMP3 } from "@/api/track";
|
||||
|
||||
export function cacheTrack(id) {
|
||||
let tracks = localforage.createInstance({
|
||||
name: "tracks",
|
||||
});
|
||||
// let tracks = localforage.createInstance({
|
||||
// name: "tracks",
|
||||
// });
|
||||
|
||||
// TODO: limit cache songs number
|
||||
// tracks.length().then(function (length) {
|
||||
|
|
@ -18,14 +18,15 @@ export function cacheTrack(id) {
|
|||
|
||||
// TODO: cache track details
|
||||
return getMP3(id).then((data) => {
|
||||
return axios
|
||||
.get(data.data[0].url.replace(/^http:/, "https:"), {
|
||||
responseType: "blob",
|
||||
})
|
||||
.then((data) => {
|
||||
tracks.setItem(`${id}`, { mp3: data.data });
|
||||
return { mp3: data.data };
|
||||
});
|
||||
// return axios
|
||||
// .get(data.data[0].url.replace(/^http:/, "https:"), {
|
||||
// responseType: "blob",
|
||||
// })
|
||||
// .then((data) => {
|
||||
// tracks.setItem(`${id}`, { mp3: data.data });
|
||||
// return { mp3: data.data };
|
||||
// });
|
||||
return { mp3: data.data[0].url.replace(/^http:/, "https:") };
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,15 @@
|
|||
import axios from "axios";
|
||||
|
||||
let baseURL = "";
|
||||
// Web 和 Electron 跑在不同端口避免同时启动时冲突
|
||||
if (process.env.IS_ELECTRON) {
|
||||
baseURL = process.env.VUE_APP_ELECTRON_API_URL;
|
||||
} else {
|
||||
baseURL = process.env.VUE_APP_NETEASE_API_URL;
|
||||
}
|
||||
|
||||
const service = axios.create({
|
||||
baseURL: process.env.VUE_APP_NETEASE_API_URL,
|
||||
baseURL,
|
||||
withCredentials: true,
|
||||
timeout: 15000,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ import { loginWithPhone, loginWithEmail } from "@/api/auth";
|
|||
import md5 from "crypto-js/md5";
|
||||
import { mapMutations } from "vuex";
|
||||
import { userPlaylist } from "@/api/user";
|
||||
import { getMusicU, setMusicU } from "@/utils/auth";
|
||||
|
||||
export default {
|
||||
name: "Login",
|
||||
|
|
@ -177,6 +178,10 @@ export default {
|
|||
md5_password: md5(this.password).toString(),
|
||||
})
|
||||
.then((data) => {
|
||||
// 获取接口返回的 MUSIC_U 写进本地 cookie 解决登录时跳转的问题
|
||||
// 但是仍然无法完全模拟登录状态,像喜欢歌曲和喜欢列表都会遇到 301 需要登录问题
|
||||
const MUSIC_U = getMusicU(data.cookie);
|
||||
setMusicU("MUSIC_U", MUSIC_U);
|
||||
if (data.code !== 502) {
|
||||
this.updateData({ key: "user", value: data.profile });
|
||||
this.afterLogin();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue