Some fixes

This commit is contained in:
Tronax 2026-05-15 21:11:59 +02:00
parent ec216264b1
commit cf0703f7f5
13 changed files with 798 additions and 0 deletions

58
README.md Normal file
View file

@ -0,0 +1,58 @@
# Volume Booster — Chrome Extension
Ein Volume Booster für Chrome: macht jeden Tab lauter (bis 600 %) oder leiser,
mit stummschalten und Schnellwahl-Presets.
## Installieren
1. ZIP entpacken (falls noch nicht geschehen).
2. In Chrome `chrome://extensions` öffnen.
3. Oben rechts **Entwicklermodus** einschalten.
4. **Entpackte Erweiterung laden** klicken und den Ordner `volume-booster` auswählen.
5. Das Lautsprecher-Icon erscheint in der Toolbar — anpinnen, fertig.
## Benutzen
- Auf das Icon klicken, um das Panel zu öffnen.
- **Slider** ziehen oder die **+ / ** Knöpfe nutzen (10 %-Schritte).
- **Mute**-Knopf (mittig) schaltet stumm und beim erneuten Klick wieder zurück.
- **Presets**: 100 % / 200 % / 400 % / 600 % für einen Klick.
- Pfeiltasten ↑ / ↓ funktionieren ebenfalls, wenn das Panel offen ist.
## Wie es funktioniert
- Bis **100 %** passiert nichts Besonderes — die Seite spielt normal.
- Über **100 %** holt der Service Worker per `chrome.tabCapture.getMediaStreamId`
eine Stream-ID des Tabs und reicht sie an ein **Offscreen-Dokument** weiter.
Dort wird der Stream durch einen Web-Audio `GainNode` geschickt und
verstärkt wieder ausgegeben.
- Der Umweg über das Offscreen-Dokument ist in Manifest V3 nötig: Der
Service Worker hat kein DOM und kann selbst kein Audio verarbeiten.
- Pro Tab wird ein eigener Verstärkungswert gespeichert (für die Session).
- Schließt du den Tab oder lädst ihn neu, wird der Stream sauber freigegeben.
## Hinweise
- Bei sehr hohen Werten kann der Ton übersteuern/verzerren — das ist eine
physikalische Grenze der Quelle, dann einfach etwas zurückdrehen.
- Funktioniert nicht auf internen Seiten wie `chrome://`,
dem Web Store oder anderen Extension-Seiten — dort gibt Chrome keinen
Audio-Stream frei.
- Beim ersten Verstärken eines Tabs muss kurz Audio laufen, damit Chrome
den Stream herausgibt.
- Verstärkung gilt jeweils für den Tab, in dem du sie einstellst.
- Nach einem Update der Extension immer einmal auf `chrome://extensions`
neu laden.
## Dateien
| Datei | Zweck |
|------------------|----------------------------------------------------------|
| `manifest.json` | Extension-Manifest (Manifest V3) |
| `background.js` | Service Worker — holt Stream-ID, steuert Offscreen-Doc |
| `offscreen.html` | Unsichtbares Dokument, Träger des Audio-Codes |
| `offscreen.js` | AudioContext + GainNode — das eigentliche Verstärken |
| `popup.html` | Aufbau des Interfaces |
| `popup.css` | Styling (dunkles Studio-Theme, Amber-Akzent) |
| `popup.js` | Logik, State, Kommunikation mit dem Worker |
| `icons/` | Icon in 16 / 32 / 48 / 128 px + SVG-Quelle |

124
background.js Normal file
View file

@ -0,0 +1,124 @@
// ============================================================
// Volume Booster — Background Service Worker
// In Manifest V3 kann der Service Worker selbst KEIN Audio
// capturen (kein DOM, kein getUserMedia). Er holt nur eine
// Stream-ID via chrome.tabCapture.getMediaStreamId und reicht
// sie an ein Offscreen-Dokument weiter, das den GainNode hält.
// ============================================================
let creating = null; // Promise-Lock, damit nicht zwei Offscreen-Docs entstehen
async function ensureOffscreen() {
const contexts = await chrome.runtime.getContexts({
contextTypes: ["OFFSCREEN_DOCUMENT"],
});
if (contexts.length > 0) return;
if (creating) {
await creating;
return;
}
creating = chrome.offscreen.createDocument({
url: "offscreen.html",
reasons: ["USER_MEDIA"],
justification: "Tab-Audio verstärken (Volume Booster)",
});
await creating;
creating = null;
}
// Nachrichten aus dem Popup
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
// Nachrichten fürs Offscreen-Dokument hier nicht behandeln
if (msg.target === "offscreen") return false;
(async () => {
try {
if (msg.type === "SET_VOLUME") {
const { tabId, value } = msg; // value: 0.0 6.0
await ensureOffscreen();
if (value === 1) {
// Exakt 100 %: Boost beenden, kein Capture nötig
const res = await chrome.runtime.sendMessage({
target: "offscreen",
type: "STOP",
tabId,
});
sendResponse(res || { ok: true, boosting: false });
return;
}
// ERST prüfen, ob dieser Tab schon einen aktiven Stream hat.
// Wenn ja, NUR den Gain ändern — keine neue Stream-ID holen!
// (Eine getMediaStreamId-ID ist einmalig und der Tab kann nicht
// ein zweites Mal gecaptured werden, solange der erste Stream läuft.)
const state = await chrome.runtime.sendMessage({
target: "offscreen",
type: "STATE",
tabId,
});
if (state && state.boosting) {
// Tab läuft schon: nur Lautstärke anpassen
const gainRes = await chrome.runtime.sendMessage({
target: "offscreen",
type: "GAIN",
tabId,
value,
});
if (gainRes && gainRes.ok) {
sendResponse(gainRes);
return;
}
// Falls der Stream zwischendurch verloren ging: unten neu capturen
}
// Tab noch nicht aktiv: jetzt eine frische Stream-ID holen
const streamId = await chrome.tabCapture.getMediaStreamId({
targetTabId: tabId,
});
const applyRes = await chrome.runtime.sendMessage({
target: "offscreen",
type: "APPLY",
tabId,
streamId,
value,
});
sendResponse(
applyRes || { ok: false, error: "Keine Antwort vom Offscreen-Dokument" }
);
} else if (msg.type === "GET_STATE") {
await ensureOffscreen();
const res = await chrome.runtime.sendMessage({
target: "offscreen",
type: "STATE",
tabId: msg.tabId,
});
sendResponse(res || { ok: true, boosting: false, value: 1 });
} else if (msg.type === "RESET") {
await ensureOffscreen();
const res = await chrome.runtime.sendMessage({
target: "offscreen",
type: "STOP",
tabId: msg.tabId,
});
sendResponse(res || { ok: true });
}
} catch (err) {
sendResponse({ ok: false, error: String((err && err.message) || err) });
}
})();
return true; // async sendResponse
});
// Aufräumen, wenn ein Tab geschlossen wird
chrome.tabs.onRemoved.addListener((tabId) => {
chrome.runtime
.sendMessage({ target: "offscreen", type: "STOP", tabId })
.catch(() => {});
});

34
icons/icon.svg Normal file
View file

@ -0,0 +1,34 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#2a2e38"/>
<stop offset="1" stop-color="#15171c"/>
</linearGradient>
<linearGradient id="amber" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#ffce8f"/>
<stop offset="1" stop-color="#ff9d2f"/>
</linearGradient>
</defs>
<!-- gerundeter Hintergrund -->
<rect x="6" y="6" width="116" height="116" rx="28" fill="url(#bg)"/>
<rect x="6.5" y="6.5" width="115" height="115" rx="27.5" fill="none" stroke="#3a3f4a" stroke-width="1"/>
<!-- Lautsprecher -->
<path d="M58 40 L40 54 H26 a4 4 0 0 0 -4 4 v12 a4 4 0 0 0 4 4 h14 l18 14 a3 3 0 0 0 5 -2.4 V42.4 A3 3 0 0 0 58 40 Z"
fill="url(#amber)"/>
<!-- Schallwellen -->
<g fill="none" stroke="url(#amber)" stroke-linecap="round">
<path d="M76 50 a18 18 0 0 1 0 28" stroke-width="7"/>
<path d="M88 40 a34 34 0 0 1 0 48" stroke-width="7"/>
</g>
<!-- Plus-Akzent (Boost) -->
<g stroke="#15171c" stroke-width="8" stroke-linecap="round">
<path d="M101 92 v18 M92 101 h18"/>
</g>
<g stroke="url(#amber)" stroke-width="5" stroke-linecap="round">
<path d="M101 92 v18 M92 101 h18"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
icons/icon128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
icons/icon16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 B

BIN
icons/icon32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
icons/icon48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -1,6 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Volume Booster", "name": "Volume Booster",
<<<<<<< HEAD
"version": "1.0", "version": "1.0",
"description": "Boost audio up to 600%", "description": "Boost audio up to 600%",
"permissions": ["activeTab", "storage"], "permissions": ["activeTab", "storage"],
@ -14,4 +15,29 @@
"js": ["content.js"] "js": ["content.js"]
} }
] ]
=======
"author": "Tronax mail@tronax.dev",
"version": "1.0.0",
"description": "Boost or lower the volume of any browser tab — up to 600%. Mit schönem Interface.",
"permissions": ["activeTab", "tabs", "storage", "tabCapture", "offscreen"],
"action": {
"default_popup": "popup.html",
"default_title": "Volume Booster",
"default_icon": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"icons": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"background": {
"service_worker": "background.js"
}
>>>>>>> 2056a0d (First Commit, Code by Claude 4.7 Opus)
} }

8
offscreen.html Normal file
View file

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html lang="de">
<head><meta charset="UTF-8" /><title>Volume Booster — Offscreen</title></head>
<body>
<!-- Unsichtbar. Trägt nur den Audio-Verarbeitungscode. -->
<script src="offscreen.js"></script>
</body>
</html>

95
offscreen.js Normal file
View file

@ -0,0 +1,95 @@
// ============================================================
// Volume Booster — Offscreen Document
// Läuft in einem unsichtbaren DOM-Kontext. Nur hier sind
// getUserMedia + AudioContext verfügbar. Hält pro Tab einen
// GainNode und gibt den verstärkten Stream wieder aus.
// ============================================================
// Map<tabId, { context, gain, source, stream }>
const active = new Map();
function teardown(tabId) {
const entry = active.get(tabId);
if (!entry) return;
try {
entry.source.disconnect();
entry.gain.disconnect();
entry.stream.getTracks().forEach((t) => t.stop());
entry.context.close();
} catch (e) {
/* trotzdem aufräumen */
}
active.delete(tabId);
}
async function applyBoost(tabId, streamId, value) {
// Falls (z. B. nach Fehler) noch ein alter Eintrag existiert: erst sauber weg.
if (active.has(tabId)) {
teardown(tabId);
}
// Stream aus der ID erzeugen
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
mandatory: {
chromeMediaSource: "tab",
chromeMediaSourceId: streamId,
},
},
video: false,
});
const context = new AudioContext();
const source = context.createMediaStreamSource(stream);
const gain = context.createGain();
gain.gain.value = value;
source.connect(gain);
gain.connect(context.destination); // Audio bleibt für den Nutzer hörbar
// Wenn der Stream endet (Tab neu geladen / geschlossen)
const track = stream.getAudioTracks()[0];
if (track) {
track.addEventListener("ended", () => teardown(tabId));
}
active.set(tabId, { context, gain, source, stream });
return { ok: true, boosting: true, value };
}
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.target !== "offscreen") return false;
(async () => {
try {
if (msg.type === "APPLY") {
const res = await applyBoost(msg.tabId, msg.streamId, msg.value);
sendResponse(res);
} else if (msg.type === "GAIN") {
// Nur die Lautstärke eines bereits aktiven Tabs ändern
const entry = active.get(msg.tabId);
if (entry) {
entry.gain.gain.value = msg.value;
sendResponse({ ok: true, boosting: true, value: msg.value });
} else {
// Kein aktiver Stream (mehr) — Popup soll neu anwenden
sendResponse({ ok: false, error: "no active stream", boosting: false });
}
} else if (msg.type === "STOP") {
teardown(msg.tabId);
sendResponse({ ok: true, boosting: false });
} else if (msg.type === "STATE") {
const entry = active.get(msg.tabId);
sendResponse({
ok: true,
boosting: !!entry,
value: entry ? entry.gain.gain.value : 1,
});
}
} catch (err) {
sendResponse({ ok: false, error: String((err && err.message) || err) });
}
})();
return true; // async
});

217
popup.css Normal file
View file

@ -0,0 +1,217 @@
/* ============================================================
Volume Booster Popup Styles
Aesthetik: dunkles "Audio-Studio"-Panel, warmer Amber-Akzent,
prägnante Typografie, taktile Buttons.
============================================================ */
@import url('https://fonts.googleapis.com/css2?family=Sora:wght@400;600;700&family=DM+Mono:wght@400;500&display=swap');
:root {
--bg: #15171c;
--panel: #1d2027;
--panel-2: #23272f;
--line: #2f343d;
--text: #e8e6e1;
--text-dim: #8b8f99;
--accent: #ff9d2f;
--accent-dk: #e07d12;
--accent-gl: rgba(255, 157, 47, 0.18);
--danger: #ff5d5d;
--radius: 14px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 320px;
font-family: 'Sora', sans-serif;
background:
radial-gradient(120% 80% at 50% -10%, #232730 0%, var(--bg) 60%);
color: var(--text);
-webkit-font-smoothing: antialiased;
user-select: none;
}
.app {
padding: 18px 18px 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
/* ---------- Kopf ---------- */
.head {
display: flex;
align-items: center;
justify-content: space-between;
}
.brand { display: flex; align-items: center; gap: 11px; }
.brand-ico {
width: 34px; height: 34px;
color: var(--accent);
padding: 7px;
background: var(--accent-gl);
border-radius: 10px;
flex-shrink: 0;
}
.brand-text { display: flex; flex-direction: column; line-height: 1.25; }
.brand-name { font-weight: 700; font-size: 14px; letter-spacing: .2px; }
.brand-site {
font-size: 11px;
color: var(--text-dim);
max-width: 150px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.badge {
font-family: 'DM Mono', monospace;
font-size: 12px;
font-weight: 500;
padding: 5px 9px;
border-radius: 8px;
background: var(--panel-2);
border: 1px solid var(--line);
color: var(--text-dim);
transition: all .2s ease;
}
.badge.boosting {
color: var(--accent);
border-color: var(--accent-dk);
background: var(--accent-gl);
}
/* ---------- Großer Wert ---------- */
.readout {
display: flex;
align-items: baseline;
justify-content: center;
gap: 4px;
padding: 6px 0 2px;
}
.readout-value {
font-family: 'DM Mono', monospace;
font-size: 54px;
font-weight: 500;
letter-spacing: -1px;
line-height: 1;
background: linear-gradient(180deg, #fff, #cfcdc7);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
transition: color .15s;
}
.readout.boosting .readout-value {
background: linear-gradient(180deg, #ffd9a0, var(--accent));
-webkit-background-clip: text;
}
.readout-unit {
font-size: 20px;
color: var(--text-dim);
font-weight: 600;
}
/* ---------- Slider ---------- */
.slider-wrap { padding: 0 2px; }
input[type="range"] {
-webkit-appearance: none;
width: 100%;
height: 8px;
border-radius: 6px;
background: var(--panel-2);
border: 1px solid var(--line);
outline: none;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 22px; height: 22px;
border-radius: 50%;
background: radial-gradient(circle at 35% 30%, #ffce8f, var(--accent) 70%);
border: 2px solid #1a1c22;
box-shadow: 0 0 0 1px var(--accent-dk), 0 4px 10px rgba(0,0,0,.5);
transition: transform .12s ease;
}
input[type="range"]::-webkit-slider-thumb:active { transform: scale(1.18); }
.ticks {
display: flex;
justify-content: space-between;
margin-top: 7px;
font-family: 'DM Mono', monospace;
font-size: 10px;
color: var(--text-dim);
}
/* ---------- Steuerknöpfe ---------- */
.controls {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
}
.ctrl {
height: 50px;
border-radius: var(--radius);
border: 1px solid var(--line);
background: linear-gradient(180deg, var(--panel-2), var(--panel));
color: var(--text);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: transform .1s ease, border-color .15s, background .15s;
}
.ctrl svg { width: 22px; height: 22px; }
.ctrl:hover { border-color: var(--accent-dk); }
.ctrl:active { transform: translateY(2px) scale(.97); }
.ctrl-mute { color: var(--text-dim); }
.ctrl-mute.active {
color: var(--danger);
border-color: var(--danger);
background: rgba(255, 93, 93, .12);
}
/* ---------- Presets ---------- */
.presets {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.preset {
font-family: 'DM Mono', monospace;
font-size: 11px;
font-weight: 500;
padding: 8px 4px;
border-radius: 10px;
border: 1px solid var(--line);
background: var(--panel);
color: var(--text-dim);
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
transition: all .15s ease;
}
.preset svg { width: 14px; height: 14px; }
.preset:hover { color: var(--text); border-color: #3c424d; }
.preset.active {
color: var(--accent);
border-color: var(--accent-dk);
background: var(--accent-gl);
}
/* ---------- Hinweis ---------- */
.note {
font-size: 10.5px;
color: var(--text-dim);
text-align: center;
line-height: 1.5;
padding-top: 2px;
}
.note.error { color: var(--danger); }

View file

@ -1,6 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
<<<<<<< HEAD
<meta charset="UTF-8"> <meta charset="UTF-8">
<style> <style>
* { box-sizing: border-box; margin: 0; padding: 0; } * { box-sizing: border-box; margin: 0; padding: 0; }
@ -64,6 +65,95 @@
<span>600%</span> <span>600%</span>
</div> </div>
<button id="reset">↺ Zurücksetzen</button> <button id="reset">↺ Zurücksetzen</button>
=======
<meta charset="UTF-8" />
<title>Volume Booster</title>
<link rel="stylesheet" href="popup.css" />
</head>
<body>
<div class="app">
<!-- Kopfzeile -->
<header class="head">
<div class="brand">
<!-- Icon: Lautsprecher mit Wellen -->
<svg class="brand-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M11 5 6 9H2v6h4l5 4V5z" />
<path d="M15.5 8.5a5 5 0 0 1 0 7" />
<path d="M18.5 5.5a9 9 0 0 1 0 13" />
</svg>
<div class="brand-text">
<span class="brand-name">Volume Booster</span>
<span class="brand-site" id="siteLabel">aktiver Tab</span>
</div>
</div>
<span class="badge" id="statusBadge">100%</span>
</header>
<!-- Großer Wert -->
<div class="readout">
<span class="readout-value" id="readoutValue">100</span>
<span class="readout-unit">%</span>
</div>
<!-- Slider -->
<div class="slider-wrap">
<input type="range" id="slider" min="0" max="600" step="10" value="100" />
<div class="ticks">
<span>0</span><span>100</span><span>300</span><span>600</span>
</div>
</div>
<!-- Steuerknöpfe -->
<div class="controls">
<button class="ctrl" id="btnDown" title="Leiser">
<!-- Minus -->
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round">
<path d="M5 12h14" />
</svg>
</button>
<button class="ctrl ctrl-mute" id="btnMute" title="Stummschalten">
<!-- Mute -->
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M11 5 6 9H2v6h4l5 4V5z" />
<path d="m22 9-6 6" />
<path d="m16 9 6 6" />
</svg>
</button>
<button class="ctrl" id="btnUp" title="Lauter">
<!-- Plus -->
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round">
<path d="M12 5v14M5 12h14" />
</svg>
</button>
</div>
<!-- Schnellwahl -->
<div class="presets">
<button class="preset" data-v="100">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 12a9 9 0 1 0 9-9" /><path d="M3 12V5h7" />
</svg>
100%
</button>
<button class="preset" data-v="200">200%</button>
<button class="preset" data-v="400">400%</button>
<button class="preset" data-v="600">
<!-- Blitz für Maximum -->
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z" />
</svg>
600%
</button>
</div>
<!-- Fußzeile / Hinweis -->
<p class="note" id="note">Über 100 % wird der Tab-Ton verstärkt.</p>
</div>
>>>>>>> 2056a0d (First Commit, Code by Claude 4.7 Opus)
<script src="popup.js"></script> <script src="popup.js"></script>
</body> </body>
</html> </html>

146
popup.js
View file

@ -1,3 +1,4 @@
<<<<<<< HEAD
const slider = document.getElementById('slider'); const slider = document.getElementById('slider');
const display = document.getElementById('display'); const display = document.getElementById('display');
const reset = document.getElementById('reset'); const reset = document.getElementById('reset');
@ -36,3 +37,148 @@ reset.addEventListener('click', () => {
} }
); );
}); });
=======
// ============================================================
// Volume Booster — Popup Logic
// ============================================================
const slider = document.getElementById("slider");
const readout = document.getElementById("readoutValue");
const readoutBox = document.querySelector(".readout");
const badge = document.getElementById("statusBadge");
const siteLabel = document.getElementById("siteLabel");
const note = document.getElementById("note");
const btnUp = document.getElementById("btnUp");
const btnDown = document.getElementById("btnDown");
const btnMute = document.getElementById("btnMute");
const presets = document.querySelectorAll(".preset");
const STEP = 10; // Schrittweite in %
const MAX = 600;
let currentTab = null;
let lastBeforeMute = 100; // Wert vor dem Stummschalten
// ---- Tab ermitteln ----
async function init() {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
currentTab = tab;
if (tab && tab.url) {
try {
siteLabel.textContent = new URL(tab.url).hostname;
} catch {
siteLabel.textContent = "aktiver Tab";
}
}
// Gespeicherten Wert für diesen Tab laden
const key = "vol_" + tab.id;
const stored = await chrome.storage.session.get(key);
const value = stored[key] != null ? stored[key] : 100;
applyUI(value);
// Falls vorher schon ein Boost lief: Zustand vom Worker holen
chrome.runtime.sendMessage({ type: "GET_STATE", tabId: tab.id }, (res) => {
if (res && res.ok && res.boosting) {
applyUI(Math.round(res.value * 100));
}
});
}
// ---- UI aktualisieren ----
function applyUI(value) {
value = Math.max(0, Math.min(MAX, value));
slider.value = value;
readout.textContent = value;
badge.textContent = value + "%";
const boosting = value > 100;
readoutBox.classList.toggle("boosting", boosting);
badge.classList.toggle("boosting", boosting);
btnMute.classList.toggle("active", value === 0);
presets.forEach((p) => {
p.classList.toggle("active", Number(p.dataset.v) === value);
});
}
// ---- Lautstärke setzen + an Worker senden ----
function setVolume(value) {
value = Math.max(0, Math.min(MAX, value));
applyUI(value);
if (!currentTab) return;
const key = "vol_" + currentTab.id;
chrome.storage.session.set({ [key]: value });
chrome.runtime.sendMessage(
{ type: "SET_VOLUME", tabId: currentTab.id, value: value / 100 },
(res) => {
if (res && res.ok) {
note.classList.remove("error");
note.textContent =
value > 100
? "Ton wird verstärkt — bei Verzerrung etwas zurückdrehen."
: value === 0
? "Tab ist stummgeschaltet."
: "Normale Lautstärke (kein Boost aktiv).";
} else {
note.classList.add("error");
const err = ((res && res.error) || "").toLowerCase();
if (err.includes("cannot be captured") || err.includes("invalid tab")) {
note.textContent =
"Dieser Tab kann nicht verstärkt werden (z. B. chrome:// oder Web Store).";
} else if (err.includes("activetab") || err.includes("permission")) {
note.textContent =
"Keine Berechtigung — Popup einmal schließen und neu öffnen.";
} else if (!res) {
note.textContent =
"Keine Antwort — Extension auf chrome://extensions neu laden.";
} else {
note.textContent = "Fehler beim Aktivieren — Seite ggf. neu laden.";
}
// Slider auf 100 zurück, da kein Boost aktiv ist
applyUI(100);
}
}
);
}
// ---- Events ----
slider.addEventListener("input", () => {
applyUI(Number(slider.value));
});
slider.addEventListener("change", () => {
setVolume(Number(slider.value));
});
btnUp.addEventListener("click", () =>
setVolume(Number(slider.value) + STEP)
);
btnDown.addEventListener("click", () =>
setVolume(Number(slider.value) - STEP)
);
btnMute.addEventListener("click", () => {
const v = Number(slider.value);
if (v === 0) {
setVolume(lastBeforeMute || 100);
} else {
lastBeforeMute = v;
setVolume(0);
}
});
presets.forEach((p) => {
p.addEventListener("click", () => setVolume(Number(p.dataset.v)));
});
// Tastatur: Pfeile hoch/runter
document.addEventListener("keydown", (e) => {
if (e.key === "ArrowUp") { e.preventDefault(); setVolume(Number(slider.value) + STEP); }
if (e.key === "ArrowDown") { e.preventDefault(); setVolume(Number(slider.value) - STEP); }
});
init();
>>>>>>> 2056a0d (First Commit, Code by Claude 4.7 Opus)