Compare commits

..

22 commits

Author SHA1 Message Date
Felvesthe
4a2fbbbb9d fix: Reset edit mode when navigating to another locked note
Signed-off-by: Felvesthe <23108389+Felvesthe@users.noreply.github.com>
2025-12-05 13:10:01 +01:00
Felvesthe
1bbbb63f90 Use different icon for strictly locked notes
Signed-off-by: Felvesthe <23108389+Felvesthe@users.noreply.github.com>
2025-12-05 12:40:04 +01:00
Felvesthe
7d377dc99d Add file existence check & fix an issue that allowed notifications to show even if disabled for strict lock
Signed-off-by: Felvesthe <23108389+Felvesthe@users.noreply.github.com>
2025-12-02 00:12:34 +01:00
Felvesthe
cd4ecd7821 Change Title Case to Sentence case to match Obsidian's plugin guidelines
Signed-off-by: Felvesthe <23108389+Felvesthe@users.noreply.github.com>
2025-12-02 00:02:48 +01:00
Felvesthe
cf02579f86 Bump version to 1.3.1
Signed-off-by: Felvesthe <23108389+Felvesthe@users.noreply.github.com>
2025-11-30 18:31:11 +01:00
Felvesthe
f990abb7ef Add option to prevent editing in normally locked notes
* Add "Prevent editing in locked notes" setting (default: off)
* Hide edit buttons when prevention is enabled
* Force preview mode when prevention is enabled for locked notes
* Normally locked notes are editable when prevention is disabled

Signed-off-by: Felvesthe <23108389+Felvesthe@users.noreply.github.com>
2025-11-30 18:30:37 +01:00
Felvesthe
07efeb3553 Bump version to 1.3.0
Signed-off-by: Felvesthe <23108389+Felvesthe@users.noreply.github.com>
2025-11-23 13:47:18 +01:00
Felvesthe
2c961c9888 settings: Change statistics view
Signed-off-by: Felvesthe <23108389+Felvesthe@users.noreply.github.com>
2025-11-23 13:45:42 +01:00
Felvesthe
3f1653f967 Initial implementation of strict lock feature
* https://github.com/Felvesthe/Note-Locker/issues/6

Signed-off-by: Felvesthe <23108389+Felvesthe@users.noreply.github.com>
2025-11-23 13:42:13 +01:00
Felvesthe
eec6cb8de2 Implement possibility to lock/unlock multiple notes at once
Signed-off-by: Felvesthe <23108389+Felvesthe@users.noreply.github.com>
2025-11-23 00:40:10 +01:00
Felvesthe
98875376b9 Initial implementation of folder lock feature
* There are several other improvements

Signed-off-by: Felvesthe <23108389+Felvesthe@users.noreply.github.com>
2025-11-23 00:08:25 +01:00
Felvesthe
4b27d16d25 Merge remote-tracking branch 'obsidian/master'
Signed-off-by: Felvesthe <23108389+Felvesthe@users.noreply.github.com>
2025-11-22 23:23:26 +01:00
Johannes Theiner
e2a64e0534
Merge pull request #151 from adamu/main
Prefix unused variables with _
2025-09-17 18:12:44 +02:00
Johannes Theiner
8933f6ce64
Merge pull request #142 from johannrichard/declutter-version-bump
build: only write new minAppVersion requirements to `versions.json`
2025-09-17 17:54:33 +02:00
Steph Ango
db97f5f629
Merge pull request #155 from obsidianmd/agents-md
AGENTS.md
2025-09-05 09:12:10 -07:00
Steph Ango
9673533aa9 language 2025-09-05 09:10:47 -07:00
Steph Ango
33075ecd13 use forward slashes for cross OS compatibility 2025-09-05 09:04:04 -07:00
Steph Ango
188bb6120f small copy tweaks 2025-09-05 09:01:58 -07:00
Steph Ango
a4398b8ecc Corrections based on feedback 2025-09-05 08:58:49 -07:00
Steph Ango
ce4fc8c209 First pass 2025-09-04 15:28:25 -07:00
Adam Millerchip
f16c1401b3 Prefix unused variables with _ 2025-08-28 14:38:40 +09:00
Johann Richard
3fe07677b5
build: only write new minAppVersion requirements to versions.json
Only add a new version requirements if `minAppVersion` is not already in `versions.json`. Should declutter `versions.json`.
2025-04-28 08:38:38 +02:00
12 changed files with 925 additions and 115 deletions

251
AGENTS.md Normal file
View 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

View file

@ -50,4 +50,4 @@ The lock will automatically transfer to the new filename.
If locks aren't working properly: If locks aren't working properly:
1. Check for conflicts with other plugins 1. Check for conflicts with other plugins
2. Ensure you're running the latest version of Obsidian 2. Ensure you're running the latest version of Obsidian
3. Try reinstalling the plugin 3. Try reinstalling the plugin

View file

@ -1,10 +1,10 @@
{ {
"id": "note-locker", "id": "note-locker",
"name": "Note Locker", "name": "Note Locker",
"version": "1.2.1", "version": "1.3.1",
"minAppVersion": "0.15.0", "minAppVersion": "0.15.0",
"description": "Lock notes to open in preview mode by default.", "description": "Lock notes to open in preview mode by default.",
"author": "Felvesthe", "author": "Felvesthe",
"authorUrl": "https://github.com/Felvesthe", "authorUrl": "https://github.com/Felvesthe",
"isDesktopOnly": false "isDesktopOnly": false
} }

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "note-locker", "name": "note-locker",
"version": "1.0.0", "version": "1.2.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "note-locker", "name": "note-locker",
"version": "1.0.0", "version": "1.2.1",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/node": "^16.11.6", "@types/node": "^16.11.6",

View file

@ -1,6 +1,6 @@
{ {
"name": "note-locker", "name": "note-locker",
"version": "1.2.1", "version": "1.3.0",
"description": "Lock notes to open in preview mode by default.", "description": "Lock notes to open in preview mode by default.",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
@ -21,4 +21,4 @@
"tslib": "2.4.0", "tslib": "2.4.0",
"typescript": "4.7.4" "typescript": "4.7.4"
} }
} }

View file

@ -6,12 +6,16 @@ import {
Plugin, Plugin,
TFile, TFile,
WorkspaceLeaf, WorkspaceLeaf,
TAbstractFile,
TFolder,
setIcon,
} from "obsidian"; } from "obsidian";
import { NoteLockerSettings, DEFAULT_SETTINGS } from "./models/types"; import { NoteLockerSettings, DEFAULT_SETTINGS } from "./models/types";
import { FileExplorerUI } from "./ui/fileExplorer"; import { FileExplorerUI } from "./ui/fileExplorer";
import { StatusBarUI } from "./ui/statusBar"; import { StatusBarUI } from "./ui/statusBar";
import { NoteLockerSettingTab } from "./settings"; import { NoteLockerSettingTab } from "./settings";
import { StrictUnlockModal } from "./ui/strictUnlockModal";
export default class NoteLockerPlugin extends Plugin { export default class NoteLockerPlugin extends Plugin {
settings: NoteLockerSettings = DEFAULT_SETTINGS; settings: NoteLockerSettings = DEFAULT_SETTINGS;
@ -41,18 +45,44 @@ export default class NoteLockerPlugin extends Plugin {
// hotkey // hotkey
this.addCommand({ this.addCommand({
id: 'toggle-note-lock', id: 'toggle-note-lock',
name: 'Toggle Lock for current note', name: 'Toggle lock for current note',
checkCallback: (checking: boolean) => { checkCallback: (checking: boolean) => {
const file = this.app.workspace.getActiveFile(); const file = this.app.workspace.getActiveFile();
if (file && file.extension === 'md') { if (file && file.extension === 'md') {
if (!checking) { 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 true;
} }
return false; 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() { onunload() {
@ -71,26 +101,43 @@ export default class NoteLockerPlugin extends Plugin {
); );
this.registerEvent( 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) view.file && this.addLockMenuItem(menu, view.file.path)
) )
); );
this.registerEvent( this.registerEvent(
this.app.workspace.on("active-leaf-change", (leaf) => { this.app.workspace.on("active-leaf-change", (leaf) => {
this.updateLeafMode(leaf); this.updateLeafMode(leaf, true);
this.statusBarUI.updateStatusBarButton(); this.statusBarUI.updateStatusBarButton();
}) })
); );
this.registerEvent( this.registerEvent(
this.app.vault.on("rename", async (file, oldPath) => { this.app.vault.on("rename", async (file, oldPath) => {
let settingsChanged = false;
if (this.settings.lockedNotes.delete(oldPath)) { if (this.settings.lockedNotes.delete(oldPath)) {
if (!this.settings.lockedNotes.has(file.path)) { if (!this.settings.lockedNotes.has(file.path)) {
this.settings.lockedNotes.add(file.path); this.settings.lockedNotes.add(file.path);
} else { settingsChanged = true;
new Notice(`⚠️ Lock skipped: "${file.name}" was already locked`);
} }
}
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(); await this.saveSettings();
this.fileExplorerUI.updateFileExplorerIcons(); this.fileExplorerUI.updateFileExplorerIcons();
} }
@ -101,12 +148,14 @@ export default class NoteLockerPlugin extends Plugin {
this.app.workspace.on("file-open", () => { this.app.workspace.on("file-open", () => {
this.statusBarUI.updateStatusBarButton(); this.statusBarUI.updateStatusBarButton();
this.fileExplorerUI.updateFileExplorerIcons(); this.fileExplorerUI.updateFileExplorerIcons();
this.updateLeafMode(this.app.workspace.getLeaf(false), true);
}) })
); );
this.registerEvent( this.registerEvent(
this.app.workspace.on("layout-change", () => { this.app.workspace.on("layout-change", () => {
this.fileExplorerUI.updateFileExplorerIcons(); this.fileExplorerUI.updateFileExplorerIcons();
this.app.workspace.iterateAllLeaves((leaf) => this.updateLeafMode(leaf));
}) })
); );
} }
@ -114,20 +163,220 @@ export default class NoteLockerPlugin extends Plugin {
private initializeExistingLeaves() { private initializeExistingLeaves() {
this.app.workspace this.app.workspace
.getLeavesOfType("markdown") .getLeavesOfType("markdown")
.forEach((leaf) => this.updateLeafMode(leaf)); .forEach((leaf) => this.updateLeafMode(leaf, true));
} }
private addLockMenuItem(menu: Menu, filePath: string) { private addBulkLockMenuItem(menu: Menu, files: TAbstractFile[]) {
const isNote = filePath.endsWith('.md'); const validItems = files.filter(f =>
if (!isNote) return; (f instanceof TFile && f.extension === 'md') ||
(f instanceof TFolder)
const isLocked = this.settings.lockedNotes.has(filePath);
menu.addItem((item) =>
item
.setTitle(isLocked ? "Unlock" : "Lock")
.setIcon(isLocked ? "unlock" : "lock")
.onClick(() => this.toggleNoteLock(filePath))
); );
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) { async toggleNoteLock(notePath: string) {
@ -159,6 +408,35 @@ export default class NoteLockerPlugin extends Plugin {
this.fileExplorerUI.updateFileExplorerIcons(); 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 { private truncateFileName(name: string): string {
const maxLength = Platform.isMobile const maxLength = Platform.isMobile
? this.settings.mobileNotificationMaxLength ? this.settings.mobileNotificationMaxLength
@ -180,21 +458,117 @@ export default class NoteLockerPlugin extends Plugin {
return view instanceof MarkdownView && view.file?.path === notePath; 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; if (!leaf || !(leaf.view instanceof MarkdownView)) return;
const { view } = leaf; const { view } = leaf;
const targetMode = const path = view.file?.path;
view.file && this.settings.lockedNotes.has(view.file.path) if (!path) return;
? "preview"
: "source";
if (leaf.getViewState().state?.mode !== targetMode) { const isStrictLocked = this.settings.strictLockedNotes.has(path);
leaf.setViewState({ const isLocked = this.isPathLocked(path);
...leaf.getViewState(),
state: { ...leaf.getViewState().state, mode: targetMode }, 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() { async loadSettings() {
@ -203,7 +577,9 @@ export default class NoteLockerPlugin extends Plugin {
this.settings = { this.settings = {
...DEFAULT_SETTINGS, ...DEFAULT_SETTINGS,
...loaded, ...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({ await this.saveData({
...this.settings, ...this.settings,
lockedNotes: Array.from(this.settings.lockedNotes), lockedNotes: Array.from(this.settings.lockedNotes),
lockedFolders: Array.from(this.settings.lockedFolders),
strictLockedNotes: Array.from(this.settings.strictLockedNotes),
}); });
} }

View file

@ -1,17 +1,23 @@
export interface NoteLockerSettings { export interface NoteLockerSettings {
lockedNotes: Set<string>; lockedNotes: Set<string>;
lockedFolders: Set<string>;
strictLockedNotes: Set<string>;
mobileNotificationMaxLength: number; mobileNotificationMaxLength: number;
desktopNotificationMaxLength: number; desktopNotificationMaxLength: number;
showFileExplorerIcons: boolean; showFileExplorerIcons: boolean;
showStatusBarButton: boolean; showStatusBarButton: boolean;
showNotifications: boolean; showNotifications: boolean;
preventEditInLockedNotes: boolean;
} }
export const DEFAULT_SETTINGS: NoteLockerSettings = { export const DEFAULT_SETTINGS: NoteLockerSettings = {
lockedNotes: new Set(), lockedNotes: new Set(),
lockedFolders: new Set(),
strictLockedNotes: new Set(),
mobileNotificationMaxLength: 18, mobileNotificationMaxLength: 18,
desktopNotificationMaxLength: 22, desktopNotificationMaxLength: 22,
showFileExplorerIcons: true, showFileExplorerIcons: true,
showStatusBarButton: true, showStatusBarButton: true,
showNotifications: true showNotifications: true,
preventEditInLockedNotes: false
}; };

View file

@ -1,4 +1,4 @@
import {App, Platform, PluginSettingTab, Setting} from "obsidian"; import { App, Platform, PluginSettingTab, Setting, MarkdownView } from "obsidian";
import type NoteLockerPlugin from "./main"; import type NoteLockerPlugin from "./main";
export class NoteLockerSettingTab extends PluginSettingTab { export class NoteLockerSettingTab extends PluginSettingTab {
@ -41,6 +41,21 @@ export class NoteLockerSettingTab extends PluginSettingTab {
await this.plugin.saveSettings(); 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) const mobileNotificationSetting = new Setting(containerEl)
.setName('Mobile notification max length') .setName('Mobile notification max length')
.setDesc('Maximum length of file names in notifications on mobile devices') .setDesc('Maximum length of file names in notifications on mobile devices')
@ -80,10 +95,24 @@ export class NoteLockerSettingTab extends PluginSettingTab {
}); });
hotkeyInfo.style.fontStyle = 'italic'; 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 lockedNotesCount = this.plugin.settings.lockedNotes.size;
const lockedFoldersCount = this.plugin.settings.lockedFolders.size;
const strictLockedNotesCount = this.plugin.settings.strictLockedNotes.size;
containerEl.createEl('p', { 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}`
});
} }
} }

View file

@ -15,21 +15,20 @@ export class FileExplorerUI {
styleEl.id = 'note-locker-styles'; styleEl.id = 'note-locker-styles';
styleEl.textContent = ` styleEl.textContent = `
.note-locker-icon { .note-locker-icon {
display: inline-flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
margin-left: 4px; margin-left: 6px;
width: 12px; width: 14px;
height: 12px; height: 14px;
font-size: 0.85em;
color: var(--text-accent); color: var(--text-accent);
vertical-align: middle; opacity: 0.8;
} }
.nav-file-title { .nav-file-title, .nav-folder-title {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.nav-file-title-content { .nav-file-title-content, .nav-folder-title-content {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
@ -38,43 +37,12 @@ export class FileExplorerUI {
`; `;
document.head.appendChild(styleEl); document.head.appendChild(styleEl);
// Initial update
this.plugin.app.workspace.onLayoutReady(() => { this.plugin.app.workspace.onLayoutReady(() => {
this.updateFileExplorerIcons(); this.updateFileExplorerIcons();
this.setupFolderObserver(); 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 { private setupFolderObserver(): void {
if (this.folderObserver) { if (this.folderObserver) {
this.folderObserver.disconnect(); this.folderObserver.disconnect();
@ -88,59 +56,165 @@ export class FileExplorerUI {
let shouldUpdate = false; let shouldUpdate = false;
for (const mutation of mutations) { for (const mutation of mutations) {
if (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0) { if (mutation.target instanceof HTMLElement &&
shouldUpdate = true; (mutation.target.classList.contains('note-locker-icon') ||
break; mutation.target.closest('.note-locker-icon'))) {
continue;
} }
if (mutation.type === 'attributes' && if (mutation.type === 'childList') {
mutation.target instanceof HTMLElement && const target = mutation.target as HTMLElement;
(mutation.target.classList.contains('nav-folder') || const isTitle = target.classList.contains('nav-file-title') || target.classList.contains('nav-folder-title');
mutation.target.classList.contains('is-collapsed'))) {
shouldUpdate = true;
break;
}
}
if (shouldUpdate) { let shouldProcess = false;
this.debouncedUpdateIcons(150);
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, { this.folderObserver.observe(fileExplorer, {
childList: true, childList: true,
attributes: true, attributes: true,
attributeFilter: ['class'], attributeFilter: ['class', 'data-path', 'is-collapsed'],
subtree: true 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 { public updateFileExplorerIcons(): void {
if (!this.plugin.settings.showFileExplorerIcons) { if (!this.plugin.settings.showFileExplorerIcons) {
this.removeFileExplorerIcons(); this.removeFileExplorerIcons();
return; return;
} }
// First, remove existing icons to avoid duplicates // Handle files
this.removeFileExplorerIcons();
// Get all file elements in the file explorer
const fileItems = document.querySelectorAll('.nav-file'); const fileItems = document.querySelectorAll('.nav-file');
fileItems.forEach((fileItem) => { fileItems.forEach((fileItem) => {
const titleEl = fileItem.querySelector('.nav-file-title'); const titleEl = fileItem.querySelector('.nav-file-title');
if (!titleEl) return; if (!titleEl) return;
const filePath = titleEl.getAttribute('data-path'); const filePath = titleEl.getAttribute('data-path');
if (!filePath || !this.plugin.settings.lockedNotes.has(filePath)) return; if (!filePath) return;
// Create lock icon element let iconName: string | null = null;
const iconEl = document.createElement('div'); if (this.plugin.settings.strictLockedNotes.has(filePath)) {
iconEl.addClass('note-locker-icon'); iconName = 'lock-keyhole';
setIcon(iconEl, 'lock'); } else if (this.plugin.settings.lockedNotes.has(filePath)) {
iconName = 'lock';
titleEl.appendChild(iconEl); }
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 { public removeFileExplorerIcons(): void {

View file

@ -1,5 +1,6 @@
import { setIcon } from "obsidian"; import { setIcon } from "obsidian";
import type NoteLockerPlugin from "../main"; import type NoteLockerPlugin from "../main";
import { StrictUnlockModal } from "./strictUnlockModal";
export class StatusBarUI { export class StatusBarUI {
private plugin: NoteLockerPlugin; private plugin: NoteLockerPlugin;
@ -18,7 +19,13 @@ export class StatusBarUI {
this.statusBarItemEl.addEventListener('click', () => { this.statusBarItemEl.addEventListener('click', () => {
const activeFile = this.plugin.app.workspace.getActiveFile(); const activeFile = this.plugin.app.workspace.getActiveFile();
if (activeFile) { 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); document.head.appendChild(style);
if (this.plugin.app.workspace.getActiveFile()) { 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' }); const iconSpan = this.statusBarItemEl.createSpan({ cls: 'locker-icon' });
setIcon(iconSpan, isLocked ? 'lock' : 'unlock');
this.statusBarItemEl.createSpan({ if (isStrictLocked) {
text: isLocked ? ' Locked' : ' Unlocked' 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.style.cursor = 'pointer';
this.statusBarItemEl.setAttribute('aria-label',
isLocked ? 'Click to unlock this note' : 'Click to lock this note');
} else { } else {
this.statusBarItemEl.style.cursor = 'default'; this.statusBarItemEl.style.cursor = 'default';
} }

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

View file

@ -3,12 +3,15 @@ import { readFileSync, writeFileSync } from "fs";
const targetVersion = process.env.npm_package_version; const targetVersion = process.env.npm_package_version;
// read minAppVersion from manifest.json and bump version to target 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; const { minAppVersion } = manifest;
manifest.version = targetVersion; manifest.version = targetVersion;
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
// update versions.json with target version and minAppVersion from manifest.json // update versions.json with target version and minAppVersion from manifest.json
let versions = JSON.parse(readFileSync("versions.json", "utf8")); // but only if the target version is not already in versions.json
versions[targetVersion] = minAppVersion; const versions = JSON.parse(readFileSync('versions.json', 'utf8'));
writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); if (!Object.values(versions).includes(minAppVersion)) {
versions[targetVersion] = minAppVersion;
writeFileSync('versions.json', JSON.stringify(versions, null, '\t'));
}