Add security hardening, Ghidra, recreate skill, checkpoints, shells, web search and themes
- Sandbox (bubblewrap) for non-admin shell/git, MCP per-user rights, hard budgets and quotas, login rate limiting, audit log and backups - Ghidra headless decompilation (ARM64 natives built from source), binary_info tool, clean room / deep workflow per session - Recreate skill and commands for analysing and re-building old software - File checkpoints with rewind/restore, background shells (bash_output, kill_shell) - web_search tool (DuckDuckGo, SearXNG, Brave, Tavily) - In-app dialogs and toasts instead of browser dialogs - 15 base themes (trilium-themes palettes plus Nord, VS Code, GitHub, Catppuccin, One, Dracula, Rose Pine, Everforest, Kanagawa) with light/dark/auto switcher and license notes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fab9add5d5
commit
5b569b6835
108 files changed
+6785
-279
No files matched your search
@@ -0,0 +1,141 @@
|
||||
import { lazy, Suspense, useEffect, useState } from "react";
|
||||
import { get } from "../api";
|
||||
import { useT } from "../i18n";
|
||||
import { Spinner } from "./Activity";
|
||||
|
||||
const ModelViewer = lazy(() => import("./ModelViewer").then((m) => ({ default: m.ModelViewer })));
|
||||
|
||||
export type PreviewKind = "image" | "pdf" | "doc" | "audio" | "module" | "model";
|
||||
|
||||
const KINDS: [RegExp, PreviewKind][] = [
|
||||
[/\.(png|jpe?g|gif|webp|bmp|svg)$/i, "image"],
|
||||
[/\.pdf$/i, "pdf"],
|
||||
[/\.(docx|pptx|xlsx|xlsm|xls|ods)$/i, "doc"],
|
||||
[/\.(wav|mp3|ogg|oga|flac|m4a)$/i, "audio"],
|
||||
[/\.(xm|mod|s3m|it)$/i, "module"],
|
||||
[/\.(glb|gltf|stl|obj|step|stp|igs|iges)$/i, "model"],
|
||||
];
|
||||
|
||||
export function previewKind(path: string): PreviewKind | undefined {
|
||||
return KINDS.find(([re]) => re.test(path))?.[1];
|
||||
}
|
||||
|
||||
const q = (p: string) => `path=${encodeURIComponent(p)}`;
|
||||
|
||||
function PdfPreview({ path }: { path: string }) {
|
||||
const t = useT();
|
||||
const [pages, setPages] = useState<number>();
|
||||
const [page, setPage] = useState(1);
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
get<{ pages: number }>(`/files/preview/pdf-info?${q(path)}`).then((r) => setPages(r.pages)).catch(() => setPages(1));
|
||||
}, [path]);
|
||||
return (
|
||||
<div className="pdf-preview">
|
||||
<div className="row pdf-nav">
|
||||
<button className="small" disabled={page <= 1} onClick={() => setPage(page - 1)}>
|
||||
‹
|
||||
</button>
|
||||
<span>{t("preview.page", { n: page, total: pages ?? "…" })}</span>
|
||||
<button className="small" disabled={!pages || page >= pages} onClick={() => setPage(page + 1)}>
|
||||
›
|
||||
</button>
|
||||
<span className="spacer" />
|
||||
<a className="button small" href={`/api/files/raw?${q(path)}`} target="_blank" rel="noreferrer">
|
||||
{t("preview.openBrowser")}
|
||||
</a>
|
||||
</div>
|
||||
<div className="pane-scroll image-preview">
|
||||
<img key={page} src={`/api/files/preview/pdf-page?${q(path)}&page=${page}`} alt={`page ${page}`} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface DocData {
|
||||
kind: "text" | "table";
|
||||
text?: string;
|
||||
sheets?: { name: string; rows: string[][]; total_rows: number }[];
|
||||
}
|
||||
|
||||
function DocPreview({ path }: { path: string }) {
|
||||
const t = useT();
|
||||
const [data, setData] = useState<DocData>();
|
||||
const [error, setError] = useState<string>();
|
||||
const [sheet, setSheet] = useState(0);
|
||||
useEffect(() => {
|
||||
setData(undefined);
|
||||
setSheet(0);
|
||||
get<DocData>(`/files/preview/doc?${q(path)}`).then(setData).catch((e) => setError(e.message));
|
||||
}, [path]);
|
||||
if (error) return <div className="notice err">{error}</div>;
|
||||
if (!data) return <div className="empty"><Spinner /></div>;
|
||||
if (data.kind === "text") return <pre className="pane-scroll doc-text">{data.text}</pre>;
|
||||
const s = data.sheets?.[sheet];
|
||||
return (
|
||||
<div className="pdf-preview">
|
||||
<div className="mode-tabs sheet-tabs">
|
||||
{data.sheets?.map((x, i) => (
|
||||
<button key={x.name} className={i === sheet ? "active" : ""} onClick={() => setSheet(i)}>
|
||||
{x.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="pane-scroll">
|
||||
{s && (
|
||||
<table className="grid sheet">
|
||||
<tbody>
|
||||
{s.rows.map((r, i) => (
|
||||
<tr key={i}>
|
||||
<th className="num">{i + 1}</th>
|
||||
{r.map((c, j) => (
|
||||
<td key={j}>{c}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
{s && s.total_rows > s.rows.length && <div className="empty">{t("preview.moreRows", { n: s.total_rows - s.rows.length })}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FilePreview({ path, kind }: { path: string; kind: PreviewKind }) {
|
||||
const t = useT();
|
||||
const raw = `/api/files/raw?${q(path)}`;
|
||||
switch (kind) {
|
||||
case "image":
|
||||
return (
|
||||
<div className="pane-scroll image-preview">
|
||||
<img src={raw} alt="" />
|
||||
</div>
|
||||
);
|
||||
case "pdf":
|
||||
return <PdfPreview path={path} />;
|
||||
case "doc":
|
||||
return <DocPreview path={path} />;
|
||||
case "audio":
|
||||
return (
|
||||
<div className="audio-preview">
|
||||
<audio controls src={raw} preload="metadata" />
|
||||
</div>
|
||||
);
|
||||
case "module":
|
||||
return (
|
||||
<div className="audio-preview">
|
||||
<div className="muted">{t("preview.moduleHint")}</div>
|
||||
<audio controls src={`/api/files/preview/module?${q(path)}`} preload="none" />
|
||||
</div>
|
||||
);
|
||||
case "model": {
|
||||
const ext = path.split(".").pop()!.toLowerCase();
|
||||
return (
|
||||
<Suspense fallback={<div className="empty"><Spinner /></div>}>
|
||||
<ModelViewer url={raw} ext={ext} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user