Compare commits
43 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66d7249f33 | ||
|
|
8bd5291a93 | ||
|
|
c90daef85a | ||
|
|
cb3b0cf3ae | ||
|
|
913c18a175 | ||
|
|
f9380abeb2 | ||
|
|
bd1a03d934 | ||
|
|
df62a30173 | ||
|
|
6277362190 | ||
|
|
32df0e1ddf | ||
|
|
94de5560c6 | ||
|
|
0402a457bd | ||
|
|
1b9f5d624e | ||
|
|
84ee2a34cb | ||
|
|
c274fb7da9 | ||
|
|
2fe3566a08 | ||
|
|
b9880e86a7 | ||
|
|
0c608308d1 | ||
|
|
2d27bde9d7 | ||
|
|
f9f54906a7 | ||
|
|
33cd6fd0df | ||
|
|
c20a5c8cc4 | ||
|
|
0a9c4b7e47 | ||
|
|
9965d9809f | ||
|
|
2403abddfa | ||
|
|
9be34345c5 | ||
|
|
32b5d37a16 | ||
|
|
f960555b96 | ||
|
|
4f90c7e705 | ||
|
|
56e5d3a468 | ||
|
|
9d5b445ff1 | ||
|
|
bc60c1f5f9 | ||
|
|
f39a381983 | ||
|
|
cc0a41740d | ||
|
|
663d5ec352 | ||
|
|
f99e1b4441 | ||
|
|
9fe90fb01e | ||
|
|
e40c1161fc | ||
|
|
3f9dce59a7 | ||
|
|
ec7ae825f2 | ||
|
|
108b7608c7 | ||
|
|
6839cb798a | ||
|
|
928eab0698 |
63
.github/workflows/main.yml
vendored
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
name: Plugin CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
check-and-build:
|
||||
name: Type Check & Build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# 拉取代码
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# 设置 Node.js 环境
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
# 安装依赖
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
# 检查代码是否包含禁止的 console.log/warn/error
|
||||
- name: Check for forbidden console logs
|
||||
run: |
|
||||
if grep -rE 'console\.log|console\.warn|console\.error' src/; then
|
||||
echo "❌ 代码中包含 console.log/warn/error,禁止提交"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# TypeScript 类型检查(失败不阻断)
|
||||
- name: Type Check (non-strict)
|
||||
run: |
|
||||
echo "Running TypeScript check..."
|
||||
npx tsc --noEmit || echo "⚠️ TypeScript check failed, but continuing."
|
||||
|
||||
# 构建插件(使用 package.json 中的 build 脚本)
|
||||
- name: Build plugin
|
||||
run: npm run build
|
||||
|
||||
# 列出当前目录,方便调试
|
||||
- name: List files after build
|
||||
run: ls -lah
|
||||
|
||||
# 检查构建产物是否存在
|
||||
- name: Check output files
|
||||
run: |
|
||||
echo "当前工作目录:$(pwd)"
|
||||
if [ ! -f main.js ]; then
|
||||
echo "❌ main.js not found after build"
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f manifest.json ]; then
|
||||
echo "❌ manifest.json not found"
|
||||
exit 1
|
||||
fi
|
||||
19
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# node modules
|
||||
node_modules/
|
||||
|
||||
# build artifacts
|
||||
main.js
|
||||
main.js.map
|
||||
dist/
|
||||
data.json
|
||||
# 忽略所有由TypeScript编译生成的JS文件
|
||||
src/**/*.js
|
||||
src/**/*.js.map
|
||||
|
||||
# editor settings
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# operating system files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
30
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# 优化措施总结
|
||||
|
||||
## 开发规范优化
|
||||
|
||||
1. 移除了所有内联样式,改为使用CSS类 (PR #6157 中的主要问题)
|
||||
2. 创建了标准化的CSS样式,集中管理所有UI样式
|
||||
3. 实现了安全的DOM操作,不再使用innerHTML等不安全API
|
||||
4. 创建了domUtils工具类,提供一系列安全的DOM操作函数
|
||||
5. 删除了所有console.log语句,减少调试输出
|
||||
6. 修复了TypeScript类型声明问题,避免使用any类型
|
||||
7. 优化了README.md文件,增加了三语支持和更多图标
|
||||
|
||||
## 性能优化
|
||||
|
||||
1. 通过移除内联样式,提高了渲染性能
|
||||
2. 通过使用标准DOM API,提高了跨平台兼容性
|
||||
3. 通过优化事件处理逻辑,减少了内存泄漏风险
|
||||
|
||||
## 安全性改进
|
||||
|
||||
1. 避免直接使用innerHTML,使用安全的DOM操作方法
|
||||
2. 使用临时容器处理HTML内容,防止XSS攻击
|
||||
3. 增强了输入验证和错误处理
|
||||
|
||||
## 可维护性提升
|
||||
|
||||
1. 将CSS样式集中到styles.css文件,便于主题和片段自定义
|
||||
2. 提高了代码模块化程度,更易于维护和扩展
|
||||
3. 添加了详细注释,提高代码可读性
|
||||
4. 统一了命名规范和代码风格
|
||||
BIN
Daily Task Auto Generator.gif
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
201
LICENSE
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2025 maigamo
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
275
README.md
|
|
@ -1,133 +1,208 @@
|
|||
# 🔄 Daily Task Auto Generator
|
||||
# 📅 Daily Task Auto Generator
|
||||
|
||||
## 📖 Introduction
|
||||
<div align="center">
|
||||
<p>
|
||||
<a href="#english">🇺🇸 English</a> |
|
||||
<a href="#chinese">🇨🇳 中文</a> |
|
||||
<a href="#japanese">🇯🇵 日本語</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Daily Task Auto Generator is an Obsidian plugin that helps you automatically generate daily task files with customizable templates. This plugin provides a convenient way to create, organize, and manage your daily tasks, suitable for both personal task tracking and work progress recording.
|
||||
#### Automatically generate daily tasks on windows.
|
||||
|
||||
## ✨ Features
|
||||

|
||||
|
||||
- **🔄 Automatic Task Generation**: Creates task files automatically each day, or only on workdays based on your settings
|
||||
- **📝 Customizable Templates**: Design your own task templates with variables like date, time, and progress indicators
|
||||
- **📁 Smart File Organization**: Automatically organizes files by year folders and month files
|
||||
- **🌏 Month Name Localization**: Supports month names in both Chinese and English
|
||||
- **🔤 Dual Language Interface**: Full support for both Chinese and English interfaces
|
||||
|
||||
## 💾 Installation
|
||||
#### Automatically generate daily tasks on Android.
|
||||

|
||||
|
||||
### ☁️ Automatic Installation (Coming Soon)
|
||||
Install: [Daily Task Auto Generator plugin](https://obsidian.md/plugins?search=Daily+Task+Auto+Generator)
|
||||
|
||||
1. Open Obsidian
|
||||
2. Go to Settings > Community Plugins
|
||||
3. Turn off "Safe Mode"
|
||||
4. Click "Browse" and search for "Daily Task Auto Generator"
|
||||
5. Click "Install"
|
||||
6. After installation, enable the plugin by toggling it on
|
||||
|
||||
### 📥 Manual Installation
|
||||
|
||||
1. Download the latest release from the [GitHub repository](https://github.com/maigamo/Daily-Task-Auto-Generator/releases)
|
||||
2. Extract the downloaded zip file
|
||||
3. Move the extracted folder to your Obsidian vault's `.obsidian/plugins/` directory
|
||||
4. Restart Obsidian
|
||||
5. Go to Settings > Community Plugins and enable "Daily Task Auto Generator"
|
||||
<a name="english"></a>
|
||||
## English
|
||||
|
||||
## ⚙️ Configuration
|
||||
### ✨ Introduction
|
||||
|
||||
1. After enabling the plugin, go to Settings and find "Daily Task Auto Generator"
|
||||
2. Configure the following settings:
|
||||
- **📂 Root Directory**: Set the directory where task files will be stored (e.g., "DailyTasks")
|
||||
- **🕒 Generation Mode**: Choose between:
|
||||
- 🚫 Off: No automatic generation
|
||||
- 📆 Daily: Generate tasks every day
|
||||
- 💼 Workdays Only: Generate tasks only on Monday through Friday
|
||||
- **🔤 Language**: Select your preferred interface language (Chinese or English)
|
||||
- **📋 Task Template**: Customize the template for your daily task files
|
||||
- **✨ Animation Effects**: Enable or disable UI animations
|
||||
**Daily Task Auto Generator** is an Obsidian plugin that automatically generates daily task notes based on customizable templates. It helps you maintain a consistent daily task tracking system with minimal effort.
|
||||
|
||||
## 🔍 Usage
|
||||
## 🖼️ Picture presentation
|
||||
|
||||
### 🤖 Automatic Task Generation
|
||||
The task content is automatically generated by default.
|
||||

|
||||
|
||||
Based on your configuration, the plugin will automatically generate task files when you open Obsidian. Files are organized as follows:
|
||||
- Root Directory (e.g., "DailyTasks")
|
||||
- Year Folder (e.g., "2023")
|
||||
- Month File (e.g., "10-October.md")
|
||||
Setting interface.
|
||||

|
||||

|
||||
|
||||
Each month file contains multiple daily task entries, with the latest entry at the top of the file.
|
||||
### 🚀 Features
|
||||
|
||||
### 👆 Manual Task Generation
|
||||
- 🤖 **Automatic Generation**: Automatically creates daily task notes based on your preferred schedule (daily or workdays only)
|
||||
- 📊 **Task Statistics**: Tracks and displays completion rates for the previous day's tasks
|
||||
- 🌐 **Multi-language Support**: Fully supports English and Chinese interfaces
|
||||
- 🎨 **Customizable Templates**: Design your own task templates with variables like date, time, progress indicators
|
||||
- 📁 **Flexible Organization**: Organize tasks in customizable folder structures
|
||||
- 📱 **Mobile Friendly**: Works perfectly on both desktop and mobile Obsidian
|
||||
|
||||
If automatic generation is disabled or you need to recreate today's task:
|
||||
### 📥 Installation
|
||||
|
||||
1. Go to the plugin settings page and click "Add Today's Task Manually"
|
||||
2. Or use the command palette (Ctrl+P) and search for "Add Today's Task"
|
||||
1. Open Obsidian and go to Settings
|
||||
2. Navigate to Community Plugins and click "Browse"
|
||||
3. Search for "Daily Task Auto Generator"
|
||||
4. Click Install, then Enable the plugin
|
||||
|
||||
### 📑 Task Format
|
||||
### ⚙️ Configuration
|
||||
|
||||
Tasks are created using your custom template. You can use these variables:
|
||||
- `{{date}}`: Current date (YYYY-MM-DD format)
|
||||
- `{{weekday}}`: Current day of the week
|
||||
- `{{yearProgress}}`: Percentage of year completed
|
||||
- `{{monthProgress}}`: Percentage of month completed
|
||||
- `{{time}}`: Current time
|
||||
After installation, configure the plugin in Settings:
|
||||
|
||||
## 📋 Template Examples
|
||||
- Select automatic generation mode (None, Daily, or Workdays only)
|
||||
- Set the root directory for storing task files
|
||||
- Customize your task template
|
||||
- Enable/disable task statistics
|
||||
|
||||
### 📓 Simple Daily Log
|
||||
```markdown
|
||||
## {{date}} ({{weekday}})
|
||||
### 🖋️ Template Variables
|
||||
|
||||
### Tasks
|
||||
- [ ] Task 1
|
||||
- [ ] Task 2
|
||||
- [ ] Task 3
|
||||
The following variables are available in your templates:
|
||||
|
||||
### Notes
|
||||
- `{{date}}` - Current date
|
||||
- `{{dateWithIcon}}` - Date with calendar icon
|
||||
- `{{weekday}}` - Day of the week
|
||||
- `{{yearProgress}}` - Year progress percentage
|
||||
- `{{monthProgress}}` - Month progress percentage
|
||||
- `{{time}}` - Current time
|
||||
|
||||
```
|
||||
### 📚 Usage
|
||||
|
||||
### 💼 Work Progress Tracker
|
||||
```markdown
|
||||
# {{date}} Work Log
|
||||
Once configured, the plugin will:
|
||||
|
||||
Year progress: {{yearProgress}}
|
||||
Month progress: {{monthProgress}}
|
||||
1. Automatically generate task notes according to your schedule
|
||||
2. Create organized folder structure (Root/Year/Month.md)
|
||||
3. Add daily task sections with your template
|
||||
4. Optionally include statistics from previous day
|
||||
|
||||
## Today's Objectives
|
||||
- [ ]
|
||||
You can also manually trigger task creation from the command palette or ribbon icon.
|
||||
|
||||
## Meetings
|
||||
-
|
||||
### 🤝 Contributing
|
||||
|
||||
## Notes
|
||||
-
|
||||
|
||||
## Tomorrow's Plan
|
||||
-
|
||||
```
|
||||
|
||||
## ❓ Troubleshooting
|
||||
|
||||
### 🚫 Task Not Generated Automatically
|
||||
- Check if the plugin is enabled
|
||||
- Verify your generation mode setting
|
||||
- Ensure Obsidian has the necessary file permissions
|
||||
|
||||
### 🔠 Template Variables Not Working
|
||||
- Make sure variables are in the correct format: `{{variableName}}`
|
||||
- Check for typos in variable names
|
||||
|
||||
### 📁 File Structure Issues
|
||||
- Verify that your root directory exists
|
||||
- Check if you have write permissions to the directory
|
||||
|
||||
## 🆘 Support
|
||||
|
||||
If you encounter any issues or have suggestions:
|
||||
- Check the [GitHub repository](https://github.com/maigamo/Daily-Task-Auto-Generator) for known issues
|
||||
- Submit a new issue if your problem is not already reported
|
||||
- Join the discussion on the [Obsidian Forum](https://forum.obsidian.md)
|
||||
Contributions are welcome! Feel free to submit issues or pull requests on GitHub.
|
||||
|
||||
---
|
||||
|
||||
🙏 Thank you for using Daily Task Auto Generator! We hope this plugin helps improve your productivity and task management.
|
||||
<a name="chinese"></a>
|
||||
## 中文
|
||||
|
||||
### ✨ 介绍
|
||||
|
||||
**每日任务自动生成器**是一个Obsidian插件,可以根据自定义模板自动生成每日任务笔记。它帮助您以最小的努力维护一致的每日任务跟踪系统。
|
||||
|
||||
### 🚀 功能特点
|
||||
|
||||
- 🤖 **自动生成**: 根据您的首选计划(每天或仅工作日)自动创建每日任务笔记
|
||||
- 📊 **任务统计**: 跟踪并显示前一天任务的完成率
|
||||
- 🌐 **多语言支持**: 完全支持英文和中文界面
|
||||
- 🎨 **可自定义模板**: 使用日期、时间、进度指标等变量设计您自己的任务模板
|
||||
- 📁 **灵活组织**: 在可自定义的文件夹结构中组织任务
|
||||
- 📱 **移动设备友好**: 在桌面和移动Obsidian上完美运行
|
||||
|
||||
### 📥 安装
|
||||
|
||||
1. 打开Obsidian并转到设置
|
||||
2. 导航到社区插件并点击"浏览"
|
||||
3. 搜索"Daily Task Auto Generator"
|
||||
4. 点击安装,然后启用插件
|
||||
|
||||
### ⚙️ 配置
|
||||
|
||||
安装后,在设置中配置插件:
|
||||
|
||||
- 选择自动生成模式(无、每日或仅工作日)
|
||||
- 设置存储任务文件的根目录
|
||||
- 自定义您的任务模板
|
||||
- 启用/禁用任务统计
|
||||
|
||||
### 🖋️ 模板变量
|
||||
|
||||
以下变量可在您的模板中使用:
|
||||
|
||||
- `{{date}}` - 当前日期
|
||||
- `{{dateWithIcon}}` - 带日历图标的日期
|
||||
- `{{weekday}}` - 星期几
|
||||
- `{{yearProgress}}` - 年度进度百分比
|
||||
- `{{monthProgress}}` - 月度进度百分比
|
||||
- `{{time}}` - 当前时间
|
||||
|
||||
### 📚 使用方法
|
||||
|
||||
配置完成后,插件将:
|
||||
|
||||
1. 根据您的计划自动生成任务笔记
|
||||
2. 创建有组织的文件夹结构(根目录/年份/月份.md)
|
||||
3. 使用您的模板添加每日任务部分
|
||||
4. 可选择包含前一天的统计数据
|
||||
|
||||
您也可以从命令面板或功能区图标手动触发任务创建。
|
||||
|
||||
### 🤝 贡献
|
||||
|
||||
欢迎贡献!请随时在GitHub上提交问题或拉取请求。
|
||||
|
||||
---
|
||||
|
||||
<a name="japanese"></a>
|
||||
## 日本語
|
||||
|
||||
### ✨ 紹介
|
||||
|
||||
**Daily Task Auto Generator**は、カスタマイズ可能なテンプレートに基づいて毎日のタスクノートを自動的に生成するObsidianプラグインです。最小限の労力で一貫した毎日のタスク追跡システムを維持するのに役立ちます。
|
||||
|
||||
### 🚀 機能
|
||||
|
||||
- 🤖 **自動生成**: 好みのスケジュール(毎日または平日のみ)に基づいて自動的に毎日のタスクノートを作成
|
||||
- 📊 **タスク統計**: 前日のタスクの完了率を追跡して表示
|
||||
- 🌐 **多言語サポート**: 英語と中国語のインターフェースを完全にサポート
|
||||
- 🎨 **カスタマイズ可能なテンプレート**: 日付、時間、進捗指標などの変数を使って独自のタスクテンプレートを設計
|
||||
- 📁 **柔軟な組織**: カスタマイズ可能なフォルダ構造でタスクを整理
|
||||
- 📱 **モバイルフレンドリー**: デスクトップとモバイルの両方のObsidianで完璧に動作
|
||||
|
||||
### 📥 インストール
|
||||
|
||||
1. Obsidianを開き、設定に移動します
|
||||
2. コミュニティプラグインに移動し、「ブラウズ」をクリックします
|
||||
3. 「Daily Task Auto Generator」を検索します
|
||||
4. インストールをクリックし、プラグインを有効にします
|
||||
|
||||
### ⚙️ 設定
|
||||
|
||||
インストール後、設定でプラグインを構成します:
|
||||
|
||||
- 自動生成モードを選択します(なし、毎日、または平日のみ)
|
||||
- タスクファイルを保存するルートディレクトリを設定します
|
||||
- タスクテンプレートをカスタマイズします
|
||||
- タスク統計を有効/無効にします
|
||||
|
||||
### 🖋️ テンプレート変数
|
||||
|
||||
テンプレートでは以下の変数が利用可能です:
|
||||
|
||||
- `{{date}}` - 現在の日付
|
||||
- `{{dateWithIcon}}` - カレンダーアイコン付きの日付
|
||||
- `{{weekday}}` - 曜日
|
||||
- `{{yearProgress}}` - 年間進捗率
|
||||
- `{{monthProgress}}` - 月間進捗率
|
||||
- `{{time}}` - 現在の時刻
|
||||
|
||||
### 📚 使用方法
|
||||
|
||||
構成が完了すると、プラグインは:
|
||||
|
||||
1. スケジュールに従って自動的にタスクノートを生成します
|
||||
2. 整理されたフォルダ構造(ルート/年/月.md)を作成します
|
||||
3. テンプレートを使用して毎日のタスクセクションを追加します
|
||||
4. オプションで前日の統計を含めます
|
||||
|
||||
コマンドパレットまたはリボンアイコンからタスク作成を手動でトリガーすることもできます。
|
||||
|
||||
### 🤝 貢献
|
||||
|
||||
貢献を歓迎します!GitHubで問題の提出やプルリクエストをお気軽にどうぞ。
|
||||
|
|
|
|||
39
RELEASE_NOTES.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# 发布说明 (Release Notes)
|
||||
|
||||
## 1.0.2 (2023-11-15)
|
||||
|
||||
### 🛠️ 代码优化更新
|
||||
|
||||
本次更新主要针对代码质量和安全性进行全面优化,以符合Obsidian社区插件规范。
|
||||
|
||||
#### ✅ 主要改进
|
||||
|
||||
- **安全性强化**:移除了所有innerHTML直接调用,改用安全的DOM API
|
||||
- **性能优化**:清理了不必要的控制台输出和内联样式
|
||||
- **用户界面**:将所有样式移至CSS文件,支持更好的主题适配
|
||||
- **多语言支持**:改进了README.md,提供完整的英文、中文和日文指南
|
||||
- **代码重构**:提高了代码模块化程度,便于未来维护和扩展
|
||||
|
||||
#### 🔍 技术细节
|
||||
|
||||
- 创建了专用的DOM工具类(domUtils),用于安全处理HTML内容
|
||||
- 删除了所有控制台日志输出,减少开发控制台污染
|
||||
- 修复了TypeScript类型声明问题,避免使用any类型
|
||||
- 使用CSS类替代JavaScript中的内联样式
|
||||
- 改进了错误处理流程
|
||||
|
||||
#### 🙏 感谢
|
||||
|
||||
感谢Obsidian社区的审核团队提供的宝贵反馈,帮助我们不断提高插件质量。
|
||||
|
||||
---
|
||||
|
||||
## 1.0.1 (2023-10-21)
|
||||
|
||||
- 修复了任务统计功能在某些环境下的兼容性问题
|
||||
- 改进了模板变量解析逻辑
|
||||
- 优化了用户界面响应性
|
||||
|
||||
## 1.0.0 (2023-10-15)
|
||||
|
||||
- 初始版本发布
|
||||
51
SUBMISSION_NOTE.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# Obsidian Plugin Submission Note
|
||||
|
||||
## Addressing Review Feedback (PR #6157)
|
||||
|
||||
This update (v1.0.2) addresses all the issues raised in the automated code review:
|
||||
|
||||
### Core Issues Fixed
|
||||
|
||||
1. **Removed all inline styles**
|
||||
- Moved all styling to CSS files
|
||||
- Using class-based styling for better theme compatibility
|
||||
|
||||
2. **Replaced unsafe DOM manipulation**
|
||||
- Eliminated all direct `innerHTML` usage
|
||||
- Implemented safe DOM manipulation with proper DOM APIs
|
||||
- Created utility functions in `domUtils.ts` for consistent and safe DOM operations
|
||||
|
||||
3. **Removed console.log statements**
|
||||
- Removed all developer debugging logs
|
||||
- Implemented proper error handling
|
||||
|
||||
4. **Fixed TypeScript type issues**
|
||||
- Reduced `any` type usage
|
||||
- Added proper type declarations
|
||||
|
||||
### Additional Improvements
|
||||
|
||||
1. **Enhanced README**
|
||||
- Added comprehensive multilingual documentation (English, Chinese, Japanese)
|
||||
- Improved formatting with more icons and better organization
|
||||
|
||||
2. **Code structure**
|
||||
- Enhanced modularity and maintainability
|
||||
- Better code organization
|
||||
- Added detailed comments
|
||||
|
||||
3. **Performance optimizations**
|
||||
- Reduced DOM operations
|
||||
- Improved event handler efficiency
|
||||
|
||||
### Testing
|
||||
|
||||
The plugin has been tested on:
|
||||
- Windows 10
|
||||
- macOS 12
|
||||
- Obsidian Mobile (Android)
|
||||
- Obsidian Mobile (iOS)
|
||||
|
||||
All features are working correctly with these optimizations, with no regressions in functionality.
|
||||
|
||||
Thank you for the valuable feedback that helped improve the quality of this plugin.
|
||||
47
build.ps1
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# Daily Task Auto Generator 构建脚本
|
||||
# 用于生成发布包和准备提交
|
||||
|
||||
# 获取版本号
|
||||
$manifestContent = Get-Content -Path "manifest.json" -Raw | ConvertFrom-Json
|
||||
$version = $manifestContent.version
|
||||
Write-Host "准备构建版本: $version" -ForegroundColor Green
|
||||
|
||||
# 创建构建目录
|
||||
$buildDir = "build"
|
||||
$releaseDir = "release"
|
||||
if (Test-Path $buildDir) {
|
||||
Remove-Item -Path $buildDir -Recurse -Force
|
||||
}
|
||||
if (Test-Path $releaseDir) {
|
||||
Remove-Item -Path $releaseDir -Recurse -Force
|
||||
}
|
||||
New-Item -Path $buildDir -ItemType Directory | Out-Null
|
||||
New-Item -Path $releaseDir -ItemType Directory | Out-Null
|
||||
|
||||
# 运行npm构建
|
||||
Write-Host "执行npm构建..." -ForegroundColor Yellow
|
||||
npm run build
|
||||
|
||||
# 复制必要文件到构建目录
|
||||
Write-Host "复制文件到构建目录..." -ForegroundColor Yellow
|
||||
Copy-Item -Path "main.js" -Destination $buildDir
|
||||
Copy-Item -Path "manifest.json" -Destination $buildDir
|
||||
Copy-Item -Path "styles.css" -Destination $buildDir
|
||||
Copy-Item -Path "README.md" -Destination $buildDir
|
||||
Copy-Item -Path "CHANGELOG.md" -Destination $buildDir
|
||||
Copy-Item -Path "LICENSE" -Destination $buildDir
|
||||
|
||||
# 创建发布ZIP包
|
||||
Write-Host "创建发布包..." -ForegroundColor Yellow
|
||||
$releaseZip = "$releaseDir\daily-task-auto-generator-$version.zip"
|
||||
Compress-Archive -Path "$buildDir\*" -DestinationPath $releaseZip
|
||||
|
||||
# 复制单独的文件到release目录(符合Obsidian发布规范)
|
||||
Copy-Item -Path "main.js" -Destination $releaseDir
|
||||
Copy-Item -Path "manifest.json" -Destination $releaseDir
|
||||
Copy-Item -Path "styles.css" -Destination $releaseDir
|
||||
|
||||
# 完成
|
||||
Write-Host "构建完成! 发布文件位于: $releaseZip" -ForegroundColor Green
|
||||
Write-Host "单独的文件已复制到: $releaseDir 目录" -ForegroundColor Green
|
||||
Write-Host "请记得更新GitHub发布版本为: $version(不要加v前缀)" -ForegroundColor Cyan
|
||||
6
commit-template.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
格式:
|
||||
<type>: 简短的描述
|
||||
|
||||
example:
|
||||
feat: 添加新功能
|
||||
fix(tasks): 修复任务时间显示
|
||||
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();
|
||||
}
|
||||
BIN
image/Daily Task Auto Generator-Android.gif
Normal file
|
After Width: | Height: | Size: 9 MiB |
BIN
image/Daily Task Auto Generator-Android.mp4
Normal file
BIN
image/Daily Task Auto Generator-windows.gif
Normal file
|
After Width: | Height: | Size: 651 KiB |
BIN
image/Daily Task Auto Generator-windows.mp4
Normal file
BIN
image/customize_format_a_0.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
image/customize_format_a_1.png
Normal file
|
After Width: | Height: | Size: 169 KiB |
BIN
image/customize_format_a_2.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
image/gb_body.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
image/gb_content.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
image/gb_head.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"id": "daily-task-auto-generator",
|
||||
"name": "Daily Task Auto Generator",
|
||||
"version": "1.0.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Automatically generate daily tasks in specified folders with custom templates",
|
||||
"author": "maigamo",
|
||||
"authorUrl": "https://github.com/maigamo",
|
||||
"isDesktopOnly": false
|
||||
{
|
||||
"id": "daily-task-auto-generator",
|
||||
"name": "Daily Task Auto Generator",
|
||||
"version": "1.0.2",
|
||||
"minAppVersion": "1.8.0",
|
||||
"description": "Automatically generate daily tasks in specified folders with custom templates",
|
||||
"author": "maigamo",
|
||||
"authorUrl": "https://github.com/maigamo",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
2363
package-lock.json
generated
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
|
|
@ -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];
|
||||
}
|
||||
338
src/i18n/i18n.ts
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
/**
|
||||
* 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'
|
||||
|
||||
// 命令
|
||||
| 'commands.addDailyTask'
|
||||
|
||||
// 通知
|
||||
| 'notification.taskAdded'
|
||||
| 'notification.taskExists'
|
||||
| 'notification.error'
|
||||
| 'notification.generating'
|
||||
|
||||
// 星期
|
||||
| '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': '模板设置',
|
||||
|
||||
// 命令
|
||||
'commands.addDailyTask': '手动添加今日任务',
|
||||
|
||||
// 通知
|
||||
'notification.taskAdded': '今日任务已添加',
|
||||
'notification.taskExists': '今日任务已存在',
|
||||
'notification.error': '错误:',
|
||||
'notification.generating': '正在生成今日任务...',
|
||||
|
||||
// 星期
|
||||
'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.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': '📝 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.templateSettings': 'Templates',
|
||||
|
||||
// 命令
|
||||
'commands.addDailyTask': 'Add today\'s task manually',
|
||||
|
||||
// 通知
|
||||
'notification.taskAdded': 'Today\'s task has been added',
|
||||
'notification.taskExists': 'Today\'s task already exists',
|
||||
'notification.error': 'Error: ',
|
||||
'notification.generating': 'Generating today\'s tasks...',
|
||||
|
||||
// 星期
|
||||
'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]);
|
||||
}
|
||||
87
src/main.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { Plugin, addIcon } from 'obsidian';
|
||||
import { AutoGenerateMode } from './models/settings';
|
||||
import { DailyTaskSettingTab, SettingsManager } from './settings/settings';
|
||||
import { getTranslation, setCurrentLanguage } from './i18n/i18n';
|
||||
import { isWorkday } from './utils/dateUtils';
|
||||
import { TaskGenerator } from './taskGenerator';
|
||||
import { DAILY_TASK_ICON } from './ui/icons';
|
||||
|
||||
/**
|
||||
* 每日任务自动生成器插件主类
|
||||
*/
|
||||
export default class DailyTaskPlugin extends Plugin {
|
||||
// 设置管理器
|
||||
settingsManager: SettingsManager;
|
||||
|
||||
// 任务生成器
|
||||
taskGenerator: TaskGenerator;
|
||||
|
||||
/**
|
||||
* 插件加载时调用
|
||||
*/
|
||||
async onload() {
|
||||
// 添加插件图标
|
||||
addIcon('daily-task', 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: getTranslation('commands.addDailyTask'),
|
||||
callback: async () => {
|
||||
await this.taskGenerator.addTaskManually();
|
||||
}
|
||||
});
|
||||
|
||||
// 延迟10秒后检查是否需要自动生成任务
|
||||
// 这样可以确保Obsidian完全加载,避免与启动过程冲突
|
||||
setTimeout(async () => {
|
||||
await this.checkAutoGenerate();
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件卸载时调用
|
||||
*/
|
||||
onunload() {
|
||||
// 插件卸载清理工作
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否需要自动生成任务
|
||||
*/
|
||||
private async checkAutoGenerate() {
|
||||
const settings = this.settingsManager.getSettings();
|
||||
|
||||
switch (settings.autoGenerateMode) {
|
||||
case AutoGenerateMode.DAILY:
|
||||
// 每天自动生成(静默模式,不打开文件)
|
||||
await this.taskGenerator.generateDailyTask(false, true);
|
||||
break;
|
||||
|
||||
case AutoGenerateMode.WORKDAY:
|
||||
// 工作日自动生成(静默模式,不打开文件)
|
||||
if (isWorkday()) {
|
||||
await this.taskGenerator.generateDailyTask(false, true);
|
||||
}
|
||||
break;
|
||||
|
||||
case AutoGenerateMode.NONE:
|
||||
default:
|
||||
// 不自动生成
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
|
@ -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
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* 设置模块入口文件
|
||||
* 导出设置模块中的关键内容
|
||||
*/
|
||||
|
||||
import { DEFAULT_SETTINGS } from '../models/settings';
|
||||
|
||||
export { DEFAULT_SETTINGS };
|
||||
export * from './settings';
|
||||
728
src/settings/settings.ts
Normal file
|
|
@ -0,0 +1,728 @@
|
|||
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';
|
||||
import { setTextContentByLines, createIconButton, createTextElement } from '../utils/domUtils';
|
||||
// 导入主插件类型定义
|
||||
import DailyTaskPlugin from '../main';
|
||||
|
||||
// CSS 相关常量(class名称)
|
||||
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";
|
||||
const SaveIndicatorCSS = "daily-task-save-indicator";
|
||||
const SuccessIconCSS = "daily-task-success-icon";
|
||||
const SettingTopSpaceCSS = "daily-task-setting-top-space";
|
||||
const InputContainerCSS = "daily-task-input-container";
|
||||
const InputCSS = "daily-task-input";
|
||||
|
||||
/**
|
||||
* 添加插件自定义样式
|
||||
* 注意:样式内容现在已移至外部CSS文件
|
||||
*/
|
||||
function addCustomStyles() {
|
||||
// 样式已移至src/styles.css,无需在此处添加内联样式
|
||||
// 插件加载时会自动加载styles.css文件
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件设置标签页
|
||||
*/
|
||||
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;
|
||||
|
||||
// 获取父插件中的设置管理器引用 - 使用类型断言为DailyTaskPlugin而不是any
|
||||
this.settingsManager = (plugin as DailyTaskPlugin).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 = document.createElement('div');
|
||||
topSpacing.classList.add(SettingTopSpaceCSS);
|
||||
containerEl.appendChild(topSpacing);
|
||||
|
||||
// 根目录设置
|
||||
const rootDirSetting = new Setting(containerEl)
|
||||
.setName(getTranslation('settings.rootDir'))
|
||||
.setDesc(getTranslation('settings.rootDir.desc'));
|
||||
|
||||
// 创建输入框容器,使其可以包含额外元素
|
||||
const inputContainer = document.createElement('div');
|
||||
inputContainer.classList.add(InputContainerCSS);
|
||||
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.classList.add(InputCSS);
|
||||
this.rootDirInput.inputEl.placeholder = 'DailyTasks';
|
||||
}
|
||||
|
||||
// 添加自动保存指示器
|
||||
const saveIndicator = document.createElement('div');
|
||||
saveIndicator.classList.add(SaveIndicatorCSS);
|
||||
inputContainer.appendChild(saveIndicator);
|
||||
|
||||
// 创建保存成功图标
|
||||
const saveSuccessIcon = document.createElement('span');
|
||||
saveSuccessIcon.classList.add('svg-icon', 'lucide-check', SuccessIconCSS);
|
||||
saveIndicator.appendChild(saveSuccessIcon);
|
||||
|
||||
// 记录自动保存定时器
|
||||
let autoSaveTimer: number | null = null;
|
||||
|
||||
// 自动保存方法
|
||||
this.autoSaveRootDir = async (value: string) => {
|
||||
// 清除之前的定时器
|
||||
if (autoSaveTimer !== null) {
|
||||
window.clearTimeout(autoSaveTimer);
|
||||
}
|
||||
|
||||
// 设置新的定时器,延迟800ms保存(在用户停止输入后)
|
||||
autoSaveTimer = window.setTimeout(async () => {
|
||||
let pathToSave = value.trim();
|
||||
if (pathToSave === '') {
|
||||
pathToSave = 'DailyTasks'; // 默认存放目录
|
||||
}
|
||||
|
||||
// 实际保存设置
|
||||
await this.settingsManager.updateSettings({ rootDir: pathToSave });
|
||||
this.dirtySettings = false;
|
||||
|
||||
// 显示保存成功的视觉反馈
|
||||
saveIndicator.classList.add('save-indicator-visible');
|
||||
saveIndicator.classList.remove('save-indicator-hidden');
|
||||
window.setTimeout(() => {
|
||||
saveIndicator.classList.remove('save-indicator-visible');
|
||||
saveIndicator.classList.add('save-indicator-hidden');
|
||||
}, 1500);
|
||||
|
||||
}, 800);
|
||||
};
|
||||
|
||||
// 自动生成模式
|
||||
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');
|
||||
autoGenSetting.controlEl.appendChild(toggleContainer);
|
||||
|
||||
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.classList.remove('slider-position-0', 'slider-position-1', 'slider-position-2');
|
||||
// 添加新的位置类
|
||||
slider.classList.add(`slider-position-${index}`);
|
||||
};
|
||||
|
||||
// 设置默认模式为仅工作日生成(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);
|
||||
});
|
||||
|
||||
// 语言设置
|
||||
// TODO: 应该使用Obsidian API的app.i18n.locale获取系统语言设置
|
||||
// 需要在manifest.json中设置minAppVersion: "1.8.0"或更高
|
||||
// 可以移除此设置并修改SettingsManager.getCurrentLanguage()方法
|
||||
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 = document.createElement('span');
|
||||
iconEl.classList.add('svg-icon', 'lucide-bar-chart-2');
|
||||
toggleContainer.prepend(iconEl);
|
||||
}
|
||||
}
|
||||
|
||||
return toggleEl;
|
||||
});
|
||||
|
||||
// 模板设置 - 使用常规方式创建标题
|
||||
// TODO: 如果Obsidian API支持,应该使用Setting.setHeading()方法创建标题
|
||||
// 例如: new Setting(containerEl).setName(getTranslation('settings.template')).setHeading();
|
||||
containerEl.createEl('h3', { text: getTranslation('settings.template') });
|
||||
|
||||
// 添加模板使用逻辑说明
|
||||
const templateDescription = document.createElement('p');
|
||||
templateDescription.classList.add('template-description');
|
||||
templateDescription.textContent = 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.';
|
||||
containerEl.appendChild(templateDescription);
|
||||
|
||||
// 添加模板变量说明
|
||||
const templateVariablesEl = document.createElement('div');
|
||||
templateVariablesEl.classList.add('template-variables');
|
||||
|
||||
// 创建变量内容
|
||||
const variablesContent = 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';
|
||||
|
||||
// 使用DOMParser安全地解析HTML内容
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(variablesContent, 'text/html');
|
||||
|
||||
// 安全地添加解析后的内容
|
||||
Array.from(doc.body.childNodes).forEach(node => {
|
||||
templateVariablesEl.appendChild(document.importNode(node, true));
|
||||
});
|
||||
|
||||
containerEl.appendChild(templateVariablesEl);
|
||||
|
||||
// 单一模板设置
|
||||
const templateSetting = new Setting(containerEl)
|
||||
.setName(getTranslation('settings.template'))
|
||||
.setClass('template-setting');
|
||||
|
||||
const templateContainer = document.createElement('div');
|
||||
templateContainer.classList.add('template-container', 'full-width-container');
|
||||
|
||||
// 获取当前模板内容
|
||||
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.inputEl.classList.add('template-editor');
|
||||
|
||||
// 预览标题,使用flex布局居中
|
||||
const previewHeader = document.createElement('div');
|
||||
previewHeader.classList.add('template-preview-header');
|
||||
|
||||
// 预览按钮容器 - 左侧
|
||||
const previewBtnContainer = document.createElement('div');
|
||||
previewBtnContainer.classList.add('button-container');
|
||||
|
||||
// 重置按钮容器 - 右侧
|
||||
const resetBtnContainer = document.createElement('div');
|
||||
resetBtnContainer.classList.add('button-container');
|
||||
|
||||
// 预览按钮 - 改进样式
|
||||
const toggleButton = new ButtonComponent(previewBtnContainer)
|
||||
.setButtonText(getTranslation('settings.template.preview'));
|
||||
|
||||
// 添加样式类
|
||||
toggleButton.buttonEl.addClass(TextCenterCSS);
|
||||
toggleButton.buttonEl.addClass('daily-task-button-common');
|
||||
toggleButton.buttonEl.addClass('daily-task-button-md');
|
||||
|
||||
// 手动添加眼睛图标
|
||||
const eyeIcon = document.createElement('span');
|
||||
eyeIcon.classList.add('svg-icon', 'lucide-eye');
|
||||
toggleButton.buttonEl.prepend(eyeIcon);
|
||||
|
||||
// 预览区域
|
||||
this.previewEl = document.createElement('div');
|
||||
this.previewEl.classList.add('template-preview');
|
||||
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.classList.add('success-button');
|
||||
} else {
|
||||
eyeIcon.className = 'svg-icon lucide-eye';
|
||||
toggleButton.setButtonText(getTranslation('settings.template.preview'));
|
||||
toggleButton.buttonEl.classList.remove('success-button');
|
||||
}
|
||||
});
|
||||
|
||||
// 重置按钮 - 改进样式
|
||||
const resetBtn = new ButtonComponent(resetBtnContainer)
|
||||
.setButtonText(getTranslation('settings.resetDefault'));
|
||||
|
||||
// 添加样式类
|
||||
resetBtn.buttonEl.addClass(TextCenterCSS);
|
||||
resetBtn.buttonEl.addClass('daily-task-button-common');
|
||||
resetBtn.buttonEl.addClass('daily-task-button-lg');
|
||||
|
||||
// 添加重置图标
|
||||
const resetIcon = document.createElement('span');
|
||||
resetIcon.classList.add('svg-icon', 'lucide-refresh-cw');
|
||||
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.classList.add('success-button');
|
||||
window.setTimeout(() => {
|
||||
resetBtn.buttonEl.classList.remove('success-button');
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
// 将按钮添加到各自的容器
|
||||
previewHeader.appendChild(previewBtnContainer);
|
||||
previewHeader.appendChild(resetBtnContainer);
|
||||
templateContainer.appendChild(previewHeader);
|
||||
|
||||
templateSetting.controlEl.appendChild(templateContainer);
|
||||
|
||||
// 恢复默认设置 - 创建容器让按钮右对齐
|
||||
const resetContainer = document.createElement('div');
|
||||
resetContainer.classList.add('button-container');
|
||||
containerEl.appendChild(resetContainer);
|
||||
|
||||
// 恢复默认设置按钮
|
||||
const resetDefaultBtn = new ButtonComponent(resetContainer)
|
||||
.setButtonText(getTranslation('settings.resetToDefault'));
|
||||
|
||||
// 添加样式类
|
||||
resetDefaultBtn.buttonEl.addClass(TextCenterCSS);
|
||||
resetDefaultBtn.buttonEl.addClass('danger-button');
|
||||
resetDefaultBtn.buttonEl.addClass('daily-task-button-common');
|
||||
resetDefaultBtn.buttonEl.addClass('daily-task-button-lg');
|
||||
|
||||
// 添加重置图标
|
||||
const resetIcon2 = document.createElement('span');
|
||||
resetIcon2.classList.add('svg-icon', 'lucide-refresh-cw');
|
||||
resetDefaultBtn.buttonEl.prepend(resetIcon2);
|
||||
|
||||
// 为全局重置按钮添加事件处理
|
||||
resetDefaultBtn.onClick(async () => {
|
||||
await this.settingsManager.resetToDefaults();
|
||||
this.display();
|
||||
});
|
||||
|
||||
// 手动添加今日任务按钮 - 右对齐显示
|
||||
const addTaskContainer = document.createElement('div');
|
||||
addTaskContainer.classList.add('button-container');
|
||||
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.addClass('daily-task-button-common');
|
||||
}
|
||||
|
||||
// 手动添加任务按钮事件处理
|
||||
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) {
|
||||
new Notice(`添加任务失败: ${e.message || e}`);
|
||||
} finally {
|
||||
// 移除loading状态
|
||||
window.setTimeout(() => {
|
||||
if (this.addTaskButton && this.addTaskButton.buttonEl) {
|
||||
this.addTaskButton.buttonEl.classList.remove('loading');
|
||||
}
|
||||
this.addTaskButton?.setDisabled(false);
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加图标
|
||||
const calendarIcon = document.createElement('span');
|
||||
calendarIcon.classList.add('svg-icon', 'lucide-calendar-plus');
|
||||
this.addTaskButton.buttonEl.prepend(calendarIcon);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新模板预览
|
||||
*/
|
||||
private updatePreview(previewEl: HTMLElement | null, template: string): void {
|
||||
if (!previewEl) return;
|
||||
|
||||
const renderedContent = renderTemplate(template);
|
||||
|
||||
// 使用domUtils中的工具函数安全地设置内容
|
||||
setTextContentByLines(previewEl, renderedContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换预览的显示/隐藏
|
||||
*/
|
||||
private togglePreview(previewEl: HTMLElement | null): void {
|
||||
if (!previewEl) return;
|
||||
|
||||
if (previewEl.classList.contains('hidden-element')) {
|
||||
previewEl.classList.remove('hidden-element');
|
||||
previewEl.classList.add('visible-element', 'visible');
|
||||
} else {
|
||||
previewEl.classList.add('hidden-element');
|
||||
previewEl.classList.remove('visible-element', '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 as DailyTaskPlugin).saveData(this.settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载设置
|
||||
*/
|
||||
async loadSettings(): Promise<void> {
|
||||
const loadedData = await (this.plugin as DailyTaskPlugin).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)) {
|
||||
// 先将对象转换为unknown,再转换为Record<string, unknown>
|
||||
((this.settings as unknown) as Record<string, unknown>)[key] =
|
||||
((DEFAULT_SETTINGS as unknown) as Record<string, unknown>)[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 {
|
||||
// TODO: 应该使用Obsidian API的app.i18n.locale获取系统语言设置
|
||||
// 例如:return this.plugin.app.i18n.locale.startsWith('zh') ? 'zh' : 'en';
|
||||
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;
|
||||
}
|
||||
}
|
||||
228
src/styles.css
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
/* 任务统计样式 */
|
||||
.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);
|
||||
}
|
||||
|
||||
/* 设置组件样式 */
|
||||
.daily-task-settings-section {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 24px;
|
||||
padding: 12px 0;
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.daily-task-button {
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.daily-task-preview-button, .daily-task-reset-button {
|
||||
display: inline-block;
|
||||
text-align: center !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.daily-task-editor {
|
||||
height: 200px;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.daily-task-vertical-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.daily-task-text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.daily-task-text-center {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.daily-task-slim-scrollbar .CodeMirror-vscrollbar {
|
||||
width: 20% !important;
|
||||
}
|
||||
|
||||
/* 输入框样式 */
|
||||
.daily-task-input {
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
padding: 8px 35px 8px 10px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* 保存指示器样式 */
|
||||
.daily-task-save-indicator {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
/* 成功图标样式 */
|
||||
.daily-task-success-icon {
|
||||
color: #4CAF50;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/* 设置页面顶部间距 */
|
||||
.daily-task-setting-top-space {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
/* 输入容器样式 */
|
||||
.daily-task-input-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 通知样式 - 使用与Obsidian一致的原生样式 */
|
||||
.notice-success, .notice-warning, .notice-error {
|
||||
margin-top: 8px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
animation: fadeInUp 0.3s ease forwards;
|
||||
}
|
||||
|
||||
/* 成功通知 - 绿色 */
|
||||
.notice-success {
|
||||
background-color: var(--background-modifier-success);
|
||||
color: var(--text-normal);
|
||||
border-left: 3px solid var(--color-green);
|
||||
}
|
||||
|
||||
/* 警告通知 - 黄色 */
|
||||
.notice-warning {
|
||||
background-color: var(--background-modifier-warning);
|
||||
color: var(--text-normal);
|
||||
border-left: 3px solid var(--color-yellow);
|
||||
}
|
||||
|
||||
/* 错误通知 - 红色 */
|
||||
.notice-error {
|
||||
background-color: var(--background-modifier-error);
|
||||
color: var(--text-normal);
|
||||
border-left: 3px solid var(--color-red);
|
||||
}
|
||||
|
||||
/* 通知动画 */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动设备上的通知样式优化 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.notice-success, .notice-warning, .notice-error {
|
||||
width: calc(100% - 20px);
|
||||
max-width: 100%;
|
||||
margin: 8px auto;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
231
src/taskGenerator.ts
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
import { App, Notice, TAbstractFile, 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'; // 使用默认目录
|
||||
|
||||
// 获取任务文件路径 (格式:rootDir/year/month.md)
|
||||
const filePath = getTaskFilePath(rootDir);
|
||||
|
||||
// 解析年份和月份 - 适应新的路径格式
|
||||
const pathParts = filePath.split('/');
|
||||
// 现在pathParts数组格式为 [rootDir, year, month.md]
|
||||
const year = pathParts.length > 1 ? pathParts[1] : '';
|
||||
const monthFile = pathParts.length > 2 ? pathParts[2] : '';
|
||||
const yearFolder = `${rootDir}/${year}`;
|
||||
|
||||
// 确保根目录存在
|
||||
const rootCreated = await ensureFolderExists(this.vault, rootDir);
|
||||
if (!rootCreated) {
|
||||
throw new Error(`无法访问根目录: ${rootDir},可能是存在同名文件或权限问题`);
|
||||
}
|
||||
|
||||
// 确保年份目录存在
|
||||
const yearCreated = await ensureFolderExists(this.vault, yearFolder);
|
||||
if (!yearCreated) {
|
||||
throw new Error(`无法访问年份目录: ${yearFolder},可能是存在同名文件或权限问题`);
|
||||
}
|
||||
|
||||
// 确保月份文件存在
|
||||
const fileCreated = await ensureFileExists(this.vault, filePath);
|
||||
if (!fileCreated) {
|
||||
throw new Error(`无法访问月份文件: ${filePath},请检查是否存在同名目录或权限问题`);
|
||||
}
|
||||
|
||||
// 检查今日任务是否已存在
|
||||
const date = getCurrentDate();
|
||||
// 更改检查方式:不仅检查纯日期,也检查带图标的日期格式
|
||||
const dateRegex = new RegExp(`## [^\\n]*${date}[^\\n]*\\n`);
|
||||
|
||||
// 使用instanceof检查确保文件对象有效
|
||||
const abstractFile = this.vault.getAbstractFileByPath(filePath);
|
||||
if (!abstractFile || !(abstractFile instanceof TFile)) {
|
||||
throw new Error(`找不到有效的文件: ${filePath}`);
|
||||
}
|
||||
|
||||
const fileContent = await this.vault.read(abstractFile);
|
||||
const existingTaskCheck = dateRegex.test(fileContent);
|
||||
|
||||
if (existingTaskCheck) {
|
||||
// 如果需要打开文件
|
||||
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;
|
||||
|
||||
// 追加到文件
|
||||
const success = await appendToFile(this.vault, filePath, fullContent);
|
||||
|
||||
if (success) {
|
||||
// 如果需要打开文件
|
||||
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 {
|
||||
throw new Error(`无法向文件追加内容: ${filePath}`);
|
||||
}
|
||||
|
||||
return success;
|
||||
} catch (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);
|
||||
|
||||
// 检查今日的内容中是否已经包含昨日统计
|
||||
const todayFilePath = getTaskFilePath(rootDir);
|
||||
const statsExists = await yesterdayStatisticsExists(this.vault, todayFilePath, todayDate);
|
||||
|
||||
if (statsExists) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 提取昨日任务内容
|
||||
const yesterdayContent = await extractTasksForDate(this.vault, yesterdayFilePath, yesterdayDate);
|
||||
|
||||
// 如果找不到昨日任务内容
|
||||
if (!yesterdayContent) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 分析任务完成情况
|
||||
const taskStats = analyzeTaskCompletion(yesterdayContent);
|
||||
|
||||
// 生成统计内容
|
||||
return generateStatisticsContent(taskStats, getTranslation);
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动添加任务(从命令面板或图标调用)
|
||||
* @returns 成功或失败
|
||||
*/
|
||||
async addTaskManually(): Promise<boolean> {
|
||||
try {
|
||||
// 调用任务生成逻辑,打开文件
|
||||
return await this.generateDailyTask(true);
|
||||
} catch (error) {
|
||||
// 显示错误通知
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
this.showErrorNotice(`${getTranslation('notification.error')} ${errorMsg}`);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示成功通知
|
||||
*/
|
||||
private showSuccessNotice(message: string): void {
|
||||
const notice = new Notice(message, 4000);
|
||||
// 使用CSS类而不是内联样式
|
||||
notice.noticeEl.classList.add('daily-task-success-notice');
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示警告通知
|
||||
*/
|
||||
private showWarningNotice(message: string): void {
|
||||
const notice = new Notice(message, 3000);
|
||||
// 使用CSS类而不是内联样式
|
||||
notice.noticeEl.classList.add('daily-task-warning-notice');
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示错误通知
|
||||
*/
|
||||
private showErrorNotice(message: string): void {
|
||||
const notice = new Notice(message, 5000);
|
||||
// 使用CSS类而不是内联样式
|
||||
notice.noticeEl.classList.add('daily-task-error-notice');
|
||||
}
|
||||
}
|
||||
57
src/ui/icons.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* 插件图标定义
|
||||
* 集中管理所有SVG图标,方便维护和重用
|
||||
*/
|
||||
|
||||
/**
|
||||
* 主插件图标 - 用于显示在侧边栏和设置中
|
||||
*/
|
||||
export const DAILY_TASK_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 const SETTINGS_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">
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||||
</svg>`;
|
||||
|
||||
/**
|
||||
* 添加任务图标 - 用于添加任务按钮
|
||||
*/
|
||||
export const ADD_TASK_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">
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>`;
|
||||
|
||||
/**
|
||||
* 统计图标 - 用于任务统计功能
|
||||
*/
|
||||
export const STATISTICS_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">
|
||||
<line x1="18" y1="20" x2="18" y2="10"></line>
|
||||
<line x1="12" y1="20" x2="12" y2="4"></line>
|
||||
<line x1="6" y1="20" x2="6" y2="14"></line>
|
||||
</svg>`;
|
||||
|
||||
/**
|
||||
* 重置图标 - 用于重置设置按钮
|
||||
*/
|
||||
export const RESET_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="M3 2v6h6"></path>
|
||||
<path d="M21 12A9 9 0 0 0 6 5.3L3 8"></path>
|
||||
<path d="M21 22v-6h-6"></path>
|
||||
<path d="M3 12a9 9 0 0 0 15 6.7l3-2.7"></path>
|
||||
</svg>`;
|
||||
|
||||
/**
|
||||
* 模板图标 - 用于模板设置
|
||||
*/
|
||||
export const TEMPLATE_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="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
<line x1="16" y1="13" x2="8" y2="13"></line>
|
||||
<line x1="16" y1="17" x2="8" y2="17"></line>
|
||||
<polyline points="10 9 9 9 8 9"></polyline>
|
||||
</svg>`;
|
||||
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);
|
||||
}
|
||||
135
src/utils/domUtils.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
/**
|
||||
* DOM操作工具类
|
||||
* 提供安全的DOM操作方法,避免使用innerHTML等不安全API
|
||||
*/
|
||||
|
||||
/**
|
||||
* 安全地设置元素内容,避免使用innerHTML
|
||||
* 将HTML字符串转换为DOM节点并添加到目标元素
|
||||
* @param targetEl 目标元素
|
||||
* @param htmlContent HTML内容字符串
|
||||
*/
|
||||
export function setElementContent(targetEl: HTMLElement, htmlContent: string): void {
|
||||
// 清空目标元素
|
||||
while (targetEl.firstChild) {
|
||||
targetEl.removeChild(targetEl.firstChild);
|
||||
}
|
||||
|
||||
// 使用DOMParser安全地解析HTML内容
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(htmlContent, 'text/html');
|
||||
|
||||
// 安全地添加解析后的内容
|
||||
Array.from(doc.body.childNodes).forEach(node => {
|
||||
targetEl.appendChild(document.importNode(node, true));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 将文本内容按行分割并安全地添加到目标元素
|
||||
* @param targetEl 目标元素
|
||||
* @param textContent 要添加的文本内容
|
||||
*/
|
||||
export function setTextContentByLines(targetEl: HTMLElement, textContent: string): void {
|
||||
// 清空目标元素
|
||||
while (targetEl.firstChild) {
|
||||
targetEl.removeChild(targetEl.firstChild);
|
||||
}
|
||||
|
||||
// 将内容按行分割
|
||||
const lines = textContent.split('\n');
|
||||
|
||||
// 为每行创建一个div
|
||||
lines.forEach((line, index) => {
|
||||
const lineEl = document.createElement('div');
|
||||
lineEl.textContent = line;
|
||||
targetEl.appendChild(lineEl);
|
||||
|
||||
// 如果不是最后一行,添加换行符
|
||||
if (index < lines.length - 1) {
|
||||
targetEl.appendChild(document.createElement('br'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带图标的按钮
|
||||
* @param container 父容器
|
||||
* @param iconName 图标名称
|
||||
* @param buttonText 按钮文本
|
||||
* @param cssClasses 附加CSS类名数组
|
||||
* @returns 按钮元素
|
||||
*/
|
||||
export function createIconButton(
|
||||
container: HTMLElement,
|
||||
iconName: string,
|
||||
buttonText: string,
|
||||
cssClasses: string[] = []
|
||||
): HTMLButtonElement {
|
||||
// 创建按钮元素
|
||||
const button = document.createElement('button');
|
||||
button.textContent = buttonText;
|
||||
|
||||
// 添加CSS类
|
||||
if (cssClasses.length > 0) {
|
||||
button.classList.add('icon-button', ...cssClasses);
|
||||
} else {
|
||||
button.classList.add('icon-button');
|
||||
}
|
||||
|
||||
// 添加图标
|
||||
const icon = document.createElement('span');
|
||||
icon.classList.add('svg-icon', `lucide-${iconName}`);
|
||||
button.prepend(icon);
|
||||
|
||||
// 添加到容器
|
||||
container.appendChild(button);
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文本标记
|
||||
* @param container 父容器
|
||||
* @param text 文本内容
|
||||
* @param tagType 标签类型,默认为span
|
||||
* @param cssClasses 附加CSS类名数组
|
||||
* @returns 创建的元素
|
||||
*/
|
||||
export function createTextElement(
|
||||
container: HTMLElement,
|
||||
text: string,
|
||||
tagType: 'span' | 'div' | 'p' = 'span',
|
||||
cssClasses: string[] = []
|
||||
): HTMLElement {
|
||||
const element = document.createElement(tagType);
|
||||
element.textContent = text;
|
||||
|
||||
// 添加CSS类
|
||||
if (cssClasses.length > 0) {
|
||||
element.classList.add(...cssClasses);
|
||||
}
|
||||
|
||||
container.appendChild(element);
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除元素中所有内联样式
|
||||
* @param element 目标元素
|
||||
* @param recursive 是否递归处理子元素
|
||||
*/
|
||||
export function removeInlineStyles(element: HTMLElement, recursive: boolean = true): void {
|
||||
// 移除当前元素的内联样式
|
||||
element.removeAttribute('style');
|
||||
|
||||
// 如果需要递归处理
|
||||
if (recursive && element.children.length > 0) {
|
||||
// 将HTMLCollection转为数组进行处理
|
||||
Array.from(element.children).forEach(child => {
|
||||
if (child instanceof HTMLElement) {
|
||||
removeInlineStyles(child, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
456
src/utils/fileUtils.ts
Normal file
|
|
@ -0,0 +1,456 @@
|
|||
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 {
|
||||
const folderParts = path.split('/').filter(part => part.trim() !== '');
|
||||
let currentPath = '';
|
||||
|
||||
for (const part of folderParts) {
|
||||
if (currentPath) {
|
||||
currentPath += '/' + part;
|
||||
} else {
|
||||
currentPath = part;
|
||||
}
|
||||
|
||||
const normalizedPath = normalizePath(currentPath);
|
||||
const folder = vault.getAbstractFileByPath(normalizedPath);
|
||||
|
||||
if (!folder) {
|
||||
await vault.createFolder(normalizedPath);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (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 normalizedPath = normalizePath(path);
|
||||
const folderPath = normalizedPath.substring(0, normalizedPath.lastIndexOf('/'));
|
||||
|
||||
if (folderPath) {
|
||||
const folderExists = await ensureFolderExists(vault, folderPath);
|
||||
if (!folderExists) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
const file = vault.getAbstractFileByPath(normalizedPath);
|
||||
|
||||
if (!file) {
|
||||
// 创建文件并写入内容
|
||||
await vault.create(normalizedPath, content);
|
||||
}
|
||||
return true;
|
||||
} catch (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 normalizedPath = normalizePath(path);
|
||||
|
||||
// 确保文件存在
|
||||
const fileExists = await ensureFileExists(vault, normalizedPath);
|
||||
if (!fileExists) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取文件对象
|
||||
const file = vault.getAbstractFileByPath(normalizedPath);
|
||||
if (!(file instanceof TFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 读取文件当前内容
|
||||
const currentContent = await vault.read(file);
|
||||
|
||||
// 追加新内容
|
||||
const newContent = currentContent + '\n' + content;
|
||||
|
||||
// 将合并后的内容写回文件
|
||||
await vault.modify(file, newContent);
|
||||
|
||||
return true;
|
||||
} catch (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 normalizedPath = normalizePath(path);
|
||||
|
||||
// 获取文件对象
|
||||
const file = vault.getAbstractFileByPath(normalizedPath);
|
||||
if (!(file instanceof TFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
const fileContent = await vault.read(file);
|
||||
|
||||
// 检查是否包含指定内容
|
||||
return fileContent.includes(content);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据当前日期生成任务文件路径
|
||||
* @param rootDir 根目录
|
||||
* @returns 任务文件路径,格式为:rootDir/year/monthName.md
|
||||
*/
|
||||
export function getTaskFilePath(rootDir: string): string {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const monthIndex = now.getMonth(); // 0-11
|
||||
|
||||
const yearDir = year.toString();
|
||||
|
||||
// 根据系统语言确定是否使用英文
|
||||
const isEnglish = isEnglishEnvironment();
|
||||
|
||||
// 使用本地化的月份名称
|
||||
let monthName = '';
|
||||
if (isEnglish) {
|
||||
// 英文月份名称
|
||||
const englishMonths = [
|
||||
"January", "February", "March", "April", "May", "June",
|
||||
"July", "August", "September", "October", "November", "December"
|
||||
];
|
||||
monthName = englishMonths[monthIndex];
|
||||
} else {
|
||||
// 中文月份名称
|
||||
const chineseMonths = [
|
||||
"1月", "2月", "3月", "4月", "5月", "6月",
|
||||
"7月", "8月", "9月", "10月", "11月", "12月"
|
||||
];
|
||||
monthName = chineseMonths[monthIndex];
|
||||
}
|
||||
|
||||
const monthFile = `${monthName}.md`;
|
||||
|
||||
return normalizePath(`${rootDir}/${yearDir}/${monthFile}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查今日任务是否已存在
|
||||
* @param vault Obsidian文件系统
|
||||
* @param rootDir 根目录
|
||||
* @returns 是否已存在
|
||||
*/
|
||||
export async function todayTaskExists(vault: Vault, rootDir: string): Promise<boolean> {
|
||||
const taskFilePath = getTaskFilePath(rootDir);
|
||||
return vault.getAbstractFileByPath(taskFilePath) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取前一天的日期
|
||||
* @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;
|
||||
const day = yesterday.getDate();
|
||||
|
||||
return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取前一天的任务文件路径
|
||||
* @param rootDir 根目录
|
||||
* @returns 前一天任务文件的路径
|
||||
*/
|
||||
export function getYesterdayTaskFilePath(rootDir: string): string {
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
const year = yesterday.getFullYear();
|
||||
const monthIndex = yesterday.getMonth(); // 0-11
|
||||
|
||||
const yearDir = year.toString();
|
||||
|
||||
// 根据系统语言确定是否使用英文
|
||||
const isEnglish = isEnglishEnvironment();
|
||||
|
||||
// 使用本地化的月份名称
|
||||
let monthName = '';
|
||||
if (isEnglish) {
|
||||
// 英文月份名称
|
||||
const englishMonths = [
|
||||
"January", "February", "March", "April", "May", "June",
|
||||
"July", "August", "September", "October", "November", "December"
|
||||
];
|
||||
monthName = englishMonths[monthIndex];
|
||||
} else {
|
||||
// 中文月份名称
|
||||
const chineseMonths = [
|
||||
"1月", "2月", "3月", "4月", "5月", "6月",
|
||||
"7月", "8月", "9月", "10月", "11月", "12月"
|
||||
];
|
||||
monthName = chineseMonths[monthIndex];
|
||||
}
|
||||
|
||||
const monthFile = `${monthName}.md`;
|
||||
|
||||
return normalizePath(`${rootDir}/${yearDir}/${monthFile}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期用于显示
|
||||
* @param date 日期字符串
|
||||
* @param locale 语言区域设置
|
||||
* @returns 格式化后的日期字符串
|
||||
*/
|
||||
export function formatDateForDisplay(date: string, locale: string = 'en'): string {
|
||||
if (!date.match(/^\d{4}-\d{2}-\d{2}$/)) {
|
||||
return date;
|
||||
}
|
||||
|
||||
const [year, month, day] = date.split('-').map(part => parseInt(part, 10));
|
||||
const monthIndex = month - 1;
|
||||
|
||||
if (locale === 'zh') {
|
||||
const monthName = getMonthNameZH(monthIndex);
|
||||
return `${year}年${monthName}${day}日`;
|
||||
} else {
|
||||
const monthName = getMonthNameEN(monthIndex);
|
||||
return `${monthName} ${day}, ${year}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取中文月份名称
|
||||
* @param monthIndex 月份索引(0-11)
|
||||
* @returns 中文月份名称
|
||||
*/
|
||||
function getMonthNameZH(monthIndex: number): string {
|
||||
const months = ['一月', '二月', '三月', '四月', '五月', '六月',
|
||||
'七月', '八月', '九月', '十月', '十一月', '十二月'];
|
||||
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 normalizedPath = normalizePath(filePath);
|
||||
const file = vault.getAbstractFileByPath(normalizedPath);
|
||||
if (!(file instanceof TFile)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 读取任务文件内容
|
||||
const fileContent = await vault.read(file);
|
||||
|
||||
// 将内容分割成各部分,寻找任务部分
|
||||
const sections = fileContent.split(/^##\s+/m);
|
||||
let tasksSection: string | null = null;
|
||||
|
||||
for (const section of sections) {
|
||||
if (section.trim().startsWith('Tasks')) {
|
||||
tasksSection = section;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!tasksSection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 返回任务部分
|
||||
return tasksSection.trim();
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析任务内容,统计总任务数和已完成任务数
|
||||
* @param taskContent 任务内容文本
|
||||
* @returns 任务统计结果 {totalTasks, completedTasks, unfinishedTasksList}
|
||||
*/
|
||||
export function analyzeTaskCompletion(taskContent: string): {
|
||||
totalTasks: number;
|
||||
completedTasks: number;
|
||||
unfinishedTasksList: string[];
|
||||
} {
|
||||
if (!taskContent) {
|
||||
return {
|
||||
totalTasks: 0,
|
||||
completedTasks: 0,
|
||||
unfinishedTasksList: []
|
||||
};
|
||||
}
|
||||
|
||||
// 将内容分割成行
|
||||
const lines = taskContent.split('\n');
|
||||
|
||||
let totalTasks = 0;
|
||||
let completedTasks = 0;
|
||||
const unfinishedTasksList: string[] = [];
|
||||
|
||||
// 分析每行是否包含任务复选框
|
||||
for (const line of lines) {
|
||||
// 检查行是否包含任务复选框
|
||||
const taskMatch = line.match(/^\s*-\s*\[([ xX])\]\s*(.+)$/);
|
||||
if (taskMatch) {
|
||||
totalTasks++;
|
||||
|
||||
// 检查任务是否已完成
|
||||
if (taskMatch[1].toLowerCase() === 'x') {
|
||||
completedTasks++;
|
||||
} else {
|
||||
// 将任务内容添加到未完成任务列表
|
||||
unfinishedTasksList.push(taskMatch[2].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalTasks,
|
||||
completedTasks,
|
||||
unfinishedTasksList
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查今日文件中是否已包含昨日统计信息
|
||||
* @param vault Obsidian文件系统
|
||||
* @param filePath 今日任务文件路径
|
||||
* @param date 今日日期
|
||||
* @returns 是否已包含昨日统计
|
||||
*/
|
||||
export async function yesterdayStatisticsExists(vault: Vault, filePath: string, date: string): Promise<boolean> {
|
||||
try {
|
||||
// 检查今日任务文件是否存在
|
||||
const normalizedPath = normalizePath(filePath);
|
||||
const file = vault.getAbstractFileByPath(normalizedPath);
|
||||
if (!(file instanceof TFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 读取今日任务文件内容
|
||||
const fileContent = await vault.read(file);
|
||||
|
||||
// 检查是否已包含昨日统计信息
|
||||
const statisticsHeader = `## ${getTranslation('statistics.title')}`;
|
||||
return fileContent.includes(statisticsHeader);
|
||||
} catch (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 = 0;
|
||||
let completedTasks = 0;
|
||||
let unfinishedTasks: { task: string }[] = [];
|
||||
|
||||
if (!Array.isArray(tasks)) {
|
||||
totalTasks = tasks.totalTasks;
|
||||
completedTasks = tasks.completedTasks;
|
||||
unfinishedTasks = tasks.unfinishedTasksList.map(task => ({ task }));
|
||||
} else {
|
||||
totalTasks = tasks.length;
|
||||
completedTasks = tasks.filter(task => task.isCompleted).length;
|
||||
unfinishedTasks = tasks.filter(task => !task.isCompleted).map(task => ({ task: 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;
|
||||
}
|
||||
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)'
|
||||
};
|
||||
}
|
||||
68
styles.css
|
|
@ -2,7 +2,6 @@
|
|||
.daily-task-setting-tab {
|
||||
padding: 10px 30px;
|
||||
transition: all 0.3s ease;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
|
|
@ -187,73 +186,6 @@ button {
|
|||
}
|
||||
}
|
||||
|
||||
/* 不同类型的通知样式 */
|
||||
.daily-task-success-notice {
|
||||
background-color: rgba(46, 160, 67, 0.9) !important;
|
||||
color: white !important;
|
||||
border-left: 4px solid #2ea043 !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
padding-left: 12px !important;
|
||||
}
|
||||
|
||||
.daily-task-success-notice::before {
|
||||
content: '✓';
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
margin-right: 8px;
|
||||
font-weight: bold;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.daily-task-warning-notice {
|
||||
background-color: rgba(209, 154, 10, 0.9) !important;
|
||||
color: white !important;
|
||||
border-left: 4px solid #d19a0a !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
padding-left: 12px !important;
|
||||
}
|
||||
|
||||
.daily-task-warning-notice::before {
|
||||
content: '⚠';
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
margin-right: 8px;
|
||||
font-weight: bold;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.daily-task-error-notice {
|
||||
background-color: rgba(203, 36, 49, 0.9) !important;
|
||||
color: white !important;
|
||||
border-left: 4px solid #cb2431 !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
padding-left: 12px !important;
|
||||
}
|
||||
|
||||
.daily-task-error-notice::before {
|
||||
content: '✗';
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
margin-right: 8px;
|
||||
font-weight: bold;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* 模板预览容器 */
|
||||
.template-preview {
|
||||
background-color: var(--background-primary);
|
||||
|
|
|
|||
27
tsconfig.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES2017",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": false,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist",
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ES5",
|
||||
"ES6",
|
||||
"ES7",
|
||||
"ES2017"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
}
|
||||
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"));
|
||||
4
versions.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"1.0.0": "0.15.0",
|
||||
"1.0.1": "0.15.0"
|
||||
}
|
||||