feat: integrate device media service for local photo access and update service icons/names

This commit is contained in:
bobbert committed 2026-02-24 23:18:54 +00:00
1 parent e5dbdfeba3
commit 0ef59ba5d3
9 files changed
+313 -94

No files matched your search

+140
View File
@@ -0,0 +1,140 @@
import * as MediaLibrary from 'expo-media-library';
/**
* Fetches photos and videos directly from the Android device media store.
*
* This replaces the deprecated Google Photos Library REST API.
* On Android, the media store includes all photos visible to the device —
* camera roll, screenshots, downloads, and images synced by the Google
* Photos app. No OAuth token is needed; only the MEDIA_LIBRARY permission.
*/
const DeviceMediaService = {
/**
* Request media-library permission (camera roll / storage).
* Returns true if granted.
*/
requestPermission: async () => {
const { status } = await MediaLibrary.requestPermissionsAsync();
if (status !== 'granted') {
console.warn('Media library permission denied');
return false;
}
return true;
},
/**
* Fetch a page of media assets from the device.
*
* @param {number} pageSize Number of items per page (default 100).
* @param {string} after Cursor returned by a previous call (for pagination).
* @param {string} mediaType 'photo' | 'video' | 'all' (default 'all').
* @returns {{ mediaItems: Array, endCursor: string|null, hasNextPage: boolean }}
*/
getPhotos: async (pageSize = 100, after = null, mediaType = 'all') => {
const hasPermission = await DeviceMediaService.requestPermission();
if (!hasPermission) {
return { mediaItems: [], endCursor: null, hasNextPage: false };
}
const mediaTypeFilter =
mediaType === 'photo' ? MediaLibrary.MediaType.photo
: mediaType === 'video' ? MediaLibrary.MediaType.video
: [MediaLibrary.MediaType.photo, MediaLibrary.MediaType.video];
const options = {
first: pageSize,
mediaType: mediaTypeFilter,
sortBy: [[MediaLibrary.SortBy.creationTime, false]], // newest first
};
if (after) options.after = after;
const result = await MediaLibrary.getAssetsAsync(options);
const mediaItems = result.assets.map((asset) => ({
source_service: 'DEVICE',
source_id: asset.id,
title: asset.filename || 'Untitled',
description: '',
local_path: asset.uri,
remote_path: '',
media_type: asset.mediaType === 'video' ? 'video' : 'photo',
created_date: asset.creationTime
? new Date(asset.creationTime).toISOString()
: null,
width: asset.width,
height: asset.height,
duration: asset.duration || 0,
}));
return {
mediaItems,
endCursor: result.endCursor || null,
hasNextPage: result.hasNextPage || false,
};
},
/**
* Fetch all albums from the device (including smart albums).
*/
getAlbums: async () => {
const hasPermission = await DeviceMediaService.requestPermission();
if (!hasPermission) return [];
const albums = await MediaLibrary.getAlbumsAsync({
includeSmartAlbums: true,
});
return albums.map((album) => ({
id: album.id,
title: album.title,
count: album.assetCount,
}));
},
/**
* Fetch assets inside a specific album.
*/
getAlbumPhotos: async (albumId, pageSize = 100, after = null) => {
const hasPermission = await DeviceMediaService.requestPermission();
if (!hasPermission) {
return { mediaItems: [], endCursor: null, hasNextPage: false };
}
const album = await MediaLibrary.getAlbumAsync(albumId);
if (!album) return { mediaItems: [], endCursor: null, hasNextPage: false };
const options = {
first: pageSize,
album,
sortBy: [[MediaLibrary.SortBy.creationTime, false]],
};
if (after) options.after = after;
const result = await MediaLibrary.getAssetsAsync(options);
const mediaItems = result.assets.map((asset) => ({
source_service: 'DEVICE',
source_id: asset.id,
title: asset.filename || 'Untitled',
description: '',
local_path: asset.uri,
remote_path: '',
media_type: asset.mediaType === 'video' ? 'video' : 'photo',
created_date: asset.creationTime
? new Date(asset.creationTime).toISOString()
: null,
width: asset.width,
height: asset.height,
duration: asset.duration || 0,
}));
return {
mediaItems,
endCursor: result.endCursor || null,
hasNextPage: result.hasNextPage || false,
};
},
};
export default DeviceMediaService;