Source code for annie.pages.utils

"""Shared UI helpers used by multiple page modules (UI)."""

from __future__ import annotations

import asyncio
import contextlib
from typing import TYPE_CHECKING

from nicegui import ui

from annie.app_state import state
from annie.core.config import settings
from annie.pages.lazy import schedule

if TYPE_CHECKING:
    from collections.abc import Callable

    from nicegui import Client


DEBOUNCE = "debounce=300"
"""Quasar prop for typed filter inputs: apply the value 300 ms after the last keystroke."""


[docs] def render_embed_ttl() -> float: """Idle-revert delay for an embedded *rendered* clip. Capped at the temp-dir sweep TTL so a rendered clip's ``ui.video`` always reverts to its placeholder no later than the periodic sweep deletes the underlying file — the element never points at a clip that has been reclaimed from disk. """ return min(state.ui.embed_ttl_seconds, settings.temp_ttl_seconds)
[docs] def notify_detached(client: Client, message: str, *, color: str) -> None: """Toast from a handler that tore down the slot it was invoked in. ``ui.notify`` resolves its client through the slot the handler runs in, which NiceGUI takes from the sender's **parent** element. A handler that deletes its own row — or refreshes the container holding its button — drops the last reference to that parent, so a later ``ui.notify`` dies with "the parent element this slot belongs to has been deleted". Re-entering ``client`` lands in :attr:`nicegui.Client.content`, which outlives any refresh, so the toast survives the teardown that caused it. Args: client: The client captured *before* the teardown (``context.client``). message: The toast text. color: The toast colour. """ with client: ui.notify(message, color=color)
[docs] def unembed_after_idle( box: ui.element, restore: Callable[[], None], *, ttl: float | None = None ) -> None: """Swap an embedded clip back to its placeholder once it has sat idle. An embedded ``ui.video`` keeps its clip buffered in the browser tab (and its element alive server-side) for as long as the row is on the page. A reviewer who plays a dozen clips while scrolling never gets that memory back. So each embed schedules its own reversal: after ``ttl`` seconds, ``restore`` rebuilds the cheap placeholder and the clip is dropped. The wait runs via :func:`annie.pages.lazy.schedule`, so a row refreshed away mid-wait has no parent slot to be deleted; ``restore`` is skipped when the box no longer exists. Args: box: The container holding the embedded clip. restore: Rebuilds the placeholder content inside ``box``. Must clear it. ttl: Idle seconds before reverting. Defaults to :attr:`annie.app_state.UiSettings.embed_ttl_seconds`; rendered clips pass :func:`render_embed_ttl` so they never outlive the temp-dir sweep. """ delay = ttl if ttl is not None else state.ui.embed_ttl_seconds async def _revert() -> None: await asyncio.sleep(delay) if not _alive(box): return # the row was refreshed or the page closed while we waited with contextlib.suppress(RuntimeError): restore() schedule(box, _revert)
def _alive(element: ui.element) -> bool: """Whether it is safe to mutate ``element`` (its client and element both exist). Accessing ``element.client`` *raises* once the client is deleted, and mutating a deleted element *warns*, so any lazy task that resumes after a reload or refresh must call this before touching the DOM. Args: element: The NiceGUI element to check. Returns: ``True`` if the element's client is alive and the element has not been deleted. """ return element._client() is not None and not element.is_deleted # noqa: SLF001