import { ArrowLeft, ChevronDown, ChevronUp, Download, FileAudio, FileBox, FileImage, FileVideo, FolderOpen, FolderPlus, RefreshCw, Search, Settings2, Upload } from "lucide-react"; import { useEffect, useMemo, useRef, useState, type ChangeEvent } from "react"; import { App, Button, Drawer, Empty, Input, Spin, Tag, Tree, Typography } from "antd"; import type { DataNode } from "antd/es/tree"; import { useNavigate } from "react-router"; import "@/lib/plugins/builtin"; import { createEagleAssetSource, EAGLE_DEFAULT_BASE_URL, eagleAssetPlugin } from "@/lib/plugins/builtin/eagle"; import type { ExternalAssetFolder, ExternalAssetItem } from "@/lib/plugins/plugin-types"; import type { Asset } from "@/stores/use-asset-store"; import { usePluginStore } from "@/stores/use-plugin-store"; import { CollectionGrid, ListToolbar, PageHeader, PaginationBar, WorkspacePage } from "@/components/layout/workspace-page"; import { AssetLibraryCard, AssetLibraryCardMedia } from "@/components/assets/asset-library-card"; import "./eagle.css"; export default function EagleLibraryPage() { const navigate = useNavigate(); const { message } = App.useApp(); const installations = usePluginStore((state) => state.installations); const hydrated = usePluginStore((state) => state.hydrated); const ensurePlugin = usePluginStore((state) => state.ensurePlugin); const installation = installations.find((item) => item.manifest.id === eagleAssetPlugin.manifest.id); const enabled = Boolean(installation?.enabled); const savedBaseUrl = installation?.config.baseUrl; const baseUrl = typeof savedBaseUrl === "string" && savedBaseUrl.trim() ? savedBaseUrl.trim() : EAGLE_DEFAULT_BASE_URL; const provider = useMemo(() => createEagleAssetSource(baseUrl), [baseUrl]); const fileInputRef = useRef(null); const [folders, setFolders] = useState([]); const [items, setItems] = useState([]); const [selectedFolder, setSelectedFolder] = useState(""); const [keyword, setKeyword] = useState(""); const [folderName, setFolderName] = useState(""); const [creatingFolder, setCreatingFolder] = useState(false); const [loading, setLoading] = useState(false); const [working, setWorking] = useState(false); const [error, setError] = useState(""); const [progress, setProgress] = useState(""); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(40); const [previewItem, setPreviewItem] = useState(null); const [foldersExpanded, setFoldersExpanded] = useState(true); const treeData = useMemo(() => renderFolderNodes(folders), [folders]); const folderPath = useMemo(() => externalFolderPath(folders, selectedFolder), [folders, selectedFolder]); const currentFolder = folders.find((folder) => folder.id === selectedFolder); const visibleItems = useMemo(() => items.slice((page - 1) * pageSize, page * pageSize), [items, page, pageSize]); const totalBytes = useMemo(() => items.reduce((total, item) => total + (item.bytes || 0), 0), [items]); useEffect(() => { ensurePlugin(eagleAssetPlugin.manifest); }, [ensurePlugin]); const load = async (folderId = selectedFolder, search = keyword) => { setLoading(true); setError(""); try { const [nextFolders, nextItems] = await Promise.all([ provider.listFolders?.(), provider.list?.({ folderId: folderId || undefined, keyword: search.trim() || undefined, limit: 100, offset: 0 }), ]); setFolders(nextFolders || []); setItems(nextItems || []); setPage(1); } catch (reason) { setError(reason instanceof Error ? reason.message : "连接 Eagle 失败,请确认 Eagle 已启动"); setItems([]); } finally { setLoading(false); } }; useEffect(() => { if (!enabled) return; setSelectedFolder(""); setKeyword(""); void load("", ""); // provider 随本机 API 地址变化;页面进入或配置变化时重新读取 Eagle。 // eslint-disable-next-line react-hooks/exhaustive-deps }, [enabled, provider]); const handleFolderSelect = (nextFolder: string) => { const folderId = nextFolder === "root" ? "" : nextFolder; setSelectedFolder(folderId); setKeyword(""); setPreviewItem(null); setPage(1); void load(folderId, ""); }; const handleCreateFolder = async () => { const name = folderName.trim(); if (!name || !provider.createFolder || creatingFolder) return; setCreatingFolder(true); setError(""); try { await provider.createFolder(name, selectedFolder || undefined); setFolderName(""); message.success("已在" + (currentFolder?.name || "Eagle 素材库") + "中新建文件夹"); await load(); } catch (reason) { setError(reason instanceof Error ? reason.message : "新建 Eagle 文件夹失败"); } finally { setCreatingFolder(false); } }; const handleUpload = async (event: ChangeEvent) => { const files = Array.from(event.target.files || []); event.target.value = ""; if (!files.length || !provider.uploadFile || working) return; setWorking(true); setError(""); let uploaded = 0; try { for (const [index, file] of files.entries()) { setProgress("正在写入 " + (index + 1) + "/" + files.length + ":" + file.name); await provider.uploadFile(file, selectedFolder || undefined); uploaded += 1; } message.success("已写入 Eagle " + uploaded + " 个文件"); await load(); } catch (reason) { setError(reason instanceof Error ? reason.message : "写入 Eagle 失败"); if (uploaded) message.warning("已写入 " + uploaded + " 个文件,剩余文件未完成"); } finally { setProgress(""); setWorking(false); } }; if (hydrated && !enabled) { return ( } onClick={() => navigate("/assets")}>返回影策素材库} />

先启用 Eagle 素材来源

启用后,这里会直接显示 Eagle 原本的文件夹和文件,不会把素材复制成影策本地素材。

); } return ( <>
{error ? "Eagle · 连接异常" : "Eagle · 已连接"}} actions={(
)} /> { setKeyword(""); setPage(1); void load(selectedFolder, ""); }} trailing={} > } value={keyword} placeholder="搜索 Eagle 素材标题、标签或文件夹" onChange={(event) => { setPage(1); setKeyword(event.target.value); }} onPressEnter={() => void load()} />
{error ?
{error}
: null}

{currentFolder?.name || "全部素材"}

{items.length} 个素材

{currentFolder ? "当前文件夹由 Eagle 管理,影策只负责展示和调用。" : "当前展示 Eagle 素材库中的全部文件。"}

{folderName ?
setFolderName(event.target.value)} onPressEnter={() => void handleCreateFolder()} placeholder="输入文件夹名称" className="min-w-48 flex-1" aria-label="新文件夹名称" />
: null} {loading ?
: items.length ? ( <> {visibleItems.map((item) => setPreviewItem(item)} />)} { setPage(nextPageSize !== pageSize ? 1 : nextPage); setPageSize(nextPageSize); }} /> ) :
}
void handleUpload(event)} />
setPreviewItem(null)} totalBytes={totalBytes} /> ); } function EagleItemCard({ item, selected, onOpen }: { item: ExternalAssetItem; selected: boolean; onOpen: () => void }) { return {assetKindLabel(item.kind)} Eagle {item.fileUrl ? : null} ; } function EagleAssetDrawer({ item, onClose, totalBytes }: { item: ExternalAssetItem | null; onClose: () => void; totalBytes: number }) { return {item ?

{item.title}

Eagle 原文件 · {item.folderPath?.join(" / ") || "根目录"}

{item.thumbnailUrl ? {item.title} : }
{item.tags?.length ?
标签
{item.tags.map((tag) => {tag})}
: null} {item.description ?
备注

{item.description}

: null} {item.fileUrl ? : null}
: null}
; } function EagleFact({ label, value }: { label: string; value: string }) { return
{label}{value}
; } function AssetKindIcon({ kind, size = "size-5" }: { kind: Asset["kind"]; size?: string }) { if (kind === "image") return