diff --git a/README.md b/README.md index 718b324..c643b4f 100644 --- a/README.md +++ b/README.md @@ -38,4 +38,21 @@ This view displays each stream as a card showing: - Total number of files - Files created this year and month - Last modified date -- Quick action buttons to open today's note or view the full stream \ No newline at end of file +- Quick action buttons to open today's note or view the full stream + +## Commands +The plugin provides several commands accessible via the Command Palette: + +- **Open Current Stream Today**: Automatically detects which stream the current view belongs to and opens that stream's today note. This works whether you're in a file that belongs to a stream, a stream view, or a create file view. This is useful when you want to quickly jump to today's note for the current stream context. +- **Open All Streams View**: Opens the dashboard view showing all streams. +- **Toggle calendar component**: Shows/hides the calendar component. +- **Toggle calendar compact mode**: Switches between compact and full calendar display. + +Individual stream commands are also available if enabled in stream settings: +- **Stream Name, today**: Opens today's note for a specific stream. +- **Open full view: Stream Name**: Opens the full stream view for a specific stream. + +### Keyboard Shortcuts +You can assign custom keyboard shortcuts to any of these commands through Obsidian's Settings → Hotkeys. Some recommended shortcuts: +- **Open Current Stream Today**: `Ctrl+Shift+T` (or `Cmd+Shift+T` on Mac) +- **Open All Streams View**: `Ctrl+Shift+A` (or `Cmd+Shift+A` on Mac) \ No newline at end of file diff --git a/main.ts b/main.ts index ee27b71..b05804d 100644 --- a/main.ts +++ b/main.ts @@ -4,6 +4,7 @@ import { Stream, StreamsSettings, LucideIcon } from './types'; import { CalendarComponent } from './src/components/CalendarComponent'; import { Logger, LogLevel } from './src/utils/Logger'; import { OpenTodayStreamCommand } from './src/commands/OpenTodayStreamCommand'; +import { OpenCurrentStreamTodayCommand } from './src/commands/OpenCurrentStreamTodayCommand'; import { StreamSelectionModal } from './src/modals/StreamSelectionModal'; import { CREATE_FILE_VIEW_TYPE, CreateFileView } from './src/views/CreateFileView'; import { STREAM_VIEW_TYPE, StreamView } from './src/views/StreamView'; @@ -775,6 +776,16 @@ export default class StreamsPlugin extends Plugin { command.execute(); } }); + + // Add command to open current stream's today note + this.addCommand({ + id: 'open-current-stream-today', + name: 'Open Current Stream Today', + callback: () => { + const command = new OpenCurrentStreamTodayCommand(this.app, this.settings.streams, this.settings.reuseCurrentTab); + command.execute(); + } + }); } private registerCalendarCommands(): void { diff --git a/src/commands/OpenCurrentStreamTodayCommand.ts b/src/commands/OpenCurrentStreamTodayCommand.ts new file mode 100644 index 0000000..319668c --- /dev/null +++ b/src/commands/OpenCurrentStreamTodayCommand.ts @@ -0,0 +1,166 @@ +import { App, MarkdownView, WorkspaceLeaf, Notice, ItemView } from 'obsidian'; +import { Stream } from '../../types'; +import { openStreamDate } from '../utils/streamUtils'; +import { Logger } from '../utils/Logger'; +import { Command } from './Command'; +import { STREAM_VIEW_TYPE } from '../views/StreamView'; +import { CREATE_FILE_VIEW_TYPE } from '../views/CreateFileView'; +import { ALL_STREAMS_VIEW_TYPE } from '../views/AllStreamsView'; + +const log = new Logger(); + +export class OpenCurrentStreamTodayCommand implements Command { + constructor( + private app: App, + private streams: Stream[], + private reuseCurrentTab: boolean = false + ) {} + + async execute(): Promise { + log.debug('Executing OpenCurrentStreamTodayCommand'); + + // 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; + } + + // Try to find the current stream based on active view + const currentStream = this.findCurrentStream(); + + if (!currentStream) { + log.debug('No current stream found, cannot open today note'); + new Notice('No stream context found. Please open a file that belongs to a stream, or use a stream-specific view.'); + return; + } + + log.debug(`Opening today's note for current stream: ${currentStream.name}`); + await openStreamDate(this.app, currentStream, new Date(), this.reuseCurrentTab); + } + + private findCurrentStream(): Stream | null { + const activeLeaf = this.app.workspace.activeLeaf; + if (!activeLeaf) { + log.debug('No active leaf found'); + return null; + } + + const view = activeLeaf.view; + if (!view) { + log.debug('No view found in active leaf'); + return null; + } + + // Handle markdown views (most common case) + if (view instanceof MarkdownView) { + const file = view.file; + if (file) { + return this.findStreamByFilePath(file.path); + } else { + log.debug('MarkdownView has no file'); + return null; + } + } + + // Handle other view types + const viewType = view.getViewType(); + log.debug(`Active view type: ${viewType}`); + + // Handle StreamView - extract stream from view state + if (viewType === STREAM_VIEW_TYPE) { + const streamView = view as any; // Cast to access private properties + if (streamView.stream && this.isValidStream(streamView.stream)) { + log.debug(`Found stream from StreamView: ${streamView.stream.name}`); + return streamView.stream; + } + } + + // Handle CreateFileView - extract stream from view state + if (viewType === CREATE_FILE_VIEW_TYPE) { + const createView = view as any; // Cast to access private properties + if (createView.stream && this.isValidStream(createView.stream)) { + log.debug(`Found stream from CreateFileView: ${createView.stream.name}`); + return createView.stream; + } + } + + // Handle AllStreamsView - this view doesn't have a specific stream + if (viewType === ALL_STREAMS_VIEW_TYPE) { + log.debug('User is in All Streams view - no specific stream context'); + new Notice('Please select a specific stream or open a file that belongs to a stream.'); + return null; + } + + // For other view types, try to extract stream from view state if possible + if (view instanceof ItemView) { + try { + const state = view.getState(); + if (state && state.stream && this.isValidStream(state.stream)) { + log.debug(`Found stream from view state: ${state.stream.name}`); + return state.stream; + } + } catch (error) { + log.debug('Could not extract stream from view state:', error); + } + } + + log.debug('No stream found in current view'); + return null; + } + + private isValidStream(stream: any): stream is Stream { + return stream && + typeof stream === 'object' && + typeof stream.id === 'string' && + typeof stream.name === 'string' && + typeof stream.folder === 'string' && + typeof stream.icon === 'string'; + } + + private findStreamByFilePath(filePath: string): Stream | null { + log.debug(`Looking for stream matching file: ${filePath}`); + + // Find which stream this file belongs to + const stream = this.streams.find(s => { + // Skip streams with empty folders + if (!s.folder || s.folder.trim() === '') { + return false; + } + + // Normalize paths for comparison + const normalizedFilePath = filePath.split(/[/\\]/).filter(Boolean); + const normalizedStreamPath = s.folder.split(/[/\\]/).filter(Boolean); + + // If stream path is empty, skip this stream + if (normalizedStreamPath.length === 0) { + return false; + } + + // Check if the file path starts with the stream path + const isInStreamFolder = normalizedStreamPath.every((part, index) => { + // Check bounds + if (index >= normalizedFilePath.length) { + return false; + } + return normalizedStreamPath[index] === normalizedFilePath[index]; + }); + + log.debug(`Checking stream "${s.name}" (${s.folder}):`, { + isInStreamFolder, + normalizedFilePath, + normalizedStreamPath + }); + + return isInStreamFolder; + }); + + if (stream) { + log.debug(`File belongs to stream: ${stream.name} (${stream.folder})`); + return stream; + } else { + log.debug('File does not belong to any stream'); + return null; + } + } +}