Greasy Fork is available in English.
Automatically sync userstyles by visiting the mirror URL with a selection UI
// ==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(); } })();