From 629bbf090f0844ed3f36160b2d4458c2b475a690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=93=D0=BB=D0=B0=D0=B7=D1=83?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2?= Date: Thu, 22 Jan 2026 22:15:18 +0300 Subject: [PATCH] Bump version to 1.1.0 and add seeking control functionality for overlay containers --- package.json | 2 +- src/content/content.css | 74 ++++++++++++++++ src/content/content.ts | 181 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 255 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 8344210..f302721 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/content/content.css b/src/content/content.css index 27719cb..14244f1 100644 --- a/src/content/content.css +++ b/src/content/content.css @@ -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; +} diff --git a/src/content/content.ts b/src/content/content.ts index ab2bd70..71453e1 100644 --- a/src/content/content.ts +++ b/src/content/content.ts @@ -8,6 +8,8 @@ class ReelsMaster { private processedContainers: WeakSet = new WeakSet(); private videoVolumeListeners: WeakMap = new WeakMap(); private domObserver: MutationObserver | null = null; + private processedOverlays: WeakSet = new WeakSet(); + private videoSeekingListeners: WeakMap> = 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();