diff --git a/src/lib/customRule.ts b/src/lib/customRule.ts index c9f014db..8baa925d 100644 --- a/src/lib/customRule.ts +++ b/src/lib/customRule.ts @@ -31,18 +31,19 @@ const doesMatchFileType = (rule: CustomRule, fileType: CustomRuleFileType): bool const isApplicable = async (plugin: IconFolderPlugin, rule: CustomRule, file: TAbstractFile): Promise => { // Gets the file type based on the specified file path. const fileType = (await plugin.app.vault.adapter.stat(file.path)).type; + const toMatch = rule.useFilePath ? file.path : file.name; try { // Rule is in some sort of regex. const regex = new RegExp(rule.rule); - if (!file.name.match(regex)) { + if (!toMatch.match(regex)) { return false; } return doesMatchFileType(rule, fileType); } catch { // Rule is not in some sort of regex, check for basic string match. - return file.name.includes(rule.rule) && doesMatchFileType(rule, fileType); + return toMatch.includes(rule.rule) && doesMatchFileType(rule, fileType); } }; @@ -70,13 +71,12 @@ const removeFromAllFiles = async (plugin: IconFolderPlugin, rule: CustomRule): P }; /** - * Really dumb way to sort the custom rules. At the moment, it only sorts the custom rules - * based on the `localCompare` function. + * Gets all the custom rules sorted by their rule property in ascending order. * @param plugin Instance of IconFolderPlugin. * @returns An array of sorted custom rules. */ const getSortedRules = (plugin: IconFolderPlugin): CustomRule[] => { - return plugin.getSettings().rules.sort((a, b) => a.rule.localeCompare(b.rule)); + return plugin.getSettings().rules.sort((a, b) => a.order - b.order); }; /** @@ -114,15 +114,16 @@ const addToAllFiles = async (plugin: IconFolderPlugin, rule: CustomRule): Promis * @param rule Custom rule that will be used to check if the rule is applicable to the file. * @param file File or folder that will be used to possibly create the icon for. * @param container Optional element where the icon will be added if the custom rules matches. + * @returns A promise that resolves to true if the icon was added, false otherwise. */ const add = async ( plugin: IconFolderPlugin, rule: CustomRule, file: TAbstractFile, container?: HTMLElement, -): Promise => { +): Promise => { if (container && dom.doesElementHasIconNode(container)) { - return; + return false; } // Gets the type of the file. @@ -130,7 +131,7 @@ const add = async ( const hasIcon = plugin.getIconNameFromPath(file.path); if (!doesMatchFileType(rule, fileType) || hasIcon) { - return; + return false; } const toMatch = rule.useFilePath ? file.path : file.name; try { @@ -138,13 +139,17 @@ const add = async ( const regex = new RegExp(rule.rule); if (toMatch.match(regex)) { dom.createIconNode(plugin, file.path, rule.icon, { color: rule.color, container }); + return true; } } catch { // Rule is not applicable to a regex format. if (toMatch.includes(rule.rule)) { dom.createIconNode(plugin, file.path, rule.icon, { color: rule.color, container }); + return true; } } + + return false; }; /** @@ -154,16 +159,16 @@ const add = async ( * @returns True if the rule exists in the path, false otherwise. */ const doesExistInPath = (rule: CustomRule, path: string): boolean => { - const name = rule.useFilePath ? path : path.split('/').pop(); + const toMatch = rule.useFilePath ? path : path.split('/').pop(); try { // Rule is in some sort of regex. const regex = new RegExp(rule.rule); - if (name.match(regex)) { + if (toMatch.match(regex)) { return true; } } catch { // Rule is not in some sort of regex, check for basic string match. - return name.includes(rule.rule); + return toMatch.includes(rule.rule); } return false; diff --git a/src/lib/iconTabs.ts b/src/lib/iconTabs.ts index 4433db3d..a0a581e2 100644 --- a/src/lib/iconTabs.ts +++ b/src/lib/iconTabs.ts @@ -94,6 +94,7 @@ const add = async (plugin: IconFolderPlugin, file: TFile, options?: AddOptions): dom.setIconForNode(plugin, rule.icon, iconContainer, rule.color); // TODO: Refactor to include option to `insertIconToNode` function. iconContainer.style.margin = null; + break; } } diff --git a/src/main.ts b/src/main.ts index 8513592b..355c61a1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -50,6 +50,17 @@ export default class IconFolderPlugin extends Plugin { this.getSettings().migrated++; } + // Migration for new order functionality of custom rules. + if (this.getSettings().migrated === 2) { + // Sorting alphabetically was the default behavior before. + this.getSettings() + .rules.sort((a, b) => a.rule.localeCompare(b.rule)) + .forEach((rule, i) => { + rule.order = i; + }); + this.getSettings().migrated++; + } + const extraPadding = (this.getSettings() as any).extraPadding as ExtraMarginSettings; if (extraPadding) { if (extraPadding.top !== 2 || extraPadding.bottom !== 2 || extraPadding.left !== 2 || extraPadding.right !== 2) { @@ -241,7 +252,7 @@ export default class IconFolderPlugin extends Plugin { // Register rename event for adding icons with custom rules to the DOM and updating // inheritance when file was moved to another directory. this.registerEvent( - this.app.vault.on('rename', (file, oldPath) => { + this.app.vault.on('rename', async (file, oldPath) => { const inheritanceExists = inheritance.doesExistInPath(this, oldPath); if (inheritanceExists) { // Apply inheritance to the renamed file. @@ -261,14 +272,34 @@ export default class IconFolderPlugin extends Plugin { }); } } else { - // Apply custom rules to the renamed file. - customRule.getSortedRules(this).forEach((rule) => { + const sortedRules = customRule.getSortedRules(this); + + // Removes possible icons from the renamed file. + sortedRules.forEach((rule) => { if (customRule.doesExistInPath(rule, oldPath)) { dom.removeIconInPath(file.path); } + }); + + // Adds possible icons to the renamed file. + sortedRules.forEach((rule) => { + if (customRule.doesExistInPath(rule, oldPath)) { + return; + } customRule.add(this, rule, file, undefined); }); + + // Updates icon tabs for the renamed file. + for (const rule of customRule.getSortedRules(this)) { + const applicable = await customRule.isApplicable(this, rule, file); + if (!applicable) { + continue; + } + + iconTabs.update(this, file as TFile, rule.icon); + break; + } } }), ); diff --git a/src/settings/data.ts b/src/settings/data.ts index 417d49bf..d3cd72fc 100644 --- a/src/settings/data.ts +++ b/src/settings/data.ts @@ -8,6 +8,7 @@ export interface ExtraMarginSettings { export interface CustomRule { rule: string; icon: string; + order: number; color?: string; useFilePath?: boolean; for?: 'everything' | 'files' | 'folders'; @@ -27,7 +28,7 @@ export interface IconFolderSettings { } export const DEFAULT_SETTINGS: IconFolderSettings = { - migrated: 1, + migrated: 2, // Bump up this number for migrations. iconPacksPath: '.obsidian/plugins/obsidian-icon-folder/icons', fontSize: 16, emojiStyle: 'none', diff --git a/src/settings/ui/customIconRule.ts b/src/settings/ui/customIconRule.ts index 3cd545a9..c093800c 100644 --- a/src/settings/ui/customIconRule.ts +++ b/src/settings/ui/customIconRule.ts @@ -2,7 +2,7 @@ import { App, Notice, Setting, TextComponent, ColorComponent, ButtonComponent, M import IconFolderSetting from './iconFolderSetting'; import IconsPickerModal from '@app/iconsPickerModal'; import IconFolderPlugin from '@app/main'; -import { getAllOpenedFiles, removeIconFromIconPack, saveIconToIconPack } from '@app/util'; +import { getAllOpenedFiles, getFileItemTitleEl, removeIconFromIconPack, saveIconToIconPack } from '@app/util'; import { CustomRule } from '../data'; import customRule from '@lib/customRule'; import iconTabs from '@lib/iconTabs'; @@ -27,9 +27,13 @@ export default class CustomIconRuleSetting extends IconFolderSetting { * @param rule Rule that will be used to update all the icons for all opened files. * @param remove Whether to remove the icons that are applicable to the rule or not. */ - private async updateIconTabs(rule: CustomRule, remove: boolean): Promise { + private async updateIconTabs(rule: CustomRule, remove: boolean, cachedPaths: string[] = []): Promise { if (this.plugin.getSettings().iconInTabsEnabled) { for (const openedFile of getAllOpenedFiles(this.plugin)) { + if (cachedPaths.includes(openedFile.path)) { + continue; + } + const applicable = await customRule.isApplicable(this.plugin, rule, openedFile); if (!applicable) { continue; @@ -80,7 +84,12 @@ export default class CustomIconRuleSetting extends IconFolderSetting { modal.onChooseItem = async (item) => { const icon = getNormalizedName(typeof item === 'object' ? item.displayName : item); - const rule: CustomRule = { rule: this.textComponent.getValue(), icon, for: 'everything' }; + const rule: CustomRule = { + rule: this.textComponent.getValue(), + icon, + for: 'everything', + order: this.plugin.getSettings().rules.length, + }; this.plugin.getSettings().rules = [...this.plugin.getSettings().rules, rule]; await this.plugin.saveIconFolderData(); @@ -102,6 +111,72 @@ export default class CustomIconRuleSetting extends IconFolderSetting { // Keeping track of the old rule so that we can get a reference to it for old values. const oldRule = { ...rule }; const settingRuleEl = new Setting(this.containerEl).setName(rule.rule).setDesc(`Icon: ${rule.icon}`); + const currentOrder = rule.order; + + /** + * Re-orders the custom rule based on the value that is passed in. + * @param valueForReorder Number that will be used to determine whether to swap the + * custom rule with the next rule or the previous rule. + */ + const orderCustomRules = async (valueForReorder: number): Promise => { + const otherRule = this.plugin.getSettings().rules[currentOrder + valueForReorder]; + // Swap the current rule with the next rule. + otherRule.order = otherRule.order - valueForReorder; + rule.order = currentOrder + valueForReorder; + // Refreshes the DOM. + await customRule.removeFromAllFiles(this.plugin, oldRule); + await this.plugin.saveIconFolderData(); + + const addedPaths: string[] = []; + for (const fileExplorer of this.plugin.getRegisteredFileExplorers()) { + const files = Object.values(fileExplorer.fileItems); + for (const rule of customRule.getSortedRules(this.plugin)) { + // Removes the icon tabs from all opened files. + this.updateIconTabs(rule, true, addedPaths); + // Adds the icon tabs to all opened files. + this.updateIconTabs(rule, false, addedPaths); + + for (const fileItem of files) { + if (addedPaths.includes(fileItem.file.path)) { + continue; + } + + const added = await customRule.add(this.plugin, rule, fileItem.file, getFileItemTitleEl(fileItem)); + if (added) { + addedPaths.push(fileItem.file.path); + } + } + } + } + + this.refreshDisplay(); + }; + + // Add the move down custom rule button to re-order the custom rule. + settingRuleEl.addExtraButton((btn) => { + const isFirstOrder = currentOrder === 0; + btn.setDisabled(isFirstOrder); + btn.extraSettingsEl.style.cursor = isFirstOrder ? 'not-allowed' : 'default'; + btn.extraSettingsEl.style.opacity = isFirstOrder ? '50%' : '100%'; + btn.setIcon('arrow-up'); + btn.setTooltip('Prioritize the custom rule'); + btn.onClick(async () => { + await orderCustomRules(-1); + }); + }); + + // Add the move up custom rule button to re-order the custom rule. + settingRuleEl.addExtraButton((btn) => { + const isLastOrder = currentOrder === this.plugin.getSettings().rules.length - 1; + btn.setDisabled(isLastOrder); + btn.extraSettingsEl.style.cursor = isLastOrder ? 'not-allowed' : 'default'; + btn.extraSettingsEl.style.opacity = isLastOrder ? '50%' : '100%'; + btn.setIcon('arrow-down'); + btn.setTooltip('Deprioritize the custom rule'); + btn.onClick(async () => { + await orderCustomRules(1); + }); + }); // Add the configuration button for configuring where the custom rule gets applied to. settingRuleEl.addButton((btn) => {