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