您需要先安装一个扩展,例如 篡改猴、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();
- }
- })();