mirror of
https://github.com/devonthesofa/obsidian-note-status.git
synced 2026-07-22 05:45:04 +00:00
feat: extension to non-markdown files
This commit is contained in:
parent
6ea99c29fa
commit
ee94f6b13c
18 changed files with 571 additions and 430 deletions
|
|
@ -160,6 +160,8 @@ Works with:
|
|||
- **QuickAdd macros**
|
||||
- **Global search**
|
||||
|
||||
Non-Markdown files (PDF, Canvas, images, etc.) can't host YAML frontmatter. Their statuses live in a lightweight JSON file under `.obsidian/plugins/obsidian-note-status/non-markdown-statuses.json`, and every command/view in the plugin reads from that store transparently. External tools such as Dataview or Templater still only see Markdown/frontmatter data, so keep using Markdown files if you need those integrations.
|
||||
|
||||
## 🛠️ API Reference
|
||||
|
||||
For developers building integrations or contributing:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { useState, useCallback } from "react";
|
||||
import { TFile } from "obsidian";
|
||||
import { NoteStatus } from "@/types/noteStatus";
|
||||
import { BaseNoteStatusService } from "@/core/noteStatusService";
|
||||
import {
|
||||
BaseNoteStatusService,
|
||||
NoteStatusService,
|
||||
} from "@/core/noteStatusService";
|
||||
import settingsService from "@/core/settingsService";
|
||||
|
||||
export interface VaultStats {
|
||||
|
|
@ -27,7 +30,7 @@ export const useVaultStats = () => {
|
|||
});
|
||||
|
||||
const calculateVaultStats = useCallback((): VaultStats => {
|
||||
const files = BaseNoteStatusService.app.vault.getMarkdownFiles();
|
||||
const files = BaseNoteStatusService.app.vault.getFiles();
|
||||
const availableStatuses =
|
||||
BaseNoteStatusService.getAllAvailableStatuses();
|
||||
const statusMetadataKeys = [settingsService.settings.tagPrefix];
|
||||
|
|
@ -49,50 +52,26 @@ export const useVaultStats = () => {
|
|||
});
|
||||
|
||||
files.forEach((file) => {
|
||||
const cachedMetadata =
|
||||
BaseNoteStatusService.app.metadataCache.getFileCache(file);
|
||||
const frontmatter = cachedMetadata?.frontmatter;
|
||||
|
||||
if (!frontmatter) return;
|
||||
|
||||
const noteStatusService = new NoteStatusService(file);
|
||||
noteStatusService.populateStatuses();
|
||||
let hasAnyStatus = false;
|
||||
|
||||
statusMetadataKeys.forEach((key) => {
|
||||
const value = frontmatter[key];
|
||||
if (value) {
|
||||
hasAnyStatus = true;
|
||||
tagDistribution[key]++;
|
||||
const statuses = noteStatusService.statuses[key] || [];
|
||||
if (!statuses.length) return;
|
||||
|
||||
const statusNames = Array.isArray(value) ? value : [value];
|
||||
statusNames.forEach((statusName) => {
|
||||
const statusStr = statusName.toString();
|
||||
hasAnyStatus = true;
|
||||
tagDistribution[key]++;
|
||||
|
||||
// Determine the scoped identifier to use
|
||||
let scopedIdentifier: string;
|
||||
|
||||
if (statusStr.includes(":")) {
|
||||
// Already scoped
|
||||
scopedIdentifier = statusStr;
|
||||
} else {
|
||||
// Legacy status - find first template that has this status
|
||||
const firstTemplateWithStatus =
|
||||
availableStatuses.find(
|
||||
(s) => s.name === statusStr && s.templateId,
|
||||
);
|
||||
scopedIdentifier = firstTemplateWithStatus
|
||||
? `${firstTemplateWithStatus.templateId}:${statusStr}`
|
||||
: statusStr; // Fallback to unscoped if no template found
|
||||
}
|
||||
|
||||
// Initialize status if not already present
|
||||
if (
|
||||
!statusDistribution.hasOwnProperty(scopedIdentifier)
|
||||
) {
|
||||
statusDistribution[scopedIdentifier] = 0;
|
||||
}
|
||||
statusDistribution[scopedIdentifier]++;
|
||||
});
|
||||
}
|
||||
statuses.forEach((status) => {
|
||||
const scopedIdentifier = status.templateId
|
||||
? `${status.templateId}:${status.name}`
|
||||
: status.name;
|
||||
if (!statusDistribution.hasOwnProperty(scopedIdentifier)) {
|
||||
statusDistribution[scopedIdentifier] = 0;
|
||||
}
|
||||
statusDistribution[scopedIdentifier]++;
|
||||
});
|
||||
});
|
||||
|
||||
if (hasAnyStatus) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ class EventBus {
|
|||
private events: { [K in EventName]: Map<string, EventBusEvents[K]> } = {
|
||||
"active-file-change": new Map(),
|
||||
"plugin-settings-changed": new Map(),
|
||||
"frontmatter-manually-changed": new Map(),
|
||||
"status-changed": new Map(),
|
||||
"triggered-open-modal": new Map(),
|
||||
};
|
||||
|
||||
|
|
@ -32,8 +32,8 @@ class EventBus {
|
|||
...args: Parameters<EventBusEvents["plugin-settings-changed"]>
|
||||
): void;
|
||||
publish(
|
||||
event: "frontmatter-manually-changed",
|
||||
...args: Parameters<EventBusEvents["frontmatter-manually-changed"]>
|
||||
event: "status-changed",
|
||||
...args: Parameters<EventBusEvents["status-changed"]>
|
||||
): void;
|
||||
publish(
|
||||
event: "triggered-open-modal",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import {
|
|||
} from "@/types/noteStatus";
|
||||
import settingsService from "@/core/settingsService";
|
||||
import eventBus from "./eventBus";
|
||||
import statusStoreManager from "./statusStoreManager";
|
||||
import type { StatusStore } from "./statusStores/types";
|
||||
|
||||
export abstract class BaseNoteStatusService {
|
||||
static app: App;
|
||||
|
|
@ -119,10 +121,12 @@ export abstract class BaseNoteStatusService {
|
|||
|
||||
export class NoteStatusService extends BaseNoteStatusService {
|
||||
private file: TFile;
|
||||
private statusStore: StatusStore;
|
||||
|
||||
constructor(file: TFile) {
|
||||
super();
|
||||
this.file = file;
|
||||
this.statusStore = statusStoreManager.getStoreForFile(file);
|
||||
}
|
||||
|
||||
private shouldStoreStatusesAsArray(): boolean {
|
||||
|
|
@ -133,39 +137,13 @@ export class NoteStatusService extends BaseNoteStatusService {
|
|||
return mode === "list";
|
||||
}
|
||||
|
||||
private normalizeStoredStatuses(value: unknown): string[] {
|
||||
if (!value) return [];
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map((status) =>
|
||||
status === undefined || status === null
|
||||
? undefined
|
||||
: String(status),
|
||||
)
|
||||
.filter((status): status is string => Boolean(status));
|
||||
}
|
||||
return [String(value)];
|
||||
private getStoreOptions() {
|
||||
return { storeAsArray: this.shouldStoreStatusesAsArray() };
|
||||
}
|
||||
|
||||
private writeStatusesToFrontmatter(
|
||||
frontmatter: Record<string, unknown>,
|
||||
frontmatterTagName: string,
|
||||
statuses: string[],
|
||||
) {
|
||||
if (!statuses.length) {
|
||||
if (this.shouldStoreStatusesAsArray()) {
|
||||
frontmatter[frontmatterTagName] = [];
|
||||
} else {
|
||||
delete frontmatter[frontmatterTagName];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.shouldStoreStatusesAsArray()) {
|
||||
frontmatter[frontmatterTagName] = [...statuses];
|
||||
} else {
|
||||
frontmatter[frontmatterTagName] = statuses[0];
|
||||
}
|
||||
private statusesAreEqual(a: string[], b: string[]): boolean {
|
||||
if (a.length !== b.length) return false;
|
||||
return a.every((value, index) => value === b[index]);
|
||||
}
|
||||
|
||||
public populateStatuses(): void {
|
||||
|
|
@ -178,34 +156,27 @@ export class NoteStatusService extends BaseNoteStatusService {
|
|||
return;
|
||||
}
|
||||
|
||||
const cachedMetadata =
|
||||
BaseNoteStatusService.app.metadataCache.getFileCache(this.file);
|
||||
const frontmatter = cachedMetadata?.frontmatter;
|
||||
if (!frontmatter) {
|
||||
return;
|
||||
}
|
||||
|
||||
STATUS_METADATA_KEYS.forEach((key) => {
|
||||
const value = frontmatter[key];
|
||||
if (value) {
|
||||
let statuses: (NoteStatusType | undefined)[] = [];
|
||||
if (Array.isArray(value)) {
|
||||
statuses = value.map((v) =>
|
||||
this.statusNameToObject(v.toString()),
|
||||
);
|
||||
} else {
|
||||
statuses = [this.statusNameToObject(value.toString())];
|
||||
}
|
||||
|
||||
statuses = statuses.filter((s) => s !== undefined);
|
||||
if (
|
||||
statuses.length &&
|
||||
!settingsService.settings.useMultipleStatuses
|
||||
) {
|
||||
statuses = statuses.slice(0, 1);
|
||||
}
|
||||
this.statuses[key] = statuses as NoteStatusType[];
|
||||
const identifiers = this.statusStore.getStatuses(this.file, key);
|
||||
if (!identifiers.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let statuses = identifiers
|
||||
.map((identifier) =>
|
||||
this.statusNameToObject(identifier.toString()),
|
||||
)
|
||||
.filter(
|
||||
(status): status is NoteStatusType => status !== undefined,
|
||||
);
|
||||
|
||||
if (
|
||||
statuses.length &&
|
||||
!settingsService.settings.useMultipleStatuses
|
||||
) {
|
||||
statuses = statuses.slice(0, 1);
|
||||
}
|
||||
this.statuses[key] = statuses;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -213,7 +184,6 @@ export class NoteStatusService extends BaseNoteStatusService {
|
|||
frontmatterTagName: string,
|
||||
status: NoteStatus,
|
||||
): Promise<boolean> {
|
||||
let removed = false;
|
||||
const targetIdentifier = status.templateId
|
||||
? BaseNoteStatusService.formatStatusIdentifier({
|
||||
templateId: status.templateId,
|
||||
|
|
@ -221,40 +191,37 @@ export class NoteStatusService extends BaseNoteStatusService {
|
|||
})
|
||||
: status.name;
|
||||
|
||||
await BaseNoteStatusService.app.fileManager.processFrontMatter(
|
||||
const removed = await this.statusStore.mutateStatuses(
|
||||
this.file,
|
||||
(frontmatter) => {
|
||||
const storedStatuses = this.normalizeStoredStatuses(
|
||||
frontmatter?.[frontmatterTagName],
|
||||
);
|
||||
if (!storedStatuses.length) return;
|
||||
frontmatterTagName,
|
||||
(current) => {
|
||||
if (!current.length) {
|
||||
return { hasChanged: false };
|
||||
}
|
||||
|
||||
// First try to find exact match (scoped or legacy)
|
||||
let i = storedStatuses.findIndex(
|
||||
let i = current.findIndex(
|
||||
(statusName: string) => statusName === targetIdentifier,
|
||||
);
|
||||
|
||||
// If not found and we're looking for a scoped status, try legacy format
|
||||
if (i === -1 && status.templateId) {
|
||||
i = storedStatuses.findIndex(
|
||||
i = current.findIndex(
|
||||
(statusName: string) => statusName === status.name,
|
||||
);
|
||||
}
|
||||
|
||||
if (i !== -1) {
|
||||
storedStatuses.splice(i, 1);
|
||||
this.writeStatusesToFrontmatter(
|
||||
frontmatter,
|
||||
frontmatterTagName,
|
||||
storedStatuses,
|
||||
);
|
||||
removed = true;
|
||||
if (i === -1) {
|
||||
return { hasChanged: false };
|
||||
}
|
||||
|
||||
const nextStatuses = [...current];
|
||||
nextStatuses.splice(i, 1);
|
||||
return { hasChanged: true, nextStatuses };
|
||||
},
|
||||
this.getStoreOptions(),
|
||||
);
|
||||
|
||||
if (removed) {
|
||||
eventBus.publish("frontmatter-manually-changed", {
|
||||
eventBus.publish("status-changed", {
|
||||
file: this.file,
|
||||
});
|
||||
}
|
||||
|
|
@ -262,20 +229,23 @@ export class NoteStatusService extends BaseNoteStatusService {
|
|||
}
|
||||
|
||||
async clearStatus(frontmatterTagName: string): Promise<boolean> {
|
||||
await BaseNoteStatusService.app.fileManager.processFrontMatter(
|
||||
const cleared = await this.statusStore.mutateStatuses(
|
||||
this.file,
|
||||
(frontmatter) => {
|
||||
if (frontmatterTagName in frontmatter) {
|
||||
this.writeStatusesToFrontmatter(
|
||||
frontmatter,
|
||||
frontmatterTagName,
|
||||
[],
|
||||
);
|
||||
frontmatterTagName,
|
||||
(current) => {
|
||||
if (!current.length) {
|
||||
return { hasChanged: false };
|
||||
}
|
||||
return { hasChanged: true, nextStatuses: [] };
|
||||
},
|
||||
this.getStoreOptions(),
|
||||
);
|
||||
eventBus.publish("frontmatter-manually-changed", { file: this.file });
|
||||
return true;
|
||||
if (cleared) {
|
||||
eventBus.publish("status-changed", {
|
||||
file: this.file,
|
||||
});
|
||||
}
|
||||
return cleared;
|
||||
}
|
||||
|
||||
async overrideStatuses(
|
||||
|
|
@ -287,29 +257,34 @@ export class NoteStatusService extends BaseNoteStatusService {
|
|||
? id
|
||||
: BaseNoteStatusService.formatStatusIdentifier(id),
|
||||
);
|
||||
await BaseNoteStatusService.app.fileManager.processFrontMatter(
|
||||
const statusesToStore = settingsService.settings.useMultipleStatuses
|
||||
? formattedIdentifiers
|
||||
: formattedIdentifiers.slice(0, 1);
|
||||
|
||||
const overridden = await this.statusStore.mutateStatuses(
|
||||
this.file,
|
||||
(frontmatter) => {
|
||||
const statusesToStore = settingsService.settings
|
||||
.useMultipleStatuses
|
||||
? formattedIdentifiers
|
||||
: formattedIdentifiers.slice(0, 1);
|
||||
this.writeStatusesToFrontmatter(
|
||||
frontmatter,
|
||||
frontmatterTagName,
|
||||
statusesToStore,
|
||||
);
|
||||
frontmatterTagName,
|
||||
(current) => {
|
||||
if (this.statusesAreEqual(current, statusesToStore)) {
|
||||
return { hasChanged: false };
|
||||
}
|
||||
return { hasChanged: true, nextStatuses: statusesToStore };
|
||||
},
|
||||
this.getStoreOptions(),
|
||||
);
|
||||
eventBus.publish("frontmatter-manually-changed", { file: this.file });
|
||||
return true;
|
||||
|
||||
if (overridden) {
|
||||
eventBus.publish("status-changed", {
|
||||
file: this.file,
|
||||
});
|
||||
}
|
||||
return overridden;
|
||||
}
|
||||
|
||||
async addStatus(
|
||||
frontmatterTagName: string,
|
||||
statusIdentifier: StatusIdentifier,
|
||||
): Promise<boolean> {
|
||||
let added = false;
|
||||
const formattedIdentifier =
|
||||
typeof statusIdentifier === "string"
|
||||
? statusIdentifier
|
||||
|
|
@ -317,64 +292,51 @@ export class NoteStatusService extends BaseNoteStatusService {
|
|||
statusIdentifier,
|
||||
);
|
||||
|
||||
await BaseNoteStatusService.app.fileManager.processFrontMatter(
|
||||
const added = await this.statusStore.mutateStatuses(
|
||||
this.file,
|
||||
(frontmatter) => {
|
||||
const noteStatusFrontmatter = this.normalizeStoredStatuses(
|
||||
frontmatter?.[frontmatterTagName],
|
||||
);
|
||||
frontmatterTagName,
|
||||
(current) => {
|
||||
if (!settingsService.settings.useMultipleStatuses) {
|
||||
const newValue = [formattedIdentifier];
|
||||
this.writeStatusesToFrontmatter(
|
||||
frontmatter,
|
||||
frontmatterTagName,
|
||||
newValue,
|
||||
);
|
||||
added = true;
|
||||
} else {
|
||||
// Check if we already have this status (exact match)
|
||||
const exactMatch = noteStatusFrontmatter.findIndex(
|
||||
(statusName: string) =>
|
||||
statusName === formattedIdentifier,
|
||||
);
|
||||
|
||||
if (exactMatch === -1) {
|
||||
// Check if we have a legacy version of this scoped status
|
||||
let legacyIndex = -1;
|
||||
if (
|
||||
typeof statusIdentifier !== "string" &&
|
||||
statusIdentifier.templateId
|
||||
) {
|
||||
legacyIndex = noteStatusFrontmatter.findIndex(
|
||||
(statusName: string) =>
|
||||
statusName === statusIdentifier.name,
|
||||
);
|
||||
}
|
||||
|
||||
if (legacyIndex !== -1) {
|
||||
// Replace legacy with scoped version
|
||||
noteStatusFrontmatter[legacyIndex] =
|
||||
formattedIdentifier;
|
||||
added = true;
|
||||
} else {
|
||||
// Add new status
|
||||
noteStatusFrontmatter.push(formattedIdentifier);
|
||||
added = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (added) {
|
||||
this.writeStatusesToFrontmatter(
|
||||
frontmatter,
|
||||
frontmatterTagName,
|
||||
noteStatusFrontmatter,
|
||||
);
|
||||
if (this.statusesAreEqual(current, newValue)) {
|
||||
return { hasChanged: false };
|
||||
}
|
||||
return { hasChanged: true, nextStatuses: newValue };
|
||||
}
|
||||
|
||||
const exactMatch = current.findIndex(
|
||||
(statusName: string) => statusName === formattedIdentifier,
|
||||
);
|
||||
|
||||
if (exactMatch !== -1) {
|
||||
return { hasChanged: false };
|
||||
}
|
||||
|
||||
let legacyIndex = -1;
|
||||
if (
|
||||
typeof statusIdentifier !== "string" &&
|
||||
statusIdentifier.templateId
|
||||
) {
|
||||
legacyIndex = current.findIndex(
|
||||
(statusName: string) =>
|
||||
statusName === statusIdentifier.name,
|
||||
);
|
||||
}
|
||||
|
||||
const nextStatuses = [...current];
|
||||
if (legacyIndex !== -1) {
|
||||
nextStatuses[legacyIndex] = formattedIdentifier;
|
||||
} else {
|
||||
nextStatuses.push(formattedIdentifier);
|
||||
}
|
||||
|
||||
return { hasChanged: true, nextStatuses };
|
||||
},
|
||||
this.getStoreOptions(),
|
||||
);
|
||||
|
||||
if (added) {
|
||||
eventBus.publish("frontmatter-manually-changed", {
|
||||
eventBus.publish("status-changed", {
|
||||
file: this.file,
|
||||
});
|
||||
}
|
||||
|
|
@ -404,23 +366,24 @@ export class MultipleNoteStatusService extends BaseNoteStatusService {
|
|||
const allStatuses = new Map<string, Set<string>>();
|
||||
|
||||
this.files.forEach((file) => {
|
||||
const cachedMetadata =
|
||||
BaseNoteStatusService.app.metadataCache.getFileCache(file);
|
||||
const frontmatter = cachedMetadata?.frontmatter;
|
||||
if (!frontmatter) return;
|
||||
let store: StatusStore;
|
||||
try {
|
||||
store = statusStoreManager.getStoreForFile(file);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
STATUS_METADATA_KEYS.forEach((key) => {
|
||||
const value = frontmatter[key];
|
||||
if (value) {
|
||||
if (!allStatuses.has(key)) {
|
||||
allStatuses.set(key, new Set());
|
||||
}
|
||||
const identifiers = store.getStatuses(file, key);
|
||||
if (!identifiers.length) return;
|
||||
|
||||
const statusNames = Array.isArray(value) ? value : [value];
|
||||
statusNames.forEach((name) =>
|
||||
allStatuses.get(key)!.add(name.toString()),
|
||||
);
|
||||
if (!allStatuses.has(key)) {
|
||||
allStatuses.set(key, new Set());
|
||||
}
|
||||
|
||||
identifiers.forEach((name) =>
|
||||
allStatuses.get(key)!.add(name.toString()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -490,25 +453,20 @@ export class MultipleNoteStatusService extends BaseNoteStatusService {
|
|||
: status.name;
|
||||
|
||||
return this.files.filter((file) => {
|
||||
const cachedMetadata =
|
||||
BaseNoteStatusService.app.metadataCache.getFileCache(file);
|
||||
const frontmatter = cachedMetadata?.frontmatter;
|
||||
if (!frontmatter) return false;
|
||||
|
||||
const value = frontmatter[frontmatterTagName];
|
||||
if (!value) return false;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
// Check for exact match first, then legacy format if scoped
|
||||
return (
|
||||
value.includes(targetIdentifier) ||
|
||||
(status.templateId && value.includes(status.name))
|
||||
);
|
||||
let store: StatusStore;
|
||||
try {
|
||||
store = statusStoreManager.getStoreForFile(file);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
// Check for exact match first, then legacy format if scoped
|
||||
return (
|
||||
value === targetIdentifier ||
|
||||
(status.templateId && value === status.name)
|
||||
|
||||
const identifiers = store.getStatuses(file, frontmatterTagName);
|
||||
if (!identifiers.length) return false;
|
||||
|
||||
return identifiers.some(
|
||||
(identifier) =>
|
||||
identifier === targetIdentifier ||
|
||||
(status.templateId && identifier === status.name),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
37
core/statusStoreManager.ts
Normal file
37
core/statusStoreManager.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { Plugin, TFile } from "obsidian";
|
||||
import { FrontmatterStatusStore } from "./statusStores/frontmatterStatusStore";
|
||||
import { NonMarkdownStatusStore } from "./statusStores/nonMarkdownStatusStore";
|
||||
import { StatusStore } from "./statusStores/types";
|
||||
|
||||
class StatusStoreManager {
|
||||
private stores: StatusStore[] = [];
|
||||
|
||||
async initialize(plugin: Plugin) {
|
||||
const frontmatterStore = new FrontmatterStatusStore(plugin.app);
|
||||
this.stores = [frontmatterStore];
|
||||
|
||||
const nonMarkdownStore = new NonMarkdownStatusStore(plugin);
|
||||
await nonMarkdownStore.initialize();
|
||||
this.registerStore(nonMarkdownStore);
|
||||
}
|
||||
|
||||
registerStore(store: StatusStore) {
|
||||
this.stores.push(store);
|
||||
}
|
||||
|
||||
getStoreForFile(file: TFile): StatusStore {
|
||||
const store = this.stores.find((candidate) =>
|
||||
candidate.canHandle(file),
|
||||
);
|
||||
|
||||
if (!store) {
|
||||
throw new Error(
|
||||
`No status store registered for file type: ${file.extension}`,
|
||||
);
|
||||
}
|
||||
|
||||
return store;
|
||||
}
|
||||
}
|
||||
|
||||
export default new StatusStoreManager();
|
||||
80
core/statusStores/frontmatterStatusStore.ts
Normal file
80
core/statusStores/frontmatterStatusStore.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { App, TFile } from "obsidian";
|
||||
import { StatusMutation, StatusStore } from "./types";
|
||||
|
||||
export class FrontmatterStatusStore implements StatusStore {
|
||||
constructor(private readonly app: App) {}
|
||||
|
||||
canHandle(file: TFile): boolean {
|
||||
return file.extension === "md";
|
||||
}
|
||||
|
||||
getStatuses(file: TFile, frontmatterTagName: string): string[] {
|
||||
const cachedMetadata = this.app.metadataCache.getFileCache(file);
|
||||
const frontmatter = cachedMetadata?.frontmatter;
|
||||
if (!frontmatter) return [];
|
||||
|
||||
return this.normalizeValue(frontmatter[frontmatterTagName]);
|
||||
}
|
||||
|
||||
async mutateStatuses(
|
||||
file: TFile,
|
||||
frontmatterTagName: string,
|
||||
mutator: (current: string[]) => StatusMutation,
|
||||
options: { storeAsArray: boolean },
|
||||
): Promise<boolean> {
|
||||
let changed = false;
|
||||
await this.app.fileManager.processFrontMatter(file, (frontmatter) => {
|
||||
const currentStatuses = this.normalizeValue(
|
||||
frontmatter?.[frontmatterTagName],
|
||||
);
|
||||
const mutation = mutator([...currentStatuses]);
|
||||
|
||||
if (!mutation.hasChanged) return;
|
||||
|
||||
this.writeStatuses(
|
||||
frontmatter,
|
||||
frontmatterTagName,
|
||||
mutation.nextStatuses,
|
||||
options.storeAsArray,
|
||||
);
|
||||
changed = true;
|
||||
});
|
||||
return changed;
|
||||
}
|
||||
|
||||
private normalizeValue(value: unknown): string[] {
|
||||
if (!value) return [];
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map((status) =>
|
||||
status === undefined || status === null
|
||||
? undefined
|
||||
: String(status),
|
||||
)
|
||||
.filter((status): status is string => Boolean(status));
|
||||
}
|
||||
return [String(value)];
|
||||
}
|
||||
|
||||
private writeStatuses(
|
||||
frontmatter: Record<string, unknown>,
|
||||
frontmatterTagName: string,
|
||||
statuses: string[],
|
||||
storeAsArray: boolean,
|
||||
) {
|
||||
if (!statuses.length) {
|
||||
if (storeAsArray) {
|
||||
frontmatter[frontmatterTagName] = [];
|
||||
} else {
|
||||
delete frontmatter[frontmatterTagName];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (storeAsArray) {
|
||||
frontmatter[frontmatterTagName] = [...statuses];
|
||||
} else {
|
||||
frontmatter[frontmatterTagName] = statuses[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
141
core/statusStores/nonMarkdownStatusStore.ts
Normal file
141
core/statusStores/nonMarkdownStatusStore.ts
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import { normalizePath, Plugin, TAbstractFile, TFile } from "obsidian";
|
||||
import eventBus from "@/core/eventBus";
|
||||
import { StatusMutation, StatusStore } from "./types";
|
||||
|
||||
type FileStatusMap = Record<string, Record<string, string[]>>;
|
||||
|
||||
type PersistedData = {
|
||||
version: number;
|
||||
files: FileStatusMap;
|
||||
};
|
||||
|
||||
export class NonMarkdownStatusStore implements StatusStore {
|
||||
private data: FileStatusMap = {};
|
||||
private readonly dataPath: string;
|
||||
|
||||
constructor(private readonly plugin: Plugin) {
|
||||
const configDir = this.plugin.app.vault.configDir;
|
||||
const pluginDir = `${configDir}/plugins/${this.plugin.manifest.id}`;
|
||||
this.dataPath = normalizePath(
|
||||
`${pluginDir}/non-markdown-statuses.json`,
|
||||
);
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
this.data = await this.loadFromDisk();
|
||||
this.registerVaultEvents();
|
||||
}
|
||||
|
||||
canHandle(file: TFile): boolean {
|
||||
return file.extension !== "md";
|
||||
}
|
||||
|
||||
getStatuses(file: TFile, frontmatterTagName: string): string[] {
|
||||
const fileStatuses = this.data[file.path];
|
||||
if (!fileStatuses) return [];
|
||||
const statuses = fileStatuses[frontmatterTagName];
|
||||
return statuses ? [...statuses] : [];
|
||||
}
|
||||
|
||||
async mutateStatuses(
|
||||
file: TFile,
|
||||
frontmatterTagName: string,
|
||||
mutator: (current: string[]) => StatusMutation,
|
||||
_options: { storeAsArray: boolean },
|
||||
): Promise<boolean> {
|
||||
const fileStatuses = this.data[file.path] ?? {};
|
||||
const current = fileStatuses[frontmatterTagName] ?? [];
|
||||
const mutation = mutator([...current]);
|
||||
|
||||
if (!mutation.hasChanged) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mutation.nextStatuses.length) {
|
||||
if (!this.data[file.path]) {
|
||||
this.data[file.path] = {};
|
||||
}
|
||||
this.data[file.path][frontmatterTagName] = [
|
||||
...mutation.nextStatuses,
|
||||
];
|
||||
} else if (this.data[file.path]) {
|
||||
delete this.data[file.path][frontmatterTagName];
|
||||
if (Object.keys(this.data[file.path]).length === 0) {
|
||||
delete this.data[file.path];
|
||||
}
|
||||
}
|
||||
|
||||
await this.persistData();
|
||||
return true;
|
||||
}
|
||||
|
||||
private async loadFromDisk(): Promise<FileStatusMap> {
|
||||
try {
|
||||
const raw = await this.plugin.app.vault.adapter.read(this.dataPath);
|
||||
const parsed = JSON.parse(raw) as PersistedData;
|
||||
if (parsed && typeof parsed === "object" && parsed.files) {
|
||||
return parsed.files;
|
||||
}
|
||||
} catch {
|
||||
// File may not exist yet or be malformed; start with empty dataset.
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
private async persistData(): Promise<void> {
|
||||
const payload: PersistedData = {
|
||||
version: 1,
|
||||
files: this.data,
|
||||
};
|
||||
await this.ensureDirectoryExists();
|
||||
await this.plugin.app.vault.adapter.write(
|
||||
this.dataPath,
|
||||
JSON.stringify(payload, null, 2),
|
||||
);
|
||||
}
|
||||
|
||||
private async ensureDirectoryExists(): Promise<void> {
|
||||
const dir = this.dataPath.split("/").slice(0, -1).join("/");
|
||||
if (!(await this.plugin.app.vault.adapter.exists(dir))) {
|
||||
await this.plugin.app.vault.adapter.mkdir(dir);
|
||||
}
|
||||
}
|
||||
|
||||
private registerVaultEvents() {
|
||||
this.plugin.registerEvent(
|
||||
this.plugin.app.vault.on("rename", (file, oldPath) => {
|
||||
this.handleRename(file, oldPath);
|
||||
}),
|
||||
);
|
||||
|
||||
this.plugin.registerEvent(
|
||||
this.plugin.app.vault.on("delete", (file) => {
|
||||
this.handleDelete(file);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private handleRename(file: TAbstractFile, oldPath: string) {
|
||||
if (!(file instanceof TFile)) return;
|
||||
const existing = this.data[oldPath];
|
||||
if (!existing) return;
|
||||
this.data[file.path] = existing;
|
||||
delete this.data[oldPath];
|
||||
this.persistData()
|
||||
.then(() => {
|
||||
eventBus.publish("status-changed", { file });
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
private handleDelete(file: TAbstractFile) {
|
||||
if (!(file instanceof TFile)) return;
|
||||
if (!this.data[file.path]) return;
|
||||
delete this.data[file.path];
|
||||
this.persistData()
|
||||
.then(() => {
|
||||
eventBus.publish("status-changed", { file });
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
}
|
||||
18
core/statusStores/types.ts
Normal file
18
core/statusStores/types.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { TFile } from "obsidian";
|
||||
|
||||
export type StatusMutation =
|
||||
| { hasChanged: false }
|
||||
| { hasChanged: true; nextStatuses: string[] };
|
||||
|
||||
export type StatusMutator = (current: string[]) => StatusMutation;
|
||||
|
||||
export interface StatusStore {
|
||||
canHandle(file: TFile): boolean;
|
||||
getStatuses(file: TFile, frontmatterTagName: string): string[];
|
||||
mutateStatuses(
|
||||
file: TFile,
|
||||
frontmatterTagName: string,
|
||||
mutator: StatusMutator,
|
||||
options: { storeAsArray: boolean },
|
||||
): Promise<boolean>;
|
||||
}
|
||||
|
|
@ -49,13 +49,14 @@ export class ContextMenuIntegration {
|
|||
const tFiles = files.filter(
|
||||
(f): f is TFile => f instanceof TFile,
|
||||
);
|
||||
if (tFiles.length) {
|
||||
this.openMultipleFilesStatusesModal(tFiles);
|
||||
} else {
|
||||
if (!tFiles.length) {
|
||||
new Notice(
|
||||
"The selected files are not valid to add status, just .md files can have status in this plugin version",
|
||||
"Select at least one file to change its status.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.openMultipleFilesStatusesModal(tFiles);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
|
@ -85,17 +86,17 @@ export class ContextMenuIntegration {
|
|||
item.setTitle("Change note status")
|
||||
.setIcon("rotate-ccw") // Lucide icon
|
||||
.onClick(async () => {
|
||||
if (view.file) {
|
||||
if (view.file instanceof TFile) {
|
||||
this.openSingleFileStatusesModal(
|
||||
view.file,
|
||||
);
|
||||
} else {
|
||||
new Notice(
|
||||
"The selected file is not valid to add status, just .md files can have status in this plugin version",
|
||||
);
|
||||
}
|
||||
if (
|
||||
!view.file ||
|
||||
!(view.file instanceof TFile)
|
||||
) {
|
||||
new Notice(
|
||||
"The selected item is not a valid file.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.openSingleFileStatusesModal(view.file);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
|
@ -154,25 +155,22 @@ export class ContextMenuIntegration {
|
|||
folder: TFolder,
|
||||
includeSubfolders: boolean,
|
||||
): void {
|
||||
const markdownFiles = this.getMarkdownFilesFromFolder(
|
||||
folder,
|
||||
includeSubfolders,
|
||||
);
|
||||
const files = this.getFilesFromFolder(folder, includeSubfolders);
|
||||
|
||||
if (!markdownFiles.length) {
|
||||
new Notice("This folder does not contain any Markdown notes.");
|
||||
if (!files.length) {
|
||||
new Notice("This folder does not contain any files.");
|
||||
return;
|
||||
}
|
||||
|
||||
const warningThreshold = 50;
|
||||
if (markdownFiles.length >= warningThreshold) {
|
||||
if (files.length >= warningThreshold) {
|
||||
new Notice(
|
||||
`This folder contains ${markdownFiles.length} notes. Applying status changes may take a while.`,
|
||||
`This folder contains ${files.length} files. Applying status changes may take a while.`,
|
||||
8000,
|
||||
);
|
||||
}
|
||||
|
||||
const multiStatusService = new MultipleNoteStatusService(markdownFiles);
|
||||
const multiStatusService = new MultipleNoteStatusService(files);
|
||||
multiStatusService.populateStatuses();
|
||||
|
||||
eventBus.publish("triggered-open-modal", {
|
||||
|
|
@ -180,7 +178,7 @@ export class ContextMenuIntegration {
|
|||
});
|
||||
}
|
||||
|
||||
private getMarkdownFilesFromFolder(
|
||||
private getFilesFromFolder(
|
||||
folder: TFolder,
|
||||
includeSubfolders: boolean,
|
||||
): TFile[] {
|
||||
|
|
@ -193,9 +191,7 @@ export class ContextMenuIntegration {
|
|||
|
||||
current.children.forEach((child) => {
|
||||
if (child instanceof TFile) {
|
||||
if (child.extension === "md") {
|
||||
files.push(child);
|
||||
}
|
||||
files.push(child);
|
||||
} else if (includeSubfolders && child instanceof TFolder) {
|
||||
queue.push(child);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export class FileExplorerIntegration implements IElementProcessor {
|
|||
this.setupWorkspaceIntegration();
|
||||
|
||||
eventBus.subscribe(
|
||||
"frontmatter-manually-changed",
|
||||
"status-changed",
|
||||
({ file }) => {
|
||||
const element = this.findFileElement(file.path);
|
||||
if (!element) return;
|
||||
|
|
@ -74,18 +74,13 @@ export class FileExplorerIntegration implements IElementProcessor {
|
|||
private getFileNoteStatusService(
|
||||
dataPath: string,
|
||||
): NoteStatusService | null {
|
||||
if (!dataPath.endsWith(".md")) {
|
||||
const abstractFile =
|
||||
this.plugin.app.vault.getAbstractFileByPath(dataPath);
|
||||
if (!(abstractFile instanceof TFile)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const file = this.plugin.app.vault.getAbstractFileByPath(
|
||||
dataPath,
|
||||
) as TFile;
|
||||
if (!file) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const noteStatusService = new NoteStatusService(file);
|
||||
const noteStatusService = new NoteStatusService(abstractFile);
|
||||
noteStatusService.populateStatuses();
|
||||
return noteStatusService;
|
||||
}
|
||||
|
|
@ -187,12 +182,9 @@ export class FileExplorerIntegration implements IElementProcessor {
|
|||
*/
|
||||
destroy(): void {
|
||||
this.observerService.cleanup();
|
||||
eventBus.unsubscribe("status-changed", this.EVENT_SUBSCRIPTION_ID);
|
||||
eventBus.unsubscribe(
|
||||
"frontmatter-manually-changed",
|
||||
this.EVENT_SUBSCRIPTION_ID,
|
||||
);
|
||||
eventBus.unsubscribe(
|
||||
"frontmatter-manually-changed",
|
||||
"status-changed",
|
||||
"fileExplorerIntegrationSubscription2",
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export class StatusModalIntegration extends Modal {
|
|||
);
|
||||
|
||||
eventBus.subscribe(
|
||||
"frontmatter-manually-changed",
|
||||
"status-changed",
|
||||
() => {
|
||||
// TODO: There are multiple calls to populateStatuses, in this case the noteStatusService is passed by reference, so redundant computations
|
||||
// FIXME: Line 27
|
||||
|
|
@ -141,7 +141,7 @@ export class StatusModalIntegration extends Modal {
|
|||
StatusModalIntegration.instance = null;
|
||||
|
||||
eventBus.unsubscribe(
|
||||
"frontmatter-manually-changed",
|
||||
"status-changed",
|
||||
"statusModalIntegrationSubscription1",
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { createRoot, Root } from "react-dom/client";
|
||||
import eventBus from "core/eventBus";
|
||||
import { MarkdownView, Plugin, WorkspaceLeaf } from "obsidian";
|
||||
import { Plugin } from "obsidian";
|
||||
import settingsService from "@/core/settingsService";
|
||||
import { StatusBar } from "@/components/StatusBar/StatusBar";
|
||||
import { NoteStatusService } from "@/core/noteStatusService";
|
||||
|
|
@ -11,7 +11,6 @@ export class StatusBarIntegration {
|
|||
private plugin: Plugin;
|
||||
private statusBarContainer: HTMLElement | null = null;
|
||||
private noteStatusService: NoteStatusService | null = null;
|
||||
private currentLeaf: WorkspaceLeaf | null = null;
|
||||
|
||||
constructor(plugin: Plugin) {
|
||||
if (StatusBarIntegration.instance) {
|
||||
|
|
@ -24,11 +23,8 @@ export class StatusBarIntegration {
|
|||
async integrate() {
|
||||
eventBus.subscribe(
|
||||
"active-file-change",
|
||||
({ leaf }) => {
|
||||
if (this.isValidMarkdownLeaf(leaf)) {
|
||||
this.currentLeaf = leaf;
|
||||
this.handleActiveFileChange().catch(console.error);
|
||||
}
|
||||
() => {
|
||||
this.handleActiveFileChange();
|
||||
},
|
||||
"statusBarIntegrationSubscription1",
|
||||
);
|
||||
|
|
@ -75,7 +71,7 @@ export class StatusBarIntegration {
|
|||
);
|
||||
|
||||
eventBus.subscribe(
|
||||
"frontmatter-manually-changed",
|
||||
"status-changed",
|
||||
() => {
|
||||
this.handleActiveFileChange().catch(console.error);
|
||||
},
|
||||
|
|
@ -83,27 +79,17 @@ export class StatusBarIntegration {
|
|||
);
|
||||
}
|
||||
|
||||
private async handleActiveFileChange() {
|
||||
this.extractStatusesFromLeaf(this.currentLeaf);
|
||||
this.render();
|
||||
}
|
||||
|
||||
private extractStatusesFromLeaf(leaf: WorkspaceLeaf | null) {
|
||||
if (!this.isValidMarkdownLeaf(leaf)) {
|
||||
return {};
|
||||
private async handleActiveFileChange(): Promise<void> {
|
||||
const file = this.plugin.app.workspace.getActiveFile();
|
||||
if (!file) {
|
||||
this.noteStatusService = null;
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
const markdownView = leaf!.view as MarkdownView;
|
||||
if (!markdownView.file) {
|
||||
return {};
|
||||
}
|
||||
|
||||
this.noteStatusService = new NoteStatusService(markdownView.file);
|
||||
this.noteStatusService = new NoteStatusService(file);
|
||||
this.noteStatusService.populateStatuses();
|
||||
}
|
||||
|
||||
private isValidMarkdownLeaf(leaf: WorkspaceLeaf | null): boolean {
|
||||
return leaf !== null && leaf.view.getViewType() === "markdown";
|
||||
this.render();
|
||||
}
|
||||
|
||||
private openStatusModal() {
|
||||
|
|
@ -181,12 +167,11 @@ export class StatusBarIntegration {
|
|||
"statusBarIntegrationSubscription2",
|
||||
);
|
||||
eventBus.unsubscribe(
|
||||
"plugin-settings-changed",
|
||||
"status-changed",
|
||||
"statusBarIntegrationSubscription3",
|
||||
);
|
||||
|
||||
this.destroyStatusBarItem();
|
||||
this.currentLeaf = null;
|
||||
this.noteStatusService = null;
|
||||
StatusBarIntegration.instance = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ export class EditorToolbarIntegration {
|
|||
);
|
||||
|
||||
eventBus.subscribe(
|
||||
"frontmatter-manually-changed",
|
||||
"status-changed",
|
||||
({ file }) => {
|
||||
// Find the button for the specific file and refresh only that one
|
||||
for (const [leaf, leafButton] of this.leafButtons.entries()) {
|
||||
|
|
@ -394,7 +394,7 @@ export class EditorToolbarIntegration {
|
|||
"editorToolbarIntegrationSubscription2",
|
||||
);
|
||||
eventBus.unsubscribe(
|
||||
"frontmatter-manually-changed",
|
||||
"status-changed",
|
||||
"editorToolbarIntegrationSubscription3",
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { ItemView, WorkspaceLeaf, TFile } from "obsidian";
|
||||
import { Root, createRoot } from "react-dom/client";
|
||||
import { GroupedStatusView as GroupedStatusViewComponent } from "@/components/GroupedStatusView/GroupedStatusView";
|
||||
import { BaseNoteStatusService } from "@/core/noteStatusService";
|
||||
import {
|
||||
BaseNoteStatusService,
|
||||
NoteStatusService,
|
||||
} from "@/core/noteStatusService";
|
||||
import eventBus from "@/core/eventBus";
|
||||
import settingsService from "@/core/settingsService";
|
||||
import { NoteStatus } from "@/types/noteStatus";
|
||||
|
|
@ -62,7 +65,7 @@ export class GroupedDashboardView extends ItemView {
|
|||
});
|
||||
|
||||
private getAllFiles = (): FileItem[] => {
|
||||
const tFiles = BaseNoteStatusService.app.vault.getMarkdownFiles();
|
||||
const tFiles = BaseNoteStatusService.app.vault.getFiles();
|
||||
return tFiles.map(this.convertTFileToFileItem);
|
||||
};
|
||||
|
||||
|
|
@ -71,16 +74,6 @@ export class GroupedDashboardView extends ItemView {
|
|||
const statusMetadataKeys = [settingsService.settings.tagPrefix];
|
||||
const availableStatuses =
|
||||
BaseNoteStatusService.getAllAvailableStatuses();
|
||||
// Create maps for both scoped and legacy status lookup
|
||||
const statusMap = new Map(
|
||||
availableStatuses.map((s) => {
|
||||
const key = s.templateId ? `${s.templateId}:${s.name}` : s.name;
|
||||
return [key, s];
|
||||
}),
|
||||
);
|
||||
const legacyStatusMap = new Map(
|
||||
availableStatuses.map((s) => [s.name, s]),
|
||||
);
|
||||
|
||||
statusMetadataKeys.forEach((key) => {
|
||||
result[key] = {};
|
||||
|
|
@ -93,41 +86,27 @@ export class GroupedDashboardView extends ItemView {
|
|||
});
|
||||
|
||||
files.forEach((file) => {
|
||||
// Find the TFile to get metadata
|
||||
const tFile = BaseNoteStatusService.app.vault.getAbstractFileByPath(
|
||||
file.path,
|
||||
) as TFile;
|
||||
if (!tFile) return;
|
||||
|
||||
const cachedMetadata =
|
||||
BaseNoteStatusService.app.metadataCache.getFileCache(tFile);
|
||||
const frontmatter = cachedMetadata?.frontmatter;
|
||||
|
||||
if (!frontmatter) return;
|
||||
const noteStatusService = new NoteStatusService(tFile);
|
||||
noteStatusService.populateStatuses();
|
||||
|
||||
statusMetadataKeys.forEach((key) => {
|
||||
const value = frontmatter[key];
|
||||
if (value) {
|
||||
const statusNames = Array.isArray(value) ? value : [value];
|
||||
statusNames.forEach((statusName) => {
|
||||
const statusStr = statusName.toString();
|
||||
// Try to find status by exact match first, then by legacy name
|
||||
let resolvedStatus = statusMap.get(statusStr);
|
||||
if (!resolvedStatus) {
|
||||
resolvedStatus = legacyStatusMap.get(statusStr);
|
||||
}
|
||||
const statusesForKey = noteStatusService.statuses[key] || [];
|
||||
if (!statusesForKey.length) return;
|
||||
|
||||
if (resolvedStatus) {
|
||||
const statusKey = resolvedStatus.templateId
|
||||
? `${resolvedStatus.templateId}:${resolvedStatus.name}`
|
||||
: resolvedStatus.name;
|
||||
if (!result[key][statusKey]) {
|
||||
result[key][statusKey] = [];
|
||||
}
|
||||
result[key][statusKey].push(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
statusesForKey.forEach((status) => {
|
||||
const statusKey = status.templateId
|
||||
? `${status.templateId}:${status.name}`
|
||||
: status.name;
|
||||
if (!result[key][statusKey]) {
|
||||
result[key][statusKey] = [];
|
||||
}
|
||||
result[key][statusKey].push(file);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -155,7 +134,7 @@ export class GroupedDashboardView extends ItemView {
|
|||
|
||||
private subscribeToEvents = (onDataChange: () => void) => {
|
||||
eventBus.subscribe(
|
||||
"frontmatter-manually-changed",
|
||||
"status-changed",
|
||||
onDataChange,
|
||||
"grouped-dashboard-view-subscription",
|
||||
);
|
||||
|
|
@ -182,7 +161,7 @@ export class GroupedDashboardView extends ItemView {
|
|||
|
||||
return () => {
|
||||
eventBus.unsubscribe(
|
||||
"frontmatter-manually-changed",
|
||||
"status-changed",
|
||||
"grouped-dashboard-view-subscription",
|
||||
);
|
||||
eventBus.unsubscribe(
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ import {
|
|||
GroupedStatusView as GroupedStatusViewComponent,
|
||||
StatusItem,
|
||||
} from "@/components/GroupedStatusView/GroupedStatusView";
|
||||
import { BaseNoteStatusService } from "@/core/noteStatusService";
|
||||
import {
|
||||
BaseNoteStatusService,
|
||||
NoteStatusService,
|
||||
} from "@/core/noteStatusService";
|
||||
import eventBus from "@/core/eventBus";
|
||||
import settingsService from "@/core/settingsService";
|
||||
import { NoteStatus } from "@/types/noteStatus";
|
||||
|
|
@ -46,7 +49,7 @@ export class GroupedStatusView extends ItemView {
|
|||
});
|
||||
|
||||
private getAllFiles = (): FileItem[] => {
|
||||
const tFiles = BaseNoteStatusService.app.vault.getMarkdownFiles();
|
||||
const tFiles = BaseNoteStatusService.app.vault.getFiles();
|
||||
return tFiles.map(this.convertTFileToFileItem);
|
||||
};
|
||||
|
||||
|
|
@ -55,16 +58,6 @@ export class GroupedStatusView extends ItemView {
|
|||
const statusMetadataKeys = [settingsService.settings.tagPrefix];
|
||||
const availableStatuses =
|
||||
BaseNoteStatusService.getAllAvailableStatuses();
|
||||
// Create maps for both scoped and legacy status lookup
|
||||
const statusMap = new Map(
|
||||
availableStatuses.map((s) => {
|
||||
const key = s.templateId ? `${s.templateId}:${s.name}` : s.name;
|
||||
return [key, s];
|
||||
}),
|
||||
);
|
||||
const legacyStatusMap = new Map(
|
||||
availableStatuses.map((s) => [s.name, s]),
|
||||
);
|
||||
|
||||
statusMetadataKeys.forEach((key) => {
|
||||
result[key] = {};
|
||||
|
|
@ -77,41 +70,27 @@ export class GroupedStatusView extends ItemView {
|
|||
});
|
||||
|
||||
files.forEach((file) => {
|
||||
// Find the TFile to get metadata
|
||||
const tFile = BaseNoteStatusService.app.vault.getAbstractFileByPath(
|
||||
file.path,
|
||||
) as TFile;
|
||||
if (!tFile) return;
|
||||
|
||||
const cachedMetadata =
|
||||
BaseNoteStatusService.app.metadataCache.getFileCache(tFile);
|
||||
const frontmatter = cachedMetadata?.frontmatter;
|
||||
|
||||
if (!frontmatter) return;
|
||||
const noteStatusService = new NoteStatusService(tFile);
|
||||
noteStatusService.populateStatuses();
|
||||
|
||||
statusMetadataKeys.forEach((key) => {
|
||||
const value = frontmatter[key];
|
||||
if (value) {
|
||||
const statusNames = Array.isArray(value) ? value : [value];
|
||||
statusNames.forEach((statusName) => {
|
||||
const statusStr = statusName.toString();
|
||||
// Try to find status by exact match first, then by legacy name
|
||||
let resolvedStatus = statusMap.get(statusStr);
|
||||
if (!resolvedStatus) {
|
||||
resolvedStatus = legacyStatusMap.get(statusStr);
|
||||
}
|
||||
const statusesForKey = noteStatusService.statuses[key] || [];
|
||||
if (!statusesForKey.length) return;
|
||||
|
||||
if (resolvedStatus) {
|
||||
const statusKey = resolvedStatus.templateId
|
||||
? `${resolvedStatus.templateId}:${resolvedStatus.name}`
|
||||
: resolvedStatus.name;
|
||||
if (!result[key][statusKey]) {
|
||||
result[key][statusKey] = [];
|
||||
}
|
||||
result[key][statusKey].push(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
statusesForKey.forEach((status) => {
|
||||
const statusKey = status.templateId
|
||||
? `${status.templateId}:${status.name}`
|
||||
: status.name;
|
||||
if (!result[key][statusKey]) {
|
||||
result[key][statusKey] = [];
|
||||
}
|
||||
result[key][statusKey].push(file);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -139,7 +118,7 @@ export class GroupedStatusView extends ItemView {
|
|||
|
||||
private subscribeToEvents = (onDataChange: () => void) => {
|
||||
eventBus.subscribe(
|
||||
"frontmatter-manually-changed",
|
||||
"status-changed",
|
||||
onDataChange,
|
||||
"grouped-status-view-subscription",
|
||||
);
|
||||
|
|
@ -167,7 +146,7 @@ export class GroupedStatusView extends ItemView {
|
|||
|
||||
return () => {
|
||||
eventBus.unsubscribe(
|
||||
"frontmatter-manually-changed",
|
||||
"status-changed",
|
||||
"grouped-status-view-subscription",
|
||||
);
|
||||
eventBus.unsubscribe(
|
||||
|
|
@ -184,14 +163,14 @@ export class GroupedStatusView extends ItemView {
|
|||
container.addClass("grouped-status-view-container");
|
||||
|
||||
// Check if vault exceeds the size limit
|
||||
const files = BaseNoteStatusService.app.vault.getMarkdownFiles();
|
||||
const files = BaseNoteStatusService.app.vault.getFiles();
|
||||
const vaultSizeLimit = settingsService.settings.vaultSizeLimit || 15000;
|
||||
|
||||
if (vaultSizeLimit > 0 && files.length > vaultSizeLimit) {
|
||||
// Show disabled message
|
||||
container.createEl("div", {
|
||||
cls: "grouped-status-view-disabled",
|
||||
text: `Grouped Status View disabled: Vault has ${files.length} notes (limit: ${vaultSizeLimit}). You can adjust this limit in plugin settings.`,
|
||||
text: `Grouped Status View disabled: Vault has ${files.length} files (limit: ${vaultSizeLimit}). You can adjust this limit in plugin settings.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export class StatusDashboardView extends ItemView {
|
|||
}
|
||||
|
||||
private calculateVaultStats = (): VaultStats => {
|
||||
const files = this.app.vault.getMarkdownFiles();
|
||||
const files = this.app.vault.getFiles();
|
||||
const availableStatuses =
|
||||
BaseNoteStatusService.getAllAvailableStatuses();
|
||||
const statusMetadataKeys = [settingsService.settings.tagPrefix];
|
||||
|
|
@ -80,49 +80,26 @@ export class StatusDashboardView extends ItemView {
|
|||
});
|
||||
|
||||
files.forEach((file) => {
|
||||
const cachedMetadata = this.app.metadataCache.getFileCache(file);
|
||||
const frontmatter = cachedMetadata?.frontmatter;
|
||||
|
||||
if (!frontmatter) return;
|
||||
|
||||
const noteStatusService = new NoteStatusService(file);
|
||||
noteStatusService.populateStatuses();
|
||||
let hasAnyStatus = false;
|
||||
|
||||
statusMetadataKeys.forEach((key) => {
|
||||
const value = frontmatter[key];
|
||||
if (value) {
|
||||
hasAnyStatus = true;
|
||||
tagDistribution[key]++;
|
||||
const statuses = noteStatusService.statuses[key] || [];
|
||||
if (!statuses.length) return;
|
||||
|
||||
const statusNames = Array.isArray(value) ? value : [value];
|
||||
statusNames.forEach((statusName) => {
|
||||
const statusStr = statusName.toString();
|
||||
hasAnyStatus = true;
|
||||
tagDistribution[key]++;
|
||||
|
||||
// Determine the scoped identifier to use
|
||||
let scopedIdentifier: string;
|
||||
|
||||
if (statusStr.includes(":")) {
|
||||
// Already scoped
|
||||
scopedIdentifier = statusStr;
|
||||
} else {
|
||||
// Legacy status - find first template that has this status
|
||||
const firstTemplateWithStatus =
|
||||
availableStatuses.find(
|
||||
(s) => s.name === statusStr && s.templateId,
|
||||
);
|
||||
scopedIdentifier = firstTemplateWithStatus
|
||||
? `${firstTemplateWithStatus.templateId}:${statusStr}`
|
||||
: statusStr; // Fallback to unscoped if no template found
|
||||
}
|
||||
|
||||
// Initialize status if not already present
|
||||
if (
|
||||
!statusDistribution.hasOwnProperty(scopedIdentifier)
|
||||
) {
|
||||
statusDistribution[scopedIdentifier] = 0;
|
||||
}
|
||||
statusDistribution[scopedIdentifier]++;
|
||||
});
|
||||
}
|
||||
statuses.forEach((status) => {
|
||||
const scopedIdentifier = status.templateId
|
||||
? `${status.templateId}:${status.name}`
|
||||
: status.name;
|
||||
if (!statusDistribution.hasOwnProperty(scopedIdentifier)) {
|
||||
statusDistribution[scopedIdentifier] = 0;
|
||||
}
|
||||
statusDistribution[scopedIdentifier]++;
|
||||
});
|
||||
});
|
||||
|
||||
if (hasAnyStatus) {
|
||||
|
|
@ -305,14 +282,14 @@ export class StatusDashboardView extends ItemView {
|
|||
container.addClass("status-dashboard-view-container");
|
||||
|
||||
// Check if vault exceeds the size limit
|
||||
const files = this.app.vault.getMarkdownFiles();
|
||||
const files = this.app.vault.getFiles();
|
||||
const vaultSizeLimit = settingsService.settings.vaultSizeLimit || 15000;
|
||||
|
||||
if (vaultSizeLimit > 0 && files.length > vaultSizeLimit) {
|
||||
// Show disabled message
|
||||
container.createEl("div", {
|
||||
cls: "status-dashboard-disabled",
|
||||
text: `Dashboard disabled: Vault has ${files.length} notes (limit: ${vaultSizeLimit}). You can adjust this limit in plugin settings.`,
|
||||
text: `Dashboard disabled: Vault has ${files.length} files (limit: ${vaultSizeLimit}). You can adjust this limit in plugin settings.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -329,7 +306,7 @@ export class StatusDashboardView extends ItemView {
|
|||
};
|
||||
|
||||
eventBus.subscribe(
|
||||
"frontmatter-manually-changed",
|
||||
"status-changed",
|
||||
handleVaultChange,
|
||||
"status-dashboard-vault-subscription",
|
||||
);
|
||||
|
|
@ -366,7 +343,7 @@ export class StatusDashboardView extends ItemView {
|
|||
async onClose() {
|
||||
// Clean up event listeners
|
||||
eventBus.unsubscribe(
|
||||
"frontmatter-manually-changed",
|
||||
"status-changed",
|
||||
"status-dashboard-vault-subscription",
|
||||
);
|
||||
eventBus.unsubscribe(
|
||||
|
|
|
|||
22
main.tsx
22
main.tsx
|
|
@ -1,4 +1,4 @@
|
|||
import { Plugin, WorkspaceLeaf } from "obsidian";
|
||||
import { Plugin, WorkspaceLeaf, TFile } from "obsidian";
|
||||
import StatusBarIntegration from "integrations/status-bar/status-bar";
|
||||
import eventBus from "core/eventBus";
|
||||
import { PluginSettingIntegration } from "./integrations/settings/pluginSettings";
|
||||
|
|
@ -18,6 +18,7 @@ import {
|
|||
VIEW_TYPE_STATUS_DASHBOARD,
|
||||
} from "./integrations/views/status-dashboard-view";
|
||||
import { StatusesInfoPopup } from "./integrations/popups/statusesInfoPopupIntegration";
|
||||
import statusStoreManager from "./core/statusStoreManager";
|
||||
|
||||
export default class NoteStatusPlugin extends Plugin {
|
||||
private statusBarIntegration: StatusBarIntegration;
|
||||
|
|
@ -29,6 +30,7 @@ export default class NoteStatusPlugin extends Plugin {
|
|||
|
||||
async onload() {
|
||||
BaseNoteStatusService.initialize(this.app);
|
||||
await statusStoreManager.initialize(this);
|
||||
await this.loadPluginSettings();
|
||||
|
||||
// INFO: initialize all integrations
|
||||
|
|
@ -158,7 +160,7 @@ export default class NoteStatusPlugin extends Plugin {
|
|||
// Propagate to custom event bus the manually frontmatter data
|
||||
this.registerEvent(
|
||||
this.app.metadataCache.on("changed", (file) => {
|
||||
eventBus.publish("frontmatter-manually-changed", { file });
|
||||
eventBus.publish("status-changed", { file });
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -180,6 +182,22 @@ export default class NoteStatusPlugin extends Plugin {
|
|||
},
|
||||
"main-status-popup-setting-subscriptor",
|
||||
);
|
||||
|
||||
this.registerEvent(
|
||||
this.app.vault.on("rename", (file) => {
|
||||
if (file instanceof TFile) {
|
||||
eventBus.publish("status-changed", { file });
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
this.registerEvent(
|
||||
this.app.vault.on("delete", (file) => {
|
||||
if (file instanceof TFile) {
|
||||
eventBus.publish("status-changed", { file });
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async loadPluginSettings() {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export type EventBusEvents = {
|
|||
value: unknown;
|
||||
currentSettings: PluginSettings;
|
||||
}) => void;
|
||||
"frontmatter-manually-changed": ({ file }: { file: TFile }) => void;
|
||||
"status-changed": ({ file }: { file: TFile }) => void;
|
||||
"triggered-open-modal": ({
|
||||
statusService,
|
||||
}: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue