Soyjak Themer

Select and apply themes from GitHub repositories to soyjak.st

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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