From 367073ed8938f05a3035db635907ea398207d4b5 Mon Sep 17 00:00:00 2001 From: m Date: Thu, 10 Sep 2026 22:21:46 +0200 Subject: [PATCH] add floating nav bar + regroup files by date --- mobile/app/_layout.tsx | 2 + mobile/app/index.tsx | 151 ++++++++++++++++++++++----- mobile/app/search.tsx | 22 ++++ mobile/app/settings.tsx | 22 ++++ mobile/components/FloatingNavBar.tsx | 3 +- mobile/i18n/en.json | 6 ++ mobile/i18n/fr.json | 6 ++ 7 files changed, 182 insertions(+), 30 deletions(-) create mode 100644 mobile/app/search.tsx create mode 100644 mobile/app/settings.tsx diff --git a/mobile/app/_layout.tsx b/mobile/app/_layout.tsx index 5c74ce4..452c39c 100644 --- a/mobile/app/_layout.tsx +++ b/mobile/app/_layout.tsx @@ -28,6 +28,8 @@ function AuthGate() { <> + + diff --git a/mobile/app/index.tsx b/mobile/app/index.tsx index 5f2d7d7..4d24ed0 100644 --- a/mobile/app/index.tsx +++ b/mobile/app/index.tsx @@ -1,21 +1,89 @@ -import { useFocusEffect, useRouter } from 'expo-router'; +import { Stack, useFocusEffect } from 'expo-router'; import { StatusBar } from 'expo-status-bar'; +import { Ionicons } from '@expo/vector-icons'; import { useCallback, useState } from 'react'; -import { FlatList, Pressable, StyleSheet, Text, View } from 'react-native'; +import { Pressable, SectionList, StyleSheet, Text, View } from 'react-native'; import { syncRoot } from '../features/syncDevice'; import { pickDirectory } from '../services/safDirectory'; -import { getFolders, saveDirectory } from '../services/localStorage'; -import type { StoredFolder } from '../services/db/types'; +import { getFiles, saveDirectory } from '../services/localStorage'; +import type { StoredFile } from '../services/db/types'; +import FloatingNavBar from '../components/FloatingNavBar'; import i18n from '../i18n'; +type FilePair = { + key: string; + left: StoredFile; + right: StoredFile | null; +}; + +type FileSection = { + key: string; + dayLabel: string; + data: FilePair[]; +}; + +function formatSize(bytes: number): string { + if ( bytes < 1024 ) return `${bytes} ${i18n.t('bytes')}`; + if ( bytes < 1024 * 1024 ) return `${(bytes / 1024).toFixed(1)} ${i18n.t('kilobytes')}`; + return `${(bytes / (1024 * 1024)).toFixed(1)} ${i18n.t('megabytes')}`; +} + +function formatDay(timestamp: number): string { + return new Intl.DateTimeFormat(i18n.locale, { day: 'numeric', month: 'long', year: 'numeric' }).format(timestamp); +} + +function startOfDay(timestamp: number): number { + const date = new Date(timestamp); + date.setHours(0, 0, 0, 0); + return date.getTime(); +} + +function chunkToPairs(files: StoredFile[]): FilePair[] { + const pairs: FilePair[] = []; + for ( let i = 0; i < files.length; i += 2 ) { + pairs.push({ + key: files[i].resource_id + (files[i + 1]?.resource_id ?? ''), + left: files[i], + right: files[i + 1] ?? null, + }); + } + return pairs; +} + +function groupFilesByDay(files: StoredFile[]): FileSection[] { + const byDay = new Map(); + for ( const file of files ) { + const day = startOfDay(file.addedAt); + const bucket = byDay.get(day); + if ( bucket ) bucket.push(file); + else byDay.set(day, [file]); + } + return [...byDay.entries()] + .sort((a, b) => b[0] - a[0]) + .map(([day, data]) => ({ + key: String(day), + dayLabel: formatDay(day), + data: chunkToPairs(data.sort((a, b) => b.addedAt - a.addedAt)), + })); +} + +function FileCard({ file }: { file: StoredFile }) { + return ( + + + {file.name} + {formatSize(file.size)} + + ); +} + export default function Index() { - const router = useRouter(); - const [roots, setRoots] = useState([]); + const [sections, setSections] = useState([]); const load = useCallback(async () => { - const saved = await getFolders(); - setRoots(saved.filter((folder) => folder.parent_resource_id === null)); + const files = await getFiles(); + setSections(groupFilesByDay(files)); }, []); useFocusEffect( @@ -39,28 +107,32 @@ export default function Index() { return ( + {i18n.t('add_folder')} - item.resource_id} - renderItem={({ item }) => ( - router.push(`/folder/${item.resource_id}`)} - > - {item.name} - {item.syncStatus} - + item.key} + renderSectionHeader={({ section }) => ( + {section.dayLabel} )} + renderItem={({ item }) => ( + + + {item.right ? : } + + )} + stickySectionHeadersEnabled + contentContainerStyle={styles.listContent} ListEmptyComponent={ - {i18n.t('no_folders_yet')} + {i18n.t('no_files_yet')} } /> + ); } @@ -69,30 +141,53 @@ const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', - padding: 16, }, button: { backgroundColor: '#1a73e8', paddingVertical: 12, borderRadius: 8, alignItems: 'center', - marginBottom: 16, + margin: 16, }, buttonText: { color: '#fff', fontSize: 16, fontWeight: '600', }, + listContent: { + paddingHorizontal: 16, + paddingBottom: 110, + }, row: { - paddingVertical: 14, - borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: '#ddd', + flexDirection: 'row', + gap: 12, + marginBottom: 12, }, - rowTitle: { - fontSize: 16, + sectionHeader: { + fontSize: 14, + fontWeight: '600', + color: '#6b7280', + textTransform: 'uppercase', + marginBottom: 12, + backgroundColor: '#fff', + }, + card: { + flex: 1, + backgroundColor: '#f7f8fa', + borderRadius: 10, + padding: 12, + borderWidth: StyleSheet.hairlineWidth, + borderColor: '#eaeaea', + }, + cardSpacer: { + flex: 1, + }, + cardTitle: { + fontSize: 14, fontWeight: '500', + marginTop: 8, }, - rowMeta: { + cardMeta: { fontSize: 12, color: '#888', marginTop: 2, diff --git a/mobile/app/search.tsx b/mobile/app/search.tsx new file mode 100644 index 0000000..ffac505 --- /dev/null +++ b/mobile/app/search.tsx @@ -0,0 +1,22 @@ +import { Stack } from 'expo-router'; +import { StatusBar } from 'expo-status-bar'; +import { StyleSheet, View } from 'react-native'; +import FloatingNavBar from '../components/FloatingNavBar'; +import i18n from '../i18n'; + +export default function Search() { + return ( + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff', + }, +}); \ No newline at end of file diff --git a/mobile/app/settings.tsx b/mobile/app/settings.tsx new file mode 100644 index 0000000..2e7b44d --- /dev/null +++ b/mobile/app/settings.tsx @@ -0,0 +1,22 @@ +import { Stack } from 'expo-router'; +import { StatusBar } from 'expo-status-bar'; +import { StyleSheet, View } from 'react-native'; +import FloatingNavBar from '../components/FloatingNavBar'; +import i18n from '../i18n'; + +export default function Settings() { + return ( + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff', + }, +}); \ No newline at end of file diff --git a/mobile/components/FloatingNavBar.tsx b/mobile/components/FloatingNavBar.tsx index 2724224..00692ef 100644 --- a/mobile/components/FloatingNavBar.tsx +++ b/mobile/components/FloatingNavBar.tsx @@ -43,7 +43,6 @@ export default function FloatingNavBar() { const router = useRouter(); const pathname = usePathname(); const insets = useSafeAreaInsets(); - const { t } = i18n; return ( @@ -76,7 +75,7 @@ export default function FloatingNavBar() { ]} numberOfLines={1} > - {t(tab.labelKey)} + {i18n.t(tab.labelKey)} ); diff --git a/mobile/i18n/en.json b/mobile/i18n/en.json index b55db50..12b72ed 100644 --- a/mobile/i18n/en.json +++ b/mobile/i18n/en.json @@ -2,8 +2,14 @@ "files": "Files", "search": "Search", "configuration": "Settings", + "nav": { + "files": "Files", + "search": "Search", + "settings": "Settings" + }, "add_folder": "Add a folder", "no_folders_yet": "No folders yet. Tap \"Add a folder\" to sync a folder.", + "no_files_yet": "No files yet. Tap \"Add a folder\" to sync your files.", "folder": "Folder", "empty_folder": "Empty folder — next syncDevice will refresh it.", "bytes": "B", diff --git a/mobile/i18n/fr.json b/mobile/i18n/fr.json index 6185e8b..e18589e 100644 --- a/mobile/i18n/fr.json +++ b/mobile/i18n/fr.json @@ -2,8 +2,14 @@ "files": "Fichiers", "search": "Recherche", "configuration": "Configuration", + "nav": { + "files": "Fichiers", + "search": "Recherche", + "settings": "Réglages" + }, "add_folder": "Ajouter un dossier", "no_folders_yet": "Aucun dossier pour le moment. Appuie sur « Ajouter un dossier » pour synchroniser un dossier.", + "no_files_yet": "Aucun fichier pour le moment. Appuie sur « Ajouter un dossier » pour synchroniser tes fichiers.", "folder": "Dossier", "empty_folder": "Dossier vide — le prochain syncDevice l'actualisera.", "bytes": "o",