initial commit
This commit is contained in:
commit
c5e137d4aa
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.env
|
||||||
|
.DS_Store
|
||||||
79
README.md
Normal file
79
README.md
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# Reels Master
|
||||||
|
|
||||||
|
Chrome расширение для улучшенного просмотра Instagram Reels с управлением громкостью и загрузкой видео.
|
||||||
|
|
||||||
|
## Возможности
|
||||||
|
|
||||||
|
- 🔊 **Управление громкостью** - Вертикальный слайдер для точной настройки громкости видео
|
||||||
|
- 📥 **Загрузка роликов** - Скачивайте рилсы одним кликом
|
||||||
|
- 🎯 **Удобное расположение** - Кнопки расположены рядом с лайками и комментариями
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
### Разработка
|
||||||
|
|
||||||
|
1. Установите зависимости:
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Соберите расширение:
|
||||||
|
```bash
|
||||||
|
pnpm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Загрузите расширение в Chrome:
|
||||||
|
- Откройте `chrome://extensions/`
|
||||||
|
- Включите "Режим разработчика" (Developer mode)
|
||||||
|
- Нажмите "Загрузить распакованное расширение" (Load unpacked)
|
||||||
|
- Выберите папку `dist`
|
||||||
|
|
||||||
|
### Режим разработки с автоперезагрузкой
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
1. Откройте Instagram и перейдите к любому рилсу
|
||||||
|
2. Справа от видео, рядом с кнопками лайка и комментариев, появятся новые элементы управления:
|
||||||
|
- **Кнопка громкости** - Нажмите для включения/выключения звука
|
||||||
|
- **Слайдер громкости** - Перетащите для регулировки уровня громкости (0-100%)
|
||||||
|
- **Кнопка загрузки** - Нажмите для скачивания текущего рилса
|
||||||
|
|
||||||
|
## Технологии
|
||||||
|
|
||||||
|
- TypeScript
|
||||||
|
- Vite
|
||||||
|
- Chrome Extension Manifest V3
|
||||||
|
- WebExtensions Polyfill
|
||||||
|
|
||||||
|
## Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
reels-master/
|
||||||
|
├── src/
|
||||||
|
│ ├── manifest.json # Манифест расширения
|
||||||
|
│ ├── content/
|
||||||
|
│ │ ├── content.ts # Основной скрипт для Instagram
|
||||||
|
│ │ └── content.css # Стили для элементов управления
|
||||||
|
│ ├── background/
|
||||||
|
│ │ └── service-worker.ts # Фоновый service worker
|
||||||
|
│ ├── popup/
|
||||||
|
│ │ ├── popup.html
|
||||||
|
│ │ ├── popup.tsx
|
||||||
|
│ │ └── popup.css
|
||||||
|
│ └── options/
|
||||||
|
│ ├── options.html
|
||||||
|
│ └── options.tsx
|
||||||
|
├── public/
|
||||||
|
│ └── icons/ # Иконки расширения
|
||||||
|
├── package.json
|
||||||
|
├── tsconfig.json
|
||||||
|
└── vite.config.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
ISC
|
||||||
29
package.json
Normal file
29
package.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "reels-master",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Chrome extension for Instagram Reels with volume control and download functionality",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite build --watch",
|
||||||
|
"build": "vite build",
|
||||||
|
"bundle": "vite build && node scripts/bundle.js",
|
||||||
|
"type-check": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"packageManager": "pnpm@10.15.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/adm-zip": "^0.5.7",
|
||||||
|
"@types/chrome": "^0.1.35",
|
||||||
|
"@types/node": "^25.0.9",
|
||||||
|
"@types/webextension-polyfill": "^0.12.4",
|
||||||
|
"@vitejs/plugin-react": "^5.1.2",
|
||||||
|
"adm-zip": "^0.5.16",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vite": "^7.3.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"webextension-polyfill": "^0.12.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
1144
pnpm-lock.yaml
generated
Normal file
1144
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
0
public/icons/.gitkeep
Normal file
0
public/icons/.gitkeep
Normal file
7
src/background/service-worker.ts
Normal file
7
src/background/service-worker.ts
Normal 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
src/content/content.css
Normal file
195
src/content/content.css
Normal 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
src/content/content.ts
Normal file
350
src/content/content.ts
Normal 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
src/manifest.json
Normal file
19
src/manifest.json
Normal 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
src/vite-env.d.ts
vendored
Normal file
6
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module '*.css' {
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM"],
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"types": ["chrome", "node"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*", "vite.config.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
57
vite.config.ts
Normal file
57
vite.config.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import { copyFileSync, existsSync } from 'fs';
|
||||||
|
import AdmZip from 'adm-zip';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
background: resolve(__dirname, 'src/background/service-worker.ts'),
|
||||||
|
content: resolve(__dirname, 'src/content/content.ts'),
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
entryFileNames: '[name]/[name].js',
|
||||||
|
chunkFileNames: '[name].js',
|
||||||
|
assetFileNames: 'assets/[name].[ext]'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
name: 'copy-manifest',
|
||||||
|
closeBundle() {
|
||||||
|
try {
|
||||||
|
copyFileSync(
|
||||||
|
resolve(__dirname, 'src/manifest.json'),
|
||||||
|
resolve(__dirname, 'dist/manifest.json')
|
||||||
|
);
|
||||||
|
console.log('✓ Copied manifest.json');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error copying manifest.json:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'create-zip',
|
||||||
|
closeBundle() {
|
||||||
|
if (process.env.NODE_ENV === 'production' || !process.argv.includes('--watch')) {
|
||||||
|
try {
|
||||||
|
const zip = new AdmZip();
|
||||||
|
const distPath = resolve(__dirname, 'dist');
|
||||||
|
|
||||||
|
if (existsSync(distPath)) {
|
||||||
|
zip.addLocalFolder(distPath);
|
||||||
|
zip.writeZip(resolve(__dirname, 'reels-master.zip'));
|
||||||
|
console.log('Created reels-master.zip');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error creating zip:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user