Screenshot for YouTube™ | 4K Frame & Thumbnail Cap
| // YouTube Screenshot Tool - Background Script | ||
| // Unified approach for Firefox and Chrome | ||
| const isBrowser = typeof browser !== 'undefined'; | ||
| const browserAPI = isBrowser ? browser : chrome; | ||
| // Default settings | ||
| const DEFAULT_SETTINGS = { | ||
| quality: 'high', | ||
| format: 'png', | ||
| filename: 'youtube_frame', | ||
| language: 'en' | ||
| }; | ||
| // Listen for keyboard commands | ||
| browserAPI.commands.onCommand.addListener(async (command) => { | ||
| if (command === 'capture_frame') { | ||
| try { | ||
| // Get active tab | ||
| const tabs = await browserAPI.tabs.query({ active: true, currentWindow: true }); | ||
| if (tabs.length === 0) return; | ||
| const tab = tabs[0]; | ||
| // Check if it's a YouTube video page | ||
| if (tab.url && tab.url.includes('youtube.com/watch')) { | ||
| // Get settings | ||
| const result = await browserAPI.storage.sync.get('settings'); | ||
| const settings = result.settings || DEFAULT_SETTINGS; | ||
| // Send capture message to content script | ||
| try { | ||
| await browserAPI.tabs.sendMessage(tab.id, { | ||
| action: 'captureFrame', | ||
| settings: settings | ||
| }); | ||
| } catch (err) { | ||
| // If message fails, content script might not be loaded | ||
| console.log('Content script not loaded, injecting...'); | ||
| // Inject script | ||
| const browserScripting = isBrowser ? browser.scripting : chrome.scripting; | ||
| if (browserScripting) { | ||
| await browserScripting.executeScript({ | ||
| target: { tabId: tab.id }, | ||
| files: ['lib/browser-polyfill.js', 'scripts/content.js'] | ||
| }); | ||
| // Wait a bit and retry | ||
| setTimeout(async () => { | ||
| try { | ||
| await browserAPI.tabs.sendMessage(tab.id, { | ||
| action: 'captureFrame', | ||
| settings: settings | ||
| }); | ||
| } catch (retryErr) { | ||
| console.error('Failed to capture after injection:', retryErr); | ||
| } | ||
| }, 100); | ||
| } | ||
| } | ||
| } | ||
| } catch (error) { | ||
| console.error('Error in command listener:', error); | ||
| } | ||
| } | ||
| }); |
+28
-20
| { | ||
| "manifest_version": 3, | ||
| "name": "YouTube Frame Extractor", | ||
| "version": "1.1.0", | ||
| "manifest_version": 2, | ||
| "name": "YouTube Screenshot Tool", | ||
| "version": "1.2.0", | ||
| "description": "Extract high-quality frames from YouTube videos without UI elements", | ||
@@ -10,4 +10,3 @@ "default_locale": "en", | ||
| "storage", | ||
| "downloads", | ||
| "scripting" | ||
| "*://*.youtube.com/*" | ||
| ], | ||
@@ -17,3 +16,8 @@ "browser_specific_settings": { | ||
| "id": "youtube-frame-extractor@example.com", | ||
| "strict_min_version": "109.0" | ||
| "strict_min_version": "142.0", | ||
| "data_collection_permissions": { | ||
| "required": [ | ||
| "none" | ||
| ] | ||
| } | ||
| } | ||
@@ -26,3 +30,3 @@ }, | ||
| }, | ||
| "action": { | ||
| "browser_action": { | ||
| "default_popup": "popup/popup.html", | ||
@@ -35,5 +39,2 @@ "default_icon": { | ||
| }, | ||
| "host_permissions": [ | ||
| "*://*.youtube.com/*" | ||
| ], | ||
| "content_scripts": [ | ||
@@ -51,13 +52,20 @@ { | ||
| "web_accessible_resources": [ | ||
| { | ||
| "resources": [ | ||
| "assets/*", | ||
| "lib/*", | ||
| "scripts/*" | ||
| ], | ||
| "matches": [ | ||
| "*://*.youtube.com/*" | ||
| ] | ||
| "assets/*", | ||
| "lib/*", | ||
| "scripts/*" | ||
| ], | ||
| "background": { | ||
| "scripts": [ | ||
| "scripts/background.js" | ||
| ] | ||
| }, | ||
| "commands": { | ||
| "capture_frame": { | ||
| "suggested_key": { | ||
| "default": "Alt+Shift+C", | ||
| "mac": "Alt+Shift+C" | ||
| }, | ||
| "description": "Capture Video Frame" | ||
| } | ||
| ] | ||
| } | ||
| } |
+38
-10
| <!DOCTYPE html> | ||
| <html> | ||
| <head> | ||
| <meta charset="UTF-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| <title>YouTube Frame Extractor</title> | ||
| <title>YouTube Screenshot Tool</title> | ||
| <link rel="stylesheet" href="../css/popup.css"> | ||
| <script src="../lib/browser-polyfill.js"></script> | ||
| </head> | ||
| <body> | ||
| <div class="popup-header"> | ||
| <div class="popup-title">YouTube Frame Extractor</div> | ||
| <div class="popup-title">YouTube Screenshot Tool</div> | ||
| <div class="language-selector"> | ||
@@ -21,5 +23,30 @@ <button id="en-btn" class="language-btn active" data-lang="en">EN</button> | ||
| <button id="capture" class="capture-btn">📸 Capture Frame</button> | ||
| <button id="thumbnail" class="thumbnail-btn">🖼️ Download Thumbnail</button> | ||
| </div> | ||
| <!-- Shortcut Section --> | ||
| <div class="shortcut-section" | ||
| style="text-align: center; margin-top: 10px; padding: 10px; background: #f5f5f5; border-radius: 8px;"> | ||
| <div class="shortcut-info" style="margin-bottom: 5px;"> | ||
| <span id="shortcutLabel" style="font-weight: bold;">Shortcut:</span> | ||
| <span id="currentShortcut" class="shortcut-key" | ||
| style="background: #e0e0e0; padding: 2px 6px; border-radius: 4px; font-family: monospace;">Loading...</span> | ||
| </div> | ||
| <button id="changeShortcut" class="link-btn" | ||
| style="background: none; border: none; color: #2196F3; cursor: pointer; text-decoration: underline; font-size: 12px;">Change | ||
| Shortcut</button> | ||
| </div> | ||
| <!-- Thumbnail Tool Section --> | ||
| <div class="thumbnail-tool-section" style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #eee;"> | ||
| <div class="form-group"> | ||
| <label for="videoUrlDisplay" style="font-size: 12px; color: #666; display: block; margin-bottom: 5px;">Detected | ||
| Video:</label> | ||
| <input type="text" id="videoUrlDisplay" readonly | ||
| style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9; box-sizing: border-box; font-size: 12px; color: #555;"> | ||
| </div> | ||
| <button id="openThumbnailTool" class="action-btn" | ||
| style="width: 100%; padding: 10px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; margin-top: 8px; font-weight: bold;">Open | ||
| Thumbnail Tool</button> | ||
| </div> | ||
| <div class="settings-section"> | ||
@@ -30,3 +57,3 @@ <div class="settings-header" id="settingsToggle"> | ||
| </div> | ||
| <div class="settings-content" id="settingsContent"> | ||
@@ -41,3 +68,3 @@ <div class="form-group"> | ||
| </div> | ||
| <div class="form-group"> | ||
@@ -51,3 +78,3 @@ <label for="format" id="formatLabel">File Format</label> | ||
| </div> | ||
| <div class="form-group"> | ||
@@ -57,3 +84,3 @@ <label for="filename" id="filenameLabel">File Name</label> | ||
| </div> | ||
| <div class="action-buttons"> | ||
@@ -65,7 +92,8 @@ <button id="saveSettings" class="save-btn">Save Settings</button> | ||
| </div> | ||
| <div id="statusMessage" class="status-message" style="display: none;"></div> | ||
| <script src="../scripts/popup.js"></script> | ||
| </body> | ||
| </html> | ||
| </html> |
+3
-71
@@ -19,6 +19,2 @@ /** | ||
| return true; // Keep the message channel open for async response | ||
| } else if (message.action === 'downloadThumbnail') { | ||
| const result = downloadThumbnail(message.language); | ||
| sendResponse(result); | ||
| return true; | ||
| } else if (message.action === 'ping' || message.action === 'injected_check') { | ||
@@ -35,9 +31,7 @@ // Respond to ping or injected_check message to confirm content script is loaded | ||
| videoNotFound: 'No video element found on this page', | ||
| videoIdNotFound: 'Could not extract video ID from URL', | ||
| thumbnailFetchFailed: 'Failed to fetch thumbnail' | ||
| videoIdNotFound: 'Could not extract video ID from URL' | ||
| }, | ||
| ar: { | ||
| videoNotFound: 'لم يتم العثور على الفيديو!', | ||
| videoIdNotFound: 'لم يتم العثور على معرف الفيديو', | ||
| thumbnailFetchFailed: 'فشل في تحميل الصورة المصغرة' | ||
| videoIdNotFound: 'لم يتم العثور على معرف الفيديو' | ||
| } | ||
@@ -121,64 +115,2 @@ }; | ||
| } | ||
| } | ||
| /** | ||
| * Downloads the video thumbnail | ||
| * @param {string} language - The current language | ||
| * @returns {boolean|string} - True on success, error message on failure | ||
| */ | ||
| function downloadThumbnail(language = 'en') { | ||
| try { | ||
| const lang = language || 'en'; | ||
| // Get video ID from URL | ||
| const url = window.location.href; | ||
| const videoId = url.match(/[?&]v=([^&]+)/)?.[1]; | ||
| if (!videoId) { | ||
| return messages[lang].videoIdNotFound; | ||
| } | ||
| // Get video title for filename | ||
| const videoTitle = document.querySelector('h1.title')?.textContent?.trim() || 'youtube_video'; | ||
| // Create a safe filename | ||
| const filename = videoTitle | ||
| .replace(/[^\w\s-]/g, '') // Remove special chars | ||
| .replace(/\s+/g, '_') // Replace spaces with underscores | ||
| .substring(0, 50) + '_thumbnail.jpg'; | ||
| // Get the maxresdefault thumbnail (highest quality) | ||
| // YouTube thumbnail URLs: https://img.youtube.com/vi/VIDEO_ID/maxresdefault.jpg | ||
| const thumbnailUrl = `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`; | ||
| // Create download link | ||
| const link = document.createElement('a'); | ||
| link.download = filename; | ||
| link.href = thumbnailUrl; | ||
| link.setAttribute('target', '_blank'); | ||
| // Fetch the image first to ensure it exists | ||
| fetch(thumbnailUrl) | ||
| .then(response => { | ||
| if (!response.ok) { | ||
| // If maxresdefault is not available, try hqdefault | ||
| return fetch(`https://img.youtube.com/vi/${videoId}/hqdefault.jpg`); | ||
| } | ||
| return response; | ||
| }) | ||
| .then(response => { | ||
| if (!response.ok) { | ||
| throw new Error(messages[lang].thumbnailFetchFailed); | ||
| } | ||
| link.click(); | ||
| }) | ||
| .catch(error => { | ||
| console.error('Error downloading thumbnail:', error); | ||
| return error.toString(); | ||
| }); | ||
| return true; | ||
| } catch (error) { | ||
| return error.toString(); | ||
| } | ||
| } | ||
| } |
+221
-279
@@ -1,6 +0,6 @@ | ||
| // استخدام الواجهة الموحدة لتحقيق التوافق مع Chrome و Firefox | ||
| // تعريف واضح للتحقق من نوع المتصفح المستخدم | ||
| // YouTube Screenshot Tool - Popup Script | ||
| // Unified approach for Firefox and Chrome | ||
| const isBrowser = typeof browser !== 'undefined'; | ||
| const isChrome = typeof chrome !== 'undefined'; | ||
| // تعريف browserAPI للاستخدام في العمليات المشتركة مثل tabs.query والمزيد | ||
| const browserAPI = isBrowser ? browser : chrome; | ||
@@ -18,3 +18,2 @@ | ||
| const captureBtn = document.getElementById('capture'); | ||
| const thumbnailBtn = document.getElementById('thumbnail'); | ||
| const settingsToggle = document.getElementById('settingsToggle'); | ||
@@ -33,3 +32,3 @@ const settingsContent = document.getElementById('settingsContent'); | ||
| // Messages cache to avoid repeated lookups | ||
| // Messages cache | ||
| const messagesCache = { | ||
@@ -40,22 +39,6 @@ en: {}, | ||
| // Elements that need localization | ||
| const elementsToLocalize = { | ||
| 'capture': 'captureBtn', | ||
| 'thumbnail': 'thumbnailBtn', | ||
| 'settingsLabel': 'settings', | ||
| 'qualityLabel': 'captureQuality', | ||
| 'formatLabel': 'fileFormat', | ||
| 'filenameLabel': 'fileName', | ||
| 'saveSettings': 'saveSettings', | ||
| 'resetSettings': 'resetSettings', | ||
| 'highQualityOption': 'highQuality', | ||
| 'mediumQualityOption': 'mediumQuality', | ||
| 'lowQualityOption': 'lowQuality' | ||
| }; | ||
| // Hard-coded messages as fallback in case Browser API fails | ||
| // Fallback messages | ||
| const fallbackMessages = { | ||
| en: { | ||
| captureBtn: "📸 Capture Frame", | ||
| thumbnailBtn: "🖼️ Download Thumbnail", | ||
| settings: "Settings", | ||
@@ -71,8 +54,7 @@ captureQuality: "Capture Quality", | ||
| captureSuccess: "Frame captured successfully!", | ||
| thumbnailSuccess: "Thumbnail downloaded successfully!", | ||
| thumbnailError: "Error downloading thumbnail" | ||
| shortcutLabel: "Shortcut:", | ||
| changeShortcut: "Change Shortcut" | ||
| }, | ||
| ar: { | ||
| captureBtn: "📸 التقط الإطار", | ||
| thumbnailBtn: "🖼️ تحميل الصورة المصغرة", | ||
| settings: "الإعدادات", | ||
@@ -88,24 +70,21 @@ captureQuality: "جودة الالتقاط", | ||
| captureSuccess: "تم التقاط الإطار بنجاح!", | ||
| thumbnailSuccess: "تم تحميل الصورة المصغرة بنجاح!", | ||
| thumbnailError: "خطأ في تحميل الصورة المصغرة" | ||
| shortcutLabel: "الاختصار:", | ||
| changeShortcut: "تغيير الاختصار" | ||
| } | ||
| }; | ||
| // Load settings and setup UI | ||
| // Initialize Popup | ||
| document.addEventListener('DOMContentLoaded', async () => { | ||
| console.log('Popup initialized'); | ||
| // Load saved settings or set defaults | ||
| // Load settings | ||
| const settings = await loadSettings(); | ||
| console.log('Loaded settings:', settings); | ||
| // Apply settings to UI | ||
| qualitySelect.value = settings.quality; | ||
| formatSelect.value = settings.format; | ||
| filenameInput.value = settings.filename; | ||
| // Set active language button | ||
| if (qualitySelect) qualitySelect.value = settings.quality; | ||
| if (formatSelect) formatSelect.value = settings.format; | ||
| if (filenameInput) filenameInput.value = settings.filename; | ||
| // Set language | ||
| const currentLang = settings.language || 'en'; | ||
| console.log('Current language:', currentLang); | ||
| languageBtns.forEach(btn => { | ||
@@ -118,123 +97,134 @@ if (btn.dataset.lang === currentLang) { | ||
| }); | ||
| // Set language | ||
| await setLanguage(currentLang); | ||
| // Initially hide settings | ||
| settingsContent.style.display = 'none'; | ||
| // Hide settings initially | ||
| if (settingsContent) settingsContent.style.display = 'none'; | ||
| // Load shortcut | ||
| await updateShortcutDisplay(); | ||
| // Initialize Thumbnail Tool | ||
| await initThumbnailTool(); | ||
| }); | ||
| // Inject content script based on browser type | ||
| async function injectContentScript(tabId) { | ||
| // Initialize Thumbnail Tool | ||
| async function initThumbnailTool() { | ||
| const videoUrlDisplay = document.getElementById('videoUrlDisplay'); | ||
| const openThumbnailToolBtn = document.getElementById('openThumbnailTool'); | ||
| if (!videoUrlDisplay || !openThumbnailToolBtn) return; | ||
| try { | ||
| // Unified approach for Firefox and Chrome in Manifest V3 | ||
| const browserScripting = isBrowser ? browser.scripting : chrome.scripting; | ||
| if (browserScripting) { | ||
| await browserScripting.executeScript({ | ||
| target: { tabId: tabId }, | ||
| files: ['lib/browser-polyfill.js', 'scripts/content.js'] | ||
| }); | ||
| const tabs = await browserAPI.tabs.query({ active: true, currentWindow: true }); | ||
| if (tabs.length > 0 && tabs[0].url && tabs[0].url.includes('youtube.com/watch')) { | ||
| videoUrlDisplay.value = tabs[0].url; | ||
| openThumbnailToolBtn.disabled = false; | ||
| openThumbnailToolBtn.style.opacity = '1'; | ||
| openThumbnailToolBtn.style.cursor = 'pointer'; | ||
| } else { | ||
| throw new Error('Scripting API not available'); | ||
| videoUrlDisplay.value = "No YouTube video detected"; | ||
| openThumbnailToolBtn.disabled = true; | ||
| openThumbnailToolBtn.style.opacity = '0.6'; | ||
| openThumbnailToolBtn.style.cursor = 'not-allowed'; | ||
| } | ||
| // Small delay to ensure content script is fully loaded | ||
| return new Promise(resolve => setTimeout(resolve, 100)); | ||
| // Remove old listeners to avoid duplicates (though DOMContentLoaded runs once) | ||
| const newBtn = openThumbnailToolBtn.cloneNode(true); | ||
| openThumbnailToolBtn.parentNode.replaceChild(newBtn, openThumbnailToolBtn); | ||
| newBtn.addEventListener('click', () => { | ||
| const url = videoUrlDisplay.value; | ||
| if (url && url.includes('youtube.com')) { | ||
| // Open external tool with the video URL as a parameter | ||
| const thumbUrl = `https://thumz.vercel.app/?video=${encodeURIComponent(url)}`; | ||
| browserAPI.tabs.create({ url: thumbUrl }); | ||
| } | ||
| }); | ||
| } catch (error) { | ||
| console.error("Script injection error:", error); | ||
| throw new Error(`Cannot inject script: ${error.message}. This extension requires permission to run scripts.`); | ||
| console.error('Error initializing thumbnail tool:', error); | ||
| } | ||
| } | ||
| // Capture button click | ||
| captureBtn.addEventListener('click', async () => { | ||
| const settings = await loadSettings(); | ||
| // Show Capturing message | ||
| showStatus('Capturing...', 'info'); | ||
| // Update shortcut display | ||
| async function updateShortcutDisplay() { | ||
| try { | ||
| // Query for active tab | ||
| const tabs = await browserAPI.tabs.query({ active: true, currentWindow: true }); | ||
| if (tabs[0].url.includes('youtube.com/watch')) { | ||
| try { | ||
| // Try to send a ping message to check if content script is loaded | ||
| await sendMessageToTab(tabs[0].id, { action: 'ping' }); | ||
| // If no error, content script is already loaded, send the capture message | ||
| await handleCaptureMessage(tabs[0].id, settings); | ||
| } catch (err) { | ||
| // Content script not loaded or error occurred, try to inject it | ||
| console.log('Content script not loaded, injecting:', err); | ||
| try { | ||
| // Use our shared function to inject the content script | ||
| await injectContentScript(tabs[0].id); | ||
| // Now send the capture message | ||
| await handleCaptureMessage(tabs[0].id, settings); | ||
| } catch (injectError) { | ||
| showStatus('Error injecting script: ' + injectError.message, 'error'); | ||
| } | ||
| const commands = await browserAPI.commands.getAll(); | ||
| const captureCommand = commands.find(c => c.name === 'capture_frame'); | ||
| if (captureCommand) { | ||
| const shortcut = captureCommand.shortcut || 'Not set'; | ||
| const shortcutElement = document.getElementById('currentShortcut'); | ||
| if (shortcutElement) { | ||
| shortcutElement.textContent = shortcut; | ||
| } | ||
| } else { | ||
| const currentLang = document.querySelector('.language-btn.active').dataset.lang || 'en'; | ||
| const messages = { | ||
| en: 'Please navigate to a YouTube video page first', | ||
| ar: 'يرجى الانتقال إلى صفحة فيديو YouTube أولاً' | ||
| }; | ||
| showStatus(messages[currentLang], 'error'); | ||
| } | ||
| } catch (error) { | ||
| showStatus('Error: ' + (error.message || 'Unknown error'), 'error'); | ||
| console.error('Error fetching commands:', error); | ||
| } | ||
| }); | ||
| } | ||
| // Thumbnail button click | ||
| thumbnailBtn.addEventListener('click', async () => { | ||
| const settings = await loadSettings(); | ||
| // Show downloading message | ||
| showStatus('Downloading thumbnail...', 'info'); | ||
| try { | ||
| // Query for active tab | ||
| const tabs = await browserAPI.tabs.query({ active: true, currentWindow: true }); | ||
| if (tabs[0].url.includes('youtube.com/watch')) { | ||
| try { | ||
| // Try to send a ping message to check if content script is loaded | ||
| await sendMessageToTab(tabs[0].id, { action: 'ping' }); | ||
| // If no error, content script is already loaded, send the thumbnail message | ||
| await handleThumbnailMessage(tabs[0].id, settings); | ||
| } catch (err) { | ||
| // Content script not loaded or error occurred, try to inject it | ||
| console.log('Content script not loaded, injecting:', err); | ||
| // Change shortcut button | ||
| const changeShortcutBtn = document.getElementById('changeShortcut'); | ||
| if (changeShortcutBtn) { | ||
| changeShortcutBtn.addEventListener('click', () => { | ||
| browserAPI.tabs.create({ url: 'chrome://extensions/shortcuts' }); | ||
| }); | ||
| } | ||
| // Capture button click | ||
| if (captureBtn) { | ||
| captureBtn.addEventListener('click', async () => { | ||
| const settings = await loadSettings(); | ||
| showStatus('Capturing...', 'info'); | ||
| try { | ||
| const tabs = await browserAPI.tabs.query({ active: true, currentWindow: true }); | ||
| if (tabs[0].url.includes('youtube.com/watch')) { | ||
| try { | ||
| // Use our shared function to inject the content script | ||
| await injectContentScript(tabs[0].id); | ||
| // Now send the thumbnail message | ||
| await handleThumbnailMessage(tabs[0].id, settings); | ||
| } catch (injectError) { | ||
| showStatus('Error injecting script: ' + injectError.message, 'error'); | ||
| await sendMessageToTab(tabs[0].id, { action: 'ping' }); | ||
| await handleCaptureMessage(tabs[0].id, settings); | ||
| } catch (err) { | ||
| console.log('Content script not loaded, injecting:', err); | ||
| try { | ||
| await injectContentScript(tabs[0].id); | ||
| await handleCaptureMessage(tabs[0].id, settings); | ||
| } catch (injectError) { | ||
| showStatus('Error injecting script: ' + injectError.message, 'error'); | ||
| } | ||
| } | ||
| } else { | ||
| const currentLang = document.querySelector('.language-btn.active').dataset.lang || 'en'; | ||
| const messages = { | ||
| en: 'Please navigate to a YouTube video page first', | ||
| ar: 'يرجى الانتقال إلى صفحة فيديو YouTube أولاً' | ||
| }; | ||
| showStatus(messages[currentLang], 'error'); | ||
| } | ||
| } catch (error) { | ||
| showStatus('Error: ' + (error.message || 'Unknown error'), 'error'); | ||
| } | ||
| }); | ||
| } | ||
| // Inject content script | ||
| async function injectContentScript(tabId) { | ||
| try { | ||
| const browserScripting = isBrowser ? browser.scripting : chrome.scripting; | ||
| if (browserScripting) { | ||
| await browserScripting.executeScript({ | ||
| target: { tabId: tabId }, | ||
| files: ['lib/browser-polyfill.js', 'scripts/content.js'] | ||
| }); | ||
| } else { | ||
| const currentLang = document.querySelector('.language-btn.active').dataset.lang || 'en'; | ||
| const messages = { | ||
| en: 'Please navigate to a YouTube video page first', | ||
| ar: 'يرجى الانتقال إلى صفحة فيديو YouTube أولاً' | ||
| }; | ||
| showStatus(messages[currentLang], 'error'); | ||
| throw new Error('Scripting API not available'); | ||
| } | ||
| return new Promise(resolve => setTimeout(resolve, 100)); | ||
| } catch (error) { | ||
| const errorMsg = getMessage('thumbnailError'); | ||
| showStatus(errorMsg + ': ' + (error.message || ''), 'error'); | ||
| console.error("Script injection error:", error); | ||
| throw new Error(`Cannot inject script: ${error.message}`); | ||
| } | ||
| }); | ||
| } | ||
| // Helper function to handle the capture message | ||
| // Handle capture message | ||
| async function handleCaptureMessage(tabId, settings) { | ||
@@ -246,3 +236,3 @@ try { | ||
| }); | ||
| if (response === true) { | ||
@@ -259,36 +249,18 @@ const successMsg = getMessage('captureSuccess'); | ||
| // Helper function to handle the thumbnail message | ||
| async function handleThumbnailMessage(tabId, settings) { | ||
| // Send message to tab | ||
| async function sendMessageToTab(tabId, message) { | ||
| try { | ||
| const response = await sendMessageToTab(tabId, { | ||
| action: 'downloadThumbnail', | ||
| language: settings.language | ||
| }); | ||
| if (response === true) { | ||
| const successMsg = getMessage('thumbnailSuccess'); | ||
| showStatus(successMsg, 'success'); | ||
| } else { | ||
| const errorMsg = getMessage('thumbnailError'); | ||
| showStatus(errorMsg + ': ' + (response || ''), 'error'); | ||
| } | ||
| return await browserAPI.tabs.sendMessage(tabId, message); | ||
| } catch (error) { | ||
| const errorMsg = getMessage('thumbnailError'); | ||
| showStatus(errorMsg + ': ' + (error.message || ''), 'error'); | ||
| throw error; | ||
| } | ||
| } | ||
| // Helper function to send a message to a tab | ||
| function sendMessageToTab(tabId, message) { | ||
| return new Promise((resolve, reject) => { | ||
| try { | ||
| browserAPI.tabs.sendMessage(tabId, message, response => { | ||
| if (browserAPI.runtime.lastError) { | ||
| reject(new Error(browserAPI.runtime.lastError.message)); | ||
| } else { | ||
| resolve(response); | ||
| } | ||
| }); | ||
| } catch (err) { | ||
| reject(err); | ||
| // Settings toggle | ||
| if (settingsToggle) { | ||
| settingsToggle.addEventListener('click', () => { | ||
| if (settingsContent.style.display === 'none') { | ||
| settingsContent.style.display = 'block'; | ||
| } else { | ||
| settingsContent.style.display = 'none'; | ||
| } | ||
@@ -298,47 +270,41 @@ }); | ||
| // Settings toggle | ||
| settingsToggle.addEventListener('click', () => { | ||
| if (settingsContent.style.display === 'none') { | ||
| settingsContent.style.display = 'block'; | ||
| } else { | ||
| settingsContent.style.display = 'none'; | ||
| } | ||
| }); | ||
| // Save settings | ||
| saveSettingsBtn.addEventListener('click', async () => { | ||
| const settings = { | ||
| quality: qualitySelect.value, | ||
| format: formatSelect.value, | ||
| filename: filenameInput.value, | ||
| language: document.querySelector('.language-btn.active').dataset.lang | ||
| }; | ||
| await saveSettings(settings); | ||
| showStatus('Settings saved', 'success'); | ||
| }); | ||
| if (saveSettingsBtn) { | ||
| saveSettingsBtn.addEventListener('click', async () => { | ||
| const settings = { | ||
| quality: qualitySelect.value, | ||
| format: formatSelect.value, | ||
| filename: filenameInput.value, | ||
| language: document.querySelector('.language-btn.active').dataset.lang | ||
| }; | ||
| await saveSettings(settings); | ||
| showStatus('Settings saved', 'success'); | ||
| }); | ||
| } | ||
| // Reset settings | ||
| resetSettingsBtn.addEventListener('click', async () => { | ||
| await saveSettings(DEFAULT_SETTINGS); | ||
| // Update UI | ||
| qualitySelect.value = DEFAULT_SETTINGS.quality; | ||
| formatSelect.value = DEFAULT_SETTINGS.format; | ||
| filenameInput.value = DEFAULT_SETTINGS.filename; | ||
| // Update language | ||
| const lang = DEFAULT_SETTINGS.language; | ||
| languageBtns.forEach(btn => { | ||
| if (btn.dataset.lang === lang) { | ||
| btn.classList.add('active'); | ||
| } else { | ||
| btn.classList.remove('active'); | ||
| } | ||
| if (resetSettingsBtn) { | ||
| resetSettingsBtn.addEventListener('click', async () => { | ||
| await saveSettings(DEFAULT_SETTINGS); | ||
| // Update UI | ||
| qualitySelect.value = DEFAULT_SETTINGS.quality; | ||
| formatSelect.value = DEFAULT_SETTINGS.format; | ||
| filenameInput.value = DEFAULT_SETTINGS.filename; | ||
| // Update language | ||
| const lang = DEFAULT_SETTINGS.language; | ||
| languageBtns.forEach(btn => { | ||
| if (btn.dataset.lang === lang) { | ||
| btn.classList.add('active'); | ||
| } else { | ||
| btn.classList.remove('active'); | ||
| } | ||
| }); | ||
| await setLanguage(lang); | ||
| showStatus('Settings reset to default', 'success'); | ||
| }); | ||
| await setLanguage(lang); | ||
| showStatus('Settings reset to default', 'success'); | ||
| }); | ||
| } | ||
@@ -349,99 +315,82 @@ // Language buttons | ||
| const lang = btn.dataset.lang; | ||
| // Update active class | ||
| languageBtns.forEach(b => b.classList.remove('active')); | ||
| btn.classList.add('active'); | ||
| // Load settings and update language | ||
| const settings = await loadSettings(); | ||
| settings.language = lang; | ||
| await saveSettings(settings); | ||
| // Update UI language | ||
| await setLanguage(lang); | ||
| console.log(`Language switched to ${lang}`); | ||
| }); | ||
| }); | ||
| // Helper functions | ||
| // Helper: Load Settings | ||
| async function loadSettings() { | ||
| return new Promise((resolve) => { | ||
| browserAPI.storage.sync.get('settings', (result) => { | ||
| resolve(result.settings || DEFAULT_SETTINGS); | ||
| }); | ||
| }); | ||
| try { | ||
| const result = await browserAPI.storage.sync.get('settings'); | ||
| return result.settings || DEFAULT_SETTINGS; | ||
| } catch (error) { | ||
| console.error('Error loading settings:', error); | ||
| return DEFAULT_SETTINGS; | ||
| } | ||
| } | ||
| // Helper: Save Settings | ||
| async function saveSettings(settings) { | ||
| return new Promise((resolve) => { | ||
| browserAPI.storage.sync.set({ settings }, resolve); | ||
| }); | ||
| try { | ||
| await browserAPI.storage.sync.set({ settings }); | ||
| } catch (error) { | ||
| console.error('Error saving settings:', error); | ||
| } | ||
| } | ||
| // Get message by key based on current language | ||
| // Helper: Get Message | ||
| function getMessage(messageName) { | ||
| const lang = document.querySelector('.language-btn.active').dataset.lang || 'en'; | ||
| // If we have cached this message, return it | ||
| if (messagesCache[lang] && messagesCache[lang][messageName]) { | ||
| return messagesCache[lang][messageName]; | ||
| } | ||
| // Try to get message from Browser API | ||
| let message = browserAPI.i18n.getMessage(messageName); | ||
| // If Browser API fails, use our fallback | ||
| if (!message && fallbackMessages[lang] && fallbackMessages[lang][messageName]) { | ||
| message = fallbackMessages[lang][messageName]; | ||
| } | ||
| // Cache the result | ||
| if (!messagesCache[lang]) { | ||
| messagesCache[lang] = {}; | ||
| } | ||
| if (!messagesCache[lang]) messagesCache[lang] = {}; | ||
| messagesCache[lang][messageName] = message || messageName; | ||
| return message || messageName; | ||
| } | ||
| // Helper: Set Language | ||
| function setLanguage(lang) { | ||
| console.log(`Setting language to: ${lang}`); | ||
| // Update active class on language buttons | ||
| document.querySelectorAll('.language-btn').forEach(btn => { | ||
| if (btn.dataset.lang === lang) { | ||
| btn.classList.add('active'); | ||
| } else { | ||
| btn.classList.remove('active'); | ||
| } | ||
| if (btn.dataset.lang === lang) btn.classList.add('active'); | ||
| else btn.classList.remove('active'); | ||
| }); | ||
| // Add RTL class for Arabic language | ||
| if (lang === 'ar') { | ||
| document.body.classList.add('rtl'); | ||
| document.documentElement.setAttribute('lang', 'ar'); | ||
| // Fix margin for RTL | ||
| const settingsHeaders = document.querySelectorAll('.settings-header h3'); | ||
| settingsHeaders.forEach(header => { | ||
| header.style.marginRight = '5px'; | ||
| header.style.marginLeft = '0'; | ||
| }); | ||
| if (settingsHeaders) { | ||
| settingsHeaders.forEach(header => { | ||
| header.style.marginRight = '5px'; | ||
| header.style.marginLeft = '0'; | ||
| }); | ||
| } | ||
| } else { | ||
| document.body.classList.remove('rtl'); | ||
| document.documentElement.setAttribute('lang', 'en'); | ||
| // Reset margin for LTR | ||
| const settingsHeaders = document.querySelectorAll('.settings-header h3'); | ||
| settingsHeaders.forEach(header => { | ||
| header.style.marginLeft = '5px'; | ||
| header.style.marginRight = '0'; | ||
| }); | ||
| if (settingsHeaders) { | ||
| settingsHeaders.forEach(header => { | ||
| header.style.marginLeft = '5px'; | ||
| header.style.marginRight = '0'; | ||
| }); | ||
| } | ||
| } | ||
| // Make sure all elements exist before updating | ||
| if (captureBtn) captureBtn.textContent = fallbackMessages[lang].captureBtn; | ||
| if (thumbnailBtn) thumbnailBtn.textContent = fallbackMessages[lang].thumbnailBtn; | ||
| const elements = { | ||
@@ -456,19 +405,15 @@ 'settingsLabel': fallbackMessages[lang].settings, | ||
| 'mediumQualityOption': fallbackMessages[lang].mediumQuality, | ||
| 'lowQualityOption': fallbackMessages[lang].lowQuality | ||
| 'lowQualityOption': fallbackMessages[lang].lowQuality, | ||
| 'shortcutLabel': fallbackMessages[lang].shortcutLabel, | ||
| 'changeShortcut': fallbackMessages[lang].changeShortcut | ||
| }; | ||
| // Update each element if it exists | ||
| for (const [id, text] of Object.entries(elements)) { | ||
| const element = document.getElementById(id); | ||
| if (element) { | ||
| element.textContent = text; | ||
| console.log(`Updated ${id} to: ${text}`); | ||
| } else { | ||
| console.warn(`Element with id ${id} not found`); | ||
| } | ||
| if (element) element.textContent = text; | ||
| } | ||
| } | ||
| // Helper: Show Status | ||
| function showStatus(message, type) { | ||
| // Translate common messages | ||
| const currentLang = document.querySelector('.language-btn.active').dataset.lang || 'en'; | ||
@@ -478,3 +423,2 @@ const commonMessages = { | ||
| 'Capturing...': 'Capturing...', | ||
| 'Downloading thumbnail...': 'Downloading thumbnail...', | ||
| 'Settings saved': 'Settings saved', | ||
@@ -485,3 +429,2 @@ 'Settings reset to default': 'Settings reset to default' | ||
| 'Capturing...': 'جاري الالتقاط...', | ||
| 'Downloading thumbnail...': 'جاري تحميل الصورة المصغرة...', | ||
| 'Settings saved': 'تم حفظ الإعدادات', | ||
@@ -492,14 +435,13 @@ 'Settings reset to default': 'تم إعادة الإعدادات للوضع الافتراضي' | ||
| // Translate message if it's one of the common ones | ||
| if (commonMessages[currentLang][message]) { | ||
| message = commonMessages[currentLang][message]; | ||
| } | ||
| statusMessage.textContent = message; | ||
| statusMessage.className = 'status-message ' + type; | ||
| statusMessage.style.display = 'block'; | ||
| setTimeout(() => { | ||
| statusMessage.style.display = 'none'; | ||
| }, 3000); | ||
| } | ||
| } |
-19
| (function() { | ||
| const video = document.querySelector('video'); | ||
| if (!video) { | ||
| alert('لم يتم العثور على الفيديو!'); | ||
| return; | ||
| } | ||
| const canvas = document.createElement('canvas'); | ||
| canvas.width = video.videoWidth; | ||
| canvas.height = video.videoHeight; | ||
| const ctx = canvas.getContext('2d'); | ||
| ctx.drawImage(video, 0, 0, canvas.width, canvas.height); | ||
| const link = document.createElement('a'); | ||
| link.download = 'frame.png'; | ||
| link.href = canvas.toDataURL(); | ||
| link.click(); | ||
| })(); |
-10
| <!DOCTYPE html> | ||
| <html> | ||
| <head> | ||
| <title>Capture Frame</title> | ||
| </head> | ||
| <body> | ||
| <button id="capture">📸 التقط الإطار</button> | ||
| <script src="popup.js"></script> | ||
| </body> | ||
| </html> |
-6
| document.getElementById('capture').addEventListener('click', async () => { | ||
| chrome.scripting.executeScript({ | ||
| target: {tabId: (await chrome.tabs.query({active: true, currentWindow: true}))[0].id}, | ||
| files: ['content.js'] | ||
| }); | ||
| }); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet