mirror of
https://github.com/mudabbir-ahmad/Media-Aggregation-and-Sorting-System.git
synced 2026-10-07 19:40:20 +00:00
feat: enhance database service with centralized error logging and improve media handling across services
This commit is contained in:
1 parent
04046702d3
commit
ad0cc1a7f5
5 files changed
+376
-157
No files matched your search
+81
-5
@@ -1,6 +1,7 @@
|
|||||||
import * as AuthSession from 'expo-auth-session';
|
import * as AuthSession from 'expo-auth-session';
|
||||||
import * as WebBrowser from 'expo-web-browser';
|
import * as WebBrowser from 'expo-web-browser';
|
||||||
import * as Application from 'expo-application';
|
import * as Application from 'expo-application';
|
||||||
|
import * as FileSystem from 'expo-file-system';
|
||||||
import { AuthServiceBase } from './AuthServiceBase';
|
import { AuthServiceBase } from './AuthServiceBase';
|
||||||
import { logError } from '../services/ErrorHandler';
|
import { logError } from '../services/ErrorHandler';
|
||||||
|
|
||||||
@@ -29,6 +30,8 @@ const SCOPES = [
|
|||||||
|
|
||||||
const PICKER_API = 'https://photospicker.googleapis.com/v1';
|
const PICKER_API = 'https://photospicker.googleapis.com/v1';
|
||||||
|
|
||||||
|
const CACHE_DIR = `${FileSystem.cacheDirectory}google_photos/`;
|
||||||
|
|
||||||
const REDIRECT_URI = `${Application.applicationId}:/oauthredirect`;
|
const REDIRECT_URI = `${Application.applicationId}:/oauthredirect`;
|
||||||
|
|
||||||
class GoogleAUTH extends AuthServiceBase {
|
class GoogleAUTH extends AuthServiceBase {
|
||||||
@@ -276,6 +279,10 @@ class GoogleAUTH extends AuthServiceBase {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalise Picker API media items to the app's standard format.
|
* Normalise Picker API media items to the app's standard format.
|
||||||
|
*
|
||||||
|
* NOTE: The Picker API's baseUrl requires an Authorization header to
|
||||||
|
* fetch the image. We store it in `remote_path` / `picker_base_url`
|
||||||
|
* so the caller can download it to a local cache file for rendering.
|
||||||
*/
|
*/
|
||||||
_normalisePickerItems(items) {
|
_normalisePickerItems(items) {
|
||||||
return (items || []).map((item) => {
|
return (items || []).map((item) => {
|
||||||
@@ -286,14 +293,80 @@ class GoogleAUTH extends AuthServiceBase {
|
|||||||
source_id: item.id || file.id,
|
source_id: item.id || file.id,
|
||||||
title: file.filename || 'Untitled',
|
title: file.filename || 'Untitled',
|
||||||
description: '',
|
description: '',
|
||||||
local_path: file.baseUrl ? `${file.baseUrl}=w400-h400` : null,
|
// local_path is null until we download it with the auth token
|
||||||
|
local_path: null,
|
||||||
remote_path: file.baseUrl || '',
|
remote_path: file.baseUrl || '',
|
||||||
|
picker_base_url: file.baseUrl || null,
|
||||||
media_type: isVideo ? 'video' : 'photo',
|
media_type: isVideo ? 'video' : 'photo',
|
||||||
|
mime_type: file.mimeType || null,
|
||||||
created_date: file.mediaFileMetadata?.creationTime || null,
|
created_date: file.mediaFileMetadata?.creationTime || null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download Picker images to local cache so <Image> can render them.
|
||||||
|
*
|
||||||
|
* The Picker API's baseUrl requires an Authorization header. React
|
||||||
|
* Native's <Image> can't add custom headers reliably, so we download
|
||||||
|
* each photo to a cache file and set local_path to the file:// URI.
|
||||||
|
*
|
||||||
|
* @param {Array} items — normalised items from _normalisePickerItems
|
||||||
|
* @returns {Array} — same items with local_path populated
|
||||||
|
*/
|
||||||
|
async downloadPickerPhotos(items) {
|
||||||
|
const token = await this.getAccessToken();
|
||||||
|
|
||||||
|
// Ensure cache directory exists
|
||||||
|
const dirInfo = await FileSystem.getInfoAsync(CACHE_DIR);
|
||||||
|
if (!dirInfo.exists) {
|
||||||
|
await FileSystem.makeDirectoryAsync(CACHE_DIR, { intermediates: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
for (const item of items) {
|
||||||
|
if (!item.picker_base_url) {
|
||||||
|
results.push(item);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use the source_id as a safe filename
|
||||||
|
const safeId = (item.source_id || 'photo').replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||||
|
const ext = item.mime_type?.includes('png') ? '.png'
|
||||||
|
: item.mime_type?.includes('gif') ? '.gif'
|
||||||
|
: item.mime_type?.includes('webp') ? '.webp'
|
||||||
|
: '.jpg';
|
||||||
|
const localUri = `${CACHE_DIR}${safeId}${ext}`;
|
||||||
|
|
||||||
|
// Skip download if already cached
|
||||||
|
const fileInfo = await FileSystem.getInfoAsync(localUri);
|
||||||
|
if (fileInfo.exists && fileInfo.size > 0) {
|
||||||
|
results.push({ ...item, local_path: localUri });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download with auth header — append =d to get the full-res download
|
||||||
|
const downloadUrl = `${item.picker_base_url}=d`;
|
||||||
|
const download = await FileSystem.downloadAsync(downloadUrl, localUri, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (download.status === 200) {
|
||||||
|
results.push({ ...item, local_path: download.uri });
|
||||||
|
} else {
|
||||||
|
logError('Download Picker Photo', `HTTP ${download.status} for ${item.title}`);
|
||||||
|
results.push(item);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logError('Download Picker Photo', err);
|
||||||
|
results.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Full interactive flow: create session → open picker in browser →
|
* Full interactive flow: create session → open picker in browser →
|
||||||
* wait for user to select photos → fetch the selected items.
|
* wait for user to select photos → fetch the selected items.
|
||||||
@@ -339,11 +412,13 @@ class GoogleAUTH extends AuthServiceBase {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Fetch selected items
|
// 4. Fetch selected items and download to local cache
|
||||||
const result = await this._fetchPickerMediaItems(session.id, 100, null);
|
const result = await this._fetchPickerMediaItems(session.id, 100, null);
|
||||||
const mediaItems = this._normalisePickerItems(result.mediaItems);
|
let mediaItems = this._normalisePickerItems(result.mediaItems);
|
||||||
|
|
||||||
console.log(`Picker returned ${mediaItems.length} selected items`);
|
console.log(`Picker returned ${mediaItems.length} selected items, downloading…`);
|
||||||
|
mediaItems = await this.downloadPickerPhotos(mediaItems);
|
||||||
|
console.log(`Downloaded ${mediaItems.filter((m) => m.local_path).length} photos to cache`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mediaItems,
|
mediaItems,
|
||||||
@@ -366,7 +441,8 @@ class GoogleAUTH extends AuthServiceBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await this._fetchPickerMediaItems(sessionId, pageSize, pageToken);
|
const result = await this._fetchPickerMediaItems(sessionId, pageSize, pageToken);
|
||||||
const mediaItems = this._normalisePickerItems(result.mediaItems);
|
let mediaItems = this._normalisePickerItems(result.mediaItems);
|
||||||
|
mediaItems = await this.downloadPickerPhotos(mediaItems);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mediaItems,
|
mediaItems,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useRef, useState, useEffect, useCallback } from 'react';
|
import React, { useRef, useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Modal, View, Text, TouchableOpacity, Image, StyleSheet,
|
Modal, View, Text, TouchableOpacity, Image, StyleSheet,
|
||||||
Dimensions, FlatList,
|
Dimensions, FlatList,
|
||||||
@@ -57,22 +57,22 @@ const MediaViewer = ({ visible, media, allMedia, onClose }) => {
|
|||||||
}, 100);
|
}, 100);
|
||||||
}, [visible, media]);
|
}, [visible, media]);
|
||||||
|
|
||||||
const onMainScroll = useCallback(
|
const activeIndexRef = useRef(0);
|
||||||
(event) => {
|
|
||||||
|
const onMainScroll = (event) => {
|
||||||
const offsetX = event.nativeEvent.contentOffset.x;
|
const offsetX = event.nativeEvent.contentOffset.x;
|
||||||
const idx = Math.round(offsetX / SCREEN_WIDTH);
|
const idx = Math.round(offsetX / SCREEN_WIDTH);
|
||||||
if (idx >= 0 && idx < mediaList.length && idx !== activeIndex) {
|
if (idx >= 0 && idx < mediaList.length && idx !== activeIndexRef.current) {
|
||||||
|
activeIndexRef.current = idx;
|
||||||
setActiveIndex(idx);
|
setActiveIndex(idx);
|
||||||
setCurrentMedia(mediaList[idx]);
|
setCurrentMedia(mediaList[idx]);
|
||||||
if (thumbListRef.current) {
|
if (thumbListRef.current) {
|
||||||
try {
|
try {
|
||||||
thumbListRef.current.scrollToIndex({ index: idx, animated: true, viewPosition: 0.5 });
|
thumbListRef.current.scrollToIndex({ index: idx, animated: true, viewPosition: 0.5 });
|
||||||
} catch (e) { /* ignore */ }
|
} catch (_) { /* ignore */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
[activeIndex, mediaList, thumbListRef]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onThumbPress = (index) => {
|
const onThumbPress = (index) => {
|
||||||
setActiveIndex(index);
|
setActiveIndex(index);
|
||||||
@@ -189,9 +189,20 @@ const MediaViewer = ({ visible, media, allMedia, onClose }) => {
|
|||||||
<View style={styles.row}>
|
<View style={styles.row}>
|
||||||
<Text style={styles.label}>Source</Text>
|
<Text style={styles.label}>Source</Text>
|
||||||
<Text style={styles.value}>
|
<Text style={styles.value}>
|
||||||
{getServiceIcon(displayMedia.source_service)} {getServiceName(displayMedia.source_service)}
|
{displayMedia.sources && displayMedia.sources.length > 1
|
||||||
|
? displayMedia.sources.map((s) => `${getServiceIcon(s)} ${getServiceName(s)}`).join(' · ')
|
||||||
|
: `${getServiceIcon(displayMedia.source_service)} ${getServiceName(displayMedia.source_service)}`
|
||||||
|
}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
{displayMedia.sources && displayMedia.sources.length > 1 && (
|
||||||
|
<View style={styles.row}>
|
||||||
|
<Text style={styles.label}>Available on</Text>
|
||||||
|
<Text style={[styles.value, { color: '#8ab4f8' }]}>
|
||||||
|
{displayMedia.sources.length} services
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
{displayMedia.created_date && (
|
{displayMedia.created_date && (
|
||||||
<View style={styles.row}>
|
<View style={styles.row}>
|
||||||
<Text style={styles.label}>Date</Text>
|
<Text style={styles.label}>Date</Text>
|
||||||
|
|||||||
@@ -21,7 +21,31 @@ const PhotoGrid = ({
|
|||||||
const itemSize = (width - GRID_GAP * (columns + 1)) / columns;
|
const itemSize = (width - GRID_GAP * (columns + 1)) / columns;
|
||||||
|
|
||||||
const getServiceIcon = (type) =>
|
const getServiceIcon = (type) =>
|
||||||
({ GOOGLE_PHOTOS: '📷', ICLOUD_PHOTOS: '🍎', NAS: '💾' }[type] || '📱');
|
({ DEVICE: '📱', GOOGLE_PHOTOS: '📷', ICLOUD_PHOTOS: '🍎', NAS: '💾' }[type] || '📱');
|
||||||
|
|
||||||
|
const renderSourceBadges = (item) => {
|
||||||
|
const sources = item.sources && item.sources.length > 0
|
||||||
|
? item.sources
|
||||||
|
: [item.source_service];
|
||||||
|
|
||||||
|
if (sources.length === 1) {
|
||||||
|
// Single source — simple badge
|
||||||
|
return (
|
||||||
|
<View style={styles.sourceBadge}>
|
||||||
|
<Text style={{ fontSize: 10 }}>{getServiceIcon(sources[0])}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple sources — show all icons with a subtle indicator
|
||||||
|
return (
|
||||||
|
<View style={styles.multiSourceBadge}>
|
||||||
|
{sources.map((src, i) => (
|
||||||
|
<Text key={src + i} style={{ fontSize: 9 }}>{getServiceIcon(src)}</Text>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderItem = ({ item }) => (
|
const renderItem = ({ item }) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -41,9 +65,7 @@ const PhotoGrid = ({
|
|||||||
<Text style={{ color: '#fff', fontSize: 20 }}>▶</Text>
|
<Text style={{ color: '#fff', fontSize: 20 }}>▶</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<View style={styles.sourceBadge}>
|
{renderSourceBadges(item)}
|
||||||
<Text style={{ fontSize: 10 }}>{getServiceIcon(item.source_service)}</Text>
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -82,6 +104,11 @@ const styles = StyleSheet.create({
|
|||||||
position: 'absolute', bottom: 4, right: 4, backgroundColor: 'rgba(0,0,0,0.5)',
|
position: 'absolute', bottom: 4, right: 4, backgroundColor: 'rgba(0,0,0,0.5)',
|
||||||
borderRadius: 8, paddingHorizontal: 4, paddingVertical: 2,
|
borderRadius: 8, paddingHorizontal: 4, paddingVertical: 2,
|
||||||
},
|
},
|
||||||
|
multiSourceBadge: {
|
||||||
|
position: 'absolute', bottom: 4, right: 4, backgroundColor: 'rgba(0,0,0,0.6)',
|
||||||
|
borderRadius: 8, paddingHorizontal: 4, paddingVertical: 2,
|
||||||
|
flexDirection: 'row', gap: 2,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default PhotoGrid;
|
export default PhotoGrid;
|
||||||
|
|||||||
+69
-19
@@ -14,6 +14,62 @@ import MediaViewer from '../components/MediaViewer';
|
|||||||
import { logError } from '../services/ErrorHandler';
|
import { logError } from '../services/ErrorHandler';
|
||||||
import { TouchableOpacity } from 'react-native';
|
import { TouchableOpacity } from 'react-native';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge media items from different services.
|
||||||
|
* Items with the same filename (title) are merged into a single entry
|
||||||
|
* with a `sources` array listing every service it appears in.
|
||||||
|
* The best local_path is kept (prefer file:// from DEVICE over a cached
|
||||||
|
* Picker download, since device URIs are always fresh).
|
||||||
|
*/
|
||||||
|
const mergeMediaAcrossServices = (items) => {
|
||||||
|
// First pass: group by normalised filename
|
||||||
|
const byFilename = new Map();
|
||||||
|
const noFilename = [];
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const key = item.title?.toLowerCase().trim();
|
||||||
|
if (!key || key === 'untitled') {
|
||||||
|
noFilename.push({ ...item, sources: [item.source_service] });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (byFilename.has(key)) {
|
||||||
|
const existing = byFilename.get(key);
|
||||||
|
// Add this service to the sources list
|
||||||
|
if (!existing.sources.includes(item.source_service)) {
|
||||||
|
existing.sources.push(item.source_service);
|
||||||
|
}
|
||||||
|
// Prefer a local device path over a cached download
|
||||||
|
if (item.source_service === 'DEVICE' && item.local_path) {
|
||||||
|
existing.local_path = item.local_path;
|
||||||
|
}
|
||||||
|
// Keep the best local_path if the existing one is missing
|
||||||
|
if (!existing.local_path && item.local_path) {
|
||||||
|
existing.local_path = item.local_path;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
byFilename.set(key, { ...item, sources: [item.source_service] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second pass: also deduplicate by source_id for items with different titles
|
||||||
|
const bySourceId = new Map();
|
||||||
|
for (const item of [...byFilename.values(), ...noFilename]) {
|
||||||
|
const key = item.source_id || item.id || item.local_path || Math.random().toString();
|
||||||
|
if (bySourceId.has(key)) {
|
||||||
|
const existing = bySourceId.get(key);
|
||||||
|
for (const src of (item.sources || [])) {
|
||||||
|
if (!existing.sources.includes(src)) existing.sources.push(src);
|
||||||
|
}
|
||||||
|
if (!existing.local_path && item.local_path) existing.local_path = item.local_path;
|
||||||
|
} else {
|
||||||
|
bySourceId.set(key, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(bySourceId.values());
|
||||||
|
};
|
||||||
|
|
||||||
const HomeScreen = ({ navigation, route }) => {
|
const HomeScreen = ({ navigation, route }) => {
|
||||||
const userId = route?.params?.userId;
|
const userId = route?.params?.userId;
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
@@ -102,11 +158,11 @@ const HomeScreen = ({ navigation, route }) => {
|
|||||||
console.log('iCloud: placeholder — would fetch photos here');
|
console.log('iCloud: placeholder — would fetch photos here');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deduplicate
|
// ─── Merge & cross-reference across services ───
|
||||||
const unique = Array.from(
|
// Match items across sources by filename so we can show "this photo
|
||||||
new Map(newMedia.map((m) => [m.source_id || m.id || m.local_path, m])).values()
|
// exists on Device AND Google Photos" etc.
|
||||||
);
|
const merged = mergeMediaAcrossServices(newMedia);
|
||||||
setAllMedia(unique);
|
setAllMedia(merged);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading media:', err);
|
console.error('Error loading media:', err);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -158,13 +214,8 @@ const HomeScreen = ({ navigation, route }) => {
|
|||||||
setGoogleNextPage(result.nextPageToken || null);
|
setGoogleNextPage(result.nextPageToken || null);
|
||||||
setGoogleSessionId(result.sessionId || null);
|
setGoogleSessionId(result.sessionId || null);
|
||||||
|
|
||||||
// Merge into current view
|
// Merge into current view with cross-service matching
|
||||||
setAllMedia((prev) => {
|
setAllMedia((prev) => mergeMediaAcrossServices([...prev, ...result.mediaItems]));
|
||||||
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`);
|
console.log(`Imported ${result.mediaItems.length} photos from Google`);
|
||||||
}
|
}
|
||||||
@@ -188,7 +239,11 @@ const HomeScreen = ({ navigation, route }) => {
|
|||||||
const filterMedia = () => {
|
const filterMedia = () => {
|
||||||
let filtered = allMedia;
|
let filtered = allMedia;
|
||||||
if (selectedFilter !== 'all') {
|
if (selectedFilter !== 'all') {
|
||||||
filtered = allMedia.filter((m) => m.source_service === selectedFilter);
|
// Check both source_service and the sources array for cross-service items
|
||||||
|
filtered = allMedia.filter((m) =>
|
||||||
|
m.source_service === selectedFilter ||
|
||||||
|
(m.sources && m.sources.includes(selectedFilter))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
filtered.sort((a, b) => new Date(b.created_date || 0) - new Date(a.created_date || 0));
|
filtered.sort((a, b) => new Date(b.created_date || 0) - new Date(a.created_date || 0));
|
||||||
setFilteredMedia(filtered);
|
setFilteredMedia(filtered);
|
||||||
@@ -233,12 +288,7 @@ const HomeScreen = ({ navigation, route }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (moreMedia.length) {
|
if (moreMedia.length) {
|
||||||
setAllMedia((prev) => {
|
setAllMedia((prev) => mergeMediaAcrossServices([...prev, ...moreMedia]));
|
||||||
const combined = [...prev, ...moreMedia];
|
|
||||||
return Array.from(
|
|
||||||
new Map(combined.map((m) => [m.source_id || m.id || m.local_path, m])).values()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading more photos:', err);
|
console.error('Error loading more photos:', err);
|
||||||
|
|||||||
+175
-120
@@ -1,11 +1,19 @@
|
|||||||
|
// noinspection SqlNoDataSourceInspection,SqlResolve
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database service — all tables are created at runtime by expo-sqlite.
|
||||||
|
* The IDE cannot resolve them statically; the SQL warnings are safe to ignore.
|
||||||
|
*/
|
||||||
import * as SQLite from 'expo-sqlite';
|
import * as SQLite from 'expo-sqlite';
|
||||||
import CredentialEncryption from './CredentialEncryption';
|
import CredentialEncryption from './CredentialEncryption';
|
||||||
|
import { logError } from './ErrorHandler';
|
||||||
|
|
||||||
let db = null;
|
let db = null;
|
||||||
let initPromise = null;
|
let initPromise = null;
|
||||||
|
|
||||||
const DatabaseService = {
|
const DatabaseService = {
|
||||||
// Open the database and create all tables
|
// ─── Initialisation ────────────────────────────────────────
|
||||||
|
|
||||||
initDatabase: async () => {
|
initDatabase: async () => {
|
||||||
if (initPromise) return initPromise;
|
if (initPromise) return initPromise;
|
||||||
|
|
||||||
@@ -14,12 +22,17 @@ const DatabaseService = {
|
|||||||
if (db) return true;
|
if (db) return true;
|
||||||
|
|
||||||
if (typeof SQLite.openDatabaseAsync !== 'function') {
|
if (typeof SQLite.openDatabaseAsync !== 'function') {
|
||||||
throw new Error('SQLite openDatabaseAsync API not available');
|
logError('DB Init', 'SQLite openDatabaseAsync API not available');
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
db = await SQLite.openDatabaseAsync('PhotosApp.db');
|
db = await SQLite.openDatabaseAsync('PhotosApp.db');
|
||||||
if (!db) throw new Error('SQLite openDatabaseAsync returned null');
|
if (!db) {
|
||||||
|
logError('DB Init', 'SQLite openDatabaseAsync returned null');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create all tables in one batch
|
||||||
await db.execAsync(`
|
await db.execAsync(`
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -27,10 +40,7 @@ const DatabaseService = {
|
|||||||
email TEXT UNIQUE NOT NULL,
|
email TEXT UNIQUE NOT NULL,
|
||||||
password TEXT NOT NULL,
|
password TEXT NOT NULL,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
)
|
);
|
||||||
`);
|
|
||||||
|
|
||||||
await db.execAsync(`
|
|
||||||
CREATE TABLE IF NOT EXISTS linked_accounts (
|
CREATE TABLE IF NOT EXISTS linked_accounts (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
user_id INTEGER NOT NULL,
|
user_id INTEGER NOT NULL,
|
||||||
@@ -40,10 +50,7 @@ const DatabaseService = {
|
|||||||
is_default INTEGER DEFAULT 0,
|
is_default INTEGER DEFAULT 0,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY(user_id) REFERENCES users(id)
|
FOREIGN KEY(user_id) REFERENCES users(id)
|
||||||
)
|
);
|
||||||
`);
|
|
||||||
|
|
||||||
await db.execAsync(`
|
|
||||||
CREATE TABLE IF NOT EXISTS media (
|
CREATE TABLE IF NOT EXISTS media (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
user_id INTEGER NOT NULL,
|
user_id INTEGER NOT NULL,
|
||||||
@@ -58,20 +65,20 @@ const DatabaseService = {
|
|||||||
synced INTEGER DEFAULT 0,
|
synced INTEGER DEFAULT 0,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY(user_id) REFERENCES users(id)
|
FOREIGN KEY(user_id) REFERENCES users(id)
|
||||||
)
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Add optional columns (safe migration — errors ignored if columns already exist)
|
// Safe migrations — errors ignored if columns already exist
|
||||||
try { await db.execAsync(`ALTER TABLE users ADD COLUMN auto_backup_services TEXT DEFAULT '[]'`); } catch (_) {}
|
try { await db.execAsync("ALTER TABLE users ADD COLUMN auto_backup_services TEXT DEFAULT '[]'"); } catch (_) {}
|
||||||
try { await db.execAsync(`ALTER TABLE users ADD COLUMN theme_preference TEXT DEFAULT 'light'`); } catch (_) {}
|
try { await db.execAsync("ALTER TABLE users ADD COLUMN theme_preference TEXT DEFAULT 'light'"); } catch (_) {}
|
||||||
|
|
||||||
console.log('Database initialized successfully');
|
console.log('Database initialized successfully');
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Database initialization error:', error);
|
logError('DB Init', error);
|
||||||
db = null;
|
db = null;
|
||||||
initPromise = null;
|
initPromise = null;
|
||||||
throw error;
|
return false;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -80,12 +87,18 @@ const DatabaseService = {
|
|||||||
|
|
||||||
ensureDb: async () => {
|
ensureDb: async () => {
|
||||||
await DatabaseService.initDatabase();
|
await DatabaseService.initDatabase();
|
||||||
if (!db) throw new Error('Database not initialized');
|
if (!db) {
|
||||||
|
logError('DB', 'Database not initialized');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ─── Users ─────────────────────────────────────────────────
|
||||||
|
|
||||||
createUser: async (username, email, password) => {
|
createUser: async (username, email, password) => {
|
||||||
try {
|
try {
|
||||||
await DatabaseService.ensureDb();
|
if (!(await DatabaseService.ensureDb())) return null;
|
||||||
const hashedPassword = await CredentialEncryption.hashPassword(password);
|
const hashedPassword = await CredentialEncryption.hashPassword(password);
|
||||||
const result = await db.runAsync(
|
const result = await db.runAsync(
|
||||||
'INSERT INTO users (username, email, password) VALUES (?, ?, ?)',
|
'INSERT INTO users (username, email, password) VALUES (?, ?, ?)',
|
||||||
@@ -93,98 +106,111 @@ const DatabaseService = {
|
|||||||
);
|
);
|
||||||
return result.lastInsertRowId;
|
return result.lastInsertRowId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating user:', error);
|
logError('Create User', error);
|
||||||
throw error;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Authenticate by username or email
|
|
||||||
loginUser: async (identifier, password) => {
|
loginUser: async (identifier, password) => {
|
||||||
try {
|
try {
|
||||||
await DatabaseService.ensureDb();
|
if (!(await DatabaseService.ensureDb())) return null;
|
||||||
const row = await db.getFirstAsync(
|
const row = await db.getFirstAsync(
|
||||||
'SELECT * FROM users WHERE username = ? OR email = ?',
|
'SELECT * FROM users WHERE username = ? OR email = ?',
|
||||||
[identifier, identifier]
|
[identifier, identifier]
|
||||||
);
|
);
|
||||||
if (!row) return null;
|
if (!row) return null;
|
||||||
|
|
||||||
const valid = await CredentialEncryption.verifyPassword(password, row.password);
|
const valid = await CredentialEncryption.verifyPassword(password, row.password);
|
||||||
if (!valid) return null;
|
return valid ? row : null;
|
||||||
|
|
||||||
return row;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error logging in user:', error);
|
logError('Login User', error);
|
||||||
throw error;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
userExists: async (username, email) => {
|
userExists: async (username, email) => {
|
||||||
try {
|
try {
|
||||||
await DatabaseService.ensureDb();
|
if (!(await DatabaseService.ensureDb())) return false;
|
||||||
const row = await db.getFirstAsync(
|
const row = await db.getFirstAsync(
|
||||||
'SELECT id FROM users WHERE username = ? OR email = ?',
|
'SELECT id FROM users WHERE username = ? OR email = ?',
|
||||||
[username, email]
|
[username, email]
|
||||||
);
|
);
|
||||||
return !!row;
|
return !!row;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking if user exists:', error);
|
logError('User Exists', error);
|
||||||
throw error;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getUserById: async (userId) => {
|
getUserById: async (userId) => {
|
||||||
try {
|
try {
|
||||||
await DatabaseService.ensureDb();
|
if (!(await DatabaseService.ensureDb())) return null;
|
||||||
if (!userId && userId !== 0) throw new Error('User ID is required');
|
if (!userId && userId !== 0) {
|
||||||
|
logError('Get User', 'User ID is required');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (await db.getFirstAsync('SELECT * FROM users WHERE id = ?', [userId])) || null;
|
return (await db.getFirstAsync('SELECT * FROM users WHERE id = ?', [userId])) || null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting user:', error);
|
logError('Get User', error);
|
||||||
throw error;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Link an external service account (Google, iCloud, NAS) to a local user
|
// ─── Linked Accounts ──────────────────────────────────────
|
||||||
|
|
||||||
linkAccount: async (userId, serviceType, accountData, isDefault = false) => {
|
linkAccount: async (userId, serviceType, accountData, isDefault = false) => {
|
||||||
try {
|
try {
|
||||||
await DatabaseService.ensureDb();
|
if (!(await DatabaseService.ensureDb())) return null;
|
||||||
if (!userId) throw new Error('User ID is required');
|
if (!userId || !serviceType || !accountData || typeof accountData !== 'object') {
|
||||||
if (!serviceType) throw new Error('Service type is required');
|
logError('Link Account', 'userId, serviceType, and accountData object are required');
|
||||||
if (!accountData || typeof accountData !== 'object') throw new Error('Account data must be a valid object');
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const encryptedData = CredentialEncryption.encrypt(accountData);
|
const encryptedData = CredentialEncryption.encrypt(accountData);
|
||||||
if (!encryptedData) throw new Error('Encryption returned empty data');
|
if (!encryptedData) {
|
||||||
|
logError('Link Account', 'Encryption returned empty data');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Replace any existing link for the same service type
|
// Replace any existing link for the same service type
|
||||||
await db.runAsync('DELETE FROM linked_accounts WHERE user_id = ? AND service_type = ?', [userId, serviceType]);
|
await db.runAsync(
|
||||||
|
'DELETE FROM linked_accounts WHERE user_id = ? AND service_type = ?',
|
||||||
|
[userId, serviceType]
|
||||||
|
);
|
||||||
const result = await db.runAsync(
|
const result = await db.runAsync(
|
||||||
'INSERT INTO linked_accounts (user_id, service_type, account_data, is_default) VALUES (?, ?, ?, ?)',
|
'INSERT INTO linked_accounts (user_id, service_type, account_data, is_default) VALUES (?, ?, ?, ?)',
|
||||||
[userId, serviceType, encryptedData, isDefault ? 1 : 0]
|
[userId, serviceType, encryptedData, isDefault ? 1 : 0]
|
||||||
);
|
);
|
||||||
return result.lastInsertRowId;
|
return result.lastInsertRowId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error linking account:', error);
|
logError('Link Account', error);
|
||||||
throw new Error('Failed to link account: ' + error.message);
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getLinkedAccounts: async (userId) => {
|
getLinkedAccounts: async (userId) => {
|
||||||
try {
|
try {
|
||||||
await DatabaseService.ensureDb();
|
if (!(await DatabaseService.ensureDb())) return [];
|
||||||
if (!userId && userId !== 0) throw new Error('User ID is required');
|
if (!userId && userId !== 0) {
|
||||||
|
logError('Get Linked Accounts', 'User ID is required');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const rows = await db.getAllAsync(
|
const rows = await db.getAllAsync(
|
||||||
'SELECT * FROM linked_accounts WHERE user_id = ? AND is_enabled = 1', [userId]
|
'SELECT * FROM linked_accounts WHERE user_id = ? AND is_enabled = 1',
|
||||||
|
[userId]
|
||||||
);
|
);
|
||||||
return rows.map((row) => ({ ...row, account_data: CredentialEncryption.decrypt(row.account_data) }));
|
return rows.map((row) => ({
|
||||||
|
...row,
|
||||||
|
account_data: CredentialEncryption.decrypt(row.account_data),
|
||||||
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting linked accounts:', error);
|
logError('Get Linked Accounts', error);
|
||||||
throw error;
|
return [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getLinkedAccountByService: async (userId, serviceType) => {
|
getLinkedAccountByService: async (userId, serviceType) => {
|
||||||
try {
|
try {
|
||||||
await DatabaseService.ensureDb();
|
if (!(await DatabaseService.ensureDb())) return null;
|
||||||
const row = await db.getFirstAsync(
|
const row = await db.getFirstAsync(
|
||||||
'SELECT * FROM linked_accounts WHERE user_id = ? AND service_type = ? AND is_enabled = 1',
|
'SELECT * FROM linked_accounts WHERE user_id = ? AND service_type = ? AND is_enabled = 1',
|
||||||
[userId, serviceType]
|
[userId, serviceType]
|
||||||
@@ -192,40 +218,83 @@ const DatabaseService = {
|
|||||||
if (!row) return null;
|
if (!row) return null;
|
||||||
return { ...row, account_data: CredentialEncryption.decrypt(row.account_data) };
|
return { ...row, account_data: CredentialEncryption.decrypt(row.account_data) };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting linked account:', error);
|
logError('Get Linked Account By Service', error);
|
||||||
throw error;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setDefaultService: async (userId, accountId) => {
|
setDefaultService: async (userId, accountId) => {
|
||||||
try {
|
try {
|
||||||
await DatabaseService.ensureDb();
|
if (!(await DatabaseService.ensureDb())) return;
|
||||||
await db.runAsync('UPDATE linked_accounts SET is_default = 0 WHERE user_id = ?', [userId]);
|
await db.runAsync('UPDATE linked_accounts SET is_default = 0 WHERE user_id = ?', [userId]);
|
||||||
await db.runAsync('UPDATE linked_accounts SET is_default = 1 WHERE id = ? AND user_id = ?', [accountId, userId]);
|
await db.runAsync('UPDATE linked_accounts SET is_default = 1 WHERE id = ? AND user_id = ?', [accountId, userId]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting default service:', error);
|
logError('Set Default Service', error);
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultService: async (userId) => {
|
getDefaultService: async (userId) => {
|
||||||
try {
|
try {
|
||||||
await DatabaseService.ensureDb();
|
if (!(await DatabaseService.ensureDb())) return null;
|
||||||
const row = await db.getFirstAsync(
|
const row = await db.getFirstAsync(
|
||||||
'SELECT * FROM linked_accounts WHERE user_id = ? AND is_default = 1', [userId]
|
'SELECT * FROM linked_accounts WHERE user_id = ? AND is_default = 1',
|
||||||
|
[userId]
|
||||||
);
|
);
|
||||||
if (!row) return null;
|
if (!row) return null;
|
||||||
return { ...row, account_data: CredentialEncryption.decrypt(row.account_data) };
|
return { ...row, account_data: CredentialEncryption.decrypt(row.account_data) };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting default service:', error);
|
logError('Get Default Service', error);
|
||||||
throw error;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggleAccountEnabled: async (accountId, enabled) => {
|
||||||
|
try {
|
||||||
|
if (!(await DatabaseService.ensureDb())) return;
|
||||||
|
await db.runAsync(
|
||||||
|
'UPDATE linked_accounts SET is_enabled = ? WHERE id = ?',
|
||||||
|
[enabled ? 1 : 0, accountId]
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logError('Toggle Account', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
removeLinkedAccount: async (accountId) => {
|
||||||
|
try {
|
||||||
|
if (!(await DatabaseService.ensureDb())) return;
|
||||||
|
await db.runAsync('DELETE FROM linked_accounts WHERE id = ?', [accountId]);
|
||||||
|
} catch (error) {
|
||||||
|
logError('Remove Account', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getAllLinkedAccounts: async (userId) => {
|
||||||
|
try {
|
||||||
|
if (!(await DatabaseService.ensureDb())) return [];
|
||||||
|
const rows = await db.getAllAsync(
|
||||||
|
'SELECT * FROM linked_accounts WHERE user_id = ?',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
return rows.map((row) => ({
|
||||||
|
...row,
|
||||||
|
account_data: CredentialEncryption.decrypt(row.account_data),
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
logError('Get All Linked Accounts', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ─── Media ─────────────────────────────────────────────────
|
||||||
|
|
||||||
addMedia: async (userId, sourceService, sourceId, title, description, localPath, remotePath, mediaType, createdDate) => {
|
addMedia: async (userId, sourceService, sourceId, title, description, localPath, remotePath, mediaType, createdDate) => {
|
||||||
try {
|
try {
|
||||||
await DatabaseService.ensureDb();
|
if (!(await DatabaseService.ensureDb())) return null;
|
||||||
if (!userId || !sourceService) throw new Error('userId and sourceService are required');
|
if (!userId || !sourceService) {
|
||||||
|
logError('Add Media', 'userId and sourceService are required');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const result = await db.runAsync(
|
const result = await db.runAsync(
|
||||||
`INSERT INTO media (user_id, source_service, source_id, title, description, local_path, remote_path, media_type, created_date)
|
`INSERT INTO media (user_id, source_service, source_id, title, description, local_path, remote_path, media_type, created_date)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
@@ -234,8 +303,8 @@ const DatabaseService = {
|
|||||||
);
|
);
|
||||||
return result.lastInsertRowId;
|
return result.lastInsertRowId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error adding media:', error);
|
logError('Add Media', error);
|
||||||
throw error;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -245,10 +314,10 @@ const DatabaseService = {
|
|||||||
*/
|
*/
|
||||||
saveMediaItem: async (userId, item) => {
|
saveMediaItem: async (userId, item) => {
|
||||||
try {
|
try {
|
||||||
await DatabaseService.ensureDb();
|
if (!(await DatabaseService.ensureDb())) return null;
|
||||||
if (!userId || !item.source_service) return;
|
if (!userId || !item?.source_service) return null;
|
||||||
|
|
||||||
// Check for existing entry to avoid duplicates
|
// Skip if already exists
|
||||||
if (item.source_id) {
|
if (item.source_id) {
|
||||||
const existing = await db.getFirstAsync(
|
const existing = await db.getFirstAsync(
|
||||||
'SELECT id FROM media WHERE user_id = ? AND source_id = ?',
|
'SELECT id FROM media WHERE user_id = ? AND source_id = ?',
|
||||||
@@ -266,108 +335,94 @@ const DatabaseService = {
|
|||||||
);
|
);
|
||||||
return result.lastInsertRowId;
|
return result.lastInsertRowId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving media item:', error);
|
logError('Save Media Item', error);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getMediaByUser: async (userId) => {
|
getMediaByUser: async (userId) => {
|
||||||
try {
|
try {
|
||||||
await DatabaseService.ensureDb();
|
if (!(await DatabaseService.ensureDb())) return [];
|
||||||
if (!userId && userId !== 0) throw new Error('User ID is required');
|
if (!userId && userId !== 0) {
|
||||||
return await db.getAllAsync('SELECT * FROM media WHERE user_id = ? ORDER BY created_date DESC', [userId]);
|
logError('Get Media', 'User ID is required');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return await db.getAllAsync(
|
||||||
|
'SELECT * FROM media WHERE user_id = ? ORDER BY created_date DESC',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting media:', error);
|
logError('Get Media', error);
|
||||||
throw error;
|
return [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getMediaByService: async (userId, serviceType) => {
|
getMediaByService: async (userId, serviceType) => {
|
||||||
try {
|
try {
|
||||||
await DatabaseService.ensureDb();
|
if (!(await DatabaseService.ensureDb())) return [];
|
||||||
return await db.getAllAsync(
|
return await db.getAllAsync(
|
||||||
'SELECT * FROM media WHERE user_id = ? AND source_service = ? ORDER BY created_date DESC',
|
'SELECT * FROM media WHERE user_id = ? AND source_service = ? ORDER BY created_date DESC',
|
||||||
[userId, serviceType]
|
[userId, serviceType]
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting media by service:', error);
|
logError('Get Media By Service', error);
|
||||||
throw error;
|
return [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
searchMedia: async (userId, query) => {
|
searchMedia: async (userId, query) => {
|
||||||
try {
|
try {
|
||||||
await DatabaseService.ensureDb();
|
if (!(await DatabaseService.ensureDb())) return [];
|
||||||
const searchTerm = `%${query}%`;
|
const searchTerm = `%${query}%`;
|
||||||
return await db.getAllAsync(
|
return await db.getAllAsync(
|
||||||
'SELECT * FROM media WHERE user_id = ? AND (title LIKE ? OR description LIKE ?) ORDER BY created_date DESC',
|
'SELECT * FROM media WHERE user_id = ? AND (title LIKE ? OR description LIKE ?) ORDER BY created_date DESC',
|
||||||
[userId, searchTerm, searchTerm]
|
[userId, searchTerm, searchTerm]
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error searching media:', error);
|
logError('Search Media', error);
|
||||||
throw error;
|
return [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ─── Settings ──────────────────────────────────────────────
|
||||||
|
|
||||||
getAutoBackupServices: async (userId) => {
|
getAutoBackupServices: async (userId) => {
|
||||||
try {
|
try {
|
||||||
await DatabaseService.ensureDb();
|
if (!(await DatabaseService.ensureDb())) return [];
|
||||||
const row = await db.getFirstAsync('SELECT auto_backup_services FROM users WHERE id = ?', [userId]);
|
const row = await db.getFirstAsync(
|
||||||
|
'SELECT auto_backup_services FROM users WHERE id = ?',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
if (!row?.auto_backup_services) return [];
|
if (!row?.auto_backup_services) return [];
|
||||||
return JSON.parse(row.auto_backup_services);
|
return JSON.parse(row.auto_backup_services);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting auto backup services:', error);
|
logError('Get Auto Backup', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setAutoBackupServices: async (userId, services) => {
|
setAutoBackupServices: async (userId, services) => {
|
||||||
try {
|
try {
|
||||||
await DatabaseService.ensureDb();
|
if (!(await DatabaseService.ensureDb())) return;
|
||||||
await db.runAsync('UPDATE users SET auto_backup_services = ? WHERE id = ?', [JSON.stringify(services), userId]);
|
await db.runAsync(
|
||||||
|
'UPDATE users SET auto_backup_services = ? WHERE id = ?',
|
||||||
|
[JSON.stringify(services), userId]
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting auto backup services:', error);
|
logError('Set Auto Backup', error);
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleAccountEnabled: async (accountId, enabled) => {
|
// ─── Lifecycle ─────────────────────────────────────────────
|
||||||
try {
|
|
||||||
await DatabaseService.ensureDb();
|
|
||||||
await db.runAsync('UPDATE linked_accounts SET is_enabled = ? WHERE id = ?', [enabled ? 1 : 0, accountId]);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error toggling account:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
removeLinkedAccount: async (accountId) => {
|
|
||||||
try {
|
|
||||||
await DatabaseService.ensureDb();
|
|
||||||
await db.runAsync('DELETE FROM linked_accounts WHERE id = ?', [accountId]);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error removing account:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getAllLinkedAccounts: async (userId) => {
|
|
||||||
try {
|
|
||||||
await DatabaseService.ensureDb();
|
|
||||||
const rows = await db.getAllAsync('SELECT * FROM linked_accounts WHERE user_id = ?', [userId]);
|
|
||||||
return rows.map((row) => ({ ...row, account_data: CredentialEncryption.decrypt(row.account_data) }));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error getting all linked accounts:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
closeDatabase: async () => {
|
closeDatabase: async () => {
|
||||||
try {
|
try {
|
||||||
if (db?.closeAsync) await db.closeAsync();
|
if (db?.closeAsync) await db.closeAsync();
|
||||||
|
} catch (error) {
|
||||||
|
logError('Close DB', error);
|
||||||
|
} finally {
|
||||||
db = null;
|
db = null;
|
||||||
initPromise = null;
|
initPromise = null;
|
||||||
} catch (error) {
|
|
||||||
console.error('Error closing database:', error);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in new issue
Block a user