diff --git a/Models/ExclusionRule.ts b/Models/ExclusionRule.ts new file mode 100644 index 0000000..ea61a6d --- /dev/null +++ b/Models/ExclusionRule.ts @@ -0,0 +1,13 @@ +/** + * Defines the Exlclusion rules that will jump over the folders defined in the regex + */ +export class ExclusionRule { + /** + * This filed is used for defining the regex with its group matchers + */ + public regex: string; + + constructor(regex?: string) { + this.regex = regex || ""; + } +} diff --git a/README.md b/README.md index 5ceb4eb..1d9cee4 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,8 @@ Thank you! ## Future plans -- [ ] Add excluded folder support -- [ ] Add excluded file support +- [x] Add excluded folder support +- [x] Add excluded file support +- [x] Add regex support for excluded folders and files (must support language accents like ñ, á, š, đ, こ, 猫, etc.) - [ ] Add a file like .gitignore which contains all the moving rules (i am assuming the list can grow quite big for some people) diff --git a/Settings/Settings.ts b/Settings/Settings.ts index 06f9fdb..d588597 100644 --- a/Settings/Settings.ts +++ b/Settings/Settings.ts @@ -1,36 +1,27 @@ -import AutoMoverPlugin from "main"; -import { MovingRule } from "Models/MovingRule"; +import type AutoMoverPlugin from "main"; +import type { ExclusionRule } from "Models/ExclusionRule"; +import type { MovingRule } from "Models/MovingRule"; export interface AutoMoverSettings { - moveOnOpen: boolean, - moveOnSave: boolean, - moveOnClose: boolean, - moveOnCreate: boolean, - movingRules: MovingRule[], + moveOnOpen: boolean; + moveOnSave: boolean; + moveOnClose: boolean; + moveOnCreate: boolean; + movingRules: MovingRule[]; + excludedFolders: ExclusionRule[]; } -// TODO: Define a value which accepts a regex that will be used to manage files -// the matcher group should accept an argument which will be the name of the folder to which the files will be moved -// e.g. if the file is a daily note like 14.09.2021.md, then the folder structure should be YYYY/MM/DD - - -// there should be a button in the settings to sort all the files according to given criteria -// the button should be disabled if the regex is invalid -> there are no matches - -// regex that should be accepted: -// YYYY -> year -// MM -> month -// DD -> day -// some string -> the string should be the name of the folder - export const DEFAULT_SETTINGS: Partial = { moveOnOpen: true, moveOnSave: true, moveOnClose: false, moveOnCreate: false, movingRules: [], -} + excludedFolders: [], +}; -function loadSettings(AutoMoverPlugin: AutoMoverPlugin): Partial { +function loadSettings( + AutoMoverPlugin: AutoMoverPlugin, +): Partial { return Object.assign({}, DEFAULT_SETTINGS, AutoMoverPlugin.loadData()); -} \ No newline at end of file +} diff --git a/Settings/SettingsTab.ts b/Settings/SettingsTab.ts index ecc2b87..ccb7dfc 100644 --- a/Settings/SettingsTab.ts +++ b/Settings/SettingsTab.ts @@ -1,4 +1,5 @@ import type AutoMoverPlugin from "main"; +import { ExclusionRule } from "Models/ExclusionRule"; import { MovingRule } from "Models/MovingRule"; import * as obsidian from "obsidian"; @@ -50,6 +51,7 @@ export class SettingsTab extends obsidian.PluginSettingTab { // }), // ); + // TUTORIAL START /** * Instead of using the default .setting-item class I created a custom class to add other styling, * with the inclusion of the relevant styles from the .setting-item class @@ -57,9 +59,7 @@ export class SettingsTab extends obsidian.PluginSettingTab { const tutorialContainer = containerEl.createDiv({ cls: "moving_rules_container", }); - const headerSetting = new obsidian.Setting(tutorialContainer) - .setName("Tutorial") - .setHeading(); + new obsidian.Setting(tutorialContainer).setName("Tutorial").setHeading(); /** * Rule description and tutorial */ @@ -97,8 +97,10 @@ export class SettingsTab extends obsidian.PluginSettingTab { }); const example2 = description.createDiv({ cls: "rule margig_right" }); example2.createSpan({ text: "Scroll (\\d+)", cls: "rule_title" }); - example2.createSpan({ text: "Scrolls\/$1", cls: "rule_title" }); + example2.createSpan({ text: "Scrolls/$1", cls: "rule_title" }); + // TUTORIAL END + // MOVING RULES START /** * Header of the rules */ @@ -121,11 +123,11 @@ export class SettingsTab extends obsidian.PluginSettingTab { cls: "rule_title", }); - const addButton = ruleHeader.createEl("button", { + const addRuleButton = ruleHeader.createEl("button", { text: "+", cls: "rule_button", }); - addButton.addEventListener("click", () => { + addRuleButton.addEventListener("click", () => { this.plugin.settings.movingRules.push(new MovingRule()); // this is used to rerender the settings tab this.display(); @@ -153,26 +155,97 @@ export class SettingsTab extends obsidian.PluginSettingTab { this.plugin.saveData(this.plugin.settings); }; - const duplicateButton = child.createEl("button", { + const duplicateRuleButton = child.createEl("button", { text: "⿻", cls: "rule_button rule_button_duplicate", }); - duplicateButton.addEventListener("click", () => { + duplicateRuleButton.addEventListener("click", () => { this.plugin.settings.movingRules.push( new MovingRule(rule.regex, rule.folder), ); this.display(); }); - const deleteButton = child.createEl("button", { + const deleteRuleButton = child.createEl("button", { text: "x", cls: "rule_button rule_button_remove", }); - deleteButton.addEventListener("click", () => { + deleteRuleButton.addEventListener("click", () => { this.plugin.settings.movingRules = this.plugin.settings.movingRules.filter((r) => r !== rule); this.display(); }); } + // MOVING RULES END + + // EXCLUSION FOLDERS START + /** + * Header for excluded folders + */ + const excludedFoldersContainer = containerEl.createDiv({ + cls: "moving_rules_container", + }); + new obsidian.Setting(excludedFoldersContainer) + .setName("Exclusion rules") + .setHeading(); + + const exclusionList = excludedFoldersContainer.createDiv({ + cls: "rule_list", + }); + const exclusionHeader = exclusionList.createDiv({ + cls: "rule margig_right", + }); + exclusionHeader.createEl("p", { + text: "Excluded folders or files (string or regex)", + cls: "rule_title", + }); + + const addExclusionButton = exclusionHeader.createEl("button", { + text: "+", + cls: "rule_button", + }); + addExclusionButton.addEventListener("click", () => { + this.plugin.settings.excludedFolders.push(new ExclusionRule()); + this.display(); + }); + + /** + * List of excluded folders + */ + for (const exclusion of this.plugin.settings.excludedFolders) { + const child = exclusionList.createDiv({ cls: "rule" }); + child.createEl("input", { + value: exclusion.regex, + cls: "rule_input", + }).onchange = (e) => { + exclusion.regex = (e.target as HTMLInputElement).value; + this.plugin.settings.excludedFolders.map((ef) => + ef === exclusion ? exclusion : ef, + ); + this.plugin.saveData(this.plugin.settings); + }; + + const duplicateExclusionButton = child.createEl("button", { + text: "⿻", + cls: "rule_button rule_button_duplicate", + }); + duplicateExclusionButton.addEventListener("click", () => { + this.plugin.settings.excludedFolders.push( + new ExclusionRule(exclusion.regex), + ); + this.display(); + }); + + const deleteExclusionButton = child.createEl("button", { + text: "x", + cls: "rule_button rule_button_remove", + }); + deleteExclusionButton.addEventListener("click", () => { + this.plugin.settings.excludedFolders = + this.plugin.settings.excludedFolders.filter((r) => r !== exclusion); + this.display(); + }); + } + // EXCLUSION FOLDERS END } } diff --git a/Utils/ExclusionMatcherUtil.ts b/Utils/ExclusionMatcherUtil.ts new file mode 100644 index 0000000..2878c42 --- /dev/null +++ b/Utils/ExclusionMatcherUtil.ts @@ -0,0 +1,37 @@ +import type { ExclusionRule } from "Models/ExclusionRule"; +import type { TFile } from "obsidian"; + +class ExclusionMatcherUtil { + private static instance: ExclusionMatcherUtil; + + private constructor() {} + + public static getInstance(): ExclusionMatcherUtil { + if (!ExclusionMatcherUtil.instance) { + ExclusionMatcherUtil.instance = new ExclusionMatcherUtil(); + } + return ExclusionMatcherUtil.instance; + } + + /** + * This method is used to check if the file is excluded by the exclusion rule + * @param file - The file to be checked + * @param exclusionRules - The exclusion rule to be used + * @returns true if the file path is excluded, false otherwise + */ + isFilePathExcluded(file: TFile, exclusionRules: ExclusionRule[]): boolean { + console.log("Exclusion rules: ", exclusionRules); + for (const rule of exclusionRules) { + const regex = new RegExp(rule.regex); + console.log("Regex: ", regex); + console.log("File path: ", file.path); + console.log("Result: ", regex.test(file.path)); + if (regex.test(file.path)) { + return true; + } + } + return false; + } +} +const exclusionMatcherUtil = ExclusionMatcherUtil.getInstance(); +export default exclusionMatcherUtil; diff --git a/Utils/RuleMatcherUtil.ts b/Utils/RuleMatcherUtil.ts index c4496ba..a10b82f 100644 --- a/Utils/RuleMatcherUtil.ts +++ b/Utils/RuleMatcherUtil.ts @@ -27,6 +27,10 @@ class RuleMatcherUtil { console.error("Rule does not have a regex: ", rule); continue; } + if (rule.folder == null || rule.folder === "") { + console.error("Rule does not have a destination folder: ", rule); + continue; + } if (!this.isValidRegex(rule.regex)) { console.error("Rule has an invalid regex: ", rule); continue; diff --git a/main.ts b/main.ts index 67696b0..bc599f1 100644 --- a/main.ts +++ b/main.ts @@ -3,6 +3,7 @@ import { SettingsTab } from "Settings/SettingsTab"; import movingUtil from "Utils/MovingUtil"; import ruleMatcherUtil from "Utils/RuleMatcherUtil"; import * as obsidian from "obsidian"; +import exclusionMatcherUtil from "Utils/ExclusionMatcherUtil"; export default class AutoMoverPlugin extends obsidian.Plugin { settings: Settings.AutoMoverSettings; @@ -19,18 +20,20 @@ export default class AutoMoverPlugin extends obsidian.Plugin { await this.loadData(), ); this.addSettingTab(new SettingsTab(this.app, this)); - if ( - this.checkIfMovingTriggersAreEnabled() && - this.checkIfThereAreRulesToApply() && - this.checkIfRulesAreValid() - ) { - if (this.settings.moveOnOpen) { - this.registerEvent( - this.app.workspace.on("file-open", (file: obsidian.TFile) => { - this.matchAndMoveFile(file); - }), - ); - } + + // negative ifs for easier reading and debugging + if (!this.areMovingTriggersEnabled()) return; + if (!this.areThereRulesToApply()) return; + if (!this.areRulesValid()) return; + + if (this.settings.moveOnOpen) { + this.registerEvent( + this.app.workspace.on("file-open", (file: obsidian.TFile) => { + if (file == null || file.path == null) return; + if (this.isFileExcluded(file)) return; + this.matchAndMoveFile(file); + }), + ); } } @@ -47,6 +50,22 @@ export default class AutoMoverPlugin extends obsidian.Plugin { new obsidian.Notice("All files moved!", 5000); } + /** + * Checks if the file is excluded by the exclusion rule + * + * @param file - The file to be checked + * @returns true if the file path is excluded, false otherwise + */ + isFileExcluded(file: obsidian.TFile): boolean { + if (!this.areThereExcludedFolders()) return false; + if (!this.areThereRulesToApply()) return false; + + return exclusionMatcherUtil.isFilePathExcluded( + file, + this.settings.excludedFolders, + ); + } + /** * Matches the file to the rule and moves it to the destination folder * @@ -54,7 +73,7 @@ export default class AutoMoverPlugin extends obsidian.Plugin { * @returns void */ matchAndMoveFile(file: obsidian.TFile): void { - if (file == null || file.path == null) return; + console.log("Moving file: ", file.path); const rule = ruleMatcherUtil.getMatchingRule( file, this.settings.movingRules, @@ -88,7 +107,7 @@ export default class AutoMoverPlugin extends obsidian.Plugin { * No point in doing anything if there is no trigger set which will cause you to move the files * @returns boolean */ - checkIfMovingTriggersAreEnabled(): boolean { + areMovingTriggersEnabled(): boolean { return ( this.settings.moveOnClose || this.settings.moveOnCreate || @@ -101,17 +120,38 @@ export default class AutoMoverPlugin extends obsidian.Plugin { * If there are no rules to apply, then there is no point in checking for them * @returns boolean */ - checkIfThereAreRulesToApply(): boolean { - return this.settings.movingRules.length > 0; + areThereRulesToApply(): boolean { + return ( + this.settings.movingRules.length > 0 && + this.settings.movingRules.every( + (rule) => rule.regex !== "" && rule.folder !== "", + ) + ); + } + + /** + * If there are no excluded folders, then we can move thing freely + * @returns boolean + */ + areThereExcludedFolders(): boolean { + return this.settings.excludedFolders.length > 0; } /** * Superficail check if the rules are valid * @returns boolean */ - checkIfRulesAreValid(): boolean { + areRulesValid(): boolean { return this.settings.movingRules.every( (rule) => rule.regex !== "" && rule.folder !== "", ); } + + /** + * Superficail check if the excluded folders are valid + * @returns boolean + */ + areExcludedFoldersValid(): boolean { + return this.settings.excludedFolders.every((rule) => rule.regex !== ""); + } }