|
|
|
<script type="ts">
|
|
|
|
import { getContext } from "svelte";
|
|
|
|
|
|
|
|
import Graph from "./Graph.svelte";
|
|
|
|
import { nonNull, saveToLocalDisk } from "./helpers";
|
|
|
|
import type { TaskDescriptor, TasksFile } from "./tasks";
|
|
|
|
import { saveTasks, getCategories, tasksToString } from "./tasks";
|
|
|
|
import TaskDisplay from "./TaskDisplay.svelte";
|
|
|
|
import TaskDetailEditor from "./TaskDetailEditor.svelte";
|
|
|
|
import { forceSimulation } from "./force-simulation";
|
|
|
|
import {
|
|
|
|
refresh as refreshTaskStatuses,
|
|
|
|
taskStatuses,
|
|
|
|
} from "./task-status-cache";
|
|
|
|
import { isLoggedIn } from "./ksp-task-grabber";
|
|
|
|
|
|
|
|
export let tasks: TasksFile;
|
|
|
|
|
|
|
|
let repulsionForce: number = -1000;
|
|
|
|
let clicked: string[] = [];
|
|
|
|
let graph: Graph;
|
|
|
|
let currentTask: TaskDescriptor | null = null;
|
|
|
|
let nodeDraggingEnabled: boolean = true;
|
|
|
|
let angle: number;
|
|
|
|
let showHiddenEdges: boolean = false;
|
|
|
|
const { open } = getContext("simple-modal");
|
|
|
|
|
|
|
|
function clickTask(e: CustomEvent<TaskDescriptor>) {
|
|
|
|
// ukladani seznamu poslednich kliknuti
|
|
|
|
clicked = [...clicked, e.detail.id];
|
|
|
|
if (clicked.length > 3)
|
|
|
|
clicked = clicked.slice(clicked.length - 3, clicked.length);
|
|
|
|
}
|
|
|
|
|
|
|
|
function startHovering(e: CustomEvent<TaskDescriptor>) {
|
|
|
|
currentTask = e.detail;
|
|
|
|
}
|
|
|
|
|
|
|
|
function addEdge() {
|
|
|
|
if (clicked.length > 1) {
|
|
|
|
const first = clicked[clicked.length - 1];
|
|
|
|
const second = clicked[clicked.length - 2];
|
|
|
|
|
|
|
|
tasks.tasks.forEach((t) => {
|
|
|
|
if (t.id == first) {
|
|
|
|
t.requires = [...t.requires, second];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
tasks = tasks;
|
|
|
|
} else {
|
|
|
|
alert("Nope, prvni musis nekam klikat...");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function runSimulation() {
|
|
|
|
forceSimulation(
|
|
|
|
tasks,
|
|
|
|
(positions) => {
|
|
|
|
tasks.tasks.forEach((t) => (t.position = positions.get(t.id)));
|
|
|
|
tasks = tasks;
|
|
|
|
},
|
|
|
|
repulsionForce
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function saveCurrentState() {
|
|
|
|
await saveTasks(tasks);
|
|
|
|
}
|
|
|
|
|
|
|
|
// autosave ;)
|
|
|
|
let saveTimeoutHandle: NodeJS.Timeout | null = null;
|
|
|
|
function autosave() {
|
|
|
|
if (saveTimeoutHandle != null) clearTimeout(saveTimeoutHandle);
|
|
|
|
|
|
|
|
saveTimeoutHandle = setTimeout(async () => {
|
|
|
|
saveTimeoutHandle = null;
|
|
|
|
await saveTasks(tasks);
|
|
|
|
}, 5000);
|
|
|
|
}
|
|
|
|
$: {
|
|
|
|
tasks;
|
|
|
|
autosave();
|
|
|
|
}
|
|
|
|
|
|
|
|
function saveLocally() {
|
|
|
|
saveToLocalDisk("tasks.json", tasksToString(tasks));
|
|
|
|
}
|
|
|
|
|
|
|
|
function openTaskDetailEditorButton(e: CustomEvent<TaskDescriptor>) {
|
|
|
|
openTaskDetailEditor(e.detail);
|
|
|
|
}
|
|
|
|
|
|
|
|
function openTaskDetailEditor(t: TaskDescriptor) {
|
|
|
|
open(
|
|
|
|
TaskDetailEditor,
|
|
|
|
{ task: t, tasks: tasks },
|
|
|
|
{ closeButton: false, styleWindow: { width: "50vw" } },
|
|
|
|
{
|
|
|
|
onClose: () => {
|
|
|
|
tasks = tasks;
|
|
|
|
},
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function addTask() {
|
|
|
|
let id = prompt("Zadej ID nové úlohy (nepůjde nikdy změnit):");
|
|
|
|
if (id == null || id == "") {
|
|
|
|
alert("Něco tam zadat musíš!");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let existing = tasks.tasks.find((t) => t.id == id);
|
|
|
|
if (existing != undefined) {
|
|
|
|
alert("úloha s tímto ID již existuje!");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let novaUloha: TaskDescriptor = {
|
|
|
|
id: id,
|
|
|
|
type: "label",
|
|
|
|
comment: "...",
|
|
|
|
requires: [],
|
|
|
|
};
|
|
|
|
|
|
|
|
tasks.tasks = [...tasks.tasks, novaUloha];
|
|
|
|
|
|
|
|
openTaskDetailEditor(novaUloha);
|
|
|
|
}
|
|
|
|
|
|
|
|
function removeTask(id: string) {
|
|
|
|
// zkontrolovat existenci
|
|
|
|
let found = false;
|
|
|
|
for (const t of tasks.tasks) {
|
|
|
|
if (t.id == id) {
|
|
|
|
found = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!found) {
|
|
|
|
alert("Pokoušíš se smazat úlohu, která neexistuje. To je docela divné!");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// existují závislosti na tuhle úlohu?
|
|
|
|
let dependencyExists = false;
|
|
|
|
for (const t of tasks.tasks) {
|
|
|
|
for (const r of t.requires) {
|
|
|
|
if (r == id) {
|
|
|
|
dependencyExists = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (dependencyExists) {
|
|
|
|
alert(
|
|
|
|
"Pokoušíš se smazat úlohu, na které je někdo jiný závislý! To nejde! Smaž první závislost."
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// je to bezpečné, mažeme
|
|
|
|
tasks.tasks = tasks.tasks.filter((t) => t.id != id);
|
|
|
|
}
|
|
|
|
|
|
|
|
function getTask(id: string): TaskDescriptor | undefined {
|
|
|
|
return tasks.tasks.find((t) => t.id == id);
|
|
|
|
}
|
|
|
|
|
|
|
|
function setAngleToTheCurrentLabel() {
|
|
|
|
let t = getTask(clicked[clicked.length - 1]);
|
|
|
|
if (clicked.length > 0 && t != undefined && t.type == "label") {
|
|
|
|
t.rotationAngle = angle;
|
|
|
|
tasks = tasks;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function loadYear() {
|
|
|
|
if (!isLoggedIn()) {
|
|
|
|
alert("Musíš se přihlásit");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const y = prompt("Který ročník (číslo 26...X)");
|
|
|
|
await refreshTaskStatuses([`${y}-Z1-1`]);
|
|
|
|
const newTasks = Array.from($taskStatuses.values()).filter(
|
|
|
|
(t) =>
|
|
|
|
t.id.startsWith(y + "-") &&
|
|
|
|
!tasks.tasks.find(
|
|
|
|
(tt) => tt.type == "open-data" && tt.taskReference == t.id
|
|
|
|
)
|
|
|
|
);
|
|
|
|
const newDescriptors: TaskDescriptor[] = newTasks.map((t) => ({
|
|
|
|
type: "open-data",
|
|
|
|
id: t.id,
|
|
|
|
taskReference: t.id,
|
|
|
|
requires: [],
|
|
|
|
position: [0, 0],
|
|
|
|
title: t.name,
|
|
|
|
}));
|
|
|
|
tasks.tasks = [...tasks.tasks, ...newDescriptors];
|
|
|
|
}
|
|
|
|
|
|
|
|
function hideSelection() {
|
|
|
|
for (let t of graph.getCurrentSelection()) {
|
|
|
|
t.hidden = true;
|
|
|
|
}
|
|
|
|
tasks = tasks;
|
|
|
|
}
|
|
|
|
|
|
|
|
function showSelection() {
|
|
|
|
for (let t of graph.getCurrentSelection()) {
|
|
|
|
t.hidden = false;
|
|
|
|
}
|
|
|
|
tasks = tasks;
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<style>
|
|
|
|
.container {
|
|
|
|
/*display: flex;*/
|
|
|
|
/* flex-direction: row;*/
|
|
|
|
margin: 0;
|
|
|
|
height: 99vh;
|
|
|
|
width: 100%;
|
|
|
|
|
|
|
|
background-color: transparent;
|
|
|
|
}
|
|
|
|
|
|
|
|
.right {
|
|
|
|
margin-right: 2em;
|
|
|
|
float: right;
|
|
|
|
width: 40vw;
|
|
|
|
height: 100%;
|
|
|
|
z-index: 20;
|
|
|
|
position: relative;
|
|
|
|
}
|
|
|
|
|
|
|
|
.toolbox {
|
|
|
|
width: 100%;
|
|
|
|
height: 40%;
|
|
|
|
background-color: #1d1f21f0;
|
|
|
|
|
|
|
|
margin: 5px;
|
|
|
|
border: 1px solid gray;
|
|
|
|
padding: 5px;
|
|
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
}
|
|
|
|
|
|
|
|
.taskDetails {
|
|
|
|
width: 100%;
|
|
|
|
margin: 0;
|
|
|
|
height: 50%;
|
|
|
|
overflow-y: auto;
|
|
|
|
background-color: #1d1f21f0;
|
|
|
|
|
|
|
|
margin: 5px;
|
|
|
|
border: 1px solid gray;
|
|
|
|
padding: 5px;
|
|
|
|
}
|
|
|
|
|
|
|
|
button {
|
|
|
|
width: 45%;
|
|
|
|
border-radius: 0.5em;
|
|
|
|
border: 2px solid white;
|
|
|
|
background-color: transparent;
|
|
|
|
color: white;
|
|
|
|
transition: auto;
|
|
|
|
}
|
|
|
|
|
|
|
|
button:hover:not(:disabled) {
|
|
|
|
background-color: #888;
|
|
|
|
}
|
|
|
|
|
|
|
|
button:active:not(:disabled) {
|
|
|
|
background-color: #eee;
|
|
|
|
color: black;
|
|
|
|
}
|
|
|
|
|
|
|
|
button:disabled {
|
|
|
|
border-color: #aaa;
|
|
|
|
color: #aaa;
|
|
|
|
}
|
|
|
|
|
|
|
|
.topLeftHint {
|
|
|
|
text-align: left;
|
|
|
|
padding-top: 1em;
|
|
|
|
padding-left: 2em;
|
|
|
|
width: 50%;
|
|
|
|
height: 5%;
|
|
|
|
float: left;
|
|
|
|
}
|
|
|
|
|
|
|
|
:global(body) {
|
|
|
|
background-color: #1d1f21;
|
|
|
|
color: white;
|
|
|
|
}
|
|
|
|
.checkbox {
|
|
|
|
width: 45%;
|
|
|
|
display: inline-block;
|
|
|
|
}
|
|
|
|
.gap {
|
|
|
|
height: 1em;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
|
|
|
|
<Graph
|
|
|
|
{tasks}
|
|
|
|
selectionToolEnabled={true}
|
|
|
|
on:selectTask={clickTask}
|
|
|
|
on:preSelectTask={startHovering}
|
|
|
|
showCenterMarker={true}
|
|
|
|
bind:this={graph}
|
|
|
|
{nodeDraggingEnabled}
|
|
|
|
on:openTask={openTaskDetailEditorButton}
|
|
|
|
{showHiddenEdges}
|
|
|
|
showHidden={true} />
|
|
|
|
|
|
|
|
<div class="container">
|
|
|
|
<div class="topLeftHint">
|
|
|
|
Last clicked: <b>{clicked.join(' | ')}</b><br /><i>Double click na node
|
|
|
|
otevře detail. Po kliknutí na label se zobrazí možnost rotace. Držením
|
|
|
|
pravého tlačítka je možné udělat skupinový výběr.</i>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="right">
|
|
|
|
<div class="toolbox">
|
|
|
|
<h3>Toolbox</h3>
|
|
|
|
<div>
|
|
|
|
<button on:click={saveCurrentState}>Uložit aktuální stav</button>
|
|
|
|
<button on:click={saveLocally}>Stáhnout data</button>
|
|
|
|
</div>
|
|
|
|
<div class="gap" />
|
|
|
|
<div>
|
|
|
|
<button on:click={addTask}>Nový node</button>
|
|
|
|
<button
|
|
|
|
disabled={clicked.length == 0}
|
|
|
|
on:click={() => removeTask(clicked[clicked.length - 1])}>Odstranit {clicked[clicked.length - 1] ?? '???'}</button>
|
|
|
|
</div>
|
|
|
|
<div class="gap" />
|
|
|
|
<div>
|
|
|
|
<button disabled={clicked.length <= 1} on:click={addEdge}>Přidat hranu {clicked[clicked.length - 2] ?? '???'}
|
|
|
|
-> {clicked[clicked.length - 1] ?? '???'}</button>
|
|
|
|
<div class="checkbox">
|
|
|
|
<label>
|
|
|
|
<input type="checkbox" bind:checked={showHiddenEdges} /> Zobrazit skryté
|
|
|
|
hrany
|
|
|
|
</label>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="gap" />
|
|
|
|
<div>
|
|
|
|
<button on:click={runSimulation}>Spustit simulaci</button>
|
|
|
|
<div class="checkbox">
|
|
|
|
<label>
|
|
|
|
<input type="checkbox" bind:checked={nodeDraggingEnabled} /> Povolit
|
|
|
|
přesouvání vrcholů
|
|
|
|
</label>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div>
|
|
|
|
Repulsion force: <input type="number" bind:value={repulsionForce} name="repulsionForceInput" max="1000" min="-10000" />
|
|
|
|
</div>
|
|
|
|
<div class="gap" />
|
|
|
|
{#if clicked.length > 0 && getTask(clicked[clicked.length - 1])?.type == 'label'}
|
|
|
|
<div>
|
|
|
|
Úhel rotace: <input bind:value={angle} type="range" max="360" min="0" on:change={setAngleToTheCurrentLabel} />
|
|
|
|
</div>
|
|
|
|
{/if}
|
|
|
|
<div>
|
|
|
|
<button
|
|
|
|
on:click={loadYear}
|
|
|
|
disabled={!isLoggedIn()}
|
|
|
|
title={isLoggedIn() ? 'Nahraje všechny úlohy z jednoho ročníku, které tu ještě nejsou' : 'Je nutné být přihlášený a na stránce v KSP template.'}>Nahrát
|
|
|
|
celý ročník</button>
|
|
|
|
</div>
|
|
|
|
<div>
|
|
|
|
<button on:click={hideSelection}>Skrýt výběr</button>
|
|
|
|
<button on:click={showSelection}>Zobrazit výběr</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="taskDetails">
|
|
|
|
{#if currentTask != null}
|
|
|
|
<h3>{currentTask.id}</h3>
|
|
|
|
<span>{nonNull(currentTask).comment}</span>
|
|
|
|
<ul>
|
|
|
|
{#each getCategories(tasks, currentTask.id) as cat}
|
|
|
|
<li>{cat}</li>
|
|
|
|
{/each}
|
|
|
|
</ul>
|
|
|
|
<TaskDisplay task={currentTask} />
|
|
|
|
{:else}
|
|
|
|
<h3>Nothing selected...</h3>
|
|
|
|
{/if}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|