mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-03 08:06:59 +00:00
- Integrated useColors hook for dynamic theming in SettingsScreen. - Updated tab labels and titles to use localized strings. - Refactored styles to utilize theme palette for consistent styling. - Improved locale subscription to re-render labels on language change. feat: implement useSheetDrag hook for bottom sheet gesture handling - Added useSheetDrag hook to manage drag-to-dismiss functionality for bottom sheets. - Configured pan responder for handling drag gestures and dismissing the sheet based on distance and velocity. refactor: restructure color and token management - Introduced useColors and useResolvedTheme hooks for better theme management. - Separated light and dark color palettes for improved maintainability. - Updated tokens to reflect new theming structure and ensure consistency across components.
179 lines
4.4 KiB
TypeScript
179 lines
4.4 KiB
TypeScript
import React, { useCallback, useEffect, useState } from 'react';
|
|
import {
|
|
ActivityIndicator,
|
|
FlatList,
|
|
StyleSheet,
|
|
Text,
|
|
TouchableOpacity,
|
|
View,
|
|
} from 'react-native';
|
|
import { FileText, Folder, HardDrive } from 'lucide-react-native';
|
|
import { getFileNodes } from '../api/files';
|
|
import type { FileNode } from '../api/types';
|
|
import { spacing, typography, type ThemePalette } from '../theme/tokens';
|
|
import { useColors } from '../theme/colors';
|
|
|
|
function formatFileSize(bytes?: number): string {
|
|
if (bytes == null) return '';
|
|
if (bytes < 1024) return `${bytes} B`;
|
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
}
|
|
|
|
export default function FilesScreen() {
|
|
const c = useColors();
|
|
const styles = React.useMemo(() => makeStyles(c), [c]);
|
|
const [files, setFiles] = useState<FileNode[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const loadFiles = useCallback(async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const nodes = await getFileNodes(null);
|
|
setFiles(nodes);
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : 'Failed to load files');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
void loadFiles();
|
|
}, [loadFiles]);
|
|
|
|
if (loading) {
|
|
return (
|
|
<View style={styles.center}>
|
|
<ActivityIndicator color={c.primary} />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<View style={styles.center}>
|
|
<Text style={styles.errorText}>{error}</Text>
|
|
<TouchableOpacity style={styles.retryButton} onPress={loadFiles}>
|
|
<Text style={styles.retryText}>Retry</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (files.length === 0) {
|
|
return (
|
|
<View style={styles.container}>
|
|
<View style={styles.header}>
|
|
<Text style={styles.title}>Files</Text>
|
|
</View>
|
|
<View style={styles.center}>
|
|
<HardDrive size={48} color={c.textMuted} />
|
|
<Text style={styles.emptyText}>No files yet</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<View style={styles.header}>
|
|
<Text style={styles.title}>Files</Text>
|
|
</View>
|
|
<FlatList
|
|
data={files}
|
|
keyExtractor={(item) => item.id}
|
|
renderItem={({ item }) => (
|
|
<View style={styles.fileRow}>
|
|
{item.type === 'directory' ? (
|
|
<Folder size={20} color={c.primary} />
|
|
) : (
|
|
<FileText size={20} color={c.textMuted} />
|
|
)}
|
|
<View style={styles.fileInfo}>
|
|
<Text style={styles.fileName} numberOfLines={1}>
|
|
{item.name}
|
|
</Text>
|
|
{item.size != null && (
|
|
<Text style={styles.fileSize}>{formatFileSize(item.size)}</Text>
|
|
)}
|
|
</View>
|
|
</View>
|
|
)}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function makeStyles(c: ThemePalette) {
|
|
return StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: c.background,
|
|
},
|
|
header: {
|
|
paddingTop: 60,
|
|
paddingHorizontal: spacing.md,
|
|
paddingBottom: spacing.sm,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: c.border,
|
|
},
|
|
title: {
|
|
fontSize: typography.h2.fontSize,
|
|
fontWeight: '700' as const,
|
|
color: c.text,
|
|
},
|
|
center: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
gap: spacing.sm,
|
|
},
|
|
emptyText: {
|
|
color: c.textMuted,
|
|
fontSize: typography.body.fontSize,
|
|
marginTop: spacing.sm,
|
|
},
|
|
errorText: {
|
|
color: c.error,
|
|
fontSize: typography.body.fontSize,
|
|
textAlign: 'center',
|
|
paddingHorizontal: spacing.lg,
|
|
},
|
|
retryButton: {
|
|
marginTop: spacing.md,
|
|
paddingHorizontal: spacing.lg,
|
|
paddingVertical: spacing.sm,
|
|
backgroundColor: c.primary,
|
|
borderRadius: 8,
|
|
},
|
|
retryText: {
|
|
color: c.primaryForeground,
|
|
fontWeight: '600' as const,
|
|
},
|
|
fileRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingVertical: spacing.sm,
|
|
paddingHorizontal: spacing.md,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: c.border,
|
|
gap: spacing.sm,
|
|
},
|
|
fileInfo: {
|
|
flex: 1,
|
|
},
|
|
fileName: {
|
|
fontSize: typography.body.fontSize,
|
|
color: c.text,
|
|
},
|
|
fileSize: {
|
|
fontSize: typography.caption.fontSize,
|
|
color: c.textMuted,
|
|
marginTop: 2,
|
|
},
|
|
});
|
|
}
|