diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6fe73ff
--- /dev/null
+++ b/README.md
@@ -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 |
diff --git a/background.js b/background.js
new file mode 100644
index 0000000..5b6b3af
--- /dev/null
+++ b/background.js
@@ -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(() => {});
+});
diff --git a/icons/icon.svg b/icons/icon.svg
new file mode 100644
index 0000000..4183a92
--- /dev/null
+++ b/icons/icon.svg
@@ -0,0 +1,34 @@
+
diff --git a/icons/icon128.png b/icons/icon128.png
new file mode 100644
index 0000000..74e6e79
Binary files /dev/null and b/icons/icon128.png differ
diff --git a/icons/icon16.png b/icons/icon16.png
new file mode 100644
index 0000000..7f16d3c
Binary files /dev/null and b/icons/icon16.png differ
diff --git a/icons/icon32.png b/icons/icon32.png
new file mode 100644
index 0000000..74f6239
Binary files /dev/null and b/icons/icon32.png differ
diff --git a/icons/icon48.png b/icons/icon48.png
new file mode 100644
index 0000000..72b7db6
Binary files /dev/null and b/icons/icon48.png differ
diff --git a/manifest.json b/manifest.json
index 1db0585..c095b4b 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,6 +1,7 @@
{
"manifest_version": 3,
"name": "Volume Booster",
+<<<<<<< HEAD
"version": "1.0",
"description": "Boost audio up to 600%",
"permissions": ["activeTab", "storage"],
@@ -14,4 +15,29 @@
"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)
}
diff --git a/offscreen.html b/offscreen.html
new file mode 100644
index 0000000..5094571
--- /dev/null
+++ b/offscreen.html
@@ -0,0 +1,8 @@
+
+
+
Volume Booster — Offscreen
+
+
+
+
+
diff --git a/offscreen.js b/offscreen.js
new file mode 100644
index 0000000..4c3c706
--- /dev/null
+++ b/offscreen.js
@@ -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
+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
+});
diff --git a/popup.css b/popup.css
new file mode 100644
index 0000000..5ff046e
--- /dev/null
+++ b/popup.css
@@ -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); }
diff --git a/popup.html b/popup.html
index 3b5998d..9799242 100644
--- a/popup.html
+++ b/popup.html
@@ -1,6 +1,7 @@
+<<<<<<< HEAD