parent
a264dbead9
commit
80017529a7
3 changed files with 101 additions and 28 deletions
|
@ -6,6 +6,9 @@
|
|||
import type { TasksFile, TaskDescriptor } from "./task-loader";
|
||||
import { createNodesAndEdges } from "./graph-types";
|
||||
import { taskForce } from "./task-force";
|
||||
import { grabTaskStates, isLoggedIn } from "./ksp-task-grabber";
|
||||
import type { TaskStatus } from "./ksp-task-grabber"
|
||||
import { json } from "d3";
|
||||
|
||||
export let tasks: TasksFile;
|
||||
export let repulsionForce: number = -1000;
|
||||
|
@ -18,6 +21,7 @@
|
|||
let clientHeight: number;
|
||||
let clientWidth: number;
|
||||
let svgElement: SVGElement;
|
||||
let taskStatuses = new Map<string, TaskStatus>();
|
||||
|
||||
// this prevents svelte from updating nodes and edges
|
||||
// when we update nodes and edges
|
||||
|
@ -87,6 +91,17 @@
|
|||
}
|
||||
|
||||
|
||||
if (isLoggedIn()) {
|
||||
const cachedTaskStatuses = localStorage.getItem("taskStatuses-cache")
|
||||
if (cachedTaskStatuses) {
|
||||
try { taskStatuses = new Map(JSON.parse(cachedTaskStatuses)) } catch(e) { console.warn(e) }
|
||||
}
|
||||
grabTaskStates(tasks.tasks.map(t => t.id)).then(t => {
|
||||
taskStatuses = t
|
||||
localStorage.setItem("taskStatuses-cache", JSON.stringify(Array.from(t.entries())))
|
||||
})
|
||||
}
|
||||
|
||||
// start simulation and center view on create
|
||||
onMount(() => {
|
||||
// set center of the SVG at (0,0)
|
||||
|
@ -137,6 +152,7 @@
|
|||
on:click={nodeClick(task.task)}
|
||||
on:hoveringChange={nodeHover(task.task)}
|
||||
on:positionChange={() => { tasks = tasks; }}
|
||||
status={taskStatuses.get(task.id)}
|
||||
draggingEnabled={nodeDraggingEnabled} />
|
||||
{/each}
|
||||
</g>
|
||||
|
|
|
@ -3,9 +3,11 @@
|
|||
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
import type { TaskId } from "./graph-types";
|
||||
import type { TaskStatus } from "./ksp-task-grabber";
|
||||
|
||||
export let task: TaskId;
|
||||
export let draggingEnabled: boolean = false;
|
||||
export let status: TaskStatus | undefined = undefined
|
||||
|
||||
let hovering: boolean = false;
|
||||
let text_element: SVGTextElement;
|
||||
|
@ -74,9 +76,12 @@
|
|||
ellipse {
|
||||
fill: #69b3a2
|
||||
}
|
||||
.submitted ellipse {
|
||||
fill: red
|
||||
}
|
||||
</style>
|
||||
|
||||
<g on:mouseenter={enter} on:mouseleave={leave} on:click={click} on:mousedown={dragStart} on:mouseup={dragStop} on:mousemove={drag}>
|
||||
<g on:mouseenter={enter} on:mouseleave={leave} on:click={click} on:mousedown={dragStart} on:mouseup={dragStop} on:mousemove={drag} class={status && status.submitted ? "submitted" : ""}>
|
||||
<ellipse rx={ellipse_rx} ry={20} {cx} {cy} />
|
||||
<text
|
||||
bind:this={text_element}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import type { TaskId } from "./graph-types"
|
||||
|
||||
export type TaskAssignmentData = {
|
||||
id: string,
|
||||
name: string,
|
||||
|
@ -13,19 +15,16 @@ type TaskLocation = {
|
|||
startElement: string
|
||||
}
|
||||
|
||||
function fixLink(link: string, currentPath: string): string {
|
||||
const url = new URL(link, currentPath)
|
||||
if (url.host == "ksp.mff.cuni.cz" || url.host == "ksp-test.ks.matfyz.cz") {
|
||||
url.host = location.host
|
||||
}
|
||||
if (url.host == location.host) {
|
||||
return url.pathname
|
||||
} else {
|
||||
return url.href
|
||||
}
|
||||
export type TaskStatus = {
|
||||
id: string
|
||||
name: string
|
||||
submitted: boolean
|
||||
points: number
|
||||
maxPoints: number
|
||||
type: string
|
||||
}
|
||||
|
||||
function fixAllLinks(e: any, currentPath: string) {
|
||||
function fixAllLinks(e: any) {
|
||||
if (typeof e.src == "string") {
|
||||
e.src = e.src
|
||||
}
|
||||
|
@ -34,11 +33,25 @@ function fixAllLinks(e: any, currentPath: string) {
|
|||
}
|
||||
let c = (e as HTMLElement).firstElementChild
|
||||
while (c) {
|
||||
fixAllLinks(c, currentPath)
|
||||
fixAllLinks(c)
|
||||
c = c.nextElementSibling
|
||||
}
|
||||
}
|
||||
|
||||
type ParsedTaskId = {
|
||||
rocnik: string
|
||||
z: boolean
|
||||
serie: string
|
||||
uloha: string
|
||||
}
|
||||
|
||||
function parseTaskId(id: string): ParsedTaskId | null {
|
||||
const m = /^(\d+)-(Z?)(\d)-(\d)$/.exec(id)
|
||||
if (!m) return null
|
||||
const [_, rocnik, z, serie, uloha] = m
|
||||
return { rocnik, z: !!z, serie, uloha }
|
||||
}
|
||||
|
||||
function getLocation(id: string, solution: boolean): TaskLocation | null {
|
||||
const m = /^(\d+)-(Z?)(\d)-(\d)$/.exec(id)
|
||||
if (!m) return null
|
||||
|
@ -58,19 +71,11 @@ function getLocation(id: string, solution: boolean): TaskLocation | null {
|
|||
}
|
||||
}
|
||||
|
||||
function parseTask(startElementId: string, html: string, currentPath: string): TaskAssignmentData {
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(html, "text/html")
|
||||
if (!doc.head.querySelector("base")) {
|
||||
let baseEl = doc.createElement('base');
|
||||
baseEl.setAttribute('href', new URL(currentPath, location.href).href);
|
||||
doc.head.append(baseEl);
|
||||
}
|
||||
|
||||
function parseTask(startElementId: string, doc: HTMLDocument): TaskAssignmentData {
|
||||
const titleElement = doc.getElementById(startElementId)
|
||||
if (!titleElement)
|
||||
throw new Error(`Document does not contain ${startElementId}`)
|
||||
fixAllLinks(titleElement, currentPath)
|
||||
fixAllLinks(titleElement)
|
||||
const elements = []
|
||||
|
||||
let e = titleElement
|
||||
|
@ -99,7 +104,7 @@ function parseTask(startElementId: string, html: string, currentPath: string): T
|
|||
|
||||
let r = ""
|
||||
for (const e of elements) {
|
||||
fixAllLinks(e, currentPath)
|
||||
fixAllLinks(e)
|
||||
r += e.outerHTML + "\n"
|
||||
}
|
||||
return {
|
||||
|
@ -111,13 +116,41 @@ function parseTask(startElementId: string, html: string, currentPath: string): T
|
|||
}
|
||||
}
|
||||
|
||||
async function loadTask({ url, startElement }: TaskLocation) {
|
||||
function parseTaskStatuses(doc: HTMLDocument): TaskStatus[] {
|
||||
const rows = Array.from(doc.querySelectorAll("table.zs-tasklist tr")).slice(1) as HTMLTableRowElement[]
|
||||
return rows.map(r => {
|
||||
const submitted = !r.classList.contains("zs-unsubmitted")
|
||||
const id = r.cells[0].innerText.trim()
|
||||
const type = r.cells[1].innerText.trim()
|
||||
const name = r.cells[2].innerText.trim()
|
||||
const pointsStr = r.cells[4].innerText.trim()
|
||||
const pointsMatch = /(–|\d+) *\/ *(\d+)/.exec(pointsStr)
|
||||
if (!pointsMatch) throw new Error()
|
||||
const points = +pointsMatch[1]
|
||||
const maxPoints = +pointsMatch[2]
|
||||
return { id, name, submitted, type, points, maxPoints }
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchHtml(url: string) {
|
||||
const r = await fetch(url, { headers: { "Accept": "text/html,application/xhtml+xml" } })
|
||||
if (r.status >= 400) {
|
||||
throw Error("Bad request")
|
||||
throw Error(r.statusText)
|
||||
}
|
||||
const rText = await r.text()
|
||||
return parseTask(startElement, rText, url)
|
||||
const html = await r.text()
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(html, "text/html")
|
||||
if (!doc.head.querySelector("base")) {
|
||||
let baseEl = doc.createElement('base');
|
||||
baseEl.setAttribute('href', new URL(url, location.href).href);
|
||||
doc.head.append(baseEl);
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
async function loadTask({ url, startElement }: TaskLocation): Promise<TaskAssignmentData> {
|
||||
const html = await fetchHtml(url)
|
||||
return parseTask(startElement, html)
|
||||
}
|
||||
|
||||
function virtualTask(id: string): TaskAssignmentData {
|
||||
|
@ -130,6 +163,25 @@ function virtualTask(id: string): TaskAssignmentData {
|
|||
}
|
||||
}
|
||||
|
||||
export function isLoggedIn(): boolean {
|
||||
return !!document.querySelector(".auth a[href='/profil/profil.cgi']")
|
||||
}
|
||||
|
||||
export async function grabTaskStates(kspIds: string[]): Promise<Map<string, TaskStatus>> {
|
||||
if (!isLoggedIn()) throw new Error()
|
||||
|
||||
const ids = new Set<string>(kspIds.map(parseTaskId).filter(t => t != null).map(t => t!.rocnik))
|
||||
const results = await Promise.all(Array.from(ids.keys()).map(async (rocnik) => {
|
||||
const html = await fetchHtml(`/cviciste/?year=${rocnik}`)
|
||||
return parseTaskStatuses(html)
|
||||
}))
|
||||
|
||||
return new Map<string, TaskStatus>(
|
||||
([] as TaskStatus[]).concat(...results)
|
||||
.map(r => [r.id, r])
|
||||
)
|
||||
}
|
||||
|
||||
export async function grabAssignment(id: string): Promise<TaskAssignmentData> {
|
||||
const l = getLocation(id, false)
|
||||
if (!l) return virtualTask(id)
|
||||
|
|
Reference in a new issue