diff --git a/README.md b/README.md
index aac6c15..50e9c2b 100644
--- a/README.md
+++ b/README.md
@@ -13,8 +13,9 @@ A text block timing tool designed for Obsidian, allowing you to add a **count-up
- ⏳ Real-time display of cumulative time cost.
- 💾 Persistent storage of timing data.
- 🔄 Users can optionally choose whether to continue timing when files or Obsidian is closed (by default, closing a file will not cause failure of the real-time stopwatch).
+- By modifying the task status, you can automatically trigger the start and update of the timer
-## Usage
+## How to use
### 1. Command Palette + Shortcut (Recommended⭐️)
@@ -24,13 +25,22 @@ A text block timing tool designed for Obsidian, allowing you to add a **count-up

-### 2. Mouse Operations
+### 2. Mouse Operations (Not Recommended)
1. Right-click on a text line.
2. Select "Start Timer," "Pause Timer," or "Resume Timer."

+### 3. Control Timer by Task Status Automatically (Recommended⭐️)
+
+1. For For Common Users, you can directly change the task status [ ] to trigger the start and update of the timer (Suits for everyone, however not convenient enough, especially for Windows users.)
+2. ⭐️For `Task Plugin` Users, you can trigger the start and update of timers by simply clicking the checkbox (Convenient but needs pre-settings)
+
+
+
+
+
## Custom Settings
Users can customize the following options in the plugin Settings:
@@ -58,8 +68,25 @@ After the user closes a file, all timers will stop immediately. However, if a fi
Users can choose to insert a timer label either before or after the text block.
+
+### 3. Control Timer by Task Status Automatically - Path Control
+
+In the default settings, path control is disabled. When you only want to use the timer in specific folders, you can use a whitelist or blacklist to control it, which will only take effect in one of the two ways.
+
+- Disable path control:this feature can always take place
+
+
+- Whitelist:only folders in the whitelist will work
+
+
+- Blacklist:folders in the blacklist will not work
+
+
## Version Log:
+- V1.0.4
+ 1. ==Major Feature==:Now you can control the timer switch by the checkbox in the task front.
+ 2. Improve the function of creating timers, now it will not directly expose the span's source code
- V1.0.3
1. ==Major Optimization==: Closing a file no longer causes the timer to fail. Users can freely choose whether to continue timing when the or Obsidian is closed (see custom settings).
2. Added custom settings: Users can now choose the position to insert the timer (before or after texts).
@@ -75,4 +102,4 @@ Users can choose to insert a timer label either before or after the text block.
## Development Information
- Developer: frankthwang
-- Version: 1.0.3
\ No newline at end of file
+- Version: 1.0.4
\ No newline at end of file
diff --git a/README.zh.md b/README.zh.md
index 1026c57..079956f 100644
--- a/README.zh.md
+++ b/README.zh.md
@@ -6,13 +6,14 @@
## 功能特性
-
+
- 为任意文本行添加计时器,支持同时执行多个计时器(正计时)
- ▶️/⏸️ 支持开始、暂停、继续计时,支持任务分段耗时累计
- ⏳ 实时显示累计时间
- 💾 计时数据持久化保存
- 🔄 可自由选择关闭文件、关闭Obsidian后是否持续计时 (默认设置下,关闭文件不会导致实时计时失效)
+- 修改任务状态,可以自动触发计时器的开始和更新
## 使用方法
@@ -21,44 +22,79 @@
1. 为命令 "Text Block Timer: Toggle timer" 配置快捷键,方便快速使用
2. 当光标在某个文本块时,按下快捷键,即可实现timer的开始、暂停、继续操作
3. 计时器会以 `【⏳00:00:00 】`格式显示在文本块的行首
-
-### 2. 鼠标操作
+
+
+### 2. 鼠标操作(不推荐)
1. 在文本行上右键点击
2. 选择"开始计时"、"暂停计时"或"继续计时"
-
+
+
+
+### 3. 通过任务状态自动控制(推荐⭐️)
+
+1. 对于所有用户,可以直接通过修改特定任务前 [ ] 的状态来控制计时器的开关 (对所有用户都有效,但是不推荐,Windows用户操作起来比较麻烦)
+2. ⭐️对于 `Task Plugin`用户而言,通过恰当的设置,只需要点击任务前的 [ ] 即可完成控制 ()
+
+
+
+
## 自定义设置
用户可以在插件设置中自定义以下选项:
### 1. 关闭文件或Obsidian后是否继续计时
+
用户可以自由选择关闭文件或关闭Obsidian后是否继续计时。
-#### 1.1 从不停止,除非用户手动停止
+#### 1.1 从不停止,除非用户手动停止
+
用户关闭文件、退出Obsidian后,计时器依然会“后台计时”,在下次打开文件时,会讲关闭期间的耗时计入计时器耗时,并自动恢复计时。
#### 1.2 仅退出Obsidian时停止,关闭文件依然后台计时(推荐,默认选项)
+
用户关闭文件后,计时器依然会“后台计时”,在下次打开文件时,会讲关闭期间的耗时计入计时器耗时,并自动恢复计时。
然而,用户退出Obsidian后,所有计时器会立即停止,防止用户忘记手动关闭计时器导致数据产生重大误差。
#### 1.3 关闭文件时立即停止
+
用户关闭文件后,所有计时器会立即停止,但在误关文件时,可能导致计时暂停。
### 2. 插入位置
-用户可以选择在文本块的前面或后面插入计时器标签。
+
+用户可以选择在文本块的前面或后面插入计时器标签。对于Day Planner用户而言,推荐插入在文本块后面。
+### 3. 通过任务状态自动控制计时器 - 路径控制
+
+在默认设置下,路径控制是关闭的。当你只希望在部分文件夹下使用该功能,你可以通过白名单或黑名单的方式进行控制,这两种方式只会生效其中一种。
+
+- 不控制:所有文件夹下都可以使用该功能
+
+
+- 白名单:只有在白名单中的文件夹下,才会生效
+
+
+- 黑名单:在黑名单中的文件夹下,不会生效
+
+
+
+
+
## 版本日志:
+- V1.0.4
+ 1. ==重大功能==:现在你可以通过任务前的 [ ] 勾选框来控制计时器的开关
+ 2. 优化了新建计时器的功能,现在不会直接暴露span源代码
- V1.0.3
1. ==重大优化==:现在关闭文件不会导致计时器失效,用户可自由选择关闭文件或关闭Obsidian后是否持续计时(见自定义设置)
2. 增加自定义设置:现在用户可自由选择插入文本块位置(文本前或文本后)
@@ -74,4 +110,4 @@
## 开发信息
- 开发者: frankthwang
-- 版本: 1.0.3
\ No newline at end of file
+- 版本: 1.0.4
diff --git a/demo/controlbycheckbox.gif b/demo/controlbycheckbox.gif
new file mode 100644
index 0000000..fca0e46
Binary files /dev/null and b/demo/controlbycheckbox.gif differ
diff --git a/demo/pathControl_black.gif b/demo/pathControl_black.gif
new file mode 100644
index 0000000..c20710d
Binary files /dev/null and b/demo/pathControl_black.gif differ
diff --git a/demo/pathControl_no.gif b/demo/pathControl_no.gif
new file mode 100644
index 0000000..8cf7c6e
Binary files /dev/null and b/demo/pathControl_no.gif differ
diff --git a/demo/pathControl_white.gif b/demo/pathControl_white.gif
new file mode 100644
index 0000000..9e95ab6
Binary files /dev/null and b/demo/pathControl_white.gif differ
diff --git a/main.js b/main.js
index f31dd2c..bdee682 100644
--- a/main.js
+++ b/main.js
@@ -1,630 +1,1225 @@
-'use strict';
-
-const obsidian = require('obsidian');
-
-// Regex constants
-const UPDATE_INTERVAL = 1000;
-
-// —— Utility: Format time and render span —— //
-class TimerRenderer {
- static render(timerData) {
- const formatted = new Date(timerData.AccumulatedTime * 1000)
- .toISOString()
- .substr(11, 8);
- const colorStyle =
- timerData.Status === 'Running' ? 'style="color: #10b981;"' : '';
- return `【⏳${formatted} 】 `;
- }
-}
-
-// —— Utility: Parse existing span data —— //
-class TimerParser {
- static parse(lineText) {
- // const spanMatch = lineText.match(/]*>/);
- const spanMatch = lineText.match(/]*>.*?<\/span>/);
- if (!spanMatch) return null;
-
- const spanTag = spanMatch[0];
- const attributesText = spanTag.substring(6, spanTag.length - 1); // 去掉 ""
-
- const attributes = {};
- const attributeList = attributesText.split(/\s+/); // 按空格分割成属性列表
- for (const attribute of attributeList) {
- if (attribute.includes('=')) {
- const [key, value] = attribute.split('=', 2);
- attributes[key] = value.replace(/"/g, '').replace(/'/g, '');
- } else {
- attributes[attribute] = true; // 处理没有值的属性(如 disabled)
- }
- }
-
- return {
- raw: spanTag,
- beforeIndex: spanMatch.index,
- afterIndex: spanMatch.index + spanTag.length,
- timerId: attributes.timerId || null,
- Status: attributes.Status || null,
- AccumulatedTime: parseInt(attributes.AccumulatedTime, 10) || 0,
- currentStartTimeStamp: parseInt(attributes.currentStartTimeStamp, 10) || null,
- lineId: parseInt(attributes.lineId, 10) || null,
- PLT: parseInt(attributes.PLT, 10) || 0,
- };
- }
-}
-
-// —— Timer lifecycle management —— //
-class TimerManager {
- constructor() {
- this.timers = new Map(); // timerId -> intervalId
- }
-
- start(timerId, tickCallback) {
- if (this.timers.has(timerId)) return;
- const id = setInterval(() => tickCallback(timerId), UPDATE_INTERVAL);
- this.timers.set(timerId, id);
- // console.log('start timerId:', timerId);
- }
-
- stop(timerId) {
- const id = this.timers.get(timerId);
- if (id) {
- clearInterval(id);
- this.timers.delete(timerId);
- // console.log('stop timerId:', timerId);
- }
- }
-
- clearAll() {
- for (const id of this.timers.values()) {
- clearInterval(id);
- }
- this.timers.clear();
- }
-}
-
-// —— Main plugin class —— //
-class TimerPlugin extends obsidian.Plugin {
- async onload() {
-
- this.manager = new TimerManager();
- // Stores context for each timerId: { editor, lineNum }
- this.timerContexts = new Map();
- this.fileFirstOpen = true;
- this.pluginLoadTime = Date.now();
-
- this.default_settings = {
- autoStopTimers: 'quit', // default value
- timerInsertLocation: 'head' // default value
- };
-
- await this.loadSettings();
-
- // 获取当前界面语言
- const currentLanguage = window.localStorage.getItem('language');
- // console.log('Current Obsidian Interface Language:', currentLanguage);
-
- if (currentLanguage === 'zh') {
- this.plugin_name = '文本块计时器';
- this.plugin_desc = '强烈建议配合快捷键使用';
- this.command_name = '启动计时器/切换计时器状态';
- this.action_paused = '暂停计时';
- this.action_continue = '继续计时';
- this.action_start = '开始计时';
- this.setting_autostop_label = '自动停止计时器';
- this.setting_autostop_desc = '哪些行为视作用户手动停止计时器';
- this.setting_never = '从不停止,除非用户手动停止';
- this.setting_quit = '仅退出Obsidian时停止,关闭文件依然后台计时';
- this.setting_close = '关闭文件时立即停止';
- this.setting_insertlocation_label = '计时器插入位置';
- this.setting_insertlocation_head = '在文本前插入';
- this.setting_insertlocation_tail = '在文本后插入';
- } else if (currentLanguage === 'zh-TW') {
- this.plugin_name = '文本塊計時器';
- this.plugin_desc = '強烈建議配合快捷鍵使用';
- this.command_name = '啟動計時器/切換計時器狀態';
- this.action_paused = '暫停計時';
- this.action_continue = '繼續計時';
- this.action_start = '開始計時';
- this.setting_autostop_label = '自動停止計時器';
- this.setting_autostop_desc = '哪些行為視為用戶手動停止計時器';
- this.setting_never = '從不停止,除非用戶手動停止';
- this.setting_quit = '僅退出Obsidian時停止,關閉文件依後台計時';
- this.setting_close = '關閉文件時立即停止';
- this.setting_insertlocation_label = '計時器插入位置';
- this.setting_insertlocation_head = '在文本前插入';
- this.setting_insertlocation_tail = '在文本後插入';
- } else if (currentLanguage === 'ja') {
- this.plugin_name = 'テキストブロックタイマー';
- this.plugin_desc = 'ショートカットキーと合わせて使うのがおすすめです';
- this.command_name = 'タイマーを始める/切替タイマ';
- this.action_paused = 'タイマーを止める';
- this.action_continue = 'タイマーを続ける';
- this.action_start = 'タイマーを始める';
- this.setting_autostop_label = 'タイマー自動停止';
- this.setting_autostop_desc = 'ユーザーが手働でタイマーを停止したものとみなします';
- this.setting_never = 'ユーザーが手動で止めない限り停止しません';
- this.setting_quit = 'Obsidianをログアウトしたときだけ停止し、ファイルを閉じたままバックグラウンドカウントします';
- this.setting_close = 'ファイルを閉じたらすぐに停止します';
- this.setting_insertlocation_label = 'タイマー挿入位置です';
- this.setting_insertlocation_head = 'テキストの前に挿入します';
- this.setting_insertlocation_tail = 'テキストの後に挿入します';
- } else if (currentLanguage === 'ko') {
- this.plugin_name = '텍스트 블록 타이머';
- this.plugin_desc = '단축키에 맞춰 사용하는 것을 권장합니다';
- this.command_name = '타이머 전환';
- this.action_paused = '타이머 일시 정지';
- this.action_continue = '타이머 계속';
- this.action_start = '타이머 시작';
- this.setting_autostop_label = '타이머 자동 정지';
- this.setting_autostop_desc = '사용자가 타이머를 수동으로 종료한 것으로 간주할 행동입니다';
- this.setting_never = '사용자가 수동으로 중지하지 않는 한 정지하지 않는다';
- this.setting_quit = 'obsidian을 종료할 때만 종료되며, 닫기 파일은 여전히 백그라운드 타임을 유지한다';
- this.setting_close = '파일을 닫을 때 즉시 중지합니다';
- this.setting_insertlocation_label = '타이머 삽입 위치';
- this.setting_insertlocation_head = '텍스트 앞에 삽입';
- this.setting_insertlocation_tail = '텍스트 뒤에 삽입';
-
- } else {
- this.plugin_name = 'Text Block Timer';
- this.plugin_desc = 'It is strongly recommened to control timer by shortcut key';
- this.command_name = 'toggle-timer';
- this.action_paused = 'Pause Timer';
- this.action_continue = 'Continue Timer';
- this.action_start = 'Start Timer';
- this.setting_autostop_label = 'Auto Stop Timers';
- this.setting_autostop_desc = 'Which actions should stop the timer automatically?';
- this.setting_never = 'Never Stop unless user stop timer manually';
- this.setting_quit = 'Auto stop when quit Obsidian, continue timing when file closed';
- this.setting_close = 'Auto stop when file closed';
- this.setting_insertlocation_label = 'Timer insert position';
- this.setting_insertlocation_head = 'Insert before text';
- this.setting_insertlocation_tail = 'Insert after text';
- }
-
- // Add to editor context menu
- this.registerEvent(
- this.app.workspace.on('editor-menu', this.onEditorMenu.bind(this))
- );
-
- // Register "Toggle timer" command
- this.addCommand({
- id: 'toggle-timer',
- name: this.command_name,
- editorCallback: (editor, view) => {
- const cursor = editor.getCursor();
- const lineNum = cursor.line;
- const lineText = editor.getLine(lineNum);
- const parsed = TimerParser.parse(lineText);
-
- if (parsed) {
- if (parsed.Status === 'Running') {
- this.handlePause(parsed, editor, lineNum);
- } else if (parsed.Status === 'Paused') {
- this.handleContinue(parsed, editor, lineNum);
- } else {
- this.handleStart(editor, lineNum);
- }
- } else {
- this.handleStart(editor, lineNum);
- }
- },
- });
-
- // 监听文件打开事件
- this.registerEvent(
- this.app.workspace.on('file-open', this.onFileOpen.bind(this))
- );
-
-
- // 注册设置
- this.addSettingTab(new TimerSettingTab(this.app, this));
-
- }
-
- onunload() {
- this.manager.clearAll();
- this.timerContexts.clear();
- }
-
- onEditorMenu(menu, editor, view) {
- const cursor = editor.getCursor();
- const lineNum = cursor.line;
- const lineText = editor.getLine(lineNum);
- const parsed = TimerParser.parse(lineText);
- const status = parsed ? parsed.Status : null;
-
- if (status === 'Running') {
- menu.addItem((item) =>
- item
- .setTitle(this.action_paused)
- .setIcon('pause')
- .onClick(() => this.handlePause(parsed, editor, lineNum))
- );
- } else if (status === 'Paused') {
- menu.addItem((item) =>
- item
- .setTitle(this.action_continue)
- .setIcon('play')
- .onClick(() => this.handleContinue(parsed, editor, lineNum))
- );
- } else {
- menu.addItem((item) =>
- item
- .setTitle(this.action_start)
- .setIcon('play')
- .onClick(() => this.handleStart(editor, lineNum))
- );
- }
- }
-
- handleStart(editor, lineNum) {
- const timerId = this.updateTimer('init', editor, lineNum);
- this.timerContexts.set(timerId, { editor, lineNum });
- this.manager.start(timerId, this.onTick.bind(this));
- this.updateTimer('continue', editor, lineNum, timerId);
- // this.timerContexts.set(timerId, { editor, lineNum });
- }
-
- handleContinue(parsed, editor, lineNum) {
- const timerId = parsed.timerId;
- this.timerContexts.set(timerId, { editor, lineNum });
- this.manager.start(timerId, this.onTick.bind(this));
- this.updateTimer('continue', editor, lineNum, timerId);
- // this.timerContexts.set(timerId, { editor, lineNum });
- }
-
- handlePause(parsed, editor, lineNum) {
- const timerId = parsed.timerId;
- this.timerContexts.set(timerId, { editor, lineNum });
- this.manager.stop(timerId);
- this.updateTimer('pause', editor, lineNum, timerId);
- // this.timerContexts.set(timerId, { editor, lineNum });
- }
-
- handleRestore(parsed, editor, lineNum) {
- const timerId = parsed.timerId;
- // 在ontick之前,先把数据恢复
- this.updateTimer('restore', editor, lineNum, timerId);
- this.timerContexts.set(timerId, { editor, lineNum });
- this.manager.start(timerId, this.onTick.bind(this));
- // this.timerContexts.set(timerId, { editor, lineNum });
- }
-
- handleForcePause(parsed, editor, lineNum) {
- const timerId = parsed.timerId;
- // this.timerContexts.set(timerId, { editor, lineNum });
- // this.manager.stop(timerId);
- this.updateTimer('forcepause', editor, lineNum, timerId);
- // this.timerContexts.set(timerId, { editor, lineNum });
- }
-
-
- onTick(timerId) {
- const ctx = this.timerContexts.get(timerId);
- if (ctx) {
- const { editor, lineNum } = ctx;
- const lineText = editor.getLine(lineNum) || '';
- if (lineText.includes(`timerId="${timerId}"`)) {
- this.updateTimer('update', editor, lineNum, timerId);
- }
- }
-
- const leaves = this.app.workspace.getLeavesOfType('markdown');
- for (const leaf of leaves) {
- if (!leaf.view || !leaf.view.editor) continue;
- const e = leaf.view.editor;
- for (let i = 0; i < e.lineCount(); i++) {
- const txt = e.getLine(i);
- if (txt.includes(`timerId="${timerId}"`)) {
- this.timerContexts.set(timerId, { editor: e, lineNum: i });
- this.updateTimer('update', e, i, timerId);
- return;
- }
- }
- }
-
- this.manager.stop(timerId);
- this.timerContexts.delete(timerId);
- }
-
- /**
- * Unified update interface
- * @param action - init | continue | pause | update
- */
- updateTimer(action, editor, lineNum, timerId = null) {
- const lineText = editor.getLine(lineNum);
- const parsed = TimerParser.parse(lineText);
- const now = Math.floor(Date.now() / 1000);
-
- let data;
- switch (action) {
- case 'init': // init:Timer初始化,TimerId为当前时间戳,初始化时为暂停状态,累计耗时=0,记录创建时的时间戳
- data = {
- timerId: Date.now().toString(),
- lineId: lineNum,
- Status: 'Paused',
- AccumulatedTime: 0,
- currentStartTimeStamp: now,
- PLT: this.pluginLoadTime,
- };
- break;
- case 'continue': // continue:暂停状态下启动: 启动瞬间的timer状态应当和暂停时状态相同,只是把status改为Running
- {
- const old = parsed || {};
- // const elapsed = old ?
- // now - old.currentStartTimeStamp : 0;
- const elapsed = 0;
- data = {
- timerId: timerId || old.timerId || Date.now().toString(),
- lineId: lineNum,
- Status: 'Running',
- AccumulatedTime:
- (old.AccumulatedTime || 0) + elapsed,
- currentStartTimeStamp: now,
- PLT: this.pluginLoadTime,
- };
- break;
- }
- case 'pause': // 逻辑应当与update一致,只是把状态改为Paused
- {
- const old = parsed;
- const elapsed = old ?
- now - old.currentStartTimeStamp : 0;
- data = {
- timerId: old.timerId,
- lineId: lineNum,
- Status: 'Paused',
- AccumulatedTime:
- (old.AccumulatedTime || 0) + elapsed,
- currentStartTimeStamp: now,
- PLT: this.pluginLoadTime,
- };
- break;
- }
- case 'update': // AccumulatedTime和currentStartTimeStamp应当更新为当前值
- {
- const old = parsed;
- const elapsed = old ?
- now - old.currentStartTimeStamp : 0;
- data = {
- timerId: old.timerId,
- lineId: lineNum,
- Status: 'Running',
- AccumulatedTime:
- (old.AccumulatedTime || 0) + elapsed,
- currentStartTimeStamp: now,
- PLT: this.pluginLoadTime,
- };
- break;
- }
- case 'restore': // 把一个有遗留数据,且状态为Running的Timer, 把ontick失效期间的时间累加到AccumulatedTime, 把currentStartTimeStamp更新为当前时间戳, 把状态改为Running
- {
- const old = parsed;
- const elapsed = old ?
- now - old.currentStartTimeStamp : 0;
- data = {
- timerId: old.timerId,
- lineId: lineNum,
- Status: 'Running',
- AccumulatedTime:
- (old.AccumulatedTime || 0) + elapsed,
- currentStartTimeStamp: now,
- PLT: this.pluginLoadTime,
- };
- break;
- }
- case 'forcepause': // forcepause: 强制暂停,一切保留原样,只是状态强制改写为暂停
- {
- const old = parsed;
- data = {
- timerId: old.timerId,
- lineId: lineNum,
- Status: 'Paused',
- AccumulatedTime:
- (old.AccumulatedTime || 0),
- currentStartTimeStamp: now,
- PLT: this.pluginLoadTime,
- };
- break;
- }
- default:
- throw new Error('Unknown action ' + action);
- }
-
- // Render new span
- const newSpan = TimerRenderer.render(data);
-
- // Determine insertion position:
- // 1. Use existing span's position if present
- // 2. Otherwise calculate based on indentation and checkboxes
- let before, after;
- if (parsed) {
- before = parsed.beforeIndex;
- after = parsed.afterIndex;
- } else if (this.settings.timerInsertLocation === 'head') {
-
- // ol+checkbox
- const olcheckboxMatch = /(^\s*#*\d\.\s\[.?\]\s)/.exec(lineText);
- const olcheckboxLen = olcheckboxMatch ? olcheckboxMatch[0].length : 0;
- // ul+checkbox
- const ulcheckboxMatch = /(^\s*#*[-/+/*]\s\[.?\]\s)/.exec(lineText);
- const ulcheckboxLen = ulcheckboxMatch ? ulcheckboxMatch[0].length : 0;
- // ol
- const orderedListMatch = /(^\s*#*\d+\.\s)/.exec(lineText);
- const orderedListLen = orderedListMatch ? orderedListMatch[0].length : 0;
- // ul
- const ulMatch = /(^\s*#*[-/+/*]\s)/.exec(lineText);
- const ulLen = ulMatch ? ulMatch[0].length : 0;
- // header
- const headerMatch = /(^\s*#+\s)/.exec(lineText);
- const headerLen = headerMatch ? headerMatch[0].length : 0;
-
-
- if (olcheckboxLen > 0) {
- var textleft
- textleft = lineText.slice(olcheckboxLen);
- // console.log('textleft after olcheckbox:', textleft);
- before = after = olcheckboxLen;
- } else if (ulcheckboxLen > 0) {
- var textleft
- textleft = lineText.slice(ulcheckboxLen);
- // console.log('textleft after checkbox:', textleft);
- before = after = ulcheckboxLen;
- } else if (orderedListLen > 0) {
- var textleft
- textleft = lineText.slice(orderedListLen);
- // console.log('textleft after ol:', textleft);
- before = after = orderedListLen;
- } else if (ulLen > 0) {
- var textleft
- textleft = lineText.slice(ulLen);
- // console.log('textleft after ul:', textleft);
- before = after = ulLen;
- } else if (headerLen > 0) {
- var textleft
- textleft = lineText.slice(headerLen);
- // console.log('textleft after header:', textleft);
- before = after = headerLen;
- } else {
- before = after = 0;
- }
- before = before ? before : 0;
- after = after ? after : 0;
- } else if (this.settings.timerInsertLocation === 'tail') {
- before = after = lineText.length;
- }
-
- editor.replaceRange(
- newSpan, { line: lineNum, ch: before }, { line: lineNum, ch: after }
- );
- // console.log('lineNum:', lineNum, 'before:', before, 'after:', after);
- return data.timerId;
- }
-
- // 用于标记是否是第一次监听到onFileOpen事件
- onFileOpen(event) {
- if (this.fileFirstOpen) {
- // 如果是第一次监听到onFileOpen事件,则针对每个leaf都执行一次恢复计时器的操作
- const leaves = this.app.workspace.getLeavesOfType('markdown');
- for (const leaf of leaves) {
- if (leaf.view && leaf.view.editor) {
- const editor = leaf.view.editor;
- this.restoreTimers(editor);
- }
- }
- this.fileFirstOpen = false; // 标记为已处理过第一次监听
- } else {
- // 如果不是第一次监听,则按照原来的逻辑处理特定的文件
- const leaves = this.app.workspace.getLeavesOfType('markdown');
- for (const leaf of leaves) {
- if (leaf.view && leaf.view.file === event) {
- const editor = leaf.view.editor;
- if (editor) {
- // 精简逻辑,可能有问题
- // this.restoreTimers(editor);
-
- // 启动一个定时器,每隔1秒检查文件行数
- const checkInterval = setInterval(() => {
- const lineCount = editor.lineCount();
- if (lineCount > 1) {
- // 如果行数大于1,则执行Restore逻辑
- this.restoreTimers(editor);
- clearInterval(checkInterval); // 删除定时器
- }
- }, 100);
-
- break;
- }
- }
- }
- }
- }
-
- // 新增Restore逻辑
- restoreTimers(editor) {
- const lineCount = editor.lineCount();
- for (let i = 0; i < lineCount; i++) {
- const lineText = editor.getLine(i);
- const parsed = TimerParser.parse(lineText);
- if (parsed && parsed.Status === 'Running' && !this.manager.timers.has(parsed.timerId)) {
-
- if (this.settings.autoStopTimers === 'never') {
- // 对于状态为Running且timerId不在this.manager中的计时器,执行暂停和继续操作(立即恢复)
- this.handleRestore(parsed, editor, i);
- } else if (this.settings.autoStopTimers === 'quit') {
- // 对于状态为Running且timerId不在this.manager中的计时器,如果计时器的本次启动时间 等于 当前插件启动时间,则继续,否则强行暂停
- // console.log('PLT:', parsed.PLT, 'pluginLoadTime:', this.pluginLoadTime);
- if (parsed.PLT === this.pluginLoadTime) {
- this.handleRestore(parsed, editor, i);
- } else {
- this.handleForcePause(parsed, editor, i);
- }
- } else if (this.settings.autoStopTimers === 'close') {
- // 对于状态为Running且timerId不在this.manager中的计时器,执行暂停操作
- this.handleForcePause(parsed, editor, i);
- }
-
- }
- }
- }
-
- // 设置项相关方法
- async loadSettings() {
- this.settings = Object.assign({}, this.default_settings, await this.loadData());
- }
-
-
- async saveSettings() {
- await this.saveData(this.settings);
- }
-}
-
-
-
-class TimerSettingTab extends obsidian.PluginSettingTab {
- constructor(app, plugin) {
- super(app, plugin);
- this.plugin = plugin;
- }
-
- display() {
- const { containerEl } = this;
- containerEl.empty();
- containerEl.createEl('h2', { text: this.plugin.plugin_name });
- containerEl.createEl('h3', { text: this.plugin.plugin_desc });
-
- new obsidian.Setting(containerEl)
- .setName(this.plugin.setting_autostop_label)
- .setDesc(this.plugin.setting_autostop_desc)
- .addDropdown(dropdown => {
- dropdown
- .addOption('never', this.plugin.setting_never)
- .addOption('quit', this.plugin.setting_quit)
- .addOption('close', this.plugin.setting_close)
- .setValue(this.plugin.settings.autoStopTimers)
- .onChange(async(value) => {
- this.plugin.settings.autoStopTimers = value;
- await this.plugin.saveSettings();
- });
- });
- new obsidian.Setting(containerEl)
- .setName(this.plugin.setting_insertlocation_label)
- .addDropdown(dropdown => {
- dropdown
- .addOption('head', this.plugin.setting_insertlocation_head)
- .addOption('tail', this.plugin.setting_insertlocation_tail)
- .setValue(this.plugin.settings.timerInsertLocation)
- .onChange(async(value) => {
- this.plugin.settings.timerInsertLocation = value;
- await this.plugin.saveSettings();
- });
- });
-
-
-
-
- ;
- }
-}
-
-module.exports = TimerPlugin;
\ No newline at end of file
+'use strict';
+
+const obsidian = require('obsidian');
+
+const { ViewPlugin, EditorView } = require('@codemirror/view');
+
+// Regex constants
+const UPDATE_INTERVAL = 1000;
+// —— Checkbox regexes —— //
+const ORDERED_LIST = /(^\s*#*\d+\.\s)/
+const UNORDERED_LIST = /(^\s*#*[-/+/*]\s)/
+const HEADER = /(^\s*#+\s)/
+const POTENTIAL_CHECKBOX_REGEX = /^(?:(\s*)(?:[-+*]|\d+\.)\s+)\[(.{0,2})\]/
+const CHECKBOX_REGEX = /^(?:(\s*)(?:[-+*]|\d+\.)\s+)\[(.{1})\]\s+/
+
+
+
+
+// —— Utility: Format time and render span —— //
+class TimerRenderer {
+ static render(timerData) {
+ const formatted = new Date(timerData.AccumulatedTime * 1000)
+ .toISOString()
+ .substr(11, 8);
+ const colorStyle =
+ timerData.Status === 'Running' ? 'style="color: #10b981;"' : '';
+ return `【⏳${formatted} 】`;
+ }
+}
+
+// —— Utility: Parse existing span data —— //
+class TimerParser {
+ static parse(lineText) {
+ // const spanMatch = lineText.match(/]*>/);
+ const spanMatch = lineText.match(/]*>.*?<\/span>/);
+ if (!spanMatch) return null;
+
+ const spanTag = spanMatch[0];
+ const attributesText = spanTag.substring(6, spanTag.length - 1).replace(/data-/g, ''); //remove "";
+
+
+ const attributes = {};
+ const attributeList = attributesText.split(/\s+/); //split by space
+ for (const attribute of attributeList) {
+ if (attribute.includes('=')) {
+ const [key, value] = attribute.split('=', 2);
+ attributes[key] = value.replace(/"/g, '').replace(/'/g, '');
+ } else {
+ attributes[attribute] = true; //handle attributes without value (such as disabled)
+ }
+ }
+
+ return {
+ raw: spanTag,
+ beforeIndex: spanMatch.index,
+ afterIndex: spanMatch.index + spanTag.length,
+ timerId: attributes.timerId || null,
+ Status: attributes.Status || null,
+ AccumulatedTime: parseInt(attributes.AccumulatedTime, 10) || 0,
+ currentStartTimeStamp: parseInt(attributes.currentStartTimeStamp, 10) || null,
+ lineId: parseInt(attributes.lineId, 10) || null,
+ PLT: parseInt(attributes.PLT, 10) || 0,
+ };
+ }
+}
+
+// —— Timer lifecycle management —— //
+class TimerManager {
+ constructor() {
+ this.timers = new Map(); // timerId -> intervalId
+ }
+
+ start(timerId, tickCallback) {
+ if (this.timers.has(timerId)) return;
+ const id = setInterval(() => tickCallback(timerId), UPDATE_INTERVAL);
+ this.timers.set(timerId, id);
+ // console.log('start timerId:', timerId);
+ }
+
+ stop(timerId) {
+ const id = this.timers.get(timerId);
+ if (id) {
+ clearInterval(id);
+ this.timers.delete(timerId);
+ // console.log('stop timerId:', timerId);
+ }
+ }
+
+ clearAll() {
+ for (const id of this.timers.values()) {
+ clearInterval(id);
+ }
+ this.timers.clear();
+ }
+}
+
+
+
+// —— Main plugin class —— //
+class TimerPlugin extends obsidian.Plugin {
+ async onload() {
+
+ this.manager = new TimerManager();
+ // Stores context for each timerId: { editor, lineNum }
+ this.timerContexts = new Map();
+ this.fileFirstOpen = true;
+ this.pluginLoadTime = Date.now();
+
+ this.default_settings = {
+ autoStopTimers: 'quit', // default value
+ timerInsertLocation: 'head', // default value
+ enableCheckboxToTimer: true, // enable checkbox to timer by default
+ runningCheckboxState: '/', // enable running checkbox state by default
+ pausedCheckboxState: '-xX', // enable paused checkbox state by default
+ checkboxToTimerPathRestriction: 'disable', // disable checkbox to timer path restriction by default
+ pathRestrictionPaths: [] // disable path restriction by default
+ };
+
+ await this.loadSettings();
+
+ // Get current interface language
+ const currentLanguage = window.localStorage.getItem('language');
+ // console.log('Current Obsidian Interface Language:', currentLanguage);
+
+ if (currentLanguage === 'zh') {
+ this.command_name = '启动计时器/切换计时器状态';
+ this.action_paused = '暂停计时';
+ this.action_continue = '继续计时';
+ this.action_start = '开始计时';
+ } else if (currentLanguage === 'zh-TW') {
+ this.command_name = '啟動計時器/切換計時器狀態';
+ this.action_paused = '暫停計時';
+ this.action_continue = '繼續計時';
+ this.action_start = '開始計時';
+ } else if (currentLanguage === 'ja') {
+ this.command_name = 'タイマーを始める/切替タイマ';
+ this.action_paused = 'タイマーを止める';
+ this.action_continue = 'タイマーを続ける';
+ this.action_start = 'タイマーを始める';
+ } else if (currentLanguage === 'ko') {
+ this.command_name = '타이머 전환';
+ this.action_paused = '타이머 일시 정지';
+ this.action_continue = '타이머 계속';
+ this.action_start = '타이머 시작';
+ } else {
+ this.command_name = 'toggle-timer';
+ this.action_paused = 'Pause Timer';
+ this.action_continue = 'Continue Timer';
+ this.action_start = 'Start Timer';
+ }
+
+ // Add to editor context menu
+ this.registerEvent(
+ this.app.workspace.on('editor-menu', this.onEditorMenu.bind(this))
+ );
+
+ // Register "Toggle timer" command
+ this.addCommand({
+ id: 'toggle-timer',
+ name: this.command_name,
+ editorCallback: (editor, view) => {
+ const cursor = editor.getCursor();
+ const lineNum = cursor.line;
+ const lineText = editor.getLine(lineNum);
+ // console.log('lineText:', lineText);
+ const parsed = TimerParser.parse(lineText);
+
+ if (parsed) {
+ if (parsed.Status === 'Running') {
+ this.handlePause(parsed, editor, lineNum);
+ } else if (parsed.Status === 'Paused') {
+ this.handleContinue(parsed, editor, lineNum);
+ } else {
+ this.handleStart(editor, lineNum);
+ }
+ } else {
+ this.handleStart(editor, lineNum);
+ }
+ },
+ });
+
+ // Listen to file open event
+ this.registerEvent(
+ this.app.workspace.on('file-open', this.onFileOpen.bind(this))
+ );
+
+
+ // Register settings
+ this.addSettingTab(new TimerSettingTab(this.app, this));
+
+ // Judge enableCheckboxToTimer
+
+ this.registerEditorExtension(
+ EditorView.updateListener.of((update) => {
+ // const file = this.app.workspace.getActiveFile();
+ // console.log(file.path)
+ // console.log('enableCheckboxToTimer:', this.settings.enableCheckboxToTimer);
+ // console.log('checkPathRestriction:', this.checkPathRestriction());
+ if (update.docChanged && this.settings.enableCheckboxToTimer && this.checkPathRestriction()) {
+ const view = this.app.workspace.getActiveViewOfType(obsidian.MarkdownView);
+ if (!view) return;
+
+ const oldDoc = update.startState.doc;
+ const newDoc = update.state.doc;
+
+ // Directly handle changes in iterChanges iterator
+ update.changes.iterChanges((oldFrom, oldTo, newFrom, newTo, inserted) => {
+ // Analyze text in old document before changes
+ const oldStartLine = oldDoc.lineAt(oldFrom).number;
+ const oldEndLine = oldDoc.lineAt(oldTo).number;
+ const newStartLine = newDoc.lineAt(newFrom).number;
+ const newEndLine = newDoc.lineAt(newTo).number;
+
+ // If the maximum value of the four values is greater than the minimum value, directly return
+ if (Math.max(oldStartLine, oldEndLine, newStartLine, newEndLine) !== Math.min(oldStartLine, oldEndLine, newStartLine, newEndLine)) return;
+ const lineNum = newStartLine - 1
+
+ const oldLineText = oldDoc.lineAt(oldFrom).text;
+ const newLineText = newDoc.lineAt(newFrom).text;
+ // console.log('oldLineText:', oldLineText);
+ // console.log('newLineText:', newLineText);
+
+
+ // If oldLineText doesn't satisfy POTENTIAL_CHECKBOX_REGEX or newLineText doesn't satisfy CHECKBOX_REGEX
+ if (!POTENTIAL_CHECKBOX_REGEX.test(oldLineText) || !CHECKBOX_REGEX.test(newLineText)) return;
+
+ // Based on strict regular expressions, parse oldLineText and newLineText to get checkboxState
+ const oldCheckboxState = oldLineText.match(CHECKBOX_REGEX) ? oldLineText.match(CHECKBOX_REGEX)[2] : undefined // undefined or 1 symbol, ?oldLineText.match(CHECKBOX_REGEX)[2]:'';
+ const newCheckboxState = newLineText.match(CHECKBOX_REGEX)[2];
+
+ // console.log('oldCheckboxState:', oldCheckboxState);
+ // console.log('newCheckboxState:', newCheckboxState);
+
+ if (!this.settings.runningCheckboxState.includes(oldCheckboxState) && this.settings.runningCheckboxState.includes(newCheckboxState)) {
+ // parse newLineText,handleStart if there's no timer,handleContinue if there's timer and status is paused
+ // console.log('parsedLineText:', newLineText);
+ const parsed = TimerParser.parse(newLineText);
+ if (!parsed) {
+ this.handleStart(view.editor, lineNum);
+ return;
+ } else if (parsed.Status === 'Paused') {
+ this.handleContinue(parsed, view.editor, lineNum);
+ }
+ }
+
+ if (!this.settings.pausedCheckboxState.includes(oldCheckboxState) && this.settings.pausedCheckboxState.includes(newCheckboxState)) {
+ // parse newLineText,diretcly return if there's no timer,handlePause if there's timer and status is running
+ const parsed = TimerParser.parse(newLineText);
+ if (!parsed) {
+ return;
+ } else if (parsed.Status === 'Running') {
+ this.handlePause(parsed, view.editor, lineNum);
+ }
+ }
+
+ });
+ }
+ })
+ );
+
+ }
+
+ onunload() {
+ this.manager.clearAll();
+ this.timerContexts.clear();
+ }
+
+ onEditorMenu(menu, editor, view) {
+ const cursor = editor.getCursor();
+ const lineNum = cursor.line;
+ const lineText = editor.getLine(lineNum);
+ const parsed = TimerParser.parse(lineText);
+ const status = parsed ? parsed.Status : null;
+
+ if (status === 'Running') {
+ menu.addItem((item) =>
+ item
+ .setTitle(this.action_paused)
+ .setIcon('pause')
+ .onClick(() => this.handlePause(parsed, editor, lineNum))
+ );
+ } else if (status === 'Paused') {
+ menu.addItem((item) =>
+ item
+ .setTitle(this.action_continue)
+ .setIcon('play')
+ .onClick(() => this.handleContinue(parsed, editor, lineNum))
+ );
+ } else {
+ menu.addItem((item) =>
+ item
+ .setTitle(this.action_start)
+ .setIcon('play')
+ .onClick(() => this.handleStart(editor, lineNum))
+ );
+ }
+ }
+
+ checkPathRestriction() {
+ const restriction = this.settings.checkboxToTimerPathRestriction;
+ const paths = this.settings.pathRestrictionPaths;
+
+ if (restriction === 'disable') return true;
+
+ // Get File Path
+ const currentFile = this.app.workspace.getActiveFile();
+ if (!currentFile) return false;
+
+ const currentFilePath = currentFile.path;
+
+ if (restriction === 'whitelist') {
+ for (const path of paths) {
+ // console.log(path)
+ // console.log(currentFilePath)
+ // console.log(currentFilePath.startsWith(path))
+ if (currentFilePath.startsWith(path))
+ return true;
+ }
+ return false
+ } else if (restriction === 'blacklist') {
+ for (const path of paths) {
+ // console.log(path)
+ // console.log(currentFilePath)
+ // console.log(currentFilePath.startsWith(path))
+ if (currentFilePath.startsWith(path)) {
+ return false;
+ }
+ }
+ return true
+ } else {
+ return false
+ }
+ }
+
+
+
+ handleStart(editor, lineNum) {
+ const timerId = this.updateTimer('init', editor, lineNum);
+ this.timerContexts.set(timerId, { editor, lineNum });
+ this.manager.start(timerId, this.onTick.bind(this));
+ }
+
+ handleContinue(parsed, editor, lineNum) {
+ const timerId = parsed.timerId;
+ this.timerContexts.set(timerId, { editor, lineNum });
+ this.manager.start(timerId, this.onTick.bind(this));
+ this.updateTimer('continue', editor, lineNum, parsed, timerId);
+ }
+
+ handlePause(parsed, editor, lineNum) {
+ const timerId = parsed.timerId;
+ this.timerContexts.set(timerId, { editor, lineNum });
+ this.manager.stop(timerId);
+ this.updateTimer('pause', editor, lineNum, parsed, timerId);
+ }
+
+ handleRestore(parsed, editor, lineNum) {
+ const timerId = parsed.timerId;
+ this.updateTimer('restore', editor, lineNum, parsed, timerId);
+ this.timerContexts.set(timerId, { editor, lineNum });
+ this.manager.start(timerId, this.onTick.bind(this));
+ }
+
+ handleForcePause(parsed, editor, lineNum) {
+ const timerId = parsed.timerId;
+ this.updateTimer('forcepause', editor, lineNum, parsed, timerId);
+ }
+
+
+ onTick(timerId) {
+ const ctx = this.timerContexts.get(timerId);
+ if (ctx) {
+ const { editor, lineNum } = ctx;
+ const lineText = editor.getLine(lineNum) || '';
+ if (lineText.includes(`timerId="${timerId}"`)) {
+ const parsed = TimerParser.parse(lineText);
+ this.updateTimer('update', editor, lineNum, parsed, timerId);
+ }
+ }
+
+ const leaves = this.app.workspace.getLeavesOfType('markdown');
+ for (const leaf of leaves) {
+ if (!leaf.view || !leaf.view.editor) continue;
+ const e = leaf.view.editor;
+ for (let i = 0; i < e.lineCount(); i++) {
+ const txt = e.getLine(i);
+ if (txt.includes(`timerId="${timerId}"`)) {
+ this.timerContexts.set(timerId, { editor: e, lineNum: i });
+ const parsed = TimerParser.parse(txt);
+ this.updateTimer('update', e, i, parsed, timerId);
+ return;
+ }
+ }
+ }
+
+ this.manager.stop(timerId);
+ this.timerContexts.delete(timerId);
+ }
+
+ /**
+ * Unified update interface
+ * @param action - init | continue | pause | update
+ */
+ updateTimer(action, editor, lineNum, parsed = null, timerId = null) {
+
+ var lineText = editor.getLine(lineNum);
+ // console.log('lineNum:', lineNum, 'lineText:', lineText);
+
+ var parsed = parsed ? parsed : TimerParser.parse(lineText);
+ const now = Math.floor(Date.now() / 1000);
+
+ let data;
+ switch (action) {
+ case 'init': //
+ data = {
+ timerId: Date.now().toString(),
+ lineId: lineNum,
+ Status: 'Running',
+ AccumulatedTime: 0,
+ currentStartTimeStamp: now,
+ PLT: this.pluginLoadTime,
+ };
+ break;
+ case 'continue':
+ {
+ const old = parsed || {};
+ const elapsed = 0;
+ data = {
+ timerId: timerId || old.timerId || Date.now().toString(),
+ lineId: lineNum,
+ Status: 'Running',
+ AccumulatedTime:
+ (old.AccumulatedTime || 0) + elapsed,
+ currentStartTimeStamp: now,
+ PLT: this.pluginLoadTime,
+ };
+ break;
+ }
+ case 'pause':
+ {
+ const old = parsed;
+ const elapsed = old ?
+ now - old.currentStartTimeStamp : 0;
+ data = {
+ timerId: old.timerId,
+ lineId: lineNum,
+ Status: 'Paused',
+ AccumulatedTime:
+ (old.AccumulatedTime || 0) + elapsed,
+ currentStartTimeStamp: now,
+ PLT: this.pluginLoadTime,
+ };
+ break;
+ }
+ case 'update':
+ {
+ const old = parsed;
+ const elapsed = old ?
+ now - old.currentStartTimeStamp : 0;
+ data = {
+ timerId: old.timerId,
+ lineId: lineNum,
+ Status: 'Running',
+ AccumulatedTime:
+ (old.AccumulatedTime || 0) + elapsed,
+ currentStartTimeStamp: now,
+ PLT: this.pluginLoadTime,
+ };
+ break;
+ }
+ case 'restore':
+ {
+ const old = parsed;
+ const elapsed = old ?
+ now - old.currentStartTimeStamp : 0;
+ data = {
+ timerId: old.timerId,
+ lineId: lineNum,
+ Status: 'Running',
+ AccumulatedTime:
+ (old.AccumulatedTime || 0) + elapsed,
+ currentStartTimeStamp: now,
+ PLT: this.pluginLoadTime,
+ };
+ break;
+ }
+ case 'forcepause':
+ {
+ const old = parsed;
+ data = {
+ timerId: old.timerId,
+ lineId: lineNum,
+ Status: 'Paused',
+ AccumulatedTime:
+ (old.AccumulatedTime || 0),
+ currentStartTimeStamp: now,
+ PLT: this.pluginLoadTime,
+ };
+ break;
+ }
+ default:
+ throw new Error('Unknown action ' + action);
+ }
+
+ // Render new span
+ var newSpan = TimerRenderer.render(data);
+
+ // in init case, add space before and after the newSpan to avoid explosion of raw html elements
+ if (action === 'init') {
+ newSpan = ' ' + newSpan + ' ';
+ }
+
+ // Determine insertion position:
+ // 1. Use existing span's position if present
+ // 2. Otherwise calculate based on indentation and checkboxes
+ let before, after;
+ if (parsed) {
+ before = parsed.beforeIndex;
+ after = parsed.afterIndex;
+ } else if (this.settings.timerInsertLocation === 'head') {
+
+ // checkbox
+ const checkboxMatch = CHECKBOX_REGEX.exec(lineText);
+ const checkboxLen = checkboxMatch ? checkboxMatch[0].length : 0;
+ // ol
+ const orderedListMatch = ORDERED_LIST.exec(lineText);
+ const orderedListLen = orderedListMatch ? orderedListMatch[0].length : 0;
+ // ul
+ const ulMatch = UNORDERED_LIST.exec(lineText);
+ const ulLen = ulMatch ? ulMatch[0].length : 0;
+ // header
+ const headerMatch = HEADER.exec(lineText);
+ const headerLen = headerMatch ? headerMatch[0].length : 0;
+
+ if (checkboxLen > 0) {
+ var textleft = lineText.slice(checkboxLen);
+ // console.log('textleft after olcheckbox:', textleft);
+ before = after = checkboxLen;
+ } else if (orderedListLen > 0) {
+ var textleft = lineText.slice(orderedListLen);
+ // console.log('textleft after ol:', textleft);
+ before = after = orderedListLen;
+ } else if (ulLen > 0) {
+ var textleft = lineText.slice(ulLen);
+ // console.log('textleft after ul:', textleft);
+ before = after = ulLen;
+ } else if (headerLen > 0) {
+ var textleft = lineText.slice(headerLen);
+ // console.log('textleft after header:', textleft);
+ before = after = headerLen;
+ } else {
+ before = after = 0;
+ }
+ before = before ? before : 0;
+ after = after ? after : 0;
+ } else if (this.settings.timerInsertLocation === 'tail') {
+ before = after = lineText.length;
+ }
+
+ // console.log('lineNum:', lineNum, 'before:', before, 'after:', after);
+ editor.replaceRange(
+ newSpan, { line: lineNum, ch: before }, { line: lineNum, ch: after }
+ );
+ // console.log('lineNum:', lineNum, 'before:', before, 'after:', after);
+ return data.timerId;
+ }
+
+ onFileOpen(event) {
+ if (this.fileFirstOpen) {
+ // If it is the first time onFileOpen event is listened, for each leaf, execute restore timers
+ const leaves = this.app.workspace.getLeavesOfType('markdown');
+ for (const leaf of leaves) {
+ if (leaf.view && leaf.view.editor) {
+ const editor = leaf.view.editor;
+ this.restoreTimers(editor);
+ }
+ }
+ this.fileFirstOpen = false;
+ } else {
+ // If it is not the first time onFileOpen event is listened, for each leaf, execute restore timers
+ const leaves = this.app.workspace.getLeavesOfType('markdown');
+ for (const leaf of leaves) {
+ if (leaf.view && leaf.view.file === event) {
+ const editor = leaf.view.editor;
+ if (editor) {
+ // this.restoreTimers(editor);
+
+ // start a timer, every 1 second, check line count
+ const checkInterval = setInterval(() => {
+ const lineCount = editor.lineCount();
+ if (lineCount > 1) {
+ // if line count is greater than 1, execute restore logic
+ this.restoreTimers(editor);
+ clearInterval(checkInterval);
+ }
+ }, 100);
+
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ restoreTimers(editor) {
+ const lineCount = editor.lineCount();
+ for (let i = 0; i < lineCount; i++) {
+ const lineText = editor.getLine(i);
+ const parsed = TimerParser.parse(lineText);
+ if (parsed && parsed.Status === 'Running' && !this.manager.timers.has(parsed.timerId)) {
+
+ if (this.settings.autoStopTimers === 'never') {
+ // For timers with status Running and timerId not in this.manager, execute pause and continue operation (immediate recovery)
+ this.handleRestore(parsed, editor, i);
+ } else if (this.settings.autoStopTimers === 'quit') {
+ // For timers with status Running and timerId not in this.manager, if timer's startup time is equal to current plugin startup time, continue, otherwise force pause
+ // console.log('PLT:', parsed.PLT, 'pluginLoadTime:', this.pluginLoadTime);
+ if (parsed.PLT === this.pluginLoadTime) {
+ this.handleRestore(parsed, editor, i);
+ } else {
+ this.handleForcePause(parsed, editor, i);
+ }
+ } else if (this.settings.autoStopTimers === 'close') {
+ // For timers with status Running and timerId not in this.manager, execute pause operation
+ this.handleForcePause(parsed, editor, i);
+ }
+
+ }
+ }
+ }
+
+ async loadSettings() {
+ this.settings = Object.assign({}, this.default_settings, await this.loadData());
+ }
+
+
+ async saveSettings() {
+ await this.saveData(this.settings);
+ }
+}
+
+class TimerSettingTab extends obsidian.PluginSettingTab {
+ constructor(app, plugin) {
+ super(app, plugin);
+ this.plugin = plugin;
+ }
+
+
+ processPath(filePath) {
+ if (filePath.endsWith('/')) {
+ return filePath;
+ }
+ const pathSegments = filePath.split('/');
+ const lastSegment = pathSegments[pathSegments.length - 1];
+ if (lastSegment.includes('.')) {
+ return filePath;
+ }
+ return filePath + '/';
+ }
+
+
+ display() {
+ const { containerEl } = this;
+ containerEl.empty();
+
+ // Load current language settings
+ const currentLanguage = window.localStorage.getItem('language') || 'en';
+ let lang;
+ // Simplified Chinese
+ if (currentLanguage === 'zh') {
+ lang = {
+ name: '文本块计时器',
+ desc: '强烈建议配合快捷键使用',
+ tutorial: '更多图片和视频教程,请访问:',
+ askforvote: '如果你喜欢这款插件,请为我的Github项目点个Star🌟',
+ issue: '如果有任何问题或建议,可以在Github项目中提出Issue,并注明使用的插件版本和复现步骤',
+ sections: {
+ basic: {
+ name: '通用设置',
+ },
+ bycommand: {
+ name: '通过命令行控制计时器',
+ desc: '强烈建议给命令`启动计时器/切换计时器状态`添加一个快捷键,并使用快捷键控制计时器'
+ },
+ byeditormenu: {
+ name: '通过右键菜单控制计时器',
+ desc: '你可以通过右键菜单快速体验计时器功能,但并不推荐这种使用方式(尤其是Mac系统)'
+ },
+ bycheckbox: {
+ name: '通过任务状态自动控制计时器',
+ desc: '你可以在切换任务状态(复选框类型)的同时,自动生成或更新计时器,对Tasks Plugin(任务插件)用户友好'
+ },
+ },
+ autostop: {
+ name: '自动停止计时器',
+ desc: '哪些行为视作用户手动停止计时器',
+ choice: {
+ never: '从不停止,除非用户手动停止',
+ quit: '仅退出 Obsidian 时停止,关闭文件依然后台计时',
+ close: '关闭文件时立即停止',
+ }
+ },
+ insertlocation: {
+ name: '计时器插入位置',
+ desc: '配合Day Planner插件使用时,推荐在文本后插入',
+ choice: {
+ head: '在文本前插入',
+ tail: '在文本后插入',
+ }
+ },
+ enableCheckboxToTimer: {
+ name: '使用任务状态控制计时器',
+ desc: '启用此功能后,你可以通过更改任务框的状态自动控制计时器的启动、暂停、继续',
+ runningSymbolStr: {
+ name: '触发计时器开始/继续的任务状态符号',
+ desc: '任务状态符号应为合法的单个字符,如 /, 如希望多种符号都能触发计时器开始/继续,请直接把这些字符连接起来,如/>+'
+ },
+ pausedSymbolStr: {
+ name: '触发计时器暂停的任务状态符号',
+ desc: '任务状态符号应为合法的单个字符,如 x, 如希望多种符号都能触发计时器暂停,请直接把这些字符连接起来,如xX-'
+ },
+ pathControl: {
+ name: '是否启用路径控制',
+ desc: '是否需要限制在哪些文件夹内才能用任务状态控制计时器,注意:当路径直接指向文件时,必须显式声明文件后缀。如 `日记/2025`是个目录,`日记/20250101.md`是文件。',
+ choice: {
+ disable: '不启用路径控制,vault下所有文件都可以用任务状态控制计时器',
+ whitelist: '启用白名单路径控制,只有白名单路径中的文件才能用任务状态控制计时器',
+ blacklist: '启用黑名单路径控制,只有黑名单路径以外的文件才能用任务状态控制计时器',
+ }
+ }
+ }
+ };
+ }
+ // Traditional Chinese
+ else if (currentLanguage === 'zh-TW') {
+ lang = {
+ name: '文本塊計時器',
+ desc: '強烈建議配合快捷鍵使用',
+ tutorial: '更多圖片和視頻教程,請訪問:',
+ askforvote: '如果你喜歡這個插件,請為我的Github專案點個Star🌟',
+ issue: '如果有任何問題或建議,請在Github專案中提出Issue,並註明使用的插件版本和複現步驟',
+ sections: {
+ basic: {
+ name: '通用設定',
+ },
+ bycommand: {
+ name: '通過命令列控制計時器',
+ desc: '強烈建議給命令列『啟動計時器/切換計時器狀態』添加一個快捷鍵,並使用快捷鍵控制計時器'
+ },
+ byeditormenu: {
+ name: '通過右鍵選單控制計時器',
+ desc: '在目標文本區塊處右鍵下拉選單,點擊計時器功能按鈕'
+ },
+ bycheckbox: {
+ name: '通過任務狀態自動控制計時器',
+ desc: '你可以在切換任務狀態的同時,自动生成或更新計時器,對Tasks Plugin(任務插件)用戶友好'
+ },
+ },
+ autostop: {
+ name: '自動停止計時器',
+ desc: '哪些行為視作用戶手動停止計時器',
+ choice: {
+ never: '從不停止,除非用戶手動停止',
+ quit: '僅退出 Obsidian 時停止,關閉文件依然後台計時',
+ close: '關閉文件時立即停止',
+ }
+ },
+ insertlocation: {
+ name: '計時器插入位置',
+ desc: '與 Day Planner 插件搭配使用時,建議在文本後插入',
+ choice: {
+ head: '在文本前插入',
+ tail: '在文本後插入',
+ }
+ },
+ enableCheckboxToTimer: {
+ name: '使用任務狀態控制計時器',
+ desc: '啟用此功能後,你可以通過更改任務框的狀態自動控制計時器的啟動、暫停、繼續',
+ runningSymbolStr: {
+ name: '觸發計時器開始/繼續的任務狀態符號',
+ desc: '任務狀態符號應為合法的單個字符,如 /, 如希望多種符號都能觸發計時器開始/继续,請直接把这些符號連接起來,如/>+'
+ },
+ pausedSymbolStr: {
+ name: '觸發計時器暫停的任務狀態符號',
+ desc: '任務狀態符號應為合法的單個字符,如 x, 如希望多種符號都能觸發計時器暫停,請直接把这些符號連接起來,如xX-'
+ },
+ pathControl: {
+ name: '是否啟用路徑控制',
+ desc: '是否需要限制在哪些路徑下才能用任務狀態控制計時器,註意:當路徑直接指向文件時,必須顯式聲明文件後綴。如 `日記/2025`是個目錄,`日記/20250101.md`是文件。',
+ choice: {
+ disable: '不啟用路徑控制,vault下所有文件都可以用任務狀態控制計時器',
+ whitelist: '啟用白名單路徑控制,只有白名單路徑中的文件才能用任務狀態控制計時器',
+ blacklist: '啟用黑名單路徑控制,只有黑名單路徑以外的文件才能用任務狀態控制計時器',
+ }
+ }
+ }
+ };
+ }
+ // Japanese
+ else if (currentLanguage === 'ja') {
+ lang = {
+ name: 'テキストブロックタイマー',
+ desc: 'ストップウォッチを追加して時間の消費を追跡します',
+ tutorial: '画像や動画のチュートリアルはこちら:',
+ askforvote: 'このプラグインが気に入ったら、GitHub プロジェクトに ⭐ をお願いします!',
+ issue: '問題や提案があれば、GitHub プロジェクトに Issue を立ててください。プラグインのバージョンと再現手順を明記してください。',
+ sections: {
+ basic: { name: '汎用設定です' },
+ bycommand: {
+ name: 'コマンドでタイマーを制御',
+ desc: 'ショートカットを toggle-timer コマンドに割り当てることを強く推奨します。'
+ },
+ byeditormenu: {
+ name: '右クリックメニューでタイマーを制御',
+ desc: 'すぐに体験したい場合は右クリックで操作できますが、Mac ユーザーの場合は非推奨です。'
+ },
+ bycheckbox: {
+ name: 'チェックボックスでタイマーを自動制御',
+ desc: 'チェックボックスを切り替えるだけでタイマーが自動作成・更新されます。Tasks プラグインユーザーに特に便利です。'
+ }
+ },
+ autostop: {
+ name: 'タイマーの自動停止',
+ desc: 'どのアクションでタイマーを自動停止しますか?',
+ choice: {
+ never: '手動で停止するまで絶対に停止しない',
+ quit: 'Obsidian を終了したら停止、ファイルを閉じてもタイマーは継続',
+ close: 'ファイルを閉じたら自動停止'
+ }
+ },
+ insertlocation: {
+ name: 'タイマーの挿入位置',
+ desc: 'Day Planner プラグインと併用する場合は「テキストの後ろ」に挿入することを推奨します。',
+ choice: {
+ head: 'テキストの前に挿入',
+ tail: 'テキストの後に挿入'
+ }
+ },
+ enableCheckboxToTimer: {
+ name: 'タスク状態制御タイマーを使います',
+ desc: 'この機能を有効にすると、タスクボックスの状態を変更することでタイマーの起動、停止、継続を自働的に制御できます。',
+ runningSymbolStr: {
+ name: '実行中記号',
+ desc: 'タイマー実行中を示す単一文字。複数は直接連結、例:/>+'
+ },
+ pausedSymbolStr: {
+ name: '一時停止記号',
+ desc: 'タイマー一時停止を示す単一文字。複数は直接連結、例:xX-'
+ },
+ pathControl: {
+ name: 'パス制限',
+ desc: '制限を通過したフォルダ/ファイルのみチェックボックス連動を利用可能です。注意:パスが直接ファイルを指す場合は、ファイルのサフィックスを書く必要があります。例:フォルダ DailyNote/2025 ファイル DailyNote/2025/20250101.md',
+ choice: {
+ disable: '制限なし(全ファイルで有効)',
+ whitelist: 'ホワイトリスト内のファイルのみ有効',
+ blacklist: 'ブラックリスト内のファイルは無効'
+ }
+ }
+ }
+ };
+ }
+ // Korean
+ else if (currentLanguage === 'ko') {
+ lang = {
+ name: '텍스트 블록 타이머',
+ desc: '단축키와 함께 사용하는 것이 좋습니다',
+ tutorial: '더 많은 사진과 비디오 튜토리얼은 다음을 방문하십시오: ',
+ askforvote: '이 플러그인이 마음에 드신다면, 제 github 프로젝트의 별을 클릭해 주세요⭐',
+ issue: '문제나 제안 사항이 있을 경우 Github 프로젝트에서 Issue를 생성해 주시기 바랍니다. 플러그인 버전과 재현 단계를 명시해 주세요',
+ sections: {
+ basic: {
+ name: '일반 설정',
+ },
+ bycommand: {
+ name: '명령줄로 타이머 제어',
+ desc: '명령줄 `타이머 시작/상태 전환`에 단축키를 추가하는 것을 강력추천하며, 단축키를 사용하여 타이머를 제어할 수 있습니다'
+ },
+ byeditormenu: {
+ name: '우클릭 메뉴로 타이머 제어',
+ desc: '목표 텍스트 블록에서 우클릭 드롭다운 메뉴를 열고, 타이머 기능 버튼을 클릭합니다'
+ },
+ bycheckbox: {
+ name: '체크박스를 통한 자동 타이머 제어',
+ desc: '태스크 상태를 전환하는 동안 타이머를 자동으로 생성하거나 업데이트할 수 있으며, Tasks Plugin 사용자들에게 매우 우호적입니다'
+ },
+ },
+ autostop: {
+ name: '타이머 자동 정지',
+ desc: '사용자가 타이머를 수동으로 정지한 것으로 간주되는 동작은?',
+ choice: {
+ never: '사용자가 수동으로 중지하지 않는 한 정지하지 않습니다',
+ quit: 'obsidian을 종료할 때만 종료되며, 닫기 파일은 여전히 백그라운드 타임을 유지한다',
+ close: '파일을 닫을 때 즉시 정지합니다',
+ }
+ },
+ insertlocation: {
+ name: '타이머 삽입 위치',
+ desc: 'Day Planner 플러그인과 함께 사용할 때는 텍스트 뒤에 삽입하는 것을 추천합니다',
+ choice: {
+ head: '텍스트 앞에 삽입합니다',
+ tail: '텍스트 뒤에 삽입합니다',
+ }
+ },
+ enableCheckboxToTimer: {
+ name: '체크박스로 타이머 제어',
+ desc: '이 기능을 사용하면 작업 상자의 상태를 변경하여 타이머의 시작, 일시 정지, 계속을 자동으로 제어할 수 있습니다',
+ runningSymbolStr: {
+ name: '타이머 시작/재개 체크박스 상태 기호',
+ desc: '시작/재개를 나타내는 체크박스 상태 기호를 지정합니다. 여러 기호를 조합하여 사용할 수 있습니다. 예: />+',
+ },
+ pausedSymbolStr: {
+ name: '타이머 일시정지 체크박스 상태 기호',
+ desc: '일시정지를 나타내는 체크박스 상태 기호를 지정합니다. 여러 기호를 조합하여 사용할 수 있습니다. 예: xX-',
+ },
+ pathControl: {
+ name: '경로 제어 활성화',
+ desc: '체크박스 상태로 타이머를 제어할 수 있는 폴더를 제한합니다,참고:경로가 파일을 직접 지칭할 때는 파일 접미사를 써야 한다,예를 들어,`일기/2025`는 카탈로그이고,`일기/20250101.md`는 파일이다',
+ choice: {
+ disable: '경로 제어를 사용하지 않고 볼트 (vault)의 모든 파일에서 작업 상태 타이머를 제어할 수 있다',
+ whitelist: '화이트리스트 경로로 제한하고, 화이트리스트 경로 내의 파일만이 체크박스 상태로 타이머 제어를 할 수 있습니다',
+ blacklist: '블랙리스트 경로를 제외하고, 블랙리스트 경로 외의 파일에서 체크박스 상태로 타이머 제어를 활성화합니다',
+ }
+ }
+ }
+ };
+ }
+ // English
+ else {
+ lang = {
+ name: 'Text Block Timer',
+ desc: 'Add a StopWatch to follow your time consumption',
+ tutorial: 'For more image and video tutorials, please visit: ',
+ askforvote: 'If you like this plugin, please give my Github project a Star🌟',
+ issue: 'If you have any issues or suggestions, please raise an Issue in the Github project and specify the plugin version you are using along with the reproduction steps',
+ sections: {
+ basic: {
+ name: 'Basic Settings',
+ },
+ bycommand: {
+ name: 'Control Timer By Command',
+ desc: 'Bind a shortcut to command `toggle-timer` is strongly recommended.'
+ },
+ byeditormenu: {
+ name: 'Control Timer By Right-Click Dropdown Menu',
+ desc: 'For quick experience, you can use right click to control timer. However it is not recommended especially for Mac users.'
+ },
+ bycheckbox: {
+ name: 'Control Timer By Checkbox Automatically',
+ desc: 'You can focus on toggling checkboxes with timers automatically created and updated for you, especially friendly for Tasks Plugin users.'
+ },
+
+ },
+
+ autostop: {
+ name: 'Auto Stop Timers',
+ desc: 'Which actions should stop the timer automatically?',
+ choice: {
+ never: 'Never Stop unless user stop timer manually',
+ quit: 'Auto stop when quit Obsidian, continue timing when file closed',
+ close: 'Auto stop when file closed'
+ }
+ },
+
+ insertlocation: {
+ name: 'Timer Insert Position',
+ desc: 'When used with the Day Planner plugin, it is recommended to insert after the text',
+ choice: {
+ head: 'Insert before text',
+ tail: 'Insert after text'
+ }
+ },
+
+ enableCheckboxToTimer: {
+ name: 'Enable Checkbox to Timer',
+ desc: 'Enable this feature to automatically control timers via checkbox states',
+ runningSymbolStr: {
+ name: 'Running State Symbols',
+ desc: 'Symbols (single characters) that indicate a running timer. Multiple symbols should be combined directly, e.g. />+'
+ },
+ pausedSymbolStr: {
+ name: 'Paused State Symbols',
+ desc: 'Symbols (single characters) that indicate a paused timer. Multiple symbols should be combined directly, e.g. xX-'
+ },
+ pathControl: {
+ name: 'Path Restriction',
+ desc: 'Only folders pass restrictions can use checkbox states to control timers. Note: When the path directly points to the file, the file suffix must be explicitly declared. Folder Example: `DailyNote/2025` File Example: `DailyNote/2025/20250101.md`',
+ choice: {
+ disable: 'No restriction - all files in the vault can use this feature',
+ whitelist: 'Only files in whitelisted paths can use this feature',
+ blacklist: 'Files in blacklisted paths cannot use this feature'
+ }
+ }
+ }
+ };
+ }
+
+ containerEl.createEl('h1', { text: lang.name });
+ containerEl.createEl('h2', { text: lang.desc });
+ containerEl.createEl('div', { text: '' });
+ // Create a new block for displaying tutorial information and links
+ const tutorialInfo = containerEl.createEl('div');
+ tutorialInfo.addClass('tutorial-info');
+
+ // Create a paragraph element
+ const tutorialParagraph = tutorialInfo.createEl('p');
+ tutorialParagraph.setText(lang.tutorial);
+
+ // Create a link element
+ const tutorialLink = tutorialParagraph.createEl('a');
+ tutorialLink.href = 'https://github.com/wth461694678/text-block-timer';
+ tutorialLink.setText('GitHub');
+ tutorialLink.target = '_blank'; // Open link in a new tab
+
+ containerEl.createEl('div', { text: '' });
+ containerEl.createEl('p', { text: lang.askforvote });
+
+ containerEl.createEl('div', { text: '' });
+ containerEl.createEl('p', { text: lang.issue });
+
+ containerEl.createEl('h3', { text: lang.sections.basic.name });
+ // Auto stop timers settings
+ new obsidian.Setting(containerEl)
+ .setName(lang.autostop.name)
+ .setDesc(lang.autostop.desc)
+ .addDropdown(dropdown => {
+ dropdown
+ .addOption('never', lang.autostop.choice.never)
+ .addOption('quit', lang.autostop.choice.quit)
+ .addOption('close', lang.autostop.choice.close)
+ .setValue(this.plugin.settings.autoStopTimers)
+ .onChange(async(value) => {
+ this.plugin.settings.autoStopTimers = value;
+ await this.plugin.saveSettings();
+ });
+ });
+
+
+ // Timer insert location settings
+ new obsidian.Setting(containerEl)
+ .setName(lang.insertlocation.name)
+ .setDesc(lang.insertlocation.desc)
+ .addDropdown(dropdown => {
+ dropdown
+ .addOption('head', lang.insertlocation.choice.head)
+ .addOption('tail', lang.insertlocation.choice.tail)
+ .setValue(this.plugin.settings.timerInsertLocation)
+ .onChange(async(value) => {
+ this.plugin.settings.timerInsertLocation = value;
+ await this.plugin.saveSettings();
+ });
+ });
+
+
+ containerEl.createEl('div', { text: '' });
+ containerEl.createEl('h3', { text: lang.sections.bycommand.name });
+ new obsidian.Setting(containerEl)
+ .setName(lang.sections.bycommand.desc);
+
+
+ containerEl.createEl('div', { text: '' });
+ containerEl.createEl('h3', { text: lang.sections.byeditormenu.name });
+ new obsidian.Setting(containerEl)
+ .setName(lang.sections.byeditormenu.desc);
+
+
+ containerEl.createEl('div', { text: '' });
+ // Add a horizontal line
+ containerEl.createEl('h3', { text: lang.sections.bycheckbox.name });
+
+
+ new obsidian.Setting(containerEl)
+ .setName(lang.sections.bycheckbox.desc);
+
+
+ // Checkbox to Timer feature settings
+ const checkboxToTimerSetting = new obsidian.Setting(containerEl);
+ checkboxToTimerSetting
+ .setName(lang.enableCheckboxToTimer.name)
+ .setDesc(lang.enableCheckboxToTimer.desc)
+ .addToggle(toggle => {
+ toggle
+ .setValue(this.plugin.settings.enableCheckboxToTimer)
+ .onChange(async(value) => {
+ this.plugin.settings.enableCheckboxToTimer = value;
+ await this.plugin.saveSettings();
+ this.display(); // 重新渲染设置页面
+ });
+ });
+
+ // Checkbox to Timer feature settings
+ if (this.plugin.settings.enableCheckboxToTimer) {
+ new obsidian.Setting(containerEl)
+ .setName(lang.enableCheckboxToTimer.runningSymbolStr.name)
+ .setDesc(lang.enableCheckboxToTimer.runningSymbolStr.desc)
+ .addText(text => {
+ text
+ .setPlaceholder('combine symbols directly, eg /')
+ .setValue(this.plugin.settings.runningCheckboxState)
+ .onChange(async(value) => {
+ // 去除方括号
+ const cleanedValue = value.replace(/\[|\]/g, '');
+ this.plugin.settings.runningCheckboxState = cleanedValue;
+
+ await this.plugin.saveSettings();
+ });
+ });
+
+ new obsidian.Setting(containerEl)
+ .setName(lang.enableCheckboxToTimer.pausedSymbolStr.name)
+ .setDesc(lang.enableCheckboxToTimer.pausedSymbolStr.desc)
+ .addText(text => {
+ text
+ .setPlaceholder('combine symbols directly, eg -xX')
+ .setValue(this.plugin.settings.pausedCheckboxState)
+ .onChange(async(value) => {
+ const cleanedValue = value.replace(/\[|\]/g, '');
+ this.plugin.settings.pausedCheckboxState = cleanedValue;
+ await this.plugin.saveSettings();
+ });
+ });
+
+ const pathRestrictionSetting = new obsidian.Setting(containerEl);
+ pathRestrictionSetting
+ .setName(lang.enableCheckboxToTimer.pathControl.name)
+ .setDesc(lang.enableCheckboxToTimer.pathControl.desc)
+ .addDropdown(dropdown => {
+ dropdown
+ .addOption('disable', lang.enableCheckboxToTimer.pathControl.choice.disable)
+ .addOption('whitelist', lang.enableCheckboxToTimer.pathControl.choice.whitelist)
+ .addOption('blacklist', lang.enableCheckboxToTimer.pathControl.choice.blacklist)
+ .setValue(this.plugin.settings.checkboxToTimerPathRestriction)
+ .onChange(async(value) => {
+ this.plugin.settings.checkboxToTimerPathRestriction = value;
+ await this.plugin.saveSettings();
+ this.display();
+ });
+ });
+
+ // If whitelist or blacklist is selected, display path input box
+ if (this.plugin.settings.checkboxToTimerPathRestriction !== 'disable') {
+ const pathsContainer = containerEl.createDiv();
+ pathsContainer.addClass('paths-container');
+
+ const addPath = (value = '') => {
+ const pathItem = pathsContainer.createDiv();
+ pathItem.addClass('path-item');
+
+ const buttonContainer = pathItem.createDiv();
+ buttonContainer.addClass('button-container');
+
+ const pathInput = buttonContainer.createEl('input');
+ pathInput.type = 'text';
+ pathInput.value = value;
+ pathInput.style.width = 'calc(100% - 60px)';
+ pathInput.addEventListener('input', async(event) => {
+ const paths = this.plugin.settings.pathRestrictionPaths || [];
+ const index = Array.from(pathsContainer.children).indexOf(pathItem);
+ paths[index] = event.target.value;
+ const newPaths = paths.map(path => this.processPath(path));
+ this.plugin.settings.pathRestrictionPaths = newPaths;
+ await this.plugin.saveSettings();
+ });
+ pathInput.addEventListener('blur', () => {
+ if (pathInput.value.trim() === '' && pathsContainer.children.length > 1) {
+ pathItem.remove();
+ const paths = this.plugin.settings.pathRestrictionPaths || [];
+ const index = Array.from(pathsContainer.children).indexOf(pathItem);
+ if (index > -1) {
+ paths.splice(index, 1);
+ const newPaths = paths.map(path => this.processPath(path));
+ this.plugin.settings.pathRestrictionPaths = newPaths;
+ this.plugin.saveSettings();
+ }
+ }
+ });
+
+ const addButton = buttonContainer.createEl('button');
+ addButton.textContent = '+';
+ addButton.style.margin = '0 3px';
+ addButton.style.padding = '0 5px';
+ addButton.addEventListener('click', () => {
+ addPath('');
+ });
+
+ const removeButton = buttonContainer.createEl('button');
+ removeButton.textContent = '-';
+ removeButton.style.margin = '0 3px';
+ removeButton.style.padding = '0 5px';
+ removeButton.addEventListener('click', () => {
+ pathItem.remove();
+ const paths = this.plugin.settings.pathRestrictionPaths || [];
+ const index = Array.from(pathsContainer.children).indexOf(pathItem);
+ if (index > -1) {
+ paths.splice(index, 1);
+ const newPaths = paths.map(path => this.processPath(path));
+ this.plugin.settings.pathRestrictionPaths = newPaths;
+ this.plugin.saveSettings();
+ }
+ });
+
+ // Hide removeButton initially, unless there are multiple paths
+ if (pathsContainer.children.length === 1) {
+ removeButton.style.display = 'none';
+ } else {
+ removeButton.style.display = 'inline-block';
+ }
+ };
+
+ // Initialize path input box
+ const paths = this.plugin.settings.pathRestrictionPaths || [];
+ paths.forEach(path => {
+ addPath(path);
+ });
+
+ // If there are no paths, add an empty input box
+ if (paths.length === 0) {
+ addPath('');
+ }
+ }
+ }
+ }
+}
+
+module.exports = TimerPlugin;
+/* nosourcemap */
\ No newline at end of file
diff --git a/manifest.json b/manifest.json
index a9b9051..2cf3841 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,6 +1,6 @@
{
"id": "text-block-timer",
- "version": "1.0.3",
+ "version": "1.0.4",
"minAppVersion": "1.8.9",
"name": "Text Block Timer",
"author": "frankthwang",