Initial MVP: local visual novel library with Ren'Py → PGVN converter

- FastAPI backend: admin bootstrap with forced credential change, admin-created
  users with one-time generated passwords, roles as permission sets
- Games and versions with content-addressed blob storage; forks copy only the
  manifest, so mods share assets with the original
- Worker: archive/RPA extraction, unrpyc for .rpyc, own .rpy parser, Python
  subset translated via ast, media converted to WebP/Opus/WebM
- React panel (library search, game page, upload, users, roles) and a
  framework-free web player with saves, history, skip/auto, transitions
- docker compose: postgres, backend, worker, nginx on a single HTTP port

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Opus 5.5 committed 2026-10-25 02:41:32 +03:00
commit 364b312064
72 files changed
+9707

No files matched your search

+60
View File
@@ -0,0 +1,60 @@
import { Navigate, NavLink, Route, Routes } from "react-router-dom";
import { useAuth } from "./auth";
import { ChangeCredentials } from "./pages/ChangeCredentials";
import { GamePage } from "./pages/GamePage";
import { Library } from "./pages/Library";
import { Login } from "./pages/Login";
import { NewGame } from "./pages/NewGame";
import { Play } from "./pages/Play";
import { Roles } from "./pages/Roles";
import { Users } from "./pages/Users";
function RequirePerm({ perm, children }: { perm: string; children: JSX.Element }) {
const { can } = useAuth();
return can(perm) ? children : <div className="page"><p className="muted">Нет доступа к этой странице.</p></div>;
}
export function App() {
const { me, loading, logout, can } = useAuth();
if (loading) return <div className="center muted">Загрузка…</div>;
if (!me) return <Login />;
if (me.must_change_password || me.must_change_username) return <ChangeCredentials forced />;
return (
<Routes>
<Route path="/play/:versionId" element={<RequirePerm perm="games.play"><Play /></RequirePerm>} />
<Route
path="*"
element={
<div className="layout">
<header className="topbar">
<NavLink to="/" className="brand">PGames</NavLink>
<nav>
<NavLink to="/" end>Библиотека</NavLink>
{can("games.upload") && <NavLink to="/games/new">Загрузить игру</NavLink>}
{can("users.manage") && <NavLink to="/admin/users">Пользователи</NavLink>}
{can("roles.manage") && <NavLink to="/admin/roles">Роли</NavLink>}
</nav>
<div className="user">
<NavLink to="/account">{me.username}</NavLink>
<span className="muted"> · {me.role.name}</span>
<button className="link" onClick={() => void logout()}>Выйти</button>
</div>
</header>
<main>
<Routes>
<Route path="/" element={<RequirePerm perm="games.play"><Library /></RequirePerm>} />
<Route path="/games/new" element={<RequirePerm perm="games.upload"><NewGame /></RequirePerm>} />
<Route path="/games/:gameId" element={<RequirePerm perm="games.play"><GamePage /></RequirePerm>} />
<Route path="/admin/users" element={<RequirePerm perm="users.manage"><Users /></RequirePerm>} />
<Route path="/admin/roles" element={<RequirePerm perm="roles.manage"><Roles /></RequirePerm>} />
<Route path="/account" element={<ChangeCredentials />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</main>
</div>
}
/>
</Routes>
);
}