diff --git a/main.ts b/main.ts
index 2d07212..8eeec1d 100644
--- a/main.ts
+++ b/main.ts
@@ -1,134 +1,913 @@
-import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
+import { App, Editor, MarkdownView, Notice, Plugin, PluginSettingTab, Setting, TFile, Menu, Vault, View, WorkspaceLeaf, addIcon } from 'obsidian';
-// Remember to rename these classes and interfaces!
+addIcon('status-pane', `
+
+`);
-interface MyPluginSettings {
- mySetting: string;
+interface NoteStatusSettings {
+ mySetting: string;
+ statusColors: Record;
+ showStatusDropdown: boolean;
+ showStatusBar: boolean;
+ dropdownPosition: 'top' | 'bottom';
+ statusBarPosition: 'left' | 'right';
+ autoHideStatusBar: boolean;
+ customStatuses: { name: string; icon: string }[];
+ showStatusIconsInExplorer: boolean;
+ collapsedStatuses: Record;
}
-const DEFAULT_SETTINGS: MyPluginSettings = {
- mySetting: 'default'
+const DEFAULT_SETTINGS: NoteStatusSettings = {
+ mySetting: 'default',
+ statusColors: {
+ active: '#00ff00',
+ onHold: '#ffa500',
+ completed: '#0000ff',
+ dropped: '#ff0000',
+ unknown: '#808080'
+ },
+ showStatusDropdown: true,
+ showStatusBar: true,
+ dropdownPosition: 'top',
+ statusBarPosition: 'right',
+ autoHideStatusBar: false,
+ customStatuses: [
+ { name: 'active', icon: '▶️' },
+ { name: 'onHold', icon: '⏸️' },
+ { name: 'completed', icon: '✅' },
+ { name: 'dropped', icon: '❌' },
+ { name: 'unknown', icon: '❓' }
+ ],
+ showStatusIconsInExplorer: true,
+ collapsedStatuses: {}
}
-export default class MyPlugin extends Plugin {
- settings: MyPluginSettings;
+class StatusPaneView extends View {
+ plugin: NoteStatus;
+ searchInput: HTMLInputElement | null = null;
+ buttonToggleStatusPanel: HTMLButtonElement | null = null;
- async onload() {
- await this.loadSettings();
+ constructor(leaf: WorkspaceLeaf, plugin: NoteStatus) {
+ super(leaf);
+ this.plugin = plugin;
+ }
- // This creates an icon in the left ribbon.
- const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (evt: MouseEvent) => {
- // Called when the user clicks the icon.
- new Notice('This is a notice!');
- });
- // Perform additional things with the ribbon
- ribbonIconEl.addClass('my-plugin-ribbon-class');
+ getViewType() {
+ return 'status-pane';
+ }
- // This adds a status bar item to the bottom of the app. Does not work on mobile apps.
- const statusBarItemEl = this.addStatusBarItem();
- statusBarItemEl.setText('Status Bar Text');
+ getDisplayText() {
+ return 'Status Pane';
+ }
- // This adds a simple command that can be triggered anywhere
- this.addCommand({
- id: 'open-sample-modal-simple',
- name: 'Open sample modal (simple)',
- callback: () => {
- new SampleModal(this.app).open();
- }
- });
- // This adds an editor command that can perform some operation on the current editor instance
- this.addCommand({
- id: 'sample-editor-command',
- name: 'Sample editor command',
- editorCallback: (editor: Editor, view: MarkdownView) => {
- console.log(editor.getSelection());
- editor.replaceSelection('Sample Editor Command');
- }
- });
- // This adds a complex command that can check whether the current state of the app allows execution of the command
- this.addCommand({
- id: 'open-sample-modal-complex',
- name: 'Open sample modal (complex)',
- checkCallback: (checking: boolean) => {
- // Conditions to check
- const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
- if (markdownView) {
- // If checking is true, we're simply "checking" if the command can be run.
- // If checking is false, then we want to actually perform the operation.
- if (!checking) {
- new SampleModal(this.app).open();
- }
+ getIcon() {
+ return 'status-pane';
+ }
- // This command will only show up in Command Palette when the check function returns true
- return true;
- }
- }
- });
+ async onOpen() {
+ await this.setupPane();
+ await this.renderGroups('');
+ }
- // This adds a settings tab so the user can configure various aspects of the plugin
- this.addSettingTab(new SampleSettingTab(this.app, this));
+ // Setup the static parts of the pane (search bar)
+ async setupPane() {
+ const { containerEl } = this;
+ containerEl.empty();
+ containerEl.addClass('status-pane');
+ containerEl.style.cssText = `
+ padding: 10px;
+ background: var(--background-secondary);
+ color: var(--text-normal);
+ font-family: var(--font-interface);
+ overflow-y: auto;
+ height: 100%;
+ `;
- // If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
- // Using this function will automatically remove the event listener when this plugin is disabled.
- this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
- console.log('click', evt);
- });
+ const searchContainer = containerEl.createDiv({ cls: 'status-pane-search' });
+ this.searchInput = searchContainer.createEl('input', {
+ type: 'text',
+ placeholder: 'Search notes...',
+ cls: 'status-pane-search-input'
+ });
+ this.searchInput.style.cssText = `
+ width: 100%;
+ padding: 8px 12px;
+ margin-bottom: 10px;
+ border: 1px solid var(--background-modifier-border);
+ border-radius: 4px;
+ background: var(--background-primary);
+ color: var(--text-normal);
+ outline: none;
+ transition: border-color 0.2s ease;
+ `;
+ this.searchInput.addEventListener('focus', () => {
+ this.searchInput!.style.borderColor = 'var(--interactive-accent)';
+ });
+ this.searchInput.addEventListener('blur', () => {
+ this.searchInput!.style.borderColor = 'var(--background-modifier-border)';
+ });
+ this.searchInput.addEventListener('input', () => {
+ this.renderGroups(this.searchInput!.value.toLowerCase());
+ });
- // When registering intervals, this function will automatically clear the interval when the plugin is disabled.
- this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
- }
+ const actionsContainer = containerEl.createDiv({ cls: 'status-pane-actions-container' });
- onunload() {
+ // Add refresh button for manual updates
+ const refreshButton = actionsContainer.createEl('button', {
+ type: 'button',
+ title: 'Refresh Statuses',
+ text: 'Refresh',
+ cls: 'status-pane-actions-refresh'
+ });
+ refreshButton.style.cssText = `
+ padding: 6px 12px;
+ margin: 5px 0 5px 10px;
+ background: var(--background-primary);
+ border: 1px solid var(--background-modifier-border);
+ border-radius: 4px;
+ cursor: pointer;
+ `;
+ refreshButton.addEventListener('click', async () => {
+ await this.renderGroups(this.searchInput?.value.toLowerCase() || '');
+ new Notice('Status pane refreshed');
+ });
+ }
- }
+ // Render only the dynamic groups
+ async renderGroups(searchQuery: string = '') {
+ const { containerEl } = this;
+ // Remove existing groups, but keep the search bar
+ const existingGroups = containerEl.querySelectorAll('.status-group');
+ existingGroups.forEach(group => group.remove());
- async loadSettings() {
- this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
- }
+ const statusGroups: Record = {};
+ this.plugin.settings.customStatuses.forEach(status => {
+ statusGroups[status.name] = [];
+ });
- async saveSettings() {
- await this.saveData(this.settings);
- }
+ const files = this.app.vault.getMarkdownFiles();
+ for (const file of files) {
+ const cachedMetadata = this.app.metadataCache.getFileCache(file);
+ let status = 'unknown';
+
+ if (cachedMetadata?.frontmatter?.status) {
+ const foundStatus = cachedMetadata.frontmatter.status.toLowerCase();
+ if (this.plugin.settings.customStatuses.some(s => s.name === foundStatus)) {
+ status = foundStatus;
+ }
+ }
+ statusGroups[status].push(file);
+ }
+
+ Object.entries(statusGroups).forEach(([status, files]) => {
+ const filteredFiles = files.filter(file =>
+ file.basename.toLowerCase().includes(searchQuery)
+ );
+ if (filteredFiles.length > 0) {
+ const groupEl = containerEl.createDiv({ cls: 'status-group nav-folder' });
+ const titleEl = groupEl.createDiv({ cls: 'nav-folder-title' });
+ const titleSpan = titleEl.createSpan({
+ text: `${status} ${this.plugin.getStatusIcon(status)} (${filteredFiles.length})`,
+ cls: `status-${status}`
+ });
+ titleSpan.style.cssText = `
+ color: ${this.plugin.settings.statusColors[status] || '#808080'};
+ font-weight: 600;
+ padding: 8px 12px;
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ transition: background 0.2s ease;
+ `;
+ titleEl.style.cursor = 'pointer';
+ const isCollapsed = this.plugin.settings.collapsedStatuses[status] ?? false;
+ if (isCollapsed) {
+ groupEl.addClass('is-collapsed');
+ }
+
+ titleEl.addEventListener('click', (e) => {
+ e.preventDefault(); // Prevent any unwanted navigation
+ groupEl.classList.toggle('is-collapsed');
+ this.plugin.settings.collapsedStatuses[status] = !this.plugin.settings.collapsedStatuses[status];
+ this.plugin.saveSettings();
+ });
+ titleEl.addEventListener('mouseover', () => {
+ titleSpan.style.background = 'var(--background-modifier-hover)';
+ });
+ titleEl.addEventListener('mouseout', () => {
+ titleSpan.style.background = 'transparent';
+ });
+
+ const childrenEl = groupEl.createDiv({ cls: 'nav-folder-children' });
+ childrenEl.style.cssText = `
+ padding-left: 20px;
+ border-left: 1px solid var(--background-modifier-border);
+ display: ${isCollapsed ? 'none' : 'block'};
+ `;
+ titleEl.addEventListener('click', () => {
+ childrenEl.style.display = childrenEl.style.display === 'none' ? 'block' : 'none';
+ });
+
+ filteredFiles.sort((a, b) => a.basename.localeCompare(b.basename)).forEach(file => {
+ const fileEl = childrenEl.createDiv({ cls: 'nav-file' });
+ const fileTitleEl = fileEl.createDiv({ cls: 'nav-file-title' });
+ const linkEl = fileTitleEl.createSpan({
+ text: file.basename,
+ cls: 'nav-file-title-content'
+ });
+ linkEl.style.cssText = `
+ padding: 6px 12px;
+ display: block;
+ color: var(--text-normal);
+ transition: background 0.2s ease;
+ `;
+ fileEl.addEventListener('click', (e) => {
+ e.preventDefault();
+ this.app.workspace.openLinkText(file.path, file.path, true);
+ });
+ fileEl.addEventListener('mouseover', () => {
+ linkEl.style.background = 'var(--background-modifier-hover)';
+ });
+ fileEl.addEventListener('mouseout', () => {
+ linkEl.style.background = 'transparent';
+ });
+ });
+ }
+ });
+ }
+
+ onClose() {
+ this.containerEl.empty();
+ return Promise.resolve();
+ }
}
-class SampleModal extends Modal {
- constructor(app: App) {
- super(app);
- }
+export default class NoteStatus extends Plugin {
+ settings: NoteStatusSettings;
+ statusBarItem: HTMLElement;
+ currentStatus: string = 'unknown';
+ statusDropdownContainer?: HTMLElement;
+ private statusPaneLeaf: WorkspaceLeaf | null = null;
- onOpen() {
- const {contentEl} = this;
- contentEl.setText('Woah!');
- }
+ async onload() {
+ await this.loadSettings();
- onClose() {
- const {contentEl} = this;
- contentEl.empty();
- }
+ this.registerView('status-pane', (leaf) => {
+ this.statusPaneLeaf = leaf;
+ return new StatusPaneView(leaf, this);
+ });
+
+ this.addRibbonIcon('status-pane', 'Open Status Pane', () => {
+ this.openStatusPane();
+ });
+
+ this.statusBarItem = this.addStatusBarItem();
+ this.statusBarItem.style.cursor = 'pointer';
+ this.statusBarItem.addEventListener('click', () => {
+ this.settings.showStatusDropdown = !this.settings.showStatusDropdown;
+ this.updateStatusDropdown();
+ this.saveSettings();
+ new Notice(`Status dropdown ${this.settings.showStatusDropdown ? 'shown' : 'hidden'}`);
+ });
+ this.updateStatusBar();
+
+ this.addCommand({
+ id: 'refresh-note-status',
+ name: 'Refresh Note Status',
+ callback: () => {
+ this.checkNoteStatus();
+ new Notice('Note status refreshed!');
+ }
+ });
+
+ this.addCommand({
+ id: 'insert-status-metadata',
+ name: 'Insert Status Metadata',
+ editorCallback: (editor: Editor, view: MarkdownView) => {
+ // Get the current note's content
+ const content = editor.getValue();
+
+ // Define the metadata to insert
+ const statusMetadata = 'status: unknown';
+
+ // Check if there's already a front matter
+ const frontMatterMatch = content.match(/^---\n([\s\S]+?)\n---/);
+
+ if (frontMatterMatch) {
+ // If front matter exists, update the status field if it exists, or add it
+ const frontMatter = frontMatterMatch[1];
+ let updatedFrontMatter = frontMatter;
+
+ // Check if 'status' already exists in front matter
+ if (/^status:/.test(frontMatter)) {
+ // Update the existing status
+ updatedFrontMatter = frontMatter.replace(/^status: .*/m, statusMetadata);
+ } else {
+ // Add the 'status' field to the front matter
+ updatedFrontMatter = `${frontMatter}\n${statusMetadata}`;
+ }
+
+ // Replace the old front matter with the updated one
+ const updatedContent = content.replace(/^---\n([\s\S]+?)\n---/, `---\n${updatedFrontMatter}\n---`);
+
+ // Set the updated content back into the editor
+ editor.setValue(updatedContent);
+ } else {
+ // If no front matter exists, create a new one with the status
+ const newFrontMatter = `---\n${statusMetadata}\n---\n${content}`;
+
+ // Set the new content with front matter
+ editor.setValue(newFrontMatter);
+ }
+ }
+ });
+
+
+ this.addCommand({
+ id: 'open-status-pane',
+ name: 'Open Status Pane',
+ callback: () => this.openStatusPane()
+ });
+
+ this.registerEvent(
+ this.app.workspace.on('editor-menu', (menu, editor, view) => {
+ menu.addItem((item) =>
+ item
+ .setTitle('Change Note Status')
+ .setIcon('tag')
+ .onClick(() => this.showStatusDropdown(editor, view))
+ );
+ })
+ );
+
+ this.registerEvent(
+ this.app.workspace.on('file-open', () => {
+ this.checkNoteStatus();
+ this.updateStatusDropdown();
+ })
+ );
+ this.registerEvent(
+ this.app.workspace.on('editor-change', () => this.checkNoteStatus())
+ );
+ this.registerEvent(
+ this.app.workspace.on('active-leaf-change', () => {
+ this.updateStatusDropdown();
+ this.updateStatusPane();
+ })
+ );
+
+ this.registerEvent(
+ this.app.vault.on('modify', (file) => {
+ if (file instanceof TFile) {
+ this.updateFileExplorerIcons(file);
+ this.updateStatusPane();
+ }
+ })
+ );
+ this.registerEvent(
+ this.app.vault.on('create', () => {
+ this.updateAllFileExplorerIcons();
+ this.updateStatusPane();
+ })
+ );
+ this.registerEvent(
+ this.app.vault.on('delete', () => {
+ this.updateAllFileExplorerIcons();
+ this.updateStatusPane();
+ })
+ );
+ this.registerEvent(
+ this.app.vault.on('rename', () => {
+ this.updateAllFileExplorerIcons();
+ this.updateStatusPane();
+ })
+ );
+ this.registerEvent(
+ this.app.metadataCache.on('changed', (file) => {
+ this.checkNoteStatus();
+ this.updateFileExplorerIcons(file);
+ this.updateStatusPane();
+ })
+ );
+
+ this.addSettingTab(new NoteStatusSettingTab(this.app, this));
+
+ this.checkNoteStatus();
+ this.updateStatusDropdown();
+ this.updateAllFileExplorerIcons();
+ }
+
+ async openStatusPane() {
+ const existing = this.app.workspace.getLeavesOfType('status-pane')[0];
+ if (existing) {
+ this.app.workspace.setActiveLeaf(existing);
+ await this.updateStatusPane();
+ } else {
+ const leaf = this.app.workspace.getLeftLeaf(false);
+ await leaf.setViewState({ type: 'status-pane', active: true });
+ }
+ }
+
+ // Modified updateNoteStatus to accept optional file parameter
+ async updateNoteStatus(newStatus: string, file?: TFile) {
+ const targetFile = file || this.app.workspace.getActiveFile();
+ if (!targetFile) return;
+
+ let content = await this.app.vault.read(targetFile);
+ let newContent = content;
+
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
+ if (frontmatterMatch) {
+ const frontmatter = frontmatterMatch[1];
+ if (frontmatter.includes('status:')) {
+ newContent = content.replace(/status:\s*\w+/i, `status: ${newStatus}`);
+ } else {
+ newContent = content.replace(/^---\n/, `---\nstatus: ${newStatus}\n`);
+ }
+ } else {
+ newContent = `---\nstatus: ${newStatus}\n---\n${content.trim()}`;
+ }
+
+ await this.app.vault.modify(targetFile, newContent);
+ if (targetFile === this.app.workspace.getActiveFile()) {
+ this.currentStatus = newStatus;
+ this.updateStatusBar();
+ this.updateStatusDropdown();
+ }
+ this.updateFileExplorerIcons(targetFile);
+ this.updateStatusPane();
+ }
+
+ async checkNoteStatus() {
+ const activeFile = this.app.workspace.getActiveFile();
+ if (!activeFile) {
+ this.currentStatus = 'unknown';
+ this.updateStatusBar();
+ this.updateStatusDropdown();
+ return;
+ }
+
+ const cachedMetadata = this.app.metadataCache.getFileCache(activeFile);
+ this.currentStatus = 'unknown';
+
+ if (cachedMetadata?.frontmatter?.status) {
+ const status = cachedMetadata.frontmatter.status.toLowerCase();
+ if (this.settings.customStatuses.some(s => s.name === status)) {
+ this.currentStatus = status;
+ }
+ }
+
+ this.updateStatusBar();
+ this.updateStatusDropdown();
+ this.updateFileExplorerIcons(activeFile);
+ }
+
+ updateStatusBar() {
+ this.statusBarItem.empty();
+ if (!this.settings.showStatusBar) {
+ this.statusBarItem.style.display = 'none';
+ return;
+ }
+
+ this.statusBarItem.style.display = 'flex';
+ this.statusBarItem.style.justifyContent = this.settings.statusBarPosition === 'left' ? 'flex-start' : 'flex-end';
+
+ const statusEl = this.statusBarItem.createEl('span', {
+ text: `Status: ${this.currentStatus}`,
+ cls: `note-status-${this.currentStatus}`
+ });
+ statusEl.style.color = this.settings.statusColors[this.currentStatus] || '#808080';
+ this.statusBarItem.createEl('span', {
+ text: this.getStatusIcon(this.currentStatus),
+ cls: 'status-icon'
+ });
+
+ if (this.settings.autoHideStatusBar && this.currentStatus === 'unknown') {
+ this.statusBarItem.style.opacity = '0';
+ this.statusBarItem.style.transition = 'opacity 0.5s ease';
+ setTimeout(() => {
+ if (this.currentStatus === 'unknown' && this.settings.showStatusBar) {
+ this.statusBarItem.style.display = 'none';
+ }
+ }, 500);
+ } else {
+ this.statusBarItem.style.opacity = '1';
+ this.statusBarItem.style.display = 'flex';
+ }
+ }
+
+ getStatusIcon(status: string): string {
+ const customStatus = this.settings.customStatuses.find(s => s.name === status);
+ return customStatus ? customStatus.icon : '❓';
+ }
+
+ toggleStatusModal() {
+ const modal = new Modal(this.app);
+ modal.contentEl.createEl('h2', { text: 'Change Note Status' });
+ this.settings.customStatuses
+ .filter(status => status.name !== 'unknown')
+ .forEach(status => {
+ const button = modal.contentEl.createEl('button', {
+ text: `${status.name} ${status.icon}`,
+ cls: `status-button status-${status.name}`
+ });
+ button.style.backgroundColor = this.settings.statusColors[status.name] || '#808080';
+ button.addEventListener('click', async () => {
+ await this.updateNoteStatus(status.name);
+ modal.close();
+ });
+ });
+ modal.open();
+ }
+
+
+ async updateStatusPane() {
+ if (this.statusPaneLeaf && this.statusPaneLeaf.view instanceof StatusPaneView) {
+ const searchQuery = (this.statusPaneLeaf.view as StatusPaneView).searchInput?.value.toLowerCase() || '';
+ await (this.statusPaneLeaf.view as StatusPaneView).renderGroups(searchQuery);
+ }
+ }
+
+ showStatusDropdown(editor: Editor, view: MarkdownView) {
+ const menu = new Menu(this.app);
+ this.settings.customStatuses
+ .filter(status => status.name !== 'unknown')
+ .forEach(status => {
+ menu.addItem((item) =>
+ item
+ .setTitle(`${status.name} ${status.icon}`)
+ .setIcon('tag')
+ .onClick(async () => {
+ await this.updateNoteStatus(status.name);
+ })
+ );
+ });
+
+ const rect = editor.getCursor('to');
+ const pos = editor.coordsAtPos(rect);
+ menu.showAtPosition({ x: pos.x, y: pos.y });
+ }
+
+ updateStatusDropdown() {
+ if (!this.settings.showStatusDropdown) {
+ if (this.statusDropdownContainer) {
+ this.statusDropdownContainer.remove();
+ this.statusDropdownContainer = undefined;
+ }
+ return;
+ }
+
+ const view = this.app.workspace.getActiveViewOfType(MarkdownView);
+ if (!view) {
+ if (this.statusDropdownContainer) {
+ this.statusDropdownContainer.remove();
+ this.statusDropdownContainer = undefined;
+ }
+ return;
+ }
+
+ const contentEl = view.contentEl;
+ if (!this.statusDropdownContainer) {
+ this.statusDropdownContainer = this.settings.dropdownPosition === 'top'
+ ? contentEl.insertBefore(document.createElement('div'), contentEl.firstChild)
+ : contentEl.appendChild(document.createElement('div'));
+ this.statusDropdownContainer.className = 'note-status-dropdown';
+ this.statusDropdownContainer.style.cssText = `
+ padding: 5px;
+ background: var(--background-secondary);
+ border-${this.settings.dropdownPosition === 'top' ? 'bottom' : 'top'}: 1px solid var(--background-modifier-border);
+ position: sticky;
+ ${this.settings.dropdownPosition}: 0;
+ z-index: 10;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ `;
+ }
+
+ this.statusDropdownContainer.empty();
+ const label = this.statusDropdownContainer.createEl('span', {
+ text: 'Status:',
+ cls: 'status-label'
+ });
+ label.style.fontWeight = 'bold';
+
+ const select = this.statusDropdownContainer.createEl('select', {
+ cls: 'status-select'
+ });
+ select.style.cssText = `
+ padding: 4px 8px;
+ border: 1px solid var(--background-modifier-border);
+ border-radius: 4px;
+ background: var(--background-primary);
+ color: var(--text-normal);
+ cursor: pointer;
+ outline: none;
+ transition: all 0.2s ease;
+ `;
+
+ this.settings.customStatuses.forEach(status => {
+ const option = select.createEl('option', {
+ text: `${status.name} ${status.icon}`,
+ value: status.name
+ });
+ option.style.color = this.settings.statusColors[status.name] || '#808080';
+ if (status.name === this.currentStatus) {
+ option.selected = true;
+ }
+ });
+
+ select.addEventListener('change', async (e) => {
+ const newStatus = (e.target as HTMLSelectElement).value;
+ if (newStatus !== 'unknown') {
+ await this.updateNoteStatus(newStatus);
+ }
+ });
+
+ select.addEventListener('mouseover', () => {
+ select.style.boxShadow = '0 2px 4px var(--background-modifier-box-shadow)';
+ });
+ select.addEventListener('mouseout', () => {
+ select.style.boxShadow = 'none';
+ });
+
+ const hideButton = this.statusDropdownContainer.createEl('button', {
+ text: 'Hide Bar',
+ cls: 'hide-status-bar-button'
+ });
+ hideButton.style.cssText = `
+ padding: 4px 8px;
+ border: 1px solid var(--background-modifier-border);
+ border-radius: 4px;
+ background: var(--background-primary);
+ color: var(--text-normal);
+ cursor: pointer;
+ transition: all 0.2s ease;
+ `;
+ hideButton.addEventListener('click', () => {
+ this.settings.showStatusDropdown = false;
+ this.updateStatusDropdown();
+ this.saveSettings();
+ new Notice('Status dropdown hidden');
+ });
+ hideButton.addEventListener('mouseover', () => {
+ hideButton.style.backgroundColor = 'var(--background-modifier-hover)';
+ });
+ hideButton.addEventListener('mouseout', () => {
+ hideButton.style.backgroundColor = 'var(--background-primary)';
+ });
+ }
+
+ async updateFileExplorerIcons(file: TFile) {
+ if (!this.settings.showStatusIconsInExplorer) return;
+
+ const content = await this.app.vault.read(file);
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
+ let status = 'unknown';
+
+ if (frontmatterMatch) {
+ const frontmatter = frontmatterMatch[1];
+ const statusMatch = frontmatter.match(/status:\s*(\w+)/i);
+ if (statusMatch) {
+ const foundStatus = statusMatch[1].toLowerCase();
+ if (this.settings.customStatuses.some(s => s.name === foundStatus)) {
+ status = foundStatus;
+ }
+ }
+ }
+
+ const fileExplorer = this.app.workspace.getLeavesOfType('file-explorer')[0];
+ if (fileExplorer && fileExplorer.view && (fileExplorer.view as any).fileItems) {
+ const fileItem = (fileExplorer.view as any).fileItems[file.path];
+ if (fileItem) {
+ const titleEl = fileItem.titleEl || fileItem.selfEl;
+ if (titleEl) {
+ const existingIcon = titleEl.querySelector('.note-status-icon');
+ if (existingIcon) existingIcon.remove();
+
+ const iconEl = titleEl.createEl('span', {
+ cls: 'note-status-icon',
+ text: this.getStatusIcon(status)
+ });
+ iconEl.style.marginLeft = '5px';
+ iconEl.style.color = this.settings.statusColors[status] || '#808080';
+ iconEl.style.fontSize = '12px';
+ }
+ }
+ }
+ }
+
+ updateAllFileExplorerIcons() {
+ if (!this.settings.showStatusIconsInExplorer) {
+ const fileExplorer = this.app.workspace.getLeavesOfType('file-explorer')[0];
+ if (fileExplorer && fileExplorer.view && (fileExplorer.view as any).fileItems) {
+ Object.values((fileExplorer.view as any).fileItems).forEach((fileItem: any) => {
+ const titleEl = fileItem.titleEl || fileItem.selfEl;
+ if (titleEl) {
+ const existingIcon = titleEl.querySelector('.note-status-icon');
+ if (existingIcon) existingIcon.remove();
+ }
+ });
+ }
+ return;
+ }
+
+ this.app.vault.getMarkdownFiles().forEach(file => {
+ this.updateFileExplorerIcons(file);
+ });
+ }
+
+ onunload() {
+ this.statusBarItem.remove();
+ if (this.statusDropdownContainer) {
+ this.statusDropdownContainer.remove();
+ }
+ const fileExplorer = this.app.workspace.getLeavesOfType('file-explorer')[0];
+ if (fileExplorer && fileExplorer.view && (fileExplorer.view as any).fileItems) {
+ Object.values((fileExplorer.view as any).fileItems).forEach((fileItem: any) => {
+ const titleEl = fileItem.titleEl || fileItem.selfEl;
+ if (titleEl) {
+ const existingIcon = titleEl.querySelector('.note-status-icon');
+ if (existingIcon) existingIcon.remove();
+ }
+ });
+ }
+ const statusPane = this.app.workspace.getLeavesOfType('status-pane')[0];
+ if (statusPane) {
+ statusPane.detach();
+ }
+ }
+
+ async loadSettings() {
+ this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
+ }
+
+ async saveSettings() {
+ await this.saveData(this.settings);
+ this.updateStatusPane();
+ }
}
-class SampleSettingTab extends PluginSettingTab {
- plugin: MyPlugin;
+class NoteStatusSettingTab extends PluginSettingTab {
+ plugin: NoteStatus;
- constructor(app: App, plugin: MyPlugin) {
- super(app, plugin);
- this.plugin = plugin;
- }
+ constructor(app: App, plugin: NoteStatus) {
+ super(app, plugin);
+ this.plugin = plugin;
+ }
- display(): void {
- const {containerEl} = this;
+ display(): void {
+ const { containerEl } = this;
+ containerEl.empty();
- containerEl.empty();
+ containerEl.createEl('h2', { text: 'Note Status Settings' });
- new Setting(containerEl)
- .setName('Setting #1')
- .setDesc('It\'s a secret')
- .addText(text => text
- .setPlaceholder('Enter your secret')
- .setValue(this.plugin.settings.mySetting)
- .onChange(async (value) => {
- this.plugin.settings.mySetting = value;
- await this.plugin.saveSettings();
- }));
- }
-}
+ new Setting(containerEl)
+ .setName('Show status dropdown')
+ .setDesc('Display status dropdown in notes (can also toggle by clicking status bar)')
+ .addToggle(toggle => toggle
+ .setValue(this.plugin.settings.showStatusDropdown)
+ .onChange(async (value) => {
+ this.plugin.settings.showStatusDropdown = value;
+ await this.plugin.saveSettings();
+ this.plugin.updateStatusDropdown();
+ }));
+
+ new Setting(containerEl)
+ .setName('Dropdown position')
+ .setDesc('Where to place the status dropdown in the note')
+ .addDropdown(dropdown => dropdown
+ .addOption('top', 'Top')
+ .addOption('bottom', 'Bottom')
+ .setValue(this.plugin.settings.dropdownPosition)
+ .onChange(async (value: 'top' | 'bottom') => {
+ this.plugin.settings.dropdownPosition = value;
+ await this.plugin.saveSettings();
+ this.plugin.updateStatusDropdown();
+ }));
+
+ new Setting(containerEl)
+ .setName('Show status bar')
+ .setDesc('Display the status bar at the bottom of the app')
+ .addToggle(toggle => toggle
+ .setValue(this.plugin.settings.showStatusBar)
+ .onChange(async (value) => {
+ this.plugin.settings.showStatusBar = value;
+ await this.plugin.saveSettings();
+ this.plugin.updateStatusBar();
+ }));
+
+ new Setting(containerEl)
+ .setName('Status bar position')
+ .setDesc('Align the status bar text')
+ .addDropdown(dropdown => dropdown
+ .addOption('left', 'Left')
+ .addOption('right', 'Right')
+ .setValue(this.plugin.settings.statusBarPosition)
+ .onChange(async (value: 'left' | 'right') => {
+ this.plugin.settings.statusBarPosition = value;
+ await this.plugin.saveSettings();
+ this.plugin.updateStatusBar();
+ }));
+
+ new Setting(containerEl)
+ .setName('Auto-hide status bar')
+ .setDesc('Hide the status bar when status is unknown')
+ .addToggle(toggle => toggle
+ .setValue(this.plugin.settings.autoHideStatusBar)
+ .onChange(async (value) => {
+ this.plugin.settings.autoHideStatusBar = value;
+ await this.plugin.saveSettings();
+ this.plugin.updateStatusBar();
+ }));
+
+ new Setting(containerEl)
+ .setName('Show status icons in file explorer')
+ .setDesc('Display status icons next to note names in the file explorer')
+ .addToggle(toggle => toggle
+ .setValue(this.plugin.settings.showStatusIconsInExplorer)
+ .onChange(async (value) => {
+ this.plugin.settings.showStatusIconsInExplorer = value;
+ await this.plugin.saveSettings();
+ this.plugin.updateAllFileExplorerIcons();
+ }));
+
+ containerEl.createEl('h3', { text: 'Custom Statuses' });
+
+ const statusList = containerEl.createDiv({ cls: 'custom-status-list' });
+ const renderStatuses = () => {
+ statusList.empty();
+ this.plugin.settings.customStatuses.forEach((status, index) => {
+ const statusSetting = new Setting(statusList)
+ .setName(status.name)
+ .addText(text => text
+ .setPlaceholder('Status Name')
+ .setValue(status.name)
+ .onChange(async (value) => {
+ if (value && !this.plugin.settings.customStatuses.some(s => s.name === value && s !== status)) {
+ const oldName = status.name;
+ status.name = value;
+ if (this.plugin.settings.statusColors[oldName]) {
+ this.plugin.settings.statusColors[value] = this.plugin.settings.statusColors[oldName];
+ delete this.plugin.settings.statusColors[oldName];
+ }
+ await this.plugin.saveSettings();
+ this.plugin.updateStatusDropdown();
+ this.plugin.updateStatusBar();
+ this.plugin.updateAllFileExplorerIcons();
+ }
+ }))
+ .addText(text => text
+ .setPlaceholder('Icon')
+ .setValue(status.icon)
+ .onChange(async (value) => {
+ status.icon = value || '❓';
+ await this.plugin.saveSettings();
+ this.plugin.updateStatusDropdown();
+ this.plugin.updateStatusBar();
+ this.plugin.updateAllFileExplorerIcons();
+ }))
+ .addColorPicker(colorPicker => colorPicker
+ .setValue(this.plugin.settings.statusColors[status.name] || '#808080')
+ .onChange(async (value) => {
+ this.plugin.settings.statusColors[status.name] = value;
+ await this.plugin.saveSettings();
+ this.plugin.updateStatusBar();
+ this.plugin.updateStatusDropdown();
+ this.plugin.updateAllFileExplorerIcons();
+ }))
+ .addButton(button => button
+ .setButtonText('Remove')
+ .setWarning()
+ .onClick(async () => {
+ this.plugin.settings.customStatuses.splice(index, 1);
+ delete this.plugin.settings.statusColors[status.name];
+ await this.plugin.saveSettings();
+ renderStatuses();
+ this.plugin.updateStatusDropdown();
+ this.plugin.updateStatusBar();
+ this.plugin.updateAllFileExplorerIcons();
+ }));
+ });
+ };
+ renderStatuses();
+
+ 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 () => {
+ const newStatus = { 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();
+ renderStatuses();
+ this.plugin.updateStatusDropdown();
+ this.plugin.updateAllFileExplorerIcons();
+ }));
+ }
+}
\ No newline at end of file
diff --git a/manifest.json b/manifest.json
index dfa940e..d6541b0 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,11 +1,20 @@
{
- "id": "sample-plugin",
- "name": "Sample Plugin",
+ "id": "obsidian-note-status",
+ "name": "Note Status",
"version": "1.0.0",
"minAppVersion": "0.15.0",
- "description": "Demonstrates some of the capabilities of the Obsidian API.",
- "author": "Obsidian",
- "authorUrl": "https://obsidian.md",
- "fundingUrl": "https://obsidian.md/pricing",
- "isDesktopOnly": false
+ "description": "Enhance your note organization with a status management system. Assign and track statuses (active, on hold, completed, dropped) for your notes with a customizable interface including status pane, dropdown, and file explorer integration.",
+ "author": "Aleix Soler",
+ "authorUrl": "https://aleixsoler.com",
+ "fundingUrl": {
+ //"Buy Me a Coffee": "https://www.buymeacoffee.com/aleixsoler",
+ "PayPal": "https://paypal.me/aleixsoler"
+ },
+ "isDesktopOnly": false,
+ "keywords": ["status", "notes", "organization", "productivity", "workflow"],
+ "license": "MIT",
+ "icon": "status-pane",
+ "repository": "https://github.com/devonthesofa/obsidian-note-status",
+ "issues": "https://github.com/devonthesofa/obsidian-note-status/issues",
+ "releaseNotes": "Initial release with status management, customizable UI, and file explorer integration."
}