├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml └── workflows │ └── release.yml ├── .gitignore ├── .npmrc ├── README.md ├── image ├── Daily-Note-View.gif ├── Daily-Note-View.mp4 └── daily-note.svg ├── manifest.json ├── package.json ├── pnpm-lock.yaml ├── src ├── component │ ├── DailyNote.svelte │ ├── DailyNoteEditorView.svelte │ └── UpAndDownNavigate.ts ├── dailyNoteSettings.ts ├── dailyNoteView.ts ├── dailyNoteViewIndex.ts ├── global.d.ts ├── leafView.ts ├── style │ └── index.css ├── types │ ├── obsidian.d.ts │ └── time.d.ts └── utils │ ├── fileManager.ts │ ├── icon.ts │ └── utils.ts ├── styles.css ├── tsconfig.json ├── version-bump.mjs ├── versions.json └── vite.config.mjs /.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 = space 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": { 5 | "node": true 6 | }, 7 | "plugins": [ 8 | "@typescript-eslint" 9 | ], 10 | "extends": [ 11 | "eslint:recommended", 12 | "plugin:@typescript-eslint/eslint-recommended", 13 | "plugin:@typescript-eslint/recommended", 14 | "plugin:svelte/recommended" 15 | ], 16 | "parserOptions": { 17 | "sourceType": "module", 18 | "parser": "@typescript-eslint/parser" 19 | }, 20 | "rules": { 21 | "no-unused-vars": "off", 22 | "no-undef": "warn", 23 | "@typescript-eslint/no-unused-vars": [ 24 | "error", 25 | { 26 | "args": "none" 27 | } 28 | ], 29 | "@typescript-eslint/no-explicit-any": "off", 30 | "prefer-const": "warn", 31 | "@typescript-eslint/ban-ts-comment": "off", 32 | "no-prototype-builtins": "off", 33 | "@typescript-eslint/no-empty-function": "off" 34 | }, 35 | "overrides": [ 36 | { 37 | "files": [ 38 | "*.svelte" 39 | ], 40 | "parser": "svelte-eslint-parser" 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: [ "bug" ] 5 | body: 6 | - type: textarea 7 | id: bug-description 8 | attributes: 9 | label: Bug Description 10 | description: A clear and concise description of the bug. 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: screenshot 15 | attributes: 16 | label: Relevant Screenshot 17 | description: If applicable, add screenshots or a screen recording to help explain your problem. 18 | - type: textarea 19 | id: reproduction-steps 20 | attributes: 21 | label: To Reproduce 22 | description: Steps to reproduce the problem 23 | placeholder: | 24 | For example: 25 | 1. Go to '...' 26 | 2. Click on '...' 27 | 3. Scroll down to '...' 28 | - type: input 29 | id: obsi-version 30 | attributes: 31 | label: Obsidian Version 32 | description: You can find the version in the *About* Tab of the settings. 33 | placeholder: 0.13.19 34 | validations: 35 | required: true 36 | - type: checkboxes 37 | id: dailynote 38 | attributes: 39 | label: Which plugin are you using for daily notes? 40 | options: 41 | - label: Default Daily Note Plugin 42 | - label: Periodic Notes Plugin 43 | - type: checkboxes 44 | id: checklist 45 | attributes: 46 | label: Checklist 47 | options: 48 | - label: I updated to the latest version of the plugin. 49 | required: true 50 | 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea 3 | title: "Feature Request: " 4 | labels: [ "feature request" ] 5 | body: 6 | - type: textarea 7 | id: feature-requested 8 | attributes: 9 | label: Feature Requested 10 | description: A clear and concise description of the feature. 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: screenshot 15 | attributes: 16 | label: Relevant Screenshot 17 | description: If applicable, add screenshots or a screen recording to help explain the request. 18 | - type: checkboxes 19 | id: checklist 20 | attributes: 21 | label: Checklist 22 | options: 23 | - label: The feature would be useful to more users than just me. 24 | required: true 25 | 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Obsidian plugin 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | env: 8 | PLUGIN_NAME: obsidian-daily-note-editor 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Use Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: 16 19 | - name: Build 20 | id: build 21 | run: | 22 | npm install 23 | npm run build 24 | mkdir ${{ env.PLUGIN_NAME }} 25 | cp main.js manifest.json styles.css ${{ env.PLUGIN_NAME }} 26 | zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }} 27 | ls 28 | echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)" 29 | - name: Upload zip file 30 | id: upload-zip 31 | uses: actions/upload-release-asset@v1 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | with: 35 | upload_url: ${{ github.event.release.upload_url }} 36 | asset_path: ./${{ env.PLUGIN_NAME }}.zip 37 | asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip 38 | asset_content_type: application/zip 39 | 40 | - name: Upload main.js 41 | id: upload-main 42 | uses: actions/upload-release-asset@v1 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | with: 46 | upload_url: ${{ github.event.release.upload_url }} 47 | asset_path: ./main.js 48 | asset_name: main.js 49 | asset_content_type: text/javascript 50 | 51 | - name: Upload manifest.json 52 | id: upload-manifest 53 | uses: actions/upload-release-asset@v1 54 | env: 55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | with: 57 | upload_url: ${{ github.event.release.upload_url }} 58 | asset_path: ./manifest.json 59 | asset_name: manifest.json 60 | asset_content_type: application/json 61 | 62 | - name: Upload styles.css 63 | id: upload-css 64 | uses: actions/upload-release-asset@v1 65 | env: 66 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 67 | with: 68 | upload_url: ${{ github.event.release.upload_url }} 69 | asset_path: ./styles.css 70 | asset_name: styles.css 71 | asset_content_type: text/css 72 | -------------------------------------------------------------------------------- /.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 | 24 | 25 | # Rar Zip Files 26 | *.rar 27 | *.zip 28 | *.7z 29 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # Daily Notes Editor 6 | 7 | A plugin for you to edit a bunch of daily notes in one page(inline), which works similar to Roam Research's default daily note view. 8 | 9 | ![Daily-Note-View](https://raw.githubusercontent.com/Quorafind/Obsidian-Daily-Notes-Editor/master/image/Daily-Note-View.gif) 10 | 11 | # Features 12 | 13 | - Open a daily note editor page to edit a bunch of daily notes in one page/inline. 14 | - You can open it from command. 15 | - You can open it from left ribbon. 16 | - View and filter notes by different time ranges: 17 | - Week, Month, Year, Quarter 18 | - Last Week, Last Month, Last Year, Last Quarter 19 | - All notes 20 | - Custom date range 21 | - Multiple view modes: 22 | - Daily Notes mode - view your daily notes 23 | - Folder mode - view all notes from a specific folder 24 | - Tag mode - view all notes with a specific tag 25 | - Sort options: 26 | - Sort by creation time (newest/oldest first) 27 | - Sort by modification time (newest/oldest first) 28 | - Customization options: 29 | - Hide frontmatter in daily notes 30 | - Hide backlinks in daily notes 31 | - Create and open Daily Notes Editor on startup 32 | - Save custom presets for quick access to specific folders or tags 33 | - Navigate between notes using keyboard shortcuts (up/down) 34 | - Right-click on folders to open all notes in that folder in the Daily Notes Editor 35 | 36 | # Installation 37 | 38 | - Install from Obsidian Community Plugins 39 | 40 | # Settings 41 | 42 | - **Hide frontmatter**: Toggle to hide frontmatter in daily notes 43 | - **Hide backlinks**: Toggle to hide backlinks in daily notes 44 | - **Create and open Daily Notes Editor on startup**: Automatically create today's daily note and open the Daily Notes Editor when Obsidian starts 45 | - **Presets**: Save and manage custom presets for quick access to specific folders or tags 46 | 47 | # Usage 48 | 49 | 1. Click the calendar icon in the left ribbon or use the command "Open Daily Notes Editor" 50 | 2. Use the action buttons in the view to: 51 | - Select time range (Week, Month, Year, All, etc.) 52 | - Change view mode (Daily, Folder, Tag) 53 | - Change sort order 54 | 3. Save your current selection as a preset for quick access later 55 | 4. Right-click on any folder in the file explorer to open all notes from that folder in the Daily Notes Editor 56 | 57 | # Thanks 58 | 59 | - [Hover Editor](https://github.com/nothingislost/obsidian-hover-editor): I use code from this plugin to generate workspace leaf in my view. 60 | - [Obsidian Daily Notes Interface](https://github.com/liamcain/obsidian-daily-notes-interface): I use code from this api to get all dailynotes and generate daily note. 61 | - [Make.md](https://www.make.md/): inspired me to create this plugin. 62 | 63 | # Say Thank You 64 | 65 | Please say thank you to NothingisLost && pjeby from [Hover Editor](https://github.com/nothingislost/obsidian-hover-editor) 66 | 67 | And [Make.md](https://www.make.md/) works with more features than this plugin, please consider to use it. 68 | 69 | --- 70 | 71 | If you still feel generous and enjoy this plugin then please support my work and enthusiasm by buying me a coffee 72 | on [https://www.buymeacoffee.com/boninall](https://www.buymeacoffee.com/boninall). 73 | 74 | 75 | -------------------------------------------------------------------------------- /image/Daily-Note-View.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quorafind/Obsidian-Daily-Notes-Editor/c4fd953fbf3516e12fcdbb8f664533c0398acea2/image/Daily-Note-View.gif -------------------------------------------------------------------------------- /image/Daily-Note-View.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quorafind/Obsidian-Daily-Notes-Editor/c4fd953fbf3516e12fcdbb8f664533c0398acea2/image/Daily-Note-View.mp4 -------------------------------------------------------------------------------- /image/daily-note.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "daily-notes-editor", 3 | "name": "Daily Notes Editor", 4 | "version": "1.1.0", 5 | "minAppVersion": "0.15.0", 6 | "description": "Edit a bunch of daily notes in one page(inline), which works similar to Roam Research's default daily note view.", 7 | "author": "Boninall", 8 | "authorUrl": "https://github.com/Quorafind", 9 | "fundingUrl": { 10 | "Buy Me a Coffee": "https://www.buymeacoffee.com/boninall", 11 | "爱发电": "https://afdian.net/a/boninall", 12 | "支付宝": "https://cdn.jsdelivr.net/gh/Quorafind/.github@main/IMAGE/%E6%94%AF%E4%BB%98%E5%AE%9D%E4%BB%98%E6%AC%BE%E7%A0%81.jpg" 13 | }, 14 | "isDesktopOnly": false 15 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "daily-notes-editor", 3 | "version": "1.1.0", 4 | "description": "A plugin for you to edit a bunch of daily notes in one page(inline), which works similar to Roam Research's default daily note view.", 5 | "main": "main.js", 6 | "scripts": { 7 | "lint": "npx eslint --fix --ext .ts,.svelte .", 8 | "dev": "npm run lint && vite build --watch --mode development", 9 | "build": "npx svelte-check && vite build --mode production", 10 | "version": "node version-bump.mjs && git add manifest.json versions.json" 11 | }, 12 | "keywords": [], 13 | "author": "Quorafind", 14 | "devDependencies": { 15 | "@rollup/plugin-node-resolve": "^15.2.3", 16 | "@rollup/plugin-replace": "^5.0.5", 17 | "@rollup/plugin-terser": "^0.4.4", 18 | "@sveltejs/vite-plugin-svelte": "^3.1.0", 19 | "@tsconfig/svelte": "^5.0.4", 20 | "@types/node": "^20.12.7", 21 | "@typescript-eslint/eslint-plugin": "6.21.0", 22 | "@typescript-eslint/parser": "6.21.0", 23 | "builtin-modules": "3.3.0", 24 | "esbuild": "0.14.47", 25 | "eslint": "^8.57.0", 26 | "eslint-plugin-svelte": "^2.37.0", 27 | "obsidian": "^1.8.7", 28 | "obsidian-daily-notes-interface": "^0.9.4", 29 | "svelte": "^4.2.15", 30 | "svelte-check": "3.7.1", 31 | "svelte-eslint-parser": "^0.35.0", 32 | "svelte-inview": "^4.0.2", 33 | "svelte-preprocess": "^5.1.4", 34 | "tslib": "2.4.0", 35 | "typescript": "5.3.3", 36 | "vite": "^5.2.10" 37 | }, 38 | "dependencies": { 39 | "@codemirror/state": "^6.5.2", 40 | "@codemirror/view": "^6.36.3", 41 | "monkey-around": "^2.3.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/component/DailyNote.svelte: -------------------------------------------------------------------------------- 1 | 176 | 177 |
178 |
179 | {#if title} 180 |
181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | {title} 190 | 191 | 192 | 193 |
194 | {/if} 195 | 203 |
204 |
205 | 206 | 312 | -------------------------------------------------------------------------------- /src/component/DailyNoteEditorView.svelte: -------------------------------------------------------------------------------- 1 | 265 | 266 |
267 | {#if renderedFiles.length === 0} 268 |
269 |
270 | No files found 271 |
272 |
273 | {/if} 274 | {#if selectionMode === "daily" && !fileManager?.hasCurrentDayNote() && (selectedRange === 'all' || selectedRange === 'week' || selectedRange === 'month' || selectedRange === 'year' || selectedRange === 'quarter')} 275 | 280 | {/if} 281 | {#each renderedFiles as file (file.path)} 282 |
handleNoteVisibilityChange(file, detail.inView)}> 287 | 293 |
294 | {/each} 295 |
299 | {#if !hasMore} 300 |
—— No more of results ——
301 | {/if} 302 |
303 | 304 | 305 | 352 | -------------------------------------------------------------------------------- /src/component/UpAndDownNavigate.ts: -------------------------------------------------------------------------------- 1 | import { 2 | App, 3 | Editor, 4 | editorInfoField, 5 | EditorPosition, 6 | MarkdownView, 7 | TFile, 8 | WorkspaceLeaf, 9 | } from "obsidian"; 10 | import { EditorView, KeyBinding, keymap } from "@codemirror/view"; 11 | import { Extension, Prec } from "@codemirror/state"; 12 | import { isDailyNoteLeaf } from "../leafView"; 13 | import DailyNoteViewPlugin from "src/dailyNoteViewIndex"; 14 | 15 | export interface UpAndDownNavigateOptions { 16 | app: App; 17 | plugin: DailyNoteViewPlugin; 18 | } 19 | 20 | /** 21 | * Get the current editor from a leaf 22 | */ 23 | function getEditor(leaf: WorkspaceLeaf): Editor | null { 24 | if (!leaf) return null; 25 | 26 | const view = leaf.view; 27 | if (!(view instanceof MarkdownView)) return null; 28 | 29 | return view.editor; 30 | } 31 | 32 | /** 33 | * Get the CodeMirror editor instance from a leaf 34 | */ 35 | export function getEditorView(leaf: WorkspaceLeaf): EditorView | null { 36 | if (!leaf) return null; 37 | 38 | const view = leaf.view; 39 | if (!(view instanceof MarkdownView)) return null; 40 | 41 | // Access the CodeMirror editor instance 42 | // @ts-ignore - Accessing private property 43 | return view.editor.cm; 44 | } 45 | 46 | /** 47 | * Find the next or previous leaf in the daily notes view 48 | * @param app The Obsidian app instance 49 | * @param currentLeaf The current leaf 50 | * @param direction 'up' or 'down' 51 | * @returns The next or previous leaf, or null if not found 52 | */ 53 | function findAdjacentLeaf( 54 | app: App, 55 | currentLeaf: WorkspaceLeaf, 56 | direction: "up" | "down" 57 | ): WorkspaceLeaf | null { 58 | if (!currentLeaf || !isDailyNoteLeaf(currentLeaf)) return null; 59 | 60 | // Get all daily note leaves 61 | const dailyNoteLeaves: WorkspaceLeaf[] = []; 62 | 63 | app.workspace.iterateAllLeaves((leaf) => { 64 | if (isDailyNoteLeaf(leaf)) { 65 | dailyNoteLeaves.push(leaf); 66 | } 67 | }); 68 | 69 | // Sort leaves by their position in the DOM 70 | dailyNoteLeaves.sort((a, b) => { 71 | const rectA = a.containerEl.getBoundingClientRect(); 72 | const rectB = b.containerEl.getBoundingClientRect(); 73 | return rectA.top - rectB.top; 74 | }); 75 | 76 | // Find the current leaf index 77 | const currentIndex = dailyNoteLeaves.findIndex( 78 | (leaf) => leaf === currentLeaf 79 | ); 80 | if (currentIndex === -1) return null; 81 | 82 | // Get the next or previous leaf 83 | const targetIndex = 84 | direction === "up" ? currentIndex - 1 : currentIndex + 1; 85 | 86 | // Return the target leaf if it exists 87 | return targetIndex >= 0 && targetIndex < dailyNoteLeaves.length 88 | ? dailyNoteLeaves[targetIndex] 89 | : null; 90 | } 91 | 92 | /** 93 | * Navigate to the adjacent leaf and focus its editor 94 | */ 95 | function navigateToAdjacentLeaf( 96 | app: App, 97 | currentLeaf: WorkspaceLeaf, 98 | direction: "up" | "down" 99 | ): boolean { 100 | const targetLeaf = findAdjacentLeaf(app, currentLeaf, direction); 101 | if (!targetLeaf) return false; 102 | 103 | // Focus the target leaf 104 | app.workspace.setActiveLeaf(targetLeaf, { focus: true }); 105 | 106 | // Get the editor 107 | const editor = getEditor(targetLeaf); 108 | if (!editor) return false; 109 | 110 | // Set cursor position based on direction 111 | let pos: EditorPosition; 112 | 113 | if (direction === "up") { 114 | // If navigating up, place cursor at the bottom of the document 115 | const lastLine = editor.lastLine(); 116 | const lastLineLength = editor.getLine(lastLine).length; 117 | pos = { line: lastLine, ch: lastLineLength }; 118 | } else { 119 | // If navigating down, place cursor at the top of the document 120 | pos = { line: 0, ch: 0 }; 121 | } 122 | 123 | // Set cursor position 124 | editor.setCursor(pos); 125 | 126 | // Ensure the cursor is visible by scrolling to it 127 | editor.scrollIntoView( 128 | { 129 | from: pos, 130 | to: pos, 131 | }, 132 | true 133 | ); 134 | 135 | // Focus the editor 136 | setTimeout(() => { 137 | // Try different methods to ensure focus 138 | if (targetLeaf.view instanceof MarkdownView) { 139 | // @ts-ignore - Accessing private property 140 | if (targetLeaf.view.editMode && targetLeaf.view.editMode.editor) { 141 | // @ts-ignore - Accessing private property 142 | targetLeaf.view.editMode.editor.focus(); 143 | } else { 144 | editor.focus(); 145 | } 146 | } 147 | }, 10); 148 | 149 | return true; 150 | } 151 | 152 | /** 153 | * Check if frontmatter is hidden in the current view 154 | */ 155 | function isFrontmatterHidden(plugin: DailyNoteViewPlugin): boolean { 156 | // @ts-ignore - Accessing settings property 157 | return plugin.settings?.hideFrontmatter === true; 158 | } 159 | 160 | /** 161 | * Check if the current position is at the first visible line when frontmatter is hidden 162 | */ 163 | function isAtFirstVisibleLine( 164 | view: EditorView, 165 | file: TFile, 166 | app: App, 167 | plugin: DailyNoteViewPlugin 168 | ): boolean { 169 | // If frontmatter is not hidden, just check if we're at line 1 170 | if (!isFrontmatterHidden(plugin)) { 171 | const pos = view.state.selection.main.head; 172 | const line = view.state.doc.lineAt(pos); 173 | return line.number === 1 && pos === line.from; 174 | } 175 | 176 | // If frontmatter is hidden, we need to check if we're at the first line after frontmatter 177 | const pos = view.state.selection.main.head; 178 | const line = view.state.doc.lineAt(pos); 179 | 180 | // Get the file's metadata cache to check frontmatter 181 | const fileCache = app.metadataCache.getFileCache(file); 182 | 183 | // If there's no frontmatter or we can't detect it, fall back to line 1 184 | if (!fileCache || !fileCache.frontmatter) { 185 | return line.number === 1 && pos === line.from; 186 | } 187 | 188 | // Get the end line of the frontmatter section 189 | const frontmatterEndLine = 190 | (fileCache.frontmatterPosition?.end?.line ?? 0) + 2; 191 | 192 | // Check if we're at the first line after frontmatter and at the beginning of that line 193 | return line.number === frontmatterEndLine && pos === line.from; 194 | } 195 | 196 | /** 197 | * Create the up and down navigation extension for CodeMirror 198 | */ 199 | export function createUpDownNavigationExtension( 200 | options: UpAndDownNavigateOptions 201 | ): Extension { 202 | const { app, plugin } = options; 203 | 204 | // Define key bindings 205 | const keyBindings: KeyBinding[] = [ 206 | { 207 | key: "ArrowUp", 208 | run: (view) => { 209 | if (!view.state) return false; 210 | // @ts-ignore 211 | const infoView = view.state.field(editorInfoField); 212 | 213 | // Get the current file 214 | // @ts-ignore 215 | const currentLeaf = infoView?.leaf; 216 | const currentFile = currentLeaf?.view?.file; 217 | 218 | // Check if we're at the first visible line (considering frontmatter) 219 | if ( 220 | currentFile && 221 | isAtFirstVisibleLine(view, currentFile, app, plugin) 222 | ) { 223 | if ( 224 | currentLeaf && 225 | navigateToAdjacentLeaf(app, currentLeaf, "up") 226 | ) { 227 | return true; 228 | } 229 | } 230 | 231 | // Let the default handler process the key 232 | return false; 233 | }, 234 | }, 235 | { 236 | key: "ArrowDown", 237 | run: (view) => { 238 | if (!view.state) return false; 239 | // Get the current cursor position 240 | const pos = view.state.selection.main.head; 241 | const line = view.state.doc.lineAt(pos); 242 | 243 | // @ts-ignore 244 | const infoView = view.state.field(editorInfoField); 245 | 246 | // Get the current file 247 | // @ts-ignore 248 | const currentLeaf = infoView?.leaf; 249 | 250 | // If cursor is at the last line and at the end of the line 251 | const lastLineNumber = view.state.doc.lines; 252 | if (line.number === lastLineNumber && pos === line.to) { 253 | if ( 254 | currentLeaf && 255 | navigateToAdjacentLeaf(app, currentLeaf, "down") 256 | ) { 257 | return true; 258 | } 259 | } 260 | 261 | // Let the default handler process the key 262 | return false; 263 | }, 264 | }, 265 | ]; 266 | 267 | return Prec.highest(keymap.of(keyBindings)); 268 | } 269 | -------------------------------------------------------------------------------- /src/dailyNoteSettings.ts: -------------------------------------------------------------------------------- 1 | import DailyNoteViewPlugin from "./dailyNoteViewIndex"; 2 | import { App, debounce, PluginSettingTab, Setting, Modal } from "obsidian"; 3 | 4 | export interface DailyNoteSettings { 5 | hideFrontmatter: boolean; 6 | hideBacklinks: boolean; 7 | createAndOpenOnStartup: boolean; 8 | useArrowUpOrDownToNavigate: boolean; 9 | 10 | preset: { 11 | type: "folder" | "tag"; 12 | target: string; 13 | }[]; 14 | } 15 | 16 | export const DEFAULT_SETTINGS: DailyNoteSettings = { 17 | hideFrontmatter: false, 18 | hideBacklinks: false, 19 | createAndOpenOnStartup: false, 20 | useArrowUpOrDownToNavigate: false, 21 | preset: [], 22 | }; 23 | 24 | export class DailyNoteSettingTab extends PluginSettingTab { 25 | plugin: DailyNoteViewPlugin; 26 | 27 | constructor(app: App, plugin: DailyNoteViewPlugin) { 28 | super(app, plugin); 29 | this.plugin = plugin; 30 | } 31 | 32 | debounceApplySettingsUpdate = debounce( 33 | async () => { 34 | await this.plugin.saveSettings(); 35 | }, 36 | 200, 37 | true 38 | ); 39 | 40 | debounceDisplay = debounce( 41 | async () => { 42 | await this.display(); 43 | }, 44 | 400, 45 | true 46 | ); 47 | 48 | applySettingsUpdate() { 49 | this.debounceApplySettingsUpdate(); 50 | } 51 | 52 | async display() { 53 | await this.plugin.loadSettings(); 54 | 55 | const { containerEl } = this; 56 | const settings = this.plugin.settings; 57 | 58 | containerEl.toggleClass("daily-note-settings-container", true); 59 | 60 | containerEl.empty(); 61 | 62 | new Setting(containerEl) 63 | .setName("Hide frontmatter") 64 | .setDesc("Hide frontmatter in daily notes") 65 | .addToggle((toggle) => 66 | toggle 67 | .setValue(settings.hideFrontmatter) 68 | .onChange(async (value) => { 69 | this.plugin.settings.hideFrontmatter = value; 70 | 71 | document.body.classList.toggle( 72 | "daily-notes-hide-frontmatter", 73 | value 74 | ); 75 | this.applySettingsUpdate(); 76 | }) 77 | ); 78 | 79 | new Setting(containerEl) 80 | .setName("Hide backlinks") 81 | .setDesc("Hide backlinks in daily notes") 82 | .addToggle((toggle) => 83 | toggle 84 | .setValue(settings.hideBacklinks) 85 | .onChange(async (value) => { 86 | this.plugin.settings.hideBacklinks = value; 87 | 88 | document.body.classList.toggle( 89 | "daily-notes-hide-backlinks", 90 | value 91 | ); 92 | this.applySettingsUpdate(); 93 | }) 94 | ); 95 | 96 | new Setting(containerEl) 97 | .setName("Create and open Daily Notes Editor on startup") 98 | .setDesc( 99 | "Automatically create today's daily note and open the Daily Notes Editor when Obsidian starts" 100 | ) 101 | .addToggle((toggle) => 102 | toggle 103 | .setValue(settings.createAndOpenOnStartup) 104 | .onChange(async (value) => { 105 | this.plugin.settings.createAndOpenOnStartup = value; 106 | this.applySettingsUpdate(); 107 | }) 108 | ); 109 | 110 | new Setting(containerEl) 111 | .setName("Use arrow up/down key to navigate between notes") 112 | .addToggle((toggle) => 113 | toggle 114 | .setValue(settings.useArrowUpOrDownToNavigate) 115 | .onChange(async (value) => { 116 | this.plugin.settings.useArrowUpOrDownToNavigate = value; 117 | this.applySettingsUpdate(); 118 | }) 119 | ); 120 | 121 | new Setting(containerEl).setName("Saved presets").setHeading(); 122 | 123 | const presetContainer = containerEl.createDiv("preset-container"); 124 | 125 | // Display existing presets 126 | if (settings.preset.length === 0) { 127 | presetContainer.createEl("p", { 128 | text: "No presets saved yet. Select a folder or tag in the Daily Notes Editor to create a preset.", 129 | cls: "no-presets-message", 130 | }); 131 | } else { 132 | settings.preset.forEach((preset, index) => { 133 | new Setting(containerEl) 134 | .setName( 135 | preset.type === "folder" 136 | ? "Focus on Folder: " 137 | : "Focus on Tag: " 138 | ) 139 | .setDesc(preset.target) 140 | .addButton((button) => { 141 | button.setIcon("trash"); 142 | button.onClick(() => { 143 | settings.preset.splice(index, 1); 144 | this.applySettingsUpdate(); 145 | this.debounceDisplay(); 146 | }); 147 | }); 148 | }); 149 | } 150 | 151 | // Add button to add a new preset 152 | new Setting(containerEl) 153 | .setName("Add new preset") 154 | .setDesc("Add a new folder or tag preset") 155 | .addButton((button) => { 156 | button 157 | .setButtonText("Add Preset") 158 | .setCta() 159 | .onClick(() => { 160 | const modal = new AddPresetModal( 161 | this.app, 162 | (type, target) => { 163 | // Check if this preset already exists 164 | const existingPresetIndex = 165 | settings.preset.findIndex( 166 | (p) => 167 | p.type === type && 168 | p.target === target 169 | ); 170 | 171 | // If it doesn't exist, add it 172 | if (existingPresetIndex === -1) { 173 | settings.preset.push({ 174 | type, 175 | target, 176 | }); 177 | this.applySettingsUpdate(); 178 | this.debounceDisplay(); 179 | } 180 | } 181 | ); 182 | modal.open(); 183 | }); 184 | }); 185 | } 186 | } 187 | 188 | // Add a new modal for adding presets 189 | class AddPresetModal extends Modal { 190 | saveCallback: (type: "folder" | "tag", target: string) => void; 191 | type: "folder" | "tag" = "folder"; 192 | targetInput: HTMLInputElement; 193 | 194 | constructor( 195 | app: App, 196 | saveCallback: (type: "folder" | "tag", target: string) => void 197 | ) { 198 | super(app); 199 | this.saveCallback = saveCallback; 200 | } 201 | 202 | onOpen() { 203 | const { contentEl } = this; 204 | contentEl.empty(); 205 | 206 | contentEl.createEl("h2", { text: "Add New Preset" }); 207 | 208 | const form = contentEl.createEl("form"); 209 | form.addEventListener("submit", (e) => { 210 | e.preventDefault(); 211 | this.save(); 212 | }); 213 | 214 | // Type selection 215 | const typeSetting = form.createDiv(); 216 | typeSetting.addClass("setting-item"); 217 | 218 | const typeSettingInfo = typeSetting.createDiv(); 219 | typeSettingInfo.addClass("setting-item-info"); 220 | typeSettingInfo.createEl("div", { 221 | text: "Preset Type", 222 | cls: "setting-item-name", 223 | }); 224 | 225 | const typeSettingControl = typeSetting.createDiv(); 226 | typeSettingControl.addClass("setting-item-control"); 227 | 228 | // Radio buttons for type selection 229 | const folderRadio = typeSettingControl.createEl("input", { 230 | type: "radio", 231 | attr: { 232 | name: "preset-type", 233 | id: "preset-type-folder", 234 | value: "folder", 235 | checked: true, 236 | }, 237 | }); 238 | typeSettingControl.createEl("label", { 239 | text: "Folder", 240 | attr: { 241 | for: "preset-type-folder", 242 | }, 243 | }); 244 | 245 | const tagRadio = typeSettingControl.createEl("input", { 246 | type: "radio", 247 | attr: { 248 | name: "preset-type", 249 | id: "preset-type-tag", 250 | value: "tag", 251 | }, 252 | }); 253 | typeSettingControl.createEl("label", { 254 | text: "Tag", 255 | attr: { 256 | for: "preset-type-tag", 257 | }, 258 | }); 259 | 260 | folderRadio.addEventListener("change", () => { 261 | if (folderRadio.checked) { 262 | this.type = "folder"; 263 | } 264 | }); 265 | 266 | tagRadio.addEventListener("change", () => { 267 | if (tagRadio.checked) { 268 | this.type = "tag"; 269 | } 270 | }); 271 | 272 | // Target input 273 | const targetSetting = form.createDiv(); 274 | targetSetting.addClass("setting-item"); 275 | 276 | const targetSettingInfo = targetSetting.createDiv(); 277 | targetSettingInfo.addClass("setting-item-info"); 278 | 279 | targetSettingInfo.createEl("div", { 280 | text: "Target", 281 | cls: "setting-item-name", 282 | }); 283 | 284 | targetSettingInfo.createEl("div", { 285 | text: "Enter the folder path or tag name", 286 | cls: "setting-item-description", 287 | }); 288 | 289 | const targetSettingControl = targetSetting.createDiv(); 290 | targetSettingControl.addClass("setting-item-control"); 291 | 292 | this.targetInput = targetSettingControl.createEl("input", { 293 | type: "text", 294 | value: "", 295 | placeholder: "Enter folder path or tag name", 296 | }); 297 | this.targetInput.addClass("target-input"); 298 | 299 | const footerEl = contentEl.createDiv(); 300 | footerEl.addClass("modal-button-container"); 301 | 302 | footerEl 303 | .createEl("button", { 304 | text: "Cancel", 305 | cls: "mod-warning", 306 | attr: { 307 | type: "button", 308 | }, 309 | }) 310 | .addEventListener("click", () => { 311 | this.close(); 312 | }); 313 | 314 | footerEl 315 | .createEl("button", { 316 | text: "Save", 317 | cls: "mod-cta", 318 | attr: { 319 | type: "submit", 320 | }, 321 | }) 322 | .addEventListener("click", (e) => { 323 | e.preventDefault(); 324 | this.save(); 325 | }); 326 | } 327 | 328 | save() { 329 | const target = this.targetInput.value.trim(); 330 | if (target) { 331 | this.saveCallback(this.type, target); 332 | this.close(); 333 | } 334 | } 335 | 336 | onClose() { 337 | const { contentEl } = this; 338 | contentEl.empty(); 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /src/dailyNoteView.ts: -------------------------------------------------------------------------------- 1 | import DailyNoteViewPlugin from "./dailyNoteViewIndex"; 2 | import { 3 | WorkspaceLeaf, 4 | ItemView, 5 | Scope, 6 | TAbstractFile, 7 | TFile, 8 | Menu, 9 | Modal, 10 | App, 11 | ButtonComponent, 12 | } from "obsidian"; 13 | import { TimeRange, TimeField } from "./types/time"; 14 | import DailyNoteEditorView from "./component/DailyNoteEditorView.svelte"; 15 | export const DAILY_NOTE_VIEW_TYPE = "daily-note-editor-view"; 16 | 17 | export function isEmebeddedLeaf(leaf: WorkspaceLeaf) { 18 | // Work around missing enhance.js API by checking match condition instead of looking up parent 19 | return (leaf as any).containerEl.matches(".dn-leaf-view"); 20 | } 21 | 22 | export class DailyNoteView extends ItemView { 23 | view: DailyNoteEditorView; 24 | plugin: DailyNoteViewPlugin; 25 | scope: Scope; 26 | 27 | selectedDaysRange: TimeRange = "all"; 28 | selectionMode: "daily" | "folder" | "tag" = "daily"; 29 | target: string = ""; 30 | timeField: TimeField = "mtime"; 31 | 32 | customRange: { 33 | start: Date; 34 | end: Date; 35 | } | null = null; 36 | 37 | constructor(leaf: WorkspaceLeaf, plugin: DailyNoteViewPlugin) { 38 | super(leaf); 39 | this.plugin = plugin; 40 | 41 | this.scope = new Scope(plugin.app.scope); 42 | } 43 | 44 | getMode = () => { 45 | return "source"; 46 | }; 47 | 48 | getViewType(): string { 49 | return DAILY_NOTE_VIEW_TYPE; 50 | } 51 | 52 | getDisplayText(): string { 53 | if (this.selectionMode === "daily") { 54 | return "Daily Notes"; 55 | } else if (this.selectionMode === "folder") { 56 | return `Folder: ${this.target}`; 57 | } else if (this.selectionMode === "tag") { 58 | return `Tag: ${this.target}`; 59 | } 60 | return "Notes"; 61 | } 62 | 63 | getIcon(): string { 64 | if (this.selectionMode === "daily") { 65 | return "calendar"; 66 | } else if (this.selectionMode === "folder") { 67 | return "folder"; 68 | } else if (this.selectionMode === "tag") { 69 | return "tag"; 70 | } 71 | return "document"; 72 | } 73 | 74 | onFileCreate = (file: TAbstractFile) => { 75 | if (file instanceof TFile) this.view.fileCreate(file); 76 | }; 77 | 78 | onFileDelete = (file: TAbstractFile) => { 79 | if (file instanceof TFile) this.view.fileDelete(file); 80 | }; 81 | 82 | setSelectedRange(range: TimeRange) { 83 | this.selectedDaysRange = range; 84 | if (this.view) { 85 | if (range === "custom") { 86 | this.view.$set({ 87 | selectedRange: range, 88 | customRange: this.customRange, 89 | }); 90 | } else { 91 | this.view.$set({ selectedRange: range }); 92 | } 93 | } 94 | } 95 | 96 | setSelectionMode(mode: "daily" | "folder" | "tag", target: string = "") { 97 | this.selectionMode = mode; 98 | this.target = target; 99 | 100 | if (this.view) { 101 | this.view.$set({ 102 | selectionMode: mode, 103 | target: target, 104 | }); 105 | } 106 | } 107 | 108 | saveCurrentSelectionAsPreset() { 109 | if (this.selectionMode !== "daily" && this.target) { 110 | // Check if this preset already exists 111 | const existingPresetIndex = this.plugin.settings.preset.findIndex( 112 | (p) => p.type === this.selectionMode && p.target === this.target 113 | ); 114 | 115 | // If it doesn't exist, add it 116 | if (existingPresetIndex === -1) { 117 | this.plugin.settings.preset.push({ 118 | type: this.selectionMode, 119 | target: this.target, 120 | }); 121 | this.plugin.saveSettings(); 122 | } 123 | } 124 | } 125 | 126 | getState(): Record { 127 | const state = super.getState(); 128 | 129 | return { 130 | ...state, 131 | selectionMode: this.selectionMode, 132 | target: this.target, 133 | timeField: this.timeField, 134 | selectedRange: this.selectedDaysRange, 135 | customRange: this.customRange, 136 | }; 137 | } 138 | 139 | async setState(state: unknown, result?: any): Promise { 140 | await super.setState(state, result); 141 | // Handle our custom state properties if they exist 142 | if (state && typeof state === "object" && !this.view) { 143 | const customState = state as { 144 | selectionMode?: "daily" | "folder" | "tag"; 145 | target?: string; 146 | timeField?: TimeField; 147 | selectedRange?: TimeRange; 148 | customRange?: { start: Date; end: Date } | null; 149 | }; 150 | 151 | if (customState.selectionMode) 152 | this.selectionMode = customState.selectionMode; 153 | if (customState.target) this.target = customState.target; 154 | if (customState.timeField) this.timeField = customState.timeField; 155 | if (customState.selectedRange) 156 | this.selectedDaysRange = customState.selectedRange; 157 | if (customState.customRange) 158 | this.customRange = customState.customRange; 159 | 160 | this.view = new DailyNoteEditorView({ 161 | target: this.contentEl, 162 | props: { 163 | plugin: this.plugin, 164 | leaf: this.leaf, 165 | selectedRange: this.selectedDaysRange, 166 | customRange: this.customRange, 167 | selectionMode: this.selectionMode, 168 | target: this.target, 169 | timeField: this.timeField, 170 | }, 171 | }); 172 | 173 | this.app.workspace.onLayoutReady(this.view.tick.bind(this)); 174 | 175 | this.registerInterval( 176 | window.setInterval(async () => { 177 | this.view.check(); 178 | }, 1000 * 60 * 60) 179 | ); 180 | } 181 | } 182 | 183 | setTimeField(field: TimeField) { 184 | this.timeField = field; 185 | if (this.view) { 186 | this.view.$set({ timeField: field }); 187 | } 188 | } 189 | 190 | openDailyNoteEditor() { 191 | this.plugin.openDailyNoteEditor(); 192 | } 193 | 194 | async onOpen(): Promise { 195 | this.scope.register(["Mod"], "f", (e) => { 196 | // do-nothing 197 | }); 198 | 199 | this.addAction("clock", "Select time field", (e) => { 200 | const menu = new Menu(); 201 | 202 | // Add time field selection options 203 | const addTimeFieldOption = (title: string, field: TimeField) => { 204 | menu.addItem((item) => { 205 | item.setTitle(title); 206 | item.setChecked(this.timeField === field); 207 | item.onClick(() => { 208 | this.setTimeField(field); 209 | }); 210 | }); 211 | }; 212 | 213 | addTimeFieldOption("Creation Time", "ctime"); 214 | addTimeFieldOption("Modification Time", "mtime"); 215 | addTimeFieldOption("Creation Time (Reverse)", "ctimeReverse"); 216 | addTimeFieldOption("Modification Time (Reverse)", "mtimeReverse"); 217 | // Add new options for sorting by name 218 | addTimeFieldOption("Name (A-Z)", "name"); 219 | addTimeFieldOption("Name (Z-A)", "nameReverse"); 220 | 221 | menu.showAtMouseEvent(e); 222 | }); 223 | 224 | // Add action for selecting view mode 225 | this.addAction("layers-2", "Select view mode", (e) => { 226 | const menu = new Menu(); 227 | 228 | // Add mode selection options 229 | const addModeOption = ( 230 | title: string, 231 | mode: "daily" | "folder" | "tag" 232 | ) => { 233 | menu.addItem((item) => { 234 | item.setTitle(title); 235 | item.setChecked( 236 | this.selectionMode === mode && !this.target 237 | ); 238 | item.onClick(() => { 239 | if (mode === "daily") { 240 | this.setSelectionMode(mode); 241 | } else { 242 | // For folder and tag modes, we need to prompt for the target 243 | const modal = new SelectTargetModal( 244 | this.plugin.app, 245 | mode, 246 | (target: string) => { 247 | this.setSelectionMode(mode, target); 248 | // Save this selection as a preset 249 | this.saveCurrentSelectionAsPreset(); 250 | } 251 | ); 252 | modal.open(); 253 | } 254 | }); 255 | }); 256 | }; 257 | 258 | addModeOption("Daily Notes", "daily"); 259 | addModeOption("Folder", "folder"); 260 | addModeOption("Tag", "tag"); 261 | 262 | // Add presets if they exist 263 | if (this.plugin.settings.preset.length > 0) { 264 | menu.addSeparator(); 265 | menu.addItem((item) => { 266 | item.setTitle("Saved Presets"); 267 | item.setDisabled(true); 268 | }); 269 | 270 | // Add each preset 271 | for (const preset of this.plugin.settings.preset) { 272 | const title = 273 | preset.type === "folder" 274 | ? `Folder: ${preset.target}` 275 | : `Tag: ${preset.target}`; 276 | 277 | menu.addItem((item) => { 278 | item.setTitle(title); 279 | item.setChecked( 280 | this.selectionMode === preset.type && 281 | this.target === preset.target 282 | ); 283 | item.onClick(() => { 284 | this.setSelectionMode(preset.type, preset.target); 285 | }); 286 | }); 287 | } 288 | } 289 | 290 | menu.showAtMouseEvent(e); 291 | }); 292 | 293 | // Add "Save as Preset" button when in folder or tag mode 294 | // this.addAction("bookmark", "Save as preset", (e) => { 295 | // // Only enable for folder and tag modes with a target 296 | // if (this.selectionMode !== "daily" && this.target) { 297 | // this.saveCurrentSelectionAsPreset(); 298 | // // Show a small notification 299 | // new Notice("Preset saved"); 300 | // } 301 | // }); 302 | 303 | // Add action for selecting time field (for folder and tag modes) 304 | 305 | this.addAction("calendar-range", "Select date range", (e) => { 306 | const menu = new Menu(); 307 | // Add range selection options 308 | const addRangeOption = (title: string, range: TimeRange) => { 309 | menu.addItem((item) => { 310 | item.setTitle(title); 311 | item.setChecked(this.selectedDaysRange === range); 312 | item.onClick(() => { 313 | this.setSelectedRange(range); 314 | }); 315 | }); 316 | }; 317 | 318 | addRangeOption("All Notes", "all"); 319 | addRangeOption("This Week", "week"); 320 | addRangeOption("This Month", "month"); 321 | addRangeOption("This Year", "year"); 322 | addRangeOption("Last Week", "last-week"); 323 | addRangeOption("Last Month", "last-month"); 324 | addRangeOption("Last Year", "last-year"); 325 | addRangeOption("This Quarter", "quarter"); 326 | addRangeOption("Last Quarter", "last-quarter"); 327 | 328 | menu.addSeparator(); 329 | menu.addItem((item) => { 330 | item.setTitle("Custom Date Range"); 331 | item.setChecked(this.selectedDaysRange === "custom"); 332 | item.onClick(() => { 333 | const modal = new CustomRangeModal(this.app, (range) => { 334 | this.customRange = range; 335 | this.setSelectedRange("custom"); 336 | }); 337 | modal.open(); 338 | }); 339 | }); 340 | 341 | menu.showAtMouseEvent(e as MouseEvent); 342 | }); 343 | 344 | this.addAction("refresh", "Refresh", () => { 345 | if (this.view) { 346 | // Tell the Svelte component to check for daily notes 347 | this.view.check(); 348 | 349 | // Update the view to get the latest files 350 | this.view.tick(); 351 | 352 | // Force a refresh of the file list 353 | this.view.$set({ 354 | selectedRange: this.selectedDaysRange, 355 | customRange: this.customRange, 356 | }); 357 | } 358 | }); 359 | 360 | this.app.vault.on("create", this.onFileCreate); 361 | this.app.vault.on("delete", this.onFileDelete); 362 | } 363 | 364 | onPaneMenu( 365 | menu: Menu, 366 | source: "more-options" | "tab-header" | string 367 | ): void { 368 | if (source === "tab-header" || source === "more-options") { 369 | menu.addItem((item) => { 370 | // @ts-ignore 371 | item.setIcon(this.leaf.pinned ? "pin-off" : "pin"); 372 | // @ts-ignore 373 | item.setTitle(this.leaf.pinned ? "Unpin" : "Pin"); 374 | item.onClick(() => { 375 | this.leaf.togglePinned(); 376 | }); 377 | }); 378 | } 379 | } 380 | 381 | /** 382 | * Refresh the view for a new day 383 | * This is called when the date changes (e.g., after midnight) 384 | */ 385 | public refreshForNewDay(): void { 386 | // If we're in daily note mode, we need to refresh the view 387 | // to show the current day's note 388 | if (this.selectionMode === "daily") { 389 | // Reset the view properties to trigger a reload 390 | if (this.view) { 391 | // Tell the Svelte component to check for daily notes 392 | this.view.check(); 393 | 394 | // Update the view to get the latest files 395 | this.view.tick(); 396 | 397 | // Force a refresh of the file list 398 | this.view.$set({ 399 | selectedRange: this.selectedDaysRange, 400 | customRange: this.customRange, 401 | }); 402 | } 403 | } 404 | } 405 | } 406 | 407 | class CustomRangeModal extends Modal { 408 | saveCallback: (range: { start: Date; end: Date }) => void; 409 | startDate: Date; 410 | endDate: Date; 411 | 412 | constructor( 413 | app: App, 414 | saveCallback: (range: { start: Date; end: Date }) => void 415 | ) { 416 | super(app); 417 | this.saveCallback = saveCallback; 418 | this.startDate = new Date(); 419 | this.endDate = new Date(); 420 | } 421 | 422 | onOpen() { 423 | const { contentEl } = this; 424 | 425 | contentEl.createEl("h2", { text: "Select Custom Date Range" }); 426 | 427 | const startDateContainer = contentEl.createEl("div", { 428 | cls: "custom-range-date-container", 429 | }); 430 | startDateContainer.createEl("span", { text: "Start Date: " }); 431 | const startDatePicker = startDateContainer.createEl("input", { 432 | type: "date", 433 | value: this.formatDate(this.startDate), 434 | }); 435 | startDatePicker.addEventListener("change", (e) => { 436 | this.startDate = new Date((e.target as HTMLInputElement).value); 437 | }); 438 | 439 | const endDateContainer = contentEl.createEl("div", { 440 | cls: "custom-range-date-container", 441 | }); 442 | endDateContainer.createEl("span", { text: "End Date: " }); 443 | const endDatePicker = endDateContainer.createEl("input", { 444 | type: "date", 445 | value: this.formatDate(this.endDate), 446 | }); 447 | endDatePicker.addEventListener("change", (e) => { 448 | this.endDate = new Date((e.target as HTMLInputElement).value); 449 | }); 450 | 451 | const buttonContainer = contentEl.createEl("div", { 452 | cls: "custom-range-button-container", 453 | }); 454 | 455 | new ButtonComponent(buttonContainer) 456 | .setButtonText("Cancel") 457 | .onClick(() => { 458 | this.close(); 459 | }); 460 | 461 | new ButtonComponent(buttonContainer) 462 | .setButtonText("Confirm") 463 | .setCta() 464 | .onClick(() => { 465 | this.saveCallback({ 466 | start: this.startDate, 467 | end: this.endDate, 468 | }); 469 | this.close(); 470 | }); 471 | } 472 | 473 | formatDate(date: Date): string { 474 | const year = date.getFullYear(); 475 | const month = String(date.getMonth() + 1).padStart(2, "0"); 476 | const day = String(date.getDate()).padStart(2, "0"); 477 | return `${year}-${month}-${day}`; 478 | } 479 | 480 | onClose() { 481 | this.contentEl.empty(); 482 | } 483 | } 484 | 485 | class SelectTargetModal extends Modal { 486 | saveCallback: (target: string) => void; 487 | mode: "folder" | "tag"; 488 | targetInput: HTMLInputElement; 489 | 490 | constructor( 491 | app: App, 492 | mode: "folder" | "tag", 493 | saveCallback: (target: string) => void 494 | ) { 495 | super(app); 496 | this.mode = mode; 497 | this.saveCallback = saveCallback; 498 | } 499 | 500 | onOpen() { 501 | const { contentEl } = this; 502 | contentEl.empty(); 503 | 504 | contentEl.createEl("h2", { 505 | text: this.mode === "folder" ? "Select Folder" : "Select Tag", 506 | }); 507 | 508 | const form = contentEl.createEl("form"); 509 | form.addEventListener("submit", (e) => { 510 | e.preventDefault(); 511 | this.save(); 512 | }); 513 | 514 | const targetSetting = form.createDiv(); 515 | targetSetting.addClass("setting-item"); 516 | 517 | const targetSettingInfo = targetSetting.createDiv(); 518 | targetSettingInfo.addClass("setting-item-info"); 519 | 520 | targetSettingInfo.createEl("div", { 521 | text: this.mode === "folder" ? "Folder Path" : "Tag Name", 522 | cls: "setting-item-name", 523 | }); 524 | 525 | targetSettingInfo.createEl("div", { 526 | text: 527 | this.mode === "folder" 528 | ? "Enter the path to the folder (e.g., 'folder/subfolder')" 529 | : "Enter the tag name without the '#' (e.g., 'tag')", 530 | cls: "setting-item-description", 531 | }); 532 | 533 | const targetSettingControl = targetSetting.createDiv(); 534 | targetSettingControl.addClass("setting-item-control"); 535 | 536 | this.targetInput = targetSettingControl.createEl("input", { 537 | type: "text", 538 | value: "", 539 | }); 540 | this.targetInput.addClass("target-input"); 541 | 542 | const footerEl = contentEl.createDiv(); 543 | footerEl.addClass("modal-button-container"); 544 | 545 | footerEl 546 | .createEl("button", { 547 | text: "Cancel", 548 | cls: "mod-warning", 549 | attr: { 550 | type: "button", 551 | }, 552 | }) 553 | .addEventListener("click", () => { 554 | this.close(); 555 | }); 556 | 557 | footerEl 558 | .createEl("button", { 559 | text: "Save", 560 | cls: "mod-cta", 561 | attr: { 562 | type: "submit", 563 | }, 564 | }) 565 | .addEventListener("click", (e) => { 566 | e.preventDefault(); 567 | this.save(); 568 | }); 569 | } 570 | 571 | save() { 572 | const target = this.targetInput.value.trim(); 573 | if (target) { 574 | this.saveCallback(target); 575 | this.close(); 576 | } 577 | } 578 | 579 | onClose() { 580 | const { contentEl } = this; 581 | contentEl.empty(); 582 | } 583 | } 584 | -------------------------------------------------------------------------------- /src/dailyNoteViewIndex.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Plugin, 3 | OpenViewState, 4 | TFile, 5 | Workspace, 6 | WorkspaceContainer, 7 | WorkspaceItem, 8 | WorkspaceLeaf, 9 | moment, 10 | requireApiVersion, 11 | TFolder, 12 | } from "obsidian"; 13 | 14 | import { around } from "monkey-around"; 15 | import { DailyNoteEditor, isDailyNoteLeaf } from "./leafView"; 16 | import "./style/index.css"; 17 | import { addIconList } from "./utils/icon"; 18 | import { 19 | DailyNoteSettings, 20 | DailyNoteSettingTab, 21 | DEFAULT_SETTINGS, 22 | } from "./dailyNoteSettings"; 23 | import { TimeField } from "./types/time"; 24 | import { 25 | getAllDailyNotes, 26 | getDailyNote, 27 | createDailyNote, 28 | } from "obsidian-daily-notes-interface"; 29 | import { createUpDownNavigationExtension } from "./component/UpAndDownNavigate"; 30 | // import { setActiveEditorExt } from "./component/SetActiveEditor"; 31 | import { DAILY_NOTE_VIEW_TYPE, DailyNoteView } from "./dailyNoteView"; 32 | 33 | export default class DailyNoteViewPlugin extends Plugin { 34 | private view: DailyNoteView; 35 | lastActiveFile: TFile; 36 | private lastCheckedDay: string; 37 | 38 | settings: DailyNoteSettings; 39 | 40 | async onload() { 41 | this.addSettingTab(new DailyNoteSettingTab(this.app, this)); 42 | await this.loadSettings(); 43 | this.patchWorkspace(); 44 | this.patchWorkspaceLeaf(); 45 | addIconList(); 46 | 47 | this.lastCheckedDay = moment().format("YYYY-MM-DD"); 48 | 49 | // Register the up and down navigation extension 50 | this.settings.useArrowUpOrDownToNavigate && 51 | this.registerEditorExtension([ 52 | createUpDownNavigationExtension({ 53 | app: this.app, 54 | plugin: this, 55 | }), 56 | // setActiveEditorExt({ app: this.app, plugin: this }), 57 | ]); 58 | 59 | this.registerView( 60 | DAILY_NOTE_VIEW_TYPE, 61 | (leaf: WorkspaceLeaf) => (this.view = new DailyNoteView(leaf, this)) 62 | ); 63 | 64 | this.addRibbonIcon( 65 | "calendar-range", 66 | "Open Daily Note Editor", 67 | (evt: MouseEvent) => this.openDailyNoteEditor() 68 | ); 69 | this.addCommand({ 70 | id: "open-daily-note-editor", 71 | name: "Open Daily Note Editor", 72 | callback: () => this.openDailyNoteEditor(), 73 | }); 74 | 75 | this.initCssRules(); 76 | 77 | // Create daily note and open the Daily Notes Editor on startup if enabled 78 | if (this.settings.createAndOpenOnStartup) { 79 | this.app.workspace.onLayoutReady(async () => { 80 | // First ensure today's daily note exists 81 | await this.ensureTodaysDailyNoteExists(); 82 | if ( 83 | this.app.workspace.getLeavesOfType(DAILY_NOTE_VIEW_TYPE) 84 | .length > 0 85 | ) 86 | return; 87 | // Then open the Daily Notes Editor 88 | await this.openDailyNoteEditor(); 89 | }); 90 | } 91 | 92 | // Also check periodically (every 15 minutes) for day changes 93 | this.registerInterval( 94 | window.setInterval(this.checkDayChange.bind(this), 1000 * 60 * 15) 95 | ); 96 | 97 | this.app.workspace.on("file-menu", (menu, file, source, leaf) => { 98 | if (file instanceof TFolder) { 99 | menu.addItem((item) => { 100 | item.setIcon("calendar-range"); 101 | item.setTitle("Open daily notes for this folder"); 102 | item.onClick(() => { 103 | this.openFolderView(file.path); 104 | }); 105 | }); 106 | } 107 | }); 108 | } 109 | 110 | onunload() { 111 | this.app.workspace.detachLeavesOfType(DAILY_NOTE_VIEW_TYPE); 112 | document.body.toggleClass("daily-notes-hide-frontmatter", false); 113 | document.body.toggleClass("daily-notes-hide-backlinks", false); 114 | } 115 | 116 | async openDailyNoteEditor() { 117 | const workspace = this.app.workspace; 118 | const leaf = workspace.getLeaf(true); 119 | await leaf.setViewState({ type: DAILY_NOTE_VIEW_TYPE }); 120 | workspace.revealLeaf(leaf); 121 | } 122 | 123 | async openFolderView(folderPath: string, timeField: TimeField = "mtime") { 124 | const workspace = this.app.workspace; 125 | const leaf = workspace.getLeaf(true); 126 | await leaf.setViewState({ type: DAILY_NOTE_VIEW_TYPE }); 127 | 128 | // Get the view and set the selection mode to folder 129 | const view = leaf.view as DailyNoteView; 130 | view.setSelectionMode("folder", folderPath); 131 | view.setTimeField(timeField); 132 | 133 | workspace.revealLeaf(leaf); 134 | } 135 | 136 | async openTagView(tagName: string, timeField: TimeField = "mtime") { 137 | const workspace = this.app.workspace; 138 | const leaf = workspace.getLeaf(true); 139 | await leaf.setViewState({ type: DAILY_NOTE_VIEW_TYPE }); 140 | 141 | // Get the view and set the selection mode to tag 142 | const view = leaf.view as DailyNoteView; 143 | view.setSelectionMode("tag", tagName); 144 | view.setTimeField(timeField); 145 | 146 | workspace.revealLeaf(leaf); 147 | } 148 | 149 | async ensureTodaysDailyNoteExists() { 150 | try { 151 | const currentDate = moment(); 152 | const allDailyNotes = getAllDailyNotes(); 153 | const currentDailyNote = getDailyNote(currentDate, allDailyNotes); 154 | 155 | if (!currentDailyNote) { 156 | await createDailyNote(currentDate); 157 | } 158 | } catch (error) { 159 | console.error("Failed to create daily note:", error); 160 | } 161 | } 162 | 163 | initCssRules() { 164 | document.body.toggleClass( 165 | "daily-notes-hide-frontmatter", 166 | this.settings.hideFrontmatter 167 | ); 168 | document.body.toggleClass( 169 | "daily-notes-hide-backlinks", 170 | this.settings.hideBacklinks 171 | ); 172 | } 173 | 174 | patchWorkspace() { 175 | let layoutChanging = false; 176 | const uninstaller = around(Workspace.prototype, { 177 | getActiveViewOfType: (next: any) => 178 | function (t: any) { 179 | const result = next.call(this, t); 180 | if (!result) { 181 | if (t?.VIEW_TYPE === "markdown") { 182 | const activeLeaf = this.activeLeaf; 183 | if (activeLeaf?.view instanceof DailyNoteView) { 184 | return activeLeaf.view.editMode; 185 | } else { 186 | return result; 187 | } 188 | } 189 | } 190 | return result; 191 | }, 192 | changeLayout(old) { 193 | return async function (workspace: unknown) { 194 | layoutChanging = true; 195 | try { 196 | // Don't consider hover popovers part of the workspace while it's changing 197 | await old.call(this, workspace); 198 | } finally { 199 | layoutChanging = false; 200 | } 201 | }; 202 | }, 203 | iterateLeaves(old) { 204 | type leafIterator = (item: WorkspaceLeaf) => boolean | void; 205 | return function (arg1, arg2) { 206 | // Fast exit if desired leaf found 207 | if (old.call(this, arg1, arg2)) return true; 208 | 209 | // Handle old/new API parameter swap 210 | const cb: leafIterator = ( 211 | typeof arg1 === "function" ? arg1 : arg2 212 | ) as leafIterator; 213 | const parent: WorkspaceItem = ( 214 | typeof arg1 === "function" ? arg2 : arg1 215 | ) as WorkspaceItem; 216 | 217 | if (!parent) return false; // <- during app startup, rootSplit can be null 218 | if (layoutChanging) return false; // Don't let HEs close during workspace change 219 | 220 | // 0.14.x doesn't have WorkspaceContainer; this can just be an instanceof check once 15.x is mandatory: 221 | if (!requireApiVersion("0.15.0")) { 222 | if ( 223 | parent === this.app.workspace.rootSplit || 224 | (WorkspaceContainer && 225 | parent instanceof WorkspaceContainer) 226 | ) { 227 | for (const popover of DailyNoteEditor.popoversForWindow( 228 | (parent as WorkspaceContainer).win 229 | )) { 230 | // Use old API here for compat w/0.14.x 231 | if (old.call(this, cb, popover.rootSplit)) 232 | return true; 233 | } 234 | } 235 | } 236 | return false; 237 | }; 238 | }, 239 | setActiveLeaf: (next: any) => 240 | function (e: WorkspaceLeaf, t?: any) { 241 | if ((e as any).parentLeaf) { 242 | (e as any).parentLeaf.activeTime = 1700000000000; 243 | 244 | next.call(this, (e as any).parentLeaf, t); 245 | if ((e.view as any).editMode) { 246 | this.activeEditor = e.view; 247 | (e as any).parentLeaf.view.editMode = e.view; 248 | } 249 | return; 250 | } 251 | return next.call(this, e, t); 252 | }, 253 | }); 254 | this.register(uninstaller); 255 | } 256 | 257 | // Used for patch workspaceleaf pinned behaviors 258 | patchWorkspaceLeaf() { 259 | this.register( 260 | around(WorkspaceLeaf.prototype, { 261 | getRoot(old) { 262 | return function () { 263 | const top = old.call(this); 264 | return top?.getRoot === this.getRoot 265 | ? top 266 | : top?.getRoot(); 267 | }; 268 | }, 269 | setPinned(old) { 270 | return function (pinned: boolean) { 271 | old.call(this, pinned); 272 | if (isDailyNoteLeaf(this) && !pinned) 273 | this.setPinned(true); 274 | }; 275 | }, 276 | openFile(old) { 277 | return function (file: TFile, openState?: OpenViewState) { 278 | if (isDailyNoteLeaf(this)) { 279 | setTimeout( 280 | around(Workspace.prototype, { 281 | recordMostRecentOpenedFile(old) { 282 | return function (_file: TFile) { 283 | // Don't update the quick switcher's recent list 284 | if (_file !== file) { 285 | return old.call(this, _file); 286 | } 287 | }; 288 | }, 289 | }), 290 | 1 291 | ); 292 | const recentFiles = 293 | this.app.plugins.plugins[ 294 | "recent-files-obsidian" 295 | ]; 296 | if (recentFiles) 297 | setTimeout( 298 | around(recentFiles, { 299 | shouldAddFile(old) { 300 | return function (_file: TFile) { 301 | // Don't update the Recent Files plugin 302 | return ( 303 | _file !== file && 304 | old.call(this, _file) 305 | ); 306 | }; 307 | }, 308 | }), 309 | 1 310 | ); 311 | } 312 | return old.call(this, file, openState); 313 | }; 314 | }, 315 | }) 316 | ); 317 | } 318 | 319 | public async loadSettings() { 320 | this.settings = Object.assign( 321 | {}, 322 | DEFAULT_SETTINGS, 323 | await this.loadData() 324 | ); 325 | } 326 | 327 | async saveSettings() { 328 | await this.saveData(this.settings); 329 | } 330 | 331 | private async checkDayChange(): Promise { 332 | const currentDay = moment().format("YYYY-MM-DD"); 333 | 334 | if (currentDay !== this.lastCheckedDay) { 335 | this.lastCheckedDay = currentDay; 336 | console.log("Day changed, updating daily notes view"); 337 | 338 | await this.ensureTodaysDailyNoteExists(); 339 | 340 | const dailyNoteLeaves = 341 | this.app.workspace.getLeavesOfType(DAILY_NOTE_VIEW_TYPE); 342 | if (dailyNoteLeaves.length > 0) { 343 | for (const leaf of dailyNoteLeaves) { 344 | const view = leaf.view as DailyNoteView; 345 | if (view) { 346 | view.refreshForNewDay(); 347 | } 348 | } 349 | } 350 | } 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /src/leafView.ts: -------------------------------------------------------------------------------- 1 | // Original code from https://github.com/nothingislost/obsidian-hover-editor/blob/9ec3449be9ab3433dc46c4c3acfde1da72ff0261/src/popover.ts 2 | // You can use this file as a basic leaf view create method in anywhere 3 | // Please rememeber if you want to use this file, you should patch the obsidian.d.ts file 4 | // And also monkey around the Obsidian original method. 5 | import { 6 | Component, 7 | EphemeralState, 8 | HoverPopover, 9 | MarkdownEditView, 10 | OpenViewState, 11 | parseLinktext, 12 | PopoverState, 13 | requireApiVersion, 14 | resolveSubpath, 15 | TFile, 16 | View, 17 | Workspace, 18 | WorkspaceLeaf, 19 | WorkspaceSplit, 20 | WorkspaceTabs, 21 | } from "obsidian"; 22 | 23 | import type DailyNoteViewPlugin from "./dailyNoteViewIndex"; 24 | import { genId } from "./utils/utils"; 25 | 26 | 27 | export interface DailyNoteEditorParent { 28 | hoverPopover: DailyNoteEditor | null; 29 | containerEl?: HTMLElement; 30 | view?: View; 31 | dom?: HTMLElement; 32 | } 33 | 34 | const popovers = new WeakMap(); 35 | type ConstructableWorkspaceSplit = new (ws: Workspace, dir: "horizontal" | "vertical") => WorkspaceSplit; 36 | 37 | export function isDailyNoteLeaf(leaf: WorkspaceLeaf) { 38 | // Work around missing enhance.js API by checking match condition instead of looking up parent 39 | return leaf.containerEl.matches(".dn-editor.dn-leaf-view .workspace-leaf"); 40 | } 41 | 42 | function nosuper(base: new (...args: unknown[]) => T): new () => T { 43 | const derived = function () { 44 | return Object.setPrototypeOf(new Component, new.target.prototype); 45 | }; 46 | derived.prototype = base.prototype; 47 | return Object.setPrototypeOf(derived, base); 48 | } 49 | 50 | export const spawnLeafView = (plugin: DailyNoteViewPlugin, initiatingEl?: HTMLElement, leaf?: WorkspaceLeaf, onShowCallback?: () => unknown): [WorkspaceLeaf, DailyNoteEditor] => { 51 | // When Obsidian doesn't set any leaf active, use leaf instead. 52 | let parent = plugin.app.workspace.activeLeaf as unknown as DailyNoteEditorParent; 53 | if (!parent) parent = leaf as unknown as DailyNoteEditorParent; 54 | 55 | if (!initiatingEl) initiatingEl = parent?.containerEl; 56 | 57 | const hoverPopover = new DailyNoteEditor(parent, initiatingEl!, plugin, undefined, onShowCallback); 58 | return [hoverPopover.attachLeaf(), hoverPopover]; 59 | 60 | }; 61 | 62 | export class DailyNoteEditor extends nosuper(HoverPopover) { 63 | onTarget: boolean; 64 | setActive: (event: MouseEvent) => void; 65 | 66 | lockedOut: boolean; 67 | abortController? = this.addChild(new Component()); 68 | detaching = false; 69 | opening = false; 70 | 71 | // @ts-ignore 72 | rootSplit: WorkspaceSplit = new (WorkspaceSplit as ConstructableWorkspaceSplit)(window.app.workspace, "vertical"); 73 | isPinned = true; 74 | 75 | titleEl: HTMLElement; 76 | containerEl: HTMLElement; 77 | 78 | // It is currently not useful. 79 | // leafInHoverEl: WorkspaceLeaf; 80 | 81 | oldPopover = this.parent?.DailyNoteEditor; 82 | document: Document; 83 | 84 | id = genId(8); 85 | bounce?: NodeJS.Timeout; 86 | boundOnZoomOut: () => void; 87 | 88 | originalPath: string; // these are kept to avoid adopting targets w/a different link 89 | originalLinkText: string; 90 | static activePopover?: DailyNoteEditor; 91 | 92 | static activeWindows() { 93 | const windows: Window[] = [window]; 94 | // @ts-ignore 95 | const {floatingSplit} = app.workspace; 96 | if (floatingSplit) { 97 | for (const split of floatingSplit.children) { 98 | if (split.win) windows.push(split.win); 99 | } 100 | } 101 | return windows; 102 | } 103 | 104 | static containerForDocument(plugin: DailyNoteViewPlugin, doc: Document) { 105 | if (doc !== document && plugin.app.workspace.floatingSplit) 106 | for (const container of plugin.app.workspace.floatingSplit.children) { 107 | if (container.doc === doc) return container; 108 | } 109 | return plugin.app.workspace.rootSplit; 110 | } 111 | 112 | static activePopovers() { 113 | return this.activeWindows().flatMap(this.popoversForWindow); 114 | } 115 | 116 | static popoversForWindow(win?: Window) { 117 | return (Array.prototype.slice.call(win?.document?.body.querySelectorAll(".dn-leaf-view") ?? []) as HTMLElement[]) 118 | .map(el => popovers.get(el)!) 119 | .filter(he => he); 120 | } 121 | 122 | static forLeaf(leaf: WorkspaceLeaf | undefined) { 123 | // leaf can be null such as when right clicking on an internal link 124 | const el = leaf && document.body.matchParent.call(leaf.containerEl, ".dn-leaf-view"); // work around matchParent race condition 125 | return el ? popovers.get(el) : undefined; 126 | } 127 | 128 | static iteratePopoverLeaves(ws: Workspace, cb: (leaf: WorkspaceLeaf) => boolean | void) { 129 | for (const popover of this.activePopovers()) { 130 | if (popover.rootSplit && ws.iterateLeaves(cb, popover.rootSplit)) return true; 131 | } 132 | return false; 133 | } 134 | 135 | hoverEl: HTMLElement; 136 | 137 | constructor( 138 | parent: DailyNoteEditorParent, 139 | public targetEl: HTMLElement, 140 | public plugin: DailyNoteViewPlugin, 141 | waitTime?: number, 142 | public onShowCallback?: () => unknown, 143 | ) { 144 | // 145 | super(); 146 | 147 | if (waitTime === undefined) { 148 | waitTime = 300; 149 | } 150 | this.onTarget = true; 151 | 152 | this.parent = parent; 153 | this.waitTime = waitTime; 154 | this.state = PopoverState.Showing; 155 | 156 | this.document = this.targetEl?.ownerDocument ?? window.activeDocument ?? window.document; 157 | this.hoverEl = this.document.defaultView!.createDiv({ 158 | cls: "dn-editor dn-leaf-view", 159 | attr: {id: "dn-" + this.id}, 160 | }); 161 | const {hoverEl} = this; 162 | 163 | this.abortController!.load(); 164 | this.timer = window.setTimeout(this.show.bind(this), waitTime); 165 | this.setActive = this._setActive.bind(this); 166 | if (hoverEl) { 167 | hoverEl.addEventListener("mousedown", this.setActive); 168 | } 169 | // custom logic begin 170 | popovers.set(this.hoverEl, this); 171 | this.hoverEl.addClass("dn-editor"); 172 | this.containerEl = this.hoverEl.createDiv("dn-content"); 173 | this.buildWindowControls(); 174 | this.setInitialDimensions(); 175 | 176 | } 177 | 178 | _setActive(evt: MouseEvent) { 179 | evt.preventDefault(); 180 | evt.stopPropagation(); 181 | this.plugin.app.workspace.setActiveLeaf(this.leaves()[0], {focus: true}); 182 | } 183 | 184 | getDefaultMode() { 185 | // return this.parent?.view?.getMode ? this.parent.view.getMode() : "source"; 186 | return "source"; 187 | } 188 | 189 | updateLeaves() { 190 | if (this.onTarget && this.targetEl && !this.document.contains(this.targetEl)) { 191 | this.onTarget = false; 192 | this.transition(); 193 | } 194 | let leafCount = 0; 195 | this.plugin.app.workspace.iterateLeaves(leaf => { 196 | leafCount++; 197 | }, this.rootSplit); 198 | 199 | if (leafCount === 0) { 200 | this.hide(); // close if we have no leaves 201 | } 202 | this.hoverEl.setAttribute("data-leaf-count", leafCount.toString()); 203 | } 204 | 205 | leaves() { 206 | const leaves: WorkspaceLeaf[] = []; 207 | this.plugin.app.workspace.iterateLeaves(leaf => { 208 | leaves.push(leaf); 209 | }, this.rootSplit); 210 | return leaves; 211 | } 212 | 213 | setInitialDimensions() { 214 | 215 | this.hoverEl.style.height = 'auto'; 216 | this.hoverEl.style.width = "100%"; 217 | } 218 | 219 | transition() { 220 | if (this.shouldShow()) { 221 | if (this.state === PopoverState.Hiding) { 222 | this.state = PopoverState.Shown; 223 | window.clearTimeout(this.timer); 224 | } 225 | } else { 226 | if (this.state === PopoverState.Showing) { 227 | this.hide(); 228 | } else { 229 | if (this.state === PopoverState.Shown) { 230 | this.state = PopoverState.Hiding; 231 | this.timer = window.setTimeout(() => { 232 | if (this.shouldShow()) { 233 | this.transition(); 234 | } else { 235 | this.hide(); 236 | } 237 | }, this.waitTime); 238 | } 239 | } 240 | } 241 | } 242 | 243 | 244 | buildWindowControls() { 245 | this.titleEl = this.document.defaultView!.createDiv("popover-titlebar"); 246 | this.titleEl.createDiv("popover-title"); 247 | 248 | this.containerEl.prepend(this.titleEl); 249 | 250 | } 251 | 252 | attachLeaf(): WorkspaceLeaf { 253 | this.rootSplit.getRoot = () => this.plugin.app.workspace[this.document === document ? "rootSplit" : "floatingSplit"]!; 254 | this.rootSplit.getContainer = () => DailyNoteEditor.containerForDocument(this.plugin, this.document); 255 | 256 | this.titleEl.insertAdjacentElement("afterend", this.rootSplit.containerEl); 257 | const leaf = this.plugin.app.workspace.createLeafInParent(this.rootSplit, 0); 258 | 259 | this.updateLeaves(); 260 | return leaf; 261 | } 262 | 263 | onload(): void { 264 | super.onload(); 265 | this.registerEvent(this.plugin.app.workspace.on("layout-change", this.updateLeaves, this)); 266 | this.registerEvent(this.plugin.app.workspace.on("layout-change", () => { 267 | // Ensure that top-level items in a popover are not tabbed 268 | // @ts-ignore 269 | this.rootSplit.children.forEach((item: any, index: any) => { 270 | if (item instanceof WorkspaceTabs) { 271 | this.rootSplit.replaceChild(index, (item as any).children[0]); 272 | } 273 | }); 274 | })); 275 | } 276 | 277 | onShow() { 278 | // Once we've been open for closeDelay, use the closeDelay as a hiding timeout 279 | const closeDelay = 600; 280 | setTimeout(() => (this.waitTime = closeDelay), closeDelay); 281 | 282 | this.oldPopover?.hide(); 283 | this.oldPopover = null; 284 | 285 | this.hoverEl.toggleClass("is-new", true); 286 | 287 | this.document.body.addEventListener( 288 | "click", 289 | () => { 290 | this.hoverEl.toggleClass("is-new", false); 291 | }, 292 | {once: true, capture: true}, 293 | ); 294 | 295 | if (this.parent) { 296 | this.parent.DailyNoteEditor = this; 297 | } 298 | 299 | // Remove original view header; 300 | const viewHeaderEl = this.hoverEl.querySelector(".view-header"); 301 | viewHeaderEl?.remove(); 302 | 303 | const sizer = this.hoverEl.querySelector(".workspace-leaf"); 304 | if (sizer) this.hoverEl.appendChild(sizer); 305 | 306 | // Remove original inline tilte; 307 | const inlineTitle = this.hoverEl.querySelector(".inline-title"); 308 | if (inlineTitle) inlineTitle.remove(); 309 | 310 | this.onShowCallback?.(); 311 | this.onShowCallback = undefined; // only call it once 312 | } 313 | 314 | detect(el: HTMLElement) { 315 | // TODO: may not be needed? the mouseover/out handers handle most detection use cases 316 | const {targetEl} = this; 317 | 318 | if (targetEl) { 319 | this.onTarget = el === targetEl || targetEl.contains(el); 320 | } 321 | } 322 | 323 | shouldShow() { 324 | return this.shouldShowSelf() || this.shouldShowChild(); 325 | } 326 | 327 | shouldShowChild(): boolean { 328 | return DailyNoteEditor.activePopovers().some(popover => { 329 | if (popover !== this && popover.targetEl && this.hoverEl.contains(popover.targetEl)) { 330 | return popover.shouldShow(); 331 | } 332 | return false; 333 | }); 334 | } 335 | 336 | shouldShowSelf() { 337 | // Don't let obsidian show() us if we've already started closing 338 | // return !this.detaching && (this.onTarget || this.onHover); 339 | return ( 340 | !this.detaching && 341 | !!( 342 | this.onTarget || 343 | (this.state == PopoverState.Shown) || 344 | this.document.querySelector(`body>.modal-container, body > #he${this.id} ~ .menu, body > #he${this.id} ~ .suggestion-container`) 345 | ) 346 | ); 347 | } 348 | 349 | show() { 350 | // native obsidian logic start 351 | if (!this.targetEl || this.document.body.contains(this.targetEl)) { 352 | this.state = PopoverState.Shown; 353 | this.timer = 0; 354 | this.targetEl.appendChild(this.hoverEl); 355 | this.onShow(); 356 | this.plugin.app.workspace.onLayoutChange(); 357 | // initializingHoverPopovers.remove(this); 358 | // activeHoverPopovers.push(this); 359 | // initializePopoverChecker(); 360 | this.load(); 361 | } else { 362 | this.hide(); 363 | } 364 | // native obsidian logic end 365 | 366 | // if this is an image view, set the dimensions to the natural dimensions of the image 367 | // an interactjs reflow will be triggered to constrain the image to the viewport if it's 368 | // too large 369 | if (this.hoverEl.dataset.imgHeight && this.hoverEl.dataset.imgWidth) { 370 | this.hoverEl.style.height = parseFloat(this.hoverEl.dataset.imgHeight) + this.titleEl.offsetHeight + "px"; 371 | this.hoverEl.style.width = parseFloat(this.hoverEl.dataset.imgWidth) + "px"; 372 | } 373 | } 374 | 375 | onHide() { 376 | this.oldPopover = null; 377 | if (this.parent?.DailyNoteEditor === this) { 378 | this.parent.DailyNoteEditor = null; 379 | } 380 | } 381 | 382 | hide() { 383 | this.onTarget = false; 384 | this.detaching = true; 385 | // Once we reach this point, we're committed to closing 386 | 387 | // in case we didn't ever call show() 388 | 389 | 390 | // A timer might be pending to call show() for the first time, make sure 391 | // it doesn't bring us back up after we close 392 | if (this.timer) { 393 | window.clearTimeout(this.timer); 394 | this.timer = 0; 395 | } 396 | 397 | // Hide our HTML element immediately, even if our leaves might not be 398 | // detachable yet. This makes things more responsive and improves the 399 | // odds of not showing an empty popup that's just going to disappear 400 | // momentarily. 401 | this.hoverEl.hide(); 402 | 403 | // If a file load is in progress, we need to wait until it's finished before 404 | // detaching leaves. Because we set .detaching, The in-progress openFile() 405 | // will call us again when it finishes. 406 | if (this.opening) return; 407 | 408 | // Leave this code here to observe the state of the leaves 409 | const leaves = this.leaves(); 410 | if (leaves.length) { 411 | // Detach all leaves before we unload the popover and remove it from the DOM. 412 | // Each leaf.detach() will trigger layout-changed and the updateLeaves() 413 | // method will then call hide() again when the last one is gone. 414 | // leaves[0].detach(); 415 | leaves[0].detach(); 416 | // this.targetEl.empty(); 417 | } else { 418 | this.parent = null; 419 | this.abortController?.unload(); 420 | this.abortController = undefined; 421 | return this.nativeHide(); 422 | } 423 | } 424 | 425 | nativeHide() { 426 | const {hoverEl, targetEl} = this; 427 | this.state = PopoverState.Hidden; 428 | hoverEl.detach(); 429 | 430 | if (targetEl) { 431 | const parent = targetEl.matchParent(".dn-leaf-view"); 432 | if (parent) popovers.get(parent)?.transition(); 433 | } 434 | 435 | this.onHide(); 436 | this.unload(); 437 | } 438 | 439 | resolveLink(linkText: string, sourcePath: string): TFile | null { 440 | const link = parseLinktext(linkText); 441 | const tFile = link ? this.plugin.app.metadataCache.getFirstLinkpathDest(link.path, sourcePath) : null; 442 | return tFile; 443 | } 444 | 445 | async openLink(linkText: string, sourcePath: string, eState?: EphemeralState, createInLeaf?: WorkspaceLeaf) { 446 | let file = this.resolveLink(linkText, sourcePath); 447 | const link = parseLinktext(linkText); 448 | if (!file && createInLeaf) { 449 | const folder = this.plugin.app.fileManager.getNewFileParent(sourcePath); 450 | file = await this.plugin.app.fileManager.createNewMarkdownFile(folder, link.path); 451 | } 452 | 453 | if (!file) { 454 | // this.displayCreateFileAction(linkText, sourcePath, eState); 455 | return; 456 | } 457 | const {viewRegistry} = this.plugin.app; 458 | const viewType = viewRegistry.typeByExtension[file.extension]; 459 | if (!viewType || !viewRegistry.viewByType[viewType]) { 460 | // this.displayOpenFileAction(file); 461 | return; 462 | } 463 | 464 | eState = Object.assign(this.buildEphemeralState(file, link), eState); 465 | const parentMode = this.getDefaultMode(); 466 | const state = this.buildState(parentMode, eState); 467 | const leaf = await this.openFile(file, state as OpenViewState, createInLeaf); 468 | const leafViewType = leaf?.view?.getViewType(); 469 | // console.log(leaf); 470 | if (leafViewType === "image") { 471 | // TODO: temporary workaround to prevent image popover from disappearing immediately when using live preview 472 | if ( 473 | this.parent?.hasOwnProperty("editorEl") && 474 | (this.parent as unknown as MarkdownEditView).editorEl!.hasClass("is-live-preview") 475 | ) { 476 | this.waitTime = 3000; 477 | } 478 | const img = leaf!.view.contentEl.querySelector("img")!; 479 | this.hoverEl.dataset.imgHeight = String(img.naturalHeight); 480 | this.hoverEl.dataset.imgWidth = String(img.naturalWidth); 481 | this.hoverEl.dataset.imgRatio = String(img.naturalWidth / img.naturalHeight); 482 | } else if (leafViewType === "pdf") { 483 | this.hoverEl.style.height = "800px"; 484 | this.hoverEl.style.width = "600px"; 485 | } 486 | if (state.state?.mode === "source") { 487 | this.whenShown(() => { 488 | // Not sure why this is needed, but without it we get issue #186 489 | if (requireApiVersion("1.0")) (leaf?.view as any)?.editMode?.reinit?.(); 490 | leaf?.view?.setEphemeralState(state.eState); 491 | }); 492 | } 493 | } 494 | 495 | whenShown(callback: () => any) { 496 | // invoke callback once the popover is visible 497 | if (this.detaching) return; 498 | const existingCallback = this.onShowCallback; 499 | this.onShowCallback = () => { 500 | if (this.detaching) return; 501 | callback(); 502 | if (typeof existingCallback === "function") existingCallback(); 503 | }; 504 | if (this.state === PopoverState.Shown) { 505 | this.onShowCallback(); 506 | this.onShowCallback = undefined; 507 | } 508 | } 509 | 510 | async openFile(file: TFile, openState?: OpenViewState, useLeaf?: WorkspaceLeaf) { 511 | if (this.detaching) return; 512 | const leaf = useLeaf ?? this.attachLeaf(); 513 | this.opening = true; 514 | 515 | try { 516 | await leaf.openFile(file, openState); 517 | } catch (e) { 518 | console.error(e); 519 | } finally { 520 | this.opening = false; 521 | if (this.detaching) this.hide(); 522 | } 523 | this.plugin.app.workspace.setActiveLeaf(leaf); 524 | 525 | return leaf; 526 | } 527 | 528 | buildState(parentMode: string, eState?: EphemeralState) { 529 | return { 530 | active: false, // Don't let Obsidian force focus if we have autofocus off 531 | state: {mode: "source"}, // Don't set any state for the view, because this leaf is stayed on another view. 532 | eState: eState, 533 | }; 534 | } 535 | 536 | buildEphemeralState( 537 | file: TFile, 538 | link?: { 539 | path: string; 540 | subpath: string; 541 | }, 542 | ) { 543 | const cache = this.plugin.app.metadataCache.getFileCache(file); 544 | const subpath = cache ? resolveSubpath(cache, link?.subpath || "") : undefined; 545 | const eState: EphemeralState = {subpath: link?.subpath}; 546 | if (subpath) { 547 | eState.line = subpath.start.line; 548 | eState.startLoc = subpath.start; 549 | eState.endLoc = subpath.end || undefined; 550 | } 551 | return eState; 552 | } 553 | } 554 | -------------------------------------------------------------------------------- /src/style/index.css: -------------------------------------------------------------------------------- 1 | .dn-editor { 2 | /*overflow-y: hidden;*/ 3 | } 4 | 5 | .dn-editor .cm-scroller { 6 | /*overflow-y: hidden;*/ 7 | padding: unset; 8 | } 9 | 10 | .dn-editor .workspace-leaf { 11 | all: unset; 12 | } 13 | 14 | .dn-editor .dn-content { 15 | display: none; 16 | /*margin: 0;*/ 17 | /*border-radius: var(--he-popover-border-radius);*/ 18 | /*overflow: hidden;*/ 19 | /*height: 100%;*/ 20 | } 21 | 22 | .dn-editor .workspace-leaf, 23 | .dn-editor .workspace-split { 24 | height: 100%; 25 | width: 100%; 26 | } 27 | 28 | .dn-editor .markdown-source-view.mod-cm6 .cm-editor { 29 | min-height: auto; 30 | } 31 | 32 | .dn-editor .cm-content { 33 | padding-bottom: 0 !important; 34 | padding-top: 0 !important; 35 | } 36 | 37 | .dn-editor .view-content { 38 | background: none !important; 39 | } 40 | 41 | .dn-editor .cm-scroller { 42 | padding: 0 !important; 43 | overflow-y: clip; 44 | } 45 | .daily-note-view .daily-note-wrapper::before { 46 | content: ""; 47 | display: block; 48 | height: 1px; 49 | width: var(--file-line-width); 50 | margin-bottom: 30px; 51 | margin-left: auto; 52 | margin-right: auto; 53 | background-color: var(--background-modifier-border); 54 | } 55 | 56 | .daily-note-view .daily-note-wrapper:first-of-type::before { 57 | height: 0px; 58 | } 59 | 60 | .daily-note-view .dn-range-indicator + .daily-note-container::before { 61 | height: 0px; 62 | } 63 | 64 | .is-popout-window .dn-editor .dn-content { 65 | margin: 0; 66 | border-radius: var(--he-popover-border-radius); 67 | overflow: hidden; 68 | height: auto; 69 | } 70 | 71 | .is-popout-window .dn-editor .workspace-leaf, 72 | .is-popout-window .dn-editor .workspace-split { 73 | height: auto; 74 | width: 100%; 75 | } 76 | 77 | .is-popout-window .dn-editor .cm-scroller { 78 | height: auto; 79 | } 80 | 81 | .is-popout-window .dn-editor .markdown-source-view.mod-cm6 { 82 | height: auto; 83 | } 84 | 85 | .is-popout-window .dn-editor .view-content { 86 | height: auto; 87 | } 88 | 89 | .is-popout-window .dn-editor .workspace-leaf-content { 90 | height: auto; 91 | } 92 | 93 | .daily-note-view .embedded-backlinks { 94 | min-height: unset !important; 95 | } 96 | 97 | .daily-note-view { 98 | display: flex; 99 | flex-direction: column; 100 | gap: var(--size-4-4); 101 | overflow-x: hidden; 102 | } 103 | 104 | body.daily-notes-hide-frontmatter 105 | .daily-note-view 106 | .markdown-source-view.is-live-preview.show-properties 107 | .metadata-container { 108 | display: none; 109 | } 110 | 111 | body.daily-notes-hide-backlinks .daily-note-view .embedded-backlinks { 112 | display: none; 113 | } 114 | 115 | /* Custom Range Modal Styles */ 116 | .custom-range-date-container { 117 | margin-bottom: var(--size-4-2); 118 | display: flex; 119 | align-items: center; 120 | } 121 | 122 | .custom-range-date-container span { 123 | width: 100px; 124 | display: inline-block; 125 | } 126 | 127 | .custom-range-date-container input { 128 | flex: 1; 129 | } 130 | 131 | .custom-range-button-container { 132 | display: flex; 133 | justify-content: flex-end; 134 | margin-top: var(--size-4-4); 135 | gap: var(--size-4-2); 136 | } 137 | 138 | /* Preset management styles */ 139 | .preset-container { 140 | margin-bottom: var(--size-4-4); 141 | } 142 | 143 | .no-presets-message { 144 | color: var(--text-muted); 145 | font-style: italic; 146 | margin: var(--size-4-2) 0; 147 | } 148 | 149 | .preset-list { 150 | display: flex; 151 | flex-direction: column; 152 | gap: var(--size-4-2); 153 | margin: var(--size-4-2) 0; 154 | } 155 | 156 | .preset-item { 157 | display: flex; 158 | justify-content: space-between; 159 | align-items: center; 160 | padding: var(--size-2-4) var(--size-4-3); 161 | background-color: var(--background-secondary); 162 | border-radius: var(--radius-s); 163 | } 164 | 165 | .preset-info { 166 | display: flex; 167 | align-items: center; 168 | } 169 | 170 | .preset-type { 171 | font-weight: bold; 172 | margin-right: var(--size-2-2); 173 | } 174 | 175 | .preset-actions { 176 | display: flex; 177 | gap: var(--size-4-2); 178 | } 179 | 180 | .preset-action-button { 181 | padding: var(--size-2-2) var(--size-2-4); 182 | border-radius: var(--radius-s); 183 | font-size: var(--font-ui-small); 184 | cursor: pointer; 185 | } 186 | 187 | .preset-open-button { 188 | background-color: var(--interactive-accent); 189 | color: var(--text-on-accent); 190 | } 191 | 192 | .preset-delete-button { 193 | background-color: var(--background-modifier-error); 194 | color: var(--text-on-accent); 195 | } 196 | 197 | /* Add preset modal styles */ 198 | .setting-item { 199 | margin-bottom: var(--size-4-4); 200 | } 201 | 202 | .target-input { 203 | width: 100%; 204 | padding: var(--size-2-3); 205 | border-radius: var(--radius-s); 206 | border: 1px solid var(--background-modifier-border); 207 | background-color: var(--background-primary); 208 | } 209 | 210 | .modal-button-container { 211 | display: flex; 212 | justify-content: flex-end; 213 | gap: var(--size-4-2); 214 | margin-top: var(--size-4-4); 215 | } 216 | 217 | .is-phone .mod-root .workspace-tabs:not(.mod-visible):has(.daily-note-view) { 218 | display: flex !important; 219 | } 220 | -------------------------------------------------------------------------------- /src/types/obsidian.d.ts: -------------------------------------------------------------------------------- 1 | import "obsidian"; 2 | import { Plugin, SuggestModal, TFile, View, WorkspaceLeaf } from "obsidian"; 3 | 4 | interface InternalPlugins { 5 | switcher: QuickSwitcherPlugin; 6 | "page-preview": InternalPlugin; 7 | graph: GraphPlugin; 8 | } 9 | 10 | declare class QuickSwitcherModal extends SuggestModal { 11 | getSuggestions(query: string): TFile[] | Promise; 12 | 13 | renderSuggestion(value: TFile, el: HTMLElement): unknown; 14 | 15 | onChooseSuggestion(item: TFile, evt: MouseEvent | KeyboardEvent): unknown; 16 | } 17 | 18 | interface InternalPlugin { 19 | disable(): void; 20 | 21 | enable(): void; 22 | 23 | enabled: boolean; 24 | _loaded: boolean; 25 | instance: { name: string; id: string }; 26 | } 27 | 28 | interface GraphPlugin extends InternalPlugin { 29 | views: { localgraph: (leaf: WorkspaceLeaf) => GraphView }; 30 | } 31 | 32 | interface GraphView extends View { 33 | engine: typeof Object; 34 | renderer: { worker: { terminate(): void } }; 35 | } 36 | 37 | interface QuickSwitcherPlugin extends InternalPlugin { 38 | instance: { 39 | name: string; 40 | id: string; 41 | QuickSwitcherModal: typeof QuickSwitcherModal; 42 | }; 43 | } 44 | 45 | declare global { 46 | const i18next: { 47 | t(id: string): string; 48 | }; 49 | 50 | interface Window { 51 | activeWindow: Window; 52 | activeDocument: Document; 53 | } 54 | } 55 | 56 | declare module "obsidian" { 57 | interface App { 58 | internalPlugins: { 59 | plugins: InternalPlugins; 60 | getPluginById( 61 | id: T 62 | ): InternalPlugins[T]; 63 | }; 64 | plugins: { 65 | manifests: Record; 66 | plugins: Record & { 67 | ["recent-files-obsidian"]: Plugin & { 68 | shouldAddFile(file: TFile): boolean; 69 | }; 70 | }; 71 | getPlugin(id: string): Plugin; 72 | getPlugin(id: "calendar"): CalendarPlugin; 73 | }; 74 | dom: { appContainerEl: HTMLElement }; 75 | viewRegistry: ViewRegistry; 76 | 77 | openWithDefaultApp(path: string): void; 78 | } 79 | 80 | interface ViewRegistry { 81 | typeByExtension: Record; // file extensions to view types 82 | viewByType: Record View>; // file extensions to view types 83 | } 84 | 85 | interface CalendarPlugin { 86 | view: View; 87 | } 88 | 89 | interface WorkspaceParent { 90 | insertChild( 91 | index: number, 92 | child: WorkspaceItem, 93 | resize?: boolean 94 | ): void; 95 | 96 | replaceChild( 97 | index: number, 98 | child: WorkspaceItem, 99 | resize?: boolean 100 | ): void; 101 | 102 | removeChild(leaf: WorkspaceLeaf, resize?: boolean): void; 103 | 104 | containerEl: HTMLElement; 105 | } 106 | 107 | interface MarkdownEditView { 108 | editorEl: HTMLElement; 109 | } 110 | 111 | class MarkdownPreviewRendererStatic extends MarkdownPreviewRenderer { 112 | static registerDomEvents( 113 | el: HTMLElement, 114 | handlerInstance: unknown, 115 | cb: (el: HTMLElement) => unknown 116 | ): void; 117 | } 118 | 119 | interface WorkspaceLeaf { 120 | openLinkText( 121 | linkText: string, 122 | path: string, 123 | state?: unknown 124 | ): Promise; 125 | 126 | updateHeader(): void; 127 | 128 | containerEl: HTMLDivElement; 129 | working: boolean; 130 | parentSplit: WorkspaceParent; 131 | parentLeaf: WorkspaceLeaf; 132 | 133 | height: number; 134 | } 135 | 136 | interface Workspace { 137 | recordHistory(leaf: WorkspaceLeaf, pushHistory: boolean): void; 138 | 139 | iterateLeaves( 140 | callback: (item: WorkspaceLeaf) => boolean | void, 141 | item: WorkspaceItem | WorkspaceItem[] 142 | ): boolean; 143 | 144 | iterateLeaves( 145 | item: WorkspaceItem | WorkspaceItem[], 146 | callback: (item: WorkspaceLeaf) => boolean | void 147 | ): boolean; 148 | 149 | getDropLocation(event: MouseEvent): { 150 | target: WorkspaceItem; 151 | sidedock: boolean; 152 | }; 153 | 154 | recursiveGetTarget( 155 | event: MouseEvent, 156 | parent: WorkspaceParent 157 | ): WorkspaceItem; 158 | 159 | recordMostRecentOpenedFile(file: TFile): void; 160 | 161 | onDragLeaf(event: MouseEvent, leaf: WorkspaceLeaf): void; 162 | 163 | onLayoutChange(): void; // tell Obsidian leaves have been added/removed/etc. 164 | activeLeafEvents(): void; 165 | } 166 | 167 | interface Editor { 168 | getClickableTokenAt(pos: EditorPosition): { 169 | text: string; 170 | type: string; 171 | start: EditorPosition; 172 | end: EditorPosition; 173 | }; 174 | } 175 | 176 | interface View { 177 | iconEl: HTMLElement; 178 | file: TFile; 179 | 180 | setMode(mode: MarkdownSubView): Promise; 181 | 182 | followLinkUnderCursor(newLeaf: boolean): void; 183 | 184 | modes: Record; 185 | 186 | getMode(): string; 187 | 188 | headerEl: HTMLElement; 189 | contentEl: HTMLElement; 190 | titleEl: HTMLElement; 191 | } 192 | 193 | interface EmptyView extends View { 194 | actionListEl: HTMLElement; 195 | emptyTitleEl: HTMLElement; 196 | } 197 | 198 | interface FileManager { 199 | createNewMarkdownFile( 200 | folder: TFolder, 201 | fileName: string 202 | ): Promise; 203 | } 204 | 205 | enum PopoverState { 206 | Showing, 207 | Shown, 208 | Hiding, 209 | Hidden, 210 | } 211 | 212 | interface Menu { 213 | items: MenuItem[]; 214 | dom: HTMLElement; 215 | hideCallback: () => unknown; 216 | } 217 | 218 | interface Workspace { 219 | floatingSplit: any; 220 | } 221 | 222 | interface MenuItem { 223 | iconEl: HTMLElement; 224 | dom: HTMLElement; 225 | } 226 | 227 | interface EphemeralState { 228 | focus?: boolean; 229 | subpath?: string; 230 | line?: number; 231 | startLoc?: Loc; 232 | endLoc?: Loc; 233 | scroll?: number; 234 | } 235 | 236 | interface HoverParent { 237 | type?: string; 238 | } 239 | 240 | interface HoverPopover { 241 | parent: any; 242 | targetEl: HTMLElement; 243 | hoverEl: HTMLElement; 244 | 245 | position(pos?: MousePos): void; 246 | 247 | hide(): void; 248 | 249 | show(): void; 250 | 251 | shouldShowSelf(): boolean; 252 | 253 | timer: number; 254 | waitTime: number; 255 | 256 | shouldShow(): boolean; 257 | 258 | transition(): void; 259 | } 260 | 261 | interface MousePos { 262 | x: number; 263 | y: number; 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/types/time.d.ts: -------------------------------------------------------------------------------- 1 | export type TimeRange = 2 | | "week" 3 | | "month" 4 | | "year" 5 | | "all" 6 | | "last-week" 7 | | "last-month" 8 | | "last-year" 9 | | "quarter" 10 | | "last-quarter" 11 | | "custom"; 12 | 13 | export type SelectionMode = "daily" | "folder" | "tag"; 14 | 15 | export type TimeField = 16 | | "ctime" 17 | | "mtime" 18 | | "ctimeReverse" 19 | | "mtimeReverse" 20 | | "name" 21 | | "nameReverse"; 22 | -------------------------------------------------------------------------------- /src/utils/fileManager.ts: -------------------------------------------------------------------------------- 1 | import { TFile, moment, App } from "obsidian"; 2 | import { 3 | getAllDailyNotes, 4 | getDailyNote, 5 | createDailyNote, 6 | getDateFromFile, 7 | getDailyNoteSettings, 8 | DEFAULT_DAILY_NOTE_FORMAT, 9 | } from "obsidian-daily-notes-interface"; 10 | import { TimeRange, TimeField } from "../types/time"; 11 | 12 | export interface FileManagerOptions { 13 | mode: "daily" | "folder" | "tag"; 14 | target?: string; 15 | timeRange?: TimeRange; 16 | customRange?: { start: Date; end: Date } | null; 17 | app?: App; 18 | timeField?: TimeField; 19 | } 20 | 21 | export class FileManager { 22 | private allFiles: TFile[] = []; 23 | private filteredFiles: TFile[] = []; 24 | private hasFetched: boolean = false; 25 | private hasCurrentDay: boolean = true; 26 | private cacheDailyNotes: Record = {}; 27 | 28 | // Make options public so it can be accessed from outside 29 | public options: FileManagerOptions; 30 | 31 | constructor(options: FileManagerOptions) { 32 | this.options = options; 33 | this.fetchFiles(); 34 | } 35 | 36 | /** 37 | * Helper method to parse time field and check if it's reverse 38 | * @param timeField The time field to parse 39 | * @returns An object containing isReverse flag and baseTimeField 40 | */ 41 | private parseTimeField(timeField: TimeField | undefined): { 42 | isReverse: boolean; 43 | baseTimeField: string; 44 | } { 45 | const field = timeField || "mtime"; 46 | const isReverse = field.endsWith("Reverse"); 47 | const baseTimeField = isReverse ? field.replace("Reverse", "") : field; 48 | return { isReverse, baseTimeField }; 49 | } 50 | 51 | /** 52 | * Helper method to sort files by time field 53 | * @param files The files to sort 54 | * @param timeField The time field to sort by 55 | * @returns Sorted files 56 | */ 57 | private sortFilesByTimeField( 58 | files: TFile[], 59 | timeField?: TimeField 60 | ): TFile[] { 61 | const { isReverse, baseTimeField } = this.parseTimeField(timeField); 62 | 63 | return [...files].sort((a, b) => { 64 | // Handle name-based sorting 65 | if (baseTimeField === "name") { 66 | // For name sorting, we sort alphabetically by filename 67 | if (isReverse) { 68 | return b.name.localeCompare(a.name); 69 | } 70 | return a.name.localeCompare(b.name); 71 | } 72 | 73 | // Handle time-based sorting (existing functionality) 74 | if (isReverse) { 75 | return a.stat[baseTimeField] - b.stat[baseTimeField]; 76 | } 77 | return b.stat[baseTimeField] - a.stat[baseTimeField]; 78 | }); 79 | } 80 | 81 | public fetchFiles(): void { 82 | if (this.hasFetched) return; 83 | 84 | switch (this.options.mode) { 85 | case "daily": 86 | this.fetchDailyNotes(); 87 | break; 88 | case "folder": 89 | this.fetchFolderFiles(); 90 | break; 91 | case "tag": 92 | this.fetchTaggedFiles(); 93 | break; 94 | } 95 | 96 | this.hasFetched = true; 97 | this.checkDailyNote(); 98 | this.filterFilesByRange(); 99 | } 100 | 101 | private fetchDailyNotes(): void { 102 | this.cacheDailyNotes = getAllDailyNotes(); 103 | 104 | // Convert the object to an array of files 105 | const notes = Object.values(this.cacheDailyNotes) as TFile[]; 106 | 107 | // Sort based on the selected time field 108 | const { isReverse, baseTimeField } = this.parseTimeField( 109 | this.options.timeField 110 | ); 111 | 112 | if (baseTimeField === "name") { 113 | // For name-based sorting, sort by filename 114 | this.allFiles = [...notes].sort((a, b) => { 115 | const result = a.name.localeCompare(b.name); 116 | return isReverse ? -result : result; 117 | }); 118 | } else { 119 | // Default sorting (by date in the filename) 120 | // Build notes list by date in descending order 121 | for (const string of Object.keys(this.cacheDailyNotes) 122 | .sort() 123 | .reverse()) { 124 | this.allFiles.push(this.cacheDailyNotes[string]); 125 | } 126 | 127 | // Apply additional time-based sorting if needed 128 | if (baseTimeField !== "ctime" && baseTimeField !== "mtime") { 129 | this.allFiles = this.sortFilesByTimeField( 130 | this.allFiles, 131 | this.options.timeField 132 | ); 133 | } 134 | } 135 | } 136 | 137 | private fetchFolderFiles(): void { 138 | if (!this.options.target || !this.options.app) return; 139 | 140 | // Get all files in the vault 141 | const allFiles = this.options.app.vault.getMarkdownFiles(); 142 | 143 | // Filter files by folder path 144 | this.allFiles = allFiles.filter((file) => { 145 | const folderPath = file.parent?.path || ""; 146 | return ( 147 | folderPath === this.options.target || 148 | folderPath.startsWith(this.options.target + "/") 149 | ); 150 | }); 151 | 152 | // Sort files by the specified time field 153 | this.allFiles = this.sortFilesByTimeField( 154 | this.allFiles, 155 | this.options.timeField 156 | ); 157 | } 158 | 159 | private fetchTaggedFiles(): void { 160 | if (!this.options.target || !this.options.app) return; 161 | 162 | // Get all files with the specified tag 163 | const allFiles = this.options.app.vault.getMarkdownFiles(); 164 | const targetTag = this.options.target.startsWith("#") 165 | ? this.options.target 166 | : "#" + this.options.target; 167 | 168 | this.allFiles = allFiles.filter((file) => { 169 | // Check if the file has the target tag in its cache 170 | const fileCache = 171 | this.options.app?.metadataCache.getFileCache(file); 172 | if (!fileCache || !fileCache.tags) return false; 173 | 174 | return fileCache.tags.some((tag) => tag.tag === targetTag); 175 | }); 176 | 177 | // Sort files by the specified time field 178 | this.allFiles = this.sortFilesByTimeField( 179 | this.allFiles, 180 | this.options.timeField 181 | ); 182 | } 183 | 184 | public filterFilesByRange(): TFile[] { 185 | // If no time range is specified, return all files 186 | if (!this.options.timeRange) { 187 | this.filteredFiles = [...this.allFiles]; 188 | return this.filteredFiles; 189 | } 190 | 191 | // Reset the filtered files list 192 | this.filteredFiles = []; 193 | 194 | // If the time range is "all", return all files 195 | if (this.options.timeRange === "all") { 196 | this.filteredFiles = [...this.allFiles]; 197 | return this.filteredFiles; 198 | } 199 | 200 | // Use different filtering methods based on different modes 201 | if (this.options.mode === "daily") { 202 | // Daily mode: filter daily notes by date 203 | this.filterDailyNotesByRange(); 204 | } else { 205 | // Folder and tag modes: filter files by creation or modification time 206 | this.filterFilesByTimeRange(); 207 | } 208 | 209 | return this.filteredFiles; 210 | } 211 | 212 | /** 213 | * Filter files by time range 214 | * Applicable to folder and tag modes 215 | */ 216 | private filterFilesByTimeRange(): void { 217 | const now = moment(); 218 | const { isReverse, baseTimeField } = this.parseTimeField( 219 | this.options.timeField 220 | ); 221 | 222 | // Filter files by creation or modification time 223 | this.filteredFiles = this.allFiles.filter((file) => { 224 | // Get the time of the file based on the base timeField option 225 | const fileDate = moment(file.stat[baseTimeField]); 226 | 227 | return this.isDateInRange(fileDate, now); 228 | }); 229 | 230 | // If using reverse time field, reverse the order of filtered files 231 | if (isReverse) { 232 | this.filteredFiles.reverse(); 233 | } 234 | } 235 | 236 | /** 237 | * Filter daily notes by date 238 | * Applicable to daily mode 239 | */ 240 | private filterDailyNotesByRange(): void { 241 | const now = moment(); 242 | const fileFormat = 243 | getDailyNoteSettings().format || DEFAULT_DAILY_NOTE_FORMAT; 244 | 245 | this.filteredFiles = this.allFiles.filter((file) => { 246 | const fileDate = moment(file.basename, fileFormat); 247 | 248 | return this.isDateInRange(fileDate, now); 249 | }); 250 | } 251 | 252 | /** 253 | * Check if the file date is in the range 254 | * @param fileDate file date 255 | * @param now current date 256 | * @returns whether in the range 257 | */ 258 | private isDateInRange( 259 | fileDate: moment.Moment, 260 | now: moment.Moment 261 | ): boolean { 262 | switch (this.options.timeRange) { 263 | case "week": 264 | return fileDate.isSame(now, "week"); 265 | case "month": 266 | return fileDate.isSame(now, "month"); 267 | case "year": 268 | return fileDate.isSame(now, "year"); 269 | case "last-week": 270 | return fileDate.isBetween( 271 | moment().subtract(1, "week").startOf("week"), 272 | moment().subtract(1, "week").endOf("week"), 273 | null, 274 | "[]" 275 | ); 276 | case "last-month": 277 | return fileDate.isBetween( 278 | moment().subtract(1, "month").startOf("month"), 279 | moment().subtract(1, "month").endOf("month"), 280 | null, 281 | "[]" 282 | ); 283 | case "last-year": 284 | return fileDate.isBetween( 285 | moment().subtract(1, "year").startOf("year"), 286 | moment().subtract(1, "year").endOf("year"), 287 | null, 288 | "[]" 289 | ); 290 | case "quarter": 291 | return fileDate.isSame(now, "quarter"); 292 | case "last-quarter": 293 | return fileDate.isBetween( 294 | moment().subtract(1, "quarter").startOf("quarter"), 295 | moment().subtract(1, "quarter").endOf("quarter"), 296 | null, 297 | "[]" 298 | ); 299 | case "custom": 300 | if (this.options.customRange) { 301 | const startDate = moment(this.options.customRange.start); 302 | const endDate = moment(this.options.customRange.end); 303 | return fileDate.isBetween(startDate, endDate, null, "[]"); 304 | } 305 | return false; 306 | default: 307 | return true; 308 | } 309 | } 310 | 311 | public checkDailyNote(): boolean { 312 | if (this.options.mode !== "daily") { 313 | this.hasCurrentDay = true; 314 | return true; 315 | } 316 | 317 | // Refresh the daily notes cache to ensure we have the latest data 318 | this.cacheDailyNotes = getAllDailyNotes(); 319 | 320 | // @ts-ignore 321 | const currentDate = moment(); 322 | const currentDailyNote = getDailyNote( 323 | currentDate, 324 | this.cacheDailyNotes 325 | ); 326 | 327 | if (!currentDailyNote) { 328 | this.hasCurrentDay = false; 329 | return false; 330 | } 331 | 332 | // Check if we need to update the allFiles and filteredFiles arrays 333 | if (this.hasCurrentDay === false) { 334 | // We didn't have the current day's note before, but now we do 335 | // So we need to update our file lists 336 | this.allFiles = []; 337 | this.fetchDailyNotes(); 338 | this.filterFilesByRange(); 339 | } 340 | 341 | this.hasCurrentDay = true; 342 | return true; 343 | } 344 | 345 | public async createNewDailyNote(): Promise { 346 | if (this.options.mode !== "daily" || this.hasCurrentDay) { 347 | return null; 348 | } 349 | 350 | const currentDate = moment(); 351 | const currentDailyNote: any = await createDailyNote(currentDate); 352 | 353 | if (currentDailyNote) { 354 | this.allFiles.push(currentDailyNote); 355 | this.allFiles = this.sortDailyNotes(this.allFiles); 356 | this.hasCurrentDay = true; 357 | this.filterFilesByRange(); 358 | return currentDailyNote; 359 | } 360 | 361 | return null; 362 | } 363 | 364 | public fileCreate(file: TFile): void { 365 | if (this.options.mode === "daily") { 366 | this.handleDailyNoteCreate(file); 367 | } else if (this.options.mode === "folder") { 368 | this.handleFolderFileCreate(file); 369 | } else if (this.options.mode === "tag") { 370 | this.handleTaggedFileCreate(file); 371 | } 372 | } 373 | 374 | private handleDailyNoteCreate(file: TFile): void { 375 | const fileDate = getDateFromFile(file as any, "day"); 376 | const fileFormat = 377 | getDailyNoteSettings().format || DEFAULT_DAILY_NOTE_FORMAT; 378 | if (!fileDate) return; 379 | 380 | if (this.filteredFiles.length === 0) { 381 | this.allFiles.push(file); 382 | this.allFiles = this.sortDailyNotes(this.allFiles); 383 | this.filterFilesByRange(); 384 | return; 385 | } 386 | 387 | const lastFilteredFile = 388 | this.filteredFiles[this.filteredFiles.length - 1]; 389 | const firstFilteredFile = this.filteredFiles[0]; 390 | const lastFilteredFileDate = moment( 391 | lastFilteredFile.basename, 392 | fileFormat 393 | ); 394 | const firstFilteredFileDate = moment( 395 | firstFilteredFile.basename, 396 | fileFormat 397 | ); 398 | 399 | if (fileDate.isBetween(lastFilteredFileDate, firstFilteredFileDate)) { 400 | this.filteredFiles.push(file); 401 | this.filteredFiles = this.sortDailyNotes(this.filteredFiles); 402 | } else if (fileDate.isBefore(lastFilteredFileDate)) { 403 | this.allFiles.push(file); 404 | this.allFiles = this.sortDailyNotes(this.allFiles); 405 | this.filterFilesByRange(); 406 | } else if (fileDate.isAfter(firstFilteredFileDate)) { 407 | this.filteredFiles.push(file); 408 | this.filteredFiles = this.sortDailyNotes(this.filteredFiles); 409 | } 410 | 411 | if (fileDate.isSame(moment(), "day")) this.hasCurrentDay = true; 412 | } 413 | 414 | private handleFolderFileCreate(file: TFile): void { 415 | if (!this.options.target) return; 416 | 417 | // Check if the file belongs to the target folder 418 | const folderPath = file.parent?.path || ""; 419 | if ( 420 | folderPath === this.options.target || 421 | folderPath.startsWith(this.options.target + "/") 422 | ) { 423 | // Add the file to the collections 424 | this.allFiles.push(file); 425 | 426 | // Sort files by the specified time field 427 | this.allFiles = this.sortFilesByTimeField( 428 | this.allFiles, 429 | this.options.timeField 430 | ); 431 | 432 | // Update filtered files 433 | this.filterFilesByRange(); 434 | } 435 | } 436 | 437 | private handleTaggedFileCreate(file: TFile): void { 438 | if (!this.options.target || !this.options.app) return; 439 | 440 | // Check if the file has the target tag 441 | const targetTag = this.options.target.startsWith("#") 442 | ? this.options.target 443 | : "#" + this.options.target; 444 | 445 | const fileCache = this.options.app.metadataCache.getFileCache(file); 446 | if (!fileCache || !fileCache.tags) return; 447 | 448 | if (fileCache.tags.some((tag) => tag.tag === targetTag)) { 449 | // Add the file to the collections 450 | this.allFiles.push(file); 451 | 452 | // Sort files by the specified time field 453 | this.allFiles = this.sortFilesByTimeField( 454 | this.allFiles, 455 | this.options.timeField 456 | ); 457 | 458 | // Update filtered files 459 | this.filterFilesByRange(); 460 | } 461 | } 462 | 463 | public fileDelete(file: TFile): void { 464 | if ( 465 | this.options.mode === "daily" && 466 | getDateFromFile(file as any, "day") 467 | ) { 468 | this.filteredFiles = this.filteredFiles.filter((f) => { 469 | return f.basename !== file.basename; 470 | }); 471 | this.allFiles = this.allFiles.filter((f) => { 472 | return f.basename !== file.basename; 473 | }); 474 | this.filterFilesByRange(); 475 | this.checkDailyNote(); 476 | } else { 477 | // Handle deletion for folder and tag modes 478 | this.filteredFiles = this.filteredFiles.filter((f) => { 479 | return f.basename !== file.basename; 480 | }); 481 | this.allFiles = this.allFiles.filter((f) => { 482 | return f.basename !== file.basename; 483 | }); 484 | } 485 | } 486 | 487 | private sortDailyNotes(notes: TFile[]): TFile[] { 488 | // Sort daily notes by date (newest first by default) 489 | // For this, we're using the file name which follows the daily note format 490 | const { isReverse, baseTimeField } = this.parseTimeField( 491 | this.options.timeField 492 | ); 493 | 494 | // If sorting by name, use alphabetical sorting which will automatically 495 | // sort chronologically for date-formatted filenames 496 | if (baseTimeField === "name") { 497 | return [...notes].sort((a, b) => { 498 | if (isReverse) { 499 | return b.name.localeCompare(a.name); 500 | } 501 | return a.name.localeCompare(b.name); 502 | }); 503 | } 504 | 505 | // Otherwise use the normal time-based sorting 506 | return this.sortFilesByTimeField(notes, this.options.timeField); 507 | } 508 | 509 | public getAllFiles(): TFile[] { 510 | return this.allFiles; 511 | } 512 | 513 | public getFilteredFiles(): TFile[] { 514 | return this.filteredFiles; 515 | } 516 | 517 | public hasCurrentDayNote(): boolean { 518 | return this.hasCurrentDay; 519 | } 520 | 521 | public updateOptions(options: Partial): void { 522 | this.options = { ...this.options, ...options }; 523 | 524 | if (options.timeRange || options.customRange) { 525 | this.filterFilesByRange(); 526 | } 527 | 528 | if (options.mode || options.target) { 529 | this.allFiles = []; 530 | this.filteredFiles = []; 531 | this.hasFetched = false; 532 | this.fetchFiles(); 533 | } 534 | } 535 | } 536 | -------------------------------------------------------------------------------- /src/utils/icon.ts: -------------------------------------------------------------------------------- 1 | import { addIcon } from "obsidian"; 2 | 3 | export const addIconList = () => { 4 | addIcon("daily-note", ``) 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | export function genId(size: number): string { 2 | const chars: string[] = []; 3 | for (let n = 0; n < size; n++) chars.push(((16 * Math.random()) | 0).toString(16) as string); 4 | return chars.join(''); 5 | } 6 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .dn-editor { 2 | /*overflow-y: hidden;*/ 3 | } 4 | 5 | .dn-editor .cm-scroller { 6 | /*overflow-y: hidden;*/ 7 | padding: unset; 8 | } 9 | 10 | .dn-editor .workspace-leaf { 11 | all: unset; 12 | } 13 | 14 | .dn-editor .dn-content { 15 | display: none; 16 | /*margin: 0;*/ 17 | /*border-radius: var(--he-popover-border-radius);*/ 18 | /*overflow: hidden;*/ 19 | /*height: 100%;*/ 20 | } 21 | 22 | .dn-editor .workspace-leaf, 23 | .dn-editor .workspace-split { 24 | height: 100%; 25 | width: 100%; 26 | } 27 | 28 | .dn-editor .markdown-source-view.mod-cm6 .cm-editor { 29 | min-height: auto; 30 | } 31 | 32 | .dn-editor .cm-content { 33 | padding-bottom: 0 !important; 34 | padding-top: 0 !important; 35 | } 36 | 37 | .dn-editor .view-content { 38 | background: none !important; 39 | } 40 | 41 | .dn-editor .cm-scroller { 42 | padding: 0 !important; 43 | overflow-y: clip; 44 | } 45 | .daily-note-view .daily-note-wrapper::before { 46 | content: ""; 47 | display: block; 48 | height: 1px; 49 | width: var(--file-line-width); 50 | margin-bottom: 30px; 51 | margin-left: auto; 52 | margin-right: auto; 53 | background-color: var(--background-modifier-border); 54 | } 55 | 56 | .daily-note-view .daily-note-wrapper:first-of-type::before { 57 | height: 0px; 58 | } 59 | 60 | .daily-note-view .dn-range-indicator + .daily-note-container::before { 61 | height: 0px; 62 | } 63 | 64 | .is-popout-window .dn-editor .dn-content { 65 | margin: 0; 66 | border-radius: var(--he-popover-border-radius); 67 | overflow: hidden; 68 | height: auto; 69 | } 70 | 71 | .is-popout-window .dn-editor .workspace-leaf, 72 | .is-popout-window .dn-editor .workspace-split { 73 | height: auto; 74 | width: 100%; 75 | } 76 | 77 | .is-popout-window .dn-editor .cm-scroller { 78 | height: auto; 79 | } 80 | 81 | .is-popout-window .dn-editor .markdown-source-view.mod-cm6 { 82 | height: auto; 83 | } 84 | 85 | .is-popout-window .dn-editor .view-content { 86 | height: auto; 87 | } 88 | 89 | .is-popout-window .dn-editor .workspace-leaf-content { 90 | height: auto; 91 | } 92 | 93 | .daily-note-view .embedded-backlinks { 94 | min-height: unset !important; 95 | } 96 | 97 | .daily-note-view { 98 | display: flex; 99 | flex-direction: column; 100 | gap: var(--size-4-4); 101 | overflow-x: hidden; 102 | } 103 | 104 | body.daily-notes-hide-frontmatter 105 | .daily-note-view 106 | .markdown-source-view.is-live-preview.show-properties 107 | .metadata-container { 108 | display: none; 109 | } 110 | 111 | body.daily-notes-hide-backlinks .daily-note-view .embedded-backlinks { 112 | display: none; 113 | } 114 | 115 | /* Custom Range Modal Styles */ 116 | .custom-range-date-container { 117 | margin-bottom: var(--size-4-2); 118 | display: flex; 119 | align-items: center; 120 | } 121 | 122 | .custom-range-date-container span { 123 | width: 100px; 124 | display: inline-block; 125 | } 126 | 127 | .custom-range-date-container input { 128 | flex: 1; 129 | } 130 | 131 | .custom-range-button-container { 132 | display: flex; 133 | justify-content: flex-end; 134 | margin-top: var(--size-4-4); 135 | gap: var(--size-4-2); 136 | } 137 | 138 | /* Preset management styles */ 139 | .preset-container { 140 | margin-bottom: var(--size-4-4); 141 | } 142 | 143 | .no-presets-message { 144 | color: var(--text-muted); 145 | font-style: italic; 146 | margin: var(--size-4-2) 0; 147 | } 148 | 149 | .preset-list { 150 | display: flex; 151 | flex-direction: column; 152 | gap: var(--size-4-2); 153 | margin: var(--size-4-2) 0; 154 | } 155 | 156 | .preset-item { 157 | display: flex; 158 | justify-content: space-between; 159 | align-items: center; 160 | padding: var(--size-2-4) var(--size-4-3); 161 | background-color: var(--background-secondary); 162 | border-radius: var(--radius-s); 163 | } 164 | 165 | .preset-info { 166 | display: flex; 167 | align-items: center; 168 | } 169 | 170 | .preset-type { 171 | font-weight: bold; 172 | margin-right: var(--size-2-2); 173 | } 174 | 175 | .preset-actions { 176 | display: flex; 177 | gap: var(--size-4-2); 178 | } 179 | 180 | .preset-action-button { 181 | padding: var(--size-2-2) var(--size-2-4); 182 | border-radius: var(--radius-s); 183 | font-size: var(--font-ui-small); 184 | cursor: pointer; 185 | } 186 | 187 | .preset-open-button { 188 | background-color: var(--interactive-accent); 189 | color: var(--text-on-accent); 190 | } 191 | 192 | .preset-delete-button { 193 | background-color: var(--background-modifier-error); 194 | color: var(--text-on-accent); 195 | } 196 | 197 | /* Add preset modal styles */ 198 | .setting-item { 199 | margin-bottom: var(--size-4-4); 200 | } 201 | 202 | .target-input { 203 | width: 100%; 204 | padding: var(--size-2-3); 205 | border-radius: var(--radius-s); 206 | border: 1px solid var(--background-modifier-border); 207 | background-color: var(--background-primary); 208 | } 209 | 210 | .modal-button-container { 211 | display: flex; 212 | justify-content: flex-end; 213 | gap: var(--size-4-2); 214 | margin-top: var(--size-4-4); 215 | } 216 | 217 | .is-phone .mod-root .workspace-tabs:not(.mod-visible):has(.daily-note-view) { 218 | display: flex !important; 219 | } 220 | .daily-note.svelte-1d2sruf.svelte-1d2sruf{margin-bottom:var(--size-4-5);padding-bottom:var(--size-4-8)}.daily-note.svelte-1d2sruf.svelte-1d2sruf:has(.daily-note-editor[data-collapsed="true"]){margin-bottom:0;padding-bottom:0}.daily-note-editor.svelte-1d2sruf.svelte-1d2sruf{min-height:100px}.daily-note-editor[data-collapsed="true"].svelte-1d2sruf.svelte-1d2sruf{display:none}.daily-note.svelte-1d2sruf .collapse-button.svelte-1d2sruf{display:none}.daily-note.svelte-1d2sruf:hover .collapse-button.svelte-1d2sruf{display:block}.daily-note.svelte-1d2sruf .collapse-button.svelte-1d2sruf{color:var(--text-muted)}.daily-note.svelte-1d2sruf .collapse-button.svelte-1d2sruf:hover{color:var(--text-normal)}.daily-note.svelte-1d2sruf:has(.is-readable-line-width) .daily-note-title.svelte-1d2sruf{max-width:calc(var(--file-line-width) + var(--size-4-4));width:calc(var(--file-line-width) + var(--size-4-4));margin-left:auto;margin-right:auto;margin-bottom:var(--size-4-8);display:flex;align-items:center;justify-content:start;gap:var(--size-4-2)}.collapse-button.svelte-1d2sruf.svelte-1d2sruf{margin-left:calc(var(--size-4-8) * -1)}.collapse-button[data-collapsed="true"].svelte-1d2sruf.svelte-1d2sruf{transform:rotate(-90deg);transition:transform 0.2s ease}.daily-note.svelte-1d2sruf:not(:has(.is-readable-line-width)) .daily-note-title.svelte-1d2sruf{display:flex;justify-content:start;align-items:center;width:100%;padding-left:calc(calc(100% - var(--file-line-width)) / 2 - var(--size-4-2));padding-right:calc(calc(100% - var(--file-line-width)) / 2 - var(--size-4-2));margin-top:var(--size-4-8);gap:var(--size-4-2)}.clickable-link.svelte-1d2sruf.svelte-1d2sruf{cursor:pointer;text-decoration:none}.clickable-link.svelte-1d2sruf.svelte-1d2sruf:hover{color:var(--color-accent);text-decoration:underline}.editor-placeholder.svelte-1d2sruf.svelte-1d2sruf{display:flex;justify-content:center;align-items:center;height:100px;color:var(--text-muted);font-style:italic}.collapse-button.svelte-1d2sruf.svelte-1d2sruf{cursor:pointer;display:flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:4px;color:var(--text-muted);transition:background-color 0.2s ease}.collapse-button.svelte-1d2sruf.svelte-1d2sruf:hover{color:var(--text-normal)}.dn-stock.svelte-4q3cv7{height:1000px;width:100%;display:flex;justify-content:center;align-items:center}.dn-stock-text.svelte-4q3cv7{text-align:center}.no-more-text.svelte-4q3cv7{margin-left:auto;margin-right:auto;text-align:center}.dn-blank-day.svelte-4q3cv7{display:flex;margin-left:auto;margin-right:auto;max-width:var(--file-line-width);color:var(--color-base-40);padding-top:20px;padding-bottom:20px;transition:all 300ms}.dn-blank-day.svelte-4q3cv7:hover{padding-top:40px;padding-bottom:40px;transition:padding 300ms}.dn-blank-day-text.svelte-4q3cv7{margin-left:auto;margin-right:auto;text-align:center}.daily-note-wrapper.svelte-4q3cv7{width:100%} -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "ES6", 8 | "allowJs": true, 9 | "noImplicitAny": false, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "isolatedModules": true, 13 | "strictNullChecks": true, 14 | "lib": [ 15 | "DOM", 16 | "ES5", 17 | "ES6", 18 | "ES7" 19 | ], 20 | "types": [ 21 | "node" 22 | ] 23 | }, 24 | // "extends": "@tsconfig/svelte/tsconfig.json", 25 | "include": [ 26 | "**/globals.d.ts", 27 | "**/*.js", 28 | "**/*.svelte", 29 | "**/*.ts" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /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.0.1": "0.15.0", 3 | "0.0.2": "0.15.0", 4 | "0.0.3": "0.15.0", 5 | "0.0.4": "0.15.0", 6 | "0.0.5": "0.15.0", 7 | "0.0.7": "0.15.0", 8 | "0.1.0": "0.15.0", 9 | "0.1.2": "0.15.0", 10 | "0.1.4": "0.15.0", 11 | "0.1.5": "0.15.0", 12 | "0.1.6": "0.15.0", 13 | "0.1.7": "0.15.0", 14 | "1.0.0": "0.15.0", 15 | "1.1.0": "0.15.0" 16 | } -------------------------------------------------------------------------------- /vite.config.mjs: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import {defineConfig} from 'vite'; 3 | import {svelte} from '@sveltejs/vite-plugin-svelte'; 4 | import autoPreprocess from 'svelte-preprocess'; 5 | import terser from '@rollup/plugin-terser'; 6 | import replace from '@rollup/plugin-replace'; 7 | import resolve from '@rollup/plugin-node-resolve'; 8 | 9 | const prod = (process.argv[4] === 'production'); 10 | 11 | export default defineConfig(({mode}) => { 12 | return { 13 | plugins: [ 14 | svelte({ 15 | preprocess: autoPreprocess() 16 | }) 17 | ], 18 | build: { 19 | sourcemap: mode === 'development' ? 'inline' : false, 20 | minify: mode !== 'development', 21 | // Use Vite lib mode https://vitejs.dev/guide/build.html#library-mode 22 | lib: { 23 | entry: path.resolve(__dirname, './src/dailyNoteViewIndex.ts'), 24 | formats: ['cjs'], 25 | }, 26 | rollupOptions: { 27 | plugins: [ 28 | mode === 'development' 29 | ? '' 30 | : terser({ 31 | compress: { 32 | defaults: false, 33 | drop_console: ['log', 'info'], 34 | }, 35 | mangle: { 36 | eval: true, 37 | module: true, 38 | toplevel: true, 39 | safari10: true, 40 | properties: false, 41 | }, 42 | output: { 43 | comments: false, 44 | ecma: '2020', 45 | }, 46 | }), 47 | resolve({ 48 | browser: false, 49 | }), 50 | replace({ 51 | preventAssignment: true, 52 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 53 | }), 54 | ], 55 | output: { 56 | // Overwrite default Vite output fileName 57 | entryFileNames: 'main.js', 58 | assetFileNames: 'styles.css', 59 | }, 60 | external: [ 61 | 'obsidian', 62 | 'electron', 63 | '@codemirror/autocomplete', 64 | '@codemirror/collab', 65 | '@codemirror/commands', 66 | '@codemirror/language', 67 | '@codemirror/lint', 68 | '@codemirror/search', 69 | '@codemirror/state', 70 | '@codemirror/view', 71 | '@lezer/common', 72 | '@lezer/highlight', 73 | '@lezer/lr', 74 | ], 75 | }, 76 | // Use root as the output dir 77 | emptyOutDir: false, 78 | outDir: '.', 79 | }, 80 | }; 81 | }); 82 | --------------------------------------------------------------------------------