mob_photos

Photo / video library access for apps built with Mob — extracted from mob core as a plugin:

Installation

# mix.exs
{:mob_photos, "~> 0.2"}

# mob.exs
config :mob, :plugins, [:mob_photos]

The plugin manifest merges READ_MEDIA_IMAGES / READ_MEDIA_VIDEO / READ_EXTERNAL_STORAGE / ACCESS_MEDIA_LOCATION into the host AndroidManifest at build time, and a placeholder NSPhotoLibraryUsageDescription into Info.plist — replace that string (App Store review rejects the placeholder on purpose).

Usage

Pick

socket = MobPhotos.pick(socket, max: 5)

def handle_info({:photos, :picked, items}, socket) do
  # iOS:     %{path: "/tmp/mob_pick_xxx.jpg", type: :image, name: "IMG_0042",
  #            size: 2_481_233, width: 4032, height: 3024}
  # Android: %{path: ".../cache/mob_pick_xxx.jpg", type: "image", name: "1000000021.jpg",
  #            size: 2_481_233, width: 4032, height: 3024}
end

def handle_info({:photos, :cancelled}, socket), do: ...

List the library

socket = Mob.Permissions.request(socket, :media)

def handle_info({:permission, :media, :granted}, socket) do
  {:noreply, MobPhotos.list_media(socket, type: :image, limit: 20)}
end

def handle_info({:media, :listed, items}, socket) do
  # newest first; each item:
  # %{uri: "content://media/external/images/media/42",   # iOS: "ph://<localIdentifier>"
  #   display_name: "IMG_0042.jpg", size: 2_481_233, mime_type: "image/jpeg",
  #   date_added: 1_700_000_000, date_taken: 1_699_999_000_123,
  #   width: 4032, height: 3024, type: "image"}
end

size, date_taken, width and height are left out of an item when the platform doesn't know them — read them with item[:date_taken].

Thumbnail + metadata

{:ok, info} = MobPhotos.thumbnail(item.uri, max_size: 1280, quality: 80)
# %{path: ".../cache/mob_thumb_3fc29d7f5e251c42.jpg", width: 960, height: 1280,
#   orig_width: 3000, orig_height: 4000, mime: "image/jpeg", size: 196_758,
#   taken_at: "2024-05-01T12:34:56-07:00",
#   latitude: 49.2827, longitude: -123.1207, altitude: 70.5,
#   make: "TestCam", model: "Probe 1"}

source can be an absolute path (e.g. a picked item's path), an Android content:// URI or an iOS ph:// id. The caller waits while the image is decoded on a native worker thread (never a BEAM scheduler). Errors: {:error, :not_found | :unsupported | :permission | :timeout} or {:error, message}.

Limits

Self-test

MobPhotos.SelfTest proves on a device that the native side is linked and answering (no picker, no prompt): run mix mob.selftest from a host app that depends on mob_photos (mob_dev >= 0.7.17). Its library leg needs the photo library granted. Without the grant it skips: {:skip, :needs_user} on a phone, or a skip that names the iOS authorization status on a simulator. For iOS simulators:

Development

Clone, then run once:

mix setup

That fetches deps and activates the repo's git hooks (.githooks/pre-push): mix format --check, mix credo --strict (incl. ExSlop), and mix compile --warnings-as-errors run on every push, plus the full test suite when mix.exs changes — the same gate CI enforces before publishing.

License

MIT