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
19 changes: 19 additions & 0 deletions src/app/appStyles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,4 +197,23 @@ export const appStyles = StyleSheet.create({
top: 3,
right: 4,
},
syncButtonContainer: {
paddingHorizontal: 16,
paddingVertical: 12,
backgroundColor: '#fff',
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
refreshingIndicator: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 12,
backgroundColor: '#f5f5f5',
gap: 8,
},
refreshingText: {
fontSize: 12,
color: '#666',
},
});
18 changes: 18 additions & 0 deletions src/app/tabs/ProjectList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { RootStackParamList } from '../../types/navigation/types';
import { StackNavigationProp } from '@react-navigation/stack';
import { Ionicons } from '@react-native-vector-icons/ionicons';
import { appStyles as styles } from '../appStyles';
import { SyncButton } from '../../components/ui/SyncButton';

const log = logger.create('ProjectListScreen');
type Nav = StackNavigationProp<RootStackParamList, 'Projects'>;
Expand All @@ -23,6 +24,7 @@ export default function ProjectsScreen() {
const navigation = useNavigation<Nav>();
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);

useEffect(() => {
loadProjects();
Expand All @@ -39,6 +41,16 @@ export default function ProjectsScreen() {
}
};

const handleSyncComplete = async () => {
log.info('Sync completed, refreshing projects list...');
setRefreshing(true);
try {
await loadProjects();
} finally {
setRefreshing(false);
}
};

if (loading) {
return (
<View style={[styles.container, styles.centered]}>
Expand All @@ -53,6 +65,10 @@ export default function ProjectsScreen() {
<FluentLogo width={160} height={54} />
</View>

<View style={styles.syncButtonContainer}>
<SyncButton onSyncComplete={handleSyncComplete} />
</View>

<View style={styles.sectionHeader}>
<Ionicons name="folder-outline" size={24} color="#000" />
<Text style={styles.sectionHeaderText}>Projects</Text>
Expand All @@ -62,6 +78,8 @@ export default function ProjectsScreen() {
data={projects}
keyExtractor={item => item.id.toString()}
contentContainerStyle={styles.listContent}
refreshing={refreshing}
onRefresh={handleSyncComplete}
renderItem={({ item }) => (
<TouchableOpacity
style={styles.cardRow}
Expand Down
87 changes: 87 additions & 0 deletions src/components/ui/SyncButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React, { useState, useCallback } from 'react';
import {
TouchableOpacity,
Text,
StyleSheet,
ActivityIndicator,
View,
StyleProp,
ViewStyle,
} from 'react-native';
import { getUserEmailSync } from '../../services/storage';
import { syncAllData } from '../../services/sync';
import { logger } from '../../utils/logger';

const log = logger.create('SyncButton');

interface SyncButtonProps {
onSyncComplete?: () => void;
style?: StyleProp<ViewStyle>;
}

export function SyncButton({ onSyncComplete, style }: SyncButtonProps) {
const [isSyncing, setIsSyncing] = useState(false);

const handleSync = useCallback(async () => {
try {
setIsSyncing(true);
const email = getUserEmailSync();

if (!email) {
log.error('No user email found for sync');
return;
}

log.info('Triggering full sync...');
await syncAllData(email);

log.info('Sync completed successfully');
onSyncComplete?.();
} catch (error) {
log.error('Sync failed', { error });
} finally {
setIsSyncing(false);
}
}, [onSyncComplete]);

return (
<View style={[styles.container, style]}>
<TouchableOpacity
style={[styles.button, isSyncing && styles.buttonDisabled]}
onPress={handleSync}
disabled={isSyncing}
activeOpacity={0.7}
>
{isSyncing ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<Text style={styles.buttonText}>Sync</Text>
)}
</TouchableOpacity>
</View>
);
}

const styles = StyleSheet.create({
container: {
alignItems: 'center',
gap: 8,
},
button: {
backgroundColor: '#1a6ef5',
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 8,
minWidth: 100,
justifyContent: 'center',
alignItems: 'center',
},
buttonDisabled: {
opacity: 0.6,
},
buttonText: {
color: '#fff',
fontSize: 14,
fontWeight: '600',
},
});
2 changes: 1 addition & 1 deletion src/db/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export async function getChapterAssignmentById(
bibleId: row.bible_id,
bookId: row.book_id,
chapterNumber: row.chapter_number,
assignedUserId: row.assigned_user_id ?? undefined,
assignedUserId: row.assigned_user_id,
status: row.status,
submittedTime: row.submitted_time ?? undefined,
updatedAt: row.updated_at,
Expand Down
72 changes: 72 additions & 0 deletions src/services/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Storage } from '@op-engineering/op-sqlite';
import { logger } from '../utils/logger';

const log = logger.create('KVStorage');

export const kvStorage = new Storage({
location: 'kv',
});

export const KV_KEYS = {
USER_ID: 'userId',
USER_EMAIL: 'userEmail',

LAST_SYNCED_AT: 'last_synced_at',
SYNC_COUNT_PROJECTS: 'sync_count_projects',
SYNC_COUNT_CHAPTERS: 'sync_count_chapters',
SYNC_COUNT_BIBLES: 'sync_count_bibles',
} as const;

export function getUserIdSync(): string {
return kvStorage.getItemSync(KV_KEYS.USER_ID) ?? '';
}

export function getUserEmailSync(): string {
return kvStorage.getItemSync(KV_KEYS.USER_EMAIL) ?? '';
}

export function setUserSync(userId: string, userEmail: string) {
kvStorage.setItemSync(KV_KEYS.USER_ID, userId);
kvStorage.setItemSync(KV_KEYS.USER_EMAIL, userEmail);
log.info('User stored in KV', { userId, userEmail });
}

export function getLastSyncedAt(): string {
return kvStorage.getItemSync(KV_KEYS.LAST_SYNCED_AT) ?? '';
}

export function setLastSyncedAt(timestamp: string) {
kvStorage.setItemSync(KV_KEYS.LAST_SYNCED_AT, timestamp);
log.info('Last synced timestamp updated', { timestamp });
}

export function getSyncCount(
key: (typeof KV_KEYS)[keyof typeof KV_KEYS],
): number {
const value = kvStorage.getItemSync(key);
return value ? parseInt(value, 10) : 0;
}

export function setSyncCount(
key: (typeof KV_KEYS)[keyof typeof KV_KEYS],
count: number,
) {
kvStorage.setItemSync(key, String(count));
log.info('Sync count updated', { key, count });
}

export function getSyncState(): SyncState {
return {
lastSyncedAt: getLastSyncedAt(),
projectsCount: getSyncCount(KV_KEYS.SYNC_COUNT_PROJECTS),
chaptersCount: getSyncCount(KV_KEYS.SYNC_COUNT_CHAPTERS),
biblesCount: getSyncCount(KV_KEYS.SYNC_COUNT_BIBLES),
};
}

export type SyncState = {
lastSyncedAt: string;
projectsCount: number;
chaptersCount: number;
biblesCount: number;
};
66 changes: 52 additions & 14 deletions src/services/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,17 @@ import {
getChaptersToSync,
} from '../db/repository';
import { logger } from '../utils/logger';
import { getDatabase } from '../db/db';
import { ApiBook, ApiVerse } from '../types/api/types';
import {
setUserSync,
setSyncCount,
setLastSyncedAt,
KV_KEYS,
} from '../services/storage';

const log = logger.create('SyncService');
const db = getDatabase();

export async function syncUser(email: string) {
try {
Expand All @@ -27,6 +35,7 @@ export async function syncUser(email: string) {

await insertUser(user);

setUserSync(String(user.id), user.email);
log.info('User synced', { email: user.email });

return user;
Expand Down Expand Up @@ -63,6 +72,9 @@ export async function syncProjects(userId: number, email: string) {
const projects = await FluentAPI.getUserProjects(userId, email);

await insertProjects(projects);
const result = await db.execute('SELECT COUNT(*) as count FROM projects');
const totalProjectsCount = result.rows?.[0]?.count || 0;
setSyncCount(KV_KEYS.SYNC_COUNT_PROJECTS, Number(totalProjectsCount));

log.info('Projects synced', { count: projects.length });
} catch (error) {
Expand All @@ -85,7 +97,17 @@ export async function syncChapterAssignments(userId: number, email: string) {
await insertProjectUnits(allAssignments);

await insertChapterAssignments(allAssignments);
log.info('Chapter assignments synced', { count: allAssignments.length });

const result = await db.execute(
'SELECT COUNT(*) as count FROM chapter_assignments',
);
const totalChaptersCount = result.rows?.[0]?.count || 0;
setSyncCount(KV_KEYS.SYNC_COUNT_CHAPTERS, Number(totalChaptersCount));

log.info('Chapter assignments synced', {
apiCount: allAssignments.length,
totalInDb: totalChaptersCount,
});
}
} catch (error) {
log.error('Chapter assignment sync failed', {
Expand All @@ -104,7 +126,8 @@ export async function syncBibleTexts(email: string) {

if (bibleGroups.size === 0) {
log.info('No chapters to sync');
return;
setSyncCount(KV_KEYS.SYNC_COUNT_BIBLES, 0);
return 0;
}

let totalTextsInserted = 0;
Expand Down Expand Up @@ -147,8 +170,16 @@ export async function syncBibleTexts(email: string) {
continue;
}
}

log.info('Bible texts sync completed', { count: totalTextsInserted });
const result = await db.execute(
'SELECT COUNT(DISTINCT bible_id) as count FROM bible_texts',
);
const uniqueBiblesCount = result.rows?.[0]?.count || 0;
setSyncCount(KV_KEYS.SYNC_COUNT_BIBLES, Number(uniqueBiblesCount));

log.info('Bible texts sync completed', {
textsInserted: totalTextsInserted,
uniqueBiblesInDb: uniqueBiblesCount,
});
} catch (error) {
log.error('Bible texts sync failed', { error });
}
Expand All @@ -157,19 +188,26 @@ export async function syncBibleTexts(email: string) {
export async function syncAllData(email: string) {
log.info('Starting full sync...');

const user = await syncUser(email);
try {
const user = await syncUser(email);

await syncMasterData();

await syncMasterData();
await syncProjects(user.id, email);

await syncProjects(user.id, email);
await syncChapterAssignments(user.id, email);

await syncChapterAssignments(user.id, email);
try {
await syncBibleTexts(email);
} catch (e) {
log.warn('Bible text sync failed, continuing...', { error: e });
}
const now = new Date().toISOString();
setLastSyncedAt(now);

try {
await syncBibleTexts(email);
} catch (e) {
log.warn('Bible text sync failed, continuing...', { error: e });
log.info('Full sync completed successfully!', { timestamp: now });
} catch (error) {
log.error('Full sync failed', { error });
throw error;
}

log.info('Full sync completed successfully!');
}
6 changes: 3 additions & 3 deletions src/types/db/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export interface ChapterAssignmentData {
bibleId: number;
bookId: number;
chapterNumber: number;
assignedUserId?: number | null;
assignedUserId?: number;
status: string;
submittedTime?: string | null;
updatedAt?: string;
Expand All @@ -72,7 +72,7 @@ export interface ChapterAssignmentRow {
bible_id: number;
book_id: number;
chapter_number: number;
assigned_user_id?: number | null;
assigned_user_id?: number;
status: string;
submitted_time?: string | null;
updated_at?: string;
Expand All @@ -86,7 +86,7 @@ export interface ChapterListItem {
chapter_number: number;
status: string;
book_name: string;
assigned_user_id?: number | null;
assigned_user_id?: number;
}

export interface ChapterRow {
Expand Down
Loading