// 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 btn-block'; 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 btn-block'; win.href = '/companion/download/windows'; win.textContent = 'Download — Windows'; menu.appendChild(win); var lin = document.createElement('a'); lin.className = 'btn btn-block'; 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('.list-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(); } })();