feat(preview): add support preview in the archive (#329)

This commit is contained in:
bitxeno
2025-12-15 19:40:37 +08:00
committed by GitHub
parent 397b63f184
commit 9e2c5c0243
5 changed files with 252 additions and 61 deletions

View File

@@ -1,5 +1,5 @@
import { objStore, selectedObjs, State, me } from "~/store"
import { Obj } from "~/types"
import { Obj, ArchiveObj } from "~/types"
import {
base_path,
api,
@@ -36,6 +36,12 @@ export const getLinkByDirAndObj = (
if (!api.startsWith(location.origin + base_path))
host = location.origin + base_path
}
const { inner_path, archive } = obj as ArchiveObj
if (archive) {
prefix = "/ae"
path = `${dir}/${archive.name}`
path = encodePath(path, encodeAll)
}
let ans = `${host}${prefix}${path}`
if (type !== "preview" && !isShare && obj.sign) {
ans += `?sign=${obj.sign}`
@@ -46,6 +52,10 @@ export const getLinkByDirAndObj = (
ans += `?pwd=${pwd}`
}
}
if (archive) {
let inner = `${inner_path}/${obj.name}`
ans += `${ans.includes("?") ? "&" : "?"}inner=${encodePath(inner, encodeAll)}`
}
return ans
}

View File

@@ -20,16 +20,35 @@ import {
Match,
Show,
Switch,
Suspense,
onCleanup,
} from "solid-js"
import { getMainColor, local, me, OrderBy, password } from "~/store"
import { Obj, ObjTree, UserMethods, UserPermissions } from "~/types"
import { useFetch, useRouter, useT, useUtil } from "~/hooks"
import { Dynamic } from "solid-js/web"
import {
getMainColor,
local,
me,
OrderBy,
password,
objStore,
ObjStore,
} from "~/store"
import {
Obj,
ObjTree,
UserMethods,
UserPermissions,
ObjType,
ArchiveObj,
} from "~/types"
import { useFetch, useRouter, useT, useUtil, useLink } from "~/hooks"
import { ListTitle } from "~/pages/home/folder/List"
import { cols } from "~/pages/home/folder/ListItem"
import { Error, MaybeLoading } from "~/components"
import { Error, MaybeLoading, FullLoading, SelectWrapper } from "~/components"
import { OpenWith } from "../file/open-with"
import { getPreviews } from "."
import {
bus,
encodePath,
formatDate,
fsArchiveList,
fsArchiveMeta,
@@ -58,6 +77,7 @@ type ListItemProps = {
innerPath: string
url?: string
pass: string
onFileClick?: () => void
}
const ListItem = (props: ListItemProps) => {
@@ -88,8 +108,8 @@ const ListItem = (props: ListItemProps) => {
on:click={(_: MouseEvent) => {
if (props.obj.is_dir) {
props.jumpCallback()
} else if (props.url) {
download(props.url)
} else if (!props.obj.is_dir && props.onFileClick) {
props.onFileClick()
}
}}
onContextMenu={(e: MouseEvent) => {
@@ -219,6 +239,7 @@ type List = {
const Preview = () => {
const t = useT()
const { pathname } = useRouter()
const { rawLink } = useLink()
const [metaLoading, fetchMeta] = useFetch(fsArchiveMeta)
const [listLoading, fetchList] = useFetch(fsArchiveList)
const loading = createMemo(() => {
@@ -238,6 +259,8 @@ const Preview = () => {
const [extractFolder, setExtractFolder] = createSignal<"" | "front" | "back">(
"",
)
const [selectedFile, setSelectedFile] = createSignal<string>("")
const [selectedPreviewName, setSelectedPreviewName] = createSignal("")
const getObjsMutex = createMutex()
const toList = (tree: ObjTree[] | Obj[]): List => {
let l: List = {}
@@ -361,6 +384,20 @@ const Preview = () => {
}
return ret
}
// Build inner file url for current path by filename
const buildInnerUrl = (name: string) => {
const innerPath =
(innerPaths().length > 0 ? "/" + innerPaths().join("/") : "") + "/" + name
return innerPath
}
// Build obj with inner property
const buildObjWithInner = (obj: Obj): ArchiveObj => {
const innerPath =
innerPaths().length > 0 ? "/" + innerPaths().join("/") : ""
return { ...obj, sign: sign, inner_path: innerPath, archive: originalObj }
}
const sortObjs = (orderBy: OrderBy, reverse?: boolean) => {
batch(() => {
setExtractFolder("")
@@ -370,13 +407,79 @@ const Preview = () => {
}
})
}
// Get all files for navigation
const files = createMemo(() =>
sortedObjs()
.filter((obj) => !obj.is_dir)
.map((f) => buildObjWithInner(f)),
)
const previews = createMemo(() => {
const file = files().find((f) => f.name === selectedFile())
if (!file) return []
return getPreviews({ ...file, provider: objStore.provider })
})
const currentPreview = createMemo(() => {
const p = previews()
if (p.length === 0) return null
if (selectedPreviewName()) {
const found = p.find((item) => item.name === selectedPreviewName())
if (found) return found
}
return p[0]
})
// Cast to ArchiveObj to make sure onCleanup can delete archive property correctly
const originalObj: ArchiveObj = {
...objStore.obj,
inner_path: undefined,
archive: undefined,
}
const originalRawUrl = objStore.raw_url
const changeFile = (name: string) => {
batch(() => {
if (name === "") {
// Restore
ObjStore.setObj(originalObj)
ObjStore.setRawUrl(originalRawUrl)
setSelectedFile("")
} else {
// Set new
const file = files().find((f) => f.name === name)
if (file) {
const innerUrl = rawLink(file)
ObjStore.setObj(file)
ObjStore.setRawUrl(innerUrl)
setSelectedFile(name)
}
}
})
}
onCleanup(() => {
// Restore original values
ObjStore.setObj(originalObj)
ObjStore.setRawUrl(originalRawUrl)
})
createEffect(() => {
selectedFile()
setSelectedPreviewName("")
})
return (
<VStack spacing="$2" w="$full">
<Breadcrumb pl="$2" pr="$2" w="$full">
<BreadcrumbItem>
<BreadcrumbLink
currentPage={innerPaths().length === 0}
on:click={() => setInnerPaths([])}
currentPage={innerPaths().length === 0 && !selectedFile()}
on:click={() => {
setInnerPaths([])
changeFile("")
}}
>
.
</BreadcrumbLink>
@@ -386,14 +489,23 @@ const Preview = () => {
<BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbLink
currentPage={innerPaths().length === i() + 1}
on:click={() => setInnerPaths(innerPaths().slice(0, i() + 1))}
currentPage={innerPaths().length === i() + 1 && !selectedFile()}
on:click={() => {
setInnerPaths(innerPaths().slice(0, i() + 1))
changeFile("")
}}
>
{name}
</BreadcrumbLink>
</BreadcrumbItem>
)}
</For>
<Show when={selectedFile()}>
<BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbLink currentPage={true}>{selectedFile()}</BreadcrumbLink>
</BreadcrumbItem>
</Show>
</Breadcrumb>
<Switch>
<Match when={error() !== ""}>
@@ -414,47 +526,75 @@ const Preview = () => {
</Password>
</Match>
<Match when={!requiringPassword() && error() === ""}>
<MaybeLoading loading={loading()}>
<VStack class="list" w="$full" spacing="$1">
<ListTitle sortCallback={sortObjs} disableCheckbox />
<For each={sortedObjs()}>
{(obj, i) => {
let url = undefined
let innerPath =
(innerPaths().length > 0
? "/" + innerPaths().join("/")
: "") +
"/" +
obj.name
if (!obj.is_dir) {
const hasQuery = raw_url.includes("?")
url =
raw_url +
`${hasQuery ? "&" : "?"}inner=${encodePath(innerPath, true)}`
if (archive_pass !== "") {
url = url + `&pass=${encodeURIComponent(archive_pass)}`
}
if (sign !== "") {
url = url + `&sign=${sign}`
}
}
return (
<ListItem
obj={obj}
index={i()}
jumpCallback={() =>
setInnerPaths(innerPaths().concat(obj.name))
}
innerPath={innerPath}
url={url}
pass={archive_pass}
<Show
when={selectedFile()}
fallback={
<MaybeLoading loading={loading()}>
<VStack class="list" w="$full" spacing="$1">
<ListTitle sortCallback={sortObjs} disableCheckbox />
<For each={sortedObjs()}>
{(obj, i) => {
const objWithInner = buildObjWithInner(obj)
// Use rawLink to construct the URL for the object
let url = !obj.is_dir ? rawLink(objWithInner) : undefined
let innerPath = buildInnerUrl(obj.name)
return (
<ListItem
obj={obj}
index={i()}
jumpCallback={() =>
setInnerPaths(innerPaths().concat(obj.name))
}
innerPath={innerPath}
url={url}
pass={archive_pass}
onFileClick={() => changeFile(obj.name)}
/>
)
}}
</For>
<ContextMenu />
</VStack>
</MaybeLoading>
}
>
<Show when={selectedFile()} fallback={<FullLoading />}>
<VStack w="$full" spacing="$2" alignItems="center">
<Show when={currentPreview()}>
<Suspense fallback={<FullLoading />}>
<Dynamic
component={currentPreview()?.component}
images={files().filter((f) => f.type === ObjType.IMAGE)}
navigate={(name) => {
changeFile(name)
}}
/>
)
}}
</For>
<ContextMenu />
</VStack>
</MaybeLoading>
</Suspense>
</Show>
<HStack w="$full" justifyContent="center" spacing="$2" p="$2">
<Show when={previews().length > 1}>
<SelectWrapper
value={currentPreview()?.name || ""}
onChange={(value) =>
setSelectedPreviewName(String(value))
}
options={previews().map((p) => ({
value: p.name,
label: p.name,
}))}
/>
</Show>
<OpenWith
file={{
name: selectedFile(),
raw_url: objStore.raw_url,
d_url: objStore.raw_url,
}}
/>
</HStack>
</VStack>
</Show>
</Show>
</Match>
</Switch>
<Show when={comment() !== ""}>

View File

@@ -1,22 +1,50 @@
import { Error, FullLoading, ImageWithError } from "~/components"
import { useRouter, useT } from "~/hooks"
import { objStore } from "~/store"
import { ObjType } from "~/types"
import { Obj, ObjType } from "~/types"
import { onCleanup, onMount } from "solid-js"
const Preview = () => {
interface PreviewProps {
images?: Obj[]
navigate?: (name: string) => void
}
const Preview = (props: PreviewProps) => {
const t = useT()
const { replace } = useRouter()
let images = objStore.objs.filter((obj) => obj.type === ObjType.IMAGE)
let images =
props.images || objStore.objs.filter((obj) => obj.type === ObjType.IMAGE)
if (images.length === 0) {
images = [objStore.obj]
}
const onKeydown = (e: KeyboardEvent) => {
const prev = () => {
const index = images.findIndex((f) => f.name === objStore.obj.name)
if (e.key === "ArrowLeft" && index > 0) {
replace(images[index - 1].name)
} else if (e.key === "ArrowRight" && index < images.length - 1) {
replace(images[index + 1].name)
if (index > 0) {
if (props.navigate) {
props.navigate(images[index - 1].name)
} else {
replace(images[index - 1].name)
}
}
}
const next = () => {
const index = images.findIndex((f) => f.name === objStore.obj.name)
if (index < images.length - 1) {
if (props.navigate) {
props.navigate(images[index + 1].name)
} else {
replace(images[index + 1].name)
}
}
}
const onKeydown = (e: KeyboardEvent) => {
if (e.key === "ArrowLeft") {
prev()
} else if (e.key === "ArrowRight") {
next()
}
}
onMount(() => {

View File

@@ -1,6 +1,6 @@
import { Component, lazy } from "solid-js"
import { getIframePreviews, me, getSettingBool, isArchive } from "~/store"
import { Obj, ObjType, UserMethods, UserPermissions } from "~/types"
import { Obj, ObjType, UserMethods, UserPermissions, ArchiveObj } from "~/types"
import { ext } from "~/utils"
import { generateIframePreview } from "./iframe"
import { useRouter } from "~/hooks"
@@ -34,6 +34,7 @@ export interface Preview {
provider?: RegExp
component: Component
prior: Prior
availableInArchive?: boolean
}
export type PreviewComponent = Pick<Preview, "name" | "component">
@@ -82,6 +83,7 @@ const previews: Preview[] = [
exts: ["url"],
component: lazy(() => import("./text-editor")),
prior: true,
availableInArchive: false,
},
{
name: "Image",
@@ -172,6 +174,7 @@ const previews: Preview[] = [
!getSettingBool("share_preview_download_by_default"))
)
},
availableInArchive: false,
},
]
@@ -186,6 +189,7 @@ export const getPreviews = (
const downloadPrior =
(!isShare() && getSettingBool("preview_download_by_default")) ||
(isShare() && getSettingBool("share_preview_download_by_default"))
const isInArchive = !!(file as ArchiveObj).archive
// internal previews
if (!isShare() || getSettingBool("share_preview")) {
previews.forEach((preview) => {
@@ -198,6 +202,10 @@ export const getPreviews = (
extsContains(preview.exts, file.name)
) {
const r = { name: preview.name, component: preview.component }
// Skip previews that are not available in archive when file is in archive
if (isInArchive && preview.availableInArchive === false) {
return
}
if (!downloadPrior && isPrior(preview.prior)) {
res.push(r)
} else {

View File

@@ -26,6 +26,11 @@ export type StoreObj = Obj & {
selected?: boolean
}
export type ArchiveObj = Obj & {
inner_path?: string
archive?: Obj
}
export type RenameObj = {
src_name: string
new_name: string