mirror of
https://github.com/TristanEDU/LiveOverlayExtension.git
synced 2026-01-13 08:08:22 +00:00
855 lines
26 KiB
JavaScript
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()
|
|
}
|
|
}
|
|
})()
|