From 6798eb93078d03c0480fcbd6a52c714df9481354 Mon Sep 17 00:00:00 2001 From: Aleix Date: Mon, 12 May 2025 15:26:47 +0200 Subject: [PATCH 01/67] reafactor: cleaned-up CSS --- styles.css | 752 ++++++++---------------------- ui/components/status-pane-view.ts | 18 +- 2 files changed, 202 insertions(+), 568 deletions(-) diff --git a/styles.css b/styles.css index e526f25..410bf45 100644 --- a/styles.css +++ b/styles.css @@ -20,12 +20,7 @@ } /* Animations */ -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -@keyframes scaleIn { +@keyframes note-status-scale-in { from { opacity: 0; transform: scale(0.9); @@ -36,7 +31,7 @@ } } -@keyframes slideInFade { +@keyframes note-status-slide-in-fade { from { opacity: 0; transform: translateY(-10px); @@ -47,7 +42,7 @@ } } -@keyframes fadeInSlideDown { +@keyframes note-status-fade-in-slide-down { from { opacity: 0; transform: translateY(-6px); @@ -58,29 +53,7 @@ } } -@keyframes statusDropdownFadeIn { - from { - opacity: 0; - transform: scale(0.95); - } - to { - opacity: 1; - transform: scale(1); - } -} - -@keyframes statusDropdownFadeOut { - from { - opacity: 1; - transform: scale(1); - } - to { - opacity: 0; - transform: scale(0.95); - } -} - -@keyframes pulse { +@keyframes note-status-pulse { 0% { transform: scale(1); } 50% { transform: scale(0.98); } 100% { transform: scale(1); } @@ -98,116 +71,31 @@ font-size: 1.2em; } -.note-status-modal-title { - margin: 0; - font-size: 1.3em; - line-height: 1.3; -} - -.note-status-modal-section { - display: flex; - flex-direction: column; - gap: 8px; -} - -.note-status-section-header { - display: flex; - justify-content: space-between; - align-items: center; -} - -.note-status-section-title { - margin: 0; - font-size: 1em; - font-weight: var(--font-semibold); - color: var(--text-normal); -} - -.note-status-status-count { - font-size: var(--font-smaller); - color: var(--text-muted); - transition: all 0.2s var(--ease-out); -} - -.note-status-status-count.has-selection { - color: var(--text-accent); - font-weight: var(--font-semibold); -} - -.note-status-status-select-container { - position: relative; -} - -.note-status-status-select { - width: 100%; - padding: 8px; - border-radius: var(--radius-s); - background: var(--background-primary); - border: var(--input-border-width) solid var(--background-modifier-border); - color: var(--text-normal); - font-size: var(--font-smaller); - outline: none; - transition: all 0.2s var(--ease-out); - max-height: 200px; -} - -.note-status-status-select option { - padding: 6px 8px; -} - -.note-status-status-select:focus { - border-color: var(--interactive-accent); - box-shadow: 0 0 0 2px var(--interactive-accent-hover); -} - -.note-status-empty-option { - font-style: italic; - color: var(--text-muted); - text-align: center; - padding: 12px !important; -} - /* Action buttons in modal */ -.note-status-modal-buttons { +.note-status-action-button { display: flex; - justify-content: space-between; - margin-top: 8px; -} - -.note-status-select-all, -.note-status-apply-button { - padding: 8px 16px; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; border-radius: var(--radius-s); - background: var(--background-secondary); - border: var(--input-border-width) solid var(--background-modifier-border); - color: var(--text-normal); + background: transparent; + color: var(--text-muted); cursor: pointer; - transition: all 0.15s var(--ease-out); - font-size: var(--font-smaller); + transition: all 0.15s ease; + border: none; + padding: 0; } -.note-status-select-all:hover, -.note-status-apply-button:hover { - background: var(--background-modifier-hover); - box-shadow: var(--shadow-s); +.note-status-action-button:hover { + background-color: var(--background-modifier-hover); + color: var(--text-normal); } -.note-status-select-all:active, -.note-status-apply-button:active { - transform: translateY(1px); - box-shadow: none; -} - -.note-status-apply-button { - background: var(--interactive-accent); +.note-status-action-button.note-status-button-active { + background-color: var(--interactive-accent); color: var(--text-on-accent); - border-color: var(--interactive-accent); -} - -.note-status-apply-button:disabled { - opacity: 0.6; - cursor: not-allowed; - pointer-events: none; + transform: scale(0.95); } /* Toolbar button styling */ @@ -256,7 +144,7 @@ /* Print styles - hide interactive elements when printing */ @media print { - .note-status-dropdown, + .note-status-unified-dropdown, .note-status-bar, .note-status-pane { display: none !important; @@ -343,7 +231,6 @@ } .note-status-search-input, -.note-status-modal-search-input, .note-status-popover-search-input { width: 100%; padding: var(--input-padding); @@ -357,7 +244,6 @@ } .note-status-search-input:focus, -.note-status-modal-search-input:focus, .note-status-popover-search-input:focus { border-color: var(--interactive-accent); box-shadow: 0 0 0 2px var(--interactive-accent-hover); @@ -430,27 +316,27 @@ } /* Status Group Styling */ -.status-group { +.note-status-group { margin-bottom: var(--size-4-3, 12px); border-radius: var(--radius-s); background: var(--background-primary-alt); box-shadow: var(--shadow-xs); overflow: hidden; - animation: fadeInSlideDown 0.3s var(--ease-out); + animation: note-status-fade-in-slide-down 0.3s var(--ease-out); } -.status-group .nav-folder-title { +.note-status-group .nav-folder-title { cursor: pointer; padding: var(--size-4-1, 4px) var(--size-4-2, 8px); background: var(--background-secondary-alt); transition: background var(--status-transition-time) var(--ease-out); } -.status-group .nav-folder-title:hover { +.note-status-group .nav-folder-title:hover { background: var(--background-modifier-hover); } -.status-group .nav-folder-title-content { +.note-status-group .nav-folder-title-content { font-weight: var(--font-semibold); display: flex; align-items: center; @@ -458,7 +344,7 @@ } /* Collapse indicators */ -.collapse-indicator { +.note-status-collapse-indicator { margin-right: var(--size-4-2, 8px); display: flex; align-items: center; @@ -467,18 +353,18 @@ opacity: 1; } -.is-collapsed .collapse-indicator { +.note-status-is-collapsed .note-status-collapse-indicator { transform: rotate(-90deg); } /* File items */ -.status-group .nav-folder-children { +.note-status-group .nav-folder-children { padding: var(--size-4-1, 4px); background: var(--background-primary); transition: height var(--status-transition-time) var(--ease-out); } -.status-group.nav-folder.is-collapsed .nav-folder-children { +.note-status-group.nav-folder.note-status-is-collapsed .nav-folder-children { display: none; } @@ -517,223 +403,25 @@ } /* Compact view */ -.compact-view .nav-file-title { +.note-status-compact-view .nav-file-title { padding: 2px var(--size-4-2, 8px); font-size: var(--font-smaller); } -.compact-view .nav-folder-children { +.note-status-compact-view .nav-folder-children { padding: 0; } -.compact-view .nav-file { +.note-status-compact-view .nav-file { margin-bottom: 0; border-radius: 0; border-bottom: 1px solid var(--background-modifier-border); } -.compact-view .nav-file:last-child { +.note-status-compact-view .nav-file:last-child { border-bottom: none; } -/* - * Status Dropdown and Popover Styles - */ - -/* Status Dropdown */ -.note-status-dropdown { - padding: var(--size-4-2, 8px); - background: var(--background-primary); - position: sticky; - z-index: var(--layer-popover); - display: flex; - flex-direction: column; - gap: var(--size-4-2, 8px); - box-shadow: var(--shadow-s); - border-radius: var(--radius-m); - margin: 8px auto; - max-width: 720px; - border: 1px solid var(--background-modifier-border); - transition: opacity var(--status-transition-time) var(--ease-out), - transform var(--status-transition-time) var(--ease-out); -} - -.note-status-dropdown.note-status-animate-in { - animation: slideInFade 0.25s var(--ease-out); -} - -.note-status-dropdown.note-status-animate-out { - opacity: 0; - transform: translateY(-10px); - pointer-events: none; -} - -/* Header styling */ -.note-status-dropdown-header { - display: flex; - justify-content: space-between; - align-items: center; - padding-bottom: var(--size-4-1, 4px); - border-bottom: 1px solid var(--background-modifier-border); -} - -.note-status-dropdown-title { - display: flex; - align-items: center; - gap: var(--size-4-1, 4px); - font-weight: var(--font-semibold); - color: var(--text-normal); -} - -.note-status-dropdown-icon { - display: flex; - align-items: center; - justify-content: center; - color: var(--text-accent); -} - -.note-status-dropdown-actions { - display: flex; - align-items: center; - gap: var(--size-4-1, 4px); -} - -.note-status-action-button { - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - border-radius: var(--radius-s); - background: transparent; - color: var(--text-muted); - cursor: pointer; - transition: all 0.15s ease; - border: none; - padding: 0; -} - -.note-status-action-button:hover { - background-color: var(--background-modifier-hover); - color: var(--text-normal); -} - -.note-status-action-button.note-status-button-active { - background-color: var(--interactive-accent); - color: var(--text-on-accent); - transform: scale(0.95); -} - -/* Status chips container */ -.note-status-chips-container { - display: flex; - flex-wrap: wrap; - gap: var(--size-4-1, 4px); - padding: var(--size-4-1, 4px); - min-height: 36px; - background: var(--background-primary-alt); - border-radius: var(--radius-s); -} - -/* Status chips styling */ -.note-status-chip { - display: inline-flex; - align-items: center; - padding: 4px 10px; - background: var(--background-secondary); - border-radius: 16px; - box-shadow: var(--shadow-xs); - font-size: var(--font-smaller); - transition: all 0.15s var(--ease-out); - border: 1px solid var(--background-modifier-border); - max-width: 180px; - animation: scaleIn 0.2s var(--ease-out); -} - -.note-status-chip.clickable { - cursor: pointer; -} - -.note-status-chip.clickable:hover { - background: var(--background-modifier-hover); - transform: translateY(-1px); - box-shadow: var(--status-hover-shadow); -} - -.note-status-chip-removing { - transform: scale(0.8); - opacity: 0; - pointer-events: none; - transition: all 0.15s ease-out; -} - -.note-status-chip-icon { - margin-right: 6px; -} - -.note-status-chip-text { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.note-status-chip-remove { - margin-left: 6px; - width: 16px; - height: 16px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - background: var(--background-modifier-border); - color: var(--text-muted); - cursor: pointer; - transition: all 0.15s var(--ease-out); -} - -.note-status-chip-remove:hover { - background: var(--text-accent); - color: var(--text-on-accent); - transform: scale(1.1); -} - -/* Add status button */ -.note-status-add-button { - display: flex; - align-items: center; - padding: 8px 12px; - background: var(--background-secondary-alt); - border-radius: var(--radius-m); - margin-top: 4px; - cursor: pointer; - transition: all 0.15s var(--ease-out); - border: 1px dashed var(--background-modifier-border); -} - -.note-status-add-button:hover { - background: var(--background-modifier-hover); - box-shadow: var(--shadow-xs); - transform: translateY(-1px); -} - -.note-status-add-button:active { - transform: translateY(0px); - box-shadow: none; -} - -.note-status-add-icon { - display: flex; - align-items: center; - justify-content: center; - margin-right: 8px; - color: var(--text-accent); -} - -.note-status-add-text { - color: var(--text-muted); - font-size: var(--font-smaller); -} - /* Status popover */ .note-status-popover { position: absolute; @@ -834,7 +522,7 @@ .note-status-option-selecting { background-color: var(--interactive-accent) !important; color: var(--text-on-accent) !important; - animation: pulse 0.3s var(--ease-out); + animation: note-status-pulse 0.3s var(--ease-out); } .note-status-option-icon { @@ -870,11 +558,29 @@ /* Animation for unified dropdown */ .note-status-popover-animate-in { - animation: statusDropdownFadeIn 0.22s var(--ease-out) forwards; + animation: note-status-dropdown-fade-in 0.22s var(--ease-out) forwards; } -.note-status-popover-animate-out { - animation: statusDropdownFadeOut 0.22s var(--ease-out) forwards; +@keyframes note-status-dropdown-fade-in { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes note-status-dropdown-fade-out { + from { + opacity: 1; + transform: scale(1); + } + to { + opacity: 0; + transform: scale(0.95); + } } /* Popover header */ @@ -957,6 +663,7 @@ border-radius: var(--radius-s); transition: all 0.2s var(--ease-out); cursor: pointer; + height: 22px; /* Consistent height */ } .note-status-bar:hover { @@ -1010,131 +717,31 @@ opacity: 1; } -/* Additional styling for multiple status badges */ -.note-status-additional-badges { - display: flex; - align-items: center; - gap: 2px; - margin-left: 4px; -} - -.note-status-mini-badge { - font-size: var(--font-ui-smallest); +/* Status chips styling */ +.note-status-chip { display: inline-flex; align-items: center; - justify-content: center; - width: 16px; - height: 16px; - border-radius: 50%; + padding: 4px 10px; background: var(--background-secondary); + border-radius: 16px; box-shadow: var(--shadow-xs); + font-size: var(--font-smaller); + transition: all 0.15s var(--ease-out); + border: 1px solid var(--background-modifier-border); + max-width: 180px; + animation: note-status-scale-in 0.2s var(--ease-out); } -/* - * Context menu and toolbar styles - */ - -/* Context menu enhancements */ -.note-status-context-menu .menu-item { - display: flex; - align-items: center; +.note-status-chip.clickable { + cursor: pointer; } -.note-status-menu-header { - font-weight: var(--font-semibold); - border-bottom: 1px solid var(--background-modifier-border); - margin-bottom: 2px; - pointer-events: none !important; +.note-status-chip.clickable:hover { + background: var(--background-modifier-hover); + transform: translateY(-1px); + box-shadow: var(--status-hover-shadow); } -.note-status-menu-section { - font-size: var(--font-ui-smaller); - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--text-muted); - pointer-events: none !important; -} - -/* Batch modal styling */ -.note-status-batch-modal { - max-width: 550px; - display: flex; - flex-direction: column; - gap: 16px; -} - -.note-status-modal-header { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 8px; -} - -/* Status batch modal specific styles */ -.note-status-batch-menu { - z-index: 1000; -} - - - - - - - - - - -/* Positioning classes for dropdowns to avoid inline styles */ -.note-status-dummy-target { - position: fixed; - z-index: 1000; - width: 0; - height: 0; - left: var(--pos-x-px, 0); - top: var(--pos-y-px, 0); - pointer-events: none; -} - -.note-status-popover-fixed { - position: fixed; - z-index: 999; -} - -/* Use CSS custom properties with attr() for dynamic positioning */ -.note-status-popover-fixed { - --pos-x: 0; - --pos-y: 0; - --max-height: 300px; - left: calc(1px * attr(data-pos-x number, 0)); - top: calc(1px * attr(data-pos-y number, 0)); - max-height: var(--max-height); -} - -/* Since attr() with units doesn't have widespread support yet, - we'll add a fallback using CSS variables that can be updated via JS */ -.note-status-popover-fixed { - left: var(--pos-x-px, 0); - top: var(--pos-y-px, 0); - max-height: var(--max-height-px, 300px); -} - -/* Status dropdown and popover animation classes */ -.note-status-popover-animate-in { - animation: statusDropdownFadeIn 0.22s var(--ease-out) forwards; -} - -.note-status-popover-animate-out { - animation: statusDropdownFadeOut 0.22s var(--ease-out) forwards; -} - -/* Status Option during selection animation */ -.note-status-option-selecting { - background-color: var(--interactive-accent) !important; - color: var(--text-on-accent) !important; - animation: pulse 0.3s var(--ease-out); -} - -/* Chip animation for removing */ .note-status-chip-removing { transform: scale(0.8); opacity: 0; @@ -1142,77 +749,39 @@ transition: all 0.15s ease-out; } -/* Fallback positioning for right/bottom adjustments */ -.note-status-popover-right { - left: auto !important; - right: var(--right-offset-px, 10px); +.note-status-chip-icon { + margin-right: 6px; } -.note-status-popover-bottom { - top: auto !important; - bottom: var(--bottom-offset-px, 10px); +.note-status-chip-text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } -.status-color-dot { - display: inline-block; - width: 8px; - height: 8px; +.note-status-chip-remove { + margin-left: 6px; + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; border-radius: 50%; - background-color: var(--dot-color, #ffffff); - margin-right: 4px; -} - -.template-buttons { - margin-top: 15px; - display: flex; - gap: 10px; -} - -/* Template selection styling */ -.template-item { - border: 1px solid var(--background-modifier-border); - border-radius: var(--status-border-radius); - margin-bottom: 10px; - padding: 10px; - background: var(--background-primary-alt); -} - -.template-header { - display: flex; - align-items: center; - margin-bottom: 5px; -} - -.template-checkbox { - margin-right: 10px; -} - -.template-name { - font-weight: bold; -} - -.template-description { - font-size: 0.9em; + background: var(--background-modifier-border); color: var(--text-muted); - margin-bottom: 8px; + cursor: pointer; + transition: all 0.15s var(--ease-out); } -.template-statuses { - display: flex; - flex-wrap: wrap; - gap: 5px; +.note-status-chip-remove:hover { + background: var(--text-accent); + color: var(--text-on-accent); + transform: scale(1.1); } -.template-status-chip { - display: inline-flex; - align-items: center; - padding: 2px 8px; - border-radius: 12px; - background: var(--background-secondary); - font-size: 0.85em; -} - -/* Pagination styles */ +/* + * Pagination styles + */ .note-status-pagination { display: flex; justify-content: space-between; @@ -1278,39 +847,7 @@ } } -/* Improve status bar styles to fix any issues */ -.note-status-bar { - display: flex; - align-items: center; - gap: 6px; - padding: 4px 8px; - border-radius: var(--radius-s); - transition: all 0.2s var(--ease-out); - cursor: pointer; - height: 22px; /* Consistent height */ -} - -/* Make sure icon container doesn't disrupt layout */ -.note-status-icon-container { - display: inline-flex; - margin-left: 4px; - gap: 2px; - align-items: center; - flex-shrink: 0; - position: relative; /* Use relative positioning */ - z-index: 1; /* Keep on top */ -} - -/* Fix title content to ensure it's always visible */ -.nav-file-title-content { - flex: 1; - min-width: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - margin-right: 4px; /* Add margin to prevent overlap */ -} - +/* Empty message and show unassigned button */ .note-status-empty-message { margin-bottom: 12px; text-align: center; @@ -1345,4 +882,101 @@ .note-status-show-unassigned-button:active { transform: translateY(0); box-shadow: var(--shadow-xs); +} + +/* Positioning classes for dropdowns */ +.note-status-dummy-target { + position: fixed; + z-index: 1000; + width: 0; + height: 0; + left: var(--pos-x-px, 0); + top: var(--pos-y-px, 0); + pointer-events: none; +} + +.note-status-popover-fixed { + position: fixed; + z-index: 999; +} + +/* Use CSS custom properties for dynamic positioning */ +.note-status-popover-fixed { + --pos-x: 0; + --pos-y: 0; + --max-height: 300px; + left: var(--pos-x-px, 0); + top: var(--pos-y-px, 0); + max-height: var(--max-height-px, 300px); +} + +/* Position adjustment classes */ +.note-status-popover-right { + left: auto !important; + right: var(--right-offset-px, 10px); +} + +.note-status-popover-bottom { + top: auto !important; + bottom: var(--bottom-offset-px, 10px); +} + +/* Template styling */ +.status-color-dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + background-color: var(--dot-color, #ffffff); + margin-right: 4px; +} + +.template-buttons { + margin-top: 15px; + display: flex; + gap: 10px; +} + +/* Template selection styling */ +.template-item { + border: 1px solid var(--background-modifier-border); + border-radius: var(--status-border-radius); + margin-bottom: 10px; + padding: 10px; + background: var(--background-primary-alt); +} + +.template-header { + display: flex; + align-items: center; + margin-bottom: 5px; +} + +.template-checkbox { + margin-right: 10px; +} + +.template-name { + font-weight: bold; +} + +.template-description { + font-size: 0.9em; + color: var(--text-muted); + margin-bottom: 8px; +} + +.template-statuses { + display: flex; + flex-wrap: wrap; + gap: 5px; +} + +.template-status-chip { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border-radius: 12px; + background: var(--background-secondary); + font-size: 0.85em; } \ No newline at end of file diff --git a/ui/components/status-pane-view.ts b/ui/components/status-pane-view.ts index 2c98460..aa0446c 100644 --- a/ui/components/status-pane-view.ts +++ b/ui/components/status-pane-view.ts @@ -60,7 +60,7 @@ export class StatusPaneView extends View { this.createActionToolbar(headerContainer); // Set initial compact view state - containerEl.toggleClass('compact-view', this.settings.compactView); + containerEl.toggleClass('note-status-compact-view', this.settings.compactView); // Add a container for the groups const groupsContainer = containerEl.createDiv({ cls: 'note-status-groups-container' }); @@ -138,7 +138,7 @@ export class StatusPaneView extends View { // Trigger settings update window.dispatchEvent(new CustomEvent('note-status:settings-changed')); - this.containerEl.toggleClass('compact-view', this.settings.compactView); + this.containerEl.toggleClass('note-status-compact-view', this.settings.compactView); await this.renderGroups(this.searchInput?.value.toLowerCase() || ''); }); @@ -248,11 +248,11 @@ export class StatusPaneView extends View { } private renderStatusGroup(container: HTMLElement, status: string, files: TFile[]): void { - const groupEl = container.createDiv({ cls: 'status-group nav-folder' }); + const groupEl = container.createDiv({ cls: 'note-status-group nav-folder' }); const titleEl = groupEl.createDiv({ cls: 'nav-folder-title' }); // Create a container for the collapse button and title - const collapseContainer = titleEl.createDiv({ cls: 'collapse-indicator' }); + const collapseContainer = titleEl.createDiv({ cls: 'note-status-collapse-indicator' }); setIcon(collapseContainer, 'chevron-down'); // Create a container for the title content @@ -268,22 +268,22 @@ export class StatusPaneView extends View { const isCollapsed = this.settings.collapsedStatuses[status] ?? false; if (isCollapsed) { - groupEl.addClass('is-collapsed'); + groupEl.addClass('note-status-is-collapsed'); collapseContainer.empty(); setIcon(collapseContainer, 'chevron-right'); } titleEl.addEventListener('click', (e) => { e.preventDefault(); - const isCurrentlyCollapsed = groupEl.hasClass('is-collapsed'); + const isCurrentlyCollapsed = groupEl.hasClass('note-status-is-collapsed'); // Toggle the collapsed state if (isCurrentlyCollapsed) { - groupEl.removeClass('is-collapsed'); + groupEl.removeClass('note-status-is-collapsed'); collapseContainer.empty(); setIcon(collapseContainer, 'chevron-down'); } else { - groupEl.addClass('is-collapsed'); + groupEl.addClass('note-status-is-collapsed'); collapseContainer.empty(); setIcon(collapseContainer, 'chevron-right'); } @@ -444,7 +444,7 @@ export class StatusPaneView extends View { */ updateSettings(settings: NoteStatusSettings): void { this.settings = settings; - this.containerEl.toggleClass('compact-view', settings.compactView); + this.containerEl.toggleClass('note-status-compact-view', settings.compactView); // Refresh the view with the current search query this.renderGroups(this.searchInput?.value.toLowerCase() || ''); From 1ed5190b584d622f0a4f312ac557f058ac1982d7 Mon Sep 17 00:00:00 2001 From: Aleix Date: Mon, 12 May 2025 15:38:21 +0200 Subject: [PATCH 02/67] refactor: update esbuild.config.mjs with CSS bundling --- esbuild.config.mjs | 27 +- styles.css | 983 +----------------------------- styles/base.css | 70 +++ styles/components/dropdown.css | 250 ++++++++ styles/components/explorer.css | 58 ++ styles/components/settings.css | 62 ++ styles/components/status-bar.css | 64 ++ styles/components/status-pane.css | 313 ++++++++++ styles/index.css | 14 + styles/utils.css | 106 ++++ 10 files changed, 963 insertions(+), 984 deletions(-) create mode 100644 styles/base.css create mode 100644 styles/components/dropdown.css create mode 100644 styles/components/explorer.css create mode 100644 styles/components/settings.css create mode 100644 styles/components/status-bar.css create mode 100644 styles/components/status-pane.css create mode 100644 styles/index.css create mode 100644 styles/utils.css diff --git a/esbuild.config.mjs b/esbuild.config.mjs index a5de8b8..11eb737 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -11,6 +11,17 @@ if you want to view the source, please visit the github repository of this plugi const prod = (process.argv[2] === "production"); +// CSS bundling function +const buildStyles = async () => { + await esbuild.build({ + entryPoints: ["styles/index.css"], + bundle: true, + outfile: "styles.css", + minify: prod, + logLevel: "info", + }); +}; + const context = await esbuild.context({ banner: { js: banner, @@ -31,7 +42,8 @@ const context = await esbuild.context({ "@lezer/common", "@lezer/highlight", "@lezer/lr", - ...builtins], + ...builtins + ], format: "cjs", target: "es2018", logLevel: "info", @@ -41,9 +53,20 @@ const context = await esbuild.context({ minify: prod, }); +// CSS context for watch mode +const cssContext = await esbuild.context({ + entryPoints: ["styles/index.css"], + bundle: true, + outfile: "styles.css", + minify: prod, + logLevel: "info", +}); + if (prod) { await context.rebuild(); + await buildStyles(); // Build CSS in production mode process.exit(0); } else { await context.watch(); -} + await cssContext.watch(); // Watch CSS files in development mode +} \ No newline at end of file diff --git a/styles.css b/styles.css index 410bf45..4179f0b 100644 --- a/styles.css +++ b/styles.css @@ -1,982 +1 @@ -/* - * Note Status Plugin for Obsidian - * Main CSS file importing all components - * - * Author: Aleix Soler -*/ - -/* Base styles and variables */ -:root { - --status-transition-time: 0.22s; - --status-border-radius: var(--radius-s, 4px); - --status-box-shadow: var(--shadow-s, 0 2px 8px rgba(0, 0, 0, 0.15)); - --status-hover-shadow: var(--shadow-m, 0 4px 12px rgba(0, 0, 0, 0.2)); - --status-icon-size: 16px; - - /* Animation curves */ - --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); - --ease-out: cubic-bezier(0, 0, 0.2, 1); - --ease-in: cubic-bezier(0.4, 0, 1, 1); -} - -/* Animations */ -@keyframes note-status-scale-in { - from { - opacity: 0; - transform: scale(0.9); - } - to { - opacity: 1; - transform: scale(1); - } -} - -@keyframes note-status-slide-in-fade { - from { - opacity: 0; - transform: translateY(-10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes note-status-fade-in-slide-down { - from { - opacity: 0; - transform: translateY(-6px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes note-status-pulse { - 0% { transform: scale(1); } - 50% { transform: scale(0.98); } - 100% { transform: scale(1); } -} - -/* Common utility classes */ -.note-status-empty-indicator { - color: var(--text-muted); - font-style: italic; - padding: var(--size-4-1, 4px); - display: flex; - align-items: center; - justify-content: center; - color: var(--text-accent); - font-size: 1.2em; -} - -/* Action buttons in modal */ -.note-status-action-button { - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - border-radius: var(--radius-s); - background: transparent; - color: var(--text-muted); - cursor: pointer; - transition: all 0.15s ease; - border: none; - padding: 0; -} - -.note-status-action-button:hover { - background-color: var(--background-modifier-hover); - color: var(--text-normal); -} - -.note-status-action-button.note-status-button-active { - background-color: var(--interactive-accent); - color: var(--text-on-accent); - transform: scale(0.95); -} - -/* Toolbar button styling */ -.note-status-toolbar-badge-container { - display: flex; - align-items: center; - justify-content: center; - position: relative; -} - -.note-status-toolbar-icon { - color: var(--text-normal) !important; - display: flex !important; - align-items: center !important; - justify-content: center !important; -} - -/* Target all possible toolbar locations */ -.note-status-count-badge { - position: absolute; - top: -5px; - right: -8px; - background-color: var(--interactive-accent); - color: var(--text-on-accent); - border-radius: 10px; - font-size: 10px; - min-width: 14px; - height: 14px; - padding: 0 3px; - display: flex; - align-items: center; - justify-content: center; - font-weight: var(--font-bold); -} - -/* Toolbar popover styling */ -.note-status-toolbar-popover { - width: 280px; - max-width: 90vw; - border-radius: var(--radius-m); - overflow: hidden; - box-shadow: var(--shadow-m); - border: 1px solid var(--background-modifier-border); - background: var(--background-primary); -} - -/* Print styles - hide interactive elements when printing */ -@media print { - .note-status-unified-dropdown, - .note-status-bar, - .note-status-pane { - display: none !important; - } -} - -/* Dark mode adjustments */ -.theme-dark .note-status-popover { - box-shadow: var(--shadow-l); -} - -.theme-dark .note-status-option-selecting { - box-shadow: 0 0 0 1px var(--interactive-accent); -} - -/* High contrast improvements */ -@media (forced-colors: active) { - .note-status-chip, - .note-status-option.is-selected { - border: 1px solid currentColor; - } - - .note-status-action-button:focus-visible, - .note-status-chip:focus-visible, - .note-status-option:focus-visible { - outline: 2px solid currentColor; - outline-offset: 2px; - } -} - -/* Component styles */ -/* - * Status Pane Styles - */ - -/* Main container */ -.note-status-pane { - padding: var(--size-4-2, 8px); - background: var(--background-secondary); - color: var(--text-normal); - font-family: var(--font-interface); - overflow-y: auto; - height: 100%; - display: flex; - flex-direction: column; -} - -/* Header elements */ -.note-status-header { - position: sticky; - top: 0; - z-index: 10; - background: var(--background-secondary); - padding-bottom: var(--size-4-2, 8px); - border-bottom: 1px solid var(--background-modifier-border); - margin-bottom: var(--size-4-2, 8px); - display: flex; - flex-direction: column; - gap: var(--size-4-2, 8px); -} - -/* Search input styles */ -.note-status-search { - width: 100%; - position: relative; -} - -.search-input-wrapper, -.note-status-search-wrapper { - position: relative; - display: flex; - align-items: center; -} - -.search-input-icon, -.note-status-search-icon { - position: absolute; - left: var(--size-4-1, 4px); - color: var(--text-muted); - display: flex; - align-items: center; - pointer-events: none; - padding: 4px; -} - -.note-status-search-input, -.note-status-popover-search-input { - width: 100%; - padding: var(--input-padding); - padding-left: calc(var(--size-4-1, 4px) * 3 + 18px); - border: var(--input-border-width) solid var(--background-modifier-border); - border-radius: var(--input-radius); - background: var(--background-primary); - color: var(--text-normal); - outline: none; - transition: all var(--status-transition-time) var(--ease-out); -} - -.note-status-search-input:focus, -.note-status-popover-search-input:focus { - border-color: var(--interactive-accent); - box-shadow: 0 0 0 2px var(--interactive-accent-hover); -} - -.search-input-clear-button, -.note-status-search-clear-button { - opacity: 0; - position: absolute; - right: var(--size-4-1, 4px); - cursor: pointer; - color: var(--text-muted); - display: flex; - align-items: center; - transition: opacity var(--status-transition-time) var(--ease-out); - padding: 4px; - border-radius: 50%; -} - -.search-input-clear-button.is-visible, -.note-status-search-clear-button.is-visible { - opacity: 1; -} - -.search-input-clear-button:hover, -.note-status-search-clear-button:hover { - color: var(--text-normal); - background-color: var(--background-modifier-hover); -} - -/* Actions toolbar */ -.status-pane-actions-container { - display: flex; - gap: var(--size-4-2, 8px); - justify-content: flex-end; - margin-top: var(--size-4-1, 4px); -} - -.note-status-view-toggle, -.note-status-actions-refresh { - padding: var(--size-4-1, 4px) var(--size-4-2, 8px); - border: var(--input-border-width) solid var(--background-modifier-border); - border-radius: var(--button-radius); - background: var(--background-primary); - color: var(--text-normal); - cursor: pointer; - transition: all var(--status-transition-time) var(--ease-out); - display: flex; - align-items: center; - justify-content: center; -} - -.note-status-view-toggle:hover, -.note-status-actions-refresh:hover { - background: var(--background-modifier-hover); - box-shadow: var(--shadow-s); -} - -.note-status-view-toggle:active, -.note-status-actions-refresh:active { - transform: translateY(1px); - box-shadow: none; -} - -/* Status Groups Container */ -.note-status-groups-container { - flex: 1; - overflow-y: auto; - padding-right: 2px; /* Prevents scrollbar from hugging the edge */ -} - -/* Status Group Styling */ -.note-status-group { - margin-bottom: var(--size-4-3, 12px); - border-radius: var(--radius-s); - background: var(--background-primary-alt); - box-shadow: var(--shadow-xs); - overflow: hidden; - animation: note-status-fade-in-slide-down 0.3s var(--ease-out); -} - -.note-status-group .nav-folder-title { - cursor: pointer; - padding: var(--size-4-1, 4px) var(--size-4-2, 8px); - background: var(--background-secondary-alt); - transition: background var(--status-transition-time) var(--ease-out); -} - -.note-status-group .nav-folder-title:hover { - background: var(--background-modifier-hover); -} - -.note-status-group .nav-folder-title-content { - font-weight: var(--font-semibold); - display: flex; - align-items: center; - gap: var(--size-4-1, 4px); -} - -/* Collapse indicators */ -.note-status-collapse-indicator { - margin-right: var(--size-4-2, 8px); - display: flex; - align-items: center; - color: var(--text-muted); - transition: transform var(--status-transition-time) var(--ease-in-out); - opacity: 1; -} - -.note-status-is-collapsed .note-status-collapse-indicator { - transform: rotate(-90deg); -} - -/* File items */ -.note-status-group .nav-folder-children { - padding: var(--size-4-1, 4px); - background: var(--background-primary); - transition: height var(--status-transition-time) var(--ease-out); -} - -.note-status-group.nav-folder.note-status-is-collapsed .nav-folder-children { - display: none; -} - -.nav-file { - border-radius: var(--radius-s); - transition: background var(--status-transition-time) var(--ease-out); - margin-bottom: 2px; - position: relative; -} - -.nav-file:hover { - background: var(--background-modifier-hover); -} - -.nav-file-title { - display: flex; - align-items: center; - padding: var(--size-4-1, 4px) var(--size-4-2, 8px); - flex-wrap: nowrap; -} - -.nav-file-icon { - color: var(--text-muted); - margin-right: var(--size-4-2, 8px); - display: flex; - align-items: center; - flex-shrink: 0; -} - -.nav-file-title-content { - flex: 1; - min-width: 0; /* Allows text to shrink properly */ - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -/* Compact view */ -.note-status-compact-view .nav-file-title { - padding: 2px var(--size-4-2, 8px); - font-size: var(--font-smaller); -} - -.note-status-compact-view .nav-folder-children { - padding: 0; -} - -.note-status-compact-view .nav-file { - margin-bottom: 0; - border-radius: 0; - border-bottom: 1px solid var(--background-modifier-border); -} - -.note-status-compact-view .nav-file:last-child { - border-bottom: none; -} - -/* Status popover */ -.note-status-popover { - position: absolute; - background: var(--background-primary); - border-radius: var(--radius-m); - box-shadow: var(--status-box-shadow); - z-index: calc(var(--layer-popover) + 10); - max-height: 300px; - display: flex; - flex-direction: column; - border: 1px solid var(--background-modifier-border); - min-width: 200px; - overflow: hidden; - width: auto !important; - opacity: 0; - transform: scale(0.95); - transition: opacity var(--status-transition-time) var(--ease-out), - transform var(--status-transition-time) var(--ease-out); - transform-origin: top left; - pointer-events: all !important; -} - -.note-status-popover.note-status-popover-animate-in { - opacity: 1; - transform: scale(1); -} - -.note-status-popover.note-status-popover-animate-out { - opacity: 0; - transform: scale(0.95); - pointer-events: none; -} - -/* Search container */ -.note-status-popover-search { - padding: 8px; - position: sticky; - top: 0; - background: var(--background-primary); - z-index: 3; - border-bottom: 1px solid var(--background-modifier-border); -} - -/* Status options container */ -.note-status-options-container { - overflow-y: auto; - overflow-x: hidden; - max-height: 250px; - padding: var(--size-4-1, 4px); - scrollbar-width: thin; - scrollbar-color: var(--background-modifier-border) transparent; -} - -.note-status-options-container::-webkit-scrollbar { - width: 6px; -} - -.note-status-options-container::-webkit-scrollbar-thumb { - background-color: var(--background-modifier-border); - border-radius: 3px; -} - -.note-status-options-container::-webkit-scrollbar-track { - background-color: transparent; -} - -/* Empty status options */ -.note-status-empty-options { - padding: 16px 12px; - text-align: center; - color: var(--text-muted); - font-style: italic; - font-size: var(--font-smaller); -} - -/* Status option items */ -.note-status-option { - display: flex; - align-items: center; - padding: 8px 12px; - margin: 2px 4px; - border-radius: var(--radius-s); - background: var(--background-primary); - color: var(--text-normal); - cursor: pointer !important; - transition: background-color 0.15s ease; -} - -.note-status-option:hover { - background: var(--background-modifier-hover); -} - -.note-status-option.is-selected { - background: var(--background-secondary-alt); - font-weight: var(--font-medium); -} - -.note-status-option-selecting { - background-color: var(--interactive-accent) !important; - color: var(--text-on-accent) !important; - animation: note-status-pulse 0.3s var(--ease-out); -} - -.note-status-option-icon { - margin-right: 8px; - font-size: 1.1em; -} - -.note-status-option-text { - flex: 1; - font-size: var(--font-smaller); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.note-status-option-check { - margin-left: auto; - color: var(--text-accent); -} - -/* Unified dropdown */ -.note-status-unified-dropdown { - width: 280px; - max-width: 90vw; - border-radius: var(--radius-m); - overflow: hidden; - box-shadow: var(--shadow-m); - border: 1px solid var(--background-modifier-border); - background: var(--background-primary); - z-index: 9999 !important; /* Ensure it's above everything */ - pointer-events: all !important; /* Ensure dropdown receives clicks */ -} - -/* Animation for unified dropdown */ -.note-status-popover-animate-in { - animation: note-status-dropdown-fade-in 0.22s var(--ease-out) forwards; -} - -@keyframes note-status-dropdown-fade-in { - from { - opacity: 0; - transform: scale(0.95); - } - to { - opacity: 1; - transform: scale(1); - } -} - -@keyframes note-status-dropdown-fade-out { - from { - opacity: 1; - transform: scale(1); - } - to { - opacity: 0; - transform: scale(0.95); - } -} - -/* Popover header */ -.note-status-popover-header { - padding: 12px; - border-bottom: 1px solid var(--background-modifier-border); - background: var(--background-secondary-alt); - display: flex; - align-items: center; - position: sticky; - top: 0; - z-index: 2; -} - -.note-status-popover-title { - display: flex; - align-items: center; - gap: 8px; - font-weight: var(--font-semibold); - color: var(--text-normal); -} - -.note-status-popover-icon { - display: flex; - align-items: center; - justify-content: center; - color: var(--text-accent); -} - -.note-status-popover-chips { - display: flex; - flex-wrap: wrap; - gap: 6px; - padding: 12px; - background: var(--background-primary-alt); - max-height: 120px; - overflow-y: auto; - border-bottom: 1px solid var(--background-modifier-border); -} - -/* - * Explorer integration styles - */ - -/* Explorer icons container */ -.note-status-icon-container { - display: inline-flex; - margin-left: var(--size-4-1, 4px); - gap: 2px; - align-items: center; - flex-shrink: 0; -} - -/* Individual status icons in explorer */ -.note-status-icon { - font-size: var(--font-ui-smaller); - display: inline-flex; - align-items: center; - justify-content: center; - width: var(--status-icon-size); - height: var(--status-icon-size); - position: relative; - z-index: 500; /* Higher than most elements */ -} - -.nav-file-title .note-status-icon::after { - bottom: 150%; /* Position above in file explorer */ -} - -/* - * Status bar styles - */ - -/* Status bar refinements */ -.note-status-bar { - display: flex; - align-items: center; - gap: 6px; - padding: 4px 8px; - border-radius: var(--radius-s); - transition: all 0.2s var(--ease-out); - cursor: pointer; - height: 22px; /* Consistent height */ -} - -.note-status-bar:hover { - background-color: var(--background-modifier-hover); -} - -.note-status-badges { - display: flex; - align-items: center; - gap: 4px; -} - -.note-status-badge { - display: flex; - align-items: center; - padding: 2px 6px; - border-radius: 10px; - background: var(--background-secondary-alt); - font-size: var(--font-ui-smaller); - max-width: 100px; - margin-right: 2px; -} - -.note-status-badge-icon { - margin-right: 2px; -} - -.note-status-badge-text { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -/* Status bar positioning and visibility */ -.status-bar .note-status-icon::after { - bottom: auto; - top: -30px; -} - -/* Auto-hide behavior */ -.note-status-bar.hidden { - display: none; -} - -.note-status-bar.auto-hide { - opacity: 0.5; - transition: opacity 0.2s var(--ease-out); -} - -.note-status-bar.visible { - opacity: 1; -} - -/* Status chips styling */ -.note-status-chip { - display: inline-flex; - align-items: center; - padding: 4px 10px; - background: var(--background-secondary); - border-radius: 16px; - box-shadow: var(--shadow-xs); - font-size: var(--font-smaller); - transition: all 0.15s var(--ease-out); - border: 1px solid var(--background-modifier-border); - max-width: 180px; - animation: note-status-scale-in 0.2s var(--ease-out); -} - -.note-status-chip.clickable { - cursor: pointer; -} - -.note-status-chip.clickable:hover { - background: var(--background-modifier-hover); - transform: translateY(-1px); - box-shadow: var(--status-hover-shadow); -} - -.note-status-chip-removing { - transform: scale(0.8); - opacity: 0; - pointer-events: none; - transition: all 0.15s ease-out; -} - -.note-status-chip-icon { - margin-right: 6px; -} - -.note-status-chip-text { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.note-status-chip-remove { - margin-left: 6px; - width: 16px; - height: 16px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - background: var(--background-modifier-border); - color: var(--text-muted); - cursor: pointer; - transition: all 0.15s var(--ease-out); -} - -.note-status-chip-remove:hover { - background: var(--text-accent); - color: var(--text-on-accent); - transform: scale(1.1); -} - -/* - * Pagination styles - */ -.note-status-pagination { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px; - border-top: 1px solid var(--background-modifier-border); - margin-top: 8px; -} - -.note-status-pagination-button { - padding: 4px 8px; - border-radius: var(--radius-s); - background: var(--background-secondary); - border: var(--input-border-width) solid var(--background-modifier-border); - color: var(--text-normal); - cursor: pointer; - transition: all 0.15s var(--ease-out); - font-size: var(--font-smaller); -} - -.note-status-pagination-button:hover { - background: var(--background-modifier-hover); - box-shadow: var(--shadow-s); -} - -.note-status-pagination-info { - font-size: var(--font-smaller); - color: var(--text-muted); -} - -/* Loading indicator */ -.note-status-loading { - display: flex; - justify-content: center; - align-items: center; - padding: 20px; - color: var(--text-muted); - font-style: italic; -} - -.note-status-loading span { - position: relative; - padding-left: 24px; -} - -.note-status-loading span:before { - content: ''; - position: absolute; - left: 0; - top: 50%; - width: 16px; - height: 16px; - margin-top: -8px; - border: 2px solid var(--background-modifier-border); - border-top-color: var(--text-accent); - border-radius: 50%; - animation: note-status-loading-spinner 0.8s linear infinite; -} - -@keyframes note-status-loading-spinner { - to { - transform: rotate(360deg); - } -} - -/* Empty message and show unassigned button */ -.note-status-empty-message { - margin-bottom: 12px; - text-align: center; - color: var(--text-muted); - font-style: italic; -} - -.note-status-button-container { - display: flex; - justify-content: center; - margin-top: 16px; -} - -.note-status-show-unassigned-button { - background-color: var(--interactive-accent); - color: var(--text-on-accent); - padding: 8px 16px; - border-radius: var(--radius-s, 4px); - border: none; - cursor: pointer; - font-size: var(--font-ui-small); - transition: all 0.2s var(--ease-out); - box-shadow: var(--shadow-s); -} - -.note-status-show-unassigned-button:hover { - background-color: var(--interactive-accent-hover); - transform: translateY(-1px); - box-shadow: var(--shadow-m); -} - -.note-status-show-unassigned-button:active { - transform: translateY(0); - box-shadow: var(--shadow-xs); -} - -/* Positioning classes for dropdowns */ -.note-status-dummy-target { - position: fixed; - z-index: 1000; - width: 0; - height: 0; - left: var(--pos-x-px, 0); - top: var(--pos-y-px, 0); - pointer-events: none; -} - -.note-status-popover-fixed { - position: fixed; - z-index: 999; -} - -/* Use CSS custom properties for dynamic positioning */ -.note-status-popover-fixed { - --pos-x: 0; - --pos-y: 0; - --max-height: 300px; - left: var(--pos-x-px, 0); - top: var(--pos-y-px, 0); - max-height: var(--max-height-px, 300px); -} - -/* Position adjustment classes */ -.note-status-popover-right { - left: auto !important; - right: var(--right-offset-px, 10px); -} - -.note-status-popover-bottom { - top: auto !important; - bottom: var(--bottom-offset-px, 10px); -} - -/* Template styling */ -.status-color-dot { - display: inline-block; - width: 8px; - height: 8px; - border-radius: 50%; - background-color: var(--dot-color, #ffffff); - margin-right: 4px; -} - -.template-buttons { - margin-top: 15px; - display: flex; - gap: 10px; -} - -/* Template selection styling */ -.template-item { - border: 1px solid var(--background-modifier-border); - border-radius: var(--status-border-radius); - margin-bottom: 10px; - padding: 10px; - background: var(--background-primary-alt); -} - -.template-header { - display: flex; - align-items: center; - margin-bottom: 5px; -} - -.template-checkbox { - margin-right: 10px; -} - -.template-name { - font-weight: bold; -} - -.template-description { - font-size: 0.9em; - color: var(--text-muted); - margin-bottom: 8px; -} - -.template-statuses { - display: flex; - flex-wrap: wrap; - gap: 5px; -} - -.template-status-chip { - display: inline-flex; - align-items: center; - padding: 2px 8px; - border-radius: 12px; - background: var(--background-secondary); - font-size: 0.85em; -} \ No newline at end of file +:root{--status-transition-time: .22s;--status-border-radius: var(--radius-s, 4px);--status-box-shadow: var(--shadow-s, 0 2px 8px rgba(0, 0, 0, .15));--status-hover-shadow: var(--shadow-m, 0 4px 12px rgba(0, 0, 0, .2));--status-icon-size: 16px;--ease-in-out: cubic-bezier(.4, 0, .2, 1);--ease-out: cubic-bezier(0, 0, .2, 1);--ease-in: cubic-bezier(.4, 0, 1, 1)}@keyframes note-status-scale-in{0%{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}}@keyframes note-status-slide-in-fade{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}@keyframes note-status-fade-in-slide-down{0%{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:translateY(0)}}@keyframes note-status-pulse{0%{transform:scale(1)}50%{transform:scale(.98)}to{transform:scale(1)}}.theme-dark .note-status-popover{box-shadow:var(--shadow-l)}.theme-dark .note-status-option-selecting{box-shadow:0 0 0 1px var(--interactive-accent)}@media (forced-colors: active){.note-status-chip,.note-status-option.is-selected{border:1px solid currentColor}.note-status-action-button:focus-visible,.note-status-chip:focus-visible,.note-status-option:focus-visible{outline:2px solid currentColor;outline-offset:2px}}@media print{.note-status-unified-dropdown,.note-status-bar,.note-status-pane{display:none!important}}.note-status-empty-indicator{color:var(--text-muted);font-style:italic;padding:var(--size-4-1, 4px);display:flex;align-items:center;justify-content:center;color:var(--text-accent);font-size:1.2em}.note-status-action-button{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:var(--radius-s);background:transparent;color:var(--text-muted);cursor:pointer;transition:all .15s ease;border:none;padding:0}.note-status-action-button:hover{background-color:var(--background-modifier-hover);color:var(--text-normal)}.note-status-action-button.note-status-button-active{background-color:var(--interactive-accent);color:var(--text-on-accent);transform:scale(.95)}.note-status-loading{display:flex;justify-content:center;align-items:center;padding:20px;color:var(--text-muted);font-style:italic}.note-status-loading span{position:relative;padding-left:24px}.note-status-loading span:before{content:"";position:absolute;left:0;top:50%;width:16px;height:16px;margin-top:-8px;border:2px solid var(--background-modifier-border);border-top-color:var(--text-accent);border-radius:50%;animation:note-status-loading-spinner .8s linear infinite}@keyframes note-status-loading-spinner{to{transform:rotate(360deg)}}.note-status-dummy-target{position:fixed;z-index:1000;width:0;height:0;left:var(--pos-x-px, 0);top:var(--pos-y-px, 0);pointer-events:none}.note-status-popover-fixed{position:fixed;z-index:999;--pos-x: 0;--pos-y: 0;--max-height: 300px;left:var(--pos-x-px, 0);top:var(--pos-y-px, 0);max-height:var(--max-height-px, 300px)}.note-status-popover-right{left:auto!important;right:var(--right-offset-px, 10px)}.note-status-popover-bottom{top:auto!important;bottom:var(--bottom-offset-px, 10px)}.note-status-bar{display:flex;align-items:center;gap:6px;padding:4px 8px;border-radius:var(--radius-s);transition:all .2s var(--ease-out);cursor:pointer;height:22px}.note-status-bar:hover{background-color:var(--background-modifier-hover)}.note-status-badges{display:flex;align-items:center;gap:4px}.note-status-badge{display:flex;align-items:center;padding:2px 6px;border-radius:10px;background:var(--background-secondary-alt);font-size:var(--font-ui-smaller);max-width:100px;margin-right:2px}.note-status-badge-icon{margin-right:2px}.note-status-badge-text{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.status-bar .note-status-icon:after{bottom:auto;top:-30px}.note-status-bar.hidden{display:none}.note-status-bar.auto-hide{opacity:.5;transition:opacity .2s var(--ease-out)}.note-status-bar.visible{opacity:1}.note-status-pane{padding:var(--size-4-2, 8px);background:var(--background-secondary);color:var(--text-normal);font-family:var(--font-interface);overflow-y:auto;height:100%;display:flex;flex-direction:column}.note-status-header{position:sticky;top:0;z-index:10;background:var(--background-secondary);padding-bottom:var(--size-4-2, 8px);border-bottom:1px solid var(--background-modifier-border);margin-bottom:var(--size-4-2, 8px);display:flex;flex-direction:column;gap:var(--size-4-2, 8px)}.note-status-search{width:100%;position:relative}.search-input-wrapper,.note-status-search-wrapper{position:relative;display:flex;align-items:center}.search-input-icon,.note-status-search-icon{position:absolute;left:var(--size-4-1, 4px);color:var(--text-muted);display:flex;align-items:center;pointer-events:none;padding:4px}.note-status-search-input,.note-status-popover-search-input{width:100%;padding:var(--input-padding);padding-left:calc(var(--size-4-1, 4px) * 3 + 18px);border:var(--input-border-width) solid var(--background-modifier-border);border-radius:var(--input-radius);background:var(--background-primary);color:var(--text-normal);outline:none;transition:all var(--status-transition-time) var(--ease-out)}.note-status-search-input:focus,.note-status-popover-search-input:focus{border-color:var(--interactive-accent);box-shadow:0 0 0 2px var(--interactive-accent-hover)}.search-input-clear-button,.note-status-search-clear-button{opacity:0;position:absolute;right:var(--size-4-1, 4px);cursor:pointer;color:var(--text-muted);display:flex;align-items:center;transition:opacity var(--status-transition-time) var(--ease-out);padding:4px;border-radius:50%}.search-input-clear-button.is-visible,.note-status-search-clear-button.is-visible{opacity:1}.search-input-clear-button:hover,.note-status-search-clear-button:hover{color:var(--text-normal);background-color:var(--background-modifier-hover)}.status-pane-actions-container{display:flex;gap:var(--size-4-2, 8px);justify-content:flex-end;margin-top:var(--size-4-1, 4px)}.note-status-view-toggle,.note-status-actions-refresh{padding:var(--size-4-1, 4px) var(--size-4-2, 8px);border:var(--input-border-width) solid var(--background-modifier-border);border-radius:var(--button-radius);background:var(--background-primary);color:var(--text-normal);cursor:pointer;transition:all var(--status-transition-time) var(--ease-out);display:flex;align-items:center;justify-content:center}.note-status-view-toggle:hover,.note-status-actions-refresh:hover{background:var(--background-modifier-hover);box-shadow:var(--shadow-s)}.note-status-view-toggle:active,.note-status-actions-refresh:active{transform:translateY(1px);box-shadow:none}.note-status-groups-container{flex:1;overflow-y:auto;padding-right:2px}.note-status-group{margin-bottom:var(--size-4-3, 12px);border-radius:var(--radius-s);background:var(--background-primary-alt);box-shadow:var(--shadow-xs);overflow:hidden;animation:note-status-fade-in-slide-down .3s var(--ease-out)}.note-status-group .nav-folder-title{cursor:pointer;padding:var(--size-4-1, 4px) var(--size-4-2, 8px);background:var(--background-secondary-alt);transition:background var(--status-transition-time) var(--ease-out)}.note-status-group .nav-folder-title:hover{background:var(--background-modifier-hover)}.note-status-group .nav-folder-title-content{font-weight:var(--font-semibold);display:flex;align-items:center;gap:var(--size-4-1, 4px)}.note-status-collapse-indicator{margin-right:var(--size-4-2, 8px);display:flex;align-items:center;color:var(--text-muted);transition:transform var(--status-transition-time) var(--ease-in-out);opacity:1}.note-status-is-collapsed .note-status-collapse-indicator{transform:rotate(-90deg)}.note-status-group .nav-folder-children{padding:var(--size-4-1, 4px);background:var(--background-primary);transition:height var(--status-transition-time) var(--ease-out)}.note-status-group.nav-folder.note-status-is-collapsed .nav-folder-children{display:none}.nav-file{border-radius:var(--radius-s);transition:background var(--status-transition-time) var(--ease-out);margin-bottom:2px;position:relative}.nav-file:hover{background:var(--background-modifier-hover)}.nav-file-title{display:flex;align-items:center;padding:var(--size-4-1, 4px) var(--size-4-2, 8px);flex-wrap:nowrap}.nav-file-icon{color:var(--text-muted);margin-right:var(--size-4-2, 8px);display:flex;align-items:center;flex-shrink:0}.nav-file-title-content{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.note-status-compact-view .nav-file-title{padding:2px var(--size-4-2, 8px);font-size:var(--font-smaller)}.note-status-compact-view .nav-folder-children{padding:0}.note-status-compact-view .nav-file{margin-bottom:0;border-radius:0;border-bottom:1px solid var(--background-modifier-border)}.note-status-compact-view .nav-file:last-child{border-bottom:none}.note-status-empty-message{margin-bottom:12px;text-align:center;color:var(--text-muted);font-style:italic}.note-status-button-container{display:flex;justify-content:center;margin-top:16px}.note-status-show-unassigned-button{background-color:var(--interactive-accent);color:var(--text-on-accent);padding:8px 16px;border-radius:var(--radius-s, 4px);border:none;cursor:pointer;font-size:var(--font-ui-small);transition:all .2s var(--ease-out);box-shadow:var(--shadow-s)}.note-status-show-unassigned-button:hover{background-color:var(--interactive-accent-hover);transform:translateY(-1px);box-shadow:var(--shadow-m)}.note-status-show-unassigned-button:active{transform:translateY(0);box-shadow:var(--shadow-xs)}.note-status-pagination{display:flex;justify-content:space-between;align-items:center;padding:8px;border-top:1px solid var(--background-modifier-border);margin-top:8px}.note-status-pagination-button{padding:4px 8px;border-radius:var(--radius-s);background:var(--background-secondary);border:var(--input-border-width) solid var(--background-modifier-border);color:var(--text-normal);cursor:pointer;transition:all .15s var(--ease-out);font-size:var(--font-smaller)}.note-status-pagination-button:hover{background:var(--background-modifier-hover);box-shadow:var(--shadow-s)}.note-status-pagination-info{font-size:var(--font-smaller);color:var(--text-muted)}.note-status-popover{position:absolute;background:var(--background-primary);border-radius:var(--radius-m);box-shadow:var(--status-box-shadow);z-index:calc(var(--layer-popover) + 10);max-height:300px;display:flex;flex-direction:column;border:1px solid var(--background-modifier-border);min-width:200px;overflow:hidden;width:auto!important;opacity:0;transform:scale(.95);transition:opacity var(--status-transition-time) var(--ease-out),transform var(--status-transition-time) var(--ease-out);transform-origin:top left;pointer-events:all!important}.note-status-popover.note-status-popover-animate-in{opacity:1;transform:scale(1)}.note-status-popover.note-status-popover-animate-out{opacity:0;transform:scale(.95);pointer-events:none}.note-status-popover-search{padding:8px;position:sticky;top:0;background:var(--background-primary);z-index:3;border-bottom:1px solid var(--background-modifier-border)}.note-status-options-container{overflow-y:auto;overflow-x:hidden;max-height:250px;padding:var(--size-4-1, 4px);scrollbar-width:thin;scrollbar-color:var(--background-modifier-border) transparent}.note-status-options-container::-webkit-scrollbar{width:6px}.note-status-options-container::-webkit-scrollbar-thumb{background-color:var(--background-modifier-border);border-radius:3px}.note-status-options-container::-webkit-scrollbar-track{background-color:transparent}.note-status-empty-options{padding:16px 12px;text-align:center;color:var(--text-muted);font-style:italic;font-size:var(--font-smaller)}.note-status-option{display:flex;align-items:center;padding:8px 12px;margin:2px 4px;border-radius:var(--radius-s);background:var(--background-primary);color:var(--text-normal);cursor:pointer!important;transition:background-color .15s ease}.note-status-option:hover{background:var(--background-modifier-hover)}.note-status-option.is-selected{background:var(--background-secondary-alt);font-weight:var(--font-medium)}.note-status-option-selecting{background-color:var(--interactive-accent)!important;color:var(--text-on-accent)!important;animation:note-status-pulse .3s var(--ease-out)}.note-status-option-icon{margin-right:8px;font-size:1.1em}.note-status-option-text{flex:1;font-size:var(--font-smaller);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.note-status-option-check{margin-left:auto;color:var(--text-accent)}.note-status-unified-dropdown{width:280px;max-width:90vw;border-radius:var(--radius-m);overflow:hidden;box-shadow:var(--shadow-m);border:1px solid var(--background-modifier-border);background:var(--background-primary);z-index:9999!important;pointer-events:all!important}.note-status-popover-animate-in{animation:note-status-dropdown-fade-in .22s var(--ease-out) forwards}@keyframes note-status-dropdown-fade-in{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes note-status-dropdown-fade-out{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}}.note-status-popover-header{padding:12px;border-bottom:1px solid var(--background-modifier-border);background:var(--background-secondary-alt);display:flex;align-items:center;position:sticky;top:0;z-index:2}.note-status-popover-title{display:flex;align-items:center;gap:8px;font-weight:var(--font-semibold);color:var(--text-normal)}.note-status-popover-icon{display:flex;align-items:center;justify-content:center;color:var(--text-accent)}.note-status-popover-chips{display:flex;flex-wrap:wrap;gap:6px;padding:12px;background:var(--background-primary-alt);max-height:120px;overflow-y:auto;border-bottom:1px solid var(--background-modifier-border)}.note-status-chip{display:inline-flex;align-items:center;padding:4px 10px;background:var(--background-secondary);border-radius:16px;box-shadow:var(--shadow-xs);font-size:var(--font-smaller);transition:all .15s var(--ease-out);border:1px solid var(--background-modifier-border);max-width:180px;animation:note-status-scale-in .2s var(--ease-out)}.note-status-chip.clickable{cursor:pointer}.note-status-chip.clickable:hover{background:var(--background-modifier-hover);transform:translateY(-1px);box-shadow:var(--status-hover-shadow)}.note-status-chip-removing{transform:scale(.8);opacity:0;pointer-events:none;transition:all .15s ease-out}.note-status-chip-icon{margin-right:6px}.note-status-chip-text{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.note-status-chip-remove{margin-left:6px;width:16px;height:16px;display:flex;align-items:center;justify-content:center;border-radius:50%;background:var(--background-modifier-border);color:var(--text-muted);cursor:pointer;transition:all .15s var(--ease-out)}.note-status-chip-remove:hover{background:var(--text-accent);color:var(--text-on-accent);transform:scale(1.1)}.note-status-toolbar-badge-container{display:flex;align-items:center;justify-content:center;position:relative}.note-status-toolbar-icon{color:var(--text-normal)!important;display:flex!important;align-items:center!important;justify-content:center!important}.note-status-count-badge{position:absolute;top:-5px;right:-8px;background-color:var(--interactive-accent);color:var(--text-on-accent);border-radius:10px;font-size:10px;min-width:14px;height:14px;padding:0 3px;display:flex;align-items:center;justify-content:center;font-weight:var(--font-bold)}.note-status-icon-container{display:inline-flex;margin-left:var(--size-4-1, 4px);gap:2px;align-items:center;flex-shrink:0}.note-status-icon{font-size:var(--font-ui-smaller);display:inline-flex;align-items:center;justify-content:center;width:var(--status-icon-size);height:var(--status-icon-size);position:relative;z-index:500}.nav-file-title .note-status-icon:after{bottom:150%}.status-color-dot{display:inline-block;width:8px;height:8px;border-radius:50%;background-color:var(--dot-color, #ffffff);margin-right:4px}.template-buttons{margin-top:15px;display:flex;gap:10px}.template-item{border:1px solid var(--background-modifier-border);border-radius:var(--status-border-radius);margin-bottom:10px;padding:10px;background:var(--background-primary-alt)}.template-header{display:flex;align-items:center;margin-bottom:5px}.template-checkbox{margin-right:10px}.template-name{font-weight:700}.template-description{font-size:.9em;color:var(--text-muted);margin-bottom:8px}.template-statuses{display:flex;flex-wrap:wrap;gap:5px}.template-status-chip{display:inline-flex;align-items:center;padding:2px 8px;border-radius:12px;background:var(--background-secondary);font-size:.85em} diff --git a/styles/base.css b/styles/base.css new file mode 100644 index 0000000..1be0960 --- /dev/null +++ b/styles/base.css @@ -0,0 +1,70 @@ +/* + * Base styles and variables for Note Status Plugin + */ +:root { + --status-transition-time: 0.22s; + --status-border-radius: var(--radius-s, 4px); + --status-box-shadow: var(--shadow-s, 0 2px 8px rgba(0, 0, 0, 0.15)); + --status-hover-shadow: var(--shadow-m, 0 4px 12px rgba(0, 0, 0, 0.2)); + --status-icon-size: 16px; + + /* Animation curves */ + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); + --ease-out: cubic-bezier(0, 0, 0.2, 1); + --ease-in: cubic-bezier(0.4, 0, 1, 1); +} + +/* Animations */ +@keyframes note-status-scale-in { + from { opacity: 0; transform: scale(0.9); } + to { opacity: 1; transform: scale(1); } +} + +@keyframes note-status-slide-in-fade { + from { opacity: 0; transform: translateY(-10px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes note-status-fade-in-slide-down { + from { opacity: 0; transform: translateY(-6px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes note-status-pulse { + 0% { transform: scale(1); } + 50% { transform: scale(0.98); } + 100% { transform: scale(1); } +} + +/* Dark mode adjustments */ +.theme-dark .note-status-popover { + box-shadow: var(--shadow-l); +} + +.theme-dark .note-status-option-selecting { + box-shadow: 0 0 0 1px var(--interactive-accent); +} + +/* High contrast improvements */ +@media (forced-colors: active) { + .note-status-chip, + .note-status-option.is-selected { + border: 1px solid currentColor; + } + + .note-status-action-button:focus-visible, + .note-status-chip:focus-visible, + .note-status-option:focus-visible { + outline: 2px solid currentColor; + outline-offset: 2px; + } +} + +/* Print styles */ +@media print { + .note-status-unified-dropdown, + .note-status-bar, + .note-status-pane { + display: none !important; + } +} \ No newline at end of file diff --git a/styles/components/dropdown.css b/styles/components/dropdown.css new file mode 100644 index 0000000..e81413c --- /dev/null +++ b/styles/components/dropdown.css @@ -0,0 +1,250 @@ +/* + * Dropdown component styles + */ +.note-status-popover { + position: absolute; + background: var(--background-primary); + border-radius: var(--radius-m); + box-shadow: var(--status-box-shadow); + z-index: calc(var(--layer-popover) + 10); + max-height: 300px; + display: flex; + flex-direction: column; + border: 1px solid var(--background-modifier-border); + min-width: 200px; + overflow: hidden; + width: auto !important; + opacity: 0; + transform: scale(0.95); + transition: opacity var(--status-transition-time) var(--ease-out), + transform var(--status-transition-time) var(--ease-out); + transform-origin: top left; + pointer-events: all !important; +} + +.note-status-popover.note-status-popover-animate-in { + opacity: 1; + transform: scale(1); +} + +.note-status-popover.note-status-popover-animate-out { + opacity: 0; + transform: scale(0.95); + pointer-events: none; +} + +/* Search container */ +.note-status-popover-search { + padding: 8px; + position: sticky; + top: 0; + background: var(--background-primary); + z-index: 3; + border-bottom: 1px solid var(--background-modifier-border); +} + +/* Status options container */ +.note-status-options-container { + overflow-y: auto; + overflow-x: hidden; + max-height: 250px; + padding: var(--size-4-1, 4px); + scrollbar-width: thin; + scrollbar-color: var(--background-modifier-border) transparent; +} + +.note-status-options-container::-webkit-scrollbar { + width: 6px; +} + +.note-status-options-container::-webkit-scrollbar-thumb { + background-color: var(--background-modifier-border); + border-radius: 3px; +} + +.note-status-options-container::-webkit-scrollbar-track { + background-color: transparent; +} + +/* Empty status options */ +.note-status-empty-options { + padding: 16px 12px; + text-align: center; + color: var(--text-muted); + font-style: italic; + font-size: var(--font-smaller); +} + +/* Status option items */ +.note-status-option { + display: flex; + align-items: center; + padding: 8px 12px; + margin: 2px 4px; + border-radius: var(--radius-s); + background: var(--background-primary); + color: var(--text-normal); + cursor: pointer !important; + transition: background-color 0.15s ease; +} + +.note-status-option:hover { + background: var(--background-modifier-hover); +} + +.note-status-option.is-selected { + background: var(--background-secondary-alt); + font-weight: var(--font-medium); +} + +.note-status-option-selecting { + background-color: var(--interactive-accent) !important; + color: var(--text-on-accent) !important; + animation: note-status-pulse 0.3s var(--ease-out); +} + +.note-status-option-icon { + margin-right: 8px; + font-size: 1.1em; +} + +.note-status-option-text { + flex: 1; + font-size: var(--font-smaller); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.note-status-option-check { + margin-left: auto; + color: var(--text-accent); +} + +/* Unified dropdown */ +.note-status-unified-dropdown { + width: 280px; + max-width: 90vw; + border-radius: var(--radius-m); + overflow: hidden; + box-shadow: var(--shadow-m); + border: 1px solid var(--background-modifier-border); + background: var(--background-primary); + z-index: 9999 !important; + pointer-events: all !important; +} + +/* Animation for unified dropdown */ +.note-status-popover-animate-in { + animation: note-status-dropdown-fade-in 0.22s var(--ease-out) forwards; +} + +@keyframes note-status-dropdown-fade-in { + from { opacity: 0; transform: scale(0.95); } + to { opacity: 1; transform: scale(1); } +} + +@keyframes note-status-dropdown-fade-out { + from { opacity: 1; transform: scale(1); } + to { opacity: 0; transform: scale(0.95); } +} + +/* Popover header */ +.note-status-popover-header { + padding: 12px; + border-bottom: 1px solid var(--background-modifier-border); + background: var(--background-secondary-alt); + display: flex; + align-items: center; + position: sticky; + top: 0; + z-index: 2; +} + +.note-status-popover-title { + display: flex; + align-items: center; + gap: 8px; + font-weight: var(--font-semibold); + color: var(--text-normal); +} + +.note-status-popover-icon { + display: flex; + align-items: center; + justify-content: center; + color: var(--text-accent); +} + +.note-status-popover-chips { + display: flex; + flex-wrap: wrap; + gap: 6px; + padding: 12px; + background: var(--background-primary-alt); + max-height: 120px; + overflow-y: auto; + border-bottom: 1px solid var(--background-modifier-border); +} + +/* Status chips styling */ +.note-status-chip { + display: inline-flex; + align-items: center; + padding: 4px 10px; + background: var(--background-secondary); + border-radius: 16px; + box-shadow: var(--shadow-xs); + font-size: var(--font-smaller); + transition: all 0.15s var(--ease-out); + border: 1px solid var(--background-modifier-border); + max-width: 180px; + animation: note-status-scale-in 0.2s var(--ease-out); +} + +.note-status-chip.clickable { + cursor: pointer; +} + +.note-status-chip.clickable:hover { + background: var(--background-modifier-hover); + transform: translateY(-1px); + box-shadow: var(--status-hover-shadow); +} + +.note-status-chip-removing { + transform: scale(0.8); + opacity: 0; + pointer-events: none; + transition: all 0.15s ease-out; +} + +.note-status-chip-icon { + margin-right: 6px; +} + +.note-status-chip-text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.note-status-chip-remove { + margin-left: 6px; + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + background: var(--background-modifier-border); + color: var(--text-muted); + cursor: pointer; + transition: all 0.15s var(--ease-out); +} + +.note-status-chip-remove:hover { + background: var(--text-accent); + color: var(--text-on-accent); + transform: scale(1.1); +} \ No newline at end of file diff --git a/styles/components/explorer.css b/styles/components/explorer.css new file mode 100644 index 0000000..6b008ad --- /dev/null +++ b/styles/components/explorer.css @@ -0,0 +1,58 @@ +/* + * Explorer integration styles + */ +.note-status-toolbar-badge-container { + display: flex; + align-items: center; + justify-content: center; + position: relative; +} + +.note-status-toolbar-icon { + color: var(--text-normal) !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; +} + +.note-status-count-badge { + position: absolute; + top: -5px; + right: -8px; + background-color: var(--interactive-accent); + color: var(--text-on-accent); + border-radius: 10px; + font-size: 10px; + min-width: 14px; + height: 14px; + padding: 0 3px; + display: flex; + align-items: center; + justify-content: center; + font-weight: var(--font-bold); +} + +/* Explorer icons container */ +.note-status-icon-container { + display: inline-flex; + margin-left: var(--size-4-1, 4px); + gap: 2px; + align-items: center; + flex-shrink: 0; +} + +/* Individual status icons in explorer */ +.note-status-icon { + font-size: var(--font-ui-smaller); + display: inline-flex; + align-items: center; + justify-content: center; + width: var(--status-icon-size); + height: var(--status-icon-size); + position: relative; + z-index: 500; /* Higher than most elements */ +} + +.nav-file-title .note-status-icon::after { + bottom: 150%; /* Position above in file explorer */ +} \ No newline at end of file diff --git a/styles/components/settings.css b/styles/components/settings.css new file mode 100644 index 0000000..705f93a --- /dev/null +++ b/styles/components/settings.css @@ -0,0 +1,62 @@ +/* + * Settings styles + */ +/* Template styling */ +.status-color-dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + background-color: var(--dot-color, #ffffff); + margin-right: 4px; +} + +.template-buttons { + margin-top: 15px; + display: flex; + gap: 10px; +} + +/* Template selection styling */ +.template-item { + border: 1px solid var(--background-modifier-border); + border-radius: var(--status-border-radius); + margin-bottom: 10px; + padding: 10px; + background: var(--background-primary-alt); +} + +.template-header { + display: flex; + align-items: center; + margin-bottom: 5px; +} + +.template-checkbox { + margin-right: 10px; +} + +.template-name { + font-weight: bold; +} + +.template-description { + font-size: 0.9em; + color: var(--text-muted); + margin-bottom: 8px; +} + +.template-statuses { + display: flex; + flex-wrap: wrap; + gap: 5px; +} + +.template-status-chip { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border-radius: 12px; + background: var(--background-secondary); + font-size: 0.85em; +} \ No newline at end of file diff --git a/styles/components/status-bar.css b/styles/components/status-bar.css new file mode 100644 index 0000000..faa63f1 --- /dev/null +++ b/styles/components/status-bar.css @@ -0,0 +1,64 @@ +/* + * Status bar styles + */ +.note-status-bar { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + border-radius: var(--radius-s); + transition: all 0.2s var(--ease-out); + cursor: pointer; + height: 22px; +} + +.note-status-bar:hover { + background-color: var(--background-modifier-hover); +} + +.note-status-badges { + display: flex; + align-items: center; + gap: 4px; +} + +.note-status-badge { + display: flex; + align-items: center; + padding: 2px 6px; + border-radius: 10px; + background: var(--background-secondary-alt); + font-size: var(--font-ui-smaller); + max-width: 100px; + margin-right: 2px; +} + +.note-status-badge-icon { + margin-right: 2px; +} + +.note-status-badge-text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Status bar positioning and visibility */ +.status-bar .note-status-icon::after { + bottom: auto; + top: -30px; +} + +/* Auto-hide behavior */ +.note-status-bar.hidden { + display: none; +} + +.note-status-bar.auto-hide { + opacity: 0.5; + transition: opacity 0.2s var(--ease-out); +} + +.note-status-bar.visible { + opacity: 1; +} \ No newline at end of file diff --git a/styles/components/status-pane.css b/styles/components/status-pane.css new file mode 100644 index 0000000..e4db2cc --- /dev/null +++ b/styles/components/status-pane.css @@ -0,0 +1,313 @@ +/* + * Status Pane Styles + */ +.note-status-pane { + padding: var(--size-4-2, 8px); + background: var(--background-secondary); + color: var(--text-normal); + font-family: var(--font-interface); + overflow-y: auto; + height: 100%; + display: flex; + flex-direction: column; +} + +/* Header elements */ +.note-status-header { + position: sticky; + top: 0; + z-index: 10; + background: var(--background-secondary); + padding-bottom: var(--size-4-2, 8px); + border-bottom: 1px solid var(--background-modifier-border); + margin-bottom: var(--size-4-2, 8px); + display: flex; + flex-direction: column; + gap: var(--size-4-2, 8px); +} + +/* Search input styles */ +.note-status-search { + width: 100%; + position: relative; +} + +.search-input-wrapper, +.note-status-search-wrapper { + position: relative; + display: flex; + align-items: center; +} + +.search-input-icon, +.note-status-search-icon { + position: absolute; + left: var(--size-4-1, 4px); + color: var(--text-muted); + display: flex; + align-items: center; + pointer-events: none; + padding: 4px; +} + +.note-status-search-input, +.note-status-popover-search-input { + width: 100%; + padding: var(--input-padding); + padding-left: calc(var(--size-4-1, 4px) * 3 + 18px); + border: var(--input-border-width) solid var(--background-modifier-border); + border-radius: var(--input-radius); + background: var(--background-primary); + color: var(--text-normal); + outline: none; + transition: all var(--status-transition-time) var(--ease-out); +} + +.note-status-search-input:focus, +.note-status-popover-search-input:focus { + border-color: var(--interactive-accent); + box-shadow: 0 0 0 2px var(--interactive-accent-hover); +} + +.search-input-clear-button, +.note-status-search-clear-button { + opacity: 0; + position: absolute; + right: var(--size-4-1, 4px); + cursor: pointer; + color: var(--text-muted); + display: flex; + align-items: center; + transition: opacity var(--status-transition-time) var(--ease-out); + padding: 4px; + border-radius: 50%; +} + +.search-input-clear-button.is-visible, +.note-status-search-clear-button.is-visible { + opacity: 1; +} + +.search-input-clear-button:hover, +.note-status-search-clear-button:hover { + color: var(--text-normal); + background-color: var(--background-modifier-hover); +} + +/* Actions toolbar */ +.status-pane-actions-container { + display: flex; + gap: var(--size-4-2, 8px); + justify-content: flex-end; + margin-top: var(--size-4-1, 4px); +} + +.note-status-view-toggle, +.note-status-actions-refresh { + padding: var(--size-4-1, 4px) var(--size-4-2, 8px); + border: var(--input-border-width) solid var(--background-modifier-border); + border-radius: var(--button-radius); + background: var(--background-primary); + color: var(--text-normal); + cursor: pointer; + transition: all var(--status-transition-time) var(--ease-out); + display: flex; + align-items: center; + justify-content: center; +} + +.note-status-view-toggle:hover, +.note-status-actions-refresh:hover { + background: var(--background-modifier-hover); + box-shadow: var(--shadow-s); +} + +.note-status-view-toggle:active, +.note-status-actions-refresh:active { + transform: translateY(1px); + box-shadow: none; +} + +/* Status Groups Container */ +.note-status-groups-container { + flex: 1; + overflow-y: auto; + padding-right: 2px; /* Prevents scrollbar from hugging the edge */ +} + +/* Status Group Styling */ +.note-status-group { + margin-bottom: var(--size-4-3, 12px); + border-radius: var(--radius-s); + background: var(--background-primary-alt); + box-shadow: var(--shadow-xs); + overflow: hidden; + animation: note-status-fade-in-slide-down 0.3s var(--ease-out); +} + +.note-status-group .nav-folder-title { + cursor: pointer; + padding: var(--size-4-1, 4px) var(--size-4-2, 8px); + background: var(--background-secondary-alt); + transition: background var(--status-transition-time) var(--ease-out); +} + +.note-status-group .nav-folder-title:hover { + background: var(--background-modifier-hover); +} + +.note-status-group .nav-folder-title-content { + font-weight: var(--font-semibold); + display: flex; + align-items: center; + gap: var(--size-4-1, 4px); +} + +/* Collapse indicators */ +.note-status-collapse-indicator { + margin-right: var(--size-4-2, 8px); + display: flex; + align-items: center; + color: var(--text-muted); + transition: transform var(--status-transition-time) var(--ease-in-out); + opacity: 1; +} + +.note-status-is-collapsed .note-status-collapse-indicator { + transform: rotate(-90deg); +} + +/* File items */ +.note-status-group .nav-folder-children { + padding: var(--size-4-1, 4px); + background: var(--background-primary); + transition: height var(--status-transition-time) var(--ease-out); +} + +.note-status-group.nav-folder.note-status-is-collapsed .nav-folder-children { + display: none; +} + +.nav-file { + border-radius: var(--radius-s); + transition: background var(--status-transition-time) var(--ease-out); + margin-bottom: 2px; + position: relative; +} + +.nav-file:hover { + background: var(--background-modifier-hover); +} + +.nav-file-title { + display: flex; + align-items: center; + padding: var(--size-4-1, 4px) var(--size-4-2, 8px); + flex-wrap: nowrap; +} + +.nav-file-icon { + color: var(--text-muted); + margin-right: var(--size-4-2, 8px); + display: flex; + align-items: center; + flex-shrink: 0; +} + +.nav-file-title-content { + flex: 1; + min-width: 0; /* Allows text to shrink properly */ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Compact view */ +.note-status-compact-view .nav-file-title { + padding: 2px var(--size-4-2, 8px); + font-size: var(--font-smaller); +} + +.note-status-compact-view .nav-folder-children { + padding: 0; +} + +.note-status-compact-view .nav-file { + margin-bottom: 0; + border-radius: 0; + border-bottom: 1px solid var(--background-modifier-border); +} + +.note-status-compact-view .nav-file:last-child { + border-bottom: none; +} + +/* Empty message and show unassigned button */ +.note-status-empty-message { + margin-bottom: 12px; + text-align: center; + color: var(--text-muted); + font-style: italic; +} + +.note-status-button-container { + display: flex; + justify-content: center; + margin-top: 16px; +} + +.note-status-show-unassigned-button { + background-color: var(--interactive-accent); + color: var(--text-on-accent); + padding: 8px 16px; + border-radius: var(--radius-s, 4px); + border: none; + cursor: pointer; + font-size: var(--font-ui-small); + transition: all 0.2s var(--ease-out); + box-shadow: var(--shadow-s); +} + +.note-status-show-unassigned-button:hover { + background-color: var(--interactive-accent-hover); + transform: translateY(-1px); + box-shadow: var(--shadow-m); +} + +.note-status-show-unassigned-button:active { + transform: translateY(0); + box-shadow: var(--shadow-xs); +} + +/* + * Pagination styles + */ +.note-status-pagination { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px; + border-top: 1px solid var(--background-modifier-border); + margin-top: 8px; +} + +.note-status-pagination-button { + padding: 4px 8px; + border-radius: var(--radius-s); + background: var(--background-secondary); + border: var(--input-border-width) solid var(--background-modifier-border); + color: var(--text-normal); + cursor: pointer; + transition: all 0.15s var(--ease-out); + font-size: var(--font-smaller); +} + +.note-status-pagination-button:hover { + background: var(--background-modifier-hover); + box-shadow: var(--shadow-s); +} + +.note-status-pagination-info { + font-size: var(--font-smaller); + color: var(--text-muted); +} \ No newline at end of file diff --git a/styles/index.css b/styles/index.css new file mode 100644 index 0000000..48f5275 --- /dev/null +++ b/styles/index.css @@ -0,0 +1,14 @@ +/* + * Note Status Plugin for Obsidian + * Main CSS file importing all components + * + * Author: Aleix Soler + */ + +@import 'base.css'; +@import 'utils.css'; +@import 'components/status-bar.css'; +@import 'components/status-pane.css'; +@import 'components/dropdown.css'; +@import 'components/explorer.css'; +@import 'components/settings.css'; \ No newline at end of file diff --git a/styles/utils.css b/styles/utils.css new file mode 100644 index 0000000..9d3496b --- /dev/null +++ b/styles/utils.css @@ -0,0 +1,106 @@ +/* + * Utility classes for Note Status Plugin + */ +.note-status-empty-indicator { + color: var(--text-muted); + font-style: italic; + padding: var(--size-4-1, 4px); + display: flex; + align-items: center; + justify-content: center; + color: var(--text-accent); + font-size: 1.2em; +} + +/* Action buttons in modal */ +.note-status-action-button { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: var(--radius-s); + background: transparent; + color: var(--text-muted); + cursor: pointer; + transition: all 0.15s ease; + border: none; + padding: 0; +} + +.note-status-action-button:hover { + background-color: var(--background-modifier-hover); + color: var(--text-normal); +} + +.note-status-action-button.note-status-button-active { + background-color: var(--interactive-accent); + color: var(--text-on-accent); + transform: scale(0.95); +} + +/* Loading indicator */ +.note-status-loading { + display: flex; + justify-content: center; + align-items: center; + padding: 20px; + color: var(--text-muted); + font-style: italic; +} + +.note-status-loading span { + position: relative; + padding-left: 24px; +} + +.note-status-loading span:before { + content: ''; + position: absolute; + left: 0; + top: 50%; + width: 16px; + height: 16px; + margin-top: -8px; + border: 2px solid var(--background-modifier-border); + border-top-color: var(--text-accent); + border-radius: 50%; + animation: note-status-loading-spinner 0.8s linear infinite; +} + +@keyframes note-status-loading-spinner { + to { transform: rotate(360deg); } +} + +/* Positioning classes for dropdowns */ +.note-status-dummy-target { + position: fixed; + z-index: 1000; + width: 0; + height: 0; + left: var(--pos-x-px, 0); + top: var(--pos-y-px, 0); + pointer-events: none; +} + +.note-status-popover-fixed { + position: fixed; + z-index: 999; + --pos-x: 0; + --pos-y: 0; + --max-height: 300px; + left: var(--pos-x-px, 0); + top: var(--pos-y-px, 0); + max-height: var(--max-height-px, 300px); +} + +/* Position adjustment classes */ +.note-status-popover-right { + left: auto !important; + right: var(--right-offset-px, 10px); +} + +.note-status-popover-bottom { + top: auto !important; + bottom: var(--bottom-offset-px, 10px); +} \ No newline at end of file From 4b098ff89729e232571b9ee5fb7f445d6ec086c1 Mon Sep 17 00:00:00 2001 From: Aleix Date: Mon, 12 May 2025 15:43:53 +0200 Subject: [PATCH 03/67] chore: update refactor --- README.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e13cb47..8afa876 100644 --- a/README.md +++ b/README.md @@ -223,13 +223,35 @@ note-status/ │ │ └── file-utils.ts # File-related helpers │ └── settings/ # Settings UI │ └── settings-tab.ts # Settings tab definition -└── styles.css # CSS styles +├── styles/ # Modular CSS files +│ ├── base.css # Variables and base styles +│ ├── utils.css # Utility classes +│ ├── components/ # Component-specific styles +│ │ ├── status-bar.css # Status bar styles +│ │ ├── status-pane.css # Status pane styles +│ │ ├── dropdown.css # Dropdown component styles +│ │ ├── explorer.css # File explorer styles +│ │ └── settings.css # Settings UI styles +│ └── index.css # Main CSS import file +└── styles.css # Auto-generated final CSS ``` ### Prerequisites - Node.js and npm installed. - Obsidian API knowledge (TypeScript-based). +### CSS Modularization +The plugin's CSS has been modularized to improve maintainability: + +- **Component-based**: Each UI component has its own CSS file +- **Auto-bundling**: The build process automatically bundles all CSS files into `styles.css` +- **Development workflow**: CSS changes are hot-reloaded during development + +When making CSS changes: +1. Modify the appropriate file in the `styles/` directory +2. For new components, create a dedicated CSS file and import it in `styles/index.css` +3. The build process will automatically bundle everything into `styles.css` + ### Building the Plugin 1. Clone this repository: ```bash From 6eed3ea10c8c7324c2079358f86a1db912b658d0 Mon Sep 17 00:00:00 2001 From: Aleix Date: Mon, 12 May 2025 16:02:30 +0200 Subject: [PATCH 04/67] refactor: simplify code structure and improve maintainability --- main.ts | 1342 +++++++++++------------ services/status-service.ts | 31 +- ui/components/status-bar.ts | 197 ++-- ui/components/status-dropdown.ts | 1085 +++++++++--------- ui/integrations/explorer-integration.ts | 120 +- 5 files changed, 1294 insertions(+), 1481 deletions(-) diff --git a/main.ts b/main.ts index bbe0189..663096c 100644 --- a/main.ts +++ b/main.ts @@ -2,7 +2,7 @@ import { Editor, MarkdownView, Notice, Plugin, TFile, addIcon, WorkspaceLeaf, de // Constants import { ICONS } from './constants/icons'; -import { DEFAULT_SETTINGS, DEFAULT_COLORS } from './constants/defaults'; +import { DEFAULT_SETTINGS } from './constants/defaults'; // Types import { NoteStatusSettings } from './models/types'; @@ -25,729 +25,617 @@ import { NoteStatusSettingTab } from './settings/settings-tab'; * Main plugin class for Note Status functionality */ export default class NoteStatus extends Plugin { - private boundSaveSettings: () => Promise; - private boundCheckNoteStatus: () => void; - private boundRefreshDropdown: () => void; - private boundRefreshUI: () => void; - - settings: NoteStatusSettings; - statusService: StatusService; - styleService: StyleService; - statusBar: StatusBar; - statusDropdown: StatusDropdown; - explorerIntegration: ExplorerIntegration; - statusContextMenu: StatusContextMenu; - private statusPaneLeaf: WorkspaceLeaf | null = null; - private lastActiveFile: TFile | null = null; - - // Debounced methods for better performance - private debouncedCheckNoteStatus = debounce( - () => this.checkNoteStatus(), - 100, - true - ); - - private debouncedUpdateExplorer = debounce( - () => this.explorerIntegration?.updateAllFileExplorerIcons(), - 150 - ); - - private debouncedUpdateStatusPane = debounce( - () => this.updateStatusPane(), - 200 - ); - - private hasShownErrorNotification = false; - - async onload() { - try { - // Load settings first before initializing other components - await this.loadSettings(); - - this.boundSaveSettings = this.saveSettings.bind(this); - this.boundCheckNoteStatus = this.checkNoteStatus.bind(this); - this.boundRefreshDropdown = () => this.statusDropdown?.render(); - this.boundRefreshUI = () => this.checkNoteStatus(); - - // Register icons and essential services first - this.registerIcons(); - this.statusService = new StatusService(this.app, this.settings); - this.styleService = new StyleService(this.settings); - - // Register views and commands right away - this.registerViews(); - this.registerCommands(); - - // Add settings tab - this.addSettingTab(new NoteStatusSettingTab(this.app, this)); - - // Set up custom events early - this.setupCustomEvents(); - - // Delay UI-heavy initialization until the layout is ready - this.app.workspace.onLayoutReady(async () => { - // Split initialization into phases for better performance - await this.initializeUIComponents(); - }); - } catch (error) { - console.error('Error loading Note Status plugin:', error); - new Notice('Error loading Note Status plugin. Check console for details.'); - } - } - - private async initializeUIComponents(): Promise { - // Initialize UI components - this.statusBar = new StatusBar(this.addStatusBarItem(), this.settings, this.statusService); - this.statusDropdown = new StatusDropdown(this.app, this.settings, this.statusService); - - // Initialize explorer integration with a slight delay - setTimeout(() => { - this.explorerIntegration = new ExplorerIntegration(this.app, this.settings, this.statusService); - this.statusContextMenu = new StatusContextMenu( - this.app, - this.settings, - this.statusService, - this.statusDropdown, - this.explorerIntegration - ); - - // Register events after explorer integration is ready - this.registerMenuHandlers(); - this.registerEvents(); - - // Check status for active file only initially - this.checkNoteStatus(); - - // Update only active file icon initially for better performance - const activeFile = this.app.workspace.getActiveFile(); - if (activeFile && this.settings.showStatusIconsInExplorer) { - this.explorerIntegration.updateFileExplorerIcons(activeFile); - } - - // Delay full explorer icon update to avoid startup lag - if (this.settings.showStatusIconsInExplorer) { - setTimeout(() => { - this.explorerIntegration.updateAllFileExplorerIcons(); - }, 2000); - } - }, 300); - } - - /** - * Initialize the plugin components - */ - private async initialize(): Promise { - // Register custom icons - this.registerIcons(); - - // Initialize services - this.initializeServices(); - - // Initialize UI components - this.initializeUI(); - - // Register views, commands, and events - this.registerViews(); - this.registerCommands(); - this.registerMenuHandlers(); - this.registerEvents(); - - // Set up custom events - this.setupCustomEvents(); - - // Add settings tab - this.addSettingTab(new NoteStatusSettingTab(this.app, this)); - - // Initialize UI on layout ready - this.app.workspace.onLayoutReady(async () => { - await this.onLayoutReady(); - }); - } - - /** - * Handle layout ready event for final initialization steps - */ - private async onLayoutReady(): Promise { - // Small delay to ensure everything is loaded - await new Promise(resolve => setTimeout(resolve, 200)); - this.checkNoteStatus(); - this.statusDropdown.update(this.getCurrentStatuses()); - - this.explorerIntegration.updateAllFileExplorerIcons(); - - // Add additional retry with delay to ensure icons are updated - setTimeout(() => { - this.explorerIntegration.updateAllFileExplorerIcons(); - }, 2000); - } - - /** - * Register custom icons - */ - private registerIcons(): void { - Object.entries(ICONS).forEach(([name, svg]) => { - // Create a parser and get a DOM element for the SVG - const parser = new DOMParser(); - const svgDoc = parser.parseFromString(svg, 'image/svg+xml'); - const svgElement = svgDoc.documentElement; - - // Get the SVG as a string using XMLSerializer - const serializer = new XMLSerializer(); - const sanitizedSvg = serializer.serializeToString(svgElement); - - if (name === 'statusPane') { - addIcon('status-pane', sanitizedSvg); - } else { - addIcon(`note-status-${name}`, sanitizedSvg); - } - }); - } - - /** - * Register views for status pane - */ - private registerViews(): void { - this.registerView('status-pane', (leaf) => { - this.statusPaneLeaf = leaf; - return new StatusPaneView(leaf, this); - }); - - // Add ribbon icon - this.addRibbonIcon('status-pane', 'Open status pane', () => { - this.openStatusPane(); - }); - } - - /** - * Initialize plugin services - */ - private initializeServices(): void { - // These services depend on settings being loaded first - this.statusService = new StatusService(this.app, this.settings); - this.styleService = new StyleService(this.settings); - } - - /** - * Initialize UI components - */ - private initializeUI(): void { - this.statusBar = new StatusBar(this.addStatusBarItem(), this.settings, this.statusService); - this.statusDropdown = new StatusDropdown(this.app, this.settings, this.statusService); - this.explorerIntegration = new ExplorerIntegration(this.app, this.settings, this.statusService); - this.statusContextMenu = new StatusContextMenu(this.app, this.settings, this.statusService, this.statusDropdown, this.explorerIntegration); - } - - /** - * Set up custom events for status changes and UI updates - */ - private setupCustomEvents(): void { - // Listen for settings changes - window.addEventListener('note-status:settings-changed', this.boundSaveSettings); - - - // 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 { - const statuses = e.detail?.statuses || ['unknown']; - this.statusBar.update(statuses); - this.statusDropdown.update(statuses); - - // Update explorer icons for the active file - const activeFile = this.app.workspace.getActiveFile(); - if (activeFile) { - this.explorerIntegration.updateFileExplorerIcons(activeFile); - } - - setTimeout(() => this.statusDropdown.render(), 50); - } catch (error) { - console.error('Error handling status change event:', error); - } - }); - - // Listen for refresh dropdown - window.addEventListener('note-status:refresh-dropdown', this.boundRefreshDropdown); - - // Listen for UI refresh - window.addEventListener('note-status:refresh-ui', this.boundRefreshUI); - } - - /** - * Register plugin commands - */ - private registerCommands(): void { - // Refresh status command - this.addCommand({ - id: 'refresh-status', - name: 'Refresh status', - callback: () => { - this.checkNoteStatus(); - new Notice('Note status refreshed!'); - } - }); - - this.addCommand({ - id: 'force-refresh-ui', - name: 'Force refresh user interface', - callback: () => this.forceRefreshUI() - }); - - - // Insert status metadata command - this.addCommand({ - id: 'insert-status-metadata', - name: 'Insert status metadata', - editorCallback: (editor: Editor, view: MarkdownView) => { - this.statusService.insertStatusMetadataInEditor(editor); - new Notice('Status metadata inserted'); - } - }); - - // Open status pane command - this.addCommand({ - id: 'open-status-pane', - name: 'Open status pane', - callback: () => this.openStatusPane() - }); - - // Add status to note command - this.addCommand({ - id: 'add-status-to-note', - name: 'Add status to current note', - callback: () => this.showAddStatusToNoteMenu() - }); - } - - /** - * Show menu for adding a status to the current note - */ - private showAddStatusToNoteMenu(): void { - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile || activeFile.extension !== 'md') { - new Notice('No markdown file is active'); - return; - } - - this.statusContextMenu.showForFile(activeFile, new MouseEvent('click')); - } - - /** - * Register menu handlers - */ - private registerMenuHandlers(): void { - // File explorer context menu - this.registerEvent( - this.app.workspace.on('file-menu', (menu, file, source) => { - if (source === 'file-explorer-context-menu' && file instanceof TFile && file.extension === 'md') { - menu.addItem((item) => - item - .setTitle('Change status') - .setIcon('tag') - .onClick(() => { - const selectedFiles = this.explorerIntegration.getSelectedFiles(); - if (selectedFiles.length > 1) { - this.statusContextMenu.showForFiles(selectedFiles); - } else { - this.statusContextMenu.showForFiles([file]); - } - }) - ); - } - }) - ); - - // Multiple files selection menu - this.registerEvent( - this.app.workspace.on('files-menu', (menu, files) => { - const mdFiles: TFile[] = []; - for (const file of files) { - if (file instanceof TFile && file.extension === 'md') { - mdFiles.push(file); - } - } - if (mdFiles.length > 0) { - menu.addItem((item) => - item - .setTitle('Change status') - .setIcon('tag') - .onClick(() => { - this.statusContextMenu.showForFiles(mdFiles); - }) - ); - } - }) - ); - - // Editor context menu - this.registerEvent( - this.app.workspace.on('editor-menu', (menu, editor, view) => { - if (view instanceof MarkdownView) { - menu.addItem((item) => - item - .setTitle('Change note status') - .setIcon('tag') - .onClick(() => this.statusDropdown.showInContextMenu(editor, view)) - ); - } - }) - ); - } - - /** - * Register workspace and vault events - */ - private registerEvents(): void { - // File open event with optimization to avoid redundant updates - this.registerEvent(this.app.workspace.on('file-open', (file) => { - if (file instanceof TFile) { - // First make sure the button exists - this.statusDropdown.addToolbarButtonToActiveLeaf(); - // Then check status and update button - this.checkNoteStatus(); - } - })); - - // Editor change event - debounced to avoid performance issues - this.registerEvent(this.app.workspace.on('editor-change', () => { - this.debouncedCheckNoteStatus(); - })); - - // Add this for toolbar button persistence - this.registerEvent( - this.app.workspace.on('active-leaf-change', () => { - this.statusDropdown.addToolbarButtonToActiveLeaf(); - // Update the status after adding the button to ensure icon is correct - this.checkNoteStatus(); - }) - ); - - // Active leaf change event - optimized to avoid redundant updates - this.registerEvent(this.app.workspace.on('active-leaf-change', () => { - const activeFile = this.app.workspace.getActiveFile(); - - // Only update if the file actually changed - if (this.lastActiveFile?.path !== activeFile?.path) { - this.lastActiveFile = activeFile; - this.statusDropdown.update(this.getCurrentStatuses()); - this.debouncedUpdateStatusPane(); - } - })); - - this.registerFileEvents(); - this.registerMetadataEvents(); - - // Layout change event - ensure status pane is properly rendered - this.registerEvent(this.app.workspace.on('layout-change', () => { - this.debouncedUpdateStatusPane(); - })); - } - - /** - * Register file modification events - */ - private registerFileEvents(): void { - // File modification events with optimization - this.registerEvent(this.app.vault.on('modify', (file) => { - if (file instanceof TFile && file.extension === 'md') { - // Only update UI for the modified file - this.explorerIntegration.updateFileExplorerIcons(file); - - // If this is the active file, also update other UI elements - if (this.app.workspace.getActiveFile()?.path === file.path) { - this.checkNoteStatus(); - this.statusDropdown.update(this.getCurrentStatuses()); - } - - // Update the status pane but debounced - this.debouncedUpdateStatusPane(); - } - })); - - // File creation, deletion, and rename events - this.registerEvent(this.app.vault.on('create', (file) => { - if (file instanceof TFile && file.extension === 'md') { - this.explorerIntegration.updateFileExplorerIcons(file); - this.debouncedUpdateStatusPane(); - } - })); - - this.registerEvent(this.app.vault.on('delete', () => { - this.debouncedUpdateExplorer(); - this.debouncedUpdateStatusPane(); - })); - - this.registerEvent(this.app.vault.on('rename', (file) => { - if (file instanceof TFile && file.extension === 'md') { - this.explorerIntegration.updateAllFileExplorerIcons(); - this.debouncedUpdateStatusPane(); - } - })); - } - - /** - * Register metadata cache events - */ - private registerMetadataEvents(): void { - // Metadata change events - this.registerEvent(this.app.metadataCache.on('changed', (file) => { - if (file instanceof TFile && file.extension === 'md') { - this.explorerIntegration.updateFileExplorerIcons(file); - - // Update other UI elements if this is the active file - if (this.app.workspace.getActiveFile()?.path === file.path) { - this.checkNoteStatus(); - this.statusDropdown.update(this.getCurrentStatuses()); - } - - this.debouncedUpdateStatusPane(); - } - })); - - // Metadata resolved event - when all files are indexed - this.registerEvent( - this.app.metadataCache.on('resolved', () => { - // When metadata cache is fully resolved, update all icons - setTimeout(() => this.explorerIntegration.updateAllFileExplorerIcons(), 500); - }) - ); - } - - /** - * Check and update the status display for the active file - */ - checkNoteStatus(): void { - try { - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile || !(activeFile instanceof TFile) || activeFile.extension !== 'md') { - this.statusBar.update(['unknown']); - this.statusDropdown.update(['unknown']); - return; - } - - const statuses = this.statusService.getFileStatuses(activeFile); - this.statusBar.update(statuses); - this.statusDropdown.update(statuses); - } catch (error) { - console.error('Error checking note status:', error); - if (!this.hasShownErrorNotification) { - new Notice('Error checking note status. Check console for details.'); - this.hasShownErrorNotification = true; - setTimeout(() => { this.hasShownErrorNotification = false; }, 10000); - } - } - } - - /** - * Get the current statuses for the active file - * Always returns an array of status names - */ - getCurrentStatuses(): string[] { - try { - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile || !(activeFile instanceof TFile) || activeFile.extension !== 'md') { - return ['unknown']; - } - return this.statusService.getFileStatuses(activeFile); - } catch (error) { - console.error('Error getting current statuses:', error); - return ['unknown']; - } - } - - /** - * Open the status pane - */ - public async openStatusPane(): Promise { - try { - // Check if already open - const existing = this.app.workspace.getLeavesOfType('status-pane')[0]; - if (existing) { - this.app.workspace.setActiveLeaf(existing); - return; - } - - // Create a new leaf and show loading state - const leaf = this.app.workspace.getLeftLeaf(false); - if (leaf) { - await leaf.setViewState({ type: 'status-pane', active: true }); - this.statusPaneLeaf = leaf; - } - } catch (error) { - console.error('Error opening status pane:', error); - new Notice('Error opening status pane. Check console for details.'); - } - } - - /** - * Show status context menu - */ - showStatusContextMenu(files: TFile[]): void { - try { - this.statusContextMenu.showForFiles(files); - } catch (error) { - console.error('Error showing status context menu:', error); - new Notice('Error showing status context menu. Check console for details.'); - } - } - - /** - * Update the status pane - */ - async updateStatusPane(): Promise { - try { - 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); - } - } catch (error) { - console.error('Error updating status pane:', error); - } - } - - /** - * Load settings with error handling - */ - async loadSettings(): Promise { - try { - const loadedData = await this.loadData(); - this.settings = Object.assign({}, DEFAULT_SETTINGS, loadedData); - this.initializeDefaultColors(); - } catch (error) { - console.error('Error loading settings:', error); - new Notice('Error loading settings. Using defaults. Check console for details.'); - this.settings = Object.assign({}, DEFAULT_SETTINGS); - this.initializeDefaultColors(); - } - } - - /** - * Initialize default colors - */ - private initializeDefaultColors(): void { - // Ensure default colors are set for all statuses - for (const [status, color] of Object.entries(DEFAULT_COLORS)) { - if (!this.settings.statusColors[status]) { - this.settings.statusColors[status] = color; - } - } - - // Also initialize colors from enabled templates if not present - this.initializeTemplateColors(); - } - - /** - * Initialize colors from templates - */ - private initializeTemplateColors(): void { - // Make sure settings are properly initialized - if (!this.settings) { - console.error('Settings not initialized properly'); - return; - } - - if (!this.settings.useCustomStatusesOnly && this.statusService) { - const templateStatuses = this.statusService.getTemplateStatuses(); - for (const status of templateStatuses) { - if (status.color && !this.settings.statusColors[status.name]) { - this.settings.statusColors[status.name] = status.color; - } - } - } - } - - /** - * Save settings with error handling - */ - async saveSettings(): Promise { - try { - await this.saveData(this.settings); - this.updateComponentSettings(); - } catch (error) { - console.error('Error saving settings:', error); - new Notice('Error saving settings. Check console for details.'); - } - } - - /** - * Update all components with new settings - */ - private updateComponentSettings(): void { - // Update services with new settings - this.statusService.updateSettings(this.settings); - this.styleService.updateSettings(this.settings); - - // Update UI components with new settings - this.statusBar.updateSettings(this.settings); - this.statusDropdown.updateSettings(this.settings); - this.explorerIntegration.updateSettings(this.settings); - this.statusContextMenu.updateSettings(this.settings); - - // Update status pane if open - if (this.statusPaneLeaf && this.statusPaneLeaf.view instanceof StatusPaneView) { - (this.statusPaneLeaf.view as StatusPaneView).updateSettings(this.settings); - } - } - - /** - * Force refresh all UI components - */ - 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 - */ - onunload(): void { - // Cancel debounced functions - this.debouncedCheckNoteStatus.cancel(); - this.debouncedUpdateExplorer.cancel(); - this.debouncedUpdateStatusPane.cancel(); - - // Clean up UI elements - this.statusBar.unload?.(); - this.statusDropdown.unload?.(); - this.explorerIntegration.unload?.(); - this.styleService.unload?.(); - - // Close status pane if open - const statusPane = this.app.workspace.getLeavesOfType('status-pane')[0]; - if (statusPane) statusPane.detach(); - - // Remove event listeners for custom events - this.removeCustomEventListeners(); - } - - /** - * Remove custom event listeners - */ - private removeCustomEventListeners(): void { - window.removeEventListener('note-status:settings-changed', this.boundSaveSettings); - window.removeEventListener('note-status:status-changed', this.boundCheckNoteStatus); - window.removeEventListener('note-status:refresh-dropdown', this.boundRefreshDropdown); - window.removeEventListener('note-status:refresh-ui', this.boundRefreshUI); - } -} + settings: NoteStatusSettings; + statusService: StatusService; + styleService: StyleService; + statusBar: StatusBar; + statusDropdown: StatusDropdown; + explorerIntegration: ExplorerIntegration; + statusContextMenu: StatusContextMenu; + + private statusPaneLeaf: WorkspaceLeaf | null = null; + private lastActiveFile: TFile | null = null; + private hasShownErrorNotification = false; + + // Event handlers bound methods + private boundSaveSettings = this.saveSettings.bind(this); + private boundCheckNoteStatus = this.checkNoteStatus.bind(this); + private boundRefreshDropdown = () => this.statusDropdown?.render(); + private boundRefreshUI = () => this.checkNoteStatus(); + + // Debounced methods for better performance + private debouncedCheckNoteStatus = debounce(() => this.checkNoteStatus(), 100, true); + private debouncedUpdateExplorer = debounce(() => this.explorerIntegration?.updateAllFileExplorerIcons(), 150); + private debouncedUpdateStatusPane = debounce(() => this.updateStatusPane(), 200); + + async onload() { + try { + await this.loadSettings(); + + // Register icons + this.registerIcons(); + + // Initialize services + this.statusService = new StatusService(this.app, this.settings); + this.styleService = new StyleService(this.settings); + + // Register views and commands + this.registerViews(); + this.registerCommands(); + + // Add settings tab + this.addSettingTab(new NoteStatusSettingTab(this.app, this)); + + // Set up custom events + this.setupCustomEvents(); + + // Initialize UI after layout is ready + this.app.workspace.onLayoutReady(() => { + this.initializeUI(); + }); + } catch (error) { + console.error('Error loading Note Status plugin:', error); + new Notice('Error loading Note Status plugin. Check console for details.'); + } + } + + /** + * Register custom icons + */ + private registerIcons(): void { + Object.entries(ICONS).forEach(([name, svg]) => { + const parser = new DOMParser(); + const svgDoc = parser.parseFromString(svg, 'image/svg+xml'); + const svgElement = svgDoc.documentElement; + + const serializer = new XMLSerializer(); + const sanitizedSvg = serializer.serializeToString(svgElement); + + if (name === 'statusPane') { + addIcon('status-pane', sanitizedSvg); + } else { + addIcon(`note-status-${name}`, sanitizedSvg); + } + }); + } + + /** + * Initialize UI components + */ + private initializeUI(): void { + // Init basic UI components + this.statusBar = new StatusBar(this.addStatusBarItem(), this.settings, this.statusService); + this.statusDropdown = new StatusDropdown(this.app, this.settings, this.statusService); + + // Initialize explorer integration with a slight delay to ensure UI elements are ready + setTimeout(() => { + this.explorerIntegration = new ExplorerIntegration(this.app, this.settings, this.statusService); + this.statusContextMenu = new StatusContextMenu( + this.app, + this.settings, + this.statusService, + this.statusDropdown, + this.explorerIntegration + ); + + // Register events + this.registerMenuHandlers(); + this.registerEvents(); + + // Check status for active file + this.checkNoteStatus(); + + // Update only active file icon initially for better performance + const activeFile = this.app.workspace.getActiveFile(); + if (activeFile && this.settings.showStatusIconsInExplorer) { + this.explorerIntegration.updateFileExplorerIcons(activeFile); + } + + // Delay full explorer icon update to avoid startup lag + if (this.settings.showStatusIconsInExplorer) { + setTimeout(() => { + this.explorerIntegration.updateAllFileExplorerIcons(); + }, 2000); + } + }, 300); + } + + /** + * Register views for status pane + */ + private registerViews(): void { + this.registerView('status-pane', (leaf) => { + this.statusPaneLeaf = leaf; + return new StatusPaneView(leaf, this); + }); + + // Add ribbon icon + this.addRibbonIcon('status-pane', 'Open status pane', () => { + this.openStatusPane(); + }); + } + + /** + * Set up custom events for status changes and UI updates + */ + private setupCustomEvents(): void { + // Listen for settings changes + window.addEventListener('note-status:settings-changed', this.boundSaveSettings); + + // 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 { + const statuses = e.detail?.statuses || ['unknown']; + this.statusBar.update(statuses); + this.statusDropdown.update(statuses); + + // Update explorer icons for the active file + const activeFile = this.app.workspace.getActiveFile(); + if (activeFile) { + this.explorerIntegration.updateFileExplorerIcons(activeFile); + } + + setTimeout(() => this.statusDropdown.render(), 50); + } catch (error) { + console.error('Error handling status change event:', error); + } + }); + + // Listen for refresh dropdown + window.addEventListener('note-status:refresh-dropdown', this.boundRefreshDropdown); + + // Listen for UI refresh + window.addEventListener('note-status:refresh-ui', this.boundRefreshUI); + } + + /** + * Register plugin commands + */ + private registerCommands(): void { + // Refresh status command + this.addCommand({ + id: 'refresh-status', + name: 'Refresh status', + callback: () => { + this.checkNoteStatus(); + new Notice('Note status refreshed!'); + } + }); + + this.addCommand({ + id: 'force-refresh-ui', + name: 'Force refresh user interface', + callback: () => this.forceRefreshUI() + }); + + // Insert status metadata command + this.addCommand({ + id: 'insert-status-metadata', + name: 'Insert status metadata', + editorCallback: (editor: Editor, view: MarkdownView) => { + this.statusService.insertStatusMetadataInEditor(editor); + new Notice('Status metadata inserted'); + } + }); + + // Open status pane command + this.addCommand({ + id: 'open-status-pane', + name: 'Open status pane', + callback: () => this.openStatusPane() + }); + + // Add status to note command + this.addCommand({ + id: 'add-status-to-note', + name: 'Add status to current note', + callback: () => this.showAddStatusToNoteMenu() + }); + } + + /** + * Show menu for adding a status to the current note + */ + private showAddStatusToNoteMenu(): void { + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile || activeFile.extension !== 'md') { + new Notice('No markdown file is active'); + return; + } + + this.statusContextMenu.showForFile(activeFile, new MouseEvent('click')); + } + + /** + * Register menu handlers + */ + private registerMenuHandlers(): void { + // File explorer context menu + this.registerEvent( + this.app.workspace.on('file-menu', (menu, file, source) => { + if (source === 'file-explorer-context-menu' && file instanceof TFile && file.extension === 'md') { + menu.addItem((item) => + item + .setTitle('Change status') + .setIcon('tag') + .onClick(() => { + const selectedFiles = this.explorerIntegration.getSelectedFiles(); + if (selectedFiles.length > 1) { + this.statusContextMenu.showForFiles(selectedFiles); + } else { + this.statusContextMenu.showForFiles([file]); + } + }) + ); + } + }) + ); + + // Multiple files selection menu + this.registerEvent( + this.app.workspace.on('files-menu', (menu, files) => { + const mdFiles: TFile[] = []; + for (const file of files) { + if (file instanceof TFile && file.extension === 'md') { + mdFiles.push(file); + } + } + if (mdFiles.length > 0) { + menu.addItem((item) => + item + .setTitle('Change status') + .setIcon('tag') + .onClick(() => { + this.statusContextMenu.showForFiles(mdFiles); + }) + ); + } + }) + ); + + // Editor context menu + this.registerEvent( + this.app.workspace.on('editor-menu', (menu, editor, view) => { + if (view instanceof MarkdownView) { + menu.addItem((item) => + item + .setTitle('Change note status') + .setIcon('tag') + .onClick(() => this.statusDropdown.showInContextMenu(editor, view)) + ); + } + }) + ); + } + + /** + * Register workspace and vault events + */ + private registerEvents(): void { + // Register file events + this.registerFileEvents(); + + // Register metadata events + this.registerMetadataEvents(); + + // File open event with optimization to avoid redundant updates + this.registerEvent(this.app.workspace.on('file-open', (file) => { + if (file instanceof TFile) { + // First make sure the button exists + this.statusDropdown.addToolbarButtonToActiveLeaf(); + // Then check status and update button + this.checkNoteStatus(); + } + })); + + // Editor change event - debounced to avoid performance issues + this.registerEvent(this.app.workspace.on('editor-change', () => { + this.debouncedCheckNoteStatus(); + })); + + // Add this for toolbar button persistence + this.registerEvent( + this.app.workspace.on('active-leaf-change', () => { + this.statusDropdown.addToolbarButtonToActiveLeaf(); + // Update the status after adding the button to ensure icon is correct + this.checkNoteStatus(); + }) + ); + + // Active leaf change event - optimized to avoid redundant updates + this.registerEvent(this.app.workspace.on('active-leaf-change', () => { + const activeFile = this.app.workspace.getActiveFile(); + + // Only update if the file actually changed + if (this.lastActiveFile?.path !== activeFile?.path) { + this.lastActiveFile = activeFile; + this.statusDropdown.update(this.getCurrentStatuses()); + this.debouncedUpdateStatusPane(); + } + })); + + // Layout change event - ensure status pane is properly rendered + this.registerEvent(this.app.workspace.on('layout-change', () => { + this.debouncedUpdateStatusPane(); + })); + } + + /** + * Register file modification events + */ + private registerFileEvents(): void { + // File modification events with optimization + this.registerEvent(this.app.vault.on('modify', (file) => { + if (file instanceof TFile && file.extension === 'md') { + // Only update UI for the modified file + this.explorerIntegration.updateFileExplorerIcons(file); + + // If this is the active file, also update other UI elements + if (this.app.workspace.getActiveFile()?.path === file.path) { + this.checkNoteStatus(); + this.statusDropdown.update(this.getCurrentStatuses()); + } + + // Update the status pane but debounced + this.debouncedUpdateStatusPane(); + } + })); + + // File creation, deletion, and rename events + this.registerEvent(this.app.vault.on('create', (file) => { + if (file instanceof TFile && file.extension === 'md') { + this.explorerIntegration.updateFileExplorerIcons(file); + this.debouncedUpdateStatusPane(); + } + })); + + this.registerEvent(this.app.vault.on('delete', () => { + this.debouncedUpdateExplorer(); + this.debouncedUpdateStatusPane(); + })); + + this.registerEvent(this.app.vault.on('rename', (file) => { + if (file instanceof TFile && file.extension === 'md') { + this.explorerIntegration.updateAllFileExplorerIcons(); + this.debouncedUpdateStatusPane(); + } + })); + } + + /** + * Register metadata cache events + */ + private registerMetadataEvents(): void { + // Metadata change events + this.registerEvent(this.app.metadataCache.on('changed', (file) => { + if (file instanceof TFile && file.extension === 'md') { + this.explorerIntegration.updateFileExplorerIcons(file); + + // Update other UI elements if this is the active file + if (this.app.workspace.getActiveFile()?.path === file.path) { + this.checkNoteStatus(); + this.statusDropdown.update(this.getCurrentStatuses()); + } + + this.debouncedUpdateStatusPane(); + } + })); + + // Metadata resolved event - when all files are indexed + this.registerEvent( + this.app.metadataCache.on('resolved', () => { + // When metadata cache is fully resolved, update all icons + setTimeout(() => this.explorerIntegration.updateAllFileExplorerIcons(), 500); + }) + ); + } + + /** + * Check and update the status display for the active file + */ + checkNoteStatus(): void { + try { + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile || !(activeFile instanceof TFile) || activeFile.extension !== 'md') { + this.statusBar.update(['unknown']); + this.statusDropdown.update(['unknown']); + return; + } + + const statuses = this.statusService.getFileStatuses(activeFile); + this.statusBar.update(statuses); + this.statusDropdown.update(statuses); + } catch (error) { + console.error('Error checking note status:', error); + if (!this.hasShownErrorNotification) { + new Notice('Error checking note status. Check console for details.'); + this.hasShownErrorNotification = true; + setTimeout(() => { this.hasShownErrorNotification = false; }, 10000); + } + } + } + + /** + * Get the current statuses for the active file + */ + getCurrentStatuses(): string[] { + try { + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile || !(activeFile instanceof TFile) || activeFile.extension !== 'md') { + return ['unknown']; + } + return this.statusService.getFileStatuses(activeFile); + } catch (error) { + console.error('Error getting current statuses:', error); + return ['unknown']; + } + } + + /** + * Open the status pane + */ + public async openStatusPane(): Promise { + try { + // Check if already open + const existing = this.app.workspace.getLeavesOfType('status-pane')[0]; + if (existing) { + this.app.workspace.setActiveLeaf(existing); + return; + } + + // Create a new leaf + const leaf = this.app.workspace.getLeftLeaf(false); + if (leaf) { + await leaf.setViewState({ type: 'status-pane', active: true }); + this.statusPaneLeaf = leaf; + } + } catch (error) { + console.error('Error opening status pane:', error); + new Notice('Error opening status pane. Check console for details.'); + } + } + + /** + * Update the status pane + */ + async updateStatusPane(): Promise { + try { + 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); + } + } catch (error) { + console.error('Error updating status pane:', error); + } + } + + /** + * Load settings with error handling + */ + async loadSettings(): Promise { + try { + const loadedData = await this.loadData(); + this.settings = Object.assign({}, DEFAULT_SETTINGS, loadedData); + + // Ensure default colors are set for all statuses + this.initializeDefaultColors(); + } catch (error) { + console.error('Error loading settings:', error); + new Notice('Error loading settings. Using defaults. Check console for details.'); + this.settings = Object.assign({}, DEFAULT_SETTINGS); + this.initializeDefaultColors(); + } + } + + /** + * Initialize default colors and template colors + */ + private initializeDefaultColors(): void { + // Initialize template colors if needed + if (!this.settings.useCustomStatusesOnly && this.statusService) { + const templateStatuses = this.statusService.getTemplateStatuses(); + for (const status of templateStatuses) { + if (status.color && !this.settings.statusColors[status.name]) { + this.settings.statusColors[status.name] = status.color; + } + } + } + } + + /** + * Save settings with error handling + */ + async saveSettings(): Promise { + try { + await this.saveData(this.settings); + this.updateComponentSettings(); + } catch (error) { + console.error('Error saving settings:', error); + new Notice('Error saving settings. Check console for details.'); + } + } + + /** + * Update all components with new settings + */ + private updateComponentSettings(): void { + // Update services with new settings + this.statusService.updateSettings(this.settings); + this.styleService.updateSettings(this.settings); + + // Update UI components with new settings + this.statusBar.updateSettings(this.settings); + this.statusDropdown.updateSettings(this.settings); + this.explorerIntegration.updateSettings(this.settings); + this.statusContextMenu.updateSettings(this.settings); + + // Update status pane if open + if (this.statusPaneLeaf && this.statusPaneLeaf.view instanceof StatusPaneView) { + (this.statusPaneLeaf.view as StatusPaneView).updateSettings(this.settings); + } + } + + /** + * Force refresh all UI components + */ + 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 + */ + onunload(): void { + // Cancel debounced functions + this.debouncedCheckNoteStatus.cancel(); + this.debouncedUpdateExplorer.cancel(); + this.debouncedUpdateStatusPane.cancel(); + + // Clean up UI elements + this.statusBar.unload?.(); + this.statusDropdown.unload?.(); + this.explorerIntegration.unload?.(); + this.styleService.unload?.(); + + // Close status pane if open + const statusPane = this.app.workspace.getLeavesOfType('status-pane')[0]; + if (statusPane) statusPane.detach(); + + // Remove custom event listeners + this.removeCustomEventListeners(); + } + + /** + * Remove custom event listeners + */ + private removeCustomEventListeners(): void { + window.removeEventListener('note-status:settings-changed', this.boundSaveSettings); + window.removeEventListener('note-status:status-changed', this.boundCheckNoteStatus); + window.removeEventListener('note-status:refresh-dropdown', this.boundRefreshDropdown); + window.removeEventListener('note-status:refresh-ui', this.boundRefreshUI); + } +} \ No newline at end of file diff --git a/services/status-service.ts b/services/status-service.ts index befbc97..74474f6 100644 --- a/services/status-service.ts +++ b/services/status-service.ts @@ -28,10 +28,10 @@ export class StatusService { * Updates the combined list of all statuses (from templates and custom) */ private updateAllStatuses(): void { - // Start with custom statuses if not using templates exclusively + // Start with custom statuses this.allStatuses = [...this.settings.customStatuses]; - // Add statuses from enabled templates + // Add statuses from enabled templates if not using custom only if (!this.settings.useCustomStatusesOnly) { const templateStatuses = this.getTemplateStatuses(); @@ -167,7 +167,7 @@ export class StatusService { const targetFile = file || this.app.workspace.getActiveFile(); if (!targetFile || !(targetFile instanceof TFile) || targetFile.extension !== 'md') return; - // Use processFrontMatter instead of manual read/modify + // Use processFrontMatter to handle the update await this.app.fileManager.processFrontMatter(targetFile, (frontmatter) => { frontmatter[this.settings.tagPrefix] = newStatuses; }); @@ -203,11 +203,6 @@ export class StatusService { const newStatuses = [...filteredStatuses, statusToAdd]; await this.updateNoteStatuses(newStatuses, targetFile); - - // Trigger UI updates - window.dispatchEvent(new CustomEvent('note-status:status-changed', { - detail: { statuses: newStatuses } - })); } /** @@ -221,11 +216,6 @@ export class StatusService { const newStatuses = currentStatuses.filter(status => status !== statusToRemove); await this.updateNoteStatuses(newStatuses, targetFile); - - // Trigger UI updates - window.dispatchEvent(new CustomEvent('note-status:status-changed', { - detail: { statuses: newStatuses } - })); } /** @@ -247,19 +237,10 @@ export class StatusService { } await this.updateNoteStatuses(newStatuses, targetFile); - - // Ensure UI updates - window.dispatchEvent(new CustomEvent('note-status:status-changed', { - detail: { statuses: newStatuses } - })); } /** * Batch update multiple files' statuses - * @param files Array of files to update - * @param statusesToSet Array of statuses to set - * @param mode 'replace' to replace all statuses, 'add' to add to existing - * @param showNotice Whether to show a notification (default: true) */ public async batchUpdateStatuses( files: TFile[], @@ -285,11 +266,14 @@ export class StatusService { } } - // Only show notice if explicitly requested and we're dealing with multiple files + // Show notice if requested and we're dealing with multiple files if (showNotice && files.length > 1) { const statusText = this.formatStatusText(statusesToSet); new Notice(`Updated ${files.length} files with ${statusText}`); } + + // Trigger UI updates + window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); } /** @@ -301,7 +285,6 @@ export class StatusService { : `${statusesToSet.length} statuses`; } - /** * Insert status metadata in the editor */ diff --git a/ui/components/status-bar.ts b/ui/components/status-bar.ts index 44c38a6..610023e 100644 --- a/ui/components/status-bar.ts +++ b/ui/components/status-bar.ts @@ -41,90 +41,124 @@ export class StatusBar { * Render the status bar based on current settings and statuses */ public render(): void { - this.statusBarEl.empty(); - this.statusBarEl.removeClass('left', 'hidden', 'auto-hide', 'visible'); - this.statusBarEl.addClass('note-status-bar'); + // Clear existing content + this.statusBarEl.empty(); + + // Reset classes + this.statusBarEl.removeClass('left', 'hidden', 'auto-hide', 'visible'); + this.statusBarEl.addClass('note-status-bar'); + + // Hide if disabled + if (!this.settings.showStatusBar) { + this.statusBarEl.addClass('hidden'); + return; + } + + // Handle display based on single vs. multiple status + if (this.currentStatuses.length === 1 || !this.settings.useMultipleStatuses) { + this.renderSingleStatus(); + } else { + this.renderMultipleStatuses(); + } + + // Handle auto-hide behavior for 'unknown' status + this.handleAutoHide(); + } - if (!this.settings.showStatusBar) { - this.statusBarEl.addClass('hidden'); - return; - } + /** + * Render a single status display + */ + private renderSingleStatus(): void { + const primaryStatus = this.currentStatuses[0]; + const statusObj = this.statusService.getAllStatuses().find(s => s.name === primaryStatus); + const tooltipValue = statusObj?.description + ? `${primaryStatus} - ${statusObj.description}` + : primaryStatus; + + // Create status text + const statusText = this.statusBarEl.createEl('span', { + text: `Status: ${primaryStatus}`, + cls: `note-status-${primaryStatus}` + }); + + // Add tooltip + setTooltip(statusText, tooltipValue); + + // Create status icon + const statusIcon = this.statusBarEl.createEl('span', { + text: this.statusService.getStatusIcon(primaryStatus), + cls: `note-status-icon status-${primaryStatus}` + }); + + // Add tooltip to icon too + setTooltip(statusIcon, tooltipValue); + } - // Handle single vs. multiple status display - if (this.currentStatuses.length === 1 || !this.settings.useMultipleStatuses) { - // Display single status - const primaryStatus = this.currentStatuses[0]; - - // Get status object to get description - const statusObj = this.statusService.getAllStatuses().find(s => s.name === primaryStatus); - const tooltipValue = statusObj?.description ? `${primaryStatus} - ${statusObj.description}` : primaryStatus; - - // Create status text - const statusText = this.statusBarEl.createEl('span', { - text: `Status: ${primaryStatus}`, - cls: `note-status-${primaryStatus}` - }); - - // Add tooltip - setTooltip(statusText, tooltipValue); + /** + * Render multiple statuses display + */ + private renderMultipleStatuses(): void { + // Create status label + this.statusBarEl.createEl('span', { + text: `Statuses: `, + cls: 'note-status-label' + }); + + // Create container for status badges + const badgesContainer = this.statusBarEl.createEl('span', { + cls: 'note-status-badges' + }); + + // Add status badges + this.currentStatuses.forEach(status => { + this.createStatusBadge(badgesContainer, status); + }); + } - // Create status icon - const statusIcon = this.statusBarEl.createEl('span', { - text: this.statusService.getStatusIcon(primaryStatus), - cls: `note-status-icon status-${primaryStatus}` - }); - - // Add tooltip to icon too - setTooltip(statusIcon, tooltipValue); - } else { - // Display multiple statuses - // Create status text - this.statusBarEl.createEl('span', { - text: `Statuses: `, - cls: 'note-status-label' - }); - - // Create container for status badges - const badgesContainer = this.statusBarEl.createEl('span', { - cls: 'note-status-badges' - }); - - // Add status badges - this.currentStatuses.forEach(status => { - const statusObj = this.statusService.getAllStatuses().find(s => s.name === status); - const tooltipValue = statusObj?.description ? `${status} - ${statusObj.description}` : status; - - const badge = badgesContainer.createEl('span', { - cls: `note-status-badge status-${status}` - }); - - // Add tooltip - setTooltip(badge, tooltipValue); - - badge.createEl('span', { - text: this.statusService.getStatusIcon(status), - cls: 'note-status-badge-icon' - }); - - badge.createEl('span', { - text: status, - cls: 'note-status-badge-text' - }); - }); - } + /** + * Create a status badge for multiple status display + */ + private createStatusBadge(container: HTMLElement, status: string): void { + const statusObj = this.statusService.getAllStatuses().find(s => s.name === status); + const tooltipValue = statusObj?.description + ? `${status} - ${statusObj.description}` + : status; + + const badge = container.createEl('span', { + cls: `note-status-badge status-${status}` + }); + + // Add tooltip + setTooltip(badge, tooltipValue); + + // Add icon + badge.createEl('span', { + text: this.statusService.getStatusIcon(status), + cls: 'note-status-badge-icon' + }); + + // Add text + badge.createEl('span', { + text: status, + cls: 'note-status-badge-text' + }); + } - // Handle auto-hide behavior - const onlyUnknown = this.currentStatuses.length === 1 && this.currentStatuses[0] === 'unknown'; - if (this.settings.autoHideStatusBar && onlyUnknown) { - this.statusBarEl.addClass('auto-hide'); - setTimeout(() => { - if (onlyUnknown && this.settings.showStatusBar) { - this.statusBarEl.addClass('hidden'); - } - }, 500); - } else { - this.statusBarEl.addClass('visible'); - } + /** + * Handle auto-hide behavior + */ + private handleAutoHide(): void { + const onlyUnknown = this.currentStatuses.length === 1 && this.currentStatuses[0] === 'unknown'; + if (this.settings.autoHideStatusBar && onlyUnknown) { + this.statusBarEl.addClass('auto-hide'); + setTimeout(() => { + if (onlyUnknown && this.settings.showStatusBar) { + this.statusBarEl.addClass('hidden'); + } + }, 500); + } else { + this.statusBarEl.addClass('visible'); + } } /** @@ -139,7 +173,6 @@ export class StatusBar { * Clean up when plugin is unloaded */ public unload(): void { - // Clean up any event listeners or DOM elements this.statusBarEl.empty(); } -} +} \ No newline at end of file diff --git a/ui/components/status-dropdown.ts b/ui/components/status-dropdown.ts index 88a972e..286787d 100644 --- a/ui/components/status-dropdown.ts +++ b/ui/components/status-dropdown.ts @@ -4,579 +4,518 @@ import { StatusService } from '../../services/status-service'; import { StatusDropdownComponent } from './status-dropdown-component'; /** - * Enhanced status dropdown with toolbar integration + * Manages status dropdown UI and interactions */ export class StatusDropdown { - private app: any; - private settings: NoteStatusSettings; - private statusService: StatusService; - private currentStatuses: string[] = ['unknown']; - private toolbarButton?: HTMLElement; - private dropdownComponent: StatusDropdownComponent; - - constructor(app: any, settings: NoteStatusSettings, statusService: StatusService) { - this.app = app; - this.settings = settings; - this.statusService = statusService; - - // Initialize the dropdown component - this.dropdownComponent = new StatusDropdownComponent(app, statusService, settings); - - // Configure dropdown component callbacks - this.setupDropdownCallbacks(); - - } - - /** - * Set up the dropdown callbacks - */ - private setupDropdownCallbacks(): void { - this.dropdownComponent.setOnStatusChange((statuses) => { - // Update current statuses and toolbar button - this.currentStatuses = [...statuses]; // Make sure to copy the array - this.updateToolbarButton(); - - // Dispatch events for UI update - window.dispatchEvent(new CustomEvent('note-status:status-changed', { - detail: { statuses } - })); - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); - }); - } - - /** - * Initialize the toolbar button in the Obsidian ribbon - */ - private initToolbarButton(): void { - // Clear any existing button - if (this.toolbarButton) { - this.toolbarButton.remove(); - this.toolbarButton = undefined; - } - - // Wait for next tick to ensure the UI is fully rendered - setTimeout(() => { - const toolbarContainer = this.findToolbarContainer(); - - if (!toolbarContainer) { - console.error('Note Status: Could not find toolbar container'); - return; - } - this.createToolbarButton(toolbarContainer); - }, 500); // Waiting 500ms to ensure the UI is ready - } - - /** - * Find the toolbar container element - */ - private findToolbarContainer(): HTMLElement | null { - return document.querySelector('.workspace-leaf .view-header .view-actions') - } - - /** - * Create the toolbar button element - */ - private createToolbarButton(toolbarContainer: HTMLElement): void { - // Create the button element - this.toolbarButton = document.createElement('button'); - this.toolbarButton.addClass('note-status-toolbar-button', 'clickable-icon', 'view-action'); - this.toolbarButton.setAttribute('aria-label', 'Note status'); - - // Update initial button state - this.updateToolbarButton(); - - // Add click handler - this.toolbarButton.addEventListener('click', (e) => { - e.stopPropagation(); - e.preventDefault(); - this.toggleStatusPopover(); - }); - - // Insert at a more reliable position - just prepend to the container - try { - toolbarContainer.prepend(this.toolbarButton); - } catch (error) { - console.error('Note Status: Error inserting toolbar button', error); - // Fallback - just append it - toolbarContainer.appendChild(this.toolbarButton); - } - } - - /** - * Updates the toolbar button appearance based on current statuses - */ - private updateToolbarButton(): void { - if (!this.toolbarButton) return; - - // Clear existing content - this.toolbarButton.empty(); - - // Check if we have a valid status - const hasValidStatus = this.currentStatuses.length > 0 && - !this.currentStatuses.every(status => status === 'unknown'); - - // Create badge container - const badgeContainer = document.createElement('div'); - badgeContainer.addClass('note-status-toolbar-badge-container'); - - if (hasValidStatus) { - // Add status badge with proper icon - this.addStatusBadge(badgeContainer); - } else { - // Add default status icon when no valid status - this.addDefaultStatusIcon(badgeContainer); - } - - this.toolbarButton.appendChild(badgeContainer); - } - - /** - * Add a status badge to the toolbar button - */ - private addStatusBadge(container: HTMLElement): void { - // Show primary status icon and indicator for multiple statuses - const primaryStatus = this.currentStatuses[0]; - const statusInfo = this.statusService.getAllStatuses().find(s => s.name === primaryStatus); - - if (statusInfo) { - // Primary status icon - const iconSpan = document.createElement('span'); - iconSpan.addClass(`note-status-toolbar-icon`, `status-${primaryStatus}`); - iconSpan.textContent = statusInfo.icon; - container.appendChild(iconSpan); - - // Add count indicator if multiple statuses - if (this.settings.useMultipleStatuses && this.currentStatuses.length > 1) { - const countBadge = document.createElement('span'); - countBadge.addClass('note-status-count-badge'); - countBadge.textContent = `+${this.currentStatuses.length - 1}`; - container.appendChild(countBadge); - } - } - } - - /** - * Add default status icon when no valid status - */ - private addDefaultStatusIcon(container: HTMLElement): void { - const iconSpan = document.createElement('span'); - iconSpan.addClass('note-status-toolbar-icon', 'status-unknown'); - - // Use the statusService to get the proper icon for 'unknown' status - iconSpan.textContent = this.statusService.getStatusIcon('unknown'); - container.appendChild(iconSpan); - } - - /** - * Updates the dropdown UI based on current statuses - */ - public update(currentStatuses: string[] | string): void { - // Normalize input to always be an array - if (typeof currentStatuses === 'string') { - this.currentStatuses = [currentStatuses]; - } else { - this.currentStatuses = [...currentStatuses]; // Create a copy to ensure it's updated - } - - // Update toolbar button with new status - this.updateToolbarButton(); - - // Update dropdown component - this.dropdownComponent.updateStatuses(this.currentStatuses); - } - - /** - * Updates settings reference - */ - public updateSettings(settings: NoteStatusSettings): void { - this.settings = settings; - this.updateToolbarButton(); - this.dropdownComponent.updateSettings(settings); - } - - /** - * Toggle the status popover - */ - private toggleStatusPopover(): void { - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile) return; - - this.openStatusDropdown({ - target: this.toolbarButton, - files: [activeFile] - }); - } - - /** - * Show status dropdown in context menu - */ - public showInContextMenu(editor: Editor, view: MarkdownView): void { - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile) return; - - // Pass context to indicate this is from editor context menu - this.openStatusDropdown({ - editor: editor, - view: view, - files: [activeFile], - onStatusChange: async (statuses) => { - if (statuses.length > 0) { - // Use a direct file operation, not batch update - if (this.settings.useMultipleStatuses) { - await this.statusService.toggleNoteStatus(statuses[0], activeFile); - } else { - await this.statusService.updateNoteStatuses(statuses, activeFile); - } - - // Trigger UI updates - window.dispatchEvent(new CustomEvent('note-status:status-changed', { - detail: { statuses, file: activeFile.path } - })); - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); - } - } - }); - } - - /** - * Get position from cursor or fallback positions - */ - private getCursorPosition(editor: Editor, view: MarkdownView): { x: number, y: number } { - try { - // Get cursor position in the document - const cursor = editor.getCursor('head'); - - // Try to find the DOM representation of the cursor position - const lineElement = view.contentEl.querySelector(`.cm-line:nth-child(${cursor.line + 1})`); - - if (lineElement) { - const rect = lineElement.getBoundingClientRect(); - // Position near the current line with some offset - return { - x: rect.left + 20, - y: rect.bottom + 5 - }; - } - - // Fallback to editor element position - const editorEl = view.contentEl.querySelector('.cm-editor'); - if (editorEl) { - const rect = editorEl.getBoundingClientRect(); - return { - x: rect.left + 100, // Offset from left - y: rect.top + 100 // Offset from top - }; - } - } catch (error) { - console.error('Error getting position for dropdown:', error); - } - - // Last resort - use middle of viewport - return { - x: window.innerWidth / 2, - y: window.innerHeight / 3 - }; - } - - /** - * Create a dummy target element for positioning - */ - private createDummyTarget(position: { x: number, y: number }): HTMLElement { - const dummyTarget = document.createElement('div'); - dummyTarget.addClass('note-status-dummy-target'); - dummyTarget.style.setProperty('--pos-x-px', `${position.x}px`); - dummyTarget.style.setProperty('--pos-y-px', `${position.y}px`); - document.body.appendChild(dummyTarget); - return dummyTarget; - } - - /** - * Render method (kept for compatibility) - */ - public render(): void { - // This is now a no-op, as the dropdown component handles everything internally - } - - /** - * Remove dropdown when plugin is unloaded - */ - public unload(): void { - // Clean up dropdown component - this.dropdownComponent.dispose(); - - // Remove toolbar button - if (this.toolbarButton) { - this.toolbarButton.remove(); - this.toolbarButton = undefined; - } - } - - /** - * Universal function to open the status dropdown in any context - * @param options Configuration options for opening the dropdown - */ - public openStatusDropdown(options: { - target?: HTMLElement; - position?: { x: number, y: number }; - files?: TFile[]; - editor?: Editor; - view?: MarkdownView; - mode?: 'replace' | 'add'; - onStatusChange?: (statuses: string[]) => void; - }): void { - // IMPORTANT: Force reset the dropdown component's state - // Add this at the beginning of the method - if (this.dropdownComponent.isOpen) { - this.dropdownComponent.close(); - // Give it a moment to clean up before proceeding - setTimeout(() => { - this._openStatusDropdown(options); - }, 50); - return; - } - - this._openStatusDropdown(options); - } - - /** - * Find common statuses across multiple files - */ - private findCommonStatuses(files: TFile[]): string[] { - if (files.length === 0) return ['unknown']; - - // Get statuses for first file - const firstFileStatuses = this.statusService.getFileStatuses(files[0]); - - // Filter to only include statuses that exist on all files - return firstFileStatuses.filter(status => { - // Skip unknown status - if (status === 'unknown') return false; - - // Check if status exists on all files - return files.every(file => - this.statusService.getFileStatuses(file).includes(status) - ); - }); - } - - /** - * Toggle a status across multiple files - */ - private async toggleStatusForFiles(files: TFile[], status: string): Promise { - // Count how many files have this status - const filesWithStatus = files.filter(file => - this.statusService.getFileStatuses(file).includes(status) - ); - - // If more than half have the status, remove it; otherwise, add it - const shouldRemove = filesWithStatus.length > files.length / 2; - - for (const file of files) { - if (shouldRemove) { - await this.statusService.removeNoteStatus(status, file); - } else { - await this.statusService.addNoteStatus(status, file); - } - } - } - - private _openStatusDropdown(options: { - target?: HTMLElement; - position?: { x: number, y: number }; - files?: TFile[]; - editor?: Editor; - view?: MarkdownView; - mode?: 'replace' | 'add'; - onStatusChange?: (statuses: string[]) => void; - }): void { - // If no files provided, use active file - const files = options.files || [this.app.workspace.getActiveFile()].filter(Boolean); - if (!files.length) { - new Notice('No files selected'); - return; - } - - // IMPORTANT: Always reset state at the beginning of each operation - // This ensures no lingering callbacks or files from previous operations - this.dropdownComponent.setTargetFile(null); - this.dropdownComponent.setOnStatusChange(() => {}); - - // Determine if we're handling single or multiple files - const isSingleFile = files.length === 1; - const targetFile = isSingleFile ? files[0] : null; - - // Set target file (if single) or null (if multiple) - this.dropdownComponent.setTargetFile(targetFile); - - // Get current statuses appropriately - let currentStatuses: string[]; - - if (targetFile) { - // For single file, get its current statuses - currentStatuses = this.statusService.getFileStatuses(targetFile); - } else if (files.length > 1) { - // For multiple files, find common statuses to display - currentStatuses = this.findCommonStatuses(files); - } else { - currentStatuses = ['unknown']; - } - - // Update dropdown with current statuses - this.dropdownComponent.updateStatuses(currentStatuses); - - // Set custom callback for status changes if provided - if (options.onStatusChange) { - // Use the provided callback directly - this.dropdownComponent.setOnStatusChange(options.onStatusChange); - } else if (!isSingleFile) { - // Create a local copy of files to avoid closure issues - const filesForBatch = [...files]; - - // Set batch operation callback for multiple files - // Use toggle behavior for batch operations like single files - this.dropdownComponent.setOnStatusChange(async (statuses) => { - if (statuses.length > 0) { - // Get the last selected status for toggling - const toggledStatus = statuses[statuses.length - 1]; - - // Toggle this status across all files - await this.toggleStatusForFiles(filesForBatch, toggledStatus); - - // Dispatch events for UI update - window.dispatchEvent(new CustomEvent('note-status:batch-update-complete', { - detail: { - status: toggledStatus, - fileCount: filesForBatch.length - } - })); - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); - } - }); - } else { - // Default callback for standard operations - this.dropdownComponent.setOnStatusChange((statuses) => { - // Just emit events for UI updates - window.dispatchEvent(new CustomEvent('note-status:status-changed', { - detail: { statuses } - })); - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); - }); - } - - // For dropdown from editor - if (options.editor && options.view) { - const position = this.getCursorPosition(options.editor, options.view); - const dummyTarget = this.createDummyTarget(position); - this.dropdownComponent.open(dummyTarget, position); - - // Clean up dummy target - setTimeout(() => { - if (dummyTarget.parentNode) { - dummyTarget.parentNode.removeChild(dummyTarget); - } - }, 100); - return; - } - - // For dropdown from toolbar button - if (options.target) { - if (options.position) { - this.dropdownComponent.open(options.target, options.position); - } else { - const rect = options.target.getBoundingClientRect(); - const position = { - x: rect.left, - y: rect.bottom + 5 - }; - this.dropdownComponent.open(options.target, position); - } - return; - } - - // For direct position (context menus) - if (options.position) { - const dummyTarget = document.createElement('div'); - dummyTarget.addClass('note-status-dummy-target'); - dummyTarget.style.setProperty('--pos-x-px', `${options.position.x}px`); - dummyTarget.style.setProperty('--pos-y-px', `${options.position.y}px`); - document.body.appendChild(dummyTarget); - - this.dropdownComponent.open(dummyTarget, options.position); - - // Clean up dummy target - setTimeout(() => { - if (dummyTarget.parentNode) { - dummyTarget.parentNode.removeChild(dummyTarget); - } - }, 100); - return; - } - - // Fallback to center position - const center = { - x: window.innerWidth / 2, - y: window.innerHeight / 3 - }; - const fallbackTarget = document.createElement('div'); - fallbackTarget.addClass('note-status-dummy-target'); - fallbackTarget.style.setProperty('--pos-x-px', `${center.x}px`); - fallbackTarget.style.setProperty('--pos-y-px', `${center.y}px`); - document.body.appendChild(fallbackTarget); - - this.dropdownComponent.open(fallbackTarget, center); - - // Clean up fallback target - setTimeout(() => { - if (fallbackTarget.parentNode) { - fallbackTarget.parentNode.removeChild(fallbackTarget); - } - }, 100); - } - - /** - * Adds the toolbar button to the active leaf - */ - public addToolbarButtonToActiveLeaf(): void { - const activeLeaf = this.app.workspace.activeLeaf; - if (!activeLeaf || !activeLeaf.view || !(activeLeaf.view instanceof MarkdownView)) { - return; - } - - // Get the toolbar container - const containerEl = activeLeaf.view.containerEl; - const toolbarContainer = containerEl.querySelector('.view-header .view-actions'); - if (!toolbarContainer) { - return; - } - - // Check if button already exists in this toolbar - const existingButton = toolbarContainer.querySelector('.note-status-toolbar-button'); - if (existingButton) { - this.toolbarButton = existingButton as HTMLElement; - this.updateToolbarButton(); // Update existing button - return; - } - - // Create new button - this.toolbarButton = document.createElement('button'); - this.toolbarButton.addClass('note-status-toolbar-button', 'clickable-icon', 'view-action'); - this.toolbarButton.setAttribute('aria-label', 'Note status'); - - // Update the button state - this.updateToolbarButton(); - - // Add click handler - this.toolbarButton.addEventListener('click', (e) => { - e.stopPropagation(); - e.preventDefault(); - this.toggleStatusPopover(); - }); - - // Add to toolbar at the beginning (left side) instead of the end - if (toolbarContainer.firstChild) { - toolbarContainer.insertBefore(this.toolbarButton, toolbarContainer.firstChild); - } else { - toolbarContainer.appendChild(this.toolbarButton); - } - } + private app: any; + private settings: NoteStatusSettings; + private statusService: StatusService; + private currentStatuses: string[] = ['unknown']; + private toolbarButton?: HTMLElement; + private dropdownComponent: StatusDropdownComponent; + + constructor(app: any, settings: NoteStatusSettings, statusService: StatusService) { + this.app = app; + this.settings = settings; + this.statusService = statusService; + + // Initialize the dropdown component + this.dropdownComponent = new StatusDropdownComponent(app, statusService, settings); + + // Configure dropdown component callbacks + this.setupDropdownCallbacks(); + } + + /** + * Set up the dropdown callbacks + */ + private setupDropdownCallbacks(): void { + this.dropdownComponent.setOnStatusChange((statuses) => { + // Update current statuses and toolbar button + this.currentStatuses = [...statuses]; // Make sure to copy the array + this.updateToolbarButton(); + + // Dispatch events for UI update + window.dispatchEvent(new CustomEvent('note-status:status-changed', { + detail: { statuses } + })); + window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); + }); + } + + /** + * Updates the toolbar button appearance based on current statuses + */ + private updateToolbarButton(): void { + if (!this.toolbarButton) return; + + // Clear existing content + this.toolbarButton.empty(); + + // Check if we have a valid status + const hasValidStatus = this.currentStatuses.length > 0 && + !this.currentStatuses.every(status => status === 'unknown'); + + // Create badge container + const badgeContainer = document.createElement('div'); + badgeContainer.addClass('note-status-toolbar-badge-container'); + + if (hasValidStatus) { + // Add primary status icon + const primaryStatus = this.currentStatuses[0]; + const statusInfo = this.statusService.getAllStatuses().find(s => s.name === primaryStatus); + + if (statusInfo) { + // Primary status icon + const iconSpan = document.createElement('span'); + iconSpan.addClass(`note-status-toolbar-icon`, `status-${primaryStatus}`); + iconSpan.textContent = statusInfo.icon; + badgeContainer.appendChild(iconSpan); + + // Add count indicator if multiple statuses + if (this.settings.useMultipleStatuses && this.currentStatuses.length > 1) { + const countBadge = document.createElement('span'); + countBadge.addClass('note-status-count-badge'); + countBadge.textContent = `+${this.currentStatuses.length - 1}`; + badgeContainer.appendChild(countBadge); + } + } + } else { + // Add default unknown status icon + const iconSpan = document.createElement('span'); + iconSpan.addClass('note-status-toolbar-icon', 'status-unknown'); + iconSpan.textContent = this.statusService.getStatusIcon('unknown'); + badgeContainer.appendChild(iconSpan); + } + + this.toolbarButton.appendChild(badgeContainer); + } + + /** + * Updates the dropdown UI based on current statuses + */ + public update(currentStatuses: string[] | string): void { + // Normalize input to always be an array + if (typeof currentStatuses === 'string') { + this.currentStatuses = [currentStatuses]; + } else { + this.currentStatuses = [...currentStatuses]; // Create a copy + } + + // Update toolbar button with new status + this.updateToolbarButton(); + + // Update dropdown component + this.dropdownComponent.updateStatuses(this.currentStatuses); + } + + /** + * Updates settings reference + */ + public updateSettings(settings: NoteStatusSettings): void { + this.settings = settings; + this.updateToolbarButton(); + this.dropdownComponent.updateSettings(settings); + } + + /** + * Toggle the status popover + */ + private toggleStatusPopover(): void { + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile) return; + + this.openStatusDropdown({ + target: this.toolbarButton, + files: [activeFile] + }); + } + + /** + * Show status dropdown in context menu + */ + public showInContextMenu(editor: Editor, view: MarkdownView): void { + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile) return; + + const position = this.getCursorPosition(editor, view); + + this.openStatusDropdown({ + position: position, + files: [activeFile], + onStatusChange: async (statuses) => { + if (statuses.length > 0) { + if (this.settings.useMultipleStatuses) { + await this.statusService.toggleNoteStatus(statuses[0], activeFile); + } else { + await this.statusService.updateNoteStatuses(statuses, activeFile); + } + + // Trigger UI updates + window.dispatchEvent(new CustomEvent('note-status:status-changed', { + detail: { statuses, file: activeFile.path } + })); + window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); + } + } + }); + } + + /** + * Get position from cursor or fallback positions + */ + private getCursorPosition(editor: Editor, view: MarkdownView): { x: number, y: number } { + try { + // Get cursor position in the document + const cursor = editor.getCursor('head'); + + // Try to find the DOM representation of the cursor position + const lineElement = view.contentEl.querySelector(`.cm-line:nth-child(${cursor.line + 1})`); + + if (lineElement) { + const rect = lineElement.getBoundingClientRect(); + // Position near the current line with some offset + return { + x: rect.left + 20, + y: rect.bottom + 5 + }; + } + + // Fallback to editor element position + const editorEl = view.contentEl.querySelector('.cm-editor'); + if (editorEl) { + const rect = editorEl.getBoundingClientRect(); + return { + x: rect.left + 100, // Offset from left + y: rect.top + 100 // Offset from top + }; + } + } catch (error) { + console.error('Error getting position for dropdown:', error); + } + + // Last resort - use middle of viewport + return { + x: window.innerWidth / 2, + y: window.innerHeight / 3 + }; + } + + /** + * Stub render method (kept for compatibility) + */ + public render(): void { + // No-op - dropdown component handles rendering internally + } + + /** + * Remove dropdown when plugin is unloaded + */ + public unload(): void { + // Clean up dropdown component + this.dropdownComponent.dispose(); + + // Remove toolbar button + if (this.toolbarButton) { + this.toolbarButton.remove(); + this.toolbarButton = undefined; + } + } + + /** + * Universal function to open the status dropdown in any context + */ + public openStatusDropdown(options: { + target?: HTMLElement; + position?: { x: number, y: number }; + files?: TFile[]; + editor?: Editor; + view?: MarkdownView; + mode?: 'replace' | 'add'; + onStatusChange?: (statuses: string[]) => void; + }): void { + // Force reset if dropdown is already open + if (this.dropdownComponent.isOpen) { + this.dropdownComponent.close(); + // Give it a moment to clean up before proceeding + setTimeout(() => { + this._openStatusDropdown(options); + }, 50); + return; + } + + this._openStatusDropdown(options); + } + + /** + * Internal method to open dropdown after reset + */ + private _openStatusDropdown(options: { + target?: HTMLElement; + position?: { x: number, y: number }; + files?: TFile[]; + editor?: Editor; + view?: MarkdownView; + mode?: 'replace' | 'add'; + onStatusChange?: (statuses: string[]) => void; + }): void { + // If no files provided, use active file + const files = options.files || [this.app.workspace.getActiveFile()].filter(Boolean); + if (!files.length) { + new Notice('No files selected'); + return; + } + + // Always reset state at the beginning + this.dropdownComponent.setTargetFile(null); + this.dropdownComponent.setOnStatusChange(() => {}); + + // Determine if we're handling single or multiple files + const isSingleFile = files.length === 1; + const targetFile = isSingleFile ? files[0] : null; + + // Set target file (if single) or null (if multiple) + this.dropdownComponent.setTargetFile(targetFile); + + // Get current statuses appropriately + let currentStatuses: string[]; + + if (targetFile) { + // For single file, get its current statuses + currentStatuses = this.statusService.getFileStatuses(targetFile); + } else if (files.length > 1) { + // For multiple files, find common statuses to display + currentStatuses = this.findCommonStatuses(files); + } else { + currentStatuses = ['unknown']; + } + + // Update dropdown with current statuses + this.dropdownComponent.updateStatuses(currentStatuses); + + // Set the appropriate callback + this.setupStatusChangeCallback(options, files, isSingleFile); + + // Position and open the dropdown + this.positionAndOpenDropdown(options); + } + + /** + * Setup the appropriate callback for status changes + */ + private setupStatusChangeCallback( + options: { + onStatusChange?: (statuses: string[]) => void; + mode?: 'replace' | 'add'; + }, + files: TFile[], + isSingleFile: boolean + ): void { + if (options.onStatusChange) { + // Use the provided callback directly + this.dropdownComponent.setOnStatusChange(options.onStatusChange); + } else if (!isSingleFile) { + // Create a local copy of files to avoid closure issues + const filesForBatch = [...files]; + + // Set batch operation callback for multiple files + this.dropdownComponent.setOnStatusChange(async (statuses) => { + if (statuses.length > 0) { + // Get the last selected status for toggling + const toggledStatus = statuses[statuses.length - 1]; + + // Toggle this status across all files + await this.toggleStatusForFiles(filesForBatch, toggledStatus); + + // Dispatch events for UI update + window.dispatchEvent(new CustomEvent('note-status:batch-update-complete', { + detail: { + status: toggledStatus, + fileCount: filesForBatch.length + } + })); + window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); + } + }); + } else { + // Default callback for single file operations + this.dropdownComponent.setOnStatusChange((statuses) => { + // Just emit events for UI updates + window.dispatchEvent(new CustomEvent('note-status:status-changed', { + detail: { statuses } + })); + window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); + }); + } + } + + /** + * Position and open the dropdown based on options + */ + private positionAndOpenDropdown(options: { + target?: HTMLElement; + position?: { x: number, y: number }; + editor?: Editor; + view?: MarkdownView; + }): void { + // For dropdown from editor + if (options.editor && options.view) { + const position = this.getCursorPosition(options.editor, options.view); + const dummyTarget = this.createDummyTarget(position); + this.dropdownComponent.open(dummyTarget, position); + + // Clean up dummy target + setTimeout(() => { + if (dummyTarget.parentNode) { + dummyTarget.parentNode.removeChild(dummyTarget); + } + }, 100); + return; + } + + // For dropdown from toolbar button + if (options.target) { + if (options.position) { + this.dropdownComponent.open(options.target, options.position); + } else { + const rect = options.target.getBoundingClientRect(); + const position = { + x: rect.left, + y: rect.bottom + 5 + }; + this.dropdownComponent.open(options.target, position); + } + return; + } + + // For direct position (context menus) + if (options.position) { + const dummyTarget = this.createDummyTarget(options.position); + this.dropdownComponent.open(dummyTarget, options.position); + + // Clean up dummy target + setTimeout(() => { + if (dummyTarget.parentNode) { + dummyTarget.parentNode.removeChild(dummyTarget); + } + }, 100); + return; + } + + // Fallback to center position + const center = { + x: window.innerWidth / 2, + y: window.innerHeight / 3 + }; + + const fallbackTarget = this.createDummyTarget(center); + this.dropdownComponent.open(fallbackTarget, center); + + // Clean up fallback target + setTimeout(() => { + if (fallbackTarget.parentNode) { + fallbackTarget.parentNode.removeChild(fallbackTarget); + } + }, 100); + } + + /** + * Create a dummy target element for positioning + */ + private createDummyTarget(position: { x: number, y: number }): HTMLElement { + const dummyTarget = document.createElement('div'); + dummyTarget.addClass('note-status-dummy-target'); + dummyTarget.style.setProperty('--pos-x-px', `${position.x}px`); + dummyTarget.style.setProperty('--pos-y-px', `${position.y}px`); + document.body.appendChild(dummyTarget); + return dummyTarget; + } + + /** + * Find common statuses across multiple files + */ + private findCommonStatuses(files: TFile[]): string[] { + if (files.length === 0) return ['unknown']; + + // Get statuses for first file + const firstFileStatuses = this.statusService.getFileStatuses(files[0]); + + // Filter to only include statuses that exist on all files, except unknown + return firstFileStatuses.filter(status => { + if (status === 'unknown') return false; + + // Check if status exists on all files + return files.every(file => + this.statusService.getFileStatuses(file).includes(status) + ); + }); + } + + /** + * Toggle a status across multiple files + */ + private async toggleStatusForFiles(files: TFile[], status: string): Promise { + // Count how many files have this status + const filesWithStatus = files.filter(file => + this.statusService.getFileStatuses(file).includes(status) + ); + + // If more than half have the status, remove it; otherwise, add it + const shouldRemove = filesWithStatus.length > files.length / 2; + + for (const file of files) { + if (shouldRemove) { + await this.statusService.removeNoteStatus(status, file); + } else { + await this.statusService.addNoteStatus(status, file); + } + } + } + + /** + * Adds the toolbar button to the active leaf + */ + public addToolbarButtonToActiveLeaf(): void { + const activeLeaf = this.app.workspace.activeLeaf; + if (!activeLeaf || !activeLeaf.view || !(activeLeaf.view instanceof MarkdownView)) { + return; + } + + // Get the toolbar container + const containerEl = activeLeaf.view.containerEl; + const toolbarContainer = containerEl.querySelector('.view-header .view-actions'); + if (!toolbarContainer) { + return; + } + + // Check if button already exists in this toolbar + const existingButton = toolbarContainer.querySelector('.note-status-toolbar-button'); + if (existingButton) { + this.toolbarButton = existingButton as HTMLElement; + this.updateToolbarButton(); // Update existing button + return; + } + + // Create new button + this.toolbarButton = document.createElement('button'); + this.toolbarButton.addClass('note-status-toolbar-button', 'clickable-icon', 'view-action'); + this.toolbarButton.setAttribute('aria-label', 'Note status'); + + // Update the button state + this.updateToolbarButton(); + + // Add click handler + this.toolbarButton.addEventListener('click', (e) => { + e.stopPropagation(); + e.preventDefault(); + this.toggleStatusPopover(); + }); + + // Add to toolbar at the beginning + if (toolbarContainer.firstChild) { + toolbarContainer.insertBefore(this.toolbarButton, toolbarContainer.firstChild); + } else { + toolbarContainer.appendChild(this.toolbarButton); + } + } } \ No newline at end of file diff --git a/ui/integrations/explorer-integration.ts b/ui/integrations/explorer-integration.ts index 58f0f8a..78b1108 100644 --- a/ui/integrations/explorer-integration.ts +++ b/ui/integrations/explorer-integration.ts @@ -4,7 +4,6 @@ import { StatusService } from '../../services/status-service'; /** * Enhanced file explorer integration for displaying status icons - * Includes performance optimizations and error handling */ export class ExplorerIntegration { private app: App; @@ -33,14 +32,6 @@ export class ExplorerIntegration { const previousShowIcons = this.settings.showStatusIconsInExplorer; this.settings = settings; - this.handleSettingsChange(previousShowIcons); - } - - /** - * Handle settings change and update UI accordingly - */ - private handleSettingsChange(previousShowIcons: boolean): void { - // Update icons based on new settings if (previousShowIcons !== this.settings.showStatusIconsInExplorer) { if (this.settings.showStatusIconsInExplorer) { this.updateAllFileExplorerIcons(); @@ -49,7 +40,6 @@ export class ExplorerIntegration { } } else if (this.settings.showStatusIconsInExplorer) { // If setting hasn't changed but is enabled, refresh the icons - // This handles cases where custom statuses or colors changed this.updateAllFileExplorerIcons(); } } @@ -64,7 +54,7 @@ export class ExplorerIntegration { return leaf.view as FileExplorerView; } - // If that fails, try to find it by searching all leaves + // Fallback to searching all leaves for (const leaf of this.app.workspace.getLeavesOfType('')) { if (leaf.view && 'fileItems' in leaf.view) { return leaf.view as FileExplorerView; @@ -100,7 +90,7 @@ export class ExplorerIntegration { return; } - // Process files in the queue in batches to avoid blocking UI + // Process files in the queue in batches const batchSize = 50; const allPaths = Array.from(this.iconUpdateQueue); @@ -135,35 +125,28 @@ export class ExplorerIntegration { /** * Update a single file's icon in the file explorer - */ + */ private updateSingleFileIcon(file: TFile, fileExplorerView: FileExplorerView): void { if (!this.settings.showStatusIconsInExplorer || file.extension !== 'md') return; try { const fileItem = fileExplorerView.fileItems[file.path]; - if (!fileItem) { - return; // File not in explorer view - } + if (!fileItem) return; // File not in explorer view const titleEl = fileItem.titleEl || fileItem.selfEl; - if (!titleEl) { - return; // No title element found - } + if (!titleEl) return; // No title element found - // Get statuses for this file - use fresh metadata cache + // Get statuses for this file const statuses = this.statusService.getFileStatuses(file); - // Remove existing status icons if present (careful not to remove other elements) + // Remove existing status icons if present this.removeExistingIcons(titleEl); // Hide unknown status if setting is enabled - if (this.shouldHideUnknownStatus(statuses)) { - return; - } + if (this.shouldHideUnknownStatus(statuses)) return; - // Add status icons WITHOUT disrupting the existing title + // Add status icons this.addStatusIcons(titleEl, statuses); - } catch (error) { console.error(`Note Status: Error updating icon for ${file.path}`, error); } @@ -186,7 +169,7 @@ export class ExplorerIntegration { const existingIcons = element.querySelectorAll('.note-status-icon, .note-status-icon-container'); existingIcons.forEach(icon => { - // Only remove elements with our classes to avoid removing title or other elements + // Only remove elements with our classes if (icon.classList.contains('note-status-icon') || icon.classList.contains('note-status-icon-container')) { icon.remove(); @@ -196,52 +179,38 @@ export class ExplorerIntegration { /** * Add status icons to a title element - */ + */ private addStatusIcons(titleEl: HTMLElement, statuses: string[]): void { - // Create container for multiple icons - using createElement to avoid side effects + // Create container for multiple icons const iconContainer = document.createElement('span'); iconContainer.className = 'note-status-icon-container'; - // Add all status icons + // Add all status icons based on settings if (this.settings.useMultipleStatuses && statuses.length > 0 && statuses[0] !== 'unknown') { // Add all icons if using multiple statuses - this.addMultipleStatusIcons(iconContainer, statuses); + statuses.forEach(status => { + this.addSingleStatusIcon(iconContainer, status); + }); } else { // Just show primary status - this.addPrimaryStatusIcon(iconContainer, statuses); + const primaryStatus = statuses[0] || 'unknown'; + if (primaryStatus !== 'unknown' || !this.settings.autoHideStatusBar) { + this.addSingleStatusIcon(iconContainer, primaryStatus); + } } // Only append if we added icons if (iconContainer.childElementCount > 0) { - // Use appendChild to add to the end instead of replacing content + // Use appendChild to add to the end titleEl.appendChild(iconContainer); } } - /** - * Add multiple status icons to a container - */ - private addMultipleStatusIcons(container: HTMLElement, statuses: string[]): void { - statuses.forEach(status => { - this.addSingleStatusIcon(container, status); - }); - } - - /** - * Add primary status icon to a container - */ - private addPrimaryStatusIcon(container: HTMLElement, statuses: string[]): void { - const primaryStatus = statuses[0] || 'unknown'; - if (primaryStatus !== 'unknown' || !this.settings.autoHideStatusBar) { - this.addSingleStatusIcon(container, primaryStatus); - } - } - /** * Add a single status icon */ private addSingleStatusIcon(container: HTMLElement, status: string): void { - // Create icon element instead of modifying container directly + // Create icon element const iconEl = document.createElement('span'); iconEl.className = `note-status-icon nav-file-tag status-${status}`; iconEl.textContent = this.statusService.getStatusIcon(status); @@ -256,26 +225,25 @@ export class ExplorerIntegration { } /** - * Update a single file's icon in the file explorer (public method) + * Update a file's icon (public method) */ public updateFileExplorerIcons(file: TFile): void { if (!file || !this.settings.showStatusIconsInExplorer || file.extension !== 'md') return; - // Add direct immediate update for critical files (like active file) + // Handle active file with higher priority 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 + // Also queue for normal processing this.queueFileUpdate(file); } /** - * Update a single file icon directly (for immediate updates) + * Update a single file icon directly */ private updateSingleFileIconDirectly(file: TFile): void { try { @@ -289,7 +257,7 @@ export class ExplorerIntegration { } /** - * Update all file icons in the explorer - with performance optimizations + * Update all file icons in the explorer */ public updateAllFileExplorerIcons(): void { // Remove all icons if setting is turned off @@ -299,23 +267,25 @@ export class ExplorerIntegration { } // Process files in batches - const processFilesInBatches = async () => { - const files = this.app.vault.getMarkdownFiles(); - const batchSize = 100; // Process 100 files at a time - - for (let i = 0; i < files.length; i += batchSize) { - const batch = files.slice(i, i + batchSize); - batch.forEach(file => this.queueFileUpdate(file)); - - // Let the UI breathe between batches - if (i + batchSize < files.length) { - await new Promise(resolve => setTimeout(resolve, 0)); - } - } - }; + this.processFilesInBatches(); + } + + /** + * Process files in batches to update icons + */ + private async processFilesInBatches(): Promise { + const files = this.app.vault.getMarkdownFiles(); + const batchSize = 100; // Process 100 files at a time - // Start processing - processFilesInBatches(); + for (let i = 0; i < files.length; i += batchSize) { + const batch = files.slice(i, i + batchSize); + batch.forEach(file => this.queueFileUpdate(file)); + + // Let the UI breathe between batches + if (i + batchSize < files.length) { + await new Promise(resolve => setTimeout(resolve, 0)); + } + } } /** From f8ffeeccf1c5bad155ddafae9afde7acd6d33b51 Mon Sep 17 00:00:00 2001 From: Aleix Date: Mon, 12 May 2025 16:19:24 +0200 Subject: [PATCH 05/67] fix: hide unkown status in explorer --- ui/integrations/explorer-integration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/integrations/explorer-integration.ts b/ui/integrations/explorer-integration.ts index 78b1108..3b08fea 100644 --- a/ui/integrations/explorer-integration.ts +++ b/ui/integrations/explorer-integration.ts @@ -194,7 +194,7 @@ export class ExplorerIntegration { } else { // Just show primary status const primaryStatus = statuses[0] || 'unknown'; - if (primaryStatus !== 'unknown' || !this.settings.autoHideStatusBar) { + if (primaryStatus !== 'unknown' || !this.settings.hideUnknownStatusInExplorer) { this.addSingleStatusIcon(iconContainer, primaryStatus); } } From ac5b4a7b9e823ad4a5ea75429c57655a5f5fe03c Mon Sep 17 00:00:00 2001 From: Aleix Date: Mon, 12 May 2025 16:24:49 +0200 Subject: [PATCH 06/67] fix: status icon visibility issues in file explorer --- settings/settings-tab.ts | 13 ++++++++++--- ui/integrations/explorer-integration.ts | 18 +++++++++++++----- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/settings/settings-tab.ts b/settings/settings-tab.ts index 928abe3..98cf799 100644 --- a/settings/settings-tab.ts +++ b/settings/settings-tab.ts @@ -53,9 +53,16 @@ export class NoteStatusSettingTab extends PluginSettingTab { .addToggle(toggle => toggle .setValue(this.plugin.settings.showStatusIconsInExplorer) .onChange(async (value) => { - this.plugin.settings.showStatusIconsInExplorer = value; - await this.plugin.saveSettings(); - })); + this.plugin.settings.showStatusIconsInExplorer = value; + await this.plugin.saveSettings(); + + // Explicitly refresh explorer icons + if (value) { + this.plugin.explorerIntegration.updateAllFileExplorerIcons(); + } else { + this.plugin.explorerIntegration.removeAllFileExplorerIcons(); + } + })); // NEW SETTING: Hide unknown status in explorer new Setting(containerEl) diff --git a/ui/integrations/explorer-integration.ts b/ui/integrations/explorer-integration.ts index 3b08fea..8390620 100644 --- a/ui/integrations/explorer-integration.ts +++ b/ui/integrations/explorer-integration.ts @@ -30,16 +30,24 @@ export class ExplorerIntegration { */ public updateSettings(settings: NoteStatusSettings): void { const previousShowIcons = this.settings.showStatusIconsInExplorer; + const previousHideUnknown = this.settings.hideUnknownStatusInExplorer; this.settings = settings; - if (previousShowIcons !== this.settings.showStatusIconsInExplorer) { + // Force refresh icons if relevant settings changed + if (previousShowIcons !== this.settings.showStatusIconsInExplorer || + previousHideUnknown !== this.settings.hideUnknownStatusInExplorer) { + // First remove all icons to ensure clean slate + this.removeAllFileExplorerIcons(); + + // Then add back if icons should be shown if (this.settings.showStatusIconsInExplorer) { - this.updateAllFileExplorerIcons(); - } else { - this.removeAllFileExplorerIcons(); + // Use a small delay to ensure DOM has been updated + setTimeout(() => { + this.updateAllFileExplorerIcons(); + }, 50); } } else if (this.settings.showStatusIconsInExplorer) { - // If setting hasn't changed but is enabled, refresh the icons + // If settings haven't changed but icons should be shown, refresh them this.updateAllFileExplorerIcons(); } } From 9401c961b14dcfa5e7c8944029360785badd6eba Mon Sep 17 00:00:00 2001 From: Aleix Date: Tue, 13 May 2025 08:45:13 +0200 Subject: [PATCH 07/67] refactor(ui): simplify StatusDropdownComponent with cleaner method organization and reduced redundancies --- ui/components/status-dropdown-component.ts | 215 ++++++--------------- 1 file changed, 54 insertions(+), 161 deletions(-) diff --git a/ui/components/status-dropdown-component.ts b/ui/components/status-dropdown-component.ts index 8d8ffc5..d028553 100644 --- a/ui/components/status-dropdown-component.ts +++ b/ui/components/status-dropdown-component.ts @@ -4,7 +4,6 @@ import { StatusService } from '../../services/status-service'; /** * Unified dropdown component for status selection - * This component handles the UI for selecting statuses across multiple contexts */ export class StatusDropdownComponent { private app: App; @@ -12,37 +11,28 @@ export class StatusDropdownComponent { private settings: NoteStatusSettings; private dropdownElement: HTMLElement | null = null; private currentStatuses: string[] = ['unknown']; - private onStatusChange: (statuses: string[]) => void; + private onStatusChange: (statuses: string[]) => void = () => {}; private animationDuration = 220; - private clickOutsideHandler: (e: MouseEvent) => void; private targetFile: TFile | null = null; public isOpen = false; + // Bind methods once in constructor + private clickOutsideHandler: (e: MouseEvent) => void; constructor(app: App, statusService: StatusService, settings: NoteStatusSettings) { this.app = app; this.statusService = statusService; this.settings = settings; - this.onStatusChange = () => {}; - // Bind methods - this.handleClickOutside = this.handleClickOutside.bind(this); - this.handleEscapeKey = this.handleEscapeKey.bind(this); - this.clickOutsideHandler = this.handleClickOutside; + this.clickOutsideHandler = this.handleClickOutside.bind(this); } /** * Updates the current statuses */ public updateStatuses(statuses: string[] | string): void { - // Normalize input to always be an array - if (typeof statuses === 'string') { - this.currentStatuses = [statuses]; - } else { - this.currentStatuses = [...statuses]; // Create a copy to ensure it's updated - } + this.currentStatuses = Array.isArray(statuses) ? [...statuses] : [statuses]; - // Update dropdown if it's open if (this.isOpen && this.dropdownElement) { this.refreshDropdownContent(); } @@ -61,7 +51,6 @@ export class StatusDropdownComponent { public updateSettings(settings: NoteStatusSettings): void { this.settings = settings; - // Update dropdown if it's open if (this.isOpen && this.dropdownElement) { this.refreshDropdownContent(); } @@ -87,10 +76,7 @@ export class StatusDropdownComponent { public toggle(targetEl: HTMLElement, position?: { x: number, y: number }): void { if (this.isOpen) { this.close(); - // Add small delay to ensure dropdown is fully closed before potentially reopening - // This prevents state conflicts when clicking in rapid succession setTimeout(() => { - // Check if we're still in the closing state to prevent reopening if user clicked elsewhere if (!this.isOpen && !this.dropdownElement) { this.open(targetEl, position); } @@ -104,13 +90,9 @@ export class StatusDropdownComponent { * Open the dropdown */ public open(targetEl: HTMLElement, position?: { x: number, y: number }): void { - // Make sure any existing dropdown is fully closed if (this.isOpen || this.dropdownElement) { this.close(); - // Add small delay before opening new dropdown to ensure proper cleanup - setTimeout(() => { - this.actuallyOpen(targetEl, position); - }, 10); + setTimeout(() => this.actuallyOpen(targetEl, position), 10); return; } @@ -120,23 +102,17 @@ export class StatusDropdownComponent { private actuallyOpen(targetEl: HTMLElement, position?: { x: number, y: number }): void { this.isOpen = true; - // Create dropdown element + // Create and position dropdown this.createDropdownElement(); - - // Fill dropdown content this.refreshDropdownContent(); - // Position the dropdown - if (position) { - this.positionAt(position.x, position.y); - } else { + position ? + this.positionAt(position.x, position.y) : this.positionRelativeTo(targetEl); - } - // Add animation class this.dropdownElement?.addClass('note-status-popover-animate-in'); - // Add event listeners + // Add event listeners with slight delay to prevent immediate triggering setTimeout(() => { document.addEventListener('click', this.clickOutsideHandler); document.addEventListener('keydown', this.handleEscapeKey); @@ -158,17 +134,13 @@ export class StatusDropdownComponent { public close(): void { if (!this.dropdownElement || !this.isOpen) return; - // Add exit animation this.dropdownElement.addClass('note-status-popover-animate-out'); - // Clean up event listeners document.removeEventListener('click', this.clickOutsideHandler); document.removeEventListener('keydown', this.handleEscapeKey); - // Set isOpen to false immediately to prevent multiple open/close conflicts this.isOpen = false; - // Remove after animation completes setTimeout(() => { if (this.dropdownElement) { this.dropdownElement.remove(); @@ -183,16 +155,11 @@ export class StatusDropdownComponent { private refreshDropdownContent(): void { if (!this.dropdownElement) return; - // Clear content this.dropdownElement.empty(); - // Create header + // Create UI sections this.createHeader(); - - // Create status chips section this.createStatusChips(); - - // Create search filter const searchInput = this.createSearchFilter(); // Create status options container @@ -200,11 +167,11 @@ export class StatusDropdownComponent { cls: 'note-status-options-container' }); - // Get all available statuses + // Get all available statuses (excluding 'unknown') const allStatuses = this.statusService.getAllStatuses() .filter(status => status.name !== 'unknown'); - // Create a function to populate options with filtering + // Function to populate options with filtering const populateOptions = (filter = '') => { this.populateStatusOptions(statusOptionsContainer, allStatuses, filter); }; @@ -218,9 +185,7 @@ export class StatusDropdownComponent { }); // Focus search input after a short delay - setTimeout(() => { - searchInput.focus(); - }, 50); + setTimeout(() => searchInput.focus(), 50); } /** @@ -230,11 +195,11 @@ export class StatusDropdownComponent { if (!this.dropdownElement) return; const headerEl = this.dropdownElement.createDiv({ cls: 'note-status-popover-header' }); - - // Title with icon const titleEl = headerEl.createDiv({ cls: 'note-status-popover-title' }); + const iconContainer = titleEl.createDiv({ cls: 'note-status-popover-icon' }); setIcon(iconContainer, 'tag'); + titleEl.createSpan({ text: 'Note status', cls: 'note-status-popover-label' }); } @@ -246,39 +211,25 @@ export class StatusDropdownComponent { const chipsContainer = this.dropdownElement.createDiv({ cls: 'note-status-popover-chips' }); - // Show 'No status' indicator if no statuses or only unknown status - if (this.hasNoValidStatus()) { - this.createEmptyStatusIndicator(chipsContainer); + const hasNoValidStatus = this.currentStatuses.length === 0 || + (this.currentStatuses.length === 1 && this.currentStatuses[0] === 'unknown'); + + if (hasNoValidStatus) { + chipsContainer.createDiv({ + cls: 'note-status-empty-indicator', + text: 'No status assigned' + }); } else { - // Add chip for each status this.createStatusChipElements(chipsContainer); } } - /** - * Check if there are no valid statuses - */ - private hasNoValidStatus(): boolean { - return this.currentStatuses.length === 0 || - (this.currentStatuses.length === 1 && this.currentStatuses[0] === 'unknown'); - } - - /** - * Create empty status indicator - */ - private createEmptyStatusIndicator(container: HTMLElement): void { - container.createDiv({ - cls: 'note-status-empty-indicator', - text: 'No status assigned' - }); - } - /** * Create chips for all current statuses */ private createStatusChipElements(container: HTMLElement): void { this.currentStatuses.forEach(status => { - if (status === 'unknown') return; // Skip unknown status + if (status === 'unknown') return; const statusObj = this.statusService.getAllStatuses().find(s => s.name === status); if (!statusObj) return; @@ -295,20 +246,17 @@ export class StatusDropdownComponent { cls: `note-status-chip status-${status}` }); - // Status icon + // Status icon and name chipEl.createSpan({ text: statusObj.icon, cls: 'note-status-chip-icon' }); - // Status name chipEl.createSpan({ text: statusObj.name, cls: 'note-status-chip-text' }); - // Add remove button regardless of the number of statuses - // This allows removal even when there's only one status this.addRemoveButton(chipEl, status); } @@ -319,7 +267,6 @@ export class StatusDropdownComponent { const statusObj = this.statusService.getAllStatuses().find(s => s.name === status); const tooltipValue = statusObj?.description ? `${status} - ${statusObj.description}`: status; - // Add tooltip to the chip element setTooltip(chipEl, tooltipValue); const removeBtn = chipEl.createDiv({ @@ -334,17 +281,12 @@ export class StatusDropdownComponent { removeBtn.addEventListener('click', async (e) => { e.stopPropagation(); - // Add remove animation chipEl.addClass('note-status-chip-removing'); - // Wait for animation to complete before actually removing setTimeout(async () => { if (this.targetFile) { - // Remove status from single file await this.removeStatus(status); } else { - // This is a batch operation - remove from all files - // Call the onStatusChange callback with the status to be removed this.onStatusChange([status]); } }, 150); @@ -357,19 +299,12 @@ export class StatusDropdownComponent { private async removeStatus(status: string): Promise { if (!this.targetFile) return; - // Remove this status await this.statusService.removeNoteStatus(status, this.targetFile); - // Get updated statuses const updatedStatuses = this.statusService.getFileStatuses(this.targetFile); - - // Update current statuses this.currentStatuses = updatedStatuses; - // Refresh chips this.refreshDropdownContent(); - - // Call status change callback this.onStatusChange(updatedStatuses); } @@ -382,13 +317,12 @@ export class StatusDropdownComponent { } const searchContainer = this.dropdownElement.createDiv({ cls: 'note-status-popover-search' }); - const searchInput = searchContainer.createEl('input', { + + return searchContainer.createEl('input', { type: 'text', placeholder: 'Filter statuses...', cls: 'note-status-popover-search-input' }); - - return searchInput; } /** @@ -401,10 +335,18 @@ export class StatusDropdownComponent { ): void { container.empty(); - const filteredStatuses = this.filterStatuses(statuses, filter); + const filteredStatuses = filter ? + statuses.filter(status => + status.name.toLowerCase().includes(filter.toLowerCase()) || + status.icon.includes(filter) + ) : + statuses; if (filteredStatuses.length === 0) { - this.createNoMatchingStatusesMessage(container, filter); + container.createDiv({ + cls: 'note-status-empty-options', + text: filter ? `No statuses match "${filter}"` : 'No statuses found' + }); return; } @@ -413,28 +355,6 @@ export class StatusDropdownComponent { }); } - /** - * Filter statuses based on search term - */ - private filterStatuses(statuses: Status[], filter: string): Status[] { - if (!filter) return statuses; - - return statuses.filter(status => - status.name.toLowerCase().includes(filter.toLowerCase()) || - status.icon.includes(filter) - ); - } - - /** - * Create a message for no matching statuses - */ - private createNoMatchingStatusesMessage(container: HTMLElement, filter: string): void { - container.createDiv({ - cls: 'note-status-empty-options', - text: filter ? `No statuses match "${filter}"` : 'No statuses found' - }); - } - /** * Create a single status option element */ @@ -445,19 +365,18 @@ export class StatusDropdownComponent { cls: `note-status-option ${isSelected ? 'is-selected' : ''} status-${status.name}` }); - // Status icon + // Status icon and name optionEl.createSpan({ text: status.icon, cls: 'note-status-option-icon' }); - // Status name optionEl.createSpan({ text: status.name, cls: 'note-status-option-text' }); - // Add tooltip with description if available + // Add tooltip if description available if (status.description) { setTooltip(optionEl, `${status.name} - ${status.description}`); } @@ -468,7 +387,6 @@ export class StatusDropdownComponent { setIcon(checkIcon, 'check'); } - // Add click handler optionEl.addEventListener('click', () => this.handleStatusOptionClick(optionEl, status)); } @@ -476,27 +394,19 @@ export class StatusDropdownComponent { * Handle click on a status option */ private handleStatusOptionClick(optionEl: HTMLElement, status: Status): void { - try { - // Add selection animation - optionEl.addClass('note-status-option-selecting'); - - // Apply status changes after brief delay for animation - setTimeout(async () => { - if (this.targetFile) { - await this.handleStatusChangeForTargetFile(status); - } else { - // This is for batch operations or when no specific file is targeted - this.onStatusChange([status.name]); - - // Usually we want to close after selection in this case - if (!this.settings.useMultipleStatuses) { - this.close(); - } + optionEl.addClass('note-status-option-selecting'); + + setTimeout(async () => { + if (this.targetFile) { + await this.handleStatusChangeForTargetFile(status); + } else { + this.onStatusChange([status.name]); + + if (!this.settings.useMultipleStatuses) { + this.close(); } - }, 150); - } catch (error) { - console.error('Error updating status:', error); - } + } + }, 150); } /** @@ -509,17 +419,11 @@ export class StatusDropdownComponent { await this.statusService.toggleNoteStatus(status.name, this.targetFile); } else { await this.statusService.updateNoteStatuses([status.name], this.targetFile); - // Close dropdown in single status mode this.close(); } - // Get fresh status from file const freshStatuses = this.statusService.getFileStatuses(this.targetFile); - - // Update current statuses this.currentStatuses = [...freshStatuses]; - - // Call status change callback this.onStatusChange(freshStatuses); } @@ -533,7 +437,6 @@ export class StatusDropdownComponent { this.dropdownElement.style.setProperty('--pos-x-px', `${x}px`); this.dropdownElement.style.setProperty('--pos-y-px', `${y}px`); - // Ensure dropdown doesn't go off-screen setTimeout(() => this.adjustPositionToViewport(), 0); } @@ -559,7 +462,6 @@ export class StatusDropdownComponent { this.dropdownElement.removeClass('note-status-popover-bottom'); } - // Set max height based on viewport const maxHeight = window.innerHeight - rect.top - 20; this.dropdownElement.style.setProperty('--max-height-px', `${maxHeight}px`); } @@ -570,19 +472,13 @@ export class StatusDropdownComponent { private positionRelativeTo(targetEl: HTMLElement): void { if (!this.dropdownElement) return; - // Apply positioning class this.dropdownElement.addClass('note-status-popover-fixed'); - // Get target element's position const targetRect = targetEl.getBoundingClientRect(); - // Position below the element - using CSS custom properties this.dropdownElement.style.setProperty('--pos-y-px', `${targetRect.bottom + 5}px`); - - // Align to left edge of target by default this.dropdownElement.style.setProperty('--pos-x-px', `${targetRect.left}px`); - // Check if dropdown would go off-screen setTimeout(() => this.adjustRelativePosition(targetRect), 0); } @@ -595,7 +491,6 @@ export class StatusDropdownComponent { const rect = this.dropdownElement.getBoundingClientRect(); if (rect.right > window.innerWidth) { - // Align to right edge instead this.dropdownElement.addClass('note-status-popover-right'); this.dropdownElement.style.setProperty('--right-offset-px', `${window.innerWidth - targetRect.right}px`); } else { @@ -603,14 +498,12 @@ export class StatusDropdownComponent { } if (rect.bottom > window.innerHeight) { - // Position above the element instead this.dropdownElement.addClass('note-status-popover-bottom'); this.dropdownElement.style.setProperty('--bottom-offset-px', `${window.innerHeight - targetRect.top + 5}px`); } else { this.dropdownElement.removeClass('note-status-popover-bottom'); } - // Set max height based on viewport const maxHeight = window.innerHeight - rect.top - 20; this.dropdownElement.style.setProperty('--max-height-px', `${maxHeight}px`); } @@ -627,7 +520,7 @@ export class StatusDropdownComponent { /** * Handle escape key to close dropdown */ - private handleEscapeKey(e: KeyboardEvent): void { + private handleEscapeKey = (e: KeyboardEvent): void => { if (e.key === 'Escape') { this.close(); } @@ -639,4 +532,4 @@ export class StatusDropdownComponent { public dispose(): void { this.close(); } -} +} \ No newline at end of file From 2fed87838bbefbf5fd479927001f72b25125621c Mon Sep 17 00:00:00 2001 From: Aleix Date: Tue, 13 May 2025 08:52:31 +0200 Subject: [PATCH 08/67] refactor(services): streamline StatusService with improved type handling and functional patterns --- services/status-service.ts | 196 +++++++++++++------------------------ 1 file changed, 66 insertions(+), 130 deletions(-) diff --git a/services/status-service.ts b/services/status-service.ts index 74474f6..f714d44 100644 --- a/services/status-service.ts +++ b/services/status-service.ts @@ -35,49 +35,31 @@ export class StatusService { if (!this.settings.useCustomStatusesOnly) { const templateStatuses = this.getTemplateStatuses(); - // Add template statuses that don't have the same name as existing statuses for (const status of templateStatuses) { - if (!this.allStatuses.some(s => s.name.toLowerCase() === status.name.toLowerCase())) { + const existingIndex = this.allStatuses.findIndex(s => + s.name.toLowerCase() === status.name.toLowerCase()); + + if (existingIndex === -1) { + // Add new status this.allStatuses.push(status); - } else { - this.updateExistingStatusColor(status); + } else if (status.color) { + // Update color in settings if it doesn't exist + if (!this.settings.statusColors[status.name]) { + this.settings.statusColors[status.name] = status.color; + } } } } } - /** - * Update color for an existing status if it comes from a template - */ - private updateExistingStatusColor(status: Status): void { - // Update status colors if they come from a template and have colors - const existingStatusIndex = this.allStatuses.findIndex( - s => s.name.toLowerCase() === status.name.toLowerCase() - ); - - if (existingStatusIndex !== -1 && status.color) { - // Update color in settings if it doesn't exist - if (!this.settings.statusColors[status.name]) { - this.settings.statusColors[status.name] = status.color; - } - } - } - /** * Gets all statuses from enabled templates */ public getTemplateStatuses(): Status[] { - const statuses: Status[] = []; - - // Find templates that are enabled - for (const templateId of this.settings.enabledTemplates) { - const template = PREDEFINED_TEMPLATES.find(t => t.id === templateId); - if (template) { - statuses.push(...template.statuses); - } - } - - return statuses; + return this.settings.enabledTemplates + .map(id => PREDEFINED_TEMPLATES.find(t => t.id === id)) + .filter(Boolean) + .flatMap(template => template ? template.statuses : []); } /** @@ -89,55 +71,38 @@ export class StatusService { /** * Get the statuses of a file from its metadata - * Returns an array of status names */ public getFileStatuses(file: TFile): string[] { const cachedMetadata = this.app.metadataCache.getFileCache(file); - const statuses: string[] = []; + if (!cachedMetadata?.frontmatter) return ['unknown']; - if (cachedMetadata?.frontmatter) { - // Check for status using the configured tag prefix - const frontmatterStatus = cachedMetadata.frontmatter[this.settings.tagPrefix]; - - if (frontmatterStatus) { - if (Array.isArray(frontmatterStatus)) { - // Handle array format - this.processStatusArray(frontmatterStatus, statuses); - } else { - // Handle single value format (string) - convert to array format internally - this.processSingleStatus(frontmatterStatus.toString(), statuses); - } + const frontmatterStatus = cachedMetadata.frontmatter[this.settings.tagPrefix]; + if (!frontmatterStatus) return ['unknown']; + + const statuses: string[] = []; + + if (Array.isArray(frontmatterStatus)) { + for (const statusName of frontmatterStatus) { + this.addValidStatus(statusName.toString(), statuses); } + } else { + this.addValidStatus(frontmatterStatus.toString(), statuses); } - // Return 'unknown' if no statuses found return statuses.length > 0 ? statuses : ['unknown']; } /** - * Process an array of statuses from frontmatter + * Add a status to the list if it's valid */ - private processStatusArray(statusArray: any[], targetStatuses: string[]): void { - for (const statusName of statusArray) { - const normalizedStatus = statusName.toString().toLowerCase(); - const matchingStatus = this.allStatuses.find(s => - s.name.toLowerCase() === normalizedStatus); - - if (matchingStatus) { - targetStatuses.push(matchingStatus.name); - } - } - } - - /** - * Process a single status string from frontmatter - */ - private processSingleStatus(statusString: string, targetStatuses: string[]): void { - const normalizedStatus = statusString.toLowerCase(); - const matchingStatus = this.allStatuses.find(s => + private addValidStatus(statusName: string, targetStatuses: string[]): void { + const normalizedStatus = statusName.toLowerCase(); + const matchingStatus = this.allStatuses.find(s => s.name.toLowerCase() === normalizedStatus); - - if (matchingStatus) targetStatuses.push(matchingStatus.name); + + if (matchingStatus) { + targetStatuses.push(matchingStatus.name); + } } /** @@ -160,29 +125,22 @@ export class StatusService { /** * Update the statuses of a note - * @param newStatuses Array of status names to set - * @param file Optional file to update, otherwise uses active file */ public async updateNoteStatuses(newStatuses: string[], file?: TFile): Promise { const targetFile = file || this.app.workspace.getActiveFile(); - if (!targetFile || !(targetFile instanceof TFile) || targetFile.extension !== 'md') return; + if (!targetFile || targetFile.extension !== 'md') return; - // Use processFrontMatter to handle the update await this.app.fileManager.processFrontMatter(targetFile, (frontmatter) => { frontmatter[this.settings.tagPrefix] = newStatuses; }); - // Dispatch events for UI update - window.dispatchEvent(new CustomEvent('note-status:status-changed', { - detail: { statuses: newStatuses, file: targetFile.path } - })); + this.notifyStatusChanged(newStatuses, targetFile); } /** * Legacy method to update a single status for backward compatibility */ public async updateNoteStatus(newStatus: string, file?: TFile): Promise { - // Always store as an array even in single status mode await this.updateNoteStatuses([newStatus], file); } @@ -191,17 +149,13 @@ export class StatusService { */ public async addNoteStatus(statusToAdd: string, file?: TFile): Promise { const targetFile = file || this.app.workspace.getActiveFile(); - if (!targetFile || !(targetFile instanceof TFile) || targetFile.extension !== 'md') return; + if (!targetFile || targetFile.extension !== 'md') return; const currentStatuses = this.getFileStatuses(targetFile); - - // Don't add if already exists if (currentStatuses.includes(statusToAdd)) return; // Filter out 'unknown' status when adding valid statuses - const filteredStatuses = currentStatuses.filter(s => s !== 'unknown'); - const newStatuses = [...filteredStatuses, statusToAdd]; - + const newStatuses = [...currentStatuses.filter(s => s !== 'unknown'), statusToAdd]; await this.updateNoteStatuses(newStatuses, targetFile); } @@ -210,7 +164,7 @@ export class StatusService { */ public async removeNoteStatus(statusToRemove: string, file?: TFile): Promise { const targetFile = file || this.app.workspace.getActiveFile(); - if (!targetFile || !(targetFile instanceof TFile) || targetFile.extension !== 'md') return; + if (!targetFile || targetFile.extension !== 'md') return; const currentStatuses = this.getFileStatuses(targetFile); const newStatuses = currentStatuses.filter(status => status !== statusToRemove); @@ -226,15 +180,10 @@ export class StatusService { if (!targetFile || targetFile.extension !== 'md') return; const currentStatuses = this.getFileStatuses(targetFile); - let newStatuses: string[]; - if (currentStatuses.includes(statusToToggle)) { - newStatuses = currentStatuses.filter(status => status !== statusToToggle); - } else { - // Filter out 'unknown' status when adding valid statuses - const filteredStatuses = currentStatuses.filter(s => s !== 'unknown'); - newStatuses = [...filteredStatuses, statusToToggle]; - } + const newStatuses = currentStatuses.includes(statusToToggle) + ? currentStatuses.filter(status => status !== statusToToggle) + : [...currentStatuses.filter(s => s !== 'unknown'), statusToToggle]; await this.updateNoteStatuses(newStatuses, targetFile); } @@ -249,40 +198,39 @@ export class StatusService { showNotice = true ): Promise { if (files.length === 0) { - if (showNotice) { - new Notice('No files selected'); - } + if (showNotice) new Notice('No files selected'); return; } - for (const file of files) { + const updatePromises = files.map(async (file) => { if (mode === 'replace') { await this.updateNoteStatuses(statusesToSet, file); } else { - // Add each status for (const status of statusesToSet) { await this.addNoteStatus(status, file); } } - } + }); + + await Promise.all(updatePromises); - // Show notice if requested and we're dealing with multiple files if (showNotice && files.length > 1) { - const statusText = this.formatStatusText(statusesToSet); + const statusText = statusesToSet.length === 1 + ? statusesToSet[0] + : `${statusesToSet.length} statuses`; new Notice(`Updated ${files.length} files with ${statusText}`); } - // Trigger UI updates window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); } /** - * Format status text for notifications + * Dispatch status changed event */ - private formatStatusText(statusesToSet: string[]): string { - return statusesToSet.length === 1 - ? statusesToSet[0] - : `${statusesToSet.length} statuses`; + private notifyStatusChanged(statuses: string[], file: TFile): void { + window.dispatchEvent(new CustomEvent('note-status:status-changed', { + detail: { statuses, file: file.path } + })); } /** @@ -313,16 +261,11 @@ export class StatusService { statusMetadata: string ): void { const frontMatter = frontMatterMatch[1]; - let updatedFrontMatter = frontMatter; - - // Check if status tag already exists in frontmatter const statusTagRegex = new RegExp(`${this.settings.tagPrefix}:\\s*\\[?[^\\]]*\\]?`, 'm'); - if (frontMatter.match(statusTagRegex)) { - updatedFrontMatter = frontMatter.replace(statusTagRegex, statusMetadata); - } else { - updatedFrontMatter = `${frontMatter}\n${statusMetadata}`; - } + const updatedFrontMatter = frontMatter.match(statusTagRegex) + ? frontMatter.replace(statusTagRegex, statusMetadata) + : `${frontMatter}\n${statusMetadata}`; const updatedContent = content.replace(/^---\n([\s\S]+?)\n---/, `---\n${updatedFrontMatter}\n---`); editor.setValue(updatedContent); @@ -341,14 +284,11 @@ export class StatusService { */ public getMarkdownFiles(searchQuery = ''): TFile[] { const files = this.app.vault.getMarkdownFiles(); + if (!searchQuery) return files; - if (!searchQuery) { - return files; - } - - return files.filter(file => - file.basename.toLowerCase().includes(searchQuery.toLowerCase()) - ); + const lowerQuery = searchQuery.toLowerCase(); + return files.filter(file => + file.basename.toLowerCase().includes(lowerQuery)); } /** @@ -358,22 +298,18 @@ export class StatusService { const statusGroups: Record = {}; // Initialize groups for all statuses - this.allStatuses.forEach(status => { + for (const status of this.allStatuses) { statusGroups[status.name] = []; - }); - - // Ensure 'unknown' status is included - if (!statusGroups['unknown']) { - statusGroups['unknown'] = []; } + + // Ensure 'unknown' status exists + statusGroups['unknown'] = statusGroups['unknown'] || []; - // Get all markdown files and filter by search query + // Get and process all files matching the search query const files = this.getMarkdownFiles(searchQuery); - for (const file of files) { const statuses = this.getFileStatuses(file); - // Add file to each of its status groups for (const status of statuses) { if (statusGroups[status]) { statusGroups[status].push(file); From f22ee80521129ddf4e865eb79b67346134e6e6bc Mon Sep 17 00:00:00 2001 From: Aleix Date: Tue, 13 May 2025 09:01:20 +0200 Subject: [PATCH 09/67] refactor(ui): optimize ExplorerIntegration with batched file processing and improved icon handling --- ui/integrations/explorer-integration.ts | 189 +++++++++--------------- 1 file changed, 72 insertions(+), 117 deletions(-) diff --git a/ui/integrations/explorer-integration.ts b/ui/integrations/explorer-integration.ts index 8390620..c139fc4 100644 --- a/ui/integrations/explorer-integration.ts +++ b/ui/integrations/explorer-integration.ts @@ -3,51 +3,49 @@ import { FileExplorerView, NoteStatusSettings } from '../../models/types'; import { StatusService } from '../../services/status-service'; /** - * Enhanced file explorer integration for displaying status icons + * Enhanced file explorer integration for status icons */ export class ExplorerIntegration { private app: App; private settings: NoteStatusSettings; private statusService: StatusService; - private iconUpdateQueue: Set = new Set(); + private iconUpdateQueue = new Set(); private isProcessingQueue = false; - // Debounced update function for better performance - private debouncedUpdateAll = debounce( - () => this.processUpdateQueue(), - 100, - true - ); + // Define with correct typing to match Obsidian's debounce return type + private debouncedUpdateAll: { + (): void; + cancel: () => void; + }; constructor(app: App, settings: NoteStatusSettings, statusService: StatusService) { this.app = app; this.settings = settings; this.statusService = statusService; + this.debouncedUpdateAll = debounce( + () => this.processUpdateQueue(), + 100, + true + ); } /** - * Update settings reference and refresh icons + * Update settings and refresh UI as needed */ public updateSettings(settings: NoteStatusSettings): void { - const previousShowIcons = this.settings.showStatusIconsInExplorer; - const previousHideUnknown = this.settings.hideUnknownStatusInExplorer; + const shouldRefreshIcons = + this.settings.showStatusIconsInExplorer !== settings.showStatusIconsInExplorer || + this.settings.hideUnknownStatusInExplorer !== settings.hideUnknownStatusInExplorer; + this.settings = settings; - // Force refresh icons if relevant settings changed - if (previousShowIcons !== this.settings.showStatusIconsInExplorer || - previousHideUnknown !== this.settings.hideUnknownStatusInExplorer) { - // First remove all icons to ensure clean slate + if (shouldRefreshIcons) { this.removeAllFileExplorerIcons(); - // Then add back if icons should be shown - if (this.settings.showStatusIconsInExplorer) { - // Use a small delay to ensure DOM has been updated - setTimeout(() => { - this.updateAllFileExplorerIcons(); - }, 50); + if (settings.showStatusIconsInExplorer) { + setTimeout(() => this.updateAllFileExplorerIcons(), 50); } - } else if (this.settings.showStatusIconsInExplorer) { - // If settings haven't changed but icons should be shown, refresh them + } else if (settings.showStatusIconsInExplorer) { this.updateAllFileExplorerIcons(); } } @@ -56,13 +54,9 @@ export class ExplorerIntegration { * Find the file explorer view */ 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 (leaf?.view) return leaf.view as FileExplorerView; - // Fallback to searching all leaves for (const leaf of this.app.workspace.getLeavesOfType('')) { if (leaf.view && 'fileItems' in leaf.view) { return leaf.view as FileExplorerView; @@ -73,7 +67,7 @@ export class ExplorerIntegration { } /** - * Add a file to the update queue + * Queue a file for icon update */ public queueFileUpdate(file: TFile): void { if (!this.settings.showStatusIconsInExplorer || file.extension !== 'md') return; @@ -92,29 +86,17 @@ export class ExplorerIntegration { try { const fileExplorerView = this.findFileExplorerView(); - if (!fileExplorerView || !fileExplorerView.fileItems) { - // Schedule retry if view not found + if (!fileExplorerView) { setTimeout(() => this.debouncedUpdateAll(), 200); return; } - // Process files in the queue in batches - const batchSize = 50; const allPaths = Array.from(this.iconUpdateQueue); + const batchSize = 50; for (let i = 0; i < allPaths.length; i += batchSize) { - const batch = allPaths.slice(i, i + batchSize); + await this.processBatch(allPaths.slice(i, i + batchSize), fileExplorerView); - // Process this 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); - } - - // Let the UI breathe between batches if (i + batchSize < allPaths.length) { await new Promise(resolve => setTimeout(resolve, 0)); } @@ -124,36 +106,44 @@ export class ExplorerIntegration { } finally { this.isProcessingQueue = false; - // If more files were added while processing, process them too if (this.iconUpdateQueue.size > 0) { this.debouncedUpdateAll(); } } } + + /** + * Process a batch of files + */ + private async processBatch(paths: string[], fileExplorerView: FileExplorerView): Promise { + for (const path of paths) { + const file = this.app.vault.getFileByPath(path); + if (file instanceof TFile) { + this.updateSingleFileIcon(file, fileExplorerView); + } + this.iconUpdateQueue.delete(path); + } + } /** - * Update a single file's icon in the file explorer + * Update a single file's icon in the explorer */ private updateSingleFileIcon(file: TFile, fileExplorerView: FileExplorerView): void { if (!this.settings.showStatusIconsInExplorer || file.extension !== 'md') return; try { const fileItem = fileExplorerView.fileItems[file.path]; - if (!fileItem) return; // File not in explorer view + if (!fileItem) return; const titleEl = fileItem.titleEl || fileItem.selfEl; - if (!titleEl) return; // No title element found + if (!titleEl) return; - // Get statuses for this file const statuses = this.statusService.getFileStatuses(file); - // Remove existing status icons if present this.removeExistingIcons(titleEl); - // Hide unknown status if setting is enabled - if (this.shouldHideUnknownStatus(statuses)) return; + if (this.shouldSkipIcon(statuses)) return; - // Add status icons this.addStatusIcons(titleEl, statuses); } catch (error) { console.error(`Note Status: Error updating icon for ${file.path}`, error); @@ -161,23 +151,20 @@ export class ExplorerIntegration { } /** - * Check if unknown status should be hidden + * Check if icon should be skipped */ - private shouldHideUnknownStatus(statuses: string[]): boolean { + private shouldSkipIcon(statuses: string[]): boolean { return this.settings.hideUnknownStatusInExplorer && statuses.length === 1 && statuses[0] === 'unknown'; } /** - * Remove existing status icons from an element + * Remove existing status icons */ private removeExistingIcons(element: HTMLElement): void { - // Only select our specific status icon elements - const existingIcons = element.querySelectorAll('.note-status-icon, .note-status-icon-container'); - - existingIcons.forEach(icon => { - // Only remove elements with our classes + const iconSelectors = '.note-status-icon, .note-status-icon-container'; + element.querySelectorAll(iconSelectors).forEach(icon => { if (icon.classList.contains('note-status-icon') || icon.classList.contains('note-status-icon-container')) { icon.remove(); @@ -186,30 +173,22 @@ export class ExplorerIntegration { } /** - * Add status icons to a title element + * Add status icons to an element */ private addStatusIcons(titleEl: HTMLElement, statuses: string[]): void { - // Create container for multiple icons const iconContainer = document.createElement('span'); iconContainer.className = 'note-status-icon-container'; - // Add all status icons based on settings if (this.settings.useMultipleStatuses && statuses.length > 0 && statuses[0] !== 'unknown') { - // Add all icons if using multiple statuses - statuses.forEach(status => { - this.addSingleStatusIcon(iconContainer, status); - }); + statuses.forEach(status => this.addSingleStatusIcon(iconContainer, status)); } else { - // Just show primary status const primaryStatus = statuses[0] || 'unknown'; if (primaryStatus !== 'unknown' || !this.settings.hideUnknownStatusInExplorer) { this.addSingleStatusIcon(iconContainer, primaryStatus); } } - // Only append if we added icons if (iconContainer.childElementCount > 0) { - // Use appendChild to add to the end titleEl.appendChild(iconContainer); } } @@ -218,78 +197,63 @@ export class ExplorerIntegration { * Add a single status icon */ private addSingleStatusIcon(container: HTMLElement, status: string): void { - // Create icon element const iconEl = document.createElement('span'); iconEl.className = `note-status-icon nav-file-tag status-${status}`; iconEl.textContent = this.statusService.getStatusIcon(status); - // Add tooltip with status name const statusObj = this.statusService.getAllStatuses().find(s => s.name === status); const tooltipValue = statusObj?.description ? `${status} - ${statusObj.description}`: status; setTooltip(iconEl, tooltipValue); - // Append to container container.appendChild(iconEl); } /** - * Update a file's icon (public method) + * Update file explorer icons for a file */ public updateFileExplorerIcons(file: TFile): void { if (!file || !this.settings.showStatusIconsInExplorer || file.extension !== 'md') return; - // Handle active file with higher priority const activeFile = this.app.workspace.getActiveFile(); - const isActiveFile = activeFile && activeFile.path === file.path; - - if (isActiveFile) { + if (activeFile?.path === file.path) { this.updateSingleFileIconDirectly(file); } - // Also queue for normal processing this.queueFileUpdate(file); } /** - * Update a single file icon directly + * Update active file icon directly */ private updateSingleFileIconDirectly(file: TFile): void { - try { - const fileExplorer = this.findFileExplorerView(); - if (!fileExplorer) return; - + const fileExplorer = this.findFileExplorerView(); + if (fileExplorer) { this.updateSingleFileIcon(file, fileExplorer); - } catch (error) { - console.error('Note Status: Error updating file icon directly', error); } } /** - * Update all file icons in the explorer + * Update all file icons in explorer */ public updateAllFileExplorerIcons(): void { - // Remove all icons if setting is turned off if (!this.settings.showStatusIconsInExplorer) { this.removeAllFileExplorerIcons(); return; } - // Process files in batches this.processFilesInBatches(); } /** - * Process files in batches to update icons + * Process files in batches */ private async processFilesInBatches(): Promise { const files = this.app.vault.getMarkdownFiles(); - const batchSize = 100; // Process 100 files at a time + const batchSize = 100; for (let i = 0; i < files.length; i += batchSize) { - const batch = files.slice(i, i + batchSize); - batch.forEach(file => this.queueFileUpdate(file)); + files.slice(i, i + batchSize).forEach(file => this.queueFileUpdate(file)); - // Let the UI breathe between batches if (i + batchSize < files.length) { await new Promise(resolve => setTimeout(resolve, 0)); } @@ -297,46 +261,37 @@ export class ExplorerIntegration { } /** - * Remove all status icons from the file explorer + * Remove all status icons */ public removeAllFileExplorerIcons(): void { const fileExplorer = this.findFileExplorerView(); - if (!fileExplorer || !fileExplorer.fileItems) return; + if (!fileExplorer?.fileItems) return; - // Use Object.values to avoid issues with concurrent modification - Object.values(fileExplorer.fileItems).forEach((fileItem) => { + Object.values(fileExplorer.fileItems).forEach(fileItem => { const titleEl = fileItem.titleEl || fileItem.selfEl; - if (!titleEl) return; - - this.removeExistingIcons(titleEl); + if (titleEl) this.removeExistingIcons(titleEl); }); - // Clear the update queue this.iconUpdateQueue.clear(); } /** - * Get selected files from the file explorer + * Get selected files from explorer */ public getSelectedFiles(): TFile[] { const fileExplorer = this.findFileExplorerView(); - if (!fileExplorer || !fileExplorer.fileItems) return []; + if (!fileExplorer?.fileItems) return []; - const selectedFiles: TFile[] = []; - - Object.entries(fileExplorer.fileItems).forEach(([_, item]) => { - if (item.el?.classList.contains('is-selected') && - item.file && item.file instanceof TFile && - item.file.extension === 'md') { - selectedFiles.push(item.file); - } - }); - - return selectedFiles; + return Object.values(fileExplorer.fileItems) + .filter(item => + item.el?.classList.contains('is-selected') && + item.file instanceof TFile && + item.file.extension === 'md') + .map(item => item.file as TFile); } /** - * Clean up when plugin is unloaded + * Clean up on unload */ public unload(): void { this.removeAllFileExplorerIcons(); From affcb45e5ae9abd5069b20d4225bc1508ca0bd02 Mon Sep 17 00:00:00 2001 From: Aleix Date: Tue, 13 May 2025 09:04:55 +0200 Subject: [PATCH 10/67] refactor(ui): streamline StatusBar with improved organization and consistent class handling --- ui/components/status-bar.ts | 316 ++++++++++++++++++------------------ 1 file changed, 155 insertions(+), 161 deletions(-) diff --git a/ui/components/status-bar.ts b/ui/components/status-bar.ts index 610023e..87573f3 100644 --- a/ui/components/status-bar.ts +++ b/ui/components/status-bar.ts @@ -6,173 +6,167 @@ import { StatusService } from '../../services/status-service'; * Handles the status bar functionality */ export class StatusBar { - private statusBarEl: HTMLElement; - private settings: NoteStatusSettings; - private statusService: StatusService; - private currentStatuses: string[] = ['unknown']; + private statusBarEl: HTMLElement; + private settings: NoteStatusSettings; + private statusService: StatusService; + private currentStatuses: string[] = ['unknown']; - constructor(statusBarEl: HTMLElement, settings: NoteStatusSettings, statusService: StatusService) { - this.statusBarEl = statusBarEl; - this.settings = settings; - this.statusService = statusService; + constructor(statusBarEl: HTMLElement, settings: NoteStatusSettings, statusService: StatusService) { + this.statusBarEl = statusBarEl; + this.settings = settings; + this.statusService = statusService; - // Add initial class - this.statusBarEl.addClass('note-status-bar'); + this.statusBarEl.addClass('note-status-bar'); + this.setupContextMenu(); + this.update(['unknown']); + } - // Add right-click handler for force refresh - this.statusBarEl.addEventListener('contextmenu', (e) => { - e.preventDefault(); - window.dispatchEvent(new CustomEvent('note-status:force-refresh')); - }); + /** + * Set up right-click context menu for force refresh + */ + private setupContextMenu(): void { + this.statusBarEl.addEventListener('contextmenu', (e) => { + e.preventDefault(); + window.dispatchEvent(new CustomEvent('note-status:force-refresh')); + }); + } - // Initial render - this.update(['unknown']); - } + /** + * Update the status bar with new statuses + */ + public update(statuses: string[]): void { + this.currentStatuses = statuses; + this.render(); + } - /** - * Update the status bar with new statuses - */ - public update(statuses: string[]): void { - this.currentStatuses = statuses; - this.render(); - } + /** + * Render the status bar based on current settings and statuses + */ + public render(): void { + this.statusBarEl.empty(); + this.resetClasses(); + + if (!this.settings.showStatusBar) { + this.statusBarEl.addClass('hidden'); + return; + } + + // Select display mode based on number of statuses and settings + if (this.currentStatuses.length === 1 || !this.settings.useMultipleStatuses) { + this.renderSingleStatus(); + } else { + this.renderMultipleStatuses(); + } + + this.handleAutoHide(); + } - /** - * Render the status bar based on current settings and statuses - */ - public render(): void { - // Clear existing content - this.statusBarEl.empty(); - - // Reset classes - this.statusBarEl.removeClass('left', 'hidden', 'auto-hide', 'visible'); - this.statusBarEl.addClass('note-status-bar'); - - // Hide if disabled - if (!this.settings.showStatusBar) { - this.statusBarEl.addClass('hidden'); - return; - } - - // Handle display based on single vs. multiple status - if (this.currentStatuses.length === 1 || !this.settings.useMultipleStatuses) { - this.renderSingleStatus(); - } else { - this.renderMultipleStatuses(); - } - - // Handle auto-hide behavior for 'unknown' status - this.handleAutoHide(); - } - - /** - * Render a single status display - */ - private renderSingleStatus(): void { - const primaryStatus = this.currentStatuses[0]; - const statusObj = this.statusService.getAllStatuses().find(s => s.name === primaryStatus); - const tooltipValue = statusObj?.description - ? `${primaryStatus} - ${statusObj.description}` - : primaryStatus; - - // Create status text - const statusText = this.statusBarEl.createEl('span', { - text: `Status: ${primaryStatus}`, - cls: `note-status-${primaryStatus}` - }); - - // Add tooltip - setTooltip(statusText, tooltipValue); - - // Create status icon - const statusIcon = this.statusBarEl.createEl('span', { - text: this.statusService.getStatusIcon(primaryStatus), - cls: `note-status-icon status-${primaryStatus}` - }); - - // Add tooltip to icon too - setTooltip(statusIcon, tooltipValue); - } - - /** - * Render multiple statuses display - */ - private renderMultipleStatuses(): void { - // Create status label - this.statusBarEl.createEl('span', { - text: `Statuses: `, - cls: 'note-status-label' - }); - - // Create container for status badges - const badgesContainer = this.statusBarEl.createEl('span', { - cls: 'note-status-badges' - }); - - // Add status badges - this.currentStatuses.forEach(status => { - this.createStatusBadge(badgesContainer, status); - }); - } - - /** - * Create a status badge for multiple status display - */ - private createStatusBadge(container: HTMLElement, status: string): void { - const statusObj = this.statusService.getAllStatuses().find(s => s.name === status); - const tooltipValue = statusObj?.description - ? `${status} - ${statusObj.description}` - : status; - - const badge = container.createEl('span', { - cls: `note-status-badge status-${status}` - }); - - // Add tooltip - setTooltip(badge, tooltipValue); - - // Add icon - badge.createEl('span', { - text: this.statusService.getStatusIcon(status), - cls: 'note-status-badge-icon' - }); - - // Add text - badge.createEl('span', { - text: status, - cls: 'note-status-badge-text' - }); - } - - /** - * Handle auto-hide behavior - */ - private handleAutoHide(): void { - const onlyUnknown = this.currentStatuses.length === 1 && this.currentStatuses[0] === 'unknown'; - if (this.settings.autoHideStatusBar && onlyUnknown) { - this.statusBarEl.addClass('auto-hide'); - setTimeout(() => { - if (onlyUnknown && this.settings.showStatusBar) { - this.statusBarEl.addClass('hidden'); - } - }, 500); - } else { - this.statusBarEl.addClass('visible'); - } - } + /** + * Reset CSS classes to their default state + */ + private resetClasses(): void { + this.statusBarEl.removeClass('left', 'hidden', 'auto-hide', 'visible'); + this.statusBarEl.addClass('note-status-bar'); + } + + /** + * Render a single status display + */ + private renderSingleStatus(): void { + const primaryStatus = this.currentStatuses[0]; + const statusObj = this.statusService.getAllStatuses().find(s => s.name === primaryStatus); + const tooltipText = statusObj?.description + ? `${primaryStatus} - ${statusObj.description}` + : primaryStatus; + + // Create status text + const statusText = this.statusBarEl.createEl('span', { + text: `Status: ${primaryStatus}`, + cls: `note-status-${primaryStatus}` + }); + setTooltip(statusText, tooltipText); + + // Create status icon + const statusIcon = this.statusBarEl.createEl('span', { + text: this.statusService.getStatusIcon(primaryStatus), + cls: `note-status-icon status-${primaryStatus}` + }); + setTooltip(statusIcon, tooltipText); + } + + /** + * Render multiple statuses display + */ + private renderMultipleStatuses(): void { + this.statusBarEl.createEl('span', { + text: 'Statuses: ', + cls: 'note-status-label' + }); + + const badgesContainer = this.statusBarEl.createEl('span', { + cls: 'note-status-badges' + }); + + this.currentStatuses.forEach(status => + this.createStatusBadge(badgesContainer, status)); + } + + /** + * Create a status badge for multiple status display + */ + private createStatusBadge(container: HTMLElement, status: string): void { + const statusObj = this.statusService.getAllStatuses().find(s => s.name === status); + const tooltipText = statusObj?.description + ? `${status} - ${statusObj.description}` + : status; + + const badge = container.createEl('span', { + cls: `note-status-badge status-${status}` + }); + setTooltip(badge, tooltipText); + + badge.createEl('span', { + text: this.statusService.getStatusIcon(status), + cls: 'note-status-badge-icon' + }); + + badge.createEl('span', { + text: status, + cls: 'note-status-badge-text' + }); + } + + /** + * Handle auto-hide behavior + */ + private handleAutoHide(): void { + const onlyUnknown = this.currentStatuses.length === 1 && + this.currentStatuses[0] === 'unknown'; + + if (this.settings.autoHideStatusBar && onlyUnknown) { + this.statusBarEl.addClass('auto-hide'); + setTimeout(() => { + if (onlyUnknown && this.settings.showStatusBar) { + this.statusBarEl.addClass('hidden'); + } + }, 500); + } else { + this.statusBarEl.addClass('visible'); + } + } - /** - * Update settings reference - */ - public updateSettings(settings: NoteStatusSettings): void { - this.settings = settings; - this.render(); - } + /** + * Update settings reference + */ + public updateSettings(settings: NoteStatusSettings): void { + this.settings = settings; + this.render(); + } - /** - * Clean up when plugin is unloaded - */ - public unload(): void { - this.statusBarEl.empty(); - } + /** + * Clean up when plugin is unloaded + */ + public unload(): void { + this.statusBarEl.empty(); + } } \ No newline at end of file From dcf2835af87819bd86bbf3ad3f1a4f7f57aac11d Mon Sep 17 00:00:00 2001 From: Aleix Date: Tue, 13 May 2025 09:11:48 +0200 Subject: [PATCH 11/67] refactor(ui): reorganize StatusDropdown with improved method organization and cleaner event handling --- ui/components/status-dropdown.ts | 379 +++++++++++++------------------ 1 file changed, 157 insertions(+), 222 deletions(-) diff --git a/ui/components/status-dropdown.ts b/ui/components/status-dropdown.ts index 286787d..ee77327 100644 --- a/ui/components/status-dropdown.ts +++ b/ui/components/status-dropdown.ts @@ -18,11 +18,7 @@ export class StatusDropdown { this.app = app; this.settings = settings; this.statusService = statusService; - - // Initialize the dropdown component this.dropdownComponent = new StatusDropdownComponent(app, statusService, settings); - - // Configure dropdown component callbacks this.setupDropdownCallbacks(); } @@ -31,81 +27,92 @@ export class StatusDropdown { */ private setupDropdownCallbacks(): void { this.dropdownComponent.setOnStatusChange((statuses) => { - // Update current statuses and toolbar button - this.currentStatuses = [...statuses]; // Make sure to copy the array + this.currentStatuses = [...statuses]; this.updateToolbarButton(); - - // Dispatch events for UI update - window.dispatchEvent(new CustomEvent('note-status:status-changed', { - detail: { statuses } - })); - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); + this.notifyStatusChanged(statuses); }); } /** - * Updates the toolbar button appearance based on current statuses + * Notify that status has changed + */ + private notifyStatusChanged(statuses: string[]): void { + window.dispatchEvent(new CustomEvent('note-status:status-changed', { + detail: { statuses } + })); + window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); + } + + /** + * Updates the toolbar button appearance */ private updateToolbarButton(): void { if (!this.toolbarButton) return; - // Clear existing content this.toolbarButton.empty(); - - // Check if we have a valid status + const hasValidStatus = this.currentStatuses.length > 0 && !this.currentStatuses.every(status => status === 'unknown'); - // Create badge container const badgeContainer = document.createElement('div'); badgeContainer.addClass('note-status-toolbar-badge-container'); if (hasValidStatus) { - // Add primary status icon - const primaryStatus = this.currentStatuses[0]; - const statusInfo = this.statusService.getAllStatuses().find(s => s.name === primaryStatus); - - if (statusInfo) { - // Primary status icon - const iconSpan = document.createElement('span'); - iconSpan.addClass(`note-status-toolbar-icon`, `status-${primaryStatus}`); - iconSpan.textContent = statusInfo.icon; - badgeContainer.appendChild(iconSpan); - - // Add count indicator if multiple statuses - if (this.settings.useMultipleStatuses && this.currentStatuses.length > 1) { - const countBadge = document.createElement('span'); - countBadge.addClass('note-status-count-badge'); - countBadge.textContent = `+${this.currentStatuses.length - 1}`; - badgeContainer.appendChild(countBadge); - } - } + this.addPrimaryStatusIcon(badgeContainer); } else { - // Add default unknown status icon - const iconSpan = document.createElement('span'); - iconSpan.addClass('note-status-toolbar-icon', 'status-unknown'); - iconSpan.textContent = this.statusService.getStatusIcon('unknown'); - badgeContainer.appendChild(iconSpan); + this.addUnknownStatusIcon(badgeContainer); } this.toolbarButton.appendChild(badgeContainer); } + + /** + * Add primary status icon to container + */ + private addPrimaryStatusIcon(container: HTMLElement): void { + const primaryStatus = this.currentStatuses[0]; + const statusInfo = this.statusService.getAllStatuses().find(s => s.name === primaryStatus); + + if (statusInfo) { + const iconSpan = document.createElement('span'); + iconSpan.addClass(`note-status-toolbar-icon`, `status-${primaryStatus}`); + iconSpan.textContent = statusInfo.icon; + container.appendChild(iconSpan); + + if (this.settings.useMultipleStatuses && this.currentStatuses.length > 1) { + this.addCountBadge(container); + } + } + } + + /** + * Add unknown status icon + */ + private addUnknownStatusIcon(container: HTMLElement): void { + const iconSpan = document.createElement('span'); + iconSpan.addClass('note-status-toolbar-icon', 'status-unknown'); + iconSpan.textContent = this.statusService.getStatusIcon('unknown'); + container.appendChild(iconSpan); + } + + /** + * Add count badge for multiple statuses + */ + private addCountBadge(container: HTMLElement): void { + const countBadge = document.createElement('span'); + countBadge.addClass('note-status-count-badge'); + countBadge.textContent = `+${this.currentStatuses.length - 1}`; + container.appendChild(countBadge); + } /** * Updates the dropdown UI based on current statuses */ public update(currentStatuses: string[] | string): void { - // Normalize input to always be an array - if (typeof currentStatuses === 'string') { - this.currentStatuses = [currentStatuses]; - } else { - this.currentStatuses = [...currentStatuses]; // Create a copy - } - - // Update toolbar button with new status + this.currentStatuses = Array.isArray(currentStatuses) ? + [...currentStatuses] : [currentStatuses]; + this.updateToolbarButton(); - - // Update dropdown component this.dropdownComponent.updateStatuses(this.currentStatuses); } @@ -118,19 +125,6 @@ export class StatusDropdown { this.dropdownComponent.updateSettings(settings); } - /** - * Toggle the status popover - */ - private toggleStatusPopover(): void { - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile) return; - - this.openStatusDropdown({ - target: this.toolbarButton, - files: [activeFile] - }); - } - /** * Show status dropdown in context menu */ @@ -141,68 +135,56 @@ export class StatusDropdown { const position = this.getCursorPosition(editor, view); this.openStatusDropdown({ - position: position, + position, files: [activeFile], onStatusChange: async (statuses) => { if (statuses.length > 0) { - if (this.settings.useMultipleStatuses) { - await this.statusService.toggleNoteStatus(statuses[0], activeFile); - } else { - await this.statusService.updateNoteStatuses(statuses, activeFile); - } - - // Trigger UI updates - window.dispatchEvent(new CustomEvent('note-status:status-changed', { - detail: { statuses, file: activeFile.path } - })); - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); + await this.updateFileStatus(activeFile, statuses); } } }); } + + /** + * Update a file's status + */ + private async updateFileStatus(file: TFile, statuses: string[]): Promise { + if (this.settings.useMultipleStatuses) { + await this.statusService.toggleNoteStatus(statuses[0], file); + } else { + await this.statusService.updateNoteStatuses(statuses, file); + } + + this.notifyStatusChanged(statuses); + } /** - * Get position from cursor or fallback positions + * Get position from cursor or fallback */ private getCursorPosition(editor: Editor, view: MarkdownView): { x: number, y: number } { try { - // Get cursor position in the document const cursor = editor.getCursor('head'); - - // Try to find the DOM representation of the cursor position const lineElement = view.contentEl.querySelector(`.cm-line:nth-child(${cursor.line + 1})`); if (lineElement) { const rect = lineElement.getBoundingClientRect(); - // Position near the current line with some offset - return { - x: rect.left + 20, - y: rect.bottom + 5 - }; + return { x: rect.left + 20, y: rect.bottom + 5 }; } - // Fallback to editor element position const editorEl = view.contentEl.querySelector('.cm-editor'); if (editorEl) { const rect = editorEl.getBoundingClientRect(); - return { - x: rect.left + 100, // Offset from left - y: rect.top + 100 // Offset from top - }; + return { x: rect.left + 100, y: rect.top + 100 }; } } catch (error) { console.error('Error getting position for dropdown:', error); } - // Last resort - use middle of viewport - return { - x: window.innerWidth / 2, - y: window.innerHeight / 3 - }; + return { x: window.innerWidth / 2, y: window.innerHeight / 3 }; } /** - * Stub render method (kept for compatibility) + * Render method (kept for compatibility) */ public render(): void { // No-op - dropdown component handles rendering internally @@ -212,10 +194,8 @@ export class StatusDropdown { * Remove dropdown when plugin is unloaded */ public unload(): void { - // Clean up dropdown component this.dropdownComponent.dispose(); - - // Remove toolbar button + if (this.toolbarButton) { this.toolbarButton.remove(); this.toolbarButton = undefined; @@ -223,7 +203,7 @@ export class StatusDropdown { } /** - * Universal function to open the status dropdown in any context + * Universal function to open the status dropdown */ public openStatusDropdown(options: { target?: HTMLElement; @@ -234,21 +214,16 @@ export class StatusDropdown { mode?: 'replace' | 'add'; onStatusChange?: (statuses: string[]) => void; }): void { - // Force reset if dropdown is already open if (this.dropdownComponent.isOpen) { this.dropdownComponent.close(); - // Give it a moment to clean up before proceeding - setTimeout(() => { - this._openStatusDropdown(options); - }, 50); - return; + setTimeout(() => this._openStatusDropdown(options), 50); + } else { + this._openStatusDropdown(options); } - - this._openStatusDropdown(options); } /** - * Internal method to open dropdown after reset + * Internal method to open dropdown */ private _openStatusDropdown(options: { target?: HTMLElement; @@ -259,46 +234,34 @@ export class StatusDropdown { mode?: 'replace' | 'add'; onStatusChange?: (statuses: string[]) => void; }): void { - // If no files provided, use active file const files = options.files || [this.app.workspace.getActiveFile()].filter(Boolean); if (!files.length) { new Notice('No files selected'); return; } - // Always reset state at the beginning - this.dropdownComponent.setTargetFile(null); - this.dropdownComponent.setOnStatusChange(() => {}); - - // Determine if we're handling single or multiple files + this.resetDropdownState(); + const isSingleFile = files.length === 1; const targetFile = isSingleFile ? files[0] : null; - - // Set target file (if single) or null (if multiple) this.dropdownComponent.setTargetFile(targetFile); - // Get current statuses appropriately - let currentStatuses: string[]; + const currentStatuses = targetFile ? + this.statusService.getFileStatuses(targetFile) : + this.findCommonStatuses(files); - if (targetFile) { - // For single file, get its current statuses - currentStatuses = this.statusService.getFileStatuses(targetFile); - } else if (files.length > 1) { - // For multiple files, find common statuses to display - currentStatuses = this.findCommonStatuses(files); - } else { - currentStatuses = ['unknown']; - } - - // Update dropdown with current statuses this.dropdownComponent.updateStatuses(currentStatuses); - - // Set the appropriate callback this.setupStatusChangeCallback(options, files, isSingleFile); - - // Position and open the dropdown this.positionAndOpenDropdown(options); } + + /** + * Reset dropdown state before opening + */ + private resetDropdownState(): void { + this.dropdownComponent.setTargetFile(null); + this.dropdownComponent.setOnStatusChange(() => {}); + } /** * Setup the appropriate callback for status changes @@ -312,22 +275,15 @@ export class StatusDropdown { isSingleFile: boolean ): void { if (options.onStatusChange) { - // Use the provided callback directly this.dropdownComponent.setOnStatusChange(options.onStatusChange); } else if (!isSingleFile) { - // Create a local copy of files to avoid closure issues const filesForBatch = [...files]; - // Set batch operation callback for multiple files this.dropdownComponent.setOnStatusChange(async (statuses) => { if (statuses.length > 0) { - // Get the last selected status for toggling const toggledStatus = statuses[statuses.length - 1]; - - // Toggle this status across all files await this.toggleStatusForFiles(filesForBatch, toggledStatus); - - // Dispatch events for UI update + window.dispatchEvent(new CustomEvent('note-status:batch-update-complete', { detail: { status: toggledStatus, @@ -338,19 +294,12 @@ export class StatusDropdown { } }); } else { - // Default callback for single file operations - this.dropdownComponent.setOnStatusChange((statuses) => { - // Just emit events for UI updates - window.dispatchEvent(new CustomEvent('note-status:status-changed', { - detail: { statuses } - })); - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); - }); + this.dropdownComponent.setOnStatusChange(this.notifyStatusChanged.bind(this)); } } /** - * Position and open the dropdown based on options + * Position and open the dropdown */ private positionAndOpenDropdown(options: { target?: HTMLElement; @@ -358,63 +307,46 @@ export class StatusDropdown { editor?: Editor; view?: MarkdownView; }): void { - // For dropdown from editor if (options.editor && options.view) { const position = this.getCursorPosition(options.editor, options.view); - const dummyTarget = this.createDummyTarget(position); - this.dropdownComponent.open(dummyTarget, position); - - // Clean up dummy target - setTimeout(() => { - if (dummyTarget.parentNode) { - dummyTarget.parentNode.removeChild(dummyTarget); - } - }, 100); + this.openWithPosition(position); return; } - // For dropdown from toolbar button if (options.target) { if (options.position) { this.dropdownComponent.open(options.target, options.position); } else { const rect = options.target.getBoundingClientRect(); - const position = { + this.dropdownComponent.open(options.target, { x: rect.left, y: rect.bottom + 5 - }; - this.dropdownComponent.open(options.target, position); + }); } return; } - // For direct position (context menus) if (options.position) { - const dummyTarget = this.createDummyTarget(options.position); - this.dropdownComponent.open(dummyTarget, options.position); - - // Clean up dummy target - setTimeout(() => { - if (dummyTarget.parentNode) { - dummyTarget.parentNode.removeChild(dummyTarget); - } - }, 100); + this.openWithPosition(options.position); return; } - // Fallback to center position - const center = { + this.openWithPosition({ x: window.innerWidth / 2, y: window.innerHeight / 3 - }; + }); + } + + /** + * Open dropdown at a specific position using dummy target + */ + private openWithPosition(position: { x: number, y: number }): void { + const dummyTarget = this.createDummyTarget(position); + this.dropdownComponent.open(dummyTarget, position); - const fallbackTarget = this.createDummyTarget(center); - this.dropdownComponent.open(fallbackTarget, center); - - // Clean up fallback target setTimeout(() => { - if (fallbackTarget.parentNode) { - fallbackTarget.parentNode.removeChild(fallbackTarget); + if (dummyTarget.parentNode) { + dummyTarget.parentNode.removeChild(dummyTarget); } }, 100); } @@ -437,30 +369,22 @@ export class StatusDropdown { private findCommonStatuses(files: TFile[]): string[] { if (files.length === 0) return ['unknown']; - // Get statuses for first file const firstFileStatuses = this.statusService.getFileStatuses(files[0]); - // Filter to only include statuses that exist on all files, except unknown - return firstFileStatuses.filter(status => { - if (status === 'unknown') return false; - - // Check if status exists on all files - return files.every(file => - this.statusService.getFileStatuses(file).includes(status) - ); - }); + return firstFileStatuses.filter(status => + status !== 'unknown' && + files.every(file => this.statusService.getFileStatuses(file).includes(status)) + ); } /** * Toggle a status across multiple files */ private async toggleStatusForFiles(files: TFile[], status: string): Promise { - // Count how many files have this status const filesWithStatus = files.filter(file => this.statusService.getFileStatuses(file).includes(status) ); - // If more than half have the status, remove it; otherwise, add it const shouldRemove = filesWithStatus.length > files.length / 2; for (const file of files) { @@ -477,45 +401,56 @@ export class StatusDropdown { */ public addToolbarButtonToActiveLeaf(): void { const activeLeaf = this.app.workspace.activeLeaf; - if (!activeLeaf || !activeLeaf.view || !(activeLeaf.view instanceof MarkdownView)) { - return; - } + if (!activeLeaf?.view || !(activeLeaf.view instanceof MarkdownView)) return; - // Get the toolbar container - const containerEl = activeLeaf.view.containerEl; - const toolbarContainer = containerEl.querySelector('.view-header .view-actions'); - if (!toolbarContainer) { - return; - } + const toolbarContainer = activeLeaf.view.containerEl.querySelector('.view-header .view-actions'); + if (!toolbarContainer) return; - // Check if button already exists in this toolbar const existingButton = toolbarContainer.querySelector('.note-status-toolbar-button'); if (existingButton) { this.toolbarButton = existingButton as HTMLElement; - this.updateToolbarButton(); // Update existing button + this.updateToolbarButton(); return; } - // Create new button - this.toolbarButton = document.createElement('button'); - this.toolbarButton.addClass('note-status-toolbar-button', 'clickable-icon', 'view-action'); - this.toolbarButton.setAttribute('aria-label', 'Note status'); + this.toolbarButton = this.createToolbarButton(); - // Update the button state - this.updateToolbarButton(); - - // Add click handler - this.toolbarButton.addEventListener('click', (e) => { - e.stopPropagation(); - e.preventDefault(); - this.toggleStatusPopover(); - }); - - // Add to toolbar at the beginning if (toolbarContainer.firstChild) { toolbarContainer.insertBefore(this.toolbarButton, toolbarContainer.firstChild); } else { toolbarContainer.appendChild(this.toolbarButton); } } + + /** + * Create the toolbar button + */ + private createToolbarButton(): HTMLElement { + const button = document.createElement('button'); + button.addClass('note-status-toolbar-button', 'clickable-icon', 'view-action'); + button.setAttribute('aria-label', 'Note status'); + + this.updateToolbarButton(); + + button.addEventListener('click', (e) => { + e.stopPropagation(); + e.preventDefault(); + this.toggleStatusPopover(); + }); + + return button; + } + + /** + * Toggle the status popover + */ + private toggleStatusPopover(): void { + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile) return; + + this.openStatusDropdown({ + target: this.toolbarButton, + files: [activeFile] + }); + } } \ No newline at end of file From 4595c8160086834c20933052e2a5590a94b4eaf8 Mon Sep 17 00:00:00 2001 From: Aleix Date: Tue, 13 May 2025 09:15:22 +0200 Subject: [PATCH 12/67] refactor(services): simplify StyleService with better separation of concerns in CSS generation --- services/style-service.ts | 165 ++++++++++++++++++++------------------ 1 file changed, 85 insertions(+), 80 deletions(-) diff --git a/services/style-service.ts b/services/style-service.ts index 61991d8..b25da5e 100644 --- a/services/style-service.ts +++ b/services/style-service.ts @@ -5,93 +5,98 @@ import { PREDEFINED_TEMPLATES } from '../constants/status-templates'; * Handles dynamic styling for the plugin */ export class StyleService { - private dynamicStyleEl?: HTMLStyleElement; - private settings: NoteStatusSettings; + private dynamicStyleEl?: HTMLStyleElement; + private settings: NoteStatusSettings; - constructor(settings: NoteStatusSettings) { - this.settings = settings; - this.initializeDynamicStyles(); - } + constructor(settings: NoteStatusSettings) { + this.settings = settings; + this.initializeDynamicStyles(); + } - /** - * Updates the settings reference - */ - public updateSettings(settings: NoteStatusSettings): void { - this.settings = settings; - this.updateDynamicStyles(); - } + /** + * Updates the settings reference + */ + public updateSettings(settings: NoteStatusSettings): void { + this.settings = settings; + this.updateDynamicStyles(); + } - /** - * Creates the style element if it doesn't exist - */ - private initializeDynamicStyles(): void { - if (!this.dynamicStyleEl) { - this.dynamicStyleEl = document.createElement('style'); - document.head.appendChild(this.dynamicStyleEl); - } - this.updateDynamicStyles(); - } + /** + * Creates the style element if it doesn't exist + */ + private initializeDynamicStyles(): void { + if (!this.dynamicStyleEl) { + this.dynamicStyleEl = document.createElement('style'); + document.head.appendChild(this.dynamicStyleEl); + } + this.updateDynamicStyles(); + } - /** - * Get all status colors including those from enabled templates - */ - private getAllStatusColors(): Record { - const colors = { ...this.settings.statusColors }; + /** + * Get all status colors including those from enabled templates + */ + private getAllStatusColors(): Record { + const colors = { ...this.settings.statusColors }; - // Add colors from template statuses if not using custom statuses only - if (!this.settings.useCustomStatusesOnly) { - for (const templateId of this.settings.enabledTemplates) { - const template = PREDEFINED_TEMPLATES.find(t => t.id === templateId); - if (template) { - // Add template colors if they don't already exist in colors - for (const status of template.statuses) { - if (status.color && !colors[status.name]) { - colors[status.name] = status.color; - } - } - } - } - } + if (this.settings.useCustomStatusesOnly) return colors; + + // Add colors from template statuses + for (const templateId of this.settings.enabledTemplates) { + const template = PREDEFINED_TEMPLATES.find(t => t.id === templateId); + if (!template) continue; + + for (const status of template.statuses) { + if (status.color && !colors[status.name]) { + colors[status.name] = status.color; + } + } + } - return colors; - } + return colors; + } - /** - * Updates the dynamic styles based on current settings - */ - public updateDynamicStyles(): void { - if (!this.dynamicStyleEl) { - this.initializeDynamicStyles(); - return; - } + /** + * Updates the dynamic styles based on current settings + */ + public updateDynamicStyles(): void { + if (!this.dynamicStyleEl) { + this.initializeDynamicStyles(); + return; + } - // Get all status colors - const allColors = this.getAllStatusColors(); + const allColors = this.getAllStatusColors(); + const cssRules = this.generateColorCssRules(allColors); + this.dynamicStyleEl.textContent = cssRules; + } + + /** + * Generate CSS rules for status colors + */ + private generateColorCssRules(colors: Record): string { + let css = ''; + + for (const [status, color] of Object.entries(colors)) { + css += ` + .status-${status} { + color: ${color} !important; + } + .note-status-bar .note-status-${status}, + .nav-file-title .note-status-${status} { + color: ${color} !important; + } + `; + } + + return css; + } - // Generate CSS for status colors - let css = ''; - for (const [status, color] of Object.entries(allColors)) { - css += ` - .status-${status} { - color: ${color} !important; - } - .note-status-bar .note-status-${status}, - .nav-file-title .note-status-${status} { - color: ${color} !important; - } - `; - } - - this.dynamicStyleEl.textContent = css; - } - - /** - * Cleans up styles when plugin is unloaded - */ - public unload(): void { - if (this.dynamicStyleEl) { - this.dynamicStyleEl.remove(); - this.dynamicStyleEl = undefined; - } - } + /** + * Cleans up styles when plugin is unloaded + */ + public unload(): void { + if (this.dynamicStyleEl) { + this.dynamicStyleEl.remove(); + this.dynamicStyleEl = undefined; + } + } } \ No newline at end of file From 67cd32002cc6be8855c5bcc4a17b6004204e344d Mon Sep 17 00:00:00 2001 From: Aleix Date: Tue, 13 May 2025 09:19:30 +0200 Subject: [PATCH 13/67] refactor(ui): simplify StatusContextMenu with better method organization and cleaner event handling --- ui/menus/status-context-menu.ts | 90 +++++++++++++++++---------------- 1 file changed, 46 insertions(+), 44 deletions(-) diff --git a/ui/menus/status-context-menu.ts b/ui/menus/status-context-menu.ts index ea3b7c2..e169608 100644 --- a/ui/menus/status-context-menu.ts +++ b/ui/menus/status-context-menu.ts @@ -1,24 +1,30 @@ import { App, Menu, TFile } from 'obsidian'; import { NoteStatusSettings } from '../../models/types'; import { StatusService } from '../../services/status-service'; -import { StatusDropdown } from 'ui/components/status-dropdown'; -import { ExplorerIntegration } from 'ui/integrations/explorer-integration'; +import { StatusDropdown } from '../components/status-dropdown'; +import { ExplorerIntegration } from '../integrations/explorer-integration'; /** * Handles context menu interactions for status changes */ export class StatusContextMenu { - private statusDropdown: StatusDropdown; + private statusDropdown: StatusDropdown; private settings: NoteStatusSettings; private statusService: StatusService; private explorerIntegration: ExplorerIntegration; public app: App; - constructor(app: App, settings: NoteStatusSettings, statusService: StatusService, statusDropdown: StatusDropdown, explorerIntegration:ExplorerIntegration) { + constructor( + app: App, + settings: NoteStatusSettings, + statusService: StatusService, + statusDropdown: StatusDropdown, + explorerIntegration: ExplorerIntegration + ) { this.app = app; this.settings = settings; this.statusService = statusService; - this.statusDropdown = statusDropdown; + this.statusDropdown = statusDropdown; this.explorerIntegration = explorerIntegration; } @@ -30,19 +36,16 @@ export class StatusContextMenu { } /** - * Shows the context menu for changing a status of one or more files + * Shows the context menu for changing status of one or more files */ public showForFiles(files: TFile[], position?: { x: number; y: number }): void { if (files.length === 0) return; - // For a single file, show dropdown directly if (files.length === 1) { this.showSingleFileDropdown(files[0], position); - return; + } else { + this.showMultipleFilesMenu(files, position); } - - // For multiple files, show menu first - this.showMultipleFilesMenu(files, position); } /** @@ -50,7 +53,7 @@ export class StatusContextMenu { */ private showSingleFileDropdown(file: TFile, position?: { x: number; y: number }): void { this.statusDropdown.openStatusDropdown({ - position: position, + position, files: [file], onStatusChange: async (statuses) => { if (statuses.length > 0) { @@ -66,27 +69,31 @@ export class StatusContextMenu { private showMultipleFilesMenu(files: TFile[], position?: { x: number; y: number }): void { const menu = new Menu(); - // Add title section menu.addItem((item) => { item.setTitle(`Update ${files.length} files`) .setDisabled(true); return item; }); - // Simplified toggle-based approach for multiple files menu.addItem((item) => item .setTitle('Manage statuses...') .setIcon('tag') .onClick(() => { this.statusDropdown.openStatusDropdown({ - position: position, - files: files + position, + files }); }) ); - // Show the menu + this.showMenu(menu, position); + } + + /** + * Show the menu at the specified position + */ + private showMenu(menu: Menu, position?: { x: number; y: number }): void { if (position) { menu.showAtPosition(position); } else { @@ -98,15 +105,12 @@ export class StatusContextMenu { * Shows a context menu for a single file */ public showForFile(file: TFile, event: MouseEvent): void { - // Ensure file is a valid TFile instance before proceeding - if (!(file instanceof TFile) || file.extension !== 'md') { - return; - } + if (!(file instanceof TFile) || file.extension !== 'md') return; const position = { x: event.clientX, y: event.clientY }; this.statusDropdown.openStatusDropdown({ - position: position, + position, files: [file], onStatusChange: async (statuses) => { if (statuses.length > 0) { @@ -120,42 +124,40 @@ export class StatusContextMenu { * Handle status update for a specific file */ private async handleStatusUpdateForFile(file: TFile, statuses: string[]): Promise { - // Add instanceof check for safety if (!(file instanceof TFile) || file.extension !== 'md' || statuses.length === 0) return; - // Update the file with the selected status - if (this.settings.useMultipleStatuses) { - // If the status is already applied, toggle it off - const currentStatuses = this.statusService.getFileStatuses(file); - if (currentStatuses.includes(statuses[0])) { - await this.statusService.removeNoteStatus(statuses[0], file); - } else { - // Add the status - await this.statusService.addNoteStatus(statuses[0], file); - } - } else { - // Replace with the single status - use the direct method, not batch - await this.statusService.updateNoteStatuses([statuses[0]], file); - } + await this.updateFileStatus(file, statuses); - // Force explorer icon update for this specific file + // Force explorer icon update this.app.metadataCache.trigger('changed', file); - - // Directly update the explorer icon this.updateExplorerIcon(file); // Dispatch events to update other UI components this.triggerUIUpdates(statuses, file); } + /** + * Update file status based on settings + */ + private async updateFileStatus(file: TFile, statuses: string[]): Promise { + if (this.settings.useMultipleStatuses) { + const currentStatuses = this.statusService.getFileStatuses(file); + if (currentStatuses.includes(statuses[0])) { + await this.statusService.removeNoteStatus(statuses[0], file); + } else { + await this.statusService.addNoteStatus(statuses[0], file); + } + } else { + await this.statusService.updateNoteStatuses([statuses[0]], file); + } + } + /** * Update explorer icon for a file */ private updateExplorerIcon(file: TFile): void { setTimeout(() => { - if (this.explorerIntegration) { - this.explorerIntegration.updateFileExplorerIcons(file); - } + this.explorerIntegration?.updateFileExplorerIcons(file); }, 50); } @@ -169,7 +171,7 @@ export class StatusContextMenu { window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); - // Force a full UI refresh to ensure all elements are updated + // Force a full UI refresh with slight delay setTimeout(() => { window.dispatchEvent(new CustomEvent('note-status:force-refresh')); }, 100); From 1b0dbefd01b11fe84500e3e62fc042e3adac2201 Mon Sep 17 00:00:00 2001 From: Aleix Date: Tue, 13 May 2025 10:06:15 +0200 Subject: [PATCH 14/67] refactor(ui): improve StatusPaneView with better component organization and rendering optimization --- ui/components/status-pane-view.ts | 837 ++++++++++++++++-------------- 1 file changed, 434 insertions(+), 403 deletions(-) diff --git a/ui/components/status-pane-view.ts b/ui/components/status-pane-view.ts index aa0446c..0133caa 100644 --- a/ui/components/status-pane-view.ts +++ b/ui/components/status-pane-view.ts @@ -7,446 +7,477 @@ import NoteStatus from 'main'; * Status Pane View for managing note statuses */ export class StatusPaneView extends View { - plugin: NoteStatus; - searchInput: HTMLInputElement | null = null; - private settings: NoteStatusSettings; - private statusService: StatusService; - private paginationState: { - itemsPerPage: number; - currentPage: Record; - }; + plugin: NoteStatus; + searchInput: HTMLInputElement | null = null; + private settings: NoteStatusSettings; + private statusService: StatusService; + private paginationState = { + itemsPerPage: 100, + currentPage: {} as Record + }; - constructor(leaf: WorkspaceLeaf, plugin: NoteStatus) { - super(leaf); - this.plugin = plugin; - this.settings = plugin.settings; - this.statusService = plugin.statusService; - - // Initialize pagination - this.paginationState = { - itemsPerPage: 100, // Show 100 items per page by default - currentPage: {} // Track current page for each status - }; - } + constructor(leaf: WorkspaceLeaf, plugin: NoteStatus) { + super(leaf); + this.plugin = plugin; + this.settings = plugin.settings; + this.statusService = plugin.statusService; + } - getViewType(): string { - return 'status-pane'; - } + getViewType(): string { + return 'status-pane'; + } - getDisplayText(): string { - return 'Status pane'; - } + getDisplayText(): string { + return 'Status pane'; + } - getIcon(): string { - return 'status-pane'; - } + getIcon(): string { + return 'status-pane'; + } - async onOpen(): Promise { - await this.setupPane(); - } + async onOpen(): Promise { + await this.setupPane(); + } - async setupPane(): Promise { - const { containerEl } = this; - containerEl.empty(); - containerEl.addClass('note-status-pane', 'nav-files-container'); + async setupPane(): Promise { + const { containerEl } = this; + containerEl.empty(); + containerEl.addClass('note-status-pane', 'nav-files-container'); - // Add a header container for better layout - const headerContainer = containerEl.createDiv({ cls: 'note-status-header' }); + this.createHeaderContainer(); + containerEl.toggleClass('note-status-compact-view', this.settings.compactView); - // Search container with search input - this.createSearchInput(headerContainer); + const groupsContainer = containerEl.createDiv({ + cls: 'note-status-groups-container' + }); + + const loadingIndicator = this.createLoadingIndicator(groupsContainer); + + // Use non-blocking render + setTimeout(async () => { + await this.renderGroups(''); + loadingIndicator.remove(); + }, 10); + } - // Actions toolbar (view toggle, refresh) - this.createActionToolbar(headerContainer); + /** + * Create header container with search and actions + */ + private createHeaderContainer(): HTMLElement { + const headerContainer = this.containerEl.createDiv({ cls: 'note-status-header' }); + this.createSearchInput(headerContainer); + this.createActionToolbar(headerContainer); + return headerContainer; + } + + /** + * Create loading indicator + */ + private createLoadingIndicator(container: HTMLElement): HTMLElement { + const loadingIndicator = container.createDiv({ cls: 'note-status-loading' }); + loadingIndicator.innerHTML = 'Loading notes...'; + return loadingIndicator; + } - // Set initial compact view state - containerEl.toggleClass('note-status-compact-view', this.settings.compactView); + private createSearchInput(container: HTMLElement): void { + const searchContainer = container.createDiv({ cls: 'note-status-search search-input-container' }); + const searchWrapper = searchContainer.createDiv({ cls: 'search-input-wrapper' }); - // Add a container for the groups - const groupsContainer = containerEl.createDiv({ cls: 'note-status-groups-container' }); - - // Add loading indicator - const loadingIndicator = groupsContainer.createDiv({ cls: 'note-status-loading' }); - loadingIndicator.innerHTML = 'Loading notes...'; - - // Use non-blocking render - setTimeout(async () => { - await this.renderGroups(''); - loadingIndicator.remove(); - }, 10); - } + // Search icon + const searchIcon = searchWrapper.createEl('span', { cls: 'search-input-icon' }); + setIcon(searchIcon, 'search'); - private createSearchInput(container: HTMLElement): void { - const searchContainer = container.createDiv({ cls: 'note-status-search search-input-container' }); - const searchWrapper = searchContainer.createDiv({ cls: 'search-input-wrapper' }); + // Create the search input + this.searchInput = searchWrapper.createEl('input', { + type: 'text', + placeholder: 'Search notes...', + cls: 'note-status-search-input search-input' + }); - // Search icon - const searchIcon = searchWrapper.createEl('span', { cls: 'search-input-icon' }); - setIcon(searchIcon, 'search'); + this.searchInput.addEventListener('input', () => { + this.renderGroups(this.searchInput!.value.toLowerCase()); + this.toggleClearButton(); + }); - // Create the search input - this.searchInput = searchWrapper.createEl('input', { - type: 'text', - placeholder: 'Search notes...', - cls: 'note-status-search-input search-input' - }); + // Clear search button + const clearSearchBtn = searchWrapper.createEl('span', { cls: 'search-input-clear-button' }); + setIcon(clearSearchBtn, 'x'); - // Add search event listener - this.searchInput.addEventListener('input', () => { - this.renderGroups(this.searchInput!.value.toLowerCase()); - this.toggleClearButton(); - }); + clearSearchBtn.addEventListener('click', () => { + if (this.searchInput) { + this.searchInput.value = ''; + this.renderGroups(''); + this.toggleClearButton(); + } + }); + } - // Clear search button (hidden by default) - const clearSearchBtn = searchWrapper.createEl('span', { cls: 'search-input-clear-button' }); - setIcon(clearSearchBtn, 'x'); + private toggleClearButton(): void { + const clearBtn = this.containerEl.querySelector('.search-input-clear-button'); + if (clearBtn && this.searchInput) { + clearBtn.toggleClass('is-visible', !!this.searchInput.value); + } + } - clearSearchBtn.addEventListener('click', () => { - if (this.searchInput) { - this.searchInput.value = ''; - this.renderGroups(''); - this.toggleClearButton(); - } - }); - } + private createActionToolbar(container: HTMLElement): void { + const actionsContainer = container.createDiv({ cls: 'status-pane-actions-container' }); + + // Toggle compact view button + this.createViewToggleButton(actionsContainer); + + // Refresh button + this.createRefreshButton(actionsContainer); + } + + /** + * Create view toggle button + */ + private createViewToggleButton(container: HTMLElement): void { + const viewToggleButton = container.createEl('button', { + type: 'button', + title: this.settings.compactView ? 'Switch to Standard View' : 'Switch to Compact View', + cls: 'note-status-view-toggle clickable-icon' + }); + + setIcon(viewToggleButton, this.settings.compactView ? 'layout' : 'table'); + + viewToggleButton.addEventListener('click', async () => { + this.settings.compactView = !this.settings.compactView; + viewToggleButton.title = this.settings.compactView ? 'Switch to Standard View' : 'Switch to Compact View'; + viewToggleButton.empty(); + setIcon(viewToggleButton, this.settings.compactView ? 'layout' : 'table'); + + window.dispatchEvent(new CustomEvent('note-status:settings-changed')); + this.containerEl.toggleClass('note-status-compact-view', this.settings.compactView); + await this.renderGroups(this.searchInput?.value.toLowerCase() || ''); + }); + } + + /** + * Create refresh button + */ + private createRefreshButton(container: HTMLElement): void { + const refreshButton = container.createEl('button', { + type: 'button', + title: 'Refresh statuses', + cls: 'note-status-actions-refresh clickable-icon' + }); + + setIcon(refreshButton, 'refresh-cw'); + + refreshButton.addEventListener('click', async () => { + await this.renderGroups(this.searchInput?.value.toLowerCase() || ''); + new Notice('Status pane refreshed'); + }); + } - private toggleClearButton(): void { - const clearBtn = this.containerEl.querySelector('.search-input-clear-button'); - if (clearBtn && this.searchInput) { - clearBtn.toggleClass('is-visible', !!this.searchInput.value); - } - } + async renderGroups(searchQuery = ''): Promise { + const { containerEl } = this; + const groupsContainerEl = containerEl.querySelector('.note-status-groups-container') as HTMLElement; + if (!groupsContainerEl) return; - private createActionToolbar(container: HTMLElement): void { - const actionsContainer = container.createDiv({ cls: 'status-pane-actions-container' }); - - // Toggle compact view button - const viewToggleButton = actionsContainer.createEl('button', { - type: 'button', - title: this.settings.compactView ? 'Switch to Standard View' : 'Switch to Compact View', - cls: 'note-status-view-toggle clickable-icon' - }); - - setIcon(viewToggleButton, this.settings.compactView ? 'layout' : 'table'); - - viewToggleButton.addEventListener('click', async () => { - this.settings.compactView = !this.settings.compactView; - viewToggleButton.title = this.settings.compactView ? 'Switch to Standard View' : 'Switch to Compact View'; - viewToggleButton.empty(); - setIcon(viewToggleButton, this.settings.compactView ? 'layout' : 'table'); - - // Trigger settings update - window.dispatchEvent(new CustomEvent('note-status:settings-changed')); - - this.containerEl.toggleClass('note-status-compact-view', this.settings.compactView); - await this.renderGroups(this.searchInput?.value.toLowerCase() || ''); - }); - - // Refresh button - const refreshButton = actionsContainer.createEl('button', { - type: 'button', - title: 'Refresh statuses', - cls: 'note-status-actions-refresh clickable-icon' - }); - - setIcon(refreshButton, 'refresh-cw'); - - refreshButton.addEventListener('click', async () => { - await this.renderGroups(this.searchInput?.value.toLowerCase() || ''); - new Notice('Status pane refreshed'); - }); - } + if (searchQuery) { + this.showSearchLoadingIndicator(groupsContainerEl, searchQuery); + await this.waitForNextFrame(); + } else { + groupsContainerEl.empty(); + } - async renderGroups(searchQuery = ''): Promise { - const { containerEl } = this; - const groupsContainer = containerEl.querySelector('.note-status-groups-container'); - if (!groupsContainer) return; + const statusGroups = this.getFilteredStatusGroups(searchQuery); + this.removeLoadingIndicator(groupsContainerEl); + this.renderStatusGroups(groupsContainerEl, statusGroups); + + // Check if no groups were rendered + if (groupsContainerEl.childElementCount === 0) { + this.showEmptyState(groupsContainerEl, searchQuery); + } + } + + /** + * Show loading indicator for search + */ + private showSearchLoadingIndicator(container: HTMLElement, query: string): void { + container.empty(); + const loadingIndicator = container.createDiv({ cls: 'note-status-loading' }); + loadingIndicator.innerHTML = `Searching for "${query}"...`; + } + + /** + * Wait for next animation frame + */ + private async waitForNextFrame(): Promise { + await new Promise(resolve => setTimeout(resolve, 0)); + } + + /** + * Remove loading indicator if it exists + */ + private removeLoadingIndicator(container: HTMLElement): void { + const loadingIndicator = container.querySelector('.note-status-loading'); + if (loadingIndicator) loadingIndicator.remove(); + } + + /** + * Render all status groups + */ + private renderStatusGroups(container: HTMLElement, statusGroups: Record): void { + Object.entries(statusGroups).forEach(([status, files]) => { + if (files.length > 0) { + // Skip unknown status if setting enabled + if (status === 'unknown' && this.settings.excludeUnknownStatus) return; + this.renderStatusGroup(container, status, files); + } + }); + } + + /** + * Show empty state message + */ + private showEmptyState(container: HTMLElement, searchQuery: string): void { + const emptyMessage = container.createDiv({ cls: 'note-status-empty-indicator' }); + + if (searchQuery) { + emptyMessage.textContent = `No notes found matching "${searchQuery}"`; + } else if (this.settings.excludeUnknownStatus) { + emptyMessage.empty(); + + emptyMessage.createDiv({ + text: 'No notes with status found. Unassigned notes are currently hidden.', + cls: 'note-status-empty-message' + }); + + const btnContainer = emptyMessage.createDiv({ + cls: 'note-status-button-container' + }); + + const showUnknownBtn = btnContainer.createEl('button', { + text: 'Show unassigned notes', + cls: 'note-status-show-unassigned-button' + }); + + showUnknownBtn.addEventListener('click', async () => { + this.settings.excludeUnknownStatus = false; + await this.plugin.saveSettings(); + this.renderGroups(searchQuery); + }); + } + } + + /** + * Get status groups with filtering + */ + private getFilteredStatusGroups(searchQuery = ''): Record { + const rawGroups = this.statusService.groupFilesByStatus(searchQuery); + const filteredGroups: Record = {}; + + Object.entries(rawGroups).forEach(([status, files]) => { + if (files.length > 0 && !(status === 'unknown' && this.settings.excludeUnknownStatus)) { + filteredGroups[status] = files; + } + }); + + return filteredGroups; + } - // Show loading indicator for non-empty search queries - if (searchQuery) { - groupsContainer.empty(); - const loadingIndicator = groupsContainer.createDiv({ cls: 'note-status-loading' }); - loadingIndicator.innerHTML = `Searching for "${searchQuery}"...`; - - // Let the UI update before continuing - await new Promise(resolve => setTimeout(resolve, 0)); - } else { - groupsContainer.empty(); - } + private renderStatusGroup(container: HTMLElement, status: string, files: TFile[]): void { + const groupEl = container.createDiv({ cls: 'note-status-group nav-folder' }); + const titleEl = groupEl.createDiv({ cls: 'nav-folder-title' }); - // Group files by status (with optimizations) - const statusGroups = this.getFilteredStatusGroups(searchQuery); - - // Remove the loading indicator if it exists - const loadingIndicator = groupsContainer.querySelector('.note-status-loading'); - if (loadingIndicator) { - loadingIndicator.remove(); - } + const collapseContainer = titleEl.createDiv({ cls: 'note-status-collapse-indicator' }); + setIcon(collapseContainer, 'chevron-down'); - // Render each status group - Object.entries(statusGroups).forEach(([status, files]) => { - if (files.length > 0) { - // Skip unknown status if setting enabled - if (status === 'unknown' && this.settings.excludeUnknownStatus) { - return; - } - this.renderStatusGroup(groupsContainer as HTMLElement, status, files); - } - }); - - // If no groups were rendered, show a message - if (groupsContainer.childElementCount === 0) { - const emptyMessage = groupsContainer.createDiv({ cls: 'note-status-empty-indicator' }); - if (searchQuery) { - emptyMessage.textContent = `No notes found matching "${searchQuery}"`; - } else if (this.settings.excludeUnknownStatus) { - // Clear any existing content and use a structured layout - emptyMessage.empty(); - - // Add message text in its own container - emptyMessage.createDiv({ - text: 'No notes with status found. Unassigned notes are currently hidden.', - cls: 'note-status-empty-message' - }); - - // Create separate container for the button - const btnContainer = emptyMessage.createDiv({ - cls: 'note-status-button-container' - }); - - // Add a styled button in its own container - const showUnknownBtn = btnContainer.createEl('button', { - text: 'Show unassigned notes', - cls: 'note-status-show-unassigned-button' - }); - - showUnknownBtn.addEventListener('click', async () => { - this.settings.excludeUnknownStatus = false; - await this.plugin.saveSettings(); - this.renderGroups(searchQuery); - }); - } - } - } - - /** - * Optimized method to get status groups with filtering - */ - private getFilteredStatusGroups(searchQuery = ''): Record { - // Use the statusService but apply our own filtering for better performance - const rawGroups = this.statusService.groupFilesByStatus(searchQuery); - const filteredGroups: Record = {}; - - // Filter out empty groups and respect the excludeUnknownStatus setting - Object.entries(rawGroups).forEach(([status, files]) => { - if (files.length > 0 && !(status === 'unknown' && this.settings.excludeUnknownStatus)) { - filteredGroups[status] = files; - } - }); - - return filteredGroups; - } + const titleContentContainer = titleEl.createDiv({ cls: 'nav-folder-title-content' }); - private renderStatusGroup(container: HTMLElement, status: string, files: TFile[]): void { - const groupEl = container.createDiv({ cls: 'note-status-group nav-folder' }); - const titleEl = groupEl.createDiv({ cls: 'nav-folder-title' }); + const statusIcon = this.statusService.getStatusIcon(status); + titleContentContainer.createSpan({ + text: `${status} ${statusIcon} (${files.length})`, + cls: `status-${status}` + }); - // Create a container for the collapse button and title - const collapseContainer = titleEl.createDiv({ cls: 'note-status-collapse-indicator' }); - setIcon(collapseContainer, 'chevron-down'); + this.setupGroupCollapsible(groupEl, collapseContainer, status); + this.renderGroupContent(groupEl, files, status); + } + + /** + * Set up collapsible behavior for a group + */ + private setupGroupCollapsible( + groupEl: HTMLElement, + collapseContainer: HTMLElement, + status: string + ): void { + const isCollapsed = this.settings.collapsedStatuses[status] ?? false; - // Create a container for the title content - const titleContentContainer = titleEl.createDiv({ cls: 'nav-folder-title-content' }); + if (isCollapsed) { + groupEl.addClass('note-status-is-collapsed'); + collapseContainer.empty(); + setIcon(collapseContainer, 'chevron-right'); + } - const statusIcon = this.statusService.getStatusIcon(status); - titleContentContainer.createSpan({ - text: `${status} ${statusIcon} (${files.length})`, - cls: `status-${status}` - }); + const titleEl = groupEl.querySelector('.nav-folder-title') as HTMLElement; + if (titleEl) { + titleEl.addEventListener('click', (e) => { + e.preventDefault(); + const isCurrentlyCollapsed = groupEl.hasClass('note-status-is-collapsed'); - // Handle collapsing/expanding behavior - const isCollapsed = this.settings.collapsedStatuses[status] ?? false; + groupEl.toggleClass('note-status-is-collapsed', !isCurrentlyCollapsed); + collapseContainer.empty(); + setIcon(collapseContainer, isCurrentlyCollapsed ? 'chevron-down' : 'chevron-right'); - if (isCollapsed) { - groupEl.addClass('note-status-is-collapsed'); - collapseContainer.empty(); - setIcon(collapseContainer, 'chevron-right'); - } + this.settings.collapsedStatuses[status] = !isCurrentlyCollapsed; + window.dispatchEvent(new CustomEvent('note-status:settings-changed')); + }); + } + } + + /** + * Render the content of a status group + */ + private renderGroupContent(groupEl: HTMLElement, files: TFile[], status: string): void { + const childrenEl = groupEl.createDiv({ cls: 'nav-folder-children' }); - titleEl.addEventListener('click', (e) => { - e.preventDefault(); - const isCurrentlyCollapsed = groupEl.hasClass('note-status-is-collapsed'); + // Sort files by name + files.sort((a, b) => a.basename.localeCompare(b.basename)); + + // Initialize pagination + if (!this.paginationState.currentPage[status]) { + this.paginationState.currentPage[status] = 0; + } + + const currentPage = this.paginationState.currentPage[status]; + const itemsPerPage = this.paginationState.itemsPerPage; + const totalPages = Math.ceil(files.length / itemsPerPage); + const startIndex = currentPage * itemsPerPage; + const endIndex = Math.min(startIndex + itemsPerPage, files.length); + + // Create file list items for current page + files.slice(startIndex, endIndex).forEach(file => { + this.createFileListItem(childrenEl, file, status); + }); + + // Add pagination if needed + if (files.length > itemsPerPage) { + this.addPaginationControls(childrenEl, status, currentPage, totalPages, files.length); + } + } + + /** + * Add pagination controls to a group + */ + private addPaginationControls( + container: HTMLElement, + status: string, + currentPage: number, + totalPages: number, + totalItems: number + ): void { + const paginationEl = container.createDiv({ cls: 'note-status-pagination' }); + + // Add previous page button if not on first page + if (currentPage > 0) { + const prevButton = paginationEl.createEl('button', { + text: 'Previous', + cls: 'note-status-pagination-button' + }); + + prevButton.addEventListener('click', (e) => { + e.stopPropagation(); + this.paginationState.currentPage[status] = currentPage - 1; + this.renderGroups(this.searchInput?.value.toLowerCase() || ''); + }); + } + + // Add page indicator + paginationEl.createSpan({ + text: `Page ${currentPage + 1} of ${totalPages} (${totalItems} notes)`, + cls: 'note-status-pagination-info' + }); + + // Add next page button if not on last page + if (currentPage < totalPages - 1) { + const nextButton = paginationEl.createEl('button', { + text: 'Next', + cls: 'note-status-pagination-button' + }); + + nextButton.addEventListener('click', (e) => { + e.stopPropagation(); + this.paginationState.currentPage[status] = currentPage + 1; + this.renderGroups(this.searchInput?.value.toLowerCase() || ''); + }); + } + } - // Toggle the collapsed state - if (isCurrentlyCollapsed) { - groupEl.removeClass('note-status-is-collapsed'); - collapseContainer.empty(); - setIcon(collapseContainer, 'chevron-down'); - } else { - groupEl.addClass('note-status-is-collapsed'); - collapseContainer.empty(); - setIcon(collapseContainer, 'chevron-right'); - } + private createFileListItem(container: HTMLElement, file: TFile, status: string): void { + if (!file || !(file instanceof TFile)) return; + + const fileEl = container.createDiv({ cls: 'nav-file' }); + const fileTitleEl = fileEl.createDiv({ cls: 'nav-file-title' }); - // Update the settings - this.settings.collapsedStatuses[status] = !isCurrentlyCollapsed; + // Add file icon if in standard view + if (!this.settings.compactView) { + const fileIcon = fileTitleEl.createDiv({ cls: 'nav-file-icon' }); + setIcon(fileIcon, 'file'); + } - // Trigger settings save - window.dispatchEvent(new CustomEvent('note-status:settings-changed')); - }); + // Add file name + fileTitleEl.createSpan({ + text: file.basename, + cls: 'nav-file-title-content' + }); - // Create and populate child elements - const childrenEl = groupEl.createDiv({ cls: 'nav-folder-children' }); + // Add status indicator + fileTitleEl.createSpan({ + cls: `note-status-icon nav-file-tag status-${status}`, + text: this.statusService.getStatusIcon(status) + }); - // Sort files by name - files.sort((a, b) => a.basename.localeCompare(b.basename)); - - // Initialize pagination for this status if not already done - if (!this.paginationState.currentPage[status]) { - this.paginationState.currentPage[status] = 0; - } - - // Calculate pagination - const currentPage = this.paginationState.currentPage[status]; - const itemsPerPage = this.paginationState.itemsPerPage; - const totalPages = Math.ceil(files.length / itemsPerPage); - const startIndex = currentPage * itemsPerPage; - const endIndex = Math.min(startIndex + itemsPerPage, files.length); - - // Create file list items for current page only - const filesToRender = files.slice(startIndex, endIndex); - filesToRender.forEach(file => { - this.createFileListItem(childrenEl, file, status); - }); - - // Add pagination controls if needed - if (files.length > itemsPerPage) { - this.addPaginationControls(childrenEl, status, currentPage, totalPages, files.length); - } - } - - /** - * Add pagination controls to a group - */ - private addPaginationControls( - container: HTMLElement, - status: string, - currentPage: number, - totalPages: number, - totalItems: number - ): void { - const paginationEl = container.createDiv({ cls: 'note-status-pagination' }); - - // Add previous page button if not on first page - if (currentPage > 0) { - const prevButton = paginationEl.createEl('button', { - text: 'Previous', - cls: 'note-status-pagination-button' - }); - - prevButton.addEventListener('click', (e) => { - e.stopPropagation(); - this.paginationState.currentPage[status] = currentPage - 1; - this.renderGroups(this.searchInput?.value.toLowerCase() || ''); - }); - } - - // Add page indicator - paginationEl.createSpan({ - text: `Page ${currentPage + 1} of ${totalPages} (${totalItems} notes)`, - cls: 'note-status-pagination-info' - }); - - // Add next page button if not on last page - if (currentPage < totalPages - 1) { - const nextButton = paginationEl.createEl('button', { - text: 'Next', - cls: 'note-status-pagination-button' - }); - - nextButton.addEventListener('click', (e) => { - e.stopPropagation(); - this.paginationState.currentPage[status] = currentPage + 1; - this.renderGroups(this.searchInput?.value.toLowerCase() || ''); - }); - } - } + // Add click handler to open the file + fileEl.addEventListener('click', (e) => { + e.preventDefault(); + this.app.workspace.openLinkText(file.path, file.path, true); + }); - private createFileListItem(container: HTMLElement, file: TFile, status: string): void { - if (!file || !(file instanceof TFile)) return; // Skip if file is invalid - - const fileEl = container.createDiv({ cls: 'nav-file' }); - const fileTitleEl = fileEl.createDiv({ cls: 'nav-file-title' }); + // Add context menu + fileEl.addEventListener('contextmenu', (e) => { + e.preventDefault(); + this.showFileContextMenu(e, file); + }); + } - // Add file icon if in standard view - if (!this.settings.compactView) { - const fileIcon = fileTitleEl.createDiv({ cls: 'nav-file-icon' }); - setIcon(fileIcon, 'file'); - } + private showFileContextMenu(e: MouseEvent, file: TFile): void { + const menu = new Menu(); + + menu.addItem((item) => + item.setTitle('Change status') + .setIcon('tag') + .onClick(() => { + this.plugin.statusContextMenu.showForFile(file, e); + }) + ); + + menu.addItem((item) => + item.setTitle('Open in new tab') + .setIcon('lucide-external-link') + .onClick(() => { + this.app.workspace.openLinkText(file.path, file.path, 'tab'); + }) + ); + + menu.showAtMouseEvent(e); + } - // Add file name with proper class for styling - fileTitleEl.createSpan({ - text: file.basename, - cls: 'nav-file-title-content' - }); + onClose(): Promise { + this.containerEl.empty(); + return Promise.resolve(); + } - // Add status indicator - fileTitleEl.createSpan({ - cls: `note-status-icon nav-file-tag status-${status}`, - text: this.statusService.getStatusIcon(status) - }); - - // Add click handler to open the file - fileEl.addEventListener('click', (e) => { - e.preventDefault(); - this.app.workspace.openLinkText(file.path, file.path, true); - }); - - // Add context menu - fileEl.addEventListener('contextmenu', (e) => { - e.preventDefault(); - this.showFileContextMenu(e, file); - }); - } - - private showFileContextMenu(e: MouseEvent, file: TFile): void { - const menu = new Menu(); - - // Add status change options - menu.addItem((item) => - item.setTitle('Change status') - .setIcon('tag') - .onClick(() => { - // Use the position from the event - this.plugin.statusContextMenu.showForFile(file, e); - }) - ); - - // Add open options - menu.addItem((item) => - item.setTitle('Open in new tab') - .setIcon('lucide-external-link') - .onClick(() => { - this.app.workspace.openLinkText(file.path, file.path, 'tab'); - }) - ); - - menu.showAtMouseEvent(e); - } - - onClose(): Promise { - this.containerEl.empty(); - return Promise.resolve(); - } - - /** - * Update view when settings change - */ - updateSettings(settings: NoteStatusSettings): void { - this.settings = settings; - this.containerEl.toggleClass('note-status-compact-view', settings.compactView); - - // Refresh the view with the current search query - this.renderGroups(this.searchInput?.value.toLowerCase() || ''); - } + /** + * Update view when settings change + */ + updateSettings(settings: NoteStatusSettings): void { + this.settings = settings; + this.containerEl.toggleClass('note-status-compact-view', settings.compactView); + this.renderGroups(this.searchInput?.value.toLowerCase() || ''); + } } \ No newline at end of file From 8754f4b0858eae280303bb54b5cd94bbeba8c299 Mon Sep 17 00:00:00 2001 From: Aleix Date: Tue, 13 May 2025 10:59:54 +0200 Subject: [PATCH 15/67] refactor(core): improve main plugin class organization and error handling --- main.ts | 105 ++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 72 insertions(+), 33 deletions(-) diff --git a/main.ts b/main.ts index 663096c..d995b3b 100644 --- a/main.ts +++ b/main.ts @@ -375,19 +375,20 @@ export default class NoteStatus extends Plugin { private registerFileEvents(): void { // File modification events with optimization this.registerEvent(this.app.vault.on('modify', (file) => { - if (file instanceof TFile && file.extension === 'md') { - // Only update UI for the modified file - this.explorerIntegration.updateFileExplorerIcons(file); + if (!(file instanceof TFile) || file.extension !== 'md') return; + + // Only update UI for the modified file + this.explorerIntegration.updateFileExplorerIcons(file); - // If this is the active file, also update other UI elements - if (this.app.workspace.getActiveFile()?.path === file.path) { - this.checkNoteStatus(); - this.statusDropdown.update(this.getCurrentStatuses()); - } - - // Update the status pane but debounced - this.debouncedUpdateStatusPane(); + // If this is the active file, also update other UI elements + const activeFile = this.app.workspace.getActiveFile(); + if (activeFile?.path === file.path) { + this.checkNoteStatus(); + this.statusDropdown.update(this.getCurrentStatuses()); } + + // Update the status pane but debounced + this.debouncedUpdateStatusPane(); })); // File creation, deletion, and rename events @@ -445,22 +446,35 @@ export default class NoteStatus extends Plugin { checkNoteStatus(): void { try { const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile || !(activeFile instanceof TFile) || activeFile.extension !== 'md') { - this.statusBar.update(['unknown']); - this.statusDropdown.update(['unknown']); + if (!activeFile || activeFile.extension !== 'md') { + this.updateStatusComponents(['unknown']); return; } const statuses = this.statusService.getFileStatuses(activeFile); - this.statusBar.update(statuses); - this.statusDropdown.update(statuses); + this.updateStatusComponents(statuses); } catch (error) { - console.error('Error checking note status:', error); - if (!this.hasShownErrorNotification) { - new Notice('Error checking note status. Check console for details.'); - this.hasShownErrorNotification = true; - setTimeout(() => { this.hasShownErrorNotification = false; }, 10000); - } + this.handleStatusError(error); + } + } + + /** + * Update status components with new statuses + */ + private updateStatusComponents(statuses: string[]): void { + this.statusBar.update(statuses); + this.statusDropdown.update(statuses); + } + + /** + * Handle errors when checking status + */ + private handleStatusError(error: any): void { + console.error('Error checking note status:', error); + if (!this.hasShownErrorNotification) { + new Notice('Error checking note status. Check console for details.'); + this.hasShownErrorNotification = true; + setTimeout(() => { this.hasShownErrorNotification = false; }, 10000); } } @@ -564,23 +578,46 @@ export default class NoteStatus extends Plugin { } } + /** + * Get the open status pane view if it exists + */ + private getStatusPaneView(): StatusPaneView | null { + if (this.statusPaneLeaf && this.statusPaneLeaf.view instanceof StatusPaneView) { + return this.statusPaneLeaf.view as StatusPaneView; + } + + const leaf = this.app.workspace.getLeavesOfType('status-pane')[0]; + if (leaf?.view instanceof StatusPaneView) { + this.statusPaneLeaf = leaf; + return leaf.view as StatusPaneView; + } + + return null; + } + /** * Update all components with new settings */ private updateComponentSettings(): void { - // Update services with new settings + // Update services this.statusService.updateSettings(this.settings); this.styleService.updateSettings(this.settings); - // Update UI components with new settings - this.statusBar.updateSettings(this.settings); - this.statusDropdown.updateSettings(this.settings); - this.explorerIntegration.updateSettings(this.settings); - this.statusContextMenu.updateSettings(this.settings); + // Update UI components + const components = [ + this.statusBar, + this.statusDropdown, + this.explorerIntegration, + this.statusContextMenu + ]; + + components.forEach(component => + component.updateSettings?.(this.settings)); // Update status pane if open - if (this.statusPaneLeaf && this.statusPaneLeaf.view instanceof StatusPaneView) { - (this.statusPaneLeaf.view as StatusPaneView).updateSettings(this.settings); + const statusPane = this.getStatusPaneView(); + if (statusPane) { + statusPane.updateSettings(this.settings); } } @@ -590,9 +627,11 @@ export default class NoteStatus extends Plugin { public forceRefreshUI(): void { try { // Cancel any pending updates - this.debouncedCheckNoteStatus.cancel(); - this.debouncedUpdateExplorer.cancel(); - this.debouncedUpdateStatusPane.cancel(); + [ + this.debouncedCheckNoteStatus, + this.debouncedUpdateExplorer, + this.debouncedUpdateStatusPane + ].forEach(fn => fn.cancel()); // Immediate updates this.checkNoteStatus(); From e57d1afc1d0975e075e4be6badc130b686ad7d08 Mon Sep 17 00:00:00 2001 From: Aleix Date: Tue, 13 May 2025 15:46:34 +0200 Subject: [PATCH 16/67] fix: status bar change content instantantly --- ui/components/status-bar.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/ui/components/status-bar.ts b/ui/components/status-bar.ts index 87573f3..ef34278 100644 --- a/ui/components/status-bar.ts +++ b/ui/components/status-bar.ts @@ -144,13 +144,10 @@ export class StatusBar { this.currentStatuses[0] === 'unknown'; if (this.settings.autoHideStatusBar && onlyUnknown) { - this.statusBarEl.addClass('auto-hide'); - setTimeout(() => { - if (onlyUnknown && this.settings.showStatusBar) { - this.statusBarEl.addClass('hidden'); - } - }, 500); + this.statusBarEl.addClass('hidden'); + this.statusBarEl.removeClass('visible'); } else { + this.statusBarEl.removeClass('hidden'); this.statusBarEl.addClass('visible'); } } From c4a51f527b9bfdf83b99dd6426371e643cb85f64 Mon Sep 17 00:00:00 2001 From: Aleix Date: Wed, 14 May 2025 08:29:48 +0200 Subject: [PATCH 17/67] fix: context status change --- ui/menus/status-context-menu.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/ui/menus/status-context-menu.ts b/ui/menus/status-context-menu.ts index e169608..a096e22 100644 --- a/ui/menus/status-context-menu.ts +++ b/ui/menus/status-context-menu.ts @@ -141,13 +141,10 @@ export class StatusContextMenu { */ private async updateFileStatus(file: TFile, statuses: string[]): Promise { if (this.settings.useMultipleStatuses) { - const currentStatuses = this.statusService.getFileStatuses(file); - if (currentStatuses.includes(statuses[0])) { - await this.statusService.removeNoteStatus(statuses[0], file); - } else { - await this.statusService.addNoteStatus(statuses[0], file); - } + // Si tenemos múltiples estados, simplemente añadimos el nuevo + await this.statusService.addNoteStatus(statuses[0], file); } else { + // Reemplazamos todos los estados con el nuevo await this.statusService.updateNoteStatuses([statuses[0]], file); } } From 183cf813b3412d9494034a937dbca6d37060fc8e Mon Sep 17 00:00:00 2001 From: Aleix Date: Wed, 14 May 2025 09:02:31 +0200 Subject: [PATCH 18/67] refactor: centralize status modificacion logic --- services/status-service.ts | 193 ++++++++++----------- ui/components/status-dropdown-component.ts | 39 +++-- ui/components/status-dropdown.ts | 28 ++- ui/menus/status-context-menu.ts | 23 +-- 4 files changed, 149 insertions(+), 134 deletions(-) diff --git a/services/status-service.ts b/services/status-service.ts index f714d44..de6604f 100644 --- a/services/status-service.ts +++ b/services/status-service.ts @@ -123,107 +123,6 @@ export class StatusService { return customStatus ? customStatus.icon : '❓'; } - /** - * Update the statuses of a note - */ - public async updateNoteStatuses(newStatuses: string[], file?: TFile): Promise { - const targetFile = file || this.app.workspace.getActiveFile(); - if (!targetFile || targetFile.extension !== 'md') return; - - await this.app.fileManager.processFrontMatter(targetFile, (frontmatter) => { - frontmatter[this.settings.tagPrefix] = newStatuses; - }); - - this.notifyStatusChanged(newStatuses, targetFile); - } - - /** - * Legacy method to update a single status for backward compatibility - */ - public async updateNoteStatus(newStatus: string, file?: TFile): Promise { - await this.updateNoteStatuses([newStatus], file); - } - - /** - * Add a status to a note's existing statuses - */ - public async addNoteStatus(statusToAdd: string, file?: TFile): Promise { - const targetFile = file || this.app.workspace.getActiveFile(); - if (!targetFile || targetFile.extension !== 'md') return; - - const currentStatuses = this.getFileStatuses(targetFile); - if (currentStatuses.includes(statusToAdd)) return; - - // Filter out 'unknown' status when adding valid statuses - const newStatuses = [...currentStatuses.filter(s => s !== 'unknown'), statusToAdd]; - await this.updateNoteStatuses(newStatuses, targetFile); - } - - /** - * Remove a status from a note's existing statuses - */ - public async removeNoteStatus(statusToRemove: string, file?: TFile): Promise { - const targetFile = file || this.app.workspace.getActiveFile(); - if (!targetFile || targetFile.extension !== 'md') return; - - const currentStatuses = this.getFileStatuses(targetFile); - const newStatuses = currentStatuses.filter(status => status !== statusToRemove); - - await this.updateNoteStatuses(newStatuses, targetFile); - } - - /** - * Toggle a status on/off for a note - */ - public async toggleNoteStatus(statusToToggle: string, file?: TFile): Promise { - const targetFile = file || this.app.workspace.getActiveFile(); - if (!targetFile || targetFile.extension !== 'md') return; - - const currentStatuses = this.getFileStatuses(targetFile); - - const newStatuses = currentStatuses.includes(statusToToggle) - ? currentStatuses.filter(status => status !== statusToToggle) - : [...currentStatuses.filter(s => s !== 'unknown'), statusToToggle]; - - await this.updateNoteStatuses(newStatuses, targetFile); - } - - /** - * Batch update multiple files' statuses - */ - public async batchUpdateStatuses( - files: TFile[], - statusesToSet: string[], - mode: 'replace' | 'add' = 'replace', - showNotice = true - ): Promise { - if (files.length === 0) { - if (showNotice) new Notice('No files selected'); - return; - } - - const updatePromises = files.map(async (file) => { - if (mode === 'replace') { - await this.updateNoteStatuses(statusesToSet, file); - } else { - for (const status of statusesToSet) { - await this.addNoteStatus(status, file); - } - } - }); - - await Promise.all(updatePromises); - - if (showNotice && files.length > 1) { - const statusText = statusesToSet.length === 1 - ? statusesToSet[0] - : `${statusesToSet.length} statuses`; - new Notice(`Updated ${files.length} files with ${statusText}`); - } - - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); - } - /** * Dispatch status changed event */ @@ -319,4 +218,96 @@ export class StatusService { return statusGroups; } + + /** + * Centralizes all status modification operations + */ + public async modifyNoteStatus(options: { + files: TFile | TFile[]; + statuses: string | string[]; + operation: 'set' | 'add' | 'remove' | 'toggle'; + showNotice?: boolean; + }): Promise { + const { operation, showNotice = true } = options; + const targetFiles = Array.isArray(options.files) ? options.files : [options.files]; + const targetStatuses = Array.isArray(options.statuses) ? options.statuses : [options.statuses]; + + if (targetFiles.length === 0) { + if (showNotice) new Notice('No files selected'); + return; + } + + // Process each file + const updatePromises = targetFiles.map(async (file) => { + if (!file || file.extension !== 'md') return; + + // Get current statuses for the file + const currentStatuses = this.getFileStatuses(file); + let newStatuses: string[] = []; + + switch (operation) { + case 'set': + // Replace all statuses with the new ones + newStatuses = [...targetStatuses]; + break; + + case 'add': + // Add new statuses without duplicates + newStatuses = [...new Set([ + ...currentStatuses.filter(s => s !== 'unknown'), + ...targetStatuses + ])]; + break; + + case 'remove': + // Remove specified statuses + newStatuses = currentStatuses.filter( + status => !targetStatuses.includes(status) + ); + break; + + case 'toggle': + // Toggle each status (add if not present, remove if present) + newStatuses = [...currentStatuses]; + for (const status of targetStatuses) { + if (currentStatuses.includes(status)) { + newStatuses = newStatuses.filter(s => s !== status); + } else { + newStatuses = [...newStatuses.filter(s => s !== 'unknown'), status]; + } + } + break; + } + + // Handle empty result (should revert to unknown) + if (newStatuses.length === 0) { + newStatuses = ['unknown']; + } + + // Apply updates to frontmatter + await this.app.fileManager.processFrontMatter(file, (frontmatter) => { + frontmatter[this.settings.tagPrefix] = newStatuses; + }); + + // Notify of changes for UI updates + this.notifyStatusChanged(newStatuses, file); + }); + + await Promise.all(updatePromises); + + // Show notification for batch operations + if (showNotice && targetFiles.length > 1) { + const statusText = targetStatuses.length === 1 + ? targetStatuses[0] + : `${targetStatuses.length} statuses`; + const operationText = operation === 'set' ? 'updated' : + operation === 'add' ? 'added to' : + operation === 'remove' ? 'removed from' : 'toggled on'; + + new Notice(`${statusText} ${operationText} ${targetFiles.length} files`); + } + + // Trigger UI refresh + window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); + } } \ No newline at end of file diff --git a/ui/components/status-dropdown-component.ts b/ui/components/status-dropdown-component.ts index d028553..d9f4454 100644 --- a/ui/components/status-dropdown-component.ts +++ b/ui/components/status-dropdown-component.ts @@ -299,7 +299,12 @@ export class StatusDropdownComponent { private async removeStatus(status: string): Promise { if (!this.targetFile) return; - await this.statusService.removeNoteStatus(status, this.targetFile); + await this.statusService.modifyNoteStatus({ + files: this.targetFile, + statuses: status, + operation: 'remove', + showNotice: false + }); const updatedStatuses = this.statusService.getFileStatuses(this.targetFile); this.currentStatuses = updatedStatuses; @@ -396,17 +401,15 @@ export class StatusDropdownComponent { private handleStatusOptionClick(optionEl: HTMLElement, status: Status): void { optionEl.addClass('note-status-option-selecting'); - setTimeout(async () => { - if (this.targetFile) { - await this.handleStatusChangeForTargetFile(status); - } else { - this.onStatusChange([status.name]); - - if (!this.settings.useMultipleStatuses) { - this.close(); - } + if (this.targetFile) { + this.handleStatusChangeForTargetFile(status); + } else { + this.onStatusChange([status.name]); + + if (!this.settings.useMultipleStatuses) { + this.close(); } - }, 150); + } } /** @@ -416,9 +419,19 @@ export class StatusDropdownComponent { if (!this.targetFile) return; if (this.settings.useMultipleStatuses) { - await this.statusService.toggleNoteStatus(status.name, this.targetFile); + await this.statusService.modifyNoteStatus({ + files: this.targetFile, + statuses: status.name, + operation: 'toggle', + showNotice: false + }); } else { - await this.statusService.updateNoteStatuses([status.name], this.targetFile); + await this.statusService.modifyNoteStatus({ + files: this.targetFile, + statuses: [status.name], + operation: 'set', + showNotice: false + }); this.close(); } diff --git a/ui/components/status-dropdown.ts b/ui/components/status-dropdown.ts index ee77327..43dae55 100644 --- a/ui/components/status-dropdown.ts +++ b/ui/components/status-dropdown.ts @@ -150,9 +150,19 @@ export class StatusDropdown { */ private async updateFileStatus(file: TFile, statuses: string[]): Promise { if (this.settings.useMultipleStatuses) { - await this.statusService.toggleNoteStatus(statuses[0], file); + await this.statusService.modifyNoteStatus({ + files: file, + statuses: statuses[0], + operation: 'toggle', + showNotice: false + }); } else { - await this.statusService.updateNoteStatuses(statuses, file); + await this.statusService.modifyNoteStatus({ + files: file, + statuses: statuses, + operation: 'set', + showNotice: false + }); } this.notifyStatusChanged(statuses); @@ -386,14 +396,14 @@ export class StatusDropdown { ); const shouldRemove = filesWithStatus.length > files.length / 2; + const operation = shouldRemove ? 'remove' : 'add'; - for (const file of files) { - if (shouldRemove) { - await this.statusService.removeNoteStatus(status, file); - } else { - await this.statusService.addNoteStatus(status, file); - } - } + await this.statusService.modifyNoteStatus({ + files: files, + statuses: status, + operation: operation, + showNotice: true + }); } /** diff --git a/ui/menus/status-context-menu.ts b/ui/menus/status-context-menu.ts index a096e22..75222c4 100644 --- a/ui/menus/status-context-menu.ts +++ b/ui/menus/status-context-menu.ts @@ -141,11 +141,19 @@ export class StatusContextMenu { */ private async updateFileStatus(file: TFile, statuses: string[]): Promise { if (this.settings.useMultipleStatuses) { - // Si tenemos múltiples estados, simplemente añadimos el nuevo - await this.statusService.addNoteStatus(statuses[0], file); + await this.statusService.modifyNoteStatus({ + files: file, + statuses: statuses[0], + operation: 'add', + showNotice: false + }); } else { - // Reemplazamos todos los estados con el nuevo - await this.statusService.updateNoteStatuses([statuses[0]], file); + await this.statusService.modifyNoteStatus({ + files: file, + statuses: statuses[0], + operation: 'set', + showNotice: false + }); } } @@ -165,12 +173,5 @@ export class StatusContextMenu { window.dispatchEvent(new CustomEvent('note-status:status-changed', { detail: { statuses, file: file.path } })); - - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); - - // Force a full UI refresh with slight delay - setTimeout(() => { - window.dispatchEvent(new CustomEvent('note-status:force-refresh')); - }, 100); } } \ No newline at end of file From 579b95099df8391799da493bf14dcb04f68f1e0a Mon Sep 17 00:00:00 2001 From: Aleix Date: Wed, 14 May 2025 09:10:34 +0200 Subject: [PATCH 19/67] refactor: handle status change --- services/status-service.ts | 56 ++++++++++++++++++++++ ui/components/status-dropdown-component.ts | 47 +++++++++++------- ui/components/status-dropdown.ts | 45 +++++++---------- ui/menus/status-context-menu.ts | 29 +++++++---- 4 files changed, 124 insertions(+), 53 deletions(-) diff --git a/services/status-service.ts b/services/status-service.ts index de6604f..cedc8ff 100644 --- a/services/status-service.ts +++ b/services/status-service.ts @@ -310,4 +310,60 @@ export class StatusService { // Trigger UI refresh window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); } + + /** + * Handles UI-triggered status changes with appropriate logic based on context + */ + public async handleStatusChange(options: { + files: TFile | TFile[]; + statuses: string | string[]; + isMultipleSelection?: boolean; + allowMultipleStatuses?: boolean; + afterChange?: (updatedStatuses: string[]) => void; + }): Promise { + const { + files, + statuses, + isMultipleSelection = false, + allowMultipleStatuses = this.settings.useMultipleStatuses, + afterChange + } = options; + + const targetFiles = Array.isArray(files) ? files : [files]; + const targetStatuses = Array.isArray(statuses) ? statuses : [statuses]; + + // Determine operation based on context + let operation: 'set' | 'add' | 'remove' | 'toggle'; + + if (isMultipleSelection) { + // For multiple files, we need to check if we should add or remove + const firstStatus = targetStatuses[0]; // Use first status for multi-file operations + const filesWithStatus = targetFiles.filter(file => + this.getFileStatuses(file).includes(firstStatus) + ); + + operation = filesWithStatus.length > targetFiles.length / 2 ? 'remove' : 'add'; + } else { + // For single file operations + if (allowMultipleStatuses) { + operation = 'toggle'; + } else { + operation = 'set'; + } + } + + // Apply the changes + await this.modifyNoteStatus({ + files: targetFiles, + statuses: targetStatuses, + operation, + showNotice: isMultipleSelection + }); + + // Optional callback with updated statuses + if (afterChange && targetFiles.length === 1 && !Array.isArray(files)) { + const updatedStatuses = this.getFileStatuses(files as TFile); + afterChange(updatedStatuses); + } + } } \ No newline at end of file diff --git a/ui/components/status-dropdown-component.ts b/ui/components/status-dropdown-component.ts index d9f4454..03445f9 100644 --- a/ui/components/status-dropdown-component.ts +++ b/ui/components/status-dropdown-component.ts @@ -299,18 +299,16 @@ export class StatusDropdownComponent { private async removeStatus(status: string): Promise { if (!this.targetFile) return; - await this.statusService.modifyNoteStatus({ + await this.statusService.handleStatusChange({ files: this.targetFile, statuses: status, - operation: 'remove', - showNotice: false + allowMultipleStatuses: false, // Queremos eliminar específicamente + afterChange: (updatedStatuses) => { + this.currentStatuses = updatedStatuses; + this.refreshDropdownContent(); + this.onStatusChange(updatedStatuses); + } }); - - const updatedStatuses = this.statusService.getFileStatuses(this.targetFile); - this.currentStatuses = updatedStatuses; - - this.refreshDropdownContent(); - this.onStatusChange(updatedStatuses); } /** @@ -398,18 +396,31 @@ export class StatusDropdownComponent { /** * Handle click on a status option */ - private handleStatusOptionClick(optionEl: HTMLElement, status: Status): void { + private async handleStatusOptionClick(optionEl: HTMLElement, status: Status): void { optionEl.addClass('note-status-option-selecting'); - if (this.targetFile) { - this.handleStatusChangeForTargetFile(status); - } else { - this.onStatusChange([status.name]); - - if (!this.settings.useMultipleStatuses) { - this.close(); + setTimeout(async () => { + if (this.targetFile) { + await this.statusService.handleStatusChange({ + files: this.targetFile, + statuses: status.name, + afterChange: (updatedStatuses) => { + this.currentStatuses = updatedStatuses; + this.onStatusChange(updatedStatuses); + + if (!this.settings.useMultipleStatuses) { + this.close(); + } + } + }); + } else { + this.onStatusChange([status.name]); + + if (!this.settings.useMultipleStatuses) { + this.close(); + } } - } + }, 150); } /** diff --git a/ui/components/status-dropdown.ts b/ui/components/status-dropdown.ts index 43dae55..320540d 100644 --- a/ui/components/status-dropdown.ts +++ b/ui/components/status-dropdown.ts @@ -149,23 +149,13 @@ export class StatusDropdown { * Update a file's status */ private async updateFileStatus(file: TFile, statuses: string[]): Promise { - if (this.settings.useMultipleStatuses) { - await this.statusService.modifyNoteStatus({ - files: file, - statuses: statuses[0], - operation: 'toggle', - showNotice: false - }); - } else { - await this.statusService.modifyNoteStatus({ - files: file, - statuses: statuses, - operation: 'set', - showNotice: false - }); - } - - this.notifyStatusChanged(statuses); + await this.statusService.handleStatusChange({ + files: file, + statuses: statuses, + afterChange: (updatedStatuses) => { + this.notifyStatusChanged(updatedStatuses); + } + }); } /** @@ -391,18 +381,19 @@ export class StatusDropdown { * Toggle a status across multiple files */ private async toggleStatusForFiles(files: TFile[], status: string): Promise { - const filesWithStatus = files.filter(file => - this.statusService.getFileStatuses(file).includes(status) - ); - - const shouldRemove = filesWithStatus.length > files.length / 2; - const operation = shouldRemove ? 'remove' : 'add'; - - await this.statusService.modifyNoteStatus({ + await this.statusService.handleStatusChange({ files: files, statuses: status, - operation: operation, - showNotice: true + isMultipleSelection: true, + afterChange: () => { + window.dispatchEvent(new CustomEvent('note-status:batch-update-complete', { + detail: { + status: status, + fileCount: files.length + } + })); + window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); + } }); } diff --git a/ui/menus/status-context-menu.ts b/ui/menus/status-context-menu.ts index 75222c4..1c7e09a 100644 --- a/ui/menus/status-context-menu.ts +++ b/ui/menus/status-context-menu.ts @@ -126,14 +126,27 @@ export class StatusContextMenu { private async handleStatusUpdateForFile(file: TFile, statuses: string[]): Promise { if (!(file instanceof TFile) || file.extension !== 'md' || statuses.length === 0) return; - await this.updateFileStatus(file, statuses); - - // Force explorer icon update - this.app.metadataCache.trigger('changed', file); - this.updateExplorerIcon(file); - - // Dispatch events to update other UI components - this.triggerUIUpdates(statuses, file); + await this.statusService.handleStatusChange({ + files: file, + statuses: statuses, + afterChange: (updatedStatuses) => { + // Force explorer icon update + this.app.metadataCache.trigger('changed', file); + this.updateExplorerIcon(file); + + // Dispatch events to update other UI components + window.dispatchEvent(new CustomEvent('note-status:status-changed', { + detail: { statuses: updatedStatuses, file: file.path } + })); + + window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); + + // Force a full UI refresh with slight delay + setTimeout(() => { + window.dispatchEvent(new CustomEvent('note-status:force-refresh')); + }, 100); + } + }); } /** From 6eab30bc2c10c604b326dc24ce6d03abb112a6b6 Mon Sep 17 00:00:00 2001 From: Aleix Date: Thu, 15 May 2025 08:53:05 +0200 Subject: [PATCH 20/67] chore: partial with bugs --- .gitignore | 1 + ui/components/status-dropdown-component.ts | 18 +++++----- ui/components/status-dropdown.ts | 12 ++++--- ui/menus/status-context-menu.ts | 38 +++++++++++----------- 4 files changed, 38 insertions(+), 31 deletions(-) diff --git a/.gitignore b/.gitignore index e09a007..2c116ac 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ data.json # Exclude macOS Finder (System Explorer) View States .DS_Store +styles.css diff --git a/ui/components/status-dropdown-component.ts b/ui/components/status-dropdown-component.ts index 03445f9..d733be1 100644 --- a/ui/components/status-dropdown-component.ts +++ b/ui/components/status-dropdown-component.ts @@ -299,16 +299,18 @@ export class StatusDropdownComponent { private async removeStatus(status: string): Promise { if (!this.targetFile) return; - await this.statusService.handleStatusChange({ + await this.statusService.modifyNoteStatus({ files: this.targetFile, statuses: status, - allowMultipleStatuses: false, // Queremos eliminar específicamente - afterChange: (updatedStatuses) => { - this.currentStatuses = updatedStatuses; - this.refreshDropdownContent(); - this.onStatusChange(updatedStatuses); - } + operation: 'remove', + showNotice: false }); + + // Get fresh statuses after the change + const updatedStatuses = this.statusService.getFileStatuses(this.targetFile); + this.currentStatuses = updatedStatuses; + this.refreshDropdownContent(); + this.onStatusChange(updatedStatuses); } /** @@ -396,7 +398,7 @@ export class StatusDropdownComponent { /** * Handle click on a status option */ - private async handleStatusOptionClick(optionEl: HTMLElement, status: Status): void { + private handleStatusOptionClick(optionEl: HTMLElement, status: Status): void { optionEl.addClass('note-status-option-selecting'); setTimeout(async () => { diff --git a/ui/components/status-dropdown.ts b/ui/components/status-dropdown.ts index 320540d..322ce6c 100644 --- a/ui/components/status-dropdown.ts +++ b/ui/components/status-dropdown.ts @@ -149,13 +149,17 @@ export class StatusDropdown { * Update a file's status */ private async updateFileStatus(file: TFile, statuses: string[]): Promise { - await this.statusService.handleStatusChange({ + const operation = this.settings.useMultipleStatuses ? 'toggle' : 'set'; + + await this.statusService.modifyNoteStatus({ files: file, statuses: statuses, - afterChange: (updatedStatuses) => { - this.notifyStatusChanged(updatedStatuses); - } + operation: operation, + showNotice: false }); + + const updatedStatuses = this.statusService.getFileStatuses(file); + this.notifyStatusChanged(updatedStatuses); } /** diff --git a/ui/menus/status-context-menu.ts b/ui/menus/status-context-menu.ts index 1c7e09a..c33f55e 100644 --- a/ui/menus/status-context-menu.ts +++ b/ui/menus/status-context-menu.ts @@ -125,30 +125,30 @@ export class StatusContextMenu { */ private async handleStatusUpdateForFile(file: TFile, statuses: string[]): Promise { if (!(file instanceof TFile) || file.extension !== 'md' || statuses.length === 0) return; + const operation = this.settings.useMultipleStatuses ? 'toggle' : 'set'; - await this.statusService.handleStatusChange({ + await this.statusService.modifyNoteStatus({ files: file, statuses: statuses, - afterChange: (updatedStatuses) => { - // Force explorer icon update - this.app.metadataCache.trigger('changed', file); - this.updateExplorerIcon(file); - - // Dispatch events to update other UI components - window.dispatchEvent(new CustomEvent('note-status:status-changed', { - detail: { statuses: updatedStatuses, file: file.path } - })); - - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); - - // Force a full UI refresh with slight delay - setTimeout(() => { - window.dispatchEvent(new CustomEvent('note-status:force-refresh')); - }, 100); - } + operation: operation, + showNotice: false }); + + // Get fresh statuses + const updatedStatuses = this.statusService.getFileStatuses(file); + this.notifyStatusChanged(updatedStatuses); + } - + /** + * Notify that status has changed + */ + private notifyStatusChanged(statuses: string[]): void { + window.dispatchEvent(new CustomEvent('note-status:status-changed', { + detail: { statuses } + })); + window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); + } + /** * Update file status based on settings */ From 6c0adea49ef130df3e55befed52bb9a6942feecb Mon Sep 17 00:00:00 2001 From: Aleix Date: Thu, 15 May 2025 09:14:36 +0200 Subject: [PATCH 21/67] chore: wip centralize logic --- services/status-service.ts | 55 ++++++++++++++--- ui/components/status-dropdown-component.ts | 43 +++---------- ui/components/status-dropdown.ts | 71 ++++++++-------------- ui/menus/status-context-menu.ts | 18 +----- 4 files changed, 78 insertions(+), 109 deletions(-) diff --git a/services/status-service.ts b/services/status-service.ts index cedc8ff..7d5589f 100644 --- a/services/status-service.ts +++ b/services/status-service.ts @@ -126,10 +126,17 @@ export class StatusService { /** * Dispatch status changed event */ - private notifyStatusChanged(statuses: string[], file: TFile): void { + public notifyStatusChanged(statuses: string[], file?: TFile): void { + // Dispatch the specific status change event window.dispatchEvent(new CustomEvent('note-status:status-changed', { - detail: { statuses, file: file.path } + detail: { + statuses, + file: file?.path + } })); + + // General UI refresh happens via this event + window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); } /** @@ -313,29 +320,36 @@ export class StatusService { /** * Handles UI-triggered status changes with appropriate logic based on context + * This is the central function that all UI components should call for status changes */ public async handleStatusChange(options: { - files: TFile | TFile[]; - statuses: string | string[]; - isMultipleSelection?: boolean; - allowMultipleStatuses?: boolean; - afterChange?: (updatedStatuses: string[]) => void; + files: TFile | TFile[]; + statuses: string | string[]; + isMultipleSelection?: boolean; + allowMultipleStatuses?: boolean; + operation?: 'set' | 'add' | 'remove' | 'toggle'; + showNotice?: boolean; + afterChange?: (updatedStatuses: string[]) => void; }): Promise { const { files, statuses, isMultipleSelection = false, allowMultipleStatuses = this.settings.useMultipleStatuses, + operation: explicitOperation, + showNotice = isMultipleSelection, afterChange } = options; const targetFiles = Array.isArray(files) ? files : [files]; const targetStatuses = Array.isArray(statuses) ? statuses : [statuses]; - // Determine operation based on context + // Determine operation based on context if not explicitly specified let operation: 'set' | 'add' | 'remove' | 'toggle'; - if (isMultipleSelection) { + if (explicitOperation) { + operation = explicitOperation; + } else if (isMultipleSelection) { // For multiple files, we need to check if we should add or remove const firstStatus = targetStatuses[0]; // Use first status for multi-file operations const filesWithStatus = targetFiles.filter(file => @@ -357,7 +371,7 @@ export class StatusService { files: targetFiles, statuses: targetStatuses, operation, - showNotice: isMultipleSelection + showNotice }); // Optional callback with updated statuses @@ -365,5 +379,26 @@ export class StatusService { const updatedStatuses = this.getFileStatuses(files as TFile); afterChange(updatedStatuses); } + + // Ensure comprehensive UI updates + this.refreshUI(targetFiles); } + + /** + * Centralizes UI refresh after status changes + */ + private refreshUI(files: TFile[]): void { + // General UI refresh + window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); + + // If it's the active file, ensure status bar and toolbar are updated + const activeFile = this.app.workspace.getActiveFile(); + if (activeFile && files.some(f => f.path === activeFile.path)) { + const statuses = this.getFileStatuses(activeFile); + window.dispatchEvent(new CustomEvent('note-status:status-changed', { + detail: { statuses, file: activeFile.path } + })); + } + } + } \ No newline at end of file diff --git a/ui/components/status-dropdown-component.ts b/ui/components/status-dropdown-component.ts index d733be1..1960a04 100644 --- a/ui/components/status-dropdown-component.ts +++ b/ui/components/status-dropdown-component.ts @@ -299,18 +299,17 @@ export class StatusDropdownComponent { private async removeStatus(status: string): Promise { if (!this.targetFile) return; - await this.statusService.modifyNoteStatus({ + await this.statusService.handleStatusChange({ files: this.targetFile, statuses: status, operation: 'remove', - showNotice: false + showNotice: false, + afterChange: (updatedStatuses) => { + this.currentStatuses = updatedStatuses; + this.refreshDropdownContent(); + this.onStatusChange(updatedStatuses); + } }); - - // Get fresh statuses after the change - const updatedStatuses = this.statusService.getFileStatuses(this.targetFile); - this.currentStatuses = updatedStatuses; - this.refreshDropdownContent(); - this.onStatusChange(updatedStatuses); } /** @@ -425,34 +424,6 @@ export class StatusDropdownComponent { }, 150); } - /** - * Handle status change for a specific target file - */ - private async handleStatusChangeForTargetFile(status: Status): Promise { - if (!this.targetFile) return; - - if (this.settings.useMultipleStatuses) { - await this.statusService.modifyNoteStatus({ - files: this.targetFile, - statuses: status.name, - operation: 'toggle', - showNotice: false - }); - } else { - await this.statusService.modifyNoteStatus({ - files: this.targetFile, - statuses: [status.name], - operation: 'set', - showNotice: false - }); - this.close(); - } - - const freshStatuses = this.statusService.getFileStatuses(this.targetFile); - this.currentStatuses = [...freshStatuses]; - this.onStatusChange(freshStatuses); - } - /** * Position the dropdown at specific coordinates */ diff --git a/ui/components/status-dropdown.ts b/ui/components/status-dropdown.ts index 322ce6c..b113493 100644 --- a/ui/components/status-dropdown.ts +++ b/ui/components/status-dropdown.ts @@ -29,19 +29,10 @@ export class StatusDropdown { this.dropdownComponent.setOnStatusChange((statuses) => { this.currentStatuses = [...statuses]; this.updateToolbarButton(); - this.notifyStatusChanged(statuses); + this.statusService.notifyStatusChanged(statuses); }); } - /** - * Notify that status has changed - */ - private notifyStatusChanged(statuses: string[]): void { - window.dispatchEvent(new CustomEvent('note-status:status-changed', { - detail: { statuses } - })); - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); - } /** * Updates the toolbar button appearance @@ -149,17 +140,11 @@ export class StatusDropdown { * Update a file's status */ private async updateFileStatus(file: TFile, statuses: string[]): Promise { - const operation = this.settings.useMultipleStatuses ? 'toggle' : 'set'; - - await this.statusService.modifyNoteStatus({ + await this.statusService.handleStatusChange({ files: file, statuses: statuses, - operation: operation, showNotice: false }); - - const updatedStatuses = this.statusService.getFileStatuses(file); - this.notifyStatusChanged(updatedStatuses); } /** @@ -277,7 +262,7 @@ export class StatusDropdown { }, files: TFile[], isSingleFile: boolean - ): void { + ): void { if (options.onStatusChange) { this.dropdownComponent.setOnStatusChange(options.onStatusChange); } else if (!isSingleFile) { @@ -286,19 +271,33 @@ export class StatusDropdown { this.dropdownComponent.setOnStatusChange(async (statuses) => { if (statuses.length > 0) { const toggledStatus = statuses[statuses.length - 1]; - await this.toggleStatusForFiles(filesForBatch, toggledStatus); - window.dispatchEvent(new CustomEvent('note-status:batch-update-complete', { - detail: { - status: toggledStatus, - fileCount: filesForBatch.length + await this.statusService.handleStatusChange({ + files: filesForBatch, + statuses: toggledStatus, + isMultipleSelection: true, + afterChange: () => { + // Keep the batch update event for any listeners that need it + window.dispatchEvent(new CustomEvent('note-status:batch-update-complete', { + detail: { + status: toggledStatus, + fileCount: filesForBatch.length + } + })); } - })); - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); + }); } }); } else { - this.dropdownComponent.setOnStatusChange(this.notifyStatusChanged.bind(this)); + // For single file, just use handleStatusChange directly + this.dropdownComponent.setOnStatusChange(async (statuses) => { + if (statuses.length > 0 && files[0]) { + await this.statusService.handleStatusChange({ + files: files[0], + statuses: statuses + }); + } + }); } } @@ -381,26 +380,6 @@ export class StatusDropdown { ); } - /** - * Toggle a status across multiple files - */ - private async toggleStatusForFiles(files: TFile[], status: string): Promise { - await this.statusService.handleStatusChange({ - files: files, - statuses: status, - isMultipleSelection: true, - afterChange: () => { - window.dispatchEvent(new CustomEvent('note-status:batch-update-complete', { - detail: { - status: status, - fileCount: files.length - } - })); - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); - } - }); - } - /** * Adds the toolbar button to the active leaf */ diff --git a/ui/menus/status-context-menu.ts b/ui/menus/status-context-menu.ts index c33f55e..2bd6a59 100644 --- a/ui/menus/status-context-menu.ts +++ b/ui/menus/status-context-menu.ts @@ -125,28 +125,12 @@ export class StatusContextMenu { */ private async handleStatusUpdateForFile(file: TFile, statuses: string[]): Promise { if (!(file instanceof TFile) || file.extension !== 'md' || statuses.length === 0) return; - const operation = this.settings.useMultipleStatuses ? 'toggle' : 'set'; - await this.statusService.modifyNoteStatus({ + await this.statusService.handleStatusChange({ files: file, statuses: statuses, - operation: operation, showNotice: false }); - - // Get fresh statuses - const updatedStatuses = this.statusService.getFileStatuses(file); - this.notifyStatusChanged(updatedStatuses); - - } - /** - * Notify that status has changed - */ - private notifyStatusChanged(statuses: string[]): void { - window.dispatchEvent(new CustomEvent('note-status:status-changed', { - detail: { statuses } - })); - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); } /** From 2bbac6471d504f2e8e4e2ddba39b60519994b9f6 Mon Sep 17 00:00:00 2001 From: Aleix Date: Thu, 15 May 2025 09:37:37 +0200 Subject: [PATCH 22/67] chore: centralize event calls --- main.ts | 3 ++- services/status-service.ts | 41 +++++++++++++-------------------- settings/settings-tab.ts | 20 ++++++++-------- ui/menus/status-context-menu.ts | 39 ------------------------------- 4 files changed, 27 insertions(+), 76 deletions(-) diff --git a/main.ts b/main.ts index d995b3b..6b29cdf 100644 --- a/main.ts +++ b/main.ts @@ -64,7 +64,8 @@ export default class NoteStatus extends Plugin { this.registerCommands(); // Add settings tab - this.addSettingTab(new NoteStatusSettingTab(this.app, this)); + this.addSettingTab(new NoteStatusSettingTab(this.app, this, + this.statusService)); // Set up custom events this.setupCustomEvents(); diff --git a/services/status-service.ts b/services/status-service.ts index 7d5589f..59e3753 100644 --- a/services/status-service.ts +++ b/services/status-service.ts @@ -123,22 +123,6 @@ export class StatusService { return customStatus ? customStatus.icon : '❓'; } - /** - * Dispatch status changed event - */ - public notifyStatusChanged(statuses: string[], file?: TFile): void { - // Dispatch the specific status change event - window.dispatchEvent(new CustomEvent('note-status:status-changed', { - detail: { - statuses, - file: file?.path - } - })); - - // General UI refresh happens via this event - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); - } - /** * Insert status metadata in the editor */ @@ -296,8 +280,6 @@ export class StatusService { frontmatter[this.settings.tagPrefix] = newStatuses; }); - // Notify of changes for UI updates - this.notifyStatusChanged(newStatuses, file); }); await Promise.all(updatePromises); @@ -313,9 +295,6 @@ export class StatusService { new Notice(`${statusText} ${operationText} ${targetFiles.length} files`); } - - // Trigger UI refresh - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); } /** @@ -331,6 +310,7 @@ export class StatusService { showNotice?: boolean; afterChange?: (updatedStatuses: string[]) => void; }): Promise { + console.log("changing", options) const { files, statuses, @@ -383,11 +363,24 @@ export class StatusService { // Ensure comprehensive UI updates this.refreshUI(targetFiles); } + + /** + * Dispatch status changed event + */ + public notifyStatusChanged(statuses: string[], file?: TFile): void { + // Dispatch the specific status change event + window.dispatchEvent(new CustomEvent('note-status:status-changed', { + detail: { + statuses, + file: file?.path + } + })); + } /** * Centralizes UI refresh after status changes */ - private refreshUI(files: TFile[]): void { + public refreshUI(files: TFile[]): void { // General UI refresh window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); @@ -395,9 +388,7 @@ export class StatusService { const activeFile = this.app.workspace.getActiveFile(); if (activeFile && files.some(f => f.path === activeFile.path)) { const statuses = this.getFileStatuses(activeFile); - window.dispatchEvent(new CustomEvent('note-status:status-changed', { - detail: { statuses, file: activeFile.path } - })); + this.notifyStatusChanged(statuses, activeFile) } } diff --git a/settings/settings-tab.ts b/settings/settings-tab.ts index 98cf799..399676d 100644 --- a/settings/settings-tab.ts +++ b/settings/settings-tab.ts @@ -2,16 +2,19 @@ import { App, PluginSettingTab, Setting, Notice } from 'obsidian'; import { Status } from '../models/types'; import { PREDEFINED_TEMPLATES } from '../constants/status-templates'; import NoteStatus from 'main'; +import { StatusService } from 'services/status-service'; /** * Settings tab for the Note Status plugin */ export class NoteStatusSettingTab extends PluginSettingTab { plugin: NoteStatus; + statusService: StatusService - constructor(app: App, plugin: any) { + constructor(app: App, plugin: any, statusService: StatusService) { super(app, plugin); this.plugin = plugin; + this.statusService = statusService; } display(): void { @@ -102,8 +105,7 @@ export class NoteStatusSettingTab extends PluginSettingTab { // Refresh the status pane if open const statusPane = this.app.workspace.getLeavesOfType('status-pane')[0]; if (statusPane && statusPane.view) { - // Trigger refresh - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); + this.statusService.refreshUI([]); } })); @@ -120,7 +122,7 @@ export class NoteStatusSettingTab extends PluginSettingTab { await this.plugin.saveSettings(); // Refresh UI to show multi-select or single-select options - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); + this.statusService.refreshUI([]); })); // Status tag prefix @@ -137,9 +139,7 @@ export class NoteStatusSettingTab extends PluginSettingTab { this.plugin.settings.tagPrefix = value.trim(); await this.plugin.saveSettings(); - - // Add this line to trigger a full UI refresh - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); + this.statusService.refreshUI([]); })); @@ -157,7 +157,7 @@ export class NoteStatusSettingTab extends PluginSettingTab { await this.plugin.saveSettings(); // Refresh the UI to show/hide template statuses - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); + this.statusService.refreshUI([]); this.display(); })); @@ -295,9 +295,7 @@ export class NoteStatusSettingTab extends PluginSettingTab { } await this.plugin.saveSettings(); - - // Refresh UI - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); + this.statusService.refreshUI([]); }); // Template name diff --git a/ui/menus/status-context-menu.ts b/ui/menus/status-context-menu.ts index 2bd6a59..7d460b8 100644 --- a/ui/menus/status-context-menu.ts +++ b/ui/menus/status-context-menu.ts @@ -132,43 +132,4 @@ export class StatusContextMenu { showNotice: false }); } - - /** - * Update file status based on settings - */ - private async updateFileStatus(file: TFile, statuses: string[]): Promise { - if (this.settings.useMultipleStatuses) { - await this.statusService.modifyNoteStatus({ - files: file, - statuses: statuses[0], - operation: 'add', - showNotice: false - }); - } else { - await this.statusService.modifyNoteStatus({ - files: file, - statuses: statuses[0], - operation: 'set', - showNotice: false - }); - } - } - - /** - * Update explorer icon for a file - */ - private updateExplorerIcon(file: TFile): void { - setTimeout(() => { - this.explorerIntegration?.updateFileExplorerIcons(file); - }, 50); - } - - /** - * Trigger UI updates after status changes - */ - private triggerUIUpdates(statuses: string[], file: TFile): void { - window.dispatchEvent(new CustomEvent('note-status:status-changed', { - detail: { statuses, file: file.path } - })); - } } \ No newline at end of file From e74eacc02f485a2b5f8a5cbfe0bb5bdef080f6d5 Mon Sep 17 00:00:00 2001 From: Aleix Date: Thu, 15 May 2025 09:59:47 +0200 Subject: [PATCH 23/67] chore: context simplification --- ui/components/status-dropdown.ts | 7 +- ui/menus/status-context-menu.ts | 115 ++++++++++--------------------- 2 files changed, 38 insertions(+), 84 deletions(-) diff --git a/ui/components/status-dropdown.ts b/ui/components/status-dropdown.ts index b113493..ac70d6a 100644 --- a/ui/components/status-dropdown.ts +++ b/ui/components/status-dropdown.ts @@ -127,12 +127,7 @@ export class StatusDropdown { this.openStatusDropdown({ position, - files: [activeFile], - onStatusChange: async (statuses) => { - if (statuses.length > 0) { - await this.updateFileStatus(activeFile, statuses); - } - } + files: [activeFile] }); } diff --git a/ui/menus/status-context-menu.ts b/ui/menus/status-context-menu.ts index 7d460b8..ca3b3a2 100644 --- a/ui/menus/status-context-menu.ts +++ b/ui/menus/status-context-menu.ts @@ -35,61 +35,6 @@ export class StatusContextMenu { this.settings = settings; } - /** - * Shows the context menu for changing status of one or more files - */ - public showForFiles(files: TFile[], position?: { x: number; y: number }): void { - if (files.length === 0) return; - - if (files.length === 1) { - this.showSingleFileDropdown(files[0], position); - } else { - this.showMultipleFilesMenu(files, position); - } - } - - /** - * Show dropdown for a single file - */ - private showSingleFileDropdown(file: TFile, position?: { x: number; y: number }): void { - this.statusDropdown.openStatusDropdown({ - position, - files: [file], - onStatusChange: async (statuses) => { - if (statuses.length > 0) { - await this.handleStatusUpdateForFile(file, statuses); - } - } - }); - } - - /** - * Show menu for multiple files - */ - private showMultipleFilesMenu(files: TFile[], position?: { x: number; y: number }): void { - const menu = new Menu(); - - menu.addItem((item) => { - item.setTitle(`Update ${files.length} files`) - .setDisabled(true); - return item; - }); - - menu.addItem((item) => - item - .setTitle('Manage statuses...') - .setIcon('tag') - .onClick(() => { - this.statusDropdown.openStatusDropdown({ - position, - files - }); - }) - ); - - this.showMenu(menu, position); - } - /** * Show the menu at the specified position */ @@ -101,6 +46,42 @@ export class StatusContextMenu { } } + /** + * Shows the context menu for changing status of one or more files + */ + public showForFiles(files: TFile[], position?: { x: number; y: number }): void { + if (files.length === 0) return; + + if (files.length === 1) { + this.statusDropdown.openStatusDropdown({ + position, + files: [files[0]] + }); + } else { + const menu = new Menu(); + + menu.addItem((item) => { + item.setTitle(`Update ${files.length} files`) + .setDisabled(true); + return item; + }); + + menu.addItem((item) => + item + .setTitle('Manage statuses...') + .setIcon('tag') + .onClick(() => { + this.statusDropdown.openStatusDropdown({ + position, + files + }); + }) + ); + + this.showMenu(menu, position); + } + } + /** * Shows a context menu for a single file */ @@ -108,28 +89,6 @@ export class StatusContextMenu { if (!(file instanceof TFile) || file.extension !== 'md') return; const position = { x: event.clientX, y: event.clientY }; - - this.statusDropdown.openStatusDropdown({ - position, - files: [file], - onStatusChange: async (statuses) => { - if (statuses.length > 0) { - await this.handleStatusUpdateForFile(file, statuses); - } - } - }); - } - - /** - * Handle status update for a specific file - */ - private async handleStatusUpdateForFile(file: TFile, statuses: string[]): Promise { - if (!(file instanceof TFile) || file.extension !== 'md' || statuses.length === 0) return; - - await this.statusService.handleStatusChange({ - files: file, - statuses: statuses, - showNotice: false - }); + this.showForFiles([file], position); } } \ No newline at end of file From 0bf3694871d008715659fadebb1521ea7a01c807 Mon Sep 17 00:00:00 2001 From: Aleix Date: Thu, 15 May 2025 10:45:15 +0200 Subject: [PATCH 24/67] chore: status context menu simplified --- main.ts | 2 +- ui/components/status-pane-view.ts | 3 ++- ui/menus/status-context-menu.ts | 32 ++++++++++++------------------- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/main.ts b/main.ts index 6b29cdf..1aa13f6 100644 --- a/main.ts +++ b/main.ts @@ -253,7 +253,7 @@ export default class NoteStatus extends Plugin { return; } - this.statusContextMenu.showForFile(activeFile, new MouseEvent('click')); + this.statusContextMenu.showForSingleFile(activeFile); } /** diff --git a/ui/components/status-pane-view.ts b/ui/components/status-pane-view.ts index 0133caa..4d0ef76 100644 --- a/ui/components/status-pane-view.ts +++ b/ui/components/status-pane-view.ts @@ -452,7 +452,8 @@ export class StatusPaneView extends View { item.setTitle('Change status') .setIcon('tag') .onClick(() => { - this.plugin.statusContextMenu.showForFile(file, e); + const position = { x: e.clientX, y: e.clientY }; + this.plugin.statusContextMenu.showForSingleFile(file, position); }) ); diff --git a/ui/menus/status-context-menu.ts b/ui/menus/status-context-menu.ts index ca3b3a2..e0d3ade 100644 --- a/ui/menus/status-context-menu.ts +++ b/ui/menus/status-context-menu.ts @@ -34,17 +34,6 @@ export class StatusContextMenu { public updateSettings(settings: NoteStatusSettings): void { this.settings = settings; } - - /** - * Show the menu at the specified position - */ - private showMenu(menu: Menu, position?: { x: number; y: number }): void { - if (position) { - menu.showAtPosition(position); - } else { - menu.showAtMouseEvent(new MouseEvent('contextmenu')); - } - } /** * Shows the context menu for changing status of one or more files @@ -53,10 +42,7 @@ export class StatusContextMenu { if (files.length === 0) return; if (files.length === 1) { - this.statusDropdown.openStatusDropdown({ - position, - files: [files[0]] - }); + this.showForSingleFile(files[0], position); } else { const menu = new Menu(); @@ -78,17 +64,23 @@ export class StatusContextMenu { }) ); - this.showMenu(menu, position); + if (position) { + menu.showAtPosition(position); + } else { + menu.showAtMouseEvent(new MouseEvent('contextmenu')); + } } } /** * Shows a context menu for a single file */ - public showForFile(file: TFile, event: MouseEvent): void { + public showForSingleFile(file: TFile, position?: { x: number; y: number }): void { if (!(file instanceof TFile) || file.extension !== 'md') return; - - const position = { x: event.clientX, y: event.clientY }; - this.showForFiles([file], position); + + this.statusDropdown.openStatusDropdown({ + position, + files: [file] + }); } } \ No newline at end of file From 5ffcd4945bca0d8174e578803d28722e740beb47 Mon Sep 17 00:00:00 2001 From: Aleix Date: Thu, 15 May 2025 10:54:56 +0200 Subject: [PATCH 25/67] fix: satus dropdown change status --- ui/components/status-dropdown-component.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/ui/components/status-dropdown-component.ts b/ui/components/status-dropdown-component.ts index 1960a04..1f66903 100644 --- a/ui/components/status-dropdown-component.ts +++ b/ui/components/status-dropdown-component.ts @@ -406,9 +406,6 @@ export class StatusDropdownComponent { files: this.targetFile, statuses: status.name, afterChange: (updatedStatuses) => { - this.currentStatuses = updatedStatuses; - this.onStatusChange(updatedStatuses); - if (!this.settings.useMultipleStatuses) { this.close(); } From 56af780122c653d5e31577fa25a403fa26e36304 Mon Sep 17 00:00:00 2001 From: Aleix Date: Thu, 15 May 2025 11:50:26 +0200 Subject: [PATCH 26/67] chore: status handlers --- ui/components/status-dropdown-component.ts | 51 +++++++--------------- ui/components/status-dropdown.ts | 32 +++++++++----- 2 files changed, 37 insertions(+), 46 deletions(-) diff --git a/ui/components/status-dropdown-component.ts b/ui/components/status-dropdown-component.ts index 1f66903..d111288 100644 --- a/ui/components/status-dropdown-component.ts +++ b/ui/components/status-dropdown-component.ts @@ -18,7 +18,10 @@ export class StatusDropdownComponent { // Bind methods once in constructor private clickOutsideHandler: (e: MouseEvent) => void; - + private onRemoveStatusHandler: (status: string, targetFile?: TFile) => Promise; + private onSelectStatusHandler: (status: string, targetFile: TFile) => Promise; + + constructor(app: App, statusService: StatusService, settings: NoteStatusSettings) { this.app = app; this.statusService = statusService; @@ -44,6 +47,13 @@ export class StatusDropdownComponent { public setTargetFile(file: TFile | null): void { this.targetFile = file; } + + public setOnRemoveStatusHandler(handler: typeof this.onRemoveStatusHandler): void { + this.onRemoveStatusHandler = handler + } + public setOnSelectStatusHandler(handler: typeof this.onSelectStatusHandler): void { + this.onSelectStatusHandler = handler + } /** * Updates settings reference @@ -285,32 +295,13 @@ export class StatusDropdownComponent { setTimeout(async () => { if (this.targetFile) { - await this.removeStatus(status); - } else { - this.onStatusChange([status]); - } + await this.onRemoveStatusHandler(status, this.targetFile); + this.refreshDropdownContent(); // TODO: Pude es pot borrar + } }, 150); }); } - /** - * Remove a status from the target file - */ - private async removeStatus(status: string): Promise { - if (!this.targetFile) return; - - await this.statusService.handleStatusChange({ - files: this.targetFile, - statuses: status, - operation: 'remove', - showNotice: false, - afterChange: (updatedStatuses) => { - this.currentStatuses = updatedStatuses; - this.refreshDropdownContent(); - this.onStatusChange(updatedStatuses); - } - }); - } /** * Create the search filter input @@ -402,22 +393,12 @@ export class StatusDropdownComponent { setTimeout(async () => { if (this.targetFile) { - await this.statusService.handleStatusChange({ - files: this.targetFile, - statuses: status.name, - afterChange: (updatedStatuses) => { - if (!this.settings.useMultipleStatuses) { - this.close(); - } - } - }); - } else { - this.onStatusChange([status.name]); + await this.onSelectStatusHandler(status.name, this.targetFile); if (!this.settings.useMultipleStatuses) { this.close(); } - } + } }, 150); } diff --git a/ui/components/status-dropdown.ts b/ui/components/status-dropdown.ts index ac70d6a..b7941d2 100644 --- a/ui/components/status-dropdown.ts +++ b/ui/components/status-dropdown.ts @@ -31,6 +31,27 @@ export class StatusDropdown { this.updateToolbarButton(); this.statusService.notifyStatusChanged(statuses); }); + + this.dropdownComponent.setOnRemoveStatusHandler(async (status, targetFile) => { + if (!targetFile) return; + + await this.statusService.handleStatusChange({ + files: targetFile, + statuses: status, + operation: 'remove', + showNotice: false, + afterChange: (updatedStatuses) => { + this.currentStatuses = updatedStatuses; + } + }); + }); + this.dropdownComponent.setOnSelectStatusHandler(async (status, targetFile) => { + await this.statusService.handleStatusChange({ + files: targetFile, + statuses: status + }); + }); + } @@ -130,17 +151,6 @@ export class StatusDropdown { files: [activeFile] }); } - - /** - * Update a file's status - */ - private async updateFileStatus(file: TFile, statuses: string[]): Promise { - await this.statusService.handleStatusChange({ - files: file, - statuses: statuses, - showNotice: false - }); - } /** * Get position from cursor or fallback From e9d5697db8cd8c84a426fe48f92c55fc59399944 Mon Sep 17 00:00:00 2001 From: Aleix Date: Thu, 15 May 2025 14:54:56 +0200 Subject: [PATCH 27/67] refactor: simplified --- ui/components/status-dropdown-component.ts | 77 ++++++++++------------ 1 file changed, 36 insertions(+), 41 deletions(-) diff --git a/ui/components/status-dropdown-component.ts b/ui/components/status-dropdown-component.ts index d111288..227f7fd 100644 --- a/ui/components/status-dropdown-component.ts +++ b/ui/components/status-dropdown-component.ts @@ -30,17 +30,6 @@ export class StatusDropdownComponent { this.clickOutsideHandler = this.handleClickOutside.bind(this); } - /** - * Updates the current statuses - */ - public updateStatuses(statuses: string[] | string): void { - this.currentStatuses = Array.isArray(statuses) ? [...statuses] : [statuses]; - - if (this.isOpen && this.dropdownElement) { - this.refreshDropdownContent(); - } - } - /** * Set the target file for status updates */ @@ -55,17 +44,6 @@ export class StatusDropdownComponent { this.onSelectStatusHandler = handler } - /** - * Updates settings reference - */ - public updateSettings(settings: NoteStatusSettings): void { - this.settings = settings; - - if (this.isOpen && this.dropdownElement) { - this.refreshDropdownContent(); - } - } - /** * Set callback for status changes */ @@ -79,6 +57,29 @@ export class StatusDropdownComponent { public getOnStatusChange(): (statuses: string[]) => void { return this.onStatusChange; } + + /** + * Updates the current statuses + */ + public updateStatuses(statuses: string[] | string): void { + this.currentStatuses = Array.isArray(statuses) ? [...statuses] : [statuses]; + + if (this.isOpen && this.dropdownElement) { + this.refreshDropdownContent(); + } + } + + /** + * Updates settings reference + */ + public updateSettings(settings: NoteStatusSettings): void { + this.settings = settings; + + if (this.isOpen && this.dropdownElement) { + this.refreshDropdownContent(); + } + } + /** * Toggle the dropdown visibility @@ -296,7 +297,7 @@ export class StatusDropdownComponent { setTimeout(async () => { if (this.targetFile) { await this.onRemoveStatusHandler(status, this.targetFile); - this.refreshDropdownContent(); // TODO: Pude es pot borrar + this.refreshDropdownContent(); // TODO: The propagation maybe is redunant } }, 150); }); @@ -382,24 +383,18 @@ export class StatusDropdownComponent { setIcon(checkIcon, 'check'); } - optionEl.addEventListener('click', () => this.handleStatusOptionClick(optionEl, status)); - } - - /** - * Handle click on a status option - */ - private handleStatusOptionClick(optionEl: HTMLElement, status: Status): void { - optionEl.addClass('note-status-option-selecting'); - - setTimeout(async () => { - if (this.targetFile) { - await this.onSelectStatusHandler(status.name, this.targetFile); - - if (!this.settings.useMultipleStatuses) { - this.close(); - } - } - }, 150); + optionEl.addEventListener('click', () => { + optionEl.addClass('note-status-option-selecting'); + setTimeout(async () => { + if (this.targetFile) { + await this.onSelectStatusHandler(status.name, this.targetFile); + + if (!this.settings.useMultipleStatuses) { + this.close(); + } + } + }, 150); + }); } /** From 1cae811a91cf7547be52a441123ebfe794f70aa4 Mon Sep 17 00:00:00 2001 From: Aleix Date: Fri, 16 May 2025 09:03:05 +0200 Subject: [PATCH 28/67] fix: multiple files modifcation --- services/status-service.ts | 1 - ui/components/status-dropdown-component.ts | 33 ++++++-- ui/components/status-dropdown.ts | 96 +++++++++------------- 3 files changed, 65 insertions(+), 65 deletions(-) diff --git a/services/status-service.ts b/services/status-service.ts index 59e3753..487737d 100644 --- a/services/status-service.ts +++ b/services/status-service.ts @@ -310,7 +310,6 @@ export class StatusService { showNotice?: boolean; afterChange?: (updatedStatuses: string[]) => void; }): Promise { - console.log("changing", options) const { files, statuses, diff --git a/ui/components/status-dropdown-component.ts b/ui/components/status-dropdown-component.ts index 227f7fd..d5e43f9 100644 --- a/ui/components/status-dropdown-component.ts +++ b/ui/components/status-dropdown-component.ts @@ -14,12 +14,13 @@ export class StatusDropdownComponent { private onStatusChange: (statuses: string[]) => void = () => {}; private animationDuration = 220; private targetFile: TFile | null = null; + private targetFiles: TFile[] = []; public isOpen = false; // Bind methods once in constructor private clickOutsideHandler: (e: MouseEvent) => void; private onRemoveStatusHandler: (status: string, targetFile?: TFile) => Promise; - private onSelectStatusHandler: (status: string, targetFile: TFile) => Promise; + private onSelectStatusHandler: (status: string, targetFile: TFile | TFile[]) => Promise; constructor(app: App, statusService: StatusService, settings: NoteStatusSettings) { @@ -35,6 +36,15 @@ export class StatusDropdownComponent { */ public setTargetFile(file: TFile | null): void { this.targetFile = file; + this.targetFiles = file ? [file] : []; + } + + /** + * Set multiple target files for status updates + */ + public setTargetFiles(files: TFile[]): void { + this.targetFiles = [...files]; + this.targetFile = files.length === 1 ? files[0] : null; } public setOnRemoveStatusHandler(handler: typeof this.onRemoveStatusHandler): void { @@ -212,6 +222,14 @@ export class StatusDropdownComponent { setIcon(iconContainer, 'tag'); titleEl.createSpan({ text: 'Note status', cls: 'note-status-popover-label' }); + + // If multiple files are selected, show count + if (this.targetFiles.length > 1) { + titleEl.createSpan({ + text: ` (${this.targetFiles.length} files)`, + cls: 'note-status-popover-count' + }); + } } /** @@ -386,11 +404,16 @@ export class StatusDropdownComponent { optionEl.addEventListener('click', () => { optionEl.addClass('note-status-option-selecting'); setTimeout(async () => { - if (this.targetFile) { - await this.onSelectStatusHandler(status.name, this.targetFile); + if (this.targetFiles.length > 0) { + // Use all target files or just the single target file + const filesToUpdate = this.targetFiles.length > 0 ? this.targetFiles : (this.targetFile ? [this.targetFile] : []); - if (!this.settings.useMultipleStatuses) { - this.close(); + if (filesToUpdate.length > 0) { + await this.onSelectStatusHandler(status.name, filesToUpdate); + + if (!this.settings.useMultipleStatuses) { + this.close(); + } } } }, 150); diff --git a/ui/components/status-dropdown.ts b/ui/components/status-dropdown.ts index b7941d2..43e1223 100644 --- a/ui/components/status-dropdown.ts +++ b/ui/components/status-dropdown.ts @@ -45,13 +45,35 @@ export class StatusDropdown { } }); }); + this.dropdownComponent.setOnSelectStatusHandler(async (status, targetFile) => { - await this.statusService.handleStatusChange({ + // Verificamos si estamos manejando múltiples archivos + const isMultipleFiles = Array.isArray(targetFile) && targetFile.length > 1; + + if (isMultipleFiles) { + const files = targetFile as TFile[]; + // Contamos cuántos archivos ya tienen este estado + const filesWithStatus = files.filter(file => + this.statusService.getFileStatuses(file).includes(status) + ); + + // Si TODOS tienen el estado, lo quitamos. Si es parcial o ninguno, lo añadimos + const operation = filesWithStatus.length === files.length ? 'remove' : 'add'; + + await this.statusService.handleStatusChange({ + files: targetFile, + statuses: status, + isMultipleSelection: true, + operation: operation + }); + } else { + // Para archivos individuales, mantenemos el comportamiento predeterminado + await this.statusService.handleStatusChange({ files: targetFile, statuses: status }); + } }); - } @@ -237,15 +259,20 @@ export class StatusDropdown { this.resetDropdownState(); const isSingleFile = files.length === 1; - const targetFile = isSingleFile ? files[0] : null; - this.dropdownComponent.setTargetFile(targetFile); - - const currentStatuses = targetFile ? - this.statusService.getFileStatuses(targetFile) : - this.findCommonStatuses(files); - this.dropdownComponent.updateStatuses(currentStatuses); - this.setupStatusChangeCallback(options, files, isSingleFile); + // Actualizamos cómo configuramos los archivos objetivo + if (isSingleFile) { + const targetFile = files[0]; + this.dropdownComponent.setTargetFile(targetFile); + const currentStatuses = this.statusService.getFileStatuses(targetFile); + this.dropdownComponent.updateStatuses(currentStatuses); + } else { + // Para múltiples archivos, configuramos toda la colección + this.dropdownComponent.setTargetFiles(files); + const commonStatuses = this.findCommonStatuses(files); + this.dropdownComponent.updateStatuses(commonStatuses); + } + this.positionAndOpenDropdown(options); } @@ -257,55 +284,6 @@ export class StatusDropdown { this.dropdownComponent.setOnStatusChange(() => {}); } - /** - * Setup the appropriate callback for status changes - */ - private setupStatusChangeCallback( - options: { - onStatusChange?: (statuses: string[]) => void; - mode?: 'replace' | 'add'; - }, - files: TFile[], - isSingleFile: boolean - ): void { - if (options.onStatusChange) { - this.dropdownComponent.setOnStatusChange(options.onStatusChange); - } else if (!isSingleFile) { - const filesForBatch = [...files]; - - this.dropdownComponent.setOnStatusChange(async (statuses) => { - if (statuses.length > 0) { - const toggledStatus = statuses[statuses.length - 1]; - - await this.statusService.handleStatusChange({ - files: filesForBatch, - statuses: toggledStatus, - isMultipleSelection: true, - afterChange: () => { - // Keep the batch update event for any listeners that need it - window.dispatchEvent(new CustomEvent('note-status:batch-update-complete', { - detail: { - status: toggledStatus, - fileCount: filesForBatch.length - } - })); - } - }); - } - }); - } else { - // For single file, just use handleStatusChange directly - this.dropdownComponent.setOnStatusChange(async (statuses) => { - if (statuses.length > 0 && files[0]) { - await this.statusService.handleStatusChange({ - files: files[0], - statuses: statuses - }); - } - }); - } - } - /** * Position and open the dropdown */ From b6db59c593b3ae2c5967685aa8d0f32e72dc52b4 Mon Sep 17 00:00:00 2001 From: Aleix Date: Fri, 16 May 2025 15:47:51 +0200 Subject: [PATCH 29/67] refactor: add components folder with statusBar --- components/statusBar/status-bar-controller.ts | 107 +++++++++++ components/statusBar/status-bar-view.ts | 107 +++++++++++ main.ts | 6 +- services/status-service.ts | 9 +- ui/components/status-bar.ts | 169 ------------------ 5 files changed, 221 insertions(+), 177 deletions(-) create mode 100644 components/statusBar/status-bar-controller.ts create mode 100644 components/statusBar/status-bar-view.ts delete mode 100644 ui/components/status-bar.ts diff --git a/components/statusBar/status-bar-controller.ts b/components/statusBar/status-bar-controller.ts new file mode 100644 index 0000000..72098bf --- /dev/null +++ b/components/statusBar/status-bar-controller.ts @@ -0,0 +1,107 @@ +import { NoteStatusSettings } from '../../models/types'; +import { StatusService } from '../../services/status-service'; +import { StatusBarView } from './status-bar-view'; + +/** + * Controller for the status bar + */ +export class StatusBarController { + private view: StatusBarView; + private settings: NoteStatusSettings; + private statusService: StatusService; + private currentStatuses: string[] = ['unknown']; + + constructor(statusBarEl: HTMLElement, settings: NoteStatusSettings, statusService: StatusService) { + this.view = new StatusBarView(statusBarEl); + this.settings = settings; + this.statusService = statusService; + + // Register right-click handler for force refresh + this.setupContextMenu(statusBarEl); + + this.update(['unknown']); + } + + /** + * Set up right-click context menu for force refresh + */ + private setupContextMenu(element: HTMLElement): void { + element.addEventListener('contextmenu', (e) => { + e.preventDefault(); + window.dispatchEvent(new CustomEvent('note-status:force-refresh')); + }); + } + + /** + * Update the status bar with new statuses + */ + public update(statuses: string[]): void { + this.currentStatuses = statuses; + this.render(); + } + + /** + * Render the status bar + */ + private render(): void { + this.view.reset(); + + if (!this.settings.showStatusBar) { + this.view.hide(); + return; + } + + if (!this.settings.useMultipleStatuses) { + this.renderStatuses([this.currentStatuses[0]]); + } else { + this.renderStatuses(this.currentStatuses); + } + + this.handleAutoHide(); + } + + /** + * Render statuses - handles both single and multiple status cases + */ + private renderStatuses(statuses: string[]): void { + const statusDetails = statuses.map(status => { + const statusObj = this.statusService.getAllStatuses().find(s => s.name === status); + return { + name: status, + icon: this.statusService.getStatusIcon(status), + tooltipText: statusObj?.description ? `${status} - ${statusObj.description}` : status + }; + }); + + this.view.renderStatuses(statusDetails); + } + + /** + * Handle auto-hide behavior + */ + private handleAutoHide(): void { + const onlyUnknown = this.currentStatuses.length === 1 && + this.currentStatuses[0] === 'unknown'; + + if (this.settings.autoHideStatusBar && onlyUnknown) { + this.view.hide(); + } else { + this.view.show(); + } + } + + /** + * Update settings reference + */ + public updateSettings(settings: NoteStatusSettings): void { + this.settings = settings; + this.render(); + } + + /** + * Clean up when plugin is unloaded + */ + public unload(): void { + this.view.destroy(); + } +} \ No newline at end of file diff --git a/components/statusBar/status-bar-view.ts b/components/statusBar/status-bar-view.ts new file mode 100644 index 0000000..8dc46f3 --- /dev/null +++ b/components/statusBar/status-bar-view.ts @@ -0,0 +1,107 @@ +import { setTooltip } from 'obsidian'; + +/** + * Renders the status bar UI + */ +export class StatusBarView { + private element: HTMLElement; + + constructor(element: HTMLElement) { + this.element = element; + this.element.addClass('note-status-bar'); + } + + /** + * Clears the element and resets CSS classes + */ + reset(): void { + this.element.empty(); + this.element.removeClass('left', 'hidden', 'auto-hide', 'visible'); + this.element.addClass('note-status-bar'); + } + + /** + * Hide the status bar + */ + hide(): void { + this.element.addClass('hidden'); + this.element.removeClass('visible'); + } + + /** + * Show the status bar + */ + show(): void { + this.element.removeClass('hidden'); + this.element.addClass('visible'); + } + + + renderStatuses(statuses: Array<{name: string, icon: string, tooltipText: string}>): void { + if (statuses.length === 1) { + this.renderSingleStatus(statuses[0].name, statuses[0].icon, statuses[0].tooltipText); + } else { + this.renderMultipleStatuses(statuses); + } + } + + /** + * Render a single status + */ + private renderSingleStatus(status: string, icon: string, tooltipText: string): void { + const statusText = this.element.createEl('span', { + text: `Status: ${status}`, + cls: `note-status-${status}` + }); + setTooltip(statusText, tooltipText); + + const statusIcon = this.element.createEl('span', { + text: icon, + cls: `note-status-icon status-${status}` + }); + setTooltip(statusIcon, tooltipText); + } + + /** + * Render multiple statuses + */ + private renderMultipleStatuses(statuses: Array<{name: string, icon: string, tooltipText: string}>): void { + this.element.createEl('span', { + text: 'Statuses: ', + cls: 'note-status-label' + }); + + const badgesContainer = this.element.createEl('span', { + cls: 'note-status-badges' + }); + + statuses.forEach(status => this.createStatusBadge(badgesContainer, status)); + } + + /** + * Create a status badge for multiple status display + */ + private createStatusBadge(container: HTMLElement, status: {name: string, icon: string, tooltipText: string}): void { + const badge = container.createEl('span', { + cls: `note-status-badge status-${status.name}` + }); + setTooltip(badge, status.tooltipText); + + badge.createEl('span', { + text: status.icon, + cls: 'note-status-badge-icon' + }); + + badge.createEl('span', { + text: status.name, + cls: 'note-status-badge-text' + }); + } + + /** + * Clean up the element + */ + destroy(): void { + this.element.empty(); + } +} \ No newline at end of file diff --git a/main.ts b/main.ts index 1aa13f6..4af3dcd 100644 --- a/main.ts +++ b/main.ts @@ -12,7 +12,6 @@ import { StatusService } from './services/status-service'; import { StyleService } from './services/style-service'; // UI Components -import { StatusBar } from './ui/components/status-bar'; import { StatusDropdown } from './ui/components/status-dropdown'; import { StatusPaneView } from './ui/components/status-pane-view'; import { ExplorerIntegration } from './ui/integrations/explorer-integration'; @@ -20,6 +19,7 @@ import { StatusContextMenu } from './ui/menus/status-context-menu'; // Settings import { NoteStatusSettingTab } from './settings/settings-tab'; +import { StatusBarController } from 'components/statusBar/status-bar-controller'; /** * Main plugin class for Note Status functionality @@ -28,7 +28,7 @@ export default class NoteStatus extends Plugin { settings: NoteStatusSettings; statusService: StatusService; styleService: StyleService; - statusBar: StatusBar; + statusBar: StatusBarController; statusDropdown: StatusDropdown; explorerIntegration: ExplorerIntegration; statusContextMenu: StatusContextMenu; @@ -105,7 +105,7 @@ export default class NoteStatus extends Plugin { */ private initializeUI(): void { // Init basic UI components - this.statusBar = new StatusBar(this.addStatusBarItem(), this.settings, this.statusService); + this.statusBar = new StatusBarController(this.addStatusBarItem(), this.settings, this.statusService); this.statusDropdown = new StatusDropdown(this.app, this.settings, this.statusService); // Initialize explorer integration with a slight delay to ensure UI elements are ready diff --git a/services/status-service.ts b/services/status-service.ts index 487737d..c63c1d3 100644 --- a/services/status-service.ts +++ b/services/status-service.ts @@ -383,11 +383,10 @@ export class StatusService { // General UI refresh window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); - // If it's the active file, ensure status bar and toolbar are updated - const activeFile = this.app.workspace.getActiveFile(); - if (activeFile && files.some(f => f.path === activeFile.path)) { - const statuses = this.getFileStatuses(activeFile); - this.notifyStatusChanged(statuses, activeFile) + // Notify status changes for each modified file + for (const file of files) { + const statuses = this.getFileStatuses(file); + this.notifyStatusChanged(statuses, file); } } diff --git a/ui/components/status-bar.ts b/ui/components/status-bar.ts deleted file mode 100644 index ef34278..0000000 --- a/ui/components/status-bar.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { setTooltip } from 'obsidian'; -import { NoteStatusSettings } from '../../models/types'; -import { StatusService } from '../../services/status-service'; - -/** - * Handles the status bar functionality - */ -export class StatusBar { - private statusBarEl: HTMLElement; - private settings: NoteStatusSettings; - private statusService: StatusService; - private currentStatuses: string[] = ['unknown']; - - constructor(statusBarEl: HTMLElement, settings: NoteStatusSettings, statusService: StatusService) { - this.statusBarEl = statusBarEl; - this.settings = settings; - this.statusService = statusService; - - this.statusBarEl.addClass('note-status-bar'); - this.setupContextMenu(); - this.update(['unknown']); - } - - /** - * Set up right-click context menu for force refresh - */ - private setupContextMenu(): void { - this.statusBarEl.addEventListener('contextmenu', (e) => { - e.preventDefault(); - window.dispatchEvent(new CustomEvent('note-status:force-refresh')); - }); - } - - /** - * Update the status bar with new statuses - */ - public update(statuses: string[]): void { - this.currentStatuses = statuses; - this.render(); - } - - /** - * Render the status bar based on current settings and statuses - */ - public render(): void { - this.statusBarEl.empty(); - this.resetClasses(); - - if (!this.settings.showStatusBar) { - this.statusBarEl.addClass('hidden'); - return; - } - - // Select display mode based on number of statuses and settings - if (this.currentStatuses.length === 1 || !this.settings.useMultipleStatuses) { - this.renderSingleStatus(); - } else { - this.renderMultipleStatuses(); - } - - this.handleAutoHide(); - } - - /** - * Reset CSS classes to their default state - */ - private resetClasses(): void { - this.statusBarEl.removeClass('left', 'hidden', 'auto-hide', 'visible'); - this.statusBarEl.addClass('note-status-bar'); - } - - /** - * Render a single status display - */ - private renderSingleStatus(): void { - const primaryStatus = this.currentStatuses[0]; - const statusObj = this.statusService.getAllStatuses().find(s => s.name === primaryStatus); - const tooltipText = statusObj?.description - ? `${primaryStatus} - ${statusObj.description}` - : primaryStatus; - - // Create status text - const statusText = this.statusBarEl.createEl('span', { - text: `Status: ${primaryStatus}`, - cls: `note-status-${primaryStatus}` - }); - setTooltip(statusText, tooltipText); - - // Create status icon - const statusIcon = this.statusBarEl.createEl('span', { - text: this.statusService.getStatusIcon(primaryStatus), - cls: `note-status-icon status-${primaryStatus}` - }); - setTooltip(statusIcon, tooltipText); - } - - /** - * Render multiple statuses display - */ - private renderMultipleStatuses(): void { - this.statusBarEl.createEl('span', { - text: 'Statuses: ', - cls: 'note-status-label' - }); - - const badgesContainer = this.statusBarEl.createEl('span', { - cls: 'note-status-badges' - }); - - this.currentStatuses.forEach(status => - this.createStatusBadge(badgesContainer, status)); - } - - /** - * Create a status badge for multiple status display - */ - private createStatusBadge(container: HTMLElement, status: string): void { - const statusObj = this.statusService.getAllStatuses().find(s => s.name === status); - const tooltipText = statusObj?.description - ? `${status} - ${statusObj.description}` - : status; - - const badge = container.createEl('span', { - cls: `note-status-badge status-${status}` - }); - setTooltip(badge, tooltipText); - - badge.createEl('span', { - text: this.statusService.getStatusIcon(status), - cls: 'note-status-badge-icon' - }); - - badge.createEl('span', { - text: status, - cls: 'note-status-badge-text' - }); - } - - /** - * Handle auto-hide behavior - */ - private handleAutoHide(): void { - const onlyUnknown = this.currentStatuses.length === 1 && - this.currentStatuses[0] === 'unknown'; - - if (this.settings.autoHideStatusBar && onlyUnknown) { - this.statusBarEl.addClass('hidden'); - this.statusBarEl.removeClass('visible'); - } else { - this.statusBarEl.removeClass('hidden'); - this.statusBarEl.addClass('visible'); - } - } - - /** - * Update settings reference - */ - public updateSettings(settings: NoteStatusSettings): void { - this.settings = settings; - this.render(); - } - - /** - * Clean up when plugin is unloaded - */ - public unload(): void { - this.statusBarEl.empty(); - } -} \ No newline at end of file From 70383d0445b37787ab2b13e61485316efde569e5 Mon Sep 17 00:00:00 2001 From: Aleix Date: Mon, 19 May 2025 08:50:13 +0200 Subject: [PATCH 30/67] refactor: dropdown kiss --- components/status-dropdown/dropdown-events.ts | 29 + .../status-dropdown/dropdown-manager.ts | 114 ++-- .../status-dropdown/dropdown-position.ts | 105 ++++ components/status-dropdown/dropdown-render.ts | 317 +++++++++++ components/status-dropdown/dropdown-ui.ts | 237 ++++++++ components/status-dropdown/index.ts | 9 + components/status-dropdown/types.ts | 36 ++ main.ts | 2 +- ui/components/status-dropdown-component.ts | 528 ------------------ 9 files changed, 786 insertions(+), 591 deletions(-) create mode 100644 components/status-dropdown/dropdown-events.ts rename ui/components/status-dropdown.ts => components/status-dropdown/dropdown-manager.ts (77%) create mode 100644 components/status-dropdown/dropdown-position.ts create mode 100644 components/status-dropdown/dropdown-render.ts create mode 100644 components/status-dropdown/dropdown-ui.ts create mode 100644 components/status-dropdown/index.ts create mode 100644 components/status-dropdown/types.ts delete mode 100644 ui/components/status-dropdown-component.ts diff --git a/components/status-dropdown/dropdown-events.ts b/components/status-dropdown/dropdown-events.ts new file mode 100644 index 0000000..d0468f4 --- /dev/null +++ b/components/status-dropdown/dropdown-events.ts @@ -0,0 +1,29 @@ +/** + * Event handlers setup and cleanup for the dropdown + */ + +/** + * Setup event handlers for the dropdown + */ +export function setupDropdownEvents(options: { + clickOutsideHandler: (e: MouseEvent) => void, + escapeKeyHandler: (e: KeyboardEvent) => void +}): void { + const { clickOutsideHandler, escapeKeyHandler } = options; + + document.addEventListener('click', clickOutsideHandler); + document.addEventListener('keydown', escapeKeyHandler); +} + +/** + * Remove dropdown event handlers + */ +export function removeDropdownEvents(options: { + clickOutsideHandler: (e: MouseEvent) => void, + escapeKeyHandler: (e: KeyboardEvent) => void +}): void { + const { clickOutsideHandler, escapeKeyHandler } = options; + + document.removeEventListener('click', clickOutsideHandler); + document.removeEventListener('keydown', escapeKeyHandler); +} \ No newline at end of file diff --git a/ui/components/status-dropdown.ts b/components/status-dropdown/dropdown-manager.ts similarity index 77% rename from ui/components/status-dropdown.ts rename to components/status-dropdown/dropdown-manager.ts index 43e1223..835d0d3 100644 --- a/ui/components/status-dropdown.ts +++ b/components/status-dropdown/dropdown-manager.ts @@ -1,38 +1,44 @@ import { MarkdownView, Editor, Notice, TFile } from 'obsidian'; -import { NoteStatusSettings } from '../../models/types'; -import { StatusService } from '../../services/status-service'; -import { StatusDropdownComponent } from './status-dropdown-component'; +import { DropdownUI } from './dropdown-ui'; +import { DropdownOptions, DropdownDependencies } from './types'; +import { createDummyTarget } from './dropdown-position'; +import { StatusService } from 'services/status-service'; +import { NoteStatusSettings } from 'models/types'; /** - * Manages status dropdown UI and interactions + * High-level manager for status dropdown interactions */ -export class StatusDropdown { +export class DropdownManager { private app: any; private settings: NoteStatusSettings; private statusService: StatusService; private currentStatuses: string[] = ['unknown']; private toolbarButton?: HTMLElement; - private dropdownComponent: StatusDropdownComponent; + private dropdownUI: DropdownUI; constructor(app: any, settings: NoteStatusSettings, statusService: StatusService) { this.app = app; this.settings = settings; this.statusService = statusService; - this.dropdownComponent = new StatusDropdownComponent(app, statusService, settings); + + const deps: DropdownDependencies = { app, settings, statusService }; + this.dropdownUI = new DropdownUI(deps); + this.setupDropdownCallbacks(); + this.setupCustomEvents(); } /** - * Set up the dropdown callbacks + * Set up dropdown callbacks */ private setupDropdownCallbacks(): void { - this.dropdownComponent.setOnStatusChange((statuses) => { + this.dropdownUI.setOnStatusChange((statuses) => { this.currentStatuses = [...statuses]; this.updateToolbarButton(); this.statusService.notifyStatusChanged(statuses); }); - this.dropdownComponent.setOnRemoveStatusHandler(async (status, targetFile) => { + this.dropdownUI.setOnRemoveStatusHandler(async (status, targetFile) => { if (!targetFile) return; await this.statusService.handleStatusChange({ @@ -46,18 +52,18 @@ export class StatusDropdown { }); }); - this.dropdownComponent.setOnSelectStatusHandler(async (status, targetFile) => { - // Verificamos si estamos manejando múltiples archivos + this.dropdownUI.setOnSelectStatusHandler(async (status, targetFile) => { + // Check if handling multiple files const isMultipleFiles = Array.isArray(targetFile) && targetFile.length > 1; if (isMultipleFiles) { const files = targetFile as TFile[]; - // Contamos cuántos archivos ya tienen este estado + // Count how many files already have this status const filesWithStatus = files.filter(file => this.statusService.getFileStatuses(file).includes(status) ); - // Si TODOS tienen el estado, lo quitamos. Si es parcial o ninguno, lo añadimos + // If ALL have the status, remove it. Otherwise, add it const operation = filesWithStatus.length === files.length ? 'remove' : 'add'; await this.statusService.handleStatusChange({ @@ -67,7 +73,7 @@ export class StatusDropdown { operation: operation }); } else { - // Para archivos individuales, mantenemos el comportamiento predeterminado + // For individual files, maintain default behavior await this.statusService.handleStatusChange({ files: targetFile, statuses: status @@ -76,6 +82,16 @@ export class StatusDropdown { }); } + /** + * Set up custom events + */ + private setupCustomEvents(): void { + window.addEventListener('note-status:dropdown-close', () => { + if (this.dropdownUI.isOpen) { + this.dropdownUI.close(); + } + }); + } /** * Updates the toolbar button appearance @@ -147,7 +163,7 @@ export class StatusDropdown { [...currentStatuses] : [currentStatuses]; this.updateToolbarButton(); - this.dropdownComponent.updateStatuses(this.currentStatuses); + this.dropdownUI.updateStatuses(this.currentStatuses); } /** @@ -156,7 +172,7 @@ export class StatusDropdown { public updateSettings(settings: NoteStatusSettings): void { this.settings = settings; this.updateToolbarButton(); - this.dropdownComponent.updateSettings(settings); + this.dropdownUI.updateSettings(settings); } /** @@ -210,28 +226,22 @@ export class StatusDropdown { * Remove dropdown when plugin is unloaded */ public unload(): void { - this.dropdownComponent.dispose(); + this.dropdownUI.dispose(); if (this.toolbarButton) { this.toolbarButton.remove(); this.toolbarButton = undefined; } + + window.removeEventListener('note-status:dropdown-close', () => {}); } /** * Universal function to open the status dropdown */ - public openStatusDropdown(options: { - target?: HTMLElement; - position?: { x: number, y: number }; - files?: TFile[]; - editor?: Editor; - view?: MarkdownView; - mode?: 'replace' | 'add'; - onStatusChange?: (statuses: string[]) => void; - }): void { - if (this.dropdownComponent.isOpen) { - this.dropdownComponent.close(); + public openStatusDropdown(options: DropdownOptions): void { + if (this.dropdownUI.isOpen) { + this.dropdownUI.close(); setTimeout(() => this._openStatusDropdown(options), 50); } else { this._openStatusDropdown(options); @@ -241,15 +251,7 @@ export class StatusDropdown { /** * Internal method to open dropdown */ - private _openStatusDropdown(options: { - target?: HTMLElement; - position?: { x: number, y: number }; - files?: TFile[]; - editor?: Editor; - view?: MarkdownView; - mode?: 'replace' | 'add'; - onStatusChange?: (statuses: string[]) => void; - }): void { + private _openStatusDropdown(options: DropdownOptions): void { const files = options.files || [this.app.workspace.getActiveFile()].filter(Boolean); if (!files.length) { new Notice('No files selected'); @@ -260,17 +262,17 @@ export class StatusDropdown { const isSingleFile = files.length === 1; - // Actualizamos cómo configuramos los archivos objetivo + // Set up target files appropriately if (isSingleFile) { const targetFile = files[0]; - this.dropdownComponent.setTargetFile(targetFile); + this.dropdownUI.setTargetFile(targetFile); const currentStatuses = this.statusService.getFileStatuses(targetFile); - this.dropdownComponent.updateStatuses(currentStatuses); + this.dropdownUI.updateStatuses(currentStatuses); } else { - // Para múltiples archivos, configuramos toda la colección - this.dropdownComponent.setTargetFiles(files); + // For multiple files, set the whole collection + this.dropdownUI.setTargetFiles(files); const commonStatuses = this.findCommonStatuses(files); - this.dropdownComponent.updateStatuses(commonStatuses); + this.dropdownUI.updateStatuses(commonStatuses); } this.positionAndOpenDropdown(options); @@ -280,8 +282,8 @@ export class StatusDropdown { * Reset dropdown state before opening */ private resetDropdownState(): void { - this.dropdownComponent.setTargetFile(null); - this.dropdownComponent.setOnStatusChange(() => {}); + this.dropdownUI.setTargetFile(null); + this.dropdownUI.setOnStatusChange(() => {}); } /** @@ -301,10 +303,10 @@ export class StatusDropdown { if (options.target) { if (options.position) { - this.dropdownComponent.open(options.target, options.position); + this.dropdownUI.open(options.target, options.position); } else { const rect = options.target.getBoundingClientRect(); - this.dropdownComponent.open(options.target, { + this.dropdownUI.open(options.target, { x: rect.left, y: rect.bottom + 5 }); @@ -327,8 +329,8 @@ export class StatusDropdown { * Open dropdown at a specific position using dummy target */ private openWithPosition(position: { x: number, y: number }): void { - const dummyTarget = this.createDummyTarget(position); - this.dropdownComponent.open(dummyTarget, position); + const dummyTarget = createDummyTarget(position); + this.dropdownUI.open(dummyTarget, position); setTimeout(() => { if (dummyTarget.parentNode) { @@ -337,18 +339,6 @@ export class StatusDropdown { }, 100); } - /** - * Create a dummy target element for positioning - */ - private createDummyTarget(position: { x: number, y: number }): HTMLElement { - const dummyTarget = document.createElement('div'); - dummyTarget.addClass('note-status-dummy-target'); - dummyTarget.style.setProperty('--pos-x-px', `${position.x}px`); - dummyTarget.style.setProperty('--pos-y-px', `${position.y}px`); - document.body.appendChild(dummyTarget); - return dummyTarget; - } - /** * Find common statuses across multiple files */ diff --git a/components/status-dropdown/dropdown-position.ts b/components/status-dropdown/dropdown-position.ts new file mode 100644 index 0000000..1601581 --- /dev/null +++ b/components/status-dropdown/dropdown-position.ts @@ -0,0 +1,105 @@ +/** + * Dropdown positioning utilities + */ + +/** + * Position the dropdown + */ +export function positionDropdown(options: { + dropdownElement: HTMLElement, + targetEl: HTMLElement, + position?: { x: number, y: number } +}): void { + const { dropdownElement, targetEl, position } = options; + + if (position) { + positionAt(dropdownElement, position.x, position.y); + } else { + positionRelativeTo(dropdownElement, targetEl); + } +} + +/** + * Position the dropdown at specific coordinates + */ +function positionAt(dropdownElement: HTMLElement, x: number, y: number): void { + dropdownElement.addClass('note-status-popover-fixed'); + dropdownElement.style.setProperty('--pos-x-px', `${x}px`); + dropdownElement.style.setProperty('--pos-y-px', `${y}px`); + + setTimeout(() => adjustPositionToViewport(dropdownElement), 0); +} + +/** + * Adjust the dropdown position to ensure it's visible in the viewport + */ +function adjustPositionToViewport(dropdownElement: HTMLElement): void { + const rect = dropdownElement.getBoundingClientRect(); + + if (rect.right > window.innerWidth) { + dropdownElement.addClass('note-status-popover-right'); + dropdownElement.style.setProperty('--right-offset-px', '10px'); + } else { + dropdownElement.removeClass('note-status-popover-right'); + } + + if (rect.bottom > window.innerHeight) { + dropdownElement.addClass('note-status-popover-bottom'); + dropdownElement.style.setProperty('--bottom-offset-px', '10px'); + } else { + dropdownElement.removeClass('note-status-popover-bottom'); + } + + const maxHeight = window.innerHeight - rect.top - 20; + dropdownElement.style.setProperty('--max-height-px', `${maxHeight}px`); +} + +/** + * Position the dropdown relative to a target element + */ +function positionRelativeTo(dropdownElement: HTMLElement, targetEl: HTMLElement): void { + dropdownElement.addClass('note-status-popover-fixed'); + + const targetRect = targetEl.getBoundingClientRect(); + + dropdownElement.style.setProperty('--pos-y-px', `${targetRect.bottom + 5}px`); + dropdownElement.style.setProperty('--pos-x-px', `${targetRect.left}px`); + + setTimeout(() => adjustRelativePosition(dropdownElement, targetRect), 0); +} + +/** + * Adjust position when positioned relative to an element + */ +function adjustRelativePosition(dropdownElement: HTMLElement, targetRect: DOMRect): void { + const rect = dropdownElement.getBoundingClientRect(); + + if (rect.right > window.innerWidth) { + dropdownElement.addClass('note-status-popover-right'); + dropdownElement.style.setProperty('--right-offset-px', `${window.innerWidth - targetRect.right}px`); + } else { + dropdownElement.removeClass('note-status-popover-right'); + } + + if (rect.bottom > window.innerHeight) { + dropdownElement.addClass('note-status-popover-bottom'); + dropdownElement.style.setProperty('--bottom-offset-px', `${window.innerHeight - targetRect.top + 5}px`); + } else { + dropdownElement.removeClass('note-status-popover-bottom'); + } + + const maxHeight = window.innerHeight - rect.top - 20; + dropdownElement.style.setProperty('--max-height-px', `${maxHeight}px`); +} + +/** + * Create a dummy target element for positioning + */ +export function createDummyTarget(position: { x: number, y: number }): HTMLElement { + const dummyTarget = document.createElement('div'); + dummyTarget.addClass('note-status-dummy-target'); + dummyTarget.style.setProperty('--pos-x-px', `${position.x}px`); + dummyTarget.style.setProperty('--pos-y-px', `${position.y}px`); + document.body.appendChild(dummyTarget); + return dummyTarget; +} \ No newline at end of file diff --git a/components/status-dropdown/dropdown-render.ts b/components/status-dropdown/dropdown-render.ts new file mode 100644 index 0000000..e21e776 --- /dev/null +++ b/components/status-dropdown/dropdown-render.ts @@ -0,0 +1,317 @@ +import { setIcon, TFile, setTooltip } from 'obsidian'; +import { StatusRemoveHandler, StatusSelectHandler } from './types'; +import { NoteStatusSettings, Status } from 'models/types'; +import { StatusService } from 'services/status-service'; + +/** + * Render the dropdown content + */ +export function renderDropdownContent(options: { + dropdownElement: HTMLElement, + settings: NoteStatusSettings, + statusService: StatusService, + currentStatuses: string[], + targetFile: TFile | null, + targetFiles: TFile[], + onRemoveStatus: StatusRemoveHandler, + onSelectStatus: StatusSelectHandler +}): void { + const { + dropdownElement, + settings, + statusService, + currentStatuses, + targetFile, + targetFiles, + onRemoveStatus, + onSelectStatus + } = options; + + dropdownElement.empty(); + + // Create UI sections + createHeader(dropdownElement, targetFiles); + createStatusChips(dropdownElement, currentStatuses, statusService, targetFile, onRemoveStatus); + const searchInput = createSearchFilter(dropdownElement); + + // Create status options container + const statusOptionsContainer = dropdownElement.createDiv({ + cls: 'note-status-options-container' + }); + + // Get all available statuses (excluding 'unknown') + const allStatuses = statusService.getAllStatuses() + .filter(status => status.name !== 'unknown'); + + // Function to populate options with filtering + const populateOptions = (filter = '') => { + populateStatusOptions({ + container: statusOptionsContainer, + statuses: allStatuses, + currentStatuses, + settings, + targetFiles, + onSelectStatus, + filter + }); + }; + + // Initial population + populateOptions(); + + // Add search functionality + searchInput.addEventListener('input', () => { + populateOptions(searchInput.value); + }); + + // Focus search input after a short delay + setTimeout(() => searchInput.focus(), 50); +} + +/** + * Create the dropdown header + */ +function createHeader(dropdownElement: HTMLElement, targetFiles: TFile[]): void { + const headerEl = dropdownElement.createDiv({ cls: 'note-status-popover-header' }); + const titleEl = headerEl.createDiv({ cls: 'note-status-popover-title' }); + + const iconContainer = titleEl.createDiv({ cls: 'note-status-popover-icon' }); + setIcon(iconContainer, 'tag'); + + titleEl.createSpan({ text: 'Note status', cls: 'note-status-popover-label' }); + + // If multiple files are selected, show count + if (targetFiles.length > 1) { + titleEl.createSpan({ + text: ` (${targetFiles.length} files)`, + cls: 'note-status-popover-count' + }); + } +} + +/** + * Create the status chips section + */ +function createStatusChips( + dropdownElement: HTMLElement, + currentStatuses: string[], + statusService: StatusService, + targetFile: TFile | null, + onRemoveStatus: StatusRemoveHandler +): void { + const chipsContainer = dropdownElement.createDiv({ cls: 'note-status-popover-chips' }); + + const hasNoValidStatus = currentStatuses.length === 0 || + (currentStatuses.length === 1 && currentStatuses[0] === 'unknown'); + + if (hasNoValidStatus) { + chipsContainer.createDiv({ + cls: 'note-status-empty-indicator', + text: 'No status assigned' + }); + } else { + createStatusChipElements(chipsContainer, currentStatuses, statusService, targetFile, onRemoveStatus); + } +} + +/** + * Create chips for all current statuses + */ +function createStatusChipElements( + container: HTMLElement, + currentStatuses: string[], + statusService: StatusService, + targetFile: TFile | null, + onRemoveStatus: StatusRemoveHandler +): void { + currentStatuses.forEach(status => { + if (status === 'unknown') return; + + const statusObj = statusService.getAllStatuses().find(s => s.name === status); + if (!statusObj) return; + + createSingleStatusChip(container, status, statusObj, targetFile, onRemoveStatus); + }); +} + +/** + * Create a single status chip + */ +function createSingleStatusChip( + container: HTMLElement, + status: string, + statusObj: Status, + targetFile: TFile | null, + onRemoveStatus: StatusRemoveHandler +): void { + const chipEl = container.createDiv({ + cls: `note-status-chip status-${status}` + }); + + // Status icon and name + chipEl.createSpan({ + text: statusObj.icon, + cls: 'note-status-chip-icon' + }); + + chipEl.createSpan({ + text: statusObj.name, + cls: 'note-status-chip-text' + }); + + addRemoveButton(chipEl, status, statusObj, targetFile, onRemoveStatus); +} + +/** + * Add a remove button to a status chip + */ +function addRemoveButton( + chipEl: HTMLElement, + status: string, + statusObj: Status, + targetFile: TFile | null, + onRemoveStatus: StatusRemoveHandler +): void { + const tooltipValue = statusObj.description ? `${status} - ${statusObj.description}`: status; + + setTooltip(chipEl, tooltipValue); + + const removeBtn = chipEl.createDiv({ + cls: 'note-status-chip-remove', + attr: { + 'aria-label': `Remove ${status} status`, + 'title': `Remove ${status} status` + } + }); + setIcon(removeBtn, 'x'); + + removeBtn.addEventListener('click', async (e) => { + e.stopPropagation(); + + chipEl.addClass('note-status-chip-removing'); + + setTimeout(async () => { + if (targetFile) { + await onRemoveStatus(status, targetFile); + } + }, 150); + }); +} + +/** + * Create the search filter input + */ +function createSearchFilter(dropdownElement: HTMLElement): HTMLInputElement { + const searchContainer = dropdownElement.createDiv({ cls: 'note-status-popover-search' }); + + return searchContainer.createEl('input', { + type: 'text', + placeholder: 'Filter statuses...', + cls: 'note-status-popover-search-input' + }); +} + +/** + * Populate status options with optional filtering + */ +function populateStatusOptions(options: { + container: HTMLElement, + statuses: Status[], + currentStatuses: string[], + settings: NoteStatusSettings, + targetFiles: TFile[], + onSelectStatus: StatusSelectHandler, + filter?: string +}): void { + const { + container, + statuses, + currentStatuses, + settings, + targetFiles, + onSelectStatus, + filter = '' + } = options; + + container.empty(); + + const filteredStatuses = filter ? + statuses.filter(status => + status.name.toLowerCase().includes(filter.toLowerCase()) || + status.icon.includes(filter) + ) : + statuses; + + if (filteredStatuses.length === 0) { + container.createDiv({ + cls: 'note-status-empty-options', + text: filter ? `No statuses match "${filter}"` : 'No statuses found' + }); + return; + } + + filteredStatuses.forEach(status => { + createStatusOption({ + container, + status, + isSelected: currentStatuses.includes(status.name), + settings, + targetFiles, + onSelectStatus + }); + }); +} + +/** + * Create a single status option element + */ +function createStatusOption(options: { + container: HTMLElement, + status: Status, + isSelected: boolean, + settings: NoteStatusSettings, + targetFiles: TFile[], + onSelectStatus: StatusSelectHandler +}): void { + const { container, status, isSelected, settings, targetFiles, onSelectStatus } = options; + + const optionEl = container.createDiv({ + cls: `note-status-option ${isSelected ? 'is-selected' : ''} status-${status.name}` + }); + + // Status icon and name + optionEl.createSpan({ + text: status.icon, + cls: 'note-status-option-icon' + }); + + optionEl.createSpan({ + text: status.name, + cls: 'note-status-option-text' + }); + + // Add tooltip if description available + if (status.description) { + setTooltip(optionEl, `${status.name} - ${status.description}`); + } + + // Check icon for selected status + if (isSelected) { + const checkIcon = optionEl.createDiv({ cls: 'note-status-option-check' }); + setIcon(checkIcon, 'check'); + } + + optionEl.addEventListener('click', () => { + optionEl.addClass('note-status-option-selecting'); + setTimeout(async () => { + if (targetFiles.length > 0) { + await onSelectStatus(status.name, targetFiles); + + if (!settings.useMultipleStatuses) { + // Close signal - The manager component will handle this + window.dispatchEvent(new CustomEvent('note-status:dropdown-close')); + } + } + }, 150); + }); +} \ No newline at end of file diff --git a/components/status-dropdown/dropdown-ui.ts b/components/status-dropdown/dropdown-ui.ts new file mode 100644 index 0000000..b44c95a --- /dev/null +++ b/components/status-dropdown/dropdown-ui.ts @@ -0,0 +1,237 @@ +import { App, TFile } from 'obsidian'; +import { DropdownDependencies, StatusRemoveHandler, StatusSelectHandler } from './types'; +import { positionDropdown } from './dropdown-position'; +import { renderDropdownContent } from './dropdown-render'; +import { setupDropdownEvents } from './dropdown-events'; +import { StatusService } from 'services/status-service'; +import { NoteStatusSettings } from 'models/types'; + +/** + * Core UI component for the status dropdown + */ +export class DropdownUI { + private app: App; + private statusService: StatusService; + private settings: NoteStatusSettings; + + private dropdownElement: HTMLElement | null = null; + private currentStatuses: string[] = ['unknown']; + private targetFile: TFile | null = null; + private targetFiles: TFile[] = []; + private animationDuration = 220; + + public isOpen = false; + private onStatusChange: (statuses: string[]) => void = () => {}; + private onRemoveStatus: StatusRemoveHandler = async () => {}; + private onSelectStatus: StatusSelectHandler = async () => {}; + + // Event handlers + private clickOutsideHandler: (e: MouseEvent) => void; + private escapeKeyHandler: (e: KeyboardEvent) => void; + + constructor({ app, settings, statusService }: DropdownDependencies) { + this.app = app; + this.statusService = statusService; + this.settings = settings; + + // Bind methods + this.clickOutsideHandler = this.handleClickOutside.bind(this); + this.escapeKeyHandler = this.handleEscapeKey.bind(this); + } + + /** + * Set the target file for status updates + */ + public setTargetFile(file: TFile | null): void { + this.targetFile = file; + this.targetFiles = file ? [file] : []; + } + + /** + * Set multiple target files for status updates + */ + public setTargetFiles(files: TFile[]): void { + this.targetFiles = [...files]; + this.targetFile = files.length === 1 ? files[0] : null; + } + + /** + * Register handler for removing a status + */ + public setOnRemoveStatusHandler(handler: StatusRemoveHandler): void { + this.onRemoveStatus = handler; + } + + /** + * Register handler for selecting a status + */ + public setOnSelectStatusHandler(handler: StatusSelectHandler): void { + this.onSelectStatus = handler; + } + + /** + * Set callback for status changes + */ + public setOnStatusChange(callback: (statuses: string[]) => void): void { + this.onStatusChange = callback; + } + + /** + * Get the current status change callback + */ + public getOnStatusChange(): (statuses: string[]) => void { + return this.onStatusChange; + } + + /** + * Updates the current statuses + */ + public updateStatuses(statuses: string[] | string): void { + this.currentStatuses = Array.isArray(statuses) ? [...statuses] : [statuses]; + + if (this.isOpen && this.dropdownElement) { + this.refreshDropdownContent(); + } + } + + /** + * Updates settings reference + */ + public updateSettings(settings: NoteStatusSettings): void { + this.settings = settings; + + if (this.isOpen && this.dropdownElement) { + this.refreshDropdownContent(); + } + } + + /** + * Toggle the dropdown visibility + */ + public toggle(targetEl: HTMLElement, position?: { x: number, y: number }): void { + if (this.isOpen) { + this.close(); + setTimeout(() => { + if (!this.isOpen && !this.dropdownElement) { + this.open(targetEl, position); + } + }, 50); + } else { + this.open(targetEl, position); + } + } + + /** + * Open the dropdown + */ + public open(targetEl: HTMLElement, position?: { x: number, y: number }): void { + if (this.isOpen || this.dropdownElement) { + this.close(); + setTimeout(() => this.actuallyOpen(targetEl, position), 10); + return; + } + + this.actuallyOpen(targetEl, position); + } + + /** + * Actually open the dropdown (internal method) + */ + private actuallyOpen(targetEl: HTMLElement, position?: { x: number, y: number }): void { + this.isOpen = true; + + // Create dropdown element + this.createDropdownElement(); + this.refreshDropdownContent(); + + // Position the dropdown + positionDropdown({ + dropdownElement: this.dropdownElement!, + targetEl, + position + }); + + this.dropdownElement?.addClass('note-status-popover-animate-in'); + + // Add event listeners with slight delay to prevent immediate triggering + setTimeout(() => { + setupDropdownEvents({ + clickOutsideHandler: this.clickOutsideHandler, + escapeKeyHandler: this.escapeKeyHandler + }); + }, 10); + } + + /** + * Create the dropdown element + */ + private createDropdownElement(): void { + this.dropdownElement = document.createElement('div'); + this.dropdownElement.addClass('note-status-popover', 'note-status-unified-dropdown'); + document.body.appendChild(this.dropdownElement); + } + + /** + * Close the dropdown + */ + public close(): void { + if (!this.dropdownElement || !this.isOpen) return; + + this.dropdownElement.addClass('note-status-popover-animate-out'); + + document.removeEventListener('click', this.clickOutsideHandler); + document.removeEventListener('keydown', this.escapeKeyHandler); + + this.isOpen = false; + + setTimeout(() => { + if (this.dropdownElement) { + this.dropdownElement.remove(); + this.dropdownElement = null; + } + }, this.animationDuration); + } + + /** + * Refresh the dropdown content + */ + private refreshDropdownContent(): void { + if (!this.dropdownElement) return; + + renderDropdownContent({ + dropdownElement: this.dropdownElement, + settings: this.settings, + statusService: this.statusService, + currentStatuses: this.currentStatuses, + targetFile: this.targetFile, + targetFiles: this.targetFiles, + onRemoveStatus: this.onRemoveStatus, + onSelectStatus: this.onSelectStatus + }); + } + + /** + * Handle click outside the dropdown + */ + private handleClickOutside(e: MouseEvent): void { + if (this.dropdownElement && !this.dropdownElement.contains(e.target as Node)) { + this.close(); + } + } + + /** + * Handle escape key to close dropdown + */ + private handleEscapeKey(e: KeyboardEvent): void { + if (e.key === 'Escape') { + this.close(); + } + } + + /** + * Dispose of resources + */ + public dispose(): void { + this.close(); + } +} \ No newline at end of file diff --git a/components/status-dropdown/index.ts b/components/status-dropdown/index.ts new file mode 100644 index 0000000..46ad9bc --- /dev/null +++ b/components/status-dropdown/index.ts @@ -0,0 +1,9 @@ +import { DropdownManager } from './dropdown-manager'; +import { DropdownOptions } from './types'; + +// Re-export for public use +export { DropdownManager as StatusDropdown }; +export type { DropdownOptions }; + +// Export default to maintain compatibility with existing code +export default DropdownManager; \ No newline at end of file diff --git a/components/status-dropdown/types.ts b/components/status-dropdown/types.ts new file mode 100644 index 0000000..b412ffe --- /dev/null +++ b/components/status-dropdown/types.ts @@ -0,0 +1,36 @@ +// # Tipos comunes +import { NoteStatusSettings } from 'models/types'; +import { App, TFile, Editor, MarkdownView } from 'obsidian'; +import { StatusService } from 'services/status-service'; + +/** + * Options for opening status dropdown + */ +export interface DropdownOptions { + target?: HTMLElement; + position?: { x: number, y: number }; + files?: TFile[]; + editor?: Editor; + view?: MarkdownView; + mode?: 'replace' | 'add' | 'remove' | 'toggle'; + onStatusChange?: (statuses: string[]) => void; +} + +/** + * Status removal handler function type + */ +export type StatusRemoveHandler = (status: string, targetFile?: TFile) => Promise; + +/** + * Status selection handler function type + */ +export type StatusSelectHandler = (status: string, targetFile: TFile | TFile[]) => Promise; + +/** + * Common dependencies for dropdown components + */ +export interface DropdownDependencies { + app: App; + settings: NoteStatusSettings; + statusService: StatusService; +} \ No newline at end of file diff --git a/main.ts b/main.ts index 4af3dcd..65c92ee 100644 --- a/main.ts +++ b/main.ts @@ -12,7 +12,6 @@ import { StatusService } from './services/status-service'; import { StyleService } from './services/style-service'; // UI Components -import { StatusDropdown } from './ui/components/status-dropdown'; import { StatusPaneView } from './ui/components/status-pane-view'; import { ExplorerIntegration } from './ui/integrations/explorer-integration'; import { StatusContextMenu } from './ui/menus/status-context-menu'; @@ -20,6 +19,7 @@ import { StatusContextMenu } from './ui/menus/status-context-menu'; // Settings import { NoteStatusSettingTab } from './settings/settings-tab'; import { StatusBarController } from 'components/statusBar/status-bar-controller'; +import { StatusDropdown } from 'components/status-dropdown'; /** * Main plugin class for Note Status functionality diff --git a/ui/components/status-dropdown-component.ts b/ui/components/status-dropdown-component.ts deleted file mode 100644 index d5e43f9..0000000 --- a/ui/components/status-dropdown-component.ts +++ /dev/null @@ -1,528 +0,0 @@ -import { App, setIcon, TFile, setTooltip } from 'obsidian'; -import { NoteStatusSettings, Status } from '../../models/types'; -import { StatusService } from '../../services/status-service'; - -/** - * Unified dropdown component for status selection - */ -export class StatusDropdownComponent { - private app: App; - private statusService: StatusService; - private settings: NoteStatusSettings; - private dropdownElement: HTMLElement | null = null; - private currentStatuses: string[] = ['unknown']; - private onStatusChange: (statuses: string[]) => void = () => {}; - private animationDuration = 220; - private targetFile: TFile | null = null; - private targetFiles: TFile[] = []; - public isOpen = false; - - // Bind methods once in constructor - private clickOutsideHandler: (e: MouseEvent) => void; - private onRemoveStatusHandler: (status: string, targetFile?: TFile) => Promise; - private onSelectStatusHandler: (status: string, targetFile: TFile | TFile[]) => Promise; - - - constructor(app: App, statusService: StatusService, settings: NoteStatusSettings) { - this.app = app; - this.statusService = statusService; - this.settings = settings; - - this.clickOutsideHandler = this.handleClickOutside.bind(this); - } - - /** - * Set the target file for status updates - */ - public setTargetFile(file: TFile | null): void { - this.targetFile = file; - this.targetFiles = file ? [file] : []; - } - - /** - * Set multiple target files for status updates - */ - public setTargetFiles(files: TFile[]): void { - this.targetFiles = [...files]; - this.targetFile = files.length === 1 ? files[0] : null; - } - - public setOnRemoveStatusHandler(handler: typeof this.onRemoveStatusHandler): void { - this.onRemoveStatusHandler = handler - } - public setOnSelectStatusHandler(handler: typeof this.onSelectStatusHandler): void { - this.onSelectStatusHandler = handler - } - - /** - * Set callback for status changes - */ - public setOnStatusChange(callback: (statuses: string[]) => void): void { - this.onStatusChange = callback; - } - - /** - * Get the current status change callback - */ - public getOnStatusChange(): (statuses: string[]) => void { - return this.onStatusChange; - } - - /** - * Updates the current statuses - */ - public updateStatuses(statuses: string[] | string): void { - this.currentStatuses = Array.isArray(statuses) ? [...statuses] : [statuses]; - - if (this.isOpen && this.dropdownElement) { - this.refreshDropdownContent(); - } - } - - /** - * Updates settings reference - */ - public updateSettings(settings: NoteStatusSettings): void { - this.settings = settings; - - if (this.isOpen && this.dropdownElement) { - this.refreshDropdownContent(); - } - } - - - /** - * Toggle the dropdown visibility - */ - public toggle(targetEl: HTMLElement, position?: { x: number, y: number }): void { - if (this.isOpen) { - this.close(); - setTimeout(() => { - if (!this.isOpen && !this.dropdownElement) { - this.open(targetEl, position); - } - }, 50); - } else { - this.open(targetEl, position); - } - } - - /** - * Open the dropdown - */ - public open(targetEl: HTMLElement, position?: { x: number, y: number }): void { - if (this.isOpen || this.dropdownElement) { - this.close(); - setTimeout(() => this.actuallyOpen(targetEl, position), 10); - return; - } - - this.actuallyOpen(targetEl, position); - } - - private actuallyOpen(targetEl: HTMLElement, position?: { x: number, y: number }): void { - this.isOpen = true; - - // Create and position dropdown - this.createDropdownElement(); - this.refreshDropdownContent(); - - position ? - this.positionAt(position.x, position.y) : - this.positionRelativeTo(targetEl); - - this.dropdownElement?.addClass('note-status-popover-animate-in'); - - // Add event listeners with slight delay to prevent immediate triggering - setTimeout(() => { - document.addEventListener('click', this.clickOutsideHandler); - document.addEventListener('keydown', this.handleEscapeKey); - }, 10); - } - - /** - * Create the dropdown element - */ - private createDropdownElement(): void { - this.dropdownElement = document.createElement('div'); - this.dropdownElement.addClass('note-status-popover', 'note-status-unified-dropdown'); - document.body.appendChild(this.dropdownElement); - } - - /** - * Close the dropdown - */ - public close(): void { - if (!this.dropdownElement || !this.isOpen) return; - - this.dropdownElement.addClass('note-status-popover-animate-out'); - - document.removeEventListener('click', this.clickOutsideHandler); - document.removeEventListener('keydown', this.handleEscapeKey); - - this.isOpen = false; - - setTimeout(() => { - if (this.dropdownElement) { - this.dropdownElement.remove(); - this.dropdownElement = null; - } - }, this.animationDuration); - } - - /** - * Refresh the dropdown content - */ - private refreshDropdownContent(): void { - if (!this.dropdownElement) return; - - this.dropdownElement.empty(); - - // Create UI sections - this.createHeader(); - this.createStatusChips(); - const searchInput = this.createSearchFilter(); - - // Create status options container - const statusOptionsContainer = this.dropdownElement.createDiv({ - cls: 'note-status-options-container' - }); - - // Get all available statuses (excluding 'unknown') - const allStatuses = this.statusService.getAllStatuses() - .filter(status => status.name !== 'unknown'); - - // Function to populate options with filtering - const populateOptions = (filter = '') => { - this.populateStatusOptions(statusOptionsContainer, allStatuses, filter); - }; - - // Initial population - populateOptions(); - - // Add search functionality - searchInput.addEventListener('input', () => { - populateOptions(searchInput.value); - }); - - // Focus search input after a short delay - setTimeout(() => searchInput.focus(), 50); - } - - /** - * Create the dropdown header - */ - private createHeader(): void { - if (!this.dropdownElement) return; - - const headerEl = this.dropdownElement.createDiv({ cls: 'note-status-popover-header' }); - const titleEl = headerEl.createDiv({ cls: 'note-status-popover-title' }); - - const iconContainer = titleEl.createDiv({ cls: 'note-status-popover-icon' }); - setIcon(iconContainer, 'tag'); - - titleEl.createSpan({ text: 'Note status', cls: 'note-status-popover-label' }); - - // If multiple files are selected, show count - if (this.targetFiles.length > 1) { - titleEl.createSpan({ - text: ` (${this.targetFiles.length} files)`, - cls: 'note-status-popover-count' - }); - } - } - - /** - * Create the status chips section - */ - private createStatusChips(): void { - if (!this.dropdownElement) return; - - const chipsContainer = this.dropdownElement.createDiv({ cls: 'note-status-popover-chips' }); - - const hasNoValidStatus = this.currentStatuses.length === 0 || - (this.currentStatuses.length === 1 && this.currentStatuses[0] === 'unknown'); - - if (hasNoValidStatus) { - chipsContainer.createDiv({ - cls: 'note-status-empty-indicator', - text: 'No status assigned' - }); - } else { - this.createStatusChipElements(chipsContainer); - } - } - - /** - * Create chips for all current statuses - */ - private createStatusChipElements(container: HTMLElement): void { - this.currentStatuses.forEach(status => { - if (status === 'unknown') return; - - const statusObj = this.statusService.getAllStatuses().find(s => s.name === status); - if (!statusObj) return; - - this.createSingleStatusChip(container, status, statusObj); - }); - } - - /** - * Create a single status chip - */ - private createSingleStatusChip(container: HTMLElement, status: string, statusObj: Status): void { - const chipEl = container.createDiv({ - cls: `note-status-chip status-${status}` - }); - - // Status icon and name - chipEl.createSpan({ - text: statusObj.icon, - cls: 'note-status-chip-icon' - }); - - chipEl.createSpan({ - text: statusObj.name, - cls: 'note-status-chip-text' - }); - - this.addRemoveButton(chipEl, status); - } - - /** - * Add a remove button to a status chip - */ - private addRemoveButton(chipEl: HTMLElement, status: string): void { - const statusObj = this.statusService.getAllStatuses().find(s => s.name === status); - const tooltipValue = statusObj?.description ? `${status} - ${statusObj.description}`: status; - - setTooltip(chipEl, tooltipValue); - - const removeBtn = chipEl.createDiv({ - cls: 'note-status-chip-remove', - attr: { - 'aria-label': `Remove ${status} status`, - 'title': `Remove ${status} status` - } - }); - setIcon(removeBtn, 'x'); - - removeBtn.addEventListener('click', async (e) => { - e.stopPropagation(); - - chipEl.addClass('note-status-chip-removing'); - - setTimeout(async () => { - if (this.targetFile) { - await this.onRemoveStatusHandler(status, this.targetFile); - this.refreshDropdownContent(); // TODO: The propagation maybe is redunant - } - }, 150); - }); - } - - - /** - * Create the search filter input - */ - private createSearchFilter(): HTMLInputElement { - if (!this.dropdownElement) { - throw new Error("Dropdown element not initialized"); - } - - const searchContainer = this.dropdownElement.createDiv({ cls: 'note-status-popover-search' }); - - return searchContainer.createEl('input', { - type: 'text', - placeholder: 'Filter statuses...', - cls: 'note-status-popover-search-input' - }); - } - - /** - * Populate status options with optional filtering - */ - private populateStatusOptions( - container: HTMLElement, - statuses: Status[], - filter = '' - ): void { - container.empty(); - - const filteredStatuses = filter ? - statuses.filter(status => - status.name.toLowerCase().includes(filter.toLowerCase()) || - status.icon.includes(filter) - ) : - statuses; - - if (filteredStatuses.length === 0) { - container.createDiv({ - cls: 'note-status-empty-options', - text: filter ? `No statuses match "${filter}"` : 'No statuses found' - }); - return; - } - - filteredStatuses.forEach(status => { - this.createStatusOption(container, status); - }); - } - - /** - * Create a single status option element - */ - private createStatusOption(container: HTMLElement, status: Status): void { - const isSelected = this.currentStatuses.includes(status.name); - - const optionEl = container.createDiv({ - cls: `note-status-option ${isSelected ? 'is-selected' : ''} status-${status.name}` - }); - - // Status icon and name - optionEl.createSpan({ - text: status.icon, - cls: 'note-status-option-icon' - }); - - optionEl.createSpan({ - text: status.name, - cls: 'note-status-option-text' - }); - - // Add tooltip if description available - if (status.description) { - setTooltip(optionEl, `${status.name} - ${status.description}`); - } - - // Check icon for selected status - if (isSelected) { - const checkIcon = optionEl.createDiv({ cls: 'note-status-option-check' }); - setIcon(checkIcon, 'check'); - } - - optionEl.addEventListener('click', () => { - optionEl.addClass('note-status-option-selecting'); - setTimeout(async () => { - if (this.targetFiles.length > 0) { - // Use all target files or just the single target file - const filesToUpdate = this.targetFiles.length > 0 ? this.targetFiles : (this.targetFile ? [this.targetFile] : []); - - if (filesToUpdate.length > 0) { - await this.onSelectStatusHandler(status.name, filesToUpdate); - - if (!this.settings.useMultipleStatuses) { - this.close(); - } - } - } - }, 150); - }); - } - - /** - * Position the dropdown at specific coordinates - */ - private positionAt(x: number, y: number): void { - if (!this.dropdownElement) return; - - this.dropdownElement.addClass('note-status-popover-fixed'); - this.dropdownElement.style.setProperty('--pos-x-px', `${x}px`); - this.dropdownElement.style.setProperty('--pos-y-px', `${y}px`); - - setTimeout(() => this.adjustPositionToViewport(), 0); - } - - /** - * Adjust the dropdown position to ensure it's visible in the viewport - */ - private adjustPositionToViewport(): void { - if (!this.dropdownElement) return; - - const rect = this.dropdownElement.getBoundingClientRect(); - - if (rect.right > window.innerWidth) { - this.dropdownElement.addClass('note-status-popover-right'); - this.dropdownElement.style.setProperty('--right-offset-px', '10px'); - } else { - this.dropdownElement.removeClass('note-status-popover-right'); - } - - if (rect.bottom > window.innerHeight) { - this.dropdownElement.addClass('note-status-popover-bottom'); - this.dropdownElement.style.setProperty('--bottom-offset-px', '10px'); - } else { - this.dropdownElement.removeClass('note-status-popover-bottom'); - } - - const maxHeight = window.innerHeight - rect.top - 20; - this.dropdownElement.style.setProperty('--max-height-px', `${maxHeight}px`); - } - - /** - * Position the dropdown relative to a target element - */ - private positionRelativeTo(targetEl: HTMLElement): void { - if (!this.dropdownElement) return; - - this.dropdownElement.addClass('note-status-popover-fixed'); - - const targetRect = targetEl.getBoundingClientRect(); - - this.dropdownElement.style.setProperty('--pos-y-px', `${targetRect.bottom + 5}px`); - this.dropdownElement.style.setProperty('--pos-x-px', `${targetRect.left}px`); - - setTimeout(() => this.adjustRelativePosition(targetRect), 0); - } - - /** - * Adjust position when positioned relative to an element - */ - private adjustRelativePosition(targetRect: DOMRect): void { - if (!this.dropdownElement) return; - - const rect = this.dropdownElement.getBoundingClientRect(); - - if (rect.right > window.innerWidth) { - this.dropdownElement.addClass('note-status-popover-right'); - this.dropdownElement.style.setProperty('--right-offset-px', `${window.innerWidth - targetRect.right}px`); - } else { - this.dropdownElement.removeClass('note-status-popover-right'); - } - - if (rect.bottom > window.innerHeight) { - this.dropdownElement.addClass('note-status-popover-bottom'); - this.dropdownElement.style.setProperty('--bottom-offset-px', `${window.innerHeight - targetRect.top + 5}px`); - } else { - this.dropdownElement.removeClass('note-status-popover-bottom'); - } - - const maxHeight = window.innerHeight - rect.top - 20; - this.dropdownElement.style.setProperty('--max-height-px', `${maxHeight}px`); - } - - /** - * Handle click outside the dropdown - */ - private handleClickOutside(e: MouseEvent): void { - if (this.dropdownElement && !this.dropdownElement.contains(e.target as Node)) { - this.close(); - } - } - - /** - * Handle escape key to close dropdown - */ - private handleEscapeKey = (e: KeyboardEvent): void => { - if (e.key === 'Escape') { - this.close(); - } - } - - /** - * Dispose of resources - */ - public dispose(): void { - this.close(); - } -} \ No newline at end of file From 5dcb503a8526b53b0a251d7742c67ef0285d7aa5 Mon Sep 17 00:00:00 2001 From: Aleix Date: Tue, 20 May 2025 09:08:09 +0200 Subject: [PATCH 31/67] chore: WIP integrations --- integrations/editor/EditorIntegration.ts | 77 ++ integrations/editor/ToolbarIntegration.ts | 166 ++++ integrations/editor/index.ts | 2 + integrations/explorer/ExplorerIntegration.ts | 265 ++++++ integrations/explorer/FileMenuIntegration.ts | 96 +++ integrations/explorer/index.ts | 2 + .../metadataCache/MetadataIntegration.ts | 73 ++ integrations/metadataCache/index.ts | 1 + .../workspace/WorkspaceIntegration.ts | 126 +++ integrations/workspace/index.ts | 1 + main.ts | 799 +++++------------- 11 files changed, 1015 insertions(+), 593 deletions(-) create mode 100644 integrations/editor/EditorIntegration.ts create mode 100644 integrations/editor/ToolbarIntegration.ts create mode 100644 integrations/editor/index.ts create mode 100644 integrations/explorer/ExplorerIntegration.ts create mode 100644 integrations/explorer/FileMenuIntegration.ts create mode 100644 integrations/explorer/index.ts create mode 100644 integrations/metadataCache/MetadataIntegration.ts create mode 100644 integrations/metadataCache/index.ts create mode 100644 integrations/workspace/WorkspaceIntegration.ts create mode 100644 integrations/workspace/index.ts diff --git a/integrations/editor/EditorIntegration.ts b/integrations/editor/EditorIntegration.ts new file mode 100644 index 0000000..5095511 --- /dev/null +++ b/integrations/editor/EditorIntegration.ts @@ -0,0 +1,77 @@ +import { App, Editor, MarkdownView, Menu } from 'obsidian'; +import { NoteStatusSettings } from '../../models/types'; +import { StatusService } from 'services/status-service'; + +/** + * Gestiona la integración con el editor de Obsidian + */ +export class EditorIntegration { + private app: App; + private settings: NoteStatusSettings; + private statusService: StatusService; + + constructor(app: App, settings: NoteStatusSettings, statusService: StatusService) { + this.app = app; + this.settings = settings; + this.statusService = statusService; + } + + /** + * Actualiza la configuración + */ + public updateSettings(settings: NoteStatusSettings): void { + this.settings = settings; + } + + /** + * Registra los menús contextuales del editor + */ + public registerEditorMenus(): void { + this.app.workspace.on('editor-menu', (menu, editor, view) => { + if (view instanceof MarkdownView) { + this.addStatusMenuItems(menu, editor, view); + } + }); + } + + /** + * Añade opciones de estado al menú del editor + */ + private addStatusMenuItems(menu: Menu, editor: Editor, view: MarkdownView): void { + menu.addItem(item => + item + .setTitle('Change note status') + .setIcon('tag') + .onClick(() => { + this.showStatusChangeUI(editor, view); + }) + ); + + menu.addItem(item => + item + .setTitle('Insert status metadata') + .setIcon('plus-circle') + .onClick(() => { + this.insertStatusMetadata(editor); + }) + ); + } + + /** + * Muestra la UI para cambiar estado + */ + private showStatusChangeUI(editor: Editor, view: MarkdownView): void { + const file = view.file; + if (!file) return; + + // En la implementación real, esto mostraría un dropdown o modal + console.log("Mostrar UI de cambio de estado"); + } + + /** + * Inserta metadatos de estado en el editor + */ + public insertStatusMetadata(editor: Editor): void { + this.statusService.insertStatusMetadataInEditor(editor); + } +} \ No newline at end of file diff --git a/integrations/editor/ToolbarIntegration.ts b/integrations/editor/ToolbarIntegration.ts new file mode 100644 index 0000000..f9562dd --- /dev/null +++ b/integrations/editor/ToolbarIntegration.ts @@ -0,0 +1,166 @@ +import { App, MarkdownView } from 'obsidian'; +import { NoteStatusSettings } from '../../models/types'; +import { StatusService } from 'services/status-service'; +import { StatusDropdown } from 'components/status-dropdown'; + +/** + * Gestiona la integración con la barra de herramientas del editor + */ +export class ToolbarIntegration { + private app: App; + private settings: NoteStatusSettings; + private statusService: StatusService; + private statusDropdown: StatusDropdown; + private toolbarButton: HTMLElement | null = null; + + constructor( + app: App, + settings: NoteStatusSettings, + statusService: StatusService, + statusDropdown: StatusDropdown + ) { + this.app = app; + this.settings = settings; + this.statusService = statusService; + this.statusDropdown = statusDropdown; + } + /** + * Actualiza la configuración + */ + public updateSettings(settings: NoteStatusSettings): void { + this.settings = settings; + this.updateToolbarButton(); + } + + /** + * Añade el botón de estado a la barra de herramientas + */ + public addToolbarButtonToActiveLeaf(): void { + const activeLeaf = this.app.workspace.activeLeaf; + if (!activeLeaf?.view || !(activeLeaf.view instanceof MarkdownView)) return; + + const toolbarContainer = activeLeaf.view.containerEl.querySelector('.view-header .view-actions'); + if (!toolbarContainer) return; + + const existingButton = toolbarContainer.querySelector('.note-status-toolbar-button'); + if (existingButton) { + this.toolbarButton = existingButton as HTMLElement; + this.updateToolbarButton(); + return; + } + + this.toolbarButton = this.createToolbarButton(); + + if (toolbarContainer.firstChild) { + toolbarContainer.insertBefore(this.toolbarButton, toolbarContainer.firstChild); + } else { + toolbarContainer.appendChild(this.toolbarButton); + } + } + + /** + * Crea el botón de la barra de herramientas + */ + private createToolbarButton(): HTMLElement { + const button = document.createElement('button'); + button.addClass('note-status-toolbar-button', 'clickable-icon', 'view-action'); + button.setAttribute('aria-label', 'Note status'); + + this.updateToolbarButton(); + + button.addEventListener('click', (e) => { + e.stopPropagation(); + e.preventDefault(); + this.handleToolbarButtonClick(); + }); + + return button; + } + + /** + * Actualiza la apariencia del botón + */ + private updateToolbarButton(): void { + if (!this.toolbarButton) return; + + this.toolbarButton.empty(); + + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile) return; + + const statuses = this.statusService.getFileStatuses(activeFile); + const hasValidStatus = statuses.length > 0 && statuses[0] !== 'unknown'; + + const badgeContainer = document.createElement('div'); + badgeContainer.addClass('note-status-toolbar-badge-container'); + + if (hasValidStatus) { + this.addStatusBadge(badgeContainer, statuses); + } else { + this.addUnknownStatusBadge(badgeContainer); + } + + this.toolbarButton.appendChild(badgeContainer); + } + + /** + * Añade insignia de estado al botón + */ + private addStatusBadge(container: HTMLElement, statuses: string[]): void { + const primaryStatus = statuses[0]; + const icon = this.statusService.getStatusIcon(primaryStatus); + + const iconSpan = document.createElement('span'); + iconSpan.addClass(`note-status-toolbar-icon`, `status-${primaryStatus}`); + iconSpan.textContent = icon; + container.appendChild(iconSpan); + + if (this.settings.useMultipleStatuses && statuses.length > 1) { + const countBadge = document.createElement('span'); + countBadge.addClass('note-status-count-badge'); + countBadge.textContent = `+${statuses.length - 1}`; + container.appendChild(countBadge); + } + } + + /** + * Añade insignia de estado desconocido + */ + private addUnknownStatusBadge(container: HTMLElement): void { + const iconSpan = document.createElement('span'); + iconSpan.addClass('note-status-toolbar-icon', 'status-unknown'); + iconSpan.textContent = this.statusService.getStatusIcon('unknown'); + container.appendChild(iconSpan); + } + + /** + * Maneja clic en el botón de la barra de herramientas + */ + private handleToolbarButtonClick(): void { + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile) return; + + // En la implementación real, esto mostraría un dropdown de estados + console.log("Mostrar dropdown de estados"); + } + + /** + * Actualiza el botón con nuevos estados + */ + public updateStatusDisplay(statuses: string[]): void { + // Actualiza el botón con los nuevos estados + if (this.toolbarButton) { + this.updateToolbarButton(); + } + } + + /** + * Limpieza al descargar el plugin + */ + public unload(): void { + if (this.toolbarButton) { + this.toolbarButton.remove(); + this.toolbarButton = null; + } + } +} \ No newline at end of file diff --git a/integrations/editor/index.ts b/integrations/editor/index.ts new file mode 100644 index 0000000..dc2b9c2 --- /dev/null +++ b/integrations/editor/index.ts @@ -0,0 +1,2 @@ +export { EditorIntegration } from './EditorIntegration'; +export { ToolbarIntegration } from './ToolbarIntegration'; \ No newline at end of file diff --git a/integrations/explorer/ExplorerIntegration.ts b/integrations/explorer/ExplorerIntegration.ts new file mode 100644 index 0000000..5490efe --- /dev/null +++ b/integrations/explorer/ExplorerIntegration.ts @@ -0,0 +1,265 @@ +import { App, TFile, debounce, setTooltip } from 'obsidian'; +import { FileExplorerView, NoteStatusSettings } from '../../models/types'; +import { StatusService } from 'services/status-service'; + +/** + * Gestiona la integración de iconos en el explorador de archivos + */ +export class ExplorerIntegration { + private app: App; + private settings: NoteStatusSettings; + private statusService: StatusService; + private iconUpdateQueue = new Set(); + private isProcessingQueue = false; + private debouncedUpdateAll: ReturnType; + + constructor(app: App, settings: NoteStatusSettings, statusService: StatusService) { + this.app = app; + this.settings = settings; + this.statusService = statusService; + this.debouncedUpdateAll = debounce(this.processUpdateQueue.bind(this), 100, true); + } + + /** + * Actualiza la configuración y refresca la UI si es necesario + */ + public updateSettings(settings: NoteStatusSettings): void { + const shouldRefreshIcons = + this.settings.showStatusIconsInExplorer !== settings.showStatusIconsInExplorer || + this.settings.hideUnknownStatusInExplorer !== settings.hideUnknownStatusInExplorer; + + this.settings = settings; + + if (shouldRefreshIcons) { + this.removeAllFileExplorerIcons(); + + if (settings.showStatusIconsInExplorer) { + setTimeout(() => this.updateAllFileExplorerIcons(), 50); + } + } else if (settings.showStatusIconsInExplorer) { + this.updateAllFileExplorerIcons(); + } + } + + /** + * Encuentra la vista del explorador de archivos + */ + private findFileExplorerView(): FileExplorerView | null { + const leaf = this.app.workspace.getLeavesOfType('file-explorer')[0]; + if (leaf?.view) return leaf.view as FileExplorerView; + + for (const leaf of this.app.workspace.getLeavesOfType('')) { + if (leaf.view && 'fileItems' in leaf.view) { + return leaf.view as FileExplorerView; + } + } + + return null; + } + + /** + * Actualiza los iconos para un archivo específico + */ + public updateFileExplorerIcons(file: TFile): void { + if (!file || !this.settings.showStatusIconsInExplorer || file.extension !== 'md') return; + + const activeFile = this.app.workspace.getActiveFile(); + if (activeFile?.path === file.path) { + this.updateSingleFileIconDirectly(file); + } + + this.queueFileUpdate(file); + } + + /** + * Actualiza todos los iconos en el explorador + */ + public updateAllFileExplorerIcons(): void { + if (!this.settings.showStatusIconsInExplorer) { + this.removeAllFileExplorerIcons(); + return; + } + + this.processFilesInBatches(); + } + + /** + * Elimina todos los iconos de estado + */ + public removeAllFileExplorerIcons(): void { + const fileExplorer = this.findFileExplorerView(); + if (!fileExplorer?.fileItems) return; + + Object.values(fileExplorer.fileItems).forEach(fileItem => { + const titleEl = fileItem.titleEl || fileItem.selfEl; + if (titleEl) this.removeExistingIcons(titleEl); + }); + + this.iconUpdateQueue.clear(); + } + + /** + * Obtiene los archivos seleccionados del explorador + */ + public getSelectedFiles(): TFile[] { + const fileExplorer = this.findFileExplorerView(); + if (!fileExplorer?.fileItems) return []; + + return Object.values(fileExplorer.fileItems) + .filter(item => + item.el?.classList.contains('is-selected') && + item.file instanceof TFile && + item.file.extension === 'md') + .map(item => item.file as TFile); + } + + // Métodos para procesamiento por lotes + + private queueFileUpdate(file: TFile): void { + if (!this.settings.showStatusIconsInExplorer || file.extension !== 'md') return; + + this.iconUpdateQueue.add(file.path); + this.debouncedUpdateAll(); + } + + private async processUpdateQueue(): Promise { + if (this.isProcessingQueue || this.iconUpdateQueue.size === 0) return; + + this.isProcessingQueue = true; + + try { + const fileExplorerView = this.findFileExplorerView(); + if (!fileExplorerView) { + setTimeout(() => this.debouncedUpdateAll(), 200); + return; + } + + const allPaths = Array.from(this.iconUpdateQueue); + const batchSize = 50; + + for (let i = 0; i < allPaths.length; i += batchSize) { + await this.processBatch(allPaths.slice(i, i + batchSize), fileExplorerView); + + if (i + batchSize < allPaths.length) { + await new Promise(resolve => setTimeout(resolve, 0)); + } + } + } catch (error) { + console.error('Note Status: Error processing file update queue', error); + } finally { + this.isProcessingQueue = false; + + if (this.iconUpdateQueue.size > 0) { + this.debouncedUpdateAll(); + } + } + } + + private async processBatch(paths: string[], fileExplorerView: FileExplorerView): Promise { + for (const path of paths) { + const file = this.app.vault.getFileByPath(path); + if (file instanceof TFile) { + this.updateSingleFileIcon(file, fileExplorerView); + } + this.iconUpdateQueue.delete(path); + } + } + + private async processFilesInBatches(): Promise { + const files = this.app.vault.getMarkdownFiles(); + const batchSize = 100; + + for (let i = 0; i < files.length; i += batchSize) { + files.slice(i, i + batchSize).forEach(file => this.queueFileUpdate(file)); + + if (i + batchSize < files.length) { + await new Promise(resolve => setTimeout(resolve, 0)); + } + } + } + + // Métodos para manipular iconos + + private updateSingleFileIcon(file: TFile, fileExplorerView: FileExplorerView): void { + if (!this.settings.showStatusIconsInExplorer || file.extension !== 'md') return; + + try { + const fileItem = fileExplorerView.fileItems[file.path]; + if (!fileItem) return; + + const titleEl = fileItem.titleEl || fileItem.selfEl; + if (!titleEl) return; + + const statuses = this.statusService.getFileStatuses(file); + + this.removeExistingIcons(titleEl); + + if (this.shouldSkipIcon(statuses)) return; + + this.addStatusIcons(titleEl, statuses); + } catch (error) { + console.error(`Note Status: Error updating icon for ${file.path}`, error); + } + } + + private updateSingleFileIconDirectly(file: TFile): void { + const fileExplorer = this.findFileExplorerView(); + if (fileExplorer) { + this.updateSingleFileIcon(file, fileExplorer); + } + } + + private shouldSkipIcon(statuses: string[]): boolean { + return this.settings.hideUnknownStatusInExplorer && + statuses.length === 1 && + statuses[0] === 'unknown'; + } + + private removeExistingIcons(element: HTMLElement): void { + const iconSelectors = '.note-status-icon, .note-status-icon-container'; + element.querySelectorAll(iconSelectors).forEach(icon => { + if (icon.classList.contains('note-status-icon') || + icon.classList.contains('note-status-icon-container')) { + icon.remove(); + } + }); + } + + private addStatusIcons(titleEl: HTMLElement, statuses: string[]): void { + const iconContainer = document.createElement('span'); + iconContainer.className = 'note-status-icon-container'; + + if (this.settings.useMultipleStatuses && statuses.length > 0 && statuses[0] !== 'unknown') { + statuses.forEach(status => this.addSingleStatusIcon(iconContainer, status)); + } else { + const primaryStatus = statuses[0] || 'unknown'; + if (primaryStatus !== 'unknown' || !this.settings.hideUnknownStatusInExplorer) { + this.addSingleStatusIcon(iconContainer, primaryStatus); + } + } + + if (iconContainer.childElementCount > 0) { + titleEl.appendChild(iconContainer); + } + } + + private addSingleStatusIcon(container: HTMLElement, status: string): void { + const iconEl = document.createElement('span'); + iconEl.className = `note-status-icon nav-file-tag status-${status}`; + iconEl.textContent = this.statusService.getStatusIcon(status); + + const statusObj = this.statusService.getAllStatuses().find(s => s.name === status); + const tooltipValue = statusObj?.description ? `${status} - ${statusObj.description}`: status; + setTooltip(iconEl, tooltipValue); + + container.appendChild(iconEl); + } + + /** + * Limpieza al descargar el plugin + */ + public unload(): void { + this.removeAllFileExplorerIcons(); + this.debouncedUpdateAll.cancel(); + } +} \ No newline at end of file diff --git a/integrations/explorer/FileMenuIntegration.ts b/integrations/explorer/FileMenuIntegration.ts new file mode 100644 index 0000000..ebcae32 --- /dev/null +++ b/integrations/explorer/FileMenuIntegration.ts @@ -0,0 +1,96 @@ +import { App, Menu, TFile } from 'obsidian'; +import { NoteStatusSettings } from '../../models/types'; +import { ExplorerIntegration } from './ExplorerIntegration'; +import { StatusService } from 'services/status-service'; + +/** + * Gestiona menús contextuales del explorador de archivos + */ +export class FileMenuIntegration { + private app: App; + private settings: NoteStatusSettings; + private statusService: StatusService; + private explorerIntegration: ExplorerIntegration; + + constructor( + app: App, + settings: NoteStatusSettings, + statusService: StatusService, + explorerIntegration: ExplorerIntegration + ) { + this.app = app; + this.settings = settings; + this.statusService = statusService; + this.explorerIntegration = explorerIntegration; + } + + /** + * Actualiza la configuración + */ + public updateSettings(settings: NoteStatusSettings): void { + this.settings = settings; + } + + /** + * Registra los elementos del menú de archivo + */ + public registerFileMenus(): void { + this.app.workspace.on('file-menu', (menu, file, source) => { + if (source === 'file-explorer-context-menu' && file instanceof TFile && file.extension === 'md') { + this.addStatusChangeMenu(menu, file); + } + }); + + this.app.workspace.on('files-menu', (menu, files) => { + const mdFiles = files.filter(file => + file instanceof TFile && file.extension === 'md' + ) as TFile[]; + + if (mdFiles.length > 0) { + this.addBatchStatusChangeMenu(menu, mdFiles); + } + }); + } + + /** + * Añade opción de cambio de estado al menú de archivo + */ + private addStatusChangeMenu(menu: Menu, file: TFile): void { + menu.addItem(item => + item + .setTitle('Change status') + .setIcon('tag') + .onClick(() => { + const selectedFiles = this.explorerIntegration.getSelectedFiles(); + if (selectedFiles.length > 1) { + this.showStatusChangeModal(selectedFiles); + } else { + this.showStatusChangeModal([file]); + } + }) + ); + } + + /** + * Añade opción de cambio de estado para múltiples archivos + */ + private addBatchStatusChangeMenu(menu: Menu, files: TFile[]): void { + menu.addItem(item => + item + .setTitle('Change status') + .setIcon('tag') + .onClick(() => { + this.showStatusChangeModal(files); + }) + ); + } + + /** + * Muestra el modal para cambiar estado + */ + private showStatusChangeModal(files: TFile[]): void { + // Implementación que mostraría un modal para seleccionar estado + // En la práctica, esta función delegaría en un servicio de UI + console.log(`Cambiar estado para ${files.length} archivos`); + } +} \ No newline at end of file diff --git a/integrations/explorer/index.ts b/integrations/explorer/index.ts new file mode 100644 index 0000000..63bdead --- /dev/null +++ b/integrations/explorer/index.ts @@ -0,0 +1,2 @@ +export { ExplorerIntegration } from './ExplorerIntegration'; +export { FileMenuIntegration } from './FileMenuIntegration'; \ No newline at end of file diff --git a/integrations/metadataCache/MetadataIntegration.ts b/integrations/metadataCache/MetadataIntegration.ts new file mode 100644 index 0000000..dfc9c6e --- /dev/null +++ b/integrations/metadataCache/MetadataIntegration.ts @@ -0,0 +1,73 @@ +import { App, TFile } from 'obsidian'; +import { NoteStatusSettings } from '../../models/types'; +import { ExplorerIntegration } from '../explorer/ExplorerIntegration'; +import { StatusService } from 'services/status-service'; + +/** + * Gestiona la integración con la caché de metadatos + */ +export class MetadataIntegration { + private app: App; + private settings: NoteStatusSettings; + private statusService: StatusService; + private explorerIntegration: ExplorerIntegration; + + constructor( + app: App, + settings: NoteStatusSettings, + statusService: StatusService, + explorerIntegration: ExplorerIntegration + ) { + this.app = app; + this.settings = settings; + this.statusService = statusService; + this.explorerIntegration = explorerIntegration; + } + + /** + * Actualiza la configuración + */ + public updateSettings(settings: NoteStatusSettings): void { + this.settings = settings; + } + + /** + * Registra los eventos de metadatos + */ + public registerMetadataEvents(): void { + // Evento cuando cambian los metadatos de un archivo + this.app.metadataCache.on('changed', (file) => { + if (file instanceof TFile && file.extension === 'md') { + this.handleMetadataChanged(file); + } + }); + + // Evento cuando se resuelven todos los metadatos (al cargar) + this.app.metadataCache.on('resolved', () => { + setTimeout(() => { + if (this.settings.showStatusIconsInExplorer) { + this.explorerIntegration.updateAllFileExplorerIcons(); + } + }, 500); + }); + } + + /** + * Maneja cambios en metadatos de un archivo + */ + private handleMetadataChanged(file: TFile): void { + // Actualiza el explorador si está habilitado + if (this.settings.showStatusIconsInExplorer) { + this.explorerIntegration.updateFileExplorerIcons(file); + } + + // Actualiza otros componentes si es el archivo activo + const activeFile = this.app.workspace.getActiveFile(); + if (activeFile?.path === file.path) { + const statuses = this.statusService.getFileStatuses(file); + window.dispatchEvent(new CustomEvent('note-status:status-changed', { + detail: { statuses, file: file.path } + })); + } + } +} \ No newline at end of file diff --git a/integrations/metadataCache/index.ts b/integrations/metadataCache/index.ts new file mode 100644 index 0000000..a79d712 --- /dev/null +++ b/integrations/metadataCache/index.ts @@ -0,0 +1 @@ +export { MetadataIntegration } from './MetadataIntegration'; \ No newline at end of file diff --git a/integrations/workspace/WorkspaceIntegration.ts b/integrations/workspace/WorkspaceIntegration.ts new file mode 100644 index 0000000..d0967c2 --- /dev/null +++ b/integrations/workspace/WorkspaceIntegration.ts @@ -0,0 +1,126 @@ +import { App, TFile, WorkspaceLeaf } from 'obsidian'; +import { NoteStatusSettings } from '../../models/types'; +import { ToolbarIntegration } from '../editor/ToolbarIntegration'; +import { StatusService } from 'services/status-service'; + +/** + * Gestiona la integración con el workspace de Obsidian + */ +export class WorkspaceIntegration { + private app: App; + private settings: NoteStatusSettings; + private statusService: StatusService; + private toolbarIntegration: ToolbarIntegration; + private lastActiveFile: TFile | null = null; + + constructor( + app: App, + settings: NoteStatusSettings, + statusService: StatusService, + toolbarIntegration: ToolbarIntegration + ) { + this.app = app; + this.settings = settings; + this.statusService = statusService; + this.toolbarIntegration = toolbarIntegration; + } + + /** + * Actualiza la configuración + */ + public updateSettings(settings: NoteStatusSettings): void { + this.settings = settings; + } + + /** + * Registra eventos del workspace + */ + public registerWorkspaceEvents(): void { + // Evento al abrir un archivo + this.app.workspace.on('file-open', (file) => { + if (file instanceof TFile) { + this.handleFileOpen(file); + } + }); + + // Evento de cambio de hoja activa + this.app.workspace.on('active-leaf-change', (leaf: WorkspaceLeaf) => { + this.handleActiveLeafChange(leaf); + }); + + // Evento de cambio en el layout + this.app.workspace.on('layout-change', () => { + this.handleLayoutChange(); + }); + } + + /** + * Maneja apertura de archivo + */ + private handleFileOpen(file: TFile): void { + // Añade el botón de la barra de herramientas + this.toolbarIntegration.addToolbarButtonToActiveLeaf(); + + // Actualiza estado + this.checkNoteStatus(); + } + + /** + * Maneja cambio de hoja activa + */ + private handleActiveLeafChange(leaf: WorkspaceLeaf): void { + // Añade el botón de la barra de herramientas + this.toolbarIntegration.addToolbarButtonToActiveLeaf(); + + const activeFile = this.app.workspace.getActiveFile(); + + // Solo actualiza si el archivo realmente cambió + if (this.lastActiveFile?.path !== activeFile?.path) { + this.lastActiveFile = activeFile; + this.checkNoteStatus(); + } + } + + /** + * Maneja cambio de layout + */ + private handleLayoutChange(): void { + // Actualiza componentes que dependen del layout + this.updateStatusPane(); + } + + /** + * Verifica y actualiza el estado de la nota activa + */ + private checkNoteStatus(): void { + try { + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile || activeFile.extension !== 'md') { + this.updateStatusComponents(['unknown']); + return; + } + + const statuses = this.statusService.getFileStatuses(activeFile); + this.updateStatusComponents(statuses); + } catch (error) { + console.error('Error checking note status:', error); + } + } + + /** + * Actualiza componentes de estado con nuevos estados + */ + private updateStatusComponents(statuses: string[]): void { + // Dispara evento para que otros componentes se actualicen + window.dispatchEvent(new CustomEvent('note-status:status-changed', { + detail: { statuses } + })); + } + + /** + * Actualiza el panel de estado + */ + private updateStatusPane(): void { + window.dispatchEvent(new CustomEvent('note-status:update-pane')); + } +} \ No newline at end of file diff --git a/integrations/workspace/index.ts b/integrations/workspace/index.ts new file mode 100644 index 0000000..f3cd419 --- /dev/null +++ b/integrations/workspace/index.ts @@ -0,0 +1 @@ +export { WorkspaceIntegration } from './WorkspaceIntegration'; \ No newline at end of file diff --git a/main.ts b/main.ts index 65c92ee..cc3b562 100644 --- a/main.ts +++ b/main.ts @@ -1,681 +1,294 @@ -import { Editor, MarkdownView, Notice, Plugin, TFile, addIcon, WorkspaceLeaf, debounce } from 'obsidian'; - -// Constants -import { ICONS } from './constants/icons'; +import { Plugin, Notice } from 'obsidian'; import { DEFAULT_SETTINGS } from './constants/defaults'; - -// Types import { NoteStatusSettings } from './models/types'; +import { StatusService } from 'services/status-service'; +import { StyleService } from 'services/style-service'; -// Services -import { StatusService } from './services/status-service'; -import { StyleService } from './services/style-service'; +// Importar integraciones +import { ExplorerIntegration, FileMenuIntegration } from './integrations/explorer'; +import { EditorIntegration, ToolbarIntegration } from './integrations/editor'; +import { MetadataIntegration } from './integrations/metadataCache'; +import { WorkspaceIntegration } from './integrations/workspace'; -// UI Components -import { StatusPaneView } from './ui/components/status-pane-view'; -import { ExplorerIntegration } from './ui/integrations/explorer-integration'; -import { StatusContextMenu } from './ui/menus/status-context-menu'; - -// Settings -import { NoteStatusSettingTab } from './settings/settings-tab'; +// Importar componentes UI import { StatusBarController } from 'components/statusBar/status-bar-controller'; -import { StatusDropdown } from 'components/status-dropdown'; +import { StatusPaneView } from 'ui/components/status-pane-view'; +import { NoteStatusSettingTab } from 'settings/settings-tab'; -/** - * Main plugin class for Note Status functionality - */ export default class NoteStatus extends Plugin { settings: NoteStatusSettings; + + // Servicios statusService: StatusService; styleService: StyleService; - statusBar: StatusBarController; - statusDropdown: StatusDropdown; - explorerIntegration: ExplorerIntegration; - statusContextMenu: StatusContextMenu; - private statusPaneLeaf: WorkspaceLeaf | null = null; - private lastActiveFile: TFile | null = null; - private hasShownErrorNotification = false; - - // Event handlers bound methods - private boundSaveSettings = this.saveSettings.bind(this); - private boundCheckNoteStatus = this.checkNoteStatus.bind(this); - private boundRefreshDropdown = () => this.statusDropdown?.render(); - private boundRefreshUI = () => this.checkNoteStatus(); - - // Debounced methods for better performance - private debouncedCheckNoteStatus = debounce(() => this.checkNoteStatus(), 100, true); - private debouncedUpdateExplorer = debounce(() => this.explorerIntegration?.updateAllFileExplorerIcons(), 150); - private debouncedUpdateStatusPane = debounce(() => this.updateStatusPane(), 200); + // Componentes UI + statusBarController: StatusBarController; + + // Integraciones + explorerIntegration: ExplorerIntegration; + fileMenuIntegration: FileMenuIntegration; + editorIntegration: EditorIntegration; + toolbarIntegration: ToolbarIntegration; + metadataIntegration: MetadataIntegration; + workspaceIntegration: WorkspaceIntegration; async onload() { try { await this.loadSettings(); - // Register icons - this.registerIcons(); + // 1. Inicializar servicios básicos + this.initializeServices(); - // Initialize services - this.statusService = new StatusService(this.app, this.settings); - this.styleService = new StyleService(this.settings); - - // Register views and commands + // 2. Registrar vistas y componentes UI this.registerViews(); + + // 3. Inicializar integraciones + this.initializeIntegrations(); + + // 4. Registrar comandos this.registerCommands(); - // Add settings tab - this.addSettingTab(new NoteStatusSettingTab(this.app, this, - this.statusService)); - - // Set up custom events + // 5. Registrar eventos personalizados this.setupCustomEvents(); - // Initialize UI after layout is ready + // 6. Esperar a que el layout esté listo para inicializar UI this.app.workspace.onLayoutReady(() => { this.initializeUI(); }); + } catch (error) { console.error('Error loading Note Status plugin:', error); new Notice('Error loading Note Status plugin. Check console for details.'); } } - /** - * Register custom icons - */ - private registerIcons(): void { - Object.entries(ICONS).forEach(([name, svg]) => { - const parser = new DOMParser(); - const svgDoc = parser.parseFromString(svg, 'image/svg+xml'); - const svgElement = svgDoc.documentElement; - - const serializer = new XMLSerializer(); - const sanitizedSvg = serializer.serializeToString(svgElement); - - if (name === 'statusPane') { - addIcon('status-pane', sanitizedSvg); - } else { - addIcon(`note-status-${name}`, sanitizedSvg); - } - }); + private async loadSettings() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); } - /** - * Initialize UI components - */ - private initializeUI(): void { - // Init basic UI components - this.statusBar = new StatusBarController(this.addStatusBarItem(), this.settings, this.statusService); - this.statusDropdown = new StatusDropdown(this.app, this.settings, this.statusService); - - // Initialize explorer integration with a slight delay to ensure UI elements are ready - setTimeout(() => { - this.explorerIntegration = new ExplorerIntegration(this.app, this.settings, this.statusService); - this.statusContextMenu = new StatusContextMenu( - this.app, - this.settings, - this.statusService, - this.statusDropdown, - this.explorerIntegration - ); - - // Register events - this.registerMenuHandlers(); - this.registerEvents(); - - // Check status for active file - this.checkNoteStatus(); - - // Update only active file icon initially for better performance - const activeFile = this.app.workspace.getActiveFile(); - if (activeFile && this.settings.showStatusIconsInExplorer) { - this.explorerIntegration.updateFileExplorerIcons(activeFile); - } - - // Delay full explorer icon update to avoid startup lag - if (this.settings.showStatusIconsInExplorer) { - setTimeout(() => { - this.explorerIntegration.updateAllFileExplorerIcons(); - }, 2000); - } - }, 300); + private initializeServices() { + this.statusService = new StatusService(this.app, this.settings); + this.styleService = new StyleService(this.settings); } - /** - * Register views for status pane - */ - private registerViews(): void { + private registerViews() { + // Registrar vista del panel de estado this.registerView('status-pane', (leaf) => { - this.statusPaneLeaf = leaf; return new StatusPaneView(leaf, this); }); - // Add ribbon icon + // Añadir ícono en la barra lateral this.addRibbonIcon('status-pane', 'Open status pane', () => { this.openStatusPane(); }); + + // Añadir pestaña de configuración + this.addSettingTab(new NoteStatusSettingTab(this.app, this, this.statusService)); } - /** - * Set up custom events for status changes and UI updates - */ - private setupCustomEvents(): void { - // Listen for settings changes - window.addEventListener('note-status:settings-changed', this.boundSaveSettings); - - // 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 { - const statuses = e.detail?.statuses || ['unknown']; - this.statusBar.update(statuses); - this.statusDropdown.update(statuses); - - // Update explorer icons for the active file - const activeFile = this.app.workspace.getActiveFile(); - if (activeFile) { - this.explorerIntegration.updateFileExplorerIcons(activeFile); - } - - setTimeout(() => this.statusDropdown.render(), 50); - } catch (error) { - console.error('Error handling status change event:', error); - } - }); - - // Listen for refresh dropdown - window.addEventListener('note-status:refresh-dropdown', this.boundRefreshDropdown); - - // Listen for UI refresh - window.addEventListener('note-status:refresh-ui', this.boundRefreshUI); + private initializeIntegrations() { + // Crear integraciones en orden de dependencia + + // 1. Integraciones básicas primero + this.explorerIntegration = new ExplorerIntegration(this.app, this.settings, this.statusService); + this.toolbarIntegration = new ToolbarIntegration(this.app, this.settings, this.statusService); + + // 2. Integraciones que dependen de otras + this.fileMenuIntegration = new FileMenuIntegration( + this.app, + this.settings, + this.statusService, + this.explorerIntegration + ); + + this.editorIntegration = new EditorIntegration(this.app, this.settings, this.statusService); + + this.metadataIntegration = new MetadataIntegration( + this.app, + this.settings, + this.statusService, + this.explorerIntegration + ); + + this.workspaceIntegration = new WorkspaceIntegration( + this.app, + this.settings, + this.statusService, + this.toolbarIntegration + ); + + // 3. Registrar eventos en cada integración + this.fileMenuIntegration.registerFileMenus(); + this.editorIntegration.registerEditorMenus(); + this.metadataIntegration.registerMetadataEvents(); + this.workspaceIntegration.registerWorkspaceEvents(); } - /** - * Register plugin commands - */ - private registerCommands(): void { - // Refresh status command + private registerCommands() { + // Comando para actualizar estado this.addCommand({ id: 'refresh-status', name: 'Refresh status', callback: () => { - this.checkNoteStatus(); + this.refreshStatus(); new Notice('Note status refreshed!'); } }); + // Comando para forzar actualización de UI this.addCommand({ id: 'force-refresh-ui', name: 'Force refresh user interface', callback: () => this.forceRefreshUI() }); - // Insert status metadata command + // Comando para insertar metadatos de estado this.addCommand({ id: 'insert-status-metadata', name: 'Insert status metadata', - editorCallback: (editor: Editor, view: MarkdownView) => { - this.statusService.insertStatusMetadataInEditor(editor); + editorCallback: (editor) => { + this.editorIntegration.insertStatusMetadata(editor); new Notice('Status metadata inserted'); } }); - // Open status pane command + // Comando para abrir panel de estado this.addCommand({ id: 'open-status-pane', name: 'Open status pane', callback: () => this.openStatusPane() }); - - // Add status to note command - this.addCommand({ - id: 'add-status-to-note', - name: 'Add status to current note', - callback: () => this.showAddStatusToNoteMenu() - }); } - /** - * Show menu for adding a status to the current note - */ - private showAddStatusToNoteMenu(): void { + private setupCustomEvents() { + // Evento para cambios de configuración + window.addEventListener('note-status:settings-changed', this.saveSettings.bind(this)); + + // Evento para cambios de estado + window.addEventListener('note-status:status-changed', this.handleStatusChanged.bind(this)); + + // Evento para actualización de UI + window.addEventListener('note-status:refresh-ui', this.refreshUI.bind(this)); + } + + private initializeUI() { + // Inicializar barra de estado + this.statusBarController = new StatusBarController( + this.addStatusBarItem(), + this.settings, + this.statusService + ); + + // Inicializar iconos del explorador (con retraso para evitar ralentizar el inicio) + if (this.settings.showStatusIconsInExplorer) { + setTimeout(() => { + this.explorerIntegration.updateAllFileExplorerIcons(); + }, 2000); + } + } + + private handleStatusChanged(event: CustomEvent) { + const { statuses, file } = event.detail; + + // Actualizar barra de estado + this.statusBarController.update(statuses); + + // Actualizar toolbar + this.toolbarIntegration.updateStatusDisplay(statuses); + + // Actualizar explorador si es necesario + if (file) { + const fileObj = this.app.vault.getFileByPath(file); + if (fileObj) { + this.explorerIntegration.updateFileExplorerIcons(fileObj); + } + } + } + + private refreshStatus() { const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile || activeFile.extension !== 'md') { - new Notice('No markdown file is active'); + if (activeFile) { + const statuses = this.statusService.getFileStatuses(activeFile); + window.dispatchEvent(new CustomEvent('note-status:status-changed', { + detail: { statuses, file: activeFile.path } + })); + } + } + + private refreshUI() { + this.refreshStatus(); + + // Actualizar otros componentes según sea necesario + // ... + } + + private forceRefreshUI() { + // Actualizar todos los elementos visibles + this.refreshStatus(); + + if (this.settings.showStatusIconsInExplorer) { + this.explorerIntegration.updateAllFileExplorerIcons(); + } + + window.dispatchEvent(new CustomEvent('note-status:update-pane')); + + new Notice('UI forcefully refreshed'); + } + + private async openStatusPane() { + // Comprobar si ya está abierto + const existing = this.app.workspace.getLeavesOfType('status-pane')[0]; + if (existing) { + this.app.workspace.setActiveLeaf(existing); return; } - - this.statusContextMenu.showForSingleFile(activeFile); - } - - /** - * Register menu handlers - */ - private registerMenuHandlers(): void { - // File explorer context menu - this.registerEvent( - this.app.workspace.on('file-menu', (menu, file, source) => { - if (source === 'file-explorer-context-menu' && file instanceof TFile && file.extension === 'md') { - menu.addItem((item) => - item - .setTitle('Change status') - .setIcon('tag') - .onClick(() => { - const selectedFiles = this.explorerIntegration.getSelectedFiles(); - if (selectedFiles.length > 1) { - this.statusContextMenu.showForFiles(selectedFiles); - } else { - this.statusContextMenu.showForFiles([file]); - } - }) - ); - } - }) - ); - // Multiple files selection menu - this.registerEvent( - this.app.workspace.on('files-menu', (menu, files) => { - const mdFiles: TFile[] = []; - for (const file of files) { - if (file instanceof TFile && file.extension === 'md') { - mdFiles.push(file); - } - } - if (mdFiles.length > 0) { - menu.addItem((item) => - item - .setTitle('Change status') - .setIcon('tag') - .onClick(() => { - this.statusContextMenu.showForFiles(mdFiles); - }) - ); - } - }) - ); + // Crear una nueva hoja + const leaf = this.app.workspace.getLeftLeaf(false); + if (leaf) { + await leaf.setViewState({ type: 'status-pane', active: true }); + } + } + + async saveSettings() { + await this.saveData(this.settings); - // Editor context menu - this.registerEvent( - this.app.workspace.on('editor-menu', (menu, editor, view) => { - if (view instanceof MarkdownView) { - menu.addItem((item) => - item - .setTitle('Change note status') - .setIcon('tag') - .onClick(() => this.statusDropdown.showInContextMenu(editor, view)) - ); - } - }) - ); - } - - /** - * Register workspace and vault events - */ - private registerEvents(): void { - // Register file events - this.registerFileEvents(); - - // Register metadata events - this.registerMetadataEvents(); - - // File open event with optimization to avoid redundant updates - this.registerEvent(this.app.workspace.on('file-open', (file) => { - if (file instanceof TFile) { - // First make sure the button exists - this.statusDropdown.addToolbarButtonToActiveLeaf(); - // Then check status and update button - this.checkNoteStatus(); - } - })); - - // Editor change event - debounced to avoid performance issues - this.registerEvent(this.app.workspace.on('editor-change', () => { - this.debouncedCheckNoteStatus(); - })); - - // Add this for toolbar button persistence - this.registerEvent( - this.app.workspace.on('active-leaf-change', () => { - this.statusDropdown.addToolbarButtonToActiveLeaf(); - // Update the status after adding the button to ensure icon is correct - this.checkNoteStatus(); - }) - ); - - // Active leaf change event - optimized to avoid redundant updates - this.registerEvent(this.app.workspace.on('active-leaf-change', () => { - const activeFile = this.app.workspace.getActiveFile(); - - // Only update if the file actually changed - if (this.lastActiveFile?.path !== activeFile?.path) { - this.lastActiveFile = activeFile; - this.statusDropdown.update(this.getCurrentStatuses()); - this.debouncedUpdateStatusPane(); - } - })); - - // Layout change event - ensure status pane is properly rendered - this.registerEvent(this.app.workspace.on('layout-change', () => { - this.debouncedUpdateStatusPane(); - })); - } - - /** - * Register file modification events - */ - private registerFileEvents(): void { - // File modification events with optimization - this.registerEvent(this.app.vault.on('modify', (file) => { - if (!(file instanceof TFile) || file.extension !== 'md') return; - - // Only update UI for the modified file - this.explorerIntegration.updateFileExplorerIcons(file); - - // If this is the active file, also update other UI elements - const activeFile = this.app.workspace.getActiveFile(); - if (activeFile?.path === file.path) { - this.checkNoteStatus(); - this.statusDropdown.update(this.getCurrentStatuses()); - } - - // Update the status pane but debounced - this.debouncedUpdateStatusPane(); - })); - - // File creation, deletion, and rename events - this.registerEvent(this.app.vault.on('create', (file) => { - if (file instanceof TFile && file.extension === 'md') { - this.explorerIntegration.updateFileExplorerIcons(file); - this.debouncedUpdateStatusPane(); - } - })); - - this.registerEvent(this.app.vault.on('delete', () => { - this.debouncedUpdateExplorer(); - this.debouncedUpdateStatusPane(); - })); - - this.registerEvent(this.app.vault.on('rename', (file) => { - if (file instanceof TFile && file.extension === 'md') { - this.explorerIntegration.updateAllFileExplorerIcons(); - this.debouncedUpdateStatusPane(); - } - })); - } - - /** - * Register metadata cache events - */ - private registerMetadataEvents(): void { - // Metadata change events - this.registerEvent(this.app.metadataCache.on('changed', (file) => { - if (file instanceof TFile && file.extension === 'md') { - this.explorerIntegration.updateFileExplorerIcons(file); - - // Update other UI elements if this is the active file - if (this.app.workspace.getActiveFile()?.path === file.path) { - this.checkNoteStatus(); - this.statusDropdown.update(this.getCurrentStatuses()); - } - - this.debouncedUpdateStatusPane(); - } - })); - - // Metadata resolved event - when all files are indexed - this.registerEvent( - this.app.metadataCache.on('resolved', () => { - // When metadata cache is fully resolved, update all icons - setTimeout(() => this.explorerIntegration.updateAllFileExplorerIcons(), 500); - }) - ); - } - - /** - * Check and update the status display for the active file - */ - checkNoteStatus(): void { - try { - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile || activeFile.extension !== 'md') { - this.updateStatusComponents(['unknown']); - return; - } - - const statuses = this.statusService.getFileStatuses(activeFile); - this.updateStatusComponents(statuses); - } catch (error) { - this.handleStatusError(error); - } - } - - /** - * Update status components with new statuses - */ - private updateStatusComponents(statuses: string[]): void { - this.statusBar.update(statuses); - this.statusDropdown.update(statuses); - } - - /** - * Handle errors when checking status - */ - private handleStatusError(error: any): void { - console.error('Error checking note status:', error); - if (!this.hasShownErrorNotification) { - new Notice('Error checking note status. Check console for details.'); - this.hasShownErrorNotification = true; - setTimeout(() => { this.hasShownErrorNotification = false; }, 10000); - } - } - - /** - * Get the current statuses for the active file - */ - getCurrentStatuses(): string[] { - try { - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile || !(activeFile instanceof TFile) || activeFile.extension !== 'md') { - return ['unknown']; - } - return this.statusService.getFileStatuses(activeFile); - } catch (error) { - console.error('Error getting current statuses:', error); - return ['unknown']; - } - } - - /** - * Open the status pane - */ - public async openStatusPane(): Promise { - try { - // Check if already open - const existing = this.app.workspace.getLeavesOfType('status-pane')[0]; - if (existing) { - this.app.workspace.setActiveLeaf(existing); - return; - } - - // Create a new leaf - const leaf = this.app.workspace.getLeftLeaf(false); - if (leaf) { - await leaf.setViewState({ type: 'status-pane', active: true }); - this.statusPaneLeaf = leaf; - } - } catch (error) { - console.error('Error opening status pane:', error); - new Notice('Error opening status pane. Check console for details.'); - } - } - - /** - * Update the status pane - */ - async updateStatusPane(): Promise { - try { - 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); - } - } catch (error) { - console.error('Error updating status pane:', error); - } - } - - /** - * Load settings with error handling - */ - async loadSettings(): Promise { - try { - const loadedData = await this.loadData(); - this.settings = Object.assign({}, DEFAULT_SETTINGS, loadedData); - - // Ensure default colors are set for all statuses - this.initializeDefaultColors(); - } catch (error) { - console.error('Error loading settings:', error); - new Notice('Error loading settings. Using defaults. Check console for details.'); - this.settings = Object.assign({}, DEFAULT_SETTINGS); - this.initializeDefaultColors(); - } - } - - /** - * Initialize default colors and template colors - */ - private initializeDefaultColors(): void { - // Initialize template colors if needed - if (!this.settings.useCustomStatusesOnly && this.statusService) { - const templateStatuses = this.statusService.getTemplateStatuses(); - for (const status of templateStatuses) { - if (status.color && !this.settings.statusColors[status.name]) { - this.settings.statusColors[status.name] = status.color; - } - } - } - } - - /** - * Save settings with error handling - */ - async saveSettings(): Promise { - try { - await this.saveData(this.settings); - this.updateComponentSettings(); - } catch (error) { - console.error('Error saving settings:', error); - new Notice('Error saving settings. Check console for details.'); - } - } - - /** - * Get the open status pane view if it exists - */ - private getStatusPaneView(): StatusPaneView | null { - if (this.statusPaneLeaf && this.statusPaneLeaf.view instanceof StatusPaneView) { - return this.statusPaneLeaf.view as StatusPaneView; - } - - const leaf = this.app.workspace.getLeavesOfType('status-pane')[0]; - if (leaf?.view instanceof StatusPaneView) { - this.statusPaneLeaf = leaf; - return leaf.view as StatusPaneView; - } - - return null; - } - - /** - * Update all components with new settings - */ - private updateComponentSettings(): void { - // Update services + // Actualizar servicios this.statusService.updateSettings(this.settings); this.styleService.updateSettings(this.settings); - - // Update UI components - const components = [ - this.statusBar, - this.statusDropdown, - this.explorerIntegration, - this.statusContextMenu - ]; - components.forEach(component => - component.updateSettings?.(this.settings)); - - // Update status pane if open - const statusPane = this.getStatusPaneView(); - if (statusPane) { - statusPane.updateSettings(this.settings); - } + // Actualizar integraciones + this.updateIntegrationsSettings(); } - /** - * Force refresh all UI components - */ - public forceRefreshUI(): void { - try { - // Cancel any pending updates - [ - this.debouncedCheckNoteStatus, - this.debouncedUpdateExplorer, - this.debouncedUpdateStatusPane - ].forEach(fn => fn.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.'); - } + private updateIntegrationsSettings() { + // Actualizar configuración en todas las integraciones + this.explorerIntegration.updateSettings(this.settings); + this.fileMenuIntegration.updateSettings(this.settings); + this.editorIntegration.updateSettings(this.settings); + this.toolbarIntegration.updateSettings(this.settings); + this.metadataIntegration.updateSettings(this.settings); + this.workspaceIntegration.updateSettings(this.settings); + + // Actualizar componentes UI + this.statusBarController.updateSettings(this.settings); } - /** - * Clean up when the plugin is unloaded - */ - onunload(): void { - // Cancel debounced functions - this.debouncedCheckNoteStatus.cancel(); - this.debouncedUpdateExplorer.cancel(); - this.debouncedUpdateStatusPane.cancel(); - - // Clean up UI elements - this.statusBar.unload?.(); - this.statusDropdown.unload?.(); - this.explorerIntegration.unload?.(); - this.styleService.unload?.(); - - // Close status pane if open - const statusPane = this.app.workspace.getLeavesOfType('status-pane')[0]; - if (statusPane) statusPane.detach(); - - // Remove custom event listeners - this.removeCustomEventListeners(); - } - - /** - * Remove custom event listeners - */ - private removeCustomEventListeners(): void { - window.removeEventListener('note-status:settings-changed', this.boundSaveSettings); - window.removeEventListener('note-status:status-changed', this.boundCheckNoteStatus); - window.removeEventListener('note-status:refresh-dropdown', this.boundRefreshDropdown); - window.removeEventListener('note-status:refresh-ui', this.boundRefreshUI); + onunload() { + // Limpiar eventos personalizados + window.removeEventListener('note-status:settings-changed', this.saveSettings.bind(this)); + window.removeEventListener('note-status:status-changed', this.handleStatusChanged.bind(this)); + window.removeEventListener('note-status:refresh-ui', this.refreshUI.bind(this)); + + // Limpiar integraciones + this.explorerIntegration.unload(); + this.toolbarIntegration.unload(); + + // Limpiar servicios + this.styleService.unload(); + + // Limpiar componentes UI + this.statusBarController.unload(); } } \ No newline at end of file From 7cc04dc3e07857c5cea82572379e90e4cc89adb8 Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Wed, 21 May 2025 09:03:43 +0200 Subject: [PATCH 32/67] chore: status bar wip --- .../status-bar-controller.ts | 0 .../status-bar-view.ts | 0 .../status-pane-view-controller.ts | 171 +++++++ .../status-pane-view/status-pane-view.ts | 245 +++++++++ ui/components/status-pane-view.ts | 484 ------------------ 5 files changed, 416 insertions(+), 484 deletions(-) rename components/{statusBar => status-bar}/status-bar-controller.ts (100%) rename components/{statusBar => status-bar}/status-bar-view.ts (100%) create mode 100644 components/status-pane-view/status-pane-view-controller.ts create mode 100644 components/status-pane-view/status-pane-view.ts delete mode 100644 ui/components/status-pane-view.ts diff --git a/components/statusBar/status-bar-controller.ts b/components/status-bar/status-bar-controller.ts similarity index 100% rename from components/statusBar/status-bar-controller.ts rename to components/status-bar/status-bar-controller.ts diff --git a/components/statusBar/status-bar-view.ts b/components/status-bar/status-bar-view.ts similarity index 100% rename from components/statusBar/status-bar-view.ts rename to components/status-bar/status-bar-view.ts diff --git a/components/status-pane-view/status-pane-view-controller.ts b/components/status-pane-view/status-pane-view-controller.ts new file mode 100644 index 0000000..99cfb20 --- /dev/null +++ b/components/status-pane-view/status-pane-view-controller.ts @@ -0,0 +1,171 @@ +import { TFile, WorkspaceLeaf, View, Menu, Notice } from 'obsidian'; +import { NoteStatusSettings } from '../../models/types'; +import { StatusService } from '../../services/status-service'; +import NoteStatus from 'main'; +import { StatusPaneView } from './status-pane-view'; + +export class StatusPaneViewController extends View { + private renderer: StatusPaneView; + private settings: NoteStatusSettings; + private statusService: StatusService; + private plugin: NoteStatus; + private searchQuery: string = ''; + private paginationState = { + itemsPerPage: 100, + currentPage: {} as Record + }; + + constructor(leaf: WorkspaceLeaf, plugin: NoteStatus) { + super(leaf); + this.plugin = plugin; + this.settings = plugin.settings; + this.statusService = plugin.statusService; + this.renderer = new StatusPaneView(this.statusService); + } + + getViewType(): string { return 'status-pane'; } + + getDisplayText(): string { return 'Status pane'; } + + getIcon(): string { return 'status-pane'; } + + async onOpen(): Promise { + await this.setupPane(); + } + + onClose(): Promise { + this.containerEl.empty(); + return Promise.resolve(); + } + + + private async setupPane(): Promise { + const { containerEl } = this; + containerEl.empty(); + containerEl.addClass('note-status-pane', 'nav-files-container'); + containerEl.toggleClass('note-status-compact-view', this.settings.compactView); + + this.renderer.createHeader(containerEl, this.settings.compactView, { + onSearch: (query) => { + this.searchQuery = query; + this.renderGroups(query); + }, + onToggleView: () => { + this.settings.compactView = !this.settings.compactView; + containerEl.toggleClass('note-status-compact-view', this.settings.compactView); + window.dispatchEvent(new CustomEvent('note-status:settings-changed')); + this.renderGroups(this.searchQuery); + }, + onRefresh: async () => { + await this.renderGroups(this.searchQuery); + new Notice('Status pane refreshed'); + } + }); + + const groupsContainer = containerEl.createDiv({ cls: 'note-status-groups-container' }); + const loadingIndicator = this.renderer.createLoadingIndicator(groupsContainer); + + setTimeout(async () => { + await this.renderGroups(''); + loadingIndicator.remove(); + }, 10); + } + + private async renderGroups(searchQuery = ''): Promise { + const groupsContainerEl = this.containerEl.querySelector('.note-status-groups-container') as HTMLElement; + if (!groupsContainerEl) return; + + if (searchQuery) { + groupsContainerEl.empty(); + this.renderer.createLoadingIndicator(groupsContainerEl, `Searching for "${searchQuery}"...`); + await new Promise(resolve => setTimeout(resolve, 0)); + } else { + groupsContainerEl.empty(); + } + + const statusGroups = this.getFilteredStatusGroups(searchQuery); + groupsContainerEl.empty(); + + const hasGroups = this.renderer.renderStatusGroups( + groupsContainerEl, + statusGroups, + { + excludeUnknown: this.settings.excludeUnknownStatus, + isCompactView: this.settings.compactView, + collapsedStatuses: this.settings.collapsedStatuses, + pagination: this.paginationState, + callbacks: { + onFileClick: (file) => { + this.app.workspace.openLinkText(file.path, file.path, true); + }, + onStatusToggle: (status, collapsed) => { + this.settings.collapsedStatuses[status] = collapsed; + window.dispatchEvent(new CustomEvent('note-status:settings-changed')); + }, + onContextMenu: (e, file) => { + this.showFileContextMenu(e, file); + }, + onPageChange: (status, page) => { + this.paginationState.currentPage[status] = page; + this.renderGroups(this.searchQuery); + } + } + } + ); + + if (!hasGroups) { + this.renderer.renderEmptyState( + groupsContainerEl, + searchQuery, + this.settings.excludeUnknownStatus, + async () => { + this.settings.excludeUnknownStatus = false; + await this.plugin.saveSettings(); + this.renderGroups(searchQuery); + } + ); + } + } + + private getFilteredStatusGroups(searchQuery = ''): Record { + const rawGroups = this.statusService.groupFilesByStatus(searchQuery); + const filteredGroups: Record = {}; + + Object.entries(rawGroups).forEach(([status, files]) => { + if (files.length > 0) { + filteredGroups[status] = files; + } + }); + + return filteredGroups; + } + + private showFileContextMenu(e: MouseEvent, file: TFile): void { + const menu = new Menu(); + + menu.addItem((item) => + item.setTitle('Change status') + .setIcon('tag') + .onClick(() => { + const position = { x: e.clientX, y: e.clientY }; + this.plugin.statusContextMenu.showForSingleFile(file, position); + }) + ); + + menu.addItem((item) => + item.setTitle('Open in new tab') + .setIcon('lucide-external-link') + .onClick(() => { + this.app.workspace.openLinkText(file.path, file.path, 'tab'); + }) + ); + + menu.showAtMouseEvent(e); + } + + updateSettings(settings: NoteStatusSettings): void { + this.settings = settings; + this.containerEl.toggleClass('note-status-compact-view', settings.compactView); + this.renderGroups(this.searchQuery); + } +} diff --git a/components/status-pane-view/status-pane-view.ts b/components/status-pane-view/status-pane-view.ts new file mode 100644 index 0000000..a11486b --- /dev/null +++ b/components/status-pane-view/status-pane-view.ts @@ -0,0 +1,245 @@ +import { TFile, setIcon } from 'obsidian'; +import { StatusService } from '../../services/status-service'; + +export class StatusPaneView{ + constructor(private statusService: StatusService) {} + + createHeader(container: HTMLElement, isCompactView: boolean, callbacks: { + onSearch: (query: string) => void, + onToggleView: () => void, + onRefresh: () => void + }): HTMLElement { + const header = container.createDiv({ cls: 'note-status-header' }); + + // Search input + const searchContainer = header.createDiv({ cls: 'note-status-search search-input-container' }); + const searchWrapper = searchContainer.createDiv({ cls: 'search-input-wrapper' }); + + const searchIcon = searchWrapper.createEl('span', { cls: 'search-input-icon' }); + setIcon(searchIcon, 'search'); + + const searchInput = searchWrapper.createEl('input', { + type: 'text', + placeholder: 'Search notes...', + cls: 'note-status-search-input search-input' + }); + + const clearBtn = searchWrapper.createEl('span', { cls: 'search-input-clear-button' }); + setIcon(clearBtn, 'x'); + + searchInput.addEventListener('input', () => { + callbacks.onSearch(searchInput.value.toLowerCase()); + clearBtn.toggleClass('is-visible', !!searchInput.value); + }); + + clearBtn.addEventListener('click', () => { + searchInput.value = ''; + callbacks.onSearch(''); + clearBtn.toggleClass('is-visible', false); + }); + + // Action buttons + const actions = header.createDiv({ cls: 'status-pane-actions-container' }); + + const viewBtn = actions.createEl('button', { + type: 'button', + title: isCompactView ? 'Switch to Standard View' : 'Switch to Compact View', + cls: 'note-status-view-toggle clickable-icon' + }); + setIcon(viewBtn, isCompactView ? 'layout' : 'table'); + viewBtn.addEventListener('click', callbacks.onToggleView); + + const refreshBtn = actions.createEl('button', { + type: 'button', + title: 'Refresh statuses', + cls: 'note-status-actions-refresh clickable-icon' + }); + setIcon(refreshBtn, 'refresh-cw'); + refreshBtn.addEventListener('click', callbacks.onRefresh); + + return header; + } + + renderStatusGroups(container: HTMLElement, statusGroups: Record, options: { + excludeUnknown: boolean, + isCompactView: boolean, + collapsedStatuses: Record, + pagination: { + itemsPerPage: number, + currentPage: Record + }, + callbacks: { + onFileClick: (file: TFile) => void, + onStatusToggle: (status: string, collapsed: boolean) => void, + onContextMenu: (e: MouseEvent, file: TFile) => void, + onPageChange: (status: string, page: number) => void + } + }): boolean { + // Clear container first + container.empty(); + + let hasGroups = false; + + Object.entries(statusGroups).forEach(([status, files]) => { + if (files.length > 0 && !(status === 'unknown' && options.excludeUnknown)) { + this.renderGroup(container, status, files, options); + hasGroups = true; + } + }); + + if (!hasGroups) { + return false; + } + + return true; + } + + private renderGroup(container: HTMLElement, status: string, files: TFile[], options: any): void { + const groupEl = container.createDiv({ cls: 'note-status-group nav-folder' }); + const titleEl = groupEl.createDiv({ cls: 'nav-folder-title' }); + const isCollapsed = options.collapsedStatuses[status] ?? false; + + // Collapse indicator + const collapseIcon = titleEl.createDiv({ cls: 'note-status-collapse-indicator' }); + setIcon(collapseIcon, isCollapsed ? 'chevron-right' : 'chevron-down'); + + // Title content + const titleContent = titleEl.createDiv({ cls: 'nav-folder-title-content' }); + const statusIcon = this.statusService.getStatusIcon(status); + titleContent.createSpan({ + text: `${status} ${statusIcon} (${files.length})`, + cls: `status-${status}` + }); + + // Set collapse state + if (isCollapsed) { + groupEl.addClass('note-status-is-collapsed'); + } + + // Toggle collapse on click + titleEl.addEventListener('click', (e) => { + e.preventDefault(); + const newCollapsed = !groupEl.hasClass('note-status-is-collapsed'); + groupEl.toggleClass('note-status-is-collapsed', newCollapsed); + collapseIcon.empty(); + setIcon(collapseIcon, newCollapsed ? 'chevron-right' : 'chevron-down'); + options.callbacks.onStatusToggle(status, newCollapsed); + }); + + // Render content + const childrenEl = groupEl.createDiv({ cls: 'nav-folder-children' }); + + // Pagination + const currentPage = options.pagination.currentPage[status] || 0; + const itemsPerPage = options.pagination.itemsPerPage; + const totalPages = Math.ceil(files.length / itemsPerPage); + const startIndex = currentPage * itemsPerPage; + const endIndex = Math.min(startIndex + itemsPerPage, files.length); + + // File items + files.slice(startIndex, endIndex).forEach(file => { + this.renderFileItem(childrenEl, file, status, options.isCompactView, options.callbacks); + }); + + // Add pagination if needed + if (files.length > itemsPerPage) { + this.addPagination(childrenEl, status, currentPage, totalPages, files.length, options.callbacks); + } + } + + private renderFileItem(container: HTMLElement, file: TFile, status: string, isCompactView: boolean, callbacks: any): void { + const fileEl = container.createDiv({ cls: 'nav-file' }); + const fileTitleEl = fileEl.createDiv({ cls: 'nav-file-title' }); + + if (!isCompactView) { + const fileIcon = fileTitleEl.createDiv({ cls: 'nav-file-icon' }); + setIcon(fileIcon, 'file'); + } + + fileTitleEl.createSpan({ + text: file.basename, + cls: 'nav-file-title-content' + }); + + fileTitleEl.createSpan({ + cls: `note-status-icon nav-file-tag status-${status}`, + text: this.statusService.getStatusIcon(status) + }); + + fileEl.addEventListener('click', (e) => { + e.preventDefault(); + callbacks.onFileClick(file); + }); + + fileEl.addEventListener('contextmenu', (e) => { + e.preventDefault(); + callbacks.onContextMenu(e, file); + }); + } + + private addPagination(container: HTMLElement, status: string, currentPage: number, totalPages: number, totalItems: number, callbacks: any): void { + const paginationEl = container.createDiv({ cls: 'note-status-pagination' }); + + if (currentPage > 0) { + const prevButton = paginationEl.createEl('button', { + text: 'Previous', + cls: 'note-status-pagination-button' + }); + + prevButton.addEventListener('click', (e) => { + e.stopPropagation(); + callbacks.onPageChange(status, currentPage - 1); + }); + } + + paginationEl.createSpan({ + text: `Page ${currentPage + 1} of ${totalPages} (${totalItems} notes)`, + cls: 'note-status-pagination-info' + }); + + if (currentPage < totalPages - 1) { + const nextButton = paginationEl.createEl('button', { + text: 'Next', + cls: 'note-status-pagination-button' + }); + + nextButton.addEventListener('click', (e) => { + e.stopPropagation(); + callbacks.onPageChange(status, currentPage + 1); + }); + } + } + + renderEmptyState(container: HTMLElement, searchQuery: string, excludeUnknown: boolean, onShowUnassigned: () => void): void { + const emptyMessage = container.createDiv({ cls: 'note-status-empty-indicator' }); + + if (searchQuery) { + emptyMessage.textContent = `No notes found matching "${searchQuery}"`; + return; + } + + if (excludeUnknown) { + emptyMessage.createDiv({ + text: 'No notes with status found. Unassigned notes are currently hidden.', + cls: 'note-status-empty-message' + }); + + const btnContainer = emptyMessage.createDiv({ + cls: 'note-status-button-container' + }); + + const showUnknownBtn = btnContainer.createEl('button', { + text: 'Show unassigned notes', + cls: 'note-status-show-unassigned-button' + }); + + showUnknownBtn.addEventListener('click', onShowUnassigned); + } + } + + createLoadingIndicator(container: HTMLElement, text?: string): HTMLElement { + const loadingIndicator = container.createDiv({ cls: 'note-status-loading' }); + loadingIndicator.innerHTML = `${text || 'Loading notes...'}`; + return loadingIndicator; + } +} diff --git a/ui/components/status-pane-view.ts b/ui/components/status-pane-view.ts deleted file mode 100644 index 4d0ef76..0000000 --- a/ui/components/status-pane-view.ts +++ /dev/null @@ -1,484 +0,0 @@ -import { TFile, WorkspaceLeaf, View, Menu, Notice, setIcon } from 'obsidian'; -import { NoteStatusSettings } from '../../models/types'; -import { StatusService } from '../../services/status-service'; -import NoteStatus from 'main'; - -/** - * Status Pane View for managing note statuses - */ -export class StatusPaneView extends View { - plugin: NoteStatus; - searchInput: HTMLInputElement | null = null; - private settings: NoteStatusSettings; - private statusService: StatusService; - private paginationState = { - itemsPerPage: 100, - currentPage: {} as Record - }; - - constructor(leaf: WorkspaceLeaf, plugin: NoteStatus) { - super(leaf); - this.plugin = plugin; - this.settings = plugin.settings; - this.statusService = plugin.statusService; - } - - getViewType(): string { - return 'status-pane'; - } - - getDisplayText(): string { - return 'Status pane'; - } - - getIcon(): string { - return 'status-pane'; - } - - async onOpen(): Promise { - await this.setupPane(); - } - - async setupPane(): Promise { - const { containerEl } = this; - containerEl.empty(); - containerEl.addClass('note-status-pane', 'nav-files-container'); - - this.createHeaderContainer(); - containerEl.toggleClass('note-status-compact-view', this.settings.compactView); - - const groupsContainer = containerEl.createDiv({ - cls: 'note-status-groups-container' - }); - - const loadingIndicator = this.createLoadingIndicator(groupsContainer); - - // Use non-blocking render - setTimeout(async () => { - await this.renderGroups(''); - loadingIndicator.remove(); - }, 10); - } - - /** - * Create header container with search and actions - */ - private createHeaderContainer(): HTMLElement { - const headerContainer = this.containerEl.createDiv({ cls: 'note-status-header' }); - this.createSearchInput(headerContainer); - this.createActionToolbar(headerContainer); - return headerContainer; - } - - /** - * Create loading indicator - */ - private createLoadingIndicator(container: HTMLElement): HTMLElement { - const loadingIndicator = container.createDiv({ cls: 'note-status-loading' }); - loadingIndicator.innerHTML = 'Loading notes...'; - return loadingIndicator; - } - - private createSearchInput(container: HTMLElement): void { - const searchContainer = container.createDiv({ cls: 'note-status-search search-input-container' }); - const searchWrapper = searchContainer.createDiv({ cls: 'search-input-wrapper' }); - - // Search icon - const searchIcon = searchWrapper.createEl('span', { cls: 'search-input-icon' }); - setIcon(searchIcon, 'search'); - - // Create the search input - this.searchInput = searchWrapper.createEl('input', { - type: 'text', - placeholder: 'Search notes...', - cls: 'note-status-search-input search-input' - }); - - this.searchInput.addEventListener('input', () => { - this.renderGroups(this.searchInput!.value.toLowerCase()); - this.toggleClearButton(); - }); - - // Clear search button - const clearSearchBtn = searchWrapper.createEl('span', { cls: 'search-input-clear-button' }); - setIcon(clearSearchBtn, 'x'); - - clearSearchBtn.addEventListener('click', () => { - if (this.searchInput) { - this.searchInput.value = ''; - this.renderGroups(''); - this.toggleClearButton(); - } - }); - } - - private toggleClearButton(): void { - const clearBtn = this.containerEl.querySelector('.search-input-clear-button'); - if (clearBtn && this.searchInput) { - clearBtn.toggleClass('is-visible', !!this.searchInput.value); - } - } - - private createActionToolbar(container: HTMLElement): void { - const actionsContainer = container.createDiv({ cls: 'status-pane-actions-container' }); - - // Toggle compact view button - this.createViewToggleButton(actionsContainer); - - // Refresh button - this.createRefreshButton(actionsContainer); - } - - /** - * Create view toggle button - */ - private createViewToggleButton(container: HTMLElement): void { - const viewToggleButton = container.createEl('button', { - type: 'button', - title: this.settings.compactView ? 'Switch to Standard View' : 'Switch to Compact View', - cls: 'note-status-view-toggle clickable-icon' - }); - - setIcon(viewToggleButton, this.settings.compactView ? 'layout' : 'table'); - - viewToggleButton.addEventListener('click', async () => { - this.settings.compactView = !this.settings.compactView; - viewToggleButton.title = this.settings.compactView ? 'Switch to Standard View' : 'Switch to Compact View'; - viewToggleButton.empty(); - setIcon(viewToggleButton, this.settings.compactView ? 'layout' : 'table'); - - window.dispatchEvent(new CustomEvent('note-status:settings-changed')); - this.containerEl.toggleClass('note-status-compact-view', this.settings.compactView); - await this.renderGroups(this.searchInput?.value.toLowerCase() || ''); - }); - } - - /** - * Create refresh button - */ - private createRefreshButton(container: HTMLElement): void { - const refreshButton = container.createEl('button', { - type: 'button', - title: 'Refresh statuses', - cls: 'note-status-actions-refresh clickable-icon' - }); - - setIcon(refreshButton, 'refresh-cw'); - - refreshButton.addEventListener('click', async () => { - await this.renderGroups(this.searchInput?.value.toLowerCase() || ''); - new Notice('Status pane refreshed'); - }); - } - - async renderGroups(searchQuery = ''): Promise { - const { containerEl } = this; - const groupsContainerEl = containerEl.querySelector('.note-status-groups-container') as HTMLElement; - if (!groupsContainerEl) return; - - if (searchQuery) { - this.showSearchLoadingIndicator(groupsContainerEl, searchQuery); - await this.waitForNextFrame(); - } else { - groupsContainerEl.empty(); - } - - const statusGroups = this.getFilteredStatusGroups(searchQuery); - this.removeLoadingIndicator(groupsContainerEl); - this.renderStatusGroups(groupsContainerEl, statusGroups); - - // Check if no groups were rendered - if (groupsContainerEl.childElementCount === 0) { - this.showEmptyState(groupsContainerEl, searchQuery); - } - } - - /** - * Show loading indicator for search - */ - private showSearchLoadingIndicator(container: HTMLElement, query: string): void { - container.empty(); - const loadingIndicator = container.createDiv({ cls: 'note-status-loading' }); - loadingIndicator.innerHTML = `Searching for "${query}"...`; - } - - /** - * Wait for next animation frame - */ - private async waitForNextFrame(): Promise { - await new Promise(resolve => setTimeout(resolve, 0)); - } - - /** - * Remove loading indicator if it exists - */ - private removeLoadingIndicator(container: HTMLElement): void { - const loadingIndicator = container.querySelector('.note-status-loading'); - if (loadingIndicator) loadingIndicator.remove(); - } - - /** - * Render all status groups - */ - private renderStatusGroups(container: HTMLElement, statusGroups: Record): void { - Object.entries(statusGroups).forEach(([status, files]) => { - if (files.length > 0) { - // Skip unknown status if setting enabled - if (status === 'unknown' && this.settings.excludeUnknownStatus) return; - this.renderStatusGroup(container, status, files); - } - }); - } - - /** - * Show empty state message - */ - private showEmptyState(container: HTMLElement, searchQuery: string): void { - const emptyMessage = container.createDiv({ cls: 'note-status-empty-indicator' }); - - if (searchQuery) { - emptyMessage.textContent = `No notes found matching "${searchQuery}"`; - } else if (this.settings.excludeUnknownStatus) { - emptyMessage.empty(); - - emptyMessage.createDiv({ - text: 'No notes with status found. Unassigned notes are currently hidden.', - cls: 'note-status-empty-message' - }); - - const btnContainer = emptyMessage.createDiv({ - cls: 'note-status-button-container' - }); - - const showUnknownBtn = btnContainer.createEl('button', { - text: 'Show unassigned notes', - cls: 'note-status-show-unassigned-button' - }); - - showUnknownBtn.addEventListener('click', async () => { - this.settings.excludeUnknownStatus = false; - await this.plugin.saveSettings(); - this.renderGroups(searchQuery); - }); - } - } - - /** - * Get status groups with filtering - */ - private getFilteredStatusGroups(searchQuery = ''): Record { - const rawGroups = this.statusService.groupFilesByStatus(searchQuery); - const filteredGroups: Record = {}; - - Object.entries(rawGroups).forEach(([status, files]) => { - if (files.length > 0 && !(status === 'unknown' && this.settings.excludeUnknownStatus)) { - filteredGroups[status] = files; - } - }); - - return filteredGroups; - } - - private renderStatusGroup(container: HTMLElement, status: string, files: TFile[]): void { - const groupEl = container.createDiv({ cls: 'note-status-group nav-folder' }); - const titleEl = groupEl.createDiv({ cls: 'nav-folder-title' }); - - const collapseContainer = titleEl.createDiv({ cls: 'note-status-collapse-indicator' }); - setIcon(collapseContainer, 'chevron-down'); - - const titleContentContainer = titleEl.createDiv({ cls: 'nav-folder-title-content' }); - - const statusIcon = this.statusService.getStatusIcon(status); - titleContentContainer.createSpan({ - text: `${status} ${statusIcon} (${files.length})`, - cls: `status-${status}` - }); - - this.setupGroupCollapsible(groupEl, collapseContainer, status); - this.renderGroupContent(groupEl, files, status); - } - - /** - * Set up collapsible behavior for a group - */ - private setupGroupCollapsible( - groupEl: HTMLElement, - collapseContainer: HTMLElement, - status: string - ): void { - const isCollapsed = this.settings.collapsedStatuses[status] ?? false; - - if (isCollapsed) { - groupEl.addClass('note-status-is-collapsed'); - collapseContainer.empty(); - setIcon(collapseContainer, 'chevron-right'); - } - - const titleEl = groupEl.querySelector('.nav-folder-title') as HTMLElement; - if (titleEl) { - titleEl.addEventListener('click', (e) => { - e.preventDefault(); - const isCurrentlyCollapsed = groupEl.hasClass('note-status-is-collapsed'); - - groupEl.toggleClass('note-status-is-collapsed', !isCurrentlyCollapsed); - collapseContainer.empty(); - setIcon(collapseContainer, isCurrentlyCollapsed ? 'chevron-down' : 'chevron-right'); - - this.settings.collapsedStatuses[status] = !isCurrentlyCollapsed; - window.dispatchEvent(new CustomEvent('note-status:settings-changed')); - }); - } - } - - /** - * Render the content of a status group - */ - private renderGroupContent(groupEl: HTMLElement, files: TFile[], status: string): void { - const childrenEl = groupEl.createDiv({ cls: 'nav-folder-children' }); - - // Sort files by name - files.sort((a, b) => a.basename.localeCompare(b.basename)); - - // Initialize pagination - if (!this.paginationState.currentPage[status]) { - this.paginationState.currentPage[status] = 0; - } - - const currentPage = this.paginationState.currentPage[status]; - const itemsPerPage = this.paginationState.itemsPerPage; - const totalPages = Math.ceil(files.length / itemsPerPage); - const startIndex = currentPage * itemsPerPage; - const endIndex = Math.min(startIndex + itemsPerPage, files.length); - - // Create file list items for current page - files.slice(startIndex, endIndex).forEach(file => { - this.createFileListItem(childrenEl, file, status); - }); - - // Add pagination if needed - if (files.length > itemsPerPage) { - this.addPaginationControls(childrenEl, status, currentPage, totalPages, files.length); - } - } - - /** - * Add pagination controls to a group - */ - private addPaginationControls( - container: HTMLElement, - status: string, - currentPage: number, - totalPages: number, - totalItems: number - ): void { - const paginationEl = container.createDiv({ cls: 'note-status-pagination' }); - - // Add previous page button if not on first page - if (currentPage > 0) { - const prevButton = paginationEl.createEl('button', { - text: 'Previous', - cls: 'note-status-pagination-button' - }); - - prevButton.addEventListener('click', (e) => { - e.stopPropagation(); - this.paginationState.currentPage[status] = currentPage - 1; - this.renderGroups(this.searchInput?.value.toLowerCase() || ''); - }); - } - - // Add page indicator - paginationEl.createSpan({ - text: `Page ${currentPage + 1} of ${totalPages} (${totalItems} notes)`, - cls: 'note-status-pagination-info' - }); - - // Add next page button if not on last page - if (currentPage < totalPages - 1) { - const nextButton = paginationEl.createEl('button', { - text: 'Next', - cls: 'note-status-pagination-button' - }); - - nextButton.addEventListener('click', (e) => { - e.stopPropagation(); - this.paginationState.currentPage[status] = currentPage + 1; - this.renderGroups(this.searchInput?.value.toLowerCase() || ''); - }); - } - } - - private createFileListItem(container: HTMLElement, file: TFile, status: string): void { - if (!file || !(file instanceof TFile)) return; - - const fileEl = container.createDiv({ cls: 'nav-file' }); - const fileTitleEl = fileEl.createDiv({ cls: 'nav-file-title' }); - - // Add file icon if in standard view - if (!this.settings.compactView) { - const fileIcon = fileTitleEl.createDiv({ cls: 'nav-file-icon' }); - setIcon(fileIcon, 'file'); - } - - // Add file name - fileTitleEl.createSpan({ - text: file.basename, - cls: 'nav-file-title-content' - }); - - // Add status indicator - fileTitleEl.createSpan({ - cls: `note-status-icon nav-file-tag status-${status}`, - text: this.statusService.getStatusIcon(status) - }); - - // Add click handler to open the file - fileEl.addEventListener('click', (e) => { - e.preventDefault(); - this.app.workspace.openLinkText(file.path, file.path, true); - }); - - // Add context menu - fileEl.addEventListener('contextmenu', (e) => { - e.preventDefault(); - this.showFileContextMenu(e, file); - }); - } - - private showFileContextMenu(e: MouseEvent, file: TFile): void { - const menu = new Menu(); - - menu.addItem((item) => - item.setTitle('Change status') - .setIcon('tag') - .onClick(() => { - const position = { x: e.clientX, y: e.clientY }; - this.plugin.statusContextMenu.showForSingleFile(file, position); - }) - ); - - menu.addItem((item) => - item.setTitle('Open in new tab') - .setIcon('lucide-external-link') - .onClick(() => { - this.app.workspace.openLinkText(file.path, file.path, 'tab'); - }) - ); - - menu.showAtMouseEvent(e); - } - - onClose(): Promise { - this.containerEl.empty(); - return Promise.resolve(); - } - - /** - * Update view when settings change - */ - updateSettings(settings: NoteStatusSettings): void { - this.settings = settings; - this.containerEl.toggleClass('note-status-compact-view', settings.compactView); - this.renderGroups(this.searchInput?.value.toLowerCase() || ''); - } -} \ No newline at end of file From 41dd6b9c3fb7b077497e3c4548ccd07639c47a79 Mon Sep 17 00:00:00 2001 From: Aleix Date: Wed, 21 May 2025 11:07:19 +0200 Subject: [PATCH 33/67] chore: rename file and folder --- ...orIntegration.ts => editor-integration.ts} | 0 integrations/editor/index.ts | 4 +- ...rIntegration.ts => toolbar-integration.ts} | 0 ...Integration.ts => explorer-integration.ts} | 0 ...ntegration.ts => file-menu-integration.ts} | 2 +- integrations/explorer/index.ts | 4 +- integrations/metadata-cache/index.ts | 1 + .../metadata-integration.ts} | 2 +- integrations/metadataCache/index.ts | 1 - integrations/workspace/index.ts | 2 +- ...ntegration.ts => workspace-integration.ts} | 2 +- main.ts | 320 +++++++++--------- ui/integrations/explorer-integration.ts | 300 ---------------- 13 files changed, 167 insertions(+), 471 deletions(-) rename integrations/editor/{EditorIntegration.ts => editor-integration.ts} (100%) rename integrations/editor/{ToolbarIntegration.ts => toolbar-integration.ts} (100%) rename integrations/explorer/{ExplorerIntegration.ts => explorer-integration.ts} (100%) rename integrations/explorer/{FileMenuIntegration.ts => file-menu-integration.ts} (97%) create mode 100644 integrations/metadata-cache/index.ts rename integrations/{metadataCache/MetadataIntegration.ts => metadata-cache/metadata-integration.ts} (96%) delete mode 100644 integrations/metadataCache/index.ts rename integrations/workspace/{WorkspaceIntegration.ts => workspace-integration.ts} (98%) delete mode 100644 ui/integrations/explorer-integration.ts diff --git a/integrations/editor/EditorIntegration.ts b/integrations/editor/editor-integration.ts similarity index 100% rename from integrations/editor/EditorIntegration.ts rename to integrations/editor/editor-integration.ts diff --git a/integrations/editor/index.ts b/integrations/editor/index.ts index dc2b9c2..176fa1c 100644 --- a/integrations/editor/index.ts +++ b/integrations/editor/index.ts @@ -1,2 +1,2 @@ -export { EditorIntegration } from './EditorIntegration'; -export { ToolbarIntegration } from './ToolbarIntegration'; \ No newline at end of file +export { EditorIntegration } from './editor-integration'; +export { ToolbarIntegration } from './toolbar-integration'; \ No newline at end of file diff --git a/integrations/editor/ToolbarIntegration.ts b/integrations/editor/toolbar-integration.ts similarity index 100% rename from integrations/editor/ToolbarIntegration.ts rename to integrations/editor/toolbar-integration.ts diff --git a/integrations/explorer/ExplorerIntegration.ts b/integrations/explorer/explorer-integration.ts similarity index 100% rename from integrations/explorer/ExplorerIntegration.ts rename to integrations/explorer/explorer-integration.ts diff --git a/integrations/explorer/FileMenuIntegration.ts b/integrations/explorer/file-menu-integration.ts similarity index 97% rename from integrations/explorer/FileMenuIntegration.ts rename to integrations/explorer/file-menu-integration.ts index ebcae32..ee78b39 100644 --- a/integrations/explorer/FileMenuIntegration.ts +++ b/integrations/explorer/file-menu-integration.ts @@ -1,6 +1,6 @@ import { App, Menu, TFile } from 'obsidian'; import { NoteStatusSettings } from '../../models/types'; -import { ExplorerIntegration } from './ExplorerIntegration'; +import { ExplorerIntegration } from './explorer-integration'; import { StatusService } from 'services/status-service'; /** diff --git a/integrations/explorer/index.ts b/integrations/explorer/index.ts index 63bdead..ab99c21 100644 --- a/integrations/explorer/index.ts +++ b/integrations/explorer/index.ts @@ -1,2 +1,2 @@ -export { ExplorerIntegration } from './ExplorerIntegration'; -export { FileMenuIntegration } from './FileMenuIntegration'; \ No newline at end of file +export { ExplorerIntegration } from './explorer-integration'; +export { FileMenuIntegration } from './file-menu-integration'; \ No newline at end of file diff --git a/integrations/metadata-cache/index.ts b/integrations/metadata-cache/index.ts new file mode 100644 index 0000000..d9eb2a9 --- /dev/null +++ b/integrations/metadata-cache/index.ts @@ -0,0 +1 @@ +export { MetadataIntegration } from './metadata-integration'; \ No newline at end of file diff --git a/integrations/metadataCache/MetadataIntegration.ts b/integrations/metadata-cache/metadata-integration.ts similarity index 96% rename from integrations/metadataCache/MetadataIntegration.ts rename to integrations/metadata-cache/metadata-integration.ts index dfc9c6e..af6c507 100644 --- a/integrations/metadataCache/MetadataIntegration.ts +++ b/integrations/metadata-cache/metadata-integration.ts @@ -1,6 +1,6 @@ import { App, TFile } from 'obsidian'; import { NoteStatusSettings } from '../../models/types'; -import { ExplorerIntegration } from '../explorer/ExplorerIntegration'; +import { ExplorerIntegration } from '../explorer/explorer-integration'; import { StatusService } from 'services/status-service'; /** diff --git a/integrations/metadataCache/index.ts b/integrations/metadataCache/index.ts deleted file mode 100644 index a79d712..0000000 --- a/integrations/metadataCache/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { MetadataIntegration } from './MetadataIntegration'; \ No newline at end of file diff --git a/integrations/workspace/index.ts b/integrations/workspace/index.ts index f3cd419..6591713 100644 --- a/integrations/workspace/index.ts +++ b/integrations/workspace/index.ts @@ -1 +1 @@ -export { WorkspaceIntegration } from './WorkspaceIntegration'; \ No newline at end of file +export { WorkspaceIntegration } from './workspace-integration'; \ No newline at end of file diff --git a/integrations/workspace/WorkspaceIntegration.ts b/integrations/workspace/workspace-integration.ts similarity index 98% rename from integrations/workspace/WorkspaceIntegration.ts rename to integrations/workspace/workspace-integration.ts index d0967c2..68b46d1 100644 --- a/integrations/workspace/WorkspaceIntegration.ts +++ b/integrations/workspace/workspace-integration.ts @@ -1,6 +1,6 @@ import { App, TFile, WorkspaceLeaf } from 'obsidian'; import { NoteStatusSettings } from '../../models/types'; -import { ToolbarIntegration } from '../editor/ToolbarIntegration'; +import { ToolbarIntegration } from '../editor/toolbar-integration'; import { StatusService } from 'services/status-service'; /** diff --git a/main.ts b/main.ts index cc3b562..6ebab03 100644 --- a/main.ts +++ b/main.ts @@ -11,9 +11,8 @@ import { MetadataIntegration } from './integrations/metadataCache'; import { WorkspaceIntegration } from './integrations/workspace'; // Importar componentes UI -import { StatusBarController } from 'components/statusBar/status-bar-controller'; -import { StatusPaneView } from 'ui/components/status-pane-view'; import { NoteStatusSettingTab } from 'settings/settings-tab'; +import { StatusBarController } from 'components/status-bar/status-bar-controller'; export default class NoteStatus extends Plugin { settings: NoteStatusSettings; @@ -38,24 +37,24 @@ export default class NoteStatus extends Plugin { await this.loadSettings(); // 1. Inicializar servicios básicos - this.initializeServices(); + // this.initializeServices(); // 2. Registrar vistas y componentes UI - this.registerViews(); + // this.registerViews(); // 3. Inicializar integraciones - this.initializeIntegrations(); + // this.initializeIntegrations(); // 4. Registrar comandos - this.registerCommands(); + // this.registerCommands(); // 5. Registrar eventos personalizados - this.setupCustomEvents(); + // this.setupCustomEvents(); // 6. Esperar a que el layout esté listo para inicializar UI - this.app.workspace.onLayoutReady(() => { - this.initializeUI(); - }); + // this.app.workspace.onLayoutReady(() => { + // this.initializeUI(); + // }); } catch (error) { console.error('Error loading Note Status plugin:', error); @@ -68,227 +67,224 @@ export default class NoteStatus extends Plugin { } private initializeServices() { - this.statusService = new StatusService(this.app, this.settings); - this.styleService = new StyleService(this.settings); + // this.statusService = new StatusService(this.app, this.settings); + // this.styleService = new StyleService(this.settings); } private registerViews() { // Registrar vista del panel de estado - this.registerView('status-pane', (leaf) => { - return new StatusPaneView(leaf, this); - }); + // this.registerView('status-pane', (leaf) => { + // return new StatusPaneView(leaf, this); + // }); // Añadir ícono en la barra lateral - this.addRibbonIcon('status-pane', 'Open status pane', () => { - this.openStatusPane(); - }); + // this.addRibbonIcon('status-pane', 'Open status pane', () => { + // this.openStatusPane(); + // }); // Añadir pestaña de configuración - this.addSettingTab(new NoteStatusSettingTab(this.app, this, this.statusService)); + // this.addSettingTab(new NoteStatusSettingTab(this.app, this, this.statusService)); } private initializeIntegrations() { // Crear integraciones en orden de dependencia - // 1. Integraciones básicas primero - this.explorerIntegration = new ExplorerIntegration(this.app, this.settings, this.statusService); - this.toolbarIntegration = new ToolbarIntegration(this.app, this.settings, this.statusService); + // // 1. Integraciones básicas primero + // this.explorerIntegration = new ExplorerIntegration(this.app, this.settings, this.statusService); + // this.toolbarIntegration = new ToolbarIntegration(this.app, this.settings, this.statusService); - // 2. Integraciones que dependen de otras - this.fileMenuIntegration = new FileMenuIntegration( - this.app, - this.settings, - this.statusService, - this.explorerIntegration - ); + // // 2. Integraciones que dependen de otras + // this.fileMenuIntegration = new FileMenuIntegration( + // this.app, + // this.settings, + // this.statusService, + // this.explorerIntegration + // ); - this.editorIntegration = new EditorIntegration(this.app, this.settings, this.statusService); + // this.editorIntegration = new EditorIntegration(this.app, this.settings, this.statusService); - this.metadataIntegration = new MetadataIntegration( - this.app, - this.settings, - this.statusService, - this.explorerIntegration - ); + // this.metadataIntegration = new MetadataIntegration( + // this.app, + // this.settings, + // this.statusService, + // this.explorerIntegration + // ); - this.workspaceIntegration = new WorkspaceIntegration( - this.app, - this.settings, - this.statusService, - this.toolbarIntegration - ); + // this.workspaceIntegration = new WorkspaceIntegration( + // this.app, + // this.settings, + // this.statusService, + // this.toolbarIntegration + // ); - // 3. Registrar eventos en cada integración - this.fileMenuIntegration.registerFileMenus(); - this.editorIntegration.registerEditorMenus(); - this.metadataIntegration.registerMetadataEvents(); - this.workspaceIntegration.registerWorkspaceEvents(); + // // 3. Registrar eventos en cada integración + // this.fileMenuIntegration.registerFileMenus(); + // this.editorIntegration.registerEditorMenus(); + // this.metadataIntegration.registerMetadataEvents(); + // this.workspaceIntegration.registerWorkspaceEvents(); } private registerCommands() { - // Comando para actualizar estado - this.addCommand({ - id: 'refresh-status', - name: 'Refresh status', - callback: () => { - this.refreshStatus(); - new Notice('Note status refreshed!'); - } - }); + // // Comando para actualizar estado + // this.addCommand({ + // id: 'refresh-status', + // name: 'Refresh status', + // callback: () => { + // this.refreshStatus(); + // new Notice('Note status refreshed!'); + // } + // }); - // Comando para forzar actualización de UI - this.addCommand({ - id: 'force-refresh-ui', - name: 'Force refresh user interface', - callback: () => this.forceRefreshUI() - }); + // // Comando para forzar actualización de UI + // this.addCommand({ + // id: 'force-refresh-ui', + // name: 'Force refresh user interface', + // callback: () => this.forceRefreshUI() + // }); - // Comando para insertar metadatos de estado - this.addCommand({ - id: 'insert-status-metadata', - name: 'Insert status metadata', - editorCallback: (editor) => { - this.editorIntegration.insertStatusMetadata(editor); - new Notice('Status metadata inserted'); - } - }); + // // Comando para insertar metadatos de estado + // this.addCommand({ + // id: 'insert-status-metadata', + // name: 'Insert status metadata', + // editorCallback: (editor) => { + // this.editorIntegration.insertStatusMetadata(editor); + // new Notice('Status metadata inserted'); + // } + // }); - // Comando para abrir panel de estado - this.addCommand({ - id: 'open-status-pane', - name: 'Open status pane', - callback: () => this.openStatusPane() - }); + // // Comando para abrir panel de estado + // this.addCommand({ + // id: 'open-status-pane', + // name: 'Open status pane', + // callback: () => this.openStatusPane() + // }); } private setupCustomEvents() { - // Evento para cambios de configuración - window.addEventListener('note-status:settings-changed', this.saveSettings.bind(this)); + // // Evento para cambios de configuración + // window.addEventListener('note-status:settings-changed', this.saveSettings.bind(this)); - // Evento para cambios de estado - window.addEventListener('note-status:status-changed', this.handleStatusChanged.bind(this)); + // // Evento para cambios de estado + // window.addEventListener('note-status:status-changed', this.handleStatusChanged.bind(this)); - // Evento para actualización de UI - window.addEventListener('note-status:refresh-ui', this.refreshUI.bind(this)); + // // Evento para actualización de UI + // window.addEventListener('note-status:refresh-ui', this.refreshUI.bind(this)); } private initializeUI() { - // Inicializar barra de estado - this.statusBarController = new StatusBarController( - this.addStatusBarItem(), - this.settings, - this.statusService - ); + // // Inicializar barra de estado + // this.statusBarController = new StatusBarController( + // this.addStatusBarItem(), + // this.settings, + // this.statusService + // ); - // Inicializar iconos del explorador (con retraso para evitar ralentizar el inicio) - if (this.settings.showStatusIconsInExplorer) { - setTimeout(() => { - this.explorerIntegration.updateAllFileExplorerIcons(); - }, 2000); - } + // // Inicializar iconos del explorador (con retraso para evitar ralentizar el inicio) + // if (this.settings.showStatusIconsInExplorer) { + // setTimeout(() => { + // this.explorerIntegration.updateAllFileExplorerIcons(); + // }, 2000); + // } } private handleStatusChanged(event: CustomEvent) { - const { statuses, file } = event.detail; + // const { statuses, file } = event.detail; - // Actualizar barra de estado - this.statusBarController.update(statuses); + // // Actualizar barra de estado + // this.statusBarController.update(statuses); - // Actualizar toolbar - this.toolbarIntegration.updateStatusDisplay(statuses); + // // Actualizar toolbar + // this.toolbarIntegration.updateStatusDisplay(statuses); - // Actualizar explorador si es necesario - if (file) { - const fileObj = this.app.vault.getFileByPath(file); - if (fileObj) { - this.explorerIntegration.updateFileExplorerIcons(fileObj); - } - } + // // Actualizar explorador si es necesario + // if (file) { + // const fileObj = this.app.vault.getFileByPath(file); + // if (fileObj) { + // this.explorerIntegration.updateFileExplorerIcons(fileObj); + // } + // } } private refreshStatus() { - const activeFile = this.app.workspace.getActiveFile(); - if (activeFile) { - const statuses = this.statusService.getFileStatuses(activeFile); - window.dispatchEvent(new CustomEvent('note-status:status-changed', { - detail: { statuses, file: activeFile.path } - })); - } + // const activeFile = this.app.workspace.getActiveFile(); + // if (activeFile) { + // const statuses = this.statusService.getFileStatuses(activeFile); + // window.dispatchEvent(new CustomEvent('note-status:status-changed', { + // detail: { statuses, file: activeFile.path } + // })); + // } } private refreshUI() { - this.refreshStatus(); - - // Actualizar otros componentes según sea necesario - // ... + // this.refreshStatus(); } private forceRefreshUI() { - // Actualizar todos los elementos visibles - this.refreshStatus(); + // // Actualizar todos los elementos visibles + // this.refreshStatus(); - if (this.settings.showStatusIconsInExplorer) { - this.explorerIntegration.updateAllFileExplorerIcons(); - } + // if (this.settings.showStatusIconsInExplorer) { + // this.explorerIntegration.updateAllFileExplorerIcons(); + // } - window.dispatchEvent(new CustomEvent('note-status:update-pane')); + // window.dispatchEvent(new CustomEvent('note-status:update-pane')); - new Notice('UI forcefully refreshed'); + // new Notice('UI forcefully refreshed'); } private async openStatusPane() { - // Comprobar si ya está abierto - const existing = this.app.workspace.getLeavesOfType('status-pane')[0]; - if (existing) { - this.app.workspace.setActiveLeaf(existing); - return; - } + // // Comprobar si ya está abierto + // const existing = this.app.workspace.getLeavesOfType('status-pane')[0]; + // if (existing) { + // this.app.workspace.setActiveLeaf(existing); + // return; + // } - // Crear una nueva hoja - const leaf = this.app.workspace.getLeftLeaf(false); - if (leaf) { - await leaf.setViewState({ type: 'status-pane', active: true }); - } + // // Crear una nueva hoja + // const leaf = this.app.workspace.getLeftLeaf(false); + // if (leaf) { + // await leaf.setViewState({ type: 'status-pane', active: true }); + // } } async saveSettings() { - await this.saveData(this.settings); + // await this.saveData(this.settings); - // Actualizar servicios - this.statusService.updateSettings(this.settings); - this.styleService.updateSettings(this.settings); + // // Actualizar servicios + // this.statusService.updateSettings(this.settings); + // this.styleService.updateSettings(this.settings); - // Actualizar integraciones - this.updateIntegrationsSettings(); + // // Actualizar integraciones + // this.updateIntegrationsSettings(); } private updateIntegrationsSettings() { - // Actualizar configuración en todas las integraciones - this.explorerIntegration.updateSettings(this.settings); - this.fileMenuIntegration.updateSettings(this.settings); - this.editorIntegration.updateSettings(this.settings); - this.toolbarIntegration.updateSettings(this.settings); - this.metadataIntegration.updateSettings(this.settings); - this.workspaceIntegration.updateSettings(this.settings); + // // Actualizar configuración en todas las integraciones + // this.explorerIntegration.updateSettings(this.settings); + // this.fileMenuIntegration.updateSettings(this.settings); + // this.editorIntegration.updateSettings(this.settings); + // this.toolbarIntegration.updateSettings(this.settings); + // this.metadataIntegration.updateSettings(this.settings); + // this.workspaceIntegration.updateSettings(this.settings); - // Actualizar componentes UI - this.statusBarController.updateSettings(this.settings); + // // Actualizar componentes UI + // this.statusBarController.updateSettings(this.settings); } onunload() { - // Limpiar eventos personalizados - window.removeEventListener('note-status:settings-changed', this.saveSettings.bind(this)); - window.removeEventListener('note-status:status-changed', this.handleStatusChanged.bind(this)); - window.removeEventListener('note-status:refresh-ui', this.refreshUI.bind(this)); + // // Limpiar eventos personalizados + // window.removeEventListener('note-status:settings-changed', this.saveSettings.bind(this)); + // window.removeEventListener('note-status:status-changed', this.handleStatusChanged.bind(this)); + // window.removeEventListener('note-status:refresh-ui', this.refreshUI.bind(this)); - // Limpiar integraciones - this.explorerIntegration.unload(); - this.toolbarIntegration.unload(); + // // Limpiar integraciones + // this.explorerIntegration.unload(); + // this.toolbarIntegration.unload(); - // Limpiar servicios - this.styleService.unload(); + // // Limpiar servicios + // this.styleService.unload(); - // Limpiar componentes UI - this.statusBarController.unload(); + // // Limpiar componentes UI + // this.statusBarController.unload(); } } \ No newline at end of file diff --git a/ui/integrations/explorer-integration.ts b/ui/integrations/explorer-integration.ts deleted file mode 100644 index c139fc4..0000000 --- a/ui/integrations/explorer-integration.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { App, TFile, debounce, setTooltip } from 'obsidian'; -import { FileExplorerView, NoteStatusSettings } from '../../models/types'; -import { StatusService } from '../../services/status-service'; - -/** - * Enhanced file explorer integration for status icons - */ -export class ExplorerIntegration { - private app: App; - private settings: NoteStatusSettings; - private statusService: StatusService; - private iconUpdateQueue = new Set(); - private isProcessingQueue = false; - - // Define with correct typing to match Obsidian's debounce return type - private debouncedUpdateAll: { - (): void; - cancel: () => void; - }; - - constructor(app: App, settings: NoteStatusSettings, statusService: StatusService) { - this.app = app; - this.settings = settings; - this.statusService = statusService; - this.debouncedUpdateAll = debounce( - () => this.processUpdateQueue(), - 100, - true - ); - } - - /** - * Update settings and refresh UI as needed - */ - public updateSettings(settings: NoteStatusSettings): void { - const shouldRefreshIcons = - this.settings.showStatusIconsInExplorer !== settings.showStatusIconsInExplorer || - this.settings.hideUnknownStatusInExplorer !== settings.hideUnknownStatusInExplorer; - - this.settings = settings; - - if (shouldRefreshIcons) { - this.removeAllFileExplorerIcons(); - - if (settings.showStatusIconsInExplorer) { - setTimeout(() => this.updateAllFileExplorerIcons(), 50); - } - } else if (settings.showStatusIconsInExplorer) { - this.updateAllFileExplorerIcons(); - } - } - - /** - * Find the file explorer view - */ - private findFileExplorerView(): FileExplorerView | null { - const leaf = this.app.workspace.getLeavesOfType('file-explorer')[0]; - if (leaf?.view) return leaf.view as FileExplorerView; - - for (const leaf of this.app.workspace.getLeavesOfType('')) { - if (leaf.view && 'fileItems' in leaf.view) { - return leaf.view as FileExplorerView; - } - } - - return null; - } - - /** - * Queue a file for icon update - */ - public queueFileUpdate(file: TFile): void { - if (!this.settings.showStatusIconsInExplorer || file.extension !== 'md') return; - - this.iconUpdateQueue.add(file.path); - this.debouncedUpdateAll(); - } - - /** - * Process the queue of files that need icon updates - */ - private async processUpdateQueue(): Promise { - if (this.isProcessingQueue || this.iconUpdateQueue.size === 0) return; - - this.isProcessingQueue = true; - - try { - const fileExplorerView = this.findFileExplorerView(); - if (!fileExplorerView) { - setTimeout(() => this.debouncedUpdateAll(), 200); - return; - } - - const allPaths = Array.from(this.iconUpdateQueue); - const batchSize = 50; - - for (let i = 0; i < allPaths.length; i += batchSize) { - await this.processBatch(allPaths.slice(i, i + batchSize), fileExplorerView); - - if (i + batchSize < allPaths.length) { - await new Promise(resolve => setTimeout(resolve, 0)); - } - } - } catch (error) { - console.error('Note Status: Error processing file update queue', error); - } finally { - this.isProcessingQueue = false; - - if (this.iconUpdateQueue.size > 0) { - this.debouncedUpdateAll(); - } - } - } - - /** - * Process a batch of files - */ - private async processBatch(paths: string[], fileExplorerView: FileExplorerView): Promise { - for (const path of paths) { - const file = this.app.vault.getFileByPath(path); - if (file instanceof TFile) { - this.updateSingleFileIcon(file, fileExplorerView); - } - this.iconUpdateQueue.delete(path); - } - } - - /** - * Update a single file's icon in the explorer - */ - private updateSingleFileIcon(file: TFile, fileExplorerView: FileExplorerView): void { - if (!this.settings.showStatusIconsInExplorer || file.extension !== 'md') return; - - try { - const fileItem = fileExplorerView.fileItems[file.path]; - if (!fileItem) return; - - const titleEl = fileItem.titleEl || fileItem.selfEl; - if (!titleEl) return; - - const statuses = this.statusService.getFileStatuses(file); - - this.removeExistingIcons(titleEl); - - if (this.shouldSkipIcon(statuses)) return; - - this.addStatusIcons(titleEl, statuses); - } catch (error) { - console.error(`Note Status: Error updating icon for ${file.path}`, error); - } - } - - /** - * Check if icon should be skipped - */ - private shouldSkipIcon(statuses: string[]): boolean { - return this.settings.hideUnknownStatusInExplorer && - statuses.length === 1 && - statuses[0] === 'unknown'; - } - - /** - * Remove existing status icons - */ - private removeExistingIcons(element: HTMLElement): void { - const iconSelectors = '.note-status-icon, .note-status-icon-container'; - element.querySelectorAll(iconSelectors).forEach(icon => { - if (icon.classList.contains('note-status-icon') || - icon.classList.contains('note-status-icon-container')) { - icon.remove(); - } - }); - } - - /** - * Add status icons to an element - */ - private addStatusIcons(titleEl: HTMLElement, statuses: string[]): void { - const iconContainer = document.createElement('span'); - iconContainer.className = 'note-status-icon-container'; - - if (this.settings.useMultipleStatuses && statuses.length > 0 && statuses[0] !== 'unknown') { - statuses.forEach(status => this.addSingleStatusIcon(iconContainer, status)); - } else { - const primaryStatus = statuses[0] || 'unknown'; - if (primaryStatus !== 'unknown' || !this.settings.hideUnknownStatusInExplorer) { - this.addSingleStatusIcon(iconContainer, primaryStatus); - } - } - - if (iconContainer.childElementCount > 0) { - titleEl.appendChild(iconContainer); - } - } - - /** - * Add a single status icon - */ - private addSingleStatusIcon(container: HTMLElement, status: string): void { - const iconEl = document.createElement('span'); - iconEl.className = `note-status-icon nav-file-tag status-${status}`; - iconEl.textContent = this.statusService.getStatusIcon(status); - - const statusObj = this.statusService.getAllStatuses().find(s => s.name === status); - const tooltipValue = statusObj?.description ? `${status} - ${statusObj.description}`: status; - setTooltip(iconEl, tooltipValue); - - container.appendChild(iconEl); - } - - /** - * Update file explorer icons for a file - */ - public updateFileExplorerIcons(file: TFile): void { - if (!file || !this.settings.showStatusIconsInExplorer || file.extension !== 'md') return; - - const activeFile = this.app.workspace.getActiveFile(); - if (activeFile?.path === file.path) { - this.updateSingleFileIconDirectly(file); - } - - this.queueFileUpdate(file); - } - - /** - * Update active file icon directly - */ - private updateSingleFileIconDirectly(file: TFile): void { - const fileExplorer = this.findFileExplorerView(); - if (fileExplorer) { - this.updateSingleFileIcon(file, fileExplorer); - } - } - - /** - * Update all file icons in explorer - */ - public updateAllFileExplorerIcons(): void { - if (!this.settings.showStatusIconsInExplorer) { - this.removeAllFileExplorerIcons(); - return; - } - - this.processFilesInBatches(); - } - - /** - * Process files in batches - */ - private async processFilesInBatches(): Promise { - const files = this.app.vault.getMarkdownFiles(); - const batchSize = 100; - - for (let i = 0; i < files.length; i += batchSize) { - files.slice(i, i + batchSize).forEach(file => this.queueFileUpdate(file)); - - if (i + batchSize < files.length) { - await new Promise(resolve => setTimeout(resolve, 0)); - } - } - } - - /** - * Remove all status icons - */ - public removeAllFileExplorerIcons(): void { - const fileExplorer = this.findFileExplorerView(); - if (!fileExplorer?.fileItems) return; - - Object.values(fileExplorer.fileItems).forEach(fileItem => { - const titleEl = fileItem.titleEl || fileItem.selfEl; - if (titleEl) this.removeExistingIcons(titleEl); - }); - - this.iconUpdateQueue.clear(); - } - - /** - * Get selected files from explorer - */ - public getSelectedFiles(): TFile[] { - const fileExplorer = this.findFileExplorerView(); - if (!fileExplorer?.fileItems) return []; - - return Object.values(fileExplorer.fileItems) - .filter(item => - item.el?.classList.contains('is-selected') && - item.file instanceof TFile && - item.file.extension === 'md') - .map(item => item.file as TFile); - } - - /** - * Clean up on unload - */ - public unload(): void { - this.removeAllFileExplorerIcons(); - this.debouncedUpdateAll.cancel(); - } -} \ No newline at end of file From 8b066f0fc21d840b7b074418acfd4abbbeecc331 Mon Sep 17 00:00:00 2001 From: Aleix Date: Wed, 21 May 2025 12:37:30 +0200 Subject: [PATCH 34/67] chore: status bar restored --- components/status-bar/index.ts | 4 ++ .../status-bar/status-bar-controller.ts | 45 +++++-------- components/status-dropdown/index.ts | 5 +- .../context-menu}/status-context-menu.ts | 4 +- .../settings}/settings-tab.ts | 4 +- .../workspace/workspace-integration.ts | 59 ++++++---------- main.ts | 67 +++++++++---------- services/status-service.ts | 10 +-- services/style-service.ts | 28 ++++---- .../status-pane-view-controller.ts | 0 .../status-pane-view/status-pane-view.ts | 0 11 files changed, 93 insertions(+), 133 deletions(-) create mode 100644 components/status-bar/index.ts rename {ui/menus => integrations/context-menu}/status-context-menu.ts (94%) rename {settings => integrations/settings}/settings-tab.ts (98%) rename {components => views}/status-pane-view/status-pane-view-controller.ts (100%) rename {components => views}/status-pane-view/status-pane-view.ts (100%) diff --git a/components/status-bar/index.ts b/components/status-bar/index.ts new file mode 100644 index 0000000..64d38cb --- /dev/null +++ b/components/status-bar/index.ts @@ -0,0 +1,4 @@ +import { StatusBarController } from './status-bar-controller'; + +export { StatusBarController as StatusBar}; +export default StatusBarController; \ No newline at end of file diff --git a/components/status-bar/status-bar-controller.ts b/components/status-bar/status-bar-controller.ts index 72098bf..c6838f2 100644 --- a/components/status-bar/status-bar-controller.ts +++ b/components/status-bar/status-bar-controller.ts @@ -11,27 +11,14 @@ export class StatusBarController { private statusService: StatusService; private currentStatuses: string[] = ['unknown']; - constructor(statusBarEl: HTMLElement, settings: NoteStatusSettings, statusService: StatusService) { - this.view = new StatusBarView(statusBarEl); + constructor(statusBarContainer: HTMLElement, settings: NoteStatusSettings, statusService: StatusService) { + this.view = new StatusBarView(statusBarContainer); this.settings = settings; this.statusService = statusService; - // Register right-click handler for force refresh - this.setupContextMenu(statusBarEl); - this.update(['unknown']); } - /** - * Set up right-click context menu for force refresh - */ - private setupContextMenu(element: HTMLElement): void { - element.addEventListener('contextmenu', (e) => { - e.preventDefault(); - window.dispatchEvent(new CustomEvent('note-status:force-refresh')); - }); - } - /** * Update the status bar with new statuses */ @@ -60,21 +47,21 @@ export class StatusBarController { this.handleAutoHide(); } - /** - * Render statuses - handles both single and multiple status cases - */ - private renderStatuses(statuses: string[]): void { - const statusDetails = statuses.map(status => { - const statusObj = this.statusService.getAllStatuses().find(s => s.name === status); - return { - name: status, - icon: this.statusService.getStatusIcon(status), - tooltipText: statusObj?.description ? `${status} - ${statusObj.description}` : status - }; - }); + /** + * Render statuses - handles both single and multiple status cases + */ + private renderStatuses(statuses: string[]): void { + const statusDetails = statuses.map(status => { + const statusObj = this.statusService.getAllStatuses().find(s => s.name === status); + return { + name: status, + icon: this.statusService.getStatusIcon(status), + tooltipText: statusObj?.description ? `${status} - ${statusObj.description}` : status + }; + }); - this.view.renderStatuses(statusDetails); - } + this.view.renderStatuses(statusDetails); + } /** * Handle auto-hide behavior diff --git a/components/status-dropdown/index.ts b/components/status-dropdown/index.ts index 46ad9bc..2e49ef2 100644 --- a/components/status-dropdown/index.ts +++ b/components/status-dropdown/index.ts @@ -1,9 +1,6 @@ import { DropdownManager } from './dropdown-manager'; import { DropdownOptions } from './types'; -// Re-export for public use -export { DropdownManager as StatusDropdown }; export type { DropdownOptions }; - -// Export default to maintain compatibility with existing code +export { DropdownManager as StatusDropdown }; export default DropdownManager; \ No newline at end of file diff --git a/ui/menus/status-context-menu.ts b/integrations/context-menu/status-context-menu.ts similarity index 94% rename from ui/menus/status-context-menu.ts rename to integrations/context-menu/status-context-menu.ts index e0d3ade..7367bd3 100644 --- a/ui/menus/status-context-menu.ts +++ b/integrations/context-menu/status-context-menu.ts @@ -1,8 +1,8 @@ import { App, Menu, TFile } from 'obsidian'; import { NoteStatusSettings } from '../../models/types'; import { StatusService } from '../../services/status-service'; -import { StatusDropdown } from '../components/status-dropdown'; -import { ExplorerIntegration } from '../integrations/explorer-integration'; +import { StatusDropdown } from 'components/status-dropdown'; +import { ExplorerIntegration } from 'integrations/explorer'; /** * Handles context menu interactions for status changes diff --git a/settings/settings-tab.ts b/integrations/settings/settings-tab.ts similarity index 98% rename from settings/settings-tab.ts rename to integrations/settings/settings-tab.ts index 399676d..d34334f 100644 --- a/settings/settings-tab.ts +++ b/integrations/settings/settings-tab.ts @@ -1,6 +1,6 @@ import { App, PluginSettingTab, Setting, Notice } from 'obsidian'; -import { Status } from '../models/types'; -import { PREDEFINED_TEMPLATES } from '../constants/status-templates'; +import { Status } from '../../models/types'; +import { PREDEFINED_TEMPLATES } from '../../constants/status-templates'; import NoteStatus from 'main'; import { StatusService } from 'services/status-service'; diff --git a/integrations/workspace/workspace-integration.ts b/integrations/workspace/workspace-integration.ts index 68b46d1..ddd8705 100644 --- a/integrations/workspace/workspace-integration.ts +++ b/integrations/workspace/workspace-integration.ts @@ -59,68 +59,51 @@ export class WorkspaceIntegration { */ private handleFileOpen(file: TFile): void { // Añade el botón de la barra de herramientas - this.toolbarIntegration.addToolbarButtonToActiveLeaf(); + // this.toolbarIntegration.addToolbarButtonToActiveLeaf(); // Actualiza estado - this.checkNoteStatus(); + this.propagateNoteStatusChange(); } /** * Maneja cambio de hoja activa */ private handleActiveLeafChange(leaf: WorkspaceLeaf): void { - // Añade el botón de la barra de herramientas - this.toolbarIntegration.addToolbarButtonToActiveLeaf(); + // // Añade el botón de la barra de herramientas + // this.toolbarIntegration.addToolbarButtonToActiveLeaf(); - const activeFile = this.app.workspace.getActiveFile(); + // const activeFile = this.app.workspace.getActiveFile(); - // Solo actualiza si el archivo realmente cambió - if (this.lastActiveFile?.path !== activeFile?.path) { - this.lastActiveFile = activeFile; - this.checkNoteStatus(); - } + // // Solo actualiza si el archivo realmente cambió + // if (this.lastActiveFile?.path !== activeFile?.path) { + // this.lastActiveFile = activeFile; + // this.propagateNoteStatusChange(); + // } } /** - * Maneja cambio de layout + * Maneja y propaga el cambio de layout */ private handleLayoutChange(): void { - // Actualiza componentes que dependen del layout - this.updateStatusPane(); + window.dispatchEvent(new CustomEvent('note-status:update-pane')); } /** - * Verifica y actualiza el estado de la nota activa + * Verifica y propaga el estado de la nota activa */ - private checkNoteStatus(): void { + private propagateNoteStatusChange(): void { try { const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile || activeFile.extension !== 'md') { - this.updateStatusComponents(['unknown']); - return; + let fileStatuses: string[] = []; + if (activeFile && activeFile.extension === 'md') { + fileStatuses = this.statusService.getFileStatuses(activeFile) } - - const statuses = this.statusService.getFileStatuses(activeFile); - this.updateStatusComponents(statuses); + // Dispara evento para que otros componentes se actualicen + window.dispatchEvent(new CustomEvent('note-status:status-changed', { + detail: { statuses: fileStatuses } + })); } catch (error) { console.error('Error checking note status:', error); } } - - /** - * Actualiza componentes de estado con nuevos estados - */ - private updateStatusComponents(statuses: string[]): void { - // Dispara evento para que otros componentes se actualicen - window.dispatchEvent(new CustomEvent('note-status:status-changed', { - detail: { statuses } - })); - } - - /** - * Actualiza el panel de estado - */ - private updateStatusPane(): void { - window.dispatchEvent(new CustomEvent('note-status:update-pane')); - } } \ No newline at end of file diff --git a/main.ts b/main.ts index 6ebab03..975cab5 100644 --- a/main.ts +++ b/main.ts @@ -7,12 +7,12 @@ import { StyleService } from 'services/style-service'; // Importar integraciones import { ExplorerIntegration, FileMenuIntegration } from './integrations/explorer'; import { EditorIntegration, ToolbarIntegration } from './integrations/editor'; -import { MetadataIntegration } from './integrations/metadataCache'; +import { MetadataIntegration } from './integrations/metadata-cache'; import { WorkspaceIntegration } from './integrations/workspace'; // Importar componentes UI -import { NoteStatusSettingTab } from 'settings/settings-tab'; -import { StatusBarController } from 'components/status-bar/status-bar-controller'; +import { NoteStatusSettingTab } from 'integrations/settings/settings-tab'; +import { StatusBar } from 'components/status-bar'; export default class NoteStatus extends Plugin { settings: NoteStatusSettings; @@ -22,7 +22,7 @@ export default class NoteStatus extends Plugin { styleService: StyleService; // Componentes UI - statusBarController: StatusBarController; + statusBar: StatusBar; // Integraciones explorerIntegration: ExplorerIntegration; @@ -35,26 +35,22 @@ export default class NoteStatus extends Plugin { async onload() { try { await this.loadSettings(); - - // 1. Inicializar servicios básicos - // this.initializeServices(); + this.initializeServices(); // 2. Registrar vistas y componentes UI // this.registerViews(); + this.initializeUI(); + // 3. Inicializar integraciones - // this.initializeIntegrations(); + this.initializeIntegrations(); // 4. Registrar comandos // this.registerCommands(); // 5. Registrar eventos personalizados - // this.setupCustomEvents(); + this.setupCustomEvents(); - // 6. Esperar a que el layout esté listo para inicializar UI - // this.app.workspace.onLayoutReady(() => { - // this.initializeUI(); - // }); } catch (error) { console.error('Error loading Note Status plugin:', error); @@ -67,8 +63,8 @@ export default class NoteStatus extends Plugin { } private initializeServices() { - // this.statusService = new StatusService(this.app, this.settings); - // this.styleService = new StyleService(this.settings); + this.statusService = new StatusService(this.app, this.settings); + this.styleService = new StyleService(this.settings); } private registerViews() { @@ -110,18 +106,18 @@ export default class NoteStatus extends Plugin { // this.explorerIntegration // ); - // this.workspaceIntegration = new WorkspaceIntegration( - // this.app, - // this.settings, - // this.statusService, - // this.toolbarIntegration - // ); + this.workspaceIntegration = new WorkspaceIntegration( + this.app, + this.settings, + this.statusService, + this.toolbarIntegration + ); // // 3. Registrar eventos en cada integración // this.fileMenuIntegration.registerFileMenus(); // this.editorIntegration.registerEditorMenus(); // this.metadataIntegration.registerMetadataEvents(); - // this.workspaceIntegration.registerWorkspaceEvents(); + this.workspaceIntegration.registerWorkspaceEvents(); } private registerCommands() { @@ -164,20 +160,20 @@ export default class NoteStatus extends Plugin { // // Evento para cambios de configuración // window.addEventListener('note-status:settings-changed', this.saveSettings.bind(this)); - // // Evento para cambios de estado - // window.addEventListener('note-status:status-changed', this.handleStatusChanged.bind(this)); + // Evento para cambios de estado + window.addEventListener('note-status:status-changed', this.handleStatusChanged.bind(this)); // // Evento para actualización de UI // window.addEventListener('note-status:refresh-ui', this.refreshUI.bind(this)); } private initializeUI() { - // // Inicializar barra de estado - // this.statusBarController = new StatusBarController( - // this.addStatusBarItem(), - // this.settings, - // this.statusService - // ); + // Inicializar barra de estado + this.statusBar = new StatusBar( + this.addStatusBarItem(), + this.settings, + this.statusService + ); // // Inicializar iconos del explorador (con retraso para evitar ralentizar el inicio) // if (this.settings.showStatusIconsInExplorer) { @@ -188,10 +184,11 @@ export default class NoteStatus extends Plugin { } private handleStatusChanged(event: CustomEvent) { - // const { statuses, file } = event.detail; + const { statuses, file } = event.detail; - // // Actualizar barra de estado - // this.statusBarController.update(statuses); + console.log("Note status changed", statuses) + // Actualizar barra de estado + this.statusBar.update(statuses); // // Actualizar toolbar // this.toolbarIntegration.updateStatusDisplay(statuses); @@ -268,7 +265,7 @@ export default class NoteStatus extends Plugin { // this.workspaceIntegration.updateSettings(this.settings); // // Actualizar componentes UI - // this.statusBarController.updateSettings(this.settings); + // this.statusBar.updateSettings(this.settings); } onunload() { @@ -285,6 +282,6 @@ export default class NoteStatus extends Plugin { // this.styleService.unload(); // // Limpiar componentes UI - // this.statusBarController.unload(); + // this.statusBar.unload(); } } \ No newline at end of file diff --git a/services/status-service.ts b/services/status-service.ts index c63c1d3..785fd90 100644 --- a/services/status-service.ts +++ b/services/status-service.ts @@ -105,14 +105,6 @@ export class StatusService { } } - /** - * Get the primary status of a file (first one, or 'unknown') - */ - public getFilePrimaryStatus(file: TFile): string { - const statuses = this.getFileStatuses(file); - return statuses[0] || 'unknown'; - } - /** * Get the icon for a given status */ @@ -213,7 +205,7 @@ export class StatusService { /** * Centralizes all status modification operations */ - public async modifyNoteStatus(options: { + private async modifyNoteStatus(options: { files: TFile | TFile[]; statuses: string | string[]; operation: 'set' | 'add' | 'remove' | 'toggle'; diff --git a/services/style-service.ts b/services/style-service.ts index b25da5e..a34634b 100644 --- a/services/style-service.ts +++ b/services/style-service.ts @@ -32,6 +32,20 @@ export class StyleService { this.updateDynamicStyles(); } + /** + * Updates the dynamic styles based on current settings + */ + private updateDynamicStyles(): void { + if (!this.dynamicStyleEl) { + this.initializeDynamicStyles(); + return; + } + + const allColors = this.getAllStatusColors(); + const cssRules = this.generateColorCssRules(allColors); + this.dynamicStyleEl.textContent = cssRules; + } + /** * Get all status colors including those from enabled templates */ @@ -55,20 +69,6 @@ export class StyleService { return colors; } - /** - * Updates the dynamic styles based on current settings - */ - public updateDynamicStyles(): void { - if (!this.dynamicStyleEl) { - this.initializeDynamicStyles(); - return; - } - - const allColors = this.getAllStatusColors(); - const cssRules = this.generateColorCssRules(allColors); - this.dynamicStyleEl.textContent = cssRules; - } - /** * Generate CSS rules for status colors */ diff --git a/components/status-pane-view/status-pane-view-controller.ts b/views/status-pane-view/status-pane-view-controller.ts similarity index 100% rename from components/status-pane-view/status-pane-view-controller.ts rename to views/status-pane-view/status-pane-view-controller.ts diff --git a/components/status-pane-view/status-pane-view.ts b/views/status-pane-view/status-pane-view.ts similarity index 100% rename from components/status-pane-view/status-pane-view.ts rename to views/status-pane-view/status-pane-view.ts From bdb13a5276699cbf391d312011d316e1d41a87ab Mon Sep 17 00:00:00 2001 From: Aleix Date: Wed, 21 May 2025 13:23:52 +0200 Subject: [PATCH 35/67] chore: toolbar button restored --- components/toolbar-button.ts | 75 ++++++++ integrations/editor/toolbar-integration.ts | 163 ++++++------------ .../workspace/workspace-integration.ts | 10 +- main.ts | 4 +- 4 files changed, 133 insertions(+), 119 deletions(-) create mode 100644 components/toolbar-button.ts diff --git a/components/toolbar-button.ts b/components/toolbar-button.ts new file mode 100644 index 0000000..9ab5b0e --- /dev/null +++ b/components/toolbar-button.ts @@ -0,0 +1,75 @@ +import { NoteStatusSettings } from "models/types"; +import { StatusService } from "services/status-service"; + +export class ToolbarButton { + private element: HTMLElement | null = null; + private settings: NoteStatusSettings; + private statusService: StatusService; + + constructor(settings: NoteStatusSettings, statusService: StatusService) { + this.settings = settings; + this.statusService = statusService; + } + + public createElement(): HTMLElement { + const button = document.createElement('button'); + button.addClass('note-status-toolbar-button', 'clickable-icon', 'view-action'); + button.setAttribute('aria-label', 'Note status'); + + this.element = button; + return button; + } + + public updateDisplay(statuses: string[]): void { + if (!this.element) return; + + this.element.empty(); + + const hasValidStatus = statuses.length > 0 && statuses[0] !== 'unknown'; + const badgeContainer = document.createElement('div'); + badgeContainer.addClass('note-status-toolbar-badge-container'); + + if (hasValidStatus) { + this.renderStatusBadge(badgeContainer, statuses); + } else { + this.renderUnknownBadge(badgeContainer); + } + + this.element.appendChild(badgeContainer); + } + + private renderStatusBadge(container: HTMLElement, statuses: string[]): void { + const primaryStatus = statuses[0]; + const icon = this.statusService.getStatusIcon(primaryStatus); + + const iconSpan = document.createElement('span'); + iconSpan.addClass(`note-status-toolbar-icon`, `status-${primaryStatus}`); + iconSpan.textContent = icon; + container.appendChild(iconSpan); + + if (this.settings.useMultipleStatuses && statuses.length > 1) { + const countBadge = document.createElement('span'); + countBadge.addClass('note-status-count-badge'); + countBadge.textContent = `+${statuses.length - 1}`; + container.appendChild(countBadge); + } + } + + private renderUnknownBadge(container: HTMLElement): void { + const iconSpan = document.createElement('span'); + iconSpan.addClass('note-status-toolbar-icon', 'status-unknown'); + iconSpan.textContent = this.statusService.getStatusIcon('unknown'); + container.appendChild(iconSpan); + } + + public updateSettings(settings: NoteStatusSettings): void { + this.settings = settings; + } + + public destroy(): void { + if (this.element) { + this.element.remove(); + this.element = null; + } + } +} \ No newline at end of file diff --git a/integrations/editor/toolbar-integration.ts b/integrations/editor/toolbar-integration.ts index f9562dd..3674e72 100644 --- a/integrations/editor/toolbar-integration.ts +++ b/integrations/editor/toolbar-integration.ts @@ -1,7 +1,8 @@ import { App, MarkdownView } from 'obsidian'; import { NoteStatusSettings } from '../../models/types'; -import { StatusService } from 'services/status-service'; -import { StatusDropdown } from 'components/status-dropdown'; +import { StatusService } from '../../services/status-service'; +import { StatusDropdown } from '../../components/status-dropdown'; +import { ToolbarButton } from 'components/toolbar-button'; /** * Gestiona la integración con la barra de herramientas del editor @@ -11,30 +12,27 @@ export class ToolbarIntegration { private settings: NoteStatusSettings; private statusService: StatusService; private statusDropdown: StatusDropdown; - private toolbarButton: HTMLElement | null = null; + private buttonView: ToolbarButton; + private buttonElement: HTMLElement | null = null; constructor( app: App, settings: NoteStatusSettings, statusService: StatusService, - statusDropdown: StatusDropdown ) { this.app = app; this.settings = settings; this.statusService = statusService; - this.statusDropdown = statusDropdown; + this.statusDropdown = new StatusDropdown(this.app, this.settings, this.statusService); + this.buttonView = new ToolbarButton(settings, statusService); } - /** - * Actualiza la configuración - */ + public updateSettings(settings: NoteStatusSettings): void { this.settings = settings; - this.updateToolbarButton(); + this.buttonView.updateSettings(settings); + this.updateStatusDisplay([]); } - /** - * Añade el botón de estado a la barra de herramientas - */ public addToolbarButtonToActiveLeaf(): void { const activeLeaf = this.app.workspace.activeLeaf; if (!activeLeaf?.view || !(activeLeaf.view instanceof MarkdownView)) return; @@ -42,125 +40,66 @@ export class ToolbarIntegration { const toolbarContainer = activeLeaf.view.containerEl.querySelector('.view-header .view-actions'); if (!toolbarContainer) return; + // Always remove existing button first + this.removeToolbarButton(); + + // Create a new button + this.buttonElement = this.buttonView.createElement(); + this.buttonElement.addEventListener('click', this.handleButtonClick.bind(this)); + + if (toolbarContainer.firstChild) { + toolbarContainer.insertBefore(this.buttonElement, toolbarContainer.firstChild); + } else { + toolbarContainer.appendChild(this.buttonElement); + } + + this.updateButtonDisplay(); + } + + private removeToolbarButton(): void { + const activeLeaf = this.app.workspace.activeLeaf; + if (!activeLeaf?.view || !(activeLeaf.view instanceof MarkdownView)) return; + + const toolbarContainer = activeLeaf.view.containerEl.querySelector('.view-header .view-actions'); + if (!toolbarContainer) return; + const existingButton = toolbarContainer.querySelector('.note-status-toolbar-button'); if (existingButton) { - this.toolbarButton = existingButton as HTMLElement; - this.updateToolbarButton(); - return; - } - - this.toolbarButton = this.createToolbarButton(); - - if (toolbarContainer.firstChild) { - toolbarContainer.insertBefore(this.toolbarButton, toolbarContainer.firstChild); - } else { - toolbarContainer.appendChild(this.toolbarButton); + existingButton.remove(); } + + this.buttonElement = null; } - /** - * Crea el botón de la barra de herramientas - */ - private createToolbarButton(): HTMLElement { - const button = document.createElement('button'); - button.addClass('note-status-toolbar-button', 'clickable-icon', 'view-action'); - button.setAttribute('aria-label', 'Note status'); - - this.updateToolbarButton(); - - button.addEventListener('click', (e) => { - e.stopPropagation(); - e.preventDefault(); - this.handleToolbarButtonClick(); - }); - - return button; - } - - /** - * Actualiza la apariencia del botón - */ - private updateToolbarButton(): void { - if (!this.toolbarButton) return; - - this.toolbarButton.empty(); - + private updateButtonDisplay(): void { const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile) return; + if (!activeFile || !this.buttonElement) return; const statuses = this.statusService.getFileStatuses(activeFile); - const hasValidStatus = statuses.length > 0 && statuses[0] !== 'unknown'; - - const badgeContainer = document.createElement('div'); - badgeContainer.addClass('note-status-toolbar-badge-container'); - - if (hasValidStatus) { - this.addStatusBadge(badgeContainer, statuses); - } else { - this.addUnknownStatusBadge(badgeContainer); - } - - this.toolbarButton.appendChild(badgeContainer); + this.buttonView.updateDisplay(statuses); } - /** - * Añade insignia de estado al botón - */ - private addStatusBadge(container: HTMLElement, statuses: string[]): void { - const primaryStatus = statuses[0]; - const icon = this.statusService.getStatusIcon(primaryStatus); + private handleButtonClick(e: MouseEvent): void { + e.stopPropagation(); + e.preventDefault(); - const iconSpan = document.createElement('span'); - iconSpan.addClass(`note-status-toolbar-icon`, `status-${primaryStatus}`); - iconSpan.textContent = icon; - container.appendChild(iconSpan); - - if (this.settings.useMultipleStatuses && statuses.length > 1) { - const countBadge = document.createElement('span'); - countBadge.addClass('note-status-count-badge'); - countBadge.textContent = `+${statuses.length - 1}`; - container.appendChild(countBadge); - } - } - - /** - * Añade insignia de estado desconocido - */ - private addUnknownStatusBadge(container: HTMLElement): void { - const iconSpan = document.createElement('span'); - iconSpan.addClass('note-status-toolbar-icon', 'status-unknown'); - iconSpan.textContent = this.statusService.getStatusIcon('unknown'); - container.appendChild(iconSpan); - } - - /** - * Maneja clic en el botón de la barra de herramientas - */ - private handleToolbarButtonClick(): void { const activeFile = this.app.workspace.getActiveFile(); if (!activeFile) return; - // En la implementación real, esto mostraría un dropdown de estados - console.log("Mostrar dropdown de estados"); + this.statusDropdown.openStatusDropdown({ + target: this.buttonElement || undefined, + files: [activeFile] + }); } - /** - * Actualiza el botón con nuevos estados - */ public updateStatusDisplay(statuses: string[]): void { - // Actualiza el botón con los nuevos estados - if (this.toolbarButton) { - this.updateToolbarButton(); - } + this.removeToolbarButton(); + this.addToolbarButtonToActiveLeaf(); } - /** - * Limpieza al descargar el plugin - */ public unload(): void { - if (this.toolbarButton) { - this.toolbarButton.remove(); - this.toolbarButton = null; - } + this.buttonView.destroy(); + this.statusDropdown.unload(); + this.removeToolbarButton(); } } \ No newline at end of file diff --git a/integrations/workspace/workspace-integration.ts b/integrations/workspace/workspace-integration.ts index ddd8705..a26a5ad 100644 --- a/integrations/workspace/workspace-integration.ts +++ b/integrations/workspace/workspace-integration.ts @@ -59,7 +59,7 @@ export class WorkspaceIntegration { */ private handleFileOpen(file: TFile): void { // Añade el botón de la barra de herramientas - // this.toolbarIntegration.addToolbarButtonToActiveLeaf(); + this.toolbarIntegration.addToolbarButtonToActiveLeaf(); // Actualiza estado this.propagateNoteStatusChange(); @@ -69,12 +69,12 @@ export class WorkspaceIntegration { * Maneja cambio de hoja activa */ private handleActiveLeafChange(leaf: WorkspaceLeaf): void { - // // Añade el botón de la barra de herramientas - // this.toolbarIntegration.addToolbarButtonToActiveLeaf(); + // Añade el botón de la barra de herramientas + this.toolbarIntegration.addToolbarButtonToActiveLeaf(); - // const activeFile = this.app.workspace.getActiveFile(); + const activeFile = this.app.workspace.getActiveFile(); - // // Solo actualiza si el archivo realmente cambió + // Solo actualiza si el archivo realmente cambió // if (this.lastActiveFile?.path !== activeFile?.path) { // this.lastActiveFile = activeFile; // this.propagateNoteStatusChange(); diff --git a/main.ts b/main.ts index 975cab5..87a0541 100644 --- a/main.ts +++ b/main.ts @@ -87,7 +87,7 @@ export default class NoteStatus extends Plugin { // // 1. Integraciones básicas primero // this.explorerIntegration = new ExplorerIntegration(this.app, this.settings, this.statusService); - // this.toolbarIntegration = new ToolbarIntegration(this.app, this.settings, this.statusService); + this.toolbarIntegration = new ToolbarIntegration(this.app, this.settings, this.statusService); // // 2. Integraciones que dependen de otras // this.fileMenuIntegration = new FileMenuIntegration( @@ -191,7 +191,7 @@ export default class NoteStatus extends Plugin { this.statusBar.update(statuses); // // Actualizar toolbar - // this.toolbarIntegration.updateStatusDisplay(statuses); + this.toolbarIntegration.updateStatusDisplay(statuses); // // Actualizar explorador si es necesario // if (file) { From a3750d47ae0f6415ba7b88beb26decd64d3aaf8c Mon Sep 17 00:00:00 2001 From: Aleix Date: Wed, 21 May 2025 14:16:32 +0200 Subject: [PATCH 36/67] fix: status change propagation --- services/status-service.ts | 29 ++++------------------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/services/status-service.ts b/services/status-service.ts index 785fd90..c9f6f52 100644 --- a/services/status-service.ts +++ b/services/status-service.ts @@ -271,7 +271,9 @@ export class StatusService { await this.app.fileManager.processFrontMatter(file, (frontmatter) => { frontmatter[this.settings.tagPrefix] = newStatuses; }); - + + + this.notifyStatusChanged(newStatuses, file) }); await Promise.all(updatePromises); @@ -345,20 +347,12 @@ export class StatusService { showNotice }); - // Optional callback with updated statuses - if (afterChange && targetFiles.length === 1 && !Array.isArray(files)) { - const updatedStatuses = this.getFileStatuses(files as TFile); - afterChange(updatedStatuses); - } - - // Ensure comprehensive UI updates - this.refreshUI(targetFiles); } /** * Dispatch status changed event */ - public notifyStatusChanged(statuses: string[], file?: TFile): void { + private notifyStatusChanged(statuses: string[], file?: TFile): void { // Dispatch the specific status change event window.dispatchEvent(new CustomEvent('note-status:status-changed', { detail: { @@ -367,19 +361,4 @@ export class StatusService { } })); } - - /** - * Centralizes UI refresh after status changes - */ - public refreshUI(files: TFile[]): void { - // General UI refresh - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); - - // Notify status changes for each modified file - for (const file of files) { - const statuses = this.getFileStatuses(file); - this.notifyStatusChanged(statuses, file); - } - } - } \ No newline at end of file From 6cbe46d6ea5b3ddb434fb2adc35e302d6bb2390f Mon Sep 17 00:00:00 2001 From: Aleix Date: Wed, 21 May 2025 14:27:59 +0200 Subject: [PATCH 37/67] chore: restore dropdown actions --- components/status-dropdown/dropdown-manager.ts | 2 +- integrations/editor/toolbar-integration.ts | 14 ++++++++------ main.ts | 9 ++++++--- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/components/status-dropdown/dropdown-manager.ts b/components/status-dropdown/dropdown-manager.ts index 835d0d3..e34b400 100644 --- a/components/status-dropdown/dropdown-manager.ts +++ b/components/status-dropdown/dropdown-manager.ts @@ -35,7 +35,7 @@ export class DropdownManager { this.dropdownUI.setOnStatusChange((statuses) => { this.currentStatuses = [...statuses]; this.updateToolbarButton(); - this.statusService.notifyStatusChanged(statuses); + // this.statusService.notifyStatusChanged(statuses); }); this.dropdownUI.setOnRemoveStatusHandler(async (status, targetFile) => { diff --git a/integrations/editor/toolbar-integration.ts b/integrations/editor/toolbar-integration.ts index 3674e72..be57a95 100644 --- a/integrations/editor/toolbar-integration.ts +++ b/integrations/editor/toolbar-integration.ts @@ -19,11 +19,12 @@ export class ToolbarIntegration { app: App, settings: NoteStatusSettings, statusService: StatusService, + statusDropdown: StatusDropdown ) { this.app = app; this.settings = settings; this.statusService = statusService; - this.statusDropdown = new StatusDropdown(this.app, this.settings, this.statusService); + this.statusDropdown = statusDropdown; this.buttonView = new ToolbarButton(settings, statusService); } @@ -33,7 +34,7 @@ export class ToolbarIntegration { this.updateStatusDisplay([]); } - public addToolbarButtonToActiveLeaf(): void { + public addToolbarButtonToActiveLeaf(statuses?: string[]): void { const activeLeaf = this.app.workspace.activeLeaf; if (!activeLeaf?.view || !(activeLeaf.view instanceof MarkdownView)) return; @@ -53,7 +54,7 @@ export class ToolbarIntegration { toolbarContainer.appendChild(this.buttonElement); } - this.updateButtonDisplay(); + this.updateButtonDisplay(statuses); } private removeToolbarButton(): void { @@ -71,11 +72,12 @@ export class ToolbarIntegration { this.buttonElement = null; } - private updateButtonDisplay(): void { + private updateButtonDisplay(overrideStatutes?: string[]): void { const activeFile = this.app.workspace.getActiveFile(); if (!activeFile || !this.buttonElement) return; - const statuses = this.statusService.getFileStatuses(activeFile); + + const statuses = overrideStatutes?.length ? overrideStatutes : this.statusService.getFileStatuses(activeFile); this.buttonView.updateDisplay(statuses); } @@ -94,7 +96,7 @@ export class ToolbarIntegration { public updateStatusDisplay(statuses: string[]): void { this.removeToolbarButton(); - this.addToolbarButtonToActiveLeaf(); + this.addToolbarButtonToActiveLeaf(statuses); } public unload(): void { diff --git a/main.ts b/main.ts index 87a0541..293c528 100644 --- a/main.ts +++ b/main.ts @@ -13,6 +13,7 @@ import { WorkspaceIntegration } from './integrations/workspace'; // Importar componentes UI import { NoteStatusSettingTab } from 'integrations/settings/settings-tab'; import { StatusBar } from 'components/status-bar'; +import { StatusDropdown } from 'components/status-dropdown'; export default class NoteStatus extends Plugin { settings: NoteStatusSettings; @@ -23,6 +24,7 @@ export default class NoteStatus extends Plugin { // Componentes UI statusBar: StatusBar; + statusDropdown: StatusDropdown // Integraciones explorerIntegration: ExplorerIntegration; @@ -84,10 +86,9 @@ export default class NoteStatus extends Plugin { private initializeIntegrations() { // Crear integraciones en orden de dependencia - // // 1. Integraciones básicas primero // this.explorerIntegration = new ExplorerIntegration(this.app, this.settings, this.statusService); - this.toolbarIntegration = new ToolbarIntegration(this.app, this.settings, this.statusService); + this.toolbarIntegration = new ToolbarIntegration(this.app, this.settings, this.statusService, this.statusDropdown); // // 2. Integraciones que dependen de otras // this.fileMenuIntegration = new FileMenuIntegration( @@ -168,6 +169,8 @@ export default class NoteStatus extends Plugin { } private initializeUI() { + this.statusDropdown = new StatusDropdown(this.app, this.settings, this.statusService); + // Inicializar barra de estado this.statusBar = new StatusBar( this.addStatusBarItem(), @@ -192,7 +195,7 @@ export default class NoteStatus extends Plugin { // // Actualizar toolbar this.toolbarIntegration.updateStatusDisplay(statuses); - + this.statusDropdown.update(statuses); // // Actualizar explorador si es necesario // if (file) { // const fileObj = this.app.vault.getFileByPath(file); From f92529e70c67e0faec6ada7187606935bfb17955 Mon Sep 17 00:00:00 2001 From: Aleix Date: Wed, 21 May 2025 15:01:51 +0200 Subject: [PATCH 38/67] chore: restore explorer icons --- .../file-context-menu-integration.ts} | 4 +- integrations/explorer/index.ts | 3 +- main.ts | 43 +++++++++---------- 3 files changed, 24 insertions(+), 26 deletions(-) rename integrations/{explorer/file-menu-integration.ts => context-menu/file-context-menu-integration.ts} (95%) diff --git a/integrations/explorer/file-menu-integration.ts b/integrations/context-menu/file-context-menu-integration.ts similarity index 95% rename from integrations/explorer/file-menu-integration.ts rename to integrations/context-menu/file-context-menu-integration.ts index ee78b39..d37a92a 100644 --- a/integrations/explorer/file-menu-integration.ts +++ b/integrations/context-menu/file-context-menu-integration.ts @@ -1,12 +1,12 @@ import { App, Menu, TFile } from 'obsidian'; import { NoteStatusSettings } from '../../models/types'; -import { ExplorerIntegration } from './explorer-integration'; +import { ExplorerIntegration } from '../explorer/explorer-integration'; import { StatusService } from 'services/status-service'; /** * Gestiona menús contextuales del explorador de archivos */ -export class FileMenuIntegration { +export class FileContextMenuIntegration { private app: App; private settings: NoteStatusSettings; private statusService: StatusService; diff --git a/integrations/explorer/index.ts b/integrations/explorer/index.ts index ab99c21..6720b6d 100644 --- a/integrations/explorer/index.ts +++ b/integrations/explorer/index.ts @@ -1,2 +1 @@ -export { ExplorerIntegration } from './explorer-integration'; -export { FileMenuIntegration } from './file-menu-integration'; \ No newline at end of file +export { ExplorerIntegration } from './explorer-integration'; \ No newline at end of file diff --git a/main.ts b/main.ts index 293c528..d06d739 100644 --- a/main.ts +++ b/main.ts @@ -5,7 +5,7 @@ import { StatusService } from 'services/status-service'; import { StyleService } from 'services/style-service'; // Importar integraciones -import { ExplorerIntegration, FileMenuIntegration } from './integrations/explorer'; +import { ExplorerIntegration } from './integrations/explorer'; import { EditorIntegration, ToolbarIntegration } from './integrations/editor'; import { MetadataIntegration } from './integrations/metadata-cache'; import { WorkspaceIntegration } from './integrations/workspace'; @@ -14,6 +14,7 @@ import { WorkspaceIntegration } from './integrations/workspace'; import { NoteStatusSettingTab } from 'integrations/settings/settings-tab'; import { StatusBar } from 'components/status-bar'; import { StatusDropdown } from 'components/status-dropdown'; +import { FileContextMenuIntegration } from 'integrations/context-menu/file-context-menu-integration'; export default class NoteStatus extends Plugin { settings: NoteStatusSettings; @@ -28,7 +29,7 @@ export default class NoteStatus extends Plugin { // Integraciones explorerIntegration: ExplorerIntegration; - fileMenuIntegration: FileMenuIntegration; + fileContextMenuIntegration: FileContextMenuIntegration; editorIntegration: EditorIntegration; toolbarIntegration: ToolbarIntegration; metadataIntegration: MetadataIntegration; @@ -86,12 +87,11 @@ export default class NoteStatus extends Plugin { private initializeIntegrations() { // Crear integraciones en orden de dependencia - // // 1. Integraciones básicas primero - // this.explorerIntegration = new ExplorerIntegration(this.app, this.settings, this.statusService); + this.explorerIntegration = new ExplorerIntegration(this.app, this.settings, this.statusService); this.toolbarIntegration = new ToolbarIntegration(this.app, this.settings, this.statusService, this.statusDropdown); // // 2. Integraciones que dependen de otras - // this.fileMenuIntegration = new FileMenuIntegration( + // this.fileContextMenuIntegration = new FileContextMenuIntegration( // this.app, // this.settings, // this.statusService, @@ -115,7 +115,7 @@ export default class NoteStatus extends Plugin { ); // // 3. Registrar eventos en cada integración - // this.fileMenuIntegration.registerFileMenus(); + // this.fileContextMenuIntegration.registerFileMenus(); // this.editorIntegration.registerEditorMenus(); // this.metadataIntegration.registerMetadataEvents(); this.workspaceIntegration.registerWorkspaceEvents(); @@ -178,12 +178,12 @@ export default class NoteStatus extends Plugin { this.statusService ); - // // Inicializar iconos del explorador (con retraso para evitar ralentizar el inicio) - // if (this.settings.showStatusIconsInExplorer) { - // setTimeout(() => { - // this.explorerIntegration.updateAllFileExplorerIcons(); - // }, 2000); - // } + // Inicializar iconos del explorador (con retraso para evitar ralentizar el inicio) + if (this.settings.showStatusIconsInExplorer) { + setTimeout(() => { + this.explorerIntegration.updateAllFileExplorerIcons(); + }, 2000); + } } private handleStatusChanged(event: CustomEvent) { @@ -192,17 +192,16 @@ export default class NoteStatus extends Plugin { console.log("Note status changed", statuses) // Actualizar barra de estado this.statusBar.update(statuses); - - // // Actualizar toolbar + // Actualizar toolbar this.toolbarIntegration.updateStatusDisplay(statuses); this.statusDropdown.update(statuses); - // // Actualizar explorador si es necesario - // if (file) { - // const fileObj = this.app.vault.getFileByPath(file); - // if (fileObj) { - // this.explorerIntegration.updateFileExplorerIcons(fileObj); - // } - // } + // Actualizar explorador si es necesario + if (this.settings.showStatusIconsInExplorer && file) { + const fileObj = this.app.vault.getFileByPath(file); + if (fileObj) { + this.explorerIntegration.updateFileExplorerIcons(fileObj); + } + } } private refreshStatus() { @@ -261,7 +260,7 @@ export default class NoteStatus extends Plugin { private updateIntegrationsSettings() { // // Actualizar configuración en todas las integraciones // this.explorerIntegration.updateSettings(this.settings); - // this.fileMenuIntegration.updateSettings(this.settings); + // this.fileContextMenuIntegration.updateSettings(this.settings); // this.editorIntegration.updateSettings(this.settings); // this.toolbarIntegration.updateSettings(this.settings); // this.metadataIntegration.updateSettings(this.settings); From 7721eb8c78985daa6b3ed0161341e55ba3c1c778 Mon Sep 17 00:00:00 2001 From: Aleix Date: Wed, 21 May 2025 15:08:33 +0200 Subject: [PATCH 39/67] refactor: clear interfaces between explorer integrations modules --- integrations/explorer/explorer-integration.ts | 205 +++++++++++------- 1 file changed, 130 insertions(+), 75 deletions(-) diff --git a/integrations/explorer/explorer-integration.ts b/integrations/explorer/explorer-integration.ts index 5490efe..3d31f2a 100644 --- a/integrations/explorer/explorer-integration.ts +++ b/integrations/explorer/explorer-integration.ts @@ -1,14 +1,16 @@ -import { App, TFile, debounce, setTooltip } from 'obsidian'; -import { FileExplorerView, NoteStatusSettings } from '../../models/types'; +import { App, TFile, setTooltip, debounce } from 'obsidian'; +import { NoteStatusSettings, FileExplorerView } from '../../models/types'; import { StatusService } from 'services/status-service'; + /** - * Gestiona la integración de iconos en el explorador de archivos + * Manages the logic for file explorer status integration */ export class ExplorerIntegration { private app: App; private settings: NoteStatusSettings; private statusService: StatusService; + private ui: ExplorerIntegrationUI; private iconUpdateQueue = new Set(); private isProcessingQueue = false; private debouncedUpdateAll: ReturnType; @@ -17,11 +19,12 @@ export class ExplorerIntegration { this.app = app; this.settings = settings; this.statusService = statusService; + this.ui = new ExplorerIntegrationUI(app, settings, statusService); this.debouncedUpdateAll = debounce(this.processUpdateQueue.bind(this), 100, true); } /** - * Actualiza la configuración y refresca la UI si es necesario + * Updates settings and refreshes UI if necessary */ public updateSettings(settings: NoteStatusSettings): void { const shouldRefreshIcons = @@ -29,9 +32,10 @@ export class ExplorerIntegration { this.settings.hideUnknownStatusInExplorer !== settings.hideUnknownStatusInExplorer; this.settings = settings; + this.ui.updateSettings(settings); if (shouldRefreshIcons) { - this.removeAllFileExplorerIcons(); + this.ui.removeAllFileExplorerIcons(); if (settings.showStatusIconsInExplorer) { setTimeout(() => this.updateAllFileExplorerIcons(), 50); @@ -42,41 +46,25 @@ export class ExplorerIntegration { } /** - * Encuentra la vista del explorador de archivos - */ - private findFileExplorerView(): FileExplorerView | null { - const leaf = this.app.workspace.getLeavesOfType('file-explorer')[0]; - if (leaf?.view) return leaf.view as FileExplorerView; - - for (const leaf of this.app.workspace.getLeavesOfType('')) { - if (leaf.view && 'fileItems' in leaf.view) { - return leaf.view as FileExplorerView; - } - } - - return null; - } - - /** - * Actualiza los iconos para un archivo específico + * Updates icons for a specific file */ public updateFileExplorerIcons(file: TFile): void { if (!file || !this.settings.showStatusIconsInExplorer || file.extension !== 'md') return; const activeFile = this.app.workspace.getActiveFile(); if (activeFile?.path === file.path) { - this.updateSingleFileIconDirectly(file); + this.ui.updateSingleFileIconDirectly(file, this.statusService); } this.queueFileUpdate(file); } /** - * Actualiza todos los iconos en el explorador + * Updates all icons in the explorer */ public updateAllFileExplorerIcons(): void { if (!this.settings.showStatusIconsInExplorer) { - this.removeAllFileExplorerIcons(); + this.ui.removeAllFileExplorerIcons(); return; } @@ -84,37 +72,15 @@ export class ExplorerIntegration { } /** - * Elimina todos los iconos de estado + * Gets selected files from the explorer */ - public removeAllFileExplorerIcons(): void { - const fileExplorer = this.findFileExplorerView(); - if (!fileExplorer?.fileItems) return; - - Object.values(fileExplorer.fileItems).forEach(fileItem => { - const titleEl = fileItem.titleEl || fileItem.selfEl; - if (titleEl) this.removeExistingIcons(titleEl); - }); - - this.iconUpdateQueue.clear(); + public getSelectedFiles(): TFile[] { + return this.ui.getSelectedFiles(); } /** - * Obtiene los archivos seleccionados del explorador + * Queue a file for icon update */ - public getSelectedFiles(): TFile[] { - const fileExplorer = this.findFileExplorerView(); - if (!fileExplorer?.fileItems) return []; - - return Object.values(fileExplorer.fileItems) - .filter(item => - item.el?.classList.contains('is-selected') && - item.file instanceof TFile && - item.file.extension === 'md') - .map(item => item.file as TFile); - } - - // Métodos para procesamiento por lotes - private queueFileUpdate(file: TFile): void { if (!this.settings.showStatusIconsInExplorer || file.extension !== 'md') return; @@ -122,13 +88,16 @@ export class ExplorerIntegration { this.debouncedUpdateAll(); } + /** + * Process the update queue + */ private async processUpdateQueue(): Promise { if (this.isProcessingQueue || this.iconUpdateQueue.size === 0) return; this.isProcessingQueue = true; try { - const fileExplorerView = this.findFileExplorerView(); + const fileExplorerView = this.ui.findFileExplorerView(); if (!fileExplorerView) { setTimeout(() => this.debouncedUpdateAll(), 200); return; @@ -155,16 +124,22 @@ export class ExplorerIntegration { } } - private async processBatch(paths: string[], fileExplorerView: FileExplorerView): Promise { + /** + * Process a batch of files + */ + private async processBatch(paths: string[], fileExplorerView: any): Promise { for (const path of paths) { const file = this.app.vault.getFileByPath(path); if (file instanceof TFile) { - this.updateSingleFileIcon(file, fileExplorerView); + this.ui.updateSingleFileIcon(file, fileExplorerView, this.statusService); } this.iconUpdateQueue.delete(path); } } + /** + * Process files in batches + */ private async processFilesInBatches(): Promise { const files = this.app.vault.getMarkdownFiles(); const batchSize = 100; @@ -178,9 +153,54 @@ export class ExplorerIntegration { } } - // Métodos para manipular iconos - - private updateSingleFileIcon(file: TFile, fileExplorerView: FileExplorerView): void { + /** + * Cleanup when unloading the plugin + */ + public unload(): void { + this.ui.removeAllFileExplorerIcons(); + this.debouncedUpdateAll.cancel(); + } +} + +/** + * Manages UI operations for file explorer icons + */ +export class ExplorerIntegrationUI { + private app: App; + private settings: NoteStatusSettings; + + constructor(app: App, settings: NoteStatusSettings, statusService: StatusService) { + this.app = app; + this.settings = settings; + } + + /** + * Updates the settings + */ + public updateSettings(settings: NoteStatusSettings): void { + this.settings = settings; + } + + /** + * Finds the file explorer view + */ + public findFileExplorerView(): FileExplorerView | null { + const leaf = this.app.workspace.getLeavesOfType('file-explorer')[0]; + if (leaf?.view) return leaf.view as FileExplorerView; + + for (const leaf of this.app.workspace.getLeavesOfType('')) { + if (leaf.view && 'fileItems' in leaf.view) { + return leaf.view as FileExplorerView; + } + } + + return null; + } + + /** + * Updates a single file icon + */ + public updateSingleFileIcon(file: TFile, fileExplorerView: FileExplorerView, statusService: StatusService): void { if (!this.settings.showStatusIconsInExplorer || file.extension !== 'md') return; try { @@ -190,31 +210,68 @@ export class ExplorerIntegration { const titleEl = fileItem.titleEl || fileItem.selfEl; if (!titleEl) return; - const statuses = this.statusService.getFileStatuses(file); + const statuses = statusService.getFileStatuses(file); this.removeExistingIcons(titleEl); if (this.shouldSkipIcon(statuses)) return; - this.addStatusIcons(titleEl, statuses); + this.addStatusIcons(titleEl, statuses, statusService); } catch (error) { console.error(`Note Status: Error updating icon for ${file.path}`, error); } } - private updateSingleFileIconDirectly(file: TFile): void { + /** + * Updates the icon for the active file directly + */ + public updateSingleFileIconDirectly(file: TFile, statusService: StatusService): void { const fileExplorer = this.findFileExplorerView(); if (fileExplorer) { - this.updateSingleFileIcon(file, fileExplorer); + this.updateSingleFileIcon(file, fileExplorer, statusService); } } + + /** + * Removes all file explorer icons + */ + public removeAllFileExplorerIcons(): void { + const fileExplorer = this.findFileExplorerView(); + if (!fileExplorer?.fileItems) return; + + Object.values(fileExplorer.fileItems).forEach(fileItem => { + const titleEl = fileItem.titleEl || fileItem.selfEl; + if (titleEl) this.removeExistingIcons(titleEl); + }); + } + + /** + * Gets the currently selected files + */ + public getSelectedFiles(): TFile[] { + const fileExplorer = this.findFileExplorerView(); + if (!fileExplorer?.fileItems) return []; + return Object.values(fileExplorer.fileItems) + .filter(item => + item.el?.classList.contains('is-selected') && + item.file instanceof TFile && + item.file.extension === 'md') + .map(item => item.file as TFile); + } + + /** + * Checks if the icon should be skipped based on status + */ private shouldSkipIcon(statuses: string[]): boolean { return this.settings.hideUnknownStatusInExplorer && statuses.length === 1 && statuses[0] === 'unknown'; } + /** + * Removes existing status icons + */ private removeExistingIcons(element: HTMLElement): void { const iconSelectors = '.note-status-icon, .note-status-icon-container'; element.querySelectorAll(iconSelectors).forEach(icon => { @@ -225,16 +282,19 @@ export class ExplorerIntegration { }); } - private addStatusIcons(titleEl: HTMLElement, statuses: string[]): void { + /** + * Adds status icons to an element + */ + private addStatusIcons(titleEl: HTMLElement, statuses: string[], statusService: StatusService): void { const iconContainer = document.createElement('span'); iconContainer.className = 'note-status-icon-container'; if (this.settings.useMultipleStatuses && statuses.length > 0 && statuses[0] !== 'unknown') { - statuses.forEach(status => this.addSingleStatusIcon(iconContainer, status)); + statuses.forEach(status => this.addSingleStatusIcon(iconContainer, status, statusService)); } else { const primaryStatus = statuses[0] || 'unknown'; if (primaryStatus !== 'unknown' || !this.settings.hideUnknownStatusInExplorer) { - this.addSingleStatusIcon(iconContainer, primaryStatus); + this.addSingleStatusIcon(iconContainer, primaryStatus, statusService); } } @@ -243,23 +303,18 @@ export class ExplorerIntegration { } } - private addSingleStatusIcon(container: HTMLElement, status: string): void { + /** + * Adds a single status icon + */ + private addSingleStatusIcon(container: HTMLElement, status: string, statusService: StatusService): void { const iconEl = document.createElement('span'); iconEl.className = `note-status-icon nav-file-tag status-${status}`; - iconEl.textContent = this.statusService.getStatusIcon(status); + iconEl.textContent = statusService.getStatusIcon(status); - const statusObj = this.statusService.getAllStatuses().find(s => s.name === status); + const statusObj = statusService.getAllStatuses().find(s => s.name === status); const tooltipValue = statusObj?.description ? `${status} - ${statusObj.description}`: status; setTooltip(iconEl, tooltipValue); container.appendChild(iconEl); } - - /** - * Limpieza al descargar el plugin - */ - public unload(): void { - this.removeAllFileExplorerIcons(); - this.debouncedUpdateAll.cancel(); - } } \ No newline at end of file From 33935c2d77f0bb08d37ab1a381c5b0e545c2dc1c Mon Sep 17 00:00:00 2001 From: Aleix Date: Wed, 21 May 2025 15:26:53 +0200 Subject: [PATCH 40/67] chore: restored file context menu --- .../file-context-menu-integration.ts | 9 +++++++-- main.ts | 17 +++++++---------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/integrations/context-menu/file-context-menu-integration.ts b/integrations/context-menu/file-context-menu-integration.ts index d37a92a..6de8e46 100644 --- a/integrations/context-menu/file-context-menu-integration.ts +++ b/integrations/context-menu/file-context-menu-integration.ts @@ -2,6 +2,7 @@ import { App, Menu, TFile } from 'obsidian'; import { NoteStatusSettings } from '../../models/types'; import { ExplorerIntegration } from '../explorer/explorer-integration'; import { StatusService } from 'services/status-service'; +import { StatusContextMenu } from './status-context-menu'; /** * Gestiona menús contextuales del explorador de archivos @@ -11,17 +12,20 @@ export class FileContextMenuIntegration { private settings: NoteStatusSettings; private statusService: StatusService; private explorerIntegration: ExplorerIntegration; + private statusContextMenu: StatusContextMenu; constructor( app: App, settings: NoteStatusSettings, statusService: StatusService, - explorerIntegration: ExplorerIntegration + explorerIntegration: ExplorerIntegration, + statusContextMenu: StatusContextMenu ) { this.app = app; this.settings = settings; this.statusService = statusService; this.explorerIntegration = explorerIntegration; + this.statusContextMenu = statusContextMenu; } /** @@ -34,7 +38,7 @@ export class FileContextMenuIntegration { /** * Registra los elementos del menú de archivo */ - public registerFileMenus(): void { + public registerFileContextMenuEvents(): void { this.app.workspace.on('file-menu', (menu, file, source) => { if (source === 'file-explorer-context-menu' && file instanceof TFile && file.extension === 'md') { this.addStatusChangeMenu(menu, file); @@ -92,5 +96,6 @@ export class FileContextMenuIntegration { // Implementación que mostraría un modal para seleccionar estado // En la práctica, esta función delegaría en un servicio de UI console.log(`Cambiar estado para ${files.length} archivos`); + this.statusContextMenu.showForFiles(files) } } \ No newline at end of file diff --git a/main.ts b/main.ts index d06d739..c7e34a4 100644 --- a/main.ts +++ b/main.ts @@ -9,12 +9,13 @@ import { ExplorerIntegration } from './integrations/explorer'; import { EditorIntegration, ToolbarIntegration } from './integrations/editor'; import { MetadataIntegration } from './integrations/metadata-cache'; import { WorkspaceIntegration } from './integrations/workspace'; +import { FileContextMenuIntegration } from 'integrations/context-menu/file-context-menu-integration'; +import { NoteStatusSettingTab } from 'integrations/settings/settings-tab'; // Importar componentes UI -import { NoteStatusSettingTab } from 'integrations/settings/settings-tab'; import { StatusBar } from 'components/status-bar'; import { StatusDropdown } from 'components/status-dropdown'; -import { FileContextMenuIntegration } from 'integrations/context-menu/file-context-menu-integration'; +import { StatusContextMenu } from 'integrations/context-menu/status-context-menu'; export default class NoteStatus extends Plugin { settings: NoteStatusSettings; @@ -90,13 +91,9 @@ export default class NoteStatus extends Plugin { this.explorerIntegration = new ExplorerIntegration(this.app, this.settings, this.statusService); this.toolbarIntegration = new ToolbarIntegration(this.app, this.settings, this.statusService, this.statusDropdown); - // // 2. Integraciones que dependen de otras - // this.fileContextMenuIntegration = new FileContextMenuIntegration( - // this.app, - // this.settings, - // this.statusService, - // this.explorerIntegration - // ); + // Integraciones que dependen de otras + const statusContextMenu = new StatusContextMenu(this.app, this.settings, this.statusService, this.statusDropdown, this.explorerIntegration); + this.fileContextMenuIntegration = new FileContextMenuIntegration(this.app, this.settings, this.statusService, this.explorerIntegration, statusContextMenu); // this.editorIntegration = new EditorIntegration(this.app, this.settings, this.statusService); @@ -115,7 +112,7 @@ export default class NoteStatus extends Plugin { ); // // 3. Registrar eventos en cada integración - // this.fileContextMenuIntegration.registerFileMenus(); + this.fileContextMenuIntegration.registerFileContextMenuEvents(); // this.editorIntegration.registerEditorMenus(); // this.metadataIntegration.registerMetadataEvents(); this.workspaceIntegration.registerWorkspaceEvents(); From 2cdd2e8808295d7b278bb3d3a00a7b887c497614 Mon Sep 17 00:00:00 2001 From: Aleix Date: Wed, 21 May 2025 15:37:23 +0200 Subject: [PATCH 41/67] refactor: logic and ui --- .../file-context-menu-integration.ts | 56 +++------ .../context-menu/status-context-menu.ts | 110 ++++++++++++------ 2 files changed, 94 insertions(+), 72 deletions(-) diff --git a/integrations/context-menu/file-context-menu-integration.ts b/integrations/context-menu/file-context-menu-integration.ts index 6de8e46..9ffa482 100644 --- a/integrations/context-menu/file-context-menu-integration.ts +++ b/integrations/context-menu/file-context-menu-integration.ts @@ -1,11 +1,11 @@ import { App, Menu, TFile } from 'obsidian'; -import { NoteStatusSettings } from '../../models/types'; -import { ExplorerIntegration } from '../explorer/explorer-integration'; +import { NoteStatusSettings } from 'models/types'; import { StatusService } from 'services/status-service'; -import { StatusContextMenu } from './status-context-menu'; +import { ExplorerIntegration } from 'integrations/explorer/explorer-integration'; +import { StatusContextMenu } from 'integrations/context-menu/status-context-menu'; /** - * Gestiona menús contextuales del explorador de archivos + * Gestiona la integración de menús contextuales con el explorador de archivos */ export class FileContextMenuIntegration { private app: App; @@ -36,15 +36,17 @@ export class FileContextMenuIntegration { } /** - * Registra los elementos del menú de archivo + * Registra los eventos del menú contextual de archivos */ public registerFileContextMenuEvents(): void { + // Evento para archivo único this.app.workspace.on('file-menu', (menu, file, source) => { if (source === 'file-explorer-context-menu' && file instanceof TFile && file.extension === 'md') { this.addStatusChangeMenu(menu, file); } }); + // Evento para múltiples archivos this.app.workspace.on('files-menu', (menu, files) => { const mdFiles = files.filter(file => file instanceof TFile && file.extension === 'md' @@ -57,45 +59,25 @@ export class FileContextMenuIntegration { } /** - * Añade opción de cambio de estado al menú de archivo + * Añade opción de cambio de estado al menú contextual de un archivo */ private addStatusChangeMenu(menu: Menu, file: TFile): void { - menu.addItem(item => - item - .setTitle('Change status') - .setIcon('tag') - .onClick(() => { - const selectedFiles = this.explorerIntegration.getSelectedFiles(); - if (selectedFiles.length > 1) { - this.showStatusChangeModal(selectedFiles); - } else { - this.showStatusChangeModal([file]); - } - }) - ); + this.statusContextMenu.addStatusMenuItemToSingleFile(menu, file, (file) => { + const selectedFiles = this.explorerIntegration.getSelectedFiles(); + if (selectedFiles.length > 1) { + this.statusContextMenu.showForFiles(selectedFiles); + } else { + this.statusContextMenu.showForFiles([file]); + } + }); } /** * Añade opción de cambio de estado para múltiples archivos */ private addBatchStatusChangeMenu(menu: Menu, files: TFile[]): void { - menu.addItem(item => - item - .setTitle('Change status') - .setIcon('tag') - .onClick(() => { - this.showStatusChangeModal(files); - }) - ); - } - - /** - * Muestra el modal para cambiar estado - */ - private showStatusChangeModal(files: TFile[]): void { - // Implementación que mostraría un modal para seleccionar estado - // En la práctica, esta función delegaría en un servicio de UI - console.log(`Cambiar estado para ${files.length} archivos`); - this.statusContextMenu.showForFiles(files) + this.statusContextMenu.addStatusMenuItemToBatch(menu, files, (files) => { + this.statusContextMenu.showForFiles(files); + }); } } \ No newline at end of file diff --git a/integrations/context-menu/status-context-menu.ts b/integrations/context-menu/status-context-menu.ts index 7367bd3..7f23f7a 100644 --- a/integrations/context-menu/status-context-menu.ts +++ b/integrations/context-menu/status-context-menu.ts @@ -1,18 +1,18 @@ import { App, Menu, TFile } from 'obsidian'; -import { NoteStatusSettings } from '../../models/types'; -import { StatusService } from '../../services/status-service'; +import { NoteStatusSettings } from 'models/types'; +import { StatusService } from 'services/status-service'; import { StatusDropdown } from 'components/status-dropdown'; -import { ExplorerIntegration } from 'integrations/explorer'; +import { ExplorerIntegration } from 'integrations/explorer/explorer-integration'; /** - * Handles context menu interactions for status changes + * Gestiona los menús contextuales para cambios de estado */ export class StatusContextMenu { - private statusDropdown: StatusDropdown; + private app: App; private settings: NoteStatusSettings; private statusService: StatusService; + private statusDropdown: StatusDropdown; private explorerIntegration: ExplorerIntegration; - public app: App; constructor( app: App, @@ -29,14 +29,40 @@ export class StatusContextMenu { } /** - * Updates settings reference + * Actualiza la configuración */ public updateSettings(settings: NoteStatusSettings): void { this.settings = settings; } /** - * Shows the context menu for changing status of one or more files + * Añade ítem de menú para cambiar estado de un archivo + */ + public addStatusMenuItemToSingleFile(menu: Menu, file: TFile, onClick: (file: TFile) => void): void { + menu.addItem(item => + item + .setTitle('Change status') + .setIcon('tag') + .onClick(() => onClick(file)) + ); + } + + /** + * Añade ítem de menú para cambiar estado de múltiples archivos + */ + public addStatusMenuItemToBatch(menu: Menu, files: TFile[], onClick: (files: TFile[]) => void): void { + menu.addItem(item => + item + .setTitle('Change status') + .setIcon('tag') + .onClick(() => onClick(files)) + ); + } + + /** + * Muestra el menú contextual para cambiar estado de uno o más archivos + * @param files Archivos a los que cambiar el estado + * @param position Posición opcional para mostrar el menú */ public showForFiles(files: TFile[], position?: { x: number; y: number }): void { if (files.length === 0) return; @@ -44,38 +70,16 @@ export class StatusContextMenu { if (files.length === 1) { this.showForSingleFile(files[0], position); } else { - const menu = new Menu(); - - menu.addItem((item) => { - item.setTitle(`Update ${files.length} files`) - .setDisabled(true); - return item; - }); - - menu.addItem((item) => - item - .setTitle('Manage statuses...') - .setIcon('tag') - .onClick(() => { - this.statusDropdown.openStatusDropdown({ - position, - files - }); - }) - ); - - if (position) { - menu.showAtPosition(position); - } else { - menu.showAtMouseEvent(new MouseEvent('contextmenu')); - } + this.showForMultipleFiles(files, position); } } /** - * Shows a context menu for a single file + * Muestra el menú contextual para un solo archivo + * @param file Archivo al que cambiar el estado + * @param position Posición opcional para mostrar el menú */ - public showForSingleFile(file: TFile, position?: { x: number; y: number }): void { + private showForSingleFile(file: TFile, position?: { x: number; y: number }): void { if (!(file instanceof TFile) || file.extension !== 'md') return; this.statusDropdown.openStatusDropdown({ @@ -83,4 +87,40 @@ export class StatusContextMenu { files: [file] }); } + + /** + * Muestra el menú contextual para múltiples archivos + * @param files Archivos a los que cambiar el estado + * @param position Posición opcional para mostrar el menú + */ + private showForMultipleFiles(files: TFile[], position?: { x: number; y: number }): void { + const menu = new Menu(); + + // Elemento de información (deshabilitado) + menu.addItem((item) => { + item.setTitle(`Update ${files.length} files`) + .setDisabled(true); + return item; + }); + + // Opción para gestionar estados + menu.addItem((item) => + item + .setTitle('Manage statuses...') + .setIcon('tag') + .onClick(() => { + this.statusDropdown.openStatusDropdown({ + position, + files + }); + }) + ); + + // Mostrar el menú en la posición adecuada + if (position) { + menu.showAtPosition(position); + } else { + menu.showAtMouseEvent(new MouseEvent('contextmenu')); + } + } } \ No newline at end of file From 39af634a9f0583f8329ab4ca3f61b34c094a86c9 Mon Sep 17 00:00:00 2001 From: Aleix Date: Wed, 21 May 2025 15:42:28 +0200 Subject: [PATCH 42/67] fix: multiple files menu centered by default --- .../context-menu/status-context-menu.ts | 58 ++++++++++--------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/integrations/context-menu/status-context-menu.ts b/integrations/context-menu/status-context-menu.ts index 7f23f7a..0dbc150 100644 --- a/integrations/context-menu/status-context-menu.ts +++ b/integrations/context-menu/status-context-menu.ts @@ -94,33 +94,37 @@ export class StatusContextMenu { * @param position Posición opcional para mostrar el menú */ private showForMultipleFiles(files: TFile[], position?: { x: number; y: number }): void { - const menu = new Menu(); - - // Elemento de información (deshabilitado) - menu.addItem((item) => { - item.setTitle(`Update ${files.length} files`) - .setDisabled(true); - return item; - }); + const menu = new Menu(); + + // Elemento de información (deshabilitado) + menu.addItem((item) => { + item.setTitle(`Update ${files.length} files`) + .setDisabled(true); + return item; + }); - // Opción para gestionar estados - menu.addItem((item) => - item - .setTitle('Manage statuses...') - .setIcon('tag') - .onClick(() => { - this.statusDropdown.openStatusDropdown({ - position, - files - }); - }) - ); - - // Mostrar el menú en la posición adecuada - if (position) { - menu.showAtPosition(position); - } else { - menu.showAtMouseEvent(new MouseEvent('contextmenu')); + // Opción para gestionar estados + menu.addItem((item) => + item + .setTitle('Manage statuses...') + .setIcon('tag') + .onClick(() => { + this.statusDropdown.openStatusDropdown({ + position, + files + }); + }) + ); + + // Mostrar el menú en la posición adecuada + if (position) { + menu.showAtPosition(position); + } else { + // Use a centered position + menu.showAtPosition({ + x: window.innerWidth / 2, + y: window.innerHeight / 3 + }); + } } - } } \ No newline at end of file From ef0f2a65ea3e5ff31ffcb036d1ffd4d25fcf0a2d Mon Sep 17 00:00:00 2001 From: Aleix Date: Wed, 21 May 2025 18:47:46 +0200 Subject: [PATCH 43/67] fix: dropdown singleton --- .../status-dropdown/dropdown-manager.ts | 31 ++++++++++--------- components/status-dropdown/dropdown-ui.ts | 10 +++--- .../workspace/workspace-integration.ts | 6 ++-- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/components/status-dropdown/dropdown-manager.ts b/components/status-dropdown/dropdown-manager.ts index e34b400..b72a896 100644 --- a/components/status-dropdown/dropdown-manager.ts +++ b/components/status-dropdown/dropdown-manager.ts @@ -1,4 +1,4 @@ -import { MarkdownView, Editor, Notice, TFile } from 'obsidian'; +import { MarkdownView, Editor, Notice, TFile, App } from 'obsidian'; import { DropdownUI } from './dropdown-ui'; import { DropdownOptions, DropdownDependencies } from './types'; import { createDummyTarget } from './dropdown-position'; @@ -9,12 +9,15 @@ import { NoteStatusSettings } from 'models/types'; * High-level manager for status dropdown interactions */ export class DropdownManager { - private app: any; + private app: App; private settings: NoteStatusSettings; private statusService: StatusService; private currentStatuses: string[] = ['unknown']; private toolbarButton?: HTMLElement; - private dropdownUI: DropdownUI; + private dropdownUI: DropdownUI; + + // Singleton para gestionar todos los dropdowns + private static activeInstance: DropdownManager | null = null; constructor(app: any, settings: NoteStatusSettings, statusService: StatusService) { this.app = app; @@ -35,7 +38,6 @@ export class DropdownManager { this.dropdownUI.setOnStatusChange((statuses) => { this.currentStatuses = [...statuses]; this.updateToolbarButton(); - // this.statusService.notifyStatusChanged(statuses); }); this.dropdownUI.setOnRemoveStatusHandler(async (status, targetFile) => { @@ -99,6 +101,7 @@ export class DropdownManager { private updateToolbarButton(): void { if (!this.toolbarButton) return; + DropdownManager.activeInstance?.dropdownUI.close(); this.toolbarButton.empty(); const hasValidStatus = this.currentStatuses.length > 0 && @@ -158,7 +161,8 @@ export class DropdownManager { /** * Updates the dropdown UI based on current statuses */ - public update(currentStatuses: string[] | string): void { + public update(currentStatuses: string[] | string, file?: TFile): void { + this.currentStatuses = Array.isArray(currentStatuses) ? [...currentStatuses] : [currentStatuses]; @@ -240,18 +244,14 @@ export class DropdownManager { * Universal function to open the status dropdown */ public openStatusDropdown(options: DropdownOptions): void { - if (this.dropdownUI.isOpen) { - this.dropdownUI.close(); - setTimeout(() => this._openStatusDropdown(options), 50); - } else { - this._openStatusDropdown(options); + // Cerrar dropdown activo antes de abrir uno nuevo + if (DropdownManager.activeInstance && DropdownManager.activeInstance !== this) { + console.log("close this:, ",DropdownManager.activeInstance) + this.resetDropdownState(); + DropdownManager.activeInstance.dropdownUI.close(); } - } + DropdownManager.activeInstance = this; - /** - * Internal method to open dropdown - */ - private _openStatusDropdown(options: DropdownOptions): void { const files = options.files || [this.app.workspace.getActiveFile()].filter(Boolean); if (!files.length) { new Notice('No files selected'); @@ -277,6 +277,7 @@ export class DropdownManager { this.positionAndOpenDropdown(options); } + /** * Reset dropdown state before opening diff --git a/components/status-dropdown/dropdown-ui.ts b/components/status-dropdown/dropdown-ui.ts index b44c95a..8c06e63 100644 --- a/components/status-dropdown/dropdown-ui.ts +++ b/components/status-dropdown/dropdown-ui.ts @@ -184,12 +184,10 @@ export class DropdownUI { this.isOpen = false; - setTimeout(() => { - if (this.dropdownElement) { - this.dropdownElement.remove(); - this.dropdownElement = null; - } - }, this.animationDuration); + if (this.dropdownElement) { + this.dropdownElement.remove(); + this.dropdownElement = null; + } } /** diff --git a/integrations/workspace/workspace-integration.ts b/integrations/workspace/workspace-integration.ts index a26a5ad..96a2c71 100644 --- a/integrations/workspace/workspace-integration.ts +++ b/integrations/workspace/workspace-integration.ts @@ -62,7 +62,7 @@ export class WorkspaceIntegration { this.toolbarIntegration.addToolbarButtonToActiveLeaf(); // Actualiza estado - this.propagateNoteStatusChange(); + this.propagateNoteStatusChange(file); } /** @@ -91,7 +91,7 @@ export class WorkspaceIntegration { /** * Verifica y propaga el estado de la nota activa */ - private propagateNoteStatusChange(): void { + private propagateNoteStatusChange(file: TFile): void { try { const activeFile = this.app.workspace.getActiveFile(); let fileStatuses: string[] = []; @@ -100,7 +100,7 @@ export class WorkspaceIntegration { } // Dispara evento para que otros componentes se actualicen window.dispatchEvent(new CustomEvent('note-status:status-changed', { - detail: { statuses: fileStatuses } + detail: { statuses: fileStatuses, file: file } })); } catch (error) { console.error('Error checking note status:', error); From 0cde6b96f8831cee8e5feb211afa9b762e1a9d2e Mon Sep 17 00:00:00 2001 From: Aleix Date: Wed, 21 May 2025 19:25:07 +0200 Subject: [PATCH 44/67] chore: remove unused funcs --- .../status-dropdown/dropdown-manager.ts | 210 ++---------------- components/status-dropdown/dropdown-render.ts | 5 - components/status-dropdown/dropdown-ui.ts | 15 -- components/toolbar-button.ts | 2 +- integrations/editor/toolbar-integration.ts | 2 +- 5 files changed, 24 insertions(+), 210 deletions(-) diff --git a/components/status-dropdown/dropdown-manager.ts b/components/status-dropdown/dropdown-manager.ts index b72a896..653e389 100644 --- a/components/status-dropdown/dropdown-manager.ts +++ b/components/status-dropdown/dropdown-manager.ts @@ -16,9 +16,6 @@ export class DropdownManager { private toolbarButton?: HTMLElement; private dropdownUI: DropdownUI; - // Singleton para gestionar todos los dropdowns - private static activeInstance: DropdownManager | null = null; - constructor(app: any, settings: NoteStatusSettings, statusService: StatusService) { this.app = app; this.settings = settings; @@ -26,19 +23,13 @@ export class DropdownManager { const deps: DropdownDependencies = { app, settings, statusService }; this.dropdownUI = new DropdownUI(deps); - this.setupDropdownCallbacks(); - this.setupCustomEvents(); } /** * Set up dropdown callbacks */ private setupDropdownCallbacks(): void { - this.dropdownUI.setOnStatusChange((statuses) => { - this.currentStatuses = [...statuses]; - this.updateToolbarButton(); - }); this.dropdownUI.setOnRemoveStatusHandler(async (status, targetFile) => { if (!targetFile) return; @@ -47,10 +38,7 @@ export class DropdownManager { files: targetFile, statuses: status, operation: 'remove', - showNotice: false, - afterChange: (updatedStatuses) => { - this.currentStatuses = updatedStatuses; - } + showNotice: false }); }); @@ -84,89 +72,13 @@ export class DropdownManager { }); } - /** - * Set up custom events - */ - private setupCustomEvents(): void { - window.addEventListener('note-status:dropdown-close', () => { - if (this.dropdownUI.isOpen) { - this.dropdownUI.close(); - } - }); - } - - /** - * Updates the toolbar button appearance - */ - private updateToolbarButton(): void { - if (!this.toolbarButton) return; - - DropdownManager.activeInstance?.dropdownUI.close(); - this.toolbarButton.empty(); - - const hasValidStatus = this.currentStatuses.length > 0 && - !this.currentStatuses.every(status => status === 'unknown'); - - const badgeContainer = document.createElement('div'); - badgeContainer.addClass('note-status-toolbar-badge-container'); - - if (hasValidStatus) { - this.addPrimaryStatusIcon(badgeContainer); - } else { - this.addUnknownStatusIcon(badgeContainer); - } - - this.toolbarButton.appendChild(badgeContainer); - } - - /** - * Add primary status icon to container - */ - private addPrimaryStatusIcon(container: HTMLElement): void { - const primaryStatus = this.currentStatuses[0]; - const statusInfo = this.statusService.getAllStatuses().find(s => s.name === primaryStatus); - - if (statusInfo) { - const iconSpan = document.createElement('span'); - iconSpan.addClass(`note-status-toolbar-icon`, `status-${primaryStatus}`); - iconSpan.textContent = statusInfo.icon; - container.appendChild(iconSpan); - - if (this.settings.useMultipleStatuses && this.currentStatuses.length > 1) { - this.addCountBadge(container); - } - } - } - - /** - * Add unknown status icon - */ - private addUnknownStatusIcon(container: HTMLElement): void { - const iconSpan = document.createElement('span'); - iconSpan.addClass('note-status-toolbar-icon', 'status-unknown'); - iconSpan.textContent = this.statusService.getStatusIcon('unknown'); - container.appendChild(iconSpan); - } - - /** - * Add count badge for multiple statuses - */ - private addCountBadge(container: HTMLElement): void { - const countBadge = document.createElement('span'); - countBadge.addClass('note-status-count-badge'); - countBadge.textContent = `+${this.currentStatuses.length - 1}`; - container.appendChild(countBadge); - } - /** * Updates the dropdown UI based on current statuses */ public update(currentStatuses: string[] | string, file?: TFile): void { - this.currentStatuses = Array.isArray(currentStatuses) ? [...currentStatuses] : [currentStatuses]; - this.updateToolbarButton(); this.dropdownUI.updateStatuses(this.currentStatuses); } @@ -175,25 +87,9 @@ export class DropdownManager { */ public updateSettings(settings: NoteStatusSettings): void { this.settings = settings; - this.updateToolbarButton(); this.dropdownUI.updateSettings(settings); } - /** - * Show status dropdown in context menu - */ - public showInContextMenu(editor: Editor, view: MarkdownView): void { - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile) return; - - const position = this.getCursorPosition(editor, view); - - this.openStatusDropdown({ - position, - files: [activeFile] - }); - } - /** * Get position from cursor or fallback */ @@ -219,46 +115,21 @@ export class DropdownManager { return { x: window.innerWidth / 2, y: window.innerHeight / 3 }; } - /** - * Render method (kept for compatibility) - */ - public render(): void { - // No-op - dropdown component handles rendering internally - } - - /** - * Remove dropdown when plugin is unloaded - */ - public unload(): void { - this.dropdownUI.dispose(); - - if (this.toolbarButton) { - this.toolbarButton.remove(); - this.toolbarButton = undefined; - } - - window.removeEventListener('note-status:dropdown-close', () => {}); - } /** * Universal function to open the status dropdown */ public openStatusDropdown(options: DropdownOptions): void { - // Cerrar dropdown activo antes de abrir uno nuevo - if (DropdownManager.activeInstance && DropdownManager.activeInstance !== this) { - console.log("close this:, ",DropdownManager.activeInstance) - this.resetDropdownState(); - DropdownManager.activeInstance.dropdownUI.close(); - } - DropdownManager.activeInstance = this; - const files = options.files || [this.app.workspace.getActiveFile()].filter(Boolean); - if (!files.length) { + if (!files.length || !files[0]) { new Notice('No files selected'); return; } - this.resetDropdownState(); + if (this.dropdownUI.isOpen) { + this.resetDropdown(); + return + } const isSingleFile = files.length === 1; @@ -282,9 +153,9 @@ export class DropdownManager { /** * Reset dropdown state before opening */ - private resetDropdownState(): void { + public resetDropdown(): void { + this.dropdownUI.close(); this.dropdownUI.setTargetFile(null); - this.dropdownUI.setOnStatusChange(() => {}); } /** @@ -354,61 +225,24 @@ export class DropdownManager { ); } - /** - * Adds the toolbar button to the active leaf - */ - public addToolbarButtonToActiveLeaf(): void { - const activeLeaf = this.app.workspace.activeLeaf; - if (!activeLeaf?.view || !(activeLeaf.view instanceof MarkdownView)) return; - - const toolbarContainer = activeLeaf.view.containerEl.querySelector('.view-header .view-actions'); - if (!toolbarContainer) return; - - const existingButton = toolbarContainer.querySelector('.note-status-toolbar-button'); - if (existingButton) { - this.toolbarButton = existingButton as HTMLElement; - this.updateToolbarButton(); - return; - } - - this.toolbarButton = this.createToolbarButton(); - - if (toolbarContainer.firstChild) { - toolbarContainer.insertBefore(this.toolbarButton, toolbarContainer.firstChild); - } else { - toolbarContainer.appendChild(this.toolbarButton); - } - } /** - * Create the toolbar button + * Render method (kept for compatibility) */ - private createToolbarButton(): HTMLElement { - const button = document.createElement('button'); - button.addClass('note-status-toolbar-button', 'clickable-icon', 'view-action'); - button.setAttribute('aria-label', 'Note status'); - - this.updateToolbarButton(); - - button.addEventListener('click', (e) => { - e.stopPropagation(); - e.preventDefault(); - this.toggleStatusPopover(); - }); - - return button; + public render(): void { + // No-op - dropdown component handles rendering internally } - - /** - * Toggle the status popover - */ - private toggleStatusPopover(): void { - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile) return; - this.openStatusDropdown({ - target: this.toolbarButton, - files: [activeFile] - }); + /** + * Remove dropdown when plugin is unloaded + */ + public unload(): void { + this.dropdownUI.dispose(); + + if (this.toolbarButton) { + this.toolbarButton.remove(); + this.toolbarButton = undefined; + } } + } \ No newline at end of file diff --git a/components/status-dropdown/dropdown-render.ts b/components/status-dropdown/dropdown-render.ts index e21e776..4742a9e 100644 --- a/components/status-dropdown/dropdown-render.ts +++ b/components/status-dropdown/dropdown-render.ts @@ -306,11 +306,6 @@ function createStatusOption(options: { setTimeout(async () => { if (targetFiles.length > 0) { await onSelectStatus(status.name, targetFiles); - - if (!settings.useMultipleStatuses) { - // Close signal - The manager component will handle this - window.dispatchEvent(new CustomEvent('note-status:dropdown-close')); - } } }, 150); }); diff --git a/components/status-dropdown/dropdown-ui.ts b/components/status-dropdown/dropdown-ui.ts index 8c06e63..d3b9deb 100644 --- a/components/status-dropdown/dropdown-ui.ts +++ b/components/status-dropdown/dropdown-ui.ts @@ -21,7 +21,6 @@ export class DropdownUI { private animationDuration = 220; public isOpen = false; - private onStatusChange: (statuses: string[]) => void = () => {}; private onRemoveStatus: StatusRemoveHandler = async () => {}; private onSelectStatus: StatusSelectHandler = async () => {}; @@ -68,20 +67,6 @@ export class DropdownUI { public setOnSelectStatusHandler(handler: StatusSelectHandler): void { this.onSelectStatus = handler; } - - /** - * Set callback for status changes - */ - public setOnStatusChange(callback: (statuses: string[]) => void): void { - this.onStatusChange = callback; - } - - /** - * Get the current status change callback - */ - public getOnStatusChange(): (statuses: string[]) => void { - return this.onStatusChange; - } /** * Updates the current statuses diff --git a/components/toolbar-button.ts b/components/toolbar-button.ts index 9ab5b0e..af204bb 100644 --- a/components/toolbar-button.ts +++ b/components/toolbar-button.ts @@ -50,7 +50,7 @@ export class ToolbarButton { if (this.settings.useMultipleStatuses && statuses.length > 1) { const countBadge = document.createElement('span'); countBadge.addClass('note-status-count-badge'); - countBadge.textContent = `+${statuses.length - 1}`; + countBadge.textContent = `${statuses.length}`; container.appendChild(countBadge); } } diff --git a/integrations/editor/toolbar-integration.ts b/integrations/editor/toolbar-integration.ts index be57a95..5e289c9 100644 --- a/integrations/editor/toolbar-integration.ts +++ b/integrations/editor/toolbar-integration.ts @@ -46,6 +46,7 @@ export class ToolbarIntegration { // Create a new button this.buttonElement = this.buttonView.createElement(); + this.buttonElement.addEventListener('click', this.handleButtonClick.bind(this)); if (toolbarContainer.firstChild) { @@ -84,7 +85,6 @@ export class ToolbarIntegration { private handleButtonClick(e: MouseEvent): void { e.stopPropagation(); e.preventDefault(); - const activeFile = this.app.workspace.getActiveFile(); if (!activeFile) return; From 73ab30bcad13521531b829c247e8f74c09476784 Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Fri, 23 May 2025 18:29:40 +0200 Subject: [PATCH 45/67] fix: update toolbar button --- integrations/editor/toolbar-integration.ts | 55 ++++++++++++---------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/integrations/editor/toolbar-integration.ts b/integrations/editor/toolbar-integration.ts index 5e289c9..b2cd5f5 100644 --- a/integrations/editor/toolbar-integration.ts +++ b/integrations/editor/toolbar-integration.ts @@ -14,6 +14,7 @@ export class ToolbarIntegration { private statusDropdown: StatusDropdown; private buttonView: ToolbarButton; private buttonElement: HTMLElement | null = null; + private currentLeafId: string | null = null; constructor( app: App, @@ -38,15 +39,25 @@ export class ToolbarIntegration { const activeLeaf = this.app.workspace.activeLeaf; if (!activeLeaf?.view || !(activeLeaf.view instanceof MarkdownView)) return; - const toolbarContainer = activeLeaf.view.containerEl.querySelector('.view-header .view-actions'); + const leafId = (activeLeaf as any).id || activeLeaf.view.containerEl.id; + + // Only recreate button if we're on a different leaf or button doesn't exist + if (this.currentLeafId !== leafId || !this.buttonElement || !this.isButtonInDOM()) { + this.recreateButton(activeLeaf.view, leafId); + } + + this.updateButtonDisplay(statuses); + } + + private recreateButton(view: MarkdownView, leafId: string): void { + const toolbarContainer = view.containerEl.querySelector('.view-header .view-actions'); if (!toolbarContainer) return; - // Always remove existing button first + // Remove old button if it exists this.removeToolbarButton(); - // Create a new button + // Create new button this.buttonElement = this.buttonView.createElement(); - this.buttonElement.addEventListener('click', this.handleButtonClick.bind(this)); if (toolbarContainer.firstChild) { @@ -55,30 +66,28 @@ export class ToolbarIntegration { toolbarContainer.appendChild(this.buttonElement); } - this.updateButtonDisplay(statuses); + this.currentLeafId = leafId; + } + + private isButtonInDOM(): boolean { + return this.buttonElement?.isConnected === true; } private removeToolbarButton(): void { - const activeLeaf = this.app.workspace.activeLeaf; - if (!activeLeaf?.view || !(activeLeaf.view instanceof MarkdownView)) return; - - const toolbarContainer = activeLeaf.view.containerEl.querySelector('.view-header .view-actions'); - if (!toolbarContainer) return; - - const existingButton = toolbarContainer.querySelector('.note-status-toolbar-button'); - if (existingButton) { - existingButton.remove(); + if (this.buttonElement) { + this.buttonElement.remove(); + this.buttonElement = null; } - - this.buttonElement = null; + this.currentLeafId = null; } - private updateButtonDisplay(overrideStatutes?: string[]): void { + private updateButtonDisplay(overrideStatuses?: string[]): void { + if (!this.buttonElement) return; + const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile || !this.buttonElement) return; + if (!activeFile) return; - - const statuses = overrideStatutes?.length ? overrideStatutes : this.statusService.getFileStatuses(activeFile); + const statuses = overrideStatuses?.length ? overrideStatuses : this.statusService.getFileStatuses(activeFile); this.buttonView.updateDisplay(statuses); } @@ -95,13 +104,11 @@ export class ToolbarIntegration { } public updateStatusDisplay(statuses: string[]): void { - this.removeToolbarButton(); - this.addToolbarButtonToActiveLeaf(statuses); + this.updateButtonDisplay(statuses); } public unload(): void { this.buttonView.destroy(); - this.statusDropdown.unload(); this.removeToolbarButton(); } -} \ No newline at end of file +} From efcbe9df4302964cb31d110f151789b7cc870957 Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Fri, 23 May 2025 18:54:24 +0200 Subject: [PATCH 46/67] chore: restore pane view --- main.ts | 51 +++++++++---------- views/status-pane-view/index.ts | 2 + .../status-pane-view-controller.ts | 32 +++--------- 3 files changed, 32 insertions(+), 53 deletions(-) create mode 100644 views/status-pane-view/index.ts diff --git a/main.ts b/main.ts index c7e34a4..4106fce 100644 --- a/main.ts +++ b/main.ts @@ -11,6 +11,9 @@ import { MetadataIntegration } from './integrations/metadata-cache'; import { WorkspaceIntegration } from './integrations/workspace'; import { FileContextMenuIntegration } from 'integrations/context-menu/file-context-menu-integration'; import { NoteStatusSettingTab } from 'integrations/settings/settings-tab'; +// +// Importar vistas +import { StatusPaneViewController } from './views/status-pane-view'; // Importar componentes UI import { StatusBar } from 'components/status-bar'; @@ -40,15 +43,9 @@ export default class NoteStatus extends Plugin { try { await this.loadSettings(); this.initializeServices(); - - // 2. Registrar vistas y componentes UI - // this.registerViews(); - + this.registerViews(); this.initializeUI(); - - // 3. Inicializar integraciones this.initializeIntegrations(); - // 4. Registrar comandos // this.registerCommands(); @@ -72,15 +69,15 @@ export default class NoteStatus extends Plugin { } private registerViews() { - // Registrar vista del panel de estado - // this.registerView('status-pane', (leaf) => { - // return new StatusPaneView(leaf, this); - // }); + // Register status pane view + this.registerView('status-pane', (leaf) => { + return new StatusPaneViewController(leaf, this); + }); - // Añadir ícono en la barra lateral - // this.addRibbonIcon('status-pane', 'Open status pane', () => { - // this.openStatusPane(); - // }); + // Add ribbon icon + this.addRibbonIcon('status-pane', 'Open status pane', () => { + this.openStatusPane(); + }); // Añadir pestaña de configuración // this.addSettingTab(new NoteStatusSettingTab(this.app, this, this.statusService)); @@ -229,18 +226,18 @@ export default class NoteStatus extends Plugin { } private async openStatusPane() { - // // Comprobar si ya está abierto - // const existing = this.app.workspace.getLeavesOfType('status-pane')[0]; - // if (existing) { - // this.app.workspace.setActiveLeaf(existing); - // return; - // } + // Check if already open + const existing = this.app.workspace.getLeavesOfType('status-pane')[0]; + if (existing) { + this.app.workspace.setActiveLeaf(existing); + return; + } - // // Crear una nueva hoja - // const leaf = this.app.workspace.getLeftLeaf(false); - // if (leaf) { - // await leaf.setViewState({ type: 'status-pane', active: true }); - // } + // Create new leaf + const leaf = this.app.workspace.getLeftLeaf(false); + if (leaf) { + await leaf.setViewState({ type: 'status-pane', active: true }); + } } async saveSettings() { @@ -283,4 +280,4 @@ export default class NoteStatus extends Plugin { // // Limpiar componentes UI // this.statusBar.unload(); } -} \ No newline at end of file +} diff --git a/views/status-pane-view/index.ts b/views/status-pane-view/index.ts new file mode 100644 index 0000000..93b29c9 --- /dev/null +++ b/views/status-pane-view/index.ts @@ -0,0 +1,2 @@ +export { StatusPaneView } from './status-pane-view'; +export { StatusPaneViewController } from './status-pane-view-controller'; diff --git a/views/status-pane-view/status-pane-view-controller.ts b/views/status-pane-view/status-pane-view-controller.ts index 99cfb20..7f59312 100644 --- a/views/status-pane-view/status-pane-view-controller.ts +++ b/views/status-pane-view/status-pane-view-controller.ts @@ -1,4 +1,4 @@ -import { TFile, WorkspaceLeaf, View, Menu, Notice } from 'obsidian'; +import { TFile, WorkspaceLeaf, View, Notice } from 'obsidian'; import { NoteStatusSettings } from '../../models/types'; import { StatusService } from '../../services/status-service'; import NoteStatus from 'main'; @@ -9,7 +9,7 @@ export class StatusPaneViewController extends View { private settings: NoteStatusSettings; private statusService: StatusService; private plugin: NoteStatus; - private searchQuery: string = ''; + private searchQuery = ''; private paginationState = { itemsPerPage: 100, currentPage: {} as Record @@ -47,6 +47,10 @@ export class StatusPaneViewController extends View { this.renderer.createHeader(containerEl, this.settings.compactView, { onSearch: (query) => { + this.paginationState = { + itemsPerPage: 100, + currentPage: {} as Record + } this.searchQuery = query; this.renderGroups(query); }, @@ -103,7 +107,6 @@ export class StatusPaneViewController extends View { window.dispatchEvent(new CustomEvent('note-status:settings-changed')); }, onContextMenu: (e, file) => { - this.showFileContextMenu(e, file); }, onPageChange: (status, page) => { this.paginationState.currentPage[status] = page; @@ -140,29 +143,6 @@ export class StatusPaneViewController extends View { return filteredGroups; } - private showFileContextMenu(e: MouseEvent, file: TFile): void { - const menu = new Menu(); - - menu.addItem((item) => - item.setTitle('Change status') - .setIcon('tag') - .onClick(() => { - const position = { x: e.clientX, y: e.clientY }; - this.plugin.statusContextMenu.showForSingleFile(file, position); - }) - ); - - menu.addItem((item) => - item.setTitle('Open in new tab') - .setIcon('lucide-external-link') - .onClick(() => { - this.app.workspace.openLinkText(file.path, file.path, 'tab'); - }) - ); - - menu.showAtMouseEvent(e); - } - updateSettings(settings: NoteStatusSettings): void { this.settings = settings; this.containerEl.toggleClass('note-status-compact-view', settings.compactView); From f9a51d3a8c1c71f979120db91af4b7d0541c25e3 Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Fri, 23 May 2025 19:08:25 +0200 Subject: [PATCH 47/67] fix: file explorer without icons --- integrations/explorer/explorer-integration.ts | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/integrations/explorer/explorer-integration.ts b/integrations/explorer/explorer-integration.ts index 3d31f2a..a1c29d3 100644 --- a/integrations/explorer/explorer-integration.ts +++ b/integrations/explorer/explorer-integration.ts @@ -14,13 +14,34 @@ export class ExplorerIntegration { private iconUpdateQueue = new Set(); private isProcessingQueue = false; private debouncedUpdateAll: ReturnType; + private fileItemsWasUndefined = false; constructor(app: App, settings: NoteStatusSettings, statusService: StatusService) { this.app = app; this.settings = settings; this.statusService = statusService; - this.ui = new ExplorerIntegrationUI(app, settings, statusService); + this.ui = new ExplorerIntegrationUI(app, settings, statusService, () => { + this.fileItemsWasUndefined = true; + }); this.debouncedUpdateAll = debounce(this.processUpdateQueue.bind(this), 100, true); + + // Listen for layout changes to detect when file explorer becomes available + this.app.workspace.on('layout-change', () => { + this.checkFileExplorerAvailability(); + }); + } + + /** + * Check if file explorer became available after being unavailable + */ + private checkFileExplorerAvailability(): void { + if (!this.settings.showStatusIconsInExplorer) return; + + const fileExplorer = this.ui.findFileExplorerView(); + if (fileExplorer?.fileItems && this.fileItemsWasUndefined) { + this.fileItemsWasUndefined = false; + setTimeout(() => this.updateAllFileExplorerIcons(), 100); + } } /** @@ -98,7 +119,8 @@ export class ExplorerIntegration { try { const fileExplorerView = this.ui.findFileExplorerView(); - if (!fileExplorerView) { + if (!fileExplorerView || !fileExplorerView.fileItems) { + this.fileItemsWasUndefined = true; setTimeout(() => this.debouncedUpdateAll(), 200); return; } @@ -168,10 +190,12 @@ export class ExplorerIntegration { export class ExplorerIntegrationUI { private app: App; private settings: NoteStatusSettings; + private onFileItemsUndefined: () => void; - constructor(app: App, settings: NoteStatusSettings, statusService: StatusService) { + constructor(app: App, settings: NoteStatusSettings, statusService: StatusService, onFileItemsUndefined: () => void) { this.app = app; this.settings = settings; + this.onFileItemsUndefined = onFileItemsUndefined; } /** @@ -204,6 +228,12 @@ export class ExplorerIntegrationUI { if (!this.settings.showStatusIconsInExplorer || file.extension !== 'md') return; try { + // Check if fileItems is initialized + if (!fileExplorerView.fileItems) { + this.onFileItemsUndefined(); + return; + } + const fileItem = fileExplorerView.fileItems[file.path]; if (!fileItem) return; @@ -317,4 +347,4 @@ export class ExplorerIntegrationUI { container.appendChild(iconEl); } -} \ No newline at end of file +} From 1155c0779420b936768ed773f2e5fe4394d91057 Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Fri, 23 May 2025 19:13:19 +0200 Subject: [PATCH 48/67] fix: use standard icon --- main.ts | 2 +- manifest.json | 2 +- views/status-pane-view/status-pane-view-controller.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/main.ts b/main.ts index 4106fce..2a7ddd3 100644 --- a/main.ts +++ b/main.ts @@ -75,7 +75,7 @@ export default class NoteStatus extends Plugin { }); // Add ribbon icon - this.addRibbonIcon('status-pane', 'Open status pane', () => { + this.addRibbonIcon('tag', 'Open status pane', () => { this.openStatusPane(); }); diff --git a/manifest.json b/manifest.json index d74effb..f6553ac 100644 --- a/manifest.json +++ b/manifest.json @@ -11,7 +11,7 @@ }, "isDesktopOnly": false, "license": "MIT", - "icon": "status-pane", + "icon": "tag", "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.", diff --git a/views/status-pane-view/status-pane-view-controller.ts b/views/status-pane-view/status-pane-view-controller.ts index 7f59312..1aec403 100644 --- a/views/status-pane-view/status-pane-view-controller.ts +++ b/views/status-pane-view/status-pane-view-controller.ts @@ -27,7 +27,7 @@ export class StatusPaneViewController extends View { getDisplayText(): string { return 'Status pane'; } - getIcon(): string { return 'status-pane'; } + getIcon(): string { return 'tag'; } async onOpen(): Promise { await this.setupPane(); From 2096745b6b1338ef56ce37295ee2e0a08ad86b25 Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Fri, 23 May 2025 19:27:55 +0200 Subject: [PATCH 49/67] chore: wip update status pane --- main.ts | 5 +++++ views/status-pane-view/status-pane-view-controller.ts | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/main.ts b/main.ts index 2a7ddd3..4a3b245 100644 --- a/main.ts +++ b/main.ts @@ -189,6 +189,11 @@ export default class NoteStatus extends Plugin { // Actualizar toolbar this.toolbarIntegration.updateStatusDisplay(statuses); this.statusDropdown.update(statuses); + // Actualizar status pane si está abierto + const statusPaneLeaf = this.app.workspace.getLeavesOfType('status-pane')[0]; + if (statusPaneLeaf?.view) { + (statusPaneLeaf.view as StatusPaneViewController).update(); + } // Actualizar explorador si es necesario if (this.settings.showStatusIconsInExplorer && file) { const fileObj = this.app.vault.getFileByPath(file); diff --git a/views/status-pane-view/status-pane-view-controller.ts b/views/status-pane-view/status-pane-view-controller.ts index 1aec403..6d26b0b 100644 --- a/views/status-pane-view/status-pane-view-controller.ts +++ b/views/status-pane-view/status-pane-view-controller.ts @@ -148,4 +148,11 @@ export class StatusPaneViewController extends View { this.containerEl.toggleClass('note-status-compact-view', settings.compactView); this.renderGroups(this.searchQuery); } + + public update(): void { + // TODO: needs to be improved/fixed, every change + // this.renderGroups(this.searchQuery).then(() => { + // + // }); + } } From d927d83ac7454c3799e929475b37f838acdb44ae Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Sat, 24 May 2025 09:04:43 +0200 Subject: [PATCH 50/67] fix: verify the view instance --- main.ts | 15 ++------------- .../status-pane-view-controller.ts | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/main.ts b/main.ts index 4a3b245..e4115db 100644 --- a/main.ts +++ b/main.ts @@ -191,7 +191,7 @@ export default class NoteStatus extends Plugin { this.statusDropdown.update(statuses); // Actualizar status pane si está abierto const statusPaneLeaf = this.app.workspace.getLeavesOfType('status-pane')[0]; - if (statusPaneLeaf?.view) { + if (statusPaneLeaf?.view && statusPaneLeaf.view instanceof StatusPaneViewController) { (statusPaneLeaf.view as StatusPaneViewController).update(); } // Actualizar explorador si es necesario @@ -231,18 +231,7 @@ export default class NoteStatus extends Plugin { } private async openStatusPane() { - // Check if already open - const existing = this.app.workspace.getLeavesOfType('status-pane')[0]; - if (existing) { - this.app.workspace.setActiveLeaf(existing); - return; - } - - // Create new leaf - const leaf = this.app.workspace.getLeftLeaf(false); - if (leaf) { - await leaf.setViewState({ type: 'status-pane', active: true }); - } + await StatusPaneViewController.open(this.app); } async saveSettings() { diff --git a/views/status-pane-view/status-pane-view-controller.ts b/views/status-pane-view/status-pane-view-controller.ts index 6d26b0b..4156db8 100644 --- a/views/status-pane-view/status-pane-view-controller.ts +++ b/views/status-pane-view/status-pane-view-controller.ts @@ -1,4 +1,4 @@ -import { TFile, WorkspaceLeaf, View, Notice } from 'obsidian'; +import { TFile, WorkspaceLeaf, View, Notice, App } from 'obsidian'; import { NoteStatusSettings } from '../../models/types'; import { StatusService } from '../../services/status-service'; import NoteStatus from 'main'; @@ -15,6 +15,19 @@ export class StatusPaneViewController extends View { currentPage: {} as Record }; + static async open(app: App): Promise { + const existing = app.workspace.getLeavesOfType('status-pane')[0]; + if (existing) { + app.workspace.setActiveLeaf(existing); + return; + } + + const leaf = app.workspace.getLeftLeaf(false); + if (leaf) { + await leaf.setViewState({ type: 'status-pane', active: true }); + } + } + constructor(leaf: WorkspaceLeaf, plugin: NoteStatus) { super(leaf); this.plugin = plugin; From c139ff56239e48a0855aec861d21ff105857709f Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Sat, 24 May 2025 11:11:28 +0200 Subject: [PATCH 51/67] fix: update toolbar if is the active file --- main.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/main.ts b/main.ts index e4115db..fda59f4 100644 --- a/main.ts +++ b/main.ts @@ -187,7 +187,10 @@ export default class NoteStatus extends Plugin { // Actualizar barra de estado this.statusBar.update(statuses); // Actualizar toolbar - this.toolbarIntegration.updateStatusDisplay(statuses); + const activeFile = this.app.workspace.getActiveFile(); + if (activeFile?.path === file) { + this.toolbarIntegration.updateStatusDisplay(statuses); + } this.statusDropdown.update(statuses); // Actualizar status pane si está abierto const statusPaneLeaf = this.app.workspace.getLeavesOfType('status-pane')[0]; From 632f7d8f7d90d7746609fed94c12028d5cad6d9d Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Sat, 24 May 2025 11:27:00 +0200 Subject: [PATCH 52/67] fix: multipole dropdown badge click fixed --- components/status-dropdown/dropdown-manager.ts | 6 ++++-- components/status-dropdown/dropdown-render.ts | 18 ++++++++++-------- components/status-dropdown/types.ts | 4 ++-- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/components/status-dropdown/dropdown-manager.ts b/components/status-dropdown/dropdown-manager.ts index 653e389..d99e010 100644 --- a/components/status-dropdown/dropdown-manager.ts +++ b/components/status-dropdown/dropdown-manager.ts @@ -34,11 +34,13 @@ export class DropdownManager { this.dropdownUI.setOnRemoveStatusHandler(async (status, targetFile) => { if (!targetFile) return; + const isMultiple = Array.isArray(targetFile); + await this.statusService.handleStatusChange({ files: targetFile, statuses: status, operation: 'remove', - showNotice: false + showNotice: isMultiple }); }); @@ -245,4 +247,4 @@ export class DropdownManager { } } -} \ No newline at end of file +} diff --git a/components/status-dropdown/dropdown-render.ts b/components/status-dropdown/dropdown-render.ts index 4742a9e..de65496 100644 --- a/components/status-dropdown/dropdown-render.ts +++ b/components/status-dropdown/dropdown-render.ts @@ -31,7 +31,9 @@ export function renderDropdownContent(options: { // Create UI sections createHeader(dropdownElement, targetFiles); - createStatusChips(dropdownElement, currentStatuses, statusService, targetFile, onRemoveStatus); + + const target = targetFiles.length > 1 ? targetFiles : targetFile; + createStatusChips(dropdownElement, currentStatuses, statusService, target ?? [], onRemoveStatus); const searchInput = createSearchFilter(dropdownElement); // Create status options container @@ -96,7 +98,7 @@ function createStatusChips( dropdownElement: HTMLElement, currentStatuses: string[], statusService: StatusService, - targetFile: TFile | null, + targetFile: TFile | TFile[], onRemoveStatus: StatusRemoveHandler ): void { const chipsContainer = dropdownElement.createDiv({ cls: 'note-status-popover-chips' }); @@ -121,7 +123,7 @@ function createStatusChipElements( container: HTMLElement, currentStatuses: string[], statusService: StatusService, - targetFile: TFile | null, + target: TFile | TFile[], onRemoveStatus: StatusRemoveHandler ): void { currentStatuses.forEach(status => { @@ -130,7 +132,7 @@ function createStatusChipElements( const statusObj = statusService.getAllStatuses().find(s => s.name === status); if (!statusObj) return; - createSingleStatusChip(container, status, statusObj, targetFile, onRemoveStatus); + createSingleStatusChip(container, status, statusObj, target, onRemoveStatus); }); } @@ -141,7 +143,7 @@ function createSingleStatusChip( container: HTMLElement, status: string, statusObj: Status, - targetFile: TFile | null, + target: TFile | TFile[], onRemoveStatus: StatusRemoveHandler ): void { const chipEl = container.createDiv({ @@ -159,7 +161,7 @@ function createSingleStatusChip( cls: 'note-status-chip-text' }); - addRemoveButton(chipEl, status, statusObj, targetFile, onRemoveStatus); + addRemoveButton(chipEl, status, statusObj, target, onRemoveStatus); } /** @@ -169,7 +171,7 @@ function addRemoveButton( chipEl: HTMLElement, status: string, statusObj: Status, - targetFile: TFile | null, + targetFile: TFile | TFile[] | null, onRemoveStatus: StatusRemoveHandler ): void { const tooltipValue = statusObj.description ? `${status} - ${statusObj.description}`: status; @@ -309,4 +311,4 @@ function createStatusOption(options: { } }, 150); }); -} \ No newline at end of file +} diff --git a/components/status-dropdown/types.ts b/components/status-dropdown/types.ts index b412ffe..da57db6 100644 --- a/components/status-dropdown/types.ts +++ b/components/status-dropdown/types.ts @@ -19,7 +19,7 @@ export interface DropdownOptions { /** * Status removal handler function type */ -export type StatusRemoveHandler = (status: string, targetFile?: TFile) => Promise; +export type StatusRemoveHandler = (status: string, targetFile?: TFile | TFile[]) => Promise; /** * Status selection handler function type @@ -33,4 +33,4 @@ export interface DropdownDependencies { app: App; settings: NoteStatusSettings; statusService: StatusService; -} \ No newline at end of file +} From 23118c613c33bd417a6de6df98f4cf32063be8cc Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Sat, 24 May 2025 14:14:40 +0200 Subject: [PATCH 53/67] chore: restore settings --- integrations/settings/settings-controller.ts | 119 +++++++ integrations/settings/settings-tab.ts | 357 +------------------ integrations/settings/settings-ui.ts | 227 ++++++++++++ main.ts | 28 +- 4 files changed, 372 insertions(+), 359 deletions(-) create mode 100644 integrations/settings/settings-controller.ts create mode 100644 integrations/settings/settings-ui.ts diff --git a/integrations/settings/settings-controller.ts b/integrations/settings/settings-controller.ts new file mode 100644 index 0000000..addcbd8 --- /dev/null +++ b/integrations/settings/settings-controller.ts @@ -0,0 +1,119 @@ +import { App } from 'obsidian'; +import { Status } from '../../models/types'; +import NoteStatus from 'main'; +import { StatusService } from 'services/status-service'; +import { NoteStatusSettingsUI, SettingsUICallbacks } from './settings-ui'; + +/** + * Controller for settings UI, handles business logic and plugin state updates + */ +export class NoteStatusSettingsController implements SettingsUICallbacks { + private app: App; + private plugin: NoteStatus; + private statusService: StatusService; + private ui: NoteStatusSettingsUI; + + constructor(app: App, plugin: NoteStatus, statusService: StatusService) { + this.app = app; + this.plugin = plugin; + this.statusService = statusService; + this.ui = new NoteStatusSettingsUI(this); + } + + /** + * Renders the settings interface + */ + display(containerEl: HTMLElement): void { + this.ui.render(containerEl, this.plugin.settings); + } + + /** + * Handles template enable/disable toggle + */ + async onTemplateToggle(templateId: string, enabled: boolean): Promise { + if (enabled) { + if (!this.plugin.settings.enabledTemplates.includes(templateId)) { + this.plugin.settings.enabledTemplates.push(templateId); + } + } else { + this.plugin.settings.enabledTemplates = this.plugin.settings.enabledTemplates.filter( + (id: string) => id !== templateId + ); + } + + await this.plugin.saveSettings(); + } + + /** + * Handles general setting changes + */ + async onSettingChange(key: string, value: any): Promise { + (this.plugin.settings as any)[key] = value; + await this.plugin.saveSettings(); + } + + /** + * Handles custom status field changes + */ + async onCustomStatusChange(index: number, field: string, value: any): Promise { + const status = this.plugin.settings.customStatuses[index]; + if (!status) return; + + if (field === 'name') { + const oldName = status.name; + status.name = value; + + if (oldName !== status.name) { + this.plugin.settings.statusColors[status.name] = + this.plugin.settings.statusColors[oldName]; + delete this.plugin.settings.statusColors[oldName]; + } + } else if (field === 'color') { + this.plugin.settings.statusColors[status.name] = value; + } else { + (status as any)[field] = value; + } + + await this.plugin.saveSettings(); + } + + /** + * Handles custom status removal + */ + async onCustomStatusRemove(index: number): Promise { + const status = this.plugin.settings.customStatuses[index]; + if (!status) return; + + this.plugin.settings.customStatuses.splice(index, 1); + delete this.plugin.settings.statusColors[status.name]; + + await this.plugin.saveSettings(); + + // Re-render the custom statuses section + const statusList = document.querySelector('.custom-status-list') as HTMLElement; + if (statusList) { + this.ui.renderCustomStatuses(statusList, this.plugin.settings); + } + } + + /** + * Handles adding new custom status + */ + async onCustomStatusAdd(): Promise { + const newStatus: Status = { + name: `status${this.plugin.settings.customStatuses.length + 1}`, + icon: '⭐' + }; + + this.plugin.settings.customStatuses.push(newStatus); + this.plugin.settings.statusColors[newStatus.name] = '#ffffff'; + + await this.plugin.saveSettings(); + + // Re-render the custom statuses section + const statusList = document.querySelector('.custom-status-list') as HTMLElement; + if (statusList) { + this.ui.renderCustomStatuses(statusList, this.plugin.settings); + } + } +} diff --git a/integrations/settings/settings-tab.ts b/integrations/settings/settings-tab.ts index d34334f..f59f470 100644 --- a/integrations/settings/settings-tab.ts +++ b/integrations/settings/settings-tab.ts @@ -1,352 +1,23 @@ -import { App, PluginSettingTab, Setting, Notice } from 'obsidian'; -import { Status } from '../../models/types'; -import { PREDEFINED_TEMPLATES } from '../../constants/status-templates'; +import { App, PluginSettingTab } from 'obsidian'; import NoteStatus from 'main'; import { StatusService } from 'services/status-service'; +import { NoteStatusSettingsController } from './settings-controller'; /** - * Settings tab for the Note Status plugin + * Settings tab for the Note Status plugin - delegates to controller */ export class NoteStatusSettingTab extends PluginSettingTab { - plugin: NoteStatus; - statusService: StatusService + private controller: NoteStatusSettingsController; - constructor(app: App, plugin: any, statusService: StatusService) { - super(app, plugin); - this.plugin = plugin; - this.statusService = statusService; - } + constructor(app: App, plugin: NoteStatus, statusService: StatusService) { + super(app, plugin); + this.controller = new NoteStatusSettingsController(app, plugin, statusService); + } - display(): void { - const { containerEl } = this; - containerEl.empty(); - - // Status Template section - this.displayTemplateSettings(containerEl.createDiv()); - - // UI section - new Setting(containerEl).setName('User interface').setHeading(); - - - // Status bar settings - new Setting(containerEl) - .setName('Show status bar') - .setDesc('Display the status bar') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.showStatusBar) - .onChange(async (value) => { - this.plugin.settings.showStatusBar = value; - await this.plugin.saveSettings(); - })); - - 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(); - })); - - // File explorer settings - new Setting(containerEl) - .setName('Show status icons in file explorer') - .setDesc('Display status icons in the file explorer') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.showStatusIconsInExplorer) - .onChange(async (value) => { - this.plugin.settings.showStatusIconsInExplorer = value; - await this.plugin.saveSettings(); - - // Explicitly refresh explorer icons - if (value) { - this.plugin.explorerIntegration.updateAllFileExplorerIcons(); - } else { - this.plugin.explorerIntegration.removeAllFileExplorerIcons(); - } - })); - - // NEW SETTING: Hide unknown status in explorer - new Setting(containerEl) - .setName('Hide unknown status in file explorer') - .setDesc('Hide status icons for files with unknown status in the file explorer') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.hideUnknownStatusInExplorer || false) - .onChange(async (value) => { - this.plugin.settings.hideUnknownStatusInExplorer = value; - await this.plugin.saveSettings(); - - // Refresh explorer icons when this setting changes - this.plugin.explorerIntegration.updateAllFileExplorerIcons(); - })); - - // Compact view setting - new Setting(containerEl) - .setName('Default to compact view') - .setDesc('Start the status pane in compact view by default') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.compactView || false) - .onChange(async (value) => { - this.plugin.settings.compactView = value; - await this.plugin.saveSettings(); - })); - - // Add option to exclude unknown status in pane view for performance - new Setting(containerEl) - .setName('Exclude unassigned notes from status pane') - .setDesc('Improves performance by excluding notes with no assigned status from the status pane. Recommended for large vaults.') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.excludeUnknownStatus || false) - .onChange(async (value) => { - this.plugin.settings.excludeUnknownStatus = value; - await this.plugin.saveSettings(); - - // Refresh the status pane if open - const statusPane = this.app.workspace.getLeavesOfType('status-pane')[0]; - if (statusPane && statusPane.view) { - this.statusService.refreshUI([]); - } - })); - - new Setting(containerEl).setName('Status tag').setHeading(); - - // Option to use multiple statuses - new Setting(containerEl) - .setName('Enable multiple statuses') - .setDesc('Allow notes to have multiple statuses at the same time') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.useMultipleStatuses) - .onChange(async (value) => { - this.plugin.settings.useMultipleStatuses = value; - await this.plugin.saveSettings(); - - // Refresh UI to show multi-select or single-select options - this.statusService.refreshUI([]); - })); - - // Status tag prefix - new Setting(containerEl) - .setName('Status tag prefix') - .setDesc('The YAML frontmatter tag name used for status (default: obsidian-note-status)') - .addText(text => text - .setValue(this.plugin.settings.tagPrefix) - .onChange(async (value) => { - // Don't allow empty tag prefix - if (!value.trim()) { - return; - } - - this.plugin.settings.tagPrefix = value.trim(); - await this.plugin.saveSettings(); - this.statusService.refreshUI([]); - })); - - - // Status management section - new Setting(containerEl).setName('Custom statuses').setHeading(); - - // Option to use only custom statuses - new Setting(containerEl) - .setName('Use only custom statuses') - .setDesc('Ignore template statuses and use only the custom statuses defined below') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.useCustomStatusesOnly || false) - .onChange(async (value) => { - this.plugin.settings.useCustomStatusesOnly = value; - await this.plugin.saveSettings(); - - // Refresh the UI to show/hide template statuses - this.statusService.refreshUI([]); - this.display(); - })); - - // Custom statuses list - const statusList = containerEl.createDiv({ cls: 'custom-status-list' }); - - 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 - now properly implemented - setting.addText(text => text - .setPlaceholder('Name') - .setValue(status.name) - .onChange(async (value) => { - const oldName = status.name; - status.name = value || 'unnamed'; - - // Update color mapping when name changes - if (oldName !== status.name) { - this.plugin.settings.statusColors[status.name] = - this.plugin.settings.statusColors[oldName]; - delete this.plugin.settings.statusColors[oldName]; - } - - await this.plugin.saveSettings(); - })); - - // Icon field - setting.addText(text => text - .setPlaceholder('Icon') - .setValue(status.icon) - .onChange(async (value) => { - status.icon = value || '❓'; - await this.plugin.saveSettings(); - })); - - // Color picker - setting.addColorPicker(colorPicker => colorPicker - .setValue(this.plugin.settings.statusColors[status.name] || '#ffffff') - .onChange(async (value) => { - this.plugin.settings.statusColors[status.name] = value; - await this.plugin.saveSettings(); - })); - - // Description field - 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') - .setClass('status-remove-button') - .setWarning() - .onClick(async () => { - this.plugin.settings.customStatuses.splice(index, 1); - delete this.plugin.settings.statusColors[status.name]; - await this.plugin.saveSettings(); - renderStatuses(); - })); - }); - }; - - renderStatuses(); - - // Add new status - 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'; // Initial white color - - await this.plugin.saveSettings(); - renderStatuses(); - })); - } - - /** - * Display template settings section - */ - private displayTemplateSettings(containerEl: HTMLElement): void { - new Setting(containerEl).setName('Status templates').setHeading(); - containerEl.createEl('p', { - text: 'Enable predefined templates to quickly add common status workflows', - cls: 'setting-item-description' - }); - - // Create templates container - const templatesContainer = containerEl.createDiv({ cls: 'templates-container' }); - - // List each template with checkbox and preview - PREDEFINED_TEMPLATES.forEach(template => { - const templateEl = templatesContainer.createDiv({ cls: 'template-item' }); - - // Template header with checkbox and name - const headerEl = templateEl.createDiv({ cls: 'template-header' }); - - // Checkbox for enabling/disabling template - const isEnabled = this.plugin.settings.enabledTemplates.includes(template.id); - const checkbox = headerEl.createEl('input', { - type: 'checkbox', - cls: 'template-checkbox' - }); - checkbox.checked = isEnabled; - - checkbox.addEventListener('change', async () => { - if (checkbox.checked) { - // Enable template - if (!this.plugin.settings.enabledTemplates.includes(template.id)) { - this.plugin.settings.enabledTemplates.push(template.id); - } - } else { - // Disable template - this.plugin.settings.enabledTemplates = this.plugin.settings.enabledTemplates.filter( - (id: string) => id !== template.id - ); - } - - await this.plugin.saveSettings(); - this.statusService.refreshUI([]); - }); - - // Template name - headerEl.createEl('span', { - text: template.name, - cls: 'template-name' - }); - - // Template description - templateEl.createEl('div', { - text: template.description, - cls: 'template-description' - }); - - // Preview statuses - const statusesEl = templateEl.createDiv({ cls: 'template-statuses' }); - - template.statuses.forEach(status => { - const statusEl = statusesEl.createEl('div', { cls: 'template-status-chip' }); - - const colorDot = statusEl.createEl('span', { cls: 'status-color-dot' }); - colorDot.style.setProperty('--dot-color', status.color || '#ffffff') - - // Status icon and name - statusEl.createSpan({ text: `${status.icon} ${status.name}` }); - }); - }); - - // Import/Export buttons for templates - const buttonsContainer = containerEl.createDiv({ cls: 'template-buttons' }); - - // Import button - const importButton = buttonsContainer.createEl('button', { - text: 'Import template', - cls: 'mod-cta' - }); - - importButton.addEventListener('click', () => { - // Show notification about upcoming feature - new Notice('Template import functionality coming soon!'); - }); - - // Export button - const exportButton = buttonsContainer.createEl('button', { - text: 'Export templates', - cls: '' - }); - - exportButton.addEventListener('click', () => { - // Show notification about upcoming feature - new Notice('Template export functionality coming soon!'); - }); - } + /** + * Displays the settings interface + */ + display(): void { + this.controller.display(this.containerEl); + } } diff --git a/integrations/settings/settings-ui.ts b/integrations/settings/settings-ui.ts new file mode 100644 index 0000000..ab166dd --- /dev/null +++ b/integrations/settings/settings-ui.ts @@ -0,0 +1,227 @@ +import { Setting } from 'obsidian'; +import { Status } from '../../models/types'; +import { PREDEFINED_TEMPLATES } from '../../constants/status-templates'; + +/** + * Callbacks interface for settings UI interactions + */ +export interface SettingsUICallbacks { + /** Handle template enable/disable toggle */ + onTemplateToggle: (templateId: string, enabled: boolean) => Promise; + /** Handle general setting changes */ + onSettingChange: (key: string, value: any) => Promise; + /** Handle custom status field changes */ + onCustomStatusChange: (index: number, field: string, value: any) => Promise; + /** Handle custom status removal */ + onCustomStatusRemove: (index: number) => Promise; + /** Handle adding new custom status */ + onCustomStatusAdd: () => Promise; +} + +/** + * Pure UI component for rendering settings interface + */ +export class NoteStatusSettingsUI { + private callbacks: SettingsUICallbacks; + + constructor(callbacks: SettingsUICallbacks) { + this.callbacks = callbacks; + } + + /** + * Renders the complete settings interface + */ + render(containerEl: HTMLElement, settings: any): void { + containerEl.empty(); + + this.renderTemplateSettings(containerEl, settings); + this.renderUISettings(containerEl, settings); + this.renderTagSettings(containerEl, settings); + this.renderCustomStatusSettings(containerEl, settings); + } + + /** + * Renders the status templates section + */ + private renderTemplateSettings(containerEl: HTMLElement, settings: any): void { + new Setting(containerEl).setName('Status templates').setHeading(); + + containerEl.createEl('p', { + text: 'Enable predefined templates to quickly add common status workflows', + cls: 'setting-item-description' + }); + + const templatesContainer = containerEl.createDiv({ cls: 'templates-container' }); + + PREDEFINED_TEMPLATES.forEach(template => { + const templateEl = templatesContainer.createDiv({ cls: 'template-item' }); + const headerEl = templateEl.createDiv({ cls: 'template-header' }); + + const isEnabled = settings.enabledTemplates.includes(template.id); + const checkbox = headerEl.createEl('input', { + type: 'checkbox', + cls: 'template-checkbox' + }); + checkbox.checked = isEnabled; + + checkbox.addEventListener('change', () => { + this.callbacks.onTemplateToggle(template.id, checkbox.checked); + }); + + headerEl.createEl('span', { + text: template.name, + cls: 'template-name' + }); + + templateEl.createEl('div', { + text: template.description, + cls: 'template-description' + }); + + const statusesEl = templateEl.createDiv({ cls: 'template-statuses' }); + template.statuses.forEach(status => { + const statusEl = statusesEl.createEl('div', { cls: 'template-status-chip' }); + const colorDot = statusEl.createEl('span', { cls: 'status-color-dot' }); + colorDot.style.setProperty('--dot-color', status.color || '#ffffff'); + statusEl.createSpan({ text: `${status.icon} ${status.name}` }); + }); + }); + } + + /** + * Renders the UI display settings section + */ + private renderUISettings(containerEl: HTMLElement, settings: any): void { + new Setting(containerEl).setName('User interface').setHeading(); + + new Setting(containerEl) + .setName('Show status bar') + .setDesc('Display the status bar') + .addToggle(toggle => toggle + .setValue(settings.showStatusBar) + .onChange(value => this.callbacks.onSettingChange('showStatusBar', value))); + + new Setting(containerEl) + .setName('Auto-hide status bar') + .setDesc('Hide the status bar when status is unknown') + .addToggle(toggle => toggle + .setValue(settings.autoHideStatusBar) + .onChange(value => this.callbacks.onSettingChange('autoHideStatusBar', value))); + + new Setting(containerEl) + .setName('Show status icons in file explorer') + .setDesc('Display status icons in the file explorer') + .addToggle(toggle => toggle + .setValue(settings.showStatusIconsInExplorer) + .onChange(value => this.callbacks.onSettingChange('showStatusIconsInExplorer', value))); + + new Setting(containerEl) + .setName('Hide unknown status in file explorer') + .setDesc('Hide status icons for files with unknown status in the file explorer') + .addToggle(toggle => toggle + .setValue(settings.hideUnknownStatusInExplorer || false) + .onChange(value => this.callbacks.onSettingChange('hideUnknownStatusInExplorer', value))); + + new Setting(containerEl) + .setName('Default to compact view') + .setDesc('Start the status pane in compact view by default') + .addToggle(toggle => toggle + .setValue(settings.compactView || false) + .onChange(value => this.callbacks.onSettingChange('compactView', value))); + + new Setting(containerEl) + .setName('Exclude unassigned notes from status pane') + .setDesc('Improves performance by excluding notes with no assigned status from the status pane. Recommended for large vaults.') + .addToggle(toggle => toggle + .setValue(settings.excludeUnknownStatus || false) + .onChange(value => this.callbacks.onSettingChange('excludeUnknownStatus', value))); + } + + /** + * Renders the status tag configuration section + */ + private renderTagSettings(containerEl: HTMLElement, settings: any): void { + new Setting(containerEl).setName('Status tag').setHeading(); + + new Setting(containerEl) + .setName('Enable multiple statuses') + .setDesc('Allow notes to have multiple statuses at the same time') + .addToggle(toggle => toggle + .setValue(settings.useMultipleStatuses) + .onChange(value => this.callbacks.onSettingChange('useMultipleStatuses', value))); + + new Setting(containerEl) + .setName('Status tag prefix') + .setDesc('The YAML frontmatter tag name used for status (default: obsidian-note-status)') + .addText(text => text + .setValue(settings.tagPrefix) + .onChange(value => { + if (value.trim()) { + this.callbacks.onSettingChange('tagPrefix', value.trim()); + } + })); + } + + /** + * Renders the custom status management section + */ + private renderCustomStatusSettings(containerEl: HTMLElement, settings: any): void { + new Setting(containerEl).setName('Custom statuses').setHeading(); + + new Setting(containerEl) + .setName('Use only custom statuses') + .setDesc('Ignore template statuses and use only the custom statuses defined below') + .addToggle(toggle => toggle + .setValue(settings.useCustomStatusesOnly || false) + .onChange(value => this.callbacks.onSettingChange('useCustomStatusesOnly', value))); + + const statusList = containerEl.createDiv({ cls: 'custom-status-list' }); + this.renderCustomStatuses(statusList, settings); + + new Setting(containerEl) + .setName('Add new status') + .setDesc('Add a custom status with a name, icon, and color') + .addButton(button => button + .setButtonText('Add Status') + .setCta() + .onClick(() => this.callbacks.onCustomStatusAdd())); + } + + /** + * Renders the list of custom statuses with edit controls + */ + renderCustomStatuses(statusList: HTMLElement, settings: any): void { + statusList.empty(); + + settings.customStatuses.forEach((status: Status, index: number) => { + const setting = new Setting(statusList) + .setName(status.name) + .setClass('status-item'); + + setting.addText(text => text + .setPlaceholder('Name') + .setValue(status.name) + .onChange(value => this.callbacks.onCustomStatusChange(index, 'name', value || 'unnamed'))); + + setting.addText(text => text + .setPlaceholder('Icon') + .setValue(status.icon) + .onChange(value => this.callbacks.onCustomStatusChange(index, 'icon', value || '❓'))); + + setting.addColorPicker(colorPicker => colorPicker + .setValue(settings.statusColors[status.name] || '#ffffff') + .onChange(value => this.callbacks.onCustomStatusChange(index, 'color', value))); + + setting.addText(text => text + .setPlaceholder('Description') + .setValue(status.description || '') + .onChange(value => this.callbacks.onCustomStatusChange(index, 'description', value))); + + setting.addButton(button => button + .setButtonText('Remove') + .setClass('status-remove-button') + .setWarning() + .onClick(() => this.callbacks.onCustomStatusRemove(index))); + }); + } +} diff --git a/main.ts b/main.ts index fda59f4..cfca185 100644 --- a/main.ts +++ b/main.ts @@ -80,7 +80,7 @@ export default class NoteStatus extends Plugin { }); // Añadir pestaña de configuración - // this.addSettingTab(new NoteStatusSettingTab(this.app, this, this.statusService)); + this.addSettingTab(new NoteStatusSettingTab(this.app, this, this.statusService)); } private initializeIntegrations() { @@ -240,25 +240,21 @@ export default class NoteStatus extends Plugin { async saveSettings() { // await this.saveData(this.settings); - // // Actualizar servicios - // this.statusService.updateSettings(this.settings); - // this.styleService.updateSettings(this.settings); + // Actualizar servicios + this.statusService.updateSettings(this.settings); + this.styleService.updateSettings(this.settings); - // // Actualizar integraciones - // this.updateIntegrationsSettings(); - } - - private updateIntegrationsSettings() { - // // Actualizar configuración en todas las integraciones - // this.explorerIntegration.updateSettings(this.settings); - // this.fileContextMenuIntegration.updateSettings(this.settings); + // Actualizar integraciones + this.explorerIntegration.updateSettings(this.settings); + this.fileContextMenuIntegration.updateSettings(this.settings); // this.editorIntegration.updateSettings(this.settings); - // this.toolbarIntegration.updateSettings(this.settings); - // this.metadataIntegration.updateSettings(this.settings); - // this.workspaceIntegration.updateSettings(this.settings); + //this.metadataIntegration.updateSettings(this.settings); + this.toolbarIntegration.updateSettings(this.settings); + this.workspaceIntegration.updateSettings(this.settings); // // Actualizar componentes UI - // this.statusBar.updateSettings(this.settings); + this.statusBar.updateSettings(this.settings); + this.statusDropdown.updateSettings(this.settings) } onunload() { From 0bd515ac6182ad0f0ef7b904b6ce6ebbb228e2bc Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Sat, 24 May 2025 14:36:31 +0200 Subject: [PATCH 54/67] feat: add strict statuses --- constants/defaults.ts | 3 ++- integrations/settings/settings-ui.ts | 8 ++++++++ models/types.ts | 3 ++- services/status-service.ts | 18 ++++++++++++++---- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/constants/defaults.ts b/constants/defaults.ts index d98016f..cbe487f 100644 --- a/constants/defaults.ts +++ b/constants/defaults.ts @@ -23,6 +23,7 @@ export const DEFAULT_SETTINGS: NoteStatusSettings = { useCustomStatusesOnly: false, useMultipleStatuses: true, tagPrefix: 'obsidian-note-status', + strictStatuses: false, // Default to show all statuses from frontmatter excludeUnknownStatus: true, // Default to exclude unknown status files for better performance }; @@ -35,4 +36,4 @@ export const DEFAULT_COLORS: Record = { completed: '#00aaff', // Blue for accent dropped: '#ff0000', // Red for error unknown: '#888888' // Gray for muted -}; \ No newline at end of file +}; diff --git a/integrations/settings/settings-ui.ts b/integrations/settings/settings-ui.ts index ab166dd..33ae528 100644 --- a/integrations/settings/settings-ui.ts +++ b/integrations/settings/settings-ui.ts @@ -160,6 +160,14 @@ export class NoteStatusSettingsUI { this.callbacks.onSettingChange('tagPrefix', value.trim()); } })); + + + new Setting(containerEl) + .setName('Strict status validation') + .setDesc('Only show statuses that are defined in templates or custom statuses. ⚠️ WARNING: When enabled, any unknown statuses will be automatically removed when modifying file statuses.') + .addToggle(toggle => toggle + .setValue(settings.strictStatuses || false) + .onChange(value => this.callbacks.onSettingChange('strictStatuses', value))); } /** diff --git a/models/types.ts b/models/types.ts index 820bfc3..d1ddb48 100644 --- a/models/types.ts +++ b/models/types.ts @@ -26,6 +26,7 @@ export interface NoteStatusSettings { useCustomStatusesOnly: boolean; // Whether to use only custom statuses or include templates useMultipleStatuses: boolean; // Whether to allow multiple statuses per note tagPrefix: string; // Prefix for the status tag (default: 'status') + strictStatuses: boolean; // Whether to only show known statuses excludeUnknownStatus: boolean; // Whether to exclude files with unknown status from the status pane } @@ -39,4 +40,4 @@ export interface FileExplorerView extends View { titleEl?: HTMLElement; selfEl?: HTMLElement; }>; -} \ No newline at end of file +} diff --git a/services/status-service.ts b/services/status-service.ts index c9f6f52..e184d55 100644 --- a/services/status-service.ts +++ b/services/status-service.ts @@ -83,12 +83,22 @@ export class StatusService { if (Array.isArray(frontmatterStatus)) { for (const statusName of frontmatterStatus) { - this.addValidStatus(statusName.toString(), statuses); + if (this.settings.strictStatuses) { + this.addValidStatus(statusName.toString(), statuses); + } else { + const cleanStatus = statusName.toString().trim(); + if (cleanStatus) statuses.push(cleanStatus); + } } } else { - this.addValidStatus(frontmatterStatus.toString(), statuses); + if (this.settings.strictStatuses) { + this.addValidStatus(frontmatterStatus.toString(), statuses); + } else { + const cleanStatus = frontmatterStatus.toString().trim(); + if (cleanStatus) statuses.push(cleanStatus); + } } - + return statuses.length > 0 ? statuses : ['unknown']; } @@ -361,4 +371,4 @@ export class StatusService { } })); } -} \ No newline at end of file +} From 853212e2edb4b4e3dcaaa781e0a285e4312dcffb Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Sat, 24 May 2025 19:06:05 +0200 Subject: [PATCH 55/67] fix: multiple status change single status check --- components/status-dropdown/dropdown-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/status-dropdown/dropdown-manager.ts b/components/status-dropdown/dropdown-manager.ts index d99e010..618aa59 100644 --- a/components/status-dropdown/dropdown-manager.ts +++ b/components/status-dropdown/dropdown-manager.ts @@ -56,7 +56,7 @@ export class DropdownManager { ); // If ALL have the status, remove it. Otherwise, add it - const operation = filesWithStatus.length === files.length ? 'remove' : 'add'; + let operation = filesWithStatus.length === files.length ? 'remove' : (!this.settings.useMultipleStatuses) ? 'set':'add'; await this.statusService.handleStatusChange({ files: targetFile, From b19a37bb07b4e1774f01b7f1aed1e496e33dddf6 Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Sat, 24 May 2025 19:10:20 +0200 Subject: [PATCH 56/67] fix: explorer setting copy --- integrations/explorer/explorer-integration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/explorer/explorer-integration.ts b/integrations/explorer/explorer-integration.ts index a1c29d3..3f80dff 100644 --- a/integrations/explorer/explorer-integration.ts +++ b/integrations/explorer/explorer-integration.ts @@ -52,7 +52,7 @@ export class ExplorerIntegration { this.settings.showStatusIconsInExplorer !== settings.showStatusIconsInExplorer || this.settings.hideUnknownStatusInExplorer !== settings.hideUnknownStatusInExplorer; - this.settings = settings; + this.settings = {...settings}; this.ui.updateSettings(settings); if (shouldRefreshIcons) { From 4169f681fa555f9a504aff2c9eda65e4f6eceaed Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Sat, 24 May 2025 19:20:54 +0200 Subject: [PATCH 57/67] fix: update status pane settings --- main.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/main.ts b/main.ts index cfca185..6fcd394 100644 --- a/main.ts +++ b/main.ts @@ -39,6 +39,8 @@ export default class NoteStatus extends Plugin { metadataIntegration: MetadataIntegration; workspaceIntegration: WorkspaceIntegration; + statusPane: StatusPaneViewController; + async onload() { try { await this.loadSettings(); @@ -71,7 +73,8 @@ export default class NoteStatus extends Plugin { private registerViews() { // Register status pane view this.registerView('status-pane', (leaf) => { - return new StatusPaneViewController(leaf, this); + this.statusPane = new StatusPaneViewController(leaf, this); + return this.statusPane; }); // Add ribbon icon @@ -251,8 +254,8 @@ export default class NoteStatus extends Plugin { //this.metadataIntegration.updateSettings(this.settings); this.toolbarIntegration.updateSettings(this.settings); this.workspaceIntegration.updateSettings(this.settings); - - // // Actualizar componentes UI + this.statusPane.updateSettings(this.settings); + // Actualizar componentes UI this.statusBar.updateSettings(this.settings); this.statusDropdown.updateSettings(this.settings) } From fca8486bc3b120b50d27aa0584169cd5e0d6a007 Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Sat, 24 May 2025 19:25:34 +0200 Subject: [PATCH 58/67] fix: persist settings --- main.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.ts b/main.ts index 6fcd394..720122c 100644 --- a/main.ts +++ b/main.ts @@ -241,7 +241,7 @@ export default class NoteStatus extends Plugin { } async saveSettings() { - // await this.saveData(this.settings); + await this.saveData(this.settings); // Actualizar servicios this.statusService.updateSettings(this.settings); @@ -254,7 +254,7 @@ export default class NoteStatus extends Plugin { //this.metadataIntegration.updateSettings(this.settings); this.toolbarIntegration.updateSettings(this.settings); this.workspaceIntegration.updateSettings(this.settings); - this.statusPane.updateSettings(this.settings); + this.statusPane?.updateSettings(this.settings); // Actualizar componentes UI this.statusBar.updateSettings(this.settings); this.statusDropdown.updateSettings(this.settings) From 531e1e3b993156c90af3e6e3ae630fc1710c1b84 Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Sat, 24 May 2025 19:48:40 +0200 Subject: [PATCH 59/67] chore: unload --- integrations/editor/toolbar-integration.ts | 18 ++++--- main.ts | 50 ++++++------------- .../status-pane-view-controller.ts | 1 - 3 files changed, 26 insertions(+), 43 deletions(-) diff --git a/integrations/editor/toolbar-integration.ts b/integrations/editor/toolbar-integration.ts index b2cd5f5..d712fe4 100644 --- a/integrations/editor/toolbar-integration.ts +++ b/integrations/editor/toolbar-integration.ts @@ -73,13 +73,17 @@ export class ToolbarIntegration { return this.buttonElement?.isConnected === true; } - private removeToolbarButton(): void { - if (this.buttonElement) { - this.buttonElement.remove(); - this.buttonElement = null; +private removeToolbarButton(): void { + this.app.workspace.iterateAllLeaves(leaf => { + if (leaf.view instanceof MarkdownView) { + const buttons = leaf.view.containerEl.querySelectorAll('.note-status-toolbar-button'); + buttons.forEach(button => button.remove()); } - this.currentLeafId = null; - } + }); + + this.buttonElement = null; + this.currentLeafId = null; +} private updateButtonDisplay(overrideStatuses?: string[]): void { if (!this.buttonElement) return; @@ -108,7 +112,7 @@ export class ToolbarIntegration { } public unload(): void { - this.buttonView.destroy(); + // this.buttonView.destroy(); this.removeToolbarButton(); } } diff --git a/main.ts b/main.ts index 720122c..64dc424 100644 --- a/main.ts +++ b/main.ts @@ -155,14 +155,8 @@ export default class NoteStatus extends Plugin { } private setupCustomEvents() { - // // Evento para cambios de configuración - // window.addEventListener('note-status:settings-changed', this.saveSettings.bind(this)); - // Evento para cambios de estado window.addEventListener('note-status:status-changed', this.handleStatusChanged.bind(this)); - - // // Evento para actualización de UI - // window.addEventListener('note-status:refresh-ui', this.refreshUI.bind(this)); } private initializeUI() { @@ -223,19 +217,6 @@ export default class NoteStatus extends Plugin { // this.refreshStatus(); } - private forceRefreshUI() { - // // Actualizar todos los elementos visibles - // this.refreshStatus(); - - // if (this.settings.showStatusIconsInExplorer) { - // this.explorerIntegration.updateAllFileExplorerIcons(); - // } - - // window.dispatchEvent(new CustomEvent('note-status:update-pane')); - - // new Notice('UI forcefully refreshed'); - } - private async openStatusPane() { await StatusPaneViewController.open(this.app); } @@ -260,20 +241,19 @@ export default class NoteStatus extends Plugin { this.statusDropdown.updateSettings(this.settings) } - onunload() { - // // Limpiar eventos personalizados - // window.removeEventListener('note-status:settings-changed', this.saveSettings.bind(this)); - // window.removeEventListener('note-status:status-changed', this.handleStatusChanged.bind(this)); - // window.removeEventListener('note-status:refresh-ui', this.refreshUI.bind(this)); - - // // Limpiar integraciones - // this.explorerIntegration.unload(); - // this.toolbarIntegration.unload(); - - // // Limpiar servicios - // this.styleService.unload(); - - // // Limpiar componentes UI - // this.statusBar.unload(); - } +onunload() { + // Clean up event listeners + window.removeEventListener('note-status:status-changed', this.handleStatusChanged.bind(this)); + + // Clean up integrations + this.explorerIntegration?.unload(); + this.toolbarIntegration?.unload(); + + // Clean up services + this.styleService?.unload(); + + // Clean up UI components + this.statusBar?.unload(); + this.statusDropdown?.unload(); +} } diff --git a/views/status-pane-view/status-pane-view-controller.ts b/views/status-pane-view/status-pane-view-controller.ts index 4156db8..19be0b0 100644 --- a/views/status-pane-view/status-pane-view-controller.ts +++ b/views/status-pane-view/status-pane-view-controller.ts @@ -117,7 +117,6 @@ export class StatusPaneViewController extends View { }, onStatusToggle: (status, collapsed) => { this.settings.collapsedStatuses[status] = collapsed; - window.dispatchEvent(new CustomEvent('note-status:settings-changed')); }, onContextMenu: (e, file) => { }, From 005c89f0ebfeb22ebb3c18c4b219bfa2752198d3 Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Sat, 24 May 2025 19:56:54 +0200 Subject: [PATCH 60/67] fix: unload --- .../file-context-menu-integration.ts | 28 ++++++++++----- .../workspace/workspace-integration.ts | 35 +++++++++++++------ main.ts | 12 +++++-- 3 files changed, 55 insertions(+), 20 deletions(-) diff --git a/integrations/context-menu/file-context-menu-integration.ts b/integrations/context-menu/file-context-menu-integration.ts index 9ffa482..7c3a0b8 100644 --- a/integrations/context-menu/file-context-menu-integration.ts +++ b/integrations/context-menu/file-context-menu-integration.ts @@ -13,6 +13,8 @@ export class FileContextMenuIntegration { private statusService: StatusService; private explorerIntegration: ExplorerIntegration; private statusContextMenu: StatusContextMenu; + private fileMenuEventRef: any; + private filesMenuEventRef: any; constructor( app: App, @@ -38,16 +40,14 @@ export class FileContextMenuIntegration { /** * Registra los eventos del menú contextual de archivos */ - public registerFileContextMenuEvents(): void { - // Evento para archivo único - this.app.workspace.on('file-menu', (menu, file, source) => { +public registerFileContextMenuEvents(): void { + this.fileMenuEventRef = (menu: Menu, file: any, source: string) => { if (source === 'file-explorer-context-menu' && file instanceof TFile && file.extension === 'md') { this.addStatusChangeMenu(menu, file); } - }); + }; - // Evento para múltiples archivos - this.app.workspace.on('files-menu', (menu, files) => { + this.filesMenuEventRef = (menu: Menu, files: any[]) => { const mdFiles = files.filter(file => file instanceof TFile && file.extension === 'md' ) as TFile[]; @@ -55,7 +55,10 @@ export class FileContextMenuIntegration { if (mdFiles.length > 0) { this.addBatchStatusChangeMenu(menu, mdFiles); } - }); + }; + + this.app.workspace.on('file-menu', this.fileMenuEventRef); + this.app.workspace.on('files-menu', this.filesMenuEventRef); } /** @@ -80,4 +83,13 @@ export class FileContextMenuIntegration { this.statusContextMenu.showForFiles(files); }); } -} \ No newline at end of file + + public unload(): void { + if (this.fileMenuEventRef) { + this.app.workspace.off('file-menu', this.fileMenuEventRef); + } + if (this.filesMenuEventRef) { + this.app.workspace.off('files-menu', this.filesMenuEventRef); + } + } +} diff --git a/integrations/workspace/workspace-integration.ts b/integrations/workspace/workspace-integration.ts index 96a2c71..dae584e 100644 --- a/integrations/workspace/workspace-integration.ts +++ b/integrations/workspace/workspace-integration.ts @@ -12,6 +12,9 @@ export class WorkspaceIntegration { private statusService: StatusService; private toolbarIntegration: ToolbarIntegration; private lastActiveFile: TFile | null = null; + private fileOpenEventRef: any; + private activeLeafChangeEventRef: any; + private layoutChangeEventRef: any; constructor( app: App, @@ -36,22 +39,23 @@ export class WorkspaceIntegration { * Registra eventos del workspace */ public registerWorkspaceEvents(): void { - // Evento al abrir un archivo - this.app.workspace.on('file-open', (file) => { + this.fileOpenEventRef = (file: any) => { if (file instanceof TFile) { this.handleFileOpen(file); } - }); + }; - // Evento de cambio de hoja activa - this.app.workspace.on('active-leaf-change', (leaf: WorkspaceLeaf) => { + this.activeLeafChangeEventRef = (leaf: WorkspaceLeaf) => { this.handleActiveLeafChange(leaf); - }); + }; - // Evento de cambio en el layout - this.app.workspace.on('layout-change', () => { + this.layoutChangeEventRef = () => { this.handleLayoutChange(); - }); + }; + + this.app.workspace.on('file-open', this.fileOpenEventRef); + this.app.workspace.on('active-leaf-change', this.activeLeafChangeEventRef); + this.app.workspace.on('layout-change', this.layoutChangeEventRef); } /** @@ -106,4 +110,15 @@ export class WorkspaceIntegration { console.error('Error checking note status:', error); } } -} \ No newline at end of file + public unload(): void { + if (this.fileOpenEventRef) { + this.app.workspace.off('file-open', this.fileOpenEventRef); + } + if (this.activeLeafChangeEventRef) { + this.app.workspace.off('active-leaf-change', this.activeLeafChangeEventRef); + } + if (this.layoutChangeEventRef) { + this.app.workspace.off('layout-change', this.layoutChangeEventRef); + } + } +} diff --git a/main.ts b/main.ts index 64dc424..56ad01c 100644 --- a/main.ts +++ b/main.ts @@ -41,6 +41,8 @@ export default class NoteStatus extends Plugin { statusPane: StatusPaneViewController; + private boundHandleStatusChanged: (event: CustomEvent) => void; + async onload() { try { await this.loadSettings(); @@ -156,7 +158,9 @@ export default class NoteStatus extends Plugin { private setupCustomEvents() { // Evento para cambios de estado - window.addEventListener('note-status:status-changed', this.handleStatusChanged.bind(this)); + + this.boundHandleStatusChanged = this.handleStatusChanged.bind(this); + window.addEventListener('note-status:status-changed', this.boundHandleStatusChanged); } private initializeUI() { @@ -243,11 +247,15 @@ export default class NoteStatus extends Plugin { onunload() { // Clean up event listeners - window.removeEventListener('note-status:status-changed', this.handleStatusChanged.bind(this)); + if (this.boundHandleStatusChanged) { + window.removeEventListener('note-status:status-changed', this.boundHandleStatusChanged); + } // Clean up integrations this.explorerIntegration?.unload(); this.toolbarIntegration?.unload(); + this.fileContextMenuIntegration?.unload(); + this.workspaceIntegration?.unload(); // Clean up services this.styleService?.unload(); From aeeb208790afa4c0f9d085d6fe0b61f35c03145e Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Sat, 24 May 2025 21:10:08 +0200 Subject: [PATCH 61/67] chore: restore metadata integration --- .../metadata-cache/metadata-integration.ts | 40 +++++++++---------- main.ts | 14 ++++--- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/integrations/metadata-cache/metadata-integration.ts b/integrations/metadata-cache/metadata-integration.ts index af6c507..6f4404a 100644 --- a/integrations/metadata-cache/metadata-integration.ts +++ b/integrations/metadata-cache/metadata-integration.ts @@ -3,14 +3,13 @@ import { NoteStatusSettings } from '../../models/types'; import { ExplorerIntegration } from '../explorer/explorer-integration'; import { StatusService } from 'services/status-service'; -/** - * Gestiona la integración con la caché de metadatos - */ export class MetadataIntegration { private app: App; private settings: NoteStatusSettings; private statusService: StatusService; private explorerIntegration: ExplorerIntegration; + private metadataChangedRef: any; + private metadataResolvedRef: any; constructor( app: App, @@ -24,44 +23,34 @@ export class MetadataIntegration { this.explorerIntegration = explorerIntegration; } - /** - * Actualiza la configuración - */ public updateSettings(settings: NoteStatusSettings): void { this.settings = settings; } - /** - * Registra los eventos de metadatos - */ public registerMetadataEvents(): void { - // Evento cuando cambian los metadatos de un archivo - this.app.metadataCache.on('changed', (file) => { + this.metadataChangedRef = (file: TFile) => { if (file instanceof TFile && file.extension === 'md') { this.handleMetadataChanged(file); } - }); + }; - // Evento cuando se resuelven todos los metadatos (al cargar) - this.app.metadataCache.on('resolved', () => { + this.metadataResolvedRef = () => { setTimeout(() => { if (this.settings.showStatusIconsInExplorer) { this.explorerIntegration.updateAllFileExplorerIcons(); } }, 500); - }); + }; + + this.app.metadataCache.on('changed', this.metadataChangedRef); + this.app.metadataCache.on('resolved', this.metadataResolvedRef); } - /** - * Maneja cambios en metadatos de un archivo - */ private handleMetadataChanged(file: TFile): void { - // Actualiza el explorador si está habilitado if (this.settings.showStatusIconsInExplorer) { this.explorerIntegration.updateFileExplorerIcons(file); } - // Actualiza otros componentes si es el archivo activo const activeFile = this.app.workspace.getActiveFile(); if (activeFile?.path === file.path) { const statuses = this.statusService.getFileStatuses(file); @@ -70,4 +59,13 @@ export class MetadataIntegration { })); } } -} \ No newline at end of file + + public unload(): void { + if (this.metadataChangedRef) { + this.app.metadataCache.off('changed', this.metadataChangedRef); + } + if (this.metadataResolvedRef) { + this.app.metadataCache.off('resolved', this.metadataResolvedRef); + } + } +} diff --git a/main.ts b/main.ts index 56ad01c..9e843c7 100644 --- a/main.ts +++ b/main.ts @@ -99,12 +99,12 @@ export default class NoteStatus extends Plugin { // this.editorIntegration = new EditorIntegration(this.app, this.settings, this.statusService); - // this.metadataIntegration = new MetadataIntegration( - // this.app, - // this.settings, - // this.statusService, - // this.explorerIntegration - // ); + this.metadataIntegration = new MetadataIntegration( + this.app, + this.settings, + this.statusService, + this.explorerIntegration + ); this.workspaceIntegration = new WorkspaceIntegration( this.app, @@ -117,6 +117,7 @@ export default class NoteStatus extends Plugin { this.fileContextMenuIntegration.registerFileContextMenuEvents(); // this.editorIntegration.registerEditorMenus(); // this.metadataIntegration.registerMetadataEvents(); +this.metadataIntegration.registerMetadataEvents(); this.workspaceIntegration.registerWorkspaceEvents(); } @@ -256,6 +257,7 @@ onunload() { this.toolbarIntegration?.unload(); this.fileContextMenuIntegration?.unload(); this.workspaceIntegration?.unload(); + this.metadataIntegration?.unload(); // Clean up services this.styleService?.unload(); From 1ccb4bf2a9863df3ce6552bf98dac46860aab567 Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Sat, 24 May 2025 21:21:03 +0200 Subject: [PATCH 62/67] chore: restore editor integration --- integrations/editor/editor-integration.ts | 63 +++++++++++------------ main.ts | 7 +-- 2 files changed, 34 insertions(+), 36 deletions(-) diff --git a/integrations/editor/editor-integration.ts b/integrations/editor/editor-integration.ts index 5095511..242df05 100644 --- a/integrations/editor/editor-integration.ts +++ b/integrations/editor/editor-integration.ts @@ -1,49 +1,54 @@ -import { App, Editor, MarkdownView, Menu } from 'obsidian'; +import { App, Editor, MarkdownView, Menu, TFile } from 'obsidian'; import { NoteStatusSettings } from '../../models/types'; import { StatusService } from 'services/status-service'; +import { StatusDropdown } from 'components/status-dropdown'; -/** - * Gestiona la integración con el editor de Obsidian - */ export class EditorIntegration { private app: App; private settings: NoteStatusSettings; private statusService: StatusService; + private statusDropdown: StatusDropdown; + private editorMenuRef: any; - constructor(app: App, settings: NoteStatusSettings, statusService: StatusService) { + constructor( + app: App, + settings: NoteStatusSettings, + statusService: StatusService, + statusDropdown: StatusDropdown + ) { this.app = app; this.settings = settings; this.statusService = statusService; + this.statusDropdown = statusDropdown; } - /** - * Actualiza la configuración - */ public updateSettings(settings: NoteStatusSettings): void { this.settings = settings; } - /** - * Registra los menús contextuales del editor - */ public registerEditorMenus(): void { - this.app.workspace.on('editor-menu', (menu, editor, view) => { - if (view instanceof MarkdownView) { + this.editorMenuRef = (menu: Menu, editor: Editor, view: MarkdownView) => { + if (view.file) { this.addStatusMenuItems(menu, editor, view); } - }); + }; + + this.app.workspace.on('editor-menu', this.editorMenuRef); } - /** - * Añade opciones de estado al menú del editor - */ private addStatusMenuItems(menu: Menu, editor: Editor, view: MarkdownView): void { menu.addItem(item => item .setTitle('Change note status') .setIcon('tag') .onClick(() => { - this.showStatusChangeUI(editor, view); + if (view.file) { + this.statusDropdown.openStatusDropdown({ + files: [view.file], + editor, + view + }); + } }) ); @@ -57,21 +62,13 @@ export class EditorIntegration { ); } - /** - * Muestra la UI para cambiar estado - */ - private showStatusChangeUI(editor: Editor, view: MarkdownView): void { - const file = view.file; - if (!file) return; - - // En la implementación real, esto mostraría un dropdown o modal - console.log("Mostrar UI de cambio de estado"); - } - - /** - * Inserta metadatos de estado en el editor - */ public insertStatusMetadata(editor: Editor): void { this.statusService.insertStatusMetadataInEditor(editor); } -} \ No newline at end of file + + public unload(): void { + if (this.editorMenuRef) { + this.app.workspace.off('editor-menu', this.editorMenuRef); + } + } +} diff --git a/main.ts b/main.ts index 9e843c7..4522588 100644 --- a/main.ts +++ b/main.ts @@ -97,7 +97,7 @@ export default class NoteStatus extends Plugin { const statusContextMenu = new StatusContextMenu(this.app, this.settings, this.statusService, this.statusDropdown, this.explorerIntegration); this.fileContextMenuIntegration = new FileContextMenuIntegration(this.app, this.settings, this.statusService, this.explorerIntegration, statusContextMenu); - // this.editorIntegration = new EditorIntegration(this.app, this.settings, this.statusService); + this.editorIntegration = new EditorIntegration(this.app, this.settings, this.statusService, this.statusDropdown); this.metadataIntegration = new MetadataIntegration( this.app, @@ -115,7 +115,7 @@ export default class NoteStatus extends Plugin { // // 3. Registrar eventos en cada integración this.fileContextMenuIntegration.registerFileContextMenuEvents(); - // this.editorIntegration.registerEditorMenus(); + this.editorIntegration.registerEditorMenus(); // this.metadataIntegration.registerMetadataEvents(); this.metadataIntegration.registerMetadataEvents(); this.workspaceIntegration.registerWorkspaceEvents(); @@ -236,7 +236,7 @@ this.metadataIntegration.registerMetadataEvents(); // Actualizar integraciones this.explorerIntegration.updateSettings(this.settings); this.fileContextMenuIntegration.updateSettings(this.settings); - // this.editorIntegration.updateSettings(this.settings); + this.editorIntegration.updateSettings(this.settings); //this.metadataIntegration.updateSettings(this.settings); this.toolbarIntegration.updateSettings(this.settings); this.workspaceIntegration.updateSettings(this.settings); @@ -258,6 +258,7 @@ onunload() { this.fileContextMenuIntegration?.unload(); this.workspaceIntegration?.unload(); this.metadataIntegration?.unload(); + this.editorIntegration?.unload(); // Clean up services this.styleService?.unload(); From be2de3d816e520911ab391f7da85cac143fdaa93 Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Sat, 24 May 2025 21:23:59 +0200 Subject: [PATCH 63/67] fix: avoid destroy metadata if exists --- services/status-service.ts | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/services/status-service.ts b/services/status-service.ts index e184d55..c5a6e77 100644 --- a/services/status-service.ts +++ b/services/status-service.ts @@ -128,20 +128,34 @@ export class StatusService { /** * Insert status metadata in the editor */ - public insertStatusMetadataInEditor(editor: Editor): void { - const content = editor.getValue(); +public insertStatusMetadataInEditor(editor: Editor): void { + const content = editor.getValue(); + const frontMatterMatch = content.match(/^---\n([\s\S]+?)\n---/); + + // Check if status metadata already exists + const statusTagRegex = new RegExp(`${this.settings.tagPrefix}:\\s*\\[?[^\\]]*\\]?`, 'm'); + + if (frontMatterMatch) { + const frontMatter = frontMatterMatch[1]; + if (frontMatter.match(statusTagRegex)) { + // Status already exists, do nothing + return; + } + // Add to existing frontmatter const defaultStatuses = ['unknown']; const statusMetadata = `${this.settings.tagPrefix}: ${JSON.stringify(defaultStatuses)}`; - - // Check if frontmatter exists - const frontMatterMatch = content.match(/^---\n([\s\S]+?)\n---/); - - if (frontMatterMatch) { - this.insertIntoExistingFrontmatter(editor, content, frontMatterMatch, statusMetadata); - } else { - this.createFrontmatterWithStatus(editor, content, statusMetadata); + this.insertIntoExistingFrontmatter(editor, content, frontMatterMatch, statusMetadata); + } else { + // No frontmatter, check if status exists in content somehow + if (content.match(statusTagRegex)) { + return; } + // Create new frontmatter + const defaultStatuses = ['unknown']; + const statusMetadata = `${this.settings.tagPrefix}: ${JSON.stringify(defaultStatuses)}`; + this.createFrontmatterWithStatus(editor, content, statusMetadata); } +} /** * Insert status metadata into existing frontmatter From bcb034a91c3494f27997918bfbce21f158612051 Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Sat, 24 May 2025 21:28:37 +0200 Subject: [PATCH 64/67] chore: hide insert metadata if already has the metadata --- integrations/editor/editor-integration.ts | 54 +++++++++++++---------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/integrations/editor/editor-integration.ts b/integrations/editor/editor-integration.ts index 242df05..0e86fb0 100644 --- a/integrations/editor/editor-integration.ts +++ b/integrations/editor/editor-integration.ts @@ -36,31 +36,37 @@ export class EditorIntegration { this.app.workspace.on('editor-menu', this.editorMenuRef); } - private addStatusMenuItems(menu: Menu, editor: Editor, view: MarkdownView): void { - menu.addItem(item => - item - .setTitle('Change note status') - .setIcon('tag') - .onClick(() => { - if (view.file) { - this.statusDropdown.openStatusDropdown({ - files: [view.file], - editor, - view - }); - } - }) - ); - - menu.addItem(item => - item - .setTitle('Insert status metadata') - .setIcon('plus-circle') - .onClick(() => { - this.insertStatusMetadata(editor); - }) - ); +private addStatusMenuItems(menu: Menu, editor: Editor, view: MarkdownView): void { + menu.addItem(item => + item + .setTitle('Change note status') + .setIcon('tag') + .onClick(() => { + if (view.file) { + this.statusDropdown.openStatusDropdown({ + files: [view.file], + editor, + view + }); + } + }) + ); + + // Only show insert metadata if it doesn't exist + if (view.file) { + const statuses = this.statusService.getFileStatuses(view.file); + if (statuses.length === 1 && statuses[0] === 'unknown') { + menu.addItem(item => + item + .setTitle('Insert status metadata') + .setIcon('plus-circle') + .onClick(() => { + this.insertStatusMetadata(editor); + }) + ); + } } +} public insertStatusMetadata(editor: Editor): void { this.statusService.insertStatusMetadataInEditor(editor); From 655d65ac8564a3538b8c53fcc478a6b1d87532ed Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Sat, 24 May 2025 21:37:06 +0200 Subject: [PATCH 65/67] feat: add command integration --- integrations/commands/command-integration.ts | 227 +++++++++++++++++++ main.ts | 53 ++--- 2 files changed, 240 insertions(+), 40 deletions(-) create mode 100644 integrations/commands/command-integration.ts diff --git a/integrations/commands/command-integration.ts b/integrations/commands/command-integration.ts new file mode 100644 index 0000000..def45b5 --- /dev/null +++ b/integrations/commands/command-integration.ts @@ -0,0 +1,227 @@ +// integrations/commands/command-integration.ts +import { App, Editor, MarkdownView, Notice, TFile } from 'obsidian'; +import { NoteStatusSettings } from '../../models/types'; +import { StatusService } from 'services/status-service'; +import { StatusDropdown } from 'components/status-dropdown'; +import { StatusPaneViewController } from 'views/status-pane-view'; +import NoteStatus from 'main'; + +export class CommandIntegration { + private app: App; + private plugin: NoteStatus; + private settings: NoteStatusSettings; + private statusService: StatusService; + private statusDropdown: StatusDropdown; + + constructor( + app: App, + plugin: NoteStatus, + settings: NoteStatusSettings, + statusService: StatusService, + statusDropdown: StatusDropdown + ) { + this.app = app; + this.plugin = plugin; + this.settings = settings; + this.statusService = statusService; + this.statusDropdown = statusDropdown; + } + + public updateSettings(settings: NoteStatusSettings): void { + this.settings = settings; + } + + public registerCommands(): void { + // Open status pane + this.plugin.addCommand({ + id: 'open-status-pane', + name: 'Open status pane', + callback: () => StatusPaneViewController.open(this.app) + }); + + // Change status of current note + this.plugin.addCommand({ + id: 'change-status', + name: 'Change status of current note', + checkCallback: (checking: boolean) => { + const file = this.app.workspace.getActiveFile(); + if (!file) return false; + + if (!checking) { + this.statusDropdown.openStatusDropdown({ files: [file] }); + } + return true; + } + }); + + // Insert status metadata + this.plugin.addCommand({ + id: 'insert-status-metadata', + name: 'Insert status metadata', + editorCheckCallback: (checking: boolean, editor: Editor, view: MarkdownView) => { + if (!view.file) return false; + + const statuses = this.statusService.getFileStatuses(view.file); + const hasNoStatus = statuses.length === 1 && statuses[0] === 'unknown'; + + if (!checking && hasNoStatus) { + this.statusService.insertStatusMetadataInEditor(editor); + new Notice('Status metadata inserted'); + } + return hasNoStatus; + } + }); + + // Cycle through statuses + this.plugin.addCommand({ + id: 'cycle-status', + name: 'Cycle to next status', + checkCallback: (checking: boolean) => { + const file = this.app.workspace.getActiveFile(); + if (!file) return false; + + if (!checking) { + this.cycleStatus(file); + } + return true; + } + }); + + // Quick status commands for common statuses + const quickStatuses = ['active', 'completed', 'onHold', 'dropped']; + quickStatuses.forEach(status => { + this.plugin.addCommand({ + id: `set-status-${status}`, + name: `Set status to ${status}`, + checkCallback: (checking: boolean) => { + const file = this.app.workspace.getActiveFile(); + if (!file) return false; + + if (!checking) { + this.statusService.handleStatusChange({ + files: file, + statuses: status, + operation: 'set' + }); + new Notice(`Status set to ${status}`); + } + return true; + } + }); + }); + + // Clear status + this.plugin.addCommand({ + id: 'clear-status', + name: 'Clear status (set to unknown)', + checkCallback: (checking: boolean) => { + const file = this.app.workspace.getActiveFile(); + if (!file) return false; + + if (!checking) { + this.statusService.handleStatusChange({ + files: file, + statuses: 'unknown', + operation: 'set' + }); + new Notice('Status cleared'); + } + return true; + } + }); + + // Copy status from current note + this.plugin.addCommand({ + id: 'copy-status', + name: 'Copy status from current note', + checkCallback: (checking: boolean) => { + const file = this.app.workspace.getActiveFile(); + if (!file) return false; + + if (!checking) { + const statuses = this.statusService.getFileStatuses(file); + (this.app as any).clipboard = statuses; + new Notice(`Copied status: ${statuses.join(', ')}`); + } + return true; + } + }); + + // Paste status to current note + this.plugin.addCommand({ + id: 'paste-status', + name: 'Paste status to current note', + checkCallback: (checking: boolean) => { + const file = this.app.workspace.getActiveFile(); + const clipboard = (this.app as any).clipboard; + if (!file || !clipboard || !Array.isArray(clipboard)) return false; + + if (!checking) { + this.statusService.handleStatusChange({ + files: file, + statuses: clipboard, + operation: 'set' + }); + new Notice(`Pasted status: ${clipboard.join(', ')}`); + } + return true; + } + }); + + // Toggle multiple statuses mode + this.plugin.addCommand({ + id: 'toggle-multiple-statuses', + name: 'Toggle multiple statuses mode', + callback: () => { + this.settings.useMultipleStatuses = !this.settings.useMultipleStatuses; + this.plugin.saveSettings(); + new Notice(`Multiple statuses mode ${this.settings.useMultipleStatuses ? 'enabled' : 'disabled'}`); + } + }); + + // Search notes by status + this.plugin.addCommand({ + id: 'search-by-status', + name: 'Search notes by current status', + checkCallback: (checking: boolean) => { + const file = this.app.workspace.getActiveFile(); + if (!file) return false; + + if (!checking) { + const statuses = this.statusService.getFileStatuses(file); + const query = `[${this.settings.tagPrefix}:"${statuses[0]}"]`; + (this.app as any).internalPlugins.getPluginById('global-search').instance.openGlobalSearch(query); + } + return true; + } + }); + } + + private cycleStatus(file: TFile): void { + const allStatuses = this.statusService.getAllStatuses() + .filter(s => s.name !== 'unknown') + .map(s => s.name); + + if (allStatuses.length === 0) { + new Notice('No statuses available'); + return; + } + + const currentStatuses = this.statusService.getFileStatuses(file); + const currentStatus = currentStatuses[0]; + + let nextIndex = 0; + if (currentStatus !== 'unknown') { + const currentIndex = allStatuses.indexOf(currentStatus); + nextIndex = (currentIndex + 1) % allStatuses.length; + } + + this.statusService.handleStatusChange({ + files: file, + statuses: allStatuses[nextIndex], + operation: 'set' + }); + + new Notice(`Status changed to ${allStatuses[nextIndex]}`); + } +} diff --git a/main.ts b/main.ts index 4522588..e380975 100644 --- a/main.ts +++ b/main.ts @@ -11,7 +11,8 @@ import { MetadataIntegration } from './integrations/metadata-cache'; import { WorkspaceIntegration } from './integrations/workspace'; import { FileContextMenuIntegration } from 'integrations/context-menu/file-context-menu-integration'; import { NoteStatusSettingTab } from 'integrations/settings/settings-tab'; -// +import { CommandIntegration } from 'integrations/commands/command-integration'; + // Importar vistas import { StatusPaneViewController } from './views/status-pane-view'; @@ -38,6 +39,7 @@ export default class NoteStatus extends Plugin { toolbarIntegration: ToolbarIntegration; metadataIntegration: MetadataIntegration; workspaceIntegration: WorkspaceIntegration; + commandIntegration: CommandIntegration; statusPane: StatusPaneViewController; @@ -98,7 +100,13 @@ export default class NoteStatus extends Plugin { this.fileContextMenuIntegration = new FileContextMenuIntegration(this.app, this.settings, this.statusService, this.explorerIntegration, statusContextMenu); this.editorIntegration = new EditorIntegration(this.app, this.settings, this.statusService, this.statusDropdown); - + this.commandIntegration = new CommandIntegration( + this.app, + this, + this.settings, + this.statusService, + this.statusDropdown + ); this.metadataIntegration = new MetadataIntegration( this.app, this.settings, @@ -116,47 +124,11 @@ export default class NoteStatus extends Plugin { // // 3. Registrar eventos en cada integración this.fileContextMenuIntegration.registerFileContextMenuEvents(); this.editorIntegration.registerEditorMenus(); - // this.metadataIntegration.registerMetadataEvents(); + this.commandIntegration.registerCommands(); this.metadataIntegration.registerMetadataEvents(); this.workspaceIntegration.registerWorkspaceEvents(); } - private registerCommands() { - // // Comando para actualizar estado - // this.addCommand({ - // id: 'refresh-status', - // name: 'Refresh status', - // callback: () => { - // this.refreshStatus(); - // new Notice('Note status refreshed!'); - // } - // }); - - // // Comando para forzar actualización de UI - // this.addCommand({ - // id: 'force-refresh-ui', - // name: 'Force refresh user interface', - // callback: () => this.forceRefreshUI() - // }); - - // // Comando para insertar metadatos de estado - // this.addCommand({ - // id: 'insert-status-metadata', - // name: 'Insert status metadata', - // editorCallback: (editor) => { - // this.editorIntegration.insertStatusMetadata(editor); - // new Notice('Status metadata inserted'); - // } - // }); - - // // Comando para abrir panel de estado - // this.addCommand({ - // id: 'open-status-pane', - // name: 'Open status pane', - // callback: () => this.openStatusPane() - // }); - } - private setupCustomEvents() { // Evento para cambios de estado @@ -237,7 +209,8 @@ this.metadataIntegration.registerMetadataEvents(); this.explorerIntegration.updateSettings(this.settings); this.fileContextMenuIntegration.updateSettings(this.settings); this.editorIntegration.updateSettings(this.settings); - //this.metadataIntegration.updateSettings(this.settings); + this.metadataIntegration?.updateSettings(this.settings); + this.commandIntegration?.updateSettings(this.settings); this.toolbarIntegration.updateSettings(this.settings); this.workspaceIntegration.updateSettings(this.settings); this.statusPane?.updateSettings(this.settings); From 026075b66196f105b2d58b2583544a796b3ce909 Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Sun, 25 May 2025 09:28:50 +0200 Subject: [PATCH 66/67] refactor: remove unused code --- main.ts | 31 +++---------------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/main.ts b/main.ts index e380975..7dd7fd6 100644 --- a/main.ts +++ b/main.ts @@ -52,13 +52,7 @@ export default class NoteStatus extends Plugin { this.registerViews(); this.initializeUI(); this.initializeIntegrations(); - // 4. Registrar comandos - // this.registerCommands(); - - // 5. Registrar eventos personalizados this.setupCustomEvents(); - - } catch (error) { console.error('Error loading Note Status plugin:', error); new Notice('Error loading Note Status plugin. Check console for details.'); @@ -91,11 +85,8 @@ export default class NoteStatus extends Plugin { } private initializeIntegrations() { - // Crear integraciones en orden de dependencia this.explorerIntegration = new ExplorerIntegration(this.app, this.settings, this.statusService); this.toolbarIntegration = new ToolbarIntegration(this.app, this.settings, this.statusService, this.statusDropdown); - - // Integraciones que dependen de otras const statusContextMenu = new StatusContextMenu(this.app, this.settings, this.statusService, this.statusDropdown, this.explorerIntegration); this.fileContextMenuIntegration = new FileContextMenuIntegration(this.app, this.settings, this.statusService, this.explorerIntegration, statusContextMenu); @@ -121,17 +112,15 @@ export default class NoteStatus extends Plugin { this.toolbarIntegration ); - // // 3. Registrar eventos en cada integración + // Registrar eventos en cada integración this.fileContextMenuIntegration.registerFileContextMenuEvents(); this.editorIntegration.registerEditorMenus(); this.commandIntegration.registerCommands(); -this.metadataIntegration.registerMetadataEvents(); + this.metadataIntegration.registerMetadataEvents(); this.workspaceIntegration.registerWorkspaceEvents(); } private setupCustomEvents() { - // Evento para cambios de estado - this.boundHandleStatusChanged = this.handleStatusChanged.bind(this); window.addEventListener('note-status:status-changed', this.boundHandleStatusChanged); } @@ -157,7 +146,6 @@ this.metadataIntegration.registerMetadataEvents(); private handleStatusChanged(event: CustomEvent) { const { statuses, file } = event.detail; - console.log("Note status changed", statuses) // Actualizar barra de estado this.statusBar.update(statuses); // Actualizar toolbar @@ -165,6 +153,7 @@ this.metadataIntegration.registerMetadataEvents(); if (activeFile?.path === file) { this.toolbarIntegration.updateStatusDisplay(statuses); } + // Actualizar dropdown this.statusDropdown.update(statuses); // Actualizar status pane si está abierto const statusPaneLeaf = this.app.workspace.getLeavesOfType('status-pane')[0]; @@ -180,20 +169,6 @@ this.metadataIntegration.registerMetadataEvents(); } } - private refreshStatus() { - // const activeFile = this.app.workspace.getActiveFile(); - // if (activeFile) { - // const statuses = this.statusService.getFileStatuses(activeFile); - // window.dispatchEvent(new CustomEvent('note-status:status-changed', { - // detail: { statuses, file: activeFile.path } - // })); - // } - } - - private refreshUI() { - // this.refreshStatus(); - } - private async openStatusPane() { await StatusPaneViewController.open(this.app); } From 7b2f4e6960c071bb4f08cae3beb3c83bd3d2ff46 Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Sun, 25 May 2025 09:40:25 +0200 Subject: [PATCH 67/67] fix: build adjustments --- components/status-dropdown/dropdown-manager.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/components/status-dropdown/dropdown-manager.ts b/components/status-dropdown/dropdown-manager.ts index 618aa59..49cd5ce 100644 --- a/components/status-dropdown/dropdown-manager.ts +++ b/components/status-dropdown/dropdown-manager.ts @@ -16,7 +16,7 @@ export class DropdownManager { private toolbarButton?: HTMLElement; private dropdownUI: DropdownUI; - constructor(app: any, settings: NoteStatusSettings, statusService: StatusService) { + constructor(app: App, settings: NoteStatusSettings, statusService: StatusService) { this.app = app; this.settings = settings; this.statusService = statusService; @@ -56,7 +56,7 @@ export class DropdownManager { ); // If ALL have the status, remove it. Otherwise, add it - let operation = filesWithStatus.length === files.length ? 'remove' : (!this.settings.useMultipleStatuses) ? 'set':'add'; + const operation = filesWithStatus.length === files.length ? 'remove' : (!this.settings.useMultipleStatuses) ? 'set':'add'; await this.statusService.handleStatusChange({ files: targetFile, @@ -77,7 +77,7 @@ export class DropdownManager { /** * Updates the dropdown UI based on current statuses */ - public update(currentStatuses: string[] | string, file?: TFile): void { + public update(currentStatuses: string[] | string, _file?: TFile): void { this.currentStatuses = Array.isArray(currentStatuses) ? [...currentStatuses] : [currentStatuses]; @@ -122,7 +122,12 @@ export class DropdownManager { * Universal function to open the status dropdown */ public openStatusDropdown(options: DropdownOptions): void { - const files = options.files || [this.app.workspace.getActiveFile()].filter(Boolean); + const activeFile = this.app.workspace.getActiveFile(); + const files = options.files || (activeFile ? [activeFile] : []); + if (!files.length) { + new Notice('No files selected'); + return; + } if (!files.length || !files[0]) { new Notice('No files selected'); return;