PhoenixKitProjects.Whiteboards (PhoenixKitProjects v0.21.0)

Copy Markdown View Source

Project whiteboards (Step 11): freeform drawing boards on core's Fresco/Etcher/annotations stack via the blank-background bridge — core's annotation persistence anchors to a phoenix_kit_files row (hard FK), so each board generates a solid-white PNG, registers it as a Storage file in the project's folder, and lets core's MediaCanvasViewer do everything else (drawing, annotation CRUD, palettes). When core grows a file-less canvas the bridge shrinks to a data migration; the board row (name/order/dimensions) is ours either way.

The blank PNG is generated in pure Elixir (no ImageMagick for the base file) and salted with a tEXt chunk carrying a fresh uuid: Storage.store_file/2 dedups per-user by content checksum, so two identical unsalted blanks would silently collide onto ONE file row — and one shared annotation set.

Deleting a board deletes the ROW; the background file (with any drawings baked into its annotations) stays in the project folder — consistent with the hub's disable-hides-never-deletes philosophy.

Summary

Functions

A solid-white truecolor PNG, salted with a tEXt chunk so every call produces DISTINCT bytes (Storage dedups per-user by content checksum — unsalted, every same-size board for one user would collide onto a single file row sharing one annotation set). Pure Elixir; no ImageMagick for the base file.

Creates a whiteboard end-to-end: blank background PNG → Storage file (owned by the creating user) → dimensions stamped → filed into the project folder → board row. opts[:actor_uuid] is REQUIRED — the files table needs an owning user for non-system files.

The DB-side composition: board row for an EXISTING file + project-folder filing + activity/broadcast. Split from create/3 so the row logic is exercisable without configured storage buckets (tests, and any future "board from an existing image" flow).

Deletes the board ROW. The background file — and the drawings living in its annotation rows — stays in the project folder, still reachable from the Files page as an annotated image.

Fetches a board scoped to its project (nil on cross-project uuids).

Boards for a project, in position/creation order.

The curated file map MediaCanvasViewer expects (same shape core's MediaViewer.curate_file/1 builds — that one is private). Nil when the file is gone or Storage errors: the LV renders a fallback card instead of a canvas.

Functions

blank_png(width, height, salt)

@spec blank_png(pos_integer(), pos_integer(), String.t()) :: binary()

A solid-white truecolor PNG, salted with a tEXt chunk so every call produces DISTINCT bytes (Storage dedups per-user by content checksum — unsalted, every same-size board for one user would collide onto a single file row sharing one annotation set). Pure Elixir; no ImageMagick for the base file.

create(project, name, opts \\ [])

@spec create(map(), String.t(), keyword()) ::
  {:ok, PhoenixKitProjects.Schemas.Whiteboard.t()} | {:error, term()}

Creates a whiteboard end-to-end: blank background PNG → Storage file (owned by the creating user) → dimensions stamped → filed into the project folder → board row. opts[:actor_uuid] is REQUIRED — the files table needs an owning user for non-system files.

Options: :width/:height (default 1920×1080, capped 8000).

create_board_for_file(project, file_uuid, attrs)

@spec create_board_for_file(map(), binary(), map()) ::
  {:ok, PhoenixKitProjects.Schemas.Whiteboard.t()} | {:error, term()}

The DB-side composition: board row for an EXISTING file + project-folder filing + activity/broadcast. Split from create/3 so the row logic is exercisable without configured storage buckets (tests, and any future "board from an existing image" flow).

delete(board, opts \\ [])

@spec delete(
  PhoenixKitProjects.Schemas.Whiteboard.t(),
  keyword()
) :: :ok | {:error, term()}

Deletes the board ROW. The background file — and the drawings living in its annotation rows — stays in the project folder, still reachable from the Files page as an annotated image.

get(project_uuid, board_uuid)

Fetches a board scoped to its project (nil on cross-project uuids).

list_for_project(project_uuid)

@spec list_for_project(binary()) :: [PhoenixKitProjects.Schemas.Whiteboard.t()]

Boards for a project, in position/creation order.

rename(board, name, opts \\ [])

Renames a board.

viewer_file(file_uuid)

@spec viewer_file(binary()) :: map() | nil

The curated file map MediaCanvasViewer expects (same shape core's MediaViewer.curate_file/1 builds — that one is private). Nil when the file is gone or Storage errors: the LV renders a fallback card instead of a canvas.