├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .npmrc
├── LICENSE
├── README.md
├── esbuild.config.mjs
├── manifest.json
├── others
├── demo.gif
├── demo.mp4
├── iconchange.png
├── pn.png
└── screenshot.png
├── package.json
├── src
├── constructDOM.ts
├── createAndOpenDailyNote.ts
├── createAndOpenPeriodicNote.ts
├── drawUI.ts
├── getOutline.ts
├── getTargetFiles.ts
├── main.ts
├── modalExtract.ts
├── modalJump.ts
├── periodicNotesTypes.ts
├── setting.ts
├── util.ts
└── view.ts
├── styles.css
├── tsconfig.json
├── version-bump.mjs
└── versions.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # top-most EditorConfig file
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | insert_final_newline = true
7 | indent_style = tab
8 | indent_size = 4
9 | tab_width = 4
10 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | npm node_modules
2 | build
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "env": { "node": true },
5 | "plugins": [
6 | "@typescript-eslint"
7 | ],
8 | "extends": [
9 | "eslint:recommended",
10 | "plugin:@typescript-eslint/eslint-recommended",
11 | "plugin:@typescript-eslint/recommended"
12 | ],
13 | "parserOptions": {
14 | "sourceType": "module"
15 | },
16 | "rules": {
17 | "no-unused-vars": "off",
18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
19 | "@typescript-eslint/ban-ts-comment": "off",
20 | "no-prototype-builtins": "off",
21 | "@typescript-eslint/no-empty-function": "off"
22 | }
23 | }
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | tag-version-prefix=""
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 iiz00
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Obsidian Daily Note Outline
2 | ページ後半に日本語のドキュメントがあります。
3 | Japanese documentation is located on the second half of this page.
4 |
5 | ## Introduction
6 | Daily notes are a good place to write down various little notes and thoughts. However, sometimes it can be difficult to find which daily note you wrote them in later.
7 | This plugin creates a custom view that displays the outlines of multiple daily notes at once. The outline can display not only headings, but also links, tags and list items.
8 |
9 | 
10 | 
11 |
12 |
13 |
14 | ## New Feature: Create Daily Notes for Unresolved Links
15 |
16 | In Daily Note Outline v1.5.0, a feature to create daily notes for unresolved links has been implemented.
17 |
18 | In short, here's how it works:
19 |
20 | If you're using the daily note format YYYY-MM-DD.md, and there's a link like `[[2024-01-01]]` in any note in your vault, but 2024-01-01.md doesn't exist, it will create a new 2024-01-01.md.
21 |
22 | - This feature is for detecting backlinks to daily notes.
23 |
24 | - For example, if you create a template `[[{date}]]` and assign a hotkey to it using the Hotkeys for Templates plugin by @Vinzent03, you can insert a link to the daily note for that day inside your notes. If you have the showBacklinks setting enabled in DNO, files that link to that date will be listed as an outline. If the corresponding daily note doesn't exist, that backlink won't show up in the outline. However, with this new feature, DNO will detect unresolved links to daily notes and create the corresponding daily notes, allowing all daily note links to be displayed in the outline.
25 |
26 | - Therefore, this feature is unnecessary for those who create daily notes every day.
27 |
28 | - Also, this feature is unnecessary for those who don't display backlinks in DNO.
29 |
30 | - There are two ways to use this feature:
31 |
32 | - Manual: Run the "Create daily notes for unresolved links" command from the command palette to detect unresolved links and create the corresponding daily notes.
33 |
34 | - Automatic: Turn on "Create daily notes corresponding to unresolved links at startup" in Settings -> Others, and the same command will run automatically when the view starts up.
35 |
36 | **Note 1**
37 | If you have a large number of links to non-existent daily notes, many daily notes may be created. (A notification to show progress will be shown if more than 5 DNs are created at once).
38 | Therefore, the first run may take a long time. Subsequent runs will be much quicker.
39 |
40 | **Note 2**
41 | If you're using the Daily Notes core plugin or the regular Periodic Notes plugin, links to past dates will create empty daily notes, while links to the current day or future dates will create daily notes with the template applied.
42 | However, if you're using the beta version of Periodic Notes (1.0.0-beta3), the template will also be applied when creating daily notes for past dates.
43 | This is because the beta version of Periodic Notes automatically applies the template when it detects that a daily note with zero content has been created.
44 |
45 |
46 | ## Getting started
47 | Install the plugin from the community plugin list.
48 | Make sure either Daily Note core plugin or Periodic Notes plugin is enabled.
49 | To display the outline, choose "Daily Note Outline: Open Outline" in the command pallete.
50 |
51 | ## How to use
52 | To change the date range to display, click on the left and right arrows.
53 | To return to the initial date range, click on the house icon.
54 | Click on the refresh icon to redraw the outline.
55 | Click on the gear icon to open setting (Right-clicking opens a context menu where you can quickly change several settings).
56 | Click on the calendar with plus icon to create/open today's daily note (you can create/open tomorrow's daily note by right-clicking).
57 | Click on each outline element to open its location.
58 | Push Ctrl key to preview.
59 | I recommend that you first set the display/hide settings for each outline element (headings, links, tags, and list items) in the settings.
60 |
61 | ## Feature
62 | ### Support for Periodic Notes plugin
63 |
64 | Daily Note Outline v1.0.0- supports for periodic notes. That is, support for weekly/monthly/quarterly/yearly notes and calendar sets.
65 | (For more information on periodic notes, see https://github.com/liamcain/obsidian-periodic-notes)
66 | To display periodic notes in DNO, the following steps are required.
67 | - Install and activate Periodic Notes community plugin and properly configure the granularity and folder paths to be used.
68 | - Activate "periodic notes" and "calendar sets" in the "Periodic Notes" section of the DNO settings.
69 |
70 | #### Usage
71 | - After activating the feature, the current granularity (day/week/month/quarter/year) and calendar set name will be displayed on DNO's view.
72 | - Left-clicking on them will toggle the granularity or calendar set to display in sequence. Right-clicking on them will show a list and allow you to select one.
73 |
74 | 
75 |
76 |
77 | **Notes**
78 | - Calendar sets is a feature added in Periodic Notes v1.0.0-beta version and is not available in v0.0.17. To use the calendar set feature, you must install the beta version of Periodic Notes, e.g., by using Obsidian BRAT plugin.
79 | - Also, since calendar sets is a beta feature of Periodic Notes, there is a possibility that it may not be available in DNO in the future due to specification changes in Periodic Notes.
80 | - If you find any problems, please let me know at the forum or GitHub repository.
81 |
82 | ## Display backlink files
83 |
84 | Daily Note Outline v1.4.0 adds displaying backlink files.(Settings -> Basics -> Show backlink files)
85 | In my use case, I have created a template file like `[[{{date}}]]` and assigned a hotkey with the Hotkeys for templates plugin by @Vinzent03 (https://github.com/Vinzent03/obsidian-hotkeys-for-templates) to insert a link to the daily note for that day.
86 | Users who create daily notes every day will find this convenient, as they can see the note with the date inserted from DNO view.
87 |
88 | **Notes**
89 | From a speed perspective, I strongly recommend installing Dataview plugin when turning this feature on.
90 | The Dataview plugin caches the vault's backlink information, and DNO attempts to use its cache when Dataview plugin is activated. In this case, the backlink information can be retrieved significantly faster than without Dataview.
91 |
92 | ### Simple filter / Include / Exclude
93 |
94 | In order to hide unnecessary items and display only the necessary ones, three types of filter functions are implemented: **Simple filter**, **Include**, and **Exclude**.
95 |
96 | **Simple filter** simply hides items that match a specified word or phrase. The hierarchy of items is not taken into account.
97 | **Include** can be applied to only one type of outline element. It treats the range from the outline element of the specified type to the next similar element as a block, and only items matching the specified word or phrase and belonging to that block are displayed.
98 | Conversely, **Exclude** hides matching items and their blocks. If you specify an element type in the "excluding ends at" section of the settings, or if Include is enabled, the block is considered to have ended at that element, and only that part of the block is hidden.
99 |
100 | **Include** and **Exclude** can be used at the same time. (However, it does not make sense to specify the exclude keyword for an element type that is specified in Include.)
101 | **Simple filter** can be used in conjunction with other filters. For example, if you specify the same keywords as those specified for Include, you can display only the elements that belong to the elements matched the include keywords, not the elements themselves.
102 |
103 | ### Extract
104 | You can extract only outline elements that contain a specific words. There are three ways to do this.
105 |
106 | 1. Click on the magnifying glass button and type in the string you want to extract.
107 | 2. Right-click on the outline element and select 'extract' from the context menu. Only elements containing the same name will be extracted.
108 | 3. Right-click on the magnifying glass button and choose 'extract tasks'. Only list items that have checkbox will be extracted.
109 | Items that have been hidden by filtering or other means will not be displayed.
110 | To cancel the extraction, click the X button (the magnifying glass button will change to an X button).
111 |
112 | ## Settings
113 | Some child items are not initially displayed and will appear when the parent item is turned on.
114 |
115 | ### Basics
116 | #### Initial search type
117 | - backward(default)
118 | - Displays the past daily notes for the specified number of days starting from today.
119 | - forward
120 | - Displays daily notes for the specified number of days starting from the date specified in Onset date.
121 |
122 | #### Include future daily notes
123 | When backward search is used, daily notes of the specified number of days in the future are also displayed (If you set it long enough, you can also use this plugin as a list of upcoming events!).
124 |
125 | #### Onset date
126 | For forward search, specify the date in YYYY-MM-DD format to start the search at startup.
127 | Clicking on the date range under UI buttons jumps to the date.
128 |
129 | #### Search duration
130 | Specify the number of days to be explored per page. It is recommended to set a shorter period for those who use Daily notes every day and a longer period for those who use it only occasionally. I would recommend about 7-56 days.
131 |
132 | #### Show headings / links / tags / list items & tasks
133 | Choose whether each element should be displayed in outline.
134 |
135 | #### Show all root list items
136 | With respect to list item, if this setting is off, it shows only the first item in a continuous list. When turned on, it displays all list items at root level.
137 |
138 | #### Show all tasks / Tasks only / Hide completed tasks
139 | Display setting for tasks (list items including checkboxes).
140 | Show all tasks will show all levels of tasks, regardless of the list settings above; Task only will hide all list items except for tasks; Hide completed tasks will hide completed tasks.
141 |
142 | #### Display file information
143 | Display file information to the right of each daily note file name.
144 | lines: number of lines in the file
145 | distance: how far away the period is from the base date (today for backward search, the date specified in Onset date for forward search)
146 |
147 | #### Position of the plugin view
148 | Specify where to display the plugin's view when redisplaying or updating.
149 | (You can specify other than the left and right sidebars, but the behavior may not be as sophisticated.)
150 |
151 | ### Periodic Notes
152 | This section will not appear unless the Periodic Notes community plugin is activated.
153 |
154 | #### calendar sets
155 | Turn on if you want to use the calendar set in the Periodic Notes plugin. (Periodic Notes beta version must be installed)
156 |
157 | #### periodic notes
158 | Turn on if you want to use periodic notes.
159 |
160 | #### Search duration for periodic notes
161 | Specify the search duration per page for each periodic note.
162 |
163 | #### attach date range to weekly notes
164 | Displays the corresponding date range next to the file name of the weekly note.
165 |
166 | ### Headings
167 | #### Heading level to display
168 | Choose whether each level of headings should be displayed in outline.
169 |
170 | ### Preview
171 | #### Inline Preview
172 | Show a few subsequent words next to each outline elements.
173 |
174 | #### Tooltip Preview
175 | Show subsequent sentences as a tooltip with mouse hover.
176 |
177 | #### Tooltip Preview direction
178 | Specify the direction to display the tooltip preview.
179 | (I couldn't find the way to automatically determine appropriate direction...)
180 |
181 | ### Simple filter
182 | #### Headings/Links/Tags/List items to ignore
183 | If each outline element contains a specified word or phrase, that outline element will not be displayed.
184 | Specify one per line.
185 |
186 | ### Include
187 | #### Element type for include
188 | Specifies the type of outline element to apply the Include filter to.
189 |
190 | #### Words to include
191 | Only outline elements containing the specified word or phrase and the block following it will be displayed.
192 | Each should be separated by a new line.
193 |
194 | #### Include the beginning part
195 | Specifies whether the beginning part of a note that precedes the appearance of the outline element specified in Element type for include is to be displayed.
196 |
197 | ### Exclude
198 | #### Exclusion end at
199 | If any of the outline element types is specified in this field, the area excluded by Exclude filter is terminated at the specified element.
200 | If any type is specified in Include filter, Exclude is terminated at that type and this field is ignored.
201 |
202 | #### Headings/ Links/ Tags/ List items to exclude
203 | The outline element containing the specified word or phrase and the area following it will be hidden.
204 | Each should be separated by a new line.
205 |
206 | ### Appearance
207 | Sets the appearance of each outline element when it is displayed.
208 | An icon and prefix string can be added to each element.
209 | If you choose 'custom' for icon, enter icon name of the Lucide icon (https://lucide.dev/ ) in 'Custom icon' field. Some icons do not seem to be able to be displayed.
210 |
211 | #### Indent other than headings
212 | When you choose 'follow preceding headings', other elements are indented according to the preceding heading level.
213 |
214 | #### Headings: Repeat heading prefix
215 | If you enter a prefix for headings, turning this item on will repeat the prefix for the number of levels of headings.
216 |
217 | #### Headings: Add indent
218 | Indentation is added according to the level of the heading.
219 |
220 | #### Tasks: Add checkbox text to prefix
221 | Append a string indicating the status of the checkbox to the end of the task prefix.
222 |
223 | ### Others
224 | #### Show only .md files / exactly matched files
225 | If you are using the beta version of the Periodic Notes plugin, a wider range of file names will be recognized as periodic notes. Each of these options hides files that are not md files or files that do not match the format.
226 |
227 | ### Debug
228 | #### show debug information
229 | If turned on, some debug infromation is displayed in the console.
230 |
231 | ## Acknowledgement
232 | In developing this plugin, I have use many great plugins in Obsidian community as references. In particular,
233 | [Daily note interface by @liamcain](https://github.com/liamcain/obsidian-daily-notes-interface) for processing daily notes.
234 | [Spaced Repetition by @st3v3nmw](https://github.com/st3v3nmw/obsidian-spaced-repetition) and [Recent Files by @tgrosinger](https://github.com/tgrosinger/recent-files-obsidian) for creating custom views.
235 | The code for creating a new periodic note is partially modified from [Periodic Notes by @liamcain](https://github.com/liamcain/obsidian-periodic-notes) (MIT License).
236 | I also searched and referred to a bunch of posts in plugin-dev channel on Discord.
237 |
238 | ## Buy Me A Coffee
239 | If you like my plugin, I would appreciate it if you could buy me a cup of coffee!
240 | [](https://www.buymeacoffee.com/iiz00)
241 |
242 | ## (want) to do
243 | - collapse a note
244 | - note refactoring
245 | - show linked mentions / created / modefied files on each day (feasible in terms of performance?)
246 | - derivative plugin for other than daily notes
247 |
248 | ### done
249 | - support for periodic notes
250 | - show number of lines of each note
251 | - show the first section if no outline element exists
252 | - UI button for change settings
253 | - simple filter / include /exclude / extract
254 | - partially
255 | - better preview
256 |
257 | ## Changelog
258 | - 1.5.0
259 | - New Features
260 | - Support for List Callouts plugin and Time list
261 | - List items marked by the List Callouts plugin by @mgmeyers are displayed with coloring.
262 | - List items with timestamps (`- HH:mm text`), used in Thino by @Boninall and some scripts, can now be assigned a distinct icon as Time List.
263 | - List callouts and Time lists can be extracted from the context menu of the extract icon.
264 | - Date Jumping
265 | - The behavior of the date range displayed at the top of DNO view has been revamped.
266 | - Clicking on the date part will display a modal for entering a date, and you can jump to the entered date.
267 | - Right-clicking on the date part allows you to select a date candidate to jump to.
268 | - Added a feature to create daily notes if unresolved links to non-existent daily notes are present. (See description in README for details).
269 | - Outline element text can now be wrapped (Settings -> Appearance -> Wrap outline element text).
270 | - Changes
271 | - "Forward search" has been deprecated. If you want to display past dates on startup, enable Settings -> Set base date as home.
272 | - Improvements
273 | - Embedded links are now treated as link outline elements.
274 | - You can now change the icon for backlinks.
275 | - Tooltip preview is now displayed even for daily notes without outline elements.
276 | - Separated lists and tasks into different items in the settings screen for better intuition.
277 | - Added a delay time setting for startup(Settings -> Others -> Startup delay time). If daily notes fail to load on startup, increasing the delay time may resolve the issue.
278 | - Fixed
279 | - Fixed an issue in Reading view where clicking on an outline element would not jump to that position properly. Now the position of the outline element is highlighted when you jump to it.
280 | - 1.4.1
281 | - Improvement
282 | - Added `show links in properties` in the settings
283 | - Increased the maximum width of tooltip preview
284 | - Fixed
285 | - Fixed tooltip preview not being fixed correctly.
286 | - Fixed error sometimes displayed in console.
287 | - 1.4.0
288 | - New experimental function
289 | - Show backlink files
290 | - You can enable the display of the backlink file to daily notes through the settings (only for existing daily notes at this time).
291 | - For speed, I strongly recommend installing the dataview plugin if you enable this feature.
292 | - Improvements
293 | - The context menu of the link element now allows you to open the linked file in a new tab/new pane/new window.
294 | - You can now set the size of the pop-out window and whether it should be displayed in the foreground(always on top).
295 | - Fixed
296 | - Fixed tooltip preview was not working correctly due to recent (re)changes in Obsidian.
297 | - 1.3.1
298 | - Fixed
299 | - Fixed DNO view becomes active when starting Obsidian
300 | - 1.3.0
301 | - New function
302 | - Collapse notes
303 | - Click the '>' icon next to the note title to collapse/expand
304 | - Click on the '><(rotate)' icon at the top of DNO view to collapse/expand all notes
305 | - Improvements
306 | - Change background color of note titles (I recommend 'accent' for the default theme)
307 | - Custom status icons for tasks can now be displayed
308 | - The icons corresponding to custom statuses cannot be changed in the DNO settings screen, but can be changed by editing data.json directly.
309 | - Changes
310 | - The update button was removed and moved to the context menu of the home button as it is used infrequently.
311 |
312 | - 1.2.3
313 | - Improvement
314 | - When extraction is turned on, the icon color changes.
315 | - Fixed
316 | - Fixed Tooltip preview was not working properly due to recent Obsidian changes.
317 | - 1.2.2
318 | - Fixed
319 | - Changed extract function to be case-insensitive.
320 | - 1.2.1
321 | - Fixed
322 | - Fixed a broken appearance with changes in Obsidian v1.3.1 (insider build).
323 | - 1.2.0
324 | - Improvement
325 | - If you are using the beta version of the Periodic Notes plugin, a wider range of file names will be recognized as periodic notes. If you turn on `Settings -> others -> Show only exactly matched files`, you will see only files that exactly match the format.
326 | - 1.1.1
327 | - Fixed
328 | - Fixed a problem that sometimes caused non .md files to be displayed unintentionally (you can redisplay them from settings -> others -> show only .md files).
329 | - 1.1.0
330 | - Improvements
331 | - Several settings can now be changed from the context menu of the Settings button (right click on the gear icon).
332 | - Outline elements other than headings can now be indented according to the previous heading when specified in Settings->Appearance.
333 | - Day of the week, week number, and first tag have been added to the list of items that can be selected in Settings->Display file information.
334 | - Linked notes can now be opened from the context menu of link elements.
335 | - Fixed
336 | - Fixed: When using the Periodic Notes plugin v0.0.17, weekly notes would not be displayed in DNO if the first day of the week was set to Monday.
337 | - Added a setting to display debugging information in the console at the bottom of the settings.
338 | - At this time, this feature is intended to narrow down the cause of reports of inability to switch to weekly notes when using the beta version of the Periodic Notes plugin.
339 | - 1.0.0
340 | - New experimental function
341 | - Support for Periodic Notes plugin (display periodic notes, calendar sets)
342 | - You have to install and enable Periodic Notes community plugin in advance.
343 | - Improvement
344 | - Plugin setting changes are now automatically reflected in the view
345 | - Fixed
346 | - Fixed some outline parsing
347 | - 0.6.0
348 | - New functions
349 | - Now task(checkbox) is treated separately from list items
350 | - you can extract only tasks by right clicking Extract icon
351 | - Change the appearance of each element (Setting)
352 | - Improvements
353 | - From the context menu, you can open the outline element in a new tab, splitted pane, or popout window
354 | - In the settings, the dependent items are now hidden when the primary item is off.
355 | - Extraction modal accepts Enter key
356 | - Fixed
357 | - Fixed the extraction function failing in some situations
358 |
359 | - 0.5.0
360 | - Important fix
361 | - Fixed overload observed in mobile version under certain situation. Please let me know if the problem persists.
362 | - New function
363 | - Extract
364 | - you can extract outline elements including specific words
365 | 1. click magnifying glass UI button and input words to extract
366 | 2. right click on an outline element and choose 'extract' in the context menu. Then only elements with same name will be displayed.
367 | 3. To finish extract, click unextract UI button
368 | - Improvements
369 | - added a UI button to create/open today's daily note
370 | - right click on the button shows the context menu to create tomorrow's daily note
371 | - you can choose default position where this plugin's view appears from the settings.
372 |
373 | - 0.4.0
374 | - New functions
375 | - Include / Exclude
376 | - you can include or exclude some outline elements with belonging elements
377 | - Improvements
378 | - after an update, the plugin now automatically open its view (no need to reopen from command pallete)
379 | - 0.3.0
380 | - New functions
381 | - 2 new ways to preview
382 | - 1. Inline Preview
383 | - show a few subsequent words next to each outline element
384 | - 2. Tooltip Preview
385 | - show subsequent sentences as a tooltip with mouse hover
386 | - Improvements
387 | - added a UI button to open plugin setting
388 | - click on the date range to jump to Onset date(specified in the setting).
389 | - Fixed
390 | fixed long name items overflowing
391 | - 0.2.0
392 | - New functions
393 | - filtering outline element by word or phrase
394 | - display some file information to the right of file name
395 | - show the first line of the file if it has no outline element
396 | - Improvements
397 | - hover preview now shows proper location of each element
398 | - 0.1.1
399 | - Initial release.
400 |
401 |
402 |
403 |
404 | # Obsidian Daily Note Outline 日本語ドキュメント
405 |
406 | ## Introduction 概要
407 | デイリーノートはちょっとしたメモや雑多な考えを書き留めるのに便利です。しかし、後からどこに書いたのか探すのに苦労することがあります。
408 | このプラグインは、サイドペインに複数のデイリーノートのアウトラインを一括表示して、デイリーノートに書いた内容を把握しやすくするためのものです。見出しだけでなくリンク、タグ、リスト項目なども表示できます。
409 |
410 | 
411 | 
412 |
413 |
414 | ## 新機能: 未解決リンクに対するデイリーノートの作成 Create daily notes for unresolved links
415 |
416 | Daily Note Outline v1.5.0で、未解決リンクに対応するデイリーノートを作成する機能が実装されました。
417 | 端的に言うとこのような動作をします:
418 | もしあなたがYYYY-MM-DD.mdという形式のデイリーノートフォーマットを使用していたとして、
419 | 例えば`[[2024-01-01]]`というリンクがvault内のいずれかのノートにあるのに、
420 | 2024-01-01.mdが存在しない場合、新たに2024-01-01.mdを作成します。
421 |
422 | - この機能はデイリーノートへのバックリンクを検出するための物です。
423 | - 例えば`[[{date}]]` というテンプレートを作成し、Hotkeys for templatesプラグインby @Vinzent03でホットキーを割り当てると、ノート内にその日のデイリーノートへのリンクを張ることができます。このようなリンクが存在する場合、DNOでshowBacklinksの設定をオンにしていると、その日付にリンクしたファイルがアウトラインに一覧として表示されます。対応するデイリーノートが未作成だとそのバックリンクはアウトラインに表示されませんが、本機能でデイリーノートへの未解決リンクを検出し、対応するデイリーノートを作成することで、全てのデイリーノートへのリンクがアウトラインとして表示可能になります。
424 | - そのため、毎日デイリーノートを作成している人はこの機能は全く不要です。
425 | - また、DNOにおいてバックリンクの表示を行わない人にもこの機能は全く不要です。
426 | - この機能を利用するには2つの方法があります。
427 | - 手動:コマンドパレットから、Create daily notes for unresolved linksコマンドを実行すると、未解決リンクを検出して対応するデイリーノートを作ります。
428 | - 自動:設定->othersからCreate daily notes corresponding to unresolved links at startupをオンにすると、viewを起動するときに自動的に同じコマンドを実行します。
429 | - 注意1:実体のないデイリーノートへのリンクを多数作成している場合、多数のデイリーノートが作成される可能性があります。(1度に5個以上のDNを作成する場合は進捗が表示されます)。そのため、初回実行時のみ長い時間がかかる可能性があります。2回目以降にかかる時間は僅かです。
430 | - 注意2:デイリーノートコアプラグイン、または通常のPeriodic Notesプラグインを使用している場合、過去の日付に対するリンクについては空のデイリーノートを、当日または未来のデイリーノートへのリンクの場合はテンプレートを適用したデイリーノートを作成します。一方、Periodic Notesのベータ版(1.0.0-beta3)を使用している場合、過去の日付のデイリーノートを作成した場合もテンプレートが適用されます。これは、ベータ版のPeriodic Notesでは、容量0のデイリーノートが作成されたのを検出した場合、テンプレートを自動的に適用する処理が為されているからです。
431 |
432 |
433 | ## Getting started はじめ方
434 | 本プラグインをcommunityプラグインリストからインストールし、有効化して下さい。
435 | Daily NoteコアプラグインもしくはPeriodic Notesプラグインが有効になっていることを確かめて下さい。
436 | アウトラインが表示されていない場合は、コマンドパレットから、「Daily Note Outline: Open Outline」を実行して下さい。
437 |
438 | ## How to use 使い方
439 | 表示する日付の範囲を変更したいときは、左右の矢印をクリックして下さい。
440 | 家のアイコンをクリックすると初期設定の範囲に戻ります。
441 | 更新アイコンをクリックするとビューを再描画します。
442 | 歯車アイコンをクリックすると設定を開きます。右クリックでいくつかの項目を素早く切り替えられるコンテキストメニューを開きます。
443 | プラスマークのついたカレンダーアイコンをクリックすると今日のデイリーノートを作成するか開きます。右クリックから翌日のデイリーノートを開けます。
444 | 各アウトライン要素をクリックするとその場所を開きます。
445 | 各要素の上でCtrlキーを押すとホバープレビューを表示します。
446 | 使用にあたり、まず設定画面で各アウトライン要素(見出し、リンク、タグ、リスト項目)ごとに表示/非表示を設定することをお勧めします。
447 |
448 | ## Feature 機能
449 | ### Periodic Notes pluginのサポート
450 | Daily Note Outline v1.0.0以降では、periodic notesへ対応しています。即ち、weekly/monthly/quarterly/yearly note、およびカレンダーセットへの対応です。
451 | (periodic notesについては詳しくは https://github.com/liamcain/obsidian-periodic-notes を参照してください)
452 | periodic notesをDNOで表示するには、以下のステップが必要です。
453 | - Periodic Notes communityプラグインをインストール、有効化し、使用する粒度やフォルダパスなどの設定を適切に行う。
454 | - DNOの設定の「Periodic Notes」セクションにおいて、「periodic notes」や「calendar sets」を有効化する。
455 |
456 | #### 使用法
457 | - 機能を有効化すると、DNOのview上方に現在表示している粒度(day/week/month/quarter/year)やカレンダーセット名が表示されるようになります。
458 | - それらを左クリックすると表示される粒度やカレンダーセットが順次切り替わります。右クリックすると一覧が表示され、選択できます。
459 |
460 | 
461 |
462 |
463 | **注意点**
464 | - カレンダーセットはperiodic notes v1.0.0-beta版で追加された機能であり、v0.0.17でなく、obsidian BRATプラグインを利用するなどして、ベータ版のPeriodic Notesをインストールしないと利用できません。また、ベータ版の機能であることから、今後の仕様変更などに際して利用できなくなる可能性があります。
465 | - 不具合など合った場合は、forumやGitHubリポジトリまで是非お知らせ下さい。
466 |
467 | ### バックリンクファイルの表示
468 | Daily Note Outline v1.4.0では、バックリンクファイルの表示を追加しています。設定のBasics -> Show backlink filesを有効化してください。
469 | 私の場合、`[[{{date}}]]`というテンプレートファイルを作成し、Hotkeys for templatesプラグインby @Vinzent03(https://github.com/Vinzent03/obsidian-hotkeys-for-templates)でホットキーを割り当て、ノートに当日デイリーノートへのリンクを挿入できるようにしています。
470 | 毎日デイリーノートを作成している方には、日付が挿入したノートをDNOから参照できて便利かと思います。
471 |
472 | **注意点**
473 | - 動作速度の観点から、バックリンクの表示をオンにする場合、Dataviewプラグインをインストールすることを強くお勧めします。Dataviewプラグインはバックリンクファイルの情報をキャッシュしており、有効化されているとDNOはそのキャッシュ情報を利用するため、処理がずっと速くなります。
474 |
475 | ### Simple filter / Include / Exclude フィルター
476 | 不必要な項目を非表示にし、必要な項目のみ表示するために、simple filter, include, exclude の3つのフィルター機能を実装しています。
477 | simple filterは、指定した単語やフレーズにマッチする項目を、単純に非表示にします。項目ごとの階層は考慮されません。
478 | includeは、1種類のアウトライン要素のみに使えます。指定した種類のアウトライン要素から、次の同種要素までの間をひとつのブロックとして扱い、
479 | 指定した単語やフレーズにマッチする項目とそのブロックに属する項目のみを表示します。
480 | 逆に、excludeはマッチした項目とそのブロックのみを非表示にします。設定の「excluding ends at」のところで要素種別を指定するか、includeを有効にしていると、
481 | その要素のところでブロックが終了したと判断され、そこまでが非表示になります。
482 | includeとexcludeは同時に使用できます。(ただし、includeに指定した要素種別にexcludeキーワードを指定しても意味がありません。)
483 | simple filterは他と併用できます。例えば、includeに指定したものと同じキーワードを指定すると、includeの対象になった要素自体は表示せず、それに属する要素のみを表示できます。
484 |
485 | ### Extract 抽出
486 | 特定の文字列を含むアウトライン要素のみを抽出できます。方法は3つあります。
487 | 1.虫眼鏡ボタンをクリックし、抽出したい文字列を入力して下さい。
488 | 2.アウトライン要素を右クリックし、コンテキストメニューからextractを選択して下さい。同じ名前を含むアウトライン要素のみが抽出されます。
489 | 3.虫眼鏡ボタンを右クリックし、extract tasksを選択して下さい。タスク(チェックボックスを含むリストアイテム)のみが抽出されます。
490 |
491 | もともとfilterなどにより非表示になっている項目は表示されません。
492 | 抽出を解除するときは、×ボタン(虫眼鏡ボタンが×ボタンに変化します)をクリックして下さい。
493 |
494 | ## Settings 設定
495 | 一部の子項目は初期状態では表示されず、親項目がオンになったときに表示されます。
496 | ### Basics
497 | #### Initial search type
498 | - 今日の日付から後ろ向きに遡って表示するか、指定日から前向きに指定日数分表示するか選びます。
499 | - backward(default)
500 | - 今日を起点として指定した日数分の過去のデイリーノートを表示します。通常こちらで良いと思います。
501 | - forward
502 | - Onset dateで指定した日付を起点として、指定日数分のデイリーノートを表示します。
503 |
504 | #### Include future daily notes
505 | サーチタイプがbackward search のとき、指定した日数分未来のデイリーノートも表示します。長くすれば将来のイベントのリストとしても使えます!
506 |
507 | #### Onset date
508 | サーチタイプがforward search のとき起動時に探索開始する日付をYYYY-MM-DDの形式で指定します。
509 | また、UIボタンの下の日付範囲の表示をクリックしてもこの日にジャンプします。
510 |
511 | #### Search duration
512 | 1ページあたりに探索するデイリーノートの期間を日で指定します。デイリーノートを頻繁に使用する人は短く、たまにしか使わない人は長く設定するといいと思います。7日~56日くらいでしょうか。
513 |
514 | #### Show headings / links / tags / list items & tasks
515 | 見出し、リンク、タグ、リスト項目(タスク含む)のそれぞれの要素をアウトラインとして表示するかどうか指定します。
516 |
517 | #### Show all root list items
518 | リスト項目に関して、この設定がオフになっていると連続したリストの初めの項目だけを表示します。オンになっていると、ルートレベルの項目(=インデントされていない項目)を全て表示します。
519 |
520 | #### Show all tasks / Tasks only / Hide completed tasks
521 | タスク(チェックボックスを含むリストアイテム)についての表示設定です。Show all tasksをオンにすると、上のリスト設定にかかわらず、全ての階層のタスクを表示します。Task onlyをオンにするとタスク以外のリストが非表示になります。Hide completed tasksは完了済みタスクを非表示にします。
522 |
523 | #### Display file information
524 | ファイル名の右側に情報を表示します
525 | lines: ファイルの行数
526 | distance: 基準日からの日数(backward searchでは今日、forward searchではforward searchで指定した日付)や週数など。
527 |
528 | #### Position of the plugin view
529 | 再表示やアップデートの際に、どこにviewを表示するかを指定します。
530 | 左右のサイドバーが無難です。それ以外も指定できますが、動作は洗練されていないかもしれません。
531 |
532 | ### Periodic Notes
533 | このセクションはPeriodic Notes コミュニティプラグインが有効化されていないと表示されません。
534 |
535 | #### calendar sets
536 | Periodic Notesプラグインのカレンダーセットを利用する場合オンにしてください(Periodic Notes ベータ版のインストールが必要です)
537 |
538 | #### periodic notes
539 | periodic notesを利用する場合オンにしてください。
540 |
541 | #### Search duration for periodic notes
542 | それぞれのperiodic notesについて1ページあたりの探索期間を指定します。
543 |
544 | #### attach date range to weekly notes
545 | Weekly noteを表示しているとき、ファイル名の横に対応する日付の範囲を表示します。
546 |
547 |
548 | ### Headings
549 | #### Heading level to display
550 | 各レベルの見出しをアウトラインとして表示するかそれぞれ指定します。
551 |
552 | ### Preview
553 | #### Inline Preview
554 | アウトライン要素の右に、続く数単語をうっすら表示します。
555 |
556 | #### Tooltip Preview
557 | アウトライン要素にマウスカーソルを合わせると、続く文章をツールチップとして表示します。
558 |
559 | #### Tooltip Preview direction
560 | ツールチッププレビューを表示する方向を指定します(自動で振り分けたかったけどやり方が分かりませんでした…)
561 |
562 | ### Simple filter
563 | #### Headings/Links/Tags/List items to ignore
564 | 指定した単語やフレーズが含まれるアウトライン要素は非表示になります。それぞれ改行で区切って下さい。
565 |
566 | ### Include
567 | #### Element type for include
568 | 指定したアウトライン要素の種別がIncludeフィルターの対象になります。
569 |
570 | #### Words to include
571 | 指定した単語やフレーズが含まれるアウトライン要素と、それに続くブロックのみが表示対象となります。
572 | それぞれ改行で区切って下さい。
573 |
574 | #### Include the beginning part
575 | ノートの冒頭で、Element type for includeで指定したタイプのアウトライン要素が登場するより前の部分を表示対象とするかどうか指定します。
576 |
577 | ### Exclude
578 | #### Exclusion end at
579 | この項目でいずれかのアウトライン要素タイプを指定した場合、Excludeフィルターによって除外される領域が指定した要素のところで打ち切られます。
580 | Includeフィルターでいずれかのタイプが指定されていた場合、そのタイプのところでExcludeは打ち切られ、この項目は無視されます。
581 |
582 | #### Headings/ Links/ Tags/ List items to exclude
583 | 指定した単語やフレーズが含まれるアウトライン要素と、それに続く領域(同種のアウトライン要素、またはExclusion end atで指定した要素が出現する前までの領域)は非表示になります。
584 | それぞれ改行で区切って下さい。
585 |
586 | ### Appearance
587 | それぞれのアウトライン要素を表示する際の見た目を設定します。
588 | 各要素にはアイコンおよびprefix文字列を付加することができます。
589 | アイコンでcustomを選んだ場合、Lucide (https://lucide.dev/ )のアイコン名を入力して下さい。一部のアイコンは表示できないようです。
590 |
591 | #### Indent other than headings
592 | 設定すると、見出し以外のアウトライン要素が直前の見出しと同じだけ(またはさらに1段階多く)インデントされます。
593 |
594 | #### Headings: Repeat heading prefix
595 | headingsのprefixを入力した場合、この項目をオンにすると、見出しのレベルの数だけprefixが繰り返されます。
596 |
597 | #### Headings: Add indent
598 | 見出しのレベルに応じてインデントを付加します。
599 |
600 | #### Tasks: Add checkbox text to prefix
601 | タスクのprefixの最後にcheckboxの状態を示す文字列を付加します。
602 |
603 | ### Others
604 | #### Show only .md files / exactly matched files
605 | Periodic Notesプラグインのベータ版を使用している場合、より幅広いファイル名がperiodic notesとして認識されます。これらのオプションはそれぞれmdファイル以外、フォーマットに合致しないファイルを非表示にします。
606 |
607 | ### Debug
608 | #### show debug information
609 | オンにするとデバッグのためのいくつかの情報をconsoleに表示します。
610 |
611 | ## Acknowledgement 謝辞
612 | 本プラグインの作成にあたり、多くの素晴らしいObsidianのプラグインを参考にさせて頂きました。特に、
613 | デイリーノートの処理にdaily note interface by @liamcain を使わせて頂きました。
614 | カスタムビューの作成にSpaced Repetition by st3v3nmwとRecent files by tgrosingerを大いに参考にさせて頂きました。
615 | Periodic noteの新規作成機能に関しては、[Periodic Notes by @liamcain](https://github.com/liamcain/obsidian-periodic-notes) (MITライセンス)のコードを一部改変して使用しています。
616 | また、discordの plugin-devの書き込みを多数参考にさせて頂きました。
617 |
618 | ## Buy Me A Coffee
619 | もしプラグインを気に入ったら、コーヒーをおごっていただけると嬉しいです!
620 | [](https://www.buymeacoffee.com/iiz00)
621 |
--------------------------------------------------------------------------------
/esbuild.config.mjs:
--------------------------------------------------------------------------------
1 | import esbuild from "esbuild";
2 | import process from "process";
3 | import builtins from 'builtin-modules'
4 |
5 | const banner =
6 | `/*
7 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
8 | if you want to view the source, please visit the github repository of this plugin
9 | */
10 | `;
11 |
12 | const prod = (process.argv[2] === 'production');
13 |
14 | esbuild.build({
15 | banner: {
16 | js: banner,
17 | },
18 | entryPoints: ['src/main.ts'],
19 | bundle: true,
20 | external: [
21 | 'obsidian',
22 | 'electron',
23 | '@codemirror/autocomplete',
24 | '@codemirror/closebrackets',
25 | '@codemirror/collab',
26 | '@codemirror/commands',
27 | '@codemirror/comment',
28 | '@codemirror/fold',
29 | '@codemirror/gutter',
30 | '@codemirror/highlight',
31 | '@codemirror/history',
32 | '@codemirror/language',
33 | '@codemirror/lint',
34 | '@codemirror/matchbrackets',
35 | '@codemirror/panel',
36 | '@codemirror/rangeset',
37 | '@codemirror/rectangular-selection',
38 | '@codemirror/search',
39 | '@codemirror/state',
40 | '@codemirror/stream-parser',
41 | '@codemirror/text',
42 | '@codemirror/tooltip',
43 | '@codemirror/view',
44 | ...builtins],
45 | format: 'cjs',
46 | watch: !prod,
47 | target: 'es2016',
48 | logLevel: "info",
49 | sourcemap: prod ? false : 'inline',
50 | treeShaking: true,
51 | outfile: 'main.js',
52 | }).catch(() => process.exit(1));
53 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "obsidian-daily-note-outline",
3 | "name": "Daily Note Outline",
4 | "version": "1.5.0",
5 | "minAppVersion": "0.15.0",
6 | "description": "Add a custom view which shows outline of multiple daily notes with headings, links, tags and list items",
7 | "author": "iiz",
8 | "authorUrl": "https://github.com/iiz00",
9 | "isDesktopOnly": false
10 | }
11 |
--------------------------------------------------------------------------------
/others/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iiz00/obsidian-daily-note-outline/d71c17a794d1d75548baa6d17d5d77da2ddb4657/others/demo.gif
--------------------------------------------------------------------------------
/others/demo.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iiz00/obsidian-daily-note-outline/d71c17a794d1d75548baa6d17d5d77da2ddb4657/others/demo.mp4
--------------------------------------------------------------------------------
/others/iconchange.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iiz00/obsidian-daily-note-outline/d71c17a794d1d75548baa6d17d5d77da2ddb4657/others/iconchange.png
--------------------------------------------------------------------------------
/others/pn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iiz00/obsidian-daily-note-outline/d71c17a794d1d75548baa6d17d5d77da2ddb4657/others/pn.png
--------------------------------------------------------------------------------
/others/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iiz00/obsidian-daily-note-outline/d71c17a794d1d75548baa6d17d5d77da2ddb4657/others/screenshot.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "obsidian-daily-note-outline",
3 | "version": "1.5.0",
4 | "description": "Add a custom view which shows outline of multiple daily notes with headings, links, tags and list items",
5 | "main": "main.js",
6 | "scripts": {
7 | "dev": "node esbuild.config.mjs",
8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
9 | "version": "node version-bump.mjs && git add manifest.json versions.json"
10 | },
11 | "keywords": [],
12 | "author": "iiz",
13 | "license": "MIT",
14 | "devDependencies": {
15 | "@types/node": "^16.11.6",
16 | "@typescript-eslint/eslint-plugin": "^5.2.0",
17 | "@typescript-eslint/parser": "^5.2.0",
18 | "builtin-modules": "^3.2.0",
19 | "esbuild": "0.13.12",
20 | "obsidian": "latest",
21 | "obsidian-dataview": "^0.5.64",
22 | "tslib": "2.3.1",
23 | "typescript": "4.4.4"
24 | },
25 | "dependencies": {
26 | "obsidian-daily-notes-interface": "^0.9.4"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/constructDOM.ts:
--------------------------------------------------------------------------------
1 | import { App, MarkdownView, Menu, TFile, parseLinktext, setIcon, setTooltip } from "obsidian";
2 | import { FileInfo, OutlineData, GRANULARITY_LIST } from "./main";
3 | import { addFlag, checkFlag, checkListCallouts, getSubpathPosition, removeFlag, shouldDisplayListItem } from "./util";
4 | import { DailyNoteOutlineViewType } from "./view";
5 |
6 | // アウトライン描画
7 | export function drawOutline( files: TFile[], info: FileInfo[], data: OutlineData[][]):void {
8 |
9 | const containerEl: HTMLElement = createDiv("nav-files-container node-insert-event");
10 | const rootEl: HTMLElement = containerEl.createDiv("tree-item nav-folder mod-root");
11 | const rootChildrenEl: HTMLElement = rootEl.createDiv("tree-item-children nav-folder-children");
12 |
13 | if (files.length == 0){
14 | rootChildrenEl.createEl("h4",{
15 | text: "Daily/periodic note loading error(try clicking on the Home icon)"
16 | });
17 | }
18 |
19 | // include only modeがオンになっているか
20 | let includeMode: boolean = (this.settings.includeOnly != 'none') && (Boolean(this.settings.wordsToInclude.length) || (this.settings.includeBeginning));
21 |
22 | // 表示オンになっている見出しレベルの最高値
23 | let maxLevel = this.settings.headingLevel.indexOf(true);
24 |
25 |
26 |
27 |
28 | for (let i=0; i {
79 | event.stopPropagation();
80 |
81 | // フォールドされている場合
82 | if (info[i].isFolded){
83 | // 個別フォールドフラグを除去
84 | if (!this.collapseAll) {
85 | if (checkFlag(files[i], 'fold', this.settings)){
86 | removeFlag(files[i], 'fold', this.settings);
87 | await this.plugin.saveSettings();
88 | }
89 | }
90 |
91 | // オープン処理
92 | dailyNoteEl.classList.remove('is-collapsed');
93 | noteCollapseIcon.classList.remove('is-collapsed');
94 | info[i].isFolded = false;
95 | dailyNoteChildrenEl.style.display = 'block';
96 |
97 | } else {
98 | // 開いている場合
99 |
100 | // 個別フォールドフラグを追加
101 | if (!this.collapseAll) {
102 | addFlag(files[i],'fold',this.settings);
103 | }
104 | await this.plugin.saveSettings();
105 |
106 | // フォールド処理
107 | dailyNoteEl.classList.add('is-collapsed');
108 | noteCollapseIcon.classList.add('is-collapsed');
109 | info[i].isFolded = true;
110 | dailyNoteChildrenEl.style.display = 'none';
111 | }
112 | })
113 |
114 |
115 | //weekly noteの場合、ファイル名に日付の範囲を付加表示
116 | let name = files[i].basename;
117 | if (this.settings.attachWeeklyNotesName && GRANULARITY_LIST[this.activeGranularity] =='week'){
118 | name = name + " (" +this.fileInfo[i].date.clone().startOf('week').format("MM/DD [-] ") + this.fileInfo[i].date.clone().endOf('week').format("MM/DD") +")";
119 | }
120 | dailyNoteTitleEl.createDiv("tree-item-inner nav-folder-title-content").setText(name);
121 |
122 | //ファイル名の後の情報を表示
123 | const infoToDisplay = (this.activeGranularity == 0 ) ? this.settings.displayFileInfoDaily : this.settings.displayFileInfoPeriodic;
124 |
125 | switch (infoToDisplay) {
126 | case 'lines':
127 | dailyNoteTitleEl.dataset.subinfo = info[i].numOfLines.toString();
128 | break;
129 | case 'days':
130 | let basedate = (this.settings.setBaseDateAsHome == false)? window.moment(): window.moment(this.settings.onset);
131 | dailyNoteTitleEl.dataset.subinfo = Math.abs(info[i].date.diff(basedate.startOf(GRANULARITY_LIST[this.activeGranularity]),GRANULARITY_LIST[this.activeGranularity])).toString();
132 | break;
133 | case 'dow':
134 | dailyNoteTitleEl.dataset.subinfo = info[i].date.format("dddd");
135 | break;
136 | case 'dowshort':
137 | dailyNoteTitleEl.dataset.subinfo = info[i].date.format("ddd");
138 | break;
139 | case 'weeknumber':
140 | dailyNoteTitleEl.dataset.subinfo = info[i].date.format("[w]ww");
141 | break;
142 | case 'tag':
143 | let firsttagIndex = data[i].findIndex( (element,index) =>
144 | data[i][index].typeOfElement =='tag');
145 | if (firsttagIndex >= 0){
146 | dailyNoteTitleEl.dataset.subinfo = data[i][firsttagIndex].displayText;
147 | }
148 | break;
149 | case 'none':
150 | break;
151 | default:
152 | break;
153 | }
154 |
155 | //ノートタイトルをクリックしたらそのファイルをopen
156 | dailyNoteTitleEl.addEventListener(
157 | "click",
158 | async(event: MouseEvent) => {
159 | await this.app.workspace.getLeaf().openFile(files[i]);
160 | },
161 | false
162 | );
163 |
164 | //hover preview
165 | dailyNoteTitleEl.addEventListener('mouseover', (event: MouseEvent) => {
166 | this.app.workspace.trigger('hover-link', {
167 | event,
168 | source: DailyNoteOutlineViewType,
169 | hoverParent: rootEl,
170 | targetEl: dailyNoteTitleEl,
171 | linktext: files[i].path,
172 | });
173 | });
174 | // コンテキストメニュー
175 | dailyNoteTitleEl.addEventListener(
176 | "contextmenu",
177 | (event: MouseEvent) => {
178 | const menu = new Menu();
179 |
180 | //新規タブに開く
181 | menu.addItem((item)=>
182 | item
183 | .setTitle("Open in new tab")
184 | .setIcon("file-plus")
185 | .onClick(()=> {
186 | event.preventDefault();
187 | this.app.workspace.getLeaf('tab').openFile(files[i]);
188 | })
189 | );
190 | //右に開く
191 | menu.addItem((item)=>
192 | item
193 | .setTitle("Open to the right")
194 | .setIcon("separator-vertical")
195 | .onClick(()=> {
196 | event.preventDefault();
197 | this.app.workspace.getLeaf('split').openFile(files[i]);
198 | })
199 | );
200 | //新規ウィンドウに開く
201 | menu.addItem((item)=>
202 | item
203 | .setTitle("Open in new window")
204 | .setIcon("scan")
205 | .onClick(async()=> {
206 | // await this.app.workspace.getLeaf('window').openFile(linkTarget);
207 | await this.app.workspace.openPopoutLeaf({size:{width:this.settings.popoutSize.width,height:this.settings.popoutSize.height}}).openFile(files[i]);
208 | if (this.settings.popoutAlwaysOnTop){
209 | setPopoutAlwaysOnTop();
210 | }
211 | })
212 | );
213 | menu.showAtMouseEvent(event);
214 | }
215 | );
216 |
217 | // constructOutlineDOM.call(this, files[i], info[i], data[i], dailyNoteChildrenEl);
218 |
219 | // include mode 用の変数を宣言
220 | let isIncluded = this.settings.includeBeginning;
221 | let includeModeHeadingLevel: number;
222 | // exclude mode 用変数
223 | let isExcluded = false;
224 | let excludeType: string;
225 | let excludeModeHeadingLevel: number;
226 | let primeType = this.settings.includeOnly == 'none' ? this.settings.primeElement : this.settings.includeOnly;
227 | // extract マッチする項目があったかどうか
228 | let isExtracted = false;
229 |
230 |
231 | // propertiesの処理
232 | if (this.settings.showPropertyLinks && info[i].frontmatterLinks){
233 | for (let j = 0; j < info[i].frontmatterLinks.length; j++){
234 |
235 | const linkTarget = this.app.metadataCache.getFirstLinkpathDest(parseLinktext(info[i].frontmatterLinks[j].link).path, files[i].path);
236 | if (!(linkTarget instanceof TFile)) {
237 | continue;
238 | }
239 | const linkSubpath = parseLinktext(info[i].frontmatterLinks[j].link).subpath;
240 | // 抽出 extract
241 | if (this.extractMode == true) {
242 | if (this.extractType == 'noraml' && info[i].frontmatterLinks[j].displayText.toLowerCase().includes(this.settings.wordsToExtract.toLowerCase())){
243 | isExtracted = true;
244 | } else {
245 | continue;
246 | }
247 | }
248 |
249 | const outlineEl: HTMLElement = dailyNoteChildrenEl.createDiv("tree-item nav-file");
250 | const outlineTitle: HTMLElement = outlineEl.createDiv("tree-item-self is-clickable nav-file-title");
251 | setIcon(outlineTitle,'link');
252 |
253 | outlineTitle.style.paddingLeft ='0.5em';
254 | outlineTitle.createDiv("tree-item-inner nav-file-title-content").setText(info[i].frontmatterLinks[j].displayText);
255 |
256 |
257 | //クリック時
258 | outlineTitle.addEventListener(
259 | "click",
260 | async (event: MouseEvent) => {
261 | await this.app.workspace.getLeaf().openFile(files[i]);
262 | },
263 | false
264 | );
265 |
266 | //hover preview
267 | outlineTitle.addEventListener('mouseover', (event: MouseEvent) => {
268 | if (linkTarget){
269 | //リンク情報にsubpath(見出しへのリンク)が含まれる場合、その位置を取得
270 | let posInfo = {};
271 | if(linkSubpath){
272 | const subpathPosition = getSubpathPosition(this.app, linkTarget, linkSubpath);
273 | if (subpathPosition?.start?.line){
274 | posInfo = { scroll: subpathPosition.start.line};
275 | }
276 | }
277 | this.app.workspace.trigger('hover-link', {
278 | event,
279 | source: DailyNoteOutlineViewType,
280 | hoverParent: rootEl, // rootEl→parentElにした
281 | targetEl: outlineTitle,
282 | linktext: linkTarget.path,
283 | state: posInfo
284 | });
285 | }
286 | });
287 |
288 | // contextmenu
289 | outlineTitle.addEventListener(
290 | "contextmenu",
291 | (event: MouseEvent) => {
292 | const menu = new Menu();
293 |
294 | //抽出 filter関連コメントアウト
295 | menu.addItem((item) =>
296 | item
297 | .setTitle("Extract")
298 | .setIcon("search")
299 | .onClick(async ()=>{
300 | this.plugin.settings.wordsToExtract = info[i].frontmatterLinks[j].displayText;
301 | await this.plugin.saveSettings();
302 | this.extractMode = true;
303 | this.extractType = 'normal';
304 | this.refreshView(false,false);
305 | })
306 | );
307 | menu.addSeparator();
308 |
309 |
310 | menu.addItem((item)=>
311 | item
312 | .setTitle("Open linked file")
313 | .setIcon("links-going-out")
314 | .onClick(async()=>{
315 | await this.app.workspace.getLeaf().openFile(linkTarget);
316 |
317 | if (linkSubpath){
318 | const subpathPosition = getSubpathPosition(this.app, linkTarget, linkSubpath);
319 | scrollToElement(subpathPosition.start?.line,0,this.app);
320 | }
321 | })
322 | );
323 | menu.addSeparator();
324 |
325 | //リンク先を新規タブに開く
326 | menu.addItem((item)=>
327 | item
328 | .setTitle("Open linked file in new tab")
329 | .setIcon("file-plus")
330 | .onClick(async()=> {
331 | await this.app.workspace.getLeaf('tab').openFile(linkTarget);
332 | if (linkSubpath){
333 | // linkSubpathがあるときはそこまでスクロール
334 | const subpathPosition = getSubpathPosition(this.app, linkTarget, linkSubpath);
335 | scrollToElement(subpathPosition.start?.line, 0, this.app);
336 | }
337 | })
338 | );
339 | //リンク先を右に開く
340 | menu.addItem((item)=>
341 | item
342 | .setTitle("Open linked file to the right")
343 | .setIcon("separator-vertical")
344 | .onClick(async()=> {
345 | await this.app.workspace.getLeaf('split').openFile(linkTarget);
346 | if (linkSubpath){
347 | const subpathPosition = getSubpathPosition(this.app, linkTarget, linkSubpath);
348 | scrollToElement(subpathPosition.start?.line, 0, this.app);
349 | }
350 | })
351 | );
352 | //リンク先を新規ウィンドウに開く
353 | menu.addItem((item)=>
354 | item
355 | .setTitle("Open linked file in new window")
356 | .setIcon("scan")
357 | .onClick(async()=> {
358 | // await this.app.workspace.getLeaf('window').openFile(linkTarget);
359 | await this.app.workspace.openPopoutLeaf({size:{width:this.settings.popoutSize.width,height:this.settings.popoutSize.height}}).openFile(linkTarget);
360 | if (linkSubpath){
361 | const subpathPosition = getSubpathPosition(this.app, linkTarget, linkSubpath);
362 | scrollToElement(subpathPosition.start?.line, 0, this.app);
363 | }
364 | if (this.settings.popoutAlwaysOnTop){
365 | setPopoutAlwaysOnTop();
366 | }
367 | })
368 | );
369 | menu.addSeparator();
370 |
371 | //新規タブに開く
372 | menu.addItem((item)=>
373 | item
374 | .setTitle("Open in new tab")
375 | .setIcon("file-plus")
376 | .onClick(async()=> {
377 | await this.app.workspace.getLeaf('tab').openFile(files[i]);
378 | })
379 | );
380 | //右に開く
381 | menu.addItem((item)=>
382 | item
383 | .setTitle("Open to the right")
384 | .setIcon("separator-vertical")
385 | .onClick(async()=> {
386 | await this.app.workspace.getLeaf('split').openFile(files[i]);
387 | })
388 | );
389 | //新規ウィンドウに開く
390 | menu.addItem((item)=>
391 | item
392 | .setTitle("Open in new window")
393 | .setIcon("scan")
394 | .onClick(async()=> {
395 | await this.app.workspace.getLeaf('window').openFile(files[i]);
396 | })
397 | );
398 | menu.showAtMouseEvent(event);
399 | }
400 | );
401 | }
402 | }
403 |
404 |
405 | //アウトライン要素の描画。data[i]が要素0ならスキップ
406 | //二重ループから抜けるためラベルelementloopをつけた
407 | if (data[i].length > 0){
408 |
409 | elementloop: for (let j=0; j includeModeHeadingLevel ){
421 | //下位見出しの場合は処理をスキップ
422 | } else {
423 | // 組み入れるワードにマッチするか判定
424 | isIncluded = false;
425 | for (const value of this.settings.wordsToInclude){
426 | if ( (value) && displayText.includes(value)){
427 | isIncluded = true;
428 | if (element == 'heading'){
429 | includeModeHeadingLevel = data[i][j].level;
430 | }
431 | }
432 | }
433 | }
434 | }
435 | if (!isIncluded){
436 | continue;
437 | }
438 |
439 | //// exclude mode
440 | if (!isExcluded || (isExcluded && (excludeType == element || primeType == element))){
441 | if (element == 'heading' && data[i][j].level > excludeModeHeadingLevel){
442 | // 下位見出しの場合は処理をスキップ
443 | } else {
444 | isExcluded = false;
445 | for (const value of this.settings.wordsToExclude[element]){
446 | if ( (value) && displayText.includes(value)){
447 | isExcluded = true;
448 | excludeType = element;
449 | if (element == 'heading'){
450 | excludeModeHeadingLevel = data[i][j].level;
451 | }
452 | }
453 | }
454 |
455 | }
456 | }
457 | if (isExcluded){
458 | continue;
459 | }
460 |
461 | //要素ごとの非表示判定 設定で非表示になっていればスキップ
462 | if (!data[i][j].task && this.settings.showElements[element] == false){
463 | continue;
464 | } else if (data[i][j].task && !this.settings.showElements.task){
465 | continue;
466 | }
467 | // simple filter 除外ワードにマッチすればスキップ
468 |
469 | for (const value of this.settings.wordsToIgnore[element]){
470 | if( (value) && displayText.includes(value)){
471 | continue elementloop;
472 | }
473 | }
474 |
475 | //// 抽出 extract
476 |
477 | if (this.extractMode == true) {
478 | if (this.extractType == 'normal' && displayText.toLowerCase().includes(this.settings.wordsToExtract.toLowerCase())){
479 | isExtracted = true;
480 | } else if (this.extractType == 'task' && data[i][j].task !== void 0){
481 | isExtracted = true;
482 | } else if (this.extractType == 'listCallout' && typeof data[i][j].listCallout == 'number') {
483 | isExtracted = true;
484 | } else if (this.extractType == 'time' && data[i][j].time){
485 | isExtracted = true;
486 | } else {
487 | continue;
488 | }
489 | }
490 |
491 | //// 要素種別ごとの処理
492 |
493 | // headings
494 | if (element == 'heading'){
495 | // 最新の見出しレベルを取得
496 | latestHeadingLevel = data[i][j].level;
497 | // 特定の見出しレベルが非表示の場合、該当すればスキップ
498 | if ( !this.settings.headingLevel[data[i][j].level - 1]){
499 | continue;
500 | }
501 | }
502 |
503 | // links
504 | if (element == 'link'){
505 | }
506 |
507 | // tags
508 | if (element == 'tag'){
509 | }
510 |
511 |
512 | // listItems
513 | let calloutsIndex = undefined;
514 | if (element == 'listItems'){
515 | if (shouldDisplayListItem(data[i][j], this.settings, calloutsIndex) == false){
516 | continue;
517 | }
518 | }
519 |
520 | //アウトライン要素部分作成
521 | const outlineEl: HTMLElement = dailyNoteChildrenEl
522 | .createDiv("tree-item nav-file");
523 | //中身を設定
524 | const outlineTitle: HTMLElement = outlineEl.createDiv("tree-item-self is-clickable nav-file-title");
525 |
526 |
527 | //アイコン icon
528 | switch(this.settings.icon[element]){
529 | case 'none':
530 | break;
531 | case 'headingwithnumber':
532 | setIcon(outlineTitle, `heading-${data[i][j].level}`);
533 | break;
534 | case 'custom':
535 | setIcon(outlineTitle, this.settings.customIcon[element]);
536 | break;
537 | default:
538 | setIcon(outlineTitle, this.settings.icon[element]);
539 | break;
540 | }
541 | // タスクだった場合アイコン上書き
542 | if (element =='listItems'){
543 | if (data[i][j].task !== void 0){
544 | if (data[i][j].task == 'x'){
545 | setIcon(outlineTitle, this.settings.icon.taskDone == 'custom' ?
546 | this.settings.customIcon.taskDone : this.settings.icon.taskDone);
547 | } else {
548 | setIcon(outlineTitle, this.settings.icon.task =='custom' ?
549 | this.settings.customIcon.task : this.settings.icon.task);
550 | }
551 | const customStatus = Object.keys(this.settings.taskIcon).find((customStatus)=> this.settings.taskIcon[customStatus].symbol === data[i][j].task);
552 | if (customStatus) {
553 | setIcon(outlineTitle, this.settings.taskIcon[customStatus].icon);
554 | }
555 | }
556 |
557 | //リストコールアウト,タイムリストへの対応(
558 | let calloutsIndex = data[i][j]?.listCallout;
559 | if (typeof calloutsIndex =='number' ) {
560 | outlineTitle.style.backgroundColor =`RGBA(${this.app.plugins.plugins['obsidian-list-callouts'].settings[calloutsIndex].color},0.15)`;
561 | if (this.app.plugins.plugins['obsidian-list-callouts'].settings[calloutsIndex].hasOwnProperty('icon') && data[i][j].task === void 0){
562 | setIcon(outlineTitle, this.app.plugins.plugins['obsidian-list-callouts'].settings[calloutsIndex].icon);
563 | displayText = displayText.replace(/^\S+\s/,'');
564 | }
565 | }
566 | if (data[i][j]?.time){
567 | if (data[i][j].task === void 0){
568 | setIcon(outlineTitle, this.settings.icon.time =='custom' ?
569 | this.settings.customIcon.time : this.settings.icon.time);
570 | }
571 | }
572 | }
573 |
574 | //prefix、インデント
575 | let prefix = this.settings.prefix[element];
576 | // 特定のprefixに対応
577 | if ( element == 'heading'){
578 | switch (this.settings.repeatHeadingPrefix){
579 | case 'level':
580 | prefix = prefix.repeat(data[i][j].level);
581 | break;
582 | case 'levelminus1':
583 | prefix = prefix.repeat(data[i][j].level - 1 );
584 | break;
585 | }
586 | }
587 | if (element == 'listItems' && data[i][j].time){
588 | prefix = this.settings.prefix.time;
589 | }
590 |
591 | let indent: number = 0.5;
592 | // 見出しのインデント
593 | if (element =='heading' && this.settings.indent.heading == true){
594 | indent = indent + (data[i][j].level - (maxLevel + 1))*1.5;
595 | }
596 | // 見出し以外のインデント
597 | if (element !='heading' && this.settings.indentFollowHeading){
598 | const additionalIndent = (latestHeadingLevel - (maxLevel + 1) + (this.settings.indentFollowHeading == 2 ? 1: 0))*1.5;
599 | indent = indent + (additionalIndent > 0 ? additionalIndent : 0);
600 | }
601 | // リンクが前のエレメントと同じ行だった場合インデント付加
602 | if (element =='link' && data[i][j].position.start.line == data[i][j-1]?.position.start.line){
603 | indent = indent + 1.5;
604 | }
605 |
606 | outlineTitle.style.paddingLeft = `${indent}em`;
607 |
608 |
609 | if (element =='listItems' && data[i][j].task !== void 0) {
610 | prefix = data[i][j].task == 'x' ?
611 | this.settings.prefix.taskDone : this.settings.prefix.task;
612 | if (this.settings.addCheckboxText){
613 | prefix = prefix + '['+data[i][j].task+'] ';
614 | }
615 | }
616 | displayText = this.stripMarkdownSymbol(displayText);
617 |
618 | const outlineTitleContent = outlineTitle.createDiv("tree-item-inner nav-file-title-content");
619 | outlineTitleContent.setText(prefix + displayText);
620 | // wrapLine trueなら折り返し設定
621 | if (this.settings.wrapLine){
622 | outlineTitleContent.classList.add('wrap-line');
623 | }
624 | // インラインプレビュー
625 | // リンクとタグは、アウトライン要素のあとに文字列が続く場合その行をプレビュー、そうでなければ次の行をプレビュー
626 | if (this.settings.inlinePreview) {
627 | let previewText: string ='';
628 |
629 | if ((element == 'link' || element == 'tag') && data[i][j].position.end.col < info[i].lines[ data[i][j].position.start.line ].length){
630 | previewText = info[i].lines[ data[i][j].position.start.line ].slice(data[i][j].position.end.col);
631 | } else {
632 | previewText = ( data[i][j].position.start.line < info[i].numOfLines -1 )?
633 | info[i].lines[ data[i][j].position.start.line + 1] : "";
634 | }
635 | outlineTitle.createDiv("nav-file-title-preview").setText(previewText);
636 | }
637 | // ツールチッププレビュー
638 | // その要素の行から次の要素の前までをプレビュー
639 | if (this.settings.tooltipPreview){
640 | let previewText2:string ='';
641 | // まず次の表示要素の引数を特定
642 | let endLine:number = info[i].numOfLines - 1; //初期値は文章末
643 | let k = j +1; // 現在のアウトライン引数+1からループ開始
644 | endpreviewloop: while (k< data[i].length) {
645 | //表示するエレメントタイプであれば行を取得してループを打ち切る
646 | if (this.settings.showElements[data[i][k].typeOfElement]){
647 | //ただし各種の実際には非表示となる条件を満たしていたら打ち切らない
648 | // リストの設定による非表示
649 | if (data[i][k].typeOfElement == 'listItems' &&
650 | ( data[i][k].level >=2 ||
651 | ((this.settings.allRootItems == false && data[i][k].level == 1) && (this.settings.allTasks == false || data[i][k].task === void 0)) ||
652 | (this.settings.taskOnly && data[i][k].task === void 0) ||
653 | (this.settings.hideCompletedTasks && data[i][k].task == 'x'))){
654 | k++;
655 | continue;
656 | // 見出しのレベルによる非表示
657 | } else if (data[i][k].typeOfElement == 'heading' &&
658 | this.settings.headingLevel[data[i][k].level - 1] == false){
659 | k++;
660 | continue;
661 | // simple filterによる非表示
662 | } else {
663 | for (const value of this.settings.wordsToIgnore[data[i][k].typeOfElement]){
664 | if( (value) && data[i][k].displayText.includes(value)){
665 | k++;
666 | continue endpreviewloop;
667 | }
668 | }
669 | endLine = data[i][k].position.start.line -1;
670 | break;
671 | }
672 | }
673 | k++;
674 | }
675 | for (let l = data[i][j].position.start.line; l <= endLine; l++){
676 | previewText2 = previewText2 + info[i].lines[l] +'\n';
677 | }
678 | // 空行を除去
679 | previewText2 = previewText2.replace(/\n$|\n(?=\n)/g,'');
680 | setTooltip(outlineTitle, previewText2, {classes:['DNO-preview']});
681 |
682 | outlineTitle.dataset.tooltipPosition = this.settings.tooltipPreviewDirection;
683 | outlineTitle.setAttribute('data-tooltip-delay','10');
684 | }
685 |
686 | outlineTitle.addEventListener(
687 | "click",
688 | async(event: MouseEvent) => {
689 | await this.app.workspace.getLeaf().openFile(files[i]);
690 | scrollToElement(data[i][j].position.start.line,data[i][j].position.start.col, this.app);
691 | },
692 | false
693 | );
694 |
695 | //hover preview
696 | outlineTitle.addEventListener('mouseover', (event: MouseEvent) => {
697 | this.app.workspace.trigger('hover-link', {
698 | event,
699 | source: DailyNoteOutlineViewType,
700 | hoverParent: rootEl,
701 | targetEl: outlineTitle,
702 | linktext: files[i].path,
703 | state:{scroll: data[i][j].position.start.line}
704 | });
705 | });
706 |
707 | // contextmenu
708 | outlineTitle.addEventListener(
709 | "contextmenu",
710 | (event: MouseEvent) => {
711 | const menu = new Menu();
712 | //抽出
713 | menu.addItem((item) =>
714 | item
715 | .setTitle("Extract")
716 | .setIcon("search")
717 | .onClick(async ()=>{
718 | this.plugin.settings.wordsToExtract = data[i][j].displayText;
719 | await this.plugin.saveSettings();
720 | this.extractMode = true;
721 | this.extractType = 'normal';
722 | this.refreshView(false,false,false);
723 | })
724 | );
725 | menu.addSeparator();
726 | // (リンクのみ)リンク先を直接開く
727 | if (element =='link'){
728 | menu.addItem((item)=>
729 | item
730 | .setTitle("Open linked file")
731 | .setIcon("links-going-out")
732 | .onClick(async()=>{
733 | await this.app.workspace.getLeaf().openFile(linkTarget);
734 | if (linkSubpath){
735 | const subpathPosition = getSubpathPosition(this.app, linkTarget, linkSubpath);
736 | scrollToElement(subpathPosition.start?.line,0,this.app);
737 | }
738 | })
739 | );
740 | menu.addSeparator();
741 | //リンク先を新規タブに開く
742 | menu.addItem((item)=>
743 | item
744 | .setTitle("Open linked file in new tab")
745 | .setIcon("file-plus")
746 | .onClick(async()=> {
747 | await this.app.workspace.getLeaf('tab').openFile(linkTarget);
748 | if (linkSubpath){
749 | const subpathPosition = getSubpathPosition(this.app, linkTarget, linkSubpath);
750 | scrollToElement(subpathPosition.start?.line, 0, this.app);
751 | }
752 | })
753 | );
754 | //リンク先を右に開く
755 | menu.addItem((item)=>
756 | item
757 | .setTitle("Open linked file to the right")
758 | .setIcon("separator-vertical")
759 | .onClick(async()=> {
760 | if (linkTarget != this.activeFile){
761 | this.holdUpdateOnce = true;
762 | }
763 | await this.app.workspace.getLeaf('split').openFile(linkTarget);
764 | if (linkSubpath){
765 | const subpathPosition = getSubpathPosition(this.app, linkTarget, linkSubpath);
766 | scrollToElement(subpathPosition.start?.line, 0, this.app);
767 | }
768 | })
769 | );
770 | //リンク先を新規ウィンドウに開く
771 | menu.addItem((item)=>
772 | item
773 | .setTitle("Open linked file in new window")
774 | .setIcon("scan")
775 | .onClick(async()=> {
776 | if (linkTarget != this.activeFile){
777 | this.holdUpdateOnce = true;
778 | }
779 | // await this.app.workspace.getLeaf('window').openFile(linkTarget);
780 | await this.app.workspace.openPopoutLeaf({size:{width:this.settings.popoutSize.width,height:this.settings.popoutSize.height}}).openFile(linkTarget);
781 | if (linkSubpath){
782 | const subpathPosition = getSubpathPosition(this.app, linkTarget, linkSubpath);
783 | scrollToElement(subpathPosition.start?.line, 0, this.app);
784 | }
785 | if (this.settings.popoutAlwaysOnTop){
786 | setPopoutAlwaysOnTop();
787 | }
788 | })
789 | );
790 | menu.addSeparator();
791 | }
792 |
793 | //新規タブに開く
794 | menu.addItem((item)=>
795 | item
796 | .setTitle("Open in new tab")
797 | .setIcon("file-plus")
798 | .onClick(async()=> {
799 | await this.app.workspace.getLeaf('tab').openFile(files[i]);
800 | this.scrollToElement(data[i][j].position.start.line, data[i][j].position.start.col);
801 |
802 | })
803 | );
804 | //右に開く
805 | menu.addItem((item)=>
806 | item
807 | .setTitle("Open to the right")
808 | .setIcon("separator-vertical")
809 | .onClick(async()=> {
810 | await this.app.workspace.getLeaf('split').openFile(files[i]);
811 | this.scrollToElement(data[i][j].position.start.line, data[i][j].position.start.col);
812 | })
813 | );
814 | //新規ウィンドウに開く
815 | menu.addItem((item)=>
816 | item
817 | .setTitle("Open in new window")
818 | .setIcon("scan")
819 | .onClick(async()=> {
820 | await this.app.workspace.getLeaf('window').openFile(files[i]);
821 | this.scrollToElement(data[i][j].position.start.line, data[i][j].position.start.col);
822 | })
823 | );
824 |
825 | menu.showAtMouseEvent(event);
826 | }
827 | );
828 |
829 | }
830 | } else {
831 | //要素0だったときの処理
832 | //各行をチェックし、空行でない初めの行を表示する(抽出モードでは行わない)
833 | if (this.extractMode == false){
834 | for (let j = 0; j < info[i].lines.length; j++){
835 |
836 | if (info[i].lines[j] == ""){
837 | continue;
838 | } else {
839 | const outlineEl: HTMLElement = dailyNoteChildrenEl
840 | .createDiv("tree-item nav-file");
841 | const outlineTitle: HTMLElement = outlineEl.createDiv("tree-item-self is-clickable nav-file-title");
842 | outlineTitle.createDiv("tree-item-inner nav-file-title-content").setText(info[i].lines[j]);
843 | outlineTitle.addEventListener(
844 | "click",
845 | async(event: MouseEvent) => {
846 | event.preventDefault();
847 | await this.app.workspace.getLeaf().openFile(files[i]);
848 | },
849 | false
850 | );
851 | // ツールチッププレビュー
852 | let previewText2: string = info[i].lines.join('\n');
853 | setTooltip(outlineTitle, previewText2, {classes:['DNO-preview']});
854 | outlineTitle.dataset.tooltipPosition = this.settings.tooltipPreviewDirection;
855 | outlineTitle.setAttribute('data-tooltip-delay','10');
856 | break;
857 | }
858 | }
859 | }
860 |
861 |
862 | }
863 | //backlink filesの処理
864 | if (!this.settings.getBacklinks || !this.settings.showBacklinks){
865 | continue;
866 | }
867 | for (let j = 0; j < info[i].backlinks?.length; j++){
868 |
869 | // 抽出 extract
870 | if (this.extractMode == true) {
871 | if (this.extractType == 'normal' && info[i].backlinks[j].basename.toLowerCase().includes(this.settings.wordsToExtract.toLowerCase())){
872 | isExtracted = true;
873 | } else {
874 | continue;
875 | }
876 | }
877 |
878 |
879 | const outlineEl: HTMLElement = dailyNoteChildrenEl.createDiv("tree-item nav-file");
880 | const outlineTitle: HTMLElement = outlineEl.createDiv("tree-item-self is-clickable nav-file-title");
881 |
882 | //アイコン icon
883 | switch(this.settings.icon.backlink){
884 | case 'none':
885 | break;
886 | case 'custom':
887 | setIcon(outlineTitle, this.settings.customIcon.backlink);
888 | break;
889 | default:
890 | setIcon(outlineTitle, this.settings.icon.backlink);
891 | break;
892 | }
893 |
894 | outlineTitle.style.paddingLeft ='0.5em';
895 | outlineTitle.createDiv("tree-item-inner nav-file-title-content").setText(info[i].backlinks[j].basename);
896 |
897 |
898 | //クリック時
899 | outlineTitle.addEventListener(
900 | "click",
901 | async(event: MouseEvent) => {
902 | await this.app.workspace.getLeaf().openFile(info[i].backlinks[j]);
903 | const view = this.app.workspace.getActiveViewOfType(MarkdownView);
904 | },
905 | false
906 | );
907 |
908 | //hover preview
909 | outlineTitle.addEventListener('mouseover', (event: MouseEvent) => {
910 | this.app.workspace.trigger('hover-link', {
911 | event,
912 | source: DailyNoteOutlineViewType,
913 | hoverParent: rootEl, // rootEl→parentElにした
914 | targetEl: outlineTitle,
915 | linktext: info[i].backlinks[j].path,
916 | });
917 | });
918 |
919 | // contextmenu
920 | outlineTitle.addEventListener(
921 | "contextmenu",
922 | (event: MouseEvent) => {
923 | const menu = new Menu();
924 | //リンク元を新規タブに開く
925 | menu.addItem((item)=>
926 | item
927 | .setTitle("Open backlink file in new tab")
928 | .setIcon("file-plus")
929 | .onClick(async()=> {
930 | await this.app.workspace.getLeaf('tab').openFile(info[i].backlinks[j]);
931 | })
932 | );
933 | //リンク先を右に開く
934 | menu.addItem((item)=>
935 | item
936 | .setTitle("Open linked file to the right")
937 | .setIcon("separator-vertical")
938 | .onClick(async()=> {
939 | await this.app.workspace.getLeaf('split').openFile(info[i].backlinks[j]);
940 | })
941 | );
942 | //リンク先を新規ウィンドウに開く
943 | menu.addItem((item)=>
944 | item
945 | .setTitle("Open linked file in new window")
946 | .setIcon("scan")
947 | .onClick(async()=> {
948 | // await this.app.workspace.getLeaf('window').openFile(info.backlinks[i]);
949 | await this.app.workspace.openPopoutLeaf({size:{width:this.settings.popoutSize.width,height:this.settings.popoutSize.height}}).openFile(info[i].backlinks[j]);
950 | if (this.settings.popoutAlwaysOnTop){
951 | setPopoutAlwaysOnTop();
952 | }
953 | })
954 | );
955 | menu.showAtMouseEvent(event);
956 | });
957 | }
958 |
959 |
960 | // noteDOMの後処理
961 |
962 | // 折りたたまれていれば子要素を非表示にする
963 | if (this.collapseAll|| this.settings.fileFlag?.[files[i].path]?.fold){
964 | dailyNoteEl.classList.add('is-collpased');
965 | noteCollapseIcon.classList.add('is-collapsed');
966 | info[i].isFolded = true;
967 | dailyNoteChildrenEl.style.display = 'none';
968 | } else {
969 | info[i].isFolded =false;
970 | }
971 |
972 |
973 | // 抽出モードで抽出した要素がない場合、ノート自体を非表示に。
974 | if (this.extractMode == true && isExtracted == false){
975 | dailyNoteEl.remove();
976 | }
977 | }
978 | // アウトライン部分の描画実行
979 | this.contentEl.appendChild(containerEl);
980 | }
981 |
982 |
983 | // スクロール
984 | export function scrollToElement(line: number, col: number, app: App): void {
985 | const view = app.workspace.getActiveViewOfType(MarkdownView);
986 | if (view) {
987 | view.setEphemeralState({line});
988 | }
989 | }
990 |
991 | function setPopoutAlwaysOnTop(){
992 | const { remote } = require('electron');
993 | const activeWindow = remote.BrowserWindow.getFocusedWindow();
994 | activeWindow.setAlwaysOnTop(true);
995 | }
--------------------------------------------------------------------------------
/src/createAndOpenDailyNote.ts:
--------------------------------------------------------------------------------
1 | import moment from "moment";
2 | import { TFile, WorkspaceLeaf } from 'obsidian';
3 | import { createDailyNote, getDailyNote, createPeriodicNote, IGranularity, getDateUID, createQuarterlyNote, createYearlyNote, getDailyNoteSettings} from "obsidian-daily-notes-interface";
4 |
5 |
6 | export async function createAndOpenDailyNote( granularity: IGranularity, date: moment, allFiles: Record): Promise {
7 | const { workspace } = window.app;
8 |
9 | const createFile = async () => {
10 | // createPeriodicNoteはquarter/yearには対応していないようなので分岐。
11 | let newNote:TFile;
12 | switch (granularity){
13 | case 'quarter':
14 | newNote = await createQuarterlyNote(date);
15 | break;
16 | case 'year':
17 | newNote = await createYearlyNote(date);
18 | break;
19 | default:
20 | newNote = await createPeriodicNote(granularity, date);
21 | }
22 | await workspace.getLeaf().openFile(newNote);
23 |
24 | };
25 | const dailynote = allFiles[getDateUID(date, granularity)];
26 | if (!dailynote){
27 | await createFile();
28 | } else {
29 | await workspace.getLeaf().openFile(dailynote);
30 | }
31 |
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/src/createAndOpenPeriodicNote.ts:
--------------------------------------------------------------------------------
1 | /*
2 | Codes in this file is based on Periodic Notes by Liam Cain https://github.com/liamcain/obsidian-periodic-notes
3 |
4 | The original work is MIT-licensed.
5 |
6 | MIT License
7 |
8 | Copyright (c) 2021 Liam Cain
9 |
10 | Permission is hereby granted, free of charge, to any person obtaining a copy
11 | of this software and associated documentation files (the "Software"), to deal
12 | in the Software without restriction, including without limitation the rights
13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | copies of the Software, and to permit persons to whom the Software is
15 | furnished to do so, subject to the following conditions:
16 |
17 | The above copyright notice and this permission notice shall be included in all
18 | copies or substantial portions of the Software.
19 |
20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | SOFTWARE.
27 | */
28 |
29 |
30 | import moment from "moment";
31 | import { App, TFile, WorkspaceLeaf, normalizePath, Notice } from 'obsidian';
32 | import { createPeriodicNote, getDailyNoteSettings, IGranularity } from "obsidian-daily-notes-interface";
33 |
34 | import { CalendarSet } from "./periodicNotesTypes";
35 |
36 | import { Granularity, PeriodicConfig } from "./periodicNotesTypes";
37 |
38 | const DEFAULT_NOTE_FORMAT = {
39 | day: "YYYY-MM-DD",
40 | week: "gggg-[W]ww",
41 | month: "YYYY-MM",
42 | quarter: "YYYY-[Q]Q",
43 | year: "YYYY"
44 | }
45 |
46 |
47 | export async function createAndOpenPeriodicNote( granularity: IGranularity, date: moment, calendarSet: CalendarSet): Promise {
48 | const { workspace } = window.app;
49 |
50 |
51 | let file = window.app.plugins.getPlugin("periodic-notes").cache.getPeriodicNote(
52 | calendarSet.id, granularity, date
53 | );
54 | if (!file) {
55 | // fileを作成
56 | const config = calendarSet[granularity];
57 | const format = calendarSet[granularity].format;
58 |
59 | const filename = date.format(format ? format : DEFAULT_NOTE_FORMAT[granularity]);
60 | const templateContents = await getTemplateContents(window.app, config.templatePath);
61 | const renderedContents = applyTemplateTransformations(
62 | filename,
63 | granularity as Granularity,
64 | date,
65 | format,
66 | templateContents
67 | );
68 | const destPath = await getNoteCreationPath(window.app, filename, config);
69 | file = await window.app.vault.create(destPath, renderedContents);
70 | }
71 |
72 | await workspace.getLeaf().openFile(file);
73 | }
74 |
75 |
76 | // Periodic Notes util.tsから
77 | async function getTemplateContents(
78 | app: App,
79 | templatePath: string | undefined
80 | ): Promise {
81 | const { metadataCache, vault } = app;
82 | const normalizedTemplatePath = normalizePath(templatePath ?? "");
83 | if (templatePath === "/") {
84 | return Promise.resolve("");
85 | }
86 |
87 | try {
88 | const templateFile = metadataCache.getFirstLinkpathDest(normalizedTemplatePath, "");
89 | return templateFile ? vault.cachedRead(templateFile) : "";
90 | } catch (err) {
91 | console.error(
92 | `Failed to read the daily note template '${normalizedTemplatePath}'`,
93 | err
94 | );
95 | new Notice("Failed to read the daily note template");
96 | return "";
97 | }
98 | }
99 |
100 | function applyTemplateTransformations(
101 | filename: string,
102 | granularity: Granularity,
103 | date: moment,
104 | format: string,
105 | rawTemplateContents: string
106 | ): string {
107 | let templateContents = rawTemplateContents;
108 |
109 | templateContents = rawTemplateContents
110 | .replace(/{{\s*date\s*}}/gi, filename)
111 | .replace(/{{\s*time\s*}}/gi, window.moment().format("HH:mm"))
112 | .replace(/{{\s*title\s*}}/gi, filename);
113 |
114 | if (granularity === "day") {
115 | templateContents = templateContents
116 | .replace(/{{\s*yesterday\s*}}/gi, date.clone().subtract(1, "day").format(format))
117 | .replace(/{{\s*tomorrow\s*}}/gi, date.clone().add(1, "d").format(format))
118 | .replace(
119 | /{{\s*(date|time)\s*(([+-]\d+)([yqmwdhs]))?\s*(:.+?)?}}/gi,
120 | (_, _timeOrDate, calc, timeDelta, unit, momentFormat) => {
121 | const now = window.moment();
122 | const currentDate = date.clone().set({
123 | hour: now.get("hour"),
124 | minute: now.get("minute"),
125 | second: now.get("second"),
126 | });
127 | if (calc) {
128 | currentDate.add(parseInt(timeDelta, 10), unit);
129 | }
130 |
131 | if (momentFormat) {
132 | return currentDate.format(momentFormat.substring(1).trim());
133 | }
134 | return currentDate.format(format);
135 | }
136 | );
137 | }
138 |
139 | if (granularity === "week") {
140 | templateContents = templateContents.replace(
141 | /{{\s*(sunday|monday|tuesday|wednesday|thursday|friday|saturday)\s*:(.*?)}}/gi,
142 | (_, dayOfWeek, momentFormat) => {
143 | const day = getDayOfWeekNumericalValue(dayOfWeek);
144 | return date.weekday(day).format(momentFormat.trim());
145 | }
146 | );
147 | }
148 |
149 | if (granularity === "month") {
150 | templateContents = templateContents.replace(
151 | /{{\s*(month)\s*(([+-]\d+)([yqmwdhs]))?\s*(:.+?)?}}/gi,
152 | (_, _timeOrDate, calc, timeDelta, unit, momentFormat) => {
153 | const now = window.moment();
154 | const monthStart = date
155 | .clone()
156 | .startOf("month")
157 | .set({
158 | hour: now.get("hour"),
159 | minute: now.get("minute"),
160 | second: now.get("second"),
161 | });
162 | if (calc) {
163 | monthStart.add(parseInt(timeDelta, 10), unit);
164 | }
165 |
166 | if (momentFormat) {
167 | return monthStart.format(momentFormat.substring(1).trim());
168 | }
169 | return monthStart.format(format);
170 | }
171 | );
172 | }
173 |
174 | if (granularity === "quarter") {
175 | templateContents = templateContents.replace(
176 | /{{\s*(quarter)\s*(([+-]\d+)([yqmwdhs]))?\s*(:.+?)?}}/gi,
177 | (_, _timeOrDate, calc, timeDelta, unit, momentFormat) => {
178 | const now = window.moment();
179 | const monthStart = date
180 | .clone()
181 | .startOf("quarter")
182 | .set({
183 | hour: now.get("hour"),
184 | minute: now.get("minute"),
185 | second: now.get("second"),
186 | });
187 | if (calc) {
188 | monthStart.add(parseInt(timeDelta, 10), unit);
189 | }
190 |
191 | if (momentFormat) {
192 | return monthStart.format(momentFormat.substring(1).trim());
193 | }
194 | return monthStart.format(format);
195 | }
196 | );
197 | }
198 |
199 | if (granularity === "year") {
200 | templateContents = templateContents.replace(
201 | /{{\s*(year)\s*(([+-]\d+)([yqmwdhs]))?\s*(:.+?)?}}/gi,
202 | (_, _timeOrDate, calc, timeDelta, unit, momentFormat) => {
203 | const now = window.moment();
204 | const monthStart = date
205 | .clone()
206 | .startOf("year")
207 | .set({
208 | hour: now.get("hour"),
209 | minute: now.get("minute"),
210 | second: now.get("second"),
211 | });
212 | if (calc) {
213 | monthStart.add(parseInt(timeDelta, 10), unit);
214 | }
215 |
216 | if (momentFormat) {
217 | return monthStart.format(momentFormat.substring(1).trim());
218 | }
219 | return monthStart.format(format);
220 | }
221 | );
222 | }
223 |
224 | return templateContents;
225 | }
226 |
227 |
228 | async function getNoteCreationPath(
229 | app: App,
230 | filename: string,
231 | periodicConfig: PeriodicConfig
232 | ): Promise {
233 | const directory = periodicConfig.folder ?? "";
234 | const filenameWithExt = !filename.endsWith(".md") ? `${filename}.md` : filename;
235 |
236 | const path = normalizePath(join(directory, filenameWithExt));
237 | await ensureFolderExists(app, path);
238 | return path;
239 | }
240 |
241 | // Credit: @creationix/path.js
242 | function join(...partSegments: string[]): string {
243 | // Split the inputs into a list of path commands.
244 | let parts: string[] = [];
245 | for (let i = 0, l = partSegments.length; i < l; i++) {
246 | parts = parts.concat(partSegments[i].split("/"));
247 | }
248 | // Interpret the path commands to get the new resolved path.
249 | const newParts = [];
250 | for (let i = 0, l = parts.length; i < l; i++) {
251 | const part = parts[i];
252 | // Remove leading and trailing slashes
253 | // Also remove "." segments
254 | if (!part || part === ".") continue;
255 | // Push new path segments.
256 | else newParts.push(part);
257 | }
258 | // Preserve the initial slash if there was one.
259 | if (parts[0] === "") newParts.unshift("");
260 | // Turn back into a single string path.
261 | return newParts.join("/");
262 | }
263 |
264 |
265 | async function ensureFolderExists(app: App, path: string): Promise {
266 | const dirs = path.replace(/\\/g, "/").split("/");
267 | dirs.pop(); // remove basename
268 |
269 | if (dirs.length) {
270 | const dir = join(...dirs);
271 | if (!app.vault.getAbstractFileByPath(dir)) {
272 | await app.vault.createFolder(dir);
273 | }
274 | }
275 | }
276 |
277 |
278 | function getDaysOfWeek(): string[] {
279 | const { moment } = window;
280 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
281 | let weekStart = (moment.localeData() as any)._week.dow;
282 | const daysOfWeek = [
283 | "sunday",
284 | "monday",
285 | "tuesday",
286 | "wednesday",
287 | "thursday",
288 | "friday",
289 | "saturday",
290 | ];
291 |
292 | while (weekStart) {
293 | const day = daysOfWeek.shift();
294 | if (day) daysOfWeek.push(day);
295 | weekStart--;
296 | }
297 | return daysOfWeek;
298 | }
299 |
300 | function getDayOfWeekNumericalValue(dayOfWeekName: string): number {
301 | return getDaysOfWeek().indexOf(dayOfWeekName.toLowerCase());
302 | }
303 |
304 |
305 | // The following code is based on Daily Note Interface Notes by Liam Cain
306 | // デイリーノートまたはPeriodic notes v0.xにおいて、指定した日付のデイリーノートを作成
307 | export async function createDailyNoteForUnresolvedLink(app: App, date: moment){
308 | if (date.isSameOrAfter(window.moment(),'day')){
309 | // 日付が本日か未来であればDailyNoteInterfaceから普通にデイリーノートを作成
310 | await createPeriodicNote('day', date);
311 | } else {
312 | // 日付が過去であればテンプレートを適用せずに空ノートをつくる
313 | const {format, folder} = getDailyNoteSettings();
314 | const filename = date.format(format);
315 | const filenameWithExt = !filename.endsWith(".md") ? `${filename}.md` : filename;
316 |
317 | const path = normalizePath(join(folder ?? "", filenameWithExt));
318 | await ensureFolderExists(app, path);
319 |
320 | try {
321 | await this.app.vault.create(path,"");
322 | } catch(e){
323 | console.error('Daily Note Outline: Could not create ',path, e.message);
324 | }
325 | }
326 |
327 | }
328 |
329 | // Periodic notes v1.xにおいて、指定した日付のデイリーノートを作成
330 | // *注意点*:この関数は過去の日付に対してはテンプレートを適用せず、空ノートを作ろうとするが、
331 | // Periodic notes 1.0.0-beta3では、デイリーノートのファイル名フォーマットに合致する新規ノートが作成されたのを検出した場合、
332 | // Periodic notes側でテンプレートを強制適用する処理がなされている。そのため、テンプレートが指定されていると空ノートにならない。
333 | export async function createDailyNoteForUnresolvedLinkPNbeta(app: App, date: moment, calendarSet: CalendarSet){
334 |
335 | const isTodayOrFuture = Boolean(date.isSameOrAfter(window.moment(),'day'));
336 | const config = calendarSet.day;
337 | const format = calendarSet.day.format;
338 |
339 | const filename = date.format(format ? format : DEFAULT_NOTE_FORMAT.day);
340 |
341 | let templateContents = "";
342 | let renderedContents = "";
343 | if (isTodayOrFuture){
344 | templateContents = await getTemplateContents(app, config.templatePath);
345 | renderedContents = applyTemplateTransformations(
346 | filename,
347 | 'day',
348 | date,
349 | format,
350 | templateContents
351 | );
352 | }
353 | const destPath = await getNoteCreationPath(app, filename, config);
354 | try {
355 | await this.app.vault.create(destPath, renderedContents);
356 | } catch(e) {
357 | console.error('Daily Note Outline: Could not create', destPath, e.message);
358 | }
359 | }
360 |
--------------------------------------------------------------------------------
/src/drawUI.ts:
--------------------------------------------------------------------------------
1 |
2 | import { setIcon, TFile, Menu, Notice } from 'obsidian';
3 | import { GRANULARITY_LIST, GRANULARITY_TO_PERIODICITY, FILEINFO_TO_DISPLAY, FILEINFO_TO_DISPLAY_DAY,DAYS_PER_UNIT, } from './main';
4 | import { createAndOpenDailyNote } from 'src/createAndOpenDailyNote';
5 | import { ModalExtract } from 'src/modalExtract';
6 | import {type CalendarSet,} from 'src/periodicNotesTypes';
7 | import { createAndOpenPeriodicNote } from './createAndOpenPeriodicNote';
8 | import { IGranularity } from 'obsidian-daily-notes-interface';
9 | import { ModalJump } from './modalJump';
10 | import { searchFirstNote } from './util';
11 |
12 | export function drawUI(): void {
13 |
14 | const navHeader: HTMLElement = createDiv("nav-header");
15 | const navButtonContainer: HTMLElement = navHeader.createDiv("nav-buttons-container");
16 |
17 | // periodic notes
18 | const granularity = GRANULARITY_LIST[this.activeGranularity];
19 |
20 | // アイコン描画
21 | uiPreviousPage.call(this, navButtonContainer, granularity);
22 | uiNextPage.call(this, navButtonContainer, granularity);
23 | uiReturnHome.call(this, navButtonContainer);
24 | // uiUpdate.call(this, navButtonContainer);
25 | uiSetting.call(this, navButtonContainer, granularity);
26 | uiCreateDailyNote.call(this, navButtonContainer, granularity);
27 | uiExtract.call(this, navButtonContainer);
28 | uiCollapse.call(this, navButtonContainer);
29 |
30 |
31 | // 日付の範囲の描画
32 | const navDateRange: HTMLElement = navHeader.createDiv("nav-date-range");
33 | let latest = this.searchRange.latest.clone();
34 | let earliest = this.searchRange.earliest.clone();
35 |
36 | // forward search 廃止に伴い変更
37 | earliest = latest.clone().subtract(this.settings.duration[granularity] - 1 ,granularity);
38 | if (latest.isSame(window.moment(),"day")){
39 | latest = latest.clone().add(Math.ceil(this.settings.offset/DAYS_PER_UNIT[granularity]),granularity);
40 | }
41 |
42 | const dateRange: string = earliest.format("YYYY/MM/DD [-] ") +
43 | latest.format( earliest.isSame(latest,'year') ? "MM/DD": "YYYY/MM/DD");
44 | navDateRange.createDiv("nav-date-range-content").setText(dateRange);
45 |
46 | //日付範囲クリック時の動作
47 | navDateRange.addEventListener(
48 | "click",
49 | async (event:MouseEvent) =>{
50 | const onSubmit = (enteredDate: string) => {
51 | const targetDate = window.moment(enteredDate);
52 | if (targetDate.isValid()){
53 | this.searchRange.latest = targetDate;
54 | this.searchRange.earliest = targetDate.clone().subtract(this.settings.duration.day -1,'days');
55 | this.refreshView(false, true, true);
56 | } else {
57 | new Notice("entered date is invalid.");
58 | }
59 | }
60 | new ModalJump(this.app, this.plugin, onSubmit).open();
61 |
62 | }
63 | );
64 | navDateRange.addEventListener(
65 | "contextmenu",
66 | (event: MouseEvent) =>{
67 | const menu = new Menu();
68 | menu.addItem((item) =>
69 | item.setTitle(`Jump to ${this.settings.onset}`)
70 | .onClick(()=>{
71 | const targetDate = window.moment(this.settings.onset,"YYYY-MM-DD");
72 | if (targetDate.isValid()){
73 | this.searchRange.latest = targetDate;
74 | this.searchRange.earliest = targetDate.clone().subtract(this.settings.duration.day -1,'days');
75 | } else {
76 | new Notice(`${this.settings.onset} is an invalid date.`);
77 | }
78 | this.refreshView(false, true, true);
79 | })
80 | );
81 | menu.addSeparator();
82 | menu.addItem((item) =>
83 | item.setTitle("Jump to 3 months ago")
84 | .onClick(() =>{
85 | this.searchRange.latest.subtract(3,'months');
86 | this.searchRange.earliest.subtract(3,'months');
87 | this.refreshView(false,true,true);
88 | })
89 | );
90 | menu.addItem((item) =>
91 | item.setTitle("Jump to 6 months ago")
92 | .onClick(() =>{
93 | this.searchRange.latest.subtract(6,'months');
94 | this.searchRange.earliest.subtract(6,'months');
95 | this.refreshView(false,true,true);
96 | })
97 | );
98 | menu.addItem((item) =>
99 | item.setTitle("Jump to 1 year ago")
100 | .onClick(() =>{
101 | this.searchRange.latest.subtract(12,'months');
102 | this.searchRange.earliest.subtract(12,'months');
103 | this.refreshView(false,true,true);
104 | })
105 | );
106 | menu.addItem((item) =>
107 | item.setTitle("Jump to 2 years ago")
108 | .onClick(() =>{
109 | this.searchRange.latest.subtract(24,'months');
110 | this.searchRange.earliest.subtract(24,'months');
111 | this.refreshView(false,true,true);
112 | })
113 | );
114 | if (window.moment().subtract(3,'months').isSameOrAfter(this.searchRange.latest)){
115 | menu.addSeparator();
116 | menu.addItem((item) =>
117 | item.setTitle("Jump to 3 months later")
118 | .onClick(() =>{
119 | this.searchRange.latest.add(3,'months');
120 | this.searchRange.earliest.add(3,'months');
121 | this.refreshView(false,true,true);
122 | })
123 | );
124 | }
125 | if (window.moment().subtract(6,'months').isSameOrAfter(this.searchRange.latest)){
126 | menu.addItem((item) =>
127 | item.setTitle("Jump to 6 months later")
128 | .onClick(() =>{
129 | this.searchRange.latest.add(6,'months');
130 | this.searchRange.earliest.add(6,'months');
131 | this.refreshView(false,true,true);
132 | })
133 | );
134 | }
135 | if (window.moment().subtract(12,'months').isSameOrAfter(this.searchRange.latest)){
136 | menu.addItem((item) =>
137 | item.setTitle("Jump to 1 year later")
138 | .onClick(() =>{
139 | this.searchRange.latest.add(12,'months');
140 | this.searchRange.earliest.add(12,'months');
141 | this.refreshView(false,true,true);
142 | })
143 | );
144 | }
145 | if (window.moment().subtract(24,'months').isSameOrAfter(this.searchRange.latest)){
146 | menu.addItem((item) =>
147 | item.setTitle("Jump to 2 years later")
148 | .onClick(() =>{
149 | this.searchRange.latest.add(24,'months');
150 | this.searchRange.earliest.add(24,'months');
151 | this.refreshView(false,true,true);
152 | })
153 | );
154 | }
155 | menu.addSeparator();
156 | menu.addItem((item) =>
157 | item.setTitle("Jump to the first note")
158 | .onClick(() =>{
159 | const targetDate = searchFirstNote.call(this);
160 | this.searchRange.latest = targetDate;
161 | this.searchRange.earliest = targetDate.clone().subtract(this.settings.duration.day -1,'days');
162 | this.refreshView(false,true,true);
163 | })
164 | );
165 |
166 | menu.showAtMouseEvent(event);
167 | }
168 | );
169 |
170 | // Periodic Notes 粒度描画 & 粒度切り替え処理
171 | if (this.settings.periodicNotesEnabled){
172 | const navGranularity: HTMLElement = navHeader.createDiv("nav-date-range");
173 | navGranularity.createDiv("nav-date-range-content").setText(GRANULARITY_LIST[this.activeGranularity]);
174 |
175 | navGranularity.addEventListener(
176 | "click",
177 | (evet:MouseEvent) =>{
178 | if (this.settings.showDebugInfo){
179 | console.log('DNO:clicked to change granularity. verPN, calendarSets:',this.activeGranularity, this.verPN, this.calendarSets);
180 | }
181 | const initialGranularity = this.activeGranularity;
182 | do {
183 | this.activeGranularity = (this.activeGranularity + 1) % GRANULARITY_LIST.length;
184 | } while ((
185 | (this.verPN == 2 && !this.calendarSets[this.activeSet][GRANULARITY_LIST[this.activeGranularity]]?.enabled) ||
186 | (this.verPN == 1 && !this.app.plugins.getPlugin("periodic-notes").settings?.[GRANULARITY_TO_PERIODICITY[GRANULARITY_LIST[this.activeGranularity]]]?.enabled)
187 | ) && this.activeGranularity != initialGranularity)
188 |
189 | this.app.workspace.requestSaveLayout();
190 | this.refreshView(true, true, true);
191 | }
192 | )
193 |
194 | // コンテキストメニュー
195 | navGranularity.addEventListener(
196 | "contextmenu",
197 | (event: MouseEvent) =>{
198 | const menu = new Menu();
199 | for (let i=0 ; i
206 | item
207 | .setTitle(GRANULARITY_LIST[i])
208 | .setIcon(icon)
209 | .onClick( ()=> {
210 | this.activeGranularity = i;
211 | this.app.workspace.requestSaveLayout();
212 | this.refreshView(true,true,true);
213 | })
214 | );
215 | }
216 | }
217 | menu.showAtMouseEvent(event);
218 | }
219 | )
220 | }
221 | // Periodic Notes カレンダーセット描画 & セット切り替え処理
222 | if (this.settings.calendarSetsEnabled){
223 | const navCalendarSet: HTMLElement = navHeader.createDiv("nav-date-range");
224 | navCalendarSet.createDiv("nav-date-range-content").setText(this.calendarSets[this.activeSet].id);
225 |
226 | navCalendarSet.addEventListener(
227 | "click",
228 | (evet:MouseEvent) =>{
229 | this.activeSet = (this.activeSet + 1) % this.calendarSets.length;
230 | this.app.workspace.requestSaveLayout();
231 | this.refreshView(false,true,true);
232 | }
233 | )
234 |
235 | // コンテキストメニュー
236 | navCalendarSet.addEventListener(
237 | "contextmenu",
238 | (event: MouseEvent) =>{
239 | const menu = new Menu();
240 | for (let i=0 ; i
243 | item
244 | .setTitle(this.calendarSets[i].id)
245 | .setIcon(icon)
246 | .onClick( ()=> {
247 | this.activeSet = i;
248 | this.app.workspace.requestSaveLayout();
249 | this.refreshView(false,true,true);
250 | })
251 | );
252 | }
253 | menu.showAtMouseEvent(event);
254 | }
255 | )
256 | }
257 |
258 | // 描画
259 | this.contentEl.empty();
260 | this.contentEl.appendChild(navHeader);
261 | }
262 |
263 |
264 | //過去に移動
265 | function uiPreviousPage (parentEl: HTMLElement, granularity: IGranularity):void{
266 |
267 | let navActionButton: HTMLElement = parentEl.createDiv("clickable-icon nav-action-button");
268 | navActionButton.ariaLabel = "previous notes";
269 | setIcon(navActionButton,"arrow-left");
270 |
271 | navActionButton.addEventListener(
272 | "click",
273 | async (event:MouseEvent) =>{
274 | this.searchRange.latest = this.searchRange.latest.subtract(this.settings.duration[granularity], granularity);
275 | this.searchRange.earliest = this.searchRange.earliest.subtract(this.settings.duration[granularity],granularity);
276 | this.refreshView(false, true, true);
277 | }
278 | );
279 | }
280 |
281 | //未来に移動
282 | function uiNextPage (parentEl: HTMLElement, granularity: IGranularity):void{
283 | let navActionButton = parentEl.createDiv("clickable-icon nav-action-button");
284 | navActionButton.ariaLabel = "next notes";
285 | setIcon(navActionButton,"arrow-right");
286 |
287 | navActionButton.addEventListener(
288 | "click",
289 | async (event:MouseEvent) =>{
290 | this.searchRange.latest = this.searchRange.latest.add(this.settings.duration[granularity],granularity);
291 | this.searchRange.earliest = this.searchRange.earliest.add(this.settings.duration[granularity],granularity);
292 | this.refreshView(false, true, true);
293 | }
294 | );
295 | }
296 |
297 | // ホームに戻る
298 | function uiReturnHome (parentEl: HTMLElement):void {
299 | let navActionButton = parentEl.createDiv("clickable-icon nav-action-button");
300 | navActionButton.ariaLabel = "home";
301 | setIcon(navActionButton,"home");
302 | navActionButton.addEventListener(
303 | "click",
304 | async (event:MouseEvent) =>{
305 | this.resetSearchRange();
306 | this.refreshView(false, true, true);
307 | }
308 | );
309 | navActionButton.addEventListener(
310 | "contextmenu",
311 | (event: MouseEvent) => {
312 | const menu = new Menu();
313 | // 更新
314 | menu.addItem((item)=>
315 | item
316 | .setTitle("refresh view")
317 | .setIcon("refresh-cw")
318 | .onClick(async()=>{
319 | this.refreshView(true, true, true);
320 | })
321 | );
322 | menu.showAtMouseEvent(event);
323 | }
324 | );
325 | }
326 |
327 | //更新
328 | function uiUpdate (parentEl: HTMLElement):void {
329 | let navActionButton = parentEl.createDiv("clickable-icon nav-action-button");
330 | navActionButton.ariaLabel = "refresh";
331 | setIcon(navActionButton,"refresh-cw");
332 | navActionButton.addEventListener(
333 | "click",
334 | async (event:MouseEvent) =>{
335 | this.refreshView(true, true, true);
336 | }
337 | );
338 | }
339 |
340 | //設定
341 | function uiSetting(parentEl: HTMLElement, granularity: IGranularity):void{
342 | let navActionButton = parentEl.createDiv("clickable-icon nav-action-button");
343 | navActionButton.ariaLabel = "open settings";
344 | setIcon(navActionButton,"settings");
345 | navActionButton.addEventListener(
346 | "click",
347 | async (event:MouseEvent) =>{
348 | this.app.setting.open();
349 | this.app.setting.openTabById(this.plugin.manifest.id);
350 | }
351 | );
352 | navActionButton.addEventListener(
353 | "contextmenu",
354 | (event: MouseEvent) => {
355 | const menu = new Menu();
356 | for (const element in this.settings.showElements){
357 | const icon = ( this.settings.showElements[element] == true)? "check":"";
358 | menu.addItem((item)=>
359 | item
360 | .setTitle(`show ${element}`)
361 | .setIcon(icon)
362 | .onClick(async()=>{
363 | this.settings.showElements[element] = !this.settings.showElements[element];
364 | await this.plugin.saveSettings();
365 | this.refreshView(false,false,false);
366 | })
367 | );
368 | }
369 | // if (this.settings.showElements.listItems){
370 | // const icon = (this.settings.taskOnly)? "check": "";
371 | // menu.addItem((item)=>
372 | // item
373 | // .setTitle("tasks only")
374 | // .setIcon(icon)
375 | // .onClick(async()=>{
376 | // this.settings.taskOnly = !this.settings.taskOnly;
377 | // await this.plugin.saveSettings();
378 | // this.refreshView(false,false,false);
379 | // })
380 | // );
381 | // }
382 |
383 | const iconBacklink = (this.settings.showBacklinks == true)? "check":"";
384 | menu.addItem((item)=>
385 | item
386 | .setTitle(`show backlink files`)
387 | .setIcon(iconBacklink)
388 | .onClick(async()=>{
389 | this.settings.showBacklinks = !this.settings.showBacklinks;
390 | await this.plugin.saveSettings();
391 | this.refreshView(false,false,false);
392 | })
393 | );
394 |
395 | menu.addSeparator();
396 | if (granularity =='day'){
397 | for (let index in FILEINFO_TO_DISPLAY_DAY){
398 | const icon = ( index == this.settings.displayFileInfoDaily)? "check":"";
399 | menu.addItem((item)=>
400 | item
401 | .setTitle(FILEINFO_TO_DISPLAY_DAY[index])
402 | .setIcon(icon)
403 | .onClick( async()=> {
404 | this.settings.displayFileInfoDaily = index;
405 | await this.plugin.saveSettings();
406 | this.refreshView(false,false,false)
407 | }))
408 | }
409 | } else {
410 | for (let index in FILEINFO_TO_DISPLAY){
411 | const icon = ( index == this.settings.displayFileInfoPeriodic)? "check":"";
412 | menu.addItem((item)=>
413 | item
414 | .setTitle(FILEINFO_TO_DISPLAY[index])
415 | .setIcon(icon)
416 | .onClick( async()=> {
417 | this.settings.displayFileInfoPeriodic = index;
418 | await this.plugin.saveSettings();
419 | this.refreshView(false,false,false)
420 | }))
421 | }
422 | }
423 | menu.addSeparator();
424 |
425 | const iconTooltip = (this.settings.tooltipPreview)? "check":"";
426 | menu.addItem((item)=>
427 | item
428 | .setTitle("show tooltip preview")
429 | .setIcon(iconTooltip)
430 | .onClick(async()=>{
431 | this.settings.tooltipPreview = !this.settings.tooltipPreview;
432 | await this.plugin.saveSettings();
433 | this.refreshView(false,false,false);
434 | })
435 | );
436 | menu.showAtMouseEvent(event);
437 | }
438 | );
439 | }
440 |
441 | // デイリーノート作成
442 | function uiCreateDailyNote(parentEl:HTMLElement, granularity: IGranularity):void{
443 | let navActionButton = parentEl.createDiv("clickable-icon nav-action-button");
444 |
445 | // periodic noteのオンオフや粒度に従ってラベルを作成
446 | let labelForToday: string = 'create/open ';
447 | let labelForNext: string = 'create/open ';
448 | if (granularity == 'day'){
449 | labelForToday = labelForToday + "today's ";
450 | labelForNext = labelForNext + "tomorrow's ";
451 | } else {
452 | labelForToday = labelForToday + "present ";
453 | labelForNext = labelForNext + "next ";
454 | }
455 | labelForToday = labelForToday + GRANULARITY_TO_PERIODICITY[granularity] + ' note';
456 | labelForNext = labelForNext + GRANULARITY_TO_PERIODICITY[granularity] + ' note';
457 |
458 | if (this.verPN == 2){
459 | labelForToday = labelForToday + ' for ' + this.calendarSets[this.activeSet].id;
460 | labelForNext = labelForNext + ' for ' + this.calendarSets[this.activeSet].id;
461 | }
462 |
463 | navActionButton.ariaLabel = labelForToday;
464 | setIcon(navActionButton,"calendar-plus");
465 | navActionButton.addEventListener(
466 | "click",
467 | async (event:MouseEvent) =>{
468 | event.preventDefault;
469 | const date = window.moment();
470 |
471 | if (this.verPN == 2){
472 | createAndOpenPeriodicNote( granularity, date, this.calendarSets[this.activeSet]);
473 | } else {
474 | createAndOpenDailyNote(granularity, date, this.allDailyNotes);
475 | }
476 | }
477 | );
478 | navActionButton.addEventListener(
479 | "contextmenu",
480 | (event: MouseEvent) => {
481 | const menu = new Menu();
482 | menu.addItem((item) =>
483 | item
484 | .setTitle(labelForNext)
485 | .setIcon("calendar-plus")
486 | .onClick(async ()=> {
487 | const date = window.moment().add(1,granularity);
488 | if (this.verPN == 2){
489 | createAndOpenPeriodicNote(granularity, date, this.calendarSets[this.activeSet]);
490 | } else {
491 | createAndOpenDailyNote(granularity, date, this.allDailyNotes);
492 | }
493 | })
494 | );
495 | menu.showAtMouseEvent(event);
496 | }
497 | );
498 | }
499 |
500 | //抽出
501 | function uiExtract(parentEl: HTMLElement):void{
502 | let navActionButton = parentEl.createDiv("clickable-icon nav-action-button");
503 | if (!this.extractMode){
504 | //抽出をオンに
505 | navActionButton.ariaLabel = "extract";
506 | setIcon(navActionButton,"search");
507 | navActionButton.addEventListener(
508 | "click",
509 | async (event:MouseEvent) =>{
510 | //入力モーダルを開く
511 | event.preventDefault;
512 | const onSubmit = (enableExtract: boolean) => {
513 | if (enableExtract){
514 | this.extractMode = true;
515 | this.extractType = 'normal';
516 | this.refreshView(false,false,false);
517 | }
518 | }
519 |
520 | new ModalExtract(this.app, this.plugin, onSubmit).open();
521 | });
522 | } else {
523 | //抽出をオフに
524 | navActionButton.ariaLabel = "unextract";
525 | setIcon(navActionButton,"x-circle");
526 | navActionButton.classList.add('is-active');
527 | navActionButton.addEventListener(
528 | "click",
529 | async (event:MouseEvent) =>{
530 | this.extractMode = false;
531 | this.extractType = 'normal';
532 | this.refreshView(false,false,false);
533 |
534 | });
535 | }
536 | navActionButton.addEventListener(
537 | "contextmenu",
538 | (event: MouseEvent) => {
539 | const menu = new Menu();
540 | menu.addItem((item) =>
541 | item
542 | .setTitle("extract tasks")
543 | .setIcon("check-square")
544 | .onClick(async ()=> {
545 | this.extractMode = true;
546 | this.extractType = 'task';
547 | this.refreshView(false,false,false);
548 | })
549 | );
550 |
551 | menu.addItem((item) =>
552 | item
553 | .setTitle("extract list callouts")
554 | .setIcon("lightbulb")
555 | .onClick(async ()=> {
556 | this.extractMode = true;
557 | this.extractType = 'listCallout';
558 | this.refreshView(false,false,false);
559 | })
560 | );
561 | menu.addItem((item) =>
562 | item
563 | .setTitle("extract time lists")
564 | .setIcon("clock")
565 | .onClick(async ()=> {
566 | this.extractMode = true;
567 | this.extractType = 'time';
568 | this.refreshView(false,false,false);
569 | })
570 | );
571 | menu.showAtMouseEvent(event);
572 | }
573 | );
574 |
575 | }
576 |
577 | //全体フォールド
578 |
579 | function uiCollapse (parentEl:HTMLElement):void{
580 | let navActionButton = parentEl.createDiv("clickable-icon nav-action-button");
581 | if (this.collapseAll){
582 | navActionButton.classList.add('is-active');
583 | }
584 | if (!this.collapseAll){
585 | //全体フォールドをオンに
586 | navActionButton.ariaLabel = "collapse all";
587 | setIcon(navActionButton,"chevrons-down-up");
588 | navActionButton.addEventListener(
589 | "click",
590 | async (event:MouseEvent) =>{
591 | this.collapseAll = true;
592 | this.refreshView(false,false,false);
593 | });
594 | } else {
595 | //全体フォールドをオフに
596 | navActionButton.ariaLabel = "expand";
597 | setIcon(navActionButton,"chevrons-down-up");
598 | navActionButton.addEventListener(
599 | "click",
600 | async (event:MouseEvent) =>{
601 | this.collapseAll = false;
602 | this.refreshView(false,false,false);
603 |
604 | });
605 | }
606 | }
--------------------------------------------------------------------------------
/src/getOutline.ts:
--------------------------------------------------------------------------------
1 | import { TFile } from "obsidian";
2 | import { getDateFromFile } from "obsidian-daily-notes-interface";
3 | import { FileInfo, GRANULARITY_LIST, OutlineData } from "./main";
4 | import { getBacklinkFilesDataview } from "./getTargetFiles";
5 | import { checkListCallouts, checkTimeList } from "./util";
6 |
7 | // デイリーノートの配列から各ファイルに関する情報を抽出
8 | export async function getFileInfo(files:TFile[]):Promise{
9 | let fileInfo:FileInfo[]=[];
10 | for (let i=0; i < files.length ; i++){
11 | const content = await this.app.vault.cachedRead(files[i]);
12 | const lines = content.split("\n");
13 |
14 | const backlinkFiles = (this.settings.showBacklinks) ? getBacklinkFilesDataview( this.app, files[i]): undefined;
15 |
16 | let info:FileInfo = {
17 | date: getDateFromFile(files[i],GRANULARITY_LIST[this.activeGranularity]),
18 | //content: content,
19 | lines: lines,
20 | numOfLines: lines.length,
21 | isFolded: false,
22 | backlinks: backlinkFiles,
23 | frontmatterLinks: undefined
24 | }
25 | // periodic notes beta対応
26 | if (this.verPN == 2){
27 | info.date = this.app.plugins.getPlugin("periodic-notes").cache.cachedFiles.get(this.calendarSets[this.activeSet].id).get(files[i].path).date;
28 | }
29 |
30 | fileInfo.push(info);
31 | }
32 | return fileInfo;
33 | }
34 |
35 | // メタデータからアウトライン要素を抽出
36 | export async function getOutline(files:TFile[],info:FileInfo[]):Promise{
37 | let data:OutlineData[][]=[];
38 | for (let i=0; i< files.length ; i++){
39 | const cache = this.app.metadataCache.getFileCache(files[i]);
40 |
41 | // properties(frontmatter)からリンクを取得
42 | info[i].frontmatterLinks = cache?.frontmatterLinks;
43 |
44 | // 空配列を指定
45 | data[i]=[];
46 | if (!cache){
47 | continue;
48 | }
49 | // headings,links,tagsを抽出
50 |
51 | // console.log('check headings',cache.hasOwnProperty("headings") );
52 | if (cache.hasOwnProperty("headings")){
53 | for (let j=0; j< cache.headings.length ; j++){
54 | const element:OutlineData = {
55 | typeOfElement : "heading",
56 | position : cache.headings[j].position,
57 | displayText : cache.headings[j].heading,
58 | level: cache.headings[j].level
59 | };
60 | data[i].push(element);
61 | }
62 | }
63 |
64 | // console.log('check links',cache.hasOwnProperty("links") );
65 | if (cache.hasOwnProperty("links")){
66 | for (let j=0; j< cache.links.length ; j++){
67 | const element:OutlineData = {
68 | typeOfElement : "link",
69 | position : cache.links[j].position,
70 | //マークダウンリンクに対応
71 | displayText :
72 | (cache.links[j].displayText =="")
73 | ? cache.links[j].original.substring(1,cache.links[j].original.indexOf("]"))
74 | : cache.links[j].displayText,
75 | link: cache.links[j].link
76 | };
77 | data[i].push(element);
78 | }
79 | }
80 | //embedsに対応
81 | if (cache.hasOwnProperty("embeds")){
82 | for (let j=0; j< cache.embeds.length ; j++){
83 | const element:OutlineData = {
84 | typeOfElement : "link",
85 | position : cache.embeds[j].position,
86 | //マークダウンリンクに対応
87 | displayText :
88 | (cache.embeds[j].displayText =="")
89 | ? cache.embeds[j].original.substring(1,cache.embeds[j].original.indexOf("]"))
90 | : cache.embeds[j].displayText,
91 | link: cache.embeds[j].link
92 | };
93 | data[i].push(element);
94 | }
95 | }
96 |
97 | // console.log('check lists');
98 | if (cache.hasOwnProperty("listItems")){
99 |
100 | for (let j=0; j< cache.listItems.length ; j++){
101 | // リスト記号とタスク記号を除去
102 | let displayText = this.fileInfo[i].lines[cache.listItems[j].position.start.line].replace(/^(\s|\t)*-\s(\[.\]\s)*/,'');
103 | //以下でリストアイテムの階層の判定を行う。
104 | //リストの先頭の項目:0、先頭ではないがルートレベル:1、第2階層以下:2としている。
105 | //parentが正の数なら第2階層以下
106 | //負の数で、絶対値がposition.start.lineと一致していればトップアイテム(0)、非一致ならルート(1)
107 | //ただし視覚的に離れたトップレベルのアイテムでも、間にheadingがないとルートアイテムとして判定されてしまうので、
108 | //前のリストアイテムとの行の差が1の時のみルートアイテムとして判定するよう修正する。
109 | let listLevel: number = 0; // 0:top item of a list 1:root leve 2:other
110 | if (cache.listItems[j].parent >0){
111 | listLevel = 2;
112 | } else if (j>0){
113 | if (!(Math.abs(cache.listItems[j].parent) == cache.listItems[j].position.start.line) &&
114 | (cache.listItems[j].position.start.line - cache.listItems[j-1].position.start.line == 1)){
115 | listLevel = 1;
116 | }
117 | }
118 | // リストコールアウト,時間付きリストを判定
119 | const listCallout = checkListCallouts(displayText, this.app.plugins.plugins?.['obsidian-list-callouts']?.settings);
120 | const time = checkTimeList(displayText);
121 | const element:OutlineData = {
122 | typeOfElement : 'listItems',
123 | position : cache.listItems[j].position,
124 | displayText : this.fileInfo[i].lines[cache.listItems[j].position.start.line].replace(/^(\s|\t)*-\s(\[.\]\s)*/,''),
125 | level : listLevel,
126 | task : cache.listItems[j].task,
127 | listCallout : listCallout,
128 | time : time
129 | };
130 | data[i].push(element);
131 | }
132 | }
133 |
134 | // console.log('check tags',cache.hasOwnProperty("tags") );
135 | if (cache.hasOwnProperty("tags")){
136 | for (let j=0; j< cache.tags.length ; j++){
137 | const element:OutlineData = {
138 | typeOfElement : "tag",
139 | position : cache.tags[j].position,
140 | displayText : cache.tags[j].tag.substring(1),
141 | };
142 | data[i].push(element);
143 | }
144 | }
145 | // 要素の登場順にソート
146 | data[i].sort((a,b)=> {
147 | return (a.position.start.offset - b.position.start.offset);
148 | });
149 |
150 | }
151 | return data;
152 |
153 | }
--------------------------------------------------------------------------------
/src/getTargetFiles.ts:
--------------------------------------------------------------------------------
1 | // 取得した全デイリーノートから探索範囲のデイリーノートを切り出す
2 |
3 | import { App, TFile } from "obsidian";
4 | import { IGranularity, getDateUID } from "obsidian-daily-notes-interface";
5 | import { DAYS_PER_UNIT, DateInfo } from "./main";
6 | import { CalendarSet } from "./periodicNotesTypes";
7 |
8 | // (ver0.x.xのperiodic notesにも対応。)
9 | export function getTargetFiles(allFiles:Record, granularity: IGranularity): TFile[]{
10 |
11 | let files: TFile[] = [];
12 | let checkDate = this.searchRange.latest.clone();
13 | let checkDateEarliest = this.searchRange.earliest.clone();
14 |
15 | if(checkDate.isSame(window.moment(),'day')){
16 | checkDate.add(Math.ceil(this.settings.offset/DAYS_PER_UNIT[granularity]),granularity);
17 | }
18 | checkDateEarliest = this.searchRange.latest.clone().subtract(this.settings.duration[granularity] - 1,granularity);
19 |
20 | while (checkDate.isSameOrAfter(checkDateEarliest,granularity)){
21 | if (allFiles[getDateUID(checkDate, granularity)]){
22 | files.push(allFiles[getDateUID(checkDate, granularity)]);
23 | }
24 | checkDate.subtract(1,granularity);
25 | }
26 | return files;
27 | }
28 |
29 | // 探索範囲のPeriodic Notesを取得する (peirodic notes 1.x.x用)
30 | export function getTargetPeriodicNotes(calendarSet: CalendarSet, granularity: IGranularity): TFile[]{
31 | let files: TFile[] = [];
32 |
33 | // day基準の探索範囲であるsearchRangeを元に、粒度に応じて探索範囲を拡張したものをcheckDate,checkDateEarliestに代入
34 | let checkDate = this.searchRange.latest.clone();
35 | let checkDateEarliest = this.searchRange.earliest.clone();
36 |
37 | if(checkDate.isSame(window.moment(),'day')){
38 | checkDate.add(Math.ceil(this.settings.offset/DAYS_PER_UNIT[granularity]),granularity);
39 | }
40 | checkDateEarliest = this.searchRange.latest.clone().subtract(this.settings.duration[granularity] - 1,granularity);
41 |
42 | if (this.settings.showDebugInfo){
43 | console.log('DNO:searching caches of Periodic Notes... calendar set:',calendarSet);
44 | }
45 |
46 | while (checkDate.isSameOrAfter(checkDateEarliest,granularity)){
47 | const caches = this.app.plugins.getPlugin("periodic-notes").cache.getPeriodicNotes(calendarSet.id,granularity,checkDate);
48 | for (const file of caches){
49 | //厳密にマッチしたファイル出なければスキップ
50 | if ( this.settings.exactMatchOnly && !file?.matchData?.exact) {
51 | continue;
52 | }
53 | //ファイルパスが.mdで終わらなければ(マークダウンファイルでなければ)スキップ
54 | if (this.settings.markdownOnly && !file?.filePath.endsWith(".md")){
55 | continue;
56 | }
57 | // ファイルパスにPNのフォルダパスが含まれていない && PNのフォルダパスが指定されている のときは処理をスキップ
58 | // *現状のPNベータでは、カレンダーセットの指定にかかわらず全セットに含まれるPNが返されるようであるため、各セットのフォルダパスでフィルタリングする
59 | // ただしフォルダパスが指定されていないときはスキップ
60 | if (!file.filePath.startsWith(calendarSet[granularity].folder.concat("/")) && calendarSet[granularity].folder){
61 | continue;
62 | }
63 | const fileobj = this.app.vault.getAbstractFileByPath(file.filePath);
64 | if (fileobj instanceof TFile){
65 | files.push(fileobj);
66 | }
67 | }
68 | checkDate.subtract(1,granularity);
69 | }
70 | return files;
71 | }
72 |
73 |
74 | export function getBacklinkFiles(app: App, file:TFile):TFile[]{
75 | let files:TFile[]=[];
76 | let backlinks = app.metadataCache.getBacklinksForFile(file).data;
77 | for ( const key in backlinks){
78 | const fileobj = app.vault.getAbstractFileByPath(key);
79 | if (fileobj instanceof TFile){
80 | files.push(fileobj);
81 | }
82 | }
83 | return files;
84 | }
85 |
86 | export function getBacklinkFilesDataview(app:App, file:TFile):TFile[]{
87 | let files:TFile[]=[];
88 |
89 | if (!app.plugins.plugins['dataview']){
90 | return getBacklinkFiles(app, file);
91 | }
92 |
93 | let backlinks = app.plugins.plugins?.dataview?.api?.pages(`"${file.path}"`)?.values[0]?.file.inlinks.values;
94 |
95 | if (!backlinks) {
96 | return getBacklinkFiles(app,file);
97 | }
98 | for ( let i = 0; i < backlinks.length; i++){
99 | const fileobj = app.vault.getAbstractFileByPath(backlinks[i].path);
100 | if (fileobj instanceof TFile){
101 | files.push(fileobj);
102 | }
103 | }
104 | return files;
105 | }
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { Plugin, Pos, TFile, WorkspaceLeaf } from 'obsidian';
2 |
3 | // Daily Note Interface
4 | import { getAllDailyNotes, getDailyNote, getDateFromFile, IGranularity } from "obsidian-daily-notes-interface";
5 | // moment.js
6 | import moment from "moment";
7 |
8 | import { DailyNoteOutlineView, DailyNoteOutlineViewType } from 'src/view'
9 | import { DailyNoteOutlineSettingTab } from 'src/setting'
10 | import { time } from 'console';
11 | import { checkUnresolvedLinks } from './util';
12 |
13 |
14 |
15 | // 設定項目
16 | export interface DailyNoteOutlineSettings {
17 | //initialSearchType: string; //forward or backward 特定日から前方探索or当日から後方探索
18 | offset: number; // 未来の日付も含む場合何日分含めるか number of future days to show
19 | onset: string; // 特定日からforwardに探索する場合の起点日 onset date
20 | duration:{
21 | day:number;
22 | week:number;
23 | month:number;
24 | quarter:number;
25 | year:number;
26 | } // 探索日数 number of days to search per page
27 | showElements:{
28 | heading: boolean;
29 | link: boolean;
30 | tag: boolean;
31 | listItems: boolean;
32 | task: boolean,
33 | };
34 | headingLevel: boolean[];
35 | allRootItems: boolean;
36 | allTasks: boolean;
37 | taskOnly: boolean;
38 | hideCompletedTasks: boolean;
39 | displayFileInfo: string; // none || lines || days legacy
40 | displayFileInfoDaily: string; // none,lines,days,dow(day of the week),dowshort,weeknumber,tag
41 | displayFileInfoPeriodic: string; // none,lines,days,tag
42 |
43 | viewPosition: string; //right || left || tab || split || popout
44 |
45 | markdownOnly: boolean;
46 | exactMatchOnly: boolean;
47 |
48 | wordsToIgnore:{ //filter
49 | heading: string[];
50 | link: string[];
51 | tag: string[];
52 | listItems: string[]
53 | };
54 |
55 | inlinePreview: boolean;
56 | tooltipPreview: boolean;
57 | tooltipPreviewDirection: string; // left || right
58 |
59 |
60 | includeOnly: string; // none || heading, link, tag, listItems
61 | wordsToInclude: string[];
62 | includeBeginning: boolean;
63 |
64 | primeElement: string; // none || heading, link, tag, listItems
65 | wordsToExclude:{
66 | heading: string[];
67 | link: string[];
68 | tag: string[];
69 | listItems: string[]
70 | };
71 | wordsToExtract: string;
72 |
73 | icon:{ //icon for each type of element
74 | heading: string;
75 | link: string;
76 | tag: string;
77 | listItems: string;
78 | note: string;
79 | task: string;
80 | taskDone: string;
81 | backlink: string;
82 | time: string;
83 | };
84 |
85 | customIcon:{
86 | heading: string;
87 | link: string;
88 | tag: string;
89 | listItems: string;
90 | note: string;
91 | task: string;
92 | taskDone: string;
93 | backlink: string;
94 | time: string;
95 | };
96 |
97 | indent:{
98 | heading: boolean;
99 | link: boolean;
100 | listItems: boolean;
101 | };
102 | indentFollowHeading: number; // 0 don't follow 1:same level 2: level+1
103 |
104 | prefix:{
105 | heading: string;
106 | link: string;
107 | tag: string;
108 | listItems: string;
109 | task: string;
110 | taskDone: string;
111 | backlink: string;
112 | time: string;
113 | };
114 | repeatHeadingPrefix: string; // none, level, level-1
115 | addCheckboxText: boolean;
116 |
117 | periodicNotesEnabled: boolean;
118 | calendarSetsEnabled: boolean;
119 | attachWeeklyNotesName: boolean;
120 |
121 | showDebugInfo: boolean;
122 |
123 | noteTitleBackgroundColor: string; // none, accent, custom
124 | customNoteTitleBackgroundColor: {
125 | accent: {
126 | light: string;
127 | dark: string;
128 | };
129 | custom: {
130 | light: string;
131 | dark: string;
132 | };
133 | };
134 | customNoteTitleBackgroundColorHover: {
135 | accent: {
136 | light: string;
137 | dark: string;
138 | };
139 | custom: {
140 | light: string;
141 | dark: string;
142 | };
143 | };
144 |
145 | // 各デイリーノートの情報
146 | fileFlag: {
147 | [path: string]: {
148 | 'fold'?: boolean;
149 | };
150 | };
151 |
152 | // taskのcustom status
153 | taskIcon: {
154 | [status: string]: {
155 | symbol: string;
156 | icon: string;
157 | };
158 | };
159 |
160 | collapseAllAtStartup: boolean;
161 |
162 | // getBacklinks: boolean;
163 | showBacklinks: boolean;
164 |
165 | showPropertyLinks: boolean;
166 |
167 | popoutSize: {
168 | width: number;
169 | height: number;
170 | }
171 | popoutAlwaysOnTop: boolean;
172 |
173 | showListCallouts: boolean;
174 | showTimeList: boolean;
175 |
176 | setBaseDateAsHome: boolean;
177 |
178 | checkUnresolvedLinksAtStartup: boolean;
179 |
180 | wrapLine: boolean;
181 | bootDelayTime: number;
182 | }
183 |
184 | // 設定項目デフォルト
185 | export const DEFAULT_SETTINGS: DailyNoteOutlineSettings = {
186 | //initialSearchType: 'backward',
187 | offset: 7,
188 | onset: '2020-03-30',
189 | duration: {
190 | day:28,
191 | week:12,
192 | month:12,
193 | quarter:8,
194 | year:3
195 | },
196 | showElements: {
197 | heading: true,
198 | link: true,
199 | tag: true,
200 | listItems: true,
201 | task: true,
202 | },
203 |
204 | headingLevel: [true, true, true, false, false, false],
205 | allRootItems: false,
206 | allTasks: true,
207 | taskOnly: false,
208 | hideCompletedTasks: false,
209 | displayFileInfo: 'lines',
210 | displayFileInfoDaily: 'lines', // none,lines,days,dow(day of the week),dowshort,weeknumber,tag
211 | displayFileInfoPeriodic: 'lines', // none,lines,days,tag
212 | viewPosition: 'right',
213 |
214 | markdownOnly: true,
215 | exactMatchOnly: false,
216 |
217 | wordsToIgnore:{
218 | heading: [],
219 | link: [],
220 | tag: [],
221 | listItems: []
222 | },
223 |
224 | inlinePreview: true,
225 | tooltipPreview: true,
226 | tooltipPreviewDirection: 'left',
227 |
228 |
229 | includeOnly: 'none',
230 | wordsToInclude: [],
231 | includeBeginning: true,
232 |
233 | primeElement: 'none',
234 | wordsToExclude:{
235 | heading: [],
236 | link: [],
237 | tag: [],
238 | listItems: []
239 | },
240 | wordsToExtract: '',
241 |
242 | icon:{
243 | heading: 'none',
244 | link: 'link',
245 | tag: 'tag',
246 | listItems: 'list',
247 | note: 'file',
248 | task: 'square',
249 | taskDone: 'check-square',
250 | backlink: 'links-coming-in',
251 | time: 'clock'
252 | },
253 | customIcon:{
254 | heading: 'hash',
255 | link: 'link',
256 | tag: 'tag',
257 | listItems: 'list',
258 | note:'file',
259 | task:'square',
260 | taskDone:'check-square',
261 | backlink:'links-coming-in',
262 | time: 'clock'
263 | },
264 |
265 | indent:{
266 | heading: true,
267 | link: true,
268 | listItems: true,
269 | },
270 | indentFollowHeading: 0,
271 | prefix:{
272 | heading: '',
273 | link: '',
274 | tag: '',
275 | listItems: '',
276 | task: '',
277 | taskDone: '',
278 | backlink:'',
279 | time: ''
280 | },
281 | repeatHeadingPrefix:'none',
282 | addCheckboxText: false,
283 |
284 | // dailyNoteCore: true,
285 | periodicNotesEnabled: true,
286 | calendarSetsEnabled: true,
287 | attachWeeklyNotesName: true,
288 |
289 | showDebugInfo: false,
290 |
291 | noteTitleBackgroundColor: 'none', // none, accent, custom
292 | customNoteTitleBackgroundColor: {
293 | accent: {
294 | light: '#E3E3E3',
295 | dark: '#363636',
296 | },
297 | custom: {
298 | light: '#BEBEBE',
299 | dark: '#4E4E4E',
300 | },
301 | },
302 | customNoteTitleBackgroundColorHover: {
303 | accent: {
304 | light: '#D3D3D3',
305 | dark: '#464646',
306 | },
307 | custom: {
308 | light: '#AEAEAE',
309 | dark: '#5E5E5E',
310 | },
311 | },
312 |
313 | fileFlag: {},
314 |
315 | // taskのcustom status
316 | taskIcon: {
317 | // todo: {symbol:' ', icon:'square'},
318 | incomplete: {symbol:'/', icon:'columns'},
319 | // done: {symbol:'x', icon:'check-square'},
320 | canceled: {symbol:'-', icon:'minus-square'},
321 | fowarded: {symbol:'>', icon:'send'},
322 | scheduling: {symbol:'<', icon:'calendar'},
323 | question: {symbol:'?', icon:'help-circle'},
324 | important: {symbol:'!', icon:'alert-triangle'},
325 | star: {symbol:'*', icon:'star'},
326 | quote: {symbol:'"', icon:'quote'},
327 | location: {symbol:'l', icon:'map-pin'},
328 | bookmark: {symbol:'b', icon:'bookmark'},
329 | information: {symbol:'i', icon:'info'},
330 | savings: {symbol:'S', icon:'dollar-sign'},
331 | idea: {symbol:'I', icon:'siren'},
332 | pros: {symbol:'p', icon:'thumbs-up'},
333 | cons: {symbol:'c', icon:'thumbs-down'},
334 | fire: {symbol:'f', icon:'flame'},
335 | key: {symbol:'k', icon:'key'},
336 | win: {symbol:'w', icon:'cake'},
337 | up: {symbol:'u', icon:'trending-up'},
338 | down: {symbol:'d', icon:'trending-down'},
339 | },
340 |
341 | collapseAllAtStartup: false,
342 |
343 | // getBacklinks: true,
344 | showBacklinks: false,
345 |
346 | showPropertyLinks: true,
347 |
348 | popoutSize: {
349 | width: 600,
350 | height: 800
351 | },
352 | popoutAlwaysOnTop: false,
353 |
354 | showListCallouts: true,
355 | showTimeList: true,
356 |
357 | setBaseDateAsHome: false,
358 |
359 | checkUnresolvedLinksAtStartup: false,
360 |
361 | wrapLine: true,
362 |
363 | bootDelayTime: 300,
364 | }
365 |
366 | // export interface FileStatus {
367 | // isFolded: boolean;
368 | // }
369 |
370 | export interface FileInfo {
371 | // file:TFile;
372 | date: moment;
373 | //content: string;
374 | lines: string[];
375 | numOfLines: number;
376 | isFolded: boolean;
377 | backlinks?: TFile[];
378 | frontmatterLinks?: {
379 | displayText?: string;
380 | key: string;
381 | link: string;
382 | original: string;
383 | }[];
384 | }
385 |
386 | export interface DateInfo {
387 | date: moment;
388 | dailyNote: boolean;
389 | unresolvedLinks: TFile[];
390 | // createdFiles: TFile[];
391 | // modifiedFiles: TFile[];
392 | }
393 |
394 | export interface OutlineData {
395 |
396 | typeOfElement:'heading'|'link'|'tag'|'listItems'|'task';
397 | position:Pos;
398 | link?:string;
399 | displayText?: string;
400 | // level :listItemsについては0:トップ、1:ルート、2:それ以下
401 | level?:number;
402 | task?: string|undefined;
403 | listCallout?: number|null;
404 | time?: string|null;
405 | }
406 |
407 | export const DAYS_PER_UNIT ={
408 | day: 1,
409 | week: 7,
410 | month: 30,
411 | quarter: 90,
412 | year:365
413 | }
414 |
415 | export const GRANULARITY_LIST: IGranularity[] = ['day','week','month','quarter','year']
416 |
417 | export const GRANULARITY_TO_PERIODICITY = {
418 | day: 'daily',
419 | week: 'weekly',
420 | month: 'monthly',
421 | quarter: 'quarterly',
422 | year: 'yearly'
423 | }
424 |
425 | export const FILEINFO_TO_DISPLAY = {
426 | none: 'none',
427 | lines: 'num of lines',
428 | days: 'distance',
429 | tag: 'first tag'
430 | }
431 |
432 | export const FILEINFO_TO_DISPLAY_DAY = {
433 | none: 'none',
434 | lines: 'num of lines',
435 | days: 'distance',
436 | tag: 'first tag',
437 | dow: 'day of week',
438 | dowshort: 'day of week(short)',
439 | weeknumber: 'week number'
440 | }
441 |
442 | export default class DailyNoteOutlinePlugin extends Plugin {
443 |
444 | settings: DailyNoteOutlineSettings;
445 | view: DailyNoteOutlineView;
446 |
447 | async onload() {
448 |
449 | await this.loadSettings();
450 | //register custome view according to Devloper Docs
451 | this.registerView(
452 | DailyNoteOutlineViewType,
453 | (leaf) => (this.view = new DailyNoteOutlineView(leaf, this, this.settings))
454 | );
455 |
456 | //コマンド追加
457 | // Open Outline: アウトライン表示用のカスタムビューを開く
458 | this.addCommand({
459 | id: 'daily-note-outline-open',
460 | name: 'Open Outline',
461 |
462 | callback: async ()=> {
463 | this.checkView(true);
464 | }
465 | });
466 |
467 | this.addCommand({
468 | id: 'create-dailynote-for-unresolved-links',
469 | name: 'Create daily notes for unresolved links',
470 | callback: async()=>{
471 | checkUnresolvedLinks.call(this.view);
472 |
473 | }
474 | })
475 |
476 |
477 | // // get Today's Note: DailyNoteInterfaceのtestのための開発用コマンド。
478 | // // 全DailyNoteを取得したのち、本日分を取得して、consoleに表示する。
479 |
480 | // this.addCommand({
481 | // id: 'daily-note-outline-getTodaysNote',
482 | // name: "Get Today's Note",
483 | // callback: async ()=>{
484 | // // 全てのデイリーノートを取得
485 | // const allDailyNotes = getAllDailyNotes();
486 | // console.log(allDailyNotes);
487 | // console.log(typeof allDailyNotes);
488 | // console.log('momentそのまま',moment());
489 | // console.log(`DailyNoteInterfaceのキーの形式に。day-${window.moment().startOf('day').format()}`);
490 |
491 | // //本日のデイリーノートを取得
492 | // const todaysNote = getDailyNote(moment(), allDailyNotes);
493 | // if (todaysNote){
494 | // console.log(todaysNote)
495 | // // 日付の取得
496 | // const noteDate = getDateFromFile(todaysNote,"day");
497 | // console.log(`日付は ${noteDate}です`);
498 |
499 | // const cache = this.app.metadataCache.getFileCache(todaysNote);
500 | // console.log('cache:', cache);
501 |
502 | // const note = this.app.vault.cachedRead(todaysNote);
503 | // console.log(note);
504 |
505 | // } else {
506 | // console.log('今日のノートがみあたりません');
507 | // }
508 | // }
509 | // });
510 |
511 | // viewの更新(アップデート時用)
512 | this.app.workspace.onLayoutReady(async()=>{
513 | this.checkView(false);
514 | })
515 |
516 | // This adds a settings tab so the user can configure various aspects of the plugin
517 | this.addSettingTab(new DailyNoteOutlineSettingTab(this.app, this));
518 |
519 | }
520 |
521 | onunload() {
522 | // Developer Docsに従ってカスタムビューをデタッチ
523 | this.app.workspace.detachLeavesOfType(DailyNoteOutlineViewType);
524 | }
525 |
526 | async loadSettings() {
527 | this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
528 | // v0.6.0以前の設定データに対して互換性を維持するための対応
529 | // 0.6.0以前はdurationは number型だったが、 day~yearのプロパティを持つオブジェクトに変更した。
530 | if (typeof this.settings.duration === "number"){
531 | this.settings.duration = DEFAULT_SETTINGS.duration;
532 | }
533 | // v1.4.1→v1.5でsettings.icon(customIcon).backlink/time / showElements.taskを追加したことへの対応
534 | if (!this.settings.icon.hasOwnProperty('backlink')){
535 | this.settings.icon.backlink = DEFAULT_SETTINGS.icon.backlink;
536 | this.settings.customIcon.backlink = DEFAULT_SETTINGS.customIcon.backlink;
537 | this.settings.prefix.backlink = DEFAULT_SETTINGS.prefix.backlink;
538 | }
539 | if (!this.settings.icon.hasOwnProperty('time')){
540 | this.settings.icon.time = DEFAULT_SETTINGS.icon.time;
541 | this.settings.customIcon.time = DEFAULT_SETTINGS.customIcon.time;
542 | this.settings.prefix.time = DEFAULT_SETTINGS.prefix.time;
543 | }
544 | if (!this.settings.showElements.hasOwnProperty('task')){
545 | this.settings.showElements.task = DEFAULT_SETTINGS.showElements.task;
546 | }
547 | }
548 |
549 | async saveSettings() {
550 | await this.saveData(this.settings);
551 | }
552 |
553 | checkView = async(activateView: boolean):Promise => {
554 |
555 | let [leaf] = this.app.workspace.getLeavesOfType(DailyNoteOutlineViewType);
556 | if (!leaf) {
557 | switch (this.settings.viewPosition) {
558 | case 'right':
559 | leaf = this.app.workspace.getRightLeaf(false);
560 | break;
561 | case 'left':
562 | leaf = this.app.workspace.getLeftLeaf(false);
563 | break;
564 | case 'tab':
565 | leaf = this.app.workspace.getLeaf('tab');
566 | break;
567 | case 'split':
568 | leaf = this.app.workspace.getLeaf('split');
569 | break;
570 | case 'popout':
571 | leaf = this.app.workspace.getLeaf('window');
572 | break;
573 | }
574 | await leaf.setViewState({ type: DailyNoteOutlineViewType});
575 | }
576 |
577 | if (activateView) {
578 | this.app.workspace.revealLeaf(leaf);
579 | }
580 | }
581 | }
--------------------------------------------------------------------------------
/src/modalExtract.ts:
--------------------------------------------------------------------------------
1 |
2 | import { App, Modal, Scope, Setting } from 'obsidian';
3 | import DailyNoteOutlinePlugin from 'src/main';
4 | export class ModalExtract extends Modal {
5 | plugin: DailyNoteOutlinePlugin;
6 | scope: Scope;
7 |
8 | inputedValue: string;
9 | enableExtract : boolean;
10 | onSubmit: (enableExtract: boolean) => void;
11 |
12 | constructor(app: App, plugin: DailyNoteOutlinePlugin, onSubmit: (enableExtract: boolean) => void) {
13 | super(app);
14 | this.plugin = plugin;
15 | this.onSubmit = onSubmit;
16 | }
17 |
18 | onOpen() {
19 | this.inputedValue = this.plugin.settings.wordsToExtract;
20 |
21 | const { contentEl } = this;
22 | contentEl.createEl("br");
23 |
24 | new Setting(contentEl)
25 | .setName("Extract by word or phrase:")
26 | .addText((text) =>{
27 | text.setValue(this.plugin.settings.wordsToExtract).inputEl.select();
28 | text.onChange((value) => {
29 | this.inputedValue = value
30 | });
31 | });
32 |
33 | new Setting(contentEl)
34 | .addButton((btn) =>
35 | btn
36 | .setButtonText("Extract")
37 | .setCta()
38 | .onClick(
39 | async () => {
40 | this.executeExtract();
41 | }
42 | ))
43 | .addButton((btn) =>
44 | btn
45 | .setButtonText("Cancel")
46 | .onClick(() => {
47 | this.close();
48 | }));
49 | // Enterでの入力
50 | this.scope.register([], 'Enter',
51 | (evt: KeyboardEvent)=>{
52 | this.executeExtract();
53 | }
54 | );
55 | }
56 |
57 | onClose() {
58 | let { contentEl } = this;
59 | contentEl.empty();
60 | }
61 |
62 | async executeExtract():Promise{
63 | this.close();
64 | this.plugin.settings.wordsToExtract = this.inputedValue;
65 | await this.plugin.saveSettings();
66 | this.onSubmit(true);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/modalJump.ts:
--------------------------------------------------------------------------------
1 |
2 | import { App, Modal, Scope, Setting } from 'obsidian';
3 | import DailyNoteOutlinePlugin from 'src/main';
4 | export class ModalJump extends Modal {
5 | plugin: DailyNoteOutlinePlugin;
6 | scope: Scope;
7 |
8 | inputedValue: string;
9 | enableExtract : boolean;
10 | onSubmit: (enteredDate: string) => void;
11 |
12 | constructor(app: App, plugin: DailyNoteOutlinePlugin, onSubmit: (enteredDate: string) => void) {
13 | super(app);
14 | this.plugin = plugin;
15 | this.onSubmit = onSubmit;
16 | }
17 |
18 | onOpen() {
19 | this.inputedValue = this.plugin.settings.onset;
20 |
21 | const { contentEl } = this;
22 | contentEl.createEl("br");
23 |
24 | new Setting(contentEl)
25 | .setName("Enter date to jump(YYYY-MM-DD):")
26 | .addText((text) =>{
27 | text.setValue(this.plugin.settings.onset).inputEl.select();
28 | text.onChange((value) => {
29 | this.inputedValue = value
30 | });
31 | });
32 |
33 | new Setting(contentEl)
34 | .addButton((btn) =>
35 | btn
36 | .setButtonText("Jump")
37 | .setCta()
38 | .onClick(
39 | async () => {
40 | this.executeJump();
41 | }
42 | ))
43 | .addButton((btn) =>
44 | btn
45 | .setButtonText("Cancel")
46 | .onClick(() => {
47 | this.close();
48 | }));
49 | // Enterでの入力
50 | this.scope.register([], 'Enter',
51 | (evt: KeyboardEvent)=>{
52 | this.executeJump();
53 | }
54 | );
55 | }
56 |
57 | onClose() {
58 | let { contentEl } = this;
59 | contentEl.empty();
60 | }
61 |
62 | async executeJump():Promise{
63 | this.close();
64 | this.onSubmit(this.inputedValue);
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/periodicNotesTypes.ts:
--------------------------------------------------------------------------------
1 | export interface PeriodicConfig {
2 | enabled: boolean;
3 | openAtStartup: boolean;
4 |
5 | format: string;
6 | folder: string;
7 | templatePath?: string;
8 | }
9 |
10 | export interface CalendarSet {
11 | id: string;
12 | ctime: string;
13 |
14 | day?: PeriodicConfig;
15 | week?: PeriodicConfig;
16 | month?: PeriodicConfig;
17 | quarter?: PeriodicConfig;
18 | year?: PeriodicConfig;
19 | fiscalYear?: PeriodicConfig;
20 | }
21 |
22 | export type Granularity =
23 | | "day"
24 | | "week"
25 | | "month"
26 | | "quarter"
27 | | "year";
--------------------------------------------------------------------------------
/src/util.ts:
--------------------------------------------------------------------------------
1 | import { App, Notice, Pos, TFile } from "obsidian";
2 | import { getDateFromFile, getDateFromPath } from "obsidian-daily-notes-interface";
3 | import { DailyNoteOutlineSettings, GRANULARITY_LIST } from "./main";
4 |
5 | import moment from "moment";
6 | import { createDailyNoteForUnresolvedLink, createDailyNoteForUnresolvedLinkPNbeta } from "./createAndOpenPeriodicNote";
7 |
8 | // data.jsonのfileFlagを掃除:値が空配列のプロパティを削除
9 | export function cleanFileFlag(file:TFile, settings:DailyNoteOutlineSettings): void {
10 | if (Object.keys(settings.fileFlag[file.path]).length == 0){
11 | delete settings.fileFlag[file.path];
12 | }
13 | }
14 |
15 | // data.jsonのfileFlagについて、file.pathのプロパティにflagで指定したフラグが存在するかチェック
16 | export function checkFlag(file:TFile, flag: 'fold', settings: DailyNoteOutlineSettings): boolean {
17 | return settings.fileFlag[file.path]?.[flag];
18 | }
19 |
20 | // fileFlagに指定したフラグを追加
21 | export function addFlag(file:TFile, flag: 'fold', settings: DailyNoteOutlineSettings): void {
22 | if(!settings.fileFlag.hasOwnProperty(file.path)){
23 | settings.fileFlag[file.path] = {};
24 | }
25 | settings.fileFlag[file.path][flag] = true;
26 | }
27 |
28 | //fileFlagから指定したフラグを除去
29 | export function removeFlag(file:TFile, flag: 'fold', settings: DailyNoteOutlineSettings): void {
30 | delete settings.fileFlag[file.path][flag];
31 | cleanFileFlag(file, settings);
32 | }
33 |
34 | //fileFlagの指定したフラグをトグル
35 | export function toggleFlag(file:TFile, flag: 'fold', settings: DailyNoteOutlineSettings): void {
36 | if (checkFlag(file, flag, settings) == true){
37 | removeFlag(file, flag, settings);
38 | } else {
39 | addFlag(file, flag, settings);
40 | }
41 | }
42 |
43 | // subpathを含むリンクのリンク先のpositionを取得
44 | export function getSubpathPosition (app:App, file:TFile, subpath:string):Pos|null{
45 | const cache = app.metadataCache.getFileCache(file);
46 | if (!cache) {
47 | return null;
48 | }
49 | const checkpath = subpath.replace(/[#^]/g,'');
50 | if (cache.headings?.length){
51 | const index = cache.headings.findIndex((element) => element.heading.replace(/[#^]/g,'') == checkpath);
52 | if (index >= 0){
53 | return cache.headings[index].position;
54 | }
55 | }
56 | if (cache.sections?.length){
57 | const index = cache.sections.findIndex((element) => element.id?.replace(/[#^]/g,'') == checkpath);
58 | if (index >= 0){
59 | return cache.sections[index].position;
60 | }
61 | }
62 | return null;
63 | }
64 |
65 | // list calloutのチェック
66 | export function checkListCallouts (checkString: string, callouts){
67 | if (!callouts) return null;
68 | for (let i=0; i{
124 | if ( this.settings.exactMatchOnly && !value.matchData?.exact) {
125 | return;
126 | }
127 | //ファイルパスが.mdで終わらなければ(マークダウンファイルでなければ)スキップ
128 | if (this.settings.markdownOnly && !value.filePath.endsWith(".md")){
129 | return;
130 | }
131 | // ファイルパスにPNのフォルダパスが含まれていない && PNのフォルダパスが指定されている のときは処理をスキップ
132 | // *現状のPNベータでは、カレンダーセットの指定にかかわらず全セットに含まれるPNが返されるようであるため、各セットのフォルダパスでフィルタリングする
133 | // ただしフォルダパスが指定されていないときはスキップ
134 | if (!value.filePath.startsWith(this.calendarSets[this.activeSet][GRANULARITY_LIST[this.activeGranularity]]?.folder.concat("/")) && this.calendarSets[this.activeSet][GRANULARITY_LIST[this.activeGranularity]]?.folder.folder){
135 | return;
136 | }
137 | if (value.granularity == GRANULARITY_LIST[this.activeGranularity] && value.date.isBefore(targetDate)){
138 | targetDate = value.date
139 | }
140 | })
141 | return targetDate;
142 | }
143 | }
144 |
145 |
146 | // 実体のないデイリーノートへのリンクを検出してノートを作成
147 | export async function checkUnresolvedLinks(){
148 | if (this.verPN === void 0){
149 | return;
150 | }
151 | const unresolved = this.app.metadataCache.unresolvedLinks;
152 | const allunresolvedDNLinks: string[] = [];
153 | let createdDN = 0;
154 | let notice: Notice;
155 |
156 | for (const fileName in unresolved){
157 | for (const unresolvedLink in unresolved[fileName]){
158 | if (!notice && createdDN>=5){
159 | notice = new Notice(`Daily Note Outline: created ${createdDN} daily notes for unresolved links`,0)
160 | }
161 | allunresolvedDNLinks.push(unresolvedLink);
162 |
163 | if(this.verPN != 2){
164 | // デイリーノートまたはPeriodic Notes v0.x
165 | const linkedDate = getDateFromPath(unresolvedLink,'day');
166 | if (!linkedDate){
167 | continue;
168 | }
169 | // ノートを作成
170 | await createDailyNoteForUnresolvedLink(this.app, linkedDate);
171 | createdDN++;
172 | if (notice){
173 | notice.setMessage(`Daily Note Outline: created ${createdDN} daily notes for unresolved links`);
174 | }
175 | } else {
176 | // Periodic Notes v1.x
177 | for (const calendarSet of this.calendarSets){
178 | if (!calendarSet.day.enabled){
179 | continue;
180 | }
181 | const format = calendarSet.day.format;
182 | const linkedDate = window.moment(unresolvedLink, format, true)
183 | if (linkedDate.isValid()){
184 | await createDailyNoteForUnresolvedLinkPNbeta(this.app, linkedDate, calendarSet);
185 | createdDN++;
186 | }
187 | }
188 | }
189 | }
190 | }
191 | if (notice) {
192 | new Notice('Daily Note Outline: created all daily notes for unresolved links');
193 | notice.hide();
194 | }
195 | }
196 |
197 |
198 |
--------------------------------------------------------------------------------
/src/view.ts:
--------------------------------------------------------------------------------
1 | import { MarkdownView, Notice, setIcon, debounce, Debouncer, Menu, ViewStateResult, View} from 'obsidian';
2 |
3 | import { ItemView, WorkspaceLeaf, TFile} from 'obsidian';
4 |
5 | import { getAllDailyNotes, getDateFromFile, createDailyNote, getDateUID,
6 | getAllWeeklyNotes, getAllMonthlyNotes, getAllQuarterlyNotes, getAllYearlyNotes, IGranularity } from "obsidian-daily-notes-interface";
7 | import moment from "moment"
8 |
9 | import DailyNoteOutlinePlugin, { DailyNoteOutlineSettings, OutlineData, FileInfo, DAYS_PER_UNIT,GRANULARITY_LIST, GRANULARITY_TO_PERIODICITY, FILEINFO_TO_DISPLAY, FILEINFO_TO_DISPLAY_DAY, DateInfo} from 'src/main';
10 |
11 | // Periodic Notes
12 | import {type CalendarSet,} from 'src/periodicNotesTypes';
13 |
14 | import { drawUI } from 'src/drawUI';
15 | import { getTargetFiles, getTargetPeriodicNotes } from './getTargetFiles';
16 | import { getFileInfo, getOutline } from './getOutline';
17 | import { drawOutline } from './constructDOM';
18 |
19 | import { getAPI } from "obsidian-dataview";
20 | import { checkUnresolvedLinks } from './util';
21 |
22 |
23 |
24 | export const DailyNoteOutlineViewType = 'daily-note-outline';
25 |
26 | // Stateで現在表示中の粒度、カレンダーセットを保持
27 | interface IDailyNoteOutlineState {
28 | activeSet: number;
29 | activeGranularity: number;
30 | }
31 |
32 | export class DailyNoteOutlineView extends ItemView implements IDailyNoteOutlineState {
33 |
34 | plugin: DailyNoteOutlinePlugin;
35 | settings:DailyNoteOutlineSettings;
36 |
37 | allDailyNotes: Record;
38 |
39 | targetFiles: TFile[];
40 | // fileStatus: FileStatus[];
41 | fileInfo: FileInfo[];
42 |
43 | dateInfo: DateInfo[];
44 |
45 | searchRange: {
46 | latest: moment,
47 | earliest: moment
48 | } = {
49 | latest: window.moment(),
50 | earliest: window.moment()
51 | };
52 | outlineData: OutlineData[][];
53 |
54 | flagRedraw: boolean;
55 | flagRegetAll: boolean;
56 |
57 | extractMode: boolean = false;
58 | extractType: 'noraml'|'task'|'listCallout'|'time' = 'noraml';
59 |
60 | //全ファイルの折りたたみ
61 | collapseAll: boolean = false;
62 |
63 | // Periodic Notes
64 | verPN: number; // Periodic Notes pluginのバージョン 0:off 1:<1.0.0 2:>=1.0.0
65 | calendarSets: CalendarSet[];
66 | activeSet: number = 0;
67 | activeGranularity: number = 0; // day | week | month | quarter | year GRANULARITY_LISTの添え字として
68 |
69 | constructor(
70 | leaf: WorkspaceLeaf,
71 | plugin: DailyNoteOutlinePlugin,
72 | settings: DailyNoteOutlineSettings,
73 | ) {
74 | super(leaf);
75 | this.plugin = plugin;
76 | this.settings = settings;
77 | }
78 |
79 | getViewType(): string {
80 | return DailyNoteOutlineViewType;
81 | }
82 |
83 | getDisplayText(): string {
84 | return 'Daily Note Outline';
85 | }
86 |
87 | getIcon(): string {
88 | return 'calendar-clock';
89 | }
90 |
91 | async onOpen(){
92 |
93 | this.initView();
94 | // 初回のデイリーノート探索処理はinitView()に移動し、実行前にわずかにウエイト(bootDelay)を入れた。
95 | // そうしないと起動時に全デイリーノートのデータ取得に失敗するようだったため。
96 |
97 | //自動更新のためのデータ変更、ファイル追加/削除の監視 observe file change/create/delete
98 | const debouncedAutoRefresh:Debouncer<[],null> = debounce(this.autoRefresh,2000,true);
99 | this.flagRedraw = false;
100 | this.flagRegetAll = false;
101 | this.registerEvent(this.app.metadataCache.on('changed', (file,data,cache) => {
102 | if (this.targetFiles?.includes(file)){
103 | this.flagRedraw = true;
104 | debouncedAutoRefresh.call(this);
105 | }
106 | }));
107 |
108 | this.registerEvent(this.app.vault.on('create',(file)=>{
109 | this.flagRegetAll = true;
110 | debouncedAutoRefresh.call(this);
111 | }));
112 | this.registerEvent(this.app.vault.on('delete',(file)=>{
113 | this.flagRegetAll = true;
114 | debouncedAutoRefresh.call(this);
115 | }));
116 | }
117 |
118 | async onClose(){
119 | // Nothin to clean up
120 | }
121 |
122 | // https://liamca.in/Obsidian/API+FAQ/views/persisting+your+view+state
123 | async setState(state: IDailyNoteOutlineState, result:ViewStateResult):Promise{
124 | if (state.activeSet){
125 | this.activeSet = state.activeSet;
126 | }
127 | if (state.activeGranularity){
128 | this.activeGranularity = state.activeGranularity;
129 | }
130 | return super.setState(state, result);
131 | }
132 |
133 | getState(): IDailyNoteOutlineState {
134 | return {
135 | activeSet: this.activeSet,
136 | activeGranularity: this.activeGranularity,
137 | };
138 | }
139 |
140 | private async initView() {
141 | await this.bootDelay(); //少し待たないと起動直後はDaily Noteの取得に失敗するようだ
142 | this.collapseAll = this.settings.collapseAllAtStartup;
143 |
144 | this.verPN = await this.checkPeriodicNotes();
145 | if (this.settings.checkUnresolvedLinksAtStartup){
146 | await checkUnresolvedLinks.call(this);
147 | }
148 | this.resetSearchRange();
149 |
150 | this.refreshView(true, true, true);
151 |
152 | }
153 |
154 | private async bootDelay(): Promise {
155 | return new Promise(resolve => { setTimeout(resolve, this.settings.bootDelayTime);});
156 | }
157 |
158 | private async autoRefresh(){
159 | if (!(this.flagRedraw || this.flagRegetAll)){
160 | return;
161 | }
162 |
163 | this.refreshView(this.flagRegetAll, this.flagRegetAll, true);
164 | this.flagRegetAll = false;
165 | this.flagRedraw = false;
166 | }
167 |
168 | // リフレッシュセンター
169 | // regetAllがtrue: 全デイリーノートを取得し直す
170 | // getTargetがtrue: 現在の探索範囲に含まれるデイリーノートを特定
171 | // getOutlineがtrue: 対象ファイル群のファイル情報、アウトライン情報を取得
172 | // その後UI部分とアウトライン部分を描画
173 | async refreshView(flagRegetAll: boolean, flagGetTarget:boolean, flagGetOutline:boolean){
174 | const startTime = performance.now();
175 | if (this.settings.showDebugInfo){
176 | console.log('DNO:start refreshing view. CalendarSets:',this.calendarSets);
177 | }
178 | this.verPN = await this.checkPeriodicNotes();
179 |
180 | if (this.verPN != 2){
181 | // core daily note/ periodic notes plugin v0.x の場合
182 | if (flagRegetAll){
183 | this.allDailyNotes = this.getAllNotes();
184 | }
185 |
186 | if (flagGetTarget){
187 | this.targetFiles = getTargetFiles.call(this, this.allDailyNotes, GRANULARITY_LIST[this.activeGranularity]);
188 | }
189 | if(flagGetOutline){
190 | this.fileInfo = await getFileInfo.call(this, this.targetFiles);
191 | this.outlineData = await getOutline.call(this, this.targetFiles,this.fileInfo);
192 | }
193 | drawUI.call(this);
194 | drawOutline.call(this, this.targetFiles, this.fileInfo, this.outlineData);
195 | } else {
196 | // periodic notes plugin v1.x対応
197 | if (!this.calendarSets[this.activeSet]){
198 | this.activeSet = 0;
199 | }
200 | if (!this.calendarSets[this.activeSet][GRANULARITY_LIST[this.activeGranularity]]?.enabled){
201 | if (this.settings.showDebugInfo){
202 | console.log('DNO: Calender set is not enabled. this.activeGranularity is set to 0. Calendarsets, this.activeSet, this.activeGranularity:',this.calendarSets, this.activeSet, this.activeGranularity);
203 | }
204 | this.activeGranularity = 0;
205 | }
206 |
207 | // await this.testBacklinks();
208 |
209 | if (flagGetTarget){
210 | this.targetFiles = getTargetPeriodicNotes.call(this, this.calendarSets[this.activeSet], GRANULARITY_LIST[this.activeGranularity])
211 | }
212 | if (flagGetOutline){
213 | this.fileInfo = await getFileInfo.call(this, this.targetFiles);
214 | this.outlineData = await getOutline.call(this, this.targetFiles,this.fileInfo);
215 | }
216 |
217 | drawUI.call(this);
218 | drawOutline.call(this, this.targetFiles, this.fileInfo, this.outlineData);
219 |
220 | }
221 |
222 | const endTime = performance.now();
223 | if (this.settings.showDebugInfo){
224 | console.log ('DNO: time required to refresh view: ', endTime - startTime);
225 | }
226 | }
227 |
228 | // [ ]の除去
229 | private stripMarkdownSymbol(text: string): string {
230 | return (text.replace(/(\[\[)|(\]\])/g,''));
231 | }
232 |
233 | // 探索範囲を設定に合わせて初期化
234 | public resetSearchRange():void {
235 | if (this.settings.setBaseDateAsHome == true){
236 | const onsetDate = window.moment(this.settings.onset,"YYYY-MM-DD");
237 | if (onsetDate.isValid()){
238 | this.searchRange.latest = onsetDate;
239 | this.searchRange.earliest = onsetDate.clone().subtract(this.settings.duration.day -1,'days');
240 | } else {
241 | // onsetDateが不正なら当日起点のbackward searchを行う
242 | new Notice('onset date is invalid');
243 | this.searchRange.latest = window.moment().startOf('day');
244 | this.searchRange.earliest = window.moment().startOf('day').subtract(this.settings.duration.day - 1,'days')
245 | }
246 | } else {
247 | this.searchRange.latest = window.moment().startOf('day');
248 | this.searchRange.earliest = window.moment().startOf('day').subtract(this.settings.duration.day - 1 ,'days');
249 | }
250 | }
251 |
252 | // Periodic Notesの状態をチェック return 0:オフ 1:v0.x.x 2:v1.0.0-
253 | private async checkPeriodicNotes(): Promise {
254 |
255 | if (this.settings.showDebugInfo){
256 | console.log('DNO:checking the version of Periodic Notes plugin');
257 | }
258 |
259 | if (app.plugins.plugins['periodic-notes']){
260 | if (Number(app.plugins.plugins['periodic-notes'].manifest.version.split(".")[0]) >=1){
261 | // ver 1.0.0以上
262 | this.calendarSets = this.app.plugins.getPlugin("periodic-notes")?.calendarSetManager.getCalendarSets();
263 |
264 | if (this.settings.showDebugInfo){
265 | console.log('DNO: return of this.app.PN.calendarSetManager.getCalendarSets(): ',this.app.plugins.getPlugin("periodic-notes")?.calendarSetManager.getCalendarSets());
266 | console.log('DNO: return of window.app.PN.calendarSetManager.getCalendarSets(): ',window.app.plugins.getPlugin("periodic-notes")?.calendarSetManager.getCalendarSets());
267 | }
268 |
269 | if (!this.calendarSets.length){
270 | new Notice('failed to import calendar sets to Daily Note Outline');
271 | }
272 | const initialGranularity = this.activeGranularity;
273 | while (!this.calendarSets[this.activeSet][GRANULARITY_LIST[this.activeGranularity]]?.enabled){
274 | this.activeGranularity = (this.activeGranularity + 1) % GRANULARITY_LIST.length;
275 | if(this.activeGranularity == initialGranularity){
276 | break;
277 | }
278 | }
279 | this.app.workspace.requestSaveLayout();
280 |
281 | if (this.settings.showDebugInfo){
282 | console.log('DNO: obtained verPN: ', this.verPN ,' calendarSets: ', this.calendarSets);
283 | }
284 | return 2;
285 | } else {
286 | // ver 0.x.x
287 | this.settings.calendarSetsEnabled = false;
288 | this.activeSet = 0;
289 | await this.plugin.saveSettings();
290 |
291 | const initialGranularity = this.activeGranularity;
292 | while (!this.app.plugins.getPlugin("periodic-notes").settings?.[GRANULARITY_TO_PERIODICITY[GRANULARITY_LIST[this.activeGranularity]]]?.enabled){
293 | this.activeGranularity = (this.activeGranularity + 1) % GRANULARITY_LIST.length;
294 | if(this.activeGranularity == initialGranularity){
295 | break;
296 | }
297 | }
298 | this.app.workspace.requestSaveLayout();
299 | return 1;
300 | }
301 | } else {
302 | // Periodic Notesがオフかインストールされていない
303 | this.settings.periodicNotesEnabled = false;
304 | this.settings.calendarSetsEnabled = false;
305 | this.activeGranularity = 0;
306 | this.activeSet = 0;
307 | await this.plugin.saveSettings();
308 | return 0;
309 | }
310 | }
311 |
312 | // granularityに従って全てのdaily/weekly/monthly/quarterly/yearlyノートを取得
313 | getAllNotes():Record {
314 | switch (this.activeGranularity){
315 | case 0:
316 | return getAllDailyNotes();
317 | case 1:
318 | return getAllWeeklyNotes();
319 | case 2:
320 | return getAllMonthlyNotes();
321 | case 3:
322 | return getAllQuarterlyNotes();
323 | case 4:
324 | return getAllYearlyNotes();
325 | }
326 | }
327 |
328 | // バックリンク、dataviewのテスト用
329 | // async testBacklinks():Promise{
330 |
331 | // const startTime = performance.now();
332 | // // 未解決リンク
333 | // let unresolvedLinks = this.app.metadataCache.unresolvedLinks;
334 | // console.log('未解決リンク',unresolvedLinks);
335 | // // 特定の未解決リンクを含むノートを抽出
336 | // const result = Object.entries(unresolvedLinks).filter((valueArray)=> valueArray[1].hasOwnProperty("2023-12-22_Friday"));
337 |
338 | // const endTime = performance.now();
339 | // console.log('未解決リンク探索所要時間', endTime - startTime);
340 |
341 | // const sTime2 = performance.now();
342 | // //dataviewAPI 取得
343 | // const dataviewAPI= getAPI();
344 | // const testValue = await dataviewAPI.pages('-"Daily"');
345 | // console.log('Dailyを除くファイル',testValue);
346 | // const eTime2 = performance.now();
347 | // console.log('dataview pages取得所要時間', eTime2-sTime2);
348 |
349 | // const sTime3 = performance.now();
350 | // // 特定のmdayを持つファイルの検索
351 | // //let testValue2 = [];
352 | // // for (let i = 0; i< 56; i++){
353 | // // const dvquery:string = 'LIST WHERE file.mday = date(today) - dur(' + i + ' d)';
354 | // // const result = await dataviewAPI.query(dvquery);
355 | // // testValue2.push(result.value.values);
356 | // // }
357 | // //console.log('データビュー2',testValue2);
358 | // const eTime3 = performance.now();
359 | // console.log('期間範囲指定mday', eTime3-sTime3);
360 | // }
361 | }
362 |
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 |
2 | /*日付表示 date range */
3 | .workspace-leaf-content[data-type="daily-note-outline"] .nav-date-range{
4 | display: inline-block;
5 | text-align:center;
6 | font-size: var(--nav-item-size);
7 | padding: var(--size-2-1) var(--size-2-3);
8 | }
9 |
10 | .workspace-leaf-content[data-type="daily-note-outline"] .nav-header{
11 | text-align:center;
12 | }
13 |
14 |
15 | /*viewの書式指定*/
16 | .workspace-leaf-content[data-type="daily-note-outline"] .view-content{
17 | height: 100%;
18 | overflow-x: hidden;
19 | overflow-y: hidden;
20 | padding: 0 0 16px 0;
21 | }
22 |
23 | .workspace-leaf-content[data-type="daily-note-outline"] .nav-files-container{
24 | box-sizing: border-box;
25 | height: calc(100% - 63px);
26 | }
27 | /* 100%からUI部分の高さを引いた*/
28 |
29 |
30 | /*デイリーノートファイル名 filename of daily notes*/
31 | /*要素を中央揃え align center*/
32 | .workspace-leaf-content[data-type="daily-note-outline"] .nav-folder-title{
33 | align-items: center;
34 | }
35 |
36 | /*ノート横に情報を表示 display file information */
37 | .workspace-leaf-content[data-type="daily-note-outline"] .nav-folder-title::after{
38 | content: attr(data-subinfo);
39 | font-size: 80%;
40 | display: inline-block;
41 | position: relative;
42 | margin-right: 2px;
43 | padding: 2px 0;
44 | }
45 |
46 | .workspace-leaf-content[data-type="daily-note-outline"] .nav-folder-title-content {
47 | white-space: nowrap;
48 | overflow: hidden;
49 | text-overflow: clip;
50 | flex-grow: 1;
51 | }
52 |
53 |
54 | /* アウトライン要素 outline elements*/
55 | .workspace-leaf-content[data-type="daily-note-outline"] .nav-file-title-content{
56 |
57 | white-space: nowrap;
58 | overflow: hidden;
59 | text-overflow: clip;
60 |
61 | display: inline-block;
62 | flex-shrink: 1;
63 | align-items: normal;
64 | }
65 |
66 | /* wrap-lineクラスがついていたらwhite-space設定をnormalに */
67 | .workspace-leaf-content[data-type="daily-note-outline"] .nav-file-title-content.wrap-line {
68 | white-space: normal;
69 | }
70 |
71 | /* icon */
72 | .workspace-leaf-content[data-type="daily-note-outline"] .nav-folder .svg-icon{
73 | height: 1.2em;
74 | flex-shrink: 0;
75 | }
76 |
77 | .workspace-leaf-content[data-type="daily-note-outline"] .nav-folder-collapse-indicator .is-open{
78 | transform: rotate(90deg);
79 | }
80 |
81 | /* 折りたたみアイコンの範囲 */
82 | .workspace-leaf-content[data-type="daily-note-outline"] .tree-item-icon.collapse-icon{
83 | padding: 4px;
84 | }
85 |
86 |
87 | /* obsidian ver1.2で項目にhover時、色が変わらなくなった事への暫定対応*/
88 | /* .workspace-leaf-content[data-type="daily-note-outline"] .nav-file-title:hover,
89 | .workspace-leaf-content[data-type="daily-note-outline"] .nav-folder-title:hover {
90 | background-color: var(--nav-item-background-hover);
91 | color:var(--nav-item-color-hover);
92 | } */
93 |
94 |
95 |
96 | /*インラインプレビュー inline preview */
97 | /*アウトライン要素行の各項目を中央揃えで同じ高さに表示 align center*/
98 | .workspace-leaf-content[data-type="daily-note-outline"] .nav-file-title{
99 | align-items: normal;
100 | }
101 |
102 | /*インラインプレビューのフォント inline preview font*/
103 | .workspace-leaf-content[data-type="daily-note-outline"] .nav-file-title-preview{
104 | display: inline-block;
105 | font-size: 85%;
106 | white-space: nowrap;
107 | overflow: hidden;
108 | text-overflow: clip;
109 | padding-left: 16px;
110 | /*margin-left:auto; 右寄せ*/
111 | color: var(--color-base-50);
112 | flex-shrink: 10000;
113 | align-self: center;
114 | }
115 |
116 | /*ツールチッププレビュー*/
117 | .DNO-preview{
118 | text-align: left;
119 | animation: unset !important;
120 | max-width: 600px;
121 | }
122 |
123 |
124 | /*設定画面 settings*/
125 | /*インデント用*/
126 | .setting-indent{
127 | padding-left: 2em;
128 | }
129 |
130 | .setting-indent-2{
131 | padding-left: 3em;
132 | }
133 | /*カテゴリ名の下のスペースを減らす*/
134 | .setting-category{
135 | margin-block-end: 0em;
136 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "inlineSourceMap": true,
5 | "inlineSources": true,
6 | "module": "ESNext",
7 | "target": "ES6",
8 | "allowJs": true,
9 | "noImplicitAny": true,
10 | "moduleResolution": "node",
11 | "importHelpers": true,
12 | "isolatedModules": true,
13 | "lib": [
14 | "DOM",
15 | "ES5",
16 | "ES6",
17 | "ES7"
18 | ]
19 | },
20 | "include": [
21 | "**/*.ts"
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/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.1.0": "0.15.0"
3 | }
4 |
--------------------------------------------------------------------------------