fix: preserve upload progress across page navigation

This commit is contained in:
mrleo1nid committed 2026-10-25 04:13:11 +03:00
1 parent ae8e175376
commit fa82c99a52
5 files changed
+197 -37

No files matched your search

+8 -2
View File
@@ -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) => {
+15 -35
View File
@@ -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>