// scrot PWA — thin client mirroring cli/scrot-clip-push, scrot-clip-pull, // scrot-shot. Token lives in localStorage (used for every fetch here) and // mirrored into a cookie (read server-side by /manifest.webmanifest to bake // the token into the share_target action — see server/src/manifest.ts). const TOKEN_KEY = 'scrot_token' const $ = (id) => document.getElementById(id) const toastEl = $('toast') const onboardEl = $('onboard') const tokenInput = $('tokenInput') function getToken() { return localStorage.getItem(TOKEN_KEY) || '' } function setToken(token) { localStorage.setItem(TOKEN_KEY, token) // 1 year, Lax is enough since this cookie is only ever read by our own // manifest route, never sent cross-site meaningfully. document.cookie = `scrot_token=${encodeURIComponent(token)}; path=/; max-age=31536000; samesite=lax` } function toast(message) { toastEl.textContent = message toastEl.style.display = 'block' clearTimeout(toast._t) toast._t = setTimeout(() => { toastEl.style.display = 'none' }, 4000) } function refreshOnboardVisibility() { const has = !!getToken() onboardEl.style.display = has ? 'none' : 'block' if (has) tokenInput.value = getToken() } // --- onboarding: pick up #t= from cli/scrot-onboard's QR/link --- function consumeHashToken() { const match = location.hash.match(/(?:^#|&)t=([^&]+)/) if (!match) return false const token = decodeURIComponent(match[1]) setToken(token) history.replaceState(null, '', location.pathname + location.search) toast('Device token saved. Add scrot to your home screen to enable "Share to scrot".') return true } const hadHashToken = consumeHashToken() if (!hadHashToken && getToken()) setToken(getToken()) // re-assert cookie in case it expired refreshOnboardVisibility() $('saveToken').addEventListener('click', () => { const token = tokenInput.value.trim() if (!token) return setToken(token) refreshOnboardVisibility() toast('Device token saved.') }) $('editToken').addEventListener('click', () => { onboardEl.style.display = 'block' tokenInput.focus() }) // --- share-target redirect landing --- const params = new URLSearchParams(location.search) if (params.get('shared') === 'clip') toast('Shared text saved as a clip.') if (params.get('shared') === 'shot') toast('Shared screenshot uploaded.') if (params.get('shared') === 'empty') toast('Nothing shareable in that share (no text or image).') if (params.has('shared')) history.replaceState(null, '', location.pathname) // --- push clip --- $('pasteFromClipboard').addEventListener('click', async () => { try { $('clipText').value = await navigator.clipboard.readText() } catch { toast('Clipboard read not available — paste manually instead.') } }) $('pushClip').addEventListener('click', async () => { const text = $('clipText').value if (!text) return const result = $('clipResult') result.textContent = 'Sending…' try { const res = await fetch('/api/blobs', { method: 'POST', headers: { 'X-Device-Token': getToken(), 'X-Blob-Kind': 'clip', 'Content-Type': 'text/plain; charset=utf-8' }, body: text, }) if (!res.ok) throw new Error(await res.text()) result.textContent = 'Sent.' } catch (err) { result.textContent = `Failed: ${err.message || err}` } }) // --- pull clip --- let lastPulled = '' $('pullClip').addEventListener('click', async () => { const result = $('pullResult') result.textContent = 'Fetching…' try { const res = await fetch('/api/clip/latest', { headers: { 'X-Device-Token': getToken() } }) if (res.status === 204) { result.textContent = 'No clip yet.' $('copyPulled').disabled = true return } if (!res.ok) throw new Error(await res.text()) lastPulled = await res.text() result.textContent = lastPulled $('copyPulled').disabled = false } catch (err) { result.textContent = `Failed: ${err.message || err}` } }) $('copyPulled').addEventListener('click', async () => { try { await navigator.clipboard.writeText(lastPulled) toast('Copied.') } catch { toast('Clipboard write not available.') } }) // --- push shot --- $('pushShot').addEventListener('click', async () => { const file = $('shotFile').files[0] const result = $('shotResult') if (!file) { result.textContent = 'Pick an image first.' return } result.textContent = 'Uploading…' try { const res = await fetch('/api/blobs', { method: 'POST', headers: { 'X-Device-Token': getToken(), 'X-Blob-Kind': 'shot', 'Content-Type': file.type || 'application/octet-stream' }, body: file, }) if (!res.ok) throw new Error(await res.text()) const { url } = await res.json() const fullUrl = new URL(url, location.origin).toString() result.innerHTML = `${fullUrl}` try { await navigator.clipboard.writeText(fullUrl) toast('Link copied to clipboard.') } catch { /* clipboard write may be unavailable; link is still shown above */ } } catch (err) { result.textContent = `Failed: ${err.message || err}` } })