many changes, including fixes and focus implementation

This commit is contained in:
poanse 2025-12-30 18:32:52 +03:00
parent 4a08bfa147
commit a19c910a0c
15 changed files with 433 additions and 126 deletions

View file

@ -1,10 +1,19 @@
import TaskmapPlugin from "./main";
import type { ProjectData } from "./ProjectData.svelte.js";
import { StatusCode, type TaskId } from "./types";
import { StatusCode, type TaskData, type TaskId } from "./types";
import { Spring } from "svelte/motion";
import type { App } from "obsidian";
import {
type App,
MarkdownView,
Notice,
TFile,
type WorkspaceLeaf,
} from "obsidian";
import type { TaskmapView } from "./TaskmapView";
import type { NodePositionsCalculator } from "./NodePositionsCalculator";
import {
type NodePositionsCalculator,
RootTaskId,
} from "./NodePositionsCalculator";
export class Context {
app: App;
@ -12,6 +21,7 @@ export class Context {
nodePositionsCalculator: NodePositionsCalculator;
pressedButtonCode = $state(-1);
selectedTaskId = $state(-1);
focusedTaskId = $state(RootTaskId);
toolbarStatus = $state(StatusCode.DRAFT);
projectData: ProjectData;
taskPositions: Array<{
@ -51,10 +61,40 @@ export class Context {
this.updateOnZoomCounter += 1;
}
public isTaskHidden(taskId: TaskId): boolean {
const task = this.projectData.getTask(taskId);
const ancestors = this.projectData
.getAncestors(this.focusedTaskId)
.map((t) => t.taskId);
const descendants = this.projectData.getDescendants(this.focusedTaskId);
return (
task.deleted ||
!(
task.taskId == this.focusedTaskId ||
ancestors.contains(task.taskId) ||
descendants.contains(task.taskId)
)
);
}
public changeFocusedTask(taskId: TaskId): void {
this.focusedTaskId = taskId;
this.updateTaskPositions();
}
public isAncestorOfHidden(taskId: TaskId): boolean {
return this.projectData
.getAncestors(this.focusedTaskId)
.map((t) => t.taskId)
.contains(taskId);
}
public updateTaskPositions() {
const positions =
this.nodePositionsCalculator.CalculatePositionsInGlobalFrame(
this.projectData.tasks.filter((t) => !t.deleted),
this.projectData.tasks.filter(
(t) => !this.isTaskHidden(t.taskId),
),
{ x: 0, y: 0 },
);
this.taskPositions = this.taskPositions.filter(
@ -63,7 +103,7 @@ export class Context {
this.taskPositions.forEach((taskPos) => {
const newPos = positions.get(taskPos.taskId);
if (newPos === undefined) {
throw new Error();
return;
}
if (taskPos.tween === null) {
taskPos.tween = new Spring(newPos, this.springOptions);
@ -132,11 +172,135 @@ export class Context {
// change its status
}
public hideTaskBranch(id: number) {
this.projectData.getTask(id).hidden = true;
}
public unhideTaskBranch(id: number) {
this.projectData.getTask(id).hidden = false;
}
public getCurrentTaskPosition(taskId: number) {
return this.taskPositions.find((t) => t.taskId === taskId)!.tween!
.current;
}
public isRemoveButtonEnabled() {
return this.selectedTaskId != RootTaskId;
}
/**
* Creates note named after task name if not exists already.
* Changes task name to a link to the node.
* Opens the note on the right side.
* @param taskId
*/
public async createLinkedNote(taskId: TaskId) {
const filepath = this.filePathFromTask(taskId);
try {
let abstractFile = this.app.vault.getAbstractFileByPath(filepath);
let tfile: TFile =
abstractFile instanceof TFile
? abstractFile
: await this.app.vault.create(
filepath,
`Created by Taskmap.`,
);
const task = this.projectData.getTask(taskId);
task.name = this.tasknameFromFilePath(filepath);
this.save();
await this.openOrFocusNote(tfile);
} catch (error) {
new Notice(
"Error creating note. It might already exist or the name is invalid.",
);
console.error(error);
}
}
public filePathFromTask(taskId: TaskId) {
const task = this.projectData.getTask(taskId);
const taskName = task.name;
// Sanitize the name for Obsidian filenames
let sanitizedName = taskName.replace(/[\\/:*?"<>|]/g, "-");
sanitizedName = this.delink(sanitizedName);
return `${sanitizedName}.md`;
}
public tasknameFromFilePath(path: string) {
if (path.endsWith(".md")) {
path = path.slice(0, path.length - 3);
}
return `[[${path}]]`;
}
/**
* Opens a file with "Smart Focus":
* 1. If file is already open anywhere -> Focus it.
* 2. If an unpinned tab exists in another pane -> Open it there.
* 3. If everything else is pinned or only one pane exists -> Create a new split.
*/
public async openOrFocusNote(file: TFile) {
const { workspace } = this.app;
// 1. Check if the file is already open in any leaf (pinned or not)
let existingLeaf: WorkspaceLeaf | null = null;
workspace.iterateAllLeaves((leaf) => {
if (
leaf.view instanceof MarkdownView &&
leaf.view.file?.path === file.path
) {
existingLeaf = leaf;
}
});
if (existingLeaf) {
workspace.setActiveLeaf(existingLeaf, { focus: true });
return;
}
// 2. Look for a reusable (unpinned) leaf in the root split
const activeLeaf = workspace.getMostRecentLeaf();
const rootLeaves: WorkspaceLeaf[] = [];
workspace.iterateRootLeaves((leaf) => {
rootLeaves.push(leaf);
});
const anotherLeaf = rootLeaves.reverse().find((leaf) => {
return leaf !== activeLeaf;
});
const anotherUnpinnedLeaf = rootLeaves.reverse().find((leaf) => {
return leaf !== activeLeaf && !leaf.getViewState().pinned;
});
if (anotherUnpinnedLeaf) {
// Reuse the unpinned secondary pane
await anotherUnpinnedLeaf.openFile(file);
workspace.setActiveLeaf(anotherUnpinnedLeaf, { focus: true });
} else if (anotherLeaf) {
workspace.setActiveLeaf(anotherLeaf, { focus: true });
const newLeaf = workspace.getLeaf("tab");
await newLeaf.openFile(file);
if (activeLeaf !== null) {
workspace.setActiveLeaf(activeLeaf);
}
} else {
// 3. If no reusable leaf exists (all are pinned or only one exists), create a split
// This will create a new vertical pane that is unpinned by default
const newLeaf = workspace.getLeaf("split", "vertical");
await newLeaf.openFile(file);
}
}
public isLink(s: string): boolean {
return s.startsWith("[[") && s.endsWith("]]");
}
public delink(s: string) {
return this.isLink(s) ? s.slice(2, s.length - 2) : s;
}
public serializeForDebugging() {
return JSON.stringify({
pressedButtonIndex: this.pressedButtonCode,

View file

@ -63,8 +63,8 @@ export function slideCustom(
`padding-${secondary_properties[1]}: ${t * padding_end_value}px;` +
`margin-${secondary_properties[0]}: ${t * margin_start_value}px;` +
`margin-${secondary_properties[1]}: ${t * margin_end_value}px;` +
`border-${secondary_properties[0]}-width: ${t * border_width_start_value}px;` +
`border-${secondary_properties[1]}-width: ${t * border_width_end_value}px;` +
// `border-${secondary_properties[0]}-width: ${t * border_width_start_value}px;` +
// `border-${secondary_properties[1]}-width: ${t * border_width_end_value}px;` +
`min-${primary_property}: 0` +
(axis === "-y"
? `; top: ${parseFloat(style["top"]) + primary_property_value * (1 - t)}px`

View file

@ -28,8 +28,9 @@ export enum AlgorithmEnum {
DoubleRow,
}
export const ParentToChildHorizontalShift = 400;
export class NodePositionsCalculator {
public readonly ParentToChildHorizontalShift = 400;
public readonly SiblingDelta = 90;
public Algorithm: AlgorithmEnum = AlgorithmEnum.DefaultTree;
public subtreeWidthByHalfPriority: Map<number, number> = new Map<
@ -222,7 +223,7 @@ export class NodePositionsCalculator {
finalChildShifts.set(
t.taskId,
V2.add(
{ x: this.ParentToChildHorizontalShift, y: 0 },
{ x: ParentToChildHorizontalShift, y: 0 },
verticalComponent,
),
);
@ -281,7 +282,7 @@ export class NodePositionsCalculator {
x:
finalChildShifts.get(x.taskId)!.x +
subtreeRelWidthAccumulator *
this.ParentToChildHorizontalShift,
ParentToChildHorizontalShift,
y: yShiftByRowId.get(this.RowIndex(x))!,
});
subtreeRelWidthAccumulator +=
@ -330,7 +331,7 @@ export class NodePositionsCalculator {
x:
finalChildShifts.get(x.taskId)!.x +
(this.xshift + this.xshift * t.length + tSum) *
this.ParentToChildHorizontalShift,
ParentToChildHorizontalShift,
y:
this.RowIndex(x) === 0
? yShiftByRowId.get(this.RowIndex(x))!

View file

@ -37,6 +37,7 @@ export class ProjectData {
priority: 0,
depth: 0,
deleted: false,
hidden: false,
});
this.curTaskId++;
}
@ -49,6 +50,7 @@ export class ProjectData {
status: StatusCode.DRAFT,
name: "default",
deleted: false,
hidden: false,
priority: 0, // TODO: change to amount of siblings
depth: this.getTask(parentId).depth + 1,
});
@ -58,10 +60,13 @@ export class ProjectData {
public removeTaskSingle(id: number) {
const task = this.getTask(id);
const parentTask = this.getTask(task.parentId);
task.deleted = true;
this.getChildren(id).forEach(
(taskId) => (this.getTask(taskId).parentId = task.parentId),
);
this.getChildren(id).forEach((taskId) => {
const t = this.getTask(taskId);
t.parentId = parentTask.taskId;
t.depth = parentTask.depth + 1;
});
}
public removeTaskBranch(id: number) {
@ -84,6 +89,16 @@ export class ProjectData {
return result;
}
public getAncestors(taskId: number) {
const res: TaskData[] = [];
let task = this.getTask(taskId);
while (task.depth != 0) {
task = this.getTask(task.parentId);
res.push(task);
}
return res;
}
public getChildren(taskId: number, includeDeleted: boolean = false) {
let res = this.tasks.filter((t) => t.parentId === taskId);
if (!includeDeleted) {
@ -105,18 +120,60 @@ export class ProjectData {
return this.getTask(taskId).deleted;
}
public isBranchHidden(id: number) {
return this.getAncestors(id).some((t) => t.hidden);
}
public toggleHidden(id: number) {
const task = this.getTask(id);
task.hidden = !task.hidden;
}
public getTaskStatus(taskId: number) {
return this.getTask(taskId)!.status;
}
public getTaskName(taskId: number) {
return this.getTask(taskId)!.name;
}
public setTaskStatus(taskId: number, status: StatusCode) {
const task = this.getTask(taskId);
if (task) {
task.status = status;
task.status = status;
this.recalculateStatusRecursive(task.parentId);
}
public recalculateStatusRecursive(taskId: TaskId) {
if (taskId == RootTaskId) {
return;
}
const task = this.getTask(taskId);
if (task.status == StatusCode.DRAFT) {
return;
}
task.status = this.calculateStatus(taskId);
this.recalculateStatusRecursive(task.parentId);
}
public calculateStatus(taskId: TaskId) {
const children = this.getChildren(taskId).map((x) => this.getTask(x));
const counts = [0, 0, 0, 0];
children.forEach((t) => (counts[t.status] += 1));
if (counts[StatusCode.DONE] == children.length) {
return StatusCode.DONE;
} else if (counts[StatusCode.DONE] > 0) {
return StatusCode.IN_PROGRESS;
} else if (counts[StatusCode.IN_PROGRESS] > 0) {
return StatusCode.IN_PROGRESS;
} else if (counts[StatusCode.DRAFT] == children.length) {
return StatusCode.DRAFT;
} else if (counts[StatusCode.READY] == children.length) {
return StatusCode.READY;
} else {
return StatusCode.READY;
}
}
public setTaskName(taskId: number, name: string) {
const task = this.getTask(taskId);
if (task) {

View file

@ -1,11 +1,11 @@
import { debounce, TextFileView, TFile, WorkspaceLeaf } from "obsidian";
import { mount } from "svelte";
import { mount, unmount } from "svelte";
import { DEFAULT_DATA, ProjectData } from "./ProjectData.svelte";
import { Context } from "./Context.svelte.js";
import { NodePositionsCalculator } from "./NodePositionsCalculator";
import TaskmapContainer from "./components/TaskmapContainer.svelte";
export const VIEW_TYPE_EXAMPLE = "example";
export const VIEW_TYPE = "taskmap-view";
export class TaskmapView extends TextFileView {
taskmapContainer: ReturnType<typeof TaskmapContainer> | undefined;
@ -20,7 +20,7 @@ export class TaskmapView extends TextFileView {
projectData: ProjectData;
getViewType() {
return VIEW_TYPE_EXAMPLE;
return VIEW_TYPE;
}
async onLoadFile(file: TFile): Promise<void> {
@ -75,6 +75,12 @@ export class TaskmapView extends TextFileView {
clear(): void {
this.debouncedSave.cancel();
this.setViewData(DEFAULT_DATA);
// 3. Proper unmounting in the clear cycle
if (this.taskmapContainer) {
unmount(this.taskmapContainer);
this.taskmapContainer = undefined;
}
this.contentEl.empty();
}
async onUnloadFile(file: TFile): Promise<void> {
@ -86,11 +92,6 @@ export class TaskmapView extends TextFileView {
}
async onClose() {
this.debouncedSave.cancel();
this.clear();
}
// onChange(value: string) {
// this.setViewData(value);
// this.debouncedSave();
// }
}

View file

@ -1,104 +1,82 @@
<script lang="ts">
import {TASK_SIZE} from "../Constants";
import {StatusCode} from "../types";
import {Context} from "../Context.svelte.js";
import { TASK_SIZE } from "../Constants";
import { StatusCode } from "../types";
import { Context } from "../Context.svelte.js";
const {
taskId,
context,
}: {
taskId: number,
context: Context,
} = $props();
const { taskId, context }: { taskId: number, context: Context } = $props();
let taskData = $derived(context.projectData.getTask(taskId));
let entered = $state(false);
function onEnter() {
entered = true;
}
function onLeave() {
entered = false;
}
function addButtonPressed (event: MouseEvent) {
console.log('add icon clicked')
function addButtonPressed(event: MouseEvent) {
context.addTask(taskId);
event.stopPropagation();
}
</script>
<div class="hover-area"
onmouseenter={onEnter}
onmouseleave={onLeave}
onblur={onLeave}
style="
<div
class="hover-container-add-task-button"
onmouseenter={() => entered = true}
onmouseleave={() => entered = false}
style="
left: {TASK_SIZE.width - 50/2}px;
top: {TASK_SIZE.height/2 - 50/2}px;
width: 50px;
height: 50px;
"
>
</div>
{#if entered}
{#if entered}
<svg
class="button-add"
class:draft={taskData.status === StatusCode.DRAFT}
class:ready={taskData.status === StatusCode.READY}
class:in-progress={taskData.status === StatusCode.IN_PROGRESS}
class:done={taskData.status === StatusCode.DONE}
onmouseenter={onEnter}
onmouseleave={onLeave}
onclick={addButtonPressed}
onblur={onLeave}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"
style="
position: absolute;
left: {TASK_SIZE.width - 41/2 - 1}px;
top: {TASK_SIZE.height/2 - 41/2}px;
width: 41;
height: 41;
"
>
<circle
cx="12" cy="12" r="11"
/>
<path
stroke-width="2"
d="M5 12h14"
/><path
stroke-width="2"
d="M12 5v14"
/>
<circle cx="12" cy="12" r="11" />
<path stroke-width="2" d="M5 12h14" /><path stroke-width="2" d="M12 5v14" />
</svg>
{/if}
{/if}
</div>
<style>
.hover-area {
.hover-container-add-task-button {
position: absolute;
/*border: 2px dashed #888;*/
border: none;
width: 50px;
height: 50px;
/* Centering logic for the SVG */
display: flex;
align-items: center;
justify-content: center;
/* Ensure the transparent area still catches mouse events */
background: transparent;
pointer-events: auto;
}
:global(svg.draft) {
stroke: #7E7E7E;
fill: #1E1E1E;
}
:global(svg.ready) {
stroke: #A1383D;
fill: #2E2122;
}
:global(svg.in-progress) {
stroke: #A6A45D;
fill: #2C2C24;
}
:global(svg.done) {
stroke: #3E9959;
fill: #212B24;
cursor: pointer;
.button-add {
width: 41px;
height: 41px;
stroke-width: 1;
fill: none;
stroke: currentColor;
}
:global(svg.draft) {
stroke: #7E7E7E;
fill: #1E1E1E;
}
:global(svg.ready) {
stroke: #A1383D;
fill: #2E2122;
}
:global(svg.in-progress) {
stroke: #A6A45D;
fill: #2C2C24;
}
:global(svg.done) {
stroke: #3E9959;
fill: #212B24;
}
}
</style>

View file

@ -1,7 +1,7 @@
<script lang="ts">
import type {Context} from "../Context.svelte.js";
import {IconCode, StatusCode} from "../types";
import {Circle, KeyRound, LocateFixed, Lock, RectangleHorizontal, Trash2, Crosshair } from 'lucide-svelte';
import {Circle, KeyRound, LocateFixed, Lock, RectangleHorizontal, Trash2, SquareArrowOutUpRight } from 'lucide-svelte';
let {
iconCode,
@ -51,10 +51,14 @@
}
if (newStatus != null) {
context.changeStatus(newStatus);
} else if (iconCode === IconCode.FOCUS) {
context.changeFocusedTask(context.selectedTaskId);
} else if (iconCode == IconCode.REMOVE_SINGLE) {
context.removeTaskSingle(context.selectedTaskId);
} else if (iconCode == IconCode.REMOVE_MULTIPLE) {
context.removeTaskBranch(context.selectedTaskId);
} else if (iconCode ==IconCode.CREATE_LINKED_NOTE) {
context.createLinkedNote(context.selectedTaskId);
}
event.stopPropagation();
@ -76,9 +80,6 @@
onmouseleave={onBlur}
onclick={(event: MouseEvent) => {
event.stopPropagation();
if (IconCode.FOCUS === iconCode) {
context.setSelectedTaskId(-1);
}
}}
role="presentation"
tabindex="-1"
@ -100,6 +101,8 @@
<Lock class={classString}/>
{:else if iconCode === IconCode.FOCUS}
<LocateFixed class={classString + " focus"}/>
{:else if iconCode === IconCode.CREATE_LINKED_NOTE}
<SquareArrowOutUpRight class={classString}/>
{:else if iconCode === IconCode.STATUS}
<Circle class={classString}/>
{:else if iconCode === IconCode.STATUS_DRAFT}
@ -124,7 +127,7 @@
padding: 0;
/*background-color: #e0e0e0;*/
cursor: pointer;
border-radius: 4px;
border-radius: 8px;
user-select: none;
background: #0f0f0fff;
@ -176,7 +179,7 @@
border-width: 0;
outline: none;
}
.button.is-pressed-down {
background-color: #343434;
color: white;

View file

@ -13,8 +13,8 @@
});
</script>
<!--in:fade={{duration: 500}}-->
<path
in:fade={{duration: 500}}
d={pathData}
fill="transparent"
stroke-linecap="round"

View file

@ -0,0 +1,64 @@
<script lang="ts">
import { TASK_SIZE } from "../Constants";
import { Context } from "../Context.svelte.js";
import {ParentToChildHorizontalShift} from "../NodePositionsCalculator";
import { Eye, EyeClosed } from 'lucide-svelte';
const { taskId, context }: { taskId: number, context: Context } = $props();
let taskData = $derived(context.projectData.getTask(taskId));
let entered = $state(false);
function hidePressed(event: MouseEvent) {
context.projectData.toggleHidden(taskId);
event.stopPropagation();
}
</script>
{#if context.projectData.getChildren(taskId).length > 0}
<div
class="hover-container-hide-branch-button"
onmouseenter={() => entered = true}
onmouseleave={() => entered = false}
style="
left: {TASK_SIZE.width / 2 + ParentToChildHorizontalShift / 2 - 50/2}px;
top: {TASK_SIZE.height / 2 - 50 / 2}px;
"
>
{#if taskData.hidden}
<EyeClosed onclick={hidePressed}/>
{:else if entered && !taskData.hidden}
<Eye onclick={hidePressed}/>
{/if}
</div>
{/if}
<style>
.hover-container-hide-branch-button {
position: absolute;
width: 50px;
height: 50px;
/* Centering logic for the SVG */
display: flex;
align-items: center;
justify-content: center;
/* Ensure the transparent area still catches mouse events */
background: transparent;
pointer-events: auto;
cursor: pointer;
:global(svg) {
transition: stroke 0.2s;
width: 41px;
height: 41px;
stroke: #bbb;
fill: #181818;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
will-change: transform,scale,translate;
}
}
</style>

View file

@ -3,6 +3,7 @@
import {StatusCode} from "../types";
import TaskText from "./TaskText.svelte";
import AddTaskButton from "./AddTaskButton.svelte";
import HideBranchButton from "./HideBranchButton.svelte";
const {
taskId,
@ -16,7 +17,7 @@
let taskData = $derived(context.projectData.getTask(taskId));
// let coords = $derived(context.getTaskPosition(taskId));
let isUnselected = $derived(context.isAncestorOfHidden(taskId));
let isHovered = $state(false);
// derived here is a must
let isSelected = $derived(context.isSelected(taskId));
@ -99,14 +100,13 @@
</script>
{#if !context.projectData.isTaskDeleted(taskId)}
{#if !context.isTaskHidden(taskId) && !context.projectData.isBranchHidden(taskId)}
{#key context.updateOnZoomCounter}
<div
class="task-container"
style="
top: {coords.y}px;
left: {coords.x}px;
position: absolute;
"
>
<div
@ -117,6 +117,7 @@
class:ready={taskData.status === StatusCode.READY}
class:in-progress={taskData.status === StatusCode.IN_PROGRESS}
class:done={taskData.status === StatusCode.DONE}
class:unselect={isUnselected}
onmouseenter={() => isHovered = true}
onmouseleave={() => isHovered = false}
onmousedown={(event: MouseEvent) => {
@ -135,6 +136,7 @@
{#if useNewTextElement}
<TaskText
{taskId}
{isUnselected}
{context}
app={context.app}
content={taskData.name}
@ -161,6 +163,7 @@
{/if}
</div>
<AddTaskButton {context} {taskId} />
<HideBranchButton {context} {taskId} />
</div>
{/key}
{/if}
@ -168,6 +171,7 @@
<style>
.task-container {
z-index: 3; /* over lines*/
position: absolute;
}
.task {
/*background: #111;*/
@ -216,4 +220,8 @@
border-color: #3E9959;
background-color: #212B24;
}
.task.unselect {
border-color: color-mix(in srgb, #7E7E7E 100%, #000000 50%);
background-color: color-mix(in srgb, #1E1E1E 100%, #000000 50%);
}
</style>

View file

@ -1,12 +1,13 @@
<script lang="ts">
import { onMount, tick } from "svelte";
import {MarkdownRenderer, Component, App} from "obsidian";
import {MarkdownRenderer, Component, App, type TFile} from "obsidian";
import {LinkSuggest} from "../LinkSuggest";
import type {Context} from "../Context.svelte.js";
// PROPS
let {
taskId,
isUnselected,
context,
app,
content,
@ -14,6 +15,7 @@
onSave
}: {
taskId: number,
isUnselected: boolean,
context: Context,
app: App;
content: string;
@ -62,9 +64,11 @@
// Open the link using Obsidian's API
// Whether to open in a new tab or in current one
const openInNewLeaf = e.ctrlKey || e.metaKey;
app.workspace.openLinkText(href, sourcePath, openInNewLeaf);
// const openInNewLeaf = e.ctrlKey || e.metaKey;
// app.workspace.openLinkText(href, sourcePath, openInNewLeaf);
const filepath = context.filePathFromTask(taskId);
const file = context.app.vault.getAbstractFileByPath(filepath) as TFile;
context.openOrFocusNote(file);
return;
}
@ -145,6 +149,7 @@
{#if isEditing}
<textarea
class="text-edit tasktext"
class:unselect={isUnselected}
maxlength="28"
bind:this={textEditEl}
bind:value={content}
@ -154,6 +159,7 @@
{:else}
<div
class="text-preview tasktext"
class:unselect={isUnselected}
tabindex="0"
bind:this={textPreviewEl}
onpointerup={handlePreviewClick}
@ -202,6 +208,9 @@
color: white;
overflow: hidden;
}
.tasktext.unselect {
color: color-mix(in srgb, #7E7E7E 100%, #000000 50%);
}
.task-text-container.selected .tasktext:hover {
cursor: text;
}

View file

@ -139,7 +139,11 @@
>
<svg class="svg-layer" overflow="visible">
<g class="svg-group" bind:this={svgGroupEl}>
{#each context.projectData.tasks.filter(t => !t.deleted).filter(t => t.taskId !== RootTaskId) as task (task.taskId)}
{#each (context.projectData.tasks
.filter(t => !context.isTaskHidden(t.taskId))
.filter(t => t.taskId !== RootTaskId)
.filter(t => !context.projectData.isBranchHidden(t.taskId))
) as task (task.taskId)}
<Connection
startPoint={V2.add(context.getCurrentTaskPosition(task.parentId), {x: TASK_SIZE.width, y: TASK_SIZE.height/2})}
endPoint={V2.add(context.getCurrentTaskPosition(task.taskId), {x: 0, y: TASK_SIZE.height/2})}
@ -153,7 +157,7 @@
onkeydown={handleKey}
role="presentation"
>
{#each context.projectData.tasks.filter(t => !t.deleted) as task (task.taskId)}
{#each context.projectData.tasks.filter(t => !context.isTaskHidden(t.taskId)) as task (task.taskId)}
<Task taskId={task.taskId} {context} coords={context.getCurrentTaskPosition(task.taskId)}/>
{/each}

View file

@ -11,7 +11,7 @@
import {quintOut} from 'svelte/easing';
import {slideCustom} from '../Custom';
import type {Context} from "../Context.svelte.js";
import {IconCode} from "../types";
import {IconCode, toIconCode} from "../types";
let {
context
@ -25,6 +25,8 @@
function getLeft() {
return position.x + TASK_SIZE.width_hovered / 2 - TOOLBAR_SIZE.width / 2;
}
let isLeafTask = $derived(context.selectedTaskId != -1 && context.projectData.getChildren(context.selectedTaskId).length === 0);
</script>
@ -43,11 +45,14 @@
"
>
{#key context.updateOnZoomCounter}
<Button iconCode={IconCode.REMOVE} {context}/>
{#if context.isRemoveButtonEnabled()}
<Button iconCode={IconCode.REMOVE} {context}/>
{/if}
<Button iconCode={IconCode.KEY} {context} />
<Button iconCode={IconCode.LOCK} {context} />
<Button iconCode={IconCode.FOCUS} {context} />
<Button iconCode={IconCode.STATUS} {context} />
<Button iconCode={IconCode.CREATE_LINKED_NOTE} {context} />
{/key}
</div>
{/if}
@ -57,7 +62,7 @@
class="subtoolbar"
transition:slideCustom={{ duration: 300, easing: quintOut, axis: '-y' }}
style="
top: {getTop() - 2 * BUTTON_SIZE - TOOLBAR_GAP - 2 * TOOLBAR_PADDING.y - SUBTOOLBAR_SHIFT}px;
top: {getTop() - 2 * BUTTON_SIZE - TOOLBAR_GAP - 2 * TOOLBAR_PADDING.y - SUBTOOLBAR_SHIFT - 2}px;
left: {getLeft() - 2}px;
"
>
@ -73,15 +78,20 @@
class="subtoolbar"
transition:slideCustom={{ duration: 300, easing: quintOut, axis: '-y' }}
style="
top: {getTop() - 4 * BUTTON_SIZE - 3*TOOLBAR_GAP - 2*TOOLBAR_PADDING.y - SUBTOOLBAR_SHIFT}px;
top: {getTop() - (isLeafTask ? 2 : 1) * 2 * BUTTON_SIZE - ((isLeafTask ? 2 : 0) + 1)*TOOLBAR_GAP - 2*TOOLBAR_PADDING.y - SUBTOOLBAR_SHIFT - 2}px;
left: {getLeft() + 4 * (BUTTON_SIZE + TOOLBAR_GAP) - 2}px;
"
>
{#key context.updateOnZoomCounter}
<Button iconCode={IconCode.STATUS_DRAFT} {context} />
<Button iconCode={IconCode.STATUS_READY} {context} />
<Button iconCode={IconCode.STATUS_IN_PROGRESS} {context} />
<Button iconCode={IconCode.STATUS_DONE} {context} />
{#if isLeafTask}
<Button iconCode={IconCode.STATUS_DRAFT} {context} />
<Button iconCode={IconCode.STATUS_READY} {context} />
<Button iconCode={IconCode.STATUS_IN_PROGRESS} {context} />
<Button iconCode={IconCode.STATUS_DONE} {context} />
{:else}
<Button iconCode={IconCode.STATUS_DRAFT} {context} />
<Button iconCode={toIconCode(context.projectData.calculateStatus(context.selectedTaskId))} {context} />
{/if}
{/key}
</div>
{/if}
@ -113,14 +123,17 @@
/*border: 1px solid #ccc;*/
/*background: #181818;*/
border-color: #343434;
border-style: solid;
border-width: 2px;
background: #0f0f0fff;
/*background-color: #1E1E1E;*/
overflow: hidden; /* Important for slide animations to look clean */
display: flex;
flex-direction: column;
gap: 2px;
padding: 4px;
border-radius: 8px 4px;
padding: 2px;
border-radius: 8px;
justify-content: center;
align-items: center;
position: absolute;

View file

@ -1,12 +1,11 @@
import { addIcon, Plugin } from "obsidian";
import { TaskmapView } from "./TaskmapView";
import { TaskmapView, VIEW_TYPE } from "./TaskmapView";
import { type PluginSettings, DEFAULT_SETTINGS } from "./PluginSettings";
import { TaskmapSettingTab } from "./TaskmapSettingTab";
import { DEFAULT_DATA } from "./ProjectData.svelte";
import { LOGO_CONTENT, LOGO_NAME } from "./IconService";
export const FILE_EXTENSION = "taskmap";
export const VIEW_TYPE = "taskmap-view";
export default class TaskmapPlugin extends Plugin {
static instance: TaskmapPlugin;

View file

@ -5,6 +5,7 @@ export enum IconCode {
KEY,
LOCK,
FOCUS,
CREATE_LINKED_NOTE,
STATUS,
REMOVE_SINGLE,
REMOVE_MULTIPLE,
@ -21,6 +22,10 @@ export enum StatusCode {
DONE,
}
export const toIconCode = (s: StatusCode) => {
return (s + IconCode.STATUS_DRAFT) as number as IconCode;
};
export interface Vector2 {
x: number;
y: number;
@ -35,6 +40,7 @@ export type TaskData = {
parentId: TaskId;
depth: number;
priority: number;
hidden: boolean;
};
export interface SlideParamsCustom {