fix: preserve upload progress across page navigation
This commit is contained in:
1 parent
ae8e175376
commit
fa82c99a52
5 files changed
+197
-37
No files matched your search
@@ -7,6 +7,7 @@ import { Cover, ErrorNote, Spinner } from "../ui/Bits";
|
||||
import { F95Fetch } from "../ui/F95Fetch";
|
||||
import { Icon } from "../ui/Icon";
|
||||
import { useUi } from "../ui/Ui";
|
||||
import { useUploads } from "../uploads";
|
||||
|
||||
const STATUS: Record<Version["status"], string> = {
|
||||
uploading: "Загружается",
|
||||
@@ -27,6 +28,7 @@ export function GamePage() {
|
||||
const { gameId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const ui = useUi();
|
||||
const { start: startUpload, tasks: uploads } = useUploads();
|
||||
const { me, can } = useAuth();
|
||||
const [game, setGame] = useState<GameDetail | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
@@ -44,6 +46,11 @@ export function GamePage() {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
const uploadedVersions = uploads.filter((task) => task.gameId === Number(gameId) && task.version).map((task) => task.version!.id).join(",");
|
||||
useEffect(() => {
|
||||
if (uploadedVersions) void load();
|
||||
}, [uploadedVersions, load]);
|
||||
|
||||
// poll while something is being converted
|
||||
const busy = game?.versions.some((v) => BUSY.includes(v.status));
|
||||
useEffect(() => {
|
||||
@@ -122,8 +129,7 @@ export function GamePage() {
|
||||
confirmLabel: "Загрузить",
|
||||
});
|
||||
if (name)
|
||||
void run(() => uploadRaw("POST", `/api/games/${game.id}/upload?filename=${encodeURIComponent(file.name)}&name=${encodeURIComponent(name)}`,
|
||||
file, () => {}), "Архив загружен, началась конвертация");
|
||||
startUpload({ file, gameId: game.id, name });
|
||||
};
|
||||
|
||||
const removeVersion = async (v: Version) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, type DragEvent, type FormEvent } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { api, formatSize, uploadRaw, type Game } from "../api";
|
||||
import { formatSize } from "../api";
|
||||
import { UploadProgress, useUploads } from "../uploads";
|
||||
import { ErrorNote, PageHeader } from "../ui/Bits";
|
||||
import { F95Fetch } from "../ui/F95Fetch";
|
||||
import { Icon } from "../ui/Icon";
|
||||
@@ -14,7 +15,7 @@ export function NewGame() {
|
||||
const [tags, setTags] = useState("");
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [over, setOver] = useState(false);
|
||||
const [progress, setProgress] = useState<number | null>(null);
|
||||
const { start } = useUploads();
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const pick = (f: File | null | undefined) => {
|
||||
@@ -32,40 +33,27 @@ export function NewGame() {
|
||||
pick(e.dataTransfer.files[0]);
|
||||
};
|
||||
|
||||
const submit = async (e: FormEvent) => {
|
||||
const submit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!file) return;
|
||||
setError("");
|
||||
setProgress(0);
|
||||
let game: Game | null = null;
|
||||
try {
|
||||
game = await api.post<Game>("/api/games", {
|
||||
title: title.trim() || file.name.replace(/\.(zip|7z|rar|tar.*)$/i, ""),
|
||||
description,
|
||||
tags: tags.split(",").map((t) => t.trim()).filter(Boolean),
|
||||
});
|
||||
await uploadRaw("POST", `/api/games/${game.id}/upload?filename=${encodeURIComponent(file.name)}`,
|
||||
file, setProgress);
|
||||
navigate(`/games/${game.id}`);
|
||||
} catch (err) {
|
||||
// don't leave an empty game card behind when the archive never arrived
|
||||
if (game) await api.del(`/api/games/${game.id}`).catch(() => {});
|
||||
setError((err as Error).message);
|
||||
setProgress(null);
|
||||
}
|
||||
start({ file, title, description, tags: tags.split(",").map((t) => t.trim()).filter(Boolean) });
|
||||
setFile(null);
|
||||
setTitle("");
|
||||
setDescription("");
|
||||
setTags("");
|
||||
};
|
||||
|
||||
const uploading = progress !== null;
|
||||
const pct = Math.round((progress ?? 0) * 100);
|
||||
|
||||
return (
|
||||
<div className="page medium">
|
||||
<PageHeader title="Новая игра"
|
||||
subtitle="Загрузите архив с игрой на Ren'Py. Сервер распакует его, сконвертирует скрипты и ассеты в формат PGVN и удалит исходники." />
|
||||
<UploadProgress />
|
||||
<form className="card form" onSubmit={submit}>
|
||||
<label className={`dropzone ${over ? "over" : ""} ${file ? "has-file" : ""}`}
|
||||
onDragOver={(e) => { e.preventDefault(); setOver(true); }} onDragLeave={() => setOver(false)} onDrop={onDrop}>
|
||||
<input type="file" accept=".zip,.7z,.rar,.tar,.gz,.tgz,.bz2,.xz" disabled={uploading}
|
||||
<input type="file" accept=".zip,.7z,.rar,.tar,.gz,.tgz,.bz2,.xz"
|
||||
key={file ? "selected" : "empty"}
|
||||
onChange={(e) => pick(e.target.files?.[0])} />
|
||||
<Icon name={file ? "file" : "upload"} size={34} className="dz-icon" />
|
||||
{file ? (
|
||||
@@ -103,19 +91,11 @@ export function NewGame() {
|
||||
</label>
|
||||
|
||||
{error && <ErrorNote>{error}</ErrorNote>}
|
||||
{uploading && (
|
||||
<div>
|
||||
<div className="progress"><div style={{ width: `${pct}%` }} /></div>
|
||||
<p className="muted small" style={{ marginTop: "0.4rem" }}>
|
||||
{pct < 100 ? `Загрузка архива… ${pct}%` : "Архив загружен, передаём на конвертацию…"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="actions">
|
||||
<button className="btn btn-primary btn-lg" disabled={!file || uploading}>
|
||||
<Icon name="upload" size={20} />{uploading ? `Загрузка ${pct}%` : "Загрузить"}
|
||||
<button className="btn btn-primary btn-lg" disabled={!file}>
|
||||
<Icon name="upload" size={20} />Загрузить
|
||||
</button>
|
||||
{!uploading && <button type="button" className="btn btn-ghost" onClick={() => navigate(-1)}>Отмена</button>}
|
||||
<button type="button" className="btn btn-ghost" onClick={() => navigate(-1)}>Отмена</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user