352 lines
12 KiB
HTML
352 lines
12 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Org Web Adapter</title>
|
|
<link rel="stylesheet" href="/static/style.css">
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>Org Web Adapter</h1>
|
|
<button id="theme-toggle" type="button" aria-label="Toggle dark mode">Dark mode</button>
|
|
</header>
|
|
<div class="layout">
|
|
<nav>
|
|
<label class="search-wrap" for="title-search">
|
|
<span class="search-label">Search titles</span>
|
|
<input id="title-search" class="search-input" type="search" placeholder="Type to filter files">
|
|
</label>
|
|
<div class="nav-toolbar-header">
|
|
<button id="toggle-nav-toolbar" class="toggle-toolbar-btn" type="button" aria-label="Toggle filter and sort controls" title="Toggle filter and sort controls">▼ Filters</button>
|
|
</div>
|
|
<div id="nav-toolbar" class="nav-toolbar">
|
|
<div class="nav-actions">
|
|
<button id="shuffle-notes" class="shuffle-btn" type="button">Shuffle notes</button>
|
|
<button id="sort-backlinks" class="shuffle-btn" type="button">Sort by backlinks</button>
|
|
<button id="sort-created" class="shuffle-btn" type="button">Sort by created date</button>
|
|
<button id="sort-modified" class="shuffle-btn" type="button">Sort by modified date</button>
|
|
<button id="jump-current" class="shuffle-btn" type="button">Jump to current note</button>
|
|
</div>
|
|
</div>
|
|
<div id="file-list">{{NAV_ITEMS}}</div>
|
|
</nav>
|
|
<main>{{MAIN_CONTENT}}</main>
|
|
<aside class="backlinks-pane">
|
|
<h3>Backlinks</h3>
|
|
<div class="backlinks-list">{{BACKLINKS}}</div>
|
|
</aside>
|
|
</div>
|
|
<script>
|
|
window.MathJax = {
|
|
tex: {
|
|
inlineMath: [["$", "$"]],
|
|
displayMath: []
|
|
},
|
|
options: {
|
|
skipHtmlTags: ["script", "noscript", "style", "textarea", "pre", "code"]
|
|
}
|
|
};
|
|
</script>
|
|
<script async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"></script>
|
|
<script>
|
|
(function () {
|
|
const key = "org_web_viewer_theme";
|
|
const body = document.body;
|
|
const btn = document.getElementById("theme-toggle");
|
|
|
|
function applyTheme(theme) {
|
|
if (theme === "dark") {
|
|
body.setAttribute("data-theme", "dark");
|
|
btn.textContent = "Light mode";
|
|
} else {
|
|
body.removeAttribute("data-theme");
|
|
btn.textContent = "Dark mode";
|
|
}
|
|
}
|
|
|
|
const urlTheme = new URLSearchParams(window.location.search).get("theme");
|
|
const saved = urlTheme || localStorage.getItem(key);
|
|
applyTheme(saved === "dark" ? "dark" : "light");
|
|
|
|
btn.addEventListener("click", function () {
|
|
const next = body.getAttribute("data-theme") === "dark" ? "light" : "dark";
|
|
localStorage.setItem(key, next);
|
|
applyTheme(next);
|
|
});
|
|
|
|
var navToolbar = document.getElementById("nav-toolbar");
|
|
var toggleBtn = document.getElementById("toggle-nav-toolbar");
|
|
|
|
function getCookie(name) {
|
|
var match = document.cookie.match(new RegExp("(?:^|;\\s*)" + name + "=([^;]*)"));
|
|
return match ? decodeURIComponent(match[1]) : null;
|
|
}
|
|
|
|
function setCookie(name, value, days) {
|
|
var expires = "";
|
|
if (days) {
|
|
var d = new Date();
|
|
d.setTime(d.getTime() + days * 86400000);
|
|
expires = "; expires=" + d.toUTCString();
|
|
}
|
|
document.cookie = name + "=" + encodeURIComponent(value) + expires + "; path=/; SameSite=Lax";
|
|
}
|
|
|
|
function applyToolbarState(collapsed) {
|
|
if (collapsed) {
|
|
navToolbar.classList.add("nav-toolbar-hidden");
|
|
toggleBtn.innerHTML = "▶ Filters";
|
|
} else {
|
|
navToolbar.classList.remove("nav-toolbar-hidden");
|
|
toggleBtn.innerHTML = "▼ Filters";
|
|
}
|
|
}
|
|
|
|
var toolbarCollapsed = getCookie("org_web_nav_toolbar") === "hidden";
|
|
applyToolbarState(toolbarCollapsed);
|
|
|
|
toggleBtn.addEventListener("click", function () {
|
|
toolbarCollapsed = !toolbarCollapsed;
|
|
setCookie("org_web_nav_toolbar", toolbarCollapsed ? "hidden" : "visible", 365);
|
|
applyToolbarState(toolbarCollapsed);
|
|
});
|
|
|
|
const searchInput = document.getElementById("title-search");
|
|
const fileList = document.getElementById("file-list");
|
|
const shuffleNotesBtn = document.getElementById("shuffle-notes");
|
|
const sortBacklinksBtn = document.getElementById("sort-backlinks");
|
|
const sortCreatedBtn = document.getElementById("sort-created");
|
|
const sortModifiedBtn = document.getElementById("sort-modified");
|
|
const jumpCurrentBtn = document.getElementById("jump-current");
|
|
const fileLinks = Array.from(document.querySelectorAll("#file-list .file-link"));
|
|
const activeFileLink = document.querySelector("#file-list .file-link.active");
|
|
|
|
var PIN_STORAGE_KEY = "org_web_viewer_pins";
|
|
|
|
function loadUserPins() {
|
|
try {
|
|
var raw = localStorage.getItem(PIN_STORAGE_KEY);
|
|
return raw ? new Set(JSON.parse(raw)) : new Set();
|
|
} catch (e) {
|
|
return new Set();
|
|
}
|
|
}
|
|
|
|
function saveUserPins(pinSet) {
|
|
localStorage.setItem(PIN_STORAGE_KEY, JSON.stringify(Array.from(pinSet)));
|
|
}
|
|
|
|
var userPins = loadUserPins();
|
|
|
|
function applyUserPins() {
|
|
for (var i = 0; i < fileLinks.length; i++) {
|
|
var link = fileLinks[i];
|
|
var filePath = link.getAttribute("data-file-path") || "";
|
|
var serverPinned = link.getAttribute("data-pinned") === "server";
|
|
if (serverPinned) {
|
|
continue;
|
|
}
|
|
if (userPins.has(filePath)) {
|
|
link.setAttribute("data-pinned", "user");
|
|
var titleSpan = link.querySelector(".file-title");
|
|
if (titleSpan && titleSpan.textContent.indexOf("\uD83D\uDCCC") !== 0) {
|
|
titleSpan.textContent = "\uD83D\uDCCC " + titleSpan.textContent;
|
|
}
|
|
} else {
|
|
link.setAttribute("data-pinned", "");
|
|
var titleSpan2 = link.querySelector(".file-title");
|
|
if (titleSpan2 && titleSpan2.textContent.indexOf("\uD83D\uDCCC") === 0) {
|
|
titleSpan2.textContent = titleSpan2.textContent.replace(/^\uD83D\uDCCC\s*/, "");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function pinRank(link) {
|
|
var p = link.getAttribute("data-pinned") || "";
|
|
if (p === "server") return 0;
|
|
if (p === "user") return 1;
|
|
return 2;
|
|
}
|
|
|
|
function stableSortWithPins(links, compareFn) {
|
|
links.sort(function (a, b) {
|
|
var ra = pinRank(a);
|
|
var rb = pinRank(b);
|
|
if (ra !== rb) return ra - rb;
|
|
if (ra < 2) return 0;
|
|
return compareFn(a, b);
|
|
});
|
|
}
|
|
|
|
function reorderLinks(links) {
|
|
var fragment = document.createDocumentFragment();
|
|
for (var i = 0; i < links.length; i++) {
|
|
fragment.appendChild(links[i]);
|
|
}
|
|
fileList.appendChild(fragment);
|
|
}
|
|
|
|
function reorderWithPins() {
|
|
var links = Array.from(fileList.querySelectorAll(".file-link"));
|
|
stableSortWithPins(links, function () { return 0; });
|
|
reorderLinks(links);
|
|
}
|
|
|
|
applyUserPins();
|
|
reorderWithPins();
|
|
|
|
fileList.addEventListener("click", function (e) {
|
|
var btn = e.target.closest(".pin-toggle");
|
|
if (!btn) return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
var link = btn.closest(".file-link");
|
|
if (!link) return;
|
|
var filePath = link.getAttribute("data-file-path") || "";
|
|
if (link.getAttribute("data-pinned") === "server") return;
|
|
if (userPins.has(filePath)) {
|
|
userPins.delete(filePath);
|
|
} else {
|
|
userPins.add(filePath);
|
|
}
|
|
saveUserPins(userPins);
|
|
applyUserPins();
|
|
reorderWithPins();
|
|
});
|
|
|
|
searchInput.addEventListener("input", function () {
|
|
var query = searchInput.value.trim().toLowerCase();
|
|
for (var i = 0; i < fileLinks.length; i++) {
|
|
var link = fileLinks[i];
|
|
var haystack = link.getAttribute("data-search") || "";
|
|
link.style.display = haystack.includes(query) ? "" : "none";
|
|
}
|
|
});
|
|
|
|
jumpCurrentBtn.addEventListener("click", function () {
|
|
if (!activeFileLink) {
|
|
return;
|
|
}
|
|
activeFileLink.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
});
|
|
|
|
shuffleNotesBtn.addEventListener("click", function () {
|
|
var links = Array.from(fileList.querySelectorAll(".file-link"));
|
|
var pinned = [];
|
|
var rest = [];
|
|
for (var i = 0; i < links.length; i++) {
|
|
if (pinRank(links[i]) < 2) {
|
|
pinned.push(links[i]);
|
|
} else {
|
|
rest.push(links[i]);
|
|
}
|
|
}
|
|
for (var j = rest.length - 1; j > 0; j -= 1) {
|
|
var k = Math.floor(Math.random() * (j + 1));
|
|
var tmp = rest[j];
|
|
rest[j] = rest[k];
|
|
rest[k] = tmp;
|
|
}
|
|
reorderLinks(pinned.concat(rest));
|
|
});
|
|
|
|
sortBacklinksBtn.addEventListener("click", function () {
|
|
var links = Array.from(fileList.querySelectorAll(".file-link"));
|
|
stableSortWithPins(links, function (a, b) {
|
|
var aCount = Number.parseInt(a.getAttribute("data-backlinks") || "0", 10);
|
|
var bCount = Number.parseInt(b.getAttribute("data-backlinks") || "0", 10);
|
|
if (aCount !== bCount) {
|
|
return bCount - aCount;
|
|
}
|
|
var aKey = a.getAttribute("data-search") || "";
|
|
var bKey = b.getAttribute("data-search") || "";
|
|
return aKey.localeCompare(bKey);
|
|
});
|
|
reorderLinks(links);
|
|
});
|
|
|
|
sortCreatedBtn.addEventListener("click", function () {
|
|
var links = Array.from(fileList.querySelectorAll(".file-link"));
|
|
stableSortWithPins(links, function (a, b) {
|
|
var aRaw = a.getAttribute("data-created-ts") || "";
|
|
var bRaw = b.getAttribute("data-created-ts") || "";
|
|
var aMissing = aRaw === "";
|
|
var bMissing = bRaw === "";
|
|
if (aMissing && !bMissing) {
|
|
return -1;
|
|
}
|
|
if (!aMissing && bMissing) {
|
|
return 1;
|
|
}
|
|
if (!aMissing && !bMissing) {
|
|
var aTs = Number.parseInt(aRaw, 10);
|
|
var bTs = Number.parseInt(bRaw, 10);
|
|
if (aTs !== bTs) {
|
|
return aTs - bTs;
|
|
}
|
|
}
|
|
var aKey = a.getAttribute("data-search") || "";
|
|
var bKey = b.getAttribute("data-search") || "";
|
|
return aKey.localeCompare(bKey);
|
|
});
|
|
reorderLinks(links);
|
|
});
|
|
|
|
sortModifiedBtn.addEventListener("click", function () {
|
|
var links = Array.from(fileList.querySelectorAll(".file-link"));
|
|
stableSortWithPins(links, function (a, b) {
|
|
var aTs = Number.parseInt(a.getAttribute("data-modified-ts") || "0", 10);
|
|
var bTs = Number.parseInt(b.getAttribute("data-modified-ts") || "0", 10);
|
|
if (aTs !== bTs) {
|
|
return bTs - aTs;
|
|
}
|
|
var aKey = a.getAttribute("data-search") || "";
|
|
var bKey = b.getAttribute("data-search") || "";
|
|
return aKey.localeCompare(bKey);
|
|
});
|
|
reorderLinks(links);
|
|
});
|
|
})();
|
|
|
|
(function () {
|
|
document.addEventListener("click", function (e) {
|
|
const btn = e.target.closest(".webhook-send-btn");
|
|
if (!btn) return;
|
|
const file = btn.getAttribute("data-file");
|
|
const heading = btn.getAttribute("data-heading");
|
|
btn.disabled = true;
|
|
btn.textContent = "Sending\u2026";
|
|
fetch("/webhook", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ file: file, heading: heading }),
|
|
})
|
|
.then(function (resp) { return resp.json(); })
|
|
.then(function (data) {
|
|
if (data.ok) {
|
|
btn.textContent = "Sent!";
|
|
btn.classList.add("webhook-sent");
|
|
} else {
|
|
btn.textContent = "Error: " + (data.error || "unknown");
|
|
btn.classList.add("webhook-error");
|
|
}
|
|
})
|
|
.catch(function () {
|
|
btn.textContent = "Failed";
|
|
btn.classList.add("webhook-error");
|
|
})
|
|
.finally(function () {
|
|
setTimeout(function () {
|
|
btn.disabled = false;
|
|
btn.textContent = "Start work";
|
|
btn.classList.remove("webhook-sent", "webhook-error");
|
|
}, 3000);
|
|
});
|
|
});
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|