- CompanyView.vue: hero, plattform, produkte, nyx CTA, footer - ImpressumView.vue + DatenschutzView.vue: legal pages - Router: HTML5 history mode (no # URLs), company routes - Reverted vite-ssg (SSR compat needs proper refactor, planned) - Removed ssr-shim.ts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
446 lines
18 KiB
JavaScript
446 lines
18 KiB
JavaScript
import { computed, ref, onMounted, watch, onUnmounted, defineComponent, mergeProps, unref, withCtx, openBlock, createBlock, createVNode, toDisplayString, Fragment, renderList, createCommentVNode, createTextVNode, useSSRContext } from "vue";
|
|
import { ssrRenderAttrs, ssrRenderList, ssrRenderClass, ssrInterpolate, ssrRenderAttr, ssrRenderComponent } from "vue/server-renderer";
|
|
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
|
import { i as useViewerStore, g as getApiBase, w as ws, l as lastViewerPath, s as scrollbarOptions, _ as _export_sfc } from "../main.mjs";
|
|
import { ArrowDownTrayIcon, FolderOpenIcon, FolderIcon, DocumentIcon } from "@heroicons/vue/20/solid";
|
|
import { useRoute, useRouter } from "vue-router";
|
|
import { marked } from "marked";
|
|
import "@unhead/vue/server";
|
|
import "pinia";
|
|
import "@heroicons/vue/24/outline";
|
|
import "overlayscrollbars";
|
|
function useViewer() {
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const { onMessage } = ws;
|
|
const viewerStore = useViewerStore();
|
|
const fstoken = computed(() => viewerStore.fstoken);
|
|
const viewerRoots = computed(() => viewerStore.roots);
|
|
const token = fstoken;
|
|
function normalizePath(path) {
|
|
if (!path) return path;
|
|
const roots = viewerStore.roots;
|
|
if (!roots.length) return path;
|
|
const [prefix, ...rest] = path.split("/");
|
|
if (roots.includes(prefix)) return path;
|
|
const canonical = roots.find((r) => r === `workspace-${prefix}` || r.endsWith(`-${prefix}`));
|
|
return canonical ? [canonical, ...rest].join("/") : path;
|
|
}
|
|
const currentPath = ref(normalizePath(
|
|
route.query.path || localStorage.getItem("viewer_last_path") || ""
|
|
));
|
|
const sidebarCollapsed = ref(window.innerWidth < 768);
|
|
const content = ref("");
|
|
const fileType = ref("");
|
|
const dirFiles = ref([]);
|
|
const dirDirs = ref([]);
|
|
const loading = ref(false);
|
|
const showLoading = ref(false);
|
|
let loadingTimer = null;
|
|
const fetchError = ref("");
|
|
const pdfCacheBust = ref(Date.now());
|
|
const mdRaw = ref(false);
|
|
const mdLineCount = computed(() => {
|
|
if (!content.value) return 0;
|
|
return content.value.split("\n").length;
|
|
});
|
|
const renderedMd = computed(() => {
|
|
if (fileType.value !== "md" || !content.value) return "";
|
|
return marked.parse(content.value);
|
|
});
|
|
const pdfSrc = computed(() => {
|
|
if (fileType.value !== "pdf" || !currentPath.value || !token.value) return "";
|
|
const base = getBaseUrl();
|
|
return `${base}/api/viewer/file?path=${encodeURIComponent(currentPath.value)}&token=${encodeURIComponent(token.value)}&t=${pdfCacheBust.value}`;
|
|
});
|
|
function getBaseUrl() {
|
|
return getApiBase();
|
|
}
|
|
function extOf(p) {
|
|
const m = p.match(/\.([^./]+)$/);
|
|
return m ? m[1].toLowerCase() : "";
|
|
}
|
|
function startLoading(silent) {
|
|
if (silent) return;
|
|
loading.value = true;
|
|
showLoading.value = false;
|
|
if (loadingTimer) clearTimeout(loadingTimer);
|
|
loadingTimer = setTimeout(() => {
|
|
showLoading.value = true;
|
|
}, 3e3);
|
|
}
|
|
function stopLoading() {
|
|
loading.value = false;
|
|
showLoading.value = false;
|
|
if (loadingTimer) {
|
|
clearTimeout(loadingTimer);
|
|
loadingTimer = null;
|
|
}
|
|
}
|
|
async function fetchContent(path, silent = false, _retry = false) {
|
|
startLoading(silent);
|
|
try {
|
|
const base = getBaseUrl();
|
|
const url = `${base}/api/viewer/file?path=${encodeURIComponent(path)}&token=${encodeURIComponent(token.value)}`;
|
|
const res = await fetch(url);
|
|
if (res.status === 401 && !_retry) {
|
|
viewerStore.invalidate();
|
|
await viewerStore.acquire(true);
|
|
return fetchContent(path, silent, true);
|
|
}
|
|
if (!res.ok) {
|
|
fetchError.value = `${res.status}: ${await res.text()}`;
|
|
return;
|
|
}
|
|
fetchError.value = "";
|
|
content.value = await res.text();
|
|
} catch (e) {
|
|
fetchError.value = e.message || "Fetch failed";
|
|
} finally {
|
|
stopLoading();
|
|
}
|
|
}
|
|
async function openFile(path) {
|
|
path = normalizePath(path);
|
|
currentPath.value = path;
|
|
localStorage.setItem("viewer_last_path", path);
|
|
lastViewerPath.value = path;
|
|
if (route.query.path !== path) {
|
|
router.push({ name: "viewer", query: { path } });
|
|
}
|
|
const ext = extOf(path);
|
|
if (!path) {
|
|
fileType.value = "dir";
|
|
content.value = "";
|
|
fetchError.value = "";
|
|
dirDirs.value = viewerRoots.value.length ? viewerRoots.value : ["shared", "workspace-titan"];
|
|
dirFiles.value = [];
|
|
return;
|
|
}
|
|
if (!ext) {
|
|
fileType.value = "dir";
|
|
content.value = "";
|
|
fetchError.value = "";
|
|
startLoading(false);
|
|
try {
|
|
const base = getBaseUrl();
|
|
const url = `${base}/api/viewer/tree?root=${encodeURIComponent(path)}&token=${encodeURIComponent(token.value)}`;
|
|
const res = await fetch(url);
|
|
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
|
const data = await res.json();
|
|
dirDirs.value = data.dirs || [];
|
|
dirFiles.value = data.files || [];
|
|
} catch (e) {
|
|
dirDirs.value = [];
|
|
dirFiles.value = [];
|
|
fetchError.value = e.message || "Failed to load directory";
|
|
} finally {
|
|
stopLoading();
|
|
}
|
|
return;
|
|
}
|
|
if (ext === "pdf") {
|
|
fileType.value = "pdf";
|
|
content.value = "";
|
|
fetchError.value = "";
|
|
pdfCacheBust.value = Date.now();
|
|
fetch(`${getBaseUrl()}/api/viewer/file?path=${encodeURIComponent(path)}&token=${encodeURIComponent(token.value)}`, { method: "HEAD" }).catch(() => {
|
|
});
|
|
return;
|
|
}
|
|
fileType.value = ext === "md" ? "md" : "text";
|
|
fetchError.value = "";
|
|
await fetchContent(path, false);
|
|
}
|
|
function onCopy(e) {
|
|
const sel = window.getSelection();
|
|
if (!sel || sel.isCollapsed) return;
|
|
const html = (() => {
|
|
const div = document.createElement("div");
|
|
div.appendChild(sel.getRangeAt(0).cloneContents());
|
|
return div.innerHTML;
|
|
})();
|
|
const clean = html.replace(/background(-color)?:[^;"]*(;|(?="))/gi, "");
|
|
const plain = sel.toString();
|
|
e.clipboardData.setData("text/html", clean);
|
|
e.clipboardData.setData("text/plain", plain);
|
|
e.preventDefault();
|
|
}
|
|
async function refreshFile(path) {
|
|
const ext = extOf(path);
|
|
if (ext === "pdf") {
|
|
pdfCacheBust.value = Date.now();
|
|
return;
|
|
}
|
|
await fetchContent(path, true);
|
|
}
|
|
let unsubscribe = null;
|
|
onMounted(() => {
|
|
unsubscribe = onMessage((data) => {
|
|
if (data.type === "viewer_file_changed" && data.path === currentPath.value) {
|
|
refreshFile(currentPath.value);
|
|
}
|
|
if (data.type === "viewer_tree_changed" && data.path === currentPath.value && fileType.value === "dir") {
|
|
openFile(currentPath.value);
|
|
}
|
|
});
|
|
viewerStore.acquire();
|
|
if (currentPath.value) openFile(currentPath.value);
|
|
});
|
|
watch(() => route.query.path, (newPath) => {
|
|
if (newPath && newPath !== currentPath.value) {
|
|
openFile(newPath);
|
|
}
|
|
});
|
|
onUnmounted(() => {
|
|
if (unsubscribe) unsubscribe();
|
|
});
|
|
return {
|
|
fstoken,
|
|
viewerRoots,
|
|
currentPath,
|
|
sidebarCollapsed,
|
|
content,
|
|
fileType,
|
|
loading,
|
|
fetchError,
|
|
mdRaw,
|
|
mdLineCount,
|
|
renderedMd,
|
|
pdfSrc,
|
|
openFile,
|
|
onCopy,
|
|
dirFiles,
|
|
dirDirs,
|
|
showLoading
|
|
};
|
|
}
|
|
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
...{ name: "ViewerView" },
|
|
__name: "ViewerView",
|
|
__ssrInlineRender: true,
|
|
setup(__props) {
|
|
const {
|
|
fstoken,
|
|
currentPath,
|
|
content,
|
|
fileType,
|
|
showLoading,
|
|
fetchError,
|
|
mdRaw,
|
|
renderedMd,
|
|
pdfSrc,
|
|
openFile,
|
|
onCopy,
|
|
dirFiles,
|
|
dirDirs
|
|
} = useViewer();
|
|
const contentLines = computed(() => content.value.split("\n"));
|
|
const fileName = computed(() => {
|
|
if (!currentPath.value) return "";
|
|
return currentPath.value.split("/").pop() || "";
|
|
});
|
|
const downloadUrl = computed(() => {
|
|
if (!currentPath.value || !fstoken.value) return "";
|
|
return `${getApiBase()}/api/viewer/file?path=${encodeURIComponent(currentPath.value)}&token=${encodeURIComponent(fstoken.value)}&dl=1`;
|
|
});
|
|
const breadcrumbs = computed(() => {
|
|
const crumbs = [{ label: "files", path: "" }];
|
|
if (!currentPath.value) return crumbs;
|
|
const parts = currentPath.value.split("/");
|
|
for (let i = 0; i < parts.length; i++) {
|
|
crumbs.push({ label: parts[i], path: parts.slice(0, i + 1).join("/") });
|
|
}
|
|
return crumbs;
|
|
});
|
|
return (_ctx, _push, _parent, _attrs) => {
|
|
_push(`<div${ssrRenderAttrs(mergeProps({ class: "viewer-layout h-full overflow-hidden" }, _attrs))} data-v-6b5cb038><main class="viewer-pane min-w-0 flex flex-col h-full overflow-hidden" data-v-6b5cb038>`);
|
|
if (unref(currentPath) || unref(fileType) === "dir") {
|
|
_push(`<div class="viewer-toolbar" data-v-6b5cb038><div class="viewer-panel breadcrumb-panel" data-v-6b5cb038><!--[-->`);
|
|
ssrRenderList(breadcrumbs.value, (crumb, i) => {
|
|
_push(`<span data-v-6b5cb038>`);
|
|
if (i > 0) {
|
|
_push(`<span class="breadcrumb-sep" data-v-6b5cb038>/</span>`);
|
|
} else {
|
|
_push(`<!---->`);
|
|
}
|
|
_push(`<span class="${ssrRenderClass([{ active: i === breadcrumbs.value.length - 1 }, "breadcrumb-item"])}" data-v-6b5cb038>${ssrInterpolate(crumb.label)}</span></span>`);
|
|
});
|
|
_push(`<!--]--></div><div class="viewer-toolbar-spacer" data-v-6b5cb038></div>`);
|
|
if (unref(fileType) === "md") {
|
|
_push(`<div class="viewer-panel toggle-panel" data-v-6b5cb038><button class="${ssrRenderClass([{ active: !unref(mdRaw) }, "md-toggle-btn"])}" data-v-6b5cb038>Rendered</button><button class="${ssrRenderClass([{ active: unref(mdRaw) }, "md-toggle-btn"])}" data-v-6b5cb038>Raw</button></div>`);
|
|
} else {
|
|
_push(`<!---->`);
|
|
}
|
|
if (unref(fileType) && unref(fileType) !== "dir") {
|
|
_push(`<a class="viewer-panel download-panel"${ssrRenderAttr("href", downloadUrl.value)}${ssrRenderAttr("download", fileName.value)} title="Download" data-v-6b5cb038>`);
|
|
_push(ssrRenderComponent(unref(ArrowDownTrayIcon), { class: "w-4 h-4" }, null, _parent));
|
|
_push(`</a>`);
|
|
} else {
|
|
_push(`<!---->`);
|
|
}
|
|
_push(`</div>`);
|
|
} else {
|
|
_push(`<!---->`);
|
|
}
|
|
_push(ssrRenderComponent(unref(OverlayScrollbarsComponent), {
|
|
class: "content flex-1 min-h-0",
|
|
options: unref(scrollbarOptions),
|
|
element: "div"
|
|
}, {
|
|
default: withCtx((_, _push2, _parent2, _scopeId) => {
|
|
if (_push2) {
|
|
if (!unref(currentPath) && unref(fileType) !== "dir") {
|
|
_push2(`<div class="viewer-empty" data-v-6b5cb038${_scopeId}>`);
|
|
_push2(ssrRenderComponent(unref(FolderOpenIcon), { class: "w-8 h-8 text-text-dim" }, null, _parent2, _scopeId));
|
|
_push2(`<p data-v-6b5cb038${_scopeId}>Select a file from the tree</p></div>`);
|
|
} else if (unref(showLoading)) {
|
|
_push2(`<div class="viewer-loading" data-v-6b5cb038${_scopeId}>loading…</div>`);
|
|
} else if (unref(fetchError)) {
|
|
_push2(`<div class="viewer-error" data-v-6b5cb038${_scopeId}>${ssrInterpolate(unref(fetchError))}</div>`);
|
|
} else if (unref(fileType) === "dir") {
|
|
_push2(`<div class="viewer-dir" data-v-6b5cb038${_scopeId}><!--[-->`);
|
|
ssrRenderList(unref(dirDirs), (d) => {
|
|
_push2(`<div class="dir-entry dir-entry--dir" data-v-6b5cb038${_scopeId}>`);
|
|
_push2(ssrRenderComponent(unref(FolderIcon), { class: "w-4 h-4" }, null, _parent2, _scopeId));
|
|
_push2(`<span data-v-6b5cb038${_scopeId}>${ssrInterpolate(d)}/</span></div>`);
|
|
});
|
|
_push2(`<!--]--><!--[-->`);
|
|
ssrRenderList(unref(dirFiles), (f) => {
|
|
_push2(`<div class="dir-entry dir-entry--file" data-v-6b5cb038${_scopeId}>`);
|
|
_push2(ssrRenderComponent(unref(DocumentIcon), { class: "w-4 h-4" }, null, _parent2, _scopeId));
|
|
_push2(`<span data-v-6b5cb038${_scopeId}>${ssrInterpolate(f.name)}</span></div>`);
|
|
});
|
|
_push2(`<!--]-->`);
|
|
if (!unref(dirDirs).length && !unref(dirFiles).length) {
|
|
_push2(`<div class="viewer-empty" data-v-6b5cb038${_scopeId}><p data-v-6b5cb038${_scopeId}>Empty directory</p></div>`);
|
|
} else {
|
|
_push2(`<!---->`);
|
|
}
|
|
_push2(`</div>`);
|
|
} else if (unref(fileType) === "pdf") {
|
|
_push2(`<iframe class="viewer-pdf"${ssrRenderAttr("src", unref(pdfSrc))} frameborder="0" data-v-6b5cb038${_scopeId}></iframe>`);
|
|
} else if (unref(fileType) === "md" && !unref(mdRaw)) {
|
|
_push2(`<div class="viewer-md md-body" data-v-6b5cb038${_scopeId}>${unref(renderedMd) ?? ""}</div>`);
|
|
} else if (unref(fileType) === "md" && unref(mdRaw)) {
|
|
_push2(`<div class="viewer-raw-md" data-v-6b5cb038${_scopeId}><pre class="viewer-raw-code" data-v-6b5cb038${_scopeId}><code data-v-6b5cb038${_scopeId}><!--[-->`);
|
|
ssrRenderList(contentLines.value, (line, i) => {
|
|
_push2(`<span class="code-line" data-v-6b5cb038${_scopeId}><span class="line-num" data-v-6b5cb038${_scopeId}>${ssrInterpolate(i + 1)}</span>${ssrInterpolate(line)}
|
|
</span>`);
|
|
});
|
|
_push2(`<!--]--></code></pre></div>`);
|
|
} else {
|
|
_push2(`<pre class="viewer-raw-code" data-v-6b5cb038${_scopeId}><code data-v-6b5cb038${_scopeId}><!--[-->`);
|
|
ssrRenderList(contentLines.value, (line, i) => {
|
|
_push2(`<span class="code-line" data-v-6b5cb038${_scopeId}><span class="line-num" data-v-6b5cb038${_scopeId}>${ssrInterpolate(i + 1)}</span>${ssrInterpolate(line)}
|
|
</span>`);
|
|
});
|
|
_push2(`<!--]--></code></pre>`);
|
|
}
|
|
} else {
|
|
return [
|
|
!unref(currentPath) && unref(fileType) !== "dir" ? (openBlock(), createBlock("div", {
|
|
key: 0,
|
|
class: "viewer-empty"
|
|
}, [
|
|
createVNode(unref(FolderOpenIcon), { class: "w-8 h-8 text-text-dim" }),
|
|
createVNode("p", null, "Select a file from the tree")
|
|
])) : unref(showLoading) ? (openBlock(), createBlock("div", {
|
|
key: 1,
|
|
class: "viewer-loading"
|
|
}, "loading…")) : unref(fetchError) ? (openBlock(), createBlock("div", {
|
|
key: 2,
|
|
class: "viewer-error"
|
|
}, toDisplayString(unref(fetchError)), 1)) : unref(fileType) === "dir" ? (openBlock(), createBlock("div", {
|
|
key: 3,
|
|
class: "viewer-dir"
|
|
}, [
|
|
(openBlock(true), createBlock(Fragment, null, renderList(unref(dirDirs), (d) => {
|
|
return openBlock(), createBlock("div", {
|
|
key: d,
|
|
class: "dir-entry dir-entry--dir",
|
|
onClick: ($event) => unref(openFile)(unref(currentPath) ? unref(currentPath) + "/" + d : d)
|
|
}, [
|
|
createVNode(unref(FolderIcon), { class: "w-4 h-4" }),
|
|
createVNode("span", null, toDisplayString(d) + "/", 1)
|
|
], 8, ["onClick"]);
|
|
}), 128)),
|
|
(openBlock(true), createBlock(Fragment, null, renderList(unref(dirFiles), (f) => {
|
|
return openBlock(), createBlock("div", {
|
|
key: f.path,
|
|
class: "dir-entry dir-entry--file",
|
|
onClick: ($event) => unref(openFile)(f.path)
|
|
}, [
|
|
createVNode(unref(DocumentIcon), { class: "w-4 h-4" }),
|
|
createVNode("span", null, toDisplayString(f.name), 1)
|
|
], 8, ["onClick"]);
|
|
}), 128)),
|
|
!unref(dirDirs).length && !unref(dirFiles).length ? (openBlock(), createBlock("div", {
|
|
key: 0,
|
|
class: "viewer-empty"
|
|
}, [
|
|
createVNode("p", null, "Empty directory")
|
|
])) : createCommentVNode("", true)
|
|
])) : unref(fileType) === "pdf" ? (openBlock(), createBlock("iframe", {
|
|
key: 4,
|
|
class: "viewer-pdf",
|
|
src: unref(pdfSrc),
|
|
frameborder: "0"
|
|
}, null, 8, ["src"])) : unref(fileType) === "md" && !unref(mdRaw) ? (openBlock(), createBlock("div", {
|
|
key: 5,
|
|
class: "viewer-md md-body",
|
|
innerHTML: unref(renderedMd),
|
|
onCopy: unref(onCopy)
|
|
}, null, 40, ["innerHTML", "onCopy"])) : unref(fileType) === "md" && unref(mdRaw) ? (openBlock(), createBlock("div", {
|
|
key: 6,
|
|
class: "viewer-raw-md"
|
|
}, [
|
|
createVNode("pre", { class: "viewer-raw-code" }, [
|
|
createVNode("code", null, [
|
|
(openBlock(true), createBlock(Fragment, null, renderList(contentLines.value, (line, i) => {
|
|
return openBlock(), createBlock("span", {
|
|
key: i,
|
|
class: "code-line"
|
|
}, [
|
|
createVNode("span", { class: "line-num" }, toDisplayString(i + 1), 1),
|
|
createTextVNode(toDisplayString(line) + "\n", 1)
|
|
]);
|
|
}), 128))
|
|
])
|
|
])
|
|
])) : (openBlock(), createBlock("pre", {
|
|
key: 7,
|
|
class: "viewer-raw-code"
|
|
}, [
|
|
createVNode("code", null, [
|
|
(openBlock(true), createBlock(Fragment, null, renderList(contentLines.value, (line, i) => {
|
|
return openBlock(), createBlock("span", {
|
|
key: i,
|
|
class: "code-line"
|
|
}, [
|
|
createVNode("span", { class: "line-num" }, toDisplayString(i + 1), 1),
|
|
createTextVNode(toDisplayString(line) + "\n", 1)
|
|
]);
|
|
}), 128))
|
|
])
|
|
]))
|
|
];
|
|
}
|
|
}),
|
|
_: 1
|
|
}, _parent));
|
|
_push(`</main></div>`);
|
|
};
|
|
}
|
|
});
|
|
const _sfc_setup = _sfc_main.setup;
|
|
_sfc_main.setup = (props, ctx) => {
|
|
const ssrContext = useSSRContext();
|
|
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("src/views/ViewerView.vue");
|
|
return _sfc_setup ? _sfc_setup(props, ctx) : void 0;
|
|
};
|
|
const ViewerView = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-6b5cb038"]]);
|
|
export {
|
|
ViewerView as default
|
|
};
|