feat(web): 存储设置与文件管理的 Alist 存储切换
- 设置 → 媒体库与目录 → 存储:登记 Alist / OpenList(令牌 / 账号密码 / 访客、路径密码、限速与监控选项), 测试连接显示版本、用户与容量;媒体库目录可选存储 - 文件管理左栏多一组「Alist 存储」,路径显示「存储名:/路径」;移动 / 复制目标可选 Alist 存储; 上传按钮;Alist 根点刷新才让 Alist 刷新目录缓存 - 运行状态的挂载根列出存储实例(令牌失效单独提示) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
c1bc389058
commit
cf364435eb
18 files changed
+618
-41
No files matched your search
@@ -11,7 +11,7 @@ import {
|
||||
} from "@umt/contracts";
|
||||
import { PlugZap } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { downloaders, libraryRoots, mediaServers } from "~/api/instances.ts";
|
||||
import { downloaders, libraryRoots, mediaServers, storages } from "~/api/instances.ts";
|
||||
import { StatusBadge } from "~/components/console/status-badge.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
@@ -360,7 +360,25 @@ const MEDIA_KIND = [
|
||||
{ value: "other", label: "其它" },
|
||||
];
|
||||
|
||||
/** 库根路径 ↔ 存储 + 路径:Alist 上的库根存成 /@alist/<存储 id>/<Alist 内路径> */
|
||||
const ALIST_PATH = /^\/@alist\/([0-9a-f-]{36})(\/.*)?$/i;
|
||||
export function splitRootPath(p: string): { storage: string; path: string } {
|
||||
const m = ALIST_PATH.exec(p);
|
||||
return m ? { storage: m[1]!.toLowerCase(), path: m[2] || "/" } : { storage: "local", path: p };
|
||||
}
|
||||
export function joinRootPath(storage: string, path: string): string {
|
||||
if (storage === "local") return path;
|
||||
const p = `/${path.trim()}`.replace(/\/+/g, "/").replace(/\/$/, "");
|
||||
return `/@alist/${storage}${p}`;
|
||||
}
|
||||
|
||||
export function LibraryRootsTable() {
|
||||
const storageList = storages.useList();
|
||||
const storageOptions = [
|
||||
{ value: "local", label: "本机" },
|
||||
...(storageList.data?.items ?? []).map((s) => ({ value: s.id, label: `${s.name}(Alist)` })),
|
||||
];
|
||||
const storageName = (id: string) => storageList.data?.items.find((s) => s.id === id)?.name ?? "Alist";
|
||||
const list = libraryRoots.useList();
|
||||
const create = libraryRoots.useCreate();
|
||||
const update = libraryRoots.useUpdate();
|
||||
@@ -371,7 +389,8 @@ export function LibraryRootsTable() {
|
||||
hooks={{ list, create, update, remove } as never}
|
||||
config={{
|
||||
noun: "媒体库目录",
|
||||
description: "保存时会在目录里放一个标记文件,用来判断目录是否挂载;目录不存在或不可写时无法保存。",
|
||||
description:
|
||||
"本机目录保存时会放一个标记文件,用来判断目录是否挂载;目录不存在或不可写时无法保存。也可以选登记好的 Alist 存储,Alist 上的目录要先建好。",
|
||||
deleteHint: "只删除登记,磁盘上的文件不动。",
|
||||
columns: [
|
||||
{
|
||||
@@ -380,7 +399,12 @@ export function LibraryRootsTable() {
|
||||
cell: (r) => (
|
||||
<div className="min-w-0">
|
||||
<p className="font-medium">{r.name}</p>
|
||||
<p className="truncate font-mono text-meta text-muted-foreground">{r.localPath}</p>
|
||||
<p className="truncate font-mono text-meta text-muted-foreground">
|
||||
{(() => {
|
||||
const sp = splitRootPath(r.localPath);
|
||||
return sp.storage === "local" ? r.localPath : `${storageName(sp.storage)}:${sp.path}`;
|
||||
})()}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -402,13 +426,20 @@ export function LibraryRootsTable() {
|
||||
fields: [
|
||||
{ name: "name", label: "名称", kind: "text", required: true, placeholder: "例如:电影库" },
|
||||
{ name: "mediaKind", label: "内容类型", kind: "select", options: MEDIA_KIND },
|
||||
{
|
||||
name: "storage",
|
||||
label: "存储",
|
||||
kind: "select",
|
||||
options: storageOptions,
|
||||
hint: "本机目录,或在「存储」里登记的 Alist / OpenList",
|
||||
},
|
||||
{
|
||||
name: "localPath",
|
||||
label: "本地路径",
|
||||
label: "路径",
|
||||
kind: "text",
|
||||
required: true,
|
||||
placeholder: "/media/movies",
|
||||
hint: "服务进程能访问的绝对路径",
|
||||
hint: "本机:服务进程能访问的绝对路径;Alist:Alist 里的路径,如 /115/电影",
|
||||
wide: true,
|
||||
},
|
||||
{
|
||||
@@ -428,6 +459,7 @@ export function LibraryRootsTable() {
|
||||
initial: {
|
||||
name: "",
|
||||
mediaKind: "movie",
|
||||
storage: "local",
|
||||
localPath: "",
|
||||
managementMode: "register_only",
|
||||
enabled: true,
|
||||
@@ -436,7 +468,8 @@ export function LibraryRootsTable() {
|
||||
fromRow: (r) => ({
|
||||
name: r.name,
|
||||
mediaKind: r.mediaKind,
|
||||
localPath: r.localPath,
|
||||
storage: splitRootPath(r.localPath).storage,
|
||||
localPath: splitRootPath(r.localPath).path,
|
||||
managementMode: r.managementMode,
|
||||
enabled: r.enabled,
|
||||
notes: r.notes ?? "",
|
||||
@@ -444,7 +477,7 @@ export function LibraryRootsTable() {
|
||||
toBody: (v) => ({
|
||||
name: str(v.name),
|
||||
mediaKind: str(v.mediaKind),
|
||||
localPath: str(v.localPath),
|
||||
localPath: joinRootPath(str(v.storage) || "local", str(v.localPath)),
|
||||
managementMode: str(v.managementMode),
|
||||
enabled: v.enabled === true,
|
||||
notes: nullStr(v.notes),
|
||||
|
||||
Reference in new issue
Block a user