Last active
January 6, 2026 23:35
-
-
Save kiranwayne/99ebb25ba98cc77ee83879c4c18a3ff7 to your computer and use it in GitHub Desktop.
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 Freedium Enhanced | |
| // @namespace https://gist.github.com/kiranwayne | |
| // @version 1.1.0 | |
| // @description Adds a custom Text width and justification toggle on freedium-mirror.cfd. | |
| // @author kiranwayne (Adapted) | |
| // @match https://freedium-mirror.cfd/* | |
| // @updateURL https://gist.github.com/kiranwayne/99ebb25ba98cc77ee83879c4c18a3ff7/raw/freedium_enhanced.js | |
| // @downloadURL https://gist.github.com/kiranwayne/99ebb25ba98cc77ee83879c4c18a3ff7/raw/freedium_enhanced.js | |
| // @grant GM_getValue | |
| // @grant GM_setValue | |
| // @grant GM_registerMenuCommand | |
| // @grant GM_unregisterMenuCommand | |
| // @run-at document-end | |
| // ==/UserScript== | |
| (async () => { | |
| 'use strict'; | |
| // --- Configuration & Constants --- | |
| const SCRIPT_NAME = 'Freedium Enhanced'; | |
| const SCRIPT_VERSION = '1.1.0'; | |
| const SCRIPT_AUTHOR = 'kiranwayne'; | |
| const CONFIG_PREFIX = 'freediumEnhanced_v1_'; | |
| // Settings Keys | |
| const WIDTH_PX_KEY = CONFIG_PREFIX + 'maxWidthPx'; | |
| const USE_DEFAULT_WIDTH_KEY = CONFIG_PREFIX + 'useDefaultWidth'; | |
| const JUSTIFY_KEY = CONFIG_PREFIX + 'justifyEnabled'; | |
| const SETTINGS_UI_VISIBLE_KEY = CONFIG_PREFIX + 'settingsUiVisible'; | |
| // DOM & Styles | |
| const STYLE_ID = 'freedium-enhanced-styles'; | |
| const SETTINGS_PANEL_ID = 'freedium-userscript-settings-panel'; | |
| // Selectors | |
| // Note: \\ is used to escape the colon in the Tailwind class name | |
| const CONTAINER_SELECTOR = '.container.md\\:max-w-3xl'; | |
| const TEXT_INNER_SELECTOR = '.container.md\\:max-w-3xl > div'; | |
| // Defaults | |
| const DEFAULT_WIDTH_PX = 1000; | |
| const MIN_WIDTH_PX = 600; | |
| const MAX_WIDTH_PX = 2500; | |
| const STEP_WIDTH_PX = 10; | |
| // --- State Variables --- | |
| let config = {}; | |
| let settingsPanel = null; | |
| let widthSlider = null, widthLabel = null, widthInput = null, defaultWidthCheckbox = null, justifyCheckbox = null; | |
| let menuCommandId = null; | |
| // --- Helper Functions --- | |
| async function loadSettings() { | |
| config.maxWidthPx = await GM_getValue(WIDTH_PX_KEY, DEFAULT_WIDTH_PX); | |
| config.useDefaultWidth = await GM_getValue(USE_DEFAULT_WIDTH_KEY, false); | |
| config.justifyEnabled = await GM_getValue(JUSTIFY_KEY, false); | |
| config.settingsUiVisible = await GM_getValue(SETTINGS_UI_VISIBLE_KEY, false); | |
| } | |
| async function saveSetting(key, value) { | |
| const configKey = key.replace(CONFIG_PREFIX, ''); | |
| config[configKey] = value; | |
| await GM_setValue(key, value); | |
| } | |
| // --- Style Injection --- | |
| function applyStyles() { | |
| let css = ''; | |
| // Width Logic | |
| if (!config.useDefaultWidth) { | |
| css += ` | |
| ${CONTAINER_SELECTOR} { | |
| max-width: ${config.maxWidthPx}px !important; | |
| } | |
| `; | |
| } | |
| // Justification Logic | |
| if (config.justifyEnabled) { | |
| css += ` | |
| ${CONTAINER_SELECTOR}, | |
| ${TEXT_INNER_SELECTOR} { | |
| text-align: justify !important; | |
| } | |
| `; | |
| } | |
| let styleEl = document.getElementById(STYLE_ID); | |
| if (!styleEl) { | |
| styleEl = document.createElement('style'); | |
| styleEl.id = STYLE_ID; | |
| document.head.appendChild(styleEl); | |
| } | |
| styleEl.textContent = css; | |
| } | |
| // --- UI Creation/Removal --- | |
| function removeSettingsUI() { | |
| document.removeEventListener('click', handleClickOutside, true); | |
| settingsPanel = document.getElementById(SETTINGS_PANEL_ID); | |
| if (settingsPanel) { | |
| settingsPanel.remove(); | |
| settingsPanel = null; | |
| } | |
| } | |
| async function handleClickOutside(event) { | |
| if (settingsPanel && document.body.contains(settingsPanel) && !settingsPanel.contains(event.target)) { | |
| // Prevent closing if clicking the Tampermonkey menu | |
| await saveSetting(SETTINGS_UI_VISIBLE_KEY, false); | |
| removeSettingsUI(); | |
| updateTampermonkeyMenu(); | |
| } | |
| } | |
| function createSettingsUI() { | |
| if (document.getElementById(SETTINGS_PANEL_ID) || !config.settingsUiVisible) return; | |
| removeSettingsUI(); | |
| settingsPanel = document.createElement('div'); | |
| settingsPanel.id = SETTINGS_PANEL_ID; | |
| // Moved top to 60px to prevent cutoff | |
| Object.assign(settingsPanel.style, { | |
| position: 'fixed', top: '60px', right: '20px', zIndex: '2147483647', | |
| display: 'block', background: '#202021', color: '#ECECF1', | |
| border: '1px solid #565869', borderRadius: '6px', padding: '15px', | |
| boxShadow: '0 4px 15px rgba(0,0,0,0.6)', minWidth: '300px', | |
| fontFamily: 'sans-serif', fontSize: '14px' | |
| }); | |
| // --- Header Section (Title, Version, Author) --- | |
| const headerDiv = document.createElement('div'); | |
| headerDiv.style.marginBottom = '15px'; | |
| headerDiv.style.paddingBottom = '10px'; | |
| headerDiv.style.borderBottom = '1px solid #565869'; | |
| const titleEl = document.createElement('h4'); | |
| titleEl.textContent = SCRIPT_NAME; | |
| Object.assign(titleEl.style, { margin: '0 0 5px 0', fontSize: '1.1em', fontWeight: 'bold', color: '#FFFFFF' }); | |
| const versionEl = document.createElement('div'); | |
| versionEl.textContent = `Version: ${SCRIPT_VERSION}`; | |
| Object.assign(versionEl.style, { fontSize: '0.85em', opacity: '0.7', marginBottom: '2px' }); | |
| const authorEl = document.createElement('div'); | |
| authorEl.textContent = `Author: ${SCRIPT_AUTHOR}`; | |
| Object.assign(authorEl.style, { fontSize: '0.85em', opacity: '0.7' }); | |
| headerDiv.appendChild(titleEl); | |
| headerDiv.appendChild(versionEl); | |
| headerDiv.appendChild(authorEl); | |
| settingsPanel.appendChild(headerDiv); | |
| // --- Controls --- | |
| // 1. Default Width Toggle | |
| const defaultWidthDiv = document.createElement('div'); | |
| defaultWidthDiv.style.marginBottom = '10px'; | |
| defaultWidthCheckbox = document.createElement('input'); | |
| defaultWidthCheckbox.type = 'checkbox'; | |
| defaultWidthCheckbox.id = 'fe-default-width-toggle'; | |
| defaultWidthCheckbox.checked = config.useDefaultWidth; | |
| const defaultWidthLabel = document.createElement('label'); | |
| defaultWidthLabel.htmlFor = 'fe-default-width-toggle'; | |
| defaultWidthLabel.textContent = ' Use Default Width'; | |
| defaultWidthLabel.style.marginLeft = '8px'; | |
| defaultWidthLabel.style.cursor = 'pointer'; | |
| defaultWidthDiv.appendChild(defaultWidthCheckbox); | |
| defaultWidthDiv.appendChild(defaultWidthLabel); | |
| settingsPanel.appendChild(defaultWidthDiv); | |
| // 2. Width Slider | |
| const widthControlsDiv = document.createElement('div'); | |
| widthControlsDiv.style.display = 'flex'; | |
| widthControlsDiv.style.alignItems = 'center'; | |
| widthControlsDiv.style.gap = '10px'; | |
| widthControlsDiv.style.marginBottom = '15px'; | |
| widthLabel = document.createElement('span'); | |
| widthLabel.style.minWidth = '50px'; | |
| widthLabel.style.fontFamily = 'monospace'; | |
| widthLabel.textContent = `${config.maxWidthPx}px`; | |
| widthSlider = document.createElement('input'); | |
| widthSlider.type = 'range'; | |
| widthSlider.min = MIN_WIDTH_PX; | |
| widthSlider.max = MAX_WIDTH_PX; | |
| widthSlider.step = STEP_WIDTH_PX; | |
| widthSlider.value = config.maxWidthPx; | |
| widthSlider.style.flexGrow = '1'; | |
| widthSlider.style.cursor = 'pointer'; | |
| widthInput = document.createElement('input'); | |
| widthInput.type = 'number'; | |
| widthInput.min = MIN_WIDTH_PX; | |
| widthInput.max = MAX_WIDTH_PX; | |
| widthInput.value = config.maxWidthPx; | |
| Object.assign(widthInput.style, { width: '70px', padding: '4px', background: '#343541', color: '#fff', border: '1px solid #565869', borderRadius: '4px' }); | |
| widthControlsDiv.appendChild(widthLabel); | |
| widthControlsDiv.appendChild(widthSlider); | |
| widthControlsDiv.appendChild(widthInput); | |
| settingsPanel.appendChild(widthControlsDiv); | |
| // 3. Justify Toggle | |
| const justifyDiv = document.createElement('div'); | |
| justifyCheckbox = document.createElement('input'); | |
| justifyCheckbox.type = 'checkbox'; | |
| justifyCheckbox.id = 'fe-justify-toggle'; | |
| justifyCheckbox.checked = config.justifyEnabled; | |
| const justifyLabel = document.createElement('label'); | |
| justifyLabel.htmlFor = 'fe-justify-toggle'; | |
| justifyLabel.textContent = ' Justify Text'; | |
| justifyLabel.style.marginLeft = '8px'; | |
| justifyLabel.style.cursor = 'pointer'; | |
| justifyDiv.appendChild(justifyCheckbox); | |
| justifyDiv.appendChild(justifyLabel); | |
| settingsPanel.appendChild(justifyDiv); | |
| document.body.appendChild(settingsPanel); | |
| // --- Event Listeners --- | |
| const updateUIState = () => { | |
| const isCustom = !defaultWidthCheckbox.checked; | |
| widthSlider.disabled = !isCustom; | |
| widthInput.disabled = !isCustom; | |
| widthSlider.style.opacity = isCustom ? 1 : 0.5; | |
| widthInput.style.opacity = isCustom ? 1 : 0.5; | |
| widthLabel.style.opacity = isCustom ? 1 : 0.5; | |
| }; | |
| defaultWidthCheckbox.addEventListener('change', async (e) => { | |
| await saveSetting(USE_DEFAULT_WIDTH_KEY, e.target.checked); | |
| applyStyles(); | |
| updateUIState(); | |
| }); | |
| // Slider input (Live update logic) | |
| // Sliders are hard to use if they don't update immediately, so we keep this live. | |
| widthSlider.addEventListener('input', (e) => { | |
| const val = parseInt(e.target.value, 10); | |
| widthInput.value = val; | |
| widthLabel.textContent = `${val}px`; | |
| config.maxWidthPx = val; | |
| if (!config.useDefaultWidth) applyStyles(); | |
| }); | |
| // Slider change (Save logic) | |
| widthSlider.addEventListener('change', async (e) => { | |
| await saveSetting(WIDTH_PX_KEY, parseInt(e.target.value, 10)); | |
| }); | |
| // Text Input: Only update on 'change' (Enter or Blur/Tab out) | |
| // We removed the 'input' event listener here. | |
| widthInput.addEventListener('change', async (e) => { | |
| let val = parseInt(e.target.value, 10); | |
| // Clamp value | |
| if (isNaN(val)) val = DEFAULT_WIDTH_PX; | |
| if (val < MIN_WIDTH_PX) val = MIN_WIDTH_PX; | |
| if (val > MAX_WIDTH_PX) val = MAX_WIDTH_PX; | |
| // Update UI to match clamped value | |
| e.target.value = val; | |
| widthSlider.value = val; | |
| widthLabel.textContent = `${val}px`; | |
| // Apply and Save | |
| config.maxWidthPx = val; | |
| if (!config.useDefaultWidth) applyStyles(); | |
| await saveSetting(WIDTH_PX_KEY, val); | |
| }); | |
| justifyCheckbox.addEventListener('change', async (e) => { | |
| await saveSetting(JUSTIFY_KEY, e.target.checked); | |
| applyStyles(); | |
| }); | |
| // Initialize state | |
| updateUIState(); | |
| setTimeout(() => document.addEventListener('click', handleClickOutside, true), 100); | |
| } | |
| // --- Tampermonkey Menu --- | |
| function updateTampermonkeyMenu() { | |
| if (menuCommandId) GM_unregisterMenuCommand(menuCommandId); | |
| const label = config.settingsUiVisible ? 'Hide Settings Panel' : 'Show Settings Panel'; | |
| menuCommandId = GM_registerMenuCommand(label, async () => { | |
| const newState = !config.settingsUiVisible; | |
| await saveSetting(SETTINGS_UI_VISIBLE_KEY, newState); | |
| if (newState) createSettingsUI(); | |
| else removeSettingsUI(); | |
| updateTampermonkeyMenu(); | |
| }); | |
| } | |
| // --- Main --- | |
| async function main() { | |
| await loadSettings(); | |
| applyStyles(); | |
| if (config.settingsUiVisible) createSettingsUI(); | |
| updateTampermonkeyMenu(); | |
| } | |
| main(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment