Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 57 additions & 2 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,17 @@ import { base_path, bus, handleRespWithoutAuthAndNotify, r } from "~/utils"
import { MustUser, UserOrGuest } from "./MustUser"
import "./index.css"
import { globalStyles } from "./theme"
import { MusicPlayer } from "~/pages/media/music/MusicLibrary"
import { RootLayout } from "./RootLayout"

const Home = lazy(() => import("~/pages/home/Layout"))
const Manage = lazy(() => import("~/pages/manage"))
const Login = lazy(() => import("~/pages/login"))
const Test = lazy(() => import("~/pages/test"))
const VideoLibrary = lazy(() => import("~/pages/media/video/VideoLibrary"))
const MusicLibrary = lazy(() => import("~/pages/media/music/MusicLibrary"))
const ImageLibrary = lazy(() => import("~/pages/media/image/ImageLibrary"))
const BookLibrary = lazy(() => import("~/pages/media/book/BookLibrary"))
Comment on lines +22 to +32
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MusicPlayer is imported from ~/pages/media/music/MusicLibrary, which likely defeats the lazy(() => import(...MusicLibrary)) route-level code-splitting (the music module becomes part of the initial bundle). Extract MusicPlayer/playerState into a separate lightweight module and import that here, keeping the full MusicLibrary page lazily loaded.

Copilot uses AI. Check for mistakes.

const App: Component = () => {
const t = useT()
Expand Down Expand Up @@ -91,19 +97,68 @@ const App: Component = () => {
</MustUser>
}
/>
{/* 带侧边栏的路由:媒体库各页面 */}
<Route
path="/@media/video/*"
element={
<MustUser>
<RootLayout>
<MusicPlayer />
<VideoLibrary />
</RootLayout>
</MustUser>
}
/>
<Route
path="/@media/music/*"
element={
<MustUser>
<RootLayout>
<MusicPlayer />
<MusicLibrary />
</RootLayout>
</MustUser>
}
/>
<Route
path="/@media/image/*"
element={
<MustUser>
<RootLayout>
<MusicPlayer />
<ImageLibrary />
</RootLayout>
</MustUser>
}
/>
<Route
path="/@media/books/*"
element={
<MustUser>
<RootLayout>
<MusicPlayer />
<BookLibrary />
</RootLayout>
</MustUser>
}
/>
<Route
path={["/@s/*", "/%40s/*"]}
element={
<UserOrGuest>
<Home />
<RootLayout>
<Home />
</RootLayout>
</UserOrGuest>
}
/>
<Route
path="*"
element={
<MustUser>
<Home />
<RootLayout>
<Home />
</RootLayout>
</MustUser>
}
/>
Expand Down
248 changes: 248 additions & 0 deletions src/app/RootLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
import {
JSX,
createSignal,
onMount,
onCleanup,
createMemo,
Show,
} from "solid-js"
import {
GlobalSidebar,
sidebarCollapsed,
setSidebarCollapsed,
} from "~/components/GlobalSidebar"
import { useColorMode, Icon } from "@hope-ui/solid"
import { TbChevronLeft, TbChevronRight } from "solid-icons/tb"
import { Nav } from "~/pages/home/Nav"
import { Layout } from "~/pages/home/header/layout"
import { TopBarActions } from "~/pages/home/toolbar/Right"
import { useRouter } from "~/hooks"
import { getSetting, objStore, State } from "~/store"
import { BsSearch } from "solid-icons/bs"
import { bus } from "~/utils"

interface RootLayoutProps {
children: JSX.Element
}

// ─── 顶栏组件 ────────────────────────────────────────────────
const TopBar = () => {
const { colorMode } = useColorMode()
const isDark = createMemo(() => colorMode() === "dark")
const { pathname } = useRouter()

// 只在文件浏览路由下显示面包屑和文件操作
const isFileBrowser = createMemo(() => !pathname().startsWith("/@media"))
const isFolder = createMemo(() => objStore.state === State.Folder)

const bg = createMemo(() =>
isDark() ? "rgba(15,20,35,0.95)" : "rgba(250,251,253,0.97)",
)
const borderColor = createMemo(() =>
isDark() ? "rgba(255,255,255,0.07)" : "rgba(0,0,0,0.07)",
)
const textColor = createMemo(() => (isDark() ? "#e2e8f0" : "#1e293b"))
const mutedColor = createMemo(() => (isDark() ? "#64748b" : "#94a3b8"))

return (
<div
style={{
position: "sticky",
top: "0",
"z-index": "50",
height: "60px",
display: "flex",
"align-items": "center",
padding: "0 16px",
background: bg(),
"border-bottom": `1px solid ${borderColor()}`,
"backdrop-filter": "blur(12px) saturate(160%)",
"-webkit-backdrop-filter": "blur(12px) saturate(160%)",
"flex-shrink": "0",
gap: "8px",
}}
>
{/* 面包屑导航 / 页面标题 */}
<div style={{ flex: "1", "min-width": "0", overflow: "hidden" }}>
<Show
when={isFileBrowser()}
fallback={
<span
style={{
color: textColor(),
"font-size": "14px",
"font-weight": "600",
}}
>
📺 媒体库
</span>
}
>
<Nav />
</Show>
</div>

{/* 右侧工具区 */}
<div
style={{
display: "flex",
"align-items": "center",
gap: "6px",
"flex-shrink": "0",
}}
>
{/* 侧边栏收起/展开按钮 */}
<button
onClick={() => setSidebarCollapsed(!sidebarCollapsed())}
title={sidebarCollapsed() ? "展开侧边栏" : "收起侧边栏"}
style={{
background: isDark()
? "rgba(255,255,255,0.06)"
: "rgba(0,0,0,0.05)",
border: `1px solid ${isDark() ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.08)"}`,
"border-radius": "7px",
width: "28px",
height: "28px",
cursor: "pointer",
display: "flex",
"align-items": "center",
"justify-content": "center",
color: mutedColor(),
transition: "all 0.18s ease",
"flex-shrink": "0",
}}
onMouseEnter={(e) => {
const el = e.currentTarget as HTMLButtonElement
el.style.background = isDark()
? "rgba(59,130,246,0.15)"
: "rgba(59,130,246,0.08)"
el.style.color = "#3b82f6"
el.style.borderColor = "rgba(59,130,246,0.3)"
}}
onMouseLeave={(e) => {
const el = e.currentTarget as HTMLButtonElement
el.style.background = isDark()
? "rgba(255,255,255,0.06)"
: "rgba(0,0,0,0.05)"
el.style.color = mutedColor()
el.style.borderColor = isDark()
? "rgba(255,255,255,0.08)"
: "rgba(0,0,0,0.08)"
}}
>
<Icon
as={sidebarCollapsed() ? TbChevronRight : TbChevronLeft}
boxSize="14px"
/>
</button>

{/* 搜索按钮和布局切换(仅文件浏览时显示) */}
<Show when={isFileBrowser()}>
{/* 搜索按钮 */}
<Show when={isFolder() && getSetting("search_index") !== "none"}>
<button
title="搜索 (Ctrl+K)"
onClick={() => bus.emit("tool", "search")}
style={{
background: isDark()
? "rgba(255,255,255,0.06)"
: "rgba(0,0,0,0.05)",
border: `1px solid ${isDark() ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.08)"}`,
"border-radius": "7px",
height: "30px",
padding: "0 10px",
cursor: "pointer",
display: "flex",
"align-items": "center",
gap: "6px",
color: mutedColor(),
"font-size": "12px",
transition: "all 0.18s ease",
}}
onMouseEnter={(e) => {
const el = e.currentTarget as HTMLButtonElement
el.style.background = isDark()
? "rgba(59,130,246,0.15)"
: "rgba(59,130,246,0.08)"
el.style.color = "#3b82f6"
el.style.borderColor = "rgba(59,130,246,0.3)"
}}
onMouseLeave={(e) => {
const el = e.currentTarget as HTMLButtonElement
el.style.background = isDark()
? "rgba(255,255,255,0.06)"
: "rgba(0,0,0,0.05)"
el.style.color = mutedColor()
el.style.borderColor = isDark()
? "rgba(255,255,255,0.08)"
: "rgba(0,0,0,0.08)"
}}
>
<Icon as={BsSearch} boxSize="13px" />
<span>搜索</span>
</button>
</Show>

{/* 工具操作按钮 */}
<TopBarActions />

{/* 布局切换 */}
<Show when={isFolder()}>
<Layout />
</Show>
</Show>
</div>
</div>
)
}

// ─── 根布局 ──────────────────────────────────────────────────
export const RootLayout = (props: RootLayoutProps) => {
const [isMobile, setIsMobile] = createSignal(
typeof window !== "undefined" ? window.innerWidth < 768 : false,
)

onMount(() => {
const handler = () => setIsMobile(window.innerWidth < 768)
window.addEventListener("resize", handler)
onCleanup(() => window.removeEventListener("resize", handler))
})

// 与 GlobalSidebar 中的 sidebarWidth 保持一致:180px / 56px
const marginLeft = createMemo(() => {
if (isMobile()) return "0px"
return sidebarCollapsed() ? "48px" : "120px"
})
Comment on lines +211 to +215
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

margin-left is inconsistent with the actual sidebar width. GlobalSidebar uses 48px (collapsed) / 130px (expanded), but here the expanded margin is 120px, which will cause the content to overlap the sidebar by ~10px. Consider deriving this from a shared constant or exporting the computed width from GlobalSidebar to avoid drift.

Copilot uses AI. Check for mistakes.

return (
<div style={{ display: "flex", "min-height": "100vh", width: "100%" }}>
<GlobalSidebar />
{/* 右侧内容区:自动填充剩余空间 */}
<div
style={{
"margin-left": marginLeft(),
transition: "margin-left 0.28s cubic-bezier(0.4,0,0.2,1)",
flex: "1",
"min-width": "0",
"min-height": "100vh",
display: "flex",
"flex-direction": "column",
}}
>
{/* 顶栏 */}
<TopBar />
{/* 页面内容 */}
<div
style={{
flex: "1",
"min-height": "0",
display: "flex",
"flex-direction": "column",
}}
>
{props.children}
</div>
</div>
</div>
)
}
Loading