mirror of
https://github.com/poanse/obsidian-taskmap.git
synced 2026-07-22 06:05:58 +00:00
fixed warnings and added taskmap files cache
This commit is contained in:
parent
51d9175b16
commit
b523367c7a
17 changed files with 334 additions and 252 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import TaskmapPlugin from "./main";
|
||||
import TaskmapPlugin from "./main";
|
||||
import {
|
||||
type BlockerPair,
|
||||
MouseDown,
|
||||
|
|
@ -14,7 +14,7 @@ import {
|
|||
TFile,
|
||||
type WorkspaceLeaf,
|
||||
} from "obsidian";
|
||||
import { TaskmapView } from "./TaskmapView";
|
||||
import { TASKMAP_VIEW_TYPE, TaskmapView } from "./TaskmapView";
|
||||
import {
|
||||
type NodePositionsCalculator,
|
||||
NoTaskId,
|
||||
|
|
@ -371,7 +371,7 @@ export class Context {
|
|||
}
|
||||
|
||||
private updateDraggedTaskPriority() {
|
||||
// Если порядок тасок по оси Y отличается от приоритетов, то меняем приоритеты и пересчитываем порядок
|
||||
// If task positions are different from priorities, change priorities and recalculate order
|
||||
const draggedParentId = this.versionedData.getTask(
|
||||
this.draggedTaskId,
|
||||
).parentId;
|
||||
|
|
@ -408,7 +408,11 @@ export class Context {
|
|||
}
|
||||
|
||||
public save() {
|
||||
this.app.workspace.getActiveViewOfType(TaskmapView)?.debouncedSave();
|
||||
// Cannot have reference to the view, so save all opened views
|
||||
this.app.workspace.getLeavesOfType(TASKMAP_VIEW_TYPE)
|
||||
.map((leaf) => leaf.view)
|
||||
.filter((view) => view instanceof TaskmapView)
|
||||
.forEach((view) => view.debouncedSave());
|
||||
}
|
||||
|
||||
public addTask(parentId: TaskId): void {
|
||||
|
|
|
|||
|
|
@ -1,154 +0,0 @@
|
|||
import { Notice, TAbstractFile, type App, TFile, TFolder } from "obsidian";
|
||||
import type { ProjectData } from "./data/ProjectData.svelte";
|
||||
import type { TaskData } from "./types";
|
||||
import { TASKMAP_VIEW_TYPE, TaskmapView } from "./TaskmapView";
|
||||
import { deserializeProjectData, updateFile } from "./SaveManager";
|
||||
import { delink, generateMarkdownLink } from "./LinkManager";
|
||||
|
||||
export function getOnDelete(app: App) {
|
||||
return async (file: TAbstractFile) => {
|
||||
console.debug(`${file.path} deleted`);
|
||||
let files: TFile[];
|
||||
if (file instanceof TFile) {
|
||||
files = [file];
|
||||
} else if (file instanceof TFolder) {
|
||||
files = app.vault
|
||||
.getMarkdownFiles()
|
||||
.filter((mdFile) =>
|
||||
mdFile.path.startsWith(file.path + "/"),
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
await handleDeleteFiles(
|
||||
app,
|
||||
files,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function getOnRename(app: App) {
|
||||
return async (file: TAbstractFile, oldPath: string) => {
|
||||
console.debug(`${file.path} renamed`);
|
||||
let files: TFile[];
|
||||
if (file instanceof TFile) {
|
||||
files = [file];
|
||||
} else if (file instanceof TFolder) {
|
||||
files = app.vault
|
||||
.getMarkdownFiles()
|
||||
.filter((mdFile) =>
|
||||
mdFile.path.startsWith(file.path + "/"),
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
await handleRenameFiles(
|
||||
app,
|
||||
files,
|
||||
oldPath,
|
||||
file.path,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export async function handleRenameFiles(
|
||||
app: App,
|
||||
changedMdFiles: TFile[],
|
||||
oldPath: string,
|
||||
newPath: string,
|
||||
) {
|
||||
const taskmapFiles = app.vault
|
||||
.getFiles()
|
||||
.filter((file) => file.path.endsWith(".taskmap"));
|
||||
|
||||
// update paths in taskmap files
|
||||
for (const taskmapFile of taskmapFiles) {
|
||||
console.debug(`handling ${taskmapFile.path}`);
|
||||
let projectData: ProjectData;
|
||||
try {
|
||||
const projectDataRaw = await app.vault.read(taskmapFile);
|
||||
projectData = deserializeProjectData(projectDataRaw);
|
||||
} catch (e) {
|
||||
console.error(`Taskmap rename hook: skipped invalid file ${taskmapFile.path}`);
|
||||
new Notice(`Taskmap rename hook: skipped invalid file: ${String(e)}`);
|
||||
continue;
|
||||
}
|
||||
const mapping = new Map<string, TaskData>();
|
||||
projectData.tasks.forEach((t) => {
|
||||
if (t.path) {
|
||||
if (t.path.startsWith(oldPath)) {
|
||||
t.path = newPath + t.path.slice(oldPath.length);
|
||||
}
|
||||
mapping.set(t.path, t);
|
||||
}
|
||||
});
|
||||
console.debug("mapping " + JSON.stringify(mapping.keys()));
|
||||
let changed = false;
|
||||
changedMdFiles
|
||||
.filter(mdFile => mapping.has(mdFile.path))
|
||||
.forEach((mdFile) => {
|
||||
const t = mapping.get(mdFile.path)!;
|
||||
t.name = generateMarkdownLink(app, mdFile);
|
||||
changed = true;
|
||||
}
|
||||
);
|
||||
if (changed) {
|
||||
console.debug(`${taskmapFile.path} changed`);
|
||||
// resave file on the file system
|
||||
await updateFile(app, taskmapFile, projectData);
|
||||
} else {
|
||||
console.debug(`${taskmapFile.path} not changed`);
|
||||
}
|
||||
}
|
||||
|
||||
// refresh all active views
|
||||
for (const view of app.workspace
|
||||
.getLeavesOfType(TASKMAP_VIEW_TYPE)
|
||||
.map((l) => l.view)
|
||||
.filter((view) => view instanceof TaskmapView)
|
||||
) {
|
||||
await view.refreshUi();
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleDeleteFiles(
|
||||
app: App,
|
||||
files: TFile[],
|
||||
) {
|
||||
const deletedPaths = new Set<string>(files.map((f) => f.path));
|
||||
const taskmapFiles = app.vault
|
||||
.getFiles()
|
||||
.filter((file) => file.path.endsWith(".taskmap"));
|
||||
for (const taskmapFile of taskmapFiles) {
|
||||
let projectData: ProjectData;
|
||||
try {
|
||||
const projectDataRaw = await app.vault.read(taskmapFile);
|
||||
projectData = deserializeProjectData(projectDataRaw);
|
||||
} catch (e) {
|
||||
console.error(`Taskmap rename hook: skipped invalid file ${taskmapFile.path}`);
|
||||
new Notice(`Taskmap rename hook: skipped invalid file: ${String(e)}`);
|
||||
continue;
|
||||
}
|
||||
let taskmapFileChanged = false;
|
||||
projectData.tasks.filter(t => t.path && deletedPaths.has(t.path)).forEach((t) => {
|
||||
t.name = delink(t.name);
|
||||
t.path = undefined;
|
||||
taskmapFileChanged = true;
|
||||
});
|
||||
if (taskmapFileChanged) {
|
||||
console.debug(`${taskmapFile.path} changed`);
|
||||
// resave file on the file system
|
||||
await updateFile(app, taskmapFile, projectData);
|
||||
} else {
|
||||
console.debug(`${taskmapFile.path} not changed`);
|
||||
}
|
||||
}
|
||||
// refresh active views
|
||||
for (const view of app.workspace
|
||||
.getLeavesOfType(TASKMAP_VIEW_TYPE)
|
||||
.map((l) => l.view)
|
||||
.filter((view) => view instanceof TaskmapView)
|
||||
) {
|
||||
await view.refreshUi();
|
||||
}
|
||||
}
|
||||
236
src/FileWatcherWithCache.ts
Normal file
236
src/FileWatcherWithCache.ts
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
import {
|
||||
Notice,
|
||||
type Plugin,
|
||||
TAbstractFile,
|
||||
type App,
|
||||
TFile,
|
||||
TFolder,
|
||||
} from "obsidian";
|
||||
import type { ProjectData } from "./data/ProjectData.svelte";
|
||||
import type { TaskData } from "./types";
|
||||
import { TASKMAP_VIEW_TYPE, TaskmapView } from "./TaskmapView";
|
||||
import { deserializeProjectData, updateFile } from "./SaveManager";
|
||||
import { delink, generateMarkdownLink, pathIsUnderFolder } from "./LinkManager";
|
||||
|
||||
function taskPathAffectedByRename(taskPath: string, oldPath: string): boolean {
|
||||
return taskPath === oldPath || pathIsUnderFolder(taskPath, oldPath);
|
||||
}
|
||||
|
||||
function remapTaskPathAfterRename(taskPath: string, oldPath: string, newPath: string): string {
|
||||
if (taskPath === oldPath) {
|
||||
return newPath;
|
||||
}
|
||||
return newPath + taskPath.slice(oldPath.length);
|
||||
}
|
||||
|
||||
/** In-memory index of `.taskmap` files; `TFile.path` stays in sync with vault renames. */
|
||||
export class FileWatcherWithCache {
|
||||
readonly cachedTaskmapFiles = new Set<TFile>();
|
||||
|
||||
initFromVault(app: App): void {
|
||||
this.cachedTaskmapFiles.clear();
|
||||
for (const f of app.vault.getFiles()) {
|
||||
if (f.extension === "taskmap") {
|
||||
this.cachedTaskmapFiles.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers vault hooks: taskmap index maintenance and path/link updates.
|
||||
* Order: link handlers before cache on delete (cache still lists children under a deleted folder);
|
||||
* cache before link on rename so `TFile.path` matches vault before we scan.
|
||||
*/
|
||||
registerTaskmapVaultHooks(plugin: Plugin): void {
|
||||
const { app } = plugin;
|
||||
plugin.registerEvent(
|
||||
app.vault.on("delete", (file) => {
|
||||
void this.onDeleteLinkUpdate(file, app);
|
||||
}),
|
||||
);
|
||||
plugin.registerEvent(
|
||||
app.vault.on("delete", (file) => {
|
||||
this.onDeleteCacheUpdate(file);
|
||||
}),
|
||||
);
|
||||
|
||||
plugin.registerEvent(
|
||||
app.vault.on("rename", (file) => {
|
||||
this.onRenameCacheUpdate(file);
|
||||
}),
|
||||
);
|
||||
plugin.registerEvent(
|
||||
app.vault.on("rename", (file, oldPath) => {
|
||||
void this.onRenameLinkUpdate(file, oldPath, app);
|
||||
}),
|
||||
);
|
||||
|
||||
plugin.registerEvent(
|
||||
app.vault.on("create", (file) => {
|
||||
this.onCreateCacheUpdate(file);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
onCreateCacheUpdate(file: TAbstractFile): void {
|
||||
if (file instanceof TFile && file.extension === "taskmap") {
|
||||
this.cachedTaskmapFiles.add(file);
|
||||
}
|
||||
// TFolder: each nested .taskmap emits its own "create"
|
||||
}
|
||||
|
||||
onDeleteCacheUpdate(file: TAbstractFile): void {
|
||||
if (file instanceof TFile && file.extension === "taskmap") {
|
||||
this.cachedTaskmapFiles.delete(file);
|
||||
} else if (file instanceof TFolder) {
|
||||
for (const f of [...this.cachedTaskmapFiles]) {
|
||||
if (pathIsUnderFolder(f.path, file.path)) {
|
||||
this.cachedTaskmapFiles.delete(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Path-only renames update `TFile.path` on the same instance — no Set mutation.
|
||||
* Still handle extension changes (e.g. `.taskmap` → `.md`).
|
||||
*/
|
||||
onRenameCacheUpdate(file: TAbstractFile): void {
|
||||
if (!(file instanceof TFile)) {
|
||||
return;
|
||||
}
|
||||
this.cachedTaskmapFiles.delete(file);
|
||||
if (file.extension === "taskmap") {
|
||||
this.cachedTaskmapFiles.add(file);
|
||||
}
|
||||
}
|
||||
|
||||
async onDeleteLinkUpdate(file: TAbstractFile, app: App) {
|
||||
console.debug(`${file.path} deleted`);
|
||||
let files: TFile[];
|
||||
if (file instanceof TFile) {
|
||||
files = [file];
|
||||
} else if (file instanceof TFolder) {
|
||||
files = [...this.cachedTaskmapFiles].filter((f) =>
|
||||
pathIsUnderFolder(f.path, file.path),
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
await this.handleDeleteFiles(app, files);
|
||||
}
|
||||
|
||||
async onRenameLinkUpdate(
|
||||
file: TAbstractFile,
|
||||
oldPath: string,
|
||||
app: App,
|
||||
) {
|
||||
console.debug(`${file.path} renamed`);
|
||||
let files: TFile[];
|
||||
if (file instanceof TFile) {
|
||||
files = [file];
|
||||
} else if (file instanceof TFolder) {
|
||||
files = [...this.cachedTaskmapFiles].filter((f) =>
|
||||
pathIsUnderFolder(f.path, file.path),
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
await this.handleRenameFiles(app, files, oldPath, file.path);
|
||||
}
|
||||
|
||||
async handleDeleteFiles(app: App, files: TFile[]) {
|
||||
const deletedPaths = new Set<string>(files.map((f) => f.path));
|
||||
for (const taskmapFile of [...this.cachedTaskmapFiles]) {
|
||||
if (deletedPaths.has(taskmapFile.path)) {
|
||||
// Entire .taskmap was removed; link handler runs before cache — skip read/write.
|
||||
continue;
|
||||
}
|
||||
let projectData: ProjectData;
|
||||
try {
|
||||
const projectDataRaw = await app.vault.read(taskmapFile);
|
||||
projectData = deserializeProjectData(projectDataRaw);
|
||||
} catch (e) {
|
||||
console.error(`Taskmap delete hook: skipped invalid file ${taskmapFile.path}`);
|
||||
new Notice(`Taskmap delete hook: skipped invalid file: ${String(e)}`);
|
||||
continue;
|
||||
}
|
||||
let taskmapFileChanged = false;
|
||||
projectData.tasks
|
||||
.filter((t) => t.path && deletedPaths.has(t.path))
|
||||
.forEach((t) => {
|
||||
t.name = delink(t.name);
|
||||
t.path = undefined;
|
||||
taskmapFileChanged = true;
|
||||
});
|
||||
if (taskmapFileChanged) {
|
||||
console.debug(`${taskmapFile.path} changed`);
|
||||
// resave file on the file system
|
||||
await updateFile(app, taskmapFile, projectData);
|
||||
} else {
|
||||
console.debug(`${taskmapFile.path} not changed`);
|
||||
}
|
||||
}
|
||||
// refresh all active views
|
||||
for (const view of app.workspace
|
||||
.getLeavesOfType(TASKMAP_VIEW_TYPE)
|
||||
.map((l) => l.view)
|
||||
.filter((view) => view instanceof TaskmapView)) {
|
||||
await view.refreshUi();
|
||||
}
|
||||
}
|
||||
|
||||
async handleRenameFiles(
|
||||
app: App,
|
||||
changedMdFiles: TFile[],
|
||||
oldPath: string,
|
||||
newPath: string,
|
||||
) {
|
||||
// update paths in taskmap files
|
||||
for (const taskmapFile of [...this.cachedTaskmapFiles]) {
|
||||
console.debug(`handling ${taskmapFile.path}`);
|
||||
let projectData: ProjectData;
|
||||
try {
|
||||
const projectDataRaw = await app.vault.read(taskmapFile);
|
||||
projectData = deserializeProjectData(projectDataRaw);
|
||||
} catch (e) {
|
||||
console.error(`Taskmap rename hook: skipped invalid file ${taskmapFile.path}`);
|
||||
new Notice(`Taskmap rename hook: skipped invalid file: ${String(e)}`);
|
||||
continue;
|
||||
}
|
||||
const mapping = new Map<string, TaskData>();
|
||||
projectData.tasks.forEach((t) => {
|
||||
if (t.path) {
|
||||
if (taskPathAffectedByRename(t.path, oldPath)) {
|
||||
t.path = remapTaskPathAfterRename(t.path, oldPath, newPath);
|
||||
}
|
||||
mapping.set(t.path, t);
|
||||
}
|
||||
});
|
||||
console.debug("mapping " + JSON.stringify(mapping.keys()));
|
||||
let changed = false;
|
||||
changedMdFiles
|
||||
.filter((mdFile) => mapping.has(mdFile.path))
|
||||
.forEach((mdFile) => {
|
||||
const t = mapping.get(mdFile.path)!;
|
||||
t.name = generateMarkdownLink(app, mdFile);
|
||||
changed = true;
|
||||
});
|
||||
if (changed) {
|
||||
console.debug(`${taskmapFile.path} changed`);
|
||||
// resave file on the file system
|
||||
await updateFile(app, taskmapFile, projectData);
|
||||
} else {
|
||||
console.debug(`${taskmapFile.path} not changed`);
|
||||
}
|
||||
}
|
||||
|
||||
// refresh all active views
|
||||
for (const view of app.workspace
|
||||
.getLeavesOfType(TASKMAP_VIEW_TYPE)
|
||||
.map((l) => l.view)
|
||||
.filter((view) => view instanceof TaskmapView)) {
|
||||
await view.refreshUi();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,15 +15,21 @@ export function delink(s: string) {
|
|||
return isLink(s) ? s.slice(2, s.length - 2) : s;
|
||||
}
|
||||
|
||||
// relativePath to TFile
|
||||
/** relativePath to TFile */
|
||||
export function getFromRelativePath(app: App, path: string) {
|
||||
return app.vault.getFileByPath(path);
|
||||
}
|
||||
// TFile to wikilink
|
||||
|
||||
/** TFile to wikilink */
|
||||
export function generateMarkdownLink(app: App, file: TFile) {
|
||||
return app.fileManager.generateMarkdownLink(file, "");
|
||||
}
|
||||
|
||||
/** True if `path` is a file inside `folderPath` (not a false `startsWith` on the folder segment). */
|
||||
export function pathIsUnderFolder(path: string, folderPath: string): boolean {
|
||||
return path.startsWith(folderPath + "/");
|
||||
}
|
||||
|
||||
export class LinkManager {
|
||||
private app: App;
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export class NodePositionsCalculator {
|
|||
}
|
||||
|
||||
/**
|
||||
* Позиции относительно рута
|
||||
* Positions relative to the root
|
||||
*/
|
||||
private CalculatePositionsInRootFrame(
|
||||
tasks: TaskData[],
|
||||
|
|
@ -98,19 +98,19 @@ export class NodePositionsCalculator {
|
|||
}
|
||||
|
||||
/**
|
||||
* Считаем позиции на основе веса поддеревьев и константы alignmentRatio
|
||||
* Calculate positions based on the weight of subtrees and the alignmentRatio constant
|
||||
*/
|
||||
private CalculatePositionsInParentFrame(
|
||||
tasks: TaskData[],
|
||||
): Map<TaskId, Vector2> {
|
||||
// Расположение ноды родителя относительно высоты поддерева. 0 - top, 0.5 - center, 1 - bottom
|
||||
// Position of the parent node relative to the height of the subtree. 0 - top, 0.5 - center, 1 - bottom
|
||||
const alignmentRatio: Vector2 = { x: 0, y: 0.5 };
|
||||
// Дополнительный сдвиг в виде доли от siblingDelta, чтобы дети сиблингов не сливались в один список
|
||||
// Additional shift in the form of a fraction of siblingDelta, so that siblings' children do not merge into a single list
|
||||
const parentDelta: Vector2 = { x: 0, y: 0.15 };
|
||||
|
||||
//// Вспомогательные структуры данных
|
||||
// Сортируем таски, чтобы упросить реализацию DP: depth DESC, parentId ASC/DESC, priority ASC
|
||||
// TODO: мб поддерживать порядок сортировки в DataModel?
|
||||
//// Helper data structures
|
||||
// Sort tasks to simplify the implementation of DP: depth DESC, parentId ASC/DESC, priority ASC
|
||||
// TODO: maybe support order sorting in DataModel?
|
||||
const sortedTasks = [...tasks].sort((a, b) => {
|
||||
if (b.depth !== a.depth) {
|
||||
return b.depth - a.depth;
|
||||
|
|
@ -138,8 +138,8 @@ export class NodePositionsCalculator {
|
|||
|
||||
const parentIds = [...childrenIdsByParentId.keys()];
|
||||
|
||||
//// Расчеты
|
||||
// ключ - айди рута поддерева
|
||||
//// Calculations
|
||||
// key - root id of the subtree
|
||||
const subtreeSizeByNodeId: Map<TaskId, Vector2> = new Map<
|
||||
TaskId,
|
||||
Vector2
|
||||
|
|
@ -168,7 +168,7 @@ export class NodePositionsCalculator {
|
|||
}
|
||||
});
|
||||
|
||||
// Шифт дочерних нод относительно левой-верхней точки прямоугольника
|
||||
// Shift of children nodes relative to the left-top point of the rectangle
|
||||
const siblingShiftsRel: Map<TaskId, Vector2> = new Map<
|
||||
TaskId,
|
||||
Vector2
|
||||
|
|
@ -189,7 +189,7 @@ export class NodePositionsCalculator {
|
|||
s = V2.add(s, prevSize);
|
||||
s = V2.add(s, siblingShiftsRel.get(previousSibling)!);
|
||||
}
|
||||
// Дополнительный сдвиг для отрисовки блокеров
|
||||
// Additional shift for drawing blockers
|
||||
// var blockerCount = BlockerDataManager.GetConnections()
|
||||
// .Count(x => x.Item1 == childId);
|
||||
// s += blockerCount;
|
||||
|
|
@ -197,14 +197,8 @@ export class NodePositionsCalculator {
|
|||
});
|
||||
});
|
||||
|
||||
// TODO: походу в проекте что-то не так с данными.
|
||||
// Нужно перейти на мета уровень и сделать удобный способ смотреть данные. Какой?
|
||||
// - Логировать десятки тасок в виде json может быть полезно, но не удобно.
|
||||
// - Можно добавить этап валидации данных, где будут проверяться предположения.
|
||||
// - Можно добавить дебажную инфу с данными выбранной таски. Не поможет, если проблема в удаленных.
|
||||
|
||||
// Шифт родителя относительно левой-верхней точки прямоугольника.
|
||||
// Вычитаем parentDelta, чтобы родитель был выровнен по детям
|
||||
// Shift of the parent relative to the left-top point of the rectangle.
|
||||
// Subtract parentDelta, so that the parent is aligned with the children
|
||||
const parentAlignmentShift: Map<TaskId, Vector2> = new Map<
|
||||
TaskId,
|
||||
Vector2
|
||||
|
|
@ -219,7 +213,7 @@ export class NodePositionsCalculator {
|
|||
);
|
||||
});
|
||||
|
||||
// Шифт дочерних нод относительно родителя и уже в нормальных координатах
|
||||
// Shift of children nodes relative to the parent and already in normal coordinates
|
||||
const finalChildShifts: Map<TaskId, Vector2> = new Map<
|
||||
TaskId,
|
||||
Vector2
|
||||
|
|
@ -273,11 +267,8 @@ export class NodePositionsCalculator {
|
|||
depthOneTasksByRow.get(idx)!.push(t);
|
||||
});
|
||||
|
||||
const yShiftByRowId: Map<number, number> = {} as Map<
|
||||
number,
|
||||
number
|
||||
>;
|
||||
Object.keys(depthOneTasksByRow).forEach((key) => {
|
||||
const yShiftByRowId: Map<number, number> = new Map<number, number>();
|
||||
[...depthOneTasksByRow.keys()].forEach((key) => {
|
||||
const rowIdx = Number(key);
|
||||
const elements = depthOneTasksByRow.get(rowIdx)!;
|
||||
|
||||
|
|
@ -308,13 +299,10 @@ export class NodePositionsCalculator {
|
|||
subtreeSizeByNodeId.get(x.taskId)!.x + this.xshift;
|
||||
});
|
||||
} else if (this.Algorithm === AlgorithmEnum.DoubleRow) {
|
||||
// Размеры поддеревьев по иксу. Если 2 таски друг над другом, то используется большее.
|
||||
this.subtreeWidthByHalfPriority = {} as Map<number, number>;
|
||||
// Sizes of subtrees by x. If 2 tasks are above each other, the larger one is used.
|
||||
this.subtreeWidthByHalfPriority = new Map<number, number>();
|
||||
|
||||
const rootChildrenByPriority: Map<number, TaskId> = {} as Map<
|
||||
number,
|
||||
TaskId
|
||||
>;
|
||||
const rootChildrenByPriority: Map<number, TaskId> = new Map<number, TaskId>();
|
||||
const rootChildren =
|
||||
childrenIdsByParentId.get(RootTaskId) ?? [];
|
||||
rootChildren.forEach((id, idx) => {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export class TaskmapSettingTab extends PluginSettingTab {
|
|||
|
||||
new Setting(containerEl)
|
||||
.setName("Zoom sensitivity (touchpad)")
|
||||
.setDesc("In percents")
|
||||
.setDesc("As a percentage")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("100")
|
||||
|
|
@ -28,7 +28,7 @@ export class TaskmapSettingTab extends PluginSettingTab {
|
|||
);
|
||||
new Setting(containerEl)
|
||||
.setName("Zoom sensitivity (mouse)")
|
||||
.setDesc("In percents")
|
||||
.setDesc("As a percentage")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("100")
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
<div
|
||||
class="hover-container-add-task-button"
|
||||
role="group"
|
||||
onmouseenter={() => entered = true}
|
||||
onmouseleave={() => entered = false}
|
||||
style="
|
||||
|
|
@ -28,7 +29,6 @@
|
|||
{#if entered}
|
||||
<svg
|
||||
role="button"
|
||||
aria-label="Add task"
|
||||
tabindex="0"
|
||||
class="button-add"
|
||||
class:draft={taskData.status === StatusCode.DRAFT}
|
||||
|
|
|
|||
|
|
@ -25,13 +25,13 @@
|
|||
let isPressedDown = $state(false);
|
||||
let isPressed = $derived(context.pressedButtonCode == iconCode);
|
||||
|
||||
const stateful = [
|
||||
let stateful = $derived([
|
||||
IconCode.REMOVE_SUBMENU,
|
||||
IconCode.STATUS_SUBMENU,
|
||||
IconCode.KEY,
|
||||
IconCode.LOCK,
|
||||
IconCode.REPARENT
|
||||
].includes(iconCode);
|
||||
].includes(iconCode));
|
||||
|
||||
function onpointerdown(event: MouseEvent) {
|
||||
isPressedDown = true;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
<div
|
||||
class="hover-container-hide-branch-button"
|
||||
role="group"
|
||||
onmouseenter={() => entered = true}
|
||||
onmouseleave={() => entered = false}
|
||||
style="
|
||||
|
|
|
|||
|
|
@ -100,10 +100,6 @@
|
|||
stroke-linejoin: round;
|
||||
will-change: transform,scale,translate;
|
||||
}
|
||||
.custom-svg {
|
||||
stroke-width: 0.083;
|
||||
fill: #bbb;
|
||||
}
|
||||
}
|
||||
|
||||
.button.disabled {
|
||||
|
|
@ -131,11 +127,4 @@
|
|||
stroke: white;
|
||||
}
|
||||
}
|
||||
.button.is-pressed-up:not(.disabled){
|
||||
background-color: #343434;
|
||||
color: white;
|
||||
:global(svg) {
|
||||
stroke: white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@
|
|||
{#if !context.taskDraggingManager.isDragging}
|
||||
<div
|
||||
class="settings-panel"
|
||||
role="toolbar"
|
||||
tabindex="-1"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
onpointerdown={(e) => e.stopPropagation()}
|
||||
onpointerup={(e) => e.stopPropagation()}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -18,14 +18,14 @@
|
|||
coords: {x: number, y: number}
|
||||
} = $props();
|
||||
|
||||
let self: HTMLElement;
|
||||
let viewport: HTMLElement;
|
||||
let self = $state<HTMLElement | undefined>(undefined);
|
||||
let viewport = $state<HTMLElement | undefined>(undefined);
|
||||
|
||||
onMount(() => {
|
||||
viewport = self?.closest('.viewport') as HTMLElement;
|
||||
viewport = self?.closest(".viewport") as HTMLElement | undefined;
|
||||
return () => {
|
||||
viewport.focus();
|
||||
}
|
||||
viewport?.focus();
|
||||
};
|
||||
});
|
||||
|
||||
let taskData = $derived(context.versionedData.getTask(taskId));
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@
|
|||
let isSelected = $derived(context.isSelected(taskId));
|
||||
let isEditing = $derived(context.editingTaskId === taskId);
|
||||
let suggest: LinkSuggest | null = null;
|
||||
let textPreviewEl: HTMLElement;
|
||||
let textEditEl: HTMLTextAreaElement;
|
||||
let textPreviewEl = $state<HTMLElement | undefined>(undefined);
|
||||
let textEditEl = $state<HTMLTextAreaElement | undefined>(undefined);
|
||||
let component = new Component(); // Required by Obsidian to manage render lifecycle
|
||||
let isDragging = $derived(context.taskDraggingManager.isDragging);
|
||||
onMount(() => {
|
||||
|
|
@ -78,7 +78,7 @@
|
|||
const target = e.target as HTMLElement;
|
||||
const link = target.closest(".internal-link");
|
||||
|
||||
if (link) {
|
||||
if (link && textPreviewEl) {
|
||||
// Trigger the native hover preview
|
||||
context.app.workspace.trigger("hover-link", {
|
||||
event: e,
|
||||
|
|
@ -92,6 +92,9 @@
|
|||
}
|
||||
|
||||
async function renderMarkdown() {
|
||||
if (!textPreviewEl) {
|
||||
return;
|
||||
}
|
||||
textPreviewEl.empty(); // Clear previous render
|
||||
const content = taskData.name;
|
||||
await MarkdownRenderer.render(
|
||||
|
|
@ -121,18 +124,22 @@
|
|||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
const el = textEditEl;
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
textEditEl.blur(); // Triggers handleBlur
|
||||
el.blur(); // Triggers handleBlur
|
||||
} else if (e.key === "Tab" && suggest !== null) {
|
||||
// another hack to select suggest on tab
|
||||
e.preventDefault();
|
||||
textEditEl.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
|
||||
el.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
|
||||
} else if (e.key == "Escape" && isEditing) {
|
||||
e.stopPropagation();
|
||||
textEditEl.blur();
|
||||
el.blur();
|
||||
} else if (e.key == "Escape") {
|
||||
textEditEl.blur();
|
||||
el.blur();
|
||||
} else {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
|
@ -148,7 +155,7 @@
|
|||
}
|
||||
|
||||
function handleInput() {
|
||||
if (textEditEl === null) {
|
||||
if (textEditEl == null) {
|
||||
return;
|
||||
}
|
||||
let path;
|
||||
|
|
@ -174,6 +181,7 @@
|
|||
class="task-text-container"
|
||||
class:selected={isSelected}
|
||||
class:not-selected={!isSelected}
|
||||
role="group"
|
||||
onpointerup={handlePreviewClick}
|
||||
>
|
||||
{#if isEditing}
|
||||
|
|
@ -190,8 +198,9 @@
|
|||
<div
|
||||
class="text-preview tasktext"
|
||||
class:unselect={isUnselected}
|
||||
role="group"
|
||||
bind:this={textPreviewEl}
|
||||
onmouseover={handlePreviewMouseOver}
|
||||
onmousemove={handlePreviewMouseOver}
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -218,9 +227,6 @@
|
|||
.task-text-container.selected .tasktext:hover {
|
||||
cursor: text;
|
||||
}
|
||||
.task-text-container.not-selected .tasktext:hover {
|
||||
/*cursor: default;*/
|
||||
}
|
||||
.tasktext {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
|
@ -269,19 +275,21 @@
|
|||
overflow: visible;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
p {
|
||||
top: 0;
|
||||
width: var(--task-width);
|
||||
height: var(--task-height);
|
||||
line-height: var(--task-line-height);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
gap: 0;
|
||||
border: none;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: visible;
|
||||
:global {
|
||||
p {
|
||||
top: 0;
|
||||
width: var(--task-width);
|
||||
height: var(--task-height);
|
||||
line-height: var(--task-line-height);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
gap: 0;
|
||||
border: none;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -146,10 +146,12 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<div
|
||||
class="viewport"
|
||||
class:is-panning={draggingManager.mouseDown === MouseDown.MIDDLE}
|
||||
class:unselect={context.chosenBlockerId !== NoTaskId || context.chosenBlockedId !== NoTaskId}
|
||||
role="application"
|
||||
bind:this={viewportEl}
|
||||
tabindex="-1"
|
||||
{onwheel}
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@
|
|||
}: {taskId: TaskId,
|
||||
context: Context} = $props();
|
||||
|
||||
let self: HTMLElement;
|
||||
let viewport: HTMLElement;
|
||||
let self: HTMLElement = $state(null! as HTMLElement);
|
||||
let viewport: HTMLElement = $state(null! as HTMLElement);
|
||||
|
||||
onMount(() => {
|
||||
viewport = self?.closest('.viewport') as HTMLElement;
|
||||
|
|
@ -90,10 +90,13 @@
|
|||
<div
|
||||
class="toolbar"
|
||||
class:no-pan={true}
|
||||
role="toolbar"
|
||||
tabindex="-1"
|
||||
bind:this={self}
|
||||
in:fade|global={{ duration: 500 }}
|
||||
out:fade|global={{ duration: 300 }}
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
onpointerdown={(e) => e.stopPropagation()}
|
||||
onpointerup={(e) => e.stopPropagation()}
|
||||
style="
|
||||
|
|
@ -111,6 +114,7 @@
|
|||
{#if context.pressedButtonCode === IconCode.REMOVE_SUBMENU}
|
||||
<div
|
||||
class="subtoolbar"
|
||||
role="group"
|
||||
transition:slideCustom={{ duration: 300, easing: quintOut, axis: '-y' }}
|
||||
style="
|
||||
top: {subtoolbarTopShift(removeButtons)}px;
|
||||
|
|
@ -135,6 +139,7 @@
|
|||
{#if context.pressedButtonCode === IconCode.STATUS_SUBMENU}
|
||||
<div
|
||||
class="subtoolbar"
|
||||
role="group"
|
||||
transition:slideCustom={{ duration: 300, easing: quintOut, axis: '-y' }}
|
||||
style="
|
||||
top: {subtoolbarTopShift(statusButtons)}px;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,6 @@ export class HistoryManager {
|
|||
}
|
||||
|
||||
lastAction() {
|
||||
return this.undoStack.last();
|
||||
return this.undoStack[this.undoStack.length - 1];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
18
src/main.ts
18
src/main.ts
|
|
@ -1,21 +1,19 @@
|
|||
import {
|
||||
addIcon,
|
||||
Plugin,
|
||||
} from "obsidian";
|
||||
import { addIcon, Plugin } from "obsidian";
|
||||
import { TASKMAP_VIEW_TYPE, TaskmapView } from "./TaskmapView";
|
||||
import {
|
||||
DEFAULT_SETTINGS,
|
||||
type TaskmapPluginSettings,
|
||||
} from "./TaskmapPluginSettings";
|
||||
import { TaskmapSettingTab } from "./TaskmapSettingTab";
|
||||
import { DEFAULT_DATA} from "./data/ProjectData.svelte";
|
||||
import { DEFAULT_DATA } from "./data/ProjectData.svelte";
|
||||
import { LOGO_CONTENT, LOGO_NAME } from "./IconService";
|
||||
import { getOnDelete, getOnRename} from "./FileWatcher";
|
||||
import { FileWatcherWithCache } from "./FileWatcherWithCache";
|
||||
|
||||
export const FILE_EXTENSION = "taskmap";
|
||||
|
||||
export default class TaskmapPlugin extends Plugin {
|
||||
settings: TaskmapPluginSettings;
|
||||
private readonly filewatcher = new FileWatcherWithCache();
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
|
@ -32,12 +30,8 @@ export default class TaskmapPlugin extends Plugin {
|
|||
|
||||
this.addSettingTab(new TaskmapSettingTab(this.app, this));
|
||||
|
||||
this.registerEvent(
|
||||
this.app.vault.on("rename", getOnRename(this.app)),
|
||||
);
|
||||
this.registerEvent(
|
||||
this.app.vault.on("delete", getOnDelete(this.app)),
|
||||
);
|
||||
this.filewatcher.initFromVault(this.app);
|
||||
this.filewatcher.registerTaskmapVaultHooks(this);
|
||||
}
|
||||
|
||||
public async createAndOpenDrawing(): Promise<string> {
|
||||
|
|
|
|||
Loading…
Reference in a new issue