Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 214 additions & 0 deletions formulus/src/components/AutoSyncSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import React, { useCallback, useState } from 'react';
import { StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native';
import { useFocusEffect } from '@react-navigation/native';
import { useTranslation } from 'react-i18next';
import { useAppTheme } from '../contexts/AppThemeContext';
import {
AUTO_SYNC_INTERVALS,
type AutoSyncInterval,
loadAutoSyncSettings,
saveAutoSyncSettings,
} from '../services/AutoSyncSettingsService';
import { autoSyncService } from '../services/AutoSyncService';
import { ToastService } from '../services/ToastService';
import { odeSpacing, odeTypography } from '../theme/odeDesign';
import { withAlpha } from '../theme/colors';

const intervalLabelKey = (interval: AutoSyncInterval): string => {
switch (interval) {
case 30_000:
return 'profiles.autoSync30Seconds';
case 2 * 60_000:
return 'profiles.autoSync2Minutes';
case 5 * 60_000:
return 'profiles.autoSync5Minutes';
case 15 * 60_000:
return 'profiles.autoSync15Minutes';
case 60 * 60_000:
return 'profiles.autoSync1Hour';
case 24 * 60 * 60_000:
return 'profiles.autoSync24Hours';
}
return 'profiles.autoSync5Minutes';
};

const AutoSyncSettings = () => {
const { t } = useTranslation();
const { themeColors } = useAppTheme();
const [enabled, setEnabled] = useState(false);
const [intervalMs, setIntervalMs] = useState<AutoSyncInterval>(5 * 60_000);
const [loading, setLoading] = useState(true);

useFocusEffect(
useCallback(() => {
let cancelled = false;

setLoading(true);

void loadAutoSyncSettings()
.then(settings => {
if (cancelled) return;

setEnabled(settings.enabled);
setIntervalMs(settings.intervalMs);
})
.catch(() => {
if (!cancelled) {
ToastService.showLong(t('profiles.autoSyncLoadFailed'));
}
})
.finally(() => {
if (!cancelled) {
setLoading(false);
}
});

return () => {
cancelled = true;
};
}, [t]),
);

const save = async (
nextEnabled: boolean,
nextInterval: AutoSyncInterval,
): Promise<void> => {
try {
await saveAutoSyncSettings({
enabled: nextEnabled,
intervalMs: nextInterval,
});

autoSyncService.refresh();
} catch {
ToastService.showLong(t('profiles.autoSyncSaveFailed'));
}
};

const handleEnabledChange = (value: boolean) => {
setEnabled(value);
void save(value, intervalMs);
};

const handleIntervalChange = (value: AutoSyncInterval) => {
setIntervalMs(value);
void save(enabled, value);
};

return (
<View style={styles.section}>
<View style={styles.toggleRow}>
<View style={styles.toggleText}>
<Text style={[styles.heading, { color: themeColors.onSurface }]}>
{t('profiles.autoSyncTitle')}
</Text>
<Text style={[styles.hint, { color: themeColors.onSurface }]}>
{t('profiles.autoSyncHint')}
</Text>
</View>

<Switch
value={enabled}
disabled={loading}
onValueChange={handleEnabledChange}
accessibilityLabel={t('profiles.autoSyncTitle')}
/>
</View>

{enabled && (
<View style={styles.intervalSection}>
<Text
style={[styles.intervalTitle, { color: themeColors.onSurface }]}>
{t('profiles.autoSyncInterval')}
</Text>

<View style={styles.intervalOptions}>
{AUTO_SYNC_INTERVALS.map(interval => {
const selected = intervalMs === interval;

return (
<TouchableOpacity
key={interval}
accessibilityRole="radio"
accessibilityState={{ checked: selected, disabled: loading }}
disabled={loading}
onPress={() => handleIntervalChange(interval)}
style={[
styles.intervalOption,
{
backgroundColor: withAlpha(
themeColors.primary as string,
selected ? 0.12 : 0,
),
},
]}>
<Text
style={[
styles.intervalText,
{
color: selected
? themeColors.primary
: themeColors.onSurface,
},
]}>
{t(intervalLabelKey(interval))}
</Text>
</TouchableOpacity>
);
})}
</View>
</View>
)}
</View>
);
};

const styles = StyleSheet.create({
section: {
marginTop: odeSpacing.md,
gap: odeSpacing.sm,
},
toggleRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: odeSpacing.md,
},
toggleText: {
flex: 1,
},
heading: {
fontSize: odeTypography.sectionTitle,
fontWeight: '600',
},
hint: {
fontSize: odeTypography.bodySm,
marginTop: odeSpacing.xs,
},
intervalSection: {
gap: odeSpacing.sm,
},
intervalTitle: {
fontSize: odeTypography.body,
fontWeight: '600',
},
intervalOptions: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: odeSpacing.xs,
},
intervalOption: {
borderWidth: 1,
borderRadius: odeSpacing.xs,
paddingHorizontal: odeSpacing.sm,
paddingVertical: odeSpacing.xs,
minHeight: 44,
justifyContent: 'center',
},
intervalText: {
fontSize: odeTypography.bodySm,
fontWeight: '500',
},
});

export default AutoSyncSettings;
11 changes: 11 additions & 0 deletions formulus/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@
"profiles.showPassword": "Show password",
"profiles.hidePassword": "Hide password",
"profiles.working": "Working…",
"profiles.autoSyncTitle": "Automatic sync",
"profiles.autoSyncHint": "Automatically sync observations while Formulus is open.",
"profiles.autoSyncInterval": "Sync interval",
"profiles.autoSync30Seconds": "Every 30 seconds",
"profiles.autoSync2Minutes": "Every 2 minutes",
"profiles.autoSync5Minutes": "Every 5 minutes",
"profiles.autoSync15Minutes": "Every 15 minutes",
"profiles.autoSync1Hour": "Every hour",
"profiles.autoSync24Hours": "Every 24 hours",
"profiles.autoSyncLoadFailed": "Could not load automatic sync settings.",
"profiles.autoSyncSaveFailed": "Could not save automatic sync settings.",
"welcome.title": "Welcome to:",
"welcome.profilesSubtitle": "Set up a profile to connect to your server.",
"welcome.getStarted": "Get started",
Expand Down
11 changes: 11 additions & 0 deletions formulus/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@
"profiles.showPassword": "Afficher le mot de passe",
"profiles.hidePassword": "Masquer le mot de passe",
"profiles.working": "Traitement…",
"profiles.autoSyncTitle": "Synchronisation automatique",
"profiles.autoSyncHint": "Synchronise automatiquement les observations lorsque Formulus est ouvert.",
"profiles.autoSyncInterval": "Intervalle de synchronisation",
"profiles.autoSync30Seconds": "Toutes les 30 secondes",
"profiles.autoSync2Minutes": "Toutes les 2 minutes",
"profiles.autoSync5Minutes": "Toutes les 5 minutes",
"profiles.autoSync15Minutes": "Toutes les 15 minutes",
"profiles.autoSync1Hour": "Toutes les heures",
"profiles.autoSync24Hours": "Toutes les 24 heures",
"profiles.autoSyncLoadFailed": "Impossible de charger les paramètres de synchronisation automatique.",
"profiles.autoSyncSaveFailed": "Impossible d’enregistrer les paramètres de synchronisation automatique.",
"welcome.title": "Bienvenue dans :",
"welcome.profilesSubtitle": "Configurez un profil pour vous connecter à votre serveur.",
"welcome.getStarted": "Commencer",
Expand Down
11 changes: 11 additions & 0 deletions formulus/src/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@
"profiles.showPassword": "Mostrar palavra-passe",
"profiles.hidePassword": "Ocultar palavra-passe",
"profiles.working": "A processar…",
"profiles.autoSyncTitle": "Sincronização automática",
"profiles.autoSyncHint": "Sincroniza automaticamente as observações enquanto o Formulus está aberto.",
"profiles.autoSyncInterval": "Intervalo de sincronização",
"profiles.autoSync30Seconds": "A cada 30 segundos",
"profiles.autoSync2Minutes": "A cada 2 minutos",
"profiles.autoSync5Minutes": "A cada 5 minutos",
"profiles.autoSync15Minutes": "A cada 15 minutos",
"profiles.autoSync1Hour": "A cada hora",
"profiles.autoSync24Hours": "A cada 24 horas",
"profiles.autoSyncLoadFailed": "Não foi possível carregar as definições de sincronização automática.",
"profiles.autoSyncSaveFailed": "Não foi possível guardar as definições de sincronização automática.",
"welcome.title": "Bem-vindo ao:",
"welcome.profilesSubtitle": "Configure um perfil para se ligar ao seu servidor.",
"welcome.getStarted": "Começar",
Expand Down
14 changes: 14 additions & 0 deletions formulus/src/profiles/ProfileRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
import { bootstrapProfileApp } from './ProfileBootstrap';
import { i18n } from '../i18n';
import { autoSyncService } from '../services/AutoSyncService';
import {
getProfileTransitionState,
registerProfileTransitionHost,
Expand Down Expand Up @@ -49,6 +50,19 @@
};
}, []);

useEffect(() => {
if (!AppComponent || unmounted || transition !== 'idle' || error) {
autoSyncService.stop();
return;
}

const stopAutoSync = autoSyncService.start();

return () => {
stopAutoSync();
};
}, [AppComponent, unmounted, transition, error]);

useEffect(
() =>
registerProfileTransitionHost({
Expand Down Expand Up @@ -106,7 +120,7 @@
}

const styles = StyleSheet.create({
screen: {

Check warning on line 123 in formulus/src/profiles/ProfileRoot.tsx

View workflow job for this annotation

GitHub Actions / Formulus (React Native)

Color literal: { backgroundColor: '#ffffff' }
flex: 1,
justifyContent: 'center',
alignItems: 'center',
Expand All @@ -114,11 +128,11 @@
backgroundColor: '#ffffff',
gap: 16,
},
title: {

Check warning on line 131 in formulus/src/profiles/ProfileRoot.tsx

View workflow job for this annotation

GitHub Actions / Formulus (React Native)

Color literal: { color: '#111111' }
fontSize: 22,
fontWeight: '600',
color: '#111111',
textAlign: 'center',
},
message: { fontSize: 16, color: '#333333', textAlign: 'center' },

Check warning on line 137 in formulus/src/profiles/ProfileRoot.tsx

View workflow job for this annotation

GitHub Actions / Formulus (React Native)

Color literal: { color: '#333333' }
});
4 changes: 4 additions & 0 deletions formulus/src/screens/ProfileConnection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
import Icon from '@react-native-vector-icons/material-design-icons';
import { useTranslation } from 'react-i18next';
import { Button, Input } from '../components/common';
import AutoSyncSettings from '../components/AutoSyncSettings';
import QRScannerModal, {
ScannerModalResults,
} from '../components/QRScannerModal';
Expand Down Expand Up @@ -423,6 +424,9 @@ const ProfileConnection = ({
</TouchableOpacity>
}
/>

{!creation && <AutoSyncSettings />}

{creation ? (
<View style={styles.actions}>
<Button
Expand Down
4 changes: 4 additions & 0 deletions formulus/src/screens/__tests__/ProfilesScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ jest.mock('react-native-safe-area-context', () => ({
},
}));
jest.mock('@react-native-vector-icons/material-design-icons', () => 'Icon');
jest.mock('../../components/AutoSyncSettings', () => ({
__esModule: true,
default: () => null,
}));
jest.mock('../../contexts/AppThemeContext', () => ({
useAppTheme: () => ({
themeColors: {
Expand Down
Loading
Loading