mirror of
https://github.com/wth461694678/text-block-timer.git
synced 2026-07-22 05:46:04 +00:00
first commit
This commit is contained in:
commit
cd902285d2
6 changed files with 320 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/.DS_Store
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal 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
31
README.md
Normal 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
231
main.js
Normal 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
10
manifest.json
Normal 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
26
styles.css
Normal 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);
|
||||
}
|
||||
Loading…
Reference in a new issue