add primary stream concept and remove selected stream

This commit is contained in:
Ben Floyd 2026-01-12 17:14:53 -07:00
parent 4a982b85bc
commit c39eff0189
14 changed files with 172 additions and 85 deletions

View file

@ -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';

View file

@ -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,

View file

@ -21,6 +21,7 @@ export const CALENDAR_ENABLED_VIEW_TYPES = [
*/
export const DEFAULT_SETTINGS = {
streams: [],
primaryStreamId: null,
showStreamsBarComponent: true,
reuseCurrentTab: false,
debugLoggingEnabled: false,

View file

@ -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;

View file

@ -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
);

View file

@ -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<void>;
}
/**
@ -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');

View file

@ -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();
}

View file

@ -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<void> {
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;
}
}

View file

@ -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<void> {
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
);
}
}

View file

@ -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';

View file

@ -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<string, { today?: HTMLElement }> = 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();
}

View file

@ -32,6 +32,7 @@ describe('RibbonService', () => {
// Mock Settings
mockSettings = {
streams: [],
primaryStreamId: null,
barStyle: 'default',
reuseCurrentTab: false,
// ... other settings as needed ...

View file

@ -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');

View file

@ -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;