├── .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 |
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 |
276 |
277 | Create a daily note for today ✍
278 |
279 |
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 |
--------------------------------------------------------------------------------