您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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(); } })();