Userstyle World - Auto Sync UserStyles with Selection UI

Automatically sync userstyles by visiting the mirror URL with a selection UI

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Userstyle World - Auto Sync UserStyles with Selection UI
// @namespace    typpi.online
// @version      2.0
// @description  Automatically sync userstyles by visiting the mirror URL with a selection UI
// @author       Nick2bad4u
// @license      Unlicense
// @homepageURL  https://github.com/Nick2bad4u/UserStyles
// @supportURL   https://github.com/Nick2bad4u/UserStyles/issues
// @icon         https://www.google.com/s2/favicons?sz=64&domain=userstyles.world
// @match        *://userstyles.world/*
// @grant        none
// ==/UserScript==

(function () {
	'use strict';

	/**
	 * @constant {string} SYNC_STATUS_ID - The ID of the element displaying the sync status.
	 */
	const SYNC_STATUS_ID = 'sync-status';
	/**
	 * @constant {string} UI_CONTAINER_ID - The ID of the UI container element.
	 */
	const UI_CONTAINER_ID = 'sync-ui-container';
	/**
	 * @const {string} MINIMIZE_BUTTON_TEXT_COLLAPSE - The text used to represent the collapse action on a minimize button.
	 */
	const MINIMIZE_BUTTON_TEXT_COLLAPSE = '-';
	/**
	 * @const {string} MINIMIZE_BUTTON_TEXT_EXPAND - The text used to represent the expand button when a section is minimized.
	 */
	const MINIMIZE_BUTTON_TEXT_EXPAND = '+';
	/**
	 * @const {string} SELECT_ALL_BUTTON_TEXT_SELECT - The text for the "Select All" button.
	 */
	const SELECT_ALL_BUTTON_TEXT_SELECT = 'Select All';
	/**
	 * @const {string} SELECT_ALL_BUTTON_TEXT_DESELECT - Text for a button that deselects all items.
	 */
	const SELECT_ALL_BUTTON_TEXT_DESELECT = 'Deselect All';

	/**
	 * Extracts style IDs from the page.
	 * @returns {string[]} An array of style IDs.
	 */
	function getStyleIDs() {
		return Array.from(document.querySelectorAll('a.card-header.thumbnail'))
			.map((link) => link.getAttribute('href'))
			.map((href) => href.match(/\/style\/(\d+)\//))
			.filter(Boolean) // Remove null matches
			.map((match) => match[1]);
	}

	/**
	 * Visits the mirror URL for a given style ID.
	 * @param {string} styleID The style ID to visit.
	 * @returns {Promise<void>} A promise that resolves when the mirror URL is visited successfully, or rejects if an error occurs.
	 */
	async function visitMirrorURL(styleID) {
		const mirrorURL = `https://userstyles.world/mirror/${styleID}`;
		try {
			const response = await fetch(mirrorURL);
			if (!response.ok) {
				throw new Error(`Failed to visit ${mirrorURL}: ${response.status} ${response.statusText}`);
			}
			updateStatus(`Successfully visited mirror URL for style ID: ${styleID}`);
		} catch (error) {
			updateStatus(`Error visiting mirror URL for style ID: ${styleID}. Error: ${error.message}`);
			console.error(`Error visiting ${mirrorURL}:`, error);
		}
	}

	/**
	 * Updates the status message in the UI.
	 * @param {string} message The message to display.
	 */
	function updateStatus(message) {
		const statusElement = document.getElementById(SYNC_STATUS_ID);
		if (statusElement) {
			statusElement.textContent = message;
		}
	}

	/**
	 * Creates a user interface for selecting and syncing styles.
	 *
	 * @param {string[]} styleIDs An array of style IDs to be displayed as selectable options.
	 *
	 * @returns {void} This function does not return a value. It creates and appends a UI container to the document body.
	 *
	 * @description
	 * This function dynamically generates a UI container with checkboxes for each style ID provided.
	 * It includes features such as:
	 *  - A title bar with a minimize/expand button.
	 *  - Checkboxes for selecting individual styles.
	 *  - Shift-click functionality for selecting multiple checkboxes at once.
	 *  - A "Select All" button to toggle the selection of all styles.
	 *  - A "Sync Selected Styles" button to initiate the syncing process for selected styles.
	 *  - A status display to provide feedback on the syncing process.
	 *
	 * The UI is appended to the document body as a fixed element, allowing users to interact with it regardless of page scrolling.
	 *
	 * @fires syncButton.onclick - When the "Sync Selected Styles" button is clicked, it triggers the syncing process for the selected style IDs.
	 * @fires selectAllButton.onclick - When the "Select All" button is clicked, it toggles the selection state of all checkboxes.
	 * @fires checkbox.onclick - When a checkbox is clicked, it updates the visibility of the "Sync Selected Styles" button based on whether any checkboxes are selected.  It also handles shift-click selection.
	 * @fires minimizeButton.onclick - When the minimize button is clicked, it collapses or expands the form.
	 */
	function createUI(styleIDs) {
		const container = document.createElement('div');
		container.id = UI_CONTAINER_ID;
		Object.assign(container.style, {
			position: 'fixed',
			bottom: '10px',
			right: '10px',
			width: '250px',
			backgroundColor: '#000',
			border: '1px solid #5a4ebc',
			borderRadius: '5px',
			padding: '15px',
			boxShadow: '0 2px 10px #0000001a',
			zIndex: '1000',
			maxHeight: '50vh',
			overflowY: 'auto',
			color: '#fff',
		});

		const titleContainer = document.createElement('div');
		Object.assign(titleContainer.style, {
			display: 'flex',
			justifyContent: 'space-between',
			alignItems: 'center',
		});
		container.appendChild(titleContainer);

		const title = document.createElement('h3');
		title.textContent = 'Select Styles to Sync';
		Object.assign(title.style, {
			marginBottom: '10px',
			fontSize: '16px',
			fontWeight: 'bold',
		});
		titleContainer.appendChild(title);

		const minimizeButton = document.createElement('button');
		minimizeButton.textContent = MINIMIZE_BUTTON_TEXT_COLLAPSE;
		Object.assign(minimizeButton.style, {
			background: 'none',
			border: 'none',
			cursor: 'pointer',
			fontSize: '16px',
			marginBottom: '10px',
			color: '#fff',
		});
		titleContainer.appendChild(minimizeButton);

		let isMinimized = false;
		let lastCheckedCheckbox = null;

		const form = document.createElement('form');
		form.style.display = 'none';

		const checkboxes = styleIDs.map((styleID) => {
			const label = document.createElement('label');
			Object.assign(label.style, {
				display: 'flex',
				alignItems: 'center',
				marginBottom: '8px',
			});

			const checkbox = document.createElement('input');
			checkbox.type = 'checkbox';
			checkbox.value = styleID;
			Object.assign(checkbox.style, {
				marginRight: '10px',
				opacity: '1',
			});

			label.appendChild(checkbox);
			label.appendChild(document.createTextNode(` Style ID: ${styleID}`));
			form.appendChild(label);

			checkbox.addEventListener('click', (event) => {
				if (event.shiftKey && lastCheckedCheckbox !== null) {
					const currentIndex = checkboxes.indexOf(checkbox);
					const lastIndex = checkboxes.indexOf(lastCheckedCheckbox);
					const start = Math.min(currentIndex, lastIndex);
					const end = Math.max(currentIndex, lastIndex);

					for (let i = start; i <= end; i++) {
						checkboxes[i].checked = lastCheckedCheckbox.checked;
					}
				}
				lastCheckedCheckbox = checkbox;
				syncButton.style.display = checkboxes.some((checkbox) => checkbox.checked) ? 'block' : 'none';
			});

			return checkbox;
		});

		const selectAllButton = document.createElement('button');
		selectAllButton.textContent = SELECT_ALL_BUTTON_TEXT_SELECT;
		selectAllButton.type = 'button';
		Object.assign(selectAllButton.style, {
			width: '100%',
			padding: '10px',
			backgroundColor: '#28a745',
			color: '#fff',
			border: 'none',
			borderRadius: '5px',
			cursor: 'pointer',
			fontSize: '14px',
			marginBottom: '10px',
			display: 'none',
		});

		selectAllButton.onclick = () => {
			const allChecked = checkboxes.every((checkbox) => checkbox.checked);
			checkboxes.forEach((checkbox) => (checkbox.checked = !allChecked));
			selectAllButton.textContent = allChecked ? SELECT_ALL_BUTTON_TEXT_SELECT : SELECT_ALL_BUTTON_TEXT_DESELECT;
			syncButton.style.display = checkboxes.some((checkbox) => checkbox.checked) ? 'block' : 'none';
		};
		container.appendChild(selectAllButton);

		const syncButton = document.createElement('button');
		syncButton.textContent = 'Sync Selected Styles';
		syncButton.type = 'button';
		Object.assign(syncButton.style, {
			width: '100%',
			padding: '10px',
			backgroundColor: '#007bff',
			color: '#fff',
			border: 'none',
			borderRadius: '5px',
			cursor: 'pointer',
			fontSize: '14px',
			marginTop: '10px',
			display: 'none',
		});
		syncButton.addEventListener('mouseover', () => {
			syncButton.style.backgroundColor = '#0056b3';
		});
		syncButton.addEventListener('mouseout', () => {
			syncButton.style.backgroundColor = '#007bff';
		});

		syncButton.onclick = () => {
			const selectedIDs = checkboxes.filter((checkbox) => checkbox.checked).map((checkbox) => checkbox.value);

			if (selectedIDs.length > 0) {
				updateStatus('Syncing selected styles...');
				const promises = selectedIDs.map(async (styleID) => {
					const mirrorURL = `https://userstyles.world/mirror/${styleID}`;
					updateStatus(`Syncing from: ${mirrorURL}`);
					await visitMirrorURL(styleID);
				});
				Promise.all(promises).then(() => {
					updateStatus(`Syncing complete for styles: ${selectedIDs.join(', ')}`);
				});
			} else {
				updateStatus('No styles selected for syncing.');
			}
		};

		const status = document.createElement('div');
		status.id = SYNC_STATUS_ID;
		Object.assign(status.style, {
			marginBottom: '10px',
			fontSize: '12px',
			color: '#fff',
		});
		container.insertBefore(status, titleContainer);

		container.appendChild(syncButton);
		container.appendChild(form);
		document.body.appendChild(container);

		/**
		 * Toggles the visibility of the form and certain buttons based on the `isMinimized` state.
		 * When minimized, the form, selectAllButton, and status are hidden, and the syncButton is shown only if any checkboxes are checked.
		 * When not minimized, the form, selectAllButton, and status are shown, and the syncButton is hidden.
		 * The minimizeButton's text content is also updated to reflect the current state.
		 */
		function toggleMinimize() {
			isMinimized = !isMinimized;
			form.style.display = isMinimized ? 'none' : 'block';
			selectAllButton.style.display = isMinimized ? 'none' : 'block';
			syncButton.style.display = isMinimized && checkboxes.some((checkbox) => checkbox.checked) ? 'block' : 'none';
			status.style.display = isMinimized ? 'none' : 'block';
			minimizeButton.textContent = isMinimized ? MINIMIZE_BUTTON_TEXT_EXPAND : MINIMIZE_BUTTON_TEXT_COLLAPSE;
		}

		minimizeButton.onclick = toggleMinimize;
		toggleMinimize(); // Initialize to minimized state
	}

	/**
	 * @function main
	 * @description This is the main function that initializes the script. It retrieves style IDs from the page,
	 * and if style IDs are found, it creates the user interface. If no style IDs are found, it logs a warning
	 * to the console and updates the status message.
	 * @returns {void}
	 */
	function main() {
		const styleIDs = getStyleIDs();
		if (styleIDs.length > 0) {
			createUI(styleIDs);
		} else {
			console.warn('No style IDs found on the page.');
			updateStatus('No style IDs found on the page.');
		}
	}

	// Execute main function after the page is fully loaded
	if (document.readyState === 'loading') {
		document.addEventListener('DOMContentLoaded', main);
	} else {
		main();
	}
})();