Skip to content

Commit efb9b35

Browse files
feat: add a file-and-folder select component and add a select interface for sharing (#197)
* feat: add a better choose interface * feat: add file show * chore: remove unused imports Co-authored-by: Copilot <[email protected]> Signed-off-by: ILoveScratch <[email protected]> * fix --------- Signed-off-by: ILoveScratch <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 389991c commit efb9b35

File tree

8 files changed

+356
-3
lines changed

8 files changed

+356
-3
lines changed
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
import { Box, HStack, Icon, Spinner, Text, VStack } from "@hope-ui/solid"
2+
import { BiSolidRightArrow, BiSolidFolderOpen } from "solid-icons/bi"
3+
import { TbFile, TbFolder } from "solid-icons/tb"
4+
import {
5+
Accessor,
6+
createContext,
7+
createSignal,
8+
useContext,
9+
Show,
10+
For,
11+
Setter,
12+
createEffect,
13+
on,
14+
} from "solid-js"
15+
import { useFetch, useT, useUtil } from "~/hooks"
16+
import { getMainColor, password } from "~/store"
17+
import { Obj } from "~/types"
18+
import {
19+
pathBase,
20+
handleResp,
21+
hoverColor,
22+
pathJoin,
23+
createMatcher,
24+
} from "~/utils"
25+
import { fsList } from "~/utils/api"
26+
27+
export type EnhancedFolderTreeHandler = {
28+
setPath: Setter<string>
29+
}
30+
31+
export interface EnhancedFolderTreeProps {
32+
onChange: (path: string) => void
33+
forceRoot?: boolean
34+
autoOpen?: boolean
35+
handle?: (handler: EnhancedFolderTreeHandler) => void
36+
showEmptyIcon?: boolean
37+
showHiddenFolder?: boolean
38+
}
39+
40+
interface EnhancedFolderTreeContext
41+
extends Omit<EnhancedFolderTreeProps, "handle"> {
42+
value: Accessor<string>
43+
}
44+
45+
const context = createContext<EnhancedFolderTreeContext>()
46+
47+
export const EnhancedFolderTree = (props: EnhancedFolderTreeProps) => {
48+
const [path, setPath] = createSignal("/")
49+
props.handle?.({ setPath })
50+
return (
51+
<Box class="folder-tree-box" w="$full" overflowX="auto">
52+
<context.Provider
53+
value={{
54+
value: path,
55+
onChange: (val) => {
56+
setPath(val)
57+
props.onChange(val)
58+
},
59+
autoOpen: props.autoOpen ?? false,
60+
forceRoot: props.forceRoot ?? false,
61+
showEmptyIcon: props.showEmptyIcon ?? false,
62+
showHiddenFolder: props.showHiddenFolder ?? true,
63+
}}
64+
>
65+
<EnhancedFolderTreeNode path="/" />
66+
</context.Provider>
67+
</Box>
68+
)
69+
}
70+
71+
const EnhancedFolderTreeNode = (props: { path: string }) => {
72+
const { isHidePath } = useUtil()
73+
const [items, setItems] = createSignal<Obj[]>()
74+
const {
75+
value,
76+
onChange,
77+
forceRoot,
78+
autoOpen,
79+
showEmptyIcon,
80+
showHiddenFolder,
81+
} = useContext(context)!
82+
83+
const emptyIconVisible = () =>
84+
Boolean(showEmptyIcon && items() !== undefined && !items()?.length)
85+
86+
const [loading, fetchItems] = useFetch(() =>
87+
fsList(props.path, password(), 1, 0, false),
88+
)
89+
90+
let isLoaded = false
91+
92+
const load = async () => {
93+
if (items()?.length) return
94+
const resp = await fetchItems()
95+
handleResp(
96+
resp,
97+
(data) => {
98+
isLoaded = true
99+
setItems(data.content || [])
100+
},
101+
() => {
102+
if (isOpen()) onToggle()
103+
},
104+
)
105+
}
106+
107+
const { isOpen, onToggle } = createDisclosure()
108+
const active = () => value() === props.path
109+
const isMatchedFolder = createMatcher(props.path)
110+
111+
const checkIfShouldOpen = async (pathname: string) => {
112+
if (!autoOpen) return
113+
if (isMatchedFolder(pathname)) {
114+
if (!isOpen()) onToggle()
115+
if (!isLoaded) load()
116+
}
117+
}
118+
119+
createEffect(on(value, checkIfShouldOpen))
120+
121+
const isHiddenFolder = () =>
122+
isHidePath(props.path) && !isMatchedFolder(value())
123+
124+
const folders = () => items()?.filter((item) => item.is_dir) || []
125+
const files = () => items()?.filter((item) => !item.is_dir) || []
126+
127+
return (
128+
<Show when={showHiddenFolder || !isHiddenFolder()}>
129+
<Box>
130+
<HStack spacing="$2">
131+
<Show
132+
when={!loading()}
133+
fallback={<Spinner size="sm" color={getMainColor()} />}
134+
>
135+
<Show
136+
when={!emptyIconVisible()}
137+
fallback={<Icon color={getMainColor()} as={BiSolidFolderOpen} />}
138+
>
139+
<Icon
140+
color={getMainColor()}
141+
as={BiSolidRightArrow}
142+
transform={isOpen() ? "rotate(90deg)" : "none"}
143+
transition="transform 0.2s"
144+
cursor="pointer"
145+
onClick={() => {
146+
onToggle()
147+
if (isOpen()) {
148+
load()
149+
}
150+
}}
151+
/>
152+
</Show>
153+
</Show>
154+
<Icon color={getMainColor()} as={TbFolder} cursor="pointer" />
155+
<Text
156+
css={{
157+
whiteSpace: "nowrap",
158+
}}
159+
fontSize="$md"
160+
cursor="pointer"
161+
px="$1"
162+
rounded="$md"
163+
bgColor={active() ? "$info8" : "transparent"}
164+
_hover={
165+
{
166+
bgColor: active() ? "$info8" : hoverColor(),
167+
} as any
168+
}
169+
onClick={() => {
170+
onChange(props.path)
171+
}}
172+
>
173+
{props.path === "/" ? "root" : pathBase(props.path)}
174+
</Text>
175+
</HStack>
176+
177+
<Show when={isOpen()}>
178+
<VStack mt="$1" pl="$4" alignItems="start" spacing="$1">
179+
<For each={folders()}>
180+
{(item) => (
181+
<EnhancedFolderTreeNode
182+
path={pathJoin(props.path, item.name)}
183+
/>
184+
)}
185+
</For>
186+
187+
<For each={files()}>
188+
{(item) => (
189+
<HStack spacing="$2" w="$full">
190+
<Box w="16px" />
191+
<Icon color={getMainColor()} as={TbFile} cursor="pointer" />
192+
<Text
193+
css={{
194+
whiteSpace: "nowrap",
195+
}}
196+
fontSize="$md"
197+
cursor="pointer"
198+
px="$1"
199+
rounded="$md"
200+
bgColor={
201+
value() === pathJoin(props.path, item.name)
202+
? "$info8"
203+
: "transparent"
204+
}
205+
_hover={
206+
{
207+
bgColor:
208+
value() === pathJoin(props.path, item.name)
209+
? "$info8"
210+
: hoverColor(),
211+
} as any
212+
}
213+
onClick={() => {
214+
onChange(pathJoin(props.path, item.name))
215+
}}
216+
>
217+
{item.name}
218+
</Text>
219+
</HStack>
220+
)}
221+
</For>
222+
</VStack>
223+
</Show>
224+
</Box>
225+
</Show>
226+
)
227+
}
228+
229+
function createDisclosure() {
230+
const [isOpen, setIsOpen] = createSignal(false)
231+
return {
232+
isOpen,
233+
onToggle: () => setIsOpen(!isOpen()),
234+
onOpen: () => setIsOpen(true),
235+
onClose: () => setIsOpen(false),
236+
}
237+
}

src/components/MultiPathInput.tsx

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import {
2+
VStack,
3+
HStack,
4+
Button,
5+
Textarea,
6+
IconButton,
7+
createDisclosure,
8+
Modal,
9+
ModalOverlay,
10+
ModalContent,
11+
ModalHeader,
12+
ModalBody,
13+
ModalFooter,
14+
ModalCloseButton,
15+
} from "@hope-ui/solid"
16+
import { createSignal } from "solid-js"
17+
import { TbPlus, TbFolder } from "solid-icons/tb"
18+
import { useT } from "~/hooks"
19+
import { EnhancedFolderTree } from "~/components/EnhancedFolderTree"
20+
21+
export interface MultiPathInputProps {
22+
value: string
23+
onChange: (value: string) => void
24+
valid?: boolean
25+
readOnly?: boolean
26+
id?: string
27+
placeholder?: string
28+
}
29+
30+
export const MultiPathInput = (props: MultiPathInputProps) => {
31+
const t = useT()
32+
const { isOpen, onOpen, onClose } = createDisclosure()
33+
const [selectedPath, setSelectedPath] = createSignal("/")
34+
35+
const addPath = () => {
36+
const currentPaths = props.value ? props.value.split("\n") : []
37+
const newPaths = [...currentPaths, selectedPath()].filter(Boolean)
38+
const uniquePaths = [...new Set(newPaths)]
39+
props.onChange(uniquePaths.join("\n"))
40+
onClose()
41+
}
42+
43+
return (
44+
<VStack w="$full" spacing="$2" alignItems="stretch">
45+
<HStack w="$full" spacing="$2">
46+
<Textarea
47+
id={props.id}
48+
flex="1"
49+
value={props.value}
50+
invalid={!props.valid}
51+
readOnly={props.readOnly}
52+
placeholder={props.placeholder || t("shares.files_placeholder")}
53+
onInput={(e) => props.onChange(e.currentTarget.value)}
54+
/>
55+
<IconButton
56+
colorScheme="accent"
57+
aria-label={t("global.choose_or_input_path")}
58+
icon={<TbFolder />}
59+
onClick={onOpen}
60+
size="lg"
61+
disabled={props.readOnly}
62+
/>
63+
</HStack>
64+
65+
<Modal size="xl" opened={isOpen()} onClose={onClose}>
66+
<ModalOverlay />
67+
<ModalContent>
68+
<ModalCloseButton />
69+
<ModalHeader>{t("global.choose_or_input_path")}</ModalHeader>
70+
<ModalBody>
71+
<EnhancedFolderTree
72+
forceRoot
73+
onChange={setSelectedPath}
74+
showHiddenFolder={true}
75+
/>
76+
</ModalBody>
77+
<ModalFooter display="flex" gap="$2">
78+
<Button onClick={onClose} colorScheme="neutral">
79+
{t("global.cancel")}
80+
</Button>
81+
<Button
82+
onClick={addPath}
83+
colorScheme="primary"
84+
leftIcon={<TbPlus />}
85+
>
86+
{t("shares.add_path")}
87+
</Button>
88+
</ModalFooter>
89+
</ModalContent>
90+
</Modal>
91+
</VStack>
92+
)
93+
}

src/components/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ export * from "./SwitchColorMode"
33
export * from "./SwitchLanguage"
44
export * from "./Hello"
55
export * from "./FolderTree"
6+
export * from "./EnhancedFolderTree"
7+
export * from "./MultiPathInput"
68
export * from "./Wether"
79
export * from "./LinkWithBase"
810
export * from "./ImageWithError"

src/lang/en/shares.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
"pwd": "Share code",
77
"files": "Share Paths",
88
"files_etc": " and {{more}} more",
9+
"files_placeholder": "Enter file or folder paths, one per line",
10+
"add_path": "Add Path",
911
"creator": "Creator",
1012
"status": "Status",
1113
"status_list": {

src/pages/home/toolbar/Share.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import {
3636
import { createStore } from "solid-js/store"
3737
import { getSetting, me, selectedObjs } from "~/store"
3838
import { TbRefresh } from "solid-icons/tb"
39-
import { SelectOptions } from "~/components"
39+
import { SelectOptions, MultiPathInput } from "~/components"
4040

4141
export const Share = () => {
4242
const t = useT()

src/pages/manage/shares/AddOrEdit.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const AddOrEdit = () => {
5656
</Show>
5757
<Item
5858
name="files"
59-
type={Type.Text}
59+
type={Type.MultiPath}
6060
value={files()}
6161
valid={filesValid()}
6262
required

0 commit comments

Comments
 (0)