mirror of
https://github.com/bfloydd/streams.git
synced 2026-07-22 05:49:02 +00:00
add primary stream concept and remove selected stream
This commit is contained in:
parent
4a982b85bc
commit
c39eff0189
14 changed files with 172 additions and 85 deletions
6
main.ts
6
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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export const CALENDAR_ENABLED_VIEW_TYPES = [
|
|||
*/
|
||||
export const DEFAULT_SETTINGS = {
|
||||
streams: [],
|
||||
primaryStreamId: null,
|
||||
showStreamsBarComponent: true,
|
||||
reuseCurrentTab: false,
|
||||
debugLoggingEnabled: false,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
64
src/slices/file-operations/OpenTodayPrimaryStreamCommand.ts
Normal file
64
src/slices/file-operations/OpenTodayPrimaryStreamCommand.ts
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ describe('RibbonService', () => {
|
|||
// Mock Settings
|
||||
mockSettings = {
|
||||
streams: [],
|
||||
primaryStreamId: null,
|
||||
barStyle: 'default',
|
||||
reuseCurrentTab: false,
|
||||
// ... other settings as needed ...
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
27
styles.css
27
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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue