855 lines
26 KiB
JavaScript

(async () => {
const existing = document.getElementById('live-overlay-root')
if (existing) {
existing.remove()
if (window.__liveOverlayCleanup) {
try {
window.__liveOverlayCleanup()
} catch (err) {
console.warn('LiveOverlay cleanup error', err)
}
delete window.__liveOverlayCleanup
}
return
}
const DEFAULTS = {
overlayUrl: '',
opacity: 0.6,
invertEnabled: false,
locked: false,
clickThrough: false,
gridEnabled: false,
position: { x: 48, y: 48 },
size: { width: 1200, height: 800 },
}
const iconNames = [
'eye',
'eye-slash',
'lock-closed',
'lock-open',
'hand',
'adjustments-horizontal',
'moon',
'arrows-pointing-out',
'arrows-pointing-in',
'arrows-up-down-left-right',
'squares-2x2',
'arrow-path',
'question-mark-circle',
]
const iconCache = {}
async function loadIcon(name) {
if (iconCache[name]) return iconCache[name]
const res = await fetch(chrome.runtime.getURL(`icons/ui/${name}.svg`))
const text = await res.text()
iconCache[name] = text
return text
}
await Promise.all(iconNames.map(loadIcon))
const stored = await new Promise((resolve) => {
chrome.storage.local.get(DEFAULTS, (value) => {
if (chrome.runtime.lastError) {
console.warn('LiveOverlay storage get error', chrome.runtime.lastError)
resolve({ ...DEFAULTS })
return
}
resolve(value)
})
})
const state = {
...DEFAULTS,
...stored,
position: { ...DEFAULTS.position, ...(stored.position || {}) },
size: { ...DEFAULTS.size, ...(stored.size || {}) },
}
if (!state.overlayUrl) {
state.overlayUrl = window.location.href
}
let overlayVisible = true
let fullscreen = false
let savedState = { size: { ...state.size }, position: { ...state.position } }
let currentFrameUrl = state.overlayUrl
const pendingSave = {}
let saveTimer = null
function scheduleSave(partial) {
Object.assign(pendingSave, partial)
if (saveTimer) {
clearTimeout(saveTimer)
}
saveTimer = setTimeout(() => {
chrome.storage.local.set(pendingSave, () => {
if (chrome.runtime.lastError) {
console.warn('LiveOverlay storage set error', chrome.runtime.lastError)
}
})
for (const key of Object.keys(pendingSave)) {
delete pendingSave[key]
}
saveTimer = null
}, 200)
}
function setButtonIcon(button, iconName) {
button.innerHTML = iconCache[iconName] || ''
const svg = button.querySelector('svg')
if (svg) {
svg.setAttribute('aria-hidden', 'true')
svg.setAttribute('focusable', 'false')
svg.setAttribute('width', '20')
svg.setAttribute('height', '20')
svg.setAttribute('stroke', 'currentColor')
if (!svg.getAttribute('fill')) {
svg.setAttribute('fill', 'none')
}
}
}
function createButton(label, options = {}) {
const { toggle = true } = options
const button = document.createElement('button')
button.type = 'button'
button.className = 'lo-btn lo-control'
button.setAttribute('aria-label', label)
button.title = label
if (toggle) {
button.setAttribute('aria-pressed', 'false')
}
return button
}
function applyFrameOptions() {
iframe.style.opacity = String(state.opacity)
iframe.style.filter = state.invertEnabled ? 'invert(1)' : 'none'
iframe.style.pointerEvents = state.clickThrough ? 'none' : 'auto'
iframe.setAttribute('aria-hidden', overlayVisible ? 'false' : 'true')
if (state.overlayUrl !== currentFrameUrl) {
currentFrameUrl = state.overlayUrl
iframe.src = state.overlayUrl
}
root.classList.toggle('lo-grid-on', !!state.gridEnabled)
root.classList.toggle('lo-click-through', !!state.clickThrough)
root.classList.toggle('lo-locked', !!state.locked)
opacityValue.textContent = Math.round(state.opacity * 100) + '%'
opacitySlider.value = String(Math.round(state.opacity * 100))
opacitySlider.setAttribute('aria-valuenow', String(Math.round(state.opacity * 100)))
urlInput.value = state.overlayUrl
updateToggleIcons()
}
function applySizeAndPosition() {
const maxWidth = Math.max(document.documentElement.clientWidth, window.innerWidth)
const maxHeight = Math.max(document.documentElement.clientHeight, window.innerHeight)
if (!fullscreen) {
state.size.width = Math.min(Math.max(state.size.width, 320), maxWidth)
state.size.height = Math.min(Math.max(state.size.height, 240), maxHeight)
const clampedWidth = Math.min(state.size.width, maxWidth)
const clampedHeight = Math.min(state.size.height, maxHeight)
state.position.x = Math.min(Math.max(state.position.x, 0), Math.max(0, maxWidth - clampedWidth))
state.position.y = Math.min(Math.max(state.position.y, 0), Math.max(0, maxHeight - clampedHeight))
}
root.style.width = fullscreen ? '100vw' : `${state.size.width}px`
root.style.height = fullscreen ? '100vh' : `${state.size.height}px`
root.style.left = fullscreen ? '0' : `${state.position.x}px`
root.style.top = fullscreen ? '0' : `${state.position.y}px`
root.classList.toggle('lo-fullscreen', fullscreen)
}
function adjustPosition(deltaX, deltaY) {
if (state.locked || fullscreen) return
state.position.x += deltaX
state.position.y += deltaY
applySizeAndPosition()
scheduleSave({ position: { ...state.position } })
if (!fullscreen) {
savedState.position = { ...state.position }
}
}
function updateToggleIcons() {
lockButton.classList.toggle('is-active', state.locked)
lockButton.setAttribute('aria-pressed', String(state.locked))
setButtonIcon(lockButton, state.locked ? 'lock-closed' : 'lock-open')
clickThroughButton.classList.toggle('is-active', state.clickThrough)
clickThroughButton.setAttribute('aria-pressed', String(state.clickThrough))
invertButton.classList.toggle('is-active', state.invertEnabled)
invertButton.setAttribute('aria-pressed', String(state.invertEnabled))
gridButton.classList.toggle('is-active', state.gridEnabled)
gridButton.setAttribute('aria-pressed', String(state.gridEnabled))
toggleVisibilityButton.classList.toggle('is-active', overlayVisible)
toggleVisibilityButton.setAttribute('aria-pressed', String(overlayVisible))
setButtonIcon(toggleVisibilityButton, overlayVisible ? 'eye' : 'eye-slash')
resizeButton.classList.toggle('is-active', fullscreen)
resizeButton.setAttribute('aria-pressed', String(fullscreen))
setButtonIcon(resizeButton, fullscreen ? 'arrows-pointing-in' : 'arrows-pointing-out')
}
const root = document.createElement('div')
root.id = 'live-overlay-root'
root.setAttribute('role', 'region')
root.setAttribute('aria-label', 'Live overlay container')
const style = document.createElement('style')
style.textContent = `
#live-overlay-root {
position: fixed;
top: 48px;
left: 48px;
display: flex;
flex-direction: column;
max-width: 100vw;
max-height: 100vh;
background: rgba(15, 23, 42, 0.85);
color: #f8fafc;
border-radius: 12px;
box-shadow: 0 20px 45px rgba(15, 23, 42, 0.35);
z-index: 2147483647;
border: 1px solid rgba(148, 163, 184, 0.45);
overflow: hidden;
backdrop-filter: blur(4px);
}
#live-overlay-root.lo-click-through iframe {
pointer-events: none;
}
#live-overlay-root.lo-locked .lo-toolbar {
cursor: default;
}
#live-overlay-root.lo-locked .lo-title {
cursor: default;
opacity: 0.7;
}
#live-overlay-root .lo-toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
background: rgba(15, 23, 42, 0.92);
border-bottom: 1px solid rgba(148, 163, 184, 0.35);
user-select: none;
touch-action: none;
position: relative;
flex-wrap: wrap;
row-gap: 6px;
}
#live-overlay-root .lo-title {
font-size: 12px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
margin-right: 6px;
cursor: move;
display: inline-flex;
align-items: center;
gap: 6px;
}
#live-overlay-root .lo-toolbar button.lo-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 8px;
background: transparent;
color: inherit;
border: 1px solid transparent;
transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
cursor: pointer;
padding: 0;
}
#live-overlay-root .lo-toolbar button.lo-btn:hover,
#live-overlay-root .lo-toolbar button.lo-btn.is-active {
background: rgba(96, 165, 250, 0.18);
border-color: rgba(148, 197, 255, 0.4);
}
#live-overlay-root .lo-toolbar button.lo-btn:focus-visible {
outline: 2px solid #22d3ee;
outline-offset: 2px;
}
#live-overlay-root .lo-toolbar button.lo-btn svg {
pointer-events: none;
}
#live-overlay-root .lo-slider {
display: inline-flex;
align-items: center;
gap: 6px;
margin-left: 6px;
}
#live-overlay-root .lo-slider input[type="range"] {
width: 120px;
accent-color: #6366f1;
}
#live-overlay-root .lo-slider span {
font-size: 12px;
min-width: 36px;
text-align: right;
}
#live-overlay-root .lo-url {
display: inline-flex;
align-items: center;
gap: 6px;
flex: 1;
min-width: 160px;
}
#live-overlay-root .lo-url input {
flex: 1;
min-width: 160px;
padding: 6px 8px;
border-radius: 8px;
border: 1px solid rgba(148, 163, 184, 0.35);
background: rgba(15, 23, 42, 0.65);
color: inherit;
font-size: 12px;
}
#live-overlay-root .lo-url input:focus-visible {
outline: 2px solid #22d3ee;
outline-offset: 2px;
}
#live-overlay-root .lo-frame {
position: relative;
flex: 1 1 auto;
background: rgba(15, 23, 42, 0.85);
min-height: 160px;
}
#live-overlay-root.lo-fullscreen {
border-radius: 0;
border: none;
}
#live-overlay-root.lo-fullscreen .lo-toolbar {
border-radius: 0;
}
#live-overlay-root iframe {
width: 100%;
height: 100%;
border: none;
background: #0f172a;
}
#live-overlay-root .lo-grid {
position: absolute;
inset: 0;
pointer-events: none;
background-image: linear-gradient(to right, rgba(148, 163, 184, 0.3) 1px, transparent 1px),
linear-gradient(to bottom, rgba(148, 163, 184, 0.3) 1px, transparent 1px);
background-size: 48px 48px;
opacity: 0;
transition: opacity 0.2s ease;
}
#live-overlay-root.lo-grid-on .lo-grid {
opacity: 1;
}
#live-overlay-root .lo-resize {
position: absolute;
width: 18px;
height: 18px;
right: 6px;
bottom: 6px;
border-right: 2px solid rgba(148, 197, 255, 0.8);
border-bottom: 2px solid rgba(148, 197, 255, 0.8);
border-radius: 2px;
cursor: se-resize;
pointer-events: auto;
}
#live-overlay-root.lo-locked .lo-resize {
display: none;
}
#live-overlay-root .lo-nudge-panel {
position: absolute;
top: calc(100% + 6px);
right: 10px;
display: none;
flex-direction: column;
gap: 4px;
background: rgba(15, 23, 42, 0.95);
border: 1px solid rgba(148, 163, 184, 0.45);
border-radius: 10px;
padding: 8px;
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.35);
z-index: 2;
}
#live-overlay-root .lo-nudge-panel.is-open {
display: flex;
}
#live-overlay-root .lo-nudge-grid {
display: grid;
grid-template-columns: repeat(3, 28px);
gap: 4px;
justify-items: center;
margin-top: 4px;
}
#live-overlay-root .lo-nudge-grid button {
width: 28px;
height: 28px;
border-radius: 6px;
background: rgba(96, 165, 250, 0.15);
border: 1px solid transparent;
color: inherit;
cursor: pointer;
font-size: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
}
#live-overlay-root .lo-nudge-grid span {
width: 28px;
height: 28px;
display: block;
}
#live-overlay-root .lo-nudge-grid button:hover {
background: rgba(96, 165, 250, 0.25);
}
#live-overlay-root .lo-nudge-grid button:focus-visible {
outline: 2px solid #22d3ee;
outline-offset: 1px;
}
#live-overlay-root .lo-nudge-wrap {
position: relative;
display: inline-flex;
align-items: center;
}
`
document.head.appendChild(style)
const toolbar = document.createElement('div')
toolbar.className = 'lo-toolbar'
toolbar.setAttribute('role', 'toolbar')
toolbar.setAttribute('aria-label', 'Live overlay controls')
const title = document.createElement('div')
title.className = 'lo-title'
title.textContent = 'Live Overlay'
toolbar.appendChild(title)
const toggleVisibilityButton = createButton('Toggle overlay visibility')
setButtonIcon(toggleVisibilityButton, 'eye')
const lockButton = createButton('Toggle overlay lock')
const clickThroughButton = createButton('Toggle click-through mode')
setButtonIcon(clickThroughButton, 'hand')
const invertButton = createButton('Invert overlay colors')
setButtonIcon(invertButton, 'moon')
const resizeButton = createButton('Toggle overlay size')
setButtonIcon(resizeButton, 'arrows-pointing-out')
const nudgeWrap = document.createElement('div')
nudgeWrap.className = 'lo-nudge-wrap'
const nudgeButton = createButton('Open nudge controls')
setButtonIcon(nudgeButton, 'arrows-up-down-left-right')
const gridButton = createButton('Toggle alignment grid')
setButtonIcon(gridButton, 'squares-2x2')
const resetButton = createButton('Reset overlay settings', { toggle: false })
setButtonIcon(resetButton, 'arrow-path')
const helpButton = createButton('Help and usage tips', { toggle: false })
setButtonIcon(helpButton, 'question-mark-circle')
const sliderWrap = document.createElement('label')
sliderWrap.className = 'lo-slider'
sliderWrap.setAttribute('aria-label', 'Overlay opacity control')
sliderWrap.title = 'Adjust overlay opacity'
const sliderIcon = document.createElement('span')
sliderIcon.className = 'lo-slider-icon'
sliderIcon.innerHTML = iconCache['adjustments-horizontal']
const sliderIconSvg = sliderIcon.querySelector('svg')
if (sliderIconSvg) {
sliderIconSvg.setAttribute('aria-hidden', 'true')
sliderIconSvg.setAttribute('focusable', 'false')
sliderIconSvg.setAttribute('width', '20')
sliderIconSvg.setAttribute('height', '20')
sliderIconSvg.setAttribute('stroke', 'currentColor')
if (!sliderIconSvg.getAttribute('fill')) {
sliderIconSvg.setAttribute('fill', 'none')
}
}
const opacitySlider = document.createElement('input')
opacitySlider.className = 'lo-control'
opacitySlider.type = 'range'
opacitySlider.min = '0'
opacitySlider.max = '100'
opacitySlider.step = '1'
opacitySlider.value = String(Math.round(state.opacity * 100))
opacitySlider.setAttribute('aria-valuemin', '0')
opacitySlider.setAttribute('aria-valuemax', '100')
opacitySlider.setAttribute('aria-valuenow', String(Math.round(state.opacity * 100)))
opacitySlider.setAttribute('aria-label', 'Overlay opacity')
const opacityValue = document.createElement('span')
opacityValue.textContent = Math.round(state.opacity * 100) + '%'
sliderWrap.appendChild(sliderIcon)
sliderWrap.appendChild(opacitySlider)
sliderWrap.appendChild(opacityValue)
const urlWrap = document.createElement('div')
urlWrap.className = 'lo-url'
const urlInput = document.createElement('input')
urlInput.type = 'url'
urlInput.placeholder = 'https://overlay.example.com'
urlInput.value = state.overlayUrl
urlInput.setAttribute('aria-label', 'Overlay URL')
urlWrap.appendChild(urlInput)
toolbar.appendChild(toggleVisibilityButton)
toolbar.appendChild(lockButton)
toolbar.appendChild(clickThroughButton)
toolbar.appendChild(invertButton)
toolbar.appendChild(resizeButton)
toolbar.appendChild(nudgeWrap)
toolbar.appendChild(gridButton)
toolbar.appendChild(resetButton)
toolbar.appendChild(helpButton)
toolbar.appendChild(sliderWrap)
toolbar.appendChild(urlWrap)
const frameWrap = document.createElement('div')
frameWrap.className = 'lo-frame'
const iframe = document.createElement('iframe')
iframe.src = currentFrameUrl
iframe.title = 'Live overlay frame'
iframe.allow = 'clipboard-read; clipboard-write'
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-pointer-lock')
frameWrap.appendChild(iframe)
const gridOverlay = document.createElement('div')
gridOverlay.className = 'lo-grid'
frameWrap.appendChild(gridOverlay)
const resizeHandle = document.createElement('div')
resizeHandle.className = 'lo-resize'
frameWrap.appendChild(resizeHandle)
const nudgePanel = document.createElement('div')
nudgePanel.className = 'lo-nudge-panel'
const nudgeTitle = document.createElement('span')
nudgeTitle.textContent = 'Nudge overlay'
nudgeTitle.style.fontSize = '11px'
nudgeTitle.style.opacity = '0.8'
nudgePanel.appendChild(nudgeTitle)
const nudgeGrid = document.createElement('div')
nudgeGrid.className = 'lo-nudge-grid'
const nudgeButtons = [
{ label: '', empty: true },
{ label: '↑', dx: 0, dy: -1, aria: 'Nudge up 1px' },
{ label: '', empty: true },
{ label: '←', dx: -1, dy: 0, aria: 'Nudge left 1px' },
{ label: 'Reset', dx: 0, dy: 0, reset: true, aria: 'Reset overlay position' },
{ label: '→', dx: 1, dy: 0, aria: 'Nudge right 1px' },
{ label: '', empty: true },
{ label: '↓', dx: 0, dy: 1, aria: 'Nudge down 1px' },
{ label: '', empty: true },
]
nudgeButtons.forEach(({ label, dx, dy, reset, aria, empty }) => {
if (empty) {
const spacer = document.createElement('span')
nudgeGrid.appendChild(spacer)
return
}
const btn = document.createElement('button')
btn.type = 'button'
btn.textContent = label
btn.setAttribute('aria-label', aria || label)
btn.addEventListener('click', () => {
if (reset) {
state.position = { ...DEFAULTS.position }
applySizeAndPosition()
scheduleSave({ position: { ...state.position } })
return
}
adjustPosition(dx, dy)
})
nudgeGrid.appendChild(btn)
})
nudgePanel.appendChild(nudgeGrid)
const nudgeHint = document.createElement('span')
nudgeHint.textContent = 'Hold Shift for 10px steps'
nudgeHint.style.fontSize = '10px'
nudgeHint.style.opacity = '0.65'
nudgeHint.style.textAlign = 'center'
nudgePanel.appendChild(nudgeHint)
nudgeWrap.appendChild(nudgeButton)
nudgeWrap.appendChild(nudgePanel)
root.appendChild(toolbar)
root.appendChild(frameWrap)
document.body.appendChild(root)
applySizeAndPosition()
applyFrameOptions()
updateToggleIcons()
let dragActive = false
let dragStart = { x: 0, y: 0 }
let origin = { x: 0, y: 0 }
function handlePointerDown(event) {
if (state.locked || fullscreen) return
if (event.button !== 0) return
if (event.target.closest('.lo-control')) return
dragActive = true
dragStart = { x: event.clientX, y: event.clientY }
origin = { ...state.position }
title.setPointerCapture(event.pointerId)
event.preventDefault()
}
function handlePointerMove(event) {
if (!dragActive) return
const deltaX = event.clientX - dragStart.x
const deltaY = event.clientY - dragStart.y
state.position.x = origin.x + deltaX
state.position.y = origin.y + deltaY
applySizeAndPosition()
}
function handlePointerUp(event) {
if (!dragActive) return
dragActive = false
title.releasePointerCapture(event.pointerId)
scheduleSave({ position: { ...state.position } })
}
title.addEventListener('pointerdown', handlePointerDown)
title.addEventListener('pointermove', handlePointerMove)
title.addEventListener('pointerup', handlePointerUp)
title.addEventListener('pointercancel', handlePointerUp)
let resizing = false
let resizeStart = { x: 0, y: 0 }
let sizeOrigin = { width: 0, height: 0 }
resizeHandle.addEventListener('pointerdown', (event) => {
if (state.locked || fullscreen) return
resizing = true
resizeStart = { x: event.clientX, y: event.clientY }
sizeOrigin = { ...state.size }
resizeHandle.setPointerCapture(event.pointerId)
event.preventDefault()
})
resizeHandle.addEventListener('pointermove', (event) => {
if (!resizing) return
const deltaX = event.clientX - resizeStart.x
const deltaY = event.clientY - resizeStart.y
state.size.width = sizeOrigin.width + deltaX
state.size.height = sizeOrigin.height + deltaY
applySizeAndPosition()
})
function finishResize(event) {
if (!resizing) return
resizing = false
resizeHandle.releasePointerCapture(event.pointerId)
scheduleSave({ size: { ...state.size } })
if (!fullscreen) {
savedState.size = { ...state.size }
}
}
resizeHandle.addEventListener('pointerup', finishResize)
resizeHandle.addEventListener('pointercancel', finishResize)
toggleVisibilityButton.addEventListener('click', () => {
overlayVisible = !overlayVisible
frameWrap.style.display = overlayVisible ? 'block' : 'none'
iframe.setAttribute('aria-hidden', overlayVisible ? 'false' : 'true')
if (!overlayVisible) {
nudgePanel.classList.remove('is-open')
nudgeButton.classList.remove('is-active')
nudgeButton.setAttribute('aria-pressed', 'false')
}
updateToggleIcons()
})
lockButton.addEventListener('click', () => {
state.locked = !state.locked
applyFrameOptions()
if (state.locked) {
nudgePanel.classList.remove('is-open')
nudgeButton.classList.remove('is-active')
nudgeButton.setAttribute('aria-pressed', 'false')
}
scheduleSave({ locked: state.locked })
})
clickThroughButton.addEventListener('click', () => {
state.clickThrough = !state.clickThrough
applyFrameOptions()
scheduleSave({ clickThrough: state.clickThrough })
})
invertButton.addEventListener('click', () => {
state.invertEnabled = !state.invertEnabled
applyFrameOptions()
scheduleSave({ invertEnabled: state.invertEnabled })
})
resizeButton.addEventListener('click', () => {
fullscreen = !fullscreen
if (fullscreen) {
savedSize = { size: { ...state.size }, position: { ...state.position } }
state.position = { x: 0, y: 0 }
} else {
state.size = { ...savedSize.size }
state.position = { ...savedSize.position }
}
applySizeAndPosition()
updateToggleIcons()
scheduleSave({ size: { ...state.size }, position: { ...state.position } })
if (!fullscreen) {
savedState = { size: { ...state.size }, position: { ...state.position } }
}
})
nudgeButton.addEventListener('click', () => {
if (state.locked) return
const isOpen = !nudgePanel.classList.contains('is-open')
nudgePanel.classList.toggle('is-open', isOpen)
nudgeButton.classList.toggle('is-active', isOpen)
nudgeButton.setAttribute('aria-pressed', String(isOpen))
})
gridButton.addEventListener('click', () => {
state.gridEnabled = !state.gridEnabled
applyFrameOptions()
scheduleSave({ gridEnabled: state.gridEnabled })
})
resetButton.addEventListener('click', () => {
Object.assign(state, JSON.parse(JSON.stringify(DEFAULTS)))
state.overlayUrl = window.location.href
currentFrameUrl = state.overlayUrl
savedState = { size: { ...state.size }, position: { ...state.position } }
overlayVisible = true
fullscreen = false
frameWrap.style.display = 'block'
nudgePanel.classList.remove('is-open')
nudgeButton.classList.remove('is-active')
nudgeButton.setAttribute('aria-pressed', 'false')
applySizeAndPosition()
applyFrameOptions()
updateToggleIcons()
const resetPayload = JSON.parse(JSON.stringify({
...DEFAULTS,
overlayUrl: state.overlayUrl,
position: { ...state.position },
size: { ...state.size },
}))
scheduleSave(resetPayload)
})
helpButton.addEventListener('click', () => {
const message = [
'Live Overlay tips:',
'• Drag the title bar to reposition.',
'• Use the resize corner or the expand button for full screen.',
'• Toggle click-through mode to interact with the page below.',
'• Settings persist automatically via chrome.storage.local.'
].join('\n')
alert(message)
})
opacitySlider.addEventListener('input', () => {
const value = Number(opacitySlider.value) / 100
state.opacity = Math.min(Math.max(value, 0), 1)
opacitySlider.setAttribute('aria-valuenow', String(Math.round(state.opacity * 100)))
applyFrameOptions()
})
opacitySlider.addEventListener('change', () => {
scheduleSave({ opacity: state.opacity })
})
urlInput.addEventListener('change', () => {
let url = urlInput.value.trim()
if (url && !/^https?:\/\//i.test(url)) {
url = `https://${url}`
urlInput.value = url
}
state.overlayUrl = url || window.location.href
applyFrameOptions()
scheduleSave({ overlayUrl: state.overlayUrl })
})
urlInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault()
urlInput.dispatchEvent(new Event('change'))
urlInput.blur()
}
})
function handleKeydown(event) {
if (state.locked || fullscreen) return
const step = event.shiftKey ? 10 : 1
switch (event.key) {
case 'ArrowUp':
adjustPosition(0, -step)
event.preventDefault()
break
case 'ArrowDown':
adjustPosition(0, step)
event.preventDefault()
break
case 'ArrowLeft':
adjustPosition(-step, 0)
event.preventDefault()
break
case 'ArrowRight':
adjustPosition(step, 0)
event.preventDefault()
break
default:
break
}
}
window.addEventListener('keydown', handleKeydown, true)
window.__liveOverlayCleanup = () => {
window.removeEventListener('keydown', handleKeydown, true)
if (saveTimer) {
clearTimeout(saveTimer)
saveTimer = null
if (Object.keys(pendingSave).length > 0) {
chrome.storage.local.set(pendingSave, () => {
if (chrome.runtime.lastError) {
console.warn('LiveOverlay storage set error', chrome.runtime.lastError)
}
})
for (const key of Object.keys(pendingSave)) {
delete pendingSave[key]
}
}
}
if (style.isConnected) {
style.remove()
}
}
})()