Skip to content

Commit

Permalink
feat: add sidebar of folder tree (#146 close AlistGo/alist#5972)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
liuycy authored Feb 18, 2024
1 parent da25476 commit 26a0408
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 29 deletions.
92 changes: 68 additions & 24 deletions src/components/FolderTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,47 @@ import {
Text,
VStack,
} from "@hope-ui/solid"
import { BiSolidRightArrow } from "solid-icons/bi"
import { BiSolidRightArrow, BiSolidFolderOpen } from "solid-icons/bi"
import {
Accessor,
createContext,
createSignal,
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<string>
}
export interface FolderTreeProps {
onChange: (path: string) => void
forceRoot?: boolean
autoOpen?: boolean
handle?: (handler: FolderTreeHandler) => void
showEmptyIcon?: boolean
}
const context = createContext<{
interface FolderTreeContext extends Omit<FolderTreeProps, "handle"> {
value: Accessor<string>
onChange: (val: string) => void
forceRoot: boolean
}>()
}
const context = createContext<FolderTreeContext>()
export const FolderTree = (props: FolderTreeProps) => {
const [path, setPath] = createSignal("/")
props.handle?.({ setPath })
return (
<Box class="folder-tree-box" w="$full" overflowX="auto">
<context.Provider
Expand All @@ -50,7 +66,9 @@ export const FolderTree = (props: FolderTreeProps) => {
setPath(val)
props.onChange(val)
},
autoOpen: props.autoOpen ?? false,
forceRoot: props.forceRoot ?? false,
showEmptyIcon: props.showEmptyIcon ?? false,
}}
>
<FolderTreeNode path="/" />
Expand All @@ -60,38 +78,64 @@ export const FolderTree = (props: FolderTreeProps) => {
}

const FolderTreeNode = (props: { path: string }) => {
const [children, setChildren] = createSignal<Obj[]>([])
const { value, onChange, forceRoot } = useContext(context)!
const [children, setChildren] = createSignal<Obj[]>()
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 (
<Box>
<HStack spacing="$2">
<Show
when={!loading()}
fallback={<Spinner size="sm" color={getMainColor()} />}
>
<Icon
color={getMainColor()}
as={BiSolidRightArrow}
transform={isOpen() ? "rotate(90deg)" : "none"}
transition="transform 0.2s"
cursor="pointer"
onClick={() => {
onToggle()
if (isOpen()) {
load()
}
}}
/>
<Show
when={!emptyIconVisible()}
fallback={<Icon color={getMainColor()} as={BiSolidFolderOpen} />}
>
<Icon
color={getMainColor()}
as={BiSolidRightArrow}
transform={isOpen() ? "rotate(90deg)" : "none"}
transition="transform 0.2s"
cursor="pointer"
onClick={() => {
onToggle()
if (isOpen()) {
load()
}
}}
/>
</Show>
</Show>
<Text
css={{
Expand Down
10 changes: 6 additions & 4 deletions src/components/Markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,17 @@ function MarkdownToc(props: { disabled?: boolean }) {
window.scrollBy({ behavior: "smooth", top: offsetY - navBottom })
}

const initialOffsetX = "calc(100% - 20px)"
const [offsetX, setOffsetX] = createSignal<number | string>(initialOffsetX)

return (
<Show when={!isTocDisabled() && isTocVisible()}>
<Box
as={Motion.div}
initial={{ x: 999 }}
animate={{ x: 0 }}
animate={{ x: offsetX() }}
onMouseEnter={() => setOffsetX(0)}
onMouseLeave={() => setOffsetX(initialOffsetX)}
zIndex="$overlay"
pos="fixed"
right="$6"
Expand All @@ -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" }}
>
<List maxH="60vh" overflowY="auto">
<For each={tocList()}>
Expand Down
5 changes: 5 additions & 0 deletions src/lang/en/home.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
2 changes: 2 additions & 0 deletions src/pages/home/Body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -23,6 +24,7 @@ export const Body = () => {
files={["readme.md", "footer.md", "bottom.md"]}
fromMeta="readme"
/>
<Sidebar />
</VStack>
</Container>
)
Expand Down
14 changes: 13 additions & 1 deletion src/pages/home/Obj.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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<HTMLDivElement>()
export { objBoxRef }

let first = true
export const Obj = () => {
const cardBg = useColorModeValue("white", "$neutral3")
Expand All @@ -30,6 +41,7 @@ export const Obj = () => {
)
return (
<VStack
ref={(el: HTMLDivElement) => setObjBoxRef(el)}
class="obj-box"
w="$full"
rounded="$xl"
Expand Down
109 changes: 109 additions & 0 deletions src/pages/home/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -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<FolderTreeHandler>()
const [sideBarRef, setSideBarRef] = createSignal<HTMLDivElement>()
const [offsetX, setOffsetX] = createSignal<number | string>(-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 (
<Box
as={Motion.div}
initial={{ x: -999 }}
animate={{ x: offsetX() }}
zIndex="$overlay"
pos="fixed"
left={3} // width of outline shadow
top={3}
h="calc(100vh - 6px)"
minW={180}
p="$2"
overflow="auto"
shadow="$lg"
rounded="$lg"
bgColor="white"
_dark={{ bgColor: "$neutral3" }}
onMouseEnter={showFullSidebar}
onMouseLeave={resetSidebar}
ref={(el: HTMLDivElement) => setSideBarRef(el)}
>
<FolderTree
autoOpen
showEmptyIcon
onChange={(path) => to(path)}
handle={(handler) => setFolderTreeHandler(handler)}
/>
</Box>
)
}

export function Sidebar() {
const visible = createMemo(() => local["show_sidebar"] !== "none")

return (
<Show when={visible()}>
<SidebarPannel />
</Show>
)
}
6 changes: 6 additions & 0 deletions src/store/local_settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
29 changes: 29 additions & 0 deletions src/utils/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

0 comments on commit 26a0408

Please sign in to comment.