mirror of
https://github.com/yazanammar/obsidian-theme-engine.git
synced 2026-07-22 06:44:37 +00:00
Create main.ts
This commit is contained in:
parent
50605ebcc0
commit
aa3640d332
1 changed files with 826 additions and 0 deletions
826
src/main.ts
Normal file
826
src/main.ts
Normal file
|
|
@ -0,0 +1,826 @@
|
|||
/*
|
||||
* Color Master - Obsidian Plugin
|
||||
* Version: 1.0.7
|
||||
* Author: Yazan Ammar (GitHub : https://github.com/YazanAmmar )
|
||||
* Description: Provides a comprehensive UI to control all Obsidian CSS color variables directly,
|
||||
* removing the need for Force Mode and expanding customization options.
|
||||
*/
|
||||
import { flattenVars } from "./utils";
|
||||
import { initializeT, t } from "./i18n";
|
||||
import {
|
||||
Plugin,
|
||||
moment,
|
||||
PluginSettingTab,
|
||||
Setting,
|
||||
Notice,
|
||||
Modal,
|
||||
ButtonComponent,
|
||||
setIcon,
|
||||
} from "obsidian";
|
||||
import { PluginSettings } from "./types";
|
||||
import {
|
||||
DEFAULT_VARS,
|
||||
DEFAULT_SETTINGS,
|
||||
TEXT_TO_BG_MAP,
|
||||
BUILT_IN_PROFILES_VARS,
|
||||
BUILT_IN_PROFILES_DATA,
|
||||
COLOR_DESCRIPTIONS,
|
||||
COLOR_DESCRIPTIONS_AR,
|
||||
COLOR_NAMES,
|
||||
COLOR_NAMES_AR,
|
||||
} from "./constants";
|
||||
import { ColorMasterSettingTab } from "./ui/settingsTab";
|
||||
|
||||
let T: ColorMaster;
|
||||
|
||||
export default class ColorMaster extends Plugin {
|
||||
settings: PluginSettings;
|
||||
iconizeWatcherInterval: number | null = null;
|
||||
colorUpdateInterval: number | null = null;
|
||||
pendingVarUpdates: Record<string, string> = {};
|
||||
settingTabInstance: ColorMasterSettingTab | null = null;
|
||||
liveNoticeRules: any[] | null = null;
|
||||
liveNoticeRuleType: "text" | "background" | null = null;
|
||||
iconizeObserver: MutationObserver;
|
||||
noticeObserver: MutationObserver;
|
||||
cssHistory: Record<string, { undoStack: string[]; redoStack: string[] }> = {};
|
||||
|
||||
startColorUpdateLoop() {
|
||||
this.stopColorUpdateLoop();
|
||||
|
||||
const fps = this.settings.colorUpdateFPS;
|
||||
if (!this.settings.pluginEnabled || !fps || fps <= 0) {
|
||||
return; // Stop if disabled or FPS is 0
|
||||
}
|
||||
|
||||
const intervalMs = 1000 / fps;
|
||||
this.colorUpdateInterval = window.setInterval(() => {
|
||||
const pendingKeys = Object.keys(this.pendingVarUpdates);
|
||||
if (pendingKeys.length === 0) return; // Do nothing if the box is empty
|
||||
|
||||
for (const varName of pendingKeys) {
|
||||
document.body.style.setProperty(
|
||||
varName,
|
||||
this.pendingVarUpdates[varName]
|
||||
);
|
||||
}
|
||||
|
||||
this.pendingVarUpdates = {}; // Empty the box after applying
|
||||
|
||||
// Trigger updates for Graph View and accessibility checkers
|
||||
this.app.workspace.trigger("css-change");
|
||||
|
||||
if (this.settingTabInstance) {
|
||||
this.settingTabInstance?.updateAccessibilityCheckers();
|
||||
}
|
||||
// ensure iconize icons update live using current computed var
|
||||
try {
|
||||
this.forceIconizeColors();
|
||||
} catch (e) {
|
||||
console.warn("forceIconizeColors failed in update loop", e);
|
||||
}
|
||||
}, intervalMs);
|
||||
}
|
||||
|
||||
// New method to stop the update loop
|
||||
stopColorUpdateLoop() {
|
||||
if (this.colorUpdateInterval) {
|
||||
window.clearInterval(this.colorUpdateInterval);
|
||||
this.colorUpdateInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// New method to easily restart the loop when settings change
|
||||
restartColorUpdateLoop() {
|
||||
this.stopColorUpdateLoop();
|
||||
this.startColorUpdateLoop();
|
||||
}
|
||||
|
||||
applyCustomCssForProfile(profileName: string) {
|
||||
try {
|
||||
if (!profileName) profileName = this.settings.activeProfile;
|
||||
const profile = this.settings.profiles?.[profileName];
|
||||
this.removeInjectedCustomCss();
|
||||
|
||||
if (!profile || !profile.isCssProfile || !profile.customCss) {
|
||||
return;
|
||||
}
|
||||
|
||||
const el = document.createElement("style");
|
||||
el.id = `cm-custom-css-for-profile`;
|
||||
el.textContent = profile.customCss;
|
||||
document.head.appendChild(el);
|
||||
} catch (e) {
|
||||
console.warn("applyCustomCssForProfile failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
removeInjectedCustomCss() {
|
||||
try {
|
||||
const oldStyle = document.getElementById("cm-custom-css-for-profile");
|
||||
if (oldStyle) oldStyle.remove();
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
|
||||
applyCssSnippets() {
|
||||
this.removeCssSnippets();
|
||||
const activeProfile = this.settings.profiles[this.settings.activeProfile];
|
||||
if (!activeProfile) return;
|
||||
|
||||
const snippets = Array.isArray(activeProfile.snippets)
|
||||
? activeProfile.snippets
|
||||
: [];
|
||||
|
||||
const enabledCss = snippets
|
||||
.filter((s) => s.enabled && s.css)
|
||||
.map((s) => `/* Snippet: ${s.name} */\n${s.css}`)
|
||||
.join("\n\n");
|
||||
|
||||
if (enabledCss) {
|
||||
const el = document.createElement("style");
|
||||
el.id = "cm-css-snippets";
|
||||
el.textContent = enabledCss;
|
||||
document.head.appendChild(el);
|
||||
}
|
||||
}
|
||||
|
||||
removeCssSnippets() {
|
||||
const el = document.getElementById("cm-css-snippets");
|
||||
if (el) el.remove();
|
||||
}
|
||||
|
||||
// New method to apply pending changes instantly
|
||||
applyPendingNow() {
|
||||
try {
|
||||
const pending = this.pendingVarUpdates || {};
|
||||
const keys = Object.keys(pending);
|
||||
if (keys.length === 0) {
|
||||
// If nothing is pending, still trigger a repaint for safety
|
||||
this.app.workspace.trigger("css-change");
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply all pending CSS properties
|
||||
for (const k of keys) {
|
||||
document.body.style.setProperty(k, pending[k]);
|
||||
}
|
||||
|
||||
// Clear pending updates
|
||||
this.pendingVarUpdates = {};
|
||||
|
||||
// Notify Obsidian and other components to update
|
||||
this.app.workspace.trigger("css-change");
|
||||
this.forceIconizeColors();
|
||||
|
||||
// Force graph view to repaint by dispatching a resize event
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
} catch (e) {
|
||||
console.error("Color Master: applyPendingNow failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
pinProfileSnapshot(profileName: string) {
|
||||
if (!profileName) profileName = this.settings.activeProfile;
|
||||
this.settings.pinnedSnapshots = this.settings.pinnedSnapshots || {};
|
||||
const profile = this.settings.profiles?.[profileName];
|
||||
if (!profile) {
|
||||
new Notice(t("NOTICE_PROFILE_NOT_FOUND"));
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshotData = {
|
||||
vars: JSON.parse(JSON.stringify(profile.vars || {})),
|
||||
customCss: profile.customCss || "",
|
||||
snippets: JSON.parse(JSON.stringify(profile.snippets || {})),
|
||||
noticeRules: JSON.parse(
|
||||
JSON.stringify(profile.noticeRules || { text: [], background: [] })
|
||||
),
|
||||
};
|
||||
|
||||
this.settings.pinnedSnapshots[profileName] = {
|
||||
pinnedAt: new Date().toISOString(),
|
||||
...snapshotData,
|
||||
};
|
||||
return this.saveSettings();
|
||||
}
|
||||
|
||||
async resetProfileToPinned(profileName: string): Promise<void> {
|
||||
if (!profileName) profileName = this.settings.activeProfile;
|
||||
const snap = this.settings.pinnedSnapshots?.[profileName];
|
||||
if (!snap || !snap.vars) {
|
||||
new Notice(t("NOTICE_NO_PINNED_SNAPSHOT"));
|
||||
return;
|
||||
}
|
||||
|
||||
const activeProfile = this.settings.profiles[profileName];
|
||||
if (!activeProfile) {
|
||||
new Notice(t("NOTICE_PROFILE_NOT_FOUND"));
|
||||
return;
|
||||
}
|
||||
|
||||
activeProfile.vars = JSON.parse(JSON.stringify(snap.vars));
|
||||
activeProfile.customCss = snap.customCss || "";
|
||||
activeProfile.snippets = snap.snippets
|
||||
? JSON.parse(JSON.stringify(snap.snippets))
|
||||
: {};
|
||||
activeProfile.noticeRules = snap.noticeRules
|
||||
? JSON.parse(JSON.stringify(snap.noticeRules))
|
||||
: { text: [], background: [] };
|
||||
|
||||
Object.keys(snap.vars).forEach((k) => {
|
||||
this.pendingVarUpdates[k] = snap.vars[k];
|
||||
});
|
||||
|
||||
await this.saveSettings();
|
||||
this.applyPendingNow();
|
||||
}
|
||||
|
||||
resetIconizeWatcher() {
|
||||
if (this.iconizeWatcherInterval) {
|
||||
window.clearInterval(this.iconizeWatcherInterval);
|
||||
}
|
||||
|
||||
const intervalMilliseconds = this.settings.cleanupInterval * 1000;
|
||||
|
||||
this.iconizeWatcherInterval = window.setInterval(() => {
|
||||
const iconizeIDs = ["obsidian-icon-folder", "iconize"];
|
||||
const isIconizeInstalled = iconizeIDs.some(
|
||||
(id) => (this.app as any).plugins.plugins[id]
|
||||
);
|
||||
|
||||
if (!isIconizeInstalled) {
|
||||
this.removeOrphanedIconizeElements();
|
||||
}
|
||||
}, intervalMilliseconds);
|
||||
|
||||
this.registerInterval(this.iconizeWatcherInterval);
|
||||
}
|
||||
|
||||
async onload() {
|
||||
initializeT(this);
|
||||
T = this;
|
||||
await this.loadSettings();
|
||||
this.liveNoticeRules = null;
|
||||
this.liveNoticeRuleType = null;
|
||||
// --- New code: for automatic language detection ---
|
||||
if (this.settings.language === "auto") {
|
||||
const obsidianLang = moment.locale();
|
||||
if (obsidianLang === "ar") {
|
||||
this.settings.language = "ar"; // If it is Arabic, we will designate it as Arabic.
|
||||
} else {
|
||||
this.settings.language = "en"; // For any other language, we set it to English.
|
||||
}
|
||||
await this.saveSettings();
|
||||
}
|
||||
|
||||
T = this;
|
||||
this.addCommand({
|
||||
id: "toggle-color-master",
|
||||
name: "Toggle Color Master",
|
||||
callback: async () => {
|
||||
this.settings.pluginEnabled = !this.settings.pluginEnabled;
|
||||
await this.saveSettings();
|
||||
new Notice(
|
||||
this.settings.pluginEnabled
|
||||
? t("PLUGIN_ENABLED_NOTICE")
|
||||
: t("PLUGIN_DISABLED_NOTICE")
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "cycle-color-profile",
|
||||
name: "Cycle Color Master Profile",
|
||||
callback: async () => {
|
||||
const names = Object.keys(this.settings.profiles || {});
|
||||
if (names.length === 0) {
|
||||
new Notice(t("NOTICE_NO_PROFILES_FOUND"));
|
||||
return;
|
||||
}
|
||||
const idx = names.indexOf(this.settings.activeProfile);
|
||||
const next = names[(idx + 1) % names.length];
|
||||
this.settings.activeProfile = next;
|
||||
await this.saveSettings();
|
||||
new Notice(t("NOTICE_ACTIVE_PROFILE_SWITCHED", next));
|
||||
},
|
||||
});
|
||||
|
||||
// Store a reference to the settings tab and add it
|
||||
this.settingTabInstance = new ColorMasterSettingTab(this.app, this);
|
||||
this.addSettingTab(this.settingTabInstance);
|
||||
|
||||
this.app.workspace.onLayoutReady(() => {
|
||||
this.applyStyles();
|
||||
setTimeout(() => this.app.workspace.trigger("css-change"), 100);
|
||||
|
||||
// Start the update engine
|
||||
this.startColorUpdateLoop();
|
||||
|
||||
this.iconizeObserver = new MutationObserver(() => {
|
||||
if (
|
||||
this.settings.pluginEnabled &&
|
||||
this.settings.overrideIconizeColors
|
||||
) {
|
||||
this.forceIconizeColors();
|
||||
}
|
||||
});
|
||||
|
||||
this.iconizeObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
this.register(() => this.iconizeObserver.disconnect());
|
||||
});
|
||||
this.resetIconizeWatcher();
|
||||
this.noticeObserver = new MutationObserver((mutations) => {
|
||||
if (!this.settings.pluginEnabled) return;
|
||||
mutations.forEach((mutation) => {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType !== 1) return;
|
||||
|
||||
if ((node as Element).matches(".notice, .toast")) {
|
||||
this.processNotice(node as HTMLElement);
|
||||
}
|
||||
(node as Element)
|
||||
.querySelectorAll(".notice, .toast")
|
||||
.forEach(this.processNotice.bind(this));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.noticeObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
document
|
||||
.querySelectorAll(".notice, .toast")
|
||||
.forEach(this.processNotice.bind(this));
|
||||
console.log("Color Master loaded with smart Iconize cleanup.");
|
||||
}
|
||||
|
||||
onunload() {
|
||||
if (this.noticeObserver) this.noticeObserver.disconnect();
|
||||
this.clearStyles();
|
||||
this.removeInjectedCustomCss();
|
||||
this.stopColorUpdateLoop();
|
||||
console.log("Color Master v1.0.7 unloaded.");
|
||||
}
|
||||
|
||||
async refreshOpenGraphViews() {
|
||||
const graphLeaves = this.app.workspace.getLeavesOfType("graph");
|
||||
if (graphLeaves.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Color Master: Found ${graphLeaves.length} graph(s). Applying Plan C (programmatic rebuild).`
|
||||
);
|
||||
|
||||
for (const leaf of graphLeaves) {
|
||||
const currentState = leaf.getViewState();
|
||||
|
||||
await leaf.setViewState({
|
||||
...currentState,
|
||||
type: "graph",
|
||||
state: { ...currentState.state },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
forceIconizeColors() {
|
||||
// read the *computed* CSS var first (this covers live-preview via pendingVarUpdates)
|
||||
let computedIconizeColor = null;
|
||||
try {
|
||||
const cssVal = getComputedStyle(document.body).getPropertyValue(
|
||||
"--iconize-icon-color"
|
||||
);
|
||||
if (cssVal) computedIconizeColor = cssVal.trim();
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
"Color Master: failed to read computed --iconize-icon-color",
|
||||
e
|
||||
);
|
||||
computedIconizeColor = null;
|
||||
}
|
||||
|
||||
// fallback to stored profile value if computed is empty
|
||||
const storedIconizeColor =
|
||||
this.settings.profiles?.[this.settings.activeProfile]?.vars?.[
|
||||
"--iconize-icon-color"
|
||||
];
|
||||
|
||||
const iconizeColor = this.settings.overrideIconizeColors
|
||||
? computedIconizeColor || storedIconizeColor || null
|
||||
: null;
|
||||
|
||||
// Iterate over elements that Iconize marks (keep the original safe logic)
|
||||
document.querySelectorAll(".iconize-icon").forEach((iconNode) => {
|
||||
const svg = iconNode.querySelector("svg");
|
||||
if (!svg) return;
|
||||
|
||||
[svg, ...svg.querySelectorAll("*")].forEach((el: any) => {
|
||||
if (typeof el.hasAttribute !== "function") return;
|
||||
|
||||
if (!iconizeColor) {
|
||||
// remove inline overrides to let theme/defaults show
|
||||
el.style.fill = "";
|
||||
el.style.stroke = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const originalFill = el.getAttribute("fill");
|
||||
const originalStroke = el.getAttribute("stroke");
|
||||
|
||||
// apply with !important so plugin/theme inline styles are overridden
|
||||
if (
|
||||
originalFill &&
|
||||
originalFill !== "none" &&
|
||||
!originalFill.startsWith("url(")
|
||||
) {
|
||||
try {
|
||||
el.style.setProperty("fill", iconizeColor, "important");
|
||||
} catch (e) {
|
||||
el.style.fill = iconizeColor;
|
||||
}
|
||||
}
|
||||
|
||||
if (originalStroke && originalStroke !== "none") {
|
||||
try {
|
||||
el.style.setProperty("stroke", iconizeColor, "important");
|
||||
} catch (e) {
|
||||
el.style.stroke = iconizeColor;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
removeOrphanedIconizeElements() {
|
||||
const iconizeIDs = ["obsidian-icon-folder", "iconize"];
|
||||
const isIconizeInstalled = iconizeIDs.some(
|
||||
(id) => (this.app as any).plugins.plugins[id]
|
||||
);
|
||||
|
||||
// We only proceed if Iconize is actually installed. If so, we do nothing.
|
||||
if (isIconizeInstalled) {
|
||||
return;
|
||||
}
|
||||
// Find all elements with the .iconize-icon class and check if they have content.
|
||||
const orphanedIcons = document.querySelectorAll(".iconize-icon");
|
||||
|
||||
// If we found any potential orphans, we log it for debugging.
|
||||
if (orphanedIcons.length > 0) {
|
||||
console.log(
|
||||
`Color Master: Found ${orphanedIcons.length} orphaned Iconize elements. Cleaning up...`
|
||||
);
|
||||
orphanedIcons.forEach((icon) => icon.remove());
|
||||
}
|
||||
}
|
||||
|
||||
applyStyles() {
|
||||
this.removeOrphanedIconizeElements();
|
||||
this.clearStyles();
|
||||
if (!this.settings.pluginEnabled) {
|
||||
this.removeInjectedCustomCss();
|
||||
return;
|
||||
}
|
||||
|
||||
const profile = this.settings.profiles[this.settings.activeProfile];
|
||||
if (!profile) {
|
||||
console.error("Color Master: Active profile not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(profile.vars)) {
|
||||
document.body.style.setProperty(key, value);
|
||||
}
|
||||
|
||||
this.forceIconizeColors();
|
||||
setTimeout(() => this.forceIconizeColors(), 100);
|
||||
|
||||
const themeType = profile.themeType || "auto";
|
||||
|
||||
if (themeType === "dark") {
|
||||
document.body.classList.remove("theme-light");
|
||||
document.body.classList.add("theme-dark");
|
||||
} else if (themeType === "light") {
|
||||
document.body.classList.remove("theme-dark");
|
||||
document.body.classList.add("theme-light");
|
||||
}
|
||||
this.applyCustomCssForProfile(this.settings.activeProfile);
|
||||
this.applyCssSnippets();
|
||||
// Add/Remove body class for RTL notices
|
||||
if (this.settings.language === "ar" || this.settings.language === "fa") {
|
||||
document.body.classList.add("color-master-rtl");
|
||||
} else {
|
||||
document.body.classList.remove("color-master-rtl");
|
||||
}
|
||||
}
|
||||
|
||||
clearStyles() {
|
||||
this.removeCssSnippets();
|
||||
const allVars = new Set();
|
||||
|
||||
Object.keys(flattenVars(DEFAULT_VARS)).forEach((key) => allVars.add(key));
|
||||
|
||||
for (const profileName in this.settings.profiles) {
|
||||
const profile = this.settings.profiles[profileName];
|
||||
if (profile && profile.vars) {
|
||||
Object.keys(profile.vars).forEach((key) => allVars.add(key));
|
||||
}
|
||||
}
|
||||
|
||||
allVars.forEach((key: string) => {
|
||||
document.body.style.removeProperty(key);
|
||||
});
|
||||
|
||||
document.querySelectorAll(".iconize-icon").forEach((iconNode) => {
|
||||
const svg = iconNode.querySelector("svg");
|
||||
if (!svg) return;
|
||||
|
||||
[svg, ...svg.querySelectorAll("*")].forEach((el: any) => {
|
||||
if (typeof el.hasAttribute !== "function") return;
|
||||
|
||||
el.style.removeProperty("fill");
|
||||
el.style.removeProperty("stroke");
|
||||
});
|
||||
});
|
||||
|
||||
const styleId = "color-master-overrides";
|
||||
const overrideStyleEl = document.getElementById(styleId);
|
||||
if (overrideStyleEl) {
|
||||
overrideStyleEl.remove();
|
||||
}
|
||||
this.app.workspace.trigger("css-change");
|
||||
document.body.classList.remove("color-master-rtl");
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
|
||||
let migrationNeeded = false;
|
||||
if (
|
||||
this.settings.noticeRules &&
|
||||
Object.keys(this.settings.profiles || {}).length > 0
|
||||
) {
|
||||
console.log(
|
||||
"Color Master: Detected old global notice rules. Starting migration..."
|
||||
);
|
||||
for (const profileName in this.settings.profiles) {
|
||||
const profile = this.settings.profiles[profileName];
|
||||
if (!profile.noticeRules) {
|
||||
profile.noticeRules = JSON.parse(
|
||||
JSON.stringify(this.settings.noticeRules)
|
||||
);
|
||||
}
|
||||
}
|
||||
delete (this.settings as any).noticeRules;
|
||||
migrationNeeded = true;
|
||||
}
|
||||
|
||||
for (const profileName in this.settings.profiles) {
|
||||
const profile = this.settings.profiles[profileName];
|
||||
if (!profile.noticeRules) {
|
||||
profile.noticeRules = { text: [], background: [] };
|
||||
migrationNeeded = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (migrationNeeded) {
|
||||
console.log(
|
||||
"Color Master: Notice rules migration complete. Saving new settings structure."
|
||||
);
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
// --- Migration logic for existing users ---
|
||||
let settingsChanged = false;
|
||||
if (!this.settings.noticeRules) {
|
||||
this.settings.noticeRules = {
|
||||
text: [],
|
||||
background: [],
|
||||
};
|
||||
settingsChanged = true;
|
||||
}
|
||||
|
||||
if (settingsChanged) {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
// --- End of migration logic ---
|
||||
if (!this.settings.installDate) {
|
||||
this.settings.installDate = new Date().toISOString();
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
if (!this.settings.pinnedSnapshots) {
|
||||
this.settings.pinnedSnapshots = {};
|
||||
}
|
||||
|
||||
// Seed pinned snapshots for up to the first 5 profiles if they don't have one
|
||||
try {
|
||||
const profileNames = Object.keys(this.settings.profiles || {});
|
||||
let changed = false;
|
||||
for (let i = 0; i < Math.min(5, profileNames.length); i++) {
|
||||
const name = profileNames[i];
|
||||
if (!this.settings.pinnedSnapshots[name]) {
|
||||
const vars = this.settings.profiles[name]?.vars || {};
|
||||
this.settings.pinnedSnapshots[name] = {
|
||||
pinnedAt: new Date().toISOString(),
|
||||
vars: JSON.parse(JSON.stringify(vars)),
|
||||
};
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
console.log("Color Master: Seeding initial pinned snapshots.");
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
let snippetsMigrationNeeded = false;
|
||||
for (const profileName in this.settings.profiles) {
|
||||
const profile = this.settings.profiles[profileName];
|
||||
if (
|
||||
profile &&
|
||||
profile.snippets &&
|
||||
!Array.isArray(profile.snippets) &&
|
||||
typeof profile.snippets === "object"
|
||||
) {
|
||||
snippetsMigrationNeeded = true;
|
||||
const snippetsArray = Object.entries(profile.snippets).map(
|
||||
([name, data]: [string, any]) => ({
|
||||
id: `snippet-${Date.now()}-${Math.random()}`,
|
||||
name: name,
|
||||
css: data.css || "",
|
||||
enabled: data.enabled !== false,
|
||||
})
|
||||
);
|
||||
profile.snippets = snippetsArray;
|
||||
}
|
||||
}
|
||||
|
||||
if (snippetsMigrationNeeded) {
|
||||
console.log(
|
||||
"Color Master: The clipping data structure is being migrated to the new format (array)."
|
||||
);
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Color Master: failed to seed pinnedSnapshots", e);
|
||||
}
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
this.applyStyles();
|
||||
await this.refreshOpenGraphViews();
|
||||
this.app.workspace.trigger("css-change");
|
||||
}
|
||||
|
||||
processNotice(el: HTMLElement) {
|
||||
let noticeText = (el.textContent || "").toLowerCase();
|
||||
const testKeywordEl = el.querySelector(".cm-test-keyword") as HTMLElement;
|
||||
|
||||
if (testKeywordEl) {
|
||||
noticeText = (testKeywordEl.textContent || "").toLowerCase();
|
||||
}
|
||||
if (!el || !el.classList || el.dataset.cmProcessed === "true") return;
|
||||
|
||||
el.dataset.cmProcessed = "true";
|
||||
|
||||
try {
|
||||
const settings = this.settings;
|
||||
const activeProfile = settings.profiles[settings.activeProfile];
|
||||
|
||||
if (!activeProfile) return;
|
||||
|
||||
const liveRules = this.liveNoticeRules;
|
||||
const liveRuleType = this.liveNoticeRuleType;
|
||||
|
||||
const bgRules =
|
||||
liveRuleType === "background" && liveRules
|
||||
? liveRules
|
||||
: activeProfile?.noticeRules?.background || [];
|
||||
|
||||
const textRules =
|
||||
liveRuleType === "text" && liveRules
|
||||
? liveRules
|
||||
: activeProfile?.noticeRules?.text || [];
|
||||
|
||||
let finalBgColor = activeProfile.vars["--cm-notice-bg-default"];
|
||||
let finalTextColor = activeProfile.vars["--cm-notice-text-default"];
|
||||
|
||||
for (const rule of bgRules) {
|
||||
if (!rule.keywords || !rule.keywords.trim()) continue;
|
||||
const keywords = (rule.keywords || "").toLowerCase();
|
||||
let match = false;
|
||||
|
||||
if (rule.isRegex) {
|
||||
try {
|
||||
const regex = new RegExp(keywords, "i");
|
||||
if (regex.test(noticeText)) match = true;
|
||||
} catch (e) {}
|
||||
} else {
|
||||
const keywordArray = keywords
|
||||
.split(",")
|
||||
.map((k: string) => k.trim())
|
||||
.filter(Boolean);
|
||||
for (const keyword of keywordArray) {
|
||||
if (noticeText.includes(keyword)) {
|
||||
match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
finalBgColor = rule.color;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const rule of textRules) {
|
||||
if (!rule.keywords || !rule.keywords.trim()) continue;
|
||||
const keywords = (rule.keywords || "").toLowerCase();
|
||||
let match = false;
|
||||
|
||||
if (rule.isRegex) {
|
||||
try {
|
||||
const regex = new RegExp(keywords, "i");
|
||||
if (regex.test(noticeText)) match = true;
|
||||
} catch (e) {}
|
||||
} else {
|
||||
const keywordArray = keywords
|
||||
.split(",")
|
||||
.map((k: string) => k.trim())
|
||||
.filter(Boolean);
|
||||
for (const keyword of keywordArray) {
|
||||
if (noticeText.includes(keyword)) {
|
||||
match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (match) {
|
||||
finalTextColor = rule.color;
|
||||
break;
|
||||
}
|
||||
}
|
||||
el.style.setProperty("background-color", finalBgColor, "important");
|
||||
el.style.setProperty("color", finalTextColor, "important");
|
||||
} catch (e) {
|
||||
console.warn("Color Master: processNotice failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes a new state to the history for a given CSS snippet/profile.
|
||||
* @param id - A unique identifier for the snippet or profile.
|
||||
* @param content - The new CSS content to save.
|
||||
*/
|
||||
pushCssHistory(id: string, content: string) {
|
||||
if (!this.cssHistory[id]) {
|
||||
this.cssHistory[id] = { undoStack: [], redoStack: [] };
|
||||
}
|
||||
const history = this.cssHistory[id];
|
||||
const lastState = history.undoStack[history.undoStack.length - 1];
|
||||
|
||||
// Only add to history if the content has actually changed
|
||||
if (lastState !== content) {
|
||||
history.undoStack.push(content);
|
||||
// When a new action is taken, the redo stack must be cleared
|
||||
history.redoStack = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Undoes the last change for a given ID and returns the previous state.
|
||||
* @param id - The unique identifier.
|
||||
* @returns The previous content, or null if no history.
|
||||
*/
|
||||
undoCssHistory(id: string): string | null {
|
||||
const history = this.cssHistory[id];
|
||||
if (history && history.undoStack.length > 1) {
|
||||
const currentState = history.undoStack.pop()!;
|
||||
history.redoStack.push(currentState);
|
||||
return history.undoStack[history.undoStack.length - 1];
|
||||
}
|
||||
return null; // Can't undo the initial state
|
||||
}
|
||||
|
||||
/**
|
||||
* Redoes the last undone change for a given ID.
|
||||
* @param id - The unique identifier.
|
||||
* @returns The next content, or null if no redo history.
|
||||
*/
|
||||
redoCssHistory(id: string): string | null {
|
||||
const history = this.cssHistory[id];
|
||||
if (history && history.redoStack.length > 0) {
|
||||
const nextState = history.redoStack.pop()!;
|
||||
history.undoStack.push(nextState);
|
||||
return nextState;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue