Packages
phoenix_kit
1.7.109
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
221 Versions
Jump to
Current section
221 Versions
Compare versions
32
files changed
+2406
additions
-464
deletions
unknownCHANGELOG.md
File is too large to be displayed (100 KB limit).
| @@ -1,6 +1,6 @@ | |
| 1 1 | {<<"links">>,[{<<"GitHub">>,<<"https://github.com/BeamLabEU/phoenix_kit">>}]}. |
| 2 2 | {<<"name">>,<<"phoenix_kit">>}. |
| 3 | - {<<"version">>,<<"1.7.108">>}. |
| 3 | + {<<"version">>,<<"1.7.109">>}. |
| 4 4 | {<<"description">>, |
| 5 5 | <<65,32,102,111,117,110,100,97,116,105,111,110,32,102,111,114,32,98,117,105, |
| 6 6 | 108,100,105,110,103,32,69,108,105,120,105,114,32,80,104,111,101,110,105, |
| @@ -56,6 +56,7 @@ | |
| 56 56 | <<"lib/modules/storage/storage.ex">>, |
| 57 57 | <<"lib/modules/storage/tessera_adapter.ex">>, |
| 58 58 | <<"lib/modules/storage/variant_naming.ex">>, |
| 59 | + <<"lib/modules/storage/etcher_adapter.ex">>, |
| 59 60 | <<"lib/modules/storage/README.md">>,<<"lib/modules/storage/workers">>, |
| 60 61 | <<"lib/modules/storage/workers/process_file_job.ex">>, |
| 61 62 | <<"lib/modules/storage/workers/delete_orphaned_file_job.ex">>, |
| @@ -209,6 +210,7 @@ | |
| 209 210 | <<"lib/phoenix_kit_web/components/dashboard/badge.ex">>, |
| 210 211 | <<"lib/phoenix_kit_web/components/dashboard/context_selector.ex">>, |
| 211 212 | <<"lib/phoenix_kit_web/components/user_dashboard_nav.ex">>, |
| 213 | + <<"lib/phoenix_kit_web/components/annotation_composer.ex">>, |
| 212 214 | <<"lib/phoenix_kit_web/components/media_browser">>, |
| 213 215 | <<"lib/phoenix_kit_web/components/media_browser/embed.ex">>, |
| 214 216 | <<"lib/phoenix_kit_web/components/layout_wrapper.ex">>, |
| @@ -376,6 +378,9 @@ | |
| 376 378 | <<"lib/phoenix_kit/aws/sesv2.ex">>, |
| 377 379 | <<"lib/phoenix_kit/aws/credentials_verifier.ex">>, |
| 378 380 | <<"lib/phoenix_kit/aws/infrastructure_setup.ex">>, |
| 381 | + <<"lib/phoenix_kit/annotations">>, |
| 382 | + <<"lib/phoenix_kit/annotations/annotations.ex">>, |
| 383 | + <<"lib/phoenix_kit/annotations/annotation.ex">>, |
| 379 384 | <<"lib/phoenix_kit/module_registry.ex">>,<<"lib/phoenix_kit/admin">>, |
| 380 385 | <<"lib/phoenix_kit/admin/presence.ex">>, |
| 381 386 | <<"lib/phoenix_kit/admin/simple_presence.ex">>, |
| @@ -459,6 +464,7 @@ | |
| 459 464 | <<"lib/phoenix_kit/migrations/postgres/v107.ex">>, |
| 460 465 | <<"lib/phoenix_kit/migrations/postgres/v68.ex">>, |
| 461 466 | <<"lib/phoenix_kit/migrations/postgres/v37.ex">>, |
| 467 | + <<"lib/phoenix_kit/migrations/postgres/v114.ex">>, |
| 462 468 | <<"lib/phoenix_kit/migrations/postgres/v54.ex">>, |
| 463 469 | <<"lib/phoenix_kit/migrations/postgres/v24.ex">>, |
| 464 470 | <<"lib/phoenix_kit/migrations/postgres/v33.ex">>, |
| @@ -468,6 +474,7 @@ | |
| 468 474 | <<"lib/phoenix_kit/migrations/postgres/v42.ex">>, |
| 469 475 | <<"lib/phoenix_kit/migrations/postgres/v32.ex">>, |
| 470 476 | <<"lib/phoenix_kit/migrations/postgres/v25.ex">>, |
| 477 | + <<"lib/phoenix_kit/migrations/postgres/v115.ex">>, |
| 471 478 | <<"lib/phoenix_kit/migrations/postgres/v55.ex">>, |
| 472 479 | <<"lib/phoenix_kit/migrations/postgres/v78.ex">>, |
| 473 480 | <<"lib/phoenix_kit/migrations/postgres/v27.ex">>, |
| @@ -483,6 +490,7 @@ | |
| 483 490 | <<"lib/phoenix_kit/migrations/postgres/v101.ex">>, |
| 484 491 | <<"lib/phoenix_kit/migrations/postgres/v56.ex">>, |
| 485 492 | <<"lib/phoenix_kit/migrations/postgres/v09.ex">>, |
| 493 | + <<"lib/phoenix_kit/migrations/postgres/v116.ex">>, |
| 486 494 | <<"lib/phoenix_kit/migrations/postgres/v26.ex">>, |
| 487 495 | <<"lib/phoenix_kit/migrations/postgres/v79.ex">>, |
| 488 496 | <<"lib/phoenix_kit/migrations/postgres/v38.ex">>, |
| @@ -709,9 +717,19 @@ | |
| 709 717 | {<<"optional">>,false}, |
| 710 718 | {<<"requirement">>,<<"~> 0.2.11">>}, |
| 711 719 | {<<"repository">>,<<"hexpm">>}], |
| 720 | + [{<<"name">>,<<"fresco">>}, |
| 721 | + {<<"app">>,<<"fresco">>}, |
| 722 | + {<<"optional">>,false}, |
| 723 | + {<<"requirement">>,<<"~> 0.1">>}, |
| 724 | + {<<"repository">>,<<"hexpm">>}], |
| 712 725 | [{<<"name">>,<<"tessera">>}, |
| 713 726 | {<<"app">>,<<"tessera">>}, |
| 714 727 | {<<"optional">>,false}, |
| 728 | + {<<"requirement">>,<<"~> 0.2">>}, |
| 729 | + {<<"repository">>,<<"hexpm">>}], |
| 730 | + [{<<"name">>,<<"etcher">>}, |
| 731 | + {<<"app">>,<<"etcher">>}, |
| 732 | + {<<"optional">>,false}, |
| 715 733 | {<<"requirement">>,<<"~> 0.1">>}, |
| 716 734 | {<<"repository">>,<<"hexpm">>}], |
| 717 735 | [{<<"name">>,<<"aws_regions">>}, |
| @@ -0,0 +1,82 @@ | |
| 1 | + defmodule PhoenixKit.Modules.Storage.EtcherAdapter do |
| 2 | + @moduledoc """ |
| 3 | + `Etcher.Storage` adapter that writes annotation events from the |
| 4 | + MediaBrowser's Fresco viewer into `phoenix_kit_annotations`. |
| 5 | + |
| 6 | + Etcher's generic API is keyed by `target_type` + `target_uuid` so the |
| 7 | + library can annotate any kind of resource. In PhoenixKit the only |
| 8 | + target is a media File, so this adapter requires `target_type == |
| 9 | + "file"` and maps `target_uuid` to `file_uuid`. |
| 10 | + |
| 11 | + ## Comment threads |
| 12 | + |
| 13 | + An annotation's discussion thread is **not** created at draw time — |
| 14 | + it's instantiated lazily when the user posts the first comment on the |
| 15 | + annotation. The comments are anchored to the **file** |
| 16 | + (`resource_type = "file"`, `resource_uuid = file_uuid`) with |
| 17 | + `metadata.annotation_uuid` carrying the back-reference, so they |
| 18 | + appear in the file's main thread alongside non-annotated discussion. |
| 19 | + No `comment_uuid` column on annotations is needed. |
| 20 | + """ |
| 21 | + |
| 22 | + @behaviour Etcher.Storage |
| 23 | + |
| 24 | + alias PhoenixKit.Annotations |
| 25 | + alias PhoenixKit.Annotations.Annotation |
| 26 | + |
| 27 | + # Whitelist of annotation schema fields the adapter accepts from event |
| 28 | + # payloads, sourced from `Annotation.adapter_writable_fields/0` so the |
| 29 | + # set stays in sync with the schema's `@cast_fields`. Anything else |
| 30 | + # (Etcher routing keys, JS-side anchor coords, client-side metadata) |
| 31 | + # is silently dropped — `String.to_existing_atom` on unknown payload |
| 32 | + # keys used to crash the LV when Etcher's payload shape grew new |
| 33 | + # client-side keys like `anchor_x` / `anchor_y`. Stored as strings |
| 34 | + # here since the filter compares against `to_string(payload_key)`. |
| 35 | + @schema_keys Enum.map(Annotation.adapter_writable_fields(), &Atom.to_string/1) |
| 36 | + |
| 37 | + @impl Etcher.Storage |
| 38 | + def create(attrs) do |
| 39 | + with {:ok, file_uuid} <- target_uuid(attrs) do |
| 40 | + attrs |
| 41 | + |> filter_to_schema() |
| 42 | + |> Map.put(:file_uuid, file_uuid) |
| 43 | + |> Annotations.create() |
| 44 | + end |
| 45 | + end |
| 46 | + |
| 47 | + @impl Etcher.Storage |
| 48 | + def list_for(target_type, target_uuid) |
| 49 | + |
| 50 | + def list_for("file", file_uuid) when is_binary(file_uuid) do |
| 51 | + Annotations.list_for_file(file_uuid) |
| 52 | + end |
| 53 | + |
| 54 | + def list_for(_other, _uuid), do: [] |
| 55 | + |
| 56 | + @impl Etcher.Storage |
| 57 | + def update(uuid, attrs) do |
| 58 | + attrs |
| 59 | + |> filter_to_schema() |
| 60 | + |> then(&Annotations.update(uuid, &1)) |
| 61 | + end |
| 62 | + |
| 63 | + @impl Etcher.Storage |
| 64 | + def delete(uuid), do: Annotations.delete(uuid) |
| 65 | + |
| 66 | + # --------------------------------------------------------------------------- |
| 67 | + |
| 68 | + defp target_uuid(%{"target_type" => "file", "target_uuid" => uuid}) when is_binary(uuid), |
| 69 | + do: {:ok, uuid} |
| 70 | + |
| 71 | + defp target_uuid(%{target_type: "file", target_uuid: uuid}) when is_binary(uuid), |
| 72 | + do: {:ok, uuid} |
| 73 | + |
| 74 | + defp target_uuid(_attrs), do: {:error, :unsupported_target} |
| 75 | + |
| 76 | + defp filter_to_schema(attrs) do |
| 77 | + Enum.reduce(attrs, %{}, fn {k, v}, acc -> |
| 78 | + key = to_string(k) |
| 79 | + if key in @schema_keys, do: Map.put(acc, String.to_existing_atom(key), v), else: acc |
| 80 | + end) |
| 81 | + end |
| 82 | + end |
| @@ -43,16 +43,19 @@ defmodule PhoenixKit.Modules.Storage.VariantGenerator do | |
| 43 43 | |
| 44 44 | ## Options |
| 45 45 | |
| 46 | - - `:async` - Whether to generate variants asynchronously (default: true) |
| 47 46 | - `:dimensions` - List of specific dimensions to generate (default: all enabled) |
| 48 47 | |
| 48 | + Variant generation is always synchronous from the caller's perspective. |
| 49 | + Internally `process_variants/2` parallelizes the per-dimension work via |
| 50 | + `Task.await_many/2` with a 10-minute cap. Callers that need fire-and-forget |
| 51 | + semantics should enqueue the work via Oban (see `process_file_job.ex`). |
| 52 | + |
| 49 53 | ## Returns |
| 50 54 | |
| 51 55 | - `{:ok, variants}` - List of generated file instances |
| 52 56 | - `{:error, reason}` - Error if generation fails |
| 53 57 | """ |
| 54 58 | def generate_variants(file, opts \\ []) do |
| 55 | - async = Keyword.get(opts, :async, true) |
| 56 59 | specific_dimensions = Keyword.get(opts, :dimensions, []) |
| 57 60 | |
| 58 61 | if should_generate_variants?(file) do |
| @@ -60,23 +63,13 @@ defmodule PhoenixKit.Modules.Storage.VariantGenerator do | |
| 60 63 | |
| 61 64 | case dimensions do |
| 62 65 | [] -> {:ok, []} |
| 63 | - _ -> run_variant_processing(file, dimensions, async) |
| 66 | + _ -> process_variants(file, dimensions) |
| 64 67 | end |
| 65 68 | else |
| 66 69 | {:ok, []} |
| 67 70 | end |
| 68 71 | end |
| 69 72 | |
| 70 | - defp run_variant_processing(file, dimensions, true) do |
| 71 | - task = Task.async(fn -> process_variants(file, dimensions) end) |
| 72 | - # 30 second timeout |
| 73 | - Task.await(task, 30_000) |
| 74 | - end |
| 75 | - |
| 76 | - defp run_variant_processing(file, dimensions, false) do |
| 77 | - process_variants(file, dimensions) |
| 78 | - end |
| 79 | - |
| 80 73 | @doc """ |
| 81 74 | Generates a specific variant for a file. |
| @@ -0,0 +1,84 @@ | |
| 1 | + defmodule PhoenixKit.Annotations.Annotation do |
| 2 | + @moduledoc """ |
| 3 | + Ecto schema for `phoenix_kit_annotations`. |
| 4 | + |
| 5 | + Stores user-drawn shapes (rectangle, circle, polygon, freehand) tied to |
| 6 | + a `PhoenixKit.Modules.Storage.File` via `file_uuid`. All geometry is |
| 7 | + in image-pixel coordinates; Fresco's coordinate adapter rescales for |
| 8 | + pan/zoom at render time. |
| 9 | + |
| 10 | + ## Comment thread linkage |
| 11 | + |
| 12 | + An annotation's discussion lives in `phoenix_kit_comments` anchored to |
| 13 | + the **file** (`resource_type = "file"`, `resource_uuid = file_uuid`) |
| 14 | + with `metadata.annotation_uuid` carrying the back-reference. This lets |
| 15 | + annotation-rooted comments appear in the file's main comments thread |
| 16 | + alongside non-annotated discussion. There is no `comment_uuid` column |
| 17 | + on annotations — the relationship is one-directional from the comment |
| 18 | + side, and a thread is created lazily when the first comment is posted. |
| 19 | + """ |
| 20 | + use Ecto.Schema |
| 21 | + import Ecto.Changeset |
| 22 | + |
| 23 | + @primary_key {:uuid, UUIDv7, autogenerate: true} |
| 24 | + @foreign_key_type UUIDv7 |
| 25 | + |
| 26 | + @kinds ~w(rectangle circle polygon freehand) |
| 27 | + |
| 28 | + @type t :: %__MODULE__{ |
| 29 | + uuid: UUIDv7.t() | nil, |
| 30 | + file_uuid: UUIDv7.t(), |
| 31 | + creator_uuid: UUIDv7.t() | nil, |
| 32 | + kind: String.t(), |
| 33 | + geometry: map(), |
| 34 | + style: map() | nil, |
| 35 | + metadata: map() | nil, |
| 36 | + position: integer(), |
| 37 | + inserted_at: DateTime.t() | nil, |
| 38 | + updated_at: DateTime.t() | nil |
| 39 | + } |
| 40 | + |
| 41 | + schema "phoenix_kit_annotations" do |
| 42 | + field :file_uuid, UUIDv7 |
| 43 | + field :creator_uuid, UUIDv7 |
| 44 | + field :kind, :string |
| 45 | + field :geometry, :map |
| 46 | + field :style, :map |
| 47 | + field :metadata, :map |
| 48 | + field :position, :integer, default: 0 |
| 49 | + |
| 50 | + timestamps(type: :utc_datetime) |
| 51 | + end |
| 52 | + |
| 53 | + @cast_fields ~w(file_uuid creator_uuid kind geometry style metadata position)a |
| 54 | + @required_fields ~w(file_uuid kind geometry)a |
| 55 | + |
| 56 | + # Fields the storage adapter is allowed to accept from event payloads. |
| 57 | + # `file_uuid` is set server-side from `target_uuid`, not by the client, |
| 58 | + # so it's excluded here — the adapter's `create/1` puts it on the |
| 59 | + # changeset after the whitelist filter. |
| 60 | + @adapter_writable_fields @cast_fields -- [:file_uuid] |
| 61 | + |
| 62 | + @doc false |
| 63 | + def changeset(annotation, attrs) do |
| 64 | + annotation |
| 65 | + |> cast(attrs, @cast_fields) |
| 66 | + |> validate_required(@required_fields) |
| 67 | + |> validate_inclusion(:kind, @kinds) |
| 68 | + |> foreign_key_constraint(:file_uuid) |
| 69 | + |> foreign_key_constraint(:creator_uuid) |
| 70 | + |> check_constraint(:kind, name: :phoenix_kit_annotations_kind_check) |
| 71 | + end |
| 72 | + |
| 73 | + @doc "List of allowed kind strings." |
| 74 | + def kinds, do: @kinds |
| 75 | + |
| 76 | + @doc """ |
| 77 | + Fields the Etcher storage adapter is allowed to take from event |
| 78 | + payloads. Single source of truth so the adapter's whitelist doesn't |
| 79 | + drift from the schema's `@cast_fields`. `file_uuid` is excluded — |
| 80 | + the adapter sets it server-side from the Etcher `target_uuid`. |
| 81 | + """ |
| 82 | + @spec adapter_writable_fields() :: [atom()] |
| 83 | + def adapter_writable_fields, do: @adapter_writable_fields |
| 84 | + end |
Loading more files…