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();
+ }
+}