mirror of
https://github.com/devonthesofa/obsidian-note-status.git
synced 2026-07-22 05:45:04 +00:00
refactor: format and build
This commit is contained in:
parent
73765169fb
commit
028aed1f2b
15 changed files with 2789 additions and 2234 deletions
|
|
@ -1,314 +1,358 @@
|
|||
import { setIcon, TFile, setTooltip } from 'obsidian';
|
||||
import { StatusRemoveHandler, StatusSelectHandler } from './types';
|
||||
import { NoteStatusSettings, Status } from 'models/types';
|
||||
import { StatusService } from 'services/status-service';
|
||||
import { setIcon, TFile, setTooltip } from "obsidian";
|
||||
import { StatusRemoveHandler, StatusSelectHandler } from "./types";
|
||||
import { NoteStatusSettings, Status } from "models/types";
|
||||
import { StatusService } from "services/status-service";
|
||||
|
||||
/**
|
||||
* Render the dropdown content
|
||||
*/
|
||||
export function renderDropdownContent(options: {
|
||||
dropdownElement: HTMLElement,
|
||||
settings: NoteStatusSettings,
|
||||
statusService: StatusService,
|
||||
currentStatuses: string[],
|
||||
targetFile: TFile | null,
|
||||
targetFiles: TFile[],
|
||||
onRemoveStatus: StatusRemoveHandler,
|
||||
onSelectStatus: StatusSelectHandler
|
||||
dropdownElement: HTMLElement;
|
||||
settings: NoteStatusSettings;
|
||||
statusService: StatusService;
|
||||
currentStatuses: string[];
|
||||
targetFile: TFile | null;
|
||||
targetFiles: TFile[];
|
||||
onRemoveStatus: StatusRemoveHandler;
|
||||
onSelectStatus: StatusSelectHandler;
|
||||
}): void {
|
||||
const {
|
||||
dropdownElement,
|
||||
settings,
|
||||
statusService,
|
||||
currentStatuses,
|
||||
targetFile,
|
||||
targetFiles,
|
||||
onRemoveStatus,
|
||||
onSelectStatus
|
||||
} = options;
|
||||
|
||||
dropdownElement.empty();
|
||||
|
||||
// Create UI sections
|
||||
createHeader(dropdownElement, targetFiles);
|
||||
const {
|
||||
dropdownElement,
|
||||
settings,
|
||||
statusService,
|
||||
currentStatuses,
|
||||
targetFile,
|
||||
targetFiles,
|
||||
onRemoveStatus,
|
||||
onSelectStatus,
|
||||
} = options;
|
||||
|
||||
const target = targetFiles.length > 1 ? targetFiles : targetFile;
|
||||
createStatusChips(dropdownElement, currentStatuses, statusService, target ?? [], onRemoveStatus);
|
||||
const searchInput = createSearchFilter(dropdownElement);
|
||||
|
||||
// Create status options container
|
||||
const statusOptionsContainer = dropdownElement.createDiv({
|
||||
cls: 'note-status-options-container'
|
||||
});
|
||||
|
||||
// Get all available statuses (excluding 'unknown')
|
||||
const allStatuses = statusService.getAllStatuses()
|
||||
.filter(status => status.name !== 'unknown');
|
||||
|
||||
// Function to populate options with filtering
|
||||
const populateOptions = (filter = '') => {
|
||||
populateStatusOptions({
|
||||
container: statusOptionsContainer,
|
||||
statuses: allStatuses,
|
||||
currentStatuses,
|
||||
settings,
|
||||
targetFiles,
|
||||
onSelectStatus,
|
||||
filter
|
||||
});
|
||||
};
|
||||
|
||||
// Initial population
|
||||
populateOptions();
|
||||
|
||||
// Add search functionality
|
||||
searchInput.addEventListener('input', () => {
|
||||
populateOptions(searchInput.value);
|
||||
});
|
||||
|
||||
// Focus search input after a short delay
|
||||
setTimeout(() => searchInput.focus(), 50);
|
||||
dropdownElement.empty();
|
||||
|
||||
// Create UI sections
|
||||
createHeader(dropdownElement, targetFiles);
|
||||
|
||||
const target = targetFiles.length > 1 ? targetFiles : targetFile;
|
||||
createStatusChips(
|
||||
dropdownElement,
|
||||
currentStatuses,
|
||||
statusService,
|
||||
target ?? [],
|
||||
onRemoveStatus,
|
||||
);
|
||||
const searchInput = createSearchFilter(dropdownElement);
|
||||
|
||||
// Create status options container
|
||||
const statusOptionsContainer = dropdownElement.createDiv({
|
||||
cls: "note-status-options-container",
|
||||
});
|
||||
|
||||
// Get all available statuses (excluding 'unknown')
|
||||
const allStatuses = statusService
|
||||
.getAllStatuses()
|
||||
.filter((status) => status.name !== "unknown");
|
||||
|
||||
// Function to populate options with filtering
|
||||
const populateOptions = (filter = "") => {
|
||||
populateStatusOptions({
|
||||
container: statusOptionsContainer,
|
||||
statuses: allStatuses,
|
||||
currentStatuses,
|
||||
settings,
|
||||
targetFiles,
|
||||
onSelectStatus,
|
||||
filter,
|
||||
});
|
||||
};
|
||||
|
||||
// Initial population
|
||||
populateOptions();
|
||||
|
||||
// Add search functionality
|
||||
searchInput.addEventListener("input", () => {
|
||||
populateOptions(searchInput.value);
|
||||
});
|
||||
|
||||
// Focus search input after a short delay
|
||||
setTimeout(() => searchInput.focus(), 50);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the dropdown header
|
||||
*/
|
||||
function createHeader(dropdownElement: HTMLElement, targetFiles: TFile[]): void {
|
||||
const headerEl = dropdownElement.createDiv({ cls: 'note-status-popover-header' });
|
||||
const titleEl = headerEl.createDiv({ cls: 'note-status-popover-title' });
|
||||
|
||||
const iconContainer = titleEl.createDiv({ cls: 'note-status-popover-icon' });
|
||||
setIcon(iconContainer, 'tag');
|
||||
|
||||
titleEl.createSpan({ text: 'Note status', cls: 'note-status-popover-label' });
|
||||
|
||||
// If multiple files are selected, show count
|
||||
if (targetFiles.length > 1) {
|
||||
titleEl.createSpan({
|
||||
text: ` (${targetFiles.length} files)`,
|
||||
cls: 'note-status-popover-count'
|
||||
});
|
||||
}
|
||||
function createHeader(
|
||||
dropdownElement: HTMLElement,
|
||||
targetFiles: TFile[],
|
||||
): void {
|
||||
const headerEl = dropdownElement.createDiv({
|
||||
cls: "note-status-popover-header",
|
||||
});
|
||||
const titleEl = headerEl.createDiv({ cls: "note-status-popover-title" });
|
||||
|
||||
const iconContainer = titleEl.createDiv({
|
||||
cls: "note-status-popover-icon",
|
||||
});
|
||||
setIcon(iconContainer, "tag");
|
||||
|
||||
titleEl.createSpan({
|
||||
text: "Note status",
|
||||
cls: "note-status-popover-label",
|
||||
});
|
||||
|
||||
// If multiple files are selected, show count
|
||||
if (targetFiles.length > 1) {
|
||||
titleEl.createSpan({
|
||||
text: ` (${targetFiles.length} files)`,
|
||||
cls: "note-status-popover-count",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the status chips section
|
||||
*/
|
||||
function createStatusChips(
|
||||
dropdownElement: HTMLElement,
|
||||
currentStatuses: string[],
|
||||
statusService: StatusService,
|
||||
targetFile: TFile | TFile[],
|
||||
onRemoveStatus: StatusRemoveHandler
|
||||
dropdownElement: HTMLElement,
|
||||
currentStatuses: string[],
|
||||
statusService: StatusService,
|
||||
targetFile: TFile | TFile[],
|
||||
onRemoveStatus: StatusRemoveHandler,
|
||||
): void {
|
||||
const chipsContainer = dropdownElement.createDiv({ cls: 'note-status-popover-chips' });
|
||||
|
||||
const hasNoValidStatus = currentStatuses.length === 0 ||
|
||||
(currentStatuses.length === 1 && currentStatuses[0] === 'unknown');
|
||||
|
||||
if (hasNoValidStatus) {
|
||||
chipsContainer.createDiv({
|
||||
cls: 'note-status-empty-indicator',
|
||||
text: 'No status assigned'
|
||||
});
|
||||
} else {
|
||||
createStatusChipElements(chipsContainer, currentStatuses, statusService, targetFile, onRemoveStatus);
|
||||
}
|
||||
const chipsContainer = dropdownElement.createDiv({
|
||||
cls: "note-status-popover-chips",
|
||||
});
|
||||
|
||||
const hasNoValidStatus =
|
||||
currentStatuses.length === 0 ||
|
||||
(currentStatuses.length === 1 && currentStatuses[0] === "unknown");
|
||||
|
||||
if (hasNoValidStatus) {
|
||||
chipsContainer.createDiv({
|
||||
cls: "note-status-empty-indicator",
|
||||
text: "No status assigned",
|
||||
});
|
||||
} else {
|
||||
createStatusChipElements(
|
||||
chipsContainer,
|
||||
currentStatuses,
|
||||
statusService,
|
||||
targetFile,
|
||||
onRemoveStatus,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create chips for all current statuses
|
||||
*/
|
||||
function createStatusChipElements(
|
||||
container: HTMLElement,
|
||||
currentStatuses: string[],
|
||||
statusService: StatusService,
|
||||
target: TFile | TFile[],
|
||||
onRemoveStatus: StatusRemoveHandler
|
||||
container: HTMLElement,
|
||||
currentStatuses: string[],
|
||||
statusService: StatusService,
|
||||
target: TFile | TFile[],
|
||||
onRemoveStatus: StatusRemoveHandler,
|
||||
): void {
|
||||
currentStatuses.forEach(status => {
|
||||
if (status === 'unknown') return;
|
||||
|
||||
const statusObj = statusService.getAllStatuses().find(s => s.name === status);
|
||||
if (!statusObj) return;
|
||||
|
||||
createSingleStatusChip(container, status, statusObj, target, onRemoveStatus);
|
||||
});
|
||||
currentStatuses.forEach((status) => {
|
||||
if (status === "unknown") return;
|
||||
|
||||
const statusObj = statusService
|
||||
.getAllStatuses()
|
||||
.find((s) => s.name === status);
|
||||
if (!statusObj) return;
|
||||
|
||||
createSingleStatusChip(
|
||||
container,
|
||||
status,
|
||||
statusObj,
|
||||
target,
|
||||
onRemoveStatus,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a single status chip
|
||||
*/
|
||||
function createSingleStatusChip(
|
||||
container: HTMLElement,
|
||||
status: string,
|
||||
statusObj: Status,
|
||||
target: TFile | TFile[],
|
||||
onRemoveStatus: StatusRemoveHandler
|
||||
container: HTMLElement,
|
||||
status: string,
|
||||
statusObj: Status,
|
||||
target: TFile | TFile[],
|
||||
onRemoveStatus: StatusRemoveHandler,
|
||||
): void {
|
||||
const chipEl = container.createDiv({
|
||||
cls: `note-status-chip status-${status}`
|
||||
});
|
||||
|
||||
// Status icon and name
|
||||
chipEl.createSpan({
|
||||
text: statusObj.icon,
|
||||
cls: 'note-status-chip-icon'
|
||||
});
|
||||
|
||||
chipEl.createSpan({
|
||||
text: statusObj.name,
|
||||
cls: 'note-status-chip-text'
|
||||
});
|
||||
|
||||
addRemoveButton(chipEl, status, statusObj, target, onRemoveStatus);
|
||||
const chipEl = container.createDiv({
|
||||
cls: `note-status-chip status-${status}`,
|
||||
});
|
||||
|
||||
// Status icon and name
|
||||
chipEl.createSpan({
|
||||
text: statusObj.icon,
|
||||
cls: "note-status-chip-icon",
|
||||
});
|
||||
|
||||
chipEl.createSpan({
|
||||
text: statusObj.name,
|
||||
cls: "note-status-chip-text",
|
||||
});
|
||||
|
||||
addRemoveButton(chipEl, status, statusObj, target, onRemoveStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a remove button to a status chip
|
||||
*/
|
||||
function addRemoveButton(
|
||||
chipEl: HTMLElement,
|
||||
status: string,
|
||||
statusObj: Status,
|
||||
targetFile: TFile | TFile[] | null,
|
||||
onRemoveStatus: StatusRemoveHandler
|
||||
chipEl: HTMLElement,
|
||||
status: string,
|
||||
statusObj: Status,
|
||||
targetFile: TFile | TFile[] | null,
|
||||
onRemoveStatus: StatusRemoveHandler,
|
||||
): void {
|
||||
const tooltipValue = statusObj.description ? `${status} - ${statusObj.description}`: status;
|
||||
|
||||
setTooltip(chipEl, tooltipValue);
|
||||
|
||||
const removeBtn = chipEl.createDiv({
|
||||
cls: 'note-status-chip-remove',
|
||||
attr: {
|
||||
'aria-label': `Remove ${status} status`,
|
||||
'title': `Remove ${status} status`
|
||||
}
|
||||
});
|
||||
setIcon(removeBtn, 'x');
|
||||
|
||||
removeBtn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
chipEl.addClass('note-status-chip-removing');
|
||||
|
||||
setTimeout(async () => {
|
||||
if (targetFile) {
|
||||
await onRemoveStatus(status, targetFile);
|
||||
}
|
||||
}, 150);
|
||||
});
|
||||
const tooltipValue = statusObj.description
|
||||
? `${status} - ${statusObj.description}`
|
||||
: status;
|
||||
|
||||
setTooltip(chipEl, tooltipValue);
|
||||
|
||||
const removeBtn = chipEl.createDiv({
|
||||
cls: "note-status-chip-remove",
|
||||
attr: {
|
||||
"aria-label": `Remove ${status} status`,
|
||||
title: `Remove ${status} status`,
|
||||
},
|
||||
});
|
||||
setIcon(removeBtn, "x");
|
||||
|
||||
removeBtn.addEventListener("click", async (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
chipEl.addClass("note-status-chip-removing");
|
||||
|
||||
setTimeout(async () => {
|
||||
if (targetFile) {
|
||||
await onRemoveStatus(status, targetFile);
|
||||
}
|
||||
}, 150);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the search filter input
|
||||
*/
|
||||
function createSearchFilter(dropdownElement: HTMLElement): HTMLInputElement {
|
||||
const searchContainer = dropdownElement.createDiv({ cls: 'note-status-popover-search' });
|
||||
|
||||
return searchContainer.createEl('input', {
|
||||
type: 'text',
|
||||
placeholder: 'Filter statuses...',
|
||||
cls: 'note-status-popover-search-input'
|
||||
});
|
||||
const searchContainer = dropdownElement.createDiv({
|
||||
cls: "note-status-popover-search",
|
||||
});
|
||||
|
||||
return searchContainer.createEl("input", {
|
||||
type: "text",
|
||||
placeholder: "Filter statuses...",
|
||||
cls: "note-status-popover-search-input",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate status options with optional filtering
|
||||
*/
|
||||
function populateStatusOptions(options: {
|
||||
container: HTMLElement,
|
||||
statuses: Status[],
|
||||
currentStatuses: string[],
|
||||
settings: NoteStatusSettings,
|
||||
targetFiles: TFile[],
|
||||
onSelectStatus: StatusSelectHandler,
|
||||
filter?: string
|
||||
container: HTMLElement;
|
||||
statuses: Status[];
|
||||
currentStatuses: string[];
|
||||
settings: NoteStatusSettings;
|
||||
targetFiles: TFile[];
|
||||
onSelectStatus: StatusSelectHandler;
|
||||
filter?: string;
|
||||
}): void {
|
||||
const {
|
||||
container,
|
||||
statuses,
|
||||
currentStatuses,
|
||||
settings,
|
||||
targetFiles,
|
||||
onSelectStatus,
|
||||
filter = ''
|
||||
} = options;
|
||||
|
||||
container.empty();
|
||||
|
||||
const filteredStatuses = filter ?
|
||||
statuses.filter(status =>
|
||||
status.name.toLowerCase().includes(filter.toLowerCase()) ||
|
||||
status.icon.includes(filter)
|
||||
) :
|
||||
statuses;
|
||||
|
||||
if (filteredStatuses.length === 0) {
|
||||
container.createDiv({
|
||||
cls: 'note-status-empty-options',
|
||||
text: filter ? `No statuses match "${filter}"` : 'No statuses found'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
filteredStatuses.forEach(status => {
|
||||
createStatusOption({
|
||||
container,
|
||||
status,
|
||||
isSelected: currentStatuses.includes(status.name),
|
||||
settings,
|
||||
targetFiles,
|
||||
onSelectStatus
|
||||
});
|
||||
});
|
||||
const {
|
||||
container,
|
||||
statuses,
|
||||
currentStatuses,
|
||||
settings,
|
||||
targetFiles,
|
||||
onSelectStatus,
|
||||
filter = "",
|
||||
} = options;
|
||||
|
||||
container.empty();
|
||||
|
||||
const filteredStatuses = filter
|
||||
? statuses.filter(
|
||||
(status) =>
|
||||
status.name.toLowerCase().includes(filter.toLowerCase()) ||
|
||||
status.icon.includes(filter),
|
||||
)
|
||||
: statuses;
|
||||
|
||||
if (filteredStatuses.length === 0) {
|
||||
container.createDiv({
|
||||
cls: "note-status-empty-options",
|
||||
text: filter
|
||||
? `No statuses match "${filter}"`
|
||||
: "No statuses found",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
filteredStatuses.forEach((status) => {
|
||||
createStatusOption({
|
||||
container,
|
||||
status,
|
||||
isSelected: currentStatuses.includes(status.name),
|
||||
settings,
|
||||
targetFiles,
|
||||
onSelectStatus,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a single status option element
|
||||
*/
|
||||
function createStatusOption(options: {
|
||||
container: HTMLElement,
|
||||
status: Status,
|
||||
isSelected: boolean,
|
||||
settings: NoteStatusSettings,
|
||||
targetFiles: TFile[],
|
||||
onSelectStatus: StatusSelectHandler
|
||||
container: HTMLElement;
|
||||
status: Status;
|
||||
isSelected: boolean;
|
||||
settings: NoteStatusSettings;
|
||||
targetFiles: TFile[];
|
||||
onSelectStatus: StatusSelectHandler;
|
||||
}): void {
|
||||
const { container, status, isSelected, settings, targetFiles, onSelectStatus } = options;
|
||||
|
||||
const optionEl = container.createDiv({
|
||||
cls: `note-status-option ${isSelected ? 'is-selected' : ''} status-${status.name}`
|
||||
});
|
||||
|
||||
// Status icon and name
|
||||
optionEl.createSpan({
|
||||
text: status.icon,
|
||||
cls: 'note-status-option-icon'
|
||||
});
|
||||
|
||||
optionEl.createSpan({
|
||||
text: status.name,
|
||||
cls: 'note-status-option-text'
|
||||
});
|
||||
|
||||
// Add tooltip if description available
|
||||
if (status.description) {
|
||||
setTooltip(optionEl, `${status.name} - ${status.description}`);
|
||||
}
|
||||
|
||||
// Check icon for selected status
|
||||
if (isSelected) {
|
||||
const checkIcon = optionEl.createDiv({ cls: 'note-status-option-check' });
|
||||
setIcon(checkIcon, 'check');
|
||||
}
|
||||
|
||||
optionEl.addEventListener('click', () => {
|
||||
optionEl.addClass('note-status-option-selecting');
|
||||
setTimeout(async () => {
|
||||
if (targetFiles.length > 0) {
|
||||
await onSelectStatus(status.name, targetFiles);
|
||||
}
|
||||
}, 150);
|
||||
});
|
||||
const { container, status, isSelected, targetFiles, onSelectStatus } =
|
||||
options;
|
||||
|
||||
const optionEl = container.createDiv({
|
||||
cls: `note-status-option ${isSelected ? "is-selected" : ""} status-${status.name}`,
|
||||
});
|
||||
|
||||
// Status icon and name
|
||||
optionEl.createSpan({
|
||||
text: status.icon,
|
||||
cls: "note-status-option-icon",
|
||||
});
|
||||
|
||||
optionEl.createSpan({
|
||||
text: status.name,
|
||||
cls: "note-status-option-text",
|
||||
});
|
||||
|
||||
// Add tooltip if description available
|
||||
if (status.description) {
|
||||
setTooltip(optionEl, `${status.name} - ${status.description}`);
|
||||
}
|
||||
|
||||
// Check icon for selected status
|
||||
if (isSelected) {
|
||||
const checkIcon = optionEl.createDiv({
|
||||
cls: "note-status-option-check",
|
||||
});
|
||||
setIcon(checkIcon, "check");
|
||||
}
|
||||
|
||||
optionEl.addEventListener("click", () => {
|
||||
optionEl.addClass("note-status-option-selecting");
|
||||
setTimeout(async () => {
|
||||
if (targetFiles.length > 0) {
|
||||
await onSelectStatus(status.name, targetFiles);
|
||||
}
|
||||
}, 150);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,302 +1,323 @@
|
|||
// integrations/commands/command-integration.ts
|
||||
import { App, Editor, MarkdownView, Notice, TFile } from 'obsidian';
|
||||
import { NoteStatusSettings } from '../../models/types';
|
||||
import { StatusService } from 'services/status-service';
|
||||
import { StatusDropdown } from 'components/status-dropdown';
|
||||
import { StatusPaneViewController } from 'views/status-pane-view';
|
||||
import NoteStatus from 'main';
|
||||
import { App, Editor, MarkdownView, Notice, TFile } from "obsidian";
|
||||
import { NoteStatusSettings } from "../../models/types";
|
||||
import { StatusService } from "services/status-service";
|
||||
import { StatusDropdown } from "components/status-dropdown";
|
||||
import { StatusPaneViewController } from "views/status-pane-view";
|
||||
import NoteStatus from "main";
|
||||
|
||||
export class CommandIntegration {
|
||||
private app: App;
|
||||
private plugin: NoteStatus;
|
||||
private settings: NoteStatusSettings;
|
||||
private statusService: StatusService;
|
||||
private statusDropdown: StatusDropdown;
|
||||
private app: App;
|
||||
private plugin: NoteStatus;
|
||||
private settings: NoteStatusSettings;
|
||||
private statusService: StatusService;
|
||||
private statusDropdown: StatusDropdown;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
plugin: NoteStatus,
|
||||
settings: NoteStatusSettings,
|
||||
statusService: StatusService,
|
||||
statusDropdown: StatusDropdown
|
||||
) {
|
||||
this.app = app;
|
||||
this.plugin = plugin;
|
||||
this.settings = settings;
|
||||
this.statusService = statusService;
|
||||
this.statusDropdown = statusDropdown;
|
||||
}
|
||||
constructor(
|
||||
app: App,
|
||||
plugin: NoteStatus,
|
||||
settings: NoteStatusSettings,
|
||||
statusService: StatusService,
|
||||
statusDropdown: StatusDropdown,
|
||||
) {
|
||||
this.app = app;
|
||||
this.plugin = plugin;
|
||||
this.settings = settings;
|
||||
this.statusService = statusService;
|
||||
this.statusDropdown = statusDropdown;
|
||||
}
|
||||
|
||||
public updateSettings(settings: NoteStatusSettings): void {
|
||||
this.settings = settings;
|
||||
// Re-register commands when settings change
|
||||
this.unload();
|
||||
this.registerCommands();
|
||||
}
|
||||
public updateSettings(settings: NoteStatusSettings): void {
|
||||
this.settings = settings;
|
||||
// Re-register commands when settings change
|
||||
this.unload();
|
||||
this.registerCommands();
|
||||
}
|
||||
|
||||
public registerCommands(): void {
|
||||
// Open status pane
|
||||
this.plugin.addCommand({
|
||||
id: 'open-status-pane',
|
||||
name: 'Open status pane',
|
||||
callback: () => StatusPaneViewController.open(this.app)
|
||||
});
|
||||
public registerCommands(): void {
|
||||
// Open status pane
|
||||
this.plugin.addCommand({
|
||||
id: "open-status-pane",
|
||||
name: "Open status pane",
|
||||
callback: () => StatusPaneViewController.open(this.app),
|
||||
});
|
||||
|
||||
// Change status of current note
|
||||
this.plugin.addCommand({
|
||||
id: 'change-status',
|
||||
name: 'Change status of current note',
|
||||
checkCallback: (checking: boolean) => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!file) return false;
|
||||
|
||||
if (!checking) {
|
||||
this.statusDropdown.openStatusDropdown({ files: [file] });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
// Change status of current note
|
||||
this.plugin.addCommand({
|
||||
id: "change-status",
|
||||
name: "Change status of current note",
|
||||
checkCallback: (checking: boolean) => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!file) return false;
|
||||
|
||||
// Add status to current note
|
||||
this.plugin.addCommand({
|
||||
id: 'add-status',
|
||||
name: 'Add status to current note',
|
||||
checkCallback: (checking: boolean) => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!file) return false;
|
||||
|
||||
if (!checking) {
|
||||
this.statusDropdown.openStatusDropdown({
|
||||
files: [file],
|
||||
mode: this.settings.useMultipleStatuses ? 'add' : 'replace'
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (!checking) {
|
||||
this.statusDropdown.openStatusDropdown({ files: [file] });
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
// Insert status metadata
|
||||
this.plugin.addCommand({
|
||||
id: 'insert-status-metadata',
|
||||
name: 'Insert status metadata',
|
||||
editorCheckCallback: (checking: boolean, editor: Editor, view: MarkdownView) => {
|
||||
if (!view.file) return false;
|
||||
|
||||
const statuses = this.statusService.getFileStatuses(view.file);
|
||||
const hasNoStatus = statuses.length === 1 && statuses[0] === 'unknown';
|
||||
|
||||
if (!checking && hasNoStatus) {
|
||||
this.statusService.insertStatusMetadataInEditor(editor);
|
||||
new Notice('Status metadata inserted');
|
||||
}
|
||||
return hasNoStatus;
|
||||
}
|
||||
});
|
||||
// Add status to current note
|
||||
this.plugin.addCommand({
|
||||
id: "add-status",
|
||||
name: "Add status to current note",
|
||||
checkCallback: (checking: boolean) => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!file) return false;
|
||||
|
||||
// Cycle through statuses
|
||||
this.plugin.addCommand({
|
||||
id: 'cycle-status',
|
||||
name: 'Cycle to next status',
|
||||
checkCallback: (checking: boolean) => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!file) return false;
|
||||
|
||||
if (!checking) {
|
||||
this.cycleStatus(file);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (!checking) {
|
||||
this.statusDropdown.openStatusDropdown({
|
||||
files: [file],
|
||||
mode: this.settings.useMultipleStatuses
|
||||
? "add"
|
||||
: "replace",
|
||||
});
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
// Register dynamic quick status commands
|
||||
this.registerQuickStatusCommands();
|
||||
// Insert status metadata
|
||||
this.plugin.addCommand({
|
||||
id: "insert-status-metadata",
|
||||
name: "Insert status metadata",
|
||||
editorCheckCallback: (
|
||||
checking: boolean,
|
||||
editor: Editor,
|
||||
view: MarkdownView,
|
||||
) => {
|
||||
if (!view.file) return false;
|
||||
|
||||
// Clear status
|
||||
this.plugin.addCommand({
|
||||
id: 'clear-status',
|
||||
name: 'Clear status (set to unknown)',
|
||||
checkCallback: (checking: boolean) => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!file) return false;
|
||||
|
||||
if (!checking) {
|
||||
this.statusService.handleStatusChange({
|
||||
files: file,
|
||||
statuses: 'unknown',
|
||||
operation: 'set'
|
||||
});
|
||||
new Notice('Status cleared');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
const statuses = this.statusService.getFileStatuses(view.file);
|
||||
const hasNoStatus =
|
||||
statuses.length === 1 && statuses[0] === "unknown";
|
||||
|
||||
// Copy status from current note
|
||||
this.plugin.addCommand({
|
||||
id: 'copy-status',
|
||||
name: 'Copy status from current note',
|
||||
checkCallback: (checking: boolean) => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!file) return false;
|
||||
|
||||
if (!checking) {
|
||||
const statuses = this.statusService.getFileStatuses(file);
|
||||
(this.app as any).clipboard = statuses;
|
||||
new Notice(`Copied status: ${statuses.join(', ')}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (!checking && hasNoStatus) {
|
||||
this.statusService.insertStatusMetadataInEditor(editor);
|
||||
new Notice("Status metadata inserted");
|
||||
}
|
||||
return hasNoStatus;
|
||||
},
|
||||
});
|
||||
|
||||
// Paste status to current note
|
||||
this.plugin.addCommand({
|
||||
id: 'paste-status',
|
||||
name: 'Paste status to current note',
|
||||
checkCallback: (checking: boolean) => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
const clipboard = (this.app as any).clipboard;
|
||||
if (!file || !clipboard || !Array.isArray(clipboard)) return false;
|
||||
|
||||
if (!checking) {
|
||||
this.statusService.handleStatusChange({
|
||||
files: file,
|
||||
statuses: clipboard,
|
||||
operation: 'set'
|
||||
});
|
||||
new Notice(`Pasted status: ${clipboard.join(', ')}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
// Cycle through statuses
|
||||
this.plugin.addCommand({
|
||||
id: "cycle-status",
|
||||
name: "Cycle to next status",
|
||||
checkCallback: (checking: boolean) => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!file) return false;
|
||||
|
||||
// Toggle multiple statuses mode
|
||||
this.plugin.addCommand({
|
||||
id: 'toggle-multiple-statuses',
|
||||
name: 'Toggle multiple statuses mode',
|
||||
callback: () => {
|
||||
this.settings.useMultipleStatuses = !this.settings.useMultipleStatuses;
|
||||
this.plugin.saveSettings();
|
||||
new Notice(`Multiple statuses mode ${this.settings.useMultipleStatuses ? 'enabled' : 'disabled'}`);
|
||||
}
|
||||
});
|
||||
if (!checking) {
|
||||
this.cycleStatus(file);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
// Search notes by status
|
||||
this.plugin.addCommand({
|
||||
id: 'search-by-status',
|
||||
name: 'Search notes by current status',
|
||||
checkCallback: (checking: boolean) => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!file) return false;
|
||||
|
||||
if (!checking) {
|
||||
const statuses = this.statusService.getFileStatuses(file);
|
||||
const query = `[${this.settings.tagPrefix}:"${statuses[0]}"]`;
|
||||
(this.app as any).internalPlugins.getPluginById('global-search').instance.openGlobalSearch(query);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
// Register dynamic quick status commands
|
||||
this.registerQuickStatusCommands();
|
||||
|
||||
/**
|
||||
* Register quick status commands based on settings
|
||||
*/
|
||||
private registerQuickStatusCommands(): void {
|
||||
const quickCommands = this.settings.quickStatusCommands || [];
|
||||
const allStatuses = this.statusService.getAllStatuses();
|
||||
|
||||
quickCommands.forEach(statusName => {
|
||||
const status = allStatuses.find(s => s.name === statusName);
|
||||
if (!status) return;
|
||||
|
||||
this.plugin.addCommand({
|
||||
id: `set-status-${statusName}`,
|
||||
name: `Set status to ${statusName}`,
|
||||
checkCallback: (checking: boolean) => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!file) return false;
|
||||
|
||||
if (!checking) {
|
||||
this.statusService.handleStatusChange({
|
||||
files: file,
|
||||
statuses: statusName,
|
||||
operation: 'set'
|
||||
});
|
||||
new Notice(`Status set to ${statusName}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Add toggle command for multiple status mode
|
||||
if (this.settings.useMultipleStatuses) {
|
||||
this.plugin.addCommand({
|
||||
id: `toggle-status-${statusName}`,
|
||||
name: `Toggle status ${statusName}`,
|
||||
checkCallback: (checking: boolean) => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!file) return false;
|
||||
|
||||
if (!checking) {
|
||||
this.statusService.handleStatusChange({
|
||||
files: file,
|
||||
statuses: statusName,
|
||||
operation: 'toggle'
|
||||
});
|
||||
const currentStatuses = this.statusService.getFileStatuses(file);
|
||||
const hasStatus = currentStatuses.includes(statusName);
|
||||
new Notice(`Status ${statusName} ${hasStatus ? 'added' : 'removed'}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// Clear status
|
||||
this.plugin.addCommand({
|
||||
id: "clear-status",
|
||||
name: "Clear status (set to unknown)",
|
||||
checkCallback: (checking: boolean) => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!file) return false;
|
||||
|
||||
private cycleStatus(file: TFile): void {
|
||||
const allStatuses = this.statusService.getAllStatuses()
|
||||
.filter(s => s.name !== 'unknown')
|
||||
.map(s => s.name);
|
||||
|
||||
if (allStatuses.length === 0) {
|
||||
new Notice('No statuses available');
|
||||
return;
|
||||
}
|
||||
if (!checking) {
|
||||
this.statusService.handleStatusChange({
|
||||
files: file,
|
||||
statuses: "unknown",
|
||||
operation: "set",
|
||||
});
|
||||
new Notice("Status cleared");
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
const currentStatuses = this.statusService.getFileStatuses(file);
|
||||
const currentStatus = currentStatuses[0];
|
||||
|
||||
let nextIndex = 0;
|
||||
if (currentStatus !== 'unknown') {
|
||||
const currentIndex = allStatuses.indexOf(currentStatus);
|
||||
nextIndex = (currentIndex + 1) % allStatuses.length;
|
||||
}
|
||||
|
||||
this.statusService.handleStatusChange({
|
||||
files: file,
|
||||
statuses: allStatuses[nextIndex],
|
||||
operation: 'set'
|
||||
});
|
||||
|
||||
new Notice(`Status changed to ${allStatuses[nextIndex]}`);
|
||||
}
|
||||
// Copy status from current note
|
||||
this.plugin.addCommand({
|
||||
id: "copy-status",
|
||||
name: "Copy status from current note",
|
||||
checkCallback: (checking: boolean) => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!file) return false;
|
||||
|
||||
public unload(): void {
|
||||
// Remove existing commands
|
||||
this.removeQuickStatusCommands();
|
||||
}
|
||||
if (!checking) {
|
||||
const statuses = this.statusService.getFileStatuses(file);
|
||||
this.app.clipboard = statuses;
|
||||
new Notice(`Copied status: ${statuses.join(", ")}`);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Remove all quick status commands
|
||||
*/
|
||||
private removeQuickStatusCommands(): void {
|
||||
const allStatuses = this.statusService.getAllStatuses();
|
||||
|
||||
allStatuses.forEach(status => {
|
||||
(this.app as any).commands.removeCommand(`note-status:set-status-${status.name}`);
|
||||
(this.app as any).commands.removeCommand(`note-status:toggle-status-${status.name}`);
|
||||
});
|
||||
}
|
||||
// Paste status to current note
|
||||
this.plugin.addCommand({
|
||||
id: "paste-status",
|
||||
name: "Paste status to current note",
|
||||
checkCallback: (checking: boolean) => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
const clipboard = this.app.clipboard;
|
||||
if (!file || !clipboard || !Array.isArray(clipboard))
|
||||
return false;
|
||||
|
||||
if (!checking) {
|
||||
this.statusService.handleStatusChange({
|
||||
files: file,
|
||||
statuses: clipboard,
|
||||
operation: "set",
|
||||
});
|
||||
new Notice(`Pasted status: ${clipboard.join(", ")}`);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
// Toggle multiple statuses mode
|
||||
this.plugin.addCommand({
|
||||
id: "toggle-multiple-statuses",
|
||||
name: "Toggle multiple statuses mode",
|
||||
callback: () => {
|
||||
this.settings.useMultipleStatuses =
|
||||
!this.settings.useMultipleStatuses;
|
||||
this.plugin.saveSettings();
|
||||
new Notice(
|
||||
`Multiple statuses mode ${this.settings.useMultipleStatuses ? "enabled" : "disabled"}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// Search notes by status
|
||||
this.plugin.addCommand({
|
||||
id: "search-by-status",
|
||||
name: "Search notes by current status",
|
||||
checkCallback: (checking: boolean) => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!file) return false;
|
||||
|
||||
if (!checking) {
|
||||
const statuses = this.statusService.getFileStatuses(file);
|
||||
const query = `[${this.settings.tagPrefix}:"${statuses[0]}"]`;
|
||||
this.app.internalPlugins
|
||||
.getPluginById("global-search")
|
||||
?.instance.openGlobalSearch(query);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register quick status commands based on settings
|
||||
*/
|
||||
private registerQuickStatusCommands(): void {
|
||||
const quickCommands = this.settings.quickStatusCommands || [];
|
||||
const allStatuses = this.statusService.getAllStatuses();
|
||||
|
||||
quickCommands.forEach((statusName) => {
|
||||
const status = allStatuses.find((s) => s.name === statusName);
|
||||
if (!status) return;
|
||||
|
||||
this.plugin.addCommand({
|
||||
id: `set-status-${statusName}`,
|
||||
name: `Set status to ${statusName}`,
|
||||
checkCallback: (checking: boolean) => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!file) return false;
|
||||
|
||||
if (!checking) {
|
||||
this.statusService.handleStatusChange({
|
||||
files: file,
|
||||
statuses: statusName,
|
||||
operation: "set",
|
||||
});
|
||||
new Notice(`Status set to ${statusName}`);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
// Add toggle command for multiple status mode
|
||||
if (this.settings.useMultipleStatuses) {
|
||||
this.plugin.addCommand({
|
||||
id: `toggle-status-${statusName}`,
|
||||
name: `Toggle status ${statusName}`,
|
||||
checkCallback: (checking: boolean) => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!file) return false;
|
||||
|
||||
if (!checking) {
|
||||
this.statusService.handleStatusChange({
|
||||
files: file,
|
||||
statuses: statusName,
|
||||
operation: "toggle",
|
||||
});
|
||||
const currentStatuses =
|
||||
this.statusService.getFileStatuses(file);
|
||||
const hasStatus =
|
||||
currentStatuses.includes(statusName);
|
||||
new Notice(
|
||||
`Status ${statusName} ${hasStatus ? "added" : "removed"}`,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private cycleStatus(file: TFile): void {
|
||||
const allStatuses = this.statusService
|
||||
.getAllStatuses()
|
||||
.filter((s) => s.name !== "unknown")
|
||||
.map((s) => s.name);
|
||||
|
||||
if (allStatuses.length === 0) {
|
||||
new Notice("No statuses available");
|
||||
return;
|
||||
}
|
||||
|
||||
const currentStatuses = this.statusService.getFileStatuses(file);
|
||||
const currentStatus = currentStatuses[0];
|
||||
|
||||
let nextIndex = 0;
|
||||
if (currentStatus !== "unknown") {
|
||||
const currentIndex = allStatuses.indexOf(currentStatus);
|
||||
nextIndex = (currentIndex + 1) % allStatuses.length;
|
||||
}
|
||||
|
||||
this.statusService.handleStatusChange({
|
||||
files: file,
|
||||
statuses: allStatuses[nextIndex],
|
||||
operation: "set",
|
||||
});
|
||||
|
||||
new Notice(`Status changed to ${allStatuses[nextIndex]}`);
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// Remove existing commands
|
||||
this.removeQuickStatusCommands();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all quick status commands
|
||||
*/
|
||||
private removeQuickStatusCommands(): void {
|
||||
const allStatuses = this.statusService.getAllStatuses();
|
||||
|
||||
allStatuses.forEach((status) => {
|
||||
this.app.commands.removeCommand(
|
||||
`note-status:set-status-${status.name}`,
|
||||
);
|
||||
this.app.commands.removeCommand(
|
||||
`note-status:toggle-status-${status.name}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,95 +1,108 @@
|
|||
import { App, Menu, TFile } from 'obsidian';
|
||||
import { NoteStatusSettings } from 'models/types';
|
||||
import { StatusService } from 'services/status-service';
|
||||
import { ExplorerIntegration } from 'integrations/explorer/explorer-integration';
|
||||
import { StatusContextMenu } from 'integrations/context-menu/status-context-menu';
|
||||
import { App, Menu, TFile } from "obsidian";
|
||||
import { NoteStatusSettings } from "models/types";
|
||||
import { StatusService } from "services/status-service";
|
||||
import { ExplorerIntegration } from "integrations/explorer/explorer-integration";
|
||||
import { StatusContextMenu } from "integrations/context-menu/status-context-menu";
|
||||
|
||||
/**
|
||||
* Gestiona la integración de menús contextuales con el explorador de archivos
|
||||
*/
|
||||
export class FileContextMenuIntegration {
|
||||
private app: App;
|
||||
private settings: NoteStatusSettings;
|
||||
private statusService: StatusService;
|
||||
private explorerIntegration: ExplorerIntegration;
|
||||
private statusContextMenu: StatusContextMenu;
|
||||
private fileMenuEventRef: any;
|
||||
private filesMenuEventRef: any;
|
||||
private app: App;
|
||||
private settings: NoteStatusSettings;
|
||||
private statusService: StatusService;
|
||||
private explorerIntegration: ExplorerIntegration;
|
||||
private statusContextMenu: StatusContextMenu;
|
||||
private fileMenuEventRef: (menu: Menu, file: TFile, source: string) => void;
|
||||
private filesMenuEventRef: (menu: Menu, files: TFile[]) => void;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
settings: NoteStatusSettings,
|
||||
statusService: StatusService,
|
||||
explorerIntegration: ExplorerIntegration,
|
||||
statusContextMenu: StatusContextMenu
|
||||
) {
|
||||
this.app = app;
|
||||
this.settings = settings;
|
||||
this.statusService = statusService;
|
||||
this.explorerIntegration = explorerIntegration;
|
||||
this.statusContextMenu = statusContextMenu;
|
||||
}
|
||||
constructor(
|
||||
app: App,
|
||||
settings: NoteStatusSettings,
|
||||
statusService: StatusService,
|
||||
explorerIntegration: ExplorerIntegration,
|
||||
statusContextMenu: StatusContextMenu,
|
||||
) {
|
||||
this.app = app;
|
||||
this.settings = settings;
|
||||
this.statusService = statusService;
|
||||
this.explorerIntegration = explorerIntegration;
|
||||
this.statusContextMenu = statusContextMenu;
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualiza la configuración
|
||||
*/
|
||||
public updateSettings(settings: NoteStatusSettings): void {
|
||||
this.settings = settings;
|
||||
}
|
||||
/**
|
||||
* Actualiza la configuración
|
||||
*/
|
||||
public updateSettings(settings: NoteStatusSettings): void {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra los eventos del menú contextual de archivos
|
||||
*/
|
||||
public registerFileContextMenuEvents(): void {
|
||||
this.fileMenuEventRef = (menu: Menu, file: any, source: string) => {
|
||||
if (source === 'file-explorer-context-menu' && file instanceof TFile && file.extension === 'md') {
|
||||
this.addStatusChangeMenu(menu, file);
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Registra los eventos del menú contextual de archivos
|
||||
*/
|
||||
public registerFileContextMenuEvents(): void {
|
||||
this.fileMenuEventRef = (menu, file, source) => {
|
||||
if (
|
||||
source === "file-explorer-context-menu" &&
|
||||
file instanceof TFile &&
|
||||
file.extension === "md"
|
||||
) {
|
||||
this.addStatusChangeMenu(menu, file);
|
||||
}
|
||||
};
|
||||
|
||||
this.filesMenuEventRef = (menu: Menu, files: any[]) => {
|
||||
const mdFiles = files.filter(file =>
|
||||
file instanceof TFile && file.extension === 'md'
|
||||
) as TFile[];
|
||||
|
||||
if (mdFiles.length > 0) {
|
||||
this.addBatchStatusChangeMenu(menu, mdFiles);
|
||||
}
|
||||
};
|
||||
this.filesMenuEventRef = (menu, files) => {
|
||||
const mdFiles = files.filter(
|
||||
(file) => file instanceof TFile && file.extension === "md",
|
||||
) as TFile[];
|
||||
|
||||
this.app.workspace.on('file-menu', this.fileMenuEventRef);
|
||||
this.app.workspace.on('files-menu', this.filesMenuEventRef);
|
||||
}
|
||||
if (mdFiles.length > 0) {
|
||||
this.addBatchStatusChangeMenu(menu, mdFiles);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Añade opción de cambio de estado al menú contextual de un archivo
|
||||
*/
|
||||
private addStatusChangeMenu(menu: Menu, file: TFile): void {
|
||||
this.statusContextMenu.addStatusMenuItemToSingleFile(menu, file, (file) => {
|
||||
const selectedFiles = this.explorerIntegration.getSelectedFiles();
|
||||
if (selectedFiles.length > 1) {
|
||||
this.statusContextMenu.showForFiles(selectedFiles);
|
||||
} else {
|
||||
this.statusContextMenu.showForFiles([file]);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.app.workspace.on("file-menu", this.fileMenuEventRef);
|
||||
this.app.workspace.on("files-menu", this.filesMenuEventRef);
|
||||
}
|
||||
|
||||
/**
|
||||
* Añade opción de cambio de estado para múltiples archivos
|
||||
*/
|
||||
private addBatchStatusChangeMenu(menu: Menu, files: TFile[]): void {
|
||||
this.statusContextMenu.addStatusMenuItemToBatch(menu, files, (files) => {
|
||||
this.statusContextMenu.showForFiles(files);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Añade opción de cambio de estado al menú contextual de un archivo
|
||||
*/
|
||||
private addStatusChangeMenu(menu: Menu, file: TFile): void {
|
||||
this.statusContextMenu.addStatusMenuItemToSingleFile(
|
||||
menu,
|
||||
file,
|
||||
(file) => {
|
||||
const selectedFiles =
|
||||
this.explorerIntegration.getSelectedFiles();
|
||||
if (selectedFiles.length > 1) {
|
||||
this.statusContextMenu.showForFiles(selectedFiles);
|
||||
} else {
|
||||
this.statusContextMenu.showForFiles([file]);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
if (this.fileMenuEventRef) {
|
||||
this.app.workspace.off('file-menu', this.fileMenuEventRef);
|
||||
}
|
||||
if (this.filesMenuEventRef) {
|
||||
this.app.workspace.off('files-menu', this.filesMenuEventRef);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Añade opción de cambio de estado para múltiples archivos
|
||||
*/
|
||||
private addBatchStatusChangeMenu(menu: Menu, files: TFile[]): void {
|
||||
this.statusContextMenu.addStatusMenuItemToBatch(
|
||||
menu,
|
||||
files,
|
||||
(files) => {
|
||||
this.statusContextMenu.showForFiles(files);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
if (this.fileMenuEventRef) {
|
||||
this.app.workspace.off("file-menu", this.fileMenuEventRef);
|
||||
}
|
||||
if (this.filesMenuEventRef) {
|
||||
this.app.workspace.off("files-menu", this.filesMenuEventRef);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,80 +1,92 @@
|
|||
import { App, Editor, MarkdownView, Menu, TFile } from 'obsidian';
|
||||
import { NoteStatusSettings } from '../../models/types';
|
||||
import { StatusService } from 'services/status-service';
|
||||
import { StatusDropdown } from 'components/status-dropdown';
|
||||
import { App, Editor, MarkdownView, Menu } from "obsidian";
|
||||
import { NoteStatusSettings } from "../../models/types";
|
||||
import { StatusService } from "services/status-service";
|
||||
import { StatusDropdown } from "components/status-dropdown";
|
||||
|
||||
export class EditorIntegration {
|
||||
private app: App;
|
||||
private settings: NoteStatusSettings;
|
||||
private statusService: StatusService;
|
||||
private statusDropdown: StatusDropdown;
|
||||
private editorMenuRef: any;
|
||||
private app: App;
|
||||
private settings: NoteStatusSettings;
|
||||
private statusService: StatusService;
|
||||
private statusDropdown: StatusDropdown;
|
||||
private editorMenuRef: (
|
||||
menu: Menu,
|
||||
editor: Editor,
|
||||
view: MarkdownView,
|
||||
) => void;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
settings: NoteStatusSettings,
|
||||
statusService: StatusService,
|
||||
statusDropdown: StatusDropdown
|
||||
) {
|
||||
this.app = app;
|
||||
this.settings = settings;
|
||||
this.statusService = statusService;
|
||||
this.statusDropdown = statusDropdown;
|
||||
}
|
||||
constructor(
|
||||
app: App,
|
||||
settings: NoteStatusSettings,
|
||||
statusService: StatusService,
|
||||
statusDropdown: StatusDropdown,
|
||||
) {
|
||||
this.app = app;
|
||||
this.settings = settings;
|
||||
this.statusService = statusService;
|
||||
this.statusDropdown = statusDropdown;
|
||||
}
|
||||
|
||||
public updateSettings(settings: NoteStatusSettings): void {
|
||||
this.settings = settings;
|
||||
}
|
||||
public updateSettings(settings: NoteStatusSettings): void {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
public registerEditorMenus(): void {
|
||||
this.editorMenuRef = (menu: Menu, editor: Editor, view: MarkdownView) => {
|
||||
if (view.file) {
|
||||
this.addStatusMenuItems(menu, editor, view);
|
||||
}
|
||||
};
|
||||
public registerEditorMenus(): void {
|
||||
this.editorMenuRef = (
|
||||
menu: Menu,
|
||||
editor: Editor,
|
||||
view: MarkdownView,
|
||||
) => {
|
||||
if (view.file) {
|
||||
this.addStatusMenuItems(menu, editor, view);
|
||||
}
|
||||
};
|
||||
|
||||
this.app.workspace.on('editor-menu', this.editorMenuRef);
|
||||
}
|
||||
this.app.workspace.on("editor-menu", this.editorMenuRef);
|
||||
}
|
||||
|
||||
private addStatusMenuItems(menu: Menu, editor: Editor, view: MarkdownView): void {
|
||||
menu.addItem(item =>
|
||||
item
|
||||
.setTitle('Change note status')
|
||||
.setIcon('tag')
|
||||
.onClick(() => {
|
||||
if (view.file) {
|
||||
this.statusDropdown.openStatusDropdown({
|
||||
files: [view.file],
|
||||
editor,
|
||||
view
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Only show insert metadata if it doesn't exist
|
||||
if (view.file) {
|
||||
const statuses = this.statusService.getFileStatuses(view.file);
|
||||
if (statuses.length === 1 && statuses[0] === 'unknown') {
|
||||
menu.addItem(item =>
|
||||
item
|
||||
.setTitle('Insert status metadata')
|
||||
.setIcon('plus-circle')
|
||||
.onClick(() => {
|
||||
this.insertStatusMetadata(editor);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public insertStatusMetadata(editor: Editor): void {
|
||||
this.statusService.insertStatusMetadataInEditor(editor);
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
if (this.editorMenuRef) {
|
||||
this.app.workspace.off('editor-menu', this.editorMenuRef);
|
||||
}
|
||||
}
|
||||
private addStatusMenuItems(
|
||||
menu: Menu,
|
||||
editor: Editor,
|
||||
view: MarkdownView,
|
||||
): void {
|
||||
menu.addItem((item) =>
|
||||
item
|
||||
.setTitle("Change note status")
|
||||
.setIcon("tag")
|
||||
.onClick(() => {
|
||||
if (view.file) {
|
||||
this.statusDropdown.openStatusDropdown({
|
||||
files: [view.file],
|
||||
editor,
|
||||
view,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// Only show insert metadata if it doesn't exist
|
||||
if (view.file) {
|
||||
const statuses = this.statusService.getFileStatuses(view.file);
|
||||
if (statuses.length === 1 && statuses[0] === "unknown") {
|
||||
menu.addItem((item) =>
|
||||
item
|
||||
.setTitle("Insert status metadata")
|
||||
.setIcon("plus-circle")
|
||||
.onClick(() => {
|
||||
this.insertStatusMetadata(editor);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public insertStatusMetadata(editor: Editor): void {
|
||||
this.statusService.insertStatusMetadataInEditor(editor);
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
if (this.editorMenuRef) {
|
||||
this.app.workspace.off("editor-menu", this.editorMenuRef);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,118 +1,135 @@
|
|||
import { App, MarkdownView } from 'obsidian';
|
||||
import { NoteStatusSettings } from '../../models/types';
|
||||
import { StatusService } from '../../services/status-service';
|
||||
import { StatusDropdown } from '../../components/status-dropdown';
|
||||
import { ToolbarButton } from 'components/toolbar-button';
|
||||
import { App, MarkdownView } from "obsidian";
|
||||
import { NoteStatusSettings } from "../../models/types";
|
||||
import { StatusService } from "../../services/status-service";
|
||||
import { StatusDropdown } from "../../components/status-dropdown";
|
||||
import { ToolbarButton } from "components/toolbar-button";
|
||||
|
||||
/**
|
||||
* Gestiona la integración con la barra de herramientas del editor
|
||||
*/
|
||||
export class ToolbarIntegration {
|
||||
private app: App;
|
||||
private settings: NoteStatusSettings;
|
||||
private statusService: StatusService;
|
||||
private statusDropdown: StatusDropdown;
|
||||
private buttonView: ToolbarButton;
|
||||
private buttonElement: HTMLElement | null = null;
|
||||
private currentLeafId: string | null = null;
|
||||
private app: App;
|
||||
private settings: NoteStatusSettings;
|
||||
private statusService: StatusService;
|
||||
private statusDropdown: StatusDropdown;
|
||||
private buttonView: ToolbarButton;
|
||||
private buttonElement: HTMLElement | null = null;
|
||||
private currentLeafId: string | null = null;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
settings: NoteStatusSettings,
|
||||
statusService: StatusService,
|
||||
statusDropdown: StatusDropdown
|
||||
) {
|
||||
this.app = app;
|
||||
this.settings = settings;
|
||||
this.statusService = statusService;
|
||||
this.statusDropdown = statusDropdown;
|
||||
this.buttonView = new ToolbarButton(settings, statusService);
|
||||
}
|
||||
|
||||
public updateSettings(settings: NoteStatusSettings): void {
|
||||
this.settings = settings;
|
||||
this.buttonView.updateSettings(settings);
|
||||
this.updateStatusDisplay([]);
|
||||
}
|
||||
constructor(
|
||||
app: App,
|
||||
settings: NoteStatusSettings,
|
||||
statusService: StatusService,
|
||||
statusDropdown: StatusDropdown,
|
||||
) {
|
||||
this.app = app;
|
||||
this.settings = settings;
|
||||
this.statusService = statusService;
|
||||
this.statusDropdown = statusDropdown;
|
||||
this.buttonView = new ToolbarButton(settings, statusService);
|
||||
}
|
||||
|
||||
public addToolbarButtonToActiveLeaf(statuses?: string[]): void {
|
||||
const activeLeaf = this.app.workspace.activeLeaf;
|
||||
if (!activeLeaf?.view || !(activeLeaf.view instanceof MarkdownView)) return;
|
||||
public updateSettings(settings: NoteStatusSettings): void {
|
||||
this.settings = settings;
|
||||
this.buttonView.updateSettings(settings);
|
||||
this.updateStatusDisplay([]);
|
||||
}
|
||||
|
||||
const leafId = (activeLeaf as any).id || activeLeaf.view.containerEl.id;
|
||||
|
||||
// Only recreate button if we're on a different leaf or button doesn't exist
|
||||
if (this.currentLeafId !== leafId || !this.buttonElement || !this.isButtonInDOM()) {
|
||||
this.recreateButton(activeLeaf.view, leafId);
|
||||
}
|
||||
|
||||
this.updateButtonDisplay(statuses);
|
||||
}
|
||||
public addToolbarButtonToActiveLeaf(statuses?: string[]): void {
|
||||
const activeLeaf = this.app.workspace.activeLeaf;
|
||||
if (!activeLeaf?.view || !(activeLeaf.view instanceof MarkdownView))
|
||||
return;
|
||||
|
||||
private recreateButton(view: MarkdownView, leafId: string): void {
|
||||
const toolbarContainer = view.containerEl.querySelector('.view-header .view-actions');
|
||||
if (!toolbarContainer) return;
|
||||
const leafId = activeLeaf.id || activeLeaf.view.containerEl.id;
|
||||
|
||||
// Remove old button if it exists
|
||||
this.removeToolbarButton();
|
||||
|
||||
// Create new button
|
||||
this.buttonElement = this.buttonView.createElement();
|
||||
this.buttonElement.addEventListener('click', this.handleButtonClick.bind(this));
|
||||
|
||||
if (toolbarContainer.firstChild) {
|
||||
toolbarContainer.insertBefore(this.buttonElement, toolbarContainer.firstChild);
|
||||
} else {
|
||||
toolbarContainer.appendChild(this.buttonElement);
|
||||
}
|
||||
|
||||
this.currentLeafId = leafId;
|
||||
}
|
||||
// Only recreate button if we're on a different leaf or button doesn't exist
|
||||
if (
|
||||
this.currentLeafId !== leafId ||
|
||||
!this.buttonElement ||
|
||||
!this.isButtonInDOM()
|
||||
) {
|
||||
this.recreateButton(activeLeaf.view, leafId);
|
||||
}
|
||||
|
||||
private isButtonInDOM(): boolean {
|
||||
return this.buttonElement?.isConnected === true;
|
||||
}
|
||||
this.updateButtonDisplay(statuses);
|
||||
}
|
||||
|
||||
private removeToolbarButton(): void {
|
||||
this.app.workspace.iterateAllLeaves(leaf => {
|
||||
if (leaf.view instanceof MarkdownView) {
|
||||
const buttons = leaf.view.containerEl.querySelectorAll('.note-status-toolbar-button');
|
||||
buttons.forEach(button => button.remove());
|
||||
}
|
||||
});
|
||||
|
||||
this.buttonElement = null;
|
||||
this.currentLeafId = null;
|
||||
}
|
||||
|
||||
private updateButtonDisplay(overrideStatuses?: string[]): void {
|
||||
if (!this.buttonElement) return;
|
||||
|
||||
const activeFile = this.app.workspace.getActiveFile();
|
||||
if (!activeFile) return;
|
||||
|
||||
const statuses = overrideStatuses?.length ? overrideStatuses : this.statusService.getFileStatuses(activeFile);
|
||||
this.buttonView.updateDisplay(statuses);
|
||||
}
|
||||
|
||||
private handleButtonClick(e: MouseEvent): void {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const activeFile = this.app.workspace.getActiveFile();
|
||||
if (!activeFile) return;
|
||||
|
||||
this.statusDropdown.openStatusDropdown({
|
||||
target: this.buttonElement || undefined,
|
||||
files: [activeFile]
|
||||
});
|
||||
}
|
||||
|
||||
public updateStatusDisplay(statuses: string[]): void {
|
||||
this.updateButtonDisplay(statuses);
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// this.buttonView.destroy();
|
||||
this.removeToolbarButton();
|
||||
}
|
||||
private recreateButton(view: MarkdownView, leafId: string): void {
|
||||
const toolbarContainer = view.containerEl.querySelector(
|
||||
".view-header .view-actions",
|
||||
);
|
||||
if (!toolbarContainer) return;
|
||||
|
||||
// Remove old button if it exists
|
||||
this.removeToolbarButton();
|
||||
|
||||
// Create new button
|
||||
this.buttonElement = this.buttonView.createElement();
|
||||
this.buttonElement.addEventListener(
|
||||
"click",
|
||||
this.handleButtonClick.bind(this),
|
||||
);
|
||||
|
||||
if (toolbarContainer.firstChild) {
|
||||
toolbarContainer.insertBefore(
|
||||
this.buttonElement,
|
||||
toolbarContainer.firstChild,
|
||||
);
|
||||
} else {
|
||||
toolbarContainer.appendChild(this.buttonElement);
|
||||
}
|
||||
|
||||
this.currentLeafId = leafId;
|
||||
}
|
||||
|
||||
private isButtonInDOM(): boolean {
|
||||
return this.buttonElement?.isConnected === true;
|
||||
}
|
||||
|
||||
private removeToolbarButton(): void {
|
||||
this.app.workspace.iterateAllLeaves((leaf) => {
|
||||
if (leaf.view instanceof MarkdownView) {
|
||||
const buttons = leaf.view.containerEl.querySelectorAll(
|
||||
".note-status-toolbar-button",
|
||||
);
|
||||
buttons.forEach((button) => button.remove());
|
||||
}
|
||||
});
|
||||
|
||||
this.buttonElement = null;
|
||||
this.currentLeafId = null;
|
||||
}
|
||||
|
||||
private updateButtonDisplay(overrideStatuses?: string[]): void {
|
||||
if (!this.buttonElement) return;
|
||||
|
||||
const activeFile = this.app.workspace.getActiveFile();
|
||||
if (!activeFile) return;
|
||||
|
||||
const statuses = overrideStatuses?.length
|
||||
? overrideStatuses
|
||||
: this.statusService.getFileStatuses(activeFile);
|
||||
this.buttonView.updateDisplay(statuses);
|
||||
}
|
||||
|
||||
private handleButtonClick(e: MouseEvent): void {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const activeFile = this.app.workspace.getActiveFile();
|
||||
if (!activeFile) return;
|
||||
|
||||
this.statusDropdown.openStatusDropdown({
|
||||
target: this.buttonElement || undefined,
|
||||
files: [activeFile],
|
||||
});
|
||||
}
|
||||
|
||||
public updateStatusDisplay(statuses: string[]): void {
|
||||
this.updateButtonDisplay(statuses);
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// this.buttonView.destroy();
|
||||
this.removeToolbarButton();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,350 +1,432 @@
|
|||
import { App, TFile, setTooltip, debounce } from 'obsidian';
|
||||
import { NoteStatusSettings, FileExplorerView } from '../../models/types';
|
||||
import { StatusService } from 'services/status-service';
|
||||
|
||||
import { App, TFile, setTooltip, debounce } from "obsidian";
|
||||
import { NoteStatusSettings, FileExplorerView } from "../../models/types";
|
||||
import { StatusService } from "services/status-service";
|
||||
|
||||
/**
|
||||
* Manages the logic for file explorer status integration
|
||||
*/
|
||||
export class ExplorerIntegration {
|
||||
private app: App;
|
||||
private settings: NoteStatusSettings;
|
||||
private statusService: StatusService;
|
||||
private ui: ExplorerIntegrationUI;
|
||||
private iconUpdateQueue = new Set<string>();
|
||||
private isProcessingQueue = false;
|
||||
private debouncedUpdateAll: ReturnType<typeof debounce>;
|
||||
private fileItemsWasUndefined = false;
|
||||
private app: App;
|
||||
private settings: NoteStatusSettings;
|
||||
private statusService: StatusService;
|
||||
private ui: ExplorerIntegrationUI;
|
||||
private iconUpdateQueue = new Set<string>();
|
||||
private isProcessingQueue = false;
|
||||
private debouncedUpdateAll: ReturnType<typeof debounce>;
|
||||
private fileItemsWasUndefined = false;
|
||||
|
||||
constructor(app: App, settings: NoteStatusSettings, statusService: StatusService) {
|
||||
this.app = app;
|
||||
this.settings = settings;
|
||||
this.statusService = statusService;
|
||||
this.ui = new ExplorerIntegrationUI(app, settings, statusService, () => {
|
||||
this.fileItemsWasUndefined = true;
|
||||
});
|
||||
this.debouncedUpdateAll = debounce(this.processUpdateQueue.bind(this), 100, true);
|
||||
|
||||
// Listen for layout changes to detect when file explorer becomes available
|
||||
this.app.workspace.on('layout-change', () => {
|
||||
this.checkFileExplorerAvailability();
|
||||
});
|
||||
}
|
||||
constructor(
|
||||
app: App,
|
||||
settings: NoteStatusSettings,
|
||||
statusService: StatusService,
|
||||
) {
|
||||
this.app = app;
|
||||
this.settings = settings;
|
||||
this.statusService = statusService;
|
||||
this.ui = new ExplorerIntegrationUI(
|
||||
app,
|
||||
settings,
|
||||
statusService,
|
||||
() => {
|
||||
this.fileItemsWasUndefined = true;
|
||||
},
|
||||
);
|
||||
this.debouncedUpdateAll = debounce(
|
||||
this.processUpdateQueue.bind(this),
|
||||
100,
|
||||
true,
|
||||
);
|
||||
|
||||
/**
|
||||
* Check if file explorer became available after being unavailable
|
||||
*/
|
||||
private checkFileExplorerAvailability(): void {
|
||||
if (!this.settings.showStatusIconsInExplorer) return;
|
||||
|
||||
const fileExplorer = this.ui.findFileExplorerView();
|
||||
if (fileExplorer?.fileItems && this.fileItemsWasUndefined) {
|
||||
this.fileItemsWasUndefined = false;
|
||||
setTimeout(() => this.updateAllFileExplorerIcons(), 100);
|
||||
}
|
||||
}
|
||||
// Listen for layout changes to detect when file explorer becomes available
|
||||
this.app.workspace.on("layout-change", () => {
|
||||
this.checkFileExplorerAvailability();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates settings and refreshes UI if necessary
|
||||
*/
|
||||
public updateSettings(settings: NoteStatusSettings): void {
|
||||
const shouldRefreshIcons =
|
||||
this.settings.showStatusIconsInExplorer !== settings.showStatusIconsInExplorer ||
|
||||
this.settings.hideUnknownStatusInExplorer !== settings.hideUnknownStatusInExplorer;
|
||||
|
||||
this.settings = {...settings};
|
||||
this.ui.updateSettings(settings);
|
||||
/**
|
||||
* Check if file explorer became available after being unavailable
|
||||
*/
|
||||
private checkFileExplorerAvailability(): void {
|
||||
if (!this.settings.showStatusIconsInExplorer) return;
|
||||
|
||||
if (shouldRefreshIcons) {
|
||||
this.ui.removeAllFileExplorerIcons();
|
||||
|
||||
if (settings.showStatusIconsInExplorer) {
|
||||
setTimeout(() => this.updateAllFileExplorerIcons(), 50);
|
||||
}
|
||||
} else if (settings.showStatusIconsInExplorer) {
|
||||
this.updateAllFileExplorerIcons();
|
||||
}
|
||||
}
|
||||
const fileExplorer = this.ui.findFileExplorerView();
|
||||
if (fileExplorer?.fileItems && this.fileItemsWasUndefined) {
|
||||
this.fileItemsWasUndefined = false;
|
||||
setTimeout(() => this.updateAllFileExplorerIcons(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates icons for a specific file
|
||||
*/
|
||||
public updateFileExplorerIcons(file: TFile): void {
|
||||
if (!file || !this.settings.showStatusIconsInExplorer || file.extension !== 'md') return;
|
||||
|
||||
const activeFile = this.app.workspace.getActiveFile();
|
||||
if (activeFile?.path === file.path) {
|
||||
this.ui.updateSingleFileIconDirectly(file, this.statusService);
|
||||
}
|
||||
|
||||
this.queueFileUpdate(file);
|
||||
}
|
||||
/**
|
||||
* Updates settings and refreshes UI if necessary
|
||||
*/
|
||||
public updateSettings(settings: NoteStatusSettings): void {
|
||||
const shouldRefreshIcons =
|
||||
this.settings.showStatusIconsInExplorer !==
|
||||
settings.showStatusIconsInExplorer ||
|
||||
this.settings.hideUnknownStatusInExplorer !==
|
||||
settings.hideUnknownStatusInExplorer;
|
||||
|
||||
/**
|
||||
* Updates all icons in the explorer
|
||||
*/
|
||||
public updateAllFileExplorerIcons(): void {
|
||||
if (!this.settings.showStatusIconsInExplorer) {
|
||||
this.ui.removeAllFileExplorerIcons();
|
||||
return;
|
||||
}
|
||||
|
||||
this.processFilesInBatches();
|
||||
}
|
||||
this.settings = { ...settings };
|
||||
this.ui.updateSettings(settings);
|
||||
|
||||
/**
|
||||
* Gets selected files from the explorer
|
||||
*/
|
||||
public getSelectedFiles(): TFile[] {
|
||||
return this.ui.getSelectedFiles();
|
||||
}
|
||||
if (shouldRefreshIcons) {
|
||||
this.ui.removeAllFileExplorerIcons();
|
||||
|
||||
/**
|
||||
* Queue a file for icon update
|
||||
*/
|
||||
private queueFileUpdate(file: TFile): void {
|
||||
if (!this.settings.showStatusIconsInExplorer || file.extension !== 'md') return;
|
||||
|
||||
this.iconUpdateQueue.add(file.path);
|
||||
this.debouncedUpdateAll();
|
||||
}
|
||||
if (settings.showStatusIconsInExplorer) {
|
||||
setTimeout(() => this.updateAllFileExplorerIcons(), 50);
|
||||
}
|
||||
} else if (settings.showStatusIconsInExplorer) {
|
||||
this.updateAllFileExplorerIcons();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the update queue
|
||||
*/
|
||||
private async processUpdateQueue(): Promise<void> {
|
||||
if (this.isProcessingQueue || this.iconUpdateQueue.size === 0) return;
|
||||
|
||||
this.isProcessingQueue = true;
|
||||
|
||||
try {
|
||||
const fileExplorerView = this.ui.findFileExplorerView();
|
||||
if (!fileExplorerView || !fileExplorerView.fileItems) {
|
||||
this.fileItemsWasUndefined = true;
|
||||
setTimeout(() => this.debouncedUpdateAll(), 200);
|
||||
return;
|
||||
}
|
||||
|
||||
const allPaths = Array.from(this.iconUpdateQueue);
|
||||
const batchSize = 50;
|
||||
|
||||
for (let i = 0; i < allPaths.length; i += batchSize) {
|
||||
await this.processBatch(allPaths.slice(i, i + batchSize), fileExplorerView);
|
||||
|
||||
if (i + batchSize < allPaths.length) {
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Note Status: Error processing file update queue', error);
|
||||
} finally {
|
||||
this.isProcessingQueue = false;
|
||||
|
||||
if (this.iconUpdateQueue.size > 0) {
|
||||
this.debouncedUpdateAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a batch of files
|
||||
*/
|
||||
private async processBatch(paths: string[], fileExplorerView: any): Promise<void> {
|
||||
for (const path of paths) {
|
||||
const file = this.app.vault.getFileByPath(path);
|
||||
if (file instanceof TFile) {
|
||||
this.ui.updateSingleFileIcon(file, fileExplorerView, this.statusService);
|
||||
}
|
||||
this.iconUpdateQueue.delete(path);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Updates icons for a specific file
|
||||
*/
|
||||
public updateFileExplorerIcons(file: TFile): void {
|
||||
if (
|
||||
!file ||
|
||||
!this.settings.showStatusIconsInExplorer ||
|
||||
file.extension !== "md"
|
||||
)
|
||||
return;
|
||||
|
||||
/**
|
||||
* Process files in batches
|
||||
*/
|
||||
private async processFilesInBatches(): Promise<void> {
|
||||
const files = this.app.vault.getMarkdownFiles();
|
||||
const batchSize = 100;
|
||||
|
||||
for (let i = 0; i < files.length; i += batchSize) {
|
||||
files.slice(i, i + batchSize).forEach(file => this.queueFileUpdate(file));
|
||||
|
||||
if (i + batchSize < files.length) {
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
const activeFile = this.app.workspace.getActiveFile();
|
||||
if (activeFile?.path === file.path) {
|
||||
this.ui.updateSingleFileIconDirectly(file, this.statusService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup when unloading the plugin
|
||||
*/
|
||||
public unload(): void {
|
||||
this.ui.removeAllFileExplorerIcons();
|
||||
this.debouncedUpdateAll.cancel();
|
||||
}
|
||||
this.queueFileUpdate(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all icons in the explorer
|
||||
*/
|
||||
public updateAllFileExplorerIcons(): void {
|
||||
if (!this.settings.showStatusIconsInExplorer) {
|
||||
this.ui.removeAllFileExplorerIcons();
|
||||
return;
|
||||
}
|
||||
|
||||
this.processFilesInBatches();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets selected files from the explorer
|
||||
*/
|
||||
public getSelectedFiles(): TFile[] {
|
||||
return this.ui.getSelectedFiles();
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue a file for icon update
|
||||
*/
|
||||
private queueFileUpdate(file: TFile): void {
|
||||
if (!this.settings.showStatusIconsInExplorer || file.extension !== "md")
|
||||
return;
|
||||
|
||||
this.iconUpdateQueue.add(file.path);
|
||||
this.debouncedUpdateAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the update queue
|
||||
*/
|
||||
private async processUpdateQueue(): Promise<void> {
|
||||
if (this.isProcessingQueue || this.iconUpdateQueue.size === 0) return;
|
||||
|
||||
this.isProcessingQueue = true;
|
||||
|
||||
try {
|
||||
const fileExplorerView = this.ui.findFileExplorerView();
|
||||
if (!fileExplorerView || !fileExplorerView.fileItems) {
|
||||
this.fileItemsWasUndefined = true;
|
||||
setTimeout(() => this.debouncedUpdateAll(), 200);
|
||||
return;
|
||||
}
|
||||
|
||||
const allPaths = Array.from(this.iconUpdateQueue);
|
||||
const batchSize = 50;
|
||||
|
||||
for (let i = 0; i < allPaths.length; i += batchSize) {
|
||||
await this.processBatch(
|
||||
allPaths.slice(i, i + batchSize),
|
||||
fileExplorerView,
|
||||
);
|
||||
|
||||
if (i + batchSize < allPaths.length) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Note Status: Error processing file update queue",
|
||||
error,
|
||||
);
|
||||
} finally {
|
||||
this.isProcessingQueue = false;
|
||||
|
||||
if (this.iconUpdateQueue.size > 0) {
|
||||
this.debouncedUpdateAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a batch of files
|
||||
*/
|
||||
private async processBatch(
|
||||
paths: string[],
|
||||
fileExplorerView: FileExplorerView,
|
||||
): Promise<void> {
|
||||
for (const path of paths) {
|
||||
const file = this.app.vault.getFileByPath(path);
|
||||
if (file instanceof TFile) {
|
||||
this.ui.updateSingleFileIcon(
|
||||
file,
|
||||
fileExplorerView,
|
||||
this.statusService,
|
||||
);
|
||||
}
|
||||
this.iconUpdateQueue.delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process files in batches
|
||||
*/
|
||||
private async processFilesInBatches(): Promise<void> {
|
||||
const files = this.app.vault.getMarkdownFiles();
|
||||
const batchSize = 100;
|
||||
|
||||
for (let i = 0; i < files.length; i += batchSize) {
|
||||
files
|
||||
.slice(i, i + batchSize)
|
||||
.forEach((file) => this.queueFileUpdate(file));
|
||||
|
||||
if (i + batchSize < files.length) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup when unloading the plugin
|
||||
*/
|
||||
public unload(): void {
|
||||
this.ui.removeAllFileExplorerIcons();
|
||||
this.debouncedUpdateAll.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages UI operations for file explorer icons
|
||||
*/
|
||||
export class ExplorerIntegrationUI {
|
||||
private app: App;
|
||||
private settings: NoteStatusSettings;
|
||||
private onFileItemsUndefined: () => void;
|
||||
private app: App;
|
||||
private settings: NoteStatusSettings;
|
||||
private onFileItemsUndefined: () => void;
|
||||
|
||||
constructor(app: App, settings: NoteStatusSettings, statusService: StatusService, onFileItemsUndefined: () => void) {
|
||||
this.app = app;
|
||||
this.settings = settings;
|
||||
this.onFileItemsUndefined = onFileItemsUndefined;
|
||||
}
|
||||
constructor(
|
||||
app: App,
|
||||
settings: NoteStatusSettings,
|
||||
statusService: StatusService,
|
||||
onFileItemsUndefined: () => void,
|
||||
) {
|
||||
this.app = app;
|
||||
this.settings = settings;
|
||||
this.onFileItemsUndefined = onFileItemsUndefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the settings
|
||||
*/
|
||||
public updateSettings(settings: NoteStatusSettings): void {
|
||||
this.settings = settings;
|
||||
}
|
||||
/**
|
||||
* Updates the settings
|
||||
*/
|
||||
public updateSettings(settings: NoteStatusSettings): void {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the file explorer view
|
||||
*/
|
||||
public findFileExplorerView(): FileExplorerView | null {
|
||||
const leaf = this.app.workspace.getLeavesOfType('file-explorer')[0];
|
||||
if (leaf?.view) return leaf.view as FileExplorerView;
|
||||
|
||||
for (const leaf of this.app.workspace.getLeavesOfType('')) {
|
||||
if (leaf.view && 'fileItems' in leaf.view) {
|
||||
return leaf.view as FileExplorerView;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Finds the file explorer view
|
||||
*/
|
||||
public findFileExplorerView(): FileExplorerView | null {
|
||||
const leaf = this.app.workspace.getLeavesOfType("file-explorer")[0];
|
||||
if (leaf?.view) return leaf.view as FileExplorerView;
|
||||
|
||||
/**
|
||||
* Updates a single file icon
|
||||
*/
|
||||
public updateSingleFileIcon(file: TFile, fileExplorerView: FileExplorerView, statusService: StatusService): void {
|
||||
if (!this.settings.showStatusIconsInExplorer || file.extension !== 'md') return;
|
||||
|
||||
try {
|
||||
// Check if fileItems is initialized
|
||||
if (!fileExplorerView.fileItems) {
|
||||
this.onFileItemsUndefined();
|
||||
return;
|
||||
}
|
||||
|
||||
const fileItem = fileExplorerView.fileItems[file.path];
|
||||
if (!fileItem) return;
|
||||
|
||||
const titleEl = fileItem.titleEl || fileItem.selfEl;
|
||||
if (!titleEl) return;
|
||||
|
||||
const statuses = statusService.getFileStatuses(file);
|
||||
|
||||
this.removeExistingIcons(titleEl);
|
||||
|
||||
if (this.shouldSkipIcon(statuses)) return;
|
||||
|
||||
this.addStatusIcons(titleEl, statuses, statusService);
|
||||
} catch (error) {
|
||||
console.error(`Note Status: Error updating icon for ${file.path}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the icon for the active file directly
|
||||
*/
|
||||
public updateSingleFileIconDirectly(file: TFile, statusService: StatusService): void {
|
||||
const fileExplorer = this.findFileExplorerView();
|
||||
if (fileExplorer) {
|
||||
this.updateSingleFileIcon(file, fileExplorer, statusService);
|
||||
}
|
||||
}
|
||||
for (const leaf of this.app.workspace.getLeavesOfType("")) {
|
||||
if (leaf.view && "fileItems" in leaf.view) {
|
||||
return leaf.view as FileExplorerView;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all file explorer icons
|
||||
*/
|
||||
public removeAllFileExplorerIcons(): void {
|
||||
const fileExplorer = this.findFileExplorerView();
|
||||
if (!fileExplorer?.fileItems) return;
|
||||
|
||||
Object.values(fileExplorer.fileItems).forEach(fileItem => {
|
||||
const titleEl = fileItem.titleEl || fileItem.selfEl;
|
||||
if (titleEl) this.removeExistingIcons(titleEl);
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently selected files
|
||||
*/
|
||||
public getSelectedFiles(): TFile[] {
|
||||
const fileExplorer = this.findFileExplorerView();
|
||||
if (!fileExplorer?.fileItems) return [];
|
||||
|
||||
return Object.values(fileExplorer.fileItems)
|
||||
.filter(item =>
|
||||
item.el?.classList.contains('is-selected') &&
|
||||
item.file instanceof TFile &&
|
||||
item.file.extension === 'md')
|
||||
.map(item => item.file as TFile);
|
||||
}
|
||||
/**
|
||||
* Updates a single file icon
|
||||
*/
|
||||
public updateSingleFileIcon(
|
||||
file: TFile,
|
||||
fileExplorerView: FileExplorerView,
|
||||
statusService: StatusService,
|
||||
): void {
|
||||
if (!this.settings.showStatusIconsInExplorer || file.extension !== "md")
|
||||
return;
|
||||
|
||||
/**
|
||||
* Checks if the icon should be skipped based on status
|
||||
*/
|
||||
private shouldSkipIcon(statuses: string[]): boolean {
|
||||
return this.settings.hideUnknownStatusInExplorer &&
|
||||
statuses.length === 1 &&
|
||||
statuses[0] === 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes existing status icons
|
||||
*/
|
||||
private removeExistingIcons(element: HTMLElement): void {
|
||||
const iconSelectors = '.note-status-icon, .note-status-icon-container';
|
||||
element.querySelectorAll(iconSelectors).forEach(icon => {
|
||||
if (icon.classList.contains('note-status-icon') ||
|
||||
icon.classList.contains('note-status-icon-container')) {
|
||||
icon.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds status icons to an element
|
||||
*/
|
||||
private addStatusIcons(titleEl: HTMLElement, statuses: string[], statusService: StatusService): void {
|
||||
const iconContainer = document.createElement('span');
|
||||
iconContainer.className = 'note-status-icon-container';
|
||||
try {
|
||||
// Check if fileItems is initialized
|
||||
if (!fileExplorerView.fileItems) {
|
||||
this.onFileItemsUndefined();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.settings.useMultipleStatuses && statuses.length > 0 && statuses[0] !== 'unknown') {
|
||||
statuses.forEach(status => this.addSingleStatusIcon(iconContainer, status, statusService));
|
||||
} else {
|
||||
const primaryStatus = statuses[0] || 'unknown';
|
||||
if (primaryStatus !== 'unknown' || !this.settings.hideUnknownStatusInExplorer) {
|
||||
this.addSingleStatusIcon(iconContainer, primaryStatus, statusService);
|
||||
}
|
||||
}
|
||||
|
||||
if (iconContainer.childElementCount > 0) {
|
||||
titleEl.appendChild(iconContainer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a single status icon
|
||||
*/
|
||||
private addSingleStatusIcon(container: HTMLElement, status: string, statusService: StatusService): void {
|
||||
const iconEl = document.createElement('span');
|
||||
iconEl.className = `note-status-icon nav-file-tag status-${status}`;
|
||||
iconEl.textContent = statusService.getStatusIcon(status);
|
||||
|
||||
const statusObj = statusService.getAllStatuses().find(s => s.name === status);
|
||||
const tooltipValue = statusObj?.description ? `${status} - ${statusObj.description}`: status;
|
||||
setTooltip(iconEl, tooltipValue);
|
||||
|
||||
container.appendChild(iconEl);
|
||||
}
|
||||
const fileItem = fileExplorerView.fileItems[file.path];
|
||||
if (!fileItem) return;
|
||||
|
||||
const titleEl = fileItem.titleEl || fileItem.selfEl;
|
||||
if (!titleEl) return;
|
||||
|
||||
const statuses = statusService.getFileStatuses(file);
|
||||
|
||||
this.removeExistingIcons(titleEl);
|
||||
|
||||
if (this.shouldSkipIcon(statuses)) return;
|
||||
|
||||
this.addStatusIcons(titleEl, statuses, statusService);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Note Status: Error updating icon for ${file.path}`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the icon for the active file directly
|
||||
*/
|
||||
public updateSingleFileIconDirectly(
|
||||
file: TFile,
|
||||
statusService: StatusService,
|
||||
): void {
|
||||
const fileExplorer = this.findFileExplorerView();
|
||||
if (fileExplorer) {
|
||||
this.updateSingleFileIcon(file, fileExplorer, statusService);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all file explorer icons
|
||||
*/
|
||||
public removeAllFileExplorerIcons(): void {
|
||||
const fileExplorer = this.findFileExplorerView();
|
||||
if (!fileExplorer?.fileItems) return;
|
||||
|
||||
Object.values(fileExplorer.fileItems).forEach((fileItem) => {
|
||||
const titleEl = fileItem.titleEl || fileItem.selfEl;
|
||||
if (titleEl) this.removeExistingIcons(titleEl);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently selected files
|
||||
*/
|
||||
public getSelectedFiles(): TFile[] {
|
||||
const fileExplorer = this.findFileExplorerView();
|
||||
if (!fileExplorer?.fileItems) return [];
|
||||
|
||||
return Object.values(fileExplorer.fileItems)
|
||||
.filter(
|
||||
(item) =>
|
||||
item.el?.classList.contains("is-selected") &&
|
||||
item.file instanceof TFile &&
|
||||
item.file.extension === "md",
|
||||
)
|
||||
.map((item) => item.file as TFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the icon should be skipped based on status
|
||||
*/
|
||||
private shouldSkipIcon(statuses: string[]): boolean {
|
||||
return (
|
||||
this.settings.hideUnknownStatusInExplorer &&
|
||||
statuses.length === 1 &&
|
||||
statuses[0] === "unknown"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes existing status icons
|
||||
*/
|
||||
private removeExistingIcons(element: HTMLElement): void {
|
||||
const iconSelectors = ".note-status-icon, .note-status-icon-container";
|
||||
element.querySelectorAll(iconSelectors).forEach((icon) => {
|
||||
if (
|
||||
icon.classList.contains("note-status-icon") ||
|
||||
icon.classList.contains("note-status-icon-container")
|
||||
) {
|
||||
icon.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds status icons to an element
|
||||
*/
|
||||
private addStatusIcons(
|
||||
titleEl: HTMLElement,
|
||||
statuses: string[],
|
||||
statusService: StatusService,
|
||||
): void {
|
||||
const iconContainer = document.createElement("span");
|
||||
iconContainer.className = "note-status-icon-container";
|
||||
|
||||
if (
|
||||
this.settings.useMultipleStatuses &&
|
||||
statuses.length > 0 &&
|
||||
statuses[0] !== "unknown"
|
||||
) {
|
||||
statuses.forEach((status) =>
|
||||
this.addSingleStatusIcon(iconContainer, status, statusService),
|
||||
);
|
||||
} else {
|
||||
const primaryStatus = statuses[0] || "unknown";
|
||||
if (
|
||||
primaryStatus !== "unknown" ||
|
||||
!this.settings.hideUnknownStatusInExplorer
|
||||
) {
|
||||
this.addSingleStatusIcon(
|
||||
iconContainer,
|
||||
primaryStatus,
|
||||
statusService,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (iconContainer.childElementCount > 0) {
|
||||
titleEl.appendChild(iconContainer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a single status icon
|
||||
*/
|
||||
private addSingleStatusIcon(
|
||||
container: HTMLElement,
|
||||
status: string,
|
||||
statusService: StatusService,
|
||||
): void {
|
||||
const iconEl = document.createElement("span");
|
||||
iconEl.className = `note-status-icon nav-file-tag status-${status}`;
|
||||
iconEl.textContent = statusService.getStatusIcon(status);
|
||||
|
||||
const statusObj = statusService
|
||||
.getAllStatuses()
|
||||
.find((s) => s.name === status);
|
||||
const tooltipValue = statusObj?.description
|
||||
? `${status} - ${statusObj.description}`
|
||||
: status;
|
||||
setTooltip(iconEl, tooltipValue);
|
||||
|
||||
container.appendChild(iconEl);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,71 +1,73 @@
|
|||
import { App, TFile } from 'obsidian';
|
||||
import { NoteStatusSettings } from '../../models/types';
|
||||
import { ExplorerIntegration } from '../explorer/explorer-integration';
|
||||
import { StatusService } from 'services/status-service';
|
||||
import { App, TFile } from "obsidian";
|
||||
import { NoteStatusSettings } from "../../models/types";
|
||||
import { ExplorerIntegration } from "../explorer/explorer-integration";
|
||||
import { StatusService } from "services/status-service";
|
||||
|
||||
export class MetadataIntegration {
|
||||
private app: App;
|
||||
private settings: NoteStatusSettings;
|
||||
private statusService: StatusService;
|
||||
private explorerIntegration: ExplorerIntegration;
|
||||
private metadataChangedRef: any;
|
||||
private metadataResolvedRef: any;
|
||||
private app: App;
|
||||
private settings: NoteStatusSettings;
|
||||
private statusService: StatusService;
|
||||
private explorerIntegration: ExplorerIntegration;
|
||||
private metadataChangedRef: (file: TFile) => void;
|
||||
private metadataResolvedRef: () => void;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
settings: NoteStatusSettings,
|
||||
statusService: StatusService,
|
||||
explorerIntegration: ExplorerIntegration
|
||||
) {
|
||||
this.app = app;
|
||||
this.settings = settings;
|
||||
this.statusService = statusService;
|
||||
this.explorerIntegration = explorerIntegration;
|
||||
}
|
||||
constructor(
|
||||
app: App,
|
||||
settings: NoteStatusSettings,
|
||||
statusService: StatusService,
|
||||
explorerIntegration: ExplorerIntegration,
|
||||
) {
|
||||
this.app = app;
|
||||
this.settings = settings;
|
||||
this.statusService = statusService;
|
||||
this.explorerIntegration = explorerIntegration;
|
||||
}
|
||||
|
||||
public updateSettings(settings: NoteStatusSettings): void {
|
||||
this.settings = settings;
|
||||
}
|
||||
public updateSettings(settings: NoteStatusSettings): void {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
public registerMetadataEvents(): void {
|
||||
this.metadataChangedRef = (file: TFile) => {
|
||||
if (file instanceof TFile && file.extension === 'md') {
|
||||
this.handleMetadataChanged(file);
|
||||
}
|
||||
};
|
||||
public registerMetadataEvents(): void {
|
||||
this.metadataChangedRef = (file) => {
|
||||
if (file instanceof TFile && file.extension === "md") {
|
||||
this.handleMetadataChanged(file);
|
||||
}
|
||||
};
|
||||
|
||||
this.metadataResolvedRef = () => {
|
||||
setTimeout(() => {
|
||||
if (this.settings.showStatusIconsInExplorer) {
|
||||
this.explorerIntegration.updateAllFileExplorerIcons();
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
this.metadataResolvedRef = () => {
|
||||
setTimeout(() => {
|
||||
if (this.settings.showStatusIconsInExplorer) {
|
||||
this.explorerIntegration.updateAllFileExplorerIcons();
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
|
||||
this.app.metadataCache.on('changed', this.metadataChangedRef);
|
||||
this.app.metadataCache.on('resolved', this.metadataResolvedRef);
|
||||
}
|
||||
this.app.metadataCache.on("changed", this.metadataChangedRef);
|
||||
this.app.metadataCache.on("resolved", this.metadataResolvedRef);
|
||||
}
|
||||
|
||||
private handleMetadataChanged(file: TFile): void {
|
||||
if (this.settings.showStatusIconsInExplorer) {
|
||||
this.explorerIntegration.updateFileExplorerIcons(file);
|
||||
}
|
||||
|
||||
const activeFile = this.app.workspace.getActiveFile();
|
||||
if (activeFile?.path === file.path) {
|
||||
const statuses = this.statusService.getFileStatuses(file);
|
||||
window.dispatchEvent(new CustomEvent('note-status:status-changed', {
|
||||
detail: { statuses, file: file.path }
|
||||
}));
|
||||
}
|
||||
}
|
||||
private handleMetadataChanged(file: TFile): void {
|
||||
if (this.settings.showStatusIconsInExplorer) {
|
||||
this.explorerIntegration.updateFileExplorerIcons(file);
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
if (this.metadataChangedRef) {
|
||||
this.app.metadataCache.off('changed', this.metadataChangedRef);
|
||||
}
|
||||
if (this.metadataResolvedRef) {
|
||||
this.app.metadataCache.off('resolved', this.metadataResolvedRef);
|
||||
}
|
||||
}
|
||||
const activeFile = this.app.workspace.getActiveFile();
|
||||
if (activeFile?.path === file.path) {
|
||||
const statuses = this.statusService.getFileStatuses(file);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("note-status:status-changed", {
|
||||
detail: { statuses, file: file.path },
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
if (this.metadataChangedRef) {
|
||||
this.app.metadataCache.off("changed", this.metadataChangedRef);
|
||||
}
|
||||
if (this.metadataResolvedRef) {
|
||||
this.app.metadataCache.off("resolved", this.metadataResolvedRef);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,119 +1,138 @@
|
|||
import { App } from 'obsidian';
|
||||
import { Status } from '../../models/types';
|
||||
import NoteStatus from 'main';
|
||||
import { StatusService } from 'services/status-service';
|
||||
import { NoteStatusSettingsUI, SettingsUICallbacks } from './settings-ui';
|
||||
import { App } from "obsidian";
|
||||
import { Status } from "../../models/types";
|
||||
import NoteStatus from "main";
|
||||
import { StatusService } from "services/status-service";
|
||||
import { NoteStatusSettingsUI } from "./settings-ui";
|
||||
import { SettingsUICallbacks } from "./types";
|
||||
|
||||
/**
|
||||
* Controller for settings UI, handles business logic and plugin state updates
|
||||
*/
|
||||
export class NoteStatusSettingsController implements SettingsUICallbacks {
|
||||
private app: App;
|
||||
private plugin: NoteStatus;
|
||||
private statusService: StatusService;
|
||||
private ui: NoteStatusSettingsUI;
|
||||
private app: App;
|
||||
private plugin: NoteStatus;
|
||||
private statusService: StatusService;
|
||||
private ui: NoteStatusSettingsUI;
|
||||
|
||||
constructor(app: App, plugin: NoteStatus, statusService: StatusService) {
|
||||
this.app = app;
|
||||
this.plugin = plugin;
|
||||
this.statusService = statusService;
|
||||
this.ui = new NoteStatusSettingsUI(this);
|
||||
}
|
||||
constructor(app: App, plugin: NoteStatus, statusService: StatusService) {
|
||||
this.app = app;
|
||||
this.plugin = plugin;
|
||||
this.statusService = statusService;
|
||||
this.ui = new NoteStatusSettingsUI(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the settings interface
|
||||
*/
|
||||
display(containerEl: HTMLElement): void {
|
||||
this.ui.render(containerEl, this.plugin.settings);
|
||||
}
|
||||
/**
|
||||
* Renders the settings interface
|
||||
*/
|
||||
display(containerEl: HTMLElement): void {
|
||||
this.ui.render(containerEl, this.plugin.settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles template enable/disable toggle
|
||||
*/
|
||||
async onTemplateToggle(templateId: string, enabled: boolean): Promise<void> {
|
||||
if (enabled) {
|
||||
if (!this.plugin.settings.enabledTemplates.includes(templateId)) {
|
||||
this.plugin.settings.enabledTemplates.push(templateId);
|
||||
}
|
||||
} else {
|
||||
this.plugin.settings.enabledTemplates = this.plugin.settings.enabledTemplates.filter(
|
||||
(id: string) => id !== templateId
|
||||
);
|
||||
}
|
||||
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
/**
|
||||
* Handles template enable/disable toggle
|
||||
*/
|
||||
onTemplateToggle: SettingsUICallbacks["onTemplateToggle"] = async (
|
||||
templateId,
|
||||
enabled,
|
||||
) => {
|
||||
if (enabled) {
|
||||
if (!this.plugin.settings.enabledTemplates.includes(templateId)) {
|
||||
this.plugin.settings.enabledTemplates.push(templateId);
|
||||
}
|
||||
} else {
|
||||
this.plugin.settings.enabledTemplates =
|
||||
this.plugin.settings.enabledTemplates.filter(
|
||||
(id: string) => id !== templateId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles general setting changes
|
||||
*/
|
||||
async onSettingChange(key: string, value: any): Promise<void> {
|
||||
(this.plugin.settings as any)[key] = value;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles custom status field changes
|
||||
*/
|
||||
async onCustomStatusChange(index: number, field: string, value: any): Promise<void> {
|
||||
const status = this.plugin.settings.customStatuses[index];
|
||||
if (!status) return;
|
||||
/**
|
||||
* Handles general setting changes
|
||||
*/
|
||||
onSettingChange: SettingsUICallbacks["onSettingChange"] = async (
|
||||
key,
|
||||
value,
|
||||
) => {
|
||||
this.plugin.settings[key] = value;
|
||||
await this.plugin.saveSettings();
|
||||
};
|
||||
|
||||
if (field === 'name') {
|
||||
const oldName = status.name;
|
||||
status.name = value;
|
||||
|
||||
if (oldName !== status.name) {
|
||||
this.plugin.settings.statusColors[status.name] =
|
||||
this.plugin.settings.statusColors[oldName];
|
||||
delete this.plugin.settings.statusColors[oldName];
|
||||
}
|
||||
} else if (field === 'color') {
|
||||
this.plugin.settings.statusColors[status.name] = value;
|
||||
} else {
|
||||
(status as any)[field] = value;
|
||||
}
|
||||
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
/**
|
||||
* Handles custom status field changes
|
||||
*/
|
||||
onCustomStatusChange: SettingsUICallbacks["onCustomStatusChange"] = async (
|
||||
index,
|
||||
field,
|
||||
value,
|
||||
) => {
|
||||
const status = this.plugin.settings.customStatuses[index];
|
||||
if (!status) return;
|
||||
|
||||
/**
|
||||
* Handles custom status removal
|
||||
*/
|
||||
async onCustomStatusRemove(index: number): Promise<void> {
|
||||
const status = this.plugin.settings.customStatuses[index];
|
||||
if (!status) return;
|
||||
if (field === "name") {
|
||||
const oldName = status.name;
|
||||
status.name = value;
|
||||
|
||||
this.plugin.settings.customStatuses.splice(index, 1);
|
||||
delete this.plugin.settings.statusColors[status.name];
|
||||
|
||||
await this.plugin.saveSettings();
|
||||
|
||||
// Re-render the custom statuses section
|
||||
const statusList = document.querySelector('.custom-status-list') as HTMLElement;
|
||||
if (statusList) {
|
||||
this.ui.renderCustomStatuses(statusList, this.plugin.settings);
|
||||
}
|
||||
}
|
||||
if (oldName !== status.name) {
|
||||
this.plugin.settings.statusColors[status.name] =
|
||||
this.plugin.settings.statusColors[oldName];
|
||||
delete this.plugin.settings.statusColors[oldName];
|
||||
}
|
||||
} else if (field === "color") {
|
||||
this.plugin.settings.statusColors[status.name] = value;
|
||||
} else {
|
||||
status[field] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles adding new custom status
|
||||
*/
|
||||
async onCustomStatusAdd(): Promise<void> {
|
||||
const newStatus: Status = {
|
||||
name: `status${this.plugin.settings.customStatuses.length + 1}`,
|
||||
icon: '⭐'
|
||||
};
|
||||
await this.plugin.saveSettings();
|
||||
};
|
||||
|
||||
this.plugin.settings.customStatuses.push(newStatus);
|
||||
this.plugin.settings.statusColors[newStatus.name] = '#ffffff';
|
||||
/**
|
||||
* Handles custom status removal
|
||||
*/
|
||||
onCustomStatusRemove: SettingsUICallbacks["onCustomStatusRemove"] = async (
|
||||
index,
|
||||
) => {
|
||||
const status = this.plugin.settings.customStatuses[index];
|
||||
if (!status) return;
|
||||
|
||||
await this.plugin.saveSettings();
|
||||
|
||||
// Re-render the custom statuses section
|
||||
const statusList = document.querySelector('.custom-status-list') as HTMLElement;
|
||||
if (statusList) {
|
||||
this.ui.renderCustomStatuses(statusList, this.plugin.settings);
|
||||
}
|
||||
}
|
||||
this.plugin.settings.customStatuses.splice(index, 1);
|
||||
delete this.plugin.settings.statusColors[status.name];
|
||||
|
||||
await this.plugin.saveSettings();
|
||||
|
||||
// Re-render the custom statuses section
|
||||
const statusList = document.querySelector(
|
||||
".custom-status-list",
|
||||
) as HTMLElement;
|
||||
if (statusList) {
|
||||
this.ui.renderCustomStatuses(statusList, this.plugin.settings);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles adding new custom status
|
||||
*/
|
||||
|
||||
onCustomStatusAdd: SettingsUICallbacks["onCustomStatusAdd"] = async () => {
|
||||
const newStatus: Status = {
|
||||
name: `status${this.plugin.settings.customStatuses.length + 1}`,
|
||||
icon: "⭐",
|
||||
};
|
||||
|
||||
this.plugin.settings.customStatuses.push(newStatus);
|
||||
this.plugin.settings.statusColors[newStatus.name] = "#ffffff";
|
||||
|
||||
await this.plugin.saveSettings();
|
||||
|
||||
// Re-render the custom statuses section
|
||||
const statusList = document.querySelector(
|
||||
".custom-status-list",
|
||||
) as HTMLElement;
|
||||
if (statusList) {
|
||||
this.ui.renderCustomStatuses(statusList, this.plugin.settings);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,345 +1,479 @@
|
|||
import { Setting } from 'obsidian';
|
||||
import { Status } from '../../models/types';
|
||||
import { PREDEFINED_TEMPLATES } from '../../constants/status-templates';
|
||||
|
||||
/**
|
||||
* Callbacks interface for settings UI interactions
|
||||
*/
|
||||
export interface SettingsUICallbacks {
|
||||
/** Handle template enable/disable toggle */
|
||||
onTemplateToggle: (templateId: string, enabled: boolean) => Promise<void>;
|
||||
/** Handle general setting changes */
|
||||
onSettingChange: (key: string, value: any) => Promise<void>;
|
||||
/** Handle custom status field changes */
|
||||
onCustomStatusChange: (index: number, field: string, value: any) => Promise<void>;
|
||||
/** Handle custom status removal */
|
||||
onCustomStatusRemove: (index: number) => Promise<void>;
|
||||
/** Handle adding new custom status */
|
||||
onCustomStatusAdd: () => Promise<void>;
|
||||
}
|
||||
import { Setting } from "obsidian";
|
||||
import { NoteStatusSettings, Status } from "../../models/types";
|
||||
import { PREDEFINED_TEMPLATES } from "../../constants/status-templates";
|
||||
import { SettingsUICallbacks } from "./types";
|
||||
|
||||
/**
|
||||
* Pure UI component for rendering settings interface
|
||||
*/
|
||||
export class NoteStatusSettingsUI {
|
||||
private callbacks: SettingsUICallbacks;
|
||||
private quickCommandsContainer: HTMLElement | null = null;
|
||||
private callbacks: SettingsUICallbacks;
|
||||
private quickCommandsContainer: HTMLElement | null = null;
|
||||
|
||||
constructor(callbacks: SettingsUICallbacks) {
|
||||
this.callbacks = callbacks;
|
||||
}
|
||||
constructor(callbacks: SettingsUICallbacks) {
|
||||
this.callbacks = callbacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the complete settings interface
|
||||
*/
|
||||
render(containerEl: HTMLElement, settings: any): void {
|
||||
containerEl.empty();
|
||||
/**
|
||||
* Renders the complete settings interface
|
||||
*/
|
||||
render(containerEl: HTMLElement, settings: NoteStatusSettings): void {
|
||||
containerEl.empty();
|
||||
|
||||
this.renderTemplateSettings(containerEl, settings);
|
||||
this.renderUISettings(containerEl, settings);
|
||||
this.renderTagSettings(containerEl, settings);
|
||||
this.renderCustomStatusSettings(containerEl, settings);
|
||||
this.renderQuickCommandsSettings(containerEl, settings);
|
||||
}
|
||||
this.renderTemplateSettings(containerEl, settings);
|
||||
this.renderUISettings(containerEl, settings);
|
||||
this.renderTagSettings(containerEl, settings);
|
||||
this.renderCustomStatusSettings(containerEl, settings);
|
||||
this.renderQuickCommandsSettings(containerEl, settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the status templates section
|
||||
*/
|
||||
private renderTemplateSettings(containerEl: HTMLElement, settings: any): void {
|
||||
new Setting(containerEl).setName('Status templates').setHeading();
|
||||
|
||||
containerEl.createEl('p', {
|
||||
text: 'Enable predefined templates to quickly add common status workflows',
|
||||
cls: 'setting-item-description'
|
||||
});
|
||||
|
||||
const templatesContainer = containerEl.createDiv({ cls: 'templates-container' });
|
||||
|
||||
PREDEFINED_TEMPLATES.forEach(template => {
|
||||
const templateEl = templatesContainer.createDiv({ cls: 'template-item' });
|
||||
const headerEl = templateEl.createDiv({ cls: 'template-header' });
|
||||
|
||||
const isEnabled = settings.enabledTemplates.includes(template.id);
|
||||
const checkbox = headerEl.createEl('input', {
|
||||
type: 'checkbox',
|
||||
cls: 'template-checkbox'
|
||||
});
|
||||
checkbox.checked = isEnabled;
|
||||
|
||||
checkbox.addEventListener('change', () => {
|
||||
this.callbacks.onTemplateToggle(template.id, checkbox.checked);
|
||||
this.refreshQuickCommandsList(settings);
|
||||
});
|
||||
|
||||
headerEl.createEl('span', {
|
||||
text: template.name,
|
||||
cls: 'template-name'
|
||||
});
|
||||
|
||||
templateEl.createEl('div', {
|
||||
text: template.description,
|
||||
cls: 'template-description'
|
||||
});
|
||||
|
||||
const statusesEl = templateEl.createDiv({ cls: 'template-statuses' });
|
||||
template.statuses.forEach(status => {
|
||||
const statusEl = statusesEl.createEl('div', { cls: 'template-status-chip' });
|
||||
const colorDot = statusEl.createEl('span', { cls: 'status-color-dot' });
|
||||
colorDot.style.setProperty('--dot-color', status.color || '#ffffff');
|
||||
statusEl.createSpan({ text: `${status.icon} ${status.name}` });
|
||||
});
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Renders the status templates section
|
||||
*/
|
||||
private renderTemplateSettings(
|
||||
containerEl: HTMLElement,
|
||||
settings: NoteStatusSettings,
|
||||
): void {
|
||||
new Setting(containerEl).setName("Status templates").setHeading();
|
||||
|
||||
/**
|
||||
* Renders the UI display settings section
|
||||
*/
|
||||
private renderUISettings(containerEl: HTMLElement, settings: any): void {
|
||||
new Setting(containerEl).setName('User interface').setHeading();
|
||||
containerEl.createEl("p", {
|
||||
text: "Enable predefined templates to quickly add common status workflows",
|
||||
cls: "setting-item-description",
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Show status bar')
|
||||
.setDesc('Display the status bar')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(settings.showStatusBar)
|
||||
.onChange(value => this.callbacks.onSettingChange('showStatusBar', value)));
|
||||
const templatesContainer = containerEl.createDiv({
|
||||
cls: "templates-container",
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Auto-hide status bar')
|
||||
.setDesc('Hide the status bar when status is unknown')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(settings.autoHideStatusBar)
|
||||
.onChange(value => this.callbacks.onSettingChange('autoHideStatusBar', value)));
|
||||
PREDEFINED_TEMPLATES.forEach((template) => {
|
||||
const templateEl = templatesContainer.createDiv({
|
||||
cls: "template-item",
|
||||
});
|
||||
const headerEl = templateEl.createDiv({ cls: "template-header" });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Show status icons in file explorer')
|
||||
.setDesc('Display status icons in the file explorer')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(settings.showStatusIconsInExplorer)
|
||||
.onChange(value => this.callbacks.onSettingChange('showStatusIconsInExplorer', value)));
|
||||
const isEnabled = settings.enabledTemplates.includes(template.id);
|
||||
const checkbox = headerEl.createEl("input", {
|
||||
type: "checkbox",
|
||||
cls: "template-checkbox",
|
||||
});
|
||||
checkbox.checked = isEnabled;
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Hide unknown status in file explorer')
|
||||
.setDesc('Hide status icons for files with unknown status in the file explorer')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(settings.hideUnknownStatusInExplorer || false)
|
||||
.onChange(value => this.callbacks.onSettingChange('hideUnknownStatusInExplorer', value)));
|
||||
checkbox.addEventListener("change", () => {
|
||||
this.callbacks.onTemplateToggle(template.id, checkbox.checked);
|
||||
this.refreshQuickCommandsList(settings);
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Default to compact view')
|
||||
.setDesc('Start the status pane in compact view by default')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(settings.compactView || false)
|
||||
.onChange(value => this.callbacks.onSettingChange('compactView', value)));
|
||||
headerEl.createEl("span", {
|
||||
text: template.name,
|
||||
cls: "template-name",
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Exclude unassigned notes from status pane')
|
||||
.setDesc('Improves performance by excluding notes with no assigned status from the status pane. Recommended for large vaults.')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(settings.excludeUnknownStatus || false)
|
||||
.onChange(value => this.callbacks.onSettingChange('excludeUnknownStatus', value)));
|
||||
}
|
||||
templateEl.createEl("div", {
|
||||
text: template.description,
|
||||
cls: "template-description",
|
||||
});
|
||||
|
||||
/**
|
||||
* Renders the status tag configuration section
|
||||
*/
|
||||
private renderTagSettings(containerEl: HTMLElement, settings: any): void {
|
||||
new Setting(containerEl).setName('Status tag').setHeading();
|
||||
const statusesEl = templateEl.createDiv({
|
||||
cls: "template-statuses",
|
||||
});
|
||||
template.statuses.forEach((status) => {
|
||||
const statusEl = statusesEl.createEl("div", {
|
||||
cls: "template-status-chip",
|
||||
});
|
||||
const colorDot = statusEl.createEl("span", {
|
||||
cls: "status-color-dot",
|
||||
});
|
||||
colorDot.style.setProperty(
|
||||
"--dot-color",
|
||||
status.color || "#ffffff",
|
||||
);
|
||||
statusEl.createSpan({ text: `${status.icon} ${status.name}` });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Enable multiple statuses')
|
||||
.setDesc('Allow notes to have multiple statuses at the same time')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(settings.useMultipleStatuses)
|
||||
.onChange(value => this.callbacks.onSettingChange('useMultipleStatuses', value)));
|
||||
/**
|
||||
* Renders the UI display settings section
|
||||
*/
|
||||
private renderUISettings(
|
||||
containerEl: HTMLElement,
|
||||
settings: NoteStatusSettings,
|
||||
): void {
|
||||
new Setting(containerEl).setName("User interface").setHeading();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Status tag prefix')
|
||||
.setDesc('The YAML frontmatter tag name used for status (default: obsidian-note-status)')
|
||||
.addText(text => text
|
||||
.setValue(settings.tagPrefix)
|
||||
.onChange(value => {
|
||||
if (value.trim()) {
|
||||
this.callbacks.onSettingChange('tagPrefix', value.trim());
|
||||
}
|
||||
}));
|
||||
new Setting(containerEl)
|
||||
.setName("Show status bar")
|
||||
.setDesc("Display the status bar")
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(settings.showStatusBar)
|
||||
.onChange((value) =>
|
||||
this.callbacks.onSettingChange("showStatusBar", value),
|
||||
),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Auto-hide status bar")
|
||||
.setDesc("Hide the status bar when status is unknown")
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(settings.autoHideStatusBar)
|
||||
.onChange((value) =>
|
||||
this.callbacks.onSettingChange(
|
||||
"autoHideStatusBar",
|
||||
value,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Strict status validation')
|
||||
.setDesc('Only show statuses that are defined in templates or custom statuses. ⚠️ WARNING: When enabled, any unknown statuses will be automatically removed when modifying file statuses.')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(settings.strictStatuses || false)
|
||||
.onChange(value => this.callbacks.onSettingChange('strictStatuses', value)));
|
||||
new Setting(containerEl)
|
||||
.setName("Show status icons in file explorer")
|
||||
.setDesc("Display status icons in the file explorer")
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(settings.showStatusIconsInExplorer)
|
||||
.onChange((value) =>
|
||||
this.callbacks.onSettingChange(
|
||||
"showStatusIconsInExplorer",
|
||||
value,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
}
|
||||
new Setting(containerEl)
|
||||
.setName("Hide unknown status in file explorer")
|
||||
.setDesc(
|
||||
"Hide status icons for files with unknown status in the file explorer",
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(settings.hideUnknownStatusInExplorer || false)
|
||||
.onChange((value) =>
|
||||
this.callbacks.onSettingChange(
|
||||
"hideUnknownStatusInExplorer",
|
||||
value,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Renders the quick commands configuration section
|
||||
*/
|
||||
private renderQuickCommandsSettings(containerEl: HTMLElement, settings: any): void {
|
||||
new Setting(containerEl)
|
||||
.setName('Quick status commands')
|
||||
.setDesc('Select which statuses should have dedicated commands in the command palette. These can be assigned hotkeys for quick access.')
|
||||
.setHeading();
|
||||
|
||||
this.quickCommandsContainer = containerEl.createDiv({ cls: 'quick-commands-container' });
|
||||
this.populateQuickCommandsList(settings);
|
||||
|
||||
}
|
||||
new Setting(containerEl)
|
||||
.setName("Default to compact view")
|
||||
.setDesc("Start the status pane in compact view by default")
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(settings.compactView || false)
|
||||
.onChange((value) =>
|
||||
this.callbacks.onSettingChange("compactView", value),
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Populate the quick commands list
|
||||
*/
|
||||
private populateQuickCommandsList(settings: any): void {
|
||||
if (!this.quickCommandsContainer) return;
|
||||
|
||||
this.quickCommandsContainer.empty();
|
||||
// Get all available statuses from service
|
||||
const allStatuses = this.getAllAvailableStatuses(settings);
|
||||
const currentQuickCommands = settings.quickStatusCommands || [];
|
||||
|
||||
allStatuses.forEach(status => {
|
||||
const setting = new Setting(this.quickCommandsContainer)
|
||||
.setName(`${status.icon} ${status.name}`)
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(currentQuickCommands.includes(status.name))
|
||||
.onChange(async (value) => {
|
||||
const updatedCommands = value
|
||||
? [...currentQuickCommands.filter((cmd: string) => cmd !== status.name), status.name]
|
||||
: currentQuickCommands.filter((cmd: string) => cmd !== status.name);
|
||||
|
||||
await this.callbacks.onSettingChange('quickStatusCommands', updatedCommands);
|
||||
}));
|
||||
|
||||
if (status.description) {
|
||||
setting.setDesc(status.description);
|
||||
}
|
||||
});
|
||||
|
||||
if (allStatuses.length === 0) {
|
||||
this.quickCommandsContainer.createDiv({
|
||||
text: 'No statuses available. Enable templates or add custom statuses first.',
|
||||
cls: 'setting-item-description'
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Refresh the quick commands list when statuses change
|
||||
*/
|
||||
private refreshQuickCommandsList(settings: any): void {
|
||||
// Add small delay to ensure settings are updated
|
||||
setTimeout(() => {
|
||||
this.populateQuickCommandsList(settings);
|
||||
}, 50);
|
||||
}
|
||||
new Setting(containerEl)
|
||||
.setName("Exclude unassigned notes from status pane")
|
||||
.setDesc(
|
||||
"Improves performance by excluding notes with no assigned status from the status pane. Recommended for large vaults.",
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(settings.excludeUnknownStatus || false)
|
||||
.onChange((value) =>
|
||||
this.callbacks.onSettingChange(
|
||||
"excludeUnknownStatus",
|
||||
value,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available statuses from templates and custom statuses
|
||||
*/
|
||||
private getAllAvailableStatuses(settings: any): Array<{name: string, icon: string, description?: string}> {
|
||||
const statuses: Array<{name: string, icon: string, description?: string}> = [];
|
||||
|
||||
// Add custom statuses
|
||||
statuses.push(...settings.customStatuses);
|
||||
|
||||
// Add template statuses if not using custom only
|
||||
if (!settings.useCustomStatusesOnly) {
|
||||
for (const templateId of settings.enabledTemplates) {
|
||||
const template = PREDEFINED_TEMPLATES.find(t => t.id === templateId);
|
||||
if (template) {
|
||||
for (const status of template.statuses) {
|
||||
if (!statuses.find(s => s.name === status.name)) {
|
||||
statuses.push(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return statuses.filter(s => s.name !== 'unknown');
|
||||
}
|
||||
/**
|
||||
* Renders the status tag configuration section
|
||||
*/
|
||||
private renderTagSettings(
|
||||
containerEl: HTMLElement,
|
||||
settings: NoteStatusSettings,
|
||||
): void {
|
||||
new Setting(containerEl).setName("Status tag").setHeading();
|
||||
|
||||
/**
|
||||
* Renders the custom status management section
|
||||
*/
|
||||
private renderCustomStatusSettings(containerEl: HTMLElement, settings: any): void {
|
||||
new Setting(containerEl).setName('Custom statuses').setHeading();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Use only custom statuses')
|
||||
.setDesc('Ignore template statuses and use only the custom statuses defined below')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(settings.useCustomStatusesOnly || false)
|
||||
.onChange(async (value) => {
|
||||
await this.callbacks.onSettingChange('useCustomStatusesOnly', value);
|
||||
// Update quick commands list when custom-only mode changes
|
||||
this.refreshQuickCommandsList(settings);
|
||||
}));
|
||||
|
||||
const statusList = containerEl.createDiv({ cls: 'custom-status-list' });
|
||||
this.renderCustomStatuses(statusList, settings);
|
||||
new Setting(containerEl)
|
||||
.setName("Enable multiple statuses")
|
||||
.setDesc("Allow notes to have multiple statuses at the same time")
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(settings.useMultipleStatuses)
|
||||
.onChange((value) =>
|
||||
this.callbacks.onSettingChange(
|
||||
"useMultipleStatuses",
|
||||
value,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Add new status')
|
||||
.setDesc('Add a custom status with a name, icon, and color')
|
||||
.addButton(button => button
|
||||
.setButtonText('Add Status')
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
await this.callbacks.onCustomStatusAdd();
|
||||
this.refreshQuickCommandsList(settings);
|
||||
}));
|
||||
}
|
||||
new Setting(containerEl)
|
||||
.setName("Status tag prefix")
|
||||
.setDesc(
|
||||
"The YAML frontmatter tag name used for status (default: obsidian-note-status)",
|
||||
)
|
||||
.addText((text) =>
|
||||
text.setValue(settings.tagPrefix).onChange((value) => {
|
||||
if (value.trim()) {
|
||||
this.callbacks.onSettingChange(
|
||||
"tagPrefix",
|
||||
value.trim(),
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Renders the list of custom statuses with edit controls
|
||||
*/
|
||||
renderCustomStatuses(statusList: HTMLElement, settings: any): void {
|
||||
statusList.empty();
|
||||
|
||||
settings.customStatuses.forEach((status: Status, index: number) => {
|
||||
const setting = new Setting(statusList)
|
||||
.setName(status.name)
|
||||
.setClass('status-item');
|
||||
new Setting(containerEl)
|
||||
.setName("Strict status validation")
|
||||
.setDesc(
|
||||
"Only show statuses that are defined in templates or custom statuses. ⚠️ WARNING: When enabled, any unknown statuses will be automatically removed when modifying file statuses.",
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(settings.strictStatuses || false)
|
||||
.onChange((value) =>
|
||||
this.callbacks.onSettingChange("strictStatuses", value),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
setting.addText(text => text
|
||||
.setPlaceholder('Name')
|
||||
.setValue(status.name)
|
||||
.onChange(async (value) => {
|
||||
await this.callbacks.onCustomStatusChange(index, 'name', value || 'unnamed');
|
||||
// Update quick commands list when status name changes
|
||||
this.refreshQuickCommandsList(settings);
|
||||
}));
|
||||
/**
|
||||
* Renders the quick commands configuration section
|
||||
*/
|
||||
private renderQuickCommandsSettings(
|
||||
containerEl: HTMLElement,
|
||||
settings: NoteStatusSettings,
|
||||
): void {
|
||||
new Setting(containerEl)
|
||||
.setName("Quick status commands")
|
||||
.setDesc(
|
||||
"Select which statuses should have dedicated commands in the command palette. These can be assigned hotkeys for quick access.",
|
||||
)
|
||||
.setHeading();
|
||||
|
||||
setting.addText(text => text
|
||||
.setPlaceholder('Icon')
|
||||
.setValue(status.icon)
|
||||
.onChange(async (value) => {
|
||||
await this.callbacks.onCustomStatusChange(index, 'icon', value || '❓');
|
||||
this.refreshQuickCommandsList(settings);
|
||||
}));
|
||||
this.quickCommandsContainer = containerEl.createDiv({
|
||||
cls: "quick-commands-container",
|
||||
});
|
||||
this.populateQuickCommandsList(settings);
|
||||
}
|
||||
|
||||
setting.addColorPicker(colorPicker => colorPicker
|
||||
.setValue(settings.statusColors[status.name] || '#ffffff')
|
||||
.onChange(value => this.callbacks.onCustomStatusChange(index, 'color', value)));
|
||||
|
||||
setting.addText(text => text
|
||||
.setPlaceholder('Description')
|
||||
.setValue(status.description || '')
|
||||
.onChange(async (value) => {
|
||||
await this.callbacks.onCustomStatusChange(index, 'description', value);
|
||||
this.refreshQuickCommandsList(settings);
|
||||
}));
|
||||
/**
|
||||
* Populate the quick commands list
|
||||
*/
|
||||
private populateQuickCommandsList(settings: NoteStatusSettings): void {
|
||||
if (!this.quickCommandsContainer) return;
|
||||
|
||||
setting.addButton(button => button
|
||||
.setButtonText('Remove')
|
||||
.setClass('status-remove-button')
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
await this.callbacks.onCustomStatusRemove(index);
|
||||
this.refreshQuickCommandsList(settings);
|
||||
}));
|
||||
});
|
||||
}
|
||||
this.quickCommandsContainer.empty();
|
||||
// Get all available statuses from service
|
||||
const allStatuses = this.getAllAvailableStatuses(settings);
|
||||
const currentQuickCommands = settings.quickStatusCommands || [];
|
||||
|
||||
allStatuses.forEach((status) => {
|
||||
if (!this.quickCommandsContainer) return;
|
||||
const setting = new Setting(this.quickCommandsContainer)
|
||||
.setName(`${status.icon} ${status.name}`)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(currentQuickCommands.includes(status.name))
|
||||
.onChange(async (value) => {
|
||||
const updatedCommands: string[] = value
|
||||
? [
|
||||
...currentQuickCommands.filter(
|
||||
(cmd: string) =>
|
||||
cmd !== status.name,
|
||||
),
|
||||
status.name,
|
||||
]
|
||||
: currentQuickCommands.filter(
|
||||
(cmd: string) => cmd !== status.name,
|
||||
);
|
||||
|
||||
await this.callbacks.onSettingChange(
|
||||
"quickStatusCommands",
|
||||
updatedCommands,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
if (status.description) {
|
||||
setting.setDesc(status.description);
|
||||
}
|
||||
});
|
||||
|
||||
if (allStatuses.length === 0) {
|
||||
this.quickCommandsContainer.createDiv({
|
||||
text: "No statuses available. Enable templates or add custom statuses first.",
|
||||
cls: "setting-item-description",
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Refresh the quick commands list when statuses change
|
||||
*/
|
||||
private refreshQuickCommandsList(settings: NoteStatusSettings): void {
|
||||
// Add small delay to ensure settings are updated
|
||||
setTimeout(() => {
|
||||
this.populateQuickCommandsList(settings);
|
||||
}, 50);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available statuses from templates and custom statuses
|
||||
*/
|
||||
private getAllAvailableStatuses(
|
||||
settings: NoteStatusSettings,
|
||||
): Array<{ name: string; icon: string; description?: string }> {
|
||||
const statuses: Array<{
|
||||
name: string;
|
||||
icon: string;
|
||||
description?: string;
|
||||
}> = [];
|
||||
|
||||
// Add custom statuses
|
||||
statuses.push(...settings.customStatuses);
|
||||
|
||||
// Add template statuses if not using custom only
|
||||
if (!settings.useCustomStatusesOnly) {
|
||||
for (const templateId of settings.enabledTemplates) {
|
||||
const template = PREDEFINED_TEMPLATES.find(
|
||||
(t) => t.id === templateId,
|
||||
);
|
||||
if (template) {
|
||||
for (const status of template.statuses) {
|
||||
if (!statuses.find((s) => s.name === status.name)) {
|
||||
statuses.push(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return statuses.filter((s) => s.name !== "unknown");
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the custom status management section
|
||||
*/
|
||||
private renderCustomStatusSettings(
|
||||
containerEl: HTMLElement,
|
||||
settings: NoteStatusSettings,
|
||||
): void {
|
||||
new Setting(containerEl).setName("Custom statuses").setHeading();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Use only custom statuses")
|
||||
.setDesc(
|
||||
"Ignore template statuses and use only the custom statuses defined below",
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(settings.useCustomStatusesOnly || false)
|
||||
.onChange(async (value) => {
|
||||
await this.callbacks.onSettingChange(
|
||||
"useCustomStatusesOnly",
|
||||
value,
|
||||
);
|
||||
// Update quick commands list when custom-only mode changes
|
||||
this.refreshQuickCommandsList(settings);
|
||||
}),
|
||||
);
|
||||
|
||||
const statusList = containerEl.createDiv({ cls: "custom-status-list" });
|
||||
this.renderCustomStatuses(statusList, settings);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Add new status")
|
||||
.setDesc("Add a custom status with a name, icon, and color")
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Add Status")
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
await this.callbacks.onCustomStatusAdd();
|
||||
this.refreshQuickCommandsList(settings);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the list of custom statuses with edit controls
|
||||
*/
|
||||
renderCustomStatuses(
|
||||
statusList: HTMLElement,
|
||||
settings: NoteStatusSettings,
|
||||
): void {
|
||||
statusList.empty();
|
||||
|
||||
settings.customStatuses.forEach((status: Status, index: number) => {
|
||||
const setting = new Setting(statusList)
|
||||
.setName(status.name)
|
||||
.setClass("status-item");
|
||||
|
||||
setting.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("Name")
|
||||
.setValue(status.name)
|
||||
.onChange(async (value) => {
|
||||
await this.callbacks.onCustomStatusChange(
|
||||
index,
|
||||
"name",
|
||||
value || "unnamed",
|
||||
);
|
||||
// Update quick commands list when status name changes
|
||||
this.refreshQuickCommandsList(settings);
|
||||
}),
|
||||
);
|
||||
|
||||
setting.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("Icon")
|
||||
.setValue(status.icon)
|
||||
.onChange(async (value) => {
|
||||
await this.callbacks.onCustomStatusChange(
|
||||
index,
|
||||
"icon",
|
||||
value || "❓",
|
||||
);
|
||||
this.refreshQuickCommandsList(settings);
|
||||
}),
|
||||
);
|
||||
|
||||
setting.addColorPicker((colorPicker) =>
|
||||
colorPicker
|
||||
.setValue(settings.statusColors[status.name] || "#ffffff")
|
||||
.onChange((value) =>
|
||||
this.callbacks.onCustomStatusChange(
|
||||
index,
|
||||
"color",
|
||||
value,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
setting.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("Description")
|
||||
.setValue(status.description || "")
|
||||
.onChange(async (value) => {
|
||||
await this.callbacks.onCustomStatusChange(
|
||||
index,
|
||||
"description",
|
||||
value,
|
||||
);
|
||||
this.refreshQuickCommandsList(settings);
|
||||
}),
|
||||
);
|
||||
|
||||
setting.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Remove")
|
||||
.setClass("status-remove-button")
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
await this.callbacks.onCustomStatusRemove(index);
|
||||
this.refreshQuickCommandsList(settings);
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
24
integrations/settings/types.ts
Normal file
24
integrations/settings/types.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { NoteStatusSettings, Status } from "../../models/types";
|
||||
|
||||
/**
|
||||
* Callbacks interface for settings UI interactions
|
||||
*/
|
||||
export interface SettingsUICallbacks {
|
||||
/** Handle template enable/disable toggle */
|
||||
onTemplateToggle: (templateId: string, enabled: boolean) => Promise<void>;
|
||||
/** Handle general setting changes */
|
||||
onSettingChange: (
|
||||
key: keyof NoteStatusSettings,
|
||||
value: boolean | string | string[],
|
||||
) => Promise<void>;
|
||||
/** Handle custom status field changes */
|
||||
onCustomStatusChange: (
|
||||
index: number,
|
||||
field: keyof Status,
|
||||
value: string,
|
||||
) => Promise<void>;
|
||||
/** Handle custom status removal */
|
||||
onCustomStatusRemove: (index: number) => Promise<void>;
|
||||
/** Handle adding new custom status */
|
||||
onCustomStatusAdd: () => Promise<void>;
|
||||
}
|
||||
|
|
@ -1,124 +1,131 @@
|
|||
import { App, TFile, WorkspaceLeaf } from 'obsidian';
|
||||
import { NoteStatusSettings } from '../../models/types';
|
||||
import { ToolbarIntegration } from '../editor/toolbar-integration';
|
||||
import { StatusService } from 'services/status-service';
|
||||
import { App, TFile, WorkspaceLeaf } from "obsidian";
|
||||
import { NoteStatusSettings } from "../../models/types";
|
||||
import { ToolbarIntegration } from "../editor/toolbar-integration";
|
||||
import { StatusService } from "services/status-service";
|
||||
|
||||
/**
|
||||
* Gestiona la integración con el workspace de Obsidian
|
||||
*/
|
||||
export class WorkspaceIntegration {
|
||||
private app: App;
|
||||
private settings: NoteStatusSettings;
|
||||
private statusService: StatusService;
|
||||
private toolbarIntegration: ToolbarIntegration;
|
||||
private lastActiveFile: TFile | null = null;
|
||||
private fileOpenEventRef: any;
|
||||
private activeLeafChangeEventRef: any;
|
||||
private layoutChangeEventRef: any;
|
||||
private app: App;
|
||||
private settings: NoteStatusSettings;
|
||||
private statusService: StatusService;
|
||||
private toolbarIntegration: ToolbarIntegration;
|
||||
private lastActiveFile: TFile | null = null;
|
||||
private fileOpenEventRef: (file: TFile) => void;
|
||||
private activeLeafChangeEventRef: (file: WorkspaceLeaf) => void;
|
||||
private layoutChangeEventRef: () => void;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
settings: NoteStatusSettings,
|
||||
statusService: StatusService,
|
||||
toolbarIntegration: ToolbarIntegration
|
||||
) {
|
||||
this.app = app;
|
||||
this.settings = settings;
|
||||
this.statusService = statusService;
|
||||
this.toolbarIntegration = toolbarIntegration;
|
||||
}
|
||||
constructor(
|
||||
app: App,
|
||||
settings: NoteStatusSettings,
|
||||
statusService: StatusService,
|
||||
toolbarIntegration: ToolbarIntegration,
|
||||
) {
|
||||
this.app = app;
|
||||
this.settings = settings;
|
||||
this.statusService = statusService;
|
||||
this.toolbarIntegration = toolbarIntegration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualiza la configuración
|
||||
*/
|
||||
public updateSettings(settings: NoteStatusSettings): void {
|
||||
this.settings = settings;
|
||||
}
|
||||
/**
|
||||
* Actualiza la configuración
|
||||
*/
|
||||
public updateSettings(settings: NoteStatusSettings): void {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra eventos del workspace
|
||||
*/
|
||||
public registerWorkspaceEvents(): void {
|
||||
this.fileOpenEventRef = (file: any) => {
|
||||
if (file instanceof TFile) {
|
||||
this.handleFileOpen(file);
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Registra eventos del workspace
|
||||
*/
|
||||
public registerWorkspaceEvents(): void {
|
||||
this.fileOpenEventRef = (file: TFile) => {
|
||||
if (file instanceof TFile) {
|
||||
this.handleFileOpen(file);
|
||||
}
|
||||
};
|
||||
|
||||
this.activeLeafChangeEventRef = (leaf: WorkspaceLeaf) => {
|
||||
this.handleActiveLeafChange(leaf);
|
||||
};
|
||||
this.activeLeafChangeEventRef = (leaf: WorkspaceLeaf) => {
|
||||
this.handleActiveLeafChange(leaf);
|
||||
};
|
||||
|
||||
this.layoutChangeEventRef = () => {
|
||||
this.handleLayoutChange();
|
||||
};
|
||||
this.layoutChangeEventRef = () => {
|
||||
this.handleLayoutChange();
|
||||
};
|
||||
|
||||
this.app.workspace.on('file-open', this.fileOpenEventRef);
|
||||
this.app.workspace.on('active-leaf-change', this.activeLeafChangeEventRef);
|
||||
this.app.workspace.on('layout-change', this.layoutChangeEventRef);
|
||||
}
|
||||
this.app.workspace.on("file-open", this.fileOpenEventRef);
|
||||
this.app.workspace.on(
|
||||
"active-leaf-change",
|
||||
this.activeLeafChangeEventRef,
|
||||
);
|
||||
this.app.workspace.on("layout-change", this.layoutChangeEventRef);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maneja apertura de archivo
|
||||
*/
|
||||
private handleFileOpen(file: TFile): void {
|
||||
// Añade el botón de la barra de herramientas
|
||||
this.toolbarIntegration.addToolbarButtonToActiveLeaf();
|
||||
|
||||
// Actualiza estado
|
||||
this.propagateNoteStatusChange(file);
|
||||
}
|
||||
/**
|
||||
* Maneja apertura de archivo
|
||||
*/
|
||||
private handleFileOpen(file: TFile): void {
|
||||
// Añade el botón de la barra de herramientas
|
||||
this.toolbarIntegration.addToolbarButtonToActiveLeaf();
|
||||
|
||||
/**
|
||||
* Maneja cambio de hoja activa
|
||||
*/
|
||||
private handleActiveLeafChange(leaf: WorkspaceLeaf): void {
|
||||
// Añade el botón de la barra de herramientas
|
||||
this.toolbarIntegration.addToolbarButtonToActiveLeaf();
|
||||
|
||||
const activeFile = this.app.workspace.getActiveFile();
|
||||
|
||||
// Solo actualiza si el archivo realmente cambió
|
||||
// if (this.lastActiveFile?.path !== activeFile?.path) {
|
||||
// this.lastActiveFile = activeFile;
|
||||
// this.propagateNoteStatusChange();
|
||||
// }
|
||||
}
|
||||
// Actualiza estado
|
||||
this.propagateNoteStatusChange(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maneja y propaga el cambio de layout
|
||||
*/
|
||||
private handleLayoutChange(): void {
|
||||
window.dispatchEvent(new CustomEvent('note-status:update-pane'));
|
||||
}
|
||||
/**
|
||||
* Maneja cambio de hoja activa
|
||||
*/
|
||||
private handleActiveLeafChange(leaf: WorkspaceLeaf): void {
|
||||
// Añade el botón de la barra de herramientas
|
||||
this.toolbarIntegration.addToolbarButtonToActiveLeaf();
|
||||
|
||||
/**
|
||||
* Verifica y propaga el estado de la nota activa
|
||||
*/
|
||||
private propagateNoteStatusChange(file: TFile): void {
|
||||
try {
|
||||
const activeFile = this.app.workspace.getActiveFile();
|
||||
let fileStatuses: string[] = [];
|
||||
if (activeFile && activeFile.extension === 'md') {
|
||||
fileStatuses = this.statusService.getFileStatuses(activeFile)
|
||||
}
|
||||
// Dispara evento para que otros componentes se actualicen
|
||||
window.dispatchEvent(new CustomEvent('note-status:status-changed', {
|
||||
detail: { statuses: fileStatuses, file: file }
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error checking note status:', error);
|
||||
}
|
||||
}
|
||||
public unload(): void {
|
||||
if (this.fileOpenEventRef) {
|
||||
this.app.workspace.off('file-open', this.fileOpenEventRef);
|
||||
}
|
||||
if (this.activeLeafChangeEventRef) {
|
||||
this.app.workspace.off('active-leaf-change', this.activeLeafChangeEventRef);
|
||||
}
|
||||
if (this.layoutChangeEventRef) {
|
||||
this.app.workspace.off('layout-change', this.layoutChangeEventRef);
|
||||
}
|
||||
}
|
||||
//const activeFile = this.app.workspace.getActiveFile();
|
||||
// Solo actualiza si el archivo realmente cambió
|
||||
// if (this.lastActiveFile?.path !== activeFile?.path) {
|
||||
// this.lastActiveFile = activeFile;
|
||||
// this.propagateNoteStatusChange();
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Maneja y propaga el cambio de layout
|
||||
*/
|
||||
private handleLayoutChange(): void {
|
||||
window.dispatchEvent(new CustomEvent("note-status:update-pane"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica y propaga el estado de la nota activa
|
||||
*/
|
||||
private propagateNoteStatusChange(file: TFile): void {
|
||||
try {
|
||||
const activeFile = this.app.workspace.getActiveFile();
|
||||
let fileStatuses: string[] = [];
|
||||
if (activeFile && activeFile.extension === "md") {
|
||||
fileStatuses = this.statusService.getFileStatuses(activeFile);
|
||||
}
|
||||
// Dispara evento para que otros componentes se actualicen
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("note-status:status-changed", {
|
||||
detail: { statuses: fileStatuses, file: file },
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error checking note status:", error);
|
||||
}
|
||||
}
|
||||
public unload(): void {
|
||||
if (this.fileOpenEventRef) {
|
||||
this.app.workspace.off("file-open", this.fileOpenEventRef);
|
||||
}
|
||||
if (this.activeLeafChangeEventRef) {
|
||||
this.app.workspace.off(
|
||||
"active-leaf-change",
|
||||
this.activeLeafChangeEventRef,
|
||||
);
|
||||
}
|
||||
if (this.layoutChangeEventRef) {
|
||||
this.app.workspace.off("layout-change", this.layoutChangeEventRef);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,44 +1,76 @@
|
|||
import { TFile, View } from 'obsidian';
|
||||
import { TFile, View } from "obsidian";
|
||||
|
||||
declare module "obsidian" {
|
||||
interface WorkspaceLeaf {
|
||||
// TODO: Is deprecated, fix me
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface Commands {
|
||||
removeCommand(id: string): void;
|
||||
}
|
||||
|
||||
interface App {
|
||||
clipboard?: unknown;
|
||||
internalPlugins: InternalPlugins;
|
||||
commands: Commands;
|
||||
}
|
||||
|
||||
interface GlobalSearchPlugin {
|
||||
openGlobalSearch(query: string): void;
|
||||
}
|
||||
|
||||
interface InternalPlugins {
|
||||
getPluginById(
|
||||
id: "global-search",
|
||||
): { instance: GlobalSearchPlugin } | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a status with name, icon and color
|
||||
*/
|
||||
export interface Status {
|
||||
name: string;
|
||||
icon: string;
|
||||
color?: string; // Optional color property
|
||||
description?: string; // Optional description property
|
||||
name: string;
|
||||
icon: string;
|
||||
color?: string; // Optional color property
|
||||
description?: string; // Optional description property
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin settings interface
|
||||
*/
|
||||
export interface NoteStatusSettings {
|
||||
statusColors: Record<string, string>;
|
||||
showStatusBar: boolean;
|
||||
autoHideStatusBar: boolean;
|
||||
customStatuses: Status[];
|
||||
showStatusIconsInExplorer: boolean;
|
||||
hideUnknownStatusInExplorer: boolean;
|
||||
collapsedStatuses: Record<string, boolean>;
|
||||
compactView: boolean;
|
||||
enabledTemplates: string[]; // IDs of enabled templates
|
||||
useCustomStatusesOnly: boolean; // Whether to use only custom statuses or include templates
|
||||
useMultipleStatuses: boolean; // Whether to allow multiple statuses per note
|
||||
tagPrefix: string; // Prefix for the status tag (default: 'status')
|
||||
strictStatuses: boolean; // Whether to only show known statuses
|
||||
excludeUnknownStatus: boolean; // Whether to exclude files with unknown status from the status pane
|
||||
quickStatusCommands: string[];
|
||||
statusColors: Record<string, string>;
|
||||
showStatusBar: boolean;
|
||||
autoHideStatusBar: boolean;
|
||||
customStatuses: Status[];
|
||||
showStatusIconsInExplorer: boolean;
|
||||
hideUnknownStatusInExplorer: boolean;
|
||||
collapsedStatuses: Record<string, boolean>;
|
||||
compactView: boolean;
|
||||
enabledTemplates: string[]; // IDs of enabled templates
|
||||
useCustomStatusesOnly: boolean; // Whether to use only custom statuses or include templates
|
||||
useMultipleStatuses: boolean; // Whether to allow multiple statuses per note
|
||||
tagPrefix: string; // Prefix for the status tag (default: 'status')
|
||||
strictStatuses: boolean; // Whether to only show known statuses
|
||||
excludeUnknownStatus: boolean; // Whether to exclude files with unknown status from the status pane
|
||||
quickStatusCommands: string[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended interface for file explorer view
|
||||
*/
|
||||
export interface FileExplorerView extends View {
|
||||
fileItems: Record<string, {
|
||||
el?: HTMLElement;
|
||||
file?: TFile;
|
||||
titleEl?: HTMLElement;
|
||||
selfEl?: HTMLElement;
|
||||
}>;
|
||||
fileItems: Record<
|
||||
string,
|
||||
{
|
||||
el?: HTMLElement;
|
||||
file?: TFile;
|
||||
titleEl?: HTMLElement;
|
||||
selfEl?: HTMLElement;
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,388 +1,434 @@
|
|||
import { App, TFile, Editor, Notice } from 'obsidian';
|
||||
import { NoteStatusSettings, Status } from '../models/types';
|
||||
import { PREDEFINED_TEMPLATES } from '../constants/status-templates';
|
||||
import { App, TFile, Editor, Notice } from "obsidian";
|
||||
import { NoteStatusSettings, Status } from "../models/types";
|
||||
import { PREDEFINED_TEMPLATES } from "../constants/status-templates";
|
||||
|
||||
/**
|
||||
* Service for handling note status operations
|
||||
*/
|
||||
export class StatusService {
|
||||
private app: App;
|
||||
private settings: NoteStatusSettings;
|
||||
private allStatuses: Status[] = [];
|
||||
private app: App;
|
||||
private settings: NoteStatusSettings;
|
||||
private allStatuses: Status[] = [];
|
||||
|
||||
constructor(app: App, settings: NoteStatusSettings) {
|
||||
this.app = app;
|
||||
this.settings = settings;
|
||||
this.updateAllStatuses();
|
||||
}
|
||||
constructor(app: App, settings: NoteStatusSettings) {
|
||||
this.app = app;
|
||||
this.settings = settings;
|
||||
this.updateAllStatuses();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the settings reference and recalculates all statuses
|
||||
*/
|
||||
public updateSettings(settings: NoteStatusSettings): void {
|
||||
this.settings = settings;
|
||||
this.updateAllStatuses();
|
||||
}
|
||||
/**
|
||||
* Updates the settings reference and recalculates all statuses
|
||||
*/
|
||||
public updateSettings(settings: NoteStatusSettings): void {
|
||||
this.settings = settings;
|
||||
this.updateAllStatuses();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the combined list of all statuses (from templates and custom)
|
||||
*/
|
||||
private updateAllStatuses(): void {
|
||||
// Start with custom statuses
|
||||
this.allStatuses = [...this.settings.customStatuses];
|
||||
/**
|
||||
* Updates the combined list of all statuses (from templates and custom)
|
||||
*/
|
||||
private updateAllStatuses(): void {
|
||||
// Start with custom statuses
|
||||
this.allStatuses = [...this.settings.customStatuses];
|
||||
|
||||
// Add statuses from enabled templates if not using custom only
|
||||
if (!this.settings.useCustomStatusesOnly) {
|
||||
const templateStatuses = this.getTemplateStatuses();
|
||||
|
||||
for (const status of templateStatuses) {
|
||||
const existingIndex = this.allStatuses.findIndex(s =>
|
||||
s.name.toLowerCase() === status.name.toLowerCase());
|
||||
|
||||
if (existingIndex === -1) {
|
||||
// Add new status
|
||||
this.allStatuses.push(status);
|
||||
} else if (status.color) {
|
||||
// Update color in settings if it doesn't exist
|
||||
if (!this.settings.statusColors[status.name]) {
|
||||
this.settings.statusColors[status.name] = status.color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add statuses from enabled templates if not using custom only
|
||||
if (!this.settings.useCustomStatusesOnly) {
|
||||
const templateStatuses = this.getTemplateStatuses();
|
||||
|
||||
/**
|
||||
* Gets all statuses from enabled templates
|
||||
*/
|
||||
public getTemplateStatuses(): Status[] {
|
||||
return this.settings.enabledTemplates
|
||||
.map(id => PREDEFINED_TEMPLATES.find(t => t.id === id))
|
||||
.filter(Boolean)
|
||||
.flatMap(template => template ? template.statuses : []);
|
||||
}
|
||||
for (const status of templateStatuses) {
|
||||
const existingIndex = this.allStatuses.findIndex(
|
||||
(s) => s.name.toLowerCase() === status.name.toLowerCase(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Get all available statuses (combined from templates and custom)
|
||||
*/
|
||||
public getAllStatuses(): Status[] {
|
||||
return this.allStatuses;
|
||||
}
|
||||
if (existingIndex === -1) {
|
||||
// Add new status
|
||||
this.allStatuses.push(status);
|
||||
} else if (status.color) {
|
||||
// Update color in settings if it doesn't exist
|
||||
if (!this.settings.statusColors[status.name]) {
|
||||
this.settings.statusColors[status.name] = status.color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the statuses of a file from its metadata
|
||||
*/
|
||||
public getFileStatuses(file: TFile): string[] {
|
||||
const cachedMetadata = this.app.metadataCache.getFileCache(file);
|
||||
if (!cachedMetadata?.frontmatter) return ['unknown'];
|
||||
|
||||
const frontmatterStatus = cachedMetadata.frontmatter[this.settings.tagPrefix];
|
||||
if (!frontmatterStatus) return ['unknown'];
|
||||
|
||||
const statuses: string[] = [];
|
||||
|
||||
if (Array.isArray(frontmatterStatus)) {
|
||||
for (const statusName of frontmatterStatus) {
|
||||
if (this.settings.strictStatuses) {
|
||||
this.addValidStatus(statusName.toString(), statuses);
|
||||
} else {
|
||||
const cleanStatus = statusName.toString().trim();
|
||||
if (cleanStatus) statuses.push(cleanStatus);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.settings.strictStatuses) {
|
||||
this.addValidStatus(frontmatterStatus.toString(), statuses);
|
||||
} else {
|
||||
const cleanStatus = frontmatterStatus.toString().trim();
|
||||
if (cleanStatus) statuses.push(cleanStatus);
|
||||
}
|
||||
}
|
||||
|
||||
return statuses.length > 0 ? statuses : ['unknown'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a status to the list if it's valid
|
||||
*/
|
||||
private addValidStatus(statusName: string, targetStatuses: string[]): void {
|
||||
const normalizedStatus = statusName.toLowerCase();
|
||||
const matchingStatus = this.allStatuses.find(s =>
|
||||
s.name.toLowerCase() === normalizedStatus);
|
||||
|
||||
if (matchingStatus) {
|
||||
targetStatuses.push(matchingStatus.name);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Gets all statuses from enabled templates
|
||||
*/
|
||||
public getTemplateStatuses(): Status[] {
|
||||
return this.settings.enabledTemplates
|
||||
.map((id) => PREDEFINED_TEMPLATES.find((t) => t.id === id))
|
||||
.filter(Boolean)
|
||||
.flatMap((template) => (template ? template.statuses : []));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the icon for a given status
|
||||
*/
|
||||
public getStatusIcon(status: string): string {
|
||||
const customStatus = this.allStatuses.find(
|
||||
s => s.name.toLowerCase() === status.toLowerCase()
|
||||
);
|
||||
return customStatus ? customStatus.icon : '❓';
|
||||
}
|
||||
/**
|
||||
* Get all available statuses (combined from templates and custom)
|
||||
*/
|
||||
public getAllStatuses(): Status[] {
|
||||
return this.allStatuses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert status metadata in the editor
|
||||
*/
|
||||
public insertStatusMetadataInEditor(editor: Editor): void {
|
||||
const content = editor.getValue();
|
||||
const frontMatterMatch = content.match(/^---\n([\s\S]+?)\n---/);
|
||||
|
||||
// Check if status metadata already exists
|
||||
const statusTagRegex = new RegExp(`${this.settings.tagPrefix}:\\s*\\[?[^\\]]*\\]?`, 'm');
|
||||
|
||||
if (frontMatterMatch) {
|
||||
const frontMatter = frontMatterMatch[1];
|
||||
if (frontMatter.match(statusTagRegex)) {
|
||||
// Status already exists, do nothing
|
||||
return;
|
||||
}
|
||||
// Add to existing frontmatter
|
||||
const defaultStatuses = ['unknown'];
|
||||
const statusMetadata = `${this.settings.tagPrefix}: ${JSON.stringify(defaultStatuses)}`;
|
||||
this.insertIntoExistingFrontmatter(editor, content, frontMatterMatch, statusMetadata);
|
||||
} else {
|
||||
// No frontmatter, check if status exists in content somehow
|
||||
if (content.match(statusTagRegex)) {
|
||||
return;
|
||||
}
|
||||
// Create new frontmatter
|
||||
const defaultStatuses = ['unknown'];
|
||||
const statusMetadata = `${this.settings.tagPrefix}: ${JSON.stringify(defaultStatuses)}`;
|
||||
this.createFrontmatterWithStatus(editor, content, statusMetadata);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert status metadata into existing frontmatter
|
||||
*/
|
||||
private insertIntoExistingFrontmatter(
|
||||
editor: Editor,
|
||||
content: string,
|
||||
frontMatterMatch: RegExpMatchArray,
|
||||
statusMetadata: string
|
||||
): void {
|
||||
const frontMatter = frontMatterMatch[1];
|
||||
const statusTagRegex = new RegExp(`${this.settings.tagPrefix}:\\s*\\[?[^\\]]*\\]?`, 'm');
|
||||
|
||||
const updatedFrontMatter = frontMatter.match(statusTagRegex)
|
||||
? frontMatter.replace(statusTagRegex, statusMetadata)
|
||||
: `${frontMatter}\n${statusMetadata}`;
|
||||
|
||||
const updatedContent = content.replace(/^---\n([\s\S]+?)\n---/, `---\n${updatedFrontMatter}\n---`);
|
||||
editor.setValue(updatedContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new frontmatter with status metadata
|
||||
*/
|
||||
private createFrontmatterWithStatus(editor: Editor, content: string, statusMetadata: string): void {
|
||||
const newFrontMatter = `---\n${statusMetadata}\n---\n\n${content.trim()}`;
|
||||
editor.setValue(newFrontMatter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all markdown files with optional filtering
|
||||
*/
|
||||
public getMarkdownFiles(searchQuery = ''): TFile[] {
|
||||
const files = this.app.vault.getMarkdownFiles();
|
||||
if (!searchQuery) return files;
|
||||
|
||||
const lowerQuery = searchQuery.toLowerCase();
|
||||
return files.filter(file =>
|
||||
file.basename.toLowerCase().includes(lowerQuery));
|
||||
}
|
||||
|
||||
/**
|
||||
* Group files by their status
|
||||
*/
|
||||
public groupFilesByStatus(searchQuery = ''): Record<string, TFile[]> {
|
||||
const statusGroups: Record<string, TFile[]> = {};
|
||||
|
||||
// Initialize groups for all statuses
|
||||
for (const status of this.allStatuses) {
|
||||
statusGroups[status.name] = [];
|
||||
}
|
||||
|
||||
// Ensure 'unknown' status exists
|
||||
statusGroups['unknown'] = statusGroups['unknown'] || [];
|
||||
|
||||
// Get and process all files matching the search query
|
||||
const files = this.getMarkdownFiles(searchQuery);
|
||||
for (const file of files) {
|
||||
const statuses = this.getFileStatuses(file);
|
||||
|
||||
for (const status of statuses) {
|
||||
if (statusGroups[status]) {
|
||||
statusGroups[status].push(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return statusGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Centralizes all status modification operations
|
||||
*/
|
||||
private async modifyNoteStatus(options: {
|
||||
files: TFile | TFile[];
|
||||
statuses: string | string[];
|
||||
operation: 'set' | 'add' | 'remove' | 'toggle';
|
||||
showNotice?: boolean;
|
||||
}): Promise<void> {
|
||||
const { operation, showNotice = true } = options;
|
||||
const targetFiles = Array.isArray(options.files) ? options.files : [options.files];
|
||||
const targetStatuses = Array.isArray(options.statuses) ? options.statuses : [options.statuses];
|
||||
|
||||
if (targetFiles.length === 0) {
|
||||
if (showNotice) new Notice('No files selected');
|
||||
return;
|
||||
}
|
||||
|
||||
// Process each file
|
||||
const updatePromises = targetFiles.map(async (file) => {
|
||||
if (!file || file.extension !== 'md') return;
|
||||
|
||||
// Get current statuses for the file
|
||||
const currentStatuses = this.getFileStatuses(file);
|
||||
let newStatuses: string[] = [];
|
||||
|
||||
switch (operation) {
|
||||
case 'set':
|
||||
// Replace all statuses with the new ones
|
||||
newStatuses = [...targetStatuses];
|
||||
break;
|
||||
|
||||
case 'add':
|
||||
// Add new statuses without duplicates
|
||||
newStatuses = [...new Set([
|
||||
...currentStatuses.filter(s => s !== 'unknown'),
|
||||
...targetStatuses
|
||||
])];
|
||||
break;
|
||||
|
||||
case 'remove':
|
||||
// Remove specified statuses
|
||||
newStatuses = currentStatuses.filter(
|
||||
status => !targetStatuses.includes(status)
|
||||
);
|
||||
break;
|
||||
|
||||
case 'toggle':
|
||||
// Toggle each status (add if not present, remove if present)
|
||||
newStatuses = [...currentStatuses];
|
||||
for (const status of targetStatuses) {
|
||||
if (currentStatuses.includes(status)) {
|
||||
newStatuses = newStatuses.filter(s => s !== status);
|
||||
} else {
|
||||
newStatuses = [...newStatuses.filter(s => s !== 'unknown'), status];
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Handle empty result (should revert to unknown)
|
||||
if (newStatuses.length === 0) {
|
||||
newStatuses = ['unknown'];
|
||||
}
|
||||
|
||||
// Apply updates to frontmatter
|
||||
await this.app.fileManager.processFrontMatter(file, (frontmatter) => {
|
||||
frontmatter[this.settings.tagPrefix] = newStatuses;
|
||||
});
|
||||
|
||||
|
||||
this.notifyStatusChanged(newStatuses, file)
|
||||
});
|
||||
|
||||
await Promise.all(updatePromises);
|
||||
|
||||
// Show notification for batch operations
|
||||
if (showNotice && targetFiles.length > 1) {
|
||||
const statusText = targetStatuses.length === 1
|
||||
? targetStatuses[0]
|
||||
: `${targetStatuses.length} statuses`;
|
||||
const operationText = operation === 'set' ? 'updated' :
|
||||
operation === 'add' ? 'added to' :
|
||||
operation === 'remove' ? 'removed from' : 'toggled on';
|
||||
|
||||
new Notice(`${statusText} ${operationText} ${targetFiles.length} files`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles UI-triggered status changes with appropriate logic based on context
|
||||
* This is the central function that all UI components should call for status changes
|
||||
*/
|
||||
public async handleStatusChange(options: {
|
||||
files: TFile | TFile[];
|
||||
statuses: string | string[];
|
||||
isMultipleSelection?: boolean;
|
||||
allowMultipleStatuses?: boolean;
|
||||
operation?: 'set' | 'add' | 'remove' | 'toggle';
|
||||
showNotice?: boolean;
|
||||
afterChange?: (updatedStatuses: string[]) => void;
|
||||
}): Promise<void> {
|
||||
const {
|
||||
files,
|
||||
statuses,
|
||||
isMultipleSelection = false,
|
||||
allowMultipleStatuses = this.settings.useMultipleStatuses,
|
||||
operation: explicitOperation,
|
||||
showNotice = isMultipleSelection,
|
||||
afterChange
|
||||
} = options;
|
||||
|
||||
const targetFiles = Array.isArray(files) ? files : [files];
|
||||
const targetStatuses = Array.isArray(statuses) ? statuses : [statuses];
|
||||
|
||||
// Determine operation based on context if not explicitly specified
|
||||
let operation: 'set' | 'add' | 'remove' | 'toggle';
|
||||
|
||||
if (explicitOperation) {
|
||||
operation = explicitOperation;
|
||||
} else if (isMultipleSelection) {
|
||||
// For multiple files, we need to check if we should add or remove
|
||||
const firstStatus = targetStatuses[0]; // Use first status for multi-file operations
|
||||
const filesWithStatus = targetFiles.filter(file =>
|
||||
this.getFileStatuses(file).includes(firstStatus)
|
||||
);
|
||||
|
||||
operation = filesWithStatus.length > targetFiles.length / 2 ? 'remove' : 'add';
|
||||
} else {
|
||||
// For single file operations
|
||||
if (allowMultipleStatuses) {
|
||||
operation = 'toggle';
|
||||
} else {
|
||||
operation = 'set';
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the changes
|
||||
await this.modifyNoteStatus({
|
||||
files: targetFiles,
|
||||
statuses: targetStatuses,
|
||||
operation,
|
||||
showNotice
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch status changed event
|
||||
*/
|
||||
private notifyStatusChanged(statuses: string[], file?: TFile): void {
|
||||
// Dispatch the specific status change event
|
||||
window.dispatchEvent(new CustomEvent('note-status:status-changed', {
|
||||
detail: {
|
||||
statuses,
|
||||
file: file?.path
|
||||
}
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Get the statuses of a file from its metadata
|
||||
*/
|
||||
public getFileStatuses(file: TFile): string[] {
|
||||
const cachedMetadata = this.app.metadataCache.getFileCache(file);
|
||||
if (!cachedMetadata?.frontmatter) return ["unknown"];
|
||||
|
||||
const frontmatterStatus =
|
||||
cachedMetadata.frontmatter[this.settings.tagPrefix];
|
||||
if (!frontmatterStatus) return ["unknown"];
|
||||
|
||||
const statuses: string[] = [];
|
||||
|
||||
if (Array.isArray(frontmatterStatus)) {
|
||||
for (const statusName of frontmatterStatus) {
|
||||
if (this.settings.strictStatuses) {
|
||||
this.addValidStatus(statusName.toString(), statuses);
|
||||
} else {
|
||||
const cleanStatus = statusName.toString().trim();
|
||||
if (cleanStatus) statuses.push(cleanStatus);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.settings.strictStatuses) {
|
||||
this.addValidStatus(frontmatterStatus.toString(), statuses);
|
||||
} else {
|
||||
const cleanStatus = frontmatterStatus.toString().trim();
|
||||
if (cleanStatus) statuses.push(cleanStatus);
|
||||
}
|
||||
}
|
||||
|
||||
return statuses.length > 0 ? statuses : ["unknown"];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a status to the list if it's valid
|
||||
*/
|
||||
private addValidStatus(statusName: string, targetStatuses: string[]): void {
|
||||
const normalizedStatus = statusName.toLowerCase();
|
||||
const matchingStatus = this.allStatuses.find(
|
||||
(s) => s.name.toLowerCase() === normalizedStatus,
|
||||
);
|
||||
|
||||
if (matchingStatus) {
|
||||
targetStatuses.push(matchingStatus.name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the icon for a given status
|
||||
*/
|
||||
public getStatusIcon(status: string): string {
|
||||
const customStatus = this.allStatuses.find(
|
||||
(s) => s.name.toLowerCase() === status.toLowerCase(),
|
||||
);
|
||||
return customStatus ? customStatus.icon : "❓";
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert status metadata in the editor
|
||||
*/
|
||||
public insertStatusMetadataInEditor(editor: Editor): void {
|
||||
const content = editor.getValue();
|
||||
const frontMatterMatch = content.match(/^---\n([\s\S]+?)\n---/);
|
||||
|
||||
// Check if status metadata already exists
|
||||
const statusTagRegex = new RegExp(
|
||||
`${this.settings.tagPrefix}:\\s*\\[?[^\\]]*\\]?`,
|
||||
"m",
|
||||
);
|
||||
|
||||
if (frontMatterMatch) {
|
||||
const frontMatter = frontMatterMatch[1];
|
||||
if (frontMatter.match(statusTagRegex)) {
|
||||
// Status already exists, do nothing
|
||||
return;
|
||||
}
|
||||
// Add to existing frontmatter
|
||||
const defaultStatuses = ["unknown"];
|
||||
const statusMetadata = `${this.settings.tagPrefix}: ${JSON.stringify(defaultStatuses)}`;
|
||||
this.insertIntoExistingFrontmatter(
|
||||
editor,
|
||||
content,
|
||||
frontMatterMatch,
|
||||
statusMetadata,
|
||||
);
|
||||
} else {
|
||||
// No frontmatter, check if status exists in content somehow
|
||||
if (content.match(statusTagRegex)) {
|
||||
return;
|
||||
}
|
||||
// Create new frontmatter
|
||||
const defaultStatuses = ["unknown"];
|
||||
const statusMetadata = `${this.settings.tagPrefix}: ${JSON.stringify(defaultStatuses)}`;
|
||||
this.createFrontmatterWithStatus(editor, content, statusMetadata);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert status metadata into existing frontmatter
|
||||
*/
|
||||
private insertIntoExistingFrontmatter(
|
||||
editor: Editor,
|
||||
content: string,
|
||||
frontMatterMatch: RegExpMatchArray,
|
||||
statusMetadata: string,
|
||||
): void {
|
||||
const frontMatter = frontMatterMatch[1];
|
||||
const statusTagRegex = new RegExp(
|
||||
`${this.settings.tagPrefix}:\\s*\\[?[^\\]]*\\]?`,
|
||||
"m",
|
||||
);
|
||||
|
||||
const updatedFrontMatter = frontMatter.match(statusTagRegex)
|
||||
? frontMatter.replace(statusTagRegex, statusMetadata)
|
||||
: `${frontMatter}\n${statusMetadata}`;
|
||||
|
||||
const updatedContent = content.replace(
|
||||
/^---\n([\s\S]+?)\n---/,
|
||||
`---\n${updatedFrontMatter}\n---`,
|
||||
);
|
||||
editor.setValue(updatedContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new frontmatter with status metadata
|
||||
*/
|
||||
private createFrontmatterWithStatus(
|
||||
editor: Editor,
|
||||
content: string,
|
||||
statusMetadata: string,
|
||||
): void {
|
||||
const newFrontMatter = `---\n${statusMetadata}\n---\n\n${content.trim()}`;
|
||||
editor.setValue(newFrontMatter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all markdown files with optional filtering
|
||||
*/
|
||||
public getMarkdownFiles(searchQuery = ""): TFile[] {
|
||||
const files = this.app.vault.getMarkdownFiles();
|
||||
if (!searchQuery) return files;
|
||||
|
||||
const lowerQuery = searchQuery.toLowerCase();
|
||||
return files.filter((file) =>
|
||||
file.basename.toLowerCase().includes(lowerQuery),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Group files by their status
|
||||
*/
|
||||
public groupFilesByStatus(searchQuery = ""): Record<string, TFile[]> {
|
||||
const statusGroups: Record<string, TFile[]> = {};
|
||||
|
||||
// Initialize groups for all statuses
|
||||
for (const status of this.allStatuses) {
|
||||
statusGroups[status.name] = [];
|
||||
}
|
||||
|
||||
// Ensure 'unknown' status exists
|
||||
statusGroups["unknown"] = statusGroups["unknown"] || [];
|
||||
|
||||
// Get and process all files matching the search query
|
||||
const files = this.getMarkdownFiles(searchQuery);
|
||||
for (const file of files) {
|
||||
const statuses = this.getFileStatuses(file);
|
||||
|
||||
for (const status of statuses) {
|
||||
if (statusGroups[status]) {
|
||||
statusGroups[status].push(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return statusGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Centralizes all status modification operations
|
||||
*/
|
||||
private async modifyNoteStatus(options: {
|
||||
files: TFile | TFile[];
|
||||
statuses: string | string[];
|
||||
operation: "set" | "add" | "remove" | "toggle";
|
||||
showNotice?: boolean;
|
||||
}): Promise<void> {
|
||||
const { operation, showNotice = true } = options;
|
||||
const targetFiles = Array.isArray(options.files)
|
||||
? options.files
|
||||
: [options.files];
|
||||
const targetStatuses = Array.isArray(options.statuses)
|
||||
? options.statuses
|
||||
: [options.statuses];
|
||||
|
||||
if (targetFiles.length === 0) {
|
||||
if (showNotice) new Notice("No files selected");
|
||||
return;
|
||||
}
|
||||
|
||||
// Process each file
|
||||
const updatePromises = targetFiles.map(async (file) => {
|
||||
if (!file || file.extension !== "md") return;
|
||||
|
||||
// Get current statuses for the file
|
||||
const currentStatuses = this.getFileStatuses(file);
|
||||
let newStatuses: string[] = [];
|
||||
|
||||
switch (operation) {
|
||||
case "set":
|
||||
// Replace all statuses with the new ones
|
||||
newStatuses = [...targetStatuses];
|
||||
break;
|
||||
|
||||
case "add":
|
||||
// Add new statuses without duplicates
|
||||
newStatuses = [
|
||||
...new Set([
|
||||
...currentStatuses.filter((s) => s !== "unknown"),
|
||||
...targetStatuses,
|
||||
]),
|
||||
];
|
||||
break;
|
||||
|
||||
case "remove":
|
||||
// Remove specified statuses
|
||||
newStatuses = currentStatuses.filter(
|
||||
(status) => !targetStatuses.includes(status),
|
||||
);
|
||||
break;
|
||||
|
||||
case "toggle":
|
||||
// Toggle each status (add if not present, remove if present)
|
||||
newStatuses = [...currentStatuses];
|
||||
for (const status of targetStatuses) {
|
||||
if (currentStatuses.includes(status)) {
|
||||
newStatuses = newStatuses.filter(
|
||||
(s) => s !== status,
|
||||
);
|
||||
} else {
|
||||
newStatuses = [
|
||||
...newStatuses.filter((s) => s !== "unknown"),
|
||||
status,
|
||||
];
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Handle empty result (should revert to unknown)
|
||||
if (newStatuses.length === 0) {
|
||||
newStatuses = ["unknown"];
|
||||
}
|
||||
|
||||
// Apply updates to frontmatter
|
||||
await this.app.fileManager.processFrontMatter(
|
||||
file,
|
||||
(frontmatter) => {
|
||||
frontmatter[this.settings.tagPrefix] = newStatuses;
|
||||
},
|
||||
);
|
||||
|
||||
this.notifyStatusChanged(newStatuses, file);
|
||||
});
|
||||
|
||||
await Promise.all(updatePromises);
|
||||
|
||||
// Show notification for batch operations
|
||||
if (showNotice && targetFiles.length > 1) {
|
||||
const statusText =
|
||||
targetStatuses.length === 1
|
||||
? targetStatuses[0]
|
||||
: `${targetStatuses.length} statuses`;
|
||||
const operationText =
|
||||
operation === "set"
|
||||
? "updated"
|
||||
: operation === "add"
|
||||
? "added to"
|
||||
: operation === "remove"
|
||||
? "removed from"
|
||||
: "toggled on";
|
||||
|
||||
new Notice(
|
||||
`${statusText} ${operationText} ${targetFiles.length} files`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles UI-triggered status changes with appropriate logic based on context
|
||||
* This is the central function that all UI components should call for status changes
|
||||
*/
|
||||
public async handleStatusChange(options: {
|
||||
files: TFile | TFile[];
|
||||
statuses: string | string[];
|
||||
isMultipleSelection?: boolean;
|
||||
allowMultipleStatuses?: boolean;
|
||||
operation?: "set" | "add" | "remove" | "toggle";
|
||||
showNotice?: boolean;
|
||||
afterChange?: (updatedStatuses: string[]) => void;
|
||||
}): Promise<void> {
|
||||
const {
|
||||
files,
|
||||
statuses,
|
||||
isMultipleSelection = false,
|
||||
allowMultipleStatuses = this.settings.useMultipleStatuses,
|
||||
operation: explicitOperation,
|
||||
showNotice = isMultipleSelection,
|
||||
} = options;
|
||||
|
||||
const targetFiles = Array.isArray(files) ? files : [files];
|
||||
const targetStatuses = Array.isArray(statuses) ? statuses : [statuses];
|
||||
|
||||
// Determine operation based on context if not explicitly specified
|
||||
let operation: "set" | "add" | "remove" | "toggle";
|
||||
|
||||
if (explicitOperation) {
|
||||
operation = explicitOperation;
|
||||
} else if (isMultipleSelection) {
|
||||
// For multiple files, we need to check if we should add or remove
|
||||
const firstStatus = targetStatuses[0]; // Use first status for multi-file operations
|
||||
const filesWithStatus = targetFiles.filter((file) =>
|
||||
this.getFileStatuses(file).includes(firstStatus),
|
||||
);
|
||||
|
||||
operation =
|
||||
filesWithStatus.length > targetFiles.length / 2
|
||||
? "remove"
|
||||
: "add";
|
||||
} else {
|
||||
// For single file operations
|
||||
if (allowMultipleStatuses) {
|
||||
operation = "toggle";
|
||||
} else {
|
||||
operation = "set";
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the changes
|
||||
await this.modifyNoteStatus({
|
||||
files: targetFiles,
|
||||
statuses: targetStatuses,
|
||||
operation,
|
||||
showNotice,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch status changed event
|
||||
*/
|
||||
private notifyStatusChanged(statuses: string[], file?: TFile): void {
|
||||
// Dispatch the specific status change event
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("note-status:status-changed", {
|
||||
detail: {
|
||||
statuses,
|
||||
file: file?.path,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,30 @@
|
|||
import { TFile, WorkspaceLeaf, View, Notice, App } from 'obsidian';
|
||||
import { NoteStatusSettings } from '../../models/types';
|
||||
import { StatusService } from '../../services/status-service';
|
||||
import NoteStatus from 'main';
|
||||
import { StatusPaneView } from './status-pane-view';
|
||||
import { TFile, WorkspaceLeaf, View, Notice, App } from "obsidian";
|
||||
import { NoteStatusSettings } from "../../models/types";
|
||||
import { StatusService } from "../../services/status-service";
|
||||
import NoteStatus from "main";
|
||||
import { StatusPaneView } from "./status-pane-view";
|
||||
|
||||
export class StatusPaneViewController extends View {
|
||||
private renderer: StatusPaneView;
|
||||
private settings: NoteStatusSettings;
|
||||
private statusService: StatusService;
|
||||
private plugin: NoteStatus;
|
||||
private searchQuery = '';
|
||||
private searchQuery = "";
|
||||
private paginationState = {
|
||||
itemsPerPage: 100,
|
||||
currentPage: {} as Record<string, number>
|
||||
currentPage: {} as Record<string, number>,
|
||||
};
|
||||
|
||||
static async open(app: App): Promise<void> {
|
||||
const existing = app.workspace.getLeavesOfType('status-pane')[0];
|
||||
const existing = app.workspace.getLeavesOfType("status-pane")[0];
|
||||
if (existing) {
|
||||
app.workspace.setActiveLeaf(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const leaf = app.workspace.getLeftLeaf(false);
|
||||
if (leaf) {
|
||||
await leaf.setViewState({ type: 'status-pane', active: true });
|
||||
await leaf.setViewState({ type: "status-pane", active: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -36,11 +36,17 @@ export class StatusPaneViewController extends View {
|
|||
this.renderer = new StatusPaneView(this.statusService);
|
||||
}
|
||||
|
||||
getViewType(): string { return 'status-pane'; }
|
||||
getViewType(): string {
|
||||
return "status-pane";
|
||||
}
|
||||
|
||||
getDisplayText(): string { return 'Status pane'; }
|
||||
getDisplayText(): string {
|
||||
return "Status pane";
|
||||
}
|
||||
|
||||
getIcon(): string { return 'tag'; }
|
||||
getIcon(): string {
|
||||
return "tag";
|
||||
}
|
||||
|
||||
async onOpen(): Promise<void> {
|
||||
await this.setupPane();
|
||||
|
|
@ -51,51 +57,66 @@ export class StatusPaneViewController extends View {
|
|||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
||||
private async setupPane(): Promise<void> {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
containerEl.addClass('note-status-pane', 'nav-files-container');
|
||||
containerEl.toggleClass('note-status-compact-view', this.settings.compactView);
|
||||
containerEl.addClass("note-status-pane", "nav-files-container");
|
||||
containerEl.toggleClass(
|
||||
"note-status-compact-view",
|
||||
this.settings.compactView,
|
||||
);
|
||||
|
||||
this.renderer.createHeader(containerEl, this.settings.compactView, {
|
||||
onSearch: (query) => {
|
||||
this.paginationState = {
|
||||
itemsPerPage: 100,
|
||||
currentPage: {} as Record<string, number>
|
||||
}
|
||||
currentPage: {} as Record<string, number>,
|
||||
};
|
||||
this.searchQuery = query;
|
||||
this.renderGroups(query);
|
||||
},
|
||||
onToggleView: () => {
|
||||
this.settings.compactView = !this.settings.compactView;
|
||||
containerEl.toggleClass('note-status-compact-view', this.settings.compactView);
|
||||
window.dispatchEvent(new CustomEvent('note-status:settings-changed'));
|
||||
containerEl.toggleClass(
|
||||
"note-status-compact-view",
|
||||
this.settings.compactView,
|
||||
);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("note-status:settings-changed"),
|
||||
);
|
||||
this.renderGroups(this.searchQuery);
|
||||
},
|
||||
onRefresh: async () => {
|
||||
await this.renderGroups(this.searchQuery);
|
||||
new Notice('Status pane refreshed');
|
||||
}
|
||||
new Notice("Status pane refreshed");
|
||||
},
|
||||
});
|
||||
|
||||
const groupsContainer = containerEl.createDiv({ cls: 'note-status-groups-container' });
|
||||
const loadingIndicator = this.renderer.createLoadingIndicator(groupsContainer);
|
||||
const groupsContainer = containerEl.createDiv({
|
||||
cls: "note-status-groups-container",
|
||||
});
|
||||
const loadingIndicator =
|
||||
this.renderer.createLoadingIndicator(groupsContainer);
|
||||
|
||||
setTimeout(async () => {
|
||||
await this.renderGroups('');
|
||||
await this.renderGroups("");
|
||||
loadingIndicator.remove();
|
||||
}, 10);
|
||||
}
|
||||
|
||||
private async renderGroups(searchQuery = ''): Promise<void> {
|
||||
const groupsContainerEl = this.containerEl.querySelector('.note-status-groups-container') as HTMLElement;
|
||||
private async renderGroups(searchQuery = ""): Promise<void> {
|
||||
const groupsContainerEl = this.containerEl.querySelector(
|
||||
".note-status-groups-container",
|
||||
) as HTMLElement;
|
||||
if (!groupsContainerEl) return;
|
||||
|
||||
if (searchQuery) {
|
||||
groupsContainerEl.empty();
|
||||
this.renderer.createLoadingIndicator(groupsContainerEl, `Searching for "${searchQuery}"...`);
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
this.renderer.createLoadingIndicator(
|
||||
groupsContainerEl,
|
||||
`Searching for "${searchQuery}"...`,
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
} else {
|
||||
groupsContainerEl.empty();
|
||||
}
|
||||
|
|
@ -113,19 +134,22 @@ export class StatusPaneViewController extends View {
|
|||
pagination: this.paginationState,
|
||||
callbacks: {
|
||||
onFileClick: (file) => {
|
||||
this.app.workspace.openLinkText(file.path, file.path, true);
|
||||
this.app.workspace.openLinkText(
|
||||
file.path,
|
||||
file.path,
|
||||
true,
|
||||
);
|
||||
},
|
||||
onStatusToggle: (status, collapsed) => {
|
||||
this.settings.collapsedStatuses[status] = collapsed;
|
||||
},
|
||||
onContextMenu: (e, file) => {
|
||||
},
|
||||
onContextMenu: () => {},
|
||||
onPageChange: (status, page) => {
|
||||
this.paginationState.currentPage[status] = page;
|
||||
this.renderGroups(this.searchQuery);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!hasGroups) {
|
||||
|
|
@ -137,12 +161,12 @@ export class StatusPaneViewController extends View {
|
|||
this.settings.excludeUnknownStatus = false;
|
||||
await this.plugin.saveSettings();
|
||||
this.renderGroups(searchQuery);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private getFilteredStatusGroups(searchQuery = ''): Record<string, TFile[]> {
|
||||
private getFilteredStatusGroups(searchQuery = ""): Record<string, TFile[]> {
|
||||
const rawGroups = this.statusService.groupFilesByStatus(searchQuery);
|
||||
const filteredGroups: Record<string, TFile[]> = {};
|
||||
|
||||
|
|
@ -157,12 +181,15 @@ export class StatusPaneViewController extends View {
|
|||
|
||||
updateSettings(settings: NoteStatusSettings): void {
|
||||
this.settings = settings;
|
||||
this.containerEl.toggleClass('note-status-compact-view', settings.compactView);
|
||||
this.containerEl.toggleClass(
|
||||
"note-status-compact-view",
|
||||
settings.compactView,
|
||||
);
|
||||
this.renderGroups(this.searchQuery);
|
||||
}
|
||||
|
||||
public update(): void {
|
||||
// TODO: needs to be improved/fixed, every change
|
||||
// TODO: needs to be improved/fixed, every change
|
||||
// this.renderGroups(this.searchQuery).then(() => {
|
||||
//
|
||||
// });
|
||||
|
|
|
|||
|
|
@ -1,87 +1,111 @@
|
|||
import { TFile, setIcon } from 'obsidian';
|
||||
import { StatusService } from '../../services/status-service';
|
||||
import { TFile, setIcon } from "obsidian";
|
||||
import { StatusService } from "../../services/status-service";
|
||||
|
||||
export class StatusPaneView{
|
||||
export type Options = {
|
||||
excludeUnknown: boolean;
|
||||
isCompactView: boolean;
|
||||
collapsedStatuses: Record<string, boolean>;
|
||||
pagination: {
|
||||
itemsPerPage: number;
|
||||
currentPage: Record<string, number>;
|
||||
};
|
||||
callbacks: {
|
||||
onFileClick: (file: TFile) => void;
|
||||
onStatusToggle: (status: string, collapsed: boolean) => void;
|
||||
onContextMenu: (e: MouseEvent, file: TFile) => void;
|
||||
onPageChange: (status: string, page: number) => void;
|
||||
};
|
||||
};
|
||||
export class StatusPaneView {
|
||||
constructor(private statusService: StatusService) {}
|
||||
|
||||
createHeader(container: HTMLElement, isCompactView: boolean, callbacks: {
|
||||
onSearch: (query: string) => void,
|
||||
onToggleView: () => void,
|
||||
onRefresh: () => void
|
||||
}): HTMLElement {
|
||||
const header = container.createDiv({ cls: 'note-status-header' });
|
||||
createHeader(
|
||||
container: HTMLElement,
|
||||
isCompactView: boolean,
|
||||
callbacks: {
|
||||
onSearch: (query: string) => void;
|
||||
onToggleView: () => void;
|
||||
onRefresh: () => void;
|
||||
},
|
||||
): HTMLElement {
|
||||
const header = container.createDiv({ cls: "note-status-header" });
|
||||
|
||||
// Search input
|
||||
const searchContainer = header.createDiv({ cls: 'note-status-search search-input-container' });
|
||||
const searchWrapper = searchContainer.createDiv({ cls: 'search-input-wrapper' });
|
||||
|
||||
const searchIcon = searchWrapper.createEl('span', { cls: 'search-input-icon' });
|
||||
setIcon(searchIcon, 'search');
|
||||
|
||||
const searchInput = searchWrapper.createEl('input', {
|
||||
type: 'text',
|
||||
placeholder: 'Search notes...',
|
||||
cls: 'note-status-search-input search-input'
|
||||
const searchContainer = header.createDiv({
|
||||
cls: "note-status-search search-input-container",
|
||||
});
|
||||
const searchWrapper = searchContainer.createDiv({
|
||||
cls: "search-input-wrapper",
|
||||
});
|
||||
|
||||
const clearBtn = searchWrapper.createEl('span', { cls: 'search-input-clear-button' });
|
||||
setIcon(clearBtn, 'x');
|
||||
const searchIcon = searchWrapper.createEl("span", {
|
||||
cls: "search-input-icon",
|
||||
});
|
||||
setIcon(searchIcon, "search");
|
||||
|
||||
searchInput.addEventListener('input', () => {
|
||||
const searchInput = searchWrapper.createEl("input", {
|
||||
type: "text",
|
||||
placeholder: "Search notes...",
|
||||
cls: "note-status-search-input search-input",
|
||||
});
|
||||
|
||||
const clearBtn = searchWrapper.createEl("span", {
|
||||
cls: "search-input-clear-button",
|
||||
});
|
||||
setIcon(clearBtn, "x");
|
||||
|
||||
searchInput.addEventListener("input", () => {
|
||||
callbacks.onSearch(searchInput.value.toLowerCase());
|
||||
clearBtn.toggleClass('is-visible', !!searchInput.value);
|
||||
clearBtn.toggleClass("is-visible", !!searchInput.value);
|
||||
});
|
||||
|
||||
clearBtn.addEventListener('click', () => {
|
||||
searchInput.value = '';
|
||||
callbacks.onSearch('');
|
||||
clearBtn.toggleClass('is-visible', false);
|
||||
clearBtn.addEventListener("click", () => {
|
||||
searchInput.value = "";
|
||||
callbacks.onSearch("");
|
||||
clearBtn.toggleClass("is-visible", false);
|
||||
});
|
||||
|
||||
// Action buttons
|
||||
const actions = header.createDiv({ cls: 'status-pane-actions-container' });
|
||||
|
||||
const viewBtn = actions.createEl('button', {
|
||||
type: 'button',
|
||||
title: isCompactView ? 'Switch to Standard View' : 'Switch to Compact View',
|
||||
cls: 'note-status-view-toggle clickable-icon'
|
||||
const actions = header.createDiv({
|
||||
cls: "status-pane-actions-container",
|
||||
});
|
||||
setIcon(viewBtn, isCompactView ? 'layout' : 'table');
|
||||
viewBtn.addEventListener('click', callbacks.onToggleView);
|
||||
|
||||
const refreshBtn = actions.createEl('button', {
|
||||
type: 'button',
|
||||
title: 'Refresh statuses',
|
||||
cls: 'note-status-actions-refresh clickable-icon'
|
||||
const viewBtn = actions.createEl("button", {
|
||||
type: "button",
|
||||
title: isCompactView
|
||||
? "Switch to Standard View"
|
||||
: "Switch to Compact View",
|
||||
cls: "note-status-view-toggle clickable-icon",
|
||||
});
|
||||
setIcon(refreshBtn, 'refresh-cw');
|
||||
refreshBtn.addEventListener('click', callbacks.onRefresh);
|
||||
setIcon(viewBtn, isCompactView ? "layout" : "table");
|
||||
viewBtn.addEventListener("click", callbacks.onToggleView);
|
||||
|
||||
const refreshBtn = actions.createEl("button", {
|
||||
type: "button",
|
||||
title: "Refresh statuses",
|
||||
cls: "note-status-actions-refresh clickable-icon",
|
||||
});
|
||||
setIcon(refreshBtn, "refresh-cw");
|
||||
refreshBtn.addEventListener("click", callbacks.onRefresh);
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
renderStatusGroups(container: HTMLElement, statusGroups: Record<string, TFile[]>, options: {
|
||||
excludeUnknown: boolean,
|
||||
isCompactView: boolean,
|
||||
collapsedStatuses: Record<string, boolean>,
|
||||
pagination: {
|
||||
itemsPerPage: number,
|
||||
currentPage: Record<string, number>
|
||||
},
|
||||
callbacks: {
|
||||
onFileClick: (file: TFile) => void,
|
||||
onStatusToggle: (status: string, collapsed: boolean) => void,
|
||||
onContextMenu: (e: MouseEvent, file: TFile) => void,
|
||||
onPageChange: (status: string, page: number) => void
|
||||
}
|
||||
}): boolean {
|
||||
renderStatusGroups(
|
||||
container: HTMLElement,
|
||||
statusGroups: Record<string, TFile[]>,
|
||||
options: Options,
|
||||
): boolean {
|
||||
// Clear container first
|
||||
container.empty();
|
||||
|
||||
let hasGroups = false;
|
||||
|
||||
Object.entries(statusGroups).forEach(([status, files]) => {
|
||||
if (files.length > 0 && !(status === 'unknown' && options.excludeUnknown)) {
|
||||
if (
|
||||
files.length > 0 &&
|
||||
!(status === "unknown" && options.excludeUnknown)
|
||||
) {
|
||||
this.renderGroup(container, status, files, options);
|
||||
hasGroups = true;
|
||||
}
|
||||
|
|
@ -94,40 +118,54 @@ export class StatusPaneView{
|
|||
return true;
|
||||
}
|
||||
|
||||
private renderGroup(container: HTMLElement, status: string, files: TFile[], options: any): void {
|
||||
const groupEl = container.createDiv({ cls: 'note-status-group nav-folder' });
|
||||
const titleEl = groupEl.createDiv({ cls: 'nav-folder-title' });
|
||||
private renderGroup(
|
||||
container: HTMLElement,
|
||||
status: string,
|
||||
files: TFile[],
|
||||
options: Options,
|
||||
): void {
|
||||
const groupEl = container.createDiv({
|
||||
cls: "note-status-group nav-folder",
|
||||
});
|
||||
const titleEl = groupEl.createDiv({ cls: "nav-folder-title" });
|
||||
const isCollapsed = options.collapsedStatuses[status] ?? false;
|
||||
|
||||
// Collapse indicator
|
||||
const collapseIcon = titleEl.createDiv({ cls: 'note-status-collapse-indicator' });
|
||||
setIcon(collapseIcon, isCollapsed ? 'chevron-right' : 'chevron-down');
|
||||
const collapseIcon = titleEl.createDiv({
|
||||
cls: "note-status-collapse-indicator",
|
||||
});
|
||||
setIcon(collapseIcon, isCollapsed ? "chevron-right" : "chevron-down");
|
||||
|
||||
// Title content
|
||||
const titleContent = titleEl.createDiv({ cls: 'nav-folder-title-content' });
|
||||
const titleContent = titleEl.createDiv({
|
||||
cls: "nav-folder-title-content",
|
||||
});
|
||||
const statusIcon = this.statusService.getStatusIcon(status);
|
||||
titleContent.createSpan({
|
||||
text: `${status} ${statusIcon} (${files.length})`,
|
||||
cls: `status-${status}`
|
||||
cls: `status-${status}`,
|
||||
});
|
||||
|
||||
// Set collapse state
|
||||
if (isCollapsed) {
|
||||
groupEl.addClass('note-status-is-collapsed');
|
||||
groupEl.addClass("note-status-is-collapsed");
|
||||
}
|
||||
|
||||
// Toggle collapse on click
|
||||
titleEl.addEventListener('click', (e) => {
|
||||
titleEl.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const newCollapsed = !groupEl.hasClass('note-status-is-collapsed');
|
||||
groupEl.toggleClass('note-status-is-collapsed', newCollapsed);
|
||||
const newCollapsed = !groupEl.hasClass("note-status-is-collapsed");
|
||||
groupEl.toggleClass("note-status-is-collapsed", newCollapsed);
|
||||
collapseIcon.empty();
|
||||
setIcon(collapseIcon, newCollapsed ? 'chevron-right' : 'chevron-down');
|
||||
setIcon(
|
||||
collapseIcon,
|
||||
newCollapsed ? "chevron-right" : "chevron-down",
|
||||
);
|
||||
options.callbacks.onStatusToggle(status, newCollapsed);
|
||||
});
|
||||
|
||||
// Render content
|
||||
const childrenEl = groupEl.createDiv({ cls: 'nav-folder-children' });
|
||||
const childrenEl = groupEl.createDiv({ cls: "nav-folder-children" });
|
||||
|
||||
// Pagination
|
||||
const currentPage = options.pagination.currentPage[status] || 0;
|
||||
|
|
@ -137,56 +175,84 @@ export class StatusPaneView{
|
|||
const endIndex = Math.min(startIndex + itemsPerPage, files.length);
|
||||
|
||||
// File items
|
||||
files.slice(startIndex, endIndex).forEach(file => {
|
||||
this.renderFileItem(childrenEl, file, status, options.isCompactView, options.callbacks);
|
||||
files.slice(startIndex, endIndex).forEach((file) => {
|
||||
this.renderFileItem(
|
||||
childrenEl,
|
||||
file,
|
||||
status,
|
||||
options.isCompactView,
|
||||
options.callbacks,
|
||||
);
|
||||
});
|
||||
|
||||
// Add pagination if needed
|
||||
if (files.length > itemsPerPage) {
|
||||
this.addPagination(childrenEl, status, currentPage, totalPages, files.length, options.callbacks);
|
||||
this.addPagination(
|
||||
childrenEl,
|
||||
status,
|
||||
currentPage,
|
||||
totalPages,
|
||||
files.length,
|
||||
options.callbacks,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private renderFileItem(container: HTMLElement, file: TFile, status: string, isCompactView: boolean, callbacks: any): void {
|
||||
const fileEl = container.createDiv({ cls: 'nav-file' });
|
||||
const fileTitleEl = fileEl.createDiv({ cls: 'nav-file-title' });
|
||||
private renderFileItem(
|
||||
container: HTMLElement,
|
||||
file: TFile,
|
||||
status: string,
|
||||
isCompactView: boolean,
|
||||
callbacks: Options["callbacks"],
|
||||
): void {
|
||||
const fileEl = container.createDiv({ cls: "nav-file" });
|
||||
const fileTitleEl = fileEl.createDiv({ cls: "nav-file-title" });
|
||||
|
||||
if (!isCompactView) {
|
||||
const fileIcon = fileTitleEl.createDiv({ cls: 'nav-file-icon' });
|
||||
setIcon(fileIcon, 'file');
|
||||
const fileIcon = fileTitleEl.createDiv({ cls: "nav-file-icon" });
|
||||
setIcon(fileIcon, "file");
|
||||
}
|
||||
|
||||
fileTitleEl.createSpan({
|
||||
text: file.basename,
|
||||
cls: 'nav-file-title-content'
|
||||
cls: "nav-file-title-content",
|
||||
});
|
||||
|
||||
fileTitleEl.createSpan({
|
||||
cls: `note-status-icon nav-file-tag status-${status}`,
|
||||
text: this.statusService.getStatusIcon(status)
|
||||
text: this.statusService.getStatusIcon(status),
|
||||
});
|
||||
|
||||
fileEl.addEventListener('click', (e) => {
|
||||
fileEl.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
callbacks.onFileClick(file);
|
||||
});
|
||||
|
||||
fileEl.addEventListener('contextmenu', (e) => {
|
||||
fileEl.addEventListener("contextmenu", (e) => {
|
||||
e.preventDefault();
|
||||
callbacks.onContextMenu(e, file);
|
||||
});
|
||||
}
|
||||
|
||||
private addPagination(container: HTMLElement, status: string, currentPage: number, totalPages: number, totalItems: number, callbacks: any): void {
|
||||
const paginationEl = container.createDiv({ cls: 'note-status-pagination' });
|
||||
private addPagination(
|
||||
container: HTMLElement,
|
||||
status: string,
|
||||
currentPage: number,
|
||||
totalPages: number,
|
||||
totalItems: number,
|
||||
callbacks: Options["callbacks"],
|
||||
): void {
|
||||
const paginationEl = container.createDiv({
|
||||
cls: "note-status-pagination",
|
||||
});
|
||||
|
||||
if (currentPage > 0) {
|
||||
const prevButton = paginationEl.createEl('button', {
|
||||
text: 'Previous',
|
||||
cls: 'note-status-pagination-button'
|
||||
const prevButton = paginationEl.createEl("button", {
|
||||
text: "Previous",
|
||||
cls: "note-status-pagination-button",
|
||||
});
|
||||
|
||||
prevButton.addEventListener('click', (e) => {
|
||||
prevButton.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
callbacks.onPageChange(status, currentPage - 1);
|
||||
});
|
||||
|
|
@ -194,24 +260,31 @@ export class StatusPaneView{
|
|||
|
||||
paginationEl.createSpan({
|
||||
text: `Page ${currentPage + 1} of ${totalPages} (${totalItems} notes)`,
|
||||
cls: 'note-status-pagination-info'
|
||||
cls: "note-status-pagination-info",
|
||||
});
|
||||
|
||||
if (currentPage < totalPages - 1) {
|
||||
const nextButton = paginationEl.createEl('button', {
|
||||
text: 'Next',
|
||||
cls: 'note-status-pagination-button'
|
||||
const nextButton = paginationEl.createEl("button", {
|
||||
text: "Next",
|
||||
cls: "note-status-pagination-button",
|
||||
});
|
||||
|
||||
nextButton.addEventListener('click', (e) => {
|
||||
nextButton.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
callbacks.onPageChange(status, currentPage + 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderEmptyState(container: HTMLElement, searchQuery: string, excludeUnknown: boolean, onShowUnassigned: () => void): void {
|
||||
const emptyMessage = container.createDiv({ cls: 'note-status-empty-indicator' });
|
||||
renderEmptyState(
|
||||
container: HTMLElement,
|
||||
searchQuery: string,
|
||||
excludeUnknown: boolean,
|
||||
onShowUnassigned: () => void,
|
||||
): void {
|
||||
const emptyMessage = container.createDiv({
|
||||
cls: "note-status-empty-indicator",
|
||||
});
|
||||
|
||||
if (searchQuery) {
|
||||
emptyMessage.textContent = `No notes found matching "${searchQuery}"`;
|
||||
|
|
@ -220,26 +293,28 @@ export class StatusPaneView{
|
|||
|
||||
if (excludeUnknown) {
|
||||
emptyMessage.createDiv({
|
||||
text: 'No notes with status found. Unassigned notes are currently hidden.',
|
||||
cls: 'note-status-empty-message'
|
||||
text: "No notes with status found. Unassigned notes are currently hidden.",
|
||||
cls: "note-status-empty-message",
|
||||
});
|
||||
|
||||
const btnContainer = emptyMessage.createDiv({
|
||||
cls: 'note-status-button-container'
|
||||
cls: "note-status-button-container",
|
||||
});
|
||||
|
||||
const showUnknownBtn = btnContainer.createEl('button', {
|
||||
text: 'Show unassigned notes',
|
||||
cls: 'note-status-show-unassigned-button'
|
||||
const showUnknownBtn = btnContainer.createEl("button", {
|
||||
text: "Show unassigned notes",
|
||||
cls: "note-status-show-unassigned-button",
|
||||
});
|
||||
|
||||
showUnknownBtn.addEventListener('click', onShowUnassigned);
|
||||
showUnknownBtn.addEventListener("click", onShowUnassigned);
|
||||
}
|
||||
}
|
||||
|
||||
createLoadingIndicator(container: HTMLElement, text?: string): HTMLElement {
|
||||
const loadingIndicator = container.createDiv({ cls: 'note-status-loading' });
|
||||
loadingIndicator.innerHTML = `<span>${text || 'Loading notes...'}</span>`;
|
||||
const loadingIndicator = container.createDiv({
|
||||
cls: "note-status-loading",
|
||||
});
|
||||
loadingIndicator.innerHTML = `<span>${text || "Loading notes..."}</span>`;
|
||||
return loadingIndicator;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue