feat: 优化内置浏览器
3
electron/electron-preload.js
vendored
@ -72,6 +72,7 @@ contextBridge.exposeInMainWorld(
|
||||
contextBridge.exposeInMainWorld(
|
||||
'process', {
|
||||
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 os = require("os");
|
||||
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 log = require("electron-log");
|
||||
const electronConf = require('electron-config')
|
||||
const userConf = new electronConf()
|
||||
const fsProm = require('fs/promises');
|
||||
const PDFDocument = require('pdf-lib').PDFDocument;
|
||||
const Screenshots = require("electron-screenshots-tool").Screenshots;
|
||||
@ -33,6 +35,19 @@ let mainWindow = null,
|
||||
let screenshotObj = 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)) {
|
||||
devloadUrl = fs.readFileSync(devloadCachePath, 'utf8')
|
||||
}
|
||||
@ -44,6 +59,8 @@ function createMainWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1280,
|
||||
height: 800,
|
||||
minWidth: 360,
|
||||
minHeight: 360,
|
||||
center: true,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
@ -65,13 +82,9 @@ function createMainWindow() {
|
||||
electronMenu.webContentsMenu(mainWindow.webContents)
|
||||
|
||||
if (devloadUrl) {
|
||||
mainWindow.loadURL(devloadUrl).then(_ => {
|
||||
|
||||
})
|
||||
mainWindow.loadURL(devloadUrl).then(_ => { }).catch(_ => { })
|
||||
} else {
|
||||
mainWindow.loadFile('./public/index.html').then(_ => {
|
||||
|
||||
})
|
||||
mainWindow.loadFile('./public/index.html').then(_ => { }).catch(_ => { })
|
||||
}
|
||||
|
||||
mainWindow.on('page-title-updated', (event, title) => {
|
||||
@ -122,7 +135,10 @@ function createSubWindow(args) {
|
||||
browser = new BrowserWindow(Object.assign({
|
||||
width: 1280,
|
||||
height: 800,
|
||||
minWidth: 360,
|
||||
minHeight: 360,
|
||||
center: true,
|
||||
show: false,
|
||||
parent: mainWindow,
|
||||
autoHideMenuBar: true,
|
||||
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 })
|
||||
}
|
||||
const originalUA = browser.webContents.session.getUserAgent() || browser.webContents.getUserAgent()
|
||||
@ -169,15 +193,11 @@ function createSubWindow(args) {
|
||||
|
||||
const hash = args.hash || args.path;
|
||||
if (/^https?:\/\//i.test(hash)) {
|
||||
browser.loadURL(hash).then(_ => {
|
||||
|
||||
})
|
||||
browser.loadURL(hash).then(_ => { }).catch(_ => { })
|
||||
return;
|
||||
}
|
||||
if (devloadUrl) {
|
||||
browser.loadURL(devloadUrl + '#' + hash).then(_ => {
|
||||
|
||||
})
|
||||
browser.loadURL(devloadUrl + '#' + hash).then(_ => { }).catch(_ => { })
|
||||
return;
|
||||
}
|
||||
browser.loadFile('./public/index.html', {
|
||||
@ -204,15 +224,11 @@ function updateSubWindow(browser, args) {
|
||||
const hash = args.hash || args.path;
|
||||
if (hash) {
|
||||
if (devloadUrl) {
|
||||
browser.loadURL(devloadUrl + '#' + hash).then(_ => {
|
||||
|
||||
})
|
||||
browser.loadURL(devloadUrl + '#' + hash).then(_ => { }).catch(_ => { })
|
||||
} else {
|
||||
browser.loadFile('./public/index.html', {
|
||||
hash
|
||||
}).then(_ => {
|
||||
|
||||
})
|
||||
}).then(_ => { }).catch(_ => { })
|
||||
}
|
||||
}
|
||||
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()
|
||||
if (!getTheLock) {
|
||||
app.quit()
|
||||
@ -355,6 +649,45 @@ ipcMain.on('updateRouter', (event, args) => {
|
||||
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隐藏,其他关闭)
|
||||
*/
|
||||
@ -512,9 +845,7 @@ ipcMain.on('storageBrowser', (event, args) => {
|
||||
},
|
||||
})
|
||||
}
|
||||
storageBrowser.loadURL(args.url).then(_ => {
|
||||
|
||||
})
|
||||
storageBrowser.loadURL(args.url).then(_ => { }).catch(_ => { })
|
||||
}
|
||||
event.returnValue = "ok"
|
||||
})
|
||||
@ -682,6 +1013,9 @@ ipcMain.on('updateCheckAndDownload', (event, args) => {
|
||||
autoUpdater.setFeedURL(args)
|
||||
}
|
||||
autoUpdater.checkForUpdates().then(info => {
|
||||
if (!info) {
|
||||
return
|
||||
}
|
||||
if (utils.compareVersion(config.version, info.updateInfo.version) >= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -26,14 +26,14 @@
|
||||
"url": "https://github.com/kuaifan/dootask.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^7.2.0",
|
||||
"@electron-forge/maker-deb": "^7.2.0",
|
||||
"@electron-forge/maker-rpm": "^7.2.0",
|
||||
"@electron-forge/maker-squirrel": "^7.2.0",
|
||||
"@electron-forge/maker-zip": "^7.2.0",
|
||||
"@electron-forge/cli": "^7.3.0",
|
||||
"@electron-forge/maker-deb": "^7.3.0",
|
||||
"@electron-forge/maker-rpm": "^7.3.0",
|
||||
"@electron-forge/maker-squirrel": "^7.3.0",
|
||||
"@electron-forge/maker-zip": "^7.3.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"electron": "^28.1.1",
|
||||
"electron-builder": "^24.9.1",
|
||||
"electron": "^29.0.1",
|
||||
"electron-builder": "^24.12.0",
|
||||
"electron-notarize": "^1.2.2",
|
||||
"form-data": "^4.0.0",
|
||||
"ora": "^4.1.1"
|
||||
@ -41,7 +41,8 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.6.2",
|
||||
"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-squirrel-startup": "^1.0.0",
|
||||
"electron-updater": "^6.1.7",
|
||||
@ -67,6 +68,7 @@
|
||||
"output": "dist"
|
||||
},
|
||||
"files": [
|
||||
"render/**/*",
|
||||
"public/**/*",
|
||||
"electron-menu.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) {
|
||||
return new Promise(resolve => {
|
||||
const encodeUrl = encodeURIComponent(url)
|
||||
webContents.executeJavaScript(`if(typeof window.__onBeforeOpenWindow === 'function'){window.__onBeforeOpenWindow({url:decodeURIComponent("${encodeUrl}")})}`, true).then(options => {
|
||||
const dataStr = JSON.stringify({url: url})
|
||||
webContents.executeJavaScript(`if(typeof window.__onBeforeOpenWindow === 'function'){window.__onBeforeOpenWindow(${dataStr})}`, true).then(options => {
|
||||
if (options !== true) {
|
||||
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
|
||||
|
||||
@ -265,19 +265,15 @@ export default {
|
||||
}
|
||||
}
|
||||
window.__onBeforeOpenWindow = ({url}) => {
|
||||
if ($A.getDomain(url) != $A.getDomain($A.apiUrl('../'))) {
|
||||
return false;
|
||||
if ($A.getDomain(url) == $A.getDomain($A.apiUrl('../'))) {
|
||||
try {
|
||||
// 下载文件不使用内置浏览器打开
|
||||
if (/^\/uploads\//i.test(new URL(url).pathname)) {
|
||||
return false;
|
||||
}
|
||||
} catch (e) { }
|
||||
}
|
||||
this.$Electron.sendMessage('windowRouter', {
|
||||
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),
|
||||
},
|
||||
});
|
||||
this.$Electron.sendMessage('openWebWindow', {url});
|
||||
return true;
|
||||
}
|
||||
this.$Electron.registerMsgListener('dispatch', args => {
|
||||
|
||||