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:
+351
-15
@@ -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()]);
|
||||
|
||||
Reference in New Issue
Block a user