Import Google Meet captions project and extension backups
This commit is contained in:
@@ -0,0 +1 @@
|
||||
.qodo
|
||||
@@ -0,0 +1,19 @@
|
||||
"use strict"
|
||||
import logger from './logger.js';
|
||||
|
||||
logger.debug("Hello, world from background!")
|
||||
|
||||
function setBadgeText(enabled) {
|
||||
const text = enabled ? "ON" : "OFF"
|
||||
void chrome.action.setBadgeText({text: text})
|
||||
}
|
||||
|
||||
function startUp() {
|
||||
chrome.storage.sync.get("slider", (data) => {
|
||||
setBadgeText(!!data.slider)
|
||||
})
|
||||
}
|
||||
|
||||
// Ensure the background script always runs.
|
||||
chrome.runtime.onStartup.addListener(startUp)
|
||||
chrome.runtime.onInstalled.addListener(startUp)
|
||||
@@ -0,0 +1,5 @@
|
||||
var config = {
|
||||
loggerLevel: 'debug', //change it to 'debug' for all logs, 'info' or more to reduce the log output in the production
|
||||
defaultCaptureInterval: 100, //change it to 1000 for 1 second interval
|
||||
};
|
||||
export default config;
|
||||
@@ -0,0 +1,64 @@
|
||||
"use strict"
|
||||
|
||||
const blurFilter = "blur(6px)"
|
||||
let textToBlur = ""
|
||||
|
||||
// Search this DOM node for text to blur and blur the parent element if found.
|
||||
function processNode(node) {
|
||||
if (node.childNodes.length > 0) {
|
||||
Array.from(node.childNodes).forEach(processNode)
|
||||
}
|
||||
if (node.nodeType === Node.TEXT_NODE &&
|
||||
node.textContent !== null && node.textContent.trim().length > 0) {
|
||||
const parent = node.parentElement
|
||||
if (parent !== null &&
|
||||
(parent.tagName === 'SCRIPT' || parent.style.filter === blurFilter)) {
|
||||
// Already blurred
|
||||
return
|
||||
}
|
||||
if (node.textContent.includes(textToBlur)) {
|
||||
blurElement(parent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function blurElement(elem) {
|
||||
elem.style.filter = blurFilter
|
||||
console.debug("blurred id:" + elem.id + " class:" + elem.className +
|
||||
" tag:" + elem.tagName + " text:" + elem.textContent)
|
||||
}
|
||||
|
||||
// Create a MutationObserver to watch for changes to the DOM.
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.addedNodes.length > 0) {
|
||||
mutation.addedNodes.forEach(processNode)
|
||||
} else {
|
||||
processNode(mutation.target)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Enable the content script by default.
|
||||
let enabled = true
|
||||
const keys = ["slider", "item"]
|
||||
|
||||
chrome.storage.sync.get(keys, (data) => {
|
||||
if (data.enabled === false) {
|
||||
enabled = false
|
||||
}
|
||||
if (data.item) {
|
||||
textToBlur = data.item
|
||||
}
|
||||
// Only start observing the DOM if the extension is enabled and there is text to blur.
|
||||
if (enabled && textToBlur.trim().length > 0) {
|
||||
observer.observe(document, {
|
||||
attributes: false,
|
||||
characterData: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
})
|
||||
// Loop through all elements on the page for initial processing.
|
||||
processNode(document)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,280 @@
|
||||
"use strict";
|
||||
|
||||
(async () => {
|
||||
// This file cannot import config.js or logger.js directly because it runs in a different context (source:https://stackoverflow.com/a/53033388/4235602)
|
||||
// So we need to load them dynamically. But our config.js and logger.js are in the same directory and can import each other
|
||||
try {
|
||||
//#region load logger
|
||||
const loggerSrc = chrome.runtime.getURL("logger.js");
|
||||
const loggerModule = await import(loggerSrc);
|
||||
const logger = loggerModule.default;
|
||||
|
||||
logger.debug("Logger initialized successfully");
|
||||
logger.debug("Content script starting...");
|
||||
//#endregion
|
||||
|
||||
//#region load config
|
||||
const configSrc = chrome.runtime.getURL("config.js");
|
||||
const configModule = await import(configSrc);
|
||||
const config = configModule.default;
|
||||
|
||||
logger.debug("Config loaded:", config);
|
||||
//#endregion
|
||||
|
||||
class CaptionCapture {
|
||||
constructor() {
|
||||
this.isCapturing = false;
|
||||
this.intervalId = null;
|
||||
this.lastSpans = [];
|
||||
this.captionsLog = [];
|
||||
this.mergedCaption = [];
|
||||
this.subtitleText = null;
|
||||
this.checkSubtitleInterval = config.defaultCaptureInterval;
|
||||
}
|
||||
|
||||
// Function to detect and get the caption spans as an array
|
||||
detectCaptionAsArray() {
|
||||
try {
|
||||
let spans = document.querySelectorAll('div.ygicle.VbkSUe');
|
||||
if (!spans || spans.length === 0) {
|
||||
logger.debug("No caption spans found");
|
||||
return [];
|
||||
}
|
||||
return Array.from(spans).map(span => span.innerText.trim()) || [];
|
||||
} catch (error) {
|
||||
logger.error("Error detecting captions:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Function to get absolute time in HH:MM:SS,mmm format (Real-Time)
|
||||
getCurrentTimestamp() {
|
||||
let now = new Date();
|
||||
let hours = String(now.getHours()).padStart(2, '0');
|
||||
let minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
let seconds = String(now.getSeconds()).padStart(2, '0');
|
||||
let millis = String(now.getMilliseconds()).padStart(3, '0');
|
||||
|
||||
return `${hours}:${minutes}:${seconds},${millis}`; // Absolute time format
|
||||
}
|
||||
|
||||
// Function to save new words with timestamp
|
||||
saveCaptionByWords(timestamp, newText) {
|
||||
this.captionsLog.push({ time: timestamp, text: newText });
|
||||
logger.debug(`${timestamp} --> ${newText}`); // Print in subtitle format
|
||||
}
|
||||
|
||||
formatTimestamp(ms) {
|
||||
const h = String(Math.floor(ms / 3600000)).padStart(2, "0");
|
||||
ms %= 3600000;
|
||||
const m = String(Math.floor(ms / 60000)).padStart(2, "0");
|
||||
ms %= 60000;
|
||||
const s = String(Math.floor(ms / 1000)).padStart(2, "0");
|
||||
const msStr = String(ms % 1000).padStart(3, "0");
|
||||
return `${h}:${m}:${s},${msStr}`;
|
||||
}
|
||||
|
||||
mergeCaptions(captions, gap = 1000) {
|
||||
if (!captions || captions.length === 0) return [];
|
||||
|
||||
const parseTime = (timestamp) => {
|
||||
const [h, m, sMs] = timestamp.split(":");
|
||||
const [s, ms] = sMs.split(",");
|
||||
return (
|
||||
parseInt(h) * 3600000 +
|
||||
parseInt(m) * 60000 +
|
||||
parseInt(s) * 1000 +
|
||||
parseInt(ms)
|
||||
);
|
||||
};
|
||||
|
||||
const merged = [];
|
||||
let current = {
|
||||
start: parseTime(captions[0].time),
|
||||
end: parseTime(captions[0].time),
|
||||
texts: [captions[0].text]
|
||||
};
|
||||
|
||||
for (let i = 1; i < captions.length; i++) {
|
||||
const entry = captions[i];
|
||||
const time = parseTime(entry.time);
|
||||
const timeDiff = time - current.end;
|
||||
|
||||
if (timeDiff > gap) {
|
||||
merged.push({
|
||||
start: this.formatTimestamp(current.start),
|
||||
end: this.formatTimestamp(current.end),
|
||||
text: current.texts.join(" ")
|
||||
});
|
||||
|
||||
current = {
|
||||
start: time,
|
||||
end: time,
|
||||
texts: [entry.text]
|
||||
};
|
||||
} else {
|
||||
current.end = time;
|
||||
current.texts.push(entry.text);
|
||||
}
|
||||
}
|
||||
|
||||
// Push the last group
|
||||
merged.push({
|
||||
start: this.formatTimestamp(current.start),
|
||||
end: this.formatTimestamp(current.end),
|
||||
text: current.texts.join(" ")
|
||||
});
|
||||
|
||||
this.mergedCaption = merged;
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
|
||||
printMergedSubtitle() {
|
||||
const merged = this.mergeCaptions(this.getCapturedCaptions(), 1000);
|
||||
if (merged.length === 0) {
|
||||
logger.debug("No subtitles to print.");
|
||||
return;
|
||||
}
|
||||
|
||||
const parseTime = (timestamp) => {
|
||||
const [h, m, sMs] = timestamp.split(":");
|
||||
const [s, ms] = sMs.split(",");
|
||||
return (
|
||||
parseInt(h) * 3600000 +
|
||||
parseInt(m) * 60000 +
|
||||
parseInt(s) * 1000 +
|
||||
parseInt(ms)
|
||||
);
|
||||
};
|
||||
|
||||
const baseTime = parseTime(merged[0].start); // time of first word
|
||||
|
||||
const toRelative = (absTime) => {
|
||||
const ms = parseTime(absTime) - baseTime;
|
||||
return this.formatTimestamp(ms);
|
||||
};
|
||||
|
||||
this.subtitleText = merged.map((item, index) =>
|
||||
`${index + 1}\n${toRelative(item.start)} --> ${toRelative(item.end)}\n${item.text}\n`
|
||||
).join("\n");
|
||||
|
||||
logger.debug(this.subtitleText);
|
||||
|
||||
return this.subtitleText;
|
||||
}
|
||||
|
||||
generateSrtFilename() {
|
||||
const now = new Date();
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
return `meet_caption_${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}.srt`;
|
||||
}
|
||||
|
||||
downloadSrtFile() {
|
||||
if (!this.subtitleText) {
|
||||
logger.warn("No subtitle content found. Please call printMergedSubtitle() first.");
|
||||
return;
|
||||
}
|
||||
|
||||
const filename = this.generateSrtFilename();
|
||||
const blob = new Blob([this.subtitleText], { type: "text/plain" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Function to start capturing captions
|
||||
startCapturing() {
|
||||
if (this.isCapturing) {
|
||||
logger.debug("Caption capturing is already running.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.isCapturing = true;
|
||||
logger.debug("Caption capturing started.");
|
||||
|
||||
this.intervalId = setInterval(() => {
|
||||
let currentSpans = this.detectCaptionAsArray();
|
||||
|
||||
let lastOldSpan = this.lastSpans[this.lastSpans.length - 1] || "";
|
||||
let lastNewSpan = currentSpans[currentSpans.length - 1] || "";
|
||||
|
||||
let isLastCaptionChanged = lastNewSpan !== lastOldSpan;
|
||||
|
||||
if (isLastCaptionChanged) {
|
||||
this.lastSpans = currentSpans; // Update stored spans
|
||||
let timestamp = this.getCurrentTimestamp();
|
||||
|
||||
// Get only the new part of the last span
|
||||
let newText = lastNewSpan.replace(lastOldSpan, "").trim();
|
||||
|
||||
if (newText) {
|
||||
logger.debug(newText)
|
||||
const wordCount = newText.trim().split(/\s+/).length;
|
||||
const BurstTextCount = 80;
|
||||
if (wordCount <= (this.checkSubtitleInterval / 100) * BurstTextCount) {
|
||||
this.saveCaptionByWords(timestamp, newText);
|
||||
} else {
|
||||
logger.debug(`Skipped burst text with ${wordCount} words at ${timestamp}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, this.checkSubtitleInterval); // Runs every 100ms for better precision
|
||||
}
|
||||
|
||||
// Function to stop capturing captions
|
||||
stopCapturing() {
|
||||
if (!this.isCapturing) {
|
||||
logger.debug("Caption capturing is not running.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.isCapturing = false;
|
||||
clearInterval(this.intervalId);
|
||||
logger.debug("Caption capturing stopped.");
|
||||
}
|
||||
|
||||
// Function to get the saved captions log
|
||||
getCapturedCaptions() {
|
||||
return this.captionsLog;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize CaptionCapture and message listener
|
||||
const captionCapture = new CaptionCapture();
|
||||
logger.debug("CaptionCapture initialized");
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
logger.debug("start of listener in content.js");
|
||||
|
||||
if (message.action === "startCapture") {
|
||||
logger.debug("startCapture content.js");
|
||||
captionCapture.startCapturing();
|
||||
sendResponse({ status: "started" });
|
||||
} else if (message.action === "stopCapture") {
|
||||
logger.debug("stopCapture content.js");
|
||||
captionCapture.stopCapturing();
|
||||
sendResponse({ status: "stopped" });
|
||||
} else if (message.action === "downloadSrt") {
|
||||
alert("Downloading SRT file...");
|
||||
logger.debug("downloadSrt content.js");
|
||||
captionCapture.printMergedSubtitle();
|
||||
captionCapture.downloadSrtFile();
|
||||
sendResponse({ status: "downloadStarted" });
|
||||
}
|
||||
});
|
||||
|
||||
logger.debug("Message listener set up");
|
||||
} catch (error) {
|
||||
logger.error("Error loading modules:", error);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import config from './config.js';
|
||||
|
||||
class Logger {
|
||||
static LEVELS = {
|
||||
debug: 0,
|
||||
info: 1,
|
||||
warn: 2,
|
||||
error: 3,
|
||||
none: 4
|
||||
};
|
||||
|
||||
static currentLevel = Logger.LEVELS.debug;
|
||||
|
||||
static setLevel(level) {
|
||||
if (level in Logger.LEVELS) {
|
||||
Logger.currentLevel = Logger.LEVELS[level];
|
||||
}
|
||||
}
|
||||
|
||||
static shouldLog(level, instanceLevel) {
|
||||
return Logger.LEVELS[level] >= instanceLevel;
|
||||
}
|
||||
|
||||
constructor(level = 'debug', scope = '') {
|
||||
this.level = Logger.LEVELS[level] ?? Logger.LEVELS.debug;
|
||||
this.scope = scope.toUpperCase();
|
||||
|
||||
for (const levelName of Object.keys(Logger.LEVELS)) {
|
||||
if (levelName === 'none') continue;
|
||||
|
||||
this[levelName] = (...args) => {
|
||||
if (Logger.shouldLog(levelName, this.level) && typeof console[levelName] === 'function') {
|
||||
console[levelName](
|
||||
`[${levelName.toUpperCase()}]${this.scope ? ` [${this.scope}]` : ''}`,
|
||||
...args
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a default global logger with no scope, using config value
|
||||
const logger = new Logger(config.loggerLevel);
|
||||
|
||||
export default logger;
|
||||
export { Logger };
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Meet caption saver",
|
||||
"version": "0.1.0",
|
||||
"description": "Save .srt file of conversation",
|
||||
"action": {
|
||||
"default_popup": "popup.html"
|
||||
},
|
||||
"permissions": [
|
||||
"storage"
|
||||
],
|
||||
"host_permissions": [
|
||||
"http://meet.google.com/*",
|
||||
"https://meet.google.com/*"
|
||||
],
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"http://meet.google.com/*",
|
||||
"https://meet.google.com/*"
|
||||
],
|
||||
"js": [
|
||||
"content.js"
|
||||
]
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js",
|
||||
"type": "module"
|
||||
},
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": [
|
||||
"logger.js",
|
||||
"config.js"
|
||||
],
|
||||
"matches": [
|
||||
"http://meet.google.com/*",
|
||||
"https://meet.google.com/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
body {
|
||||
min-width: 100px;
|
||||
min-height: 60px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.downloadBtn {
|
||||
margin: 6px 1px;
|
||||
padding: 3px 3px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ff4444;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
padding: 0 10px;
|
||||
max-width: 200px;
|
||||
word-wrap: break-word;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* The switch - the box around the slider */
|
||||
.switch {
|
||||
position: relative;
|
||||
width: 60px;
|
||||
height: 34px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 6px 1px;
|
||||
}
|
||||
|
||||
/* Hide default HTML checkbox */
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* The slider */
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
.slider::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
input:checked+.slider {
|
||||
background-color: #2196F3;
|
||||
}
|
||||
|
||||
input:checked+.slider:before {
|
||||
transform: translateX(26px);
|
||||
/* Move the slider to the right when checked */
|
||||
}
|
||||
|
||||
/* Rounded sliders */
|
||||
.slider.round {
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.slider.round::before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.secret {
|
||||
margin: 5px;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>My popup</title>
|
||||
<link rel="stylesheet" type="text/css" href="popup.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<label class="switch">
|
||||
<input id="slider" type="checkbox">
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
<button class="downloadBtn">Download .srt</button>
|
||||
<div id="errorMessage" class="error-message"></div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="popup.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,118 @@
|
||||
"use strict";
|
||||
import logger from './logger.js';
|
||||
|
||||
// #region Constants and Helper Functions
|
||||
function setBadgeText(enabled) {
|
||||
const text = enabled ? "ON" : "OFF"
|
||||
void chrome.action.setBadgeText({text: text})
|
||||
}
|
||||
|
||||
function isGoogleMeetPage(url) {
|
||||
return url && (url.startsWith('http://meet.google.com/') || url.startsWith('https://meet.google.com/'));
|
||||
}
|
||||
|
||||
function showErrorMessage(message) {
|
||||
const errorElement = document.getElementById("errorMessage");
|
||||
errorElement.textContent = message;
|
||||
errorElement.style.display = message ? "block" : "none";
|
||||
}
|
||||
|
||||
function handleContentScriptError(error) {
|
||||
logger.debug("Error sending message:", chrome.runtime.lastError);
|
||||
if (error.message.includes("Receiving end does not exist")) {
|
||||
showErrorMessage("Please refresh the Google Meet page to use this extension");
|
||||
} else {
|
||||
showErrorMessage("An error occurred. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
function resetSliderState() {
|
||||
checkbox.checked = false;
|
||||
setBadgeText(false);
|
||||
chrome.storage.sync.set({ "slider": false });
|
||||
}
|
||||
// #endregion Constants and Helper Functions
|
||||
|
||||
// #region DOM Elements
|
||||
const checkbox = document.getElementById("slider");
|
||||
const downloadButton = document.querySelector(".downloadBtn");
|
||||
// #endregion DOM Elements
|
||||
|
||||
// #region Storage Operations
|
||||
function initializeSliderState() {
|
||||
chrome.storage.sync.get("slider", (data) => {
|
||||
const isEnabled = !!data.slider;
|
||||
checkbox.checked = isEnabled;
|
||||
setBadgeText(isEnabled);
|
||||
});
|
||||
}
|
||||
|
||||
function saveSliderState(isChecked) {
|
||||
chrome.storage.sync.set({ "slider": isChecked }, (error) => {
|
||||
if (error) {
|
||||
showErrorMessage("Failed to save settings. Please try again.");
|
||||
return;
|
||||
}
|
||||
setBadgeText(isChecked);
|
||||
});
|
||||
}
|
||||
// #endregion Storage Operations
|
||||
|
||||
// #region Message Handling
|
||||
function sendMessageToContentScript(tabId, action) {
|
||||
chrome.tabs.sendMessage(tabId, { action }, (response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
handleContentScriptError(chrome.runtime.lastError);
|
||||
return;
|
||||
}
|
||||
showErrorMessage(""); // Clear any previous error messages
|
||||
});
|
||||
}
|
||||
|
||||
function handleTabAction(action) {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
if (tabs.length === 0) {
|
||||
showErrorMessage("No active tab found");
|
||||
resetSliderState();
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTab = tabs[0];
|
||||
if (!isGoogleMeetPage(currentTab.url)) {
|
||||
showErrorMessage("Extension can only be used on Google Meet pages");
|
||||
resetSliderState();
|
||||
return;
|
||||
}
|
||||
|
||||
sendMessageToContentScript(currentTab.id, action);
|
||||
});
|
||||
}
|
||||
// #endregion Message Handling
|
||||
|
||||
// #region Event Listeners
|
||||
function setupSliderListener() {
|
||||
checkbox.addEventListener("change", (event) => {
|
||||
const isChecked = event.target.checked;
|
||||
saveSliderState(isChecked);
|
||||
handleTabAction(isChecked ? "startCapture" : "stopCapture");
|
||||
});
|
||||
}
|
||||
|
||||
function setupDownloadButtonListener() {
|
||||
downloadButton.addEventListener("click", () => {
|
||||
handleTabAction("downloadSrt");
|
||||
});
|
||||
}
|
||||
// #endregion Event Listeners
|
||||
|
||||
// #region Initialization
|
||||
function initializePopup() {
|
||||
logger.debug("Initializing popup");
|
||||
initializeSliderState();
|
||||
setupSliderListener();
|
||||
setupDownloadButtonListener();
|
||||
}
|
||||
|
||||
// Start the popup initialization
|
||||
initializePopup();
|
||||
// #endregion Initialization
|
||||
Reference in New Issue
Block a user