├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── FUNDING.yml └── workflows │ └── release.yml ├── .gitignore ├── .gitmodules ├── .npmrc ├── LICENSE ├── README.md ├── esbuild.config.mjs ├── manifest.json ├── package-lock.json ├── package.json ├── src ├── Notice.ts ├── components │ ├── CompareTypeSelect.svelte │ ├── FileOrFolderSelect.svelte │ └── index.ts ├── inbox-helpers.ts ├── main.ts ├── obsidian │ ├── markdown-file-info-helpers.ts │ ├── obsidian.d.ts │ ├── tabstractfile-helpers.ts │ ├── vault-helpers.ts │ └── workspace-helpers.ts ├── register-events.ts ├── settings-tab │ ├── InboxSettings.svelte │ ├── SettingsTab.svelte │ └── SettingsTab.ts ├── settings │ ├── Inbox.ts │ ├── InboxPluginSettingsV1.ts │ ├── InboxPluginSettingsV2.ts │ ├── TrackingTypes.ts │ ├── migrate-settings.test.ts │ └── migrate-settings.ts ├── store.ts └── walkthrough │ ├── WalkthroughAction.ts │ ├── WalkthroughStatus.ts │ ├── WalkthroughView.svelte │ ├── WalkthroughView.ts │ └── walkthrough-state-machine.ts ├── tsconfig.json ├── version-bump.mjs └── versions.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 4 10 | tab_width = 4 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | npm node_modules 2 | build -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "no-prototype-builtins": "off", 21 | "@typescript-eslint/no-empty-function": "off" 22 | } 23 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: zachatoo # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | # patreon: # Replace with a single Patreon username 5 | # open_collective: # Replace with a single Open Collective username 6 | ko_fi: zachatoo # Replace with a single Ko-fi username 7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | # liberapay: # Replace with a single Liberapay username 10 | # issuehunt: # Replace with a single IssueHunt username 11 | # otechie: # Replace with a single Otechie username 12 | # lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Obsidian Plugin 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | with: 12 | fetch-depth: 0 # otherwise, you will failed to push refs to dest repo 13 | - name: Use Node.js 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: "14.x" 17 | # Get the version number and put it in a variable 18 | - name: Get Version 19 | id: version 20 | run: | 21 | echo "::set-output name=tag::$(git describe --abbrev=0)" 22 | # Build the plugin 23 | - name: Build 24 | id: build 25 | run: | 26 | npm install 27 | npm run build --if-present 28 | # Package the required files into a zip 29 | - name: Package 30 | run: | 31 | mkdir ${{ github.event.repository.name }} 32 | cp main.js manifest.json README.md ${{ github.event.repository.name }} 33 | zip -r ${{ github.event.repository.name }}.zip ${{ github.event.repository.name }} 34 | # Create the release on github 35 | - name: Create Release 36 | id: create_release 37 | uses: actions/create-release@v1 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | VERSION: ${{ github.ref }} 41 | with: 42 | tag_name: ${{ github.ref }} 43 | release_name: ${{ github.ref }} 44 | draft: false 45 | prerelease: false 46 | # Upload the packaged release file 47 | - name: Upload zip file 48 | id: upload-zip 49 | uses: actions/upload-release-asset@v1 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | with: 53 | upload_url: ${{ steps.create_release.outputs.upload_url }} 54 | asset_path: ./${{ github.event.repository.name }}.zip 55 | asset_name: ${{ github.event.repository.name }}-${{ steps.version.outputs.tag }}.zip 56 | asset_content_type: application/zip 57 | # Upload the main.js 58 | - name: Upload main.js 59 | id: upload-main 60 | uses: actions/upload-release-asset@v1 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | with: 64 | upload_url: ${{ steps.create_release.outputs.upload_url }} 65 | asset_path: ./main.js 66 | asset_name: main.js 67 | asset_content_type: text/javascript 68 | # Upload the manifest.json 69 | - name: Upload manifest.json 70 | id: upload-manifest 71 | uses: actions/upload-release-asset@v1 72 | env: 73 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 74 | with: 75 | upload_url: ${{ steps.create_release.outputs.upload_url }} 76 | asset_path: ./manifest.json 77 | asset_name: manifest.json 78 | asset_content_type: application/json 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | main.js 14 | 15 | # Exclude sourcemaps 16 | *.map 17 | 18 | # obsidian 19 | data.json 20 | 21 | # Exclude macOS Finder (System Explorer) View States 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "external/obsidian-plugin-scripts"] 2 | path = external/obsidian-plugin-scripts 3 | url = https://github.com/Zachatoo/obsidian-plugin-scripts.git 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Zach Young 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 | ![Obsidian Downloads](https://img.shields.io/badge/dynamic/json?logo=obsidian&color=%23483699&label=downloads&query=%24%5B%22inbox%22%5D.downloads&url=https%3A%2F%2Fraw.githubusercontent.com%2Fobsidianmd%2Fobsidian-releases%2Fmaster%2Fcommunity-plugin-stats.json) 2 | 3 | # Obsidian Inbox 4 | 5 | When using a third party tool to quickly write content to your Obsidian vault without opening your vault, often it's easiest to write it to an "inbox" note or folder to process later. 6 | 7 | This plugin will let you know if there's content to process in your inbox note/folder when launching Obsidian. 8 | 9 | ## Installation 10 | 11 | Recommended to install from the Obsidian community store. 12 | 13 | You can manually install this using the [BRAT](https://github.com/TfTHacker/obsidian42-brat) Obsidian plugin. Generic installation instructions are available on that plugin's documentation. 14 | 15 | ## Settings 16 | 17 | ### Track note or folder 18 | 19 | Whether to track inbox by referencing the contents of a single file or a folder of files. 20 | 21 | ### Inbox path 22 | 23 | Path for inbox note/folder. 24 | 25 | For example, if your inbox note is in the root of your vault and called "Mobile Inbox", then the path would be "Mobile Inbox.md". 26 | 27 | ### Compare type 28 | 29 | What to compare the inbox note contents to when deciding whether or not to notify. 'Compare to last tracked' will compare to a snapshot from when Obsidian was last closed. 'Compare to base' will compare to a base contents that you define. 30 | 31 | ### Inbox base contents 32 | 33 | If note content matches this exactly, then you will not be notified. This is only available if you select 'Compare to base' as the compare type. 34 | 35 | For example, if the "unproccessed" version of your inbox note looks like this 36 | 37 | ```md 38 | # Mobile Inbox 39 | ``` 40 | 41 | then you should set your inbox base contents to match. That way, you will only be notified if there's additional contents besides the heading. 42 | 43 | ### Inbox notice duration 44 | 45 | Duration to show Notice when there is data to process, in seconds. Set to `0` for infinite duration. Clear to use global default Notice duration. 46 | 47 | ## Attributions 48 | 49 | - Thank you to marcusolsson for [obsidian-svelte](https://github.com/marcusolsson/obsidian-svelte) that I used for creating many of the UI elements. 50 | -------------------------------------------------------------------------------- /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 | 7 | const banner = `/* 8 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 9 | if you want to view the source, please visit the github repository of this plugin 10 | */ 11 | `; 12 | 13 | const prod = process.argv[2] === "production"; 14 | 15 | esbuild 16 | .build({ 17 | plugins: [ 18 | esbuildSvelte({ 19 | compilerOptions: { css: true }, 20 | preprocess: sveltePreprocess(), 21 | }), 22 | ], 23 | banner: { 24 | js: banner, 25 | }, 26 | entryPoints: ["src/main.ts"], 27 | bundle: true, 28 | external: [ 29 | "obsidian", 30 | "electron", 31 | "@codemirror/autocomplete", 32 | "@codemirror/collab", 33 | "@codemirror/commands", 34 | "@codemirror/language", 35 | "@codemirror/lint", 36 | "@codemirror/search", 37 | "@codemirror/state", 38 | "@codemirror/view", 39 | "@lezer/common", 40 | "@lezer/highlight", 41 | "@lezer/lr", 42 | ...builtins, 43 | ], 44 | format: "cjs", 45 | watch: !prod, 46 | target: "es2018", 47 | logLevel: "info", 48 | sourcemap: prod ? false : "inline", 49 | treeShaking: true, 50 | outfile: "main.js", 51 | }) 52 | .catch(() => process.exit(1)); 53 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "inbox", 3 | "name": "Inbox", 4 | "version": "3.0.2", 5 | "minAppVersion": "0.15.0", 6 | "description": "Show in app notification if there is data to process in the \"inbox\" note.", 7 | "author": "Zachatoo", 8 | "authorUrl": "https://zachyoung.dev", 9 | "fundingUrl": "https://github.com/sponsors/Zachatoo", 10 | "isDesktopOnly": false 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-inbox", 3 | "version": "3.0.2", 4 | "description": "Show in app notification if there is data to process in the \"inbox\" note/folder.", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "test": "vitest", 10 | "test:ci": "vitest run", 11 | "version": "npx ts-node --esm external/obsidian-plugin-scripts/version-bump.mts && git add package.json package-lock.json manifest.json versions.json" 12 | }, 13 | "keywords": [ 14 | "obsidian-plugin" 15 | ], 16 | "author": "Zachatoo", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "@tsconfig/svelte": "^3.0.0", 20 | "@types/node": "^16.11.6", 21 | "@typescript-eslint/eslint-plugin": "5.29.0", 22 | "@typescript-eslint/parser": "5.29.0", 23 | "builtin-modules": "3.3.0", 24 | "esbuild": "0.14.47", 25 | "esbuild-svelte": "^0.7.3", 26 | "eslint-plugin-svelte3": "^4.0.0", 27 | "obsidian": "1.5.7-1", 28 | "svelte": "^3.55.1", 29 | "svelte-preprocess": "^5.0.1", 30 | "tslib": "2.4.0", 31 | "typescript": "^4.9.0", 32 | "vitest": "^0.34.4" 33 | }, 34 | "dependencies": { 35 | "@popperjs/core": "^2.11.6", 36 | "obsidian-svelte": "^0.1.10", 37 | "svelte-portal": "^2.2.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Notice.ts: -------------------------------------------------------------------------------- 1 | import { Notice } from "obsidian"; 2 | 3 | const DEFAULT_NOTICE_TIMEOUT_SECONDS = 5; 4 | 5 | export class InfoNotice extends Notice { 6 | constructor( 7 | message: string | DocumentFragment, 8 | timeout = DEFAULT_NOTICE_TIMEOUT_SECONDS 9 | ) { 10 | super(`Inbox\n${message}`, timeout * 1000); 11 | console.info(`obsidian-inbox: ${message}`); 12 | } 13 | } 14 | 15 | export class ErrorNotice extends Notice { 16 | constructor( 17 | message: string | DocumentFragment, 18 | timeout = DEFAULT_NOTICE_TIMEOUT_SECONDS 19 | ) { 20 | super(`Inbox\n${message}`, timeout * 1000); 21 | console.error(`obsidian-inbox: ${message}`); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/CompareTypeSelect.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | 34 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CompareTypeSelect } from "./CompareTypeSelect.svelte"; 2 | export { default as FileOrFolderSelect } from "./FileOrFolderSelect.svelte"; 3 | -------------------------------------------------------------------------------- /src/inbox-helpers.ts: -------------------------------------------------------------------------------- 1 | import { get } from "svelte/store"; 2 | import store from "./store"; 3 | import type { TrackingType } from "./settings/TrackingTypes"; 4 | import { ErrorNotice } from "./Notice"; 5 | import { App, TFile } from "obsidian"; 6 | import { 7 | getAllFilesInFolderRecursive, 8 | readFile, 9 | } from "./obsidian/tabstractfile-helpers"; 10 | import { getFolders } from "./obsidian/vault-helpers"; 11 | 12 | export function setTrackingType(trackingType: TrackingType, index: number) { 13 | const settings = get(store); 14 | const matchingInbox = settings.inboxes.at(index); 15 | if (!matchingInbox) { 16 | new ErrorNotice(`Failed to find inbox at index ${index}.`); 17 | return; 18 | } 19 | matchingInbox.trackingType = trackingType; 20 | matchingInbox.path = ""; 21 | store.set(settings); 22 | } 23 | 24 | export async function setInboxNote({ 25 | app, 26 | notePath, 27 | index, 28 | }: { 29 | app: App; 30 | notePath: string; 31 | index: number; 32 | }) { 33 | const matchingFile = app.vault 34 | .getMarkdownFiles() 35 | .find((file) => file.path === notePath); 36 | if (!matchingFile || !(matchingFile instanceof TFile)) { 37 | new ErrorNotice( 38 | `Failed to set inbox note, ${notePath} could not be found or is not a note.` 39 | ); 40 | return; 41 | } 42 | 43 | const settings = get(store); 44 | const matchingInbox = settings.inboxes.at(index); 45 | if (!matchingInbox) { 46 | new ErrorNotice(`Failed to find inbox at index ${index}.`); 47 | return; 48 | } 49 | 50 | matchingInbox.path = notePath; 51 | const contents = await readFile(app, matchingFile); 52 | matchingInbox.inboxNoteContents = contents; 53 | store.set(settings); 54 | } 55 | 56 | export async function setInboxFolder({ 57 | app, 58 | folderPath, 59 | index, 60 | }: { 61 | app: App; 62 | folderPath: string; 63 | index: number; 64 | }) { 65 | const folder = getFolders(app.vault).find( 66 | (folder) => folder.path === folderPath 67 | ); 68 | if (!folder) { 69 | new ErrorNotice( 70 | `Failed to set inbox folder, ${folderPath} could not be found.` 71 | ); 72 | return; 73 | } 74 | 75 | const settings = get(store); 76 | const matchingInbox = settings.inboxes.at(index); 77 | if (!matchingInbox) { 78 | new ErrorNotice(`Failed to find inbox at index ${index}.`); 79 | return; 80 | } 81 | 82 | matchingInbox.path = folder.path; 83 | const filesInFolder = getAllFilesInFolderRecursive(folder); 84 | filesInFolder.sort((a, b) => a.localeCompare(b)); 85 | matchingInbox.inboxFolderFiles.sort((a, b) => a.localeCompare(b)); 86 | matchingInbox.inboxFolderFiles = filesInFolder; 87 | store.set(settings); 88 | } 89 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { Platform, Plugin, TFile, WorkspaceLeaf, TFolder } from "obsidian"; 2 | import { get } from "svelte/store"; 3 | import store from "./store"; 4 | import { getAllFilesInFolderRecursive } from "./obsidian/tabstractfile-helpers"; 5 | import { findMarkdownLeavesMatchingPath } from "./obsidian/workspace-helpers"; 6 | import { ErrorNotice, InfoNotice } from "./Notice"; 7 | import { 8 | DEFAULT_SETTINGS, 9 | isInboxPluginSettingsV2, 10 | } from "./settings/InboxPluginSettingsV2"; 11 | import { migrateSettings } from "./settings/migrate-settings"; 12 | import { SettingsTab } from "./settings-tab/SettingsTab"; 13 | import { 14 | InboxWalkthroughView, 15 | VIEW_TYPE_WALKTHROUGH, 16 | } from "./walkthrough/WalkthroughView"; 17 | import { WalkthroughStatuses } from "./walkthrough/WalkthroughStatus"; 18 | import { registerEvents } from "./register-events"; 19 | 20 | export default class InboxPlugin extends Plugin { 21 | hasPerformedCheck: boolean; 22 | 23 | async onload() { 24 | this.hasPerformedCheck = false; 25 | await this.loadSettings(); 26 | 27 | this.register( 28 | store.subscribe(async (settings) => { 29 | await this.saveData(settings); 30 | }) 31 | ); 32 | 33 | this.registerView( 34 | VIEW_TYPE_WALKTHROUGH, 35 | (leaf) => new InboxWalkthroughView(leaf, this) 36 | ); 37 | 38 | registerEvents(this); 39 | 40 | this.addSettingTab(new SettingsTab(this.app, this)); 41 | 42 | this.app.workspace.onLayoutReady(async () => { 43 | const settings = get(store); 44 | if (settings.walkthroughStatus === WalkthroughStatuses.unstarted) { 45 | store.walkthrough.start(); 46 | this.ensureWalkthroughViewExists(); 47 | } else { 48 | await this.notifyIfInboxNeedsProcessing(); 49 | } 50 | }); 51 | } 52 | 53 | onunload() { 54 | this.app.workspace.detachLeavesOfType(VIEW_TYPE_WALKTHROUGH); 55 | this.hasPerformedCheck = false; 56 | } 57 | 58 | async loadSettings() { 59 | let settings: unknown = Object.assign( 60 | {}, 61 | DEFAULT_SETTINGS, 62 | await this.loadData() 63 | ); 64 | 65 | settings = migrateSettings(settings); 66 | 67 | if (isInboxPluginSettingsV2(settings)) { 68 | store.set(settings); 69 | } else { 70 | new ErrorNotice( 71 | `Failed to load settings.\nSettings could not be migrated to match schema.\n${settings}` 72 | ); 73 | } 74 | } 75 | 76 | ensureWalkthroughViewExists(active = false) { 77 | const { workspace } = this.app; 78 | 79 | let leaf: WorkspaceLeaf | null; 80 | const existingPluginLeaves = workspace.getLeavesOfType( 81 | VIEW_TYPE_WALKTHROUGH 82 | ); 83 | 84 | // There's already an existing leaf with our view, do not create leaf 85 | if (existingPluginLeaves.length > 0) { 86 | leaf = existingPluginLeaves[0]; 87 | } else { 88 | // View doesn't exist yet, reate it and make it visible 89 | leaf = workspace.getRightLeaf(false); 90 | if (leaf) { 91 | workspace.revealLeaf(leaf); 92 | leaf.setViewState({ type: VIEW_TYPE_WALKTHROUGH }); 93 | } 94 | } 95 | 96 | if (active && leaf) { 97 | workspace.setActiveLeaf(leaf); 98 | } 99 | } 100 | 101 | getIsWalkthroughViewOpen() { 102 | return ( 103 | this.app.workspace.getLeavesOfType(VIEW_TYPE_WALKTHROUGH).length > 0 104 | ); 105 | } 106 | 107 | async notifyIfInboxNeedsProcessing() { 108 | const settings = get(store); 109 | try { 110 | if (settings.inboxes.length > 0) { 111 | const updatedInboxes = await Promise.all( 112 | settings.inboxes.map(async (inbox, index) => { 113 | if (!inbox.path) { 114 | return inbox; 115 | } 116 | 117 | const inboxAbstractFile = 118 | this.app.vault.getAbstractFileByPath(inbox.path); 119 | if (!inboxAbstractFile) { 120 | new ErrorNotice( 121 | `Failed to find inbox ${inbox.trackingType.toString()} at path ${ 122 | inbox.path 123 | }.` 124 | ); 125 | return inbox; 126 | } 127 | 128 | let shouldNotify = false; 129 | if (inboxAbstractFile instanceof TFile) { 130 | const contents = ( 131 | await this.app.vault.read(inboxAbstractFile) 132 | ).trim(); 133 | switch (inbox.compareType) { 134 | case "compareToBase": 135 | shouldNotify = 136 | contents !== 137 | inbox.inboxNoteBaseContents.trim(); 138 | break; 139 | case "compareToLastTracked": 140 | shouldNotify = 141 | contents !== 142 | inbox.inboxNoteContents.trim(); 143 | break; 144 | default: 145 | break; 146 | } 147 | inbox.inboxNoteContents = contents; 148 | } else if (inboxAbstractFile instanceof TFolder) { 149 | const filesInFolder = 150 | getAllFilesInFolderRecursive(inboxAbstractFile); 151 | filesInFolder.sort((a, b) => a.localeCompare(b)); 152 | inbox.inboxFolderFiles.sort((a, b) => 153 | a.localeCompare(b) 154 | ); 155 | shouldNotify = 156 | filesInFolder.join("") !== 157 | inbox.inboxFolderFiles.join(""); 158 | inbox.inboxFolderFiles = filesInFolder; 159 | } 160 | 161 | if (shouldNotify) { 162 | const enableClickToView = 163 | Platform.isDesktop && 164 | inboxAbstractFile instanceof TFile; 165 | const baseMessage = `You have data to process in ${inbox.path}`; 166 | const message = enableClickToView 167 | ? `${baseMessage}\nClick to dismiss, or right click to view inbox note.` 168 | : `${baseMessage}\nClick to dismiss.`; 169 | const notice = new InfoNotice( 170 | message, 171 | inbox.noticeDurationSeconds ?? undefined 172 | ); 173 | 174 | if (enableClickToView) { 175 | notice.noticeEl.oncontextmenu = () => { 176 | this.ensureLeafAtPathIsActive(inbox.path); 177 | notice.hide(); 178 | }; 179 | } 180 | } 181 | return inbox; 182 | }) 183 | ); 184 | settings.inboxes = updatedInboxes; 185 | store.set(settings); 186 | } 187 | } catch (error) { 188 | new ErrorNotice(`Failed to process inboxes.\n${error}`); 189 | } 190 | this.hasPerformedCheck = true; 191 | } 192 | 193 | ensureLeafAtPathIsActive(path: string) { 194 | const leavesMatchingPath = findMarkdownLeavesMatchingPath( 195 | this.app.workspace, 196 | path 197 | ); 198 | if (leavesMatchingPath.some(Boolean)) { 199 | this.app.workspace.setActiveLeaf(leavesMatchingPath[0], { 200 | focus: true, 201 | }); 202 | return; 203 | } 204 | 205 | const file = this.app.vault.getAbstractFileByPath(path); 206 | if (file instanceof TFile) { 207 | const leaf = this.app.workspace.getLeaf(true); 208 | leaf.openFile(file); 209 | return; 210 | } 211 | 212 | new ErrorNotice(`Failed to find note at path ${path}.`); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/obsidian/markdown-file-info-helpers.ts: -------------------------------------------------------------------------------- 1 | import type { MarkdownFileInfo } from "obsidian"; 2 | 3 | export function getValueFromMarkdownFileInfo( 4 | fileInfo: MarkdownFileInfo 5 | ): string { 6 | // Prefer using editor from public api if available 7 | // Is not available if in preview mode in canvas 8 | if (fileInfo.editor) { 9 | return fileInfo.editor.getValue(); 10 | } 11 | 12 | // Fallback to data not exposed in public api 13 | // Likely will not be hit 14 | return fileInfo.data ?? ""; 15 | } 16 | -------------------------------------------------------------------------------- /src/obsidian/obsidian.d.ts: -------------------------------------------------------------------------------- 1 | import "obsidian"; 2 | 3 | declare module "obsidian" { 4 | interface Notice { 5 | noticeEl: HTMLElement; 6 | } 7 | 8 | interface MarkdownFileInfo { 9 | data: string | null | undefined; 10 | } 11 | 12 | interface App { 13 | setting: { 14 | open: () => void; 15 | close: () => void; 16 | }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/obsidian/tabstractfile-helpers.ts: -------------------------------------------------------------------------------- 1 | import { App, TFile, TFolder } from "obsidian"; 2 | 3 | export function getAllFilesInFolderRecursive(folder: TFolder): string[] { 4 | return folder.children.flatMap((child) => { 5 | if (child instanceof TFolder) { 6 | return getAllFilesInFolderRecursive(child); 7 | } 8 | return child.path; 9 | }); 10 | } 11 | 12 | export async function readFile(app: App, file: TFile): Promise { 13 | return (await app.vault.read(file)).trim(); 14 | } 15 | -------------------------------------------------------------------------------- /src/obsidian/vault-helpers.ts: -------------------------------------------------------------------------------- 1 | import { TFolder, Vault } from "obsidian"; 2 | 3 | export function getFolders(vault: Vault): TFolder[] { 4 | return vault 5 | .getAllLoadedFiles() 6 | .filter((x): x is TFolder => x instanceof TFolder); 7 | } 8 | -------------------------------------------------------------------------------- /src/obsidian/workspace-helpers.ts: -------------------------------------------------------------------------------- 1 | import { MarkdownView, WorkspaceLeaf, type Workspace } from "obsidian"; 2 | 3 | export function findMarkdownLeavesMatchingPath( 4 | workspace: Workspace, 5 | path: string 6 | ) { 7 | const results: WorkspaceLeaf[] = []; 8 | workspace.iterateAllLeaves((leaf) => { 9 | if (leaf.view instanceof MarkdownView) { 10 | const leafFilePath = leaf.getViewState().state?.file; 11 | if (leafFilePath && leafFilePath === path) { 12 | results.push(leaf); 13 | } 14 | } 15 | }); 16 | return results; 17 | } 18 | -------------------------------------------------------------------------------- /src/register-events.ts: -------------------------------------------------------------------------------- 1 | import { get } from "svelte/store"; 2 | import type InboxPlugin from "./main"; 3 | import store from "./store"; 4 | import { TrackingTypes } from "./settings/TrackingTypes"; 5 | 6 | export function registerEvents(plugin: InboxPlugin) { 7 | plugin.registerEvent( 8 | plugin.app.metadataCache.on("changed", async (file, data, cache) => { 9 | const settings = get(store); 10 | if (plugin.hasPerformedCheck) { 11 | const matchingInboxes = settings.inboxes.filter( 12 | (inbox) => 13 | inbox.trackingType === TrackingTypes.note && 14 | inbox.path === file.path 15 | ); 16 | if (matchingInboxes.length > 0) { 17 | for (const inbox of matchingInboxes) { 18 | inbox.inboxNoteContents = data.trim(); 19 | } 20 | store.set(settings); 21 | } 22 | } 23 | }) 24 | ); 25 | 26 | plugin.registerEvent( 27 | plugin.app.vault.on("create", async (file) => { 28 | const settings = get(store); 29 | if (plugin.hasPerformedCheck) { 30 | const matchingInboxes = settings.inboxes.filter( 31 | (inbox) => 32 | inbox.trackingType === TrackingTypes.folder && 33 | file.path.startsWith(inbox.path) 34 | ); 35 | if (matchingInboxes.length > 0) { 36 | for (const inbox of matchingInboxes) { 37 | inbox.inboxFolderFiles.push(file.name); 38 | inbox.inboxFolderFiles.sort((a, b) => 39 | a.localeCompare(b) 40 | ); 41 | } 42 | store.set(settings); 43 | } 44 | } 45 | }) 46 | ); 47 | 48 | plugin.registerEvent( 49 | plugin.app.vault.on("rename", async (file, oldPath) => { 50 | const settings = get(store); 51 | if (plugin.hasPerformedCheck) { 52 | const oldName = oldPath.split("/").at(-1); 53 | const matchingInboxes = settings.inboxes.filter( 54 | (inbox) => 55 | inbox.trackingType === TrackingTypes.folder && 56 | inbox.inboxFolderFiles.includes(file.name) 57 | ); 58 | if (matchingInboxes.length > 0) { 59 | for (const inbox of matchingInboxes) { 60 | inbox.inboxFolderFiles = [ 61 | ...inbox.inboxFolderFiles.filter( 62 | (x) => x !== oldName 63 | ), 64 | file.name, 65 | ]; 66 | inbox.inboxFolderFiles.sort((a, b) => 67 | a.localeCompare(b) 68 | ); 69 | } 70 | store.set(settings); 71 | } 72 | } 73 | }) 74 | ); 75 | 76 | plugin.registerEvent( 77 | plugin.app.vault.on("delete", async (file) => { 78 | const settings = get(store); 79 | if (plugin.hasPerformedCheck) { 80 | const matchingInboxes = settings.inboxes.filter( 81 | (inbox) => 82 | inbox.trackingType === TrackingTypes.folder && 83 | file.path.startsWith(inbox.path) 84 | ); 85 | if (matchingInboxes.length > 0) { 86 | for (const inbox of matchingInboxes) { 87 | inbox.inboxFolderFiles = inbox.inboxFolderFiles.filter( 88 | (x) => x !== file.name 89 | ); 90 | inbox.inboxFolderFiles.sort((a, b) => 91 | a.localeCompare(b) 92 | ); 93 | } 94 | store.set(settings); 95 | } 96 | } 97 | }) 98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /src/settings-tab/InboxSettings.svelte: -------------------------------------------------------------------------------- 1 | 32 | 33 |
34 |
35 | { 38 | if (detail !== inbox.trackingType) { 39 | setTrackingType(detail, index); 40 | } 41 | }} 42 | /> 43 | 44 | {#if inbox.trackingType === "note"} 45 | file.path} 50 | on:change={async ({ detail }) => { 51 | if (detail !== inbox.path) { 52 | await setInboxNote({ app, notePath: detail, index }); 53 | } 54 | }} 55 | /> 56 | 57 | { 60 | $store.inboxes[index].compareType = detail; 61 | }} 62 | /> 63 | 64 | {#if inbox.compareType === "compareToBase"} 65 |