import { EventRef, Plugin, WorkspaceLeaf, normalizePath, Notice, } from "obsidian"; import { GitHubSyncSettings, DEFAULT_SETTINGS } from "./settings/settings"; import GitHubSyncSettingsTab from "./settings/tab"; import SyncManager, { ConflictFile, ConflictResolution } from "./sync-manager"; import Logger from "./logger"; import { ConflictsResolutionView, CONFLICTS_RESOLUTION_VIEW_TYPE, } from "./views/conflicts-resolution/view"; export default class GitHubSyncPlugin extends Plugin { settings: GitHubSyncSettings; syncManager: SyncManager; logger: Logger; statusBarItem: HTMLElement | null = null; syncRibbonIcon: HTMLElement | null = null; conflictsRibbonIcon: HTMLElement | null = null; activeLeafChangeListener: EventRef | null = null; vaultCreateListener: EventRef | null = null; vaultModifyListener: EventRef | null = null; // Called in ConflictResolutionView when the user solves all the conflicts. // This is initialized every time we open the view to set new conflicts so // we can notify the SyncManager that everything has been resolved and the sync // process can continue on. conflictsResolver: ((resolutions: ConflictResolution[]) => void) | null = null; // We keep track of the sync conflicts in here too in case the // conflicts view must be rebuilt, or the user closes the view // and it gets destroyed. // By keeping them here we can recreate it easily. private conflicts: ConflictFile[] = []; async onUserEnable() { if ( this.settings.githubToken === "" || this.settings.githubOwner === "" || this.settings.githubRepo === "" || this.settings.githubBranch === "" ) { new Notice("Go to settings to configure syncing"); } } getConflictsView(): ConflictsResolutionView | null { const leaves = this.app.workspace.getLeavesOfType( CONFLICTS_RESOLUTION_VIEW_TYPE, ); if (leaves.length === 0) { return null; } return leaves[0].view as ConflictsResolutionView; } async activateView() { const { workspace } = this.app; let leaf: WorkspaceLeaf | null = null; const leaves = workspace.getLeavesOfType(CONFLICTS_RESOLUTION_VIEW_TYPE); if (leaves.length > 0) { leaf = leaves[0]; } else { leaf = workspace.getLeaf(false)!; await leaf.setViewState({ type: CONFLICTS_RESOLUTION_VIEW_TYPE, active: true, }); } workspace.revealLeaf(leaf); } async onload() { await this.loadSettings(); this.logger = new Logger(this.app.vault, this.settings.enableLogging); this.logger.init(); this.registerView( CONFLICTS_RESOLUTION_VIEW_TYPE, (leaf) => new ConflictsResolutionView(leaf, this, this.conflicts), ); this.addSettingTab(new GitHubSyncSettingsTab(this.app, this)); this.syncManager = new SyncManager( this.app.vault, this.settings, this.onConflicts.bind(this), this.logger, ); await this.syncManager.loadMetadata(); if (this.settings.syncStrategy == "interval") { this.restartSyncInterval(); } this.app.workspace.onLayoutReady(async () => { // Create the events handling only after tha layout is ready to avoid // getting spammed with create events. // See the official Obsidian docs: // https://docs.obsidian.md/Reference/TypeScript+API/Vault/on('create') await this.syncManager.startEventsListener(); // Load the ribbons after layout is ready so they're shown after the core // buttons if (this.settings.showStatusBarItem) { this.showStatusBarItem(); } if (this.settings.showConflictsRibbonButton) { this.showConflictsRibbonIcon(); } if (this.settings.showSyncRibbonButton) { this.showSyncRibbonIcon(); } }); this.addCommand({ id: "sync-files", name: "Sync with GitHub", repeatable: false, icon: "refresh-cw", callback: this.sync.bind(this), }); this.addCommand({ id: "merge", name: "Open sync conflicts view", repeatable: false, icon: "refresh-cw", callback: this.openConflictsView.bind(this), }); } async sync() { if ( this.settings.githubToken === "" || this.settings.githubOwner === "" || this.settings.githubRepo === "" || this.settings.githubBranch === "" ) { new Notice("Sync plugin not configured"); return; } if (this.settings.firstSync) { await this.syncManager.firstSync(); this.settings.firstSync = false; this.saveSettings(); } else { await this.syncManager.sync(); } this.updateStatusBarItem(); } async onunload() { this.stopSyncInterval(); } showStatusBarItem() { if (this.statusBarItem) { return; } this.statusBarItem = this.addStatusBarItem(); if (!this.activeLeafChangeListener) { this.activeLeafChangeListener = this.app.workspace.on( "active-leaf-change", () => this.updateStatusBarItem(), ); } if (!this.vaultCreateListener) { this.vaultCreateListener = this.app.vault.on("create", () => { this.updateStatusBarItem(); }); } if (!this.vaultModifyListener) { this.vaultModifyListener = this.app.vault.on("modify", () => { this.updateStatusBarItem(); }); } } hideStatusBarItem() { this.statusBarItem?.remove(); this.statusBarItem = null; } updateStatusBarItem() { if (!this.statusBarItem) { return; } const activeFile = this.app.workspace.getActiveFile(); if (!activeFile) { return; } let state = "Unknown"; const fileData = this.syncManager.getFileMetadata(activeFile.path); if (!fileData) { state = "Untracked"; } else if (fileData.dirty) { state = "Outdated"; } else if (!fileData.dirty) { state = "Up to date"; } this.statusBarItem.setText(`GitHub: ${state}`); } showSyncRibbonIcon() { if (this.syncRibbonIcon) { return; } this.syncRibbonIcon = this.addRibbonIcon( "refresh-cw", "Sync with GitHub", this.sync.bind(this), ); } hideSyncRibbonIcon() { this.syncRibbonIcon?.remove(); this.syncRibbonIcon = null; } showConflictsRibbonIcon() { if (this.conflictsRibbonIcon) { return; } this.conflictsRibbonIcon = this.addRibbonIcon( "merge", "Open sync conflicts view", this.openConflictsView.bind(this), ); } hideConflictsRibbonIcon() { this.conflictsRibbonIcon?.remove(); this.conflictsRibbonIcon = null; } async openConflictsView() { await this.activateView(); this.getConflictsView()?.setConflictFiles(this.conflicts); } async onConflicts(conflicts: ConflictFile[]): Promise { this.conflicts = conflicts; return await new Promise(async (resolve) => { this.conflictsResolver = resolve; await this.activateView(); this.getConflictsView()?.setConflictFiles(conflicts); }); } async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); } async saveSettings() { await this.saveData(this.settings); } // Proxy methods from sync manager to ease handling the interval // when settings are changed startSyncInterval() { const intervalID = this.syncManager.startSyncInterval( this.settings.syncInterval, ); this.registerInterval(intervalID); } stopSyncInterval() { this.syncManager.stopSyncInterval(); } restartSyncInterval() { this.syncManager.stopSyncInterval(); this.syncManager.startSyncInterval(this.settings.syncInterval); } async reset() { this.settings = DEFAULT_SETTINGS; this.saveSettings(); await this.syncManager.resetMetadata(); } }