feat: 完成windows与linux下的Titlebar (#1482)

* feat: 完成windows与linux下的Titlebar

* update

* fix: win titlebar

* 优化部分api命名

* 视觉效果更好的标题间距

* fix: Linux title没有居中

* update

* fix: Main style

* 向renderer公开IpcRenderer.on

* update
This commit is contained in:
memorydream 2022-04-09 00:19:58 +08:00 committed by GitHub
parent 24798a0bf3
commit 1444bbefa2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 147 additions and 19 deletions

View File

@ -11,6 +11,7 @@ import Store from 'electron-store'
import { release } from 'os' import { release } from 'os'
import path, { join } from 'path' import path, { join } from 'path'
import logger from './logger' import logger from './logger'
import { initIpcMain } from './ipcMain'
const isWindows = process.platform === 'win32' const isWindows = process.platform === 'win32'
const isMac = process.platform === 'darwin' const isMac = process.platform === 'darwin'
@ -64,6 +65,7 @@ async function createWindow() {
minHeight: 720, minHeight: 720,
vibrancy: 'fullscreen-ui', vibrancy: 'fullscreen-ui',
titleBarStyle: 'hiddenInset', titleBarStyle: 'hiddenInset',
frame: !(isWindows || isLinux), // TODO: 适用于linux下独立的启用开关
} }
if (store.get('window')) { if (store.get('window')) {
options.x = store.get('window.x') options.x = store.get('window.x')
@ -99,6 +101,8 @@ async function createWindow() {
app.whenReady().then(async () => { app.whenReady().then(async () => {
logger.info('[index] App ready') logger.info('[index] App ready')
createWindow() createWindow()
handleWindowEvents()
initIpcMain(win)
// Install devtool extension // Install devtool extension
if (isDev) { if (isDev) {
@ -138,3 +142,13 @@ app.on('activate', () => {
createWindow() createWindow()
} }
}) })
const handleWindowEvents = () => {
win?.on('maximize', () => {
win?.webContents.send('is-maximized', true)
})
win?.on('unmaximize', () => {
win?.webContents.send('is-maximized', false)
})
}

View File

@ -1,8 +1,11 @@
import { ipcMain } from 'electron' import { BrowserWindow, ipcMain, app } from 'electron'
import { db, Tables } from './db' import { db, Tables } from './db'
export enum Events { export enum Events {
ClearAPICache = 'clear-api-cache', ClearAPICache = 'clear-api-cache',
Minimize = 'minimize',
MaximizeOrUnmaximize = 'maximize-or-unmaximize',
Close = 'close',
} }
ipcMain.on(Events.ClearAPICache, () => { ipcMain.on(Events.ClearAPICache, () => {
@ -15,3 +18,18 @@ ipcMain.on(Events.ClearAPICache, () => {
db.truncate(Tables.AUDIO) db.truncate(Tables.AUDIO)
db.vacuum() db.vacuum()
}) })
export function initIpcMain(win: BrowserWindow | null) {
ipcMain.on(Events.Minimize, () => {
win?.minimize()
})
ipcMain.on(Events.MaximizeOrUnmaximize, () => {
if (!win) return
win.isMaximized() ? win.unmaximize() : win.maximize()
})
ipcMain.on(Events.Close, () => {
app.exit()
})
}

View File

@ -1,8 +1,17 @@
const { contextBridge, ipcRenderer } = require('electron') const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('ipcRenderer', ipcRenderer) contextBridge.exposeInMainWorld('ipcRenderer', {
sendSync: ipcRenderer.sendSync,
send: ipcRenderer.send,
on: (channel, listener) => {
ipcRenderer.on(channel, listener)
return () => ipcRenderer.removeListener(channel, listener)
},
})
contextBridge.exposeInMainWorld('env', { contextBridge.exposeInMainWorld('env', {
isElectron: true, isElectron: true,
isEnableTitlebar:
process.platform === 'win32' || process.platform === 'linux',
isLinux: process.platform === 'linux', isLinux: process.platform === 'linux',
isMac: process.platform === 'darwin', isMac: process.platform === 'darwin',
isWin: process.platform === 'win32', isWin: process.platform === 'win32',

View File

@ -11,7 +11,7 @@ import Lyric from '@/components/Lyric'
const App = () => { const App = () => {
return ( return (
<QueryClientProvider client={reactQueryClient}> <QueryClientProvider client={reactQueryClient}>
{window.env?.isWin && <TitleBar />} {window.env?.isEnableTitlebar && <TitleBar />}
<div id='layout' className='grid select-none grid-cols-[16rem_auto]'> <div id='layout' className='grid select-none grid-cols-[16rem_auto]'>
<Sidebar /> <Sidebar />

View File

@ -7,6 +7,7 @@ declare global {
const useContext: typeof import('react')['useContext'] const useContext: typeof import('react')['useContext']
const useDebugValue: typeof import('react')['useDebugValue'] const useDebugValue: typeof import('react')['useDebugValue']
const useEffect: typeof import('react')['useEffect'] const useEffect: typeof import('react')['useEffect']
const useEffectOnce: typeof import('react-use')['useEffectOnce']
const useImperativeHandle: typeof import('react')['useImperativeHandle'] const useImperativeHandle: typeof import('react')['useImperativeHandle']
const useInfiniteQuery: typeof import('react-query')['useInfiniteQuery'] const useInfiniteQuery: typeof import('react-query')['useInfiniteQuery']
const useMemo: typeof import('react')['useMemo'] const useMemo: typeof import('react')['useMemo']

View File

@ -8,7 +8,13 @@ const Main = () => {
className='relative flex h-screen max-h-screen flex-grow flex-col overflow-y-auto bg-white dark:bg-[#1d1d1d]' className='relative flex h-screen max-h-screen flex-grow flex-col overflow-y-auto bg-white dark:bg-[#1d1d1d]'
> >
<Topbar /> <Topbar />
<main id='main' className='mb-24 flex-grow px-8'> <main
id='main'
className={classNames(
'mb-24 flex-grow px-8',
window.env?.isEnableTitlebar && 'mt-8'
)}
>
<Router /> <Router />
</main> </main>
</div> </div>

View File

@ -35,7 +35,9 @@ const primaryTabs: PrimaryTab[] = [
const PrimaryTabs = () => { const PrimaryTabs = () => {
return ( return (
<div> <div>
<div className='app-region-drag h-14'></div> <div
className={classNames(window.env?.isMac && 'app-region-drag', 'h-14')}
></div>
{primaryTabs.map(tab => ( {primaryTabs.map(tab => (
<NavLink <NavLink
onClick={() => scrollToTop()} onClick={() => scrollToTop()}

View File

@ -1,20 +1,94 @@
import { player } from '@/store'
import SvgIcon from './SvgIcon' import SvgIcon from './SvgIcon'
const Controls = () => {
const [isMaximized, setIsMaximized] = useState(false)
useEffectOnce(() => {
return window.ipcRenderer?.on('is-maximized', (e, value) => {
setIsMaximized(value)
})
})
const minimize = () => {
window.ipcRenderer?.send('minimize')
}
const maxRestore = () => {
window.ipcRenderer?.send('maximize-or-unmaximize')
}
const close = () => {
window.ipcRenderer?.send('close')
}
return (
<div className='app-region-no-drag flex h-full'>
<button
onClick={minimize}
className='flex w-[2.875rem] items-center justify-center hover:bg-[#e9e9e9]'
>
<SvgIcon className='h-3 w-3' name='windows-minimize' />
</button>
<button
onClick={maxRestore}
className='flex w-[2.875rem] items-center justify-center hover:bg-[#e9e9e9]'
>
<SvgIcon
className='h-3 w-3'
name={isMaximized ? 'windows-un-maximize' : 'windows-maximize'}
/>
</button>
<button
onClick={close}
className='flex w-[2.875rem] items-center justify-center hover:bg-[#c42b1c] hover:text-white'
>
<SvgIcon className='h-3 w-3' name='windows-close' />
</button>
</div>
)
}
const Title = ({ className }: { className?: string }) => {
const playerSnapshot = useSnapshot(player)
const track = useMemo(() => playerSnapshot.track, [playerSnapshot.track])
return (
<div className={classNames('text-sm text-gray-500', className)}>
{track?.name && (
<>
<span>{track.name}</span>
<span className='mx-2'>-</span>
</>
)}
<span>YesPlayMusic</span>
</div>
)
}
const Win = () => {
return (
<div className='flex h-8 w-screen items-center justify-between bg-gray-50'>
<Title className='ml-3' />
<Controls />
</div>
)
}
const Linux = () => {
return (
<div className='flex h-8 w-screen items-center justify-between bg-gray-50'>
<div></div>
<Title className='text-center' />
<Controls />
</div>
)
}
const TitleBar = () => { const TitleBar = () => {
return ( return (
<div className='app-region-drag flex h-8 w-screen items-center justify-between bg-gray-50'> <div className='app-region-drag fixed z-30'>
<div className='ml-2 text-sm text-gray-500'>YesPlayMusic</div> {window.env?.isWin ? <Win /> : <Linux />}
<div className='flex h-full'>
<button className='app-region-no-drag flex w-[2.875rem] items-center justify-center hover:bg-[#e9e9e9]'>
<SvgIcon className='h-3 w-3' name='windows-minimize' />
</button>
<button className='app-region-no-drag flex w-[2.875rem] items-center justify-center hover:bg-[#e9e9e9]'>
<SvgIcon className='h-3 w-3' name='windows-maximize' />
</button>
<button className='app-region-no-drag flex w-[2.875rem] items-center justify-center hover:bg-[#c42b1c] hover:text-white'>
<SvgIcon className='h-3 w-3' name='windows-close' />
</button>
</div>
</div> </div>
) )
} }

View File

@ -119,7 +119,9 @@ const Topbar = () => {
return ( return (
<div <div
className={classNames( className={classNames(
'app-region-drag sticky top-0 z-30 flex h-16 min-h-[4rem] w-full cursor-default items-center justify-between px-8 transition duration-300', 'sticky z-30 flex h-16 min-h-[4rem] w-full cursor-default items-center justify-between px-8 transition duration-300',
window.env?.isMac && 'app-region-drag',
window.env?.isEnableTitlebar ? 'top-8' : 'top-0',
!scroll.arrivedState.top && !scroll.arrivedState.top &&
'bg-white bg-opacity-[.86] backdrop-blur-xl backdrop-saturate-[1.8] dark:bg-[#222] dark:bg-opacity-[.86]' 'bg-white bg-opacity-[.86] backdrop-blur-xl backdrop-saturate-[1.8] dark:bg-[#222] dark:bg-opacity-[.86]'
)} )}

View File

@ -6,6 +6,7 @@ declare global {
ipcRenderer?: import('electron').IpcRenderer ipcRenderer?: import('electron').IpcRenderer
env?: { env?: {
isElectron: boolean isElectron: boolean
isEnableTitlebar: boolean
isLinux: boolean isLinux: boolean
isMac: boolean isMac: boolean
isWin: boolean isWin: boolean

View File

@ -36,6 +36,7 @@ export default defineConfig({
{ 'react-query': ['useQuery', 'useMutation', 'useInfiniteQuery'] }, { 'react-query': ['useQuery', 'useMutation', 'useInfiniteQuery'] },
{ 'react-router-dom': ['useNavigate', 'useParams'] }, { 'react-router-dom': ['useNavigate', 'useParams'] },
{ 'react-hot-toast': ['toast'] }, { 'react-hot-toast': ['toast'] },
{ 'react-use': ['useEffectOnce']},
{ valtio: ['useSnapshot'] }, { valtio: ['useSnapshot'] },
], ],
}), }),