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;