Expand file type support beyond Markdown to include code, config, and build files with syntax highlighting and improved UI filtering

This commit is contained in:
2026-03-21 11:16:46 -04:00
parent 8a22c1db28
commit 2ed5f65a7a
5 changed files with 738 additions and 70 deletions
+351 -15
View File
@@ -9,6 +9,60 @@
let currentVault = null;
let currentPath = null;
let searchTimeout = null;
let showingSource = false;
let cachedRawSource = null;
let allVaults = [];
let selectedContextVault = "all";
// ---------------------------------------------------------------------------
// File extension → Lucide icon mapping
// ---------------------------------------------------------------------------
const EXT_ICONS = {
".md": "file-text",
".txt": "file-text",
".log": "file-text",
".py": "file-code",
".js": "file-code",
".ts": "file-code",
".jsx": "file-code",
".tsx": "file-code",
".html": "file-code",
".css": "file-code",
".scss": "file-code",
".less": "file-code",
".json": "file-json",
".yaml": "file-cog",
".yml": "file-cog",
".toml": "file-cog",
".xml": "file-code",
".sh": "terminal",
".bash": "terminal",
".zsh": "terminal",
".bat": "terminal",
".cmd": "terminal",
".ps1": "terminal",
".java": "file-code",
".c": "file-code",
".cpp": "file-code",
".h": "file-code",
".hpp": "file-code",
".cs": "file-code",
".go": "file-code",
".rs": "file-code",
".rb": "file-code",
".php": "file-code",
".sql": "database",
".csv": "table",
".ini": "file-cog",
".cfg": "file-cog",
".conf": "file-cog",
".env": "file-cog",
};
function getFileIcon(name) {
const ext = "." + name.split(".").pop().toLowerCase();
return EXT_ICONS[ext] || "file";
}
// ---------------------------------------------------------------------------
// Safe CDN helpers
@@ -66,11 +120,84 @@
return res.json();
}
// ---------------------------------------------------------------------------
// Mobile sidebar
// ---------------------------------------------------------------------------
function initMobile() {
const hamburger = document.getElementById("hamburger-btn");
const overlay = document.getElementById("sidebar-overlay");
const sidebar = document.getElementById("sidebar");
hamburger.addEventListener("click", () => {
sidebar.classList.toggle("mobile-open");
overlay.classList.toggle("active");
});
overlay.addEventListener("click", () => {
sidebar.classList.remove("mobile-open");
overlay.classList.remove("active");
});
}
function closeMobileSidebar() {
const sidebar = document.getElementById("sidebar");
const overlay = document.getElementById("sidebar-overlay");
if (sidebar) sidebar.classList.remove("mobile-open");
if (overlay) overlay.classList.remove("active");
}
// ---------------------------------------------------------------------------
// Vault context switching
// ---------------------------------------------------------------------------
function initVaultContext() {
const filter = document.getElementById("vault-filter");
filter.addEventListener("change", async () => {
selectedContextVault = filter.value;
showingSource = false;
cachedRawSource = null;
await refreshSidebarForContext();
await refreshTagsForContext();
showWelcome();
});
}
async function refreshSidebarForContext() {
const container = document.getElementById("vault-tree");
container.innerHTML = "";
const vaultsToShow = selectedContextVault === "all"
? allVaults
: allVaults.filter((v) => v.name === selectedContextVault);
vaultsToShow.forEach((v) => {
const vaultItem = el("div", { class: "tree-item vault-item", "data-vault": v.name }, [
icon("chevron-right", 14),
icon("database", 16),
document.createTextNode(` ${v.name} `),
smallBadge(v.file_count),
]);
vaultItem.addEventListener("click", () => toggleVault(vaultItem, v.name));
container.appendChild(vaultItem);
const childContainer = el("div", { class: "tree-children collapsed", id: `vault-children-${v.name}` });
container.appendChild(childContainer);
});
safeCreateIcons();
}
async function refreshTagsForContext() {
const vaultParam = selectedContextVault === "all" ? "" : `?vault=${encodeURIComponent(selectedContextVault)}`;
const data = await api(`/api/tags${vaultParam}`);
renderTagCloud(data.tags);
}
// ---------------------------------------------------------------------------
// Sidebar — Vault tree
// ---------------------------------------------------------------------------
async function loadVaults() {
const vaults = await api("/api/vaults");
allVaults = vaults;
const container = document.getElementById("vault-tree");
const filter = document.getElementById("vault-filter");
container.innerHTML = "";
@@ -156,11 +283,16 @@
}
});
} else {
const fileIconName = getFileIcon(item.name);
const displayName = item.name.match(/\.md$/i) ? item.name.replace(/\.md$/i, "") : item.name;
const fileItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [
icon("file-text", 16),
document.createTextNode(` ${item.name.replace(/\.md$/i, "")}`),
icon(fileIconName, 16),
document.createTextNode(` ${displayName}`),
]);
fileItem.addEventListener("click", () => openFile(vaultName, item.path));
fileItem.addEventListener("click", () => {
openFile(vaultName, item.path);
closeMobileSidebar();
});
container.appendChild(fileItem);
}
});
@@ -168,15 +300,74 @@
safeCreateIcons();
}
// ---------------------------------------------------------------------------
// Sidebar filter
// ---------------------------------------------------------------------------
function initSidebarFilter() {
const input = document.getElementById("sidebar-filter-input");
input.addEventListener("input", () => {
const q = input.value.trim().toLowerCase();
filterSidebarTree(q);
filterTagCloud(q);
});
}
function filterSidebarTree(query) {
const tree = document.getElementById("vault-tree");
const items = tree.querySelectorAll(".tree-item");
if (!query) {
items.forEach((item) => item.classList.remove("filtered-out"));
tree.querySelectorAll(".tree-children").forEach((c) => c.classList.remove("filtered-out"));
return;
}
// First pass: mark all as filtered out
items.forEach((item) => item.classList.add("filtered-out"));
tree.querySelectorAll(".tree-children").forEach((c) => c.classList.add("filtered-out"));
// Second pass: show matching items and their ancestors
items.forEach((item) => {
const text = item.textContent.toLowerCase();
if (text.includes(query)) {
item.classList.remove("filtered-out");
// Show all ancestor containers
let parent = item.parentElement;
while (parent && parent !== tree) {
parent.classList.remove("filtered-out");
if (parent.classList.contains("tree-children")) {
parent.classList.remove("collapsed");
}
parent = parent.parentElement;
}
}
});
}
function filterTagCloud(query) {
const tags = document.querySelectorAll("#tag-cloud .tag-item");
tags.forEach((tag) => {
const text = tag.textContent.toLowerCase();
if (!query || text.includes(query)) {
tag.classList.remove("filtered-out");
} else {
tag.classList.add("filtered-out");
}
});
}
// ---------------------------------------------------------------------------
// Tags
// ---------------------------------------------------------------------------
async function loadTags() {
const data = await api("/api/tags");
renderTagCloud(data.tags);
}
function renderTagCloud(tags) {
const cloud = document.getElementById("tag-cloud");
cloud.innerHTML = "";
const tags = data.tags;
const counts = Object.values(tags);
if (counts.length === 0) return;
@@ -198,7 +389,8 @@
function searchByTag(tag) {
const input = document.getElementById("search-input");
input.value = "";
performSearch("", "all", tag);
const vault = document.getElementById("vault-filter").value;
performSearch("", vault, tag);
}
// ---------------------------------------------------------------------------
@@ -207,12 +399,16 @@
async function openFile(vaultName, filePath) {
currentVault = vaultName;
currentPath = filePath;
showingSource = false;
cachedRawSource = null;
// Highlight active
document.querySelectorAll(".tree-item.active").forEach((el) => el.classList.remove("active"));
const selector = `.tree-item[data-vault="${vaultName}"][data-path="${filePath}"]`;
const active = document.querySelector(selector);
if (active) active.classList.add("active");
const selector = `.tree-item[data-vault="${vaultName}"][data-path="${CSS.escape(filePath)}"]`;
try {
const active = document.querySelector(selector);
if (active) active.classList.add("active");
} catch (e) { /* selector might fail on special chars */ }
const url = `/api/file/${encodeURIComponent(vaultName)}?path=${encodeURIComponent(filePath)}`;
const data = await api(url);
@@ -248,15 +444,38 @@
tagsDiv.appendChild(t);
});
// Copy path button
const copyBtn = el("button", { class: "btn-copy-path" }, [document.createTextNode("Copier le chemin")]);
// Action buttons
const copyBtn = el("button", { class: "btn-action", title: "Copier le chemin" }, [
icon("copy", 14),
document.createTextNode("Copier"),
]);
copyBtn.addEventListener("click", () => {
navigator.clipboard.writeText(`${data.vault}/${data.path}`).then(() => {
copyBtn.textContent = "Copié !";
setTimeout(() => (copyBtn.textContent = "Copier le chemin"), 1500);
copyBtn.querySelector("span") || (copyBtn.lastChild.textContent = "Copié !");
copyBtn.lastChild.textContent = "Copié !";
setTimeout(() => (copyBtn.lastChild.textContent = "Copier"), 1500);
});
});
const sourceBtn = el("button", { class: "btn-action", title: "Voir la source" }, [
icon("code", 14),
document.createTextNode("Source"),
]);
const downloadBtn = el("button", { class: "btn-action", title: "Télécharger" }, [
icon("download", 14),
document.createTextNode("Télécharger"),
]);
downloadBtn.addEventListener("click", () => {
const dlUrl = `/api/file/${encodeURIComponent(data.vault)}/download?path=${encodeURIComponent(data.path)}`;
const a = document.createElement("a");
a.href = dlUrl;
a.download = data.path.split("/").pop();
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
// Frontmatter
let fmSection = null;
if (data.frontmatter && Object.keys(data.frontmatter).length > 0) {
@@ -273,20 +492,48 @@
fmSection = el("div", {}, [fmToggle, fmContent]);
}
// Markdown content
const mdDiv = el("div", { class: "md-content" });
// Content container (rendered HTML)
const mdDiv = el("div", { class: "md-content", id: "file-rendered-content" });
mdDiv.innerHTML = data.html;
// Raw source container (hidden initially)
const rawDiv = el("div", { class: "raw-source-view", id: "file-raw-content", style: "display:none" });
// Source button toggle logic
sourceBtn.addEventListener("click", async () => {
const rendered = document.getElementById("file-rendered-content");
const raw = document.getElementById("file-raw-content");
if (!rendered || !raw) return;
showingSource = !showingSource;
if (showingSource) {
sourceBtn.classList.add("active");
if (!cachedRawSource) {
const rawUrl = `/api/file/${encodeURIComponent(data.vault)}/raw?path=${encodeURIComponent(data.path)}`;
const rawData = await api(rawUrl);
cachedRawSource = rawData.raw;
}
raw.textContent = cachedRawSource;
rendered.style.display = "none";
raw.style.display = "block";
} else {
sourceBtn.classList.remove("active");
rendered.style.display = "block";
raw.style.display = "none";
}
});
// Assemble
area.innerHTML = "";
area.appendChild(breadcrumb);
area.appendChild(el("div", { class: "file-header" }, [
el("div", { class: "file-title" }, [document.createTextNode(data.title)]),
tagsDiv,
el("div", { class: "file-actions" }, [copyBtn]),
el("div", { class: "file-actions" }, [copyBtn, sourceBtn, downloadBtn]),
]));
if (fmSection) area.appendChild(fmSection);
area.appendChild(mdDiv);
area.appendChild(rawDiv);
// Highlight code blocks
area.querySelectorAll("pre code").forEach((block) => {
@@ -303,6 +550,7 @@
});
});
safeCreateIcons();
area.scrollTop = 0;
}
@@ -375,6 +623,89 @@
area.appendChild(container);
}
// ---------------------------------------------------------------------------
// Resizable sidebar (horizontal)
// ---------------------------------------------------------------------------
function initSidebarResize() {
const handle = document.getElementById("sidebar-resize-handle");
const sidebar = document.getElementById("sidebar");
if (!handle || !sidebar) return;
// Restore saved width
const savedWidth = localStorage.getItem("obsigate-sidebar-width");
if (savedWidth) {
sidebar.style.width = savedWidth + "px";
}
let startX = 0;
let startWidth = 0;
function onMouseMove(e) {
const newWidth = Math.min(500, Math.max(200, startWidth + (e.clientX - startX)));
sidebar.style.width = newWidth + "px";
}
function onMouseUp() {
document.body.classList.remove("resizing");
handle.classList.remove("active");
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
localStorage.setItem("obsigate-sidebar-width", parseInt(sidebar.style.width));
}
handle.addEventListener("mousedown", (e) => {
e.preventDefault();
startX = e.clientX;
startWidth = sidebar.getBoundingClientRect().width;
document.body.classList.add("resizing");
handle.classList.add("active");
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
});
}
// ---------------------------------------------------------------------------
// Resizable tag section (vertical)
// ---------------------------------------------------------------------------
function initTagResize() {
const handle = document.getElementById("tag-resize-handle");
const tagSection = document.getElementById("tag-cloud-section");
if (!handle || !tagSection) return;
// Restore saved height
const savedHeight = localStorage.getItem("obsigate-tag-height");
if (savedHeight) {
tagSection.style.height = savedHeight + "px";
}
let startY = 0;
let startHeight = 0;
function onMouseMove(e) {
// Dragging up increases height, dragging down decreases
const newHeight = Math.min(400, Math.max(60, startHeight - (e.clientY - startY)));
tagSection.style.height = newHeight + "px";
}
function onMouseUp() {
document.body.classList.remove("resizing-v");
handle.classList.remove("active");
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
localStorage.setItem("obsigate-tag-height", parseInt(tagSection.style.height));
}
handle.addEventListener("mousedown", (e) => {
e.preventDefault();
startY = e.clientY;
startHeight = tagSection.getBoundingClientRect().height;
document.body.classList.add("resizing-v");
handle.classList.add("active");
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
});
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
@@ -430,6 +761,11 @@
initTheme();
document.getElementById("theme-toggle").addEventListener("click", toggleTheme);
initSearch();
initMobile();
initVaultContext();
initSidebarFilter();
initSidebarResize();
initTagResize();
try {
await Promise.all([loadVaults(), loadTags()]);