feat: 实现托盘菜单 (#1538)
* 从 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
|
@ -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',
|
||||
|
|
BIN
src/main/assets/icons/exit.png
Normal file
After Width: | Height: | Size: 223 B |
BIN
src/main/assets/icons/left.png
Normal file
After Width: | Height: | Size: 191 B |
BIN
src/main/assets/icons/like.png
Normal file
After Width: | Height: | Size: 308 B |
BIN
src/main/assets/icons/menu.png
Normal file
After Width: | Height: | Size: 311 B |
BIN
src/main/assets/icons/menu@88.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
src/main/assets/icons/pause.png
Normal file
After Width: | Height: | Size: 953 B |
BIN
src/main/assets/icons/play.png
Normal file
After Width: | Height: | Size: 396 B |
BIN
src/main/assets/icons/repeat.png
Normal file
After Width: | Height: | Size: 344 B |
BIN
src/main/assets/icons/right.png
Normal file
After Width: | Height: | Size: 218 B |
BIN
src/main/assets/icons/unlike.png
Normal file
After Width: | Height: | Size: 932 B |
|
@ -9,9 +9,11 @@ import {
|
|||
} from 'electron'
|
||||
import Store from 'electron-store'
|
||||
import { release } from 'os'
|
||||
import path, { join } from 'path'
|
||||
import { join } from 'path'
|
||||
import log from './log'
|
||||
import { initIpcMain } from './ipcMain'
|
||||
import { createTray, YPMTray } from './tray'
|
||||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
|
||||
const isWindows = process.platform === 'win32'
|
||||
const isMac = process.platform === 'darwin'
|
||||
|
@ -29,6 +31,7 @@ interface TypedElectronStore {
|
|||
|
||||
class Main {
|
||||
win: BrowserWindow | null = null
|
||||
tray: YPMTray | null = null
|
||||
store = new Store<TypedElectronStore>({
|
||||
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
|
||||
|
|
|
@ -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 = <T extends keyof IpcChannelsParams>(
|
||||
channel: T,
|
||||
|
@ -13,11 +14,16 @@ const on = <T extends keyof IpcChannelsParams>(
|
|||
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缓存
|
||||
*/
|
||||
|
|
173
src/main/tray.ts
Normal file
|
@ -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)
|
||||
}
|
|
@ -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 = () => {
|
|||
|
||||
<Toaster position='bottom-center' containerStyle={{ bottom: '5rem' }} />
|
||||
|
||||
<IpcRendererReact />
|
||||
|
||||
{/* Devtool */}
|
||||
<ReactQueryDevtools
|
||||
initialIsOpen={false}
|
||||
|
|
52
src/renderer/IpcRendererReact.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { IpcChannels } from '@/shared/IpcChannels'
|
||||
import useUserLikedTracksIDs, {
|
||||
useMutationLikeATrack,
|
||||
} from '@/renderer/hooks/useUserLikedTracksIDs'
|
||||
import { player } from '@/renderer/store'
|
||||
import useIpcRenderer from '@/renderer/hooks/useIpcRenderer'
|
||||
import { State as PlayerState } from '@/renderer/utils/player'
|
||||
|
||||
const IpcRendererReact = () => {
|
||||
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
|
|
@ -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 = () => {
|
|||
<div className='flex flex-col justify-center leading-tight'>
|
||||
<div
|
||||
onClick={toTrackListSource}
|
||||
className='line-clamp-1 font-semibold text-black decoration-gray-600 decoration-2 hover:underline dark:text-white dark:decoration-gray-300'
|
||||
className={classNames(
|
||||
'line-clamp-1 font-semibold text-black decoration-gray-600 decoration-2 dark:text-white dark:decoration-gray-300',
|
||||
hasTrackListSource && 'hover:underline'
|
||||
)}
|
||||
>
|
||||
{track?.name}
|
||||
</div>
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
14
src/renderer/hooks/useIpcRenderer.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { IpcChannelsParams, IpcChannelsReturns } from '@/shared/IpcChannels'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
|
||||
const useIpcRenderer = <T extends keyof IpcChannelsParams> (
|
||||
channcel: T,
|
||||
listener: (event: any, value: IpcChannelsReturns[T]) => void
|
||||
) => {
|
||||
useEffect(() => {
|
||||
return window.ipcRenderer?.on(channcel, listener)
|
||||
}, [])
|
||||
}
|
||||
|
||||
export default useIpcRenderer
|
35
src/renderer/ipcRenderer.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { player } from '@/renderer/store'
|
||||
import { IpcChannels, IpcChannelsReturns, IpcChannelsParams } from '@/shared/IpcChannels'
|
||||
|
||||
const on = <T extends keyof IpcChannelsParams>(
|
||||
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
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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<typeof setInterval> | 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()
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
5
src/shared/playerDataTypes.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export enum RepeatMode {
|
||||
Off = 'off',
|
||||
On = 'on',
|
||||
One = 'one',
|
||||
}
|