first commit

This commit is contained in:
frankthwang 2025-04-13 23:14:24 +08:00
commit cd902285d2
6 changed files with 320 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/.DS_Store

21
LICENSE Normal file
View file

@ -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.

31
README.md Normal file
View file

@ -0,0 +1,31 @@
# 文本块计时器插件
一个为Obsidian设计的文本块计时工具可以在笔记中为任意文本行添加计时功能。
## 功能特性
- 为文本行添加计时器
- ▶️/⏸️ 支持开始、暂停、继续计时,支持一个任务的多段计时累计
- ⏳ 实时显示累计时间
- 📊 计时数据持久化保存
## 使用方法
1. 在文本行上右键点击
2. 选择"开始计时"、"暂停计时"或"继续计时"
3. 计时器会以 `⏳00:00:00`格式显示在文本块的末尾
## 安装方法
1. 在Obsidian中打开"设置" → "社区插件"
2. 点击"浏览"并搜索"文本块计时器"
3. 安装并启用插件
## 注意事项
- 建议保持文本块所在文件状打开,否则会导致计时器的实时跳秒失效。但文件会保留计时器状态,下次打开文件时可以手动恢复计时。
## 开发信息
- 开发者: frankthwang
- 版本: 1.0.0

231
main.js Normal file
View file

@ -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);
// 使用正则表达式匹配当前行的内容,内容格式为 `<span class="timer-btn" timerId="${timerData.timerId}" Status="${timerData.Status}" AccumulatedTime="${timerData.AccumulatedTime}" currentStartTimeStamp="${timerData.currentStartTimeStamp}" lineId="${timerData.lineId}" ${colorStyle}>${timerData.statusicon} ⏳${formattedTime}</span>`
const regex = /<span class="timer-btn" timerId="([^"]+)" Status="([^"]+)" AccumulatedTime="([^"]+)" currentStartTimeStamp="([^"]+)" lineId="([^"]+)"([^>]+)>([^<]+)<\/span>/;
// const regex = /<span class="timer-btn" timerId="([^"]+)" Status="([^"]+)" AccumulatedTime="([^"]+)" currentStartTimeStamp="([^"]+)" lineId="([^"]+)".*>([^<]+)<\/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 class="timer-btn" timerId="([^"]+)" Status="([^"]+)" AccumulatedTime="([^"]+)" currentStartTimeStamp="([^"]+)" lineId="([^"]+)"(.*?)>([^<]+)<\/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 = `<span class="timer-btn" timerId="${timerData.timerId}" Status="${timerData.Status}" AccumulatedTime="${timerData.AccumulatedTime}" currentStartTimeStamp="${timerData.currentStartTimeStamp}" lineId="${timerData.lineId}" ${colorStyle}>${timerData.statusicon}${formattedTime}</span>`;
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;

10
manifest.json Normal file
View file

@ -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
}

26
styles.css Normal file
View file

@ -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);
}