mirror of
https://github.com/felvesthe/Note-Locker.git
synced 2026-07-22 05:44:06 +00:00
Initial implementation of folder lock feature
* There are several other improvements Signed-off-by: Felvesthe <23108389+Felvesthe@users.noreply.github.com>
This commit is contained in:
parent
4b27d16d25
commit
98875376b9
5 changed files with 226 additions and 85 deletions
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "note-locker",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "note-locker",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.1",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
|
|
|
|||
110
src/main.ts
110
src/main.ts
|
|
@ -71,7 +71,7 @@ export default class NoteLockerPlugin extends Plugin {
|
|||
);
|
||||
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("editor-menu",(menu, _, view) =>
|
||||
this.app.workspace.on("editor-menu", (menu, _, view) =>
|
||||
view.file && this.addLockMenuItem(menu, view.file.path)
|
||||
)
|
||||
);
|
||||
|
|
@ -85,12 +85,25 @@ export default class NoteLockerPlugin extends Plugin {
|
|||
|
||||
this.registerEvent(
|
||||
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);
|
||||
} else {
|
||||
new Notice(`⚠️ Lock skipped: "${file.name}" was already locked`);
|
||||
settingsChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle locked folders
|
||||
if (this.settings.lockedFolders.delete(oldPath)) {
|
||||
if (!this.settings.lockedFolders.has(file.path)) {
|
||||
this.settings.lockedFolders.add(file.path);
|
||||
settingsChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (settingsChanged) {
|
||||
await this.saveSettings();
|
||||
this.fileExplorerUI.updateFileExplorerIcons();
|
||||
}
|
||||
|
|
@ -117,17 +130,69 @@ export default class NoteLockerPlugin extends Plugin {
|
|||
.forEach((leaf) => this.updateLeafMode(leaf));
|
||||
}
|
||||
|
||||
private addLockMenuItem(menu: Menu, filePath: string) {
|
||||
const isNote = filePath.endsWith('.md');
|
||||
if (!isNote) return;
|
||||
private addLockMenuItem(menu: Menu, path: string) {
|
||||
if (this.isParentFolderLocked(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isLocked = this.settings.lockedNotes.has(filePath);
|
||||
menu.addItem((item) =>
|
||||
item
|
||||
.setTitle(isLocked ? "Unlock" : "Lock")
|
||||
.setIcon(isLocked ? "unlock" : "lock")
|
||||
.onClick(() => this.toggleNoteLock(filePath))
|
||||
);
|
||||
const file = this.app.vault.getAbstractFileByPath(path);
|
||||
|
||||
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))
|
||||
);
|
||||
} else if (file && !(file instanceof TFile)) {
|
||||
const isLocked = this.settings.lockedFolders.has(path);
|
||||
menu.addItem((item) =>
|
||||
item
|
||||
.setTitle(isLocked ? "Unlock all notes in folder" : "Lock all notes in folder")
|
||||
.setIcon(isLocked ? "unlock" : "lock")
|
||||
.onClick(() => this.toggleFolderLock(path))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private isParentFolderLocked(path: string): boolean {
|
||||
let currentPath = path;
|
||||
while (currentPath.includes("/")) {
|
||||
currentPath = currentPath.substring(0, currentPath.lastIndexOf("/"));
|
||||
if (this.settings.lockedFolders.has(currentPath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async toggleFolderLock(folderPath: string) {
|
||||
const isLocked = this.settings.lockedFolders.has(folderPath);
|
||||
|
||||
isLocked
|
||||
? this.settings.lockedFolders.delete(folderPath)
|
||||
: this.settings.lockedFolders.add(folderPath);
|
||||
|
||||
await this.saveSettings();
|
||||
|
||||
new Notice(`${isLocked ? '🔓 Unlocked' : '🔒 Locked'} folder: ${folderPath}`);
|
||||
|
||||
this.updateAllNotesInFolder(folderPath);
|
||||
this.statusBarUI.updateStatusBarButton();
|
||||
this.fileExplorerUI.updateFileExplorerIcons();
|
||||
}
|
||||
|
||||
private updateAllNotesInFolder(folderPath: string) {
|
||||
this.app.workspace
|
||||
.getLeavesOfType("markdown")
|
||||
.forEach((leaf) => {
|
||||
if (leaf.view instanceof MarkdownView && leaf.view.file) {
|
||||
if (leaf.view.file.path.startsWith(folderPath + "/")) {
|
||||
this.updateLeafMode(leaf);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async toggleNoteLock(notePath: string) {
|
||||
|
|
@ -180,12 +245,25 @@ export default class NoteLockerPlugin extends Plugin {
|
|||
return view instanceof MarkdownView && view.file?.path === notePath;
|
||||
}
|
||||
|
||||
public isPathLocked(path: string): boolean {
|
||||
if (this.settings.lockedNotes.has(path)) return true;
|
||||
|
||||
let currentPath = path;
|
||||
while (currentPath.includes("/")) {
|
||||
currentPath = currentPath.substring(0, currentPath.lastIndexOf("/"));
|
||||
if (this.settings.lockedFolders.has(currentPath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private updateLeafMode(leaf: WorkspaceLeaf | null) {
|
||||
if (!leaf || !(leaf.view instanceof MarkdownView)) return;
|
||||
|
||||
const { view } = leaf;
|
||||
const targetMode =
|
||||
view.file && this.settings.lockedNotes.has(view.file.path)
|
||||
view.file && this.isPathLocked(view.file.path)
|
||||
? "preview"
|
||||
: "source";
|
||||
|
||||
|
|
@ -203,7 +281,8 @@ export default class NoteLockerPlugin extends Plugin {
|
|||
this.settings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
...loaded,
|
||||
lockedNotes: new Set(loaded.lockedNotes || [])
|
||||
lockedNotes: new Set(loaded.lockedNotes || []),
|
||||
lockedFolders: new Set(loaded.lockedFolders || [])
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -212,6 +291,7 @@ export default class NoteLockerPlugin extends Plugin {
|
|||
await this.saveData({
|
||||
...this.settings,
|
||||
lockedNotes: Array.from(this.settings.lockedNotes),
|
||||
lockedFolders: Array.from(this.settings.lockedFolders),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
export interface NoteLockerSettings {
|
||||
lockedNotes: Set<string>;
|
||||
lockedFolders: Set<string>;
|
||||
mobileNotificationMaxLength: number;
|
||||
desktopNotificationMaxLength: number;
|
||||
showFileExplorerIcons: boolean;
|
||||
|
|
@ -9,6 +10,7 @@ export interface NoteLockerSettings {
|
|||
|
||||
export const DEFAULT_SETTINGS: NoteLockerSettings = {
|
||||
lockedNotes: new Set(),
|
||||
lockedFolders: new Set(),
|
||||
mobileNotificationMaxLength: 18,
|
||||
desktopNotificationMaxLength: 22,
|
||||
showFileExplorerIcons: true,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import {App, Platform, PluginSettingTab, Setting} from "obsidian";
|
||||
import { App, Platform, PluginSettingTab, Setting } from "obsidian";
|
||||
import type NoteLockerPlugin from "./main";
|
||||
|
||||
export class NoteLockerSettingTab extends PluginSettingTab {
|
||||
|
|
@ -80,10 +80,10 @@ export class NoteLockerSettingTab extends PluginSettingTab {
|
|||
});
|
||||
hotkeyInfo.style.fontStyle = 'italic';
|
||||
|
||||
containerEl.createEl('h3', { text: 'Locked Notes' });
|
||||
containerEl.createEl('h3', { text: 'Locked Notes & Folders' });
|
||||
const lockedNotesCount = this.plugin.settings.lockedNotes.size;
|
||||
containerEl.createEl('p', {
|
||||
text: `You currently have ${lockedNotesCount} locked note${lockedNotesCount !== 1 ? 's' : ''}.`
|
||||
text: `You currently have ${lockedNotesCount} locked note${lockedNotesCount !== 1 ? 's' : ''} and ${this.plugin.settings.lockedFolders.size} locked folder${this.plugin.settings.lockedFolders.size !== 1 ? 's' : ''}.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,21 +15,20 @@ export class FileExplorerUI {
|
|||
styleEl.id = 'note-locker-styles';
|
||||
styleEl.textContent = `
|
||||
.note-locker-icon {
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-left: 4px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
font-size: 0.85em;
|
||||
margin-left: 6px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: var(--text-accent);
|
||||
vertical-align: middle;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.nav-file-title {
|
||||
.nav-file-title, .nav-folder-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.nav-file-title-content {
|
||||
.nav-file-title-content, .nav-folder-title-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
|
|
@ -38,43 +37,12 @@ export class FileExplorerUI {
|
|||
`;
|
||||
document.head.appendChild(styleEl);
|
||||
|
||||
// Initial update
|
||||
this.plugin.app.workspace.onLayoutReady(() => {
|
||||
this.updateFileExplorerIcons();
|
||||
this.setupFolderObserver();
|
||||
|
||||
// Register handlers for folder expansion/collapse
|
||||
this.registerFolderClickHandlers();
|
||||
});
|
||||
}
|
||||
|
||||
private registerFolderClickHandlers(): void {
|
||||
const fileExplorer = document.querySelector('.workspace-split.mod-left-split .nav-files-container');
|
||||
if (!fileExplorer) return;
|
||||
|
||||
fileExplorer.addEventListener('click', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
const folderCollapseIndicator = target.closest('.nav-folder-collapse-indicator') ||
|
||||
target.closest('.nav-folder-title');
|
||||
|
||||
if (folderCollapseIndicator) {
|
||||
// A folder was clicked, so schedule an update after DOM changes
|
||||
this.debouncedUpdateIcons(250);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private debouncedUpdateIcons(delay: number = 100): void {
|
||||
if (this.updateDebounceTimeout !== null) {
|
||||
window.clearTimeout(this.updateDebounceTimeout);
|
||||
}
|
||||
|
||||
this.updateDebounceTimeout = window.setTimeout(() => {
|
||||
this.updateFileExplorerIcons();
|
||||
this.updateDebounceTimeout = null;
|
||||
}, delay);
|
||||
}
|
||||
|
||||
private setupFolderObserver(): void {
|
||||
if (this.folderObserver) {
|
||||
this.folderObserver.disconnect();
|
||||
|
|
@ -88,59 +56,150 @@ export class FileExplorerUI {
|
|||
let shouldUpdate = false;
|
||||
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0) {
|
||||
shouldUpdate = true;
|
||||
break;
|
||||
if (mutation.target instanceof HTMLElement &&
|
||||
(mutation.target.classList.contains('note-locker-icon') ||
|
||||
mutation.target.closest('.note-locker-icon'))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mutation.type === 'attributes' &&
|
||||
mutation.target instanceof HTMLElement &&
|
||||
(mutation.target.classList.contains('nav-folder') ||
|
||||
mutation.target.classList.contains('is-collapsed'))) {
|
||||
shouldUpdate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (mutation.type === 'childList') {
|
||||
const target = mutation.target as HTMLElement;
|
||||
const isTitle = target.classList.contains('nav-file-title') || target.classList.contains('nav-folder-title');
|
||||
|
||||
if (shouldUpdate) {
|
||||
this.debouncedUpdateIcons(150);
|
||||
let shouldProcess = false;
|
||||
|
||||
for (let i = 0; i < mutation.addedNodes.length; i++) {
|
||||
const node = mutation.addedNodes[i];
|
||||
if (!(node instanceof HTMLElement && node.classList.contains('note-locker-icon'))) {
|
||||
shouldProcess = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldProcess) {
|
||||
for (let i = 0; i < mutation.removedNodes.length; i++) {
|
||||
shouldProcess = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldProcess) {
|
||||
if (isTitle) {
|
||||
const container = target.parentElement;
|
||||
if (container) this.processItem(container);
|
||||
} else {
|
||||
this.handleAddedNodes(mutation.addedNodes);
|
||||
}
|
||||
}
|
||||
|
||||
} else if (mutation.type === 'attributes') {
|
||||
if (mutation.target instanceof HTMLElement) {
|
||||
if (mutation.target.classList.contains('nav-file') || mutation.target.classList.contains('nav-folder')) {
|
||||
this.processItem(mutation.target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.folderObserver.observe(fileExplorer, {
|
||||
childList: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['class'],
|
||||
attributeFilter: ['class', 'data-path', 'is-collapsed'],
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
private handleAddedNodes(nodeList: NodeList): void {
|
||||
nodeList.forEach((node) => {
|
||||
if (node instanceof HTMLElement) {
|
||||
if (node.classList.contains('nav-file') || node.classList.contains('nav-folder')) {
|
||||
this.processItem(node);
|
||||
}
|
||||
|
||||
if (node.classList.contains('nav-file-title') || node.classList.contains('nav-folder-title')) {
|
||||
const container = node.parentElement;
|
||||
if (container) this.processItem(container);
|
||||
}
|
||||
|
||||
const items = node.querySelectorAll('.nav-file, .nav-folder');
|
||||
items.forEach(item => this.processItem(item as HTMLElement));
|
||||
|
||||
const titles = node.querySelectorAll('.nav-file-title, .nav-folder-title');
|
||||
titles.forEach(title => {
|
||||
const container = title.parentElement;
|
||||
if (container) this.processItem(container);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private processItem(item: HTMLElement): void {
|
||||
const titleEl = item.querySelector('.nav-file-title, .nav-folder-title');
|
||||
if (!titleEl) return;
|
||||
|
||||
const path = titleEl.getAttribute('data-path');
|
||||
if (!path) return;
|
||||
|
||||
let shouldHaveIcon = false;
|
||||
if (item.classList.contains('nav-file')) {
|
||||
shouldHaveIcon = this.plugin.settings.lockedNotes.has(path);
|
||||
} else if (item.classList.contains('nav-folder')) {
|
||||
shouldHaveIcon = this.plugin.settings.lockedFolders.has(path);
|
||||
}
|
||||
|
||||
this.updateIconState(titleEl, shouldHaveIcon);
|
||||
}
|
||||
|
||||
public updateFileExplorerIcons(): void {
|
||||
if (!this.plugin.settings.showFileExplorerIcons) {
|
||||
this.removeFileExplorerIcons();
|
||||
return;
|
||||
}
|
||||
|
||||
// First, remove existing icons to avoid duplicates
|
||||
this.removeFileExplorerIcons();
|
||||
|
||||
// Get all file elements in the file explorer
|
||||
// Handle files
|
||||
const fileItems = document.querySelectorAll('.nav-file');
|
||||
|
||||
fileItems.forEach((fileItem) => {
|
||||
const titleEl = fileItem.querySelector('.nav-file-title');
|
||||
if (!titleEl) return;
|
||||
|
||||
const filePath = titleEl.getAttribute('data-path');
|
||||
if (!filePath || !this.plugin.settings.lockedNotes.has(filePath)) return;
|
||||
if (!filePath) return;
|
||||
|
||||
// Create lock icon element
|
||||
const iconEl = document.createElement('div');
|
||||
iconEl.addClass('note-locker-icon');
|
||||
setIcon(iconEl, 'lock');
|
||||
|
||||
titleEl.appendChild(iconEl);
|
||||
// Only show icon if file is explicitly locked
|
||||
const shouldHaveIcon = this.plugin.settings.lockedNotes.has(filePath);
|
||||
this.updateIconState(titleEl, shouldHaveIcon);
|
||||
});
|
||||
|
||||
// Handle folders
|
||||
const folderItems = document.querySelectorAll('.nav-folder');
|
||||
folderItems.forEach((folderItem) => {
|
||||
const titleEl = folderItem.querySelector('.nav-folder-title');
|
||||
if (!titleEl) return;
|
||||
|
||||
const folderPath = titleEl.getAttribute('data-path');
|
||||
if (!folderPath) return;
|
||||
|
||||
const shouldHaveIcon = this.plugin.settings.lockedFolders.has(folderPath);
|
||||
this.updateIconState(titleEl, shouldHaveIcon);
|
||||
});
|
||||
}
|
||||
|
||||
private updateIconState(targetEl: Element, shouldHaveIcon: boolean): void {
|
||||
const existingIcon = targetEl.querySelector('.note-locker-icon');
|
||||
|
||||
if (shouldHaveIcon) {
|
||||
if (!existingIcon) {
|
||||
const iconEl = document.createElement('div');
|
||||
iconEl.addClass('note-locker-icon');
|
||||
setIcon(iconEl, 'lock');
|
||||
targetEl.appendChild(iconEl);
|
||||
}
|
||||
} else {
|
||||
if (existingIcon) {
|
||||
existingIcon.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public removeFileExplorerIcons(): void {
|
||||
|
|
|
|||
Loading…
Reference in a new issue