import React, { useEffect, useState, useRef } from "react"; import { Monitor, Smartphone, Globe, Download, Check, LoaderCircle, Square, } from "lucide-react"; import type { Project } from "../engine/schema.ts"; import { gameArchive, download } from "../engine/archive.ts"; import { buildKit } from "../engine/build-kit.ts"; import { normalizeOptions } from "../native/options.mjs"; const labels: Record = { web: "Веб", linux: "Linux", windows: "Windows", android: "Android", }; const states: Record = { queued: "В очереди", building: "Собирается", succeeded: "Готово", failed: "Ошибка", cancelled: "Отменено", }; async function api(url: string, data?: unknown) { const r = await fetch(url, { method: data ? "POST" : "GET", headers: data ? { "content-type": "application/json" } : undefined, body: data ? JSON.stringify(data) : undefined, }); const d: any = await r.json(); if (!r.ok) throw Error(d.error || "Ошибка сборки"); return d; } export function BuildPanel({ project, connected, }: { project: Project; connected: boolean; }) { const [target, setTarget] = useState(connected ? "linux" : "web"), [options, setOptions] = useState(() => { try { return normalizeOptions( JSON.parse( localStorage.getItem("forma-build-" + project.id) || "null", ) || { name: project.name }, ); } catch { return { ...normalizeOptions(), name: project.name }; } }); const [cap, setCap] = useState(null), [jobs, setJobs] = useState([]), [selected, setSelected] = useState(null), [error, setError] = useState(""), [working, setWorking] = useState(false); useEffect(() => { try { localStorage.setItem( "forma-build-" + project.id, JSON.stringify({ ...options, target: target === "web" ? "linux" : target, }), ); } catch {} }, [project.id, options, target]); const [notice, setNotice] = useState(""); const alive = useRef(true); useEffect(() => { alive.current = true; return () => { alive.current = false; }; }, []); const option = (key: string, value: any) => setOptions((o) => ({ ...o, [key]: value })); useEffect(() => { if (!connected) return; let cancelled = false; let timer: ReturnType; const poll = async () => { try { const d = await api("/api/builds"); if (!cancelled) setJobs(d.jobs); } catch (e) { if (!cancelled) setError(String(e)); } if (!cancelled) timer = setTimeout(poll, 1600); }; void api("/api/builds/capabilities") .then((d) => { if (!cancelled) setCap(d); }) .catch((e) => { if (!cancelled) setError(String(e)); }); void poll(); return () => { cancelled = true; clearTimeout(timer); }; }, [connected]); const current = jobs.find((j) => j.id === selected) || jobs[0]; const ready = target === "web" || Boolean(cap?.targets?.[target]?.ready); const start = async (kit = false) => { setError(""); setNotice(""); setWorking(true); try { if (target === "web") { download( await gameArchive(project), project.name + "-web.zip", "application/zip", ); if (alive.current) setNotice( "Веб-сборка скачана. Распакуйте ZIP и разместите на HTTP-хостинге.", ); } else { const o = normalizeOptions({ ...options, target }); if (kit) { download( await buildKit(project, o), o.appId + "-" + target + "-build-kit.zip", "application/zip", ); if (alive.current) setNotice( "Комплект скачан. Команды сборки находятся в README.txt.", ); } else { const j = await api("/api/builds", { options: o, expectedRevision: project.revision, }); if (alive.current) { setJobs((js) => [j, ...js]); setSelected(j.id); } } } } catch (e) { if (alive.current) setError(String(e)); } finally { if (alive.current) setWorking(false); } }; return (
{target !== "web" && ( <>
{target === "android" ? ( <> ) : ( <> )}
{!connected ? "Для создания приложения запустите локальную Forma на компьютере. Здесь можно скачать комплект с игрой и командами сборки." : !cap ? "Проверка инструментов сборки…" : ready ? "Сборка выполняется на этом компьютере. Первой сборке нужен интернет для загрузки инструментов." : "Инструменты ещё не установлены: " + cap.targets[target].missing.join("; ")}
{target === "android" && (

{options.mode === "debug" ? "Тестовый APK подписывается автоматически и подходит для установки на телефон." : "Релизный APK требует вашего ключа подписи. Задайте FORMA_KEYSTORE, FORMA_KEYSTORE_PASSWORD, FORMA_KEY_ALIAS и FORMA_KEY_PASSWORD в окружении локального сервера."}

)} {target === "windows" && (

EXE собирается без цифровой подписи издателя.

)} )}
{target !== "web" && ( )} Ревизия проекта {project.revision}
{error && (

{error}

)} {notice && (

{notice}

)} {connected && jobs.length > 0 && (
Сборки проекта
{current && ( <>
{current.status === "succeeded" ? ( ) : ["queued", "building"].includes(current.status) ? ( ) : null} {states[current.status]} ·{" "} {new Date(current.createdAt).toLocaleTimeString()} · r {current.revision} {["queued", "building"].includes(current.status) && ( )}
                {current.logs.join("\n")}
              
{current.error &&

{current.error}

} {current.artifacts?.map((f: any) => ( ))} )}
)}
); }