New:Socket for Asana Is Now Available.Learn more
Get Started

Screenshot for YouTube™ | 4K Frame & Thumbnail Cap

Package Overview
Versions
2
Alerts
File Explorer
Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

youtube-frame-extractor@example.com - firefox Package Compare versions

Comparing version
1.2.0
to
1.2.1
+211
thumb.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Thumz - YouTube Thumbnail Tool</title>
<style>
:root {
--primary-color: #2196F3;
--primary-hover: #1976D2;
--bg-color: #f5f7fa;
--card-bg: #ffffff;
--text-color: #333333;
--border-color: #e0e0e0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
width: 100%;
max-width: 600px;
padding: 20px;
}
.card {
background-color: var(--card-bg);
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 30px;
text-align: center;
}
h1 {
margin-top: 0;
color: var(--primary-color);
font-size: 24px;
margin-bottom: 20px;
}
.input-group {
margin-bottom: 20px;
text-align: left;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 600;
font-size: 14px;
}
input[type="text"] {
width: 100%;
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 8px;
font-size: 16px;
box-sizing: border-box;
transition: border-color 0.3s;
}
input[type="text"]:focus {
border-color: var(--primary-color);
outline: none;
}
.btn {
background-color: var(--primary-color);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.3s;
width: 100%;
}
.btn:hover {
background-color: var(--primary-hover);
}
.preview-area {
margin-top: 30px;
border-top: 1px solid var(--border-color);
padding-top: 20px;
display: none;
}
.preview-placeholder {
background-color: #eee;
height: 200px;
border-radius: 8px;
display: flex;
justify-content: center;
align-items: center;
color: #888;
margin-bottom: 15px;
}
.preview-img {
max-width: 100%;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.status-msg {
margin-top: 10px;
font-size: 14px;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="card">
<h1>Thumz Downloader</h1>
<div class="input-group">
<label for="videoUrl">YouTube Video URL / ID</label>
<input type="text" id="videoUrl" placeholder="Enter YouTube URL or ID...">
</div>
<button id="generateBtn" class="btn">Generate Thumbnail</button>
<div id="previewArea" class="preview-area">
<h3>Preview</h3>
<div id="imageContainer">
<div class="preview-placeholder">
<span>Image Preview Will Appear Here</span>
</div>
</div>
<p class="status-msg">
Processing on external server...
</p>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const videoInput = document.getElementById('videoUrl');
const generateBtn = document.getElementById('generateBtn');
const previewArea = document.getElementById('previewArea');
const imageContainer = document.getElementById('imageContainer');
// 1. Read 'video' query parameter
const urlParams = new URLSearchParams(window.location.search);
const videoParam = urlParams.get('video');
if (videoParam) {
videoInput.value = videoParam;
// Auto-click generate if URL is present
// setTimeout(() => generateBtn.click(), 500);
}
// 2. Handle Generate Button
generateBtn.addEventListener('click', () => {
const val = videoInput.value.trim();
if (!val) {
alert('Please enter a video URL or ID');
return;
}
generateThumbnail(val);
});
// 3. Generation Logic
function generateThumbnail(videoValue) {
previewArea.style.display = 'block';
// Extract ID
let videoId = videoValue;
const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
const match = videoValue.match(regExp);
if (match && match[7].length == 11) {
videoId = match[7];
}
console.log('Processing video ID:', videoId);
// Display High Res Thumbnail directly from YouTube (Client-side)
// This is safe on a standard web page (not extension popup)
const maxResUrl = `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`;
imageContainer.innerHTML = `
<img src="${maxResUrl}" class="preview-img" alt="Thumbnail">
<div style="margin-top: 15px;">
<a href="${maxResUrl}" download="thumbnail.jpg" target="_blank" class="btn" style="display: inline-block; text-decoration: none; width: auto;">Download HD Image</a>
</div>
`;
}
});
</script>
</body>
</html>
+318
-140
:root {
--primary-color: #3d85c6;
--secondary-color: #f1f1f1;
--text-color: #333;
--light-text: #777;
--border-color: #ddd;
--success-color: #4caf50;
--error-color: #f44336;
--accent-color: #ff5722;
--primary-gradient: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
--secondary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--accent-gradient: linear-gradient(135deg, #ff9a9e 0%, #fecfef 99%, #fecfef 100%);
--gold-gradient: linear-gradient(135deg, #f6d365 0%, #fda085 100%);
--bg-color: #ffffff;
--surface-color: #f8f9fa;
--text-primary: #2d3748;
--text-secondary: #718096;
--border-color: #e2e8f0;
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-colored: 0 4px 14px 0 rgba(0, 118, 255, 0.39);
--radius-md: 8px;
--radius-lg: 12px;
--radius-full: 9999px;
}

@@ -16,24 +26,25 @@

box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
}
body {
width: 320px;
min-height: 200px;
padding: 15px;
color: var(--text-color);
background-color: #fff;
width: 350px;
background-color: var(--bg-color);
color: var(--text-primary);
overflow-x: hidden;
}
.rtl {
direction: rtl;
text-align: right;
.popup-container {
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
/* Header */
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
justify-content: space-between;
margin-bottom: 15px;
padding-bottom: 10px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-color);

@@ -43,189 +54,356 @@ }

.popup-title {
font-weight: 700;
font-size: 16px;
font-weight: 600;
color: var(--primary-color);
display: flex;
align-items: center;
gap: 6px;
background: var(--secondary-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.popup-title .icon {
font-size: 18px;
-webkit-text-fill-color: initial; /* Don't apply gradient to emoji */
}
/* Language Selector */
.language-selector {
display: flex;
gap: 5px;
background: var(--surface-color);
padding: 2px;
border-radius: var(--radius-md);
border: 1px solid var(--border-color);
}
.language-btn {
background-color: white;
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 3px 6px;
border: none;
background: transparent;
padding: 4px 8px;
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
cursor: pointer;
font-size: 12px;
min-width: 30px;
height: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
text-align: center;
color: var(--text-color);
font-weight: normal;
border-radius: 6px;
transition: all 0.2s;
}
.language-btn:hover {
background-color: var(--secondary-color);
opacity: 1;
.language-btn.active {
background: white;
color: var(--text-primary);
box-shadow: var(--shadow-sm);
}
.language-btn.active {
background-color: var(--primary-color);
/* Hero Section (Capture) */
.hero-section {
text-align: center;
}
.primary-action-btn {
width: 100%;
padding: 14px;
background: var(--primary-gradient);
border: none;
border-radius: var(--radius-lg);
color: white;
border-color: var(--primary-color);
font-weight: 700;
font-size: 16px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
box-shadow: var(--shadow-colored);
}
/* تعريف خاص للزر الإنجليزي */
#en-btn {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
.primary-action-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 118, 255, 0.23);
}
/* تعريف خاص للزر العربي */
#ar-btn {
font-family: 'Tahoma', 'Segoe UI', sans-serif;
.primary-action-btn:active {
transform: translateY(0);
}
.capture-section {
margin-bottom: 15px;
/* Growth Section */
.growth-section {
display: flex;
flex-direction: column;
gap: 10px;
}
.capture-btn {
background-color: var(--primary-color);
.growth-btn {
flex: 1;
padding: 10px;
border: 1px solid var(--border-color);
background: white;
border-radius: var(--radius-md);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
font-weight: 600;
font-size: 13px;
transition: all 0.2s;
color: var(--text-primary);
}
.thumbnail-btn {
background-color: var(--accent-color);
.growth-btn:hover {
background: var(--surface-color);
transform: translateY(-1px);
border-color: #cbd5e0;
}
button {
width: 100%;
padding: 10px 12px;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s ease;
.share-btn .btn-icon {
font-size: 16px;
}
button:hover {
opacity: 0.9;
.rate-btn {
border-color: #f6d365;
background: linear-gradient(to right, #fffdf5, #fff);
}
button:active {
transform: scale(0.98);
.rate-btn .btn-text {
background: var(--gold-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.settings-section {
margin-top: 20px;
.rate-btn .btn-icon {
color: orange; /* Fallback */
}
.settings-header {
/* Shortcuts */
.shortcut-section {
background: var(--surface-color);
padding: 10px 12px;
border-radius: var(--radius-md);
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
font-size: 13px;
}
.shortcut-info {
display: flex;
align-items: center;
gap: 8px;
color: var(--text-secondary);
}
.shortcut-key {
background: white;
border: 1px solid var(--border-color);
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
font-weight: 600;
color: var(--text-primary);
}
.link-btn {
background: none;
border: none;
color: #4facfe;
cursor: pointer;
font-size: 12px;
font-weight: 600;
}
.settings-header h3 {
font-size: 14px;
margin-left: 5px;
.link-btn:hover {
text-decoration: underline;
}
/* Tools & Settings */
.tools-section {
border-top: 1px solid var(--border-color);
padding-top: 12px;
}
.tools-header {
margin-bottom: 8px;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
color: var(--text-secondary);
letter-spacing: 0.5px;
}
.thumbnail-tool-container {
background: var(--surface-color);
border-radius: var(--radius-md);
padding: 10px;
margin-bottom: 10px;
}
.secondary-btn {
width: 100%;
padding: 8px;
background: white;
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-primary);
font-weight: 600;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.secondary-btn:hover {
border-color: #4facfe;
color: #4facfe;
}
.secondary-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
border-color: var(--border-color);
color: var(--text-secondary);
}
.video-status {
margin-top: 6px;
font-size: 11px;
color: var(--text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.settings-toggle-container {
display: flex;
justify-content: flex-end;
}
.icon-btn {
background: none;
border: none;
cursor: pointer;
font-size: 18px;
opacity: 0.7;
transition: opacity 0.2s;
padding: 4px;
}
.icon-btn:hover {
opacity: 1;
}
.settings-content {
margin-bottom: 15px;
display: none;
background: var(--surface-color);
margin-top: 10px;
padding: 12px;
border-radius: var(--radius-md);
border: 1px solid var(--border-color);
animation: slideDown 0.2s ease-out;
}
@keyframes slideDown {
from { opacity: 0; transform: translateY(-5px); }
to { opacity: 1; transform: translateY(0); }
}
.form-group {
margin-bottom: 10px;
margin-bottom: 10px;
}
label {
display: block;
margin-bottom: 5px;
font-size: 13px;
color: var(--light-text);
.form-group label {
display: block;
font-size: 12px;
font-weight: 600;
margin-bottom: 4px;
color: var(--text-secondary);
}
select, input {
width: 100%;
padding: 8px 10px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 13px;
.form-group select, .form-group input {
width: 100%;
padding: 8px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 13px;
outline: none;
transition: border-color 0.2s;
}
.form-group select:focus, .form-group input:focus {
border-color: #4facfe;
}
.action-buttons {
display: flex;
gap: 10px;
margin-top: 15px;
display: flex;
gap: 8px;
margin-top: 12px;
}
.save-btn, .reset-btn {
flex: 1;
padding: 8px;
border: none;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s;
}
.save-btn {
background: var(--text-primary);
color: white;
}
.reset-btn {
background-color: #f1f1f1;
color: var(--text-color);
background: #e2e8f0;
color: var(--text-primary);
}
.reset-btn:hover {
background-color: #e5e5e5;
.save-btn:hover, .reset-btn:hover {
opacity: 0.9;
}
/* Status Message */
.status-message {
margin-top: 10px;
padding: 8px;
border-radius: 4px;
font-size: 13px;
text-align: center;
padding: 10px;
border-radius: var(--radius-md);
font-size: 13px;
text-align: center;
font-weight: 500;
animation: fadeIn 0.3s;
display: none; /* Controlled by JS */
}
.success {
background-color: #e8f5e9;
color: var(--success-color);
.status-message.success {
background-color: #d1fae5;
color: #065f46;
}
.error {
background-color: #ffebee;
color: var(--error-color);
.status-message.error {
background-color: #fee2e2;
color: #991b1b;
}
/* Responsiveness */
@media (prefers-color-scheme: dark) {
body {
background-color: #292a2d;
color: #e8eaed;
}
:root {
--text-color: #e8eaed;
--light-text: #9aa0a6;
--border-color: #5f6368;
--secondary-color: #3c4043;
}
.reset-btn {
background-color: #3c4043;
color: #e8eaed;
}
.reset-btn:hover {
background-color: #4a4d51;
}
.success {
background-color: #0d3320;
color: #81c995;
}
.error {
background-color: #3e0008;
color: #f28b82;
}
}
.status-message.info {
background-color: #eff6ff;
color: #1e40af;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* RTL Support */
.rtl {
direction: rtl;
}
.rtl .popup-title {
flex-direction: row-reverse;
}
.rtl .primary-action-btn,
.rtl .growth-btn,
.rtl .shortcut-info {
flex-direction: row-reverse;
}

@@ -1,8 +0,544 @@

(function(a,b){if("function"==typeof define&&define.amd)define("webextension-polyfill",["module"],b);else if("undefined"!=typeof exports)b(module);else{var c={exports:{}};b(c),a.browser=c.exports}})("undefined"==typeof globalThis?"undefined"==typeof self?this:self:globalThis,function(a){"use strict";if(!(globalThis.chrome&&globalThis.chrome.runtime&&globalThis.chrome.runtime.id))throw new Error("This script should only be loaded in a browser extension.");if(!(globalThis.browser&&globalThis.browser.runtime&&globalThis.browser.runtime.id)){a.exports=(a=>{const b={alarms:{clear:{minArgs:0,maxArgs:1},clearAll:{minArgs:0,maxArgs:0},get:{minArgs:0,maxArgs:1},getAll:{minArgs:0,maxArgs:0}},bookmarks:{create:{minArgs:1,maxArgs:1},get:{minArgs:1,maxArgs:1},getChildren:{minArgs:1,maxArgs:1},getRecent:{minArgs:1,maxArgs:1},getSubTree:{minArgs:1,maxArgs:1},getTree:{minArgs:0,maxArgs:0},move:{minArgs:2,maxArgs:2},remove:{minArgs:1,maxArgs:1},removeTree:{minArgs:1,maxArgs:1},search:{minArgs:1,maxArgs:1},update:{minArgs:2,maxArgs:2}},browserAction:{disable:{minArgs:0,maxArgs:1,fallbackToNoCallback:!0},enable:{minArgs:0,maxArgs:1,fallbackToNoCallback:!0},getBadgeBackgroundColor:{minArgs:1,maxArgs:1},getBadgeText:{minArgs:1,maxArgs:1},getPopup:{minArgs:1,maxArgs:1},getTitle:{minArgs:1,maxArgs:1},openPopup:{minArgs:0,maxArgs:0},setBadgeBackgroundColor:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setBadgeText:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setIcon:{minArgs:1,maxArgs:1},setPopup:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setTitle:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0}},browsingData:{remove:{minArgs:2,maxArgs:2},removeCache:{minArgs:1,maxArgs:1},removeCookies:{minArgs:1,maxArgs:1},removeDownloads:{minArgs:1,maxArgs:1},removeFormData:{minArgs:1,maxArgs:1},removeHistory:{minArgs:1,maxArgs:1},removeLocalStorage:{minArgs:1,maxArgs:1},removePasswords:{minArgs:1,maxArgs:1},removePluginData:{minArgs:1,maxArgs:1},settings:{minArgs:0,maxArgs:0}},commands:{getAll:{minArgs:0,maxArgs:0}},contextMenus:{remove:{minArgs:1,maxArgs:1},removeAll:{minArgs:0,maxArgs:0},update:{minArgs:2,maxArgs:2}},cookies:{get:{minArgs:1,maxArgs:1},getAll:{minArgs:1,maxArgs:1},getAllCookieStores:{minArgs:0,maxArgs:0},remove:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}},devtools:{inspectedWindow:{eval:{minArgs:1,maxArgs:2,singleCallbackArg:!1}},panels:{create:{minArgs:3,maxArgs:3,singleCallbackArg:!0},elements:{createSidebarPane:{minArgs:1,maxArgs:1}}}},downloads:{cancel:{minArgs:1,maxArgs:1},download:{minArgs:1,maxArgs:1},erase:{minArgs:1,maxArgs:1},getFileIcon:{minArgs:1,maxArgs:2},open:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},pause:{minArgs:1,maxArgs:1},removeFile:{minArgs:1,maxArgs:1},resume:{minArgs:1,maxArgs:1},search:{minArgs:1,maxArgs:1},show:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0}},extension:{isAllowedFileSchemeAccess:{minArgs:0,maxArgs:0},isAllowedIncognitoAccess:{minArgs:0,maxArgs:0}},history:{addUrl:{minArgs:1,maxArgs:1},deleteAll:{minArgs:0,maxArgs:0},deleteRange:{minArgs:1,maxArgs:1},deleteUrl:{minArgs:1,maxArgs:1},getVisits:{minArgs:1,maxArgs:1},search:{minArgs:1,maxArgs:1}},i18n:{detectLanguage:{minArgs:1,maxArgs:1},getAcceptLanguages:{minArgs:0,maxArgs:0}},identity:{launchWebAuthFlow:{minArgs:1,maxArgs:1}},idle:{queryState:{minArgs:1,maxArgs:1}},management:{get:{minArgs:1,maxArgs:1},getAll:{minArgs:0,maxArgs:0},getSelf:{minArgs:0,maxArgs:0},setEnabled:{minArgs:2,maxArgs:2},uninstallSelf:{minArgs:0,maxArgs:1}},notifications:{clear:{minArgs:1,maxArgs:1},create:{minArgs:1,maxArgs:2},getAll:{minArgs:0,maxArgs:0},getPermissionLevel:{minArgs:0,maxArgs:0},update:{minArgs:2,maxArgs:2}},pageAction:{getPopup:{minArgs:1,maxArgs:1},getTitle:{minArgs:1,maxArgs:1},hide:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setIcon:{minArgs:1,maxArgs:1},setPopup:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setTitle:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},show:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0}},permissions:{contains:{minArgs:1,maxArgs:1},getAll:{minArgs:0,maxArgs:0},remove:{minArgs:1,maxArgs:1},request:{minArgs:1,maxArgs:1}},runtime:{getBackgroundPage:{minArgs:0,maxArgs:0},getPlatformInfo:{minArgs:0,maxArgs:0},openOptionsPage:{minArgs:0,maxArgs:0},requestUpdateCheck:{minArgs:0,maxArgs:0},sendMessage:{minArgs:1,maxArgs:3},sendNativeMessage:{minArgs:2,maxArgs:2},setUninstallURL:{minArgs:1,maxArgs:1}},sessions:{getDevices:{minArgs:0,maxArgs:1},getRecentlyClosed:{minArgs:0,maxArgs:1},restore:{minArgs:0,maxArgs:1}},storage:{local:{clear:{minArgs:0,maxArgs:0},get:{minArgs:0,maxArgs:1},getBytesInUse:{minArgs:0,maxArgs:1},remove:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}},managed:{get:{minArgs:0,maxArgs:1},getBytesInUse:{minArgs:0,maxArgs:1}},sync:{clear:{minArgs:0,maxArgs:0},get:{minArgs:0,maxArgs:1},getBytesInUse:{minArgs:0,maxArgs:1},remove:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}}},tabs:{captureVisibleTab:{minArgs:0,maxArgs:2},create:{minArgs:1,maxArgs:1},detectLanguage:{minArgs:0,maxArgs:1},discard:{minArgs:0,maxArgs:1},duplicate:{minArgs:1,maxArgs:1},executeScript:{minArgs:1,maxArgs:2},get:{minArgs:1,maxArgs:1},getCurrent:{minArgs:0,maxArgs:0},getZoom:{minArgs:0,maxArgs:1},getZoomSettings:{minArgs:0,maxArgs:1},goBack:{minArgs:0,maxArgs:1},goForward:{minArgs:0,maxArgs:1},highlight:{minArgs:1,maxArgs:1},insertCSS:{minArgs:1,maxArgs:2},move:{minArgs:2,maxArgs:2},query:{minArgs:1,maxArgs:1},reload:{minArgs:0,maxArgs:2},remove:{minArgs:1,maxArgs:1},removeCSS:{minArgs:1,maxArgs:2},sendMessage:{minArgs:2,maxArgs:3},setZoom:{minArgs:1,maxArgs:2},setZoomSettings:{minArgs:1,maxArgs:2},update:{minArgs:1,maxArgs:2}},topSites:{get:{minArgs:0,maxArgs:0}},webNavigation:{getAllFrames:{minArgs:1,maxArgs:1},getFrame:{minArgs:1,maxArgs:1}},webRequest:{handlerBehaviorChanged:{minArgs:0,maxArgs:0}},windows:{create:{minArgs:0,maxArgs:1},get:{minArgs:1,maxArgs:2},getAll:{minArgs:0,maxArgs:1},getCurrent:{minArgs:0,maxArgs:1},getLastFocused:{minArgs:0,maxArgs:1},remove:{minArgs:1,maxArgs:1},update:{minArgs:2,maxArgs:2}}};if(0===Object.keys(b).length)throw new Error("api-metadata.json has not been included in browser-polyfill");class c extends WeakMap{constructor(a,b=void 0){super(b),this.createItem=a}get(a){return this.has(a)||this.set(a,this.createItem(a)),super.get(a)}}const d=a=>a&&"object"==typeof a&&"function"==typeof a.then,e=(b,c)=>(...d)=>{a.runtime.lastError?b.reject(new Error(a.runtime.lastError.message)):c.singleCallbackArg||1>=d.length&&!1!==c.singleCallbackArg?b.resolve(d[0]):b.resolve(d)},f=a=>1==a?"argument":"arguments",g=(a,b)=>function(c,...d){if(d.length<b.minArgs)throw new Error(`Expected at least ${b.minArgs} ${f(b.minArgs)} for ${a}(), got ${d.length}`);if(d.length>b.maxArgs)throw new Error(`Expected at most ${b.maxArgs} ${f(b.maxArgs)} for ${a}(), got ${d.length}`);return new Promise((f,g)=>{if(b.fallbackToNoCallback)try{c[a](...d,e({resolve:f,reject:g},b))}catch(e){console.warn(`${a} API method doesn't seem to support the callback parameter, `+"falling back to call it without a callback: ",e),c[a](...d),b.fallbackToNoCallback=!1,b.noCallback=!0,f()}else b.noCallback?(c[a](...d),f()):c[a](...d,e({resolve:f,reject:g},b))})},h=(a,b,c)=>new Proxy(b,{apply(b,d,e){return c.call(d,a,...e)}});let i=Function.call.bind(Object.prototype.hasOwnProperty);const j=(a,b={},c={})=>{let d=Object.create(null),e=Object.create(a);return new Proxy(e,{has(b,c){return c in a||c in d},get(e,f){if(f in d)return d[f];if(!(f in a))return;let k=a[f];if("function"==typeof k){if("function"==typeof b[f])k=h(a,a[f],b[f]);else if(i(c,f)){let b=g(f,c[f]);k=h(a,a[f],b)}else k=k.bind(a);}else if("object"==typeof k&&null!==k&&(i(b,f)||i(c,f)))k=j(k,b[f],c[f]);else if(i(c,"*"))k=j(k,b[f],c["*"]);else return Object.defineProperty(d,f,{configurable:!0,enumerable:!0,get(){return a[f]},set(b){a[f]=b}}),k;return d[f]=k,k},set(b,c,e){return c in d?d[c]=e:a[c]=e,!0},defineProperty(a,b,c){return Reflect.defineProperty(d,b,c)},deleteProperty(a,b){return Reflect.deleteProperty(d,b)}})},k=a=>({addListener(b,c,...d){b.addListener(a.get(c),...d)},hasListener(b,c){return b.hasListener(a.get(c))},removeListener(b,c){b.removeListener(a.get(c))}}),l=new c(a=>"function"==typeof a?function(b){const c=j(b,{},{getContent:{minArgs:0,maxArgs:0}});a(c)}:a),m=new c(a=>"function"==typeof a?function(b,c,e){let f,g,h=!1,i=new Promise(a=>{f=function(b){h=!0,a(b)}});try{g=a(b,c,f)}catch(a){g=Promise.reject(a)}const j=!0!==g&&d(g);if(!0!==g&&!j&&!h)return!1;const k=a=>{a.then(a=>{e(a)},a=>{let b;b=a&&(a instanceof Error||"string"==typeof a.message)?a.message:"An unexpected error occurred",e({__mozWebExtensionPolyfillReject__:!0,message:b})}).catch(a=>{console.error("Failed to send onMessage rejected reply",a)})};return j?k(g):k(i),!0}:a),n=({reject:b,resolve:c},d)=>{a.runtime.lastError?a.runtime.lastError.message==="The message port closed before a response was received."?c():b(new Error(a.runtime.lastError.message)):d&&d.__mozWebExtensionPolyfillReject__?b(new Error(d.message)):c(d)},o=(a,b,c,...d)=>{if(d.length<b.minArgs)throw new Error(`Expected at least ${b.minArgs} ${f(b.minArgs)} for ${a}(), got ${d.length}`);if(d.length>b.maxArgs)throw new Error(`Expected at most ${b.maxArgs} ${f(b.maxArgs)} for ${a}(), got ${d.length}`);return new Promise((a,b)=>{const e=n.bind(null,{resolve:a,reject:b});d.push(e),c.sendMessage(...d)})},p={devtools:{network:{onRequestFinished:k(l)}},runtime:{onMessage:k(m),onMessageExternal:k(m),sendMessage:o.bind(null,"sendMessage",{minArgs:1,maxArgs:3})},tabs:{sendMessage:o.bind(null,"sendMessage",{minArgs:2,maxArgs:3})}},q={clear:{minArgs:1,maxArgs:1},get:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}};return b.privacy={network:{"*":q},services:{"*":q},websites:{"*":q}},j(a,p,b)})(chrome)}else a.exports=globalThis.browser});
//# sourceMappingURL=browser-polyfill.min.js.map
// webextension-polyfill v.0.12.0 (https://github.com/mozilla/webextension-polyfill)
/* @@package_name - v@@version - @@timestamp */
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
if (!(globalThis.chrome && globalThis.chrome.runtime && globalThis.chrome.runtime.id)) {
throw new Error("This script should only be loaded in a browser extension.");
}
if (!(globalThis.browser && globalThis.browser.runtime && globalThis.browser.runtime.id)) {
const CHROME_SEND_MESSAGE_CALLBACK_NO_RESPONSE_MESSAGE = "The message port closed before a response was received.";
// Wrapping the bulk of this polyfill in a one-time-use function is a minor
// optimization for Firefox. Since Spidermonkey does not fully parse the
// contents of a function until the first time it's called, and since it will
// never actually need to be called, this allows the polyfill to be included
// in Firefox nearly for free.
const wrapAPIs = extensionAPIs => {
// NOTE: apiMetadata is associated to the content of the api-metadata.json file
// at build time by replacing the following "include" with the content of the
// JSON file.
const apiMetadata = {/* include("api-metadata.json") */};
if (Object.keys(apiMetadata).length === 0) {
throw new Error("api-metadata.json has not been included in browser-polyfill");
}
/**
* A WeakMap subclass which creates and stores a value for any key which does
* not exist when accessed, but behaves exactly as an ordinary WeakMap
* otherwise.
*
* @param {function} createItem
* A function which will be called in order to create the value for any
* key which does not exist, the first time it is accessed. The
* function receives, as its only argument, the key being created.
*/
class DefaultWeakMap extends WeakMap {
constructor(createItem, items = undefined) {
super(items);
this.createItem = createItem;
}
get(key) {
if (!this.has(key)) {
this.set(key, this.createItem(key));
}
return super.get(key);
}
}
/**
* Returns true if the given object is an object with a `then` method, and can
* therefore be assumed to behave as a Promise.
*
* @param {*} value The value to test.
* @returns {boolean} True if the value is thenable.
*/
const isThenable = value => {
return value && typeof value === "object" && typeof value.then === "function";
};
/**
* Creates and returns a function which, when called, will resolve or reject
* the given promise based on how it is called:
*
* - If, when called, `chrome.runtime.lastError` contains a non-null object,
* the promise is rejected with that value.
* - If the function is called with exactly one argument, the promise is
* resolved to that value.
* - Otherwise, the promise is resolved to an array containing all of the
* function's arguments.
*
* @param {object} promise
* An object containing the resolution and rejection functions of a
* promise.
* @param {function} promise.resolve
* The promise's resolution function.
* @param {function} promise.reject
* The promise's rejection function.
* @param {object} metadata
* Metadata about the wrapped method which has created the callback.
* @param {boolean} metadata.singleCallbackArg
* Whether or not the promise is resolved with only the first
* argument of the callback, alternatively an array of all the
* callback arguments is resolved. By default, if the callback
* function is invoked with only a single argument, that will be
* resolved to the promise, while all arguments will be resolved as
* an array if multiple are given.
*
* @returns {function}
* The generated callback function.
*/
const makeCallback = (promise, metadata) => {
return (...callbackArgs) => {
if (extensionAPIs.runtime.lastError) {
promise.reject(new Error(extensionAPIs.runtime.lastError.message));
} else if (metadata.singleCallbackArg ||
(callbackArgs.length <= 1 && metadata.singleCallbackArg !== false)) {
promise.resolve(callbackArgs[0]);
} else {
promise.resolve(callbackArgs);
}
};
};
const pluralizeArguments = (numArgs) => numArgs == 1 ? "argument" : "arguments";
/**
* Creates a wrapper function for a method with the given name and metadata.
*
* @param {string} name
* The name of the method which is being wrapped.
* @param {object} metadata
* Metadata about the method being wrapped.
* @param {integer} metadata.minArgs
* The minimum number of arguments which must be passed to the
* function. If called with fewer than this number of arguments, the
* wrapper will raise an exception.
* @param {integer} metadata.maxArgs
* The maximum number of arguments which may be passed to the
* function. If called with more than this number of arguments, the
* wrapper will raise an exception.
* @param {boolean} metadata.singleCallbackArg
* Whether or not the promise is resolved with only the first
* argument of the callback, alternatively an array of all the
* callback arguments is resolved. By default, if the callback
* function is invoked with only a single argument, that will be
* resolved to the promise, while all arguments will be resolved as
* an array if multiple are given.
*
* @returns {function(object, ...*)}
* The generated wrapper function.
*/
const wrapAsyncFunction = (name, metadata) => {
return function asyncFunctionWrapper(target, ...args) {
if (args.length < metadata.minArgs) {
throw new Error(`Expected at least ${metadata.minArgs} ${pluralizeArguments(metadata.minArgs)} for ${name}(), got ${args.length}`);
}
if (args.length > metadata.maxArgs) {
throw new Error(`Expected at most ${metadata.maxArgs} ${pluralizeArguments(metadata.maxArgs)} for ${name}(), got ${args.length}`);
}
return new Promise((resolve, reject) => {
if (metadata.fallbackToNoCallback) {
// This API method has currently no callback on Chrome, but it return a promise on Firefox,
// and so the polyfill will try to call it with a callback first, and it will fallback
// to not passing the callback if the first call fails.
try {
target[name](...args, makeCallback({resolve, reject}, metadata));
} catch (cbError) {
console.warn(`${name} API method doesn't seem to support the callback parameter, ` +
"falling back to call it without a callback: ", cbError);
target[name](...args);
// Update the API method metadata, so that the next API calls will not try to
// use the unsupported callback anymore.
metadata.fallbackToNoCallback = false;
metadata.noCallback = true;
resolve();
}
} else if (metadata.noCallback) {
target[name](...args);
resolve();
} else {
target[name](...args, makeCallback({resolve, reject}, metadata));
}
});
};
};
/**
* Wraps an existing method of the target object, so that calls to it are
* intercepted by the given wrapper function. The wrapper function receives,
* as its first argument, the original `target` object, followed by each of
* the arguments passed to the original method.
*
* @param {object} target
* The original target object that the wrapped method belongs to.
* @param {function} method
* The method being wrapped. This is used as the target of the Proxy
* object which is created to wrap the method.
* @param {function} wrapper
* The wrapper function which is called in place of a direct invocation
* of the wrapped method.
*
* @returns {Proxy<function>}
* A Proxy object for the given method, which invokes the given wrapper
* method in its place.
*/
const wrapMethod = (target, method, wrapper) => {
return new Proxy(method, {
apply(targetMethod, thisObj, args) {
return wrapper.call(thisObj, target, ...args);
},
});
};
let hasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty);
/**
* Wraps an object in a Proxy which intercepts and wraps certain methods
* based on the given `wrappers` and `metadata` objects.
*
* @param {object} target
* The target object to wrap.
*
* @param {object} [wrappers = {}]
* An object tree containing wrapper functions for special cases. Any
* function present in this object tree is called in place of the
* method in the same location in the `target` object tree. These
* wrapper methods are invoked as described in {@see wrapMethod}.
*
* @param {object} [metadata = {}]
* An object tree containing metadata used to automatically generate
* Promise-based wrapper functions for asynchronous. Any function in
* the `target` object tree which has a corresponding metadata object
* in the same location in the `metadata` tree is replaced with an
* automatically-generated wrapper function, as described in
* {@see wrapAsyncFunction}
*
* @returns {Proxy<object>}
*/
const wrapObject = (target, wrappers = {}, metadata = {}) => {
let cache = Object.create(null);
let handlers = {
has(proxyTarget, prop) {
return prop in target || prop in cache;
},
get(proxyTarget, prop, receiver) {
if (prop in cache) {
return cache[prop];
}
if (!(prop in target)) {
return undefined;
}
let value = target[prop];
if (typeof value === "function") {
// This is a method on the underlying object. Check if we need to do
// any wrapping.
if (typeof wrappers[prop] === "function") {
// We have a special-case wrapper for this method.
value = wrapMethod(target, target[prop], wrappers[prop]);
} else if (hasOwnProperty(metadata, prop)) {
// This is an async method that we have metadata for. Create a
// Promise wrapper for it.
let wrapper = wrapAsyncFunction(prop, metadata[prop]);
value = wrapMethod(target, target[prop], wrapper);
} else {
// This is a method that we don't know or care about. Return the
// original method, bound to the underlying object.
value = value.bind(target);
}
} else if (typeof value === "object" && value !== null &&
(hasOwnProperty(wrappers, prop) ||
hasOwnProperty(metadata, prop))) {
// This is an object that we need to do some wrapping for the children
// of. Create a sub-object wrapper for it with the appropriate child
// metadata.
value = wrapObject(value, wrappers[prop], metadata[prop]);
} else if (hasOwnProperty(metadata, "*")) {
// Wrap all properties in * namespace.
value = wrapObject(value, wrappers[prop], metadata["*"]);
} else {
// We don't need to do any wrapping for this property,
// so just forward all access to the underlying object.
Object.defineProperty(cache, prop, {
configurable: true,
enumerable: true,
get() {
return target[prop];
},
set(value) {
target[prop] = value;
},
});
return value;
}
cache[prop] = value;
return value;
},
set(proxyTarget, prop, value, receiver) {
if (prop in cache) {
cache[prop] = value;
} else {
target[prop] = value;
}
return true;
},
defineProperty(proxyTarget, prop, desc) {
return Reflect.defineProperty(cache, prop, desc);
},
deleteProperty(proxyTarget, prop) {
return Reflect.deleteProperty(cache, prop);
},
};
// Per contract of the Proxy API, the "get" proxy handler must return the
// original value of the target if that value is declared read-only and
// non-configurable. For this reason, we create an object with the
// prototype set to `target` instead of using `target` directly.
// Otherwise we cannot return a custom object for APIs that
// are declared read-only and non-configurable, such as `chrome.devtools`.
//
// The proxy handlers themselves will still use the original `target`
// instead of the `proxyTarget`, so that the methods and properties are
// dereferenced via the original targets.
let proxyTarget = Object.create(target);
return new Proxy(proxyTarget, handlers);
};
/**
* Creates a set of wrapper functions for an event object, which handles
* wrapping of listener functions that those messages are passed.
*
* A single wrapper is created for each listener function, and stored in a
* map. Subsequent calls to `addListener`, `hasListener`, or `removeListener`
* retrieve the original wrapper, so that attempts to remove a
* previously-added listener work as expected.
*
* @param {DefaultWeakMap<function, function>} wrapperMap
* A DefaultWeakMap object which will create the appropriate wrapper
* for a given listener function when one does not exist, and retrieve
* an existing one when it does.
*
* @returns {object}
*/
const wrapEvent = wrapperMap => ({
addListener(target, listener, ...args) {
target.addListener(wrapperMap.get(listener), ...args);
},
hasListener(target, listener) {
return target.hasListener(wrapperMap.get(listener));
},
removeListener(target, listener) {
target.removeListener(wrapperMap.get(listener));
},
});
const onRequestFinishedWrappers = new DefaultWeakMap(listener => {
if (typeof listener !== "function") {
return listener;
}
/**
* Wraps an onRequestFinished listener function so that it will return a
* `getContent()` property which returns a `Promise` rather than using a
* callback API.
*
* @param {object} req
* The HAR entry object representing the network request.
*/
return function onRequestFinished(req) {
const wrappedReq = wrapObject(req, {} /* wrappers */, {
getContent: {
minArgs: 0,
maxArgs: 0,
},
});
listener(wrappedReq);
};
});
const onMessageWrappers = new DefaultWeakMap(listener => {
if (typeof listener !== "function") {
return listener;
}
/**
* Wraps a message listener function so that it may send responses based on
* its return value, rather than by returning a sentinel value and calling a
* callback. If the listener function returns a Promise, the response is
* sent when the promise either resolves or rejects.
*
* @param {*} message
* The message sent by the other end of the channel.
* @param {object} sender
* Details about the sender of the message.
* @param {function(*)} sendResponse
* A callback which, when called with an arbitrary argument, sends
* that value as a response.
* @returns {boolean}
* True if the wrapped listener returned a Promise, which will later
* yield a response. False otherwise.
*/
return function onMessage(message, sender, sendResponse) {
let didCallSendResponse = false;
let wrappedSendResponse;
let sendResponsePromise = new Promise(resolve => {
wrappedSendResponse = function(response) {
didCallSendResponse = true;
resolve(response);
};
});
let result;
try {
result = listener(message, sender, wrappedSendResponse);
} catch (err) {
result = Promise.reject(err);
}
const isResultThenable = result !== true && isThenable(result);
// If the listener didn't returned true or a Promise, or called
// wrappedSendResponse synchronously, we can exit earlier
// because there will be no response sent from this listener.
if (result !== true && !isResultThenable && !didCallSendResponse) {
return false;
}
// A small helper to send the message if the promise resolves
// and an error if the promise rejects (a wrapped sendMessage has
// to translate the message into a resolved promise or a rejected
// promise).
const sendPromisedResult = (promise) => {
promise.then(msg => {
// send the message value.
sendResponse(msg);
}, error => {
// Send a JSON representation of the error if the rejected value
// is an instance of error, or the object itself otherwise.
let message;
if (error && (error instanceof Error ||
typeof error.message === "string")) {
message = error.message;
} else {
message = "An unexpected error occurred";
}
sendResponse({
__mozWebExtensionPolyfillReject__: true,
message,
});
}).catch(err => {
// Print an error on the console if unable to send the response.
console.error("Failed to send onMessage rejected reply", err);
});
};
// If the listener returned a Promise, send the resolved value as a
// result, otherwise wait the promise related to the wrappedSendResponse
// callback to resolve and send it as a response.
if (isResultThenable) {
sendPromisedResult(result);
} else {
sendPromisedResult(sendResponsePromise);
}
// Let Chrome know that the listener is replying.
return true;
};
});
const wrappedSendMessageCallback = ({reject, resolve}, reply) => {
if (extensionAPIs.runtime.lastError) {
// Detect when none of the listeners replied to the sendMessage call and resolve
// the promise to undefined as in Firefox.
// See https://github.com/mozilla/webextension-polyfill/issues/130
if (extensionAPIs.runtime.lastError.message === CHROME_SEND_MESSAGE_CALLBACK_NO_RESPONSE_MESSAGE) {
resolve();
} else {
reject(new Error(extensionAPIs.runtime.lastError.message));
}
} else if (reply && reply.__mozWebExtensionPolyfillReject__) {
// Convert back the JSON representation of the error into
// an Error instance.
reject(new Error(reply.message));
} else {
resolve(reply);
}
};
const wrappedSendMessage = (name, metadata, apiNamespaceObj, ...args) => {
if (args.length < metadata.minArgs) {
throw new Error(`Expected at least ${metadata.minArgs} ${pluralizeArguments(metadata.minArgs)} for ${name}(), got ${args.length}`);
}
if (args.length > metadata.maxArgs) {
throw new Error(`Expected at most ${metadata.maxArgs} ${pluralizeArguments(metadata.maxArgs)} for ${name}(), got ${args.length}`);
}
return new Promise((resolve, reject) => {
const wrappedCb = wrappedSendMessageCallback.bind(null, {resolve, reject});
args.push(wrappedCb);
apiNamespaceObj.sendMessage(...args);
});
};
const staticWrappers = {
devtools: {
network: {
onRequestFinished: wrapEvent(onRequestFinishedWrappers),
},
},
runtime: {
onMessage: wrapEvent(onMessageWrappers),
onMessageExternal: wrapEvent(onMessageWrappers),
sendMessage: wrappedSendMessage.bind(null, "sendMessage", {minArgs: 1, maxArgs: 3}),
},
tabs: {
sendMessage: wrappedSendMessage.bind(null, "sendMessage", {minArgs: 2, maxArgs: 3}),
},
};
const settingMetadata = {
clear: {minArgs: 1, maxArgs: 1},
get: {minArgs: 1, maxArgs: 1},
set: {minArgs: 1, maxArgs: 1},
};
apiMetadata.privacy = {
network: {"*": settingMetadata},
services: {"*": settingMetadata},
websites: {"*": settingMetadata},
};
return wrapObject(extensionAPIs, staticWrappers, apiMetadata);
};
// The build process adds a UMD wrapper around this file, which makes the
// `module` variable available.
module.exports = wrapAPIs(chrome);
} else {
module.exports = globalThis.browser;
}
{
"manifest_version": 2,
"name": "YouTube Screenshot Tool",
"version": "1.2.0",
"version": "1.2.1",
"description": "Extract high-quality frames from YouTube videos without UI elements",

@@ -6,0 +6,0 @@ "default_locale": "en",

@@ -13,53 +13,72 @@ <!DOCTYPE html>

<body>
<div class="popup-header">
<div class="popup-title">YouTube Screenshot Tool</div>
<div class="language-selector">
<button id="en-btn" class="language-btn active" data-lang="en">EN</button>
<button id="ar-btn" class="language-btn" data-lang="ar">عربي</button>
<div class="popup-container">
<div class="popup-header">
<div class="popup-title">
<span class="icon">📸</span> YouTube Screenshot
</div>
<div class="language-selector">
<button id="en-btn" class="language-btn active" data-lang="en">EN</button>
<button id="ar-btn" class="language-btn" data-lang="ar">عربي</button>
</div>
</div>
</div>
<div class="capture-section">
<button id="capture" class="capture-btn">📸 Capture Frame</button>
</div>
<!-- Main Action Section -->
<div class="hero-section">
<button id="capture" class="primary-action-btn">
<span class="btn-icon">📸</span>
<span class="btn-text">Capture Frame</span>
</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>
<!-- Growth Section (Viral Features) -->
<div class="growth-section">
<button id="shareBtn" class="growth-btn share-btn">
<span class="btn-icon">📢</span>
<span class="btn-text">Spread the Word</span>
</button>
<button id="rateBtn" class="growth-btn rate-btn">
<span class="btn-icon">⭐</span>
<span class="btn-text">Rate Us</span>
</button>
</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;">
<!-- Shortcut Section -->
<div class="shortcut-section">
<div class="shortcut-info">
<span id="shortcutLabel">Shortcut:</span>
<span id="currentShortcut" class="shortcut-key">...</span>
</div>
<button id="changeShortcut" class="link-btn">Change</button>
</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">
<div class="settings-header" id="settingsToggle">
<span>⚙️</span>
<h3 id="settingsLabel">Settings</h3>
<!-- Tools & Settings -->
<div class="tools-section">
<div class="tools-header">
<span id="toolsLabel">Tools & Settings</span>
</div>
<div class="thumbnail-tool-container">
<button id="openThumbnailTool" class="secondary-btn">
<span class="btn-text">Open Thumbnail Tool</span>
</button>
<div class="video-status">
<span id="videoUrlDisplay">No video detected</span>
</div>
</div>
<div class="settings-toggle-container">
<button id="settingsToggle" class="icon-btn" aria-label="Settings">
⚙️
</button>
</div>
</div>
<!-- Collapsible Settings Panel -->
<div class="settings-content" id="settingsContent">
<div class="form-group">
<label for="quality" id="qualityLabel">Capture Quality</label>
<label for="quality" id="qualityLabel">Quality</label>
<select id="quality">
<option value="high" id="highQualityOption">High Quality</option>
<option value="medium" id="mediumQualityOption">Medium Quality</option>
<option value="low" id="lowQualityOption">Low Quality</option>
<option value="high" id="highQualityOption">High</option>
<option value="medium" id="mediumQualityOption">Medium</option>
<option value="low" id="lowQualityOption">Low</option>
</select>

@@ -69,3 +88,3 @@ </div>

<div class="form-group">
<label for="format" id="formatLabel">File Format</label>
<label for="format" id="formatLabel">Format</label>
<select id="format">

@@ -79,3 +98,3 @@ <option value="png">PNG</option>

<div class="form-group">
<label for="filename" id="filenameLabel">File Name</label>
<label for="filename" id="filenameLabel">Filename</label>
<input type="text" id="filename" value="youtube_frame">

@@ -85,10 +104,10 @@ </div>

<div class="action-buttons">
<button id="saveSettings" class="save-btn">Save Settings</button>
<button id="resetSettings" class="reset-btn">Reset to Default</button>
<button id="saveSettings" class="save-btn">Save</button>
<button id="resetSettings" class="reset-btn">Reset</button>
</div>
</div>
<div id="statusMessage" class="status-message"></div>
</div>
<div id="statusMessage" class="status-message" style="display: none;"></div>
<script src="../scripts/popup.js"></script>

@@ -95,0 +114,0 @@ </body>

@@ -8,2 +8,5 @@ // YouTube Screenshot Tool - Popup Script

// Store URL (Placeholder - Update with actual store link)
const STORE_URL = "https://addons.mozilla.org/en-US/firefox/addon/youtube-frame-extractor/"; // Update this!
// Default settings

@@ -19,2 +22,4 @@ const DEFAULT_SETTINGS = {

const captureBtn = document.getElementById('capture');
const shareBtn = document.getElementById('shareBtn');
const rateBtn = document.getElementById('rateBtn');
const settingsToggle = document.getElementById('settingsToggle');

@@ -44,13 +49,19 @@ const settingsContent = document.getElementById('settingsContent');

settings: "Settings",
captureQuality: "Capture Quality",
fileFormat: "File Format",
fileName: "File Name",
highQuality: "High Quality",
mediumQuality: "Medium Quality",
lowQuality: "Low Quality",
saveSettings: "Save Settings",
resetSettings: "Reset to Default",
captureQuality: "Quality",
fileFormat: "Format",
fileName: "Filename",
highQuality: "High",
mediumQuality: "Medium",
lowQuality: "Low",
saveSettings: "Save",
resetSettings: "Reset",
captureSuccess: "Frame captured successfully!",
shortcutLabel: "Shortcut:",
changeShortcut: "Change Shortcut"
changeShortcut: "Change",
shareText: "Spread the Word",
rateText: "Rate Us",
toolsLabel: "Tools & Settings",
openThumbnailTool: "Open Thumbnail Tool",
shareMessage: "I'm loving this YouTube Screenshot Tool! It helps me extract high-quality frames. Try it: " + STORE_URL,
copied: "Copied! ✅"
},

@@ -60,13 +71,19 @@ ar: {

settings: "الإعدادات",
captureQuality: "جودة الالتقاط",
fileFormat: "صيغة الملف",
captureQuality: "الجودة",
fileFormat: "الصيغة",
fileName: "اسم الملف",
highQuality: "جودة عالية",
mediumQuality: "جودة متوسطة",
lowQuality: "جودة منخفضة",
saveSettings: "حفظ الإعدادات",
resetSettings: "إعادة للإعدادات الافتراضية",
highQuality: "عالية",
mediumQuality: "متوسطة",
lowQuality: "منخفضة",
saveSettings: "حفظ",
resetSettings: "إعادة تعيين",
captureSuccess: "تم التقاط الإطار بنجاح!",
shortcutLabel: "الاختصار:",
changeShortcut: "تغيير الاختصار"
changeShortcut: "تغيير",
shareText: "انشر التطبيق",
rateText: "قيمنا",
toolsLabel: "الأدوات والإعدادات",
openThumbnailTool: "فتح أداة الصور المصغرة",
shareMessage: "أداة رائعة لالتقاط الصور من يوتيوب بجودة عالية! جربها: " + STORE_URL,
copied: "تم النسخ! ✅"
}

@@ -118,11 +135,9 @@ };

if (tabs.length > 0 && tabs[0].url && tabs[0].url.includes('youtube.com/watch')) {
videoUrlDisplay.value = tabs[0].url;
videoUrlDisplay.textContent = tabs[0].url; // Changed to textContent for span
openThumbnailToolBtn.disabled = false;
openThumbnailToolBtn.style.opacity = '1';
openThumbnailToolBtn.style.cursor = 'pointer';
openThumbnailToolBtn.classList.remove('disabled');
} else {
videoUrlDisplay.value = "No YouTube video detected";
videoUrlDisplay.textContent = "No YouTube video detected";
openThumbnailToolBtn.disabled = true;
openThumbnailToolBtn.style.opacity = '0.6';
openThumbnailToolBtn.style.cursor = 'not-allowed';
openThumbnailToolBtn.classList.add('disabled');
}

@@ -135,3 +150,3 @@

newBtn.addEventListener('click', () => {
const url = videoUrlDisplay.value;
const url = videoUrlDisplay.textContent;
if (url && url.includes('youtube.com')) {

@@ -143,2 +158,3 @@ // Open external tool with the video URL as a parameter

});
} catch (error) {

@@ -175,2 +191,39 @@ console.error('Error initializing thumbnail tool:', error);

// Share Button Logic
if (shareBtn) {
shareBtn.addEventListener('click', async () => {
const lang = document.querySelector('.language-btn.active').dataset.lang || 'en';
const message = fallbackMessages[lang].shareMessage;
try {
await navigator.clipboard.writeText(message);
// Visual feedback
const originalText = shareBtn.querySelector('.btn-text').textContent;
const originalIcon = shareBtn.querySelector('.btn-icon').textContent;
shareBtn.querySelector('.btn-text').textContent = fallbackMessages[lang].copied;
shareBtn.querySelector('.btn-icon').textContent = "✅";
shareBtn.classList.add('success');
setTimeout(() => {
shareBtn.querySelector('.btn-text').textContent = originalText;
shareBtn.querySelector('.btn-icon').textContent = originalIcon;
shareBtn.classList.remove('success');
}, 2000);
} catch (err) {
console.error('Failed to copy text: ', err);
showStatus('Failed to copy to clipboard', 'error');
}
});
}
// Rate Button Logic
if (rateBtn) {
rateBtn.addEventListener('click', () => {
browserAPI.tabs.create({ url: STORE_URL });
});
}
// Capture button click

@@ -373,23 +426,8 @@ if (captureBtn) {

document.documentElement.setAttribute('lang', 'ar');
const settingsHeaders = document.querySelectorAll('.settings-header h3');
if (settingsHeaders) {
settingsHeaders.forEach(header => {
header.style.marginRight = '5px';
header.style.marginLeft = '0';
});
}
} else {
document.body.classList.remove('rtl');
document.documentElement.setAttribute('lang', 'en');
const settingsHeaders = document.querySelectorAll('.settings-header h3');
if (settingsHeaders) {
settingsHeaders.forEach(header => {
header.style.marginLeft = '5px';
header.style.marginRight = '0';
});
}
}
if (captureBtn) captureBtn.textContent = fallbackMessages[lang].captureBtn;
// Update text content for static elements
const elements = {

@@ -406,3 +444,4 @@ 'settingsLabel': fallbackMessages[lang].settings,

'shortcutLabel': fallbackMessages[lang].shortcutLabel,
'changeShortcut': fallbackMessages[lang].changeShortcut
'changeShortcut': fallbackMessages[lang].changeShortcut,
'toolsLabel': fallbackMessages[lang].toolsLabel
};

@@ -414,2 +453,16 @@

}
// Update buttons with icons specially
if (captureBtn) {
captureBtn.querySelector('.btn-text').textContent = fallbackMessages[lang].captureBtn.replace("📸 ", "");
}
if (shareBtn) {
shareBtn.querySelector('.btn-text').textContent = fallbackMessages[lang].shareText;
}
if (rateBtn) {
rateBtn.querySelector('.btn-text').textContent = fallbackMessages[lang].rateText;
}
if (document.getElementById('openThumbnailTool')) {
document.getElementById('openThumbnailTool').querySelector('.btn-text').textContent = fallbackMessages[lang].openThumbnailTool;
}
}

@@ -424,3 +477,4 @@

'Settings saved': 'Settings saved',
'Settings reset to default': 'Settings reset to default'
'Settings reset to default': 'Settings reset to default',
'Failed to copy to clipboard': 'Failed to copy to clipboard'
},

@@ -430,3 +484,4 @@ ar: {

'Settings saved': 'تم حفظ الإعدادات',
'Settings reset to default': 'تم إعادة الإعدادات للوضع الافتراضي'
'Settings reset to default': 'تم إعادة الإعدادات للوضع الافتراضي',
'Failed to copy to clipboard': 'فشل نسخ النص'
}

@@ -433,0 +488,0 @@ };

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