Standa Lukeš
4 years ago
4 changed files with 317 additions and 6 deletions
@ -0,0 +1,205 @@ |
|||
<script lang="ts"> |
|||
import { isLoggedIn, parseTaskId } from "./ksp-task-grabber"; |
|||
import type { TaskStatus } from "./ksp-task-grabber"; |
|||
import type { TaskSubmitStatus, SubtaskSubmitStatus } from './ksp-submit-api' |
|||
import * as api from './ksp-submit-api' |
|||
import { taskStatuses } from './task-status-cache' |
|||
|
|||
export let id: string; |
|||
const taskFromCache: TaskStatus | undefined = $taskStatuses.get(id) |
|||
let task: TaskSubmitStatus |
|||
let subtaskId: string | null | undefined = null |
|||
let uploadSubtaskId: string | null | undefined = null |
|||
let expiresInSec: number = 0 |
|||
let tick = 0 |
|||
let validSubmitSubtasks: SubtaskSubmitStatus[] = [] |
|||
let downloading = false |
|||
let generating = false |
|||
|
|||
$: { |
|||
if (task && task.id == "cviciste/" + id) { |
|||
break $ |
|||
} |
|||
|
|||
task = { |
|||
// fill with guesses to prevent flashing |
|||
id, |
|||
name: "", |
|||
points: taskFromCache?.points ?? 0, |
|||
max_points: taskFromCache?.maxPoints ?? 1, |
|||
subtasks: [ |
|||
{ id: "1", input_generated: false, max_points: 1, points: 0 } |
|||
] |
|||
} |
|||
subtaskId = "1" |
|||
|
|||
api.taskStatus(id) |
|||
.then(t => { |
|||
task = t |
|||
}) |
|||
} |
|||
|
|||
$: { |
|||
if (!task.subtasks.find(t => t.id == subtaskId)) |
|||
subtaskId = task.subtasks.find(t => t.points < t.max_points - 0.001)?.id |
|||
} |
|||
|
|||
$: { |
|||
tick |
|||
expiresInSec = subtaskId ? calcExpires(subtaskId) : 0 |
|||
} |
|||
window.setInterval(() => { tick++ }, 1000) |
|||
|
|||
$: { |
|||
tick |
|||
validSubmitSubtasks = task.subtasks.filter(t => calcExpires(t.id) > 0) |
|||
} |
|||
$: { |
|||
if (!validSubmitSubtasks.map(t => t.id).includes(uploadSubtaskId!)) { |
|||
uploadSubtaskId = validSubmitSubtasks[0]?.id |
|||
} |
|||
} |
|||
|
|||
function calcExpires(subtask: string): number { |
|||
const st = task.subtasks.find(t => t.id == subtask)! |
|||
if (!st.input_generated) { |
|||
return 0 |
|||
} |
|||
const validUntil = new Date(st.input_valid_until!).valueOf() |
|||
const now = Date.now() |
|||
if (validUntil < now) { |
|||
return 0 |
|||
} else { |
|||
return (validUntil - now) / 1000 |
|||
} |
|||
} |
|||
|
|||
function nameSubtask(id: string) { |
|||
const map: any = { |
|||
"1": "první", |
|||
"2": "druhý", |
|||
"3": "třetí", |
|||
"4": "čtvrtý", |
|||
"5": "pátý", |
|||
"6": "šestý", |
|||
"7": "sedmý", |
|||
"8": "osmý", |
|||
"9": "devátý", |
|||
"10": "desátý" |
|||
} |
|||
return map[id] ?? id |
|||
} |
|||
|
|||
function magicTrickSaveBlob(blob: Blob, fileName: string) { |
|||
const url = URL.createObjectURL(blob) |
|||
magicTrickSaveFile(url, fileName) |
|||
window.URL.revokeObjectURL(url); |
|||
} |
|||
function magicTrickSaveFile(url: string, fileName: string) { |
|||
var a = document.createElement("a"); |
|||
document.body.appendChild(a); |
|||
a.style.display = "none"; |
|||
a.href = url; |
|||
a.download = fileName; |
|||
a.click(); |
|||
a.remove() |
|||
} |
|||
|
|||
async function download() { |
|||
// copy to prevent races |
|||
const [id_, subtaskId_] = [id, subtaskId] |
|||
if (!subtaskId_) { return } |
|||
if (expiresInSec < 20) { |
|||
const x = await api.generateInput(id_, subtaskId_) |
|||
if (id_ != id) { return } |
|||
const subtasks = [...task.subtasks] |
|||
subtasks[subtasks.findIndex(s => s.id == x.id)] = x |
|||
task = { ...task, subtasks } |
|||
} |
|||
// It's probably better to download the input using the "old" method |
|||
// TODO: specify that as an API |
|||
|
|||
const parsedId = parseTaskId(id_) |
|||
magicTrickSaveFile(`/cviciste/?in=1:sub=${subtaskId}:task=${id_}:year=${parsedId!.rocnik}`, subtaskId_ + ".in.txt") |
|||
|
|||
// const blob = await api.getInput(id_, subtaskId_) |
|||
// magicTrickSaveBlob(blob, subtaskId_ + ".in.txt") |
|||
} |
|||
|
|||
async function upload(file: File) { |
|||
const x = await api.submit(id, uploadSubtaskId!, file) |
|||
alert(x.verdict) |
|||
} |
|||
|
|||
function fileChange(ev: Event) { |
|||
const file = (ev.target as HTMLInputElement).files![0] |
|||
upload(file) |
|||
} |
|||
|
|||
function drop(ev: DragEvent) { |
|||
ev.preventDefault() |
|||
const files = ev.dataTransfer?.files ?? [] |
|||
if (files.length > 1) { |
|||
alert("Drag & Drop funguje, ale musíš jenom jeden soubor") |
|||
} |
|||
if (files.length > 0) { |
|||
upload(files[0]) |
|||
} |
|||
} |
|||
function dragOver(ev: DragEvent) { |
|||
ev.preventDefault() |
|||
} |
|||
</script> |
|||
|
|||
<style> |
|||
|
|||
</style> |
|||
|
|||
<svelte:body on:drop={drop} on:dragover={dragOver} /> |
|||
|
|||
<div class="odevzdavatko"> |
|||
<div class="download"> |
|||
<button class="download" |
|||
on:click={download} |
|||
disabled={subtaskId == null || downloading || generating}> |
|||
{#if generating} |
|||
Generuji... |
|||
{:else if downloading} |
|||
Stahuji... |
|||
{:else if expiresInSec > 20} |
|||
Stáhnout |
|||
{:else} |
|||
Vygenerovat a stáhnout |
|||
{/if} |
|||
</button> |
|||
<select bind:value={subtaskId}> |
|||
<option value={null}></option> |
|||
{#each task.subtasks as subtask} |
|||
<option value={subtask.id}> |
|||
{nameSubtask(subtask.id)} |
|||
{#if subtask.points > subtask.max_points - 0.0001} |
|||
<span title="Splněno">✔️</span> |
|||
{/if} |
|||
</option> |
|||
{/each} |
|||
</select> |
|||
vstup. |
|||
|
|||
</div> |
|||
|
|||
{#if validSubmitSubtasks.length > 0} |
|||
<div class="upload"> |
|||
Odevzdat |
|||
<select value={uploadSubtaskId}> |
|||
{#each validSubmitSubtasks as subtask} |
|||
<option value={subtask.id}> |
|||
{nameSubtask(subtask.id)} |
|||
</option> |
|||
{/each} |
|||
</select> |
|||
vstup: |
|||
|
|||
<input type="file" on:change={fileChange}> (nebo přetáhni soubor na stránku) |
|||
</div> |
|||
{/if} |
|||
</div> |
@ -0,0 +1,87 @@ |
|||
import { fetchHtml } from "./ksp-task-grabber"; |
|||
|
|||
let apitoken : string | null = null |
|||
|
|||
async function getToken(): Promise<string | undefined> { |
|||
if (apitoken != null) { |
|||
return apitoken |
|||
} |
|||
let doc = await fetchHtml("/auth/apitoken.cgi?show=1") |
|||
const token = Array.from(doc.querySelectorAll("#content p")).map(x => /Aktuální token: (.*)/.exec(x.innerHTML.trim())).filter(x => x != null).map(x => x![1])[0] |
|||
if (token) { |
|||
return apitoken = token; |
|||
} |
|||
const form = doc.getElementById("apitoken") as HTMLFormElement |
|||
const op = form.elements.namedItem("op")!.value |
|||
const submit = form.elements.namedItem("submit")!.value |
|||
const csrfToken = form.elements.namedItem("_token")!.value |
|||
const body = `op=${encodeURIComponent(op)}&submit=${encodeURIComponent(submit)}&_token=${encodeURIComponent(csrfToken)}` |
|||
console.log(`Creating new API token`) |
|||
await fetch("/auth/apitoken.cgi", { method: "POST", body, headers: [["Content-Type", "application/x-www-form-urlencoded"]], redirect: "manual" }) |
|||
return await getToken() |
|||
} |
|||
|
|||
|
|||
async function request(url: string, options: RequestInit = {}): Promise<Response> { |
|||
const token = await getToken() |
|||
const headers = new Headers(options.headers) |
|||
headers.append("Authorization", "Bearer " + token) |
|||
const opts: RequestInit = { ...options, headers } |
|||
const r = await fetch(url, opts) |
|||
if (r.status >= 400) { |
|||
throw await r.json() |
|||
} |
|||
return r |
|||
} |
|||
|
|||
async function requestJson<T>(url: string, options: RequestInit = {}): Promise<T> { |
|||
const r = await request(url, options) |
|||
return await r.json() |
|||
} |
|||
|
|||
export function listTasks(): Promise<string[]> { |
|||
return requestJson("/api/tasks/list?set=cviciste") |
|||
} |
|||
|
|||
export type SubtaskSubmitStatus = { |
|||
id: string |
|||
points: number |
|||
max_points: number |
|||
input_generated: boolean |
|||
input_valid_until?: string |
|||
submitted_on?: string |
|||
verdict?: string |
|||
} |
|||
|
|||
export type TaskSubmitStatus = { |
|||
id: string |
|||
name: string |
|||
points: number |
|||
max_points: number |
|||
subtasks: SubtaskSubmitStatus[] |
|||
} |
|||
|
|||
export function taskStatus(id: string): Promise<TaskSubmitStatus> { |
|||
return requestJson(`/api/tasks/status?task=${encodeURIComponent("cviciste/" + id)}`) |
|||
} |
|||
|
|||
export function generateInput(id: string, subtask: string): Promise<SubtaskSubmitStatus> { |
|||
return requestJson(`/api/tasks/generate?task=${encodeURIComponent("cviciste/" + id)}&subtask=${encodeURIComponent(subtask)}`, { method: "POST" }) |
|||
} |
|||
|
|||
export async function getInput(id: string, subtask: string): Promise<Blob> { |
|||
const r = await request(`/api/tasks/input?task=${encodeURIComponent("cviciste/" + id)}&subtask=${encodeURIComponent(subtask)}`) |
|||
return await r.blob() |
|||
} |
|||
|
|||
export async function submit(id: string, subtask: string, uploadedData: string | Blob): Promise<SubtaskSubmitStatus> { |
|||
return requestJson( |
|||
`/api/tasks/submit?task=${encodeURIComponent("cviciste/" + id)}&subtask=${encodeURIComponent(subtask)}`, |
|||
{ |
|||
method: "POST", |
|||
body: uploadedData, |
|||
headers: [ |
|||
["Content-Type", "text/plain"] |
|||
] |
|||
}) |
|||
} |
Reference in new issue