diff --git a/README.md b/README.md index e8ab5bc..ca8f309 100644 --- a/README.md +++ b/README.md @@ -86,4 +86,4 @@ Sort of, just expect a few visual bugs! They currently do some fighting over con ## License -This plugin is released under an [MIT No Attribution](https://choosealicense.com/licenses/mit-0/) license, which means you're free to modify and share its source code without needing to credit the author (me). It also protects the code author from liability for damages, so I recommend using a similar license if you republish this code. +This plugin is released under an [MIT No Attribution](https://choosealicense.com/licenses/mit-0/) license, which means you're free to modify and share its source code without crediting the authors of this repository. It also protects those authors from liability for damages, so I recommend using a similar license if you republish this code. diff --git a/i18n/ar.json b/i18n/ar.json index f2d34e1..f110fa6 100644 --- a/i18n/ar.json +++ b/i18n/ar.json @@ -280,6 +280,7 @@ "toggleAllFolderIcons": "تبديل جميع أيقونات المجلد", "toggleMinimalFolderIcons": "تبديل أيقونات المجلد البسيطة", "toggleMarkdownTabIcons": "تبديل أيقونات علامات تبويب Markdown", + "toggleTagPillIcons": "تبديل أيقونات حبوب الوسوم", "toggleMenuActions": "تبديل إجراءات القائمة", "toggleQuickSwitcherIcons": "تبديل أيقونات الانتقال السريع", "toggleBiggerSearchResults": "تبديل نتائج البحث الأكبر", @@ -299,7 +300,6 @@ "desc": "إعداد قواعد تلقائية لأيقونات الملفات والمجلدات.", "manage": "إدارة" }, - "headingSidebarAndTabIcons": "أيقونات الشريط الجانبي وعلامات التبويب", "biggerIcons": { "name": "أيقونات أكبر", "desc": "إظهار أيقونات أكبر من واجهة المستخدم الافتراضية." @@ -310,6 +310,7 @@ "descDesktop": "انقر فوق أيقونة لفتح أداة اختيار الأيقونات.", "descMobile": "انقر فوق أيقونة لفتح أداة اختيار الأيقونات." }, + "headingSidebarsAndTabs": "الأشرطة الجانبية وعلامات التبويب", "showAllFileIcons": { "name": "إظهار جميع أيقونات الملفات", "desc": "إظهار أيقونات الملفات التي لا تحتوي على أيقونة مخصصة." @@ -326,11 +327,24 @@ "name": "إظهار أيقونات علامات تبويب Markdown", "desc": "إظهار أيقونات علامات تبويب ملفات Markdown." }, + "headingEditor": "المحرر", + "showTitleIcons": { + "name": "إظهار أيقونات العنوان", + "desc": "إظهار الأيقونات في عناوين الملاحظات إذا تم تمكين العناوين." + }, + "showTagPillIcons": { + "name": "إظهار أيقونات حبوب الوسوم", + "desc": "إظهار الأيقونات في #الوسوم وفي خاصية tags." + }, "headingMenusAndDialogs": "القوائم ومربعات الحوار", "showMenuActions": { "name": "إظهار إجراءات القائمة", "desc": "إظهار الإجراءات المتعلقة بالأيقونات في قوائم السياق." }, + "showSuggestionIcons": { + "name": "إظهار أيقونات الاقتراحات", + "desc": "إظهار الأيقونات في النوافذ المنبثقة للاقتراحات." + }, "showQuickSwitcherIcons": { "name": "الانتقال السريع إظهار أيقونات", "desc": "الانتقال السريع إظهار الأيقونات في نتائج بحث." diff --git a/i18n/de.json b/i18n/de.json index ca90f10..f5b40a4 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -280,6 +280,7 @@ "toggleAllFolderIcons": "Alle Ordnersymbole umschalten", "toggleMinimalFolderIcons": "Minimale Ordnersymbole umschalten", "toggleMarkdownTabIcons": "Markdown-Tabsymbole umschalten", + "toggleTagPillIcons": "Tag-Pillensymbole umschalten", "toggleMenuActions": "Menüaktionen umschalten", "toggleQuickSwitcherIcons": "Schnellauswahlsymbole umschalten", "toggleBiggerSearchResults": "Größere Suchergebnisse umschalten", @@ -299,7 +300,6 @@ "desc": "Automatisierte Regeln für Datei- und Ordnersymbole einrichten.", "manage": "Verwalten" }, - "headingSidebarAndTabIcons": "Seitenleisten- und Registerkartensymbole", "biggerIcons": { "name": "Größere Symbole", "desc": "Größere Symbole als die Standard-Benutzeroberfläche anzeigen." @@ -310,6 +310,7 @@ "descDesktop": "Klicken Sie auf ein Symbol, um die Symbolauswahl zu öffnen.", "descMobile": "Tippen Sie auf ein Symbol, um die Symbolauswahl zu öffnen." }, + "headingSidebarsAndTabs": "Seitenleisten und Registerkarten", "showAllFileIcons": { "name": "Alle Dateisymbole anzeigen", "desc": "Symbole für Dateien anzeigen, die kein benutzerdefiniertes Symbol haben." @@ -326,11 +327,24 @@ "name": "Markdown-Tabsymbole anzeigen", "desc": "Tabsymbole für Markdown-Dateien anzeigen." }, + "headingEditor": "Editor", + "showTitleIcons": { + "name": "Titelsymbole anzeigen", + "desc": "Symbole in Notiztiteln anzeigen, wenn Titel in Notiz aktiviert sind." + }, + "showTagPillIcons": { + "name": "Tag-Pillensymbole anzeigen", + "desc": "Symbole in #Hashtags und der Tags-Property anzeigen." + }, "headingMenusAndDialogs": "Menüs & Dialoge", "showMenuActions": { "name": "Menüaktionen anzeigen", "desc": "Symbolbezogene Aktionen in Kontextmenüs anzeigen." }, + "showSuggestionIcons": { + "name": "Vorschlagsymbole anzeigen", + "desc": "Symbole in Vorschlags-Pop-ups anzeigen." + }, "showQuickSwitcherIcons": { "name": "Schnellauswahlsymbole anzeigen", "desc": "Symbole in den Suchergebnissen von Schnellauswahl anzeigen." diff --git a/i18n/es.json b/i18n/es.json index b6b7bfe..d4daff3 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -280,6 +280,7 @@ "toggleAllFolderIcons": "Alternar todos los íconos de carpetas", "toggleMinimalFolderIcons": "Alternar íconos de carpeta mínimos", "toggleMarkdownTabIcons": "Alternar íconos de pestañas de Markdown", + "toggleTagPillIcons": "Alternar íconos de píldoras de etiquetas", "toggleMenuActions": "Alternar acciones del menú", "toggleQuickSwitcherIcons": "Alternar íconos del selector rápido", "toggleBiggerSearchResults": "Alternar resultados de búsqueda más grandes", @@ -299,7 +300,6 @@ "desc": "Configurar reglas automatizadas para los íconos de archivos y carpetas", "manage": "Administrar" }, - "headingSidebarAndTabIcons": "Íconos de la barra lateral y de las pestañas", "biggerIcons": { "name": "Íconos más grandes", "desc": "Mostrar íconos más grandes que la interfaz de usuario predeterminada." @@ -310,6 +310,7 @@ "descDesktop": "Haga clic en un ícono para abrir el selector de íconos.", "descMobile": "Toque un ícono para abrir el selector de íconos." }, + "headingSidebarsAndTabs": "Barras laterales y pestañas", "showAllFileIcons": { "name": "Mostrar todos los íconos de archivos", "desc": "Mostrar íconos para archivos sin un ícono personalizado." @@ -326,11 +327,24 @@ "name": "Mostrar íconos de pestañas de Markdown", "desc": "Mostrar íconos de pestañas para archivos Markdown." }, + "headingEditor": "Editor", + "showTitleIcons": { + "name": "Mostrar íconos de título", + "desc": "Mostrar íconos en títulos de notas, si los títulos en una línea están activados." + }, + "showTagPillIcons": { + "name": "Mostrar íconos de píldoras de etiquetas", + "desc": "Mostrar íconos en #etiquetas y en la propiedad tags." + }, "headingMenusAndDialogs": "Menús y diálogos", "showMenuActions": { "name": "Mostrar acciones de menú", "desc": "Mostrar acciones relacionadas con íconos en los menús contextuales." }, + "showSuggestionIcons": { + "name": "Mostrar íconos de sugerencias", + "desc": "Mostrar íconos en ventanas emergentes de sugerencias." + }, "showQuickSwitcherIcons": { "name": "Mostrar íconos del selector rápido", "desc": "Mostrar íconos en los resultados de búsqueda de los selectores rápidos." diff --git a/i18n/fr.json b/i18n/fr.json index 8df196b..d612c5a 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -299,7 +299,6 @@ "desc": "Configurer des règles automatisées pour les icônes de fichiers et de dossiers.", "manage": "Gérer" }, - "headingSidebarAndTabIcons": "Icônes de la barre latérale et des onglets", "biggerIcons": { "name": "Icônes plus grandes", "desc": "Afficher des icônes plus grandes que l'interface utilisateur par défaut." @@ -310,6 +309,7 @@ "descDesktop": "Cliquez sur une icône pour ouvrir le sélecteur d'icônes.", "descMobile": "Appuyez sur une icône pour ouvrir le sélecteur d'icônes." }, + "headingSidebarsAndTabs": "Icônes de la barre latérale et des onglets", "showAllFileIcons": { "name": "Afficher toutes les icônes de fichiers", "desc": "Afficher les icônes des fichiers qui n'ont pas d'icône personnalisée." diff --git a/i18n/id.json b/i18n/id.json index 3253201..eab2272 100644 --- a/i18n/id.json +++ b/i18n/id.json @@ -280,6 +280,7 @@ "toggleAllFolderIcons": "Beralih semua ikon folder", "toggleMinimalFolderIcons": "Beralih ikon folder minimal", "toggleMarkdownTabIcons": "Beralih ikon tab Markdown", + "toggleTagPillIcons": "Beralih ikon pil tag", "toggleMenuActions": "Beralih tindakan menu", "toggleQuickSwitcherIcons": "Beralih ikon peralih cepat", "toggleBiggerSearchResults": "Beralih hasil pencarian yang lebih besar", @@ -299,7 +300,6 @@ "desc": "Siapkan aturan otomatis untuk ikon berkas dan folder.", "manage": "Kelola" }, - "headingSidebarAndTabIcons": "Ikon bilah sisi & tab", "biggerIcons": { "name": "Ikon yang lebih besar", "desc": "Tampilkan ikon yang lebih besar daripada UI default." @@ -310,6 +310,7 @@ "descDesktop": "Klik ikon untuk membuka pemilih ikon.", "descMobile": "Ketuk ikon untuk membuka pemilih ikon." }, + "headingSidebarsAndTabs": "Bilah sisi dan tab", "showAllFileIcons": { "name": "Tampilkan semua ikon berkas", "desc": "Tampilkan ikon untuk berkas yang tidak memiliki ikon khusus." @@ -326,11 +327,24 @@ "name": "Tampilkan ikon tab Markdown", "desc": "Tampilkan ikon tab untuk file Markdown." }, + "headingEditor": "Editor", + "showTitleIcons": { + "name": "Tampilkan ikon judul", + "desc": "Tampilkan ikon di judul catatan jika judul sebaris diaktifkan." + }, + "showTagPillIcons": { + "name": "Tampilkan ikon pil tag", + "desc": "Tampilkan ikon di #hashtags dan properti tags." + }, "headingMenusAndDialogs": "Menu & dialog", "showMenuActions": { "name": "Tampilkan tindakan menu", "desc": "Tampilkan tindakan terkait ikon dalam menu konteks." }, + "showSuggestionIcons": { + "name": "Tampilkan ikon saran", + "desc": "Tampilkan ikon di jendela pop-up saran." + }, "showQuickSwitcherIcons": { "name": "Tampilkan ikon peralih cepat", "desc": "Tampilkan ikon dalam hasil pencarian peralih cepat." diff --git a/i18n/ja.json b/i18n/ja.json index fd0f3ba..86d714d 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -280,6 +280,7 @@ "toggleAllFolderIcons": "すべてのフォルダアイコンを切り替える", "toggleMinimalFolderIcons": "最小限のフォルダアイコンを切り替える", "toggleMarkdownTabIcons": "マークダウンタブアイコンを切り替える", + "toggleTagPillIcons": "タグピルアイコンを切り替える", "toggleMenuActions": "メニューアクションの切り替え", "toggleQuickSwitcherIcons": "クイックスイッチャーアイコンの切り替え", "toggleBiggerSearchResults": "検索結果を大きく切り替えます", @@ -299,7 +300,6 @@ "desc": "ファイルとフォルダのアイコンの自動ルールを設定します。", "manage": "管理" }, - "headingSidebarAndTabIcons": "サイドバーとタブのアイコン", "biggerIcons": { "name": "大きいアイコン", "desc": "デフォルトの UI よりも大きなアイコンを表示します。" @@ -310,6 +310,7 @@ "descDesktop": "アイコンをクリックするとアイコンピッカーが開きます。", "descMobile": "アイコンをタップするとアイコンピッカーが開きます。" }, + "headingSidebarsAndTabs": "サイドバーとタブ", "showAllFileIcons": { "name": "すべてのファイルアイコンを表示する", "desc": "カスタムアイコンのないファイルのアイコンを表示します。" @@ -326,11 +327,24 @@ "name": "マークダウンタブアイコンを表示", "desc": "マークダウンファイルのタブアイコンを表示します。" }, + "headingEditor": "エディタ", + "showTitleIcons": { + "name": "タイトルアイコンを表示する", + "desc": "ノートタイトルのアイコンを表示します(インラインタイトルが有効な場合)。" + }, + "showTagPillIcons": { + "name": "タグピルアイコンを表示する", + "desc": "#hashtags と tags プロパティ内のアイコンを表示します。" + }, "headingMenusAndDialogs": "メニューとダイアログ", "showMenuActions": { "name": "メニューアクションを表示", "desc": "コンテキストメニューにアイコン関連のアクションを表示します。" }, + "showSuggestionIcons": { + "name": "サジェストアイコンを表示する", + "desc": "候補ポップアップにアイコンを表示します。" + }, "showQuickSwitcherIcons": { "name": "クイックスイッチャーアイコンを表示", "desc": "クイックスイッチャーの検索結果にアイコンを表示します。" diff --git a/i18n/ru.json b/i18n/ru.json index 6b2c35e..8bd777b 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -280,6 +280,7 @@ "toggleAllFolderIcons": "Переключить все значки папок", "toggleMinimalFolderIcons": "Переключить минимальные значки папок", "toggleMarkdownTabIcons": "Переключить значки вкладок Markdown", + "toggleTagPillIcons": "Переключить значки тегов-капсул", "toggleMenuActions": "Переключить действия меню", "toggleQuickSwitcherIcons": "Переключить значки Быстрый переход", "toggleBiggerSearchResults": "Переключить большие результаты поиска", @@ -299,7 +300,6 @@ "desc": "Настройте автоматизированные правила для значков файлов и папок.", "manage": "Настроить" }, - "headingSidebarAndTabIcons": "Значки боковой панели и вкладок", "biggerIcons": { "name": "Большие значки", "desc": "Показывать значки большего размера, чем в пользовательском интерфейсе по умолчанию." @@ -310,6 +310,7 @@ "descDesktop": "Нажмите на значок, чтобы открыть средство выбора значков.", "descMobile": "Нажмите на значок, чтобы открыть средство выбора значков." }, + "headingSidebarsAndTabs": "Боковые панели и вкладки", "showAllFileIcons": { "name": "Показать все значки файлов", "desc": "Показать значки файлов, у которых нет настраиваемого значка." @@ -326,11 +327,24 @@ "name": "Показать значки вкладок Markdown", "desc": "Показать значки вкладок для файлов Markdown." }, + "headingEditor": "Редактор", + "showTitleIcons": { + "name": "Показать значки заголовков", + "desc": "Показать значки в заголовках заметок, если включён встроенный заголовок." + }, + "showTagPillIcons": { + "name": "Показать значки тегов-капсул", + "desc": "Показать значки в #хештегах и свойстве tags." + }, "headingMenusAndDialogs": "Меню и диалоги", "showMenuActions": { "name": "Показать действия меню", "desc": "Показать действия, связанные со значками, в контекстных меню." }, + "showSuggestionIcons": { + "name": "Показать значки автозаполнения", + "desc": "Показать значки во всплывающих окнах при автозаполнении." + }, "showQuickSwitcherIcons": { "name": "Показать значки быстрого переключения", "desc": "Показать значки в результатах поиска быстрых переключателей." diff --git a/i18n/uk.json b/i18n/uk.json index a5748ea..d6b05e0 100644 --- a/i18n/uk.json +++ b/i18n/uk.json @@ -280,6 +280,7 @@ "toggleAllFolderIcons": "Перемкнути усі значки тек", "toggleMinimalFolderIcons": "Перемкнути найменші значки тек", "toggleMarkdownTabIcons": "Перемкнути значків вкладок Markdown", + "toggleTagPillIcons": "Перемкнути значки тегів-капсул", "toggleMenuActions": "Перемкнути дій меню", "toggleQuickSwitcherIcons": "Перемкнути значки швидкий перехід", "toggleBiggerSearchResults": "Перемкнути більші результати пошуку", @@ -299,7 +300,6 @@ "desc": "Налаштування автоматичних правил для значків файлів та тек.", "manage": "Налаштувати" }, - "headingSidebarAndTabIcons": "Значки бічної панелі та вкладок", "biggerIcons": { "name": "Великі значки", "desc": "Показувати більші значки, ніж у стандартному інтерфейсі." @@ -310,6 +310,7 @@ "descDesktop": "Натисніть на значок, щоб відкрити вікно вибору значків.", "descMobile": "Торкніться значка, щоб відкрити вікно вибору значків." }, + "headingSidebarsAndTabs": "Бічні панелі та вкладки", "showAllFileIcons": { "name": "Показувати усі значки файлів", "desc": "Показувати значки для файлів, які не мають користувацького значка." @@ -326,11 +327,24 @@ "name": "Показувати значки вкладок Markdown", "desc": "Показувати значки вкладок для файлів Markdown." }, + "headingEditor": "Редактор", + "showTitleIcons": { + "name": "Показувати значки заголовків", + "desc": "Показувати значки в заголовках нотаток, якщо увімкнено назву на початку." + }, + "showTagPillIcons": { + "name": "Показувати значки тегів-капсул", + "desc": "Показувати значки в #хештегах і у властивості tags." + }, "headingMenusAndDialogs": "Меню та діалогові вікна", "showMenuActions": { "name": "Показувати дії меню", "desc": "Показувати дії, пов’язані зі значками, у контекстних меню." }, + "showSuggestionIcons": { + "name": "Показувати значки автозаповнення", + "desc": "Показувати значки у спливаючих вікнах автозаповнення." + }, "showQuickSwitcherIcons": { "name": "Показувати значки швидкий перехід", "desc": "Показувати значки в результатах пошуку швидкий перехід." diff --git a/i18n/zh.json b/i18n/zh.json index 3cf2deb..a117fbb 100644 --- a/i18n/zh.json +++ b/i18n/zh.json @@ -280,6 +280,7 @@ "toggleAllFolderIcons": "切换所有文件夹图标", "toggleMinimalFolderIcons": "切换极简文件夹图标", "toggleMarkdownTabIcons": "切换 Markdown 标签图标", + "toggleTagPillIcons": "切换标签胶囊图标", "toggleMenuActions": "切换菜单操作", "toggleQuickSwitcherIcons": "切换快速切换图标", "toggleBiggerSearchResults": "切换更大的搜索结果", @@ -299,7 +300,6 @@ "desc": "为文件和文件夹图标设置自动规则。", "manage": "管理" }, - "headingSidebarAndTabIcons": "侧边栏和标签图标", "biggerIcons": { "name": "更大的图标", "desc": "显示比默认 UI 更大的图标。" @@ -310,6 +310,7 @@ "descDesktop": "单击图标以打开图标选择器。", "descMobile": "点击图标以打开图标选择器。" }, + "headingSidebarsAndTabs": "侧边栏和标签页标题", "showAllFileIcons": { "name": "显示所有文件图标", "desc": "显示没有自定义图标的文件图标。" @@ -326,11 +327,24 @@ "name": "显示 Markdown 标签图标", "desc": "显示 Markdown 文件的标签图标。" }, + "headingEditor": "编辑器", + "showTitleIcons": { + "name": "显示标题图标", + "desc": "如果启用了页内标题,则显示笔记标题中的图标。" + }, + "showTagPillIcons": { + "name": "显示标签胶囊图标", + "desc": "显示 #标签 和 tags 属性中的图标。" + }, "headingMenusAndDialogs": "菜单和对话框", "showMenuActions": { "name": "显示菜单操作", "desc": "在上下文菜单中显示与图标相关的操作。" }, + "showSuggestionIcons": { + "name": "显示建议图标", + "desc": "在建议弹出窗口中显示图标。" + }, "showQuickSwitcherIcons": { "name": "显示快速切换器图标", "desc": "在快速切换器的搜索结果中显示图标。" diff --git a/manifest.json b/manifest.json index a662254..5d2a7e7 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "iconic", "name": "Iconic", - "version": "1.1.2", + "version": "1.1.3", "minAppVersion": "1.6.0", "description": "Customize your icons and their colors directly from the UI, including tabs, files & folders, bookmarks, tags, properties, and ribbon commands.", "author": "Holo", diff --git a/package-lock.json b/package-lock.json index b1fe28d..3e505ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9 +1,44 @@ { "name": "iconic", - "version": "1.1.2", + "version": "1.1.3", "lockfileVersion": 1, "requires": true, "dependencies": { + "@codemirror/language": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.2.tgz", + "integrity": "sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==", + "dev": true, + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "@codemirror/state": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", + "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "dev": true, + "requires": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "@codemirror/view": { + "version": "6.38.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.0.tgz", + "integrity": "sha512-yvSchUwHOdupXkd7xJ0ob36jdsSR/I+/C+VbY0ffBiL5NiSTEBDfB1ZGWbbIlDd5xgdUkody+lukAdOxYrOBeg==", + "dev": true, + "requires": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "@esbuild/android-arm": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.3.tgz", @@ -158,6 +193,36 @@ "dev": true, "optional": true }, + "@lezer/common": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", + "dev": true + }, + "@lezer/highlight": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "dev": true, + "requires": { + "@lezer/common": "^1.0.0" + } + }, + "@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "dev": true, + "requires": { + "@lezer/common": "^1.0.0" + } + }, + "@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "dev": true + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -194,9 +259,9 @@ } }, "@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true }, "@types/json-schema": { @@ -336,6 +401,12 @@ "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", "dev": true }, + "crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "dev": true + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -565,9 +636,9 @@ "dev": true }, "obsidian": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.7.2.tgz", - "integrity": "sha512-k9hN9brdknJC+afKr5FQzDRuEFGDKbDjfCazJwpgibwCAoZNYHYV8p/s3mM8I6AsnKrPKNXf8xGuMZ4enWelZQ==", + "version": "1.8.7", + "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.8.7.tgz", + "integrity": "sha512-h4bWwNFAGRXlMlMAzdEiIM2ppTGlrh7uGOJS6w4gClrsjc+ei/3YAtU2VdFUlCiPuTHpY4aBpFJJW75S1Tl/JA==", "dev": true, "requires": { "@types/codemirror": "5.60.8", @@ -628,6 +699,12 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, + "style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "dev": true + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -666,6 +743,12 @@ "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", "dev": true }, + "w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "dev": true + }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index 3374de6..ba95813 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iconic", - "version": "1.1.2", + "version": "1.1.3", "description": "Customize your icons and their colors directly from the UI, including tabs, files & folders, bookmarks, tags, properties, and ribbon commands.", "main": "main.js", "scripts": { @@ -12,12 +12,15 @@ "author": "Holo", "license": "MIT-0", "devDependencies": { + "@codemirror/language": "^6.11.2", + "@codemirror/state": "^6.5.2", + "@codemirror/view": "^6.38.0", "@types/node": "^16.18.98", "@typescript-eslint/eslint-plugin": "5.29.0", "@typescript-eslint/parser": "5.29.0", "builtin-modules": "3.3.0", "esbuild": "0.17.3", - "obsidian": "latest", + "obsidian": "^1.8.7", "tslib": "2.4.0", "typescript": "4.7.4" } diff --git a/src/IconicPlugin.ts b/src/IconicPlugin.ts index ae6d007..850eb1f 100644 --- a/src/IconicPlugin.ts +++ b/src/IconicPlugin.ts @@ -1,9 +1,10 @@ -import { Command, Platform, Plugin, TAbstractFile, TFile, TFolder, View, WorkspaceLeaf, getIconIds } from 'obsidian'; +import { Command, Platform, Plugin, TAbstractFile, TFile, TFolder, View, WorkspaceLeaf, apiVersion, getIconIds } from 'obsidian'; import IconicSettingTab from 'src/IconicSettingTab'; import EMOJIS from 'src/Emojis'; import STRINGS from 'src/Strings'; import MenuManager from 'src/managers/MenuManager'; import RuleManager, { RuleTrigger } from 'src/managers/RuleManager'; +import IconManager from 'src/managers/IconManager'; import AppIconManager from 'src/managers/AppIconManager'; import TabIconManager from 'src/managers/TabIconManager'; import FileIconManager from 'src/managers/FileIconManager'; @@ -12,6 +13,7 @@ import TagIconManager from 'src/managers/TagIconManager'; import PropertyIconManager from 'src/managers/PropertyIconManager'; import EditorIconManager from 'src/managers/EditorIconManager'; import RibbonIconManager from 'src/managers/RibbonIconManager'; +import SuggestionIconManager from 'src/managers/SuggestionIconManager'; import QuickSwitcherIconManager from 'src/managers/QuickSwitcherIconManager'; import IconPicker from 'src/dialogs/IconPicker'; import RulePicker from 'src/dialogs/RulePicker'; @@ -22,7 +24,11 @@ export { STRINGS }; export type Category = 'app' | 'tab' | 'file' | 'folder' | 'group' | 'search' | 'graph' | 'url' | 'tag' | 'property' | 'ribbon' | 'rule'; export type AppItemId = 'help' | 'settings' | 'pin' | 'sidebarLeft' | 'sidebarRight' | 'minimize' | 'maximize' | 'unmaximize' | 'close'; -export const FILE_TAB_TYPES = ['markdown', 'canvas', 'bases', 'image', 'audio', 'video', 'pdf']; +export const FILE_TAB_TYPES = [ + 'markdown', 'canvas', 'bases', 'image', 'audio', 'video', 'pdf', +].concat([ + 'kanban', // Community plugin tab types +]); const SYNCABLE_TYPES = ['image', 'audio', 'video', 'pdf', 'unsupported']; const IMAGE_EXTENSIONS = ['bmp', 'png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'avif']; const AUDIO_EXTENSIONS = ['mp3', 'wav', 'm4a', '3gp', 'flac', 'ogg', 'oga', 'opus']; @@ -75,7 +81,10 @@ interface IconicSettings { showAllFolderIcons: boolean, minimalFolderIcons: boolean; showMarkdownTabIcons: boolean; + showTitleIcons: boolean; + showTagPillIcons: boolean; showMenuActions: boolean; + showSuggestionIcons: boolean; showQuickSwitcherIcons: boolean; showItemName: string; biggerSearchResults: string; @@ -134,7 +143,10 @@ const DEFAULT_SETTINGS: IconicSettings = { showAllFolderIcons: false, minimalFolderIcons: true, showMarkdownTabIcons: true, + showTitleIcons: true, + showTagPillIcons: true, showMenuActions: true, + showSuggestionIcons: true, showQuickSwitcherIcons: true, showItemName: 'desktop', biggerSearchResults: 'mobile', @@ -177,6 +189,7 @@ export default class IconicPlugin extends Plugin { propertyIconManager?: PropertyIconManager; editorIconManager?: EditorIconManager; ribbonIconManager?: RibbonIconManager; + suggestionIconManager?: SuggestionIconManager; quickSwitcherIconManager?: QuickSwitcherIconManager; dialogCommands: Command[] = []; @@ -310,9 +323,7 @@ export default class IconicPlugin extends Plugin { const page = tAbstractFile instanceof TFile ? 'file' : 'folder'; // If a created file/folder triggers a new ruling, refresh icons if (this.ruleManager.triggerRulings(page, 'rename', 'move', 'modify')) { - if (page === 'file') this.tabIconManager?.refreshIcons(); - this.fileIconManager?.refreshIcons(); - this.bookmarkIconManager?.refreshIcons(); + this.refreshManagers(page); } })); @@ -329,14 +340,10 @@ export default class IconicPlugin extends Plugin { const page = tAbstractFile instanceof TFile ? 'file' : 'folder'; // If a renamed file/folder triggers a new ruling, refresh icons if (filename !== oldFilename && this.ruleManager.triggerRulings(page, 'rename')) { - if (page === 'file') this.tabIconManager?.refreshIcons(); - this.fileIconManager?.refreshIcons(); - this.bookmarkIconManager?.refreshIcons(); + this.refreshManagers(page); // If a moved file/folder triggers a new ruling, refresh icons } else if (tree !== oldTree && this.ruleManager.triggerRulings(page, 'move')) { - if (page === 'file') this.tabIconManager?.refreshIcons(); - this.fileIconManager?.refreshIcons(); - this.bookmarkIconManager?.refreshIcons(); + this.refreshManagers(page); } })); @@ -430,8 +437,7 @@ export default class IconicPlugin extends Plugin { callback: () => { this.settings.showAllFileIcons = !this.settings.showAllFileIcons; this.saveSettings(); - this.tabIconManager?.refreshIcons(); - this.fileIconManager?.refreshIcons(); + this.refreshManagers('file'); } })); @@ -442,9 +448,7 @@ export default class IconicPlugin extends Plugin { callback: () => { this.settings.showAllFolderIcons = !this.settings.showAllFolderIcons; this.saveSettings(); - this.fileIconManager?.refreshIcons(); - this.bookmarkIconManager?.refreshIcons(); - this.tagIconManager?.refreshIcons(); + this.refreshManagers('file', 'tag'); } })); @@ -455,9 +459,7 @@ export default class IconicPlugin extends Plugin { callback: () => { this.settings.minimalFolderIcons = !this.settings.minimalFolderIcons; this.saveSettings(); - this.fileIconManager?.refreshIcons(); - this.bookmarkIconManager?.refreshIcons(); - this.tagIconManager?.refreshIcons(); + this.refreshManagers('file', 'tag'); } })); @@ -472,6 +474,17 @@ export default class IconicPlugin extends Plugin { } }); + // COMMAND: Toggle tag pill icons + this.addCommand({ + id: 'toggle-tag-pill-icons', + name: STRINGS.commands.toggleTagPillIcons, + callback: () => { + this.settings.showTagPillIcons = !this.settings.showTagPillIcons; + this.saveSettings(); + this.refreshManagers('tag'); + } + }); + // COMMAND: Toggle menu actions this.addCommand({ id: 'toggle-menu-actions', @@ -518,9 +531,7 @@ export default class IconicPlugin extends Plugin { IconPicker.openSingle(this, file, (newIcon, newColor) => { this.saveFileIcon(file, newIcon, newColor); - this.fileIconManager?.refreshIcons(); - this.tabIconManager?.refreshIcons(); - this.bookmarkIconManager?.refreshIcons(); + this.refreshManagers('file'); }); }, }); @@ -542,9 +553,7 @@ export default class IconicPlugin extends Plugin { const page = tAbstractFile instanceof TFile ? 'file' : 'folder'; // If a modified file/folder triggers a new ruling, refresh icons if (this.ruleManager.triggerRulings(page, 'modify')) { - if (page === 'file') this.tabIconManager?.refreshIcons(); - this.fileIconManager?.refreshIcons(); - this.bookmarkIconManager?.refreshIcons(); + this.refreshManagers(page); } } @@ -562,21 +571,49 @@ export default class IconicPlugin extends Plugin { try { this.propertyIconManager = new PropertyIconManager(this) } catch (e) { console.error(e) } try { this.editorIconManager = new EditorIconManager(this) } catch (e) { console.error(e) } try { this.ribbonIconManager = new RibbonIconManager(this) } catch (e) { console.error(e) } + try { this.suggestionIconManager = new SuggestionIconManager(this) } catch (e) { console.error(e) } try { this.quickSwitcherIconManager = new QuickSwitcherIconManager(this) } catch (e) { console.error(e) } } /** - * Refresh all manager instances. + * Refresh all icon managers, or a specific group of them. */ - refreshManagers(): void { - this.appIconManager?.refreshIcons(); - this.tabIconManager?.refreshIcons(); - this.fileIconManager?.refreshIcons(); - this.bookmarkIconManager?.refreshIcons(); - this.tagIconManager?.refreshIcons(); - this.propertyIconManager?.refreshIcons(); - this.editorIconManager?.refreshIcons(); - this.ribbonIconManager?.refreshIcons(); + refreshManagers(...categories: Category[]): void { + if (categories) { + categories = ['app', 'tab', 'file', 'folder', 'tag', 'property', 'ribbon']; + } + const managers = new Set(); + + if (categories?.includes('app')) { + managers.add(this.appIconManager); + } + if (categories?.includes('tab')) { + managers.add(this.tabIconManager); + } + if (categories?.includes('file')) { + managers.add(this.tabIconManager); + managers.add(this.fileIconManager); + managers.add(this.bookmarkIconManager); + managers.add(this.editorIconManager); + } + if (categories?.includes('folder')) { + managers.add(this.fileIconManager); + managers.add(this.bookmarkIconManager); + } + if (categories?.includes('tag')) { + managers.add(this.tagIconManager); + managers.add(this.editorIconManager); + } + if (categories?.includes('property')) { + managers.add(this.propertyIconManager); + managers.add(this.editorIconManager); + } + if (categories?.includes('ribbon')) { + managers.add(this.ribbonIconManager); + } + + managers.delete(undefined); + for (const manager of managers) manager?.refreshIcons(); } /** @@ -652,12 +689,16 @@ export default class IconicPlugin extends Plugin { } case 'sidebarLeft': { name = STRINGS.appItems.sidebarLeft; - iconDefault = 'sidebar-left'; + iconDefault = apiVersion >= '1.9' // Pre-1.9.0 compatible + ? 'sidebar-toggle-button-icon' + : 'sidebar-left'; break; } case 'sidebarRight': { name = STRINGS.appItems.sidebarRight; - iconDefault = 'sidebar-right'; + iconDefault = apiVersion >= '1.9' // Pre-1.9.0 compatible + ? 'sidebar-toggle-button-icon' + : 'sidebar-right'; break; } case 'minimize': name = STRINGS.appItems.minimize; break; @@ -1396,6 +1437,7 @@ export default class IconicPlugin extends Plugin { this.propertyIconManager?.unload(); this.editorIconManager?.unload(); this.ribbonIconManager?.unload(); + this.suggestionIconManager?.unload(); this.quickSwitcherIconManager?.unload(); this.refreshBodyClasses(true); } diff --git a/src/IconicSettingTab.ts b/src/IconicSettingTab.ts index f4eced2..dd3d436 100644 --- a/src/IconicSettingTab.ts +++ b/src/IconicSettingTab.ts @@ -42,9 +42,6 @@ export default class IconicSettingTab extends PluginSettingTab { }); }); - // HEADING: Lists & tabs - new Setting(this.containerEl).setName(STRINGS.settings.headingSidebarAndTabIcons).setHeading(); - // Bigger icons new Setting(this.containerEl) .setName(STRINGS.settings.biggerIcons.name) @@ -98,6 +95,9 @@ export default class IconicSettingTab extends PluginSettingTab { this.refreshIndicator(this.indicators.clickableIcons, dropdown.getValue()); }); + // HEADING: Sidebars & tabs + new Setting(this.containerEl).setName(STRINGS.settings.headingSidebarsAndTabs).setHeading(); + // Show all file icons new Setting(this.containerEl) .setName(STRINGS.settings.showAllFileIcons.name) @@ -107,8 +107,7 @@ export default class IconicSettingTab extends PluginSettingTab { .onChange(value => { this.plugin.settings.showAllFileIcons = value; this.plugin.saveSettings(); - this.plugin.tabIconManager?.refreshIcons(); - this.plugin.fileIconManager?.refreshIcons(); + this.plugin.refreshManagers('file'); }) ); @@ -121,8 +120,7 @@ export default class IconicSettingTab extends PluginSettingTab { .onChange(value => { this.plugin.settings.showAllFolderIcons = value; this.plugin.saveSettings(); - this.plugin.fileIconManager?.refreshIcons(); - this.plugin.bookmarkIconManager?.refreshIcons(); + this.plugin.refreshManagers('folder'); }) ); @@ -135,8 +133,7 @@ export default class IconicSettingTab extends PluginSettingTab { .onChange(value => { this.plugin.settings.minimalFolderIcons = value; this.plugin.saveSettings(); - this.plugin.fileIconManager?.refreshIcons(); - this.plugin.bookmarkIconManager?.refreshIcons(); + this.plugin.refreshManagers('folder'); }) ); @@ -153,6 +150,35 @@ export default class IconicSettingTab extends PluginSettingTab { }) ); + // HEADING: Editor + new Setting(this.containerEl).setHeading().setName(STRINGS.settings.headingEditor); + + // Show title icons + new Setting(this.containerEl) + .setName(STRINGS.settings.showTitleIcons.name) + .setDesc(STRINGS.settings.showTitleIcons.desc) + .addToggle(toggle => toggle + .setValue(this.plugin.settings.showTitleIcons) + .onChange(value => { + this.plugin.settings.showTitleIcons = value; + this.plugin.saveSettings(); + this.plugin.refreshManagers('file'); + }) + ); + + // Show tag pill icons + new Setting(this.containerEl) + .setName(STRINGS.settings.showTagPillIcons.name) + .setDesc(STRINGS.settings.showTagPillIcons.desc) + .addToggle(toggle => toggle + .setValue(this.plugin.settings.showTagPillIcons) + .onChange(value => { + this.plugin.settings.showTagPillIcons = value; + this.plugin.saveSettings(); + this.plugin.refreshManagers('tag'); + }) + ); + // HEADING: Menus & dialogs new Setting(this.containerEl).setHeading().setName(STRINGS.settings.headingMenusAndDialogs); @@ -169,6 +195,18 @@ export default class IconicSettingTab extends PluginSettingTab { }) ); + // Show suggestion icons + new Setting(this.containerEl) + .setName(STRINGS.settings.showSuggestionIcons.name) + .setDesc(STRINGS.settings.showSuggestionIcons.desc) + .addToggle(toggle => toggle + .setValue(this.plugin.settings.showSuggestionIcons) + .onChange(value => { + this.plugin.settings.showSuggestionIcons = value; + this.plugin.saveSettings(); + }) + ); + // Show quick switcher icons new Setting(this.containerEl) .setName(STRINGS.settings.showQuickSwitcherIcons.name) @@ -178,7 +216,6 @@ export default class IconicSettingTab extends PluginSettingTab { .onChange(value => { this.plugin.settings.showQuickSwitcherIcons = value; this.plugin.saveSettings(); - // this.plugin.promptManager?.refreshIcons(); }) ); @@ -341,7 +378,7 @@ export default class IconicSettingTab extends PluginSettingTab { .onChange(value => { this.plugin.settings.uncolorQuick = value; this.plugin.saveSettings(); - this.plugin.ribbonIconManager?.refreshIcons(); + this.plugin.refreshManagers('ribbon'); }) ); diff --git a/src/Strings.ts b/src/Strings.ts index cd5f650..bb6c319 100644 --- a/src/Strings.ts +++ b/src/Strings.ts @@ -280,6 +280,7 @@ export default class Strings { toggleAllFolderIcons: 'Toggle all folder icons', toggleMinimalFolderIcons: 'Toggle minimal folder icons', toggleMarkdownTabIcons: 'Toggle Markdown tab icons', + toggleTagPillIcons: 'Toggle tag pill icons', toggleMenuActions: 'Toggle menu actions', toggleQuickSwitcherIcons: 'Toggle quick switcher icons', toggleBiggerSearchResults: 'Toggle bigger search results', @@ -299,7 +300,6 @@ export default class Strings { desc: 'Set up automated rules for file and folder icons.', manage: 'Manage', }, - headingSidebarAndTabIcons: 'Sidebar & tab icons', biggerIcons: { name: 'Bigger icons', desc: 'Show bigger icons than the default UI.', @@ -310,6 +310,7 @@ export default class Strings { descDesktop: 'Click an icon to open the icon picker.', descMobile: 'Tap an icon to open the icon picker.', }, + headingSidebarsAndTabs: 'Sidebars & tabs', showAllFileIcons: { name: 'Show all file icons', desc: 'Show icons for files that have no custom icon.', @@ -326,11 +327,24 @@ export default class Strings { name: 'Show Markdown tab icons', desc: 'Show tab icons for Markdown files.', }, + headingEditor: 'Editor', + showTitleIcons: { + name: 'Show title icons', + desc: 'Show icons in note titles, if inline titles are enabled.', + }, + showTagPillIcons: { + name: 'Show tag pill icons', + desc: 'Show icons in #hashtags and the tags property.', + }, headingMenusAndDialogs: 'Menus & dialogs', showMenuActions: { name: 'Show menu actions', desc: 'Show icon-related actions in context menus.', }, + showSuggestionIcons: { + name: 'Show suggestion icons', + desc: 'Show icons in suggestion pop-ups.', + }, showQuickSwitcherIcons: { name: 'Show quick switcher icons', desc: 'Show icons in search results of quick switchers.', diff --git a/src/dialogs/IconPicker.ts b/src/dialogs/IconPicker.ts index 70118ab..db791eb 100644 --- a/src/dialogs/IconPicker.ts +++ b/src/dialogs/IconPicker.ts @@ -1,4 +1,4 @@ -import { ButtonComponent, ColorComponent, ExtraButtonComponent, Hotkey, Menu, Modal, Platform, Setting, TextComponent, prepareFuzzySearch } from 'obsidian'; +import { ButtonComponent, ColorComponent, ExtraButtonComponent, Hotkey, Menu, Modal, Platform, Setting, TextComponent, displayTooltip, prepareFuzzySearch, setTooltip } from 'obsidian'; import IconicPlugin, { Category, Item, Icon, ICONS, EMOJIS, STRINGS } from 'src/IconicPlugin'; import ColorUtils, { COLORS } from 'src/ColorUtils'; import { RuleItem } from 'src/managers/RuleManager'; @@ -90,7 +90,6 @@ export default class IconPicker extends Modal { private emojiModeButton: ExtraButtonComponent; private mobileModeButton: ButtonComponent; private colorPickerEl: HTMLElement; - private mobileTooltipEl: HTMLElement | null; // State private colorPickerPaused = false; @@ -130,21 +129,6 @@ export default class IconPicker extends Modal { this.scope.register(null, ' ', event => this.confirmFocus(event)); this.scope.register(null, 'Delete', event => this.deleteFocus(event)); this.scope.register(null, 'Backspace', event => this.deleteFocus(event)); - - // Clear mobile tooltip when dialog is touched - this.iconManager.setEventListener(this.modalEl, 'pointerdown', () => { - this.mobileTooltipEl?.remove(); - this.mobileTooltipEl = null; - }); - - // Update color picker tooltip when it appears, in case the ariaLabel has changed - this.iconManager.setMutationObserver(activeDocument.body, { childList: true }, mutation => { - for (const addedNode of mutation.addedNodes) { - if (addedNode instanceof HTMLElement && addedNode.hasClass('tooltip')) { - if (this.colorPickerHovered) this.updateColorTooltip(); - } - } - }); } /** @@ -300,7 +284,7 @@ export default class IconPicker extends Modal { this.searchSetting = new Setting(this.contentEl) .addExtraButton(colorResetButton => { colorResetButton .setIcon('lucide-rotate-ccw') - .setTooltip(STRINGS.iconPicker.resetColor) + .setTooltip(STRINGS.iconPicker.resetColor, { delay: 300 }) .onClick(() => this.resetColor()); colorResetButton.extraSettingsEl.addClass('iconic-reset-color'); colorResetButton.extraSettingsEl.toggleClass('iconic-invisible', this.color === null); @@ -315,9 +299,9 @@ export default class IconPicker extends Modal { .onChange(value => { if (this.colorPickerPaused) return; this.color = value; - this.colorPickerEl.ariaLabel = this.color; this.colorResetButton.extraSettingsEl.removeClass('iconic-invisible'); this.colorResetButton.extraSettingsEl.tabIndex = 0; + this.updateColorTooltip(); this.updateSearchResults(); }); this.colorPicker = colorPicker; @@ -333,9 +317,16 @@ export default class IconPicker extends Modal { // Color picker let openRgbPicker = false; this.colorPickerEl = this.searchSetting.controlEl.find('input[type="color"]'); - this.colorPickerEl.dataset.tooltipDelay = '300'; - this.iconManager.setEventListener(this.colorPickerEl, 'pointerenter', () => this.colorPickerHovered = true); - this.iconManager.setEventListener(this.colorPickerEl, 'pointerleave', () => this.colorPickerHovered = false); + // Reset tooltip delay when cursor starts hovering + this.iconManager.setEventListener(this.colorPickerEl, 'pointerenter', () => { + this.updateColorTooltip(); + this.colorPickerHovered = true; + }); + this.iconManager.setEventListener(this.colorPickerEl, 'pointerleave', () => { + this.colorPickerHovered = false; + this.updateColorTooltip(); + }); + // Primary color picker this.iconManager.setEventListener(this.colorPickerEl, 'click', event => { if (openRgbPicker === true) { openRgbPicker = false; @@ -344,6 +335,7 @@ export default class IconPicker extends Modal { event.preventDefault(); } }); + // Secondary color picker this.iconManager.setEventListener(this.colorPickerEl, 'contextmenu', event => { navigator?.vibrate(100); // Not supported on iOS if (this.plugin.settings.colorPicker2 === 'rgb') { @@ -371,11 +363,6 @@ export default class IconPicker extends Modal { this.searchResultsSetting.settingEl.scrollLeft += event.deltaY; } }, { passive: true }); - // Clear mobile tooltip when scrolling - this.iconManager.setEventListener(this.searchResultsSetting.settingEl, 'scroll', () => { - this.mobileTooltipEl?.remove(); - this.mobileTooltipEl = null; - }, { passive: true }); // Match styling of bookmark edit dialog const buttonContainerEl = this.modalEl.createDiv({ cls: 'modal-button-container' }); @@ -420,14 +407,14 @@ export default class IconPicker extends Modal { this.updateMobileSearchMode(); } else { this.iconModeButton = new ExtraButtonComponent(buttonContainerEl) - .setTooltip(STRINGS.iconPicker.toggleIcons, { placement: 'top' }) + .setTooltip(STRINGS.iconPicker.toggleIcons, { placement: 'top', delay: 300 }) .onClick(() => { dialogState.iconMode = !dialogState.iconMode; this.updateDesktopSearchMode(); }); this.iconModeButton.extraSettingsEl.tabIndex = 0; this.emojiModeButton = new ExtraButtonComponent(buttonContainerEl) - .setTooltip(STRINGS.iconPicker.toggleEmojis, { placement: 'top' }) + .setTooltip(STRINGS.iconPicker.toggleEmojis, { placement: 'top', delay: 300 }) .onClick(() => { dialogState.emojiMode = !dialogState.emojiMode; this.updateDesktopSearchMode(); @@ -534,7 +521,6 @@ export default class IconPicker extends Modal { this.colorResetButton.extraSettingsEl.addClass('iconic-invisible'); this.colorResetButton.extraSettingsEl.tabIndex = -1; this.updateColorPicker(); - this.updateColorTooltip(); this.updateSearchResults(); } @@ -595,18 +581,18 @@ export default class IconPicker extends Modal { : STRINGS.iconPicker.changeMixes.replace('{#}', this.items.length.toString()) ); this.searchField.setPlaceholder(STRINGS.iconPicker.searchMix); - } else if (dialogState.iconMode) { - this.setTitle(this.items.length === 1 - ? STRINGS.iconPicker.changeIcon - : STRINGS.iconPicker.changeIcons.replace('{#}', this.items.length.toString()) - ); - this.searchField.setPlaceholder(STRINGS.iconPicker.searchIcons); - } else { + } else if (dialogState.emojiMode) { this.setTitle(this.items.length === 1 ? STRINGS.iconPicker.changeEmoji : STRINGS.iconPicker.changeEmojis.replace('{#}', this.items.length.toString()) ); this.searchField.setPlaceholder(STRINGS.iconPicker.searchEmojis); + } else { + this.setTitle(this.items.length === 1 + ? STRINGS.iconPicker.changeIcon + : STRINGS.iconPicker.changeIcons.replace('{#}', this.items.length.toString()) + ); + this.searchField.setPlaceholder(STRINGS.iconPicker.searchIcons); } this.updateSearchResults(); @@ -619,53 +605,28 @@ export default class IconPicker extends Modal { this.colorPickerPaused = true; this.colorPicker.setValueRgb(ColorUtils.toRgbObject(this.color)); this.colorPickerPaused = false; - - if (!this.color) { - this.colorPickerEl.ariaLabel = STRINGS.iconPicker.changeColor; - } else if (COLOR_KEYS.includes(this.color)) { - this.colorPickerEl.ariaLabel = STRINGS.iconPicker.colors[this.color as keyof typeof STRINGS.iconPicker.colors]; - } else { - this.colorPickerEl.ariaLabel = this.color; - } - - if (this.colorPickerHovered) { - this.updateColorTooltip(); - } + this.updateColorTooltip(); } /** - * Update tooltip currently displayed for the color picker. + * Update just the color picker tooltip. */ private updateColorTooltip(): void { - const tooltipEl = activeDocument.body.find(':scope > .tooltip'); - if (tooltipEl && tooltipEl.firstChild) { - tooltipEl.style.removeProperty('width'); - tooltipEl.firstChild.nodeValue = this.colorPickerEl.ariaLabel; + // Set tooltip message + let tooltip = STRINGS.iconPicker.changeColor; + if (this.color) { + if (COLOR_KEYS.includes(this.color)) { + tooltip = STRINGS.iconPicker.colors[this.color as keyof typeof STRINGS.iconPicker.colors]; + } else { + tooltip = this.color; + } } - } - /** - * Display a long-press tooltip for mobile users. - */ - private setMobileTooltip(iconEl: HTMLElement, label: string): void { - this.mobileTooltipEl?.remove(); - this.mobileTooltipEl = null; - const iconRect = iconEl.getBoundingClientRect(); - const left = Math.max(0, iconRect.left + iconRect.width / 2); - const top = iconRect.top - 48; - this.mobileTooltipEl = activeDocument.body.createDiv({ - cls: ['tooltip', 'mod-top'], - text: label, - }); - this.mobileTooltipEl.createDiv('tooltip-arrow'); - this.mobileTooltipEl.style.fontSize = 'var(--font-ui-medium)'; - this.mobileTooltipEl.style.left = left + 'px'; - this.mobileTooltipEl.style.top = top + 'px'; - this.mobileTooltipEl.style.width = 'auto'; - this.mobileTooltipEl.style.whiteSpace = 'nowrap'; - const rect = this.mobileTooltipEl.getBoundingClientRect(); - if (rect.left < 0) { - this.mobileTooltipEl.style.left = left + rect.left + 'px'; + // Update tooltip instantly if cursor is hovering over color picker + if (this.colorPickerHovered) { + displayTooltip(this.colorPickerEl, tooltip, { delay: 1 }); + } else { + setTooltip(this.colorPickerEl, tooltip, { delay: 300 }); } } @@ -712,7 +673,10 @@ export default class IconPicker extends Modal { for (const iconEntry of this.searchResults) { const [icon, iconName] = iconEntry; this.searchResultsSetting.addExtraButton(iconButton => { - iconButton.setTooltip(iconName, { delay: 300 }); + iconButton.setTooltip(iconName, { + delay: 300, + placement: Platform.isPhone ? 'top' : 'bottom', + }); const iconEl = iconButton.extraSettingsEl; iconEl.addClass('iconic-search-result'); iconEl.tabIndex = -1; @@ -721,9 +685,9 @@ export default class IconPicker extends Modal { this.closeAndSave(icon, this.color); }); - if (Platform.isMobile) this.iconManager.setEventListener(iconEl, 'contextmenu', () => { + if (Platform.isPhone) this.iconManager.setEventListener(iconEl, 'contextmenu', () => { navigator?.vibrate(100); // Not supported on iOS - this.setMobileTooltip(iconEl, iconName); + displayTooltip(iconEl, iconName, { placement: 'top' }); }); }); } @@ -793,9 +757,7 @@ export default class IconPicker extends Modal { ? this.plugin.ruleManager.saveRule(page, newRule) : this.plugin.ruleManager.deleteRule(page, rule.id); if (isRulingChanged) { - if (page === 'file') this.plugin.tabIconManager?.refreshIcons(); - this.plugin.fileIconManager?.refreshIcons(); - this.plugin.bookmarkIconManager?.refreshIcons(); + this.plugin.refreshManagers(page); } this.updateOverruleReminder(); }); @@ -822,7 +784,6 @@ export default class IconPicker extends Modal { */ onClose(): void { this.contentEl.empty(); - this.mobileTooltipEl?.remove(); this.iconManager.stopEventListeners(); this.iconManager.stopMutationObservers(); this.plugin.saveSettings(); // Save any changes to dialogState diff --git a/src/dialogs/RulePicker.ts b/src/dialogs/RulePicker.ts index ea6b310..0a9c0fd 100644 --- a/src/dialogs/RulePicker.ts +++ b/src/dialogs/RulePicker.ts @@ -139,35 +139,19 @@ export default class RulePicker extends Modal { } } - /** - * Refresh any icon managers influenced by the current page. - */ - private refreshPageManagers(): void { - switch (this.plugin.settings.dialogState.rulePage) { - case 'file': { - this.plugin.tabIconManager?.refreshIcons(); - this.plugin.fileIconManager?.refreshIcons(); - this.plugin.bookmarkIconManager?.refreshIcons(); - } - case 'folder': { - this.plugin.fileIconManager?.refreshIcons(); - this.plugin.bookmarkIconManager?.refreshIcons(); - } - } - } - /** * Append a given rule to the page. */ private appendRule(rule: RuleItem, isNewRule?: boolean): void { + const page = this.plugin.settings.dialogState.rulePage; const ruleSetting = new RuleSetting( this.contentEl, this.plugin, this.iconManager, - this.plugin.settings.dialogState.rulePage, + page, rule, this.ruleEls, - () => this.refreshPageManagers(), + () => this.plugin.refreshManagers(page), isNewRule, ); this.ruleEls.push(ruleSetting.settingEl); @@ -181,9 +165,11 @@ export default class RulePicker extends Modal { .onClick(() => { ruleSetting.settingEl.remove(); this.ruleEls.remove(ruleSetting.settingEl); - const rulePage = this.plugin.settings.dialogState.rulePage; - const isRulingChanged = this.plugin.ruleManager.deleteRule(rulePage, rule.id); - if (isRulingChanged) this.refreshPageManagers(); + const page = this.plugin.settings.dialogState.rulePage; + const isRulingChanged = this.plugin.ruleManager.deleteRule(page, rule.id); + if (isRulingChanged) { + this.plugin.refreshManagers(page); + } }); }); menu.showAtMouseEvent(event); diff --git a/src/managers/AppIconManager.ts b/src/managers/AppIconManager.ts index b4a0885..b2ea7f3 100644 --- a/src/managers/AppIconManager.ts +++ b/src/managers/AppIconManager.ts @@ -28,10 +28,11 @@ export default class AppIconManager extends IconManager { constructor(plugin: IconicPlugin) { super(plugin); this.plugin.registerEvent(this.app.workspace.on('layout-change', () => this.refreshIcons())); - this.refreshIcons(); + this.plugin.refreshManagers('app'); } /** + * @override * Refresh all app icons. * * Some button elements get replaced by the app when switching workspaces, @@ -260,7 +261,7 @@ export default class AppIconManager extends IconManager { .setIcon('lucide-image-plus') .onClick(() => IconPicker.openSingle(this.plugin, appItem, (newIcon, newColor) => { this.plugin.saveAppIcon(appItem, newIcon, newColor); - this.refreshIcons(); + this.plugin.refreshManagers('app'); })) ); @@ -271,7 +272,7 @@ export default class AppIconManager extends IconManager { .setIcon(appItem.icon ? 'lucide-image-minus' : 'lucide-rotate-ccw') .onClick(() => { this.plugin.saveAppIcon(appItem, null, null); - this.refreshIcons(); + this.plugin.refreshManagers('app'); }) ); } diff --git a/src/managers/BookmarkIconManager.ts b/src/managers/BookmarkIconManager.ts index 8807121..fe748d1 100644 --- a/src/managers/BookmarkIconManager.ts +++ b/src/managers/BookmarkIconManager.ts @@ -66,6 +66,7 @@ export default class BookmarkIconManager extends IconManager { } /** + * @override * Refresh all bookmark icons. */ refreshIcons(unloading?: boolean): void { @@ -141,9 +142,7 @@ export default class BookmarkIconManager extends IconManager { this.refreshIcon(rule, iconEl, event => { IconPicker.openSingle(this.plugin, bmark, (newIcon, newColor) => { this.plugin.saveBookmarkIcon(bmark, newIcon, newColor); - this.refreshIcons(); - this.plugin.tabIconManager?.refreshIcons(); - this.plugin.fileIconManager?.refreshIcons(); + this.plugin.refreshManagers('file', 'folder'); }); event.stopPropagation(); }); @@ -217,16 +216,12 @@ export default class BookmarkIconManager extends IconManager { if (selectedBmarks.length < 2) { IconPicker.openSingle(this.plugin, clickedBmark, (newIcon, newColor) => { this.plugin.saveBookmarkIcon(clickedBmark, newIcon, newColor); - this.refreshIcons(); - this.plugin.tabIconManager?.refreshIcons(); - this.plugin.fileIconManager?.refreshIcons(); + this.plugin.refreshManagers('file', 'folder'); }); } else { IconPicker.openMulti(this.plugin, selectedBmarks, (newIcon, newColor) => { this.plugin.saveBookmarkIcons(selectedBmarks, newIcon, newColor); - this.refreshIcons(); - this.plugin.tabIconManager?.refreshIcons(); - this.plugin.fileIconManager?.refreshIcons(); + this.plugin.refreshManagers('file', 'folder'); }); } }) @@ -255,9 +250,7 @@ export default class BookmarkIconManager extends IconManager { } else { this.plugin.saveBookmarkIcons(selectedBmarks, null, null); } - this.refreshIcons(); - this.plugin.tabIconManager?.refreshIcons(); - this.plugin.fileIconManager?.refreshIcons(); + this.plugin.refreshManagers('file', 'folder'); }) ); } @@ -277,9 +270,7 @@ export default class BookmarkIconManager extends IconManager { ? this.plugin.ruleManager.saveRule('file', newRule) : this.plugin.ruleManager.deleteRule('file', rule.id); if (isRulingChanged) { - this.refreshIcons(); - this.plugin.tabIconManager?.refreshIcons(); - this.plugin.fileIconManager?.refreshIcons(); + this.plugin.refreshManagers('file'); } })); }); diff --git a/src/managers/EditorIconManager.ts b/src/managers/EditorIconManager.ts index a066ad8..a99a733 100644 --- a/src/managers/EditorIconManager.ts +++ b/src/managers/EditorIconManager.ts @@ -1,7 +1,10 @@ -import { MarkdownPreviewView, MarkdownView, Platform } from 'obsidian'; +import { Editor, MarkdownView, Menu } from 'obsidian'; +import { ViewPlugin, ViewUpdate } from '@codemirror/view'; +import { syntaxTree } from '@codemirror/language'; import IconicPlugin, { TagItem, PropertyItem, STRINGS } from 'src/IconicPlugin'; import ColorUtils from 'src/ColorUtils'; import IconManager from 'src/managers/IconManager'; +import RuleEditor from 'src/dialogs/RuleEditor'; import IconPicker from 'src/dialogs/IconPicker'; /** @@ -11,59 +14,52 @@ export default class EditorIconManager extends IconManager { constructor(plugin: IconicPlugin) { super(plugin); - // Watch for suggestion menus - this.setMutationObserver(activeDocument.body, { childList: true }, mutation => { - const activeEl = activeDocument.activeElement; - if (!activeEl) return; - for (const addedNode of mutation.addedNodes) { - if (addedNode instanceof HTMLElement && addedNode.hasClass('suggestion-container')) { - // Check where the text cursor is - if (activeEl.hasClass('metadata-property-key-input')) { - this.onPropertySuggestionMenu(addedNode); - } else if (activeEl.hasClass('multi-select-input') && activeEl.closest('.metadata-property[data-property-key="tags"]')) { - this.onTagSuggestionMenu(addedNode); - } - break; - } - } - }); - - // Markdown post-processor for hashtags (reading mode) + // Style hashtags in reading mode this.plugin.registerMarkdownPostProcessor(sectionEl => { const tags = this.plugin.getTagItems(); - if (tags.length === 0) return; const tagEls = sectionEl.findAll('a.tag'); - for (const tagEl of tagEls) { - const tagId = tagEl.getAttribute('href')?.replace('#', ''); - if (!tagId) continue; - const tag = tags.find(tag => tag.id === tagId); - if (!tag) continue; - EditorIconManager.setTagColor(tag, tagEl); - const iconEl = tagEl.find('.iconic-icon') ?? createSpan(); - tagEl.prepend(iconEl); - if (this.plugin.isSettingEnabled('clickableIcons')) { - this.refreshIcon(tag, iconEl, event => { - IconPicker.openSingle(this.plugin, tag, (newIcon, newColor) => { - this.plugin.saveTagIcon(tag, newIcon, newColor); - this.refreshIcons(); - this.plugin.tagIconManager?.refreshIcons(); - }); - event.stopPropagation(); - }); - } else { - this.refreshIcon(tag, iconEl); - } - if (this.plugin.settings.showMenuActions) { - this.setEventListener(tagEl, 'contextmenu', event => { - this.onTagNewContextMenu(tag.id, event); - }); - } else { - this.stopEventListener(tagEl, 'contextmenu'); - } - } + this.refreshReadingHashtags(tags, tagEls); }); - // Initialize any open MarkdownViews + const manager = this; + plugin.registerEditorExtension(ViewPlugin.fromClass(class { + update(update: ViewUpdate): void { + let viewport = update.view.viewport; + let tree = syntaxTree(update.view.state); + + tree.iterate({ from: viewport.from, to: viewport.to, enter: (nodeRef) => { + if (!nodeRef.name.includes('hashtag-begin')) return; + + // Get both tag elements + const beginEl = update.view.domAtPos(nodeRef.to).node.parentElement; + if (!(beginEl instanceof HTMLElement)) return; + const endEl = beginEl?.nextElementSibling; + if (!(endEl instanceof HTMLElement) || !endEl.hasClass('cm-hashtag-end')) return; + + // Get tag + const tagId = endEl.getText(); + const tag = manager.plugin.getTagItem(tagId); + + // Refresh tag + const onContextMenu = () => { + if (tag) manager.onTagContextMenu(tag.id, true); + }; + manager.refreshTag(beginEl, tag, onContextMenu); + if (tag) tag.icon = null; + manager.refreshTag(endEl, tag, onContextMenu); + }}) + } + })); + + // Initialize MarkdownViews as they open + this.plugin.registerEvent(this.app.workspace.on('active-leaf-change', leaf => { + if (leaf?.view instanceof MarkdownView) { + this.observeViewIcons(leaf.view); + this.refreshViewIcons(leaf.view); + } + })); + + // Initialize any current MarkdownViews for (const leaf of this.app.workspace.getLeavesOfType('markdown')) { if (leaf.view instanceof MarkdownView) { this.observeViewIcons(leaf.view); @@ -71,23 +67,29 @@ export default class EditorIconManager extends IconManager { } } - // Refresh icons in the active leaf - this.plugin.registerEvent(this.app.workspace.on('active-leaf-change', () => { - this.refreshIcons(); - })); - // If we add a new property to a file, refresh property icons this.plugin.registerEvent(this.app.vault.on('modify', () => { this.refreshIcons(); })); } + /** + * Refresh title icon whenever editing mode changes. + */ + private observeEditingMode(view: MarkdownView): void { + this.setMutationObserver(view.containerEl, { attributes: true }, mutation => { + if (mutation.attributeName === 'data-mode') { + this.refreshTitleIcon(view); + } + }); + } + /** * Refresh whenever a given MarkdownView needs to redraw its icons. */ private observeViewIcons(view: MarkdownView): void { - // Note container - this.observeContainer(view.containerEl, view); + // Editing mode + this.observeEditingMode(view); // Properties list // @ts-expect-error (Private API) @@ -95,21 +97,12 @@ export default class EditorIconManager extends IconManager { if (!propsEl) return; this.observeProperties(propsEl, view); - // "Tags" property + // `tags` property const tagsEl: HTMLElement = propsEl.find('.metadata-property[data-property-key="tags"] .multi-select-container'); if (!tagsEl) return; this.observeTagsProperty(tagsEl, view); } - /** - * Refresh whenever a given container switches edit modes. - */ - private observeContainer(containerEl: HTMLElement, view: MarkdownView): void { - this.setMutationsObserver(containerEl, { attributeFilter: ['data-mode'] }, () => { - this.refreshViewIcons(view); - }); - } - /** * Refresh whenever a given properties list needs to redraw its icons. */ @@ -131,7 +124,7 @@ export default class EditorIconManager extends IconManager { }); this.setEventListener(propsEl, 'click', event => { - const pointEls = activeDocument.elementsFromPoint(event.x, event.y); + const pointEls = event.doc.elementsFromPoint(event.x, event.y); const iconEl = pointEls.find(el => el.hasClass('metadata-property-icon')); const propEl = pointEls.find(el => el.hasClass('metadata-property')); if (iconEl && propEl instanceof HTMLElement) { @@ -141,8 +134,7 @@ export default class EditorIconManager extends IconManager { if (this.plugin.isSettingEnabled('clickableIcons')) { IconPicker.openSingle(this.plugin, prop, (newIcon, newColor) => { this.plugin.savePropertyIcon(prop, newIcon, newColor); - this.refreshIcons(); - this.plugin.propertyIconManager?.refreshIcons(); + this.plugin.refreshManagers('property'); }); event.stopPropagation(); } else { @@ -153,7 +145,7 @@ export default class EditorIconManager extends IconManager { if (this.plugin.settings.showMenuActions) { this.setEventListener(propsEl, 'contextmenu', event => { - const pointEls = activeDocument.elementsFromPoint(event.x, event.y); + const pointEls = event.doc.elementsFromPoint(event.x, event.y); const iconEl = pointEls.find(el => el.hasClass('metadata-property-icon')); const propEl = pointEls.find(el => el.hasClass('metadata-property')); if (iconEl && propEl instanceof HTMLElement) { @@ -169,13 +161,14 @@ export default class EditorIconManager extends IconManager { } /** - * Refresh whenever the "tags" property changes. + * Refresh whenever the `tags` property changes. */ private observeTagsProperty(tagsEl: HTMLElement, view: MarkdownView): void { this.setMutationsObserver(tagsEl, { childList: true }, () => this.refreshViewIcons(view)); } /** + * @override * Refresh all icons in all MarkdownViews. */ refreshIcons(unloading?: boolean): void { @@ -190,14 +183,115 @@ export default class EditorIconManager extends IconManager { * Refresh all icons in a single MarkdownView. */ refreshViewIcons(view: MarkdownView, unloading?: boolean): void { - // Trigger markdown post-processor - if (view.currentMode instanceof MarkdownPreviewView) { - view.currentMode.rerender(true); - } + // Refresh title icon + this.refreshTitleIcon(view, unloading); + + // Refresh property icons const props = this.plugin.getPropertyItems(unloading); - const tags = this.plugin.getTagItems(unloading); this.refreshPropertyIcons(props, view); - this.refreshTagIcons(tags, view); + + // Refresh `tags` property + const tags = this.plugin.getTagItems(unloading); + this.refreshTagsPropertyIcons(tags, view, unloading); + + // Refresh hashtags + const tagEls = view.containerEl.findAll('a.tag'); + this.refreshReadingHashtags(tags, tagEls, unloading); + this.refreshLivePreviewHashtags(view.editor); + } + + /** + * Refresh inline title icon. + */ + private refreshTitleIcon(view: MarkdownView, unloading?: boolean): void { + if (!view.file) return; + // @ts-expect-error (Private API) + const titleEl = view.inlineTitleEl; + if (!(titleEl instanceof HTMLElement)) return; + const headerEl = titleEl.closest('.mod-header, .cm-sizer'); + if (!(headerEl instanceof HTMLElement)) return; + + // Remove wrapper if necessary + if (!this.plugin.settings.showTitleIcons || unloading) { + const wrapperEl = titleEl.closest('.iconic-title-wrapper'); + if (wrapperEl) { + headerEl.prepend(titleEl); + wrapperEl.remove(); + } + return; + } + + // Set up title wrapper + const wrapperEl = headerEl.find(':scope > .iconic-title-wrapper') + ?? createDiv({ cls: 'iconic-title-wrapper' }); + const iconEl = wrapperEl.find(':scope > .iconic-icon') + ?? createDiv({ cls: 'iconic-icon' });; + wrapperEl.append(iconEl, titleEl); + headerEl.prepend(wrapperEl); + + // Get file and/or rule icon + const file = this.plugin.getFileItem(view.file.path); + const rule = this.plugin.ruleManager.checkRuling('file', file.id) ?? file; + if (!rule.icon && !rule.color) file.iconDefault = null; + + // Refresh icon + if (this.plugin.isSettingEnabled('clickableIcons')) { + this.refreshIcon(rule, iconEl, () => { + IconPicker.openSingle(this.plugin, file, (newIcon, newColor) => { + this.plugin.saveFileIcon(file, newIcon, newColor); + this.plugin.refreshManagers('file'); + }); + }); + } else { + this.refreshIcon(rule, iconEl); + } + iconEl.addClass('iconic-icon'); + + // Add menu actions + if (this.plugin.settings.showMenuActions) { + this.setEventListener(iconEl, 'contextmenu', event => { + navigator?.vibrate(100); // Not supported on iOS + const menu = new Menu(); + menu.addItem(item => item + .setTitle(STRINGS.menu.changeIcon) + .setIcon('lucide-image-plus') + .setSection('icon') + .onClick(() => { + IconPicker.openSingle(this.plugin, file, (newIcon, newColor) => { + this.plugin.saveFileIcon(file, newIcon, newColor); + this.plugin.refreshManagers('file', 'folder'); + }); + }) + ); + if (file.icon || file.color) menu.addItem(item => item + .setTitle(STRINGS.menu.removeIcon) + .setIcon('lucide-image-minus') + .setSection('icon') + .onClick(() => { + this.plugin.saveFileIcon(file, null, null); + this.plugin.refreshManagers('file'); + }) + ); + const rule = this.plugin.ruleManager.checkRuling('file', file.id); + if (rule) menu.addItem(item => { item + .setTitle('Edit rule...') + .setIcon('lucide-image-play') + .setSection('icon') + .onClick(() => RuleEditor.open(this.plugin, 'file', rule, newRule => { + const isRulingChanged = newRule + ? this.plugin.ruleManager.saveRule('file', newRule) + : this.plugin.ruleManager.deleteRule('file', rule.id); + if (isRulingChanged) { + this.refreshIcons(); + this.plugin.refreshManagers('file'); + } + })); + }); + menu.showAtPosition(event); + }); + } else { + this.stopEventListener(iconEl, 'contextmenu'); + } } /** @@ -220,89 +314,110 @@ export default class EditorIconManager extends IconManager { } /** - * Refresh all tag icons in a single MarkdownView. - */ - private refreshTagIcons(tags: TagItem[], view: MarkdownView): void { + * Refresh all tag icons in the `tags` property. + */ + private refreshTagsPropertyIcons(tags: TagItem[], view: MarkdownView, unloading?: boolean): void { // @ts-expect-error (Private API) const propListEl: HTMLElement = view.metadataEditor?.propertyListEl; if (!propListEl) return; const propTagEls = view.contentEl.findAll('.metadata-property[data-property-key="tags"] .multi-select-pill'); if (!propTagEls) return; - // "Tags" property + // Refresh each tag pill for (const propTagEl of propTagEls) { const tagId = propTagEl.find(':scope > .multi-select-pill-content')?.getText(); if (!tagId) continue; - const tag = tags.find(tag => tag.id === tagId); - if (!tag) continue; - if (tag.icon) { - const iconEl = propTagEl.find('.iconic-icon') ?? createSpan(); - if (iconEl !== propTagEl.firstChild) { - propTagEl.insertBefore(iconEl, propTagEl.firstChild); - } - if (this.plugin.isSettingEnabled('clickableIcons')) { - this.refreshIcon(tag, iconEl, event => { - IconPicker.openSingle(this.plugin, tag, (newIcon, newColor) => { - this.plugin.saveTagIcon(tag, newIcon, newColor); - this.refreshIcons(); - this.plugin.tagIconManager?.refreshIcons(); - }); - event.stopPropagation(); + const tag = tags.find(tag => tag.id === tagId) ?? null; + this.refreshTag(propTagEl, tag, () => { + if (tag) this.onTagContextMenu(tag.id); + }, unloading); + } + } + + /** + * Refresh all hashtag elements in reading mode. + */ + private refreshReadingHashtags(tags: TagItem[], tagEls: HTMLElement[], unloading?: boolean): void { + for (const tagEl of tagEls) { + const tagId = tagEl.getAttribute('href')?.replace('#', ''); + if (!tagId) continue; + const tag = tags.find(tag => tag.id === tagId) ?? null; + this.refreshTag(tagEl, tag, event => { + if (tag) this.onCreateTagContextMenu(tag.id, event); + }, unloading); + } + } + + /** + * Refresh all hashtag elements in live preview mode. + */ + private refreshLivePreviewHashtags(editor: Editor): void { + editor.replaceRange('', editor.getCursor()); + } + + /** + * Refresh a given tag pill element. + */ + private refreshTag(tagEl: HTMLElement, tag: TagItem | null, onContextMenu: (event: MouseEvent) => void, unloading?: boolean): void { + // Remove styling if necessary + if (!this.plugin.settings.showTagPillIcons || !tag || unloading) { + tagEl.find('.iconic-icon')?.remove(); + this.setTagColor(tagEl, null); + this.stopEventListener(tagEl, 'contextmenu'); + return; + } + + // Set icon & color + if (tag.icon) { + const iconEl = tagEl.find('.iconic-icon') ?? createSpan(); + tagEl.prepend(iconEl); + if (tag && this.plugin.isSettingEnabled('clickableIcons')) { + this.refreshIcon(tag, iconEl, event => { + IconPicker.openSingle(this.plugin, tag, (newIcon, newColor) => { + this.plugin.saveTagIcon(tag, newIcon, newColor); + this.plugin.refreshManagers('tag'); }); - } else { - this.refreshIcon(tag, iconEl); - } - } else { - const iconEl = propTagEl.find('.iconic-icon'); - iconEl?.remove(); - } - EditorIconManager.setTagColor(tag, propTagEl); - if (this.plugin.settings.showMenuActions) { - this.setEventListener(propTagEl, 'contextmenu', () => this.onTagContextMenu(tag.id)); + event.stopPropagation(); + }); } else { - this.stopEventListener(propTagEl, 'contextmenu'); + this.refreshIcon(tag, iconEl); } + } else { + const iconEl = tagEl.find('.iconic-icon'); + iconEl?.remove(); } + this.setTagColor(tagEl, tag?.color ?? null); - // Hashtags (editing mode) - if (view.getMode() === 'source') { - let editingViewEl: HTMLElement | undefined; - for (const childEl of view.contentEl.children) { - if (childEl instanceof HTMLElement && childEl.hasClass('markdown-source-view')) { - editingViewEl = childEl; - break; - } - } - const tagEndEls = editingViewEl?.findAll('.cm-hashtag-end') ?? []; - for (const tagEndEl of tagEndEls) { - const tagId = tagEndEl.getText(); - if (!tagId) continue; - const tag = tags.find(tag => tag.id === tagId); - if (!tag) continue; - // Decorate 1st half of tag - const tagBeginEl = tagEndEl.previousElementSibling; - if (tagBeginEl instanceof HTMLElement && tagBeginEl.hasClass('cm-hashtag-begin')) { - EditorIconManager.setTagColor(tag, tagBeginEl); - if (this.plugin.settings.showMenuActions) { - this.setEventListener(tagBeginEl, 'contextmenu', event => { - if (Platform.isDesktop) this.onTagContextMenu(tag.id, true); - if (Platform.isMobile) this.onTagNewContextMenu(tag.id, event); - }); - } else { - this.stopEventListener(tagBeginEl, 'contextmenu'); - } - } - // Decorate 2nd half of tag - EditorIconManager.setTagColor(tag, tagEndEl); - if (this.plugin.settings.showMenuActions) { - this.setEventListener(tagEndEl, 'contextmenu', event => { - if (Platform.isDesktop) this.onTagContextMenu(tag.id, true); - if (Platform.isMobile) this.onTagNewContextMenu(tag.id, event); - }); - } else { - this.stopEventListener(tagEndEl, 'contextmenu'); - } - } + // Set menu actions + if (this.plugin.settings.showMenuActions) { + this.setEventListener(tagEl, 'contextmenu', event => onContextMenu(event)); + } else { + this.stopEventListener(tagEl, 'contextmenu'); + } + } + + /** + * Apply a tag color to a tag pill element. + */ + private setTagColor(tagEl: HTMLElement, color: string | null): void { + if (color) { + const cssRgb = ColorUtils.toRgb(color); + const cssRgba = cssRgb.replace('rgb(', 'rgba(').replace(')', ''); + tagEl.style.setProperty('--tag-color', cssRgb); + tagEl.style.setProperty('--tag-color-hover', cssRgb); + tagEl.style.setProperty('--tag-color-remove-hover', cssRgb); + tagEl.style.setProperty('--tag-background', cssRgba + ', 0.1)'); + tagEl.style.setProperty('--tag-background-hover', cssRgba + ', 0.1)'); + tagEl.style.setProperty(`--tag-border-color`, cssRgba + ', 0.25)'); + tagEl.style.setProperty(`--tag-border-color-hover`, cssRgba + ', 0.5)'); + } else { + tagEl.style.removeProperty('--tag-color'); + tagEl.style.removeProperty('--tag-color-hover'); + tagEl.style.removeProperty('--tag-color-remove-hover'); + tagEl.style.removeProperty('--tag-background'); + tagEl.style.removeProperty('--tag-background-hover'); + tagEl.style.removeProperty(`--tag-border-color`); + tagEl.style.removeProperty(`--tag-border-color-hover`); } } @@ -310,7 +425,7 @@ export default class EditorIconManager extends IconManager { * When user context-clicks a property, add custom items to the menu. */ private onPropertyContextMenu(propId: string): void { - navigator?.vibrate(100); // Might not be supported on iOS + navigator?.vibrate(100); // Not supported on iOS this.plugin.menuManager.closeAndFlush(); const prop = this.plugin.getPropertyItem(propId); @@ -321,8 +436,7 @@ export default class EditorIconManager extends IconManager { .setSection('icon') .onClick(() => IconPicker.openSingle(this.plugin, prop, (newIcon, newColor) => { this.plugin.savePropertyIcon(prop, newIcon, newColor); - this.refreshIcons(); - this.plugin.propertyIconManager?.refreshIcons(); + this.plugin.refreshManagers('property'); })) ); @@ -334,8 +448,7 @@ export default class EditorIconManager extends IconManager { .setSection('icon') .onClick(() => { this.plugin.savePropertyIcon(prop, null, null); - this.refreshIcons(); - this.plugin.propertyIconManager?.refreshIcons(); + this.plugin.refreshManagers('property'); }) ); } @@ -345,7 +458,6 @@ export default class EditorIconManager extends IconManager { * When user context-clicks a tag, add custom items to the menu. */ private onTagContextMenu(tagId: string, isEditingMode?: boolean): void { - navigator?.vibrate(100); // Not supported on iOS this.plugin.menuManager.closeAndFlush(); const tag = this.plugin.getTagItem(tagId); if (!tag) return; @@ -357,8 +469,7 @@ export default class EditorIconManager extends IconManager { .setSection('icon') .onClick(() => IconPicker.openSingle(this.plugin, tag, (newIcon, newColor) => { this.plugin.saveTagIcon(tag, newIcon, newColor); - this.refreshIcons(); - this.plugin.tagIconManager?.refreshIcons(); + this.plugin.refreshManagers('tag'); })) ); @@ -370,91 +481,26 @@ export default class EditorIconManager extends IconManager { .setSection('icon') .onClick(() => { this.plugin.saveTagIcon(tag, null, null); - this.refreshIcons(); - this.plugin.tagIconManager?.refreshIcons(); + this.plugin.refreshManagers('tag'); }) ); } } /** - * When user context-clicks a tag, open a menu. + * When user context-clicks a tag without a menu, create a new one. */ - private onTagNewContextMenu(tagId: string, event: MouseEvent): void { + private onCreateTagContextMenu(tagId: string, event: MouseEvent): void { + navigator?.vibrate(100); // Not supported on iOS this.plugin.tagIconManager?.onContextMenu(tagId, event); } - /** - * Refresh all icons in a property suggestions menu. - */ - private onPropertySuggestionMenu(suggestMenuEl: HTMLElement): void { - this.stopMutationObserver(suggestMenuEl); - - const propEls = suggestMenuEl.findAll(':scope > .suggestion > .suggestion-item'); - for (const propEl of propEls) { - const propId = propEl.find(':scope > .suggestion-content > .suggestion-title')?.getText(); - if (propId) { - const prop = this.plugin.getPropertyItem(propId); - const iconEl = propEl.find(':scope > .suggestion-icon > .suggestion-flair'); - if (iconEl) this.refreshIcon(prop, iconEl); - } - } - - this.setMutationsObserver(suggestMenuEl, { - subtree: true, - childList: true, - }, () => this.onPropertySuggestionMenu(suggestMenuEl)); - } - - /** - * Refresh all icons in a tag suggestions menu. - */ - private onTagSuggestionMenu(suggestMenuEl: HTMLElement): void { - this.stopMutationObserver(suggestMenuEl); - - const tagEls = suggestMenuEl.findAll(':scope > .suggestion > .suggestion-item'); - const tags = this.plugin.getTagItems(); - for (const tagEl of tagEls) { - const tagId = tagEl.getText(); - const tag = tags.find(tag => tag.id === tagId); - if (tag) { - tagEl.addClass('mod-complex'); - tagEl.empty(); - const iconEl = tagEl.createDiv({ cls: 'suggestion-icon' }).createSpan({ cls: 'suggestion-flair' }); - tagEl.createDiv({ cls: 'suggestion-content' }).createDiv({ cls: 'suggestion-title', text: tagId }); - if (iconEl) this.refreshIcon(tag, iconEl); - } - } - - this.setMutationsObserver(suggestMenuEl, { - subtree: true, - childList: true, - }, () => this.onTagSuggestionMenu(suggestMenuEl)); - } - - private static setTagColor(tag: TagItem, tagEl: HTMLElement): void { - if (tag.color) { - const cssRgb = ColorUtils.toRgb(tag.color); - const cssRgba = cssRgb.replace('rgb(', 'rgba(').replace(')', ''); - tagEl.style.setProperty('color', cssRgb); - tagEl.style.setProperty('background-color', cssRgba + ', 0.1)'); - tagEl.style.setProperty(`--tag-border-color`, cssRgba + ', 0.25)'); - tagEl.style.setProperty(`--tag-border-color-hover`, cssRgba + ', 0.5)'); - if (tagEl.hasClass('multi-select-pill')) tagEl.style.setProperty(`--pill-color-remove`, cssRgb); - } else { - tagEl.style.removeProperty('color'); - tagEl.style.removeProperty('background-color'); - tagEl.style.removeProperty(`--tag-border-color`); - tagEl.style.removeProperty(`--tag-border-color-hover`); - if (tagEl.hasClass('multi-select-pill')) tagEl.style.removeProperty(`--pill-color-remove`); - } - } - /** * @override */ unload(): void { this.refreshIcons(true); + this.stopMutationObservers(); super.unload(); } } diff --git a/src/managers/FileIconManager.ts b/src/managers/FileIconManager.ts index de7cc71..2728320 100644 --- a/src/managers/FileIconManager.ts +++ b/src/managers/FileIconManager.ts @@ -62,6 +62,7 @@ export default class FileIconManager extends IconManager { } /** + * @override * Refresh all file icons. */ refreshIcons(unloading?: boolean): void { @@ -169,9 +170,7 @@ export default class FileIconManager extends IconManager { this.refreshIcon(rule, iconEl, event => { IconPicker.openSingle(this.plugin, file, (newIcon, newColor) => { this.plugin.saveFileIcon(file, newIcon, newColor); - this.refreshIcons(); - this.plugin.tabIconManager?.refreshIcons(); - this.plugin.bookmarkIconManager?.refreshIcons(); + this.plugin.refreshManagers('file', 'folder'); }); event.stopPropagation(); }); @@ -229,16 +228,12 @@ export default class FileIconManager extends IconManager { if (files.length === 1) { IconPicker.openSingle(this.plugin, files[0], (newIcon, newColor) => { this.plugin.saveFileIcon(files[0], newIcon, newColor); - this.refreshIcons(); - this.plugin.tabIconManager?.refreshIcons(); - this.plugin.bookmarkIconManager?.refreshIcons(); + this.plugin.refreshManagers('file', 'folder'); }); } else { IconPicker.openMulti(this.plugin, files, (newIcon, newColor) => { this.plugin.saveFileIcons(files, newIcon, newColor); - this.refreshIcons(); - this.plugin.tabIconManager?.refreshIcons(); - this.plugin.bookmarkIconManager?.refreshIcons(); + this.plugin.refreshManagers('file', 'folder'); }); } }) @@ -266,9 +261,7 @@ export default class FileIconManager extends IconManager { } else { this.plugin.saveFileIcons(files, null, null); } - this.refreshIcons(); - this.plugin.tabIconManager?.refreshIcons(); - this.plugin.bookmarkIconManager?.refreshIcons(); + this.plugin.refreshManagers('file', 'folder'); }) ); } @@ -288,8 +281,7 @@ export default class FileIconManager extends IconManager { : this.plugin.ruleManager.deleteRule(page, rule.id); if (isRulingChanged) { this.refreshIcons(); - if (page === 'file') this.plugin.tabIconManager?.refreshIcons(); - this.plugin.bookmarkIconManager?.refreshIcons(); + this.plugin.refreshManagers(page); } })); }); diff --git a/src/managers/IconManager.ts b/src/managers/IconManager.ts index f2479e7..450bf49 100644 --- a/src/managers/IconManager.ts +++ b/src/managers/IconManager.ts @@ -18,6 +18,13 @@ export default abstract class IconManager { this.plugin = plugin; } + /** + * Refresh all icons controlled by this icon manager. Should be overridden. + */ + refreshIcons(unloading?: boolean): void { + return; + } + /** * Refresh icon inside a given element. */ diff --git a/src/managers/MenuManager.ts b/src/managers/MenuManager.ts index b7dee60..8683423 100644 --- a/src/managers/MenuManager.ts +++ b/src/managers/MenuManager.ts @@ -1,4 +1,4 @@ -import { Menu, MenuItem } from 'obsidian'; +import { Menu, MenuItem, MenuPositionDef } from 'obsidian'; /** * Intercepts context menus to add custom items. @@ -11,11 +11,13 @@ export default class MenuManager { constructor() { const manager = this; + + // Store original method this.showAtPositionOriginal = Menu.prototype.showAtPosition; // Catch menus as they open this.showAtPositionProxy = new Proxy(Menu.prototype.showAtPosition, { - apply(showAtPosition, menu, args) { + apply(showAtPosition, menu: Menu, args: [position: MenuPositionDef, doc?: Document]) { manager.menu = menu; if (manager.queuedActions.length > 0) { manager.runQueuedActions.call(manager); // Menu is unhappy with your customer service diff --git a/src/managers/PropertyIconManager.ts b/src/managers/PropertyIconManager.ts index b193b5e..5c3edfd 100644 --- a/src/managers/PropertyIconManager.ts +++ b/src/managers/PropertyIconManager.ts @@ -44,6 +44,7 @@ export default class PropertyIconManager extends IconManager { } /** + * @override * Refresh all property icons. */ refreshIcons(unloading?: boolean): void { @@ -65,8 +66,7 @@ export default class PropertyIconManager extends IconManager { this.refreshIcon(prop, iconEl, event => { IconPicker.openSingle(this.plugin, prop, (newIcon, newColor) => { this.plugin.savePropertyIcon(prop, newIcon, newColor); - this.refreshIcons(); - this.plugin.editorIconManager?.refreshIcons(); + this.plugin.refreshManagers('property'); }); event.stopPropagation(); }); @@ -120,14 +120,12 @@ export default class PropertyIconManager extends IconManager { if (selectedProps.length < 2) { IconPicker.openSingle(this.plugin, clickedProp, (newIcon, newColor) => { this.plugin.savePropertyIcon(clickedProp, newIcon, newColor); - this.refreshIcons(); - this.plugin.editorIconManager?.refreshIcons(); + this.plugin.refreshManagers('property'); }); } else { IconPicker.openMulti(this.plugin, selectedProps, (newIcon, newColor) => { this.plugin.savePropertyIcons(selectedProps, newIcon, newColor); - this.refreshIcons(); - this.plugin.editorIconManager?.refreshIcons(); + this.plugin.refreshManagers('property'); }); } }) @@ -155,8 +153,7 @@ export default class PropertyIconManager extends IconManager { } else { this.plugin.savePropertyIcons(selectedProps, null, null); } - this.refreshIcons(); - this.plugin.editorIconManager?.refreshIcons(); + this.plugin.refreshManagers('property'); }) ); } diff --git a/src/managers/QuickSwitcherIconManager.ts b/src/managers/QuickSwitcherIconManager.ts index 5e42ed0..014c635 100644 --- a/src/managers/QuickSwitcherIconManager.ts +++ b/src/managers/QuickSwitcherIconManager.ts @@ -1,4 +1,4 @@ -import { Plugin, SuggestModal, TFile, WorkspaceLeaf } from 'obsidian'; +import { Instruction, Plugin, SuggestModal, TFile, WorkspaceLeaf } from 'obsidian'; import IconicPlugin, { FILE_TAB_TYPES } from 'src/IconicPlugin'; import IconManager from 'src/managers/IconManager'; @@ -8,7 +8,7 @@ type PluginModal = SuggestModal & { plugin: Plugin }; * Allow type-safe access to a modal.plugin property. */ function isPluginModal(modal: SuggestModal): modal is PluginModal { - return (modal as SuggestModal & { plugin: unknown }).plugin instanceof Plugin; + return (modal as PluginModal).plugin instanceof Plugin; } const QUICK_SWITCHER = 'qs'; @@ -27,6 +27,8 @@ export default class QuickSwitcherIconManager extends IconManager { constructor(plugin: IconicPlugin) { super(plugin); const manager = this; + + // Store original methods this.onOpenOriginal = SuggestModal.prototype.onOpen; this.setInstructionsOriginal = SuggestModal.prototype.setInstructions; @@ -44,17 +46,16 @@ export default class QuickSwitcherIconManager extends IconManager { // Proxy renderSuggestion() for each instance modal.renderSuggestion = new Proxy(modal.renderSuggestion, { - apply(renderSuggestion, modal, args) { - const [value, el] = args; + apply(renderSuggestion, modal: SuggestModal, args: [any, HTMLElement]) { switch (modalType) { case QUICK_SWITCHER: { modal.modalEl.addClass('iconic-quick-switcher'); - manager.refreshSuggestionIcon(value, el); + manager.refreshSuggestionIcon(...args); break; } case QUICK_SWITCHER_PP: { modal.modalEl.addClass('iconic-quick-switcher'); - manager.refreshSuggestionIconQSPP(value, el); + manager.refreshSuggestionIconQSPP(...args); break; } } @@ -68,7 +69,7 @@ export default class QuickSwitcherIconManager extends IconManager { // Catch Another Quick Switcher modals, which never call super.onOpen() this.setInstructionsProxy = new Proxy(SuggestModal.prototype.setInstructions, { - apply(setInstructions, modal, args) { + apply(setInstructions, modal: SuggestModal, args: [Instruction[]]) { if (manager.isDisabled()) { return setInstructions.call(modal, ...args); } @@ -80,16 +81,15 @@ export default class QuickSwitcherIconManager extends IconManager { // Proxy renderSuggestion() for every instance modal.renderSuggestion = new Proxy(modal.renderSuggestion, { - apply(renderSuggestion, modal, args) { + apply(renderSuggestion, modal: SuggestModal, args: [any, HTMLElement]) { if (manager.isDisabled()) { return renderSuggestion.call(modal, ...args); } - const [value, el] = args; - // Call the base method first so custom elements are available + // Call base method first to pre-populate elements const returnValue = renderSuggestion.call(modal, ...args); modal.modalEl.addClass('iconic-another-quick-switcher'); - // Refresh s - manager.refreshSuggestionIconAQS(value, el); + // Refresh suggestions + manager.refreshSuggestionIconAQS(...args); return returnValue; } }); @@ -161,6 +161,7 @@ export default class QuickSwitcherIconManager extends IconManager { */ private refreshSuggestionIconQSPP(value: any, el: HTMLElement): void { switch (value?.type) { + case 'relatedItemsList': // Fallthrough case 'file': { if (value.file instanceof TFile) { const file = this.plugin.getFileItem(value.file.path); diff --git a/src/managers/RibbonIconManager.ts b/src/managers/RibbonIconManager.ts index 988e805..58afb8d 100644 --- a/src/managers/RibbonIconManager.ts +++ b/src/managers/RibbonIconManager.ts @@ -1,6 +1,6 @@ import { Menu, Platform } from 'obsidian'; import IconicPlugin, { RibbonItem, STRINGS } from 'src/IconicPlugin'; -import MenuManager from './MenuManager'; +import MenuManager from 'src/managers/MenuManager'; import IconManager from 'src/managers/IconManager'; import IconPicker from 'src/dialogs/IconPicker'; @@ -55,6 +55,7 @@ export default class RibbonIconManager extends IconManager { } /** + * @override * Refresh all ribbon icons. */ refreshIcons(unloading?: boolean): void { @@ -117,7 +118,7 @@ export default class RibbonIconManager extends IconManager { if (Platform.isPhone) { const quickDropdownEl = containerEl.find('.setting-item-control > .dropdown'); if (quickDropdownEl) this.setEventListener(quickDropdownEl, 'change', () => { - this.refreshIcons(); + this.plugin.refreshManagers('ribbon'); this.refreshConfigIcons(containerEl); }); @@ -129,8 +130,8 @@ export default class RibbonIconManager extends IconManager { this.refreshIcon(quickItem, quickIconEl, () => { IconPicker.openSingle(this.plugin, quickItem, (newIcon, newColor) => { this.plugin.saveRibbonIcon(quickItem, newIcon, newColor); + this.plugin.refreshManagers('ribbon'); this.refreshConfigIcons(containerEl); - this.refreshIcons(); }); }); } @@ -159,7 +160,7 @@ export default class RibbonIconManager extends IconManager { this.refreshIcon(item, iconEl, event => { IconPicker.openSingle(this.plugin, item, (newIcon, newColor) => { this.plugin.saveRibbonIcon(item, newIcon, newColor); - this.refreshIcons(); + this.plugin.refreshManagers('ribbon'); this.refreshConfigIcons(containerEl); }); event.stopPropagation(); @@ -192,7 +193,7 @@ export default class RibbonIconManager extends IconManager { .setSection('icon') .onClick(() => IconPicker.openSingle(this.plugin, ribbonItem, (newIcon, newColor) => { this.plugin.saveRibbonIcon(ribbonItem, newIcon, newColor); - this.refreshIcons(); + this.plugin.refreshManagers('ribbon'); })) ); @@ -204,7 +205,7 @@ export default class RibbonIconManager extends IconManager { .setSection('icon') .onClick(() => { this.plugin.saveRibbonIcon(ribbonItem, null, null); - this.refreshIcons(); + this.plugin.refreshManagers('ribbon'); }) ); } diff --git a/src/managers/RuleManager.ts b/src/managers/RuleManager.ts index 2787533..1589e40 100644 --- a/src/managers/RuleManager.ts +++ b/src/managers/RuleManager.ts @@ -45,9 +45,7 @@ export default class RuleManager { if (this.fileTriggers.has('time') || this.fileTriggers.has('date') && isMidnight) { const isRulingChanged = this.triggerRulings('file', 'time'); if (isRulingChanged) { - this.plugin.tabIconManager?.refreshIcons(); - this.plugin.fileIconManager?.refreshIcons(); - this.plugin.bookmarkIconManager?.refreshIcons(); + this.plugin.refreshManagers('file'); } } } diff --git a/src/managers/SuggestionIconManager.ts b/src/managers/SuggestionIconManager.ts new file mode 100644 index 0000000..9de8d2d --- /dev/null +++ b/src/managers/SuggestionIconManager.ts @@ -0,0 +1,219 @@ +import { AbstractInputSuggest, EditorSuggest } from 'obsidian'; +import IconicPlugin from 'src/IconicPlugin'; +import IconManager from 'src/managers/IconManager'; + +const PROPERTY_SUGGESTION = 'property'; +const TAG_SUGGESTION = 'tag'; + +/** + * Intercepts suggestion popovers to add custom icons. + */ +export default class SuggestionIconManager extends IconManager { + // @ts-expect-error (Private API) + private showAbstractSuggestionsOriginal: typeof AbstractInputSuggest.prototype.showSuggestions; + // @ts-expect-error (Private API) + private showAbstractSuggestionsProxy: typeof AbstractInputSuggest.prototype.showSuggestions; + private renderAbstractSuggestionProxy: typeof AbstractInputSuggest.prototype.renderSuggestion; + + // @ts-expect-error (Private API) + private showEditorSuggestionsOriginal: typeof AbstractInputSuggest.prototype.showSuggestions; + // @ts-expect-error (Private API) + private showEditorSuggestionsProxy: typeof AbstractInputSuggest.prototype.showSuggestions; + private renderEditorSuggestionProxy: typeof AbstractInputSuggest.prototype.renderSuggestion; + + constructor(plugin: IconicPlugin) { + super(plugin); + this.setupAbstractSuggestionProxies(); + this.setupEditorSuggestionProxies(); + } + + /** + * Intercept property key/value suggestion popovers. + */ + private setupAbstractSuggestionProxies(): void { + const manager = this; + + // Store original method + // @ts-expect-error (Private API) + this.showAbstractSuggestionsOriginal = AbstractInputSuggest.prototype.showSuggestions; + + // Catch popovers before they open + // @ts-expect-error (Private API) + this.showAbstractSuggestionsProxy = new Proxy(AbstractInputSuggest.prototype.showSuggestions, { + apply(showSuggestions, popover: AbstractInputSuggest, args) { + if (manager.isDisabled()) { + return showSuggestions.call(popover, ...args); + } + + // Proxy renderSuggestion() for each instance + if (popover.renderSuggestion !== manager.renderAbstractSuggestionProxy) { + manager.renderAbstractSuggestionProxy = new Proxy(popover.renderSuggestion, { + apply(renderSuggestion, popover: AbstractInputSuggest, args: [any, HTMLElement]) { + // Call base method first to pre-populate elements + const returnValue = renderSuggestion.call(popover, ...args); + if (manager.isDisabled()) return returnValue; + + const [value, el] = args; + if (!value || !(el instanceof HTMLElement)) return; + + switch (manager.getSuggestionType(value)) { + case PROPERTY_SUGGESTION: manager.refreshPropertyIcon(value, el); break; + case TAG_SUGGESTION: manager.refreshTagIcon(value, el); break; + } + + return returnValue; + } + }); + + // Replace original method + popover.renderSuggestion = manager.renderAbstractSuggestionProxy; + } + + return showSuggestions.call(popover, ...args); + } + }); + + // @ts-expect-error (Private API) + // Replace original method + AbstractInputSuggest.prototype.showSuggestions = this.showAbstractSuggestionsProxy; + } + + /** + * Intercept editor suggestion popovers. + */ + private setupEditorSuggestionProxies(): void { + const manager = this; + + // Store original method + // @ts-expect-error (Private API) + this.showEditorSuggestionsOriginal = EditorSuggest.prototype.showSuggestions; + + // Catch popovers before they open + // @ts-expect-error (Private API) + this.showEditorSuggestionsProxy = new Proxy(EditorSuggest.prototype.showSuggestions, { + apply(showSuggestions, popover: EditorSuggest, args) { + if (manager.isDisabled()) { + return showSuggestions.call(popover, ...args); + } + + // Proxy renderSuggestion() for each instance + if (popover.renderSuggestion !== manager.renderEditorSuggestionProxy) { + manager.renderEditorSuggestionProxy = new Proxy(popover.renderSuggestion, { + apply(renderSuggestion, popover: EditorSuggest, args: [any, HTMLElement]) { + // Call base method first to pre-populate elements + const returnValue = renderSuggestion.call(popover, ...args); + if (manager.isDisabled()) return returnValue; + + const [value, el] = args; + if (!value || !(el instanceof HTMLElement)) return; + + switch (manager.getSuggestionType(value)) { + case PROPERTY_SUGGESTION: manager.refreshPropertyIcon(value, el); break; + case TAG_SUGGESTION: manager.refreshTagIcon(value, el); break; + } + + return returnValue; + } + }); + + // Replace original method + popover.renderSuggestion = manager.renderEditorSuggestionProxy; + } + + return showSuggestions.call(popover, ...args); + } + }); + + // @ts-expect-error (Private API) + // Replace original method + EditorSuggest.prototype.showSuggestions = this.showEditorSuggestionsProxy; + } + + /** + * Determine which type of suggestion this is. + */ + private getSuggestionType(value: any): string | null { + if ('tag' in value) return TAG_SUGGESTION; + if ('text' in value && 'type' in value) return PROPERTY_SUGGESTION; + return null; + } + + /** + * Refresh a property suggestion icon. + */ + private refreshPropertyIcon(value: any, el: HTMLElement): void { + switch (value?.type) { + // Property suggestions + case 'text': { + const propId = value?.text; + if (propId) { + const prop = this.plugin.getPropertyItem(propId); + const iconEl = el.find(':scope > .suggestion-icon > .suggestion-flair'); + if (iconEl) this.refreshIcon(prop, iconEl); + } + break; + } + // BASES: File attribute suggestions + case 'file': break; + // BASES: Formula suggestions + case 'formula': break; + // BASES: Property suggestions + case 'note': { + const propId = value?.name; + if (propId) { + const prop = this.plugin.getPropertyItem(propId); + const iconEl = el.find(':scope > .suggestion-icon > .suggestion-flair'); + if (iconEl) this.refreshIcon(prop, iconEl); + } + break; + } + } + } + + /** + * Refresh a tag suggestion icon. + */ + private refreshTagIcon(value: any, el: HTMLElement): void { + const tagId = value?.tag; + if (tagId) { + el.addClass('mod-complex', 'iconic-item'); + const tag = this.plugin.getTagItem(tagId); + const iconContainerEl = el.find(':scope > .suggestion-icon') + ?? createDiv({ cls: 'suggestion-icon' }); + const iconEl = iconContainerEl.find(':scope > .suggestion-flair') + ?? iconContainerEl.createSpan({ cls: 'suggestion-flair' }); + el.prepend(iconContainerEl); + if (tag) { + tag.iconDefault = 'lucide-tag'; + if (!tag.icon && !tag.color) iconEl.addClass('iconic-invisible'); + this.refreshIcon(tag, iconEl); + } + } + } + + /** + * Check whether user has disabled suggestion icons. + */ + private isDisabled(): boolean { + return !this.plugin.settings.showSuggestionIcons; + } + + /** + * @override + */ + unload(): void { + super.unload(); + + // @ts-expect-error (Private API) + if (AbstractInputSuggest.prototype.showSuggestions === this.showAbstractSuggestionsProxy) { + // @ts-expect-error (Private API) + AbstractInputSuggest.prototype.showSuggestions = this.showAbstractSuggestionsOriginal; + } + + // @ts-expect-error (Private API) + if (EditorSuggest.prototype.showSuggestions === this.showEditorSuggestionsProxy) { + // @ts-expect-error (Private API) + EditorSuggest.prototype.showSuggestions = this.showEditorSuggestionsOriginal; + } + } +} diff --git a/src/managers/TabIconManager.ts b/src/managers/TabIconManager.ts index 570dc5a..8011de0 100644 --- a/src/managers/TabIconManager.ts +++ b/src/managers/TabIconManager.ts @@ -1,6 +1,6 @@ import { Platform } from 'obsidian'; import IconicPlugin, { Category, FileItem, TabItem, STRINGS } from 'src/IconicPlugin'; -import IconManager from './IconManager'; +import IconManager from 'src/managers/IconManager'; import RuleEditor from 'src/dialogs/RuleEditor'; import IconPicker from 'src/dialogs/IconPicker'; @@ -13,13 +13,19 @@ export default class TabIconManager extends IconManager { this.plugin.registerEvent(this.app.workspace.on('layout-change', () => this.refreshIcons())); this.plugin.registerEvent(this.app.workspace.on('active-leaf-change', () => this.refreshIcons())); - // Refresh dropdown tab list + // Refresh icons in tab selector dropdown ▼ const tabListEl = activeDocument.body.find('.mod-root .workspace-tab-header-tab-list > .clickable-icon'); if (tabListEl) this.setEventListener(tabListEl, 'click', () => { const tabs = this.plugin.getTabItems().filter(tab => tab.isRoot); this.plugin.menuManager.forSection('tablist', (item, i) => { const tab = tabs[i]; - if (tab) { + if (!tab) return; + if (tab.category === 'file') { + const rule = this.plugin.ruleManager.checkRuling('file', tab.id) ?? tab; + rule.iconDefault = rule.iconDefault ?? 'lucide-file'; + // @ts-expect-error (Private API) + this.refreshIcon(rule, item.iconEl); + } else { tab.iconDefault = tab.iconDefault ?? 'lucide-file'; // @ts-expect-error (Private API) this.refreshIcon(tab, item.iconEl); @@ -31,6 +37,7 @@ export default class TabIconManager extends IconManager { } /** + * @override * Refresh all tab icons. */ refreshIcons(unloading?: boolean): void { @@ -52,9 +59,7 @@ export default class TabIconManager extends IconManager { this.refreshIcon(rule, iconEl, event => { IconPicker.openSingle(this.plugin, file, (newIcon, newColor) => { this.plugin.saveFileIcon(file, newIcon, newColor); - this.refreshIcons(); - this.plugin.fileIconManager?.refreshIcons(); - this.plugin.bookmarkIconManager?.refreshIcons(); + this.plugin.refreshManagers('file'); }); event.stopPropagation(); }); @@ -62,7 +67,7 @@ export default class TabIconManager extends IconManager { this.refreshIcon(rule, iconEl, event => { IconPicker.openSingle(this.plugin, tab, (newIcon, newColor) => { this.plugin.saveTabIcon(tab, newIcon, newColor); - this.refreshIcons(); + this.plugin.refreshManagers('tab'); }); event.stopPropagation(); }); @@ -166,7 +171,7 @@ export default class TabIconManager extends IconManager { .setSection('icon') .onClick(() => IconPicker.openSingle(this.plugin, tab, (newIcon, newColor) => { this.plugin.saveTabIcon(tab, newIcon, newColor); - this.refreshIcons(); + this.plugin.refreshManagers('tab'); })) ); @@ -178,7 +183,7 @@ export default class TabIconManager extends IconManager { .setSection('icon') .onClick(() => { this.plugin.saveTabIcon(tab, null, null); - this.refreshIcons(); + this.plugin.refreshManagers('tab'); }) ); } @@ -197,9 +202,7 @@ export default class TabIconManager extends IconManager { .setSection('icon') .onClick(() => IconPicker.openSingle(this.plugin, file, (newIcon, newColor) => { this.plugin.saveFileIcon(file, newIcon, newColor); - this.refreshIcons(); - this.plugin.fileIconManager?.refreshIcons(); - this.plugin.bookmarkIconManager?.refreshIcons(); + this.plugin.refreshManagers('file'); })) ); @@ -211,9 +214,7 @@ export default class TabIconManager extends IconManager { .setSection('icon') .onClick(() => { this.plugin.saveFileIcon(file, null, null); - this.refreshIcons(); - this.plugin.fileIconManager?.refreshIcons(); - this.plugin.bookmarkIconManager?.refreshIcons(); + this.plugin.refreshManagers('file'); }) ); } @@ -230,9 +231,7 @@ export default class TabIconManager extends IconManager { ? this.plugin.ruleManager.saveRule('file', newRule) : this.plugin.ruleManager.deleteRule('file', rule.id); if (isRulingChanged) { - this.refreshIcons(); - this.plugin.fileIconManager?.refreshIcons(); - this.plugin.bookmarkIconManager?.refreshIcons(); + this.plugin.refreshManagers('file'); } })); }); diff --git a/src/managers/TagIconManager.ts b/src/managers/TagIconManager.ts index 9fd6ff7..c61ac7e 100644 --- a/src/managers/TagIconManager.ts +++ b/src/managers/TagIconManager.ts @@ -47,6 +47,7 @@ export default class TagIconManager extends IconManager { } /** + * @override * Refresh all tag icons. */ refreshIcons(unloading?: boolean): void { @@ -69,6 +70,8 @@ export default class TagIconManager extends IconManager { const tag = tags.find(tag => tag.id === tagId); if (!tag) continue; + if (tag.color) tag.iconDefault = 'lucide-tag'; + let iconEl = selfEl.find(':scope > .tree-item-icon') ?? selfEl.createDiv({ cls: 'tree-item-icon' }); if (iconEl.hasClass('collapse-icon') && !tag.icon && !tag.iconDefault) { this.refreshIcon(tag, iconEl); // Skip click listener if icon will be a collapse arrow @@ -76,8 +79,7 @@ export default class TagIconManager extends IconManager { this.refreshIcon(tag, iconEl, event => { IconPicker.openSingle(this.plugin, tag, (newIcon, newColor) => { this.plugin.saveTagIcon(tag, newIcon, newColor); - this.refreshIcons(); - this.plugin.editorIconManager?.refreshIcons(); + this.plugin.refreshManagers('tag'); }); event.stopPropagation(); }); @@ -116,8 +118,7 @@ export default class TagIconManager extends IconManager { .setSection('icon') .onClick(() => IconPicker.openSingle(this.plugin, tag, (newIcon, newColor) => { this.plugin.saveTagIcon(tag, newIcon, newColor); - this.refreshIcons(); - this.plugin.editorIconManager?.refreshIcons(); + this.plugin.refreshManagers('tag'); })) ); @@ -129,8 +130,7 @@ export default class TagIconManager extends IconManager { .setSection('icon') .onClick(() => { this.plugin.saveTagIcon(tag, null, null); - this.refreshIcons(); - this.plugin.editorIconManager?.refreshIcons(); + this.plugin.refreshManagers('tag'); }) ); } diff --git a/styles.css b/styles.css index 8455bc8..814428e 100644 --- a/styles.css +++ b/styles.css @@ -24,6 +24,11 @@ body { padding: 0 13px; } +/* Right sidebar toggle */ +.sidebar-toggle-button.mod-right .clickable-icon.iconic-icon:not(:has(.sidebar-toggle-button-icon)) { + transform: unset; +} + /* Tabs */ .iconic-markdown-tab-icons .workspace .mod-root .workspace-tab-header[data-type="markdown"] .workspace-tab-header-inner-icon, .workspace .mod-root .workspace-tab-header[data-type="empty"] .workspace-tab-header-inner-icon { @@ -98,16 +103,47 @@ a.tag { .multi-select-pill > .iconic-icon + .multi-select-pill-content { margin-inline-start: 0; } -.multi-select-pill > .iconic-icon + .multi-select-pill-content + .multi-select-pill-remove-button { - color: inherit; +.cm-hashtag-begin > .iconic-icon { + --icon-size: var(--icon-xs); + display: inline-block; + margin-inline-end: var(--size-4-1); + transform: translateY(1.5px); } .iconic-clickable-icons .tag > .iconic-icon:hover, -.iconic-clickable-icons .multi-select-pill > .iconic-icon:hover { +.iconic-clickable-icons .multi-select-pill > .iconic-icon:hover, +.iconic-clickable-icons .cm-hashtag-begin > .iconic-icon:hover { + cursor: var(--cursor-link); + filter: contrast(200%); +} + +/* Editor title */ +.iconic-title-wrapper { + display: none; + flex-direction: row; + align-items: center; + gap: 0.3em; + font-size: var(--inline-title-size); /* Allow gap size to scale naturally */ +} +.show-inline-title .iconic-title-wrapper { + display: flex; +} +.iconic-title-wrapper > .iconic-icon { + --icon-size: 1.25em; + margin-block-end: var(--inline-title-margin-bottom); + line-height: 0; +} +.iconic-clickable-icons .iconic-title-wrapper > .iconic-icon:hover { cursor: var(--cursor-link); filter: contrast(200%); } +.is-mobile .iconic-title-wrapper > .iconic-icon { + padding-top: 0.25em; +} +.iconic-title-wrapper > .inline-title { + font-size: inherit; +} -/* Properties editor */ +/* Editor properties */ .iconic-clickable-icons .metadata-property-icon.iconic-icon:hover { cursor: var(--cursor-link); filter: contrast(200%); @@ -121,11 +157,17 @@ a.tag { cursor: var(--cursor-link); } +/* Suggestions */ +.suggestion-item.mod-complex.iconic-item { + padding: var(--size-2-3) var(--size-4-2) var(--size-2-3) var(--size-4-1); + justify-content: normal; +} + /* Quick Switcher */ .iconic-quick-switcher .iconic-icon { height: var(--icon-size); } -.iconic-quick-switcher .prompt-input { +.iconic-quick-switcher > .prompt-input-container > .prompt-input { padding-inline-start: calc(var(--icon-size) + var(--size-4-8)); } .iconic-quick-switcher .suggestion-item {