mirror of
https://github.com/matheuszarkov/gradient-text.git
synced 2026-07-22 07:44:08 +00:00
618 lines
22 KiB
JavaScript
618 lines
22 KiB
JavaScript
const { Plugin, PluginSettingTab, Setting, Modal } = require('obsidian');
|
|
|
|
// ─── Color utilities ──────────────────────────────────────────────────────────
|
|
|
|
function hexToRgb(hex) {
|
|
hex = hex.replace(/^#/, '');
|
|
if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
|
|
const n = parseInt(hex, 16);
|
|
return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
|
|
}
|
|
|
|
function rgbToHex(r, g, b) {
|
|
return '#' + [r, g, b]
|
|
.map(v => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, '0'))
|
|
.join('');
|
|
}
|
|
|
|
function rgbToHsv(r, g, b) {
|
|
r /= 255; g /= 255; b /= 255;
|
|
const max = Math.max(r, g, b), min = Math.min(r, g, b), d = max - min;
|
|
let h = 0;
|
|
if (d !== 0) {
|
|
switch (max) {
|
|
case r: h = ((g - b) / d % 6) / 6; break;
|
|
case g: h = ((b - r) / d + 2) / 6; break;
|
|
case b: h = ((r - g) / d + 4) / 6; break;
|
|
}
|
|
if (h < 0) h += 1;
|
|
}
|
|
return { h: h * 360, s: max === 0 ? 0 : (d / max) * 100, v: max * 100 };
|
|
}
|
|
|
|
function hsvToRgb(h, s, v) {
|
|
h /= 360; s /= 100; v /= 100;
|
|
const i = Math.floor(h * 6);
|
|
const f = h * 6 - i;
|
|
const p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s);
|
|
let r, g, b;
|
|
switch (i % 6) {
|
|
case 0: r = v; g = t; b = p; break;
|
|
case 1: r = q; g = v; b = p; break;
|
|
case 2: r = p; g = v; b = t; break;
|
|
case 3: r = p; g = q; b = v; break;
|
|
case 4: r = t; g = p; b = v; break;
|
|
case 5: r = v; g = p; b = q; break;
|
|
}
|
|
return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) };
|
|
}
|
|
|
|
function rgbToHsl(r, g, b) {
|
|
r /= 255; g /= 255; b /= 255;
|
|
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
const l = (max + min) / 2;
|
|
if (max === min) return { h: 0, s: 0, l: Math.round(l * 100) };
|
|
const d = max - min;
|
|
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
let h;
|
|
switch (max) {
|
|
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
|
|
case g: h = ((b - r) / d + 2) / 6; break;
|
|
case b: h = ((r - g) / d + 4) / 6; break;
|
|
}
|
|
return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) };
|
|
}
|
|
|
|
function hslToHex(h, s, l) {
|
|
s /= 100; l /= 100;
|
|
const k = n => (n + h / 30) % 12;
|
|
const a = s * Math.min(l, 1 - l);
|
|
const f = n => Math.round((l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)))) * 255);
|
|
return rgbToHex(f(0), f(8), f(4));
|
|
}
|
|
|
|
function buildCustomGradient(colors, angle = 45) {
|
|
const c1 = colors.color1 || '#ffffff';
|
|
const c3 = colors.color3 || '#000000';
|
|
if (colors.color2) {
|
|
return `linear-gradient(${angle}deg, ${c1} 0%, ${colors.color2} 50%, ${c3} 100%)`;
|
|
}
|
|
return `linear-gradient(${angle}deg, ${c1} 0%, ${c3} 100%)`;
|
|
}
|
|
|
|
// ─── Color picker modal ───────────────────────────────────────────────────────
|
|
|
|
class ColorPickerModal extends Modal {
|
|
constructor(app, currentColor, onSave) {
|
|
super(app);
|
|
this.onSave = onSave;
|
|
this.mode = 'HEX';
|
|
const hex = /^#[0-9a-fA-F]{6}$/.test(currentColor) ? currentColor : '#ff0000';
|
|
const { r, g, b } = hexToRgb(hex);
|
|
const hsv = rgbToHsv(r, g, b);
|
|
this.h = hsv.h;
|
|
this.s = hsv.s;
|
|
this.v = hsv.v;
|
|
this._dragging = null;
|
|
this._onMouseMove = null;
|
|
this._onMouseUp = null;
|
|
}
|
|
|
|
getCurrentHex() {
|
|
const { r, g, b } = hsvToRgb(this.h, this.s, this.v);
|
|
return rgbToHex(r, g, b);
|
|
}
|
|
|
|
onOpen() {
|
|
const { contentEl, modalEl } = this;
|
|
contentEl.addClass('gh-color-picker');
|
|
modalEl.addClass('gh-color-picker-modal');
|
|
|
|
this.svCanvas = contentEl.createEl('canvas', { cls: 'gh-sv-canvas' });
|
|
this.svCanvas.width = 260;
|
|
this.svCanvas.height = 175;
|
|
this.svCtx = this.svCanvas.getContext('2d');
|
|
|
|
this.hueCanvas = contentEl.createEl('canvas', { cls: 'gh-hue-canvas' });
|
|
this.hueCanvas.width = 260;
|
|
this.hueCanvas.height = 18;
|
|
this.hueCtx = this.hueCanvas.getContext('2d');
|
|
|
|
const row1 = contentEl.createDiv('gh-cp-row1');
|
|
this.previewEl = row1.createDiv('gh-cp-preview');
|
|
this.inputEl = row1.createEl('input', { cls: 'gh-cp-input', type: 'text' });
|
|
const modeGroup1 = row1.createDiv('gh-cp-mode-group');
|
|
this.modeBtns = {};
|
|
['HEX', 'RGB'].forEach(m => {
|
|
const btn = modeGroup1.createEl('button', { text: m, cls: 'gh-cp-mode-btn' });
|
|
if (m === this.mode) btn.addClass('active');
|
|
btn.addEventListener('click', () => this.switchMode(m));
|
|
this.modeBtns[m] = btn;
|
|
});
|
|
|
|
const row2 = contentEl.createDiv('gh-cp-row2');
|
|
const modeGroup2 = row2.createDiv('gh-cp-mode-group');
|
|
const hslBtn = modeGroup2.createEl('button', { text: 'HSL', cls: 'gh-cp-mode-btn' });
|
|
hslBtn.addEventListener('click', () => this.switchMode('HSL'));
|
|
this.modeBtns['HSL'] = hslBtn;
|
|
|
|
const actions = row2.createDiv('gh-cp-actions');
|
|
const saveBtn = actions.createEl('button', { text: 'Save', cls: 'gh-cp-save mod-cta' });
|
|
saveBtn.addEventListener('click', () => { this.onSave(this.getCurrentHex()); this.close(); });
|
|
const cancelBtn = actions.createEl('button', { text: 'Cancel', cls: 'gh-cp-cancel' });
|
|
cancelBtn.addEventListener('click', () => this.close());
|
|
|
|
this.renderAll();
|
|
this.setupEvents();
|
|
}
|
|
|
|
switchMode(m) {
|
|
this.mode = m;
|
|
Object.values(this.modeBtns).forEach(b => b.removeClass('active'));
|
|
this.modeBtns[m].addClass('active');
|
|
this.updateInput();
|
|
}
|
|
|
|
renderAll() {
|
|
this.drawSV();
|
|
this.drawHue();
|
|
this.updatePreview();
|
|
this.updateInput();
|
|
}
|
|
|
|
drawSV() {
|
|
const ctx = this.svCtx;
|
|
const w = this.svCanvas.width, h = this.svCanvas.height;
|
|
const { r, g, b } = hsvToRgb(this.h, 100, 100);
|
|
|
|
ctx.fillStyle = `rgb(${r},${g},${b})`;
|
|
ctx.fillRect(0, 0, w, h);
|
|
|
|
const wGrad = ctx.createLinearGradient(0, 0, w, 0);
|
|
wGrad.addColorStop(0, 'rgba(255,255,255,1)');
|
|
wGrad.addColorStop(1, 'rgba(255,255,255,0)');
|
|
ctx.fillStyle = wGrad;
|
|
ctx.fillRect(0, 0, w, h);
|
|
|
|
const bGrad = ctx.createLinearGradient(0, 0, 0, h);
|
|
bGrad.addColorStop(0, 'rgba(0,0,0,0)');
|
|
bGrad.addColorStop(1, 'rgba(0,0,0,1)');
|
|
ctx.fillStyle = bGrad;
|
|
ctx.fillRect(0, 0, w, h);
|
|
|
|
const cx = (this.s / 100) * w;
|
|
const cy = (1 - this.v / 100) * h;
|
|
ctx.beginPath();
|
|
ctx.arc(cx, cy, 7, 0, Math.PI * 2);
|
|
ctx.strokeStyle = 'white';
|
|
ctx.lineWidth = 2;
|
|
ctx.stroke();
|
|
ctx.beginPath();
|
|
ctx.arc(cx, cy, 8.5, 0, Math.PI * 2);
|
|
ctx.strokeStyle = 'rgba(0,0,0,0.35)';
|
|
ctx.lineWidth = 1;
|
|
ctx.stroke();
|
|
}
|
|
|
|
drawHue() {
|
|
const ctx = this.hueCtx;
|
|
const w = this.hueCanvas.width, h = this.hueCanvas.height;
|
|
|
|
const grad = ctx.createLinearGradient(0, 0, w, 0);
|
|
for (let i = 0; i <= 12; i++) grad.addColorStop(i / 12, `hsl(${i * 30},100%,50%)`);
|
|
ctx.fillStyle = grad;
|
|
ctx.fillRect(0, 0, w, h);
|
|
|
|
const cx = (this.h / 360) * w;
|
|
const r = h / 2 - 1;
|
|
ctx.beginPath();
|
|
ctx.arc(cx, h / 2, r, 0, Math.PI * 2);
|
|
ctx.strokeStyle = 'white';
|
|
ctx.lineWidth = 2;
|
|
ctx.stroke();
|
|
ctx.beginPath();
|
|
ctx.arc(cx, h / 2, r + 1, 0, Math.PI * 2);
|
|
ctx.strokeStyle = 'rgba(0,0,0,0.35)';
|
|
ctx.lineWidth = 1;
|
|
ctx.stroke();
|
|
}
|
|
|
|
updatePreview() {
|
|
this.previewEl.style.backgroundColor = this.getCurrentHex();
|
|
}
|
|
|
|
updateInput() {
|
|
const hex = this.getCurrentHex();
|
|
if (this.mode === 'HEX') {
|
|
this.inputEl.value = hex.toUpperCase();
|
|
} else if (this.mode === 'RGB') {
|
|
const { r, g, b } = hexToRgb(hex);
|
|
this.inputEl.value = `${r}, ${g}, ${b}`;
|
|
} else {
|
|
const { r, g, b } = hexToRgb(hex);
|
|
const { h, s, l } = rgbToHsl(r, g, b);
|
|
this.inputEl.value = `${h}, ${s}%, ${l}%`;
|
|
}
|
|
}
|
|
|
|
applyInput() {
|
|
const val = this.inputEl.value.trim();
|
|
let hex = null;
|
|
if (this.mode === 'HEX') {
|
|
const h = val.startsWith('#') ? val : '#' + val;
|
|
if (/^#[0-9a-fA-F]{6}$/.test(h)) hex = h;
|
|
} else if (this.mode === 'RGB') {
|
|
const p = val.split(',').map(x => parseInt(x.trim()));
|
|
if (p.length === 3 && p.every(x => !isNaN(x) && x >= 0 && x <= 255)) hex = rgbToHex(...p);
|
|
} else {
|
|
const p = val.split(',').map(x => parseFloat(x.trim().replace('%', '')));
|
|
if (p.length === 3 && !p.some(isNaN)) hex = hslToHex(p[0], p[1], p[2]);
|
|
}
|
|
if (hex) {
|
|
const { r, g, b } = hexToRgb(hex);
|
|
const hsv = rgbToHsv(r, g, b);
|
|
this.h = hsv.h; this.s = hsv.s; this.v = hsv.v;
|
|
this.renderAll();
|
|
}
|
|
}
|
|
|
|
setupEvents() {
|
|
const clamp = (v, mn, mx) => Math.max(mn, Math.min(mx, v));
|
|
|
|
const pickSV = (e) => {
|
|
const rect = this.svCanvas.getBoundingClientRect();
|
|
this.s = clamp((e.clientX - rect.left) / rect.width, 0, 1) * 100;
|
|
this.v = (1 - clamp((e.clientY - rect.top) / rect.height, 0, 1)) * 100;
|
|
this.renderAll();
|
|
};
|
|
|
|
const pickHue = (e) => {
|
|
const rect = this.hueCanvas.getBoundingClientRect();
|
|
this.h = clamp((e.clientX - rect.left) / rect.width, 0, 1) * 360;
|
|
this.renderAll();
|
|
};
|
|
|
|
this.svCanvas.addEventListener('mousedown', (e) => { this._dragging = 'sv'; pickSV(e); });
|
|
this.hueCanvas.addEventListener('mousedown', (e) => { this._dragging = 'hue'; pickHue(e); });
|
|
|
|
this._onMouseMove = (e) => {
|
|
if (this._dragging === 'sv') pickSV(e);
|
|
else if (this._dragging === 'hue') pickHue(e);
|
|
};
|
|
this._onMouseUp = () => { this._dragging = null; };
|
|
document.addEventListener('mousemove', this._onMouseMove);
|
|
document.addEventListener('mouseup', this._onMouseUp);
|
|
|
|
this.inputEl.addEventListener('keydown', (e) => { if (e.key === 'Enter') this.applyInput(); });
|
|
this.inputEl.addEventListener('blur', () => this.applyInput());
|
|
}
|
|
|
|
onClose() {
|
|
if (this._onMouseMove) document.removeEventListener('mousemove', this._onMouseMove);
|
|
if (this._onMouseUp) document.removeEventListener('mouseup', this._onMouseUp);
|
|
this.contentEl.empty();
|
|
}
|
|
}
|
|
|
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
|
|
const PRESET_OPTIONS = [
|
|
{ value: 'green-snake', label: 'Green Snake' },
|
|
{ value: 'midnight-green', label: 'Midnight Green' },
|
|
{ value: 'inferno', label: 'Inferno' },
|
|
{ value: 'underwater', label: 'Underwater' },
|
|
{ value: 'mystic-purple', label: 'Mystic Purple' },
|
|
{ value: 'custom', label: 'Custom' },
|
|
{ value: 'none', label: 'None' }
|
|
];
|
|
|
|
// Groups that are always active (have a preset selected by default)
|
|
const ALWAYS_ON_GROUPS = ['heading', 'bold', 'italic', 'wikilink', 'external'];
|
|
|
|
// Groups that are opt-in (default 'off')
|
|
const OPT_IN_GROUPS = ['normal'];
|
|
|
|
const ALL_GROUPS = [...ALWAYS_ON_GROUPS, ...OPT_IN_GROUPS];
|
|
|
|
const DEFAULT_CUSTOM = { color1: '#85ff95', color2: null, color3: '#bcff7d' };
|
|
|
|
const DEFAULT_SETTINGS = {
|
|
disableAllGradients: false,
|
|
headingGradient: 'green-snake',
|
|
headingCustomColors: { ...DEFAULT_CUSTOM },
|
|
boldGradient: 'green-snake',
|
|
boldCustomColors: { ...DEFAULT_CUSTOM },
|
|
italicGradient: 'green-snake',
|
|
italicCustomColors: { ...DEFAULT_CUSTOM },
|
|
wikilinkGradient: 'green-snake',
|
|
wikilinkCustomColors: { ...DEFAULT_CUSTOM },
|
|
externalGradient: 'green-snake',
|
|
externalCustomColors: { ...DEFAULT_CUSTOM },
|
|
normalGradient: 'off',
|
|
normalCustomColors: { ...DEFAULT_CUSTOM }
|
|
};
|
|
|
|
// ─── Settings tab ─────────────────────────────────────────────────────────────
|
|
|
|
class GradientTextSettingTab extends PluginSettingTab {
|
|
constructor(app, plugin) {
|
|
super(app, plugin);
|
|
this.plugin = plugin;
|
|
}
|
|
|
|
display() {
|
|
const { containerEl } = this;
|
|
containerEl.empty();
|
|
containerEl.createEl('h2', { text: 'Gradient Text' });
|
|
|
|
// ── Global toggle (top) ────────────────────────────────────────────────
|
|
new Setting(containerEl)
|
|
.setName('Disable all gradients')
|
|
.setDesc('Turn off all gradient effects and use the theme\'s default text colors.')
|
|
.addToggle(toggle => {
|
|
toggle
|
|
.setValue(this.plugin.settings.disableAllGradients)
|
|
.onChange(async value => {
|
|
this.plugin.settings.disableAllGradients = value;
|
|
await this.plugin.saveSettings();
|
|
});
|
|
});
|
|
|
|
// ── Per-element gradient settings ──────────────────────────────────────
|
|
this.addGradientSetting('Heading gradient', 'headingGradient', 'heading', 'Heading');
|
|
this.addGradientSetting('Normal text gradient', 'normalGradient', 'normal', 'Normal', true);
|
|
this.addGradientSetting('Bold gradient', 'boldGradient', 'bold', 'Bold');
|
|
this.addGradientSetting('Italic gradient', 'italicGradient', 'italic', 'Italic');
|
|
this.addGradientSetting('Wikilink gradient', 'wikilinkGradient', 'wikilink', 'Wikilink');
|
|
this.addGradientSetting('External link gradient', 'externalGradient', 'external', 'External');
|
|
}
|
|
|
|
/**
|
|
* @param {string} name Display name for the setting row
|
|
* @param {string} key Settings key (e.g. 'normalGradient')
|
|
* @param {string} group Group name (e.g. 'normal')
|
|
* @param {string} previewLabel Short preview text shown right of the dropdown
|
|
* @param {boolean} includeOff Whether to prepend an 'Off' option (opt-in groups)
|
|
*/
|
|
addGradientSetting(name, key, group, previewLabel, includeOff = false) {
|
|
const customKey = group + 'CustomColors';
|
|
const setting = new Setting(this.containerEl).setName(name);
|
|
|
|
// ── Color dots (left of dropdown, visible only in 'custom' mode) ───────
|
|
const dotsEl = setting.controlEl.createDiv('gh-color-dots');
|
|
|
|
const renderDots = () => {
|
|
dotsEl.empty();
|
|
if (this.plugin.settings[key] !== 'custom') {
|
|
dotsEl.style.display = 'none';
|
|
return;
|
|
}
|
|
dotsEl.style.display = 'flex';
|
|
const colors = this.plugin.settings[customKey];
|
|
|
|
['color1', 'color2', 'color3'].forEach(ck => {
|
|
const dot = dotsEl.createDiv('gh-color-dot');
|
|
const color = colors[ck];
|
|
|
|
if (color) {
|
|
dot.style.backgroundColor = color;
|
|
} else {
|
|
dot.addClass('gh-dot-empty');
|
|
}
|
|
|
|
if (ck === 'color2') {
|
|
dot.title = color
|
|
? 'Middle color · right-click to remove'
|
|
: 'Click to add a middle color (optional)';
|
|
}
|
|
|
|
dot.addEventListener('click', () => {
|
|
new ColorPickerModal(this.app, color || '#808080', async newColor => {
|
|
this.plugin.settings[customKey][ck] = newColor;
|
|
await this.plugin.saveSettings();
|
|
renderDots();
|
|
}).open();
|
|
});
|
|
|
|
if (ck === 'color2' && color) {
|
|
dot.addEventListener('contextmenu', async e => {
|
|
e.preventDefault();
|
|
this.plugin.settings[customKey].color2 = null;
|
|
await this.plugin.saveSettings();
|
|
renderDots();
|
|
});
|
|
}
|
|
});
|
|
|
|
const resetBtn = dotsEl.createDiv('gh-reset-btn');
|
|
resetBtn.textContent = '↺';
|
|
resetBtn.title = 'Reset to default colors';
|
|
resetBtn.addEventListener('click', async () => {
|
|
this.plugin.settings[customKey] = { ...DEFAULT_CUSTOM };
|
|
await this.plugin.saveSettings();
|
|
renderDots();
|
|
});
|
|
};
|
|
|
|
renderDots();
|
|
|
|
// ── Dropdown ───────────────────────────────────────────────────────────
|
|
// previewEl is created after, so capture it via late binding
|
|
let previewEl = null;
|
|
|
|
const updatePreviewOff = (value) => {
|
|
if (!previewEl) return;
|
|
if (value === 'off' || value === 'none') {
|
|
previewEl.style.backgroundImage = 'none';
|
|
previewEl.style.webkitTextFillColor = 'var(--text-faint)';
|
|
} else {
|
|
previewEl.style.backgroundImage = '';
|
|
previewEl.style.webkitTextFillColor = '';
|
|
}
|
|
};
|
|
|
|
setting.addDropdown(dd => {
|
|
if (includeOff) dd.addOption('off', 'Off');
|
|
PRESET_OPTIONS.forEach(opt => dd.addOption(opt.value, opt.label));
|
|
dd.setValue(this.plugin.settings[key])
|
|
.onChange(async value => {
|
|
this.plugin.settings[key] = value;
|
|
await this.plugin.saveSettings();
|
|
renderDots();
|
|
updatePreviewOff(value);
|
|
});
|
|
});
|
|
|
|
// ── Preview label (right of dropdown) ──────────────────────────────────
|
|
previewEl = setting.controlEl.createDiv({
|
|
cls: `gh-setting-preview gh-preview-${group}`,
|
|
text: previewLabel
|
|
});
|
|
|
|
updatePreviewOff(this.plugin.settings[key]);
|
|
}
|
|
}
|
|
|
|
// ─── Plugin ───────────────────────────────────────────────────────────────────
|
|
|
|
module.exports = class GradientTextPlugin extends Plugin {
|
|
async onload() {
|
|
await this.loadSettings();
|
|
this.applyAll();
|
|
this.addSettingTab(new GradientTextSettingTab(this.app, this));
|
|
|
|
// ── Command palette entries ────────────────────────────────────────────
|
|
this.addCommand({
|
|
id: 'disable-all-gradients',
|
|
name: 'Disable all gradients',
|
|
callback: async () => {
|
|
this.settings.disableAllGradients = true;
|
|
await this.saveSettings();
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'enable-all-gradients',
|
|
name: 'Enable all gradients',
|
|
callback: async () => {
|
|
this.settings.disableAllGradients = false;
|
|
await this.saveSettings();
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'toggle-all-gradients',
|
|
name: 'Toggle all gradients on / off',
|
|
callback: async () => {
|
|
this.settings.disableAllGradients = !this.settings.disableAllGradients;
|
|
await this.saveSettings();
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'toggle-normal-gradient',
|
|
name: 'Toggle normal text gradient',
|
|
callback: async () => {
|
|
this.settings.normalGradient =
|
|
this.settings.normalGradient === 'off' ? 'green-snake' : 'off';
|
|
await this.saveSettings();
|
|
}
|
|
});
|
|
}
|
|
|
|
onunload() {
|
|
this.clearAll();
|
|
}
|
|
|
|
clearAll() {
|
|
const body = document.body;
|
|
if (!body) return;
|
|
|
|
ALL_GROUPS.forEach(group => {
|
|
PRESET_OPTIONS
|
|
.filter(o => o.value !== 'custom' && o.value !== 'none')
|
|
.forEach(opt => body.classList.remove(`gh-${group}-${opt.value}`));
|
|
|
|
body.classList.remove(`gh-${group}-disabled`);
|
|
|
|
body.style.removeProperty(`--gh-${group}-gradient`);
|
|
body.style.removeProperty(`--gh-${group}-gradient-start`);
|
|
body.style.removeProperty(`--gh-${group}-gradient-end`);
|
|
if (group === 'wikilink' || group === 'external') {
|
|
body.style.removeProperty(`--gh-${group}-underline-color`);
|
|
}
|
|
});
|
|
|
|
body.classList.remove('gh-all-gradient-disabled');
|
|
body.classList.remove('gh-normal-enabled');
|
|
}
|
|
|
|
applyAll() {
|
|
const body = document.body;
|
|
if (!body) return;
|
|
|
|
this.clearAll();
|
|
|
|
if (this.settings.disableAllGradients) {
|
|
body.classList.add('gh-all-gradient-disabled');
|
|
return;
|
|
}
|
|
|
|
// Always-on groups (heading, bold, italic, wikilink, external)
|
|
ALWAYS_ON_GROUPS.forEach(group => {
|
|
const value = this.settings[group + 'Gradient'];
|
|
const colors = this.settings[group + 'CustomColors'];
|
|
|
|
if (value === 'none') {
|
|
body.classList.add(`gh-${group}-disabled`);
|
|
} else if (value === 'custom') {
|
|
body.style.setProperty(`--gh-${group}-gradient`, buildCustomGradient(colors));
|
|
body.style.setProperty(`--gh-${group}-gradient-start`, colors.color1 || '#ffffff');
|
|
body.style.setProperty(`--gh-${group}-gradient-end`, colors.color3 || '#000000');
|
|
if (group === 'wikilink' || group === 'external') {
|
|
body.style.setProperty(`--gh-${group}-underline-color`, colors.color1 || '#ffffff');
|
|
}
|
|
} else {
|
|
body.classList.add(`gh-${group}-${value}`);
|
|
}
|
|
});
|
|
|
|
// Opt-in group: normal text (default 'off' = no gradient)
|
|
const normalValue = this.settings.normalGradient;
|
|
const normalColors = this.settings.normalCustomColors;
|
|
|
|
if (normalValue && normalValue !== 'off' && normalValue !== 'none') {
|
|
body.classList.add('gh-normal-enabled');
|
|
if (normalValue === 'custom') {
|
|
body.style.setProperty('--gh-normal-gradient', buildCustomGradient(normalColors));
|
|
body.style.setProperty('--gh-normal-gradient-start', normalColors.color1 || '#ffffff');
|
|
body.style.setProperty('--gh-normal-gradient-end', normalColors.color3 || '#000000');
|
|
} else {
|
|
body.classList.add(`gh-normal-${normalValue}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
async loadSettings() {
|
|
const saved = await this.loadData() || {};
|
|
|
|
// Migrate old key name from v0.1
|
|
if (saved.disableHeadingGradient !== undefined && saved.disableAllGradients === undefined) {
|
|
saved.disableAllGradients = saved.disableHeadingGradient;
|
|
}
|
|
|
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, saved);
|
|
|
|
ALL_GROUPS.forEach(group => {
|
|
const k = group + 'CustomColors';
|
|
if (!this.settings[k] || typeof this.settings[k] !== 'object') {
|
|
this.settings[k] = { ...DEFAULT_CUSTOM };
|
|
}
|
|
});
|
|
}
|
|
|
|
async saveSettings() {
|
|
await this.saveData(this.settings);
|
|
this.applyAll();
|
|
}
|
|
};
|