mirror of
https://codeberg.org/demostf/frontend.git
synced 2026-06-03 18:24:12 +02:00
upload page
This commit is contained in:
parent
b0b3c0f3ed
commit
a8f0d8e299
8 changed files with 171 additions and 18 deletions
40
script/header.js
Normal file
40
script/header.js
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
DataView.prototype.getString = function(offset, length){
|
||||||
|
let end = typeof length == 'number' ? offset + length : this.byteLength;
|
||||||
|
let text = '';
|
||||||
|
let val = -1;
|
||||||
|
|
||||||
|
while (offset < this.byteLength && offset < end){
|
||||||
|
val = this.getUint8(offset++);
|
||||||
|
if (val === 0) break;
|
||||||
|
text += String.fromCharCode(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function parseHeader(file, cb) {
|
||||||
|
const data = await readFile(file);
|
||||||
|
const view = new DataView(data);
|
||||||
|
return {
|
||||||
|
'type': view.getString(0, 8),
|
||||||
|
'server': view.getString(16, 260),
|
||||||
|
'nick': view.getString(276, 260),
|
||||||
|
'map': view.getString(536, 260),
|
||||||
|
'game': view.getString(796, 260),
|
||||||
|
'duration': view.getFloat32(1056, true),
|
||||||
|
'ticks': view.getUint32(1060, true),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readFile(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = function () {
|
||||||
|
resolve(reader.result)
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
7
script/ready.js
Normal file
7
script/ready.js
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export function ready(cb) {
|
||||||
|
if (document.readyState === "complete") {
|
||||||
|
cb();
|
||||||
|
} else {
|
||||||
|
document.addEventListener("DOMContentLoaded", cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export function test() {
|
|
||||||
console.log("import test");
|
|
||||||
}
|
|
||||||
18
script/time.js
Normal file
18
script/time.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
export function formatDuration(input) {
|
||||||
|
if (!input) {
|
||||||
|
return '0:00';
|
||||||
|
}
|
||||||
|
const hours = Math.floor(input / 3600);
|
||||||
|
const minutes = Math.floor((input - (hours * 3600)) / 60);
|
||||||
|
const seconds = Math.floor(input - (hours * 3600) - (minutes * 60));
|
||||||
|
|
||||||
|
const hourString = (hours < 10) ? "0" + hours : "" + hours;
|
||||||
|
const minuteString = (minutes < 10) ? "0" + minutes : "" + minutes;
|
||||||
|
const secondString = (seconds < 10) ? "0" + seconds : "" + seconds;
|
||||||
|
|
||||||
|
if (hourString !== '00') {
|
||||||
|
return hourString + ':' + minuteString + ':' + secondString;
|
||||||
|
} else {
|
||||||
|
return minuteString + ':' + secondString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,72 @@
|
||||||
import {test} from './test';
|
import {ready} from './ready';
|
||||||
|
import {parseHeader} from './header';
|
||||||
|
import {formatDuration} from './time';
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", test);
|
ready(() => {
|
||||||
if (document.readyState === "complete") {
|
const red_name = document.querySelector(".red input");
|
||||||
test();
|
const blue_name = document.querySelector(".blue input");
|
||||||
|
const file = document.querySelector(`.dropzone input[type="file"]`);
|
||||||
|
const drop_text = document.querySelector(`.dropzone .text`);
|
||||||
|
const button = document.querySelector(`.upload > button`);
|
||||||
|
const map = document.querySelector(`.demo-info .map`);
|
||||||
|
const time = document.querySelector(`.demo-info .time`);
|
||||||
|
const apiBase = document.querySelector(`input[name="api"]`).value;
|
||||||
|
const key = document.querySelector(`.key`).textContent;
|
||||||
|
let selectedFile = null;
|
||||||
|
console.log(key);
|
||||||
|
|
||||||
|
file.addEventListener("change", async (event) => {
|
||||||
|
let file = event.target.files[0];
|
||||||
|
drop_text.textContent = file.name;
|
||||||
|
const header = await parseHeader(file)
|
||||||
|
|
||||||
|
if (header.type === "HL2DEMO" && header.game === "tf") {
|
||||||
|
map.textContent = header.map;
|
||||||
|
time.textContent = formatDuration(header.duration);
|
||||||
|
button.removeAttribute("disabled")
|
||||||
|
selectedFile = file;
|
||||||
|
} else {
|
||||||
|
drop_text.textContent = "Malformed demo or not a TF2 demo";
|
||||||
|
map.textContent = "";
|
||||||
|
time.textContent = "";
|
||||||
|
button.setAttribute("disabled", "disabled");
|
||||||
|
selectedFile = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
button.addEventListener("click", async () => {
|
||||||
|
button.setAttribute("disabled", "disabled");
|
||||||
|
if (!selectedFile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drop_text.textContent = "Uploading...";
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.location.href = await uploadDemo(apiBase, key, red_name.value || 'RED', blue_name.value || 'BLU', selectedFile.name, selectedFile);
|
||||||
|
} catch (e) {
|
||||||
|
drop_text.textContent = `Error ${e.message}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
async function uploadDemo(apiBase, key, red, blue, name, demo) {
|
||||||
|
const data = new FormData();
|
||||||
|
data.append('key', key);
|
||||||
|
data.append('red', red);
|
||||||
|
data.append('blu', blue);
|
||||||
|
data.append('name', name);
|
||||||
|
data.append('demo', demo, demo.name);
|
||||||
|
const response = await fetch(apiBase + "upload", {
|
||||||
|
method: 'POST',
|
||||||
|
body: data
|
||||||
|
});
|
||||||
|
if (response.status >= 400) {
|
||||||
|
throw new Error(await response.text());
|
||||||
|
}
|
||||||
|
const body = await response.text();
|
||||||
|
const matches = body.match(/STV available at: https?:\/\/[^/]+\/(\d+)/);
|
||||||
|
if (matches) {
|
||||||
|
return matches[1];
|
||||||
|
} else {
|
||||||
|
throw new Error(body);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
11
src/main.rs
11
src/main.rs
|
|
@ -221,9 +221,16 @@ async fn logout(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn upload(State(_app): State<Arc<App>>, session: SessionData) -> impl IntoResponse {
|
async fn upload(State(app): State<Arc<App>>, session: SessionData) -> impl IntoResponse {
|
||||||
if let Some(token) = session.token() {
|
if let Some(token) = session.token() {
|
||||||
render(UploadPage { key: token }, session).into_response()
|
render(
|
||||||
|
UploadPage {
|
||||||
|
key: token.as_str(),
|
||||||
|
api: app.api.as_str(),
|
||||||
|
},
|
||||||
|
session,
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
StatusCode::FOUND,
|
StatusCode::FOUND,
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,20 @@ use crate::pages::Page;
|
||||||
use maud::{html, Markup};
|
use maud::{html, Markup};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
pub struct UploadPage {
|
pub struct UploadPage<'a> {
|
||||||
pub key: String,
|
pub key: &'a str,
|
||||||
|
pub api: &'a str,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UploadPage {
|
impl<'a> UploadPage<'a> {
|
||||||
pub fn plugin_section(&self) -> PluginSection {
|
pub fn plugin_section(&self) -> PluginSection<'a> {
|
||||||
PluginSection {
|
PluginSection {
|
||||||
key: Some(self.key.as_str()),
|
key: Some(self.key),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Page for UploadPage {
|
impl Page for UploadPage<'_> {
|
||||||
fn title(&self) -> Cow<'static, str> {
|
fn title(&self) -> Cow<'static, str> {
|
||||||
"Upload - demos.tf".into()
|
"Upload - demos.tf".into()
|
||||||
}
|
}
|
||||||
|
|
@ -39,20 +40,26 @@ impl Page for UploadPage {
|
||||||
noscript {
|
noscript {
|
||||||
"Javascript is required for demo upload."
|
"Javascript is required for demo upload."
|
||||||
}
|
}
|
||||||
"Drop files or click to upload"
|
span.text { "Drop files or click to upload" }
|
||||||
|
input type = "file" {}
|
||||||
}
|
}
|
||||||
|
.demo-info {
|
||||||
|
span.map {}
|
||||||
|
span.time {}
|
||||||
|
}
|
||||||
|
input type = "hidden" name = "api" value = (self.api) {}
|
||||||
button.button.button-primary disabled { "Upload" }
|
button.button.button-primary disabled { "Upload" }
|
||||||
}
|
}
|
||||||
section {
|
section {
|
||||||
.title {
|
.title {
|
||||||
h3 { "API Key" }
|
h3 { "API Key" }
|
||||||
}
|
}
|
||||||
pre { (self.key) }
|
pre.key { (self.key) }
|
||||||
p { "This key is used by the plugin to authenticate you as the uploader and link the uploaded demo to your account." }
|
p { "This key is used by the plugin to authenticate you as the uploader and link the uploaded demo to your account." }
|
||||||
}
|
}
|
||||||
(self.plugin_section())
|
(self.plugin_section())
|
||||||
}
|
}
|
||||||
script src = (script) type = "text/javascript" {}
|
script defer src = (script) type = "text/javascript" {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,15 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
line-height: 300px;
|
line-height: 300px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
& input[type="file"] {
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue