Some fixes
This commit is contained in:
parent
ec216264b1
commit
cf0703f7f5
13 changed files with 798 additions and 0 deletions
58
README.md
Normal file
58
README.md
Normal 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
124
background.js
Normal 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
34
icons/icon.svg
Normal 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
BIN
icons/icon128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
BIN
icons/icon16.png
Normal file
BIN
icons/icon16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 664 B |
BIN
icons/icon32.png
Normal file
BIN
icons/icon32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
icons/icon48.png
Normal file
BIN
icons/icon48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2 KiB |
|
|
@ -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
8
offscreen.html
Normal 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
95
offscreen.js
Normal 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
217
popup.css
Normal 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); }
|
||||||
90
popup.html
90
popup.html
|
|
@ -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
146
popup.js
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue