Skip to content

Commit

Permalink
support floating window
Browse files Browse the repository at this point in the history
  • Loading branch information
pompurin404 committed Oct 5, 2024
1 parent 23c1754 commit ab971e8
Show file tree
Hide file tree
Showing 14 changed files with 252 additions and 15 deletions.
5 changes: 1 addition & 4 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
### Breaking Changes

- 此版本修改了应用的显示名称,macOS用户可能无法自动更新,需要手动删除 `/Applications/mihomo-party.app`

### New Features

- 允许切换订阅卡片显示过期时间还是更新时间
- 添加悬浮窗功能,可以在设置中开启
2 changes: 2 additions & 0 deletions src/main/core/mihomoApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import WebSocket from 'ws'
import { tray } from '../resolve/tray'
import { calcTraffic } from '../utils/calc'
import { getRuntimeConfig } from './factory'
import { floatingWindow } from '../resolve/floatingWindow'

let axiosIns: AxiosInstance = null!
let mihomoTrafficWs: WebSocket | null = null
Expand Down Expand Up @@ -202,6 +203,7 @@ const mihomoTraffic = async (): Promise<void> => {
`${calcTraffic(json.down)}/s`.padStart(9)
)
}
floatingWindow?.webContents.send('mihomoTraffic', json)
} catch {
// ignore
}
Expand Down
15 changes: 13 additions & 2 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { existsSync, writeFileSync } from 'fs'
import { exePath, taskDir } from './utils/dirs'
import path from 'path'
import { startMonitor } from './resolve/trafficMonitor'
import { showFloatingWindow } from './resolve/floatingWindow'

let quitTimeout: NodeJS.Timeout | null = null
export let mainWindow: BrowserWindow | null = null
Expand Down Expand Up @@ -149,8 +150,12 @@ app.whenReady().then(async () => {
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
const { showFloatingWindow: showFloating = false } = await getAppConfig()
registerIpcMainHandlers()
await createWindow()
if (showFloating) {
showFloatingWindow()
}
await createTray()
await initShortcut()
app.on('activate', function () {
Expand Down Expand Up @@ -191,7 +196,8 @@ export async function createWindow(): Promise<void> {
const { useWindowFrame = false } = await getAppConfig()
const mainWindowState = windowStateKeeper({
defaultWidth: 800,
defaultHeight: 600
defaultHeight: 600,
file: 'window-state.json'
})
// https://github.com/electron/electron/issues/16521#issuecomment-582955104
Menu.setApplicationMenu(null)
Expand Down Expand Up @@ -269,7 +275,6 @@ export async function createWindow(): Promise<void> {
shell.openExternal(details.url)
return { action: 'deny' }
})

// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
Expand All @@ -288,3 +293,9 @@ export function showMainWindow(): void {
mainWindow.focusOnWebView()
}
}

export function closeMainWindow(): void {
if (mainWindow) {
mainWindow.close()
}
}
68 changes: 68 additions & 0 deletions src/main/resolve/floatingWindow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { is } from '@electron-toolkit/utils'
import { BrowserWindow } from 'electron'
import windowStateKeeper from 'electron-window-state'
import { join } from 'path'
import { getAppConfig } from '../config'
import { applyTheme } from './theme'
import { buildContextMenu } from './tray'

export let floatingWindow: BrowserWindow | null = null

async function createFloatingWindow(): Promise<void> {
const floatingWindowState = windowStateKeeper({
file: 'floating-window-state.json'
})
const { customTheme = 'default.css' } = await getAppConfig()
floatingWindow = new BrowserWindow({
width: 126,
height: 50,
x: floatingWindowState.x,
y: floatingWindowState.y,
show: false,
frame: false,
alwaysOnTop: true,
resizable: false,
transparent: true,
skipTaskbar: true,
minimizable: false,
maximizable: false,
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
spellcheck: false,
sandbox: false
}
})
floatingWindowState.manage(floatingWindow)
floatingWindow.on('ready-to-show', () => {
applyTheme(customTheme)
floatingWindow?.show()
})
floatingWindow.on('moved', () => {
if (floatingWindow) floatingWindowState.saveState(floatingWindow)
})
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
floatingWindow.loadURL(`${process.env['ELECTRON_RENDERER_URL']}/floating.html`)
} else {
floatingWindow.loadFile(join(__dirname, '../renderer/floating.html'))
}
}
export function showFloatingWindow(): void {
if (floatingWindow) {
floatingWindow.show()
} else {
createFloatingWindow()
}
}

export function closeFloatingWindow(): void {
if (floatingWindow) {
floatingWindow.close()
floatingWindow.destroy()
floatingWindow = null
}
}

export async function showContextMenu(): Promise<void> {
const menu = await buildContextMenu()
menu.popup()
}
14 changes: 11 additions & 3 deletions src/main/resolve/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import AdmZip from 'adm-zip'
import { getControledMihomoConfig } from '../config'
import { existsSync } from 'fs'
import { mainWindow } from '..'
import { floatingWindow } from './floatingWindow'

let insertedCSSKey: string | undefined = undefined
let insertedCSSKeyMain: string | undefined = undefined
let insertedCSSKeyFloating: string | undefined = undefined

export async function resolveThemes(): Promise<{ key: string; label: string }[]> {
const files = await readdir(themesDir())
Expand Down Expand Up @@ -67,6 +69,12 @@ export async function writeTheme(theme: string, css: string): Promise<void> {

export async function applyTheme(theme: string): Promise<void> {
const css = await readTheme(theme)
await mainWindow?.webContents.removeInsertedCSS(insertedCSSKey || '')
insertedCSSKey = await mainWindow?.webContents.insertCSS(css)
await mainWindow?.webContents.removeInsertedCSS(insertedCSSKeyMain || '')
insertedCSSKeyMain = await mainWindow?.webContents.insertCSS(css)
try {
await floatingWindow?.webContents.removeInsertedCSS(insertedCSSKeyFloating || '')
insertedCSSKeyFloating = await floatingWindow?.webContents.insertCSS(css)
} catch {
// ignore
}
}
10 changes: 5 additions & 5 deletions src/main/resolve/tray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ import {
mihomoGroups,
patchMihomoConfig
} from '../core/mihomoApi'
import { mainWindow, showMainWindow } from '..'
import { closeMainWindow, mainWindow, showMainWindow } from '..'
import { app, clipboard, ipcMain, Menu, nativeImage, shell, Tray } from 'electron'
import { dataDir, logDir, mihomoCoreDir, mihomoWorkDir } from '../utils/dirs'
import { triggerSysProxy } from '../sys/sysproxy'
import { quitWithoutCore, restartCore } from '../core/manager'

export let tray: Tray | null = null

const buildContextMenu = async (): Promise<Menu> => {
export const buildContextMenu = async (): Promise<Menu> => {
const { mode, tun } = await getControledMihomoConfig()
const {
sysProxy,
Expand Down Expand Up @@ -291,7 +291,7 @@ export async function createTray(): Promise<void> {
})
tray?.addListener('right-click', async () => {
if (mainWindow?.isVisible()) {
mainWindow?.close()
closeMainWindow()
} else {
showMainWindow()
}
Expand All @@ -303,7 +303,7 @@ export async function createTray(): Promise<void> {
if (process.platform === 'win32') {
tray?.addListener('click', () => {
if (mainWindow?.isVisible()) {
mainWindow?.close()
closeMainWindow()
} else {
showMainWindow()
}
Expand All @@ -315,7 +315,7 @@ export async function createTray(): Promise<void> {
if (process.platform === 'linux') {
tray?.addListener('click', () => {
if (mainWindow?.isVisible()) {
mainWindow?.close()
closeMainWindow()
} else {
showMainWindow()
}
Expand Down
8 changes: 7 additions & 1 deletion src/main/utils/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ import { listWebdavBackups, webdavBackup, webdavDelete, webdavRestore } from '..
import { getInterfaces } from '../sys/interface'
import { copyEnv } from '../resolve/tray'
import { registerShortcut } from '../resolve/shortcut'
import { mainWindow } from '..'
import { closeMainWindow, mainWindow, showMainWindow } from '..'
import {
applyTheme,
fetchThemes,
Expand All @@ -81,6 +81,7 @@ import v8 from 'v8'
import { getGistUrl } from '../resolve/gistApi'
import { getImageDataURL } from './image'
import { startMonitor } from '../resolve/trafficMonitor'
import { closeFloatingWindow, showContextMenu, showFloatingWindow } from '../resolve/floatingWindow'

function ipcErrorWrapper<T>( // eslint-disable-next-line @typescript-eslint/no-explicit-any
fn: (...args: any[]) => Promise<T> // eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -209,6 +210,11 @@ export function registerIpcMainHandlers(): void {
ipcMain.handle('isAlwaysOnTop', () => {
return mainWindow?.isAlwaysOnTop()
})
ipcMain.handle('showMainWindow', showMainWindow)
ipcMain.handle('closeMainWindow', closeMainWindow)
ipcMain.handle('showFloatingWindow', showFloatingWindow)
ipcMain.handle('closeFloatingWindow', closeFloatingWindow)
ipcMain.handle('showContextMenu', () => ipcErrorWrapper(showContextMenu)())
ipcMain.handle('openFile', (_e, type, id, ext) => openFile(type, id, ext))
ipcMain.handle('openDevTools', () => {
mainWindow?.webContents.openDevTools()
Expand Down
17 changes: 17 additions & 0 deletions src/renderer/floating.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" lang="zh" />
<title>Mihomo Party</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data:; frame-src http://127.0.0.1:*;"
/>
</head>

<body>
<div id="root"></div>
<script type="module" src="/src/floating.tsx"></script>
</body>
</html>
52 changes: 52 additions & 0 deletions src/renderer/src/FloatingApp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useEffect, useState } from 'react'
import MihomoIcon from './components/base/mihomo-icon'
import { calcTraffic } from './utils/calc'
import { showContextMenu, showMainWindow } from './utils/ipc'

const FloatingApp: React.FC = () => {
const [upload, setUpload] = useState(0)
const [download, setDownload] = useState(0)
useEffect(() => {
window.electron.ipcRenderer.on('mihomoTraffic', async (_e, info: IMihomoTrafficInfo) => {
setUpload(info.up)
setDownload(info.down)
})
return (): void => {
window.electron.ipcRenderer.removeAllListeners('mihomoTraffic')
}
}, [])
return (
<div className="app-drag p-[4px] h-[100vh]">
<div className="floating-bg shadow-md flex rounded-[calc(calc(100vh-8px)/2)] bg-content1 h-[calc(100vh-8px)] w-[calc(100vw-8px)]">
<div className="flex justify-center items-center h-full w-[calc(100vh-8px)]">
<div
onContextMenu={(e) => {
e.preventDefault()
showContextMenu()
}}
onClick={() => {
showMainWindow()
}}
className="app-nodrag cursor-pointer floating-thumb bg-primary hover:opacity-hover rounded-full h-[calc(100vh-14px)] w-[calc(100vh-14px)]"
>
<MihomoIcon className="floating-icon text-primary-foreground h-full leading-full text-[22px] mx-auto" />
</div>
</div>
<div className="flex flex-col justify-center w-[calc(100%-42px)]">
<div className="flex justify-end">
<div className="floating-text whitespace-nowrap overflow-hidden text-[12px] mr-[10px] font-bold">
{calcTraffic(upload)}/s
</div>
</div>
<div className="w-full flex justify-end">
<div className="floating-text whitespace-nowrap overflow-hidden text-[12px] mr-[10px] font-bold">
{calcTraffic(download)}/s
</div>
</div>
</div>
</div>
</div>
)
}

export default FloatingApp
19 changes: 19 additions & 0 deletions src/renderer/src/assets/floating.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

html {
background-color: transparent !important;
}

.app-nodrag {
-webkit-app-region: none;
}

.app-drag {
-webkit-app-region: drag;
}

* {
user-select: none;
}
17 changes: 17 additions & 0 deletions src/renderer/src/components/settings/general-config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import useSWR from 'swr'
import {
applyTheme,
checkAutoRun,
closeFloatingWindow,
copyEnv,
disableAutoRun,
enableAutoRun,
Expand All @@ -15,6 +16,7 @@ import {
importThemes,
relaunchApp,
resolveThemes,
showFloatingWindow,
startMonitor,
writeTheme
} from '@renderer/utils/ipc'
Expand All @@ -37,6 +39,7 @@ const GeneralConfig: React.FC = () => {
useDockIcon = true,
showTraffic = true,
proxyInTray = true,
showFloatingWindow: showFloating = false,
useWindowFrame = false,
autoQuitWithoutCore = false,
autoQuitWithoutCoreDelay = 60,
Expand Down Expand Up @@ -175,6 +178,20 @@ const GeneralConfig: React.FC = () => {
<SelectItem key="powershell">PowerShell</SelectItem>
</Select>
</SettingItem>
<SettingItem title="显示悬浮窗" divider>
<Switch
size="sm"
isSelected={showFloating}
onValueChange={async (v) => {
await patchAppConfig({ showFloatingWindow: v })
if (v) {
showFloatingWindow()
} else {
closeFloatingWindow()
}
}}
/>
</SettingItem>
{platform !== 'linux' && (
<>
<SettingItem title="托盘菜单显示节点信息" divider>
Expand Down
19 changes: 19 additions & 0 deletions src/renderer/src/floating.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
import { NextUIProvider } from '@nextui-org/react'
import '@renderer/assets/floating.css'
import FloatingApp from '@renderer/FloatingApp'
import BaseErrorBoundary from './components/base/base-error-boundary'

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<NextUIProvider>
<NextThemesProvider attribute="class" enableSystem defaultTheme="dark">
<BaseErrorBoundary>
<FloatingApp />
</BaseErrorBoundary>
</NextThemesProvider>
</NextUIProvider>
</React.StrictMode>
)
Loading

0 comments on commit ab971e8

Please sign in to comment.