commit cd902285d24ad792a552ea256958805426cfa691 Author: frankthwang <461694678@qq.com> Date: Sun Apr 13 23:14:24 2025 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c96066d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.DS_Store \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..95defcc --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 frankthwang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9ea7684 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# 文本块计时器插件 + +一个为Obsidian设计的文本块计时工具,可以在笔记中为任意文本行添加计时功能。 + +## 功能特性 + +- 为文本行添加计时器 +- ▶️/⏸️ 支持开始、暂停、继续计时,支持一个任务的多段计时累计 +- ⏳ 实时显示累计时间 +- 📊 计时数据持久化保存 + +## 使用方法 + +1. 在文本行上右键点击 +2. 选择"开始计时"、"暂停计时"或"继续计时" +3. 计时器会以 `⏳00:00:00`格式显示在文本块的末尾 + +## 安装方法 + +1. 在Obsidian中打开"设置" → "社区插件" +2. 点击"浏览"并搜索"文本块计时器" +3. 安装并启用插件 + +## 注意事项 + +- 建议保持文本块所在文件状打开,否则会导致计时器的实时跳秒失效。但文件会保留计时器状态,下次打开文件时可以手动恢复计时。 + +## 开发信息 + +- 开发者: frankthwang +- 版本: 1.0.0 diff --git a/main.js b/main.js new file mode 100644 index 0000000..6aa2c9a --- /dev/null +++ b/main.js @@ -0,0 +1,231 @@ +'use strict'; + +var obsidian = require('obsidian'); + +class TimerPlugin extends obsidian.Plugin { + + async onload() { + // console.log('Loading Timer Plugin'); + + // 用于存储所有计时器的映射 + this.timers = new Map(); + + this.registerEvent( + this.app.workspace.on('editor-menu', (menu, editor, view) => { // 监听编辑器菜单事件 + const cursor = editor.getCursor(); + const lineId = cursor.line; + const lineText = editor.getLine(lineId); + + // 使用正则表达式匹配当前行的内容,内容格式为 `${timerData.statusicon} ⏳${formattedTime}` + const regex = /]+)>([^<]+)<\/span>/; + + // const regex = /([^<]+)<\/span>/; + const timerMatch = lineText.match(regex); + + // 提示解析结果 + if (timerMatch) { + const timerData = { + timerId: timerMatch[1], + Status: timerMatch[2], + AccumulatedTime: timerMatch[3], + currentStartTimeStamp: timerMatch[4], + }; + // new obsidian.Notice(`Timer Data: ID=${timerData.timerId}, Status=${timerData.Status}, Accumulated Time=${timerData.AccumulatedTime} seconds, Start Time=${timerData.currentStartTimeStamp}`); + } else { + // new obsidian.Notice('No timer data found in this line'); + } + + // 获取timerMatch的状态 + const timerStatus = timerMatch ? timerMatch[2] : null; + + // 如果timer在计时中,暂停计时按钮 + if (timerStatus === 'Running') { + menu.addItem((item) => { + item + .setTitle('暂停计时') + .setIcon('pause') + .onClick(() => { + // new obsidian.Notice('暂停计时'); + this.stopTimer(timerMatch[1], editor, cursor.line); + }); + }); + } // 如果timer在暂停中,继续计时按钮 + else if (timerStatus === 'Paused') { + menu.addItem((item) => { + item + .setTitle('继续计时') + .setIcon('play') + .onClick(() => { + // new obsidian.Notice('继续计时'); + this.startTimer(editor, cursor.line, timerMatch[1]); + }); + }); + } // 否则,开始/继续计时 + else { + menu.addItem((item) => { + item + .setTitle('开始计时') + .setIcon('play') + .onClick(() => { + // new obsidian.Notice('开始计时'); + this.initTimer(editor, lineId); + // this.startTimer(editor, lineId, Date.now().toString()); + }); + }); + } + + }) + ); + } + + // 开始计时器 + initTimer(editor, lineId) { + + // 更新计时器状态 + this.updateTimerInfo(editor, 'init', lineId); + + // // 更新计时器状态 + // this.updateTimerInfo(editor, 'continue', lineId); + + // // 创建一个定时器 + // const intervalId = setInterval(() => { + // this.updateTimerInfo(editor, 'update', lineId, timerId); + // }, 1000); + + // // 将定时器存储到映射中 + // this.timers.set(timerId, intervalId); + } + + // 开始计时器 + startTimer(editor, lineId, timerId) { + // 更新计时器状态 + this.updateTimerInfo(editor, 'continue', lineId); + + // 创建一个定时器 + const intervalId = setInterval(() => { + this.updateTimerInfo(editor, 'update', lineId, timerId); + }, 1000); + + // 将定时器存储到映射中 + this.timers.set(timerId, intervalId); + } + + // 停止计时器 + stopTimer(timerId, editor, lineId) { + if (this.timers.has(timerId)) { + // 获取定时器ID并清除定时器 + const intervalId = this.timers.get(timerId); + + clearInterval(intervalId); + + // 从映射中移除定时器 + this.timers.delete(timerId); + } + + // 更新计时器状态 + this.updateTimerInfo(editor, 'pause', lineId); + + + } + + // 更新计时器信息 + updateTimerInfo(editor, action, lineId, timerId = null) { + const cursor = editor.getCursor(); + let regex = /([^<]+)<\/span>/; + let lineText = editor.getLine(lineId); + let timerMatch = lineText.match(regex); + let timerInfoStart = timerMatch ? timerMatch.index : lineText.length; + let timerInfoEnd = timerInfoStart + (timerMatch ? timerMatch[0].length : 0); + + // 将timerData定义在if语句外部 + let timerData; + if (action === 'init') { + timerData = { + timerId: Date.now().toString(), + lineId: cursor.line, + Status: 'Paused', + AccumulatedTime: 0, + currentStartTimeStamp: null, + statusicon: '▶️', + } + } else if (action === 'continue') { + timerData = { + // 如果没有timerId,则使用当前时间戳作为ID + timerId: timerMatch ? timerMatch[1] : Date.now().toString(), + lineId: cursor.line, + Status: 'Running', + // 如果没有,就用0作为AccumulatedTime + AccumulatedTime: timerMatch ? parseInt(timerMatch[3], 10) : 0, + currentStartTimeStamp: Math.floor(Date.now() / 1000), + statusicon: '⏸️', + }; + } else if (action === 'pause') { + timerData = { + timerId: timerMatch[1], + lineId: cursor.line, + Status: 'Paused', + AccumulatedTime: parseInt(timerMatch[3], 10) + (Math.floor(Date.now() / 1000) - parseInt(timerMatch[4], 10)), + currentStartTimeStamp: null, + statusicon: '▶️', + }; + } else if (action === 'update') { + + // 用LineText.includes检查匹配到的文本中是否有timerId=timerId + if (!lineText.includes(`timerId="${timerId}"`)) { + // console.log(`!lineText.includes(timerId=${timerId})`); + // 如果没有匹配到,则在全文本中查找timerId,返回找到timerId=timerId的第一行 + // console.log('start to find timerId in all lines,looking for timerId:', timerId); + // console.log('editor:', editor); + let allLines = editor.getValue().split('\n'); + // console.log('allLines:', allLines) + let lineIndex = allLines.findIndex(line => line.includes(`timerId="${timerId}"`)); + // console.log('filtered lineIndex:', lineIndex); + if (lineIndex !== -1) { + lineId = lineIndex; + lineText = allLines[lineIndex]; + } else { + // 销毁定时器 + if (this.timers.has(timerId)) { + const intervalId = this.timers.get(timerId); + clearInterval(intervalId); + this.timers.delete(timerId); + // console.log('Timer with ID', timerId, 'destroyed'); + } + } + } + // console.log('lineId:', lineId); + // console.log('lineText:', lineText); + timerMatch = lineText.match(regex); + timerInfoStart = timerMatch ? timerMatch.index : lineText.length; + timerInfoEnd = timerInfoStart + (timerMatch ? timerMatch[0].length : 0); + + timerData = { + timerId: timerMatch[1], + lineId: lineId, + Status: 'Running', + AccumulatedTime: parseInt(timerMatch[3], 10) + (Math.floor(Date.now() / 1000) - parseInt(timerMatch[4], 10)), + currentStartTimeStamp: Math.floor(Date.now() / 1000), + statusicon: '⏸️', + }; + } else { + throw new Error('Invalid action'); + } + + const formattedTime = new Date(timerData.AccumulatedTime * 1000).toISOString().substr(11, 8); + const colorStyle = timerData.Status === 'Running' ? 'style="color: #10b981;"' : ''; + const updatedTimerInfo = `${timerData.statusicon} ⏳${formattedTime}`; + + editor.replaceRange(updatedTimerInfo, { line: lineId, ch: timerInfoStart }, { line: lineId, ch: timerInfoEnd }); + } + + onunload() { + // console.log('Unloading Timer Plugin'); + + // 清除所有计时器 + for (const intervalId of this.timers.values()) { + clearInterval(intervalId); + } + } +} + +module.exports = TimerPlugin; \ No newline at end of file diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..9fadb6e --- /dev/null +++ b/manifest.json @@ -0,0 +1,10 @@ +{ + "id": "text-block-timer", + "version": "1.0.0", + "minAppVersion": "1.8.9", + "name": "Text Block Timer", + "author": "frankthwang", + "description": "Add a timer to text block to track task time consumption.", + "repo": "wth461694678/text-block-timer", + "isDesktopOnly": true +} \ No newline at end of file diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..aa384b4 --- /dev/null +++ b/styles.css @@ -0,0 +1,26 @@ +.timer-container { + display: inline-flex; + align-items: center; + margin-left: 10px; +} + +.timer-button { + width: 0; + height: 0; + border-style: solid; + border-width: 8px 0 8px 12px; + border-color: transparent transparent transparent var(--text-normal); + cursor: pointer; + margin-right: 5px; +} + +.timer-button.paused { + border-style: double; + border-width: 0 0 0 12px; + border-color: transparent transparent transparent var(--text-normal); +} + +.timer-display { + font-size: 0.9em; + color: var(--text-muted); +} \ No newline at end of file