mirror of
https://github.com/bfloydd/streams.git
synced 2026-07-22 05:49:02 +00:00
add context move
This commit is contained in:
parent
d469bace0d
commit
4804d224c3
5 changed files with 455 additions and 0 deletions
|
|
@ -12,6 +12,7 @@ import { StreamManagementService } from '../slices/stream-management';
|
|||
import { MobileIntegrationService } from '../slices/mobile-integration';
|
||||
import { APIService } from '../slices/api';
|
||||
import { CommandRegistrationService } from '../slices/command-registration';
|
||||
import { ContextMenuService } from '../slices/context-menu';
|
||||
|
||||
export class ServiceLoader {
|
||||
/**
|
||||
|
|
@ -28,6 +29,7 @@ export class ServiceLoader {
|
|||
sliceContainer.register('ribbon-integration', new RibbonService());
|
||||
sliceContainer.register('mobile-integration', new MobileIntegrationService());
|
||||
sliceContainer.register('command-registration', new CommandRegistrationService());
|
||||
sliceContainer.register('context-menu', new ContextMenuService());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
164
src/slices/context-menu/ContextMenuService.ts
Normal file
164
src/slices/context-menu/ContextMenuService.ts
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
import { PluginAwareSliceService } from '../../shared/base-slice';
|
||||
import { StreamManagementService } from '../stream-management/StreamManagementService';
|
||||
import { centralizedLogger } from '../../shared/centralized-logger';
|
||||
import { MoveTextToStreamModal, MoveTextOptions } from './MoveTextToStreamModal';
|
||||
import { MarkdownView, Notice, Menu, Editor, TFile, TAbstractFile } from 'obsidian';
|
||||
import { Stream } from '../../shared/types';
|
||||
|
||||
export class ContextMenuService extends PluginAwareSliceService {
|
||||
private registeredEvents: Array<() => void> = [];
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
if (this.initialized) return;
|
||||
|
||||
this.registerEditorContextMenu();
|
||||
this.registerFileContextMenu();
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
cleanup(): void {
|
||||
this.registeredEvents.forEach(unregister => unregister());
|
||||
this.registeredEvents = [];
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
private registerEditorContextMenu(): void {
|
||||
const plugin = this.getPlugin();
|
||||
|
||||
const unregister = plugin.registerEvent(
|
||||
plugin.app.workspace.on('editor-menu', (menu: Menu, editor: Editor) => {
|
||||
const selectedText = editor.getSelection();
|
||||
|
||||
if (selectedText && this.hasStreams()) {
|
||||
this.addMoveTextMenuItem(menu, selectedText, editor);
|
||||
}
|
||||
})
|
||||
) as unknown as () => void;
|
||||
|
||||
this.registeredEvents.push(unregister);
|
||||
}
|
||||
|
||||
private registerFileContextMenu(): void {
|
||||
// File context menu functionality removed as requested
|
||||
}
|
||||
|
||||
|
||||
private addMoveTextMenuItem(menu: Menu, selectedText: string, editor: Editor): void {
|
||||
menu.addItem((item) => {
|
||||
item
|
||||
.setTitle('Move selected text to stream')
|
||||
.setIcon('arrow-right')
|
||||
.onClick(async () => {
|
||||
await this.showMoveTextModal(selectedText, editor);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private hasStreams(): boolean {
|
||||
return this.getStreams().length > 0;
|
||||
}
|
||||
|
||||
private async showMoveTextModal(selectedText: string, editor: Editor): Promise<void> {
|
||||
const streams = this.getStreams();
|
||||
if (streams.length === 0) {
|
||||
new Notice('No streams available');
|
||||
return;
|
||||
}
|
||||
|
||||
const moveOptions: MoveTextOptions = {
|
||||
selectedText,
|
||||
sourceEditor: editor,
|
||||
sourceView: this.getPlugin().app.workspace.getActiveViewOfType(MarkdownView)
|
||||
};
|
||||
|
||||
const modal = new MoveTextToStreamModal(
|
||||
this.getPlugin().app,
|
||||
streams,
|
||||
moveOptions,
|
||||
(options) => this.moveTextToStream(options)
|
||||
);
|
||||
|
||||
modal.open();
|
||||
}
|
||||
|
||||
private async moveTextToStream(options: {
|
||||
stream: Stream;
|
||||
date: string;
|
||||
prepend: boolean;
|
||||
addDivider: boolean;
|
||||
text: string;
|
||||
sourceEditor: Editor;
|
||||
sourceView: MarkdownView | null;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
const { stream, date, prepend, addDivider, text, sourceEditor } = options;
|
||||
|
||||
const fileName = `${date}.md`;
|
||||
const filePath = `${stream.folder}/${fileName}`;
|
||||
|
||||
let targetFile = this.getPlugin().app.vault.getAbstractFileByPath(filePath);
|
||||
let targetContent = '';
|
||||
|
||||
if (targetFile) {
|
||||
targetContent = await this.getPlugin().app.vault.read(targetFile as TFile);
|
||||
} else {
|
||||
targetContent = `# ${date}\n\n`;
|
||||
targetFile = await this.getPlugin().app.vault.create(filePath, targetContent);
|
||||
}
|
||||
|
||||
const textToAdd = this.prepareTextToAdd(text, addDivider, prepend);
|
||||
const newContent = this.insertTextIntoContent(targetContent, textToAdd, prepend);
|
||||
|
||||
await this.getPlugin().app.vault.modify(targetFile as TFile, newContent);
|
||||
|
||||
if (sourceEditor.getSelection() === text) {
|
||||
sourceEditor.replaceSelection('');
|
||||
}
|
||||
|
||||
new Notice(`Text moved to ${stream.name} (${date})`);
|
||||
|
||||
} catch (error) {
|
||||
centralizedLogger.error('Error moving text to stream:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private prepareTextToAdd(text: string, addDivider: boolean, prepend: boolean): string {
|
||||
if (!addDivider) {
|
||||
return `\n\n${text}\n`;
|
||||
}
|
||||
|
||||
return prepend
|
||||
? `\n\n${text}\n\n---\n`
|
||||
: `\n\n---\n\n${text}\n`;
|
||||
}
|
||||
|
||||
private insertTextIntoContent(content: string, textToAdd: string, prepend: boolean): string {
|
||||
if (!prepend) {
|
||||
return content + textToAdd;
|
||||
}
|
||||
|
||||
const headingMatch = content.match(/^# .+$/m);
|
||||
if (headingMatch) {
|
||||
const insertIndex = headingMatch.index! + headingMatch[0].length;
|
||||
return content.slice(0, insertIndex) + textToAdd + content.slice(insertIndex);
|
||||
}
|
||||
|
||||
return textToAdd + content;
|
||||
}
|
||||
|
||||
private getStreams(): Stream[] {
|
||||
const plugin = this.getPlugin() as any;
|
||||
return plugin.settings?.streams || [];
|
||||
}
|
||||
|
||||
private getStreamService(): StreamManagementService | null {
|
||||
return this.getService('stream-management') as StreamManagementService | null;
|
||||
}
|
||||
|
||||
private getService(serviceName: string): unknown {
|
||||
const container = (this.getPlugin() as any).sliceContainer;
|
||||
return container?.get(serviceName);
|
||||
}
|
||||
}
|
||||
243
src/slices/context-menu/MoveTextToStreamModal.ts
Normal file
243
src/slices/context-menu/MoveTextToStreamModal.ts
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
import { App, Modal, Setting, TFile, MarkdownView, Notice } from 'obsidian';
|
||||
import { Stream } from '../../shared/types';
|
||||
import { centralizedLogger } from '../../shared/centralized-logger';
|
||||
|
||||
export interface MoveTextOptions {
|
||||
selectedText: string;
|
||||
sourceEditor: any;
|
||||
sourceView: any;
|
||||
}
|
||||
|
||||
export class MoveTextToStreamModal extends Modal {
|
||||
private streams: Stream[];
|
||||
private selectedStream: Stream | null = null;
|
||||
private useToday: boolean = true;
|
||||
private selectedDate: string = new Date().toISOString().split('T')[0];
|
||||
private prependMode: boolean = false; // true = prepend, false = append
|
||||
private addDivider: boolean = true; // true = add --- divider, false = no divider
|
||||
private selectedText: string;
|
||||
private sourceEditor: any;
|
||||
private sourceView: any;
|
||||
private onMove: (options: {
|
||||
stream: Stream;
|
||||
date: string;
|
||||
prepend: boolean;
|
||||
addDivider: boolean;
|
||||
text: string;
|
||||
sourceEditor: any;
|
||||
sourceView: any;
|
||||
}) => Promise<void>;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
streams: Stream[],
|
||||
moveOptions: MoveTextOptions,
|
||||
onMove: (options: {
|
||||
stream: Stream;
|
||||
date: string;
|
||||
prepend: boolean;
|
||||
addDivider: boolean;
|
||||
text: string;
|
||||
sourceEditor: any;
|
||||
sourceView: any;
|
||||
}) => Promise<void>
|
||||
) {
|
||||
super(app);
|
||||
this.streams = streams;
|
||||
this.onMove = onMove;
|
||||
this.selectedStream = streams.length > 0 ? streams[0] : null;
|
||||
this.selectedText = moveOptions.selectedText;
|
||||
this.sourceEditor = moveOptions.sourceEditor;
|
||||
this.sourceView = moveOptions.sourceView;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
// Title
|
||||
contentEl.createEl('h2', { text: 'Move Text to Stream' });
|
||||
|
||||
// Stream selection
|
||||
new Setting(contentEl)
|
||||
.setName('Target Stream')
|
||||
.setDesc('Select the stream to move the text to')
|
||||
.addDropdown(dropdown => {
|
||||
this.streams.forEach(stream => {
|
||||
dropdown.addOption(stream.id, stream.name);
|
||||
});
|
||||
|
||||
if (this.selectedStream) {
|
||||
dropdown.setValue(this.selectedStream.id);
|
||||
}
|
||||
|
||||
dropdown.onChange(value => {
|
||||
this.selectedStream = this.streams.find(s => s.id === value) || null;
|
||||
});
|
||||
});
|
||||
|
||||
// Date selection toggle
|
||||
new Setting(contentEl)
|
||||
.setName('Date Selection')
|
||||
.setDesc('Choose when to add the text')
|
||||
.addToggle(toggle => {
|
||||
toggle
|
||||
.setValue(this.useToday)
|
||||
.onChange(value => {
|
||||
this.useToday = value;
|
||||
this.updateDateSetting();
|
||||
});
|
||||
})
|
||||
.addText(text => {
|
||||
text
|
||||
.setValue('Today')
|
||||
.setDisabled(true);
|
||||
});
|
||||
|
||||
// Custom date setting (stubbed for future calendar implementation)
|
||||
this.createDateSetting(contentEl);
|
||||
|
||||
// Prepend/Append toggle
|
||||
new Setting(contentEl)
|
||||
.setName('Text Position')
|
||||
.setDesc('Choose where to place the text in the target file')
|
||||
.addToggle(toggle => {
|
||||
toggle
|
||||
.setValue(this.prependMode)
|
||||
.onChange(value => {
|
||||
this.prependMode = value;
|
||||
});
|
||||
})
|
||||
.addText(text => {
|
||||
text
|
||||
.setValue(this.prependMode ? 'Prepend (add to top)' : 'Append (add to bottom)')
|
||||
.setDisabled(true);
|
||||
});
|
||||
|
||||
// Divider toggle
|
||||
new Setting(contentEl)
|
||||
.setName('Add Divider')
|
||||
.setDesc('Add --- separator around the moved text')
|
||||
.addToggle(toggle => {
|
||||
toggle
|
||||
.setValue(this.addDivider)
|
||||
.onChange(value => {
|
||||
this.addDivider = value;
|
||||
});
|
||||
})
|
||||
.addText(text => {
|
||||
text
|
||||
.setValue(this.addDivider ? 'Add --- separator' : 'No separator')
|
||||
.setDisabled(true);
|
||||
});
|
||||
|
||||
|
||||
// Action buttons
|
||||
const buttonContainer = contentEl.createEl('div', {
|
||||
cls: 'move-text-buttons'
|
||||
});
|
||||
|
||||
const moveButton = buttonContainer.createEl('button', {
|
||||
text: 'Move Text',
|
||||
cls: 'mod-cta'
|
||||
});
|
||||
|
||||
const cancelButton = buttonContainer.createEl('button', {
|
||||
text: 'Cancel',
|
||||
cls: 'mod-secondary'
|
||||
});
|
||||
|
||||
// Event handlers
|
||||
moveButton.addEventListener('click', async () => {
|
||||
await this.handleMove();
|
||||
});
|
||||
|
||||
cancelButton.addEventListener('click', () => {
|
||||
this.close();
|
||||
});
|
||||
|
||||
// Update date setting initially
|
||||
this.updateDateSetting();
|
||||
}
|
||||
|
||||
private createDateSetting(container: HTMLElement): void {
|
||||
this.dateSetting = new Setting(container)
|
||||
.setName('Custom Date')
|
||||
.setDesc('Select a specific date (future: calendar picker)')
|
||||
.addText(text => {
|
||||
text
|
||||
.setValue(this.selectedDate)
|
||||
.setPlaceholder('YYYY-MM-DD')
|
||||
.onChange(value => {
|
||||
this.selectedDate = value;
|
||||
});
|
||||
})
|
||||
.addButton(button => {
|
||||
button
|
||||
.setButtonText('📅')
|
||||
.setTooltip('Open calendar picker (coming soon)')
|
||||
.onClick(() => {
|
||||
// TODO: Implement calendar picker
|
||||
this.showCalendarStub();
|
||||
});
|
||||
});
|
||||
|
||||
this.dateSetting.settingEl.style.display = this.useToday ? 'none' : 'block';
|
||||
}
|
||||
|
||||
private updateDateSetting(): void {
|
||||
if (this.dateSetting) {
|
||||
this.dateSetting.settingEl.style.display = this.useToday ? 'none' : 'block';
|
||||
}
|
||||
}
|
||||
|
||||
private showCalendarStub(): void {
|
||||
// TODO: Implement calendar picker
|
||||
// For now, just show a notice
|
||||
new Notice('Calendar picker coming soon!');
|
||||
}
|
||||
|
||||
private getSelectedText(): string {
|
||||
return this.selectedText;
|
||||
}
|
||||
|
||||
private async handleMove(): Promise<void> {
|
||||
if (!this.selectedStream) {
|
||||
new Notice('Please select a stream');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.useToday && !this.selectedDate) {
|
||||
new Notice('Please select a date');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const targetDate = this.useToday ?
|
||||
new Date().toISOString().split('T')[0] :
|
||||
this.selectedDate;
|
||||
|
||||
await this.onMove({
|
||||
stream: this.selectedStream,
|
||||
date: targetDate,
|
||||
prepend: this.prependMode,
|
||||
addDivider: this.addDivider,
|
||||
text: this.getSelectedText(),
|
||||
sourceEditor: this.sourceEditor,
|
||||
sourceView: this.sourceView
|
||||
});
|
||||
|
||||
this.close();
|
||||
} catch (error) {
|
||||
centralizedLogger.error('Error moving text to stream:', error);
|
||||
new Notice('Failed to move text to stream');
|
||||
}
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
|
||||
private dateSetting: Setting | null = null;
|
||||
}
|
||||
3
src/slices/context-menu/index.ts
Normal file
3
src/slices/context-menu/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { ContextMenuService } from './ContextMenuService';
|
||||
export { MoveTextToStreamModal } from './MoveTextToStreamModal';
|
||||
export type { MoveTextOptions } from './MoveTextToStreamModal';
|
||||
43
styles.css
43
styles.css
|
|
@ -682,4 +682,47 @@
|
|||
|
||||
.is-phone .streams-create-file-date {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
/*********************************************************
|
||||
* MOVE TEXT MODAL STYLES
|
||||
*********************************************************/
|
||||
|
||||
.move-text-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.move-text-buttons button {
|
||||
min-width: 80px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.move-text-buttons button.mod-cta {
|
||||
background: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.move-text-buttons button.mod-cta:hover {
|
||||
background: var(--interactive-accent-hover);
|
||||
}
|
||||
|
||||
.move-text-buttons button.mod-secondary {
|
||||
background: var(--interactive-normal);
|
||||
color: var(--text-normal);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.move-text-buttons button.mod-secondary:hover {
|
||||
background: var(--interactive-hover);
|
||||
border-color: var(--text-accent);
|
||||
}
|
||||
Loading…
Reference in a new issue