├── .eslintrc.js ├── .eslintrc.typescript.js ├── .gitignore ├── .prettierrc.js ├── .release-it.cjs ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── esbuild.config.mjs ├── manifest-beta.json ├── manifest.json ├── package.json ├── pnpm-lock.yaml ├── scripts └── ob-bumper.mjs ├── src ├── click-handler.ts ├── drag-patch.ts ├── fe-handler │ ├── active-folder.ts │ ├── base.ts │ ├── file-explorer.less │ ├── focus.less │ ├── folder-focus.ts │ ├── folder-icon.less │ ├── folder-mark.ts │ └── index.ts ├── fe-patch.ts ├── fn-main.ts ├── main.less ├── misc.ts ├── modules │ ├── long-press.ts │ └── set-folder-icon.ts ├── settings.less ├── settings.ts └── typings │ ├── obsidian-ex.d.ts │ └── svg.d.ts ├── tsconfig.json └── versions.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const typescriptOptions = { 2 | tsconfigRootDir: __dirname, 3 | project: "tsconfig.json", 4 | }; 5 | 6 | /** 7 | * @type {import('eslint').Linter.Config} 8 | */ 9 | module.exports = { 10 | extends: ["prettier", "./.eslintrc.typescript.js"], 11 | plugins: ["prettier"], 12 | root: true, 13 | parserOptions: { 14 | ...typescriptOptions, 15 | ecmaVersion: "latest", 16 | sourceType: "module", 17 | }, 18 | rules: { 19 | "prettier/prettier": "error", 20 | "arrow-body-style": "off", 21 | "prefer-arrow-callback": "off", 22 | "import/no-unresolved": [2, { ignore: [".less$"] }], 23 | }, 24 | settings: { 25 | "import/resolver": { 26 | typescript: typescriptOptions, 27 | }, 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /.eslintrc.typescript.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom config base for projects using typescript / javascript. 3 | * @see https://github.com/belgattitude/nextjs-monorepo-example/tree/main/packages/eslint-config-bases 4 | */ 5 | 6 | module.exports = { 7 | env: { 8 | es6: true, 9 | node: true, 10 | }, 11 | parser: "@typescript-eslint/parser", 12 | parserOptions: { 13 | ecmaFeatures: { 14 | jsx: true, 15 | globalReturn: false, 16 | }, 17 | ecmaVersion: "2022", 18 | project: ["tsconfig.json"], 19 | sourceType: "module", 20 | }, 21 | settings: { 22 | "import/resolver": { 23 | typescript: {}, 24 | }, 25 | }, 26 | extends: [ 27 | "eslint:recommended", 28 | "plugin:@typescript-eslint/recommended", 29 | "plugin:import/recommended", 30 | "plugin:import/typescript", 31 | ], 32 | plugins: ["prefer-arrow", "simple-import-sort"], 33 | rules: { 34 | "spaced-comment": [ 35 | "error", 36 | "always", 37 | { 38 | line: { 39 | markers: ["/"], 40 | exceptions: ["-", "+"], 41 | }, 42 | block: { 43 | markers: ["!"], 44 | exceptions: ["*"], 45 | balanced: true, 46 | }, 47 | }, 48 | ], 49 | "linebreak-style": ["error", "unix"], 50 | "no-empty-function": "off", 51 | "import/default": "off", 52 | "import/no-duplicates": ["error", { considerQueryString: true }], 53 | "import/no-unresolved": "off", 54 | "import/no-named-as-default-member": "off", 55 | "import/no-named-as-default": "off", 56 | "import/order": [ 57 | "error", 58 | { 59 | groups: [ 60 | "builtin", 61 | "external", 62 | "internal", 63 | "parent", 64 | "sibling", 65 | "index", 66 | "object", 67 | ], 68 | alphabetize: { order: "asc", caseInsensitive: true }, 69 | }, 70 | ], 71 | "@typescript-eslint/ban-tslint-comment": ["error"], 72 | "@typescript-eslint/ban-ts-comment": [ 73 | "error", 74 | { 75 | "ts-expect-error": "allow-with-description", 76 | minimumDescriptionLength: 10, 77 | "ts-ignore": false, 78 | "ts-nocheck": true, 79 | "ts-check": false, 80 | }, 81 | ], 82 | "@typescript-eslint/no-explicit-any": "off", 83 | "@typescript-eslint/no-non-null-assertion": "off", 84 | "@typescript-eslint/no-empty-function": [ 85 | "error", 86 | { allow: ["private-constructors"] }, 87 | ], 88 | "@typescript-eslint/no-unused-vars": [ 89 | "warn", 90 | { 91 | argsIgnorePattern: "^_", 92 | ignoreRestSiblings: true, 93 | destructuredArrayIgnorePattern: "^_", 94 | }, 95 | ], 96 | "@typescript-eslint/consistent-type-exports": "error", 97 | "@typescript-eslint/consistent-type-imports": [ 98 | "error", 99 | { prefer: "type-imports" }, 100 | ], 101 | "@typescript-eslint/naming-convention": [ 102 | "error", 103 | { 104 | selector: "default", 105 | format: ["camelCase"], 106 | leadingUnderscore: "forbid", 107 | trailingUnderscore: "forbid", 108 | }, 109 | { 110 | selector: "variable", 111 | format: ["camelCase", "UPPER_CASE", "snake_case"], 112 | leadingUnderscore: "allow", 113 | }, 114 | { 115 | selector: ["function"], 116 | format: ["camelCase"], 117 | }, 118 | { 119 | selector: "parameter", 120 | format: ["camelCase", "snake_case"], 121 | leadingUnderscore: "allow", 122 | }, 123 | { 124 | selector: "class", 125 | format: ["PascalCase"], 126 | }, 127 | { 128 | selector: "classProperty", 129 | format: ["camelCase"], 130 | leadingUnderscore: "allow", 131 | }, 132 | { 133 | selector: "objectLiteralProperty", 134 | format: [ 135 | "camelCase", 136 | // Some external libraries use snake_case for params 137 | "snake_case", 138 | // Env variables are generally uppercase 139 | "UPPER_CASE", 140 | // DB / Graphql might use PascalCase for relationships 141 | "PascalCase", 142 | ], 143 | leadingUnderscore: "allowSingleOrDouble", 144 | trailingUnderscore: "allowSingleOrDouble", 145 | }, 146 | { 147 | selector: ["typeAlias", "interface"], 148 | format: ["PascalCase"], 149 | }, 150 | { 151 | selector: ["typeProperty"], 152 | format: ["camelCase", "UPPER_CASE"], 153 | // For graphql __typename 154 | leadingUnderscore: "allowDouble", 155 | }, 156 | { 157 | selector: ["typeParameter"], 158 | format: ["PascalCase"], 159 | }, 160 | { 161 | selector: ["enum"], 162 | format: ["PascalCase"], 163 | }, 164 | ], 165 | }, 166 | overrides: [ 167 | { 168 | // commonjs or assumed 169 | files: ["*.js", "*.cjs", "*.mjs"], 170 | parser: "espree", 171 | parserOptions: { 172 | ecmaVersion: 2020, 173 | }, 174 | rules: { 175 | "@typescript-eslint/naming-convention": "off", 176 | "@typescript-eslint/ban-ts-comment": "off", 177 | "@typescript-eslint/no-explicit-any": "off", 178 | "@typescript-eslint/no-var-requires": "off", 179 | "@typescript-eslint/explicit-module-boundary-types": "off", 180 | "@typescript-eslint/consistent-type-exports": "off", 181 | "@typescript-eslint/consistent-type-imports": "off", 182 | "import/order": "off", 183 | }, 184 | }, 185 | ], 186 | }; 187 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # npm 2 | node_modules 3 | package-lock.json 4 | # yarn.lock 5 | 6 | # build 7 | build 8 | 9 | # https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored 10 | .pnp.* 11 | .yarn/* 12 | !.yarn/patches 13 | !.yarn/plugins 14 | !.yarn/releases 15 | !.yarn/sdks 16 | !.yarn/versions -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('prettier').Config} 3 | */ 4 | module.exports = { 5 | singleQuote: false, 6 | semi: true, 7 | tabWidth: 2, 8 | bracketSpacing: true, 9 | trailingComma: "all", 10 | bracketSameLine: false, 11 | useTabs: false, 12 | endOfLine: "lf", 13 | overrides: [], 14 | }; -------------------------------------------------------------------------------- /.release-it.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | hooks: { 3 | // "before:init": ["pnpm run eslint"], 4 | "after:release": 5 | "echo Successfully released obsidian plugin ${name} v${version} to ${repo.repository}.", 6 | }, 7 | git: { 8 | commitMessage: "chore: release obsidian plugin v${version}", 9 | tagName: "${version}", 10 | tagAnnotation: "Release Obsidian Plugin v${version}", 11 | addUntrackedFiles: true, 12 | }, 13 | plugins: { 14 | "@release-it/conventional-changelog": { 15 | preset: "angular", 16 | infile: "CHANGELOG.md", 17 | }, 18 | "./scripts/ob-bumper.mjs": { 19 | indent: 2, 20 | }, 21 | }, 22 | npm: { 23 | publish: false, 24 | }, 25 | github: { 26 | release: true, 27 | assets: [ 28 | "build/main.js", 29 | "build/manifest.json", 30 | "build/styles.css", 31 | ], 32 | proxy: process.env.HTTPS_PROXY, 33 | releaseName: "${version}", 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "conventionalCommits.scopes": ["click-handler", "drag-patch"], 3 | "editor.formatOnSave": false, 4 | "editor.defaultFormatter": "esbenp.prettier-vscode", 5 | "eslint.format.enable": true, 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.eslint": true 8 | }, 9 | // Disable vscode formatting for js,jsx,ts,tsx files 10 | // to allow dbaeumer.vscode-eslint to format them 11 | "[javascript]": { 12 | "editor.formatOnSave": false, 13 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 14 | }, 15 | "[typescript]": { 16 | "editor.formatOnSave": false, 17 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 18 | }, 19 | "[typescriptreact]": { 20 | "editor.formatOnSave": false, 21 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## [0.16.5](https://github.com/aidenlx/alx-folder-note/compare/0.16.4...0.16.5) (2023-05-05) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * click event ([a27e40a](https://github.com/aidenlx/alx-folder-note/commit/a27e40a370e38f55dc9c3b14513fab3cc62f5f12)) 9 | * compatible with Obsidian 1.2.0 version ([1b13e92](https://github.com/aidenlx/alx-folder-note/commit/1b13e926d7f21f85afc8959de0d8ee77f89b4a37)) 10 | 11 | 12 | ### Performance Improvements 13 | 14 | * **drag-patch:** patch drag manager with existing MarkdownView instead of init a new one ([07f64d6](https://github.com/aidenlx/alx-folder-note/commit/07f64d6112a93fd46b97c051520fd16dcc2f79ba)) 15 | * use native frontmatter edit method instead of graymatter ([3948b68](https://github.com/aidenlx/alx-folder-note/commit/3948b68b21d437d9b5a90ae0da762ad52187f23b)) 16 | 17 | ## [0.16.4](https://github.com/aidenlx/alx-folder-note/compare/0.16.3...0.16.4) (2022-05-03) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * **drag-patch:** fix no cursor shown on drag folder over editor in v0.14.8+ ([beb8c5e](https://github.com/aidenlx/alx-folder-note/commit/beb8c5e3294b24453b98febc579a1c3860f2bb37))## [0.16.3](https://github.com/aidenlx/alx-folder-note/compare/0.16.2...0.16.3) (2022-04-19) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * **drag-patch:** fix compatibility issue with legacy editor ([91ec54a](https://github.com/aidenlx/alx-folder-note/commit/91ec54a95fec14397c6ba9b18f346c45022bf581)), closes [#73](https://github.com/aidenlx/alx-folder-note/issues/73)## [0.16.2](https://github.com/aidenlx/alx-folder-note/compare/0.16.1...0.16.2) (2022-04-15) 28 | 29 | 30 | ### Bug Fixes 31 | 32 | * **drag-patch:** fix broken drag folder with folder note into another folder ([d8c5325](https://github.com/aidenlx/alx-folder-note/commit/d8c53254d878091e79f84a3d3fcfc6453afe30dc)), closes [#71](https://github.com/aidenlx/alx-folder-note/issues/71)## [0.16.1](https://github.com/aidenlx/alx-folder-note/compare/0.16.0...0.16.1) (2022-04-04) 33 | 34 | 35 | ### Bug Fixes 36 | 37 | * improve handling of loading folder-note-core API ([8d72225](https://github.com/aidenlx/alx-folder-note/commit/8d722255af83ef7e4a2da75ff94ba6fc94751412)), closes [#67](https://github.com/aidenlx/alx-folder-note/issues/67)# [0.16.0](https://github.com/aidenlx/alx-folder-note/compare/0.15.0...0.16.0) (2022-03-25) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * **click-handler:** fix shift selection not working on folder with note ([a4742e1](https://github.com/aidenlx/alx-folder-note/commit/a4742e1d90d6e277b48e6c6ea2e33d7e3c44f21b)) 43 | 44 | 45 | ### Features 46 | 47 | * **click handler:** add option to expand collasped folder with note while opening it ([3f98380](https://github.com/aidenlx/alx-folder-note/commit/3f9838017f5ea9a8ee5449c5b85e52f572db6522)), closes [#32](https://github.com/aidenlx/alx-folder-note/issues/32) 48 | * **click-handler:** folder note is triggered only when click on folder title again ([f145d5a](https://github.com/aidenlx/alx-folder-note/commit/f145d5a3552a23fdf10e409ed511f4c0aee89e9e)), closes [#64](https://github.com/aidenlx/alx-folder-note/issues/64) 49 | * support drag folder to editor to insert link ([26e6cf9](https://github.com/aidenlx/alx-folder-note/commit/26e6cf96bd51355a3c71cc7bec892d989df0c62e)), closes [#45](https://github.com/aidenlx/alx-folder-note/issues/45)# [0.15.0](https://github.com/aidenlx/alx-folder-note/compare/0.14.0...0.15.0) (2022-03-06) 50 | 51 | 52 | ### Bug Fixes 53 | 54 | * **settings:** fix input[number] stylings ([eaeb33a](https://github.com/aidenlx/alx-folder-note/commit/eaeb33ab7648e71c05b8f79c221d5a26d9d16245)) 55 | 56 | 57 | ### Features 58 | 59 | * **fe-handler:** adjust folder title and icon style ([03a8340](https://github.com/aidenlx/alx-folder-note/commit/03a83407afdf310eeccc50a629725299f299efdd)) 60 | * **fe-patch:** support hover on folder to preview its folder note ([aa918ff](https://github.com/aidenlx/alx-folder-note/commit/aa918ff80887f6b1cae2237f5ad86fc43ba000db)), closes [#62](https://github.com/aidenlx/alx-folder-note/issues/62) 61 | * **folder-icon:** add command and file menu option to set folder icon ([03d4ec1](https://github.com/aidenlx/alx-folder-note/commit/03d4ec1c239476d6047d093afdf237ca29c01aab)) 62 | * increase folder's collapse-indicator size on mobile ([b619b0d](https://github.com/aidenlx/alx-folder-note/commit/b619b0dbb512692383592f93562a75df152725b8)) 63 | 64 | # [0.14.0](https://github.com/aidenlx/alx-folder-note/compare/0.13.1...0.14.0) (2022-02-25) 65 | 66 | 67 | ### Bug Fixes 68 | 69 | * **folder-icon:** fix basic styles broken in some themes; svg and emoji icons are now aligned properly ([12e28b4](https://github.com/aidenlx/alx-folder-note/commit/12e28b438dcb71304d19d3bf25d0ff5eca8a490a)), closes [#61](https://github.com/aidenlx/alx-folder-note/issues/61) 70 | 71 | 72 | ### Features 73 | 74 | * add `is-active` class on active folder's titleEl ([eccb473](https://github.com/aidenlx/alx-folder-note/commit/eccb4737b0557a807763da711e5cd3edd5591984)), closes [#53](https://github.com/aidenlx/alx-folder-note/issues/53) 75 | * **click-handler:** add option to disable click folder title to open folder note on mobile ([2d2bad6](https://github.com/aidenlx/alx-folder-note/commit/2d2bad6f4a84baa6ff935edf8e77d12f628da6f3)), closes [#56](https://github.com/aidenlx/alx-folder-note/issues/56) [#52](https://github.com/aidenlx/alx-folder-note/issues/52) 76 | * **settings:** add option to configure long press delay ([745680a](https://github.com/aidenlx/alx-folder-note/commit/745680a2f64539b7678713f2f64376373b62381a)), closes [#42](https://github.com/aidenlx/alx-folder-note/issues/42) 77 | 78 | ## [0.13.1](https://github.com/aidenlx/alx-folder-note/compare/0.13.0...0.13.1) (2022-02-19) 79 | 80 | 81 | ### Bug Fixes 82 | 83 | * fix folderv change notice keep popup after ignore it ([ca000ed](https://github.com/aidenlx/alx-folder-note/commit/ca000ed89e54e33e4a285f18dd5c427749d6b129)) 84 | 85 | # [0.13.0](https://github.com/aidenlx/alx-folder-note/compare/0.12.3...0.13.0) (2022-02-19) 86 | 87 | 88 | ### Features 89 | 90 | * add notice about folderv changes ([22ede5e](https://github.com/aidenlx/alx-folder-note/commit/22ede5efa74b581236c56886ad179042f589d3f2)) 91 | * **settings:** add clickable notice that jump to setting tab when folder-note-core not enabled ([156d1b3](https://github.com/aidenlx/alx-folder-note/commit/156d1b3c1a607677560805d1708f0ffc02a298f0)) 92 | * **settings:** add guide to install dependencies ([8ea9769](https://github.com/aidenlx/alx-folder-note/commit/8ea9769fff29fb622f44f9ae0133bdc3a55c2f1a)) 93 | 94 | 95 | ### Performance Improvements 96 | 97 | * separate folderv code from main repo to boost loading ([73e2149](https://github.com/aidenlx/alx-folder-note/commit/73e214987de2ef767877d06127fd364fa4893f06)) 98 | 99 | 100 | ### BREAKING CHANGES 101 | 102 | * folder overview (folderv) is now an optional component that requires a dedicated plugin, go to setting tab to install it 103 | 104 | ## [0.12.3](https://github.com/aidenlx/alx-folder-note/compare/0.12.2...0.12.3) (2021-12-04) 105 | 106 | 107 | ### Bug Fixes 108 | 109 | * **fe-handler:** remove unintended console output ([25e79db](https://github.com/aidenlx/alx-folder-note/commit/25e79db1f41748e2e01b91dfea02b14e925f9eb1)) 110 | * **folder-icon:** fix subfolder icon var polluted by parent folder's icon ([5ff6cc9](https://github.com/aidenlx/alx-folder-note/commit/5ff6cc95e6c5f6b8e18839151613b92b8e8cefe5)) 111 | 112 | ## [0.12.2](https://github.com/aidenlx/alx-folder-note/compare/0.12.1...0.12.2) (2021-11-29) 113 | 114 | 115 | ### Features 116 | 117 | * **fe-handler:** add focus on folder for file explorer ([1759c82](https://github.com/aidenlx/alx-folder-note/commit/1759c827a80b88894ca653b7478f4bfb372ebbe2)) 118 | * **fe-handler:** focused folder now auto unfold and scroll into view ([a872028](https://github.com/aidenlx/alx-folder-note/commit/a872028d05423fc18051972dfb7382c2478897c0)) 119 | * **focus:** long press (0.8s) on folder to toggle focus ([e5d96b9](https://github.com/aidenlx/alx-folder-note/commit/e5d96b9e231a0ae0a0817fc698e0a84640012c3f)) 120 | * **focus:** non-focused folders are now dimmed instead of being hidden ([529f685](https://github.com/aidenlx/alx-folder-note/commit/529f685792e524ad0828cc481924e5b68d616e18)) 121 | * **settings:** add option to disable long press to toggle focus ([a948d56](https://github.com/aidenlx/alx-folder-note/commit/a948d56ae1536844b29876ba550cac091772b208)) 122 | 123 | ## [0.12.1](https://github.com/aidenlx/alx-folder-note/compare/0.12.0...0.12.1) (2021-11-27) 124 | 125 | 126 | ### Features 127 | 128 | * **fe-handler:** add option to hide collapse indicator when folder contains only folder note ([7fa8ee8](https://github.com/aidenlx/alx-folder-note/commit/7fa8ee833acf898f781dc37aa1d7857e576897d5)), closes [#40](https://github.com/aidenlx/alx-folder-note/issues/40) 129 | 130 | * refactor: add body.alx-folder-icons when option is enabled (ff1fa46) 131 | * fix(initialize): fix feHanlder failed to update when workspace changes (9fe694a) 132 | * feat(fe-handler): set folder icon in folder note (7ba11ad) 133 | * docs(readme): update introduction (517c55e) 134 | 135 | ## [0.11.2](https://github.com/aidenlx/alx-folder-note/compare/0.11.1...0.11.2) (2021-09-15) 136 | 137 | 138 | ### Bug Fixes 139 | 140 | * **file-card:** fix mid-click and mod-click on file card ([fe89454](https://github.com/aidenlx/alx-folder-note/commit/fe8945443ad172b7d3562a27e09ca9c2543d3e8b)), closes [#20](https://github.com/aidenlx/alx-folder-note/issues/20) 141 | * **folderv:** folder notes are no longer included in folderv ([4b9fdf3](https://github.com/aidenlx/alx-folder-note/commit/4b9fdf368b2e6f61d72719283de124cac302bf3b)) 142 | * **initialize:** add more retrys to get FileExplorer view ([38625dc](https://github.com/aidenlx/alx-folder-note/commit/38625dc26859253996e4b190567660c85c40f721)), closes [#21](https://github.com/aidenlx/alx-folder-note/issues/21) [#12](https://github.com/aidenlx/alx-folder-note/issues/12) 143 | 144 | ## [0.11.1](https://github.com/aidenlx/alx-folder-note/compare/0.11.0...0.11.1) (2021-09-12) 145 | 146 | 147 | ### Features 148 | 149 | * **settings:** expose log level settings of folder-note-core ([b7aecbf](https://github.com/aidenlx/alx-folder-note/commit/b7aecbfc9013f679dae9b9e17a25086bd83c68f0)) 150 | 151 | # [0.11.0](https://github.com/aidenlx/alx-folder-note/compare/0.10.0...0.11.0) (2021-09-12) 152 | 153 | 154 | ### Bug Fixes 155 | 156 | * **fe-handler:** fix click not set on newly created folder items ([38ea6cf](https://github.com/aidenlx/alx-folder-note/commit/38ea6cfb8dd291713f5e65e68e963aec3a47012d)) 157 | * **fe-handler:** fix click not set on newly created folder items (again) ([b50b6a6](https://github.com/aidenlx/alx-folder-note/commit/b50b6a61e9b7673731950f1c291aa425566a045d)) 158 | * fix compatibility with folder-note-core v1.0.0 ([96c3105](https://github.com/aidenlx/alx-folder-note/commit/96c31053b72b45b839dcb02118dc07e22379127d)) 159 | 160 | 161 | ### Features 162 | 163 | * migrate code to folder-note-core ([29c20eb](https://github.com/aidenlx/alx-folder-note/commit/29c20ebbbfff55315c0a2fc2b551394b7a623ba5)) 164 | 165 | # [0.10.0](https://github.com/aidenlx/alx-folder-note/compare/0.9.2...0.10.0) (2021-08-12) 166 | 167 | 168 | ### Bug Fixes 169 | 170 | * **folderv:** tippy now append to parent ([a4d57c1](https://github.com/aidenlx/alx-folder-note/commit/a4d57c1aae848fd84676755375d33da589d07a3d)) 171 | 172 | 173 | ### Features 174 | 175 | * add api support ([27e3489](https://github.com/aidenlx/alx-folder-note/commit/27e34894b2fa04c1ec7b7c50082f30dbbf7a0872)) 176 | * **folderv:** add soft link support ([4c5d33d](https://github.com/aidenlx/alx-folder-note/commit/4c5d33d82756020bcccb2bf91dda7a28e4bdccda)) 177 | 178 | ## [0.9.2](https://github.com/aidenlx/alx-folder-note/compare/0.9.1...0.9.2) (2021-08-09) 179 | 180 | 181 | ### Bug Fixes 182 | 183 | * **command:** add missing command for link-to-parent-folder; fix commands not available in preview mode ([4acd8d7](https://github.com/aidenlx/alx-folder-note/commit/4acd8d734a32ad6f17a85dd69f3fdfa6b41b502a)) 184 | * fix glob filter matched as regex; add notice for invaild options; rename sort field ([6747616](https://github.com/aidenlx/alx-folder-note/commit/67476164ec46c73c2fedd3720e9977620a72d6e9)) 185 | 186 | ## [0.9.1](https://github.com/aidenlx/alx-folder-note/compare/0.9.0...0.9.1) (2021-08-09) 187 | 188 | 189 | ### Bug Fixes 190 | 191 | * **folderv:** fix tooltip ([adc983b](https://github.com/aidenlx/alx-folder-note/commit/adc983b7bc59ea6a2ca4289504164cb58029dd2e)) 192 | 193 | # [0.9.0](https://github.com/aidenlx/alx-folder-note/compare/0.8.0...0.9.0) (2021-08-09) 194 | 195 | 196 | ### Bug Fixes 197 | 198 | * fix css global pollution ([220f069](https://github.com/aidenlx/alx-folder-note/commit/220f06955d4053c3e1b34a0f7f87e815e5ee890d)), closes [#14](https://github.com/aidenlx/alx-folder-note/issues/14) 199 | 200 | 201 | ### Features 202 | 203 | * add support for dark mode ([e590015](https://github.com/aidenlx/alx-folder-note/commit/e5900159f1deff7ce41c49bedfc971266db5d8e3)), closes [#15](https://github.com/aidenlx/alx-folder-note/issues/15) 204 | 205 | # [0.8.0](https://github.com/aidenlx/alx-folder-note/compare/0.7.0...0.8.0) (2021-08-08) 206 | 207 | 208 | ### Bug Fixes 209 | 210 | * **sort:** fix regex not parsed correctly; fix test when regex flag set to g ([d0bdf4a](https://github.com/aidenlx/alx-folder-note/commit/d0bdf4a4d61e22383277178aa9c96b95ae1c8d1c)) 211 | 212 | 213 | ### Features 214 | 215 | * add custom title and description field name; update default settting ([33ad9f8](https://github.com/aidenlx/alx-folder-note/commit/33ad9f880d47d0cca8c52f87e0c9d9b805c4d687)) 216 | * add mid-click on folder to create new folder note ([ebefb6a](https://github.com/aidenlx/alx-folder-note/commit/ebefb6a7572ffd16afd5fe63ddbece22e4b9b3a8)) 217 | 218 | # [0.7.0](https://github.com/alx-plugins/alx-folder-note/compare/0.6.0...0.7.0) (2021-08-08) 219 | 220 | ### Bug Fixes 221 | 222 | * **fe-handler.ts:** slience the error of no afitem found for path in waitingList ([1146529](https://github.com/alx-plugins/alx-folder-note/commit/11465298acc3ea39dfeceb8c1a816e8eb41a0bfd)) 223 | * remove monkey-around when unload ([9df247f](https://github.com/alx-plugins/alx-folder-note/commit/9df247f0448aab4394d94f6e834a65f259f8fd83)) 224 | * **vault-handler.ts:** fix folder note not update internal links when auto rename ([90922bf](https://github.com/alx-plugins/alx-folder-note/commit/90922bf115ddd89850f4d8de2df8bb060a726eb6)) 225 | 226 | ### Features 227 | 228 | * folder overview support ([44303ee](https://github.com/alx-plugins/alx-folder-note/commit/44303eeade5209649144019ced2ed51cb6fd90ac), [7da2a69](https://github.com/alx-plugins/alx-folder-note/commit/7da2a69a6c47deb09b830844cbad5ebd4e920499), [a4a128e](https://github.com/alx-plugins/alx-folder-note/commit/a4a128e78a824c03717e74ea5dc81ef7d37133f0), [9ddd1e1](https://github.com/alx-plugins/alx-folder-note/commit/9ddd1e1bd1daec0ee91c0b4b574a250e630c33d8), [d00bb3e](https://github.com/alx-plugins/alx-folder-note/commit/d00bb3e2f32f3b5346ea554ed38bed4bc2c52e1c)), closes [#6](https://github.com/alx-plugins/alx-folder-note/issues/6) 229 | 230 | ### Performance Improvements 231 | 232 | * move debounce from vaultHandler to feHandler ([c83e125](https://github.com/alx-plugins/alx-folder-note/commit/c83e125fea78aea31c64b59c92d2a237b7bc82dd)) 233 | 234 | # [0.6.0](https://github.com/alx-plugins/alx-folder-note/compare/0.5.0...0.6.0) (2021-06-11) 235 | 236 | 237 | ### Features 238 | 239 | * add underline to folder with note ([faeba8f](https://github.com/alx-plugins/alx-folder-note/commit/faeba8fab9d4ac78d09e83844a19ed8726266418)) 240 | * **commands.ts:** add command to link current note to parent folder ([0fe327d](https://github.com/alx-plugins/alx-folder-note/commit/0fe327dd0421c6cf30676bc072f96b016c0ca1eb)) 241 | 242 | # [0.5.0](https://github.com/alx-plugins/alx-folder-note/compare/0.4.0...0.5.0) (2021-06-10) 243 | 244 | 245 | ### Bug Fixes 246 | 247 | * **settings.ts:** fix folder note hide fails when toggle deleteOutsideNoteWithFolder ([4eabf9b](https://github.com/alx-plugins/alx-folder-note/commit/4eabf9bb7dadd0137cbeffa22db47a4bca277837)) 248 | 249 | 250 | ### Features 251 | 252 | * add commands for folder note/folder operation ([a936d25](https://github.com/alx-plugins/alx-folder-note/commit/a936d2537c37f9a4588ad4cff1e6ca87234ee1ed)) 253 | * add debounce to vault events ([9db2a46](https://github.com/alx-plugins/alx-folder-note/commit/9db2a46fbad2b54762f33c09ae3b740fffd17af8)) 254 | * revealInFolder now jump to folder when folder note is hidden ([069b109](https://github.com/alx-plugins/alx-folder-note/commit/069b109b3596b6de4f31c6759dd0819b876d2272)) 255 | 256 | 257 | ### Reverts 258 | 259 | * remove fileCountInExplorer ([df9b68a](https://github.com/alx-plugins/alx-folder-note/commit/df9b68a6776c3755905d5f640c97ed9b31cf02f1)) 260 | 261 | # [0.4.0](https://github.com/alx-plugins/alx-folder-note/compare/0.3.0...0.4.0) (2021-05-29) 262 | 263 | 264 | ### Features 265 | 266 | * add option to toggle fileCountInExplorer ([46688ef](https://github.com/alx-plugins/alx-folder-note/commit/46688eff1aa43bec9cb1978f0a84ad8ba9e24ae1)) 267 | * **folder-count.ts:** auto update count when file changes ([5458bdf](https://github.com/alx-plugins/alx-folder-note/commit/5458bdf9f58159944aa523600cc231675b04100c)) 268 | * add initial support for file count in explorer ([f520f66](https://github.com/alx-plugins/alx-folder-note/commit/f520f66ce7acbfcf73878e83d9c4ea9d371bd450)) 269 | 270 | # [0.3.0](https://github.com/alx-plugins/alx-folder-note/compare/0.2.0...0.3.0) (2021-05-28) 271 | 272 | 273 | ### Bug Fixes 274 | 275 | * **valut-handler.ts:** autorename no longer rename index files ([b057bd3](https://github.com/alx-plugins/alx-folder-note/commit/b057bd3bb500327906b7bb4082e146bde923a53a)) 276 | 277 | 278 | ### Features 279 | 280 | * **vault-handler.ts:** add prompt to delete folder when folder note is removed ([fdc6c28](https://github.com/alx-plugins/alx-folder-note/commit/fdc6c28a7a2af6eace89d1d2f278d7e6d4aa93a8)) 281 | * add create-folder-for-note command ([47470ad](https://github.com/alx-plugins/alx-folder-note/commit/47470ad626a2156cf05b72686d89f9a57430deef)) 282 | 283 | # [0.2.0](https://github.com/alx-plugins/alx-folder-note/compare/0.1.1...0.2.0) (2021-05-28) 284 | 285 | 286 | ### Bug Fixes 287 | 288 | * **find.ts:** getFolderNote() throw error when move folder content ([d60e7ad](https://github.com/alx-plugins/alx-folder-note/commit/d60e7ad6c83782251f0ebf15f519dce6af815ca0)) 289 | 290 | 291 | ### Features 292 | 293 | * **vault-handler.ts:** add prompt to comfirm deletion of folder note outside ([01b5032](https://github.com/alx-plugins/alx-folder-note/commit/01b5032be0e6b3aa4f500dcca5da76dd89bc9d0b)) 294 | 295 | ## [0.1.1](https://github.com/alx-plugins/alx-folder-note/compare/0.1.0...0.1.1) (2021-05-27) 296 | 297 | 298 | ### Bug Fixes 299 | 300 | * **vault-handler.ts:** autoRename folder note not working when hideNoteInExplorer is disabled ([6c5ca94](https://github.com/alx-plugins/alx-folder-note/commit/6c5ca94f85dd812e0954c0f5c5fe868b54bfd4b1)) 301 | 302 | # 0.1.0 (2021-05-27) 303 | 304 | 305 | ### Bug Fixes 306 | 307 | * **click-handler.ts:** mid click not opening new leaf ([da71c1a](https://github.com/alx-plugins/alx-folder-note/commit/da71c1a1a31a4007f23413ec0d58038592221ce8)) 308 | * **note-handler.ts:** hideAll() fail to revert all hidden folder notes ([389300f](https://github.com/alx-plugins/alx-folder-note/commit/389300f1caa9c537a62f809f07f1a46ca6a61fec)) 309 | * **settings.ts:** expose hideNoteInExplorer in setting tab ([f77202c](https://github.com/alx-plugins/alx-folder-note/commit/f77202c5feba83af76a557509f4e03407e3eff81)) 310 | 311 | 312 | ### Features 313 | 314 | * **find.ts:** findFolderFromNote() now accepts path strings ([f490125](https://github.com/alx-plugins/alx-folder-note/commit/f4901255dcc9622e8f1c15511e6ef7e8159491c5)) 315 | * **settings.ts:** add live update to folderNotePref and indexName ([3e42e67](https://github.com/alx-plugins/alx-folder-note/commit/3e42e670969215020719594659f034c1154a4ca5)) 316 | * **vault-handler.ts:** folder note rename/mv now update hide state and folder ([8fec0ee](https://github.com/alx-plugins/alx-folder-note/commit/8fec0eeaff887b7f1a0e9072261d7a5e46e00d6f)) 317 | * add types for undocumented FileExplorerView ([d1d673d](https://github.com/alx-plugins/alx-folder-note/commit/d1d673dacf65458efd3b39d7992476868911e1d8)) 318 | * port core function from fork.dev ([6b24c46](https://github.com/alx-plugins/alx-folder-note/commit/6b24c4606d336fcc41ffd72fce153d6379d611ca)) 319 | * support create new folder note ([2a1026f](https://github.com/alx-plugins/alx-folder-note/commit/2a1026f36670df846394963b02ab82fc59bc2bde)) 320 | * update method to unload and load when layout is already ready ([521294b](https://github.com/alx-plugins/alx-folder-note/commit/521294bfa878697d25bc784fcc59d800038f4ff1)) 321 | * **settings.ts:** port settings from xpgo's ([f8c2e2f](https://github.com/alx-plugins/alx-folder-note/commit/f8c2e2fa9435e49cc7b333b3e61bc8f0920a7cc2)) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2021-present AidenLx 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AidenLx's Folder Note 2 | 3 | Add description, summary and more info to folders with folder notes. 4 | 5 | alx-folder-note is a maintained, completely rewritten, and improved folder note plugin that covers all core features of the original [folder note plugin](https://github.com/xpgo/obsidian-folder-note-plugin)) (except folder overview), with enhanced file explorer patches and lots of QoL improvements, aiming to make the folder with note work as seamless as native files.) 6 | 7 | Special thanks to [xpgo](https://github.com/xpgo), the author of original [folder note plugin](https://github.com/xpgo/obsidian-folder-note-plugin)! 8 | 9 | ![demo](https://user-images.githubusercontent.com/31102694/128635308-0a58279e-8bf0-4608-9330-fe11180953dd.png) 10 | 11 | Note: 12 | 13 | 1. Starting from v0.13.0, Folder Overview (folderv) has become an optional component, you can go to the Folder Overview section of the setting tab to install it 14 | 2. Starting from v0.11.0, this plugin require [folder-note-core](https://github.com/aidenlx/folder-note-core) to work properly. For old user, check [here](https://github.com/aidenlx/alx-folder-note/wiki/migrate-from-v0.10.0-and-lower) for migration guide. 15 | 16 | ## Intro 17 | 18 | - [create folder note](https://github.com/aidenlx/alx-folder-note/wiki/create-folder-note) easily, with [mulitple preferences](https://github.com/aidenlx/alx-folder-note/wiki/folder-note-pref) and [template support](https://github.com/aidenlx/alx-folder-note/wiki/core-settings#template) 19 | - folder and folder note working as one 20 | - [click on folder in file explorer to open folder note](https://github.com/aidenlx/alx-folder-note/wiki/open-folder-note-from-folder) 21 | - [folder and linked note synced as one](https://github.com/aidenlx/alx-folder-note/wiki/core-settings#auto-rename): change folder name from folder note; folder note moves with your folder 22 | - [folder note hidden from file explorer](https://github.com/aidenlx/alx-folder-note/wiki/core-settings#hide-note-in-explorer) 23 | - [reveal linked folder in file explorer](https://github.com/aidenlx/alx-folder-note/wiki/core-settings#hide-note-in-explorer)
24 | - [delete folder within folder note](https://github.com/aidenlx/alx-folder-note/wiki/delete-folder-from-folder-note) 25 | - [create folder overview](https://github.com/aidenlx/alx-folder-note/wiki/folder-overview) with codeblock `folderv` (Optional) 26 | - view all files within folder with brief cards 27 | - specify title and description in frontmatter (with [customizable field name](https://github.com/aidenlx/alx-folder-note/wiki/folderv-settings#field-names)) 28 | - [fetch title from h1](https://github.com/aidenlx/alx-folder-note/wiki/folderv-settings#h1-as-title-source) if title not specified 29 | - [filter files](https://github.com/aidenlx/alx-folder-note/wiki/folderv-options#filter) with regex/glob 30 | - [sort files](https://github.com/aidenlx/alx-folder-note/wiki/folderv-options#sort) by name/create time/last modified time 31 | - folder focus mode: right-click on folder in file explorer and select Toggle Focus can dim other folders and files outside of selected folder, select option again to revert ![CleanShot_2021-11-29_at_18 30 53](https://user-images.githubusercontent.com/31102694/166448049-aea0457a-d19f-4b29-8f7c-b66b5bd26629.gif) 32 | - you can also long press with mouse on the folder name in file explorer to toggle folder focus (disabled by default, desktop only) 33 | - [folder icon in file explorer](https://github.com/aidenlx/alx-folder-note/issues/11) 34 | More to come: 35 | 36 | - [ ] `folderv`: show [children specified in dataview/frontmatter fields](https://github.com/SkepticMystic/breadcrumbs/wiki/Relationships---Basics) (works with [relation-resolver plugin](https://github.com/aidenlx/relation-resolver)) 37 | - [ ] `folderv`: list view 38 | 39 | ## How to use 40 | 41 | Check [wiki](https://github.com/aidenlx/alx-folder-note/wiki) for more details 42 | 43 | ## Compatibility 44 | 45 | The required API feature is only available for Obsidian v0.14.8+. 46 | 47 | ## Installation 48 | 49 | Note: Starting from v0.11.0, this plugin require [folder-note-core](https://github.com/aidenlx/folder-note-core) to work properly, which is also available on community plugins list. 50 | 51 | ### From Obsidian 52 | 53 | 1. Open `Settings` > `Third-party plugin` 54 | 2. Make sure Safe mode is **off** 55 | 3. Click `Browse community plugins` 56 | 4. Search for this plugin 57 | 5. Click `Install` 58 | 6. Once installed, close the community plugins window and the plugin is ready to use. 59 | 60 | ### From GitHub 61 | 62 | 1. Download the Latest Release from the Releases section of the GitHub Repository 63 | 2. Put files to your vault's plugins folder: `/.obsidian/plugins/alx-folder-note` 64 | 3. Reload Obsidian 65 | 4. If prompted about Safe Mode, you can disable safe mode and enable the plugin. 66 | Otherwise, head to Settings, third-party plugins, make sure safe mode is off and 67 | enable the plugin from there. 68 | 69 | > Note: The `.obsidian` folder may be hidden. On macOS, you should be able to press `Command+Shift+Dot` to show the folder in Finder. 70 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import obPlugin from "@aidenlx/esbuild-plugin-obsidian"; 2 | import { build, context } from "esbuild"; 3 | import { lessLoader } from "esbuild-plugin-less"; 4 | 5 | const banner = `/* 6 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 7 | if you want to view the source visit the plugins github repository 8 | */ 9 | `; 10 | 11 | const isProd = process.env.BUILD === "production"; 12 | 13 | const opts = { 14 | entryPoints: ["src/fn-main.ts"], 15 | bundle: true, 16 | platform: "browser", 17 | external: ["obsidian"], 18 | format: "cjs", 19 | mainFields: ["browser", "module", "main"], 20 | banner: { js: banner }, 21 | sourcemap: isProd ? false : "inline", 22 | minify: isProd, 23 | define: { 24 | "process.env.NODE_ENV": JSON.stringify(process.env.BUILD), 25 | }, 26 | outfile: "build/main.js", 27 | plugins: [ 28 | lessLoader({ 29 | javascriptEnabled: true, 30 | }), 31 | obPlugin(), 32 | ], 33 | }; 34 | 35 | if (!isProd) { 36 | const ctx = await context({ ...opts, logLevel: "error" }); 37 | try { 38 | await ctx.watch(); 39 | } catch (err) { 40 | console.error(err); 41 | await cleanup(); 42 | } 43 | process.on("SIGINT", cleanup); 44 | // eslint-disable-next-line no-inner-declarations 45 | async function cleanup() { 46 | await ctx.dispose(); 47 | // scheduleOnDisposeCallbacks defer function calls using setTimeout(...,0) 48 | // so we need to wait a bit before exiting 49 | // setTimeout(() => process.exit(), 100); 50 | } 51 | } else { 52 | await build(opts); 53 | } 54 | -------------------------------------------------------------------------------- /manifest-beta.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "alx-folder-note", 3 | "name": "AidenLx's Folder Note", 4 | "version": "0.16.5", 5 | "minAppVersion": "1.1.0", 6 | "description": "Add description, summary and more info to folders with folder notes.", 7 | "author": "AidenLx", 8 | "authorUrl": "https://github.com/aidenlx", 9 | "isDesktopOnly": false 10 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "alx-folder-note", 3 | "name": "AidenLx's Folder Note", 4 | "version": "0.16.5", 5 | "minAppVersion": "1.1.0", 6 | "description": "Add description, summary and more info to folders with folder notes.", 7 | "author": "AidenLx", 8 | "authorUrl": "https://github.com/aidenlx", 9 | "isDesktopOnly": false 10 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@aidenlx/folder-note", 3 | "version": "0.16.5", 4 | "description": "Add description, summary and more info to folders with folder notes.", 5 | "scripts": { 6 | "dev": "cross-env BUILD=dev node esbuild.config.mjs", 7 | "check": "tsc --noEmit", 8 | "build": "cross-env BUILD=production node esbuild.config.mjs", 9 | "prettier": "prettier --write 'src/**/*.+(ts|tsx|json|html|css)'", 10 | "eslint": "eslint . --ext .ts,.tsx --fix", 11 | "release": "release-it" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "@aidenlx/esbuild-plugin-obsidian": "^0.1.8", 18 | "@aidenlx/folder-note-core": "^1.3.3", 19 | "@aidenlx/obsidian-icon-shortcodes": "^0.9.0", 20 | "@aidenlx/obsidian-plugin-bumper": "^0.1.12", 21 | "@release-it/bumper": "^4.0.0", 22 | "@release-it/conventional-changelog": "^5.1.0", 23 | "@types/array.prototype.flat": "^1.2.1", 24 | "@types/node": "^17.0.23", 25 | "@typescript-eslint/eslint-plugin": "^5.55.0", 26 | "@typescript-eslint/parser": "^5.55.0", 27 | "array.prototype.flat": "^1.2.5", 28 | "assert-never": "^1.2.1", 29 | "conventional-changelog-angular": "^5.0.13", 30 | "cross-env": "^7.0.3", 31 | "cz-conventional-changelog": "^3.3.0", 32 | "esbuild": "^0.17.4", 33 | "esbuild-plugin-less": "^1.1.6", 34 | "eslint": "~8.36.0", 35 | "eslint-config-prettier": "^8.5.0", 36 | "eslint-import-resolver-typescript": "^2.7.0", 37 | "eslint-plugin-import": "^2.26.0", 38 | "eslint-plugin-jsdoc": "^38.0.6", 39 | "eslint-plugin-prefer-arrow": "~1.2.3", 40 | "eslint-plugin-prettier": "^4.2.1", 41 | "eslint-plugin-simple-import-sort": "~7.0.0", 42 | "fast-deep-equal": "^3.1.3", 43 | "monkey-around": "^2.3.0", 44 | "obsidian": "~1.2.8", 45 | "path-browserify": "^1.0.1", 46 | "prettier": "^2.6.1", 47 | "release-it": "^15.4.2", 48 | "semver": "^7.3.5", 49 | "typescript": "~5.0.4" 50 | }, 51 | "browser": { 52 | "path": "path-browserify" 53 | }, 54 | "config": { 55 | "commitizen": { 56 | "path": "./node_modules/cz-conventional-changelog" 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /scripts/ob-bumper.mjs: -------------------------------------------------------------------------------- 1 | import { promises } from "fs"; 2 | import { copyFile } from "fs/promises"; 3 | import { join } from "path"; 4 | const { readFile, writeFile } = promises; 5 | import { Plugin } from "release-it"; 6 | import semverPrerelease from "semver/functions/prerelease.js"; 7 | 8 | const mainManifest = "manifest.json", 9 | betaManifest = "manifest-beta.json", 10 | versionsList = "versions.json"; 11 | const targets = [mainManifest, betaManifest, versionsList]; 12 | 13 | const isPreRelease = (version) => semverPrerelease(version) !== null; 14 | 15 | class ObsidianVersionBump extends Plugin { 16 | async readJson(path) { 17 | // const { isDryRun } = this.config; 18 | try { 19 | const result = JSON.parse(await readFile(path, "utf8")); 20 | return result; 21 | } catch (error) { 22 | if (error.code === "ENOENT") { 23 | return null; 24 | } 25 | throw error; 26 | } 27 | } 28 | async writeJson(file, data) { 29 | // const { isDryRun } = this.config; 30 | const { indent = 4 } = this.getContext(); 31 | await writeFile(file, JSON.stringify(data, null, indent)); 32 | // this.log.exec(`Write to ${file}`, isDryRun); 33 | } 34 | 35 | /** 36 | * always read from previous version of manifest 37 | */ 38 | async readManifest() { 39 | const { isDryRun } = this.config; 40 | const latest = isPreRelease(this.config.contextOptions.latestVersion); 41 | let manifestToRead = this.getManifest(latest); 42 | this.log.exec(`Reading manifest from ${manifestToRead}`, isDryRun); 43 | let manifest; 44 | if (!(manifest = await this.readJson(manifestToRead))) { 45 | manifestToRead = this.getManifest(!latest); 46 | this.log.exec(`retry reading manifest from ${manifestToRead}`, isDryRun); 47 | manifest = await this.readJson(manifestToRead); 48 | } 49 | if (!manifest) throw new Error("Missing manifest data"); 50 | return manifest; 51 | } 52 | 53 | async writeManifest(targetVersion, manifest) { 54 | const { isDryRun } = this.config; 55 | const manifestToWrite = this.getManifest(isPreRelease(targetVersion)); 56 | const updatedMainfest = { ...manifest, version: targetVersion }; 57 | !isDryRun && (await this.writeJson(manifestToWrite, updatedMainfest)); 58 | this.log.exec( 59 | `Wrote version ${targetVersion} to ${manifestToWrite}`, 60 | isDryRun, 61 | ); 62 | await this.syncManifest(targetVersion); 63 | } 64 | 65 | /** 66 | * if bump to official release 67 | * sync manifest-beta.json with manifest.json 68 | */ 69 | async syncManifest(targetVersion) { 70 | const target = isPreRelease(targetVersion); 71 | const { isDryRun } = this.config; 72 | if (!target) { 73 | !isDryRun && (await copyFile(mainManifest, betaManifest)); 74 | this.log.exec(`Syncing ${betaManifest} with ${mainManifest}`, isDryRun); 75 | } 76 | } 77 | 78 | async copyToRoot() { 79 | const { copyTo } = this.getContext(); 80 | if (!copyTo) return; 81 | const { isDryRun } = this.config; 82 | if (!isDryRun) { 83 | await Promise.all( 84 | targets.map((file) => 85 | copyFile(file, join(copyTo, file)).catch((err) => { 86 | if (err.code !== "ENOENT") throw err; 87 | }), 88 | ), 89 | ); 90 | } 91 | this.log.exec(`Copied ${targets.join(", ")} to ${copyTo}`, isDryRun); 92 | } 93 | 94 | /** 95 | * update versions.json with target version and minAppVersion from manifest.json 96 | */ 97 | async writeVersion(targetVersion, { minAppVersion }) { 98 | const { isDryRun } = this.config; 99 | const versions = await this.readJson(versionsList); 100 | versions[targetVersion] = minAppVersion; 101 | !isDryRun && (await this.writeJson(versionsList, versions)); 102 | this.log.exec( 103 | `Wrote version ${targetVersion} to ${versionsList}`, 104 | isDryRun, 105 | ); 106 | } 107 | 108 | getManifest(isPreRelease) { 109 | return isPreRelease ? betaManifest : mainManifest; 110 | } 111 | 112 | async bump(targetVersion) { 113 | // read minAppVersion from manifest and bump version to target version 114 | const manifest = await this.readManifest(targetVersion); 115 | this.log.info(`min obsidian app version: ${manifest.minAppVersion}`); 116 | await Promise.all([ 117 | this.writeManifest(targetVersion, manifest), 118 | this.writeVersion(targetVersion, manifest), 119 | ]); 120 | await this.copyToRoot(); 121 | } 122 | } 123 | export default ObsidianVersionBump; 124 | -------------------------------------------------------------------------------- /src/click-handler.ts: -------------------------------------------------------------------------------- 1 | import type { FolderItem } from "obsidian"; 2 | import { Platform } from "obsidian"; 3 | 4 | import type ALxFolderNote from "./fn-main"; 5 | import { isModifier, getFileItemInnerTitleEl } from "./misc"; 6 | import type { LongPressEvent } from "./modules/long-press"; 7 | 8 | export const getClickHandler = (plugin: ALxFolderNote) => { 9 | const { getFolderNote, getFolderNotePath, getNewFolderNote } = plugin.CoreApi; 10 | return async (item: FolderItem, evt: MouseEvent): Promise => { 11 | if ( 12 | !item || 13 | (Platform.isMobile && !plugin.settings.mobileClickToOpen) || 14 | // allow folder shift selection to work 15 | evt.shiftKey || 16 | // triggered only when click on title 17 | !( 18 | getFileItemInnerTitleEl(item) === evt.target || 19 | getFileItemInnerTitleEl(item).contains(evt.target as Node) 20 | ) || 21 | // ignore file being renamed 22 | item.fileExplorer.fileBeingRenamed === item.file 23 | ) 24 | return false; 25 | 26 | if (evt.type === "auxclick" && evt.button !== 1) return false; 27 | 28 | // get the folder path 29 | const folder = item.file; 30 | const createNew = 31 | (evt.type === "click" && 32 | isModifier(evt, plugin.settings.modifierForNewNote)) || 33 | (evt.type === "auxclick" && evt.button === 1); 34 | try { 35 | // check if folder note exists 36 | let folderNote = getFolderNote(folder), 37 | fnPath; 38 | if (createNew && !folderNote && (fnPath = getFolderNotePath(folder))) { 39 | folderNote = await plugin.app.vault.create( 40 | fnPath.path, 41 | getNewFolderNote(folder), 42 | ); 43 | } 44 | 45 | if (!folderNote) return false; 46 | 47 | // show the note 48 | await plugin.app.workspace.openLinkText( 49 | folderNote.path, 50 | "", 51 | createNew || evt.type === "auxclick", 52 | { active: true }, 53 | ); 54 | if (plugin.settings.expandFolderOnClick && item.collapsed) 55 | await item.setCollapsed(false); 56 | return true; 57 | } catch (error) { 58 | console.error(error); 59 | return false; 60 | } 61 | }; 62 | }; 63 | export const pressHandler = ( 64 | item: FolderItem, 65 | _evt: LongPressEvent, 66 | ): boolean => { 67 | if (!item || item.fileExplorer.fileBeingRenamed === item.file) return false; 68 | const folder = item.file; 69 | item.fileExplorer.folderNoteUtils?.folderFocus.toggleFocusFolder(folder); 70 | return true; 71 | }; 72 | -------------------------------------------------------------------------------- /src/drag-patch.ts: -------------------------------------------------------------------------------- 1 | import { around } from "monkey-around"; 2 | import type { ClipboardManager, DragManager, MarkdownView } from "obsidian"; 3 | import { Platform, TFolder, WorkspaceLeaf } from "obsidian"; 4 | 5 | import type ALxFolderNote from "./fn-main"; 6 | 7 | declare global { 8 | var i18next: any; 9 | } 10 | declare module "obsidian" { 11 | interface App { 12 | dragManager: DragManager; 13 | getObsidianUrl(file: TFile): string; 14 | } 15 | interface DragInfo { 16 | source?: string; 17 | type: string; 18 | icon: string; 19 | title: string; 20 | file?: TFolder | TFile; 21 | } 22 | interface DragFolderInfo extends DragInfo { 23 | type: "folder"; 24 | file: TFolder; 25 | } 26 | interface DragFileInfo extends DragInfo { 27 | type: "file"; 28 | file: TFile; 29 | } 30 | interface DragFilesInfo extends DragInfo { 31 | type: "files"; 32 | files: TFile[]; 33 | } 34 | class DragManager { 35 | draggable: DragInfo | null; 36 | setAction: (action: string) => any; 37 | dragFile(evt: DragEvent, file: TFile, source?: string): DragFolderInfo; 38 | dragFiles(evt: DragEvent, files: TFile[], source?: string): DragFilesInfo; 39 | dragFolder( 40 | evt: DragEvent, 41 | folder: TFolder, 42 | source?: string, 43 | ): DragFolderInfo; 44 | // handleDrop: ( 45 | // el: HTMLElement, 46 | // handler: ( 47 | // evt: DragEvent, 48 | // dragable: DragInfo, 49 | // draging: boolean, 50 | // ) => { 51 | // action: string; 52 | // dropEffect: string; 53 | // hoverEl?: HTMLElement; 54 | // hoverClass?: string; 55 | // }, 56 | // arg0: boolean, 57 | // ) => void; 58 | } 59 | interface MarkdownView { 60 | editMode?: MarkdownEditView; 61 | sourceMode?: MarkdownEditView; 62 | } 63 | interface MarkdownEditView { 64 | clipboardManager: ClipboardManager; 65 | } 66 | class ClipboardManager { 67 | app: App; 68 | handleDrop: (evt: DragEvent) => boolean; 69 | handleDragOver: (evt: DragEvent) => void; 70 | } 71 | } 72 | 73 | const HD = { 74 | none: [], 75 | copy: ["copy"], 76 | copyLink: ["copy", "link"], 77 | copyMove: ["copy", "move"], 78 | link: ["link"], 79 | linkMove: ["link", "move"], 80 | move: ["move"], 81 | all: ["copy", "link", "move"], 82 | uninitialized: [], 83 | } as const; 84 | function VD(e: DragEvent, t: DataTransfer["dropEffect"]) { 85 | t && 86 | (function (e, t) { 87 | if ("none" === t) return !0; 88 | const n = HD[e.dataTransfer!.effectAllowed]; 89 | return !!n && (n as any).contains(t); 90 | })(e, t) && 91 | (e.dataTransfer!.dropEffect = t); 92 | } 93 | 94 | const getMarkdownView = () => { 95 | const leaves = app.workspace.getLeavesOfType("markdown"); 96 | if (leaves.length > 0) { 97 | return leaves[0].view as MarkdownView; 98 | } else return null; 99 | }; 100 | 101 | const PatchDragManager = (plugin: ALxFolderNote) => { 102 | const { getFolderNote } = plugin.CoreApi; 103 | 104 | const patchClipboardManager = (): boolean => { 105 | const view = getMarkdownView(); 106 | if (!view) return false; 107 | const editMode = view.editMode ?? view.sourceMode; 108 | 109 | if (!editMode) 110 | throw new Error("Failed to patch clipboard manager: no edit view found"); 111 | 112 | plugin.register( 113 | around( 114 | editMode.clipboardManager.constructor.prototype as ClipboardManager, 115 | { 116 | handleDragOver: (next) => 117 | function (this: ClipboardManager, evt, ...args) { 118 | const { draggable } = this.app.dragManager; 119 | if ( 120 | draggable && 121 | !(Platform.isMacOS ? evt.shiftKey : evt.altKey) && 122 | draggable.file instanceof TFolder && 123 | getFolderNote(draggable.file) 124 | ) { 125 | // evt.preventDefault(); 126 | VD(evt, "link"); 127 | this.app.dragManager.setAction( 128 | i18next.t("interface.drag-and-drop.insert-link-here"), 129 | ); 130 | } else { 131 | next.call(this, evt, ...args); 132 | } 133 | }, 134 | handleDrop: (next) => 135 | function (this: ClipboardManager, evt, ...args) { 136 | const fallback = () => next.call(this, evt, ...args); 137 | const { draggable } = plugin.app.dragManager; 138 | let note; 139 | if ( 140 | draggable?.type === "folder" && 141 | draggable.file instanceof TFolder && 142 | (note = getFolderNote(draggable.file)) 143 | ) { 144 | draggable.file = note; 145 | draggable.type = "file"; 146 | } 147 | return fallback(); 148 | }, 149 | }, 150 | ), 151 | ); 152 | console.log("alx-folder-note: clipboard manager patched"); 153 | return true; 154 | }; 155 | 156 | plugin.app.workspace.onLayoutReady(() => { 157 | if (!patchClipboardManager()) { 158 | const evt = app.workspace.on("layout-change", () => { 159 | patchClipboardManager() && app.workspace.offref(evt); 160 | }); 161 | plugin.registerEvent(evt); 162 | } 163 | }); 164 | 165 | plugin.register( 166 | around(app.dragManager.constructor.prototype as DragManager, { 167 | dragFolder: (next) => 168 | function (this: DragManager, evt, folder, source, ...args) { 169 | let note; 170 | if ((note = getFolderNote(folder))) { 171 | const url = app.getObsidianUrl(note); 172 | evt.dataTransfer!.setData("text/plain", url); 173 | evt.dataTransfer!.setData("text/uri-list", url); 174 | } 175 | return next.call(this, evt, folder, source, ...args); 176 | }, 177 | }), 178 | ); 179 | }; 180 | export default PatchDragManager; 181 | -------------------------------------------------------------------------------- /src/fe-handler/active-folder.ts: -------------------------------------------------------------------------------- 1 | import type { FileExplorerView, TFolder, WorkspaceLeaf } from "obsidian"; 2 | import { MarkdownView } from "obsidian"; 3 | 4 | import type ALxFolderNote from "../fn-main"; 5 | import { getFileItemTitleEl } from "../misc"; 6 | import FEHandler_Base from "./base"; 7 | 8 | const isActiveClass = "is-active"; 9 | 10 | export default class ActiveFolder extends FEHandler_Base { 11 | queues = {}; 12 | constructor(plugin: ALxFolderNote, fileExplorer: FileExplorerView) { 13 | super(plugin, fileExplorer); 14 | const { workspace } = plugin.app; 15 | this.handleActiveLeafChange(workspace.activeLeaf); 16 | plugin.registerEvent( 17 | workspace.on( 18 | "active-leaf-change", 19 | this.handleActiveLeafChange.bind(this), 20 | ), 21 | ); 22 | this.plugin.register(() => (this.activeFolder = null)); 23 | } 24 | private _activeFolder: TFolder | null = null; 25 | public set activeFolder(folder: TFolder | null) { 26 | const getTitleEl = (folder: TFolder | null) => 27 | folder && this.fileExplorer.fileItems[folder.path] 28 | ? getFileItemTitleEl(this.fileExplorer.fileItems[folder.path]) 29 | : undefined; 30 | if (!folder) { 31 | getTitleEl(this._activeFolder)?.removeClass(isActiveClass); 32 | } else if (folder !== this._activeFolder) { 33 | getTitleEl(this._activeFolder)?.removeClass(isActiveClass); 34 | getTitleEl(folder)?.addClass(isActiveClass); 35 | } 36 | this._activeFolder = folder; 37 | } 38 | public get activeFolder(): TFolder | null { 39 | return this._activeFolder; 40 | } 41 | private handleActiveLeafChange(leaf: WorkspaceLeaf | null) { 42 | let folder; 43 | if ( 44 | leaf && 45 | leaf.view instanceof MarkdownView && 46 | (folder = this.fncApi.getFolderFromNote(leaf.view.file)) 47 | ) { 48 | this.activeFolder = folder; 49 | } else { 50 | this.activeFolder = null; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/fe-handler/base.ts: -------------------------------------------------------------------------------- 1 | import "./file-explorer.less"; 2 | 3 | import type { AFItem, FileExplorerView } from "obsidian"; 4 | import { debounce } from "obsidian"; 5 | 6 | import type ALxFolderNote from "../fn-main"; 7 | import type { afItemMark } from "../misc"; 8 | 9 | export default abstract class FEHandler_Base { 10 | longPressRegistered = new WeakSet(); 11 | constructor( 12 | public plugin: ALxFolderNote, 13 | public fileExplorer: FileExplorerView, 14 | ) {} 15 | get fncApi() { 16 | return this.plugin.CoreApi; 17 | } 18 | get app() { 19 | return this.plugin.app; 20 | } 21 | get files() { 22 | return this.fileExplorer.files; 23 | } 24 | getAfItem = (path: string): afItemMark | null => 25 | this.fileExplorer.fileItems[path] ?? null; 26 | iterateItems = (callback: (item: AFItem) => any): void => 27 | Object.values(this.fileExplorer.fileItems).forEach(callback); 28 | 29 | abstract queues: Record< 30 | string, 31 | { 32 | action: (id: string, ...args: any[]) => any; 33 | queue: Set | Map; 34 | } 35 | >; 36 | private debouncers = {} as Record any>; 37 | private _execQueue(queueName: string) { 38 | const { action, queue } = this.queues[queueName]; 39 | if (queue.size <= 0) return; 40 | if (queue instanceof Set) { 41 | queue.forEach((id) => action(id)); 42 | } else { 43 | queue.forEach((args, id) => action(id, ...args)); 44 | } 45 | queue.clear(); 46 | } 47 | protected execQueue(queueName: string) { 48 | if (!Object.keys(this.queues).includes(queueName)) return; 49 | const debouncer = 50 | this.debouncers[queueName] ?? 51 | (this.debouncers[queueName] = debounce( 52 | () => this._execQueue(queueName), 53 | 200, 54 | true, 55 | )); 56 | debouncer(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/fe-handler/file-explorer.less: -------------------------------------------------------------------------------- 1 | body:not(.alx-no-hide-note) .nav-file.alx-folder-note > .nav-file-title { 2 | display: none; 3 | } 4 | 5 | .nav-folder.alx-folder-with-note { 6 | & > .nav-folder-title > .nav-folder-title-content { 7 | /* color: var(--text-accent); */ 8 | text-decoration-line: underline; 9 | text-decoration-color: var(--text-faint); 10 | text-decoration-thickness: 2px; 11 | text-underline-offset: 1px; 12 | } 13 | 14 | &.alx-empty-folder > .nav-folder-title > .nav-folder-collapse-indicator { 15 | visibility: hidden; 16 | } 17 | } 18 | 19 | body.is-mobile:not(.alx-no-click-on-mobile) 20 | .nav-folder 21 | > .nav-folder-title 22 | > .nav-folder-collapse-indicator { 23 | width: 28px; 24 | // justify-content: center; 25 | } 26 | -------------------------------------------------------------------------------- /src/fe-handler/focus.less: -------------------------------------------------------------------------------- 1 | .nav-files-container.alx-folder-focus .nav-folder:not(.mod-root) { 2 | .nav-file-title, 3 | .nav-folder-title { 4 | opacity: 0.2; 5 | } 6 | &.alx-focused-folder { 7 | .nav-file-title, 8 | .nav-folder-title { 9 | opacity: 1; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/fe-handler/folder-focus.ts: -------------------------------------------------------------------------------- 1 | import "./focus.less"; 2 | 3 | import type { FileExplorerView, FolderItem } from "obsidian"; 4 | import { TFolder } from "obsidian"; 5 | 6 | import type ALxFolderNote from "../fn-main"; 7 | import FEHandler_Base from "./base"; 8 | 9 | const focusedFolderCls = "alx-focused-folder"; 10 | const focusModeCls = "alx-folder-focus"; 11 | 12 | export default class FolderFocus extends FEHandler_Base { 13 | queues = {}; 14 | constructor(plugin: ALxFolderNote, fileExplorer: FileExplorerView) { 15 | super(plugin, fileExplorer); 16 | const { workspace } = plugin.app; 17 | this.plugin.register( 18 | () => this.focusedFolder && this.toggleFocusFolder(null), 19 | ); 20 | 21 | [ 22 | workspace.on("file-menu", (menu, af) => { 23 | if (!(af instanceof TFolder) || af.isRoot()) return; 24 | menu.addItem((item) => 25 | item 26 | .setTitle("Toggle Focus") 27 | .setIcon("crossed-star") 28 | .onClick(() => this.toggleFocusFolder(af)), 29 | ); 30 | }), 31 | ].forEach(this.plugin.registerEvent.bind(this.plugin)); 32 | } 33 | 34 | private _focusedFolder: { 35 | folder: FolderItem; 36 | collapsedCache: boolean; 37 | } | null = null; 38 | get focusedFolder() { 39 | return this._focusedFolder?.folder ?? null; 40 | } 41 | set focusedFolder(item: FolderItem | null) { 42 | // restore previous folder collapse state 43 | if (this._focusedFolder) { 44 | const { folder, collapsedCache } = this._focusedFolder; 45 | if (folder.collapsed !== collapsedCache) 46 | folder.setCollapsed(collapsedCache); 47 | } 48 | this._focusedFolder = item 49 | ? { folder: item, collapsedCache: item.collapsed } 50 | : null; 51 | // unfold folder if it's collapsed 52 | if (item && item.collapsed) { 53 | item.setCollapsed(false); 54 | // @ts-ignore 55 | this.plugin.app.nextFrame(() => { 56 | // @ts-ignore 57 | this.fileExplorer.dom.infinityScroll.computeSync(); 58 | // @ts-ignore 59 | this.fileExplorer.dom.infinityScroll.scrollIntoView(item); 60 | }); 61 | } 62 | this.fileExplorer.dom.navFileContainerEl.toggleClass(focusModeCls, !!item); 63 | } 64 | toggleFocusFolder(folder: TFolder | null) { 65 | const folderItem = folder 66 | ? (this.getAfItem(folder.path) as FolderItem | null) 67 | : null; 68 | if (this.focusedFolder) { 69 | this._focusFolder(this.focusedFolder, true); 70 | } 71 | // if given same folder as current cached, toggle it off 72 | if (folderItem && folderItem.file.path === this.focusedFolder?.file.path) { 73 | this.focusedFolder = null; 74 | } else { 75 | folderItem && this._focusFolder(folderItem, false); 76 | this.focusedFolder = folderItem; 77 | } 78 | } 79 | private _focusFolder(folder: FolderItem, revert = false) { 80 | if (folder.file.isRoot()) throw new Error("Cannot focus on root dir"); 81 | folder.el.toggleClass(focusedFolderCls, !revert); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/fe-handler/folder-icon.less: -------------------------------------------------------------------------------- 1 | @folder-title-selector: .nav-folder-title > .nav-folder-title-content; 2 | 3 | .alx-folder-icons .nav-folder.alx-folder-with-note[data-icon] { 4 | &[data-icon-type="emoji"] > @{folder-title-selector}::before { 5 | content: var(--alx-folder-icon-txt); 6 | background-image: var(--alx-folder-icon-url); 7 | height: unset !important; 8 | vertical-align: text-top; 9 | } 10 | &[data-icon-type="svg"] > @{folder-title-selector}::before { 11 | content: " "; 12 | background-image: var(--alx-folder-icon-url); 13 | vertical-align: text-bottom; 14 | } 15 | /** basic style setup */ 16 | & > @{folder-title-selector}::before { 17 | width: 1em; 18 | height: 1em; 19 | margin-right: 3px; 20 | display: inline-block; 21 | background-size: contain; 22 | background-repeat: no-repeat; 23 | background-position: center center; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/fe-handler/folder-mark.ts: -------------------------------------------------------------------------------- 1 | import "./folder-icon.less"; 2 | 3 | import { dirname } from "path"; 4 | import type { 5 | AFItem, 6 | CachedMetadata, 7 | FileExplorerView, 8 | FolderItem, 9 | } from "obsidian"; 10 | import { TAbstractFile, TFile, TFolder } from "obsidian"; 11 | 12 | import type ALxFolderNote from "../fn-main"; 13 | import type { afItemMark } from "../misc"; 14 | import { isFolder } from "../misc"; 15 | import FEHandler_Base from "./base"; 16 | 17 | export const folderIconMark = "alx-folder-icons"; 18 | 19 | const folderNoteClass = "alx-folder-note"; 20 | const folderClass = "alx-folder-with-note"; 21 | const emptyFolderClass = "alx-empty-folder"; 22 | 23 | export default class FolderMark extends FEHandler_Base { 24 | constructor(plugin: ALxFolderNote, fileExplorer: FileExplorerView) { 25 | super(plugin, fileExplorer); 26 | this.initFolderMark(); 27 | if (this.plugin.settings.folderIcon) { 28 | this.initFolderIcon(); 29 | } 30 | if (this.plugin.settings.hideCollapseIndicator) { 31 | this.initHideCollapseIndicator(); 32 | } 33 | } 34 | queues = { 35 | mark: { 36 | queue: new Map(), 37 | action: (path: string, revert: boolean) => { 38 | const item = this.getAfItem(path); 39 | if (!item) { 40 | console.warn("no afitem found for path %s, escaping...", path); 41 | return; 42 | } 43 | if (isFolder(item)) { 44 | if (revert === !!item.isFolderWithNote) { 45 | item.el.toggleClass(folderClass, !revert); 46 | item.isFolderWithNote = revert ? undefined : true; 47 | if (this.plugin.settings.hideCollapseIndicator) 48 | item.el.toggleClass( 49 | emptyFolderClass, 50 | revert ? false : item.file.children.length === 1, 51 | ); 52 | } 53 | this._updateIcon(path, revert, item); 54 | } else if (revert === !!item.isFolderNote) { 55 | item.el.toggleClass(folderNoteClass, !revert); 56 | item.isFolderNote = revert ? undefined : true; 57 | } 58 | }, 59 | }, 60 | changedFolder: { 61 | queue: new Set(), 62 | action: (path: string) => { 63 | const note = this.fncApi.getFolderNote(path); 64 | if (note) { 65 | (this.getAfItem(path) as FolderItem)?.el.toggleClass( 66 | emptyFolderClass, 67 | note.parent.children.length === 1, 68 | ); 69 | } 70 | }, 71 | }, 72 | }; 73 | private initFolderMark() { 74 | const { vault, metadataCache } = this.app; 75 | this.markAll(); 76 | // #region folder note events setup 77 | [ 78 | vault.on("folder-note:create", (note: TFile, folder: TFolder) => { 79 | this.setMark(note); 80 | this.setMark(folder); 81 | }), 82 | vault.on("folder-note:delete", (note: TFile, folder: TFolder) => { 83 | this.setMark(note, true); 84 | this.setMark(folder, true); 85 | }), 86 | vault.on("folder-note:rename", () => { 87 | // fe-item in dom will be reused, do nothing for now 88 | }), 89 | vault.on("folder-note:cfg-changed", () => { 90 | this.markAll(true); 91 | window.setTimeout(this.markAll, 200); 92 | }), 93 | metadataCache.on("changed", (file) => { 94 | let folder; 95 | if ((folder = this.fncApi.getFolderFromNote(file))) { 96 | this.setMark(folder); 97 | } 98 | }), 99 | ].forEach(this.plugin.registerEvent.bind(this.plugin)); 100 | } 101 | // #region set class mark for folder notes and folders 102 | public setMark = ( 103 | target: AFItem | TAbstractFile | string, 104 | revert = false, 105 | ) => { 106 | if (!target) return; 107 | const { queue } = this.queues.mark; 108 | let path: string; 109 | if (target instanceof TAbstractFile) { 110 | path = target.path; 111 | } else if (typeof target === "string") { 112 | path = target; 113 | } else { 114 | path = target.file.path; 115 | } 116 | queue.set(path, [revert]); 117 | this.execQueue("mark"); 118 | }; 119 | public markAll = (revert = false) => { 120 | this.iterateItems((item: AFItem) => { 121 | if (isFolder(item) && !revert) { 122 | this.markFolderNote(item.file); 123 | } else if (revert) { 124 | this.setMark(item, true); 125 | } 126 | }); 127 | }; 128 | markFolderNote = (af: TAbstractFile): boolean => { 129 | if (af instanceof TFolder && af.isRoot()) return false; 130 | const { getFolderNote, getFolderFromNote } = this.fncApi; 131 | 132 | let found: TAbstractFile | null = null; 133 | if (af instanceof TFile) found = getFolderFromNote(af); 134 | else if (af instanceof TFolder) found = getFolderNote(af); 135 | 136 | if (found) { 137 | this.setMark(found); 138 | this.setMark(af); 139 | } else { 140 | this.setMark(af, true); 141 | } 142 | return !!found; 143 | }; 144 | // #endregion 145 | // #region folder icon setup 146 | private initFolderIcon() { 147 | document.body.toggleClass(folderIconMark, this.plugin.settings.folderIcon); 148 | const { vault } = this.app; 149 | const updateIcon = () => { 150 | for (const path of this.foldersWithIcon) { 151 | this.setMark(path); 152 | } 153 | }; 154 | [ 155 | vault.on("iconsc:initialized", updateIcon), 156 | vault.on("iconsc:changed", updateIcon), 157 | ].forEach(this.plugin.registerEvent.bind(this.plugin)); 158 | } 159 | foldersWithIcon = new Set(); 160 | private _updateIcon(path: string, revert: boolean, item: afItemMark) { 161 | const api = this.plugin.IconSCAPI; 162 | if (!api) return; 163 | 164 | let folderNotePath: string | undefined, 165 | metadata: CachedMetadata | undefined; 166 | const revertIcon = () => { 167 | delete item.el.dataset.icon; 168 | delete item.el.dataset["icon-type"]; 169 | this.foldersWithIcon.delete(path); 170 | item.el.style.removeProperty("--alx-folder-icon-txt"); 171 | item.el.style.removeProperty("--alx-folder-icon-url"); 172 | }; 173 | if (revert) { 174 | revertIcon(); 175 | } else if ( 176 | (folderNotePath = this.fncApi.getFolderNotePath(path)?.path) && 177 | (metadata = this.plugin.app.metadataCache.getCache(folderNotePath)) 178 | ) { 179 | let iconId = metadata.frontmatter?.icon, 180 | icon; 181 | if ( 182 | iconId && 183 | typeof iconId === "string" && 184 | (icon = api.getIcon(iconId, true)) 185 | ) { 186 | this.foldersWithIcon.add(path); 187 | item.el.dataset.icon = iconId.replace(/^:|:$/g, ""); 188 | if (!api.isEmoji(iconId)) { 189 | item.el.dataset.iconType = "svg"; 190 | item.el.style.setProperty("--alx-folder-icon-url", `url("${icon}")`); 191 | item.el.style.setProperty("--alx-folder-icon-txt", '" "'); 192 | } else { 193 | item.el.dataset.iconType = "emoji"; 194 | item.el.style.setProperty("--alx-folder-icon-url", '""'); 195 | item.el.style.setProperty("--alx-folder-icon-txt", `"${icon}"`); 196 | } 197 | } else if (item.el.dataset.icon) { 198 | revertIcon(); 199 | } 200 | } 201 | } 202 | // #endregion 203 | // #region set hide collapse indicator 204 | private initHideCollapseIndicator() { 205 | if (!this.plugin.settings.hideCollapseIndicator) return; 206 | const { vault } = this.app; 207 | [ 208 | vault.on("create", (file) => this.setChangedFolder(file.parent.path)), 209 | vault.on("delete", (file) => { 210 | const parent = dirname(file.path); 211 | this.setChangedFolder(parent === "." ? "/" : parent); 212 | }), 213 | vault.on("rename", (file, oldPath) => { 214 | this.setChangedFolder(file.parent.path); 215 | const parent = dirname(oldPath); 216 | this.setChangedFolder(parent === "." ? "/" : parent); 217 | }), 218 | ].forEach(this.plugin.registerEvent.bind(this.plugin)); 219 | } 220 | 221 | setChangedFolder = (folderPath: string) => { 222 | if (!folderPath || folderPath === "/") return; 223 | this.queues.changedFolder.queue.add(folderPath); 224 | this.execQueue("changedFolder"); 225 | }; 226 | // #endregion 227 | } 228 | -------------------------------------------------------------------------------- /src/fe-handler/index.ts: -------------------------------------------------------------------------------- 1 | import type { FileExplorerView } from "obsidian"; 2 | 3 | import type ALxFolderNote from "../fn-main"; 4 | import ActiveFolder from "./active-folder"; 5 | import FolderFocus from "./folder-focus"; 6 | import FolderMark from "./folder-mark"; 7 | 8 | const getFileExplorerHandlers = ( 9 | plugin: ALxFolderNote, 10 | fileExplorer: FileExplorerView, 11 | ) => ({ 12 | // initialized (mark folders, hook evt handlers...) when constructed 13 | plugin, 14 | folderFocus: new FolderFocus(plugin, fileExplorer), 15 | folderMark: new FolderMark(plugin, fileExplorer), 16 | activeFolder: new ActiveFolder(plugin, fileExplorer), 17 | }); 18 | 19 | export default getFileExplorerHandlers; 20 | -------------------------------------------------------------------------------- /src/fe-patch.ts: -------------------------------------------------------------------------------- 1 | import { around } from "monkey-around"; 2 | import type { 3 | FileExplorerPlugin as FEPluginCls, 4 | FileExplorerView as FEViewCls, 5 | FileExplorerView, 6 | FolderItem as FolderItemCls, 7 | TAbstractFile, 8 | } from "obsidian"; 9 | import { TFile, TFolder } from "obsidian"; 10 | 11 | import { getClickHandler, pressHandler } from "./click-handler"; 12 | import getFileExplorerHandlers from "./fe-handler"; 13 | import type ALxFolderNote from "./fn-main"; 14 | import { getViewOfType } from "./misc"; 15 | import type { LongPressEvent } from "./modules/long-press"; 16 | import AddLongPressEvt from "./modules/long-press"; 17 | 18 | const getFolderItemFromEl = (navEl: HTMLElement, view: FEViewCls) => { 19 | const folder = view.files.get(navEl); 20 | return folder instanceof TFolder 21 | ? (view.fileItems[folder.path] as FolderItemCls) 22 | : null; 23 | }; 24 | const Rt = (evt: MouseEvent, target: HTMLElement) => { 25 | const n = evt.relatedTarget; 26 | return !(n instanceof Node && target.contains(n)); 27 | }; 28 | /** 29 | * reset existing file explorer views 30 | */ 31 | const resetFileExplorer = async (plugin: ALxFolderNote) => { 32 | for (const leaf of plugin.app.workspace.getLeavesOfType("file-explorer")) { 33 | const state = leaf.getViewState(); 34 | await leaf.setViewState({ type: "empty" }); 35 | leaf.setViewState(state); 36 | } 37 | }; 38 | 39 | const PatchFileExplorer = (plugin: ALxFolderNote) => { 40 | const { getFolderFromNote } = plugin.CoreApi, 41 | clickHandler = getClickHandler(plugin); 42 | 43 | let FileExplorerViewInst: FEViewCls | null = getViewOfType( 44 | "file-explorer", 45 | plugin.app, 46 | ), 47 | FileExplorerPluginInst = 48 | plugin.app.internalPlugins.plugins["file-explorer"]?.instance; 49 | if (!FileExplorerViewInst || !FileExplorerPluginInst) return; 50 | 51 | // get constructors 52 | const FileExplorerView = FileExplorerViewInst.constructor as typeof FEViewCls, 53 | FileExplorerPlugin = 54 | FileExplorerPluginInst.constructor as typeof FEPluginCls, 55 | FolderItem = FileExplorerViewInst.createFolderDom( 56 | plugin.app.vault.getRoot(), 57 | ).constructor as typeof FolderItemCls; 58 | 59 | FileExplorerViewInst = null; 60 | 61 | const uninstallers: ReturnType[] = [ 62 | around(FileExplorerView.prototype, { 63 | load: (next) => 64 | function (this: FEViewCls) { 65 | const self = this; 66 | next.call(self); 67 | self.folderNoteUtils = getFileExplorerHandlers(plugin, self); 68 | AddLongPressEvt(plugin, self.dom.navFileContainerEl); 69 | self.containerEl.on( 70 | "auxclick", 71 | ".nav-folder", 72 | (evt: MouseEvent, navEl: HTMLElement) => { 73 | const item = getFolderItemFromEl(navEl, self); 74 | item && clickHandler(item, evt); 75 | }, 76 | ); 77 | self.containerEl.on( 78 | "long-press" as any, 79 | ".nav-folder", 80 | (evt: LongPressEvent, navEl: HTMLElement) => { 81 | const item = getFolderItemFromEl(navEl, self); 82 | item && pressHandler(item, evt); 83 | }, 84 | ); 85 | }, 86 | onFileMouseover: (next) => 87 | function (this: FileExplorerView, evt, navTitleEl) { 88 | next.call(this, evt, navTitleEl); 89 | if (!Rt(evt, navTitleEl)) return; 90 | const af = this.currentHoverFile; 91 | if ( 92 | !af || 93 | // if event is triggered on same file, do nothing 94 | (this._AFN_HOVER && this._AFN_HOVER === af) || 95 | !(af instanceof TFolder) 96 | ) 97 | return; 98 | const note = plugin.CoreApi.getFolderNote(af); 99 | if (note) { 100 | this.app.workspace.trigger("hover-link", { 101 | event: evt, 102 | source: "file-explorer", 103 | hoverParent: this, 104 | targetEl: navTitleEl, 105 | linktext: note.path, 106 | }); 107 | } 108 | // indicate that this file is handled by monkey patch 109 | this._AFN_HOVER = af; 110 | }, 111 | onFileMouseout: (next) => 112 | function (this: FileExplorerView, evt, navTitleEl) { 113 | next.call(this, evt, navTitleEl); 114 | if (!Rt(evt, navTitleEl)) return; 115 | delete this._AFN_HOVER; 116 | }, 117 | }), 118 | // patch reveal in folder to alter folder note target to linked folder 119 | around(FileExplorerPlugin.prototype, { 120 | revealInFolder: (next) => 121 | function (this: FEPluginCls, file: TAbstractFile) { 122 | if (file instanceof TFile && plugin.settings.hideNoteInExplorer) { 123 | const findResult = getFolderFromNote(file); 124 | if (findResult) file = findResult; 125 | } 126 | return next.call(this, file); 127 | }, 128 | }), 129 | around(FolderItem.prototype, { 130 | onTitleElClick: (next) => 131 | async function (this: FolderItemCls, evt) { 132 | // if folder note click not success, 133 | // fallback to default 134 | if (!(await clickHandler(this, evt))) next.call(this, evt); 135 | }, 136 | onSelfClick: (next) => 137 | async function (this: FolderItemCls, evt) { 138 | // if folder note click not success, 139 | // fallback to default 140 | if (!(await clickHandler(this, evt))) next.call(this, evt); 141 | }, 142 | }), 143 | ]; 144 | resetFileExplorer(plugin); 145 | plugin.register(() => { 146 | // uninstall monkey patches 147 | uninstallers.forEach((revert) => revert()); 148 | resetFileExplorer(plugin); 149 | }); 150 | }; 151 | export default PatchFileExplorer; 152 | -------------------------------------------------------------------------------- /src/fn-main.ts: -------------------------------------------------------------------------------- 1 | import "./main.less"; 2 | 3 | import type { FolderNoteAPI } from "@aidenlx/folder-note-core"; 4 | import { getApi as getFNCApi } from "@aidenlx/folder-note-core"; 5 | import { getApi as getISCApi } from "@aidenlx/obsidian-icon-shortcodes"; 6 | import { Notice, Plugin } from "obsidian"; 7 | 8 | import PatchDragManager from "./drag-patch"; 9 | import PatchFileExplorer from "./fe-patch"; 10 | import { ClickNotice } from "./misc"; 11 | import registerSetFolderIconCmd from "./modules/set-folder-icon"; 12 | import type { ALxFolderNoteSettings } from "./settings"; 13 | import { 14 | ALxFolderNoteSettingTab, 15 | DEFAULT_SETTINGS, 16 | MobileNoClickMark, 17 | noHideNoteMark, 18 | } from "./settings"; 19 | 20 | const foldervNotifiedKey = "foldervNotified"; 21 | 22 | export default class ALxFolderNote extends Plugin { 23 | settings: ALxFolderNoteSettings = DEFAULT_SETTINGS; 24 | 25 | get CoreApi(): FolderNoteAPI { 26 | let message; 27 | const api = getFNCApi(this) || getFNCApi(); 28 | if (api) { 29 | return api; 30 | } else { 31 | message = "Failed to initialize alx-folder-note"; 32 | new ClickNotice(message + ": Click here for more details", () => 33 | this.app.setting.openTabById(this.manifest.id), 34 | ); 35 | throw new Error(message + ": folder-note-core not available"); 36 | } 37 | } 38 | get IconSCAPI() { 39 | if (this.settings.folderIcon) { 40 | return getISCApi(this); 41 | } 42 | return null; 43 | } 44 | 45 | noticeFoldervChange() { 46 | if ( 47 | !this.app.plugins.plugins["alx-folder-note-folderv"] && // not installed 48 | !Number(localStorage.getItem(foldervNotifiedKey)) // not notified 49 | ) { 50 | new ClickNotice( 51 | (frag) => { 52 | frag.appendText( 53 | "Since v0.13.0, folder overview (folderv) has become an optional component " + 54 | "that requires a dedicated plugin, ", 55 | ); 56 | frag 57 | .createEl("button", { 58 | text: "Go to Folder Overview Section of the Setting Tab to Install", 59 | }) 60 | .addEventListener("click", () => 61 | this.app.setting.openTabById(this.manifest.id), 62 | ); 63 | frag.createEl("button", { 64 | text: "Don't show this again", 65 | }); 66 | }, 67 | () => localStorage.setItem(foldervNotifiedKey, "1"), 68 | 5e3, 69 | ); 70 | } 71 | } 72 | 73 | initialized = false; 74 | initialize() { 75 | if (this.initialized) return; 76 | PatchFileExplorer(this); 77 | document.body.toggleClass( 78 | MobileNoClickMark, 79 | !this.settings.mobileClickToOpen, 80 | ); 81 | document.body.toggleClass( 82 | noHideNoteMark, 83 | !this.settings.hideNoteInExplorer, 84 | ); 85 | this.initialized = true; 86 | } 87 | 88 | async onload() { 89 | console.log("loading alx-folder-note"); 90 | 91 | await this.loadSettings(); 92 | 93 | const tab = new ALxFolderNoteSettingTab(this.app, this); 94 | if (!tab.checkMigrated()) 95 | new Notice( 96 | "Old config not yet migrated, \n" + 97 | "Open Settings Tab of ALx Folder Note for details", 98 | ); 99 | this.addSettingTab(tab); 100 | 101 | let initCalled = false; 102 | const init = () => { 103 | initCalled = true; 104 | registerSetFolderIconCmd(this); 105 | this.app.workspace.onLayoutReady(this.initialize.bind(this)); 106 | PatchDragManager(this); 107 | this.noticeFoldervChange(); 108 | }; 109 | 110 | if (getFNCApi(this)) { 111 | init(); 112 | } else { 113 | if (this.app.plugins.enabledPlugins.has("folder-note-core")) { 114 | const timeoutId = window.setTimeout(() => { 115 | if (!initCalled) { 116 | this.app.vault.offref(evtRef); 117 | throw new Error( 118 | "folder-note-core enabled but fail to load within 5s", 119 | ); 120 | } 121 | }, 5e3); 122 | const evtRef = this.app.vault.on("folder-note:api-ready", () => { 123 | init(); 124 | if (timeoutId) window.clearTimeout(timeoutId); 125 | this.app.vault.offref(evtRef); // register event only once 126 | }); 127 | } else { 128 | this.CoreApi; // prompt to enable folder-note-core 129 | } 130 | } 131 | } 132 | 133 | async loadSettings() { 134 | this.settings = { ...this.settings, ...(await this.loadData()) }; 135 | this.setupLongPressDelay(); 136 | } 137 | 138 | async saveSettings() { 139 | await this.saveData(this.settings); 140 | } 141 | 142 | get longPressDelay(): number { 143 | return this.settings.longPressDelay; 144 | } 145 | set longPressDelay(delay: number) { 146 | this.settings.longPressDelay = delay; 147 | document.body.dataset[longPressDelayDataKey] = `${delay}`; 148 | } 149 | setupLongPressDelay() { 150 | // set long press delay to the body 151 | this.longPressDelay = this.longPressDelay; 152 | this.register(() => delete document.body.dataset[longPressDelayDataKey]); 153 | } 154 | } 155 | 156 | const longPressDelayDataKey = "longPressDelay"; 157 | -------------------------------------------------------------------------------- /src/main.less: -------------------------------------------------------------------------------- 1 | .notice button { 2 | margin: 5px 0; 3 | } 4 | -------------------------------------------------------------------------------- /src/misc.ts: -------------------------------------------------------------------------------- 1 | import { dirname, extname, join } from "path"; 2 | import assertNever from "assert-never"; 3 | import type { 4 | AFItem, 5 | App, 6 | FolderItem, 7 | Modifier, 8 | TAbstractFile, 9 | TFile, 10 | View, 11 | FileItem, 12 | } from "obsidian"; 13 | import { WorkspaceLeaf, Notice, Platform, TFolder } from "obsidian"; 14 | 15 | export type afItemMark = AFItem & { 16 | evtDone?: true; 17 | isFolderNote?: true; 18 | isFolderWithNote?: true; 19 | }; 20 | 21 | export const getViewOfType = ( 22 | type: string, 23 | app: App, 24 | ): V | null => { 25 | const vc = app.viewRegistry.getViewCreatorByType(type); 26 | return vc ? (vc(new (WorkspaceLeaf as any)(app)) as V) : null; 27 | }; 28 | 29 | export const isFolder = (item: AFItem): item is FolderItem => 30 | (item as FolderItem).file instanceof TFolder; 31 | 32 | export const isMd = (file: TFile | string) => 33 | typeof file === "string" ? extname(file) === ".md" : file.extension === "md"; 34 | 35 | export enum NoteLoc { 36 | Index, 37 | Inside, 38 | Outside, 39 | } 40 | 41 | export const isModifier = (evt: MouseEvent, pref: Modifier): boolean => { 42 | const { altKey, metaKey, ctrlKey, shiftKey } = evt; 43 | switch (pref) { 44 | case "Mod": 45 | return Platform.isMacOS ? metaKey : ctrlKey; 46 | case "Ctrl": 47 | return ctrlKey; 48 | case "Meta": 49 | return metaKey; 50 | case "Shift": 51 | return shiftKey; 52 | case "Alt": 53 | return altKey; 54 | default: 55 | assertNever(pref); 56 | } 57 | }; 58 | 59 | /** 60 | * @param newName include extension 61 | */ 62 | export const getRenamedPath = (af: TAbstractFile, newName: string) => 63 | join(getParentPath(af.path), newName); 64 | 65 | export const getParentPath = (src: string) => { 66 | const path = dirname(src); 67 | if (path === ".") return "/"; 68 | else return path; 69 | }; 70 | 71 | export class ClickNotice extends Notice { 72 | constructor( 73 | message: string | ((desc: DocumentFragment) => void), 74 | action: (evt: MouseEvent) => any, 75 | timeout?: number, 76 | ) { 77 | super(typeof message === "string" ? message : "", timeout); 78 | this.noticeEl.addEventListener("click", action); 79 | if (typeof message === "function") { 80 | this.noticeEl.empty(); 81 | const frag = new DocumentFragment(); 82 | message(frag); 83 | this.noticeEl.append(frag); 84 | } 85 | } 86 | } 87 | 88 | export function getFileItemTitleEl(fileItem: FileItem): HTMLElement { 89 | return fileItem.titleEl ?? fileItem.selfEl; 90 | } 91 | 92 | export function getFileItemInnerTitleEl(fileItem: FileItem): HTMLElement { 93 | return fileItem.titleInnerEl ?? fileItem.innerEl; 94 | } 95 | -------------------------------------------------------------------------------- /src/modules/long-press.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * long-press-event 3 | * Pure JavaScript long-press-event 4 | * https://github.com/john-doherty/long-press-event 5 | * @author John Doherty 6 | * @license MIT 7 | */ 8 | 9 | import { Platform } from "obsidian"; 10 | 11 | import type ALxFolderNote from "../fn-main"; 12 | 13 | // local timer object based on rAF 14 | let timer: { 15 | value: number; 16 | } | null = null; 17 | 18 | // track number of pixels the mouse moves during long press 19 | let startX = 0; // mouse x position when timer started 20 | let startY = 0; // mouse y position when timer started 21 | const maxDiffX = 10; // max number of X pixels the mouse can move during long press before it is canceled 22 | const maxDiffY = 10; // max number of Y pixels the mouse can move during long press before it is canceled 23 | 24 | /** 25 | * Behaves the same as setTimeout except uses requestAnimationFrame() where possible for better performance 26 | * @param {function} fn The callback function 27 | * @param {int} delay The delay in milliseconds 28 | * @returns {object} handle to the timeout object 29 | */ 30 | const requestTimeout = (fn: Function, delay: number): { value: number } => { 31 | const start = new Date().getTime(); 32 | const handle: { value?: number } = {}; 33 | 34 | const loop = () => { 35 | const current = new Date().getTime(); 36 | const delta = current - start; 37 | 38 | if (delta >= delay) { 39 | fn(); 40 | } else { 41 | handle.value = requestAnimationFrame(loop); 42 | } 43 | }; 44 | 45 | handle.value = requestAnimationFrame(loop); 46 | 47 | return handle as Required; 48 | }; 49 | 50 | /** 51 | * Behaves the same as clearTimeout except uses cancelRequestAnimationFrame() where possible for better performance 52 | * @param {object} handle The callback function 53 | * @returns {void} 54 | */ 55 | const clearRequestTimeout = (handle: typeof timer) => { 56 | handle && cancelAnimationFrame(handle.value); 57 | }; 58 | 59 | /** 60 | * method responsible for clearing a pending long press timer 61 | * @returns {void} 62 | */ 63 | const clearLongPressTimer = () => { 64 | clearRequestTimeout(timer); 65 | timer = null; 66 | }; 67 | 68 | export type LongPressEvent = CustomEvent<{ clientX: number; clientY: number }>; 69 | 70 | /** 71 | * Fires the 'long-press' event on element 72 | * @param originalEvent The original event being fired 73 | * @returns {void} 74 | */ 75 | const fireLongPressEvent = (originalEvent: PointerEvent): void => { 76 | clearLongPressTimer(); 77 | 78 | // fire the long-press event 79 | const allowClickEvent = originalEvent.target?.dispatchEvent( 80 | new CustomEvent("long-press", { 81 | bubbles: true, 82 | cancelable: true, 83 | 84 | // custom event data (legacy) 85 | detail: { 86 | clientX: originalEvent.clientX, 87 | clientY: originalEvent.clientY, 88 | }, 89 | 90 | // add coordinate data that would typically acompany a touch/click event 91 | // @ts-ignore 92 | clientX: originalEvent.clientX, 93 | clientY: originalEvent.clientY, 94 | offsetX: originalEvent.offsetX, 95 | offsetY: originalEvent.offsetY, 96 | pageX: originalEvent.pageX, 97 | pageY: originalEvent.pageY, 98 | screenX: originalEvent.screenX, 99 | screenY: originalEvent.screenY, 100 | }) as LongPressEvent, 101 | ); 102 | 103 | if (!allowClickEvent) { 104 | const suppressEvent = (e: Event) => { 105 | moniterIn?.removeEventListener("click", suppressEvent, true); 106 | cancelEvent(e); 107 | }; 108 | 109 | // suppress the next click event if e.preventDefault() was called in long-press handler 110 | moniterIn?.addEventListener("click", suppressEvent, true); 111 | } 112 | }; 113 | 114 | /** 115 | * method responsible for starting the long press timer 116 | * @param {event} e - event object 117 | * @returns {void} 118 | */ 119 | const startLongPressTimer = (e: PointerEvent) => { 120 | clearLongPressTimer(); 121 | 122 | const el = e.target as EventTarget; 123 | 124 | // get delay from html attribute if it exists, otherwise default to 800 125 | const longPressDelayInMs = parseInt( 126 | getNearestAttribute(el, "data-long-press-delay", "800"), 127 | 10, 128 | ); // default 800 129 | 130 | // start the timer 131 | timer = requestTimeout(fireLongPressEvent.bind(el, e), longPressDelayInMs); 132 | }; 133 | 134 | /** 135 | * Cancels the current event 136 | * @param {object} e - browser event object 137 | * @returns {void} 138 | */ 139 | const cancelEvent = (e: Event) => { 140 | e.stopImmediatePropagation(); 141 | e.preventDefault(); 142 | e.stopPropagation(); 143 | }; 144 | 145 | /** 146 | * Starts the timer on mouse down and logs current position 147 | * @param {object} e - browser event object 148 | * @returns {void} 149 | */ 150 | const mouseDownHandler = (e: PointerEvent) => { 151 | startX = e.clientX; 152 | startY = e.clientY; 153 | startLongPressTimer(e); 154 | }; 155 | 156 | /** 157 | * If the mouse moves n pixels during long-press, cancel the timer 158 | * @param {object} e - browser event object 159 | * @returns {void} 160 | */ 161 | const mouseMoveHandler = (e: DragEvent) => { 162 | clearLongPressTimer(); 163 | // // calculate total number of pixels the pointer has moved 164 | // let diffX = Math.abs(startX - e.clientX); 165 | // let diffY = Math.abs(startY - e.clientY); 166 | 167 | // console.log(diffX >= maxDiffX || diffY >= maxDiffY, diffX, diffY); 168 | // // if pointer has moved more than allowed, cancel the long-press timer and therefore the event 169 | // if (diffX >= maxDiffX || diffY >= maxDiffY) { 170 | // clearLongPressTimer(); 171 | // } 172 | }; 173 | 174 | /** 175 | * Gets attribute off HTML element or nearest parent 176 | * @param {object} el - HTML element to retrieve attribute from 177 | * @param {string} attributeName - name of the attribute 178 | * @param {any} defaultValue - default value to return if no match found 179 | * @returns {any} attribute value or defaultValue 180 | */ 181 | const getNearestAttribute = ( 182 | el: EventTarget | null, 183 | attributeName: string, 184 | defaultValue: string, 185 | ) => { 186 | // walk up the dom tree looking for data-action and data-trigger 187 | while (el instanceof Element && el !== document.documentElement) { 188 | const attributeValue = el.getAttribute(attributeName); 189 | 190 | if (attributeValue) { 191 | return attributeValue; 192 | } 193 | 194 | el = el.parentNode; 195 | } 196 | 197 | return defaultValue; 198 | }; 199 | 200 | let moniterIn: HTMLElement | null; 201 | 202 | const removeEvtListener = (el: HTMLElement) => { 203 | moniterIn = null; 204 | el.removeEventListener("pointerup", clearLongPressTimer, true); 205 | el.removeEventListener("drag", mouseMoveHandler, true); 206 | el.removeEventListener("wheel", clearLongPressTimer, true); 207 | el.removeEventListener("scroll", clearLongPressTimer, true); 208 | 209 | el.removeEventListener("pointerdown", mouseDownHandler, true); // <- start 210 | }; 211 | 212 | const AddLongPressEvt = (plugin: ALxFolderNote, el: HTMLElement) => { 213 | // disable on mobile (conflict with file-menu) 214 | if (!plugin.settings.longPressFocus || Platform.isMobile) return; 215 | if (moniterIn) { 216 | removeEvtListener(moniterIn); 217 | } 218 | moniterIn = el; 219 | // hook events that clear a pending long press event 220 | el.addEventListener("pointerup", clearLongPressTimer, true); 221 | el.addEventListener("drag", mouseMoveHandler, true); 222 | el.addEventListener("wheel", clearLongPressTimer, true); 223 | el.addEventListener("scroll", clearLongPressTimer, true); 224 | 225 | // hook events that can trigger a long press event 226 | el.addEventListener("pointerdown", mouseDownHandler, true); // <- start 227 | 228 | plugin.register(() => removeEvtListener(el)); 229 | }; 230 | 231 | export default AddLongPressEvt; 232 | -------------------------------------------------------------------------------- /src/modules/set-folder-icon.ts: -------------------------------------------------------------------------------- 1 | import type { IconInfo } from "@aidenlx/obsidian-icon-shortcodes/lib/icon-packs/types"; 2 | import { MarkdownView, TFile, TFolder } from "obsidian"; 3 | 4 | import type ALxFolderNote from "../fn-main"; 5 | 6 | const registerSetFolderIconCmd = (plugin: ALxFolderNote) => { 7 | const { workspace, vault, fileManager } = plugin.app; 8 | const setIconField = async (icon: IconInfo | null, file: TFile) => { 9 | if (!icon) return; 10 | await fileManager.processFrontMatter(file, (fm) => { 11 | fm.icon = icon.id; 12 | }); 13 | }; 14 | plugin.addCommand({ 15 | id: "set-folder-icon", 16 | name: "Set Folder Icon", 17 | checkCallback: (checking) => { 18 | const iscAPI = plugin.IconSCAPI; 19 | if (!iscAPI) return false; 20 | const mdView = workspace.getActiveViewOfType(MarkdownView); 21 | if (!mdView) return false; 22 | const folder = plugin.CoreApi.getFolderFromNote(mdView.file); 23 | if (!folder) return false; 24 | if (checking) return true; 25 | iscAPI.getIconFromUser().then((icon) => setIconField(icon, mdView.file)); 26 | }, 27 | }); 28 | plugin.registerEvent( 29 | workspace.on("file-menu", (menu, af, src) => { 30 | const iscAPI = plugin.IconSCAPI; 31 | if (!iscAPI) return; 32 | let note; 33 | if ( 34 | (af instanceof TFolder && (note = plugin.CoreApi.getFolderNote(af))) || 35 | (af instanceof TFile && 36 | ((note = af), plugin.CoreApi.getFolderFromNote(af))) 37 | ) { 38 | const folderNote = note; 39 | menu.addItem((item) => 40 | item 41 | .setIcon("image-glyph") 42 | .setTitle("Set Folder Icon") 43 | .onClick(async () => 44 | setIconField(await iscAPI.getIconFromUser(), folderNote), 45 | ), 46 | ); 47 | } 48 | }), 49 | ); 50 | }; 51 | export default registerSetFolderIconCmd; 52 | -------------------------------------------------------------------------------- /src/settings.less: -------------------------------------------------------------------------------- 1 | .setting-item-control > input[type="number"] { 2 | &:invalid { 3 | color: var(--text-error); 4 | background-color: var(--background-modifier-error); 5 | & + span.validity::after { 6 | content: "✖"; 7 | padding-left: 5px; 8 | } 9 | } 10 | &.input-short { 11 | width: 5em; 12 | } 13 | & + span.unit { 14 | margin-left: 5px; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | import "./settings.less"; 2 | 3 | import { getApi } from "@aidenlx/obsidian-icon-shortcodes"; 4 | import type { App, Modifier } from "obsidian"; 5 | import { Platform, PluginSettingTab, Setting } from "obsidian"; 6 | 7 | import { folderIconMark } from "./fe-handler/folder-mark"; 8 | import type ALxFolderNote from "./fn-main"; 9 | import type { NoteLoc } from "./misc"; 10 | 11 | export const noHideNoteMark = "alx-no-hide-note"; 12 | export const MobileNoClickMark = "alx-no-click-on-mobile"; 13 | 14 | export interface ALxFolderNoteSettings { 15 | modifierForNewNote: Modifier; 16 | hideNoteInExplorer: boolean; 17 | hideCollapseIndicator: boolean; 18 | longPressFocus: boolean; 19 | folderIcon: boolean; 20 | folderNotePref: NoteLoc | null; 21 | deleteOutsideNoteWithFolder: boolean | null; 22 | indexName: string | null; 23 | autoRename: boolean | null; 24 | folderNoteTemplate: string | null; 25 | mobileClickToOpen: boolean; 26 | longPressDelay: number; 27 | expandFolderOnClick: boolean; 28 | } 29 | 30 | export const DEFAULT_SETTINGS: ALxFolderNoteSettings = { 31 | modifierForNewNote: "Mod", 32 | hideNoteInExplorer: true, 33 | hideCollapseIndicator: false, 34 | longPressFocus: false, 35 | folderIcon: true, 36 | folderNotePref: null, 37 | deleteOutsideNoteWithFolder: null, 38 | indexName: null, 39 | autoRename: null, 40 | folderNoteTemplate: null, 41 | mobileClickToOpen: true, 42 | longPressDelay: 800, 43 | expandFolderOnClick: false, 44 | }; 45 | 46 | type SettingKeyWithType = { 47 | [K in keyof ALxFolderNoteSettings]: ALxFolderNoteSettings[K] extends T 48 | ? K 49 | : never; 50 | }[keyof ALxFolderNoteSettings]; 51 | 52 | const old = [ 53 | "folderNotePref", 54 | "deleteOutsideNoteWithFolder", 55 | "indexName", 56 | "autoRename", 57 | "folderNoteTemplate", 58 | ] as const; 59 | 60 | export class ALxFolderNoteSettingTab extends PluginSettingTab { 61 | plugin: ALxFolderNote; 62 | 63 | constructor(app: App, plugin: ALxFolderNote) { 64 | super(app, plugin); 65 | this.plugin = plugin; 66 | } 67 | 68 | checkMigrated(): boolean { 69 | return old.every((key) => this.plugin.settings[key] === null); 70 | } 71 | 72 | getInitGuide( 73 | desc: string, 74 | targetPluginID: string, 75 | container: HTMLElement, 76 | ): Setting { 77 | return new Setting(container) 78 | .setDesc( 79 | desc + 80 | "use the buttons to install & enable it then reload alx-folder-note to take effects", 81 | ) 82 | .addButton((btn) => 83 | btn 84 | .setIcon("down-arrow-with-tail") 85 | .setTooltip("Go to Plugin Page") 86 | .onClick(() => 87 | window.open(`obsidian://show-plugin?id=${targetPluginID}`), 88 | ), 89 | ) 90 | .addButton((btn) => 91 | btn 92 | .setIcon("reset") 93 | .setTooltip("Reload alx-folder-note") 94 | .onClick(async () => { 95 | await this.app.plugins.disablePlugin(this.plugin.manifest.id); 96 | await this.app.plugins.enablePlugin(this.plugin.manifest.id); 97 | this.app.setting.openTabById(this.plugin.manifest.id); 98 | }), 99 | ); 100 | } 101 | 102 | display(): void { 103 | const { containerEl } = this; 104 | containerEl.empty(); 105 | 106 | new Setting(containerEl).setHeading().setName("Core"); 107 | try { 108 | this.plugin.CoreApi; // throw error when not available 109 | if (this.checkMigrated()) { 110 | this.plugin.CoreApi.renderCoreSettings(containerEl); 111 | } else this.setMigrate(); 112 | } catch (error) { 113 | this.getInitGuide( 114 | "Seems like Folder Note Core is not enabled, ", 115 | "folder-note-core", 116 | containerEl, 117 | ); 118 | return; 119 | } 120 | 121 | this.setFolderIcon(); 122 | this.setModifier(); 123 | this.setHide(); 124 | this.addToggle(this.containerEl, "expandFolderOnClick") 125 | .setName("Expand Folder on Click") 126 | .setDesc( 127 | "Expand collapsed folders with note while opening them by clicking on folder title", 128 | ); 129 | this.setMobile(); 130 | this.setFocus(); 131 | 132 | new Setting(containerEl).setHeading().setName("Folder Overview"); 133 | const folderv = this.app.plugins.plugins["alx-folder-note-folderv"]; 134 | if (folderv?.renderFoldervSettings) { 135 | folderv.renderFoldervSettings(containerEl); 136 | } else { 137 | this.getInitGuide( 138 | "Folder Overview (folderv) is now an optional component, ", 139 | "alx-folder-note-folderv", 140 | containerEl, 141 | ); 142 | } 143 | 144 | new Setting(containerEl).setHeading().setName("Debug"); 145 | this.plugin.CoreApi.renderLogLevel(containerEl); 146 | } 147 | 148 | setMigrate() { 149 | new Setting(this.containerEl) 150 | .setName("Migrate settings to Folder Note Core") 151 | .setDesc( 152 | "Some settings has not been migrated to Folder Note Core, " + 153 | "click Migrate to migrate old config " + 154 | "or Cancel to use config in Folder Note Core in favor of old config", 155 | ) 156 | .addButton((cb) => 157 | cb.setButtonText("Migrate").onClick(async () => { 158 | const toImport = old.reduce( 159 | (obj, k) => ((obj[k] = this.plugin.settings[k] ?? undefined), obj), 160 | {} as any, 161 | ); 162 | this.plugin.CoreApi.importSettings(toImport); 163 | old.forEach((k) => ((this.plugin.settings as any)[k] = null)); 164 | await this.plugin.saveSettings(); 165 | this.display(); 166 | }), 167 | ) 168 | .addButton((cb) => 169 | cb.setButtonText("Cancel").onClick(async () => { 170 | old.forEach((k) => ((this.plugin.settings as any)[k] = null)); 171 | await this.plugin.saveSettings(); 172 | this.display(); 173 | }), 174 | ); 175 | } 176 | 177 | setMobile() { 178 | if (!Platform.isMobile) return; 179 | this.addToggle(this.containerEl, "mobileClickToOpen", (value) => 180 | document.body.toggleClass(MobileNoClickMark, !value), 181 | ) 182 | .setName("Click folder title to open folder note on mobile") 183 | .setDesc( 184 | "Disable this if you want to the default action. You can still use context menu to open folder note", 185 | ); 186 | } 187 | 188 | setModifier = () => { 189 | new Setting(this.containerEl) 190 | .setName("Modifier for New Note") 191 | .setDesc("Choose a modifier to click folders with to create folder notes") 192 | .addDropdown((dropDown) => { 193 | type NoShift = Exclude; 194 | const windowsOpts: Record = { 195 | Mod: "Ctrl (Cmd in macOS)", 196 | Ctrl: "Ctrl (Ctrl in macOS)", 197 | Meta: "⊞ Win", 198 | // Shift: "Shift", 199 | Alt: "Alt", 200 | }; 201 | const macOSOpts: Record = { 202 | Mod: "⌘ Cmd (Ctrl in Windows)", 203 | Ctrl: "⌃ Control", 204 | Meta: "⌘ Cmd (Win in Windows)", 205 | // Shift: "⇧ Shift", 206 | Alt: "⌥ Option", 207 | }; 208 | 209 | const options = Platform.isMacOS ? macOSOpts : windowsOpts; 210 | 211 | dropDown 212 | .addOptions(options) 213 | .setValue(this.plugin.settings.modifierForNewNote) 214 | .onChange(async (value: string) => { 215 | this.plugin.settings.modifierForNewNote = value as NoShift; 216 | await this.plugin.saveSettings(); 217 | }); 218 | }); 219 | }; 220 | 221 | setHide() { 222 | this.addToggle(this.containerEl, "hideNoteInExplorer", (value) => 223 | document.body.toggleClass(noHideNoteMark, !value), 224 | ) 225 | .setName("Hide Folder Note") 226 | .setDesc("Hide folder note files from file explorer"); 227 | this.addToggle(this.containerEl, "hideCollapseIndicator") 228 | .setName("Hide Collapse Indicator") 229 | .setDesc( 230 | "Hide collapse indicator when folder contains only folder note, reload obsidian to take effects", 231 | ); 232 | } 233 | setFolderIcon() { 234 | this.addToggle(this.containerEl, "folderIcon", (value) => 235 | document.body.toggleClass(folderIconMark, value), 236 | ) 237 | .setName("Set Folder Icon in Folder Notes") 238 | .setDesc( 239 | createFragment((el) => { 240 | el.appendText( 241 | "Set `icon` field with icon shortcode in frontmatter of foler note to specify linked folder's icon", 242 | ); 243 | el.createEl("br"); 244 | 245 | el.createEl("a", { 246 | href: "https://github.com/aidenlx/obsidian-icon-shortcodes", 247 | text: "Icon Shortcodes v0.5.1+", 248 | }); 249 | el.appendText(" Required. "); 250 | if (!getApi(this.plugin)) el.appendText("(Currently not enabled)"); 251 | el.createEl("br"); 252 | 253 | el.appendText("Restart obsidian to take effects"); 254 | }), 255 | ); 256 | } 257 | setFocus() { 258 | new Setting(this.containerEl) 259 | .setHeading() 260 | .setName("Focus") 261 | .setDesc( 262 | `You can use "Toggle Focus" option in folder context menu${ 263 | Platform.isMobile ? "" : " or long press on folder title" 264 | } to focus on a specific folder`, 265 | ); 266 | if (!Platform.isMobile) 267 | this.addToggle(this.containerEl, "longPressFocus") 268 | .setName("Long Press on Folder to Focus") 269 | .setDesc( 270 | "Long press with mouse on folder name inside file explorer to focus the folder. " + 271 | "Only work on Desktop, reload obsidian to take effects", 272 | ); 273 | new Setting(this.containerEl) 274 | .addText((text) => { 275 | Object.assign(text.inputEl, { 276 | type: "number", 277 | min: "0.2", 278 | step: "0.1", 279 | required: true, 280 | }); 281 | text.inputEl.addClass("input-short"); 282 | text.inputEl.insertAdjacentElement( 283 | "afterend", 284 | createSpan({ cls: ["validity", "unit"], text: "second(s)" }), 285 | ); 286 | text 287 | .setValue(`${this.plugin.longPressDelay / 1e3}`) 288 | .onChange(async (val) => { 289 | const delay = +val * 1e3; 290 | this.plugin.longPressDelay = delay; 291 | await this.plugin.saveSettings(); 292 | }); 293 | }) 294 | .setName("Long Press Delay"); 295 | } 296 | 297 | addToggle( 298 | addTo: HTMLElement, 299 | key: SettingKeyWithType, 300 | onSet?: (value: boolean) => any, 301 | ): Setting { 302 | return new Setting(addTo).addToggle((toggle) => { 303 | toggle 304 | .setValue(this.plugin.settings[key]) 305 | .onChange( 306 | (value) => ( 307 | (this.plugin.settings[key] = value), 308 | onSet && onSet(value), 309 | this.plugin.saveSettings() 310 | ), 311 | ); 312 | }); 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/typings/obsidian-ex.d.ts: -------------------------------------------------------------------------------- 1 | import "obsidian"; 2 | 3 | import type getFileExplorerHandlers from "../fe-handler"; 4 | 5 | declare module "obsidian" { 6 | class FileExplorerView extends ItemView { 7 | prototype: FileExplorerView; 8 | fileBeingRenamed: TAbstractFile | null; 9 | fileItems: { 10 | [key: string]: AFItem; 11 | "/": FolderItem; 12 | }; 13 | files: WeakMap; 14 | getViewType(): string; 15 | getDisplayText(): string; 16 | onClose(): Promise; 17 | dom: { 18 | infinityScroll: HTMLDivElement; 19 | navFileContainerEl: HTMLDivElement; 20 | }; 21 | onOpen(): Promise; 22 | onFileClick(evt: MouseEvent, navEl: HTMLDivElement): void; 23 | handleFileClick(evt: MouseEvent, item: AFItem): boolean; 24 | createFolderDom(folder: TFolder): FolderItem; 25 | folderNoteUtils?: ReturnType; 26 | currentHoverFile: TAbstractFile | null; 27 | onFileMouseover(evt: MouseEvent, navTitleEl: HTMLElement): void; 28 | onFileMouseout(evt: MouseEvent, navTitleEl: HTMLElement): void; 29 | _AFN_HOVER?: TFolder; 30 | } 31 | 32 | interface ViewRegistry { 33 | typeByExtension: Record; 34 | viewByType: Record; 35 | getViewCreatorByType(type: string): ViewCreator | undefined; 36 | isExtensionRegistered(ext: string): boolean; 37 | registerExtensions(exts: string[], type: string): void; 38 | registerViewWithExtensions( 39 | exts: string[], 40 | type: string, 41 | viewCreator: ViewCreator, 42 | ): void; 43 | unregisterExtensions(exts: string[]): void; 44 | } 45 | 46 | type AFItem = FileItem | FolderItem; 47 | 48 | class FileItem { 49 | el: HTMLDivElement; 50 | file: TFile; 51 | fileExplorer: FileExplorerView; 52 | info: any; 53 | /** 54 | * @deprecated After Obsidian 1.2.0, use `selfEl` instead. 55 | */ 56 | titleEl?: HTMLDivElement; 57 | /** 58 | * @deprecated After Obsidian 1.2.0, use `innerEl` instead. 59 | */ 60 | titleInnerEl?: HTMLDivElement; 61 | selfEl: HTMLDivElement; 62 | innerEl: HTMLDivElement; 63 | } 64 | 65 | class FolderItem { 66 | el: HTMLDivElement; 67 | fileExplorer: FileExplorerView; 68 | info: any; 69 | /** 70 | * @deprecated After Obsidian 1.2.0, use `selfEl` instead. 71 | */ 72 | titleEl?: HTMLDivElement; 73 | /** 74 | * @deprecated After Obsidian 1.2.0, use `innerEl` instead. 75 | */ 76 | titleInnerEl?: HTMLDivElement; 77 | selfEl: HTMLDivElement; 78 | innerEl: HTMLDivElement; 79 | file: TFolder; 80 | children: AFItem[]; 81 | childrenEl: HTMLDivElement; 82 | collapseIndicatorEl: HTMLDivElement; 83 | collapsed: boolean; 84 | pusherEl: HTMLDivElement; 85 | setCollapsed(collapsed: boolean): Promise; 86 | /** 87 | * @deprecated After Obsidian 1.2.0, use `onSelfClick` instead. 88 | */ 89 | onTitleElClick?(evt: MouseEvent): any; 90 | onSelfClick(evt: MouseEvent): any; 91 | } 92 | 93 | interface Vault { 94 | exists(normalizedPath: string, sensitive?: boolean): Promise; 95 | } 96 | 97 | class FileExplorerPlugin extends Plugin_2 { 98 | revealInFolder(this: any, ...args: any[]): any; 99 | } 100 | 101 | interface App { 102 | viewRegistry: ViewRegistry; 103 | plugins: { 104 | enabledPlugins: Set; 105 | plugins: { 106 | [id: string]: any; 107 | "alx-folder-note-folderv"?: { 108 | renderFoldervSettings(containerEl: HTMLElement): void; 109 | }; 110 | }; 111 | enablePlugin(id: string): Promise; 112 | disablePlugin(id: string): Promise; 113 | }; 114 | internalPlugins: { 115 | plugins: { 116 | [id: string]: any; 117 | ["file-explorer"]?: { 118 | instance: FileExplorerPlugin; 119 | }; 120 | }; 121 | }; 122 | setting: { 123 | openTabById(id: string): any; 124 | }; 125 | } 126 | interface Notice { 127 | noticeEl: HTMLElement; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/typings/svg.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const src: string; 3 | export default src; 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "outDir": "dist", 6 | "target": "ES2022", 7 | "lib": ["dom", "ES2018"], 8 | "moduleResolution": "bundler", 9 | "module": "ESNext", 10 | "strict": true, 11 | "useUnknownInCatchVariables": true, 12 | "allowJs": true, 13 | "skipLibCheck": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noEmit": true, 16 | "esModuleInterop": false, 17 | "allowSyntheticDefaultImports": true, 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "incremental": true, 21 | "newLine": "lf" 22 | }, 23 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/worker/*/api.ts"], 24 | "exclude": ["**/node_modules", "**/.*/", "dist", "build"] 25 | } 26 | 27 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "0.1.0": "0.9.12", 3 | "0.1.1": "0.9.12", 4 | "0.2.0": "0.9.12", 5 | "0.3.0": "0.9.12", 6 | "0.4.0": "0.9.12", 7 | "0.5.0": "0.12.5", 8 | "0.6.0": "0.12.5", 9 | "0.7.0": "0.12.5", 10 | "0.8.0": "0.12.5", 11 | "0.9.0": "0.12.5", 12 | "0.9.1": "0.12.5", 13 | "0.9.2": "0.12.5", 14 | "0.10.0": "0.12.5", 15 | "0.11.0": "0.12.5", 16 | "0.11.1": "0.12.5", 17 | "0.11.2": "0.12.5", 18 | "0.12.0": "0.12.5", 19 | "0.12.1": "0.12.5", 20 | "0.12.2": "0.12.5", 21 | "0.12.3": "0.12.5", 22 | "0.13.0": "0.12.5", 23 | "0.13.1": "0.12.5", 24 | "0.14.0": "0.12.5", 25 | "0.15.0": "0.12.5", 26 | "0.16.0": "0.13.24", 27 | "0.16.1": "0.13.24", 28 | "0.16.2": "0.13.24", 29 | "0.16.3": "0.13.24", 30 | "0.16.4": "0.14.8", 31 | "0.16.5": "1.1.0" 32 | } --------------------------------------------------------------------------------