From ffdf66b57e8578ed2fef306fac508ce2e3d5a6f7 Mon Sep 17 00:00:00 2001 From: memorydream <34763046+memorydream@users.noreply.github.com> Date: Wed, 20 Apr 2022 20:25:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E6=89=98=E7=9B=98?= =?UTF-8?q?=E8=8F=9C=E5=8D=95=20(#1538)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 从 v1 添加托盘相关图标 * feat: ipcRenderer事件 * feat: 托盘菜单实现 * 修复合并后的错误 * fix: 托盘图标的like * 将 tray 相关的 ipc 放入ipcMain.ts * update * update * feat: 设置托盘Tooltip * fix * fix: tray play/pause fade * fix: 暂时将tray like与tooltip的设置移入Player组件中 useUserLikedTracksIDs 会在重新聚焦而不是切换track时触发,导致托盘无法实时更新数据 基于以上一点,在Player组件中有了一个用于设置tray数据的useEffect,故将tray tooltip的设置也放入其中,使tray的数据尽可能简单的和player数据保持一致 * 将部分ipcRenderer调用挪到单独的IpcRendererReact组件 * 移除SetTrayPlayState,复用已有channel * update --- .electron-builder.config.js | 4 + src/main/assets/icons/exit.png | Bin 0 -> 223 bytes src/main/assets/icons/left.png | Bin 0 -> 191 bytes src/main/assets/icons/like.png | Bin 0 -> 308 bytes src/main/assets/icons/menu.png | Bin 0 -> 311 bytes src/main/assets/icons/menu@88.png | Bin 0 -> 1084 bytes src/main/assets/icons/pause.png | Bin 0 -> 953 bytes src/main/assets/icons/play.png | Bin 0 -> 396 bytes src/main/assets/icons/repeat.png | Bin 0 -> 344 bytes src/main/assets/icons/right.png | Bin 0 -> 218 bytes src/main/assets/icons/unlike.png | Bin 0 -> 932 bytes src/main/index.ts | 18 ++- src/main/ipcMain.ts | 25 +++- src/main/tray.ts | 173 +++++++++++++++++++++++++++ src/renderer/App.tsx | 3 + src/renderer/IpcRendererReact.tsx | 52 ++++++++ src/renderer/components/Player.tsx | 12 +- src/renderer/components/TitleBar.tsx | 7 +- src/renderer/hooks/useIpcRenderer.ts | 14 +++ src/renderer/ipcRenderer.ts | 35 ++++++ src/renderer/main.tsx | 3 + src/renderer/utils/player.ts | 21 ++-- src/shared/IpcChannels.ts | 39 ++++++ src/shared/playerDataTypes.ts | 5 + 24 files changed, 392 insertions(+), 19 deletions(-) create mode 100644 src/main/assets/icons/exit.png create mode 100644 src/main/assets/icons/left.png create mode 100644 src/main/assets/icons/like.png create mode 100644 src/main/assets/icons/menu.png create mode 100644 src/main/assets/icons/menu@88.png create mode 100644 src/main/assets/icons/pause.png create mode 100644 src/main/assets/icons/play.png create mode 100644 src/main/assets/icons/repeat.png create mode 100644 src/main/assets/icons/right.png create mode 100644 src/main/assets/icons/unlike.png create mode 100644 src/main/tray.ts create mode 100644 src/renderer/IpcRendererReact.tsx create mode 100644 src/renderer/hooks/useIpcRenderer.ts create mode 100644 src/renderer/ipcRenderer.ts create mode 100644 src/shared/playerDataTypes.ts diff --git a/.electron-builder.config.js b/.electron-builder.config.js index 72bb482..099a41a 100644 --- a/.electron-builder.config.js +++ b/.electron-builder.config.js @@ -103,6 +103,10 @@ module.exports = { from: 'src/main/migrations', to: 'dist/main/migrations', }, + { + from: 'src/main/assets', + to: 'dist/main/assets', + }, '!**/node_modules/*/{*.MD,*.md,README,readme}', '!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}', '!**/node_modules/*.d.ts', diff --git a/src/main/assets/icons/exit.png b/src/main/assets/icons/exit.png new file mode 100644 index 0000000000000000000000000000000000000000..01e21b2c7f8e0e8f7e902cdd4221a7e46d5476a2 GIT binary patch literal 223 zcmV<503iQ~P)pf~V4a09N&=M*#sAn*(5B7o~o;61Q)fDLd?7k*H(02MF51vnWA7LbaM!1Js4 z0q7_!Kz+HvO~4odl$U%(oAF0D#0U}s^tjqN{5!<8u3n`eXXP3gfH`ua`w{KJJIO8H Z#t-s-9uZNcsP=D?O z&YZbNw0gHTNh%cYXjocWFnLEq^NeeZhniJd1T0iqEqA=S_qY82MpM2w?H9?lyY_$m1;$J5e1adJ9}ES$h{4m<&t;ucLK6T0Urqr4 literal 0 HcmV?d00001 diff --git a/src/main/assets/icons/like.png b/src/main/assets/icons/like.png new file mode 100644 index 0000000000000000000000000000000000000000..4bea102372bdb0ed5f1402210278a7bbdd01f4be GIT binary patch literal 308 zcmV-40n7f0P)~6Y~%%4prrn5S+Ni$v5_4s8+ieF39_+MN~h*( zn(qHzb!Rip%=vxi%sId5={R~F_38j)m`vcOG|!~@6(9MfCEx~qgfK)eDR?T|yTwuk z$pIUf#TD)_j;$1Uf_E%n6^}TJvm6lV9UyLK8j;~%^~@abh0%W&^)EIDJmVavEs#yD zVY+<63U;xKw;_-TM4=CHT>@ggkHxHwB`4~1i9uZ#s=Xu3*FZvikDVqTRA&zu!8(3$ zfp5%}DK%O7IQ2X<;f?LOy0ra;FjHjkIjl2nfL2>{2K)l(z9O$GvO>yQ24)M&xjw?O&@ z%PTgk#zH0uM)OOYFW7nt_ANKf%-Jgcsad_U*x}^lx~Cn-`ZmSS*y&=ay!530mXofU zOXmhoRJ2`wd3LA8p=qBp@6GFcIw#m}s{f1RZkZQb!vkAd%yTNbce$-IY534NM`_dX z$4yL|dxh#cT&tv9G7Nv487yTfunp5N-So$sBd}4aJXidexyl^}wbRy0tgZq=*QRvT zYLuUHw~qHIeqk~9Tpr)PZ>qmT17$X?I(B=X+iB*0!@K9sr10zn`isHS)z4*}Q$iB} DR(W*O literal 0 HcmV?d00001 diff --git a/src/main/assets/icons/menu@88.png b/src/main/assets/icons/menu@88.png new file mode 100644 index 0000000000000000000000000000000000000000..cc14a82d1a3de3faff2aaea28d180e0885c8780a GIT binary patch literal 1084 zcmV-C1jGA@P)ZVtY z+*8a+6OLu#leUB5(nv z;i>dQ7Z0wW{DC}Jq@5^(E8l(c0Fi1UwTJ>r8&Qd%b1gzqfrCIQ@R${|4aGji+#J6k z4z?sGJ%)_21m>ZdefwvMjVUN`m$X2*h=#m;Tg2JFeo3P9kPT<_DI&+W!2j*flgKBi zt;4BA7!tuh8rUBDB`S%hT=Clo*7c~)pL8Qgk^ zvDXH%I4AlHW??Bh3ADXpP|(r_(3(M8}ZC=IvL6I}$}Kxw#?p6KGi+$rEKWm4WJ zO+rbrH~5w^<^#P>1r3A?mtCjC8O)e0<{H5qd2&PIK5==2Q%}zP=MGSB6`LE}OnnYV zoV&`rvWE`u%opYYU+b#Nmbaa15oUV+3VCEQU*pPLvolT}+{Wx%!{x@J?`H)Hx0(*j zUE7IX)ig6TpG|*d{+JYOgRgQ@qV zgMk?Rwzjsv+D15nm(7b=D0{UlilQirq9}@@Ncjziaia>bFNlf&0000_;n*qKa;#p>Khj^YiwdGiX_lSpB zQO*;e5Jx#3koZ>UipOt)3m$(7=GpWD;1@lmLJKP`%!=F~o+OSKdYLuOBW@B>&6N!0 zQQBj$kwy{;NKjD5CUiK6Q8sC2?S1jOZy6=>F#1}kfRm$uB6*{vmZ_^sStDJgrK2L| z{(qjfHa+Ea4<&%^SJ(X<2ZH-Rt?IhJr>+)=)sGuwjVDT`pIFU?P#Qw%hHCTrM|FXH1Mahr@A#Wujazd$<<@y!uT!!QQq2?2}6a*KE_9*>U@ zwApN8-oOp|Fcyo&MzNo@TCF$4g+MqQt|5NZb$yi}j{?F%GMT)mGnU6|G#YE5JcL4_ zD{_Ug!2zkfN1>idrLLmUsOUT~8mIRj b{p0xtjMff*aXD5p00000NkvXXu0mjfGd-`s literal 0 HcmV?d00001 diff --git a/src/main/assets/icons/play.png b/src/main/assets/icons/play.png new file mode 100644 index 0000000000000000000000000000000000000000..90537c85fe10c4d197a333591c6de4f3fbad60d5 GIT binary patch literal 396 zcmV;70dxL|P)xeaGOaU1GX>iBP*QK(5*4pq7 zz_rDtITR9(QmNXb*;lK{kpyEM04Xi!qmU**y_oT=%CgW-nrn#y2s28{u}1zE;M(3M zK$A+#g}Ax6j@Sd!i_)+A=3E3I&J*B+ckSPrMAi{=%vel-cN;MiAF#cN5VWPVTxtcp zebxF$E0w3fBwS16L8wb%;?q*#ubO>2opbt@mrcqjv$wLyWH8xn{sdtWHjBlkNW#BBNB7Zn zdw%@}r=Fhsa?ba>-iCilA0E;=0P7g3AUU8NAIaNOQiwEmFrIlm!YB6PEC&o@6APJg9iR(0=qv$C=&i_E z1lyP>iuYAH&m)LXLw-Wb01@&Gsy0Vx$l$&Eu>9gkd*MaEGnBHipNxG%QiHs`slH#USp}{cn+4y@eR6 qNj&2f=S4S~zJ*e+>AqD2EkWPRYcCPoeH01+000017p&dM R7!GtSgQu&X%Q~loCICFuRIdO4 literal 0 HcmV?d00001 diff --git a/src/main/assets/icons/unlike.png b/src/main/assets/icons/unlike.png new file mode 100644 index 0000000000000000000000000000000000000000..a0afa24600ff68f90eeacc9a4d23bd15c9ced3d6 GIT binary patch literal 932 zcmV;V16%xwP)IpX|ADT8tAZdXf{2qpz^_G0Jhv&dNPEJ~d3f)65BHu6_|rNw?4AT*m=4dRlG18^ zP3n7x4}%CJh&h?r)>3vchpzGSNYxY%anY-LUB?(Gs5S$9{lqiO;tugVaeB+K1n&_K zvyxmOJ|T{AIw0|_&=rs01Q$L263la%1;8(QO2sx-+L)ENK|DzuHS`K=o=4myq?#*P z%A>T#U?YPRl8~UFf=%df5T|U?%sTrL4c{`#G#*FBU3dS6}la~$Z~2kJG~{XKQv`WX;-0j|xKzf5zQ{U*KE(xS&eWCyss zX=%y4r)R_ zgd~TeCZ|*kO9ycf1T_PpAPBdhwK-^M2?|<@7N;5%B7|(QhK59Ob;!a+2_k+VNnoOO zuJt~aAyE%5_rH73Iq$stAIJT3@xKLcc%E;I--&@hK(5tlQ?e}IrBbP9>W4z1 z5k*m^jYi{rI-OQ}IjRG%*K4ZR>xXa?2B40R1VK0;cZfCY8wfp4J%i9%CX;z02VLvQ zWReH@ga8JEVWLnd7$r&awA<|<$N{ZZJ5M#8PB#hCFdQBe2l$WWcDrZocKeIZ=aWBA zq*DdI-#?C!%_lQQL`f|Bk(P%U)xm@m|pTcn-k7uUQXe|aUV8UaIGWMUn%nW{5Hd| zOf%>cU`;rf$Iv#h$MwanY&Lt%dMpnVV4y*n1P1kg)q4lr^zPBY_m`^x0000({ defaults: { window: { @@ -58,7 +61,8 @@ class Main { this.createWindow() this.handleAppEvents() this.handleWindowEvents() - initIpcMain(this.win) + this.createTray() + initIpcMain(this.win, this.tray) this.initDevTools() }) } @@ -83,6 +87,12 @@ class Main { this.win.webContents.openDevTools() } + createTray() { + if (isWindows || isLinux || isDev) { + this.tray = createTray(this.win!) + } + } + createWindow() { const options: BrowserWindowConstructorOptions = { title: 'YesPlayMusic', @@ -119,11 +129,11 @@ class Main { // Window maximize and minimize this.win.on('maximize', () => { - this.win && this.win.webContents.send('is-maximized', true) + this.win && this.win.webContents.send(IpcChannels.IsMaximized, true) }) this.win.on('unmaximize', () => { - this.win && this.win.webContents.send('is-maximized', false) + this.win && this.win.webContents.send(IpcChannels.IsMaximized, false) }) // Save window position diff --git a/src/main/ipcMain.ts b/src/main/ipcMain.ts index 832ca7c..a6cb2c3 100644 --- a/src/main/ipcMain.ts +++ b/src/main/ipcMain.ts @@ -5,6 +5,7 @@ import cache from './cache' import log from './log' import fs from 'fs' import { APIs } from '../shared/CacheAPIs' +import { YPMTray } from './tray' const on = ( channel: T, @@ -13,11 +14,16 @@ const on = ( ipcMain.on(channel, listener) } +export function initIpcMain(win: BrowserWindow | null, tray: YPMTray | null) { + initWindowIpcMain(win) + initTrayIpcMain(tray) +} + /** * 处理需要win对象的事件 * @param {BrowserWindow} win */ -export function initIpcMain(win: BrowserWindow | null) { +function initWindowIpcMain(win: BrowserWindow | null) { on(IpcChannels.Minimize, () => { win?.minimize() }) @@ -32,6 +38,23 @@ export function initIpcMain(win: BrowserWindow | null) { }) } +/** + * 处理需要tray对象的事件 + * @param {YPMTray} tray + */ +function initTrayIpcMain(tray: YPMTray | null) { + on(IpcChannels.SetTrayTooltip, (e, { text }) => tray?.setTooltip(text)) + + on(IpcChannels.SetTrayLikeState, (e, { isLiked }) => + tray?.setLikeState(isLiked) + ) + + on(IpcChannels.Play, () => tray?.setPlayState(true)) + on(IpcChannels.Pause, () => tray?.setPlayState(false)) + + on(IpcChannels.Repeat, (e, { mode }) => tray?.setRepeatMode(mode)) +} + /** * 清除API缓存 */ diff --git a/src/main/tray.ts b/src/main/tray.ts new file mode 100644 index 0000000..cc46bd2 --- /dev/null +++ b/src/main/tray.ts @@ -0,0 +1,173 @@ +import path from 'path' +import { + app, + BrowserWindow, + Menu, + MenuItemConstructorOptions, + nativeImage, + Tray, +} from 'electron' +import { IpcChannels } from '@/shared/IpcChannels' +import { RepeatMode } from '@/shared/playerDataTypes' + +const iconDirRoot = + process.env.NODE_ENV === 'development' + ? path.join(process.cwd(), './src/main/assets/icons') + : path.join(__dirname, './assets/icons') + +enum MenuItemIDs { + Play = 'play', + Pause = 'pause', + Like = 'like', + Unlike = 'unlike', +} + +export interface YPMTray { + setTooltip(text: string): void + setLikeState(isLiked: boolean): void + setPlayState(isPlaying: boolean): void + setRepeatMode(mode: RepeatMode): void +} + +function createNativeImage(filename: string) { + return nativeImage.createFromPath(path.join(iconDirRoot, filename)) +} + +function createMenuTemplate(win: BrowserWindow): MenuItemConstructorOptions[] { + let template: MenuItemConstructorOptions[] = + process.platform === 'linux' + ? [ + { + label: '显示主面板', + click: () => win.show(), + }, + { + type: 'separator', + }, + ] + : [] + + return template.concat([ + { + label: '播放', + click: () => win.webContents.send(IpcChannels.Play), + icon: createNativeImage('play.png'), + id: MenuItemIDs.Play, + }, + { + label: '暂停', + click: () => win.webContents.send(IpcChannels.Pause), + icon: createNativeImage('pause.png'), + id: MenuItemIDs.Pause, + visible: false, + }, + { + label: '上一首', + click: () => win.webContents.send(IpcChannels.Previous), + icon: createNativeImage('left.png'), + }, + { + label: '下一首', + click: () => win.webContents.send(IpcChannels.Next), + icon: createNativeImage('right.png'), + }, + { + label: '循环模式', + icon: createNativeImage('repeat.png'), + submenu: [ + { + label: '关闭循环', + click: () => win.webContents.send(IpcChannels.Repeat, RepeatMode.Off), + id: RepeatMode.Off, + checked: true, + type: 'radio', + }, + { + label: '列表循环', + click: () => win.webContents.send(IpcChannels.Repeat, RepeatMode.On), + id: RepeatMode.On, + type: 'radio', + }, + { + label: '单曲循环', + click: () => win.webContents.send(IpcChannels.Repeat, RepeatMode.One), + id: RepeatMode.One, + type: 'radio', + }, + ], + }, + { + label: '加入喜欢', + click: () => win.webContents.send(IpcChannels.Like), + icon: createNativeImage('like.png'), + id: MenuItemIDs.Like, + }, + { + label: '取消喜欢', + click: () => win.webContents.send(IpcChannels.Like), + icon: createNativeImage('unlike.png'), + id: MenuItemIDs.Unlike, + visible: false, + }, + { + label: '退出', + click: () => app.exit(), + icon: createNativeImage('exit.png'), + }, + ]) +} + +class YPMTrayImpl implements YPMTray { + private _win: BrowserWindow + private _tray: Tray + private _template: MenuItemConstructorOptions[] + private _contextMenu: Menu + + constructor(win: BrowserWindow) { + this._win = win + const icon = createNativeImage('menu@88.png').resize({ + height: 20, + width: 20, + }) + this._tray = new Tray(icon) + this._template = createMenuTemplate(this._win) + this._contextMenu = Menu.buildFromTemplate(this._template) + + this._updateContextMenu() + this.setTooltip('YesPlayMusic') + + this._tray.on('click', () => win.show()) + } + + private _updateContextMenu() { + this._tray.setContextMenu(this._contextMenu) + } + + setTooltip(text: string) { + this._tray.setToolTip(text) + } + + setLikeState(isLiked: boolean) { + this._contextMenu.getMenuItemById(MenuItemIDs.Like)!.visible = !isLiked + this._contextMenu.getMenuItemById(MenuItemIDs.Unlike)!.visible = isLiked + this._updateContextMenu() + } + + setPlayState(isPlaying: boolean) { + this._contextMenu.getMenuItemById(MenuItemIDs.Play)!.visible = !isPlaying + this._contextMenu.getMenuItemById(MenuItemIDs.Pause)!.visible = isPlaying + this._updateContextMenu() + } + + setRepeatMode(mode: RepeatMode) { + const item = this._contextMenu.getMenuItemById(mode) + if (item) { + item.checked = true + this._updateContextMenu() + } + } +} + +export function createTray(win: BrowserWindow): YPMTray { + return new YPMTrayImpl(win) +} diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 9391d01..5ce7a6a 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -7,6 +7,7 @@ import reactQueryClient from '@/renderer/utils/reactQueryClient' import Main from '@/renderer/components/Main' import TitleBar from '@/renderer/components/TitleBar' import Lyric from '@/renderer/components/Lyric' +import IpcRendererReact from '@/renderer/IpcRendererReact' const App = () => { return ( @@ -23,6 +24,8 @@ const App = () => { + + {/* Devtool */} { + const [isPlaying, setIsPlaying] = useState(false) + const playerSnapshot = useSnapshot(player) + const track = useMemo(() => playerSnapshot.track, [playerSnapshot.track]) + const state = useMemo(() => playerSnapshot.state, [playerSnapshot.state]) + const trackIDRef = useRef(0) + + // Liked songs ids + const { data: userLikedSongs } = useUserLikedTracksIDs() + const mutationLikeATrack = useMutationLikeATrack() + + useIpcRenderer(IpcChannels.Like, () => { + const id = trackIDRef.current + id && mutationLikeATrack.mutate(id) + }) + + useEffect(() => { + trackIDRef.current = track?.id ?? 0 + + const text = track?.name ? `${track.name} - YesPlayMusic` : 'YesPlayMusic' + window.ipcRenderer?.send(IpcChannels.SetTrayTooltip, { + text, + }) + document.title = text + }, [track]) + + useEffect(() => { + window.ipcRenderer?.send(IpcChannels.SetTrayLikeState, { + isLiked: userLikedSongs?.ids?.includes(track?.id ?? 0) ?? false, + }) + }, [userLikedSongs, track]) + + useEffect(() => { + const playing = [PlayerState.Playing, PlayerState.Loading].includes(state) + if (isPlaying === playing) return + + window.ipcRenderer?.send(playing ? IpcChannels.Play : IpcChannels.Pause) + setIsPlaying(playing) + }, [state]) + + return <> +} + +export default IpcRendererReact diff --git a/src/renderer/components/Player.tsx b/src/renderer/components/Player.tsx index 35ffada..e831c3c 100644 --- a/src/renderer/components/Player.tsx +++ b/src/renderer/components/Player.tsx @@ -10,8 +10,8 @@ import { resizeImage } from '@/renderer/utils/common' import { State as PlayerState, Mode as PlayerMode, - RepeatMode as PlayerRepeatMode, } from '@/renderer/utils/player' +import { RepeatMode as PlayerRepeatMode } from '@/shared/playerDataTypes' const PlayingTrack = () => { const navigate = useNavigate() @@ -26,13 +26,16 @@ const PlayingTrack = () => { const { data: userLikedSongs } = useUserLikedTracksIDs() const mutationLikeATrack = useMutationLikeATrack() + const hasTrackListSource = + snappedPlayer.mode !== PlayerMode.FM && trackListSource?.type + const toAlbum = () => { const id = track?.al?.id if (id) navigate(`/album/${id}`) } const toTrackListSource = () => { - if (trackListSource?.type) + if (hasTrackListSource) navigate(`/${trackListSource.type}/${trackListSource.id}`) } @@ -59,7 +62,10 @@ const PlayingTrack = () => {
{track?.name}
diff --git a/src/renderer/components/TitleBar.tsx b/src/renderer/components/TitleBar.tsx index b98f4d1..2de912a 100644 --- a/src/renderer/components/TitleBar.tsx +++ b/src/renderer/components/TitleBar.tsx @@ -1,14 +1,13 @@ import { player } from '@/renderer/store' import SvgIcon from './SvgIcon' import { IpcChannels } from '@/shared/IpcChannels' +import useIpcRenderer from '@/renderer/hooks/useIpcRenderer' const Controls = () => { const [isMaximized, setIsMaximized] = useState(false) - useEffectOnce(() => { - return window.ipcRenderer?.on(IpcChannels.IsMaximized, (e, value) => { - setIsMaximized(value) - }) + useIpcRenderer(IpcChannels.IsMaximized, (e, value) => { + setIsMaximized(value) }) const minimize = () => { diff --git a/src/renderer/hooks/useIpcRenderer.ts b/src/renderer/hooks/useIpcRenderer.ts new file mode 100644 index 0000000..eaf5f30 --- /dev/null +++ b/src/renderer/hooks/useIpcRenderer.ts @@ -0,0 +1,14 @@ +import { IpcChannelsParams, IpcChannelsReturns } from '@/shared/IpcChannels' +import { useEffect } from 'react' + + +const useIpcRenderer = ( + channcel: T, + listener: (event: any, value: IpcChannelsReturns[T]) => void +) => { + useEffect(() => { + return window.ipcRenderer?.on(channcel, listener) + }, []) +} + +export default useIpcRenderer diff --git a/src/renderer/ipcRenderer.ts b/src/renderer/ipcRenderer.ts new file mode 100644 index 0000000..ca5efe4 --- /dev/null +++ b/src/renderer/ipcRenderer.ts @@ -0,0 +1,35 @@ +import { player } from '@/renderer/store' +import { IpcChannels, IpcChannelsReturns, IpcChannelsParams } from '@/shared/IpcChannels' + +const on = ( + channel: T, + listener: (event: any, params: IpcChannelsReturns[T]) => void +) => { + window.ipcRenderer?.on(channel, listener) +} + +export function ipcRenderer() { + on(IpcChannels.Play, () => { + player.play(true) + }) + + on(IpcChannels.Pause, () => { + player.pause(true) + }) + + on(IpcChannels.PlayOrPause, () => { + player.playOrPause() + }) + + on(IpcChannels.Next, () => { + player.nextTrack() + }) + + on(IpcChannels.Previous, () => { + player.prevTrack() + }) + + on(IpcChannels.Repeat, (e, mode) => { + player.repeatMode = mode + }) +} diff --git a/src/renderer/main.tsx b/src/renderer/main.tsx index aa596aa..c04a110 100644 --- a/src/renderer/main.tsx +++ b/src/renderer/main.tsx @@ -11,6 +11,7 @@ import './styles/accentColor.scss' import App from './App' import pkg from '../../package.json' import ReactGA from 'react-ga4' +import { ipcRenderer } from '@/renderer/ipcRenderer' ReactGA.initialize('G-KMJJCFZDKF') @@ -26,6 +27,8 @@ Sentry.init({ tracesSampleRate: 1.0, }) +ipcRenderer() + const container = document.getElementById('root') as HTMLElement const root = ReactDOMClient.createRoot(container) diff --git a/src/renderer/utils/player.ts b/src/renderer/utils/player.ts index 51628e5..bfe7cf3 100644 --- a/src/renderer/utils/player.ts +++ b/src/renderer/utils/player.ts @@ -11,6 +11,8 @@ import axios from 'axios' import { resizeImage } from './common' import { fetchPlaylistWithReactQuery } from '@/renderer/hooks/usePlaylist' import { fetchAlbumWithReactQuery } from '@/renderer/hooks/useAlbum' +import { IpcChannels } from '@/shared/IpcChannels' +import { RepeatMode } from '@/shared/playerDataTypes' type TrackID = number export enum TrackListSourceType { @@ -32,11 +34,6 @@ export enum State { Paused = 'paused', Loading = 'loading', } -export enum RepeatMode { - Off = 'off', - On = 'on', - One = 'one', -} const PLAY_PAUSE_FADE_DURATION = 200 @@ -47,6 +44,7 @@ export class Player { private _progress: number = 0 private _progressInterval: ReturnType | undefined private _volume: number = 1 // 0 to 1 + private _repeatMode: RepeatMode = RepeatMode.Off state: State = State.Initializing mode: Mode = Mode.TrackList @@ -54,25 +52,26 @@ export class Player { trackListSource: TrackListSource | null = null fmTrackList: TrackID[] = [] shuffle: boolean = false - repeatMode: RepeatMode = RepeatMode.Off fmTrack: Track | null = null init(params: { [key: string]: any }) { if (params._track) this._track = params._track if (params._trackIndex) this._trackIndex = params._trackIndex if (params._volume) this._volume = params._volume + if (params._repeatMode) this._repeatMode = params._repeatMode if (params.state) this.trackList = params.state if (params.mode) this.mode = params.mode if (params.trackList) this.trackList = params.trackList if (params.trackListSource) this.trackListSource = params.trackListSource if (params.fmTrackList) this.fmTrackList = params.fmTrackList if (params.shuffle) this.shuffle = params.shuffle - if (params.repeatMode) this.repeatMode = params.repeatMode if (params.fmTrack) this.fmTrack = params.fmTrack this.state = State.Ready this._playAudio(false) // just load the audio, not play this._initFM() + + window.ipcRenderer?.send(IpcChannels.Repeat, { mode: this._repeatMode }) } get howler() { @@ -151,6 +150,14 @@ export class Player { Howler.volume(this._volume) } + get repeatMode(): RepeatMode { + return this._repeatMode + } + set repeatMode(value) { + this._repeatMode = value + window.ipcRenderer?.send(IpcChannels.Repeat, { mode: this._repeatMode }) + } + private async _initFM() { if (this.fmTrackList.length === 0) await this._loadMoreFMTracks() diff --git a/src/shared/IpcChannels.ts b/src/shared/IpcChannels.ts index 3708bca..98400e7 100644 --- a/src/shared/IpcChannels.ts +++ b/src/shared/IpcChannels.ts @@ -1,4 +1,5 @@ import { APIs } from './CacheAPIs' +import { RepeatMode } from './playerDataTypes' export const enum IpcChannels { ClearAPICache = 'clear-api-cache', @@ -9,8 +10,19 @@ export const enum IpcChannels { GetApiCacheSync = 'get-api-cache-sync', DevDbExportJson = 'dev-db-export-json', CacheCoverColor = 'cache-cover-color', + SetTrayTooltip = 'set-tray-tooltip', + SetTrayLikeState = 'set-tray-like-state', + // 准备三个播放相关channel, 为 mpris 预留接口 + Play = 'play', + Pause = 'pause', + PlayOrPause = 'play-or-pause', + Next = 'next', + Previous = 'previous', + Like = 'like', + Repeat = 'repeat', } +// ipcMain.on params export interface IpcChannelsParams { [IpcChannels.ClearAPICache]: void [IpcChannels.Minimize]: void @@ -26,8 +38,26 @@ export interface IpcChannelsParams { id: number color: string } + [IpcChannels.SetTrayTooltip]: { + text: string + } + [IpcChannels.SetTrayLikeState]: { + isLiked: boolean + } + [IpcChannels.Play]: void + [IpcChannels.Pause]: void + [IpcChannels.PlayOrPause]: void + [IpcChannels.Next]: void + [IpcChannels.Previous]: void + [IpcChannels.Like]: { + isLiked: boolean + } + [IpcChannels.Repeat]: { + mode: RepeatMode + } } +// ipcRenderer.on params export interface IpcChannelsReturns { [IpcChannels.ClearAPICache]: void [IpcChannels.Minimize]: void @@ -37,4 +67,13 @@ export interface IpcChannelsReturns { [IpcChannels.GetApiCacheSync]: any [IpcChannels.DevDbExportJson]: void [IpcChannels.CacheCoverColor]: void + [IpcChannels.SetTrayTooltip]: void + [IpcChannels.SetTrayLikeState]: void + [IpcChannels.Play]: void + [IpcChannels.Pause]: void + [IpcChannels.PlayOrPause]: void + [IpcChannels.Next]: void + [IpcChannels.Previous]: void + [IpcChannels.Like]: void + [IpcChannels.Repeat]: RepeatMode } diff --git a/src/shared/playerDataTypes.ts b/src/shared/playerDataTypes.ts new file mode 100644 index 0000000..55533b9 --- /dev/null +++ b/src/shared/playerDataTypes.ts @@ -0,0 +1,5 @@ +export enum RepeatMode { + Off = 'off', + On = 'on', + One = 'one', +}