mirror of
https://github.com/al0cam/AutoMover.git
synced 2026-07-22 05:41:25 +00:00
feat: added exclusion rules for folders and files with regex support
This commit is contained in:
parent
d615af51e1
commit
d1dedf2db1
7 changed files with 212 additions and 53 deletions
13
Models/ExclusionRule.ts
Normal file
13
Models/ExclusionRule.ts
Normal file
|
|
@ -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 || "";
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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<AutoMoverSettings> = {
|
||||
moveOnOpen: true,
|
||||
moveOnSave: true,
|
||||
moveOnClose: false,
|
||||
moveOnCreate: false,
|
||||
movingRules: [],
|
||||
}
|
||||
excludedFolders: [],
|
||||
};
|
||||
|
||||
function loadSettings(AutoMoverPlugin: AutoMoverPlugin): Partial<AutoMoverSettings> {
|
||||
function loadSettings(
|
||||
AutoMoverPlugin: AutoMoverPlugin,
|
||||
): Partial<AutoMoverSettings> {
|
||||
return Object.assign({}, DEFAULT_SETTINGS, AutoMoverPlugin.loadData());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
37
Utils/ExclusionMatcherUtil.ts
Normal file
37
Utils/ExclusionMatcherUtil.ts
Normal file
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
74
main.ts
74
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 !== "");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue