From eacd2c9e3c348e994734674be05145ea252676de Mon Sep 17 00:00:00 2001 From: HeroBlackInk Date: Thu, 26 Feb 2026 19:10:46 +0800 Subject: [PATCH] fix(sync): pull Todoist labels/priority to vault and align link generation --- src/settings/settings.ts | 4 +- src/sync/toObsidian.ts | 10 +++ src/ui/modals.ts | 4 +- src/vault/fileOperation.ts | 125 ++++++++++++++++++++++++++++++++++--- 4 files changed, 130 insertions(+), 13 deletions(-) diff --git a/src/settings/settings.ts b/src/settings/settings.ts index 123fa0c..55362bf 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -272,8 +272,8 @@ export class UltimateTodoistSyncSettingTab extends PluginSettingTab { ); new Setting(containerEl) - .setName('Use Desktop URIs') - .setDesc('Open Todoist tasks in desktop app (todoist://) instead of browser (https://).') + .setName('Use App URI Scheme') + .setDesc('When enabled, generated task links use todoist:// (desktop app). When disabled, links use https://app.todoist.com/app/task/... (web).') .addToggle(component => component .setValue(this.plugin.settings.useAppURI) diff --git a/src/sync/toObsidian.ts b/src/sync/toObsidian.ts index 6b66139..1342efd 100644 --- a/src/sync/toObsidian.ts +++ b/src/sync/toObsidian.ts @@ -135,6 +135,16 @@ export class TodoistToObsidianSync { await this.plugin.fileOperation.syncTaskDueDateToFile(taskId, task.due?.date || ""); this.plugin.logOperation?.log('FILE_TASK_DUEDATE_SYNCED', `Synced due date: ${taskId}`, mapping.filePath, taskId, 'todoist→obsidian'); } + + const prioritySynced = await this.plugin.fileOperation.syncTaskPriorityToFile(taskId, task.priority || 1); + if (prioritySynced) { + this.plugin.logOperation?.log('FILE_TASK_PRIORITY_SYNCED', `Synced priority: ${taskId}`, mapping.filePath, taskId, 'todoist→obsidian'); + } + + const labelsSynced = await this.plugin.fileOperation.syncTaskLabelsToFile(taskId, task.labels || []); + if (labelsSynced) { + this.plugin.logOperation?.log('FILE_TASK_LABELS_SYNCED', `Synced labels: ${taskId}`, mapping.filePath, taskId, 'todoist→obsidian'); + } } private async syncNotesToObsidian( diff --git a/src/ui/modals.ts b/src/ui/modals.ts index 0cb36a7..cc7f180 100644 --- a/src/ui/modals.ts +++ b/src/ui/modals.ts @@ -1117,14 +1117,12 @@ export class TaskManagerModal extends Modal { // Invalidate file cache after content/date writes so tag/priority sync reads fresh data this._fileCache.delete(filePath); // Sync tags (labels) from Todoist to file - const todoistLabels = task.labels || []; + const todoistLabels = taskParser.normalizeLabelsForCompare([...(task.labels || []), 'todoist']); let currentLine = await this.getTaskLine(taskId, filePath); if (currentLine && todoistLabels.length > 0) { const existingTags = taskParser.getAllTagsFromLineText(currentLine); - // Remove existing tags (except #todoist and project tags) let updatedLine = currentLine; for (const tag of existingTags) { - if (tag === 'todoist') continue; updatedLine = updatedLine.replace(new RegExp(`#${tag}\\b`, 'g'), ''); } // Add Todoist labels as tags before #todoist diff --git a/src/vault/fileOperation.ts b/src/vault/fileOperation.ts index 7b73466..c2a00a8 100644 --- a/src/vault/fileOperation.ts +++ b/src/vault/fileOperation.ts @@ -261,8 +261,10 @@ export class FileOperation { console.error(`Task ${taskID} not found in taskFileMapping`); continue; } - const todoistTask = await this.plugin.todoistSyncAPI.GetTaskById(taskID) - const todoistLink = todoistTask?.url || '' + await this.plugin.todoistSyncAPI.GetTaskById(taskID) + const todoistLink = this.plugin.settings.useAppURI + ? `todoist://task?id=${taskID}` + : `https://app.todoist.com/app/task/${taskID}` const link = `[link](${todoistLink})` const newLine = this.plugin.taskParser.addTodoistLink(line,link) this.plugin.debugLog(newLine) @@ -501,6 +503,109 @@ export class FileOperation { return modified; } + private normalizeLabelsForSync(labels: string[] | undefined): string[] { + if (!labels || labels.length === 0) return []; + const normalized = labels + .map(label => (label || '').trim().replace(/^#/, '')) + .filter(label => label.length > 0); + return Array.from(new Set(normalized)); + } + + async syncTaskPriorityToFile(taskId: string, newPriority: number): Promise { + const taskMapping = this.plugin.cacheOperation.getTaskFileMapping(taskId); + if (!taskMapping) return false; + const filepath = taskMapping.filePath; + + const file = this.app.vault.getAbstractFileByPath(filepath); + const fileContent = await this.app.vault.read(file); + const lines = fileContent.split('\n'); + let modified = false; + + const numericPriority = Number(newPriority); + const targetPriority = Number.isFinite(numericPriority) && numericPriority >= 1 && numericPriority <= 4 + ? Math.floor(numericPriority) + : 1; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (!line.includes(taskId) || !this.plugin.taskParser.hasTodoistTag(line)) continue; + + const currentPriority = this.plugin.taskParser.getTaskPriority(line); + if (currentPriority === targetPriority) break; + + const metadataIndex = line.indexOf('%%[todoist_id::'); + const prefix = (metadataIndex >= 0 ? line.slice(0, metadataIndex) : line) + .replace(/\s!!([1-4])(?=\s|$)/g, '') + .replace(/ {2,}/g, ' ') + .trimEnd(); + const suffix = metadataIndex >= 0 ? line.slice(metadataIndex).trimStart() : ''; + + const nextPrefix = targetPriority > 1 ? `${prefix} !!${targetPriority}` : prefix; + lines[i] = suffix ? `${nextPrefix} ${suffix}` : nextPrefix; + modified = true; + break; + } + + if (modified) { + const newFileContent = lines.join('\n'); + await this.plugin.backupOperation?.backupFile(filepath); + await this.app.vault.modify(file, newFileContent); + this.plugin.logOperation?.log('FILE_TASK_PRIORITY_SYNCED', `Synced priority from Todoist: ${taskId}`, filepath, taskId, this.plugin.isSyncingFromTodoist ? 'todoist→obsidian' : 'obsidian→todoist'); + } + + return modified; + } + + async syncTaskLabelsToFile(taskId: string, newLabels: string[]): Promise { + const taskMapping = this.plugin.cacheOperation.getTaskFileMapping(taskId); + if (!taskMapping) return false; + const filepath = taskMapping.filePath; + + const file = this.app.vault.getAbstractFileByPath(filepath); + const fileContent = await this.app.vault.read(file); + const lines = fileContent.split('\n'); + let modified = false; + + const todoistLabels = this.normalizeLabelsForSync(newLabels); + const desiredLabels = this.plugin.taskParser.normalizeLabelsForCompare([...todoistLabels, 'todoist']); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (!line.includes(taskId) || !this.plugin.taskParser.hasTodoistTag(line)) continue; + + const currentLabels = this.plugin.taskParser.normalizeLabelsForCompare( + this.plugin.taskParser.getAllTagsFromLineText(line) + ); + const isSame = currentLabels.length === desiredLabels.length + && currentLabels.every((label, idx) => label === desiredLabels[idx]); + if (isSame) break; + + const metadataIndex = line.indexOf('%%[todoist_id::'); + const prefix = metadataIndex >= 0 ? line.slice(0, metadataIndex) : line; + const suffix = metadataIndex >= 0 ? line.slice(metadataIndex).trimStart() : ''; + + const prefixWithoutTags = prefix + .replace(/#[\w\u4e00-\u9fa5-]+/g, '') + .replace(/ {2,}/g, ' ') + .trimEnd(); + const tagText = desiredLabels.map(label => `#${label}`).join(' '); + const nextPrefix = tagText ? `${prefixWithoutTags} ${tagText}` : prefixWithoutTags; + + lines[i] = suffix ? `${nextPrefix} ${suffix}` : nextPrefix; + modified = true; + break; + } + + if (modified) { + const newFileContent = lines.join('\n'); + await this.plugin.backupOperation?.backupFile(filepath); + await this.app.vault.modify(file, newFileContent); + this.plugin.logOperation?.log('FILE_TASK_LABELS_SYNCED', `Synced labels from Todoist: ${taskId}`, filepath, taskId, this.plugin.isSyncingFromTodoist ? 'todoist→obsidian' : 'obsidian→todoist'); + } + + return modified; + } + async syncTaskNoteToFile(taskId: string, noteContent: string, noteDate: string): Promise { const taskMapping = this.plugin.cacheOperation.getTaskFileMapping(taskId); if (!taskMapping) return false; @@ -663,10 +768,15 @@ export class FileOperation { hasChanges = true; } - // 3. Replace Web URL: https://todoist.com/app/task/oldId -> https://todoist.com/app/task/newId - const oldWebUrlPattern = new RegExp(`https://todoist\\.com/app/task/${oldId}`, 'g'); - if (oldWebUrlPattern.test(line)) { - line = line.replace(oldWebUrlPattern, `https://todoist.com/app/task/${newId}`); + const oldWebUrlPatternLegacy = new RegExp(`https://todoist\\.com/app/task/${oldId}`, 'g'); + if (oldWebUrlPatternLegacy.test(line)) { + line = line.replace(oldWebUrlPatternLegacy, `https://todoist.com/app/task/${newId}`); + hasChanges = true; + } + + const oldWebUrlPatternNew = new RegExp(`https://app\\.todoist\\.com/app/task/${oldId}`, 'g'); + if (oldWebUrlPatternNew.test(line)) { + line = line.replace(oldWebUrlPatternNew, `https://app.todoist.com/app/task/${newId}`); hasChanges = true; } @@ -811,8 +921,7 @@ export class FileOperation { * @returns 标签数组(不带 # 前缀) */ private extractLabelsFromLine(line: string): string[] { - return this.plugin.taskParser.getAllTagsFromLineText(line) - .filter(l => l !== 'todoist'); + return this.plugin.taskParser.getAllTagsFromLineText(line); }