Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add reordering functionality for custom rules #237

Merged
merged 6 commits into from
Sep 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions src/lib/customRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,19 @@ const doesMatchFileType = (rule: CustomRule, fileType: CustomRuleFileType): bool
const isApplicable = async (plugin: IconFolderPlugin, rule: CustomRule, file: TAbstractFile): Promise<boolean> => {
// 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);
}
};

Expand Down Expand Up @@ -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);
};

/**
Expand Down Expand Up @@ -114,37 +114,42 @@ 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<void> => {
): Promise<boolean> => {
if (container && dom.doesElementHasIconNode(container)) {
return;
return false;
}

// Gets the type of the file.
const fileType = (await plugin.app.vault.adapter.stat(file.path)).type;

const hasIcon = plugin.getIconNameFromPath(file.path);
if (!doesMatchFileType(rule, fileType) || hasIcon) {
return;
return false;
}
const toMatch = rule.useFilePath ? file.path : file.name;
try {
// Rule is in some sort of regex.
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;
};

/**
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/lib/iconTabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down
37 changes: 34 additions & 3 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,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) {
Expand Down Expand Up @@ -240,7 +251,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.
Expand All @@ -260,14 +271,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;
}
}
}),
);
Expand Down
3 changes: 2 additions & 1 deletion src/settings/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface ExtraMarginSettings {
export interface CustomRule {
rule: string;
icon: string;
order: number;
color?: string;
useFilePath?: boolean;
for?: 'everything' | 'files' | 'folders';
Expand All @@ -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',
Expand Down
81 changes: 78 additions & 3 deletions src/settings/ui/customIconRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<void> {
private async updateIconTabs(rule: CustomRule, remove: boolean, cachedPaths: string[] = []): Promise<void> {
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;
Expand Down Expand Up @@ -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();

Expand All @@ -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<void> => {
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) => {
Expand Down