Jump to content

User:TonySt/av-dark.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
class AntiVandalDarkMode {
	constructor() {
		this.options_color = mw.storage.store.getItem("AntiVandalDarkModeColor");
		if (!this.options_color) {
			this.options_color = 'auto'; //default auto
		}
		this.options_cascadia_font = mw.storage.store.getItem("AntiVandalDarkModeFont");
		if (!this.options_cascadia_font) {
			this.options_cascadia_font = 'on'; // Default to 'on'
		}
	}
	addFontLinks() {
		const head = document.head;

		const link1 = document.createElement('link');
		link1.rel = 'preconnect';
		link1.href = 'https://fonts.googleapis.com';
		head.appendChild(link1);

		const link2 = document.createElement('link');
		link2.rel = 'preconnect';
		link2.href = 'https://fonts.gstatic.com';
		link2.crossOrigin = 'anonymous';
		head.appendChild(link2);

		const link3 = document.createElement('link');
		link3.rel = 'stylesheet';
		link3.href = 'https://fonts.googleapis.com/css2?family=Cascadia+Mono:ital,wght@0,200..700;1,200..700&display=swap';
		head.appendChild(link3);
	}
	appendStyles() {
		document.querySelectorAll('style').forEach(styleElement => {
			styleElement.textContent += `
					/* custom avdark */

					.av-dark,
					.av-dark .diffActionContainer,
					.av-dark .queueControls,
					.av-dark .diffToolbar,
					.av-dark .diffContainer,
					.av-dark .diffToolbarItem,
					.av-dark .diffActionBox,
					.av-dark .settingsContainer,
					.av-dark .changelogContainer,
					.av-dark .diffChangeContainer tbody {
						background-color:#303841;
						color:#d8dee9;
					}

					.av-black,
					.av-black .diffActionContainer,
					.av-black .queueControls,
					.av-black .diffToolbar,
					.av-black .diffContainer,
					.av-black .diffToolbarItem,
					.av-black .diffActionBox,
					.av-black .settingsContainer,
					.av-black .changelogContainer,
					.av-black .diffChangeContainer tbody {
						background-color:#000000;
						color:#ffffff;
					}

					.av-dark .av-dark-mono .diffChangeContainer table,
					.av-dark .av-dark-mono .diffChangeContainer tbody,
					.av-black .av-dark-mono .diffChangeContainer table,
					.av-black .av-dark-mono .diffChangeContainer tbody
					 {
						font-family:"Cascadia Mono", sans-serif;
						font-size:14px;
					}
					.av-dark .queueItemChange, .av-black .queueItemChange {
						background: linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0));
					}
					.av-dark .queueItemTag, .av-black .queueItemTag {
						background-color:#000;
					}
					.av-dark .currentQueueItem, .av-dark .diffProgressBar, .av-dark .settingsSectionSelected, .av-dark .setControls, .av-dark .ores,
					.av-black .currentQueueItem, .av-black .diffProgressBar, .av-black .settingsSectionSelected, .av-black .setControls, .av-black .ores {
						background-color: #303030;
						color:#d8dee9;
					}
					.av-dark div ::selection, .av-black div ::selection {
						background-color:#d8dee9;
						color:#303841;
					}
					.av-dark a, .av-black a {
						color:#fff;
					}

					.darkModeSwitch {
						display: flex;
						border: 1px solid #ccc;
						border-radius: 5px;
						overflow: hidden;
						width: fit-content;
					}

					.darkModeSwitch input[type="radio"] {
						display: none;
					}

					.darkModeSwitch label {
						flex: 1;
						padding: 10px 5px;
						text-align: center;
						cursor: pointer;
						background-color: #f0f0f0;
						color: #333;
						transition: background-color 0.2s, color 0.2s;
						border-right: 1px solid #ccc;
					}

					/* Remove border from the last label */
					.darkModeSwitch label:last-of-type {
						border-right: none;
					}

					/* Style for when a radio button is checked */
					.darkModeSwitch input[type="radio"]:checked + label {
						background-color: #007bff; /* Highlight color for selected option */
						color: white;
						font-weight: bold;
					}

					/* Hover effect for labels */
					.darkModeSwitch label:hover {
						background-color: #e0e0e0;
						color: #111;
					}

					.darkModeSwitch input[type="radio"]:checked + label:hover {
						 background-color: #0056b3; /* Darker hover for selected */
					}

					.toggleOptionContainer {
						display: grid;
						margin: 25px 0;
						grid-template-columns: auto auto;
						align-items: center;
						border: 1px solid gray;
						border-radius: 5px;
						padding: 5px;
						justify-content: space-between;
					}

					/* For WebKit browsers (Chrome, Safari, newer Edge, Opera) */
					::-webkit-scrollbar {
					    display: none;
					    width: 0; /* Also set width to 0 to remove space */
					    height: 0; /* For horizontal scrollbars, if applicable */
					}
					
					/* For Firefox */
					* {
					    scrollbar-width: none; /* "none" hides the scrollbar */
					}
					
					/* For Internet Explorer and older Edge */
					* {
					    -ms-overflow-style: none; /* "none" hides the scrollbar */
					}
		`;
		});
	}

	appendInitialStyles() {
		document.querySelectorAll('style').forEach(styleElement => {
		  styleElement.textContent += `
					.av-dark, .av-dark button.start {
						background-color:#303841;
						color:#d8dee9;
					}
					.av-black, .av-black button.start { /* Apply black mode to initial splash elements if selected */
						background-color:#000000;
						color:#ffffff;
					}
					.av-dark button.start, .av-black button.start {
						border: 1px solid #34c6eb;
						border-radius: 5%;
					}
					.av-dark div ::selection, .av-black div ::selection {
						background-color:#d8dee9;
						color:#303841;
					}
					.av-dark a, .av-black a {
						color:#fff;
					}
		`;
		});
	}

	/**
	 * Creates a group of radio buttons for a toggle switch.
	 * @param {HTMLElement} parentDiv - The div to prepend the toggle group to.
	 * @param {Array<Object>} optionsConfig - An array of objects, each defining an option {id, value, text}.
	 * @param {string} storageKey - The key to use for storing the selected value in mw.storage.
	 * @param {Function} applyFunction - The function to call when an option is selected, passing the new value.
	 * @param {string} currentSelectedValue - The currently selected value for this toggle group.
	 * @returns {HTMLElement} The created toggle container element.
	 */
	createToggleGroup(labelString, parentDiv, optionsConfig, storageKey, applyFunction, currentSelectedValue) {
		const toggleOptionContainer = document.createElement('div');
		const toggleBox = document.createElement('div');
		const toggleLabelSpan = document.createElement('span');

		toggleOptionContainer.className = 'toggleOptionContainer';
		toggleBox.className = 'darkModeSwitch'; // Reusing the same class for styling
		toggleLabelSpan.innerHTML = labelString;

		optionsConfig.forEach(option => {
			const input = document.createElement('input');
			input.type = 'radio';
			input.id = option.id;
			input.name = storageKey; // Use storageKey as name for radio group
			input.value = option.value;

			const label = document.createElement('label');
			label.htmlFor = option.id;
			label.textContent = option.text;

			if (option.value === currentSelectedValue) {
				input.checked = true;
			}

			toggleBox.appendChild(input);
			toggleBox.appendChild(label);

			input.addEventListener('change', (event) => {
				console.log(`${storageKey} setting changed to: ${event.target.value}`);
				mw.storage.store.setItem(storageKey, event.target.value, 600); // 604800);
				applyFunction(event.target.value);
			});
		});
		// Prepend each toggle group to the interfaceSettingsDiv
		toggleOptionContainer.append(toggleLabelSpan);
		toggleOptionContainer.append(toggleBox);
		
		parentDiv.append(toggleOptionContainer)
		return toggleOptionContainer;
	}

	/**
	 * Adds the dark mode options and Cascadia font options to the interface settings.
	 */
	addOptions() {
		const interfaceSettingsDiv = document.querySelector('.interfaceSettings');

		if (interfaceSettingsDiv) {
			// Color Mode Toggle
			const colorOptions = [
				{ id: 'autoMode', value: 'auto', text: 'Auto' },
				{ id: 'lightMode', value: 'light', text: 'Light' },
				{ id: 'darkMode', value: 'dark', text: 'Dark' },
				{ id: 'blackMode', value: 'black', text: 'Black' }
			];
			this.createToggleGroup('Mode',interfaceSettingsDiv, colorOptions, 'AntiVandalDarkModeColor', this.applyMode.bind(this), this.options_color);

			// Cascadia Font Mode Toggle
			const cascadiaOptions = [
				{ id: 'cascadiaOn', value: 'on', text: 'on' },
				{ id: 'cascadiaOff', value: 'off', text: 'off' }
			];
			this.createToggleGroup('Use Cascadia Font in Dark Modes',interfaceSettingsDiv, cascadiaOptions, 'AntiVandalDarkModeFont', this.applyCascadiaFontMode.bind(this), this.options_cascadia_font);

			this.applyMode(this.options_color);

		} else {
			console.warn('Element with class "interfaceSettings" not found.');
		}
	}
	/**
	 * Applies or removes the 'av-dark' or 'av-black' class to the body based on the selected mode.
	 * @param {string} new_mode - The mode to apply ('auto', 'light', 'dark', 'black').
	 */
	applyMode(new_mode) {
		let body_classes = document.body.classList;
		// Ensure both classes are removed before applying the new one
		body_classes.remove('av-dark', 'av-black');

		if (new_mode == 'auto') {
			if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
				body_classes.add('av-dark');
			}
		} else if (new_mode == 'dark') {
			body_classes.add('av-dark');
		} else if (new_mode == 'black') {
			body_classes.add('av-black');
		}
		// 'light' mode needs no class added
	}

	/**
	 * Applies or removes the 'av-dark-mono' class to the target elements
	 * based on the selected Cascadia font mode.
	 * @param {string} mode - The Cascadia font mode ('on' or 'off').
	 */
	applyCascadiaFontMode(mode) {
		const mainContainer = document.querySelector('.mainContainer');

		// Apply or remove 'av-dark-mono' class from mainContainer, which then applies font via CSS
		if (mainContainer) {
			if (mode === 'on') {
				mainContainer.classList.add('av-dark-mono');
			} else {
				mainContainer.classList.remove('av-dark-mono');
			}
		} else {
			console.warn('mainContainer not found for Cascadia font application.');
		}
	}

	addToSplash() {
		document.querySelector('.container').innerHTML += "<p style='padding-top:15px;font-size:75%;'>Plugin enabled: <a href='https://en.wikipedia.org/wiki/User:TonySt/av-dark' style='color:#34c6eb; text-decoration:none;' target='_BLANK'>av-dark</a></p>";
	}
	runMain() {
		avdark.addFontLinks();
		avdark.appendStyles();
		avdark.addOptions();

		// Initialize observer for diffChangeContainer elements to apply Cascadia font
		const diffContainerObserver = new MutationObserver(mutations => {
			const targetElements = document.querySelectorAll('.diffChangeContainer table, .diffChangeContainer tbody');
			if (targetElements.length > 0) {
				avdark.applyCascadiaFontMode(avdark.options_cascadia_font);
				diffContainerObserver.disconnect();
			}
		});

		// Observe the document body for changes that might include .diffChangeContainer
		diffContainerObserver.observe(document.body, { childList: true, subtree: true });
	}
}

avdark = new AntiVandalDarkMode();

const avdarkInitialObserver = new MutationObserver(mutations => {
	// Check if any mutation involves adding/changing an element with 'start'
	if (mutations.some(m => (m.type === 'childList' && Array.from(m.addedNodes).some(n => n.nodeType === Node.ELEMENT_NODE && (n.classList.contains('start') || n.querySelector('.start')))) || (m.type === 'attributes' && m.target.nodeType === Node.ELEMENT_NODE && m.target.classList.contains('start')))) {
		// Only apply modes and styles relevant to the initial splash page
		avdark.applyMode(avdark.options_color);
		avdark.appendInitialStyles();
		avdark.addToSplash();
		avdarkInitialObserver.disconnect();
	}
});
// Observe the entire document body for child additions, attribute changes, and within subtrees
avdarkInitialObserver.observe(document.body, { childList: true, attributes: true, subtree: true });

const avdarkMainObserver = new MutationObserver(mutations => {
	// Check if any mutation involves adding/changing an element with 'mainContainer'
	if (mutations.some(m => (m.type === 'childList' && Array.from(m.addedNodes).some(n => n.nodeType === Node.ELEMENT_NODE && (n.classList.contains('mainContainer') || n.querySelector('.mainContainer')))) || (m.type === 'attributes' && m.target.nodeType === Node.ELEMENT_NODE && m.target.classList.contains('mainContainer')))) {
		avdark.runMain();
		avdarkMainObserver.disconnect();
	}
});

// Observe the entire document body for child additions, attribute changes, and within subtrees
avdarkMainObserver.observe(document.body, { childList: true, attributes: true, subtree: true });