Last active
June 1, 2025 11:54
-
-
Save Sphiment/38b36d4261698e027d43e250b095b6a5 to your computer and use it in GitHub Desktop.
Enhanced quran.com UI for saving screenshots of Ayat
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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