178 lines
6.6 KiB
JavaScript
178 lines
6.6 KiB
JavaScript
// Detects the local datascape-companion via a /status probe and wires up
|
|
// the footer status icon, file-row click interception, and the "reveal in
|
|
// file manager" page action. All companion calls are best-effort: if the
|
|
// fetch fails the page falls back to default browser behavior.
|
|
|
|
(function () {
|
|
var COMPANION_PORT = 17680;
|
|
var COMPANION_BASE = 'http://127.0.0.1:' + COMPANION_PORT;
|
|
var STATUS_TIMEOUT_MS = 1500;
|
|
|
|
var state = { available: false, info: null };
|
|
|
|
function wikiPathFromHref(href) {
|
|
// href is the URL the wiki rendered for the listing item (e.g.
|
|
// "/photos/2024/img.jpg"). Strip leading slash and decode so the
|
|
// companion sees a relative wiki path matching its on-disk layout.
|
|
try {
|
|
var u = new URL(href, window.location.href);
|
|
if (u.origin !== window.location.origin) return null;
|
|
var p = u.pathname.replace(/^\/+/, '');
|
|
return decodeURIComponent(p);
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function companionGET(path, params) {
|
|
var qs = '';
|
|
if (params) {
|
|
var parts = [];
|
|
for (var k in params) {
|
|
parts.push(encodeURIComponent(k) + '=' + encodeURIComponent(params[k]));
|
|
}
|
|
qs = '?' + parts.join('&');
|
|
}
|
|
var ctrl = new AbortController();
|
|
var timer = setTimeout(function () { ctrl.abort(); }, STATUS_TIMEOUT_MS);
|
|
return fetch(COMPANION_BASE + path + qs, {
|
|
method: 'GET',
|
|
mode: 'cors',
|
|
credentials: 'omit',
|
|
signal: ctrl.signal
|
|
}).finally(function () { clearTimeout(timer); });
|
|
}
|
|
|
|
function renderFlyout(menu) {
|
|
menu.innerHTML = '';
|
|
if (state.available) {
|
|
var info = state.info || {};
|
|
var name = info.name || 'datascape-companion';
|
|
var ver = info.version ? ' v' + info.version : '';
|
|
var head = document.createElement('div');
|
|
head.className = 'panel-header';
|
|
head.textContent = 'Companion';
|
|
menu.appendChild(head);
|
|
|
|
var label = document.createElement('div');
|
|
label.className = 'companion-line';
|
|
label.textContent = name + ver;
|
|
menu.appendChild(label);
|
|
|
|
var link = document.createElement('a');
|
|
link.className = 'btn dropdown-item';
|
|
link.href = COMPANION_BASE + '/config';
|
|
link.target = '_blank';
|
|
link.rel = 'noopener';
|
|
link.textContent = 'Settings';
|
|
menu.appendChild(link);
|
|
} else {
|
|
var head2 = document.createElement('div');
|
|
head2.className = 'panel-header';
|
|
head2.textContent = 'Companion not detected';
|
|
menu.appendChild(head2);
|
|
|
|
var msg = document.createElement('div');
|
|
msg.className = 'companion-line muted';
|
|
msg.textContent = 'Install the companion to open files locally.';
|
|
menu.appendChild(msg);
|
|
|
|
var win = document.createElement('a');
|
|
win.className = 'btn dropdown-item';
|
|
win.href = '/companion/download/windows';
|
|
win.textContent = 'Download — Windows';
|
|
menu.appendChild(win);
|
|
|
|
var lin = document.createElement('a');
|
|
lin.className = 'btn dropdown-item';
|
|
lin.href = '/companion/download/linux';
|
|
lin.textContent = 'Download — Linux';
|
|
menu.appendChild(lin);
|
|
}
|
|
}
|
|
|
|
function updateFooterIcon() {
|
|
var wrap = document.querySelector('[data-companion-status]');
|
|
if (!wrap) return;
|
|
wrap.hidden = false;
|
|
var btn = wrap.querySelector('.companion-icon');
|
|
if (state.available) {
|
|
btn.textContent = '●';
|
|
btn.classList.add('companion-on');
|
|
btn.classList.remove('companion-off');
|
|
btn.title = 'Companion detected';
|
|
} else {
|
|
btn.textContent = '○';
|
|
btn.classList.add('companion-off');
|
|
btn.classList.remove('companion-on');
|
|
btn.title = 'Companion not detected';
|
|
}
|
|
var menu = wrap.querySelector('.companion-flyout');
|
|
renderFlyout(menu);
|
|
if (typeof wireDropdown === 'function') wireDropdown(btn);
|
|
}
|
|
|
|
function wireFileLinks() {
|
|
if (!state.available) return;
|
|
document.addEventListener('click', function (e) {
|
|
var item = e.target.closest && e.target.closest('.listing-item');
|
|
if (!item) return;
|
|
var anchor = e.target.closest('a');
|
|
if (!anchor) return;
|
|
// Only intercept the primary file link, and only for files (not folders).
|
|
// Folders end with "/" — let the browser navigate normally.
|
|
var path = item.dataset.path || anchor.getAttribute('href');
|
|
if (!path || path.endsWith('/')) return;
|
|
// Allow modified clicks (open in new tab, etc.) to pass through.
|
|
if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
var rel = wikiPathFromHref(path);
|
|
if (rel === null) return;
|
|
e.preventDefault();
|
|
companionGET('/open-file', { path: rel }).catch(function () {
|
|
// Fallback: navigate to the file (download / inline view).
|
|
window.location.href = anchor.href;
|
|
});
|
|
});
|
|
}
|
|
|
|
function wireRevealButton() {
|
|
if (!state.available) return;
|
|
var btns = document.querySelectorAll('[data-companion-reveal]');
|
|
btns.forEach(function (btn) {
|
|
btn.hidden = false;
|
|
btn.addEventListener('click', function () {
|
|
var rel = wikiPathFromHref(window.location.pathname);
|
|
if (rel === null) rel = '';
|
|
companionGET('/open-folder', { path: rel }).catch(function () { });
|
|
});
|
|
});
|
|
}
|
|
|
|
function probeStatus() {
|
|
return companionGET('/status').then(function (r) {
|
|
if (!r.ok) throw new Error('status ' + r.status);
|
|
return r.json();
|
|
}).then(function (info) {
|
|
state.available = true;
|
|
state.info = info;
|
|
}).catch(function () {
|
|
state.available = false;
|
|
state.info = null;
|
|
});
|
|
}
|
|
|
|
function init() {
|
|
probeStatus().then(function () {
|
|
updateFooterIcon();
|
|
wireFileLinks();
|
|
wireRevealButton();
|
|
});
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
})();
|