One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

Photo Library

Save, browse, edit, and export photos and videos on iOS and Android

One.PhotoLibrary is the full read and write API for the system photo library: save captures, browse asset metadata page by page, organize albums, mark favorites, replace content in place, and export originals to files. Reach for it when your app manages the library itself; for a one-shot attachment, the system pickers on the image picker page are simpler.

On iOS it drives PHPhotoLibrary from the Photos framework. On Android it queries MediaStore through the content resolver. On web there is no library, so permissions read denied, lists come back empty, and calls that return a value reject.

Usage

import { One } from 'one'
async function saveCapture(imageFileUri: string) {
const permission = await One.PhotoLibrary.requestAddPermission()
if (permission === 'authorized') {
const assetId = await One.PhotoLibrary.saveImage(imageFileUri)
console.log('Saved Photos asset', assetId)
}
}

Saving needs only add permission, which never prompts on current Android. The save resolves with the new asset identifier once the library commits the change.

How it works

Two permissions split the API. Add-only covers saveImage and saveVideo. Read/write covers everything else: listing, albums, favorites, deletion, replacement, and export. Declare the messages your app uses in native.app.photoLibrary before prebuild: addOnly becomes NSPhotoLibraryAddUsageDescription and readWrite becomes NSPhotoLibraryUsageDescription on iOS, plus a flag that stops the system from showing the limited-access alert on its own, so your app chooses when to call presentLimitedLibraryPicker. On Android the same config stamps the READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, and legacy storage permissions into the manifest. Calls without their message reject with E_PHOTO_LIBRARY_MANIFEST before touching the library.

Reads fetch newest first in bounded pages: integer offset from 0 to 1,000,000 and integer limit from 1 to 100, defaulting to the first 50. Each asset carries its identifier, media type, pixel size, duration, optional creation date, and favorite and Live Photo flags. Metadata only; no call returns image or video bytes. On iOS fetches run on a background queue and writes commit through performChanges, resolving only after Photos accepts the change. On Android queries run on a worker thread against MediaStore, and albums are media buckets, listed but never edited: renaming, membership changes, deletion, and content replace and revert resolve without doing anything, and creating an album rejects. Deleting an asset removes it from the whole library and may show a system confirmation; Photos can retain the item in Recently Deleted.

Saves accept local file:// URIs produced by the file system, image picker, or document picker, never network URLs, and resolve with the new identifier. An app with read/write access can save without requesting add-only access. Exports write to a unique temporary file and return its file:// URI: the untouched original, the visible edited image, or the visible edited video. Each takes an allowNetwork flag, default false, that permits an iCloud download; without it, a cloud-only asset fails rather than using data. Content replacement keeps the asset identifier and the original, so revertAssetContent restores it; Photos may ask the user to confirm either edit. On web, permissions read denied, lists and the picker return empty results, void writes resolve silently, and every call that returns an asset, album, file, or identifier rejects.

API reference

Methods: permissionsTypeDescription
getAddPermissionStatus() => PhotoLibraryPermissionStatusThe add-only status, without prompting.
() => PhotoLibraryPermissionStatusThe add-only status, without prompting.
requestAddPermission() => Promise<PhotoLibraryPermissionStatus>Ask for add-only access for saving.
() => Promise<PhotoLibraryPermissionStatus>Ask for add-only access for saving.
getReadPermissionStatus() => PhotoLibraryPermissionStatusThe read and write status, without prompting.
() => PhotoLibraryPermissionStatusThe read and write status, without prompting.
requestReadPermission() => Promise<PhotoLibraryPermissionStatus>Ask for read and write access for browsing.
() => Promise<PhotoLibraryPermissionStatus>Ask for read and write access for browsing.
presentLimitedLibraryPicker() => Promise<string[]>Let a limited user pick more assets. Resolves with the new ids.
() => Promise<string[]>Let a limited user pick more assets. Resolves with the new ids.
Methods: readingTypeDescription
listAssets(offset: number, limit: number) => Promise<PhotoLibraryAssetPage>One newest-first page. Defaults to offset 0, limit 50.
(offset: number, limit: number) => Promise<PhotoLibraryAssetPage>One newest-first page. Defaults to offset 0, limit 50.
getAsset(identifier: string) => Promise<PhotoLibraryAsset>Current metadata for one asset id.
(identifier: string) => Promise<PhotoLibraryAsset>Current metadata for one asset id.
Methods: albumsTypeDescription
listAlbums(offset: number, limit: number) => Promise<PhotoLibraryAlbumPage>User albums by title. Defaults to offset 0, limit 50.
(offset: number, limit: number) => Promise<PhotoLibraryAlbumPage>User albums by title. Defaults to offset 0, limit 50.
getAlbum(identifier: string) => Promise<PhotoLibraryAlbum>Current title for one album id.
(identifier: string) => Promise<PhotoLibraryAlbum>Current title for one album id.
createAlbum(title: string) => Promise<string>Create an album. Resolves with its id. iOS only.
(title: string) => Promise<string>Create an album. Resolves with its id. iOS only.
renameAlbum(identifier: string, title: string) => Promise<void>Rename an album. Silent on Android.
(identifier: string, title: string) => Promise<void>Rename an album. Silent on Android.
listAlbumAssets(identifier: string, offset: number, limit: number) => Promise<PhotoLibraryAssetPage>One newest-first page of an album. Defaults to offset 0, limit 50.
(identifier: string, offset: number, limit: number) => Promise<PhotoLibraryAssetPage>One newest-first page of an album. Defaults to offset 0, limit 50.
addAssetToAlbum(albumIdentifier: string, assetIdentifier: string) => Promise<void>Add an asset to an album. Silent on Android.
(albumIdentifier: string, assetIdentifier: string) => Promise<void>Add an asset to an album. Silent on Android.
removeAssetFromAlbum(albumIdentifier: string, assetIdentifier: string) => Promise<void>Remove an asset from an album. Silent on Android.
(albumIdentifier: string, assetIdentifier: string) => Promise<void>Remove an asset from an album. Silent on Android.
deleteAlbum(identifier: string) => Promise<void>Delete the album, keeping its assets. Silent on Android.
(identifier: string) => Promise<void>Delete the album, keeping its assets. Silent on Android.
Methods: writingTypeDescription
setFavorite(identifier: string, favorite: boolean) => Promise<void>Mark or unmark an asset as a favorite.
(identifier: string, favorite: boolean) => Promise<void>Mark or unmark an asset as a favorite.
deleteAsset(identifier: string) => Promise<void>Delete an asset from the whole library.
(identifier: string) => Promise<void>Delete an asset from the whole library.
replaceImageContent(identifier: string, uri: string) => Promise<void>Replace a still with an upright JPEG file. iOS only.
(identifier: string, uri: string) => Promise<void>Replace a still with an upright JPEG file. iOS only.
replaceVideoContent(identifier: string, uri: string) => Promise<void>Replace a video with an upright movie file. iOS only.
(identifier: string, uri: string) => Promise<void>Replace a video with an upright movie file. iOS only.
revertAssetContent(identifier: string) => Promise<void>Restore the original after a replace. iOS only.
(identifier: string) => Promise<void>Restore the original after a replace. iOS only.
saveImage(uri: string) => Promise<string>Save a local image file. Resolves with the new id.
(uri: string) => Promise<string>Save a local image file. Resolves with the new id.
saveVideo(uri: string) => Promise<string>Save a local video file. Resolves with the new id.
(uri: string) => Promise<string>Save a local video file. Resolves with the new id.
Methods: exportTypeDescription
exportOriginalAsset(identifier: string, allowNetwork: boolean) => Promise<string>Write the untouched original to a temp file. Network off by default.
(identifier: string, allowNetwork: boolean) => Promise<string>Write the untouched original to a temp file. Network off by default.
exportCurrentImage(identifier: string, allowNetwork: boolean) => Promise<string>Write the visible edited image to a temp file.
(identifier: string, allowNetwork: boolean) => Promise<string>Write the visible edited image to a temp file.
exportCurrentVideo(identifier: string, allowNetwork: boolean) => Promise<string>Write the visible edited video to a temp file.
(identifier: string, allowNetwork: boolean) => Promise<string>Write the visible edited video to a temp file.
AssetTypeDescription
identifierstringThe stable id to pass to the other methods.
stringThe stable id to pass to the other methods.
mediaTypePhotoLibraryMediaTypeImage, video, audio, or unknown.
PhotoLibraryMediaTypeImage, video, audio, or unknown.
widthnumberPixel width of the asset.
numberPixel width of the asset.
heightnumberPixel height of the asset.
numberPixel height of the asset.
durationMsnumberPlayback length in milliseconds.
numberPlayback length in milliseconds.
creationDateMsnumberCreation time, when the library knows it.
numberCreation time, when the library knows it.
isFavoritebooleanWhether the asset is a favorite.
booleanWhether the asset is a favorite.
isLivePhotobooleanWhether the asset is a Live Photo.
booleanWhether the asset is a Live Photo.
Asset pageTypeDescription
assetsPhotoLibraryAsset[]The assets on this page, newest first.
PhotoLibraryAsset[]The assets on this page, newest first.
totalCountnumberThe assets visible to the app in total.
numberThe assets visible to the app in total.
AlbumTypeDescription
identifierstringThe stable id to pass to the album methods.
stringThe stable id to pass to the album methods.
titlestringThe current album title.
stringThe current album title.
Album pageTypeDescription
albumsPhotoLibraryAlbum[]The albums on this page, by title.
PhotoLibraryAlbum[]The albums on this page, by title.
totalCountnumberThe user albums in total.
numberThe user albums in total.

Platform differences

iOSAndroidWeb
StorePhoto libraryMedia storeNone, stubbed
SavingNeeds add rightsAllowed past 28Rejects
BrowsingNeeds read rightsNeeds media rightsEmpty pages
AlbumsFull editingListing onlyEmpty, silent
FavoritesSupportedNeeds Android 11Silent
ReplacingEdits in placeSilent, no changeSilent, no change
PickerNew picks onlyNew picks onlyEmpty list

Limits and gotchas

Examples

Save a capture to the library

import { useState } from 'react'
import { Button, Text } from 'react-native'
import { One } from 'one'
function SavePhoto({ fileUri }: { fileUri: string }) {
const [saved, setSaved] = useState(false)
async function save() {
const permission = await One.PhotoLibrary.requestAddPermission()
if (permission !== 'authorized') return
await One.PhotoLibrary.saveImage(fileUri)
setSaved(true)
}
return (
<>
<Button title="Save to library" onPress={save} />
{saved ? <Text>Saved.</Text> : null}
</>
)
}

Add-only permission is the cheapest grant that can save, and on current Android it resolves authorized without a prompt. The button stays usable after a deny, so the user can retry once they grant access in Settings.

Browse the newest photos

import { useEffect, useState } from 'react'
import { Text } from 'react-native'
import { One } from 'one'
type LibraryAsset = Awaited<ReturnType<typeof One.PhotoLibrary.listAssets>>['assets'][number]
function NewestPhotos() {
const [assets, setAssets] = useState<LibraryAsset[]>([])
useEffect(() => {
async function load() {
const permission = await One.PhotoLibrary.requestReadPermission()
if (permission !== 'authorized' && permission !== 'limited') return
const page = await One.PhotoLibrary.listAssets(0, 50)
setAssets(page.assets)
}
load()
}, [])
return (
<>
{assets.map((asset) => (
<Text key={asset.identifier}>
{asset.mediaType} {asset.width}x{asset.height}
{asset.isLivePhoto ? ' live' : ''}
</Text>
))}
</>
)
}

Limited access still returns a page, restricted to the user’s selection, so both grants share the same listing path. An asset flagged live can be shown with One.iOS.LivePhotoView.

Export an original and clean up

import { One } from 'one'
async function copyOriginal(identifier: string, share: (uri: string) => Promise<void>) {
const uri = await One.PhotoLibrary.exportOriginalAsset(identifier)
try {
const info = await One.FileSystem.getInfo(uri)
console.log('Exported bytes:', info.size)
await share(uri)
} finally {
await One.FileSystem.delete(uri)
}
}

The export lands in a temporary file outside the library, so reading it needs no further permission. The finally deletes it once the share finishes, keeping the temp directory from filling with exports.

Edit this page on GitHub.