diff --git a/src/screens/HomeScreen.js b/src/screens/HomeScreen.js index 86d63d6..ea8588b 100644 --- a/src/screens/HomeScreen.js +++ b/src/screens/HomeScreen.js @@ -11,6 +11,7 @@ import NASAUTH from '../AUTH/NASAUTH'; import TopBar from '../components/TopBar'; import PhotoGrid from '../components/PhotoGrid'; import MediaViewer from '../components/MediaViewer'; +import { logError } from '../services/ErrorHandler'; import { TouchableOpacity } from 'react-native'; const HomeScreen = ({ navigation, route }) => { @@ -30,6 +31,7 @@ const HomeScreen = ({ navigation, route }) => { const [googleNextPage, setGoogleNextPage] = useState(null); const [googleSessionId, setGoogleSessionId] = useState(null); const [loadingMore, setLoadingMore] = useState(false); + const [importing, setImporting] = useState(false); useFocusEffect( useCallback(() => { @@ -71,57 +73,12 @@ const HomeScreen = ({ navigation, route }) => { console.error('Device media fetch error:', err); } - // ─── Google Photos cloud (via Picker API — includes cloud-only photos) ─── - 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; - googleAuth.expiresAt = data.expiresAt || 0; - - let photos = await googleAuth.getPhotos(100, null, null); - - // If the stored token has stale scopes (missing photospicker scope), - // automatically re-authenticate to get a fresh token and retry. - if (photos?.error?.code === 403) { - console.log('Stored token missing Picker scope — re-authenticating…'); - try { - await googleAuth.revokeAccess(); - const fresh = await googleAuth.authenticate(); - await DatabaseService.linkAccount(userId, 'GOOGLE_PHOTOS', { - accessToken: fresh.accessToken, - refreshToken: fresh.refreshToken || null, - expiresAt: googleAuth.expiresAt, - email: fresh.email || data.email || 'google-user', - }); - photos = await googleAuth.getPhotos(100, null, null); - } catch (authErr) { - console.warn('Google re-auth failed:', authErr.message); - } - } - - if (photos?.error) { - console.warn('Google Photos cloud fetch failed:', photos.error.message); - } else { - if (photos?.mediaItems?.length) newMedia.push(...photos.mediaItems); - setGoogleNextPage(photos?.nextPageToken || null); - setGoogleSessionId(photos?.sessionId || null); - } - - // Persist any refreshed tokens back to DB - 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 cloud fetch error:', err); - } + // ─── Previously imported Google Photos (stored in DB) ─── + try { + const googleMedia = await DatabaseService.getMediaByService(userId, 'GOOGLE_PHOTOS'); + if (googleMedia?.length) newMedia.push(...googleMedia); + } catch (err) { + console.error('DB Google media fetch error:', err); } // ─── NAS fetch ─── @@ -145,7 +102,7 @@ const HomeScreen = ({ navigation, route }) => { console.log('iCloud: placeholder — would fetch photos here'); } - // Deduplicate by source_id (Google Photos cloud items may overlap with device items by filename) + // Deduplicate const unique = Array.from( new Map(newMedia.map((m) => [m.source_id || m.id || m.local_path, m])).values() ); @@ -158,6 +115,76 @@ const HomeScreen = ({ navigation, route }) => { } }; + // ─── Import from Google Photos (opens picker in browser) ─── + const importGooglePhotos = async () => { + const googleAccount = linkedAccounts.find((a) => a.service_type === 'GOOGLE_PHOTOS'); + if (!googleAccount) { + console.warn('No Google account linked'); + return; + } + + setImporting(true); + try { + const data = googleAccount.account_data || {}; + const googleAuth = new GoogleAUTH(); + googleAuth.accessToken = data.accessToken || null; + googleAuth.refreshToken = data.refreshToken || null; + googleAuth.expiresAt = data.expiresAt || 0; + + let result = await googleAuth.pickPhotos(); + + // If 403 (stale scopes), auto re-auth and retry + if (result?.error?.code === 403) { + console.log('Stale token — re-authenticating…'); + await googleAuth.revokeAccess(); + const fresh = await googleAuth.authenticate(); + await DatabaseService.linkAccount(userId, 'GOOGLE_PHOTOS', { + accessToken: fresh.accessToken, + refreshToken: fresh.refreshToken || null, + expiresAt: googleAuth.expiresAt, + email: fresh.email || data.email || 'google-user', + }); + result = await googleAuth.pickPhotos(); + } + + if (result?.error) { + logError('Import Google Photos', result.error.message); + } else if (result?.mediaItems?.length) { + // Save imported items to DB so they persist across app restarts + for (const item of result.mediaItems) { + await DatabaseService.saveMediaItem(userId, item); + } + + setGoogleNextPage(result.nextPageToken || null); + setGoogleSessionId(result.sessionId || null); + + // Merge into current view + 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() + ); + }); + + console.log(`Imported ${result.mediaItems.length} photos from Google`); + } + + // Persist refreshed tokens + 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) { + logError('Import Google Photos', err); + } finally { + setImporting(false); + } + }; + const filterMedia = () => { let filtered = allMedia; if (selectedFilter !== 'all') { @@ -169,16 +196,14 @@ const HomeScreen = ({ navigation, route }) => { const loadMorePhotos = async () => { if (loadingMore) return; - // Load more from whichever source has more pages const canLoadDevice = deviceHasMore; - const canLoadGoogle = !!googleNextPage; + const canLoadGoogle = !!googleNextPage && !!googleSessionId; if (!canLoadDevice && !canLoadGoogle) return; setLoadingMore(true); try { const moreMedia = []; - // More device photos if (canLoadDevice) { const result = await DeviceMediaService.getPhotos(100, deviceEndCursor); if (result.mediaItems.length) moreMedia.push(...result.mediaItems); @@ -186,7 +211,6 @@ const HomeScreen = ({ navigation, route }) => { setDeviceHasMore(result.hasNextPage); } - // More Google Photos cloud photos (reuse the Picker session) if (canLoadGoogle) { const googleAccount = linkedAccounts.find((a) => a.service_type === 'GOOGLE_PHOTOS'); if (googleAccount) { @@ -196,20 +220,15 @@ const HomeScreen = ({ navigation, route }) => { googleAuth.refreshToken = data.refreshToken || null; googleAuth.expiresAt = data.expiresAt || 0; - const photos = await googleAuth.getPhotos(100, googleNextPage, googleSessionId); - if (photos?.mediaItems?.length) moreMedia.push(...photos.mediaItems); + const photos = await googleAuth.getMorePhotos(100, googleNextPage, googleSessionId); + if (photos?.mediaItems?.length) { + moreMedia.push(...photos.mediaItems); + for (const item of photos.mediaItems) { + await DatabaseService.saveMediaItem(userId, item); + } + } setGoogleNextPage(photos?.nextPageToken || null); if (photos?.sessionId) setGoogleSessionId(photos.sessionId); - - // Persist refreshed tokens - 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', - }); - } } } @@ -231,6 +250,8 @@ const HomeScreen = ({ navigation, route }) => { 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); + const hasGoogleAccount = linkedAccounts.some((a) => a.service_type === 'GOOGLE_PHOTOS'); + if (loading && allMedia.length === 0) { return ( @@ -284,12 +305,33 @@ const HomeScreen = ({ navigation, route }) => { + {/* Import from Google Photos button */} + {hasGoogleAccount && ( + + {importing ? ( + + ) : ( + <> + 📷 + Import from Google Photos + + )} + + )} + {filteredMedia.length === 0 ? ( 📸 No Photos Yet - Grant media library permission or take some photos to get started + {hasGoogleAccount + ? 'Tap "Import from Google Photos" above to select cloud photos,\nor grant media library permission to see on-device photos.' + : 'Grant media library permission or take some photos to get started'} { + try { + await DatabaseService.ensureDb(); + if (!userId || !item.source_service) return; + + // Check for existing entry to avoid duplicates + if (item.source_id) { + const existing = await db.getFirstAsync( + 'SELECT id FROM media WHERE user_id = ? AND source_id = ?', + [userId, item.source_id] + ); + if (existing) return existing.id; + } + + const result = await db.runAsync( + `INSERT INTO media (user_id, source_service, source_id, title, description, local_path, remote_path, media_type, created_date) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [userId, item.source_service, item.source_id || null, item.title || null, + item.description || null, item.local_path || null, item.remote_path || null, + item.media_type || null, item.created_date || null] + ); + return result.lastInsertRowId; + } catch (error) { + console.error('Error saving media item:', error); + } + }, + getMediaByUser: async (userId) => { try { await DatabaseService.ensureDb();