From 5574cba5218ae4adf413d6c9e740731f0a8f1db5 Mon Sep 17 00:00:00 2001 From: Peter Heiss Date: Tue, 25 Jun 2024 21:07:43 +0200 Subject: [PATCH] use Automaton for easier code management. This should help, if more features are coming. Processor is now the single point of reusable code. Automaton.ts is the stuff, who processes the steps. So now you can go to a specific step and they do not interrupt each other. The automaton stops if there is happening any error. --- main.ts | 4 +- manifest.json | 2 +- src/processing/Automaton.ts | 79 +++++++ src/processing/Processor.ts | 225 ++++++++++++++++++ src/processing/createTasks.ts | 118 ++++++++++ src/processing/getTasks.ts | 48 ++++ src/processing/processor.ts | 420 ---------------------------------- src/processing/removeTasks.ts | 69 ++++++ src/processing/updateTasks.ts | 100 ++++++++ 9 files changed, 641 insertions(+), 424 deletions(-) create mode 100644 src/processing/Automaton.ts create mode 100644 src/processing/Processor.ts create mode 100644 src/processing/createTasks.ts create mode 100644 src/processing/getTasks.ts delete mode 100644 src/processing/processor.ts create mode 100644 src/processing/removeTasks.ts create mode 100644 src/processing/updateTasks.ts diff --git a/main.ts b/main.ts index 6491910..2017be1 100644 --- a/main.ts +++ b/main.ts @@ -1,8 +1,7 @@ import {Notice, Plugin} from 'obsidian'; import {DEFAULT_SETTINGS, SettingTab, VikunjaPluginSettings} from "./src/settings/SettingTab"; -import {MainModal} from "./src/modals/mainModal"; import {Tasks} from "./src/vikunja/tasks"; -import {Processor} from "./src/processing/processor"; +import {Processor} from "./src/processing/Processor"; import {UserUser} from "./vikunja_sdk"; import {backendToFindTasks, chooseOutputFile} from "./src/enums"; import {appHasDailyNotesPluginLoaded} from "obsidian-daily-notes-interface"; @@ -96,7 +95,6 @@ export default class VikunjaPlugin extends Plugin { // Called when the user clicks the icon. new Notice('Start syncing with Vikunja'); await this.processor.exec(); - new Notice('Syncing with Vikunja finished'); }); // This adds a status bar item to the bottom of the app. Does not work on mobile apps. diff --git a/manifest.json b/manifest.json index 51b8851..f7e21dc 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "vikunja-sync", "name": "Vikunja Sync", - "version": "1.0.3", + "version": "1.0.4", "minAppVersion": "0.15.0", "description": "Integrates Vikunja into Obsidian as Task Management Platform.", "author": "Peter Heiss", diff --git a/src/processing/Automaton.ts b/src/processing/Automaton.ts new file mode 100644 index 0000000..d3a32f1 --- /dev/null +++ b/src/processing/Automaton.ts @@ -0,0 +1,79 @@ +import VikunjaPlugin from "../../main"; +import {App} from "obsidian"; +import {ModelsTask} from "../../vikunja_sdk"; +import {PluginTask} from "../vaultSearcher/vaultSearcher"; +import {GetTasks} from "./getTasks"; +import {RemoveTasks} from "./removeTasks"; +import UpdateTasks from "./updateTasks"; +import CreateTasks from "./createTasks"; +import {Processor} from "./processor"; + +interface StepsOutput { + localTasks: PluginTask[]; + vikunjaTasks: ModelsTask[]; +} + +interface IAutomatonSteps { + step(localTasks: PluginTask[], vikunjaTasks: ModelsTask[]): Promise; + +} + +enum AutomatonStatus { + READY, + RUNNING, + ERROR, + FINISHED, +} + +class Automaton { + app: App; + plugin: VikunjaPlugin; + steps: IAutomatonSteps[]; + currentStep: number = 0; + status: AutomatonStatus; + processor: Processor; + + constructor(app: App, plugin: VikunjaPlugin, processor: Processor) { + this.app = app; + this.plugin = plugin; + this.processor = processor; + + this.steps = [ + new GetTasks(app, plugin, processor), + new RemoveTasks(app, plugin), + new CreateTasks(app, plugin, processor), + new UpdateTasks(app, plugin, processor), + ]; + + this.status = AutomatonStatus.READY; + } + + async run() { + let localTasks: PluginTask[] = []; + let vikunjaTasks: ModelsTask[] = []; + this.status = AutomatonStatus.RUNNING; + + while (this.currentStep < this.steps.length) { + let output: StepsOutput; + try { + output = await this.execStep(localTasks, vikunjaTasks); + } catch (e) { + this.status = AutomatonStatus.ERROR; + console.error("Automaton: Error in step " + this.currentStep + ", Error: " + e); + return; + } + localTasks = output.localTasks; + vikunjaTasks = output.vikunjaTasks; + } + + this.status = AutomatonStatus.FINISHED; + } + + private async execStep(localTasks: PluginTask[], vikunjaTasks: ModelsTask[]): Promise { + return this.steps[this.currentStep++].step(localTasks, vikunjaTasks); + } + +} + +export {Automaton, AutomatonStatus}; +export type {IAutomatonSteps, StepsOutput,}; diff --git a/src/processing/Processor.ts b/src/processing/Processor.ts new file mode 100644 index 0000000..0c8b9e8 --- /dev/null +++ b/src/processing/Processor.ts @@ -0,0 +1,225 @@ +import Plugin from "../../main"; +import {App, MarkdownView, Notice} from "obsidian"; +import {PluginTask, VaultSearcher} from "../vaultSearcher/vaultSearcher"; +import {TaskFormatter, TaskParser} from "../taskFormats/taskFormats"; +import {Automaton, AutomatonStatus, IAutomatonSteps} from "./Automaton"; +import UpdateTasks from "./updateTasks"; +import {EmojiTaskFormatter, EmojiTaskParser} from "../taskFormats/emojiTaskFormat"; +import {backendToFindTasks, supportedTasksPluginsFormat} from "../enums"; +import {DataviewSearcher} from "../vaultSearcher/dataviewSearcher"; + + +class Processor { + app: App; + plugin: Plugin; + vaultSearcher: VaultSearcher; + taskParser: TaskParser; + taskFormatter: TaskFormatter; + private alreadyUpdateTasksOnStartup = false; + private lastLineChecked: Map; + private automaton: Automaton; + + constructor(app: App, plugin: Plugin) { + this.app = app; + this.plugin = plugin; + + this.vaultSearcher = this.getVaultSearcher(); + this.taskParser = this.getTaskParser(); + this.taskFormatter = this.getTaskFormatter(); + + this.lastLineChecked = new Map(); + } + + /* + * The main method to sync tasks between Vikunja and Obsidian. + */ + async exec() { + if (this.plugin.settings.debugging) console.log("Processor: Start processing"); + + if (this.plugin.foundProblem) { + new Notice("Vikunja Plugin: Found problems in plugin. Have to be fixed first. Syncing is stopped."); + if (this.plugin.settings.debugging) console.log("Processor: Found problems in plugin. Have to be fixed first."); + return; + } + + // Check if user is logged in + if (!this.plugin.userObject) { + // FIXME Currently cannot be used, because there is a bug in Vikunja, which returns 401 in api to get the user object. + //this.plugin.userObject = await new User(this.app, this.plugin).getUser(); + } + + if (this.plugin.settings.debugging) console.log("Processor: Reset automaton"); + this.automaton = new Automaton(this.app, this.plugin, this); + + await this.automaton.run(); + + switch (this.automaton.status) { + case AutomatonStatus.ERROR: + new Notice("Error while syncing tasks"); + break; + case AutomatonStatus.FINISHED: + new Notice("Finished syncing tasks"); + break; + } + + if (this.plugin.settings.debugging) console.log("Processor: End processing"); + } + + async saveToVault(task: PluginTask) { + const newTask = await this.getTaskContent(task); + + await this.app.vault.process(task.file, data => { + if (this.plugin.settings.appendMode) { + return data + "\n" + newTask; + } else { + const lines = data.split("\n"); + for (let i = 0; i < lines.length; i++) { + if (lines[i].includes(this.plugin.settings.insertAfter)) { + lines.splice(i + 1, 0, newTask); + break; + } + } + return lines.join("\n"); + } + }); + } + + async getTaskContent(task: PluginTask) { + const content: string = await this.taskFormatter.format(task.task); + return `${content} `; + } + + /* + * Split tasks into two groups: + * - tasksToUpdateInVault: Tasks which have updates in Vikunja + * - tasksToUpdateInVikunja: Tasks which have updates in the vault + * + * tasksToUpdateInVault has already all informations needed for vault update. + * + * This method should only be triggered on startup of obsidian and only once. After this, we cannot guerantee that the updated information of files are in sync. + */ + async updateTasksOnStartup() { + if (this.plugin.settings.debugging) console.log("Processor: Update tasks in vault and vikunja"); + if (this.alreadyUpdateTasksOnStartup) throw new Error("Update tasks on startup can only be called once"); + if (this.plugin.foundProblem) { + if (this.plugin.settings.debugging) console.log("Processor: Found problems in plugin. Have to be fixed first. Syncing is stopped."); + return; + } + if (!this.plugin.settings.updateOnStartup) { + if (this.plugin.settings.debugging) console.log("Processor: Update on startup is disabled"); + return; + } + + const localTasks = await this.vaultSearcher.getTasks(this.taskParser); + const vikunjaTasks = await this.plugin.tasksApi.getAllTasks(); + + const updateStep: IAutomatonSteps = new UpdateTasks(this.app, this.plugin, this); + await updateStep.step(localTasks, vikunjaTasks); + + this.alreadyUpdateTasksOnStartup = true; + } + + /* + * Check if an update is available in the line, where the cursor is currently placed, which should be pushed to Vikunja + */ + async checkUpdateInLineAvailable(): Promise { + if (!this.plugin.settings.updateOnCursorMovement) { + if (this.plugin.settings.debugging) console.log("Processor: Update on cursor movement is disabled"); + return; + } + + const view = this.app.workspace.getActiveViewOfType(MarkdownView) + if (!view) { + if (this.plugin.settings.debugging) console.log("Processor: No markdown view found"); + return; + } + + const cursor = view.app.workspace.getActiveViewOfType(MarkdownView)?.editor.getCursor() + + const currentFilename = view.app.workspace.getActiveViewOfType(MarkdownView)?.app.workspace.activeEditor?.file?.name; + if (!currentFilename) { + + if (this.plugin.settings.debugging) console.log("Processor: No filename found"); + return; + } + + const currentLine = cursor?.line + if (!currentLine) { + if (this.plugin.settings.debugging) console.log("Processor: No line found"); + return; + } + + const file = view.file; + if (!file) { + if (this.plugin.settings.debugging) console.log("Processor: No file found"); + return; + } + + const lastLine = this.lastLineChecked.get(currentFilename); + let pluginTask = undefined; + if (!!lastLine) { + const lastLineText = view.editor.getLine(lastLine); + if (this.plugin.settings.debugging) console.log("Processor: Last line,", lastLine, "Last line text", lastLineText); + try { + const parsedTask = await this.taskParser.parse(lastLineText); + pluginTask = { + file: file, + lineno: lastLine, + task: parsedTask + }; + } catch (e) { + if (this.plugin.settings.debugging) console.log("Processor: Error while parsing task", e); + } + } + + this.lastLineChecked.set(currentFilename, currentLine); + return pluginTask; + } + + async updateToVault(task: PluginTask) { + const newTask = await this.getTaskContent(task); + + await this.app.vault.process(task.file, (data: string) => { + const lines = data.split("\n"); + lines.splice(task.lineno, 1, newTask); + return lines.join("\n"); + }); + } + + getVaultSearcher(): VaultSearcher { + let vaultSearcher: VaultSearcher; + switch (this.plugin.settings.backendToFindTasks) { + case backendToFindTasks.Dataview: + // Prepare dataview + vaultSearcher = new DataviewSearcher(this.app, this.plugin); + break; + default: + throw new Error("No valid backend to find tasks in vault selected"); + } + return vaultSearcher; + } + + getTaskFormatter(): EmojiTaskFormatter { + switch (this.plugin.settings.useTasksFormat) { + case supportedTasksPluginsFormat.Emoji: + return new EmojiTaskFormatter(this.app, this.plugin); + default: + throw new Error("No valid TaskFormat selected"); + } + } + + getTaskParser() { + let taskParser: TaskParser; + switch (this.plugin.settings.useTasksFormat) { + case supportedTasksPluginsFormat.Emoji: + taskParser = new EmojiTaskParser(this.app, this.plugin); + break; + default: + throw new Error("No valid TaskFormat selected"); + } + return taskParser; + } + +} + +export {Processor}; diff --git a/src/processing/createTasks.ts b/src/processing/createTasks.ts new file mode 100644 index 0000000..adf1aae --- /dev/null +++ b/src/processing/createTasks.ts @@ -0,0 +1,118 @@ +import {IAutomatonSteps, StepsOutput} from "./automaton"; +import {PluginTask} from "../vaultSearcher/vaultSearcher"; +import {ModelsTask} from "../../vikunja_sdk"; +import {App, moment, Notice, TFile} from "obsidian"; +import { + appHasDailyNotesPluginLoaded, + createDailyNote, + getAllDailyNotes, + getDailyNote +} from "obsidian-daily-notes-interface"; +import {chooseOutputFile} from "../enums"; +import VikunjaPlugin from "../../main"; +import {Processor} from "./processor"; + +class CreateTasks implements IAutomatonSteps { + app: App; + plugin: VikunjaPlugin; + processor: Processor; + + constructor(app: App, plugin: VikunjaPlugin, processor: Processor) { + this.app = app; + this.plugin = plugin; + this.processor = processor; + } + + async step(localTasks: PluginTask[], vikunjaTasks: ModelsTask[]): Promise { + await this.createTasks(localTasks, vikunjaTasks); + + return {localTasks, vikunjaTasks}; + } + + private async createTasks(localTasks: PluginTask[], vikunjaTasks: ModelsTask[]) { + if (this.plugin.settings.debugging) console.log("Step CreateTask: Creating labels in Vikunja", localTasks); + localTasks = await this.createLabels(localTasks); + + if (this.plugin.settings.debugging) console.log("Step CreateTask: Creating tasks in Vikunja and vault", localTasks, vikunjaTasks); + await this.pullTasksFromVikunjaToVault(localTasks, vikunjaTasks); + await this.pushTasksFromVaultToVikunja(localTasks, vikunjaTasks); + } + + private async createLabels(localTasks: PluginTask[]) { + return await Promise.all(localTasks + .map(async task => { + if (!task.task) throw new Error("Task is not defined"); + if (!task.task.labels) return task; + + task.task.labels = await this.plugin.labelsApi.getAndCreateLabels(task.task.labels); + if (this.plugin.settings.debugging) console.log("Step CreateTask: Preparing labels for local tasks for vikunja update", task); + return task; + } + )); + } + + private async pushTasksFromVaultToVikunja(localTasks: PluginTask[], vikunjaTasks: ModelsTask[]) { + const tasksToPushToVikunja = localTasks.filter(task => !vikunjaTasks.find(vikunjaTask => vikunjaTask.id === task.task.id)); + if (this.plugin.settings.debugging) console.log("Step CreateTask: Pushing tasks to vikunja", tasksToPushToVikunja); + const createdTasksInVikunja = await this.plugin.tasksApi.createTasks(tasksToPushToVikunja.map(task => task.task)); + if (this.plugin.settings.debugging) console.log("Step CreateTask: Created tasks in vikunja", createdTasksInVikunja); + + const tasksToUpdateInVault = localTasks.map(task => { + const createdTask = createdTasksInVikunja.find((vikunjaTask: ModelsTask) => vikunjaTask.title === task.task.title); + if (createdTask) { + task.task = createdTask; + } + return task; + }); + for (const task of tasksToUpdateInVault) { + await this.processor.updateToVault(task); + } + } + + private async pullTasksFromVikunjaToVault(localTasks: PluginTask[], vikunjaTasks: ModelsTask[]) { + if (this.plugin.settings.debugging) console.log("Step CreateTask: Pulling tasks from vikunja to vault, vault tasks", localTasks, "vikunja tasks", vikunjaTasks); + + const tasksToPushToVault = vikunjaTasks.filter(task => !localTasks.find(vaultTask => vaultTask.task.id === task.id)); + if (this.plugin.settings.debugging) console.log("Step CreateTask: Pushing tasks to vault", tasksToPushToVault); + + const createdTasksInVault: PluginTask[] = []; + for (const task of tasksToPushToVault) { + let file: TFile; + const chosenFile = this.app.vault.getFileByPath(this.plugin.settings.chosenOutputFile); + const date = moment(); + const dailies = getAllDailyNotes() + + switch (this.plugin.settings.chooseOutputFile) { + case chooseOutputFile.File: + if (!chosenFile) throw new Error("Output file not found"); + file = chosenFile; + break; + case chooseOutputFile.DailyNote: + if (!appHasDailyNotesPluginLoaded()) { + new Notice("Daily notes core plugin is not loaded. So we cannot create daily note. Please install daily notes core plugin. Interrupt now.") + continue; + } + + file = getDailyNote(date, dailies) + if (file == null) { + file = await createDailyNote(date) + } + break; + default: + throw new Error("No valid chooseOutputFile selected"); + } + const pluginTask: PluginTask = { + file: file, + lineno: 0, + task: task + }; + createdTasksInVault.push(pluginTask); + } + + for (const task of createdTasksInVault) { + await this.processor.saveToVault(task); + } + } +} + +export default CreateTasks; diff --git a/src/processing/getTasks.ts b/src/processing/getTasks.ts new file mode 100644 index 0000000..355657f --- /dev/null +++ b/src/processing/getTasks.ts @@ -0,0 +1,48 @@ +import {PluginTask, VaultSearcher} from "src/vaultSearcher/vaultSearcher"; +import {ModelsTask} from "vikunja_sdk"; +import {IAutomatonSteps, StepsOutput} from "./automaton"; +import VikunjaPlugin from "../../main"; +import {App} from "obsidian"; +import {TaskParser} from "../taskFormats/taskFormats"; +import {Processor} from "./processor"; + +class GetTasks implements IAutomatonSteps { + app: App; + plugin: VikunjaPlugin; + vaultSearcher: VaultSearcher; + taskParser: TaskParser; + processor: Processor; + + constructor(app: App, plugin: VikunjaPlugin, processor: Processor) { + this.app = app; + this.plugin = plugin; + this.processor = processor; + + this.vaultSearcher = this.processor.getVaultSearcher(); + this.taskParser = this.processor.getTaskParser(); + } + + async step(_1: PluginTask[], _2: ModelsTask[]): Promise { + // Get all tasks in vikunja and vault + if (this.plugin.settings.debugging) console.log("Step GetTask: Pulling tasks from vault"); + const localTasks = await this.getTasksFromVault(); + if (this.plugin.settings.debugging) console.log("Step GetTask: Got tasks from vault", localTasks); + + if (this.plugin.settings.debugging) console.log("Step GetTask: Pulling tasks from Vikunja"); + const vikunjaTasks = await this.getTasksFromVikunja(); + if (this.plugin.settings.debugging) console.log("Step GetTask: Got tasks from Vikunja", vikunjaTasks); + return {localTasks, vikunjaTasks}; + } + + private async getTasksFromVault(): Promise { + return await this.vaultSearcher.getTasks(this.taskParser); + } + + private async getTasksFromVikunja(): Promise { + return await this.plugin.tasksApi.getAllTasks(); + } + +} + + +export {GetTasks}; diff --git a/src/processing/processor.ts b/src/processing/processor.ts deleted file mode 100644 index da95dfa..0000000 --- a/src/processing/processor.ts +++ /dev/null @@ -1,420 +0,0 @@ -import Plugin from "../../main"; -import {App, MarkdownView, moment, Notice, TFile} from "obsidian"; -import {backendToFindTasks, chooseOutputFile, supportedTasksPluginsFormat} from "../enums"; -import {PluginTask, VaultSearcher} from "../vaultSearcher/vaultSearcher"; -import {DataviewSearcher} from "../vaultSearcher/dataviewSearcher"; -import {TaskFormatter, TaskParser} from "../taskFormats/taskFormats"; -import {EmojiTaskFormatter, EmojiTaskParser} from "../taskFormats/emojiTaskFormat"; -import {ModelsTask} from "../../vikunja_sdk"; -import { - appHasDailyNotesPluginLoaded, - createDailyNote, - getAllDailyNotes, - getDailyNote -} from "obsidian-daily-notes-interface"; - -interface UpdatedSplit { - tasksToUpdateInVault: PluginTask[]; - tasksToUpdateInVikunja: PluginTask[]; -} - -class Processor { - app: App; - plugin: Plugin; - vaultSearcher: VaultSearcher; - taskParser: TaskParser; - taskFormatter: TaskFormatter; - private alreadyUpdateTasksOnStartup = false; - private lastLineChecked: Map; - - constructor(app: App, plugin: Plugin) { - this.app = app; - this.plugin = plugin; - - this.vaultSearcher = this.getVaultSearcher(); - this.taskParser = this.getTaskParser(); - this.taskFormatter = this.getTaskFormatter(); - this.lastLineChecked = new Map(); - } - - - async exec() { - if (this.plugin.settings.debugging) console.log("Processor: Start processing"); - if (this.plugin.foundProblem) { - new Notice("Vikunja Plugin: Found problems in plugin. Have to be fixed first. Syncing is stopped."); - if (this.plugin.settings.debugging) console.log("Processor: Found problems in plugin. Have to be fixed first."); - return; - } - - // Check if user is logged in - if (!this.plugin.userObject) { - // FIXME Currently cannot be used, because there is a bug in Vikunja, which returns 401 in api to get the user object. - //this.plugin.userObject = await new User(this.app, this.plugin).getUser(); - } - - // Get all tasks in vikunja and vault - if (this.plugin.settings.debugging) console.log("Processor: Pulling tasks from vault"); - let localTasks = await this.vaultSearcher.getTasks(this.taskParser); - if (this.plugin.settings.debugging) console.log("Processor: Got tasks from vault", localTasks); - - if (this.plugin.settings.debugging) console.log("Processor: Pulling tasks from Vikunja"); - const vikunjaTasksBeforeDeletion = await this.plugin.tasksApi.getAllTasks(); - if (this.plugin.settings.debugging) console.log("Processor: Got tasks from Vikunja", vikunjaTasksBeforeDeletion); - - const { - tasksToUpdateInVault, - tasksToUpdateInVikunja - } = this.splitTaskAfterUpdatedStatus(localTasks, vikunjaTasksBeforeDeletion); - if (this.plugin.settings.debugging) console.log("Processor: Split tasks after updated status, outstanding updates in vault", tasksToUpdateInVault, "outstanding updates in vikunja", tasksToUpdateInVikunja); - - // Processing steps - if (this.plugin.settings.debugging) console.log("Processor: Deleting tasks and labels in Vikunja"); - const deletedVikunjaTasks = await this.removeTasksInVikunjaIfNotInVault(localTasks, vikunjaTasksBeforeDeletion); - await this.removeLabelsInVikunjaIfNotInVault(localTasks, vikunjaTasksBeforeDeletion); - // Filter out deleted tasks - const vikunjaTasks = vikunjaTasksBeforeDeletion.filter(task => !deletedVikunjaTasks.find(deletedTask => deletedTask.id === task.id)); - - if (this.plugin.settings.debugging) console.log("Processor: Creating labels in Vikunja", localTasks); - localTasks = await this.createLabels(localTasks); - - if (this.plugin.settings.debugging) console.log("Processor: Creating tasks in Vikunja and vault", localTasks, vikunjaTasks); - await this.pullTasksFromVikunjaToVault(localTasks, vikunjaTasks); - await this.pushTasksFromVaultToVikunja(localTasks, vikunjaTasks); - - if (this.plugin.settings.debugging) console.log("Processor: Updating tasks in vault and Vikunja"); - await this.updateTasksInVikunja(tasksToUpdateInVikunja); - await this.updateTasksInVault(tasksToUpdateInVault); - - if (this.plugin.settings.debugging) console.log("Processor: End processing"); - } - - async getTaskContent(task: PluginTask) { - const content: string = await this.taskFormatter.format(task.task); - return `${content} `; - } - - async updateToVault(task: PluginTask) { - const newTask = await this.getTaskContent(task); - - await this.app.vault.process(task.file, data => { - const lines = data.split("\n"); - lines.splice(task.lineno, 1, newTask); - return lines.join("\n"); - }); - } - - async saveToVault(task: PluginTask) { - const newTask = await this.getTaskContent(task); - - await this.app.vault.process(task.file, data => { - if (this.plugin.settings.appendMode) { - return data + "\n" + newTask; - } else { - const lines = data.split("\n"); - for (let i = 0; i < lines.length; i++) { - if (lines[i].includes(this.plugin.settings.insertAfter)) { - lines.splice(i + 1, 0, newTask); - break; - } - } - return lines.join("\n"); - } - }); - } - - getTaskFormatter(): EmojiTaskFormatter { - switch (this.plugin.settings.useTasksFormat) { - case supportedTasksPluginsFormat.Emoji: - return new EmojiTaskFormatter(this.app, this.plugin); - default: - throw new Error("No valid TaskFormat selected"); - } - } - - /* - * Split tasks into two groups: - * - tasksToUpdateInVault: Tasks which have updates in Vikunja - * - tasksToUpdateInVikunja: Tasks which have updates in the vault - * - * tasksToUpdateInVault has already all informations needed for vault update. - * - * This method should only be triggered on startup of obsidian and only once. After this, we cannot guerantee that the updated information of files are in sync. - */ - async updateTasksOnStartup() { - if (this.plugin.settings.debugging) console.log("Processor: Update tasks in vault and vikunja"); - if (this.alreadyUpdateTasksOnStartup) throw new Error("Update tasks on startup can only be called once"); - if (this.plugin.foundProblem) { - if (this.plugin.settings.debugging) console.log("Processor: Found problems in plugin. Have to be fixed first. Syncing is stopped."); - return; - } - if (!this.plugin.settings.updateOnStartup) { - if (this.plugin.settings.debugging) console.log("Processor: Update on startup is disabled"); - return; - } - - const localTasks = await this.vaultSearcher.getTasks(this.taskParser); - const vikunjaTasks = await this.plugin.tasksApi.getAllTasks(); - - const { - tasksToUpdateInVault, - tasksToUpdateInVikunja - } = this.splitTaskAfterUpdatedStatus(localTasks, vikunjaTasks); - - - await this.updateTasksInVault(tasksToUpdateInVault); - await this.updateTasksInVikunja(tasksToUpdateInVikunja); - - this.alreadyUpdateTasksOnStartup = true; - } - - /* - * Check if an update is available in the line, where the cursor is currently placed, which should be pushed to Vikunja - */ - async checkUpdateInLineAvailable(): Promise { - if (!this.plugin.settings.updateOnCursorMovement) { - if (this.plugin.settings.debugging) console.log("Processor: Update on cursor movement is disabled"); - return; - } - - const view = this.app.workspace.getActiveViewOfType(MarkdownView) - if (!view) { - if (this.plugin.settings.debugging) console.log("Processor: No markdown view found"); - return; - } - - const cursor = view.app.workspace.getActiveViewOfType(MarkdownView)?.editor.getCursor() - - const currentFilename = view.app.workspace.getActiveViewOfType(MarkdownView)?.app.workspace.activeEditor?.file?.name; - if (!currentFilename) { - - if (this.plugin.settings.debugging) console.log("Processor: No filename found"); - return; - } - - const currentLine = cursor?.line - if (!currentLine) { - if (this.plugin.settings.debugging) console.log("Processor: No line found"); - return; - } - - const file = view.file; - if (!file) { - if (this.plugin.settings.debugging) console.log("Processor: No file found"); - return; - } - - const lastLine = this.lastLineChecked.get(currentFilename); - let pluginTask = undefined; - if (!!lastLine) { - const lastLineText = view.editor.getLine(lastLine); - if (this.plugin.settings.debugging) console.log("Processor: Last line,", lastLine, "Last line text", lastLineText); - try { - const parsedTask = await this.taskParser.parse(lastLineText); - pluginTask = { - file: file, - lineno: lastLine, - task: parsedTask - }; - } catch (e) { - if (this.plugin.settings.debugging) console.log("Processor: Error while parsing task", e); - } - } - - this.lastLineChecked.set(currentFilename, currentLine); - return pluginTask; - } - - private async createLabels(localTasks: PluginTask[]) { - return await Promise.all(localTasks - .map(async task => { - if (!task.task) throw new Error("Task is not defined"); - if (!task.task.labels) return task; - - task.task.labels = await this.plugin.labelsApi.getAndCreateLabels(task.task.labels); - if (this.plugin.settings.debugging) console.log("Processor: Preparing labels for local tasks for vikunja update", task); - return task; - } - )); - } - - private getVaultSearcher() { - let vaultSearcher: VaultSearcher; - switch (this.plugin.settings.backendToFindTasks) { - case backendToFindTasks.Dataview: - // Prepare dataview - vaultSearcher = new DataviewSearcher(this.app, this.plugin); - break; - default: - throw new Error("No valid backend to find tasks in vault selected"); - } - return vaultSearcher; - } - - private getTaskParser() { - let taskParser: TaskParser; - switch (this.plugin.settings.useTasksFormat) { - case supportedTasksPluginsFormat.Emoji: - taskParser = new EmojiTaskParser(this.app, this.plugin); - break; - default: - throw new Error("No valid TaskFormat selected"); - } - return taskParser; - } - - private async pushTasksFromVaultToVikunja(localTasks: PluginTask[], vikunjaTasks: ModelsTask[]) { - const tasksToPushToVikunja = localTasks.filter(task => !vikunjaTasks.find(vikunjaTask => vikunjaTask.id === task.task.id)); - if (this.plugin.settings.debugging) console.log("Processor: Pushing tasks to vikunja", tasksToPushToVikunja); - const createdTasksInVikunja = await this.plugin.tasksApi.createTasks(tasksToPushToVikunja.map(task => task.task)); - if (this.plugin.settings.debugging) console.log("Processor: Created tasks in vikunja", createdTasksInVikunja); - - const tasksToUpdateInVault = localTasks.map(task => { - const createdTask = createdTasksInVikunja.find(vikunjaTask => vikunjaTask.title === task.task.title); - if (createdTask) { - task.task = createdTask; - } - return task; - }); - for (const task of tasksToUpdateInVault) { - await this.updateToVault(task); - } - } - - private async pullTasksFromVikunjaToVault(localTasks: PluginTask[], vikunjaTasks: ModelsTask[]) { - if (this.plugin.settings.debugging) console.log("Processor: Pulling tasks from vikunja to vault, vault tasks", localTasks, "vikunja tasks", vikunjaTasks); - - const tasksToPushToVault = vikunjaTasks.filter(task => !localTasks.find(vaultTask => vaultTask.task.id === task.id)); - if (this.plugin.settings.debugging) console.log("Processor: Pushing tasks to vault", tasksToPushToVault); - - const createdTasksInVault: PluginTask[] = []; - for (const task of tasksToPushToVault) { - let file: TFile; - const chosenFile = this.app.vault.getFileByPath(this.plugin.settings.chosenOutputFile); - const date = moment(); - const dailies = getAllDailyNotes() - - switch (this.plugin.settings.chooseOutputFile) { - case chooseOutputFile.File: - if (!chosenFile) throw new Error("Output file not found"); - file = chosenFile; - break; - case chooseOutputFile.DailyNote: - if (!appHasDailyNotesPluginLoaded()) { - new Notice("Daily notes core plugin is not loaded. So we cannot create daily note. Please install daily notes core plugin. Interrupt now.") - continue; - } - - file = getDailyNote(date, dailies) - if (file == null) { - file = await createDailyNote(date) - } - break; - default: - throw new Error("No valid chooseOutputFile selected"); - } - const pluginTask: PluginTask = { - file: file, - lineno: 0, - task: task - }; - createdTasksInVault.push(pluginTask); - } - - for (const task of createdTasksInVault) { - await this.saveToVault(task); - } - } - - private async updateTasksInVikunja(updateTasks: PluginTask[]) { - if (this.plugin.settings.debugging) console.log("Processor: Update tasks in vikunja"); - - for (const task of updateTasks) { - await this.plugin.tasksApi.updateTask(task.task); - } - } - - private async updateTasksInVault(updateTasks: PluginTask[]) { - if (this.plugin.settings.debugging) console.log("Processor: Update tasks in vault"); - - for (const task of updateTasks) { - await this.updateToVault(task); - } - } - - /* - * Remove tasks in Vikunja if they are not in the vault anymore. - * Returns the tasks which are not in the vault anymore. Filter it yourself if needed. - */ - private async removeTasksInVikunjaIfNotInVault(localTasks: PluginTask[], vikunjaTasks: ModelsTask[]): Promise { - // Check placed here, so no wrong deletion happens - if (!this.plugin.settings.removeTasksIfInVaultNotFound) { - if (this.plugin.settings.debugging) console.log("Processor: Not deleting tasks in vikunja if ID not found in vault"); - return []; - } - - let tasksToDeleteInVikunja = vikunjaTasks.filter(task => !localTasks.find(vaultTask => vaultTask.task.id === task.id)); - if (this.plugin.settings.debugging) console.log("Processor: Deleting tasks in vikunja", tasksToDeleteInVikunja); - - if (this.plugin.settings.removeTasksOnlyInDefaultProject) { - tasksToDeleteInVikunja = tasksToDeleteInVikunja.filter(task => task.projectId === this.plugin.settings.defaultVikunjaProject); - } - await this.plugin.tasksApi.deleteTasks(tasksToDeleteInVikunja); - - return tasksToDeleteInVikunja; - } - - private async removeLabelsInVikunjaIfNotInVault(localTasks: PluginTask[], _vikunjaTasks: ModelsTask[]) { - if (!this.plugin.settings.removeLabelsIfInVaultNotUsed) { - if (this.plugin.settings.debugging) console.log("Processor: Not deleting labels in vikunja if ID not found in vault"); - return; - } - - for (const task of localTasks) { - if (!task.task.labels) continue; - - const labels = task.task.labels; - if (this.plugin.settings.debugging) console.log("Processor: Found labels which are used in Vault", labels); - await this.plugin.labelsApi.deleteUnusedLabels(labels); - } - } - - /* - * Split tasks into two groups: - * - tasksToUpdateInVault: Tasks which have updates in Vikunja - * - tasksToUpdateInVikunja: Tasks which have updates in the vault - * - * tasksToUpdateInVault has already all informations needed for vault update. - */ - private splitTaskAfterUpdatedStatus(localTasks: PluginTask[], vikunjaTasks: ModelsTask[]): UpdatedSplit { - if (this.plugin.settings.debugging) console.log("Processor: Find tasks which have updates on the other platform"); - - let tasksToUpdateInVault: PluginTask[] = []; - let tasksToUpdateInVikunja: PluginTask[] = []; - for (const task of localTasks) { - const vikunjaTask = vikunjaTasks.find(vikunjaTask => vikunjaTask.id === task.task.id); - if (!vikunjaTask) continue; - if (!vikunjaTask || !vikunjaTask.updated || !task.task.updated) { - if (this.plugin.settings.debugging) console.log("Task updated field is not defined", task, vikunjaTask); - throw new Error("Task updated field is not defined"); - } - - let comparison; - if (vikunjaTask.updated > task.task.updated) { - task.task = vikunjaTask; - tasksToUpdateInVault.push(task); - comparison = "Vikunja"; - } else { - tasksToUpdateInVikunja.push(task); - comparison = "Vault"; - } - if (this.plugin.settings.debugging) console.log(`Processor: Task updated will be updated in ${comparison}, updated on vikunja`, vikunjaTask.updated, " updated on vault", task.task.updated); - } - - return { - tasksToUpdateInVault: tasksToUpdateInVault, - tasksToUpdateInVikunja: tasksToUpdateInVikunja - }; - } -} - -export {Processor}; diff --git a/src/processing/removeTasks.ts b/src/processing/removeTasks.ts new file mode 100644 index 0000000..a3398fa --- /dev/null +++ b/src/processing/removeTasks.ts @@ -0,0 +1,69 @@ +import {IAutomatonSteps, StepsOutput} from "./automaton"; +import {PluginTask} from "../vaultSearcher/vaultSearcher"; +import {ModelsTask} from "../../vikunja_sdk"; +import {App} from "obsidian"; +import VikunjaPlugin from "../../main"; + +class RemoveTasks implements IAutomatonSteps { + app: App; + plugin: VikunjaPlugin; + + constructor(app: App, plugin: VikunjaPlugin) { + this.app = app; + this.plugin = plugin; + } + + async step(localTasks: PluginTask[], vikunjaTasksBeforeDeletion: ModelsTask[]): Promise { + + const vikunjaTasks = await this.removeTasksInVikunja(localTasks, vikunjaTasksBeforeDeletion); + + return {localTasks, vikunjaTasks}; + } + + private async removeTasksInVikunja(localTasks: PluginTask[], vikunjaTasksBeforeDeletion: ModelsTask[]) { + if (this.plugin.settings.debugging) console.log("Step RemoveTask: Deleting tasks and labels in Vikunja"); + const deletedVikunjaTasks = await this.removeTasksInVikunjaIfNotInVault(localTasks, vikunjaTasksBeforeDeletion); + await this.removeLabelsInVikunjaIfNotInVault(localTasks, vikunjaTasksBeforeDeletion); + // Filter out deleted tasks + return vikunjaTasksBeforeDeletion.filter(task => !deletedVikunjaTasks.find(deletedTask => deletedTask.id === task.id)); + } + + /* + * Remove tasks in Vikunja if they are not in the vault anymore. + * Returns the tasks which are not in the vault anymore. Filter it yourself if needed. + */ + private async removeTasksInVikunjaIfNotInVault(localTasks: PluginTask[], vikunjaTasks: ModelsTask[]): Promise { + // Check placed here, so no wrong deletion happens + if (!this.plugin.settings.removeTasksIfInVaultNotFound) { + if (this.plugin.settings.debugging) console.log("Step RemoveTask: Not deleting tasks in vikunja if ID not found in vault"); + return []; + } + + let tasksToDeleteInVikunja = vikunjaTasks.filter(task => !localTasks.find(vaultTask => vaultTask.task.id === task.id)); + if (this.plugin.settings.debugging) console.log("Step RemoveTask: Deleting tasks in vikunja", tasksToDeleteInVikunja); + + if (this.plugin.settings.removeTasksOnlyInDefaultProject) { + tasksToDeleteInVikunja = tasksToDeleteInVikunja.filter(task => task.projectId === this.plugin.settings.defaultVikunjaProject); + } + await this.plugin.tasksApi.deleteTasks(tasksToDeleteInVikunja); + + return tasksToDeleteInVikunja; + } + + private async removeLabelsInVikunjaIfNotInVault(localTasks: PluginTask[], _vikunjaTasks: ModelsTask[]) { + if (!this.plugin.settings.removeLabelsIfInVaultNotUsed) { + if (this.plugin.settings.debugging) console.log("Step RemoveTask: Not deleting labels in vikunja if ID not found in vault"); + return; + } + + for (const task of localTasks) { + if (!task.task.labels) continue; + + const labels = task.task.labels; + if (this.plugin.settings.debugging) console.log("Step RemoveTask: Found labels which are used in Vault", labels); + await this.plugin.labelsApi.deleteUnusedLabels(labels); + } + } +} + +export {RemoveTasks}; diff --git a/src/processing/updateTasks.ts b/src/processing/updateTasks.ts new file mode 100644 index 0000000..32828ff --- /dev/null +++ b/src/processing/updateTasks.ts @@ -0,0 +1,100 @@ +import {IAutomatonSteps, StepsOutput} from "./automaton"; +import {PluginTask} from "../vaultSearcher/vaultSearcher"; +import {ModelsTask} from "../../vikunja_sdk"; +import {App} from "obsidian"; +import VikunjaPlugin from "../../main"; +import {Processor} from "./processor"; + +interface UpdatedSplit { + tasksToUpdateInVault: PluginTask[]; + tasksToUpdateInVikunja: PluginTask[]; +} + +class UpdateTasks implements IAutomatonSteps { + app: App; + plugin: VikunjaPlugin; + processor: Processor; + + constructor(app: App, plugin: VikunjaPlugin, processor: Processor) { + this.app = app; + this.plugin = plugin; + this.processor = processor; + } + + async step(localTasks: PluginTask[], vikunjaTasks: ModelsTask[]): Promise { + if (this.plugin.settings.debugging) console.log("Step UpdateTask: Updating tasks in vault and Vikunja"); + const { + tasksToUpdateInVault, + tasksToUpdateInVikunja + } = this.splitTaskAfterUpdatedStatus(localTasks, vikunjaTasks); + if (this.plugin.settings.debugging) console.log("Step UpdateTask: Split tasks after updated status, outstanding updates in vault", tasksToUpdateInVault, "outstanding updates in vikunja", tasksToUpdateInVikunja); + + await this.updateTasks(tasksToUpdateInVikunja, tasksToUpdateInVault); + + return {localTasks, vikunjaTasks}; + } + + + /* + * Split tasks into two groups: + * - tasksToUpdateInVault: Tasks which have updates in Vikunja + * - tasksToUpdateInVikunja: Tasks which have updates in the vault + * + * tasksToUpdateInVault has already all informations needed for vault update. + */ + private splitTaskAfterUpdatedStatus(localTasks: PluginTask[], vikunjaTasks: ModelsTask[]): UpdatedSplit { + if (this.plugin.settings.debugging) console.log("Step UpdateTask: Find tasks which have updates on the other platform"); + + let tasksToUpdateInVault: PluginTask[] = []; + let tasksToUpdateInVikunja: PluginTask[] = []; + for (const task of localTasks) { + const vikunjaTask = vikunjaTasks.find(vikunjaTask => vikunjaTask.id === task.task.id); + if (!vikunjaTask) continue; + if (!vikunjaTask || !vikunjaTask.updated || !task.task.updated) { + if (this.plugin.settings.debugging) console.log("Step UpdateTask: updated field is not defined", task, vikunjaTask); + throw new Error("Task updated field is not defined"); + } + + let comparison; + if (vikunjaTask.updated > task.task.updated) { + task.task = vikunjaTask; + tasksToUpdateInVault.push(task); + comparison = "Vikunja"; + } else { + tasksToUpdateInVikunja.push(task); + comparison = "Vault"; + } + if (this.plugin.settings.debugging) console.log(`Step UpdateTask: Task updated will be updated in ${comparison}, updated on vikunja`, vikunjaTask.updated, " updated on vault", task.task.updated); + } + + return { + tasksToUpdateInVault: tasksToUpdateInVault, + tasksToUpdateInVikunja: tasksToUpdateInVikunja + }; + } + + private async updateTasks(tasksToUpdateInVikunja: PluginTask[], tasksToUpdateInVault: PluginTask[]) { + if (this.plugin.settings.debugging) console.log("Step UpdateTask: Updating tasks in vault and Vikunja"); + await this.updateTasksInVikunja(tasksToUpdateInVikunja); + await this.updateTasksInVault(tasksToUpdateInVault); + } + + private async updateTasksInVikunja(updateTasks: PluginTask[]) { + if (this.plugin.settings.debugging) console.log("Step UpdateTask: Update tasks in vikunja"); + + for (const task of updateTasks) { + await this.plugin.tasksApi.updateTask(task.task); + } + } + + private async updateTasksInVault(updateTasks: PluginTask[]) { + if (this.plugin.settings.debugging) console.log("Step UpdateTask: Update tasks in vault"); + + for (const task of updateTasks) { + await this.processor.updateToVault(task); + } + } + +} + +export default UpdateTasks;