mirror of
https://github.com/yazanammar/obsidian-theme-engine.git
synced 2026-07-22 06:44:37 +00:00
refactor: modularize core engine and settings UI
- extracted core logic into dedicated modules - simplified main.ts and settingsTab.ts - improved maintainability and separation of concerns - no functional changes intended
This commit is contained in:
parent
c5a192157d
commit
61fa8f29ad
13 changed files with 2461 additions and 2325 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import js from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
import obsidianmd from "eslint-plugin-obsidianmd";
|
||||
import eslintComments from "eslint-plugin-eslint-comments";
|
||||
import eslintComments from "@eslint-community/eslint-plugin-eslint-comments";
|
||||
import globals from "globals";
|
||||
|
||||
export default tseslint.config(
|
||||
|
|
@ -25,7 +25,7 @@ export default tseslint.config(
|
|||
{
|
||||
files: ["src/**/*.ts", "src/**/*.tsx"],
|
||||
plugins: {
|
||||
"eslint-comments": eslintComments,
|
||||
"@eslint-community/eslint-comments": eslintComments,
|
||||
},
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
|
|
@ -47,8 +47,9 @@ export default tseslint.config(
|
|||
"obsidianmd/hardcoded-config-path": "error",
|
||||
"obsidianmd/no-forbidden-elements": "error",
|
||||
"@typescript-eslint/await-thenable": "error",
|
||||
"eslint-comments/require-description": "error",
|
||||
"eslint-comments/no-restricted-disable": [
|
||||
"@eslint-community/eslint-comments/require-description": "error",
|
||||
"@eslint-community/eslint-comments/no-unused-disable": "error",
|
||||
"@eslint-community/eslint-comments/no-restricted-disable": [
|
||||
"error",
|
||||
"obsidianmd/no-forbidden-elements",
|
||||
"obsidianmd/ui/sentence-case",
|
||||
|
|
|
|||
146
package-lock.json
generated
146
package-lock.json
generated
|
|
@ -16,7 +16,7 @@
|
|||
"@eslint-community/eslint-plugin-eslint-comments": "^4.4.0",
|
||||
"@types/node": "^25.2.3",
|
||||
"@types/sortablejs": "^1.15.9",
|
||||
"@typescript-eslint/utils": "^8.55.0",
|
||||
"@typescript-eslint/utils": "^8.56.0",
|
||||
"cpy-cli": "^7.0.0",
|
||||
"esbuild": "^0.27.3",
|
||||
"esbuild-sass-plugin": "^3.6.0",
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
"prettier": "^3.8.1",
|
||||
"sass": "^1.97.3",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.55.0"
|
||||
"typescript-eslint": "^8.56.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/protobuf": {
|
||||
|
|
@ -1311,17 +1311,17 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.55.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.55.0.tgz",
|
||||
"integrity": "sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz",
|
||||
"integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.55.0",
|
||||
"@typescript-eslint/type-utils": "8.55.0",
|
||||
"@typescript-eslint/utils": "8.55.0",
|
||||
"@typescript-eslint/visitor-keys": "8.55.0",
|
||||
"@typescript-eslint/scope-manager": "8.56.0",
|
||||
"@typescript-eslint/type-utils": "8.56.0",
|
||||
"@typescript-eslint/utils": "8.56.0",
|
||||
"@typescript-eslint/visitor-keys": "8.56.0",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.4.0"
|
||||
|
|
@ -1334,8 +1334,8 @@
|
|||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.55.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"@typescript-eslint/parser": "^8.56.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
|
|
@ -1350,16 +1350,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.55.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.55.0.tgz",
|
||||
"integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz",
|
||||
"integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.55.0",
|
||||
"@typescript-eslint/types": "8.55.0",
|
||||
"@typescript-eslint/typescript-estree": "8.55.0",
|
||||
"@typescript-eslint/visitor-keys": "8.55.0",
|
||||
"@typescript-eslint/scope-manager": "8.56.0",
|
||||
"@typescript-eslint/types": "8.56.0",
|
||||
"@typescript-eslint/typescript-estree": "8.56.0",
|
||||
"@typescript-eslint/visitor-keys": "8.56.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -1370,19 +1370,19 @@
|
|||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.55.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.55.0.tgz",
|
||||
"integrity": "sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz",
|
||||
"integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.55.0",
|
||||
"@typescript-eslint/types": "^8.55.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.56.0",
|
||||
"@typescript-eslint/types": "^8.56.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -1397,14 +1397,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.55.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.55.0.tgz",
|
||||
"integrity": "sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz",
|
||||
"integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.55.0",
|
||||
"@typescript-eslint/visitor-keys": "8.55.0"
|
||||
"@typescript-eslint/types": "8.56.0",
|
||||
"@typescript-eslint/visitor-keys": "8.56.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
|
@ -1415,9 +1415,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.55.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.55.0.tgz",
|
||||
"integrity": "sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz",
|
||||
"integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
@ -1432,15 +1432,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.55.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.55.0.tgz",
|
||||
"integrity": "sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz",
|
||||
"integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.55.0",
|
||||
"@typescript-eslint/typescript-estree": "8.55.0",
|
||||
"@typescript-eslint/utils": "8.55.0",
|
||||
"@typescript-eslint/types": "8.56.0",
|
||||
"@typescript-eslint/typescript-estree": "8.56.0",
|
||||
"@typescript-eslint/utils": "8.56.0",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.4.0"
|
||||
},
|
||||
|
|
@ -1452,14 +1452,14 @@
|
|||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.55.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.55.0.tgz",
|
||||
"integrity": "sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz",
|
||||
"integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
@ -1471,16 +1471,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.55.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.55.0.tgz",
|
||||
"integrity": "sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz",
|
||||
"integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.55.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.55.0",
|
||||
"@typescript-eslint/types": "8.55.0",
|
||||
"@typescript-eslint/visitor-keys": "8.55.0",
|
||||
"@typescript-eslint/project-service": "8.56.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.56.0",
|
||||
"@typescript-eslint/types": "8.56.0",
|
||||
"@typescript-eslint/visitor-keys": "8.56.0",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^9.0.5",
|
||||
"semver": "^7.7.3",
|
||||
|
|
@ -1499,16 +1499,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.55.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.55.0.tgz",
|
||||
"integrity": "sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz",
|
||||
"integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.55.0",
|
||||
"@typescript-eslint/types": "8.55.0",
|
||||
"@typescript-eslint/typescript-estree": "8.55.0"
|
||||
"@typescript-eslint/scope-manager": "8.56.0",
|
||||
"@typescript-eslint/types": "8.56.0",
|
||||
"@typescript-eslint/typescript-estree": "8.56.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
|
@ -1518,19 +1518,19 @@
|
|||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.55.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.55.0.tgz",
|
||||
"integrity": "sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz",
|
||||
"integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.55.0",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
"@typescript-eslint/types": "8.56.0",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
|
@ -1541,13 +1541,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
||||
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz",
|
||||
"integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
|
|
@ -6183,16 +6183,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.55.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.55.0.tgz",
|
||||
"integrity": "sha512-HE4wj+r5lmDVS9gdaN0/+iqNvPZwGfnJ5lZuz7s5vLlg9ODw0bIiiETaios9LvFI1U94/VBXGm3CB2Y5cNFMpw==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.0.tgz",
|
||||
"integrity": "sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.55.0",
|
||||
"@typescript-eslint/parser": "8.55.0",
|
||||
"@typescript-eslint/typescript-estree": "8.55.0",
|
||||
"@typescript-eslint/utils": "8.55.0"
|
||||
"@typescript-eslint/eslint-plugin": "8.56.0",
|
||||
"@typescript-eslint/parser": "8.56.0",
|
||||
"@typescript-eslint/typescript-estree": "8.56.0",
|
||||
"@typescript-eslint/utils": "8.56.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
|
@ -6202,7 +6202,7 @@
|
|||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -19,20 +19,20 @@
|
|||
"tslib": "^2.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "^4.4.0",
|
||||
"@types/node": "^25.2.3",
|
||||
"@types/sortablejs": "^1.15.9",
|
||||
"@typescript-eslint/utils": "^8.55.0",
|
||||
"@typescript-eslint/utils": "^8.56.0",
|
||||
"cpy-cli": "^7.0.0",
|
||||
"esbuild": "^0.27.3",
|
||||
"esbuild-sass-plugin": "^3.6.0",
|
||||
"eslint": "^9.39.2",
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "^4.4.0",
|
||||
"eslint-plugin-obsidianmd": "^0.1.9",
|
||||
"globals": "^17.3.0",
|
||||
"obsidian": "^1.12.0",
|
||||
"prettier": "^3.8.1",
|
||||
"sass": "^1.97.3",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.55.0"
|
||||
"typescript-eslint": "^8.56.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
407
src/core/background-media.ts
Normal file
407
src/core/background-media.ts
Normal file
|
|
@ -0,0 +1,407 @@
|
|||
import { Notice, requestUrl } from 'obsidian';
|
||||
import { BUILT_IN_PROFILES_VARS } from '../constants';
|
||||
import { t } from '../i18n/strings';
|
||||
import type ThemeEngine from '../main';
|
||||
import { FileConflictModal } from '../ui/modals';
|
||||
import { findNextAvailablePath, maybeConvertToJpg } from '../utils';
|
||||
|
||||
export type MediaType = 'image' | 'video';
|
||||
export type ConflictChoice = 'replace' | 'keep' | 'prompt';
|
||||
|
||||
export const clearBackgroundMedia = (): void => {
|
||||
document.body.setCssProps({ '--cm-background-image': null });
|
||||
document.body.classList.remove('cm-workspace-background-active');
|
||||
document.body.classList.remove('cm-settings-background-active');
|
||||
|
||||
const oldVideo = document.getElementById('cm-background-video');
|
||||
if (oldVideo) oldVideo.remove();
|
||||
};
|
||||
|
||||
export const applyTransparencyToVars = async (plugin: ThemeEngine): Promise<boolean> => {
|
||||
const profile = plugin.settings.profiles?.[plugin.settings.activeProfile];
|
||||
if (!profile) return false;
|
||||
|
||||
const activeProfileName = plugin.settings.activeProfile;
|
||||
const profileOriginalVars =
|
||||
BUILT_IN_PROFILES_VARS[activeProfileName as keyof typeof BUILT_IN_PROFILES_VARS];
|
||||
const baseVars = profileOriginalVars || BUILT_IN_PROFILES_VARS.Default;
|
||||
|
||||
const varsToMakeTransparent = [
|
||||
'--background-primary',
|
||||
'--background-secondary',
|
||||
'--background-modifier-border',
|
||||
'--titlebar-background-focused',
|
||||
'--background-modifier-hover',
|
||||
];
|
||||
|
||||
let settingsChanged = false;
|
||||
for (const varName of varsToMakeTransparent) {
|
||||
if (profile.vars[varName] !== 'transparent') {
|
||||
const defaultValue = baseVars[varName as keyof typeof baseVars];
|
||||
if (!profile.vars[varName] || profile.vars[varName] === defaultValue) {
|
||||
profile.vars[varName] = 'transparent';
|
||||
settingsChanged = true;
|
||||
if (plugin.settings.colorUpdateFPS > 0) {
|
||||
plugin.pendingVarUpdates[varName] = 'transparent';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (settingsChanged) {
|
||||
await plugin.saveData(plugin.settings);
|
||||
if (plugin.settingTabInstance && plugin.settingTabInstance.containerEl.offsetHeight > 0) {
|
||||
plugin.settingTabInstance.display();
|
||||
}
|
||||
}
|
||||
|
||||
return settingsChanged;
|
||||
};
|
||||
|
||||
export const restoreDefaultBackgroundVars = async (plugin: ThemeEngine): Promise<boolean> => {
|
||||
const profile = plugin.settings.profiles?.[plugin.settings.activeProfile];
|
||||
if (!profile) return false;
|
||||
|
||||
const activeProfileName = plugin.settings.activeProfile;
|
||||
const profileOriginalVars =
|
||||
BUILT_IN_PROFILES_VARS[activeProfileName as keyof typeof BUILT_IN_PROFILES_VARS];
|
||||
const baseVars = profileOriginalVars || BUILT_IN_PROFILES_VARS.Default;
|
||||
|
||||
const varsToRestore = [
|
||||
'--background-primary',
|
||||
'--background-secondary',
|
||||
'--background-modifier-border',
|
||||
'--titlebar-background-focused',
|
||||
'--background-modifier-hover',
|
||||
];
|
||||
|
||||
let settingsRestored = false;
|
||||
for (const varName of varsToRestore) {
|
||||
if (profile.vars[varName] === 'transparent') {
|
||||
const defaultValue = baseVars[varName as keyof typeof baseVars];
|
||||
if (defaultValue) {
|
||||
profile.vars[varName] = defaultValue;
|
||||
settingsRestored = true;
|
||||
if (plugin.settings.colorUpdateFPS > 0) {
|
||||
plugin.pendingVarUpdates[varName] = defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (settingsRestored) {
|
||||
await plugin.saveData(plugin.settings);
|
||||
if (plugin.settingTabInstance && plugin.settingTabInstance.containerEl.offsetHeight > 0) {
|
||||
plugin.settingTabInstance.display();
|
||||
}
|
||||
if (plugin.settings.colorUpdateFPS === 0) {
|
||||
plugin.applyPendingNow();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const applyBackgroundMedia = async (plugin: ThemeEngine): Promise<void> => {
|
||||
const profile = plugin.settings.profiles?.[plugin.settings.activeProfile];
|
||||
|
||||
clearBackgroundMedia();
|
||||
|
||||
if (!profile || profile.backgroundEnabled === false || !profile.backgroundPath) {
|
||||
if (profile && profile.backgroundEnabled === false) {
|
||||
await restoreDefaultBackgroundVars(plugin);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await applyTransparencyToVars(plugin);
|
||||
|
||||
const path = profile.backgroundPath;
|
||||
const type = profile.backgroundType;
|
||||
if (type === 'image') {
|
||||
const imageUrl = plugin.app.vault.adapter.getResourcePath(path);
|
||||
document.body.setCssProps({
|
||||
'--cm-background-image': `url("${imageUrl}")`,
|
||||
});
|
||||
|
||||
document.body.classList.add('cm-workspace-background-active');
|
||||
} else if (type === 'video') {
|
||||
const videoUrl = plugin.app.vault.adapter.getResourcePath(path);
|
||||
|
||||
let videoEl = document.querySelector<HTMLVideoElement>('#cm-background-video');
|
||||
if (!videoEl) {
|
||||
videoEl = document.createElement('video');
|
||||
videoEl.id = 'cm-background-video';
|
||||
document.body.appendChild(videoEl);
|
||||
}
|
||||
|
||||
videoEl.src = videoUrl;
|
||||
videoEl.autoplay = true;
|
||||
videoEl.loop = true;
|
||||
videoEl.muted = profile.videoMuted !== false;
|
||||
videoEl.playsInline = true;
|
||||
videoEl.setCssProps({
|
||||
opacity: (profile.videoOpacity || 0.5).toString(),
|
||||
});
|
||||
|
||||
videoEl.load();
|
||||
document.body.classList.add('cm-workspace-background-active');
|
||||
}
|
||||
};
|
||||
|
||||
export const ensureBackgroundsFolderExists = async (plugin: ThemeEngine): Promise<void> => {
|
||||
const backgroundsPath = `${plugin.app.vault.configDir}/backgrounds`;
|
||||
try {
|
||||
if (!(await plugin.app.vault.adapter.exists(backgroundsPath))) {
|
||||
await plugin.app.vault.adapter.mkdir(backgroundsPath);
|
||||
console.debug(`Theme Engine: Created global backgrounds folder at ${backgroundsPath}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Theme Engine: Failed to create backgrounds folder on startup.', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const removeBackgroundMediaByPath = async (
|
||||
plugin: ThemeEngine,
|
||||
pathToDelete: string,
|
||||
): Promise<void> => {
|
||||
if (!pathToDelete) return;
|
||||
|
||||
const varsToRestore = [
|
||||
'--background-primary',
|
||||
'--background-secondary',
|
||||
'--background-modifier-border',
|
||||
'--titlebar-background-focused',
|
||||
'--background-modifier-hover',
|
||||
];
|
||||
|
||||
let settingsChanged = false;
|
||||
|
||||
for (const profileName in plugin.settings.profiles) {
|
||||
const profile = plugin.settings.profiles[profileName];
|
||||
|
||||
if (profile.backgroundPath === pathToDelete) {
|
||||
profile.backgroundPath = '';
|
||||
profile.backgroundType = undefined;
|
||||
profile.backgroundEnabled = false;
|
||||
|
||||
const profileOriginalVars =
|
||||
BUILT_IN_PROFILES_VARS[profileName as keyof typeof BUILT_IN_PROFILES_VARS] ||
|
||||
BUILT_IN_PROFILES_VARS.Default;
|
||||
|
||||
for (const varName of varsToRestore) {
|
||||
if (profile.vars[varName] === 'transparent') {
|
||||
const defaultValue = profileOriginalVars[varName as keyof typeof profileOriginalVars];
|
||||
|
||||
if (defaultValue) {
|
||||
profile.vars[varName] = defaultValue;
|
||||
} else {
|
||||
delete profile.vars[varName];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
settingsChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (await plugin.app.vault.adapter.exists(pathToDelete)) {
|
||||
await plugin.app.vault.adapter.remove(pathToDelete);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Theme Engine: Could not delete background file '${pathToDelete}'.`, error);
|
||||
}
|
||||
|
||||
if (settingsChanged) {
|
||||
await plugin.saveSettings();
|
||||
|
||||
const activeProfile = plugin.settings.profiles[plugin.settings.activeProfile];
|
||||
if (activeProfile.backgroundPath === '') {
|
||||
for (const varName of varsToRestore) {
|
||||
const value = activeProfile.vars[varName];
|
||||
if (value) plugin.pendingVarUpdates[varName] = value;
|
||||
}
|
||||
plugin.applyPendingNow();
|
||||
}
|
||||
}
|
||||
|
||||
if (plugin.settingTabInstance) {
|
||||
plugin.settingTabInstance.display();
|
||||
}
|
||||
};
|
||||
|
||||
export const setBackgroundMedia = async (
|
||||
plugin: ThemeEngine,
|
||||
arrayBuffer: ArrayBuffer,
|
||||
fileName: string,
|
||||
conflictChoice: ConflictChoice = 'prompt',
|
||||
): Promise<void> => {
|
||||
const activeProfile = plugin.settings.profiles?.[plugin.settings.activeProfile];
|
||||
if (!activeProfile) return;
|
||||
|
||||
const { arrayBuffer: finalArrayBuffer, fileName: finalFileName } = await maybeConvertToJpg(
|
||||
activeProfile,
|
||||
arrayBuffer,
|
||||
fileName,
|
||||
);
|
||||
|
||||
const fileExt = finalFileName.split('.').pop()?.toLowerCase();
|
||||
const mediaType: MediaType = fileExt === 'mp4' || fileExt === 'webm' ? 'video' : 'image';
|
||||
|
||||
const backgroundsPath = `${plugin.app.vault.configDir}/backgrounds`;
|
||||
let targetPath = `${backgroundsPath}/${finalFileName}`;
|
||||
|
||||
try {
|
||||
if (!(await plugin.app.vault.adapter.exists(backgroundsPath))) {
|
||||
await plugin.app.vault.adapter.mkdir(backgroundsPath);
|
||||
}
|
||||
|
||||
const fileExists = await plugin.app.vault.adapter.exists(targetPath);
|
||||
|
||||
if (fileExists && conflictChoice === 'prompt') {
|
||||
new FileConflictModal(plugin.app, plugin, finalArrayBuffer, finalFileName, (choice) => {
|
||||
void setBackgroundMedia(plugin, finalArrayBuffer, finalFileName, choice).catch((error) => {
|
||||
console.error('Failed to set background media:', error);
|
||||
});
|
||||
}).open();
|
||||
return;
|
||||
}
|
||||
|
||||
if (fileExists && conflictChoice === 'keep') {
|
||||
targetPath = await findNextAvailablePath(plugin.app.vault.adapter, targetPath);
|
||||
}
|
||||
if (fileExists && conflictChoice === 'replace') {
|
||||
await plugin.app.vault.adapter.remove(targetPath);
|
||||
}
|
||||
|
||||
await plugin.app.vault.createBinary(targetPath, finalArrayBuffer);
|
||||
|
||||
const oldImagePath = activeProfile.backgroundPath;
|
||||
|
||||
activeProfile.backgroundPath = targetPath;
|
||||
activeProfile.backgroundType = mediaType;
|
||||
|
||||
await plugin.saveSettings();
|
||||
new Notice(t('notices.bgSet'));
|
||||
|
||||
if (oldImagePath && oldImagePath !== targetPath && conflictChoice === 'replace') {
|
||||
if (await plugin.app.vault.adapter.exists(oldImagePath)) {
|
||||
await plugin.app.vault.adapter.remove(oldImagePath);
|
||||
}
|
||||
}
|
||||
|
||||
if (plugin.settingTabInstance) {
|
||||
plugin.settingTabInstance.display();
|
||||
}
|
||||
} catch (error) {
|
||||
new Notice(t('notices.backgroundLoadError'));
|
||||
console.error('Theme Engine: Error setting background media:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const setBackgroundMediaFromUrl = async (
|
||||
plugin: ThemeEngine,
|
||||
url: string,
|
||||
): Promise<void> => {
|
||||
if (!url) {
|
||||
new Notice(t('notices.backgroundUrlLoadError'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await requestUrl({ url });
|
||||
const arrayBuffer = response.arrayBuffer;
|
||||
|
||||
let fileName = url.split('/').pop();
|
||||
if (fileName) {
|
||||
fileName = fileName.split('?')[0];
|
||||
}
|
||||
|
||||
if (!fileName || fileName.indexOf('.') === -1 || fileName.length > 50) {
|
||||
const extension = response.headers['content-type']?.split('/')[1] || 'png';
|
||||
fileName = `image-${Date.now()}.${extension}`;
|
||||
}
|
||||
|
||||
await setBackgroundMedia(plugin, arrayBuffer, fileName, 'prompt');
|
||||
} catch (error) {
|
||||
new Notice(t('notices.backgroundUrlLoadError'));
|
||||
console.error('Theme Engine: Error fetching image from URL:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const selectBackgroundMedia = async (
|
||||
plugin: ThemeEngine,
|
||||
newPath: string,
|
||||
mediaType: MediaType,
|
||||
): Promise<void> => {
|
||||
const activeProfile = plugin.settings.profiles?.[plugin.settings.activeProfile];
|
||||
if (!activeProfile) return;
|
||||
|
||||
activeProfile.backgroundPath = newPath;
|
||||
activeProfile.backgroundType = mediaType;
|
||||
activeProfile.backgroundEnabled = true;
|
||||
|
||||
await plugin.saveSettings();
|
||||
new Notice(t('notices.bgSet'));
|
||||
};
|
||||
|
||||
export const renameBackgroundMedia = async (
|
||||
plugin: ThemeEngine,
|
||||
oldPath: string,
|
||||
newFullName: string,
|
||||
): Promise<string | false> => {
|
||||
const adapter = plugin.app.vault.adapter;
|
||||
|
||||
if (!newFullName || newFullName.includes('/') || newFullName.includes('\\')) {
|
||||
new Notice(t('notices.invalidFilename'));
|
||||
return false;
|
||||
}
|
||||
|
||||
const pathParts = oldPath.split('/');
|
||||
const originalFileName = pathParts.pop() || '';
|
||||
const folderPath = pathParts.join('/');
|
||||
const newPath = `${folderPath}/${newFullName}`;
|
||||
const oldExtMatch = originalFileName.match(/\.([a-zA-Z0-9]+)$/);
|
||||
const oldExt = oldExtMatch ? oldExtMatch[0] : '';
|
||||
|
||||
if (oldExt && !newFullName.toLowerCase().endsWith(oldExt.toLowerCase())) {
|
||||
console.warn(
|
||||
`Theme Engine: Rename blocked. Attempted to change extension from "${oldExt}" in "${newFullName}".`,
|
||||
);
|
||||
new Notice(t('notices.invalidFilename') + ' (Extension mismatch)');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (await adapter.exists(newPath)) {
|
||||
if (oldPath.toLowerCase() !== newPath.toLowerCase()) {
|
||||
new Notice(t('notices.filenameExists', newFullName));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await adapter.rename(oldPath, newPath);
|
||||
|
||||
let settingsChanged = false;
|
||||
for (const profileName in plugin.settings.profiles) {
|
||||
const profile = plugin.settings.profiles[profileName];
|
||||
if (profile && profile.backgroundPath === oldPath) {
|
||||
profile.backgroundPath = newPath;
|
||||
settingsChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (settingsChanged) {
|
||||
await plugin.saveSettings();
|
||||
}
|
||||
|
||||
new Notice(t('notices.renameSuccess', newFullName));
|
||||
return newPath;
|
||||
} catch (error) {
|
||||
new Notice(t('notices.renameError'));
|
||||
console.error('Theme Engine: Error renaming background image:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
242
src/core/migrations.ts
Normal file
242
src/core/migrations.ts
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
import { PLUGIN_COMMAND_SUFFIXES } from '../commands';
|
||||
import type ThemeEngine from '../main';
|
||||
|
||||
type HotkeyEntry = { modifiers: string[]; key: string };
|
||||
|
||||
export const LEGACY_PLUGIN_IDS = [
|
||||
'obsidian-theme-engine',
|
||||
'color-master',
|
||||
'obsidian-color-master',
|
||||
] as const;
|
||||
const LEGACY_COMMAND_PLUGIN_IDS = [
|
||||
'obsidian-theme-engine',
|
||||
'color-master',
|
||||
'obsidian-color-master',
|
||||
] as const;
|
||||
export const CURRENT_PLUGIN_ID = 'theme-engine';
|
||||
|
||||
const isObjectRecord = (value: unknown): value is Record<string, unknown> => {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
};
|
||||
|
||||
const normalizeHotkeyArray = (value: unknown): HotkeyEntry[] => {
|
||||
const normalized: HotkeyEntry[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
if (!Array.isArray(value)) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
for (const item of value) {
|
||||
if (!isObjectRecord(item)) continue;
|
||||
|
||||
const keyRaw = item.key;
|
||||
if (typeof keyRaw !== 'string' || keyRaw.trim() === '') continue;
|
||||
|
||||
const modifiersRaw = item.modifiers;
|
||||
const modifiers = Array.isArray(modifiersRaw)
|
||||
? modifiersRaw.filter((modifier): modifier is string => typeof modifier === 'string')
|
||||
: [];
|
||||
|
||||
const entry = { modifiers, key: keyRaw };
|
||||
const signature = JSON.stringify(entry);
|
||||
|
||||
if (seen.has(signature)) continue;
|
||||
seen.add(signature);
|
||||
normalized.push(entry);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
};
|
||||
|
||||
const mergeHotkeyArrays = (currentValue: unknown, legacyValue: unknown): HotkeyEntry[] => {
|
||||
const merged = [...normalizeHotkeyArray(currentValue), ...normalizeHotkeyArray(legacyValue)];
|
||||
const deduped: HotkeyEntry[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const entry of merged) {
|
||||
const signature = JSON.stringify(entry);
|
||||
if (seen.has(signature)) continue;
|
||||
seen.add(signature);
|
||||
deduped.push(entry);
|
||||
}
|
||||
|
||||
return deduped;
|
||||
};
|
||||
|
||||
const writeJsonWithBackup = async (
|
||||
plugin: ThemeEngine,
|
||||
path: string,
|
||||
backupPath: string,
|
||||
data: unknown,
|
||||
): Promise<void> => {
|
||||
const adapter = plugin.app.vault.adapter;
|
||||
|
||||
if (!(await adapter.exists(backupPath)) && (await adapter.exists(path))) {
|
||||
const currentRaw = await adapter.read(path);
|
||||
await adapter.write(backupPath, currentRaw);
|
||||
}
|
||||
|
||||
await adapter.write(path, JSON.stringify(data, null, 2));
|
||||
};
|
||||
|
||||
export const migrateLegacyPluginDataIfNeeded = async (
|
||||
plugin: ThemeEngine,
|
||||
): Promise<{
|
||||
migrated: boolean;
|
||||
sourcePath?: string;
|
||||
}> => {
|
||||
let currentData: unknown = null;
|
||||
|
||||
try {
|
||||
currentData = await plugin.loadData();
|
||||
} catch (error) {
|
||||
console.warn('Theme Engine: Failed to read current plugin data before migration.', error);
|
||||
}
|
||||
|
||||
if (isObjectRecord(currentData) && Object.keys(currentData).length > 0) {
|
||||
return { migrated: false };
|
||||
}
|
||||
|
||||
const adapter = plugin.app.vault.adapter;
|
||||
const pluginsDir = `${plugin.app.vault.configDir}/plugins`;
|
||||
|
||||
for (const legacyPluginId of LEGACY_PLUGIN_IDS) {
|
||||
const sourcePath = `${pluginsDir}/${legacyPluginId}/data.json`;
|
||||
|
||||
try {
|
||||
if (!(await adapter.exists(sourcePath))) continue;
|
||||
|
||||
const raw = await adapter.read(sourcePath);
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (!isObjectRecord(parsed)) {
|
||||
console.warn(
|
||||
`Theme Engine: Legacy data at "${sourcePath}" is not an object. Skipping migration source.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const migratedSettings: Record<string, unknown> = { ...parsed };
|
||||
migratedSettings.idMigration = {
|
||||
from: legacyPluginId,
|
||||
to: CURRENT_PLUGIN_ID,
|
||||
at: new Date().toISOString(),
|
||||
sourcePath,
|
||||
};
|
||||
|
||||
await plugin.saveData(migratedSettings);
|
||||
console.debug(`Theme Engine: Migrated settings data from "${sourcePath}".`);
|
||||
return { migrated: true, sourcePath };
|
||||
} catch (error) {
|
||||
console.warn(`Theme Engine: Failed to migrate data from "${sourcePath}".`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return { migrated: false };
|
||||
};
|
||||
|
||||
export const migrateLegacyHotkeysIfNeeded = async (plugin: ThemeEngine): Promise<boolean> => {
|
||||
const adapter = plugin.app.vault.adapter;
|
||||
const configDir = plugin.app.vault.configDir;
|
||||
const hotkeysPath = `${configDir}/hotkeys.json`;
|
||||
const backupPath = `${configDir}/hotkeys.${CURRENT_PLUGIN_ID}-id-migration.bak.json`;
|
||||
|
||||
try {
|
||||
if (!(await adapter.exists(hotkeysPath))) return false;
|
||||
|
||||
const raw = await adapter.read(hotkeysPath);
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (!isObjectRecord(parsed)) return false;
|
||||
|
||||
const hotkeyMap = parsed;
|
||||
let changed = false;
|
||||
|
||||
for (const suffix of PLUGIN_COMMAND_SUFFIXES) {
|
||||
const newCommandId = `${CURRENT_PLUGIN_ID}:${suffix}`;
|
||||
const currentValue = hotkeyMap[newCommandId];
|
||||
const normalizedCurrent = normalizeHotkeyArray(currentValue);
|
||||
let merged: HotkeyEntry[] = normalizedCurrent;
|
||||
|
||||
for (const legacyCommandPluginId of LEGACY_COMMAND_PLUGIN_IDS) {
|
||||
const oldCommandId = `${legacyCommandPluginId}:${suffix}`;
|
||||
const legacyValue = hotkeyMap[oldCommandId];
|
||||
|
||||
if (typeof legacyValue === 'undefined') continue;
|
||||
|
||||
merged = mergeHotkeyArrays(merged, legacyValue);
|
||||
delete hotkeyMap[oldCommandId];
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (JSON.stringify(normalizedCurrent) !== JSON.stringify(merged)) {
|
||||
hotkeyMap[newCommandId] = merged;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed) return false;
|
||||
|
||||
await writeJsonWithBackup(plugin, hotkeysPath, backupPath, hotkeyMap);
|
||||
console.debug(
|
||||
`Theme Engine: Migrated hotkeys from legacy namespaces to "${CURRENT_PLUGIN_ID}:*".`,
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('Theme Engine: Failed to migrate hotkeys.', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const migrateCommunityPluginListIfNeeded = async (plugin: ThemeEngine): Promise<boolean> => {
|
||||
const adapter = plugin.app.vault.adapter;
|
||||
const configDir = plugin.app.vault.configDir;
|
||||
const listPath = `${configDir}/community-plugins.json`;
|
||||
const backupPath = `${configDir}/community-plugins.${CURRENT_PLUGIN_ID}-id-migration.bak.json`;
|
||||
|
||||
try {
|
||||
if (!(await adapter.exists(listPath))) return false;
|
||||
|
||||
const raw = await adapter.read(listPath);
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (!Array.isArray(parsed)) return false;
|
||||
|
||||
const entries = parsed.filter((entry): entry is string => typeof entry === 'string');
|
||||
let changed = entries.length !== parsed.length;
|
||||
const updatedEntries: string[] = [];
|
||||
let hasCurrentId = false;
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry === CURRENT_PLUGIN_ID) {
|
||||
if (!hasCurrentId) {
|
||||
updatedEntries.push(entry);
|
||||
hasCurrentId = true;
|
||||
} else {
|
||||
changed = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (LEGACY_PLUGIN_IDS.some((legacyId) => legacyId === entry)) {
|
||||
if (!hasCurrentId) {
|
||||
updatedEntries.push(CURRENT_PLUGIN_ID);
|
||||
hasCurrentId = true;
|
||||
}
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
updatedEntries.push(entry);
|
||||
}
|
||||
|
||||
if (!changed) return false;
|
||||
|
||||
await writeJsonWithBackup(plugin, listPath, backupPath, updatedEntries);
|
||||
console.debug(
|
||||
`Theme Engine: Migrated community plugin list entry from legacy IDs to "${CURRENT_PLUGIN_ID}".`,
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('Theme Engine: Failed to migrate community plugin list.', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
232
src/core/notice-engine.ts
Normal file
232
src/core/notice-engine.ts
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
import type ThemeEngine from '../main';
|
||||
import type { NoticeRule } from '../types';
|
||||
|
||||
const matchesRule = (noticeText: string, rule: NoticeRule): boolean => {
|
||||
if (!rule.keywords?.trim()) return false;
|
||||
const keywords = rule.keywords.toLowerCase();
|
||||
let match = false;
|
||||
|
||||
if (rule.isRegex) {
|
||||
try {
|
||||
if (new RegExp(keywords, 'i').test(noticeText)) match = true;
|
||||
} catch {
|
||||
// ignore invalid regex
|
||||
}
|
||||
} else {
|
||||
const keywordArray = keywords
|
||||
.split(',')
|
||||
.map((k: string) => k.trim())
|
||||
.filter(Boolean);
|
||||
if (keywordArray.length > 0) {
|
||||
const escaped = keywordArray.map((k: string) => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
||||
if (new RegExp(`\\b(${escaped.join('|')})\\b`, 'i').test(noticeText)) {
|
||||
match = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return match;
|
||||
};
|
||||
|
||||
export const processNotice = (plugin: ThemeEngine, el: HTMLElement): void => {
|
||||
if (!el || !el.classList || el.dataset.cmProcessed === 'true') return;
|
||||
|
||||
let noticeText = (el.textContent || '').toLowerCase();
|
||||
const testKeywordEl = el.querySelector<HTMLElement>('.cm-test-keyword');
|
||||
if (testKeywordEl) {
|
||||
noticeText = (testKeywordEl.textContent || '').toLowerCase();
|
||||
}
|
||||
|
||||
el.dataset.cmProcessed = 'true';
|
||||
|
||||
try {
|
||||
const settings = plugin.settings;
|
||||
const activeProfile = settings.profiles[settings.activeProfile];
|
||||
if (!activeProfile) return;
|
||||
|
||||
const liveRules = plugin.liveNoticeRules;
|
||||
const liveRuleType = plugin.liveNoticeRuleType;
|
||||
|
||||
const bgRules: NoticeRule[] =
|
||||
liveRuleType === 'background' && liveRules
|
||||
? liveRules
|
||||
: activeProfile?.noticeRules?.background || [];
|
||||
|
||||
let finalBgColor = activeProfile.vars['--cm-notice-bg-default'];
|
||||
|
||||
for (const rule of bgRules) {
|
||||
if (matchesRule(noticeText, rule)) {
|
||||
finalBgColor = rule.color;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (finalBgColor) {
|
||||
el.dataset.cmNoticeBg = finalBgColor;
|
||||
}
|
||||
|
||||
const textRules: NoticeRule[] =
|
||||
liveRuleType === 'text' && liveRules ? liveRules : activeProfile?.noticeRules?.text || [];
|
||||
|
||||
let finalTextColor = activeProfile.vars['--cm-notice-text-default'];
|
||||
const highlightRules: NoticeRule[] = [];
|
||||
const fullColorRules: NoticeRule[] = [];
|
||||
|
||||
for (const rule of textRules) {
|
||||
if (rule.highlightOnly) {
|
||||
highlightRules.push(rule);
|
||||
} else {
|
||||
fullColorRules.push(rule);
|
||||
}
|
||||
}
|
||||
|
||||
for (const rule of fullColorRules) {
|
||||
if (matchesRule(noticeText, rule)) {
|
||||
finalTextColor = rule.color;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (finalTextColor) {
|
||||
el.dataset.cmNoticeText = finalTextColor;
|
||||
}
|
||||
|
||||
if (highlightRules.length > 0) {
|
||||
const textNodes: Node[] = [];
|
||||
|
||||
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, {
|
||||
acceptNode: (node) => {
|
||||
return node.parentElement?.classList.contains('cm-keyword-highlight')
|
||||
? NodeFilter.FILTER_REJECT
|
||||
: NodeFilter.FILTER_ACCEPT;
|
||||
},
|
||||
});
|
||||
|
||||
while (walker.nextNode()) textNodes.push(walker.currentNode);
|
||||
|
||||
for (const node of textNodes) {
|
||||
const nodeContent = node.textContent;
|
||||
if (!nodeContent?.trim()) continue;
|
||||
|
||||
const parent = node.parentElement;
|
||||
if (!parent) continue;
|
||||
|
||||
const allMatches: { start: number; end: number; color: string }[] = [];
|
||||
|
||||
for (const rule of highlightRules) {
|
||||
if (!rule.keywords?.trim()) continue;
|
||||
const color = rule.color;
|
||||
|
||||
if (rule.isRegex) {
|
||||
try {
|
||||
const regex = new RegExp(rule.keywords, 'gi');
|
||||
let match;
|
||||
while ((match = regex.exec(nodeContent)) !== null) {
|
||||
if (match[0].length === 0) break;
|
||||
allMatches.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length,
|
||||
color,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Theme Engine: Invalid Regex in notice rule', error);
|
||||
}
|
||||
} else {
|
||||
const keywords = rule.keywords
|
||||
.split(',')
|
||||
.map((k: string) => k.trim())
|
||||
.filter(Boolean);
|
||||
if (keywords.length > 0) {
|
||||
const escaped = keywords.map((k: string) => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
||||
const ruleRegex = new RegExp(`\\b(${escaped.join('|')})\\b`, 'gi');
|
||||
let match;
|
||||
while ((match = ruleRegex.exec(nodeContent)) !== null) {
|
||||
allMatches.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length,
|
||||
color,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allMatches.length === 0) continue;
|
||||
|
||||
allMatches.sort((a, b) => a.start - b.start);
|
||||
const uniqueMatches = [];
|
||||
let currentEnd = -1;
|
||||
|
||||
for (const match of allMatches) {
|
||||
if (match.start >= currentEnd) {
|
||||
uniqueMatches.push(match);
|
||||
currentEnd = match.end;
|
||||
}
|
||||
}
|
||||
|
||||
if (uniqueMatches.length === 0) continue;
|
||||
|
||||
const fragments = new DocumentFragment();
|
||||
let lastIndex = 0;
|
||||
|
||||
for (const match of uniqueMatches) {
|
||||
if (match.start > lastIndex) {
|
||||
fragments.appendChild(
|
||||
document.createTextNode(nodeContent.substring(lastIndex, match.start)),
|
||||
);
|
||||
}
|
||||
const span = document.createElement('span');
|
||||
span.className = 'cm-keyword-highlight';
|
||||
span.setCssProps({ color: match.color });
|
||||
span.textContent = nodeContent.substring(match.start, match.end);
|
||||
fragments.appendChild(span);
|
||||
lastIndex = match.end;
|
||||
}
|
||||
|
||||
if (lastIndex < nodeContent.length) {
|
||||
fragments.appendChild(document.createTextNode(nodeContent.substring(lastIndex)));
|
||||
}
|
||||
|
||||
parent.replaceChild(fragments, node);
|
||||
}
|
||||
}
|
||||
|
||||
updateNoticeStyles(plugin);
|
||||
} catch (error) {
|
||||
console.warn('Theme Engine: processNotice failed', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateNoticeStyles = (plugin: ThemeEngine): void => {
|
||||
const styleId = 'cm-dynamic-notice-styles';
|
||||
|
||||
const notices = document.querySelectorAll<HTMLElement>(
|
||||
'[data-cm-notice-bg], [data-cm-notice-text]',
|
||||
);
|
||||
if (notices.length === 0) {
|
||||
plugin.clearRuntimeStyle(styleId);
|
||||
return;
|
||||
}
|
||||
|
||||
const cssRules: string[] = [];
|
||||
notices.forEach((notice, i) => {
|
||||
const uniqueId = notice.dataset.cmNoticeId || `cm-notice-${i}`;
|
||||
notice.dataset.cmNoticeId = uniqueId;
|
||||
|
||||
const bgColor = notice.dataset.cmNoticeBg;
|
||||
const textColor = notice.dataset.cmNoticeText;
|
||||
|
||||
let rule = `[data-cm-notice-id="${uniqueId}"] {`;
|
||||
if (bgColor) {
|
||||
rule += ` background-color: ${bgColor} !important;`;
|
||||
}
|
||||
if (textColor) {
|
||||
rule += ` color: ${textColor} !important;`;
|
||||
}
|
||||
rule += ' }';
|
||||
cssRules.push(rule);
|
||||
});
|
||||
|
||||
plugin.setRuntimeStyle(styleId, cssRules.join('\n'));
|
||||
};
|
||||
133
src/core/profile-state.ts
Normal file
133
src/core/profile-state.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import { Notice } from 'obsidian';
|
||||
import { t } from '../i18n/strings';
|
||||
import type ThemeEngine from '../main';
|
||||
import type { NoticeRule, Profile, Snippet } from '../types';
|
||||
|
||||
type LegacyPinnedSnapshot = {
|
||||
vars?: Record<string, string>;
|
||||
customCss?: string;
|
||||
snippets?: Snippet[];
|
||||
noticeRules?: {
|
||||
text: NoticeRule[];
|
||||
background: NoticeRule[];
|
||||
};
|
||||
};
|
||||
|
||||
export const pinProfileSnapshot = async (
|
||||
plugin: ThemeEngine,
|
||||
profileName: string,
|
||||
): Promise<void> => {
|
||||
const targetProfile = profileName || plugin.settings.activeProfile;
|
||||
plugin.settings.pinnedSnapshots = plugin.settings.pinnedSnapshots || {};
|
||||
const profile = plugin.settings.profiles?.[targetProfile];
|
||||
if (!profile) {
|
||||
new Notice(t('notices.profileNotFound'));
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshotProfile: Profile = JSON.parse(
|
||||
JSON.stringify({
|
||||
...profile,
|
||||
vars: profile.vars || {},
|
||||
themeType: profile.themeType || 'auto',
|
||||
snippets: Array.isArray(profile.snippets) ? profile.snippets : [],
|
||||
customCss: profile.customCss || '',
|
||||
noticeRules: profile.noticeRules || { text: [], background: [] },
|
||||
}),
|
||||
);
|
||||
|
||||
plugin.settings.pinnedSnapshots[targetProfile] = {
|
||||
pinnedAt: new Date().toISOString(),
|
||||
profile: snapshotProfile,
|
||||
};
|
||||
await plugin.saveSettings();
|
||||
};
|
||||
|
||||
export const resetProfileToPinned = async (
|
||||
plugin: ThemeEngine,
|
||||
profileName: string,
|
||||
): Promise<void> => {
|
||||
const targetProfile = profileName || plugin.settings.activeProfile;
|
||||
const snap = plugin.settings.pinnedSnapshots?.[targetProfile];
|
||||
if (!snap) {
|
||||
new Notice(t('notices.noPinnedSnapshot'));
|
||||
return;
|
||||
}
|
||||
|
||||
const activeProfile = plugin.settings.profiles[targetProfile];
|
||||
if (!activeProfile) {
|
||||
new Notice(t('notices.profileNotFound'));
|
||||
return;
|
||||
}
|
||||
|
||||
const legacySnap = snap as LegacyPinnedSnapshot;
|
||||
const pinnedProfile: Profile | null = snap.profile
|
||||
? JSON.parse(JSON.stringify(snap.profile))
|
||||
: legacySnap.vars
|
||||
? {
|
||||
vars: JSON.parse(JSON.stringify(legacySnap.vars)),
|
||||
themeType: activeProfile.themeType || 'auto',
|
||||
snippets: Array.isArray(legacySnap.snippets)
|
||||
? JSON.parse(JSON.stringify(legacySnap.snippets))
|
||||
: [],
|
||||
customCss: legacySnap.customCss || '',
|
||||
noticeRules: legacySnap.noticeRules
|
||||
? JSON.parse(JSON.stringify(legacySnap.noticeRules))
|
||||
: { text: [], background: [] },
|
||||
}
|
||||
: null;
|
||||
|
||||
if (!pinnedProfile || !pinnedProfile.vars) {
|
||||
new Notice(t('notices.noPinnedSnapshot'));
|
||||
return;
|
||||
}
|
||||
|
||||
activeProfile.vars = pinnedProfile.vars;
|
||||
activeProfile.customCss = pinnedProfile.customCss || '';
|
||||
activeProfile.snippets = Array.isArray(pinnedProfile.snippets) ? pinnedProfile.snippets : [];
|
||||
activeProfile.noticeRules = pinnedProfile.noticeRules || { text: [], background: [] };
|
||||
|
||||
Object.keys(pinnedProfile.vars).forEach((key) => {
|
||||
plugin.pendingVarUpdates[key] = pinnedProfile.vars[key];
|
||||
});
|
||||
|
||||
await plugin.saveSettings();
|
||||
plugin.applyPendingNow();
|
||||
};
|
||||
|
||||
export const pushCssHistory = (plugin: ThemeEngine, id: string, content: string): void => {
|
||||
if (!plugin.cssHistory[id]) {
|
||||
plugin.cssHistory[id] = { undoStack: [], redoStack: [] };
|
||||
}
|
||||
const history = plugin.cssHistory[id];
|
||||
const lastState = history.undoStack[history.undoStack.length - 1];
|
||||
|
||||
if (lastState !== content) {
|
||||
history.undoStack.push(content);
|
||||
history.redoStack = [];
|
||||
}
|
||||
};
|
||||
|
||||
export const undoCssHistory = (plugin: ThemeEngine, id: string): string | null => {
|
||||
const history = plugin.cssHistory[id];
|
||||
if (history && history.undoStack.length > 1) {
|
||||
const currentState = history.undoStack.pop();
|
||||
if (typeof currentState === 'string') {
|
||||
history.redoStack.push(currentState);
|
||||
}
|
||||
return history.undoStack[history.undoStack.length - 1];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const redoCssHistory = (plugin: ThemeEngine, id: string): string | null => {
|
||||
const history = plugin.cssHistory[id];
|
||||
if (history && history.redoStack.length > 0) {
|
||||
const nextState = history.redoStack.pop();
|
||||
if (typeof nextState === 'string') {
|
||||
history.undoStack.push(nextState);
|
||||
return nextState;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
270
src/core/settings-store.ts
Normal file
270
src/core/settings-store.ts
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
import { ButtonComponent, Notice } from 'obsidian';
|
||||
import { DEFAULT_SETTINGS, DEFAULT_VARS } from '../constants';
|
||||
import { t } from '../i18n/strings';
|
||||
import type ThemeEngine from '../main';
|
||||
import type { PluginSettings } from '../types';
|
||||
import { convertColorToHex, flattenVars } from '../utils';
|
||||
|
||||
type AppWithCommands = {
|
||||
commands: {
|
||||
executeCommandById: (commandId: string) => void;
|
||||
};
|
||||
};
|
||||
|
||||
type LegacySnippetData = {
|
||||
css?: string;
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export const loadSettings = async (plugin: ThemeEngine): Promise<void> => {
|
||||
plugin.settings = Object.assign({}, DEFAULT_SETTINGS, await plugin.loadData());
|
||||
|
||||
let migrationNeeded = false;
|
||||
if (plugin.settings.noticeRules && Object.keys(plugin.settings.profiles || {}).length > 0) {
|
||||
console.debug('Theme Engine: Detected old global notice rules. Starting migration...');
|
||||
for (const profileName in plugin.settings.profiles) {
|
||||
const profile = plugin.settings.profiles[profileName];
|
||||
if (!profile.noticeRules) {
|
||||
profile.noticeRules = JSON.parse(JSON.stringify(plugin.settings.noticeRules));
|
||||
}
|
||||
}
|
||||
delete plugin.settings.noticeRules;
|
||||
migrationNeeded = true;
|
||||
}
|
||||
|
||||
for (const profileName in plugin.settings.profiles) {
|
||||
const profile = plugin.settings.profiles[profileName];
|
||||
if (!profile.noticeRules) {
|
||||
profile.noticeRules = { text: [], background: [] };
|
||||
migrationNeeded = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (migrationNeeded) {
|
||||
console.debug('Theme Engine: Notice rules migration complete. Saving new settings structure.');
|
||||
await plugin.saveData(plugin.settings);
|
||||
}
|
||||
|
||||
if (!plugin.settings.installDate) {
|
||||
plugin.settings.installDate = new Date().toISOString();
|
||||
|
||||
const isDarkMode = document.body.classList.contains('theme-dark');
|
||||
if (isDarkMode) {
|
||||
plugin.settings.activeProfile = 'Default';
|
||||
} else {
|
||||
plugin.settings.activeProfile = 'Citrus Zest';
|
||||
}
|
||||
|
||||
await plugin.saveData(plugin.settings);
|
||||
}
|
||||
if (!plugin.settings.pinnedSnapshots) {
|
||||
plugin.settings.pinnedSnapshots = {};
|
||||
}
|
||||
|
||||
let snippetsMigrationNeeded = false;
|
||||
for (const profileName in plugin.settings.profiles) {
|
||||
const profile = plugin.settings.profiles[profileName];
|
||||
if (
|
||||
profile &&
|
||||
profile.snippets &&
|
||||
!Array.isArray(profile.snippets) &&
|
||||
typeof profile.snippets === 'object'
|
||||
) {
|
||||
snippetsMigrationNeeded = true;
|
||||
const legacySnippets = profile.snippets as unknown as Record<string, LegacySnippetData>;
|
||||
const snippetsArray = Object.entries(legacySnippets).map(([name, data]) => ({
|
||||
id: `snippet-${Date.now()}-${Math.random()}`,
|
||||
name,
|
||||
css: data.css || '',
|
||||
enabled: data.enabled !== false,
|
||||
}));
|
||||
profile.snippets = snippetsArray;
|
||||
}
|
||||
}
|
||||
|
||||
if (snippetsMigrationNeeded) {
|
||||
console.debug(
|
||||
'Theme Engine: The clipping data structure is being migrated to the new format (array).',
|
||||
);
|
||||
await plugin.saveData(plugin.settings);
|
||||
}
|
||||
|
||||
let profileMigrationNeeded = false;
|
||||
for (const profileName in plugin.settings.profiles) {
|
||||
const profile = plugin.settings.profiles[profileName];
|
||||
if (profile && profile.backgroundPath && typeof profile.backgroundEnabled === 'undefined') {
|
||||
profile.backgroundEnabled = true;
|
||||
profileMigrationNeeded = true;
|
||||
} else if (profile && !profile.backgroundPath && profile.backgroundEnabled === true) {
|
||||
profile.backgroundEnabled = false;
|
||||
profileMigrationNeeded = true;
|
||||
}
|
||||
}
|
||||
if (profileMigrationNeeded) {
|
||||
console.debug('Theme Engine: Setting default backgroundEnabled status for profiles.');
|
||||
await plugin.saveData(plugin.settings);
|
||||
}
|
||||
};
|
||||
|
||||
export const saveSettings = async (plugin: ThemeEngine): Promise<void> => {
|
||||
await plugin.saveData(plugin.settings);
|
||||
await plugin.applyStyles();
|
||||
await plugin.refreshOpenGraphViews();
|
||||
plugin.app.workspace.trigger('css-change');
|
||||
};
|
||||
|
||||
export const resetPluginData = async (
|
||||
plugin: ThemeEngine,
|
||||
options: {
|
||||
deleteProfiles: boolean;
|
||||
deleteSnippets: boolean;
|
||||
deleteSettings: boolean;
|
||||
deleteBackgrounds: boolean;
|
||||
deleteLanguages?: boolean;
|
||||
},
|
||||
): Promise<void> => {
|
||||
const oldInstallDate = plugin.settings.installDate;
|
||||
|
||||
const newSettings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS)) as PluginSettings;
|
||||
|
||||
if (!options.deleteSettings) {
|
||||
newSettings.language = plugin.settings.language;
|
||||
newSettings.useRtlLayout = plugin.settings.useRtlLayout;
|
||||
newSettings.overrideIconizeColors = plugin.settings.overrideIconizeColors;
|
||||
newSettings.cleanupInterval = plugin.settings.cleanupInterval;
|
||||
newSettings.colorUpdateFPS = plugin.settings.colorUpdateFPS;
|
||||
newSettings.lastSearchQuery = plugin.settings.lastSearchQuery;
|
||||
newSettings.lastScrollPosition = plugin.settings.lastScrollPosition;
|
||||
newSettings.advancedResetOptions = plugin.settings.advancedResetOptions;
|
||||
}
|
||||
|
||||
if (!options.deleteProfiles) {
|
||||
newSettings.profiles = plugin.settings.profiles;
|
||||
newSettings.activeProfile = plugin.settings.activeProfile;
|
||||
newSettings.pinnedSnapshots = plugin.settings.pinnedSnapshots;
|
||||
}
|
||||
|
||||
if (!options.deleteSnippets) {
|
||||
newSettings.globalSnippets = plugin.settings.globalSnippets;
|
||||
}
|
||||
|
||||
if (!options.deleteLanguages) {
|
||||
newSettings.customLanguages = plugin.settings.customLanguages;
|
||||
}
|
||||
|
||||
if (options.deleteSnippets && !options.deleteProfiles) {
|
||||
for (const profileName in newSettings.profiles) {
|
||||
if (newSettings.profiles[profileName]) {
|
||||
newSettings.profiles[profileName].snippets = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newSettings.installDate = oldInstallDate;
|
||||
|
||||
if (options.deleteSettings) {
|
||||
newSettings.advancedResetOptions = plugin.settings.advancedResetOptions;
|
||||
newSettings.language = 'auto';
|
||||
}
|
||||
|
||||
plugin.settings = newSettings;
|
||||
await plugin.saveData(plugin.settings);
|
||||
|
||||
console.debug('Theme Engine: Selective data reset complete.', options);
|
||||
|
||||
if (options.deleteBackgrounds) {
|
||||
const backgroundsPath = `${plugin.app.vault.configDir}/backgrounds`;
|
||||
try {
|
||||
if (await plugin.app.vault.adapter.exists(backgroundsPath)) {
|
||||
console.debug('Theme Engine: Deleting backgrounds folder...');
|
||||
await plugin.app.vault.adapter.rmdir(backgroundsPath, true);
|
||||
}
|
||||
|
||||
if (!options.deleteProfiles) {
|
||||
for (const profileName in plugin.settings.profiles) {
|
||||
const profile = plugin.settings.profiles[profileName];
|
||||
if (profile) {
|
||||
profile.backgroundPath = undefined;
|
||||
profile.backgroundType = undefined;
|
||||
}
|
||||
}
|
||||
await plugin.saveData(plugin.settings);
|
||||
}
|
||||
} catch (folderError) {
|
||||
const errorMessage = folderError instanceof Error ? folderError.message : String(folderError);
|
||||
console.error(`Theme Engine: Error deleting backgrounds: ${errorMessage}`);
|
||||
new Notice(t('notices.deleteBackgroundsError', errorMessage));
|
||||
}
|
||||
}
|
||||
|
||||
const notice = new Notice(t('notices.resetSuccess'), 15000);
|
||||
const buttonContainer = notice.messageEl.createDiv({
|
||||
cls: 'modal-button-container',
|
||||
});
|
||||
new ButtonComponent(buttonContainer)
|
||||
.setButtonText(t('buttons.reload'))
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
const appWithCommands = plugin.app as typeof plugin.app & AppWithCommands;
|
||||
appWithCommands.commands.executeCommandById('app:reload');
|
||||
});
|
||||
};
|
||||
|
||||
export const captureCurrentComputedVars = async (
|
||||
plugin: ThemeEngine,
|
||||
): Promise<Record<string, string>> => {
|
||||
console.debug('Theme Engine: Capturing current computed styles...');
|
||||
|
||||
plugin.clearStyles();
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
|
||||
const capturedVars: Record<string, string> = {};
|
||||
const flatDefaultVars = flattenVars(DEFAULT_VARS);
|
||||
const allVarKeys = Object.keys(flatDefaultVars);
|
||||
const bodyStyles = getComputedStyle(document.body);
|
||||
|
||||
const isColorRegex =
|
||||
/^(#|rgb|hsl|transparent|var\(--)|(white|black|red|blue|green|yellow|orange|purple|cyan|magenta)$/i;
|
||||
|
||||
for (const varName of allVarKeys) {
|
||||
const computedValue = bodyStyles.getPropertyValue(varName).trim();
|
||||
const defaultValue = flatDefaultVars[varName] || '';
|
||||
|
||||
if (computedValue) {
|
||||
if (isColorRegex.test(defaultValue)) {
|
||||
try {
|
||||
capturedVars[varName] = convertColorToHex(computedValue);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to convert color ${varName}: ${computedValue}`, error);
|
||||
capturedVars[varName] = computedValue;
|
||||
}
|
||||
} else {
|
||||
capturedVars[varName] = computedValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.debug(`Theme Engine: Captured ${Object.keys(capturedVars).length} variables.`);
|
||||
|
||||
void plugin.applyStyles();
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
|
||||
return capturedVars;
|
||||
};
|
||||
|
||||
export const getThemeDefaults = async (plugin: ThemeEngine): Promise<Record<string, string>> => {
|
||||
const currentThemeMode = document.body.classList.contains('theme-dark') ? 'dark' : 'light';
|
||||
|
||||
if (plugin.cachedThemeDefaults && plugin.lastCachedThemeMode === currentThemeMode) {
|
||||
return plugin.cachedThemeDefaults;
|
||||
}
|
||||
|
||||
console.debug('Theme Engine: Cache miss or theme change. Refreshing defaults...');
|
||||
|
||||
const newDefaults = await captureCurrentComputedVars(plugin);
|
||||
|
||||
plugin.cachedThemeDefaults = newDefaults;
|
||||
plugin.lastCachedThemeMode = currentThemeMode;
|
||||
|
||||
return newDefaults;
|
||||
};
|
||||
324
src/core/style-engine.ts
Normal file
324
src/core/style-engine.ts
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
import { DEFAULT_VARS } from '../constants';
|
||||
import type ThemeEngine from '../main';
|
||||
import { flattenVars, isIconizeEnabled } from '../utils';
|
||||
|
||||
type StylableElement = Element & {
|
||||
setCssProps: (props: Record<string, string | null>) => void;
|
||||
};
|
||||
|
||||
type VaultWithConfigGetter = {
|
||||
getConfig: (key: string) => string;
|
||||
};
|
||||
|
||||
type ReloadableView = {
|
||||
reload?: () => void;
|
||||
};
|
||||
|
||||
export const applyCustomCssForProfile = (plugin: ThemeEngine, profileName: string): void => {
|
||||
try {
|
||||
const targetProfileName = profileName || plugin.settings.activeProfile;
|
||||
const profile = plugin.settings.profiles?.[targetProfileName];
|
||||
removeInjectedCustomCss(plugin);
|
||||
|
||||
if (!profile || !profile.isCssProfile || !profile.customCss) {
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.setRuntimeStyle('cm-custom-css-for-profile', profile.customCss);
|
||||
} catch (error) {
|
||||
console.warn('applyCustomCssForProfile failed', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const removeInjectedCustomCss = (plugin: ThemeEngine): void => {
|
||||
try {
|
||||
plugin.clearRuntimeStyle('cm-custom-css-for-profile');
|
||||
const oldStyle = document.getElementById('cm-custom-css-for-profile');
|
||||
if (oldStyle) oldStyle.remove();
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const applyCssSnippets = (plugin: ThemeEngine): void => {
|
||||
removeCssSnippets(plugin);
|
||||
const activeProfile = plugin.settings.profiles[plugin.settings.activeProfile];
|
||||
if (!activeProfile) return;
|
||||
|
||||
const globalSnippets = plugin.settings.globalSnippets || [];
|
||||
const profileSnippets = Array.isArray(activeProfile.snippets) ? activeProfile.snippets : [];
|
||||
|
||||
const allSnippets = [...globalSnippets, ...profileSnippets].filter(Boolean);
|
||||
|
||||
const enabledCss = allSnippets
|
||||
.filter((snippet) => snippet.enabled && snippet.css)
|
||||
.map((snippet) => {
|
||||
const upgradedCss = snippet.css.replace(
|
||||
/\bbody\s*(?=\{)/g,
|
||||
'body.theme-dark, body.theme-light',
|
||||
);
|
||||
return `/* Snippet: ${snippet.name} ${snippet.isGlobal ? '(Global)' : ''} */\n${upgradedCss}`;
|
||||
})
|
||||
.join('\n\n');
|
||||
|
||||
if (enabledCss) {
|
||||
plugin.setRuntimeStyle('cm-css-snippets', enabledCss);
|
||||
}
|
||||
};
|
||||
|
||||
export const removeCssSnippets = (plugin: ThemeEngine): void => {
|
||||
plugin.clearRuntimeStyle('cm-css-snippets');
|
||||
const el = document.getElementById('cm-css-snippets');
|
||||
if (el) el.remove();
|
||||
};
|
||||
|
||||
export const applyPendingNow = (plugin: ThemeEngine): void => {
|
||||
try {
|
||||
const pending = plugin.pendingVarUpdates || {};
|
||||
const keys = Object.keys(pending);
|
||||
if (keys.length === 0) {
|
||||
plugin.app.workspace.trigger('css-change');
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
const value = pending[key] ?? null;
|
||||
document.body.setCssProps({ [key]: value });
|
||||
}
|
||||
|
||||
plugin.pendingVarUpdates = {};
|
||||
|
||||
plugin.app.workspace.trigger('css-change');
|
||||
forceIconizeColors(plugin);
|
||||
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
} catch (error) {
|
||||
console.error('Theme Engine: applyPendingNow failed', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const refreshOpenGraphViews = async (plugin: ThemeEngine): Promise<void> => {
|
||||
const graphLeaves = plugin.app.workspace.getLeavesOfType('graph');
|
||||
if (graphLeaves.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug(
|
||||
`Theme Engine: Found ${graphLeaves.length} graph(s). Applying Plan C (programmatic rebuild).`,
|
||||
);
|
||||
|
||||
for (const leaf of graphLeaves) {
|
||||
const currentState = leaf.getViewState();
|
||||
|
||||
await leaf.setViewState({
|
||||
...currentState,
|
||||
type: 'graph',
|
||||
state: { ...currentState.state },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const forceIconizeColors = (plugin: ThemeEngine): void => {
|
||||
let computedIconizeColor = null;
|
||||
try {
|
||||
const cssVal = getComputedStyle(document.body).getPropertyValue('--iconize-icon-color');
|
||||
if (cssVal) computedIconizeColor = cssVal.trim();
|
||||
} catch (error) {
|
||||
console.warn('Theme Engine: failed to read computed --iconize-icon-color', error);
|
||||
computedIconizeColor = null;
|
||||
}
|
||||
|
||||
const storedIconizeColor =
|
||||
plugin.settings.profiles?.[plugin.settings.activeProfile]?.vars?.['--iconize-icon-color'];
|
||||
|
||||
const iconizeColor = plugin.settings.overrideIconizeColors
|
||||
? computedIconizeColor || storedIconizeColor || null
|
||||
: null;
|
||||
|
||||
document.querySelectorAll('.iconize-icon').forEach((iconNode) => {
|
||||
const svg = iconNode.querySelector('svg');
|
||||
if (!svg) return;
|
||||
|
||||
([svg, ...svg.querySelectorAll('*')] as StylableElement[]).forEach((el) => {
|
||||
if (typeof el.setCssProps !== 'function') return;
|
||||
|
||||
if (!iconizeColor) {
|
||||
el.setCssProps({
|
||||
fill: null,
|
||||
stroke: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const originalFill = el.getAttribute('fill');
|
||||
const originalStroke = el.getAttribute('stroke');
|
||||
|
||||
if (originalFill && originalFill !== 'none' && !originalFill.startsWith('url(')) {
|
||||
el.setCssProps({ fill: iconizeColor });
|
||||
}
|
||||
|
||||
if (originalStroke && originalStroke !== 'none') {
|
||||
el.setCssProps({ stroke: iconizeColor });
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const removeOrphanedIconizeElements = (plugin: ThemeEngine): void => {
|
||||
const iconizeInstalled = isIconizeEnabled(plugin.app);
|
||||
|
||||
if (iconizeInstalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
let settingsChanged = false;
|
||||
if (plugin.settings.overrideIconizeColors === true) {
|
||||
console.debug('Theme Engine: Iconize plugin not found. Disabling override setting.');
|
||||
plugin.settings.overrideIconizeColors = false;
|
||||
settingsChanged = true;
|
||||
}
|
||||
|
||||
const orphanedIcons = document.querySelectorAll('.iconize-icon');
|
||||
|
||||
if (orphanedIcons.length > 0) {
|
||||
console.debug(
|
||||
`Theme Engine: Found ${orphanedIcons.length} orphaned Iconize elements. Cleaning up...`,
|
||||
);
|
||||
orphanedIcons.forEach((icon) => icon.remove());
|
||||
}
|
||||
|
||||
if (settingsChanged) {
|
||||
void plugin.saveSettings().catch((err) => {
|
||||
console.error('Failed to save settings after removing Iconize leftovers:', err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const applyStyles = async (plugin: ThemeEngine): Promise<void> => {
|
||||
removeOrphanedIconizeElements(plugin);
|
||||
clearStyles(plugin);
|
||||
if (!plugin.settings.pluginEnabled) {
|
||||
removeInjectedCustomCss(plugin);
|
||||
return;
|
||||
}
|
||||
|
||||
const profile = plugin.settings.profiles[plugin.settings.activeProfile];
|
||||
if (!profile) {
|
||||
console.error('Theme Engine: Active profile not found!');
|
||||
return;
|
||||
}
|
||||
|
||||
const profileVars = Object.entries(profile.vars);
|
||||
if (profileVars.length > 0) {
|
||||
const cssString = `body.theme-dark, body.theme-light {
|
||||
${profileVars
|
||||
.map(([key, value]) => (value ? `${key}: ${value};` : ''))
|
||||
.filter(Boolean)
|
||||
.join('\n ')}
|
||||
}`;
|
||||
|
||||
plugin.setRuntimeStyle('cm-profile-variables', cssString);
|
||||
}
|
||||
|
||||
forceIconizeColors(plugin);
|
||||
setTimeout(() => forceIconizeColors(plugin), 100);
|
||||
|
||||
const themeType = profile.themeType || 'auto';
|
||||
|
||||
if (themeType === 'dark') {
|
||||
document.body.classList.remove('theme-light');
|
||||
document.body.classList.add('theme-dark');
|
||||
} else if (themeType === 'light') {
|
||||
document.body.classList.remove('theme-dark');
|
||||
document.body.classList.add('theme-light');
|
||||
} else {
|
||||
const vaultWithConfig = plugin.app.vault as typeof plugin.app.vault & VaultWithConfigGetter;
|
||||
const currentConfig = vaultWithConfig.getConfig('theme');
|
||||
const isSystemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
if (currentConfig === 'obsidian' || (currentConfig === 'system' && isSystemDark)) {
|
||||
document.body.classList.remove('theme-light');
|
||||
document.body.classList.add('theme-dark');
|
||||
} else {
|
||||
document.body.classList.remove('theme-dark');
|
||||
document.body.classList.add('theme-light');
|
||||
}
|
||||
}
|
||||
applyCustomCssForProfile(plugin, plugin.settings.activeProfile);
|
||||
applyCssSnippets(plugin);
|
||||
|
||||
const langCode = plugin.settings.language;
|
||||
const customLang = plugin.settings.customLanguages?.[langCode];
|
||||
const isCoreRtlLang = langCode === 'ar' || langCode === 'fa';
|
||||
const isCustomRtlLang = customLang?.isRtl === true;
|
||||
const isRtlEnabled = plugin.settings.useRtlLayout;
|
||||
const isRTL = (isCoreRtlLang || isCustomRtlLang) && isRtlEnabled;
|
||||
|
||||
if (isRTL) {
|
||||
document.body.classList.add('theme-engine-rtl');
|
||||
} else {
|
||||
document.body.classList.remove('theme-engine-rtl');
|
||||
}
|
||||
await plugin.applyBackgroundMedia();
|
||||
plugin.app.workspace.trigger('css-change');
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
|
||||
setTimeout(() => {
|
||||
plugin.app.workspace.trigger('css-change');
|
||||
const graphLeaves = plugin.app.workspace.getLeavesOfType('graph');
|
||||
graphLeaves.forEach((leaf) => {
|
||||
const view = leaf.view as ReloadableView | null;
|
||||
if (view?.reload) {
|
||||
view.reload();
|
||||
}
|
||||
});
|
||||
}, 300);
|
||||
};
|
||||
|
||||
export const clearStyles = (plugin: ThemeEngine): void => {
|
||||
plugin.clearRuntimeStyle('cm-profile-variables');
|
||||
plugin.clearRuntimeStyle('cm-dynamic-notice-styles');
|
||||
|
||||
const profileStyleEl = document.getElementById('cm-profile-variables');
|
||||
if (profileStyleEl) {
|
||||
profileStyleEl.remove();
|
||||
}
|
||||
|
||||
removeCssSnippets(plugin);
|
||||
|
||||
const allVars = new Set();
|
||||
Object.keys(flattenVars(DEFAULT_VARS)).forEach((key) => allVars.add(key));
|
||||
for (const profileName in plugin.settings.profiles) {
|
||||
const profile = plugin.settings.profiles[profileName];
|
||||
if (profile && profile.vars) {
|
||||
Object.keys(profile.vars).forEach((key) => allVars.add(key));
|
||||
}
|
||||
}
|
||||
allVars.forEach((key: string) => {
|
||||
document.body.setCssProps({ [key]: null });
|
||||
});
|
||||
|
||||
document.querySelectorAll('.iconize-icon').forEach((iconNode) => {
|
||||
const svg = iconNode.querySelector('svg');
|
||||
if (!svg) return;
|
||||
|
||||
([svg, ...svg.querySelectorAll('*')] as StylableElement[]).forEach((el) => {
|
||||
if (typeof el.setCssProps !== 'function') return;
|
||||
|
||||
el.setCssProps({
|
||||
fill: null,
|
||||
stroke: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const styleId = 'theme-engine-overrides';
|
||||
const overrideStyleEl = document.getElementById(styleId);
|
||||
if (overrideStyleEl) {
|
||||
overrideStyleEl.remove();
|
||||
}
|
||||
plugin.app.workspace.trigger('css-change');
|
||||
document.body.classList.remove('theme-engine-rtl');
|
||||
plugin._clearBackgroundMedia();
|
||||
};
|
||||
1669
src/main.ts
1669
src/main.ts
File diff suppressed because it is too large
Load diff
312
src/ui/settings/render.ts
Normal file
312
src/ui/settings/render.ts
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
import { ButtonComponent, Notice, Setting, SettingGroup, setIcon } from 'obsidian';
|
||||
import { CORE_LANGUAGES, LocaleCode } from '../../i18n/types';
|
||||
import { loadLanguage, t } from '../../i18n/strings';
|
||||
import type { ThemeEngineSettingTab } from '../settingsTab';
|
||||
import { drawColorPickers } from '../components/color-pickers';
|
||||
import { drawCssSnippetsUI } from '../components/snippets-ui';
|
||||
import { drawImportExport } from '../components/import-export';
|
||||
import { drawLikePluginCard } from '../components/like-plugin-card';
|
||||
import { drawOptionsSection } from '../components/options-section';
|
||||
import { drawProfileManager } from '../components/profile-manager';
|
||||
import {
|
||||
AddNewLanguageModal,
|
||||
ConfirmationModal,
|
||||
LanguageInfoModal,
|
||||
LanguageSettingsModal,
|
||||
LanguageTranslatorModal,
|
||||
} from '../modals';
|
||||
import { debounce } from '../../utils';
|
||||
|
||||
export const renderSettingsTab = async (tab: ThemeEngineSettingTab): Promise<void> => {
|
||||
const themeDefaults = await tab.plugin.getThemeDefaults();
|
||||
|
||||
const { containerEl } = tab;
|
||||
containerEl.classList.add('theme-engine-settings-tab');
|
||||
containerEl.empty();
|
||||
containerEl.classList.add('theme-engine-hidden');
|
||||
|
||||
const langCode = tab.plugin.settings.language;
|
||||
const customLang = tab.plugin.settings.customLanguages?.[langCode];
|
||||
const isCoreRtlLang = langCode === 'ar' || langCode === 'fa';
|
||||
const isCustomRtlLang = customLang?.isRtl === true;
|
||||
const isRtlCapable = isCoreRtlLang || isCustomRtlLang;
|
||||
const isRtlEnabled = tab.plugin.settings.useRtlLayout;
|
||||
const isRTL = isRtlCapable && isRtlEnabled;
|
||||
const isCustom = !!customLang;
|
||||
const isCore = !!CORE_LANGUAGES[langCode as LocaleCode];
|
||||
|
||||
tab.containerEl.setAttribute('dir', isRTL ? 'rtl' : 'ltr');
|
||||
|
||||
const topSettingsContainer = containerEl.createDiv({
|
||||
cls: 'cm-top-settings-group',
|
||||
});
|
||||
|
||||
let createTopSetting: () => Setting;
|
||||
|
||||
if (typeof SettingGroup === 'function') {
|
||||
const topGroup = new SettingGroup(topSettingsContainer);
|
||||
topGroup.addClass('cm-native-setting-group');
|
||||
|
||||
createTopSetting = () => {
|
||||
let createdSetting: Setting | null = null;
|
||||
topGroup.addSetting((setting) => {
|
||||
createdSetting = setting;
|
||||
});
|
||||
|
||||
if (!createdSetting) {
|
||||
console.warn('Theme Engine: Failed to create top setting group item. Falling back.');
|
||||
return new Setting(topSettingsContainer);
|
||||
}
|
||||
|
||||
return createdSetting;
|
||||
};
|
||||
} else {
|
||||
topSettingsContainer.classList.add('cm-fallback-setting-group');
|
||||
const topSettingsBody = topSettingsContainer.createDiv({
|
||||
cls: 'cm-setting-group-body',
|
||||
});
|
||||
createTopSetting = () => new Setting(topSettingsBody);
|
||||
}
|
||||
|
||||
createTopSetting()
|
||||
.setName(t('settings.enablePlugin'))
|
||||
.setDesc(t('settings.enablePluginDesc'))
|
||||
.addToggle((toggle) => {
|
||||
toggle.setValue(tab.plugin.settings.pluginEnabled).onChange(async (value) => {
|
||||
tab.plugin.settings.pluginEnabled = value;
|
||||
|
||||
if (value) {
|
||||
tab.plugin.enableObservers();
|
||||
} else {
|
||||
tab.plugin.disableObservers();
|
||||
}
|
||||
|
||||
await tab.plugin.saveSettings();
|
||||
tab.plugin.restartColorUpdateLoop();
|
||||
new Notice(value ? t('notices.pluginEnabled') : t('notices.pluginDisabled'));
|
||||
});
|
||||
});
|
||||
|
||||
const languageSetting = createTopSetting()
|
||||
.setName(t('settings.language'))
|
||||
.setDesc(t('settings.languageDesc'));
|
||||
|
||||
const langIcon = languageSetting.nameEl.createSpan({
|
||||
cls: 'cm-setting-icon',
|
||||
});
|
||||
setIcon(langIcon, 'languages');
|
||||
languageSetting.nameEl.prepend(langIcon);
|
||||
|
||||
const flyoutContainer = languageSetting.controlEl.createDiv({
|
||||
cls: 'cm-flyout-menu-container',
|
||||
});
|
||||
|
||||
const buttonListEl = flyoutContainer.createDiv({
|
||||
cls: 'cm-flyout-menu-buttons',
|
||||
});
|
||||
|
||||
const triggerBtn = new ButtonComponent(flyoutContainer)
|
||||
.setIcon('settings')
|
||||
.setTooltip(t('tooltips.langMenu'))
|
||||
.setClass('cm-flyout-trigger-btn')
|
||||
.onClick(() => {
|
||||
buttonListEl.toggleClass('is-open', !buttonListEl.classList.contains('is-open'));
|
||||
});
|
||||
triggerBtn.buttonEl.classList.add('cm-control-icon-button');
|
||||
|
||||
new ButtonComponent(buttonListEl)
|
||||
.setIcon('info')
|
||||
.setTooltip(t('tooltips.langInfo'))
|
||||
.setClass('cm-control-icon-button')
|
||||
.onClick(() => {
|
||||
new LanguageInfoModal(tab.app, tab.plugin).open();
|
||||
});
|
||||
|
||||
new ButtonComponent(buttonListEl)
|
||||
.setIcon('pencil')
|
||||
.setTooltip(t('tooltips.editLang'))
|
||||
.setClass('cm-control-icon-button')
|
||||
.onClick(() => {
|
||||
const selectedLangCode = tab.plugin.settings.language;
|
||||
new LanguageTranslatorModal(tab.app, tab.plugin, tab, selectedLangCode).open();
|
||||
});
|
||||
|
||||
new ButtonComponent(buttonListEl)
|
||||
.setIcon('plus')
|
||||
.setTooltip(t('modals.addLang.title'))
|
||||
.setClass('cm-control-icon-button')
|
||||
.onClick(() => {
|
||||
new AddNewLanguageModal(tab.app, tab.plugin, tab).open();
|
||||
});
|
||||
|
||||
if (isRtlCapable) {
|
||||
new ButtonComponent(buttonListEl)
|
||||
.setIcon('settings-2')
|
||||
.setTooltip(t('settings.languageSettingsModalTitle'))
|
||||
.setClass('cm-control-icon-button')
|
||||
.onClick(() => {
|
||||
new LanguageSettingsModal(tab.app, tab.plugin).open();
|
||||
});
|
||||
}
|
||||
|
||||
if (isCore && isCustom) {
|
||||
const restoreBtn = new ButtonComponent(buttonListEl)
|
||||
.setIcon('history')
|
||||
.setTooltip(t('tooltips.restoreDefaultLang'))
|
||||
.setClass('mod-warning')
|
||||
.onClick(() => {
|
||||
new ConfirmationModal(
|
||||
tab.app,
|
||||
tab.plugin,
|
||||
t('modals.confirmation.restoreLangTitle'),
|
||||
t('modals.confirmation.restoreLangDesc', CORE_LANGUAGES[langCode as LocaleCode]),
|
||||
() => {
|
||||
void (async () => {
|
||||
if (tab.plugin.settings.customLanguages) {
|
||||
delete tab.plugin.settings.customLanguages[langCode];
|
||||
loadLanguage(tab.plugin.settings);
|
||||
await tab.plugin.saveSettings();
|
||||
tab.display();
|
||||
new Notice(t('notices.langRestored'));
|
||||
}
|
||||
})().catch((err) => {
|
||||
console.error('Failed to restore language:', err);
|
||||
});
|
||||
},
|
||||
{ buttonText: t('buttons.restore'), buttonClass: 'mod-warning' },
|
||||
).open();
|
||||
});
|
||||
restoreBtn.buttonEl.classList.add('cm-control-icon-button');
|
||||
}
|
||||
|
||||
if (isCustom && !isCore) {
|
||||
const deleteBtn = new ButtonComponent(buttonListEl)
|
||||
.setIcon('trash')
|
||||
.setTooltip(t('buttons.delete') + ` (${customLang.languageName})`)
|
||||
.setClass('mod-warning')
|
||||
.onClick(() => {
|
||||
new ConfirmationModal(
|
||||
tab.app,
|
||||
tab.plugin,
|
||||
t('modals.confirmation.deleteLangTitle'),
|
||||
t('modals.confirmation.deleteLangDesc', customLang.languageName),
|
||||
() => {
|
||||
void (async () => {
|
||||
if (!tab.plugin.settings.customLanguages) return;
|
||||
delete tab.plugin.settings.customLanguages[langCode];
|
||||
tab.plugin.settings.language = 'en';
|
||||
loadLanguage(tab.plugin.settings);
|
||||
await tab.plugin.saveSettings();
|
||||
tab.display();
|
||||
new Notice(t('notices.langDeleted', customLang.languageName));
|
||||
})().catch((err) => {
|
||||
console.error('Failed to delete custom language:', err);
|
||||
});
|
||||
},
|
||||
{ buttonText: t('buttons.delete'), buttonClass: 'mod-warning' },
|
||||
).open();
|
||||
});
|
||||
deleteBtn.buttonEl.classList.add('cm-control-icon-button');
|
||||
}
|
||||
|
||||
languageSetting.addDropdown((dropdown) => {
|
||||
const customLangs = tab.plugin.settings.customLanguages || {};
|
||||
|
||||
for (const code in CORE_LANGUAGES) {
|
||||
let displayName = CORE_LANGUAGES[code as LocaleCode];
|
||||
if (customLangs[code]) {
|
||||
displayName = customLangs[code].languageName;
|
||||
}
|
||||
dropdown.addOption(code, displayName);
|
||||
}
|
||||
|
||||
const customCodes = Object.keys(customLangs);
|
||||
for (const code of customCodes) {
|
||||
if (CORE_LANGUAGES[code as LocaleCode]) continue;
|
||||
|
||||
const langName = customLangs[code].languageName;
|
||||
dropdown.addOption(code, langName);
|
||||
}
|
||||
|
||||
dropdown.setValue(tab.plugin.settings.language);
|
||||
dropdown.onChange(async (value) => {
|
||||
tab.plugin.settings.language = value;
|
||||
loadLanguage(tab.plugin.settings);
|
||||
|
||||
await tab.plugin.saveSettings();
|
||||
tab.display();
|
||||
});
|
||||
});
|
||||
|
||||
tab.staticContentContainer = containerEl.createDiv({
|
||||
cls: 'cm-static-sections',
|
||||
});
|
||||
drawProfileManager(tab.staticContentContainer, tab);
|
||||
drawImportExport(tab.staticContentContainer, tab);
|
||||
drawOptionsSection(tab.staticContentContainer, tab);
|
||||
tab.staticContentContainer.createEl('hr');
|
||||
drawCssSnippetsUI(tab.staticContentContainer, tab);
|
||||
containerEl.createEl('hr', { cls: 'cm-search-divider' });
|
||||
tab.initSearchUI(containerEl);
|
||||
drawColorPickers(tab.containerEl, tab, themeDefaults);
|
||||
containerEl.createEl('hr');
|
||||
|
||||
const noResultsBadge = containerEl.createDiv({
|
||||
cls: 'cm-search-empty-badge is-hidden',
|
||||
});
|
||||
const noResultsIcon = noResultsBadge.createDiv({
|
||||
cls: 'cm-search-empty-icon',
|
||||
});
|
||||
setIcon(noResultsIcon, 'search-x');
|
||||
noResultsBadge.createDiv({
|
||||
cls: 'cm-search-empty-title',
|
||||
text: t('settings.noResultsFound'),
|
||||
});
|
||||
noResultsBadge.createDiv({
|
||||
cls: 'cm-search-empty-subtitle',
|
||||
text: t('settings.noResultsHint'),
|
||||
});
|
||||
tab.noSearchResultsEl = noResultsBadge;
|
||||
|
||||
tab.likeCardEl = drawLikePluginCard(containerEl, tab);
|
||||
|
||||
tab._applySearchFilter();
|
||||
|
||||
if (tab._searchState.query) {
|
||||
setTimeout(() => {
|
||||
const firstVisibleRow = tab.containerEl.querySelector<HTMLElement>(
|
||||
'.cm-var-row:not(.cm-hidden)',
|
||||
);
|
||||
|
||||
if (firstVisibleRow) {
|
||||
firstVisibleRow.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
const scrollContainer = tab.containerEl.closest<HTMLElement>('.vertical-tab-content');
|
||||
|
||||
if (!scrollContainer) {
|
||||
console.warn('Theme Engine: Could not find scroll container.');
|
||||
} else {
|
||||
const debouncedScrollSave = debounce(() => {
|
||||
tab.plugin.settings.lastScrollPosition = scrollContainer.scrollTop;
|
||||
void tab.plugin.saveData(tab.plugin.settings).catch((err) => {
|
||||
console.error('Failed to save scroll position:', err);
|
||||
});
|
||||
}, 200);
|
||||
|
||||
tab.plugin.registerDomEvent(scrollContainer, 'scroll', debouncedScrollSave);
|
||||
|
||||
if (!tab._searchState.query && tab.plugin.settings.lastScrollPosition) {
|
||||
scrollContainer.scrollTo({
|
||||
top: tab.plugin.settings.lastScrollPosition,
|
||||
behavior: 'auto',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
containerEl.classList.remove('theme-engine-hidden');
|
||||
};
|
||||
339
src/ui/settings/search.ts
Normal file
339
src/ui/settings/search.ts
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
import { DropdownComponent, SearchComponent, setIcon } from 'obsidian';
|
||||
import { DEFAULT_VARS } from '../../constants';
|
||||
import { t } from '../../i18n/strings';
|
||||
import type { ThemeEngineSettingTab } from '../settingsTab';
|
||||
import { debounce } from '../../utils';
|
||||
|
||||
export type SearchState = {
|
||||
query: string;
|
||||
regex: boolean;
|
||||
caseSensitive: boolean;
|
||||
section: string;
|
||||
};
|
||||
|
||||
export const clearSearchAndFilters = (tab: ThemeEngineSettingTab): void => {
|
||||
if (!tab.searchInput || !tab.sectionSelect) return;
|
||||
|
||||
tab.searchInput.value = '';
|
||||
tab.sectionSelect.value = '';
|
||||
|
||||
tab._searchState.query = '';
|
||||
tab._searchState.section = '';
|
||||
|
||||
tab.plugin.settings.lastSearchQuery = '';
|
||||
tab.plugin.settings.lastSearchSection = '';
|
||||
void tab.plugin.saveData(tab.plugin.settings).catch((err) => {
|
||||
console.error('Failed to save search input state:', err);
|
||||
});
|
||||
|
||||
const filterButton = tab.containerEl.querySelector('button[data-cm-action="filter"]');
|
||||
const filterOptionsContainer = tab.containerEl.querySelector('.cm-search-filter-options');
|
||||
|
||||
if (filterButton) filterButton.classList.remove('is-active');
|
||||
if (filterOptionsContainer) filterOptionsContainer.classList.add('is-hidden');
|
||||
|
||||
tab._applySearchFilter();
|
||||
};
|
||||
|
||||
export const initSearchUI = (tab: ThemeEngineSettingTab, containerEl: HTMLElement): void => {
|
||||
tab._searchState = {
|
||||
query: tab.plugin.settings.lastSearchQuery || '',
|
||||
regex: false,
|
||||
caseSensitive: false,
|
||||
section: tab.plugin.settings.lastSearchSection || '',
|
||||
};
|
||||
|
||||
const searchShell = containerEl.createDiv({
|
||||
cls: 'cm-search-shell',
|
||||
});
|
||||
tab.searchContainer = searchShell;
|
||||
|
||||
const searchBarContainer = searchShell.createDiv({
|
||||
cls: 'cm-search-bar-container',
|
||||
});
|
||||
|
||||
const searchInputContainer = searchBarContainer.createDiv({
|
||||
cls: 'cm-search-input-container',
|
||||
});
|
||||
|
||||
const searchComponent = new SearchComponent(searchInputContainer)
|
||||
.setPlaceholder(t('settings.searchPlaceholder'))
|
||||
.setValue(tab._searchState.query)
|
||||
.onChange((value) => {
|
||||
if (tab._searchState.regex) return;
|
||||
tab._searchState.query = value;
|
||||
tab.plugin.settings.lastSearchQuery = value;
|
||||
void tab.plugin.saveData(tab.plugin.settings).catch((err) => {
|
||||
console.error('Failed to save search/filter reset state:', err);
|
||||
});
|
||||
debouncedFilter();
|
||||
});
|
||||
|
||||
tab.searchInput = searchComponent.inputEl;
|
||||
|
||||
tab.searchInput.addEventListener('keydown', (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter' && tab._searchState.regex) {
|
||||
tab._searchState.query = (event.target as HTMLInputElement).value;
|
||||
debouncedFilter();
|
||||
}
|
||||
});
|
||||
|
||||
const searchActions = searchBarContainer.createDiv({
|
||||
cls: 'cm-search-actions',
|
||||
});
|
||||
|
||||
tab.caseToggle = searchActions.createEl('button', {
|
||||
cls: 'cm-search-action-btn',
|
||||
});
|
||||
tab.caseToggle.textContent = 'Aa';
|
||||
tab.caseToggle.setAttr('aria-label', t('settings.ariaCase'));
|
||||
|
||||
tab.regexToggle = searchActions.createEl('button', {
|
||||
cls: 'cm-search-action-btn',
|
||||
});
|
||||
setIcon(tab.regexToggle, 'regex');
|
||||
tab.regexToggle.setAttr('aria-label', t('settings.ariaRegex'));
|
||||
|
||||
const filterOptionsContainer = searchActions.createDiv({
|
||||
cls: 'cm-search-filter-options is-hidden',
|
||||
});
|
||||
|
||||
const dropdown = new DropdownComponent(filterOptionsContainer)
|
||||
.addOption('', t('settings.allSections'))
|
||||
.onChange(async (value) => {
|
||||
tab._searchState.section = value;
|
||||
|
||||
tab.plugin.settings.lastSearchSection = value;
|
||||
await tab.plugin.saveData(tab.plugin.settings);
|
||||
|
||||
filterOptionsContainer.classList.toggle('is-filter-active', value !== '');
|
||||
debouncedFilter();
|
||||
});
|
||||
|
||||
dropdown.setValue(tab._searchState.section);
|
||||
if (tab._searchState.section) {
|
||||
filterOptionsContainer.classList.remove('is-hidden');
|
||||
}
|
||||
|
||||
try {
|
||||
Object.keys(DEFAULT_VARS || {}).forEach((category) => {
|
||||
let categoryKey: string;
|
||||
const lowerCategory = category.toLowerCase();
|
||||
|
||||
if (lowerCategory === 'interactive elements') categoryKey = 'interactive';
|
||||
else if (lowerCategory === 'ui elements') categoryKey = 'ui';
|
||||
else if (lowerCategory === 'graph view') categoryKey = 'graph';
|
||||
else if (lowerCategory === 'plugin integrations') categoryKey = 'pluginintegrations';
|
||||
else categoryKey = lowerCategory.replace(/ /g, '');
|
||||
|
||||
const translatedCategory = t(`categories.${categoryKey}`) || category;
|
||||
dropdown.addOption(category, translatedCategory);
|
||||
});
|
||||
} catch {
|
||||
// ignore invalid category translation
|
||||
}
|
||||
|
||||
const activeProfile = tab.plugin.settings.profiles[tab.plugin.settings.activeProfile];
|
||||
if (activeProfile?.customVarMetadata && Object.keys(activeProfile.customVarMetadata).length > 0) {
|
||||
dropdown.addOption('Custom', t('categories.custom'));
|
||||
}
|
||||
|
||||
dropdown.selectEl.classList.add('cm-search-small');
|
||||
tab.sectionSelect = dropdown.selectEl;
|
||||
|
||||
const filterButton = searchActions.createEl('button', {
|
||||
cls: 'cm-search-action-btn',
|
||||
});
|
||||
setIcon(filterButton, 'sliders-horizontal');
|
||||
filterButton.dataset.cmAction = 'filter';
|
||||
|
||||
if (tab._searchState.section) filterButton.classList.add('is-active');
|
||||
|
||||
const debouncedFilter = debounce(() => tab._applySearchFilter(), 180);
|
||||
|
||||
tab.caseToggle.addEventListener('click', () => {
|
||||
tab._searchState.caseSensitive = !tab._searchState.caseSensitive;
|
||||
tab.caseToggle.toggleClass('is-active', tab._searchState.caseSensitive);
|
||||
debouncedFilter();
|
||||
});
|
||||
|
||||
tab.regexToggle.addEventListener('click', () => {
|
||||
tab._searchState.regex = !tab._searchState.regex;
|
||||
tab.regexToggle.toggleClass('is-active', tab._searchState.regex);
|
||||
if (tab._searchState.regex) {
|
||||
searchComponent.setPlaceholder(t('settings.regexPlaceholder'));
|
||||
} else {
|
||||
searchComponent.setPlaceholder(t('settings.searchPlaceholder'));
|
||||
}
|
||||
tab._searchState.query = tab.searchInput.value;
|
||||
debouncedFilter();
|
||||
});
|
||||
|
||||
filterButton.addEventListener('click', () => {
|
||||
if (filterButton.classList.contains('is-active')) {
|
||||
tab._clearSearchAndFilters();
|
||||
} else {
|
||||
filterOptionsContainer.classList.remove('is-hidden');
|
||||
filterButton.classList.add('is-active');
|
||||
}
|
||||
});
|
||||
|
||||
tab.sectionSelect.addEventListener('change', (event: Event) => {
|
||||
tab._searchState.section = (event.target as HTMLSelectElement).value;
|
||||
debouncedFilter();
|
||||
});
|
||||
};
|
||||
|
||||
export const applySearchFilter = (tab: ThemeEngineSettingTab): void => {
|
||||
const state = tab._searchState;
|
||||
const activeProfile = tab.plugin.settings.profiles[tab.plugin.settings.activeProfile];
|
||||
const isSearching = state.query.trim().length > 0 || state.section !== '';
|
||||
const rows = Array.from(
|
||||
tab.containerEl.querySelectorAll<HTMLElement>('.cm-var-row, .cm-searchable-row'),
|
||||
);
|
||||
|
||||
let queryRegex: RegExp | null = null;
|
||||
|
||||
if (state.query && state.query.trim()) {
|
||||
if (state.regex) {
|
||||
try {
|
||||
queryRegex = new RegExp(state.query, state.caseSensitive ? '' : 'i');
|
||||
} catch {
|
||||
queryRegex = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rows.forEach((row) => {
|
||||
const varName = row.dataset.var || '';
|
||||
const snippetName = row.dataset.name || '';
|
||||
const textInput = row.querySelector<HTMLInputElement>("input[type='text']");
|
||||
const varValue = textInput ? textInput.value.trim() : '';
|
||||
|
||||
let displayName = '';
|
||||
let description = '';
|
||||
const customMeta = activeProfile?.customVarMetadata?.[varName];
|
||||
|
||||
if (customMeta) {
|
||||
displayName = customMeta.name;
|
||||
description = customMeta.desc;
|
||||
} else {
|
||||
displayName = t(`colors.names.${varName}`) || snippetName;
|
||||
description = t(`colors.descriptions.${varName}`) || '';
|
||||
}
|
||||
|
||||
if (state.section && state.section !== row.dataset.category) {
|
||||
row.classList.add('cm-hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.query && state.query.trim()) {
|
||||
const query = state.query.trim();
|
||||
let isMatch = false;
|
||||
|
||||
if (state.regex && queryRegex) {
|
||||
isMatch =
|
||||
queryRegex.test(varName) ||
|
||||
queryRegex.test(varValue) ||
|
||||
queryRegex.test(displayName) ||
|
||||
queryRegex.test(description);
|
||||
} else {
|
||||
const queryLower = state.caseSensitive ? query : query.toLowerCase();
|
||||
const nameLower = state.caseSensitive ? varName : varName.toLowerCase();
|
||||
const valueLower = state.caseSensitive ? varValue : varValue.toLowerCase();
|
||||
const displayNameLower = state.caseSensitive ? displayName : displayName.toLowerCase();
|
||||
const descriptionLower = state.caseSensitive ? description : description.toLowerCase();
|
||||
|
||||
isMatch =
|
||||
nameLower.includes(queryLower) ||
|
||||
valueLower.includes(queryLower) ||
|
||||
displayNameLower.includes(queryLower) ||
|
||||
descriptionLower.includes(queryLower);
|
||||
}
|
||||
|
||||
if (!isMatch) {
|
||||
row.classList.add('cm-hidden');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
row.classList.remove('cm-hidden');
|
||||
tab._highlightRowMatches(row, state);
|
||||
});
|
||||
|
||||
const headings = tab.containerEl.querySelectorAll<HTMLElement>('.cm-category-container');
|
||||
headings.forEach((heading) => {
|
||||
const category = heading.dataset.category;
|
||||
const hasVisibleRows = tab.containerEl.querySelector(
|
||||
`.cm-var-row[data-category="${category}"]:not(.cm-hidden)`,
|
||||
);
|
||||
|
||||
if (hasVisibleRows) {
|
||||
heading.classList.remove('cm-hidden');
|
||||
} else {
|
||||
heading.classList.add('cm-hidden');
|
||||
}
|
||||
});
|
||||
|
||||
const hasVisibleSearchResults = rows.some((row) => !row.classList.contains('cm-hidden'));
|
||||
const showNoResults = isSearching && !hasVisibleSearchResults;
|
||||
|
||||
if (tab.noSearchResultsEl) {
|
||||
tab.noSearchResultsEl.classList.toggle('is-hidden', !showNoResults);
|
||||
}
|
||||
if (tab.likeCardEl) {
|
||||
tab.likeCardEl.classList.toggle('is-hidden', showNoResults);
|
||||
}
|
||||
};
|
||||
|
||||
export const highlightRowMatches = (row: HTMLElement, state: SearchState): void => {
|
||||
const query = state.query.trim();
|
||||
|
||||
const highlightElement = (element: HTMLElement | null) => {
|
||||
if (!element) return;
|
||||
|
||||
if (!element.dataset.originalText) {
|
||||
element.dataset.originalText = element.textContent || '';
|
||||
}
|
||||
const originalText = element.dataset.originalText;
|
||||
|
||||
element.empty();
|
||||
|
||||
if (!query) {
|
||||
element.setText(originalText);
|
||||
return;
|
||||
}
|
||||
|
||||
const flags = state.caseSensitive ? 'g' : 'gi';
|
||||
let regex: RegExp;
|
||||
|
||||
try {
|
||||
regex = state.regex
|
||||
? new RegExp(query, flags)
|
||||
: new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), flags);
|
||||
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(originalText)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
element.appendText(originalText.substring(lastIndex, match.index));
|
||||
}
|
||||
element.createSpan({ cls: 'cm-search-match', text: match[0] });
|
||||
lastIndex = regex.lastIndex;
|
||||
}
|
||||
|
||||
if (lastIndex < originalText.length) {
|
||||
element.appendText(originalText.substring(lastIndex));
|
||||
}
|
||||
} catch {
|
||||
element.setText(originalText);
|
||||
}
|
||||
};
|
||||
|
||||
const nameEl = row.querySelector<HTMLElement>('.cm-var-name');
|
||||
const descEl = row.querySelector<HTMLElement>('.setting-item-description');
|
||||
|
||||
highlightElement(nameEl);
|
||||
highlightElement(descEl);
|
||||
};
|
||||
|
|
@ -1,34 +1,17 @@
|
|||
import {
|
||||
App,
|
||||
ButtonComponent,
|
||||
DropdownComponent,
|
||||
Notice,
|
||||
PluginSettingTab,
|
||||
Setting,
|
||||
SettingGroup,
|
||||
setIcon,
|
||||
SearchComponent,
|
||||
} from 'obsidian';
|
||||
import { App, ButtonComponent, Notice, PluginSettingTab } from 'obsidian';
|
||||
import Sortable from 'sortablejs';
|
||||
import { DEFAULT_VARS, TEXT_TO_BG_MAP } from '../constants';
|
||||
import { t } from '../i18n/strings';
|
||||
import type ThemeEngine from '../main';
|
||||
import { flattenVars, getAccessibilityRating, getContrastRatio, debounce } from '../utils';
|
||||
import { drawColorPickers } from './components/color-pickers';
|
||||
import { drawImportExport } from './components/import-export';
|
||||
import { drawLikePluginCard } from './components/like-plugin-card';
|
||||
import { drawOptionsSection } from './components/options-section';
|
||||
import { drawProfileManager } from './components/profile-manager';
|
||||
import { drawCssSnippetsUI } from './components/snippets-ui';
|
||||
import { LanguageSettingsModal } from './modals';
|
||||
import Sortable from 'sortablejs';
|
||||
import { loadLanguage } from '../i18n/strings';
|
||||
import { CORE_LANGUAGES, LocaleCode } from '../i18n/types';
|
||||
import { flattenVars, getAccessibilityRating, getContrastRatio } from '../utils';
|
||||
import { renderSettingsTab } from './settings/render';
|
||||
import {
|
||||
AddNewLanguageModal,
|
||||
LanguageTranslatorModal,
|
||||
ConfirmationModal,
|
||||
LanguageInfoModal,
|
||||
} from './modals';
|
||||
applySearchFilter as applySearchFilterCore,
|
||||
clearSearchAndFilters,
|
||||
highlightRowMatches as highlightRowMatchesCore,
|
||||
initSearchUI as initSearchUICore,
|
||||
type SearchState,
|
||||
} from './settings/search';
|
||||
|
||||
export class ThemeEngineSettingTab extends PluginSettingTab {
|
||||
plugin: ThemeEngine;
|
||||
|
|
@ -37,12 +20,7 @@ export class ThemeEngineSettingTab extends PluginSettingTab {
|
|||
caseToggle: HTMLButtonElement;
|
||||
regexToggle: HTMLButtonElement;
|
||||
sectionSelect: HTMLSelectElement;
|
||||
_searchState: {
|
||||
query: string;
|
||||
regex: boolean;
|
||||
caseSensitive: boolean;
|
||||
section: string;
|
||||
};
|
||||
_searchState: SearchState;
|
||||
staticContentContainer: HTMLDivElement;
|
||||
resetPinBtn: ButtonComponent | null = null;
|
||||
pinBtn: ButtonComponent | null = null;
|
||||
|
|
@ -92,340 +70,19 @@ export class ThemeEngineSettingTab extends PluginSettingTab {
|
|||
}
|
||||
|
||||
_clearSearchAndFilters() {
|
||||
if (!this.searchInput || !this.sectionSelect) return;
|
||||
|
||||
this.searchInput.value = '';
|
||||
this.sectionSelect.value = '';
|
||||
|
||||
this._searchState.query = '';
|
||||
this._searchState.section = '';
|
||||
|
||||
// Clear saved settings
|
||||
this.plugin.settings.lastSearchQuery = '';
|
||||
this.plugin.settings.lastSearchSection = '';
|
||||
void this.plugin.saveData(this.plugin.settings).catch((err) => {
|
||||
console.error('Failed to save search input state:', err);
|
||||
});
|
||||
|
||||
const filterButton = this.containerEl.querySelector('button[data-cm-action="filter"]');
|
||||
const filterOptionsContainer = this.containerEl.querySelector('.cm-search-filter-options');
|
||||
|
||||
if (filterButton) filterButton.classList.remove('is-active');
|
||||
if (filterOptionsContainer) filterOptionsContainer.classList.add('is-hidden');
|
||||
|
||||
this._applySearchFilter();
|
||||
clearSearchAndFilters(this);
|
||||
}
|
||||
|
||||
initSearchUI(containerEl: HTMLElement) {
|
||||
// Load saved section here
|
||||
this._searchState = {
|
||||
query: this.plugin.settings.lastSearchQuery || '',
|
||||
regex: false,
|
||||
caseSensitive: false,
|
||||
section: this.plugin.settings.lastSearchSection || '',
|
||||
};
|
||||
|
||||
const searchShell = containerEl.createDiv({
|
||||
cls: 'cm-search-shell',
|
||||
});
|
||||
this.searchContainer = searchShell;
|
||||
|
||||
const searchBarContainer = searchShell.createDiv({
|
||||
cls: 'cm-search-bar-container',
|
||||
});
|
||||
|
||||
// --- 1. Input field section ---
|
||||
const searchInputContainer = searchBarContainer.createDiv({
|
||||
cls: 'cm-search-input-container',
|
||||
});
|
||||
|
||||
const searchComponent = new SearchComponent(searchInputContainer)
|
||||
.setPlaceholder(t('settings.searchPlaceholder'))
|
||||
.setValue(this._searchState.query)
|
||||
.onChange((value) => {
|
||||
if (this._searchState.regex) return;
|
||||
this._searchState.query = value;
|
||||
this.plugin.settings.lastSearchQuery = value;
|
||||
void this.plugin.saveData(this.plugin.settings).catch((err) => {
|
||||
console.error('Failed to save search/filter reset state:', err);
|
||||
});
|
||||
debouncedFilter();
|
||||
});
|
||||
|
||||
this.searchInput = searchComponent.inputEl;
|
||||
|
||||
this.searchInput.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && this._searchState.regex) {
|
||||
this._searchState.query = (e.target as HTMLInputElement).value;
|
||||
debouncedFilter();
|
||||
}
|
||||
});
|
||||
|
||||
// --- 2. Control buttons section ---
|
||||
const searchActions = searchBarContainer.createDiv({
|
||||
cls: 'cm-search-actions',
|
||||
});
|
||||
|
||||
this.caseToggle = searchActions.createEl('button', {
|
||||
cls: 'cm-search-action-btn',
|
||||
});
|
||||
this.caseToggle.textContent = 'Aa';
|
||||
this.caseToggle.setAttr('aria-label', t('settings.ariaCase'));
|
||||
|
||||
this.regexToggle = searchActions.createEl('button', {
|
||||
cls: 'cm-search-action-btn',
|
||||
});
|
||||
setIcon(this.regexToggle, 'regex');
|
||||
this.regexToggle.setAttr('aria-label', t('settings.ariaRegex'));
|
||||
|
||||
const filterOptionsContainer = searchActions.createDiv({
|
||||
cls: 'cm-search-filter-options is-hidden',
|
||||
});
|
||||
|
||||
const dropdown = new DropdownComponent(filterOptionsContainer)
|
||||
.addOption('', t('settings.allSections'))
|
||||
.onChange(async (value) => {
|
||||
this._searchState.section = value;
|
||||
|
||||
// Save the filter section
|
||||
this.plugin.settings.lastSearchSection = value;
|
||||
await this.plugin.saveData(this.plugin.settings);
|
||||
|
||||
filterOptionsContainer.classList.toggle('is-filter-active', value !== '');
|
||||
debouncedFilter();
|
||||
});
|
||||
|
||||
// Restore filter state
|
||||
dropdown.setValue(this._searchState.section);
|
||||
if (this._searchState.section) {
|
||||
filterOptionsContainer.classList.remove('is-hidden');
|
||||
}
|
||||
|
||||
try {
|
||||
Object.keys(DEFAULT_VARS || {}).forEach((category) => {
|
||||
let categoryKey: string;
|
||||
const lowerCategory = category.toLowerCase();
|
||||
|
||||
if (lowerCategory === 'interactive elements') categoryKey = 'interactive';
|
||||
else if (lowerCategory === 'ui elements') categoryKey = 'ui';
|
||||
else if (lowerCategory === 'graph view') categoryKey = 'graph';
|
||||
else if (lowerCategory === 'plugin integrations') categoryKey = 'pluginintegrations';
|
||||
else categoryKey = lowerCategory.replace(/ /g, '');
|
||||
|
||||
const translatedCategory = t(`categories.${categoryKey}`) || category;
|
||||
dropdown.addOption(category, translatedCategory);
|
||||
});
|
||||
} catch {
|
||||
// ignore invalid category translation
|
||||
}
|
||||
|
||||
const activeProfile = this.plugin.settings.profiles[this.plugin.settings.activeProfile];
|
||||
if (
|
||||
activeProfile?.customVarMetadata &&
|
||||
Object.keys(activeProfile.customVarMetadata).length > 0
|
||||
) {
|
||||
dropdown.addOption('Custom', t('categories.custom'));
|
||||
}
|
||||
|
||||
dropdown.selectEl.classList.add('cm-search-small');
|
||||
this.sectionSelect = dropdown.selectEl;
|
||||
|
||||
const filterButton = searchActions.createEl('button', {
|
||||
cls: 'cm-search-action-btn',
|
||||
});
|
||||
setIcon(filterButton, 'sliders-horizontal');
|
||||
filterButton.dataset.cmAction = 'filter';
|
||||
|
||||
// Activate button if section is saved
|
||||
if (this._searchState.section) filterButton.classList.add('is-active');
|
||||
|
||||
const debouncedFilter = debounce(() => this._applySearchFilter(), 180);
|
||||
|
||||
this.caseToggle.addEventListener('click', () => {
|
||||
this._searchState.caseSensitive = !this._searchState.caseSensitive;
|
||||
this.caseToggle.toggleClass('is-active', this._searchState.caseSensitive);
|
||||
debouncedFilter();
|
||||
});
|
||||
|
||||
this.regexToggle.addEventListener('click', () => {
|
||||
this._searchState.regex = !this._searchState.regex;
|
||||
this.regexToggle.toggleClass('is-active', this._searchState.regex);
|
||||
if (this._searchState.regex) {
|
||||
searchComponent.setPlaceholder(t('settings.regexPlaceholder'));
|
||||
} else {
|
||||
searchComponent.setPlaceholder(t('settings.searchPlaceholder'));
|
||||
}
|
||||
this._searchState.query = this.searchInput.value;
|
||||
debouncedFilter();
|
||||
});
|
||||
|
||||
filterButton.addEventListener('click', () => {
|
||||
if (filterButton.classList.contains('is-active')) {
|
||||
this._clearSearchAndFilters();
|
||||
} else {
|
||||
filterOptionsContainer.classList.remove('is-hidden');
|
||||
filterButton.classList.add('is-active');
|
||||
}
|
||||
});
|
||||
|
||||
this.sectionSelect.addEventListener('change', (e: Event) => {
|
||||
this._searchState.section = (e.target as HTMLSelectElement).value;
|
||||
debouncedFilter();
|
||||
});
|
||||
initSearchUICore(this, containerEl);
|
||||
}
|
||||
|
||||
_applySearchFilter() {
|
||||
const s = this._searchState;
|
||||
const activeProfile = this.plugin.settings.profiles[this.plugin.settings.activeProfile];
|
||||
const isSearching = s.query.trim().length > 0 || s.section !== '';
|
||||
const rows = Array.from(
|
||||
this.containerEl.querySelectorAll<HTMLElement>('.cm-var-row, .cm-searchable-row'),
|
||||
);
|
||||
|
||||
let qRegex: RegExp | null = null;
|
||||
|
||||
if (s.query && s.query.trim()) {
|
||||
if (s.regex) {
|
||||
try {
|
||||
qRegex = new RegExp(s.query, s.caseSensitive ? '' : 'i');
|
||||
} catch {
|
||||
qRegex = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rows.forEach((row) => {
|
||||
const varName = row.dataset.var || '';
|
||||
const snippetName = row.dataset.name || '';
|
||||
const textInput = row.querySelector<HTMLInputElement>("input[type='text']");
|
||||
const varValue = textInput ? textInput.value.trim() : '';
|
||||
|
||||
let displayName = '';
|
||||
let description = '';
|
||||
const customMeta = activeProfile?.customVarMetadata?.[varName];
|
||||
|
||||
if (customMeta) {
|
||||
displayName = customMeta.name;
|
||||
description = customMeta.desc;
|
||||
} else {
|
||||
displayName = t(`colors.names.${varName}`) || snippetName;
|
||||
description = t(`colors.descriptions.${varName}`) || '';
|
||||
}
|
||||
|
||||
if (s.section && s.section !== row.dataset.category) {
|
||||
row.classList.add('cm-hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
if (s.query && s.query.trim()) {
|
||||
const q = s.query.trim();
|
||||
let isMatch = false;
|
||||
|
||||
if (s.regex && qRegex) {
|
||||
isMatch =
|
||||
qRegex.test(varName) ||
|
||||
qRegex.test(varValue) ||
|
||||
qRegex.test(displayName) ||
|
||||
qRegex.test(description);
|
||||
} else {
|
||||
const queryLower = s.caseSensitive ? q : q.toLowerCase();
|
||||
const nameLower = s.caseSensitive ? varName : varName.toLowerCase();
|
||||
const valueLower = s.caseSensitive ? varValue : varValue.toLowerCase();
|
||||
const displayNameLower = s.caseSensitive ? displayName : displayName.toLowerCase();
|
||||
const descriptionLower = s.caseSensitive ? description : description.toLowerCase();
|
||||
|
||||
isMatch =
|
||||
nameLower.includes(queryLower) ||
|
||||
valueLower.includes(queryLower) ||
|
||||
displayNameLower.includes(queryLower) ||
|
||||
descriptionLower.includes(queryLower);
|
||||
}
|
||||
|
||||
if (!isMatch) {
|
||||
row.classList.add('cm-hidden');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
row.classList.remove('cm-hidden');
|
||||
this._highlightRowMatches(row, s);
|
||||
});
|
||||
|
||||
const headings = this.containerEl.querySelectorAll<HTMLElement>('.cm-category-container');
|
||||
headings.forEach((heading) => {
|
||||
const category = heading.dataset.category;
|
||||
const hasVisibleRows = this.containerEl.querySelector(
|
||||
`.cm-var-row[data-category="${category}"]:not(.cm-hidden)`,
|
||||
);
|
||||
|
||||
if (hasVisibleRows) {
|
||||
heading.classList.remove('cm-hidden');
|
||||
} else {
|
||||
heading.classList.add('cm-hidden');
|
||||
}
|
||||
});
|
||||
|
||||
const hasVisibleSearchResults = rows.some((row) => !row.classList.contains('cm-hidden'));
|
||||
const showNoResults = isSearching && !hasVisibleSearchResults;
|
||||
|
||||
if (this.noSearchResultsEl) {
|
||||
this.noSearchResultsEl.classList.toggle('is-hidden', !showNoResults);
|
||||
}
|
||||
if (this.likeCardEl) {
|
||||
this.likeCardEl.classList.toggle('is-hidden', showNoResults);
|
||||
}
|
||||
applySearchFilterCore(this);
|
||||
}
|
||||
|
||||
_highlightRowMatches(row: HTMLElement, state: typeof this._searchState) {
|
||||
const query = state.query.trim();
|
||||
|
||||
const highlightElement = (element: HTMLElement | null) => {
|
||||
if (!element) return;
|
||||
|
||||
if (!element.dataset.originalText) {
|
||||
element.dataset.originalText = element.textContent || '';
|
||||
}
|
||||
const originalText = element.dataset.originalText;
|
||||
|
||||
element.empty();
|
||||
|
||||
if (!query) {
|
||||
element.setText(originalText);
|
||||
return;
|
||||
}
|
||||
|
||||
const flags = state.caseSensitive ? 'g' : 'gi';
|
||||
let regex: RegExp;
|
||||
|
||||
try {
|
||||
regex = state.regex
|
||||
? new RegExp(query, flags)
|
||||
: new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), flags);
|
||||
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(originalText)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
element.appendText(originalText.substring(lastIndex, match.index));
|
||||
}
|
||||
element.createSpan({ cls: 'cm-search-match', text: match[0] });
|
||||
lastIndex = regex.lastIndex;
|
||||
}
|
||||
|
||||
if (lastIndex < originalText.length) {
|
||||
element.appendText(originalText.substring(lastIndex));
|
||||
}
|
||||
} catch {
|
||||
element.setText(originalText);
|
||||
}
|
||||
};
|
||||
|
||||
const nameEl = row.querySelector<HTMLElement>('.cm-var-name');
|
||||
const descEl = row.querySelector<HTMLElement>('.setting-item-description');
|
||||
|
||||
highlightElement(nameEl);
|
||||
highlightElement(descEl);
|
||||
_highlightRowMatches(row: HTMLElement, state: SearchState) {
|
||||
highlightRowMatchesCore(row, state);
|
||||
}
|
||||
|
||||
_updatePinButtons() {
|
||||
|
|
@ -452,12 +109,12 @@ export class ThemeEngineSettingTab extends PluginSettingTab {
|
|||
}
|
||||
|
||||
_getCurrentProfileJson() {
|
||||
const p = this.plugin.settings.profiles?.[this.plugin.settings.activeProfile];
|
||||
if (!p) return null;
|
||||
const profile = this.plugin.settings.profiles?.[this.plugin.settings.activeProfile];
|
||||
if (!profile) return null;
|
||||
return {
|
||||
name: this.plugin.settings.activeProfile,
|
||||
exportedAt: new Date().toISOString(),
|
||||
profile: p,
|
||||
profile,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -481,12 +138,12 @@ export class ThemeEngineSettingTab extends PluginSettingTab {
|
|||
type: 'application/json',
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.download = `${this.plugin.settings.activeProfile}.profile.json`;
|
||||
a.href = url;
|
||||
a.click();
|
||||
const anchor = document.createElement('a');
|
||||
anchor.download = `${this.plugin.settings.activeProfile}.profile.json`;
|
||||
anchor.href = url;
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
anchor.remove();
|
||||
new Notice(t('notices.exportSuccess'));
|
||||
}
|
||||
|
||||
|
|
@ -520,316 +177,12 @@ export class ThemeEngineSettingTab extends PluginSettingTab {
|
|||
}
|
||||
|
||||
display(): void {
|
||||
// call the async renderer and handle any error
|
||||
void this.renderDisplay().catch((err) => {
|
||||
console.error('Error in SettingsTab.renderDisplay:', err);
|
||||
});
|
||||
}
|
||||
|
||||
private async renderDisplay(): Promise<void> {
|
||||
const themeDefaults = await this.plugin.getThemeDefaults();
|
||||
|
||||
const { containerEl } = this;
|
||||
containerEl.classList.add('theme-engine-settings-tab');
|
||||
containerEl.empty();
|
||||
containerEl.classList.add('theme-engine-hidden');
|
||||
|
||||
const langCode = this.plugin.settings.language;
|
||||
const customLang = this.plugin.settings.customLanguages?.[langCode];
|
||||
const isCoreRtlLang = langCode === 'ar' || langCode === 'fa';
|
||||
const isCustomRtlLang = customLang?.isRtl === true;
|
||||
const isRtlCapable = isCoreRtlLang || isCustomRtlLang;
|
||||
const isRtlEnabled = this.plugin.settings.useRtlLayout;
|
||||
const isRTL = isRtlCapable && isRtlEnabled;
|
||||
const isCustom = !!customLang;
|
||||
const isCore = !!CORE_LANGUAGES[langCode as LocaleCode];
|
||||
|
||||
this.containerEl.setAttribute('dir', isRTL ? 'rtl' : 'ltr');
|
||||
|
||||
const topSettingsContainer = containerEl.createDiv({
|
||||
cls: 'cm-top-settings-group',
|
||||
});
|
||||
|
||||
let createTopSetting: () => Setting;
|
||||
|
||||
if (typeof SettingGroup === 'function') {
|
||||
const topGroup = new SettingGroup(topSettingsContainer);
|
||||
topGroup.addClass('cm-native-setting-group');
|
||||
|
||||
createTopSetting = () => {
|
||||
let createdSetting: Setting | null = null;
|
||||
topGroup.addSetting((setting) => {
|
||||
createdSetting = setting;
|
||||
});
|
||||
|
||||
if (!createdSetting) {
|
||||
console.warn('Theme Engine: Failed to create top setting group item. Falling back.');
|
||||
return new Setting(topSettingsContainer);
|
||||
}
|
||||
|
||||
return createdSetting;
|
||||
};
|
||||
} else {
|
||||
topSettingsContainer.classList.add('cm-fallback-setting-group');
|
||||
const topSettingsBody = topSettingsContainer.createDiv({
|
||||
cls: 'cm-setting-group-body',
|
||||
});
|
||||
createTopSetting = () => new Setting(topSettingsBody);
|
||||
}
|
||||
|
||||
createTopSetting()
|
||||
.setName(t('settings.enablePlugin'))
|
||||
.setDesc(t('settings.enablePluginDesc'))
|
||||
.addToggle((toggle) => {
|
||||
toggle.setValue(this.plugin.settings.pluginEnabled).onChange(async (value) => {
|
||||
this.plugin.settings.pluginEnabled = value;
|
||||
|
||||
if (value) {
|
||||
this.plugin.enableObservers();
|
||||
} else {
|
||||
this.plugin.disableObservers();
|
||||
}
|
||||
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.restartColorUpdateLoop();
|
||||
new Notice(value ? t('notices.pluginEnabled') : t('notices.pluginDisabled'));
|
||||
});
|
||||
});
|
||||
|
||||
const languageSetting = createTopSetting()
|
||||
.setName(t('settings.language'))
|
||||
.setDesc(t('settings.languageDesc'));
|
||||
|
||||
const langIcon = languageSetting.nameEl.createSpan({
|
||||
cls: 'cm-setting-icon',
|
||||
});
|
||||
setIcon(langIcon, 'languages');
|
||||
languageSetting.nameEl.prepend(langIcon);
|
||||
|
||||
const flyoutContainer = languageSetting.controlEl.createDiv({
|
||||
cls: 'cm-flyout-menu-container',
|
||||
});
|
||||
|
||||
const buttonListEl = flyoutContainer.createDiv({
|
||||
cls: 'cm-flyout-menu-buttons',
|
||||
});
|
||||
|
||||
const triggerBtn = new ButtonComponent(flyoutContainer)
|
||||
.setIcon('settings')
|
||||
.setTooltip(t('tooltips.langMenu'))
|
||||
.setClass('cm-flyout-trigger-btn')
|
||||
.onClick(() => {
|
||||
buttonListEl.toggleClass('is-open', !buttonListEl.classList.contains('is-open'));
|
||||
});
|
||||
triggerBtn.buttonEl.classList.add('cm-control-icon-button');
|
||||
|
||||
// Info Button
|
||||
new ButtonComponent(buttonListEl)
|
||||
.setIcon('info')
|
||||
.setTooltip(t('tooltips.langInfo'))
|
||||
.setClass('cm-control-icon-button')
|
||||
.onClick(() => {
|
||||
new LanguageInfoModal(this.app, this.plugin).open();
|
||||
});
|
||||
|
||||
// Edit Button
|
||||
new ButtonComponent(buttonListEl)
|
||||
.setIcon('pencil')
|
||||
.setTooltip(t('tooltips.editLang'))
|
||||
.setClass('cm-control-icon-button')
|
||||
.onClick(() => {
|
||||
const langCode = this.plugin.settings.language;
|
||||
new LanguageTranslatorModal(this.app, this.plugin, this, langCode).open();
|
||||
});
|
||||
|
||||
// Add Button
|
||||
new ButtonComponent(buttonListEl)
|
||||
.setIcon('plus')
|
||||
.setTooltip(t('modals.addLang.title'))
|
||||
.setClass('cm-control-icon-button')
|
||||
.onClick(() => {
|
||||
new AddNewLanguageModal(this.app, this.plugin, this).open();
|
||||
});
|
||||
|
||||
// RTL Button
|
||||
if (isRtlCapable) {
|
||||
new ButtonComponent(buttonListEl)
|
||||
.setIcon('settings-2') //
|
||||
.setTooltip(t('settings.languageSettingsModalTitle'))
|
||||
.setClass('cm-control-icon-button')
|
||||
.onClick(() => {
|
||||
new LanguageSettingsModal(this.app, this.plugin).open();
|
||||
});
|
||||
}
|
||||
|
||||
// History/Restore Button for Core Languages that are customized
|
||||
if (isCore && isCustom) {
|
||||
const restoreBtn = new ButtonComponent(buttonListEl)
|
||||
.setIcon('history')
|
||||
.setTooltip(t('tooltips.restoreDefaultLang'))
|
||||
.setClass('mod-warning')
|
||||
.onClick(() => {
|
||||
new ConfirmationModal(
|
||||
this.app,
|
||||
this.plugin,
|
||||
t('modals.confirmation.restoreLangTitle'),
|
||||
t('modals.confirmation.restoreLangDesc', CORE_LANGUAGES[langCode as LocaleCode]),
|
||||
() => {
|
||||
void (async () => {
|
||||
if (this.plugin.settings.customLanguages) {
|
||||
delete this.plugin.settings.customLanguages[langCode];
|
||||
loadLanguage(this.plugin.settings);
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
new Notice(t('notices.langRestored'));
|
||||
}
|
||||
})().catch((err) => {
|
||||
// Short human comment in English (as you requested)
|
||||
console.error('Failed to restore language:', err);
|
||||
});
|
||||
},
|
||||
{ buttonText: t('buttons.restore'), buttonClass: 'mod-warning' },
|
||||
).open();
|
||||
});
|
||||
restoreBtn.buttonEl.classList.add('cm-control-icon-button');
|
||||
}
|
||||
|
||||
if (isCustom && !isCore) {
|
||||
const deleteBtn = new ButtonComponent(buttonListEl)
|
||||
.setIcon('trash')
|
||||
.setTooltip(t('buttons.delete') + ` (${customLang.languageName})`)
|
||||
.setClass('mod-warning')
|
||||
.onClick(() => {
|
||||
new ConfirmationModal(
|
||||
this.app,
|
||||
this.plugin,
|
||||
t('modals.confirmation.deleteLangTitle'),
|
||||
t('modals.confirmation.deleteLangDesc', customLang.languageName),
|
||||
() => {
|
||||
void (async () => {
|
||||
if (!this.plugin.settings.customLanguages) return;
|
||||
delete this.plugin.settings.customLanguages[langCode];
|
||||
this.plugin.settings.language = 'en';
|
||||
loadLanguage(this.plugin.settings);
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
new Notice(t('notices.langDeleted', customLang.languageName));
|
||||
})().catch((err) => {
|
||||
console.error('Failed to delete custom language:', err);
|
||||
});
|
||||
},
|
||||
{ buttonText: t('buttons.delete'), buttonClass: 'mod-warning' },
|
||||
).open();
|
||||
});
|
||||
deleteBtn.buttonEl.classList.add('cm-control-icon-button');
|
||||
}
|
||||
|
||||
languageSetting.addDropdown((dropdown) => {
|
||||
const customLangs = this.plugin.settings.customLanguages || {};
|
||||
|
||||
// 1. Add Core languages FIRST
|
||||
for (const code in CORE_LANGUAGES) {
|
||||
// Use custom name if it exists, otherwise use core name
|
||||
let displayName = CORE_LANGUAGES[code as LocaleCode];
|
||||
if (customLangs[code]) {
|
||||
displayName = customLangs[code].languageName;
|
||||
}
|
||||
dropdown.addOption(code, displayName);
|
||||
}
|
||||
|
||||
// 2. Add purely Custom languages (that are NOT core overrides)
|
||||
const customCodes = Object.keys(customLangs);
|
||||
for (const code of customCodes) {
|
||||
if (CORE_LANGUAGES[code as LocaleCode]) continue;
|
||||
|
||||
const langName = customLangs[code].languageName;
|
||||
dropdown.addOption(code, langName);
|
||||
}
|
||||
|
||||
dropdown.setValue(this.plugin.settings.language);
|
||||
dropdown.onChange(async (value) => {
|
||||
this.plugin.settings.language = value;
|
||||
loadLanguage(this.plugin.settings);
|
||||
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
});
|
||||
});
|
||||
|
||||
this.staticContentContainer = containerEl.createDiv({
|
||||
cls: 'cm-static-sections',
|
||||
});
|
||||
drawProfileManager(this.staticContentContainer, this);
|
||||
drawImportExport(this.staticContentContainer, this);
|
||||
drawOptionsSection(this.staticContentContainer, this);
|
||||
this.staticContentContainer.createEl('hr');
|
||||
drawCssSnippetsUI(this.staticContentContainer, this);
|
||||
containerEl.createEl('hr', { cls: 'cm-search-divider' });
|
||||
this.initSearchUI(containerEl);
|
||||
drawColorPickers(this.containerEl, this, themeDefaults);
|
||||
containerEl.createEl('hr');
|
||||
|
||||
const noResultsBadge = containerEl.createDiv({
|
||||
cls: 'cm-search-empty-badge is-hidden',
|
||||
});
|
||||
const noResultsIcon = noResultsBadge.createDiv({
|
||||
cls: 'cm-search-empty-icon',
|
||||
});
|
||||
setIcon(noResultsIcon, 'search-x');
|
||||
noResultsBadge.createDiv({
|
||||
cls: 'cm-search-empty-title',
|
||||
text: t('settings.noResultsFound'),
|
||||
});
|
||||
noResultsBadge.createDiv({
|
||||
cls: 'cm-search-empty-subtitle',
|
||||
text: t('settings.noResultsHint'),
|
||||
});
|
||||
this.noSearchResultsEl = noResultsBadge;
|
||||
|
||||
this.likeCardEl = drawLikePluginCard(containerEl, this);
|
||||
|
||||
//---Implement automatic search and scrolling---
|
||||
|
||||
// Make sure that the filter is applied
|
||||
this._applySearchFilter();
|
||||
|
||||
if (this._searchState.query) {
|
||||
setTimeout(() => {
|
||||
// Find the first "visible" line
|
||||
const firstVisibleRow = this.containerEl.querySelector<HTMLElement>(
|
||||
'.cm-var-row:not(.cm-hidden)',
|
||||
);
|
||||
|
||||
if (firstVisibleRow) {
|
||||
firstVisibleRow.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
const scrollContainer = this.containerEl.closest<HTMLElement>('.vertical-tab-content');
|
||||
|
||||
if (!scrollContainer) {
|
||||
console.warn('Theme Engine: Could not find scroll container.');
|
||||
} else {
|
||||
const debouncedScrollSave = debounce(() => {
|
||||
this.plugin.settings.lastScrollPosition = scrollContainer.scrollTop;
|
||||
void this.plugin.saveData(this.plugin.settings).catch((err) => {
|
||||
console.error('Failed to save scroll position:', err);
|
||||
});
|
||||
}, 200);
|
||||
|
||||
this.plugin.registerDomEvent(scrollContainer, 'scroll', debouncedScrollSave);
|
||||
|
||||
if (!this._searchState.query && this.plugin.settings.lastScrollPosition) {
|
||||
scrollContainer.scrollTo({
|
||||
top: this.plugin.settings.lastScrollPosition,
|
||||
behavior: 'auto',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
containerEl.classList.remove('theme-engine-hidden');
|
||||
await renderSettingsTab(this);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue