UI: unified design system, light/dark themes, redesigned pages and player UI
Tokens and components in styles.css, shared Ui (dialogs, toasts), icons, bottom tab bar on mobile, keyboard/gamepad navigation, library shelf with filters and conversion badges, hero game page, restyled player screens. Games list now returns per-game version counts. Adds docs/UI_DESIGN.md. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
51a7e77e56
commit
9ee3e1ff6e
27 files changed
+2399
-508
No files matched your search
@@ -57,6 +57,9 @@ class Game(Base):
|
||||
tags: Mapped[list[str]] = mapped_column(ARRAY(String), default=list)
|
||||
engine: Mapped[str] = mapped_column(String(32), default="renpy")
|
||||
cover_hash: Mapped[str | None] = mapped_column(String(80))
|
||||
# the version "Play" opens; translations are published by merging into it
|
||||
main_version_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("game_versions.id", ondelete="SET NULL", use_alter=True))
|
||||
created_by_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
@@ -74,6 +77,9 @@ class GameVersion(Base):
|
||||
description: Mapped[str] = mapped_column(Text, default="")
|
||||
# uploading -> queued -> converting -> ready | failed; importing = app.import_dir run
|
||||
status: Mapped[str] = mapped_column(String(16), default="uploading", index=True)
|
||||
# draft: visible to the owner and reviewers only
|
||||
visibility: Mapped[str] = mapped_column(String(16), default="published")
|
||||
kind: Mapped[str] = mapped_column(String(16), default="original") # original|translation|mod
|
||||
progress: Mapped[str] = mapped_column(String(255), default="")
|
||||
upload_name: Mapped[str | None] = mapped_column(String(255))
|
||||
manifest: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
||||
@@ -84,6 +90,19 @@ class GameVersion(Base):
|
||||
owner: Mapped[User | None] = relationship(lazy="joined")
|
||||
|
||||
|
||||
class VersionHistory(Base):
|
||||
"""Previous manifests of a version that was changed in place (e.g. a merged translation)."""
|
||||
__tablename__ = "version_history"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
version_id: Mapped[int] = mapped_column(ForeignKey("game_versions.id", ondelete="CASCADE"),
|
||||
index=True)
|
||||
user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"))
|
||||
note: Mapped[str] = mapped_column(String(255), default="")
|
||||
manifest: Mapped[dict[str, Any]] = mapped_column(JSONB)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
|
||||
class Save(Base):
|
||||
__tablename__ = "saves"
|
||||
__table_args__ = (UniqueConstraint("user_id", "version_id", "slot"),)
|
||||
|
||||
@@ -7,6 +7,8 @@ PERMISSIONS: dict[str, str] = {
|
||||
"games.delete": "Удаление игр и любых версий",
|
||||
"versions.fork": "Создание своих версий (форков)",
|
||||
"versions.manage_any": "Управление чужими версиями",
|
||||
"translations.edit": "Перевод игр в своих версиях",
|
||||
"versions.publish": "Публикация версий и переводов, выбор главной версии",
|
||||
"users.manage": "Управление пользователями",
|
||||
"roles.manage": "Управление ролями",
|
||||
}
|
||||
@@ -15,6 +17,7 @@ ALL = sorted(PERMISSIONS)
|
||||
|
||||
SYSTEM_ROLES: dict[str, list[str]] = {
|
||||
"admin": ALL,
|
||||
"moderator": ["games.play", "games.upload", "games.edit", "versions.fork"],
|
||||
"moderator": ["games.play", "games.upload", "games.edit", "versions.fork",
|
||||
"translations.edit", "versions.publish"],
|
||||
"player": ["games.play"],
|
||||
}
|
||||
@@ -3,7 +3,7 @@ from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
from sqlalchemy import exists, or_, select, true
|
||||
from sqlalchemy import exists, func, or_, select, true
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
|
||||
from app.blobs import put_bytes
|
||||
@@ -40,7 +40,7 @@ def can_edit_game(user: User, game: Game) -> bool:
|
||||
|
||||
|
||||
@router.get("", response_model=list[GameOut])
|
||||
async def list_games(user: Player, db: DB, q: str = "", tag: str = "") -> list[Game]:
|
||||
async def list_games(user: Player, db: DB, q: str = "", tag: str = "") -> list[dict]:
|
||||
stmt = select(Game).order_by(Game.title)
|
||||
if q.strip():
|
||||
pattern = f"%{q.strip()}%"
|
||||
@@ -57,7 +57,27 @@ async def list_games(user: Player, db: DB, q: str = "", tag: str = "") -> list[G
|
||||
exists().where(GameVersion.game_id == Game.id, visible_versions_filter(user))
|
||||
| (Game.created_by_id == user.id)
|
||||
)
|
||||
return list((await db.scalars(stmt.limit(500))).all())
|
||||
games = list((await db.scalars(stmt.limit(500))).all())
|
||||
counts: dict[int, dict[str, int]] = {}
|
||||
if games:
|
||||
rows = await db.execute(
|
||||
select(GameVersion.game_id, GameVersion.status, func.count())
|
||||
.where(GameVersion.game_id.in_([g.id for g in games]), visible_versions_filter(user))
|
||||
.group_by(GameVersion.game_id, GameVersion.status)
|
||||
)
|
||||
for game_id, st, n in rows:
|
||||
key = "ready" if st == "ready" else "failed" if st == "failed" else "pending"
|
||||
counts.setdefault(game_id, {}).setdefault(key, 0)
|
||||
counts[game_id][key] += n
|
||||
return [
|
||||
{
|
||||
**GameOut.model_validate(g).model_dump(),
|
||||
"ready_versions": counts.get(g.id, {}).get("ready", 0),
|
||||
"pending_versions": counts.get(g.id, {}).get("pending", 0),
|
||||
"failed_versions": counts.get(g.id, {}).get("failed", 0),
|
||||
}
|
||||
for g in games
|
||||
]
|
||||
|
||||
|
||||
@router.post("", response_model=GameOut, status_code=201)
|
||||
|
||||
@@ -90,6 +90,10 @@ class GameOut(ORM):
|
||||
engine: str
|
||||
cover_hash: str | None
|
||||
created_at: datetime
|
||||
# version counts visible to the requesting user (filled by the list endpoint)
|
||||
ready_versions: int = 0
|
||||
pending_versions: int = 0
|
||||
failed_versions: int = 0
|
||||
|
||||
|
||||
class GameDetailOut(GameOut):
|
||||
|
||||
Reference in new issue
Block a user