Files
XCDesktop/api/modules/todo/service.ts
2026-03-08 01:34:54 +08:00

217 lines
6.9 KiB
TypeScript

import fs from 'fs/promises'
import path from 'path'
import { resolveNotebookPath } from '../../utils/pathSafety.js'
import { NotFoundError } from '../../../shared/errors/index.js'
import { parseTodoContent, generateTodoContent } from './parser.js'
import type { DayTodo, TodoFilePath, ParsedTodoFile, GetTodoResult } from './types.js'
export interface TodoServiceDependencies {
}
export class TodoService {
constructor(private deps: TodoServiceDependencies = {}) {}
getTodoFilePath(year: number, month: number): TodoFilePath {
const yearStr = year.toString()
const monthStr = month.toString().padStart(2, '0')
const relPath = `TODO/${yearStr}/${yearStr}${monthStr}TODO.md`
const { fullPath } = resolveNotebookPath(relPath)
return { relPath, fullPath }
}
async ensureTodoFileExists(fullPath: string): Promise<void> {
const dir = path.dirname(fullPath)
await fs.mkdir(dir, { recursive: true })
try {
await fs.access(fullPath)
} catch {
await fs.writeFile(fullPath, '', 'utf-8')
}
}
async loadAndParseTodoFile(year: number, month: number): Promise<ParsedTodoFile> {
const { fullPath } = this.getTodoFilePath(year, month)
try {
await fs.access(fullPath)
} catch {
throw new NotFoundError('TODO file not found')
}
const content = await fs.readFile(fullPath, 'utf-8')
return { fullPath, dayTodos: parseTodoContent(content) }
}
async saveTodoFile(fullPath: string, dayTodos: DayTodo[]): Promise<void> {
const content = generateTodoContent(dayTodos)
await fs.writeFile(fullPath, content, 'utf-8')
}
async getTodo(year: number, month: number): Promise<GetTodoResult> {
const { fullPath } = this.getTodoFilePath(year, month)
let dayTodos: DayTodo[] = []
try {
await fs.access(fullPath)
const content = await fs.readFile(fullPath, 'utf-8')
dayTodos = parseTodoContent(content)
} catch {
// 文件不存在
}
const now = new Date()
const todayStr = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}`
const yesterday = new Date(now)
yesterday.setDate(yesterday.getDate() - 1)
const yesterdayStr = `${yesterday.getFullYear()}-${(yesterday.getMonth() + 1).toString().padStart(2, '0')}-${yesterday.getDate().toString().padStart(2, '0')}`
if (year === now.getFullYear() && month === now.getMonth() + 1) {
const migrated = this.migrateIncompleteItems(dayTodos, todayStr, yesterdayStr)
if (migrated) {
const newContent = generateTodoContent(dayTodos)
await this.ensureTodoFileExists(fullPath)
await fs.writeFile(fullPath, newContent, 'utf-8')
}
}
return { dayTodos, year, month }
}
private migrateIncompleteItems(dayTodos: DayTodo[], todayStr: string, yesterdayStr: string): boolean {
let migrated = false
const yesterdayTodo = dayTodos.find(d => d.date === yesterdayStr)
if (yesterdayTodo) {
const incompleteItems = yesterdayTodo.items.filter(item => !item.completed)
if (incompleteItems.length > 0) {
const todayTodo = dayTodos.find(d => d.date === todayStr)
if (todayTodo) {
const existingIds = new Set(todayTodo.items.map(i => i.id))
const itemsToAdd = incompleteItems.map((item, idx) => ({
...item,
id: existingIds.has(item.id) ? `${todayStr}-migrated-${idx}` : item.id
}))
todayTodo.items = [...itemsToAdd, ...todayTodo.items]
} else {
dayTodos.push({
date: todayStr,
items: incompleteItems.map((item, idx) => ({
...item,
id: `${todayStr}-migrated-${idx}`
}))
})
}
yesterdayTodo.items = yesterdayTodo.items.filter(item => item.completed)
if (yesterdayTodo.items.length === 0) {
const index = dayTodos.findIndex(d => d.date === yesterdayStr)
if (index !== -1) {
dayTodos.splice(index, 1)
}
}
migrated = true
}
}
return migrated
}
async saveTodo(year: number, month: number, dayTodos: DayTodo[]): Promise<void> {
const { fullPath } = this.getTodoFilePath(year, month)
await this.ensureTodoFileExists(fullPath)
const content = generateTodoContent(dayTodos)
await fs.writeFile(fullPath, content, 'utf-8')
}
async addTodo(year: number, month: number, date: string, todoContent: string): Promise<DayTodo[]> {
const { fullPath } = this.getTodoFilePath(year, month)
await this.ensureTodoFileExists(fullPath)
let fileContent = await fs.readFile(fullPath, 'utf-8')
const dayTodos = parseTodoContent(fileContent)
const existingDay = dayTodos.find(d => d.date === date)
if (existingDay) {
const newId = `${date}-${existingDay.items.length}`
existingDay.items.push({
id: newId,
content: todoContent,
completed: false
})
} else {
dayTodos.push({
date,
items: [{
id: `${date}-0`,
content: todoContent,
completed: false
}]
})
}
fileContent = generateTodoContent(dayTodos)
await fs.writeFile(fullPath, fileContent, 'utf-8')
return dayTodos
}
async toggleTodo(year: number, month: number, date: string, itemIndex: number, completed: boolean): Promise<DayTodo[]> {
const { fullPath, dayTodos } = await this.loadAndParseTodoFile(year, month)
const day = dayTodos.find(d => d.date === date)
if (!day || itemIndex >= day.items.length) {
throw new NotFoundError('TODO item not found')
}
day.items[itemIndex].completed = completed
await this.saveTodoFile(fullPath, dayTodos)
return dayTodos
}
async updateTodo(year: number, month: number, date: string, itemIndex: number, newContent: string): Promise<DayTodo[]> {
const { fullPath, dayTodos } = await this.loadAndParseTodoFile(year, month)
const day = dayTodos.find(d => d.date === date)
if (!day || itemIndex >= day.items.length) {
throw new NotFoundError('TODO item not found')
}
day.items[itemIndex].content = newContent
await this.saveTodoFile(fullPath, dayTodos)
return dayTodos
}
async deleteTodo(year: number, month: number, date: string, itemIndex: number): Promise<DayTodo[]> {
const { fullPath, dayTodos } = await this.loadAndParseTodoFile(year, month)
const dayIndex = dayTodos.findIndex(d => d.date === date)
if (dayIndex === -1) {
throw new NotFoundError('Day not found')
}
const day = dayTodos[dayIndex]
if (itemIndex >= day.items.length) {
throw new NotFoundError('TODO item not found')
}
day.items.splice(itemIndex, 1)
if (day.items.length === 0) {
dayTodos.splice(dayIndex, 1)
}
await this.saveTodoFile(fullPath, dayTodos)
return dayTodos
}
}
export const createTodoService = (deps?: TodoServiceDependencies): TodoService => {
return new TodoService(deps)
}