Add pinned files and filter hiding
This commit is contained in:
9
main.py
9
main.py
@ -656,12 +656,17 @@ def build_index_page(
|
||||
created_key_attr = "" if created_key is None else str(created_key)
|
||||
modified_ts = str(int(f.mtime))
|
||||
pin_icon = "\U0001F4CC " if f.pinned else ""
|
||||
display_title = f"{pin_icon}TODAY" if f.pinned else safe_title
|
||||
pinned_attr = "server" if f.pinned else ""
|
||||
nav_items.append(
|
||||
f"<a class='file-link {active}' data-search='{searchable}' "
|
||||
f"data-backlinks='{backlinks_count}' data-created-ts='{created_key_attr}' "
|
||||
f"data-modified-ts='{modified_ts}' href='{safe_href}'>"
|
||||
f"<span class='file-title' title='{full_title}'>{pin_icon}{safe_title}</span>"
|
||||
f"data-modified-ts='{modified_ts}' data-pinned='{pinned_attr}' "
|
||||
f"data-file-path='{full_path}' href='{safe_href}'>"
|
||||
f"<span class='file-title' title='{full_title}'>{display_title}</span>"
|
||||
f"<span class='file-path' title='{full_path}'>{safe_path}</span>"
|
||||
f"<button class='pin-toggle' type='button' title='Pin/unpin this file' "
|
||||
f"aria-label='Pin or unpin this file'>📌</button>"
|
||||
"</a>"
|
||||
)
|
||||
|
||||
|
||||
@ -106,6 +106,41 @@ nav {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.nav-toolbar-header {
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
.toggle-toolbar-btn {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 0.45rem;
|
||||
background: var(--panel);
|
||||
color: var(--ink-soft);
|
||||
padding: 0.3rem 0.6rem;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.toggle-toolbar-btn:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.nav-toolbar {
|
||||
overflow: hidden;
|
||||
transition: max-height 0.2s ease, opacity 0.2s ease;
|
||||
max-height: 500px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.nav-toolbar-hidden {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search-wrap {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
@ -156,11 +191,12 @@ nav {
|
||||
|
||||
.file-link {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.6rem 0.7rem;
|
||||
padding: 0.6rem 2rem 0.6rem 0.7rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 0.5rem;
|
||||
color: var(--ink-soft);
|
||||
@ -168,6 +204,11 @@ nav {
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.file-link[data-pinned="user"] {
|
||||
border-color: color-mix(in srgb, var(--accent) 40%, var(--line));
|
||||
background: color-mix(in srgb, var(--accent-soft) 30%, var(--panel));
|
||||
}
|
||||
|
||||
.file-title {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
@ -199,6 +240,37 @@ nav {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pin-toggle {
|
||||
position: absolute;
|
||||
top: 0.45rem;
|
||||
right: 0.35rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1;
|
||||
padding: 0.15rem;
|
||||
border-radius: 0.25rem;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
.file-link:hover .pin-toggle,
|
||||
.file-link:focus-within .pin-toggle {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.file-link:hover .pin-toggle:hover,
|
||||
.file-link:focus-within .pin-toggle:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.file-link[data-pinned="user"] .pin-toggle,
|
||||
.file-link[data-pinned="server"] .pin-toggle {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 1.2rem;
|
||||
overflow: auto;
|
||||
|
||||
@ -17,6 +17,10 @@
|
||||
<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>
|
||||
@ -24,6 +28,7 @@
|
||||
<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>
|
||||
@ -70,6 +75,43 @@
|
||||
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");
|
||||
@ -79,22 +121,110 @@
|
||||
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");
|
||||
searchInput.addEventListener("input", function () {
|
||||
const query = searchInput.value.trim().toLowerCase();
|
||||
for (const link of fileLinks) {
|
||||
const haystack = link.getAttribute("data-search") || "";
|
||||
link.style.display = haystack.includes(query) ? "" : "none";
|
||||
|
||||
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) {
|
||||
const fragment = document.createDocumentFragment();
|
||||
for (const link of links) {
|
||||
fragment.appendChild(link);
|
||||
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;
|
||||
@ -103,38 +233,47 @@
|
||||
});
|
||||
|
||||
shuffleNotesBtn.addEventListener("click", function () {
|
||||
const links = Array.from(fileList.querySelectorAll(".file-link"));
|
||||
for (let i = links.length - 1; i > 0; i -= 1) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
const tmp = links[i];
|
||||
links[i] = links[j];
|
||||
links[j] = tmp;
|
||||
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]);
|
||||
}
|
||||
reorderLinks(links);
|
||||
}
|
||||
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 () {
|
||||
const links = Array.from(fileList.querySelectorAll(".file-link"));
|
||||
links.sort(function (a, b) {
|
||||
const aCount = Number.parseInt(a.getAttribute("data-backlinks") || "0", 10);
|
||||
const bCount = Number.parseInt(b.getAttribute("data-backlinks") || "0", 10);
|
||||
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;
|
||||
}
|
||||
const aKey = a.getAttribute("data-search") || "";
|
||||
const bKey = b.getAttribute("data-search") || "";
|
||||
var aKey = a.getAttribute("data-search") || "";
|
||||
var bKey = b.getAttribute("data-search") || "";
|
||||
return aKey.localeCompare(bKey);
|
||||
});
|
||||
reorderLinks(links);
|
||||
});
|
||||
|
||||
sortCreatedBtn.addEventListener("click", function () {
|
||||
const links = Array.from(fileList.querySelectorAll(".file-link"));
|
||||
links.sort(function (a, b) {
|
||||
const aRaw = a.getAttribute("data-created-ts") || "";
|
||||
const bRaw = b.getAttribute("data-created-ts") || "";
|
||||
const aMissing = aRaw === "";
|
||||
const bMissing = bRaw === "";
|
||||
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;
|
||||
}
|
||||
@ -142,29 +281,29 @@
|
||||
return 1;
|
||||
}
|
||||
if (!aMissing && !bMissing) {
|
||||
const aTs = Number.parseInt(aRaw, 10);
|
||||
const bTs = Number.parseInt(bRaw, 10);
|
||||
var aTs = Number.parseInt(aRaw, 10);
|
||||
var bTs = Number.parseInt(bRaw, 10);
|
||||
if (aTs !== bTs) {
|
||||
return aTs - bTs;
|
||||
}
|
||||
}
|
||||
const aKey = a.getAttribute("data-search") || "";
|
||||
const bKey = b.getAttribute("data-search") || "";
|
||||
var aKey = a.getAttribute("data-search") || "";
|
||||
var bKey = b.getAttribute("data-search") || "";
|
||||
return aKey.localeCompare(bKey);
|
||||
});
|
||||
reorderLinks(links);
|
||||
});
|
||||
|
||||
sortModifiedBtn.addEventListener("click", function () {
|
||||
const links = Array.from(fileList.querySelectorAll(".file-link"));
|
||||
links.sort(function (a, b) {
|
||||
const aTs = Number.parseInt(a.getAttribute("data-modified-ts") || "0", 10);
|
||||
const bTs = Number.parseInt(b.getAttribute("data-modified-ts") || "0", 10);
|
||||
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;
|
||||
}
|
||||
const aKey = a.getAttribute("data-search") || "";
|
||||
const bKey = b.getAttribute("data-search") || "";
|
||||
var aKey = a.getAttribute("data-search") || "";
|
||||
var bKey = b.getAttribute("data-search") || "";
|
||||
return aKey.localeCompare(bKey);
|
||||
});
|
||||
reorderLinks(links);
|
||||
|
||||
Reference in New Issue
Block a user