add change stream ui

This commit is contained in:
Ben Floyd 2025-08-16 10:54:07 -06:00
parent 7eededb9c9
commit 7f1dc8a007
4 changed files with 301 additions and 33 deletions

10
main.ts
View file

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

View file

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

View file

@ -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
*/

View file

@ -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 */