Bump version to 1.1.0 and add seeking control functionality for overlay containers
All checks were successful
Build and Release Extension / build (push) Successful in 19s

This commit is contained in:
Илья Глазунов 2026-01-22 22:15:18 +03:00
parent c2d520b753
commit 629bbf090f
3 changed files with 255 additions and 2 deletions

View File

@ -1,6 +1,6 @@
{
"name": "reels-master",
"version": "1.0.0",
"version": "1.1.0",
"description": "Chrome extension for Instagram Reels with volume control and download functionality",
"main": "index.js",
"scripts": {

View File

@ -207,3 +207,77 @@
.reels-master-spinner {
animation: spin 1s linear infinite;
}
/* Seeking Control - для overlay контейнера */
.reels-master-seeking {
width: 100%;
padding: 8px 16px 12px 16px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.4), transparent);
display: flex;
flex-direction: column;
gap: 6px;
pointer-events: auto;
cursor: default;
}
.reels-master-seeking-slider {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 3px;
background: rgba(255, 255, 255, 0.3);
outline: none;
border-radius: 2px;
cursor: pointer;
position: relative;
pointer-events: auto;
touch-action: none;
}
.reels-master-seeking-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 12px;
height: 12px;
background: white;
cursor: pointer;
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
transition: all 0.2s;
}
.reels-master-seeking-slider::-webkit-slider-thumb:hover {
background: #f0f0f0;
transform: scale(1.3);
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.6);
}
.reels-master-seeking-slider::-moz-range-thumb {
width: 12px;
height: 12px;
background: white;
cursor: pointer;
border-radius: 50%;
border: none;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
transition: all 0.2s;
}
.reels-master-seeking-slider::-moz-range-thumb:hover {
background: #f0f0f0;
transform: scale(1.3);
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.6);
}
.reels-master-time-display {
display: flex;
justify-content: space-between;
color: white;
font-size: 11px;
font-weight: 500;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
user-select: none;
opacity: 0.9;
pointer-events: none;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}

View File

@ -8,6 +8,8 @@ class ReelsMaster {
private processedContainers: WeakSet<HTMLElement> = new WeakSet();
private videoVolumeListeners: WeakMap<HTMLVideoElement, boolean> = new WeakMap();
private domObserver: MutationObserver | null = null;
private processedOverlays: WeakSet<HTMLElement> = new WeakSet();
private videoSeekingListeners: WeakMap<HTMLVideoElement, Set<HTMLInputElement>> = new WeakMap();
constructor() {
this.init();
@ -52,6 +54,7 @@ class ReelsMaster {
private start(): void {
console.log('Reels Master: Starting...');
this.injectControlsToAllContainers();
this.injectSeekingToAllOverlays();
this.setupDOMObserver();
}
@ -133,6 +136,7 @@ class ReelsMaster {
if (shouldCheck) {
requestAnimationFrame(() => {
this.injectControlsToAllContainers();
this.injectSeekingToAllOverlays();
});
}
});
@ -460,6 +464,181 @@ class ReelsMaster {
`;
}
}
}
private injectSeekingToAllOverlays(): void {
if (!window.location.pathname.includes('/reels/')) return;
const overlayContainers = this.findAllOverlayContainers();
console.log(`Reels Master: Found ${overlayContainers.length} overlay containers`);
for (const container of overlayContainers) {
this.injectSeekingToOverlay(container);
}
}
private findAllOverlayContainers(): HTMLElement[] {
const containers: HTMLElement[] = [];
const followButtons = document.querySelectorAll('[role="button"]');
for (const button of followButtons) {
if (button.textContent?.trim() === 'Follow') {
let parent = button.parentElement;
let depth = 0;
const maxDepth = 15;
while (parent && depth < maxDepth) {
const hasAvatar = parent.querySelector('img[alt*="profile picture"]');
const hasFollow = parent.querySelector('[role="button"]');
if (hasAvatar && hasFollow && parent.children.length >= 2) {
if (!containers.includes(parent as HTMLElement)) {
containers.push(parent as HTMLElement);
}
break;
}
parent = parent.parentElement;
depth++;
}
}
}
return containers;
}
private injectSeekingToOverlay(overlayContainer: HTMLElement): void {
if (this.processedOverlays.has(overlayContainer)) {
return;
}
if (overlayContainer.querySelector('.reels-master-seeking')) {
this.processedOverlays.add(overlayContainer);
return;
}
const video = this.findVideoForOverlay(overlayContainer);
if (!video) {
console.log('Reels Master: Video not found for overlay');
return;
}
const seekingControl = this.createSeekingControl(video);
overlayContainer.appendChild(seekingControl);
this.processedOverlays.add(overlayContainer);
console.log('Reels Master: Seeking control injected to overlay');
}
private findVideoForOverlay(overlayContainer: HTMLElement): HTMLVideoElement | null {
let parent = overlayContainer.parentElement;
while (parent) {
const video = parent.querySelector('video');
if (video) {
return video;
}
parent = parent.parentElement;
if (parent === document.body) break;
}
return this.getClosestVideoToElement(overlayContainer);
}
private createSeekingControl(video: HTMLVideoElement): HTMLDivElement {
const seekingContainer = document.createElement('div');
seekingContainer.className = 'reels-master-seeking';
seekingContainer.addEventListener('click', (e) => {
e.stopPropagation();
});
seekingContainer.addEventListener('mousedown', (e) => {
e.stopPropagation();
});
seekingContainer.addEventListener('touchstart', (e) => {
e.stopPropagation();
});
const timeDisplay = document.createElement('div');
timeDisplay.className = 'reels-master-time-display';
const currentTimeSpan = document.createElement('span');
currentTimeSpan.textContent = '0:00';
const durationSpan = document.createElement('span');
durationSpan.textContent = '0:00';
timeDisplay.appendChild(currentTimeSpan);
timeDisplay.appendChild(durationSpan);
const slider = document.createElement('input');
slider.type = 'range';
slider.min = '0';
slider.max = '100';
slider.value = '0';
slider.className = 'reels-master-seeking-slider';
slider.addEventListener('click', (e) => e.stopPropagation());
slider.addEventListener('mousedown', (e) => e.stopPropagation());
slider.addEventListener('mouseup', (e) => e.stopPropagation());
slider.addEventListener('touchstart', (e) => e.stopPropagation());
slider.addEventListener('touchend', (e) => e.stopPropagation());
slider.addEventListener('touchmove', (e) => e.stopPropagation());
const updateDuration = () => {
if (video.duration && !isNaN(video.duration) && video.duration !== Infinity) {
slider.max = String(video.duration);
durationSpan.textContent = this.formatTime(video.duration);
}
};
const updateTime = () => {
if (!isNaN(video.duration) && video.duration !== Infinity) {
slider.value = String(video.currentTime);
currentTimeSpan.textContent = this.formatTime(video.currentTime);
}
};
video.addEventListener('loadedmetadata', updateDuration);
video.addEventListener('durationchange', updateDuration);
video.addEventListener('timeupdate', updateTime);
updateDuration();
updateTime();
let isSeeking = false;
slider.addEventListener('input', (e) => {
const time = parseFloat((e.target as HTMLInputElement).value);
currentTimeSpan.textContent = this.formatTime(time);
isSeeking = true;
});
slider.addEventListener('change', (e) => {
const time = parseFloat((e.target as HTMLInputElement).value);
video.currentTime = time;
isSeeking = false;
});
if (!this.videoSeekingListeners.has(video)) {
this.videoSeekingListeners.set(video, new Set());
}
this.videoSeekingListeners.get(video)!.add(slider);
seekingContainer.appendChild(timeDisplay);
seekingContainer.appendChild(slider);
return seekingContainer;
}
private formatTime(seconds: number): string {
if (isNaN(seconds) || seconds === Infinity) {
return '0:00';
}
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}}
new ReelsMaster();