diff --git a/LICENSE b/LICENSE index 0d48c96..b6597ac 100644 --- a/LICENSE +++ b/LICENSE @@ -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 diff --git a/README.md b/README.md index 3ec786a..e80a903 100644 --- a/README.md +++ b/README.md @@ -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). --- diff --git a/src/commands/index.ts b/src/commands/index.ts index 8f335a8..49cd1fe 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -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; }, }); } diff --git a/src/i18n/locales/ar.ts b/src/i18n/locales/ar.ts index c7bdc6f..be2d107 100644 --- a/src/i18n/locales/ar.ts +++ b/src/i18n/locales/ar.ts @@ -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', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 2c7a468..dcec1cc 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -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', diff --git a/src/i18n/locales/fa.ts b/src/i18n/locales/fa.ts index 51ff140..a58008d 100644 --- a/src/i18n/locales/fa.ts +++ b/src/i18n/locales/fa.ts @@ -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', diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 3b57a6a..0e4cb09 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -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.", diff --git a/src/ui/modals/editors/paste-css.ts b/src/ui/modals/editors/paste-css.ts index 245f42f..23eef3a 100644 --- a/src/ui/modals/editors/paste-css.ts +++ b/src/ui/modals/editors/paste-css.ts @@ -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'; diff --git a/src/ui/modals/editors/snippet-css.ts b/src/ui/modals/editors/snippet-css.ts index 042ada8..2f55658 100644 --- a/src/ui/modals/editors/snippet-css.ts +++ b/src/ui/modals/editors/snippet-css.ts @@ -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, diff --git a/src/ui/modals/profiles/import-json.ts b/src/ui/modals/profiles/import-json.ts index c0052cd..1126ae5 100644 --- a/src/ui/modals/profiles/import-json.ts +++ b/src/ui/modals/profiles/import-json.ts @@ -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'; diff --git a/src/ui/modals/settings/background.ts b/src/ui/modals/settings/background.ts index 4ffb030..fe534cf 100644 --- a/src/ui/modals/settings/background.ts +++ b/src/ui/modals/settings/background.ts @@ -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 | null; - return (...args: unknown[]) => { - const later = () => { - timeout = null; - func(...args); - }; - if (timeout) { - clearTimeout(timeout); - } - timeout = setTimeout(later, wait); - }; - } - onClose() { const { contentEl } = this; contentEl.empty(); diff --git a/src/ui/modals/settings/translator.ts b/src/ui/modals/settings/translator.ts index 6f14d31..784ec16 100644 --- a/src/ui/modals/settings/translator.ts +++ b/src/ui/modals/settings/translator.ts @@ -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'; diff --git a/src/ui/settings/render.ts b/src/ui/settings/render.ts index ba65a42..dbf8552 100644 --- a/src/ui/settings/render.ts +++ b/src/ui/settings/render.ts @@ -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 => { const themeDefaults = await tab.plugin.getThemeDefaults(); diff --git a/src/ui/settings/search.ts b/src/ui/settings/search.ts index ed0e1a2..fd3a3d3 100644 --- a/src/ui/settings/search.ts +++ b/src/ui/settings/search.ts @@ -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; diff --git a/src/utils.ts b/src/utils.ts index 4264f59..17cc6ec 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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 | 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 { if (!(await adapter.exists(path))) {