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,
|
||||
"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)
|
||||
}
|
||||
|
|
|
|||
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>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<<<<<<< HEAD
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
|
@ -64,6 +65,95 @@
|
|||
<span>600%</span>
|
||||
</div>
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
146
popup.js
146
popup.js
|
|
@ -1,3 +1,4 @@
|
|||
<<<<<<< HEAD
|
||||
const slider = document.getElementById('slider');
|
||||
const display = document.getElementById('display');
|
||||
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