From 20363baab69902c10a445d6f37698ca627edb369 Mon Sep 17 00:00:00 2001 From: Al0cam Date: Mon, 11 Aug 2025 11:16:18 +0200 Subject: [PATCH 1/6] initial creation of RuleIO --- IO/RuleIO.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 IO/RuleIO.ts diff --git a/IO/RuleIO.ts b/IO/RuleIO.ts new file mode 100644 index 0000000..d4d392c --- /dev/null +++ b/IO/RuleIO.ts @@ -0,0 +1,20 @@ +class SettingsIO { + private static instance: SettingsIO; + + private constructor() {} + + public static getInstance(): SettingsIO { + if (!SettingsIO.instance) { + SettingsIO.instance = new SettingsIO(); + } + return SettingsIO.instance; + } + + // reading from yaml + // reading from json + // reading from regular txt file + +} + +const settingsIO = SettingsIO.getInstance(); +export default settingsIO; From c881ce7a4a84917ee7e026e2ee3b36eabeee9fc7 Mon Sep 17 00:00:00 2001 From: Al0cam Date: Tue, 12 Aug 2025 14:58:44 +0200 Subject: [PATCH 2/6] updated settings tab, to add some rerendering options --- Models/MovingRule.ts | 16 +++- Settings/ExclusionSection.ts | 73 ++++++++++++++ Settings/MovingRuleSection.ts | 81 ++++++++++++++++ Settings/Settings.ts | 2 + Settings/SettingsTab.ts | 174 ++++------------------------------ Settings/TagSection.ts | 76 +++++++++++++++ Utils/TagUtil.ts | 14 +++ 7 files changed, 277 insertions(+), 159 deletions(-) create mode 100644 Settings/ExclusionSection.ts create mode 100644 Settings/MovingRuleSection.ts create mode 100644 Settings/TagSection.ts create mode 100644 Utils/TagUtil.ts diff --git a/Models/MovingRule.ts b/Models/MovingRule.ts index d2d203a..0ae6e45 100644 --- a/Models/MovingRule.ts +++ b/Models/MovingRule.ts @@ -1,6 +1,20 @@ +/** + * Regex based rule for moving files + */ export class MovingRule { /** - * This filed is used for defining the regex with its group matchers + * This field is used for defining the regex with its group matchers + * + * This can contain the follwoing: + * - Names of files w/w/o extensions + * - Names of folders + * - Extensions of files + * - Regex strings w/w/o groups + * - Any combination of the above + * + * Tag update: + * - The regex can now hold tags as well, which can be used to match files based on their tags. + * */ public regex: string; /** diff --git a/Settings/ExclusionSection.ts b/Settings/ExclusionSection.ts new file mode 100644 index 0000000..4d23338 --- /dev/null +++ b/Settings/ExclusionSection.ts @@ -0,0 +1,73 @@ +import { ExclusionRule } from "Models/ExclusionRule"; +import type AutoMoverPlugin from "main"; +import { Setting } from "obsidian"; + +export function exclusionSection( + containerEl: HTMLElement, + plugin: AutoMoverPlugin, +) { + /** + * Header for excluded folders + */ + const exclusionRuleContainer = containerEl.createDiv({ + cls: "moving_rules_container", + }); + new Setting(exclusionRuleContainer).setName("Exclusion rules").setHeading(); + + const exclusionList = exclusionRuleContainer.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", () => { + plugin.settings.exclusionRules.push(new ExclusionRule()); + this.display(); + }); + + /** + * List of excluded folders + */ + for (const exclusion of plugin.settings.exclusionRules) { + const child = exclusionList.createDiv({ cls: "rule" }); + child.createEl("input", { + value: exclusion.regex, + cls: "rule_input", + }).onchange = (e) => { + exclusion.regex = (e.target as HTMLInputElement).value; + plugin.settings.exclusionRules.map((ef) => + ef === exclusion ? exclusion : ef, + ); + plugin.saveData(plugin.settings); + }; + + const duplicateExclusionButton = child.createEl("button", { + text: "⿻", + cls: "rule_button rule_button_duplicate", + }); + duplicateExclusionButton.addEventListener("click", () => { + plugin.settings.exclusionRules.push(new ExclusionRule(exclusion.regex)); + this.display(); + }); + + const deleteExclusionButton = child.createEl("button", { + text: "x", + cls: "rule_button rule_button_remove", + }); + deleteExclusionButton.addEventListener("click", () => { + plugin.settings.exclusionRules = plugin.settings.exclusionRules.filter( + (r) => r !== exclusion, + ); + this.display(); + }); + } +} diff --git a/Settings/MovingRuleSection.ts b/Settings/MovingRuleSection.ts new file mode 100644 index 0000000..5747883 --- /dev/null +++ b/Settings/MovingRuleSection.ts @@ -0,0 +1,81 @@ +import { MovingRule } from "Models/MovingRule"; +import type AutoMoverPlugin from "main"; +import { Setting } from "obsidian"; + +export default function movingRuleSection( + containerEl: HTMLElement, + plugin: AutoMoverPlugin, +) { + /** + * Header of the rules + */ + const movingRulesContainer = containerEl.createDiv({ + cls: "moving_rules_container", + }); + + new Setting(movingRulesContainer).setName("Moving rules").setHeading(); + + const ruleList = movingRulesContainer.createDiv({ cls: "rule_list" }); + const ruleHeader = ruleList.createDiv({ cls: "rule margig_right" }); + ruleHeader.createEl("p", { + text: "Search criteria (string or regex)", + cls: "rule_title", + }); + ruleHeader.createEl("p", { + text: "Folder (string that can contain regex groups)", + cls: "rule_title", + }); + + const addRuleButton = ruleHeader.createEl("button", { + text: "+", + cls: "rule_button", + }); + addRuleButton.addEventListener("click", () => { + plugin.settings.movingRules.push(new MovingRule()); + // this is used to rerender the settings tab + this.display(); + }); + + /** + * List of rules + */ + for (const rule of plugin.settings.movingRules) { + const child = ruleList.createDiv({ cls: "rule" }); + child.createEl("input", { + value: rule.regex, + cls: "rule_input", + }).onchange = (e) => { + rule.regex = (e.target as HTMLInputElement).value; + plugin.settings.movingRules.map((r) => (r === rule ? rule : r)); + plugin.saveData(plugin.settings); + }; + child.createEl("input", { + value: rule.folder, + cls: "rule_input", + }).onchange = (e) => { + rule.folder = (e.target as HTMLInputElement).value; + plugin.settings.movingRules.map((r) => (r === rule ? rule : r)); + plugin.saveData(plugin.settings); + }; + + const duplicateRuleButton = child.createEl("button", { + text: "⿻", + cls: "rule_button rule_button_duplicate", + }); + duplicateRuleButton.addEventListener("click", () => { + plugin.settings.movingRules.push(new MovingRule(rule.regex, rule.folder)); + this.display(); + }); + + const deleteRuleButton = child.createEl("button", { + text: "x", + cls: "rule_button rule_button_remove", + }); + deleteRuleButton.addEventListener("click", () => { + plugin.settings.movingRules = plugin.settings.movingRules.filter( + (r) => r !== rule, + ); + this.display(); + }); + } +} diff --git a/Settings/Settings.ts b/Settings/Settings.ts index 380ffc4..547d1fe 100644 --- a/Settings/Settings.ts +++ b/Settings/Settings.ts @@ -7,6 +7,7 @@ export interface AutoMoverSettings { // moveOnSave: boolean; movingRules: MovingRule[]; exclusionRules: ExclusionRule[]; + tagRules: MovingRule[]; automaticMoving: boolean; timer: number | null; // in miliseconds } @@ -16,6 +17,7 @@ export const DEFAULT_SETTINGS: Partial = { // moveOnSave: true, movingRules: [], exclusionRules: [], + tagRules: [], automaticMoving: false, timer: null, }; diff --git a/Settings/SettingsTab.ts b/Settings/SettingsTab.ts index 59b02ac..45d8a14 100644 --- a/Settings/SettingsTab.ts +++ b/Settings/SettingsTab.ts @@ -1,24 +1,26 @@ import settingsIO from "IO/SettingsIO"; -import { ExclusionRule } from "Models/ExclusionRule"; -import { MovingRule } from "Models/MovingRule"; import timerUtil from "Utils/TimerUtil"; import type AutoMoverPlugin from "main"; -import * as obsidian from "obsidian"; +import { type App, PluginSettingTab, Setting } from "obsidian"; +import { exclusionSection } from "./ExclusionSection"; +import movingRuleSection from "./MovingRuleSection"; +import { tagSection } from "./TagSection"; -export class SettingsTab extends obsidian.PluginSettingTab { +export class SettingsTab extends PluginSettingTab { plugin: AutoMoverPlugin; - constructor(app: obsidian.App, plugin: AutoMoverPlugin) { + constructor(app: App, plugin: AutoMoverPlugin) { super(app, plugin); this.plugin = plugin; } + // how to call rerendering from child section? display(): void { const { containerEl } = this; containerEl.empty(); - new obsidian.Setting(containerEl).setName("Automatic moving").setHeading(); - new obsidian.Setting(containerEl) + new Setting(containerEl).setName("Automatic moving").setHeading(); + new Setting(containerEl) .setName("Export/Import of settings") .setDesc( `Import will replace the current settings. @@ -46,7 +48,7 @@ export class SettingsTab extends obsidian.PluginSettingTab { }); }); - new obsidian.Setting(containerEl) + new Setting(containerEl) .setName("Move on open") .setDesc("Should the file be moved when it is opened?") .addToggle((cb) => @@ -56,7 +58,7 @@ export class SettingsTab extends obsidian.PluginSettingTab { }), ); - new obsidian.Setting(containerEl) + new Setting(containerEl) .setName("Manually move") .setDesc( "Execute the command to go through your notes and move them according to the rules specified below.", @@ -70,7 +72,7 @@ export class SettingsTab extends obsidian.PluginSettingTab { const automaticMovingContainer = containerEl.createDiv({}); - new obsidian.Setting(automaticMovingContainer) + new Setting(automaticMovingContainer) .setName("Automatic moving") .setDesc( `Execute a timed event that goes through all the files and moves them according to the rules specified below. @@ -127,7 +129,7 @@ export class SettingsTab extends obsidian.PluginSettingTab { const tutorialContainer = containerEl.createDiv({ cls: "moving_rules_container", }); - new obsidian.Setting(tutorialContainer).setName("Tutorial").setHeading(); + new Setting(tutorialContainer).setName("Tutorial").setHeading(); /** * Rule description and tutorial */ @@ -168,152 +170,8 @@ export class SettingsTab extends obsidian.PluginSettingTab { example2.createSpan({ text: "Scrolls/$1", cls: "rule_title" }); // TUTORIAL END - // MOVING RULES START - /** - * Header of the rules - */ - const movingRulesContainer = containerEl.createDiv({ - cls: "moving_rules_container", - }); - - new obsidian.Setting(movingRulesContainer) - .setName("Moving rules") - .setHeading(); - - const ruleList = movingRulesContainer.createDiv({ cls: "rule_list" }); - const ruleHeader = ruleList.createDiv({ cls: "rule margig_right" }); - ruleHeader.createEl("p", { - text: "Search criteria (string or regex)", - cls: "rule_title", - }); - ruleHeader.createEl("p", { - text: "Folder (string that can contain regex groups)", - cls: "rule_title", - }); - - const addRuleButton = ruleHeader.createEl("button", { - text: "+", - cls: "rule_button", - }); - addRuleButton.addEventListener("click", () => { - this.plugin.settings.movingRules.push(new MovingRule()); - // this is used to rerender the settings tab - this.display(); - }); - - /** - * List of rules - */ - for (const rule of this.plugin.settings.movingRules) { - const child = ruleList.createDiv({ cls: "rule" }); - child.createEl("input", { - value: rule.regex, - cls: "rule_input", - }).onchange = (e) => { - rule.regex = (e.target as HTMLInputElement).value; - this.plugin.settings.movingRules.map((r) => (r === rule ? rule : r)); - this.plugin.saveData(this.plugin.settings); - }; - child.createEl("input", { - value: rule.folder, - cls: "rule_input", - }).onchange = (e) => { - rule.folder = (e.target as HTMLInputElement).value; - this.plugin.settings.movingRules.map((r) => (r === rule ? rule : r)); - this.plugin.saveData(this.plugin.settings); - }; - - const duplicateRuleButton = child.createEl("button", { - text: "⿻", - cls: "rule_button rule_button_duplicate", - }); - duplicateRuleButton.addEventListener("click", () => { - this.plugin.settings.movingRules.push( - new MovingRule(rule.regex, rule.folder), - ); - this.display(); - }); - - const deleteRuleButton = child.createEl("button", { - text: "x", - cls: "rule_button rule_button_remove", - }); - 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 exclusionRuleContainer = containerEl.createDiv({ - cls: "moving_rules_container", - }); - new obsidian.Setting(exclusionRuleContainer) - .setName("Exclusion rules") - .setHeading(); - - const exclusionList = exclusionRuleContainer.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.exclusionRules.push(new ExclusionRule()); - this.display(); - }); - - /** - * List of excluded folders - */ - for (const exclusion of this.plugin.settings.exclusionRules) { - 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.exclusionRules.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.exclusionRules.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.exclusionRules = - this.plugin.settings.exclusionRules.filter((r) => r !== exclusion); - this.display(); - }); - } - // EXCLUSION FOLDERS END + movingRuleSection(containerEl, this.plugin); + exclusionSection(containerEl, this.plugin); + tagSection(containerEl, this.plugin); } } diff --git a/Settings/TagSection.ts b/Settings/TagSection.ts new file mode 100644 index 0000000..4ce3807 --- /dev/null +++ b/Settings/TagSection.ts @@ -0,0 +1,76 @@ +import { MovingRule } from "Models/MovingRule"; +import * as obsidian from "obsidian"; +import type AutoMoverPlugin from "main"; + +export function tagSection(containerEl: HTMLElement, plugin: AutoMoverPlugin) { + /** + * Header for excluded folders + */ + const tagRuleContainer = containerEl.createDiv({ + cls: "moving_rules_container", + }); + new obsidian.Setting(tagRuleContainer).setName("Tag rules").setHeading(); + + const tagList = tagRuleContainer.createDiv({ + cls: "rule_list", + }); + const tagHeader = tagList.createDiv({ + cls: "rule margig_right", + }); + tagHeader.createEl("p", { + text: "Tag search criteria (string or regex)", + cls: "rule_title", + }); + + const addTagButton = tagHeader.createEl("button", { + text: "+", + cls: "rule_button", + }); + addTagButton.addEventListener("click", () => { + plugin.settings.tagRules.push(new MovingRule()); + this.display(); + }); + + /** + * List of tag rules + */ + for (const rule of plugin.settings.tagRules) { + const child = tagList.createDiv({ cls: "rule" }); + child.createEl("input", { + value: rule.regex, + cls: "rule_input", + }).onchange = (e) => { + rule.regex = (e.target as HTMLInputElement).value; + plugin.settings.tagRules.map((r) => (r === rule ? rule : r)); + plugin.saveData(plugin.settings); + }; + child.createEl("input", { + value: rule.folder, + cls: "rule_input", + }).onchange = (e) => { + rule.folder = (e.target as HTMLInputElement).value; + plugin.settings.tagRules.map((r) => (r === rule ? rule : r)); + plugin.saveData(plugin.settings); + }; + + const duplicateRuleButton = child.createEl("button", { + text: "⿻", + cls: "rule_button rule_button_duplicate", + }); + duplicateRuleButton.addEventListener("click", () => { + plugin.settings.tagRules.push(new MovingRule(rule.regex, rule.folder)); + this.display(); + }); + + const deleteRuleButton = child.createEl("button", { + text: "x", + cls: "rule_button rule_button_remove", + }); + deleteRuleButton.addEventListener("click", () => { + plugin.settings.tagRules = plugin.settings.tagRules.filter( + (r) => r !== rule, + ); + this.display(); + }); + } +} diff --git a/Utils/TagUtil.ts b/Utils/TagUtil.ts new file mode 100644 index 0000000..c514478 --- /dev/null +++ b/Utils/TagUtil.ts @@ -0,0 +1,14 @@ +class TagUtil { + private static instance: TagUtil; + private constructor() {} + + public static getInstance(): TagUtil { + if (!TagUtil.instance) { + TagUtil.instance = new TagUtil(); + } + return TagUtil.instance; + } +} + +const tagUtil = TagUtil.getInstance(); +export default tagUtil; From c3144528b32284509f456dadfccb5dcdb34979a0 Mon Sep 17 00:00:00 2001 From: Al0cam Date: Wed, 13 Aug 2025 12:40:18 +0200 Subject: [PATCH 3/6] updated with callback to display() --- Settings/ExclusionSection.ts | 7 ++++--- Settings/MovingRuleSection.ts | 10 +++++----- Settings/SettingsTab.ts | 23 +++++------------------ Settings/TagSection.ts | 12 ++++++++---- 4 files changed, 22 insertions(+), 30 deletions(-) diff --git a/Settings/ExclusionSection.ts b/Settings/ExclusionSection.ts index 4d23338..058f1ed 100644 --- a/Settings/ExclusionSection.ts +++ b/Settings/ExclusionSection.ts @@ -5,6 +5,7 @@ import { Setting } from "obsidian"; export function exclusionSection( containerEl: HTMLElement, plugin: AutoMoverPlugin, + display: () => void, ) { /** * Header for excluded folders @@ -31,7 +32,7 @@ export function exclusionSection( }); addExclusionButton.addEventListener("click", () => { plugin.settings.exclusionRules.push(new ExclusionRule()); - this.display(); + display(); }); /** @@ -56,7 +57,7 @@ export function exclusionSection( }); duplicateExclusionButton.addEventListener("click", () => { plugin.settings.exclusionRules.push(new ExclusionRule(exclusion.regex)); - this.display(); + display(); }); const deleteExclusionButton = child.createEl("button", { @@ -67,7 +68,7 @@ export function exclusionSection( plugin.settings.exclusionRules = plugin.settings.exclusionRules.filter( (r) => r !== exclusion, ); - this.display(); + display(); }); } } diff --git a/Settings/MovingRuleSection.ts b/Settings/MovingRuleSection.ts index 5747883..e40054e 100644 --- a/Settings/MovingRuleSection.ts +++ b/Settings/MovingRuleSection.ts @@ -1,10 +1,11 @@ +import AutoMoverPlugin from "main"; import { MovingRule } from "Models/MovingRule"; -import type AutoMoverPlugin from "main"; import { Setting } from "obsidian"; export default function movingRuleSection( containerEl: HTMLElement, plugin: AutoMoverPlugin, + display: () => void, ) { /** * Header of the rules @@ -32,8 +33,7 @@ export default function movingRuleSection( }); addRuleButton.addEventListener("click", () => { plugin.settings.movingRules.push(new MovingRule()); - // this is used to rerender the settings tab - this.display(); + display(); }); /** @@ -64,7 +64,7 @@ export default function movingRuleSection( }); duplicateRuleButton.addEventListener("click", () => { plugin.settings.movingRules.push(new MovingRule(rule.regex, rule.folder)); - this.display(); + display(); }); const deleteRuleButton = child.createEl("button", { @@ -75,7 +75,7 @@ export default function movingRuleSection( plugin.settings.movingRules = plugin.settings.movingRules.filter( (r) => r !== rule, ); - this.display(); + display(); }); } } diff --git a/Settings/SettingsTab.ts b/Settings/SettingsTab.ts index 45d8a14..1394d04 100644 --- a/Settings/SettingsTab.ts +++ b/Settings/SettingsTab.ts @@ -15,7 +15,7 @@ export class SettingsTab extends PluginSettingTab { } // how to call rerendering from child section? - display(): void { + display = () => { const { containerEl } = this; containerEl.empty(); @@ -108,19 +108,6 @@ export class SettingsTab extends PluginSettingTab { }), ); - // there is no default event to move on save, therefore, i'd need to define a new one - // running move on change could be an option, but it would produce an overhead in performance because of constant checking for changes - // - // new obsidian.Setting(containerEl) - // .setName("Move on save") - // .setDesc("Should the file be moved when it is saved?") - // .addToggle((cb) => - // cb.setValue(this.plugin.settings.moveOnSave).onChange(async (value) => { - // this.plugin.settings.moveOnSave = value; - // await this.plugin.saveData(this.plugin.settings); - // }), - // ); - // TUTORIAL START /** * Instead of using the default .setting-item class I created a custom class to add other styling, @@ -170,8 +157,8 @@ export class SettingsTab extends PluginSettingTab { example2.createSpan({ text: "Scrolls/$1", cls: "rule_title" }); // TUTORIAL END - movingRuleSection(containerEl, this.plugin); - exclusionSection(containerEl, this.plugin); - tagSection(containerEl, this.plugin); - } + movingRuleSection(containerEl, this.plugin, this.display); + exclusionSection(containerEl, this.plugin, this.display); + tagSection(containerEl, this.plugin, this.display); + }; } diff --git a/Settings/TagSection.ts b/Settings/TagSection.ts index 4ce3807..caed175 100644 --- a/Settings/TagSection.ts +++ b/Settings/TagSection.ts @@ -2,7 +2,11 @@ import { MovingRule } from "Models/MovingRule"; import * as obsidian from "obsidian"; import type AutoMoverPlugin from "main"; -export function tagSection(containerEl: HTMLElement, plugin: AutoMoverPlugin) { +export function tagSection( + containerEl: HTMLElement, + plugin: AutoMoverPlugin, + display: () => void, +) { /** * Header for excluded folders */ @@ -28,7 +32,7 @@ export function tagSection(containerEl: HTMLElement, plugin: AutoMoverPlugin) { }); addTagButton.addEventListener("click", () => { plugin.settings.tagRules.push(new MovingRule()); - this.display(); + display(); }); /** @@ -59,7 +63,7 @@ export function tagSection(containerEl: HTMLElement, plugin: AutoMoverPlugin) { }); duplicateRuleButton.addEventListener("click", () => { plugin.settings.tagRules.push(new MovingRule(rule.regex, rule.folder)); - this.display(); + display(); }); const deleteRuleButton = child.createEl("button", { @@ -70,7 +74,7 @@ export function tagSection(containerEl: HTMLElement, plugin: AutoMoverPlugin) { plugin.settings.tagRules = plugin.settings.tagRules.filter( (r) => r !== rule, ); - this.display(); + display(); }); } } From c88637303809928e3374a5fe2156d859bb405051 Mon Sep 17 00:00:00 2001 From: Al0cam Date: Sat, 27 Sep 2025 19:01:00 +0200 Subject: [PATCH 4/6] feat: added tag moving --- Utils/RuleMatcherUtil.ts | 50 ++++++++++++++++++------- biome.json | 65 ++++++++++++++++---------------- main.ts | 80 +++++++++++++++++++++++++--------------- 3 files changed, 119 insertions(+), 76 deletions(-) diff --git a/Utils/RuleMatcherUtil.ts b/Utils/RuleMatcherUtil.ts index a10b82f..209aabd 100644 --- a/Utils/RuleMatcherUtil.ts +++ b/Utils/RuleMatcherUtil.ts @@ -1,5 +1,5 @@ import type { MovingRule } from "Models/MovingRule"; -import type { TFile } from "obsidian"; +import type { TagCache, TFile } from "obsidian"; class RuleMatcherUtil { private static instance: RuleMatcherUtil; @@ -21,7 +21,7 @@ class RuleMatcherUtil { * @param rules * @returns MovingRule | null */ - public getMatchingRule(file: TFile, rules: MovingRule[]): MovingRule | null { + public getMatchingRuleByName(file: TFile, rules: MovingRule[]): MovingRule | null { for (const rule of rules) { if (rule.regex == null || rule.regex === "") { console.error("Rule does not have a regex: ", rule); @@ -42,6 +42,37 @@ class RuleMatcherUtil { return null; } + /** + * Returns the first rule that matches any of the tags + * If no rule matches, returns null + * @param tags + * @param rules + * @returns MovingRule | null + */ + public getMatchingRuleByTag(tags: TagCache[], rules: MovingRule[]): MovingRule | null { + for (const rule of rules) { + if (rule.regex == null || rule.regex === "") { + console.error("Tag Rule does not have a regex: ", rule); + continue; + } + if (rule.folder == null || rule.folder === "") { + console.error("Tag Rule does not have a destination folder: ", rule); + continue; + } + if (!this.isValidRegex(rule.regex)) { + console.error("Tag Rule has an invalid regex: ", rule); + continue; + } + + for (const tag of tags) { + if (tag.tag.match(rule.regex)) { + return rule; + } + } + } + return null; + } + /** * Returns the regex groups of the file that match the rule * If no groups match, returns an empty array @@ -50,10 +81,7 @@ class RuleMatcherUtil { * @param rule * @returns string[] */ - public getGroupMatches( - file: TFile, - rule: MovingRule, - ): RegExpMatchArray | null { + public getGroupMatches(file: TFile, rule: MovingRule): RegExpMatchArray | null { const matches = file.name.match(rule.regex); return matches; } @@ -84,10 +112,7 @@ class RuleMatcherUtil { * @param rule * @param matches */ - public constructFinalDesinationPath( - rule: MovingRule, - matches: RegExpMatchArray, - ): string { + public constructFinalDesinationPath(rule: MovingRule, matches: RegExpMatchArray): string { let folderPath = rule.folder; const unnamedGroups = this.getUnnamedGroups(rule.regex); // it has been asserted before that there is no way this can be null before the call, the rule and file are checked to be valid @@ -105,10 +130,7 @@ class RuleMatcherUtil { * @returns boolean */ public isRegexGrouped(rule: MovingRule): boolean { - return ( - this.doesDestinationUseGroups(rule) && - this.getUnnamedGroups(rule.regex) != null - ); + return this.doesDestinationUseGroups(rule) && this.getUnnamedGroups(rule.regex) != null; } /** diff --git a/biome.json b/biome.json index 78fbd4c..6901a28 100644 --- a/biome.json +++ b/biome.json @@ -1,34 +1,35 @@ { - "$schema": "https://biomejs.dev/schemas/2.0.6/schema.json", - "vcs": { - "enabled": false, - "clientKind": "git", - "useIgnoreFile": false - }, - "files": { - "ignoreUnknown": false - }, - "formatter": { - "enabled": true, - "indentStyle": "space", - "indentWidth": 2 - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true - } - }, - "javascript": { - "formatter": { - "quoteStyle": "double" - } - }, - "assist": { - "actions": { - "source": { - "organizeImports": "on" - } - } - } + "$schema": "https://biomejs.dev/schemas/2.2.2/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 120 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "assist": { + "actions": { + "source": { + "organizeImports": "on" + } + } + } } diff --git a/main.ts b/main.ts index 07a84a0..a836749 100644 --- a/main.ts +++ b/main.ts @@ -17,17 +17,12 @@ export default class AutoMoverPlugin extends obsidian.Plugin { /** * Loading settings and creating the settings tab */ - this.settings = Object.assign( - {}, - Settings.DEFAULT_SETTINGS, - await this.loadData(), - ); + this.settings = Object.assign({}, Settings.DEFAULT_SETTINGS, await this.loadData()); this.addSettingTab(new SettingsTab(this.app, this)); // negative ifs for easier reading and debugging if (!this.areMovingTriggersEnabled()) return; if (!this.areThereRulesToApply()) return; - if (!this.areRulesValid()) return; this.registerEvent( this.app.workspace.on("file-open", (file: obsidian.TFile) => { @@ -98,10 +93,7 @@ export default class AutoMoverPlugin extends obsidian.Plugin { if (!this.areThereExcludedFolders()) return false; if (!this.areThereRulesToApply()) return false; - return exclusionMatcherUtil.isFilePathExcluded( - file, - this.settings.exclusionRules, - ); + return exclusionMatcherUtil.isFilePathExcluded(file, this.settings.exclusionRules); } /** @@ -112,29 +104,56 @@ export default class AutoMoverPlugin extends obsidian.Plugin { */ matchAndMoveFile(file: obsidian.TFile): void { // console.log("Moving file: ", file.path); - const rule = ruleMatcherUtil.getMatchingRule( - file, - this.settings.movingRules, - ); - if (rule == null || rule.folder == null) return; + if (this.matchAndMoveByName(file)) return; + else this.matchAndMoveByTag(file); + } + + /** + * Matches the file by name and moves it to the destination folder + * + * @param file - File to be matched and moved + * @returns true if the file was moved, false otherwise + */ + matchAndMoveByName(file: obsidian.TFile): boolean { + const rule = ruleMatcherUtil.getMatchingRuleByName(file, this.settings.movingRules); + if (rule == null || rule.folder == null) return false; + if (ruleMatcherUtil.isRegexGrouped(rule)) { const matches = ruleMatcherUtil.getGroupMatches(file, rule); - const finalDestinationPath = ruleMatcherUtil.constructFinalDesinationPath( - rule, - matches!, - ); + const finalDestinationPath = ruleMatcherUtil.constructFinalDesinationPath(rule, matches!); movingUtil.moveFile(file, finalDestinationPath); } else { movingUtil.moveFile(file, rule.folder); } + return true; + } + + /** + * Matches the file by tags it contains and moves it to the destination folder + * + * @param file - File to be matched and moved + * @returns true if the file was moved, false otherwise + */ + matchAndMoveByTag(file: obsidian.TFile): boolean { + const tags = this.app.metadataCache.getFileCache(file)?.tags; + if (tags == null || tags.length === 0) return false; + + const tagRule = ruleMatcherUtil.getMatchingRuleByTag(tags, this.settings.tagRules); + + if (tagRule == null || tagRule.folder == null) return false; + + if (ruleMatcherUtil.isRegexGrouped(tagRule)) { + const matches = ruleMatcherUtil.getGroupMatches(file, tagRule); + const finalDestinationPath = ruleMatcherUtil.constructFinalDesinationPath(tagRule, matches!); + movingUtil.moveFile(file, finalDestinationPath); + } else { + movingUtil.moveFile(file, tagRule.folder); + } + return true; } async asyncloadSettings() { - this.settings = Object.assign( - {}, - Settings.DEFAULT_SETTINGS, - await this.loadData(), - ); + this.settings = Object.assign({}, Settings.DEFAULT_SETTINGS, await this.loadData()); } async onunload() { @@ -156,10 +175,10 @@ export default class AutoMoverPlugin extends obsidian.Plugin { */ areThereRulesToApply(): boolean { return ( - this.settings.movingRules.length > 0 && - this.settings.movingRules.every( - (rule) => rule.regex !== "" && rule.folder !== "", - ) + (this.settings.movingRules.length > 0 && + this.settings.movingRules.every((rule) => rule.regex !== "" && rule.folder !== "")) || + (this.settings.tagRules.length > 0 && + this.settings.tagRules.every((rule) => rule.regex !== "" && rule.folder !== "")) ); } @@ -176,8 +195,9 @@ export default class AutoMoverPlugin extends obsidian.Plugin { * @returns boolean */ areRulesValid(): boolean { - return this.settings.movingRules.every( - (rule) => rule.regex !== "" && rule.folder !== "", + return ( + this.settings.movingRules.every((rule) => rule.regex !== "" && rule.folder !== "") && + this.settings.tagRules.every((rule) => rule.regex !== "" && rule.folder !== "") ); } From 4a1a0950cb839d2dd3928c76c01cee1663e64513 Mon Sep 17 00:00:00 2001 From: Al0cam Date: Sat, 27 Sep 2025 19:16:57 +0200 Subject: [PATCH 5/6] doc: updated readme, bumped version, moved tags above exclusions --- README.md | 8 ++++++++ Settings/SettingsTab.ts | 34 ++++++++++------------------------ manifest.json | 2 +- package.json | 6 +++--- versions.json | 2 +- 5 files changed, 23 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index d8bb777..6e332c2 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,13 @@ Other examples could include: 3. 72:30:00 -> triggers every 3 days and 30 minutes (if your Device is online and obsidian runnning for that long) +### Tag rules UI +From patch 1.0.6 onwards, you can move files using tags. +It takes into account the first rule that is a match, +**if it first matches with a FileName rule, it won't check the Tag rules.** +There are no pictures for this one, as it is identical to the previous ruleset. + + ### Exclusion rules UI From patch 1.0.2 onwards, you can exclude files and folders from being moved. The UI that is used for the exclusion rules is the same as the one used for the search criteria. @@ -193,6 +200,7 @@ Thank you! - [x] Exposing the move files button to the left toolbar - [x] Exposing the move files to the commands accessible via command palette - [x] Add import and export of rules +- [x] Add #tag rule support - [ ] Add a file like .gitignore which contains all the moving rules (i am assuming the list can grow quite big for some people) - [ ] Auto tagging of moved files with the destination folder name (last folder in the path) - [ ] Add undo button to the notification popup for the moved files diff --git a/Settings/SettingsTab.ts b/Settings/SettingsTab.ts index 1394d04..5f46b6f 100644 --- a/Settings/SettingsTab.ts +++ b/Settings/SettingsTab.ts @@ -14,7 +14,6 @@ export class SettingsTab extends PluginSettingTab { this.plugin = plugin; } - // how to call rerendering from child section? display = () => { const { containerEl } = this; containerEl.empty(); @@ -39,10 +38,7 @@ export class SettingsTab extends PluginSettingTab { if (importedSettings) { this.plugin.settings = importedSettings; await this.plugin.saveData(this.plugin.settings); - this.plugin.app.workspace.trigger( - "AutoMover:automatic-moving-update", - this.plugin.settings, - ); + this.plugin.app.workspace.trigger("AutoMover:automatic-moving-update", this.plugin.settings); this.display(); } }); @@ -60,9 +56,7 @@ export class SettingsTab extends PluginSettingTab { new Setting(containerEl) .setName("Manually move") - .setDesc( - "Execute the command to go through your notes and move them according to the rules specified below.", - ) + .setDesc("Execute the command to go through your notes and move them according to the rules specified below.") .addButton((button) => { button.setButtonText("Move files"); button.onClick(async () => { @@ -81,17 +75,12 @@ export class SettingsTab extends PluginSettingTab { ) .setClass("timer-setting") .addToggle((cb) => - cb - .setValue(this.plugin.settings.automaticMoving) - .onChange(async (value) => { - this.plugin.settings.automaticMoving = value; - await this.plugin.saveData(this.plugin.settings); - this.app.workspace.trigger( - "AutoMover:automatic-moving-update", - this.plugin.settings, - ); - this.display(); - }), + cb.setValue(this.plugin.settings.automaticMoving).onChange(async (value) => { + this.plugin.settings.automaticMoving = value; + await this.plugin.saveData(this.plugin.settings); + this.app.workspace.trigger("AutoMover:automatic-moving-update", this.plugin.settings); + this.display(); + }), ) .addText((cb) => cb @@ -101,10 +90,7 @@ export class SettingsTab extends PluginSettingTab { .onChange(async (value) => { this.plugin.settings.timer = timerUtil.parseTimeToMs(value); await this.plugin.saveData(this.plugin.settings); - this.app.workspace.trigger( - "AutoMover:automatic-moving-update", - this.plugin.settings, - ); + this.app.workspace.trigger("AutoMover:automatic-moving-update", this.plugin.settings); }), ); @@ -158,7 +144,7 @@ export class SettingsTab extends PluginSettingTab { // TUTORIAL END movingRuleSection(containerEl, this.plugin, this.display); - exclusionSection(containerEl, this.plugin, this.display); tagSection(containerEl, this.plugin, this.display); + exclusionSection(containerEl, this.plugin, this.display); }; } diff --git a/manifest.json b/manifest.json index 3609020..1222bb5 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "auto-mover", "name": "AutoMover", - "version": "1.0.5", + "version": "1.0.6", "minAppVersion": "0.15.0", "description": "Moves files with specified names into the same folder.", "author": "Al0cam", diff --git a/package.json b/package.json index fbaee84..bc002ff 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "obsidian-sample-plugin", - "version": "1.0.0", - "description": "This is a sample plugin for Obsidian (https://obsidian.md)", + "name": "AutoMover", + "version": "1.0.6", + "description": "AutoMover: Moves files with specified names/tags where you want.", "main": "main.js", "scripts": { "dev": "node esbuild.config.mjs", diff --git a/versions.json b/versions.json index 26382a1..94fc44d 100644 --- a/versions.json +++ b/versions.json @@ -1,3 +1,3 @@ { - "1.0.0": "0.15.0" + "1.0.6": "0.15.0" } From 7f271e80460f866926d772b7356decca8b2367c9 Mon Sep 17 00:00:00 2001 From: Al0cam Date: Sat, 27 Sep 2025 19:27:22 +0200 Subject: [PATCH 6/6] chore: removed unused TagUtil --- Utils/TagUtil.ts | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 Utils/TagUtil.ts diff --git a/Utils/TagUtil.ts b/Utils/TagUtil.ts deleted file mode 100644 index c514478..0000000 --- a/Utils/TagUtil.ts +++ /dev/null @@ -1,14 +0,0 @@ -class TagUtil { - private static instance: TagUtil; - private constructor() {} - - public static getInstance(): TagUtil { - if (!TagUtil.instance) { - TagUtil.instance = new TagUtil(); - } - return TagUtil.instance; - } -} - -const tagUtil = TagUtil.getInstance(); -export default tagUtil;