devonthesofa_obsidian-note-.../integrations/context-menu/contextMenuIntegration.tsx
2025-11-21 12:51:08 +01:00

215 lines
5 KiB
TypeScript

import eventBus from "core/eventBus";
import {
Menu,
Notice,
Plugin,
TAbstractFile,
TFile,
TFolder,
WorkspaceLeaf,
} from "obsidian";
import {
BaseNoteStatusService,
MultipleNoteStatusService,
NoteStatusService,
} from "@/core/noteStatusService";
import settingsService from "@/core/settingsService";
export class ContextMenuIntegration {
private static instance: ContextMenuIntegration | null = null;
private plugin: Plugin;
private currentLeaf: WorkspaceLeaf | null = null;
private noteStatusService:
| NoteStatusService
| MultipleNoteStatusService
| null = null;
constructor(plugin: Plugin) {
if (ContextMenuIntegration.instance) {
throw new Error("The context menu instance is already created");
}
this.plugin = plugin;
ContextMenuIntegration.instance = this;
}
async integrate() {
this.plugin.registerEvent(
this.plugin.app.workspace.on(
"files-menu",
(
menu: Menu,
files: TAbstractFile[],
source: string,
leaf?: WorkspaceLeaf,
) => {
menu.addItem((item) => {
item.setTitle("Change note status")
.setIcon("rotate-ccw") // Lucide icon
.onClick(async () => {
const tFiles = files.filter(
(f): f is TFile => f instanceof TFile,
);
if (!tFiles.length) {
new Notice(
"Select at least one file to change its status.",
);
return;
}
this.openMultipleFilesStatusesModal(tFiles);
});
});
},
),
);
this.plugin.registerEvent(
this.plugin.app.workspace.on("file-menu", (menu, file) => {
if (file instanceof TFile) {
menu.addItem((item) => {
item.setTitle("Change note state")
.setIcon("rotate-ccw")
.onClick(async () => {
this.openSingleFileStatusesModal(file);
});
});
} else if (file instanceof TFolder) {
this.addFolderStatusActions(menu, file);
}
}),
);
this.plugin.registerEvent(
this.plugin.app.workspace.on(
"editor-menu",
(menu, editor, view) => {
menu.addItem((item) => {
item.setTitle("Change note status")
.setIcon("rotate-ccw") // Lucide icon
.onClick(async () => {
if (
!view.file ||
!(view.file instanceof TFile)
) {
new Notice(
"The selected item is not a valid file.",
);
return;
}
this.openSingleFileStatusesModal(view.file);
});
});
},
),
);
}
private openMultipleFilesStatusesModal(tFiles: TFile[]) {
const multipleStatusService = new MultipleNoteStatusService(tFiles);
multipleStatusService.populateStatuses();
eventBus.publish("triggered-open-modal", {
statusService: multipleStatusService,
});
}
private openSingleFileStatusesModal(tFile: TFile) {
const noteStatusService = new NoteStatusService(tFile);
noteStatusService.populateStatuses();
eventBus.publish("triggered-open-modal", {
statusService: noteStatusService,
});
}
private addFolderStatusActions(menu: Menu, folder: TFolder) {
const availableStatuses =
BaseNoteStatusService.getAllAvailableStatuses();
if (!availableStatuses.length) {
menu.addItem((item) => {
item.setTitle("Apply note status (no statuses available)")
.setIcon("alert-triangle")
.setDisabled(true);
});
return;
}
menu.addItem((item) => {
item.setTitle("Apply note status to folder")
.setIcon("folder-sync")
.onClick(() => {
this.openFolderStatusModal(folder, false);
});
});
if (settingsService.settings.applyStatusRecursivelyToSubfolders) {
menu.addItem((item) => {
item.setTitle("Apply note status to folder and subfolders")
.setIcon("git-merge")
.onClick(() => {
this.openFolderStatusModal(folder, true);
});
});
}
}
private openFolderStatusModal(
folder: TFolder,
includeSubfolders: boolean,
): void {
const files = this.getFilesFromFolder(folder, includeSubfolders);
if (!files.length) {
new Notice("This folder does not contain any files.");
return;
}
const warningThreshold = 50;
if (files.length >= warningThreshold) {
new Notice(
`This folder contains ${files.length} files. Applying status changes may take a while.`,
8000,
);
}
const multiStatusService = new MultipleNoteStatusService(files);
multiStatusService.populateStatuses();
eventBus.publish("triggered-open-modal", {
statusService: multiStatusService,
});
}
private getFilesFromFolder(
folder: TFolder,
includeSubfolders: boolean,
): TFile[] {
const files: TFile[] = [];
const queue: TFolder[] = [folder];
while (queue.length) {
const current = queue.shift();
if (!current) continue;
current.children.forEach((child) => {
if (child instanceof TFile) {
files.push(child);
} else if (includeSubfolders && child instanceof TFolder) {
queue.push(child);
}
});
if (!includeSubfolders) {
break;
}
}
return files;
}
destroy() {
this.currentLeaf = null;
this.noteStatusService = null;
ContextMenuIntegration.instance = null;
}
}
export default ContextMenuIntegration;