Files
clash-verge-rev/src/pages/_layout.tsx
T

180 lines
4.9 KiB
TypeScript

import { Paper, ThemeProvider } from '@mui/material'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { Outlet, useNavigate } from 'react-router'
import { BaseErrorBoundary } from '@/components/base'
import { LayoutSidebar } from '@/components/layout/layout-sidebar'
import { NoticeManager } from '@/components/layout/notice-manager'
import { ServiceMigrationDialog } from '@/components/layout/service-migration-dialog'
import { SysproxyPrivilegeDialog } from '@/components/layout/sysproxy-privilege-dialog'
import {
WindowControls,
WindowResizeHandles,
} from '@/components/layout/window-controller'
import { useI18n } from '@/hooks/use-i18n'
import { useVerge } from '@/hooks/use-verge'
import { useWindowDecorations } from '@/hooks/use-window'
import { useThemeMode } from '@/services/states'
import getSystem from '@/utils/get-system'
import {
useCustomTheme,
useLayoutEvents,
useLoadingOverlay,
usePendingFailures,
} from './_layout/hooks'
import { handleNoticeMessage } from './_layout/utils'
import 'dayjs/locale/ru'
import 'dayjs/locale/zh-cn'
dayjs.extend(relativeTime)
const OS = getSystem()
const Layout = () => {
const mode = useThemeMode()
const isDark = mode !== 'light'
const { t } = useTranslation()
const { theme } = useCustomTheme()
const { verge } = useVerge()
const { language } = verge ?? {}
const navCollapsed = verge?.collapse_navbar ?? false
const { switchLanguage } = useI18n()
const navigate = useNavigate()
const themeReady = useMemo(() => Boolean(theme), [theme])
const windowControlsRef = useRef<any>(null)
const { decorated } = useWindowDecorations()
const customTitlebar = useMemo(
() =>
decorated === false ? (
<div className="the_titlebar">
<div
className="the_titlebar-drag-region"
data-tauri-drag-region="true"
/>
<WindowControls ref={windowControlsRef} />
</div>
) : null,
[decorated],
)
useLoadingOverlay(themeReady)
const handleNotice = useCallback(
(payload: [string, string]) => {
const [status, msg] = payload
try {
handleNoticeMessage(status, msg, t, navigate)
} catch (error) {
console.error('[通知处理] 失败:', error)
}
},
[t, navigate],
)
useLayoutEvents(handleNotice)
usePendingFailures()
useEffect(() => {
if (language) {
dayjs.locale(language === 'zh' ? 'zh-cn' : language)
switchLanguage(language)
}
}, [language, switchLanguage])
if (!themeReady) {
return (
<div
style={{
width: '100vw',
height: '100vh',
background: mode === 'light' ? '#fff' : '#181a1b',
transition: 'background 0.2s',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: mode === 'light' ? '#333' : '#fff',
}}
></div>
)
}
return (
<ThemeProvider theme={theme}>
{/* 左侧底部窗口控制按钮 */}
<NoticeManager position={verge?.notice_position} />
<ServiceMigrationDialog />
<SysproxyPrivilegeDialog />
<div
style={{
animation: 'fadeIn 0.5s',
WebkitAnimation: 'fadeIn 0.5s',
}}
/>
<style>
{`
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
`}
</style>
<Paper
square
elevation={0}
className={`${OS} layout${navCollapsed ? ' layout--nav-collapsed' : ''}`}
style={{
borderTopLeftRadius: '0px',
borderTopRightRadius: '0px',
}}
onContextMenu={(e) => {
if (
OS === 'windows' &&
!['input', 'textarea'].includes(
e.currentTarget.tagName.toLowerCase(),
) &&
!e.currentTarget.isContentEditable
) {
e.preventDefault()
}
}}
sx={[
({ palette }) => ({ bgcolor: palette.background.paper }),
OS === 'linux'
? {
borderRadius: '8px',
width: '100vw',
height: '100vh',
}
: {},
]}
>
{decorated === false && <WindowResizeHandles />}
{/* Custom titlebar - rendered only when decorated is false, memoized for performance */}
{customTitlebar}
<div className="layout-content">
<LayoutSidebar isDark={isDark} isCollapsed={navCollapsed} />
<div className="layout-content__right">
<div className="the-bar"></div>
<div className="the-content">
<BaseErrorBoundary>
<Outlet />
</BaseErrorBoundary>
</div>
</div>
</div>
</Paper>
</ThemeProvider>
)
}
export default Layout