diff --git a/main.ts b/main.ts index 43b517b..dbd3c6f 100644 --- a/main.ts +++ b/main.ts @@ -36,6 +36,19 @@ export default class StreamsPlugin extends Plugin implements StreamsAPI { this.settings.barStyle = 'default'; await this.saveSettings(); } + + // Migration: ensure encryptThisStream exists for existing streams + let needsSave = false; + for (const stream of this.settings.streams) { + if (stream.encryptThisStream === undefined) { + stream.encryptThisStream = false; + needsSave = true; + } + } + + if (needsSave) { + await this.saveSettings(); + } } async saveSettings() { @@ -97,4 +110,8 @@ export default class StreamsPlugin extends Plugin implements StreamsAPI { hasStreams(): boolean { return serviceRegistry.api?.hasStreams() || false; } + + getFileOperationsService(): any { + return serviceRegistry.fileOperations; + } } diff --git a/src/shared/interfaces.ts b/src/shared/interfaces.ts index e248edd..73179a9 100644 --- a/src/shared/interfaces.ts +++ b/src/shared/interfaces.ts @@ -71,4 +71,5 @@ export interface StreamsPluginInterface extends Plugin { updateAllStreamsBarComponents(): void; setActiveStream(streamId: string, force?: boolean): void; getActiveStream(): Stream | undefined; + getFileOperationsService(): any; } diff --git a/src/shared/service-registry.ts b/src/shared/service-registry.ts index 3eafae1..9f3d186 100644 --- a/src/shared/service-registry.ts +++ b/src/shared/service-registry.ts @@ -6,6 +6,7 @@ import { sliceContainer } from './container'; import { DebugLoggingService } from '../slices/debug-logging'; import { StreamManagementService } from '../slices/stream-management'; import { APIService } from '../slices/api'; +import { FileOperationsService } from '../slices/file-operations'; export class ServiceRegistry { private static instance: ServiceRegistry; @@ -31,6 +32,10 @@ export class ServiceRegistry { get api(): APIService | undefined { return sliceContainer.get('api') as APIService; } + + get fileOperations(): FileOperationsService | undefined { + return sliceContainer.get('file-operations') as FileOperationsService; + } } // Export singleton instance diff --git a/src/shared/types.ts b/src/shared/types.ts index 1d816a0..b5877c2 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -23,6 +23,7 @@ export interface Stream { icon: LucideIcon; showTodayInRibbon: boolean; addCommand: boolean; + encryptThisStream: boolean; // New field for encryption toggle } export interface StreamsSettings { diff --git a/src/slices/calendar-navigation/CalendarNavigationService.ts b/src/slices/calendar-navigation/CalendarNavigationService.ts index 53c6d6b..c07081d 100644 --- a/src/slices/calendar-navigation/CalendarNavigationService.ts +++ b/src/slices/calendar-navigation/CalendarNavigationService.ts @@ -323,7 +323,8 @@ export class CalendarNavigationService extends SettingsAwareSliceService { icon: 'book', folder: 'Streams', showTodayInRibbon: true, - addCommand: true + addCommand: true, + encryptThisStream: false }; } diff --git a/src/slices/calendar-navigation/StreamsBarComponent.ts b/src/slices/calendar-navigation/StreamsBarComponent.ts index 5159d93..e836a6d 100644 --- a/src/slices/calendar-navigation/StreamsBarComponent.ts +++ b/src/slices/calendar-navigation/StreamsBarComponent.ts @@ -214,7 +214,6 @@ export class StreamsBarComponent extends Component { } } else { // Don't add calendar component to sidebars or other panes - centralizedLogger.debug('Calendar component not added - not in main editor area'); this.component.remove(); return; } @@ -1031,8 +1030,6 @@ export class StreamsBarComponent extends Component { // Refresh the streams dropdown to show the correct selection this.refreshStreamsDropdown(); - - centralizedLogger.debug(`StreamsBarComponent updated to active stream: ${newActiveStream.name}`); } private handleSettingsChange(settings: any): void { diff --git a/src/slices/file-operations/CreateFileView.ts b/src/slices/file-operations/CreateFileView.ts index fe0293c..95a81ad 100644 --- a/src/slices/file-operations/CreateFileView.ts +++ b/src/slices/file-operations/CreateFileView.ts @@ -36,7 +36,6 @@ export class CreateFileView extends ItemView { this.filePath = filePath; this.stream = stream; this.dateStateManager = DateStateManager.getInstance(); - centralizedLogger.debug(`CreateFileView constructor called with filePath: ${filePath}`); } getViewType(): string { @@ -67,17 +66,13 @@ export class CreateFileView extends ItemView { async setState(state: { stream?: Stream; date?: string | Date; filePath?: string }, result?: unknown): Promise { try { - centralizedLogger.debug(`CreateFileView setState called with state:`, state); - // Check if the view is still valid - more comprehensive checks if (!this || !this.contentEl || !this.leaf || this.contentEl === null || this.leaf === null) { - centralizedLogger.debug(`CreateFileView setState called but view is no longer valid, skipping`); return; } // Additional safety check - ensure the view is still attached to the DOM if (!document.contains(this.contentEl)) { - centralizedLogger.debug(`CreateFileView setState called but view is no longer in DOM, skipping`); return; } @@ -135,8 +130,6 @@ export class CreateFileView extends ItemView { } async onOpen(): Promise { - centralizedLogger.debug(`CreateFileView onOpen called with filePath: ${this.filePath}, stream: ${this.stream.name}`); - // Set this as the active stream in the main plugin this.setActiveStream(); @@ -191,13 +184,9 @@ export class CreateFileView extends ItemView { // Create our create file view content this.createFileViewContent(this.contentEl); - - centralizedLogger.debug('CreateFileView content rendered and made visible'); } async onClose(): Promise { - centralizedLogger.debug(`CreateFileView onClose called for filePath: ${this.filePath}`); - // Clean up the MutationObserver if ((this as any).emptyStateObserver) { (this as any).emptyStateObserver.disconnect(); @@ -262,7 +251,7 @@ export class CreateFileView extends ItemView { eventBus.emit('create-file-view-opened', this.leaf); }); } catch (error) { - centralizedLogger.debug('Could not trigger calendar component:', error); + // Calendar component trigger failed - not critical } } @@ -291,6 +280,45 @@ export class CreateFileView extends ItemView { } private async createAndOpenFile(): Promise { + try { + // Get the file operations service to use the strategy pattern + const plugin = (this.app as any).plugins?.plugins?.['streams']; + if (!plugin) { + centralizedLogger.error('Streams plugin not found'); + return; + } + + // Check if encryption is enabled but Meld is not available + if (this.stream.encryptThisStream) { + const fileOpsService = plugin.getFileOperationsService?.(); + if (fileOpsService && !fileOpsService.isMeldPluginAvailable()) { + // Show error and don't create file + new (this.app as any).Notice(fileOpsService.getMeldUnavailableMessage()); + return; + } + } + + // Create the file normally first (without encryption) + const file = await this.createFileNormally(); + + if (file instanceof TFile) { + // Open the file in the current leaf (this will replace CreateFileView) + await this.leaf.openFile(file); + + // If encryption is enabled, trigger it after the file is opened + if (this.stream.encryptThisStream) { + // Small delay to ensure the file is fully loaded + setTimeout(async () => { + await this.triggerEncryption(file); + }, 200); + } + } + } catch (error) { + centralizedLogger.error('Error creating file:', error); + } + } + + private async createFileNormally(): Promise { try { const folderPath = this.filePath.substring(0, this.filePath.lastIndexOf('/')); @@ -305,13 +333,99 @@ export class CreateFileView extends ItemView { } } + // Create the file normally (without encryption) const file = await this.app.vault.create(this.filePath, ''); + return file instanceof TFile ? file : null; + } catch (error) { + centralizedLogger.error('Error creating file normally:', error); + return null; + } + } + + private async triggerEncryption(file: TFile): Promise { + try { + // Ensure the file is the active file + const activeFile = this.app.workspace.getActiveFile(); - if (file instanceof TFile) { - await this.leaf.openFile(file); + if (activeFile?.path !== file.path) { + // Find a leaf with this file and make it active + const fileLeaf = this.app.workspace.getLeavesOfType('markdown') + .find(leaf => { + try { + const view = leaf.view as any; + return view?.file?.path === file.path; + } catch (e) { + return false; + } + }); + + if (fileLeaf) { + this.app.workspace.setActiveLeaf(fileLeaf, { focus: true }); + } else { + centralizedLogger.error(`Could not find leaf with file: ${file.path}`); + return; + } + } + + // Small delay to ensure the file is properly active + await new Promise(resolve => setTimeout(resolve, 100)); + + // Try to execute the Meld encryption command + const command = (this.app as any).commands?.commands?.['meld-encrypt:meld-encrypt-convert-to-or-from-encrypted-note']; + + if (command && command.callback && typeof command.callback === 'function') { + try { + await command.callback(); + } catch (cmdError) { + centralizedLogger.error(`Meld command execution failed:`, cmdError); + } + } else { + // Fallback: Use command palette API + try { + await (this.app as any).commands.executeCommandById('meld-encrypt:meld-encrypt-convert-to-or-from-encrypted-note'); + } catch (altError) { + centralizedLogger.error('Meld encryption command failed:', altError); + } } } catch (error) { - centralizedLogger.error('Error creating file:', error); + centralizedLogger.error(`Error triggering encryption for file ${file.path}:`, error); + } + } + + private async createFileWithStrategy(): Promise { + try { + const folderPath = this.filePath.substring(0, this.filePath.lastIndexOf('/')); + + if (folderPath) { + try { + const folderExists = this.app.vault.getAbstractFileByPath(folderPath); + if (!folderExists) { + await this.app.vault.createFolder(folderPath); + } + } catch (error) { + // Using existing folder + } + } + + // Get the file operations service + const plugin = (this.app as any).plugins?.plugins?.['streams']; + if (!plugin) { + centralizedLogger.error('Streams plugin not found'); + return null; + } + + const fileOpsService = plugin.getFileOperationsService?.(); + if (fileOpsService) { + // Use the strategy pattern + return await fileOpsService.createFile(this.filePath, '', this.stream); + } else { + // Fallback to normal file creation + const file = await this.app.vault.create(this.filePath, ''); + return file instanceof TFile ? file : null; + } + } catch (error) { + centralizedLogger.error('Error creating file with strategy:', error); + return null; } } diff --git a/src/slices/file-operations/FileOperationsService.ts b/src/slices/file-operations/FileOperationsService.ts index bb78b1b..ed3ac74 100644 --- a/src/slices/file-operations/FileOperationsService.ts +++ b/src/slices/file-operations/FileOperationsService.ts @@ -1,17 +1,31 @@ -import { App } from 'obsidian'; +import { App, TFile, WorkspaceLeaf } from 'obsidian'; import { PluginAwareSliceService } from '../../shared/base-slice'; import { CommandService, ViewService } from '../../shared/interfaces'; import { OpenStreamDateCommand } from './OpenStreamDateCommand'; import { OpenTodayStreamCommand } from './OpenTodayStreamCommand'; import { OpenTodayCurrentStreamCommand } from './OpenTodayCurrentStreamCommand'; import { CreateFileView, CREATE_FILE_VIEW_TYPE } from './CreateFileView'; +import { FileCreationInterface, NormalFileStrategy, MeldEncryptedFileStrategy } from './file-creation-strategies'; +import { MeldDetectionService } from '../meld-integration'; +import { centralizedLogger } from '../../shared/centralized-logger'; export class FileOperationsService extends PluginAwareSliceService implements CommandService, ViewService { private registeredCommands: string[] = []; + private meldDetectionService: MeldDetectionService; + private normalFileStrategy: FileCreationInterface; + private meldEncryptedFileStrategy: FileCreationInterface; async initialize(): Promise { if (this.initialized) return; + // Initialize strategies + this.meldDetectionService = new MeldDetectionService(); + this.meldDetectionService.setPlugin(this.getPlugin()); + await this.meldDetectionService.initialize(); + + this.normalFileStrategy = new NormalFileStrategy(); + this.meldEncryptedFileStrategy = new MeldEncryptedFileStrategy(); + this.registerViews(); this.registerCommands(); @@ -21,11 +35,17 @@ export class FileOperationsService extends PluginAwareSliceService implements Co cleanup(): void { this.unregisterCommands(); this.unregisterViews(); + + // Cleanup Meld detection service + if (this.meldDetectionService) { + this.meldDetectionService.cleanup(); + } + this.initialized = false; } registerViews(): void { - // No views to register - CreateFileView is created directly + // No views to register } unregisterViews(): void { @@ -83,4 +103,60 @@ export class FileOperationsService extends PluginAwareSliceService implements Co const plugin = this.getPlugin() as any; return plugin.settings || {}; } + + /** + * Get the appropriate file creation strategy for a stream + */ + private getFileCreationStrategy(stream: any): FileCreationInterface { + if (stream.encryptThisStream) { + // Check if Meld is available before using encryption strategy + if (this.meldDetectionService.isMeldPluginAvailable()) { + return this.meldEncryptedFileStrategy; + } else { + // Fall back to normal strategy if Meld is not available + console.warn('Meld plugin not available, falling back to normal file creation'); + return this.normalFileStrategy; + } + } + return this.normalFileStrategy; + } + + /** + * Create a file using the appropriate strategy + */ + async createFile(filePath: string, content: string, stream: any): Promise { + const strategy = this.getFileCreationStrategy(stream); + return await strategy.createFile(this.getPlugin().app, filePath, content); + } + + /** + * Check if Meld plugin is available + */ + isMeldPluginAvailable(): boolean { + return this.meldDetectionService?.isMeldPluginAvailable() || false; + } + + /** + * Get Meld unavailable message + */ + getMeldUnavailableMessage(): string { + return this.meldDetectionService?.getMeldUnavailableMessage() || 'Meld plugin is not available'; + } + + + /** + * Check if file content appears to be encrypted + */ + public isEncryptedContent(content: string): boolean { + // Common patterns that indicate encrypted content + const encryptedPatterns = [ + /^-----BEGIN PGP MESSAGE-----/, + /^-----BEGIN ENCRYPTED MESSAGE-----/, + /^-----BEGIN MESSAGE-----/, + /^U2FsdGVkX1/, // Base64 encoded encrypted content (common in some encryption tools) + /^[A-Za-z0-9+/]{100,}={0,2}$/ // Long base64 strings (potential encrypted content) + ]; + + return encryptedPatterns.some(pattern => pattern.test(content.trim())); + } } diff --git a/src/slices/file-operations/file-creation-strategies/MeldEncryptedFileStrategy.ts b/src/slices/file-operations/file-creation-strategies/MeldEncryptedFileStrategy.ts new file mode 100644 index 0000000..4c07cc9 --- /dev/null +++ b/src/slices/file-operations/file-creation-strategies/MeldEncryptedFileStrategy.ts @@ -0,0 +1,137 @@ +import { App, TFile } from 'obsidian'; +import { FileCreationInterface } from './FileCreationStrategy'; +import { centralizedLogger } from '../../../shared/centralized-logger'; + +/** + * Strategy for creating encrypted files using Meld plugin + */ +export class MeldEncryptedFileStrategy implements FileCreationInterface { + private meldCommandId = 'meld-encrypt:meld-encrypt-convert-to-or-from-encrypted-note'; + + async createFile(app: App, filePath: string, content: string): Promise { + try { + // Ensure the folder exists + const folderPath = filePath.substring(0, filePath.lastIndexOf('/')); + if (folderPath && !app.vault.getAbstractFileByPath(folderPath)) { + await app.vault.createFolder(folderPath); + } + + // First create the file normally + const file = await app.vault.create(filePath, content); + + if (!(file instanceof TFile)) { + centralizedLogger.error(`Failed to create file before encryption: ${filePath} - result is not a TFile`); + return null; + } + + // Then encrypt it using Meld plugin + await this.encryptFile(app, file); + + return file; + } catch (error) { + centralizedLogger.error(`Error creating encrypted file ${filePath}:`, error); + return null; + } + } + + private async encryptFile(app: App, file: TFile): Promise { + try { + // Check if Meld plugin is available + if (!this.isMeldPluginAvailable(app)) { + throw new Error('Meld plugin is not available or not enabled'); + } + + // Get the Meld plugin instance + const meldPlugin = (app as any).plugins?.plugins?.['meld-encrypt']; + if (!meldPlugin) { + throw new Error('Meld plugin instance not found'); + } + + // Try to call the encryption method directly on the plugin + if (meldPlugin.encryptFile && typeof meldPlugin.encryptFile === 'function') { + await meldPlugin.encryptFile(file); + return; + } + + // Try to call encryption with file path parameter + if (meldPlugin.encryptFileByPath && typeof meldPlugin.encryptFileByPath === 'function') { + await meldPlugin.encryptFileByPath(file.path); + return; + } + + // Fallback: Use command execution + const command = (app as any).commands?.commands?.[this.meldCommandId]; + if (command) { + if (command.callback.length > 0) { + // Command accepts parameters + await command.callback(file.path); + } else { + // Command needs active file context + const activeLeaf = app.workspace.activeLeaf; + let fileLeaf = null; + + try { + // Find or create a leaf with the file + const existingLeaf = app.workspace.getLeavesOfType('markdown') + .find(leaf => { + try { + const view = leaf.view as any; + return view?.file?.path === file.path; + } catch (e) { + return false; + } + }); + + if (existingLeaf) { + fileLeaf = existingLeaf; + } else { + fileLeaf = app.workspace.getLeaf('tab'); + await fileLeaf.openFile(file); + } + + // Set as active leaf and execute command + app.workspace.setActiveLeaf(fileLeaf, { focus: true }); + await new Promise(resolve => setTimeout(resolve, 100)); + await command.callback(); + + } finally { + // Restore original active leaf + if (activeLeaf && activeLeaf !== fileLeaf) { + app.workspace.setActiveLeaf(activeLeaf, { focus: false }); + } + } + } + } else { + throw new Error(`Meld encryption command not found: ${this.meldCommandId}`); + } + } catch (error) { + centralizedLogger.error(`Error encrypting file ${file.path}:`, error); + throw error; + } + } + + private isMeldPluginAvailable(app: App): boolean { + try { + // Check if the Meld plugin is installed and enabled + const plugins = (app as any).plugins?.plugins; + if (!plugins) return false; + + // Check for Meld plugin + const meldPlugin = plugins['meld-encrypt']; + if (!meldPlugin) return false; + + // Check if the specific command exists + const commands = (app as any).commands?.commands; + if (!commands) return false; + + return !!commands[this.meldCommandId]; + } catch (error) { + centralizedLogger.error('Error checking Meld plugin availability:', error); + return false; + } + } + + getStrategyName(): string { + return 'MeldEncryptedFile'; + } +} diff --git a/src/slices/file-operations/file-creation-strategies/NormalFileStrategy.ts b/src/slices/file-operations/file-creation-strategies/NormalFileStrategy.ts new file mode 100644 index 0000000..a141267 --- /dev/null +++ b/src/slices/file-operations/file-creation-strategies/NormalFileStrategy.ts @@ -0,0 +1,35 @@ +import { App, TFile } from 'obsidian'; +import { FileCreationInterface } from './FileCreationStrategy'; +import { centralizedLogger } from '../../../shared/centralized-logger'; + +/** + * Strategy for creating normal (unencrypted) files + */ +export class NormalFileStrategy implements FileCreationInterface { + async createFile(app: App, filePath: string, content: string): Promise { + try { + // Ensure the folder exists + const folderPath = filePath.substring(0, filePath.lastIndexOf('/')); + if (folderPath && !app.vault.getAbstractFileByPath(folderPath)) { + await app.vault.createFolder(folderPath); + } + + // Create the file + const file = await app.vault.create(filePath, content); + + if (file instanceof TFile) { + return file; + } else { + centralizedLogger.error(`Failed to create normal file: ${filePath} - result is not a TFile`); + return null; + } + } catch (error) { + centralizedLogger.error(`Error creating normal file ${filePath}:`, error); + return null; + } + } + + getStrategyName(): string { + return 'NormalFile'; + } +} diff --git a/src/slices/file-operations/file-creation-strategies/index.ts b/src/slices/file-operations/file-creation-strategies/index.ts new file mode 100644 index 0000000..dfa4d7f --- /dev/null +++ b/src/slices/file-operations/file-creation-strategies/index.ts @@ -0,0 +1,3 @@ +export type { FileCreationInterface } from './FileCreationStrategy'; +export { NormalFileStrategy } from './NormalFileStrategy'; +export { MeldEncryptedFileStrategy } from './MeldEncryptedFileStrategy'; diff --git a/src/slices/file-operations/streamUtils.ts b/src/slices/file-operations/streamUtils.ts index e32bd80..89afb40 100644 --- a/src/slices/file-operations/streamUtils.ts +++ b/src/slices/file-operations/streamUtils.ts @@ -4,6 +4,22 @@ import { centralizedLogger } from '../../shared/centralized-logger'; import { CreateFileView, CREATE_FILE_VIEW_TYPE } from './CreateFileView'; import { DateStateManager } from '../../shared/date-state-manager'; +/** + * Check if file content appears to be encrypted + */ +function isEncryptedContent(content: string): boolean { + // Common patterns that indicate encrypted content + const encryptedPatterns = [ + /^-----BEGIN PGP MESSAGE-----/, + /^-----BEGIN ENCRYPTED MESSAGE-----/, + /^-----BEGIN MESSAGE-----/, + /^U2FsdGVkX1/, // Base64 encoded encrypted content (common in some encryption tools) + /^[A-Za-z0-9+/]{100,}={0,2}$/ // Long base64 strings (potential encrypted content) + ]; + + return encryptedPatterns.some(pattern => pattern.test(content.trim())); +} + /** * Interface for Obsidian's internal ViewRegistry, which manages all view registrations */ @@ -109,7 +125,12 @@ export async function openStreamDate(app: App, stream: Stream, date: Date = new // Looking for file at path let file = app.vault.getAbstractFileByPath(filePath); - // File exists check + + // If file not found, check for encrypted version (.mdenc) + if (!file) { + const encryptedFilePath = filePath.replace(/\.md$/, '.mdenc'); + file = app.vault.getAbstractFileByPath(encryptedFilePath); + } if (!file) { // File not found, showing create file view @@ -186,7 +207,6 @@ export async function openStreamDate(app: App, stream: Stream, date: Date = new filePath: filePath } }); - centralizedLogger.debug(`Successfully set view state for CreateFileView`); } catch (error) { centralizedLogger.error(`Error setting view state for CreateFileView:`, error); // If setViewState fails, we can't proceed @@ -206,6 +226,39 @@ export async function openStreamDate(app: App, stream: Stream, date: Date = new if (file instanceof TFile) { try { + // Check if this is a .mdenc file + const isMdencFile = file.path.endsWith('.mdenc'); + + if (isMdencFile) { + // For .mdenc files, just open them normally and let Meld handle the encryption + + // Update the date state manager to reflect the current date + const dateStateManager = DateStateManager.getInstance(); + dateStateManager.setCurrentDate(date); + + let leaf: WorkspaceLeaf | null = null; + + if (reuseCurrentTab) { + const activeLeaf = app.workspace.activeLeaf; + leaf = activeLeaf || app.workspace.getLeaf('tab'); + } else { + leaf = app.workspace.getLeaf('tab'); + } + + if (leaf) { + await leaf.openFile(file); + app.workspace.setActiveLeaf(leaf, { focus: true }); + } + return; + } + + // For .md files, just open them normally + + // Update the date state manager to reflect the current date + const dateStateManager = DateStateManager.getInstance(); + dateStateManager.setCurrentDate(date); + + // File is not encrypted, proceed with normal opening let leaf: WorkspaceLeaf | null = null; if (reuseCurrentTab) { @@ -213,7 +266,6 @@ export async function openStreamDate(app: App, stream: Stream, date: Date = new const activeLeaf = app.workspace.activeLeaf; if (activeLeaf) { leaf = activeLeaf; - // log.debug('Reusing current active leaf for markdown view (reuseCurrentTab enabled)'); } else { // Fallback: look for existing leaf with the same file const existingLeaf = app.workspace.getLeavesOfType('markdown') @@ -227,17 +279,14 @@ export async function openStreamDate(app: App, stream: Stream, date: Date = new const filePath = normalizePath(file.path); return viewPath === filePath; } catch (e) { - // log.debug('Error comparing files:', e); return false; } }); if (existingLeaf) { leaf = existingLeaf; - // log.debug('Found existing leaf with same file'); } else { leaf = app.workspace.getLeaf('tab'); - // log.debug('Created a new leaf for markdown view'); } } } else { @@ -253,17 +302,14 @@ export async function openStreamDate(app: App, stream: Stream, date: Date = new const filePath = normalizePath(file.path); return viewPath === filePath; } catch (e) { - // log.debug('Error comparing files:', e); return false; } }); if (existingLeaf) { leaf = existingLeaf; - // log.debug('Found existing leaf with same file'); } else { leaf = app.workspace.getLeaf('tab'); - // log.debug('Created a new leaf for markdown view'); } } diff --git a/src/slices/meld-integration/MeldDetectionService.ts b/src/slices/meld-integration/MeldDetectionService.ts new file mode 100644 index 0000000..9e63566 --- /dev/null +++ b/src/slices/meld-integration/MeldDetectionService.ts @@ -0,0 +1,83 @@ +import { App } from 'obsidian'; +import { PluginAwareSliceService } from '../../shared/base-slice'; +import { centralizedLogger } from '../../shared/centralized-logger'; + +/** + * Service for detecting and validating Meld plugin availability + */ +export class MeldDetectionService extends PluginAwareSliceService { + private meldCommandId = 'meld-encrypt:meld-encrypt-convert-to-or-from-encrypted-note'; + + async initialize(): Promise { + if (this.initialized) return; + this.initialized = true; + } + + cleanup(): void { + this.initialized = false; + } + + /** + * Check if Meld plugin is installed and enabled + */ + isMeldPluginAvailable(): boolean { + try { + const app = this.getPlugin().app; + + // Check if the Meld plugin is installed and enabled + const plugins = (app as any).plugins?.plugins; + if (!plugins) return false; + + // Check for Meld plugin + const meldPlugin = plugins['meld-encrypt']; + if (!meldPlugin) return false; + + // Check if the specific command exists + const commands = (app as any).commands?.commands; + if (!commands) return false; + + return !!commands[this.meldCommandId]; + } catch (error) { + centralizedLogger.error('Error checking Meld plugin availability:', error); + return false; + } + } + + /** + * Get the Meld encryption command ID + */ + getMeldCommandId(): string { + return this.meldCommandId; + } + + /** + * Execute the Meld encryption command + */ + async executeMeldEncryption(): Promise { + try { + if (!this.isMeldPluginAvailable()) { + throw new Error('Meld plugin is not available or not enabled'); + } + + const app = this.getPlugin().app; + const command = (app as any).commands?.commands?.[this.meldCommandId]; + + if (command) { + await command.callback(); + return true; + } else { + throw new Error(`Meld encryption command not found: ${this.meldCommandId}`); + } + } catch (error) { + centralizedLogger.error('Error executing Meld encryption command:', error); + return false; + } + } + + /** + * Get a user-friendly error message for when Meld is not available + */ + getMeldUnavailableMessage(): string { + return 'Meld plugin is not installed or not enabled. Please install and enable the Meld plugin to use encryption features.'; + } +} diff --git a/src/slices/meld-integration/index.ts b/src/slices/meld-integration/index.ts new file mode 100644 index 0000000..604b8c6 --- /dev/null +++ b/src/slices/meld-integration/index.ts @@ -0,0 +1 @@ +export { MeldDetectionService } from './MeldDetectionService'; diff --git a/src/slices/settings-management/SettingsService.ts b/src/slices/settings-management/SettingsService.ts index 1e22017..c24d086 100644 --- a/src/slices/settings-management/SettingsService.ts +++ b/src/slices/settings-management/SettingsService.ts @@ -129,7 +129,8 @@ export class StreamsSettingTab extends PluginSettingTab { folder: '', icon: 'file-text' as LucideIcon, showTodayInRibbon: true, - addCommand: false + addCommand: false, + encryptThisStream: false }; this.plugin.settings.streams.push(newStream); await this.plugin.saveSettings(); @@ -200,6 +201,9 @@ export class StreamsSettingTab extends PluginSettingTab { eventBus.emit(EVENTS.SETTINGS_CHANGED, this.plugin.settings, 'settings-management'); })); + // Encrypt this stream + this.addEncryptionToggle(container, stream); + // Remove stream new Setting(container) .addButton(button => button @@ -212,4 +216,43 @@ export class StreamsSettingTab extends PluginSettingTab { this.display(); })); } + + private addEncryptionToggle(container: HTMLElement, stream: Stream): void { + // Check if Meld plugin is available + const fileOpsService = this.plugin.getFileOperationsService?.(); + const isMeldAvailable = fileOpsService?.isMeldPluginAvailable() || false; + + const encryptionSetting = new Setting(container) + .setName('Encrypt this stream') + .setDesc(isMeldAvailable + ? 'When enabled, files created in this stream will be encrypted using the Meld plugin' + : 'Meld plugin is not available. Please install and enable the Meld plugin to use encryption features.' + ) + .addToggle(toggle => { + toggle + .setValue(stream.encryptThisStream || false) + .setDisabled(!isMeldAvailable) + .onChange(async (value) => { + if (value && !isMeldAvailable) { + new Notice('Meld plugin is not available. Please install and enable the Meld plugin first.'); + return; + } + + stream.encryptThisStream = value; + await this.plugin.saveSettings(); + eventBus.emit(EVENTS.SETTINGS_CHANGED, this.plugin.settings, 'settings-management'); + + new Notice(`Encryption ${value ? 'enabled' : 'disabled'} for stream "${stream.name}"`); + }); + }); + + // Add warning if Meld is not available + if (!isMeldAvailable) { + const warningEl = container.createDiv('streams-encryption-warning'); + warningEl.style.color = 'var(--text-error)'; + warningEl.style.fontSize = '0.9em'; + warningEl.style.marginTop = '0.5em'; + warningEl.textContent = '⚠️ Meld plugin is required for encryption features'; + } + } }