diff --git a/.gitignore b/.gitignore index cefde83..784ff8a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ cache/ # Binaries datascape *.exe +bin/ +companion/datascape-companion-* diff --git a/Makefile b/Makefile index 31b8cfa..f8019f5 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,29 @@ NAS := luxick@192.168.3.3 -.PHONY: deploy -deploy: +COMPANION_WIN := companion/datascape-companion-windows-amd64.exe +COMPANION_LIN := companion/datascape-companion-linux-amd64 + +.PHONY: deploy companion companion-windows companion-linux companion-release + +# Cross-compiled companion artifacts the wiki binary embeds. Both must exist +# before `go build .` so embed.FS picks them up. +companion-release: $(COMPANION_WIN) $(COMPANION_LIN) + +$(COMPANION_WIN): + GOOS=windows GOARCH=amd64 go build -o $@ ./cmd/companion + +$(COMPANION_LIN): + GOOS=linux GOARCH=amd64 go build -o $@ ./cmd/companion + +companion-windows: $(COMPANION_WIN) +companion-linux: $(COMPANION_LIN) + +# Local companion build for the host OS (handy for development). +companion: + mkdir -p bin + go build -o bin/ ./cmd/companion + +deploy: companion-release GOOS=linux GOARCH=arm GOARM=7 go build -o datascape-arm . ssh $(NAS) 'kill $$(cat /share/homes/luxick/.local/bin/datascape.pid) 2>/dev/null; rm -f /share/homes/luxick/.local/bin/datascape.pid' scp datascape-arm $(NAS):/share/homes/luxick/.local/bin/datascape diff --git a/assets/companion.js b/assets/companion.js new file mode 100644 index 0000000..1c6520e --- /dev/null +++ b/assets/companion.js @@ -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(); + } +})(); diff --git a/assets/layout.html b/assets/layout.html index 12c403f..4581c2f 100644 --- a/assets/layout.html +++ b/assets/layout.html @@ -11,6 +11,7 @@ + {{block "headScripts" .}}{{end}}
@@ -34,6 +35,10 @@ {{block "extras" .}}{{end}} diff --git a/assets/page/fab.js b/assets/page/fab.js new file mode 100644 index 0000000..3ef6794 --- /dev/null +++ b/assets/page/fab.js @@ -0,0 +1,18 @@ +// Lift the floating action button above the footer when the footer is on +// screen, so it never overlaps the request-time line or the companion icon. +// Mirrors the TOC's header-aware top-offset behaviour in toc.js. +(function () { + var fab = document.querySelector(".fab"); + var footer = document.querySelector("footer"); + if (!fab || !footer) return; + + function updateBottom() { + var rect = footer.getBoundingClientRect(); + var overlap = Math.max(0, window.innerHeight - rect.top); + fab.style.bottom = (overlap + 16) + "px"; + } + + window.addEventListener("scroll", updateBottom, { passive: true }); + window.addEventListener("resize", updateBottom); + updateBottom(); +})(); diff --git a/assets/page/main.html b/assets/page/main.html index e92fafb..d65f57a 100644 --- a/assets/page/main.html +++ b/assets/page/main.html @@ -11,7 +11,7 @@