mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-21 17:20:19 +00:00
feat: 添加导航功能,支持快捷键和鼠标手势操作
This commit is contained in:
parent
a6385b699e
commit
fee1c12357
20
electron/electron.js
vendored
20
electron/electron.js
vendored
@ -43,6 +43,7 @@ const PDFDocument = require('pdf-lib').PDFDocument;
|
|||||||
|
|
||||||
// 本地模块和配置
|
// 本地模块和配置
|
||||||
const utils = require('./lib/utils');
|
const utils = require('./lib/utils');
|
||||||
|
const navigation = require('./lib/navigation');
|
||||||
const config = require('./package.json');
|
const config = require('./package.json');
|
||||||
const electronDown = require("./electron-down");
|
const electronDown = require("./electron-down");
|
||||||
const electronMenu = require("./electron-menu");
|
const electronMenu = require("./electron-menu");
|
||||||
@ -367,6 +368,9 @@ function createMainWindow() {
|
|||||||
// 设置右键菜单
|
// 设置右键菜单
|
||||||
electronMenu.webContentsMenu(mainWindow.webContents)
|
electronMenu.webContentsMenu(mainWindow.webContents)
|
||||||
|
|
||||||
|
// 设置导航快捷键(返回/前进)
|
||||||
|
navigation.setup(mainWindow)
|
||||||
|
|
||||||
// 加载地址
|
// 加载地址
|
||||||
utils.loadUrl(mainWindow, serverUrl)
|
utils.loadUrl(mainWindow, serverUrl)
|
||||||
}
|
}
|
||||||
@ -623,6 +627,9 @@ function createChildWindow(args) {
|
|||||||
// 设置右键菜单
|
// 设置右键菜单
|
||||||
electronMenu.webContentsMenu(browser.webContents)
|
electronMenu.webContentsMenu(browser.webContents)
|
||||||
|
|
||||||
|
// 设置导航快捷键(返回/前进)
|
||||||
|
navigation.setup(browser)
|
||||||
|
|
||||||
// 加载地址
|
// 加载地址
|
||||||
const hash = `${args.hash || args.path}`;
|
const hash = `${args.hash || args.path}`;
|
||||||
if (/^https?:/i.test(hash)) {
|
if (/^https?:/i.test(hash)) {
|
||||||
@ -848,9 +855,20 @@ function createWebTabWindow(args) {
|
|||||||
webTabClosedByShortcut = true
|
webTabClosedByShortcut = true
|
||||||
} else if (utils.isMetaOrControl(input) && input.shift && input.key.toLowerCase() === 'i') {
|
} else if (utils.isMetaOrControl(input) && input.shift && input.key.toLowerCase() === 'i') {
|
||||||
devToolsWebTab(0)
|
devToolsWebTab(0)
|
||||||
|
} else {
|
||||||
|
const item = currentWebTab()
|
||||||
|
if (item) {
|
||||||
|
navigation.handleInput(event, input, item.view.webContents)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 设置鼠标侧键和触控板手势导航
|
||||||
|
navigation.setupWindowEvents(webTabWindow, () => {
|
||||||
|
const item = currentWebTab()
|
||||||
|
return item ? item.view.webContents : null
|
||||||
|
})
|
||||||
|
|
||||||
webTabWindow.loadFile('./render/tabs/index.html', {}).then(_ => { }).catch(_ => { })
|
webTabWindow.loadFile('./render/tabs/index.html', {}).then(_ => { }).catch(_ => { })
|
||||||
}
|
}
|
||||||
if (webTabWindow.isMinimized()) {
|
if (webTabWindow.isMinimized()) {
|
||||||
@ -962,6 +980,8 @@ function createWebTabWindow(args) {
|
|||||||
webTabClosedByShortcut = true
|
webTabClosedByShortcut = true
|
||||||
} else if (utils.isMetaOrControl(input) && input.shift && input.key.toLowerCase() === 'i') {
|
} else if (utils.isMetaOrControl(input) && input.shift && input.key.toLowerCase() === 'i') {
|
||||||
browserView.webContents.toggleDevTools()
|
browserView.webContents.toggleDevTools()
|
||||||
|
} else {
|
||||||
|
navigation.handleInput(event, input, browserView.webContents)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
235
electron/lib/navigation.js
vendored
Normal file
235
electron/lib/navigation.js
vendored
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
/**
|
||||||
|
* 窗口导航相关工具函数
|
||||||
|
*
|
||||||
|
* 规则:
|
||||||
|
* - 顶层页面是 localhost 时:禁用顶层 goBack/goForward,
|
||||||
|
* 避免影响应用主路由历史(例如 Vue hash 路由)。
|
||||||
|
* - 若此时焦点在 iframe 内:允许 iframe 自己执行 history.back()/history.forward()(不影响顶层路由)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
const utils = require('./utils')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {'back'|'forward'} NavDirection
|
||||||
|
*/
|
||||||
|
|
||||||
|
function getWebContentsUrl(webContents) {
|
||||||
|
try {
|
||||||
|
return webContents?.getURL?.() || ''
|
||||||
|
} catch (e) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBackKey(input) {
|
||||||
|
return (input.alt && input.key === 'ArrowLeft') || (input.meta && input.key === '[')
|
||||||
|
}
|
||||||
|
|
||||||
|
function isForwardKey(input) {
|
||||||
|
return (input.alt && input.key === 'ArrowRight') || (input.meta && input.key === ']')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尝试从 Electron 提供的 focusedFrame 获取当前聚焦 frame(兼容属性/方法两种形态)
|
||||||
|
* @param webContents
|
||||||
|
* @returns {Electron.WebFrameMain|null}
|
||||||
|
*/
|
||||||
|
function getFocusedFrameDirect(webContents) {
|
||||||
|
const focused = webContents?.focusedFrame
|
||||||
|
if (!focused) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return typeof focused === 'function' ? focused.call(webContents) : focused
|
||||||
|
} catch (e) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前聚焦的 Frame(优先返回更深层的 iframe)
|
||||||
|
* @param webContents
|
||||||
|
* @returns {Promise<Electron.WebFrameMain|null>}
|
||||||
|
*/
|
||||||
|
async function getFocusedFrame(webContents) {
|
||||||
|
const mainFrame = webContents?.mainFrame
|
||||||
|
if (!mainFrame) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const direct = getFocusedFrameDirect(webContents)
|
||||||
|
if (direct) {
|
||||||
|
return direct
|
||||||
|
}
|
||||||
|
|
||||||
|
const frames = Array.isArray(mainFrame.framesInSubtree) && mainFrame.framesInSubtree.length > 0
|
||||||
|
? mainFrame.framesInSubtree
|
||||||
|
: [mainFrame]
|
||||||
|
|
||||||
|
// document.hasFocus() 可能在主文档与子 frame 同时为 true,因此取“最深”的那个
|
||||||
|
const focusedList = await Promise.all(frames.map((frame) => {
|
||||||
|
if (!frame?.executeJavaScript) {
|
||||||
|
return Promise.resolve(false)
|
||||||
|
}
|
||||||
|
return frame
|
||||||
|
.executeJavaScript('document.hasFocus && document.hasFocus()')
|
||||||
|
.then(Boolean)
|
||||||
|
.catch(() => false)
|
||||||
|
}))
|
||||||
|
|
||||||
|
for (let i = focusedList.length - 1; i >= 0; i--) {
|
||||||
|
if (focusedList[i]) {
|
||||||
|
return frames[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mainFrame
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在“聚焦 iframe”内执行前进/后退(不影响顶层路由历史)
|
||||||
|
* @param webContents
|
||||||
|
* @param {NavDirection} direction
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async function navigateFocusedSubframe(webContents, direction) {
|
||||||
|
const mainFrame = webContents?.mainFrame
|
||||||
|
if (!mainFrame) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const frame = await getFocusedFrame(webContents)
|
||||||
|
if (!frame || frame === mainFrame) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const js = direction === 'forward' ? 'history.forward()' : 'history.back()'
|
||||||
|
try {
|
||||||
|
await frame.executeJavaScript(js)
|
||||||
|
return true
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尝试在顶层 webContents 上执行前进/后退
|
||||||
|
* @param webContents
|
||||||
|
* @param {NavDirection} direction
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function navigateTopLevel(webContents, direction) {
|
||||||
|
if (!webContents) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (direction === 'back') {
|
||||||
|
if (!webContents.canGoBack()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
webContents.goBack()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (direction === 'forward') {
|
||||||
|
if (!webContents.canGoForward()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
webContents.goForward()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一导航入口:
|
||||||
|
* - 顶层 intranet:阻止顶层导航,尝试让聚焦 iframe 自己 history.back/forward
|
||||||
|
* - 外网:正常顶层导航
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
* @param webContents
|
||||||
|
* @param {NavDirection} direction
|
||||||
|
* @returns {boolean} 是否“接管”了这次操作(intranet 总是接管)
|
||||||
|
*/
|
||||||
|
function handleDirection(event, webContents, direction) {
|
||||||
|
const url = getWebContentsUrl(webContents)
|
||||||
|
const intranet = utils.isLocalHost(url)
|
||||||
|
|
||||||
|
if (intranet) {
|
||||||
|
event?.preventDefault?.()
|
||||||
|
navigateFocusedSubframe(webContents, direction)
|
||||||
|
.catch(() => {})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const ok = navigateTopLevel(webContents, direction)
|
||||||
|
if (ok) {
|
||||||
|
event?.preventDefault?.()
|
||||||
|
}
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDirectionFromInput(input) {
|
||||||
|
if (isBackKey(input)) return 'back'
|
||||||
|
if (isForwardKey(input)) return 'forward'
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDirectionFromAppCommand(cmd) {
|
||||||
|
if (cmd === 'browser-backward') return 'back'
|
||||||
|
if (cmd === 'browser-forward') return 'forward'
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDirectionFromSwipe(direction) {
|
||||||
|
// macOS swipe: left = back, right = forward(与当前用户验证一致)
|
||||||
|
if (direction === 'left') return 'back'
|
||||||
|
if (direction === 'right') return 'forward'
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInput(event, input, webContents) {
|
||||||
|
const direction = resolveDirectionFromInput(input)
|
||||||
|
if (!direction) return false
|
||||||
|
return handleDirection(event, webContents, direction)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupWindowEvents(win, getWebContents) {
|
||||||
|
if (!win) return
|
||||||
|
|
||||||
|
win.on('app-command', (event, cmd) => {
|
||||||
|
const direction = resolveDirectionFromAppCommand(cmd)
|
||||||
|
if (!direction) return
|
||||||
|
const webContents = getWebContents?.()
|
||||||
|
if (!webContents) return
|
||||||
|
handleDirection(event, webContents, direction)
|
||||||
|
})
|
||||||
|
|
||||||
|
win.on('swipe', (event, direction) => {
|
||||||
|
const navDirection = resolveDirectionFromSwipe(direction)
|
||||||
|
if (!navDirection) return
|
||||||
|
const webContents = getWebContents?.()
|
||||||
|
if (!webContents) return
|
||||||
|
handleDirection(event, webContents, navDirection)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup(win) {
|
||||||
|
if (!win || !win.webContents) return
|
||||||
|
|
||||||
|
win.webContents.on('before-input-event', (event, input) => {
|
||||||
|
handleInput(event, input, win.webContents)
|
||||||
|
})
|
||||||
|
|
||||||
|
setupWindowEvents(win, () => win.webContents)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
isBackKey,
|
||||||
|
isForwardKey,
|
||||||
|
getWebContentsUrl,
|
||||||
|
getFocusedFrame,
|
||||||
|
navigateFocusedSubframe,
|
||||||
|
handleInput,
|
||||||
|
setupWindowEvents,
|
||||||
|
setup,
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user