mirror of
https://github.com/maigamo/Daily-Task-Auto-Generator.git
synced 2026-07-22 05:48:30 +00:00
Init public
This commit is contained in:
parent
ec7ae825f2
commit
3f9dce59a7
27 changed files with 6949 additions and 1466 deletions
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# node modules
|
||||
node_modules/
|
||||
|
||||
# build artifacts
|
||||
main.js
|
||||
main.js.map
|
||||
data.json
|
||||
|
||||
# editor settings
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# operating system files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
48
esbuild.config.mjs
Normal file
48
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from "builtin-modules";
|
||||
|
||||
const banner =
|
||||
`/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
if you want to view the source, please visit the github repository of this plugin
|
||||
*/
|
||||
`;
|
||||
|
||||
const prod = process.argv[2] === "production";
|
||||
|
||||
const context = await esbuild.context({
|
||||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ["src/main.ts"],
|
||||
bundle: true,
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/collab",
|
||||
"@codemirror/commands",
|
||||
"@codemirror/language",
|
||||
"@codemirror/lint",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
...builtins],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: "main.js",
|
||||
});
|
||||
|
||||
if (prod) {
|
||||
await context.rebuild();
|
||||
process.exit(0);
|
||||
} else {
|
||||
await context.watch();
|
||||
}
|
||||
2363
package-lock.json
generated
Normal file
2363
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
28
package.json
Normal file
28
package.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "daily-task-auto-generator",
|
||||
"version": "1.0.0",
|
||||
"description": "Obsidian plugin to automatically generate daily tasks",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||
},
|
||||
"keywords": [
|
||||
"obsidian",
|
||||
"tasks",
|
||||
"automation"
|
||||
],
|
||||
"author": "maigamo",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "^5.29.0",
|
||||
"@typescript-eslint/parser": "^5.29.0",
|
||||
"builtin-modules": "^3.3.0",
|
||||
"esbuild": "0.17.3",
|
||||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
}
|
||||
}
|
||||
10
src/ambient.d.ts
vendored
Normal file
10
src/ambient.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* 扩展HTMLElement接口,添加Obsidian特有的方法
|
||||
*/
|
||||
interface HTMLElement {
|
||||
addClass(cls: string): void;
|
||||
removeClass(cls: string): void;
|
||||
toggleClass(cls: string): void;
|
||||
empty(): void;
|
||||
createEl<K extends keyof HTMLElementTagNameMap>(tag: K, attrs?: object): HTMLElementTagNameMap[K];
|
||||
}
|
||||
178
src/i18n/i18n.js
Normal file
178
src/i18n/i18n.js
Normal file
File diff suppressed because one or more lines are too long
326
src/i18n/i18n.ts
Normal file
326
src/i18n/i18n.ts
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
/**
|
||||
* i18n.ts
|
||||
* 国际化支持模块
|
||||
*/
|
||||
|
||||
// 定义翻译键类型
|
||||
export type TranslationKey =
|
||||
// 设置页面
|
||||
| 'settings.title'
|
||||
| 'settings.rootDir'
|
||||
| 'settings.rootDir.desc'
|
||||
| 'settings.rootDir.saved'
|
||||
| 'settings.save'
|
||||
| 'settings.autoGenerate'
|
||||
| 'settings.autoGenerate.desc'
|
||||
| 'settings.mode.none'
|
||||
| 'settings.mode.daily'
|
||||
| 'settings.mode.workday'
|
||||
| 'settings.language'
|
||||
| 'settings.language.desc'
|
||||
| 'settings.language.auto'
|
||||
| 'settings.language.zh'
|
||||
| 'settings.language.en'
|
||||
| 'settings.animations'
|
||||
| 'settings.animations.desc'
|
||||
| 'settings.template'
|
||||
| 'settings.template.zh'
|
||||
| 'settings.template.en'
|
||||
| 'settings.template.preview'
|
||||
| 'settings.template.hide'
|
||||
| 'settings.resetDefault'
|
||||
| 'settings.addTaskButton'
|
||||
| 'settings.resetToDefault'
|
||||
| 'settings.notificationDuration'
|
||||
| 'settings.notificationDuration.desc'
|
||||
| 'settings.preview'
|
||||
| 'template.dateWithIcon'
|
||||
|
||||
// 通知
|
||||
| 'notification.taskAdded'
|
||||
| 'notification.taskExists'
|
||||
| 'notification.error'
|
||||
|
||||
// 星期
|
||||
| 'weekday.mon'
|
||||
| 'weekday.tue'
|
||||
| 'weekday.wed'
|
||||
| 'weekday.thu'
|
||||
| 'weekday.fri'
|
||||
| 'weekday.sat'
|
||||
| 'weekday.sun'
|
||||
|
||||
// 插件名称和描述
|
||||
| 'plugin.name'
|
||||
| 'plugin.description'
|
||||
|
||||
// 一般按钮和消息
|
||||
| 'button.addTask'
|
||||
| 'button.save'
|
||||
| 'button.cancel'
|
||||
| 'button.done'
|
||||
|
||||
// 设置
|
||||
| 'settings.basicSettings'
|
||||
| 'settings.templateSettings'
|
||||
| 'settings.rootDir'
|
||||
| 'settings.rootDir.desc'
|
||||
| 'settings.rootDir.saved'
|
||||
| 'settings.save'
|
||||
| 'settings.autoGenerate'
|
||||
| 'settings.autoGenerate.desc'
|
||||
| 'settings.mode.none'
|
||||
| 'settings.mode.daily'
|
||||
| 'settings.mode.workday'
|
||||
| 'settings.language'
|
||||
| 'settings.language.desc'
|
||||
| 'settings.language.auto'
|
||||
| 'settings.language.zh'
|
||||
| 'settings.language.en'
|
||||
| 'settings.animations'
|
||||
| 'settings.animations.desc'
|
||||
| 'settings.template'
|
||||
| 'settings.template.zh'
|
||||
| 'settings.template.en'
|
||||
| 'settings.template.preview'
|
||||
| 'settings.template.hide'
|
||||
| 'settings.resetDefault'
|
||||
| 'settings.addTaskButton'
|
||||
| 'settings.resetToDefault'
|
||||
| 'settings.notificationDuration'
|
||||
| 'settings.notificationDuration.desc'
|
||||
| 'settings.preview'
|
||||
| 'template.dateWithIcon'
|
||||
|
||||
// 通知
|
||||
| 'notification.taskAdded'
|
||||
| 'notification.taskExists'
|
||||
| 'notification.error'
|
||||
|
||||
// 星期
|
||||
| 'weekday.mon'
|
||||
| 'weekday.tue'
|
||||
| 'weekday.wed'
|
||||
| 'weekday.thu'
|
||||
| 'weekday.fri'
|
||||
| 'weekday.sat'
|
||||
| 'weekday.sun'
|
||||
|
||||
// 插件名称和描述
|
||||
| 'plugin.name'
|
||||
| 'plugin.description'
|
||||
|
||||
// 一般按钮和消息
|
||||
| 'button.addTask'
|
||||
| 'button.save'
|
||||
| 'button.cancel'
|
||||
| 'button.done'
|
||||
|
||||
// 添加任务统计相关翻译
|
||||
| 'settings.taskStatistics'
|
||||
| 'settings.taskStatistics.desc'
|
||||
| 'statistics.title'
|
||||
| 'statistics.totalTasks'
|
||||
| 'statistics.completedTasks'
|
||||
| 'statistics.completionRate'
|
||||
| 'statistics.unfinishedTasks'
|
||||
| 'statistics.suggestions'
|
||||
| 'statistics.moreTasks.singular'
|
||||
| 'statistics.moreTasks.plural';
|
||||
|
||||
// 中文翻译
|
||||
const translationsZH: Record<TranslationKey, string> = {
|
||||
// 设置页面
|
||||
'settings.title': '每日任务自动生成器设置',
|
||||
'settings.rootDir': '📁 任务文件存放目录',
|
||||
'settings.rootDir.desc': '指定保存任务文件的根目录,任务将按"年份/月份.md"格式存储',
|
||||
'settings.rootDir.saved': '✓ 目录已保存',
|
||||
'settings.save': '💾 保存',
|
||||
'settings.autoGenerate': '🔄 自动生成模式',
|
||||
'settings.autoGenerate.desc': '选择何时自动生成每日任务',
|
||||
'settings.mode.none': '❌ 关闭',
|
||||
'settings.mode.daily': '📆 每天',
|
||||
'settings.mode.workday': '💼 仅工作日',
|
||||
'settings.language': '🔤 界面语言',
|
||||
'settings.language.desc': '选择插件界面显示的语言',
|
||||
'settings.language.auto': '🔍 自动检测',
|
||||
'settings.language.zh': '🇨🇳 中文',
|
||||
'settings.language.en': '🇬🇧 英文',
|
||||
'settings.animations': '✨ 动画效果',
|
||||
'settings.animations.desc': '启用界面动画效果',
|
||||
'settings.template': '📝 任务模板',
|
||||
'settings.template.zh': '🇨🇳 中文模板',
|
||||
'settings.template.en': '🇬🇧 英文模板',
|
||||
'settings.template.preview': '👁️ 显示预览',
|
||||
'settings.template.hide': '👁️🗨️ 隐藏预览',
|
||||
'settings.resetToDefault': '🔄 恢复默认设置',
|
||||
'settings.addTaskButton': '➕ 手动添加今日任务',
|
||||
'settings.notificationDuration': '⏱️ 通知显示时间',
|
||||
'settings.notificationDuration.desc': '成功/失败提示显示时间(毫秒)',
|
||||
'settings.preview': '预览模板效果',
|
||||
'settings.resetDefault': '恢复默认设置',
|
||||
'template.dateWithIcon': '带图标的当前日期',
|
||||
'settings.basicSettings': '基本设置',
|
||||
'settings.templateSettings': '模板设置',
|
||||
|
||||
// 通知
|
||||
'notification.taskAdded': '今日任务已添加',
|
||||
'notification.taskExists': '今日任务已存在',
|
||||
'notification.error': '错误:',
|
||||
|
||||
// 星期
|
||||
'weekday.mon': '星期一',
|
||||
'weekday.tue': '星期二',
|
||||
'weekday.wed': '星期三',
|
||||
'weekday.thu': '星期四',
|
||||
'weekday.fri': '星期五',
|
||||
'weekday.sat': '星期六',
|
||||
'weekday.sun': '星期日',
|
||||
|
||||
// 插件名称和描述
|
||||
'plugin.name': '每日任务自动生成器',
|
||||
'plugin.description': '一个强大的任务自动生成器,帮助你高效地管理日常任务',
|
||||
|
||||
// 一般按钮和消息
|
||||
'button.addTask': '添加任务',
|
||||
'button.save': '保存',
|
||||
'button.cancel': '取消',
|
||||
'button.done': '完成',
|
||||
|
||||
// 添加任务统计相关翻译
|
||||
'settings.taskStatistics': '📊 任务完成统计',
|
||||
'settings.taskStatistics.desc': '开启后,每日生成任务前会自动统计前一天的任务完成情况',
|
||||
'statistics.title': '📊 昨日任务统计',
|
||||
'statistics.totalTasks': '任务总数',
|
||||
'statistics.completedTasks': '已完成任务',
|
||||
'statistics.completionRate': '完成率',
|
||||
'statistics.unfinishedTasks': '未完成的任务',
|
||||
'statistics.suggestions': '建议今日考虑完成以下任务',
|
||||
'statistics.moreTasks.singular': '还有1个未完成任务',
|
||||
'statistics.moreTasks.plural': '还有更多未完成任务',
|
||||
};
|
||||
|
||||
// 英文翻译
|
||||
const translationsEN: Record<TranslationKey, string> = {
|
||||
// 设置页面
|
||||
'settings.title': 'Daily Task Auto Generator Settings',
|
||||
'settings.rootDir': '📁 Task Directory',
|
||||
'settings.rootDir.desc': 'Specify the root directory for storing task files, tasks will be stored in "Year/Month.md" format',
|
||||
'settings.rootDir.saved': '✓ Directory saved',
|
||||
'settings.save': '💾 Save',
|
||||
'settings.autoGenerate': '🔄 Auto Generate Mode',
|
||||
'settings.autoGenerate.desc': 'Choose when to automatically generate daily tasks',
|
||||
'settings.mode.none': '❌ Off',
|
||||
'settings.mode.daily': '📆 Daily',
|
||||
'settings.mode.workday': '💼 Workdays Only',
|
||||
'settings.language': '🔤 Interface Language',
|
||||
'settings.language.desc': 'Select the language for the plugin interface',
|
||||
'settings.language.auto': '🔍 Auto Detect',
|
||||
'settings.language.zh': '🇨🇳 Chinese',
|
||||
'settings.language.en': '🇬🇧 English',
|
||||
'settings.animations': '✨ Animation Effects',
|
||||
'settings.animations.desc': 'Enable interface animation effects',
|
||||
'settings.template': '📝 Task Template',
|
||||
'settings.template.zh': '🇨🇳 Chinese Template',
|
||||
'settings.template.en': '🇬🇧 English Template',
|
||||
'settings.template.preview': '👁️ Show Preview',
|
||||
'settings.template.hide': '👁️🗨️ Hide Preview',
|
||||
'settings.resetToDefault': '🔄 Reset to Default',
|
||||
'settings.addTaskButton': '➕ Add Today\'s Task Manually',
|
||||
'settings.notificationDuration': '⏱️ Notification Duration',
|
||||
'settings.notificationDuration.desc': 'Duration to show success/failure notifications (milliseconds)',
|
||||
'settings.preview': 'Preview Template',
|
||||
'settings.resetDefault': 'Reset to Default',
|
||||
'template.dateWithIcon': 'Current date with icon',
|
||||
'settings.basicSettings': 'Basic Settings',
|
||||
'settings.templateSettings': 'Template Settings',
|
||||
|
||||
// 通知
|
||||
'notification.taskAdded': 'Today\'s task has been added',
|
||||
'notification.taskExists': 'Today\'s task already exists',
|
||||
'notification.error': 'Error: ',
|
||||
|
||||
// 星期
|
||||
'weekday.mon': 'Monday',
|
||||
'weekday.tue': 'Tuesday',
|
||||
'weekday.wed': 'Wednesday',
|
||||
'weekday.thu': 'Thursday',
|
||||
'weekday.fri': 'Friday',
|
||||
'weekday.sat': 'Saturday',
|
||||
'weekday.sun': 'Sunday',
|
||||
|
||||
// 插件名称和描述
|
||||
'plugin.name': 'Daily Task Auto Generator',
|
||||
'plugin.description': 'A powerful task auto generator to help you efficiently manage daily tasks',
|
||||
|
||||
// 一般按钮和消息
|
||||
'button.addTask': 'Add Task',
|
||||
'button.save': 'Save',
|
||||
'button.cancel': 'Cancel',
|
||||
'button.done': 'Done',
|
||||
|
||||
// 添加任务统计相关翻译
|
||||
'settings.taskStatistics': '📊 Task Completion Statistics',
|
||||
'settings.taskStatistics.desc': 'When enabled, automatically analyze yesterday\'s task completion before generating today\'s tasks',
|
||||
'statistics.title': '📊 Yesterday\'s Task Summary',
|
||||
'statistics.totalTasks': 'Total Tasks',
|
||||
'statistics.completedTasks': 'Completed Tasks',
|
||||
'statistics.completionRate': 'Completion Rate',
|
||||
'statistics.unfinishedTasks': 'Unfinished Tasks',
|
||||
'statistics.suggestions': 'Consider completing the following tasks today',
|
||||
'statistics.moreTasks.singular': 'more task',
|
||||
'statistics.moreTasks.plural': 'more tasks',
|
||||
};
|
||||
|
||||
// 翻译查找表
|
||||
const translations: Record<string, Record<TranslationKey, string>> = {
|
||||
'zh': translationsZH,
|
||||
'en': translationsEN
|
||||
};
|
||||
|
||||
// 当前语言
|
||||
let currentLanguage = 'en';
|
||||
|
||||
/**
|
||||
* 设置当前语言
|
||||
* @param language 语言代码
|
||||
*/
|
||||
export function setCurrentLanguage(language: string): void {
|
||||
currentLanguage = language;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取翻译文本
|
||||
* @param key 翻译键
|
||||
* @param fallbackLanguage 备用语言
|
||||
* @returns 翻译后的文本
|
||||
*/
|
||||
export function getTranslation(key: TranslationKey, fallbackLanguage?: string): string {
|
||||
const language = fallbackLanguage || currentLanguage;
|
||||
|
||||
// 获取对应语言的翻译
|
||||
const translation = translations[language]?.[key];
|
||||
|
||||
// 如果没有找到对应翻译,尝试使用英文,再没有则返回键名
|
||||
if (!translation) {
|
||||
return translations['en'][key] || key;
|
||||
}
|
||||
|
||||
return translation;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取星期几的本地化名称
|
||||
* @param dayOfWeek 星期几的数字表示 (0-6, 0代表星期日)
|
||||
* @returns 本地化的星期名称
|
||||
*/
|
||||
export function getLocalizedWeekday(dayOfWeek: number): string {
|
||||
const weekdayKeys: TranslationKey[] = [
|
||||
'weekday.sun', 'weekday.mon', 'weekday.tue',
|
||||
'weekday.wed', 'weekday.thu', 'weekday.fri', 'weekday.sat'
|
||||
];
|
||||
|
||||
// 确保dayOfWeek在有效范围内
|
||||
const normalizedDayOfWeek = ((dayOfWeek % 7) + 7) % 7;
|
||||
return getTranslation(weekdayKeys[normalizedDayOfWeek]);
|
||||
}
|
||||
100
src/main.ts
Normal file
100
src/main.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { Plugin, addIcon } from 'obsidian';
|
||||
import { AutoGenerateMode } from './models/settings';
|
||||
import { DailyTaskSettingTab, SettingsManager } from './settings/settings';
|
||||
import { setCurrentLanguage } from './i18n/i18n';
|
||||
import { isWorkday } from './utils/dateUtils';
|
||||
import { TaskGenerator } from './taskGenerator';
|
||||
|
||||
// 定义插件图标
|
||||
const ICON = `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
|
||||
</svg>`;
|
||||
|
||||
/**
|
||||
* 每日任务自动生成器插件主类
|
||||
*/
|
||||
export default class DailyTaskPlugin extends Plugin {
|
||||
// 设置管理器
|
||||
settingsManager: SettingsManager;
|
||||
|
||||
// 任务生成器
|
||||
taskGenerator: TaskGenerator;
|
||||
|
||||
/**
|
||||
* 插件加载时调用
|
||||
*/
|
||||
async onload() {
|
||||
console.log('Loading Daily Task Auto Generator plugin');
|
||||
|
||||
// 添加插件图标
|
||||
addIcon('daily-task', ICON);
|
||||
|
||||
// 初始化设置管理器
|
||||
this.settingsManager = new SettingsManager(this);
|
||||
await this.settingsManager.loadSettings();
|
||||
|
||||
// 初始化任务生成器
|
||||
this.taskGenerator = new TaskGenerator(this.app, this.settingsManager);
|
||||
|
||||
// 设置语言
|
||||
setCurrentLanguage(this.settingsManager.getCurrentLanguage());
|
||||
|
||||
// 添加设置标签页
|
||||
this.addSettingTab(new DailyTaskSettingTab(this.app, this));
|
||||
|
||||
// 添加手动生成任务命令
|
||||
this.addCommand({
|
||||
id: 'add-daily-task',
|
||||
name: '手动添加今日任务',
|
||||
callback: async () => {
|
||||
await this.taskGenerator.addTaskManually();
|
||||
}
|
||||
});
|
||||
|
||||
// 延迟10秒后检查是否需要自动生成任务
|
||||
// 这样可以确保Obsidian完全加载,避免与启动过程冲突
|
||||
console.log('计划在10秒后检查是否需要自动生成任务');
|
||||
setTimeout(async () => {
|
||||
console.log('开始检查是否需要自动生成任务');
|
||||
await this.checkAutoGenerate();
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件卸载时调用
|
||||
*/
|
||||
onunload() {
|
||||
console.log('Unloading Daily Task Auto Generator plugin');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否需要自动生成任务
|
||||
*/
|
||||
private async checkAutoGenerate() {
|
||||
const settings = this.settingsManager.getSettings();
|
||||
|
||||
switch (settings.autoGenerateMode) {
|
||||
case AutoGenerateMode.DAILY:
|
||||
// 每天自动生成(静默模式,不打开文件,减少日志)
|
||||
console.log('根据设置,每天自动生成任务(静默模式)');
|
||||
await this.taskGenerator.generateDailyTask(false, true);
|
||||
break;
|
||||
|
||||
case AutoGenerateMode.WORKDAY:
|
||||
// 工作日自动生成(静默模式,不打开文件,减少日志)
|
||||
if (isWorkday()) {
|
||||
console.log('根据设置,工作日自动生成任务(静默模式)');
|
||||
await this.taskGenerator.generateDailyTask(false, true);
|
||||
} else {
|
||||
console.log('今天不是工作日,跳过自动生成任务');
|
||||
}
|
||||
break;
|
||||
|
||||
case AutoGenerateMode.NONE:
|
||||
default:
|
||||
// 不自动生成
|
||||
console.log('已禁用自动生成任务功能');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
69
src/models/settings.js
Normal file
69
src/models/settings.js
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
/**
|
||||
* 自动生成模式枚举
|
||||
*/
|
||||
export var AutoGenerateMode;
|
||||
(function (AutoGenerateMode) {
|
||||
AutoGenerateMode["NONE"] = "none";
|
||||
AutoGenerateMode["DAILY"] = "daily";
|
||||
AutoGenerateMode["WORKDAY"] = "workday"; // 仅工作日自动生成
|
||||
})(AutoGenerateMode || (AutoGenerateMode = {}));
|
||||
/**
|
||||
* 设置界面语言
|
||||
*/
|
||||
export var Language;
|
||||
(function (Language) {
|
||||
Language["AUTO"] = "auto";
|
||||
Language["ZH"] = "zh";
|
||||
Language["EN"] = "en";
|
||||
})(Language || (Language = {}));
|
||||
/**
|
||||
* 默认中文模板
|
||||
*/
|
||||
export const DEFAULT_TEMPLATE_ZH = `## {{dateWithIcon}}({{weekday}})
|
||||
|
||||
### 🧘 今日计划
|
||||
---
|
||||
|
||||
- [ ] 冥想 10 分钟
|
||||
- [ ] 复盘前一日计划
|
||||
- [ ] 阅读 20 页书
|
||||
|
||||
### 📝 工作任务
|
||||
---
|
||||
|
||||
- [ ] 整理今日工作计划
|
||||
- [ ] 完成重要项目进度
|
||||
`;
|
||||
/**
|
||||
* 默认英文模板
|
||||
*/
|
||||
export const DEFAULT_TEMPLATE_EN = `## {{dateWithIcon}} ({{weekday}})
|
||||
|
||||
### 🧘 Today's Plan
|
||||
---
|
||||
|
||||
- [ ] Meditate for 10 minutes
|
||||
- [ ] Review yesterday's plan
|
||||
- [ ] Read 20 pages
|
||||
|
||||
### 📝 Work Tasks
|
||||
---
|
||||
|
||||
- [ ] Organize today's work schedule
|
||||
- [ ] Progress on important projects
|
||||
`;
|
||||
/**
|
||||
* 默认设置
|
||||
*/
|
||||
export const DEFAULT_SETTINGS = {
|
||||
rootDir: 'DailyTasks',
|
||||
autoGenerateMode: AutoGenerateMode.WORKDAY,
|
||||
language: Language.AUTO,
|
||||
templateZh: DEFAULT_TEMPLATE_ZH,
|
||||
templateEn: DEFAULT_TEMPLATE_EN,
|
||||
customTemplate: '',
|
||||
hasCustomTemplate: false,
|
||||
taskStatistics: false,
|
||||
successNotificationDuration: 3000
|
||||
};
|
||||
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2V0dGluZ3MuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJzZXR0aW5ncy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7R0FFRztBQUNILE1BQU0sQ0FBTixJQUFZLGdCQUlYO0FBSkQsV0FBWSxnQkFBZ0I7SUFDeEIsaUNBQWEsQ0FBQTtJQUNiLG1DQUFlLENBQUE7SUFDZix1Q0FBbUIsQ0FBQSxDQUFDLFdBQVc7QUFDbkMsQ0FBQyxFQUpXLGdCQUFnQixLQUFoQixnQkFBZ0IsUUFJM0I7QUFFRDs7R0FFRztBQUNILE1BQU0sQ0FBTixJQUFZLFFBSVg7QUFKRCxXQUFZLFFBQVE7SUFDaEIseUJBQWEsQ0FBQTtJQUNiLHFCQUFTLENBQUE7SUFDVCxxQkFBUyxDQUFBO0FBQ2IsQ0FBQyxFQUpXLFFBQVEsS0FBUixRQUFRLFFBSW5CO0FBNEJEOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sbUJBQW1CLEdBQUc7Ozs7Ozs7Ozs7Ozs7O0NBY2xDLENBQUM7QUFFRjs7R0FFRztBQUNILE1BQU0sQ0FBQyxNQUFNLG1CQUFtQixHQUFHOzs7Ozs7Ozs7Ozs7OztDQWNsQyxDQUFDO0FBRUY7O0dBRUc7QUFDSCxNQUFNLENBQUMsTUFBTSxnQkFBZ0IsR0FBc0I7SUFDL0MsT0FBTyxFQUFFLFlBQVk7SUFDckIsZ0JBQWdCLEVBQUUsZ0JBQWdCLENBQUMsT0FBTztJQUMxQyxRQUFRLEVBQUUsUUFBUSxDQUFDLElBQUk7SUFDdkIsVUFBVSxFQUFFLG1CQUFtQjtJQUMvQixVQUFVLEVBQUUsbUJBQW1CO0lBQy9CLGNBQWMsRUFBRSxFQUFFO0lBQ2xCLGlCQUFpQixFQUFFLEtBQUs7SUFDeEIsY0FBYyxFQUFFLEtBQUs7SUFDckIsMkJBQTJCLEVBQUUsSUFBSTtDQUNwQyxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiDoh6rliqjnlJ/miJDmqKHlvI/mnprkuL5cbiAqL1xuZXhwb3J0IGVudW0gQXV0b0dlbmVyYXRlTW9kZSB7XG4gICAgTk9ORSA9ICdub25lJywgICAgIC8vIOWFs+mXreiHquWKqOeUn+aIkFxuICAgIERBSUxZID0gJ2RhaWx5JywgICAvLyDmr4/ml6Xoh6rliqjnlJ/miJBcbiAgICBXT1JLREFZID0gJ3dvcmtkYXknIC8vIOS7heW3peS9nOaXpeiHquWKqOeUn+aIkFxufVxuXG4vKipcbiAqIOiuvue9rueVjOmdouivreiogFxuICovXG5leHBvcnQgZW51bSBMYW5ndWFnZSB7XG4gICAgQVVUTyA9ICdhdXRvJyxcbiAgICBaSCA9ICd6aCcsXG4gICAgRU4gPSAnZW4nXG59XG5cbi8qKlxuICog5o+S5Lu26K6+572u5o6l5Y+jXG4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgRGFpbHlUYXNrU2V0dGluZ3Mge1xuICAgIC8vIOWfuuehgOmFjee9rlxuICAgIHJvb3REaXI6IHN0cmluZzsgICAgICAgICAgICAgICAvLyDku7vliqHmlofku7blrZjmlL7nmoTmoLnnm67lvZVcbiAgICBhdXRvR2VuZXJhdGVNb2RlOiBBdXRvR2VuZXJhdGVNb2RlOyAvLyDoh6rliqjnlJ/miJDmqKHlvI9cbiAgICBsYW5ndWFnZTogc3RyaW5nOyAgICAgICAgICAgIC8vIOeVjOmdouivreiogFxuXG4gICAgLy8g5qih5p2/6YWN572uXG4gICAgdGVtcGxhdGVaaDogc3RyaW5nOyAgICAgICAgICAgIC8vIOS4reaWh+S7u+WKoeaooeadv1xuICAgIHRlbXBsYXRlRW46IHN0cmluZzsgICAgICAgICAgICAvLyDoi7Hmlofku7vliqHmqKHmnb9cblxuICAgIC8vIOaWsOWinu+8mueUqOaIt+iHquWumuS5ieaooeadv1xuICAgIGN1c3RvbVRlbXBsYXRlOiBzdHJpbmc7XG5cbiAgICAvLyDmlrDlop7vvJrmoIforrDmmK/lkKbkvb/nlKjoh6rlrprkuYnmqKHmnb9cbiAgICBoYXNDdXN0b21UZW1wbGF0ZTogYm9vbGVhbjtcblxuICAgIC8vIOaWsOWinu+8muaYr+WQpuW8gOWQr+S7u+WKoee7n+iuoeWKn+iDvVxuICAgIHRhc2tTdGF0aXN0aWNzOiBib29sZWFuO1xuXG4gICAgLy8gVUnphY3nva5cbiAgICBzdWNjZXNzTm90aWZpY2F0aW9uRHVyYXRpb246IG51bWJlcjsgLy8g5oiQ5Yqf6YCa55+l5pi+56S65pe26Ze0KOavq+enkilcbn1cblxuLyoqXG4gKiDpu5jorqTkuK3mlofmqKHmnb9cbiAqL1xuZXhwb3J0IGNvbnN0IERFRkFVTFRfVEVNUExBVEVfWkggPSBgIyMge3tkYXRlV2l0aEljb259fe+8iHt7d2Vla2RheX1977yJXG5cbiMjIyDwn6eYIOS7iuaXpeiuoeWIklxuLS0tXG5cbi0gWyBdIOWGpeaDsyAxMCDliIbpkp8gIFxuLSBbIF0g5aSN55uY5YmN5LiA5pel6K6h5YiSICBcbi0gWyBdIOmYheivuyAyMCDpobXkuaZcblxuIyMjIPCfk50g5bel5L2c5Lu75YqhXG4tLS1cblxuLSBbIF0g5pW055CG5LuK5pel5bel5L2c6K6h5YiSXG4tIFsgXSDlrozmiJDph43opoHpobnnm67ov5vluqZcbmA7XG5cbi8qKlxuICog6buY6K6k6Iux5paH5qih5p2/XG4gKi9cbmV4cG9ydCBjb25zdCBERUZBVUxUX1RFTVBMQVRFX0VOID0gYCMjIHt7ZGF0ZVdpdGhJY29ufX0gKHt7d2Vla2RheX19KVxuXG4jIyMg8J+nmCBUb2RheSdzIFBsYW5cbi0tLVxuXG4tIFsgXSBNZWRpdGF0ZSBmb3IgMTAgbWludXRlcyAgXG4tIFsgXSBSZXZpZXcgeWVzdGVyZGF5J3MgcGxhbiAgXG4tIFsgXSBSZWFkIDIwIHBhZ2VzXG5cbiMjIyDwn5OdIFdvcmsgVGFza3Ncbi0tLVxuXG4tIFsgXSBPcmdhbml6ZSB0b2RheSdzIHdvcmsgc2NoZWR1bGVcbi0gWyBdIFByb2dyZXNzIG9uIGltcG9ydGFudCBwcm9qZWN0c1xuYDtcblxuLyoqXG4gKiDpu5jorqTorr7nva5cbiAqL1xuZXhwb3J0IGNvbnN0IERFRkFVTFRfU0VUVElOR1M6IERhaWx5VGFza1NldHRpbmdzID0ge1xuICAgIHJvb3REaXI6ICdEYWlseVRhc2tzJyxcbiAgICBhdXRvR2VuZXJhdGVNb2RlOiBBdXRvR2VuZXJhdGVNb2RlLldPUktEQVksXG4gICAgbGFuZ3VhZ2U6IExhbmd1YWdlLkFVVE8sXG4gICAgdGVtcGxhdGVaaDogREVGQVVMVF9URU1QTEFURV9aSCxcbiAgICB0ZW1wbGF0ZUVuOiBERUZBVUxUX1RFTVBMQVRFX0VOLFxuICAgIGN1c3RvbVRlbXBsYXRlOiAnJywgLy8g6buY6K6k5Li656m677yM6KGo56S65L2/55So6K+t6KiA55u45YWz55qE6buY6K6k5qih5p2/XG4gICAgaGFzQ3VzdG9tVGVtcGxhdGU6IGZhbHNlLCAvLyDpu5jorqTkuI3kvb/nlKjoh6rlrprkuYnmqKHmnb9cbiAgICB0YXNrU3RhdGlzdGljczogZmFsc2UsIC8vIOm7mOiupOWFs+mXreS7u+WKoee7n+iuoeWKn+iDvVxuICAgIHN1Y2Nlc3NOb3RpZmljYXRpb25EdXJhdGlvbjogMzAwMFxufTsgIl19
|
||||
96
src/models/settings.ts
Normal file
96
src/models/settings.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
/**
|
||||
* 自动生成模式枚举
|
||||
*/
|
||||
export enum AutoGenerateMode {
|
||||
NONE = 'none', // 关闭自动生成
|
||||
DAILY = 'daily', // 每日自动生成
|
||||
WORKDAY = 'workday' // 仅工作日自动生成
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置界面语言
|
||||
*/
|
||||
export enum Language {
|
||||
AUTO = 'auto',
|
||||
ZH = 'zh',
|
||||
EN = 'en'
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件设置接口
|
||||
*/
|
||||
export interface DailyTaskSettings {
|
||||
// 基础配置
|
||||
rootDir: string; // 任务文件存放的根目录
|
||||
autoGenerateMode: AutoGenerateMode; // 自动生成模式
|
||||
language: string; // 界面语言
|
||||
|
||||
// 模板配置
|
||||
templateZh: string; // 中文任务模板
|
||||
templateEn: string; // 英文任务模板
|
||||
|
||||
// 新增:用户自定义模板
|
||||
customTemplate: string;
|
||||
|
||||
// 新增:标记是否使用自定义模板
|
||||
hasCustomTemplate: boolean;
|
||||
|
||||
// 新增:是否开启任务统计功能
|
||||
taskStatistics: boolean;
|
||||
|
||||
// UI配置
|
||||
successNotificationDuration: number; // 成功通知显示时间(毫秒)
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认中文模板
|
||||
*/
|
||||
export const DEFAULT_TEMPLATE_ZH = `## {{dateWithIcon}}({{weekday}})
|
||||
|
||||
### 🧘 今日计划
|
||||
---
|
||||
|
||||
- [ ] 冥想 10 分钟
|
||||
- [ ] 复盘前一日计划
|
||||
- [ ] 阅读 20 页书
|
||||
|
||||
### 📝 工作任务
|
||||
---
|
||||
|
||||
- [ ] 整理今日工作计划
|
||||
- [ ] 完成重要项目进度
|
||||
`;
|
||||
|
||||
/**
|
||||
* 默认英文模板
|
||||
*/
|
||||
export const DEFAULT_TEMPLATE_EN = `## {{dateWithIcon}} ({{weekday}})
|
||||
|
||||
### 🧘 Today's Plan
|
||||
---
|
||||
|
||||
- [ ] Meditate for 10 minutes
|
||||
- [ ] Review yesterday's plan
|
||||
- [ ] Read 20 pages
|
||||
|
||||
### 📝 Work Tasks
|
||||
---
|
||||
|
||||
- [ ] Organize today's work schedule
|
||||
- [ ] Progress on important projects
|
||||
`;
|
||||
|
||||
/**
|
||||
* 默认设置
|
||||
*/
|
||||
export const DEFAULT_SETTINGS: DailyTaskSettings = {
|
||||
rootDir: 'DailyTasks',
|
||||
autoGenerateMode: AutoGenerateMode.WORKDAY,
|
||||
language: Language.AUTO,
|
||||
templateZh: DEFAULT_TEMPLATE_ZH,
|
||||
templateEn: DEFAULT_TEMPLATE_EN,
|
||||
customTemplate: '', // 默认为空,表示使用语言相关的默认模板
|
||||
hasCustomTemplate: false, // 默认不使用自定义模板
|
||||
taskStatistics: false, // 默认关闭任务统计功能
|
||||
successNotificationDuration: 3000
|
||||
};
|
||||
136
src/obsidian.d.ts
vendored
Normal file
136
src/obsidian.d.ts
vendored
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
/**
|
||||
* Obsidian核心API类型声明
|
||||
*/
|
||||
|
||||
declare module "obsidian" {
|
||||
export class Plugin {
|
||||
app: App;
|
||||
manifest: PluginManifest;
|
||||
loadData(): Promise<any>;
|
||||
saveData(data: any): Promise<void>;
|
||||
addSettingTab(settingTab: PluginSettingTab): void;
|
||||
addCommand(command: Command): void;
|
||||
}
|
||||
|
||||
export interface App {
|
||||
workspace: Workspace;
|
||||
vault: Vault;
|
||||
}
|
||||
|
||||
export interface Workspace {
|
||||
getLeaf(): WorkspaceLeaf;
|
||||
}
|
||||
|
||||
export interface WorkspaceLeaf {
|
||||
openFile(file: TFile): Promise<void>;
|
||||
}
|
||||
|
||||
export interface Vault {
|
||||
getAbstractFileByPath(path: string): TAbstractFile | null;
|
||||
createFolder(path: string): Promise<void>;
|
||||
create(path: string, data: string): Promise<TFile>;
|
||||
read(file: TFile): Promise<string>;
|
||||
modify(file: TFile, data: string): Promise<void>;
|
||||
}
|
||||
|
||||
export class TAbstractFile {
|
||||
path: string;
|
||||
name: string;
|
||||
vault: Vault;
|
||||
}
|
||||
|
||||
export class TFile extends TAbstractFile {
|
||||
extension: string;
|
||||
}
|
||||
|
||||
export class TFolder extends TAbstractFile {
|
||||
children: TAbstractFile[];
|
||||
}
|
||||
|
||||
export interface PluginManifest {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface Command {
|
||||
id: string;
|
||||
name: string;
|
||||
callback: () => any;
|
||||
checkCallback?: (checking: boolean) => boolean | void;
|
||||
hotkeys?: Hotkey[];
|
||||
}
|
||||
|
||||
export interface Hotkey {
|
||||
modifiers: string[];
|
||||
key: string;
|
||||
}
|
||||
|
||||
export abstract class PluginSettingTab {
|
||||
constructor(app: App, plugin: Plugin);
|
||||
containerEl: HTMLElement;
|
||||
abstract display(): void;
|
||||
}
|
||||
|
||||
export class Setting {
|
||||
constructor(containerEl: HTMLElement);
|
||||
setName(name: string): this;
|
||||
setDesc(desc: string): this;
|
||||
setClass(cls: string): this;
|
||||
addText(cb: (text: TextComponent) => any): this;
|
||||
addButton(cb: (button: ButtonComponent) => any): this;
|
||||
addTextArea(cb: (text: TextAreaComponent) => any): this;
|
||||
addToggle(cb: (toggle: ToggleComponent) => any): this;
|
||||
addDropdown(cb: (dropdown: DropdownComponent) => any): this;
|
||||
controlEl: HTMLElement;
|
||||
}
|
||||
|
||||
export class ButtonComponent {
|
||||
constructor(containerEl: HTMLElement);
|
||||
buttonEl: HTMLButtonElement;
|
||||
setButtonText(name: string): this;
|
||||
setDisabled(disabled: boolean): this;
|
||||
setCta(): this;
|
||||
onClick(callback: () => any): this;
|
||||
}
|
||||
|
||||
export class TextComponent {
|
||||
constructor(containerEl: HTMLElement);
|
||||
inputEl: HTMLInputElement;
|
||||
setValue(value: string): this;
|
||||
getValue(): string;
|
||||
onChange(callback: (value: string) => any): this;
|
||||
}
|
||||
|
||||
export class TextAreaComponent {
|
||||
constructor(containerEl: HTMLElement);
|
||||
inputEl: HTMLTextAreaElement;
|
||||
setValue(value: string): this;
|
||||
getValue(): string;
|
||||
setPlaceholder(placeholder: string): this;
|
||||
onChange(callback: (value: string) => any): this;
|
||||
}
|
||||
|
||||
export class ToggleComponent {
|
||||
constructor(containerEl: HTMLElement);
|
||||
setValue(value: boolean): this;
|
||||
onChange(callback: (value: boolean) => any): this;
|
||||
}
|
||||
|
||||
export class DropdownComponent {
|
||||
constructor(containerEl: HTMLElement);
|
||||
addOption(value: string, display: string): this;
|
||||
setValue(value: string): this;
|
||||
onChange(callback: (value: string) => any): this;
|
||||
}
|
||||
|
||||
export function addIcon(iconId: string, svgContent: string): void;
|
||||
|
||||
export class Notice {
|
||||
constructor(message: string, timeout?: number);
|
||||
noticeEl: HTMLElement;
|
||||
}
|
||||
|
||||
export function normalizePath(path: string): string;
|
||||
}
|
||||
9
src/settings/index.ts
Normal file
9
src/settings/index.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* 设置模块入口文件
|
||||
* 导出设置模块中的关键内容
|
||||
*/
|
||||
|
||||
import { DEFAULT_SETTINGS } from '../models/settings';
|
||||
|
||||
export { DEFAULT_SETTINGS };
|
||||
export * from './settings';
|
||||
692
src/settings/settings.js
Normal file
692
src/settings/settings.js
Normal file
File diff suppressed because one or more lines are too long
843
src/settings/settings.ts
Normal file
843
src/settings/settings.ts
Normal file
|
|
@ -0,0 +1,843 @@
|
|||
import { App, ButtonComponent, DropdownComponent, Notice, Plugin, PluginSettingTab, Setting, TextAreaComponent, TextComponent, ToggleComponent } from 'obsidian';
|
||||
import { AutoGenerateMode, DEFAULT_SETTINGS, DEFAULT_TEMPLATE_EN, DEFAULT_TEMPLATE_ZH, DailyTaskSettings, Language } from '../models/settings';
|
||||
import { getTranslation, setCurrentLanguage } from '../i18n/i18n';
|
||||
import { renderTemplate } from '../utils/templateEngine';
|
||||
import { TaskGenerator } from '../taskGenerator';
|
||||
|
||||
// 前向声明,避免循环导入
|
||||
declare class DailyTaskPlugin {
|
||||
saveData(data: any): Promise<void>;
|
||||
loadData(): Promise<any>;
|
||||
app: App;
|
||||
}
|
||||
|
||||
// CSS 相关代码
|
||||
const SettingsSectionCSS = "daily-task-settings-section";
|
||||
const ButtonCSS = "daily-task-button";
|
||||
const PreviewButtonCSS = "daily-task-preview-button";
|
||||
const ResetButtonCSS = "daily-task-reset-button";
|
||||
const EditorCSS = "daily-task-editor";
|
||||
const VerticalStackCSS = "daily-task-vertical-stack";
|
||||
const TextRightCSS = "daily-task-text-right";
|
||||
const TextCenterCSS = "daily-task-text-center";
|
||||
const ScrollbarSlimCSS = "daily-task-slim-scrollbar";
|
||||
|
||||
/**
|
||||
* 添加插件自定义样式
|
||||
*/
|
||||
function addCustomStyles() {
|
||||
const cssText = `
|
||||
.${SettingsSectionCSS} {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 24px;
|
||||
padding: 12px 0;
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.${ButtonCSS} {
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.${PreviewButtonCSS}, .${ResetButtonCSS} {
|
||||
display: inline-block;
|
||||
text-align: center !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.${EditorCSS} {
|
||||
height: 200px;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.${VerticalStackCSS} {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.${TextRightCSS} {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.${TextCenterCSS} {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.${ScrollbarSlimCSS} .CodeMirror-vscrollbar {
|
||||
width: 20% !important;
|
||||
}
|
||||
|
||||
/* 自定义通知样式 */
|
||||
.daily-task-success-notice {
|
||||
background-color: rgba(0, 255, 127, 0.2) !important;
|
||||
}
|
||||
|
||||
.daily-task-warning-notice {
|
||||
background-color: rgba(255, 165, 0, 0.2) !important;
|
||||
}
|
||||
|
||||
.daily-task-error-notice {
|
||||
background-color: rgba(255, 69, 0, 0.2) !important;
|
||||
}
|
||||
`;
|
||||
|
||||
// 添加自定义样式
|
||||
const styleElement = document.createElement('style');
|
||||
styleElement.textContent = cssText;
|
||||
document.head.appendChild(styleElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件设置标签页
|
||||
*/
|
||||
export class DailyTaskSettingTab extends PluginSettingTab {
|
||||
plugin: Plugin;
|
||||
settingsManager: SettingsManager;
|
||||
taskGenerator: TaskGenerator;
|
||||
previewEl: HTMLElement | null = null;
|
||||
addTaskButton: ButtonComponent | null = null;
|
||||
|
||||
// 目录输入框
|
||||
rootDirInput: TextComponent | null = null;
|
||||
|
||||
// 标记设置是否已修改但未保存
|
||||
dirtySettings: boolean = false;
|
||||
|
||||
// 自动保存目录的方法
|
||||
autoSaveRootDir: (value: string) => Promise<void>;
|
||||
|
||||
constructor(app: App, plugin: Plugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
|
||||
// 获取父插件中的设置管理器引用
|
||||
this.settingsManager = (plugin as any).settingsManager;
|
||||
|
||||
// 创建任务生成器
|
||||
this.taskGenerator = new TaskGenerator(app, this.settingsManager);
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
containerEl.classList.add('daily-task-setting-tab');
|
||||
|
||||
const settings = this.settingsManager.getSettings();
|
||||
|
||||
// 添加顶部间距(增加间距以改善界面美观)
|
||||
const topSpacing = containerEl.createEl('div');
|
||||
topSpacing.style.marginTop = '30px';
|
||||
|
||||
// 根目录设置
|
||||
const rootDirSetting = new Setting(containerEl)
|
||||
.setName(getTranslation('settings.rootDir'))
|
||||
.setDesc(getTranslation('settings.rootDir.desc'));
|
||||
|
||||
// 创建输入框容器,使其可以包含额外元素
|
||||
const inputContainer = document.createElement('div');
|
||||
inputContainer.style.display = 'flex';
|
||||
inputContainer.style.width = '100%';
|
||||
inputContainer.style.position = 'relative';
|
||||
rootDirSetting.controlEl.appendChild(inputContainer);
|
||||
|
||||
this.rootDirInput = new TextComponent(inputContainer)
|
||||
.setValue(settings.rootDir)
|
||||
.onChange(async (value) => {
|
||||
if (value.trim() !== '') {
|
||||
// 设置已更改,准备自动保存
|
||||
this.dirtySettings = true;
|
||||
|
||||
// 启动自动保存定时器
|
||||
this.autoSaveRootDir(value);
|
||||
}
|
||||
});
|
||||
|
||||
// 给input元素直接设置placeholder属性
|
||||
if (this.rootDirInput && this.rootDirInput.inputEl) {
|
||||
this.rootDirInput.inputEl.placeholder = 'DailyTasks';
|
||||
// 增加宽度
|
||||
this.rootDirInput.inputEl.style.width = '100%';
|
||||
// 美化输入框样式
|
||||
this.rootDirInput.inputEl.style.borderRadius = '4px';
|
||||
this.rootDirInput.inputEl.style.padding = '8px 35px 8px 10px';
|
||||
this.rootDirInput.inputEl.style.transition = 'all 0.3s ease';
|
||||
}
|
||||
|
||||
// 添加自动保存指示器
|
||||
const saveIndicator = document.createElement('div');
|
||||
saveIndicator.classList.add('save-indicator');
|
||||
saveIndicator.style.position = 'absolute';
|
||||
saveIndicator.style.right = '10px';
|
||||
saveIndicator.style.top = '50%';
|
||||
saveIndicator.style.transform = 'translateY(-50%)';
|
||||
saveIndicator.style.opacity = '0';
|
||||
saveIndicator.style.transition = 'opacity 0.3s ease';
|
||||
inputContainer.appendChild(saveIndicator);
|
||||
|
||||
// 创建保存成功图标
|
||||
const saveSuccessIcon = createSpan({cls: 'svg-icon lucide-check'});
|
||||
saveSuccessIcon.style.color = '#4CAF50';
|
||||
saveSuccessIcon.style.width = '18px';
|
||||
saveSuccessIcon.style.height = '18px';
|
||||
saveIndicator.appendChild(saveSuccessIcon);
|
||||
|
||||
// 记录自动保存定时器
|
||||
let autoSaveTimer: number | null = null;
|
||||
|
||||
// 自动保存方法
|
||||
this.autoSaveRootDir = async (value: string) => {
|
||||
// 清除之前的定时器
|
||||
if (autoSaveTimer !== null) {
|
||||
clearTimeout(autoSaveTimer);
|
||||
}
|
||||
|
||||
// 设置新的定时器,延迟800ms保存(在用户停止输入后)
|
||||
autoSaveTimer = setTimeout(async () => {
|
||||
let pathToSave = value.trim();
|
||||
if (pathToSave === '') {
|
||||
pathToSave = 'DailyTasks'; // 默认存放目录
|
||||
}
|
||||
|
||||
// 保存设置
|
||||
await this.settingsManager.updateSettings({ rootDir: pathToSave });
|
||||
this.dirtySettings = false;
|
||||
|
||||
// 显示保存成功指示器
|
||||
saveIndicator.style.opacity = '1';
|
||||
|
||||
// 3秒后隐藏
|
||||
setTimeout(() => {
|
||||
saveIndicator.style.opacity = '0';
|
||||
}, 2000);
|
||||
}, 800) as unknown as number;
|
||||
};
|
||||
|
||||
// 自动生成模式
|
||||
const autoGenSetting = new Setting(containerEl)
|
||||
.setName(getTranslation('settings.autoGenerate'))
|
||||
.setDesc(getTranslation('settings.autoGenerate.desc'));
|
||||
|
||||
// 自定义三选滑块
|
||||
const toggleContainer = document.createElement('div');
|
||||
toggleContainer.classList.add('mode-toggle-container');
|
||||
toggleContainer.style.width = '20%'; // 缩短滑动条长度为原来的20%
|
||||
toggleContainer.style.marginLeft = 'auto'; // 靠右显示
|
||||
|
||||
const modes = [
|
||||
{ value: AutoGenerateMode.NONE, label: getTranslation('settings.mode.none') },
|
||||
{ value: AutoGenerateMode.DAILY, label: getTranslation('settings.mode.daily') },
|
||||
{ value: AutoGenerateMode.WORKDAY, label: getTranslation('settings.mode.workday') }
|
||||
];
|
||||
|
||||
// 滑块指示器
|
||||
const slider = document.createElement('div');
|
||||
slider.classList.add('mode-toggle-slider');
|
||||
toggleContainer.appendChild(slider);
|
||||
|
||||
// 更新滑块位置
|
||||
const updateSlider = (index: number) => {
|
||||
slider.style.left = `${index * 33.33}%`;
|
||||
};
|
||||
|
||||
// 设置默认模式为仅工作日生成(WorkDay)
|
||||
let currentModeIndex = modes.findIndex(mode => mode.value === settings.autoGenerateMode);
|
||||
if (currentModeIndex === -1) {
|
||||
// 如果没有找到有效的模式,设置为工作日模式
|
||||
currentModeIndex = modes.findIndex(mode => mode.value === AutoGenerateMode.WORKDAY);
|
||||
// 更新设置到工作日模式
|
||||
this.settingsManager.updateSettings({ autoGenerateMode: AutoGenerateMode.WORKDAY });
|
||||
}
|
||||
updateSlider(currentModeIndex);
|
||||
|
||||
// 创建选项
|
||||
modes.forEach((mode, index) => {
|
||||
const option = document.createElement('div');
|
||||
option.classList.add('mode-toggle-option');
|
||||
option.textContent = mode.label;
|
||||
|
||||
if (mode.value === settings.autoGenerateMode) {
|
||||
option.classList.add('active');
|
||||
}
|
||||
|
||||
option.addEventListener('click', async () => {
|
||||
// 更新视觉状态
|
||||
toggleContainer.querySelectorAll('.mode-toggle-option').forEach(el => {
|
||||
el.classList.remove('active');
|
||||
});
|
||||
option.classList.add('active');
|
||||
|
||||
// 更新滑块位置
|
||||
updateSlider(index);
|
||||
|
||||
// 保存设置
|
||||
await this.settingsManager.updateSettings({ autoGenerateMode: mode.value });
|
||||
});
|
||||
|
||||
toggleContainer.appendChild(option);
|
||||
});
|
||||
|
||||
autoGenSetting.controlEl.appendChild(toggleContainer);
|
||||
|
||||
// 语言设置
|
||||
new Setting(containerEl)
|
||||
.setName(getTranslation('settings.language'))
|
||||
.setDesc(getTranslation('settings.language.desc'))
|
||||
.addDropdown(dropdown => {
|
||||
dropdown
|
||||
.addOption(Language.AUTO, getTranslation('settings.language.auto'))
|
||||
.addOption(Language.ZH, getTranslation('settings.language.zh'))
|
||||
.addOption(Language.EN, getTranslation('settings.language.en'))
|
||||
.setValue(settings.language)
|
||||
.onChange(async (value) => {
|
||||
await this.settingsManager.updateSettings({ language: value as Language });
|
||||
// 需要重新加载设置页面以更新翻译
|
||||
this.display();
|
||||
});
|
||||
});
|
||||
|
||||
// 通知显示时间
|
||||
new Setting(containerEl)
|
||||
.setName(getTranslation('settings.notificationDuration'))
|
||||
.setDesc(getTranslation('settings.notificationDuration.desc'))
|
||||
.addText(text => {
|
||||
const component = text
|
||||
.setValue(settings.successNotificationDuration.toString())
|
||||
.onChange(async (value) => {
|
||||
const duration = parseInt(value);
|
||||
if (!isNaN(duration) && duration > 0) {
|
||||
await this.settingsManager.updateSettings({ successNotificationDuration: duration });
|
||||
}
|
||||
});
|
||||
|
||||
// 直接设置placeholder属性
|
||||
component.inputEl.placeholder = '3000';
|
||||
|
||||
return component;
|
||||
});
|
||||
|
||||
// 任务统计功能开关
|
||||
new Setting(containerEl)
|
||||
.setName(getTranslation('settings.taskStatistics'))
|
||||
.setDesc(getTranslation('settings.taskStatistics.desc'))
|
||||
.addToggle((toggle) => {
|
||||
// 设置开关样式
|
||||
const toggleEl = toggle
|
||||
.setValue(settings.taskStatistics)
|
||||
.onChange(async (value) => {
|
||||
await this.settingsManager.updateSettings({ taskStatistics: value });
|
||||
});
|
||||
|
||||
// 自定义开关样式 - 使用DOM元素访问
|
||||
// @ts-ignore - 添加这行来忽略TypeScript警告
|
||||
const toggleControl = toggle.toggleEl || toggle.containerEl.querySelector('.checkbox-container');
|
||||
if (toggleControl) {
|
||||
toggleControl.classList.add('task-statistics-toggle');
|
||||
|
||||
// 添加图标 - 使用DOM父元素访问
|
||||
const toggleContainer = toggleControl.parentElement;
|
||||
if (toggleContainer) {
|
||||
const iconEl = createSpan({cls: 'svg-icon lucide-bar-chart-2'});
|
||||
iconEl.style.marginRight = '8px';
|
||||
iconEl.style.color = 'var(--text-accent)';
|
||||
toggleContainer.prepend(iconEl);
|
||||
|
||||
// 添加过渡效果
|
||||
toggleContainer.style.transition = 'all 0.3s ease';
|
||||
toggleControl.style.transition = 'all 0.3s ease';
|
||||
}
|
||||
}
|
||||
|
||||
return toggleEl;
|
||||
});
|
||||
|
||||
// 模板设置
|
||||
containerEl.createEl('h3', { text: getTranslation('settings.template') });
|
||||
|
||||
// 添加模板使用逻辑说明
|
||||
const templateDescription = containerEl.createEl('p', {
|
||||
text: this.settingsManager.getCurrentLanguage() === 'zh' ?
|
||||
'注意:默认模板会根据当前语言环境自动选择对应语言的内容。如果您自定义模板,将在所有语言环境中使用您的自定义内容。' :
|
||||
'Note: Default template automatically adapts to your language environment. If you customize the template, your content will be used in all language environments.'
|
||||
});
|
||||
templateDescription.style.fontSize = '0.85em';
|
||||
templateDescription.style.opacity = '0.8';
|
||||
templateDescription.style.marginBottom = '15px';
|
||||
|
||||
// 添加模板变量说明
|
||||
const templateVariablesEl = containerEl.createEl('p');
|
||||
templateVariablesEl.innerHTML = this.settingsManager.getCurrentLanguage() === 'zh' ?
|
||||
'<strong>可用变量:</strong> {{date}} - 日期, {{dateWithIcon}} - 带图标的日期, {{weekday}} - 星期几, {{yearProgress}} - 年进度, {{monthProgress}} - 月进度, {{time}} - 当前时间' :
|
||||
'<strong>Available variables:</strong> {{date}} - Date, {{dateWithIcon}} - Date with icon, {{weekday}} - Day of week, {{yearProgress}} - Year progress, {{monthProgress}} - Month progress, {{time}} - Current time';
|
||||
templateVariablesEl.style.fontSize = '0.85em';
|
||||
templateVariablesEl.style.marginBottom = '10px';
|
||||
|
||||
// 单一模板设置
|
||||
const templateSetting = new Setting(containerEl)
|
||||
.setName(getTranslation('settings.template'))
|
||||
.setClass('template-setting');
|
||||
|
||||
const templateContainer = document.createElement('div');
|
||||
templateContainer.style.width = '100%';
|
||||
|
||||
// 获取当前模板内容
|
||||
const currentTemplate = this.settingsManager.hasCustomTemplate() ?
|
||||
this.settingsManager.getSettings().customTemplate :
|
||||
this.settingsManager.getTemplateByLanguage();
|
||||
|
||||
const textarea = new TextAreaComponent(templateContainer)
|
||||
.setValue(currentTemplate)
|
||||
.setPlaceholder(this.settingsManager.getCurrentLanguage() === 'zh' ?
|
||||
'在此处输入任务模板...' :
|
||||
'Enter task template here...')
|
||||
.onChange(async (value) => {
|
||||
// 更新为自定义模板
|
||||
await this.settingsManager.updateSettings({
|
||||
customTemplate: value,
|
||||
hasCustomTemplate: true
|
||||
});
|
||||
this.updatePreview(this.previewEl, value);
|
||||
});
|
||||
|
||||
// 改进TextArea样式
|
||||
textarea.inputEl.classList.add('template-editor');
|
||||
textarea.inputEl.style.height = '200px';
|
||||
textarea.inputEl.style.border = '1px solid var(--background-modifier-border-hover)';
|
||||
textarea.inputEl.style.borderRadius = '4px';
|
||||
textarea.inputEl.style.padding = '12px';
|
||||
textarea.inputEl.style.lineHeight = '1.5';
|
||||
textarea.inputEl.style.fontSize = '14px';
|
||||
textarea.inputEl.style.fontFamily = 'var(--font-monospace)';
|
||||
textarea.inputEl.style.transition = 'all 0.2s ease';
|
||||
textarea.inputEl.style.boxShadow = 'inset 0 1px 3px rgba(0, 0, 0, 0.1)';
|
||||
textarea.inputEl.style.backgroundColor = 'var(--background-primary)';
|
||||
textarea.inputEl.style.color = 'var(--text-normal)';
|
||||
textarea.inputEl.style.resize = 'vertical';
|
||||
|
||||
// 当获取焦点时改变边框样式
|
||||
textarea.inputEl.addEventListener('focus', () => {
|
||||
textarea.inputEl.style.border = '1px solid var(--interactive-accent)';
|
||||
textarea.inputEl.style.boxShadow = '0 0 0 2px rgba(var(--interactive-accent-rgb), 0.2), inset 0 1px 3px rgba(0, 0, 0, 0.1)';
|
||||
textarea.inputEl.style.outline = 'none';
|
||||
});
|
||||
|
||||
// 失去焦点时恢复原来的边框样式
|
||||
textarea.inputEl.addEventListener('blur', () => {
|
||||
textarea.inputEl.style.border = '1px solid var(--background-modifier-border-hover)';
|
||||
textarea.inputEl.style.boxShadow = 'inset 0 1px 3px rgba(0, 0, 0, 0.1)';
|
||||
});
|
||||
|
||||
// 预览标题,使用flex布局居中
|
||||
const previewHeader = document.createElement('div');
|
||||
previewHeader.classList.add('template-preview-header');
|
||||
previewHeader.style.display = 'flex';
|
||||
previewHeader.style.justifyContent = 'space-between'; // 改为两端对齐
|
||||
previewHeader.style.marginTop = '15px';
|
||||
previewHeader.style.marginBottom = '10px';
|
||||
previewHeader.style.width = '100%';
|
||||
|
||||
// 预览按钮容器 - 左侧
|
||||
const previewBtnContainer = document.createElement('div');
|
||||
previewBtnContainer.style.display = 'flex';
|
||||
previewBtnContainer.style.alignItems = 'center';
|
||||
|
||||
// 重置按钮容器 - 右侧
|
||||
const resetBtnContainer = document.createElement('div');
|
||||
resetBtnContainer.style.display = 'flex';
|
||||
resetBtnContainer.style.alignItems = 'center';
|
||||
|
||||
// 预览按钮 - 改进样式
|
||||
const toggleButton = new ButtonComponent(previewBtnContainer)
|
||||
.setButtonText(getTranslation('settings.template.preview'));
|
||||
|
||||
// 添加样式类
|
||||
toggleButton.buttonEl.addClass(TextCenterCSS);
|
||||
toggleButton.buttonEl.style.textAlign = 'center';
|
||||
toggleButton.buttonEl.style.display = 'flex';
|
||||
toggleButton.buttonEl.style.alignItems = 'center';
|
||||
toggleButton.buttonEl.style.justifyContent = 'center';
|
||||
toggleButton.buttonEl.style.width = '130px';
|
||||
toggleButton.buttonEl.style.borderRadius = '4px';
|
||||
toggleButton.buttonEl.style.boxShadow = '0 2px 5px rgba(0,0,0,0.1)';
|
||||
toggleButton.buttonEl.style.transition = 'all 0.2s ease';
|
||||
|
||||
// 手动添加眼睛图标
|
||||
const eyeIcon = createSpan({cls: 'svg-icon lucide-eye'});
|
||||
eyeIcon.style.marginRight = '6px';
|
||||
toggleButton.buttonEl.prepend(eyeIcon);
|
||||
|
||||
// 预览区域
|
||||
this.previewEl = document.createElement('div');
|
||||
this.previewEl.classList.add('template-preview');
|
||||
this.previewEl.style.marginTop = '15px';
|
||||
this.previewEl.style.padding = '15px';
|
||||
this.previewEl.style.border = '1px dashed var(--background-modifier-border)';
|
||||
this.previewEl.style.borderRadius = '8px';
|
||||
this.previewEl.style.backgroundColor = 'var(--background-secondary)';
|
||||
this.previewEl.style.display = 'none';
|
||||
this.previewEl.style.maxHeight = '200px';
|
||||
this.previewEl.style.overflow = 'auto';
|
||||
this.previewEl.style.boxShadow = 'inset 0 0 5px rgba(0,0,0,0.1)';
|
||||
this.updatePreview(this.previewEl, currentTemplate);
|
||||
templateContainer.appendChild(this.previewEl);
|
||||
|
||||
toggleButton.onClick(() => {
|
||||
this.togglePreview(this.previewEl);
|
||||
// 切换图标和按钮文本
|
||||
if (this.previewEl && this.previewEl.classList.contains('visible')) {
|
||||
eyeIcon.className = 'svg-icon lucide-eye-off';
|
||||
toggleButton.setButtonText(getTranslation('settings.template.hide'));
|
||||
toggleButton.buttonEl.style.backgroundColor = 'var(--background-modifier-success)';
|
||||
} else {
|
||||
eyeIcon.className = 'svg-icon lucide-eye';
|
||||
toggleButton.setButtonText(getTranslation('settings.template.preview'));
|
||||
toggleButton.buttonEl.style.backgroundColor = '';
|
||||
}
|
||||
});
|
||||
|
||||
// 重置按钮 - 改进样式
|
||||
const resetBtn = new ButtonComponent(resetBtnContainer)
|
||||
.setButtonText(getTranslation('settings.resetDefault'));
|
||||
|
||||
// 添加样式类
|
||||
resetBtn.buttonEl.addClass(TextCenterCSS);
|
||||
resetBtn.buttonEl.style.textAlign = 'center';
|
||||
resetBtn.buttonEl.style.display = 'flex';
|
||||
resetBtn.buttonEl.style.alignItems = 'center';
|
||||
resetBtn.buttonEl.style.justifyContent = 'center';
|
||||
resetBtn.buttonEl.style.width = '150px';
|
||||
resetBtn.buttonEl.style.borderRadius = '4px';
|
||||
resetBtn.buttonEl.style.boxShadow = '0 2px 5px rgba(0,0,0,0.1)';
|
||||
resetBtn.buttonEl.style.transition = 'all 0.2s ease';
|
||||
|
||||
// 添加重置图标
|
||||
const resetIcon = createSpan({cls: 'svg-icon lucide-refresh-cw'});
|
||||
resetIcon.style.marginRight = '6px';
|
||||
resetBtn.buttonEl.prepend(resetIcon);
|
||||
|
||||
// 添加重置事件
|
||||
resetBtn.onClick(async () => {
|
||||
// 将自定义模板设置为空,回到使用默认模板
|
||||
await this.settingsManager.updateSettings({
|
||||
customTemplate: '',
|
||||
hasCustomTemplate: false
|
||||
});
|
||||
|
||||
// 获取当前语言的默认模板
|
||||
const defaultTemplate = this.settingsManager.getTemplateByLanguage();
|
||||
|
||||
// 更新输入框和预览
|
||||
textarea.setValue(defaultTemplate);
|
||||
this.updatePreview(this.previewEl, defaultTemplate);
|
||||
|
||||
// 显示成功提示动画
|
||||
resetBtn.buttonEl.style.backgroundColor = 'var(--background-modifier-success)';
|
||||
setTimeout(() => {
|
||||
resetBtn.buttonEl.style.backgroundColor = '';
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
// 将按钮添加到各自的容器
|
||||
previewHeader.appendChild(previewBtnContainer);
|
||||
previewHeader.appendChild(resetBtnContainer);
|
||||
templateContainer.appendChild(previewHeader);
|
||||
|
||||
templateSetting.controlEl.appendChild(templateContainer);
|
||||
|
||||
// 恢复默认设置 - 创建容器让按钮右对齐
|
||||
const resetContainer = document.createElement('div');
|
||||
resetContainer.style.display = 'flex';
|
||||
resetContainer.style.justifyContent = 'flex-end'; // 确保右对齐
|
||||
resetContainer.style.marginTop = '20px';
|
||||
resetContainer.style.marginBottom = '10px';
|
||||
containerEl.appendChild(resetContainer);
|
||||
|
||||
// 恢复默认设置按钮
|
||||
const resetDefaultBtn = new ButtonComponent(resetContainer)
|
||||
.setButtonText(getTranslation('settings.resetToDefault'));
|
||||
|
||||
// 添加样式类
|
||||
resetDefaultBtn.buttonEl.addClass(TextCenterCSS);
|
||||
resetDefaultBtn.buttonEl.addClass('danger-button');
|
||||
resetDefaultBtn.buttonEl.style.textAlign = 'center';
|
||||
resetDefaultBtn.buttonEl.style.display = 'flex';
|
||||
resetDefaultBtn.buttonEl.style.alignItems = 'center';
|
||||
resetDefaultBtn.buttonEl.style.justifyContent = 'center';
|
||||
resetDefaultBtn.buttonEl.style.width = '150px';
|
||||
|
||||
// 添加重置图标和危险按钮样式
|
||||
resetDefaultBtn.buttonEl.prepend(createSpan({cls: 'svg-icon lucide-refresh-cw'}));
|
||||
|
||||
// 为全局重置按钮添加事件处理
|
||||
resetDefaultBtn.onClick(async () => {
|
||||
await this.settingsManager.resetToDefaults();
|
||||
this.display();
|
||||
});
|
||||
|
||||
// 手动添加今日任务按钮 - 右对齐显示
|
||||
const addTaskContainer = document.createElement('div');
|
||||
addTaskContainer.style.display = 'flex';
|
||||
addTaskContainer.style.justifyContent = 'flex-end'; // 确保右对齐
|
||||
addTaskContainer.style.marginTop = '20px';
|
||||
containerEl.appendChild(addTaskContainer);
|
||||
|
||||
this.addTaskButton = new ButtonComponent(addTaskContainer)
|
||||
.setButtonText(getTranslation('settings.addTaskButton'))
|
||||
.setCta();
|
||||
|
||||
// 添加样式类 - 确保按钮文字居中
|
||||
if (this.addTaskButton && this.addTaskButton.buttonEl) {
|
||||
this.addTaskButton.buttonEl.addClass(TextCenterCSS);
|
||||
this.addTaskButton.buttonEl.style.textAlign = 'center';
|
||||
this.addTaskButton.buttonEl.style.display = 'flex';
|
||||
this.addTaskButton.buttonEl.style.alignItems = 'center';
|
||||
this.addTaskButton.buttonEl.style.justifyContent = 'center';
|
||||
}
|
||||
|
||||
// 手动添加任务按钮事件处理
|
||||
this.addTaskButton.onClick(async () => {
|
||||
// 检查目录设置
|
||||
const rootDir = this.settingsManager.getSettings().rootDir;
|
||||
|
||||
// 添加loading状态
|
||||
if (this.addTaskButton && this.addTaskButton.buttonEl) {
|
||||
this.addTaskButton.buttonEl.classList.add('loading');
|
||||
}
|
||||
this.addTaskButton?.setDisabled(true);
|
||||
|
||||
try {
|
||||
// 添加任务
|
||||
await this.taskGenerator.addTaskManually();
|
||||
} catch (e) {
|
||||
console.error("添加任务出错:", e);
|
||||
new Notice(`添加任务失败: ${e.message || e}`);
|
||||
} finally {
|
||||
// 移除loading状态
|
||||
setTimeout(() => {
|
||||
if (this.addTaskButton && this.addTaskButton.buttonEl) {
|
||||
this.addTaskButton.buttonEl.classList.remove('loading');
|
||||
}
|
||||
this.addTaskButton?.setDisabled(false);
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加图标
|
||||
this.addTaskButton.buttonEl.prepend(createSpan({cls: 'svg-icon lucide-calendar-plus'}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新模板预览
|
||||
*/
|
||||
private updatePreview(previewEl: HTMLElement | null, template: string): void {
|
||||
if (!previewEl) return;
|
||||
|
||||
const renderedContent = renderTemplate(template);
|
||||
// 使用MarkdownRenderer需要导入相关组件
|
||||
previewEl.innerHTML = renderedContent.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换预览的显示/隐藏
|
||||
*/
|
||||
private togglePreview(previewEl: HTMLElement | null): void {
|
||||
if (!previewEl) return;
|
||||
|
||||
if (previewEl.style.display === 'none') {
|
||||
previewEl.style.display = 'block';
|
||||
previewEl.classList.add('visible');
|
||||
} else {
|
||||
previewEl.style.display = 'none';
|
||||
previewEl.classList.remove('visible');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置管理器
|
||||
* 负责加载、保存和提供设置访问接口
|
||||
*/
|
||||
export class SettingsManager {
|
||||
private plugin: Plugin;
|
||||
private settings: DailyTaskSettings;
|
||||
|
||||
constructor(plugin: Plugin) {
|
||||
this.plugin = plugin;
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前设置
|
||||
*/
|
||||
getSettings(): DailyTaskSettings {
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新设置并保存
|
||||
* @param settings 要更新的设置
|
||||
*/
|
||||
async updateSettings(settings: Partial<DailyTaskSettings>): Promise<void> {
|
||||
this.settings = {
|
||||
...this.settings,
|
||||
...settings
|
||||
};
|
||||
await this.saveSettings();
|
||||
|
||||
// 更新当前语言
|
||||
this.updateCurrentLanguage();
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存设置到数据存储
|
||||
*/
|
||||
async saveSettings(): Promise<void> {
|
||||
await this.plugin.saveData(this.settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载设置
|
||||
*/
|
||||
async loadSettings(): Promise<void> {
|
||||
const loadedData = await this.plugin.loadData();
|
||||
if (loadedData) {
|
||||
// 合并默认设置和已保存的设置
|
||||
this.settings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
...loadedData
|
||||
};
|
||||
|
||||
// 确保在升级插件后,新增的设置项也有默认值
|
||||
this.ensureSettingsCompleteness();
|
||||
} else {
|
||||
// 如果没有加载到数据,使用默认设置但将自动生成模式改为工作日
|
||||
this.settings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
autoGenerateMode: AutoGenerateMode.WORKDAY
|
||||
};
|
||||
}
|
||||
|
||||
// 更新当前语言
|
||||
this.updateCurrentLanguage();
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保设置完整性,为新增的设置项提供默认值
|
||||
*/
|
||||
private ensureSettingsCompleteness(): void {
|
||||
const defaultKeys = Object.keys(DEFAULT_SETTINGS);
|
||||
defaultKeys.forEach(key => {
|
||||
// 如果当前设置中缺少某个默认设置项,添加默认值
|
||||
if (!(key in this.settings)) {
|
||||
(this.settings as any)[key] = (DEFAULT_SETTINGS as any)[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复默认设置
|
||||
*/
|
||||
async resetToDefaults(): Promise<void> {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS);
|
||||
await this.saveSettings();
|
||||
|
||||
// 更新当前语言
|
||||
this.updateCurrentLanguage();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据语言获取当前使用的模板
|
||||
* 如果当前模板不是默认模板,则不再区分语言
|
||||
*/
|
||||
getCurrentTemplate(): string {
|
||||
const language = this.getCurrentLanguage();
|
||||
|
||||
// 中文环境
|
||||
if (language === 'zh') {
|
||||
// 如果中文模板已被修改(不等于默认模板),使用中文模板
|
||||
if (this.settings.templateZh !== DEFAULT_TEMPLATE_ZH) {
|
||||
return this.settings.templateZh;
|
||||
}
|
||||
// 如果英文模板已被修改,使用英文模板
|
||||
if (this.settings.templateEn !== DEFAULT_TEMPLATE_EN) {
|
||||
return this.settings.templateEn;
|
||||
}
|
||||
// 如果都是默认模板,使用中文默认模板
|
||||
return this.settings.templateZh;
|
||||
}
|
||||
// 英文环境
|
||||
else {
|
||||
// 如果英文模板已被修改(不等于默认模板),使用英文模板
|
||||
if (this.settings.templateEn !== DEFAULT_TEMPLATE_EN) {
|
||||
return this.settings.templateEn;
|
||||
}
|
||||
// 如果中文模板已被修改,使用中文模板
|
||||
if (this.settings.templateZh !== DEFAULT_TEMPLATE_ZH) {
|
||||
return this.settings.templateZh;
|
||||
}
|
||||
// 如果都是默认模板,使用英文默认模板
|
||||
return this.settings.templateEn;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前语言设置
|
||||
*/
|
||||
getCurrentLanguage(): string {
|
||||
if (this.settings.language === Language.AUTO) {
|
||||
// 自动检测系统语言
|
||||
const systemLanguage = window.navigator.language.toLowerCase();
|
||||
return systemLanguage.startsWith('zh') ? 'zh' : 'en';
|
||||
}
|
||||
return this.settings.language;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新当前语言
|
||||
*/
|
||||
private updateCurrentLanguage(): void {
|
||||
const language = this.getCurrentLanguage();
|
||||
setCurrentLanguage(language);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前语言的模板
|
||||
*/
|
||||
getTemplateByLanguage(): string {
|
||||
const language = this.getCurrentLanguage();
|
||||
|
||||
// 中文环境
|
||||
if (language === 'zh') {
|
||||
// 如果中文模板已被修改(不等于默认模板),使用中文模板
|
||||
if (this.settings.templateZh !== DEFAULT_TEMPLATE_ZH) {
|
||||
return this.settings.templateZh;
|
||||
}
|
||||
// 如果英文模板已被修改,使用英文模板
|
||||
if (this.settings.templateEn !== DEFAULT_TEMPLATE_EN) {
|
||||
return this.settings.templateEn;
|
||||
}
|
||||
// 如果都是默认模板,使用中文默认模板
|
||||
return this.settings.templateZh;
|
||||
}
|
||||
// 英文环境
|
||||
else {
|
||||
// 如果英文模板已被修改(不等于默认模板),使用英文模板
|
||||
if (this.settings.templateEn !== DEFAULT_TEMPLATE_EN) {
|
||||
return this.settings.templateEn;
|
||||
}
|
||||
// 如果中文模板已被修改,使用中文模板
|
||||
if (this.settings.templateZh !== DEFAULT_TEMPLATE_ZH) {
|
||||
return this.settings.templateZh;
|
||||
}
|
||||
// 如果都是默认模板,使用英文默认模板
|
||||
return this.settings.templateEn;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否存在自定义模板
|
||||
*/
|
||||
hasCustomTemplate(): boolean {
|
||||
return !!this.settings.customTemplate;
|
||||
}
|
||||
}
|
||||
91
src/styles.css
Normal file
91
src/styles.css
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
/* 任务统计样式 */
|
||||
.daily-task-statistics {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(var(--interactive-accent-rgb), 0.1);
|
||||
border-left: 4px solid var(--interactive-accent);
|
||||
}
|
||||
|
||||
.daily-task-statistics h3 {
|
||||
margin-top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.1em;
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
||||
.daily-task-statistics h4 {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.95em;
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
||||
/* 提示建议样式 */
|
||||
.daily-task-suggestions {
|
||||
padding: 10px 15px;
|
||||
border-radius: 6px;
|
||||
background-color: rgba(var(--background-modifier-success-rgb), 0.1);
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* 任务统计开关样式 */
|
||||
.task-statistics-toggle {
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.task-statistics-toggle:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.task-statistics-toggle.is-enabled {
|
||||
background-color: var(--interactive-accent) !important;
|
||||
}
|
||||
|
||||
/* 自定义滚动条样式 */
|
||||
.daily-task-setting-tab ::-webkit-scrollbar {
|
||||
width: 8px; /* 设置滚动条宽度 */
|
||||
}
|
||||
|
||||
.daily-task-setting-tab ::-webkit-scrollbar-track {
|
||||
background: var(--background-secondary); /* 设置轨道背景 */
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.daily-task-setting-tab ::-webkit-scrollbar-thumb {
|
||||
background: var(--interactive-accent); /* 设置滑块颜色 */
|
||||
border-radius: 4px;
|
||||
opacity: 0.7;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.daily-task-setting-tab ::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--interactive-accent-hover); /* 滑块悬停颜色 */
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 应用滚动条样式到所有可滚动元素 */
|
||||
.daily-task-slim-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.daily-task-slim-scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.daily-task-slim-scrollbar::-webkit-scrollbar-track {
|
||||
background: var(--background-secondary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.daily-task-slim-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: var(--interactive-accent);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.daily-task-slim-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--interactive-accent-hover);
|
||||
}
|
||||
262
src/taskGenerator.js
Normal file
262
src/taskGenerator.js
Normal file
File diff suppressed because one or more lines are too long
295
src/taskGenerator.ts
Normal file
295
src/taskGenerator.ts
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
import { App, Notice, TFile, Vault } from "obsidian";
|
||||
import { getTranslation } from "./i18n/i18n";
|
||||
import { SettingsManager } from "./settings/settings";
|
||||
import { appendToFile, ensureFileExists, ensureFolderExists, getTaskFilePath, todayTaskExists, fileContains, getYesterdayDate, getYesterdayTaskFilePath, extractTasksForDate, analyzeTaskCompletion, yesterdayStatisticsExists, generateStatisticsContent } from "./utils/fileUtils";
|
||||
import { renderTemplate } from "./utils/templateEngine";
|
||||
import { getCurrentDate } from "./utils/dateUtils";
|
||||
|
||||
/**
|
||||
* 任务生成器
|
||||
* 负责创建任务文件和添加任务内容
|
||||
*/
|
||||
export class TaskGenerator {
|
||||
private app: App;
|
||||
private vault: Vault;
|
||||
private settingsManager: SettingsManager;
|
||||
|
||||
constructor(app: App, settingsManager: SettingsManager) {
|
||||
this.app = app;
|
||||
this.vault = app.vault;
|
||||
this.settingsManager = settingsManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成每日任务
|
||||
* @param openFile 是否打开文件
|
||||
* @param quietMode 静默模式,减少日志输出
|
||||
* @returns 成功或失败
|
||||
*/
|
||||
async generateDailyTask(openFile: boolean = true, quietMode: boolean = false): Promise<boolean> {
|
||||
try {
|
||||
const settings = this.settingsManager.getSettings();
|
||||
const rootDir = settings.rootDir.trim() || 'DailyTasks'; // 使用默认目录
|
||||
|
||||
// 获取任务文件路径
|
||||
const filePath = getTaskFilePath(rootDir);
|
||||
if (!quietMode) console.log(`生成任务文件路径: ${filePath}`);
|
||||
|
||||
// 解析年份和月份
|
||||
const pathParts = filePath.split('/');
|
||||
const year = pathParts.length > 1 ? pathParts[1] : '';
|
||||
const monthName = pathParts.length > 2 ? pathParts[2].replace('.md', '') : '';
|
||||
const yearFolder = `${rootDir}/${year}`;
|
||||
|
||||
// 确保根目录存在
|
||||
if (!quietMode) console.log(`正在确保根目录存在: ${rootDir}`);
|
||||
const rootCreated = await ensureFolderExists(this.vault, rootDir);
|
||||
if (!rootCreated) {
|
||||
console.error(`无法访问或创建根目录: ${rootDir}`);
|
||||
throw new Error(`无法访问根目录: ${rootDir},可能是存在同名文件或权限问题`);
|
||||
}
|
||||
if (!quietMode) console.log(`根目录确认: ${rootDir}`);
|
||||
|
||||
// 确保年份目录存在
|
||||
if (!quietMode) console.log(`正在确保年份目录存在: ${yearFolder}`);
|
||||
const yearCreated = await ensureFolderExists(this.vault, yearFolder);
|
||||
if (!yearCreated) {
|
||||
console.error(`无法访问或创建年份目录: ${yearFolder}`);
|
||||
throw new Error(`无法访问年份目录: ${yearFolder},可能是存在同名文件或权限问题`);
|
||||
}
|
||||
if (!quietMode) console.log(`年份目录确认: ${yearFolder}`);
|
||||
|
||||
// 确保月份文件存在
|
||||
if (!quietMode) console.log(`正在确保月份文件存在: ${filePath} (${monthName})`);
|
||||
const fileCreated = await ensureFileExists(this.vault, filePath);
|
||||
if (!fileCreated) {
|
||||
console.error(`无法访问或创建月份文件: ${filePath}`);
|
||||
throw new Error(`无法访问月份文件: ${filePath},请检查是否存在同名目录或权限问题`);
|
||||
}
|
||||
if (!quietMode) console.log(`月份文件确认: ${filePath}`);
|
||||
|
||||
// 检查今日任务是否已存在
|
||||
const date = getCurrentDate();
|
||||
// 更改检查方式:不仅检查纯日期,也检查带图标的日期格式
|
||||
const dateRegex = new RegExp(`## [^\\n]*${date}[^\\n]*\\n`);
|
||||
const fileContent = await this.vault.read(this.vault.getAbstractFileByPath(filePath) as TFile);
|
||||
const existingTaskCheck = dateRegex.test(fileContent);
|
||||
|
||||
if (existingTaskCheck) {
|
||||
console.log(`今日(${date})任务已存在于文件中,跳过创建`);
|
||||
|
||||
// 如果需要打开文件
|
||||
if (openFile) {
|
||||
// 显示提示并打开文件
|
||||
this.showWarningNotice(`📌 ${getTranslation('notification.taskExists')}`);
|
||||
const file = this.vault.getAbstractFileByPath(filePath);
|
||||
if (file && file instanceof TFile) {
|
||||
const leaf = this.app.workspace.getLeaf();
|
||||
await leaf.openFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
return true; // 任务已存在,视为成功
|
||||
}
|
||||
|
||||
// 如果开启了任务统计功能,在添加今日任务前添加昨日统计
|
||||
let statisticsContent = '';
|
||||
if (settings.taskStatistics) {
|
||||
statisticsContent = await this.generateYesterdayStatistics(rootDir, date, quietMode);
|
||||
}
|
||||
|
||||
// 获取任务模板 - 使用新的模板逻辑
|
||||
let template = '';
|
||||
if (this.settingsManager.hasCustomTemplate()) {
|
||||
// 使用用户自定义模板
|
||||
template = this.settingsManager.getSettings().customTemplate;
|
||||
} else {
|
||||
// 使用语言相关的默认模板
|
||||
template = this.settingsManager.getTemplateByLanguage();
|
||||
}
|
||||
|
||||
// 渲染模板
|
||||
const renderedContent = renderTemplate(template);
|
||||
|
||||
// 合并统计信息和今日任务内容
|
||||
const fullContent = statisticsContent
|
||||
? `${renderedContent}\n\n${statisticsContent}`
|
||||
: renderedContent;
|
||||
|
||||
// 追加到文件
|
||||
if (!quietMode) console.log(`正在向文件追加内容`);
|
||||
const success = await appendToFile(this.vault, filePath, fullContent);
|
||||
|
||||
if (success) {
|
||||
console.log(`✅ 任务内容追加成功 ${date}`);
|
||||
|
||||
// 如果需要打开文件
|
||||
if (openFile) {
|
||||
// 打开创建的文件
|
||||
const file = this.vault.getAbstractFileByPath(filePath);
|
||||
if (file && file instanceof TFile) {
|
||||
const leaf = this.app.workspace.getLeaf();
|
||||
await leaf.openFile(file);
|
||||
|
||||
// 延迟一下再显示通知,确保文件已经打开
|
||||
setTimeout(() => {
|
||||
this.showSuccessNotice(`✨ ${getTranslation('notification.taskAdded')}`);
|
||||
}, 300);
|
||||
} else {
|
||||
throw new Error(`文件创建成功但无法打开: ${filePath}`);
|
||||
}
|
||||
} else {
|
||||
// 静默模式,只在控制台记录
|
||||
console.log(`✨ 今日(${date})任务已静默添加,无需打开文件`);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`无法向文件追加内容: ${filePath}`);
|
||||
}
|
||||
|
||||
return success;
|
||||
} catch (error) {
|
||||
console.error("Error generating daily task:", error);
|
||||
|
||||
// 显示错误通知
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
this.showErrorNotice(`${getTranslation('notification.error')} ${errorMsg}`);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成昨日任务统计信息
|
||||
* @param rootDir 根目录
|
||||
* @param todayDate 今日日期(用于检查是否已存在统计)
|
||||
* @param quietMode 静默模式
|
||||
* @returns 统计信息内容或空字符串
|
||||
*/
|
||||
async generateYesterdayStatistics(rootDir: string, todayDate: string, quietMode: boolean = false): Promise<string> {
|
||||
try {
|
||||
// 获取昨天的日期和文件路径
|
||||
const yesterdayDate = getYesterdayDate();
|
||||
const yesterdayFilePath = getYesterdayTaskFilePath(rootDir);
|
||||
|
||||
if (!quietMode) console.log(`正在获取昨日(${yesterdayDate})任务统计,文件路径: ${yesterdayFilePath}`);
|
||||
|
||||
// 检查今日的内容中是否已经包含昨日统计
|
||||
const todayFilePath = getTaskFilePath(rootDir);
|
||||
const statsExists = await yesterdayStatisticsExists(this.vault, todayFilePath, todayDate);
|
||||
|
||||
if (statsExists) {
|
||||
if (!quietMode) console.log('昨日统计信息已存在,跳过生成');
|
||||
return '';
|
||||
}
|
||||
|
||||
// 提取昨日任务内容
|
||||
const yesterdayContent = await extractTasksForDate(this.vault, yesterdayFilePath, yesterdayDate);
|
||||
|
||||
// 如果找不到昨日任务内容
|
||||
if (!yesterdayContent) {
|
||||
if (!quietMode) console.log(`找不到昨日(${yesterdayDate})任务内容,跳过统计`);
|
||||
return '';
|
||||
}
|
||||
|
||||
// 分析任务完成情况
|
||||
const taskStats = analyzeTaskCompletion(yesterdayContent);
|
||||
|
||||
if (!quietMode) {
|
||||
console.log(`昨日任务统计: 总数=${taskStats.totalTasks}, 已完成=${taskStats.completedTasks}`);
|
||||
console.log(`未完成任务: ${taskStats.unfinishedTasksList.length}个`);
|
||||
}
|
||||
|
||||
// 如果没有任务,不生成统计信息
|
||||
if (taskStats.totalTasks === 0) {
|
||||
if (!quietMode) console.log('昨日没有任务,跳过统计');
|
||||
return '';
|
||||
}
|
||||
|
||||
// 确保在所有语言环境下都调用generateStatisticsContent
|
||||
return generateStatisticsContent(taskStats, getTranslation);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`生成昨日统计信息时出错:`, error);
|
||||
return ''; // 出错时返回空字符串,不影响今日任务生成
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动添加今日任务
|
||||
* @returns 成功或失败
|
||||
*/
|
||||
async addTaskManually(): Promise<boolean> {
|
||||
try {
|
||||
const settings = this.settingsManager.getSettings();
|
||||
const rootDir = settings.rootDir.trim() || 'DailyTasks'; // 使用默认目录
|
||||
|
||||
// 检查今日任务是否已存在
|
||||
const exists = await todayTaskExists(this.vault, rootDir);
|
||||
|
||||
if (exists) {
|
||||
// 任务已存在,显示提示
|
||||
this.showWarningNotice(`📌 ${getTranslation('notification.taskExists')}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 生成任务
|
||||
return await this.generateDailyTask();
|
||||
} catch (error) {
|
||||
console.error("Error adding task manually:", error);
|
||||
|
||||
// 显示错误通知
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
this.showErrorNotice(`${getTranslation('notification.error')} ${errorMsg}`);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示成功通知
|
||||
* @param message 通知内容
|
||||
*/
|
||||
private showSuccessNotice(message: string): void {
|
||||
const notice = new Notice(
|
||||
message,
|
||||
this.settingsManager.getSettings().successNotificationDuration
|
||||
);
|
||||
|
||||
// 添加成功样式
|
||||
if (notice.noticeEl) {
|
||||
notice.noticeEl.addClass('daily-task-success-notice');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示警告通知
|
||||
* @param message 通知内容
|
||||
*/
|
||||
private showWarningNotice(message: string): void {
|
||||
const notice = new Notice(
|
||||
message,
|
||||
this.settingsManager.getSettings().successNotificationDuration
|
||||
);
|
||||
|
||||
// 添加警告样式
|
||||
if (notice.noticeEl) {
|
||||
notice.noticeEl.addClass('daily-task-warning-notice');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示错误通知
|
||||
* @param message 通知内容
|
||||
*/
|
||||
private showErrorNotice(message: string): void {
|
||||
const notice = new Notice(
|
||||
message,
|
||||
this.settingsManager.getSettings().successNotificationDuration
|
||||
);
|
||||
|
||||
// 添加错误样式
|
||||
if (notice.noticeEl) {
|
||||
notice.noticeEl.addClass('daily-task-error-notice');
|
||||
}
|
||||
}
|
||||
}
|
||||
156
src/utils/dateUtils.js
Normal file
156
src/utils/dateUtils.js
Normal file
File diff suppressed because one or more lines are too long
177
src/utils/dateUtils.ts
Normal file
177
src/utils/dateUtils.ts
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import { getLocalizedWeekday, getTranslation } from "../i18n/i18n";
|
||||
|
||||
/**
|
||||
* 日期处理工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取当前年份
|
||||
* @returns 当前年份,如2025
|
||||
*/
|
||||
export function getCurrentYear(): string {
|
||||
return new Date().getFullYear().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前月份数字
|
||||
* @returns 当前月份,如04
|
||||
*/
|
||||
export function getCurrentMonth(): string {
|
||||
const month = (new Date().getMonth() + 1).toString();
|
||||
return month.padStart(2, '0');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前月份的本地化名称
|
||||
* @param isEnglish 是否使用英文
|
||||
* @returns 月份名称,如中文环境下的"4月",英文环境下的"April"
|
||||
*/
|
||||
export function getLocalizedMonthName(isEnglish: boolean = false): string {
|
||||
const monthIndex = new Date().getMonth(); // 0-11
|
||||
|
||||
// 中文月份名称
|
||||
const chineseMonths = [
|
||||
"1月", "2月", "3月", "4月", "5月", "6月",
|
||||
"7月", "8月", "9月", "10月", "11月", "12月"
|
||||
];
|
||||
|
||||
// 英文月份名称
|
||||
const englishMonths = [
|
||||
"January", "February", "March", "April", "May", "June",
|
||||
"July", "August", "September", "October", "November", "December"
|
||||
];
|
||||
|
||||
return isEnglish ? englishMonths[monthIndex] : chineseMonths[monthIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当天对应的图标
|
||||
* @returns 对应日期的图标
|
||||
*/
|
||||
export function getDayIcon(): string {
|
||||
const day = new Date().getDate(); // 1-31
|
||||
|
||||
// 为每天分配一个独特的图标
|
||||
const dayIcons = [
|
||||
"🌑", // 1日
|
||||
"🌒", // 2日
|
||||
"🌓", // 3日
|
||||
"🌔", // 4日
|
||||
"🌕", // 5日
|
||||
"🌖", // 6日
|
||||
"🌗", // 7日
|
||||
"🌘", // 8日
|
||||
"🌟", // 9日
|
||||
"⭐", // 10日
|
||||
"🌈", // 11日
|
||||
"🌞", // 12日
|
||||
"🌤️", // 13日
|
||||
"⛅", // 14日
|
||||
"🌦️", // 15日
|
||||
"🌧️", // 16日
|
||||
"⛈️", // 17日
|
||||
"🌩️", // 18日
|
||||
"🌪️", // 19日
|
||||
"🌫️", // 20日
|
||||
"🌬️", // 21日
|
||||
"🍀", // 22日
|
||||
"🌱", // 23日
|
||||
"🌲", // 24日
|
||||
"🌳", // 25日
|
||||
"🌴", // 26日
|
||||
"🌵", // 27日
|
||||
"🌺", // 28日
|
||||
"🌻", // 29日
|
||||
"🌼", // 30日
|
||||
"🌸", // 31日
|
||||
];
|
||||
|
||||
// 索引从0开始,天数从1开始,所以减1
|
||||
return dayIcons[day - 1] || "📅"; // 如果出现意外,返回默认日历图标
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前是否为英文环境
|
||||
* @returns 是否为英文环境
|
||||
*/
|
||||
export function isEnglishEnvironment(): boolean {
|
||||
// 通过翻译系统中的周一测试当前语言
|
||||
// 获取"weekday.mon"的翻译,如果是"Monday"则为英文环境
|
||||
const mondayText = getTranslation("weekday.mon");
|
||||
return mondayText === "Monday";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前日期
|
||||
* @returns 当前日期,如16
|
||||
*/
|
||||
export function getCurrentDay(): string {
|
||||
const day = new Date().getDate().toString();
|
||||
return day.padStart(2, '0');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前完整日期
|
||||
* @returns 完整日期,如2025-04-16
|
||||
*/
|
||||
export function getCurrentDate(): string {
|
||||
return `${getCurrentYear()}-${getCurrentMonth()}-${getCurrentDay()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前带图标的完整日期
|
||||
* @returns 带图标的完整日期,如🌕 2025-04-16
|
||||
*/
|
||||
export function getCurrentDateWithIcon(): string {
|
||||
const icon = getDayIcon();
|
||||
return `${icon} ${getCurrentDate()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前是否为工作日(周一至周五)
|
||||
* @returns 是否为工作日
|
||||
*/
|
||||
export function isWorkday(): boolean {
|
||||
const day = new Date().getDay();
|
||||
// 0是周日,1-5是周一至周五,6是周六
|
||||
return day >= 1 && day <= 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前星期几的本地化名称
|
||||
* @returns 本地化的星期名称
|
||||
*/
|
||||
export function getCurrentWeekdayName(): string {
|
||||
const day = new Date().getDay();
|
||||
return getLocalizedWeekday(day);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算当年进度百分比
|
||||
* @returns 当年进度百分比
|
||||
*/
|
||||
export function getYearProgress(): number {
|
||||
const now = new Date();
|
||||
const start = new Date(now.getFullYear(), 0, 1); // 当年1月1日
|
||||
const end = new Date(now.getFullYear() + 1, 0, 1); // 下一年1月1日
|
||||
|
||||
const totalDays = (end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24);
|
||||
const passedDays = (now.getTime() - start.getTime()) / (1000 * 60 * 60 * 24);
|
||||
|
||||
return Math.round((passedDays / totalDays) * 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算当月进度百分比
|
||||
* @returns 当月进度百分比
|
||||
*/
|
||||
export function getMonthProgress(): number {
|
||||
const now = new Date();
|
||||
const start = new Date(now.getFullYear(), now.getMonth(), 1); // 当月1日
|
||||
const end = new Date(now.getFullYear(), now.getMonth() + 1, 1); // 下个月1日
|
||||
|
||||
const totalDays = (end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24);
|
||||
const passedDays = (now.getTime() - start.getTime()) / (1000 * 60 * 60 * 24);
|
||||
|
||||
return Math.round((passedDays / totalDays) * 100);
|
||||
}
|
||||
428
src/utils/fileUtils.js
Normal file
428
src/utils/fileUtils.js
Normal file
File diff suppressed because one or more lines are too long
464
src/utils/fileUtils.ts
Normal file
464
src/utils/fileUtils.ts
Normal file
|
|
@ -0,0 +1,464 @@
|
|||
import { normalizePath, TAbstractFile, TFile, TFolder, Vault } from "obsidian";
|
||||
import { getCurrentDate, getCurrentMonth, getCurrentYear, getLocalizedMonthName, isEnglishEnvironment } from "./dateUtils";
|
||||
import { getTranslation, TranslationKey } from '../i18n/i18n';
|
||||
import { Notice } from 'obsidian';
|
||||
import { DEFAULT_SETTINGS } from '../settings/index';
|
||||
|
||||
/**
|
||||
* 文件操作工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 确保目录存在,如果不存在则创建
|
||||
* @param vault Obsidian文件系统
|
||||
* @param path 目录路径
|
||||
* @returns 是否成功创建或已存在
|
||||
*/
|
||||
export async function ensureFolderExists(vault: Vault, path: string): Promise<boolean> {
|
||||
try {
|
||||
// 确保路径以/结尾,便于处理
|
||||
path = path.endsWith('/') ? path : path + '/';
|
||||
|
||||
// 逐级创建目录
|
||||
const folders = path.split('/').filter(p => p.length > 0);
|
||||
let currentPath = '';
|
||||
|
||||
for (const folder of folders) {
|
||||
// 更新当前路径
|
||||
if (currentPath) {
|
||||
currentPath += '/' + folder;
|
||||
} else {
|
||||
currentPath = folder;
|
||||
}
|
||||
|
||||
// 检查路径是否存在
|
||||
console.log(`检查目录: ${currentPath}`);
|
||||
const existingItem = vault.getAbstractFileByPath(currentPath);
|
||||
|
||||
if (!existingItem) {
|
||||
// 路径不存在,创建文件夹
|
||||
console.log(`目录不存在,正在创建: ${currentPath}`);
|
||||
try {
|
||||
await vault.createFolder(currentPath);
|
||||
console.log(`目录创建成功: ${currentPath}`);
|
||||
} catch (e) {
|
||||
// 捕获可能的"文件夹已存在"错误,但继续执行
|
||||
// 这里处理可能的竞态条件:检查不存在但创建时已存在的情况
|
||||
console.log(`创建目录时出现异常,可能已被其他进程创建: ${e}`);
|
||||
// 再次检查目录是否存在
|
||||
const folderAfterError = vault.getAbstractFileByPath(currentPath);
|
||||
if (!folderAfterError || !(folderAfterError instanceof TFolder)) {
|
||||
console.error(`目录创建失败,且无法确认目录已存在: ${currentPath}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if (!(existingItem instanceof TFolder)) {
|
||||
// 路径存在但不是文件夹(可能是同名文件)
|
||||
console.error(`路径 ${currentPath} 已存在但不是文件夹,而是: ${existingItem.constructor.name}`);
|
||||
return false;
|
||||
} else {
|
||||
// 文件夹已存在,继续检查下一级
|
||||
console.log(`目录已存在,无需创建: ${currentPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`创建目录时出现未预期错误(${path}):`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保文件存在,如果不存在则创建
|
||||
* @param vault Obsidian文件系统
|
||||
* @param path 文件路径
|
||||
* @param content 文件内容
|
||||
* @returns 是否成功创建或已存在
|
||||
*/
|
||||
export async function ensureFileExists(vault: Vault, path: string, content: string = ''): Promise<boolean> {
|
||||
try {
|
||||
// 先检查这个路径是否有文件或目录
|
||||
const existingItem = vault.getAbstractFileByPath(path);
|
||||
|
||||
// 如果已存在
|
||||
if (existingItem) {
|
||||
// 检查是否为文件而不是文件夹
|
||||
if (existingItem instanceof TFile) {
|
||||
console.log(`文件已存在,无需创建: ${path}`);
|
||||
return true; // 文件已存在,跳过创建
|
||||
} else {
|
||||
// 存在但不是文件(可能是同名文件夹)
|
||||
console.error(`路径 ${path} 存在但不是文件,无法创建文件,可能是同名目录`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 文件不存在,检查父文件夹是否存在
|
||||
const lastSlashIndex = path.lastIndexOf('/');
|
||||
if (lastSlashIndex > 0) {
|
||||
const parentPath = path.substring(0, lastSlashIndex);
|
||||
console.log(`检查文件父目录: ${parentPath}`);
|
||||
const parentExists = await ensureFolderExists(vault, parentPath);
|
||||
if (!parentExists) {
|
||||
console.error(`无法确保父目录存在: ${parentPath}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建文件
|
||||
try {
|
||||
console.log(`开始创建文件: ${path}`);
|
||||
await vault.create(path, content);
|
||||
console.log(`文件创建成功: ${path}`);
|
||||
return true;
|
||||
} catch (e) {
|
||||
// 如果创建时报错,再次检查文件是否已被创建
|
||||
// 这可能是由于竞态条件或其他进程同时创建了该文件
|
||||
console.log(`创建文件时出现异常: ${e}`);
|
||||
|
||||
// 再次检查文件是否存在
|
||||
const fileAfterError = vault.getAbstractFileByPath(path);
|
||||
if (fileAfterError && fileAfterError instanceof TFile) {
|
||||
console.log(`尽管出现异常,但文件已存在,可能被其他进程创建: ${path}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
console.error(`文件创建最终失败: ${path}`);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`创建文件时出现未预期错误(${path}):`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向文件追加内容
|
||||
* @param vault Obsidian文件系统
|
||||
* @param path 文件路径
|
||||
* @param content 要追加的内容
|
||||
* @returns 是否成功追加
|
||||
*/
|
||||
export async function appendToFile(vault: Vault, path: string, content: string): Promise<boolean> {
|
||||
try {
|
||||
const file = vault.getAbstractFileByPath(path);
|
||||
if (file && file instanceof TFile) {
|
||||
// 文件存在,追加内容
|
||||
try {
|
||||
const currentContent = await vault.read(file);
|
||||
await vault.modify(file, currentContent + "\n\n" + content);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(`读取或修改文件时出错: ${e}`);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// 文件不存在,创建文件并写入内容
|
||||
console.log(`文件不存在,尝试创建: ${path}`);
|
||||
return await ensureFileExists(vault, path, content);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error appending to file at ${path}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件中是否包含指定内容
|
||||
* @param vault Obsidian文件系统
|
||||
* @param path 文件路径
|
||||
* @param content 要检查的内容
|
||||
* @returns 是否包含指定内容
|
||||
*/
|
||||
export async function fileContains(vault: Vault, path: string, content: string): Promise<boolean> {
|
||||
try {
|
||||
const file = vault.getAbstractFileByPath(path);
|
||||
if (file && file instanceof TFile) {
|
||||
try {
|
||||
const currentContent = await vault.read(file);
|
||||
return currentContent.includes(content);
|
||||
} catch (e) {
|
||||
console.error(`读取文件内容时出错: ${e}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 文件不存在,显然不包含指定内容
|
||||
console.log(`文件不存在,无法检查内容: ${path}`);
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error(`Error checking file content at ${path}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据当前日期生成任务文件路径
|
||||
* @param rootDir 根目录
|
||||
* @returns 任务文件路径
|
||||
*/
|
||||
export function getTaskFilePath(rootDir: string): string {
|
||||
const year = getCurrentYear();
|
||||
|
||||
// 根据环境选择不同的月份命名方式
|
||||
const isEnglish = isEnglishEnvironment();
|
||||
const monthName = getLocalizedMonthName(isEnglish);
|
||||
|
||||
// 使用本地化的月份名称生成文件路径
|
||||
return normalizePath(`${rootDir}/${year}/${monthName}.md`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查今日任务是否已存在
|
||||
* @param vault Obsidian文件系统
|
||||
* @param rootDir 根目录
|
||||
* @returns 是否已存在
|
||||
*/
|
||||
export async function todayTaskExists(vault: Vault, rootDir: string): Promise<boolean> {
|
||||
const taskFilePath = getTaskFilePath(rootDir);
|
||||
const date = getCurrentDate();
|
||||
|
||||
// 查找文件是否存在以及是否包含今天的日期标题(包括带图标格式)
|
||||
try {
|
||||
console.log(`检查今日任务是否存在于: ${taskFilePath}`);
|
||||
// 更改检查方式:改用正则表达式匹配任何包含当前日期的标题行
|
||||
const file = vault.getAbstractFileByPath(taskFilePath);
|
||||
if (file && file instanceof TFile) {
|
||||
const content = await vault.read(file);
|
||||
const dateRegex = new RegExp(`## [^\\n]*${date}[^\\n]*\\n`);
|
||||
return dateRegex.test(content);
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error(`检查今日任务时出错:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取前一天的日期
|
||||
* @returns 前一天的日期,格式为YYYY-MM-DD
|
||||
*/
|
||||
export function getYesterdayDate(): string {
|
||||
const today = new Date();
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(today.getDate() - 1);
|
||||
|
||||
const year = yesterday.getFullYear();
|
||||
const month = (yesterday.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = yesterday.getDate().toString().padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取前一天的任务文件路径
|
||||
* @param rootDir 根目录
|
||||
* @returns 前一天任务文件的路径
|
||||
*/
|
||||
export function getYesterdayTaskFilePath(rootDir: string): string {
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
const year = yesterday.getFullYear().toString();
|
||||
const isEnglish = isEnglishEnvironment();
|
||||
|
||||
// 获取本地化的月份名称
|
||||
const month = new Date().getMonth();
|
||||
const yesterdayMonth = yesterday.getMonth();
|
||||
|
||||
// 创建一个临时Date对象用于获取前一天的月份
|
||||
const tempDate = new Date();
|
||||
tempDate.setMonth(yesterdayMonth);
|
||||
|
||||
// 判断是否为英文环境,获取对应的月份名称
|
||||
const monthName = isEnglish ?
|
||||
getMonthNameEN(yesterdayMonth) :
|
||||
getMonthNameZH(yesterdayMonth);
|
||||
|
||||
return normalizePath(`${rootDir}/${year}/${monthName}.md`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取中文月份名称
|
||||
* @param monthIndex 月份索引(0-11)
|
||||
* @returns 中文月份名称
|
||||
*/
|
||||
function getMonthNameZH(monthIndex: number): string {
|
||||
const months = [
|
||||
"1月", "2月", "3月", "4月", "5月", "6月",
|
||||
"7月", "8月", "9月", "10月", "11月", "12月"
|
||||
];
|
||||
return months[monthIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取英文月份名称
|
||||
* @param monthIndex 月份索引(0-11)
|
||||
* @returns 英文月份名称
|
||||
*/
|
||||
function getMonthNameEN(monthIndex: number): string {
|
||||
const months = [
|
||||
"January", "February", "March", "April", "May", "June",
|
||||
"July", "August", "September", "October", "November", "December"
|
||||
];
|
||||
return months[monthIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件内容中提取特定日期的任务内容
|
||||
* @param vault Obsidian保险库
|
||||
* @param filePath 文件路径
|
||||
* @param date 日期(YYYY-MM-DD格式)
|
||||
* @returns 该日期的任务内容或null
|
||||
*/
|
||||
export async function extractTasksForDate(vault: Vault, filePath: string, date: string): Promise<string | null> {
|
||||
try {
|
||||
// 检查文件是否存在
|
||||
const file = vault.getAbstractFileByPath(filePath);
|
||||
if (!file || !(file instanceof TFile)) {
|
||||
console.log(`找不到文件: ${filePath}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
const content = await vault.read(file);
|
||||
|
||||
// 寻找日期标题,支持带图标格式
|
||||
const dateHeaderRegex = new RegExp(`## [^\\n]*${date}[^\\n]*\\n(.*?)(?=\\n## |$)`, 's');
|
||||
const match = content.match(dateHeaderRegex);
|
||||
|
||||
if (match && match[1]) {
|
||||
return match[1].trim();
|
||||
}
|
||||
|
||||
console.log(`找不到日期 ${date} 的任务内容`);
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error(`提取任务内容时出错: ${error}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析任务内容,统计总任务数和已完成任务数
|
||||
* @param taskContent 任务内容文本
|
||||
* @returns 任务统计结果 {totalTasks, completedTasks, unfinishedTasksList}
|
||||
*/
|
||||
export function analyzeTaskCompletion(taskContent: string): {
|
||||
totalTasks: number;
|
||||
completedTasks: number;
|
||||
unfinishedTasksList: string[];
|
||||
} {
|
||||
// 默认返回结果
|
||||
const result = {
|
||||
totalTasks: 0,
|
||||
completedTasks: 0,
|
||||
unfinishedTasksList: [] as string[]
|
||||
};
|
||||
|
||||
if (!taskContent) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// 匹配所有任务行(包括已完成和未完成)
|
||||
const allTasksRegex = /- \[([ x])\] (.+)$/gm;
|
||||
const completedTasksRegex = /- \[x\] (.+)$/gm;
|
||||
const unfinishedTasksRegex = /- \[ \] (.+)$/gm;
|
||||
|
||||
// 统计总任务数
|
||||
const allTasksMatches = [...taskContent.matchAll(allTasksRegex)];
|
||||
result.totalTasks = allTasksMatches.length;
|
||||
|
||||
// 统计已完成任务数
|
||||
const completedTasksMatches = [...taskContent.matchAll(completedTasksRegex)];
|
||||
result.completedTasks = completedTasksMatches.length;
|
||||
|
||||
// 提取未完成任务内容
|
||||
const unfinishedTasksMatches = [...taskContent.matchAll(unfinishedTasksRegex)];
|
||||
result.unfinishedTasksList = unfinishedTasksMatches.map(match => match[1].trim());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查昨日统计信息是否已存在
|
||||
* @param vault Obsidian保险库
|
||||
* @param filePath 文件路径
|
||||
* @param date 今日日期(用于检查昨日统计)
|
||||
* @returns 是否存在昨日统计信息
|
||||
*/
|
||||
export async function yesterdayStatisticsExists(vault: Vault, filePath: string, date: string): Promise<boolean> {
|
||||
try {
|
||||
// 检查文件是否存在
|
||||
const file = vault.getAbstractFileByPath(filePath);
|
||||
if (!file || !(file instanceof TFile)) {
|
||||
console.log(`找不到文件: ${filePath}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
const content = await vault.read(file);
|
||||
|
||||
// 寻找今日日期下的昨日统计标题,支持带图标格式
|
||||
const statisticsTitleRegex = new RegExp(`## [^\\n]*${date}[^\\n]*\\n.*?${getTranslation('statistics.title')}`, 's');
|
||||
return statisticsTitleRegex.test(content);
|
||||
} catch (error) {
|
||||
console.error(`检查昨日统计信息时出错: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成昨日统计信息内容
|
||||
* @param tasks 任务统计对象或任务列表
|
||||
* @param t 翻译函数
|
||||
* @returns 格式化的统计信息字符串
|
||||
*/
|
||||
export function generateStatisticsContent(
|
||||
tasks: { totalTasks: number; completedTasks: number; unfinishedTasksList: string[] } | { task: string; isCompleted: boolean }[],
|
||||
t: (key: TranslationKey) => string
|
||||
): string {
|
||||
let totalTasks: number;
|
||||
let completedTasks: number;
|
||||
let unfinishedTasks: { task: string }[] = [];
|
||||
|
||||
// 检查输入类型
|
||||
if (Array.isArray(tasks)) {
|
||||
// 如果是任务列表数组
|
||||
totalTasks = tasks.length;
|
||||
completedTasks = tasks.filter(task => task.isCompleted).length;
|
||||
unfinishedTasks = tasks.filter(task => !task.isCompleted).map(task => ({ task: task.task }));
|
||||
} else {
|
||||
// 如果是统计对象
|
||||
totalTasks = tasks.totalTasks;
|
||||
completedTasks = tasks.completedTasks;
|
||||
unfinishedTasks = tasks.unfinishedTasksList.map(task => ({ task }));
|
||||
}
|
||||
|
||||
const completionRate = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
|
||||
const displayTasks = unfinishedTasks.slice(0, 5);
|
||||
|
||||
let content = `## ${t('statistics.title')}\n---\n`;
|
||||
content += `- ${t('statistics.totalTasks')}: ${totalTasks}\n`;
|
||||
content += `- ${t('statistics.completedTasks')}: ${completedTasks}\n`;
|
||||
content += `- ${t('statistics.completionRate')}: ${completionRate}%\n\n`;
|
||||
|
||||
if (unfinishedTasks.length > 0) {
|
||||
content += `### ${t('statistics.suggestions')}\n---\n`;
|
||||
|
||||
displayTasks.forEach(item => {
|
||||
content += `- [ ] ${item.task}\n`;
|
||||
});
|
||||
|
||||
if (unfinishedTasks.length > 5) {
|
||||
const remaining = unfinishedTasks.length - 5;
|
||||
const moreTasks = remaining === 1
|
||||
? t('statistics.moreTasks.singular')
|
||||
: `${remaining} ${t('statistics.moreTasks.plural')}`;
|
||||
content += `- *${moreTasks}*\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
51
src/utils/templateEngine.js
Normal file
51
src/utils/templateEngine.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { getCurrentDate, getCurrentWeekdayName, getYearProgress, getMonthProgress, getCurrentDateWithIcon } from "./dateUtils";
|
||||
/**
|
||||
* 模板变量渲染引擎
|
||||
*/
|
||||
/**
|
||||
* 获取当前时间
|
||||
* @returns 当前时间,如10:30
|
||||
*/
|
||||
function getCurrentTime() {
|
||||
const now = new Date();
|
||||
const hours = now.getHours().toString().padStart(2, '0');
|
||||
const minutes = now.getMinutes().toString().padStart(2, '0');
|
||||
return `${hours}:${minutes}`;
|
||||
}
|
||||
/**
|
||||
* 渲染模板内容,替换其中的变量
|
||||
* @param template 模板内容
|
||||
* @returns 渲染后的内容
|
||||
*/
|
||||
export function renderTemplate(template) {
|
||||
// 定义变量映射
|
||||
const variableMap = {
|
||||
'date': getCurrentDate(),
|
||||
'dateWithIcon': getCurrentDateWithIcon(),
|
||||
'weekday': getCurrentWeekdayName(),
|
||||
'yearProgress': getYearProgress(),
|
||||
'monthProgress': getMonthProgress(),
|
||||
'time': getCurrentTime()
|
||||
};
|
||||
// 替换模板中的变量
|
||||
let renderedContent = template;
|
||||
for (const [variable, value] of Object.entries(variableMap)) {
|
||||
renderedContent = renderedContent.replace(new RegExp(`{{${variable}}}`, 'g'), value.toString());
|
||||
}
|
||||
return renderedContent;
|
||||
}
|
||||
/**
|
||||
* 获取变量说明
|
||||
* @returns 变量说明,包括中英文
|
||||
*/
|
||||
export function getTemplateVariables() {
|
||||
return {
|
||||
'date': '当前日期 / Current date (YYYY-MM-DD)',
|
||||
'dateWithIcon': '带图标的当前日期 / Current date with daily icon',
|
||||
'weekday': '当前星期 / Current weekday',
|
||||
'yearProgress': '年度进度百分比 / Year progress percentage',
|
||||
'monthProgress': '月度进度百分比 / Month progress percentage',
|
||||
'time': '当前时间 / Current time (HH:MM)'
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGVtcGxhdGVFbmdpbmUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJ0ZW1wbGF0ZUVuZ2luZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQ0gsY0FBYyxFQUNkLHFCQUFxQixFQUNyQixlQUFlLEVBQ2YsZ0JBQWdCLEVBQ2hCLHNCQUFzQixFQUN6QixNQUFNLGFBQWEsQ0FBQztBQUVyQjs7R0FFRztBQUVIOzs7R0FHRztBQUNILFNBQVMsY0FBYztJQUNuQixNQUFNLEdBQUcsR0FBRyxJQUFJLElBQUksRUFBRSxDQUFDO0lBQ3ZCLE1BQU0sS0FBSyxHQUFHLEdBQUcsQ0FBQyxRQUFRLEVBQUUsQ0FBQyxRQUFRLEVBQUUsQ0FBQyxRQUFRLENBQUMsQ0FBQyxFQUFFLEdBQUcsQ0FBQyxDQUFDO0lBQ3pELE1BQU0sT0FBTyxHQUFHLEdBQUcsQ0FBQyxVQUFVLEVBQUUsQ0FBQyxRQUFRLEVBQUUsQ0FBQyxRQUFRLENBQUMsQ0FBQyxFQUFFLEdBQUcsQ0FBQyxDQUFDO0lBQzdELE9BQU8sR0FBRyxLQUFLLElBQUksT0FBTyxFQUFFLENBQUM7QUFDakMsQ0FBQztBQUVEOzs7O0dBSUc7QUFDSCxNQUFNLFVBQVUsY0FBYyxDQUFDLFFBQWdCO0lBQzNDLFNBQVM7SUFDVCxNQUFNLFdBQVcsR0FBb0M7UUFDakQsTUFBTSxFQUFFLGNBQWMsRUFBRTtRQUN4QixjQUFjLEVBQUUsc0JBQXNCLEVBQUU7UUFDeEMsU0FBUyxFQUFFLHFCQUFxQixFQUFFO1FBQ2xDLGNBQWMsRUFBRSxlQUFlLEVBQUU7UUFDakMsZUFBZSxFQUFFLGdCQUFnQixFQUFFO1FBQ25DLE1BQU0sRUFBRSxjQUFjLEVBQUU7S0FDM0IsQ0FBQztJQUVGLFdBQVc7SUFDWCxJQUFJLGVBQWUsR0FBRyxRQUFRLENBQUM7SUFDL0IsS0FBSyxNQUFNLENBQUMsUUFBUSxFQUFFLEtBQUssQ0FBQyxJQUFJLE1BQU0sQ0FBQyxPQUFPLENBQUMsV0FBVyxDQUFDLEVBQUU7UUFDekQsZUFBZSxHQUFHLGVBQWUsQ0FBQyxPQUFPLENBQ3JDLElBQUksTUFBTSxDQUFDLEtBQUssUUFBUSxJQUFJLEVBQUUsR0FBRyxDQUFDLEVBQ2xDLEtBQUssQ0FBQyxRQUFRLEVBQUUsQ0FDbkIsQ0FBQztLQUNMO0lBRUQsT0FBTyxlQUFlLENBQUM7QUFDM0IsQ0FBQztBQUVEOzs7R0FHRztBQUNILE1BQU0sVUFBVSxvQkFBb0I7SUFDaEMsT0FBTztRQUNILE1BQU0sRUFBRSxrQ0FBa0M7UUFDMUMsY0FBYyxFQUFFLHlDQUF5QztRQUN6RCxTQUFTLEVBQUUsd0JBQXdCO1FBQ25DLGNBQWMsRUFBRSxvQ0FBb0M7UUFDcEQsZUFBZSxFQUFFLHFDQUFxQztRQUN0RCxNQUFNLEVBQUUsNkJBQTZCO0tBQ3hDLENBQUM7QUFDTixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgXHJcbiAgICBnZXRDdXJyZW50RGF0ZSwgXHJcbiAgICBnZXRDdXJyZW50V2Vla2RheU5hbWUsIFxyXG4gICAgZ2V0WWVhclByb2dyZXNzLCBcclxuICAgIGdldE1vbnRoUHJvZ3Jlc3MsIFxyXG4gICAgZ2V0Q3VycmVudERhdGVXaXRoSWNvbiBcclxufSBmcm9tIFwiLi9kYXRlVXRpbHNcIjtcclxuXHJcbi8qKlxyXG4gKiDmqKHmnb/lj5jph4/muLLmn5PlvJXmk45cclxuICovXHJcblxyXG4vKipcclxuICog6I635Y+W5b2T5YmN5pe26Ze0XHJcbiAqIEByZXR1cm5zIOW9k+WJjeaXtumXtO+8jOWmgjEwOjMwXHJcbiAqL1xyXG5mdW5jdGlvbiBnZXRDdXJyZW50VGltZSgpOiBzdHJpbmcge1xyXG4gICAgY29uc3Qgbm93ID0gbmV3IERhdGUoKTtcclxuICAgIGNvbnN0IGhvdXJzID0gbm93LmdldEhvdXJzKCkudG9TdHJpbmcoKS5wYWRTdGFydCgyLCAnMCcpO1xyXG4gICAgY29uc3QgbWludXRlcyA9IG5vdy5nZXRNaW51dGVzKCkudG9TdHJpbmcoKS5wYWRTdGFydCgyLCAnMCcpO1xyXG4gICAgcmV0dXJuIGAke2hvdXJzfToke21pbnV0ZXN9YDtcclxufVxyXG5cclxuLyoqXHJcbiAqIOa4suafk+aooeadv+WGheWuue+8jOabv+aNouWFtuS4reeahOWPmOmHj1xyXG4gKiBAcGFyYW0gdGVtcGxhdGUg5qih5p2/5YaF5a65XHJcbiAqIEByZXR1cm5zIOa4suafk+WQjueahOWGheWuuVxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIHJlbmRlclRlbXBsYXRlKHRlbXBsYXRlOiBzdHJpbmcpOiBzdHJpbmcge1xyXG4gICAgLy8g5a6a5LmJ5Y+Y6YeP5pig5bCEXHJcbiAgICBjb25zdCB2YXJpYWJsZU1hcDogUmVjb3JkPHN0cmluZywgc3RyaW5nIHwgbnVtYmVyPiA9IHtcclxuICAgICAgICAnZGF0ZSc6IGdldEN1cnJlbnREYXRlKCksXHJcbiAgICAgICAgJ2RhdGVXaXRoSWNvbic6IGdldEN1cnJlbnREYXRlV2l0aEljb24oKSxcclxuICAgICAgICAnd2Vla2RheSc6IGdldEN1cnJlbnRXZWVrZGF5TmFtZSgpLFxyXG4gICAgICAgICd5ZWFyUHJvZ3Jlc3MnOiBnZXRZZWFyUHJvZ3Jlc3MoKSxcclxuICAgICAgICAnbW9udGhQcm9ncmVzcyc6IGdldE1vbnRoUHJvZ3Jlc3MoKSxcclxuICAgICAgICAndGltZSc6IGdldEN1cnJlbnRUaW1lKClcclxuICAgIH07XHJcbiAgICBcclxuICAgIC8vIOabv+aNouaooeadv+S4reeahOWPmOmHj1xyXG4gICAgbGV0IHJlbmRlcmVkQ29udGVudCA9IHRlbXBsYXRlO1xyXG4gICAgZm9yIChjb25zdCBbdmFyaWFibGUsIHZhbHVlXSBvZiBPYmplY3QuZW50cmllcyh2YXJpYWJsZU1hcCkpIHtcclxuICAgICAgICByZW5kZXJlZENvbnRlbnQgPSByZW5kZXJlZENvbnRlbnQucmVwbGFjZShcclxuICAgICAgICAgICAgbmV3IFJlZ0V4cChge3ske3ZhcmlhYmxlfX19YCwgJ2cnKSwgXHJcbiAgICAgICAgICAgIHZhbHVlLnRvU3RyaW5nKClcclxuICAgICAgICApO1xyXG4gICAgfVxyXG4gICAgXHJcbiAgICByZXR1cm4gcmVuZGVyZWRDb250ZW50O1xyXG59XHJcblxyXG4vKipcclxuICog6I635Y+W5Y+Y6YeP6K+05piOXHJcbiAqIEByZXR1cm5zIOWPmOmHj+ivtOaYju+8jOWMheaLrOS4reiLseaWh1xyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIGdldFRlbXBsYXRlVmFyaWFibGVzKCk6IFJlY29yZDxzdHJpbmcsIHN0cmluZz4ge1xyXG4gICAgcmV0dXJuIHtcclxuICAgICAgICAnZGF0ZSc6ICflvZPliY3ml6XmnJ8gLyBDdXJyZW50IGRhdGUgKFlZWVktTU0tREQpJyxcclxuICAgICAgICAnZGF0ZVdpdGhJY29uJzogJ+W4puWbvuagh+eahOW9k+WJjeaXpeacnyAvIEN1cnJlbnQgZGF0ZSB3aXRoIGRhaWx5IGljb24nLFxyXG4gICAgICAgICd3ZWVrZGF5JzogJ+W9k+WJjeaYn+acnyAvIEN1cnJlbnQgd2Vla2RheScsXHJcbiAgICAgICAgJ3llYXJQcm9ncmVzcyc6ICflubTluqbov5vluqbnmb7liIbmr5QgLyBZZWFyIHByb2dyZXNzIHBlcmNlbnRhZ2UnLFxyXG4gICAgICAgICdtb250aFByb2dyZXNzJzogJ+aciOW6pui/m+W6pueZvuWIhuavlCAvIE1vbnRoIHByb2dyZXNzIHBlcmNlbnRhZ2UnLFxyXG4gICAgICAgICd0aW1lJzogJ+W9k+WJjeaXtumXtCAvIEN1cnJlbnQgdGltZSAoSEg6TU0pJ1xyXG4gICAgfTtcclxufSAiXX0=
|
||||
65
src/utils/templateEngine.ts
Normal file
65
src/utils/templateEngine.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import {
|
||||
getCurrentDate,
|
||||
getCurrentWeekdayName,
|
||||
getYearProgress,
|
||||
getMonthProgress,
|
||||
getCurrentDateWithIcon
|
||||
} from "./dateUtils";
|
||||
|
||||
/**
|
||||
* 模板变量渲染引擎
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取当前时间
|
||||
* @returns 当前时间,如10:30
|
||||
*/
|
||||
function getCurrentTime(): string {
|
||||
const now = new Date();
|
||||
const hours = now.getHours().toString().padStart(2, '0');
|
||||
const minutes = now.getMinutes().toString().padStart(2, '0');
|
||||
return `${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染模板内容,替换其中的变量
|
||||
* @param template 模板内容
|
||||
* @returns 渲染后的内容
|
||||
*/
|
||||
export function renderTemplate(template: string): string {
|
||||
// 定义变量映射
|
||||
const variableMap: Record<string, string | number> = {
|
||||
'date': getCurrentDate(),
|
||||
'dateWithIcon': getCurrentDateWithIcon(),
|
||||
'weekday': getCurrentWeekdayName(),
|
||||
'yearProgress': getYearProgress(),
|
||||
'monthProgress': getMonthProgress(),
|
||||
'time': getCurrentTime()
|
||||
};
|
||||
|
||||
// 替换模板中的变量
|
||||
let renderedContent = template;
|
||||
for (const [variable, value] of Object.entries(variableMap)) {
|
||||
renderedContent = renderedContent.replace(
|
||||
new RegExp(`{{${variable}}}`, 'g'),
|
||||
value.toString()
|
||||
);
|
||||
}
|
||||
|
||||
return renderedContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取变量说明
|
||||
* @returns 变量说明,包括中英文
|
||||
*/
|
||||
export function getTemplateVariables(): Record<string, string> {
|
||||
return {
|
||||
'date': '当前日期 / Current date (YYYY-MM-DD)',
|
||||
'dateWithIcon': '带图标的当前日期 / Current date with daily icon',
|
||||
'weekday': '当前星期 / Current weekday',
|
||||
'yearProgress': '年度进度百分比 / Year progress percentage',
|
||||
'monthProgress': '月度进度百分比 / Month progress percentage',
|
||||
'time': '当前时间 / Current time (HH:MM)'
|
||||
};
|
||||
}
|
||||
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES2017",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": false,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ES5",
|
||||
"ES6",
|
||||
"ES7",
|
||||
"ES2017"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
}
|
||||
18
version-bump.mjs
Normal file
18
version-bump.mjs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { readFileSync, writeFileSync } from "fs";
|
||||
|
||||
const targetVersion = process.argv[2];
|
||||
const minAppVersion = process.argv[3];
|
||||
|
||||
// read minAppVersion from manifest.json if it is not provided
|
||||
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
|
||||
const { minAppVersion: currentMinAppVersion } = manifest;
|
||||
manifest.version = targetVersion;
|
||||
if (minAppVersion) {
|
||||
manifest.minAppVersion = minAppVersion;
|
||||
}
|
||||
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
|
||||
|
||||
// update versions.json with target version and minAppVersion
|
||||
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
|
||||
versions[targetVersion] = minAppVersion || currentMinAppVersion;
|
||||
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
|
||||
3
versions.json
Normal file
3
versions.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"1.0.0": "0.15.0"
|
||||
}
|
||||
Loading…
Reference in a new issue