mirror of
https://github.com/mudabbir-ahmad/Media-Aggregation-and-Sorting-System.git
synced 2026-10-07 19:40:20 +00:00
added service health indicators for Google Photos, iCloud, and NAS; implemented health check logic in settings and onboarding screens
This commit is contained in:
1 parent
6f4ec66e2e
commit
9519b32d68
3 files changed
+242
-39
No files matched your search
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
|
||||
/**
|
||||
* Circular health indicator dot
|
||||
*
|
||||
* status values:
|
||||
* 'inactive' → grey (not used / not connected)
|
||||
* 'healthy' → green (connected and working)
|
||||
* 'error' → red (connected but errors fetching data)
|
||||
* 'checking' → amber (currently checking)
|
||||
*/
|
||||
const COLORS = {
|
||||
inactive: '#9e9e9e',
|
||||
healthy: '#34a853',
|
||||
error: '#d93025',
|
||||
checking: '#fbbc04',
|
||||
};
|
||||
|
||||
const ServiceHealthIndicator = ({ status = 'inactive', size = 10 }) => (
|
||||
<View
|
||||
style={[
|
||||
styles.dot,
|
||||
{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: size / 2,
|
||||
backgroundColor: COLORS[status] || COLORS.inactive,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
dot: {
|
||||
marginRight: 8,
|
||||
},
|
||||
});
|
||||
|
||||
export default ServiceHealthIndicator;
|
||||
|
||||
@@ -8,6 +8,7 @@ import DatabaseService from '../services/DatabaseService';
|
||||
import GoogleAUTH from '../AUTH/GoogleAUTH';
|
||||
import NASAUTH from '../AUTH/NASAUTH';
|
||||
import NASSetupInstructions from '../components/NASSetupInstructions';
|
||||
import ServiceHealthIndicator from '../components/ServiceHealthIndicator';
|
||||
|
||||
const OnboardingScreen = ({ navigation, route }) => {
|
||||
const { userId } = route.params;
|
||||
@@ -25,6 +26,11 @@ const OnboardingScreen = ({ navigation, route }) => {
|
||||
const [iosConnected, setIosConnected] = useState(false);
|
||||
const [nasConnected, setNasConnected] = useState(false);
|
||||
|
||||
// Service health: 'inactive' | 'healthy' | 'error' | 'checking'
|
||||
const [googleHealth, setGoogleHealth] = useState('inactive');
|
||||
const [iosHealth, setIosHealth] = useState('inactive');
|
||||
const [nasHealth, setNasHealth] = useState('inactive');
|
||||
|
||||
// NAS config
|
||||
const [nasHost, setNasHost] = useState('');
|
||||
const [nasPort, setNasPort] = useState('22');
|
||||
@@ -52,6 +58,7 @@ const OnboardingScreen = ({ navigation, route }) => {
|
||||
const handleGoogleAuth = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setGoogleHealth('checking');
|
||||
const googleAuth = new GoogleAUTH();
|
||||
const result = await googleAuth.authenticate();
|
||||
|
||||
@@ -62,9 +69,11 @@ const OnboardingScreen = ({ navigation, route }) => {
|
||||
});
|
||||
|
||||
setGoogleConnected(true);
|
||||
setGoogleHealth('healthy');
|
||||
Alert.alert('Success', 'Google Photos connected!');
|
||||
} catch (error) {
|
||||
console.error('Google auth error:', error);
|
||||
setGoogleHealth('error');
|
||||
Alert.alert('Error', 'Failed to connect Google Photos: ' + error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -74,14 +83,17 @@ const OnboardingScreen = ({ navigation, route }) => {
|
||||
const handleIosAuth = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setIosHealth('checking');
|
||||
await DatabaseService.linkAccount(userId, 'ICLOUD_PHOTOS', {
|
||||
status: 'placeholder',
|
||||
note: 'iCloud integration pending Apple API access',
|
||||
});
|
||||
setIosConnected(true);
|
||||
setIosHealth('healthy');
|
||||
Alert.alert('Info', 'iCloud Photos registered. Full integration coming soon.');
|
||||
} catch (error) {
|
||||
console.error('iCloud auth error:', error);
|
||||
setIosHealth('error');
|
||||
Alert.alert('Error', 'Failed to set up iCloud: ' + error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -95,6 +107,7 @@ const OnboardingScreen = ({ navigation, route }) => {
|
||||
}
|
||||
try {
|
||||
setLoading(true);
|
||||
setNasHealth('checking');
|
||||
const nasAuth = new NASAUTH();
|
||||
await nasAuth.authenticate(
|
||||
nasHost.trim(), parseInt(nasPort) || 22,
|
||||
@@ -107,9 +120,11 @@ const OnboardingScreen = ({ navigation, route }) => {
|
||||
photoFolder: nasPhotoFolder.trim(),
|
||||
});
|
||||
setNasConnected(true);
|
||||
setNasHealth('healthy');
|
||||
Alert.alert('Success', 'NAS connected!');
|
||||
} catch (error) {
|
||||
console.error('NAS auth error:', error);
|
||||
setNasHealth('error');
|
||||
Alert.alert('Error', 'Failed to connect NAS: ' + error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -187,6 +202,7 @@ const OnboardingScreen = ({ navigation, route }) => {
|
||||
{googleEnabled && (
|
||||
<View style={[styles.configCard, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
||||
<View style={styles.configHeader}>
|
||||
<ServiceHealthIndicator status={googleHealth} size={10} />
|
||||
<Text style={styles.configIcon}>📷</Text>
|
||||
<Text style={[styles.configTitle, { color: theme.text }]}>Google Photos</Text>
|
||||
{googleConnected && <Text style={[styles.connectedBadge, { color: theme.accent }]}>✓ Connected</Text>}
|
||||
@@ -205,6 +221,7 @@ const OnboardingScreen = ({ navigation, route }) => {
|
||||
{iosEnabled && (
|
||||
<View style={[styles.configCard, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
||||
<View style={styles.configHeader}>
|
||||
<ServiceHealthIndicator status={iosHealth} size={10} />
|
||||
<Text style={styles.configIcon}>🍎</Text>
|
||||
<Text style={[styles.configTitle, { color: theme.text }]}>iCloud Photos</Text>
|
||||
{iosConnected && <Text style={[styles.connectedBadge, { color: theme.accent }]}>✓ Connected</Text>}
|
||||
@@ -223,6 +240,7 @@ const OnboardingScreen = ({ navigation, route }) => {
|
||||
{nasEnabled && (
|
||||
<View style={[styles.configCard, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
||||
<View style={styles.configHeader}>
|
||||
<ServiceHealthIndicator status={nasHealth} size={10} />
|
||||
<Text style={styles.configIcon}>💾</Text>
|
||||
<Text style={[styles.configTitle, { color: theme.text }]}>NAS Storage</Text>
|
||||
{nasConnected && <Text style={[styles.connectedBadge, { color: theme.accent }]}>✓ Connected</Text>}
|
||||
|
||||
+183
-39
@@ -7,16 +7,21 @@ import { useFocusEffect } from '@react-navigation/native';
|
||||
import { useTheme } from '../services/ThemeContext';
|
||||
import DatabaseService from '../services/DatabaseService';
|
||||
import NASSetupInstructions from '../components/NASSetupInstructions';
|
||||
import ServiceHealthIndicator from '../components/ServiceHealthIndicator';
|
||||
import NASAUTH from '../AUTH/NASAUTH';
|
||||
import GoogleAUTH from '../AUTH/GoogleAUTH';
|
||||
|
||||
const SettingsScreen = ({ navigation, route }) => {
|
||||
const userId = route?.params?.userId;
|
||||
const { theme, themeName, setThemeName, themes } = useTheme();
|
||||
const { theme, themeName, setThemeName } = useTheme();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [accounts, setAccounts] = useState([]);
|
||||
const [autoBackup, setAutoBackup] = useState([]);
|
||||
const [nasOffload, setNasOffload] = useState(false);
|
||||
|
||||
// Per-service health: { GOOGLE_PHOTOS: 'healthy'|'error'|'inactive', ... }
|
||||
const [serviceHealth, setServiceHealth] = useState({});
|
||||
|
||||
// NAS edit fields
|
||||
const [editingNas, setEditingNas] = useState(false);
|
||||
const [nasHost, setNasHost] = useState('');
|
||||
@@ -45,20 +50,131 @@ const SettingsScreen = ({ navigation, route }) => {
|
||||
setNasUsername(nasAcc.account_data.username || '');
|
||||
setNasPhotoFolder(nasAcc.account_data.photoFolder || '/home/user/photos');
|
||||
}
|
||||
|
||||
// Check health for each enabled account
|
||||
checkAllServiceHealth(allAccounts);
|
||||
} catch (err) {
|
||||
console.error('Error loading settings:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── Health checks ─────────────────────────────────────────
|
||||
const checkAllServiceHealth = async (accs) => {
|
||||
const health = {};
|
||||
for (const acc of accs) {
|
||||
if (!acc.is_enabled) {
|
||||
health[acc.service_type] = 'inactive';
|
||||
continue;
|
||||
}
|
||||
health[acc.service_type] = 'checking';
|
||||
setServiceHealth((prev) => ({ ...prev, [acc.service_type]: 'checking' }));
|
||||
health[acc.service_type] = await checkSingleHealth(acc);
|
||||
}
|
||||
setServiceHealth(health);
|
||||
};
|
||||
|
||||
const checkSingleHealth = async (acc) => {
|
||||
try {
|
||||
if (acc.service_type === 'GOOGLE_PHOTOS') {
|
||||
const gAuth = new GoogleAUTH();
|
||||
gAuth.accessToken = acc.account_data?.accessToken;
|
||||
gAuth.refreshToken = acc.account_data?.refreshToken;
|
||||
gAuth.expiresAt = Date.now() + 3600000;
|
||||
const res = await gAuth.getPhotos(1);
|
||||
return res?.mediaItems !== undefined ? 'healthy' : 'error';
|
||||
}
|
||||
if (acc.service_type === 'NAS') {
|
||||
const nasAuth = new NASAUTH();
|
||||
const d = acc.account_data || {};
|
||||
await nasAuth.authenticate(d.host, d.port, d.username, d.password, d.photoFolder);
|
||||
await nasAuth.revokeAccess();
|
||||
return 'healthy';
|
||||
}
|
||||
if (acc.service_type === 'ICLOUD_PHOTOS') {
|
||||
// Placeholder – always "healthy" since it's a stub
|
||||
return 'healthy';
|
||||
}
|
||||
return 'inactive';
|
||||
} catch {
|
||||
return 'error';
|
||||
}
|
||||
};
|
||||
|
||||
const recheckHealth = async (acc) => {
|
||||
setServiceHealth((prev) => ({ ...prev, [acc.service_type]: 'checking' }));
|
||||
const status = await checkSingleHealth(acc);
|
||||
setServiceHealth((prev) => ({ ...prev, [acc.service_type]: status }));
|
||||
};
|
||||
|
||||
// ─── Service actions ───────────────────────────────────────
|
||||
const toggleService = async (accountId, currentEnabled) => {
|
||||
try {
|
||||
await DatabaseService.toggleAccountEnabled(accountId, !currentEnabled);
|
||||
await loadData();
|
||||
} catch (err) {
|
||||
} catch {
|
||||
Alert.alert('Error', 'Failed to update service');
|
||||
}
|
||||
};
|
||||
|
||||
const deleteAccount = (acc) => {
|
||||
Alert.alert(
|
||||
'Delete Service',
|
||||
`Remove ${getServiceName(acc.service_type)} from your account? This cannot be undone.`,
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Delete',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
await DatabaseService.removeLinkedAccount(acc.id);
|
||||
setServiceHealth((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[acc.service_type];
|
||||
return next;
|
||||
});
|
||||
await loadData();
|
||||
Alert.alert('Deleted', `${getServiceName(acc.service_type)} has been removed.`);
|
||||
} catch {
|
||||
Alert.alert('Error', 'Failed to remove service');
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
const reauthenticateService = async (acc) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
if (acc.service_type === 'GOOGLE_PHOTOS') {
|
||||
const gAuth = new GoogleAUTH();
|
||||
const result = await gAuth.authenticate();
|
||||
await DatabaseService.linkAccount(userId, 'GOOGLE_PHOTOS', {
|
||||
accessToken: result.accessToken,
|
||||
refreshToken: result.refreshToken || null,
|
||||
email: result.email || 'google-user',
|
||||
});
|
||||
Alert.alert('Success', 'Google Photos re-authenticated!');
|
||||
} else if (acc.service_type === 'ICLOUD_PHOTOS') {
|
||||
await DatabaseService.linkAccount(userId, 'ICLOUD_PHOTOS', {
|
||||
status: 'placeholder',
|
||||
note: 'iCloud integration pending Apple API access',
|
||||
});
|
||||
Alert.alert('Info', 'iCloud Photos re-registered (placeholder).');
|
||||
} else if (acc.service_type === 'NAS') {
|
||||
setEditingNas(true);
|
||||
setLoading(false);
|
||||
return; // user edits inline
|
||||
}
|
||||
await loadData();
|
||||
} catch (error) {
|
||||
Alert.alert('Error', 'Re-authentication failed: ' + error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleAutoBackup = async (serviceType) => {
|
||||
const updated = autoBackup.includes(serviceType)
|
||||
? autoBackup.filter((s) => s !== serviceType)
|
||||
@@ -130,7 +246,7 @@ const SettingsScreen = ({ navigation, route }) => {
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.scrollContent} showsVerticalScrollIndicator={false}>
|
||||
{/* Theme Section */}
|
||||
{/* ── Theme ──────────────────────────────────────── */}
|
||||
<Text style={[styles.sectionTitle, { color: theme.textSecondary }]}>APPEARANCE</Text>
|
||||
<View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
||||
{themeOptions.map((opt) => (
|
||||
@@ -153,7 +269,7 @@ const SettingsScreen = ({ navigation, route }) => {
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Services Section */}
|
||||
{/* ── Services ──────────────────────────────────── */}
|
||||
<Text style={[styles.sectionTitle, { color: theme.textSecondary }]}>SERVICES</Text>
|
||||
<View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
||||
{accounts.length === 0 ? (
|
||||
@@ -163,36 +279,60 @@ const SettingsScreen = ({ navigation, route }) => {
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
accounts.map((acc) => (
|
||||
<View
|
||||
key={acc.id}
|
||||
style={[styles.serviceRow, { borderBottomColor: theme.borderLight }]}
|
||||
>
|
||||
<Text style={styles.serviceIcon}>{getServiceIcon(acc.service_type)}</Text>
|
||||
<View style={styles.serviceInfo}>
|
||||
<Text style={[styles.serviceName, { color: theme.text }]}>
|
||||
{getServiceName(acc.service_type)}
|
||||
</Text>
|
||||
<Text style={[styles.serviceStatus, { color: acc.is_enabled ? theme.accent : theme.textTertiary }]}>
|
||||
{acc.is_enabled ? 'Enabled' : 'Disabled'}
|
||||
</Text>
|
||||
accounts.map((acc) => {
|
||||
const health = serviceHealth[acc.service_type] || (acc.is_enabled ? 'checking' : 'inactive');
|
||||
return (
|
||||
<View key={acc.id}>
|
||||
{/* Main row */}
|
||||
<View style={[styles.serviceRow, { borderBottomColor: theme.borderLight }]}>
|
||||
<TouchableOpacity onPress={() => recheckHealth(acc)} activeOpacity={0.6}>
|
||||
<ServiceHealthIndicator status={health} size={10} />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.serviceIcon}>{getServiceIcon(acc.service_type)}</Text>
|
||||
<View style={styles.serviceInfo}>
|
||||
<Text style={[styles.serviceName, { color: theme.text }]}>
|
||||
{getServiceName(acc.service_type)}
|
||||
</Text>
|
||||
<Text style={[styles.serviceStatus, { color: acc.is_enabled ? theme.accent : theme.textTertiary }]}>
|
||||
{acc.is_enabled ? 'Enabled' : 'Disabled'}
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={!!acc.is_enabled}
|
||||
onValueChange={() => toggleService(acc.id, acc.is_enabled)}
|
||||
trackColor={{ false: theme.border, true: '#a8dab5' }}
|
||||
thumbColor={acc.is_enabled ? theme.accent : '#f4f3f4'}
|
||||
/>
|
||||
</View>
|
||||
{/* Action buttons row */}
|
||||
<View style={[styles.serviceActions, { borderBottomColor: theme.borderLight }]}>
|
||||
{acc.service_type === 'NAS' && acc.is_enabled && (
|
||||
<TouchableOpacity
|
||||
style={[styles.actionBtn, { borderColor: theme.border }]}
|
||||
onPress={() => setEditingNas(!editingNas)}
|
||||
>
|
||||
<Text style={[styles.actionBtnText, { color: theme.primary }]}>Edit</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={[styles.actionBtn, { borderColor: theme.border }]}
|
||||
onPress={() => reauthenticateService(acc)}
|
||||
disabled={loading}
|
||||
>
|
||||
<Text style={[styles.actionBtnText, { color: theme.primary }]}>
|
||||
{loading ? '...' : 'Re-auth'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.actionBtn, { borderColor: theme.danger }]}
|
||||
onPress={() => deleteAccount(acc)}
|
||||
>
|
||||
<Text style={[styles.actionBtnText, { color: theme.danger }]}>Delete</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
{acc.service_type === 'NAS' && acc.is_enabled && (
|
||||
<TouchableOpacity
|
||||
style={[styles.editButton, { borderColor: theme.border }]}
|
||||
onPress={() => setEditingNas(!editingNas)}
|
||||
>
|
||||
<Text style={[styles.editButtonText, { color: theme.primary }]}>Edit</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<Switch
|
||||
value={!!acc.is_enabled}
|
||||
onValueChange={() => toggleService(acc.id, acc.is_enabled)}
|
||||
trackColor={{ false: theme.border, true: '#a8dab5' }}
|
||||
thumbColor={acc.is_enabled ? theme.accent : '#f4f3f4'}
|
||||
/>
|
||||
</View>
|
||||
))
|
||||
);
|
||||
})
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -248,7 +388,7 @@ const SettingsScreen = ({ navigation, route }) => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Auto Backup */}
|
||||
{/* ── Auto Backup ───────────────────────────────── */}
|
||||
<Text style={[styles.sectionTitle, { color: theme.textSecondary }]}>AUTOMATIC BACKUP</Text>
|
||||
<View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
||||
<Text style={[styles.cardSubtext, { color: theme.textSecondary }]}>
|
||||
@@ -309,7 +449,7 @@ const SettingsScreen = ({ navigation, route }) => {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Sign Out */}
|
||||
{/* ── Sign Out ──────────────────────────────────── */}
|
||||
<TouchableOpacity
|
||||
style={[styles.logoutButton, { borderColor: theme.danger }]}
|
||||
onPress={handleLogout}
|
||||
@@ -356,14 +496,18 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 12,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
serviceIcon: { fontSize: 22, marginRight: 12 },
|
||||
serviceIcon: { fontSize: 22, marginRight: 8 },
|
||||
serviceInfo: { flex: 1 },
|
||||
serviceName: { fontSize: 14, fontWeight: '600', marginBottom: 2 },
|
||||
serviceStatus: { fontSize: 12 },
|
||||
editButton: {
|
||||
borderWidth: 1, borderRadius: 6, paddingHorizontal: 10, paddingVertical: 4, marginRight: 10,
|
||||
serviceActions: {
|
||||
flexDirection: 'row', paddingHorizontal: 14, paddingVertical: 8, gap: 8,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
editButtonText: { fontSize: 12, fontWeight: '600' },
|
||||
actionBtn: {
|
||||
borderWidth: 1, borderRadius: 6, paddingHorizontal: 12, paddingVertical: 5,
|
||||
},
|
||||
actionBtnText: { fontSize: 12, fontWeight: '600' },
|
||||
emptyRow: { padding: 16 },
|
||||
emptyText: { fontSize: 13, textAlign: 'center' },
|
||||
// NAS fields
|
||||
|
||||
Reference in new issue
Block a user