mirror of
https://github.com/al0cam/AutoMover.git
synced 2026-07-22 05:41:25 +00:00
commit
0459f5f5cf
14 changed files with 430 additions and 277 deletions
20
IO/RuleIO.ts
Normal file
20
IO/RuleIO.ts
Normal file
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
74
Settings/ExclusionSection.ts
Normal file
74
Settings/ExclusionSection.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { ExclusionRule } from "Models/ExclusionRule";
|
||||
import type AutoMoverPlugin from "main";
|
||||
import { Setting } from "obsidian";
|
||||
|
||||
export function exclusionSection(
|
||||
containerEl: HTMLElement,
|
||||
plugin: AutoMoverPlugin,
|
||||
display: () => void,
|
||||
) {
|
||||
/**
|
||||
* 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());
|
||||
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));
|
||||
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,
|
||||
);
|
||||
display();
|
||||
});
|
||||
}
|
||||
}
|
||||
81
Settings/MovingRuleSection.ts
Normal file
81
Settings/MovingRuleSection.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import AutoMoverPlugin from "main";
|
||||
import { MovingRule } from "Models/MovingRule";
|
||||
import { Setting } from "obsidian";
|
||||
|
||||
export default function movingRuleSection(
|
||||
containerEl: HTMLElement,
|
||||
plugin: AutoMoverPlugin,
|
||||
display: () => void,
|
||||
) {
|
||||
/**
|
||||
* 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());
|
||||
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));
|
||||
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,
|
||||
);
|
||||
display();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AutoMoverSettings> = {
|
|||
// moveOnSave: true,
|
||||
movingRules: [],
|
||||
exclusionRules: [],
|
||||
tagRules: [],
|
||||
automaticMoving: false,
|
||||
timer: null,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,24 +1,25 @@
|
|||
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;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
display = () => {
|
||||
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.
|
||||
|
|
@ -37,16 +38,13 @@ export class SettingsTab extends obsidian.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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
new obsidian.Setting(containerEl)
|
||||
new Setting(containerEl)
|
||||
.setName("Move on open")
|
||||
.setDesc("Should the file be moved when it is opened?")
|
||||
.addToggle((cb) =>
|
||||
|
|
@ -56,11 +54,9 @@ 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.",
|
||||
)
|
||||
.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 () => {
|
||||
|
|
@ -70,7 +66,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.
|
||||
|
|
@ -79,17 +75,12 @@ export class SettingsTab extends obsidian.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
|
||||
|
|
@ -99,26 +90,10 @@ export class SettingsTab extends obsidian.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);
|
||||
}),
|
||||
);
|
||||
|
||||
// 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,
|
||||
|
|
@ -127,7 +102,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 +143,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, this.display);
|
||||
tagSection(containerEl, this.plugin, this.display);
|
||||
exclusionSection(containerEl, this.plugin, this.display);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
80
Settings/TagSection.ts
Normal file
80
Settings/TagSection.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { MovingRule } from "Models/MovingRule";
|
||||
import * as obsidian from "obsidian";
|
||||
import type AutoMoverPlugin from "main";
|
||||
|
||||
export function tagSection(
|
||||
containerEl: HTMLElement,
|
||||
plugin: AutoMoverPlugin,
|
||||
display: () => void,
|
||||
) {
|
||||
/**
|
||||
* 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());
|
||||
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));
|
||||
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,
|
||||
);
|
||||
display();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
65
biome.json
65
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
80
main.ts
80
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 !== "")
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"1.0.0": "0.15.0"
|
||||
"1.0.6": "0.15.0"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue