feat: setting: add Load Default Settings button; home: display loaded folder info (#100)

* fix: resolve all prompt errors

* feat(home): display loaded folder info

* feat(setting): add `Load Default Settings` button
This commit is contained in:
j2rong4cn
2025-07-14 16:38:25 +08:00
committed by GitHub
parent bdc705b1e2
commit 037e103ab5
27 changed files with 186 additions and 149 deletions

View File

@@ -19,7 +19,7 @@ import { MustUser } from "./MustUser"
import "./index.css"
import { useI18n } from "@solid-primitives/i18n"
import { initialLang, langMap, loadedLangs } from "./i18n"
import { Resp } from "~/types"
import { PEmptyResp, PResp, Resp } from "~/types"
import { setArchiveExtensions } from "~/store/archive"
const Home = lazy(() => import("~/pages/home/Layout"))

View File

@@ -44,7 +44,7 @@ export const FullLoading = (props: {
export const MaybeLoading = (props: {
children?: JSXElement
loading: boolean
loading?: boolean
}) => {
return (
<Show when={!props.loading} fallback={<FullLoading />}>

View File

@@ -31,6 +31,7 @@ const [isTocDisabled, setTocDisabled] = createStorageSignal(
export { isTocVisible, setTocDisabled }
type PluginType = typeof remarkGfm | typeof rehypeRaw
function MarkdownToc(props: {
disabled?: boolean
markdownRef: HTMLDivElement
@@ -214,10 +215,10 @@ export function Markdown(props: {
})
return content
})
const [remarkPlugins, setRemarkPlugins] = createSignal<Function[]>([
const [remarkPlugins, setRemarkPlugins] = createSignal<PluginType[]>([
remarkGfm,
])
const [rehypePlugins, setRehypePlugins] = createSignal<Function[]>([
const [rehypePlugins, setRehypePlugins] = createSignal<PluginType[]>([
rehypeRaw,
])
createEffect(
@@ -228,8 +229,8 @@ export function Markdown(props: {
const { default: reMarkMath } = await import("remark-math")
const { default: rehypeKatex } = await import("rehype-katex")
insertKatexCSS()
setRemarkPlugins([...remarkPlugins(), reMarkMath])
setRehypePlugins([...rehypePlugins(), rehypeKatex])
setRemarkPlugins([...remarkPlugins(), reMarkMath as PluginType])
setRehypePlugins([...rehypePlugins(), rehypeKatex as PluginType])
}
insertMermaidJS()
setTimeout(() => {

View File

@@ -92,7 +92,7 @@ export const ModalInput = (props: ModalInputProps) => {
initialFocus="#modal-input"
>
<ModalOverlay />
<ModalContent onDrop={(e) => props.onDrop?.(e, setValue)}>
<ModalContent onDrop={(e: DragEvent) => props.onDrop?.(e, setValue)}>
{/* <ModalCloseButton /> */}
<ModalHeader>{t(props.title)}</ModalHeader>
<ModalBody>

View File

@@ -5,17 +5,14 @@ export const useLoading = <T>(
p: (...arg: any[]) => Promise<T>,
fetch?: boolean,
t?: boolean, // initial loading true
): [
Accessor<boolean>,
(...arg: any[]) => Promise<unknown extends T ? any : T>,
] => {
const [loading, setLoading] = createSignal<boolean>(t ?? false)
): [Accessor<typeof t>, typeof p] => {
const [loading, setLoading] = createSignal(t)
return [
loading,
async (...arg: any[]) => {
setLoading(true)
const data = await p(...arg)
if (!fetch || (data as unknown as EmptyResp).code !== 401) {
if (!fetch || (data as EmptyResp).code !== 401) {
// why?
// because if setLoading(false) here will rerender before navigate
// maybe cause some bugs
@@ -29,10 +26,7 @@ export const useLoading = <T>(
export const useFetch = <T>(
p: (...arg: any[]) => PResp<T>,
loading?: boolean,
): [
Accessor<boolean>,
(...arg: Parameters<typeof p>) => PResp<unknown extends T ? any : T>,
] => {
): [Accessor<typeof loading>, typeof p] => {
return useLoading(p, true, loading)
}
@@ -40,13 +34,13 @@ const useListLoading = <T, K>(
p: (key: K, ...arg: any[]) => Promise<T>,
fetch?: boolean,
initial?: K,
): [Accessor<K | undefined>, (key: K, ...arg: any[]) => Promise<any>] => {
const [loading, setLoading] = createSignal<K | undefined>(initial)
): [Accessor<typeof initial>, typeof p] => {
const [loading, setLoading] = createSignal(initial)
return [
loading,
async (key: K, ...arg: any[]) => {
setLoading(() => key)
const data: unknown = await p(key, ...arg)
const data = await p(key, ...arg)
if (!fetch || (data as EmptyResp).code !== 401) {
setLoading(undefined)
}
@@ -58,6 +52,6 @@ const useListLoading = <T, K>(
export const useListFetch = <T, K>(
p: (key: K, ...arg: any[]) => PResp<T>,
initial?: K,
): [Accessor<K | undefined>, (key: K, ...arg: any[]) => Promise<any>] => {
): [Accessor<typeof initial>, typeof p] => {
return useListLoading(p, true, initial)
}

View File

@@ -1,5 +1,6 @@
{
"obj": {
"selected": "Selected",
"name": "Name",
"size": "Size",
"modified": "Modified"

View File

@@ -124,5 +124,7 @@
"version": "Version",
"video_autoplay": "Video autoplay",
"video_types": "Video types",
"load_default_setting": "Load default settings",
"load_default_setting_success": "Loaded successfully, Not saved",
"webauthn_login_enabled": "Webauthn login enabled"
}

View File

@@ -11,7 +11,7 @@ const Index = () => {
useTitle(getSetting("site_title"))
const announcement = getSetting("announcement")
if (announcement) {
notify.render(() => <Markdown children={announcement} />)
notify.render(<Markdown children={announcement} />)
}
return (
<>

View File

@@ -4,11 +4,13 @@ import {
BreadcrumbLink,
BreadcrumbProps,
BreadcrumbSeparator,
HStack,
Text,
} from "@hope-ui/solid"
import { Link } from "@solidjs/router"
import { createMemo, For, Show } from "solid-js"
import { usePath, useRouter, useT } from "~/hooks"
import { getSetting, local } from "~/store"
import { getSetting, local, objStore, State } from "~/store"
import { encodePath, hoverColor, joinBase } from "~/utils"
export const Nav = () => {
@@ -17,6 +19,38 @@ export const Nav = () => {
const t = useT()
const { setPathAs } = usePath()
const folderInfo = createMemo(() => {
const { folder, file } = objStore.objs.reduce(
(acc, item) => {
if (item.is_dir) {
acc.folder++
} else {
acc.file++
}
return acc
},
{ folder: 0, file: 0 },
)
const parts: string[] = []
if (folder) parts.push(`${t("home.search.scopes.folder")}:${folder}`)
if (file) parts.push(`${t("home.search.scopes.file")}:${file}`)
return parts.join(" ")
})
const selectInfo = createMemo(() => {
const { selected } = objStore.objs.reduce(
(acc, item) => {
if (item.selected) acc.selected++
return acc
},
{ selected: 0 },
)
if (!selected) {
return ""
}
return ` ${t("home.obj.selected")}:${selected}`
})
const stickyProps = createMemo<BreadcrumbProps>(() => {
const mask: BreadcrumbProps = {
_after: {
@@ -48,45 +82,58 @@ export const Nav = () => {
})
return (
<Breadcrumb {...stickyProps} background="$background" class="nav" w="$full">
<For each={paths()}>
{(name, i) => {
const isLast = createMemo(() => i() === paths().length - 1)
const path = paths()
.slice(0, i() + 1)
.join("/")
const href = encodePath(path)
let text = () => name
if (text() === "") {
text = () => getSetting("home_icon") + t("manage.sidemenu.home")
}
return (
<BreadcrumbItem class="nav-item">
<BreadcrumbLink
class="nav-link"
css={{
wordBreak: "break-all",
}}
color="unset"
_hover={{ bgColor: hoverColor(), color: "unset" }}
_active={{ transform: "scale(.95)", transition: "0.1s" }}
cursor="pointer"
p="$1"
rounded="$lg"
currentPage={isLast()}
as={isLast() ? undefined : Link}
href={joinBase(href)}
onMouseEnter={() => setPathAs(path)}
>
{text}
</BreadcrumbLink>
<Show when={!isLast()}>
<BreadcrumbSeparator class="nav-separator" />
</Show>
</BreadcrumbItem>
)
}}
</For>
</Breadcrumb>
<HStack background="$background" class="nav" w="$full">
<Breadcrumb {...stickyProps} w="$full">
<For each={paths()}>
{(name, i) => {
const isLast = createMemo(() => i() === paths().length - 1)
const path = paths()
.slice(0, i() + 1)
.join("/")
const href = encodePath(path)
let text = () => name
if (text() === "") {
text = () => getSetting("home_icon") + t("manage.sidemenu.home")
}
return (
<BreadcrumbItem class="nav-item">
<BreadcrumbLink
class="nav-link"
css={{
wordBreak: "break-all",
}}
color="unset"
_hover={{ bgColor: hoverColor(), color: "unset" }}
_active={{ transform: "scale(.95)", transition: "0.1s" }}
cursor="pointer"
p="$1"
rounded="$lg"
currentPage={isLast()}
as={isLast() ? undefined : Link}
href={joinBase(href)}
onMouseEnter={() => setPathAs(path)}
>
{text()}
</BreadcrumbLink>
<Show when={!isLast()}>
<BreadcrumbSeparator class="nav-separator" />
</Show>
</BreadcrumbItem>
)
}}
</For>
</Breadcrumb>
<Show when={objStore.state == State.Folder}>
<Text
css={{
whiteSpace: "nowrap",
}}
p="$1"
>
{folderInfo()}
{selectInfo()}
</Text>
</Show>
</HStack>
)
}

View File

@@ -1,4 +1,5 @@
import { Box } from "@hope-ui/solid"
// @ts-ignore
import * as AsciinemaPlayer from "asciinema-player"
import "asciinema-player/dist/bundle/asciinema-player.css"
import { onMount } from "solid-js"

View File

@@ -30,7 +30,7 @@ const Preview = () => {
let libheif: any
let decoder: any
let canvas: HTMLCanvasElement | null = null
let canvas: HTMLCanvasElement | undefined
onMount(() => {
window.addEventListener("keydown", onKeydown)
@@ -74,21 +74,20 @@ const Preview = () => {
}
// 加载脚本
const loadScript = (src: string, id: string) => {
return new Promise<void>((resolve, reject) => {
const loadScript = (src: string, id: string) =>
new Promise<void>((resolve, reject) => {
const script = document.createElement("script")
script.src = src
script.id = id
script.onload = () => resolve()
script.onerror = () => reject(new Error(`脚本加载失败: ${src}`))
script.onerror = () => reject(`脚本加载失败: ${src}`)
document.head.appendChild(script)
})
}
// 获取WASM文件
const fetchWasm = async (url: string) => {
const response = await fetch(url)
if (!response.ok) throw new Error(`WASM加载失败: ${url}`)
if (!response.ok) throw `WASM加载失败: ${url}`
return await response.arrayBuffer()
}
@@ -100,13 +99,13 @@ const Preview = () => {
// 获取HEIC文件
const response = await fetch(url)
if (!response.ok) throw new Error("文件获取失败")
if (!response.ok) throw "文件获取失败"
const buffer = await response.arrayBuffer()
// 解码HEIC文件
const images = decoder.decode(buffer)
if (!images || images.length === 0) {
throw new Error("没有可解码的图像")
throw "没有可解码的图像"
}
// 显示第一张图像

View File

@@ -166,7 +166,7 @@ export const VideoBox = (props: {
whiteSpace: "nowrap",
}}
defaultChecked={autoNext === "true"}
onChange={(e) => {
onChange={(e: { currentTarget: HTMLInputElement }) => {
props.onAutoNextChange(e.currentTarget.checked)
localStorage.setItem(
"video_auto_next",

View File

@@ -58,13 +58,7 @@ function LocalSettingEdit(props: LocalSetting) {
</SelectTrigger>
<SelectContent>
<SelectListbox>
<For
each={
typeof props.options === "function"
? props.options()
: props.options
}
>
<For each={props.options}>
{(item) => (
<SelectOption value={item}>
<SelectOptionText>
@@ -81,7 +75,7 @@ function LocalSettingEdit(props: LocalSetting) {
<Match when={props.type === "boolean"}>
<HopeSwitch
defaultChecked={local[props.key] === "true"}
onChange={(e) => {
onChange={(e: { currentTarget: HTMLInputElement }) => {
setLocal(props.key, e.currentTarget.checked.toString())
}}
/>

View File

@@ -36,11 +36,7 @@ const CornerBottom = (props: Props) => {
</linearGradient>
</defs>
<g opacity="1">
<use
xlink:href="#path-2"
fill="url(#linearGradient-3)"
fill-opacity="1"
/>
<use href="#path-2" fill="url(#linearGradient-3)" fill-opacity="1" />
</g>
</svg>
)

View File

@@ -36,11 +36,7 @@ const CornerTop = (props: Props) => {
</linearGradient>
</defs>
<g opacity="1">
<use
xlink:href="#path-1"
fill="url(#linearGradient-2)"
fill-opacity="1"
/>
<use href="#path-1" fill="url(#linearGradient-2)" fill-opacity="1" />
</g>
</svg>
)

View File

@@ -12,7 +12,7 @@ import { useT } from "~/hooks"
export interface DeletePopoverProps {
name: string
loading: boolean
loading?: boolean
onClick: () => void
}
export const DeletePopover = (props: DeletePopoverProps) => {

View File

@@ -105,7 +105,7 @@ const AddOrEdit = () => {
const initEdit = async () => {
const resp = await loadMeta()
handleResp(resp, setMeta)
handleResp<Meta>(resp, setMeta)
}
if (id) {
initEdit()
@@ -154,14 +154,12 @@ const AddOrEdit = () => {
// @ts-ignore
<Item
name={item.name}
type={item.type as "string" | "bool" | "text"}
value={meta[item.name as keyof Meta] as string | boolean}
onChange={(val: any): void =>
setMeta(item.name as keyof Meta, val)
}
sub={meta[item.sub] as boolean}
onSub={(val: boolean): void => setMeta(item.sub, val)}
help={item.help}
type={item.type}
value={meta[item.name]}
onChange={(val: any): void => setMeta(item.name, val)}
sub={meta[item.sub]}
onSub={(val): void => setMeta(item.sub, val)}
help={(item as { help: boolean }).help}
/>
)
}}

View File

@@ -19,7 +19,7 @@ import {
useT,
} from "~/hooks"
import { handleResp, notify, r } from "~/utils"
import { Meta, PageResp } from "~/types"
import { Meta, PEmptyResp, PPageResp } from "~/types"
import { DeletePopover } from "../common/DeletePopover"
import { Wether } from "~/components"
@@ -27,16 +27,18 @@ const Metas = () => {
const t = useT()
useManageTitle("manage.sidemenu.metas")
const { to } = useRouter()
const [getMetasLoading, getMetas] = useFetch(() => r.get("/admin/meta/list"))
const [getMetasLoading, getMetas] = useFetch(
(): PPageResp<Meta> => r.get("/admin/meta/list"),
)
const [metas, setMetas] = createSignal<Meta[]>([])
const refresh = async () => {
const resp: PageResp<Meta> = await getMetas()
const resp = await getMetas()
handleResp(resp, (data) => setMetas(data.content))
}
refresh()
const [deleting, deleteMeta] = useListFetch((id: number) =>
r.post(`/admin/meta/delete?id=${id}`),
const [deleting, deleteMeta] = useListFetch(
(id: number): PEmptyResp => r.post(`/admin/meta/delete?id=${id}`),
)
return (
<VStack spacing="$2" alignItems="start" w="$full">

View File

@@ -21,18 +21,22 @@ const CommonSettings = (props: CommonSettingsProps) => {
const [settings, setSettings] = createStore<SettingItem[]>([])
const refresh = async () => {
const resp = await getSettings()
handleResp(resp, setSettings)
handleResp<SettingItem[]>(resp, setSettings)
}
refresh()
const [saveLoading, saveSettings] = useFetch(
(): PEmptyResp => r.post("/admin/setting/save", getTarget(settings)),
)
const [defaultLoading, defaultSettings] = useFetch(
(): PResp<SettingItem[]> =>
r.post(`/admin/setting/default?group=${props.group}`),
)
const [loading, setLoading] = createSignal(false)
return (
<VStack w="$full" alignItems="start" spacing="$2">
<ResponsiveGrid>
<Index each={settings}>
{(item, _) => (
{(item) => (
<Item
{...item()}
onChange={(val) => {
@@ -57,7 +61,7 @@ const CommonSettings = (props: CommonSettingsProps) => {
<Button
colorScheme="accent"
onClick={refresh}
loading={settingsLoading() || loading()}
loading={settingsLoading() || loading() || defaultLoading()}
>
{t("global.refresh")}
</Button>
@@ -70,6 +74,19 @@ const CommonSettings = (props: CommonSettingsProps) => {
>
{t("global.save")}
</Button>
<Button
colorScheme="warning"
loading={settingsLoading() || loading() || defaultLoading()}
onClick={async () => {
const resp = await defaultSettings()
handleResp(resp, (data) => {
notify.info(t("settings.load_default_setting_success"))
setSettings(data)
})
}}
>
{t("settings.load_default_setting")}
</Button>
</HStack>
</VStack>
)

View File

@@ -24,7 +24,7 @@ const S3Settings = () => {
const [settings, setSettings] = createStore<SettingItem[]>([])
const refresh = async () => {
const resp = await getSettings()
handleResp(resp, setSettings)
handleResp<SettingItem[]>(resp, setSettings)
}
refresh()
const [saveLoading, saveSettings] = useFetch(

View File

@@ -26,7 +26,6 @@ import { TiDelete } from "solid-icons/ti"
export type ItemProps = SettingItem & {
onChange?: (value: string) => void
onDelete?: () => void
// value: () => string;
hideLabel?: boolean
w?: string
}
@@ -58,7 +57,6 @@ const Item = (props: ItemProps) => {
<Input
type={props.type === Type.Number ? "number" : ""}
id={props.key}
// value={props.value()}
value={props.value}
onInput={(e) => props.onChange?.(e.currentTarget.value)}
readOnly={props.flag === Flag.READONLY}
@@ -67,11 +65,9 @@ const Item = (props: ItemProps) => {
<Match when={props.type === Type.Bool}>
<HopeSwitch
id={props.key}
defaultChecked={props.value === "true"}
// checked={props.value() === "true"}
onChange={(e: any) =>
// props.onChange?.(props.value() === "true" ? "false" : "true")
props.onChange?.(e.currentTarget.checked ? "true" : "false")
checked={props.value === "true"}
onChange={(e: { currentTarget: HTMLInputElement }) =>
props.onChange?.(e.currentTarget?.checked ? "true" : "false")
}
readOnly={props.flag === Flag.READONLY}
/>
@@ -80,7 +76,6 @@ const Item = (props: ItemProps) => {
<Textarea
id={props.key}
value={props.value}
// value={props.value()}
onChange={(e) => props.onChange?.(e.currentTarget.value)}
readOnly={props.flag === Flag.READONLY}
/>
@@ -88,8 +83,7 @@ const Item = (props: ItemProps) => {
<Match when={props.type === Type.Select}>
<Select
id={props.key}
defaultValue={props.value}
// value={props.value()}
value={props.value}
onChange={(e) => props.onChange?.(e)}
readOnly={props.flag === Flag.READONLY}
>

View File

@@ -130,8 +130,10 @@ const Storages = () => {
</Show>
<HopeSwitch
checked={layout() === "table"}
onChange={(e) => {
setLayout(e.currentTarget.checked ? "table" : "grid")
onChange={(e: Event) => {
setLayout(
(e.currentTarget as HTMLInputElement).checked ? "table" : "grid",
)
}}
>
{t("storages.other.table_layout")}

View File

@@ -14,7 +14,7 @@ import { handleResp, notify, r } from "~/utils"
import { PEmptyResp, PResp, User, UserMethods, UserPermissions } from "~/types"
import { createStore } from "solid-js/store"
import { For, Show } from "solid-js"
import { me, setMe } from "~/store"
import { Me, me, setMe } from "~/store"
import { PublicKeys } from "./PublicKeys"
const Permission = (props: {
@@ -63,7 +63,7 @@ const AddOrEdit = () => {
const initEdit = async () => {
const resp = await loadUser()
handleResp(resp, setUser)
handleResp<User>(resp, setUser)
}
if (id) {
initEdit()
@@ -153,7 +153,7 @@ const AddOrEdit = () => {
handleResp(resp, async () => {
notify.success(t("global.save_success"))
if (user.username === me().username)
handleResp(await r.get("/me"), setMe)
handleResp(await (r.get("/me") as PResp<Me>), setMe)
back()
})
}}

View File

@@ -58,6 +58,7 @@ export const initialLocalSettings = [
default: "direct",
type: "select",
options: ["direct", "dblclick", "disable_while_checked"],
hidden: false,
},
]
export type LocalSetting = (typeof initialLocalSettings)[number]

View File

@@ -16,39 +16,25 @@ export enum State {
File, // File state
NeedPassword,
}
const [objStore, setObjStore] = createStore<{
obj: Obj
raw_url: string
related: Obj[]
objs: StoreObj[]
total: number
write?: boolean
readme: string
header: string
provider: string
// pageIndex: number;
// pageSize: number;
state: State
err: string
}>({
const initialObjStore = {
obj: {} as Obj,
raw_url: "",
related: [],
related: [] as Obj[],
objs: [],
objs: [] as StoreObj[],
total: 0,
readme: "",
header: "",
provider: "",
// pageIndex: 1,
// pageSize: 50,
state: State.Initial,
err: "",
})
}
const [objStore, setObjStore] = createStore<
typeof initialObjStore & {
write?: boolean
}
>(initialObjStore)
const setObjs = (objs: Obj[]) => {
lastChecked.start = -1
@@ -96,7 +82,6 @@ export type OrderBy = "name" | "size" | "modified"
export const sortObjs = (orderBy: OrderBy, reverse?: boolean) => {
log("sort:", orderBy, reverse)
naturalSort.insensitive = true
setObjStore(
"objs",
produce((objs) =>

View File

@@ -26,7 +26,13 @@ const notify = {
padding: "$3",
}}
>
<div style={{ flexGrow: 1, display: "flex", alignItems: "center" }}>
<div
style={{
"flex-grow": 1,
display: "flex",
"align-items": "center",
}}
>
<div style={{ margin: "auto" }}>{element}</div>
</div>
<div style={{ display: "inline-block", padding: "5px" }}>

View File

@@ -15,6 +15,7 @@
"~/*": ["./src/*"]
// "@solidjs/router": ["./solid-router/src/index.tsx"]
},
"skipLibCheck": true,
"resolveJsonModule": true
}
}