Improve Android app UI and controller flows
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user