1
- import {
2
- Plugin ,
3
- TFile ,
4
- Notice ,
5
- Editor ,
6
- MarkdownSectionInformation ,
7
- ButtonComponent ,
8
- } from "obsidian" ;
1
+ import { Plugin , TFile , Notice , Editor , MarkdownView } from "obsidian" ;
9
2
import { defaultSettings , TimeTreeSettings } from "./settings" ;
10
3
import { TimeTreeSettingsTab } from "./settings-tab" ;
11
4
import { FrontMatterManager } from "./front-matter-manager" ;
@@ -17,6 +10,7 @@ export default class TimeTreePlugin extends Plugin {
17
10
private frontMatterManager : FrontMatterManager ;
18
11
private calculator : TimeTreeCalculator ;
19
12
private computeIntervalHandle : any ;
13
+ private buttonObserver : MutationObserver | null = null ;
20
14
21
15
async onload ( ) : Promise < void > {
22
16
await this . loadSettings ( ) ;
@@ -62,10 +56,38 @@ export default class TimeTreePlugin extends Plugin {
62
56
} ,
63
57
} ) ;
64
58
59
+ this . buttonObserver = new MutationObserver ( ( mutations ) => {
60
+ mutations . forEach ( ( mutation ) => {
61
+ mutation . addedNodes . forEach ( ( node ) => {
62
+ if ( node instanceof HTMLElement ) {
63
+ const btn = node . querySelector (
64
+ ".simple-time-tracker-btn"
65
+ ) as HTMLButtonElement | null ;
66
+ if ( btn ) {
67
+ btn . addEventListener ( "click" , ( ) => {
68
+ const btnStatus =
69
+ btn . getAttribute ( "aria-label" ) ;
70
+ if ( btnStatus === "End" ) {
71
+ this . elapsedTime ( ) ;
72
+ }
73
+ } ) ;
74
+ }
75
+ }
76
+ } ) ;
77
+ } ) ;
78
+ } ) ;
79
+ this . buttonObserver . observe ( document . body , {
80
+ childList : true ,
81
+ subtree : true ,
82
+ } ) ;
83
+
65
84
this . scheduleComputeTimeTree ( ) ;
66
85
}
67
86
68
87
onunload ( ) : void {
88
+ if ( this . buttonObserver ) {
89
+ this . buttonObserver . disconnect ( ) ;
90
+ }
69
91
if ( this . computeIntervalHandle ) {
70
92
clearInterval ( this . computeIntervalHandle ) ;
71
93
}
@@ -84,7 +106,76 @@ export default class TimeTreePlugin extends Plugin {
84
106
this . scheduleComputeTimeTree ( ) ;
85
107
}
86
108
109
+ async adjustCursorOutsideTracker ( editor : Editor ) : Promise < void > {
110
+ // Get the full content and split it into lines.
111
+ const content = editor . getValue ( ) ;
112
+ const lines = content . split ( "\n" ) ;
113
+
114
+ // Determine the end of YAML front matter if present. Line L is the last line of YAML metadata.
115
+ let yamlEnd = 0 ;
116
+ if ( lines [ 0 ] . trim ( ) === "---" ) {
117
+ for ( let i = 1 ; i < lines . length ; i ++ ) {
118
+ if ( lines [ i ] . trim ( ) === "---" ) {
119
+ yamlEnd = i + 1 ; // YAML metadata ends at line L
120
+ break ;
121
+ }
122
+ }
123
+ }
124
+
125
+ // Collect tracker block boundaries, but only after the YAML metadata.
126
+ const trackerBlocks : { start : number ; end : number } [ ] = [ ] ;
127
+ for ( let i = yamlEnd ; i < lines . length ; i ++ ) {
128
+ if ( lines [ i ] . trimEnd ( ) === "```simple-time-tracker" ) {
129
+ const blockStart = i ;
130
+ // Look for the closing marker.
131
+ for ( let j = i + 1 ; j < lines . length ; j ++ ) {
132
+ if ( lines [ j ] . trimEnd ( ) === "```" ) {
133
+ trackerBlocks . push ( { start : blockStart , end : j } ) ;
134
+ i = j ; // Skip the rest of this block
135
+ break ;
136
+ }
137
+ }
138
+ }
139
+ }
140
+
141
+ // Define a helper to check if a given line index is inside any tracker block.
142
+ const isLineInTracker = ( line : number ) : boolean => {
143
+ return trackerBlocks . some (
144
+ ( block ) => line >= block . start && line <= block . end
145
+ ) ;
146
+ } ;
147
+
148
+ // Find the first line after YAML metadata that is not inside any tracker block.
149
+ let targetLine = yamlEnd ;
150
+ while ( targetLine < lines . length && isLineInTracker ( targetLine ) ) {
151
+ targetLine ++ ;
152
+ }
153
+ if ( targetLine >= lines . length ) {
154
+ targetLine = lines . length - 1 ;
155
+ }
156
+
157
+ // Get the current cursor position.
158
+ const cursor = editor . getCursor ( ) ;
159
+
160
+ // If the cursor is not inside any tracker block, do nothing.
161
+ const cursorInTracker = trackerBlocks . some (
162
+ ( block ) => cursor . line >= block . start && cursor . line <= block . end
163
+ ) ;
164
+ if ( ! cursorInTracker ) {
165
+ return ;
166
+ }
167
+
168
+ // Move the cursor to the first line after YAML metadata that is not part of any tracker block.
169
+ editor . setCursor ( { line : targetLine , ch : 0 } ) ;
170
+ }
171
+
87
172
async startStopTracker ( ) : Promise < void > {
173
+ const activeView = this . app . workspace . getActiveViewOfType ( MarkdownView ) ;
174
+ if ( activeView ) {
175
+ await this . adjustCursorOutsideTracker ( activeView . editor ) ;
176
+ } else {
177
+ new Notice ( "No active Markdown editor found." ) ;
178
+ }
88
179
const btn = document . querySelector (
89
180
".simple-time-tracker-btn"
90
181
) as HTMLButtonElement | null ;
@@ -103,6 +194,14 @@ export default class TimeTreePlugin extends Plugin {
103
194
}
104
195
let elapsed = 0 ;
105
196
elapsed = await this . calculator . calculateElapsedTime ( activeFile ) ;
197
+ await new Promise ( ( resolve ) => setTimeout ( resolve , 10 ) ) ;
198
+ await this . frontMatterManager . updateProperty (
199
+ activeFile ,
200
+ ( frontmatter ) => {
201
+ frontmatter . elapsed = elapsed ;
202
+ return frontmatter ;
203
+ }
204
+ ) ;
106
205
await this . calculator . communicateAscendants ( activeFile ) ;
107
206
const rootPath = this . settings . rootNotePath ;
108
207
if ( rootPath ) {
0 commit comments