Files

156 lines
5.0 KiB
JavaScript
Raw Permalink Normal View History

// 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=<token> 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 = `<a href="${fullUrl}" target="_blank" rel="noopener">${fullUrl}</a>`
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}`
}
})