First Commit, Code by Claude 4.7 Opus
This commit is contained in:
commit
4f1e373c4f
7 changed files with 650 additions and 0 deletions
46
README.md
Normal file
46
README.md
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
# Reels Controls — Video-Steuerung für Instagram Reels
|
||||||
|
|
||||||
|
Ein Chrome-Addon, das Instagram Reels eine echte Videosteuerung gibt: Play/Pause, Lautstärkeregler, Fortschrittsbalken mit Scrubbing und Tastenkürzel.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Diesen Ordner (`reels-controls`) irgendwo dauerhaft ablegen — nicht löschen, Chrome lädt das Addon bei jedem Start von hier.
|
||||||
|
2. In Chrome `chrome://extensions` öffnen.
|
||||||
|
3. Oben rechts den **Entwicklermodus** einschalten.
|
||||||
|
4. Auf **Entpackte Erweiterung laden** klicken und den Ordner `reels-controls` auswählen.
|
||||||
|
5. `instagram.com` öffnen (oder neu laden) und ein Reel aufrufen.
|
||||||
|
|
||||||
|
Funktioniert genauso in Edge, Brave und anderen Chromium-Browsern über deren jeweilige Erweiterungsseite.
|
||||||
|
|
||||||
|
## Bedienung
|
||||||
|
|
||||||
|
Die Steuerleiste sitzt unten über dem Video. Sie ist leicht transparent und wird beim Drüberfahren mit der Maus voll sichtbar.
|
||||||
|
|
||||||
|
- **Play/Pause-Button** — links
|
||||||
|
- **Fortschrittsbalken** — anklicken oder ziehen, um zu spulen
|
||||||
|
- **Zeitanzeige** — aktuelle Position / Gesamtlänge
|
||||||
|
- **Lautstärke-Button** — stummschalten/aktivieren
|
||||||
|
- **Lautstärke-Slider** — stufenlos regeln
|
||||||
|
|
||||||
|
Die zuletzt eingestellte Lautstärke wird gemerkt und auf neue Reels übernommen (für die aktuelle Browser-Sitzung).
|
||||||
|
|
||||||
|
### Tastenkürzel
|
||||||
|
|
||||||
|
Gelten immer für das gerade sichtbare Reel:
|
||||||
|
|
||||||
|
| Taste | Funktion |
|
||||||
|
|-------|----------|
|
||||||
|
| Leertaste | Play / Pause |
|
||||||
|
| M | Stumm an/aus |
|
||||||
|
| ← / → | 5 Sekunden zurück / vor |
|
||||||
|
| ↑ / ↓ | Lautstärke +/− 10 % |
|
||||||
|
|
||||||
|
## Wie es funktioniert
|
||||||
|
|
||||||
|
Instagram ist eine Single-Page-App und tauscht die `<video>`-Elemente beim Scrollen ständig aus. Das Content-Script (`content.js`) nutzt deshalb einen `MutationObserver` plus einen periodischen Fallback-Scan, erkennt neu eingefügte Reel-Videos und hängt jedem eine eigene Steuerleiste an. Alle Klicks auf der Leiste werden mit `stopPropagation()` abgefangen, damit Instagrams eigene Klick-zum-Pausieren-Logik nicht dazwischenfunkt.
|
||||||
|
|
||||||
|
## Hinweise
|
||||||
|
|
||||||
|
- Das Addon braucht **keine** besonderen Berechtigungen außer Zugriff auf `instagram.com` und sendet nichts nach außen — alles läuft lokal im Browser.
|
||||||
|
- Wenn Instagram sein DOM stark umbaut, kann es sein, dass die Video-Erkennung angepasst werden muss (die Heuristik in `isReelVideo()` über Videogröße). Das ist die wahrscheinlichste Stelle für künftige Reparaturen.
|
||||||
|
- Da es eine entpackte Erweiterung ist, zeigt Chrome bei jedem Start evtl. einen Hinweis im Entwicklermodus — das ist normal.
|
||||||
398
content.js
Normal file
398
content.js
Normal file
|
|
@ -0,0 +1,398 @@
|
||||||
|
// ============================================================
|
||||||
|
// Reels Controls — content.js
|
||||||
|
// Hängt eine eigene Videosteuerung an Instagram-Reels-Videos.
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const ATTACHED = "data-rc-attached";
|
||||||
|
const STORAGE_KEY = "rc_volume";
|
||||||
|
|
||||||
|
// Zuletzt genutzte Lautstärke (persistiert über sessionStorage,
|
||||||
|
// damit nicht jedes neue Reel wieder auf 100% / stumm springt).
|
||||||
|
let lastVolume = 1;
|
||||||
|
let lastMuted = false;
|
||||||
|
try {
|
||||||
|
const saved = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || "null");
|
||||||
|
if (saved && typeof saved.volume === "number") {
|
||||||
|
lastVolume = saved.volume;
|
||||||
|
lastMuted = !!saved.muted;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
function persistVolume() {
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(
|
||||||
|
STORAGE_KEY,
|
||||||
|
JSON.stringify({ volume: lastVolume, muted: lastMuted })
|
||||||
|
);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// Erkennen, ob ein <video> ein Reel ist.
|
||||||
|
// Reels liegen in einem hochformatigen Container; wir nehmen
|
||||||
|
// jedes Video, das groß genug ist und im Hauptbereich liegt.
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
function isReelVideo(video) {
|
||||||
|
if (!(video instanceof HTMLVideoElement)) return false;
|
||||||
|
const rect = video.getBoundingClientRect();
|
||||||
|
// Sehr kleine Videos (Avatare, Vorschauen) ignorieren.
|
||||||
|
return rect.height > 250 && rect.width > 150;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtTime(sec) {
|
||||||
|
if (!isFinite(sec) || sec < 0) sec = 0;
|
||||||
|
const m = Math.floor(sec / 60);
|
||||||
|
const s = Math.floor(sec % 60);
|
||||||
|
return m + ":" + String(s).padStart(2, "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
// SVG-Icons inline (keine externen Assets nötig).
|
||||||
|
const ICONS = {
|
||||||
|
play: '<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>',
|
||||||
|
pause:
|
||||||
|
'<svg viewBox="0 0 24 24"><path d="M6 5h4v14H6zM14 5h4v14h-4z"/></svg>',
|
||||||
|
volHigh:
|
||||||
|
'<svg viewBox="0 0 24 24"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3a4.5 4.5 0 0 0-2.5-4v8a4.5 4.5 0 0 0 2.5-4zM14 3.2v2.1a7 7 0 0 1 0 13.4v2.1a9 9 0 0 0 0-17.6z"/></svg>',
|
||||||
|
volMute:
|
||||||
|
'<svg viewBox="0 0 24 24"><path d="M3 9v6h4l5 5V4L7 9H3zm18.5 3-2.3-2.3-1.4 1.4 2.3 2.3-2.3 2.3 1.4 1.4 2.3-2.3 2.3 2.3 1.4-1.4-2.3-2.3 2.3-2.3-1.4-1.4z"/></svg>',
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// Steuerleiste für ein Video bauen.
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
function buildControls(video) {
|
||||||
|
const bar = document.createElement("div");
|
||||||
|
bar.className = "rc-bar";
|
||||||
|
// Klicks auf der Leiste sollen NICHT bis zu Instagram durchgehen
|
||||||
|
// (sonst pausiert/scrollt IG selbst).
|
||||||
|
["click", "mousedown", "pointerdown", "dblclick"].forEach((ev) =>
|
||||||
|
bar.addEventListener(ev, (e) => e.stopPropagation())
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Play / Pause -----------------------------------------
|
||||||
|
const playBtn = document.createElement("button");
|
||||||
|
playBtn.className = "rc-btn rc-play";
|
||||||
|
playBtn.title = "Wiedergabe / Pause (Leertaste)";
|
||||||
|
|
||||||
|
const togglePlay = () => {
|
||||||
|
if (video.paused) video.play();
|
||||||
|
else video.pause();
|
||||||
|
};
|
||||||
|
playBtn.addEventListener("click", togglePlay);
|
||||||
|
|
||||||
|
const syncPlayIcon = () => {
|
||||||
|
playBtn.innerHTML = video.paused ? ICONS.play : ICONS.pause;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Fortschrittsbalken -----------------------------------
|
||||||
|
const progWrap = document.createElement("div");
|
||||||
|
progWrap.className = "rc-progress";
|
||||||
|
const progFill = document.createElement("div");
|
||||||
|
progFill.className = "rc-progress-fill";
|
||||||
|
const progKnob = document.createElement("div");
|
||||||
|
progKnob.className = "rc-progress-knob";
|
||||||
|
progWrap.appendChild(progFill);
|
||||||
|
progWrap.appendChild(progKnob);
|
||||||
|
|
||||||
|
let scrubbing = false;
|
||||||
|
const seekFromEvent = (e) => {
|
||||||
|
const rect = progWrap.getBoundingClientRect();
|
||||||
|
const ratio = Math.min(
|
||||||
|
1,
|
||||||
|
Math.max(0, (e.clientX - rect.left) / rect.width)
|
||||||
|
);
|
||||||
|
if (isFinite(video.duration)) {
|
||||||
|
video.currentTime = ratio * video.duration;
|
||||||
|
updateProgress();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
progWrap.addEventListener("pointerdown", (e) => {
|
||||||
|
scrubbing = true;
|
||||||
|
progWrap.setPointerCapture(e.pointerId);
|
||||||
|
seekFromEvent(e);
|
||||||
|
});
|
||||||
|
progWrap.addEventListener("pointermove", (e) => {
|
||||||
|
if (scrubbing) seekFromEvent(e);
|
||||||
|
});
|
||||||
|
progWrap.addEventListener("pointerup", (e) => {
|
||||||
|
scrubbing = false;
|
||||||
|
try {
|
||||||
|
progWrap.releasePointerCapture(e.pointerId);
|
||||||
|
} catch (_) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Zeitanzeige ------------------------------------------
|
||||||
|
const timeLabel = document.createElement("span");
|
||||||
|
timeLabel.className = "rc-time";
|
||||||
|
timeLabel.textContent = "0:00 / 0:00";
|
||||||
|
|
||||||
|
// --- Lautstärke -------------------------------------------
|
||||||
|
const volBtn = document.createElement("button");
|
||||||
|
volBtn.className = "rc-btn rc-vol-btn";
|
||||||
|
volBtn.title = "Stummschalten (M)";
|
||||||
|
|
||||||
|
const volSlider = document.createElement("input");
|
||||||
|
volSlider.type = "range";
|
||||||
|
volSlider.min = "0";
|
||||||
|
volSlider.max = "1";
|
||||||
|
volSlider.step = "0.01";
|
||||||
|
volSlider.className = "rc-vol-slider";
|
||||||
|
|
||||||
|
const applyVolumeUI = () => {
|
||||||
|
volBtn.innerHTML = video.muted || video.volume === 0
|
||||||
|
? ICONS.volMute
|
||||||
|
: ICONS.volHigh;
|
||||||
|
volSlider.value = video.muted ? 0 : video.volume;
|
||||||
|
volSlider.style.setProperty(
|
||||||
|
"--rc-fill",
|
||||||
|
(video.muted ? 0 : video.volume) * 100 + "%"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
volSlider.addEventListener("input", () => {
|
||||||
|
const v = parseFloat(volSlider.value);
|
||||||
|
// Wunsch-Lautstärke: bei 0 den letzten hörbaren Wert behalten,
|
||||||
|
// damit das Entstummen wieder dorthin zurückkehrt.
|
||||||
|
if (v > 0) video._rcDesiredVolume = v;
|
||||||
|
video._rcUserMuted = v === 0;
|
||||||
|
rcGuarding = true;
|
||||||
|
video.volume = v;
|
||||||
|
video.muted = v === 0;
|
||||||
|
rcGuarding = false;
|
||||||
|
lastVolume = video._rcDesiredVolume;
|
||||||
|
lastMuted = video._rcUserMuted;
|
||||||
|
persistVolume();
|
||||||
|
applyVolumeUI();
|
||||||
|
});
|
||||||
|
|
||||||
|
volBtn.addEventListener("click", () => {
|
||||||
|
video._rcUserMuted = !video._rcUserMuted;
|
||||||
|
rcGuarding = true;
|
||||||
|
video.muted = video._rcUserMuted;
|
||||||
|
// Beim Entstummen die gewünschte Lautstärke wiederherstellen.
|
||||||
|
if (!video._rcUserMuted) {
|
||||||
|
if (!video._rcDesiredVolume || video._rcDesiredVolume === 0) {
|
||||||
|
video._rcDesiredVolume = 0.5;
|
||||||
|
}
|
||||||
|
video.volume = video._rcDesiredVolume;
|
||||||
|
}
|
||||||
|
rcGuarding = false;
|
||||||
|
lastVolume = video._rcDesiredVolume;
|
||||||
|
lastMuted = video._rcUserMuted;
|
||||||
|
persistVolume();
|
||||||
|
applyVolumeUI();
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Zusammenbauen ----------------------------------------
|
||||||
|
bar.appendChild(playBtn);
|
||||||
|
bar.appendChild(progWrap);
|
||||||
|
bar.appendChild(timeLabel);
|
||||||
|
bar.appendChild(volBtn);
|
||||||
|
bar.appendChild(volSlider);
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// Live-Synchronisation
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
function updateProgress() {
|
||||||
|
const dur = isFinite(video.duration) ? video.duration : 0;
|
||||||
|
const ratio = dur ? video.currentTime / dur : 0;
|
||||||
|
progFill.style.width = ratio * 100 + "%";
|
||||||
|
progKnob.style.left = ratio * 100 + "%";
|
||||||
|
timeLabel.textContent =
|
||||||
|
fmtTime(video.currentTime) + " / " + fmtTime(dur);
|
||||||
|
}
|
||||||
|
|
||||||
|
video.addEventListener("timeupdate", updateProgress);
|
||||||
|
video.addEventListener("durationchange", updateProgress);
|
||||||
|
video.addEventListener("play", syncPlayIcon);
|
||||||
|
video.addEventListener("pause", syncPlayIcon);
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// Mute-Guard: Instagram setzt Videos beim Start/Weiter-
|
||||||
|
// schalten eigenständig auf muted ODER zieht die Lautstärke
|
||||||
|
// hoch/runter. Wir fangen jede solche Änderung ab und
|
||||||
|
// erzwingen den vom Nutzer gewünschten Zustand.
|
||||||
|
//
|
||||||
|
// `_rcUserMuted` = will der Nutzer stumm? (true/false)
|
||||||
|
// `_rcDesiredVolume` = welche Lautstärke will der Nutzer?
|
||||||
|
// (0..1, der echte Wunschwert)
|
||||||
|
//
|
||||||
|
// Solange der Nutzer nichts selbst ändert, ist JEDE
|
||||||
|
// Abweichung von diesen Werten ein Eingriff von Instagram
|
||||||
|
// und wird sofort zurückgesetzt.
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
let rcGuarding = false; // verhindert Endlosschleife im Handler
|
||||||
|
|
||||||
|
function enforceVolume() {
|
||||||
|
if (rcGuarding) return;
|
||||||
|
rcGuarding = true;
|
||||||
|
// Lautstärke immer auf den Wunschwert zwingen ...
|
||||||
|
if (Math.abs(video.volume - video._rcDesiredVolume) > 0.001) {
|
||||||
|
video.volume = video._rcDesiredVolume;
|
||||||
|
}
|
||||||
|
// ... und Mute-Zustand auf die Nutzer-Absicht.
|
||||||
|
if (video.muted !== video._rcUserMuted) {
|
||||||
|
video.muted = video._rcUserMuted;
|
||||||
|
}
|
||||||
|
rcGuarding = false;
|
||||||
|
applyVolumeUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
video.addEventListener("volumechange", enforceVolume);
|
||||||
|
|
||||||
|
// Anfangszustand: gespeicherte Werte übernehmen.
|
||||||
|
video._rcUserMuted = lastMuted;
|
||||||
|
video._rcDesiredVolume = lastVolume;
|
||||||
|
rcGuarding = true;
|
||||||
|
video.volume = lastVolume;
|
||||||
|
video.muted = lastMuted;
|
||||||
|
rcGuarding = false;
|
||||||
|
|
||||||
|
// Sicherheitsnetz: kurz nach play() greift IG manchmal noch
|
||||||
|
// einmal nach. Wir prüfen die ersten Sekunden mehrfach nach.
|
||||||
|
video.addEventListener("play", () => {
|
||||||
|
enforceVolume();
|
||||||
|
[60, 200, 500, 1000].forEach((ms) => setTimeout(enforceVolume, ms));
|
||||||
|
});
|
||||||
|
video.addEventListener("loadeddata", enforceVolume);
|
||||||
|
|
||||||
|
syncPlayIcon();
|
||||||
|
applyVolumeUI();
|
||||||
|
updateProgress();
|
||||||
|
|
||||||
|
return bar;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// Steuerung an ein Video anheften.
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
function attach(video) {
|
||||||
|
if (video.getAttribute(ATTACHED) === "1") return;
|
||||||
|
if (!isReelVideo(video)) return;
|
||||||
|
|
||||||
|
video.setAttribute(ATTACHED, "1");
|
||||||
|
|
||||||
|
// Wir hängen die Leiste an den nächstgelegenen positionierten
|
||||||
|
// Vorfahren, damit sie über dem Video sitzt.
|
||||||
|
let host = video.parentElement;
|
||||||
|
if (!host) return;
|
||||||
|
// Sicherstellen, dass der Host positioniert ist.
|
||||||
|
const pos = getComputedStyle(host).position;
|
||||||
|
if (pos === "static") host.style.position = "relative";
|
||||||
|
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.className = "rc-wrapper";
|
||||||
|
wrapper.appendChild(buildControls(video));
|
||||||
|
host.appendChild(wrapper);
|
||||||
|
|
||||||
|
// Falls Instagram das Video entfernt, räumen wir mit auf.
|
||||||
|
const cleanup = new MutationObserver(() => {
|
||||||
|
if (!document.contains(video)) {
|
||||||
|
wrapper.remove();
|
||||||
|
cleanup.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cleanup.observe(document.body, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function scan() {
|
||||||
|
document.querySelectorAll("video").forEach(attach);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// Beobachter: IG lädt Reels nach beim Scrollen.
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
// Leichtgewichtig drosseln.
|
||||||
|
if (observer._raf) return;
|
||||||
|
observer._raf = requestAnimationFrame(() => {
|
||||||
|
observer._raf = null;
|
||||||
|
scan();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Erstes Scannen + periodischer Fallback (IG-SPA-Navigation).
|
||||||
|
scan();
|
||||||
|
setInterval(scan, 1500);
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// Globale Tastenkürzel — gelten für das aktuell sichtbare Reel.
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
function visibleReel() {
|
||||||
|
let best = null;
|
||||||
|
let bestArea = 0;
|
||||||
|
document.querySelectorAll("video[" + ATTACHED + '="1"]').forEach((v) => {
|
||||||
|
const r = v.getBoundingClientRect();
|
||||||
|
const vh = window.innerHeight;
|
||||||
|
const visible =
|
||||||
|
Math.min(r.bottom, vh) - Math.max(r.top, 0);
|
||||||
|
if (visible > bestArea) {
|
||||||
|
bestArea = visible;
|
||||||
|
best = v;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener(
|
||||||
|
"keydown",
|
||||||
|
(e) => {
|
||||||
|
// Nicht stören, wenn der Nutzer in ein Eingabefeld tippt.
|
||||||
|
const tag = (e.target.tagName || "").toLowerCase();
|
||||||
|
if (tag === "input" || tag === "textarea" || e.target.isContentEditable)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const v = visibleReel();
|
||||||
|
if (!v) return;
|
||||||
|
|
||||||
|
if (e.code === "Space") {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (v.paused) v.play();
|
||||||
|
else v.pause();
|
||||||
|
} else if (e.key.toLowerCase() === "m") {
|
||||||
|
v._rcUserMuted = !v._rcUserMuted; // Nutzer-Absicht festhalten
|
||||||
|
v.muted = v._rcUserMuted;
|
||||||
|
if (!v._rcUserMuted) {
|
||||||
|
if (!v._rcDesiredVolume) v._rcDesiredVolume = 0.5;
|
||||||
|
v.volume = v._rcDesiredVolume;
|
||||||
|
}
|
||||||
|
lastVolume = v._rcDesiredVolume;
|
||||||
|
lastMuted = v._rcUserMuted;
|
||||||
|
persistVolume();
|
||||||
|
} else if (e.key === "ArrowLeft") {
|
||||||
|
v.currentTime = Math.max(0, v.currentTime - 5);
|
||||||
|
} else if (e.key === "ArrowRight") {
|
||||||
|
v.currentTime = Math.min(v.duration || 0, v.currentTime + 5);
|
||||||
|
} else if (e.key === "ArrowUp") {
|
||||||
|
e.preventDefault();
|
||||||
|
v._rcUserMuted = false; // Nutzer will Ton
|
||||||
|
v._rcDesiredVolume = Math.min(1, (v._rcDesiredVolume || 0) + 0.1);
|
||||||
|
v.muted = false;
|
||||||
|
v.volume = v._rcDesiredVolume;
|
||||||
|
lastVolume = v._rcDesiredVolume;
|
||||||
|
lastMuted = false;
|
||||||
|
persistVolume();
|
||||||
|
} else if (e.key === "ArrowDown") {
|
||||||
|
e.preventDefault();
|
||||||
|
v._rcDesiredVolume = Math.max(0, (v._rcDesiredVolume || 0) - 0.1);
|
||||||
|
v.volume = v._rcDesiredVolume;
|
||||||
|
if (v._rcDesiredVolume === 0) {
|
||||||
|
v._rcUserMuted = true; // bewusst auf 0 = Nutzer will stumm
|
||||||
|
v.muted = true;
|
||||||
|
}
|
||||||
|
lastVolume = v._rcDesiredVolume || lastVolume;
|
||||||
|
lastMuted = v._rcUserMuted;
|
||||||
|
persistVolume();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true // capture: vor Instagram abgreifen
|
||||||
|
);
|
||||||
|
})();
|
||||||
181
controls.css
Normal file
181
controls.css
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
/* ============================================================
|
||||||
|
Reels Controls — controls.css
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
.rc-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
pointer-events: none; /* nur die Leiste selbst ist klickbar */
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 12px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rc-bar {
|
||||||
|
pointer-events: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 460px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(0, 0, 0, 0.62);
|
||||||
|
backdrop-filter: blur(10px) saturate(140%);
|
||||||
|
-webkit-backdrop-filter: blur(10px) saturate(140%);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui,
|
||||||
|
sans-serif;
|
||||||
|
opacity: 0.35;
|
||||||
|
transform: translateY(2px);
|
||||||
|
transition: opacity 0.18s ease, transform 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rc-wrapper:hover .rc-bar,
|
||||||
|
.rc-bar:focus-within {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Buttons ------------------------------------------------ */
|
||||||
|
.rc-btn {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s ease, transform 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rc-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rc-btn:active {
|
||||||
|
transform: scale(0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rc-btn svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
fill: #fff;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Fortschrittsbalken ------------------------------------- */
|
||||||
|
.rc-progress {
|
||||||
|
position: relative;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
height: 16px; /* großzügige Klickfläche */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rc-progress::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rc-progress-fill {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
height: 4px;
|
||||||
|
width: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: linear-gradient(90deg, #f58529, #dd2a7b, #8134af);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rc-progress-knob {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rc-progress:hover .rc-progress-knob,
|
||||||
|
.rc-bar:focus-within .rc-progress-knob {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Zeitanzeige -------------------------------------------- */
|
||||||
|
.rc-time {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-size: 11px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
white-space: nowrap;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Lautstärke-Slider ------------------------------------- */
|
||||||
|
.rc-vol-slider {
|
||||||
|
flex: 0 0 70px;
|
||||||
|
height: 16px;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rc-vol-slider::-webkit-slider-runnable-track {
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
#fff 0%,
|
||||||
|
#fff var(--rc-fill, 100%),
|
||||||
|
rgba(255, 255, 255, 0.25) var(--rc-fill, 100%)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rc-vol-slider::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
margin-top: -4px;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rc-vol-slider::-moz-range-track {
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rc-vol-slider::-moz-range-progress {
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rc-vol-slider::-moz-range-thumb {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
BIN
icons/icon128.png
Normal file
BIN
icons/icon128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
BIN
icons/icon16.png
Normal file
BIN
icons/icon16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 272 B |
BIN
icons/icon48.png
Normal file
BIN
icons/icon48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 647 B |
25
manifest.json
Normal file
25
manifest.json
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "Reels Controls — Video-Steuerung für Instagram Reels",
|
||||||
|
"author": "Tronax mail@tronax.dev",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Fügt Instagram Reels eine echte Videosteuerung hinzu: Play/Pause, Lautstärke, Fortschrittsbalken mit Scrubbing und Tastenkürzel.",
|
||||||
|
"permissions": [],
|
||||||
|
"host_permissions": [
|
||||||
|
"https://www.instagram.com/*"
|
||||||
|
],
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["https://www.instagram.com/*"],
|
||||||
|
"js": ["content.js"],
|
||||||
|
"css": ["controls.css"],
|
||||||
|
"run_at": "document_idle",
|
||||||
|
"all_frames": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"icons": {
|
||||||
|
"16": "icons/icon16.png",
|
||||||
|
"48": "icons/icon48.png",
|
||||||
|
"128": "icons/icon128.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue