191 lines
6.5 KiB
TypeScript
191 lines
6.5 KiB
TypeScript
/**
|
||
* AppShell — 应用壳
|
||
*
|
||
* 窗口拖拽约定(Tauri overlay title bar):
|
||
* - 仅使用 data-tauri-drag-region 处理拖拽和双击缩放
|
||
* - 不要再叠加 startDragging() 或 CSS app-region: drag,两者会抢鼠标导致窗口拖不动
|
||
* - 标签栏空白区也由 Tauri 原生拖拽区处理
|
||
*
|
||
* RDP/VNC 全屏时只隐藏 chrome,保持 TabContent 挂载,避免会话 remount 立刻退出全屏。
|
||
*/
|
||
|
||
import type { ReactNode } from 'react'
|
||
import { useTranslation } from 'react-i18next'
|
||
import { ChevronsRight } from 'lucide-react'
|
||
import Sidebar from '../components/layout/Sidebar'
|
||
import TabBar from '../components/layout/TabBar'
|
||
import TabContent from '../components/layout/TabContent'
|
||
import StatusBar from '../components/layout/StatusBar'
|
||
import type { Session } from '../stores'
|
||
import { useDesktopFullscreenStore } from '../stores/desktopFullscreenStore'
|
||
import { getDesktopPlatform } from '../platform/runtime'
|
||
|
||
export interface AppShellProps {
|
||
activeTabId: string | null
|
||
isMobile: boolean
|
||
sidebarCollapsed: boolean
|
||
mobileSidebarOpen: boolean
|
||
onToggleSidebar: () => void
|
||
onToggleMobileSidebar: () => void
|
||
onCloseMobileSidebar: () => void
|
||
onNewSession: (group?: string) => void
|
||
onQuickConnect: () => void
|
||
onRdpConnect?: () => void
|
||
onVncConnect?: () => void
|
||
onImportSessions?: () => void
|
||
onOpenLauncher: () => void
|
||
onEditSession: (session: Session) => void
|
||
children?: ReactNode
|
||
}
|
||
|
||
function TitleBar() {
|
||
const platform = getDesktopPlatform()
|
||
// Overlay title bar is macOS-only; Win/Linux keep the native chrome.
|
||
if (platform !== 'macos') return null
|
||
|
||
return (
|
||
<header
|
||
data-tauri-drag-region
|
||
className="bg-dark-900/95 relative z-20 flex h-10 flex-shrink-0 select-none items-center border-b border-dark-800"
|
||
>
|
||
<div data-tauri-drag-region className="h-full w-[78px] flex-shrink-0" aria-hidden />
|
||
<div data-tauri-drag-region className="flex h-full min-w-0 flex-1 items-center justify-center px-2" />
|
||
<div data-tauri-drag-region className="h-full w-[78px] flex-shrink-0" aria-hidden />
|
||
</header>
|
||
)
|
||
}
|
||
|
||
function MobileMenuButton({ onClick }: { onClick: () => void }) {
|
||
const { t } = useTranslation('common')
|
||
return (
|
||
<button
|
||
type="button"
|
||
onClick={onClick}
|
||
className="titlebar-no-drag flex h-10 w-10 items-center justify-center text-txt-muted transition-colors hover:bg-dark-800 hover:text-content-primary"
|
||
aria-label={t('sidebar.toggle')}
|
||
>
|
||
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden>
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||
</svg>
|
||
</button>
|
||
)
|
||
}
|
||
|
||
function ExpandSidebarButton({ onClick }: { onClick: () => void }) {
|
||
const { t } = useTranslation('common')
|
||
return (
|
||
<button
|
||
type="button"
|
||
onClick={onClick}
|
||
className="titlebar-no-drag border-dark-700/30 hover:bg-dark-700/50 group flex h-10 w-9 flex-shrink-0 items-center justify-center border-r text-txt-faint transition-colors hover:text-txt-muted"
|
||
title={t('sidebar.expand')}
|
||
aria-label={t('sidebar.expandAria')}
|
||
>
|
||
<ChevronsRight className="h-3.5 w-3.5 opacity-70 transition-opacity group-hover:opacity-100" strokeWidth={1.75} />
|
||
</button>
|
||
)
|
||
}
|
||
|
||
export default function AppShell({
|
||
activeTabId,
|
||
isMobile,
|
||
sidebarCollapsed,
|
||
mobileSidebarOpen,
|
||
onToggleSidebar,
|
||
onToggleMobileSidebar,
|
||
onCloseMobileSidebar,
|
||
onNewSession,
|
||
onQuickConnect,
|
||
onRdpConnect,
|
||
onVncConnect,
|
||
onImportSessions,
|
||
onOpenLauncher,
|
||
onEditSession,
|
||
children,
|
||
}: AppShellProps) {
|
||
const handleSidebarNewSession = (group?: string) => {
|
||
onNewSession(group)
|
||
if (isMobile) onCloseMobileSidebar()
|
||
}
|
||
|
||
const handleSidebarQuickConnect = () => {
|
||
onQuickConnect()
|
||
if (isMobile) onCloseMobileSidebar()
|
||
}
|
||
|
||
const handleSidebarEditSession = (session: Session) => {
|
||
onEditSession(session)
|
||
if (isMobile) onCloseMobileSidebar()
|
||
}
|
||
|
||
const desktopSidebarHidden = !isMobile && sidebarCollapsed
|
||
const desktopFullscreen = useDesktopFullscreenStore((state) => state.active)
|
||
|
||
return (
|
||
<div className={`app-shell text-content-primary ${desktopFullscreen ? 'bg-black' : 'bg-dark-900'}`}>
|
||
{!desktopFullscreen && <TitleBar />}
|
||
|
||
<div className="flex min-h-0 flex-1 overflow-hidden">
|
||
{!desktopFullscreen && isMobile && mobileSidebarOpen && (
|
||
<div
|
||
className="titlebar-no-drag fixed inset-0 z-40 bg-black/50 transition-opacity duration-300"
|
||
onClick={onCloseMobileSidebar}
|
||
aria-hidden
|
||
/>
|
||
)}
|
||
|
||
{!desktopFullscreen && (
|
||
<aside
|
||
className={`titlebar-no-drag h-full min-h-0 flex-shrink-0 transition-[width,transform] duration-200 ease-in-out ${
|
||
isMobile
|
||
? `fixed inset-y-0 left-0 top-10 z-50 ${mobileSidebarOpen ? 'translate-x-0' : '-translate-x-full'}`
|
||
: desktopSidebarHidden
|
||
? 'w-0 overflow-hidden border-0'
|
||
: ''
|
||
} `}
|
||
aria-hidden={desktopSidebarHidden}
|
||
>
|
||
<Sidebar
|
||
onNewSession={handleSidebarNewSession}
|
||
onQuickConnect={handleSidebarQuickConnect}
|
||
onEditSession={handleSidebarEditSession}
|
||
onCollapse={isMobile ? undefined : onToggleSidebar}
|
||
/>
|
||
</aside>
|
||
)}
|
||
|
||
<main className={`flex min-h-0 min-w-0 flex-1 flex-col ${desktopFullscreen ? 'bg-black' : 'bg-dark-800'}`}>
|
||
{!desktopFullscreen && (
|
||
<div className="flex items-stretch">
|
||
{isMobile && <MobileMenuButton onClick={onToggleMobileSidebar} />}
|
||
{desktopSidebarHidden && <ExpandSidebarButton onClick={onToggleSidebar} />}
|
||
<div className="min-w-0 flex-1">
|
||
<TabBar onOpenLauncher={onOpenLauncher} />
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="titlebar-no-drag min-h-0 flex-1 overflow-hidden">
|
||
<TabContent
|
||
activeTabId={activeTabId}
|
||
onNewSession={onNewSession}
|
||
onQuickConnect={onQuickConnect}
|
||
onRdpConnect={onRdpConnect}
|
||
onVncConnect={onVncConnect}
|
||
onImportSessions={onImportSessions}
|
||
/>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
{!desktopFullscreen && (
|
||
<div className="titlebar-no-drag flex-shrink-0">
|
||
<StatusBar />
|
||
</div>
|
||
)}
|
||
|
||
<div className="contents">{children}</div>
|
||
</div>
|
||
)
|
||
}
|