feat(files): wide file manager layout and basic previews (FM-2)
- Files page uses a wide layout (route handle layout:"wide"): no max width, full-height directory tree docked next to the sidebar, resizable (200-520px, remembered, double-click resets) and collapsible; mobile keeps the tree in a drawer. - GET /files/preview (admin console session only, same path guards as download): text/NFO with encoding detection (BOM/UTF-16/UTF-8/GB18030/Big5), subtitles first 200 cues (SRT/VTT/ASS/SSA, PGS timings), .torrent file list/trackers (passkey masked)/infohash, image dimensions. Reads at most 1 MiB of text; .torrent capped at 4 MiB. - Download also allows text files, .torrent and avif/gif. - Research notes in docs/design/file-preview-research.md. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
3add4bab41
commit
289e8b297a
20 files changed
+1294
-159
No files matched your search
@@ -519,3 +519,39 @@ export function applyIdentifyCompleted(r: FilesIdentifyResult, p: IdentifyComple
|
||||
items: r.items.map((it) => by.get(identifyKey(it)) ?? it),
|
||||
};
|
||||
}
|
||||
|
||||
// ── 预览(FM-2)──
|
||||
|
||||
export type PreviewKind = "text" | "subtitle" | "torrent" | "image";
|
||||
const PREVIEW_EXT: Record<string, PreviewKind> = {};
|
||||
for (const e of "nfo txt log md markdown json xml yaml yml ini conf cfg csv tsv cue sfv md5 sha1 sha256 url".split(" "))
|
||||
PREVIEW_EXT[e] = "text";
|
||||
for (const e of "srt vtt ass ssa sup".split(" ")) PREVIEW_EXT[e] = "subtitle";
|
||||
for (const e of "jpg jpeg png webp avif gif bmp".split(" ")) PREVIEW_EXT[e] = "image";
|
||||
PREVIEW_EXT.torrent = "torrent";
|
||||
|
||||
/** 与服务端 preview-parse.ts 同一张扩展名表;目录、链接、无扩展名返回 null */
|
||||
export function previewKindOf(e: Pick<FileEntry, "type" | "ext">): PreviewKind | null {
|
||||
if (e.type !== "file" || !e.ext) return null;
|
||||
return PREVIEW_EXT[e.ext.toLowerCase()] ?? null;
|
||||
}
|
||||
|
||||
/** 毫秒 → 00:01:02.345 */
|
||||
export function cueTime(ms: number): string {
|
||||
const h = Math.floor(ms / 3_600_000);
|
||||
const m = Math.floor((ms % 3_600_000) / 60_000);
|
||||
const s = Math.floor((ms % 60_000) / 1000);
|
||||
const f = ms % 1000;
|
||||
const p = (n: number, w = 2) => String(n).padStart(w, "0");
|
||||
return `${p(h)}:${p(m)}:${p(s)}.${p(f, 3)}`;
|
||||
}
|
||||
|
||||
/** JSON 文本美化;解析失败或已截断时原样返回 */
|
||||
export function prettyJson(text: string, truncated: boolean): string {
|
||||
if (truncated) return text;
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(text), null, 2);
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user