+75
-47
@@ -22,8 +22,8 @@ // Cross-browser compatibility shim: Firefox exposes `browser`, Chrome exposes `chrome` | ||
| const TARGET_RMS = 0.08; // Target RMS (~-22 dBFS, comfortable speech level) | ||
| const NOISE_FLOOR = 0.005; // Ignore samples quieter than this | ||
| const MAX_GAIN = 5.62; // +15dB absolute ceiling | ||
| const MIN_GAIN = 0.1; // -20dB absolute floor | ||
| const TARGET_RMS = 0.08; // Target RMS (~-22 dBFS, comfortable speech level) | ||
| const NOISE_FLOOR = 0.005; // Ignore samples quieter than this | ||
| const MAX_GAIN = 5.62; // +15dB absolute ceiling | ||
| const MIN_GAIN = 0.1; // -20dB absolute floor | ||
| const LOCK_TC = 30000; // ms - full confidence, lock gain | ||
| const LOCK_TC = 30000; // ms - full confidence, lock gain | ||
| const DRIFT_TC = 3 * 60 * 1000; // ms - slow drift correction period after lock | ||
@@ -36,8 +36,14 @@ | ||
| // Cuts apply faster than boosts to protect against sudden loud audio | ||
| const TC_CUT = 0.15; | ||
| const TC_CUT = 0.15; | ||
| const TC_BOOST = 1.2; | ||
| // Initial and full confidence gain limits for interpolation in gainLimitsForElapsed() | ||
| const GAIN_LIMIT_INITIAL = { maxCutDB: 6, maxBoostDB: 3 }; | ||
| const GAIN_LIMIT_FULL = { maxCutDB: 20, maxBoostDB: 15 }; | ||
| const GAIN_LIMIT_INITIAL = { | ||
| maxCutDB: 6, | ||
| maxBoostDB: 3, | ||
| }; | ||
| const GAIN_LIMIT_FULL = { | ||
| maxCutDB: 20, | ||
| maxBoostDB: 15, | ||
| }; | ||
@@ -56,7 +62,7 @@ let audioCtx = null; | ||
| let measurementSamples = []; | ||
| let playingMs = 0; // cumulative ms of actual playback (excludes paused time) | ||
| let lastTickTime = null; // wall clock at last tick, for incrementing playingMs | ||
| let playingMs = 0; // cumulative ms of actual playback (excludes paused time) | ||
| let lastTickTime = null; // wall clock at last tick, for incrementing playingMs | ||
| let locked = false; | ||
| let currentGain = 1.0; | ||
| let intervalId = null; // setInterval handle for measurementLoop | ||
| let intervalId = null; // setInterval handle for measurementLoop | ||
| let lastDriftCorrection = null; | ||
@@ -67,7 +73,10 @@ | ||
| // Load enabled state from storage with error handling | ||
| browser.storage.local.get("enabled").then(result => { | ||
| enabled = result.enabled !== false; // default true | ||
| }).catch(err => { | ||
| console.warn("[YT Levelr] Failed to load enabled state:", err); | ||
| }); | ||
| browser.storage.local | ||
| .get("enabled") | ||
| .then((result) => { | ||
| enabled = result.enabled !== false; // default true | ||
| }) | ||
| .catch((err) => { | ||
| console.warn("[YT Levelr] Failed to load enabled state:", err); | ||
| }); | ||
@@ -104,3 +113,3 @@ // Listen for messages from popup | ||
| waveform, | ||
| gainLimits: limits | ||
| gainLimits: limits, | ||
| }); | ||
@@ -116,3 +125,3 @@ return true; // keeps the message channel open for sendResponse | ||
| const state = { | ||
| targetRMS: TARGET_RMS | ||
| targetRMS: TARGET_RMS, | ||
| }; | ||
@@ -143,7 +152,11 @@ | ||
| // compressor/gain/analyser nodes without accumulating stale connections. | ||
| try { sourceNode.disconnect(); } catch(e) {} | ||
| try { | ||
| sourceNode.disconnect(); | ||
| } catch { | ||
| // disconnect throws if already disconnected; safe to ignore | ||
| } | ||
| // Gentle compressor to tame transient peaks before gain adjustment | ||
| compressorNode = audioCtx.createDynamicsCompressor(); | ||
| compressorNode.threshold.value = -18; // dB | ||
| compressorNode.threshold.value = -18; // dB | ||
| compressorNode.knee.value = 10; | ||
@@ -161,7 +174,9 @@ compressorNode.ratio.value = 3; | ||
| // Graph: source -> compressor -> gain -> analyser -> destination | ||
| // Graph: source -> compressor -> analyser -> gain -> destination | ||
| // Analyser is upstream of gain so RMS measurements reflect true input level, | ||
| // not the already-adjusted output (which would cause the gain to chase itself). | ||
| sourceNode.connect(compressorNode); | ||
| compressorNode.connect(gainNode); | ||
| gainNode.connect(analyserNode); | ||
| analyserNode.connect(audioCtx.destination); | ||
| compressorNode.connect(analyserNode); | ||
| analyserNode.connect(gainNode); | ||
| gainNode.connect(audioCtx.destination); | ||
@@ -176,3 +191,5 @@ log("Audio graph connected"); | ||
| function getRMS() { | ||
| if (!analyserNode) return 0; | ||
| if (!analyserNode) { | ||
| return 0; | ||
| } | ||
| const buf = new Float32Array(analyserNode.fftSize); | ||
@@ -205,8 +222,8 @@ try { | ||
| function gainLimitsForElapsed(elapsed) { | ||
| const t = Math.min(1, elapsed / LOCK_TC); // 0.0 at start, 1.0 at full confidence | ||
| const maxCutDB = GAIN_LIMIT_INITIAL.maxCutDB + t * (GAIN_LIMIT_FULL.maxCutDB - GAIN_LIMIT_INITIAL.maxCutDB); | ||
| const t = Math.min(1, elapsed / LOCK_TC); // 0.0 at start, 1.0 at full confidence | ||
| const maxCutDB = GAIN_LIMIT_INITIAL.maxCutDB + t * (GAIN_LIMIT_FULL.maxCutDB - GAIN_LIMIT_INITIAL.maxCutDB); | ||
| const maxBoostDB = GAIN_LIMIT_INITIAL.maxBoostDB + t * (GAIN_LIMIT_FULL.maxBoostDB - GAIN_LIMIT_INITIAL.maxBoostDB); | ||
| return { | ||
| min: Math.pow(10, -maxCutDB / 20), | ||
| max: Math.pow(10, maxBoostDB / 20), | ||
| min: Math.pow(10, -maxCutDB / 20), | ||
| max: Math.pow(10, maxBoostDB / 20), | ||
| }; | ||
@@ -218,6 +235,3 @@ } | ||
| const limits = gainLimitsForElapsed(elapsed !== undefined ? elapsed : LOCK_TC); | ||
| const clamped = Math.max( | ||
| Math.max(MIN_GAIN, limits.min), | ||
| Math.min(Math.min(MAX_GAIN, limits.max), g) | ||
| ); | ||
| const clamped = Math.max(Math.max(MIN_GAIN, limits.min), Math.min(Math.min(MAX_GAIN, limits.max), g)); | ||
@@ -240,3 +254,5 @@ const isCut = clamped < currentGain; | ||
| // which callers should treat the same as paused. | ||
| if (!videoEl || videoEl.muted || videoEl.volume < 0.05) return null; | ||
| if (!videoEl || videoEl.muted || videoEl.volume < 0.05) { | ||
| return null; | ||
| } | ||
| return videoEl.volume; | ||
@@ -250,3 +266,5 @@ } | ||
| function measurementLoop() { | ||
| if (!analyserNode || !enabled) return; | ||
| if (!analyserNode || !enabled) { | ||
| return; | ||
| } | ||
@@ -290,3 +308,5 @@ // Resume AudioContext if it was suspended (e.g. browser autoplay policy) | ||
| // Skip silence | ||
| if (trueRMS < NOISE_FLOOR) return; | ||
| if (trueRMS < NOISE_FLOOR) { | ||
| return; | ||
| } | ||
@@ -304,3 +324,5 @@ if (!locked) { | ||
| applyGain(targetGain, playingMs); | ||
| log(`Gain update at ${(playingMs/1000).toFixed(1)}s playing: ${currentGain.toFixed(3)}x (true RMS: ${medianRMS.toFixed(4)}, volume: ${volumeScale.toFixed(2)})`); | ||
| log( | ||
| `Gain update at ${(playingMs / 1000).toFixed(1)}s playing: ${currentGain.toFixed(3)}x (true RMS: ${medianRMS.toFixed(4)}, volume: ${volumeScale.toFixed(2)})`, | ||
| ); | ||
| } | ||
@@ -336,8 +358,8 @@ | ||
| function median(arr) { | ||
| if (arr.length === 0) return 0; | ||
| if (arr.length === 0) { | ||
| return 0; | ||
| } | ||
| const sorted = [...arr].sort((a, b) => a - b); | ||
| const mid = Math.floor(sorted.length / 2); | ||
| return sorted.length % 2 !== 0 | ||
| ? sorted[mid] | ||
| : (sorted[mid - 1] + sorted[mid]) / 2; | ||
| return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2; | ||
| } | ||
@@ -358,3 +380,3 @@ | ||
| // AudioContext construction; a play event satisfies this requirement. | ||
| waitForVideo().then(el => { | ||
| waitForVideo().then((el) => { | ||
| videoEl = el; | ||
@@ -366,3 +388,3 @@ | ||
| setupAudioGraph(videoEl); | ||
| } catch (err) { | ||
| } catch { | ||
| // setupAudioGraph already logged; don't crash the whole listener | ||
@@ -374,3 +396,5 @@ return; | ||
| } | ||
| if (intervalId) clearInterval(intervalId); | ||
| if (intervalId) { | ||
| clearInterval(intervalId); | ||
| } | ||
| intervalId = setInterval(measurementLoop, 300); | ||
@@ -383,3 +407,5 @@ }; | ||
| } else { | ||
| videoEl.addEventListener("play", initGraph, { once: true }); | ||
| videoEl.addEventListener("play", initGraph, { | ||
| once: true, | ||
| }); | ||
| } | ||
@@ -390,6 +416,8 @@ }); | ||
| function waitForVideo() { | ||
| return new Promise(resolve => { | ||
| return new Promise((resolve) => { | ||
| const check = () => { | ||
| const el = document.querySelector("video"); | ||
| if (el) return resolve(el); | ||
| if (el) { | ||
| return resolve(el); | ||
| } | ||
| setTimeout(check, 200); | ||
@@ -396,0 +424,0 @@ }; |
+1
-1
| { | ||
| "manifest_version": 3, | ||
| "name": "YT Levelr", | ||
| "version": "1.3.1", | ||
| "version": "1.3.3", | ||
| "description": "Automatic loudness levelling for YouTube \u2014 useful for podcasts", | ||
@@ -6,0 +6,0 @@ "homepage_url": "https://github.com/AndyP2/yt-levelr", |
+7
-0
@@ -33,2 +33,9 @@ <!DOCTYPE html> | ||
| @media (max-width: 480px) { | ||
| body { | ||
| width: 100%; | ||
| max-width: 100%; | ||
| } | ||
| } | ||
| header { | ||
@@ -35,0 +42,0 @@ padding: 14px 16px 12px; |
+154
-97
@@ -11,3 +11,5 @@ // Cross-browser compatibility shim: Firefox exposes `browser`, Chrome exposes `chrome` | ||
| function rmsToDb(rms) { | ||
| if (rms <= 0) return -96; | ||
| if (rms <= 0) { | ||
| return -96; | ||
| } | ||
| return 20 * Math.log10(rms); | ||
@@ -31,6 +33,5 @@ } | ||
| const canvas = document.getElementById("waveform-canvas"); | ||
| const waveformWrap = document.getElementById("waveform-wrap"); | ||
| const ctx = canvas.getContext("2d"); | ||
| const CW = canvas.width; // 228 | ||
| const CH = canvas.height; // 88 | ||
| const CW = canvas.width; // 228 | ||
| const CH = canvas.height; // 88 | ||
@@ -49,12 +50,12 @@ // Y axis: -48dBFS (bottom) to 0dBFS (top), linear in dB | ||
| // Colours matching CSS vars | ||
| const C_BG = "#1a1a1a"; | ||
| const C_GRID = "#242424"; | ||
| const C_GRID_TEXT = "#444"; | ||
| const C_WAVEFORM = "rgba(200,240,96,0.25)"; | ||
| const C_BG = "#1a1a1a"; | ||
| const C_GRID = "#242424"; | ||
| const C_GRID_TEXT = "#444"; | ||
| const C_WAVEFORM = "rgba(200,240,96,0.25)"; | ||
| const C_WAVEFORM_LINE = "rgba(200,240,96,0.55)"; | ||
| const C_TARGET = "rgba(200,240,96,0.3)"; | ||
| const C_BAND = "rgba(200,240,96,0.07)"; | ||
| const C_BAND_EDGE = "rgba(200,240,96,0.2)"; | ||
| const C_GAIN_LINE = "#c8f060"; | ||
| const C_NULL = "rgba(255,255,255,0.04)"; | ||
| const C_TARGET = "rgba(200,240,96,0.3)"; | ||
| const C_BAND = "rgba(200,240,96,0.07)"; | ||
| const C_BAND_EDGE = "rgba(200,240,96,0.2)"; | ||
| const C_GAIN_LINE = "#c8f060"; | ||
| const C_NULL = "rgba(255,255,255,0.04)"; | ||
@@ -112,6 +113,6 @@ function drawWaveform(state) { | ||
| const targetDb = rmsToDb(state.targetRMS); | ||
| const cutDB = gainToDb(1 / state.gainLimits.min); | ||
| const boostDB = gainToDb(state.gainLimits.max); | ||
| const bandTop = dbToY(targetDb + boostDB); | ||
| const bandBot = dbToY(targetDb - cutDB); | ||
| const cutDB = gainToDb(1 / state.gainLimits.min); | ||
| const boostDB = gainToDb(state.gainLimits.max); | ||
| const bandTop = dbToY(targetDb + boostDB); | ||
| const bandBot = dbToY(targetDb - cutDB); | ||
@@ -125,4 +126,6 @@ ctx.fillStyle = C_BAND; | ||
| ctx.beginPath(); | ||
| ctx.moveTo(0, bandTop); ctx.lineTo(CW, bandTop); | ||
| ctx.moveTo(0, bandBot); ctx.lineTo(CW, bandBot); | ||
| ctx.moveTo(0, bandTop); | ||
| ctx.lineTo(CW, bandTop); | ||
| ctx.moveTo(0, bandBot); | ||
| ctx.lineTo(CW, bandBot); | ||
| ctx.stroke(); | ||
@@ -136,3 +139,6 @@ ctx.setLineDash([]); | ||
| for (let i = 0; i < N; i++) { | ||
| if (waveform[i] === null) { started = false; continue; } | ||
| if (waveform[i] === null) { | ||
| started = false; | ||
| continue; | ||
| } | ||
| const x = i * colW; | ||
@@ -159,7 +165,14 @@ const y = dbToY(rmsToDb(waveform[i])); | ||
| for (let i = 0; i < N; i++) { | ||
| if (waveform[i] === null) { started = false; continue; } | ||
| if (waveform[i] === null) { | ||
| started = false; | ||
| continue; | ||
| } | ||
| const x = i * colW + colW / 2; | ||
| const y = dbToY(rmsToDb(waveform[i])); | ||
| if (!started) { ctx.moveTo(x, y); started = true; } | ||
| else ctx.lineTo(x, y); | ||
| if (!started) { | ||
| ctx.moveTo(x, y); | ||
| started = true; | ||
| } else { | ||
| ctx.lineTo(x, y); | ||
| } | ||
| } | ||
@@ -191,13 +204,13 @@ ctx.strokeStyle = C_WAVEFORM_LINE; | ||
| const toggleEl = document.getElementById("enabled-toggle"); | ||
| const toggleLabel = document.getElementById("toggle-label"); | ||
| const gainDisplay = document.getElementById("gain-display"); | ||
| const gainBar = document.getElementById("gain-bar"); | ||
| const statusDot = document.getElementById("status-dot"); | ||
| const statusText = document.getElementById("status-text"); | ||
| const toggleEl = document.getElementById("enabled-toggle"); | ||
| const toggleLabel = document.getElementById("toggle-label"); | ||
| const gainDisplay = document.getElementById("gain-display"); | ||
| const gainBar = document.getElementById("gain-bar"); | ||
| const statusDot = document.getElementById("status-dot"); | ||
| const statusText = document.getElementById("status-text"); | ||
| const confidenceBar = document.getElementById("confidence-bar"); | ||
| const confidencePct = document.getElementById("confidence-pct"); | ||
| const targetSlider = document.getElementById("target-slider"); | ||
| const targetVal = document.getElementById("target-val"); | ||
| const remeasureBtn = document.getElementById("remeasure-btn"); | ||
| const targetSlider = document.getElementById("target-slider"); | ||
| const targetVal = document.getElementById("target-val"); | ||
| const remeasureBtn = document.getElementById("remeasure-btn"); | ||
@@ -214,4 +227,4 @@ // ---- Confidence ---- | ||
| browser.storage.local.get(["enabled", "targetDB"]).then(result => { | ||
| const enabled = result.enabled !== false; | ||
| browser.storage.local.get(["enabled", "targetDB"]).then((result) => { | ||
| const enabled = result.enabled !== false; | ||
| const targetDB = result.targetDB !== undefined ? result.targetDB : -22; | ||
@@ -229,65 +242,79 @@ | ||
| let pollInterval = null; // Track interval for cleanup | ||
| let lastPollTime = 0; // Last poll timestamp for debouncing | ||
| const POLL_INTERVAL_MS = 1000; // Reduced from 800ms to 1000ms for better performance | ||
| const POLL_INTERVAL_MS = 1000; | ||
| const POLL_DEBOUNCE_MS = 500; // Debounce polling when inactive | ||
| let lastPollTime = 0; // Last poll timestamp for debouncing | ||
| function pollState() { | ||
| browser.tabs.query({ active: true, currentWindow: true }).then(tabs => { | ||
| if (!tabs[0]) return; | ||
| browser.tabs | ||
| .query({ | ||
| active: true, | ||
| }) | ||
| .then((tabs) => { | ||
| if (!tabs[0]) { | ||
| return; | ||
| } | ||
| const now = Date.now(); | ||
| const now = Date.now(); | ||
| // Debounce polling when tab is inactive | ||
| if (now - lastPollTime < POLL_DEBOUNCE_MS) { | ||
| return; | ||
| } | ||
| lastPollTime = now; | ||
| // Debounce polling when tab is inactive | ||
| if (now - lastPollTime < POLL_DEBOUNCE_MS) { | ||
| return; | ||
| } | ||
| lastPollTime = now; | ||
| browser.tabs.sendMessage(tabs[0].id, { type: "getState" }).then(state => { | ||
| if (!state) return; | ||
| browser.tabs | ||
| .sendMessage(tabs[0].id, { | ||
| type: "getState", | ||
| }) | ||
| .then((state) => { | ||
| if (!state) { | ||
| return; | ||
| } | ||
| const gainDb = gainToDb(state.gain); | ||
| gainDisplay.textContent = state.gain.toFixed(2); | ||
| // Restore the unit span safely | ||
| const unitSpan = document.createElement("span"); | ||
| unitSpan.className = "unit"; | ||
| unitSpan.textContent = "x"; | ||
| gainDisplay.appendChild(unitSpan); | ||
| gainBar.style.width = gainToBarPercent(state.gain) + "%"; | ||
| const gainDb = gainToDb(state.gain); | ||
| gainDisplay.textContent = state.gain.toFixed(2); | ||
| // Restore the unit span safely | ||
| const unitSpan = document.createElement("span"); | ||
| unitSpan.className = "unit"; | ||
| unitSpan.textContent = "x"; | ||
| gainDisplay.appendChild(unitSpan); | ||
| gainBar.style.width = gainToBarPercent(state.gain) + "%"; | ||
| if (!state.enabled) { | ||
| statusDot.className = "status-dot off"; | ||
| statusText.textContent = "disabled"; | ||
| confidenceBar.className = "confidence-bar-fill"; | ||
| confidenceBar.style.width = "0%"; | ||
| confidencePct.textContent = "—"; | ||
| } else if (state.locked) { | ||
| statusDot.className = "status-dot locked"; | ||
| statusText.textContent = `locked · ${gainDb >= 0 ? "+" : ""}${gainDb.toFixed(1)} dB`; | ||
| confidenceBar.className = "confidence-bar-fill locked"; | ||
| confidencePct.textContent = "locked"; | ||
| } else { | ||
| const pct = elapsedToConfidence(state.elapsed); | ||
| statusDot.className = "status-dot measuring"; | ||
| statusText.textContent = "measuring…"; | ||
| confidenceBar.className = "confidence-bar-fill"; | ||
| confidenceBar.style.width = pct + "%"; | ||
| confidencePct.textContent = Math.round(pct) + "%"; | ||
| } | ||
| if (!state.enabled) { | ||
| statusDot.className = "status-dot off"; | ||
| statusText.textContent = "disabled"; | ||
| confidenceBar.className = "confidence-bar-fill"; | ||
| confidenceBar.style.width = "0%"; | ||
| confidencePct.textContent = "—"; | ||
| } else if (state.locked) { | ||
| statusDot.className = "status-dot locked"; | ||
| statusText.textContent = `locked · ${gainDb >= 0 ? "+" : ""}${gainDb.toFixed(1)} dB`; | ||
| confidenceBar.className = "confidence-bar-fill locked"; | ||
| confidencePct.textContent = "locked"; | ||
| } else { | ||
| const pct = elapsedToConfidence(state.elapsed); | ||
| statusDot.className = "status-dot measuring"; | ||
| statusText.textContent = "measuring…"; | ||
| confidenceBar.className = "confidence-bar-fill"; | ||
| confidenceBar.style.width = pct + "%"; | ||
| confidencePct.textContent = Math.round(pct) + "%"; | ||
| } | ||
| drawWaveform(state); | ||
| }).catch(err => { | ||
| console.log("[YT Levelr popup] sendMessage failed:", err.message); | ||
| statusDot.className = "status-dot off"; | ||
| statusText.textContent = "not on a YouTube video"; | ||
| confidenceBar.className = "confidence-bar-fill"; | ||
| confidenceBar.style.width = "0%"; | ||
| confidencePct.textContent = "\u2014"; | ||
| drawWaveform({ waveform: new Array(100).fill(null) }); | ||
| drawWaveform(state); | ||
| }) | ||
| .catch((err) => { | ||
| console.log("[YT Levelr popup] sendMessage failed:", err.message); | ||
| statusDot.className = "status-dot off"; | ||
| statusText.textContent = "not on a YouTube video"; | ||
| confidenceBar.className = "confidence-bar-fill"; | ||
| confidenceBar.style.width = "0%"; | ||
| confidencePct.textContent = "\u2014"; | ||
| drawWaveform({ | ||
| waveform: new Array(100).fill(null), | ||
| }); | ||
| }); | ||
| }) | ||
| .catch((err) => { | ||
| console.warn("[YT Levelr popup] tabs.query failed:", err); | ||
| }); | ||
| }).catch(err => { | ||
| console.warn("[YT Levelr popup] tabs.query failed:", err); | ||
| }); | ||
| } | ||
@@ -297,3 +324,3 @@ | ||
| pollState(); | ||
| pollInterval = setInterval(pollState, POLL_INTERVAL_MS); | ||
| setInterval(pollState, POLL_INTERVAL_MS); | ||
@@ -306,6 +333,17 @@ // ---- Controls ---- | ||
| document.body.classList.toggle("disabled", !enabled); | ||
| browser.storage.local.set({ enabled }); | ||
| browser.tabs.query({ active: true, currentWindow: true }).then(tabs => { | ||
| if (tabs[0]) browser.tabs.sendMessage(tabs[0].id, { type: "setEnabled", value: enabled }); | ||
| browser.storage.local.set({ | ||
| enabled, | ||
| }); | ||
| browser.tabs | ||
| .query({ | ||
| active: true, | ||
| }) | ||
| .then((tabs) => { | ||
| if (tabs[0]) { | ||
| browser.tabs.sendMessage(tabs[0].id, { | ||
| type: "setEnabled", | ||
| value: enabled, | ||
| }); | ||
| } | ||
| }); | ||
| }); | ||
@@ -317,12 +355,31 @@ | ||
| const rms = dbToRMS(db); | ||
| browser.storage.local.set({ targetDB: db }); | ||
| browser.tabs.query({ active: true, currentWindow: true }).then(tabs => { | ||
| if (tabs[0]) browser.tabs.sendMessage(tabs[0].id, { type: "setTarget", value: rms }); | ||
| browser.storage.local.set({ | ||
| targetDB: db, | ||
| }); | ||
| browser.tabs | ||
| .query({ | ||
| active: true, | ||
| }) | ||
| .then((tabs) => { | ||
| if (tabs[0]) { | ||
| browser.tabs.sendMessage(tabs[0].id, { | ||
| type: "setTarget", | ||
| value: rms, | ||
| }); | ||
| } | ||
| }); | ||
| }); | ||
| remeasureBtn.addEventListener("click", () => { | ||
| browser.tabs.query({ active: true, currentWindow: true }).then(tabs => { | ||
| if (tabs[0]) browser.tabs.sendMessage(tabs[0].id, { type: "remeasure" }); | ||
| }); | ||
| browser.tabs | ||
| .query({ | ||
| active: true, | ||
| }) | ||
| .then((tabs) => { | ||
| if (tabs[0]) { | ||
| browser.tabs.sendMessage(tabs[0].id, { | ||
| type: "remeasure", | ||
| }); | ||
| } | ||
| }); | ||
| statusDot.className = "status-dot measuring"; | ||
@@ -329,0 +386,0 @@ statusText.textContent = "measuring…"; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet