2024-10-20 15:55:21 +00:00
|
|
|
import { getLinkpath, Plugin, Notice, TFolder, TFile } from "obsidian";
|
2024-10-03 20:39:37 +00:00
|
|
|
import RedirectSettingsTab from "components/Settings";
|
2024-09-27 16:47:19 +00:00
|
|
|
|
2025-07-26 22:07:20 +00:00
|
|
|
// --- Setup ---
|
2024-09-27 16:47:19 +00:00
|
|
|
interface RedirectSettings {
|
2024-10-10 19:25:23 +00:00
|
|
|
openInNewTab: boolean;
|
|
|
|
|
switchToNewTab: boolean;
|
2024-09-16 18:31:09 +00:00
|
|
|
}
|
|
|
|
|
|
2024-09-27 16:47:19 +00:00
|
|
|
const DEFAULT_SETTINGS: RedirectSettings = {
|
2024-10-10 19:25:23 +00:00
|
|
|
openInNewTab: false,
|
|
|
|
|
switchToNewTab: false,
|
2024-09-27 16:47:19 +00:00
|
|
|
};
|
|
|
|
|
|
2025-07-26 22:07:20 +00:00
|
|
|
// --- Main Plugin Class ---
|
2024-09-27 16:47:19 +00:00
|
|
|
export default class RedirectPlugin extends Plugin {
|
2025-07-26 22:07:20 +00:00
|
|
|
// --- Properties ---
|
2024-09-27 16:47:19 +00:00
|
|
|
settings: RedirectSettings;
|
2024-12-04 20:38:35 +00:00
|
|
|
redirectsFolder: TFolder | null = null;
|
2025-07-26 20:45:24 +00:00
|
|
|
isMovingNote = false;
|
|
|
|
|
isBypassRedirect = false;
|
2024-12-04 20:38:35 +00:00
|
|
|
|
2025-07-26 22:07:20 +00:00
|
|
|
// --- Lifecycle methods and commands---
|
|
|
|
|
async onload() {
|
|
|
|
|
await this.loadSettings();
|
2024-10-12 12:29:36 +00:00
|
|
|
|
2025-07-26 22:07:20 +00:00
|
|
|
this.addSettingTab(new RedirectSettingsTab(this.app, this));
|
2024-10-30 22:07:05 +00:00
|
|
|
|
2025-07-26 22:07:20 +00:00
|
|
|
this.app.workspace.onLayoutReady(async () => {
|
|
|
|
|
await this.createRedirectsFolder();
|
|
|
|
|
await this.redirect();
|
|
|
|
|
});
|
2024-10-31 19:37:04 +00:00
|
|
|
|
2025-07-26 22:07:20 +00:00
|
|
|
this.app.workspace.on("file-open", this.handleFileOpen);
|
2024-10-11 21:08:49 +00:00
|
|
|
|
2025-07-26 22:07:20 +00:00
|
|
|
this.addCommand({
|
|
|
|
|
id: "paste-redirect-syntax",
|
|
|
|
|
name: "Paste redirect syntax onto selection",
|
|
|
|
|
editorCallback: (editor, view) => {
|
|
|
|
|
const selection = editor.getSelection();
|
|
|
|
|
editor.replaceSelection(`::>[[${selection}]]`);
|
|
|
|
|
const { ch, line } = editor.getCursor();
|
|
|
|
|
if (!selection) {
|
|
|
|
|
editor.setCursor({
|
|
|
|
|
ch: ch - 2,
|
|
|
|
|
line: line,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
});
|
2024-10-11 21:08:49 +00:00
|
|
|
|
2025-07-26 22:07:20 +00:00
|
|
|
this.addCommand({
|
2025-08-02 22:17:53 +00:00
|
|
|
id: "bypass-forwarding",
|
|
|
|
|
name: "Bypass forwarding to target note",
|
2025-07-26 22:07:20 +00:00
|
|
|
callback: () => {
|
|
|
|
|
this.isBypassRedirect = true;
|
|
|
|
|
new Notice("Bypassing redirects.");
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
2024-10-11 21:08:49 +00:00
|
|
|
|
2025-07-26 22:07:20 +00:00
|
|
|
onunload() {
|
|
|
|
|
this.app.workspace.off("file-open", this.handleFileOpen);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Event Handlers ---
|
|
|
|
|
handleFileOpen = async (file: TFile) => {
|
|
|
|
|
if (this.isMovingNote) return;
|
|
|
|
|
if (this.isBypassRedirect) {
|
|
|
|
|
this.isBypassRedirect = false;
|
|
|
|
|
return;
|
2025-07-26 20:45:24 +00:00
|
|
|
}
|
2025-07-26 22:07:20 +00:00
|
|
|
await this.redirect();
|
|
|
|
|
};
|
2025-07-26 20:45:24 +00:00
|
|
|
|
2025-07-26 22:07:20 +00:00
|
|
|
async changeSettings(newTab: boolean, switchToTab: boolean): Promise<void> {
|
|
|
|
|
this.settings.openInNewTab = newTab;
|
|
|
|
|
this.settings.switchToNewTab = switchToTab;
|
|
|
|
|
await this.saveSettings();
|
2025-07-26 21:51:16 +00:00
|
|
|
}
|
|
|
|
|
|
2025-07-26 22:07:20 +00:00
|
|
|
// --- Core Logic ---
|
2025-07-26 21:51:16 +00:00
|
|
|
private async redirect(): Promise<void> {
|
|
|
|
|
const fileAndContent = await this.getCurrentFile();
|
|
|
|
|
if (!fileAndContent) return;
|
|
|
|
|
|
|
|
|
|
const { currentFile, content } = fileAndContent;
|
|
|
|
|
const targetNoteFile = this.getTargetFile(content);
|
|
|
|
|
if (!targetNoteFile) return;
|
|
|
|
|
|
|
|
|
|
await this.moveRedirectNote(currentFile);
|
|
|
|
|
await this.openTargetNote(targetNoteFile);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-26 22:07:20 +00:00
|
|
|
// --- Helper functions ---
|
2024-10-04 19:24:25 +00:00
|
|
|
|
2024-10-20 17:08:44 +00:00
|
|
|
private async moveRedirectNote(
|
2024-10-15 21:48:43 +00:00
|
|
|
redirectingNote: TFile | null
|
2025-07-26 22:07:20 +00:00
|
|
|
): Promise<TFile | void> {
|
2025-07-26 21:51:16 +00:00
|
|
|
if (
|
|
|
|
|
!redirectingNote ||
|
|
|
|
|
redirectingNote.path === `_forwards/${redirectingNote.name}`
|
|
|
|
|
) {
|
2024-10-11 21:08:49 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-26 21:51:16 +00:00
|
|
|
new Notice(
|
|
|
|
|
`Moving ${redirectingNote.name} to the _forwards folder.`,
|
|
|
|
|
2000
|
|
|
|
|
);
|
|
|
|
|
|
2024-10-20 17:08:44 +00:00
|
|
|
if (!this.redirectsFolder) {
|
2024-10-15 21:48:43 +00:00
|
|
|
await this.createRedirectsFolder();
|
2024-10-30 22:07:05 +00:00
|
|
|
}
|
2024-10-12 12:29:36 +00:00
|
|
|
|
2024-10-30 22:07:05 +00:00
|
|
|
try {
|
2025-07-26 20:45:24 +00:00
|
|
|
this.isMovingNote = true;
|
2024-12-04 21:20:25 +00:00
|
|
|
const redirectingNoteInFolder = await this.app.vault.copy(
|
2024-10-30 22:07:05 +00:00
|
|
|
redirectingNote,
|
|
|
|
|
`/_forwards/${redirectingNote.name}`
|
2024-10-04 19:24:25 +00:00
|
|
|
);
|
2024-10-31 19:37:04 +00:00
|
|
|
|
2024-10-31 20:53:08 +00:00
|
|
|
if (this.settings.openInNewTab) {
|
2025-07-26 21:51:16 +00:00
|
|
|
await this.openTargetNote(redirectingNoteInFolder);
|
2024-10-31 20:53:08 +00:00
|
|
|
}
|
2024-10-20 15:55:21 +00:00
|
|
|
|
2025-07-26 20:45:24 +00:00
|
|
|
// Delay deletion to avoid race condition where the original note is deleted before the new note is fully opened in the UI.
|
2024-12-11 21:04:25 +00:00
|
|
|
setTimeout(async () => {
|
|
|
|
|
await this.deleteNote(redirectingNote);
|
2025-07-26 20:45:24 +00:00
|
|
|
this.isMovingNote = false;
|
2024-12-11 21:04:25 +00:00
|
|
|
}, 500);
|
2024-10-31 20:53:08 +00:00
|
|
|
} catch (error) {
|
2025-07-26 20:45:24 +00:00
|
|
|
this.isMovingNote = false;
|
2024-11-03 22:04:06 +00:00
|
|
|
console.error(error);
|
2024-10-31 20:53:08 +00:00
|
|
|
}
|
2024-10-20 15:55:21 +00:00
|
|
|
}
|
2024-10-04 19:24:25 +00:00
|
|
|
|
2025-07-26 22:07:20 +00:00
|
|
|
private async openTargetNote(targetNoteFile: TFile) {
|
|
|
|
|
await this.app.workspace.openLinkText(
|
|
|
|
|
targetNoteFile.name,
|
|
|
|
|
targetNoteFile.path,
|
|
|
|
|
this.settings.openInNewTab,
|
|
|
|
|
{ active: this.settings.switchToNewTab }
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async createRedirectsFolder() {
|
2024-10-15 21:48:43 +00:00
|
|
|
try {
|
2025-07-26 22:07:20 +00:00
|
|
|
this.redirectsFolder = await this.app.vault.createFolder(
|
|
|
|
|
"/_forwards"
|
|
|
|
|
);
|
2024-10-15 21:48:43 +00:00
|
|
|
} catch (error) {
|
2025-07-26 22:07:20 +00:00
|
|
|
console.warn(
|
|
|
|
|
"`_forwards` folder not created or already exists.",
|
|
|
|
|
error
|
2024-10-15 21:48:43 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-26 22:07:20 +00:00
|
|
|
private async getCurrentFile(): Promise<{
|
|
|
|
|
currentFile: TFile;
|
|
|
|
|
content: string;
|
|
|
|
|
} | null> {
|
|
|
|
|
const currentFile = this.app.workspace.getActiveFile();
|
|
|
|
|
if (!currentFile) {
|
|
|
|
|
return null;
|
2025-07-26 21:51:16 +00:00
|
|
|
}
|
2025-07-26 22:07:20 +00:00
|
|
|
return {
|
|
|
|
|
currentFile: currentFile,
|
|
|
|
|
content: (await this.getCurrentFileContent(currentFile)) || "",
|
|
|
|
|
};
|
2025-07-26 21:51:16 +00:00
|
|
|
}
|
|
|
|
|
|
2025-07-26 22:07:20 +00:00
|
|
|
private async getCurrentFileContent(
|
|
|
|
|
currentFile: TFile
|
|
|
|
|
): Promise<string | null> {
|
|
|
|
|
try {
|
|
|
|
|
return await this.app.vault.read(currentFile);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
new Notice(`Cannot read ${currentFile}.`);
|
|
|
|
|
console.error(`Failed to read ${currentFile} content: `, error);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-10-15 21:48:43 +00:00
|
|
|
|
2025-07-26 22:07:20 +00:00
|
|
|
private getTargetFile(currentFileContent: string): TFile | null {
|
|
|
|
|
const linkTextRegex = /::>\[\[(.*?)\]\]/i;
|
|
|
|
|
const targetNoteName = currentFileContent.match(linkTextRegex)?.at(1);
|
2024-10-15 21:48:43 +00:00
|
|
|
|
2025-07-26 22:07:20 +00:00
|
|
|
if (!targetNoteName) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const targetNoteFile = this.app.metadataCache.getFirstLinkpathDest(
|
|
|
|
|
getLinkpath(targetNoteName),
|
|
|
|
|
""
|
|
|
|
|
);
|
2024-10-21 19:02:28 +00:00
|
|
|
|
2025-07-26 22:07:20 +00:00
|
|
|
if (!targetNoteFile) {
|
|
|
|
|
new Notice(
|
|
|
|
|
"Target note not found. Create one and try again.",
|
|
|
|
|
3500
|
|
|
|
|
);
|
|
|
|
|
console.error(
|
|
|
|
|
`No target note file found for target: ${targetNoteName}. You can create one manually and try again.`
|
|
|
|
|
);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2025-07-26 20:45:24 +00:00
|
|
|
|
2025-07-26 22:07:20 +00:00
|
|
|
return targetNoteFile;
|
2024-09-16 18:31:09 +00:00
|
|
|
}
|
|
|
|
|
|
2025-07-26 22:07:20 +00:00
|
|
|
private async deleteNote(orgRedirectingNote: TFile): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
await this.app.fileManager.trashFile(orgRedirectingNote);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
new Notice(
|
|
|
|
|
"Failed to delete directing note from original location. Please delete manually.",
|
|
|
|
|
4500
|
|
|
|
|
);
|
|
|
|
|
console.error(error);
|
|
|
|
|
}
|
2024-10-15 21:48:43 +00:00
|
|
|
}
|
2024-09-27 16:47:19 +00:00
|
|
|
|
2025-07-26 22:07:20 +00:00
|
|
|
// --- Settings Persistence ---
|
2024-09-16 18:31:09 +00:00
|
|
|
async loadSettings() {
|
2024-10-15 21:48:43 +00:00
|
|
|
try {
|
|
|
|
|
this.settings = Object.assign(
|
|
|
|
|
{},
|
|
|
|
|
DEFAULT_SETTINGS,
|
|
|
|
|
await this.loadData()
|
|
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to load settings: ", error);
|
|
|
|
|
}
|
2024-09-16 18:31:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async saveSettings() {
|
2024-10-15 21:48:43 +00:00
|
|
|
try {
|
|
|
|
|
await this.saveData(this.settings);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to save settings: ", error);
|
|
|
|
|
}
|
2024-09-16 18:31:09 +00:00
|
|
|
}
|
|
|
|
|
}
|