`;
popup.appendChild(dragHandle);
popup.appendChild(content);
return popup;
}
// Generate HTML content for the popup
function generatePopupContent() {
return `
`;
}
function makeDraggable(element, handle) {
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
if (handle) {
handle.onmousedown = dragMouseDown;
} else {
element.onmousedown = dragMouseDown;
}
function dragMouseDown(e) {
e = e || window.event;
e.preventDefault();
if (element.style.transform && element.style.transform.includes('translate')) {
const rect = element.getBoundingClientRect();
element.style.transform = 'none';
element.style.top = rect.top + 'px';
element.style.left = rect.left + 'px';
}
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
element.style.top = (element.offsetTop - pos2) + "px";
element.style.left = (element.offsetLeft - pos1) + "px";
}
function closeDragElement() {
document.onmouseup = null;
document.onmousemove = null;
}
}
// Set up all event listeners for the settings UI
function setupEventListeners(settingsButton, settingsPopup) {
// Drag popup
settingsButton.addEventListener("click", () => {
settingsPopup.style.display = "block";
const dragHandle = document.getElementById("lingqAddonSettingsDragHandle");
if (dragHandle) {
makeDraggable(settingsPopup, dragHandle);
}
});
// Toggle popup visibility
settingsButton.addEventListener("click", () => {
settingsPopup.style.display = "block";
});
// Close button
document.getElementById("closeSettingsBtn").addEventListener("click", () => {
settingsPopup.style.display = "none";
});
// Reset button
document.getElementById("resetSettingsBtn").addEventListener("click", resetSettings);
// Style type selector
const styleTypeSelector = document.getElementById("styleTypeSelector");
styleTypeSelector.addEventListener("change", function() {
const selectedStyleType = this.value;
storage.set("styleType", selectedStyleType);
document.getElementById("videoSettings").style.display =
selectedStyleType === "video" ? "block" : "none";
applyStyles(selectedStyleType, document.getElementById("colorModeSelector").value);
});
// Color mode selector
document.getElementById("colorModeSelector").addEventListener("change", updateColorMode);
// Setup sliders
setupSlider("fontSizeSlider", "fontSizeValue", "fontSize", "rem", "--font_size", (val) => `${val}rem`);
setupSlider("lineHeightSlider", "lineHeightValue", "lineHeight", "", "--line_height", (val) => val);
setupSlider("heightBigSlider", "heightBigValue", "heightBig", "px", "--height_big", (val) => `${val}px`);
// Setup color inputs
setupColorInput("fontColorPicker", "fontColorText", "fontColor", "--font_color");
setupTextInput("lingqBackgroundText", "lingqBackground", "--lingq_background");
setupTextInput("lingqBorderText", "lingqBorder", "--lingq_border");
setupTextInput("lingqBorderLearnedText", "lingqBorderLearned", "--lingq_border_learned");
setupTextInput("blueBorderText", "blueBorder", "--blue_border");
setupColorInput("playingUnderlinePicker", "playingUnderlineText", "playingUnderline", "--is_playing_underline");
}
// Helper function to set up slider controls
function setupSlider(sliderId, valueId, settingKey, unit, cssVar, valueTransform) {
const slider = document.getElementById(sliderId);
const valueDisplay = document.getElementById(valueId);
slider.addEventListener("input", function() {
valueDisplay.textContent = this.value + (unit || "");
const value = parseFloat(this.value);
storage.set(settingKey, value);
document.documentElement.style.setProperty(cssVar, valueTransform(value));
});
}
// Helper function to set up color input pairs
function setupColorInput(pickerId, textId, settingKey, cssVar) {
const picker = document.getElementById(pickerId);
const text = document.getElementById(textId);
picker.addEventListener("input", function() {
text.value = this.value;
saveColorSetting(settingKey, this.value);
document.documentElement.style.setProperty(cssVar, this.value);
});
text.addEventListener("change", function() {
picker.value = this.value;
saveColorSetting(settingKey, this.value);
document.documentElement.style.setProperty(cssVar, this.value);
});
}
// Helper function to set up text inputs for colors
function setupTextInput(textId, settingKey, cssVar) {
const input = document.getElementById(textId);
input.addEventListener("change", function() {
saveColorSetting(settingKey, this.value);
document.documentElement.style.setProperty(cssVar, this.value);
});
}
// Function to save color setting with appropriate prefix
function saveColorSetting(key, value) {
const currentColorMode = document.getElementById("colorModeSelector").value;
const prefix = currentColorMode === "dark" ? "dark_" : "white_";
storage.set(prefix + key, value);
}
// Update color mode and related settings
function updateColorMode(event) {
event.stopPropagation();
const selectedColorMode = this.value;
const settingsPopup = document.getElementById("lingqAddonSettingsPopup");
settingsPopup.style.backgroundColor = selectedColorMode === "dark" ? "#2a2c2e" : "#ffffff";
storage.set("colorMode", selectedColorMode);
// Load color settings for the selected mode
const colorSettings = getColorSettings(selectedColorMode);
// Update all color inputs
updateColorInputs(colorSettings);
// Update CSS variables
document.documentElement.style.setProperty(
"--background-color",
selectedColorMode === "dark" ? "#2a2c2e" : "#ffffff"
);
updateCssColorVariables(colorSettings);
applyStyles(document.getElementById("styleTypeSelector").value, selectedColorMode);
}
// Update all color input fields with new settings
function updateColorInputs(colorSettings) {
document.getElementById("fontColorPicker").value = colorSettings.fontColor;
document.getElementById("fontColorText").value = colorSettings.fontColor;
document.getElementById("lingqBackgroundText").value = colorSettings.lingqBackground;
document.getElementById("lingqBorderText").value = colorSettings.lingqBorder;
document.getElementById("lingqBorderLearnedText").value = colorSettings.lingqBorderLearned;
document.getElementById("blueBorderText").value = colorSettings.blueBorder;
document.getElementById("playingUnderlinePicker").value = colorSettings.playingUnderline;
document.getElementById("playingUnderlineText").value = colorSettings.playingUnderline;
}
// Update CSS color variables
function updateCssColorVariables(colorSettings) {
document.documentElement.style.setProperty("--font_color", colorSettings.fontColor);
document.documentElement.style.setProperty("--lingq_background", colorSettings.lingqBackground);
document.documentElement.style.setProperty("--lingq_border", colorSettings.lingqBorder);
document.documentElement.style.setProperty("--lingq_border_learned", colorSettings.lingqBorderLearned);
document.documentElement.style.setProperty("--blue_border", colorSettings.blueBorder);
document.documentElement.style.setProperty("--is_playing_underline", colorSettings.playingUnderline);
}
// Reset settings to defaults
function resetSettings() {
if (!confirm("Reset all settings to default?")) return;
const currentColorMode = document.getElementById("colorModeSelector").value;
// Default values
const defaultSettings = {
styleType: "video",
colorMode: currentColorMode,
fontSize: 1.1,
lineHeight: 1.7,
heightBig: 400,
};
// Default color settings for current mode
const defaultColorSettings = currentColorMode === "dark"
? defaults.darkColors
: defaults.whiteColors;
// Update all inputs
document.getElementById("styleTypeSelector").value = defaultSettings.styleType;
document.getElementById("fontSizeSlider").value = defaultSettings.fontSize;
document.getElementById("fontSizeValue").textContent = defaultSettings.fontSize;
document.getElementById("lineHeightSlider").value = defaultSettings.lineHeight;
document.getElementById("lineHeightValue").textContent = defaultSettings.lineHeight;
document.getElementById("heightBigSlider").value = defaultSettings.heightBig;
document.getElementById("heightBigValue").textContent = defaultSettings.heightBig;
// Update color inputs
updateColorInputs(defaultColorSettings);
// Save general settings
for (const [key, value] of Object.entries(defaultSettings)) {
storage.set(key, value);
}
// Save color settings with prefix
const prefix = currentColorMode === "dark" ? "dark_" : "white_";
for (const [key, value] of Object.entries(defaultColorSettings)) {
storage.set(prefix + key, value);
}
// Apply styles
applyStyles(defaultSettings.styleType, currentColorMode);
// Show/hide video settings
document.getElementById("videoSettings").style.display =
defaultSettings.styleType === "video" ? "block" : "none";
// Update CSS variables directly
document.documentElement.style.setProperty("--font_size", `${defaultSettings.fontSize}rem`);
document.documentElement.style.setProperty("--line_height", defaultSettings.lineHeight);
document.documentElement.style.setProperty("--height_big", `${defaultSettings.heightBig}px`);
updateCssColorVariables(defaultColorSettings);
}
// CSS Management
let styleElement = null;
// Apply styles based on current settings
function applyStyles(styleType, colorMode) {
// Load color settings for the current mode
const colorSettings = getColorSettings(colorMode);
let css = generateBaseCSS(colorSettings, colorMode);
let specificCSS = "";
let theme_btn = "";
// Apply color mode CSS
switch (colorMode) {
case "dark":
theme_btn = document.querySelector(".reader-themes-component > button:nth-child(5)");
break;
case "white":
theme_btn = document.querySelector(".reader-themes-component > button:nth-child(1)");
break;
}
// Apply style type CSS
switch (styleType) {
case "video":
specificCSS = generateVideoCSS();
break;
case "video2":
specificCSS = generateVideo2CSS();
break;
case "audio":
specificCSS = generateAudioCSS();
break;
case "off":
css = generateOffModeCSS(colorSettings);
break;
}
// Append the style-specific CSS
css += specificCSS;
// Remove the old style tag
if (styleElement) {
styleElement.remove();
styleElement = null;
}
// Create & append the new style tag
if (css) {
styleElement = document.createElement("style");
styleElement.textContent = css;
document.querySelector("head").appendChild(styleElement);
}
if (theme_btn) {
theme_btn.click();
}
}
// Generate base CSS
function generateBaseCSS(colorSettings, colorMode) {
return `
:root {
--font_size: ${settings.fontSize}rem;
--line_height: ${settings.lineHeight};
--grid-layout: 1fr var(--height_big) 80px;
--article_height: calc(var(--app-height) - 205px - var(--height_big));
--font_color: ${colorSettings.fontColor};
--lingq_background: ${colorSettings.lingqBackground};
--lingq_border: ${colorSettings.lingqBorder};
--lingq_border_learned: ${colorSettings.lingqBorderLearned};
--blue_border: ${colorSettings.blueBorder};
--is_playing_underline: ${colorSettings.playingUnderline};
--background-color: ${colorMode === "dark" ? "#2a2c2e" : "#ffffff"}
}
#lingqAddonSettings {
color: var(--font_color);
}
#lingqAddonSettingsPopup {
background-color: var(--background-color);
color: var(--font_color);
}
.main-wrapper {
padding-top: calc(var(--spacing) * 12) !important;
}
#main-nav .navbar,
#main-nav .navbar-brand {
min-height: 2.75rem !important;
}
.main-header svg {
width: 20px !important;
height: 20px !important;
}
#lesson-reader {
grid-template-rows: var(--grid-layout);
overflow-y: hidden;
}
.sentence-text {
height: var(--article_height) !important;
}
.reader-container-wrapper {
height: 100% !important;
}
/*video viewer*/
.main-footer {
grid-area: 3 / 1 / 3 / 1 !important;
align-self: end;
}
.main-content {
grid-template-rows: 45px 1fr !important;
overflow: hidden;
align-items: anchor-center;
}
.main-content > .main-header {
margin-top: 30px;
}
.modal-container .modls {
pointer-events: none;
justify-content: end !important;
align-items: flex-start;
}
.modal-background {
background-color: rgb(26 28 30 / 0%) !important;
}
.modal-section.modal-section--head {
display: none !important;
}
.video-player .video-wrapper,
.sent-video-player .video-wrapper {
height: var(--height_big);
overflow: hidden;
pointer-events: auto;
}
.modal.video-player .modal-content {
max-width: var(--width_big) !important;
margin: var(--video_margin);
}
/*make prev/next page buttons compact*/
.reader-component {
grid-template-columns: 0.5rem 1fr 0rem !important;
}
.reader-component > div > a.button > span {
width: 0.5rem !important;
}
.reader-component > div > a.button > span > svg {
width: 15px !important;
height: 15px !important;
}
/*font settings*/
.reader-container {
margin: 0 !important;
float: left !important;
line-height: var(--line_height) !important;
padding: 0 0 100px 0 !important;
font-size: var(--font_size) !important;
columns: unset !important;
overflow-y: scroll !important;
max-width: unset !important;
}
.reader-container p {
margin-top: 0 !important;
}
.reader-container p span.sentence-item {
color: var(--font_color) !important;
}
.sentence.is-playing,
.sentence.is-playing span {
text-underline-offset: .2em !important;
text-decoration-color: var(--is_playing_underline) !important;
}
/*LingQ highlightings*/
.phrase-item {
padding: 0 !important;
}
.phrase-item:not(.phrase-item-status--4, .phrase-item-status--4x2)) {
background-color: var(--lingq_background) !important;
}
.phrase-item.phrase-item-status--4,
.phrase-item.phrase-item-status--4x2 {
background-color: rgba(0, 0, 0, 0) !important;
}
.phrase-cluster:not(:has(.phrase-item-status--4, .phrase-item-status--4x2)) {
border: 1px solid var(--lingq_border) !important;
border-radius: .25rem;
}
.phrase-cluster:has(.phrase-item-status--4, .phrase-item-status--4x2) {
border: 1px solid var(--lingq_border_learned) !important;
border-radius: .25rem;
}
.reader-container .sentence .lingq-word:not(.is-learned) {
border: 1px solid var(--lingq_border) !important;
background-color: var(--lingq_background) !important;
}
.reader-container .sentence .lingq-word.is-learned {
border: 1px solid var(--lingq_border_learned) !important;
}
.reader-container .sentence .blue-word {
border: 1px solid var(--blue_border) !important;
}
.phrase-cluster:hover,
.phrase-created:hover {
padding: 0 !important;
}
.phrase-cluster:hover .phrase-item,
.phrase-created .phrase-item {
padding: 0 !important;
}
.reader-container .sentence .selected-text {
padding: 0 !important;
}
`;
}
// Generate Video mode CSS
function generateVideoCSS() {
return `
:root {
--width_big: calc(100vw - 424px - 5px);
--height_big: ${settings.heightBig}px;
--video_margin: 0 0 90px 10px !important;
}
.widget-area {
grid-area: 1 / 2 / -1 / 2 !important;
}
.main-content {
grid-area: 1 / 1 / 1 / 1 !important;
}
`;
}
// Generate Video2 mode CSS
function generateVideo2CSS() {
return `
:root {
--width_big: calc(50vw - 217px);
--height_big: calc(100vh - 80px);
--grid-layout: 1fr 80px;
--video_margin: 0 10px 20px 10px !important;
--article_height: calc(var(--app-height) - 265px);
}
.page.reader-page.has-widget-fixed:not(.is-edit-mode):not(.workspace-sentence-reviewer) {
grid-template-columns: 1fr 424px 1fr;
}
.main-content {
grid-area: 1 / 1 / -1 / 1 !important;
}
.widget-area {
grid-area: 1 / 2 / -1 / 2 !important;
}
.main-footer {
grid-area: 2 / 1 / 2 / 1 !important;
}
.modal-container .modls {
align-items: end;
}
`;
}
// Generate Audio mode CSS
function generateAudioCSS() {
return `
:root {
--height_big: 10px;
}
.widget-area {
grid-area: 1 / 2 / -1 / 2 !important;
}
.main-content {
grid-area: 1 / 1 / 2 / 1 !important;
}
`;
}
// Generate Off mode CSS
function generateOffModeCSS(colorSettings) {
return `
:root {
--width_small: 440px;
--height_small: 260px;
--right_pos: 0.5%;
--bottom_pos: 5.5%;
}
.video-player.is-minimized .video-wrapper,
.sent-video-player.is-minimized .video-wrapper {
height: var(--height_small);
width: var(--width_small);
overflow: auto;
resize: both;
}
.video-player.is-minimized .modal-content,
.sent-video-player.is-minimized .modal-content {
max-width: calc(var(--width_small)* 3);
margin-bottom: 0;
}
.video-player.is-minimized,
.sent-video-player.is-minimized {
left: auto;
top: auto;
right: var(--right_pos);
bottom: var(--bottom_pos);
z-index: 99999999;
overflow: visible
}
`;
}
// Keyboard shortcuts and other functionality
function setupKeyboardShortcuts() {
document.addEventListener("keydown", function(event) {
const targetElement = event.target;
const isTextInput = targetElement.type === "text" || targetElement.type === "textarea";
if (isTextInput) return;
const shortcuts = {
'q': () => clickElement(".modal-section > div > button:nth-child(2)"), // video full screen toggle
'Q': () => clickElement(".modal-section > div > button:nth-child(2)"), // video full screen toggle
'w': () => clickElement(".audio-player--controllers > div:nth-child(1) > a"), // 5 sec Backward
'e': () => clickElement(".audio-player--controllers > div:nth-child(2) > a"), // 5 sec Forward
'r': () => document.dispatchEvent(new KeyboardEvent("keydown", { key: "k" })), // Make word Known
'`': () => focusElement(".reference-input-text"), // Move cursor to reference input
'd': () => clickElement(".dictionary-resources > a:nth-child(1)"), // Open Dictionary
'f': () => clickElement(".dictionary-resources > a:nth-child(1)"), // Open Dictionary
't': () => clickElement(".dictionary-resources > a:nth-last-child(1)"), // Open Translator
'c': () => copySelectedText() // Copy selected text
};
if (shortcuts[event.key]) {
event.preventDefault();
event.stopPropagation();
shortcuts[event.key]();
}
}, true);
}
// Helper function to click an element
function clickElement(selector) {
const element = document.querySelector(selector);
if (element) element.click();
}
// Helper function to focus an element
function focusElement(selector) {
const element = document.querySelector(selector);
if (element) {
element.focus();
element.setSelectionRange(element.value.length, element.value.length);
}
}
// Helper function to copy selected text
function copySelectedText() {
const selected_text = document.querySelector(".reference-word");
if (selected_text) {
navigator.clipboard.writeText(selected_text.textContent);
}
}
// Custom embedded player
function setupYoutubePlayerCustomization() {
function replaceNoCookie() {
document.querySelectorAll("iframe").forEach(function(iframe) {
let src = iframe.getAttribute("src");
if (src && src.includes("disablekb=1")) {
src = src.replace("disablekb=1", "disablekb=0"); // keyboard controls are enabled
src = src + "&cc_load_policy=1"; // caption is shown by default
src = src + "&controls=0"; // player controls do not display in the player
iframe.setAttribute("src", src);
}
});
}
const iframeObserver = new MutationObserver(function(mutationsList) {
for (const mutation of mutationsList) {
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach((node) => {
if (node.nodeName === "IFRAME") {
replaceNoCookie();
clickElement('.modal-section.modal-section--head button[title="Expand"]');
}
});
}
}
});
iframeObserver.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["src"]
});
}
// Scroll customization
function setupScrollCustomization() {
setTimeout(() => {
const readerContainer = document.querySelector(".reader-container");
if (readerContainer) {
readerContainer.addEventListener("wheel", (event) => {
event.preventDefault();
const delta = event.deltaY;
const scrollAmount = 0.3;
readerContainer.scrollTop += delta * scrollAmount;
});
}
}, 3000);
}
// Focus on playing sentence
function setupSentenceFocus() {
function focusPlayingSentence() {
const playingSentence = document.querySelector(".sentence.is-playing");
if (playingSentence) {
playingSentence.scrollIntoView({
behavior: "smooth",
block: "center"
});
}
}
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (
mutation.type === "attributes" &&
mutation.attributeName === "class" &&
mutation.target.classList.contains("sentence")
) {
focusPlayingSentence();
}
});
});
const container = document.querySelector(".sentence-text");
if (container) {
observer.observe(container, {
attributes: true,
subtree: true
});
}
}
// Initialize everything
function init() {
createUI();
applyStyles(settings.styleType, settings.colorMode);
setupKeyboardShortcuts();
setupYoutubePlayerCustomization();
setupScrollCustomization();
setupSentenceFocus();
}
// Start the script
init();
})();