搭建待办管理系统
1.背景
作为一个All-in-one (All in Boom)的狂热爱好者,其他的Todo软件是不可能用的(例如Mircosoft Todo和滴答清单),这种东西自然要Obsidian解决。 不过Obsidian基于Markdown
,自带的Todo很是简陋,循环/提醒等都没有。除了能写日记里面,其他的完全比不上那些专业的软件,需要很多时间整理。 因此,经过一段时间的使用,是时候搭建自己的任务管理系统了。
2.功能需求设计
- 快速添加,减少添加一个任务的心智成本
- 循环,通过每周、每月、每年、法定工作日、农历每年等重复规则,减少重复的成本
- 提醒,防止看着看着不记得东西了
- 开始和截止时间
3.定义格式
让我们先为我们的Todo定义一条格式吧。 为了兼容Obsidian Reminder,我们的todo内容要放在一行。 所以我们的格式是:
- [ ] (@yyyy-mm-dd hh:mm) ...
- 最开始,我们添加todo格式的列表,表明这是一个todo
- 然后,添加reminder格式的提醒日期
- 最后,添加我们想要的内容(当然,只能在一行内)
4.定时任务
定义
定义好格式,那么,我们先来搞定定时任务
一般来说,会有以下的几种格式:
- 每天
- 每周(选择几天)
- 每月(选择几天)
- 每年(选择哪几天)
- 自定义 (按指定xx天循环一次)
有一些东西不是很好处理,例如闰年,这些如果单独加一个按年的循环,会方便很多
然后,考虑到有单双周存在,我们再加入一个循环间隔,可以指定每隔xx进行一次(例如,每隔两周,周三添加一个待办)
为此,让我们来规定一下格式吧
- [type::(daily|yearly|monthly|weekly|custom)] [interval::int] [custom_long?:: int] [day::int] [indexDay::date] [time::time]content
- type: 指的是类型,提供一些预设
- interval: 两个循环之间的间隔,例如如果是连续的两周,那就是0,如果希望一周之后间隔一周再来,那么就是1
- custom_long: 自定义时长的单个循环节的长度,可选
- day: 包含哪些天来创建,例如一周可以选择1,2,3,4,5,6,7,也就是周一 -> 周日,使用的是逗号分隔。
- indexDay: 从哪天开始计算
- time: 提醒时间
- content: 待办的主体
代码
const modalForm = app.plugins.plugins.modalforms.api;
const result = await modalForm.openForm("scheduled-task-creator");
const data = result.data;
let { type, interval, indexDay, time_boolean, content } = data;
if (interval < 0) {
console.error("间隔必须大于等于0");
}
if (type === "custom" && !data.custom_long) {
console.error("请输入自定义时间");
}
const is_day_custom_ok = () => {
if (data.day_custom.length === 0) {
console.error("自定日期不能为空!请重新按照提示选择");
return false;
}
if (!data.day_custom) return false;
for (item of data.day_custom) {
if (isNaN(item)) {
return false;
}
if (item < 1 || item > data.custom_long) {
return false;
}
}
return true;
};
if (type === "custom" && is_day_custom_ok() === false) {
console.error("自定日期内选择错误!请重新按照提示选择");
}
if (!indexDay) {
const date = new Date();
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
indexDay = `${year}-${month}-${day}`;
}
const handlers = {
daily: () => {
return true;
},
weekly: () => {
return data.day_weekly;
},
monthly: () => {
return data.day_monthly;
},
yearly: () => {
return data.day_yearly;
},
custom: () => {
return data.day_custom;
},
};
const time = time_boolean ? `[time::${data.time}]` : "";
const custom_long = type === "custom" ? `[custom_long::${data.custom_long}]` : "";
return `- [type::${type}] [interval::${interval}] [day::${handlers[type]()}] ${time} ${custom_long} [indexDay::${indexDay}] ${content}`;
生成
然后,我们需要想办法让这个定时系统能够被解析出来并且放到我们的日记里面。 既然是日记,那么肯定是在当天生成。那么,选择templater来帮我们处理就是最好的选择了。 但是元数据怎么读呢? 这时就请该请出Dataview了。 先让我们把dataview的api拿进来
const dv = this.app.plugins.plugins.dataview.api;
dataview中,查询一个笔记,可以直接读取到其中的列表,以及每一项,并且每一项中的元数据也都可以读取到。所以,我们只要简单的把它们拿出来,然后处理一下就好。 不过读取内容可能会稍微麻烦一点,需要写一个正则。
const file = dv.pages(`"${todo_file_path}"`)[0].file;
// 元数据已经被dataview读取了
const todo_list = file.lists;
const result = [];
for (const list of todo_list) {
const text = list.text;
// 通过正则,去掉元数据的内容
const content = text.replace(/\[.*?::.*?\]\s*/g, "").trim();
result.push({
type: list.type,
interval: list.interval,
custom_long: list.custom_long ? list.custom_long : null,
day: list.day,
indexDay: list.indexDay,
time: list.time,
content: content,
});
}
读取了内容,我们就要看看,哪些Todo今天是要添加的,哪些不用理。 不过每个循环方式(例如按周,年)都有不同的逻辑,所以我们需要为每个逻辑写一个函数,判断今天能不能加。 最后,添加一点简单的细节,出现错误的时候也得通知一下用户吧,主体部分就大功告成了
for (const item of todos) {
if (!handler[item.type]) {
new Notice("出现了错误的任务类型,无法被处理!查看控制台获得更多信息");
console.log(item);
continue;
}
let flag = false;
try {
flag = handler[item.type](item);
} catch (e) {
console.error(e);
new Notice("Todo生成时出现了错误,无法被处理!查看控制台获得更多信息");
continue;
}
if (!flag) continue;
if (item.time) {
result += `- [ ] (@${tp.date.now("YYYY-MM-DD")} ${item.time}) ${
item.content
}\n`;
} else {
result += `- [ ] ${item.content}\n`;
}
}
return result;
完整代码
const dv = this.app.plugins.plugins.dataview.api;
const todo_file_path = "99 Obsidian/组件/Todo";
function file_formatter() {
const file = dv.pages(`"${todo_file_path}"`)[0].file;
const todo_list = file.lists;
const result = [];
for (const list of todo_list) {
const text = list.text;
const content = text.replace(/\[.*?::.*?\]\s*/g, "").trim();
result.push({
type: list.type,
interval: list.interval,
custom_long: list.custom_long ? list.custom_long : null,
day: list.day,
indexDay: list.indexDay,
time: list.time,
content: content,
});
}
return result;
}
function get_diff_day(indexd) {
const indexDay = indexd.toJSDate();
const currentDay = new Date();
indexDay.setHours(0, 0, 0, 0);
currentDay.setHours(0, 0, 0, 0);
const millisecondsInADay = 24 * 60 * 60 * 1000;
const indexDayInDays = Math.floor(indexDay.getTime() / millisecondsInADay);
const currentDayInDays = Math.floor(
currentDay.getTime() / millisecondsInADay
);
const diffDays = currentDayInDays - indexDayInDays;
return diffDays;
}
function get_diff_week(indexd) {
const indexDay = indexd.toJSDate();
const currentDay = new Date();
indexDay.setHours(0, 0, 0, 0);
currentDay.setHours(0, 0, 0, 0);
// 获取给定日期所在周的星期一
function getMonday(d) {
const date = new Date(d);
let day = date.getDay();
if (day === 0) {
day = 7; // 将周日视为第7天
}
date.setDate(date.getDate() - day + 1);
date.setHours(0, 0, 0, 0);
return date;
}
const indexMonday = getMonday(indexDay);
const currentMonday = getMonday(currentDay);
const milliSecondsPerWeek = 7 * 24 * 60 * 60 * 1000;
const weekDifference = Math.floor(
(currentMonday - indexMonday) / milliSecondsPerWeek
);
return weekDifference;
}
function get_diff_month(indexd) {
const indexDay = indexd.toJSDate();
const currentDay = new Date();
indexDay.setHours(0, 0, 0, 0);
currentDay.setHours(0, 0, 0, 0);
const yearDiff = currentDay.getFullYear() - indexDay.getFullYear();
const monthDiff = currentDay.getMonth() - indexDay.getMonth();
// 计算总的月数差异
const totalMonthDiff = yearDiff * 12 + monthDiff;
return totalMonthDiff;
}
function get_diff_year(indexd) {
const indexDay = indexd.toJSDate();
const currentDay = new Date();
indexDay.setHours(0, 0, 0, 0);
currentDay.setHours(0, 0, 0, 0);
const yearDiff = currentDay.getFullYear() - indexDay.getFullYear();
return yearDiff;
}
function get_long_custom(indexd, custom_long) {
const indexDay = indexd.toJSDate();
const currentDay = new Date();
indexDay.setHours(0, 0, 0, 0);
currentDay.setHours(0, 0, 0, 0);
const millisecondsInADay = 24 * 60 * 60 * 1000;
const diffDays = Math.floor((currentDay - indexDay) / millisecondsInADay);
// diffday = 10
if (diffDays < 0) {
return null; // 如果当前日期在索引日期之前,返回null
}
// 周期=2 10/3 = 3
const period = Math.floor(diffDays / custom_long);
const dayInPeriod = diffDays % custom_long;
return {
period: period,
dayInPeriod: dayInPeriod + 1, // 返回第几天,从1开始计数
};
}
const handler = {
// something will be use:
// interval: 循环间隔
// custom_long 自定义长度,只有custom用
// day: 哪些日子
// indexDay: 从哪一天开始
daily: (item) => {
const diffDays = get_diff_day(item.indexDay);
if (diffDays < 0) {
return false;
}
const interval = item.interval;
if (interval === 0) {
return true;
}
return diffDays % (interval + 1) === 0;
},
weekly: (item) => {
const diffweek = get_diff_week(item.indexDay);
if (diffweek < 0) {
return false;
}
const interval = item.interval;
if (interval !== 0 && diffweek % (interval + 1) !== 0) {
return false;
}
// 计算今天是一周的第多少天
const dayMapping = {
周一: 1,
周二: 2,
周三: 3,
周四: 4,
周五: 5,
周六: 6,
周日: 0,
};
const enable_days = item.day
.split(",")
.map((day) => dayMapping[day.trim()]);
const today = new Date().getDay();
return enable_days.includes(today);
},
monthly: (item) => {
const diffmonth = get_diff_month(item.indexDay);
if (diffmonth < 0) {
return false;
}
const interval = item.interval;
if (interval !== 0 && diffmonth % (interval + 1) !== 0) {
return false;
}
const enable_days = item.day
.split(",")
.map((day) => parseInt(day.replace(/号/g, "").trim()));
const today = new Date().getDate();
return enable_days.includes(today);
},
yearly: (item) => {
const diffyear = get_diff_year(item.indexDay);
if (diffyear < 0) {
return false;
}
const interval = item.interval;
if (interval !== 0 && diffyear % (interval + 1) !== 0) {
return false;
}
const enable_days = item.day
.split(",")
.map((day) => {
const match = day.trim().match(/(\d+)月(\d+)号/);
if (match) {
return { month: parseInt(match[1]), day: parseInt(match[2]) };
}
return null;
})
.filter(Boolean);
const today = new Date();
const today_month = today.getMonth() + 1;
const today_day = today.getDate();
return enable_days.some(
(day) => day.month === today_month && day.day === today_day
);
},
custom: (item) => {
if (item.custom_long === null) {
return false;
}
const diff_custom = get_long_custom(item.indexDay, item.custom_long);
if (diff_custom === null) {
return false;
}
const interval = item.interval;
if (interval !== 0 && diff_custom.period % (interval + 1) !== 0) {
return false;
}
const enable_days = item.day.split(",").map((day) => parseInt(day.trim()));
return enable_days.includes(diff_custom.dayInPeriod);
},
};
function get_regular_todo(tp) {
const todos = file_formatter();
let result = "";
for (const item of todos) {
if (!handler[item.type]) {
new Notice("出现了错误的任务类型,无法被处理!查看控制台获得更多信息");
console.log(item);
continue;
}
let flag = false;
try {
flag = handler[item.type](item);
} catch (e) {
console.error(e);
new Notice("Todo生成时出现了错误,无法被处理!查看控制台获得更多信息");
continue;
}
if (!flag) continue;
if (item.time) {
result += `- [ ] (@${tp.date.now("YYYY-MM-DD")} ${item.time}) ${
item.content
}\n`;
} else {
result += `- [ ] ${item.content}\n`;
}
}
return result;
}
module.exports = get_regular_todo;