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",