Improve Android app UI and controller flows

This commit is contained in:
2026-10-09 16:43:58 +08:00
parent c0e26b97fe
commit 608229a497
90 changed files with 8103 additions and 1133 deletions
@@ -27,7 +27,7 @@ SCENE_WIDTH = 64
SCENE_HEIGHT = 64
RGB_BYTE_LENGTH = SCENE_WIDTH * SCENE_HEIGHT * 3
NAME_LIMIT = 80
TEMPLATE_SCHEMA_VERSION = 1
TEMPLATE_SCHEMA_VERSION = 2
TEMPLATE_RECORD_FIELDS = frozenset({
"id",
"name",
@@ -79,7 +79,7 @@ def _migrate_template_v0_to_v1(document: dict[str, Any]) -> dict[str, Any]:
return dict(document)
TEMPLATE_MIGRATIONS = {0: _migrate_template_v0_to_v1}
TEMPLATE_MIGRATIONS = {0: _migrate_template_v0_to_v1, 1: lambda document: dict(document)}
def _is_record(value: Any) -> bool:
@@ -102,7 +102,7 @@ def normalize_template_id(value: Any) -> str:
raise TemplateNotFoundError("template not found") from exc
def validate_scene(value: Any, *, strict_schema: bool = False) -> dict[str, Any]:
def _validate_scene_v1(value: Any, *, strict_schema: bool = False) -> dict[str, Any]:
if not _is_record(value):
raise TemplateValidationError("scene must be an object")
if strict_schema:
@@ -190,6 +190,25 @@ def validate_scene(value: Any, *, strict_schema: bool = False) -> dict[str, Any]
}
def validate_scene(value: Any, *, strict_schema: bool = False) -> dict[str, Any]:
if strict_schema and isinstance(value, dict):
for field in ("version", "width", "height"):
if field in value and type(value[field]) is not int:
raise TemplateValidationError(f"scene {field} must be an integer")
if isinstance(value, dict) and value.get("version") == 2:
from app.scene_layers import validate_v2
try:
return validate_v2(value, _validate_scene_v1)
except (ValueError, TypeError, KeyError) as exc:
raise TemplateValidationError(str(exc)) from exc
return _validate_scene_v1(value, strict_schema=strict_schema)
def prepare_scene(value: Any) -> dict[str, Any]:
from app.scene_layers import upgrade_scene
return upgrade_scene(validate_scene(value, strict_schema=True))
def scene_digest(scene: dict[str, Any]) -> str:
canonical = json.dumps(scene, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:16]
@@ -241,6 +260,9 @@ def render_scene_image(
font_resolver: FontResolver | None = None,
) -> Image.Image:
checked = validate_scene(scene)
if checked["version"] == 2:
from app.scene_layers import render_v2
return render_v2(checked, font_resolver)
pixels = base64.b64decode(checked["pixelRgb"], validate=True)
image = Image.frombytes("RGB", (SCENE_WIDTH, SCENE_HEIGHT), pixels).convert("RGBA")
for element in checked["elements"]:
@@ -382,7 +404,7 @@ class TemplateStore:
name = normalize_name(document.get("name"))
scene = validate_scene(
document.get("scene"),
strict_schema=source_version == TEMPLATE_SCHEMA_VERSION,
strict_schema=True,
)
digest = scene_digest(scene)
if (
@@ -414,6 +436,12 @@ class TemplateStore:
raise TemplateValidationError(
f"template record does not match schema version {TEMPLATE_SCHEMA_VERSION}"
)
if source_version != TEMPLATE_SCHEMA_VERSION:
record["scene"] = prepare_scene(record["scene"])
record["digest"] = scene_digest(record["scene"])
record["thumbnail"] = f"{template_id}-{record['digest']}.png"
elif record["scene"]["version"] != 2:
raise TemplateValidationError("current template requires scene v2")
return record, source_version != TEMPLATE_SCHEMA_VERSION
def _load_record_path(self, path: Path) -> tuple[dict[str, Any], bool]:
@@ -519,7 +547,7 @@ class TemplateStore:
def create(self, name: Any, scene: Any) -> dict[str, Any]:
checked_name = normalize_name(name)
checked_scene = validate_scene(scene)
checked_scene = prepare_scene(scene)
with self._lock:
self._ensure_unique_name(checked_name)
template_id = str(uuid4())
@@ -550,7 +578,7 @@ class TemplateStore:
def update_scene(self, template_id: Any, scene: Any, expected_revision: str) -> dict[str, Any]:
normalized_id = normalize_template_id(template_id)
checked_scene = validate_scene(scene)
checked_scene = prepare_scene(scene)
with self._lock:
current = self._read_record_path(self._record_path(normalized_id))
if current is None:
@@ -574,6 +602,18 @@ class TemplateStore:
self._reconcile()
return {**self._metadata(updated), "scene": updated["scene"]}
def append_layer(self, template_id: Any, layer: Any, expected_revision: str) -> dict[str, Any]:
from app.scene_layers import copy_layer, validate_layer
try:
checked = validate_layer(layer, _validate_scene_v1)
except (ValueError, TypeError, KeyError) as exc:
raise TemplateValidationError(str(exc)) from exc
with self._lock:
current = self.get(template_id)
scene = prepare_scene(current["scene"])
scene["layers"].append(copy_layer(checked, scene["layers"]))
return self.update_scene(template_id, scene, expected_revision)
def rename(self, template_id: Any, name: Any, expected_revision: str) -> dict[str, Any]:
normalized_id = normalize_template_id(template_id)
checked_name = normalize_name(name)