import { Diff2HtmlUI, type Diff2HtmlUIConfig } from "diff2html/lib/ui/js/diff2html-ui-base"; import { Event } from "Enums/Event"; import { ItemView, WorkspaceLeaf, Platform, type ViewStateResult } from "obsidian"; import { Resolve } from "Services/DependencyService"; import type { EventService } from "Services/EventService"; import type { DiffService } from "Services/DiffService"; import { Services } from "Services/Services"; import { VIEW_TYPE_MAIN, type MainView } from "./MainView"; import { tick } from "svelte"; import { Copy } from "Enums/Copy"; export const VIEW_TYPE_DIFF = 'vaultkeeper-ai-diff-view'; interface DiffViewState { diffString: string; config: Diff2HtmlUIConfig; } export class DiffView extends ItemView { private readonly eventService: EventService; private readonly diffService: DiffService; private diffString: string = ""; private config: Diff2HtmlUIConfig = {}; private diffContainer: HTMLElement | null = null; private mobileButtonsContainer: HTMLElement | null = null; constructor(leaf: WorkspaceLeaf) { super(leaf); this.eventService = Resolve(Services.EventService); this.diffService = Resolve(Services.DiffService); this.registerEvent(this.eventService.on(Event.DiffClosed, () => { this.leaf.detach(); })); } protected override onClose(): Promise { // trigger DiffClosed event in case the user closed the tab this.eventService.trigger(Event.DiffClosed); return Promise.resolve(); } public getViewType(): string { return VIEW_TYPE_DIFF; } public getDisplayText(): string { return "Vaultkeeper AI diff"; } public async setState(state: DiffViewState, result: ViewStateResult): Promise { this.diffString = state.diffString; this.config = state.config; this.renderDiff(); return super.setState(state, result); } public getState(): Record { return { diffString: this.diffString, config: this.config }; } private renderDiff() { const container = this.resetContainer(); if (Platform.isMobile) { container.addClass('diff-view-mobile'); } this.diffContainer = container.createDiv({ cls: 'd2h-wrapper' }); const diff2htmlUi = new Diff2HtmlUI(this.diffContainer, this.diffString, this.config); diff2htmlUi.draw(); if (Platform.isMobile) { this.mobileButtonsContainer = this.createMobileButtons(); } window.requestAnimationFrame(() => { const firstChange = this.diffContainer?.querySelector('.d2h-change, .d2h-ins, .d2h-del'); firstChange?.scrollIntoView({ behavior: 'smooth', block: 'center' }); }); } private resetContainer(): HTMLElement { const container = this.contentEl; container.empty(); if (this.diffContainer) { this.diffContainer.remove(); this.diffContainer = null; } if (this.mobileButtonsContainer) { this.mobileButtonsContainer.remove(); this.mobileButtonsContainer = null; } return container; } private createMobileButtons(): HTMLElement { const container = this.contentEl.createDiv({ cls: 'diff-mobile-controls' }); const acceptButton = container.createEl('button', { cls: 'diff-mobile-button diff-mobile-accept', text: Copy.ButtonApprove }); acceptButton.setAttribute('aria-label', Copy.ButtonApprove); const suggestButton = container.createEl('button', { cls: 'diff-mobile-button diff-mobile-suggest', text: Copy.ButtonSuggest }); suggestButton.setAttribute('aria-label', Copy.ButtonSuggest); const rejectButton = container.createEl('button', { cls: 'diff-mobile-button diff-mobile-reject', text: Copy.ButtonReject }); rejectButton.setAttribute('aria-label', Copy.ButtonReject); this.registerDomEvent(acceptButton, 'click', async () => { this.diffService.onAccept(); await this.refocusMainView(); }); this.registerDomEvent(suggestButton, 'click', async () => { await this.refocusMainView(true); }); this.registerDomEvent(rejectButton, 'click', async () => { this.diffService.onReject(); await this.refocusMainView(); }); return container; } private async refocusMainView(focusChatInput: boolean = false): Promise { await tick().then(async () => { const { workspace } = this.app; const leaves = workspace.getLeavesOfType(VIEW_TYPE_MAIN); if (leaves.length > 0) { await workspace.revealLeaf(leaves[0]); workspace.setActiveLeaf(leaves[0], { focus: true }); if (focusChatInput) { (leaves[0].view as MainView).input?.focusInput(true); } } }); } }