feat: Introduce a dedicated popout page for standalone file viewing, including content rendering and actions.

This commit is contained in:
Bruno Charest 2026-03-24 10:17:41 -04:00
parent 46e054f5dd
commit f963c37012

View File

@ -14,57 +14,8 @@
padding: 0;
height: 100%;
overflow: hidden;
background: #0d1117; /* GitHub-like dark background */
color: #e6edf3;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
background: var(--bg-primary);
}
.popout-container {
display: flex;
flex-direction: column;
height: 100vh;
border: 1px solid #30363d;
box-sizing: border-box;
}
.popout-header {
padding: 12px 20px;
background: #161b22;
border-bottom: 1px solid #30363d;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.popout-title {
font-size: 0.9rem;
font-weight: 600;
color: #7d8590;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.popout-content {
flex: 1;
padding: 3.5rem 2rem;
overflow-y: auto;
}
.main-wrapper {
max-width: 840px;
margin: 0 auto;
padding: 2.5rem;
background: #161b22;
border: 1px solid #30363d;
border-radius: 12px;
box-shadow: 0 12px 48px rgba(0,0,0,0.3);
}
.markdown-body {
line-height: 1.7;
color: #e6edf3;
}
/* Hide default scrollbar but keep functionality */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #30363d; border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: #484f58; }
.popout-loading {
position: absolute;
@ -72,7 +23,8 @@
display: flex;
align-items: center;
justify-content: center;
background: #0d1117;
background: var(--bg-primary);
color: var(--text-primary);
z-index: 100;
transition: opacity 0.3s;
}
@ -81,22 +33,15 @@
<body class="popup-mode">
<div class="popout-loading" id="loading">Chargement...</div>
<div class="popout-container">
<div class="popout-header">
<div class="popout-title" id="file-title">ObsiGate</div>
<div style="font-size: 0.75rem; color: #7d8590;" id="vault-name"></div>
</div>
<div class="popout-content">
<div class="main-wrapper">
<div id="content-area" class="markdown-body"></div>
</div>
</div>
<div class="main-layout" style="height: 100vh;">
<main class="content-area" id="content-area" style="margin-top: 15px;">
<!-- JavaScript will inject breadcrumb, title, and markdown content here -->
</main>
</div>
<script>
async function initPopout() {
const pathParts = window.location.pathname.split('/');
// Expected: /popout/{vault}/{path...}
const vault = decodeURIComponent(pathParts[2]);
const path = decodeURIComponent(pathParts.slice(3).join('/'));
@ -105,19 +50,123 @@
return;
}
document.getElementById('vault-name').textContent = vault;
document.getElementById('file-title').textContent = path.split('/').pop();
try {
const response = await fetch(`/api/file/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`, {
credentials: 'include'
});
if (!response.ok) throw new Error("Erreur lors du chargement du fichier");
if (!response.ok) throw new Error("Erreur lors du chargement (Essayez de vous reconnecter sur le site principal)");
const data = await response.json();
document.getElementById('content-area').innerHTML = data.html;
document.title = data.title + " - ObsiGate";
const area = document.getElementById('content-area');
area.innerHTML = "";
// Build breadcrumb (identical structure to app.js)
const parts = path.split("/");
const breadcrumb = document.createElement("div");
breadcrumb.className = "breadcrumb";
breadcrumb.appendChild(createBreadcrumbSep());
breadcrumb.appendChild(createBreadcrumbItem(vault));
parts.forEach((part, i) => {
breadcrumb.appendChild(createBreadcrumbSep());
const name = i === parts.length - 1 ? part.replace(/\.md$/i, "") : part;
breadcrumb.appendChild(createBreadcrumbItem(name));
});
area.appendChild(breadcrumb);
// Build file header
const header = document.createElement("div");
header.className = "file-header";
const titleDiv = document.createElement("div");
titleDiv.className = "file-title";
titleDiv.textContent = data.title;
header.appendChild(titleDiv);
// Tags
if (data.tags && data.tags.length > 0) {
const tagsDiv = document.createElement("div");
tagsDiv.className = "file-tags";
data.tags.forEach(t => {
const span = document.createElement("span");
span.className = "file-tag";
span.textContent = "#" + t;
tagsDiv.appendChild(span);
});
header.appendChild(tagsDiv);
}
// Action buttons
const actionsDiv = document.createElement("div");
actionsDiv.className = "file-actions";
const copyBtn = document.createElement("button");
copyBtn.className = "btn-action";
copyBtn.title = "Copier la source";
copyBtn.innerHTML = '<i data-lucide="copy" style="width:14px;height:14px"></i> Copier';
copyBtn.addEventListener("click", async () => {
try {
const rawUrl = `/api/file/${encodeURIComponent(vault)}/raw?path=${encodeURIComponent(path)}`;
const rawData = await (await fetch(rawUrl, {credentials: 'include'})).json();
await navigator.clipboard.writeText(rawData.raw);
copyBtn.textContent = "Copié !";
setTimeout(() => (copyBtn.innerHTML = '<i data-lucide="copy" style="width:14px;height:14px"></i> Copier'), 1500);
if (window.lucide) window.lucide.createIcons();
} catch (e) {}
});
actionsDiv.appendChild(copyBtn);
const dlBtn = document.createElement("button");
dlBtn.className = "btn-action";
dlBtn.title = "Télécharger";
dlBtn.innerHTML = '<i data-lucide="download" style="width:14px;height:14px"></i> Télécharger';
dlBtn.addEventListener("click", () => {
const a = document.createElement("a");
a.href = `/api/file/${encodeURIComponent(vault)}/download?path=${encodeURIComponent(path)}`;
a.download = path.split('/').pop();
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
actionsDiv.appendChild(dlBtn);
header.appendChild(actionsDiv);
area.appendChild(header);
// Frontmatter
if (data.frontmatter && Object.keys(data.frontmatter).length > 0) {
const fmSection = document.createElement("div");
const fmToggle = document.createElement("div");
fmToggle.className = "frontmatter-toggle";
fmToggle.textContent = "▶ Frontmatter";
const fmContent = document.createElement("div");
fmContent.className = "frontmatter-content";
fmContent.textContent = JSON.stringify(data.frontmatter, null, 2);
fmToggle.addEventListener("click", () => {
fmContent.classList.toggle("open");
fmToggle.textContent = fmContent.classList.contains("open") ? "▼ Frontmatter" : "▶ Frontmatter";
});
fmSection.appendChild(fmToggle);
fmSection.appendChild(fmContent);
area.appendChild(fmSection);
}
// Markdown content wrapper
const mdDiv = document.createElement("div");
mdDiv.className = "md-content";
mdDiv.id = "file-rendered-content";
mdDiv.innerHTML = data.html;
area.appendChild(mdDiv);
// Render lucide icons
if (window.lucide) {
window.lucide.createIcons();
}
// Highlight code blocks
document.querySelectorAll('pre code').forEach((el) => {
hljs.highlightElement(el);
@ -134,6 +183,20 @@
}
}
function createBreadcrumbSep() {
const sep = document.createElement("span");
sep.className = "sep";
sep.textContent = " / ";
return sep;
}
function createBreadcrumbItem(text) {
const item = document.createElement("span");
item.className = "breadcrumb-item";
item.textContent = text;
return item;
}
window.onload = initPopout;
</script>
</body>