├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── FUNDING.yml └── workflows │ └── release.yml ├── .gitignore ├── .husky └── pre-push ├── .npmrc ├── LICENSE ├── README.md ├── bun-fix.d.ts ├── bun.lockb ├── esbuild.config.mjs ├── manifest.json ├── package.json ├── release.sh ├── src ├── BetterPluginsPagePluginSettingTab.ts ├── CommonSetting.ts ├── FilterModal.ts ├── Interfaces.ts ├── NoticeManager.ts ├── SettingManager.ts ├── SettingsSchemas.ts ├── getName.ts ├── getPlugins.ts ├── getUpdatedWithinMilliseconds.ts ├── main.ts ├── observer.ts ├── typings │ ├── NestedKeyof.d.ts │ ├── NestedValue.d.ts │ ├── Prettify.d.ts │ ├── WithPrefix.d.ts │ └── obsidian-ex.d.ts └── util │ ├── AsyncQueue.ts │ └── State.ts ├── styles.css ├── tsconfig.json ├── version-bump.mjs └── versions.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 4 10 | tab_width = 4 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | main.js 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "no-prototype-builtins": "off", 21 | "@typescript-eslint/no-empty-function": "off" 22 | } 23 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [hananoshikayomaru] 2 | custom: https://www.buymeacoffee.com/yomaru 3 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Obsidian plugin 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - uses: oven-sh/setup-bun@v1 16 | with: 17 | bun-version: latest 18 | 19 | - name: Build plugin 20 | run: | 21 | bun install 22 | bun run build 23 | 24 | - name: Create release 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | run: | 28 | tag="${GITHUB_REF#refs/tags/}" 29 | 30 | gh release create "$tag" \ 31 | --title="$tag" \ 32 | --draft \ 33 | main.js manifest.json styles.css 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | main.js 14 | 15 | # Exclude sourcemaps 16 | *.map 17 | 18 | # obsidian 19 | data.json 20 | 21 | # Exclude macOS Finder (System Explorer) View States 22 | .DS_Store 23 | dist -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | bun run typecheck 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Yeung Man Lung Ken 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Better Plugins Page for Obsidian 2 | 3 | ![CleanShot 2023-12-14 at 10 56 59@2x](https://github.com/TopTierTools/obsidian-better-plugin-page/assets/43137033/53fadbfa-440b-4f66-8e4c-c2ba14353d36) 4 | 5 | ![658shots_so](https://github.com/TopTierTools/obsidian-better-plugin-page/assets/43137033/dea8a916-be00-42a2-b96a-8d30791ffee8) 6 | 7 | The "Better Plugins Page" plugin enhances the user experience when managing plugins in Obsidian by providing additional features and options for organizing and filtering community plugins. This plugin is designed to help users easily discover, hide, save, and filter plugins based on their preferences. This plugin also add plugin note feature to encourage people write note about how they use their plugins. 8 | 9 | See the demo: 10 | 11 | Buy Me A Coffee [![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/hananoshikayomaru) 12 | 13 | ## Design Philosophy 14 | 15 | ![Thumbnails 001](https://github.com/TopTierTools/obsidian-better-plugin-page/assets/43137033/725fe1a8-8d15-42ed-995d-86d36ee50a5c) 16 | 17 | ![Simple, Robust, Powerful = Complete](https://github.com/TopTierTools/obsidian-better-plugin-page/assets/43137033/83126663-8932-42a7-bfb4-47b109da1c29) 18 | 19 | We only build good softwares. And we follow the 3 big principles (simple, robust, powerful). To learn more, visit the company manifest written by our founder: 20 | 21 | ## Key Features 22 | 23 | 1. **Filter and Sort:** Seamlessly filter and sort community plugins based on criteria such as download count and last update date, allowing you to find plugins that match your preferences more efficiently. 24 | 2. **Hide/Show Plugins:** Customize your plugins page by hiding or showing specific plugins. This feature allows you to declutter your interface and focus on the plugins that matter most to you. 25 | 3. **Hidden Plugins List:** Maintain a list of hidden plugins, which can be easily managed through the Obsidian settings. This list ensures that you can control which plugins are displayed and which remain hidden. 26 | 4. **Toggle Visibility:** Quickly toggle the visibility of hidden plugins, giving you the flexibility to show or hide them on demand. 27 | 5. **User-Friendly Interface:** The plugin integrates seamlessly with the Obsidian user interface, providing clear buttons and icons for hiding, showing, and filtering plugins. It enhances the default Obsidian plugin management experience. 28 | 6. **Filter by Updated Time**: you can easily filter community plugins based on their last update date. 29 | 7. **Filter by User Download Count**: Gain insights into the popularity of community plugins by filtering them based on their download count. This feature enables you to discover plugins that are widely used and trusted by the Obsidian community. 30 | 8. **Saved plugin**, Save your favorite plugins and access them conveniently within Obsidian. Keep a curated list of plugins that enhance your productivity and creativity. 31 | 9. **Filter by Saved Plugin**, Only see your favourite plugins 32 | 10. **Plugin note**, Add notes and annotations to plugins for easy reference. Keep track of your thoughts, tips, and ideas related to each plugin. If you don't want this feature, you can turn this off in the plugin setting tab. 33 | 34 | ## Installation 35 | 36 | ### Through community plugin store 37 | 38 | Waiting for approval: 39 | 40 | ### Through BRAT 41 | 42 | 1. install the BRAT plugin 43 | 2. go to the plugin option, add beta plugin, copy and paste the link of this repo. 44 | 3. the plugin will automatically appear in the list of installed community plugins, enabled this plugin 45 | 46 | ### Manual installation 47 | 48 | 1. cd to `.obsidian/plugins` 49 | 2. git clone this repo 50 | 3. `cd obsidian-better-plugin-pages && bun install && bun run build` 51 | 4. there you go 🎉 52 | 53 | ## Say thank you 54 | 55 | If you are enjoying this plugin then please support my work and enthusiasm by sponsoring me on Github or buying me a coffee on . 56 | 57 | Buy Me A Coffee [![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/hananoshikayomaru) 58 | -------------------------------------------------------------------------------- /bun-fix.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TopTierTools/obsidian-better-plugin-page/a16644b89fe4886f55e77a158409f6e8ff950663/bun.lockb -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from "builtin-modules"; 4 | 5 | const banner = `/* 6 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 7 | if you want to view the source, please visit the github repository of this plugin 8 | */ 9 | `; 10 | 11 | const prod = process.argv[2] === "production"; 12 | 13 | const context = await esbuild.context({ 14 | banner: { 15 | js: banner, 16 | }, 17 | entryPoints: ["src/main.ts"], 18 | bundle: true, 19 | external: [ 20 | "obsidian", 21 | "electron", 22 | "@codemirror/autocomplete", 23 | "@codemirror/collab", 24 | "@codemirror/commands", 25 | "@codemirror/language", 26 | "@codemirror/lint", 27 | "@codemirror/search", 28 | "@codemirror/state", 29 | "@codemirror/view", 30 | "@lezer/common", 31 | "@lezer/highlight", 32 | "@lezer/lr", 33 | ...builtins, 34 | ], 35 | format: "cjs", 36 | target: "es2018", 37 | logLevel: "info", 38 | sourcemap: prod ? false : "inline", 39 | treeShaking: true, 40 | outfile: "main.js", 41 | }); 42 | 43 | if (prod) { 44 | await context.rebuild(); 45 | process.exit(0); 46 | } else { 47 | await context.watch(); 48 | } 49 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "better-plugins-page", 3 | "name": "Better Plugins Page", 4 | "version": "1.0.13", 5 | "minAppVersion": "0.15.0", 6 | "description": "Better Plugins Page for Obsidian, add filtering and hiding to the page", 7 | "author": "HananoshikaYomaru", 8 | "authorUrl": "https://yomaru.dev", 9 | "fundingUrl": { 10 | "buymeacoffee": "https://www.buymeacoffee.com/yomaru", 11 | "Github Sponsor": "https://github.com/sponsors/HananoshikaYomaru" 12 | }, 13 | "isDesktopOnly": false 14 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-better-plugins-page", 3 | "version": "1.0.13", 4 | "description": "Better Plugins Page for Obsidian, add filtering and hiding to the page", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "bun esbuild.config.mjs", 8 | "build": "bun esbuild.config.mjs production", 9 | "version": "bun version-bump.mjs && git add manifest.json versions.json", 10 | "typecheck": "tsc -noEmit -skipLibCheck", 11 | "prepare": "husky install", 12 | "release": "bash ./release.sh" 13 | }, 14 | "keywords": [ 15 | "obsidian", 16 | "plugins" 17 | ], 18 | "author": "", 19 | "license": "MIT", 20 | "devDependencies": { 21 | "@total-typescript/ts-reset": "^0.5.1", 22 | "@types/jquery": "^3.5.29", 23 | "@types/lodash": "^4.14.202", 24 | "@types/node": "^16.11.6", 25 | "@typescript-eslint/eslint-plugin": "5.29.0", 26 | "@typescript-eslint/parser": "5.29.0", 27 | "builtin-modules": "3.3.0", 28 | "bun-types": "^1.0.5-canary.20231009T140142", 29 | "esbuild": "0.17.3", 30 | "husky": "^8.0.3", 31 | "obsidian": "latest", 32 | "tslib": "2.4.0", 33 | "typescript": "4.7.4" 34 | }, 35 | "dependencies": { 36 | "chrono-node": "^2.7.3", 37 | "jquery": "^3.7.1", 38 | "lodash": "^4.17.21", 39 | "observable-slim": "^0.1.6", 40 | "zod": "^3.22.4" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Set the default update type 4 | UPDATE_TYPE="patch" 5 | 6 | # Parse command-line arguments 7 | while [[ $# -gt 0 ]]; do 8 | key="$1" 9 | case $key in 10 | -m | --minor) 11 | UPDATE_TYPE="minor" 12 | shift 13 | ;; 14 | -M | --major) 15 | UPDATE_TYPE="major" 16 | shift 17 | ;; 18 | *) 19 | echo "Unknown option: $key" 20 | exit 1 21 | ;; 22 | esac 23 | done 24 | 25 | # Get the version number from manifest.json 26 | MANIFEST_VERSION=$(jq -r '.version' manifest.json) 27 | 28 | # Get the version number from package.json 29 | PACKAGE_VERSION=$(node -p -e "require('./package.json').version") 30 | 31 | # Ensure the version from package.json matches the version in manifest.json 32 | if [ "$PACKAGE_VERSION" != "$MANIFEST_VERSION" ]; then 33 | echo "Version mismatch between package.json and manifest.json" 34 | exit 1 35 | fi 36 | 37 | # Increment the version based on the specified update type 38 | if [ "$UPDATE_TYPE" = "minor" ]; then 39 | NEW_VERSION=$(semver $PACKAGE_VERSION -i minor) 40 | elif [ "$UPDATE_TYPE" = "major" ]; then 41 | NEW_VERSION=$(semver $PACKAGE_VERSION -i major) 42 | else 43 | NEW_VERSION=$(semver $PACKAGE_VERSION -i patch) 44 | fi 45 | 46 | echo "Current version: $PACKAGE_VERSION" 47 | echo "New version: $NEW_VERSION" 48 | 49 | # Update the version in package.json 50 | jq --arg version "$NEW_VERSION" '.version = $version' package.json >tmp.json && mv tmp.json package.json 51 | echo "Changed package.json version to $NEW_VERSION" 52 | 53 | # Print the updated version of manifest.json using 'bun' 54 | bun run version 55 | echo "Updated version of manifest using bun. The current version of manifest.json is $(jq -r '.version' manifest.json)" 56 | 57 | # Create a git commit and tag 58 | git add . && git commit -m "release: $NEW_VERSION" 59 | git tag -a "$NEW_VERSION" -m "release: $NEW_VERSION" 60 | echo "Created tag $NEW_VERSION" 61 | 62 | # Push the commit and tag to the remote repository 63 | git push origin "$NEW_VERSION" 64 | echo "Pushed tag $NEW_VERSION to the origin branch $NEW_VERSION" 65 | git push 66 | echo "Pushed to the origin master branch" 67 | -------------------------------------------------------------------------------- /src/BetterPluginsPagePluginSettingTab.ts: -------------------------------------------------------------------------------- 1 | import { App, PluginSettingTab, Setting } from "obsidian"; 2 | import BetterPluginsPagePlugin from "./main"; 3 | import { CommonSetting } from "./CommonSetting"; 4 | 5 | export class BetterPluginsPagePluginSettingTab extends PluginSettingTab { 6 | plugin: BetterPluginsPagePlugin; 7 | 8 | constructor(app: App, plugin: BetterPluginsPagePlugin) { 9 | super(app, plugin); 10 | this.plugin = plugin; 11 | } 12 | 13 | display(): void { 14 | const { containerEl } = this; 15 | 16 | containerEl.empty(); 17 | containerEl.addClasses(["better-plugins-page-plugin-setting-tab"]); 18 | 19 | const commonSetting = new CommonSetting(this.plugin); 20 | commonSetting.createHiddenPluginsSetting(containerEl); 21 | commonSetting.createSavedPluginsSetting(containerEl); 22 | 23 | // add a toggle for enabling the plugin note feature 24 | new Setting(containerEl) 25 | .setName("Enable plugin note feature") 26 | .addToggle((toggle) => { 27 | toggle 28 | .setValue( 29 | this.plugin.settingManager.getSettings() 30 | .pluginNoteFeatureEnabled 31 | ) 32 | .onChange(async (value) => { 33 | this.plugin.settingManager.updateSettings((setting) => { 34 | setting.value.pluginNoteFeatureEnabled = value; 35 | }); 36 | this.plugin.modalContentObserver?.disconnect(); 37 | }); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/CommonSetting.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "obsidian"; 2 | import BetterPluginsPagePlugin from "./main"; 3 | import { getPlugins } from "@/getPlugins"; 4 | 5 | export class CommonSetting { 6 | plugin: BetterPluginsPagePlugin; 7 | 8 | constructor(plugin: BetterPluginsPagePlugin) { 9 | this.plugin = plugin; 10 | } 11 | 12 | createHiddenPluginsSetting(container: HTMLElement) { 13 | const hiddenPluginsSetting = new Setting(container) 14 | .setName("Hidden plugins") 15 | .setDesc("One line per plugin name") 16 | .addTextArea((text) => 17 | text 18 | .setValue( 19 | this.plugin.settingManager.getSettings().hiddenPlugins 20 | ) 21 | .onChange(async (value) => { 22 | this.plugin.settingManager.updateSettings((setting) => { 23 | setting.value.hiddenPlugins = value; 24 | }); 25 | // console.log("hiddenPlugins", this.plugin.hiddenPlugins); 26 | this.plugin.debouncedFilterPlugins(); 27 | }) 28 | ); 29 | 30 | hiddenPluginsSetting.settingEl.addClasses(["hidden-plugins-setting"]); 31 | } 32 | 33 | createSavedPluginsSetting(container: HTMLElement) { 34 | const savedPluginsSetting = new Setting(container) 35 | .setName("Saved plugins") 36 | .setDesc("One line per plugin name") 37 | .addTextArea((text) => 38 | text 39 | .setValue( 40 | this.plugin.settingManager.getSettings().savedPlugins 41 | ) 42 | .onChange(async (value) => { 43 | this.plugin.settingManager.updateSettings((setting) => { 44 | setting.value.savedPlugins = value; 45 | }); 46 | // console.log("savedPlugins", this.plugin.savedPlugins); 47 | this.plugin.debouncedFilterPlugins(); 48 | }) 49 | ); 50 | 51 | savedPluginsSetting.settingEl.addClasses(["saved-plugins-setting"]); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/FilterModal.ts: -------------------------------------------------------------------------------- 1 | import { ButtonComponent, Modal, Setting } from "obsidian"; 2 | import { CommonSetting } from "./CommonSetting"; 3 | import BetterPluginsPagePlugin from "./main"; 4 | 5 | export enum UpdatedFilterOption { 6 | Within = "with in", 7 | Before = "before", 8 | } 9 | 10 | export enum UpdatedTimeRangeOption { 11 | none = "", 12 | OneWeek = "1 week", 13 | TwoWeek = "2 weeks", 14 | OneMonth = "1 month", 15 | ThreeMonth = "3 months", 16 | SixMonth = "6 months", 17 | OneYear = "1 year", 18 | } 19 | 20 | export enum DownloadCountCompareOption { 21 | less = "less", 22 | greater = "greater", 23 | } 24 | 25 | export class FilterModal extends Modal { 26 | plugin: BetterPluginsPagePlugin; 27 | commonSetting: CommonSetting; 28 | 29 | constructor(plugin: BetterPluginsPagePlugin) { 30 | super(plugin.app); 31 | this.plugin = plugin; 32 | this.commonSetting = new CommonSetting(plugin); 33 | } 34 | 35 | onOpen() { 36 | // sort the setting manager hidden pages value 37 | this.plugin.settingManager.updateSettings((setting) => { 38 | setting.value.hiddenPlugins = setting.value.hiddenPlugins 39 | .split("\n") 40 | .sort() 41 | .join("\n"); 42 | 43 | setting.value.savedPlugins = setting.value.savedPlugins 44 | .split("\n") 45 | .sort() 46 | .join("\n"); 47 | }); 48 | 49 | const { contentEl } = this; 50 | contentEl.empty(); 51 | contentEl.addClasses(["better-plugins-page-plugin-setting-tab"]); 52 | 53 | // add a heading 2 54 | contentEl.createEl("h2", { text: "Filter", cls: "test" }); 55 | 56 | const updatedFilterDropDown = new Setting(contentEl) 57 | .setName("Updated") 58 | .addDropdown((dropdown) => { 59 | const updatedWithinCompare = localStorage.getItem( 60 | "updated-within-compare" 61 | ); 62 | 63 | return dropdown 64 | .addOption( 65 | UpdatedFilterOption.Before, 66 | UpdatedFilterOption.Before 67 | ) 68 | .addOption( 69 | UpdatedFilterOption.Within, 70 | UpdatedFilterOption.Within 71 | ) 72 | .setValue( 73 | updatedWithinCompare ?? UpdatedFilterOption.Within 74 | ) 75 | .onChange(async (value) => { 76 | // set this in local storage 77 | localStorage.setItem( 78 | "updated-within-compare", 79 | value.toString() 80 | ); 81 | // Trigger filtering when the dropdown changes 82 | this.plugin.debouncedFilterPlugins(); 83 | }); 84 | }) 85 | .addDropdown((dropdown) => { 86 | const updatedWithin = localStorage.getItem("updated-within"); 87 | return dropdown 88 | .addOption( 89 | UpdatedTimeRangeOption.none, 90 | UpdatedTimeRangeOption.none 91 | ) 92 | .addOption( 93 | UpdatedTimeRangeOption.OneWeek, 94 | UpdatedTimeRangeOption.OneWeek 95 | ) 96 | .addOption( 97 | UpdatedTimeRangeOption.TwoWeek, 98 | UpdatedTimeRangeOption.TwoWeek 99 | ) 100 | .addOption( 101 | UpdatedTimeRangeOption.OneMonth, 102 | UpdatedTimeRangeOption.OneMonth 103 | ) 104 | .addOption( 105 | UpdatedTimeRangeOption.ThreeMonth, 106 | UpdatedTimeRangeOption.ThreeMonth 107 | ) 108 | .addOption( 109 | UpdatedTimeRangeOption.SixMonth, 110 | UpdatedTimeRangeOption.SixMonth 111 | ) 112 | .addOption( 113 | UpdatedTimeRangeOption.OneYear, 114 | UpdatedTimeRangeOption.OneYear 115 | ) 116 | .setValue(updatedWithin ?? UpdatedTimeRangeOption.none) 117 | .onChange(async (value) => { 118 | // set this in local storage 119 | localStorage.setItem( 120 | "updated-within", 121 | value.toString() 122 | ); 123 | // Trigger filtering when the dropdown changes 124 | this.plugin.debouncedFilterPlugins(); 125 | }); 126 | }); 127 | 128 | const downloadCountDropDown = new Setting(contentEl) 129 | .setName("Download count") 130 | .addDropdown((dropdown) => { 131 | const downloadCountCompare = localStorage.getItem( 132 | "download-count-compare" 133 | ); 134 | 135 | return dropdown 136 | .addOption( 137 | DownloadCountCompareOption.less, 138 | DownloadCountCompareOption.less 139 | ) 140 | .addOption( 141 | DownloadCountCompareOption.greater, 142 | DownloadCountCompareOption.greater 143 | ) 144 | 145 | .setValue( 146 | downloadCountCompare ?? 147 | DownloadCountCompareOption.greater 148 | ) 149 | .onChange(async (value) => { 150 | // set in local storage 151 | localStorage.setItem( 152 | "download-count-compare", 153 | value.toString() 154 | ); 155 | // Trigger filtering when the dropdown changes 156 | this.plugin.debouncedFilterPlugins(); 157 | }); 158 | }) 159 | .addText((text) => { 160 | const downloadCount = localStorage.getItem("download-count"); 161 | // this text input can only accept number 162 | text.inputEl.type = "number"; 163 | return text 164 | .setPlaceholder("1000") 165 | .setValue(downloadCount ?? "") 166 | .onChange(async (value) => { 167 | // set in local storage 168 | localStorage.setItem( 169 | "download-count", 170 | value.toString() 171 | ); 172 | // Trigger filtering when the dropdown changes 173 | this.plugin.debouncedFilterPlugins(); 174 | }); 175 | }); 176 | 177 | // add a toggle to the modal 178 | const showSavedPluginToggle = new Setting(contentEl) 179 | .setName("Only show saved plugins") 180 | .addToggle((toggle) => { 181 | const onlyShowSavedPlugins = 182 | localStorage.getItem("show-saved-plugins"); 183 | 184 | // parse the value to boolean, by default is false 185 | const onlyShowSavedPluginBool = 186 | onlyShowSavedPlugins === "true" ? true : false; 187 | return toggle 188 | .setValue(onlyShowSavedPluginBool) 189 | .onChange(async (value) => { 190 | // store the value in local storage 191 | localStorage.setItem( 192 | "show-saved-plugins", 193 | value.toString() 194 | ); 195 | 196 | // Trigger filtering when the toggle changes 197 | this.plugin.debouncedFilterPlugins(); 198 | }); 199 | }); 200 | showSavedPluginToggle.settingEl.addClasses([ 201 | "show-saved-plugins-toggle", 202 | ]); 203 | 204 | this.commonSetting.createSavedPluginsSetting(contentEl); 205 | 206 | // add a heading 2 to the modal 207 | contentEl.createEl("h2", { text: "Hidden plugins" }); 208 | 209 | // add a toggle to the modal 210 | const toggle = new Setting(contentEl) 211 | .setName("Show hidden plugins") 212 | .addToggle((toggle) => { 213 | const showHiddenPlugins = localStorage.getItem( 214 | "show-hidden-plugins" 215 | ); 216 | 217 | // parse the value to boolean, by default is false 218 | const showHiddenPluginsBool = 219 | showHiddenPlugins === "true" ? true : false; 220 | return toggle 221 | .setValue(showHiddenPluginsBool) 222 | .onChange(async (value) => { 223 | // store the value in local storage 224 | localStorage.setItem( 225 | "show-hidden-plugins", 226 | value.toString() 227 | ); 228 | 229 | // Trigger filtering when the toggle changes 230 | this.plugin.debouncedFilterPlugins(); 231 | }); 232 | }); 233 | toggle.settingEl.addClasses(["show-hidden-plugins-toggle"]); 234 | 235 | this.commonSetting.createHiddenPluginsSetting(contentEl); 236 | 237 | // add a button group to the modal 238 | const buttonGroup = contentEl.createDiv("button-group"); 239 | buttonGroup.addClasses([ 240 | "better-plugins-page-plugin-setting-button-group", 241 | ]); 242 | // add a button to the button group 243 | const resetButton = new ButtonComponent(buttonGroup) 244 | .setButtonText("Reset") 245 | .onClick(() => { 246 | // reset the local storage 247 | localStorage.removeItem("download-count"); 248 | localStorage.removeItem("download-count-compare"); 249 | localStorage.removeItem("updated-within"); 250 | localStorage.removeItem("updated-within-compare"); 251 | localStorage.removeItem("show-hidden-plugins"); 252 | localStorage.removeItem("show-saved-plugins"); 253 | // reset the plugin setting 254 | this.plugin.settingManager.updateSettings((setting) => { 255 | setting.value.hiddenPlugins = ""; 256 | setting.value.savedPlugins = ""; 257 | }); 258 | 259 | // reset the whole modal by calling onOpen 260 | this.onOpen(); 261 | 262 | // trigger filtering 263 | this.plugin.debouncedFilterPlugins(); 264 | }); 265 | 266 | // add the button groups to the modal 267 | contentEl.appendChild(buttonGroup); 268 | } 269 | 270 | onClose() { 271 | const { contentEl } = this; 272 | contentEl.empty(); 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/Interfaces.ts: -------------------------------------------------------------------------------- 1 | import { NoticeManager } from "@/NoticeManager"; 2 | import type { Notice, Plugin as _Plugin } from "obsidian"; 3 | export interface ISettingManager { 4 | /** 5 | * save settings 6 | */ 7 | saveSettings(): Promise; 8 | 9 | /** 10 | * update the settings of the plugin. The updateFunc will be called with the current settings as the argument 11 | * 12 | * @returns the updated settings 13 | */ 14 | updateSettings( 15 | updateFunc: (setting: typeof this.setting) => void 16 | ): SettingType; 17 | 18 | /** 19 | * get the settings of the plugin 20 | */ 21 | getSettings(): SettingType; 22 | 23 | /** 24 | * return the settings of the plugin 25 | */ 26 | loadSettings(): Promise; 27 | } 28 | 29 | export interface Plugin extends _Plugin { 30 | settingManager: ISettingManager; 31 | noticeManager: NoticeManager; 32 | createNotice: ( 33 | message: string | DocumentFragment, 34 | duration?: number | undefined 35 | ) => Notice; 36 | } 37 | -------------------------------------------------------------------------------- /src/NoticeManager.ts: -------------------------------------------------------------------------------- 1 | import { Notice, Plugin } from "obsidian"; 2 | 3 | // map the color to the color of the notice 4 | const colorMap = { 5 | success: "green", 6 | warning: "yellow", 7 | error: "red", 8 | info: "blue", 9 | }; 10 | 11 | export class NoticeManager { 12 | plugin: Plugin; 13 | constructor(plugin: Plugin) { 14 | this.plugin = plugin; 15 | } 16 | 17 | createNotice = ( 18 | message: string, 19 | duration?: number | undefined, 20 | color?: "success" | "warning" | "error" | "info" 21 | ): Notice => { 22 | let notice: Notice; 23 | if (color) { 24 | // create a fragment, create a div inside, set the color and text of the div 25 | const fragment = document.createDocumentFragment(); 26 | const div = document.createElement("div"); 27 | div.style.color = colorMap[color]; 28 | div.setText(`${this.plugin.manifest.name}: ${message}`); 29 | fragment.appendChild(div); 30 | 31 | notice = new Notice(fragment, duration); 32 | } else { 33 | notice = new Notice( 34 | `${this.plugin.manifest.name}: ${message}`, 35 | duration 36 | ); 37 | } 38 | return notice; 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /src/SettingManager.ts: -------------------------------------------------------------------------------- 1 | import { ISettingManager } from "@/Interfaces"; 2 | import { AsyncQueue } from "@/util/AsyncQueue"; 3 | import { SettingSchema } from "@/SettingsSchemas"; 4 | 5 | import { State } from "@/util/State"; 6 | 7 | import { z } from "zod"; 8 | import { getPlugins } from "@/getPlugins"; 9 | import { Plugin } from "@/Interfaces"; 10 | 11 | export type Setting = Prettify>; 12 | 13 | const corruptedMessage = 14 | "The setting is corrupted. You will not be able to save the setting. Please backup your data.json, remove it and reload the plugin. Then migrate your old setting back."; 15 | 16 | /** 17 | * @remarks the setting will not keep the temporary setting. It will only keep the saved settings. 18 | */ 19 | export class MySettingManager implements ISettingManager { 20 | private plugin: Plugin; 21 | private setting: State = new State(DEFAULT_SETTING); 22 | private asyncQueue = new AsyncQueue(); 23 | 24 | /** 25 | * whether the setting is loaded successfully 26 | */ 27 | private isLoaded = false; 28 | 29 | /** 30 | * @remarks don't forget to call `loadSettings` after creating this class 31 | */ 32 | constructor(plugin: Plugin) { 33 | this.plugin = plugin; 34 | } 35 | 36 | /** 37 | * this function will update the setting and save it to the json file. But it is still a sync function. 38 | * You should always use this function to update setting 39 | */ 40 | updateSettings( 41 | updateFunc: (setting: typeof this.setting) => void 42 | ): Setting { 43 | // update the setting first 44 | updateFunc(this.setting); 45 | // save the setting to json 46 | this.asyncQueue.push(this.saveSettings.bind(this)); 47 | // return the updated setting 48 | return this.getSettings(); 49 | } 50 | 51 | getSettings() { 52 | return { 53 | ...this.setting.value, 54 | hiddenPluginsArray: getPlugins(this.setting.value.hiddenPlugins), 55 | savedPluginsArray: getPlugins(this.setting.value.savedPlugins), 56 | }; 57 | } 58 | 59 | /** 60 | * load the settings from the json file 61 | */ 62 | async loadSettings() { 63 | // load the data, this can be null if the plugin is used for the first time 64 | const loadedData = (await this.plugin.loadData()) as unknown | null; 65 | 66 | // console.log("loaded: ", loadedData); 67 | 68 | // if the data is null, then we need to initialize the data 69 | if (!loadedData) { 70 | this.setting.value = DEFAULT_SETTING; 71 | this.isLoaded = true; 72 | await this.saveSettings(); 73 | return this.setting.value; 74 | } 75 | 76 | const result = SettingSchema.safeParse(loadedData); 77 | // the data schema is wrong or the data is corrupted, then we need to initialize the data 78 | if (!result.success) { 79 | this.plugin.createNotice(corruptedMessage); 80 | console.warn("parsed loaded data failed", result.error.flatten()); 81 | this.isLoaded = false; 82 | this.setting.value = DEFAULT_SETTING; 83 | return this.setting.value; 84 | } 85 | 86 | // console.log("parsed loaded data successfully"); 87 | 88 | this.setting.value = result.data; 89 | return this.setting.value; 90 | } 91 | 92 | /** 93 | * save the settings to the json file 94 | */ 95 | async saveSettings() { 96 | if (!this.isLoaded) { 97 | // try to parse it again to see if it is corrupted 98 | const result = SettingSchema.safeParse(this.setting.value); 99 | 100 | if (!result.success) { 101 | this.plugin.createNotice(corruptedMessage); 102 | console.warn( 103 | "parsed loaded data failed", 104 | result.error.flatten() 105 | ); 106 | return; 107 | } 108 | 109 | this.isLoaded = true; 110 | // console.log("parsed loaded data successfully"); 111 | } 112 | await this.plugin.saveData(this.setting.value); 113 | } 114 | } 115 | 116 | export const DEFAULT_SETTING: Setting = { 117 | hiddenPlugins: "", 118 | savedPlugins: "", 119 | pluginNoteCache: {}, 120 | pluginNoteFeatureEnabled: true, 121 | }; 122 | -------------------------------------------------------------------------------- /src/SettingsSchemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const SettingSchema = z.object({ 4 | hiddenPlugins: z.string().default(""), 5 | savedPlugins: z.string().default(""), 6 | pluginNoteCache: z.record(z.string()).default({}), 7 | pluginNoteFeatureEnabled: z.boolean().default(true), 8 | }); 9 | -------------------------------------------------------------------------------- /src/getName.ts: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | 3 | export const prunedName = (text: string) => { 4 | return text.endsWith("Installed") ? text.slice(0, -9) : text; 5 | }; 6 | 7 | export const getName = (pluginCard: HTMLElement) => { 8 | const name = $(pluginCard).find(".community-item-name").contents().text(); 9 | // if end with "Installed" remove it 10 | return prunedName(name).trim(); 11 | }; 12 | -------------------------------------------------------------------------------- /src/getPlugins.ts: -------------------------------------------------------------------------------- 1 | export const getPlugins = (pluginsString: string) => { 2 | if (pluginsString.trim() === "") { 3 | return []; 4 | } 5 | return pluginsString.trim().split("\n"); 6 | }; 7 | -------------------------------------------------------------------------------- /src/getUpdatedWithinMilliseconds.ts: -------------------------------------------------------------------------------- 1 | import { UpdatedTimeRangeOption } from "@/FilterModal"; 2 | 3 | export const getUpdatedWithinMilliseconds = (updatedWithin: string) => { 4 | let updatedWithinMilliseconds = 0; 5 | if (updatedWithin === UpdatedTimeRangeOption.OneWeek) 6 | updatedWithinMilliseconds = 7 * 24 * 60 * 60 * 1000; 7 | else if (updatedWithin === UpdatedTimeRangeOption.TwoWeek) 8 | updatedWithinMilliseconds = 14 * 24 * 60 * 60 * 1000; 9 | else if (updatedWithin === UpdatedTimeRangeOption.OneMonth) 10 | updatedWithinMilliseconds = 30 * 24 * 60 * 60 * 1000; 11 | else if (updatedWithin === UpdatedTimeRangeOption.ThreeMonth) 12 | updatedWithinMilliseconds = 90 * 24 * 60 * 60 * 1000; 13 | else if (updatedWithin === UpdatedTimeRangeOption.SixMonth) 14 | updatedWithinMilliseconds = 180 * 24 * 60 * 60 * 1000; 15 | else if (updatedWithin === UpdatedTimeRangeOption.OneYear) 16 | updatedWithinMilliseconds = 365 * 24 * 60 * 60 * 1000; 17 | return updatedWithinMilliseconds; 18 | }; 19 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { Notice, setIcon, setTooltip, Plugin, TFile } from "obsidian"; 2 | 3 | import "@total-typescript/ts-reset"; 4 | import "@total-typescript/ts-reset/dom"; 5 | import { MySettingManager } from "@/SettingManager"; 6 | import $ from "jquery"; 7 | import { observeIsPresent } from "@/observer"; 8 | import debounce from "lodash/debounce"; 9 | import * as chrono from "chrono-node"; 10 | // import { BetterPluginsPagePluginSettingTab } from "./BetterPluginsPagePluginSettingTab"; 11 | import { 12 | DownloadCountCompareOption, 13 | FilterModal, 14 | UpdatedFilterOption, 15 | UpdatedTimeRangeOption, 16 | } from "./FilterModal"; 17 | import { getPlugins } from "./getPlugins"; 18 | import { getName, prunedName } from "./getName"; 19 | import { getUpdatedWithinMilliseconds } from "./getUpdatedWithinMilliseconds"; 20 | import { NoticeManager } from "@/NoticeManager"; 21 | import { BetterPluginsPagePluginSettingTab } from "@/BetterPluginsPagePluginSettingTab"; 22 | 23 | export default class BetterPluginsPagePlugin extends Plugin { 24 | settingManager: MySettingManager; 25 | noticeManager: NoticeManager; 26 | 27 | isPluginsPagePresentObserver: MutationObserver; 28 | modalContentObserver: MutationObserver; 29 | lock = false; 30 | 31 | // we need this observer to observe the search results changes 32 | communityItemsObserver = new MutationObserver((mutationsList) => { 33 | // Check if there are changes in the search results 34 | for (const mutation of mutationsList) { 35 | if (mutation.type === "childList") { 36 | if (this.lock) return; 37 | this.debouncedFilterPlugins(); 38 | } 39 | } 40 | }); 41 | 42 | async onload() { 43 | // initialize the setting manager and notice manager 44 | this.settingManager = new MySettingManager(this); 45 | this.noticeManager = new NoticeManager(this); 46 | 47 | // load the setting using setting manager 48 | await this.settingManager.loadSettings(); 49 | 50 | // This adds a simple command that can be triggered anywhere 51 | this.addCommand({ 52 | id: "open-plugins-page", 53 | name: "Open plugins directory page", 54 | callback: this.openPluginsPage.bind(this), 55 | }); 56 | 57 | // This adds a settings tab so the user can configure various aspects of the plugin 58 | this.addSettingTab( 59 | new BetterPluginsPagePluginSettingTab(this.app, this) 60 | ); 61 | 62 | this.isPluginsPagePresentObserver = observeIsPresent( 63 | "div.mod-community-modal", 64 | (isPresent) => { 65 | if (isPresent) { 66 | this.onPluginsPageShow(); 67 | } else { 68 | this.onPluginsPageClose(); 69 | } 70 | } 71 | ); 72 | } 73 | 74 | onPluginsPageClose = () => { 75 | // Disconnect the observer 76 | this.communityItemsObserver.disconnect(); 77 | // Disconnect the modal content observer 78 | this.modalContentObserver?.disconnect(); 79 | }; 80 | 81 | openPluginsPage() { 82 | // new SampleModal(this.app).open(); 83 | // the community plugins tab id is "community-plugins", we get it from this.app.setting.settingTabs 84 | this.app.setting.open(); 85 | this.app.setting.openTabById("community-plugins"); 86 | 87 | // select the button.mod-cta:contains("Browse") element 88 | const browseButton = $( 89 | 'button.mod-cta:contains("Browse")' 90 | ) as JQuery; 91 | // click the button 92 | browseButton.trigger("click"); 93 | } 94 | 95 | debouncedFilterPlugins = debounce( 96 | () => { 97 | // Get all community items and store them in an array 98 | const communityItems = Array.from( 99 | document.querySelectorAll(".community-item") 100 | ); 101 | try { 102 | this.lock = true; 103 | 104 | // Get filter options from localStorage 105 | const downloadCountCompare = 106 | localStorage.getItem("download-count-compare") ?? 107 | DownloadCountCompareOption.greater; 108 | const downloadCount = parseInt( 109 | localStorage.getItem("download-count") ?? "0", 110 | 10 111 | ); 112 | const updatedWithinCompare = 113 | localStorage.getItem("updated-within-compare") ?? 114 | UpdatedFilterOption.Within; 115 | const updatedWithin = 116 | localStorage.getItem("updated-within") ?? 117 | UpdatedTimeRangeOption.none; 118 | const onlyShowSavedPlugins = 119 | localStorage.getItem("show-saved-plugins") === "true"; 120 | const showHiddenPlugins = 121 | localStorage.getItem("show-hidden-plugins") === "true"; 122 | 123 | // Get settings from the setting manager 124 | const settings = this.settingManager.getSettings(); 125 | 126 | communityItems.forEach((element) => { 127 | const jElement = $(element); 128 | const itemName = getName(element as HTMLElement); 129 | 130 | // Check if the plugin is saved 131 | const isSaved = 132 | settings.savedPluginsArray.includes(itemName); 133 | element.setAttribute("data-saved", isSaved.toString()); 134 | 135 | if (downloadCount > 0) { 136 | const downloadsText = jElement 137 | .find(".community-item-downloads-text") 138 | .text() 139 | .replace(/,/g, ""); 140 | const itemDownloads = downloadsText 141 | ? parseInt(downloadsText, 10) 142 | : 0; 143 | 144 | if ( 145 | (downloadCountCompare === "greater" && 146 | itemDownloads <= downloadCount) || 147 | (downloadCountCompare === "less" && 148 | itemDownloads >= downloadCount) 149 | ) { 150 | jElement.hide(); 151 | return; 152 | } 153 | } 154 | 155 | if (updatedWithin !== UpdatedTimeRangeOption.none) { 156 | const updatedWithinMilliseconds = 157 | getUpdatedWithinMilliseconds(updatedWithin); 158 | const updatedText = jElement 159 | .find(".community-item-updated") 160 | .text() 161 | .replace("Updated ", ""); 162 | const updatedDate = chrono.parseDate(updatedText); 163 | 164 | if (updatedDate) { 165 | const currentDate = new Date(); 166 | const timeDifference = 167 | currentDate.getTime() - updatedDate.getTime(); 168 | 169 | if ( 170 | (updatedWithinCompare === 171 | UpdatedFilterOption.Within && 172 | timeDifference <= 173 | updatedWithinMilliseconds) || 174 | (updatedWithinCompare === 175 | UpdatedFilterOption.Before && 176 | timeDifference > updatedWithinMilliseconds) 177 | ) { 178 | jElement.show(); 179 | } else { 180 | jElement.hide(); 181 | return; 182 | } 183 | } 184 | } 185 | 186 | if (onlyShowSavedPlugins && !isSaved) { 187 | jElement.hide(); 188 | return; 189 | } 190 | 191 | const isHidden = 192 | settings.hiddenPluginsArray.includes(itemName); 193 | 194 | if (!isHidden) { 195 | jElement 196 | .removeClass( 197 | "better-plugins-page-hidden-community-item" 198 | ) 199 | .show(); 200 | } else { 201 | jElement.toggleClass( 202 | "better-plugins-page-hidden-community-item", 203 | showHiddenPlugins 204 | ); 205 | showHiddenPlugins ? jElement.show() : jElement.hide(); 206 | } 207 | }); 208 | } catch (e) { 209 | console.error(e); 210 | // Handle the error 211 | } finally { 212 | this.lock = false; 213 | const summaryText = document.querySelector( 214 | ".community-modal-search-summary" 215 | ); 216 | this.addButtons(communityItems as HTMLElement[]); 217 | const visiblePlugins = communityItems.filter( 218 | (element: HTMLDivElement) => 219 | !(element.style.display === "none") 220 | ).length; 221 | summaryText?.setText(`Showing ${visiblePlugins} plugins:`); 222 | } 223 | }, 224 | 500, 225 | { leading: true, trailing: true } 226 | ); 227 | 228 | // Function to add the "Hide" and "Show" buttons to all community item cards 229 | addButtons(cards: HTMLElement[]) { 230 | cards.forEach((element) => { 231 | const card = element; 232 | const isInstalledPlugin = 233 | $(element).find( 234 | ".community-item-name .flair.mod-pop:contains('Installed')" 235 | ).length > 0; 236 | 237 | // set the data-installed attribute to the card 238 | card.setAttribute("data-installed", isInstalledPlugin.toString()); 239 | 240 | // Check if the buttons container already exists in the card 241 | let buttonsContainer = card.querySelector( 242 | ".buttons-container" 243 | ) as HTMLDivElement; 244 | 245 | if (!buttonsContainer) { 246 | // Create the buttons container with a column layout 247 | buttonsContainer = document.createElement("div"); 248 | buttonsContainer.classList.add("buttons-container"); 249 | 250 | // Define button configurations 251 | const buttonConfigs = [ 252 | { 253 | className: "hide-button", 254 | icon: "eye-off", 255 | tooltip: "Hide", 256 | toggle: true, 257 | settingKey: "hiddenPlugins", 258 | }, 259 | { 260 | className: "show-button", 261 | icon: "eye", 262 | tooltip: "Show", 263 | toggle: false, 264 | settingKey: "hiddenPlugins", 265 | }, 266 | { 267 | className: "save-button", 268 | icon: "star", 269 | tooltip: "Save", 270 | toggle: true, 271 | settingKey: "savedPlugins", 272 | }, 273 | { 274 | className: "unsave-button", 275 | icon: "star-off", 276 | tooltip: "Unsave", 277 | toggle: false, 278 | settingKey: "savedPlugins", 279 | }, 280 | ] as const; 281 | 282 | buttonConfigs.forEach((config) => { 283 | const button = document.createElement("button"); 284 | button.classList.add(config.className, "clickable-icon"); 285 | setIcon(button, config.icon); 286 | setTooltip(button, config.tooltip); 287 | 288 | button.addEventListener("click", (event) => { 289 | event.stopImmediatePropagation(); 290 | event.stopPropagation(); 291 | const itemName = getName(card); 292 | this.togglePlugin( 293 | itemName, 294 | config.toggle, 295 | config.settingKey 296 | ); 297 | }); 298 | 299 | // Append the button to the container 300 | buttonsContainer.appendChild(button); 301 | }); 302 | 303 | // Append the container to the card 304 | card.appendChild(buttonsContainer); 305 | } 306 | }); 307 | } 308 | 309 | /** 310 | * Function to toggle a community item to the hidden or saved plugins list. 311 | * @param {string} pluginName - The name of the plugin to toggle. 312 | * @param {boolean} toggle - Whether to toggle (hide/save) the plugin. 313 | * @param {string} settingKey - The key for the setting ("hiddenPlugins" or "savedPlugins"). 314 | */ 315 | togglePlugin( 316 | pluginName: string, 317 | toggle: boolean, 318 | settingKey: "hiddenPlugins" | "savedPlugins" 319 | ) { 320 | const settingValue = getPlugins( 321 | this.settingManager.getSettings()[settingKey] 322 | ); 323 | const pluginIndex = settingValue.indexOf(pluginName); 324 | 325 | if (toggle && pluginIndex === -1) { 326 | settingValue.push(pluginName); 327 | } else if (!toggle && pluginIndex !== -1) { 328 | settingValue.splice(pluginIndex, 1); 329 | } 330 | 331 | this.updateSettingAndFilter(settingValue, settingKey); 332 | } 333 | 334 | /** 335 | * Update the setting and trigger the filter function. 336 | * @param {string[]} updatedSetting - The updated setting array (hidden or saved plugins). 337 | * @param {string} settingKey - The key for the setting ("hiddenPlugins" or "savedPlugins"). 338 | */ 339 | updateSettingAndFilter( 340 | updatedSetting: string[], 341 | settingKey: "hiddenPlugins" | "savedPlugins" 342 | ) { 343 | const newSetting = updatedSetting.join("\n"); 344 | 345 | // Update the corresponding setting 346 | this.settingManager.updateSettings((setting) => { 347 | setting.value[settingKey] = newSetting; 348 | this.debouncedFilterPlugins(); 349 | }); 350 | } 351 | 352 | tryGetNote = (pluginName: string) => { 353 | // get the note path from the setting cache 354 | // const notePath = 355 | // this.settingManager.getSettings().pluginNoteCache[pluginName]; 356 | 357 | // try to get the note from the path 358 | // const note = this.app.vault.getAbstractFileByPath( 359 | // notePath ?? "" 360 | // ) as TFile | null; 361 | 362 | // get the note with title equal to pluginName 363 | // if the note doesn't exist, then simply return 364 | // if the note exists, then we create a button to link to the note 365 | // if (note) return note; 366 | 367 | const notes = this.app.vault 368 | .getMarkdownFiles() 369 | .filter( 370 | (file) => 371 | file.basename.toLocaleLowerCase() === 372 | pluginName.toLocaleLowerCase() 373 | ); 374 | if (notes.length === 0) return null; 375 | if (notes.length > 1) { 376 | this.noticeManager.createNotice( 377 | "There are multiple plugin notes with the same name. Open the first note discovered. But please rename the notes to be unique.", 378 | 5000, 379 | "warning" 380 | ); 381 | } 382 | 383 | const targetNote = notes[0]!; 384 | // save this note path to the setting cache 385 | this.settingManager.updateSettings((setting) => { 386 | setting.value.pluginNoteCache[pluginName] = targetNote.path; 387 | }); 388 | return targetNote; 389 | }; 390 | 391 | onPluginDetailsShow() { 392 | const communityModalButtonContainer = document.querySelector( 393 | "div.community-modal-button-container" 394 | ) as HTMLDivElement; 395 | 396 | if (!communityModalButtonContainer) return; 397 | 398 | const existingButton = communityModalButtonContainer.querySelector( 399 | ".add-note-link-button" 400 | ); 401 | 402 | if (!existingButton) { 403 | const pluginNameElement = document.querySelector( 404 | ".community-modal-info-name" 405 | ); 406 | 407 | if (!pluginNameElement) return; 408 | 409 | const pluginName = prunedName(pluginNameElement.textContent || ""); 410 | 411 | if (pluginName) { 412 | const note = this.tryGetNote(pluginName); 413 | 414 | const button = document.createElement("button"); 415 | button.classList.add("add-note-link-button"); 416 | button.textContent = note ? "My Note" : "Create Note"; 417 | setTooltip( 418 | button, 419 | note ? note.path : `Create ${pluginName}.md` 420 | ); 421 | 422 | button.addEventListener("click", async () => { 423 | try { 424 | if (!note) { 425 | const newNote = await this.app.vault.create( 426 | `${pluginName}.md`, 427 | "" 428 | ); 429 | this.app.workspace.getLeaf().openFile(newNote); 430 | } else { 431 | this.app.workspace.getLeaf().openFile(note); 432 | } 433 | 434 | const closeButton = $( 435 | ".modal-container .modal-close-button" 436 | ) as JQuery; 437 | closeButton.trigger("click"); 438 | } catch (e) { 439 | console.error(e); 440 | this.noticeManager.createNotice( 441 | "Failed to create note. Please check the console for more details.", 442 | 5000, 443 | "error" 444 | ); 445 | } 446 | }); 447 | 448 | communityModalButtonContainer.appendChild(button); 449 | } 450 | } 451 | } 452 | 453 | onPluginsPageShow() { 454 | const settingItemControl = $( 455 | ".community-modal-controls .setting-item:not('.mod-toggle') .setting-item-control" 456 | ); 457 | 458 | if ( 459 | !settingItemControl.find(".better-plugins-page-filter-btn").length 460 | ) { 461 | const button = $("") 462 | .addClass("clickable-icon") 463 | .addClass("better-plugins-page-filter-btn") 464 | .on("click", () => new FilterModal(this).open()); 465 | 466 | setIcon(button[0] as HTMLButtonElement, "filter"); 467 | setTooltip(button[0] as HTMLButtonElement, "Filter"); 468 | settingItemControl.append(button); 469 | } 470 | 471 | const communityModalSearchResults = document.querySelector( 472 | "div.community-modal-search-results" 473 | )!; 474 | this.communityItemsObserver.observe(communityModalSearchResults, { 475 | childList: true, 476 | subtree: true, 477 | }); 478 | 479 | if (this.settingManager.getSettings().pluginNoteFeatureEnabled) 480 | this.modalContentObserver = observeIsPresent( 481 | ".community-modal-details .community-modal-info-name", 482 | (isPresent) => { 483 | if (isPresent) { 484 | this.onPluginDetailsShow(); 485 | } else { 486 | // dummy 487 | } 488 | } 489 | ); 490 | 491 | // set timeout 500 ms and then trigger the filtering 492 | setTimeout(() => { 493 | this.debouncedFilterPlugins(); 494 | }, 500); 495 | } 496 | 497 | createNotice = ( 498 | ...props: Parameters 499 | ): Notice => this.noticeManager.createNotice(...props); 500 | 501 | onunload() { 502 | super.onunload(); 503 | this.isPluginsPagePresentObserver.disconnect(); 504 | this.communityItemsObserver.disconnect(); // Disconnect the communityItemsObserver 505 | } 506 | } 507 | -------------------------------------------------------------------------------- /src/observer.ts: -------------------------------------------------------------------------------- 1 | export function observeIsPresent( 2 | selector: string, 3 | onChange?: (isPresent: boolean) => void 4 | ) { 5 | let isTargetNodePresent = false; 6 | 7 | // Create a new MutationObserver with a callback function 8 | const observer = new MutationObserver(function () { 9 | // Select the target node 10 | const targetNode = document.querySelector(selector); 11 | 12 | // Check whether the target node exists 13 | const isPresentNow = !!targetNode; 14 | 15 | if (isPresentNow && !isTargetNodePresent) { 16 | // The target node has appeared 17 | if (onChange) onChange(true); 18 | isTargetNodePresent = true; 19 | } else if (!isPresentNow && isTargetNodePresent) { 20 | // The target node has disappeared 21 | if (onChange) onChange(false); 22 | isTargetNodePresent = false; 23 | } 24 | }); 25 | 26 | // Start observing the changes in the document body (or any relevant parent node) 27 | observer.observe(document.body, { childList: true, subtree: true }); 28 | return observer; 29 | } 30 | -------------------------------------------------------------------------------- /src/typings/NestedKeyof.d.ts: -------------------------------------------------------------------------------- 1 | type NestedKeyOf = { 2 | [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object 3 | ? `${Key}` | `${Key}.${NestedKeyOf}` 4 | : `${Key}`; 5 | }[keyof ObjectType & (string | number)]; 6 | -------------------------------------------------------------------------------- /src/typings/NestedValue.d.ts: -------------------------------------------------------------------------------- 1 | type NestedValue = Path extends keyof ObjectType 2 | ? ObjectType[Path] 3 | : Path extends `${infer Key}.${infer Rest}` 4 | ? ObjectType[Key] extends object 5 | ? NestedValue 6 | : never 7 | : never; 8 | -------------------------------------------------------------------------------- /src/typings/Prettify.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | type Prettify = { 3 | [K in keyof T]: T[K]; 4 | } & {}; 5 | -------------------------------------------------------------------------------- /src/typings/WithPrefix.d.ts: -------------------------------------------------------------------------------- 1 | type WithPrefixNumber = `${T}${number}`; 2 | -------------------------------------------------------------------------------- /src/typings/obsidian-ex.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | App, 3 | CachedMetadata, 4 | Command, 5 | Constructor, 6 | DataWriteOptions, 7 | EditorPosition, 8 | EditorRange, 9 | EditorSuggest, 10 | EventRef, 11 | Events, 12 | FileView, 13 | KeymapEventHandler, 14 | KeymapEventListener, 15 | KeymapInfo, 16 | Loc, 17 | Modifier, 18 | ObsidianProtocolHandler, 19 | OpenViewState, 20 | PaneType, 21 | Plugin, 22 | Reference, 23 | SplitDirection, 24 | TAbstractFile, 25 | TFile, 26 | TFolder, 27 | View, 28 | ViewState, 29 | WorkspaceLeaf, 30 | WorkspaceMobileDrawer, 31 | WorkspaceSidedock, 32 | WorkspaceSplit, 33 | WorkspaceTabs, 34 | WorkspaceWindow, 35 | WorkspaceWindowInitData, 36 | } from "obsidian"; 37 | import { EditorView } from "@codemirror/view"; 38 | import { EditorState, Extension } from "@codemirror/state"; 39 | 40 | /* eslint-disable @typescript-eslint/no-explicit-any */ 41 | 42 | interface Account { 43 | /** 44 | * The company associated with the activated commercial license 45 | */ 46 | company: string; 47 | /** 48 | * The email address associated with the account 49 | */ 50 | email: string; 51 | /** 52 | * 53 | */ 54 | expiry: number; 55 | /** 56 | * 57 | */ 58 | key: string | undefined; 59 | /** 60 | * 61 | */ 62 | keyValidation: string; 63 | /** 64 | * The license available to the account 65 | */ 66 | license: "" | "insider"; 67 | /** 68 | * Profile name 69 | */ 70 | name: string; 71 | /** 72 | * 73 | */ 74 | seats: number; 75 | /** 76 | * 77 | */ 78 | token: string; 79 | 80 | // TODO: Add Sync and Publish API functions here 81 | } 82 | 83 | // interface AppMenuBarManager { 84 | // /** 85 | // * Reference to App 86 | // */ 87 | // app: App; 88 | // 89 | // /** 90 | // * 91 | // */ 92 | // requestRender: () => void; 93 | // 94 | // /** 95 | // * 96 | // */ 97 | // requestUpdateViewState: () => void; 98 | // } 99 | 100 | interface Commands { 101 | /** 102 | * Reference to App 103 | */ 104 | app: App; 105 | 106 | /** 107 | * Commands *without* editor callback, will always be available in the command palette 108 | * @example `app:open-vault` or `app:reload` 109 | */ 110 | commands: Record; 111 | /** 112 | * Commands *with* editor callback, will only be available when editor is active and callback returns true 113 | * @example `editor:fold-all` or `command-palette:open` 114 | */ 115 | editorCommands: Record; 116 | /** 117 | * Add a command to the command registry 118 | * @param command Command to add 119 | */ 120 | addCommand: (command: Command) => void; 121 | /** 122 | * Execute a command by reference 123 | * @param command Command to execute 124 | */ 125 | executeCommand: (command: Command) => boolean; 126 | /** 127 | * Execute a command by ID 128 | * @param commandId ID of command to execute 129 | */ 130 | executeCommandById: (commandId: string) => boolean; 131 | /** 132 | * Find a command by ID 133 | * @param commandId 134 | */ 135 | findCommand: (commandId: string) => Command | undefined; 136 | /** 137 | * Lists **all** commands, both with and without editor callback 138 | */ 139 | listCommands: () => Command[]; 140 | /** 141 | * Remove a command from the command registry 142 | * @param commandId Command to remove 143 | */ 144 | removeCommand: (commandId: string) => void; 145 | } 146 | 147 | // interface ThemeRecord { 148 | // /** 149 | // * Author of the theme 150 | // */ 151 | // author: string; 152 | // /** 153 | // * @internal 154 | // */ 155 | // authorEl?: HTMLElement; 156 | // /** 157 | // * Amount of downloads the theme 158 | // */ 159 | // downloads: number; 160 | // /** 161 | // * @internal 162 | // */ 163 | // el?: HTMLElement; 164 | // matches: null; 165 | // modes: ['light', 'dark'] | ['light']; 166 | // name: string; 167 | // nameEl?: HTMLElement; 168 | // repo: string; 169 | // score: number; 170 | // screenshot: string; 171 | // updateIconEl?: HTMLElement; 172 | // /** 173 | // * Whether the theme was updated 174 | // */ 175 | // updated: number; 176 | // } 177 | 178 | interface ThemeManifest { 179 | /** 180 | * Name of the author of the theme 181 | */ 182 | author: string; 183 | /** 184 | * URL to the author's website 185 | */ 186 | authorUrl?: string; 187 | /** 188 | * Storage location of the theme relative to the vault root 189 | */ 190 | dir: string; 191 | /** 192 | * URL for funding the author 193 | */ 194 | fundingUrl?: string; 195 | /** 196 | * Minimum Obsidian version compatible with the theme 197 | */ 198 | minAppVersion: string; 199 | /** 200 | * Name of the theme 201 | */ 202 | name: string; 203 | /** 204 | * Version of the theme 205 | * @remark Defaults to "0.0.0" if no theme manifest was provided in the repository 206 | */ 207 | version: "0.0.0" | string; 208 | } 209 | 210 | interface CustomCSS extends Component { 211 | /** 212 | * Reference to App 213 | */ 214 | app: App; 215 | /** 216 | * @internal 217 | */ 218 | boundRaw: () => void; 219 | /** 220 | * @internal Cache of CSS snippet filepath (relative to vault root) to CSS snippet contents 221 | */ 222 | csscache: Map; 223 | /** 224 | * Set of enabled snippet, given by filenames 225 | */ 226 | enabledSnippets: Set; 227 | /** 228 | * @internal 229 | * Contains references to Style elements containing custom CSS snippets 230 | */ 231 | extraStyleEls: HTMLStyleElement[]; 232 | /** 233 | * List of theme names not fully updated to post v1.0.0 theme guidelines 234 | */ 235 | oldThemes: string[]; 236 | /** 237 | * @internal 238 | */ 239 | queue: WeakMap; 240 | /** 241 | * @internal 242 | */ 243 | requestLoadSnippets: () => void; 244 | /** 245 | * @internal 246 | */ 247 | requestLoadTheme: () => void; 248 | /** 249 | * @internal 250 | */ 251 | requestReadThemes: () => void; 252 | /** 253 | * List of snippets detected by Obsidian, given by their filenames 254 | */ 255 | snippets: string[]; 256 | /** 257 | * Currently active theme, given by its name 258 | * @remark "" is the default Obsidian theme 259 | */ 260 | theme: "" | string; 261 | /** 262 | * Mapping of theme names to their manifest 263 | */ 264 | themes: Record; 265 | /** 266 | * @internal 267 | */ 268 | updates: Record; 269 | 270 | /** 271 | * Check whether a specific theme can be updated 272 | * @param themeName - Name of the theme to check 273 | */ 274 | checkForUpdate: (themeName: string) => void; 275 | /** 276 | * Check all themes for updates 277 | */ 278 | checkForUpdates: () => void; 279 | /** 280 | * Disable translucency of application background 281 | */ 282 | disableTranslucency: () => void; 283 | /** 284 | * Fetch legacy theme CSS using the pre-v1.0.0 theme download pipeline 285 | * @returns string obsidian.css contents 286 | */ 287 | downloadLegacyTheme: ({ repo: string }) => Promise; 288 | /** 289 | * Enable translucency of application background 290 | */ 291 | enableTranslucency: () => void; 292 | /** 293 | * Fetch a theme's manifest using repository URL 294 | * @remark Do **not** include github prefix, only `username/repo` 295 | */ 296 | getManifest: (repoUrl: string) => Promise; 297 | /** 298 | * Convert snippet name to its corresponding filepath (relative to vault root) 299 | * @returns string `.obsidian/snippets/${snippetName}.css` 300 | */ 301 | getSnippetPath: (snippetName: string) => string; 302 | /** 303 | * Returns the folder path where snippets are stored (relative to vault root) 304 | */ 305 | getSnippetsFolder: () => string; 306 | /** 307 | * Returns the folder path where themes are stored (relative to vault root) 308 | */ 309 | getThemesFolder: () => string; 310 | /** 311 | * Convert theme name to its corresponding filepath (relative to vault root) 312 | * @returns string `.obsidian/themes/${themeName}/theme.css` 313 | */ 314 | getThemePath: (themeName: string) => string; 315 | /** 316 | * Returns whether there are themes that can be updated 317 | */ 318 | hasUpdates: () => boolean; 319 | /** 320 | * Install a legacy theme using the pre-v1.0.0 theme download pipeline
321 | * Will create a corresponding dummy manifest for the theme 322 | * @remark Name will be used as the folder name for the theme 323 | */ 324 | installLegacyTheme: ({ 325 | name: string, 326 | repo: string, 327 | author: string, 328 | }) => Promise; 329 | /** 330 | * Install a theme using the regular theme download pipeline 331 | */ 332 | installTheme: ( 333 | { name: string, repo: string, author: string }, 334 | version: string 335 | ) => Promise; 336 | /** 337 | * Check whether a specific theme is installed by theme name 338 | */ 339 | isThemeInstalled: (themeName: string) => boolean; 340 | /** 341 | * @internal 342 | */ 343 | onRaw: (e: any) => void; 344 | /** 345 | * @internal 346 | */ 347 | onload: () => void; 348 | /** 349 | * @todo 350 | * @internal 351 | */ 352 | readSnippets: () => void; 353 | /** 354 | * @todo 355 | * @internal 356 | */ 357 | readThemes: () => void; 358 | /** 359 | * Remove a theme by theme name 360 | */ 361 | removeTheme: (themeName: string) => Promise; 362 | /** 363 | * Set the activation status of a snippet by snippet name 364 | */ 365 | setCssEnabledStatus: (snippetName: string, enabled: boolean) => void; 366 | /** 367 | * Set the active theme by theme name 368 | */ 369 | setTheme: (themeName: string) => void; 370 | /** 371 | * Set the translucency of application background 372 | */ 373 | setTranslucency: (translucency: boolean) => void; 374 | } 375 | 376 | interface ObsidianDOM { 377 | /** 378 | * Root element of the application 379 | */ 380 | appContainerEl: HTMLElement; 381 | /** 382 | * Child of `appContainerEl` containing the main content of the application 383 | */ 384 | horizontalMainContainerEl: HTMLElement; 385 | /** 386 | * Status bar element containing word count among other things 387 | */ 388 | statusBarEl: HTMLElement; 389 | /** 390 | * Child of `horizontalMainContainerEl` containing the workspace DOM 391 | */ 392 | workspaceEl: HTMLElement; 393 | } 394 | 395 | // interface EmbedRegistry { 396 | // embedByExtension: Map any>; 397 | // } 398 | 399 | interface PositionedReference extends Reference { 400 | /** 401 | * Position of the reference in the file 402 | */ 403 | position: { 404 | start: Loc; 405 | end: Loc; 406 | }; 407 | } 408 | 409 | interface LinkUpdate { 410 | /** 411 | * Reference to App 412 | */ 413 | app: App; 414 | /** 415 | * Link position in the file 416 | */ 417 | reference: PositionedReference; 418 | /** 419 | * File that was resolved 420 | */ 421 | resolvedFile: TFile; 422 | /** 423 | * Paths the file could have been resolved to 424 | */ 425 | resolvedPaths: string[]; 426 | /** 427 | * File that contains the link 428 | */ 429 | sourceFile: TFile; 430 | } 431 | 432 | interface HotkeyManager { 433 | /** 434 | * Reference to App 435 | */ 436 | app: App; 437 | /** 438 | * @internal Whether hotkeys have been baked (checks completed) 439 | */ 440 | baked: boolean; 441 | /** 442 | * Assigned hotkeys 443 | */ 444 | bakedHotkeys: KeymapInfo[]; 445 | /** 446 | * Array of hotkey index to command ID 447 | */ 448 | bakedIds: string[]; 449 | /** 450 | * Custom (non-Obsidian default) hotkeys, one to many mapping of command ID to assigned hotkey 451 | */ 452 | customKeys: Record; 453 | /** 454 | * Default hotkeys, one to many mapping of command ID to assigned hotkey 455 | */ 456 | defaultKeys: Record; 457 | 458 | /** 459 | * Add a hotkey to the default hotkeys 460 | * @param command - Command ID to add hotkey to 461 | * @param keys - Hotkeys to add 462 | */ 463 | addDefaultHotkeys: (command: string, keys: KeymapInfo[]) => void; 464 | /** 465 | * Get hotkey associated with command ID 466 | * @param command - Command ID to get hotkey for 467 | */ 468 | getDefaultHotkeys: (command: string) => KeymapInfo[]; 469 | /** 470 | * Remove a hotkey from the default hotkeys 471 | * @param command - Command ID to remove hotkey from 472 | */ 473 | removeDefaultHotkeys: (command: string) => void; 474 | /** 475 | * Add a hotkey to the custom hotkeys (overrides default hotkeys) 476 | * @param command - Command ID to add hotkey to 477 | * @param keys - Hotkeys to add 478 | */ 479 | setHotkeys: (command: string, keys: KeymapInfo[]) => void; 480 | /** 481 | * Get hotkey associated with command ID 482 | * @param command - Command ID to get hotkey for 483 | */ 484 | getHotkeys: (command: string) => KeymapInfo[]; 485 | /** 486 | * Remove a hotkey from the custom hotkeys 487 | * @param command - Command ID to remove hotkey from 488 | */ 489 | removeHotkeys: (command: string) => void; 490 | /** 491 | * Pretty-print hotkey of a command 492 | * @param command 493 | */ 494 | printHotkeyForCommand: (command: string) => string; 495 | /** 496 | * Trigger a command by keyboard event 497 | * @param event - Keyboard event to trigger command with 498 | * @param keypress - Pressed key information 499 | */ 500 | onTrigger: (event: KeyboardEvent, keypress: KeymapInfo) => boolean; 501 | /** 502 | * @internal Bake hotkeys (create mapping of pressed key to command ID) 503 | */ 504 | bake: () => void; 505 | /** 506 | * @internal Load hotkeys from storage 507 | */ 508 | load: () => void; 509 | /** 510 | * @internal Save custom hotkeys to storage 511 | */ 512 | save: () => void; 513 | } 514 | 515 | type InternalPlugin = 516 | | "audio-recorder" 517 | | "backlink" 518 | | "bookmarks" 519 | | "canvas" 520 | | "command-palette" 521 | | "daily-notes" 522 | | "editor-status" 523 | | "file-explorer" 524 | | "file-recovery" 525 | | "global-search" 526 | | "graph" 527 | | "markdown-importer" 528 | | "note-composer" 529 | | "outgoing-link" 530 | | "outline" 531 | | "page-preview" 532 | | "properties" 533 | | "publish" 534 | | "random-note" 535 | | "slash-command" 536 | | "slides" 537 | | "starred" 538 | | "switcher" 539 | | "sync" 540 | | "tag-pane" 541 | | "templates" 542 | | "word-count" 543 | | "workspaces" 544 | | "zk-prefixer"; 545 | 546 | interface InternalPlugins extends Events { 547 | /** 548 | * Reference to App 549 | */ 550 | app: App; 551 | /** 552 | * Mapping of whether an internal plugin is enabled 553 | */ 554 | config: Record; 555 | /** 556 | * @internal 557 | */ 558 | migration: boolean; 559 | /** 560 | * Plugin configs for internal plugins 561 | */ 562 | plugins: Record; 563 | /** 564 | * @internal Request save of plugin configs 565 | */ 566 | requestSaveConfig: () => void; 567 | 568 | /** 569 | * Get an enabled internal plugin by ID 570 | * @param id - ID of the plugin to get 571 | */ 572 | getEnabledPluginById: (id: InternalPlugin) => Plugin | null; 573 | /** 574 | * Get all enabled internal plugins 575 | */ 576 | getEnabledPlugins: () => Plugin[]; 577 | /** 578 | * Get an internal plugin by ID 579 | * @param id - ID of the plugin to get 580 | */ 581 | getPluginById: (id: InternalPlugin) => Plugin; 582 | 583 | /** 584 | * @internal - Load plugin configs and enable plugins 585 | */ 586 | enable: () => Promise; 587 | /** 588 | * @internal 589 | */ 590 | loadPlugin: ({ id: string, name: string }) => string; 591 | /** 592 | * @internal 593 | */ 594 | on: (inp: any, cb: () => void, arg: string) => void; 595 | /** 596 | * @internal 597 | */ 598 | onRaw: (cb1: any, cb2: any) => void; 599 | /** 600 | * @internal - Save current plugin configs 601 | */ 602 | saveConfig: () => Promise; 603 | /** 604 | * @internal 605 | */ 606 | trigger: (arg: string) => void; 607 | } 608 | 609 | interface KeyScope { 610 | /** 611 | * Callback of function to execute when key is pressed 612 | */ 613 | func: () => void; 614 | /** 615 | * Key to match 616 | */ 617 | key: string | null; 618 | /** 619 | * Modifiers to match 620 | */ 621 | modifiers: string | null; 622 | /** 623 | * Scope where the key interceptor is registered 624 | */ 625 | scope: EScope; 626 | } 627 | 628 | // interface KeymapManager { 629 | // /** 630 | // * Modifiers pressed within keyscope 631 | // */ 632 | // modifiers: string; 633 | // /** 634 | // * @internal 635 | // */ 636 | // prevScopes: EScope[]; 637 | // /** 638 | // * @internal - Root scope of the application 639 | // */ 640 | // rootScope: EScope; 641 | // 642 | // /** 643 | // * Get the root scope of the application 644 | // */ 645 | // getRootScope: () => EScope; 646 | // /** 647 | // * Check whether a specific modifier was part of the keypress 648 | // */ 649 | // hasModifier: (modifier: Modifier) => boolean; 650 | // /** 651 | // * Check whether event has same modifiers as the current keypress 652 | // */ 653 | // matchModifiers: (event: KeyboardEvent) => boolean; 654 | // /** 655 | // * @internal - On focus keyscope 656 | // */ 657 | // onFocusIn: (event: FocusEvent) => void; 658 | // /** 659 | // * @internal - On keypress find matching keyscope and execute callback 660 | // */ 661 | // onKeyEvent: (event: KeyboardEvent) => void; 662 | // /** 663 | // * @internal - Pop a scope from the prevScopes stack 664 | // */ 665 | // popScope: (scope: EScope) => void; 666 | // /** 667 | // * @internal - Push a scope to the prevScopes stack and set it as the current scope 668 | // */ 669 | // pushScope: (scope: EScope) => void; 670 | // /** 671 | // * @internal - Update last pressed modifiers 672 | // */ 673 | // updateModifiers: (event: KeyboardEvent) => void; 674 | // } 675 | 676 | // interface LoadProgress { 677 | // /** 678 | // * Application's document 679 | // */ 680 | // doc: Document; 681 | // /** 682 | // * @internal Loading bar element 683 | // */ 684 | // el: HTMLElement; 685 | // /** 686 | // * @internal First part of the line 687 | // */ 688 | // line1El: HTMLElement; 689 | // /** 690 | // * @internal Second part of the line 691 | // */ 692 | // line2El: HTMLElement; 693 | // /** 694 | // * @internal Main line element 695 | // */ 696 | // lineEl: HTMLElement; 697 | // /** 698 | // * @internal Message element for the loading bar 699 | // */ 700 | // messageEl: HTMLElement; 701 | // /** 702 | // * @internal Timeout for the loading bar 703 | // */ 704 | // showTimeout: number; 705 | // 706 | // /** 707 | // * @internal Delay showing the loading bar 708 | // */ 709 | // delayedShow: () => LoadProgress; 710 | // /** 711 | // * @internal Hide and remove the loading bar 712 | // */ 713 | // hide: () => LoadProgress; 714 | // /** 715 | // * @internal Update the loading bar message 716 | // * @param message - Message to update to 717 | // */ 718 | // setMessage: (message: string) => LoadProgress; 719 | // /** 720 | // * @internal Update the loading bar progress 721 | // * @param current - Current progress 722 | // * @param max - Maximum progress 723 | // */ 724 | // setProgress: (current: number, max: number) => LoadProgress; 725 | // /** 726 | // * @internal Set the loading bar to unknown progress 727 | // */ 728 | // setUnknownProgress: () => LoadProgress; 729 | // } 730 | 731 | interface BlockCache { 732 | /** 733 | * Reference to App 734 | */ 735 | app: App; 736 | 737 | /** 738 | * @internal 739 | */ 740 | cache: any; 741 | } 742 | 743 | interface FileCacheEntry { 744 | /** 745 | * Hash of file contents 746 | */ 747 | hash: string; 748 | /** 749 | * Last modified time of file 750 | */ 751 | mtime: number; 752 | /** 753 | * Size of file in bytes 754 | */ 755 | size: number; 756 | } 757 | 758 | interface CustomArrayDict { 759 | data: Record; 760 | 761 | add: (key: T, value: Q) => void; 762 | remove: (key: T, value: Q) => void; 763 | removeKey: (key: T) => void; 764 | get: (key: T) => Q; 765 | keys: () => T[]; 766 | clear: (key: T) => void; 767 | clearAll: () => void; 768 | contains: (key: T) => boolean; 769 | count: () => number; 770 | } 771 | 772 | interface PropertyInfo { 773 | /** 774 | * Name of property 775 | */ 776 | name: string; 777 | /** 778 | * Type of property 779 | */ 780 | type: string; 781 | /** 782 | * Usage count of property 783 | */ 784 | count: number; 785 | } 786 | 787 | type PropertyWidgetType = 788 | | "aliases" 789 | | "checkbox" 790 | | "date" 791 | | "datetime" 792 | | "multitext" 793 | | "number" 794 | | "tags" 795 | | "text"; 796 | 797 | interface PropertyWidget { 798 | /** 799 | * @internal 800 | */ 801 | default: () => void; 802 | /** 803 | * Lucide-dev icon associated with the widget 804 | */ 805 | icon: string; 806 | /** 807 | * @internal Name proxy 808 | */ 809 | name: any; 810 | /** 811 | * @internal Render function for the widget 812 | */ 813 | render: ( 814 | element: HTMLElement, 815 | metadataField: any, 816 | property: PropertyInfo 817 | ) => void; 818 | /** 819 | * @internal Reserved keys for the widget 820 | */ 821 | reservedKeys: string[]; 822 | /** 823 | * Widget type 824 | */ 825 | type: string; 826 | /** 827 | * @internal Validate correctness of property input with respects to the widget 828 | */ 829 | validate: (value: any) => boolean; 830 | } 831 | 832 | interface MetadataTypeManager extends Events { 833 | /** 834 | * Reference to App 835 | */ 836 | app: App; 837 | /** 838 | * Registered properties of the vault 839 | */ 840 | properties: Record; 841 | /** 842 | * @internal Registered type widgets 843 | */ 844 | registeredTypeWidgets: Record; 845 | /** 846 | * Associated widget types for each property 847 | */ 848 | types: Record; 849 | 850 | /** 851 | * Get all registered properties of the vault 852 | */ 853 | getAllProperties: () => Record; 854 | /** 855 | * Get assigned widget type for property 856 | */ 857 | getAssignedType: (property: string) => PropertyWidgetType | null; 858 | /** 859 | * Get info for property 860 | */ 861 | getPropertyInfo: (property: string) => PropertyInfo; 862 | /** 863 | * @internal Get expected widget type for property and the one inferred from the property value 864 | */ 865 | getTypeInfo: ({ key: string, type: string, value: any }) => { 866 | inferred: PropertyWidget; 867 | expected: PropertyWidget; 868 | }; 869 | /** 870 | * Get all properties with an assigned widget type 871 | */ 872 | getTypes: () => string[]; 873 | /** 874 | * @internal Load property types from config 875 | */ 876 | loadData: () => Promise; 877 | /** 878 | * @internal 879 | */ 880 | on: (args: any) => void; 881 | /** 882 | * @internal Save property types to config 883 | */ 884 | save: () => Promise; 885 | /** 886 | * @internal Get all properties from metadata cache 887 | */ 888 | savePropertyInfo: () => void; 889 | /** 890 | * @internal Set widget type for property 891 | */ 892 | setType: (property: string, type: PropertyWidgetType) => void; 893 | /** 894 | * @internal 895 | */ 896 | trigger: (e: any) => void; 897 | /** 898 | * @internal Unset widget type for property 899 | */ 900 | unsetType: (property: string) => void; 901 | } 902 | // 903 | // interface MobileNavbar { 904 | // /** 905 | // * Reference to App 906 | // */ 907 | // app: App; 908 | // /** 909 | // * @internal Back button element 910 | // */ 911 | // backButtonEl: HTMLElement; 912 | // /** 913 | // * @internal Container element 914 | // */ 915 | // containerEl: HTMLElement; 916 | // /** 917 | // * @internal Forward button element 918 | // */ 919 | // forwardButtonEl: HTMLElement; 920 | // /** 921 | // * Whether the mobile navbar is currently visible 922 | // */ 923 | // isVisible: boolean; 924 | // /** 925 | // * @internal On ribbon click 926 | // */ 927 | // onRibbonClick: () => void; 928 | // /** 929 | // * @internal Ribbon menu flair element 930 | // */ 931 | // ribbonMenuFlairEl: HTMLElement; 932 | // /** 933 | // * @internal Ribbon menu item element 934 | // */ 935 | // ribbonMenuItemEl: HTMLElement; 936 | // /** 937 | // * @internal Tab button element 938 | // */ 939 | // tabButtonEl: HTMLElement; 940 | // 941 | // /** 942 | // * @internal Hide mobile navbar 943 | // */ 944 | // hide: () => void; 945 | // /** 946 | // * @internal Show mobile navbar 947 | // */ 948 | // show: () => void; 949 | // /** 950 | // * @internal Show ribbon menu 951 | // */ 952 | // showRibbonMenu: () => void; 953 | // /** 954 | // * @internal Update navigation buttons 955 | // */ 956 | // updateNavButtons: () => void; 957 | // /** 958 | // * @internal Update ribbon menu item 959 | // */ 960 | // updateRibbonMenuItem: () => void; 961 | // } 962 | // 963 | // interface MobileToolbar { 964 | // /** 965 | // * Reference to App 966 | // */ 967 | // app: App; 968 | // /** 969 | // * @internal Container element 970 | // */ 971 | // containerEl: HTMLElement; 972 | // /** 973 | // * @internal Last selected command ID 974 | // */ 975 | // lastCommandIds: string; 976 | // /** 977 | // * @internal Options container element 978 | // */ 979 | // optionsContainerEl: HTMLElement; 980 | // 981 | // /** 982 | // * @internal Compile all actions for the toolbar 983 | // */ 984 | // compileToolbar: () => void; 985 | // /** 986 | // * @internal Hide mobile toolbar 987 | // */ 988 | // hide: () => void; 989 | // /** 990 | // * @internal Show mobile toolbar 991 | // */ 992 | // show: () => void; 993 | // } 994 | 995 | interface PluginManifest { 996 | /** 997 | * Name of the author of the plugin 998 | */ 999 | author: string; 1000 | /** 1001 | * URL to the author's website 1002 | */ 1003 | authorUrl?: string; 1004 | /** 1005 | * Description of the plugin's functionality 1006 | */ 1007 | description: string; 1008 | /** 1009 | * Storage location of the plugin relative to the vault root 1010 | */ 1011 | dir: string; 1012 | /** 1013 | * URL for funding the author 1014 | */ 1015 | fundingUrl?: string; 1016 | /** 1017 | * Unique identifier of the plugin 1018 | */ 1019 | id: string; 1020 | /** 1021 | * Whether the plugin is designed for desktop use only 1022 | */ 1023 | isDesktopOnly: boolean; 1024 | /** 1025 | * Minimum Obsidian version compatible with the plugin 1026 | */ 1027 | minAppVersion: string; 1028 | /** 1029 | * Name of the plugin 1030 | */ 1031 | name: string; 1032 | /** 1033 | * Version of the plugin 1034 | */ 1035 | version: string; 1036 | } 1037 | 1038 | interface PluginUpdateManifest { 1039 | /** 1040 | * Manifest of the plugin 1041 | */ 1042 | manifest: PluginManifest; 1043 | /** 1044 | * Repository of the plugin 1045 | */ 1046 | repo: string; 1047 | /** 1048 | * New version of the plugin 1049 | */ 1050 | version: string; 1051 | } 1052 | 1053 | interface Plugins { 1054 | /** 1055 | * Reference to App 1056 | */ 1057 | app: App; 1058 | /** 1059 | * Set of enabled plugin IDs 1060 | */ 1061 | enabledPlugins: Set; 1062 | /** 1063 | * @internal Plugin ID that is currently being enabled 1064 | */ 1065 | loadingPluginId: string | null; 1066 | /** 1067 | * Manifests of all the plugins 1068 | */ 1069 | manifests: Record; 1070 | /** 1071 | * Mapping of plugin ID to plugin instance 1072 | */ 1073 | plugins: Record; 1074 | /** 1075 | * Mapping of plugin ID to available updates 1076 | */ 1077 | updates: Map; 1078 | 1079 | /** 1080 | * @internal Check online list for deprecated plugins to automatically disable 1081 | */ 1082 | checkForDeprecations: () => Promise; 1083 | /** 1084 | * Check for plugin updates 1085 | */ 1086 | checkForUpdates: () => Promise; 1087 | /** 1088 | * Unload a plugin by ID 1089 | */ 1090 | disablePlugin: (id: string) => Promise; 1091 | /** 1092 | * Unload a plugin by ID and save config for persistence 1093 | */ 1094 | disablePluginAndSave: (id: string) => Promise; 1095 | /** 1096 | * Enable a plugin by ID 1097 | */ 1098 | enablePlugin: (id: string) => Promise; 1099 | /** 1100 | * Enable a plugin by ID and save config for persistence 1101 | */ 1102 | enablePluginAndSave: (id: string) => Promise; 1103 | /** 1104 | * Get a plugin by ID 1105 | */ 1106 | getPlugin: (id: string) => Plugin | null; 1107 | /** 1108 | * Get the folder where plugins are stored 1109 | */ 1110 | getPluginFolder: () => string; 1111 | /** 1112 | * @internal Load plugin manifests and enable plugins from config 1113 | */ 1114 | initialize: () => Promise; 1115 | /** 1116 | * Install a plugin from a given URL 1117 | */ 1118 | installPlugin: ( 1119 | repo: string, 1120 | manifest: PluginManifest, 1121 | version: string 1122 | ) => Promise; 1123 | /** 1124 | * Check whether a plugin is deprecated 1125 | */ 1126 | isDeprecated: (id: string) => boolean; 1127 | /** 1128 | * Check whether community plugins are enabled 1129 | */ 1130 | isEnabled: () => boolean; 1131 | /** 1132 | * Load a specific plugin's manifest by its ID 1133 | */ 1134 | loadManifest: (id: string) => Promise; 1135 | /** 1136 | * @internal Load all plugin manifests from plugin folder 1137 | */ 1138 | loadManifests: () => Promise; 1139 | /** 1140 | *Load a plugin by its ID 1141 | */ 1142 | loadPlugin: (id: string) => Promise; 1143 | /** 1144 | * @internal 1145 | */ 1146 | onRaw: (e: any) => void; 1147 | /** 1148 | * @internal - Save current plugin configs 1149 | */ 1150 | saveConfig: () => Promise; 1151 | /** 1152 | * @internal Toggle whether community plugins are enabled 1153 | */ 1154 | setEnable: (enabled: boolean) => Promise; 1155 | /** 1156 | * Uninstall a plugin by ID 1157 | */ 1158 | uninstallPlugin: (id: string) => Promise; 1159 | /** 1160 | * Unload a plugin by ID 1161 | */ 1162 | unloadPlugin: (id: string) => Promise; 1163 | } 1164 | 1165 | interface WindowSelection { 1166 | focusEl: HTMLElement; 1167 | range: Range; 1168 | win: Window; 1169 | } 1170 | 1171 | type ConfigItem = 1172 | | "accentColor" 1173 | | "alwaysUpdateLinks" 1174 | | "attachmentFolderPath" 1175 | | "autoConvertHtml" 1176 | | "autoPairBrackets" 1177 | | "autoPairMarkdown" 1178 | | "baseFontSize" 1179 | | "baseFontSizeAction" 1180 | | "cssTheme" 1181 | | "defaultViewMode" 1182 | | "emacsyKeys" 1183 | | "enabledCssSnippets" 1184 | | "fileSortOrder" 1185 | | "focusNewTab" 1186 | | "foldHeading" 1187 | | "foldIndent" 1188 | | "hotkeys" 1189 | | "interfaceFontFamily" 1190 | | "legacyEditor" 1191 | | "livePreview" 1192 | | "mobilePullAction" 1193 | | "mobileQuickRibbonItem" 1194 | | "mobileToolbarCommands" 1195 | | "monospaceFontFamily" 1196 | | "nativeMenus" 1197 | | "newFileFolderPath" 1198 | | "newFileLocation" 1199 | | "newLinkFormat" 1200 | | "pdfExportSettings" 1201 | | "promptDelete" 1202 | | "propertiesInDocument" 1203 | | "readableLineLength" 1204 | | "rightToLeft" 1205 | | "showIndentGuide" 1206 | | "showInlineTitle" 1207 | | "showLineNumber" 1208 | | "showUnsupportedFiles" 1209 | | "showViewHeader" 1210 | | "smartIndentList" 1211 | | "spellcheck" 1212 | | "spellcheckLanguages" 1213 | | "strictLineBreaks" 1214 | | "tabSize" 1215 | | "textFontFamily" 1216 | | "theme" 1217 | | "translucency" 1218 | | "trashOption" 1219 | | "types" 1220 | | "useMarkdownLinks" 1221 | | "useTab" 1222 | | "userIgnoreFilters" 1223 | | "vimMode"; 1224 | 1225 | interface AppVaultConfig { 1226 | /** 1227 | * Appearance > Accent color 1228 | */ 1229 | accentColor: "" | string; 1230 | /** 1231 | * Files & Links > Automatically update internal links 1232 | */ 1233 | alwaysUpdateLinks?: false | boolean; 1234 | /** 1235 | * Files & Links > Attachment folder path 1236 | */ 1237 | attachmentFolderPath?: "/" | string; 1238 | /** 1239 | * Editor > Auto convert HTML 1240 | */ 1241 | autoConvertHtml?: true | boolean; 1242 | /** 1243 | * Editor > Auto pair brackets 1244 | */ 1245 | autoPairBrackets?: true | boolean; 1246 | /** 1247 | * Editor > Auto pair Markdown syntax 1248 | */ 1249 | autoPairMarkdown?: true | boolean; 1250 | /** 1251 | * Appearance > Font size 1252 | */ 1253 | baseFontSize?: 16 | number; 1254 | /** 1255 | * Appearance > Quick font size adjustment 1256 | */ 1257 | baseFontSizeAction?: true | boolean; 1258 | /** 1259 | * Community Plugins > Browse > Sort order 1260 | */ 1261 | communityPluginSortOrder: 1262 | | "download" 1263 | | "update" 1264 | | "release" 1265 | | "alphabetical"; 1266 | /** 1267 | * Themes > Browse > Sort order 1268 | */ 1269 | communityThemeSortOrder: "download" | "update" | "release" | "alphabetical"; 1270 | /** 1271 | * Appearance > Theme 1272 | * @remark "" is the default Obsidian theme 1273 | */ 1274 | cssTheme?: "" | string; 1275 | /** 1276 | * Editor > Default view for new tabs 1277 | */ 1278 | defaultViewMode?: "source" | "preview"; 1279 | /** 1280 | * 1281 | */ 1282 | emacsyKeys?: true | boolean; 1283 | /** 1284 | * Appearance > CSS snippets 1285 | */ 1286 | enabledCssSnippets?: string[]; 1287 | /** 1288 | * 1289 | */ 1290 | fileSortOrder?: "alphabetical"; 1291 | /** 1292 | * Editor > Always focus new tabs 1293 | */ 1294 | focusNewTab?: true | boolean; 1295 | /** 1296 | * Editor > Fold heading 1297 | */ 1298 | foldHeading?: true | boolean; 1299 | /** 1300 | * Editor > Fold indent 1301 | */ 1302 | foldIndent?: true | boolean; 1303 | /** 1304 | * Hotkeys 1305 | * @deprecated Likely not used anymore 1306 | */ 1307 | hotkeys?: Record; 1308 | /** 1309 | * Appearance > Interface font 1310 | */ 1311 | interfaceFontFamily?: "" | string; 1312 | /** 1313 | * Editor > Use legacy editor 1314 | */ 1315 | legacyEditor?: false | boolean; 1316 | /** 1317 | * 1318 | */ 1319 | livePreview?: true | boolean; 1320 | /** 1321 | * Mobile > Configure mobile Quick Action 1322 | */ 1323 | mobilePullAction?: "command-palette:open" | string; 1324 | /** 1325 | * 1326 | */ 1327 | mobileQuickRibbonItem?: "" | string; 1328 | /** 1329 | * Mobile > Manage toolbar options 1330 | */ 1331 | mobileToolbarCommands?: string[]; 1332 | /** 1333 | * 1334 | */ 1335 | monospaceFontFamily?: "" | string; 1336 | /** 1337 | * Appearance > Native menus 1338 | */ 1339 | nativeMenus?: null | boolean; 1340 | /** 1341 | * Files & Links > Default location for new notes | 'folder' > Folder to create new notes in 1342 | */ 1343 | newFileFolderPath?: "/" | string; 1344 | /** 1345 | * Files & Links > Default location for new notes 1346 | */ 1347 | newFileLocation?: "root" | "current" | "folder"; 1348 | /** 1349 | * Files & Links > New link format 1350 | */ 1351 | newLinkFormat?: "shortest" | "relative" | "absolute"; 1352 | /** 1353 | * Saved on executing 'Export to PDF' command 1354 | */ 1355 | pdfExportSettings?: { 1356 | pageSize: "letter" | string; 1357 | landscape: false | boolean; 1358 | margin: "0" | string; 1359 | downscalePercent: 100 | number; 1360 | }; 1361 | /** 1362 | * Files & Links > Confirm line deletion 1363 | */ 1364 | promptDelete?: true | boolean; 1365 | /** 1366 | * Editor > Properties in document 1367 | */ 1368 | propertiesInDocument?: "visible" | "hidden" | "source"; 1369 | /** 1370 | * Editor > Readable line length 1371 | */ 1372 | readableLineLength?: true | boolean; 1373 | /** 1374 | * Editor > Right-to-left (RTL) 1375 | */ 1376 | rightToLeft?: false | boolean; 1377 | /** 1378 | * @deprecated Removed as of version 1.4.3 1379 | */ 1380 | showFrontmatter?: false | boolean; 1381 | /** 1382 | * Editor > Show indentation guides 1383 | */ 1384 | showIndentGuide?: true | boolean; 1385 | /** 1386 | * Editor > Show inline title 1387 | */ 1388 | showInlineTitle?: true | boolean; 1389 | /** 1390 | * Editor > Show line numbers 1391 | */ 1392 | showLineNumber?: false | boolean; 1393 | /** 1394 | * Files & Links > Detect all file extensions 1395 | */ 1396 | showUnsupportedFiles?: false | boolean; 1397 | /** 1398 | * Appearance > Show tab title bar 1399 | */ 1400 | showViewHeader?: false | boolean; 1401 | /** 1402 | * Editor > Smart indent lists 1403 | */ 1404 | smartIndentList?: true | boolean; 1405 | /** 1406 | * Editor > Spellcheck 1407 | */ 1408 | spellcheck?: false | boolean; 1409 | /** 1410 | * @deprecated 1411 | */ 1412 | spellcheckDictionary?: [] | string[]; 1413 | /** 1414 | * Editor > Spellcheck languages 1415 | */ 1416 | spellcheckLanguages?: null | string[]; 1417 | /** 1418 | * Editor > Strict line breaks 1419 | */ 1420 | strictLineBreaks?: false | boolean; 1421 | /** 1422 | * Editor > Tab indent size 1423 | */ 1424 | tabSize?: 4 | number; 1425 | /** 1426 | * Appearance > Text font 1427 | */ 1428 | textFontFamily?: "" | string; 1429 | /** 1430 | * Appearance > Base color scheme 1431 | * @remark Not be confused with cssTheme, this setting is for the light/dark mode 1432 | * @remark "moonstone" is light theme, "obsidian" is dark theme 1433 | */ 1434 | theme?: "moonstone" | "obsidian"; 1435 | /** 1436 | * Appearance > Translucent window 1437 | */ 1438 | translucency?: false | boolean; 1439 | /** 1440 | * Files & Links > Deleted files 1441 | */ 1442 | trashOption?: "system" | "local" | "none"; 1443 | /** 1444 | * @deprecated Probably left-over code from old properties type storage 1445 | */ 1446 | types: object; 1447 | /** 1448 | * Files & Links > Use [[Wikilinks]] 1449 | */ 1450 | useMarkdownLinks?: false | boolean; 1451 | /** 1452 | * Editor > Indent using tabs 1453 | */ 1454 | useTab?: true | boolean; 1455 | /** 1456 | * Files & Links > Excluded files 1457 | */ 1458 | userIgnoreFilters?: null | string[]; 1459 | /** 1460 | * Editor > Vim key bindings 1461 | */ 1462 | vimMode?: false | boolean; 1463 | } 1464 | 1465 | interface FileEntry { 1466 | /** 1467 | * Creation time (if file) 1468 | */ 1469 | ctime?: number; 1470 | /** 1471 | * Modification time (if file) 1472 | */ 1473 | mtime?: number; 1474 | /** 1475 | * Full path to file or folder 1476 | * @remark Might be used for resolving symlinks 1477 | */ 1478 | realpath: string; 1479 | /** 1480 | * Size in bytes (if file) 1481 | */ 1482 | size?: number; 1483 | /** 1484 | * Type of entry 1485 | */ 1486 | type: "file" | "folder"; 1487 | } 1488 | 1489 | interface ViewRegistry extends Events { 1490 | /** 1491 | * Mapping of file extensions to view type 1492 | */ 1493 | typeByExtension: Record; 1494 | /** 1495 | * Mapping of view type to view constructor 1496 | */ 1497 | viewByType: Record View>; 1498 | 1499 | /** 1500 | * Get the view type associated with a file extension 1501 | * @param extension File extension 1502 | */ 1503 | getTypeByExtension: (extension: string) => string; 1504 | /** 1505 | * Get the view constructor associated with a view type 1506 | */ 1507 | getViewCreatorByType: (type: string) => (leaf: WorkspaceLeaf) => View; 1508 | /** 1509 | * Check whether a view type is registered 1510 | */ 1511 | isExtensionRegistered: (extension: string) => boolean; 1512 | /** 1513 | * @internal 1514 | */ 1515 | on: (args: any[]) => EventRef; 1516 | /** 1517 | * Register a view type for a file extension 1518 | * @param extension File extension 1519 | * @param type View type 1520 | * @remark Prefer registering the extension via the Plugin class 1521 | */ 1522 | registerExtensions: (extension: string[], type: string) => void; 1523 | /** 1524 | * Register a view constructor for a view type 1525 | */ 1526 | registerView: ( 1527 | type: string, 1528 | viewCreator: (leaf: WorkspaceLeaf) => View 1529 | ) => void; 1530 | /** 1531 | * Register a view and its associated file extensions 1532 | */ 1533 | registerViewWithExtensions: ( 1534 | extensions: string[], 1535 | type: string, 1536 | viewCreator: (leaf: WorkspaceLeaf) => View 1537 | ) => void; 1538 | /** 1539 | * @internal 1540 | */ 1541 | trigger: (type: string) => void; 1542 | /** 1543 | * Unregister extensions for a view type 1544 | */ 1545 | unregisterExtensions: (extension: string[]) => void; 1546 | /** 1547 | * Unregister a view type 1548 | */ 1549 | unregisterView: (type: string) => void; 1550 | } 1551 | 1552 | interface HoverLinkSource { 1553 | display: string; 1554 | defaultMod: boolean; 1555 | } 1556 | 1557 | interface RecentFileTracker { 1558 | /** 1559 | * List of last opened file paths, limited to 50 1560 | */ 1561 | lastOpenFiles: string[]; 1562 | /** 1563 | * Reference to Vault 1564 | */ 1565 | vault: EVault; 1566 | /** 1567 | * Reference to Workspace 1568 | */ 1569 | workspace: EWorkspace; 1570 | 1571 | /** 1572 | * @internal 1573 | */ 1574 | collect: (file: TFile) => void; 1575 | /** 1576 | * Returns the last 10 opened files 1577 | */ 1578 | getLastOpenFiles: () => string[]; 1579 | /** 1580 | * Get last n files of type (defaults to 10) 1581 | */ 1582 | getRecentFiles: ({ 1583 | showMarkdown: boolean, 1584 | showCanvas: boolean, 1585 | showNonImageAttachments: boolean, 1586 | showImages: boolean, 1587 | maxCount: number, 1588 | }?) => string[]; 1589 | /** 1590 | * Set the last opened files 1591 | */ 1592 | load: (savedFiles: string[]) => void; 1593 | /** 1594 | * @internal On file create, save file to last opened files 1595 | */ 1596 | onFileCreated: (file: TFile) => void; 1597 | /** 1598 | * @internal On file open, save file to last opened files 1599 | */ 1600 | onFileOpen: (prevFile: TFile, file: TFile) => void; 1601 | /** 1602 | * @internal On file rename, update file path in last opened files 1603 | */ 1604 | onRename: (file: TFile, oldPath: string) => void; 1605 | /** 1606 | * @internal Get last opened files 1607 | */ 1608 | serialize: () => string[]; 1609 | } 1610 | 1611 | interface StateHistory { 1612 | /** 1613 | * Ephemeral cursor state within Editor of leaf 1614 | */ 1615 | eState: { 1616 | cursor: EditorRange; 1617 | scroll: number; 1618 | }; 1619 | /** 1620 | * Icon of the leaf 1621 | */ 1622 | icon?: string; 1623 | /** 1624 | * History of previous and future states of leaf 1625 | */ 1626 | leafHistory?: { 1627 | backHistory: StateHistory[]; 1628 | forwardHistory: StateHistory[]; 1629 | }; 1630 | /** 1631 | * Id of parent to which the leaf belonged 1632 | */ 1633 | parentId?: string; 1634 | /** 1635 | * Id of root to which the leaf belonged 1636 | */ 1637 | rootId?: string; 1638 | /** 1639 | * Last state of the leaf 1640 | */ 1641 | state: ViewState; 1642 | /** 1643 | * Title of the leaf 1644 | */ 1645 | title?: string; 1646 | } 1647 | 1648 | interface LeafEntry { 1649 | children?: LeafEntry[]; 1650 | direction?: SplitDirection; 1651 | id: string; 1652 | state?: ViewState; 1653 | type: string; 1654 | width?: number; 1655 | } 1656 | 1657 | interface SerializedWorkspace { 1658 | /** 1659 | * Last active leaf 1660 | */ 1661 | active: string; 1662 | /** 1663 | * Last opened files 1664 | */ 1665 | lastOpenFiles: string[]; 1666 | /** 1667 | * Left opened leaf 1668 | */ 1669 | left: LeafEntry; 1670 | /** 1671 | * Left ribbon 1672 | */ 1673 | leftRibbon: { hiddenItems: Record }; 1674 | /** 1675 | * Main (center) workspace leaf 1676 | */ 1677 | main: LeafEntry; 1678 | /** 1679 | * Right opened leaf 1680 | */ 1681 | right: LeafEntry; 1682 | } 1683 | 1684 | interface ImportedAttachments { 1685 | data: Promise; 1686 | extension: string; 1687 | filename: string; 1688 | name: string; 1689 | } 1690 | 1691 | declare module "obsidian" { 1692 | interface App { 1693 | /** 1694 | * The account signed in to Obsidian 1695 | */ 1696 | account: Account; 1697 | /** 1698 | * ID that uniquely identifies the vault 1699 | * @tutorial Used for implementing device *and* vault-specific 1700 | * data storage using LocalStorage or IndexedDB 1701 | */ 1702 | appId: string; 1703 | // /** 1704 | // * @internal 1705 | // */ 1706 | // appMenuBarManager: AppMenuBarManager; 1707 | /** 1708 | * Contains all registered commands 1709 | * @tutorial Can be used to manually invoke the functionality of a specific command 1710 | */ 1711 | commands: Commands; 1712 | /** 1713 | * Custom CSS (snippets/themes) applied to the application 1714 | * @tutorial Can be used to view which snippets are enabled or available, 1715 | * or inspect loaded-in theme manifests 1716 | */ 1717 | customCss: CustomCSS; 1718 | /** References to important DOM elements of the application */ 1719 | dom: ObsidianDOM; 1720 | // /** 1721 | // * @internal 1722 | // */ 1723 | // dragManager: any; 1724 | // /** 1725 | // * @internal 1726 | // */ 1727 | // embedRegistry: EmbedRegistry; 1728 | /** 1729 | * Manage the creation, deletion and renaming of files from the UI. 1730 | * @remark Prefer using the `vault` API for programmatic file management 1731 | */ 1732 | fileManager: EFileManager; 1733 | // /** 1734 | // * @internal 1735 | // */ 1736 | // foldManager: any; 1737 | /** 1738 | * Manages global hotkeys 1739 | * @tutorial Can be used for manually invoking a command, or finding which hotkey is assigned to a specific key input 1740 | * @remark This should not be used for adding hotkeys to your custom commands, this can easily be done via the official API 1741 | */ 1742 | hotkeyManager: HotkeyManager; 1743 | /** 1744 | * Manager of internal 'core' plugins 1745 | * @tutorial Can be used to check whether a specific internal plugin is enabled, or grab specific parts 1746 | * from its config for simplifying your own plugin's settings 1747 | */ 1748 | internalPlugins: InternalPlugins; 1749 | /** 1750 | * Whether the application is currently running on mobile 1751 | * @remark Prefer usage of `Platform.isMobile` 1752 | * @remark Will be true if `app.emulateMobile()` was enabled 1753 | */ 1754 | isMobile: boolean; 1755 | // /** 1756 | // * @internal 1757 | // */ 1758 | // keymap: KeymapManager; 1759 | // /** 1760 | // * @internal 1761 | // */ 1762 | // loadProgress: LoadProgress; 1763 | /** 1764 | * Manages the gathering and updating of metadata for all files in the vault 1765 | * @tutorial Use for finding tags and backlinks for specific files, grabbing frontmatter properties, ... 1766 | */ 1767 | metadataCache: EMetadataCache; 1768 | /** 1769 | * Manages the frontmatter properties of the vault and the rendering of the properties 1770 | * @tutorial Fetching properties used in all frontmatter fields, may potentially be used for adding custom frontmatter widgets 1771 | */ 1772 | metadataTypeManager: MetadataTypeManager; 1773 | 1774 | // /** 1775 | // * @internal 1776 | // */ 1777 | // mobileNavbar?: MobileNavbar; 1778 | // /** 1779 | // * @internal 1780 | // */ 1781 | // mobileToolbar?: MobileToolbar; 1782 | 1783 | // /** 1784 | // * @internal Events to execute on the next frame 1785 | // */ 1786 | // nextFrameEvents: any[]; 1787 | // /** 1788 | // * @internal Timer for the next frame 1789 | // */ 1790 | // nextFrameTimer: number; 1791 | 1792 | /** 1793 | * Manages loading and enabling of community (non-core) plugins 1794 | * @tutorial Can be used to communicate with other plugins, custom plugin management, ... 1795 | * @remark Be careful when overriding loading logic, as this may result in other plugins not loading 1796 | */ 1797 | plugins: Plugins; 1798 | /** 1799 | * @internal Root keyscope of the application 1800 | */ 1801 | scope: EScope; 1802 | /** 1803 | * Manages the settings modal and its tabs 1804 | * @tutorial Can be used to open the settings modal to a specific tab, extend the settings modal functionality, ... 1805 | * @remark Do not use this to get settings values from other plugins, it is better to do this via `app.plugins.getPlugin(ID)?.settings` (check how the plugin stores its settings) 1806 | */ 1807 | setting: Setting; 1808 | // /** 1809 | // * @internal 1810 | // */ 1811 | // shareReceiver: { app: App } 1812 | // /** 1813 | // * @internal Status bar of the application 1814 | // */ 1815 | // statusBar: { app: App , containerEl: HTMLElement } 1816 | /** 1817 | * Name of the vault with version suffix 1818 | * @remark Formatted as 'NAME - Obsidian vX.Y.Z' 1819 | */ 1820 | title: string; 1821 | /** 1822 | * Manages all file operations for the vault, contains hooks for file changes, and an adapter 1823 | * for low-level file system operations 1824 | * @tutorial Used for creating your own files and folders, renaming, ... 1825 | * @tutorial Use `app.vault.adapter` for accessing files outside the vault (desktop-only) 1826 | * @remark Prefer using the regular `vault` whenever possible 1827 | */ 1828 | vault: Vault; 1829 | /** 1830 | * Manages the construction of appropriate views when opening a file of a certain type 1831 | * @remark Prefer usage of view registration via the Plugin class 1832 | */ 1833 | viewRegistry: ViewRegistry; 1834 | /** 1835 | * Manages the workspace layout, construction, rendering and manipulation of leaves 1836 | * @tutorial Used for accessing the active editor leaf, grabbing references to your views, ... 1837 | */ 1838 | workspace: Workspace; 1839 | 1840 | /** 1841 | * Sets the accent color of the application to the OS preference 1842 | */ 1843 | adaptToSystemTheme: () => void; 1844 | /** 1845 | * Sets the accent color of the application (light/dark mode) 1846 | */ 1847 | changeTheme: (theme: "moonstone" | "obsidian") => void; 1848 | /** 1849 | * Copies Obsidian URI of given file to clipboard 1850 | * @param file File to generate URI for 1851 | */ 1852 | copyObsidianUrl: (file: TFile) => void; 1853 | /** 1854 | * Disables all CSS transitions in the vault (until manually re-enabled) 1855 | */ 1856 | disableCssTransition: () => void; 1857 | /** 1858 | * Restarts Obsidian and renders workspace in mobile mode 1859 | * @tutorial Very useful for testing the rendering of your plugin on mobile devices 1860 | */ 1861 | emulateMobile: (emulate: boolean) => void; 1862 | /** 1863 | * Enables all CSS transitions in the vault 1864 | */ 1865 | enableCssTransition: () => void; 1866 | /** 1867 | * Manually fix all file links pointing towards image/audio/video resources in element 1868 | * @param element Element to fix links in 1869 | */ 1870 | fixFileLinks: (element: HTMLElement) => void; 1871 | /** 1872 | * Applies an obfuscation font to all text characters in the vault 1873 | * @tutorial Useful for hiding sensitive information or sharing pretty screenshots of your vault 1874 | * @remark Uses the `Flow Circular` font 1875 | * @remark You will have to restart the app to get normal text back 1876 | */ 1877 | garbleText: () => void; 1878 | /** 1879 | * Get the accent color of the application 1880 | * @remark Often a better alternative than `app.vault.getConfig('accentColor')` as it returns an empty string if no accent color was set 1881 | */ 1882 | getAccentColor: () => string; 1883 | /** 1884 | * Get the current title of the application 1885 | * @remark The title is based on the currently active leaf 1886 | */ 1887 | getAppTitle: () => string; 1888 | /** 1889 | * Get the URI for opening specified file in Obsidian 1890 | */ 1891 | getObsidianUrl: (file: TFile) => string; 1892 | /** 1893 | * Get currently active spellcheck languages 1894 | * @deprecated Originally spellcheck languages were stored in app settings, 1895 | * languages are now stored in `localStorage.getItem(spellcheck-languages)` 1896 | */ 1897 | getSpellcheckLanguages: () => string[]; 1898 | /** 1899 | * Get the current color scheme of the application 1900 | * @remark Identical to `app.vault.getConfig('theme')` 1901 | */ 1902 | getTheme: () => "moonstone" | "obsidian"; 1903 | /** 1904 | * Import attachments into specified folder 1905 | */ 1906 | importAttachments: ( 1907 | imports: ImportedAttachments[], 1908 | folder: TFolder 1909 | ) => Promise; 1910 | /** 1911 | * @internal Initialize the entire application using the provided FS adapter 1912 | */ 1913 | initializeWithAdapter: (adapter: EDataAdapter) => Promise; 1914 | /** 1915 | * Load a value from the localstorage given key 1916 | * @param key Key of value to load 1917 | * @remark This method is device *and* vault specific 1918 | * @tutorial Use load/saveLocalStorage for saving configuration data that needs to be unique to the current vault 1919 | */ 1920 | loadLocalStorage: (key: string) => string; 1921 | /** 1922 | * @internal Add callback to execute on next frame 1923 | */ 1924 | nextFrame: (callback: () => void) => void; 1925 | /** 1926 | * @internal Add callback to execute on next frame, and remove after execution 1927 | */ 1928 | nextFrameOnceCallback: (callback: () => void) => void; 1929 | /** 1930 | * @internal Add callback to execute on next frame with promise 1931 | */ 1932 | nextFramePromise: (callback: () => Promise) => Promise; 1933 | /** 1934 | * @internal 1935 | */ 1936 | on: (args: any[]) => EventRef; 1937 | /** 1938 | * @internal 1939 | */ 1940 | onMouseEvent: (evt: MouseEvent) => void; 1941 | /** 1942 | * @internal Execute all logged callback (called when next frame is loaded) 1943 | */ 1944 | onNextFrame: (callback: () => void) => void; 1945 | /** 1946 | * Open the help vault (or site if mobile) 1947 | */ 1948 | openHelp: () => void; 1949 | /** 1950 | * Open the vault picker 1951 | */ 1952 | openVaultChooser: () => void; 1953 | /** 1954 | * Open the file with OS defined default file browser application 1955 | */ 1956 | openWithDefaultApp: (path: string) => void; 1957 | /** 1958 | * @internal Register all basic application commands 1959 | */ 1960 | registerCommands: () => void; 1961 | /** 1962 | * @internal Register a hook for saving workspace data before unload 1963 | */ 1964 | registerQuitHook: () => void; 1965 | /** 1966 | * @internal Save attachment at default attachments location 1967 | */ 1968 | saveAttachment: ( 1969 | path: string, 1970 | extension: string, 1971 | data: ArrayBuffer 1972 | ) => Promise; 1973 | /** 1974 | * Save a value to the localstorage given key 1975 | * @param key Key of value to save 1976 | * @param value Value to save 1977 | * @remark This method is device *and* vault specific 1978 | * @tutorial Use load/saveLocalStorage for saving configuration data that needs to be unique to the current vault 1979 | */ 1980 | saveLocalStorage: (key: string, value: any) => void; 1981 | /** 1982 | * Set the accent color of the application 1983 | * @remark Also updates the CSS `--accent` variables 1984 | */ 1985 | setAccentColor: (color: string) => void; 1986 | /** 1987 | * Set the path where attachments should be stored 1988 | */ 1989 | setAttachmentFolder: (path: string) => void; 1990 | /** 1991 | * Set the spellcheck languages 1992 | */ 1993 | setSpellcheckLanguages: (languages: string[]) => void; 1994 | /** 1995 | * Set the current color scheme of the application and reload the CSS 1996 | */ 1997 | setTheme: (theme: "moonstone" | "obsidian") => void; 1998 | /** 1999 | * Open the OS file picker at path location 2000 | */ 2001 | showInFolder: (path: string) => void; 2002 | /** 2003 | * Show the release notes for provided version as a new leaf 2004 | * @param version Version to show release notes for (defaults to current version) 2005 | */ 2006 | showReleaseNotes: (version?: string) => void; 2007 | /** 2008 | * Updates the accent color and reloads the CSS 2009 | */ 2010 | updateAccentColor: () => void; 2011 | /** 2012 | * Update the font family of the application and reloads the CSS 2013 | */ 2014 | updateFontFamily: () => void; 2015 | /** 2016 | * Update the font size of the application and reloads the CSS 2017 | */ 2018 | updateFontSize: () => void; 2019 | /** 2020 | * Update the inline title rendering in notes 2021 | */ 2022 | updateInlineTitleDisplay: () => void; 2023 | /** 2024 | * Update the color scheme of the application and reloads the CSS 2025 | */ 2026 | updateTheme: () => void; 2027 | /** 2028 | * Update the view header display in notes 2029 | */ 2030 | updateViewHeaderDisplay: () => void; 2031 | } 2032 | 2033 | interface Scope { 2034 | /** 2035 | * Overridden keys that exist in this scope 2036 | */ 2037 | keys: KeyScope[]; 2038 | 2039 | /** 2040 | * @internal Scope that this scope is a child of 2041 | */ 2042 | parent: EScope | undefined; 2043 | /** 2044 | * @internal - Callback to execute when scope is matched 2045 | */ 2046 | cb: (() => boolean) | undefined; 2047 | /** 2048 | * @internal 2049 | */ 2050 | tabFocusContainer: HTMLElement | null; 2051 | /** 2052 | * @internal Execute keypress within this scope 2053 | * @param event - Keyboard event 2054 | * @param keypress - Pressed key information 2055 | */ 2056 | handleKey: (event: KeyboardEvent, keypress: KeymapInfo) => any; 2057 | /** 2058 | * @internal 2059 | * @deprecated - Executes same functionality as `Scope.register` 2060 | */ 2061 | registerKey: ( 2062 | modifiers: Modifier[], 2063 | key: string | null, 2064 | func: KeymapEventListener 2065 | ) => KeymapEventHandler; 2066 | /** 2067 | * @internal 2068 | */ 2069 | setTabFocusContainer: (container: HTMLElement) => void; 2070 | } 2071 | 2072 | class MetadataCache { 2073 | /** 2074 | * Reference to App 2075 | */ 2076 | app: App; 2077 | /** 2078 | * @internal 2079 | */ 2080 | blockCache: BlockCache; 2081 | /** 2082 | * @internal IndexedDB database 2083 | */ 2084 | db: IDBDatabase; 2085 | /** 2086 | * @internal File contents cache 2087 | */ 2088 | fileCache: Record; 2089 | /** 2090 | * @internal Amount of tasks currently in progress 2091 | */ 2092 | inProgressTaskCount: number; 2093 | /** 2094 | * @internal Whether the cache is fully loaded 2095 | */ 2096 | initialized: boolean; 2097 | /** 2098 | * @internal 2099 | */ 2100 | linkResolverQueue: any; 2101 | /** 2102 | * @internal File hash to metadata cache entry mapping 2103 | */ 2104 | metadataCache: Record; 2105 | /** 2106 | * @internal Callbacks to execute on cache clean 2107 | */ 2108 | onCleanCacheCallbacks: any[]; 2109 | /** 2110 | * @internal Mapping of filename to collection of files that share the same name 2111 | */ 2112 | uniqueFileLookup: CustomArrayDict; 2113 | /** 2114 | * @internal 2115 | */ 2116 | userIgnoreFilterCache: any; 2117 | /** 2118 | * @internal 2119 | */ 2120 | userIgnoreFilters: any; 2121 | /** 2122 | * @internal 2123 | */ 2124 | userIgnoreFiltersString: string; 2125 | /** 2126 | * Reference to Vault 2127 | */ 2128 | vault: Vault; 2129 | /** 2130 | * @internal 2131 | */ 2132 | workQueue: any; 2133 | /** 2134 | * @internal 2135 | */ 2136 | worker: Worker; 2137 | /** 2138 | * @internal 2139 | */ 2140 | workerResolve: any; 2141 | 2142 | /** 2143 | * Get all property infos of the vault 2144 | */ 2145 | getAllPropertyInfos: () => Record; 2146 | /** 2147 | * Get all backlink information for a file 2148 | */ 2149 | getBacklinksForFile: ( 2150 | file?: TFile 2151 | ) => CustomArrayDict; 2152 | /** 2153 | * Get paths of all files cached in the vault 2154 | */ 2155 | getCachedFiles: () => string[]; 2156 | /** 2157 | * Get an entry from the file cache 2158 | */ 2159 | getFileInfo: (path: string) => FileCacheEntry; 2160 | /** 2161 | * Get property values for frontmatter property key 2162 | */ 2163 | getFrontmatterPropertyValuesForKey: (key: string) => string[]; 2164 | /** 2165 | * Get all links (resolved or unresolved) in the vault 2166 | */ 2167 | getLinkSuggestions: () => { file: TFile | null; path: string }[]; 2168 | /** 2169 | * Get destination of link path 2170 | */ 2171 | getLinkpathDest: (origin: string = "", path: string) => TFile[]; 2172 | /** 2173 | * Get all links within the vault per file 2174 | */ 2175 | getLinks: () => Record; 2176 | /** 2177 | * Get all tags within the vault and their usage count 2178 | */ 2179 | getTags: () => Record; 2180 | 2181 | /** 2182 | * @internal Clear all caches to null values 2183 | */ 2184 | cleanupDeletedCache: () => void; 2185 | /** 2186 | * @internal 2187 | */ 2188 | clear: () => any; 2189 | /** 2190 | * @internal 2191 | */ 2192 | computeMetadataAsync: (e: any) => Promise; 2193 | /** 2194 | * @internal Remove all entries that contain deleted path 2195 | */ 2196 | deletePath: (path: string) => void; 2197 | /** 2198 | * @internal Initialize Database connection and load up caches 2199 | */ 2200 | initialize: () => Promise; 2201 | /** 2202 | * @internal Check whether there are no cache tasks in progress 2203 | */ 2204 | isCacheClean: () => boolean; 2205 | /** 2206 | * @internal Check whether file can support metadata (by checking extension support) 2207 | */ 2208 | isSupportedFile: (file: TFile) => boolean; 2209 | /** 2210 | * @internal Check whether string is part of the user ignore filters 2211 | */ 2212 | isUserIgnored: (filter: any) => boolean; 2213 | /** 2214 | * Iterate over all link references in the vault with callback 2215 | */ 2216 | iterateReferences: (callback: (path: string) => void) => void; 2217 | /** 2218 | * @internal 2219 | */ 2220 | linkResolver: () => void; 2221 | /** 2222 | * @internal Execute onCleanCache callbacks if cache is clean 2223 | */ 2224 | onCleanCache: () => void; 2225 | /** 2226 | * @internal On creation of the cache: update metadata cache 2227 | */ 2228 | onCreate: (file: TFile) => void; 2229 | /** 2230 | * @internal On creation or modification of the cache: update metadata cache 2231 | */ 2232 | onCreateOrModify: (file: TFile) => void; 2233 | /** 2234 | * @internal On deletion of the cache: update metadata cache 2235 | */ 2236 | onDelete: (file: TFile) => void; 2237 | /** 2238 | * @internal 2239 | */ 2240 | onReceiveMessageFromWorker: (e: any) => void; 2241 | /** 2242 | * @internal On rename of the cache: update metadata cache 2243 | */ 2244 | onRename: (file: TFile, oldPath: string) => void; 2245 | /** 2246 | * @internal Check editor for unresolved links and mark these as unresolved 2247 | */ 2248 | resolveLinks: (editor: Element) => void; 2249 | /** 2250 | * @internal Update file cache entry and sync to indexedDB 2251 | */ 2252 | saveFileCache: (path: string, entry: FileCacheEntry) => void; 2253 | /** 2254 | * @internal Update metadata cache entry and sync to indexedDB 2255 | */ 2256 | saveMetaCache: (path: string, entry: CachedMetadata) => void; 2257 | /** 2258 | * @internal Show a notice that the cache is being rebuilt 2259 | */ 2260 | showIndexingNotice: () => void; 2261 | /** 2262 | * @internal 2263 | */ 2264 | trigger: (e: any) => void; 2265 | /** 2266 | * @internal Re-resolve all links for changed path 2267 | */ 2268 | updateRelatedLinks: (path: string) => void; 2269 | /** 2270 | * @internal Update user ignore filters from settings 2271 | */ 2272 | updateUserIgnoreFilters: () => void; 2273 | /** 2274 | * @internal Bind actions to listeners on vault 2275 | */ 2276 | watchVaultChanges: () => void; 2277 | /** 2278 | * @internal Send message to worker to update metadata cache 2279 | */ 2280 | work: (cacheEntry: any) => void; 2281 | } 2282 | 2283 | interface SettingTab { 2284 | /** 2285 | * Unique ID of the tab 2286 | */ 2287 | id: string; 2288 | /** 2289 | * Sidebar name of the tab 2290 | */ 2291 | name: string; 2292 | /** 2293 | * Sidebar navigation element of the tab 2294 | */ 2295 | navEl: HTMLElement; 2296 | /** 2297 | * Reference to the settings modal 2298 | */ 2299 | setting: Setting; 2300 | /** 2301 | * Reference to the plugin that initialised the tab 2302 | * @if Tab is a plugin tab 2303 | */ 2304 | plugin?: Plugin; 2305 | /** 2306 | * Reference to installed plugins element 2307 | * @if Tab is the community plugins tab 2308 | */ 2309 | installedPluginsEl?: HTMLElement; 2310 | 2311 | // TODO: Editor, Files & Links, Appearance and About all have private properties too 2312 | } 2313 | 2314 | interface FileManager { 2315 | /** 2316 | * Reference to App 2317 | */ 2318 | app: App; 2319 | /** 2320 | * Creates a new Markdown file in specified location and opens it in a new view 2321 | * @param path - Path to the file to create (missing folders will be created) 2322 | * @param manner - Where to open the view containing the new file 2323 | */ 2324 | createAndOpenMarkdownFile: ( 2325 | path: string, 2326 | location: PaneType 2327 | ) => Promise; 2328 | /** 2329 | * Create a new file in the vault at specified location 2330 | * @param location - Location to create the file in, defaults to root 2331 | * @param filename - Name of the file to create, defaults to "Untitled" (incremented if file already exists) 2332 | * @param extension - Extension of the file to create, defaults to "md" 2333 | * @param contents - Contents of the file to create, defaults to empty string 2334 | */ 2335 | createNewFile: ( 2336 | location: TFolder = null, 2337 | filename: string = null, 2338 | extension: string = "md", 2339 | contents: string = "" 2340 | ) => Promise; 2341 | /** 2342 | * Creates a new untitled folder in the vault at specified location 2343 | * @param location - Location to create the folder in, defaults to root 2344 | */ 2345 | createNewFolder: (location: TFolder = null) => Promise; 2346 | /** 2347 | * Creates a new Markdown file in the vault at specified location 2348 | */ 2349 | createNewMarkdownFile: ( 2350 | location: TFolder = null, 2351 | filename: string = null, 2352 | contents: string = "" 2353 | ) => Promise; 2354 | /** 2355 | * Creates a new Markdown file based on linktext and path 2356 | * @param filename - Name of the file to create 2357 | * @param path - Path to where the file should be created 2358 | */ 2359 | createNewMarkdownFileFromLinktext: ( 2360 | filename: string, 2361 | path: string 2362 | ) => Promise; 2363 | /** 2364 | * @internal 2365 | */ 2366 | getAllLinkResolutions: () => []; 2367 | /** 2368 | * Gets the folder that new markdown files should be saved to, based on the current settings 2369 | * @param path - The path of the current opened/focused file, used when the user 2370 | * wants new files to be created in the same folder as the current file 2371 | */ 2372 | getMarkdownNewFileParent: (path: string) => TFolder; 2373 | /** 2374 | * Insert text into a file 2375 | * @param file - File to insert text into 2376 | * @param primary_text - Text to insert (will not be inserted if secondary_text exists) 2377 | * @param basename - ??? 2378 | * @param secondary_text - Text to insert (always inserted) 2379 | * @param atStart - Whether to insert text at the start or end of the file 2380 | */ 2381 | insertTextIntoFile: ( 2382 | file: TFile, 2383 | primary_text: string, 2384 | basename: string, 2385 | secondary_text: string, 2386 | atStart: boolean = true 2387 | ) => Promise; 2388 | /** 2389 | * Iterate over all links in the vault with callback 2390 | * @param callback - Callback to execute for each link 2391 | */ 2392 | iterateAllRefs: ( 2393 | callback: (path: string, link: PositionedReference) => void 2394 | ) => void; 2395 | /** 2396 | * Merge two files 2397 | * @param file - File to merge to 2398 | * @param otherFile - File to merge from 2399 | * @param override - If not empty, will override the contents of the file with this string 2400 | * @param atStart - Whether to insert text at the start or end of the file 2401 | */ 2402 | mergeFile: ( 2403 | file: TFile, 2404 | otherFile: TFile, 2405 | override: string, 2406 | atStart: boolean 2407 | ) => Promise; 2408 | /** 2409 | * Prompt the user to delete a file 2410 | */ 2411 | promptForDeletion: (file: TFile) => Promise; 2412 | /** 2413 | * Prompt the user to rename a file 2414 | */ 2415 | promptForFileRename: (file: TFile) => Promise; 2416 | /** 2417 | * @internal 2418 | * Register an extension to be the parent for a specific file type 2419 | */ 2420 | registerFileParentCreator: ( 2421 | extension: string, 2422 | location: TFolder 2423 | ) => void; 2424 | /** 2425 | * @internal 2426 | * @param callback - Callback to execute for each link 2427 | */ 2428 | runAsyncLinkUpdate: (callback: (link: LinkUpdate) => any) => void; 2429 | /** 2430 | * @internal 2431 | * @param path 2432 | * @param data 2433 | */ 2434 | storeTextFileBackup: (path: string, data: string) => void; 2435 | /** 2436 | * Remove a file and put it in the trash (no confirmation modal) 2437 | */ 2438 | trashFile: (file: TFile) => Promise; 2439 | /** 2440 | * @internal: Unregister extension as root input directory for file type 2441 | */ 2442 | unregisterFileCreator: (extension: string) => void; 2443 | /** 2444 | * @internal 2445 | */ 2446 | updateAllLinks: (links: any[]) => Promise; 2447 | /** 2448 | * @internal 2449 | */ 2450 | updateInternalLinks: (data: any) => any; 2451 | 2452 | /** 2453 | * @internal 2454 | */ 2455 | fileParentCreatorByType: Map any>; 2456 | /** 2457 | * @internal 2458 | */ 2459 | inProgressUpdates: null | any[]; 2460 | /** 2461 | * @internal 2462 | */ 2463 | linkUpdaters: Map any>; 2464 | /** 2465 | * @internal 2466 | */ 2467 | updateQueue: Map any>; 2468 | /** 2469 | * Reference to Vault 2470 | */ 2471 | vault: Vault; 2472 | } 2473 | 2474 | interface Modal { 2475 | /** 2476 | * @internal Background applied to application to dim it 2477 | */ 2478 | bgEl: HTMLElement; 2479 | /** 2480 | * @internal Opacity percentage of the background 2481 | */ 2482 | bgOpacity: number; 2483 | /** 2484 | * @internal Whether the background is being dimmed 2485 | */ 2486 | dimBackground: boolean; 2487 | /** 2488 | * @internal Modal container element 2489 | */ 2490 | modalEl: HTMLElement; 2491 | /** 2492 | * @internal Selection logic handler 2493 | */ 2494 | selection: WindowSelection; 2495 | /** 2496 | * Reference to the global Window object 2497 | */ 2498 | win: Window; 2499 | 2500 | /** 2501 | * @internal On escape key press close modal 2502 | */ 2503 | onEscapeKey: () => void; 2504 | /** 2505 | * @internal On closing of the modal 2506 | */ 2507 | onWindowClose: () => void; 2508 | /** 2509 | * @internal Set the background opacity of the dimmed background 2510 | * @param opacity Opacity percentage 2511 | */ 2512 | setBackgroundOpacity: (opacity: string) => this; 2513 | /** 2514 | * @internal Set the content of the modal 2515 | * @param content Content to set 2516 | */ 2517 | setContent: (content: HTMLElement | string) => this; 2518 | /** 2519 | * @internal Set whether the background should be dimmed 2520 | * @param dim Whether the background should be dimmed 2521 | */ 2522 | setDimBackground: (dim: boolean) => this; 2523 | /** 2524 | * @internal Set the title of the modal 2525 | * @param title Title to set 2526 | */ 2527 | setTitle: (title: string) => this; 2528 | } 2529 | 2530 | interface Setting extends Modal { 2531 | /** 2532 | * Current active tab of the settings modal 2533 | */ 2534 | activateTab: ESettingTab; 2535 | /** 2536 | * @internal Container element containing the community plugins 2537 | */ 2538 | communityPluginTabContainer: HTMLElement; 2539 | /** 2540 | * @internal Container element containing the community plugins header 2541 | */ 2542 | communityPluginTabHeaderGroup: HTMLElement; 2543 | /** 2544 | * Previously opened tab ID 2545 | */ 2546 | lastTabId: string; 2547 | /** 2548 | * List of all plugin tabs (core and community, ordered by precedence) 2549 | */ 2550 | pluginTabs: ESettingTab[]; 2551 | /** 2552 | * List of all core settings tabs (editor, files & links, ...) 2553 | */ 2554 | settingTabs: ESettingTab[]; 2555 | /** 2556 | * @internal Container element containing the core settings 2557 | */ 2558 | tabContainer: HTMLElement; 2559 | /** 2560 | * @internal Container for currently active settings tab 2561 | */ 2562 | tabContentContainer: HTMLElement; 2563 | /** 2564 | * @internal Container for all settings tabs 2565 | */ 2566 | tabHeadersEl: HTMLElement; 2567 | 2568 | /** 2569 | * Open a specific tab by ID 2570 | * @param id ID of the tab to open 2571 | */ 2572 | openTabById: (id: string) => void; 2573 | /** 2574 | * @internal Add a new plugin tab to the settings modal 2575 | * @param tab Tab to add 2576 | */ 2577 | addSettingTab: (tab: ESettingTab) => void; 2578 | /** 2579 | * @internal Closes the currently active tab 2580 | */ 2581 | closeActiveTab: () => void; 2582 | /** 2583 | * @internal Check whether tab is a plugin tab 2584 | * @param tab Tab to check 2585 | */ 2586 | isPluginSettingTab: (tab: ESettingTab) => boolean; 2587 | /** 2588 | * @internal Open a specific tab by tab reference 2589 | * @param tab Tab to open 2590 | */ 2591 | openTab: (tab: ESettingTab) => void; 2592 | /** 2593 | * @internal Remove a plugin tab from the settings modal 2594 | * @param tab Tab to remove 2595 | */ 2596 | removeSettingTab: (tab: ESettingTab) => void; 2597 | /** 2598 | * @internal Update the title of the modal 2599 | * @param tab Tab to update the title to 2600 | */ 2601 | updateModalTitle: (tab: ESettingTab) => void; 2602 | /** 2603 | * @internal Update a tab section 2604 | */ 2605 | updatePluginSection: () => void; 2606 | } 2607 | 2608 | interface DataAdapter { 2609 | /** 2610 | * Base OS path for the vault (e.g. /home/user/vault, or C:\Users\user\documents\vault) 2611 | */ 2612 | basePath: string; 2613 | /** 2614 | * @internal 2615 | */ 2616 | btime: { btime: (path: string, btime: number) => void }; 2617 | /** 2618 | * Mapping of file/folder path to vault entry, includes non-MD files 2619 | */ 2620 | files: Record; 2621 | /** 2622 | * Reference to node fs module 2623 | */ 2624 | fs?: fs; 2625 | /** 2626 | * Reference to node fs:promises module 2627 | */ 2628 | fsPromises?: fsPromises; 2629 | /** 2630 | * @internal 2631 | */ 2632 | insensitive: boolean; 2633 | /** 2634 | * Reference to electron ipcRenderer module 2635 | */ 2636 | ipcRenderer?: IpcRenderer; 2637 | /** 2638 | * Reference to node path module 2639 | */ 2640 | path: path; 2641 | /** 2642 | * @internal 2643 | */ 2644 | promise: Promise; 2645 | /** 2646 | * Reference to node URL module 2647 | */ 2648 | url: URL; 2649 | /** 2650 | * @internal 2651 | */ 2652 | watcher: any; 2653 | /** 2654 | * @internal 2655 | */ 2656 | watchers: Record; 2657 | 2658 | /** 2659 | * @internal Apply data write options to file 2660 | * @param normalizedPath Path to file 2661 | * @param options Data write options 2662 | */ 2663 | applyWriteOptions: ( 2664 | normalizedPath: string, 2665 | options: DataWriteOptions 2666 | ) => Promise; 2667 | /** 2668 | * Get base path of vault (OS path) 2669 | */ 2670 | getBasePath: () => string; 2671 | /** 2672 | * Get full path of file (OS path) 2673 | * @param normalizedPath Path to file 2674 | * @return URL path to file 2675 | */ 2676 | getFilePath: (normalizedPath: string) => URL; 2677 | /** 2678 | * Get full path of file (OS path) 2679 | * @param normalizedPath Path to file 2680 | * @return string full path to file 2681 | */ 2682 | getFullPath: (normalizedPath: string) => string; 2683 | /** 2684 | * Get full path of file (OS path) 2685 | * @param normalizedPath Path to file 2686 | * @return string full path to file 2687 | */ 2688 | getFullRealPath: (normalizedPath: string) => string; 2689 | /** 2690 | * @internal Get resource path of file (URL path) 2691 | * @param normalizedPath Path to file 2692 | * @return string URL of form: app://FILEHASH/path/to/file 2693 | */ 2694 | getResourcePath: (normalizedPath: string) => string; 2695 | /** 2696 | * @internal Handles vault events 2697 | */ 2698 | handler: () => void; 2699 | /** 2700 | * @internal Kill file system action due to timeout 2701 | */ 2702 | kill: () => void; 2703 | /** 2704 | * @internal 2705 | */ 2706 | killLastAction: () => void; 2707 | /** 2708 | * @internal Generates `this.files` from the file system 2709 | */ 2710 | listAll: () => Promise; 2711 | /** 2712 | * @internal Generates `this.files` for specific directory of the vault 2713 | */ 2714 | listRecursive: (normalizedPath: string) => Promise; 2715 | /** 2716 | * @internal Helper function for `listRecursive` reads children of directory 2717 | * @param normalizedPath Path to directory 2718 | */ 2719 | listRecursiveChild: (normalizedPath: string) => Promise; 2720 | /** 2721 | * @internal 2722 | */ 2723 | onFileChange: (normalizedPath: string) => void; 2724 | /** 2725 | * @internal 2726 | */ 2727 | queue: (cb: any) => Promise; 2728 | 2729 | /** 2730 | * @internal 2731 | */ 2732 | reconcileDeletion: ( 2733 | normalizedPath: string, 2734 | normalizedNewPath: string, 2735 | option: boolean 2736 | ) => void; 2737 | /** 2738 | * @internal 2739 | */ 2740 | reconcileFile: ( 2741 | normalizedPath: string, 2742 | normalizedNewPath: string, 2743 | option: boolean 2744 | ) => void; 2745 | /** 2746 | * @internal 2747 | */ 2748 | reconcileFileCreation: ( 2749 | normalizedPath: string, 2750 | normalizedNewPath: string, 2751 | option: boolean 2752 | ) => void; 2753 | /** 2754 | * @internal 2755 | */ 2756 | reconcileFileInternal: ( 2757 | normalizedPath: string, 2758 | normalizedNewPath: string 2759 | ) => void; 2760 | /** 2761 | * @internal 2762 | */ 2763 | reconcileFolderCreation: ( 2764 | normalizedPath: string, 2765 | normalizedNewPath: string 2766 | ) => void; 2767 | /** 2768 | * @internal 2769 | */ 2770 | reconcileInternalFile: (normalizedPath: string) => void; 2771 | /** 2772 | * @internal 2773 | */ 2774 | reconcileSymbolicLinkCreation: ( 2775 | normalizedPath: string, 2776 | normalizedNewPath: string 2777 | ) => void; 2778 | /** 2779 | * @internal Remove file from files listing and trigger deletion event 2780 | */ 2781 | removeFile: (normalizedPath: string) => void; 2782 | /** 2783 | * @internal 2784 | */ 2785 | startWatchpath: (normalizedPath: string) => Promise; 2786 | /** 2787 | * @internal Remove all listeners 2788 | */ 2789 | stopWatch: () => void; 2790 | /** 2791 | * @internal Remove listener on specific path 2792 | */ 2793 | stopWatchPath: (normalizedPath: string) => void; 2794 | /** 2795 | * @internal Set whether OS is insensitive to case 2796 | */ 2797 | testInsensitive: () => void; 2798 | /** 2799 | * @internal 2800 | */ 2801 | thingsHappening: () => void; 2802 | /** 2803 | * @internal Trigger an event on handler 2804 | */ 2805 | trigger: (any) => void; 2806 | /** 2807 | * @internal 2808 | */ 2809 | update: (normalizedPath: string) => any; 2810 | /** 2811 | * @internal Add change watcher to path 2812 | */ 2813 | watch: (normalizedPath: string) => Promise; 2814 | /** 2815 | * @internal Watch recursively for changes 2816 | */ 2817 | watchHiddenRecursive: (normalizedPath: string) => Promise; 2818 | } 2819 | 2820 | interface Workspace { 2821 | /** 2822 | * Currently active tab group 2823 | */ 2824 | activeTabGroup: WorkspaceTabs; 2825 | /** 2826 | * Reference to App 2827 | */ 2828 | app: App; 2829 | /** 2830 | * @internal 2831 | */ 2832 | backlinkInDocument?: any; 2833 | /** 2834 | * Registered CodeMirror editor extensions, to be applied to all CM instances 2835 | */ 2836 | editorExtensions: Extension[]; 2837 | /** 2838 | * @internal 2839 | */ 2840 | editorSuggest: { 2841 | currentSuggest?: EditorSuggest; 2842 | suggests: EditorSuggest[]; 2843 | }; 2844 | /** 2845 | * @internal 2846 | */ 2847 | floatingSplit: WorkspaceSplit; 2848 | /** 2849 | * @internal 2850 | */ 2851 | hoverLinkSources: Record; 2852 | /** 2853 | * Last opened file in the vault 2854 | */ 2855 | lastActiveFile: TFile; 2856 | /** 2857 | * @internal 2858 | */ 2859 | lastTabGroupStacked: boolean; 2860 | /** 2861 | * @internal 2862 | */ 2863 | layoutItemQueue: any[]; 2864 | /** 2865 | * Workspace has finished loading 2866 | */ 2867 | layoutReady: boolean; 2868 | /** 2869 | * @internal 2870 | */ 2871 | leftSidebarToggleButtonEl: HTMLElement; 2872 | /** 2873 | * @internal Array of renderCallbacks 2874 | */ 2875 | mobileFileInfos: any[]; 2876 | /** 2877 | * @internal 2878 | */ 2879 | onLayoutReadyCallbacks?: any; 2880 | /** 2881 | * Protocol handlers registered on the workspace 2882 | */ 2883 | protocolHandlers: Map; 2884 | /** 2885 | * Tracks last opened files in the vault 2886 | */ 2887 | recentFileTracker: RecentFileTracker; 2888 | /** 2889 | * @internal 2890 | */ 2891 | rightSidebarToggleButtonEl: HTMLElement; 2892 | /** 2893 | * @internal Keyscope registered to the vault 2894 | */ 2895 | scope: EScope; 2896 | /** 2897 | * List of states that were closed and may be reopened 2898 | */ 2899 | undoHistory: StateHistory[]; 2900 | 2901 | /** 2902 | * @internal Change active leaf and trigger leaf change event 2903 | */ 2904 | activeLeafEvents: () => void; 2905 | /** 2906 | * @internal Add file to mobile file info 2907 | */ 2908 | addMobileFileInfo: (file: any) => void; 2909 | /** 2910 | * @internal Clear layout of workspace and destruct all leaves 2911 | */ 2912 | clearLayout: () => Promise; 2913 | /** 2914 | * @internal Create a leaf in the selected tab group or last used tab group 2915 | * @param tabs Tab group to create leaf in 2916 | */ 2917 | createLeafInTabGroup: (tabs?: WorkspaceTabs) => WorkspaceLeaf; 2918 | /** 2919 | * @internal Deserialize workspace entries into actual Leaf objects 2920 | * @param leaf Leaf entry to deserialize 2921 | * @param ribbon Whether the leaf belongs to the left or right ribbon 2922 | */ 2923 | deserializeLayout: ( 2924 | leaf: LeafEntry, 2925 | ribbon?: "left" | "right" 2926 | ) => Promise; 2927 | /** 2928 | * @internal Reveal leaf in side ribbon with specified view type and state 2929 | * @param type View type of leaf 2930 | * @param ribbon Side ribbon to reveal leaf in 2931 | * @param viewstate Open state of leaf 2932 | */ 2933 | ensureSideLeaf: ( 2934 | type: string, 2935 | ribbon: "left" | "right", 2936 | viewstate: OpenViewState 2937 | ) => void; 2938 | /** 2939 | * Get active file view if exists 2940 | */ 2941 | getActiveFileView: () => FileView | null; 2942 | /** 2943 | * @deprecated Use `getActiveViewOfType` instead 2944 | */ 2945 | getActiveLeafOfViewType(type: Constructor): T | null; 2946 | /** 2947 | * Get adjacent leaf in specified direction 2948 | * @remark Potentially does not work 2949 | */ 2950 | getAdjacentLeafInDirection: ( 2951 | leaf: WorkspaceLeaf, 2952 | direction: "top" | "bottom" | "left" | "right" 2953 | ) => WorkspaceLeaf | null; 2954 | /** 2955 | * @internal Get the direction where the leaf should be dropped on dragevent 2956 | */ 2957 | getDropDirection: ( 2958 | e: DragEvent, 2959 | rect: DOMRect, 2960 | directions: ["left", "right"], 2961 | leaf: WorkspaceLeaf 2962 | ) => "left" | "right" | "top" | "bottom" | "center"; 2963 | /** 2964 | * @internal Get the leaf where the leaf should be dropped on dragevent 2965 | * @param e Drag event 2966 | */ 2967 | getDropLocation: (e: DragEvent) => WorkspaceLeaf | null; 2968 | /** 2969 | * Get the workspace split for the currently focused container 2970 | */ 2971 | getFocusedContainer: () => WorkspaceSplit; 2972 | /** 2973 | * Get n last opened files of type (defaults to 10) 2974 | */ 2975 | getRecentFiles: ({ 2976 | showMarkdown: boolean, 2977 | showCanvas: boolean, 2978 | showNonImageAttachments: boolean, 2979 | showImages: boolean, 2980 | maxCount: number, 2981 | }?) => string[]; 2982 | /** 2983 | * Get leaf in the side ribbon/dock and split if necessary 2984 | * @param sideRibbon Side ribbon to get leaf from 2985 | * @param split Whether to split the leaf if it does not exist 2986 | */ 2987 | getSideLeaf: ( 2988 | sideRibbon: WorkspaceSidedock | WorkspaceMobileDrawer, 2989 | split: boolean 2990 | ) => WorkspaceLeaf; 2991 | /** 2992 | * @internal 2993 | */ 2994 | handleExternalLinkContextMenu: (menu: Menu, linkText: string) => void; 2995 | /** 2996 | * @internal 2997 | */ 2998 | handleLinkContextMenu: ( 2999 | menu: Menu, 3000 | linkText: string, 3001 | sourcePath: string 3002 | ) => void; 3003 | /** 3004 | * @internal Check if leaf has been attached to the workspace 3005 | */ 3006 | isAttached: (leaf?: WorkspaceLeaf) => boolean; 3007 | /** 3008 | * Iterate the leaves of a split 3009 | */ 3010 | iterateLeaves: ( 3011 | split: WorkspaceSplit, 3012 | callback: (leaf: WorkspaceLeaf) => any 3013 | ) => void; 3014 | /** 3015 | * Iterate the tabs of a split till meeting a condition 3016 | */ 3017 | iterateTabs: ( 3018 | tabs: WorkspaceSplit | WorkspaceSplit[], 3019 | cb: (leaf) => boolean 3020 | ) => boolean; 3021 | /** 3022 | * @internal Load workspace from disk and initialize 3023 | */ 3024 | loadLayout: () => Promise; 3025 | /** 3026 | * @internal 3027 | */ 3028 | on: (args: any[]) => EventRef; 3029 | /** 3030 | * @internal Handles drag event on leaf 3031 | */ 3032 | onDragLeaf: (e: DragEvent, leaf: WorkspaceLeaf) => void; 3033 | /** 3034 | * @internal Handles layout change and saves layout to disk 3035 | */ 3036 | onLayoutChange: (leaf?: WorkspaceLeaf) => void; 3037 | /** 3038 | * @internal 3039 | */ 3040 | onLinkContextMenu: (args: any[]) => void; 3041 | /** 3042 | * @internal 3043 | */ 3044 | onQuickPreview: (args: any[]) => void; 3045 | /** 3046 | * @internal 3047 | */ 3048 | onResize: () => void; 3049 | /** 3050 | * @internal 3051 | */ 3052 | onStartLink: (leaf: WorkspaceLeaf) => void; 3053 | /** 3054 | * Open a leaf in a popup window 3055 | * @remark Prefer usage of `app.workspace.openPopoutLeaf` 3056 | */ 3057 | openPopout: (data?: WorkspaceWindowInitData) => WorkspaceWindow; 3058 | /** 3059 | * @internal Push leaf change to history 3060 | */ 3061 | pushUndoHistory: ( 3062 | leaf: WorkspaceLeaf, 3063 | parentID: string, 3064 | rootID: string 3065 | ) => void; 3066 | /** 3067 | * @internal Get drag event target location 3068 | */ 3069 | recursiveGetTarget: ( 3070 | e: DragEvent, 3071 | leaf: WorkspaceLeaf 3072 | ) => WorkspaceTabs | null; 3073 | /** 3074 | * @internal Register a CodeMirror editor extension 3075 | * @remark Prefer registering the extension via the Plugin class 3076 | */ 3077 | registerEditorExtension: (extension: Extension) => void; 3078 | /** 3079 | * @internal Registers hover link source 3080 | */ 3081 | registerHoverLinkSource: (key: string, source: HoverLinkSource) => void; 3082 | /** 3083 | * @internal Registers Obsidian protocol handler 3084 | */ 3085 | registerObsidianProtocolHandler: ( 3086 | protocol: string, 3087 | handler: ObsidianProtocolHandler 3088 | ) => void; 3089 | /** 3090 | * @internal Constructs hook for receiving URI actions 3091 | */ 3092 | registerUriHook: () => void; 3093 | /** 3094 | * @internal Request execution of activeLeaf change events 3095 | */ 3096 | requestActiveLeafEvents: () => void; 3097 | /** 3098 | * @internal Request execution of resize event 3099 | */ 3100 | requestResize: () => void; 3101 | /** 3102 | * @internal Request execution of layout save event 3103 | */ 3104 | requestSaveLayout: () => void; 3105 | /** 3106 | * @internal Request execution of layout update event 3107 | */ 3108 | requestUpdateLayout: () => void; 3109 | /** 3110 | * Save workspace layout to disk 3111 | */ 3112 | saveLayout: () => Promise; 3113 | /** 3114 | * @internal Use deserialized layout data to reconstruct the workspace 3115 | */ 3116 | setLayout: (data: SerializedWorkspace) => Promise; 3117 | /** 3118 | * @internal Split leaves in specified direction 3119 | */ 3120 | splitLeaf: ( 3121 | leaf: WorkspaceLeaf, 3122 | newleaf: WorkspaceLeaf, 3123 | direction?: SplitDirection, 3124 | before?: boolean 3125 | ) => void; 3126 | /** 3127 | * Split provided leaf, or active leaf if none provided 3128 | */ 3129 | splitLeafOrActive: ( 3130 | leaf?: WorkspaceLeaf, 3131 | direction?: SplitDirection 3132 | ) => void; 3133 | /** 3134 | * @internal 3135 | */ 3136 | trigger: (e: any) => void; 3137 | /** 3138 | * @internal Unregister a CodeMirror editor extension 3139 | */ 3140 | unregisterEditorExtension: (extension: Extension) => void; 3141 | /** 3142 | * @internal Unregister hover link source 3143 | */ 3144 | unregisterHoverLinkSource: (key: string) => void; 3145 | /** 3146 | * @internal Unregister Obsidian protocol handler 3147 | */ 3148 | unregisterObsidianProtocolHandler: (protocol: string) => void; 3149 | /** 3150 | * @internal 3151 | */ 3152 | updateFrameless: () => void; 3153 | /** 3154 | * @internal Invoke workspace layout update, redraw and save 3155 | */ 3156 | updateLayout: () => void; 3157 | /** 3158 | * @internal Update visibility of tab group 3159 | */ 3160 | updateMobileVisibleTabGroup: () => void; 3161 | /** 3162 | * Update the internal title of the application 3163 | * @remark This title is shown as the application title in the OS taskbar 3164 | */ 3165 | updateTitle: () => void; 3166 | } 3167 | 3168 | interface Vault { 3169 | /** 3170 | * Low-level file system adapter for read and write operations 3171 | * @tutorial Can be used to read binaries, or files not located directly within the vault 3172 | */ 3173 | adapter: DataAdapter; 3174 | /** 3175 | * @internal Max size of the cache in bytes 3176 | */ 3177 | cacheLimit: number; 3178 | /** 3179 | * Object containing all config settings for the vault (editor, appearance, ... settings) 3180 | * @remark Prefer usage of `app.vault.getConfig(key)` to get settings, config does not contain 3181 | * settings that were not changed from their default value 3182 | */ 3183 | config: AppVaultConfig; 3184 | /** 3185 | * @internal Timestamp of the last config change 3186 | */ 3187 | configTs: number; 3188 | /** 3189 | * @internal Mapping of path to Obsidian folder or file structure 3190 | */ 3191 | fileMap: Record; 3192 | 3193 | on(name: "config-changed", callback: () => void, ctx?: any): EventRef; 3194 | 3195 | /** 3196 | * @internal Add file as child/parent to respective folders 3197 | */ 3198 | addChild: (file: TAbstractFile) => void; 3199 | /** 3200 | * @internal Check whether new file path is available 3201 | */ 3202 | checkForDuplicate: (file: TAbstractFile, newPath: string) => boolean; 3203 | /** 3204 | * @internal Check whether path has valid formatting (no dots/spaces at end, ...) 3205 | */ 3206 | checkPath: (path: string) => boolean; 3207 | /** 3208 | * @internal Remove a vault config file 3209 | */ 3210 | deleteConfigJson: (configFile: string) => Promise; 3211 | /** 3212 | * Check whether a file exists in the vault 3213 | */ 3214 | exists: (file: TAbstractFile, senstive?: boolean) => Promise; 3215 | /** 3216 | * @internal 3217 | */ 3218 | generateFiles: (any) => Promise; 3219 | /** 3220 | * Get an abstract file by path, insensitive to case 3221 | */ 3222 | getAbstractFileByPathInsensitive: ( 3223 | path: string 3224 | ) => TAbstractFile | null; 3225 | /** 3226 | * @internal Get path for file that does not conflict with other existing files 3227 | */ 3228 | getAvailablePath: (path: string, extension: string) => string; 3229 | /** 3230 | * @internal Get path for attachment that does not conflict with other existing files 3231 | */ 3232 | getAvailablePathForAttachments: ( 3233 | filename: string, 3234 | file: TAbstractFile, 3235 | extension: string 3236 | ) => string; 3237 | /** 3238 | * Get value from config by key 3239 | * @remark Default value will be selected if config value was not manually changed 3240 | * @param key Key of config value 3241 | */ 3242 | getConfig: (string: ConfigItem) => any; 3243 | /** 3244 | * Get path to config file (relative to vault root) 3245 | */ 3246 | getConfigFile: (configFile: string) => string; 3247 | /** 3248 | * Get direct parent of file 3249 | * @param file File to get parent of 3250 | */ 3251 | getDirectParent: (file: TAbstractFile) => TFolder | null; 3252 | /** 3253 | * @internal Check whether files map cache is empty 3254 | */ 3255 | isEmpty: () => boolean; 3256 | /** 3257 | * @internal Iterate over the files and read them 3258 | */ 3259 | iterateFiles: (files: TFile[], cachedRead: boolean) => void; 3260 | /** 3261 | * @internal Load vault adapter 3262 | */ 3263 | load: () => Promise; 3264 | /** 3265 | * @internal Listener for all events on the vault 3266 | */ 3267 | onChange: (eventType: string, path: string, x: any, y: any) => void; 3268 | /** 3269 | * Read a config file from the vault and parse it as JSON 3270 | * @param config Name of config file 3271 | */ 3272 | readConfigJson: (config: string) => Promise; 3273 | /** 3274 | * Read a config file (full path) from the vault and parse it as JSON 3275 | * @param path Full path to config file 3276 | */ 3277 | readJson: (path: string) => Promise; 3278 | /** 3279 | * Read a plugin config file (full path relative to vault root) from the vault and parse it as JSON 3280 | * @param path Full path to plugin config file 3281 | */ 3282 | readPluginData: (path: string) => Promise; 3283 | /** 3284 | * Read a file from the vault as a string 3285 | * @param path Path to file 3286 | */ 3287 | readRaw: (path: string) => Promise; 3288 | /** 3289 | * @internal Reload all config files 3290 | */ 3291 | reloadConfig: () => void; 3292 | /** 3293 | * @internal Remove file as child/parent from respective folders 3294 | * @param file File to remove 3295 | */ 3296 | removeChild: (file: TAbstractFile) => void; 3297 | /** 3298 | * @internal Get the file by absolute path 3299 | * @param path Path to file 3300 | */ 3301 | resolveFilePath: (path: string) => TAbstractFile | null; 3302 | /** 3303 | * @internal Get the file by Obsidian URL 3304 | */ 3305 | resolveFileUrl: (url: string) => TAbstractFile | null; 3306 | /** 3307 | * @internal Debounced function for saving config 3308 | */ 3309 | requestSaveConfig: () => void; 3310 | /** 3311 | * @internal Save app and appearance configs to disk 3312 | */ 3313 | saveConfig: () => Promise; 3314 | /** 3315 | * Set value of config by key 3316 | * @param key Key of config value 3317 | * @param value Value to set 3318 | */ 3319 | setConfig: (key: ConfigItem, value: any) => void; 3320 | /** 3321 | * Set where the config files are stored (relative to vault root) 3322 | * @param configDir Path to config files 3323 | */ 3324 | setConfigDir: (configDir: string) => void; 3325 | /** 3326 | * @internal Set file cache limit 3327 | */ 3328 | setFileCacheLimit: (limit: number) => void; 3329 | /** 3330 | * @internal Load all config files into memory 3331 | */ 3332 | setupConfig: () => Promise; 3333 | /** 3334 | * @internal Trigger an event on handler 3335 | */ 3336 | trigger: (type: string) => void; 3337 | /** 3338 | * Write a config file to disk 3339 | * @param config Name of config file 3340 | * @param data Data to write 3341 | */ 3342 | writeConfigJson: (config: string, data: object) => Promise; 3343 | /** 3344 | * Write a config file (full path) to disk 3345 | * @param path Full path to config file 3346 | * @param data Data to write 3347 | * @param pretty Whether to insert tabs or spaces 3348 | */ 3349 | writeJson: ( 3350 | path: string, 3351 | data: object, 3352 | pretty?: boolean 3353 | ) => Promise; 3354 | /** 3355 | * Write a plugin config file (path relative to vault root) to disk 3356 | */ 3357 | writePluginData: (path: string, data: object) => Promise; 3358 | } 3359 | 3360 | // TODO: Add missing elements to other Obsidian interfaces and classes 3361 | 3362 | interface Editor { 3363 | /** 3364 | * CodeMirror editor instance 3365 | */ 3366 | cm: EditorViewI; 3367 | /** 3368 | * HTML instance the CM editor is attached to 3369 | */ 3370 | containerEl: HTMLElement; 3371 | 3372 | /** 3373 | * Make ranges of text highlighted within the editor given specified class (style) 3374 | */ 3375 | addHighlights: ( 3376 | ranges: { from: EditorPosition; to: EditorPosition }[], 3377 | style: "is-flashing" | "obsidian-search-match-highlight", 3378 | remove_previous: boolean, 3379 | x: boolean 3380 | ) => void; 3381 | /** 3382 | * Convert editor position to screen position 3383 | * @param pos Editor position 3384 | * @param mode Relative to the editor or the application window 3385 | */ 3386 | coordsAtPos: ( 3387 | pos: EditorPosition, 3388 | relative_to_editor = false 3389 | ) => { left: number; top: number; bottom: number; right: number }; 3390 | /** 3391 | * Unfolds all folded lines one level up 3392 | * @remark If level 1 and 2 headings are folded, level 2 headings will be unfolded 3393 | */ 3394 | foldLess: () => void; 3395 | /** 3396 | * Folds all the blocks that are of the lowest unfolded level 3397 | * @remark If there is a document with level 1 and 2 headings, level 2 headings will be folded 3398 | */ 3399 | foldMore: () => void; 3400 | /** 3401 | * Get all ranges that can be folded away in the editor 3402 | */ 3403 | getAllFoldableLines: () => { from: number; to: number }[]; 3404 | /** 3405 | * Get a clickable link - if it exists - at specified position 3406 | */ 3407 | getClickableTokenAt: ( 3408 | pos: EditorPosition 3409 | ) => { 3410 | start: EditorPosition; 3411 | end: EditorPosition; 3412 | text: string; 3413 | type: string; 3414 | } | null; 3415 | /** 3416 | * Get all blocks that were folded by their starting character position 3417 | */ 3418 | getFoldOffsets: () => Set; 3419 | /** 3420 | * Checks whether the editor has a highlight of specified class 3421 | * @remark If no style is specified, checks whether the editor has any highlights 3422 | */ 3423 | hasHighlight: (style?: string) => boolean; 3424 | /** 3425 | * Wraps current line around specified characters 3426 | * @remark Was added in a recent Obsidian update (1.4.0 update cycle) 3427 | **/ 3428 | insertBlock: (start: string, end: string) => void; 3429 | /** 3430 | * Get the closest character position to the specified coordinates 3431 | */ 3432 | posAtCoords: (coords: { left: number; top: number }) => EditorPosition; 3433 | /** 3434 | * Removes all highlights of specified class 3435 | */ 3436 | removeHighlights: (style: string) => void; 3437 | /** 3438 | * Adds a search cursor to the editor 3439 | */ 3440 | searchCursor: (searchString: string) => { 3441 | current: () => { from: EditorPosition; to: EditorPosition }; 3442 | findAll: () => { from: EditorPosition; to: EditorPosition }[]; 3443 | findNext: () => { from: EditorPosition; to: EditorPosition }; 3444 | findPrevious: () => { from: EditorPosition; to: EditorPosition }; 3445 | replace: (replacement?: string, origin: string) => void; 3446 | replaceAll: (replacement?: string, origin: string) => void; 3447 | }; 3448 | /** 3449 | * Applies specified markdown syntax to selected text or word under cursor 3450 | */ 3451 | toggleMarkdownFormatting: ( 3452 | syntax: 3453 | | "bold" 3454 | | "italic" 3455 | | "strikethrough" 3456 | | "highlight" 3457 | | "code" 3458 | | "math" 3459 | | "comment" 3460 | ) => void; 3461 | 3462 | /** 3463 | * Clean-up function executed after indenting lists 3464 | */ 3465 | afterIndent: () => void; 3466 | /** 3467 | * Expand text 3468 | * @internal 3469 | */ 3470 | expandText: () => void; 3471 | /** 3472 | * Indents a list by one level 3473 | */ 3474 | indentList: () => void; 3475 | /** 3476 | * Insert a template callout at the current cursor position 3477 | */ 3478 | insertCallout: () => void; 3479 | /** 3480 | * Insert a template code block at the current cursor position 3481 | */ 3482 | insertCodeblock: () => void; 3483 | /** 3484 | * Insert a markdown link at the current cursor position 3485 | */ 3486 | insertLink: () => void; 3487 | /** 3488 | * Insert a mathjax equation block at the current cursor position 3489 | */ 3490 | insertMathJax: () => void; 3491 | /** 3492 | * Insert specified text at the current cursor position 3493 | * @remark Might be broken, inserts at the end of the document 3494 | */ 3495 | insertText: (text: string) => void; 3496 | /** 3497 | * Inserts a new line and continues a markdown bullet point list at the same level 3498 | */ 3499 | newlineAndIndentContinueMarkdownList: () => void; 3500 | /** 3501 | * Inserts a new line at the same indent level 3502 | */ 3503 | newlineAndIndentOnly: () => void; 3504 | /** 3505 | * Get the character position at a mouse event 3506 | */ 3507 | posAtMouse: (e: MouseEvent) => EditorPosition; 3508 | /** 3509 | * Toggles blockquote syntax on paragraph under cursor 3510 | */ 3511 | toggleBlockquote: () => void; 3512 | /** 3513 | * Toggle bullet point list syntax on paragraph under cursor 3514 | */ 3515 | toggleBulletList: () => void; 3516 | /** 3517 | * Toggle checkbox syntax on paragraph under cursor 3518 | */ 3519 | toggleCheckList: () => void; 3520 | /** 3521 | * Toggle numbered list syntax on paragraph under cursor 3522 | */ 3523 | toggleNumberList: () => void; 3524 | /** 3525 | * Convert word under cursor into a wikilink 3526 | * @param embed Whether to embed the link or not 3527 | */ 3528 | triggerWikiLink: (embed: boolean) => void; 3529 | /** 3530 | * Unindents a list by one level 3531 | */ 3532 | unindentList: () => void; 3533 | } 3534 | 3535 | interface View { 3536 | headerEl: HTMLElement; 3537 | titleEl: HTMLElement; 3538 | } 3539 | 3540 | interface WorkspaceLeaf { 3541 | id?: string; 3542 | 3543 | tabHeaderEl: HTMLElement; 3544 | tabHeaderInnerIconEl: HTMLElement; 3545 | tabHeaderInnerTitleEl: HTMLElement; 3546 | } 3547 | 3548 | interface Menu { 3549 | dom: HTMLElement; 3550 | items: MenuItem[]; 3551 | onMouseOver: (evt: MouseEvent) => void; 3552 | hide: () => void; 3553 | } 3554 | 3555 | interface MenuItem { 3556 | callback: () => void; 3557 | dom: HTMLElement; 3558 | setSubmenu: () => Menu; 3559 | onClick: (evt: MouseEvent) => void; 3560 | disabled: boolean; 3561 | } 3562 | 3563 | interface MarkdownPreviewView { 3564 | renderer: ReadViewRenderer; 3565 | } 3566 | 3567 | interface EventRef { 3568 | /** 3569 | * Context applied to the event callback 3570 | */ 3571 | ctx?: any; 3572 | 3573 | /** 3574 | * Events object the event was registered on 3575 | */ 3576 | e: Events; 3577 | 3578 | /** 3579 | * Function to be called on event trigger on the events object 3580 | */ 3581 | fn: (any) => void; 3582 | 3583 | /** 3584 | * Event name the event was registered on 3585 | */ 3586 | name: string; 3587 | } 3588 | } 3589 | 3590 | interface RendererSection { 3591 | el: HTMLElement; 3592 | html: string; 3593 | rendered: boolean; 3594 | } 3595 | 3596 | interface ReadViewRenderer { 3597 | addBottomPadding: boolean; 3598 | lastRender: number; 3599 | lastScroll: number; 3600 | lastText: string; 3601 | previewEl: HTMLElement; 3602 | pusherEl: HTMLElement; 3603 | clear: () => void; 3604 | queueRender: () => void; 3605 | parseSync: () => void; 3606 | parseAsync: () => void; 3607 | set: (text: string) => void; 3608 | text: string; 3609 | sections: RendererSection[]; 3610 | asyncSections: any[]; 3611 | recycledSections: any[]; 3612 | rendered: any[]; 3613 | } 3614 | 3615 | interface CMState extends EditorState { 3616 | vim: { 3617 | inputState: { 3618 | changeQueue: null; 3619 | keyBuffer: []; 3620 | motion: null; 3621 | motionArgs: null; 3622 | motionRepeat: []; 3623 | operator: null; 3624 | operatorArgs: null; 3625 | prefixRepeat: []; 3626 | registerName: null; 3627 | }; 3628 | insertMode: false; 3629 | insertModeRepeat: undefined; 3630 | lastEditActionCommand: undefined; 3631 | lastEditInputState: undefined; 3632 | lastHPos: number; 3633 | lastHSPos: number; 3634 | lastMotion: { 3635 | name?: string; 3636 | }; 3637 | lastPastedText: null; 3638 | lastSelection: null; 3639 | }; 3640 | vimPlugin: { 3641 | lastKeydown: string; 3642 | }; 3643 | } 3644 | 3645 | interface CMView extends EditorView { 3646 | state: CMState; 3647 | } 3648 | 3649 | interface EditorViewI extends EditorView { 3650 | cm?: CMView; 3651 | } 3652 | -------------------------------------------------------------------------------- /src/util/AsyncQueue.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * this class is used to queue async tasks. 3 | */ 4 | export class AsyncQueue { 5 | public readonly queue: (() => Promise | void)[] = []; 6 | 7 | /** 8 | * push a task to the queue. If the queue is empty, the task will be executed immediately. 9 | */ 10 | push(task: () => Promise | void): void { 11 | this.queue.push(task); 12 | if (this.queue.length === 1) { 13 | this.run(); 14 | } 15 | } 16 | 17 | /** 18 | * recursively run the tasks in the queue. 19 | * Stop when the queue is empty. 20 | */ 21 | private async run(): Promise { 22 | const task = this.queue[0]; 23 | if (!task) return; 24 | try { 25 | await task(); 26 | } catch (error) { 27 | console.error(`Error executing task: ${error}`); 28 | } finally { 29 | this.queue.shift(); 30 | } 31 | await this.run(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/util/State.ts: -------------------------------------------------------------------------------- 1 | import ObservableSlim from "observable-slim"; 2 | 3 | // ====================================================== // 4 | // ====================== State ====================== // 5 | // ====================================================== // 6 | 7 | // Wrapper class to make any object/primitive observable 8 | 9 | export type StateListener = (changeData: StateChange) => void; 10 | 11 | /** 12 | * Wrapper class to make any object/primitive observable 13 | */ 14 | export class State { 15 | private readonly listeners = new Map(); 16 | private static listener_count = 0; 17 | 18 | private val: ProxyConstructor | T; 19 | private static stateCount = 0; 20 | readonly id: number; 21 | 22 | constructor(value: T) { 23 | State.stateCount++; 24 | this.id = State.stateCount; 25 | 26 | this.val = 27 | typeof value === "object" ? ObservableSlim.create(value, false, this.onValueChange) : value; 28 | } 29 | 30 | get value(): T { 31 | return this.val as T; 32 | } 33 | 34 | set value(val: T) { 35 | const previousValue = this.val; 36 | if (typeof val !== "object") { 37 | this.val = val; 38 | } else { 39 | this.val = ObservableSlim.create(val, false, this.onValueChange); 40 | } 41 | this.onValueChange([ 42 | { 43 | type: "update", 44 | property: "", 45 | currentPath: "", 46 | jsonPointer: "", 47 | target: this.val, 48 | // @ts-ignore 49 | proxy: (this.val as never).__getProxy, 50 | previousValue, 51 | newValue: this.val, 52 | }, 53 | ]); 54 | } 55 | 56 | public onChange = (callback: (change: StateChange) => void): (() => void) => { 57 | const listenerId = this.generateListenerId(); 58 | this.listeners.set(listenerId, callback); 59 | return () => this.unsubscribe(listenerId); // return unsubscribe function 60 | }; 61 | 62 | /** 63 | * create a substate of this state. 64 | * 65 | * @remarks You cannot create a substate for a primitive type state. 66 | */ 67 | public createSubState(key: T extends object ? `value.${NestedKeyof}` : string): State { 68 | const subStateKeys = key.split("."); 69 | const subStateValue: S = subStateKeys.reduce((obj: Record, key: string) => { 70 | const val = obj[key]; 71 | if (val !== undefined) { 72 | return val as Record; 73 | } 74 | throw new InvalidStateKeyError(key, this); 75 | }, this as Record) as S; 76 | 77 | // if this is a primitive type, we cannot create a substate 78 | if (typeof subStateValue !== "object") { 79 | throw new Error("SubStates of properties that are Primitives are not supported yet."); 80 | } 81 | 82 | // @ts-ignore 83 | return new State(subStateValue?.__getTarget); 84 | 85 | // if (typeof subStateValue === "object" && type) { 86 | // // check if is like generic type S 87 | 88 | // // ts-ignore 89 | // return new State(subStateValue?.__getTarget); 90 | // } else throw new Error("SubStates of properties that are Primitives are not supported yet."); 91 | } 92 | 93 | public getRawValue(): T { 94 | if (typeof this.val === "object") { 95 | // @ts-ignore 96 | return (this.val as unknown as ProxyConstructor).__getTarget; 97 | } 98 | return this.val as T; 99 | } 100 | 101 | private generateListenerId = () => { 102 | State.listener_count++; 103 | return State.listener_count; 104 | }; 105 | 106 | private unsubscribe = (listenerId: number) => { 107 | this.listeners.delete(listenerId); 108 | }; 109 | 110 | private notifyAll = (changeData: StateChange) => { 111 | this.listeners.forEach((listener) => listener(changeData)); 112 | }; 113 | 114 | private onValueChange = (changes: StateChange[]) => { 115 | changes.forEach((change) => { 116 | this.notifyAll(Object.assign({}, change, { triggerStateId: this.id })); 117 | }); 118 | }; 119 | } 120 | 121 | // custom error type for invalid state keys 122 | export class InvalidStateKeyError extends Error { 123 | constructor(subStateKey: string, state: State) { 124 | super(); 125 | this.message = `Key does not exist! 126 | Detailed error: 127 | "${subStateKey}" could not be found in {"value":${JSON.stringify(state.value)}} 128 | `; 129 | } 130 | } 131 | 132 | export type NestedKeyof = T extends object 133 | ? NestedKeyOf | WithPrefixNumber> 134 | : string; 135 | export interface StateChange { 136 | type: "add" | "delete" | "update"; 137 | property: string; // equals "value" if the whole state is changed 138 | 139 | currentPath: NestedKeyof; // path of the property 140 | jsonPointer: string; // path as json pointer syntax 141 | target: T; // the target object 142 | proxy?: ProxyConstructor; // the proxy of the object 143 | 144 | previousValue?: T; // may be undefined if the property is new 145 | newValue?: T; // may be undefined if the property is deleted 146 | } 147 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .better-plugins-page-plugin-setting-tab { 2 | /* padding-top: 20px; */ 3 | 4 | .hidden-plugins-setting, 5 | .saved-plugins-setting { 6 | flex-direction: column; 7 | align-items: stretch; 8 | } 9 | 10 | .hidden-plugins-setting textarea, 11 | .saved-plugins-setting textarea { 12 | margin-top: 10px; 13 | min-width: 100%; 14 | max-width: 100%; 15 | width: 100%; 16 | min-height: 120px; 17 | } 18 | 19 | > h2 { 20 | color: var(--interactive-accent) !important; 21 | } 22 | 23 | > h2 + .setting-item { 24 | border-top: 0px; 25 | } 26 | } 27 | 28 | /* Hide community items with the specified class */ 29 | .better-plugins-page-hidden-community-item { 30 | opacity: 0.4; 31 | } 32 | 33 | .community-item:not(:hover) .buttons-container { 34 | display: none; 35 | } 36 | 37 | .community-item .buttons-container { 38 | flex-direction: column; 39 | align-items: flex-start; 40 | } 41 | 42 | /* Hide the button by default */ 43 | .community-item .buttons-container button { 44 | display: none !important; 45 | } 46 | 47 | /* Show the button when hovering over the card */ 48 | .community-item:hover .buttons-container { 49 | /* You can style the button as needed */ 50 | /* For example, you can set its position to the top right corner */ 51 | position: absolute; 52 | top: 10px; 53 | right: 10px; 54 | } 55 | 56 | .community-item.better-plugins-page-hidden-community-item:hover 57 | button.show-button, 58 | .community-item:hover:not(.better-plugins-page-hidden-community-item):not( 59 | [data-installed="true"] 60 | ) 61 | button.hide-button, 62 | .community-item.better-plugins-page-hidden-community-item:hover 63 | .buttons-container, 64 | .community-item[data-saved="false"]:hover button.save-button, 65 | .community-item[data-saved="true"]:hover button.unsave-button { 66 | display: block !important; 67 | } 68 | 69 | .community-item .buttons-container button { 70 | cursor: pointer; 71 | } 72 | 73 | .better-plugin-page-download-count-filtered { 74 | display: none; 75 | } 76 | 77 | .better-plugins-page-plugin-setting-button-group { 78 | display: flex; 79 | flex-direction: row; 80 | align-items: center; 81 | justify-content: flex-end; 82 | } 83 | 84 | /* Add margin to create a 10px gap between flex items */ 85 | .better-plugins-page-plugin-setting-button-group > * { 86 | margin-right: 10px; /* Adjust as needed */ 87 | } 88 | 89 | /* Remove the margin from the last item to prevent extra spacing */ 90 | .better-plugins-page-plugin-setting-button-group > :last-child { 91 | margin-right: 0; 92 | } 93 | 94 | /* Add bookmark icon to saved community item */ 95 | .community-item[data-saved="true"] { 96 | position: relative; 97 | } 98 | 99 | .community-item[data-saved="true"]::before { 100 | content: ""; 101 | position: absolute; 102 | top: -5px; 103 | right: 10px; 104 | width: 24px; /* Adjust the size as needed */ 105 | height: 24px; /* Adjust the size as needed */ 106 | background: url("data:image/svg+xml;charset=utf8,"); 107 | background-size: cover; 108 | background-repeat: no-repeat; 109 | z-index: 1; 110 | } 111 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "ES6", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "isolatedModules": true, 13 | "strictNullChecks": true, 14 | "allowSyntheticDefaultImports": true, 15 | "noUncheckedIndexedAccess": true, 16 | "skipLibCheck": true, 17 | "lib": ["DOM", "ES5", "ES6", "ES7", "ESNext"], 18 | "types": ["bun-types"], 19 | "paths": { 20 | "@/*": ["./src/*"] 21 | } 22 | }, 23 | "include": ["**/*.ts"], 24 | "exclude": ["node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = JSON.parse(readFileSync("package.json", "utf8")).version; 4 | 5 | // read minAppVersion from manifest.json and bump version to target version 6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 7 | const { minAppVersion } = manifest; 8 | manifest.version = targetVersion; 9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); 10 | 11 | // update versions.json with target version and minAppVersion from manifest.json 12 | let versions = JSON.parse(readFileSync("versions.json", "utf8")); 13 | versions[targetVersion] = minAppVersion; 14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); 15 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": "0.15.0", 3 | "1.0.1": "0.15.0", 4 | "1.0.2": "0.15.0", 5 | "1.0.3": "0.15.0", 6 | "1.0.4": "0.15.0", 7 | "1.0.5": "0.15.0", 8 | "1.0.6": "0.15.0", 9 | "1.0.7": "0.15.0", 10 | "1.0.8": "0.15.0", 11 | "1.0.9": "0.15.0", 12 | "1.0.10": "0.15.0", 13 | "1.0.11": "0.15.0", 14 | "1.0.12": "0.15.0", 15 | "1.0.13": "0.15.0" 16 | } --------------------------------------------------------------------------------