53 changed files with 2504 additions and 495 deletions
@ -0,0 +1,12 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="en"> |
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
|
<title>Document</title> |
||||
|
</head> |
||||
|
<body> |
||||
|
dsada |
||||
|
<a href="https://baidu.com" target="_blank">百度</a> |
||||
|
</body> |
||||
|
</html> |
@ -0,0 +1,165 @@ |
|||||
|
import { inject, injectable } from "inversify" |
||||
|
// import Setting from "./modules/setting"
|
||||
|
// import DB from "./modules/db"
|
||||
|
import Api from "./modules/api" |
||||
|
import WindowManager from "./modules/window-manager" |
||||
|
import { app, nativeTheme, protocol, WebContentsView } from "electron" |
||||
|
import { electronApp } from "@electron-toolkit/utils" |
||||
|
import Tabs from "./modules/tabs/Tabs" |
||||
|
import { getFileUrl } from "./utils" |
||||
|
import BaseClass from "./base/base" |
||||
|
|
||||
|
protocol.registerSchemesAsPrivileged([ |
||||
|
// {
|
||||
|
// scheme: "http",
|
||||
|
// privileges: { standard: true, bypassCSP: true, allowServiceWorkers: true, supportFetchAPI: true, corsEnabled: true, stream: true },
|
||||
|
// },
|
||||
|
// {
|
||||
|
// scheme: "https",
|
||||
|
// privileges: { standard: true, bypassCSP: true, allowServiceWorkers: true, supportFetchAPI: true, corsEnabled: true, stream: true },
|
||||
|
// },
|
||||
|
// { scheme: "mailto", privileges: { standard: true } },
|
||||
|
{ |
||||
|
scheme: "api", |
||||
|
privileges: { |
||||
|
standard: true, |
||||
|
secure: true, |
||||
|
supportFetchAPI: true, |
||||
|
}, |
||||
|
}, |
||||
|
]) |
||||
|
|
||||
|
@injectable() |
||||
|
class App extends BaseClass { |
||||
|
destroy() { |
||||
|
// destroyAll()
|
||||
|
// 这里是应用正常退出
|
||||
|
} |
||||
|
// private _setting: Setting
|
||||
|
// private _db: DB
|
||||
|
private _Api: Api |
||||
|
private _windowManager: WindowManager |
||||
|
private _tabs: Tabs |
||||
|
|
||||
|
constructor( |
||||
|
// @inject(Setting) setting: Setting,
|
||||
|
// @inject(DB) db: DB,
|
||||
|
@inject(Api) Api: Api, |
||||
|
@inject(WindowManager) windowManager: WindowManager, |
||||
|
@inject(Tabs) tabs: Tabs, |
||||
|
) { |
||||
|
super() |
||||
|
// this._setting = setting
|
||||
|
// this._db = db
|
||||
|
this._Api = Api |
||||
|
this._windowManager = windowManager |
||||
|
this._tabs = tabs |
||||
|
} |
||||
|
|
||||
|
async init() { |
||||
|
this._windowManager.init() |
||||
|
app.whenReady().then(() => { |
||||
|
electronApp.setAppUserModelId("top.xieyaxin") |
||||
|
this.create() |
||||
|
this._Api.init() |
||||
|
}) |
||||
|
app.on("window-all-closed", () => { |
||||
|
if (process.platform !== "darwin") { |
||||
|
app.quit() |
||||
|
} |
||||
|
}) |
||||
|
app.on("will-quit", () => { |
||||
|
this.destroy() |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
create() { |
||||
|
this._windowManager.showMainWindow() |
||||
|
const mainWindow = this._windowManager.getMainWindow() |
||||
|
if (mainWindow) { |
||||
|
nativeTheme.themeSource = "light" |
||||
|
mainWindow.setTitleBarOverlay({ |
||||
|
height: 29, // the smallest size of the title bar on windows accounting for the border on windows 11
|
||||
|
color: "#F8F8F8", |
||||
|
symbolColor: "#000000", |
||||
|
}) |
||||
|
this._windowManager.showWindow("main-top") |
||||
|
const mainTopWindow = this._windowManager.get("main-top") |
||||
|
setTimeout(() => { |
||||
|
// console.log(mainWindow.getParentWindow());
|
||||
|
setTimeout(() => { |
||||
|
mainWindow.contentView.children.length = 0 |
||||
|
const view = new WebContentsView() |
||||
|
view.addChildView(mainTopWindow!.contentView) |
||||
|
view.webContents.loadURL(getFileUrl("about.html")) |
||||
|
// mainTopWindow!.contentView.setBounds({ x: 0, y: 0, width: 100, height: 30 })
|
||||
|
// view.setBounds({ x: 0, y: 0, width: 100, height: 30 })
|
||||
|
mainWindow.contentView.addChildView(view) |
||||
|
// mainWindow.contentView.children.sort()
|
||||
|
console.log(mainWindow.contentView.children) |
||||
|
}, 5000) |
||||
|
// mainWindow.webContents = mainTopWindow!.webContents
|
||||
|
mainWindow.reload() |
||||
|
console.log(mainWindow.webContents.getURL()) |
||||
|
|
||||
|
// mainTopWindow?.destroy()
|
||||
|
// mainWindow.contentView.addChildView(mainWindow.contentView)
|
||||
|
console.log(`child count: `, mainWindow.contentView.children.length) |
||||
|
}, 2000) |
||||
|
// if (mainTopWindow) {
|
||||
|
// mainTopWindow.setParentWindow(mainWindow)
|
||||
|
// mainTopWindow.setIgnoreMouseEvents(true, { forward: false })
|
||||
|
// const listenMove = () => {
|
||||
|
// if (mainWindow && mainTopWindow) {
|
||||
|
// const pos = mainWindow.getPosition()
|
||||
|
// mainTopWindow.setPosition(pos[0], pos[1])
|
||||
|
// }
|
||||
|
// }
|
||||
|
// mainWindow?.on("move", listenMove)
|
||||
|
// const listenResize = () => {
|
||||
|
// if (mainWindow && mainTopWindow) {
|
||||
|
// const size = mainWindow.getSize()
|
||||
|
// console.log(size)
|
||||
|
// mainTopWindow.setSize(size[0], size[1])
|
||||
|
// const pos = mainWindow.getPosition()
|
||||
|
// mainTopWindow.setPosition(pos[0], pos[1])
|
||||
|
// }
|
||||
|
// }
|
||||
|
// listenResize()
|
||||
|
// mainWindow?.on("resize", listenResize)
|
||||
|
// }
|
||||
|
} |
||||
|
// 考虑双browserwindow模式
|
||||
|
/** |
||||
|
* 因为browserwindow可以设置穿透,考虑将tab放在底层window上,其他组件放在上层window上。 |
||||
|
*/ |
||||
|
// const webContentsView = new WebContentsView({
|
||||
|
// webPreferences: {
|
||||
|
// preload: join(__dirname, "../preload/index.mjs"),
|
||||
|
// transparent: true,
|
||||
|
// nodeIntegration: true,
|
||||
|
// spellcheck: false,
|
||||
|
// contextIsolation: true,
|
||||
|
// },
|
||||
|
// })
|
||||
|
// // mainWindow!.contentView = webContentsView
|
||||
|
// // setTimeout(() => {
|
||||
|
// mainWindow!.contentView.addChildView(webContentsView)
|
||||
|
// // mainWindow?.setIgnoreMouseEvents(true, { forward: true })
|
||||
|
// // }, 2000);
|
||||
|
// webContentsView.webContents.loadURL(getFileUrl("index.html"))
|
||||
|
// const listenResize = () => {
|
||||
|
// const size = mainWindow!.getSize()
|
||||
|
// webContentsView.setBounds({ x: 0, y: 0, width: size[0], height: size[1] })
|
||||
|
// }
|
||||
|
// listenResize()
|
||||
|
// mainWindow!.addListener("resize", listenResize)
|
||||
|
|
||||
|
this._tabs.add("https://baidu.com", true) |
||||
|
this._tabs.add("https://zhihu.com") |
||||
|
return mainWindow |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default App |
||||
|
export { App } |
@ -0,0 +1,28 @@ |
|||||
|
import IOC from "./_iocClass" |
||||
|
import { Container } from "inversify" |
||||
|
import iocModules, { destroyAllModules } from "./modules/_ioc" |
||||
|
import iocController, { destroyAllController } from "./controller/_ioc" |
||||
|
import iocCommand, { destroyAllCommand } from "./commands/_ioc" |
||||
|
import App from "./App" |
||||
|
|
||||
|
async function destroyAll() { |
||||
|
await destroyAllModules(_ioc) |
||||
|
await destroyAllController(_ioc) |
||||
|
await destroyAllCommand(_ioc) |
||||
|
} |
||||
|
|
||||
|
const _modulesIOC = new Container() |
||||
|
_modulesIOC.load(iocModules) |
||||
|
|
||||
|
const _commandIOC = _modulesIOC.createChild() |
||||
|
_commandIOC.load(iocCommand) |
||||
|
|
||||
|
const _controllerIOC = _commandIOC.createChild() |
||||
|
_controllerIOC.load(iocController) |
||||
|
|
||||
|
const _ioc = _controllerIOC.createChild() |
||||
|
_ioc.bind(IOC).toSelf().inSingletonScope() |
||||
|
_ioc.bind(App).toSelf().inSingletonScope() |
||||
|
|
||||
|
export { IOC, destroyAll, _ioc } |
||||
|
export default IOC |
@ -0,0 +1,20 @@ |
|||||
|
import { interfaces } from "inversify" |
||||
|
import BaseClass from "./base/base" |
||||
|
import { destroyAll, _ioc } from "./_ioc" |
||||
|
|
||||
|
class IOC extends BaseClass { |
||||
|
destroy() { |
||||
|
destroyAll() |
||||
|
} |
||||
|
|
||||
|
get<T = unknown>(serviceIdentifier: interfaces.ServiceIdentifier<T>) { |
||||
|
return _ioc.get<T>(serviceIdentifier) |
||||
|
} |
||||
|
|
||||
|
getAsync<T = unknown>(serviceIdentifier: interfaces.ServiceIdentifier<T>) { |
||||
|
return _ioc.getAsync<T>(serviceIdentifier) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export { IOC } |
||||
|
export default IOC |
@ -1,4 +1,10 @@ |
|||||
abstract class Base {} |
import EventEmitter from "node:events" |
||||
|
|
||||
export { Base } |
abstract class BaseClass { |
||||
export default Base |
public _events = new EventEmitter() |
||||
|
abstract init(...argus: any[]) |
||||
|
abstract destroy() |
||||
|
} |
||||
|
|
||||
|
export { BaseClass } |
||||
|
export default BaseClass |
||||
|
@ -0,0 +1,4 @@ |
|||||
|
abstract class BaseContainer {} |
||||
|
|
||||
|
export { BaseContainer } |
||||
|
export default BaseContainer |
@ -0,0 +1,7 @@ |
|||||
|
export default class BasicCommand { |
||||
|
static name: string = "BasicCommand" |
||||
|
|
||||
|
log() { |
||||
|
console.log("1231") |
||||
|
} |
||||
|
} |
@ -0,0 +1,59 @@ |
|||||
|
import { inject } from "inversify" |
||||
|
import Tabs from "vc/modules/tabs" |
||||
|
import WindowManager from "vc/modules/window-manager" |
||||
|
import { broadcast } from "vc/utils" |
||||
|
|
||||
|
class TabsCommand { |
||||
|
static name: string = "TabsCommand" |
||||
|
|
||||
|
constructor( |
||||
|
@inject(Tabs) private _Tabs: Tabs, |
||||
|
@inject(WindowManager) private _WindowManager: WindowManager, |
||||
|
) { |
||||
|
this.listenerTabActive = this.listenerTabActive.bind(this) |
||||
|
this._Tabs.events.addListener("tab-active", this.listenerTabActive) |
||||
|
} |
||||
|
|
||||
|
reload() { |
||||
|
this._WindowManager.getMainWindow()?.reload() |
||||
|
} |
||||
|
|
||||
|
sync() { |
||||
|
this.listenerTabActive() |
||||
|
} |
||||
|
|
||||
|
listenerTabActive() { |
||||
|
broadcast("TabsCommand.update", this.getAllTabs()) |
||||
|
} |
||||
|
|
||||
|
add(url) { |
||||
|
this._Tabs.add(url, true, this._WindowManager.getMainWindow()!) |
||||
|
} |
||||
|
|
||||
|
nagivate(index: number, url: string) { |
||||
|
console.log(`跳转${index}:${url}`) |
||||
|
|
||||
|
this._Tabs.navigate(+index, url) |
||||
|
} |
||||
|
|
||||
|
setActive(index) { |
||||
|
this._Tabs.changeActive(index) |
||||
|
} |
||||
|
|
||||
|
closeTab(e) { |
||||
|
this._Tabs.remove(e.body.active) |
||||
|
} |
||||
|
|
||||
|
getAllTabs() { |
||||
|
return this._Tabs._tabs.map(v => ({ |
||||
|
url: v.url, |
||||
|
showUrl: v.showUrl, |
||||
|
title: v.title, |
||||
|
favicons: v.favicons, |
||||
|
isActive: v.isActive, |
||||
|
})) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export { TabsCommand } |
||||
|
export default TabsCommand |
@ -0,0 +1,15 @@ |
|||||
|
import { Container, ContainerModule } from "inversify" |
||||
|
import BasicCommand from "./BasicCommand" |
||||
|
import TabsCommand from "./TabsCommand" |
||||
|
|
||||
|
const modules = new ContainerModule(bind => { |
||||
|
bind(BasicCommand.name).to(BasicCommand).inSingletonScope() |
||||
|
bind(TabsCommand.name).to(TabsCommand).inSingletonScope() |
||||
|
}) |
||||
|
|
||||
|
async function destroyAllCommand(ioc: Container) { |
||||
|
await ioc.unloadAsync(modules) |
||||
|
} |
||||
|
|
||||
|
export { modules, destroyAllCommand } |
||||
|
export default modules |
@ -0,0 +1,31 @@ |
|||||
|
import { inject, injectable } from "inversify" |
||||
|
import BaseContainer from "vc/base/baseContainer" |
||||
|
import Tabs from "vc/modules/tabs" |
||||
|
import WindowManager from "vc/modules/window-manager" |
||||
|
|
||||
|
@injectable() |
||||
|
class BasicService extends BaseContainer { |
||||
|
static name: string = "BasicService" |
||||
|
|
||||
|
constructor( |
||||
|
@inject(WindowManager) private _WindowManager: WindowManager, |
||||
|
@inject(Tabs) private _Tabs: Tabs, |
||||
|
) { |
||||
|
super() |
||||
|
} |
||||
|
|
||||
|
showAbout() { |
||||
|
this._WindowManager.showWindow("about") |
||||
|
return { |
||||
|
a: "fuck", |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
openTabDevtool() { |
||||
|
// this._Tabs.reload(0)
|
||||
|
this._Tabs.openDevtool(0) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export { BasicService } |
||||
|
export default BasicService |
@ -0,0 +1,45 @@ |
|||||
|
import { inject, injectable } from "inversify" |
||||
|
import BaseContainer from "vc/base/baseContainer" |
||||
|
import Tabs from "vc/modules/tabs" |
||||
|
import WindowManager from "vc/modules/window-manager" |
||||
|
|
||||
|
@injectable() |
||||
|
class TabsService extends BaseContainer { |
||||
|
static name: string = "TabsService" |
||||
|
|
||||
|
constructor( |
||||
|
@inject(Tabs) private _Tabs: Tabs, |
||||
|
@inject(WindowManager) private _WindowManager: WindowManager, |
||||
|
) { |
||||
|
super() |
||||
|
} |
||||
|
|
||||
|
add(e) { |
||||
|
this._Tabs.add(e.body.url, true, this._WindowManager.getMainWindow()!) |
||||
|
} |
||||
|
|
||||
|
setActive(e) { |
||||
|
this._Tabs.changeActive(e.body.active) |
||||
|
} |
||||
|
|
||||
|
closeTab(e) { |
||||
|
this._Tabs.remove(e.body.active) |
||||
|
} |
||||
|
|
||||
|
closeTabAll(e) { |
||||
|
this._Tabs.removeAll(e.body.active) |
||||
|
} |
||||
|
|
||||
|
getAllTabs() { |
||||
|
return this._Tabs._tabs.map(v => ({ |
||||
|
url: v.url, |
||||
|
showUrl: v.showUrl, |
||||
|
title: v.title, |
||||
|
favicons: v.favicons, |
||||
|
isActive: v.isActive, |
||||
|
})) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export { TabsService } |
||||
|
export default TabsService |
@ -0,0 +1,15 @@ |
|||||
|
import { Container, ContainerModule } from "inversify" |
||||
|
import BasicService from "./BasicService" |
||||
|
import TabsService from "./TabsService" |
||||
|
|
||||
|
const modules = new ContainerModule(bind => { |
||||
|
bind(BasicService.name).to(BasicService).inSingletonScope() |
||||
|
bind(TabsService.name).to(TabsService).inSingletonScope() |
||||
|
}) |
||||
|
|
||||
|
async function destroyAllController(ioc: Container) { |
||||
|
await ioc.unloadAsync(modules) |
||||
|
} |
||||
|
|
||||
|
export { modules, destroyAllController } |
||||
|
export default modules |
@ -1,79 +1,6 @@ |
|||||
import "reflect-metadata" |
import "reflect-metadata" |
||||
import { app, shell, BrowserWindow, ipcMain } from "electron" |
import { _ioc } from "vc/_ioc" |
||||
import { join } from "path" |
|
||||
import { electronApp, optimizer, is } from "@electron-toolkit/utils" |
|
||||
import icon from "res/icon.png?asset" |
|
||||
import { container } from "vc/modules/ioc" |
|
||||
import { App } from "vc/App" |
import { App } from "vc/App" |
||||
|
|
||||
container.get(App).init() |
const curApp = _ioc.get(App) |
||||
|
curApp.init() |
||||
function createWindow(): void { |
|
||||
// Create the browser window.
|
|
||||
const mainWindow = new BrowserWindow({ |
|
||||
width: 900, |
|
||||
height: 670, |
|
||||
show: false, |
|
||||
autoHideMenuBar: true, |
|
||||
...(process.platform === "linux" ? { icon } : {}), |
|
||||
webPreferences: { |
|
||||
preload: join(__dirname, "../preload/index.mjs"), |
|
||||
sandbox: false, |
|
||||
}, |
|
||||
}) |
|
||||
|
|
||||
mainWindow.on("ready-to-show", () => { |
|
||||
mainWindow.show() |
|
||||
}) |
|
||||
|
|
||||
mainWindow.webContents.setWindowOpenHandler(details => { |
|
||||
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"]) { |
|
||||
mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]) |
|
||||
} else { |
|
||||
mainWindow.loadFile(join(__dirname, "../renderer/index.html")) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// This method will be called when Electron has finished
|
|
||||
// initialization and is ready to create browser windows.
|
|
||||
// Some APIs can only be used after this event occurs.
|
|
||||
app.whenReady().then(() => { |
|
||||
// Set app user model id for windows
|
|
||||
electronApp.setAppUserModelId("com.electron") |
|
||||
|
|
||||
// Default open or close DevTools by F12 in development
|
|
||||
// and ignore CommandOrControl + R in production.
|
|
||||
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
|
||||
app.on("browser-window-created", (_, window) => { |
|
||||
optimizer.watchWindowShortcuts(window) |
|
||||
}) |
|
||||
|
|
||||
// IPC test
|
|
||||
ipcMain.on("ping", () => console.log(icon)) |
|
||||
|
|
||||
createWindow() |
|
||||
|
|
||||
app.on("activate", function () { |
|
||||
// On macOS it's common to re-create a window in the app when the
|
|
||||
// dock icon is clicked and there are no other windows open.
|
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow() |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
|
||||
// for applications and their menu bar to stay active until the user quits
|
|
||||
// explicitly with Cmd + Q.
|
|
||||
app.on("window-all-closed", () => { |
|
||||
if (process.platform !== "darwin") { |
|
||||
app.quit() |
|
||||
} |
|
||||
}) |
|
||||
|
|
||||
// In this file you can include the rest of your app"s specific main process
|
|
||||
// code. You can also put them in separate files and require them here.
|
|
||||
|
@ -0,0 +1,31 @@ |
|||||
|
import { Container, ContainerModule } from "inversify" |
||||
|
import { Setting } from "./setting" |
||||
|
import { DB } from "./db" |
||||
|
import { Api } from "./api" |
||||
|
import { WindowManager } from "./window-manager" |
||||
|
import { Tabs } from "./tabs" |
||||
|
import Commands from "./commands" |
||||
|
|
||||
|
const modules = new ContainerModule(bind => { |
||||
|
bind(Setting).toConstantValue(new Setting()) |
||||
|
bind(Api).toSelf().inSingletonScope() |
||||
|
bind(WindowManager).toSelf().inSingletonScope() |
||||
|
bind(Commands).toSelf().inSingletonScope() |
||||
|
bind(Tabs).toSelf().inSingletonScope() |
||||
|
bind(DB).toSelf().inSingletonScope() |
||||
|
}) |
||||
|
|
||||
|
async function destroyAllModules(ioc: Container) { |
||||
|
await Promise.all([ |
||||
|
ioc.get(Setting).destroy(), |
||||
|
ioc.get(WindowManager).destroy(), |
||||
|
ioc.get(Commands).destroy(), |
||||
|
ioc.get(Tabs).destroy(), |
||||
|
ioc.get(Api).destroy(), |
||||
|
ioc.get(DB).destroy(), |
||||
|
]) |
||||
|
ioc.unloadAsync(modules) |
||||
|
} |
||||
|
|
||||
|
export default modules |
||||
|
export { modules, destroyAllModules } |
@ -0,0 +1,90 @@ |
|||||
|
import { session, net } from "electron" |
||||
|
import { inject, injectable } from "inversify" |
||||
|
import IOC from "vc/_ioc" |
||||
|
import BaseClass from "vc/base/base" |
||||
|
|
||||
|
@injectable() |
||||
|
class Api extends BaseClass { |
||||
|
constructor(@inject(IOC) private _IOC: IOC) { |
||||
|
super() |
||||
|
this.interceptHandler = this.interceptHandler.bind(this) |
||||
|
} |
||||
|
|
||||
|
destroy() { |
||||
|
// TODO
|
||||
|
} |
||||
|
init(partition?: string) { |
||||
|
// const ses = partition ? session.fromPartition(partition) : session.defaultSession
|
||||
|
const ses = partition ? session.fromPartition(partition) : session.defaultSession |
||||
|
ses.protocol.handle("api", this.interceptHandler) |
||||
|
} |
||||
|
async interceptHandler(request: Request) { |
||||
|
if (request.url.startsWith("api://fuck/")) { |
||||
|
let curUrl = request.url |
||||
|
const isScriteText = curUrl.endsWith("?script") |
||||
|
if (isScriteText) { |
||||
|
curUrl = curUrl.replace("?script", "") |
||||
|
} |
||||
|
const isPost = request.method.toLowerCase() === "post" |
||||
|
const file = curUrl.replace("api://fuck/", "") |
||||
|
const array = file.split("/") |
||||
|
const routePath = array.slice(0, -1).join("/") |
||||
|
const fnName = array[array.length - 1] |
||||
|
// https://vitejs.cn/vite5-cn/guide/features.html#dynamic-import
|
||||
|
const module = await this._IOC.getAsync(routePath) |
||||
|
// const module = await import(`vc/controller/${routePath}.ts`)
|
||||
|
const opts = { body: {}, query: {} } |
||||
|
if (isPost) { |
||||
|
opts.body = await request.json() |
||||
|
} |
||||
|
const headers: HeadersInit = {} |
||||
|
if (isScriteText) { |
||||
|
headers["content-type"] = "text/javascript" |
||||
|
} |
||||
|
if (isPost) { |
||||
|
headers["content-type"] = "application/json" |
||||
|
} |
||||
|
if (module && module[fnName]) { |
||||
|
if (typeof module[fnName] === "string") { |
||||
|
const result = module[fnName] |
||||
|
return new Response(result, { |
||||
|
status: 200, |
||||
|
headers: Object.keys(headers).length ? headers : undefined, |
||||
|
}) |
||||
|
} |
||||
|
if (typeof module[fnName] === "function") { |
||||
|
let result = await module[fnName](opts) |
||||
|
if (typeof result === "object") { |
||||
|
result = JSON.stringify(result) |
||||
|
} |
||||
|
return new Response(result, { |
||||
|
status: 200, |
||||
|
headers: Object.keys(headers).length ? headers : undefined, |
||||
|
}) |
||||
|
} |
||||
|
if (typeof module[fnName] === "object") { |
||||
|
let result = module[fnName] |
||||
|
if (typeof result === "object") { |
||||
|
result = JSON.stringify(result) |
||||
|
} |
||||
|
return new Response(result, { |
||||
|
status: 200, |
||||
|
headers: Object.keys(headers).length ? headers : undefined, |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
return new Response("", { |
||||
|
status: 500, |
||||
|
headers: Object.keys(headers).length ? headers : undefined, |
||||
|
}) |
||||
|
} else if (request.url.startsWith("api://")) { |
||||
|
return new Response("error", { |
||||
|
status: 500, |
||||
|
}) |
||||
|
} |
||||
|
return net.fetch(request.url, request) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default Api |
||||
|
export { Api } |
@ -0,0 +1,6 @@ |
|||||
|
|
||||
|
## 资源 |
||||
|
|
||||
|
- https://juejin.cn/post/7311619723317657611#heading-6 |
||||
|
- https://juejin.cn/post/7208108117836873784 |
||||
|
- https://www.electronjs.org/zh/docs/latest/api/protocol#protocolregisterschemesasprivilegedcustomschemes |
@ -0,0 +1,72 @@ |
|||||
|
// import https from "node:https"
|
||||
|
|
||||
|
// const interceptRequestRemote = async (request, callback) => {
|
||||
|
// const client = https.request(request.url, {
|
||||
|
// method: request.method,
|
||||
|
// headers: { ...request.headers },
|
||||
|
// });
|
||||
|
// if (request.uploadData) {
|
||||
|
// for (const data of request.uploadData) {
|
||||
|
// if (data.type === "rawData") {
|
||||
|
// // 直接创建Buffer对象
|
||||
|
// client.write(data.bytes);
|
||||
|
// // buffers.push(Buffer.from(data.bytes));
|
||||
|
// } else if (data.type === "blob") {
|
||||
|
// // 通过blobUUID获取Buffer对象
|
||||
|
// const buffer = await sess.getBlobData(data.blobUUID);
|
||||
|
// client.write(buffer);
|
||||
|
// }
|
||||
|
// }
|
||||
|
// }
|
||||
|
// client.on("error", (err) => {
|
||||
|
// console.error(`sess request error: ${request.url}`, err);
|
||||
|
// });
|
||||
|
// client.on("response", (response) => {
|
||||
|
// let body = [];
|
||||
|
// response.on("error", (err) => {
|
||||
|
// console.error(`sess request response error: ${request.url}`, err);
|
||||
|
// });
|
||||
|
// response.on("data", (chunk) => {
|
||||
|
// body.push(chunk);
|
||||
|
// });
|
||||
|
// response.on("end", () => {
|
||||
|
// body = Buffer.concat(body);
|
||||
|
// callback({
|
||||
|
// statusCode: response.statusCode,
|
||||
|
// headers: response.headers,
|
||||
|
// data: body,
|
||||
|
// });
|
||||
|
// });
|
||||
|
// });
|
||||
|
// console.log(`sess request: ${request.url}`);
|
||||
|
// client.end();
|
||||
|
// };
|
||||
|
|
||||
|
// const interceptHandler = (request, callback) => {
|
||||
|
// const localPath = checkIsNeedLocal(request);
|
||||
|
// if (localPath) {
|
||||
|
// fs.readFile(localPath, (err, data) => {
|
||||
|
// if (err) {
|
||||
|
// console.error("readFile error", err);
|
||||
|
// interceptRequestRemote(request, callback);
|
||||
|
// return;
|
||||
|
// }
|
||||
|
// const ext = path.extname(localPath);
|
||||
|
// const mimeType =
|
||||
|
// ext === ".js"
|
||||
|
// ? "application/javascript"
|
||||
|
// : ext === ".css"
|
||||
|
// ? "text/css"
|
||||
|
// : "text/html";
|
||||
|
// callback({
|
||||
|
// data,
|
||||
|
// mimeType,
|
||||
|
// });
|
||||
|
// });
|
||||
|
// } else {
|
||||
|
// interceptRequestRemote(request, callback);
|
||||
|
// }
|
||||
|
// };
|
||||
|
|
||||
|
// ses.protocol.interceptBufferProtocol("https", interceptHandler);
|
||||
|
|
@ -0,0 +1,93 @@ |
|||||
|
import { IMenuItemOption, IPopupMenuOption } from "#" |
||||
|
import { ipcMain, Menu, MenuItem } from "electron" |
||||
|
import { inject } from "inversify" |
||||
|
import IOC from "vc/_ioc" |
||||
|
import BaseClass from "vc/base/base" |
||||
|
import { isPromise } from "vc/utils" |
||||
|
import WindowManager from "../window-manager" |
||||
|
|
||||
|
export default class Commands extends BaseClass { |
||||
|
destroy() { |
||||
|
// TODO
|
||||
|
} |
||||
|
|
||||
|
constructor( |
||||
|
@inject(IOC) private _IOC: IOC, |
||||
|
@inject(WindowManager) private _WindowManager: WindowManager, |
||||
|
) { |
||||
|
super() |
||||
|
} |
||||
|
|
||||
|
init() { |
||||
|
ipcMain.addListener("command", async (event, key, command: string, ...argus) => { |
||||
|
// console.log(event.sender);
|
||||
|
try { |
||||
|
const splitClass = command.split(".") |
||||
|
const run = await this._IOC.getAsync<any>(splitClass[0]) |
||||
|
if (run) { |
||||
|
const result: Promise<any> | any = run[splitClass[1]](...argus) |
||||
|
if (isPromise(result)) { |
||||
|
result |
||||
|
.then((res: any) => { |
||||
|
event.reply(key, null, res ?? null) |
||||
|
event.returnValue = res ?? null |
||||
|
}) |
||||
|
.catch((err: Error) => { |
||||
|
event.reply(key, err) |
||||
|
event.returnValue = null |
||||
|
}) |
||||
|
} else { |
||||
|
event.reply(key, null, result ?? null) |
||||
|
event.returnValue = result ?? null |
||||
|
} |
||||
|
} else { |
||||
|
event.reply(key, new Error(`不存在该命令:${command}`)) |
||||
|
event.returnValue = null |
||||
|
} |
||||
|
} catch (error) { |
||||
|
event.reply(key, error) |
||||
|
event.returnValue = null |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
ipcMain.on("x_popup_menu", (_, name: string, options: IPopupMenuOption) => { |
||||
|
const menu = new Menu() |
||||
|
const readMenu = (items: IMenuItemOption[]) => { |
||||
|
return items.map(opt => { |
||||
|
if (typeof opt._click_evt === "string") { |
||||
|
const evt: string = opt._click_evt |
||||
|
opt.click = () => { |
||||
|
// broadcast(evt)
|
||||
|
this.sendMessage(name, evt) |
||||
|
} |
||||
|
} |
||||
|
if (opt.submenu && Array.isArray(opt.submenu)) { |
||||
|
opt.submenu = readMenu(opt.submenu) |
||||
|
} |
||||
|
|
||||
|
return opt |
||||
|
}) |
||||
|
} |
||||
|
const arrays = readMenu(options.items) |
||||
|
|
||||
|
arrays.forEach(v => { |
||||
|
const item = new MenuItem(v) |
||||
|
menu.append(item) |
||||
|
}) |
||||
|
|
||||
|
menu.on("menu-will-close", () => { |
||||
|
this.sendMessage(name, `popup_menu_close:${options.menu_id}`) |
||||
|
// broadcast(`popup_menu_close:${options.menu_id}`)
|
||||
|
}) |
||||
|
|
||||
|
menu.popup() |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
sendMessage(name: string, evt: string, ...argu: any[]) { |
||||
|
const win = this._WindowManager.get(name) |
||||
|
if (win) { |
||||
|
win.webContents.send(evt, ...argu) |
||||
|
} |
||||
|
} |
||||
|
} |
@ -1,18 +0,0 @@ |
|||||
import { Container } from "inversify" |
|
||||
import { ContainerModule } from "inversify" |
|
||||
import { Setting } from "./setting" |
|
||||
import { DB } from "./db" |
|
||||
import App from "../App" |
|
||||
|
|
||||
const module = new ContainerModule(bind => { |
|
||||
bind(Setting).toConstantValue(new Setting()) |
|
||||
bind(DB).toSelf().inSingletonScope() |
|
||||
bind(App).toSelf().inSingletonScope() |
|
||||
}) |
|
||||
|
|
||||
const container = new Container() |
|
||||
|
|
||||
container.load(module) |
|
||||
|
|
||||
export default container |
|
||||
export { container } |
|
@ -0,0 +1,6 @@ |
|||||
|
export function Layout(width, height) { |
||||
|
// Tab布局位置
|
||||
|
const NavbarHeight = 30 |
||||
|
const OffsetHeight = NavbarHeight + 100 |
||||
|
return { x: 0, y: OffsetHeight, width: width, height: height - OffsetHeight } |
||||
|
} |
@ -0,0 +1,261 @@ |
|||||
|
import { BrowserWindow, WebContentsView, WebPreferences } from "electron" |
||||
|
import { join } from "node:path" |
||||
|
import BaseClass from "vc/base/base" |
||||
|
import _debug from "debug" |
||||
|
import { Layout } from "./Constant" |
||||
|
import FuckHTML from "res/fuck.html?asset" |
||||
|
import { fileURLToPath } from "node:url" |
||||
|
|
||||
|
const debug = _debug("app:tab") |
||||
|
|
||||
|
interface IOption { |
||||
|
url: string |
||||
|
active: boolean |
||||
|
} |
||||
|
|
||||
|
class Tab extends BaseClass { |
||||
|
init() { |
||||
|
// TODO
|
||||
|
} |
||||
|
public url: string = "" |
||||
|
public showUrl: string = "" |
||||
|
public title: string = "" |
||||
|
public favicons: string[] = [] |
||||
|
public active: boolean = false |
||||
|
public alive: boolean = false |
||||
|
public isDestory: boolean = false |
||||
|
public playing: boolean = false |
||||
|
public visible: boolean = false |
||||
|
private webContentsView: WebContentsView | null = null |
||||
|
private curWindow: BrowserWindow | null = null |
||||
|
|
||||
|
private defaultOptions: IOption = { |
||||
|
url: "", |
||||
|
active: false, |
||||
|
} |
||||
|
|
||||
|
private options: IOption |
||||
|
|
||||
|
get isActive() { |
||||
|
return this.active |
||||
|
} |
||||
|
|
||||
|
get events() { |
||||
|
return this._events |
||||
|
} |
||||
|
|
||||
|
constructor(options = {}, window: BrowserWindow) { |
||||
|
super() |
||||
|
this.listenResize = this.listenResize.bind(this) |
||||
|
this.options = { |
||||
|
...this.defaultOptions, |
||||
|
...options, |
||||
|
} |
||||
|
this.url = this.getUrl(this.options.url) |
||||
|
this.showUrl = this.options.url |
||||
|
this.curWindow = window |
||||
|
this.setActive(this.options.active) |
||||
|
} |
||||
|
destroyTimer: NodeJS.Timeout | null = null |
||||
|
stopDestroyTimer() { |
||||
|
if (this.destroyTimer !== null) { |
||||
|
clearTimeout(this.destroyTimer) |
||||
|
this.destroyTimer = null |
||||
|
} |
||||
|
} |
||||
|
startDestroyTimer() { |
||||
|
this.stopDestroyTimer() |
||||
|
if (this.visible) return |
||||
|
if (this.playing) return |
||||
|
this.destroyTimer = setTimeout(() => { |
||||
|
if (this.webContentsView && !this.webContentsView.webContents.isDestroyed()) { |
||||
|
this.curWindow?.contentView.removeChildView(this.webContentsView!) |
||||
|
// @ts-ignore 超过8s没有激活的tab就销毁
|
||||
|
this.webContentsView.webContents.destroy() |
||||
|
this.webContentsView = null |
||||
|
this.alive = false |
||||
|
} |
||||
|
}, 8000) |
||||
|
} |
||||
|
|
||||
|
setActive(active: boolean) { |
||||
|
if (!active) { |
||||
|
if (!this.webContentsView) return |
||||
|
this.curWindow!.removeListener("resize", this.listenResize) |
||||
|
this.webContentsView.setVisible(false) |
||||
|
this.visible = false |
||||
|
this.startDestroyTimer() |
||||
|
} else { |
||||
|
this.stopDestroyTimer() |
||||
|
this.visible = true |
||||
|
if (!this.webContentsView) { |
||||
|
this.create() |
||||
|
// , this.curWindow!.contentView.children.length - 1
|
||||
|
this.curWindow!.contentView.addChildView(this.webContentsView!) |
||||
|
this.alive = true |
||||
|
} |
||||
|
this.listenResize() |
||||
|
this.curWindow!.addListener("resize", this.listenResize) |
||||
|
this.webContentsView!.setVisible(true) |
||||
|
} |
||||
|
this.active = active |
||||
|
} |
||||
|
|
||||
|
openDevtool() { |
||||
|
if (!this.webContentsView) return |
||||
|
this.webContentsView.webContents.openDevTools({ |
||||
|
mode: "right", |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
reload() { |
||||
|
if (!this.webContentsView) return |
||||
|
this.webContentsView.webContents.reload() |
||||
|
} |
||||
|
|
||||
|
create() { |
||||
|
let securityAttr: Partial<WebPreferences> = {} |
||||
|
if (this.url.startsWith("file:")) { |
||||
|
// 预加载脚本
|
||||
|
securityAttr = { |
||||
|
preload: join(__dirname, "../preload/index.mjs"), |
||||
|
sandbox: false, |
||||
|
} |
||||
|
} |
||||
|
this.webContentsView = new WebContentsView({ |
||||
|
webPreferences: { |
||||
|
sandbox: true, |
||||
|
webSecurity: true, |
||||
|
allowRunningInsecureContent: false, |
||||
|
nodeIntegration: false, |
||||
|
spellcheck: false, |
||||
|
contextIsolation: true, |
||||
|
...securityAttr, |
||||
|
}, |
||||
|
}) |
||||
|
const webContents = this.webContentsView.webContents |
||||
|
this.webContentsView.webContents.loadURL(this.url) |
||||
|
// this.webContentsView.webContents.executeJavaScript(`
|
||||
|
// const click = (x,y)=>{
|
||||
|
// const ev = new MouseEvent("click", {
|
||||
|
// view: window,
|
||||
|
// bubbles: true,
|
||||
|
// cancelable: true,
|
||||
|
// screenX: x,
|
||||
|
// screenY: y
|
||||
|
// })
|
||||
|
|
||||
|
// const el = document.elementFromPoint(x,y);
|
||||
|
// console.log(el)
|
||||
|
// el.dispatchEvent(ev);
|
||||
|
// }
|
||||
|
// console.log("点击初始化完成")
|
||||
|
// `)
|
||||
|
this.webContentsView.webContents.setWindowOpenHandler(ev => { |
||||
|
debug(ev) |
||||
|
this.events.emit("window-open", ev) |
||||
|
return { action: "deny" } |
||||
|
}) |
||||
|
webContents.addListener("media-paused", () => { |
||||
|
this.playing = false |
||||
|
this.startDestroyTimer() |
||||
|
}) |
||||
|
webContents.addListener("media-started-playing", () => { |
||||
|
this.playing = true |
||||
|
this.stopDestroyTimer() |
||||
|
}) |
||||
|
webContents.addListener("did-finish-load", () => { |
||||
|
this.url = webContents.getURL() |
||||
|
this.showUrl = this.getShowUrl(this.url) |
||||
|
this.events.emit("update") |
||||
|
}) |
||||
|
webContents.addListener("did-navigate-in-page", () => { |
||||
|
this.url = webContents.getURL() |
||||
|
this.showUrl = this.getShowUrl(this.url) |
||||
|
this.events.emit("update") |
||||
|
}) |
||||
|
webContents.addListener("page-title-updated", (_, title) => { |
||||
|
this.title = title |
||||
|
debug(`tab页更新:`, title) |
||||
|
this.events.emit("update") |
||||
|
}) |
||||
|
webContents.addListener("page-favicon-updated", (_, favicons) => { |
||||
|
this.favicons = favicons |
||||
|
debug(favicons) |
||||
|
this.events.emit("update") |
||||
|
}) |
||||
|
// 待机的销毁,但不去除实例
|
||||
|
webContents.addListener("destroyed", () => { |
||||
|
this.#destoryWebContentsView() |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
print() { |
||||
|
return { |
||||
|
url: this.url, |
||||
|
showUrl: this.showUrl, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private getUrl(url) { |
||||
|
if (url === "about:blank") { |
||||
|
debug(FuckHTML) |
||||
|
return FuckHTML |
||||
|
} |
||||
|
return url |
||||
|
} |
||||
|
|
||||
|
private getShowUrl(url) { |
||||
|
try { |
||||
|
if (fileURLToPath(url) === FuckHTML) { |
||||
|
debug(url) |
||||
|
debug(FuckHTML) |
||||
|
return "about:blank" |
||||
|
} |
||||
|
} catch (error) { |
||||
|
// ignore
|
||||
|
} |
||||
|
return url |
||||
|
} |
||||
|
|
||||
|
listenResize() { |
||||
|
if (!this.curWindow) { |
||||
|
return |
||||
|
} |
||||
|
if (!this.webContentsView) { |
||||
|
return |
||||
|
} |
||||
|
const size = this.curWindow.getContentSize() |
||||
|
this.webContentsView.setBounds(Layout(size[0], size[1])) |
||||
|
} |
||||
|
|
||||
|
navigate(url: string) { |
||||
|
if (!this.webContentsView) return |
||||
|
this.webContentsView.webContents.loadURL(this.getUrl(url)) |
||||
|
} |
||||
|
|
||||
|
#destoryWebContentsView() { |
||||
|
this.stopDestroyTimer() |
||||
|
if (this.webContentsView && this.curWindow && !this.curWindow.isDestroyed()) { |
||||
|
this.curWindow.contentView.removeChildView(this.webContentsView) |
||||
|
this.curWindow.removeListener("resize", this.listenResize) |
||||
|
} |
||||
|
if (this.webContentsView && !this.webContentsView.webContents.isDestroyed()) { |
||||
|
this.webContentsView.webContents.removeAllListeners() |
||||
|
this.webContentsView.removeAllListeners() |
||||
|
// @ts-ignore 超过8s没有激活的tab就销毁
|
||||
|
this.webContentsView.webContents.destroy() |
||||
|
} |
||||
|
this.webContentsView = null |
||||
|
} |
||||
|
|
||||
|
destroy() { |
||||
|
this.#destoryWebContentsView() |
||||
|
this.events.removeAllListeners() |
||||
|
this.isDestory = true |
||||
|
debug("Tab destroy") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export { Tab } |
||||
|
export default Tab |
@ -0,0 +1,95 @@ |
|||||
|
import Tab from "./Tab" |
||||
|
import BaseClass from "vc/base/base" |
||||
|
import _debug from "debug" |
||||
|
import { BrowserWindow } from "electron" |
||||
|
import EventEmitter from "events" |
||||
|
|
||||
|
const debug = _debug("app:tabs") |
||||
|
|
||||
|
class Tabs extends BaseClass { |
||||
|
destroy() { |
||||
|
this._tabs.forEach(v => v.destroy()) |
||||
|
this._tabs = [] |
||||
|
} |
||||
|
|
||||
|
public events = new EventEmitter() |
||||
|
|
||||
|
constructor() { |
||||
|
super() |
||||
|
} |
||||
|
|
||||
|
_tabs: Tab[] = [] |
||||
|
|
||||
|
init(mainWindow) { |
||||
|
this.add("about:blank", true, mainWindow) |
||||
|
} |
||||
|
|
||||
|
add(url: string, active: boolean, win: BrowserWindow) { |
||||
|
const tab = new Tab({ url }, win) |
||||
|
tab.events.on("window-open", ev => { |
||||
|
debug(ev) |
||||
|
this.add(ev.url, true, win) |
||||
|
this.events.emit("tab-active") |
||||
|
// tab.navigate(ev.url)
|
||||
|
}) |
||||
|
tab.events.on("update", () => { |
||||
|
this.events.emit("tab-active") |
||||
|
}) |
||||
|
this._tabs.push(tab) |
||||
|
if (active) { |
||||
|
this.changeActive(this._tabs.length - 1) |
||||
|
} |
||||
|
this.events.emit("tab-active") |
||||
|
} |
||||
|
|
||||
|
changeActive(index: number) { |
||||
|
this._tabs.forEach((tab, i) => { |
||||
|
tab.setActive(i === index) |
||||
|
}) |
||||
|
this.events.emit("tab-active", index) |
||||
|
} |
||||
|
|
||||
|
openDevtool(index: number) { |
||||
|
if (this._tabs[index]) { |
||||
|
this._tabs[index].openDevtool() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
reload(index: number) { |
||||
|
if (this._tabs[index]) { |
||||
|
this._tabs[index].reload() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
navigate(index: number, url: string) { |
||||
|
if (this._tabs[index]) { |
||||
|
this._tabs[index].navigate(url) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
remove(index: number) { |
||||
|
this._tabs[index].destroy() |
||||
|
if (this._tabs[index].isActive && index - 1 >= 0) { |
||||
|
this.changeActive(index - 1) |
||||
|
} |
||||
|
this._tabs.splice(index, 1) |
||||
|
this.events.emit("tab-active") |
||||
|
} |
||||
|
|
||||
|
removeAll(index: number[]) { |
||||
|
index |
||||
|
.map(v => { |
||||
|
return this._tabs[+v] |
||||
|
}) |
||||
|
.forEach(tab => { |
||||
|
tab.destroy() |
||||
|
}) |
||||
|
this._tabs = this._tabs.filter(v => { |
||||
|
return !v.isDestory |
||||
|
}) |
||||
|
this.events.emit("tab-active") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export { Tabs } |
||||
|
export default Tabs |
@ -0,0 +1,346 @@ |
|||||
|
import { BrowserWindow, app, dialog } from "electron" |
||||
|
import { cloneDeep, merge } from "lodash" |
||||
|
import { defaultConfig, defaultWindowConfig, getWindowsMap, IConfig, Param } from "./windowsMap" |
||||
|
import { optimizer } from "@electron-toolkit/utils" |
||||
|
import BaseClass from "vc/base/base" |
||||
|
import _debug from "debug" |
||||
|
|
||||
|
const debug = _debug("app:window-manager") |
||||
|
|
||||
|
declare module "electron" { |
||||
|
interface BrowserWindow { |
||||
|
$$forceClose?: boolean |
||||
|
$$lastChoice?: number |
||||
|
$$opts?: Param |
||||
|
} |
||||
|
} |
||||
|
export { WindowManager } |
||||
|
export default class WindowManager extends BaseClass { |
||||
|
constructor() { |
||||
|
super() |
||||
|
} |
||||
|
|
||||
|
destroy() { |
||||
|
// TODO
|
||||
|
} |
||||
|
globalChioce: number = -1 |
||||
|
#showWin(info: Param) { |
||||
|
if (this.#windows.length >= 6) { |
||||
|
dialog.showErrorBox("错误", "窗口数量超出限制") |
||||
|
return |
||||
|
} |
||||
|
if (!info.name) { |
||||
|
dialog.showErrorBox("错误", "窗口未指定唯一key") |
||||
|
return |
||||
|
} |
||||
|
const index = this.findIndex(info.name) |
||||
|
if (index === -1) { |
||||
|
this.#windows.push(this.#add(info)) |
||||
|
} else { |
||||
|
if (this.#windows[index].isDestroyed()) { |
||||
|
this.#windows[index] = this.#add(info) |
||||
|
} else { |
||||
|
if (info.url && info.loadURLInSameWin) { |
||||
|
this.#windows[index].loadURL(info.url) |
||||
|
} |
||||
|
this.#windows[index].show() |
||||
|
} |
||||
|
} |
||||
|
this.showCurrentWindow() |
||||
|
} |
||||
|
|
||||
|
showMainWindow() { |
||||
|
this.#showWin(this.mainInfo) |
||||
|
} |
||||
|
|
||||
|
showWindow(name: string, opts?: Partial<IConfig>) { |
||||
|
let have = false |
||||
|
for (const key in this.#urlMap) { |
||||
|
const info = this.#urlMap[key] |
||||
|
if (new RegExp(key).test(name)) { |
||||
|
opts && merge(info, opts) |
||||
|
info.name = name |
||||
|
if (!info.ignoreEmptyUrl && !info.url) { |
||||
|
dialog.showErrorBox("错误", name + "窗口未提供url") |
||||
|
return |
||||
|
} |
||||
|
this.#showWin(info as Param) |
||||
|
have = true |
||||
|
} |
||||
|
} |
||||
|
if (!have) { |
||||
|
dialog.showErrorBox("错误", name + "窗口未创建成功") |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
init() { |
||||
|
/** |
||||
|
* 当应用被激活时触发 |
||||
|
*/ |
||||
|
app.on("activate", () => { |
||||
|
this.showMainWindow() |
||||
|
}) |
||||
|
// Default open or close DevTools by F12 in development
|
||||
|
// and ignore CommandOrControl + R in production.
|
||||
|
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
||||
|
app.on("browser-window-created", (_, window) => { |
||||
|
optimizer.watchWindowShortcuts(window) |
||||
|
}) |
||||
|
/** |
||||
|
* 应用程序开始关闭时回调,可以通过event.preventDefault()阻止,以下两点需要注意: |
||||
|
* 1. 如果是autoUpdater.quitAndInstall()关闭的,那么会所有窗口关闭,并且在close事件之后执行 |
||||
|
* 2. 关机,重启,用户退出时不会触发 |
||||
|
*/ |
||||
|
app.on("before-quit", (event: Electron.Event) => { |
||||
|
const mainWin = this.get(this.mainInfo.name) |
||||
|
if (!mainWin || (mainWin && mainWin?.$$forceClose)) { |
||||
|
// app.exit()
|
||||
|
} else { |
||||
|
event.preventDefault() |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
app.on("window-all-closed", () => { |
||||
|
if (process.platform !== "darwin") { |
||||
|
app.quit() |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
#urlMap = getWindowsMap() |
||||
|
|
||||
|
getWndows() { |
||||
|
return this.#windows |
||||
|
} |
||||
|
|
||||
|
length() { |
||||
|
return this.#windows.length |
||||
|
} |
||||
|
|
||||
|
public get mainInfo() { |
||||
|
return this.#urlMap["main"] as Param |
||||
|
} |
||||
|
|
||||
|
#windows: BrowserWindow[] = [] |
||||
|
|
||||
|
#defaultConfig: IConfig = defaultConfig |
||||
|
|
||||
|
#add(config: Param) { |
||||
|
const curConfig = cloneDeep(this.#defaultConfig ?? {}) |
||||
|
for (const key in config) { |
||||
|
if (Object.prototype.hasOwnProperty.call(config, key)) { |
||||
|
const value = config[key] |
||||
|
// if (Reflect.has(curConfig, key)) {
|
||||
|
curConfig[key] = value |
||||
|
// }
|
||||
|
} |
||||
|
} |
||||
|
const privateConfig = merge(curConfig.overideWindowOpts ? {} : cloneDeep(defaultWindowConfig), curConfig.windowOpts ?? {}) |
||||
|
let parentWindow |
||||
|
if (typeof privateConfig.parent === "string") { |
||||
|
parentWindow = this.get(privateConfig.parent) |
||||
|
} |
||||
|
if (parentWindow) { |
||||
|
privateConfig.parent = parentWindow |
||||
|
} |
||||
|
const browserWin = new BrowserWindow(privateConfig) |
||||
|
browserWin.webContents.setWindowOpenHandler(() => { |
||||
|
if (curConfig.denyWindowOpen) { |
||||
|
return { action: "deny" } |
||||
|
} |
||||
|
return { action: "allow" } |
||||
|
}) |
||||
|
// @ts-ignore 不需要解释为啥
|
||||
|
browserWin.webContents.$$senderName = curConfig.name |
||||
|
browserWin.$$forceClose = false |
||||
|
browserWin.$$lastChoice = -1 |
||||
|
browserWin.on("close", (event: any) => { |
||||
|
if (this.globalChioce === 1) { |
||||
|
this.#onClose(curConfig.name) |
||||
|
return |
||||
|
} |
||||
|
if (!curConfig.confrimWindowClose) { |
||||
|
this.#onClose(curConfig.name) |
||||
|
return |
||||
|
} |
||||
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
|
const that = this |
||||
|
function justQuit() { |
||||
|
browserWin.$$lastChoice = 1 |
||||
|
// app.quit()
|
||||
|
// 不要用quit();试了会弹两次
|
||||
|
browserWin.$$forceClose = true |
||||
|
if (curConfig.name === that.mainInfo.name) { |
||||
|
that.globalChioce = 1 |
||||
|
app.quit() // exit()直接关闭客户端,不会执行quit();
|
||||
|
} else { |
||||
|
that.delete(curConfig.name) |
||||
|
that.showCurrentWindow() |
||||
|
} |
||||
|
} |
||||
|
if (browserWin.$$forceClose) { |
||||
|
that.delete(curConfig.name) |
||||
|
app.quit() |
||||
|
} else { |
||||
|
let choice = -1 |
||||
|
if (browserWin && browserWin!.$$lastChoice !== undefined && browserWin.$$lastChoice >= 0) { |
||||
|
choice = browserWin.$$lastChoice |
||||
|
} else { |
||||
|
choice = dialog.showMessageBoxSync(browserWin, { |
||||
|
type: "info", |
||||
|
title: curConfig.confrimWindowCloseText.title, |
||||
|
defaultId: curConfig.confrimWindowCloseText.defaultId, |
||||
|
cancelId: curConfig.confrimWindowCloseText.cancelId, |
||||
|
message: curConfig.confrimWindowCloseText.message, |
||||
|
buttons: curConfig.confrimWindowCloseText.buttons, |
||||
|
}) |
||||
|
} |
||||
|
if (choice === 1) { |
||||
|
justQuit() |
||||
|
} else { |
||||
|
event && event.preventDefault() |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
browserWin.$$opts = curConfig |
||||
|
// 在此注册窗口
|
||||
|
browserWin.webContents.addListener("did-finish-load", () => { |
||||
|
browserWin.webContents.executeJavaScript(`window._global=${JSON.stringify({ name: curConfig.name })};`) |
||||
|
browserWin.webContents.send("bind-window-manager", curConfig.name) |
||||
|
}) |
||||
|
// https://www.electronjs.org/zh/docs/latest/tutorial/security#12-%E5%88%9B%E5%BB%BAwebview%E5%89%8D%E7%A1%AE%E8%AE%A4%E5%85%B6%E9%80%89%E9%A1%B9
|
||||
|
// browserWin.webContents.on("will-attach-webview", (_event, webPreferences) => {
|
||||
|
// if (webPreferences.preload !== path.resolve(app.getAppPath(), "webview.js")) {
|
||||
|
// // 如果未使用,则删除预加载脚本或验证其位置是否合法
|
||||
|
// delete webPreferences.preload
|
||||
|
// }
|
||||
|
|
||||
|
// // 禁用 Node.js 集成
|
||||
|
// webPreferences.nodeIntegration = false
|
||||
|
|
||||
|
// // 验证正在加载的 URL
|
||||
|
// // if (!params.src.startsWith('https://example.com/')) {
|
||||
|
// // event.preventDefault()
|
||||
|
// // }
|
||||
|
// })
|
||||
|
if (curConfig.type === "info") { |
||||
|
// 隐藏菜单
|
||||
|
browserWin.setMenuBarVisibility(false) |
||||
|
} |
||||
|
if (curConfig.url) { |
||||
|
browserWin.loadURL(curConfig.url) |
||||
|
// logger.debug(`当前窗口网址:${curConfig.url}`)
|
||||
|
} |
||||
|
if (curConfig.windowOpts?.show === false) { |
||||
|
if (curConfig.url) { |
||||
|
browserWin.once("ready-to-show", () => { |
||||
|
debug(`准备展示:`, curConfig.url) |
||||
|
browserWin?.show() |
||||
|
}) |
||||
|
} else { |
||||
|
browserWin?.show() |
||||
|
} |
||||
|
} |
||||
|
return browserWin |
||||
|
} |
||||
|
|
||||
|
showCurrentWindow() { |
||||
|
debug(`current open window: ${this.#windows.map(v => v.$$opts!.name).join(",")}`) |
||||
|
} |
||||
|
|
||||
|
#onClose(name: string) { |
||||
|
for (let i = this.#windows.length - 1; i >= 0; i--) { |
||||
|
const win = this.#windows[i] |
||||
|
if (name === win.$$opts!.name) { |
||||
|
win.destroy() |
||||
|
this.#windows.splice(i, 1) |
||||
|
} |
||||
|
} |
||||
|
this.showCurrentWindow() |
||||
|
} |
||||
|
|
||||
|
get(name: string) { |
||||
|
return this.#windows.find(v => { |
||||
|
return v.$$opts!.name === name |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
getFocusWindow() { |
||||
|
const mainWindow = this.getMainWindow() |
||||
|
if (mainWindow?.isFocused()) { |
||||
|
return mainWindow |
||||
|
} |
||||
|
for (let i = 0; i < this.#windows.length; i++) { |
||||
|
const win = this.#windows[i] |
||||
|
if (win.isFocused()) { |
||||
|
return win |
||||
|
} |
||||
|
} |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
getMainWindow() { |
||||
|
return this.#windows.find(v => { |
||||
|
return v.$$opts!.name === this.mainInfo.name |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
close(name: string | RegExp) { |
||||
|
const indexList = this.findAllIndex(name) |
||||
|
for (let i = indexList.length - 1; i >= 0; i--) { |
||||
|
const index = indexList[i] |
||||
|
const win = this.#windows[index] |
||||
|
win.close() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
delete(name: string | RegExp) { |
||||
|
const indexList = this.findAllIndex(name) |
||||
|
for (let i = indexList.length - 1; i >= 0; i--) { |
||||
|
const index = indexList[i] |
||||
|
this.#windows.splice(index, 1) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
findIndex(name: string | RegExp) { |
||||
|
const index = this.#windows.findIndex(v => { |
||||
|
if (typeof name === "string") { |
||||
|
return v.$$opts!.name === name |
||||
|
} else { |
||||
|
return name.test(v.$$opts!.name) |
||||
|
} |
||||
|
}) |
||||
|
return index |
||||
|
} |
||||
|
|
||||
|
findAllIndex(name: string | RegExp) { |
||||
|
const result: number[] = [] |
||||
|
for (let i = 0; i < this.#windows.length; i++) { |
||||
|
const win = this.#windows[i] |
||||
|
if (typeof name === "string" && win.$$opts!.name === name) { |
||||
|
result.push(i) |
||||
|
} else if (typeof name !== "string" && name.test(win.$$opts!.name)) { |
||||
|
result.push(i) |
||||
|
} |
||||
|
} |
||||
|
return result |
||||
|
} |
||||
|
|
||||
|
// show(name: string | RegExp) {
|
||||
|
// let indexList = this.findAllIndex(name)
|
||||
|
// if (!!indexList.length) {
|
||||
|
// for (let i = 0; i < indexList.length; i++) {
|
||||
|
// const index = indexList[i];
|
||||
|
// const win = this.#windows[index]
|
||||
|
// if (win.isDestroyed()) {
|
||||
|
// this.#windows[index] = this.#add(win.$$opts)
|
||||
|
// } else {
|
||||
|
// win.show()
|
||||
|
// }
|
||||
|
// }
|
||||
|
// } else {
|
||||
|
// console.warn("该窗口不存在")
|
||||
|
// }
|
||||
|
// }
|
||||
|
} |
@ -0,0 +1,129 @@ |
|||||
|
import config from "config" |
||||
|
import { BrowserWindowConstructorOptions } from "electron" |
||||
|
import { getFileUrl } from "vc/utils" |
||||
|
import icon from "res/icon.png?asset" |
||||
|
import { join } from "path" |
||||
|
|
||||
|
export type Param = Partial<IConfig> & Required<Pick<IConfig, "name">> |
||||
|
|
||||
|
export interface IConfig { |
||||
|
name?: string |
||||
|
url?: string |
||||
|
loadURLInSameWin?: boolean |
||||
|
type?: "info" |
||||
|
windowOpts?: BrowserWindowConstructorOptions |
||||
|
overideWindowOpts?: boolean |
||||
|
ignoreEmptyUrl?: boolean |
||||
|
denyWindowOpen?: boolean |
||||
|
confrimWindowClose?: boolean |
||||
|
confrimWindowCloseText?: { |
||||
|
title: string |
||||
|
message: string |
||||
|
buttons: string[] |
||||
|
defaultId: number |
||||
|
cancelId: number |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export const defaultConfig: IConfig = { |
||||
|
denyWindowOpen: true, |
||||
|
} |
||||
|
|
||||
|
export const defaultWindowConfig = { |
||||
|
height: 600, |
||||
|
useContentSize: true, |
||||
|
width: 800, |
||||
|
show: true, |
||||
|
resizable: true, |
||||
|
minWidth: 900, |
||||
|
minHeight: 600, |
||||
|
frame: true, |
||||
|
transparent: false, |
||||
|
alwaysOnTop: false, |
||||
|
webPreferences: {}, |
||||
|
} |
||||
|
|
||||
|
export function getWindowsMap(): Record<string, IConfig> { |
||||
|
return { |
||||
|
main: { |
||||
|
name: "main", |
||||
|
url: getFileUrl("index.html"), |
||||
|
confrimWindowClose: true, |
||||
|
confrimWindowCloseText: { |
||||
|
title: config.app_title, |
||||
|
defaultId: 0, |
||||
|
cancelId: 0, |
||||
|
message: "确定要关闭吗?", |
||||
|
buttons: ["没事", "直接退出"], |
||||
|
}, |
||||
|
windowOpts: { |
||||
|
show: false, |
||||
|
titleBarStyle: "hidden", |
||||
|
titleBarOverlay: true, |
||||
|
...(process.platform === "linux" ? { icon } : {}), |
||||
|
webPreferences: { |
||||
|
webviewTag: false, |
||||
|
preload: join(__dirname, "../preload/index.mjs"), |
||||
|
nodeIntegration: true, |
||||
|
contextIsolation: true, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
_blank: { |
||||
|
overideWindowOpts: false, |
||||
|
confrimWindowClose: true, |
||||
|
confrimWindowCloseText: { |
||||
|
title: config.app_title, |
||||
|
defaultId: 0, |
||||
|
cancelId: 0, |
||||
|
message: "确定要关闭吗?", |
||||
|
buttons: ["没事", "直接退出"], |
||||
|
}, |
||||
|
type: "info", |
||||
|
windowOpts: { |
||||
|
height: 600, |
||||
|
useContentSize: true, |
||||
|
width: 800, |
||||
|
show: true, |
||||
|
resizable: true, |
||||
|
minWidth: 900, |
||||
|
minHeight: 600, |
||||
|
frame: true, |
||||
|
transparent: false, |
||||
|
alwaysOnTop: false, |
||||
|
// icon: appIconPath,
|
||||
|
title: config.app_title, |
||||
|
webPreferences: { |
||||
|
devTools: false, |
||||
|
sandbox: true, |
||||
|
nodeIntegration: false, |
||||
|
contextIsolation: true, |
||||
|
webviewTag: false, |
||||
|
preload: undefined, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
"^about": { |
||||
|
url: getFileUrl("about.html"), |
||||
|
overideWindowOpts: true, |
||||
|
confrimWindowClose: false, |
||||
|
type: "info", |
||||
|
windowOpts: { |
||||
|
width: 600, |
||||
|
height: 200, |
||||
|
minimizable: false, |
||||
|
darkTheme: true, |
||||
|
modal: true, |
||||
|
show: false, |
||||
|
resizable: false, |
||||
|
// icon: appIconPath,
|
||||
|
webPreferences: { |
||||
|
devTools: false, |
||||
|
sandbox: false, |
||||
|
nodeIntegration: false, |
||||
|
contextIsolation: true, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
} |
@ -0,0 +1,21 @@ |
|||||
|
import { is } from "@electron-toolkit/utils" |
||||
|
import { join } from "node:path" |
||||
|
import { webContents } from "electron" |
||||
|
|
||||
|
export function getFileUrl(app: string, route: string = "") { |
||||
|
let winURL = "" |
||||
|
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) { |
||||
|
winURL = process.env["ELECTRON_RENDERER_URL"] + `/${app}#/${route}` |
||||
|
} else { |
||||
|
winURL = join(__dirname, `../renderer/${app}#/${route}`) |
||||
|
} |
||||
|
return winURL |
||||
|
} |
||||
|
|
||||
|
export function isPromise(value: () => any) { |
||||
|
return value && Object.prototype.toString.call(value) === "[object Promise]" |
||||
|
} |
||||
|
|
||||
|
export const broadcast = (event: string, ...args: any[]) => { |
||||
|
webContents.getAllWebContents().forEach(browser => browser.send(event, ...args)) |
||||
|
} |
@ -0,0 +1,71 @@ |
|||||
|
import { ipcRenderer } from "electron" |
||||
|
|
||||
|
let count = 0 |
||||
|
export function call(command: string, ...args: any[]): Promise<any> { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
if (!command) { |
||||
|
console.warn("命令不能为空") |
||||
|
return |
||||
|
} |
||||
|
count++ |
||||
|
const timestamp = new Date().getTime() |
||||
|
const key = timestamp + "-" + count |
||||
|
let timeID: any = null |
||||
|
ipcRenderer.once(key, fn) |
||||
|
|
||||
|
function fn(_, err: any, res: any) { |
||||
|
clearTimeout(timeID) |
||||
|
if (err) { |
||||
|
reject(err) |
||||
|
return |
||||
|
} |
||||
|
resolve(res) |
||||
|
} |
||||
|
|
||||
|
ipcRenderer.send("command", key, command, ...args) |
||||
|
|
||||
|
// 超过5s就取消监听
|
||||
|
timeID = setTimeout(() => { |
||||
|
reject(new Error(`超过5s未响应: ${command}`)) |
||||
|
ipcRenderer.removeListener(key, fn) |
||||
|
}, 5000) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
export function callLong(command: string, ...args: any[]): Promise<any> { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
if (!command) { |
||||
|
console.warn("命令不能为空") |
||||
|
return |
||||
|
} |
||||
|
count++ |
||||
|
const timestamp = new Date().getTime() |
||||
|
const key = timestamp + "-" + count |
||||
|
ipcRenderer.once(key, fn) |
||||
|
|
||||
|
function fn(_, err: any, res: any) { |
||||
|
if (err) { |
||||
|
reject(err) |
||||
|
return |
||||
|
} |
||||
|
resolve(res) |
||||
|
} |
||||
|
|
||||
|
ipcRenderer.send("command", key, command, ...args) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
export function callSync(command: string, ...args: any[]) { |
||||
|
if (!command) { |
||||
|
console.warn("命令不能为空") |
||||
|
return |
||||
|
} |
||||
|
count++ |
||||
|
const timestamp = new Date().getTime() |
||||
|
const key = timestamp + "-" + count |
||||
|
const result = ipcRenderer.sendSync("command", key, command, ...args) |
||||
|
if (!result) { |
||||
|
return |
||||
|
} |
||||
|
return result |
||||
|
} |
@ -0,0 +1,28 @@ |
|||||
|
<!doctype html> |
||||
|
<html lang="en"> |
||||
|
<head> |
||||
|
<meta charset="UTF-8" /> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
||||
|
<title>关于我</title> |
||||
|
<style> |
||||
|
html, |
||||
|
body { |
||||
|
height: 100%; |
||||
|
width: 100%; |
||||
|
margin: 0; |
||||
|
padding: 0; |
||||
|
outline: none; |
||||
|
border: 0; |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
} |
||||
|
</style> |
||||
|
</head> |
||||
|
|
||||
|
<body> |
||||
|
<article> |
||||
|
<h1 id="demo" style="text-align: center">您好,亲爱的冒险者!</h1> |
||||
|
</article> |
||||
|
</body> |
||||
|
</html> |
@ -1,30 +1,163 @@ |
|||||
<script setup lang="ts"> |
<script setup lang="ts"> |
||||
import Versions from "./components/Versions.vue" |
import NavBar from "@renderer/components/NavBar.vue" |
||||
|
import { onBeforeMount, ref } from "vue" |
||||
|
import { PopupMenu } from "./bridge/PopupMenu" |
||||
|
|
||||
const ipcHandle = () => window.electron.ipcRenderer.send("ping") |
const list = ref<any[]>([]) |
||||
|
const curUrl = ref<any>("") |
||||
|
const curIndex = ref<any>(-1) |
||||
|
const listener = (_, v) => { |
||||
|
list.value = v |
||||
|
const el = v.find(v => v.isActive) |
||||
|
console.log(el); |
||||
|
|
||||
|
curIndex.value = v.findIndex(v => v.isActive) |
||||
|
if (el) { |
||||
|
curUrl.value = el.showUrl |
||||
|
} else { |
||||
|
curUrl.value = "" |
||||
|
} |
||||
|
} |
||||
|
if (import.meta.hot) { |
||||
|
api.off("TabsCommand.update", listener) |
||||
|
} |
||||
|
api.on("TabsCommand.update", listener) |
||||
|
api.call("TabsCommand.sync") |
||||
|
|
||||
|
onBeforeMount(async () => { |
||||
|
list.value = await fetch("api://fuck/TabsService/getAllTabs").then(async res => await res.json()) |
||||
|
}) |
||||
|
|
||||
|
// const url = ref("") |
||||
|
|
||||
|
// async function addTab() { |
||||
|
// if (!url.value) url.value = "about:blank" |
||||
|
// await fetch("api://fuck/TabsService/add", { |
||||
|
// method: "POST", |
||||
|
// body: JSON.stringify({ url: url.value }), |
||||
|
// }) |
||||
|
// url.value = "" |
||||
|
// onClick() |
||||
|
// } |
||||
|
|
||||
|
function handleTabContextMenu(_, index) { |
||||
|
const menu = new PopupMenu([ |
||||
|
{ |
||||
|
label: "右侧关闭", |
||||
|
click() { |
||||
|
const all: number[] = [] |
||||
|
list.value.forEach((_, i) => { |
||||
|
if (i <= index) return |
||||
|
all.push(i) |
||||
|
}) |
||||
|
fetch("api://fuck/TabsService/closeTabAll", { |
||||
|
method: "POST", |
||||
|
body: JSON.stringify({ active: all }), |
||||
|
}) |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
type: "separator", |
||||
|
}, |
||||
|
]) |
||||
|
menu.show() |
||||
|
} |
||||
|
|
||||
|
function changeTab(_, index) { |
||||
|
api.call("TabsCommand.setActive", index) |
||||
|
} |
||||
|
|
||||
|
function addTabInput() { |
||||
|
if (curUrl.value) { |
||||
|
if (curIndex.value !== undefined && curIndex.value >= 0) { |
||||
|
api.call("TabsCommand.nagivate", curIndex.value, curUrl.value) |
||||
|
} else { |
||||
|
api.call("TabsCommand.add", curUrl.value) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
function addTab() { |
||||
|
api.call("TabsCommand.add", "about:blank") |
||||
|
} |
||||
|
|
||||
|
async function closeTab(_, index) { |
||||
|
await fetch("api://fuck/TabsService/closeTab", { |
||||
|
method: "POST", |
||||
|
body: JSON.stringify({ active: index }), |
||||
|
}) |
||||
|
onClick() |
||||
|
} |
||||
|
|
||||
|
const onClick = async () => { |
||||
|
list.value = await api.call("TabsCommand.getAllTabs") |
||||
|
// list.value = await fetch("api://fuck/TabsService/getAllTabs").then(async res => await res.json()) |
||||
|
// fetch("api://fuck/BasicService/showAbout").then(async res => console.log(await res.json())) |
||||
|
// fetch("api://index/openAbout", { |
||||
|
// method: "POST", |
||||
|
// body: JSON.stringify({ a: "234" }), |
||||
|
// }).then(async res => console.log(await res.json())) |
||||
|
} |
||||
|
|
||||
|
function onClickDevTool() { |
||||
|
fetch("api://fuck/BasicService/openTabDevtool") |
||||
|
} |
||||
</script> |
</script> |
||||
|
|
||||
<template> |
<template> |
||||
<img alt="logo" class="logo" src="./assets/electron.svg" /> |
<div h-full flex flex-col> |
||||
<div class="creator">Powered by electron-vite</div> |
<NavBar></NavBar> |
||||
<div class="text"> |
<div flex-1 h-0 overflow-auto flex flex-col> |
||||
Build an Electron app with |
<div h="100px" flex flex-col b-b="1px solid #E5E5E5"> |
||||
<span class="vue">Vue</span> |
<div flex gap-1 my-1 px-1 w-full> |
||||
and |
<div |
||||
<span class="ts">TypeScript</span> |
v-for="(item, index) in list" |
||||
</div> |
:key="index" |
||||
<p class="tip"> |
p-1 |
||||
Please try pressing |
b-b="1px solid gray" |
||||
<code>F12</code> |
b-l="1px solid gray" |
||||
to open the devTool |
b-r="1px solid gray" |
||||
</p> |
:b-t="item.isActive ? '1px solid red' : '1px solid gray'" |
||||
<div class="actions"> |
flex |
||||
<div class="action"> |
flex-1 |
||||
<a href="https://electron-vite.org/" target="_blank" rel="noreferrer">Documentation</a> |
w-0 |
||||
</div> |
items-center |
||||
<div class="action"> |
cursor="pointer" |
||||
<a target="_blank" rel="noreferrer" @click="ipcHandle">Send IPC</a> |
gap="5px" |
||||
|
max-w="200px" |
||||
|
@contextmenu="handleTabContextMenu(item, index)" |
||||
|
@click="changeTab(item, index)" |
||||
|
> |
||||
|
<div flex-1 w-0 line-1 text-normal>{{ item.title || "加载中..." }}</div> |
||||
|
<span p-1 rounded hover="bg-gray-2 text-hover" @click.stop="closeTab(item, index)">X</span> |
||||
|
</div> |
||||
|
<div |
||||
|
p-1 |
||||
|
b-b="1px solid gray" |
||||
|
b-l="1px solid gray" |
||||
|
b-r="1px solid gray" |
||||
|
b-t="1px solid gray" |
||||
|
flex |
||||
|
items-center |
||||
|
cursor="pointer" |
||||
|
gap="5px" |
||||
|
hover="bg-gray-2 text-hover" |
||||
|
> |
||||
|
<span p-1 rounded @click.stop="addTab()">+</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div mx="5px" overflow="auto" flex-1 h-0 flex items-center gap-x="5px"> |
||||
|
<div flex-1 w-0 h="35px" px-3 rounded="35px" b="1px solid gray-4" flex items-center> |
||||
|
<input v-model="curUrl" placeholder="输入点什么" w-full text="16px" b-0 leading="25px" outline-0 type="text" /> |
||||
|
</div> |
||||
|
<div inline-block hover="bg-gray-2 text-hover" px-1 py-1 rounded cursor="pointer" @click="addTabInput()"> |
||||
|
<button text="14px" bg-transparent b-0 cursor="pointer">前往</button> |
||||
|
</div> |
||||
|
<div inline-block hover="bg-gray-2 text-hover" px-1 py-1 rounded cursor="pointer" @click="onClickDevTool()"> |
||||
|
<button text="14px" bg-transparent b-0 cursor="pointer">DevTool</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div flex-1 h-0 flex items-center justify-center>fuck</div> |
||||
</div> |
</div> |
||||
</div> |
</div> |
||||
<Versions /> |
|
||||
</template> |
</template> |
||||
|
@ -1,67 +0,0 @@ |
|||||
:root { |
|
||||
--ev-c-white: #ffffff; |
|
||||
--ev-c-white-soft: #f8f8f8; |
|
||||
--ev-c-white-mute: #f2f2f2; |
|
||||
|
|
||||
--ev-c-black: #1b1b1f; |
|
||||
--ev-c-black-soft: #222222; |
|
||||
--ev-c-black-mute: #282828; |
|
||||
|
|
||||
--ev-c-gray-1: #515c67; |
|
||||
--ev-c-gray-2: #414853; |
|
||||
--ev-c-gray-3: #32363f; |
|
||||
|
|
||||
--ev-c-text-1: rgba(255, 255, 245, 0.86); |
|
||||
--ev-c-text-2: rgba(235, 235, 245, 0.6); |
|
||||
--ev-c-text-3: rgba(235, 235, 245, 0.38); |
|
||||
|
|
||||
--ev-button-alt-border: transparent; |
|
||||
--ev-button-alt-text: var(--ev-c-text-1); |
|
||||
--ev-button-alt-bg: var(--ev-c-gray-3); |
|
||||
--ev-button-alt-hover-border: transparent; |
|
||||
--ev-button-alt-hover-text: var(--ev-c-text-1); |
|
||||
--ev-button-alt-hover-bg: var(--ev-c-gray-2); |
|
||||
} |
|
||||
|
|
||||
:root { |
|
||||
--color-background: var(--ev-c-black); |
|
||||
--color-background-soft: var(--ev-c-black-soft); |
|
||||
--color-background-mute: var(--ev-c-black-mute); |
|
||||
|
|
||||
--color-text: var(--ev-c-text-1); |
|
||||
} |
|
||||
|
|
||||
*, |
|
||||
*::before, |
|
||||
*::after { |
|
||||
box-sizing: border-box; |
|
||||
margin: 0; |
|
||||
font-weight: normal; |
|
||||
} |
|
||||
|
|
||||
ul { |
|
||||
list-style: none; |
|
||||
} |
|
||||
|
|
||||
body { |
|
||||
min-height: 100vh; |
|
||||
color: var(--color-text); |
|
||||
background: var(--color-background); |
|
||||
line-height: 1.6; |
|
||||
font-family: |
|
||||
Inter, |
|
||||
-apple-system, |
|
||||
BlinkMacSystemFont, |
|
||||
"Segoe UI", |
|
||||
Roboto, |
|
||||
Oxygen, |
|
||||
Ubuntu, |
|
||||
Cantarell, |
|
||||
"Fira Sans", |
|
||||
"Droid Sans", |
|
||||
"Helvetica Neue", |
|
||||
sans-serif; |
|
||||
text-rendering: optimizeLegibility; |
|
||||
-webkit-font-smoothing: antialiased; |
|
||||
-moz-osx-font-smoothing: grayscale; |
|
||||
} |
|
Before Width: | Height: | Size: 5.7 KiB |
@ -1,171 +0,0 @@ |
|||||
@import "./base.css"; |
|
||||
|
|
||||
body { |
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
justify-content: center; |
|
||||
overflow: hidden; |
|
||||
background-image: url("./wavy-lines.svg"); |
|
||||
background-size: cover; |
|
||||
user-select: none; |
|
||||
} |
|
||||
|
|
||||
code { |
|
||||
font-weight: 600; |
|
||||
padding: 3px 5px; |
|
||||
border-radius: 2px; |
|
||||
background-color: var(--color-background-mute); |
|
||||
font-family: |
|
||||
ui-monospace, |
|
||||
SFMono-Regular, |
|
||||
SF Mono, |
|
||||
Menlo, |
|
||||
Consolas, |
|
||||
Liberation Mono, |
|
||||
monospace; |
|
||||
font-size: 85%; |
|
||||
} |
|
||||
|
|
||||
#app { |
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
justify-content: center; |
|
||||
flex-direction: column; |
|
||||
margin-bottom: 80px; |
|
||||
} |
|
||||
|
|
||||
.logo { |
|
||||
margin-bottom: 20px; |
|
||||
-webkit-user-drag: none; |
|
||||
height: 128px; |
|
||||
width: 128px; |
|
||||
will-change: filter; |
|
||||
transition: filter 300ms; |
|
||||
} |
|
||||
|
|
||||
.logo:hover { |
|
||||
filter: drop-shadow(0 0 1.2em #6988e6aa); |
|
||||
} |
|
||||
|
|
||||
.creator { |
|
||||
font-size: 14px; |
|
||||
line-height: 16px; |
|
||||
color: var(--ev-c-text-2); |
|
||||
font-weight: 600; |
|
||||
margin-bottom: 10px; |
|
||||
} |
|
||||
|
|
||||
.text { |
|
||||
font-size: 28px; |
|
||||
color: var(--ev-c-text-1); |
|
||||
font-weight: 700; |
|
||||
line-height: 32px; |
|
||||
text-align: center; |
|
||||
margin: 0 10px; |
|
||||
padding: 16px 0; |
|
||||
} |
|
||||
|
|
||||
.tip { |
|
||||
font-size: 16px; |
|
||||
line-height: 24px; |
|
||||
color: var(--ev-c-text-2); |
|
||||
font-weight: 600; |
|
||||
} |
|
||||
|
|
||||
.vue { |
|
||||
background: -webkit-linear-gradient(315deg, #42d392 25%, #647eff); |
|
||||
background-clip: text; |
|
||||
-webkit-background-clip: text; |
|
||||
-webkit-text-fill-color: transparent; |
|
||||
font-weight: 700; |
|
||||
} |
|
||||
|
|
||||
.ts { |
|
||||
background: -webkit-linear-gradient(315deg, #3178c6 45%, #f0dc4e); |
|
||||
background-clip: text; |
|
||||
-webkit-background-clip: text; |
|
||||
-webkit-text-fill-color: transparent; |
|
||||
font-weight: 700; |
|
||||
} |
|
||||
|
|
||||
.actions { |
|
||||
display: flex; |
|
||||
padding-top: 32px; |
|
||||
margin: -6px; |
|
||||
flex-wrap: wrap; |
|
||||
justify-content: flex-start; |
|
||||
} |
|
||||
|
|
||||
.action { |
|
||||
flex-shrink: 0; |
|
||||
padding: 6px; |
|
||||
} |
|
||||
|
|
||||
.action a { |
|
||||
cursor: pointer; |
|
||||
text-decoration: none; |
|
||||
display: inline-block; |
|
||||
border: 1px solid transparent; |
|
||||
text-align: center; |
|
||||
font-weight: 600; |
|
||||
white-space: nowrap; |
|
||||
border-radius: 20px; |
|
||||
padding: 0 20px; |
|
||||
line-height: 38px; |
|
||||
font-size: 14px; |
|
||||
border-color: var(--ev-button-alt-border); |
|
||||
color: var(--ev-button-alt-text); |
|
||||
background-color: var(--ev-button-alt-bg); |
|
||||
} |
|
||||
|
|
||||
.action a:hover { |
|
||||
border-color: var(--ev-button-alt-hover-border); |
|
||||
color: var(--ev-button-alt-hover-text); |
|
||||
background-color: var(--ev-button-alt-hover-bg); |
|
||||
} |
|
||||
|
|
||||
.versions { |
|
||||
position: absolute; |
|
||||
bottom: 30px; |
|
||||
margin: 0 auto; |
|
||||
padding: 15px 0; |
|
||||
font-family: "Menlo", "Lucida Console", monospace; |
|
||||
display: inline-flex; |
|
||||
overflow: hidden; |
|
||||
align-items: center; |
|
||||
border-radius: 22px; |
|
||||
background-color: #202127; |
|
||||
backdrop-filter: blur(24px); |
|
||||
} |
|
||||
|
|
||||
.versions li { |
|
||||
display: block; |
|
||||
float: left; |
|
||||
border-right: 1px solid var(--ev-c-gray-1); |
|
||||
padding: 0 20px; |
|
||||
font-size: 14px; |
|
||||
line-height: 14px; |
|
||||
opacity: 0.8; |
|
||||
&:last-child { |
|
||||
border: none; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@media (max-width: 720px) { |
|
||||
.text { |
|
||||
font-size: 20px; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@media (max-width: 620px) { |
|
||||
.versions { |
|
||||
display: none; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@media (max-width: 350px) { |
|
||||
.tip, |
|
||||
.actions { |
|
||||
display: none; |
|
||||
} |
|
||||
} |
|
@ -0,0 +1,22 @@ |
|||||
|
*, |
||||
|
*::before, |
||||
|
*::after { |
||||
|
box-sizing: border-box; |
||||
|
margin: 0; |
||||
|
font-weight: normal; |
||||
|
} |
||||
|
|
||||
|
html { |
||||
|
--text-normal: #6b6b6b; |
||||
|
--text-hover: #000000; |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
body { |
||||
|
--at-apply: text-normal; |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
#app { |
||||
|
height: 100%; |
||||
|
} |
Before Width: | Height: | Size: 1.7 KiB |
@ -0,0 +1,66 @@ |
|||||
|
/** |
||||
|
* ContextMenu |
||||
|
* @author: oldj |
||||
|
* @homepage: https://oldj.net
|
||||
|
*/ |
||||
|
|
||||
|
import { IMenuItemOption } from "#" |
||||
|
|
||||
|
let _idx: number = 0 |
||||
|
|
||||
|
type OffFunction = () => void |
||||
|
|
||||
|
export class PopupMenu { |
||||
|
private _id: string |
||||
|
private _items: IMenuItemOption[] |
||||
|
private _offs: any[] = [] |
||||
|
|
||||
|
constructor(menu_items: IMenuItemOption[]) { |
||||
|
this._id = `popup_menu_${Math.floor(Math.random() * 1e8)}` |
||||
|
this._items = menu_items |
||||
|
} |
||||
|
|
||||
|
show() { |
||||
|
// console.log('show')
|
||||
|
this.onHide() |
||||
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
|
const that = this |
||||
|
function readMenu(_items: IMenuItemOption[]) { |
||||
|
return _items.map(i => { |
||||
|
const d = { ...i } |
||||
|
if (typeof d.click === "function") { |
||||
|
const r = Math.floor(Math.random() * 1e8) |
||||
|
const evt = `popup_menu_item_${_idx++}_${r}` |
||||
|
const off = api.once(evt, d.click as any) |
||||
|
that._offs.push(off) |
||||
|
d._click_evt = evt |
||||
|
delete d.click |
||||
|
} |
||||
|
if (d.submenu && Array.isArray(d.submenu)) { |
||||
|
d.submenu = readMenu(d.submenu) |
||||
|
} |
||||
|
return d |
||||
|
}) |
||||
|
} |
||||
|
const items = readMenu(this._items) |
||||
|
|
||||
|
api.popupMenu({ |
||||
|
menu_id: this._id, |
||||
|
items, |
||||
|
}) |
||||
|
;((offs: OffFunction[]) => { |
||||
|
api.once(`popup_menu_close:${this._id}`, () => { |
||||
|
// console.log(`on popup_menu_close:${this._id}`)
|
||||
|
setTimeout(() => { |
||||
|
offs.map(o => o()) |
||||
|
}, 100) |
||||
|
}) |
||||
|
})(this._offs) |
||||
|
} |
||||
|
|
||||
|
private onHide() { |
||||
|
// console.log('hide...')
|
||||
|
this._offs.map(o => o()) |
||||
|
this._offs = [] |
||||
|
} |
||||
|
} |
@ -0,0 +1,25 @@ |
|||||
|
<template> |
||||
|
<div relative h="30px" leading="29px" pr="137px" select-none border-b="1px solid #E5E5E5" bg="#F8F8F8"> |
||||
|
<div absolute top-0 right-0 bottom-0 left-0 style="-webkit-app-region: drag"></div> |
||||
|
<div h-full px-2 flex items-center gap-1 justify-between> |
||||
|
<div flex items-center gap-1> |
||||
|
<img w="16px" h="16px" :src="icon" /> |
||||
|
<div relative h-full inline-flex items-center text-sm>{{ config.app_title }}</div> |
||||
|
</div> |
||||
|
<div float-right h-full flex items-center relative style="-webkit-app-region: no-drag"> |
||||
|
<div text-sm px-2 hover:rounded-md hover:bg-gray-2 hover:cursor-pointer text="hover:hover" @click="onClickAbout">关于</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import icon from "@res/icon.png" |
||||
|
import config from "config" |
||||
|
|
||||
|
const onClickAbout = () => { |
||||
|
fetch("api://fuck/BasicService/showAbout") |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped></style> |
@ -0,0 +1 @@ |
|||||
|
declare const api |
@ -0,0 +1,9 @@ |
|||||
|
interface IMenuItemOption extends Electron.MenuItemConstructorOptions { |
||||
|
// 参见:https://www.electronjs.org/docs/api/menu-item
|
||||
|
_click_evt?: string |
||||
|
} |
||||
|
|
||||
|
export interface IPopupMenuOption { |
||||
|
menu_id: string |
||||
|
items: IMenuItemOption[] |
||||
|
} |
@ -1,6 +1,76 @@ |
|||||
import { defineConfig, presetAttributify, presetUno } from "unocss" |
import { defineConfig, presetAttributify, presetUno, transformerDirectives } from "unocss" |
||||
import presetRemToPx from "@unocss/preset-rem-to-px" |
import presetRemToPx from "@unocss/preset-rem-to-px" |
||||
|
|
||||
export default defineConfig({ |
export default defineConfig({ |
||||
presets: [presetAttributify(), presetUno(), presetRemToPx()], |
presets: [presetAttributify(), presetUno(), presetRemToPx()], |
||||
|
transformers: [transformerDirectives()], |
||||
|
shortcuts: [ |
||||
|
// 正方形 square-100px
|
||||
|
[ |
||||
|
/^square-\[?(.*?)\]?$/, |
||||
|
([, size]) => { |
||||
|
return `w-${size} h-${size}` |
||||
|
}, |
||||
|
], |
||||
|
// 圆形 circle-100px
|
||||
|
[ |
||||
|
/^circle-\[?(.*?)\]?$/, |
||||
|
([, size]) => { |
||||
|
return `square-${size} rounded-full` |
||||
|
}, |
||||
|
], |
||||
|
// 垂直水平居中
|
||||
|
["flex-center", "flex justify-center items-center"], |
||||
|
], |
||||
|
rules: [ |
||||
|
[ |
||||
|
/^text-(.*)$/, |
||||
|
([, c]) => { |
||||
|
if (c === "normal") return { color: "var(--text-normal)" } |
||||
|
if (c === "hover") return { color: "var(--text-hover)" } |
||||
|
}, |
||||
|
], |
||||
|
// 多行文本超出部分省略号 line-n (已内置 line-clamp-n)
|
||||
|
[ |
||||
|
/^line-(\d+)$/, |
||||
|
([, l]) => { |
||||
|
if (~~l === 1) { |
||||
|
return { |
||||
|
overflow: "hidden", |
||||
|
"text-overflow": "ellipsis", |
||||
|
"white-space": "nowrap", |
||||
|
width: "100%", |
||||
|
} |
||||
|
} |
||||
|
return { |
||||
|
overflow: "hidden", |
||||
|
display: "-webkit-box", |
||||
|
"-webkit-box-orient": "vertical", |
||||
|
"-webkit-line-clamp": l, |
||||
|
} |
||||
|
}, |
||||
|
], |
||||
|
// 一侧圆角 rounded-left-5px (已内置 rounded-l-n)
|
||||
|
[ |
||||
|
/^rounded-(left|right|top|bottom)-(.*?)$/, |
||||
|
([, position, m]) => { |
||||
|
let x1, x2, y1, y2 |
||||
|
if (["left", "right"].includes(position)) { |
||||
|
y1 = "top" |
||||
|
y2 = "bottom" |
||||
|
x1 = x2 = position |
||||
|
} else { |
||||
|
x1 = "left" |
||||
|
x2 = "right" |
||||
|
y1 = y2 = position |
||||
|
} |
||||
|
if (m === "full") m = "99999px" |
||||
|
|
||||
|
return { |
||||
|
[`border-${y1}-${x1}-radius`]: m, |
||||
|
[`border-${y2}-${x2}-radius`]: m, |
||||
|
} |
||||
|
}, |
||||
|
], |
||||
|
], |
||||
}) |
}) |
||||
|
@ -0,0 +1,15 @@ |
|||||
|
|
||||
|
插件化: |
||||
|
|
||||
|
https://rubickcenter.github.io/docs/core/index.html#%E5%9F%BA%E4%BA%8E-browserview-%E5%AE%9E%E7%8E%B0%E6%8F%92%E4%BB%B6%E5%8C%96%E8%83%BD%E5%8A%9B |
||||
|
|
||||
|
|
||||
|
electron+vue虚拟桌面开发遇坑之透明窗口鼠标穿透 |
||||
|
|
||||
|
|
||||
|
https://blog.csdn.net/weixin_42421494/article/details/102800491 |
||||
|
|
||||
|
|
||||
|
截图 |
||||
|
|
||||
|
https://zhuanlan.zhihu.com/p/46043613?from_voters_page=true |
Loading…
Reference in new issue