301 lines
10 KiB
JavaScript
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();
|