mirror of
https://github.com/felvesthe/Note-Locker.git
synced 2026-07-22 12:30:24 +00:00
Compare commits
22 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a2fbbbb9d | ||
|
|
1bbbb63f90 | ||
|
|
7d377dc99d | ||
|
|
cd4ecd7821 | ||
|
|
cf02579f86 | ||
|
|
f990abb7ef | ||
|
|
07efeb3553 | ||
|
|
2c961c9888 | ||
|
|
3f1653f967 | ||
|
|
eec6cb8de2 | ||
|
|
98875376b9 | ||
|
|
4b27d16d25 | ||
|
|
e2a64e0534 | ||
|
|
8933f6ce64 | ||
|
|
db97f5f629 | ||
|
|
9673533aa9 | ||
|
|
33075ecd13 | ||
|
|
188bb6120f | ||
|
|
a4398b8ecc | ||
|
|
ce4fc8c209 | ||
|
|
f16c1401b3 | ||
|
|
3fe07677b5 |
12 changed files with 925 additions and 115 deletions
251
AGENTS.md
Normal file
251
AGENTS.md
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
# Obsidian community plugin
|
||||
|
||||
## Project overview
|
||||
|
||||
- Target: Obsidian Community Plugin (TypeScript → bundled JavaScript).
|
||||
- Entry point: `main.ts` compiled to `main.js` and loaded by Obsidian.
|
||||
- Required release artifacts: `main.js`, `manifest.json`, and optional `styles.css`.
|
||||
|
||||
## Environment & tooling
|
||||
|
||||
- Node.js: use current LTS (Node 18+ recommended).
|
||||
- **Package manager: npm** (required for this sample - `package.json` defines npm scripts and dependencies).
|
||||
- **Bundler: esbuild** (required for this sample - `esbuild.config.mjs` and build scripts depend on it). Alternative bundlers like Rollup or webpack are acceptable for other projects if they bundle all external dependencies into `main.js`.
|
||||
- Types: `obsidian` type definitions.
|
||||
|
||||
**Note**: This sample project has specific technical dependencies on npm and esbuild. If you're creating a plugin from scratch, you can choose different tools, but you'll need to replace the build configuration accordingly.
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### Dev (watch)
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Production build
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Linting
|
||||
|
||||
- To use eslint install eslint from terminal: `npm install -g eslint`
|
||||
- To use eslint to analyze this project use this command: `eslint main.ts`
|
||||
- eslint will then create a report with suggestions for code improvement by file and line number.
|
||||
- If your source code is in a folder, such as `src`, you can use eslint with this command to analyze all files in that folder: `eslint ./src/`
|
||||
|
||||
## File & folder conventions
|
||||
|
||||
- **Organize code into multiple files**: Split functionality across separate modules rather than putting everything in `main.ts`.
|
||||
- Source lives in `src/`. Keep `main.ts` small and focused on plugin lifecycle (loading, unloading, registering commands).
|
||||
- **Example file structure**:
|
||||
```
|
||||
src/
|
||||
main.ts # Plugin entry point, lifecycle management
|
||||
settings.ts # Settings interface and defaults
|
||||
commands/ # Command implementations
|
||||
command1.ts
|
||||
command2.ts
|
||||
ui/ # UI components, modals, views
|
||||
modal.ts
|
||||
view.ts
|
||||
utils/ # Utility functions, helpers
|
||||
helpers.ts
|
||||
constants.ts
|
||||
types.ts # TypeScript interfaces and types
|
||||
```
|
||||
- **Do not commit build artifacts**: Never commit `node_modules/`, `main.js`, or other generated files to version control.
|
||||
- Keep the plugin small. Avoid large dependencies. Prefer browser-compatible packages.
|
||||
- Generated output should be placed at the plugin root or `dist/` depending on your build setup. Release artifacts must end up at the top level of the plugin folder in the vault (`main.js`, `manifest.json`, `styles.css`).
|
||||
|
||||
## Manifest rules (`manifest.json`)
|
||||
|
||||
- Must include (non-exhaustive):
|
||||
- `id` (plugin ID; for local dev it should match the folder name)
|
||||
- `name`
|
||||
- `version` (Semantic Versioning `x.y.z`)
|
||||
- `minAppVersion`
|
||||
- `description`
|
||||
- `isDesktopOnly` (boolean)
|
||||
- Optional: `author`, `authorUrl`, `fundingUrl` (string or map)
|
||||
- Never change `id` after release. Treat it as stable API.
|
||||
- Keep `minAppVersion` accurate when using newer APIs.
|
||||
- Canonical requirements are coded here: https://github.com/obsidianmd/obsidian-releases/blob/master/.github/workflows/validate-plugin-entry.yml
|
||||
|
||||
## Testing
|
||||
|
||||
- Manual install for testing: copy `main.js`, `manifest.json`, `styles.css` (if any) to:
|
||||
```
|
||||
<Vault>/.obsidian/plugins/<plugin-id>/
|
||||
```
|
||||
- Reload Obsidian and enable the plugin in **Settings → Community plugins**.
|
||||
|
||||
## Commands & settings
|
||||
|
||||
- Any user-facing commands should be added via `this.addCommand(...)`.
|
||||
- If the plugin has configuration, provide a settings tab and sensible defaults.
|
||||
- Persist settings using `this.loadData()` / `this.saveData()`.
|
||||
- Use stable command IDs; avoid renaming once released.
|
||||
|
||||
## Versioning & releases
|
||||
|
||||
- Bump `version` in `manifest.json` (SemVer) and update `versions.json` to map plugin version → minimum app version.
|
||||
- Create a GitHub release whose tag exactly matches `manifest.json`'s `version`. Do not use a leading `v`.
|
||||
- Attach `manifest.json`, `main.js`, and `styles.css` (if present) to the release as individual assets.
|
||||
- After the initial release, follow the process to add/update your plugin in the community catalog as required.
|
||||
|
||||
## Security, privacy, and compliance
|
||||
|
||||
Follow Obsidian's **Developer Policies** and **Plugin Guidelines**. In particular:
|
||||
|
||||
- Default to local/offline operation. Only make network requests when essential to the feature.
|
||||
- No hidden telemetry. If you collect optional analytics or call third-party services, require explicit opt-in and document clearly in `README.md` and in settings.
|
||||
- Never execute remote code, fetch and eval scripts, or auto-update plugin code outside of normal releases.
|
||||
- Minimize scope: read/write only what's necessary inside the vault. Do not access files outside the vault.
|
||||
- Clearly disclose any external services used, data sent, and risks.
|
||||
- Respect user privacy. Do not collect vault contents, filenames, or personal information unless absolutely necessary and explicitly consented.
|
||||
- Avoid deceptive patterns, ads, or spammy notifications.
|
||||
- Register and clean up all DOM, app, and interval listeners using the provided `register*` helpers so the plugin unloads safely.
|
||||
|
||||
## UX & copy guidelines (for UI text, commands, settings)
|
||||
|
||||
- Prefer sentence case for headings, buttons, and titles.
|
||||
- Use clear, action-oriented imperatives in step-by-step copy.
|
||||
- Use **bold** to indicate literal UI labels. Prefer "select" for interactions.
|
||||
- Use arrow notation for navigation: **Settings → Community plugins**.
|
||||
- Keep in-app strings short, consistent, and free of jargon.
|
||||
|
||||
## Performance
|
||||
|
||||
- Keep startup light. Defer heavy work until needed.
|
||||
- Avoid long-running tasks during `onload`; use lazy initialization.
|
||||
- Batch disk access and avoid excessive vault scans.
|
||||
- Debounce/throttle expensive operations in response to file system events.
|
||||
|
||||
## Coding conventions
|
||||
|
||||
- TypeScript with `"strict": true` preferred.
|
||||
- **Keep `main.ts` minimal**: Focus only on plugin lifecycle (onload, onunload, addCommand calls). Delegate all feature logic to separate modules.
|
||||
- **Split large files**: If any file exceeds ~200-300 lines, consider breaking it into smaller, focused modules.
|
||||
- **Use clear module boundaries**: Each file should have a single, well-defined responsibility.
|
||||
- Bundle everything into `main.js` (no unbundled runtime deps).
|
||||
- Avoid Node/Electron APIs if you want mobile compatibility; set `isDesktopOnly` accordingly.
|
||||
- Prefer `async/await` over promise chains; handle errors gracefully.
|
||||
|
||||
## Mobile
|
||||
|
||||
- Where feasible, test on iOS and Android.
|
||||
- Don't assume desktop-only behavior unless `isDesktopOnly` is `true`.
|
||||
- Avoid large in-memory structures; be mindful of memory and storage constraints.
|
||||
|
||||
## Agent do/don't
|
||||
|
||||
**Do**
|
||||
- Add commands with stable IDs (don't rename once released).
|
||||
- Provide defaults and validation in settings.
|
||||
- Write idempotent code paths so reload/unload doesn't leak listeners or intervals.
|
||||
- Use `this.register*` helpers for everything that needs cleanup.
|
||||
|
||||
**Don't**
|
||||
- Introduce network calls without an obvious user-facing reason and documentation.
|
||||
- Ship features that require cloud services without clear disclosure and explicit opt-in.
|
||||
- Store or transmit vault contents unless essential and consented.
|
||||
|
||||
## Common tasks
|
||||
|
||||
### Organize code across multiple files
|
||||
|
||||
**main.ts** (minimal, lifecycle only):
|
||||
```ts
|
||||
import { Plugin } from "obsidian";
|
||||
import { MySettings, DEFAULT_SETTINGS } from "./settings";
|
||||
import { registerCommands } from "./commands";
|
||||
|
||||
export default class MyPlugin extends Plugin {
|
||||
settings: MySettings;
|
||||
|
||||
async onload() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
registerCommands(this);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**settings.ts**:
|
||||
```ts
|
||||
export interface MySettings {
|
||||
enabled: boolean;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: MySettings = {
|
||||
enabled: true,
|
||||
apiKey: "",
|
||||
};
|
||||
```
|
||||
|
||||
**commands/index.ts**:
|
||||
```ts
|
||||
import { Plugin } from "obsidian";
|
||||
import { doSomething } from "./my-command";
|
||||
|
||||
export function registerCommands(plugin: Plugin) {
|
||||
plugin.addCommand({
|
||||
id: "do-something",
|
||||
name: "Do something",
|
||||
callback: () => doSomething(plugin),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Add a command
|
||||
|
||||
```ts
|
||||
this.addCommand({
|
||||
id: "your-command-id",
|
||||
name: "Do the thing",
|
||||
callback: () => this.doTheThing(),
|
||||
});
|
||||
```
|
||||
|
||||
### Persist settings
|
||||
|
||||
```ts
|
||||
interface MySettings { enabled: boolean }
|
||||
const DEFAULT_SETTINGS: MySettings = { enabled: true };
|
||||
|
||||
async onload() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
```
|
||||
|
||||
### Register listeners safely
|
||||
|
||||
```ts
|
||||
this.registerEvent(this.app.workspace.on("file-open", f => { /* ... */ }));
|
||||
this.registerDomEvent(window, "resize", () => { /* ... */ });
|
||||
this.registerInterval(window.setInterval(() => { /* ... */ }, 1000));
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Plugin doesn't load after build: ensure `main.js` and `manifest.json` are at the top level of the plugin folder under `<Vault>/.obsidian/plugins/<plugin-id>/`.
|
||||
- Build issues: if `main.js` is missing, run `npm run build` or `npm run dev` to compile your TypeScript source code.
|
||||
- Commands not appearing: verify `addCommand` runs after `onload` and IDs are unique.
|
||||
- Settings not persisting: ensure `loadData`/`saveData` are awaited and you re-render the UI after changes.
|
||||
- Mobile-only issues: confirm you're not using desktop-only APIs; check `isDesktopOnly` and adjust.
|
||||
|
||||
## References
|
||||
|
||||
- Obsidian sample plugin: https://github.com/obsidianmd/obsidian-sample-plugin
|
||||
- API documentation: https://docs.obsidian.md
|
||||
- Developer policies: https://docs.obsidian.md/Developer+policies
|
||||
- Plugin guidelines: https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines
|
||||
- Style guide: https://help.obsidian.md/style-guide
|
||||
|
|
@ -50,4 +50,4 @@ The lock will automatically transfer to the new filename.
|
|||
If locks aren't working properly:
|
||||
1. Check for conflicts with other plugins
|
||||
2. Ensure you're running the latest version of Obsidian
|
||||
3. Try reinstalling the plugin
|
||||
3. Try reinstalling the plugin
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"id": "note-locker",
|
||||
"name": "Note Locker",
|
||||
"version": "1.2.1",
|
||||
"version": "1.3.1",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Lock notes to open in preview mode by default.",
|
||||
"author": "Felvesthe",
|
||||
"authorUrl": "https://github.com/Felvesthe",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
}
|
||||
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",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "note-locker",
|
||||
"version": "1.2.1",
|
||||
"version": "1.3.0",
|
||||
"description": "Lock notes to open in preview mode by default.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
@ -21,4 +21,4 @@
|
|||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
434
src/main.ts
434
src/main.ts
|
|
@ -6,12 +6,16 @@ import {
|
|||
Plugin,
|
||||
TFile,
|
||||
WorkspaceLeaf,
|
||||
TAbstractFile,
|
||||
TFolder,
|
||||
setIcon,
|
||||
} from "obsidian";
|
||||
|
||||
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;
|
||||
|
|
@ -41,18 +45,44 @@ export default class NoteLockerPlugin extends Plugin {
|
|||
// hotkey
|
||||
this.addCommand({
|
||||
id: 'toggle-note-lock',
|
||||
name: 'Toggle Lock for current note',
|
||||
name: 'Toggle lock for current note',
|
||||
checkCallback: (checking: boolean) => {
|
||||
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() {
|
||||
|
|
@ -71,26 +101,43 @@ export default class NoteLockerPlugin extends Plugin {
|
|||
);
|
||||
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("editor-menu",(menu, _, view) =>
|
||||
this.app.workspace.on("files-menu", (menu, files) =>
|
||||
this.addBulkLockMenuItem(menu, files)
|
||||
)
|
||||
);
|
||||
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("editor-menu", (menu, _, view) =>
|
||||
view.file && this.addLockMenuItem(menu, view.file.path)
|
||||
)
|
||||
);
|
||||
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("active-leaf-change", (leaf) => {
|
||||
this.updateLeafMode(leaf);
|
||||
this.updateLeafMode(leaf, true);
|
||||
this.statusBarUI.updateStatusBarButton();
|
||||
})
|
||||
);
|
||||
|
||||
this.registerEvent(
|
||||
this.app.vault.on("rename", async (file, oldPath) => {
|
||||
let settingsChanged = false;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
|
@ -101,12 +148,14 @@ export default class NoteLockerPlugin extends Plugin {
|
|||
this.app.workspace.on("file-open", () => {
|
||||
this.statusBarUI.updateStatusBarButton();
|
||||
this.fileExplorerUI.updateFileExplorerIcons();
|
||||
this.updateLeafMode(this.app.workspace.getLeaf(false), true);
|
||||
})
|
||||
);
|
||||
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("layout-change", () => {
|
||||
this.fileExplorerUI.updateFileExplorerIcons();
|
||||
this.app.workspace.iterateAllLeaves((leaf) => this.updateLeafMode(leaf));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
@ -114,20 +163,220 @@ export default class NoteLockerPlugin extends Plugin {
|
|||
private initializeExistingLeaves() {
|
||||
this.app.workspace
|
||||
.getLeavesOfType("markdown")
|
||||
.forEach((leaf) => this.updateLeafMode(leaf));
|
||||
.forEach((leaf) => this.updateLeafMode(leaf, true));
|
||||
}
|
||||
|
||||
private addLockMenuItem(menu: Menu, filePath: string) {
|
||||
const isNote = filePath.endsWith('.md');
|
||||
if (!isNote) return;
|
||||
|
||||
const isLocked = this.settings.lockedNotes.has(filePath);
|
||||
menu.addItem((item) =>
|
||||
item
|
||||
.setTitle(isLocked ? "Unlock" : "Lock")
|
||||
.setIcon(isLocked ? "unlock" : "lock")
|
||||
.onClick(() => this.toggleNoteLock(filePath))
|
||||
private addBulkLockMenuItem(menu: Menu, files: TAbstractFile[]) {
|
||||
const validItems = files.filter(f =>
|
||||
(f instanceof TFile && f.extension === 'md') ||
|
||||
(f instanceof TFolder)
|
||||
);
|
||||
|
||||
if (validItems.length < 2) return;
|
||||
|
||||
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) {
|
||||
menu.addItem((item) =>
|
||||
item
|
||||
.setTitle(`Unlock ${validItems.length} items`)
|
||||
.setIcon("unlock")
|
||||
.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) =>
|
||||
item
|
||||
.setTitle(`Lock ${validItems.length} items`)
|
||||
.setIcon("lock")
|
||||
.onClick(() => this.toggleBulkLock(validItems, true))
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
for (const file of files) {
|
||||
if (file instanceof TFile) {
|
||||
if (shouldLock) {
|
||||
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) {
|
||||
this.settings.lockedFolders.add(file.path);
|
||||
} else {
|
||||
this.settings.lockedFolders.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 || (f instanceof TFolder && path.startsWith(f.path + '/')))) {
|
||||
this.updateLeafMode(leaf);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const file = this.app.vault.getAbstractFileByPath(path);
|
||||
|
||||
if (file instanceof TFile && file.extension === 'md') {
|
||||
const isLocked = this.settings.lockedNotes.has(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) =>
|
||||
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) {
|
||||
|
|
@ -159,6 +408,35 @@ export default class NoteLockerPlugin extends Plugin {
|
|||
this.fileExplorerUI.updateFileExplorerIcons();
|
||||
}
|
||||
|
||||
async toggleStrictLock(notePath: string) {
|
||||
const isStrictLocked = this.settings.strictLockedNotes.has(notePath);
|
||||
|
||||
isStrictLocked
|
||||
? this.settings.strictLockedNotes.delete(notePath)
|
||||
: this.settings.strictLockedNotes.add(notePath);
|
||||
|
||||
await this.saveSettings();
|
||||
|
||||
const file = this.app.vault.getAbstractFileByPath(notePath);
|
||||
|
||||
if (!file) {
|
||||
new Notice("Error: Note not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.settings.showNotifications) {
|
||||
const fileName = file instanceof TFile ? file.basename :
|
||||
notePath.split('/').pop()?.replace(/\..+$/, '') || notePath;
|
||||
const displayName = this.truncateFileName(fileName);
|
||||
|
||||
new Notice(`${isStrictLocked ? '🔓 Strictly unlocked' : '🔒 Strictly locked'}: ${displayName}`);
|
||||
}
|
||||
|
||||
this.updateAllNoteInstances(notePath);
|
||||
this.statusBarUI.updateStatusBarButton();
|
||||
this.fileExplorerUI.updateFileExplorerIcons();
|
||||
}
|
||||
|
||||
private truncateFileName(name: string): string {
|
||||
const maxLength = Platform.isMobile
|
||||
? this.settings.mobileNotificationMaxLength
|
||||
|
|
@ -180,21 +458,117 @@ export default class NoteLockerPlugin extends Plugin {
|
|||
return view instanceof MarkdownView && view.file?.path === notePath;
|
||||
}
|
||||
|
||||
private updateLeafMode(leaf: WorkspaceLeaf | null) {
|
||||
public isPathLocked(path: string): boolean {
|
||||
if (this.settings.strictLockedNotes.has(path)) return true;
|
||||
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;
|
||||
}
|
||||
|
||||
public updateLeafMode(leaf: WorkspaceLeaf | null, fromNavigation: boolean = false) {
|
||||
if (!leaf || !(leaf.view instanceof MarkdownView)) return;
|
||||
|
||||
const { view } = leaf;
|
||||
const targetMode =
|
||||
view.file && this.settings.lockedNotes.has(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' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else if (isLocked) {
|
||||
const state = leaf.getViewState();
|
||||
if ((fromNavigation || this.settings.preventEditInLockedNotes) && state.state?.mode === 'source') {
|
||||
leaf.setViewState({
|
||||
...state,
|
||||
state: { ...state.state, mode: 'preview' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isStrictLocked) {
|
||||
this.hideEditButtons(view);
|
||||
this.addStrictLockButton(view, path);
|
||||
} else if (isLocked && this.settings.preventEditInLockedNotes) {
|
||||
this.hideEditButtons(view);
|
||||
this.removeStrictLockButton(view);
|
||||
} else {
|
||||
this.showEditButtons(view);
|
||||
this.removeStrictLockButton(view);
|
||||
}
|
||||
}
|
||||
|
||||
private hideEditButtons(view: MarkdownView) {
|
||||
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';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private showEditButtons(view: MarkdownView) {
|
||||
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 = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private addStrictLockButton(view: MarkdownView, path: string) {
|
||||
const container = view.containerEl;
|
||||
let lockBtn = container.querySelector('.note-locker-strict-btn') as HTMLElement;
|
||||
if (!lockBtn) {
|
||||
lockBtn = document.createElement('div');
|
||||
lockBtn.addClass('view-action', 'clickable-icon', 'note-locker-strict-btn');
|
||||
lockBtn.setAttribute('aria-label', 'Strictly locked');
|
||||
setIcon(lockBtn, 'lock-keyhole');
|
||||
|
||||
lockBtn.addEventListener('click', () => {
|
||||
new StrictUnlockModal(this.app, () => {
|
||||
this.toggleStrictLock(path);
|
||||
}).open();
|
||||
});
|
||||
|
||||
const actionsContainer = container.querySelector('.view-actions');
|
||||
if (actionsContainer) {
|
||||
actionsContainer.prepend(lockBtn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private removeStrictLockButton(view: MarkdownView) {
|
||||
const container = view.containerEl;
|
||||
const lockBtn = container.querySelector('.note-locker-strict-btn');
|
||||
if (lockBtn) lockBtn.remove();
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
|
|
@ -203,7 +577,9 @@ 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 || []),
|
||||
strictLockedNotes: new Set(loaded.strictLockedNotes || [])
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -212,6 +588,8 @@ export default class NoteLockerPlugin extends Plugin {
|
|||
await this.saveData({
|
||||
...this.settings,
|
||||
lockedNotes: Array.from(this.settings.lockedNotes),
|
||||
lockedFolders: Array.from(this.settings.lockedFolders),
|
||||
strictLockedNotes: Array.from(this.settings.strictLockedNotes),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,23 @@
|
|||
export interface NoteLockerSettings {
|
||||
lockedNotes: Set<string>;
|
||||
lockedFolders: Set<string>;
|
||||
strictLockedNotes: Set<string>;
|
||||
mobileNotificationMaxLength: number;
|
||||
desktopNotificationMaxLength: number;
|
||||
showFileExplorerIcons: boolean;
|
||||
showStatusBarButton: boolean;
|
||||
showNotifications: boolean;
|
||||
preventEditInLockedNotes: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: NoteLockerSettings = {
|
||||
lockedNotes: new Set(),
|
||||
lockedFolders: new Set(),
|
||||
strictLockedNotes: new Set(),
|
||||
mobileNotificationMaxLength: 18,
|
||||
desktopNotificationMaxLength: 22,
|
||||
showFileExplorerIcons: true,
|
||||
showStatusBarButton: true,
|
||||
showNotifications: true
|
||||
showNotifications: true,
|
||||
preventEditInLockedNotes: false
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import {App, Platform, PluginSettingTab, Setting} from "obsidian";
|
||||
import { App, Platform, PluginSettingTab, Setting, MarkdownView } from "obsidian";
|
||||
import type NoteLockerPlugin from "./main";
|
||||
|
||||
export class NoteLockerSettingTab extends PluginSettingTab {
|
||||
|
|
@ -41,6 +41,21 @@ export class NoteLockerSettingTab extends PluginSettingTab {
|
|||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Prevent editing in locked notes')
|
||||
.setDesc('If enabled, normally locked notes will be read-only. If disabled, you can switch to edit mode.')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.preventEditInLockedNotes)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.preventEditInLockedNotes = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.app.workspace.iterateAllLeaves((leaf) => {
|
||||
if (leaf.view instanceof MarkdownView && leaf.view.file) {
|
||||
this.plugin.updateLeafMode(leaf);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
const mobileNotificationSetting = new Setting(containerEl)
|
||||
.setName('Mobile notification max length')
|
||||
.setDesc('Maximum length of file names in notifications on mobile devices')
|
||||
|
|
@ -80,10 +95,24 @@ export class NoteLockerSettingTab extends PluginSettingTab {
|
|||
});
|
||||
hotkeyInfo.style.fontStyle = 'italic';
|
||||
|
||||
containerEl.createEl('h3', { text: 'Locked Notes' });
|
||||
containerEl.createEl('h3', { text: 'Locked notes & folders statistics' });
|
||||
const lockedNotesCount = this.plugin.settings.lockedNotes.size;
|
||||
const lockedFoldersCount = this.plugin.settings.lockedFolders.size;
|
||||
const strictLockedNotesCount = this.plugin.settings.strictLockedNotes.size;
|
||||
|
||||
containerEl.createEl('p', {
|
||||
text: `You currently have ${lockedNotesCount} locked note${lockedNotesCount !== 1 ? 's' : ''}.`
|
||||
text: 'You currently have:'
|
||||
});
|
||||
containerEl
|
||||
.createEl('ul')
|
||||
.createEl('li', {
|
||||
text: `Locked notes: ${lockedNotesCount}`
|
||||
})
|
||||
.createEl('li', {
|
||||
text: `Locked folders: ${lockedFoldersCount}`
|
||||
})
|
||||
.createEl('li', {
|
||||
text: `Strictly locked notes: ${strictLockedNotesCount}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,165 @@ 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 iconName: string | null = null;
|
||||
if (item.classList.contains('nav-file')) {
|
||||
if (this.plugin.settings.strictLockedNotes.has(path)) {
|
||||
iconName = 'lock-keyhole';
|
||||
} else if (this.plugin.settings.lockedNotes.has(path)) {
|
||||
iconName = 'lock';
|
||||
}
|
||||
} else if (item.classList.contains('nav-folder')) {
|
||||
if (this.plugin.settings.lockedFolders.has(path)) {
|
||||
iconName = 'lock';
|
||||
}
|
||||
}
|
||||
|
||||
this.updateIconState(titleEl, iconName);
|
||||
}
|
||||
|
||||
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);
|
||||
let iconName: string | null = null;
|
||||
if (this.plugin.settings.strictLockedNotes.has(filePath)) {
|
||||
iconName = 'lock-keyhole';
|
||||
} else if (this.plugin.settings.lockedNotes.has(filePath)) {
|
||||
iconName = 'lock';
|
||||
}
|
||||
this.updateIconState(titleEl, iconName);
|
||||
});
|
||||
|
||||
// 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;
|
||||
|
||||
let iconName: string | null = null;
|
||||
if (this.plugin.settings.lockedFolders.has(folderPath)) {
|
||||
iconName = 'lock';
|
||||
}
|
||||
this.updateIconState(titleEl, iconName);
|
||||
});
|
||||
}
|
||||
|
||||
private updateIconState(targetEl: Element, iconName: string | null): void {
|
||||
const existingIcon = targetEl.querySelector('.note-locker-icon');
|
||||
|
||||
if (iconName) {
|
||||
if (!existingIcon) {
|
||||
const iconEl = document.createElement('div');
|
||||
iconEl.addClass('note-locker-icon');
|
||||
setIcon(iconEl, iconName);
|
||||
targetEl.appendChild(iconEl);
|
||||
} else {
|
||||
setIcon(existingIcon as HTMLElement, iconName);
|
||||
}
|
||||
} else {
|
||||
if (existingIcon) {
|
||||
existingIcon.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public removeFileExplorerIcons(): void {
|
||||
|
|
|
|||
|
|
@ -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,27 @@ 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-keyhole');
|
||||
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';
|
||||
}
|
||||
|
|
|
|||
50
src/ui/strictUnlockModal.ts
Normal file
50
src/ui/strictUnlockModal.ts
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -3,12 +3,15 @@ import { readFileSync, writeFileSync } from "fs";
|
|||
const targetVersion = process.env.npm_package_version;
|
||||
|
||||
// read minAppVersion from manifest.json and bump version to target version
|
||||
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
|
||||
const manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
|
||||
const { minAppVersion } = manifest;
|
||||
manifest.version = targetVersion;
|
||||
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
|
||||
|
||||
// update versions.json with target version and minAppVersion from manifest.json
|
||||
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
|
||||
versions[targetVersion] = minAppVersion;
|
||||
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
|
||||
// but only if the target version is not already in versions.json
|
||||
const versions = JSON.parse(readFileSync('versions.json', 'utf8'));
|
||||
if (!Object.values(versions).includes(minAppVersion)) {
|
||||
versions[targetVersion] = minAppVersion;
|
||||
writeFileSync('versions.json', JSON.stringify(versions, null, '\t'));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue