< 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 = false;
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);
}
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]
}
< / 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 } />
< 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 >
< 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 >