mirror of
https://github.com/pompurin404/mihomo-party.git
synced 2024-11-15 19:22:31 +08:00
support floating window
This commit is contained in:
parent
0d9e28f8d1
commit
1b523c94f5
|
@ -1,7 +1,8 @@
|
||||||
### Breaking Changes
|
|
||||||
|
|
||||||
- 此版本修改了应用的显示名称,macOS用户可能无法自动更新,需要手动删除 `/Applications/mihomo-party.app`
|
|
||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
|
|
||||||
- 允许切换订阅卡片显示过期时间还是更新时间
|
- 允许切换订阅卡片显示过期时间还是更新时间
|
||||||
|
- 添加悬浮窗功能,可以在设置中开启
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- 修复某些 Windows 管理员权限无法正常启动的问题
|
||||||
|
|
|
@ -22,6 +22,14 @@ export default defineConfig({
|
||||||
plugins: [externalizeDepsPlugin()]
|
plugins: [externalizeDepsPlugin()]
|
||||||
},
|
},
|
||||||
renderer: {
|
renderer: {
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
index: resolve('src/renderer/index.html'),
|
||||||
|
floating: resolve('src/renderer/floating.html')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@renderer': resolve('src/renderer/src')
|
'@renderer': resolve('src/renderer/src')
|
||||||
|
|
|
@ -5,6 +5,7 @@ import WebSocket from 'ws'
|
||||||
import { tray } from '../resolve/tray'
|
import { tray } from '../resolve/tray'
|
||||||
import { calcTraffic } from '../utils/calc'
|
import { calcTraffic } from '../utils/calc'
|
||||||
import { getRuntimeConfig } from './factory'
|
import { getRuntimeConfig } from './factory'
|
||||||
|
import { floatingWindow } from '../resolve/floatingWindow'
|
||||||
|
|
||||||
let axiosIns: AxiosInstance = null!
|
let axiosIns: AxiosInstance = null!
|
||||||
let mihomoTrafficWs: WebSocket | null = null
|
let mihomoTrafficWs: WebSocket | null = null
|
||||||
|
@ -202,6 +203,7 @@ const mihomoTraffic = async (): Promise<void> => {
|
||||||
`${calcTraffic(json.down)}/s`.padStart(9)
|
`${calcTraffic(json.down)}/s`.padStart(9)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
floatingWindow?.webContents.send('mihomoTraffic', json)
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { existsSync, writeFileSync } from 'fs'
|
||||||
import { exePath, taskDir } from './utils/dirs'
|
import { exePath, taskDir } from './utils/dirs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { startMonitor } from './resolve/trafficMonitor'
|
import { startMonitor } from './resolve/trafficMonitor'
|
||||||
|
import { showFloatingWindow } from './resolve/floatingWindow'
|
||||||
|
|
||||||
let quitTimeout: NodeJS.Timeout | null = null
|
let quitTimeout: NodeJS.Timeout | null = null
|
||||||
export let mainWindow: BrowserWindow | null = null
|
export let mainWindow: BrowserWindow | null = null
|
||||||
|
@ -149,8 +150,12 @@ app.whenReady().then(async () => {
|
||||||
app.on('browser-window-created', (_, window) => {
|
app.on('browser-window-created', (_, window) => {
|
||||||
optimizer.watchWindowShortcuts(window)
|
optimizer.watchWindowShortcuts(window)
|
||||||
})
|
})
|
||||||
|
const { showFloatingWindow: showFloating = false } = await getAppConfig()
|
||||||
registerIpcMainHandlers()
|
registerIpcMainHandlers()
|
||||||
await createWindow()
|
await createWindow()
|
||||||
|
if (showFloating) {
|
||||||
|
showFloatingWindow()
|
||||||
|
}
|
||||||
await createTray()
|
await createTray()
|
||||||
await initShortcut()
|
await initShortcut()
|
||||||
app.on('activate', function () {
|
app.on('activate', function () {
|
||||||
|
@ -191,7 +196,8 @@ export async function createWindow(): Promise<void> {
|
||||||
const { useWindowFrame = false } = await getAppConfig()
|
const { useWindowFrame = false } = await getAppConfig()
|
||||||
const mainWindowState = windowStateKeeper({
|
const mainWindowState = windowStateKeeper({
|
||||||
defaultWidth: 800,
|
defaultWidth: 800,
|
||||||
defaultHeight: 600
|
defaultHeight: 600,
|
||||||
|
file: 'window-state.json'
|
||||||
})
|
})
|
||||||
// https://github.com/electron/electron/issues/16521#issuecomment-582955104
|
// https://github.com/electron/electron/issues/16521#issuecomment-582955104
|
||||||
Menu.setApplicationMenu(null)
|
Menu.setApplicationMenu(null)
|
||||||
|
@ -269,7 +275,6 @@ export async function createWindow(): Promise<void> {
|
||||||
shell.openExternal(details.url)
|
shell.openExternal(details.url)
|
||||||
return { action: 'deny' }
|
return { action: 'deny' }
|
||||||
})
|
})
|
||||||
|
|
||||||
// HMR for renderer base on electron-vite cli.
|
// HMR for renderer base on electron-vite cli.
|
||||||
// Load the remote URL for development or the local html file for production.
|
// Load the remote URL for development or the local html file for production.
|
||||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||||
|
@ -288,3 +293,9 @@ export function showMainWindow(): void {
|
||||||
mainWindow.focusOnWebView()
|
mainWindow.focusOnWebView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function closeMainWindow(): void {
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
75
src/main/resolve/floatingWindow.ts
Normal file
75
src/main/resolve/floatingWindow.ts
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import { is } from '@electron-toolkit/utils'
|
||||||
|
import { BrowserWindow, ipcMain } 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,
|
||||||
|
closable: 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)
|
||||||
|
})
|
||||||
|
ipcMain.on('updateFloatingWindow', () => {
|
||||||
|
if (floatingWindow) {
|
||||||
|
floatingWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||||
|
floatingWindow?.webContents.send('appConfigUpdated')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
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()
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import {
|
||||||
import { triggerSysProxy } from '../sys/sysproxy'
|
import { triggerSysProxy } from '../sys/sysproxy'
|
||||||
import { patchMihomoConfig } from '../core/mihomoApi'
|
import { patchMihomoConfig } from '../core/mihomoApi'
|
||||||
import { quitWithoutCore, restartCore } from '../core/manager'
|
import { quitWithoutCore, restartCore } from '../core/manager'
|
||||||
|
import { closeFloatingWindow, floatingWindow, showFloatingWindow } from './floatingWindow'
|
||||||
|
|
||||||
export async function registerShortcut(
|
export async function registerShortcut(
|
||||||
oldShortcut: string,
|
oldShortcut: string,
|
||||||
|
@ -31,6 +32,17 @@ export async function registerShortcut(
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
case 'showFloatingWindowShortcut': {
|
||||||
|
return globalShortcut.register(newShortcut, async () => {
|
||||||
|
if (floatingWindow) {
|
||||||
|
await patchAppConfig({ showFloatingWindow: false })
|
||||||
|
closeFloatingWindow()
|
||||||
|
} else {
|
||||||
|
await patchAppConfig({ showFloatingWindow: true })
|
||||||
|
showFloatingWindow()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
case 'triggerSysProxyShortcut': {
|
case 'triggerSysProxyShortcut': {
|
||||||
return globalShortcut.register(newShortcut, async () => {
|
return globalShortcut.register(newShortcut, async () => {
|
||||||
const {
|
const {
|
||||||
|
@ -42,10 +54,11 @@ export async function registerShortcut(
|
||||||
new Notification({
|
new Notification({
|
||||||
title: `系统代理已${!enable ? '开启' : '关闭'}`
|
title: `系统代理已${!enable ? '开启' : '关闭'}`
|
||||||
}).show()
|
}).show()
|
||||||
|
mainWindow?.webContents.send('appConfigUpdated')
|
||||||
|
floatingWindow?.webContents.send('appConfigUpdated')
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
mainWindow?.webContents.send('appConfigUpdated')
|
|
||||||
ipcMain.emit('updateTrayMenu')
|
ipcMain.emit('updateTrayMenu')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -64,10 +77,11 @@ export async function registerShortcut(
|
||||||
new Notification({
|
new Notification({
|
||||||
title: `虚拟网卡已${!enable ? '开启' : '关闭'}`
|
title: `虚拟网卡已${!enable ? '开启' : '关闭'}`
|
||||||
}).show()
|
}).show()
|
||||||
|
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||||
|
floatingWindow?.webContents.send('appConfigUpdated')
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
|
||||||
ipcMain.emit('updateTrayMenu')
|
ipcMain.emit('updateTrayMenu')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -122,6 +136,7 @@ export async function registerShortcut(
|
||||||
|
|
||||||
export async function initShortcut(): Promise<void> {
|
export async function initShortcut(): Promise<void> {
|
||||||
const {
|
const {
|
||||||
|
showFloatingWindowShortcut,
|
||||||
showWindowShortcut,
|
showWindowShortcut,
|
||||||
triggerSysProxyShortcut,
|
triggerSysProxyShortcut,
|
||||||
triggerTunShortcut,
|
triggerTunShortcut,
|
||||||
|
@ -138,6 +153,13 @@ export async function initShortcut(): Promise<void> {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (showFloatingWindowShortcut) {
|
||||||
|
try {
|
||||||
|
await registerShortcut('', showFloatingWindowShortcut, 'showFloatingWindowShortcut')
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
if (triggerSysProxyShortcut) {
|
if (triggerSysProxyShortcut) {
|
||||||
try {
|
try {
|
||||||
await registerShortcut('', triggerSysProxyShortcut, 'triggerSysProxyShortcut')
|
await registerShortcut('', triggerSysProxyShortcut, 'triggerSysProxyShortcut')
|
||||||
|
|
|
@ -6,8 +6,10 @@ import AdmZip from 'adm-zip'
|
||||||
import { getControledMihomoConfig } from '../config'
|
import { getControledMihomoConfig } from '../config'
|
||||||
import { existsSync } from 'fs'
|
import { existsSync } from 'fs'
|
||||||
import { mainWindow } from '..'
|
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 }[]> {
|
export async function resolveThemes(): Promise<{ key: string; label: string }[]> {
|
||||||
const files = await readdir(themesDir())
|
const files = await readdir(themesDir())
|
||||||
|
@ -67,6 +69,12 @@ export async function writeTheme(theme: string, css: string): Promise<void> {
|
||||||
|
|
||||||
export async function applyTheme(theme: string): Promise<void> {
|
export async function applyTheme(theme: string): Promise<void> {
|
||||||
const css = await readTheme(theme)
|
const css = await readTheme(theme)
|
||||||
await mainWindow?.webContents.removeInsertedCSS(insertedCSSKey || '')
|
await mainWindow?.webContents.removeInsertedCSS(insertedCSSKeyMain || '')
|
||||||
insertedCSSKey = await mainWindow?.webContents.insertCSS(css)
|
insertedCSSKeyMain = await mainWindow?.webContents.insertCSS(css)
|
||||||
|
try {
|
||||||
|
await floatingWindow?.webContents.removeInsertedCSS(insertedCSSKeyFloating || '')
|
||||||
|
insertedCSSKeyFloating = await floatingWindow?.webContents.insertCSS(css)
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,15 +15,16 @@ import {
|
||||||
mihomoGroups,
|
mihomoGroups,
|
||||||
patchMihomoConfig
|
patchMihomoConfig
|
||||||
} from '../core/mihomoApi'
|
} from '../core/mihomoApi'
|
||||||
import { mainWindow, showMainWindow } from '..'
|
import { closeMainWindow, mainWindow, showMainWindow } from '..'
|
||||||
import { app, clipboard, ipcMain, Menu, nativeImage, shell, Tray } from 'electron'
|
import { app, clipboard, ipcMain, Menu, nativeImage, shell, Tray } from 'electron'
|
||||||
import { dataDir, logDir, mihomoCoreDir, mihomoWorkDir } from '../utils/dirs'
|
import { dataDir, logDir, mihomoCoreDir, mihomoWorkDir } from '../utils/dirs'
|
||||||
import { triggerSysProxy } from '../sys/sysproxy'
|
import { triggerSysProxy } from '../sys/sysproxy'
|
||||||
import { quitWithoutCore, restartCore } from '../core/manager'
|
import { quitWithoutCore, restartCore } from '../core/manager'
|
||||||
|
import { closeFloatingWindow, floatingWindow, showFloatingWindow } from './floatingWindow'
|
||||||
|
|
||||||
export let tray: Tray | null = null
|
export let tray: Tray | null = null
|
||||||
|
|
||||||
const buildContextMenu = async (): Promise<Menu> => {
|
export const buildContextMenu = async (): Promise<Menu> => {
|
||||||
const { mode, tun } = await getControledMihomoConfig()
|
const { mode, tun } = await getControledMihomoConfig()
|
||||||
const {
|
const {
|
||||||
sysProxy,
|
sysProxy,
|
||||||
|
@ -31,6 +32,7 @@ const buildContextMenu = async (): Promise<Menu> => {
|
||||||
autoCloseConnection,
|
autoCloseConnection,
|
||||||
proxyInTray = true,
|
proxyInTray = true,
|
||||||
triggerSysProxyShortcut = '',
|
triggerSysProxyShortcut = '',
|
||||||
|
showFloatingWindowShortcut = '',
|
||||||
showWindowShortcut = '',
|
showWindowShortcut = '',
|
||||||
triggerTunShortcut = '',
|
triggerTunShortcut = '',
|
||||||
ruleModeShortcut = '',
|
ruleModeShortcut = '',
|
||||||
|
@ -90,6 +92,21 @@ const buildContextMenu = async (): Promise<Menu> => {
|
||||||
showMainWindow()
|
showMainWindow()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'show-floating',
|
||||||
|
accelerator: showFloatingWindowShortcut,
|
||||||
|
label: floatingWindow?.isVisible() ? '关闭悬浮窗' : '显示悬浮窗',
|
||||||
|
type: 'normal',
|
||||||
|
click: async (): Promise<void> => {
|
||||||
|
if (floatingWindow) {
|
||||||
|
await patchAppConfig({ showFloatingWindow: false })
|
||||||
|
closeFloatingWindow()
|
||||||
|
} else {
|
||||||
|
await patchAppConfig({ showFloatingWindow: true })
|
||||||
|
showFloatingWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'rule',
|
id: 'rule',
|
||||||
label: '规则模式',
|
label: '规则模式',
|
||||||
|
@ -140,10 +157,11 @@ const buildContextMenu = async (): Promise<Menu> => {
|
||||||
try {
|
try {
|
||||||
await triggerSysProxy(enable)
|
await triggerSysProxy(enable)
|
||||||
await patchAppConfig({ sysProxy: { enable } })
|
await patchAppConfig({ sysProxy: { enable } })
|
||||||
|
mainWindow?.webContents.send('appConfigUpdated')
|
||||||
|
floatingWindow?.webContents.send('appConfigUpdated')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
mainWindow?.webContents.send('appConfigUpdated')
|
|
||||||
ipcMain.emit('updateTrayMenu')
|
ipcMain.emit('updateTrayMenu')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -155,14 +173,20 @@ const buildContextMenu = async (): Promise<Menu> => {
|
||||||
checked: tun?.enable ?? false,
|
checked: tun?.enable ?? false,
|
||||||
click: async (item): Promise<void> => {
|
click: async (item): Promise<void> => {
|
||||||
const enable = item.checked
|
const enable = item.checked
|
||||||
if (enable) {
|
try {
|
||||||
await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } })
|
if (enable) {
|
||||||
} else {
|
await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } })
|
||||||
await patchControledMihomoConfig({ tun: { enable } })
|
} else {
|
||||||
|
await patchControledMihomoConfig({ tun: { enable } })
|
||||||
|
}
|
||||||
|
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||||
|
floatingWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||||
|
await restartCore()
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
ipcMain.emit('updateTrayMenu')
|
||||||
}
|
}
|
||||||
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
|
||||||
await restartCore()
|
|
||||||
ipcMain.emit('updateTrayMenu')
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
...groupsMenu,
|
...groupsMenu,
|
||||||
|
@ -291,7 +315,7 @@ export async function createTray(): Promise<void> {
|
||||||
})
|
})
|
||||||
tray?.addListener('right-click', async () => {
|
tray?.addListener('right-click', async () => {
|
||||||
if (mainWindow?.isVisible()) {
|
if (mainWindow?.isVisible()) {
|
||||||
mainWindow?.close()
|
closeMainWindow()
|
||||||
} else {
|
} else {
|
||||||
showMainWindow()
|
showMainWindow()
|
||||||
}
|
}
|
||||||
|
@ -303,7 +327,7 @@ export async function createTray(): Promise<void> {
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
tray?.addListener('click', () => {
|
tray?.addListener('click', () => {
|
||||||
if (mainWindow?.isVisible()) {
|
if (mainWindow?.isVisible()) {
|
||||||
mainWindow?.close()
|
closeMainWindow()
|
||||||
} else {
|
} else {
|
||||||
showMainWindow()
|
showMainWindow()
|
||||||
}
|
}
|
||||||
|
@ -315,7 +339,7 @@ export async function createTray(): Promise<void> {
|
||||||
if (process.platform === 'linux') {
|
if (process.platform === 'linux') {
|
||||||
tray?.addListener('click', () => {
|
tray?.addListener('click', () => {
|
||||||
if (mainWindow?.isVisible()) {
|
if (mainWindow?.isVisible()) {
|
||||||
mainWindow?.close()
|
closeMainWindow()
|
||||||
} else {
|
} else {
|
||||||
showMainWindow()
|
showMainWindow()
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ import { listWebdavBackups, webdavBackup, webdavDelete, webdavRestore } from '..
|
||||||
import { getInterfaces } from '../sys/interface'
|
import { getInterfaces } from '../sys/interface'
|
||||||
import { copyEnv } from '../resolve/tray'
|
import { copyEnv } from '../resolve/tray'
|
||||||
import { registerShortcut } from '../resolve/shortcut'
|
import { registerShortcut } from '../resolve/shortcut'
|
||||||
import { mainWindow } from '..'
|
import { closeMainWindow, mainWindow, showMainWindow } from '..'
|
||||||
import {
|
import {
|
||||||
applyTheme,
|
applyTheme,
|
||||||
fetchThemes,
|
fetchThemes,
|
||||||
|
@ -81,6 +81,7 @@ import v8 from 'v8'
|
||||||
import { getGistUrl } from '../resolve/gistApi'
|
import { getGistUrl } from '../resolve/gistApi'
|
||||||
import { getImageDataURL } from './image'
|
import { getImageDataURL } from './image'
|
||||||
import { startMonitor } from '../resolve/trafficMonitor'
|
import { startMonitor } from '../resolve/trafficMonitor'
|
||||||
|
import { closeFloatingWindow, showContextMenu, showFloatingWindow } from '../resolve/floatingWindow'
|
||||||
|
|
||||||
function ipcErrorWrapper<T>( // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
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
|
fn: (...args: any[]) => Promise<T> // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
@ -209,6 +210,11 @@ export function registerIpcMainHandlers(): void {
|
||||||
ipcMain.handle('isAlwaysOnTop', () => {
|
ipcMain.handle('isAlwaysOnTop', () => {
|
||||||
return mainWindow?.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('openFile', (_e, type, id, ext) => openFile(type, id, ext))
|
||||||
ipcMain.handle('openDevTools', () => {
|
ipcMain.handle('openDevTools', () => {
|
||||||
mainWindow?.webContents.openDevTools()
|
mainWindow?.webContents.openDevTools()
|
||||||
|
|
17
src/renderer/floating.html
Normal file
17
src/renderer/floating.html
Normal file
|
@ -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>
|
61
src/renderer/src/FloatingApp.tsx
Normal file
61
src/renderer/src/FloatingApp.tsx
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import MihomoIcon from './components/base/mihomo-icon'
|
||||||
|
import { calcTraffic } from './utils/calc'
|
||||||
|
import { showContextMenu, showMainWindow } from './utils/ipc'
|
||||||
|
import { useAppConfig } from './hooks/use-app-config'
|
||||||
|
import { useControledMihomoConfig } from './hooks/use-controled-mihomo-config'
|
||||||
|
|
||||||
|
const FloatingApp: React.FC = () => {
|
||||||
|
const { appConfig } = useAppConfig()
|
||||||
|
const { controledMihomoConfig } = useControledMihomoConfig()
|
||||||
|
const { sysProxy } = appConfig || {}
|
||||||
|
const { tun } = controledMihomoConfig || {}
|
||||||
|
const sysProxyEnabled = sysProxy?.enable
|
||||||
|
const tunEnabled = tun?.enable
|
||||||
|
|
||||||
|
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 drop-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 ${tunEnabled ? 'bg-secondary' : sysProxyEnabled ? 'bg-primary' : 'bg-default'} 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
|
20
src/renderer/src/assets/floating.css
Normal file
20
src/renderer/src/assets/floating.css
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
html {
|
||||||
|
background: none !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-nodrag {
|
||||||
|
-webkit-app-region: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-drag {
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
user-select: none;
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import useSWR from 'swr'
|
||||||
import {
|
import {
|
||||||
applyTheme,
|
applyTheme,
|
||||||
checkAutoRun,
|
checkAutoRun,
|
||||||
|
closeFloatingWindow,
|
||||||
copyEnv,
|
copyEnv,
|
||||||
disableAutoRun,
|
disableAutoRun,
|
||||||
enableAutoRun,
|
enableAutoRun,
|
||||||
|
@ -15,6 +16,7 @@ import {
|
||||||
importThemes,
|
importThemes,
|
||||||
relaunchApp,
|
relaunchApp,
|
||||||
resolveThemes,
|
resolveThemes,
|
||||||
|
showFloatingWindow,
|
||||||
startMonitor,
|
startMonitor,
|
||||||
writeTheme
|
writeTheme
|
||||||
} from '@renderer/utils/ipc'
|
} from '@renderer/utils/ipc'
|
||||||
|
@ -37,6 +39,7 @@ const GeneralConfig: React.FC = () => {
|
||||||
useDockIcon = true,
|
useDockIcon = true,
|
||||||
showTraffic = true,
|
showTraffic = true,
|
||||||
proxyInTray = true,
|
proxyInTray = true,
|
||||||
|
showFloatingWindow: showFloating = false,
|
||||||
useWindowFrame = false,
|
useWindowFrame = false,
|
||||||
autoQuitWithoutCore = false,
|
autoQuitWithoutCore = false,
|
||||||
autoQuitWithoutCoreDelay = 60,
|
autoQuitWithoutCoreDelay = 60,
|
||||||
|
@ -175,6 +178,20 @@ const GeneralConfig: React.FC = () => {
|
||||||
<SelectItem key="powershell">PowerShell</SelectItem>
|
<SelectItem key="powershell">PowerShell</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
<SettingItem title="显示悬浮窗" divider>
|
||||||
|
<Switch
|
||||||
|
size="sm"
|
||||||
|
isSelected={showFloating}
|
||||||
|
onValueChange={async (v) => {
|
||||||
|
await patchAppConfig({ showFloatingWindow: v })
|
||||||
|
if (v) {
|
||||||
|
showFloatingWindow()
|
||||||
|
} else {
|
||||||
|
closeFloatingWindow()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
{platform !== 'linux' && (
|
{platform !== 'linux' && (
|
||||||
<>
|
<>
|
||||||
<SettingItem title="托盘菜单显示节点信息" divider>
|
<SettingItem title="托盘菜单显示节点信息" divider>
|
||||||
|
|
|
@ -43,6 +43,7 @@ const ShortcutConfig: React.FC = () => {
|
||||||
const { appConfig, patchAppConfig } = useAppConfig()
|
const { appConfig, patchAppConfig } = useAppConfig()
|
||||||
const {
|
const {
|
||||||
showWindowShortcut = '',
|
showWindowShortcut = '',
|
||||||
|
showFloatingWindowShortcut = '',
|
||||||
triggerSysProxyShortcut = '',
|
triggerSysProxyShortcut = '',
|
||||||
triggerTunShortcut = '',
|
triggerTunShortcut = '',
|
||||||
ruleModeShortcut = '',
|
ruleModeShortcut = '',
|
||||||
|
@ -63,6 +64,15 @@ const ShortcutConfig: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
<SettingItem title="打开/关闭悬浮窗" divider>
|
||||||
|
<div className="flex justify-end w-[60%]">
|
||||||
|
<ShortcutInput
|
||||||
|
value={showFloatingWindowShortcut}
|
||||||
|
patchAppConfig={patchAppConfig}
|
||||||
|
action="showFloatingWindowShortcut"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingItem>
|
||||||
<SettingItem title="打开/关闭系统代理" divider>
|
<SettingItem title="打开/关闭系统代理" divider>
|
||||||
<div className="flex justify-end w-[60%]">
|
<div className="flex justify-end w-[60%]">
|
||||||
<ShortcutInput
|
<ShortcutInput
|
||||||
|
|
|
@ -29,6 +29,7 @@ const SysproxySwitcher: React.FC = () => {
|
||||||
try {
|
try {
|
||||||
await triggerSysProxy(enable)
|
await triggerSysProxy(enable)
|
||||||
await patchAppConfig({ sysProxy: { enable } })
|
await patchAppConfig({ sysProxy: { enable } })
|
||||||
|
window.electron.ipcRenderer.send('updateFloatingWindow')
|
||||||
window.electron.ipcRenderer.send('updateTrayMenu')
|
window.electron.ipcRenderer.send('updateTrayMenu')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(e)
|
alert(e)
|
||||||
|
|
|
@ -51,6 +51,7 @@ const TunSwitcher: React.FC = () => {
|
||||||
await patchControledMihomoConfig({ tun: { enable } })
|
await patchControledMihomoConfig({ tun: { enable } })
|
||||||
}
|
}
|
||||||
await restartCore()
|
await restartCore()
|
||||||
|
window.electron.ipcRenderer.send('updateFloatingWindow')
|
||||||
window.electron.ipcRenderer.send('updateTrayMenu')
|
window.electron.ipcRenderer.send('updateTrayMenu')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
25
src/renderer/src/floating.tsx
Normal file
25
src/renderer/src/floating.tsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
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'
|
||||||
|
import { AppConfigProvider } from './hooks/use-app-config'
|
||||||
|
import { ControledMihomoConfigProvider } from './hooks/use-controled-mihomo-config'
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<NextUIProvider>
|
||||||
|
<NextThemesProvider attribute="class" enableSystem defaultTheme="dark">
|
||||||
|
<BaseErrorBoundary>
|
||||||
|
<AppConfigProvider>
|
||||||
|
<ControledMihomoConfigProvider>
|
||||||
|
<FloatingApp />
|
||||||
|
</ControledMihomoConfigProvider>
|
||||||
|
</AppConfigProvider>
|
||||||
|
</BaseErrorBoundary>
|
||||||
|
</NextThemesProvider>
|
||||||
|
</NextUIProvider>
|
||||||
|
</React.StrictMode>
|
||||||
|
)
|
|
@ -327,6 +327,26 @@ export async function subStoreCollections(): Promise<ISubStoreSub[]> {
|
||||||
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('subStoreCollections'))
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('subStoreCollections'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function showMainWindow(): Promise<void> {
|
||||||
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('showMainWindow'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function closeMainWindow(): Promise<void> {
|
||||||
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('closeMainWindow'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function showFloatingWindow(): Promise<void> {
|
||||||
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('showFloatingWindow'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function closeFloatingWindow(): Promise<void> {
|
||||||
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('closeFloatingWindow'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function showContextMenu(): Promise<void> {
|
||||||
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('showContextMenu'))
|
||||||
|
}
|
||||||
|
|
||||||
export async function openFile(
|
export async function openFile(
|
||||||
type: 'profile' | 'override',
|
type: 'profile' | 'override',
|
||||||
id: string,
|
id: string,
|
||||||
|
|
2
src/shared/types.d.ts
vendored
2
src/shared/types.d.ts
vendored
|
@ -212,6 +212,7 @@ interface IAppConfig {
|
||||||
proxyCols: 'auto' | '1' | '2' | '3' | '4'
|
proxyCols: 'auto' | '1' | '2' | '3' | '4'
|
||||||
connectionDirection: 'asc' | 'desc'
|
connectionDirection: 'asc' | 'desc'
|
||||||
connectionOrderBy: 'time' | 'upload' | 'download' | 'uploadSpeed' | 'downloadSpeed'
|
connectionOrderBy: 'time' | 'upload' | 'download' | 'uploadSpeed' | 'downloadSpeed'
|
||||||
|
showFloatingWindow?: boolean
|
||||||
connectionCardStatus?: CardStatus
|
connectionCardStatus?: CardStatus
|
||||||
dnsCardStatus?: CardStatus
|
dnsCardStatus?: CardStatus
|
||||||
logCardStatus?: CardStatus
|
logCardStatus?: CardStatus
|
||||||
|
@ -262,6 +263,7 @@ interface IAppConfig {
|
||||||
useNameserverPolicy: boolean
|
useNameserverPolicy: boolean
|
||||||
nameserverPolicy: { [key: string]: string | string[] }
|
nameserverPolicy: { [key: string]: string | string[] }
|
||||||
showWindowShortcut?: string
|
showWindowShortcut?: string
|
||||||
|
showFloatingWindowShortcut?: string
|
||||||
triggerSysProxyShortcut?: string
|
triggerSysProxyShortcut?: string
|
||||||
triggerTunShortcut?: string
|
triggerTunShortcut?: string
|
||||||
ruleModeShortcut?: string
|
ruleModeShortcut?: string
|
||||||
|
|
Loading…
Reference in New Issue
Block a user