diff --git a/src/main.ts b/src/main.ts index 6008dfe..eceba9d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,6 +14,7 @@ import { NoteLockerSettings, DEFAULT_SETTINGS } from "./models/types"; import { FileExplorerUI } from "./ui/fileExplorer"; import { StatusBarUI } from "./ui/statusBar"; import { NoteLockerSettingTab } from "./settings"; +import { StrictUnlockModal } from "./ui/strictUnlockModal"; export default class NoteLockerPlugin extends Plugin { settings: NoteLockerSettings = DEFAULT_SETTINGS; @@ -48,13 +49,39 @@ export default class NoteLockerPlugin extends Plugin { const file = this.app.workspace.getActiveFile(); if (file && file.extension === 'md') { if (!checking) { - this.toggleNoteLock(file.path); + if (this.settings.strictLockedNotes.has(file.path)) { + new StrictUnlockModal(this.app, () => { + this.toggleStrictLock(file.path); + }).open(); + } else { + this.toggleNoteLock(file.path); + } } return true; } return false; }, }); + + this.addCommand({ + id: 'switch-to-edit-mode-intercept', + name: 'Switch to Edit Mode (Strict Lock Check)', + hotkeys: [{ modifiers: ["Mod"], key: "e" }], + checkCallback: (checking: boolean) => { + const file = this.app.workspace.getActiveFile(); + if (file && file.extension === 'md') { + if (this.settings.strictLockedNotes.has(file.path)) { + if (!checking) { + new StrictUnlockModal(this.app, () => { + this.toggleStrictLock(file.path); + }).open(); + } + return true; + } + } + return false; + }, + }); } onunload() { @@ -95,7 +122,6 @@ export default class NoteLockerPlugin extends Plugin { this.app.vault.on("rename", async (file, oldPath) => { let settingsChanged = false; - // Handle locked notes if (this.settings.lockedNotes.delete(oldPath)) { if (!this.settings.lockedNotes.has(file.path)) { this.settings.lockedNotes.add(file.path); @@ -103,7 +129,6 @@ export default class NoteLockerPlugin extends Plugin { } } - // Handle locked folders if (this.settings.lockedFolders.delete(oldPath)) { if (!this.settings.lockedFolders.has(file.path)) { this.settings.lockedFolders.add(file.path); @@ -128,6 +153,7 @@ export default class NoteLockerPlugin extends Plugin { this.registerEvent( this.app.workspace.on("layout-change", () => { this.fileExplorerUI.updateFileExplorerIcons(); + this.app.workspace.iterateAllLeaves((leaf) => this.updateLeafMode(leaf)); }) ); } @@ -148,14 +174,21 @@ export default class NoteLockerPlugin extends Plugin { let allLocked = true; let allUnlocked = true; + let allStrictUnlocked = true; for (const item of validItems) { const isLocked = this.isPathLocked(item.path); + const isStrictLocked = this.settings.strictLockedNotes.has(item.path); + if (isLocked) { allUnlocked = false; } else { allLocked = false; } + + if (isStrictLocked) { + allStrictUnlocked = false; + } } if (allLocked) { @@ -163,7 +196,18 @@ export default class NoteLockerPlugin extends Plugin { item .setTitle(`Unlock ${validItems.length} items`) .setIcon("unlock") - .onClick(() => this.toggleBulkLock(validItems, false)) + .onClick(() => { + if (!allStrictUnlocked) { + new StrictUnlockModal( + this.app, + () => this.toggleBulkLock(validItems, false), + "Strictly Locked Items", + "At least one of the selected items is strictly locked." + ).open(); + } else { + this.toggleBulkLock(validItems, false); + } + }) ); } else if (allUnlocked) { menu.addItem((item) => @@ -173,7 +217,18 @@ export default class NoteLockerPlugin extends Plugin { .onClick(() => this.toggleBulkLock(validItems, true)) ); } - // if mixed, do nothing + + const hasFiles = validItems.some(f => f instanceof TFile); + if (allStrictUnlocked && hasFiles) { + if (allUnlocked || allLocked) { + menu.addItem((item) => + item + .setTitle(`Strict Lock ${validItems.length} items`) + .setIcon("lock") + .onClick(() => this.toggleBulkStrictLock(validItems, true)) + ); + } + } } private async toggleBulkLock(files: TAbstractFile[], shouldLock: boolean) { @@ -183,6 +238,7 @@ export default class NoteLockerPlugin extends Plugin { this.settings.lockedNotes.add(file.path); } else { this.settings.lockedNotes.delete(file.path); + this.settings.strictLockedNotes.delete(file.path); } } else if (file instanceof TFolder) { if (shouldLock) { @@ -206,6 +262,30 @@ export default class NoteLockerPlugin extends Plugin { }); } + private async toggleBulkStrictLock(files: TAbstractFile[], shouldLock: boolean) { + for (const file of files) { + if (file instanceof TFile) { + if (shouldLock) { + this.settings.strictLockedNotes.add(file.path); + } else { + this.settings.strictLockedNotes.delete(file.path); + } + } + } + + await this.saveSettings(); + this.fileExplorerUI.updateFileExplorerIcons(); + + this.app.workspace.iterateAllLeaves((leaf) => { + if (leaf.view instanceof MarkdownView && leaf.view.file) { + const path = leaf.view.file.path; + if (files.some(f => f.path === path)) { + this.updateLeafMode(leaf); + } + } + }); + } + private addLockMenuItem(menu: Menu, path: string) { if (this.isParentFolderLocked(path)) { return; @@ -215,12 +295,38 @@ export default class NoteLockerPlugin extends Plugin { if (file instanceof TFile && file.extension === 'md') { const isLocked = this.settings.lockedNotes.has(path); - menu.addItem((item) => - item - .setTitle(isLocked ? "Unlock" : "Lock") - .setIcon(isLocked ? "unlock" : "lock") - .onClick(() => this.toggleNoteLock(path)) - ); + const isStrictLocked = this.settings.strictLockedNotes.has(path); + + if (!isStrictLocked) { + menu.addItem((item) => + item + .setTitle(isLocked ? "Unlock" : "Lock") + .setIcon(isLocked ? "unlock" : "lock") + .onClick(() => this.toggleNoteLock(path)) + ); + } + + if (!isLocked) { + if (!isStrictLocked) { + menu.addItem((item) => + item + .setTitle("Strict Lock") + .setIcon("lock") + .onClick(() => this.toggleStrictLock(path)) + ); + } else { + menu.addItem((item) => + item + .setTitle("Strict Unlock") + .setIcon("unlock") + .onClick(() => { + new StrictUnlockModal(this.app, () => { + this.toggleStrictLock(path); + }).open(); + }) + ); + } + } } else if (file && !(file instanceof TFile)) { const isLocked = this.settings.lockedFolders.has(path); menu.addItem((item) => @@ -300,6 +406,24 @@ export default class NoteLockerPlugin extends Plugin { this.fileExplorerUI.updateFileExplorerIcons(); } + async toggleStrictLock(notePath: string) { + const isStrictLocked = this.settings.strictLockedNotes.has(notePath); + + if (isStrictLocked) { + this.settings.strictLockedNotes.delete(notePath); + } else { + this.settings.strictLockedNotes.add(notePath); + } + + await this.saveSettings(); + + new Notice(`${isStrictLocked ? '🔓 Strictly Unlocked' : '🔒 Strictly Locked'}: ${notePath}`); + + this.updateAllNoteInstances(notePath); + this.statusBarUI.updateStatusBarButton(); + this.fileExplorerUI.updateFileExplorerIcons(); + } + private truncateFileName(name: string): string { const maxLength = Platform.isMobile ? this.settings.mobileNotificationMaxLength @@ -322,6 +446,7 @@ export default class NoteLockerPlugin extends Plugin { } public isPathLocked(path: string): boolean { + if (this.settings.strictLockedNotes.has(path)) return true; if (this.settings.lockedNotes.has(path)) return true; let currentPath = path; @@ -338,16 +463,84 @@ export default class NoteLockerPlugin extends Plugin { if (!leaf || !(leaf.view instanceof MarkdownView)) return; const { view } = leaf; - const targetMode = - view.file && this.isPathLocked(view.file.path) - ? "preview" - : "source"; + const path = view.file?.path; + if (!path) return; - if (leaf.getViewState().state?.mode !== targetMode) { - leaf.setViewState({ - ...leaf.getViewState(), - state: { ...leaf.getViewState().state, mode: targetMode }, + const isStrictLocked = this.settings.strictLockedNotes.has(path); + const isLocked = this.isPathLocked(path); + + if (isStrictLocked) { + const state = leaf.getViewState(); + if (state.state?.mode === 'source') { + leaf.setViewState({ + ...state, + state: { ...state.state, mode: 'preview' }, + }); + + if (this.app.workspace.activeLeaf === leaf) { + new StrictUnlockModal(this.app, () => { + this.toggleStrictLock(path); + }).open(); + } + return; + } + } else if (isLocked) { + const state = leaf.getViewState(); + if (state.state?.mode === 'source') { + leaf.setViewState({ + ...state, + state: { ...state.state, mode: 'preview' }, + }); + } + } + + if (isStrictLocked) { + const container = view.containerEl; + const allActions = container.querySelectorAll('.view-action'); + + allActions.forEach(btn => { + if (btn instanceof HTMLElement) { + const ariaLabel = btn.getAttribute('aria-label') || ''; + const hasPencilIcon = !!btn.querySelector('.lucide-pencil'); + + if (hasPencilIcon || ariaLabel.includes('Edit') || ariaLabel.includes('edit') || ariaLabel.includes('Switch to editing')) { + btn.style.display = 'none'; + } + } }); + + let lockBtn = container.querySelector('.note-locker-strict-btn'); + if (!lockBtn) { + lockBtn = document.createElement('div'); + lockBtn.addClass('view-action', 'clickable-icon', 'note-locker-strict-btn'); + lockBtn.setAttribute('aria-label', 'Strictly Locked'); + lockBtn.innerHTML = ''; + + lockBtn.addEventListener('click', () => { + new StrictUnlockModal(this.app, () => { + this.toggleStrictLock(path); + }).open(); + }); + + const actionsContainer = container.querySelector('.view-actions'); + if (actionsContainer) { + actionsContainer.prepend(lockBtn); + } + } + } else { + const container = view.containerEl; + const editBtns = container.querySelectorAll('.view-action'); + editBtns.forEach(btn => { + if (btn instanceof HTMLElement) { + if (btn.getAttribute('aria-label')?.includes('Edit') || + btn.getAttribute('aria-label')?.includes('edit') || + btn.querySelector('.lucide-pencil')) { + btn.style.display = ''; + } + } + }); + const lockBtn = container.querySelector('.note-locker-strict-btn'); + if (lockBtn) lockBtn.remove(); } } @@ -358,7 +551,8 @@ export default class NoteLockerPlugin extends Plugin { ...DEFAULT_SETTINGS, ...loaded, lockedNotes: new Set(loaded.lockedNotes || []), - lockedFolders: new Set(loaded.lockedFolders || []) + lockedFolders: new Set(loaded.lockedFolders || []), + strictLockedNotes: new Set(loaded.strictLockedNotes || []) }; } } @@ -368,6 +562,7 @@ export default class NoteLockerPlugin extends Plugin { ...this.settings, lockedNotes: Array.from(this.settings.lockedNotes), lockedFolders: Array.from(this.settings.lockedFolders), + strictLockedNotes: Array.from(this.settings.strictLockedNotes), }); } diff --git a/src/models/types.ts b/src/models/types.ts index 0c512e8..d4dc261 100644 --- a/src/models/types.ts +++ b/src/models/types.ts @@ -1,6 +1,7 @@ export interface NoteLockerSettings { lockedNotes: Set; lockedFolders: Set; + strictLockedNotes: Set; mobileNotificationMaxLength: number; desktopNotificationMaxLength: number; showFileExplorerIcons: boolean; @@ -11,6 +12,7 @@ export interface NoteLockerSettings { export const DEFAULT_SETTINGS: NoteLockerSettings = { lockedNotes: new Set(), lockedFolders: new Set(), + strictLockedNotes: new Set(), mobileNotificationMaxLength: 18, desktopNotificationMaxLength: 22, showFileExplorerIcons: true, diff --git a/src/ui/fileExplorer.ts b/src/ui/fileExplorer.ts index 86ac808..9b197e4 100644 --- a/src/ui/fileExplorer.ts +++ b/src/ui/fileExplorer.ts @@ -143,7 +143,7 @@ export class FileExplorerUI { let shouldHaveIcon = false; if (item.classList.contains('nav-file')) { - shouldHaveIcon = this.plugin.settings.lockedNotes.has(path); + shouldHaveIcon = this.plugin.settings.lockedNotes.has(path) || this.plugin.settings.strictLockedNotes.has(path); } else if (item.classList.contains('nav-folder')) { shouldHaveIcon = this.plugin.settings.lockedFolders.has(path); } @@ -166,8 +166,7 @@ export class FileExplorerUI { const filePath = titleEl.getAttribute('data-path'); if (!filePath) return; - // Only show icon if file is explicitly locked - const shouldHaveIcon = this.plugin.settings.lockedNotes.has(filePath); + const shouldHaveIcon = this.plugin.settings.lockedNotes.has(filePath) || this.plugin.settings.strictLockedNotes.has(filePath); this.updateIconState(titleEl, shouldHaveIcon); }); diff --git a/src/ui/statusBar.ts b/src/ui/statusBar.ts index 04adcb7..1097ec2 100644 --- a/src/ui/statusBar.ts +++ b/src/ui/statusBar.ts @@ -1,5 +1,6 @@ import { setIcon } from "obsidian"; import type NoteLockerPlugin from "../main"; +import { StrictUnlockModal } from "./strictUnlockModal"; export class StatusBarUI { private plugin: NoteLockerPlugin; @@ -18,7 +19,13 @@ export class StatusBarUI { this.statusBarItemEl.addEventListener('click', () => { const activeFile = this.plugin.app.workspace.getActiveFile(); if (activeFile) { - this.plugin.toggleNoteLock(activeFile.path); + if (this.plugin.settings.strictLockedNotes.has(activeFile.path)) { + new StrictUnlockModal(this.plugin.app, () => { + this.plugin.toggleStrictLock(activeFile.path); + }).open(); + } else { + this.plugin.toggleNoteLock(activeFile.path); + } } }); @@ -41,15 +48,28 @@ export class StatusBarUI { document.head.appendChild(style); if (this.plugin.app.workspace.getActiveFile()) { + const activeFile = this.plugin.app.workspace.getActiveFile(); + const path = activeFile ? activeFile.path : ''; + const isStrictLocked = this.plugin.settings.strictLockedNotes.has(path); + const isLocked = this.isCurrentNoteActive(); + const iconSpan = this.statusBarItemEl.createSpan({ cls: 'locker-icon' }); - setIcon(iconSpan, isLocked ? 'lock' : 'unlock'); - this.statusBarItemEl.createSpan({ - text: isLocked ? ' Locked' : ' Unlocked' - }); + + if (isStrictLocked) { + setIcon(iconSpan, 'lock'); + iconSpan.setText('🔓'); + this.statusBarItemEl.createSpan({ text: ' Strictly Locked' }); + this.statusBarItemEl.setAttribute('aria-label', 'Strictly locked. Click to unlock.'); + } else { + setIcon(iconSpan, isLocked ? 'lock' : 'unlock'); + this.statusBarItemEl.createSpan({ + text: isLocked ? ' Locked' : ' Unlocked' + }); + this.statusBarItemEl.setAttribute('aria-label', + isLocked ? 'Click to unlock this note' : 'Click to lock this note'); + } this.statusBarItemEl.style.cursor = 'pointer'; - this.statusBarItemEl.setAttribute('aria-label', - isLocked ? 'Click to unlock this note' : 'Click to lock this note'); } else { this.statusBarItemEl.style.cursor = 'default'; } diff --git a/src/ui/strictUnlockModal.ts b/src/ui/strictUnlockModal.ts new file mode 100644 index 0000000..84ff96a --- /dev/null +++ b/src/ui/strictUnlockModal.ts @@ -0,0 +1,50 @@ +import { Modal, App, Setting } from "obsidian"; + +export class StrictUnlockModal extends Modal { + private onUnlock: () => void; + private title: string; + private message: string; + + constructor( + app: App, + onUnlock: () => void, + title: string = "Strictly Locked Note", + message: string = "This note is strictly locked to prevent accidental editing.") { + super(app); + this.onUnlock = onUnlock; + this.title = title; + this.message = message; + } + + onOpen() { + const { contentEl } = this; + contentEl.empty(); + + contentEl.createEl("h2", { text: this.title }); + contentEl.createEl("p", { text: this.message }); + contentEl.createEl("p", { text: "Are you sure you want to unlock?" }); + + new Setting(contentEl) + .addButton((btn) => + btn + .setButtonText("Unlock") + .setCta() + .onClick(() => { + this.onUnlock(); + this.close(); + }) + ) + .addButton((btn) => + btn + .setButtonText("Cancel") + .onClick(() => { + this.close(); + }) + ); + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +}