From 92a730ce3fa331cfa89eaa555d2e829ce87d3f3d Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Sat, 30 May 2026 12:57:56 +0200 Subject: [PATCH] new settings tab --- bun.lock | 64 ++--- manifest-beta.json | 2 +- manifest.json | 2 +- package.json | 12 +- packages/obsidian/src/main.ts | 2 +- .../obsidian/src/ui/add-vault-script-modal.ts | 101 +++---- packages/obsidian/src/ui/permission-modal.ts | 40 +-- packages/obsidian/src/ui/settings-tab.ts | 50 ++-- .../settings/approved-hashes-settings-page.ts | 251 ++++++++++++++++++ .../ui/settings/runtime-settings-section.ts | 156 +++++++---- .../settings/script-storage-settings-page.ts | 208 +++++++++++++++ .../src/ui/settings/setting-page-utils.ts | 49 ++++ .../settings/stored-data-settings-section.ts | 146 ++-------- .../vault-scripts-settings-section.ts | 89 +++---- 14 files changed, 824 insertions(+), 348 deletions(-) create mode 100644 packages/obsidian/src/ui/settings/approved-hashes-settings-page.ts create mode 100644 packages/obsidian/src/ui/settings/script-storage-settings-page.ts create mode 100644 packages/obsidian/src/ui/settings/setting-page-utils.ts diff --git a/bun.lock b/bun.lock index 7965d65..d04657a 100644 --- a/bun.lock +++ b/bun.lock @@ -5,24 +5,24 @@ "": { "name": "obsidian-safe-js-plugin", "dependencies": { - "ses": "^2.0.0", + "ses": "^2.1.0", "zod": "^4.4.3", }, "devDependencies": { "@happy-dom/global-registrator": "^20.9.0", "@lemons_dev/lemons-obsidian-plugin-automation": "^0.1.3", - "@types/bun": "^1.3.13", + "@types/bun": "^1.3.14", "eslint": "^9.39.4", "eslint-plugin-import-x": "^4.16.2", "eslint-plugin-no-relative-import-paths": "^1.6.1", - "eslint-plugin-obsidianmd": "^0.2.9", + "eslint-plugin-obsidianmd": "^0.3.0", "eslint-plugin-only-warn": "^1.2.1", - "obsidian": "latest", + "obsidian": "obsidianmd/obsidian-api#2e88986ef3348e501d390afddcdfa08e132eb2f2", "prettier": "^3.8.3", "tslib": "^2.8.1", "typescript": "^6.0.3", - "typescript-eslint": "^8.59.3", - "vite": "^8.0.12", + "typescript-eslint": "8.59.3", + "vite": "^8.0.14", "vite-plugin-banner": "^0.8.1", "vite-plugin-static-copy": "^4.1.0", }, @@ -89,41 +89,41 @@ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], - "@oxc-project/types": ["@oxc-project/types@0.129.0", "", {}, "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg=="], + "@oxc-project/types": ["@oxc-project/types@0.132.0", "", {}, "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ=="], "@package-json/types": ["@package-json/types@0.0.12", "", {}, "sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw=="], "@pkgr/core": ["@pkgr/core@0.1.2", "", {}, "sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ=="], - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0", "", { "os": "android", "cpu": "arm64" }, "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.2", "", { "os": "android", "cpu": "arm64" }, "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ=="], - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w=="], - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA=="], - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA=="], - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0", "", { "os": "linux", "cpu": "arm" }, "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w=="], - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig=="], - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw=="], - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA=="], - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ=="], - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ=="], - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw=="], - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0", "", { "os": "none", "cpu": "arm64" }, "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w=="], - "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg=="], + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.2", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ=="], - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A=="], - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0", "", { "os": "win32", "cpu": "x64" }, "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0", "", {}, "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ=="], @@ -131,7 +131,7 @@ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], - "@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@types/codemirror": ["@types/codemirror@5.60.8", "", { "dependencies": { "@types/tern": "*" } }, "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw=="], @@ -249,7 +249,7 @@ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="], @@ -345,7 +345,7 @@ "eslint-plugin-no-unsanitized": ["eslint-plugin-no-unsanitized@4.1.5", "", { "peerDependencies": { "eslint": "^9 || ^10" } }, "sha512-MSB4hXPVFQrI8weqzs6gzl7reP2k/qSjtCoL2vUMSDejIIq9YL1ZKvq5/ORBXab/PvfBBrWO2jWviYpL+4Ghfg=="], - "eslint-plugin-obsidianmd": ["eslint-plugin-obsidianmd@0.2.9", "", { "dependencies": { "@eslint/config-helpers": "^0.4.2", "@eslint/js": "^9.30.1", "@eslint/json": "0.14.0", "@microsoft/eslint-plugin-sdl": "^1.1.0", "@types/eslint": "9.6.1", "@types/node": "20.12.12", "@typescript-eslint/types": "^8.33.1", "@typescript-eslint/utils": "^8.33.1", "eslint": ">=9.0.0", "eslint-plugin-depend": "1.3.1", "eslint-plugin-import": "^2.31.0", "eslint-plugin-json-schema-validator": "5.1.0", "eslint-plugin-no-unsanitized": "^4.1.5", "eslint-plugin-security": "2.1.1", "globals": "14.0.0", "obsidian": "1.12.3", "semver": "^7.7.4", "typescript": "5.4.5", "typescript-eslint": "^8.35.1" }, "bin": { "eslint-plugin-obsidian": "dist/lib/index.js" } }, "sha512-+dF5Zz5T6/j0QYGu+wHbY3UZb45Kh+QFkFdfvkVk05o4YIIVqHMlrTFrlRVhuBd6Htu8QxcFOwzeMTN4aysVTA=="], + "eslint-plugin-obsidianmd": ["eslint-plugin-obsidianmd@0.3.0", "", { "dependencies": { "@eslint/config-helpers": "^0.4.2", "@eslint/js": "^9.30.1", "@eslint/json": "0.14.0", "@microsoft/eslint-plugin-sdl": "^1.1.0", "@types/eslint": "9.6.1", "@types/node": "20.12.12", "@typescript-eslint/types": "^8.33.1", "@typescript-eslint/utils": "^8.33.1", "eslint": ">=9.0.0", "eslint-plugin-depend": "1.3.1", "eslint-plugin-import": "^2.31.0", "eslint-plugin-json-schema-validator": "5.1.0", "eslint-plugin-no-unsanitized": "^4.1.5", "eslint-plugin-security": "2.1.1", "globals": "14.0.0", "obsidian": "1.12.3", "semver": "^7.7.4", "typescript": "5.4.5", "typescript-eslint": "^8.35.1" }, "bin": { "eslint-plugin-obsidian": "dist/lib/index.js" } }, "sha512-QvGDI6B2nxJBrsZKGTg31da2A/fEJNlnwN+fRZkaoPIu1QL3fYXUdpP7ThyMdr/0iTYQxifb9lt2X9cpydQx1w=="], "eslint-plugin-only-warn": ["eslint-plugin-only-warn@1.2.1", "", {}, "sha512-j37hwfaQDEOfkZ1Dpvu/HnWLavlzQxQxfbrU/9Jb4R9qzrE1eTYuRJyrxq7LzLRI8miG5FOV6veoUVhx7AI84w=="], @@ -589,7 +589,7 @@ "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], - "obsidian": ["obsidian@1.12.3", "", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.6" } }, "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw=="], + "obsidian": ["obsidian@github:obsidianmd/obsidian-api#2e88986", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.6" } }, "obsidianmd-obsidian-api-2e88986", "sha512-miO210/4NYxwNHnoXgDdbskikOzTKBZV39i6nOOD9jokKfMeXbWoRUNEMJGwxyVDsTIvpUeHUjfZ3kDvRaob7A=="], "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], @@ -615,7 +615,7 @@ "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - "postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="], + "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], @@ -645,7 +645,7 @@ "ret": ["ret@0.1.15", "", {}, "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="], - "rolldown": ["rolldown@1.0.0", "", { "dependencies": { "@oxc-project/types": "=0.129.0", "@rolldown/pluginutils": "1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0", "@rolldown/binding-darwin-arm64": "1.0.0", "@rolldown/binding-darwin-x64": "1.0.0", "@rolldown/binding-freebsd-x64": "1.0.0", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0", "@rolldown/binding-linux-arm64-gnu": "1.0.0", "@rolldown/binding-linux-arm64-musl": "1.0.0", "@rolldown/binding-linux-ppc64-gnu": "1.0.0", "@rolldown/binding-linux-s390x-gnu": "1.0.0", "@rolldown/binding-linux-x64-gnu": "1.0.0", "@rolldown/binding-linux-x64-musl": "1.0.0", "@rolldown/binding-openharmony-arm64": "1.0.0", "@rolldown/binding-wasm32-wasi": "1.0.0", "@rolldown/binding-win32-arm64-msvc": "1.0.0", "@rolldown/binding-win32-x64-msvc": "1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA=="], + "rolldown": ["rolldown@1.0.2", "", { "dependencies": { "@oxc-project/types": "=0.132.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.2", "@rolldown/binding-darwin-arm64": "1.0.2", "@rolldown/binding-darwin-x64": "1.0.2", "@rolldown/binding-freebsd-x64": "1.0.2", "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", "@rolldown/binding-linux-arm64-gnu": "1.0.2", "@rolldown/binding-linux-arm64-musl": "1.0.2", "@rolldown/binding-linux-ppc64-gnu": "1.0.2", "@rolldown/binding-linux-s390x-gnu": "1.0.2", "@rolldown/binding-linux-x64-gnu": "1.0.2", "@rolldown/binding-linux-x64-musl": "1.0.2", "@rolldown/binding-openharmony-arm64": "1.0.2", "@rolldown/binding-wasm32-wasi": "1.0.2", "@rolldown/binding-win32-arm64-msvc": "1.0.2", "@rolldown/binding-win32-x64-msvc": "1.0.2" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g=="], "safe-array-concat": ["safe-array-concat@1.1.4", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="], @@ -659,7 +659,7 @@ "semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], - "ses": ["ses@2.0.0", "", { "dependencies": { "@endo/cache-map": "^1.1.0", "@endo/env-options": "^1.1.11", "@endo/immutable-arraybuffer": "^1.1.2" } }, "sha512-Avcinq9dg/LS5W3YW+eRcpolfI0jXP4OsM/JJKCqyqHygUCqpNSsfn8YoNBmfyN2DkSMqjpbWvWPlfY1Jr4oXw=="], + "ses": ["ses@2.1.0", "", { "dependencies": { "@endo/cache-map": "^1.1.0", "@endo/env-options": "^1.1.11", "@endo/immutable-arraybuffer": "^1.1.2" } }, "sha512-mM3s60n24C42W5Q14vTBM9h9l2lfcUEtvoFSFkHSF8hMjMSDjLFObn8COjNbDfsRRS2gePkMves3GR2LPAEMKQ=="], "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], @@ -747,7 +747,7 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - "vite": ["vite@8.0.12", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.14", "rolldown": "1.0.0", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg=="], + "vite": ["vite@8.0.14", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.2", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw=="], "vite-plugin-banner": ["vite-plugin-banner@0.8.1", "", {}, "sha512-0+gGguHk3MH0HvzMSOCJC6fGgH4+jtY9KlKVZh+hwwE+PBkGVzY8xe657JL74vEgbeUJD37XjVqTrmve8XvZBQ=="], @@ -815,6 +815,8 @@ "eslint-plugin-obsidianmd/@types/node": ["@types/node@20.12.12", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw=="], + "eslint-plugin-obsidianmd/obsidian": ["obsidian@1.12.3", "", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.6" } }, "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw=="], + "eslint-plugin-obsidianmd/typescript": ["typescript@5.4.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="], "eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -849,6 +851,8 @@ "eslint-plugin-obsidianmd/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "eslint-plugin-obsidianmd/obsidian/moment": ["moment@2.29.4", "", {}, "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="], + "json-schema-migrate/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], diff --git a/manifest-beta.json b/manifest-beta.json index 3aed714..89e9fb6 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -2,7 +2,7 @@ "id": "safe-js", "name": "Safe JS", "version": "0.1.3", - "minAppVersion": "1.12.3", + "minAppVersion": "1.13.0", "description": "Run JavaScript through sandboxed Web Workers and a limited RPC surface.", "author": "Moritz Jung", "authorUrl": "https://www.moritzjung.dev/", diff --git a/manifest.json b/manifest.json index 3aed714..89e9fb6 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "id": "safe-js", "name": "Safe JS", "version": "0.1.3", - "minAppVersion": "1.12.3", + "minAppVersion": "1.13.0", "description": "Run JavaScript through sandboxed Web Workers and a limited RPC surface.", "author": "Moritz Jung", "authorUrl": "https://www.moritzjung.dev/", diff --git a/package.json b/package.json index 1de2502..0058126 100644 --- a/package.json +++ b/package.json @@ -26,23 +26,23 @@ "devDependencies": { "@happy-dom/global-registrator": "^20.9.0", "@lemons_dev/lemons-obsidian-plugin-automation": "^0.1.3", - "@types/bun": "^1.3.13", + "@types/bun": "^1.3.14", "eslint": "^9.39.4", "eslint-plugin-import-x": "^4.16.2", "eslint-plugin-no-relative-import-paths": "^1.6.1", - "eslint-plugin-obsidianmd": "^0.2.9", + "eslint-plugin-obsidianmd": "^0.3.0", "eslint-plugin-only-warn": "^1.2.1", - "obsidian": "latest", + "obsidian": "obsidianmd/obsidian-api#2e88986ef3348e501d390afddcdfa08e132eb2f2", "prettier": "^3.8.3", "tslib": "^2.8.1", "typescript": "^6.0.3", - "typescript-eslint": "^8.59.3", - "vite": "^8.0.12", + "typescript-eslint": "8.59.3", + "vite": "^8.0.14", "vite-plugin-banner": "^0.8.1", "vite-plugin-static-copy": "^4.1.0" }, "dependencies": { - "ses": "^2.0.0", + "ses": "^2.1.0", "zod": "^4.4.3" } } diff --git a/packages/obsidian/src/main.ts b/packages/obsidian/src/main.ts index 79eff23..51f624d 100644 --- a/packages/obsidian/src/main.ts +++ b/packages/obsidian/src/main.ts @@ -20,7 +20,7 @@ export default class SafeJsPlugin extends Plugin { api!: SafeJsPublicApi; private executionService!: SafeJsExecutionService; private scriptManager!: VaultScriptManager; - settings!: SafeJsSettings; + declare settings: SafeJsSettings; async onload(): Promise { await this.loadSettings(); diff --git a/packages/obsidian/src/ui/add-vault-script-modal.ts b/packages/obsidian/src/ui/add-vault-script-modal.ts index eff60e8..dbbac9f 100644 --- a/packages/obsidian/src/ui/add-vault-script-modal.ts +++ b/packages/obsidian/src/ui/add-vault-script-modal.ts @@ -1,6 +1,6 @@ import type { App } from 'obsidian'; import type { TextComponent } from 'obsidian'; -import { Modal, Notice, Setting } from 'obsidian'; +import { Modal, Notice, SettingGroup } from 'obsidian'; import { isJavaScriptVaultScriptPath } from 'packages/obsidian/src/scripts/script-settings'; export interface AddVaultScriptModalValues { @@ -43,55 +43,64 @@ export class AddVaultScriptModal extends Modal { contentEl.empty(); contentEl.createEl('h2', { text: this.title }); let pathInput: TextComponent | undefined; + const group = new SettingGroup(contentEl); - new Setting(contentEl) - .setName('Vault path') - .setDesc('Use a vault-relative path that ends in .js.') - .addText(text => { - pathInput = text; - return text - .setPlaceholder('Scripts/example.js') - .setValue(this.path) - .onChange(value => { - this.path = value; - }); - }); - - new Setting(contentEl) - .setName('Command name') - .setDesc('Leave blank to use the file name.') - .addText(text => - text - .setPlaceholder('Example script') - .setValue(this.name) - .onChange(value => { - this.name = value; - }), - ); - - new Setting(contentEl) - .setName('Run on startup') - .setDesc('Run this script when Obsidian finishes loading.') - .addToggle(toggle => - toggle.setValue(this.runOnStartup).onChange(value => { - this.runOnStartup = value; + group.addSetting(setting => + void setting + .setName('Vault path') + .setDesc('Use a vault-relative path that ends in .js.') + .addText(text => { + pathInput = text; + return text + .setPlaceholder('Scripts/example.js') + .setValue(this.path) + .onChange(value => { + this.path = value; + }); }), - ); + ); - new Setting(contentEl) - .addButton(button => - button.setButtonText('Cancel').onClick(() => { - this.close(); - }), - ) - .addButton(button => - button - .setButtonText(this.actionText) - .setCta() - .onClick(() => { - void this.submit(); + group.addSetting(setting => + void setting + .setName('Command name') + .setDesc('Leave blank to use the file name.') + .addText(text => + text + .setPlaceholder('Example script') + .setValue(this.name) + .onChange(value => { + this.name = value; + }), + ), + ); + + group.addSetting(setting => + void setting + .setName('Run on startup') + .setDesc('Run this script when Obsidian finishes loading.') + .addToggle(toggle => + toggle.setValue(this.runOnStartup).onChange(value => { + this.runOnStartup = value; }), - ); + ), + ); + + group.addSetting(setting => + void setting + .addButton(button => + button.setButtonText('Cancel').onClick(() => { + this.close(); + }), + ) + .addButton(button => + button + .setButtonText(this.actionText) + .setCta() + .onClick(() => { + void this.submit(); + }), + ), + ); pathInput?.inputEl.focus(); } diff --git a/packages/obsidian/src/ui/permission-modal.ts b/packages/obsidian/src/ui/permission-modal.ts index c23b5a0..8b1a714 100644 --- a/packages/obsidian/src/ui/permission-modal.ts +++ b/packages/obsidian/src/ui/permission-modal.ts @@ -1,6 +1,6 @@ import type { PermissionId } from '@lemons_dev/obsidian-safe-js-api'; import type { App } from 'obsidian'; -import { Modal, Setting } from 'obsidian'; +import { Modal, SettingGroup } from 'obsidian'; import type { PermissionPrompt, PermissionPromptRequest } from 'packages/obsidian/src/execution/execution-service'; import { RICH_OUTPUT_PERMISSION } from 'packages/obsidian/src/permissions/permissions'; import type { RpcRegistry } from 'packages/obsidian/src/rpc/rpc-registry'; @@ -21,17 +21,16 @@ export class ObsidianPermissionPrompt implements PermissionPrompt { return; } - let modal: PermissionApprovalModal; + const modal = new PermissionApprovalModal(this.app, this.rpcRegistry, request, approved => { + cleanup(); + resolve(approved); + }); const abortListener = (): void => { modal.close(); }; const cleanup = (): void => { request.signal?.removeEventListener('abort', abortListener); }; - modal = new PermissionApprovalModal(this.app, this.rpcRegistry, request, approved => { - cleanup(); - resolve(approved); - }); request.signal?.addEventListener('abort', abortListener, { once: true }); modal.open(); }); @@ -88,20 +87,23 @@ class PermissionApprovalModal extends Modal { this.renderPermission(list, permission); } - new Setting(contentEl) - .addButton(button => - button.setButtonText('Cancel').onClick(() => { - this.resolve(false); - }), - ) - .addButton(button => - button - .setButtonText('Allow') - .setCta() - .onClick(() => { - this.resolve(true); + const group = new SettingGroup(contentEl); + group.addSetting(setting => + void setting + .addButton(button => + button.setButtonText('Cancel').onClick(() => { + this.resolve(false); }), - ); + ) + .addButton(button => + button + .setButtonText('Allow') + .setCta() + .onClick(() => { + this.resolve(true); + }), + ), + ); } onClose(): void { diff --git a/packages/obsidian/src/ui/settings-tab.ts b/packages/obsidian/src/ui/settings-tab.ts index be910eb..e8f67e6 100644 --- a/packages/obsidian/src/ui/settings-tab.ts +++ b/packages/obsidian/src/ui/settings-tab.ts @@ -1,4 +1,5 @@ import type { App } from 'obsidian'; +import type { SettingDefinitionItem } from 'obsidian'; import { PluginSettingTab } from 'obsidian'; import type SafeJsPlugin from 'packages/obsidian/src/main'; import type { PermissionSettingsStore } from 'packages/obsidian/src/permissions/approval-store'; @@ -15,7 +16,7 @@ import { VaultScriptsSettingsSection } from 'packages/obsidian/src/ui/settings/v export class SafeJsSettingTab extends PluginSettingTab { plugin: SafeJsPlugin; private readonly approvalStore: LocalStoragePermissionApprovalStore; - private readonly permissionSettingsStore: PermissionSettingsStore; + private readonly runtimeSettingsSection: RuntimeSettingsSection; private readonly scriptManager?: VaultScriptManager; constructor( @@ -27,27 +28,42 @@ export class SafeJsSettingTab extends PluginSettingTab { super(app, plugin); this.plugin = plugin; this.approvalStore = new LocalStoragePermissionApprovalStore(new AppPermissionStorage(app)); - this.permissionSettingsStore = permissionSettingsStore; + this.runtimeSettingsSection = new RuntimeSettingsSection(plugin, permissionSettingsStore, () => { + this.update(); + }); this.scriptManager = scriptManager; } + getSettingDefinitions(): SettingDefinitionItem[] { + return [ + ...this.runtimeSettingsSection.getSettingDefinitions(), + ...new VaultScriptsSettingsSection( + this.app, + this.plugin, + () => { + this.update(); + }, + this.scriptManager, + ).getSettingDefinitions(), + ...new StoredDataSettingsSection(this.app, this.approvalStore, () => { + this.update(); + }).getSettingDefinitions(), + ]; + } + + getControlValue(key: string): unknown { + return this.runtimeSettingsSection.getControlValue(key); + } + + async setControlValue(key: string, value: unknown): Promise { + await this.runtimeSettingsSection.setControlValue(key, value); + } + display(): void { const { containerEl } = this; containerEl.empty(); - - new RuntimeSettingsSection(this.plugin, this.permissionSettingsStore, () => { - this.display(); - }).render(containerEl); - new VaultScriptsSettingsSection( - this.app, - this.plugin, - () => { - this.display(); - }, - this.scriptManager, - ).render(containerEl); - new StoredDataSettingsSection(this.app, this.approvalStore, () => { - this.display(); - }).render(containerEl); + containerEl.createEl('p', { + text: 'Safe JS settings require Obsidian 1.13.0 or newer. This app appears to be running an older version.', + }); } } diff --git a/packages/obsidian/src/ui/settings/approved-hashes-settings-page.ts b/packages/obsidian/src/ui/settings/approved-hashes-settings-page.ts new file mode 100644 index 0000000..0f24054 --- /dev/null +++ b/packages/obsidian/src/ui/settings/approved-hashes-settings-page.ts @@ -0,0 +1,251 @@ +import { Notice, SettingGroup, SettingPage } from 'obsidian'; +import type { PermissionApproval } from 'packages/obsidian/src/permissions/approval-store'; +import type { LocalStoragePermissionApprovalStore } from 'packages/obsidian/src/permissions/approval-store'; +import { + ALL_FILTER_VALUE, + formatCount, + formatDate, + groupBySorted, + renderHiddenSettingItemCount, + visibleSettingItems, +} from 'packages/obsidian/src/ui/settings/setting-page-utils'; + +export class ApprovedHashesSettingPage extends SettingPage { + private permissionFilter = ALL_FILTER_VALUE; + private readonly approvalStore: LocalStoragePermissionApprovalStore; + private readonly onStorageChanged: () => void; + private searchQuery = ''; + private sourceFilter = ALL_FILTER_VALUE; + private readonly staleEntryAgeMs: number; + + constructor(approvalStore: LocalStoragePermissionApprovalStore, staleEntryAgeMs: number, onStorageChanged: () => void) { + super(); + this.approvalStore = approvalStore; + this.onStorageChanged = onStorageChanged; + this.staleEntryAgeMs = staleEntryAgeMs; + this.title = 'Approved script hashes'; + } + + display(): void { + this.containerEl.empty(); + const approvals = this.approvalStore.list(); + const filteredApprovals = this.filterApprovals(approvals); + + this.renderApprovalActions(approvals); + this.renderApprovalFilters(approvals); + this.renderApprovalGroups(filteredApprovals); + } + + private clearOldApprovals(): void { + const deletedCount = this.approvalStore.deleteOlderThan(Date.now() - this.staleEntryAgeMs); + new Notice(`Cleared ${deletedCount} approved script ${deletedCount === 1 ? 'hash' : 'hashes'}.`); + this.refreshSettings(); + } + + private clearAllApprovals(): void { + const deletedCount = this.approvalStore.deleteAll(); + new Notice(`Cleared ${deletedCount} approved script ${deletedCount === 1 ? 'hash' : 'hashes'}.`); + this.refreshSettings(); + } + + private revokeApproval(approval: PermissionApproval | undefined): void { + if (approval === undefined) { + return; + } + + this.approvalStore.delete(approval); + new Notice(`Revoked approval for script hash ${approval.codeHash}.`); + this.refreshSettings(); + } + + private revokeApprovalGroup(approvals: PermissionApproval[], source: string): void { + let deletedCount = 0; + + for (const approval of approvals) { + if (this.approvalStore.delete(approval)) { + deletedCount += 1; + } + } + + new Notice(`Revoked ${deletedCount} approved script ${deletedCount === 1 ? 'hash' : 'hashes'} for ${formatSource(source)}.`); + this.refreshSettings(); + } + + private renderApprovalActions(approvals: PermissionApproval[]): void { + const group = new SettingGroup(this.containerEl); + + group.addSetting(setting => + void setting + .setName('Clear approved hashes') + .setDesc(`${formatCount(approvals.length, 'stored approval hash', 'stored approval hashes')}.`) + .addButton(button => + button.setButtonText('Clear older than 30 days').onClick(() => { + this.clearOldApprovals(); + }), + ) + .addButton(button => + button + .setButtonText('Clear all') + .setDestructive() + .onClick(() => { + this.clearAllApprovals(); + }), + ), + ); + } + + private renderApprovalFilters(approvals: PermissionApproval[]): void { + const group = new SettingGroup(this.containerEl); + + group.addSetting( + setting => + void setting + .setName('Search') + .setDesc('Filter by hash, source, or permission.') + .addSearch(search => + search.setValue(this.searchQuery).onChange(value => { + this.searchQuery = value; + this.display(); + }), + ), + ); + + group.addSetting( + setting => + void setting + .setName('Filters') + .addDropdown(dropdown => + dropdown + .addOptions(this.getPermissionFilterOptions(approvals)) + .setValue(this.permissionFilter) + .onChange(value => { + this.permissionFilter = value; + this.display(); + }), + ) + .addDropdown(dropdown => + dropdown + .addOptions(this.getSourceFilterOptions(approvals)) + .setValue(this.sourceFilter) + .onChange(value => { + this.sourceFilter = value; + this.display(); + }), + ) + .addButton(button => + button.setButtonText('Clear filters').onClick(() => { + this.permissionFilter = ALL_FILTER_VALUE; + this.sourceFilter = ALL_FILTER_VALUE; + this.searchQuery = ''; + this.display(); + }), + ), + ); + } + + private renderApprovalGroups(approvals: PermissionApproval[]): void { + if (approvals.length === 0) { + const group = new SettingGroup(this.containerEl); + group.addSetting(setting => void setting.setName('No approved script hashes match the current filters.')); + return; + } + + for (const [source, sourceApprovals] of groupApprovalsBySource(approvals)) { + const group = new SettingGroup(this.containerEl).setHeading(formatSource(source)); + group.addSetting(setting => + void setting + .setName('Revoke group') + .setDesc(formatCount(sourceApprovals.length, 'approval hash')) + .addButton(button => + button + .setButtonText('Revoke group') + .setDestructive() + .onClick(() => { + this.revokeApprovalGroup(sourceApprovals, source); + }), + ), + ); + + for (const approval of visibleSettingItems(sourceApprovals)) { + group.addSetting( + setting => + void setting + .setName(approval.codeHash) + .setDesc(`${approval.permissions.join(', ')} - ${formatDate(approval.updatedAt)}`) + .addButton(button => + button.setButtonText('Revoke').onClick(() => { + this.revokeApproval(approval); + }), + ), + ); + } + + renderHiddenSettingItemCount(group, sourceApprovals.length - visibleSettingItems(sourceApprovals).length, 'approved hashes'); + } + } + + private filterApprovals(approvals: PermissionApproval[]): PermissionApproval[] { + const normalizedQuery = this.searchQuery.trim().toLowerCase(); + + return approvals.filter(approval => { + if (this.permissionFilter !== ALL_FILTER_VALUE && !approval.permissions.some(permission => permission === this.permissionFilter)) { + return false; + } + + if (this.sourceFilter !== ALL_FILTER_VALUE && sourceFilterValue(approval) !== this.sourceFilter) { + return false; + } + + if (normalizedQuery === '') { + return true; + } + + return approvalSearchText(approval).includes(normalizedQuery); + }); + } + + private getPermissionFilterOptions(approvals: PermissionApproval[]): Record { + const options: Record = { [ALL_FILTER_VALUE]: 'All permissions' }; + + for (const permission of [...new Set(approvals.flatMap(approval => approval.permissions))].sort()) { + options[permission] = permission; + } + + return options; + } + + private getSourceFilterOptions(approvals: PermissionApproval[]): Record { + const options: Record = { [ALL_FILTER_VALUE]: 'All sources' }; + + for (const approval of approvals) { + options[sourceFilterValue(approval)] = formatSource(sourceFilterValue(approval)); + } + + return options; + } + + private refreshSettings(): void { + this.onStorageChanged(); + this.display(); + } +} + +function approvalSearchText(approval: PermissionApproval): string { + return `${approval.codeHash} ${formatSource(sourceFilterValue(approval))} ${approval.permissions.join(' ')}`.toLowerCase(); +} + +function groupApprovalsBySource(approvals: PermissionApproval[]): [string, PermissionApproval[]][] { + return groupBySorted( + approvals, + approval => sourceFilterValue(approval), + (left, right) => formatSource(left).localeCompare(formatSource(right)), + ); +} + +function sourceFilterValue(approval: PermissionApproval): string { + return approval.callerPluginId === undefined ? 'source:notes' : `plugin:${approval.callerPluginId}`; +} + +function formatSource(source: string): string { + return source === 'source:notes' ? 'notes' : source.slice('plugin:'.length); +} diff --git a/packages/obsidian/src/ui/settings/runtime-settings-section.ts b/packages/obsidian/src/ui/settings/runtime-settings-section.ts index 37da55b..80a1fce 100644 --- a/packages/obsidian/src/ui/settings/runtime-settings-section.ts +++ b/packages/obsidian/src/ui/settings/runtime-settings-section.ts @@ -1,9 +1,13 @@ -import { Setting } from 'obsidian'; +import type { SettingDefinitionItem } from 'obsidian'; import type SafeJsPlugin from 'packages/obsidian/src/main'; import type { PermissionSettingsStore } from 'packages/obsidian/src/permissions/approval-store'; import type { SafeJsSettings } from 'packages/obsidian/src/settings/settings-schema'; import { DEFAULT_SETTINGS } from 'packages/obsidian/src/settings/settings-schema'; +export type RuntimeSettingsKey = keyof Pick; +export const AUTO_ALLOW_LOW_RISK_PERMISSIONS_KEY = 'autoAllowLowRiskPermissions'; +export type RuntimeControlKey = RuntimeSettingsKey | typeof AUTO_ALLOW_LOW_RISK_PERMISSIONS_KEY; + export class RuntimeSettingsSection { private readonly onSettingsChanged: () => void; private readonly permissionSettingsStore: PermissionSettingsStore; @@ -15,54 +19,75 @@ export class RuntimeSettingsSection { this.plugin = plugin; } - render(containerEl: HTMLElement): void { - this.createSection(containerEl, 'Runtime settings'); - new Setting(containerEl) - .setName('Execution timeouts') - .setDesc('Cancel scripts that run longer than the configured timeout.') - .addToggle(toggle => - toggle.setValue(this.plugin.settings.executionTimeoutsEnabled).onChange(async value => { - this.plugin.settings.executionTimeoutsEnabled = value; - await this.plugin.saveSettings(); - this.onSettingsChanged(); - }), - ); - - new Setting(containerEl) - .setName('Execution timeout') - .setDesc(this.executionTimeoutDescription(this.plugin.settings)) - .addText(text => - text - .setPlaceholder(String(DEFAULT_SETTINGS.executionTimeoutMs)) - .setValue(String(this.plugin.settings.executionTimeoutMs)) - .onChange(async value => { - this.plugin.settings.executionTimeoutMs = this.normalizeTimeoutMs(value); - await this.plugin.saveSettings(); - }), - ); - - new Setting(containerEl) - .setName('Debug blocks') - .setDesc('Enable support for the safe-js-debug code block language.') - .addToggle(toggle => - toggle.setValue(this.plugin.settings.debugBlocksEnabled).onChange(async value => { - this.plugin.settings.debugBlocksEnabled = value; - await this.plugin.saveSettings(); - }), - ); - - new Setting(containerEl) - .setName('Auto-allow low-risk permissions') - .setDesc('Allow low-risk permissions without prompting. Approvals are still remembered per script hash on this device.') - .addToggle(toggle => - toggle.setValue(this.permissionSettingsStore.loadAutoAllowLowRiskPermissions()).onChange(value => { - this.permissionSettingsStore.saveAutoAllowLowRiskPermissions(value); - }), - ); + getSettingDefinitions(): SettingDefinitionItem[] { + return [ + { + type: 'group', + items: [ + { + name: 'Execution timeouts', + desc: 'Cancel scripts that run longer than the configured timeout.', + control: { type: 'toggle', key: 'executionTimeoutsEnabled' }, + }, + { + name: 'Execution timeout', + desc: this.executionTimeoutDescription(this.plugin.settings), + control: { + type: 'number', + key: 'executionTimeoutMs', + min: 1, + placeholder: String(DEFAULT_SETTINGS.executionTimeoutMs), + validate: value => (this.isValidTimeoutMs(value) ? undefined : 'Enter a positive timeout in milliseconds.'), + }, + }, + { + name: 'Debug blocks', + desc: 'Enable support for the safe-js-debug code block language.', + control: { type: 'toggle', key: 'debugBlocksEnabled' }, + }, + { + name: 'Auto-allow low-risk permissions', + desc: 'Allow low-risk permissions without prompting. Approvals are still remembered per script hash on this device.', + control: { type: 'toggle', key: AUTO_ALLOW_LOW_RISK_PERMISSIONS_KEY }, + }, + ], + }, + ]; } - private createSection(containerEl: HTMLElement, heading: string): void { - new Setting(containerEl).setName(heading).setHeading(); + getControlValue(key: string): unknown { + if (!isRuntimeControlKey(key)) { + return undefined; + } + + if (key === AUTO_ALLOW_LOW_RISK_PERMISSIONS_KEY) { + return this.permissionSettingsStore.loadAutoAllowLowRiskPermissions(); + } + + return this.plugin.settings[key]; + } + + async setControlValue(key: string, value: unknown): Promise { + if (!isRuntimeControlKey(key)) { + return; + } + + if (key === AUTO_ALLOW_LOW_RISK_PERMISSIONS_KEY) { + this.permissionSettingsStore.saveAutoAllowLowRiskPermissions(value === true); + return; + } + + if (key === 'executionTimeoutsEnabled') { + await this.saveExecutionTimeoutsEnabled(value); + return; + } + + if (key === 'debugBlocksEnabled') { + await this.saveDebugBlocksEnabled(value); + return; + } + + await this.saveExecutionTimeoutMs(value); } private executionTimeoutDescription(settings: SafeJsSettings): string { @@ -71,8 +96,39 @@ export class RuntimeSettingsSection { : 'Timeouts are disabled. This value is kept for later use.'; } - private normalizeTimeoutMs(value: string): number { - const parsedValue = Number.parseInt(value, 10); - return Number.isFinite(parsedValue) && parsedValue > 0 ? parsedValue : DEFAULT_SETTINGS.executionTimeoutMs; + private isValidTimeoutMs(value: number): boolean { + return Number.isFinite(value) && value > 0; + } + + private async saveDebugBlocksEnabled(value: unknown): Promise { + if (typeof value !== 'boolean') { + return; + } + + this.plugin.settings.debugBlocksEnabled = value; + await this.plugin.saveSettings(); + } + + private async saveExecutionTimeoutMs(value: unknown): Promise { + if (typeof value !== 'number' || !this.isValidTimeoutMs(value)) { + return; + } + + this.plugin.settings.executionTimeoutMs = value; + await this.plugin.saveSettings(); + } + + private async saveExecutionTimeoutsEnabled(value: unknown): Promise { + if (typeof value !== 'boolean') { + return; + } + + this.plugin.settings.executionTimeoutsEnabled = value; + await this.plugin.saveSettings(); + this.onSettingsChanged(); } } + +function isRuntimeControlKey(key: string): key is RuntimeControlKey { + return key === AUTO_ALLOW_LOW_RISK_PERMISSIONS_KEY || key === 'debugBlocksEnabled' || key === 'executionTimeoutMs' || key === 'executionTimeoutsEnabled'; +} diff --git a/packages/obsidian/src/ui/settings/script-storage-settings-page.ts b/packages/obsidian/src/ui/settings/script-storage-settings-page.ts new file mode 100644 index 0000000..d1348ef --- /dev/null +++ b/packages/obsidian/src/ui/settings/script-storage-settings-page.ts @@ -0,0 +1,208 @@ +import type { App } from 'obsidian'; +import { Notice, SettingGroup, SettingPage } from 'obsidian'; +import type { ScriptStorageEntry } from 'packages/obsidian/src/storage/script-storage'; +import { ScriptStorageManager } from 'packages/obsidian/src/storage/script-storage'; +import { + ALL_FILTER_VALUE, + formatBytes, + formatCount, + formatDate, + groupBySorted, + renderHiddenSettingItemCount, + visibleSettingItems, +} from 'packages/obsidian/src/ui/settings/setting-page-utils'; + +export class ScriptStorageSettingPage extends SettingPage { + private readonly app: App; + private readonly onStorageChanged: () => void; + private scopeFilter = ALL_FILTER_VALUE; + private searchQuery = ''; + private readonly staleEntryAgeMs: number; + + constructor(app: App, staleEntryAgeMs: number, onStorageChanged: () => void) { + super(); + this.app = app; + this.onStorageChanged = onStorageChanged; + this.staleEntryAgeMs = staleEntryAgeMs; + this.title = 'Script storage'; + } + + display(): void { + this.containerEl.empty(); + const entries = ScriptStorageManager.listAll(this.app); + const filteredEntries = this.filterEntries(entries); + + this.renderScriptStorageActions(entries); + this.renderScriptStorageFilters(entries); + this.renderScriptStorageGroups(filteredEntries); + } + + private clearOldScriptStorage(): void { + const deletedCount = ScriptStorageManager.deleteOlderThanAll(this.app, Date.now() - this.staleEntryAgeMs); + new Notice(`Cleared ${deletedCount} script storage ${deletedCount === 1 ? 'key' : 'keys'}.`); + this.refreshSettings(); + } + + private clearAllScriptStorage(): void { + const deletedCount = ScriptStorageManager.deleteAllKnown(this.app); + new Notice(`Cleared ${deletedCount} script storage ${deletedCount === 1 ? 'key' : 'keys'}.`); + this.refreshSettings(); + } + + private clearScriptStorageScope(scope: string | null): void { + const deletedCount = new ScriptStorageManager(this.app, Date.now, scope).deleteAll(); + new Notice(`Cleared ${deletedCount} script storage ${deletedCount === 1 ? 'key' : 'keys'} from ${formatScope(scope)}.`); + this.refreshSettings(); + } + + private deleteScriptStorageEntry(entry: ScriptStorageEntry): void { + new ScriptStorageManager(this.app, Date.now, entry.scope).delete(entry.key); + new Notice(`Deleted script storage key ${entry.key}.`); + this.refreshSettings(); + } + + private renderScriptStorageActions(entries: ScriptStorageEntry[]): void { + const group = new SettingGroup(this.containerEl); + + group.addSetting(setting => + void setting + .setName('Clear script storage') + .setDesc(`${formatCount(entries.length, 'indexed storage key')}.`) + .addButton(button => + button.setButtonText('Clear older than 30 days').onClick(() => { + this.clearOldScriptStorage(); + }), + ) + .addButton(button => + button + .setButtonText('Clear all') + .setDestructive() + .onClick(() => { + this.clearAllScriptStorage(); + }), + ), + ); + } + + private renderScriptStorageFilters(entries: ScriptStorageEntry[]): void { + const group = new SettingGroup(this.containerEl); + + group.addSetting(setting => void setting + .setName('Search') + .setDesc('Filter by key or scope.') + .addSearch(search => + search.setValue(this.searchQuery).onChange(value => { + this.searchQuery = value; + this.display(); + }), + )); + + group.addSetting(setting => void setting + .setName('Filters') + .addDropdown(dropdown => + dropdown + .addOptions(this.getScopeFilterOptions(entries)) + .setValue(this.scopeFilter) + .onChange(value => { + this.scopeFilter = value; + this.display(); + }), + ) + .addButton(button => + button.setButtonText('Clear filters').onClick(() => { + this.scopeFilter = ALL_FILTER_VALUE; + this.searchQuery = ''; + this.display(); + }), + )); + } + + private renderScriptStorageGroups(entries: ScriptStorageEntry[]): void { + if (entries.length === 0) { + const group = new SettingGroup(this.containerEl); + group.addSetting(setting => void setting.setName('No indexed script storage keys match the current filters.')); + return; + } + + for (const [scope, scopeEntries] of groupEntriesByScope(entries)) { + const group = new SettingGroup(this.containerEl).setHeading(formatScope(scope)); + group.addSetting(setting => + void setting + .setName('Clear scope') + .setDesc(`${formatCount(scopeEntries.length, 'key')} - ${formatBytes(sumEntryBytes(scopeEntries))}`) + .addButton(button => + button + .setButtonText('Clear scope') + .setDestructive() + .onClick(() => { + this.clearScriptStorageScope(scope); + }), + ), + ); + + for (const entry of visibleSettingItems(scopeEntries)) { + group.addSetting(setting => void setting + .setName(entry.key) + .setDesc(`${formatBytes(entry.sizeBytes)} - ${formatDate(entry.updatedAt)}`) + .addButton(button => + button.setButtonText('Delete').onClick(() => { + this.deleteScriptStorageEntry(entry); + }), + )); + } + + renderHiddenSettingItemCount(group, scopeEntries.length - visibleSettingItems(scopeEntries).length, 'storage keys'); + } + } + + private filterEntries(entries: ScriptStorageEntry[]): ScriptStorageEntry[] { + const normalizedQuery = this.searchQuery.trim().toLowerCase(); + + return entries.filter(entry => { + if (this.scopeFilter !== ALL_FILTER_VALUE && scopeFilterValue(entry.scope) !== this.scopeFilter) { + return false; + } + + if (normalizedQuery === '') { + return true; + } + + return `${entry.key} ${formatScope(entry.scope)}`.toLowerCase().includes(normalizedQuery); + }); + } + + private getScopeFilterOptions(entries: ScriptStorageEntry[]): Record { + const options: Record = { [ALL_FILTER_VALUE]: 'All scopes' }; + + for (const entry of entries) { + options[scopeFilterValue(entry.scope)] = formatScope(entry.scope); + } + + return options; + } + + private refreshSettings(): void { + this.onStorageChanged(); + this.display(); + } +} + +function groupEntriesByScope(entries: ScriptStorageEntry[]): [string | null, ScriptStorageEntry[]][] { + return groupBySorted( + entries, + entry => entry.scope, + (left, right) => formatScope(left).localeCompare(formatScope(right)), + ); +} + +function scopeFilterValue(scope: string | null): string { + return scope === null ? 'global' : `scope:${scope}`; +} + +function sumEntryBytes(entries: ScriptStorageEntry[]): number { + return entries.reduce((total, entry) => total + entry.sizeBytes, 0); +} + +function formatScope(scope: string | null): string { + return scope === null ? 'global' : `scoped ${scope.slice(0, 12)}`; +} diff --git a/packages/obsidian/src/ui/settings/setting-page-utils.ts b/packages/obsidian/src/ui/settings/setting-page-utils.ts new file mode 100644 index 0000000..5e3f498 --- /dev/null +++ b/packages/obsidian/src/ui/settings/setting-page-utils.ts @@ -0,0 +1,49 @@ +import type { SettingGroup } from 'obsidian'; + +export const ALL_FILTER_VALUE = '__all__'; +export const DEFAULT_VISIBLE_SETTING_ITEMS = 20; + +export function formatBytes(bytes: number): string { + if (bytes < 1024) { + return `${bytes} B`; + } + + return `${(bytes / 1024).toFixed(1)} KB`; +} + +export function formatDate(timestamp: number): string { + return new Date(timestamp).toLocaleString(); +} + +export function formatCount(count: number, singularLabel: string, pluralLabel: string = `${singularLabel}s`): string { + return `${count} ${count === 1 ? singularLabel : pluralLabel}`; +} + +export function groupBySorted(items: TItem[], getKey: (item: TItem) => TKey, compareKeys: (left: TKey, right: TKey) => number): [TKey, TItem[]][] { + const groupedItems = new Map(); + + for (const item of items) { + const key = getKey(item); + const group = groupedItems.get(key); + + if (group === undefined) { + groupedItems.set(key, [item]); + } else { + group.push(item); + } + } + + return [...groupedItems.entries()].sort(([left], [right]) => compareKeys(left, right)); +} + +export function visibleSettingItems(items: TItem[], limit: number = DEFAULT_VISIBLE_SETTING_ITEMS): TItem[] { + return items.slice(0, limit); +} + +export function renderHiddenSettingItemCount(group: SettingGroup, hiddenCount: number, itemLabel: string): void { + if (hiddenCount <= 0) { + return; + } + + group.addSetting(setting => void setting.setName(`${hiddenCount} more ${itemLabel} ${hiddenCount === 1 ? 'is' : 'are'} hidden.`)); +} diff --git a/packages/obsidian/src/ui/settings/stored-data-settings-section.ts b/packages/obsidian/src/ui/settings/stored-data-settings-section.ts index 874eb18..16ca7a1 100644 --- a/packages/obsidian/src/ui/settings/stored-data-settings-section.ts +++ b/packages/obsidian/src/ui/settings/stored-data-settings-section.ts @@ -1,9 +1,9 @@ import type { App } from 'obsidian'; -import { Notice, Setting, SettingGroup } from 'obsidian'; -import type { PermissionApproval } from 'packages/obsidian/src/permissions/approval-store'; +import type { SettingDefinitionItem } from 'obsidian'; import type { LocalStoragePermissionApprovalStore } from 'packages/obsidian/src/permissions/approval-store'; -import type { ScriptStorageEntry } from 'packages/obsidian/src/storage/script-storage'; import { ScriptStorageManager } from 'packages/obsidian/src/storage/script-storage'; +import { ApprovedHashesSettingPage } from 'packages/obsidian/src/ui/settings/approved-hashes-settings-page'; +import { ScriptStorageSettingPage } from 'packages/obsidian/src/ui/settings/script-storage-settings-page'; export class StoredDataSettingsSection { private readonly app: App; @@ -17,133 +17,23 @@ export class StoredDataSettingsSection { this.onStorageChanged = onStorageChanged; } - render(containerEl: HTMLElement): void { - this.renderApprovalStorage(containerEl); - this.renderScriptStorage(containerEl); - } - - private renderApprovalStorage(containerEl: HTMLElement): void { + getSettingDefinitions(): SettingDefinitionItem[] { const approvals = this.approvalStore.list(); - this.createSection(containerEl, 'Approved script hashes'); - - new Setting(containerEl) - .setName('Clear approved hashes') - .setDesc('Remove remembered permission approvals for changed or old scripts.') - .addButton(button => - button.setButtonText('Clear older than 30 days').onClick(() => { - const deletedCount = this.approvalStore.deleteOlderThan(Date.now() - this.staleEntryAgeMs); - new Notice(`Cleared ${deletedCount} approved script ${deletedCount === 1 ? 'hash' : 'hashes'}.`); - this.onStorageChanged(); - }), - ) - .addButton(button => - button.setButtonText('Clear all').onClick(() => { - const deletedCount = this.approvalStore.deleteAll(); - new Notice(`Cleared ${deletedCount} approved script ${deletedCount === 1 ? 'hash' : 'hashes'}.`); - this.onStorageChanged(); - }), - ); - - this.renderApprovalList(new SettingGroup(containerEl), approvals); - } - - private renderApprovalList(group: SettingGroup, approvals: PermissionApproval[]): void { - if (approvals.length === 0) { - group.addSetting(setting => { - setting.setName('No approved script hashes are stored.'); - }); - return; - } - - for (const approval of approvals.slice(0, 20)) { - group.addSetting(setting => { - setting - .setName(approval.codeHash) - .setDesc(`${formatCaller(approval.callerPluginId)} - ${approval.permissions.join(', ')} - ${formatDate(approval.updatedAt)}`) - .addButton(button => - button.setButtonText('Revoke').onClick(() => { - this.approvalStore.delete(approval); - new Notice(`Revoked approval for script hash ${approval.codeHash}.`); - this.onStorageChanged(); - }), - ); - }); - } - - if (approvals.length > 20) { - group.addSetting(setting => { - setting.setName(`${approvals.length - 20} more approved hashes are hidden.`); - }); - } - } - - private renderScriptStorage(containerEl: HTMLElement): void { const entries = ScriptStorageManager.listAll(this.app); - this.createSection(containerEl, 'Script storage'); - new Setting(containerEl) - .setName('Clear script storage') - .setDesc('Remove data written through the storage API by scripts.') - .addButton(button => - button.setButtonText('Clear older than 30 days').onClick(() => { - const deletedCount = ScriptStorageManager.deleteOlderThanAll(this.app, Date.now() - this.staleEntryAgeMs); - new Notice(`Cleared ${deletedCount} script storage ${deletedCount === 1 ? 'key' : 'keys'}.`); - this.onStorageChanged(); - }), - ) - .addButton(button => - button.setButtonText('Clear all').onClick(() => { - const deletedCount = ScriptStorageManager.deleteAllKnown(this.app); - new Notice(`Cleared ${deletedCount} script storage ${deletedCount === 1 ? 'key' : 'keys'}.`); - this.onStorageChanged(); - }), - ); - - this.renderScriptStorageList(new SettingGroup(containerEl), entries); - } - - private renderScriptStorageList(group: SettingGroup, entries: ScriptStorageEntry[]): void { - if (entries.length === 0) { - group.addSetting(setting => { - setting.setName('No indexed script storage keys are stored.'); - }); - return; - } - - for (const entry of entries.slice(0, 20)) { - group.addSetting(setting => { - setting.setName(entry.key).setDesc(`${formatScope(entry.scope)} - ${formatBytes(entry.sizeBytes)} - ${formatDate(entry.updatedAt)}`); - }); - } - - if (entries.length > 20) { - group.addSetting(setting => { - setting.setName(`${entries.length - 20} more storage keys are hidden.`); - }); - } - } - - private createSection(containerEl: HTMLElement, heading: string): void { - new Setting(containerEl).setName(heading).setHeading(); + return [ + { + type: 'page', + name: 'Approved script hashes', + desc: `${approvals.length} stored`, + page: () => new ApprovedHashesSettingPage(this.approvalStore, this.staleEntryAgeMs, this.onStorageChanged), + }, + { + type: 'page', + name: 'Script storage', + desc: `${entries.length} indexed`, + page: () => new ScriptStorageSettingPage(this.app, this.staleEntryAgeMs, this.onStorageChanged), + }, + ]; } } - -function formatDate(timestamp: number): string { - return new Date(timestamp).toLocaleString(); -} - -function formatBytes(bytes: number): string { - if (bytes < 1024) { - return `${bytes} B`; - } - - return `${(bytes / 1024).toFixed(1)} KB`; -} - -function formatCaller(callerPluginId: string | undefined): string { - return callerPluginId ?? 'notes'; -} - -function formatScope(scope: string | null): string { - return scope === null ? 'global' : `scoped ${scope.slice(0, 12)}`; -} diff --git a/packages/obsidian/src/ui/settings/vault-scripts-settings-section.ts b/packages/obsidian/src/ui/settings/vault-scripts-settings-section.ts index 02b554f..4455472 100644 --- a/packages/obsidian/src/ui/settings/vault-scripts-settings-section.ts +++ b/packages/obsidian/src/ui/settings/vault-scripts-settings-section.ts @@ -1,5 +1,5 @@ import type { App } from 'obsidian'; -import { Setting, SettingGroup } from 'obsidian'; +import type { SettingDefinitionItem } from 'obsidian'; import type SafeJsPlugin from 'packages/obsidian/src/main'; import type { SafeJsScriptConfig } from 'packages/obsidian/src/scripts/script-settings'; import { createScriptConfig, displayNameFromPath, isJavaScriptVaultScriptPath } from 'packages/obsidian/src/scripts/script-settings'; @@ -20,49 +20,40 @@ export class VaultScriptsSettingsSection { this.scriptManager = scriptManager; } - render(containerEl: HTMLElement): void { - this.createSection(containerEl, 'Vault scripts'); - const group = new SettingGroup(containerEl); - - for (const script of this.plugin.settings.scripts) { - this.renderVaultScript(group, script); - } - - this.renderAddVaultScript(group); - } - - private renderVaultScript(group: SettingGroup, script: SafeJsScriptConfig): void { - group.addSetting(setting => { - setting - .setName(script.name) - .setDesc(formatVaultScriptDescription(script)) - .addButton(button => - button.setButtonText('Edit').onClick(() => { - this.openEditVaultScriptModal(script); - }), - ) - .addButton(button => - button.setButtonText('Remove').onClick(() => { - void this.removeVaultScript(script); - }), - ); - }); - } - - private renderAddVaultScript(group: SettingGroup): void { - group.addSetting(setting => { - setting - .setName('Add script') - .setDesc('Configure a vault .js file as a command.') - .addButton(button => - button - .setButtonText('Add script') - .setCta() - .onClick(() => { - this.openAddVaultScriptModal(); - }), - ); - }); + getSettingDefinitions(): SettingDefinitionItem[] { + return [ + { + type: 'page', + name: 'Vault scripts', + desc: 'Configure vault .js files as commands.', + items: [ + { + type: 'list', + emptyState: 'No vault scripts are configured.', + addItem: { + name: 'Add script', + action: (): void => { + this.openAddVaultScriptModal(); + }, + }, + onDelete: (index): void => { + void this.removeVaultScript(this.plugin.settings.scripts[index]); + }, + items: this.plugin.settings.scripts.map(script => ({ + name: script.name, + desc: formatVaultScriptDescription(script), + render: (setting): void => { + setting.addExtraButton(button => + button.setIcon('edit').onClick(() => { + this.openEditVaultScriptModal(script); + }), + ); + }, + })), + }, + ], + }, + ]; } private openAddVaultScriptModal(): void { @@ -81,7 +72,11 @@ export class VaultScriptsSettingsSection { ).open(); } - private async removeVaultScript(script: SafeJsScriptConfig): Promise { + private async removeVaultScript(script: SafeJsScriptConfig | undefined): Promise { + if (script === undefined) { + return; + } + this.plugin.settings.scripts = this.plugin.settings.scripts.filter(candidate => candidate.id !== script.id); await this.saveScriptSettings(); this.onSettingsChanged(); @@ -143,10 +138,6 @@ export class VaultScriptsSettingsSection { await this.plugin.saveSettings(); this.scriptManager?.reloadCommands(); } - - private createSection(containerEl: HTMLElement, heading: string): void { - new Setting(containerEl).setName(heading).setHeading(); - } } function formatVaultScriptDescription(script: SafeJsScriptConfig): string {