Skip to content

Instantly share code, notes, and snippets.

@Sphiment
Last active June 1, 2025 11:54
Show Gist options
  • Save Sphiment/38b36d4261698e027d43e250b095b6a5 to your computer and use it in GitHub Desktop.
Save Sphiment/38b36d4261698e027d43e250b095b6a5 to your computer and use it in GitHub Desktop.
Enhanced quran.com UI for saving screenshots of Ayat
// ==UserScript==
// @name Quran.com Tweaks
// @namespace https://quran.com/
// @version 1.1
// @description Enhanced quran.com UI for saving screenshots of Ayat
// @author Sphiment
// @source https://gist.github.com/Sphiment/38b36d4261698e027d43e250b095b6a5
// @match https://quran.com/*
// @grant GM_addStyle
// ==/UserScript==
(function() {
'use strict';
const STORAGE_KEY = 'qmSettings';
const defaults = {
enabled: true,
lineHeight: 2,
bgColor: '#000000',
textColor: '#ffffff',
showTranslations: false,
panelCollapsed: false
};
let settings = Object.assign({}, defaults, JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'));
const saveSettings = () => localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
const updateSetting = (key, value, cssVar) => {
settings[key] = value;
if (cssVar) document.documentElement.style.setProperty(cssVar, value);
saveSettings();
};
// Translation visibility
const toggleTranslations = () => {
document.querySelectorAll('[class^="TranslationText_translationName__"]').forEach(el => {
el.style.display = settings.showTranslations ? '' : 'none';
});
};
new MutationObserver(toggleTranslations).observe(document.body, { childList: true, subtree: true });
// Main styles
const userStyle = GM_addStyle(`
:root {
--quran-line-height: ${settings.lineHeight};
--quran-bg-color: ${settings.bgColor};
--quran-text-color: ${settings.textColor};
}
[data-theme] {
--color-text-default: var(--quran-text-color) !important;
}
.TranslationText_ltr__wgffa {
text-align: center !important;
color: var(--quran-text-color) !important;
}
.VerseText_verseText__2VPlA {
display: block !important;
align-items: normal !important;
line-height: var(--quran-line-height) !important;
color: var(--quran-text-color) !important;
}
.VerseText_verseTextContainer__l2hfY {
text-align: center !important;
}
.QuranReader_container__BlSji {
background-color: var(--quran-bg-color) !important;
}
.VerseSplitLine {
margin: 0 !important;
}
.Separator_base__vB4w1 {
height: 0px !important;
}
`);
// Panel styles
GM_addStyle(`
#qm-control-panel {
position: fixed;
bottom: 20px;
left: 20px;
background: linear-gradient(135deg, rgba(20, 68, 20, 0.95), rgba(0,100,0,0.95));
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
color: #ffffff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 13px;
z-index: 10000;
min-width: 220px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
animation: slideIn 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
#qm-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: rgba(255, 255, 255, 0.1);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
cursor: move;
user-select: none;
}
#qm-panel-title {
font-weight: 600;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
}
#qm-panel-title::before {
content: "📖";
font-size: 16px;
}
#qm-toggle-btn {
background: none;
border: none;
color: #ffffff;
font-size: 16px;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
}
#qm-toggle-btn:hover {
background: rgba(255, 255, 255, 0.1);
transform: scale(1.1);
}
#qm-panel-content {
padding: 16px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
}
#qm-panel-content.collapsed {
max-height: 0;
padding: 0 16px;
opacity: 0;
}
#qm-panel-content.expanded {
max-height: 500px;
opacity: 1;
}
.qm-control-group {
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.qm-control-group:last-child {
margin-bottom: 0;
border-bottom: none;
padding-bottom: 0;
}
.qm-control-label {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
}
.qm-checkbox-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 8px 0;
transition: color 0.2s ease;
}
.qm-checkbox-label:hover {
color: #ffffff;
}
.qm-checkbox {
width: 16px;
height: 16px;
appearance: none;
background: rgba(255, 255, 255, 0.1);
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
}
.qm-checkbox:checked {
background: #4CAF50;
border-color: #4CAF50;
}
.qm-checkbox:checked::after {
content: "✓";
position: absolute;
top: -2px;
left: 1px;
color: white;
font-size: 12px;
font-weight: bold;
}
.qm-slider {
width: 100%;
height: 6px;
appearance: none;
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
outline: none;
transition: all 0.2s ease;
}
.qm-slider::-webkit-slider-thumb {
appearance: none;
width: 18px;
height: 18px;
background: linear-gradient(135deg, #4CAF50, #45a049);
border-radius: 50%;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
transition: all 0.2s ease;
}
.qm-slider::-webkit-slider-thumb:hover {
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
.qm-slider::-moz-range-thumb {
width: 18px;
height: 18px;
background: linear-gradient(135deg, #4CAF50, #45a049);
border-radius: 50%;
cursor: pointer;
border: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.qm-color-input {
width: 40px;
height: 40px;
border: none;
border-radius: 8px;
cursor: pointer;
background: none;
padding: 0;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
transition: all 0.2s ease;
}
.qm-color-input:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.qm-value-display {
background: rgba(255, 255, 255, 0.1);
padding: 4px 8px;
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 12px;
min-width: 35px;
text-align: center;
color: #ffffff;
}
.qm-color-controls {
display: flex;
align-items: center;
gap: 12px;
}
@keyframes slideIn {
from {
transform: translateX(-100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
`);
// Line splitting logic for Arabic verses
const splitLines = () => {
document.querySelectorAll('div[class*="VerseText_verseTextWrap"]').forEach(wrap => {
if (wrap._splitDone) return;
const words = Array.from(wrap.querySelectorAll('div[class*="QuranWord_container"]'));
if (!words.length) return;
const lines = {};
words.forEach(w => (lines[Math.round(w.getBoundingClientRect().top)] ??= []).push(w));
wrap.innerHTML = '';
Object.keys(lines).map(Number).sort((a,b)=>a-b).forEach(y => {
const div = document.createElement('div');
div.classList.add('VerseText_verseText__2VPlA', 'VerseSplitLine');
lines[y].forEach(w => div.appendChild(w));
wrap.appendChild(div);
});
wrap._splitDone = true;
});
};
const debounce = (fn, delay = 250) => {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
};
const runSplit = debounce(splitLines);
const observer = new MutationObserver(runSplit);
// Enable/disable behavior
const applyEnableState = () => {
if (settings.enabled) {
userStyle.disabled = false;
observer.observe(document.body, { childList: true, subtree: true });
window.addEventListener('resize', runSplit);
runSplit();
} else {
userStyle.disabled = true;
observer.disconnect();
window.removeEventListener('resize', runSplit);
}
};
// Helper functions for control panel
const createElement = (tag, props = {}, children = []) => {
const el = document.createElement(tag);
Object.assign(el, props);
children.forEach(child => el.appendChild(typeof child === 'string' ? document.createTextNode(child) : child));
return el;
};
const createCheckbox = (labelText, checked, onChange) => {
const checkbox = createElement('input', { type: 'checkbox', className: 'qm-checkbox', checked });
checkbox.addEventListener('change', onChange);
return createElement('label', { className: 'qm-checkbox-label' }, [checkbox, labelText]);
};
const createSlider = (value, min, max, step, onChange) => {
const slider = createElement('input', { type: 'range', className: 'qm-slider', min, max, step, value });
slider.addEventListener('input', onChange);
return slider;
};
const createColorInput = (value, onChange) => {
const input = createElement('input', { type: 'color', className: 'qm-color-input', value });
input.addEventListener('input', onChange);
return input;
};
// Control panel builder
const buildControlPanel = () => {
let isDragging = false;
const panel = createElement('div', { id: 'qm-control-panel' });
const header = createElement('div', { id: 'qm-panel-header' });
const title = createElement('div', { id: 'qm-panel-title' }, ['Quran UI Settings']);
const toggleBtn = createElement('button', {
id: 'qm-toggle-btn',
innerHTML: settings.panelCollapsed ? '▲' : '▼',
title: 'Toggle panel'
});
header.appendChild(title);
header.appendChild(toggleBtn);
const content = createElement('div', {
id: 'qm-panel-content',
className: settings.panelCollapsed ? 'collapsed' : 'expanded'
});
// Toggle functionality
const togglePanel = () => {
if (!isDragging) {
settings.panelCollapsed = !settings.panelCollapsed;
content.className = settings.panelCollapsed ? 'collapsed' : 'expanded';
toggleBtn.innerHTML = settings.panelCollapsed ? '▲' : '▼';
saveSettings();
}
isDragging = false;
};
header.addEventListener('click', togglePanel);
panel._setDragging = (dragging) => { isDragging = dragging; };
// Controls
const controls = [
createCheckbox('Enable UI Tweaks', settings.enabled, (e) => {
settings.enabled = e.target.checked;
saveSettings();
location.reload();
}),
createCheckbox('Show Translations Names', settings.showTranslations, (e) => {
settings.showTranslations = e.target.checked;
saveSettings();
toggleTranslations();
})
];
// Line height control
const lineHeightGroup = createElement('div', { className: 'qm-control-group' });
const lineHeightLabel = createElement('div', { className: 'qm-control-label' }, ['Line Height']);
const lineHeightValue = createElement('span', { className: 'qm-value-display' }, [settings.lineHeight.toString()]);
lineHeightLabel.appendChild(lineHeightValue);
const lineHeightSlider = createSlider(settings.lineHeight, 1, 3, 0.1, (e) => {
const value = parseFloat(e.target.value);
lineHeightValue.textContent = value;
updateSetting('lineHeight', value, '--quran-line-height');
});
lineHeightGroup.appendChild(lineHeightLabel);
lineHeightGroup.appendChild(lineHeightSlider);
// Color controls
const bgColorGroup = createElement('div', { className: 'qm-control-group' });
bgColorGroup.appendChild(createElement('div', { className: 'qm-control-label' }, ['Background Color']));
bgColorGroup.appendChild(createElement('div', { className: 'qm-color-controls' }, [
createColorInput(settings.bgColor, (e) => updateSetting('bgColor', e.target.value, '--quran-bg-color'))
]));
const textColorGroup = createElement('div', { className: 'qm-control-group' });
textColorGroup.appendChild(createElement('div', { className: 'qm-control-label' }, ['Text Color']));
textColorGroup.appendChild(createElement('div', { className: 'qm-color-controls' }, [
createColorInput(settings.textColor, (e) => updateSetting('textColor', e.target.value, '--quran-text-color'))
]));
// Add all controls to content
controls.forEach(control => {
const group = createElement('div', { className: 'qm-control-group' });
group.appendChild(control);
content.appendChild(group);
});
content.appendChild(lineHeightGroup);
content.appendChild(bgColorGroup);
content.appendChild(textColorGroup);
panel.appendChild(header);
panel.appendChild(content);
document.body.appendChild(panel);
// Make draggable
makeDraggable(panel, header);
};
// Draggable functionality
const makeDraggable = (element, handle) => {
let offsetX = 0, offsetY = 0, startX = 0, startY = 0, hasMoved = false;
handle.onmousedown = (e) => {
e.preventDefault();
hasMoved = false;
startX = e.clientX;
startY = e.clientY;
const rect = element.getBoundingClientRect();
Object.assign(element.style, {
top: rect.top + 'px',
left: rect.left + 'px',
bottom: 'auto',
right: 'auto'
});
document.onmousemove = (e) => {
e.preventDefault();
offsetX = e.clientX - startX;
offsetY = e.clientY - startY;
if (!hasMoved && (Math.abs(offsetX) > 5 || Math.abs(offsetY) > 5)) {
hasMoved = true;
element.style.transition = 'none';
if (element._setDragging) element._setDragging(true);
}
if (hasMoved) {
startX = e.clientX;
startY = e.clientY;
element.style.top = (element.offsetTop + offsetY) + 'px';
element.style.left = (element.offsetLeft + offsetX) + 'px';
}
};
document.onmouseup = () => {
element.style.transition = '';
document.onmousemove = document.onmouseup = null;
setTimeout(() => {
if (element._setDragging) element._setDragging(false);
}, 10);
};
};
};
// Initialize Quran.com Tweaks
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
buildControlPanel();
applyEnableState();
toggleTranslations();
}, 1000);
});
} else {
setTimeout(() => {
buildControlPanel();
applyEnableState();
toggleTranslations();
}, 1000);
}
function _minimalSplit(node) {
if (node._s) return;
node._s = 1;
let textNode = [...node.childNodes].find(c => c.nodeType === Node.TEXT_NODE && c.textContent.trim());
if (!textNode) return;
let txt = textNode.textContent.trim();
let range = document.createRange();
let prevCount = 1;
let breakPoints = [];
for (let i = 0; i < txt.length; i++) {
range.setStart(textNode, 0);
range.setEnd(textNode, i + 1);
let rectCount = range.getClientRects().length;
if (rectCount > prevCount) {
breakPoints.push(i);
prevCount = rectCount;
}
}
if (breakPoints.length < 1) return;
let parts = [];
let lastIndex = 0;
breakPoints.forEach(i => {
parts.push(txt.slice(lastIndex, i));
lastIndex = i;
});
parts.push(txt.slice(lastIndex));
if (parts.length < 2) return;
let parent = node.parentElement;
if (!parent) return;
for (let i = 1; i < parts.length; i++) {
let dup = node.cloneNode(true);
dup._s = 1;
parent.insertBefore(dup, node.nextSibling);
dup.textContent = parts[i].trim();
}
node.textContent = parts[0].trim();
}
// Observer callback to process newly added translation elements
const minimalObserver = new MutationObserver(mutations => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(addedNode => {
if (addedNode.nodeType === Node.ELEMENT_NODE) {
if (addedNode.matches('[class*="TranslationText_text__"]')) {
_minimalSplit(addedNode);
}
addedNode.querySelectorAll('[class*="TranslationText_text__"]').forEach(_minimalSplit);
}
});
});
});
// On page load, process existing translation elements
window.addEventListener('load', () => {
document.querySelectorAll('[class*="TranslationText_text__"]').forEach(_minimalSplit);
});
// Start observing for translation elements
minimalObserver.observe(document.body, { childList: true, subtree: true });
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment