feat: 优化内置浏览器
3
electron/electron-preload.js
vendored
@ -72,6 +72,7 @@ contextBridge.exposeInMainWorld(
|
|||||||
contextBridge.exposeInMainWorld(
|
contextBridge.exposeInMainWorld(
|
||||||
'process', {
|
'process', {
|
||||||
type: process.type,
|
type: process.type,
|
||||||
versions: process.versions
|
versions: process.versions,
|
||||||
|
platform: process.platform,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
378
electron/electron.js
vendored
@ -1,9 +1,11 @@
|
|||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const os = require("os");
|
const os = require("os");
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const {app, BrowserWindow, ipcMain, dialog, clipboard, nativeImage, shell, Tray, Menu, globalShortcut, Notification} = require('electron')
|
const {app, BrowserWindow, ipcMain, dialog, clipboard, nativeImage, shell, Tray, Menu, globalShortcut, Notification, BrowserView, nativeTheme} = require('electron')
|
||||||
const {autoUpdater} = require("electron-updater")
|
const {autoUpdater} = require("electron-updater")
|
||||||
const log = require("electron-log");
|
const log = require("electron-log");
|
||||||
|
const electronConf = require('electron-config')
|
||||||
|
const userConf = new electronConf()
|
||||||
const fsProm = require('fs/promises');
|
const fsProm = require('fs/promises');
|
||||||
const PDFDocument = require('pdf-lib').PDFDocument;
|
const PDFDocument = require('pdf-lib').PDFDocument;
|
||||||
const Screenshots = require("electron-screenshots-tool").Screenshots;
|
const Screenshots = require("electron-screenshots-tool").Screenshots;
|
||||||
@ -33,6 +35,19 @@ let mainWindow = null,
|
|||||||
let screenshotObj = null,
|
let screenshotObj = null,
|
||||||
screenshotKey = null;
|
screenshotKey = null;
|
||||||
|
|
||||||
|
let webWindow = null,
|
||||||
|
webTabView = [],
|
||||||
|
webTabHeight = 38;
|
||||||
|
|
||||||
|
let showState = {},
|
||||||
|
onShowWindow = (win) => {
|
||||||
|
if (typeof showState[win.webContents.id] === 'undefined') {
|
||||||
|
showState[win.webContents.id] = true
|
||||||
|
win.setBackgroundColor('rgba(255, 255, 255, 0)')
|
||||||
|
win.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (fs.existsSync(devloadCachePath)) {
|
if (fs.existsSync(devloadCachePath)) {
|
||||||
devloadUrl = fs.readFileSync(devloadCachePath, 'utf8')
|
devloadUrl = fs.readFileSync(devloadCachePath, 'utf8')
|
||||||
}
|
}
|
||||||
@ -44,6 +59,8 @@ function createMainWindow() {
|
|||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
width: 1280,
|
width: 1280,
|
||||||
height: 800,
|
height: 800,
|
||||||
|
minWidth: 360,
|
||||||
|
minHeight: 360,
|
||||||
center: true,
|
center: true,
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
@ -65,13 +82,9 @@ function createMainWindow() {
|
|||||||
electronMenu.webContentsMenu(mainWindow.webContents)
|
electronMenu.webContentsMenu(mainWindow.webContents)
|
||||||
|
|
||||||
if (devloadUrl) {
|
if (devloadUrl) {
|
||||||
mainWindow.loadURL(devloadUrl).then(_ => {
|
mainWindow.loadURL(devloadUrl).then(_ => { }).catch(_ => { })
|
||||||
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
mainWindow.loadFile('./public/index.html').then(_ => {
|
mainWindow.loadFile('./public/index.html').then(_ => { }).catch(_ => { })
|
||||||
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mainWindow.on('page-title-updated', (event, title) => {
|
mainWindow.on('page-title-updated', (event, title) => {
|
||||||
@ -122,7 +135,10 @@ function createSubWindow(args) {
|
|||||||
browser = new BrowserWindow(Object.assign({
|
browser = new BrowserWindow(Object.assign({
|
||||||
width: 1280,
|
width: 1280,
|
||||||
height: 800,
|
height: 800,
|
||||||
|
minWidth: 360,
|
||||||
|
minHeight: 360,
|
||||||
center: true,
|
center: true,
|
||||||
|
show: false,
|
||||||
parent: mainWindow,
|
parent: mainWindow,
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
webPreferences: Object.assign({
|
webPreferences: Object.assign({
|
||||||
@ -155,6 +171,14 @@ function createSubWindow(args) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
browser.once('ready-to-show', () => {
|
||||||
|
onShowWindow(browser);
|
||||||
|
})
|
||||||
|
|
||||||
|
browser.webContents.once('dom-ready', () => {
|
||||||
|
onShowWindow(browser);
|
||||||
|
})
|
||||||
|
|
||||||
subWindow.push({ name, browser })
|
subWindow.push({ name, browser })
|
||||||
}
|
}
|
||||||
const originalUA = browser.webContents.session.getUserAgent() || browser.webContents.getUserAgent()
|
const originalUA = browser.webContents.session.getUserAgent() || browser.webContents.getUserAgent()
|
||||||
@ -169,15 +193,11 @@ function createSubWindow(args) {
|
|||||||
|
|
||||||
const hash = args.hash || args.path;
|
const hash = args.hash || args.path;
|
||||||
if (/^https?:\/\//i.test(hash)) {
|
if (/^https?:\/\//i.test(hash)) {
|
||||||
browser.loadURL(hash).then(_ => {
|
browser.loadURL(hash).then(_ => { }).catch(_ => { })
|
||||||
|
|
||||||
})
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (devloadUrl) {
|
if (devloadUrl) {
|
||||||
browser.loadURL(devloadUrl + '#' + hash).then(_ => {
|
browser.loadURL(devloadUrl + '#' + hash).then(_ => { }).catch(_ => { })
|
||||||
|
|
||||||
})
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
browser.loadFile('./public/index.html', {
|
browser.loadFile('./public/index.html', {
|
||||||
@ -204,15 +224,11 @@ function updateSubWindow(browser, args) {
|
|||||||
const hash = args.hash || args.path;
|
const hash = args.hash || args.path;
|
||||||
if (hash) {
|
if (hash) {
|
||||||
if (devloadUrl) {
|
if (devloadUrl) {
|
||||||
browser.loadURL(devloadUrl + '#' + hash).then(_ => {
|
browser.loadURL(devloadUrl + '#' + hash).then(_ => { }).catch(_ => { })
|
||||||
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
browser.loadFile('./public/index.html', {
|
browser.loadFile('./public/index.html', {
|
||||||
hash
|
hash
|
||||||
}).then(_ => {
|
}).then(_ => { }).catch(_ => { })
|
||||||
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (args.name) {
|
if (args.name) {
|
||||||
@ -223,6 +239,284 @@ function updateSubWindow(browser, args) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建内置浏览器
|
||||||
|
* @param args {url, ?}
|
||||||
|
*/
|
||||||
|
function createWebWindow(args) {
|
||||||
|
if (!args) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!utils.isJson(args)) {
|
||||||
|
args = {url: args}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allowedUrls.test(args.url)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建父级窗口
|
||||||
|
if (!webWindow) {
|
||||||
|
let config = Object.assign(args.config || {}, userConf.get('webWindow', {}));
|
||||||
|
let webPreferences = args.webPreferences || {};
|
||||||
|
const titleBarOverlay = {
|
||||||
|
height: webTabHeight
|
||||||
|
}
|
||||||
|
if (nativeTheme.shouldUseDarkColors) {
|
||||||
|
titleBarOverlay.color = '#3B3B3D'
|
||||||
|
titleBarOverlay.symbolColor = '#C5C5C5'
|
||||||
|
}
|
||||||
|
webWindow = new BrowserWindow(Object.assign({
|
||||||
|
x: mainWindow.getBounds().x + webTabHeight,
|
||||||
|
y: mainWindow.getBounds().y + webTabHeight,
|
||||||
|
width: 1280,
|
||||||
|
height: 800,
|
||||||
|
minWidth: 360,
|
||||||
|
minHeight: 360,
|
||||||
|
center: true,
|
||||||
|
show: false,
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
titleBarStyle: 'hidden',
|
||||||
|
titleBarOverlay,
|
||||||
|
webPreferences: Object.assign({
|
||||||
|
preload: path.join(__dirname, 'electron-preload.js'),
|
||||||
|
webSecurity: true,
|
||||||
|
nodeIntegration: true,
|
||||||
|
contextIsolation: true,
|
||||||
|
nativeWindowOpen: true
|
||||||
|
}, webPreferences),
|
||||||
|
}, config))
|
||||||
|
|
||||||
|
webWindow.on('resize', () => {
|
||||||
|
resizeWebTab(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
webWindow.on('enter-full-screen', () => {
|
||||||
|
utils.onDispatchEvent(webWindow.webContents, {
|
||||||
|
event: 'enter-full-screen',
|
||||||
|
}).then(_ => { })
|
||||||
|
})
|
||||||
|
|
||||||
|
webWindow.on('leave-full-screen', () => {
|
||||||
|
utils.onDispatchEvent(webWindow.webContents, {
|
||||||
|
event: 'leave-full-screen',
|
||||||
|
}).then(_ => { })
|
||||||
|
})
|
||||||
|
|
||||||
|
webWindow.on('close', event => {
|
||||||
|
if (!willQuitApp) {
|
||||||
|
closeWebTab(0)
|
||||||
|
event.preventDefault()
|
||||||
|
} else {
|
||||||
|
userConf.set('webWindow', webWindow.getBounds())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
webWindow.on('closed', () => {
|
||||||
|
webWindow = null
|
||||||
|
})
|
||||||
|
|
||||||
|
webWindow.once('ready-to-show', () => {
|
||||||
|
onShowWindow(webWindow);
|
||||||
|
})
|
||||||
|
|
||||||
|
webWindow.webContents.once('dom-ready', () => {
|
||||||
|
onShowWindow(webWindow);
|
||||||
|
})
|
||||||
|
|
||||||
|
webWindow.webContents.on('before-input-event', (event, input) => {
|
||||||
|
if (input.meta && input.key.toLowerCase() === 'r') {
|
||||||
|
reloadWebTab(0)
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
webWindow.loadFile('./render/tabs/index.html', {}).then(_ => {
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
webWindow.focus();
|
||||||
|
|
||||||
|
// 创建子窗口
|
||||||
|
const browserView = new BrowserView({
|
||||||
|
useHTMLTitleAndIcon: true,
|
||||||
|
useLoadingView: true,
|
||||||
|
useErrorView: true,
|
||||||
|
webPreferences: {
|
||||||
|
type: 'browserView',
|
||||||
|
preload: path.join(__dirname, 'electron-preload.js'),
|
||||||
|
nodeIntegrationInSubFrames: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (nativeTheme.shouldUseDarkColors) {
|
||||||
|
browserView.setBackgroundColor('#575757')
|
||||||
|
} else {
|
||||||
|
browserView.setBackgroundColor('#FFFFFF')
|
||||||
|
}
|
||||||
|
browserView.setBounds({
|
||||||
|
x: 0,
|
||||||
|
y: webTabHeight,
|
||||||
|
width: webWindow.getContentBounds().width || 1280,
|
||||||
|
height: (webWindow.getContentBounds().height || 800) - webTabHeight,
|
||||||
|
})
|
||||||
|
browserView.webContents.setWindowOpenHandler(({url}) => {
|
||||||
|
createWebWindow({url})
|
||||||
|
return {action: 'deny'}
|
||||||
|
})
|
||||||
|
browserView.webContents.on('page-title-updated', (event, title) => {
|
||||||
|
utils.onDispatchEvent(webWindow.webContents, {
|
||||||
|
event: 'title',
|
||||||
|
id: browserView.webContents.id,
|
||||||
|
title: title,
|
||||||
|
url: browserView.webContents.getURL(),
|
||||||
|
}).then(_ => { })
|
||||||
|
})
|
||||||
|
browserView.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL, isMainFrame) => {
|
||||||
|
if (!errorDescription) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.onDispatchEvent(webWindow.webContents, {
|
||||||
|
event: 'title',
|
||||||
|
id: browserView.webContents.id,
|
||||||
|
title: errorDescription,
|
||||||
|
url: browserView.webContents.getURL(),
|
||||||
|
}).then(_ => { })
|
||||||
|
})
|
||||||
|
browserView.webContents.on('page-favicon-updated', (event, favicons) => {
|
||||||
|
utils.onDispatchEvent(webWindow.webContents, {
|
||||||
|
event: 'favicon',
|
||||||
|
id: browserView.webContents.id,
|
||||||
|
favicons
|
||||||
|
}).then(_ => { })
|
||||||
|
})
|
||||||
|
browserView.webContents.on('did-start-loading', _ => {
|
||||||
|
utils.onDispatchEvent(webWindow.webContents, {
|
||||||
|
event: 'start-loading',
|
||||||
|
id: browserView.webContents.id,
|
||||||
|
}).then(_ => { })
|
||||||
|
})
|
||||||
|
browserView.webContents.on('did-stop-loading', _ => {
|
||||||
|
utils.onDispatchEvent(webWindow.webContents, {
|
||||||
|
event: 'stop-loading',
|
||||||
|
id: browserView.webContents.id,
|
||||||
|
}).then(_ => { })
|
||||||
|
})
|
||||||
|
browserView.webContents.on('before-input-event', (event, input) => {
|
||||||
|
if (input.meta && input.key.toLowerCase() === 'r') {
|
||||||
|
browserView.webContents.reload()
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
browserView.webContents.loadURL(args.url).then(_ => { }).catch(_ => { })
|
||||||
|
|
||||||
|
webWindow.addBrowserView(browserView)
|
||||||
|
webTabView.push({
|
||||||
|
id: browserView.webContents.id,
|
||||||
|
view: browserView
|
||||||
|
})
|
||||||
|
|
||||||
|
utils.onDispatchEvent(webWindow.webContents, {
|
||||||
|
event: 'create',
|
||||||
|
id: browserView.webContents.id,
|
||||||
|
url: args.url,
|
||||||
|
}).then(_ => { })
|
||||||
|
switchWebTab(browserView.webContents.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前内置浏览器标签
|
||||||
|
* @returns {Electron.BrowserView|undefined}
|
||||||
|
*/
|
||||||
|
function currentWebTab() {
|
||||||
|
const views = webWindow.getBrowserViews()
|
||||||
|
const view = views.length ? views[views.length - 1] : undefined
|
||||||
|
if (!view) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return webTabView.find(item => item.id == view.webContents.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新加载内置浏览器标签
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
function reloadWebTab(id) {
|
||||||
|
const item = id === 0 ? currentWebTab() : webTabView.find(item => item.id == id)
|
||||||
|
if (!item) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item.view.webContents.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调整内置浏览器标签尺寸
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
function resizeWebTab(id) {
|
||||||
|
const item = id === 0 ? currentWebTab() : webTabView.find(item => item.id == id)
|
||||||
|
if (!item) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item.view.setBounds({
|
||||||
|
x: 0,
|
||||||
|
y: webTabHeight,
|
||||||
|
width: webWindow.getContentBounds().width || 1280,
|
||||||
|
height: (webWindow.getContentBounds().height || 800) - webTabHeight,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换内置浏览器标签
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
function switchWebTab(id) {
|
||||||
|
const item = id === 0 ? currentWebTab() : webTabView.find(item => item.id == id)
|
||||||
|
if (!item) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resizeWebTab(item.id)
|
||||||
|
webWindow.setTopBrowserView(item.view)
|
||||||
|
item.view.webContents.focus()
|
||||||
|
utils.onDispatchEvent(webWindow.webContents, {
|
||||||
|
event: 'switch',
|
||||||
|
id: item.id,
|
||||||
|
}).then(_ => { })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭内置浏览器标签
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
function closeWebTab(id) {
|
||||||
|
const item = id === 0 ? currentWebTab() : webTabView.find(item => item.id == id)
|
||||||
|
if (!item) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (webTabView.length === 1) {
|
||||||
|
webWindow.hide()
|
||||||
|
}
|
||||||
|
webWindow.removeBrowserView(item.view)
|
||||||
|
item.view.webContents.close()
|
||||||
|
|
||||||
|
const index = webTabView.findIndex(({id}) => item.id == id)
|
||||||
|
if (index > -1) {
|
||||||
|
webTabView.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.onDispatchEvent(webWindow.webContents, {
|
||||||
|
event: 'close',
|
||||||
|
id: item.id,
|
||||||
|
}).then(_ => { })
|
||||||
|
|
||||||
|
if (webTabView.length === 0) {
|
||||||
|
userConf.set('webWindow', webWindow.getBounds())
|
||||||
|
webWindow.destroy()
|
||||||
|
} else {
|
||||||
|
switchWebTab(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getTheLock = app.requestSingleInstanceLock()
|
const getTheLock = app.requestSingleInstanceLock()
|
||||||
if (!getTheLock) {
|
if (!getTheLock) {
|
||||||
app.quit()
|
app.quit()
|
||||||
@ -355,6 +649,45 @@ ipcMain.on('updateRouter', (event, args) => {
|
|||||||
event.returnValue = "ok"
|
event.returnValue = "ok"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内置浏览器 - 打开创建
|
||||||
|
* @param args {url, ?}
|
||||||
|
*/
|
||||||
|
ipcMain.on('openWebWindow', (event, args) => {
|
||||||
|
createWebWindow(args)
|
||||||
|
event.returnValue = "ok"
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内置浏览器 - 激活标签
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
ipcMain.on('webTabSwitch', (event, id) => {
|
||||||
|
switchWebTab(id)
|
||||||
|
event.returnValue = "ok"
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内置浏览器 - 关闭标签
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
ipcMain.on('webTabClose', (event, id) => {
|
||||||
|
closeWebTab(id)
|
||||||
|
event.returnValue = "ok"
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内置浏览器 - 在外部浏览器打开
|
||||||
|
*/
|
||||||
|
ipcMain.on('webTabBrowser', (event) => {
|
||||||
|
const item = currentWebTab()
|
||||||
|
if (!item) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
openExternal(item.view.webContents.getURL())
|
||||||
|
event.returnValue = "ok"
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 隐藏窗口(mac、win隐藏,其他关闭)
|
* 隐藏窗口(mac、win隐藏,其他关闭)
|
||||||
*/
|
*/
|
||||||
@ -512,9 +845,7 @@ ipcMain.on('storageBrowser', (event, args) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
storageBrowser.loadURL(args.url).then(_ => {
|
storageBrowser.loadURL(args.url).then(_ => { }).catch(_ => { })
|
||||||
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
event.returnValue = "ok"
|
event.returnValue = "ok"
|
||||||
})
|
})
|
||||||
@ -682,6 +1013,9 @@ ipcMain.on('updateCheckAndDownload', (event, args) => {
|
|||||||
autoUpdater.setFeedURL(args)
|
autoUpdater.setFeedURL(args)
|
||||||
}
|
}
|
||||||
autoUpdater.checkForUpdates().then(info => {
|
autoUpdater.checkForUpdates().then(info => {
|
||||||
|
if (!info) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (utils.compareVersion(config.version, info.updateInfo.version) >= 0) {
|
if (utils.compareVersion(config.version, info.updateInfo.version) >= 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,14 +26,14 @@
|
|||||||
"url": "https://github.com/kuaifan/dootask.git"
|
"url": "https://github.com/kuaifan/dootask.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-forge/cli": "^7.2.0",
|
"@electron-forge/cli": "^7.3.0",
|
||||||
"@electron-forge/maker-deb": "^7.2.0",
|
"@electron-forge/maker-deb": "^7.3.0",
|
||||||
"@electron-forge/maker-rpm": "^7.2.0",
|
"@electron-forge/maker-rpm": "^7.3.0",
|
||||||
"@electron-forge/maker-squirrel": "^7.2.0",
|
"@electron-forge/maker-squirrel": "^7.3.0",
|
||||||
"@electron-forge/maker-zip": "^7.2.0",
|
"@electron-forge/maker-zip": "^7.3.0",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"electron": "^28.1.1",
|
"electron": "^29.0.1",
|
||||||
"electron-builder": "^24.9.1",
|
"electron-builder": "^24.12.0",
|
||||||
"electron-notarize": "^1.2.2",
|
"electron-notarize": "^1.2.2",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
"ora": "^4.1.1"
|
"ora": "^4.1.1"
|
||||||
@ -41,7 +41,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"crc": "^3.8.0",
|
"crc": "^3.8.0",
|
||||||
"electron-log": "^5.0.1",
|
"electron-config": "^2.0.0",
|
||||||
|
"electron-log": "^5.1.1",
|
||||||
"electron-screenshots-tool": "^1.1.2",
|
"electron-screenshots-tool": "^1.1.2",
|
||||||
"electron-squirrel-startup": "^1.0.0",
|
"electron-squirrel-startup": "^1.0.0",
|
||||||
"electron-updater": "^6.1.7",
|
"electron-updater": "^6.1.7",
|
||||||
@ -67,6 +68,7 @@
|
|||||||
"output": "dist"
|
"output": "dist"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
"render/**/*",
|
||||||
"public/**/*",
|
"public/**/*",
|
||||||
"electron-menu.js",
|
"electron-menu.js",
|
||||||
"electron-preload.js",
|
"electron-preload.js",
|
||||||
|
|||||||
269
electron/render/tabs/assets/css/style.css
vendored
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
:root {
|
||||||
|
--tab-font-family: -apple-system, 'Segoe UI', roboto, oxygen-sans, ubuntu, cantarell, 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||||
|
--tab-font-size: 12px;
|
||||||
|
--tab-transition: background-color 200ms ease-out, color 200ms ease-out;
|
||||||
|
--tab-cursor: pointer; /* 设置鼠标指针为手型 */
|
||||||
|
--tab-color: #7f8792;
|
||||||
|
--tab-background: #EFF0F4;
|
||||||
|
--tab-active-color: #222529;
|
||||||
|
--tab-active-background: #FFFFFF;
|
||||||
|
--tab-close-color: #9DA3AC;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
font-family: var(--tab-font-family);
|
||||||
|
font-feature-settings: 'clig', 'kern';
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
cursor: default;
|
||||||
|
background-color: var(--tab-background);
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav ul {
|
||||||
|
display: flex;
|
||||||
|
height: 30px;
|
||||||
|
margin: 8px 46px 0 0;
|
||||||
|
user-select: none;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav ul::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav ul li {
|
||||||
|
display: inline-flex;
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
padding: 7px 8px;
|
||||||
|
margin: 0 8px 0 0;
|
||||||
|
min-width: 100px;
|
||||||
|
max-width: 240px;
|
||||||
|
scroll-margin: 12px;
|
||||||
|
font-size: var(--tab-font-size);
|
||||||
|
color: var(--tab-color);
|
||||||
|
cursor: var(--tab-cursor);
|
||||||
|
transition: var(--tab-transition);
|
||||||
|
-webkit-app-region: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav ul li:first-child {
|
||||||
|
margin-left: 8px;
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.nav ul li.active {
|
||||||
|
color: var(--tab-active-color);
|
||||||
|
background: var(--tab-active-background);
|
||||||
|
border-radius: 6px 6px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav ul li.active::before {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: -6px;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background-image: url(../image/select_left.png);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav ul li.active::after {
|
||||||
|
position: absolute;
|
||||||
|
right: -6px;
|
||||||
|
bottom: 0;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background-image: url(../image/select_right.png);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav ul li.active .tab-icon.background {
|
||||||
|
background-image: url(../image/link_normal_selected_icon.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.nav ul li:not(.active)::after {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
width: 1px;
|
||||||
|
height: 16px;
|
||||||
|
background: rgba(0, 0, 0, 0.08);
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav ul li:not(.active):last-child::after {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 浏览器打开 */
|
||||||
|
.browser {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 38px;
|
||||||
|
padding: 0 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-app-region: none;
|
||||||
|
}
|
||||||
|
.browser span {
|
||||||
|
display: inline-block;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background-size: cover;
|
||||||
|
background-image: url(../image/link_normal_selected_icon.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图标 */
|
||||||
|
.tab-icon {
|
||||||
|
display: inline-block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-icon.background {
|
||||||
|
background-image: url(../image/link_normal_icon.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-icon.loading {
|
||||||
|
background-image: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-icon .tab-icon-loading {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border: 2px solid #eeeeee;
|
||||||
|
border-bottom-color: #84C56A;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
animation: spin 0.75s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-icon:not(.loading) .tab-icon-loading {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-icon img {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.8) rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(0.8) rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标题 */
|
||||||
|
.tab-title {
|
||||||
|
display: inline-block;
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 8px;
|
||||||
|
margin-left: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 150%;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 关闭 */
|
||||||
|
.tab-close {
|
||||||
|
display: inline-block;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
margin-right: 2px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-close::after,
|
||||||
|
.tab-close::before {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 50%;
|
||||||
|
transform: translate(50%, -50%) scale(0.9) rotate(45deg);
|
||||||
|
content: "";
|
||||||
|
width: 2px;
|
||||||
|
height: 11px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: var(--tab-close-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-close::before {
|
||||||
|
transform: translate(50%, -50%) scale(0.9) rotate(-45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 不同平台样式 */
|
||||||
|
body.win32 .nav ul {
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-right: 186px;
|
||||||
|
}
|
||||||
|
body.win32 .browser {
|
||||||
|
right: 140px;
|
||||||
|
}
|
||||||
|
body.darwin .nav ul {
|
||||||
|
margin-left: 76px;
|
||||||
|
}
|
||||||
|
body.darwin.full-screen .nav ul {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗黑模式 */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--tab-color: #C5C5C5;
|
||||||
|
--tab-background: #3B3B3D;
|
||||||
|
--tab-active-color: #E1E1E1;
|
||||||
|
--tab-active-background: #575757;
|
||||||
|
--tab-close-color: #E3E3E3;
|
||||||
|
}
|
||||||
|
.nav ul li.active::before {
|
||||||
|
background-image: url(../image/dark/select_left.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav ul li.active::after {
|
||||||
|
background-image: url(../image/dark/select_right.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav ul li.active .tab-icon.background {
|
||||||
|
background-image: url(../image/dark/link_normal_selected_icon.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
.browser span {
|
||||||
|
background-image: url(../image/dark/link_normal_selected_icon.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-icon.background {
|
||||||
|
background-image: url(../image/dark/link_normal_icon.png);
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
electron/render/tabs/assets/image/dark/link_normal_icon.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
BIN
electron/render/tabs/assets/image/dark/select_left.png
Normal file
|
After Width: | Height: | Size: 205 B |
BIN
electron/render/tabs/assets/image/dark/select_right.png
Normal file
|
After Width: | Height: | Size: 219 B |
BIN
electron/render/tabs/assets/image/link_normal_icon.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
electron/render/tabs/assets/image/link_normal_selected_icon.png
Normal file
|
After Width: | Height: | Size: 914 B |
BIN
electron/render/tabs/assets/image/select_left.png
Normal file
|
After Width: | Height: | Size: 235 B |
BIN
electron/render/tabs/assets/image/select_right.png
Normal file
|
After Width: | Height: | Size: 236 B |
14
electron/render/tabs/assets/js/vue.global.min.js
vendored
Normal file
165
electron/render/tabs/index.html
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Untitled</title>
|
||||||
|
<link rel="stylesheet" href="./assets/css/style.css">
|
||||||
|
<script src="./assets/js/vue.global.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app" class="app">
|
||||||
|
<div class="nav">
|
||||||
|
<ul>
|
||||||
|
<li v-for="item in tabs" :data-id="item.id" :class="{active: activeId === item.id}" @click="onSwitch(item)">
|
||||||
|
<div v-if="item.state === 'loading'" class="tab-icon loading">
|
||||||
|
<div class="tab-icon-loading"></div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="tab-icon background" :style="iconStyle(item)"></div>
|
||||||
|
<div class="tab-title" :title="item.title">{{tabTitle(item)}}</div>
|
||||||
|
<div class="tab-close" @click.stop="onClose(item)"></div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div v-if="tabs.length > 0" class="browser" @click="onBrowser">
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const App = {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
activeId: 0,
|
||||||
|
tabs: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeCreate() {
|
||||||
|
document.body.classList.add(window.process.platform)
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
window.__onDispatchEvent = (detail) => {
|
||||||
|
const {id, event} = detail
|
||||||
|
switch (event) {
|
||||||
|
case 'create':
|
||||||
|
this.tabs.push(Object.assign({
|
||||||
|
id,
|
||||||
|
title: '',
|
||||||
|
url: '',
|
||||||
|
icon: '',
|
||||||
|
state: 'loading'
|
||||||
|
}, detail))
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'close':
|
||||||
|
const closeIndex = this.tabs.findIndex(item => item.id === id)
|
||||||
|
if (closeIndex > -1) {
|
||||||
|
this.tabs.splice(closeIndex, 1)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'switch':
|
||||||
|
this.activeId = id
|
||||||
|
this.scrollTabActive()
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'title':
|
||||||
|
const titleItem = this.tabs.find(item => item.id === id)
|
||||||
|
if (titleItem) {
|
||||||
|
titleItem.title = detail.title
|
||||||
|
titleItem.url = detail.url
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'favicon':
|
||||||
|
const faviconItem = this.tabs.find(item => item.id === id)
|
||||||
|
if (faviconItem) {
|
||||||
|
faviconItem.icon = detail.favicons[detail.favicons.length - 1]
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'start-loading':
|
||||||
|
const startItem = this.tabs.find(item => item.id === id)
|
||||||
|
if (startItem) {
|
||||||
|
startItem.state = 'loading'
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'stop-loading':
|
||||||
|
const stopItem = this.tabs.find(item => item.id === id)
|
||||||
|
if (stopItem) {
|
||||||
|
stopItem.state = 'loaded'
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'enter-full-screen':
|
||||||
|
document.body.classList.add('full-screen')
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'leave-full-screen':
|
||||||
|
document.body.classList.remove('full-screen')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
pageTitle() {
|
||||||
|
const activeItem = this.tabs.find(item => item.id === this.activeId)
|
||||||
|
return activeItem ? activeItem.title : 'Untitled'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
pageTitle(title) {
|
||||||
|
document.title = title;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onSwitch(item) {
|
||||||
|
this.sendMessage('webTabSwitch', item.id)
|
||||||
|
},
|
||||||
|
|
||||||
|
onClose(item) {
|
||||||
|
this.sendMessage('webTabClose', item.id);
|
||||||
|
},
|
||||||
|
|
||||||
|
onBrowser() {
|
||||||
|
this.sendMessage('webTabBrowser')
|
||||||
|
},
|
||||||
|
|
||||||
|
iconStyle(item) {
|
||||||
|
return item.icon ? `background-image: url(${item.icon})` : ''
|
||||||
|
},
|
||||||
|
|
||||||
|
tabTitle(item) {
|
||||||
|
if (item.title) {
|
||||||
|
return item.title
|
||||||
|
}
|
||||||
|
if (item.state === 'loading') {
|
||||||
|
return 'Loading...'
|
||||||
|
}
|
||||||
|
if (item.url) {
|
||||||
|
return `${item.url}`.replace(/^https*:\/\//, '')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
scrollTabActive() {
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
const child = document.querySelector(`.nav ul li[data-id=${this.activeId}]`)
|
||||||
|
if (child) {
|
||||||
|
child.scrollIntoView({behavior: 'smooth', block: 'nearest'})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
},
|
||||||
|
|
||||||
|
sendMessage(event, args) {
|
||||||
|
electron?.sendMessage(event, args)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Vue.createApp(App).mount('#app')
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
21
electron/utils.js
vendored
@ -321,8 +321,8 @@ module.exports = {
|
|||||||
*/
|
*/
|
||||||
onBeforeOpenWindow(webContents, url) {
|
onBeforeOpenWindow(webContents, url) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const encodeUrl = encodeURIComponent(url)
|
const dataStr = JSON.stringify({url: url})
|
||||||
webContents.executeJavaScript(`if(typeof window.__onBeforeOpenWindow === 'function'){window.__onBeforeOpenWindow({url:decodeURIComponent("${encodeUrl}")})}`, true).then(options => {
|
webContents.executeJavaScript(`if(typeof window.__onBeforeOpenWindow === 'function'){window.__onBeforeOpenWindow(${dataStr})}`, true).then(options => {
|
||||||
if (options !== true) {
|
if (options !== true) {
|
||||||
resolve()
|
resolve()
|
||||||
}
|
}
|
||||||
@ -332,6 +332,23 @@ module.exports = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分发事件
|
||||||
|
* @param webContents
|
||||||
|
* @param data
|
||||||
|
* @returns {Promise<unknown>}
|
||||||
|
*/
|
||||||
|
onDispatchEvent(webContents, data) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const dataStr = JSON.stringify(data)
|
||||||
|
webContents.executeJavaScript(`window.__onDispatchEvent(${dataStr})`, true).then(options => {
|
||||||
|
resolve(options)
|
||||||
|
}).catch(_ => {
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 版本比较
|
* 版本比较
|
||||||
* @param version1
|
* @param version1
|
||||||
|
|||||||
@ -265,19 +265,15 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.__onBeforeOpenWindow = ({url}) => {
|
window.__onBeforeOpenWindow = ({url}) => {
|
||||||
if ($A.getDomain(url) != $A.getDomain($A.apiUrl('../'))) {
|
if ($A.getDomain(url) == $A.getDomain($A.apiUrl('../'))) {
|
||||||
return false;
|
try {
|
||||||
|
// 下载文件不使用内置浏览器打开
|
||||||
|
if (/^\/uploads\//i.test(new URL(url).pathname)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
}
|
}
|
||||||
this.$Electron.sendMessage('windowRouter', {
|
this.$Electron.sendMessage('openWebWindow', {url});
|
||||||
name: `window-${encodeURIComponent(url)}`,
|
|
||||||
path: url,
|
|
||||||
force: false,
|
|
||||||
config: {
|
|
||||||
parent: null,
|
|
||||||
width: Math.min(window.screen.availWidth, 1440),
|
|
||||||
height: Math.min(window.screen.availHeight, 900),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
this.$Electron.registerMsgListener('dispatch', args => {
|
this.$Electron.registerMsgListener('dispatch', args => {
|
||||||
|
|||||||