// ==UserScript==
// @name LingQ Addon
// @description Provides custom LingQ layouts
// @match https://www.lingq.com/*/learn/*/web/reader/*
// @version 3.3
// @grant GM_setValue
// @grant GM_getValue
// @namespace https://greasyfork.org/users/1458847
// ==/UserScript==
(function () {
"use strict";
// Utility functions
const storage = {
get: (key, defaultValue) => {
const value = GM_getValue(key);
return value === undefined ? defaultValue : value;
},
set: (key, value) => GM_setValue(key, value)
};
// Default values
const defaults = {
styleType: "video",
colorMode: "dark",
fontSize: 1.1,
lineHeight: 1.7,
heightBig: 400,
darkColors: {
fontColor: "#e0e0e0",
lingqBackground: "rgba(109, 89, 44, 0.7)",
lingqBorder: "rgba(254, 203, 72, 0.3)",
lingqBorderLearned: "rgba(254, 203, 72, 0.5)",
blueBorder: "rgba(72, 154, 254, 0.5)",
playingUnderline: "#ffffff"
},
whiteColors: {
fontColor: "#000000",
lingqBackground: "rgba(255, 200, 0, 0.4)",
lingqBorder: "rgba(255, 200, 0, 0.3)",
lingqBorderLearned: "rgba(255, 200, 0, 1)",
blueBorder: "rgba(0, 111, 255, 0.3)",
playingUnderline: "#000000"
}
};
// Load stored settings
const settings = {
styleType: storage.get("styleType", defaults.styleType),
colorMode: storage.get("colorMode", defaults.colorMode),
fontSize: storage.get("fontSize", defaults.fontSize),
lineHeight: storage.get("lineHeight", defaults.lineHeight),
heightBig: storage.get("heightBig", defaults.heightBig)
};
// Helper function to get color settings based on mode
function getColorSettings(colorMode) {
const prefix = colorMode === "dark" ? "dark_" : "white_";
const defaultColors = colorMode === "dark" ? defaults.darkColors : defaults.whiteColors;
return {
fontColor: storage.get(prefix + "fontColor", defaultColors.fontColor),
lingqBackground: storage.get(prefix + "lingqBackground", defaultColors.lingqBackground),
lingqBorder: storage.get(prefix + "lingqBorder", defaultColors.lingqBorder),
lingqBorderLearned: storage.get(prefix + "lingqBorderLearned", defaultColors.lingqBorderLearned),
blueBorder: storage.get(prefix + "blueBorder", defaultColors.blueBorder),
playingUnderline: storage.get(prefix + "playingUnderline", defaultColors.playingUnderline)
};
}
// Get current color settings
const colorSettings = getColorSettings(settings.colorMode);
// UI Creation
function createUI() {
// Create settings button
const settingsButton = document.createElement("button");
settingsButton.id = "lingqAddonSettings";
settingsButton.textContent = "⚙️";
settingsButton.title = "LingQ Addon Settings";
settingsButton.style.cssText = `
background: none;
border: none;
cursor: pointer;
font-size: 1.2rem;
margin-left: 10px;
padding: 5px;
`;
// Find the #main-nav element
let mainNav = document.querySelector(
"#main-nav > nav > div:nth-child(2) > div:nth-child(1)",
) || document.querySelector("#main-nav");
if (mainNav) {
mainNav.appendChild(settingsButton);
} else {
console.error("#main-nav element not found. Settings button not inserted.");
}
// Create settings popup
const settingsPopup = createSettingsPopup();
document.body.appendChild(settingsPopup);
// Add event listeners
setupEventListeners(settingsButton, settingsPopup);
}
// Create settings popup with all controls
function createSettingsPopup() {
const popup = document.createElement("div");
popup.id = "lingqAddonSettingsPopup";
popup.style.cssText = `
position: fixed;
top: 40%;
left: 40%;
transform: translate(-40%, -40%);
background-color: var(--background-color, #2a2c2e);
color: var(--font_color, #e0e0e0);
border: 1px solid s;
border-radius: 8px;
box-shadow: 8px 8px 8px rgba(0, 0, 0, 0.2);
z-index: 10000;
display: none;
width: 400px;
max-height: 80vh;
overflow-y: auto;
`;
// popup content
const content = document.createElement("div");
content.style.cssText = `padding: 20px; `;
content.innerHTML = generatePopupContent();
// drag handle
const dragHandle = document.createElement("div");
dragHandle.id = "lingqAddonSettingsDragHandle";
dragHandle.style.cssText = `
cursor: move;
background-color: rgba(128, 128, 128, 0.2);
padding: 8px;
border-radius: 8px 8px 0 0;
text-align: center;
user-select: none;
`;
dragHandle.innerHTML = `<h3 style="margin: 0; user-select: none;">LingQ Addon Settings</h3>`;
popup.appendChild(dragHandle);
popup.appendChild(content);
return popup;
}
// Generate HTML content for the popup
function generatePopupContent() {
return `
<div style="margin-bottom: 15px;">
<label for="styleTypeSelector">Layout Style:</label>
<select id="styleTypeSelector" style="width: 100%; margin-top: 5px; padding: 5px;">
<option value="video" ${settings.styleType === "video" ? "selected" : ""}>Video</option>
<option value="video2" ${settings.styleType === "video2" ? "selected" : ""}>Video2</option>
<option value="audio" ${settings.styleType === "audio" ? "selected" : ""}>Audio</option>
<option value="off" ${settings.styleType === "off" ? "selected" : ""}>Off</option>
</select>
</div>
<div id="videoSettings" style="margin-bottom: 15px; ${settings.styleType === "video" ? "" : "display: none"}">
<label for="heightBigSlider">Video Height: <span id="heightBigValue">${settings.heightBig}</span>px</label>
<input type="range" id="heightBigSlider" min="300" max="800" step="10" value="${settings.heightBig}" style="width: 100%;">
</div>
<div style="margin-bottom: 15px;">
<label for="fontSizeSlider">Font Size: <span id="fontSizeValue">${settings.fontSize}</span>rem</label>
<input type="range" id="fontSizeSlider" min="0.8" max="1.8" step="0.05" value="${settings.fontSize}" style="width: 100%;">
</div>
<div style="margin-bottom: 15px;">
<label for="lineHeightSlider">Line Height: <span id="lineHeightValue">${settings.lineHeight}</span></label>
<input type="range" id="lineHeightSlider" min="1.2" max="3.0" step="0.1" value="${settings.lineHeight}" style="width: 100%;">
</div>
<div style="margin-bottom: 15px; border: 1px solid var(--font_color, #e0e0e0); padding: 10px; border-radius: 5px;">
<label for="colorModeSelector">Color Mode:</label>
<select id="colorModeSelector" style="width: 100%; margin-top: 5px; padding: 5px;">
<option value="dark" ${settings.colorMode === "dark" ? "selected" : ""}>Dark</option>
<option value="white" ${settings.colorMode === "white" ? "selected" : ""}>White</option>
</select>
<div style="margin-top: 10px;">
<label for="fontColorText">Font Color:</label>
<div style="display: flex; align-items: center;">
<div id="fontColorPicker" class="color-picker"></div>
<input type="text" id="fontColorText" value="${colorSettings.fontColor}" style="flex-grow: 1; padding: 5px; margin-left: 10px;">
</div>
</div>
<div style="margin-top: 10px;">
<label for="lingqBackgroundText">LingQ Background:</label>
<div style="display: flex; align-items: center;">
<div id="lingqBackgroundPicker" class="color-picker"></div>
<input type="text" id="lingqBackgroundText" value="${colorSettings.lingqBackground}" style="flex-grow: 1; padding: 5px; margin-left: 10px;">
</div>
</div>
<div style="margin-top: 10px;">
<label for="lingqBorderText">LingQ Border:</label>
<div style="display: flex; align-items: center;">
<div id="lingqBorderPicker" class="color-picker"></div>
<input type="text" id="lingqBorderText" value="${colorSettings.lingqBorder}" style="flex-grow: 1; padding: 5px; margin-left: 10px;">
</div>
</div>
<div style="margin-top: 10px;">
<label for="lingqBorderLearnedText">LingQ Border Learned:</label>
<div style="display: flex; align-items: center;">
<div id="lingqBorderLearnedPicker" class="color-picker"></div>
<input type="text" id="lingqBorderLearnedText" value="${colorSettings.lingqBorderLearned}" style="flex-grow: 1; padding: 5px; margin-left: 10px;">
</div>
</div>
<div style="margin-top: 10px;">
<label for="blueBorderText">Blue Border:</label>
<div style="display: flex; align-items: center;">
<div id="blueBorderPicker" class="color-picker"></div>
<input type="text" id="blueBorderText" value="${colorSettings.blueBorder}" style="flex-grow: 1; padding: 5px; margin-left: 10px;">
</div>
</div>
<div style="margin-top: 10px;">
<label for="playingUnderlineText">Playing Underline:</label>
<div style="display: flex; align-items: center;">
<div id="playingUnderlinePicker" class="color-picker"></div>
<input type="text" id="playingUnderlineText" value="${colorSettings.playingUnderline}" style="flex-grow: 1; padding: 5px; margin-left: 10px;">
</div>
</div>
</div>
<div style="display: flex; justify-content: space-between; margin-top: 20px;">
<button id="resetSettingsBtn" style="padding: 5px 10px; margin-right: 10px; cursor: pointer;">Reset</button>
<button id="closeSettingsBtn" style="padding: 5px 10px; cursor: pointer;">Close</button>
</div>
`;
}
// Add Pickr CSS to the document
function addPickrStyles() {
const pickrCSS = document.createElement('style');
pickrCSS.textContent = `
.color-picker {
width: 30px;
height: 30px;
border-radius: 4px;
cursor: pointer;
position: relative;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.pcr-app {
z-index: 10001 !important;
}
.pcr-app .pcr-interaction .pcr-result {
color: var(--font_color) !important;
}
`;
document.head.appendChild(pickrCSS);
}
// Initialize Pickr color pickers
function initializePickrs() {
// Load Pickr library dynamically
return new Promise((resolve) => {
// Add Pickr CSS
const pickrCss = document.createElement('link');
pickrCss.rel = 'stylesheet';
pickrCss.href = 'https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/themes/nano.min.css';
document.head.appendChild(pickrCss);
// Add Pickr JS
const pickrScript = document.createElement('script');
pickrScript.src = 'https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/pickr.min.js';
pickrScript.onload = () => resolve();
document.head.appendChild(pickrScript);
}).then(() => {
// Add custom styles for Pickr
addPickrStyles();
// Setup each color picker
setupRGBAPickr('lingqBackgroundPicker', 'lingqBackgroundText', 'lingqBackground', '--lingq_background');
setupRGBAPickr('lingqBorderPicker', 'lingqBorderText', 'lingqBorder', '--lingq_border');
setupRGBAPickr('lingqBorderLearnedPicker', 'lingqBorderLearnedText', 'lingqBorderLearned', '--lingq_border_learned');
setupRGBAPickr('blueBorderPicker', 'blueBorderText', 'blueBorder', '--blue_border');
setupRGBAPickr('fontColorPicker', 'fontColorText', 'fontColor', '--font_color');
setupRGBAPickr('playingUnderlinePicker', 'playingUnderlineText', 'playingUnderline', '--is_playing_underline');
});
}
// Setup RGBA color picker with Pickr
function setupRGBAPickr(pickerId, textId, settingKey, cssVar) {
const pickerElement = document.getElementById(pickerId);
const textElement = document.getElementById(textId);
if (!pickerElement || !textElement) return;
// Set initial color for the picker element
pickerElement.style.backgroundColor = textElement.value;
// Create Pickr instance
const pickr = Pickr.create({
el: pickerElement,
theme: 'nano',
useAsButton: true,
default: textElement.value,
components: {
preview: true,
opacity: true,
hue: true,
}
});
// Handle color change
pickr.on('change', (color) => {
const rgbaColor = color.toRGBA();
// Round the RGBA values
const r = Math.round(rgbaColor[0]);
const g = Math.round(rgbaColor[1]);
const b = Math.round(rgbaColor[2]);
const a = rgbaColor[3];
const roundedRGBA = `rgba(${r}, ${g}, ${b}, ${a})`;
textElement.value = roundedRGBA;
pickerElement.style.backgroundColor = roundedRGBA; // Update picker background
document.documentElement.style.setProperty(cssVar, roundedRGBA);
saveColorSetting(settingKey, roundedRGBA);
});
// Update picker when text input changes
textElement.addEventListener('change', function() {
const rgbaColor = this.value;
pickr.setColor(this.value);
saveColorSetting(settingKey, rgbaColor);
document.documentElement.style.setProperty(cssVar, rgbaColor);
pickerElement.style.backgroundColor = rgbaColor;
});
// Update picker background color when color changes
pickr.on('hide', () => {
const rgbaColor = pickr.getColor().toRGBA().toString();
pickerElement.style.backgroundColor = rgbaColor;
});
}
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) {
// Initialize Pickr after popup is displayed
settingsButton.addEventListener("click", () => {
setTimeout(() => {
initializePickrs();
}, 100);
});
// 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`);
}
// 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() {
const value = parseFloat(this.value);
const transformedValue = valueTransform(value);
valueDisplay.textContent = transformedValue.toString().replace(unit, '');
storage.set(settingKey, value);
document.documentElement.style.setProperty(cssVar, transformedValue);
});
}
// 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 color picker backgrounds
updateColorPickerBackgrounds(colorSettings);
}
// Update all color input fields with new settings
function updateColorInputs(colorSettings) {
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("playingUnderlineText").value = colorSettings.playingUnderline;
//Update Pickr color pickers with new values
const fontColorPicker = document.getElementById("fontColorPicker");
if (fontColorPicker) fontColorPicker.style.backgroundColor = colorSettings.fontColor;
const playingUnderlinePicker = document.getElementById("playingUnderlinePicker");
if(playingUnderlinePicker) playingUnderlinePicker.style.backgroundColor = colorSettings.playingUnderline;
}
// Update color picker backgrounds
function updateColorPickerBackgrounds(colorSettings) {
const pickerIds = [
{ id: "lingqBackgroundPicker", color: colorSettings.lingqBackground },
{ id: "lingqBorderPicker", color: colorSettings.lingqBorder },
{ id: "lingqBorderLearnedPicker", color: colorSettings.lingqBorderLearned },
{ id: "blueBorderPicker", color: colorSettings.blueBorder },
{ id: "fontColorPicker", color: colorSettings.fontColor },
{ id: "playingUnderlinePicker", color: colorSettings.playingUnderline }
];
pickerIds.forEach(item => {
const picker = document.getElementById(item.id);
if (picker) {
picker.style.backgroundColor = item.color;
}
});
}
// 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);
// Update color picker backgrounds
updateColorPickerBackgrounds(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};
--article_height: calc(var(--app-height) - var(--height_big) - 50px);
--grid-layout: calc(var(--article_height) - 10px) calc(var(--height_big) - 80px) 90px;
--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"}
}
.color-picker {
height: 15px !important;
}
#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: calc(var(--article_height) - 70px) !important;
}
.reader-container-wrapper {
height: 100% !important;
}
/*video viewer*/
.main-footer {
grid-area: 3 / 1 / 3 / 1 !important;
align-self: end;
margin: 10px 0;
}
.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,
.reader-container p .sentence {
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 - 10px);
--height_big: 400px;
--video_margin: 0 0 10px 5px !important;
}
.main-content {
grid-area: 1 / 1 / 2 / 2 !important;
}
.widget-area {
grid-area: 1 / 2 / 3 / 2 !important;
height: 100% !important;
}
.main-footer {
grid-area: 3 / 2 / 4 / 3 !important;
align-self: end;
}
.section--player.is-expanded {
padding: 5px !important;
width: 400px !important;
margin: 10px 0 !important;
}
.sentence-mode-button {
margin: 0 0 10px 0;
}
`;
}
// 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;
margin: 10px 0;
}
.modal-container .modls {
align-items: end;
}
`;
}
// Generate Audio mode CSS
function generateAudioCSS() {
return `
:root {
--height_big: 80px;
}
.main-content {
grid-area: 1 / 1 / 2 / 2 !important;
}
.widget-area {
grid-area: 1 / 2 / 2 / 2 !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();
})();