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

+54 -66
View File
@@ -5,7 +5,7 @@ import {
import { useFocusEffect } from '@react-navigation/native';
import { useTheme } from '../services/ThemeContext';
import DatabaseService from '../services/DatabaseService';
import GoogleAUTH from '../AUTH/GoogleAUTH';
import DeviceMediaService from '../services/DeviceMediaService';
import NASAUTH from '../AUTH/NASAUTH';
import TopBar from '../components/TopBar';
import PhotoGrid from '../components/PhotoGrid';
@@ -24,7 +24,8 @@ const HomeScreen = ({ navigation, route }) => {
const [refreshing, setRefreshing] = useState(false);
const [selectedMedia, setSelectedMedia] = useState(null);
const [viewerVisible, setViewerVisible] = useState(false);
const [googleNextPage, setGoogleNextPage] = useState(null);
const [deviceEndCursor, setDeviceEndCursor] = useState(null);
const [deviceHasMore, setDeviceHasMore] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
useFocusEffect(
@@ -53,45 +54,21 @@ const HomeScreen = ({ navigation, route }) => {
const loadAllMedia = async () => {
if (!loading) setLoading(true);
try {
const mediaFromDb = await DatabaseService.getMediaByUser(userId);
const newMedia = [];
// Google Photos — restore tokens from the linked account and fetch
const googleAccount = linkedAccounts.find((a) => a.service_type === 'GOOGLE_PHOTOS');
if (googleAccount) {
try {
const data = googleAccount.account_data || {};
const googleAuth = new GoogleAUTH();
googleAuth.accessToken = data.accessToken || null;
googleAuth.refreshToken = data.refreshToken || null;
// Set expiresAt to 0 so getAccessToken() will refresh if needed
googleAuth.expiresAt = data.expiresAt || 0;
const photos = await googleAuth.getPhotos(100);
if (photos?.mediaItems?.length) newMedia.push(...photos.mediaItems);
setGoogleNextPage(photos?.nextPageToken || null);
// Persist refreshed tokens back to DB if they changed
if (googleAuth.accessToken && googleAuth.accessToken !== data.accessToken) {
await DatabaseService.linkAccount(userId, 'GOOGLE_PHOTOS', {
accessToken: googleAuth.accessToken,
refreshToken: googleAuth.refreshToken || data.refreshToken,
expiresAt: googleAuth.expiresAt,
email: data.email || 'google-user',
});
}
} catch (err) {
console.error('Google Photos fetch error:', err);
// ─── Device photos (camera roll + Google Photos synced images) ───
try {
const deviceResult = await DeviceMediaService.getPhotos(100);
if (deviceResult.mediaItems.length) {
newMedia.push(...deviceResult.mediaItems);
}
setDeviceEndCursor(deviceResult.endCursor);
setDeviceHasMore(deviceResult.hasNextPage);
} catch (err) {
console.error('Device media fetch error:', err);
}
// iCloud (placeholder)
const iosAccount = linkedAccounts.find((a) => a.service_type === 'ICLOUD_PHOTOS');
if (iosAccount) {
console.log('iCloud: placeholder — would fetch photos here');
}
// NAS fetch
// ─── NAS fetch ───
const nasAccount = linkedAccounts.find((a) => a.service_type === 'NAS');
if (nasAccount) {
try {
@@ -106,8 +83,15 @@ const HomeScreen = ({ navigation, route }) => {
}
}
const combined = [...mediaFromDb, ...newMedia];
const unique = Array.from(new Map(combined.map((m) => [m.source_id || m.id || m.path, m])).values());
// ─── iCloud (placeholder) ───
const iosAccount = linkedAccounts.find((a) => a.service_type === 'ICLOUD_PHOTOS');
if (iosAccount) {
console.log('iCloud: placeholder — would fetch photos here');
}
const unique = Array.from(
new Map(newMedia.map((m) => [m.source_id || m.id || m.local_path, m])).values()
);
setAllMedia(unique);
} catch (err) {
console.error('Error loading media:', err);
@@ -127,26 +111,20 @@ const HomeScreen = ({ navigation, route }) => {
};
const loadMorePhotos = async () => {
if (loadingMore || !googleNextPage) return;
if (loadingMore || !deviceHasMore) return;
setLoadingMore(true);
try {
const googleAccount = linkedAccounts.find((a) => a.service_type === 'GOOGLE_PHOTOS');
if (googleAccount) {
const data = googleAccount.account_data || {};
const googleAuth = new GoogleAUTH();
googleAuth.accessToken = data.accessToken || null;
googleAuth.refreshToken = data.refreshToken || null;
googleAuth.expiresAt = data.expiresAt || 0;
const photos = await googleAuth.getPhotos(100, googleNextPage);
if (photos?.mediaItems?.length) {
setAllMedia((prev) => {
const combined = [...prev, ...photos.mediaItems];
return Array.from(new Map(combined.map((m) => [m.source_id || m.id || m.path, m])).values());
});
}
setGoogleNextPage(photos?.nextPageToken || null);
const result = await DeviceMediaService.getPhotos(100, deviceEndCursor);
if (result.mediaItems.length) {
setAllMedia((prev) => {
const combined = [...prev, ...result.mediaItems];
return Array.from(
new Map(combined.map((m) => [m.source_id || m.id || m.local_path, m])).values()
);
});
}
setDeviceEndCursor(result.endCursor);
setDeviceHasMore(result.hasNextPage);
} catch (err) {
console.error('Error loading more photos:', err);
} finally {
@@ -154,8 +132,8 @@ const HomeScreen = ({ navigation, route }) => {
}
};
const getServiceIcon = (type) => ({ GOOGLE_PHOTOS: '📷', ICLOUD_PHOTOS: '🍎', NAS: '💾' }[type] || '📱');
const getServiceName = (type) => ({ GOOGLE_PHOTOS: 'Google Photos', ICLOUD_PHOTOS: 'iCloud', NAS: 'NAS' }[type] || type);
const getServiceIcon = (type) => ({ DEVICE: '📱', GOOGLE_PHOTOS: '📷', ICLOUD_PHOTOS: '🍎', NAS: '💾' }[type] || '📱');
const getServiceName = (type) => ({ DEVICE: 'On Device', GOOGLE_PHOTOS: 'Google Photos', ICLOUD_PHOTOS: 'iCloud', NAS: 'NAS' }[type] || type);
if (loading && allMedia.length === 0) {
return (
@@ -166,6 +144,16 @@ const HomeScreen = ({ navigation, route }) => {
);
}
// Build the list of available source filters
const sourceFilters = [
{ key: 'DEVICE', icon: '📱', name: 'On Device' },
...linkedAccounts.map((acc) => ({
key: acc.service_type,
icon: getServiceIcon(acc.service_type),
name: getServiceName(acc.service_type),
})),
];
const FilterChips = () => (
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.filterRow} style={styles.filterContainer}>
<TouchableOpacity
@@ -174,15 +162,15 @@ const HomeScreen = ({ navigation, route }) => {
>
<Text style={[styles.chipText, selectedFilter === 'all' && { color: theme.primary, fontWeight: '600' }]}>All</Text>
</TouchableOpacity>
{linkedAccounts.map((acc) => (
{sourceFilters.map((f) => (
<TouchableOpacity
key={acc.id}
style={[styles.chip, { backgroundColor: selectedFilter === acc.service_type ? theme.chipActiveBg : theme.chipBg }]}
onPress={() => setSelectedFilter(acc.service_type)}
key={f.key}
style={[styles.chip, { backgroundColor: selectedFilter === f.key ? theme.chipActiveBg : theme.chipBg }]}
onPress={() => setSelectedFilter(f.key)}
>
<Text style={styles.chipIcon}>{getServiceIcon(acc.service_type)}</Text>
<Text style={[styles.chipText, selectedFilter === acc.service_type && { color: theme.primary, fontWeight: '600' }]}>
{getServiceName(acc.service_type)}
<Text style={styles.chipIcon}>{f.icon}</Text>
<Text style={[styles.chipText, selectedFilter === f.key && { color: theme.primary, fontWeight: '600' }]}>
{f.name}
</Text>
</TouchableOpacity>
))}
@@ -198,14 +186,14 @@ const HomeScreen = ({ navigation, route }) => {
onMenuPress={() => navigation.navigate('Settings', { userId })}
/>
{linkedAccounts.length > 0 && <FilterChips />}
<FilterChips />
{filteredMedia.length === 0 ? (
<View style={styles.centerContainer}>
<Text style={{ fontSize: 56, marginBottom: 16 }}>📸</Text>
<Text style={[styles.emptyTitle, { color: theme.text }]}>No Photos Yet</Text>
<Text style={[styles.emptyText, { color: theme.textSecondary }]}>
Photos from your connected services will appear here
Grant media library permission or take some photos to get started
</Text>
<TouchableOpacity
style={[styles.retryButton, { backgroundColor: theme.primary }]}