initial commit

This commit is contained in:
Илья Глазунов
2026-01-20 20:54:51 +03:00
commit c5e137d4aa
12 changed files with 1909 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
// Background Service Worker for Reels Master
console.log('Reels Master: Background service worker loaded');
// Listen for extension installation
chrome.runtime.onInstalled.addListener(() => {
console.log('Reels Master: Extension installed');
});
+195
View File
@@ -0,0 +1,195 @@
/* Reels Master - Custom Styles */
.reels-master-controls {
z-index: 9999;
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 16px;
align-items: center;
}
/* Volume Control */
.reels-master-volume {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
.reels-master-volume-button {
background: none;
border: none;
cursor: pointer;
padding: 8px;
color: white;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
position: relative;
z-index: 2;
}
.reels-master-volume-button:hover {
transform: scale(1.1);
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.5));
}
.reels-master-volume-button:active {
transform: scale(0.95) !important;
}
/* Slider Container */
.reels-master-slider-container {
position: absolute;
bottom: calc(100% + 4px);
left: 50%;
transform: translateX(-50%) translateY(20px);
padding: 24px;
padding-bottom: 24px;
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transition-delay: 0.5s;
will-change: transform, opacity;
}
.reels-master-volume:hover .reels-master-slider-container,
.reels-master-slider-container:hover {
opacity: 1;
visibility: visible;
pointer-events: all;
transform: translateX(-50%) translateY(0);
transition-delay: 0s;
}
/* Keep slider visible when hovering over it */
.reels-master-slider-container:hover {
transition-delay: 0s;
}
@keyframes slideUp {
from {
transform: translateX(-50%) translateY(10px);
opacity: 0;
}
to {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
}
/* Download Button */
.reels-master-download {
background: none;
border: none;
cursor: pointer;
padding: 8px;
color: white;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
}
.reels-master-download:hover {
transform: scale(1.1);
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.5));
}
.reels-master-download:active {
transform: scale(0.95) !important;
}
/* Volume Slider Styling */
.reels-master-volume-slider {
-webkit-appearance: none;
appearance: none;
width: 4px;
height: 100px;
background: rgba(255, 255, 255, 0.3);
outline: none;
border-radius: 2px;
writing-mode: bt-lr;
-webkit-appearance: slider-vertical;
cursor: pointer;
}
/* Slider thumb for webkit browsers */
.reels-master-volume-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-volume-slider::-webkit-slider-thumb:hover {
background: #f0f0f0;
transform: scale(1.2);
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.6);
}
/* Slider thumb for Firefox */
.reels-master-volume-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-volume-slider::-moz-range-thumb:hover {
background: #f0f0f0;
transform: scale(1.2);
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.6);
}
/* Track styling for webkit */
.reels-master-volume-slider::-webkit-slider-runnable-track {
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
}
/* Track styling for Firefox */
.reels-master-volume-slider::-moz-range-track {
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
}
/* Tooltip effect on hover */
.reels-master-download {
position: relative;
}
.reels-master-download::after {
content: 'Download';
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: -30px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
}
.reels-master-download:hover::after {
opacity: 1;
}
+350
View File
@@ -0,0 +1,350 @@
import './content.css';
console.log('Reels Master: Content script loaded');
interface ReelsControls {
volumeSlider: HTMLInputElement | null;
downloadButton: HTMLButtonElement | null;
container: HTMLDivElement | null;
}
class ReelsMaster {
private currentVideo: HTMLVideoElement | null = null;
private controls: ReelsControls = {
volumeSlider: null,
downloadButton: null,
container: null
};
private observer: MutationObserver | null = null;
private storedVolume: number | null = null;
private storedMuted: boolean = false;
constructor() {
this.init();
}
private init(): void {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.start());
} else {
this.start();
}
}
private start(): void {
console.log('Reels Master: Starting...');
this.checkForReels();
this.observeUrlChanges();
this.observeDOMChanges();
window.addEventListener('scroll', () => {
this.checkForReels();
}, { passive: true });
}
private observeUrlChanges(): void {
let lastUrl = location.href;
new MutationObserver(() => {
const currentUrl = location.href;
if (currentUrl !== lastUrl) {
lastUrl = currentUrl;
console.log('Reels Master: URL changed to', currentUrl);
setTimeout(() => this.checkForReels(), 500);
}
}).observe(document.querySelector('body')!, {
subtree: true,
childList: true
});
}
private observeDOMChanges(): void {
this.observer = new MutationObserver(() => {
this.checkForReels();
});
this.observer.observe(document.body, {
childList: true,
subtree: true
});
}
private checkForReels(): void {
if (!window.location.pathname.includes('/reels/')) {
this.cleanup();
return;
}
const video = this.getActiveVideo();
if (!video || video === this.currentVideo) {
return;
}
console.log('Reels Master: Found new video element');
this.currentVideo = video;
this.injectControls();
}
private getActiveVideo(): HTMLVideoElement | null {
const videos = Array.from(document.querySelectorAll('video'));
if (videos.length === 0) return null;
const center = window.innerHeight / 2;
let closestVideo: HTMLVideoElement | null = null;
let minDistance = Infinity;
for (const video of videos) {
const rect = video.getBoundingClientRect();
if (rect.height === 0) continue;
const videoCenter = rect.top + (rect.height / 2);
const distance = Math.abs(center - videoCenter);
if (distance < minDistance) {
minDistance = distance;
closestVideo = video;
}
}
return closestVideo;
}
private injectControls(): void {
if (!this.currentVideo) return;
const actionButtons = this.findActionButtonsContainer();
if (!actionButtons) {
console.log('Reels Master: Action buttons container not found');
setTimeout(() => this.injectControls(), 1000);
return;
}
if (this.controls.container) {
this.controls.container.remove();
}
this.controls.container = this.createControlsContainer();
this.controls.volumeSlider = this.createVolumeSlider();
this.controls.downloadButton = this.createDownloadButton();
this.controls.container.appendChild(this.createVolumeControl());
this.controls.container.appendChild(this.controls.downloadButton);
actionButtons.insertBefore(this.controls.container, actionButtons.firstChild);
if (this.storedVolume !== null && this.currentVideo) {
this.currentVideo.volume = this.storedVolume;
}
if (this.currentVideo) {
this.currentVideo.muted = this.storedMuted;
}
console.log('Reels Master: Controls injected');
}
private findActionButtonsContainer(): HTMLElement | null {
const likeButton = document.querySelector('svg[aria-label="Like"]');
if (likeButton) {
let parent = likeButton.parentElement;
while (parent) {
const childDivs = parent.querySelectorAll(':scope > div');
if (childDivs.length >= 3) {
const hasLike = parent.querySelector('svg[aria-label="Like"]');
const hasComment = parent.querySelector('svg[aria-label="Comment"]');
const hasShare = parent.querySelector('svg[aria-label="Share"]');
if (hasLike && hasComment && hasShare) {
return parent as HTMLElement;
}
}
parent = parent.parentElement;
}
}
return null;
}
private createControlsContainer(): HTMLDivElement {
const container = document.createElement('div');
container.className = 'reels-master-controls';
return container;
}
private createVolumeControl(): HTMLDivElement {
const volumeControl = document.createElement('div');
volumeControl.className = 'reels-master-volume';
const volumeButton = document.createElement('button');
volumeButton.className = 'reels-master-volume-button';
volumeButton.innerHTML = `
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
</svg>
`;
const sliderContainer = document.createElement('div');
sliderContainer.className = 'reels-master-slider-container';
this.controls.volumeSlider = this.createVolumeSlider();
sliderContainer.appendChild(this.controls.volumeSlider);
volumeButton.onclick = () => {
if (this.currentVideo) {
this.currentVideo.muted = !this.currentVideo.muted;
this.storedMuted = this.currentVideo.muted;
if (this.controls.volumeSlider) {
this.controls.volumeSlider.value = this.currentVideo.muted ? '0' : String(this.currentVideo.volume * 100);
}
this.updateVolumeIcon(volumeButton);
}
};
volumeControl.appendChild(sliderContainer);
volumeControl.appendChild(volumeButton);
return volumeControl;
}
private createVolumeSlider(): HTMLInputElement {
const slider = document.createElement('input');
slider.type = 'range';
slider.min = '0';
slider.max = '100';
let initialValue = '50';
if (this.currentVideo) {
if (this.currentVideo.muted) {
initialValue = '0';
} else {
initialValue = String(this.currentVideo.volume * 100);
}
}
slider.value = initialValue;
slider.className = 'reels-master-volume-slider';
slider.oninput = (e) => {
if (this.currentVideo) {
const value = parseInt((e.target as HTMLInputElement).value);
this.currentVideo.volume = value / 100;
this.currentVideo.muted = value === 0;
this.storedVolume = this.currentVideo.volume;
this.storedMuted = this.currentVideo.muted;
const volumeControl = slider.closest('.reels-master-volume');
const volumeButton = volumeControl?.querySelector('.reels-master-volume-button') as HTMLButtonElement;
if (volumeButton) {
this.updateVolumeIcon(volumeButton);
}
}
};
return slider;
}
private updateVolumeIcon(button: HTMLButtonElement): void {
if (!this.currentVideo) return;
const volume = this.currentVideo.muted ? 0 : this.currentVideo.volume;
let icon = '';
if (volume === 0) {
icon = `<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>
</svg>`;
} else if (volume < 0.5) {
icon = `<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M7 9v6h4l5 5V4l-5 5H7z"/>
</svg>`;
} else {
icon = `<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
</svg>`;
}
button.innerHTML = icon;
}
private createDownloadButton(): HTMLButtonElement {
const button = document.createElement('button');
button.className = 'reels-master-download';
button.innerHTML = `
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
</svg>
`;
button.title = 'Download Reel';
button.onclick = () => this.downloadReel();
return button;
}
private async downloadReel(): Promise<void> {
if (!this.currentVideo) {
console.error('Reels Master: No video found');
return;
}
try {
const videoUrl = this.currentVideo.src;
if (!videoUrl) {
alert('Unable to find video URL');
return;
}
if (this.controls.downloadButton) {
this.controls.downloadButton.innerHTML = `
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>
`;
}
const response = await fetch(videoUrl);
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `reel_${Date.now()}.mp4`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
setTimeout(() => {
if (this.controls.downloadButton) {
this.controls.downloadButton.innerHTML = `
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
</svg>
`;
}
}, 2000);
} catch (error) {
console.error('Reels Master: Download failed', error);
alert('Failed to download video. Please try again.');
if (this.controls.downloadButton) {
this.controls.downloadButton.innerHTML = `
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
</svg>
`;
}
}
}
private cleanup(): void {
if (this.controls.container) {
this.controls.container.remove();
this.controls.container = null;
this.controls.volumeSlider = null;
this.controls.downloadButton = null;
}
this.currentVideo = null;
}
}
new ReelsMaster();
+19
View File
@@ -0,0 +1,19 @@
{
"manifest_version": 3,
"name": "Reels Master",
"version": "1.0.0",
"description": "Instagram Reels volume control and download",
"background": {
"service_worker": "background/background.js"
},
"content_scripts": [
{
"matches": ["*://*.instagram.com/*"],
"js": ["content/content.js"],
"css": ["assets/content.css"],
"run_at": "document_end"
}
],
"permissions": ["storage"],
"host_permissions": ["*://*.instagram.com/*", "*://*.cdninstagram.com/*"]
}
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="vite/client" />
declare module '*.css' {
const content: string;
export default content;
}