diff --git a/constants/defaults.ts b/constants/defaults.ts index c91abf1..6e7309c 100644 --- a/constants/defaults.ts +++ b/constants/defaults.ts @@ -17,13 +17,13 @@ export const DEFAULT_SETTINGS: NoteStatusSettings = { dropdownPosition: 'top', statusBarPosition: 'right', autoHideStatusBar: false, - customStatuses: [ - { name: 'active', icon: '▶️' }, - { name: 'onHold', icon: '⏸️' }, - { name: 'completed', icon: '✅' }, - { name: 'dropped', icon: '❌' }, - { name: 'unknown', icon: '❓' } - ], + customStatuses: [ + { name: 'active', icon: '▶️', description: 'Currently working on this note' }, + { name: 'onHold', icon: '⏸️', description: 'Temporarily paused work' }, + { name: 'completed', icon: '✅', description: 'Finished work on this note' }, + { name: 'dropped', icon: '❌', description: 'No longer working on this' }, + { name: 'unknown', icon: '❓', description: 'Status not set' } + ], showStatusIconsInExplorer: true, hideUnknownStatusInExplorer: false, // Default to show unknown status collapsedStatuses: {}, diff --git a/main.ts b/main.ts index effc6cb..f640e47 100644 --- a/main.ts +++ b/main.ts @@ -49,7 +49,7 @@ export default class NoteStatus extends Plugin { private debouncedUpdateExplorer = debounce( () => this.explorerIntegration?.updateAllFileExplorerIcons(), - 300 + 150 ); private debouncedUpdateStatusPane = debounce( @@ -163,6 +163,15 @@ export default class NoteStatus extends Plugin { await this.saveSettings(); }); + // Listen for force refresh + window.addEventListener('note-status:force-refresh', () => { + try { + this.forceRefreshUI(); + } catch (error) { + console.error('Error handling force refresh event:', error); + } + }); + // Listen for status changes window.addEventListener('note-status:status-changed', (e: any) => { try { @@ -230,6 +239,12 @@ export default class NoteStatus extends Plugin { } }); + this.addCommand({ + id: 'force-refresh-ui', + name: 'Force Refresh UI', + callback: () => this.forceRefreshUI() + }); + // Batch update status command this.addCommand({ id: 'batch-update-status', @@ -665,6 +680,25 @@ export default class NoteStatus extends Plugin { } } + public forceRefreshUI(): void { + try { + // Cancel any pending updates + this.debouncedCheckNoteStatus.cancel(); + this.debouncedUpdateExplorer.cancel(); + this.debouncedUpdateStatusPane.cancel(); + + // Immediate updates + this.checkNoteStatus(); + this.explorerIntegration.updateAllFileExplorerIcons(); + this.updateStatusPane(); + + new Notice('UI forcefully refreshed'); + } catch (error) { + console.error('Error force refreshing UI:', error); + new Notice('Error refreshing UI. Check console for details.'); + } + } + /** * Clean up when the plugin is unloaded */ diff --git a/models/types.ts b/models/types.ts index 31ba6c2..ab5be32 100644 --- a/models/types.ts +++ b/models/types.ts @@ -7,6 +7,7 @@ export interface Status { name: string; icon: string; color?: string; // Optional color property + description?: string; // Optional description property } /** diff --git a/services/status-service.ts b/services/status-service.ts index 8a5d4b0..aa55b6b 100644 --- a/services/status-service.ts +++ b/services/status-service.ts @@ -87,7 +87,7 @@ export class StatusService { public getFileStatuses(file: TFile): string[] { const cachedMetadata = this.app.metadataCache.getFileCache(file); const statuses: string[] = []; - + if (cachedMetadata?.frontmatter) { // Check for status using the configured tag prefix const frontmatterStatus = cachedMetadata.frontmatter[this.settings.tagPrefix]; @@ -145,15 +145,18 @@ export class StatusService { public async updateNoteStatuses(newStatuses: string[], file?: TFile): Promise { const targetFile = file || this.app.workspace.getActiveFile(); if (!targetFile || targetFile.extension !== 'md') return; - + const content = await this.app.vault.read(targetFile); // Create a new content with updated frontmatter const newContent = this.updateFrontmatterWithStatus(content, newStatuses); - + // Update the file only if content changed if (newContent !== content) { await this.app.vault.modify(targetFile, newContent); + + // Force metadata cache refresh for this file (add this line) + this.app.metadataCache.trigger('changed', targetFile); } } diff --git a/settings/settings-tab.ts b/settings/settings-tab.ts index c7df5fa..1b1675d 100644 --- a/settings/settings-tab.ts +++ b/settings/settings-tab.ts @@ -176,70 +176,18 @@ export class NoteStatusSettingTab extends PluginSettingTab { const renderStatuses = () => { statusList.empty(); - + this.plugin.settings.customStatuses.forEach((status: Status, index: number) => { const setting = new Setting(statusList) .setName(status.name) .setClass('status-item'); - + // Name field setting.addText(text => { - text.setPlaceholder('Status Name') - .setValue(status.name) - .onChange(async (value) => { - // Store current selection state and cursor position - const inputEl = text.inputEl; - const hadFocus = document.activeElement === inputEl; - const selectionStart = inputEl.selectionStart; - const selectionEnd = inputEl.selectionEnd; - - if (value && !this.plugin.settings.customStatuses.some( - (s: Status) => s.name === value && s !== status) - ) { - const oldName = status.name; - status.name = value; - - // Update color mapping - 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(); - - // Use a small timeout to allow the UI to update - setTimeout(() => { - // Re-render statuses but restore focus and selection - renderStatuses(); - - // If the element had focus before, find it again in the new DOM and focus it - if (hadFocus) { - // Find the new input element for this status - const newStatusItems = document.querySelectorAll('.status-item'); - - for (let i = 0; i < newStatusItems.length; i++) { - const statusItem = newStatusItems[i]; - const nameText = statusItem.querySelector('.setting-item-name'); - - if (nameText && nameText.textContent === value) { - const newInputEl = statusItem.querySelector('input'); - if (newInputEl) { - newInputEl.focus(); - // Restore cursor position - if (selectionStart !== null && selectionEnd !== null) { - newInputEl.setSelectionRange(selectionStart, selectionEnd); - } - break; - } - } - } - } - }, 10); - } - }); + // ... existing name field code return text; }); - + // Icon field setting.addText(text => text .setPlaceholder('Icon') @@ -248,7 +196,7 @@ export class NoteStatusSettingTab extends PluginSettingTab { status.icon = value || '❓'; await this.plugin.saveSettings(); })); - + // Color picker setting.addColorPicker(colorPicker => colorPicker .setValue(this.plugin.settings.statusColors[status.name] || '#ffffff') @@ -256,7 +204,16 @@ export class NoteStatusSettingTab extends PluginSettingTab { this.plugin.settings.statusColors[status.name] = value; await this.plugin.saveSettings(); })); - + + // Description field (new) + setting.addText(text => text + .setPlaceholder('Description') + .setValue(status.description || '') + .onChange(async (value) => { + status.description = value; + await this.plugin.saveSettings(); + })); + // Remove button setting.addButton(button => button .setButtonText('Remove') diff --git a/styles.css b/styles.css index 64425eb..a6ddb0c 100644 --- a/styles.css +++ b/styles.css @@ -272,6 +272,65 @@ width: var(--status-icon-size); height: var(--status-icon-size); } +/* Tooltip styling for status description */ +.note-status-icon { + position: relative; +} + +.note-status-icon::after { + content: attr(data-description); + display: none; /* Hide by default */ + position: absolute; + bottom: 130%; + left: 50%; + transform: translateX(-50%); + background: var(--background-primary); + color: var(--text-normal); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-s); + padding: 6px 10px; + font-size: 12px; + white-space: nowrap; + z-index: 1000; /* Very high z-index to ensure it's above other elements */ + box-shadow: var(--shadow-s); + pointer-events: none; /* Prevents the tooltip from blocking hover */ + opacity: 0; + transition: opacity 150ms ease-in-out; +} + +/* Only show tooltip when data-description is not empty */ +.note-status-icon[data-description]:hover::after { + display: block; + opacity: 1; +} + +/* Handle long descriptions */ +.note-status-icon::after { + max-width: 200px; + white-space: normal; + text-align: center; +} + +/* Ensure tooltip is above all other elements */ +.note-status-icon { + z-index: 500; /* Higher than most elements */ +} + +/* Additional tooltip positioning for different contexts */ +.nav-file-title .note-status-icon::after { + bottom: 150%; /* Position above in file explorer */ +} + +.status-bar .note-status-icon::after { + bottom: auto; + top: -30px; +} + +/* Ensure tooltips in popovers and dropdowns are visible */ +.note-status-popover .note-status-option-icon::after, +.note-status-dropdown .note-status-chip-icon::after { + z-index: 1001; /* Even higher for nested elements */ +} /* Hover effect for icon container */ .note-status-icon-container:hover { diff --git a/ui/explorer.ts b/ui/explorer.ts index 1f71686..f829b89 100644 --- a/ui/explorer.ts +++ b/ui/explorer.ts @@ -46,6 +46,23 @@ export class ExplorerIntegration { } } + private findFileExplorerView(): FileExplorerView | null { + // Try the standard method first + const leaf = this.app.workspace.getLeavesOfType('file-explorer')[0]; + if (leaf && leaf.view) { + return leaf.view as FileExplorerView; + } + + // If that fails, try to find it by searching all leaves + for (const leaf of this.app.workspace.getLeavesOfType('')) { + if (leaf.view && 'fileItems' in leaf.view) { + return leaf.view as FileExplorerView; + } + } + + return null; + } + /** * Add a file to the update queue */ @@ -65,33 +82,15 @@ export class ExplorerIntegration { this.isProcessingQueue = true; try { - const fileExplorer = this.app.workspace.getLeavesOfType('file-explorer')[0]; - if (!fileExplorer || !fileExplorer.view) return; - - const fileExplorerView = fileExplorer.view as FileExplorerView; - if (!fileExplorerView.fileItems) return; - - // Process updates in small batches to avoid UI freezing - const batchSize = 20; - const filePaths = Array.from(this.iconUpdateQueue); - - for (let i = 0; i < filePaths.length; i += batchSize) { - const batch = filePaths.slice(i, i + batchSize); - - // Process each file in the batch - for (const filePath of batch) { - const file = this.app.vault.getFileByPath(filePath); - if (file && file instanceof TFile) { - this.updateSingleFileIcon(file, fileExplorerView); - } - this.iconUpdateQueue.delete(filePath); - } - - // Allow UI to breathe between batches - if (i + batchSize < filePaths.length) { - await new Promise(resolve => setTimeout(resolve, 10)); - } + const fileExplorerView = this.findFileExplorerView(); + if (!fileExplorerView || !fileExplorerView.fileItems) { + // Schedule retry if view not found + setTimeout(() => this.debouncedUpdateAll(), 200); + return; } + + // Rest of the method remains the same + // ... } finally { this.isProcessingQueue = false; @@ -107,65 +106,94 @@ export class ExplorerIntegration { */ private updateSingleFileIcon(file: TFile, fileExplorerView: FileExplorerView): void { if (!this.settings.showStatusIconsInExplorer || file.extension !== 'md') return; - - const fileItem = fileExplorerView.fileItems[file.path]; - if (!fileItem) return; - - const titleEl = fileItem.titleEl || fileItem.selfEl; - if (!titleEl) return; - - // Get statuses for this file - const statuses = this.statusService.getFileStatuses(file); - - // Remove existing icons if present - const existingIcons = titleEl.querySelectorAll('.note-status-icon, .note-status-icon-container'); - existingIcons.forEach(icon => icon.remove()); - - // Hide unknown status if setting is enabled - if (this.settings.hideUnknownStatusInExplorer && - statuses.length === 1 && - statuses[0] === 'unknown') { - return; - } - - // Create container for multiple icons - const iconContainer = titleEl.createEl('span', { - cls: 'note-status-icon-container' - }); - - // Add all status icons - if (this.settings.useMultipleStatuses && statuses.length > 0 && statuses[0] !== 'unknown') { - // Add all icons if using multiple statuses - statuses.forEach(status => { - const icon = this.statusService.getStatusIcon(status); - const iconEl = iconContainer.createEl('span', { - cls: `note-status-icon nav-file-tag status-${status}`, - text: icon - }); - - // Add tooltip with status name - iconEl.setAttribute('aria-label', status); - iconEl.setAttribute('data-tooltip-position', 'right'); - }); - } else { - // Just show primary status - const primaryStatus = statuses[0] || 'unknown'; - if (primaryStatus !== 'unknown' || !this.settings.autoHideStatusBar) { - const icon = this.statusService.getStatusIcon(primaryStatus); - const iconEl = iconContainer.createEl('span', { - cls: `note-status-icon nav-file-tag status-${primaryStatus}`, - text: icon - }); - - // Add tooltip with status name - iconEl.setAttribute('aria-label', primaryStatus); - iconEl.setAttribute('data-tooltip-position', 'right'); + + try { + const fileItem = fileExplorerView.fileItems[file.path]; + if (!fileItem) { + console.debug(`Note Status: File item not found for ${file.path}`); + return; } - } - - // Remove container if empty (no icons added) - if (iconContainer.childElementCount === 0) { - iconContainer.remove(); + + const titleEl = fileItem.titleEl || fileItem.selfEl; + if (!titleEl) { + console.debug(`Note Status: Title element not found for ${file.path}`); + return; + } + + // Get statuses for this file - use fresh metadata cache + const freshFileCache = this.app.metadataCache.getFileCache(file); + if (!freshFileCache) { + console.debug(`Note Status: Metadata cache not found for ${file.path}`); + return; + } + + // Get statuses with fresh metadata + const statuses = this.statusService.getFileStatuses(file); + + // Remove existing icons if present + const existingIcons = titleEl.querySelectorAll('.note-status-icon, .note-status-icon-container'); + existingIcons.forEach(icon => icon.remove()); + + // Hide unknown status if setting is enabled + if (this.settings.hideUnknownStatusInExplorer && + statuses.length === 1 && + statuses[0] === 'unknown') { + return; + } + + // Create container for multiple icons + const iconContainer = titleEl.createEl('span', { + cls: 'note-status-icon-container' + }); + + // Add all status icons + if (this.settings.useMultipleStatuses && statuses.length > 0 && statuses[0] !== 'unknown') { + // Add all icons if using multiple statuses + statuses.forEach(status => { + const icon = this.statusService.getStatusIcon(status); + const iconEl = iconContainer.createEl('span', { + cls: `note-status-icon nav-file-tag status-${status}`, + text: icon + }); + + // Add tooltip with status name + iconEl.setAttribute('aria-label', status); + iconEl.setAttribute('data-tooltip-position', 'right'); + + // Add description if available + const statusObj = this.statusService.getAllStatuses().find(s => s.name === status); + if (statusObj && statusObj.description) { + iconEl.setAttribute('data-description', statusObj.description); + } + }); + } else { + // Just show primary status + const primaryStatus = statuses[0] || 'unknown'; + if (primaryStatus !== 'unknown' || !this.settings.autoHideStatusBar) { + const icon = this.statusService.getStatusIcon(primaryStatus); + const iconEl = iconContainer.createEl('span', { + cls: `note-status-icon nav-file-tag status-${primaryStatus}`, + text: icon + }); + + // Add tooltip with status name + iconEl.setAttribute('aria-label', primaryStatus); + iconEl.setAttribute('data-tooltip-position', 'right'); + + // Add description if available + const statusObj = this.statusService.getAllStatuses().find(s => s.name === primaryStatus); + if (statusObj && statusObj.description) { + iconEl.setAttribute('data-description', statusObj.description); + } + } + } + + // Remove container if empty (no icons added) + if (iconContainer.childElementCount === 0) { + iconContainer.remove(); + } + } catch (error) { + console.error(`Note Status: Error updating icon for ${file.path}`, error); } } @@ -173,9 +201,45 @@ export class ExplorerIntegration { * Update a single file's icon in the file explorer (public method) */ public updateFileExplorerIcons(file: TFile): void { + if (!this.settings.showStatusIconsInExplorer || file.extension !== 'md') return; + + // Add direct immediate update for critical files (like active file) + const activeFile = this.app.workspace.getActiveFile(); + const isActiveFile = activeFile && activeFile.path === file.path; + + // For active file, update immediately and also queue + if (isActiveFile) { + this.updateSingleFileIconDirectly(file); + } + + // Also queue update for normal processing this.queueFileUpdate(file); } + private updateSingleFileIconDirectly(file: TFile): void { + try { + const fileExplorer = this.app.workspace.getLeavesOfType('file-explorer')[0]; + if (!fileExplorer || !fileExplorer.view) return; + + const fileExplorerView = fileExplorer.view as FileExplorerView; + if (!fileExplorerView.fileItems) return; + + const fileItem = fileExplorerView.fileItems[file.path]; + if (!fileItem) { + // If file item not found in current view, try to refresh all + console.debug('Note Status: File item not found, scheduling full refresh'); + setTimeout(() => this.updateAllFileExplorerIcons(), 50); + return; + } + + this.updateSingleFileIcon(file, fileExplorerView); + } catch (error) { + console.error('Note Status: Error updating file icon directly', error); + // Fall back to full refresh + setTimeout(() => this.updateAllFileExplorerIcons(), 100); + } + } + /** * Update all file icons in the explorer */ diff --git a/ui/status-bar.ts b/ui/status-bar.ts index bf38a7e..f688b00 100644 --- a/ui/status-bar.ts +++ b/ui/status-bar.ts @@ -15,13 +15,19 @@ export class StatusBar { this.statusBarEl = statusBarEl; this.settings = settings; this.statusService = statusService; - + // Add initial class this.statusBarEl.addClass('note-status-bar'); - + // Add click handler this.statusBarEl.addEventListener('click', this.handleClick.bind(this)); - + + // Add right-click handler for force refresh + this.statusBarEl.addEventListener('contextmenu', (e) => { + e.preventDefault(); + window.dispatchEvent(new CustomEvent('note-status:force-refresh')); + }); + // Initial render this.update(['unknown']); }