selfhosted-overleaf-project.../public/overleaf.html
2025-06-09 00:14:39 +02:00

335 lines
8.5 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Project Viewer</title>
<link rel="stylesheet" href="/prism.css" />
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: sans-serif;
height: 100vh;
display: flex;
flex-direction: column;
background: #1e1e1e;
color: white;
user-select: none;
overflow: hidden;
}
header {
height: 40px;
background: #1b222c;
padding: 0 1rem;
display: flex;
align-items: center;
text-align: center;
border-bottom: 1px solid #444;
font-weight: bold;
flex-shrink: 0;
}
main {
flex: 1;
display: flex;
overflow: hidden;
position: relative;
}
.sidebar {
width: 200px;
background: #252525;
border-right: 1px solid #444;
overflow-y: auto;
flex-shrink: 0;
}
.sidebar ul {
list-style: none;
}
.sidebar li {
padding: 0.75rem 1rem;
cursor: pointer;
border-bottom: 1px solid #333;
display: flex;
align-items: center;
transition: background-color 0.2s ease;
}
.sidebar li:hover {
background: #3a3a3a;
}
.sidebar li.active {
background: #4a90e2;
color: white;
}
.editor {
flex: 2;
background: #1e1e1e;
overflow: auto;
padding: 1rem;
font-size: 14px;
min-width: 200px;
user-select: text;
}
.editor pre {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
}
.preview-container {
flex: 1;
background: #2d2d2d;
position: relative;
display: flex;
flex-direction: column;
min-width: 200px;
}
.preview {
flex: 1;
padding: 0.5rem;
overflow: hidden;
position: relative;
}
.preview iframe {
width: 100%;
height: 100%;
border: none;
background: white;
}
.pdf-error {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #f33;
font-weight: bold;
background: #330000cc;
padding: 1rem 2rem;
border-radius: 6px;
display: none;
z-index: 10;
user-select: none;
}
footer {
height: 30px;
background: #222;
font-size: 12px;
text-align: center;
padding-top: 5px;
color: #777;
flex-shrink: 0;
}
/* Draggable separator */
#dragbar {
width: 10px;
cursor: ew-resize;
background: #444;
flex-shrink: 0;
transition: background-color 0.2s ease;
user-select: none;
}
#dragbar:hover {
background: #666;
}
#open-in-editor-button {
margin-left: auto;
color: white;
background-color: #098842;
border-radius: 100px;
padding: 0.25rem 0.5rem;
text-decoration: none;
font-weight: bold;
font-size: 14px;
cursor: pointer;
}
</style>
</head>
<body>
<header>Project Viewer (Read-only)<a id="open-in-editor-button">Open in Editor</a></header>
<main>
<nav class="sidebar">
<ul id="file-list"></ul>
</nav>
<div class="editor">
<pre class="line-numbers"><code id="code" class="language-latex">// Loading...</code></pre>
</div>
<div id="dragbar"></div>
<section class="preview-container">
<div class="preview">
<iframe id="pdf" title="PDF preview"></iframe>
<div id="pdf-error" class="pdf-error">PDF failed to load or was not compiled.</div>
</div>
</section>
</main>
<footer>Static viewer for LaTeX projects</footer>
<script src="/prism.js"></script>
<script>
const projectId = window.location.pathname.split("/")[2];
const base = `/projects/${projectId}`;
const fileListEl = document.getElementById("file-list");
const codeEl = document.getElementById("code");
const pdfEl = document.getElementById("pdf");
const pdfErrorEl = document.getElementById("pdf-error");
const sidebarEl = document.querySelector(".sidebar");
const editorEl = document.querySelector(".editor");
const previewContainerEl = document.querySelector(".preview-container");
const dragbar = document.getElementById("dragbar");
const mainEl = document.querySelector("main");
document.getElementById("open-in-editor-button").onclick = () => {
window.open(`https://tex.jonasjones.dev/project/${projectId}`, "_blank");
};
async function listFiles() {
try {
const res = await fetch(`${base}/`);
if (!res.ok) throw new Error("Failed to fetch file list");
const text = await res.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, "text/html");
const links = Array.from(doc.querySelectorAll("a"));
const sourceFiles = [];
let pdfFiles = [];
for (const link of links) {
const name = link.getAttribute("href");
if (!name) continue;
if (name === '../' || name.endsWith('/')) continue;
if (/\.(tex|bib|cls|sty|txt)$/i.test(name)) {
sourceFiles.push(name);
} else if (name.toLowerCase().endsWith(".pdf")) {
pdfFiles.push(name);
}
}
if (sourceFiles.length === 0) {
codeEl.textContent = "// No source files found.";
}
// Add files to sidebar WITHOUT numbering
sourceFiles.forEach((file, idx) => {
const li = document.createElement("li");
li.textContent = file;
if (idx === 0) li.classList.add("active");
li.onclick = () => {
document.querySelectorAll(".sidebar li").forEach(el => el.classList.remove("active"));
li.classList.add("active");
loadSourceFile(file);
};
fileListEl.appendChild(li);
});
if (sourceFiles.length > 0) {
loadSourceFile(sourceFiles[0]);
}
// PDF fallback: output.pdf > main.pdf > any .pdf
let pdfToLoad = pdfFiles.find(f => f.toLowerCase() === "output.pdf")
|| pdfFiles.find(f => f.toLowerCase() === "main.pdf")
|| pdfFiles[0];
if (pdfToLoad) {
loadPDF(pdfToLoad);
} else {
showPDFError(true);
}
} catch (err) {
codeEl.textContent = `// Error loading file list: ${err.message}`;
showPDFError(true);
}
}
async function loadSourceFile(file) {
try {
const res = await fetch(`${base}/${file}`);
if (!res.ok) throw new Error("Failed to load file");
const content = await res.text();
codeEl.textContent = content;
// Change prism language class based on file extension
const ext = file.split('.').pop().toLowerCase();
codeEl.className = `language-${ext === "tex" ? "latex" : ext}`;
Prism.highlightElement(codeEl);
} catch (err) {
codeEl.textContent = `// Error loading file: ${err.message}`;
}
}
function loadPDF(pdfFile) {
showPDFError(false);
pdfEl.src = `${base}/${pdfFile}`;
}
function showPDFError(show) {
pdfErrorEl.style.display = show ? "block" : "none";
if (show) {
pdfEl.style.display = "none";
} else {
pdfEl.style.display = "block";
}
}
pdfEl.onerror = () => {
showPDFError(true);
};
pdfEl.onload = () => {
showPDFError(false);
};
// Draggable vertical separator
let isDragging = false;
dragbar.addEventListener("mousedown", e => {
isDragging = true;
document.body.style.cursor = "ew-resize";
e.preventDefault();
});
window.addEventListener("mouseup", e => {
if (isDragging) {
isDragging = false;
document.body.style.cursor = "";
}
});
window.addEventListener("mousemove", e => {
if (!isDragging) return;
const containerRect = mainEl.getBoundingClientRect();
const sidebarWidth = sidebarEl.offsetWidth;
const availableWidth = containerRect.width - sidebarWidth - dragbar.offsetWidth;
let newEditorWidth = e.clientX - containerRect.left - sidebarWidth;
newEditorWidth = Math.max(200, Math.min(newEditorWidth, availableWidth - 200));
editorEl.style.flex = "none";
previewContainerEl.style.flex = "none";
editorEl.style.width = newEditorWidth + "px";
previewContainerEl.style.width = (availableWidth - newEditorWidth) + "px";
});
listFiles();
</script>
</body>
</html>