diff --git a/main.ts b/main.ts index 8396da4..59531ca 100644 --- a/main.ts +++ b/main.ts @@ -39,6 +39,12 @@ export default class StreamsPlugin extends Plugin implements StreamsAPI { const loadedData = await this.loadData(); this.settings = Object.assign({}, DEFAULT_SETTINGS, loadedData); + // Migration: ensure primaryStreamId exists + if (this.settings.primaryStreamId === undefined) { + this.settings.primaryStreamId = null; + await this.saveSettings(); + } + // Migration: ensure barStyle exists if (!this.settings.barStyle) { this.settings.barStyle = 'default'; diff --git a/src/shared/ConfigurationService.ts b/src/shared/ConfigurationService.ts index b53f840..4c94274 100644 --- a/src/shared/ConfigurationService.ts +++ b/src/shared/ConfigurationService.ts @@ -34,6 +34,7 @@ export interface SystemLimitsConfiguration { export interface DefaultSettingsConfiguration { readonly streams: readonly []; + readonly primaryStreamId: string | null; readonly showStreamsBarComponent: boolean; readonly reuseCurrentTab: boolean; @@ -104,6 +105,7 @@ export class ConfigurationService extends BaseSliceService implements Configurat defaults: { streams: [], + primaryStreamId: null, showStreamsBarComponent: true, reuseCurrentTab: false, @@ -220,6 +222,7 @@ export class ConfigurationService extends BaseSliceService implements Configurat getDefaultSettings(): StreamsSettings { return { streams: [...this.config.defaults.streams], + primaryStreamId: this.config.defaults.primaryStreamId, showStreamsBarComponent: this.config.defaults.showStreamsBarComponent, reuseCurrentTab: this.config.defaults.reuseCurrentTab, diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 9be3598..75dde21 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -21,6 +21,7 @@ export const CALENDAR_ENABLED_VIEW_TYPES = [ */ export const DEFAULT_SETTINGS = { streams: [], + primaryStreamId: null, showStreamsBarComponent: true, reuseCurrentTab: false, debugLoggingEnabled: false, diff --git a/src/shared/types.ts b/src/shared/types.ts index 55e2dd8..75f7705 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -31,6 +31,11 @@ export interface Stream { export interface StreamsSettings { streams: Stream[]; + /** + * The single "primary" stream used by "Go to primary stream". + * Null means no primary stream is configured. + */ + primaryStreamId: string | null; showStreamsBarComponent: boolean; reuseCurrentTab: boolean; diff --git a/src/slices/calendar-navigation/ComponentUIBuilder.ts b/src/slices/calendar-navigation/ComponentUIBuilder.ts index ac4b72c..fd9c9d2 100644 --- a/src/slices/calendar-navigation/ComponentUIBuilder.ts +++ b/src/slices/calendar-navigation/ComponentUIBuilder.ts @@ -1,7 +1,7 @@ import { App, setIcon, WorkspaceLeaf } from 'obsidian'; import { Stream } from '../../shared/types'; import { SettingsManager, UIController } from '../../shared/interfaces'; -import { OpenTodayCurrentStreamCommand } from '../file-operations/OpenTodayCurrentStreamCommand'; +import { OpenTodayPrimaryStreamCommand } from '../file-operations/OpenTodayPrimaryStreamCommand'; import { getSetting } from '../../shared/obsidian-types'; import { CalendarRenderer } from './CalendarRenderer'; import { StreamSelector } from './StreamSelector'; @@ -251,15 +251,14 @@ export class ComponentUIBuilder { private setupHomeButton(navControls: HTMLElement): void { const homeButton = navControls.createDiv('streams-bar-home-button'); setIcon(homeButton, 'home'); - homeButton.setAttribute('aria-label', 'Go to current stream today'); + homeButton.setAttribute('aria-label', 'Go to primary stream'); this.eventRegistry.register(homeButton, 'click', async (e: Event) => { e.stopPropagation(); - const command = new OpenTodayCurrentStreamCommand( + const command = new OpenTodayPrimaryStreamCommand( this.app, this.streams, + this.settingsManager as any, this.reuseCurrentTab, - (this.settingsManager as any) ?? undefined, - this.stateManager.getActiveStream(), this.leaf, this.dateStateManager ); diff --git a/src/slices/calendar-navigation/StreamSelector.ts b/src/slices/calendar-navigation/StreamSelector.ts index e9c1272..58a8a51 100644 --- a/src/slices/calendar-navigation/StreamSelector.ts +++ b/src/slices/calendar-navigation/StreamSelector.ts @@ -5,9 +5,9 @@ import { DateStateManager } from '../../shared/DateStateManager'; import { centralizedLogger } from '../../shared/CentralizedLogger'; interface PluginInterface { - settings: { + settings?: { + primaryStreamId?: string | null; }; - setActiveStream(streamId: string, force?: boolean, suppressEvent?: boolean): Promise; } /** @@ -57,6 +57,8 @@ export class StreamSelector extends Component { this.dropdown.empty(); + const primaryStreamId = this.plugin?.settings?.primaryStreamId ?? null; + // Filter out disabled streams const enabledStreams = this.streams.filter(stream => !stream.disabled); @@ -73,6 +75,14 @@ export class StreamSelector extends Component { const streamName = streamItem.createDiv('streams-bar-stream-item-name'); streamName.setText(stream.name); + // Subtle indicator for the primary stream (configured in settings) + if (primaryStreamId && stream.id === primaryStreamId) { + streamItem.addClass('streams-bar-stream-item-primary'); + const primaryIndicator = streamItem.createDiv('streams-bar-stream-item-primary-indicator'); + primaryIndicator.setAttribute('title', 'Primary stream'); + primaryIndicator.setAttribute('aria-label', 'Primary stream'); + } + // Add encryption icon if stream is encrypted if (stream.encryptThisStream) { const encryptionIcon = streamItem.createDiv('streams-bar-stream-item-encryption'); diff --git a/src/slices/file-operations/FileOperationsService.ts b/src/slices/file-operations/FileOperationsService.ts index e5e8c61..7f97704 100644 --- a/src/slices/file-operations/FileOperationsService.ts +++ b/src/slices/file-operations/FileOperationsService.ts @@ -4,7 +4,7 @@ import { CommandService, ViewService } from '../../shared/interfaces'; import { Stream } from '../../shared/types'; import { OpenStreamDateCommand } from './OpenStreamDateCommand'; import { OpenTodayStreamCommand } from './OpenTodayStreamCommand'; -import { OpenTodayCurrentStreamCommand } from './OpenTodayCurrentStreamCommand'; +import { OpenTodayPrimaryStreamCommand } from './OpenTodayPrimaryStreamCommand'; import { FileCreationInterface, NormalFileStrategy, MeldEncryptedFileStrategy } from './file-creation-strategies'; export class FileOperationsService extends PluginAwareSliceService implements CommandService, ViewService { @@ -43,13 +43,13 @@ export class FileOperationsService extends PluginAwareSliceService implements Co plugin.addCommand({ id: 'open-today-current-stream', - name: 'Open today for current stream', + name: 'Go to primary stream', callback: () => { - const command = new OpenTodayCurrentStreamCommand( - plugin.app, - plugin.settings?.streams || [], - plugin.settings?.reuseCurrentTab || false, - plugin + const command = new OpenTodayPrimaryStreamCommand( + plugin.app, + plugin.settings?.streams || [], + plugin, + plugin.settings?.reuseCurrentTab || false ); command.execute(); } diff --git a/src/slices/file-operations/OpenTodayCurrentStreamCommand.ts b/src/slices/file-operations/OpenTodayCurrentStreamCommand.ts deleted file mode 100644 index 700da24..0000000 --- a/src/slices/file-operations/OpenTodayCurrentStreamCommand.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { App, Notice, WorkspaceLeaf, MarkdownView } from 'obsidian'; -import { Stream } from '../../shared/types'; -import { openStreamDate } from './streamUtils'; -import { Logger } from '../debug-logging/Logger'; -import { Command, StreamManager } from '../../shared/interfaces'; -import { DateStateManager } from '../../shared/DateStateManager'; -import { StreamContextService } from '../../shared/StreamContextService'; - -const log = new Logger(); - -export class OpenTodayCurrentStreamCommand implements Command { - constructor( - private app: App, - private streams: Stream[], - private reuseCurrentTab: boolean = false, - private plugin?: StreamManager, - private targetStream?: Stream, - private targetLeaf?: WorkspaceLeaf, - private dateStateManager?: DateStateManager - ) { } - - async execute(): Promise { - log.debug('Executing OpenTodayCurrentStreamCommand'); - - // Check if there are any streams configured - if (this.streams.length === 0) { - log.debug('No streams configured'); - new Notice('No streams configured. Please add streams in the plugin settings first.'); - return; - } - - // Get the current stream from the targetStream or dynamic resolution - const currentStream = this.targetStream || this.findCurrentStream(); - - if (!currentStream) { - log.debug('No current stream found, cannot open today note'); - new Notice('No active stream context found. Please open a file belonging to a stream first.'); - return; - } - - log.debug(`Opening today's note for current stream: ${currentStream.name}`); - await openStreamDate(this.app, currentStream, new Date(), this.reuseCurrentTab, this.targetLeaf, this.dateStateManager); - } - - private findCurrentStream(): Stream | null { - const streamContextService = new StreamContextService(); - - // 1. Try target leaf if provided - if (this.targetLeaf && this.targetLeaf.view instanceof MarkdownView && this.targetLeaf.view.file) { - const stream = streamContextService.getStreamForFile(this.targetLeaf.view.file, this.streams); - if (stream) return stream; - } - - // 2. Try active file - const activeFile = this.app.workspace.getActiveFile(); - if (activeFile) { - const stream = streamContextService.getStreamForFile(activeFile, this.streams); - if (stream) return stream; - } - - log.debug('No stream context found for current context'); - return null; - } -} diff --git a/src/slices/file-operations/OpenTodayPrimaryStreamCommand.ts b/src/slices/file-operations/OpenTodayPrimaryStreamCommand.ts new file mode 100644 index 0000000..b82ea71 --- /dev/null +++ b/src/slices/file-operations/OpenTodayPrimaryStreamCommand.ts @@ -0,0 +1,64 @@ +import { App, Notice, WorkspaceLeaf } from 'obsidian'; +import { Stream } from '../../shared/types'; +import { openStreamDate } from './streamUtils'; +import { Logger } from '../debug-logging/Logger'; +import { Command, SettingsManager } from '../../shared/interfaces'; +import { DateStateManager } from '../../shared/DateStateManager'; + +const log = new Logger(); + +/** + * Opens today's note for the single configured "primary" stream. + * + * The primary stream is selected in settings (stored as settings.primaryStreamId). + */ +export class OpenTodayPrimaryStreamCommand implements Command { + constructor( + private app: App, + private streams: Stream[], + private settingsManager: SettingsManager, + private reuseCurrentTab: boolean = false, + private targetLeaf?: WorkspaceLeaf, + private dateStateManager?: DateStateManager + ) { } + + async execute(): Promise { + log.debug('Executing OpenTodayPrimaryStreamCommand'); + + if (this.streams.length === 0) { + new Notice('No streams configured. Please add streams in the plugin settings first.'); + return; + } + + const primaryStreamId = this.settingsManager.settings.primaryStreamId; + if (!primaryStreamId) { + new Notice('No primary stream is set. Select one in the Streams plugin settings.'); + return; + } + + const primaryStream = this.streams.find(s => s.id === primaryStreamId) ?? null; + if (!primaryStream) { + // Settings references a stream that no longer exists. + this.settingsManager.settings.primaryStreamId = null; + await this.settingsManager.saveSettings(); + new Notice('Primary stream is missing. Please select a primary stream in settings.'); + return; + } + + if (primaryStream.disabled) { + new Notice(`Primary stream "${primaryStream.name}" is disabled. Enable it or choose a different primary stream.`); + return; + } + + log.debug(`Opening today's note for primary stream: ${primaryStream.name}`); + await openStreamDate( + this.app, + primaryStream, + new Date(), + this.reuseCurrentTab, + this.targetLeaf, + this.dateStateManager + ); + } +} + diff --git a/src/slices/file-operations/index.ts b/src/slices/file-operations/index.ts index c91e6d2..4306fa7 100644 --- a/src/slices/file-operations/index.ts +++ b/src/slices/file-operations/index.ts @@ -3,7 +3,7 @@ export { CreateFileView, CREATE_FILE_VIEW_TYPE } from './CreateFileView'; export { CreateFileViewEncrypted, CREATE_FILE_VIEW_ENCRYPTED_TYPE } from './CreateFileViewEncrypted'; export { InstallMeldView, INSTALL_MELD_VIEW_TYPE } from './InstallMeldView'; export { OpenStreamDateCommand } from './OpenStreamDateCommand'; -export { OpenTodayCurrentStreamCommand } from './OpenTodayCurrentStreamCommand'; +export { OpenTodayPrimaryStreamCommand } from './OpenTodayPrimaryStreamCommand'; export { OpenTodayStreamCommand } from './OpenTodayStreamCommand'; export * from './streamUtils'; export { LeafSelectionService } from './LeafSelectionService'; diff --git a/src/slices/ribbon-integration/RibbonService.ts b/src/slices/ribbon-integration/RibbonService.ts index 0678cef..c4e9ad9 100644 --- a/src/slices/ribbon-integration/RibbonService.ts +++ b/src/slices/ribbon-integration/RibbonService.ts @@ -3,7 +3,7 @@ import { SettingsAwareSliceService } from '../../shared/BaseSlice'; import { Stream, StreamsSettings } from '../../shared/types'; import { eventBus, EVENTS } from '../../shared/EventBus'; import { OpenTodayStreamCommand } from '../file-operations/OpenTodayStreamCommand'; -import { OpenTodayCurrentStreamCommand } from '../file-operations/OpenTodayCurrentStreamCommand'; +import { OpenTodayPrimaryStreamCommand } from '../file-operations/OpenTodayPrimaryStreamCommand'; export class RibbonService extends SettingsAwareSliceService { private ribbonIconsByStream: Map = new Map(); @@ -86,16 +86,16 @@ export class RibbonService extends SettingsAwareSliceService { private createAllStreamsIcon(): void { - // Open today for current stream button + // Go to primary stream button this.getPlugin().addRibbonIcon( 'calendar', - 'Streams: Open today for current stream', + 'Streams: Go to primary stream', () => { - const command = new OpenTodayCurrentStreamCommand( + const command = new OpenTodayPrimaryStreamCommand( this.getPlugin().app, this.getStreams(), - this.getSettings().reuseCurrentTab, - this.getPlugin() + this.getPlugin(), + this.getSettings().reuseCurrentTab ); command.execute(); } diff --git a/src/slices/ribbon-integration/__tests__/RibbonService.test.ts b/src/slices/ribbon-integration/__tests__/RibbonService.test.ts index 34bdee7..20e9abf 100644 --- a/src/slices/ribbon-integration/__tests__/RibbonService.test.ts +++ b/src/slices/ribbon-integration/__tests__/RibbonService.test.ts @@ -32,6 +32,7 @@ describe('RibbonService', () => { // Mock Settings mockSettings = { streams: [], + primaryStreamId: null, barStyle: 'default', reuseCurrentTab: false, // ... other settings as needed ... diff --git a/src/slices/settings-management/SettingsService.ts b/src/slices/settings-management/SettingsService.ts index 7f7f712..ce99723 100644 --- a/src/slices/settings-management/SettingsService.ts +++ b/src/slices/settings-management/SettingsService.ts @@ -138,6 +138,28 @@ export class StreamsSettingTab extends PluginSettingTab { new Setting(containerEl).setName('Streams').setHeading(); + new Setting(containerEl) + .setName('Primary stream') + .setDesc('Used by the "Go to primary stream" command') + .addDropdown(dropdown => { + const current = this.settingsManager.settings.primaryStreamId ?? ''; + + dropdown.addOption('', 'Not set'); + + // Only offer enabled streams; primary stream must be a single usable stream. + this.settingsManager.settings.streams + .filter(s => !s.disabled) + .forEach(stream => dropdown.addOption(stream.id, stream.name)); + + dropdown.setValue(current); + + dropdown.onChange(async (value) => { + this.settingsManager.settings.primaryStreamId = value ? value : null; + await this.settingsManager.saveSettings(); + eventBus.emit(EVENTS.SETTINGS_CHANGED, this.settingsManager.settings, 'settings-management'); + }); + }); + new Setting(containerEl) .setName('Add stream') .setDesc('Create a new note stream') @@ -301,6 +323,13 @@ export class StreamsSettingTab extends PluginSettingTab { .setValue(stream.disabled || false) .onChange(async (value) => { stream.disabled = value; + + // If the disabled stream was the primary stream, clear it. + if (value && this.settingsManager.settings.primaryStreamId === stream.id) { + this.settingsManager.settings.primaryStreamId = null; + new Notice('Primary stream was disabled and has been cleared.'); + } + await this.settingsManager.saveSettings(); eventBus.emit(EVENTS.SETTINGS_CHANGED, this.settingsManager.settings, 'settings-management'); @@ -326,6 +355,12 @@ export class StreamsSettingTab extends PluginSettingTab { .setButtonText('Remove stream') .setWarning() .onClick(async () => { + // If the removed stream was the primary stream, clear it. + if (this.settingsManager.settings.primaryStreamId === stream.id) { + this.settingsManager.settings.primaryStreamId = null; + new Notice('Primary stream was removed and has been cleared.'); + } + this.settingsManager.settings.streams.splice(index, 1); await this.settingsManager.saveSettings(); eventBus.emit(EVENTS.SETTINGS_CHANGED, this.settingsManager.settings, 'settings-management'); diff --git a/styles.css b/styles.css index f8a5abf..3944498 100644 --- a/styles.css +++ b/styles.css @@ -586,6 +586,33 @@ color: var(--text-on-accent); } +/* Primary stream indicator (subtle) */ +.streams-bar-stream-item-primary-indicator { + width: 6px; + height: 6px; + border-radius: 999px; + margin-left: 8px; + background: var(--text-muted); + opacity: 0.45; + flex: 0 0 auto; +} + +.streams-bar-stream-item:hover .streams-bar-stream-item-primary-indicator { + opacity: 0.65; +} + +.streams-bar-stream-item-selected .streams-bar-stream-item-primary-indicator { + background: var(--text-on-accent); + opacity: 0.7; +} + +/* Slightly larger in modern style so it still reads as a dot */ +.streams-bar-component.modern-style .streams-bar-stream-item-primary-indicator { + width: 7px; + height: 7px; + opacity: 0.4; +} + /* Encryption icon for main stream display */ .streams-bar-encryption-icon { display: flex;