From 7f1dc8a007bbfae4379829cafe9e9edcf5a0ec6d Mon Sep 17 00:00:00 2001 From: Ben Floyd Date: Sat, 16 Aug 2025 10:54:07 -0600 Subject: [PATCH] add change stream ui --- main.ts | 10 +- settings.ts | 27 ++--- src/components/CalendarComponent.ts | 154 +++++++++++++++++++++++++++- styles.css | 143 +++++++++++++++++++++++--- 4 files changed, 301 insertions(+), 33 deletions(-) diff --git a/main.ts b/main.ts index 31f69bf..116e5d2 100644 --- a/main.ts +++ b/main.ts @@ -398,7 +398,7 @@ export default class StreamsPlugin extends Plugin { if (stream) { this.log.debug(`File belongs to stream: ${stream.name} (${stream.folder})`); - const component = new CalendarComponent(leaf, stream, this.app, this.settings.reuseCurrentTab); + const component = new CalendarComponent(leaf, stream, this.app, this.settings.reuseCurrentTab, this.settings.streams); const componentId = filePath || crypto.randomUUID(); this.calendarComponents.set(componentId, component); this.log.debug('Calendar component created successfully'); @@ -496,7 +496,7 @@ export default class StreamsPlugin extends Plugin { }); // Create the calendar component - const component = new CalendarComponent(leaf, stream, this.app, this.settings.reuseCurrentTab); + const component = new CalendarComponent(leaf, stream, this.app, this.settings.reuseCurrentTab, this.settings.streams); // Set current viewed date const formattedDate = dateString.split('T')[0]; @@ -541,6 +541,12 @@ export default class StreamsPlugin extends Plugin { } } + public updateAllCalendarComponents() { + this.calendarComponents.forEach(component => { + component.updateStreamsList(this.settings.streams); + }); + } + async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); diff --git a/settings.ts b/settings.ts index aa03928..8797261 100644 --- a/settings.ts +++ b/settings.ts @@ -67,6 +67,7 @@ export class StreamsSettingTab extends PluginSettingTab { }; this.plugin.settings.streams.push(newStream); await this.plugin.saveSettings(true); + this.plugin.updateAllCalendarComponents(); this.display(); })); @@ -93,10 +94,11 @@ export class StreamsSettingTab extends PluginSettingTab { stream.name = value; }) .then(textComponent => { - textComponent.inputEl.addEventListener('blur', async () => { - this.plugin.addStreamViewCommand(stream); - await this.plugin.saveSettings(); - }); + textComponent.inputEl.addEventListener('blur', async () => { + this.plugin.addStreamViewCommand(stream); + await this.plugin.saveSettings(); + this.plugin.updateAllCalendarComponents(); + }); })); new Setting(card) @@ -291,14 +293,15 @@ export class StreamsSettingTab extends PluginSettingTab { .onClick(async () => { this.plugin.log.debug(`Deleting stream ${stream.id} (${stream.name})`); - this.plugin.settings.streams.splice(index, 1); - - await this.plugin.saveSettings(true); - - this.plugin.removeStreamCommand(stream.id); - this.plugin.removeStreamViewCommand(stream.id); - - this.display(); + this.plugin.settings.streams.splice(index, 1); + + await this.plugin.saveSettings(true); + + this.plugin.removeStreamCommand(stream.id); + this.plugin.removeStreamViewCommand(stream.id); + this.plugin.updateAllCalendarComponents(); + + this.display(); })); } diff --git a/src/components/CalendarComponent.ts b/src/components/CalendarComponent.ts index 52a34b5..ddf3ad2 100644 --- a/src/components/CalendarComponent.ts +++ b/src/components/CalendarComponent.ts @@ -31,13 +31,16 @@ export class CalendarComponent extends Component { private currentViewedDate: string | null = null; private todayButton: HTMLElement; private reuseCurrentTab: boolean; + private streamsDropdown: HTMLElement | null = null; + private streams: Stream[]; - constructor(leaf: WorkspaceLeaf, stream: Stream, app: App, reuseCurrentTab: boolean = false) { + constructor(leaf: WorkspaceLeaf, stream: Stream, app: App, reuseCurrentTab: boolean = false, streams: Stream[] = []) { super(); this.log.debug('Creating calendar component for stream:', stream.name); this.selectedStream = stream; this.app = app; this.reuseCurrentTab = reuseCurrentTab; + this.streams = streams; this.component = document.createElement('div'); this.component.addClass('streams-calendar-component'); @@ -121,9 +124,7 @@ export class CalendarComponent extends Component { this.toggleExpanded(collapsedView, expandedView); }); - const streamLabel = collapsedView.createDiv('streams-calendar-label'); - streamLabel.setText(this.selectedStream.name); - + // Create the navigation controls const navControls = collapsedView.createDiv('streams-calendar-nav-controls'); const prevDayButton = navControls.createDiv('streams-calendar-day-nav prev-day'); @@ -148,6 +149,20 @@ export class CalendarComponent extends Component { await this.navigateToAdjacentDay(1); }); + // Create the "Change Stream" section + const changeStreamSection = collapsedView.createDiv('streams-calendar-change-stream'); + const changeStreamText = changeStreamSection.createDiv('streams-calendar-change-stream-text'); + changeStreamText.setText('Change Stream'); + changeStreamSection.addEventListener('click', (e) => { + e.stopPropagation(); + this.toggleStreamsDropdown(); + }); + + // Create the streams dropdown + this.streamsDropdown = collapsedView.createDiv('streams-calendar-streams-dropdown'); + this.streamsDropdown.style.display = 'none'; + this.populateStreamsDropdown(); + const expandedView = this.component.createDiv('streams-calendar-expanded'); const topNav = expandedView.createDiv('streams-calendar-top-nav'); @@ -202,6 +217,11 @@ export class CalendarComponent extends Component { if (this.expanded && !this.component.contains(e.target as Node)) { this.toggleExpanded(collapsedView, expandedView); } + + // Close streams dropdown when clicking outside + if (this.streamsDropdown && this.streamsDropdown.style.display !== 'none' && !this.component.contains(e.target as Node)) { + this.toggleStreamsDropdown(); + } }); // Add keyboard event listener for Escape key @@ -406,6 +426,119 @@ export class CalendarComponent extends Component { } } + private toggleStreamsDropdown() { + if (this.streamsDropdown) { + const isVisible = this.streamsDropdown.style.display !== 'none'; + this.streamsDropdown.style.display = isVisible ? 'none' : 'block'; + } + } + + private populateStreamsDropdown() { + if (!this.streamsDropdown) return; + + this.streamsDropdown.empty(); + + // Display streams in the same order as they appear in Obsidian Streams Settings + // The streams array passed from main.ts (this.settings.streams) maintains the exact order + // from the settings UI, so we iterate through them in that order + this.streams.forEach(stream => { + const streamItem = this.streamsDropdown!.createDiv('streams-calendar-stream-item'); + + // Add a class to indicate if this is the currently selected stream + if (stream.id === this.selectedStream.id) { + streamItem.addClass('streams-calendar-stream-item-selected'); + } + + const streamIcon = streamItem.createDiv('streams-calendar-stream-item-icon'); + setIcon(streamIcon, stream.icon); + const streamName = streamItem.createDiv('streams-calendar-stream-item-name'); + streamName.setText(stream.name); + + // Add a checkmark for the currently selected stream + if (stream.id === this.selectedStream.id) { + const checkmark = streamItem.createDiv('streams-calendar-stream-item-checkmark'); + setIcon(checkmark, 'check'); + } + + streamItem.addEventListener('click', (e) => { + e.stopPropagation(); + this.selectStream(stream); + this.toggleStreamsDropdown(); + }); + }); + } + + private selectStream(stream: Stream) { + // Update the selected stream + this.selectedStream = stream; + + // Update the calendar component for the new stream + if (this.grid) { + this.updateCalendarGrid(this.grid); + } + + // Close the dropdown + if (this.streamsDropdown) { + this.streamsDropdown.style.display = 'none'; + } + + // Navigate to the selected stream's daily note + this.navigateToStreamDailyNote(stream); + + // Refresh the dropdown to update visual indicators + this.refreshStreamsDropdown(); + + this.log.debug(`Switched to stream: ${stream.name}`); + } + + private async navigateToStreamDailyNote(stream: Stream) { + try { + // Use the current viewed date if available, otherwise use today + let targetDate: Date; + if (this.currentViewedDate) { + targetDate = this.parseViewedDate(this.currentViewedDate); + } else { + targetDate = new Date(); + } + + const year = targetDate.getFullYear(); + const month = String(targetDate.getMonth() + 1).padStart(2, '0'); + const day = String(targetDate.getDate()).padStart(2, '0'); + const fileName = `${year}-${month}-${day}.md`; + + // Construct the file path + const folderPath = stream.folder + .split(/[/\\]/) + .filter(Boolean) + .join('/'); + const filePath = folderPath ? `${folderPath}/${fileName}` : fileName; + + // Try to get the existing file + let file = this.app.vault.getAbstractFileByPath(filePath); + + // If file doesn't exist, create it + if (!file || !(file instanceof TFile)) { + file = await this.app.vault.create(filePath, ''); + } + + if (file instanceof TFile) { + // Update the current viewed date to match the new stream + this.currentViewedDate = targetDate.toISOString().split('T')[0]; + + // Open the file in the current leaf + const leaf = this.app.workspace.activeLeaf; + if (leaf) { + await leaf.openFile(file); + } + + // Update the today button to reflect the new date + this.updateTodayButton(); + } + } catch (error) { + this.log.error('Error navigating to stream daily note:', error); + } + } + public setCurrentViewedDate(dateString: string): void { this.log.debug(`Setting currentViewedDate explicitly to: ${dateString}`); this.currentViewedDate = dateString; @@ -422,6 +555,19 @@ export class CalendarComponent extends Component { } } + public updateStreamsList(streams: Stream[]) { + this.streams = streams; + if (this.streamsDropdown) { + this.populateStreamsDropdown(); + } + } + + public refreshStreamsDropdown() { + if (this.streamsDropdown) { + this.populateStreamsDropdown(); + } + } + /** * Parse a YYYY-MM-DD date string into a Date object */ diff --git a/styles.css b/styles.css index 9629314..e8c98c7 100644 --- a/styles.css +++ b/styles.css @@ -201,6 +201,132 @@ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); } +/* Calendar label */ +.streams-calendar-label { + font-size: 0.7em; + color: var(--text-muted); + margin-bottom: 2px; +} + +.streams-calendar-nav-controls { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 2px; + background-color: var(--background-primary-alt); + border-radius: 4px; + padding: 2px 2px; + border: none; +} + +.streams-calendar-change-stream { + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + padding: 4px 8px; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s ease; + color: var(--text-muted); + margin-top: 4px; +} + +.streams-calendar-change-stream:hover { + background-color: var(--interactive-hover); + color: var(--text-normal); +} + +.streams-calendar-change-stream-icon { + display: flex; + align-items: center; +} + +.streams-calendar-change-stream-icon svg { + width: 16px; + height: 16px; + color: var(--text-muted); +} + +.streams-calendar-change-stream-text { + font-size: 0.7em; + font-weight: 500; +} + +/* Streams dropdown styles */ +.streams-calendar-streams-dropdown { + position: absolute; + top: calc(100% + 8px); + left: 0; + right: 0; + background-color: var(--background-secondary); + border-radius: 8px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); + z-index: 1006; + border: 1px solid var(--background-modifier-border); + overflow: hidden; +} + +.streams-calendar-stream-item { + display: flex; + align-items: center; + padding: 8px 12px; + cursor: pointer; + transition: background-color 0.2s ease; + border-bottom: 1px solid var(--background-modifier-border); +} + +.streams-calendar-stream-item:last-child { + border-bottom: none; +} + +.streams-calendar-stream-item:hover { + background-color: var(--interactive-hover); +} + +.streams-calendar-stream-item-icon { + display: flex; + align-items: center; + margin-right: 8px; +} + +.streams-calendar-stream-item-icon svg { + width: 16px; + height: 16px; + color: var(--text-muted); +} + +.streams-calendar-stream-item-name { + font-size: 0.9em; + color: var(--text-normal); + font-weight: 500; +} + +.streams-calendar-stream-item-selected { + background-color: var(--interactive-accent); + color: var(--text-on-accent); +} + +.streams-calendar-stream-item-selected .streams-calendar-stream-item-name { + color: var(--text-on-accent); +} + +.streams-calendar-stream-item-selected .streams-calendar-stream-item-icon svg { + color: var(--text-on-accent); +} + +.streams-calendar-stream-item-checkmark { + display: flex; + align-items: center; + margin-left: auto; +} + +.streams-calendar-stream-item-checkmark svg { + width: 14px; + height: 14px; + color: var(--text-on-accent); +} + .streams-calendar-expanded { position: relative; top: 0; @@ -410,11 +536,7 @@ background-color: var(--text-accent); } -.streams-calendar-label { - font-size: 0.7em; - color: var(--text-muted); - margin-bottom: 2px; -} +/* Removed old calendar label styles */ .streams-today-icon-border { border-left: 2px solid var(--text-accent); @@ -778,13 +900,4 @@ clip: rect(0, 0, 0, 0) !important; } -.streams-calendar-nav-controls { - display: flex; - align-items: center; - justify-content: space-between; - margin-top: 2px; - background-color: var(--background-primary-alt); - border-radius: 4px; - padding: 2px 2px; - border: none; -} +/* Removed old nav controls styles */