feat: Introduce a dedicated popout page for standalone file viewing, including content rendering and actions.
This commit is contained in:
parent
46e054f5dd
commit
f963c37012
@ -14,57 +14,8 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #0d1117; /* GitHub-like dark background */
|
background: var(--bg-primary);
|
||||||
color: #e6edf3;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
||||||
}
|
}
|
||||||
.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 {
|
.popout-loading {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -72,7 +23,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: #0d1117;
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
transition: opacity 0.3s;
|
transition: opacity 0.3s;
|
||||||
}
|
}
|
||||||
@ -81,22 +33,15 @@
|
|||||||
<body class="popup-mode">
|
<body class="popup-mode">
|
||||||
<div class="popout-loading" id="loading">Chargement...</div>
|
<div class="popout-loading" id="loading">Chargement...</div>
|
||||||
|
|
||||||
<div class="popout-container">
|
<div class="main-layout" style="height: 100vh;">
|
||||||
<div class="popout-header">
|
<main class="content-area" id="content-area" style="margin-top: 15px;">
|
||||||
<div class="popout-title" id="file-title">ObsiGate</div>
|
<!-- JavaScript will inject breadcrumb, title, and markdown content here -->
|
||||||
<div style="font-size: 0.75rem; color: #7d8590;" id="vault-name"></div>
|
</main>
|
||||||
</div>
|
|
||||||
<div class="popout-content">
|
|
||||||
<div class="main-wrapper">
|
|
||||||
<div id="content-area" class="markdown-body"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
async function initPopout() {
|
async function initPopout() {
|
||||||
const pathParts = window.location.pathname.split('/');
|
const pathParts = window.location.pathname.split('/');
|
||||||
// Expected: /popout/{vault}/{path...}
|
|
||||||
const vault = decodeURIComponent(pathParts[2]);
|
const vault = decodeURIComponent(pathParts[2]);
|
||||||
const path = decodeURIComponent(pathParts.slice(3).join('/'));
|
const path = decodeURIComponent(pathParts.slice(3).join('/'));
|
||||||
|
|
||||||
@ -105,19 +50,123 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('vault-name').textContent = vault;
|
|
||||||
document.getElementById('file-title').textContent = path.split('/').pop();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/file/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`, {
|
const response = await fetch(`/api/file/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`, {
|
||||||
credentials: 'include'
|
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();
|
const data = await response.json();
|
||||||
document.getElementById('content-area').innerHTML = data.html;
|
|
||||||
document.title = data.title + " - ObsiGate";
|
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
|
// Highlight code blocks
|
||||||
document.querySelectorAll('pre code').forEach((el) => {
|
document.querySelectorAll('pre code').forEach((el) => {
|
||||||
hljs.highlightElement(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;
|
window.onload = initPopout;
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user