├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .npmrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── bun.lockb ├── docs ├── .gitignore ├── README.md ├── babel.config.js ├── bun.lock ├── docs │ ├── AIAssistant.md │ ├── Advanced │ │ ├── ObsidianUri.md │ │ └── scriptsWithSettings.md │ ├── Choices │ │ ├── CaptureChoice.md │ │ ├── MacroChoice.md │ │ ├── MultiChoice.md │ │ └── TemplateChoice.md │ ├── Examples │ │ ├── Attachments │ │ │ ├── BookFinder.js │ │ │ ├── EzImport.js │ │ │ ├── TodoistScript.js │ │ │ ├── citationsManager.js │ │ │ ├── getLongLatFromAddress.js │ │ │ ├── movies.js │ │ │ ├── togglManager.js │ │ │ └── zettelizer.js │ │ ├── Capture_AddJournalEntry.md │ │ ├── Capture_AddTaskToKanbanBoard.md │ │ ├── Capture_FetchTasksFromTodoist.md │ │ ├── Macro_AddLocationLongLatFromAddress.md │ │ ├── Macro_BookFinder.md │ │ ├── Macro_ChangePropertyInDailyNotes.md │ │ ├── Macro_LogBookToDailyJournal.md │ │ ├── Macro_MoveNotesWithATagToAFolder.md │ │ ├── Macro_MovieAndSeriesScript.md │ │ ├── Macro_TogglManager.md │ │ ├── Macro_Zettelizer.md │ │ ├── Template_AddAnInboxItem.md │ │ └── Template_AutomaticBookNotesFromReadwise.md │ ├── FormatSyntax.md │ ├── Images │ │ ├── AI_Assistant_Macro.gif │ │ ├── AI_Assistant_Setup.gif │ │ ├── Todoist-GetAllTasksFromProject.png │ │ ├── TogglManager.gif │ │ ├── TogglManagerMacro.png │ │ ├── TogglManagerMacroChoice.png │ │ ├── longLatDemo.gif │ │ ├── moviescript.gif │ │ ├── moviescript_settings.jpg │ │ ├── readwise_template_choice.png │ │ ├── script_with_settings.png │ │ └── zettelizer_demo.gif │ ├── InlineScripts.md │ ├── ManualInstallation.md │ ├── Misc │ │ └── AHK_OpenQuickAddFromDesktop.md │ ├── QuickAddAPI.md │ └── index.md ├── docusaurus.config.js ├── package.json ├── sidebars.js ├── src │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.module.css │ │ ├── index.tsx │ │ └── markdown-page.md ├── static │ ├── .nojekyll │ └── img │ │ ├── docusaurus.png │ │ ├── favicon.ico │ │ ├── undraw_docusaurus_mountain.svg │ │ ├── undraw_docusaurus_react.svg │ │ └── undraw_docusaurus_tree.svg ├── tsconfig.json └── vercel.json ├── esbuild.config.mjs ├── manifest.json ├── package.json ├── pnpm-lock.yaml ├── src ├── IChoiceExecutor.ts ├── LaTeXSymbols.ts ├── MacrosManager.ts ├── ai │ ├── AIAssistant.ts │ ├── OpenAIModelParameters.ts │ ├── OpenAIRequest.ts │ ├── Provider.ts │ ├── aiHelpers.ts │ ├── makeNoticeHandler.ts │ └── preventCursorChange.ts ├── choiceExecutor.ts ├── constants.ts ├── engine │ ├── CaptureChoiceEngine.ts │ ├── MacroChoiceEngine.ts │ ├── QuickAddChoiceEngine.ts │ ├── QuickAddEngine.ts │ ├── SingleInlineScriptEngine.ts │ ├── SingleMacroEngine.ts │ ├── SingleTemplateEngine.ts │ ├── StartupMacroEngine.ts │ ├── TemplateChoiceEngine.ts │ └── TemplateEngine.ts ├── formatters │ ├── captureChoiceFormatter.ts │ ├── completeFormatter.ts │ ├── fileNameDisplayFormatter.ts │ ├── formatDisplayFormatter.ts │ ├── formatter.ts │ └── helpers │ │ ├── getEndOfSection.test.ts │ │ └── getEndOfSection.ts ├── global.d.ts ├── gui │ ├── AIAssistantProvidersModal.ts │ ├── AIAssistantSettingsModal.ts │ ├── ChoiceBuilder │ │ ├── FolderList.svelte │ │ ├── captureChoiceBuilder.ts │ │ ├── choiceBuilder.ts │ │ ├── macroChoiceBuilder.ts │ │ └── templateChoiceBuilder.ts │ ├── GenericCheckboxPrompt │ │ └── genericCheckboxPrompt.ts │ ├── GenericInfoDialog │ │ └── GenericInfoDialog.ts │ ├── GenericInputPrompt │ │ └── GenericInputPrompt.ts │ ├── GenericSuggester │ │ └── genericSuggester.ts │ ├── GenericWideInputPrompt │ │ └── GenericWideInputPrompt.ts │ ├── GenericYesNoPrompt │ │ └── GenericYesNoPrompt.ts │ ├── InputPrompt.ts │ ├── InputSuggester │ │ └── inputSuggester.ts │ ├── MacroGUIs │ │ ├── AIAssistantCommandSettingsModal.ts │ │ ├── AIAssistantInfiniteCommandSettingsModal.ts │ │ ├── CommandList.svelte │ │ ├── Components │ │ │ ├── AIAssistantCommand.svelte │ │ │ ├── NestedChoiceCommand.svelte │ │ │ ├── StandardCommand.svelte │ │ │ ├── UserScriptCommand.svelte │ │ │ └── WaitCommand.svelte │ │ ├── MacroBuilder.ts │ │ └── UserScriptSettingsModal.ts │ ├── MathModal.ts │ ├── UpdateModal │ │ └── UpdateModal.ts │ ├── choiceList │ │ ├── AddChoiceBox.svelte │ │ ├── ChoiceItemRightButtons.svelte │ │ ├── ChoiceList.svelte │ │ ├── ChoiceListItem.svelte │ │ ├── ChoiceView.svelte │ │ └── MultiChoiceListItem.svelte │ └── suggesters │ │ ├── LaTeXSuggester.ts │ │ ├── choiceSuggester.ts │ │ ├── exclusiveSuggester.ts │ │ ├── fileSuggester.ts │ │ ├── formatSyntaxSuggester.ts │ │ ├── genericTextSuggester.ts │ │ ├── suggest.ts │ │ └── tagSuggester.ts ├── logger │ ├── consoleErrorLogger.ts │ ├── errorLevel.ts │ ├── guiLogger.ts │ ├── ilogger.ts │ ├── logDecorator.ts │ ├── logManager.ts │ ├── quickAddError.ts │ └── quickAddLogger.ts ├── main.ts ├── migrations │ ├── Migrations.ts │ ├── addDefaultAIProviders.ts │ ├── helpers │ │ ├── isCaptureChoice.ts │ │ ├── isMultiChoice.ts │ │ ├── isNestedChoiceCommand.ts │ │ └── isOldTemplateChoice.ts │ ├── incrementFileNameSettingMoveToDefaultBehavior.ts │ ├── migrate.ts │ ├── migrateToMacroIDFromEmbeddedMacro.ts │ ├── mutualExclusionInsertAfterAndWriteToBottomOfFile.ts │ ├── setVersionAfterUpdateModalRelease.ts │ └── useQuickAddTemplateFolder.ts ├── quickAddApi.ts ├── quickAddSettingsTab.ts ├── settingsStore.ts ├── styles.css ├── types │ ├── IconType.ts │ ├── choices │ │ ├── CaptureChoice.ts │ │ ├── Choice.ts │ │ ├── ICaptureChoice.ts │ │ ├── IChoice.ts │ │ ├── IMacroChoice.ts │ │ ├── IMultiChoice.ts │ │ ├── ITemplateChoice.ts │ │ ├── MacroChoice.ts │ │ ├── MultiChoice.ts │ │ ├── TemplateChoice.ts │ │ └── choiceType.ts │ ├── fileViewMode.ts │ ├── macros │ │ ├── ChoiceCommand.ts │ │ ├── Command.ts │ │ ├── CommandType.ts │ │ ├── EditorCommands │ │ │ ├── CopyCommand.ts │ │ │ ├── CutCommand.ts │ │ │ ├── EditorCommand.ts │ │ │ ├── EditorCommandType.ts │ │ │ ├── IEditorCommand.ts │ │ │ ├── PasteCommand.ts │ │ │ ├── SelectActiveLineCommand.ts │ │ │ └── SelectLinkOnActiveLineCommand.ts │ │ ├── IChoiceCommand.ts │ │ ├── ICommand.ts │ │ ├── IMacro.ts │ │ ├── IObsidianCommand.ts │ │ ├── IUserScript.ts │ │ ├── ObsidianCommand.ts │ │ ├── QuickAddMacro.ts │ │ ├── QuickCommands │ │ │ ├── AIAssistantCommand.ts │ │ │ ├── IAIAssistantCommand.ts │ │ │ ├── INestedChoiceCommand.ts │ │ │ ├── IWaitCommand.ts │ │ │ ├── NestedChoiceCommand.ts │ │ │ └── WaitCommand.ts │ │ └── UserScript.ts │ └── newTabDirection.ts ├── utility.ts ├── utilityObsidian.ts └── utils │ ├── errorUtils.ts │ ├── invariant.ts │ └── setPasswordOnBlur.ts ├── tsconfig.json ├── version-bump.mjs ├── versions.json └── vitest.config.ts /.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 | npm node_modules 2 | build -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "extends": ["plugin:@typescript-eslint/recommended-requiring-type-checking"], 5 | "files": ["*.ts"], 6 | "parserOptions": { 7 | "project": ["./tsconfig.json"] 8 | } 9 | } 10 | ], 11 | "root": true, 12 | "parser": "@typescript-eslint/parser", 13 | "env": { "node": true }, 14 | "plugins": ["@typescript-eslint"], 15 | "extends": [ 16 | "plugin:@typescript-eslint/recommended" 17 | ], 18 | "parserOptions": { 19 | "sourceType": "module", 20 | "project": "./tsconfig.json" 21 | }, 22 | "rules": { 23 | "no-unused-vars": "off", 24 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 25 | "@typescript-eslint/ban-ts-comment": "off", 26 | "no-prototype-builtins": "off", 27 | "@typescript-eslint/no-empty-function": "off", 28 | "@typescript-eslint/consistent-type-imports": "warn", 29 | "@typescript-eslint/switch-exhaustiveness-check": "error" 30 | }, 31 | "ignorePatterns": ["node_modules/", "main.js"] 32 | } 33 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: chhoumann 2 | custom: https://www.buymeacoffee.com/chhoumann -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] " 5 | labels: bug 6 | 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 1. Go to '...' 15 | 2. Click on '....' 16 | 3. Scroll down to '....' 17 | 4. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Desktop (please complete the following information):** 26 | - OS: [e.g. iOS] 27 | - Browser [e.g. chrome, safari] 28 | - Version [e.g. 22] 29 | 30 | **Smartphone (please complete the following information):** 31 | - Device: [e.g. iPhone6] 32 | - OS: [e.g. iOS8.1] 33 | - Browser [e.g. stock browser, safari] 34 | - Version [e.g. 22] 35 | 36 | **Additional context** 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE REQUEST] " 5 | labels: enhancement 6 | 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Additional context** 16 | Add any other context or screenshots about the feature request here. 17 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: push 3 | 4 | jobs: 5 | test: 6 | name: Test 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [20] 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | - name: Install Bun 17 | uses: oven-sh/setup-bun@v1 18 | with: 19 | bun-version: latest 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - name: Install dependencies 25 | run: | 26 | bun install 27 | - name: Run tests 28 | run: | 29 | bun run test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Intellij 2 | *.iml 3 | .idea 4 | 5 | # npm 6 | node_modules 7 | package-lock.json 8 | 9 | # build 10 | main.js 11 | styles.css 12 | *.js.map 13 | 14 | # obsidian 15 | data.json 16 | 17 | **/*.map 18 | src/**/*.js -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/node_modules": true, 4 | "**/bower_components": true, 5 | "**/*.code-search": true, 6 | "main.js": true, 7 | } 8 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Christian Bager Bach Houmann 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 | # QuickAdd 2 | QuickAdd is a powerful combination of four tools (called choices): templates, captures, macros, and multis. 3 | 4 | A [**Template**](https://quickadd.obsidian.guide/docs/Choices/TemplateChoice) is a definition of how to create a new note, and composes with Obsidian's own Templates core plugin or community template plugins. For example, it would allow you to define a quick action to create a new note in a particular location, with a templatized title, and templated content. 5 | 6 | A [**Capture**](https://quickadd.obsidian.guide/docs/Choices/CaptureChoice) allows you to quickly add content to predefined files. For example, you could set up a quick action to add a link to the open file to your daily note under a specific section. 7 | 8 | [**Macros**](https://quickadd.obsidian.guide/docs/Choices/MacroChoice) will allow you to compound these two together into powerful chained workflows. Imagine pressing one hotkey to automatically create a new note to track a chess match with a specific template, while automatically adding a reference to it in your "list of matches" note and in your daily note. 9 | 10 | [Multi choices](https://quickadd.obsidian.guide/docs/Choices/MultiChoice) are purely organisational: folders of other choices. 11 | 12 | Throughout your choices, you can use the [QuickAdd format syntax](https://quickadd.obsidian.guide/docs/FormatSyntax), which is similar to the Obsidian template syntax. You could, for example, use ``{{DATE}}`` to insert the current date in a filename. 13 | 14 | ### Demo video 15 | [![Demo video](https://img.youtube.com/vi/gYK3VDQsZJo/0.jpg)](https://www.youtube.com/watch?v=gYK3VDQsZJo) 16 | 17 | ## Installation 18 | 19 | QuickAdd can be installed through the community plugin browser in Obsidian, or through manual installation. See the [installation documentation](https://quickadd.obsidian.guide/docs/#installation) for more information. 20 | 21 | ## Getting Started 22 | 23 | For detailed instructions and examples on using QuickAdd, see the [QuickAdd documentation](https://quickadd.obsidian.guide/). 24 | 25 | ## Support 26 | 27 | If you have any questions or encounter any problems while using QuickAdd, you can use the [community discussions](https://github.com/chhoumann/quickadd/discussions) for support. 28 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chhoumann/quickadd/ae003f88dfde67e333b0331772ff0ce7b7daf00e/bun.lockb -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/docs/Advanced/ObsidianUri.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Open QuickAdd from a URI 3 | --- 4 | 5 | QuickAdd choices can be launched from external scripts or apps such as Shortcuts on Mac and iOS, through the use of the `obsidian://quickadd` URI. 6 | 7 | ``` 8 | obsidian://quickadd?choice=[&value-VALUE_NAME=...] 9 | ``` 10 | 11 | :::note 12 | 13 | All parameter names and values must be properly [URL encoded](https://en.wikipedia.org/wiki/Percent-encoding) to work. You can use an online tool like [urlencoder.org](https://www.urlencoder.org/) to help you easily encode parts of the URI. 14 | 15 | ::: 16 | 17 | The only required parameter is `choice` which selects the choice to run by its name. The name must match exactly, otherwise it will not be able to be found. 18 | 19 | [Variables to your choice](../FormatSyntax.md) are passed as additional `value-VARIABLE_NAME` parameters, with `value-` prefixing the name. Variables with a space in their name can still be used, but the spaces in the name must be encoded as `%20` as usual. For example, a capture asking for a variable named `log notes` would be passed as `value-log%20notes=...` in the URI. 20 | 21 | Keep in mind that unnamed variables (a bare `{{VALUE}}`/`{{NAME}}` or `{{MVALUE}}`) cannot be filled by the URI and you will instead be prompted inside Obsidian as usual. 22 | 23 | ## Vault parameter 24 | 25 | Like every Obsidian URI, you can use the special `vault` parameter to specify which vault to run QuickAdd in. If left blank, it will be executed in your most recent vault. 26 | 27 | ``` 28 | obsidian://quickadd?vault=My%20Vault&choice=Daily%20log&value-contents=Lorem%20ipsum. 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/docs/Advanced/scriptsWithSettings.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Scripts with user settings 3 | --- 4 | 5 | QuickAdd supports scripts with settings. This allows you to create scripts that can be configured by the user. 6 | 7 | Any script with settings will have a ⚙️ button next to the script name in a macro. Clicking the button will open a settings menu for the script. 8 | 9 | As an example, see the [Movies](../Examples/Macro_MovieAndSeriesScript.md) macro. 10 | 11 | ## Creating a script with settings 12 | A script with settings is a JavaScript file that exports an object with two properties: `entry` and `settings`. 13 | 14 | The `entry` property is a function that is called when the script is executed. The function is passed two arguments: `QuickAdd` and `settings` (naming is up to you). 15 | `QuickAdd` is an object containing the same as what is usually passed to [scripts in macros](../Choices/MacroChoice). `settings` is an object containing the settings for the script, as set by the user. 16 | 17 | The `settings` property is an object containing the settings for the script. It has three properties: `name`, `author` and `options`. 18 | 19 | `name` is the name of the script, as shown in the settings menu. 20 | 21 | `author` is the author of the script, as shown in the settings menu. 22 | 23 | `options` is an object containing the settings for the script. The keys are the names of the settings, and the values are objects containing the setup parameters for the setting. 24 | 25 | For example, the following script will have a text field setting with the key `Text field`, a checkbox setting with the key `Checkbox`, a dropdown setting with the key `Dropdown` and a format setting with the key `Format`. This is shown in the image below. 26 | 27 | ![Settings menu for the script](../Images/script_with_settings.png) 28 | 29 | It's possible to give a description to a setting by adding a `description` property to the setting object. 30 | 31 | ```js 32 | const TEXT_FIELD = "Text field"; 33 | 34 | module.exports = { 35 | entry: async (QuickAdd, settings) => { 36 | // Logic here 37 | const textFieldSettingValue = settings[TEXT_FIELD]; 38 | }, 39 | settings: { 40 | name: "Demo", 41 | author: "Christian B. B. Houmann", 42 | options: { 43 | [TEXT_FIELD]: { 44 | type: "text", 45 | defaultValue: "", 46 | placeholder: "Placeholder", 47 | description: "Description here.", 48 | }, 49 | "Checkbox": { 50 | type: "checkbox", 51 | defaultValue: false, 52 | }, 53 | "Dropdown": { 54 | type: "dropdown", 55 | defaultValue: "Option 1", 56 | options: [ 57 | "Option 1", 58 | "Option 2", 59 | "Option 3", 60 | ], 61 | }, 62 | "Format": { 63 | type: "format", 64 | defaultValue: "{{DATE:YYYY-MM-DD}}", 65 | placeholder: "Placeholder", 66 | }, 67 | } 68 | }, 69 | }; 70 | ``` 71 | 72 | ## Setting types 73 | - `text` and `input`: A text field. 74 | - `checkbox` and `toggle`: A checkbox. 75 | - `dropdown` and `select`: A dropdown. 76 | - `format`: A format field, adhering to [format syntax](../FormatSyntax.md). -------------------------------------------------------------------------------- /docs/docs/Choices/MultiChoice.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Multis 3 | --- 4 | 5 | Multi-choices are pretty simple. They're like folders for other choices. Here are mine. They're the ones which you can 'open' and 'close'. 6 | 7 | ![image](https://user-images.githubusercontent.com/29108628/121774481-e39f7f80-cb82-11eb-92bf-6d265529ba06.png) 8 | 9 | To actually add something in this "folder", you need to drag it in! This is not easy to do when it is the first item in the multi-folder. 10 | 11 | Make sure the multi is unfolded (as it is in the screenshot). Click the drag handle of one of the choices you want to add and drag it to just below and to the right of the drag handle for the multi. When successful, the choice will be indented under the multi. 12 | -------------------------------------------------------------------------------- /docs/docs/Choices/TemplateChoice.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Template 3 | --- 4 | 5 | The template choice type is not meant to be a replacement for [Templater](https://github.com/SilentVoid13/Templater/) plugin or core `Templates`. It's meant to augment them, to add more possibilities. You can use both QuickAdd format syntax in a Templater template - and both will work. 6 | 7 | ## Mandatory 8 | **Template Path**. This is a path to the template you wish to insert. 9 | 10 | ## Optional 11 | **File Name Format**. You can specify a format for the file name, which is based on the format syntax - which you can see further down this page. 12 | Basically, this allows you to have dynamic file names. If you wrote `£ {{DATE}} {{NAME}}`, it would translate to a file name like `£ 2021-06-12 Manually-Written-File-Name`, where `Manually-Written-File-Name` is a value you enter when invoking the template. 13 | 14 | **Create in folder**. In which folder should the file be created in. 15 | You can specify as many folders as you want. If you don't, it'll just create the file in the root directory. If you specify one folder, it'll automatically create the file in there. 16 | If you specify multiple folders, you'll get a suggester asking which of the folders you wish to create the file in. 17 | 18 | **Append link**. The file you're currently in will get a link to a newly created file. 19 | 20 | **Increment file name**. If a file with that name already exists, increment the file name with a number. So if a file called `untitled` already exists, the new file will be called `untitled1`. 21 | 22 | **Open**. Will open the file you've created. By default, it opens in the active pane. If you enable **New tab**, it'll open in a new tab in the direction you specified. 23 | ![image](https://user-images.githubusercontent.com/29108628/121773888-3f680980-cb7f-11eb-919b-97d56ef9268e.png) 24 | -------------------------------------------------------------------------------- /docs/docs/Examples/Attachments/BookFinder.js: -------------------------------------------------------------------------------- 1 | const notice = (msg) => new Notice(msg, 5000); 2 | const log = (msg) => console.log(msg); 3 | 4 | const GOOGLE_BOOKS_API_URL = "https://www.googleapis.com/books/v1/volumes"; 5 | const GOOGLE_BOOKS_TITLE_TERM = "intitle:" 6 | 7 | let QuickAdd; 8 | 9 | module.exports = async function start(params) { 10 | QuickAdd = params; 11 | 12 | let clipBoardContents = await QuickAdd.quickAddApi.utility.getClipboard(); 13 | const title = await QuickAdd.quickAddApi.inputPrompt( 14 | "Enter Book title: ", clipBoardContents, clipBoardContents // clipBoardContents is added once as the prompt text and once as the default value 15 | ); 16 | if (!title) { 17 | notice("No title entered."); 18 | throw new Error("No title entered."); 19 | } 20 | 21 | const encodedTitle = encodeURIComponent(GOOGLE_BOOKS_TITLE_TERM + title); 22 | const finalURL = GOOGLE_BOOKS_API_URL + "?q=" + encodedTitle + "&maxResults=10"; 23 | const response = await fetch(finalURL); 24 | const bookDesc = await response.json(); 25 | 26 | // In an ideal world we would popup a picker that shows the user: Book Title, Author(s) and cover. They would select the correct version from there 27 | 28 | QuickAdd.variables = { 29 | ...bookDesc.items[0], 30 | title: bookDesc.items[0].volumeInfo.title, 31 | // How to get mutiple authors or categories out with commas between them 32 | authors: bookDesc.items[0].volumeInfo.authors, 33 | categories: bookDesc.items[0].volumeInfo.categories, 34 | description: bookDesc.items[0].volumeInfo.description, 35 | fileName: replaceIllegalFileNameCharactersInString(bookDesc.items[0].volumeInfo.title), 36 | Poster:bookDesc.items[0].volumeInfo.imageLinks.smallThumbnail 37 | }; 38 | } 39 | 40 | function replaceIllegalFileNameCharactersInString(string) { 41 | return string.replace(/[\\,#%&\{\}\/*<>?$\'\":@]*/g, ""); 42 | } -------------------------------------------------------------------------------- /docs/docs/Examples/Attachments/TodoistScript.js: -------------------------------------------------------------------------------- 1 | module.exports = {SelectFromAllTasks, GetAllTasksFromProject, GetAllTasksFromSection}; 2 | 3 | const getTodoistPluginApi = (app) => app.plugins.plugins["todoist-sync-plugin"].api; 4 | 5 | /* API */ 6 | async function SelectFromAllTasks(params) { 7 | const tasks = await getAllTasks(params); 8 | if (tasks.length === 0) { 9 | new Notice("No tasks."); 10 | return; 11 | } 12 | const selectedTasks = await selectTasks(params, tasks); 13 | 14 | await closeSelectedTasks(params.app, selectedTasks); 15 | return formatTasksToTasksPluginTask(selectedTasks); 16 | } 17 | 18 | async function GetAllTasksFromProject(params) { 19 | const [allTasks, projects] = await Promise.all([getAllTasks(params), getProjects(params.app)]); 20 | const targetProject = await params.quickAddApi.suggester(project => { 21 | project.tasks = allTasks.filter(task => task.projectID === project.id); 22 | 23 | return `${project.name} (${project.tasks.length})`; 24 | }, projects); 25 | if (!targetProject) return; 26 | 27 | if (targetProject.tasks.length === 0) { 28 | new Notice(`No tasks in '${targetProject.name}'.`); 29 | return; 30 | } else { 31 | new Notice(`Added ${targetProject.tasks.length} tasks from '${targetProject.name}'.`) 32 | } 33 | 34 | await closeSelectedTasks(params.app, targetProject.tasks); 35 | return formatTasksToTasksPluginTask(targetProject.tasks); 36 | } 37 | 38 | async function GetAllTasksFromSection(params) { 39 | const [projects, sections, allTasks] = await Promise.all([getProjects(params.app), getSections(params.app), getAllTasks(params)]); 40 | 41 | const targetSection = await params.quickAddApi.suggester(section => { 42 | const sectionProject = projects.find(project => project.id === section["project_id"]); 43 | section.tasks = allTasks.filter(task => task.sectionID === section.id); 44 | return `${sectionProject.name} > ${section.name} (${section.tasks.length})`; 45 | }, sections); 46 | 47 | if (targetSection.tasks.length === 0) { 48 | new Notice(`No tasks in '${targetSection.name}'.`); 49 | return; 50 | } else { 51 | new Notice(`Added ${targetSection.tasks.length} tasks from '${targetSection.name}'.`) 52 | } 53 | 54 | await closeSelectedTasks(targetSection.tasks); 55 | return formatTasksToTasksPluginTask(targetSection.tasks); 56 | } 57 | 58 | /* Helpers */ 59 | async function getAllTasks(params) { 60 | const api = getTodoistPluginApi(params.app); 61 | const {ok: tasks} = await api.getTasks(); 62 | return tasks; 63 | } 64 | 65 | async function selectTasks(params, tasks) { 66 | const selectedTaskNames = await params.quickAddApi.checkboxPrompt(tasks.map(task => task.content)); 67 | return tasks.filter(task => selectedTaskNames.some(t => t.contains(task.content))); 68 | } 69 | 70 | async function closeSelectedTasks(app, tasks) { 71 | const api = getTodoistPluginApi(app); 72 | tasks.forEach(async task => await api.closeTask(task.id)); 73 | } 74 | 75 | function formatTasksToTasksPluginTask(tasks) { 76 | return tasks.map(task => 77 | task.rawDatetime ? 78 | task = `- [ ] ${task.content} 📅 ${task.rawDatetime.format("YYYY-MM-DD")}` : 79 | task = `- [ ] ${task.content}` 80 | ).join("\n") + "\n"; 81 | } 82 | 83 | async function getTasksGroupedByProject(app) { 84 | const api = getTodoistPluginApi(app); 85 | const {ok: projects} = await api.getTasksGroupedByProject(); 86 | return projects; 87 | } 88 | 89 | async function getProjects(app) { 90 | const api = getTodoistPluginApi(app); 91 | const {ok: projects} = await api.getProjects(); 92 | return projects; 93 | } 94 | 95 | async function getSections(app) { 96 | const api = getTodoistPluginApi(app); 97 | const {ok: sections} = await api.getSections(); 98 | return sections; 99 | } -------------------------------------------------------------------------------- /docs/docs/Examples/Attachments/citationsManager.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: start, 3 | settings: { 4 | name: "Citations Manager", 5 | author: "Christian B. B. Houmann", 6 | options: { 7 | "Ignore empty values": { 8 | type: "toggle", 9 | defaultValue: true, 10 | }, 11 | }, 12 | } 13 | } 14 | 15 | const ignoreEmpty = "Ignore empty values"; 16 | 17 | async function start(params, settings) { 18 | const citationsPlugin = params.app.plugins.plugins["obsidian-citation-plugin"]; 19 | 20 | if (citationsPlugin) { 21 | await handleCitationsPlugin(params, citationsPlugin, settings); 22 | } else { 23 | new Notice("Citations plugin not found.", 5000); 24 | throw new Error("Citations plugin not found."); 25 | } 26 | } 27 | 28 | async function handleCitationsPlugin(params, citationsPlugin, settings) { 29 | // Open suggester with library 30 | const library = citationsPlugin.library.entries; 31 | const selectedLibraryEntryKey = await params.quickAddApi.suggester(entry => { 32 | const item = library[entry]; 33 | if (item.title) return item.title; 34 | return entry; 35 | }, Object.keys(library)); 36 | const entry = library[selectedLibraryEntryKey]; 37 | 38 | if (!entry && !selectedLibraryEntryKey) { 39 | new Notice("No library entry selected.", 5000); 40 | throw new Error("No library entry selected."); 41 | } else if (!entry) { 42 | new Notice("Invalid entry. Selected library entry: " + selectedLibraryEntryKey, 5000); 43 | throw new Error("Invalid entry. Selected library entry: " + selectedLibraryEntryKey); 44 | } 45 | 46 | params.variables = { 47 | ...params.variables, 48 | fileName: replaceIllegalFileNameCharactersInString(entry.title), 49 | citekey: selectedLibraryEntryKey, 50 | id: selectedLibraryEntryKey, 51 | author: entry.authorString.split(', ').map(author => `[[${author}]]`).join(", "), 52 | doi: entry.DOI, 53 | 54 | // https://github.com/hans/obsidian-citation-plugin/blob/cb601fceda8c70c0404dd250c50cdf83d5d04979/src/types.ts#L46 55 | abstract: entry.abstract, 56 | authorString: entry.authorString, 57 | containerTitle: entry.containerTitle, 58 | DOI: entry.DOI, 59 | eprint: entry.eprint, 60 | eprinttype: entry.eprinttype, 61 | eventPlace: entry.eventPlace, 62 | note: entry.note, 63 | page: entry.page, 64 | publisher: entry.publisher, 65 | publisherPlace: entry.publisherPlace, 66 | title: entry.title, 67 | URL: entry.URL, 68 | year: entry.year?.toString(), 69 | zoteroSelectURI: entry.zoteroSelectURI, 70 | type: entry.type, 71 | issuedDate: entry.issuedDate, 72 | keywords: entry?.data?.fields?.keywords ? importAllKeywordsAsTags(entry.data.fields.keywords) : "", 73 | }; 74 | 75 | if (settings[ignoreEmpty]) { 76 | Object.keys(params.variables).forEach(key => { 77 | if (params.variables[key] === undefined) { 78 | params.variables[key] = " "; 79 | } 80 | }); 81 | } 82 | } 83 | 84 | function replaceIllegalFileNameCharactersInString(string) { 85 | return string.replace(/[\\,#%&\{\}\/*<>$\'\":@\?]*/g, ''); 86 | } 87 | 88 | function importAllKeywordsAsTags(keywords) { 89 | keywords.forEach((element , index) => keywords[index] = (" #" + element.replace(" ","_"))) 90 | 91 | return(keywords) 92 | } -------------------------------------------------------------------------------- /docs/docs/Examples/Attachments/getLongLatFromAddress.js: -------------------------------------------------------------------------------- 1 | module.exports = async (params) => { 2 | const {createYamlProperty} = params.app.plugins.plugins["metaedit"].api; 3 | const address = await params.quickAddApi.inputPrompt("🏠 Address"); 4 | if (!address) { 5 | new Notice("No address given", 5000); 6 | return; 7 | } 8 | 9 | const result = await apiGet(address); 10 | if (!result.length) { 11 | new Notice("No results found", 5000); 12 | return; 13 | } 14 | 15 | const {lat, lon} = result[0]; 16 | 17 | const activeFile = params.app.workspace.getActiveFile(); 18 | if (!activeFile) { 19 | new Notice("No active file", 5000); 20 | return; 21 | } 22 | 23 | await createYamlProperty("location", `[${lat}, ${lon}]`, activeFile); 24 | } 25 | 26 | 27 | async function apiGet(searchQuery) { 28 | let finalURL = new URL(`https://nominatim.openstreetmap.org/search?q=${searchQuery}&format=json`); 29 | 30 | return await fetch(finalURL, { 31 | method: 'GET', cache: 'no-cache', 32 | headers: { 33 | 'Content-Type': 'application/json', 34 | }, 35 | }).then(async (res) => await res.json()); 36 | } -------------------------------------------------------------------------------- /docs/docs/Examples/Attachments/togglManager.js: -------------------------------------------------------------------------------- 1 | let togglApi; 2 | let quickAddApi; 3 | let projects; 4 | 5 | const back = "<- Back"; 6 | const menu = { 7 | "🧠 Learning & Skill Development": { 8 | togglProjectName: "Learning & Skill Development", 9 | menuOptions: { 10 | "✍ Note Making": "Note Making", 11 | "🃏 Spaced Repetition": "Spaced Repetition", 12 | "📖 Read Later Processing": "Read Later Processing", 13 | "👨‍💻 Computer Science & Software Engineering": "Computer Science & Software Engineering", 14 | } 15 | }, 16 | "🤴 Personal": { 17 | togglProjectName: "Personal", 18 | menuOptions: { 19 | "🏋️‍♂️ Exercise": "Exercise", 20 | "🧹 Chores": "Chores", 21 | "👨‍🔬 Systems Work": "Systems Work", 22 | "🌀 Weekly Review": "Weekly Review", 23 | "📆 Monthly Review": "Monthly Review", 24 | "✔ Planning": "Planning", 25 | } 26 | }, 27 | "👨‍🎓 School": { 28 | togglProjectName: "School", 29 | menuOptions: { 30 | "🧠 Machine Intelligence (MI)": "Machine Intelligence (MI)", 31 | "💾 Database Systems (DBS)": "Database Systems (DBS)", 32 | "🏃‍♂ Agile Software Engineering (ASE)": "Agile Software Engineering (ASE)", 33 | "💻 P5": "P5", 34 | } 35 | } 36 | }; 37 | 38 | module.exports = async function togglManager(params) { 39 | togglApi = params.app.plugins.plugins["obsidian-toggl-integration"].toggl._apiManager; 40 | quickAddApi = params.quickAddApi; 41 | projects = await togglApi.getProjects(); 42 | 43 | openMainMenu(menu); 44 | } 45 | 46 | const dateInSeconds = (date) => { 47 | return Math.floor(date / 1000); 48 | } 49 | 50 | async function startTimer(entryName, projectID) { 51 | await togglApi.startTimer({description: entryName, pid: projectID}); 52 | } 53 | 54 | async function openMainMenu(menu) { 55 | const {suggester} = quickAddApi; 56 | const options = Object.keys(menu); 57 | 58 | const choice = await suggester(options, options); 59 | if (!choice) return; 60 | 61 | const project = menu[choice]; 62 | await openSubMenu(project); 63 | } 64 | 65 | async function openSubMenu(project) { 66 | const {suggester} = quickAddApi; 67 | const options = [...Object.keys(project.menuOptions), back]; 68 | 69 | const choice = await suggester(options, options); 70 | if (!choice) return; 71 | 72 | if (choice === back) { 73 | return await openMainMenu(menu); 74 | } 75 | 76 | const entryName = project.menuOptions[choice]; 77 | const projectID = projects.find(p => p.name === project.togglProjectName).id; 78 | 79 | startTimer(entryName, projectID); 80 | } 81 | -------------------------------------------------------------------------------- /docs/docs/Examples/Attachments/zettelizer.js: -------------------------------------------------------------------------------- 1 | module.exports = async (params) => { 2 | console.log("Starting...") 3 | console.log(params); 4 | const currentFile = params.app.workspace.getActiveFile(); 5 | if (!currentFile) { 6 | new Notice("No active file."); 7 | return; 8 | } 9 | console.log("Found active file: ", currentFile.basename); 10 | 11 | const currentFileCache = params.app.metadataCache.getFileCache(currentFile); 12 | const headingsInFile = currentFileCache.headings; 13 | if (!headingsInFile) { 14 | new Notice(`No headers in file ${currentFile.name}`); 15 | return; 16 | } 17 | console.log("Found headings in active file: ", headingsInFile); 18 | 19 | const folder = "40 Slipbox/44 Zettels"; 20 | if (!params.app.vault.adapter.exists(folder)) { 21 | new Notice(`Could not find folder ${folder}`); 22 | return; 23 | } 24 | 25 | console.log("Folder does exist: ", folder); 26 | 27 | headingsInFile.forEach(async heading => { 28 | console.log(`Checking ${heading.heading}. It is level ${heading.level}`); 29 | if (heading.level === 3) { 30 | const splitHeading = heading.heading.split(" "); 31 | const location = splitHeading[0].trim(); 32 | const text = splitHeading.length > 1 ? [...splitHeading.slice(1)].join(' ').trim() : ""; 33 | 34 | const path = `${folder}/${text.replace(/[\\,#%&\{\}\/*<>$\'\":@]*/g, '')}.md`; 35 | const content = `![[${currentFile.basename}#${location}${text ? " " + text : ""}]]`; 36 | 37 | console.log(`Path: ${path}.\nContent: ${content}`); 38 | 39 | if (text && !(await params.app.vault.adapter.exists(path))) 40 | await params.app.vault.create(path, content); 41 | else if (text) 42 | new Notice(`File ${path} already exists.`, 5000); 43 | } 44 | }); 45 | 46 | console.log("Finished!"); 47 | } -------------------------------------------------------------------------------- /docs/docs/Examples/Capture_AddJournalEntry.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Capture: Add journal entry" 3 | --- 4 | 5 | This captures a new journal entry in my daily journal under the `What did I do today` header. 6 | 7 | Capture To: `bins/daily/{{DATE:gggg-MM-DD - ddd MMM D}}.md` 8 | 9 | Insert after: `## What did I do today?` 10 | 11 | Capture format: 12 | 13 | ``` 14 | - {{DATE:HH:mm}} {{VALUE}}\n 15 | ``` 16 | 17 | ![image](https://user-images.githubusercontent.com/29108628/121774877-c2d82980-cb84-11eb-99c4-a20a14e41856.png) 18 | -------------------------------------------------------------------------------- /docs/docs/Examples/Capture_AddTaskToKanbanBoard.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Capture: Add a Task to a Kanban Board" 3 | --- 4 | 5 | This will add a task to the chosen Kanban Board. 6 | 7 | In Capture To, select the board. 8 | 9 | Select the Task option. 10 | 11 | Then select the Insert after option, and write `## ` followed by the name of the lane you want to add the task to. 12 | 13 | In my case, I want to add tasks to a lane called `Backlog`, so it becomes `## Backlog`. 14 | 15 | If you want, you can experiment with the format syntax - you could, for example, experiment with adding dates and times. 16 | 17 | To add a date for a task, you could just write `{{VALUE}} @{{{DATE}}}` in the format syntax. This would add the current date as the date for the card. 18 | 19 | You could also use `{{VALUE}} @{{{VDATE:DATE,gggg-MM-DD}}}` to get asked which date you want to input - but do note that this requires the Natural Language Dates plugin. 20 | 21 | Read more about [format syntax here](../FormatSyntax.md). 22 | 23 | ![image](https://user-images.githubusercontent.com/29108628/123068109-e23b4600-d411-11eb-8886-8362ad09ec11.png) 24 | -------------------------------------------------------------------------------- /docs/docs/Examples/Capture_FetchTasksFromTodoist.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Capture: Fetch Tasks From Todoist" 3 | --- 4 | 5 | For this capture to work, you will need the Todoist plugin for Obsidian. 6 | You will also need to set it up with your API key. 7 | 8 | This is very useful for capturing tasks on the go with your phone, and then adding them to Obsidian when you get back to your computer. 9 | 10 | You will need to set up a [macro](../Choices/MacroChoice.md) with the [Todoist Script](./Attachments/TodoistScript.js). 11 | 12 | The script has three exports, `SelectFromAllTasks`, `GetAllTasksFromProject`, and `GetAllTasksFromSection`. 13 | 14 | - `SelectFromAllTasks` will prompt you to select tasks from all tasks on your Todoist account, 15 | - `GetAllTasksFromProject` will prompt you for a project and get all tasks from that project, and 16 | - `GetAllTasksFromSection` will prompt you for a section and get all tasks from that section. 17 | 18 | Personally, I just let QuickAdd ask me which one to execute. 19 | 20 | However, when you are entering the user script in the macro, you can add `::GetAllTasksFromProject` (or, `::` followed by any of the other exports) to directly call one of the exported functions. 21 | 22 | ![Get all tasks from project](../Images/Todoist-GetAllTasksFromProject.png) 23 | 24 | **IMPORTANT:** If you do _NOT_ want this script to complete tasks in Todoist that you put into your vault, remove the function call to `closeSelectedTasks`. 25 | 26 | Now, you will need a [Capture choice](../Choices/CaptureChoice.md) with the following settings. 27 | 28 | - _Capture To File Name:_ the path to the file where you want to store the tasks. 29 | - _Capture format:_ Enabled - and in the format, write`{{MACRO:}}` where `MACRONAME` is the name of the macro that you made earlier. 30 | 31 | The tasks are written in this format: 32 | `- [ ] 📆 ` 33 | 34 | Which equals: `- [ ] Buy groceries 📆 2021-06-27` 35 | 36 | This task will be recognized by the Tasks plugin for Obsidian, as well. 37 | If there isn't a date set for the task, they'll simply be entered as `- [ ] Buy groceries`. 38 | 39 | ### Steps 40 | 41 | _NOTE:_ If you simply follow the process below, you will be asked which export to execute each time. 42 | That is fine - if you want to be asked - but you can also make separate [Capture choices](../Choices/CaptureChoice.md) for each exported function, meaning, it'll execute that function without asking you which one to execute. 43 | Just set up the macro as shown in the image above. 44 | 45 | 1. Set up the Todoist plugin - grab the API key from your Todoist account. There's a link in the plugin's settings. 46 | 2. Grab the code block from the example and add it to your vault as a javascript file. I'd encourage you to call it something like todoistTaskSync.js to be explicit. 47 | 3. Follow along with what I do in the gif below 48 | 49 | ![GKkCNWZHLv](https://user-images.githubusercontent.com/29108628/123500983-26ad2880-d642-11eb-9e45-b537271312d1.gif) 50 | 51 | ### Installation video 52 | 53 | https://user-images.githubusercontent.com/29108628/123511101-bde4a100-d67f-11eb-90c1-5bd146c5d0f2.mp4 54 | -------------------------------------------------------------------------------- /docs/docs/Examples/Macro_AddLocationLongLatFromAddress.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Macro: Add location long-lat from address" 3 | --- 4 | This is especially useful for the [Obsidian Map View plugin](https://github.com/esm7/obsidian-map-view). 5 | 6 | You can find the script [here](./Attachments/getLongLatFromAddress.js). 7 | Here is a [guide to installing user scripts](./Capture_FetchTasksFromTodoist.md) like this one. 8 | 9 | 1. Grab the script from [this page](./Attachments/getLongLatFromAddress.js). This can be done in multiple ways: 10 | 1. By clicking the 'Raw' button, and then saving the 'page' (CTRL+S on Windows, probably command+S on Mac), or 11 | 2. Copying the file contents and saving it as `getLongLatFromAddress.js`. The `.js` is _crucial_. 12 | 2. Save the file to somewhere in your vault. It doesn't matter where, as long as it's in your vault. 13 | 3. Open QuickAdd settings, and then click 'Manage Macros'. 14 | 4. Enter a macro name (I call mine 'Mapper'), and click 'Add macro'. 15 | 5. The macro should appear. Click its 'Configure' button. 16 | 6. There will be 3 input fields. Place your cursor in the one besides 'User Scripts', and it should display a suggester. Assuming you have no other `.js` files in your vault besides the one we just grabbed, it should be the only one shown. Either way, you'll want to click it, and then click 'Add'. It should get added as number 1. 17 | 7. Go back to the QuickAdd main settings. Add a new choice with a name of your choosing. This choice should be a _Macro_ choice, which can be selected using the dropdown next to the 'Add Choice' button. Add this choice, and then 18 | 8. It will appear on the list of choices. Click the ⚙ (gear) button for it, to configure it. 19 | 9. Select the macro you've just created. 20 | 10. Go back out of the QuickAdd settings. You can now run QuickAdd with the `Run QuickAdd` command in the command palette. The Choice you've made should appear. 21 | 22 | It adds a YAML property to the active file called ``location`` with `[lat, long]` as its value given the address you enter. 23 | 24 | **Important:** Requires MetaEdit. If you have your edit mode in MetaEdit set to All Multi, do note that you will need to remove the braces on line 23 in the script, so it looks like this: ```await createYamlProperty("location", `${lat}, ${lon}`, activeFile);```. 25 | 26 | ![Demo](../Images/longLatDemo.gif) 27 | 28 | -------------------------------------------------------------------------------- /docs/docs/Examples/Macro_ChangePropertyInDailyNotes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Macro: Change properties in your daily notes (requires MetaEdit)" 3 | --- 4 | 5 | This macro opens a suggester containing all properties in my daily journal note. 6 | 7 | When I select one of them, I get prompted to add a value to it. 8 | 9 | To use this, you need to change the path to your daily note - as this one only fits those similar to mine. 10 | 1. Change the date format from ``gggg-MM-DD - ddd MMM D`` to your daily notes' format. 11 | 2. Change the path to the daily note. Mine is in the ``bins/daily/`` folder - you should change yours such that it matches wherever your daily notes are. 12 | 13 | Once you've done this, it'll work! 14 | 15 | In case you already know which properties you want to change, and you don't want to get asked about the rest, you could just make an array containing the names of the properties instead. You'd pass that array to the ``suggester`` method. 16 | 17 | ````js 18 | module.exports = async (params) => { 19 | const {quickAddApi: {inputPrompt, suggester}} = params; 20 | const {update, getPropertiesInFile} = app.plugins.plugins["metaedit"].api; 21 | const date = window.moment().format("gggg-MM-DD - ddd MMM D"); 22 | const dailyJournalFilePath = `bins/daily/${date}.md`; 23 | 24 | const propertiesInDailyJournal = await getPropertiesInFile(dailyJournalFilePath); 25 | const targetProp = await suggester(propertiesInDailyJournal.map(p => p.key), propertiesInDailyJournal); 26 | 27 | const newPropertyValue = await inputPrompt(`Log ${targetProp.key}`, targetProp.content, targetProp.content); 28 | 29 | await update(targetProp.key, newPropertyValue, dailyJournalFilePath); 30 | } 31 | ```` -------------------------------------------------------------------------------- /docs/docs/Examples/Macro_LogBookToDailyJournal.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Macro: Log book to daily journal" 3 | --- 4 | 5 | ![image](https://user-images.githubusercontent.com/29108628/121774885-d1bedc00-cb84-11eb-9776-d1cdd353e99e.png) 6 | ![image](https://user-images.githubusercontent.com/29108628/121774905-ef8c4100-cb84-11eb-9657-b24759096886.png) 7 | 8 | ```js 9 | // You have to export the function you wish to run. 10 | // QuickAdd automatically passes a parameter, which is an object with the Obsidian app object 11 | // and the QuickAdd API (see description further on this page). 12 | module.exports = async (params) => { 13 | // Object destructuring. We pull inputPrompt out of the QuickAdd API in params. 14 | const { 15 | quickAddApi: { inputPrompt }, 16 | } = params; 17 | // Here, I pull in the update function from the MetaEdit API. 18 | const { update } = app.plugins.plugins["metaedit"].api; 19 | // This opens a prompt with the header "📖 Book Name". val will be whatever you enter. 20 | const val = await inputPrompt("📖 Book Name"); 21 | // This gets the current date in the specified format. 22 | const date = window.moment().format("gggg-MM-DD - ddd MMM D"); 23 | // Invoke the MetaEdit update function on the Book property in my daily journal note. 24 | // It updates the value of Book to the value entered (val). 25 | await update("Book", val, `bins/daily/${date}.md`); 26 | }; 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/docs/Examples/Macro_MoveNotesWithATagToAFolder.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Macro: Move notes with a tag to a folder" 3 | --- 4 | 5 | This script allows you to move notes with a certain tag to a folder. 6 | ![h44DF7W7Ef](https://user-images.githubusercontent.com/29108628/122404732-c18d6f00-cf7f-11eb-8a6f-17d47db8b015.gif) 7 | 8 | ```js 9 | module.exports = async function moveFilesWithTag(params) { 10 | const { 11 | app, 12 | quickAddApi: { suggester, yesNoPrompt }, 13 | } = params; 14 | const allTags = Object.keys(app.metadataCache.getTags()); 15 | const tag = await suggester(allTags, allTags); 16 | if (!tag) return; 17 | const shouldMoveNested = await yesNoPrompt( 18 | "Should I move nested tags, too?", 19 | `If you say no, I'll only move tags that are strictly equal to what you've chosen. If you say yes, I'll move tags that are nested under ${tag}.` 20 | ); 21 | 22 | const cache = app.metadataCache.getCachedFiles(); 23 | let filesToMove = []; 24 | 25 | cache.forEach((key) => { 26 | if (key.contains("template")) return; 27 | const fileCache = app.metadataCache.getCache(key); 28 | let hasFrontmatterCacheTag, hasTag; 29 | 30 | if (!shouldMoveNested) { 31 | hasFrontmatterCacheTag = fileCache.frontmatter?.tags 32 | ?.split(" ") 33 | .some((t) => t === tag.replace("#", "")); 34 | hasFrontmatterCacheTag = 35 | hasFrontmatterCacheTag || 36 | fileCache.frontmatter?.Tags?.split(" ").some( 37 | (t) => t === tag.replace("#", "") 38 | ); 39 | hasFrontmatterCacheTag = 40 | hasFrontmatterCacheTag || 41 | fileCache.frontmatter?.tag 42 | ?.split(" ") 43 | .some((t) => t === tag.replace("#", "")); 44 | hasFrontmatterCacheTag = 45 | hasFrontmatterCacheTag || 46 | fileCache.frontmatter?.Tag?.split(" ").some( 47 | (t) => t === tag.replace("#", "") 48 | ); 49 | hasTag = fileCache?.tags?.some((t) => t.tag === tag); 50 | } else { 51 | hasFrontmatterCacheTag = fileCache.frontmatter?.tags 52 | ?.split(" ") 53 | .some((t) => t.contains(tag.replace("#", ""))); 54 | hasFrontmatterCacheTag = 55 | hasFrontmatterCacheTag || 56 | fileCache.frontmatter?.Tags?.split(" ").some((t) => 57 | t.contains(tag.replace("#", "")) 58 | ); 59 | hasFrontmatterCacheTag = 60 | hasFrontmatterCacheTag || 61 | fileCache.frontmatter?.tag 62 | ?.split(" ") 63 | .some((t) => t.contains(tag.replace("#", ""))); 64 | hasFrontmatterCacheTag = 65 | hasFrontmatterCacheTag || 66 | fileCache.frontmatter?.Tag?.split(" ").some((t) => 67 | t.contains(tag.replace("#", "")) 68 | ); 69 | hasTag = fileCache?.tags?.some((t) => t.tag.contains(tag)); 70 | } 71 | 72 | if (hasFrontmatterCacheTag || hasTag) filesToMove.push(key); 73 | }); 74 | 75 | const folders = app.vault 76 | .getAllLoadedFiles() 77 | .filter((f) => f.children) 78 | .map((f) => f.path); 79 | const targetFolder = await suggester(folders, folders); 80 | if (!targetFolder) return; 81 | 82 | for (const file of filesToMove) { 83 | const tfile = app.vault.getAbstractFileByPath(file); 84 | await app.fileManager.renameFile( 85 | tfile, 86 | `${targetFolder}/${tfile.name}` 87 | ); 88 | } 89 | }; 90 | ``` 91 | -------------------------------------------------------------------------------- /docs/docs/Examples/Macro_MovieAndSeriesScript.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Movie & Series Script 3 | --- 4 | 5 | This script allows you to easily insert a movie or TV show note into your vault. 6 | 7 | We use OMDb api to get the movie or TV show information. You can get an API key on the website [here](https://www.omdbapi.com/). This will be needed to use this script. 8 | 9 | ## Demo 10 | 11 | ![Demo](../Images/moviescript.gif) 12 | 13 | ## Installation 14 | 15 | We'll need to install a QuickAdd user script for this to work. I have made a video which shows you how to do so - [click here](https://www.youtube.com/watch?v=gYK3VDQsZJo&t=1730s). 16 | You will need to put the user script into a new macro and then create a Macro choice in the main menu to activate it. 17 | You can find the script [here](./Attachments/movies.js). 18 | 19 | 1. Save the script (`movies.js`) to your vault somewhere. Make sure it is saved as a JavaScript file, meaning that it has the `.js` at the end. 20 | 2. Create a new template in your designated templates folder. Example template is provided below. 21 | 3. Open the Macro Manager by opening the QuickAdd plugin settings and clicking `Manage Macros`. 22 | 4. Create a new Macro - you decide what to name it. I named mine `Movie`. 23 | 5. Add the user script to the command list. 24 | 6. Add a new Template step to the macro. This will be what creates the note in your vault. Settings are as follows: 25 | 1. Set the template path to the template you created. 26 | 2. Enable File Name Format and use `{{VALUE:fileName}}` as the file name format. You can specify this however you like. The `fileName` value is the name of the Movie or TV show without illegal file name characters. 27 | 3. The remaining settings are for you to specify depending on your needs. 28 | 7. Click on the cog icon to the right of the script step to configure the script settings. This should allow you to enter the API key you got from OMDb. [Image demonstration](../Images/moviescript_settings.jpg). 29 | 8. Go back out to your QuickAdd main menu and add a new Macro choice. Again, you decide the name. I named mine `🎬 Movie`. This is what activates the macro. 30 | 9. Attach the Macro to the Macro Choice you just created. Do so by clicking the cog ⚙ icon and selecting it. 31 | 32 | You can now use the macro to create notes with movie or TV show information in your vault. 33 | 34 | ### Example template 35 | 36 | ```markdown 37 | --- 38 | cover: { { VALUE:Poster } } 39 | --- 40 | 41 | category:: {{VALUE:typeLink}} 42 | director:: {{VALUE:directorLink}} 43 | genre:: {{VALUE:genreLinks}} 44 | imdbId:: {{VALUE:imdbID}} 45 | ratingImdb:: {{VALUE:imdbRating}} 46 | rating:: 47 | year:: {{VALUE:Year}} 48 | cast:: {{VALUE:actorLinks}} 49 | plot:: {{VALUE:Plot}} 50 | 51 | ![poster]({{VALUE:Poster}}) 52 | ``` 53 | 54 | ## Usage 55 | 56 | It's possible to access whichever JSON variables are sent in response through a `{{VALUE:}}` tag (e.g. `{{VALUE:Title}}`). Below is an example response for the TV show 'Arcane'. 57 | 58 | ```json 59 | { 60 | "Title": "Arcane", 61 | "Year": "2021–", 62 | "Rated": "TV-14", 63 | "Released": "06 Nov 2021", 64 | "Runtime": "N/A", 65 | "Genre": "Animation, Action, Adventure", 66 | "Director": "N/A", 67 | "Writer": "N/A", 68 | "Actors": "Hailee Steinfeld, Kevin Alejandro, Jason Spisak", 69 | "Plot": "Set in utopian Piltover and the oppressed underground of Zaun, the story follows the origins of two iconic League champions-and the power that will tear them apart.", 70 | "Language": "English", 71 | "Country": "United States, France", 72 | "Awards": "N/A", 73 | "Poster": "https://m.media-amazon.com/images/M/MV5BYmU5OWM5ZTAtNjUzOC00NmUyLTgyOWMtMjlkNjdlMDAzMzU1XkEyXkFqcGdeQXVyMDM2NDM2MQ@@._V1_SX300.jpg", 74 | "Ratings": [ 75 | { 76 | "Source": "Internet Movie Database", 77 | "Value": "9.2/10" 78 | } 79 | ], 80 | "Metascore": "N/A", 81 | "imdbRating": "9.2", 82 | "imdbVotes": "105,113", 83 | "imdbID": "tt11126994", 84 | "Type": "series", 85 | "totalSeasons": "2", 86 | "Response": "True" 87 | } 88 | ``` 89 | -------------------------------------------------------------------------------- /docs/docs/Examples/Macro_TogglManager.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Toggl Manager 3 | --- 4 | 5 | This [Macro](../Choices/MacroChoice.md) allows you to set preset time entries for [Toggl Track](https://track.toggl.com). 6 | 7 | It uses the [Toggl plugin](https://github.com/mcndt/obsidian-toggl-integration) for [Obsidian](https://obsidian.md). Make sure that is set up before you continue. 8 | 9 | ![Toggl Manager](../Images/TogglManager.gif) 10 | 11 | We'll need to install a QuickAdd user script for this to work. I have made a video which shows you how to do so - [click here](https://www.youtube.com/watch?v=gYK3VDQsZJo&t=1730s). 12 | You will need to put the user script into a new macro and then create a Macro choice in the main menu to activate it. 13 | You can find the script [here](./Attachments/togglManager.js). 14 | 15 | ## Installation 16 | 1. Save the script (`togglManager.js`) to your vault somewhere. Make sure it is saved as a JavaScript file, meaning that it has the `.js` at the end. 17 | 2. Open the Macro Manager by opening the QuickAdd plugin settings and clicking `Manage Macros`. 18 | 3. Create a new Macro - you decide what to name it. I named mine ``⏳ TogglManager``. 19 | 4. Add the user script to the command list. 20 | 5. Go back out to your QuickAdd main menu and add a new Macro choice. Again, you decide the name. I named mine ``⏳ Toggl Manager``. This is what activates the macro. 21 | 6. Attach the Macro to the Macro Choice you just created. Do so by clicking the cog ⚙ icon and selecting it. 22 | 23 | Your Macro should look like this: 24 | 25 | ![TogglManager Macro](../Images/TogglManagerMacro.png) 26 | 27 | Your Macro Choice should look like this: 28 | 29 | ![Toggl Manager Macro Choice](../Images/TogglManagerMacroChoice.png) 30 | 31 | ## Configuration 32 | You will need to configure your script to match your own settings. I have included some example settings from my own setup, but you'll likely want to make it match your own preferences. 33 | 34 | To customize the script, open the JavaScript file you just saved. You'll see this menu setup: 35 | ````js 36 | const menu = { 37 | "🧠 Learning & Skill Development": { // Sub-menu for Learning and Skill Development 38 | togglProjectName: "Learning & Skill Development", // Name of your corresponding Toggl project 39 | menuOptions: { 40 | "✍ Note Making": "Note Making", // Preset time entry. The left part is what's displayed, and the right part is what Toggl gets. 41 | "🃏 Spaced Repetition": "Spaced Repetition", // So for this one, I would see '🃏 Spaced Repetition' in my menu, but Toggl would receive 'Spaced Repetition' as the entry. 42 | "📖 Read Later Processing": "Read Later Processing", 43 | "👨‍💻 Computer Science & Software Engineering": "Computer Science & Software Engineering", 44 | } 45 | }, 46 | "🤴 Personal": { 47 | togglProjectName: "Personal", 48 | menuOptions: { 49 | "🏋️‍♂️ Exercise": "Exercise", 50 | "🧹 Chores": "Chores", 51 | "👨‍🔬 Systems Work": "Systems Work", 52 | "🌀 Weekly Review": "Weekly Review", 53 | "📆 Monthly Review": "Monthly Review", 54 | "✔ Planning": "Planning", 55 | } 56 | }, 57 | "👨‍🎓 School": { 58 | togglProjectName: "School", 59 | menuOptions: { 60 | "🧠 Machine Intelligence (MI)": "Machine Intelligence (MI)", 61 | "💾 Database Systems (DBS)": "Database Systems (DBS)", 62 | "🏃‍♂ Agile Software Engineering (ASE)": "Agile Software Engineering (ASE)", 63 | "💻 P5": "P5", 64 | } 65 | } 66 | }; 67 | ```` 68 | 69 | In the menu, there'll be 3 sub-menus with their own time entries. I have added some comments to explain the anatomy of the menu. 70 | 71 | You can customize it however you like. You can add more menus, remove menus, and so on. 72 | -------------------------------------------------------------------------------- /docs/docs/Examples/Macro_Zettelizer.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Zettelizer 3 | --- 4 | 5 | ![Zettelizer Demo](../Images/zettelizer_demo.gif) 6 | 7 | You can get the `.js` file for this userscript [here](./Attachments/zettelizer.js). 8 | To install it, you can follow the same process as in the [fetch tasks from Todoist example - with video](./Capture_FetchTasksFromTodoist.md). 9 | 10 | ## Setup 11 | You will need to define the folder you want the script to place the new notes in. 12 | 13 | This can be done on line 19, where it says ``const folder = "..."``. Change the text inside the `""` to match the desired folder path. 14 | 15 | Currently, the script _only_ looks for level 3 headers. This means headers with three pound symbols, like so ``### header``. 16 | 17 | You can freely change this. On line 29 it says ``if (heading.level === 3)``. You can change this to any other number, denoting the heading level desired. You can also, rather than checking for equality (`===`), check for other conditions, such as `heading.level >= 1`, which denotes headers of level 1 or greater. 18 | 19 | The script looks for headers in your active file with the desired level. 20 | If such a header is found, it will ignore the first 'word' (any sequence of characters - i.e., letters, numbers, symbols, etc - followed by a space). Then, it will create a file with a name containing the remaining text in the heading. 21 | 22 | In that file, it will link to the heading it created the file from. -------------------------------------------------------------------------------- /docs/docs/Examples/Template_AddAnInboxItem.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Template: Add an Inbox Item" 3 | --- 4 | 5 | Template Path: `bins/templates/Inbox Template.md` 6 | 7 | File Name Format: `{{DATE:YYYY-MM-DD-HH-mm-ss}} {{NAME}}` 8 | 9 | ![image](https://user-images.githubusercontent.com/29108628/121774925-fe72f380-cb84-11eb-8a4f-fd654d2d8c25.png) 10 | -------------------------------------------------------------------------------- /docs/docs/Images/AI_Assistant_Macro.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chhoumann/quickadd/ae003f88dfde67e333b0331772ff0ce7b7daf00e/docs/docs/Images/AI_Assistant_Macro.gif -------------------------------------------------------------------------------- /docs/docs/Images/AI_Assistant_Setup.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chhoumann/quickadd/ae003f88dfde67e333b0331772ff0ce7b7daf00e/docs/docs/Images/AI_Assistant_Setup.gif -------------------------------------------------------------------------------- /docs/docs/Images/Todoist-GetAllTasksFromProject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chhoumann/quickadd/ae003f88dfde67e333b0331772ff0ce7b7daf00e/docs/docs/Images/Todoist-GetAllTasksFromProject.png -------------------------------------------------------------------------------- /docs/docs/Images/TogglManager.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chhoumann/quickadd/ae003f88dfde67e333b0331772ff0ce7b7daf00e/docs/docs/Images/TogglManager.gif -------------------------------------------------------------------------------- /docs/docs/Images/TogglManagerMacro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chhoumann/quickadd/ae003f88dfde67e333b0331772ff0ce7b7daf00e/docs/docs/Images/TogglManagerMacro.png -------------------------------------------------------------------------------- /docs/docs/Images/TogglManagerMacroChoice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chhoumann/quickadd/ae003f88dfde67e333b0331772ff0ce7b7daf00e/docs/docs/Images/TogglManagerMacroChoice.png -------------------------------------------------------------------------------- /docs/docs/Images/longLatDemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chhoumann/quickadd/ae003f88dfde67e333b0331772ff0ce7b7daf00e/docs/docs/Images/longLatDemo.gif -------------------------------------------------------------------------------- /docs/docs/Images/moviescript.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chhoumann/quickadd/ae003f88dfde67e333b0331772ff0ce7b7daf00e/docs/docs/Images/moviescript.gif -------------------------------------------------------------------------------- /docs/docs/Images/moviescript_settings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chhoumann/quickadd/ae003f88dfde67e333b0331772ff0ce7b7daf00e/docs/docs/Images/moviescript_settings.jpg -------------------------------------------------------------------------------- /docs/docs/Images/readwise_template_choice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chhoumann/quickadd/ae003f88dfde67e333b0331772ff0ce7b7daf00e/docs/docs/Images/readwise_template_choice.png -------------------------------------------------------------------------------- /docs/docs/Images/script_with_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chhoumann/quickadd/ae003f88dfde67e333b0331772ff0ce7b7daf00e/docs/docs/Images/script_with_settings.png -------------------------------------------------------------------------------- /docs/docs/Images/zettelizer_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chhoumann/quickadd/ae003f88dfde67e333b0331772ff0ce7b7daf00e/docs/docs/Images/zettelizer_demo.gif -------------------------------------------------------------------------------- /docs/docs/InlineScripts.md: -------------------------------------------------------------------------------- 1 | # Inline scripts 2 | QuickAdd supports the usage of inline scripts in [Template choices](./Choices/TemplateChoice.md) and [Capture choices](./Choices/CaptureChoice.md). 3 | 4 | Inline scripts allow you to execute any JavaScript code you want. 5 | 6 | You are given the [QuickAdd API](./QuickAddAPI.md), just as with user scripts. In inline scripts, it is passed in as ``this``, as can be seen in the example below. 7 | 8 | ```` 9 | ```js quickadd 10 | const input = await this.quickAddApi.inputPrompt("✍"); 11 | return `Input given: ${input}`; 12 | ``` 13 | ```` 14 | 15 | When you are making an inline script, remember to write ``js quickadd`` and not just ``js`` when denoting the language - otherwise you're just inserting a code snippet. 16 | 17 | If you want to insert something, simply ``return`` it. The return type __must__ be a string -------------------------------------------------------------------------------- /docs/docs/ManualInstallation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Manual Installation 3 | --- 4 | 5 | 1. Go to [Releases](https://github.com/chhoumann/quickadd/releases) and download the ZIP file from the latest release. The one that looks like `quickadd-x.x.x.zip`. 6 | 2. This ZIP file should be extracted in your Obsidian plugins folder. If you don't know where that is, you can go to `Community Plugins` inside Obsidian. There is a folder icon on the right of `Installed Plugins`. Click that and it opens your plugins folder. 7 | 3. Extract the contents of the ZIP file there. 8 | 4. Now you should have a folder in plugins called 'quickadd' containing a `main.js` file, `manifest.json` file, and a `styles.css` file. 9 | -------------------------------------------------------------------------------- /docs/docs/Misc/AHK_OpenQuickAddFromDesktop.md: -------------------------------------------------------------------------------- 1 | --- 2 | hidden: true 3 | title: Open QuickAdd from your Desktop 4 | --- 5 | 6 | **UPDATE: A more reliable method is to use the [Global Hotkeys](https://github.com/mjessome/obsidian-global-hotkeys) plugin for Obsidian.** 7 | 8 | This is an [AutoHotkey](https://www.autohotkey.com/) script which unminimizes/focuses Obsidian and sends some keypresses to it. 9 | 10 | I've bound this to my QuickAdd activation hotkey, so this script automatically brings Obsidian to the front of my screen with QuickAdd open. 11 | 12 | ```ahk 13 | #SingleInstance, Force 14 | SendMode Input 15 | SetWorkingDir, %A_ScriptDir% 16 | SetTitleMatchMode, RegEx 17 | 18 | !^+g:: 19 | WinActivate, i) Obsidian 20 | ControlSend,, {CtrlDown}{AltDown}{ShiftDown}G{CtrlUp}{CtrlUp}{ShiftUp}, i)Obsidian 21 | Return 22 | ``` 23 | 24 | I'm using CTRL+SHIFT+ALT+G as my shortcut, both in Obsidian and for the AHK script to activate. I use a keyboard shortcut to send those keys (lol, I know - but it's to avoid potential conflicts). 25 | Here's a guide to what the `!^+` mean, and how you can customize it: https://www.autohotkey.com/docs/Hotkeys.htm 26 | 27 | #### Update 28 | 29 | If you are willing to install the `Obsidian Advanced URI` plugin, this script is much easier for you to use. 30 | 31 | ```ahk 32 | SendMode Input 33 | SetWorkingDir, %A_ScriptDir% 34 | SetTitleMatchMode, RegEx 35 | 36 | !^+g:: 37 | WinActivate, i) Obsidian 38 | 39 | Run "obsidian://advanced-uri?vault=&commandname=QuickAdd: Run QuickAdd" 40 | Return 41 | ``` 42 | 43 | Simply replace `` with the name of your vault. 44 | 45 | **This version is more reliable**, as the other one can fail to activate occasionally. 46 | 47 | It uses the same hotkey to activate as above (`CTRL+SHIFT+ALT+G`). If you wish to change it: 48 | 49 | - `!` means `Alt` 50 | - `^` means `Ctrl` 51 | - `+` means `Shift` 52 | 53 | So, you can replace the `!^+g` with any hotkey of your choosing. 54 | -------------------------------------------------------------------------------- /docs/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | title: Getting Started 4 | --- 5 | 6 | ## Installation 7 | 8 | **This plugin is in the community plugin browser in Obsidian**. You can search for it and install it there . 9 | 10 | You can also do a [manual installation](./ManualInstallation). 11 | 12 | ## First steps 13 | 14 | The first thing you'll want to do is add a new choice. A choice can be one of four types. 15 | 16 | - [Template Choice](./Choices/TemplateChoice) - Insert templates into your vault. Works together with Obsidian template syntax and popular _Templater_ plugin, augmenting them and adding more options. 17 | - [Capture Choice](./Choices/CaptureChoice) - Quick capture your manually written information and save it. Daily notes, work log, to-read-and-watch-later list, etc. 18 | - [Macro Choice](./Choices/MacroChoice) - Macros to augment your workflow. Use the full power of Javascript programming language and Obsidian functions to do anything your want. E.g. [create a personal movie database](./Examples/Macro_MovieAndSeriesScript) by writing a movie name and getting the movie notes fully customized and filled with correct film's up-to-date data. 19 | - [Multi Choice](./Choices/MultiChoice) - Folders to better organize the previous 3 choices. Usability feature, not a new functionality. 20 | 21 | In your choices, you can use [format syntax](./FormatSyntax), which is similar to the Obsidian template syntax. 22 | 23 | You could, for example, use `{{DATE}}` to get the current date. 24 | 25 | ## I want to... 26 | 27 | ### Be inspired 28 | 29 | Take a look at some examples... 30 | 31 | - [Capture: Add Journal Entry](docs/Examples/Capture_AddJournalEntry.md) 32 | - [Macro: Log book to daily journal](docs/Examples/Macro_LogBookToDailyJournal.md) 33 | - [Template: Add an Inbox Item](docs/Examples/Template_AddAnInboxItem.md) 34 | - [Macro: Move all notes with a tag to a certain folder](docs/Examples/Macro_MoveNotesWithATagToAFolder.md) 35 | - [Template: Automatically create a new book note with notes & highlights from Readwise](docs/Examples/Template_AutomaticBookNotesFromReadwise.md) 36 | - [Capture: Add a task to a Kanban board](docs/Examples/Capture_AddTaskToKanbanBoard.md) 37 | - [Macro: Easily change properties in your daily note (requires MetaEdit)](docs/Examples/Macro_ChangePropertyInDailyNotes.md) 38 | - [Capture: Fetch tasks from Todoist and capture to a file](docs/Examples/Capture_FetchTasksFromTodoist.md) 39 | - [Macro: Zettelizer - easily create new notes from headings while keeping the contents in the file](docs/Examples/Macro_Zettelizer.md) 40 | - [Macro: Obsidian Map View plugin helper - insert location from address](docs/Examples/Macro_AddLocationLongLatFromAddress.md) 41 | - [Macro: Toggl Manager - set preset Toggl Track time entries and start them from Obsidian](docs/Examples/Macro_TogglManager.md) 42 | - [How I Read Research Papers with Obsidian and Zotero](https://bagerbach.com/blog/how-i-read-research-papers-with-obsidian-and-zotero/) 43 | - [How I Import Literature Notes into Obsidian](https://bagerbach.com/blog/importing-source-notes-to-obsidian) 44 | - [Macro: Fetching movies and TV shows into your vault](docs/Examples/Macro_MovieAndSeriesScript.md) 45 | 46 | ### Create powerful scripts and macros to automate my workflow 47 | 48 | Take a look at the [QuickAdd API](docs/QuickAddAPI.md), [format syntax](docs/FormatSyntax.md), [inline scripts](docs/InlineScripts.md), and [macros](docs/Choices/MacroChoice.md). 49 | 50 | ### Use QuickAdd even when Obsidian is minimized / in the background 51 | 52 | You got it. Take a look at [this AutoHotKey script](./Misc/AHK_OpenQuickAddFromDesktop). 53 | -------------------------------------------------------------------------------- /docs/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Note: type annotations allow type checking and IDEs autocompletion 3 | 4 | const {themes} = require('prism-react-renderer'); 5 | const lightCodeTheme = themes.github; 6 | const darkCodeTheme = themes.dracula; 7 | 8 | /** @type {import('@docusaurus/types').Config} */ 9 | const config = { 10 | title: 'QuickAdd', 11 | tagline: 'Quickly add new pages or content to your vault.', 12 | url: 'https://quickadd.obsidian.guide', 13 | baseUrl: '/', 14 | onBrokenLinks: 'throw', 15 | onBrokenMarkdownLinks: 'warn', 16 | favicon: 'img/favicon.ico', 17 | 18 | // GitHub pages deployment config. 19 | // If you aren't using GitHub pages, you don't need these. 20 | organizationName: 'chhoumann', // Usually your GitHub org/user name. 21 | projectName: 'quickadd', // Usually your repo name. 22 | 23 | // Even if you don't use internalization, you can use this field to set useful 24 | // metadata like html lang. For example, if your site is Chinese, you may want 25 | // to replace "en" with "zh-Hans". 26 | i18n: { 27 | defaultLocale: 'en', 28 | locales: ['en'], 29 | }, 30 | 31 | presets: [ 32 | [ 33 | 'classic', 34 | /** @type {import('@docusaurus/preset-classic').Options} */ 35 | ({ 36 | docs: { 37 | sidebarPath: require.resolve('./sidebars.js'), 38 | // Please change this to your repo. 39 | // Remove this to remove the "edit this page" links. 40 | editUrl: 41 | 'https://github.com/chhoumann/quickadd/tree/master/docs/', 42 | }, 43 | blog: { 44 | showReadingTime: true, 45 | // Please change this to your repo. 46 | // Remove this to remove the "edit this page" links. 47 | // editUrl: 48 | // 'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/', 49 | }, 50 | theme: { 51 | customCss: require.resolve('./src/css/custom.css'), 52 | }, 53 | }), 54 | ], 55 | ], 56 | 57 | themeConfig: 58 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ 59 | ({ 60 | navbar: { 61 | title: 'QuickAdd', 62 | items: [ 63 | { 64 | type: 'doc', 65 | docId: 'index', 66 | position: 'left', 67 | label: '📚 Docs', 68 | }, 69 | { 70 | type: 'doc', 71 | docId: 'QuickAddAPI', 72 | position: 'left', 73 | label: '🔧 API', 74 | }, 75 | { 76 | to: '/docs/Examples/Macro_BookFinder', 77 | position: 'left', 78 | label: '💡 Examples', 79 | }, 80 | { 81 | href: 'https://github.com/chhoumann/quickadd', 82 | position: 'right', 83 | className: 'header-github-link', 84 | 'aria-label': 'GitHub repository', 85 | }, 86 | ], 87 | }, 88 | footer: { 89 | style: 'dark', 90 | links: [ 91 | { 92 | title: 'Docs', 93 | items: [ 94 | { 95 | label: 'Documentation', 96 | to: '/docs/', 97 | }, 98 | ], 99 | }, 100 | { 101 | title: 'Community', 102 | items: [ 103 | { 104 | label: 'Discussions', 105 | href: 'https://github.com/chhoumann/quickadd/discussions', 106 | }, 107 | ], 108 | }, 109 | ], 110 | }, 111 | prism: { 112 | theme: lightCodeTheme, 113 | darkTheme: darkCodeTheme, 114 | }, 115 | colorMode: { 116 | defaultMode: 'light', 117 | disableSwitch: false, 118 | respectPrefersColorScheme: true, 119 | }, 120 | }), 121 | 122 | themes: [ 123 | [ 124 | require.resolve("@easyops-cn/docusaurus-search-local"), 125 | { 126 | hashed: true, 127 | language: ["en"], 128 | highlightSearchTermsOnTargetPage: true, 129 | explicitSearchResultPath: true, 130 | docsRouteBasePath: "/docs", 131 | searchBarShortcutHint: false, 132 | }, 133 | ], 134 | ], 135 | }; 136 | 137 | module.exports = config; 138 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "3.8.0", 19 | "@docusaurus/preset-classic": "3.8.0", 20 | "@easyops-cn/docusaurus-search-local": "^0.49.2", 21 | "@mdx-js/react": "^3.1.0", 22 | "clsx": "^2.1.1", 23 | "prism-react-renderer": "^2.4.1", 24 | "react": "^19.1.0", 25 | "react-dom": "^19.1.0" 26 | }, 27 | "devDependencies": { 28 | "@docusaurus/module-type-aliases": "3.8.0", 29 | "@tsconfig/docusaurus": "^2.0.3", 30 | "typescript": "^5.8.3" 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.5%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | }, 44 | "engines": { 45 | "node": ">=18.0.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /docs/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | // @ts-check 13 | 14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 15 | const sidebars = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | // tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], 18 | 19 | // But you can create a sidebar manually 20 | /* 21 | tutorialSidebar: [ 22 | 'intro', 23 | 'hello', 24 | { 25 | type: 'category', 26 | label: 'Tutorial', 27 | items: ['tutorial-basics/create-a-document'], 28 | }, 29 | ], 30 | */ 31 | sidebar: [ 32 | { 33 | type: "doc", 34 | id: "index", 35 | label: "🏠 Getting Started", 36 | }, 37 | { 38 | type: "category", 39 | label: "📚 Core Concepts", 40 | collapsed: false, 41 | items: [ 42 | { 43 | type: "doc", 44 | id: "Choices/TemplateChoice", 45 | label: "📄 Template Choices", 46 | }, 47 | { 48 | type: "doc", 49 | id: "Choices/CaptureChoice", 50 | label: "⚡ Capture Choices", 51 | }, 52 | { 53 | type: "doc", 54 | id: "Choices/MacroChoice", 55 | label: "🤖 Macro Choices", 56 | }, 57 | { 58 | type: "doc", 59 | id: "Choices/MultiChoice", 60 | label: "📁 Multi Choices", 61 | }, 62 | ], 63 | }, 64 | { 65 | type: "category", 66 | label: "🔧 Features", 67 | items: [ 68 | { 69 | type: "doc", 70 | id: "FormatSyntax", 71 | label: "📝 Format Syntax", 72 | }, 73 | { 74 | type: "doc", 75 | id: "InlineScripts", 76 | label: "💻 Inline Scripts", 77 | }, 78 | { 79 | type: "doc", 80 | id: "AIAssistant", 81 | label: "🧠 AI Assistant", 82 | }, 83 | ], 84 | }, 85 | { 86 | type: "category", 87 | label: "💡 Examples", 88 | collapsed: true, 89 | items: [ 90 | { 91 | type: "category", 92 | label: "📝 Capture Examples", 93 | items: [ 94 | "Examples/Capture_AddJournalEntry", 95 | "Examples/Capture_AddTaskToKanbanBoard", 96 | "Examples/Capture_FetchTasksFromTodoist", 97 | ], 98 | }, 99 | { 100 | type: "category", 101 | label: "📄 Template Examples", 102 | items: [ 103 | "Examples/Template_AddAnInboxItem", 104 | "Examples/Template_AutomaticBookNotesFromReadwise", 105 | ], 106 | }, 107 | { 108 | type: "category", 109 | label: "🤖 Macro Examples", 110 | items: [ 111 | "Examples/Macro_BookFinder", 112 | "Examples/Macro_MovieAndSeriesScript", 113 | "Examples/Macro_LogBookToDailyJournal", 114 | "Examples/Macro_ChangePropertyInDailyNotes", 115 | "Examples/Macro_MoveNotesWithATagToAFolder", 116 | "Examples/Macro_Zettelizer", 117 | "Examples/Macro_AddLocationLongLatFromAddress", 118 | "Examples/Macro_TogglManager", 119 | ], 120 | }, 121 | ], 122 | }, 123 | { 124 | type: "category", 125 | label: "🚀 Advanced", 126 | collapsed: true, 127 | items: [ 128 | { 129 | type: "doc", 130 | id: "QuickAddAPI", 131 | label: "📖 QuickAdd API", 132 | }, 133 | { 134 | type: "doc", 135 | id: "Advanced/scriptsWithSettings", 136 | label: "⚙️ Scripts with Settings", 137 | }, 138 | { 139 | type: "doc", 140 | id: "Advanced/ObsidianUri", 141 | label: "🔗 Obsidian URI", 142 | }, 143 | ], 144 | }, 145 | { 146 | type: "category", 147 | label: "ℹ️ Other", 148 | collapsed: true, 149 | items: [ 150 | { 151 | type: "doc", 152 | id: "ManualInstallation", 153 | label: "💾 Manual Installation", 154 | }, 155 | { 156 | type: "autogenerated", 157 | dirName: "Misc", 158 | }, 159 | ], 160 | }, 161 | ], 162 | }; 163 | 164 | module.exports = sidebars; 165 | -------------------------------------------------------------------------------- /docs/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 6rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | background: var(--ifm-background-color); 12 | border-bottom: 1px solid var(--ifm-toc-border-color); 13 | } 14 | 15 | @media screen and (max-width: 996px) { 16 | .heroBanner { 17 | padding: 3rem 1rem; 18 | } 19 | } 20 | 21 | .buttons { 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | gap: 1rem; 26 | margin-top: 2rem; 27 | flex-wrap: wrap; 28 | } 29 | 30 | .hero__title { 31 | font-size: 3rem; 32 | margin-bottom: 1rem; 33 | font-weight: 700; 34 | } 35 | 36 | @media screen and (max-width: 996px) { 37 | .hero__title { 38 | font-size: 2.25rem; 39 | } 40 | } 41 | 42 | .features { 43 | display: flex; 44 | align-items: center; 45 | padding: 4rem 0; 46 | width: 100%; 47 | } 48 | -------------------------------------------------------------------------------- /docs/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chhoumann/quickadd/ae003f88dfde67e333b0331772ff0ce7b7daf00e/docs/static/.nojekyll -------------------------------------------------------------------------------- /docs/static/img/docusaurus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chhoumann/quickadd/ae003f88dfde67e333b0331772ff0ce7b7daf00e/docs/static/img/docusaurus.png -------------------------------------------------------------------------------- /docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chhoumann/quickadd/ae003f88dfde67e333b0331772ff0ce7b7daf00e/docs/static/img/favicon.ico -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@tsconfig/docusaurus/tsconfig.json", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /docs/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "installCommand": "bun install", 3 | "buildCommand": "bun run build", 4 | "framework": null 5 | } -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from "builtin-modules"; 4 | import esbuildSvelte from "esbuild-svelte"; 5 | import sveltePreprocess from "svelte-preprocess"; 6 | import { copy } from "esbuild-plugin-copy"; 7 | 8 | const banner = `/* 9 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 10 | if you want to view the source, please visit the github repository of this plugin 11 | */ 12 | `; 13 | 14 | const prod = process.argv[2] === "production"; 15 | const options = Object.freeze({ 16 | banner: { 17 | js: banner, 18 | }, 19 | entryPoints: ["src/main.ts", "src/styles.css"], 20 | bundle: true, 21 | minify: prod, 22 | external: [ 23 | "obsidian", 24 | "electron", 25 | "@codemirror/autocomplete", 26 | "@codemirror/closebrackets", 27 | "@codemirror/collab", 28 | "@codemirror/commands", 29 | "@codemirror/comment", 30 | "@codemirror/fold", 31 | "@codemirror/gutter", 32 | "@codemirror/highlight", 33 | "@codemirror/history", 34 | "@codemirror/language", 35 | "@codemirror/lint", 36 | "@codemirror/matchbrackets", 37 | "@codemirror/panel", 38 | "@codemirror/rangeset", 39 | "@codemirror/rectangular-selection", 40 | "@codemirror/search", 41 | "@codemirror/state", 42 | "@codemirror/stream-parser", 43 | "@codemirror/text", 44 | "@codemirror/tooltip", 45 | "@codemirror/view", 46 | "@lezer/common", 47 | "@lezer/highlight", 48 | "@lezer/lr", 49 | ...builtins, 50 | ], 51 | mainFields: ["svelte", "browser", "module", "main"], 52 | plugins: [ 53 | esbuildSvelte({ 54 | compilerOptions: { css: true }, 55 | preprocess: sveltePreprocess(), 56 | }), 57 | ...(!prod 58 | ? [ 59 | copy({ 60 | resolveFrom: "cwd", 61 | assets: { 62 | from: ["./styles.css"], 63 | to: [ 64 | // Path to dev's Obsidian CSS snippets folder, for hot-reload of CSS 65 | "/mnt/c/Users/chhou/Documents/dev/.obsidian/snippets/quickadd.css", 66 | ], 67 | }, 68 | }), 69 | ] 70 | : []), 71 | ], 72 | format: "cjs", 73 | target: "ES2020", 74 | logLevel: "info", 75 | sourcemap: prod ? false : "inline", 76 | treeShaking: true, 77 | outdir: ".", 78 | }); 79 | 80 | if (!prod) { 81 | const context = await esbuild.context(options); 82 | 83 | await context.watch(); 84 | } else { 85 | esbuild.build(options).catch(() => process.exit(1)); 86 | } 87 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "quickadd", 3 | "name": "QuickAdd", 4 | "version": "1.13.3", 5 | "minAppVersion": "1.6.0", 6 | "description": "Quickly add new pages or content to your vault.", 7 | "author": "Christian B. B. Houmann", 8 | "authorUrl": "https://bagerbach.com", 9 | "fundingUrl": "https://www.buymeacoffee.com/chhoumann", 10 | "helpUrl": "https://quickadd.obsidian.guide/docs/", 11 | "isDesktopOnly": false 12 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quickadd", 3 | "version": "1.13.3", 4 | "description": "Quickly add new pages or content to your vault.", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "lint": "eslint --ext .ts .", 9 | "build": "tsc -noEmit -skipLibCheck && bun lint && node esbuild.config.mjs production", 10 | "version": "node version-bump.mjs && git add manifest.json versions.json", 11 | "semantic-release": "semantic-release", 12 | "test": "vitest run --passWithNoTests" 13 | }, 14 | "keywords": [], 15 | "author": "Christian B. B. Houmann", 16 | "license": "MIT", 17 | "devDependencies": { 18 | "@fortawesome/free-regular-svg-icons": "6.4.0", 19 | "@fortawesome/free-solid-svg-icons": "6.4.0", 20 | "@popperjs/core": "^2.11.7", 21 | "@semantic-release/git": "^10.0.1", 22 | "@sveltejs/vite-plugin-svelte": "^2.2.0", 23 | "@testing-library/jest-dom": "^5.16.5", 24 | "@testing-library/svelte": "^3.2.2", 25 | "@types/node": "20.1.7", 26 | "@types/uuid": "9.0.1", 27 | "@typescript-eslint/eslint-plugin": "^5.59.6", 28 | "@typescript-eslint/parser": "^5.59.6", 29 | "cz-conventional-changelog": "^3.3.0", 30 | "esbuild": "^0.17.19", 31 | "esbuild-plugin-copy": "^2.1.1", 32 | "esbuild-svelte": "^0.7.3", 33 | "eslint": "^8.40.0", 34 | "jsdom": "^22.0.0", 35 | "obsidian": "^1.5.7-1", 36 | "semantic-release": "^21.0.2", 37 | "svelte": "^3.59.1", 38 | "svelte-awesome": "3.2.0", 39 | "svelte-check": "^3.3.2", 40 | "svelte-dnd-action": "0.9.22", 41 | "svelte-preprocess": "^5.0.3", 42 | "three-way-merge": "^0.1.0", 43 | "tslib": "^2.5.0", 44 | "typescript": "^5.0.4", 45 | "uuid": "9.0.0", 46 | "vite": "^4.3.7", 47 | "vitest": "^0.31.0" 48 | }, 49 | "dependencies": { 50 | "builtin-modules": "^3.3.0", 51 | "fuse.js": "6.6.2", 52 | "js-tiktoken": "^1.0.6", 53 | "zustand": "^4.3.8" 54 | }, 55 | "repository": { 56 | "type": "git", 57 | "url": "https://github.com/chhoumann/quickadd.git" 58 | }, 59 | "config": { 60 | "commitizen": { 61 | "path": "./node_modules/cz-conventional-changelog" 62 | } 63 | }, 64 | "release": { 65 | "tagFormat": "${version}", 66 | "branches": [ 67 | "master" 68 | ], 69 | "plugins": [ 70 | [ 71 | "@semantic-release/commit-analyzer", 72 | { 73 | "releaseRules": [ 74 | { 75 | "type": "chore", 76 | "release": "patch" 77 | } 78 | ] 79 | } 80 | ], 81 | "@semantic-release/release-notes-generator", 82 | [ 83 | "@semantic-release/npm", 84 | { 85 | "npmPublish": false 86 | } 87 | ], 88 | [ 89 | "@semantic-release/git", 90 | { 91 | "assets": [ 92 | "package.json", 93 | "package-lock.json", 94 | "manifest.json", 95 | "versions.json" 96 | ], 97 | "message": "release(version): Release ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 98 | } 99 | ], 100 | [ 101 | "@semantic-release/github", 102 | { 103 | "assets": [ 104 | { 105 | "path": "main.js", 106 | "label": "main.js" 107 | }, 108 | { 109 | "path": "manifest.json", 110 | "label": "manifest.json" 111 | }, 112 | { 113 | "path": "styles.css", 114 | "label": "styles.css" 115 | } 116 | ] 117 | } 118 | ] 119 | ] 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/IChoiceExecutor.ts: -------------------------------------------------------------------------------- 1 | import type IChoice from "./types/choices/IChoice"; 2 | 3 | export interface IChoiceExecutor { 4 | execute(choice: IChoice): Promise; 5 | variables: Map; 6 | } 7 | -------------------------------------------------------------------------------- /src/ai/OpenAIModelParameters.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_TOP_P = 1; 2 | export const DEFAULT_TEMPERATURE = 1; 3 | export const DEFAULT_FREQUENCY_PENALTY = 0; 4 | export const DEFAULT_PRESENCE_PENALTY = 0; 5 | 6 | export interface OpenAIModelParameters { 7 | /** 8 | * What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. (source: OpenAI) 9 | */ 10 | temperature: number; 11 | /** 12 | * An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. (source: OpenAI) 13 | */ 14 | top_p: number; 15 | /** 16 | * Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim. (source: OpenAI) 17 | */ 18 | frequency_penalty: number; 19 | /** 20 | * Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics. (source: OpenAI) 21 | */ 22 | presence_penalty: number; 23 | } 24 | -------------------------------------------------------------------------------- /src/ai/Provider.ts: -------------------------------------------------------------------------------- 1 | export interface AIProvider { 2 | name: string; 3 | endpoint: string; 4 | apiKey: string; 5 | models: Model[]; 6 | } 7 | 8 | export interface Model { 9 | name: string; 10 | maxTokens: number; 11 | } 12 | 13 | const OpenAIProvider: AIProvider = { 14 | name: "OpenAI", 15 | endpoint: "https://api.openai.com/v1", 16 | apiKey: "", 17 | models: [ 18 | { 19 | name: "text-davinci-003", 20 | maxTokens: 4096, 21 | }, 22 | { 23 | name: "gpt-3.5-turbo", 24 | maxTokens: 4096, 25 | }, 26 | { 27 | name: "gpt-3.5-turbo-16k", 28 | maxTokens: 16384, 29 | }, 30 | { 31 | name: "gpt-3.5-turbo-1106", 32 | maxTokens: 16385, 33 | }, 34 | { 35 | name: "gpt-4", 36 | maxTokens: 8192, 37 | }, 38 | { 39 | name: "gpt-4-32k", 40 | maxTokens: 32768, 41 | }, 42 | { 43 | name: "gpt-4-1106-preview", 44 | maxTokens: 128000, 45 | }, 46 | { 47 | name: "gpt-4-turbo", 48 | maxTokens: 128000, 49 | }, 50 | { 51 | name: "gpt-4o", 52 | maxTokens: 128000, 53 | }, 54 | { 55 | name: "gpt-4o-mini", 56 | maxTokens: 128000, 57 | }, 58 | ], 59 | }; 60 | 61 | 62 | export const DefaultProviders: AIProvider[] = [ 63 | OpenAIProvider, 64 | ]; 65 | -------------------------------------------------------------------------------- /src/ai/aiHelpers.ts: -------------------------------------------------------------------------------- 1 | import { settingsStore } from "src/settingsStore"; 2 | 3 | export function getModelNames() { 4 | const aiSettings = settingsStore.getState().ai; 5 | 6 | return aiSettings.providers 7 | .flatMap((provider) => provider.models) 8 | .map((model) => model.name); 9 | } 10 | 11 | export function getModelByName(model: string) { 12 | const aiSettings = settingsStore.getState().ai; 13 | 14 | return aiSettings.providers 15 | .flatMap((provider) => provider.models) 16 | .find((m) => m.name === model); 17 | } 18 | 19 | export function getModelMaxTokens(model: string) { 20 | const aiSettings = settingsStore.getState().ai; 21 | 22 | const modelData = aiSettings.providers 23 | .flatMap((provider) => provider.models) 24 | .find((m) => m.name === model); 25 | 26 | if (modelData) { 27 | return modelData.maxTokens; 28 | } 29 | 30 | throw new Error(`Model ${model} not found with any provider.`); 31 | } 32 | 33 | export function getModelProvider(modelName: string) { 34 | const aiSettings = settingsStore.getState().ai; 35 | 36 | return aiSettings.providers.find((provider) => 37 | provider.models.some((m) => m.name === modelName) 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/ai/makeNoticeHandler.ts: -------------------------------------------------------------------------------- 1 | import { Notice } from "obsidian"; 2 | import { log } from "src/logger/logManager"; 3 | 4 | const noticeMsg = (task: string, message: string) => 5 | `Assistant is ${task}.${message ? `\n\n${message}` : ""}`; 6 | 7 | interface NoticeHandler { 8 | setMessage: (status: string, msg: string) => void; 9 | hide: () => void; 10 | } 11 | export function makeNoticeHandler(showMessages: boolean): NoticeHandler { 12 | if (showMessages) { 13 | const n = new Notice(noticeMsg("starting", ""), 1000000); 14 | 15 | return { 16 | setMessage: (status: string, msg: string) => { 17 | n.setMessage(noticeMsg(status, msg)); 18 | }, 19 | hide: () => n.hide(), 20 | }; 21 | } 22 | 23 | return { 24 | setMessage: (status: string, msg: string) => { 25 | log.logMessage(`(${status}) ${msg}`); 26 | }, 27 | hide: () => { }, 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/ai/preventCursorChange.ts: -------------------------------------------------------------------------------- 1 | export function preventCursorChange(): () => void { 2 | const cursor = app.workspace.activeEditor?.editor?.getCursor(); 3 | const selection = app.workspace.activeEditor?.editor?.listSelections(); 4 | 5 | return () => { 6 | if (cursor) app.workspace.activeEditor?.editor?.setCursor(cursor); 7 | if (selection) 8 | app.workspace.activeEditor?.editor?.setSelections(selection); 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/choiceExecutor.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "obsidian"; 2 | import type QuickAdd from "./main"; 3 | import type IChoice from "./types/choices/IChoice"; 4 | import type ITemplateChoice from "./types/choices/ITemplateChoice"; 5 | import type ICaptureChoice from "./types/choices/ICaptureChoice"; 6 | import type IMacroChoice from "./types/choices/IMacroChoice"; 7 | import { TemplateChoiceEngine } from "./engine/TemplateChoiceEngine"; 8 | import { CaptureChoiceEngine } from "./engine/CaptureChoiceEngine"; 9 | import { MacroChoiceEngine } from "./engine/MacroChoiceEngine"; 10 | import type { IChoiceExecutor } from "./IChoiceExecutor"; 11 | import type IMultiChoice from "./types/choices/IMultiChoice"; 12 | import ChoiceSuggester from "./gui/suggesters/choiceSuggester"; 13 | 14 | export class ChoiceExecutor implements IChoiceExecutor { 15 | public variables: Map = new Map(); 16 | 17 | constructor(private app: App, private plugin: QuickAdd) {} 18 | 19 | async execute(choice: IChoice): Promise { 20 | switch (choice.type) { 21 | case "Template": { 22 | const templateChoice: ITemplateChoice = 23 | choice as ITemplateChoice; 24 | await this.onChooseTemplateType(templateChoice); 25 | break; 26 | } 27 | case "Capture": { 28 | const captureChoice: ICaptureChoice = choice as ICaptureChoice; 29 | await this.onChooseCaptureType(captureChoice); 30 | break; 31 | } 32 | case "Macro": { 33 | const macroChoice: IMacroChoice = choice as IMacroChoice; 34 | await this.onChooseMacroType(macroChoice); 35 | break; 36 | } 37 | case "Multi": { 38 | const multiChoice: IMultiChoice = choice as IMultiChoice; 39 | this.onChooseMultiType(multiChoice); 40 | break; 41 | } 42 | default: 43 | break; 44 | } 45 | } 46 | 47 | private async onChooseTemplateType( 48 | templateChoice: ITemplateChoice 49 | ): Promise { 50 | await new TemplateChoiceEngine( 51 | this.app, 52 | this.plugin, 53 | templateChoice, 54 | this 55 | ).run(); 56 | } 57 | 58 | private async onChooseCaptureType(captureChoice: ICaptureChoice) { 59 | await new CaptureChoiceEngine( 60 | this.app, 61 | this.plugin, 62 | captureChoice, 63 | this 64 | ).run(); 65 | } 66 | 67 | private async onChooseMacroType(macroChoice: IMacroChoice) { 68 | const macroEngine = new MacroChoiceEngine( 69 | this.app, 70 | this.plugin, 71 | macroChoice, 72 | this.plugin.settings.macros, 73 | this, 74 | this.variables 75 | ); 76 | await macroEngine.run(); 77 | 78 | Object.entries(macroEngine.params.variables).forEach(([key, value]) => { 79 | this.variables.set(key, value as string); 80 | }); 81 | } 82 | 83 | private onChooseMultiType(multiChoice: IMultiChoice) { 84 | ChoiceSuggester.Open(this.plugin, multiChoice.choices, this); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/engine/QuickAddChoiceEngine.ts: -------------------------------------------------------------------------------- 1 | import type IChoice from "../types/choices/IChoice"; 2 | import { QuickAddEngine } from "./QuickAddEngine"; 3 | 4 | export abstract class QuickAddChoiceEngine extends QuickAddEngine { 5 | abstract choice: IChoice; 6 | } 7 | -------------------------------------------------------------------------------- /src/engine/QuickAddEngine.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "obsidian"; 2 | import { TFile, TFolder } from "obsidian"; 3 | import { MARKDOWN_FILE_EXTENSION_REGEX } from "../constants"; 4 | import { log } from "../logger/logManager"; 5 | 6 | export abstract class QuickAddEngine { 7 | public app: App; 8 | 9 | protected constructor(app: App) { 10 | this.app = app; 11 | } 12 | 13 | public abstract run(): void; 14 | 15 | protected async createFolder(folder: string): Promise { 16 | const folderExists = await this.app.vault.adapter.exists(folder); 17 | 18 | if (!folderExists) { 19 | await this.app.vault.createFolder(folder); 20 | } 21 | } 22 | 23 | protected normalizeMarkdownFilePath( 24 | folderPath: string, 25 | fileName: string 26 | ): string { 27 | const actualFolderPath: string = folderPath ? `${folderPath}/` : ""; 28 | const formattedFileName: string = fileName.replace( 29 | MARKDOWN_FILE_EXTENSION_REGEX, 30 | "" 31 | ); 32 | return `${actualFolderPath}${formattedFileName}.md`; 33 | } 34 | 35 | protected async fileExists(filePath: string): Promise { 36 | return await this.app.vault.adapter.exists(filePath); 37 | } 38 | 39 | protected getFileByPath(filePath: string): TFile { 40 | const file = this.app.vault.getAbstractFileByPath(filePath); 41 | 42 | if (!file) { 43 | log.logError(`${filePath} not found`); 44 | throw new Error(`${filePath} not found`); 45 | } 46 | 47 | if (file instanceof TFolder) { 48 | log.logError(`${filePath} found but it's a folder`); 49 | throw new Error(`${filePath} found but it's a folder`); 50 | } 51 | 52 | if (!(file instanceof TFile)) 53 | throw new Error(`${filePath} is not a file`); 54 | 55 | return file; 56 | } 57 | 58 | protected async createFileWithInput( 59 | filePath: string, 60 | fileContent: string 61 | ): Promise { 62 | const dirMatch = filePath.match(/(.*)[/\\]/); 63 | let dirName = ""; 64 | if (dirMatch) dirName = dirMatch[1]; 65 | 66 | const dir = app.vault.getAbstractFileByPath(dirName); 67 | 68 | if (!dir || !(dir instanceof TFolder)) { 69 | await this.createFolder(dirName); 70 | 71 | } 72 | 73 | return await this.app.vault.create(filePath, fileContent); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/engine/SingleInlineScriptEngine.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "obsidian"; 2 | import type QuickAdd from "../main"; 3 | import type { IChoiceExecutor } from "../IChoiceExecutor"; 4 | import { MacroChoiceEngine } from "./MacroChoiceEngine"; 5 | 6 | export class SingleInlineScriptEngine extends MacroChoiceEngine { 7 | constructor( 8 | app: App, 9 | plugin: QuickAdd, 10 | choiceExecutor: IChoiceExecutor, 11 | variables: Map 12 | ) { 13 | //@ts-ignore 14 | super(app, plugin, null, null, choiceExecutor, variables); 15 | } 16 | 17 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 18 | public async runAndGetOutput(code: string): Promise { 19 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access 20 | const AsyncFunction = Object.getPrototypeOf( 21 | async function () {} 22 | ).constructor; 23 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call 24 | const userCode = new AsyncFunction(code); 25 | 26 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call 27 | return await userCode.bind(this.params, this).call(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/engine/SingleMacroEngine.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "obsidian"; 2 | import type { IMacro } from "../types/macros/IMacro"; 3 | import { MacroChoiceEngine } from "./MacroChoiceEngine"; 4 | import type QuickAdd from "../main"; 5 | import type { IChoiceExecutor } from "../IChoiceExecutor"; 6 | import { getUserScriptMemberAccess } from "../utilityObsidian"; 7 | import { log } from "../logger/logManager"; 8 | 9 | export class SingleMacroEngine extends MacroChoiceEngine { 10 | private memberAccess: string[]; 11 | 12 | constructor( 13 | app: App, 14 | plugin: QuickAdd, 15 | macros: IMacro[], 16 | choiceExecutor: IChoiceExecutor, 17 | variables: Map 18 | ) { 19 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 20 | super(app, plugin, null!, macros, choiceExecutor, variables); 21 | } 22 | 23 | public async runAndGetOutput(macroName: string): Promise { 24 | const { basename, memberAccess } = getUserScriptMemberAccess(macroName); 25 | const macro = this.macros.find((macro) => macro.name === basename); 26 | if (!macro) { 27 | log.logError(`macro '${macroName}' does not exist.`); 28 | throw new Error(`macro '${macroName}' does not exist.`); 29 | } 30 | 31 | if (memberAccess && memberAccess.length > 0) { 32 | this.memberAccess = memberAccess; 33 | } 34 | 35 | await this.executeCommands(macro.commands); 36 | return this.output as string; 37 | } 38 | 39 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 40 | protected override async onExportIsObject(obj: any): Promise { 41 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 42 | if (!this.memberAccess) return await super.onExportIsObject(obj); 43 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 44 | let newObj = obj; 45 | this.memberAccess.forEach((key) => { 46 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access 47 | newObj = newObj[key]; 48 | }); 49 | 50 | await this.userScriptDelegator(newObj); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/engine/SingleTemplateEngine.ts: -------------------------------------------------------------------------------- 1 | import { TemplateEngine } from "./TemplateEngine"; 2 | import type { App } from "obsidian"; 3 | import type QuickAdd from "../main"; 4 | import type { IChoiceExecutor } from "../IChoiceExecutor"; 5 | import { log } from "../logger/logManager"; 6 | 7 | export class SingleTemplateEngine extends TemplateEngine { 8 | constructor( 9 | app: App, 10 | plugin: QuickAdd, 11 | private templatePath: string, 12 | choiceExecutor?: IChoiceExecutor 13 | ) { 14 | super(app, plugin, choiceExecutor); 15 | } 16 | public async run(): Promise { 17 | let templateContent: string = await this.getTemplateContent( 18 | this.templatePath 19 | ); 20 | if (!templateContent) { 21 | log.logError(`Template ${this.templatePath} not found.`); 22 | } 23 | 24 | templateContent = await this.formatter.formatFileContent( 25 | templateContent 26 | ); 27 | 28 | return templateContent; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/engine/StartupMacroEngine.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "obsidian"; 2 | import type { IMacro } from "../types/macros/IMacro"; 3 | import { MacroChoiceEngine } from "./MacroChoiceEngine"; 4 | import type QuickAdd from "../main"; 5 | import type { IChoiceExecutor } from "../IChoiceExecutor"; 6 | 7 | export class StartupMacroEngine extends MacroChoiceEngine { 8 | constructor( 9 | app: App, 10 | plugin: QuickAdd, 11 | macros: IMacro[], 12 | choiceExecutor: IChoiceExecutor 13 | ) { 14 | //@ts-ignore 15 | super(app, plugin, null, macros, choiceExecutor, null); 16 | } 17 | 18 | // eslint-disable-next-line @typescript-eslint/require-await 19 | async run(): Promise { 20 | this.macros.forEach((macro) => { 21 | if (macro.runOnStartup) { 22 | void this.executeCommands(macro.commands); 23 | } 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/formatters/fileNameDisplayFormatter.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/require-await */ 2 | import { Formatter } from "./formatter"; 3 | import type { App } from "obsidian"; 4 | import { getNaturalLanguageDates } from "../utilityObsidian"; 5 | 6 | export class FileNameDisplayFormatter extends Formatter { 7 | constructor(private app: App) { 8 | super(); 9 | } 10 | 11 | public async format(input: string): Promise { 12 | let output: string = input; 13 | 14 | output = await this.replaceMacrosInString(output); 15 | output = this.replaceDateInString(output); 16 | output = this.replaceTimeInString(output); 17 | output = await this.replaceValueInString(output); 18 | output = await this.replaceDateVariableInString(output); 19 | output = await this.replaceVariableInString(output); 20 | output = await this.replaceFieldVarInString(output); 21 | 22 | return `File Name: ${output}`; 23 | } 24 | protected promptForValue(header?: string): string { 25 | return `FileName`; 26 | } 27 | 28 | protected getVariableValue(variableName: string): string { 29 | return variableName; 30 | } 31 | 32 | protected getCurrentFileLink() { 33 | return this.app.workspace.getActiveFile()?.path ?? ""; 34 | } 35 | 36 | protected getNaturalLanguageDates() { 37 | return getNaturalLanguageDates(this.app); 38 | } 39 | 40 | protected suggestForValue(suggestedValues: string[]) { 41 | return "_suggest_"; 42 | } 43 | 44 | protected promptForMathValue(): Promise { 45 | return Promise.resolve("_math_"); 46 | } 47 | 48 | protected getMacroValue(macroName: string) { 49 | return `_macro: ${macroName}`; 50 | } 51 | 52 | protected async promptForVariable(variableName: string): Promise { 53 | return `_${variableName}_`; 54 | } 55 | 56 | protected async getTemplateContent(templatePath: string): Promise { 57 | return `/${templatePath}/`; 58 | } 59 | 60 | protected async getSelectedText(): Promise { 61 | return "_selected_"; 62 | } 63 | 64 | protected suggestForField(variableName: string) { 65 | return `_field: ${variableName}_`; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/formatters/formatDisplayFormatter.ts: -------------------------------------------------------------------------------- 1 | import { Formatter } from "./formatter"; 2 | import type { App } from "obsidian"; 3 | import { getNaturalLanguageDates } from "../utilityObsidian"; 4 | import type QuickAdd from "../main"; 5 | import { SingleTemplateEngine } from "../engine/SingleTemplateEngine"; 6 | 7 | export class FormatDisplayFormatter extends Formatter { 8 | constructor(private app: App, private plugin: QuickAdd) { 9 | super(); 10 | } 11 | 12 | public async format(input: string): Promise { 13 | let output: string = input; 14 | 15 | output = this.replaceDateInString(output); 16 | output = this.replaceTimeInString(output); 17 | output = await this.replaceValueInString(output); 18 | output = await this.replaceDateVariableInString(output); 19 | output = await this.replaceVariableInString(output); 20 | output = await this.replaceLinkToCurrentFileInString(output); 21 | output = await this.replaceMacrosInString(output); 22 | output = await this.replaceTemplateInString(output); 23 | output = await this.replaceFieldVarInString(output); 24 | output = this.replaceLinebreakInString(output); 25 | 26 | return output; 27 | } 28 | protected promptForValue(header?: string): string { 29 | return "_value_"; 30 | } 31 | 32 | protected getVariableValue(variableName: string): string { 33 | return variableName; 34 | } 35 | 36 | protected getCurrentFileLink() { 37 | return this.app.workspace.getActiveFile()?.path ?? "_noPageOpen_"; 38 | } 39 | 40 | protected getNaturalLanguageDates() { 41 | return getNaturalLanguageDates(this.app); 42 | } 43 | 44 | protected suggestForValue(suggestedValues: string[]) { 45 | return "_suggest_"; 46 | } 47 | 48 | protected getMacroValue(macroName: string) { 49 | return `_macro: ${macroName}_`; 50 | } 51 | 52 | protected promptForMathValue(): Promise { 53 | return Promise.resolve("_math_"); 54 | } 55 | 56 | protected promptForVariable(variableName: string): Promise { 57 | return Promise.resolve(`${variableName}_`); 58 | } 59 | 60 | protected async getTemplateContent(templatePath: string): Promise { 61 | try { 62 | return await new SingleTemplateEngine( 63 | this.app, 64 | this.plugin, 65 | templatePath, 66 | undefined 67 | ).run(); 68 | } catch (e) { 69 | return `Template (not found): ${templatePath}`; 70 | } 71 | } 72 | 73 | // eslint-disable-next-line @typescript-eslint/require-await 74 | protected async getSelectedText(): Promise { 75 | return "_selected_"; 76 | } 77 | 78 | protected async suggestForField(variableName: string) { 79 | return Promise.resolve(`_field: ${variableName}_`); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from "obsidian"; 2 | 3 | declare module "obsidian" { 4 | interface App { 5 | plugins: { 6 | plugins: { 7 | [pluginId: string]: Plugin & { 8 | [pluginImplementations: string]: unknown; 9 | }; 10 | }; 11 | enablePlugin: (id: string) => Promise; 12 | disablePlugin: (id: string) => Promise; 13 | }; 14 | internalPlugins: { 15 | plugins: { 16 | [pluginId: string]: Plugin & { 17 | [pluginImplementations: string]: unknown; 18 | }; 19 | }; 20 | enablePlugin: (id: string) => Promise; 21 | disablePlugin: (id: string) => Promise; 22 | }; 23 | commands: { 24 | commands: { 25 | [commandName: string]: (...args: unknown[]) => Promise; 26 | }, 27 | editorCommands: { 28 | [commandName: string]: (...args: unknown[]) => Promise; 29 | }, 30 | findCommand: (commandId: string) => Command; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/gui/ChoiceBuilder/FolderList.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 | {#each folders as folder, i} 14 |
15 | {folder} 16 | 17 | deleteFolder(folder)} class="clickable"> 18 | 19 | 20 |
21 | {/each} 22 |
23 | 24 | -------------------------------------------------------------------------------- /src/gui/ChoiceBuilder/choiceBuilder.ts: -------------------------------------------------------------------------------- 1 | import type { App, Setting } from "obsidian"; 2 | import { Modal } from "obsidian"; 3 | import type IChoice from "../../types/choices/IChoice"; 4 | import type { SvelteComponent } from "svelte"; 5 | import GenericInputPrompt from "../GenericInputPrompt/GenericInputPrompt"; 6 | import { log } from "../../logger/logManager"; 7 | import { GenericTextSuggester } from "../suggesters/genericTextSuggester"; 8 | 9 | export abstract class ChoiceBuilder extends Modal { 10 | private resolvePromise: (input: IChoice) => void; 11 | private rejectPromise: (reason?: unknown) => void; 12 | private input: IChoice; 13 | public waitForClose: Promise; 14 | abstract choice: IChoice; 15 | private didSubmit = false; 16 | protected svelteElements: SvelteComponent[] = []; 17 | 18 | protected constructor(app: App) { 19 | super(app); 20 | 21 | this.waitForClose = new Promise((resolve, reject) => { 22 | this.resolvePromise = resolve; 23 | this.rejectPromise = reject; 24 | }); 25 | 26 | this.containerEl.addClass("quickAddModal"); 27 | this.open(); 28 | } 29 | 30 | protected abstract display(): unknown; 31 | 32 | protected reload() { 33 | this.contentEl.empty(); 34 | this.display(); 35 | } 36 | 37 | protected addFileSearchInputToSetting( 38 | setting: Setting, 39 | value: string, 40 | onChangeCallback: (value: string) => void 41 | ): void { 42 | setting.addSearch((searchComponent) => { 43 | searchComponent.setValue(value); 44 | searchComponent.setPlaceholder("File path"); 45 | 46 | const markdownFiles: string[] = this.app.vault 47 | .getMarkdownFiles() 48 | .map((f) => f.path); 49 | new GenericTextSuggester( 50 | this.app, 51 | searchComponent.inputEl, 52 | markdownFiles 53 | ); 54 | 55 | searchComponent.onChange(onChangeCallback); 56 | }); 57 | 58 | return; 59 | } 60 | 61 | protected addCenteredChoiceNameHeader(choice: IChoice): void { 62 | const headerEl: HTMLHeadingElement = this.contentEl.createEl("h2", { 63 | cls: "choiceNameHeader", 64 | }); 65 | headerEl.setText(choice.name); 66 | 67 | // eslint-disable-next-line @typescript-eslint/no-misused-promises 68 | headerEl.addEventListener("click", async (ev) => { 69 | try { 70 | const newName: string = await GenericInputPrompt.Prompt( 71 | this.app, 72 | choice.name, 73 | "Choice name", 74 | choice.name 75 | ); 76 | if (newName !== choice.name) { 77 | choice.name = newName; 78 | headerEl.setText(newName); 79 | } 80 | } catch (e) { 81 | log.logMessage(`No new name given for ${choice.name}`); 82 | } 83 | }); 84 | } 85 | 86 | onClose() { 87 | super.onClose(); 88 | this.resolvePromise(this.choice); 89 | this.svelteElements.forEach((el) => { 90 | if (el && el.$destroy) el.$destroy(); 91 | }); 92 | 93 | if (!this.didSubmit) this.rejectPromise("No answer given."); 94 | else this.resolvePromise(this.input); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/gui/GenericCheckboxPrompt/genericCheckboxPrompt.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "obsidian"; 2 | import { ButtonComponent, Modal, ToggleComponent } from "obsidian"; 3 | 4 | export default class GenericCheckboxPrompt extends Modal { 5 | private resolvePromise: (value: string[]) => void; 6 | private rejectPromise: (reason?: unknown) => void; 7 | public promise: Promise; 8 | private resolved: boolean; 9 | private _selectedItems: string[]; 10 | 11 | public static Open(app: App, items: string[], selectedItems?: string[]) { 12 | const newSuggester = new GenericCheckboxPrompt( 13 | app, 14 | items, 15 | selectedItems 16 | ); 17 | return newSuggester.promise; 18 | } 19 | 20 | public constructor( 21 | app: App, 22 | private items: string[], 23 | readonly selectedItems: string[] = [] 24 | ) { 25 | super(app); 26 | // This clones the item so that we don't get any unexpected modifications of the 27 | // arguments 28 | this._selectedItems = [...selectedItems]; 29 | 30 | this.promise = new Promise((resolve, reject) => { 31 | this.resolvePromise = resolve; 32 | this.rejectPromise = reject; 33 | }); 34 | 35 | this.display(); 36 | this.open(); 37 | } 38 | 39 | private display() { 40 | this.contentEl.empty(); 41 | this.containerEl.addClass("quickAddModal", "checkboxPrompt"); 42 | this.addCheckboxRows(); 43 | this.addSubmitButton(); 44 | } 45 | 46 | onClose() { 47 | super.onClose(); 48 | 49 | if (!this.resolved) this.rejectPromise("no input given."); 50 | } 51 | 52 | private addCheckboxRows() { 53 | const rowContainer: HTMLDivElement = this.contentEl.createDiv( 54 | "checkboxRowContainer" 55 | ); 56 | this.items.forEach((item) => this.addCheckboxRow(item, rowContainer)); 57 | } 58 | 59 | private addCheckboxRow(item: string, container: HTMLDivElement) { 60 | const checkboxRow: HTMLDivElement = container.createDiv("checkboxRow"); 61 | 62 | checkboxRow.createEl("span", { 63 | text: item, 64 | }); 65 | const checkbox: ToggleComponent = new ToggleComponent(checkboxRow); 66 | checkbox 67 | .setTooltip(`Toggle ${item}`) 68 | .setValue(this._selectedItems.contains(item)) 69 | .onChange((value) => { 70 | if (value) this._selectedItems.push(item); 71 | else { 72 | const index = this._selectedItems.findIndex( 73 | (value) => item === value 74 | ); 75 | this._selectedItems.splice(index, 1); 76 | } 77 | }); 78 | } 79 | 80 | private addSubmitButton() { 81 | const submitButtonContainer: HTMLDivElement = this.contentEl.createDiv( 82 | "submitButtonContainer" 83 | ); 84 | const submitButton: ButtonComponent = new ButtonComponent( 85 | submitButtonContainer 86 | ); 87 | 88 | submitButton 89 | .setButtonText("Submit") 90 | .setCta() 91 | .onClick((evt) => { 92 | this.resolved = true; 93 | this.resolvePromise(this._selectedItems); 94 | 95 | this.close(); 96 | }); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/gui/GenericInfoDialog/GenericInfoDialog.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "obsidian"; 2 | import { ButtonComponent, Modal } from "obsidian"; 3 | 4 | export default class GenericInfoDialog extends Modal { 5 | private resolvePromise: () => void; 6 | public waitForClose: Promise; 7 | 8 | public static Show( 9 | app: App, 10 | header: string, 11 | text: string[] | string 12 | ): Promise { 13 | const newPromptModal = new GenericInfoDialog(app, header, text); 14 | return newPromptModal.waitForClose; 15 | } 16 | 17 | private constructor( 18 | app: App, 19 | private header: string, 20 | private text: string[] | string 21 | ) { 22 | super(app); 23 | 24 | this.waitForClose = new Promise((resolve) => { 25 | this.resolvePromise = resolve; 26 | }); 27 | 28 | this.open(); 29 | this.display(); 30 | } 31 | 32 | private display() { 33 | this.contentEl.empty(); 34 | this.titleEl.textContent = this.header; 35 | 36 | if (String.isString(this.text)) 37 | this.contentEl.createEl("p", { text: this.text }); 38 | else if (Array.isArray(this.text)) 39 | this.text.forEach((line) => 40 | this.contentEl.createEl("p", { text: line }) 41 | ); 42 | 43 | const buttonsDiv = this.contentEl.createDiv(); 44 | 45 | const noButton = new ButtonComponent(buttonsDiv) 46 | .setButtonText("OK") 47 | .onClick(() => this.close()); 48 | 49 | Object.assign(buttonsDiv.style, { 50 | display: "flex", 51 | justifyContent: "flex-end", 52 | } as Partial); 53 | 54 | noButton.buttonEl.focus(); 55 | } 56 | 57 | onClose() { 58 | super.onClose(); 59 | this.resolvePromise(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/gui/GenericSuggester/genericSuggester.ts: -------------------------------------------------------------------------------- 1 | import { FuzzySuggestModal } from "obsidian"; 2 | import type { FuzzyMatch , App} from "obsidian"; 3 | 4 | export default class GenericSuggester extends FuzzySuggestModal { 5 | private resolvePromise: (value: T) => void; 6 | private rejectPromise: (reason?: unknown) => void; 7 | public promise: Promise; 8 | private resolved: boolean; 9 | 10 | public static Suggest(app: App, displayItems: string[], items: T[]) { 11 | const newSuggester = new GenericSuggester(app, displayItems, items); 12 | return newSuggester.promise; 13 | } 14 | 15 | public constructor( 16 | app: App, 17 | private displayItems: string[], 18 | private items: T[] 19 | ) { 20 | super(app); 21 | 22 | this.promise = new Promise((resolve, reject) => { 23 | this.resolvePromise = resolve; 24 | this.rejectPromise = reject; 25 | }); 26 | 27 | this.inputEl.addEventListener("keydown", (event: KeyboardEvent) => { 28 | // chooser is undocumented & not officially a part of the Obsidian API, hence the precautions in using it. 29 | if (event.code !== "Tab" || !("chooser" in this)) { 30 | return; 31 | } 32 | 33 | const { values, selectedItem } = this.chooser as { 34 | values: { 35 | item: string; 36 | match: { score: number; matches: unknown[]; }; 37 | }[]; 38 | selectedItem: number; 39 | [key: string]: unknown; 40 | }; 41 | 42 | const { value } = this.inputEl; 43 | this.inputEl.value = values[selectedItem].item ?? value; 44 | }); 45 | 46 | this.open(); 47 | } 48 | 49 | getItemText(item: T): string { 50 | return this.displayItems[this.items.indexOf(item)]; 51 | } 52 | 53 | getItems(): T[] { 54 | return this.items; 55 | } 56 | 57 | selectSuggestion( 58 | value: FuzzyMatch, 59 | evt: MouseEvent | KeyboardEvent 60 | ) { 61 | this.resolved = true; 62 | super.selectSuggestion(value, evt); 63 | } 64 | 65 | onChooseItem(item: T, evt: MouseEvent | KeyboardEvent): void { 66 | this.resolved = true; 67 | this.resolvePromise(item); 68 | } 69 | 70 | onClose() { 71 | super.onClose(); 72 | 73 | if (!this.resolved) this.rejectPromise("no input given."); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/gui/GenericYesNoPrompt/GenericYesNoPrompt.ts: -------------------------------------------------------------------------------- 1 | import type { App} from "obsidian"; 2 | import { ButtonComponent, Modal } from "obsidian"; 3 | 4 | export default class GenericYesNoPrompt extends Modal { 5 | private resolvePromise: (input: boolean) => void; 6 | private rejectPromise: (reason?: unknown) => void; 7 | private input: boolean; 8 | public waitForClose: Promise; 9 | private didSubmit = false; 10 | 11 | public static Prompt( 12 | app: App, 13 | header: string, 14 | text?: string 15 | ): Promise { 16 | const newPromptModal = new GenericYesNoPrompt(app, header, text); 17 | return newPromptModal.waitForClose; 18 | } 19 | 20 | private constructor( 21 | app: App, 22 | private header: string, 23 | private text?: string 24 | ) { 25 | super(app); 26 | 27 | this.waitForClose = new Promise((resolve, reject) => { 28 | this.resolvePromise = resolve; 29 | this.rejectPromise = reject; 30 | }); 31 | 32 | this.open(); 33 | this.display(); 34 | } 35 | 36 | private display() { 37 | this.containerEl.addClass("quickAddModal", "qaYesNoPrompt"); 38 | this.contentEl.empty(); 39 | this.titleEl.textContent = this.header; 40 | this.contentEl.createEl("p", { text: this.text }); 41 | 42 | const buttonsDiv = this.contentEl.createDiv({ 43 | cls: "yesNoPromptButtonContainer", 44 | }); 45 | 46 | const noButton = new ButtonComponent(buttonsDiv) 47 | .setButtonText("No") 48 | .onClick(() => this.submit(false)); 49 | 50 | const yesButton = new ButtonComponent(buttonsDiv) 51 | .setButtonText("Yes") 52 | .onClick(() => this.submit(true)) 53 | .setWarning(); 54 | 55 | yesButton.buttonEl.focus(); 56 | 57 | addArrowKeyNavigation([noButton.buttonEl, yesButton.buttonEl]); 58 | } 59 | 60 | private submit(input: boolean) { 61 | this.input = input; 62 | this.didSubmit = true; 63 | this.close(); 64 | } 65 | 66 | onClose() { 67 | super.onClose(); 68 | 69 | if (!this.didSubmit) this.rejectPromise("No answer given."); 70 | else this.resolvePromise(this.input); 71 | } 72 | } 73 | 74 | function addArrowKeyNavigation(buttons: HTMLButtonElement[]): void { 75 | buttons.forEach((button) => { 76 | button.addEventListener("keydown", (event) => { 77 | if (event.key === "ArrowRight" || event.key === "ArrowLeft") { 78 | const currentIndex = buttons.indexOf(button); 79 | const nextIndex = (currentIndex + (event.key === "ArrowRight" ? 1 : -1) + buttons.length) % buttons.length; 80 | buttons[nextIndex].focus(); 81 | event.preventDefault(); 82 | } 83 | }); 84 | }); 85 | } 86 | -------------------------------------------------------------------------------- /src/gui/InputPrompt.ts: -------------------------------------------------------------------------------- 1 | import GenericWideInputPrompt from "./GenericWideInputPrompt/GenericWideInputPrompt"; 2 | import GenericInputPrompt from "./GenericInputPrompt/GenericInputPrompt"; 3 | import QuickAdd from "../main"; 4 | 5 | export default class InputPrompt { 6 | public factory() { 7 | if (QuickAdd.instance.settings.inputPrompt === "multi-line") { 8 | return GenericWideInputPrompt; 9 | } else { 10 | return GenericInputPrompt; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/gui/InputSuggester/inputSuggester.ts: -------------------------------------------------------------------------------- 1 | import { FuzzySuggestModal } from "obsidian"; 2 | import type { FuzzyMatch , App} from "obsidian"; 3 | 4 | type Options = { 5 | limit: FuzzySuggestModal["limit"]; 6 | emptyStateText: FuzzySuggestModal["emptyStateText"]; 7 | placeholder: Parameters< 8 | FuzzySuggestModal["setPlaceholder"] 9 | >[0] extends string 10 | ? string 11 | : never; 12 | }; 13 | 14 | /** 15 | * Similar to GenericSuggester, except users can write their own input, and it gets added to the list of suggestions. 16 | */ 17 | export default class InputSuggester extends FuzzySuggestModal { 18 | private resolvePromise: (value: string) => void; 19 | private rejectPromise: (reason?: unknown) => void; 20 | public promise: Promise; 21 | private resolved: boolean; 22 | 23 | public static Suggest( 24 | app: App, 25 | displayItems: string[], 26 | items: string[], 27 | options: Partial = {} 28 | ) { 29 | const newSuggester = new InputSuggester( 30 | app, 31 | displayItems, 32 | items, 33 | options 34 | ); 35 | return newSuggester.promise; 36 | } 37 | 38 | public constructor( 39 | app: App, 40 | private displayItems: string[], 41 | private items: string[], 42 | options: Partial = {} 43 | ) { 44 | super(app); 45 | 46 | this.promise = new Promise((resolve, reject) => { 47 | this.resolvePromise = resolve; 48 | this.rejectPromise = reject; 49 | }); 50 | 51 | this.inputEl.addEventListener("keydown", (event: KeyboardEvent) => { 52 | // chooser is undocumented & not officially a part of the Obsidian API, hence the precautions in using it. 53 | if (event.code !== "Tab" || !("chooser" in this)) { 54 | return; 55 | } 56 | 57 | const { values, selectedItem } = this.chooser as { 58 | values: { 59 | item: string; 60 | match: { score: number; matches: unknown[] }; 61 | }[]; 62 | selectedItem: number; 63 | [key: string]: unknown; 64 | }; 65 | 66 | const { value } = this.inputEl; 67 | this.inputEl.value = values[selectedItem].item ?? value; 68 | }); 69 | 70 | if (options.placeholder) this.setPlaceholder(options.placeholder); 71 | if (options.limit) this.limit = options.limit; 72 | if (options.emptyStateText) 73 | this.emptyStateText = options.emptyStateText; 74 | 75 | this.open(); 76 | } 77 | 78 | getItemText(item: string): string { 79 | if (item === this.inputEl.value) return item; 80 | 81 | return this.displayItems[this.items.indexOf(item)]; 82 | } 83 | 84 | getItems(): string[] { 85 | if (this.inputEl.value === "") return this.items; 86 | return [this.inputEl.value, ...this.items]; 87 | } 88 | 89 | selectSuggestion( 90 | value: FuzzyMatch, 91 | evt: MouseEvent | KeyboardEvent 92 | ) { 93 | this.resolved = true; 94 | super.selectSuggestion(value, evt); 95 | } 96 | 97 | onChooseItem(item: string, evt: MouseEvent | KeyboardEvent): void { 98 | this.resolved = true; 99 | this.resolvePromise(item); 100 | } 101 | 102 | onClose() { 103 | super.onClose(); 104 | 105 | if (!this.resolved) this.rejectPromise("no input given."); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/gui/MacroGUIs/Components/AIAssistantCommand.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 |
23 |
  • {command.name}
  • 24 |
    25 | 26 | configureAssistant()} class="clickable"> 27 | 28 | 29 | 30 | deleteCommand()} class="clickable"> 31 | 32 | 33 | 34 | 39 | 40 | 41 |
    42 |
    43 | 44 | 47 | -------------------------------------------------------------------------------- /src/gui/MacroGUIs/Components/NestedChoiceCommand.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 |
    23 |
  • {command.name}
  • 24 |
    25 | 26 | configureChoice()} class="clickable"> 27 | 28 | 29 | 30 | deleteCommand()} class="clickable"> 31 | 32 | 33 | 34 | 39 | 40 | 41 |
    42 |
    43 | 44 | 47 | -------------------------------------------------------------------------------- /src/gui/MacroGUIs/Components/StandardCommand.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |
    19 |
  • {command.name}
  • 20 |
    21 | 22 | deleteCommand(command.id)} class="clickable"> 23 | 24 | 25 | 26 | 31 | 32 | 33 |
    34 |
    35 | 36 | -------------------------------------------------------------------------------- /src/gui/MacroGUIs/Components/UserScriptCommand.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 |
    23 |
  • {command.name}
  • 24 |
    25 | configureChoice()} on:keypress={() => configureChoice()} class="clickable"> 26 | 27 | 28 | deleteCommand()} on:keypress={() => configureChoice()} class="clickable"> 29 | 30 | 31 | 32 | 37 | 38 | 39 |
    40 |
    41 | 42 | 45 | -------------------------------------------------------------------------------- /src/gui/MacroGUIs/Components/WaitCommand.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 |
    28 |
  • {command.name} for ms
  • 29 |
    30 | 31 | deleteCommand(command.id)} class="clickable"> 32 | 33 | 34 | 35 | 40 | 41 | 42 |
    43 |
    44 | 45 | 61 | -------------------------------------------------------------------------------- /src/gui/choiceList/AddChoiceBox.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 |
    25 | 26 | 32 | 33 |
    34 | 35 | -------------------------------------------------------------------------------- /src/gui/choiceList/ChoiceItemRightButtons.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 |
    31 | 32 |
    33 | 34 |
    35 | {#if showConfigureButton} 36 | 37 |
    38 | 39 |
    40 | {/if} 41 | 42 | {#if showDuplicateButton} 43 | 44 |
    45 | 46 |
    47 | {/if} 48 | 49 | 50 |
    51 | 52 |
    53 | 54 | 55 |
    62 | 63 |
    64 |
    65 | 66 | -------------------------------------------------------------------------------- /src/gui/choiceList/ChoiceList.svelte: -------------------------------------------------------------------------------- 1 | 43 | 44 |
    50 | {#each choices.filter(c => c.id !== SHADOW_PLACEHOLDER_ITEM_ID) as choice(choice.id)} 51 | {#if choice.type !== "Multi"} 52 | 62 | {:else} 63 | 74 | {/if} 75 | {/each} 76 |
    77 | 78 | -------------------------------------------------------------------------------- /src/gui/choiceList/ChoiceListItem.svelte: -------------------------------------------------------------------------------- 1 | 44 | 45 |
    46 | 47 | 48 | 61 |
    62 | 63 | 64 | -------------------------------------------------------------------------------- /src/gui/choiceList/MultiChoiceListItem.svelte: -------------------------------------------------------------------------------- 1 | 49 | 50 |
    51 |
    52 | 53 |
    choice.collapsed = !choice.collapsed}> 54 | 55 | 56 |
    57 | 58 | 71 |
    72 | 73 | {#if !collapseId || (collapseId && choice.id !== collapseId)} 74 | {#if !choice.collapsed} 75 |
    76 | 84 |
    85 | {/if} 86 | {/if} 87 |
    88 | 89 | 110 | -------------------------------------------------------------------------------- /src/gui/suggesters/LaTeXSuggester.ts: -------------------------------------------------------------------------------- 1 | import { TextInputSuggest } from "./suggest"; 2 | import Fuse from "fuse.js"; 3 | import { renderMath } from "obsidian"; 4 | import { LATEX_CURSOR_MOVE_HERE, LaTeXSymbols } from "../../LaTeXSymbols"; 5 | import QuickAdd from "../../main"; 6 | 7 | const LATEX_REGEX = new RegExp(/\\([a-z{}A-Z0-9]*)$/); 8 | 9 | export class LaTeXSuggester extends TextInputSuggest { 10 | private lastInput = ""; 11 | private symbols; 12 | private elementsRendered; 13 | 14 | constructor(public inputEl: HTMLInputElement | HTMLTextAreaElement) { 15 | super(QuickAdd.instance.app, inputEl); 16 | this.symbols = Object.assign([], LaTeXSymbols); 17 | 18 | this.elementsRendered = this.symbols.reduce((elements, symbol) => { 19 | try { 20 | //@ts-ignore 21 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call 22 | elements[symbol.toString()] = renderMath(symbol, true); 23 | 24 | // Ignoring symbols that we can't use 25 | } catch {} //eslint-disable-line no-empty 26 | 27 | return elements; 28 | }, {}); 29 | } 30 | 31 | getSuggestions(inputStr: string): string[] { 32 | if (this.inputEl.selectionStart === null) { 33 | return []; 34 | } 35 | 36 | const cursorPosition: number = this.inputEl.selectionStart; 37 | const inputBeforeCursor: string = inputStr.substr(0, cursorPosition); 38 | const lastBackslashPos: number = inputBeforeCursor.lastIndexOf("\\"); 39 | const commandText = inputBeforeCursor.substr(lastBackslashPos); 40 | 41 | const match = LATEX_REGEX.exec(commandText); 42 | 43 | let suggestions: string[] = []; 44 | 45 | if (match) { 46 | this.lastInput = match[1]; 47 | suggestions = this.symbols.filter((val) => 48 | //@ts-ignore 49 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call 50 | val.toLowerCase().contains(this.lastInput) 51 | ); 52 | } 53 | 54 | const fuse = new Fuse(suggestions, { 55 | findAllMatches: true, 56 | threshold: 0.8, 57 | }); 58 | const searchResults = fuse.search(this.lastInput); 59 | return searchResults.map((value) => value.item); 60 | } 61 | 62 | renderSuggestion(item: string, el: HTMLElement): void { 63 | if (item) { 64 | el.setText(item); 65 | //@ts-ignore 66 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 67 | el.append(this.elementsRendered[item]); 68 | } 69 | } 70 | 71 | selectSuggestion(item: string): void { 72 | if (this.inputEl.selectionStart === null) return; 73 | 74 | const cursorPosition: number = this.inputEl.selectionStart; 75 | const lastInputLength: number = this.lastInput.length; 76 | const currentInputValue: string = this.inputEl.value; 77 | let insertedEndPosition = 0; 78 | 79 | const textToInsert = item.replace(/\\\\/g, "\\"); 80 | 81 | this.inputEl.value = `${currentInputValue.substr( 82 | 0, 83 | cursorPosition - lastInputLength - 1 84 | )}${textToInsert}${currentInputValue.substr(cursorPosition)}`; 85 | insertedEndPosition = 86 | cursorPosition - lastInputLength + item.length - 1; 87 | 88 | this.inputEl.trigger("input"); 89 | this.close(); 90 | 91 | if (item.contains(LATEX_CURSOR_MOVE_HERE)) { 92 | const cursorPos = this.inputEl.value.indexOf( 93 | LATEX_CURSOR_MOVE_HERE 94 | ); 95 | this.inputEl.value = this.inputEl.value.replace( 96 | LATEX_CURSOR_MOVE_HERE, 97 | "" 98 | ); 99 | this.inputEl.setSelectionRange(cursorPos, cursorPos); 100 | } else { 101 | this.inputEl.setSelectionRange( 102 | insertedEndPosition, 103 | insertedEndPosition 104 | ); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/gui/suggesters/choiceSuggester.ts: -------------------------------------------------------------------------------- 1 | import type { FuzzyMatch} from "obsidian"; 2 | import { FuzzySuggestModal, MarkdownRenderer } from "obsidian"; 3 | import type IChoice from "../../types/choices/IChoice"; 4 | import { ChoiceExecutor } from "../../choiceExecutor"; 5 | import { MultiChoice } from "../../types/choices/MultiChoice"; 6 | import type IMultiChoice from "../../types/choices/IMultiChoice"; 7 | import type QuickAdd from "../../main"; 8 | import type { IChoiceExecutor } from "../../IChoiceExecutor"; 9 | 10 | export default class ChoiceSuggester extends FuzzySuggestModal { 11 | private choiceExecutor: IChoiceExecutor = new ChoiceExecutor( 12 | this.app, 13 | this.plugin 14 | ); 15 | 16 | public static Open( 17 | plugin: QuickAdd, 18 | choices: IChoice[], 19 | choiceExecutor?: IChoiceExecutor 20 | ) { 21 | new ChoiceSuggester(plugin, choices, choiceExecutor).open(); 22 | } 23 | 24 | constructor( 25 | private plugin: QuickAdd, 26 | private choices: IChoice[], 27 | choiceExecutor?: IChoiceExecutor 28 | ) { 29 | super(plugin.app); 30 | if (choiceExecutor) this.choiceExecutor = choiceExecutor; 31 | } 32 | 33 | renderSuggestion(item: FuzzyMatch, el: HTMLElement): void { 34 | el.empty(); 35 | void MarkdownRenderer.renderMarkdown(item.item.name, el, '', this.plugin); 36 | el.classList.add("quickadd-choice-suggestion"); 37 | } 38 | 39 | getItemText(item: IChoice): string { 40 | return item.name; 41 | } 42 | 43 | getItems(): IChoice[] { 44 | return this.choices; 45 | } 46 | 47 | async onChooseItem( 48 | item: IChoice, 49 | evt: MouseEvent | KeyboardEvent 50 | ): Promise { 51 | if (item.type === "Multi") 52 | this.onChooseMultiType(item); 53 | else await this.choiceExecutor.execute(item); 54 | } 55 | 56 | private onChooseMultiType(multi: IMultiChoice) { 57 | const choices = [...multi.choices]; 58 | 59 | if (multi.name != "← Back") 60 | choices.push(new MultiChoice("← Back").addChoices(this.choices)); 61 | 62 | ChoiceSuggester.Open(this.plugin, choices); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/gui/suggesters/exclusiveSuggester.ts: -------------------------------------------------------------------------------- 1 | import { TextInputSuggest } from "./suggest"; 2 | import type { App } from "obsidian"; 3 | 4 | export class ExclusiveSuggester extends TextInputSuggest { 5 | constructor( 6 | public app: App, 7 | public inputEl: HTMLInputElement | HTMLTextAreaElement, 8 | private suggestItems: string[], 9 | private currentItems: string[] 10 | ) { 11 | super(app, inputEl); 12 | } 13 | 14 | updateCurrentItems(currentItems: string[]) { 15 | this.currentItems = currentItems; 16 | } 17 | 18 | getSuggestions(inputStr: string): string[] { 19 | return this.suggestItems.filter((item) => item.contains(inputStr)); 20 | } 21 | 22 | selectSuggestion(item: string): void { 23 | this.inputEl.value = item; 24 | this.inputEl.trigger("input"); 25 | this.close(); 26 | } 27 | 28 | renderSuggestion(value: string, el: HTMLElement): void { 29 | if (value) el.setText(value); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/gui/suggesters/genericTextSuggester.ts: -------------------------------------------------------------------------------- 1 | import { TextInputSuggest } from "./suggest"; 2 | import type { App } from "obsidian"; 3 | 4 | export class GenericTextSuggester extends TextInputSuggest { 5 | constructor( 6 | public app: App, 7 | public inputEl: HTMLInputElement | HTMLTextAreaElement, 8 | private items: string[], 9 | private maxSuggestions = Infinity 10 | ) { 11 | super(app, inputEl); 12 | } 13 | 14 | getSuggestions(inputStr: string): string[] { 15 | const inputLowerCase: string = inputStr.toLowerCase(); 16 | 17 | const filtered = this.items.filter((item) => { 18 | if (item.toLowerCase().contains(inputLowerCase)) return item; 19 | }); 20 | 21 | if (!filtered) this.close(); 22 | 23 | const limited = filtered.slice(0, this.maxSuggestions); 24 | 25 | return limited; 26 | } 27 | 28 | selectSuggestion(item: string): void { 29 | this.inputEl.value = item; 30 | this.inputEl.trigger("input"); 31 | this.close(); 32 | } 33 | 34 | renderSuggestion(value: string, el: HTMLElement): void { 35 | if (value) el.setText(value); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/gui/suggesters/tagSuggester.ts: -------------------------------------------------------------------------------- 1 | import Fuse from "fuse.js"; 2 | import type { App } from "obsidian"; 3 | import { TAG_REGEX } from "../../constants"; 4 | import { TextInputSuggest } from "./suggest"; 5 | 6 | export class SilentTagSuggester extends TextInputSuggest { 7 | private lastInput = ""; 8 | private tags: string[]; 9 | 10 | constructor( 11 | public app: App, 12 | public inputEl: HTMLInputElement | HTMLTextAreaElement 13 | ) { 14 | super(app, inputEl); 15 | 16 | // @ts-expect-error 17 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call 18 | this.tags = Object.keys(app.metadataCache.getTags()); 19 | } 20 | 21 | getSuggestions(inputStr: string): string[] { 22 | if (this.inputEl.selectionStart === null) { 23 | return []; 24 | } 25 | 26 | const cursorPosition: number = this.inputEl.selectionStart; 27 | const inputBeforeCursor: string = inputStr.substr(0, cursorPosition); 28 | const tagMatch = TAG_REGEX.exec(inputBeforeCursor); 29 | 30 | if (!tagMatch) { 31 | return []; 32 | } 33 | 34 | const tagInput: string = tagMatch[1]; 35 | this.lastInput = tagInput; 36 | const suggestions = this.tags.filter((tag) => 37 | tag.toLowerCase().contains(tagInput.toLowerCase()) 38 | ); 39 | 40 | const fuse = new Fuse(suggestions, { 41 | findAllMatches: true, 42 | threshold: 0.8, 43 | }); 44 | const search = fuse.search(this.lastInput).map((value) => value.item); 45 | 46 | return search; 47 | } 48 | 49 | renderSuggestion(item: string, el: HTMLElement): void { 50 | el.setText(item); 51 | } 52 | 53 | selectSuggestion(item: string): void { 54 | if (!this.inputEl.selectionStart) return; 55 | 56 | const cursorPosition: number = this.inputEl.selectionStart; 57 | const lastInputLength: number = this.lastInput.length; 58 | const currentInputValue: string = this.inputEl.value; 59 | let insertedEndPosition = 0; 60 | 61 | this.inputEl.value = this.getNewInputValueForTag( 62 | currentInputValue, 63 | item, 64 | cursorPosition, 65 | lastInputLength 66 | ); 67 | insertedEndPosition = 68 | cursorPosition - lastInputLength + item.length - 1; 69 | 70 | this.inputEl.trigger("input"); 71 | this.close(); 72 | this.inputEl.setSelectionRange( 73 | insertedEndPosition, 74 | insertedEndPosition 75 | ); 76 | } 77 | 78 | private getNewInputValueForTag( 79 | currentInputElValue: string, 80 | selectedItem: string, 81 | cursorPosition: number, 82 | lastInputLength: number 83 | ) { 84 | return `${currentInputElValue.substr( 85 | 0, 86 | cursorPosition - lastInputLength - 1 87 | )}${selectedItem}${currentInputElValue.substr(cursorPosition)}`; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/logger/consoleErrorLogger.ts: -------------------------------------------------------------------------------- 1 | import { ErrorLevel } from "./errorLevel"; 2 | import { QuickAddLogger } from "./quickAddLogger"; 3 | import type { QuickAddError } from "./quickAddError"; 4 | import { MAX_ERROR_LOG_SIZE } from "../utils/errorUtils"; 5 | 6 | /** 7 | * Logger implementation that outputs to the browser console and maintains an error log 8 | * with a maximum size to prevent memory leaks. Uses native Error objects to leverage 9 | * browser DevTools stack trace display. 10 | */ 11 | export class ConsoleErrorLogger extends QuickAddLogger { 12 | /** 13 | * In-memory log of errors for debugging 14 | * Limited to MAX_ERROR_LOG_SIZE entries to prevent memory leaks 15 | */ 16 | public ErrorLog: QuickAddError[] = []; 17 | 18 | /** 19 | * Logs an error to the console with proper stack trace handling 20 | * 21 | * @param errorMsg - Error message or Error object 22 | * @param stack - Optional stack trace string 23 | * @param originalError - Optional original Error object 24 | */ 25 | public logError(errorMsg: string, stack?: string, originalError?: Error) { 26 | const error = this.getQuickAddError(errorMsg, ErrorLevel.Error, stack, originalError); 27 | this.addMessageToErrorLog(error); 28 | 29 | // Always pass the original error or create a new one to leverage Dev Tools' stack trace UI 30 | const errorToLog = originalError || new Error(errorMsg); 31 | 32 | // Just log the message as the first argument and the error object as the second 33 | console.error(this.formatOutputString(error), errorToLog); 34 | } 35 | 36 | /** 37 | * Logs a warning to the console with proper stack trace handling 38 | * 39 | * @param warningMsg - Warning message or Error object 40 | * @param stack - Optional stack trace string 41 | * @param originalError - Optional original Error object 42 | */ 43 | public logWarning(warningMsg: string, stack?: string, originalError?: Error) { 44 | const warning = this.getQuickAddError(warningMsg, ErrorLevel.Warning, stack, originalError); 45 | this.addMessageToErrorLog(warning); 46 | 47 | // Always pass the original error or create a new one to leverage Dev Tools' stack trace UI 48 | const errorToLog = originalError || new Error(warningMsg); 49 | 50 | console.warn(this.formatOutputString(warning), errorToLog); 51 | } 52 | 53 | /** 54 | * Logs a message to the console 55 | * 56 | * @param logMsg - Log message 57 | * @param stack - Optional stack trace string 58 | * @param originalError - Optional original Error object 59 | */ 60 | public logMessage(logMsg: string, stack?: string, originalError?: Error) { 61 | const log = this.getQuickAddError(logMsg, ErrorLevel.Log, stack, originalError); 62 | this.addMessageToErrorLog(log); 63 | 64 | // For regular logs, we'll still show the error if available 65 | if (originalError) { 66 | console.log(this.formatOutputString(log), originalError); 67 | } else { 68 | console.log(this.formatOutputString(log)); 69 | } 70 | } 71 | 72 | /** 73 | * Adds an error to the error log, maintaining the maximum size limit 74 | * by removing the oldest entries when needed 75 | * 76 | * @param error - Error to add to the log 77 | */ 78 | private addMessageToErrorLog(error: QuickAddError): void { 79 | // Add the new error 80 | this.ErrorLog.push(error); 81 | 82 | // If we've exceeded the maximum size, remove the oldest entries 83 | if (this.ErrorLog.length > MAX_ERROR_LOG_SIZE) { 84 | this.ErrorLog = this.ErrorLog.slice(-MAX_ERROR_LOG_SIZE); 85 | } 86 | } 87 | 88 | /** 89 | * Clears the error log 90 | */ 91 | public clearErrorLog(): void { 92 | this.ErrorLog = []; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/logger/errorLevel.ts: -------------------------------------------------------------------------------- 1 | export enum ErrorLevel { 2 | Error = "ERROR", 3 | Warning = "WARNING", 4 | Log = "LOG", 5 | } 6 | -------------------------------------------------------------------------------- /src/logger/guiLogger.ts: -------------------------------------------------------------------------------- 1 | import { Notice } from "obsidian"; 2 | import { QuickAddLogger } from "./quickAddLogger"; 3 | import type QuickAdd from "../main"; 4 | import { ErrorLevel } from "./errorLevel"; 5 | 6 | export class GuiLogger extends QuickAddLogger { 7 | constructor(private plugin: QuickAdd) { 8 | super(); 9 | } 10 | 11 | logError(msg: string, stack?: string, originalError?: Error): void { 12 | const error = this.getQuickAddError(msg, ErrorLevel.Error, stack, originalError); 13 | new Notice(this.formatOutputString(error), 15000); 14 | } 15 | 16 | logWarning(msg: string, stack?: string, originalError?: Error): void { 17 | const warning = this.getQuickAddError(msg, ErrorLevel.Warning, stack, originalError); 18 | new Notice(this.formatOutputString(warning)); 19 | } 20 | 21 | logMessage(msg: string, stack?: string, originalError?: Error): void {} 22 | } 23 | -------------------------------------------------------------------------------- /src/logger/ilogger.ts: -------------------------------------------------------------------------------- 1 | export interface ILogger { 2 | logError(msg: string, stack?: string, originalError?: Error): void; 3 | 4 | logWarning(msg: string, stack?: string, originalError?: Error): void; 5 | 6 | logMessage(msg: string, stack?: string, originalError?: Error): void; 7 | } 8 | -------------------------------------------------------------------------------- /src/logger/logDecorator.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-return */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 3 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 4 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 5 | /* eslint-disable @typescript-eslint/no-explicit-any */ 6 | import { log } from "./logManager"; 7 | 8 | /** 9 | * A decorator function that logs the method call with its arguments and return value. 10 | * Used by writing @logDecorator above a method. 11 | */ 12 | function logDecorator(target: any, key: string, descriptor: PropertyDescriptor) { 13 | const originalMethod = descriptor.value; 14 | 15 | descriptor.value = function (...args: any[]) { 16 | log.logMessage(`Method ${key} called with arguments: ${JSON.stringify(args)}`); 17 | const result = originalMethod.apply(this, args); 18 | log.logMessage(`Method ${key} returned: ${JSON.stringify(result)}`); 19 | return result; 20 | }; 21 | 22 | return descriptor; 23 | } 24 | 25 | export default logDecorator; -------------------------------------------------------------------------------- /src/logger/logManager.ts: -------------------------------------------------------------------------------- 1 | import type { ILogger } from "./ilogger"; 2 | 3 | /** 4 | * Helper function to convert any value to an Error object 5 | * This function ensures that an Error object is always returned, preserving 6 | * the original Error if provided or creating a new one otherwise. 7 | * 8 | * @param err - The error value to convert (can be any type) 9 | * @returns A proper Error object with stack trace 10 | * 11 | * @example 12 | * ```ts 13 | * try { 14 | * // Some operation 15 | * } catch (err) { 16 | * log.logError(toError(err)); 17 | * } 18 | * ``` 19 | */ 20 | export function toError(err: unknown): Error { 21 | if (err instanceof Error) return err; 22 | return new Error(typeof err === 'string' ? err : String(err)); 23 | } 24 | 25 | export class LogManager { 26 | public static loggers: ILogger[] = []; 27 | 28 | public register(logger: ILogger): LogManager { 29 | LogManager.loggers.push(logger); 30 | 31 | return this; 32 | } 33 | 34 | logError(message: string | Error) { 35 | const messageStr = message instanceof Error ? message.message : message; 36 | const stack = message instanceof Error ? message.stack : undefined; 37 | const originalError = message instanceof Error ? message : undefined; 38 | 39 | LogManager.loggers.forEach((logger) => logger.logError(messageStr, stack, originalError)); 40 | } 41 | 42 | logWarning(message: string | Error) { 43 | const messageStr = message instanceof Error ? message.message : message; 44 | const stack = message instanceof Error ? message.stack : undefined; 45 | const originalError = message instanceof Error ? message : undefined; 46 | 47 | LogManager.loggers.forEach((logger) => logger.logWarning(messageStr, stack, originalError)); 48 | } 49 | 50 | logMessage(message: string | Error) { 51 | const messageStr = message instanceof Error ? message.message : message; 52 | const stack = message instanceof Error ? message.stack : undefined; 53 | const originalError = message instanceof Error ? message : undefined; 54 | 55 | LogManager.loggers.forEach((logger) => logger.logMessage(messageStr, stack, originalError)); 56 | } 57 | } 58 | 59 | export const log = new LogManager(); 60 | -------------------------------------------------------------------------------- /src/logger/quickAddError.ts: -------------------------------------------------------------------------------- 1 | import type { ErrorLevel } from "./errorLevel"; 2 | 3 | export interface QuickAddError { 4 | message: string; 5 | level: ErrorLevel; 6 | time: number; 7 | stack?: string; 8 | originalError?: Error; 9 | } 10 | -------------------------------------------------------------------------------- /src/logger/quickAddLogger.ts: -------------------------------------------------------------------------------- 1 | import type { ILogger } from "./ilogger"; 2 | import type { ErrorLevel } from "./errorLevel"; 3 | import type { QuickAddError } from "./quickAddError"; 4 | 5 | export abstract class QuickAddLogger implements ILogger { 6 | abstract logError(msg: string, stack?: string, originalError?: Error): void; 7 | 8 | abstract logMessage(msg: string, stack?: string, originalError?: Error): void; 9 | 10 | abstract logWarning(msg: string, stack?: string, originalError?: Error): void; 11 | 12 | protected formatOutputString(error: QuickAddError): string { 13 | // Just return the basic message without stack trace, as we'll pass the error object separately 14 | return `QuickAdd: (${error.level}) ${error.message}`; 15 | } 16 | 17 | protected getQuickAddError( 18 | message: string, 19 | level: ErrorLevel, 20 | stack?: string, 21 | originalError?: Error 22 | ): QuickAddError { 23 | return { 24 | message, 25 | level, 26 | time: Date.now(), 27 | stack, 28 | originalError 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/migrations/Migrations.ts: -------------------------------------------------------------------------------- 1 | import type QuickAdd from "src/main"; 2 | import type { QuickAddSettings } from "src/quickAddSettingsTab"; 3 | 4 | export type Migration = { 5 | description: string; 6 | migrate: (plugin: QuickAdd) => Promise; 7 | }; 8 | 9 | export type Migrations = { 10 | [key in keyof QuickAddSettings["migrations"]]: Migration; 11 | }; 12 | -------------------------------------------------------------------------------- /src/migrations/addDefaultAIProviders.ts: -------------------------------------------------------------------------------- 1 | import { DefaultProviders } from "src/ai/Provider"; 2 | import type { Migration } from "./Migrations"; 3 | import { settingsStore } from "src/settingsStore"; 4 | 5 | const addDefaultAIProviders: Migration = { 6 | description: "Add default AI providers to the settings.", 7 | // eslint-disable-next-line @typescript-eslint/require-await 8 | migrate: async (_) => { 9 | const ai = settingsStore.getState().ai; 10 | 11 | const defaultProvidersWithOpenAIKey = DefaultProviders.map( 12 | (provider) => { 13 | if (provider.name === "OpenAI") { 14 | if ("OpenAIApiKey" in ai && typeof ai.OpenAIApiKey === "string") { 15 | provider.apiKey = ai.OpenAIApiKey; 16 | } 17 | } 18 | 19 | return provider; 20 | } 21 | ); 22 | 23 | if ("OpenAIApiKey" in ai) { 24 | delete ai.OpenAIApiKey; 25 | } 26 | 27 | settingsStore.setState({ 28 | ai: { 29 | ...settingsStore.getState().ai, 30 | providers: defaultProvidersWithOpenAIKey, 31 | }, 32 | }); 33 | 34 | 35 | }, 36 | }; 37 | 38 | export default addDefaultAIProviders; 39 | -------------------------------------------------------------------------------- /src/migrations/helpers/isCaptureChoice.ts: -------------------------------------------------------------------------------- 1 | import type { CaptureChoice } from "src/types/choices/CaptureChoice"; 2 | import type IChoice from "src/types/choices/IChoice"; 3 | 4 | export function isCaptureChoice(choice: IChoice): choice is CaptureChoice { 5 | return choice.type === "Capture"; 6 | } 7 | -------------------------------------------------------------------------------- /src/migrations/helpers/isMultiChoice.ts: -------------------------------------------------------------------------------- 1 | import type { MultiChoice } from "src/types/choices/MultiChoice"; 2 | 3 | export function isMultiChoice(choice: unknown): choice is MultiChoice { 4 | if ( 5 | choice === null || 6 | typeof choice !== "object" || 7 | !("type" in choice) || 8 | !("choices" in choice) 9 | ) { 10 | return false; 11 | } 12 | 13 | return choice.type === "Multi" && choice.choices !== undefined; 14 | } 15 | -------------------------------------------------------------------------------- /src/migrations/helpers/isNestedChoiceCommand.ts: -------------------------------------------------------------------------------- 1 | import type { NestedChoiceCommand } from "src/types/macros/QuickCommands/NestedChoiceCommand"; 2 | 3 | export function isNestedChoiceCommand( 4 | command: unknown 5 | ): command is NestedChoiceCommand { 6 | if ( 7 | command === null || 8 | typeof command !== "object" || 9 | !("choice" in command) 10 | ) { 11 | return false; 12 | } 13 | 14 | return command.choice !== undefined; 15 | } 16 | -------------------------------------------------------------------------------- /src/migrations/helpers/isOldTemplateChoice.ts: -------------------------------------------------------------------------------- 1 | import type { TemplateChoice } from "src/types/choices/TemplateChoice"; 2 | 3 | export type OldTemplateChoice = TemplateChoice & { 4 | incrementFileName?: boolean; 5 | }; 6 | 7 | export function isOldTemplateChoice( 8 | choice: unknown 9 | ): choice is OldTemplateChoice { 10 | if (typeof choice !== "object" || choice === null) return false; 11 | 12 | return "incrementFileName" in choice; 13 | } 14 | -------------------------------------------------------------------------------- /src/migrations/incrementFileNameSettingMoveToDefaultBehavior.ts: -------------------------------------------------------------------------------- 1 | import type QuickAdd from "src/main"; 2 | import type IChoice from "src/types/choices/IChoice"; 3 | import type { IMacro } from "src/types/macros/IMacro"; 4 | import { isMultiChoice } from "./helpers/isMultiChoice"; 5 | import { isNestedChoiceCommand } from "./helpers/isNestedChoiceCommand"; 6 | import { isOldTemplateChoice } from "./helpers/isOldTemplateChoice"; 7 | import type { Migration } from "./Migrations"; 8 | 9 | function recursiveRemoveIncrementFileName(choices: IChoice[]): IChoice[] { 10 | for (const choice of choices) { 11 | if (isMultiChoice(choice)) { 12 | choice.choices = recursiveRemoveIncrementFileName(choice.choices); 13 | } 14 | 15 | if (isOldTemplateChoice(choice)) { 16 | choice.setFileExistsBehavior = true; 17 | choice.fileExistsMode = "Increment the file name"; 18 | 19 | delete choice.incrementFileName; 20 | } 21 | } 22 | 23 | return choices; 24 | } 25 | 26 | function removeIncrementFileName(macros: IMacro[]): IMacro[] { 27 | for (const macro of macros) { 28 | if (!Array.isArray(macro.commands)) continue; 29 | 30 | for (const command of macro.commands) { 31 | if ( 32 | isNestedChoiceCommand(command) && 33 | isOldTemplateChoice(command.choice) 34 | ) { 35 | command.choice.setFileExistsBehavior = true; 36 | command.choice.fileExistsMode = "Increment the file name"; 37 | 38 | delete command.choice.incrementFileName; 39 | } 40 | } 41 | } 42 | 43 | return macros; 44 | } 45 | 46 | const incrementFileNameSettingMoveToDefaultBehavior: Migration = { 47 | description: 48 | "'Increment file name' setting moved to 'Set default behavior if file already exists' setting", 49 | // eslint-disable-next-line @typescript-eslint/require-await 50 | migrate: async (plugin: QuickAdd): Promise => { 51 | const choicesCopy = structuredClone(plugin.settings.choices); 52 | const choices = recursiveRemoveIncrementFileName(choicesCopy); 53 | 54 | const macrosCopy = structuredClone(plugin.settings.macros); 55 | const macros = removeIncrementFileName(macrosCopy); 56 | 57 | plugin.settings.choices = structuredClone(choices); 58 | plugin.settings.macros = structuredClone(macros); 59 | }, 60 | }; 61 | 62 | export default incrementFileNameSettingMoveToDefaultBehavior; 63 | -------------------------------------------------------------------------------- /src/migrations/migrate.ts: -------------------------------------------------------------------------------- 1 | import { log } from "src/logger/logManager"; 2 | import type QuickAdd from "src/main"; 3 | import type { Migrations } from "./Migrations"; 4 | import migrateToMacroIDFromEmbeddedMacro from "./migrateToMacroIDFromEmbeddedMacro"; 5 | import useQuickAddTemplateFolder from "./useQuickAddTemplateFolder"; 6 | import incrementFileNameSettingMoveToDefaultBehavior from "./incrementFileNameSettingMoveToDefaultBehavior"; 7 | import mutualExclusionInsertAfterAndWriteToBottomOfFile from "./mutualExclusionInsertAfterAndWriteToBottomOfFile"; 8 | import setVersionAfterUpdateModalRelease from "./setVersionAfterUpdateModalRelease"; 9 | import addDefaultAIProviders from "./addDefaultAIProviders"; 10 | 11 | const migrations: Migrations = { 12 | migrateToMacroIDFromEmbeddedMacro, 13 | useQuickAddTemplateFolder, 14 | incrementFileNameSettingMoveToDefaultBehavior, 15 | mutualExclusionInsertAfterAndWriteToBottomOfFile, 16 | setVersionAfterUpdateModalRelease, 17 | addDefaultAIProviders, 18 | }; 19 | 20 | async function migrate(plugin: QuickAdd): Promise { 21 | const migrationsToRun = Object.keys(migrations).filter( 22 | (migration: keyof Migrations) => !plugin.settings.migrations[migration] 23 | ); 24 | 25 | if (migrationsToRun.length === 0) { 26 | log.logMessage("No migrations to run."); 27 | 28 | return; 29 | } 30 | 31 | // Could batch-run with Promise.all, but we want to log each migration as it runs. 32 | for (const migration of migrationsToRun as (keyof Migrations)[]) { 33 | log.logMessage( 34 | `Running migration ${migration}: ${migrations[migration].description}` 35 | ); 36 | 37 | const backup = structuredClone(plugin.settings); 38 | 39 | try { 40 | await migrations[migration].migrate(plugin); 41 | 42 | plugin.settings.migrations[migration] = true; 43 | 44 | log.logMessage(`Migration ${migration} successful.`); 45 | } catch (error) { 46 | log.logError( 47 | `Migration '${migration}' was unsuccessful. Please create an issue with the following error message: \n\n${error as string}\n\nQuickAdd will now revert to backup.` 48 | ); 49 | 50 | plugin.settings = backup; 51 | } 52 | } 53 | 54 | void plugin.saveSettings(); 55 | } 56 | 57 | export default migrate; 58 | -------------------------------------------------------------------------------- /src/migrations/migrateToMacroIDFromEmbeddedMacro.ts: -------------------------------------------------------------------------------- 1 | import type QuickAdd from "src/main"; 2 | import type IChoice from "src/types/choices/IChoice"; 3 | import type IMacroChoice from "src/types/choices/IMacroChoice"; 4 | import type IMultiChoice from "src/types/choices/IMultiChoice"; 5 | 6 | export default { 7 | description: "Migrate to macro ID from embedded macro in macro choices.", 8 | migrate: async (plugin: QuickAdd) => { 9 | // Did not make sense to have copies of macros in the choices when they are maintained for themselves. 10 | // Instead we reference by id now. Have to port this over for all users. 11 | function convertMacroChoiceMacroToIdHelper(choice: IChoice): IChoice { 12 | if (choice.type === "Multi") { 13 | let multiChoice = choice as IMultiChoice; 14 | const multiChoices = multiChoice.choices.map( 15 | convertMacroChoiceMacroToIdHelper 16 | ); 17 | multiChoice = { ...multiChoice, choices: multiChoices }; 18 | return multiChoice; 19 | } 20 | 21 | if (choice.type !== "Macro") return choice; 22 | const macroChoice = choice as IMacroChoice; 23 | 24 | if (macroChoice.macro) { 25 | macroChoice.macroId = macroChoice.macro.id; 26 | delete macroChoice.macro; 27 | } 28 | 29 | return macroChoice; 30 | } 31 | 32 | plugin.settings.choices = plugin.settings.choices.map( 33 | convertMacroChoiceMacroToIdHelper 34 | ); 35 | 36 | await plugin.saveSettings(); 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /src/migrations/mutualExclusionInsertAfterAndWriteToBottomOfFile.ts: -------------------------------------------------------------------------------- 1 | import type IChoice from "src/types/choices/IChoice"; 2 | import type { IMacro } from "src/types/macros/IMacro"; 3 | import { isCaptureChoice } from "./helpers/isCaptureChoice"; 4 | import { isMultiChoice } from "./helpers/isMultiChoice"; 5 | import { isNestedChoiceCommand } from "./helpers/isNestedChoiceCommand"; 6 | import type { Migration } from "./Migrations"; 7 | 8 | function recursiveMigrateSettingInChoices(choices: IChoice[]): IChoice[] { 9 | for (const choice of choices) { 10 | if (isMultiChoice(choice)) { 11 | choice.choices = recursiveMigrateSettingInChoices(choice.choices); 12 | } 13 | 14 | if (isCaptureChoice(choice)) { 15 | if (choice.insertAfter.enabled && choice.prepend) { 16 | choice.prepend = false; 17 | } 18 | } 19 | } 20 | 21 | return choices; 22 | } 23 | 24 | function migrateSettingsInMacros(macros: IMacro[]): IMacro[] { 25 | for (const macro of macros) { 26 | if (!Array.isArray(macro.commands)) continue; 27 | 28 | for (const command of macro.commands) { 29 | if ( 30 | isNestedChoiceCommand(command) && 31 | isCaptureChoice(command.choice) 32 | ) { 33 | if ( 34 | command.choice.insertAfter.enabled && 35 | command.choice.prepend 36 | ) { 37 | command.choice.prepend = false; 38 | } 39 | } 40 | } 41 | } 42 | 43 | return macros; 44 | } 45 | 46 | const mutualExclusionInsertAfterAndWriteToBottomOfFile: Migration = { 47 | description: 48 | "Mutual exclusion of insertAfter and writeToBottomOfFile settings. If insertAfter is enabled, writeToBottomOfFile is disabled. To support changes in settings UI.", 49 | // eslint-disable-next-line @typescript-eslint/require-await 50 | migrate: async (plugin) => { 51 | const choicesCopy = structuredClone(plugin.settings.choices); 52 | const choices = recursiveMigrateSettingInChoices(choicesCopy); 53 | 54 | const macrosCopy = structuredClone(plugin.settings.macros); 55 | const macros = migrateSettingsInMacros(macrosCopy); 56 | 57 | plugin.settings.choices = choices; 58 | plugin.settings.macros = macros; 59 | }, 60 | }; 61 | 62 | export default mutualExclusionInsertAfterAndWriteToBottomOfFile; 63 | -------------------------------------------------------------------------------- /src/migrations/setVersionAfterUpdateModalRelease.ts: -------------------------------------------------------------------------------- 1 | import { settingsStore } from "src/settingsStore"; 2 | import type { Migration } from "./Migrations"; 3 | 4 | /** 5 | * This was used with v. 0.14.0, which was the release version prior to the update modal release. 6 | * Previously, it set the version to 0.14.0, but now we want to set it to the current version. 7 | * It would otherwise break the plugin for new users. 8 | */ 9 | 10 | const setVersionAfterUpdateModalRelease: Migration = { 11 | description: "Set version to the current plugin version.", 12 | // eslint-disable-next-line @typescript-eslint/require-await 13 | migrate: async (plugin) => { 14 | settingsStore.setState({ version: plugin.manifest.version }); 15 | }, 16 | }; 17 | 18 | export default setVersionAfterUpdateModalRelease; 19 | -------------------------------------------------------------------------------- /src/migrations/useQuickAddTemplateFolder.ts: -------------------------------------------------------------------------------- 1 | import { log } from "src/logger/logManager"; 2 | import type QuickAdd from "src/main"; 3 | 4 | export default { 5 | description: 6 | "Use QuickAdd template folder instead of Obsidian templates plugin folder / Templater templates folder.", 7 | // eslint-disable-next-line @typescript-eslint/require-await 8 | migrate: async (plugin: QuickAdd): Promise => { 9 | try { 10 | const templaterPlugin = app.plugins.plugins["templater"]; 11 | const obsidianTemplatesPlugin = 12 | app.internalPlugins.plugins["templates"]; 13 | 14 | if (!templaterPlugin && !obsidianTemplatesPlugin) { 15 | log.logMessage("No template plugin found. Skipping migration."); 16 | 17 | return; 18 | } 19 | 20 | if (obsidianTemplatesPlugin) { 21 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 22 | const obsidianTemplatesSettings = 23 | //@ts-ignore 24 | obsidianTemplatesPlugin.instance.options; 25 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 26 | if (obsidianTemplatesSettings["folder"]) { 27 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 28 | plugin.settings.templateFolderPath = 29 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 30 | obsidianTemplatesSettings["folder"]; 31 | 32 | log.logMessage( 33 | "Migrated template folder path to Obsidian Templates' setting." 34 | ); 35 | } 36 | } 37 | 38 | if (templaterPlugin) { 39 | const templaterSettings = templaterPlugin.settings; 40 | //@ts-ignore 41 | if (templaterSettings["template_folder"]) { 42 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 43 | plugin.settings.templateFolderPath = 44 | //@ts-ignore 45 | templaterSettings["template_folder"]; 46 | 47 | log.logMessage( 48 | "Migrated template folder path to Templaters setting." 49 | ); 50 | } 51 | } 52 | } catch (error) { 53 | log.logError("Failed to migrate template folder path."); 54 | 55 | throw error; 56 | } 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /src/settingsStore.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "zustand/vanilla"; 2 | import type { QuickAddSettings } from "./quickAddSettingsTab"; 3 | import { DEFAULT_SETTINGS } from "./quickAddSettingsTab"; 4 | import type { IMacro } from "./types/macros/IMacro"; 5 | import { QuickAddMacro } from "./types/macros/QuickAddMacro"; 6 | 7 | type SettingsState = QuickAddSettings; 8 | 9 | export const settingsStore = (() => { 10 | const useSettingsStore = createStore((set, _get) => ({ 11 | ...structuredClone(DEFAULT_SETTINGS), 12 | })); 13 | 14 | const { getState, setState, subscribe } = useSettingsStore; 15 | 16 | return { 17 | getState, 18 | setState, 19 | subscribe, 20 | setMacro: (macroId: IMacro["id"], macro: IMacro) => { 21 | setState((state) => { 22 | const macroIdx = state.macros.findIndex((m) => m.id === macroId); 23 | if (macroIdx === -1) { 24 | throw new Error("Macro not found"); 25 | } 26 | 27 | const newState = { 28 | ...state, 29 | macros: [...state.macros], 30 | }; 31 | 32 | newState.macros[macroIdx] = macro; 33 | 34 | return newState; 35 | }); 36 | }, 37 | createMacro: (name: string) => { 38 | if (name === "" || getState().macros.some((m) => m.name === name)) { 39 | throw new Error("Invalid macro name"); 40 | } 41 | 42 | const macro = new QuickAddMacro(name); 43 | setState((state) => ({ 44 | ...state, 45 | macros: [...state.macros, macro], 46 | })); 47 | 48 | return macro; 49 | }, 50 | getMacro: (macroId: IMacro["id"]) => { 51 | return getState().macros.find((m) => m.id === macroId); 52 | }, 53 | }; 54 | })(); 55 | -------------------------------------------------------------------------------- /src/types/choices/CaptureChoice.ts: -------------------------------------------------------------------------------- 1 | import { Choice } from "./Choice"; 2 | import type ICaptureChoice from "./ICaptureChoice"; 3 | import { NewTabDirection } from "../newTabDirection"; 4 | import type { FileViewMode } from "../fileViewMode"; 5 | 6 | export class CaptureChoice extends Choice implements ICaptureChoice { 7 | appendLink: boolean; 8 | captureTo: string; 9 | captureToActiveFile: boolean; 10 | createFileIfItDoesntExist: { 11 | enabled: boolean; 12 | createWithTemplate: boolean; 13 | template: string; 14 | }; 15 | format: { enabled: boolean; format: string }; 16 | insertAfter: { 17 | enabled: boolean; 18 | after: string; 19 | insertAtEnd: boolean; 20 | considerSubsections: boolean; 21 | createIfNotFound: boolean; 22 | createIfNotFoundLocation: string; 23 | }; 24 | prepend: boolean; 25 | task: boolean; 26 | openFileInNewTab: { 27 | enabled: boolean; 28 | direction: NewTabDirection; 29 | focus: boolean; 30 | }; 31 | openFile: boolean; 32 | openFileInMode: FileViewMode; 33 | 34 | constructor(name: string) { 35 | super(name, "Capture"); 36 | 37 | this.appendLink = false; 38 | this.captureTo = ""; 39 | this.captureToActiveFile = false; 40 | this.createFileIfItDoesntExist = { 41 | enabled: false, 42 | createWithTemplate: false, 43 | template: "", 44 | }; 45 | this.format = { enabled: false, format: "" }; 46 | this.insertAfter = { 47 | enabled: false, 48 | after: "", 49 | insertAtEnd: false, 50 | considerSubsections: false, 51 | createIfNotFound: false, 52 | createIfNotFoundLocation: "top", 53 | }; 54 | this.prepend = false; 55 | this.task = false; 56 | this.openFileInNewTab = { 57 | enabled: false, 58 | direction: NewTabDirection.vertical, 59 | focus: true, 60 | }; 61 | this.openFile = false; 62 | this.openFileInMode = "default"; 63 | } 64 | 65 | public static Load(choice: ICaptureChoice): CaptureChoice { 66 | return choice as CaptureChoice; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/types/choices/Choice.ts: -------------------------------------------------------------------------------- 1 | import type { ChoiceType } from "./choiceType"; 2 | import { v4 as uuidv4 } from "uuid"; 3 | import type IChoice from "./IChoice"; 4 | 5 | export abstract class Choice implements IChoice { 6 | id: string; 7 | name: string; 8 | type: ChoiceType; 9 | command: boolean; 10 | 11 | protected constructor(name: string, type: ChoiceType) { 12 | this.id = uuidv4(); 13 | this.name = name; 14 | this.type = type; 15 | this.command = false; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/types/choices/ICaptureChoice.ts: -------------------------------------------------------------------------------- 1 | import type IChoice from "./IChoice"; 2 | import type { NewTabDirection } from "../newTabDirection"; 3 | import type { FileViewMode } from "../fileViewMode"; 4 | 5 | export default interface ICaptureChoice extends IChoice { 6 | captureTo: string; 7 | captureToActiveFile: boolean; 8 | createFileIfItDoesntExist: { 9 | enabled: boolean; 10 | createWithTemplate: boolean; 11 | template: string; 12 | }; 13 | format: { enabled: boolean; format: string }; 14 | /** Capture to bottom of file (after current file content). */ 15 | prepend: boolean; 16 | appendLink: boolean; 17 | task: boolean; 18 | insertAfter: { 19 | enabled: boolean; 20 | after: string; 21 | insertAtEnd: boolean; 22 | considerSubsections: boolean; 23 | createIfNotFound: boolean; 24 | createIfNotFoundLocation: string; 25 | }; 26 | openFileInNewTab: { 27 | enabled: boolean; 28 | direction: NewTabDirection; 29 | focus: boolean; 30 | }; 31 | openFile: boolean; 32 | openFileInMode: FileViewMode; 33 | } 34 | -------------------------------------------------------------------------------- /src/types/choices/IChoice.ts: -------------------------------------------------------------------------------- 1 | import type { ChoiceType } from "./choiceType"; 2 | 3 | export default interface IChoice { 4 | name: string; 5 | id: string; 6 | type: ChoiceType; 7 | command: boolean; 8 | } 9 | -------------------------------------------------------------------------------- /src/types/choices/IMacroChoice.ts: -------------------------------------------------------------------------------- 1 | import type IChoice from "./IChoice"; 2 | import type { IMacro } from "../macros/IMacro"; 3 | 4 | export default interface IMacroChoice extends IChoice { 5 | macro?: IMacro; 6 | macroId: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/types/choices/IMultiChoice.ts: -------------------------------------------------------------------------------- 1 | import type IChoice from "./IChoice"; 2 | 3 | export default interface IMultiChoice extends IChoice { 4 | choices: IChoice[]; 5 | collapsed: boolean; 6 | } 7 | -------------------------------------------------------------------------------- /src/types/choices/ITemplateChoice.ts: -------------------------------------------------------------------------------- 1 | import type IChoice from "./IChoice"; 2 | import type { NewTabDirection } from "../newTabDirection"; 3 | import type { FileViewMode } from "../fileViewMode"; 4 | import type { fileExistsChoices } from "src/constants"; 5 | 6 | export default interface ITemplateChoice extends IChoice { 7 | templatePath: string; 8 | folder: { 9 | enabled: boolean; 10 | folders: string[]; 11 | chooseWhenCreatingNote: boolean; 12 | createInSameFolderAsActiveFile: boolean; 13 | chooseFromSubfolders: boolean; 14 | }; 15 | fileNameFormat: { enabled: boolean; format: string }; 16 | appendLink: boolean; 17 | openFile: boolean; 18 | openFileInNewTab: { 19 | enabled: boolean; 20 | direction: NewTabDirection; 21 | focus: boolean; 22 | }; 23 | openFileInMode: FileViewMode; 24 | fileExistsMode: (typeof fileExistsChoices)[number]; 25 | setFileExistsBehavior: boolean; 26 | } 27 | -------------------------------------------------------------------------------- /src/types/choices/MacroChoice.ts: -------------------------------------------------------------------------------- 1 | import { Choice } from "./Choice"; 2 | import type IMacroChoice from "./IMacroChoice"; 3 | import type { IMacro } from "../macros/IMacro"; 4 | 5 | export class MacroChoice extends Choice implements IMacroChoice { 6 | macro?: IMacro; 7 | macroId = ""; 8 | 9 | constructor(name: string) { 10 | super(name, "Macro"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/types/choices/MultiChoice.ts: -------------------------------------------------------------------------------- 1 | import { Choice } from "./Choice"; 2 | import type IChoice from "./IChoice"; 3 | import type IMultiChoice from "./IMultiChoice"; 4 | 5 | export class MultiChoice extends Choice implements IMultiChoice { 6 | choices: IChoice[] = []; 7 | collapsed: boolean; 8 | 9 | constructor(name: string) { 10 | super(name, "Multi"); 11 | } 12 | 13 | public addChoice(choice: IChoice): MultiChoice { 14 | this.choices.push(choice); 15 | return this; 16 | } 17 | 18 | public addChoices(choices: IChoice[]): MultiChoice { 19 | this.choices.push(...choices); 20 | return this; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/types/choices/TemplateChoice.ts: -------------------------------------------------------------------------------- 1 | import type ITemplateChoice from "./ITemplateChoice"; 2 | import { Choice } from "./Choice"; 3 | import { NewTabDirection } from "../newTabDirection"; 4 | import type { FileViewMode } from "../fileViewMode"; 5 | import type { fileExistsChoices } from "src/constants"; 6 | 7 | export class TemplateChoice extends Choice implements ITemplateChoice { 8 | appendLink: boolean; 9 | fileNameFormat: { enabled: boolean; format: string }; 10 | folder: { 11 | enabled: boolean; 12 | folders: string[]; 13 | chooseWhenCreatingNote: boolean; 14 | createInSameFolderAsActiveFile: boolean; 15 | chooseFromSubfolders: boolean; 16 | }; 17 | openFileInNewTab: { 18 | enabled: boolean; 19 | direction: NewTabDirection; 20 | focus: boolean; 21 | }; 22 | openFile: boolean; 23 | openFileInMode: FileViewMode; 24 | templatePath: string; 25 | fileExistsMode: (typeof fileExistsChoices)[number]; 26 | setFileExistsBehavior: boolean; 27 | 28 | constructor(name: string) { 29 | super(name, "Template"); 30 | 31 | this.templatePath = ""; 32 | this.fileNameFormat = { enabled: false, format: "" }; 33 | this.folder = { 34 | enabled: false, 35 | folders: [], 36 | chooseWhenCreatingNote: false, 37 | createInSameFolderAsActiveFile: false, 38 | chooseFromSubfolders: false, 39 | }; 40 | this.appendLink = false; 41 | this.openFileInNewTab = { 42 | enabled: false, 43 | direction: NewTabDirection.vertical, 44 | focus: true, 45 | }; 46 | this.openFile = false; 47 | this.openFileInMode = "default"; 48 | this.fileExistsMode = "Increment the file name"; 49 | this.setFileExistsBehavior = false; 50 | } 51 | 52 | public static Load(choice: ITemplateChoice): TemplateChoice { 53 | return choice as TemplateChoice; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/types/choices/choiceType.ts: -------------------------------------------------------------------------------- 1 | export type ChoiceType = "Capture" | "Macro" | "Multi" | "Template"; 2 | -------------------------------------------------------------------------------- /src/types/fileViewMode.ts: -------------------------------------------------------------------------------- 1 | export type FileViewMode = "source" | "preview" | "default"; 2 | -------------------------------------------------------------------------------- /src/types/macros/ChoiceCommand.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "./Command"; 2 | import { CommandType } from "./CommandType"; 3 | import type { IChoiceCommand } from "./IChoiceCommand"; 4 | 5 | export class ChoiceCommand extends Command implements IChoiceCommand { 6 | choiceId: string; 7 | 8 | constructor(name: string, choiceId: string) { 9 | super(name, CommandType.Choice); 10 | 11 | this.choiceId = choiceId; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/types/macros/Command.ts: -------------------------------------------------------------------------------- 1 | import type { CommandType } from "./CommandType"; 2 | import type { ICommand } from "./ICommand"; 3 | import { v4 as uuidv4 } from "uuid"; 4 | 5 | export abstract class Command implements ICommand { 6 | name: string; 7 | type: CommandType; 8 | id: string; 9 | 10 | protected constructor(name: string, type: CommandType) { 11 | this.name = name; 12 | this.type = type; 13 | this.id = uuidv4(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/types/macros/CommandType.ts: -------------------------------------------------------------------------------- 1 | export enum CommandType { 2 | Obsidian = "Obsidian", 3 | UserScript = "UserScript", 4 | Choice = "Choice", 5 | Wait = "Wait", 6 | NestedChoice = "NestedChoice", 7 | EditorCommand = "EditorCommand", 8 | AIAssistant = "AIAssistant", 9 | InfiniteAIAssistant = "InfiniteAIAssistant", 10 | } 11 | -------------------------------------------------------------------------------- /src/types/macros/EditorCommands/CopyCommand.ts: -------------------------------------------------------------------------------- 1 | import { EditorCommandType } from "./EditorCommandType"; 2 | import type { App } from "obsidian"; 3 | import { EditorCommand } from "./EditorCommand"; 4 | 5 | export class CopyCommand extends EditorCommand { 6 | constructor() { 7 | super(EditorCommandType.Copy); 8 | } 9 | 10 | static async run(app: App) { 11 | const selectedText: string = EditorCommand.getSelectedText(app); 12 | await navigator.clipboard.writeText(selectedText); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/types/macros/EditorCommands/CutCommand.ts: -------------------------------------------------------------------------------- 1 | import { EditorCommandType } from "./EditorCommandType"; 2 | import type { App } from "obsidian"; 3 | import { EditorCommand } from "./EditorCommand"; 4 | import { log } from "../../../logger/logManager"; 5 | 6 | export class CutCommand extends EditorCommand { 7 | constructor() { 8 | super(EditorCommandType.Cut); 9 | } 10 | 11 | static async run(app: App) { 12 | const selectedText: string = EditorCommand.getSelectedText(app); 13 | const activeView = EditorCommand.getActiveMarkdownView(app); 14 | 15 | if (!selectedText) { 16 | log.logError("nothing selected."); 17 | return; 18 | } 19 | 20 | await navigator.clipboard.writeText(selectedText); 21 | activeView.editor.replaceSelection(""); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/types/macros/EditorCommands/EditorCommand.ts: -------------------------------------------------------------------------------- 1 | import type { EditorCommandType } from "./EditorCommandType"; 2 | import { CommandType } from "../CommandType"; 3 | import { Command } from "../Command"; 4 | import type { IEditorCommand } from "./IEditorCommand"; 5 | import type { App } from "obsidian"; 6 | import { MarkdownView } from "obsidian"; 7 | import { log } from "../../../logger/logManager"; 8 | 9 | export abstract class EditorCommand extends Command implements IEditorCommand { 10 | editorCommandType: EditorCommandType; 11 | 12 | protected constructor(type: EditorCommandType) { 13 | super(type, CommandType.EditorCommand); 14 | 15 | this.editorCommandType = type; 16 | } 17 | 18 | static getSelectedText(app: App): string { 19 | return this.getActiveMarkdownView(app).editor.getSelection(); 20 | } 21 | 22 | static getActiveMarkdownView(app: App): MarkdownView { 23 | const activeView = app.workspace.getActiveViewOfType(MarkdownView); 24 | 25 | if (!activeView) { 26 | log.logError("no active markdown view."); 27 | throw new Error("no active markdown view."); 28 | } 29 | 30 | return activeView; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/types/macros/EditorCommands/EditorCommandType.ts: -------------------------------------------------------------------------------- 1 | export enum EditorCommandType { 2 | Cut = "Cut", 3 | Copy = "Copy", 4 | Paste = "Paste", 5 | SelectActiveLine = "Select active line", 6 | SelectLinkOnActiveLine = "Select link on active line", 7 | } 8 | -------------------------------------------------------------------------------- /src/types/macros/EditorCommands/IEditorCommand.ts: -------------------------------------------------------------------------------- 1 | import type { ICommand } from "../ICommand"; 2 | import type { EditorCommandType } from "./EditorCommandType"; 3 | 4 | export interface IEditorCommand extends ICommand { 5 | editorCommandType: EditorCommandType; 6 | } 7 | -------------------------------------------------------------------------------- /src/types/macros/EditorCommands/PasteCommand.ts: -------------------------------------------------------------------------------- 1 | import { EditorCommandType } from "./EditorCommandType"; 2 | import type { App } from "obsidian"; 3 | import { log } from "../../../logger/logManager"; 4 | import { EditorCommand } from "./EditorCommand"; 5 | 6 | export class PasteCommand extends EditorCommand { 7 | constructor() { 8 | super(EditorCommandType.Paste); 9 | } 10 | 11 | static async run(app: App) { 12 | const clipboard = await navigator.clipboard.readText(); 13 | const activeView = EditorCommand.getActiveMarkdownView(app); 14 | 15 | if (!activeView) { 16 | log.logError("no active markdown view."); 17 | return; 18 | } 19 | 20 | activeView.editor.replaceSelection(clipboard); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/types/macros/EditorCommands/SelectActiveLineCommand.ts: -------------------------------------------------------------------------------- 1 | import { EditorCommandType } from "./EditorCommandType"; 2 | import { EditorCommand } from "./EditorCommand"; 3 | import type { App } from "obsidian"; 4 | 5 | export class SelectActiveLineCommand extends EditorCommand { 6 | constructor() { 7 | super(EditorCommandType.SelectActiveLine); 8 | } 9 | 10 | public static run(app: App) { 11 | const activeView = EditorCommand.getActiveMarkdownView(app); 12 | 13 | const { line: lineNumber } = activeView.editor.getCursor(); 14 | const line = activeView.editor.getLine(lineNumber); 15 | const lineLength = line.length; 16 | 17 | activeView.editor.setSelection( 18 | { line: lineNumber, ch: 0 }, 19 | { line: lineNumber, ch: lineLength } 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/types/macros/EditorCommands/SelectLinkOnActiveLineCommand.ts: -------------------------------------------------------------------------------- 1 | import { EditorCommand } from "./EditorCommand"; 2 | import { EditorCommandType } from "./EditorCommandType"; 3 | import type { App } from "obsidian"; 4 | import { WIKI_LINK_REGEX } from "../../../constants"; 5 | import { log } from "../../../logger/logManager"; 6 | 7 | export class SelectLinkOnActiveLineCommand extends EditorCommand { 8 | constructor() { 9 | super(EditorCommandType.SelectLinkOnActiveLine); 10 | } 11 | 12 | static run(app: App) { 13 | const activeView = EditorCommand.getActiveMarkdownView(app); 14 | 15 | const { line: lineNumber } = activeView.editor.getCursor(); 16 | const line = activeView.editor.getLine(lineNumber); 17 | 18 | const match = WIKI_LINK_REGEX.exec(line); 19 | if (!match) { 20 | log.logError(`no internal link found on line ${lineNumber}.`); 21 | return; 22 | } 23 | 24 | const matchStart: number = match.index; 25 | const matchEnd: number = match[0].length + matchStart; 26 | 27 | activeView.editor.setSelection( 28 | { line: lineNumber, ch: matchStart }, 29 | { line: lineNumber, ch: matchEnd } 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/types/macros/IChoiceCommand.ts: -------------------------------------------------------------------------------- 1 | import type { ICommand } from "./ICommand"; 2 | 3 | export interface IChoiceCommand extends ICommand { 4 | choiceId: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/types/macros/ICommand.ts: -------------------------------------------------------------------------------- 1 | import type { CommandType } from "./CommandType"; 2 | 3 | export interface ICommand { 4 | name: string; 5 | type: CommandType; 6 | id: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/types/macros/IMacro.ts: -------------------------------------------------------------------------------- 1 | import type { ICommand } from "./ICommand"; 2 | 3 | export interface IMacro { 4 | name: string; 5 | id: string; 6 | commands: ICommand[]; 7 | runOnStartup: boolean; 8 | } 9 | -------------------------------------------------------------------------------- /src/types/macros/IObsidianCommand.ts: -------------------------------------------------------------------------------- 1 | import type { ICommand } from "./ICommand"; 2 | 3 | export interface IObsidianCommand extends ICommand { 4 | commandId: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/types/macros/IUserScript.ts: -------------------------------------------------------------------------------- 1 | import type { ICommand } from "./ICommand"; 2 | 3 | export interface IUserScript extends ICommand { 4 | path: string; 5 | settings: { [key: string]: unknown }; 6 | } 7 | -------------------------------------------------------------------------------- /src/types/macros/ObsidianCommand.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "./Command"; 2 | import { CommandType } from "./CommandType"; 3 | import type { IObsidianCommand } from "./IObsidianCommand"; 4 | import { v4 as uuidv4 } from "uuid"; 5 | 6 | export class ObsidianCommand extends Command implements IObsidianCommand { 7 | name: string; 8 | id: string; 9 | commandId: string; 10 | type: CommandType; 11 | 12 | constructor(name: string, commandId: string) { 13 | super(name, CommandType.Obsidian); 14 | this.commandId = commandId; 15 | } 16 | 17 | generateId = () => (this.id = uuidv4()); 18 | } 19 | -------------------------------------------------------------------------------- /src/types/macros/QuickAddMacro.ts: -------------------------------------------------------------------------------- 1 | import type { ICommand } from "./ICommand"; 2 | import type { IMacro } from "./IMacro"; 3 | import { v4 as uuidv4 } from "uuid"; 4 | 5 | export class QuickAddMacro implements IMacro { 6 | id: string; 7 | name: string; 8 | commands: ICommand[]; 9 | runOnStartup: boolean; 10 | 11 | constructor(name: string) { 12 | this.name = name; 13 | this.id = uuidv4(); 14 | this.commands = []; 15 | this.runOnStartup = false; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/types/macros/QuickCommands/AIAssistantCommand.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../Command"; 2 | import { CommandType } from "../CommandType"; 3 | import type { IAIAssistantCommand } from "./IAIAssistantCommand"; 4 | import { settingsStore } from "src/settingsStore"; 5 | import type { OpenAIModelParameters } from "src/ai/OpenAIModelParameters"; 6 | import { DEFAULT_FREQUENCY_PENALTY, DEFAULT_PRESENCE_PENALTY, DEFAULT_TEMPERATURE, DEFAULT_TOP_P } from "src/ai/OpenAIModelParameters"; 7 | 8 | export class AIAssistantCommand extends Command implements IAIAssistantCommand { 9 | id: string; 10 | name: string; 11 | type: CommandType; 12 | 13 | model: string; 14 | systemPrompt: string; 15 | outputVariableName: string; 16 | promptTemplate: { 17 | enable: boolean; 18 | name: string; 19 | }; 20 | modelParameters: Partial; 21 | 22 | constructor() { 23 | super("AI Assistant", CommandType.AIAssistant); 24 | 25 | const defaults = settingsStore.getState().ai; 26 | 27 | this.model = defaults.defaultModel; 28 | this.systemPrompt = defaults.defaultSystemPrompt; 29 | this.outputVariableName = "output"; 30 | this.promptTemplate = { enable: false, name: "" }; 31 | this.modelParameters = { 32 | temperature: DEFAULT_TEMPERATURE, 33 | top_p: DEFAULT_TOP_P, 34 | frequency_penalty: DEFAULT_FREQUENCY_PENALTY, 35 | presence_penalty: DEFAULT_PRESENCE_PENALTY, 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/types/macros/QuickCommands/IAIAssistantCommand.ts: -------------------------------------------------------------------------------- 1 | import type { ICommand } from "../ICommand"; 2 | import type { OpenAIModelParameters } from "src/ai/OpenAIModelParameters"; 3 | 4 | interface IBaseAIAssistantCommand extends ICommand { 5 | model: string; 6 | systemPrompt: string; 7 | outputVariableName: string; 8 | modelParameters: Partial; 9 | } 10 | 11 | export interface IAIAssistantCommand extends IBaseAIAssistantCommand { 12 | model: string; 13 | promptTemplate: { 14 | enable: boolean; 15 | name: string; 16 | }; 17 | } 18 | 19 | export interface IInfiniteAIAssistantCommand extends IBaseAIAssistantCommand { 20 | model: string; 21 | resultJoiner: string; 22 | chunkSeparator: string; 23 | maxChunkTokens: number; 24 | mergeChunks: boolean; 25 | } 26 | -------------------------------------------------------------------------------- /src/types/macros/QuickCommands/INestedChoiceCommand.ts: -------------------------------------------------------------------------------- 1 | import type { ICommand } from "../ICommand"; 2 | import type IChoice from "../../choices/IChoice"; 3 | 4 | export interface INestedChoiceCommand extends ICommand { 5 | choice: IChoice; 6 | } 7 | -------------------------------------------------------------------------------- /src/types/macros/QuickCommands/IWaitCommand.ts: -------------------------------------------------------------------------------- 1 | import type { ICommand } from "../ICommand"; 2 | 3 | export interface IWaitCommand extends ICommand { 4 | time: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/types/macros/QuickCommands/NestedChoiceCommand.ts: -------------------------------------------------------------------------------- 1 | import { CommandType } from "../CommandType"; 2 | import { Command } from "../Command"; 3 | import type IChoice from "../../choices/IChoice"; 4 | import type { INestedChoiceCommand } from "./INestedChoiceCommand"; 5 | 6 | export class NestedChoiceCommand 7 | extends Command 8 | implements INestedChoiceCommand 9 | { 10 | choice: IChoice; 11 | 12 | constructor(choice: IChoice) { 13 | super(choice.name, CommandType.NestedChoice); 14 | 15 | this.choice = choice; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/types/macros/QuickCommands/WaitCommand.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../Command"; 2 | import { CommandType } from "../CommandType"; 3 | import type { IWaitCommand } from "./IWaitCommand"; 4 | 5 | export class WaitCommand extends Command implements IWaitCommand { 6 | id: string; 7 | name: string; 8 | time: number; 9 | type: CommandType; 10 | 11 | constructor(time: number) { 12 | super("Wait", CommandType.Wait); 13 | this.time = time; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/types/macros/UserScript.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "./Command"; 2 | import { CommandType } from "./CommandType"; 3 | import type { IUserScript } from "./IUserScript"; 4 | 5 | export class UserScript extends Command implements IUserScript { 6 | name: string; 7 | path: string; 8 | type: CommandType; 9 | settings: { [key: string]: unknown }; 10 | 11 | constructor(name: string, path: string) { 12 | super(name, CommandType.UserScript); 13 | this.path = path; 14 | this.settings = {}; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/types/newTabDirection.ts: -------------------------------------------------------------------------------- 1 | export enum NewTabDirection { 2 | vertical = "vertical", 3 | horizontal = "horizontal", 4 | } 5 | -------------------------------------------------------------------------------- /src/utility.ts: -------------------------------------------------------------------------------- 1 | export function waitFor(ms: number): Promise { 2 | return new Promise((res) => setTimeout(res, ms)); 3 | } 4 | 5 | export function getLinesInString(input: string) { 6 | return input.split("\n"); 7 | } 8 | 9 | // https://stackoverflow.com/questions/3115150/how-to-escape-regular-expression-special-characters-using-javascript 10 | export function escapeRegExp(text: string) { 11 | return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); 12 | } -------------------------------------------------------------------------------- /src/utils/errorUtils.ts: -------------------------------------------------------------------------------- 1 | import { log } from "../logger/logManager"; 2 | import type { ErrorLevel } from "../logger/errorLevel"; 3 | import { ErrorLevel as ErrorLevelEnum } from "../logger/errorLevel"; 4 | 5 | /** 6 | * Maximum number of errors to keep in the error log 7 | */ 8 | export const MAX_ERROR_LOG_SIZE = 100; 9 | 10 | /** 11 | * Converts any value to an Error object, preserving the original Error if provided 12 | * 13 | * @param err - The error value to convert 14 | * @param contextMessage - Optional context message to prepend to error message 15 | * @returns A proper Error object with stack trace 16 | * 17 | * @example 18 | * ```ts 19 | * try { 20 | * // Some operation that might throw 21 | * } catch (err) { 22 | * const error = toError(err, "Failed during template processing"); 23 | * log.logError(error); 24 | * } 25 | * ``` 26 | */ 27 | export function toError(err: unknown, contextMessage?: string): Error { 28 | // If it's already an Error, just add context if needed 29 | if (err instanceof Error) { 30 | if (contextMessage) { 31 | err.message = `${contextMessage}: ${err.message}`; 32 | } 33 | return err; 34 | } 35 | 36 | // If it's a string, create a new Error with it 37 | if (typeof err === 'string') { 38 | return new Error(contextMessage ? `${contextMessage}: ${err}` : err); 39 | } 40 | 41 | // For everything else, convert to string and create an Error 42 | const errorMessage = contextMessage 43 | ? `${contextMessage}: ${String(err)}` 44 | : String(err); 45 | 46 | return new Error(errorMessage); 47 | } 48 | 49 | /** 50 | * Reports an error to the logging system with additional context 51 | * Converts any error type to a proper Error object and logs it with the appropriate level 52 | * 53 | * @param err - The error to report 54 | * @param contextMessage - Optional context message to add 55 | * @param level - Error level (defaults to ERROR) 56 | * 57 | * @example 58 | * ```ts 59 | * try { 60 | * // Some operation 61 | * } catch (err) { 62 | * reportError(err, "Failed during template processing"); 63 | * } 64 | * ``` 65 | */ 66 | export function reportError( 67 | err: unknown, 68 | contextMessage?: string, 69 | level: ErrorLevel = ErrorLevelEnum.Error 70 | ): void { 71 | const error = toError(err, contextMessage); 72 | 73 | switch (level) { 74 | case ErrorLevelEnum.Error: 75 | log.logError(error); 76 | break; 77 | case ErrorLevelEnum.Warning: 78 | log.logWarning(error); 79 | break; 80 | case ErrorLevelEnum.Log: 81 | log.logMessage(error); 82 | break; 83 | default: 84 | // Ensure exhaustiveness 85 | log.logError(error); 86 | } 87 | } 88 | 89 | /** 90 | * Error boundary - wraps a function and reports any errors it throws 91 | * 92 | * @param fn - Function to execute 93 | * @param contextMessage - Context message for any errors 94 | * @param level - Error level for logging 95 | * @returns The function's return value or undefined if an error occurred 96 | * 97 | * @example 98 | * ```ts 99 | * const result = withErrorHandling( 100 | * () => JSON.parse(someString), 101 | * "Failed to parse JSON" 102 | * ); 103 | * ``` 104 | */ 105 | export function withErrorHandling( 106 | fn: () => T, 107 | contextMessage?: string, 108 | level: ErrorLevel = ErrorLevelEnum.Error 109 | ): T | undefined { 110 | try { 111 | return fn(); 112 | } catch (err) { 113 | reportError(err, contextMessage, level); 114 | return undefined; 115 | } 116 | } 117 | 118 | /** 119 | * Async error boundary - wraps an async function and reports any errors it throws 120 | * 121 | * @param fn - Async function to execute 122 | * @param contextMessage - Context message for any errors 123 | * @param level - Error level for logging 124 | * @returns Promise resolving to the function's return value or undefined if an error occurred 125 | * 126 | * @example 127 | * ```ts 128 | * const result = await withAsyncErrorHandling( 129 | * () => fetch(url).then(r => r.json()), 130 | * "Failed to fetch data" 131 | * ); 132 | * ``` 133 | */ 134 | export async function withAsyncErrorHandling( 135 | fn: () => Promise, 136 | contextMessage?: string, 137 | level: ErrorLevel = ErrorLevelEnum.Error 138 | ): Promise { 139 | try { 140 | return await fn(); 141 | } catch (err) { 142 | reportError(err, contextMessage, level); 143 | return undefined; 144 | } 145 | } -------------------------------------------------------------------------------- /src/utils/invariant.ts: -------------------------------------------------------------------------------- 1 | export default function invariant( 2 | condition: unknown, 3 | message?: string | (() => string) 4 | ): asserts condition { 5 | if (!condition) { 6 | throw new Error(typeof message === "function" ? message() : message); 7 | } 8 | 9 | return; 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/setPasswordOnBlur.ts: -------------------------------------------------------------------------------- 1 | export function setPasswordOnBlur(el: HTMLInputElement) { 2 | el.addEventListener("focus", () => { 3 | el.type = "text"; 4 | }); 5 | 6 | el.addEventListener("blur", () => { 7 | el.type = "password"; 8 | }); 9 | 10 | el.type = "password"; 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "ES2020", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "isolatedModules": true, 13 | "types": ["svelte", "node"], 14 | "strictNullChecks": true, 15 | "lib": ["DOM", "ES5", "ES6", "ES7"], 16 | "experimentalDecorators": true 17 | }, 18 | "include": ["**/*.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = process.env.npm_package_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 | "0.2.16": "0.12.4", 3 | "0.4.17": "0.12.17", 4 | "0.4.18": "0.13.19", 5 | "0.4.21": "0.13.19", 6 | "0.5.5": "0.13.19", 7 | "0.6.0": "0.13.19", 8 | "0.6.1": "0.13.19", 9 | "0.6.2": "0.13.19", 10 | "0.6.3": "0.13.19", 11 | "0.7.0": "0.13.19", 12 | "0.8.0": "0.13.19", 13 | "0.9.0": "0.13.19", 14 | "0.9.1": "0.13.19", 15 | "0.9.2": "0.13.19", 16 | "0.9.3": "0.13.19", 17 | "0.10.0": "0.13.19", 18 | "0.10.1": "0.13.19", 19 | "0.11.0": "0.13.19", 20 | "0.11.1": "0.13.19", 21 | "0.11.2": "0.13.19", 22 | "0.11.3": "0.13.19", 23 | "0.11.4": "0.13.19", 24 | "0.11.5": "0.13.19", 25 | "0.11.6": "0.13.19", 26 | "0.11.7": "0.13.19", 27 | "0.11.8": "0.13.19", 28 | "0.11.9": "0.13.19", 29 | "0.11.10": "0.13.19", 30 | "0.11.11": "0.13.19", 31 | "0.12.0": "0.13.19", 32 | "0.13.0": "0.13.19", 33 | "0.14.0": "0.13.19", 34 | "0.15.0": "0.13.19", 35 | "0.16.0": "0.13.19", 36 | "0.17.0": "0.13.19", 37 | "0.17.1": "0.13.19", 38 | "0.18.0": "0.13.19", 39 | "0.18.1": "0.13.19", 40 | "0.18.2": "0.13.19", 41 | "0.18.3": "0.13.19", 42 | "0.19.0": "0.13.19", 43 | "0.19.1": "0.13.19", 44 | "0.19.2": "0.13.19", 45 | "0.19.3": "0.13.19", 46 | "0.19.4": "0.13.19", 47 | "0.20.0": "0.13.19", 48 | "0.20.1": "0.13.19", 49 | "0.21.0": "0.13.19", 50 | "0.22.0": "0.13.19", 51 | "0.23.0": "0.13.19", 52 | "1.0.0": "0.13.19", 53 | "1.0.1": "0.13.19", 54 | "1.0.2": "0.13.19", 55 | "1.1.0": "0.13.19", 56 | "1.2.0": "0.13.19", 57 | "1.2.1": "0.13.19", 58 | "1.3.0": "0.13.19", 59 | "1.4.0": "0.13.19", 60 | "1.5.0": "0.13.19", 61 | "1.6.0": "0.13.19", 62 | "1.6.1": "0.13.19", 63 | "1.7.0": "0.13.19", 64 | "1.8.0": "0.13.19", 65 | "1.8.1": "0.13.19", 66 | "1.9.0": "1.6.0", 67 | "1.9.1": "1.6.0", 68 | "1.9.2": "1.6.0", 69 | "1.10.0": "1.6.0", 70 | "1.11.0": "1.6.0", 71 | "1.11.1": "1.6.0", 72 | "1.11.2": "1.6.0", 73 | "1.11.3": "1.6.0", 74 | "1.11.4": "1.6.0", 75 | "1.11.5": "1.6.0", 76 | "1.12.0": "1.6.0", 77 | "1.13.0": "1.6.0", 78 | "1.13.1": "1.6.0", 79 | "1.13.2": "1.6.0", 80 | "1.13.3": "1.6.0" 81 | } -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | // import { svelte } from "@sveltejs/vite-plugin-svelte"; 3 | // import sveltePreprocess from "svelte-preprocess"; 4 | import * as path from "path"; 5 | 6 | export default defineConfig({ 7 | plugins: [ 8 | // svelte({ 9 | // hot: !process.env.VITEST, 10 | // preprocess: sveltePreprocess(), 11 | // }), 12 | ], 13 | resolve: { 14 | alias: { 15 | src: path.resolve("./src"), 16 | }, 17 | }, 18 | test: { 19 | include: ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], 20 | globals: true, 21 | environment: "jsdom", 22 | }, 23 | }); 24 | --------------------------------------------------------------------------------