Files

301 lines
10 KiB
JavaScript

/* Bookmarks page — sovereign://bookmarks
*
* Fetches the bookmark tree from sovereign://bookmarks/list (JSON) and
* renders it as an expandable/collapsible tree. Folders may be nested
* arbitrarily (e.g. "Work/Projects/Secret"). Add/delete/create/move/rename
* use the sovereign://bookmarks/{add,delete,createdir,deletedir,move,renamedir}
* endpoints.
*
* Expand/collapse state is persisted in localStorage so the user's
* expanded folders survive page reloads.
*/
function sovereignGet(url) {
return new Promise(function(resolve, reject) {
var x = new XMLHttpRequest();
x.open('GET', url, true);
x.onreadystatechange = function() {
if (x.readyState !== 4) return;
try { resolve(JSON.parse(x.responseText)); }
catch (e) { reject(e); }
};
x.onerror = function() { reject(new Error('XHR error')); };
x.send();
});
}
function esc(s) {
var d = document.createElement('div');
d.textContent = s == null ? '' : String(s);
return d.innerHTML;
}
/* ── Expand/collapse state (persisted in localStorage) ─────────────── */
var EXPANDED_KEY = 'sovereign_bookmarks_expanded';
function getExpanded() {
try {
var raw = localStorage.getItem(EXPANDED_KEY);
return raw ? JSON.parse(raw) : {};
} catch (e) { return {}; }
}
function setExpanded(path, isExpanded) {
var state = getExpanded();
if (isExpanded) state[path] = true;
else delete state[path];
try { localStorage.setItem(EXPANDED_KEY, JSON.stringify(state)); }
catch (e) { /* ignore quota errors */ }
}
/* ── Tree rendering ────────────────────────────────────────────────── */
function renderTree(data) {
var haveSigner = !!data.have_signer;
document.getElementById('readonly-note').style.display =
haveSigner ? 'none' : '';
document.getElementById('add-form').style.display =
haveSigner ? '' : 'none';
var c = document.getElementById('bm-tree');
var tree = data.tree;
if (!tree || !tree.children || tree.children.length === 0) {
c.innerHTML =
'<p class="empty">No bookmarks yet. Click the bookmark button ' +
'in the toolbar to save a page.</p>';
return;
}
c.innerHTML = '';
(tree.children || []).forEach(function(child) {
c.appendChild(renderNode(child, haveSigner, 0));
});
}
function renderNode(node, haveSigner, depth) {
/* A folder row + its bookmark list + its children (recursive). */
var expandedState = getExpanded();
var isExpanded = !!expandedState[node.path];
var wrap = document.createElement('div');
wrap.className = 'tree-node';
/* Folder row. */
var row = document.createElement('div');
row.className = 'folder-row';
var caret = document.createElement('span');
caret.className = 'caret' + (isExpanded ? ' expanded' : '');
caret.textContent = '\u25B6'; /* right-pointing triangle */
caret.onclick = function() {
var nowExpanded = caret.classList.toggle('expanded');
childList.style.display = nowExpanded ? '' : 'none';
setExpanded(node.path, nowExpanded);
};
row.appendChild(caret);
var icon = document.createElement('span');
icon.className = 'folder-icon';
icon.textContent = '\u{1F4C1}'; /* folder emoji */
row.appendChild(icon);
var name = document.createElement('span');
name.className = 'folder-name';
name.textContent = node.name + ' (' + node.count + ')';
row.appendChild(name);
/* Per-folder actions. */
var actions = document.createElement('span');
actions.className = 'folder-actions';
if (haveSigner) {
var addHere = document.createElement('a');
addHere.className = 'btn';
addHere.href = '#';
addHere.textContent = 'Add Here';
addHere.onclick = function(e) {
e.preventDefault();
document.getElementById('bm-path').value = node.path;
document.getElementById('bm-url').focus();
};
actions.appendChild(addHere);
var newSub = document.createElement('a');
newSub.className = 'btn';
newSub.href = '#';
newSub.textContent = 'New Subfolder';
newSub.onclick = function(e) {
e.preventDefault();
var sub = prompt('New subfolder name under "' + node.path + '/":');
if (sub && sub.trim()) {
var newPath = node.path + '/' + sub.trim();
location.href = 'sovereign://bookmarks/createdir?name=' +
encodeURIComponent(newPath);
}
};
actions.appendChild(newSub);
if (node.path !== 'General' && node.path !== 'Bookmarks Bar') {
var rename = document.createElement('a');
rename.className = 'btn';
rename.href = '#';
rename.textContent = 'Rename';
rename.onclick = function(e) {
e.preventDefault();
var newName = prompt('Rename folder "' + node.path + '" to:',
node.path);
if (newName && newName.trim() && newName.trim() !== node.path) {
location.href = 'sovereign://bookmarks/renamedir?old=' +
encodeURIComponent(node.path) + '&new=' +
encodeURIComponent(newName.trim());
}
};
actions.appendChild(rename);
var del = document.createElement('a');
del.className = 'btn btn-del';
del.href = '#';
del.textContent = 'Delete';
del.onclick = function(e) {
e.preventDefault();
if (confirm('Delete folder "' + node.path + '"? ' +
'Bookmarks will be moved to General.')) {
location.href = 'sovereign://bookmarks/deletedir?dir=' +
encodeURIComponent(node.path);
}
};
actions.appendChild(del);
}
}
row.appendChild(actions);
wrap.appendChild(row);
/* Child list: bookmarks + subfolders. */
var childList = document.createElement('div');
childList.className = 'tree-children';
childList.style.display = isExpanded ? '' : 'none';
childList.style.marginLeft = '20px';
/* Bookmarks in this folder. */
(node.bookmarks || []).forEach(function(bm) {
childList.appendChild(renderBookmark(bm, node, haveSigner));
});
/* Subfolders. */
(node.children || []).forEach(function(child) {
childList.appendChild(renderNode(child, haveSigner, depth + 1));
});
wrap.appendChild(childList);
return wrap;
}
function renderBookmark(bm, folder, haveSigner) {
var row = document.createElement('div');
row.className = 'bm';
var info = document.createElement('div');
var title = document.createElement('div');
title.className = 'bm-title';
title.textContent = (bm.title && bm.title.length) ? bm.title : '(untitled)';
info.appendChild(title);
var urlLink = document.createElement('div');
var a = document.createElement('a');
a.className = 'bm-url';
a.href = bm.url;
a.textContent = bm.url;
urlLink.appendChild(a);
info.appendChild(urlLink);
var meta = document.createElement('div');
meta.className = 'bm-meta';
meta.textContent = 'Added: ' + bm.added;
info.appendChild(meta);
row.appendChild(info);
if (haveSigner) {
var actions = document.createElement('div');
actions.className = 'actions';
var editBtn = document.createElement('a');
editBtn.className = 'btn';
editBtn.href = '#';
editBtn.textContent = 'Edit';
editBtn.onclick = function(e) {
e.preventDefault();
var curTitle = (bm.title && bm.title.length) ? bm.title : '';
var newTitle = prompt('Edit bookmark name:', curTitle);
if (newTitle === null) return;
var t = newTitle.trim();
if (t === curTitle.trim()) return;
location.href = 'sovereign://bookmarks/rename?dir=' +
encodeURIComponent(folder.path) + '&url=' +
encodeURIComponent(bm.url) + '&title=' +
encodeURIComponent(t);
};
actions.appendChild(editBtn);
var moveBtn = document.createElement('a');
moveBtn.className = 'btn';
moveBtn.href = '#';
moveBtn.textContent = 'Move';
moveBtn.onclick = function(e) {
e.preventDefault();
var dest = prompt('Move bookmark to folder path:', folder.path);
if (dest && dest.trim() && dest.trim() !== folder.path) {
location.href = 'sovereign://bookmarks/move?from=' +
encodeURIComponent(folder.path) + '&to=' +
encodeURIComponent(dest.trim()) + '&url=' +
encodeURIComponent(bm.url);
}
};
actions.appendChild(moveBtn);
var delBtn = document.createElement('a');
delBtn.className = 'btn btn-del';
delBtn.href = '#';
delBtn.textContent = 'Delete';
delBtn.onclick = function(e) {
e.preventDefault();
if (confirm('Delete this bookmark?')) {
location.href = 'sovereign://bookmarks/delete?dir=' +
encodeURIComponent(folder.path) + '&url=' +
encodeURIComponent(bm.url);
}
};
actions.appendChild(delBtn);
row.appendChild(actions);
}
return row;
}
/* ── Fetch + actions ───────────────────────────────────────────────── */
function fetchBookmarks() {
sovereignGet('sovereign://bookmarks/list?_=' + Date.now())
.then(function(data) { renderTree(data); })
.catch(function(e) {
document.getElementById('bm-tree').innerHTML =
'<p class="empty">Failed to load bookmarks: ' + esc(e.message) + '</p>';
});
}
function addBookmark() {
var url = encodeURIComponent(document.getElementById('bm-url').value);
var title = encodeURIComponent(document.getElementById('bm-title').value);
var path = encodeURIComponent(document.getElementById('bm-path').value);
if (!url) { alert('URL is required'); return; }
location.href = 'sovereign://bookmarks/add?dir=' + path + '&url=' + url +
'&title=' + title;
}
function createDir() {
var name = encodeURIComponent(document.getElementById('new-path').value);
if (!name) { alert('Folder path is required'); return; }
location.href = 'sovereign://bookmarks/createdir?name=' + name;
}
fetchBookmarks();