|
@@ -1,4 +1,5 @@
|
|
|
import React, { useState, useEffect, useRef, useCallback } from 'react'
|
|
import React, { useState, useEffect, useRef, useCallback } from 'react'
|
|
|
|
|
+import { BsX } from 'react-icons/bs'
|
|
|
|
|
|
|
|
interface Tab {
|
|
interface Tab {
|
|
|
id: string
|
|
id: string
|
|
@@ -27,6 +28,11 @@ const BrowserWindow: React.FC = () => {
|
|
|
setActiveTabId(id)
|
|
setActiveTabId(id)
|
|
|
}, [])
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
+ const handleTabClosed = useCallback((_event: unknown, id: string, nextActiveTabId: string | null) => {
|
|
|
|
|
+ setTabs((prev) => prev.filter((tab) => tab.id !== id))
|
|
|
|
|
+ setActiveTabId((prev) => nextActiveTabId ?? (prev === id ? null : prev))
|
|
|
|
|
+ }, [])
|
|
|
|
|
+
|
|
|
const handleTabsSync = useCallback(
|
|
const handleTabsSync = useCallback(
|
|
|
(_event: unknown, snapshots: Array<{ id: string; url: string; title: string }>) => {
|
|
(_event: unknown, snapshots: Array<{ id: string; url: string; title: string }>) => {
|
|
|
if (!snapshots?.length) return
|
|
if (!snapshots?.length) return
|
|
@@ -70,6 +76,7 @@ const BrowserWindow: React.FC = () => {
|
|
|
ipc.on('tab-created', handleTabCreated)
|
|
ipc.on('tab-created', handleTabCreated)
|
|
|
ipc.on('tab-update', handleTabUpdate)
|
|
ipc.on('tab-update', handleTabUpdate)
|
|
|
ipc.on('tab-activate', handleTabActivate)
|
|
ipc.on('tab-activate', handleTabActivate)
|
|
|
|
|
+ ipc.on('tab-closed', handleTabClosed)
|
|
|
ipc.on('browser-tabs-sync', handleTabsSync)
|
|
ipc.on('browser-tabs-sync', handleTabsSync)
|
|
|
ipc.send('browser-action', { type: 'browser-ui-ready' })
|
|
ipc.send('browser-action', { type: 'browser-ui-ready' })
|
|
|
|
|
|
|
@@ -80,10 +87,11 @@ const BrowserWindow: React.FC = () => {
|
|
|
ipc.removeListener('tab-created', handleTabCreated)
|
|
ipc.removeListener('tab-created', handleTabCreated)
|
|
|
ipc.removeListener('tab-update', handleTabUpdate)
|
|
ipc.removeListener('tab-update', handleTabUpdate)
|
|
|
ipc.removeListener('tab-activate', handleTabActivate)
|
|
ipc.removeListener('tab-activate', handleTabActivate)
|
|
|
|
|
+ ipc.removeListener('tab-closed', handleTabClosed)
|
|
|
ipc.removeListener('browser-tabs-sync', handleTabsSync)
|
|
ipc.removeListener('browser-tabs-sync', handleTabsSync)
|
|
|
window.removeEventListener('resize', updateViewBounds)
|
|
window.removeEventListener('resize', updateViewBounds)
|
|
|
}
|
|
}
|
|
|
- }, [handleTabActivate, handleTabCreated, handleTabUpdate, handleTabsSync, updateViewBounds])
|
|
|
|
|
|
|
+ }, [handleTabActivate, handleTabClosed, handleTabCreated, handleTabUpdate, handleTabsSync, updateViewBounds])
|
|
|
|
|
|
|
|
// 当 tabs 或 activeTabId 变化导致布局变化时,调整 BrowserView 的 bounds
|
|
// 当 tabs 或 activeTabId 变化导致布局变化时,调整 BrowserView 的 bounds
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
@@ -95,6 +103,14 @@ const BrowserWindow: React.FC = () => {
|
|
|
window.electron.ipcRenderer.send('browser-action', { type: 'switch-tab', id })
|
|
window.electron.ipcRenderer.send('browser-action', { type: 'switch-tab', id })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ const handleCloseTab = (event: React.MouseEvent<HTMLButtonElement>, id: string) => {
|
|
|
|
|
+ event.stopPropagation()
|
|
|
|
|
+ if (tabs.length <= 1) return
|
|
|
|
|
+ window.electron.ipcRenderer.send('browser-action', { type: 'close-tab', id })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const canCloseTabs = tabs.length > 1
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<div className="browser-window" style={{ display: 'flex', flexDirection: 'column', height: '100vh', width: '100vw', backgroundColor: 'var(--im-bg)', overflow: 'hidden' }}>
|
|
<div className="browser-window" style={{ display: 'flex', flexDirection: 'column', height: '100vh', width: '100vw', backgroundColor: 'var(--im-bg)', overflow: 'hidden' }}>
|
|
|
{/* Title Bar / Tab Bar */}
|
|
{/* Title Bar / Tab Bar */}
|
|
@@ -128,6 +144,20 @@ const BrowserWindow: React.FC = () => {
|
|
|
<div style={{ flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
<div style={{ flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
|
{tab.title || 'Loading...'}
|
|
{tab.title || 'Loading...'}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ className="browser-tab-close"
|
|
|
|
|
+ onClick={(event) => handleCloseTab(event, tab.id)}
|
|
|
|
|
+ disabled={!canCloseTabs}
|
|
|
|
|
+ aria-label="关闭标签页"
|
|
|
|
|
+ title={canCloseTabs ? '关闭标签页' : '最后一个标签页不能关闭,请关闭窗口'}
|
|
|
|
|
+ style={{
|
|
|
|
|
+ color: activeTabId === tab.id ? 'var(--im-text-muted)' : '#94a3b8',
|
|
|
|
|
+ visibility: canCloseTabs ? 'visible' : 'hidden'
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <BsX size={14} />
|
|
|
|
|
+ </button>
|
|
|
</div>
|
|
</div>
|
|
|
))}
|
|
))}
|
|
|
</div>
|
|
</div>
|
|
@@ -153,6 +183,30 @@ const BrowserWindow: React.FC = () => {
|
|
|
0% { transform: rotate(0deg); }
|
|
0% { transform: rotate(0deg); }
|
|
|
100% { transform: rotate(360deg); }
|
|
100% { transform: rotate(360deg); }
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ .browser-tab-close {
|
|
|
|
|
+ width: 20px;
|
|
|
|
|
+ height: 20px;
|
|
|
|
|
+ margin-left: 6px;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .browser-tab-close:hover:not(:disabled) {
|
|
|
|
|
+ background: rgba(15, 23, 42, 0.08);
|
|
|
|
|
+ color: var(--im-text);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .browser-tab-close:disabled {
|
|
|
|
|
+ cursor: default;
|
|
|
|
|
+ }
|
|
|
`}</style>
|
|
`}</style>
|
|
|
</div>
|
|
</div>
|
|
|
)
|
|
)
|