Add companion application
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
// 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() {
|
||||
var btn = document.querySelector('[data-companion-reveal]');
|
||||
if (!btn) return;
|
||||
if (!state.available) return;
|
||||
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();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user