refactor: update branding, locales, and switch to Obsidian native debounce

- Update README title and license year to 2026.
- Clean up settings labels by removing redundant "settings" word in multiple locales (en, ar, fa, fr).
- Refactor profile navigation and theme toggle to use `checkCallback` for better command palette integration.
- Replace custom debounce implementation with official Obsidian API across UI components and utils.
- Remove redundant debounce code from utils and background scripts.
This commit is contained in:
YazanAmmar 2026-04-14 23:22:50 +03:00
parent e8d8e40021
commit 786923c1b7
15 changed files with 64 additions and 79 deletions

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 Yazan Ammar
Copyright (c) 2026 Yazan Ammar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -1,4 +1,4 @@
# Obsidian Theme Engine
# Theme Engine for Obsidian
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![GitHub all releases](https://img.shields.io/github/downloads/YazanAmmar/obsidian-theme-engine/total?style=flat&color=brightgreen)](https://github.com/YazanAmmar/obsidian-theme-engine/releases)
@ -571,7 +571,7 @@ A: Yes, absolutely. The plugin fully supports **Arabic** (العَرَبيَّة
MIT — see [LICENSE](https://github.com/YazanAmmar/obsidian-theme-engine/blob/main/LICENSE).
> [!NOTE]
> This license does not grant rights to use the project's logo or visual identity.
> This license does not grant rights to use the project's logo or visual identity see [TRADEMARK.md](./TRADEMARK.md).
---
@ -582,7 +582,7 @@ The official logo (phoenix emblem) and visual identity of this project are prote
- You may use, modify, and distribute the code under the MIT license
- You may NOT use the official logo or visual identity in forks, derivatives, or redistributed versions
> For full trademark and branding rules, see [TRADEMARK.md](./TRADEMARK.md)
> For full trademark and branding rules, see [TRADEMARK.md](./TRADEMARK.md).
---

View file

@ -39,17 +39,24 @@ export function registerCommands(plugin: ThemeEngine) {
plugin.addCommand({
id: PLUGIN_COMMAND_SUFFIXES[1],
name: t('commands.cycleNext'),
callback: async () => {
checkCallback: (checking: boolean) => {
const names = Object.keys(plugin.settings.profiles || {});
if (names.length === 0) {
new Notice(t('notices.noProfilesFound'));
return;
}
if (names.length === 0) return false;
if (checking) return true;
const idx = names.indexOf(plugin.settings.activeProfile);
const next = names[(idx + 1) % names.length];
const next = names[((idx >= 0 ? idx : -1) + 1) % names.length];
plugin.settings.activeProfile = next;
await plugin.saveSettings();
new Notice(t('notices.activeProfileSwitched', next));
void plugin
.saveSettings()
.then(() => {
new Notice(t('notices.activeProfileSwitched', next));
})
.catch((error) => {
console.error('Failed to switch to the next profile.', error);
});
return true;
},
});
@ -57,19 +64,27 @@ export function registerCommands(plugin: ThemeEngine) {
plugin.addCommand({
id: PLUGIN_COMMAND_SUFFIXES[2],
name: t('commands.cyclePrevious'),
callback: async () => {
checkCallback: (checking: boolean) => {
const names = Object.keys(plugin.settings.profiles || {});
if (names.length === 0) {
new Notice(t('notices.noProfilesFound'));
return;
}
if (names.length === 0) return false;
if (checking) return true;
const currentIndex = names.indexOf(plugin.settings.activeProfile);
const previousIndex = (currentIndex - 1 + names.length) % names.length;
const previousIndex =
currentIndex >= 0 ? (currentIndex - 1 + names.length) % names.length : names.length - 1;
const previousProfile = names[previousIndex];
plugin.settings.activeProfile = previousProfile;
await plugin.saveSettings();
new Notice(t('notices.activeProfileSwitched', previousProfile));
void plugin
.saveSettings()
.then(() => {
new Notice(t('notices.activeProfileSwitched', previousProfile));
})
.catch((error) => {
console.error('Failed to switch to the previous profile.', error);
});
return true;
},
});
@ -88,14 +103,13 @@ export function registerCommands(plugin: ThemeEngine) {
plugin.addCommand({
id: PLUGIN_COMMAND_SUFFIXES[4],
name: t('commands.toggleTheme'),
callback: async () => {
checkCallback: (checking: boolean) => {
const activeProfileName = plugin.settings.activeProfile;
const activeProfile = plugin.settings.profiles[activeProfileName];
if (!activeProfile) {
new Notice(t('notices.profileNotFound'));
return;
}
if (!activeProfile) return false;
if (checking) return true;
const currentTheme = activeProfile.themeType || 'auto';
let nextTheme: 'auto' | 'dark' | 'light';
@ -113,8 +127,15 @@ export function registerCommands(plugin: ThemeEngine) {
}
activeProfile.themeType = nextTheme;
await plugin.saveSettings();
new Notice(noticeMessage);
void plugin
.saveSettings()
.then(() => {
new Notice(noticeMessage);
})
.catch((error) => {
console.error('Failed to toggle the active profile theme.', error);
});
return true;
},
});
}

View file

@ -61,7 +61,7 @@ export default {
tooltipCopyJson: 'نسخ JSON للملف الشخصي الحالي إلى الحافظة',
},
options: {
heading: 'إعدادات متقدمة',
heading: 'خيارات متقدمة',
liveUpdateName: 'معدل التحديث المباشر (إطار بالثانية)',
liveUpdateDesc:
'يحدد عدد مرات تحديث معاينة الألوان في الثانية أثناء السحب (0 = تعطيل المعاينة). القيم المنخفضة تحسن الأداء.',
@ -82,7 +82,7 @@ export default {
resetPluginButton: 'إعادة تعيين كل البيانات...',
backgroundName: 'تعيين خلفية مخصصة',
backgroundDesc: 'إدارة صورة أو فيديو الخلفية لهذا الملف الشخصي.',
backgroundModalSettingsTitle: 'إعدادات الخلفية',
backgroundModalSettingsTitle: 'خيارات الخلفية',
backgroundEnableName: 'تفعيل الخلفية',
backgroundEnableDesc: 'التحكم بظهور أو إخفاء الخلفية المخصصة لهذا الملف الشخصي.',
convertImagesName: 'تحويل الصور إلى JPG',

View file

@ -62,7 +62,7 @@ export default {
tooltipCopyJson: 'Copy current profile JSON to clipboard',
},
options: {
heading: 'Advanced settings',
heading: 'Advanced options',
liveUpdateName: 'Live update fps',
liveUpdateDesc:
'Sets how many times per second the UI previews color changes while dragging (0 = disable live preview). Lower values can improve performance.',
@ -84,7 +84,7 @@ export default {
backgroundName: 'Set custom background',
backgroundDesc: 'Manage the background image or video for this profile.',
backgroundModalSettingsTitle: 'Background settings',
backgroundModalSettingsTitle: 'Background options',
backgroundEnableName: 'Enable background',
backgroundEnableDesc: 'Toggle the visibility of the custom background for this profile.',
convertImagesName: 'Convert images to JPG',

View file

@ -62,7 +62,7 @@
tooltipCopyJson: 'کپی کردن JSON نمایه فعلی در کلیپ‌بورد',
},
options: {
heading: 'تنظیمات پیشرفته',
heading: 'گزینه‌های پیشرفته',
liveUpdateName: 'فریم در ثانیه به‌روزرسانی زنده',
liveUpdateDesc:
'تعداد دفعاتی که رابط کاربری در هر ثانیه پیش‌نمایش تغییرات رنگ را هنگام کشیدن نشان می‌دهد تنظیم می‌کند (0 = غیرفعال کردن پیش‌نمایش زنده). مقادیر کمتر می‌تواند عملکرد را بهبود بخشد.',
@ -83,7 +83,7 @@
resetPluginButton: 'بازنشانی تمام داده‌ها...',
backgroundName: 'تنظیم پس‌زمینه سفارشی',
backgroundDesc: 'مدیریت تصویر یا ویدئو پس‌زمینه برای این پروفایل.',
backgroundModalSettingsTitle: 'تنظیمات پس‌زمینه',
backgroundModalSettingsTitle: 'گزینه‌های پس‌زمینه',
backgroundEnableName: 'فعال کردن پس‌زمینه',
backgroundEnableDesc: 'نمایش یا پنهان کردن پس‌زمینه سفارشی برای این پروفایل را تغییر دهید.',
convertImagesName: 'تبدیل تصاویر به JPG',

View file

@ -62,7 +62,7 @@
tooltipCopyJson: 'Copier le JSON du profil actuel dans le presse-papiers',
},
options: {
heading: 'Paramètres avancés',
heading: 'Options avancées',
liveUpdateName: 'FPS de la mise à jour en direct',
liveUpdateDesc:
"Définit le nombre de fois par seconde où l'interface prévisualise les changements de couleur lors du glissement (0 = désactiver l'aperçu en direct). Des valeurs plus basses peuvent améliorer les performances.",
@ -83,7 +83,7 @@
resetPluginButton: 'Réinitialiser toutes les données...',
backgroundName: 'Définir un arrière-plan personnalisé',
backgroundDesc: "Gérer l'image ou la vidéo d'arrière-plan pour ce profil.",
backgroundModalSettingsTitle: "Paramètres d'arrière-plan",
backgroundModalSettingsTitle: "Options d'arrière-plan",
backgroundEnableName: "Activer l'arrière-plan",
backgroundEnableDesc:
"Activer/désactiver la visibilité de l'arrière-plan personnalisé pour ce profil.",

View file

@ -1,8 +1,7 @@
import { App, Notice, Setting, TextComponent } from 'obsidian';
import { App, Notice, Setting, TextComponent, debounce } from 'obsidian';
import { DEFAULT_PROFILE } from '../../../constants';
import { t } from '../../../i18n/strings';
import type ThemeEngine from '../../../main';
import { debounce } from '../../../utils';
import type { ThemeEngineSettingTab } from '../../settingsTab';
import { ThemeEngineBaseModal } from '../base';

View file

@ -1,8 +1,7 @@
import { App, Notice, Setting, TextComponent, TextAreaComponent } from 'obsidian';
import { App, Notice, Setting, TextComponent, TextAreaComponent, debounce } from 'obsidian';
import { t } from '../../../i18n/strings';
import type ThemeEngine from '../../../main';
import type { Snippet } from '../../../types';
import { debounce } from '../../../utils';
import type { ThemeEngineSettingTab } from '../../settingsTab';
import { ThemeEngineBaseModal } from '../base';
@ -25,14 +24,6 @@ export class SnippetCssModal extends ThemeEngineBaseModal {
isGlobalSnippet: boolean;
isSaving: boolean = false;
_debounce(func: (...args: unknown[]) => void, delay: number) {
let timeout: number;
return (...args: unknown[]) => {
clearTimeout(timeout);
timeout = window.setTimeout(() => func.apply(this, args), delay);
};
}
constructor(
app: App,
plugin: ThemeEngine,

View file

@ -1,8 +1,7 @@
import { App, Notice, Setting, TextComponent } from 'obsidian';
import { App, Notice, Setting, TextComponent, debounce } from 'obsidian';
import { t } from '../../../i18n/strings';
import type ThemeEngine from '../../../main';
import type { Profile, Snippet } from '../../../types';
import { debounce } from '../../../utils';
import type { ThemeEngineSettingTab } from '../../settingsTab';
import { ThemeEngineBaseModal } from '../base';

View file

@ -1,7 +1,6 @@
import { App, ButtonComponent, Notice, requestUrl, Setting } from 'obsidian';
import { App, ButtonComponent, Notice, debounce, requestUrl, Setting } from 'obsidian';
import { t } from '../../../i18n/strings';
import type ThemeEngine from '../../../main';
import { debounce } from '../../../utils';
import type { ThemeEngineSettingTab } from '../../settingsTab';
import { ThemeEngineBaseModal } from '../base';
@ -168,20 +167,6 @@ export class BackgroundImageSettingsModal extends ThemeEngineBaseModal {
});
}, 0);
debounce(func: (...args: unknown[]) => void, wait: number) {
let timeout: ReturnType<typeof setTimeout> | null;
return (...args: unknown[]) => {
const later = () => {
timeout = null;
func(...args);
};
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(later, wait);
};
}
onClose() {
const { contentEl } = this;
contentEl.empty();

View file

@ -2,6 +2,7 @@ import {
App,
Notice,
SearchComponent,
debounce,
setIcon,
Setting,
TextAreaComponent,
@ -17,7 +18,7 @@ import {
import { CORE_LANGUAGES, type LocaleCode, type LocalizedValue } from '../../../i18n/types';
import type ThemeEngine from '../../../main';
import type { CustomTranslation } from '../../../types';
import { debounce, unflattenStrings } from '../../../utils';
import { unflattenStrings } from '../../../utils';
import type { ThemeEngineSettingTab } from '../../settingsTab';
import { ThemeEngineBaseModal } from '../base';

View file

@ -1,4 +1,4 @@
import { ButtonComponent, Notice, Setting, SettingGroup, setIcon } from 'obsidian';
import { ButtonComponent, Notice, Setting, SettingGroup, debounce, setIcon } from 'obsidian';
import { CORE_LANGUAGES, LocaleCode } from '../../i18n/types';
import { loadLanguage, t } from '../../i18n/strings';
import type { ThemeEngineSettingTab } from '../settingsTab';
@ -15,7 +15,6 @@ import {
LanguageSettingsModal,
LanguageTranslatorModal,
} from '../modals';
import { debounce } from '../../utils';
export const renderSettingsTab = async (tab: ThemeEngineSettingTab): Promise<void> => {
const themeDefaults = await tab.plugin.getThemeDefaults();

View file

@ -1,8 +1,7 @@
import { DropdownComponent, SearchComponent, setIcon } from 'obsidian';
import { DropdownComponent, SearchComponent, debounce, setIcon } from 'obsidian';
import { DEFAULT_VARS } from '../../constants';
import { t } from '../../i18n/strings';
import type { ThemeEngineSettingTab } from '../settingsTab';
import { debounce } from '../../utils';
export type SearchState = {
query: string;

View file

@ -71,15 +71,6 @@ export function isIconizeEnabled(app: App): boolean {
return isPluginEnabled(app, iconizeIDs);
}
// Standard debounce to limit function execution rate
export function debounce(fn: (...args: unknown[]) => void, ms = 200) {
let t: ReturnType<typeof setTimeout> | null = null;
return (...args: unknown[]) => {
if (t) clearTimeout(t);
t = setTimeout(() => fn.apply(this, args), ms);
};
}
// Auto-increments filename if path exists (e.g., file-2.png)
export async function findNextAvailablePath(adapter: DataAdapter, path: string): Promise<string> {
if (!(await adapter.exists(path))) {