UnrealSoftware Theme Switcher

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

// ==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
        });
    }

})();