From 26a0408987295a5ae411251f037cab819f26509b Mon Sep 17 00:00:00 2001 From: liuycy Date: Sun, 18 Feb 2024 15:22:47 +0800 Subject: [PATCH] feat: add sidebar of folder tree (#146 close alist-org/alist#5972) * feat: add sidebar for the tree of folders * feat: add sidebar for the tree of folders * fix: css transform overlay may block pointer event * feat: show empty icon in sidebar * fix: should close folder in folder tree while failed * fix: sidebar shadow & more smooth animation while reset sidebar --- src/components/FolderTree.tsx | 92 ++++++++++++++++++++-------- src/components/Markdown.tsx | 10 ++-- src/lang/en/home.json | 5 ++ src/pages/home/Body.tsx | 2 + src/pages/home/Obj.tsx | 14 ++++- src/pages/home/Sidebar.tsx | 109 ++++++++++++++++++++++++++++++++++ src/store/local_settings.ts | 6 ++ src/utils/path.ts | 29 +++++++++ 8 files changed, 238 insertions(+), 29 deletions(-) create mode 100644 src/pages/home/Sidebar.tsx diff --git a/src/components/FolderTree.tsx b/src/components/FolderTree.tsx index 139a0d883..2d6c734ae 100644 --- a/src/components/FolderTree.tsx +++ b/src/components/FolderTree.tsx @@ -16,7 +16,7 @@ import { Text, VStack, } from "@hope-ui/solid" -import { BiSolidRightArrow } from "solid-icons/bi" +import { BiSolidRightArrow, BiSolidFolderOpen } from "solid-icons/bi" import { Accessor, createContext, @@ -24,23 +24,39 @@ import { useContext, Show, For, + Setter, + createEffect, + on, } from "solid-js" import { useFetch, useT } from "~/hooks" import { getMainColor, password } from "~/store" import { Obj } from "~/types" -import { pathBase, handleResp, hoverColor, pathJoin, fsDirs } from "~/utils" +import { + pathBase, + handleResp, + hoverColor, + pathJoin, + fsDirs, + createMatcher, +} from "~/utils" +export type FolderTreeHandler = { + setPath: Setter +} export interface FolderTreeProps { onChange: (path: string) => void forceRoot?: boolean + autoOpen?: boolean + handle?: (handler: FolderTreeHandler) => void + showEmptyIcon?: boolean } -const context = createContext<{ +interface FolderTreeContext extends Omit { value: Accessor - onChange: (val: string) => void - forceRoot: boolean -}>() +} +const context = createContext() export const FolderTree = (props: FolderTreeProps) => { const [path, setPath] = createSignal("/") + props.handle?.({ setPath }) return ( { setPath(val) props.onChange(val) }, + autoOpen: props.autoOpen ?? false, forceRoot: props.forceRoot ?? false, + showEmptyIcon: props.showEmptyIcon ?? false, }} > @@ -60,18 +78,39 @@ export const FolderTree = (props: FolderTreeProps) => { } const FolderTreeNode = (props: { path: string }) => { - const [children, setChildren] = createSignal([]) - const { value, onChange, forceRoot } = useContext(context)! + const [children, setChildren] = createSignal() + const { value, onChange, forceRoot, autoOpen, showEmptyIcon } = + useContext(context)! + const emptyIconVisible = () => + Boolean(showEmptyIcon && children() !== undefined && !children()?.length) const [loading, fetchDirs] = useFetch(() => fsDirs(props.path, password(), forceRoot), ) + let isLoaded = false const load = async () => { - if (children().length > 0) return - const resp = await fetchDirs() - handleResp(resp, setChildren) + if (children()?.length) return + const resp = await fetchDirs() // this api may return null + handleResp( + resp, + (data) => { + isLoaded = true + setChildren(data) + }, + () => { + if (isOpen()) onToggle() // close folder while failed + }, + ) } const { isOpen, onToggle } = createDisclosure() const active = () => value() === props.path + const checkIfShouldOpen = async (pathname: string) => { + if (!autoOpen) return + if (createMatcher(props.path)(pathname)) { + if (!isOpen()) onToggle() + if (!isLoaded) load() + } + } + createEffect(on(value, checkIfShouldOpen)) return ( @@ -79,19 +118,24 @@ const FolderTreeNode = (props: { path: string }) => { when={!loading()} fallback={} > - { - onToggle() - if (isOpen()) { - load() - } - }} - /> + } + > + { + onToggle() + if (isOpen()) { + load() + } + }} + /> + (initialOffsetX) + return ( setOffsetX(0)} + onMouseLeave={() => setOffsetX(initialOffsetX)} zIndex="$overlay" pos="fixed" right="$6" @@ -123,10 +128,7 @@ function MarkdownToc(props: { disabled?: boolean }) { shadow="$outline" rounded="$lg" bgColor="white" - transition="all .3s ease-out" - transform="translateX(calc(100% - 20px))" _dark={{ bgColor: "$neutral3" }} - _hover={{ transform: "none" }} > diff --git a/src/lang/en/home.json b/src/lang/en/home.json index 38353267d..343c0de9b 100644 --- a/src/lang/en/home.json +++ b/src/lang/en/home.json @@ -117,6 +117,11 @@ "static": "Normal", "sticky": "Stick to top of page", "only_navbar_sticky": "Only nav bar sticky" + }, + "show_sidebar": "Show sidebar", + "show_sidebar_options": { + "none": "None", + "visible": "Visible" } }, "package_download": { diff --git a/src/pages/home/Body.tsx b/src/pages/home/Body.tsx index fc66afce2..bb1101d4b 100644 --- a/src/pages/home/Body.tsx +++ b/src/pages/home/Body.tsx @@ -3,6 +3,7 @@ import { Nav } from "./Nav" import { Obj } from "./Obj" import { Readme } from "./Readme" import { Container } from "./Container" +import { Sidebar } from "./Sidebar" export const Body = () => { return ( @@ -23,6 +24,7 @@ export const Body = () => { files={["readme.md", "footer.md", "bottom.md"]} fromMeta="readme" /> + ) diff --git a/src/pages/home/Obj.tsx b/src/pages/home/Obj.tsx index c83053109..b3f23d20e 100644 --- a/src/pages/home/Obj.tsx +++ b/src/pages/home/Obj.tsx @@ -1,5 +1,13 @@ import { useColorModeValue, VStack } from "@hope-ui/solid" -import { Suspense, Switch, Match, lazy, createEffect, on } from "solid-js" +import { + Suspense, + Switch, + Match, + lazy, + createEffect, + on, + createSignal, +} from "solid-js" import { FullLoading, Error } from "~/components" import { resetGlobalPage, useObjTitle, usePath, useRouter } from "~/hooks" import { objStore, recordScroll, /*layout,*/ State } from "~/store" @@ -10,6 +18,9 @@ const Password = lazy(() => import("./Password")) // const ListSkeleton = lazy(() => import("./Folder/ListSkeleton")); // const GridSkeleton = lazy(() => import("./Folder/GridSkeleton")); +const [objBoxRef, setObjBoxRef] = createSignal() +export { objBoxRef } + let first = true export const Obj = () => { const cardBg = useColorModeValue("white", "$neutral3") @@ -30,6 +41,7 @@ export const Obj = () => { ) return ( setObjBoxRef(el)} class="obj-box" w="$full" rounded="$xl" diff --git a/src/pages/home/Sidebar.tsx b/src/pages/home/Sidebar.tsx new file mode 100644 index 000000000..60fa1d81b --- /dev/null +++ b/src/pages/home/Sidebar.tsx @@ -0,0 +1,109 @@ +import { Box } from "@hope-ui/solid" +import { Motion } from "@motionone/solid" +import { useLocation } from "@solidjs/router" +import { + Show, + createEffect, + createMemo, + createSignal, + on, + onCleanup, + onMount, +} from "solid-js" +import { FolderTree, FolderTreeHandler } from "~/components" +import { useRouter } from "~/hooks" +import { local, objStore } from "~/store" +import { objBoxRef } from "./Obj" + +function SidebarPannel() { + const { to } = useRouter() + const location = useLocation() + + const [folderTreeHandler, setFolderTreeHandler] = + createSignal() + const [sideBarRef, setSideBarRef] = createSignal() + const [offsetX, setOffsetX] = createSignal(-999) + + const showFullSidebar = () => setOffsetX(0) + const resetSidebar = () => { + const $objBox = objBoxRef() + const $sideBar = sideBarRef() + if (!$objBox || !$sideBar) return + const gap = $objBox.offsetLeft > 50 ? 16 : 0 + if ($sideBar.clientWidth < $objBox.offsetLeft - gap) { + setOffsetX(0) + } else { + setOffsetX(`calc(-100% + ${$objBox.offsetLeft}px - ${gap}px)`) + } + } + + let rafId: number + + onMount(() => { + const handler = folderTreeHandler() + handler?.setPath(location.pathname) + rafId = requestAnimationFrame(resetSidebar) + window.addEventListener("resize", resetSidebar) + onCleanup(() => window.removeEventListener("resize", resetSidebar)) + }) + + createEffect( + on( + () => objStore.state, + () => { + cancelAnimationFrame(rafId) + rafId = requestAnimationFrame(resetSidebar) + }, + ), + ) + + createEffect( + on( + () => location.pathname, + () => { + const handler = folderTreeHandler() + handler?.setPath(location.pathname) + }, + ), + ) + + return ( + setSideBarRef(el)} + > + to(path)} + handle={(handler) => setFolderTreeHandler(handler)} + /> + + ) +} + +export function Sidebar() { + const visible = createMemo(() => local["show_sidebar"] !== "none") + + return ( + + + + ) +} diff --git a/src/store/local_settings.ts b/src/store/local_settings.ts index c6c04bb23..dd12d6761 100644 --- a/src/store/local_settings.ts +++ b/src/store/local_settings.ts @@ -29,6 +29,12 @@ export const initialLocalSettings = [ type: "select", options: ["top", "bottom", "none"], }, + { + key: "show_sidebar", + default: "none", + type: "select", + options: ["none", "visible"], + }, { key: "position_of_header_navbar", default: "static", diff --git a/src/utils/path.ts b/src/utils/path.ts index 3dbbd8062..dfd7e679b 100644 --- a/src/utils/path.ts +++ b/src/utils/path.ts @@ -60,3 +60,32 @@ export const ext = (path: string): string => { export const baseName = (fullName: string) => { return fullName.split(".").slice(0, -1).join(".") } + +export function createMatcher(path: string) { + const segments = path.split("/").filter(Boolean) + const len = segments.length + + return (location: string) => { + const locSegments = location.split("/").filter(Boolean) + const lenDiff = locSegments.length - len + if (lenDiff < 0) return null + + let matchPath = len ? "" : "/" + + for (let i = 0; i < len; i++) { + const segment = segments[i] + const locSegment = locSegments[i] + + if ( + segment.localeCompare(locSegment, undefined, { + sensitivity: "base", + }) !== 0 + ) { + return null + } + matchPath += `/${locSegment}` + } + + return matchPath + } +}