mirror of
https://github.com/poanse/obsidian-taskmap.git
synced 2026-07-22 06:05:58 +00:00
many changes, including fixes and focus implementation
This commit is contained in:
parent
4a08bfa147
commit
a19c910a0c
15 changed files with 433 additions and 126 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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))!
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<!--in:fade={{duration: 500}}-->
|
||||
<path
|
||||
in:fade={{duration: 500}}
|
||||
d={pathData}
|
||||
fill="transparent"
|
||||
stroke-linecap="round"
|
||||
|
|
|
|||
64
src/components/HideBranchButton.svelte
Normal file
64
src/components/HideBranchButton.svelte
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue