Soyjak Themer

Select and apply themes from GitHub repositories to soyjak.st

// ==UserScript==
// @name         Soyjak Themer
// @namespace    http://tampermonkey.net/
// @version      1.3.1
// @description  Select and apply themes from GitHub repositories to soyjak.st
// @author       ReignOfTea
// @match        https://www.soyjak.party/*
// @match        https://soyjak.party/*
// @match        https://www.soyjak.st/*
// @match        https://soyjak.st/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=www.soyjak.st
// @license AGPL-3.0
// @grant        none
// ==/UserScript==

(function () {
    "use strict";

    const STORAGE_KEYS = {
        THEME: "soyjak_selected_theme",
        USER_CSS: "soyjak_user_css",
        THEMES_CACHE: "soyjak_themes_cache",
        CACHE_TIMESTAMP: "soyjak_themes_cache_timestamp",
        FAVORITES: "soyjak_favorite_themes",
        REPOS: "soyjak_theme_repos"
    };

    const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 Days

    const DEFAULT_REPOS = [
        {
            name: "tchan",
            apiUrl: "https://api.github.com/repos/ReignOfTea/tchan/contents/?ref=master",
            basePath: "https://raw.githubusercontent.com/ReignOfTea/tchan/master/",
            isDefault: true,
        },
        {
            name: "lainchan",
            apiUrl: "https://api.github.com/repos/lainchan/lainchan/contents/stylesheets?ref=php7.4",
            basePath: "https://raw.githubusercontent.com/lainchan/lainchan/php7.4/stylesheets/",
            isDefault: true,
        },
        {
            name: "vichan",
            apiUrl: "https://api.github.com/repos/vichan-devel/vichan/contents/stylesheets?ref=master",
            basePath: "https://raw.githubusercontent.com/vichan-devel/vichan/master/stylesheets/",
            isDefault: true,
        },
    ];

    let state = {
        initialThemeApplied: false,
        originalCSS: null,
        previewingTheme: false
    };

    const repoManager = {
        getAll() {
            try {
                const repos = JSON.parse(
                    localStorage.getItem(STORAGE_KEYS.REPOS) || "null"
                );

                if (!repos) {
                    localStorage.setItem(STORAGE_KEYS.REPOS, JSON.stringify(DEFAULT_REPOS));
                    return DEFAULT_REPOS;
                }

                return repos;
            } catch (error) {
                console.error("Error getting repositories:", error);
                localStorage.setItem(STORAGE_KEYS.REPOS, JSON.stringify(DEFAULT_REPOS));
                return DEFAULT_REPOS;
            }
        },

        add(name, apiUrl, basePath) {
            try {
                const repos = this.getAll();

                const existingIndex = repos.findIndex((repo) => repo.name === name);
                if (existingIndex >= 0) {
                    if (repos[existingIndex].isDefault) {
                        uiHelper.showToast(
                            `Cannot modify default repository "${name}"`,
                            "error"
                        );
                        return false;
                    }

                    repos[existingIndex] = { name, apiUrl, basePath };
                    uiHelper.showToast(`Updated repository "${name}"`, "info");
                } else {
                    repos.push({ name, apiUrl, basePath });
                    uiHelper.showToast(`Added repository "${name}"`, "info");
                }

                localStorage.setItem(STORAGE_KEYS.REPOS, JSON.stringify(repos));
                uiHelper.updateReposList();
                return true;
            } catch (error) {
                console.error("Error adding repository:", error);
                uiHelper.showToast("Error adding repository", "error");
                return false;
            }
        },

        remove(name) {
            try {
                const repos = this.getAll();
                const repo = repos.find(r => r.name === name);

                if (!repo) return false;

                if (repo.isDefault && !confirm("This is a default repository. Are you sure you want to remove it?")) {
                    return false;
                }

                const filteredRepos = repos.filter((repo) => repo.name !== name);

                if (filteredRepos.length < repos.length) {
                    localStorage.setItem(
                        STORAGE_KEYS.REPOS,
                        JSON.stringify(filteredRepos)
                    );
                    uiHelper.showToast(`Removed repository "${name}"`, "info");
                    uiHelper.updateReposList();
                    return true;
                }

                return false;
            } catch (error) {
                console.error("Error removing repository:", error);
                uiHelper.showToast("Error removing repository", "error");
                return false;
            }
        },

        resetToDefault() {
            try {
                if (
                    confirm(
                        "Reset all repositories to default? This will remove any custom repositories you have added."
                    )
                ) {
                    localStorage.setItem(
                        STORAGE_KEYS.REPOS,
                        JSON.stringify(DEFAULT_REPOS)
                    );
                    uiHelper.showToast("Repositories reset to default", "info");
                    uiHelper.updateReposList();
                    return true;
                }
                return false;
            } catch (error) {
                console.error("Error resetting repositories:", error);
                uiHelper.showToast("Error resetting repositories", "error");
                return false;
            }
        }
    };

    const themeManager = {
        async fetchRepoContents(repo) {
            try {
                const response = await fetch(repo.apiUrl);
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }

                const data = await response.json();

                return data
                    .filter((item) => item.name.endsWith(".css"))
                    .map((item) => {
                        return {
                            name: item.name.replace(".css", ""),
                            url: repo.basePath + item.name,
                            repo: repo.name,
                        };
                    });
            } catch (error) {
                console.error(`Error fetching themes from ${repo.name}:`, error);
                uiHelper.showToast(`Error fetching themes from ${repo.name}`, "error");
                return [];
            }
        },

        async fetchAllThemes() {
            const statusElement = document.getElementById("theme-status");
            if (statusElement) {
                statusElement.textContent = "Fetching available themes...";
            }

            try {
                const allRepos = repoManager.getAll();
                const allThemesPromises = allRepos.map((repo) =>
                    this.fetchRepoContents(repo)
                );
                const allThemesArrays = await Promise.all(allThemesPromises);
                const allThemes = allThemesArrays.flat();

                const themesObject = {};
                allThemes.forEach((theme) => {
                    themesObject[theme.name] = theme;
                });

                localStorage.setItem(STORAGE_KEYS.THEMES_CACHE, JSON.stringify(themesObject));
                localStorage.setItem(
                    STORAGE_KEYS.CACHE_TIMESTAMP,
                    Date.now().toString()
                );

                if (statusElement) {
                    statusElement.textContent = `Found ${allThemes.length} themes from ${allRepos.length} repositories.`;
                }

                return themesObject;
            } catch (error) {
                console.error("Error fetching all themes:", error);
                if (statusElement) {
                    statusElement.textContent =
                        "Error fetching themes. Check console for details.";
                }
                uiHelper.showToast("Error fetching themes", "error");
                return {};
            }
        },

        async getThemes(forceRefresh = false) {
            try {
                const cachedThemes = localStorage.getItem(STORAGE_KEYS.THEMES_CACHE);
                const cacheTimestamp = localStorage.getItem(STORAGE_KEYS.CACHE_TIMESTAMP);

                const now = Date.now();
                const cacheAge = cacheTimestamp
                    ? now - parseInt(cacheTimestamp)
                    : Infinity;

                if (!forceRefresh && cachedThemes && cacheAge < CACHE_DURATION) {
                    return JSON.parse(cachedThemes);
                }

                return await this.fetchAllThemes();
            } catch (error) {
                console.error("Error getting themes:", error);
                uiHelper.showToast("Error getting themes", "error");
                return {};
            }
        },

        async applyTheme(themeName, skipConfirmation = false, initialLoad = false) {
            if (
                !skipConfirmation &&
                !initialLoad &&
                !confirm(`Apply theme: ${themeName}?`)
            ) {
                return;
            }

            try {
                const themes = await this.getThemes();
                if (!themes[themeName]) {
                    uiHelper.showToast(`Theme "${themeName}" not found`, "error");
                    return false;
                }

                const response = await fetch(themes[themeName].url);
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }

                let css = await response.text();

                css = this.processCSS(css);

                if (!state.originalCSS && !initialLoad) {
                    const existingStyle = document.getElementById(
                        "applied-theme-style"
                    );
                    if (existingStyle) {
                        state.originalCSS = existingStyle.textContent;
                    }
                }

                let styleElement = document.getElementById("applied-theme-style");
                if (!styleElement) {
                    styleElement = document.createElement("style");
                    styleElement.id = "applied-theme-style";
                    document.head.appendChild(styleElement);
                }

                styleElement.textContent = css;

                localStorage.setItem(STORAGE_KEYS.THEME, themeName);
                localStorage.setItem(STORAGE_KEYS.USER_CSS, css);

                if (!initialLoad) {
                    uiHelper.showToast(`Theme "${themeName}" applied`, "success");
                }

                state.previewingTheme = false;

                const cancelPreviewButton =
                    document.getElementById("cancel-preview");
                if (cancelPreviewButton) {
                    cancelPreviewButton.style.display = "none";
                }

                return true;
            } catch (error) {
                console.error("Error applying theme:", error);
                uiHelper.showToast(`Error applying theme: ${error.message}`, "error");
                return false;
            }
        },

        async previewTheme(themeName) {
            try {
                const themes = await this.getThemes();
                if (!themes[themeName]) {
                    uiHelper.showToast(`Theme "${themeName}" not found`, "error");
                    return false;
                }

                const response = await fetch(themes[themeName].url);
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }

                let css = await response.text();

                css = this.processCSS(css);

                const styleElement = document.getElementById("applied-theme-style");
                if (styleElement) {
                    state.originalCSS = styleElement.textContent;
                } else {
                    state.originalCSS = null;
                }

                let previewStyleElement = document.getElementById("applied-theme-style");
                if (!previewStyleElement) {
                    previewStyleElement = document.createElement("style");
                    previewStyleElement.id = "applied-theme-style";
                    document.head.appendChild(previewStyleElement);
                }

                previewStyleElement.textContent = css;
                state.previewingTheme = true;
                uiHelper.showToast(`Previewing "${themeName}"`, "info");

                return true;
            } catch (error) {
                console.error("Error previewing theme:", error);
                uiHelper.showToast(`Error previewing theme: ${error.message}`, "error");
                return false;
            }
        },

        processCSS(css) {
            let processedCSS = "/* Processed by Soyjak Theme Selector to ensure style overrides */\n";

            const cssRules = css.split('}');

            for (let i = 0; i < cssRules.length; i++) {
                if (cssRules[i].trim() === '') continue;

                const openBracePos = cssRules[i].indexOf('{');
                if (openBracePos === -1) {
                    processedCSS += cssRules[i] + '}\n';
                    continue;
                }

                const selector = cssRules[i].substring(0, openBracePos).trim();
                let declarations = cssRules[i].substring(openBracePos + 1).trim();

                if (selector.startsWith('@')) {
                    processedCSS += cssRules[i] + '}\n';
                    continue;
                }

                const declarationParts = declarations.split(';');
                let processedDeclarations = '';

                for (let j = 0; j < declarationParts.length; j++) {
                    const declaration = declarationParts[j].trim();
                    if (declaration === '') continue;

                    if (declaration.includes('!important')) {
                        processedDeclarations += declaration + '; ';
                    } else {
                        processedDeclarations += declaration + ' !important; ';
                    }
                }

                processedCSS += selector + ' { ' + processedDeclarations + '}\n';
            }

            return processedCSS;
        },

        cancelPreview() {
            if (!state.previewingTheme) {
                return;
            }

            const styleElement = document.getElementById("applied-theme-style");

            if (state.originalCSS === null) {
                if (styleElement) {
                    styleElement.remove();
                }
            } else {
                if (styleElement) {
                    styleElement.textContent = state.originalCSS;
                } else {
                    const newStyleElement = document.createElement("style");
                    newStyleElement.id = "applied-theme-style";
                    newStyleElement.textContent = state.originalCSS;
                    document.head.appendChild(newStyleElement);
                }
            }

            state.previewingTheme = false;
            uiHelper.showToast("Preview canceled", "info");

            const cancelPreviewButton = document.getElementById("cancel-preview");
            if (cancelPreviewButton) {
                cancelPreviewButton.style.display = "none";
            }
        },
        resetTheme() {
            try {
                if (!confirm("Reset to default theme?")) {
                    return false;
                }

                localStorage.removeItem(STORAGE_KEYS.THEME);
                localStorage.removeItem(STORAGE_KEYS.USER_CSS);

                const styleElement = document.getElementById("applied-theme-style");
                if (styleElement) {
                    styleElement.remove();
                }

                const select = document.getElementById("external-theme-select");
                if (select) {
                    select.value = "";
                }

                const themeInfo = document.getElementById("theme-info");
                if (themeInfo) {
                    themeInfo.style.display = "none";
                }

                const statusElement = document.getElementById("theme-status");
                if (statusElement) {
                    statusElement.textContent = "Theme reset to default.";
                }

                state.originalCSS = null;
                state.previewingTheme = false;

                uiHelper.showToast("Reset to default theme", "info");
                return true;
            } catch (error) {
                console.error("Error resetting theme:", error);
                uiHelper.showToast("Error resetting theme", "error");
                return false;
            }
        }
    };

    const favoritesManager = {
        getFavorites() {
            try {
                return JSON.parse(localStorage.getItem(STORAGE_KEYS.FAVORITES) || "[]");
            } catch (error) {
                console.error("Error getting favorites:", error);
                return [];
            }
        },

        toggleFavorite(themeName) {
            try {
                const favorites = this.getFavorites();
                const index = favorites.indexOf(themeName);

                if (index === -1) {
                    favorites.push(themeName);
                    uiHelper.showToast(`Added "${themeName}" to favorites`, "success");
                } else {
                    favorites.splice(index, 1);
                    uiHelper.showToast(`Removed "${themeName}" from favorites`, "info");
                }

                localStorage.setItem(STORAGE_KEYS.FAVORITES, JSON.stringify(favorites));
                return index === -1;
            } catch (error) {
                console.error("Error toggling favorite:", error);
                uiHelper.showToast("Error updating favorites", "error");
                return false;
            }
        }
    };

    const settingsManager = {
        exportSettings() {
            try {
                const settings = {
                    selectedTheme: localStorage.getItem(STORAGE_KEYS.THEME),
                    favorites: favoritesManager.getFavorites(),
                    repositories: repoManager.getAll(),
                };

                const blob = new Blob([JSON.stringify(settings, null, 2)], {
                    type: "application/json",
                });
                const url = URL.createObjectURL(blob);
                const a = document.createElement("a");
                a.href = url;
                a.download = "soyjak-theme-settings.json";
                a.click();

                URL.revokeObjectURL(url);
                uiHelper.showToast("Settings exported successfully", "info");
                return true;
            } catch (error) {
                console.error("Error exporting settings:", error);
                uiHelper.showToast("Error exporting settings", "error");
                return false;
            }
        },

        importSettings(file) {
            const reader = new FileReader();
            reader.onload = (e) => {
                try {
                    const settings = JSON.parse(e.target.result);

                    if (settings.repositories && Array.isArray(settings.repositories)) {
                        localStorage.setItem(
                            STORAGE_KEYS.REPOS,
                            JSON.stringify(settings.repositories)
                        );
                    }

                    if (settings.favorites && Array.isArray(settings.favorites)) {
                        localStorage.setItem(
                            STORAGE_KEYS.FAVORITES,
                            JSON.stringify(settings.favorites)
                        );
                    }

                    if (settings.selectedTheme) {
                        localStorage.setItem(STORAGE_KEYS.THEME, settings.selectedTheme);
                        themeManager.applyTheme(settings.selectedTheme, true);
                    }

                    uiHelper.updateReposList();
                    uiHelper.populateThemeSelect();
                    uiHelper.updateCacheInfo();
                    uiHelper.showToast("Settings imported successfully", "success");
                    return true;
                } catch (error) {
                    console.error("Error importing settings:", error);
                    uiHelper.showToast("Error importing settings", "error");
                    return false;
                }
            };
            reader.readAsText(file);
        }
    };

    const uiHelper = {
        showToast(message, type = "info") {
            const toast = document.createElement("div");
            toast.className = `theme-toast ${type}`;
            toast.textContent = message;
            document.body.appendChild(toast);

            setTimeout(() => {
                toast.classList.add("show");
            }, 10);

            setTimeout(() => {
                toast.classList.remove("show");
                setTimeout(() => {
                    if (document.body.contains(toast)) {
                        document.body.removeChild(toast);
                    }
                }, 500);
            }, 3000);
        },

        addStyles() {
            const styleElement = document.createElement("style");
            styleElement.textContent = `
                .theme-selector-container {
                    padding: 15px;
                    font-family: Arial, sans-serif;
                    max-width: 800px;
                    margin: 0 auto;
                }

                .theme-selector-header {
                    margin-bottom: 20px;
                    text-align: center;
                }

                .theme-selector-tabs {
                    display: flex;
                    border-bottom: 1px solid #ccc;
                    margin-bottom: 15px;
                }

                .theme-selector-tab {
                    padding: 8px 15px;
                    cursor: pointer;
                    border: 1px solid transparent;
                    border-bottom: none;
                    margin-right: 5px;
                    border-radius: 5px 5px 0 0;
                }

                .theme-selector-tab.active {
                    border-color: #ccc;
                    background-color: #f9f9f9;
                    margin-bottom: -1px;
                    padding-bottom: 9px;
                }

                .theme-selector-tab-content {
                    display: none;
                    padding: 15px;
                    border: 1px solid #ccc;
                    border-top: none;
                    background-color: #f9f9f9;
                }

                .theme-selector-tab-content.active {
                    display: block;
                }

                .theme-selector-search {
                    width: 100%;
                    padding: 8px;
                    margin-bottom: 10px;
                    box-sizing: border-box;
                }

                .theme-selector-row {
                    display: flex;
                    justify-content: space-between;
                    margin-bottom: 10px;
                }

                .theme-selector-button {
                    padding: 8px 15px;
                    cursor: pointer;
                    background-color: #f0f0f0;
                    border: 1px solid #ccc;
                    border-radius: 3px;
                    flex: 1;
                    margin: 0 5px;
                }

                .theme-selector-button:first-child {
                    margin-left: 0;
                }

                .theme-selector-button:last-child {
                    margin-right: 0;
                }

                .theme-selector-button:hover {
                    background-color: #e0e0e0;
                }

                .theme-selector-full-button {
                    width: 100%;
                    padding: 8px 15px;
                    cursor: pointer;
                    background-color: #f0f0f0;
                    border: 1px solid #ccc;
                    border-radius: 3px;
                    margin-bottom: 10px;
                }

                .theme-selector-full-button:hover {
                    background-color: #e0e0e0;
                }

                .theme-selector-info {
                    margin-top: 15px;
                    padding: 10px;
                    border: 1px solid #ddd;
                    background-color: #f9f9f9;
                    border-radius: 3px;
                }

                .theme-selector-status {
                    margin-top: 15px;
                    font-style: italic;
                    color: #666;
                }

                .theme-toast {
                    position: fixed;
                    bottom: 20px;
                    right: 20px;
                    padding: 10px 20px;
                    background-color: #333;
                    color: white;
                    border-radius: 5px;
                    z-index: 10000;
                    opacity: 0;
                    transition: opacity 0.5s;
                    max-width: 300px;
                }

                .theme-toast.show {
                    opacity: 1;
                }

                .theme-toast.info {
                    background-color: #2196F3;
                }

                .theme-toast.success {
                    background-color: #4CAF50;
                }

                .theme-toast.warning {
                    background-color: #FF9800;
                }

                .theme-toast.error {
                    background-color: #F44336;
                }

                .favorite-star {
                    cursor: pointer;
                    margin-left: 5px;
                    color: #ccc;
                }

                .favorite-star.active {
                    color: gold;
                }

                .repo-item {
                    margin-bottom: 5px;
                }

                .remove-repo-btn {
                    background-color: #f44336;
                    color: white;
                    border: none;
                    padding: 3px 8px;
                    border-radius: 3px;
                    cursor: pointer;
                }

                .remove-repo-btn:hover {
                    background-color: #d32f2f;
                }
            `;
            document.head.appendChild(styleElement);
        },

        async populateThemeSelect() {
            const select = document.getElementById("external-theme-select");
            if (!select) return;

            const themes = await themeManager.getThemes();
            const favorites = favoritesManager.getFavorites();

            select.innerHTML = "";

            const placeholderOption = document.createElement("option");
            placeholderOption.value = "";
            placeholderOption.textContent = "-- Select a theme --";
            select.appendChild(placeholderOption);

            const sortedThemeNames = Object.keys(themes).sort((a, b) => {
                const aIsFavorite = favorites.includes(a);
                const bIsFavorite = favorites.includes(b);

                if (aIsFavorite && !bIsFavorite) return -1;
                if (!aIsFavorite && bIsFavorite) return 1;

                return a.localeCompare(b);
            });

            const favoritesGroup = document.createElement("optgroup");
            favoritesGroup.label = "Favorites";

            const repoGroups = {};

            sortedThemeNames.forEach((themeName) => {
                const theme = themes[themeName];
                const isFavorite = favorites.includes(themeName);

                const option = document.createElement("option");
                option.value = themeName;
                option.textContent = themeName;
                option.dataset.repo = theme.repo;

                if (isFavorite) {
                    option.dataset.favorite = "true";
                    favoritesGroup.appendChild(option);
                } else {
                    if (!repoGroups[theme.repo]) {
                        repoGroups[theme.repo] = document.createElement("optgroup");
                        repoGroups[theme.repo].label = theme.repo;
                    }

                    repoGroups[theme.repo].appendChild(option);
                }
            });

            if (favoritesGroup.children.length > 0) {
                select.appendChild(favoritesGroup);
            }

            Object.values(repoGroups).forEach((group) => {
                if (group.children.length > 0) {
                    select.appendChild(group);
                }
            });

            const savedTheme = localStorage.getItem(STORAGE_KEYS.THEME);
            if (savedTheme && themes[savedTheme]) {
                select.value = savedTheme;
            }

            this.updateThemeInfo();
        },

        filterThemes(searchText) {
            const select = document.getElementById("external-theme-select");
            if (!select) return;

            const options = select.querySelectorAll("option");
            const optgroups = select.querySelectorAll("optgroup");

            searchText = searchText.toLowerCase();

            optgroups.forEach((group) => {
                group.style.display = "";
            });

            options.forEach((option) => {
                if (option.value === "") return;
                const themeName = option.textContent.toLowerCase();
                const matches = themeName.includes(searchText);

                option.style.display = matches ? "" : "none";
            });

            optgroups.forEach((group) => {
                const visibleOptions = Array.from(
                    group.querySelectorAll("option")
                ).filter((opt) => opt.style.display !== "none");
                group.style.display = visibleOptions.length > 0 ? "" : "none";
            });
        },

        async updateThemeInfo() {
            const select = document.getElementById("external-theme-select");
            const infoDiv = document.getElementById("theme-info");

            if (!select || !infoDiv) return;

            const selectedTheme = select.value;

            if (!selectedTheme) {
                infoDiv.style.display = "none";
                return;
            }

            const themes = await themeManager.getThemes();
            const theme = themes[selectedTheme];

            if (!theme) {
                infoDiv.style.display = "none";
                return;
            }

            const favorites = favoritesManager.getFavorites();
            const isFavorite = favorites.includes(selectedTheme);

            infoDiv.innerHTML = `
                <h3>
                    ${selectedTheme}
                    <span class="favorite-star ${isFavorite ? "active" : ""
                }" data-theme="${selectedTheme}">★</span>
                </h3>
                <p><strong>Repository:</strong> ${theme.repo}</p>
                <p><strong>URL:</strong> <a href="${theme.url}" target="_blank">${theme.url
                }</a></p>
            `;

            infoDiv.style.display = "block";

            const favoriteStar = infoDiv.querySelector(".favorite-star");
            if (favoriteStar) {
                favoriteStar.addEventListener("click", (event) => {
                    const themeName = event.target.dataset.theme;
                    if (!themeName) return;

                    const isNowFavorite = favoritesManager.toggleFavorite(themeName);
                    event.target.classList.toggle("active", isNowFavorite);

                    this.populateThemeSelect();
                });
            }
        },

        updateReposList() {
            const reposList = document.getElementById("repos-list");
            if (!reposList) return;

            const repos = repoManager.getAll();
            reposList.innerHTML = "";

            if (repos.length === 0) {
                reposList.innerHTML = "<p>No repositories configured.</p>";
                return;
            }

            repos.forEach((repo) => {
                const repoItem = document.createElement("div");
                repoItem.className = "repo-item";
                repoItem.innerHTML = `
                    <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; padding: 5px; border-bottom: 1px solid #ddd;">
                        <div>
                            <strong>${repo.name}</strong> ${repo.isDefault
                        ? '<span style="color: #666; font-size: 0.8em;">(Default)</span>'
                        : ""
                    }
                            <div style="font-size: 0.8em; color: #666;">API: ${repo.apiUrl}</div>
                            <div style="font-size: 0.8em; color: #666;">Base: ${repo.basePath}</div>
                        </div>
                        <button class="remove-repo-btn" data-repo="${repo.name}">Remove</button>
                    </div>
                `;
                reposList.appendChild(repoItem);

                const removeBtn = repoItem.querySelector(".remove-repo-btn");
                if (removeBtn) {
                    removeBtn.addEventListener("click", () => {
                        repoManager.remove(repo.name);
                    });
                }
            });
        },

        updateCacheInfo() {
            const cacheInfoDiv = document.getElementById("cache-info");
            if (!cacheInfoDiv) return;

            const cacheTimestamp = localStorage.getItem(STORAGE_KEYS.CACHE_TIMESTAMP);
            const themesCache = localStorage.getItem(STORAGE_KEYS.THEMES_CACHE);
            const repos = repoManager.getAll();
            const defaultRepoCount = repos.filter((repo) => repo.isDefault).length;
            const customRepoCount = repos.length - defaultRepoCount;

            if (!cacheTimestamp || !themesCache) {
                cacheInfoDiv.innerHTML = "<p>No theme cache found.</p>";
                return;
            }

            try {
                const themes = JSON.parse(themesCache);
                const themeCount = Object.keys(themes).length;
                const cacheDate = new Date(parseInt(cacheTimestamp));
                const now = new Date();
                const diffMs = now - cacheDate;
                const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
                const diffHours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
                const diffMins = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));

                cacheInfoDiv.innerHTML = `
                    <p><strong>Themes in cache:</strong> ${themeCount}</p>
                    <p><strong>Total repositories:</strong> ${repos.length}</p>
                    <p><strong>Default repositories:</strong> ${defaultRepoCount}</p>
                    <p><strong>Custom repositories:</strong> ${customRepoCount}</p>
                    <p><strong>Last updated:</strong> ${cacheDate.toLocaleString()}</p>
                    <p><strong>Cache age:</strong> ${diffDays}d ${diffHours}h ${diffMins}m</p>
                    <p><strong>Cache expires:</strong> After 7 days</p>
                `;
            } catch (e) {
                cacheInfoDiv.innerHTML = "<p>Error reading cache information.</p>";
                console.error("Error parsing cache:", e);
            }
        },

        switchTab(tabId) {
            const tabContents = document.querySelectorAll(
                ".theme-selector-tab-content"
            );
            tabContents.forEach((content) => {
                content.classList.remove("active");
            });

            const tabs = document.querySelectorAll(".theme-selector-tab");
            tabs.forEach((tab) => {
                tab.classList.remove("active");
            });

            const selectedTab = document.getElementById(`tab-${tabId}`);
            const selectedContent = document.getElementById(`tab-content-${tabId}`);

            if (selectedTab) {
                selectedTab.classList.add("active");
            }

            if (selectedContent) {
                selectedContent.classList.add("active");
            }
        },

        createThemeSelectorUI(options) {
            const optionsHTML = `
                <div class="theme-selector-container">
                    <div class="theme-selector-header">
                        <p>Select a theme from the dropdown and apply it to change the site's appearance.</p>
                    </div>

                    <div class="theme-selector-tabs">
                        <div id="tab-themes" class="theme-selector-tab active">Themes</div>
                        <div id="tab-settings" class="theme-selector-tab">Settings</div>
                    </div>

                    <div id="tab-content-themes" class="theme-selector-tab-content active">
                        <input type="text" id="theme-search" class="theme-selector-search" placeholder="Search themes...">
                        <select id="external-theme-select" style="width: 100%; margin-bottom: 5px;"></select>

                        <div class="theme-selector-row">
                            <button id="preview-external-theme" class="theme-selector-button">Preview</button>
                            <button id="apply-external-theme" class="theme-selector-button">Apply Theme</button>
                        </div>

                        <button id="cancel-preview" class="theme-selector-full-button" style="display: none;">Cancel Preview</button>
                        <button id="reset-external-theme" class="theme-selector-full-button">Reset to Default</button>

                        <div id="theme-info" class="theme-selector-info" style="display: none;"></div>
                    </div>

                    <div id="tab-content-settings" class="theme-selector-tab-content">
                        <h3>Theme Settings</h3>

                        <div class="theme-selector-row">
                            <button id="refresh-themes" class="theme-selector-full-button">Refresh Theme List</button>
                        </div>

                        <div class="theme-selector-row">
                            <button id="export-theme-settings" class="theme-selector-button">Export Settings</button>
                            <button id="import-theme-settings" class="theme-selector-button">Import Settings</button>
                        </div>

                        <input type="file" id="import-file" style="display: none;" accept=".json">

                        <div style="margin-top: 20px;">
                            <h4>Theme Repositories</h4>
                            <p>Manage repositories to fetch themes from.</p>

                            <div style="margin-bottom: 10px;">
                                <input type="text" id="repo-name" placeholder="Repository Name" style="width: 100%; margin-bottom: 5px; padding: 5px;">
                                <input type="text" id="repo-api-url" placeholder="GitHub API URL (e.g., https://api.github.com/repos/user/repo/contents/path)" style="width: 100%; margin-bottom: 5px; padding: 5px;">
                                <input type="text" id="repo-base-path" placeholder="Base Path (e.g., https://raw.githubusercontent.com/user/repo/branch/path/)" style="width: 100%; margin-bottom: 5px; padding: 5px;">
                                <button id="add-repo-btn" class="theme-selector-full-button">Add Repository</button>
                            </div>

                            <div id="repos-list" style="margin-top: 10px; max-height: 300px; overflow-y: auto; border: 1px solid #ddd; padding: 5px;"></div>

                            <button id="reset-repos-btn" class="theme-selector-full-button" style="margin-top: 10px;">Reset to Default Repositories</button>
                        </div>

                        <div style="margin-top: 20px;">
                            <h4>Cache Information</h4>
                            <div id="cache-info"></div>
                        </div>
                    </div>

                    <div id="theme-status" class="theme-selector-status"></div>
                </div>
            `;

            options.insertAdjacentHTML("beforeend", optionsHTML);
            this.updateCacheInfo();
            this.updateReposList();
            this.populateThemeSelect();

            this.setupEventListeners();
        },

        setupEventListeners() {
            const select = document.getElementById("external-theme-select");
            const searchInput = document.getElementById("theme-search");
            const previewButton = document.getElementById("preview-external-theme");
            const cancelPreviewButton = document.getElementById("cancel-preview");
            const applyButton = document.getElementById("apply-external-theme");
            const resetButton = document.getElementById("reset-external-theme");
            const refreshButton = document.getElementById("refresh-themes");
            const exportButton = document.getElementById("export-theme-settings");
            const importButton = document.getElementById("import-theme-settings");
            const importFile = document.getElementById("import-file");
            const tabElements = document.querySelectorAll(".theme-selector-tab");
            const addRepoBtn = document.getElementById("add-repo-btn");
            const resetReposBtn = document.getElementById("reset-repos-btn");

            if (select) {
                select.addEventListener("change", () => {
                    this.updateThemeInfo();
                });
            }

            if (searchInput) {
                searchInput.addEventListener("input", (e) => {
                    this.filterThemes(e.target.value);
                });
            }

            if (previewButton) {
                previewButton.addEventListener("click", () => {
                    const selectedTheme = select?.value;
                    if (selectedTheme) {
                        themeManager.previewTheme(selectedTheme);
                        if (cancelPreviewButton) {
                            cancelPreviewButton.style.display = "block";
                        }
                    } else {
                        this.showToast("Please select a theme first", "warning");
                    }
                });
            }

            if (cancelPreviewButton) {
                cancelPreviewButton.addEventListener("click", () => {
                    themeManager.cancelPreview();
                    cancelPreviewButton.style.display = "none";
                });
            }

            if (applyButton) {
                applyButton.addEventListener("click", () => {
                    const selectedTheme = select?.value;
                    if (selectedTheme) {
                        themeManager.applyTheme(selectedTheme);
                        if (cancelPreviewButton) {
                            cancelPreviewButton.style.display = "none";
                        }
                    } else {
                        this.showToast("Please select a theme first", "warning");
                    }
                });
            }

            if (resetButton) {
                resetButton.addEventListener("click", () => {
                    themeManager.resetTheme();
                    if (cancelPreviewButton) {
                        cancelPreviewButton.style.display = "none";
                    }
                });
            }

            if (refreshButton) {
                refreshButton.addEventListener("click", async () => {
                    const statusElement = document.getElementById("theme-status");
                    if (statusElement) {
                        statusElement.textContent = "Refreshing theme list...";
                    }

                    await themeManager.getThemes(true);
                    await this.populateThemeSelect();
                    this.updateCacheInfo();

                    if (statusElement) {
                        statusElement.textContent = "Theme list refreshed.";
                    }

                    this.showToast("Theme list refreshed", "info");
                });
            }

            if (exportButton) {
                exportButton.addEventListener("click", settingsManager.exportSettings);
            }

            if (importButton && importFile) {
                importButton.addEventListener("click", () => {
                    importFile.click();
                });

                importFile.addEventListener("change", (e) => {
                    if (e.target.files && e.target.files.length > 0) {
                        settingsManager.importSettings(e.target.files[0]);
                        e.target.value = "";
                    }
                });
            }

            if (addRepoBtn) {
                addRepoBtn.addEventListener("click", () => {
                    const nameInput = document.getElementById("repo-name");
                    const apiUrlInput = document.getElementById("repo-api-url");
                    const basePathInput = document.getElementById("repo-base-path");

                    if (!nameInput || !apiUrlInput || !basePathInput) return;

                    const name = nameInput.value.trim();
                    const apiUrl = apiUrlInput.value.trim();
                    const basePath = basePathInput.value.trim();

                    if (!name || !apiUrl || !basePath) {
                        this.showToast("Please fill in all repository fields", "error");
                        return;
                    }

                    if (repoManager.add(name, apiUrl, basePath)) {
                        nameInput.value = "";
                        apiUrlInput.value = "";
                        basePathInput.value = "";

                        const refreshThemesBtn = document.getElementById("refresh-themes");
                        if (refreshThemesBtn) {
                            refreshThemesBtn.click();
                        }
                    }
                });
            }

            if (resetReposBtn) {
                resetReposBtn.addEventListener("click", () => {
                    if (repoManager.resetToDefault()) {
                        const refreshThemesBtn = document.getElementById("refresh-themes");
                        if (refreshThemesBtn) {
                            refreshThemesBtn.click();
                        }
                    }
                });
            }

            tabElements.forEach((tab) => {
                tab.addEventListener("click", () => {
                    const tabId = tab.id.replace("tab-", "");
                    this.switchTab(tabId);
                });
            });
        }
    };

    async function init() {
        console.log("Initializing Soyjak Theme Selector");

        uiHelper.addStyles();

        try {
            const themes = await themeManager.getThemes();
            console.log(`Loaded ${Object.keys(themes).length} themes`);

            const savedTheme = localStorage.getItem(STORAGE_KEYS.THEME);
            const savedCss = localStorage.getItem(STORAGE_KEYS.USER_CSS);

            if (savedCss && !state.initialThemeApplied) {
                const styleElement = document.createElement("style");
                styleElement.id = "applied-theme-style";
                styleElement.textContent = savedCss;
                document.head.appendChild(styleElement);
                state.initialThemeApplied = true;
                console.log("Applied saved CSS from localStorage");
            } else if (savedTheme && themes[savedTheme] && !state.initialThemeApplied) {
                console.log(`Found saved theme: ${savedTheme}`);
                state.initialThemeApplied = true;
                await themeManager.applyTheme(savedTheme, true, true);
            }
        } catch (error) {
            console.error("Error during initialization:", error);
        }

        let checkAttempts = 0;
        const maxAttempts = 100;
        const checkInterval = setInterval(() => {
            checkAttempts++;

            if (typeof Options !== "undefined") {
                clearInterval(checkInterval);
                console.log("Options object found, adding theme selector tab");

                try {
                    let options = Options.add_tab(
                        "external-themes",
                        "css3",
                        "External Themes"
                    ).content[0];
                    uiHelper.createThemeSelectorUI(options);
                } catch (error) {
                    console.error("Error creating theme selector UI:", error);
                }
            }

            if (checkAttempts >= maxAttempts) {
                clearInterval(checkInterval);
                console.log("Stopped checking for Options object (timeout)");
            }
        }, 100);
    }

    if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", init);
    } else {
        init();
    }
})();