UnrealSoftware Theme Switcher

Automatically switches theme on unrealsoftware.de based on browser's color scheme preference

You will need to install an extension such as Tampermonkey, Greasemonkey 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 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.

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

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

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         UnrealSoftware Theme Switcher
// @version      1.0.0
// @description  Automatically switches theme on unrealsoftware.de based on browser's color scheme preference
// @author       Adrian Gajos
// @match        https://unrealsoftware.de/*
// @license      MIT
// @namespace https://greasyfork.org/users/1486099
// ==/UserScript==

(function() {
    'use strict';

    // Configuration
    const CONFIG = {
        SELECTORS: {
            SAH_LINK: 'a.js-tt[href*="sah="]'
        },
        ENDPOINTS: {
            SETTINGS: 'https://unrealsoftware.de/settings.php'
        },
        COOKIES: {
            LANGUAGE: 'language'
        },
        THEMES: {
            DARK: '',
            BRIGHT: 'bright'
        },
        TIMEOUT: 5000 // 5 seconds timeout for fetch requests
    };

    // Logging utility
    const Logger = {
        info: (message, ...args) => console.info(`[UnrealSW Theme]: ${message}`, ...args),
        warn: (message, ...args) => console.warn(`[UnrealSW Theme]: ${message}`, ...args),
        error: (message, ...args) => console.error(`[UnrealSW Theme]: ${message}`, ...args)
    };

    /**
     * Extracts the SAH (security hash) parameter from the page
     * @returns {string|null} The SAH parameter or null if not found
     */
    function extractSecurityHash() {
        try {
            const sahElement = document.querySelector(CONFIG.SELECTORS.SAH_LINK);
            
            if (!sahElement) {
                Logger.warn('Security hash element not found');
                return null;
            }

            const href = sahElement.getAttribute('href');
            if (!href) {
                Logger.warn('No href attribute found on security hash element');
                return null;
            }

            const match = href.match(/[?&]sah=([^&]+)/);
            if (!match || !match[1]) {
                Logger.warn('Security hash parameter not found in URL:', href);
                return null;
            }

            return match[1];
        } catch (error) {
            Logger.error('Error extracting security hash:', error);
            return null;
        }
    }

    /**
     * Retrieves a cookie value by name
     * @param {string} name - Cookie name
     * @returns {string|null} Cookie value or null if not found
     */
    function getCookieValue(name) {
        try {
            const value = `; ${document.cookie}`;
            const parts = value.split(`; ${name}=`);
            
            if (parts.length === 2) {
                return parts.pop().split(';').shift();
            }
            
            return null;
        } catch (error) {
            Logger.error(`Error retrieving cookie '${name}':`, error);
            return null;
        }
    }

    /**
     * Determines if the browser prefers dark mode
     * @returns {boolean} True if dark mode is preferred
     */
    function prefersDarkMode() {
        return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
    }

    /**
     * Checks if the current theme matches the browser preference
     * @returns {boolean} True if theme is already correct
     */
    function isThemeCorrect() {
        const isDarkPreferred = prefersDarkMode();
        const languageCookie = getCookieValue(CONFIG.COOKIES.LANGUAGE) || '';
        const isBrightTheme = languageCookie.includes(CONFIG.THEMES.BRIGHT);

        // Theme is correct if:
        // - Dark mode preferred AND not using bright theme
        // - Light mode preferred AND using bright theme
        return (isDarkPreferred && !isBrightTheme) || (!isDarkPreferred && isBrightTheme);
    }

    /**
     * Creates a fetch request with timeout
     * @param {string} url - Request URL
     * @param {Object} options - Fetch options
     * @param {number} timeout - Timeout in milliseconds
     * @returns {Promise} Fetch promise with timeout
     */
    function fetchWithTimeout(url, options = {}, timeout = CONFIG.TIMEOUT) {
        return Promise.race([
            fetch(url, options),
            new Promise((_, reject) =>
                setTimeout(() => reject(new Error('Request timeout')), timeout)
            )
        ]);
    }

    /**
     * Switches the theme by calling the settings endpoint
     * @param {string} securityHash - The SAH parameter
     * @returns {Promise<boolean>} Success status
     */
    async function switchTheme(securityHash) {
        try {
            const isDarkPreferred = prefersDarkMode();
            const themeParam = isDarkPreferred ? CONFIG.THEMES.DARK : CONFIG.THEMES.BRIGHT;
            const themeName = isDarkPreferred ? 'dark' : 'bright';
            
            const url = new URL(CONFIG.ENDPOINTS.SETTINGS);
            url.searchParams.set('sah', securityHash);
            url.searchParams.set('set_style', themeParam);

            Logger.info(`Switching to ${themeName} theme...`);

            const response = await fetchWithTimeout(url.toString(), {
                method: 'GET',
                credentials: 'include',
                headers: {
                    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
                    'Cache-Control': 'no-cache'
                }
            });

            if (!response.ok) {
                throw new Error(`HTTP ${response.status}: ${response.statusText}`);
            }

            Logger.info(`Theme successfully switched to ${themeName}. Refreshing page...`);
            return true;

        } catch (error) {
            Logger.error('Failed to switch theme:', error);
            return false;
        }
    }

    /**
     * Main execution function
     */
    async function main() {
        try {
            // Check if theme adjustment is needed
            if (isThemeCorrect()) {
                Logger.info('Theme is already correct, no action needed');
                return;
            }

            // Extract security hash
            const securityHash = extractSecurityHash();
            if (!securityHash) {
                Logger.error('Cannot proceed without security hash');
                return;
            }

            // Switch theme
            const success = await switchTheme(securityHash);
            if (success) {
                // Add a small delay to ensure the server processes the request
                setTimeout(() => {
                    window.location.reload();
                }, 100);
            }

        } catch (error) {
            Logger.error('Unexpected error in main execution:', error);
        }
    }

    // Wait for DOM to be ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', main);
    } else {
        // DOM is already ready, execute immediately
        main();
    }

    // Optional: Listen for theme changes during the session
    if (window.matchMedia) {
        const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
        mediaQuery.addEventListener('change', () => {
            Logger.info('System theme changed, checking if adjustment needed...');
            setTimeout(main, 500); // Small delay to ensure page is stable
        });
    }

})();