@oxyhq/services 5.3.11 → 5.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/lib/commonjs/assets/assets/icons/OxyServices.tsx +67 -0
- package/lib/commonjs/assets/assets/icons/logo_OxyServices.svg +1 -0
- package/lib/commonjs/assets/icons/OxyServices.js +53 -0
- package/lib/commonjs/assets/icons/OxyServices.js.map +1 -0
- package/lib/commonjs/assets/icons/logo_OxyServices.svg +1 -0
- package/lib/commonjs/core/index.js +119 -23
- package/lib/commonjs/core/index.js.map +1 -1
- package/lib/commonjs/index.js +2 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/lib/sonner.js +15 -11
- package/lib/commonjs/lib/sonner.js.map +1 -1
- package/lib/commonjs/node/index.js +2 -0
- package/lib/commonjs/node/index.js.map +1 -1
- package/lib/commonjs/ui/components/GroupedItem.js +109 -0
- package/lib/commonjs/ui/components/GroupedItem.js.map +1 -0
- package/lib/commonjs/ui/components/GroupedSection.js +33 -0
- package/lib/commonjs/ui/components/GroupedSection.js.map +1 -0
- package/lib/commonjs/ui/components/OxyProvider.js +95 -112
- package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
- package/lib/commonjs/ui/components/ProfileCard.js +124 -0
- package/lib/commonjs/ui/components/ProfileCard.js.map +1 -0
- package/lib/commonjs/ui/components/QuickActions.js +87 -0
- package/lib/commonjs/ui/components/QuickActions.js.map +1 -0
- package/lib/commonjs/ui/components/Section.js +36 -0
- package/lib/commonjs/ui/components/Section.js.map +1 -0
- package/lib/commonjs/ui/components/SectionTitle.js +35 -0
- package/lib/commonjs/ui/components/SectionTitle.js.map +1 -0
- package/lib/commonjs/ui/components/bottomSheet/index.js +6 -6
- package/lib/commonjs/ui/components/index.js +97 -0
- package/lib/commonjs/ui/components/index.js.map +1 -0
- package/lib/commonjs/ui/navigation/OxyRouter.js +20 -3
- package/lib/commonjs/ui/navigation/OxyRouter.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountCenterScreen.js +190 -207
- package/lib/commonjs/ui/screens/AccountCenterScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountManagementDemo.js +299 -0
- package/lib/commonjs/ui/screens/AccountManagementDemo.js.map +1 -0
- package/lib/commonjs/ui/screens/AccountOverviewScreen.js +669 -401
- package/lib/commonjs/ui/screens/AccountOverviewScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js +695 -498
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountSwitcherScreen.js +451 -488
- package/lib/commonjs/ui/screens/AccountSwitcherScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/AppInfoScreen.js +498 -185
- package/lib/commonjs/ui/screens/AppInfoScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/BillingManagementScreen.js +636 -0
- package/lib/commonjs/ui/screens/BillingManagementScreen.js.map +1 -0
- package/lib/commonjs/ui/screens/FileManagementScreen.js +2497 -0
- package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -0
- package/lib/commonjs/ui/screens/PremiumSubscriptionScreen.js +1620 -0
- package/lib/commonjs/ui/screens/PremiumSubscriptionScreen.js.map +1 -0
- package/lib/commonjs/ui/screens/ProfileScreen.js +117 -13
- package/lib/commonjs/ui/screens/ProfileScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/SessionManagementScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/SignInScreen.js +1 -1
- package/lib/commonjs/ui/screens/SignUpScreen.js +1 -1
- package/lib/commonjs/utils/polyfills.js +42 -0
- package/lib/commonjs/utils/polyfills.js.map +1 -0
- package/lib/module/assets/assets/icons/OxyServices.tsx +67 -0
- package/lib/module/assets/assets/icons/logo_OxyServices.svg +1 -0
- package/lib/module/assets/icons/OxyServices.js +46 -0
- package/lib/module/assets/icons/OxyServices.js.map +1 -0
- package/lib/module/assets/icons/logo_OxyServices.svg +1 -0
- package/lib/module/core/index.js +119 -23
- package/lib/module/core/index.js.map +1 -1
- package/lib/module/index.js +3 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/lib/sonner.js +13 -1
- package/lib/module/lib/sonner.js.map +1 -1
- package/lib/module/node/index.js +3 -0
- package/lib/module/node/index.js.map +1 -1
- package/lib/module/ui/components/GroupedItem.js +104 -0
- package/lib/module/ui/components/GroupedItem.js.map +1 -0
- package/lib/module/ui/components/GroupedSection.js +28 -0
- package/lib/module/ui/components/GroupedSection.js.map +1 -0
- package/lib/module/ui/components/OxyProvider.js +97 -114
- package/lib/module/ui/components/OxyProvider.js.map +1 -1
- package/lib/module/ui/components/ProfileCard.js +119 -0
- package/lib/module/ui/components/ProfileCard.js.map +1 -0
- package/lib/module/ui/components/QuickActions.js +82 -0
- package/lib/module/ui/components/QuickActions.js.map +1 -0
- package/lib/module/ui/components/Section.js +31 -0
- package/lib/module/ui/components/Section.js.map +1 -0
- package/lib/module/ui/components/SectionTitle.js +30 -0
- package/lib/module/ui/components/SectionTitle.js.map +1 -0
- package/lib/module/ui/components/bottomSheet/index.js +2 -5
- package/lib/module/ui/components/bottomSheet/index.js.map +1 -1
- package/lib/module/ui/components/index.js +18 -0
- package/lib/module/ui/components/index.js.map +1 -0
- package/lib/module/ui/navigation/OxyRouter.js +20 -3
- package/lib/module/ui/navigation/OxyRouter.js.map +1 -1
- package/lib/module/ui/screens/AccountCenterScreen.js +191 -208
- package/lib/module/ui/screens/AccountCenterScreen.js.map +1 -1
- package/lib/module/ui/screens/AccountManagementDemo.js +296 -0
- package/lib/module/ui/screens/AccountManagementDemo.js.map +1 -0
- package/lib/module/ui/screens/AccountOverviewScreen.js +671 -403
- package/lib/module/ui/screens/AccountOverviewScreen.js.map +1 -1
- package/lib/module/ui/screens/AccountSettingsScreen.js +698 -501
- package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/module/ui/screens/AccountSwitcherScreen.js +450 -488
- package/lib/module/ui/screens/AccountSwitcherScreen.js.map +1 -1
- package/lib/module/ui/screens/AppInfoScreen.js +498 -186
- package/lib/module/ui/screens/AppInfoScreen.js.map +1 -1
- package/lib/module/ui/screens/BillingManagementScreen.js +631 -0
- package/lib/module/ui/screens/BillingManagementScreen.js.map +1 -0
- package/lib/module/ui/screens/FileManagementScreen.js +2492 -0
- package/lib/module/ui/screens/FileManagementScreen.js.map +1 -0
- package/lib/module/ui/screens/PremiumSubscriptionScreen.js +1615 -0
- package/lib/module/ui/screens/PremiumSubscriptionScreen.js.map +1 -0
- package/lib/module/ui/screens/ProfileScreen.js +118 -14
- package/lib/module/ui/screens/ProfileScreen.js.map +1 -1
- package/lib/module/ui/screens/SessionManagementScreen.js.map +1 -1
- package/lib/module/ui/screens/SignInScreen.js +1 -1
- package/lib/module/ui/screens/SignInScreen.js.map +1 -1
- package/lib/module/ui/screens/SignUpScreen.js +1 -1
- package/lib/module/ui/screens/SignUpScreen.js.map +1 -1
- package/lib/module/utils/polyfills.js +36 -0
- package/lib/module/utils/polyfills.js.map +1 -0
- package/lib/typescript/assets/icons/OxyServices.d.ts +29 -0
- package/lib/typescript/assets/icons/OxyServices.d.ts.map +1 -0
- package/lib/typescript/core/index.d.ts +26 -1
- package/lib/typescript/core/index.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +1 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/lib/sonner.d.ts +5 -1
- package/lib/typescript/lib/sonner.d.ts.map +1 -1
- package/lib/typescript/models/interfaces.d.ts +1 -2
- package/lib/typescript/models/interfaces.d.ts.map +1 -1
- package/lib/typescript/node/index.d.ts +1 -0
- package/lib/typescript/node/index.d.ts.map +1 -1
- package/lib/typescript/ui/components/GroupedItem.d.ts +17 -0
- package/lib/typescript/ui/components/GroupedItem.d.ts.map +1 -0
- package/lib/typescript/ui/components/GroupedSection.d.ts +19 -0
- package/lib/typescript/ui/components/GroupedSection.d.ts.map +1 -0
- package/lib/typescript/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/ui/components/ProfileCard.d.ts +20 -0
- package/lib/typescript/ui/components/ProfileCard.d.ts.map +1 -0
- package/lib/typescript/ui/components/QuickActions.d.ts +15 -0
- package/lib/typescript/ui/components/QuickActions.d.ts.map +1 -0
- package/lib/typescript/ui/components/Section.d.ts +11 -0
- package/lib/typescript/ui/components/Section.d.ts.map +1 -0
- package/lib/typescript/ui/components/SectionTitle.d.ts +9 -0
- package/lib/typescript/ui/components/SectionTitle.d.ts.map +1 -0
- package/lib/typescript/ui/components/bottomSheet/index.d.ts +3 -2
- package/lib/typescript/ui/components/bottomSheet/index.d.ts.map +1 -1
- package/lib/typescript/ui/components/index.d.ts +13 -0
- package/lib/typescript/ui/components/index.d.ts.map +1 -0
- package/lib/typescript/ui/navigation/OxyRouter.d.ts.map +1 -1
- package/lib/typescript/ui/navigation/types.d.ts +8 -0
- package/lib/typescript/ui/navigation/types.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AccountCenterScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AccountManagementDemo.d.ts +8 -0
- package/lib/typescript/ui/screens/AccountManagementDemo.d.ts.map +1 -0
- package/lib/typescript/ui/screens/AccountOverviewScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts +1 -4
- package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AccountSwitcherScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AppInfoScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/BillingManagementScreen.d.ts +5 -0
- package/lib/typescript/ui/screens/BillingManagementScreen.d.ts.map +1 -0
- package/lib/typescript/ui/screens/FileManagementScreen.d.ts +8 -0
- package/lib/typescript/ui/screens/FileManagementScreen.d.ts.map +1 -0
- package/lib/typescript/ui/screens/PremiumSubscriptionScreen.d.ts +5 -0
- package/lib/typescript/ui/screens/PremiumSubscriptionScreen.d.ts.map +1 -0
- package/lib/typescript/ui/screens/ProfileScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/SessionManagementScreen.d.ts.map +1 -1
- package/lib/typescript/utils/polyfills.d.ts +6 -0
- package/lib/typescript/utils/polyfills.d.ts.map +1 -0
- package/package.json +11 -3
- package/src/__tests__/polyfills.test.ts +30 -0
- package/src/__tests__/setup.ts +43 -0
- package/src/__tests__/ui/screens/AccountSettingsScreen.test.tsx +8 -8
- package/src/assets/icons/OxyServices.tsx +67 -0
- package/src/assets/icons/logo_OxyServices.svg +1 -0
- package/src/core/index.ts +127 -19
- package/src/index.ts +3 -0
- package/src/lib/sonner.ts +10 -1
- package/src/models/interfaces.ts +1 -2
- package/src/node/index.ts +3 -0
- package/src/ui/components/GroupedItem.tsx +118 -0
- package/src/ui/components/GroupedSection.tsx +45 -0
- package/src/ui/components/OxyProvider.tsx +95 -120
- package/src/ui/components/ProfileCard.tsx +129 -0
- package/src/ui/components/QuickActions.tsx +90 -0
- package/src/ui/components/Section.tsx +37 -0
- package/src/ui/components/SectionTitle.tsx +31 -0
- package/src/ui/components/bottomSheet/index.tsx +13 -11
- package/src/ui/components/index.ts +15 -0
- package/src/ui/navigation/OxyRouter.tsx +20 -3
- package/src/ui/navigation/types.ts +10 -1
- package/src/ui/screens/AccountCenterScreen.tsx +188 -159
- package/src/ui/screens/AccountManagementDemo.tsx +297 -0
- package/src/ui/screens/AccountOverviewScreen.tsx +474 -310
- package/src/ui/screens/AccountSettingsScreen.tsx +648 -463
- package/src/ui/screens/AccountSwitcherScreen.tsx +385 -449
- package/src/ui/screens/AppInfoScreen.tsx +571 -140
- package/src/ui/screens/BillingManagementScreen.tsx +589 -0
- package/src/ui/screens/FileManagementScreen.tsx +2513 -0
- package/src/ui/screens/PremiumSubscriptionScreen.tsx +1628 -0
- package/src/ui/screens/ProfileScreen.tsx +101 -7
- package/src/ui/screens/SessionManagementScreen.tsx +1 -0
- package/src/ui/screens/SignInScreen.tsx +1 -1
- package/src/ui/screens/SignUpScreen.tsx +1 -1
- package/src/utils/polyfills.ts +34 -0
- package/lib/commonjs/lib/sonner.web.js +0 -17
- package/lib/commonjs/lib/sonner.web.js.map +0 -1
- package/lib/module/lib/sonner.web.js +0 -4
- package/lib/module/lib/sonner.web.js.map +0 -1
- package/lib/typescript/__tests__/ui/screens/AccountSettingsScreen.test.d.ts +0 -2
- package/lib/typescript/__tests__/ui/screens/AccountSettingsScreen.test.d.ts.map +0 -1
- package/lib/typescript/lib/sonner.web.d.ts +0 -2
- package/lib/typescript/lib/sonner.web.d.ts.map +0 -1
- package/src/lib/sonner.web.ts +0 -1
|
@@ -0,0 +1,2513 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
TouchableOpacity,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
ScrollView,
|
|
8
|
+
ActivityIndicator,
|
|
9
|
+
Platform,
|
|
10
|
+
RefreshControl,
|
|
11
|
+
Dimensions,
|
|
12
|
+
Modal,
|
|
13
|
+
TextInput,
|
|
14
|
+
Image,
|
|
15
|
+
} from 'react-native';
|
|
16
|
+
import { BaseScreenProps } from '../navigation/types';
|
|
17
|
+
import { useOxy } from '../context/OxyContext';
|
|
18
|
+
import { fontFamilies } from '../styles/fonts';
|
|
19
|
+
import { toast } from '../../lib/sonner';
|
|
20
|
+
import { Ionicons } from '@expo/vector-icons';
|
|
21
|
+
import { FileMetadata } from '../../models/interfaces';
|
|
22
|
+
|
|
23
|
+
interface FileManagementScreenProps extends BaseScreenProps {
|
|
24
|
+
userId?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
28
|
+
onClose,
|
|
29
|
+
theme,
|
|
30
|
+
goBack,
|
|
31
|
+
navigate,
|
|
32
|
+
userId,
|
|
33
|
+
containerWidth = 400, // Fallback for when not provided by the router
|
|
34
|
+
}) => {
|
|
35
|
+
const { user, oxyServices } = useOxy();
|
|
36
|
+
|
|
37
|
+
// Debug: log the actual container width
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
console.log('[FileManagementScreen] Container width (full):', containerWidth);
|
|
40
|
+
// Padding structure:
|
|
41
|
+
// - containerWidth = full bottom sheet container width (measured from OxyProvider)
|
|
42
|
+
// - photoScrollContainer adds padding: 16 (32px total horizontal padding)
|
|
43
|
+
// - Available content width = containerWidth - 32
|
|
44
|
+
const availableContentWidth = containerWidth - 32;
|
|
45
|
+
console.log('[FileManagementScreen] Available content width:', availableContentWidth);
|
|
46
|
+
console.log('[FileManagementScreen] Spacing fix applied: 4px uniform gap both horizontal and vertical');
|
|
47
|
+
}, [containerWidth]);
|
|
48
|
+
const [files, setFiles] = useState<FileMetadata[]>([]);
|
|
49
|
+
const [loading, setLoading] = useState(true);
|
|
50
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
51
|
+
const [uploading, setUploading] = useState(false);
|
|
52
|
+
const [uploadProgress, setUploadProgress] = useState<{current: number, total: number} | null>(null);
|
|
53
|
+
const [deleting, setDeleting] = useState<string | null>(null);
|
|
54
|
+
const [selectedFile, setSelectedFile] = useState<FileMetadata | null>(null);
|
|
55
|
+
const [showFileDetails, setShowFileDetails] = useState(false);
|
|
56
|
+
const [openedFile, setOpenedFile] = useState<FileMetadata | null>(null);
|
|
57
|
+
const [fileContent, setFileContent] = useState<string | null>(null);
|
|
58
|
+
const [loadingFileContent, setLoadingFileContent] = useState(false);
|
|
59
|
+
const [showFileDetailsInViewer, setShowFileDetailsInViewer] = useState(false);
|
|
60
|
+
const [viewMode, setViewMode] = useState<'all' | 'photos'>('all');
|
|
61
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
62
|
+
const [filteredFiles, setFilteredFiles] = useState<FileMetadata[]>([]);
|
|
63
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
64
|
+
const [photoDimensions, setPhotoDimensions] = useState<{[key: string]: {width: number, height: number}}>({});
|
|
65
|
+
const [loadingDimensions, setLoadingDimensions] = useState(false);
|
|
66
|
+
const [hoveredPreview, setHoveredPreview] = useState<string | null>(null);
|
|
67
|
+
|
|
68
|
+
const isDarkTheme = theme === 'dark';
|
|
69
|
+
const textColor = isDarkTheme ? '#FFFFFF' : '#000000';
|
|
70
|
+
const backgroundColor = isDarkTheme ? '#121212' : '#f2f2f2';
|
|
71
|
+
const secondaryBackgroundColor = isDarkTheme ? '#222222' : '#FFFFFF';
|
|
72
|
+
const borderColor = isDarkTheme ? '#444444' : '#E0E0E0';
|
|
73
|
+
const primaryColor = '#007AFF';
|
|
74
|
+
const dangerColor = '#FF3B30';
|
|
75
|
+
const successColor = '#34C759';
|
|
76
|
+
|
|
77
|
+
const targetUserId = userId || user?.id;
|
|
78
|
+
|
|
79
|
+
const loadFiles = useCallback(async (isRefresh = false) => {
|
|
80
|
+
if (!targetUserId) return;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
if (isRefresh) {
|
|
84
|
+
setRefreshing(true);
|
|
85
|
+
} else {
|
|
86
|
+
setLoading(true);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const response = await oxyServices.listUserFiles(targetUserId);
|
|
90
|
+
setFiles(response.files || []);
|
|
91
|
+
} catch (error: any) {
|
|
92
|
+
console.error('Failed to load files:', error);
|
|
93
|
+
toast.error(error.message || 'Failed to load files');
|
|
94
|
+
} finally {
|
|
95
|
+
setLoading(false);
|
|
96
|
+
setRefreshing(false);
|
|
97
|
+
}
|
|
98
|
+
}, [targetUserId, oxyServices]);
|
|
99
|
+
|
|
100
|
+
// Filter files based on search query and view mode
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
let filteredByMode = files;
|
|
103
|
+
|
|
104
|
+
// Filter by view mode first
|
|
105
|
+
if (viewMode === 'photos') {
|
|
106
|
+
filteredByMode = files.filter(file => file.contentType.startsWith('image/'));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Then filter by search query
|
|
110
|
+
if (!searchQuery.trim()) {
|
|
111
|
+
setFilteredFiles(filteredByMode);
|
|
112
|
+
} else {
|
|
113
|
+
const query = searchQuery.toLowerCase();
|
|
114
|
+
const filtered = filteredByMode.filter(file =>
|
|
115
|
+
file.filename.toLowerCase().includes(query) ||
|
|
116
|
+
file.contentType.toLowerCase().includes(query) ||
|
|
117
|
+
(file.metadata?.description && file.metadata.description.toLowerCase().includes(query))
|
|
118
|
+
);
|
|
119
|
+
setFilteredFiles(filtered);
|
|
120
|
+
}
|
|
121
|
+
}, [files, searchQuery, viewMode]);
|
|
122
|
+
|
|
123
|
+
// Load photo dimensions for justified grid
|
|
124
|
+
const loadPhotoDimensions = useCallback(async (photos: FileMetadata[]) => {
|
|
125
|
+
if (photos.length === 0) return;
|
|
126
|
+
|
|
127
|
+
setLoadingDimensions(true);
|
|
128
|
+
const newDimensions: {[key: string]: {width: number, height: number}} = { ...photoDimensions };
|
|
129
|
+
let hasNewDimensions = false;
|
|
130
|
+
|
|
131
|
+
// Only load dimensions for photos we don't have yet
|
|
132
|
+
const photosToLoad = photos.filter(photo => !newDimensions[photo.id]);
|
|
133
|
+
|
|
134
|
+
if (photosToLoad.length === 0) {
|
|
135
|
+
setLoadingDimensions(false);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
await Promise.all(
|
|
141
|
+
photosToLoad.map(async (photo) => {
|
|
142
|
+
try {
|
|
143
|
+
const downloadUrl = oxyServices.getFileDownloadUrl(photo.id);
|
|
144
|
+
|
|
145
|
+
if (Platform.OS === 'web') {
|
|
146
|
+
const img = new (window as any).Image();
|
|
147
|
+
await new Promise<void>((resolve, reject) => {
|
|
148
|
+
img.onload = () => {
|
|
149
|
+
newDimensions[photo.id] = {
|
|
150
|
+
width: img.naturalWidth,
|
|
151
|
+
height: img.naturalHeight
|
|
152
|
+
};
|
|
153
|
+
hasNewDimensions = true;
|
|
154
|
+
resolve();
|
|
155
|
+
};
|
|
156
|
+
img.onerror = () => {
|
|
157
|
+
// Fallback dimensions for failed loads
|
|
158
|
+
newDimensions[photo.id] = { width: 1, height: 1 };
|
|
159
|
+
hasNewDimensions = true;
|
|
160
|
+
resolve();
|
|
161
|
+
};
|
|
162
|
+
img.src = downloadUrl;
|
|
163
|
+
});
|
|
164
|
+
} else {
|
|
165
|
+
// For mobile, use Image.getSize from react-native
|
|
166
|
+
await new Promise<void>((resolve) => {
|
|
167
|
+
Image.getSize(
|
|
168
|
+
downloadUrl,
|
|
169
|
+
(width: number, height: number) => {
|
|
170
|
+
newDimensions[photo.id] = { width, height };
|
|
171
|
+
hasNewDimensions = true;
|
|
172
|
+
resolve();
|
|
173
|
+
},
|
|
174
|
+
() => {
|
|
175
|
+
// Fallback dimensions
|
|
176
|
+
newDimensions[photo.id] = { width: 1, height: 1 };
|
|
177
|
+
hasNewDimensions = true;
|
|
178
|
+
resolve();
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
} catch (error) {
|
|
184
|
+
// Fallback dimensions for any errors
|
|
185
|
+
newDimensions[photo.id] = { width: 1, height: 1 };
|
|
186
|
+
hasNewDimensions = true;
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (hasNewDimensions) {
|
|
192
|
+
setPhotoDimensions(newDimensions);
|
|
193
|
+
}
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error('Error loading photo dimensions:', error);
|
|
196
|
+
} finally {
|
|
197
|
+
setLoadingDimensions(false);
|
|
198
|
+
}
|
|
199
|
+
}, [oxyServices, photoDimensions]);
|
|
200
|
+
|
|
201
|
+
// Create justified rows from photos with responsive algorithm
|
|
202
|
+
const createJustifiedRows = useCallback((photos: FileMetadata[]) => {
|
|
203
|
+
if (photos.length === 0) return [];
|
|
204
|
+
|
|
205
|
+
const rows: FileMetadata[][] = [];
|
|
206
|
+
const photosPerRow = 3; // Fixed 3 photos per row for consistency
|
|
207
|
+
|
|
208
|
+
for (let i = 0; i < photos.length; i += photosPerRow) {
|
|
209
|
+
const rowPhotos = photos.slice(i, i + photosPerRow);
|
|
210
|
+
rows.push(rowPhotos);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return rows;
|
|
214
|
+
}, []);
|
|
215
|
+
|
|
216
|
+
const processFileUploads = async (selectedFiles: File[]) => {
|
|
217
|
+
if (selectedFiles.length === 0) return;
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
// Show initial progress
|
|
221
|
+
setUploadProgress({ current: 0, total: selectedFiles.length });
|
|
222
|
+
|
|
223
|
+
// Validate file sizes (example: 50MB limit per file)
|
|
224
|
+
const maxSize = 50 * 1024 * 1024; // 50MB
|
|
225
|
+
const oversizedFiles = selectedFiles.filter(file => file.size > maxSize);
|
|
226
|
+
|
|
227
|
+
if (oversizedFiles.length > 0) {
|
|
228
|
+
const fileList = oversizedFiles.map(f => f.name).join('\n');
|
|
229
|
+
window.alert(`File Size Limit\n\nThe following files are too large (max 50MB):\n${fileList}`);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Option 1: Bulk upload (faster, all-or-nothing) for 5 or fewer files
|
|
234
|
+
if (selectedFiles.length <= 5) {
|
|
235
|
+
const filenames = selectedFiles.map(f => f.name);
|
|
236
|
+
const response = await oxyServices.uploadFiles(
|
|
237
|
+
selectedFiles,
|
|
238
|
+
filenames,
|
|
239
|
+
{
|
|
240
|
+
userId: targetUserId,
|
|
241
|
+
uploadDate: new Date().toISOString(),
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
toast.success(`${response.files.length} file(s) uploaded successfully`);
|
|
246
|
+
// Small delay to ensure backend processing is complete
|
|
247
|
+
setTimeout(async () => {
|
|
248
|
+
await loadFiles();
|
|
249
|
+
}, 500);
|
|
250
|
+
} else {
|
|
251
|
+
// Option 2: Individual uploads for better progress and error handling
|
|
252
|
+
let successCount = 0;
|
|
253
|
+
let failureCount = 0;
|
|
254
|
+
const errors: string[] = [];
|
|
255
|
+
|
|
256
|
+
for (let i = 0; i < selectedFiles.length; i++) {
|
|
257
|
+
const file = selectedFiles[i];
|
|
258
|
+
setUploadProgress({ current: i + 1, total: selectedFiles.length });
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
await oxyServices.uploadFile(file, file.name, {
|
|
262
|
+
userId: targetUserId,
|
|
263
|
+
uploadDate: new Date().toISOString(),
|
|
264
|
+
});
|
|
265
|
+
successCount++;
|
|
266
|
+
} catch (error: any) {
|
|
267
|
+
failureCount++;
|
|
268
|
+
errors.push(`${file.name}: ${error.message || 'Upload failed'}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Show results summary
|
|
273
|
+
if (successCount > 0) {
|
|
274
|
+
toast.success(`${successCount} file(s) uploaded successfully`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (failureCount > 0) {
|
|
278
|
+
const errorMessage = `${failureCount} file(s) failed to upload${errors.length > 0 ? ':\n' + errors.slice(0, 3).join('\n') + (errors.length > 3 ? '\n...' : '') : ''}`;
|
|
279
|
+
toast.error(errorMessage);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Small delay to ensure backend processing is complete
|
|
283
|
+
setTimeout(async () => {
|
|
284
|
+
await loadFiles();
|
|
285
|
+
}, 500);
|
|
286
|
+
}
|
|
287
|
+
} catch (error: any) {
|
|
288
|
+
console.error('Upload error:', error);
|
|
289
|
+
toast.error(error.message || 'Failed to upload files');
|
|
290
|
+
} finally {
|
|
291
|
+
setUploadProgress(null);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const handleFileUpload = async () => {
|
|
296
|
+
try {
|
|
297
|
+
setUploading(true);
|
|
298
|
+
setUploadProgress(null);
|
|
299
|
+
|
|
300
|
+
if (Platform.OS === 'web') {
|
|
301
|
+
// Web file picker implementation
|
|
302
|
+
const input = document.createElement('input');
|
|
303
|
+
input.type = 'file';
|
|
304
|
+
input.multiple = true;
|
|
305
|
+
input.accept = '*/*';
|
|
306
|
+
|
|
307
|
+
input.onchange = async (e: any) => {
|
|
308
|
+
const selectedFiles = Array.from(e.target.files) as File[];
|
|
309
|
+
await processFileUploads(selectedFiles);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
input.click();
|
|
313
|
+
} else {
|
|
314
|
+
// Mobile - show info that file picker can be added
|
|
315
|
+
const installCommand = 'npm install expo-document-picker';
|
|
316
|
+
const message = `Mobile File Upload\n\nTo enable file uploads on mobile, install expo-document-picker:\n\n${installCommand}\n\nThen import and use DocumentPicker.getDocumentAsync() in this method.`;
|
|
317
|
+
|
|
318
|
+
if (window.confirm(`${message}\n\nWould you like to copy the install command?`)) {
|
|
319
|
+
toast.info(`Install: ${installCommand}`);
|
|
320
|
+
} else {
|
|
321
|
+
toast.info('Mobile file upload requires expo-document-picker');
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
} catch (error: any) {
|
|
325
|
+
toast.error(error.message || 'Failed to upload file');
|
|
326
|
+
} finally {
|
|
327
|
+
setUploading(false);
|
|
328
|
+
setUploadProgress(null);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const handleFileDelete = async (fileId: string, filename: string) => {
|
|
333
|
+
// Use web-compatible confirmation dialog
|
|
334
|
+
const confirmed = window.confirm(`Are you sure you want to delete "${filename}"? This action cannot be undone.`);
|
|
335
|
+
|
|
336
|
+
if (!confirmed) {
|
|
337
|
+
console.log('Delete cancelled by user');
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
console.log('Deleting file:', { fileId, filename });
|
|
343
|
+
console.log('Target user ID:', targetUserId);
|
|
344
|
+
console.log('Current user ID:', user?.id);
|
|
345
|
+
setDeleting(fileId);
|
|
346
|
+
|
|
347
|
+
const result = await oxyServices.deleteFile(fileId);
|
|
348
|
+
console.log('Delete result:', result);
|
|
349
|
+
|
|
350
|
+
toast.success('File deleted successfully');
|
|
351
|
+
|
|
352
|
+
// Reload files after successful deletion
|
|
353
|
+
setTimeout(async () => {
|
|
354
|
+
await loadFiles();
|
|
355
|
+
}, 500);
|
|
356
|
+
} catch (error: any) {
|
|
357
|
+
console.error('Delete error:', error);
|
|
358
|
+
console.error('Error details:', error.response?.data || error.message);
|
|
359
|
+
|
|
360
|
+
// Provide specific error messages
|
|
361
|
+
if (error.message?.includes('File not found') || error.message?.includes('404')) {
|
|
362
|
+
toast.error('File not found. It may have already been deleted.');
|
|
363
|
+
// Still reload files to refresh the list
|
|
364
|
+
setTimeout(async () => {
|
|
365
|
+
await loadFiles();
|
|
366
|
+
}, 500);
|
|
367
|
+
} else if (error.message?.includes('permission') || error.message?.includes('403')) {
|
|
368
|
+
toast.error('You do not have permission to delete this file.');
|
|
369
|
+
} else {
|
|
370
|
+
toast.error(error.message || 'Failed to delete file');
|
|
371
|
+
}
|
|
372
|
+
} finally {
|
|
373
|
+
setDeleting(null);
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// Drag and drop handlers for web
|
|
378
|
+
const handleDragOver = (e: any) => {
|
|
379
|
+
if (Platform.OS === 'web' && user?.id === targetUserId) {
|
|
380
|
+
e.preventDefault();
|
|
381
|
+
setIsDragging(true);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const handleDragLeave = (e: any) => {
|
|
386
|
+
if (Platform.OS === 'web') {
|
|
387
|
+
e.preventDefault();
|
|
388
|
+
setIsDragging(false);
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const handleDrop = async (e: any) => {
|
|
393
|
+
if (Platform.OS === 'web' && user?.id === targetUserId) {
|
|
394
|
+
e.preventDefault();
|
|
395
|
+
setIsDragging(false);
|
|
396
|
+
setUploading(true);
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
const files = Array.from(e.dataTransfer.files) as File[];
|
|
400
|
+
await processFileUploads(files);
|
|
401
|
+
} catch (error: any) {
|
|
402
|
+
toast.error(error.message || 'Failed to upload files');
|
|
403
|
+
} finally {
|
|
404
|
+
setUploading(false);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const handleFileDownload = async (fileId: string, filename: string) => {
|
|
410
|
+
try {
|
|
411
|
+
if (Platform.OS === 'web') {
|
|
412
|
+
console.log('Downloading file:', { fileId, filename });
|
|
413
|
+
|
|
414
|
+
// Use the public download URL method
|
|
415
|
+
const downloadUrl = oxyServices.getFileDownloadUrl(fileId);
|
|
416
|
+
console.log('Download URL:', downloadUrl);
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
// Method 1: Try simple link download first
|
|
420
|
+
const link = document.createElement('a');
|
|
421
|
+
link.href = downloadUrl;
|
|
422
|
+
link.download = filename;
|
|
423
|
+
link.target = '_blank';
|
|
424
|
+
document.body.appendChild(link);
|
|
425
|
+
link.click();
|
|
426
|
+
document.body.removeChild(link);
|
|
427
|
+
|
|
428
|
+
toast.success('File download started');
|
|
429
|
+
} catch (linkError) {
|
|
430
|
+
console.warn('Link download failed, trying fetch method:', linkError);
|
|
431
|
+
|
|
432
|
+
// Method 2: Fallback to fetch download
|
|
433
|
+
const response = await fetch(downloadUrl);
|
|
434
|
+
if (!response.ok) {
|
|
435
|
+
if (response.status === 404) {
|
|
436
|
+
throw new Error('File not found. It may have been deleted.');
|
|
437
|
+
} else {
|
|
438
|
+
throw new Error(`Download failed: ${response.status} ${response.statusText}`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const blob = await response.blob();
|
|
443
|
+
const url = window.URL.createObjectURL(blob);
|
|
444
|
+
|
|
445
|
+
const link = document.createElement('a');
|
|
446
|
+
link.href = url;
|
|
447
|
+
link.download = filename;
|
|
448
|
+
document.body.appendChild(link);
|
|
449
|
+
link.click();
|
|
450
|
+
document.body.removeChild(link);
|
|
451
|
+
|
|
452
|
+
// Clean up the blob URL
|
|
453
|
+
window.URL.revokeObjectURL(url);
|
|
454
|
+
|
|
455
|
+
toast.success('File downloaded successfully');
|
|
456
|
+
}
|
|
457
|
+
} else {
|
|
458
|
+
toast.info('File download not implemented for mobile yet');
|
|
459
|
+
}
|
|
460
|
+
} catch (error: any) {
|
|
461
|
+
console.error('Download error:', error);
|
|
462
|
+
toast.error(error.message || 'Failed to download file');
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
const formatFileSize = (bytes: number): string => {
|
|
467
|
+
if (bytes === 0) return '0 Bytes';
|
|
468
|
+
const k = 1024;
|
|
469
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
470
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
471
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const getFileIcon = (contentType: string): string => {
|
|
475
|
+
if (contentType.startsWith('image/')) return 'image';
|
|
476
|
+
if (contentType.startsWith('video/')) return 'videocam';
|
|
477
|
+
if (contentType.startsWith('audio/')) return 'musical-notes';
|
|
478
|
+
if (contentType.includes('pdf')) return 'document-text';
|
|
479
|
+
if (contentType.includes('word') || contentType.includes('doc')) return 'document';
|
|
480
|
+
if (contentType.includes('excel') || contentType.includes('sheet')) return 'grid';
|
|
481
|
+
if (contentType.includes('zip') || contentType.includes('archive')) return 'archive';
|
|
482
|
+
return 'document-outline';
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const handleFileOpen = async (file: FileMetadata) => {
|
|
486
|
+
try {
|
|
487
|
+
setLoadingFileContent(true);
|
|
488
|
+
setOpenedFile(file);
|
|
489
|
+
|
|
490
|
+
// For text files, images, and other viewable content, try to load the content
|
|
491
|
+
if (file.contentType.startsWith('text/') ||
|
|
492
|
+
file.contentType.includes('json') ||
|
|
493
|
+
file.contentType.includes('xml') ||
|
|
494
|
+
file.contentType.includes('javascript') ||
|
|
495
|
+
file.contentType.includes('typescript') ||
|
|
496
|
+
file.contentType.startsWith('image/') ||
|
|
497
|
+
file.contentType.includes('pdf') ||
|
|
498
|
+
file.contentType.startsWith('video/') ||
|
|
499
|
+
file.contentType.startsWith('audio/')) {
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
const downloadUrl = oxyServices.getFileDownloadUrl(file.id);
|
|
503
|
+
const response = await fetch(downloadUrl);
|
|
504
|
+
|
|
505
|
+
if (response.ok) {
|
|
506
|
+
if (file.contentType.startsWith('image/') ||
|
|
507
|
+
file.contentType.includes('pdf') ||
|
|
508
|
+
file.contentType.startsWith('video/') ||
|
|
509
|
+
file.contentType.startsWith('audio/')) {
|
|
510
|
+
// For images, PDFs, videos, and audio, we'll use the URL directly
|
|
511
|
+
setFileContent(downloadUrl);
|
|
512
|
+
} else {
|
|
513
|
+
// For text files, get the content
|
|
514
|
+
const content = await response.text();
|
|
515
|
+
setFileContent(content);
|
|
516
|
+
}
|
|
517
|
+
} else {
|
|
518
|
+
if (response.status === 404) {
|
|
519
|
+
toast.error('File not found. It may have been deleted.');
|
|
520
|
+
} else {
|
|
521
|
+
toast.error(`Failed to load file: ${response.status} ${response.statusText}`);
|
|
522
|
+
}
|
|
523
|
+
setFileContent(null);
|
|
524
|
+
}
|
|
525
|
+
} catch (error: any) {
|
|
526
|
+
console.error('Failed to load file content:', error);
|
|
527
|
+
if (error.message?.includes('404') || error.message?.includes('not found')) {
|
|
528
|
+
toast.error('File not found. It may have been deleted.');
|
|
529
|
+
} else {
|
|
530
|
+
toast.error('Failed to load file content');
|
|
531
|
+
}
|
|
532
|
+
setFileContent(null);
|
|
533
|
+
}
|
|
534
|
+
} else {
|
|
535
|
+
// For non-viewable files, don't load content
|
|
536
|
+
setFileContent(null);
|
|
537
|
+
}
|
|
538
|
+
} catch (error: any) {
|
|
539
|
+
console.error('Failed to open file:', error);
|
|
540
|
+
toast.error(error.message || 'Failed to open file');
|
|
541
|
+
} finally {
|
|
542
|
+
setLoadingFileContent(false);
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
const handleCloseFile = () => {
|
|
547
|
+
setOpenedFile(null);
|
|
548
|
+
setFileContent(null);
|
|
549
|
+
setShowFileDetailsInViewer(false);
|
|
550
|
+
// Don't reset view mode when closing a file
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
const showFileDetailsModal = (file: FileMetadata) => {
|
|
554
|
+
setSelectedFile(file);
|
|
555
|
+
setShowFileDetails(true);
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
const renderSimplePhotoItem = useCallback((photo: FileMetadata, index: number) => {
|
|
559
|
+
const downloadUrl = oxyServices.getFileDownloadUrl(photo.id);
|
|
560
|
+
|
|
561
|
+
// Calculate photo item width based on actual container size from bottom sheet
|
|
562
|
+
let itemsPerRow = 3; // Default for mobile
|
|
563
|
+
if (containerWidth > 768) itemsPerRow = 4; // Desktop/tablet
|
|
564
|
+
else if (containerWidth > 480) itemsPerRow = 3; // Large mobile
|
|
565
|
+
|
|
566
|
+
// Account for the photoScrollContainer padding (16px on each side = 32px total)
|
|
567
|
+
const scrollContainerPadding = 32; // Total horizontal padding from photoScrollContainer
|
|
568
|
+
const gaps = (itemsPerRow - 1) * 4; // Gap between items (4px)
|
|
569
|
+
const availableWidth = containerWidth - scrollContainerPadding;
|
|
570
|
+
const itemWidth = (availableWidth - gaps) / itemsPerRow;
|
|
571
|
+
|
|
572
|
+
return (
|
|
573
|
+
<TouchableOpacity
|
|
574
|
+
key={photo.id}
|
|
575
|
+
style={[
|
|
576
|
+
styles.simplePhotoItem,
|
|
577
|
+
{
|
|
578
|
+
width: itemWidth,
|
|
579
|
+
height: itemWidth,
|
|
580
|
+
marginRight: (index + 1) % itemsPerRow === 0 ? 0 : 4,
|
|
581
|
+
}
|
|
582
|
+
]}
|
|
583
|
+
onPress={() => handleFileOpen(photo)}
|
|
584
|
+
activeOpacity={0.8}
|
|
585
|
+
>
|
|
586
|
+
<View style={styles.simplePhotoContainer}>
|
|
587
|
+
{Platform.OS === 'web' ? (
|
|
588
|
+
<img
|
|
589
|
+
src={downloadUrl}
|
|
590
|
+
alt={photo.filename}
|
|
591
|
+
style={{
|
|
592
|
+
width: '100%',
|
|
593
|
+
height: '100%',
|
|
594
|
+
objectFit: 'cover',
|
|
595
|
+
borderRadius: 8,
|
|
596
|
+
transition: 'transform 0.2s ease',
|
|
597
|
+
}}
|
|
598
|
+
loading="lazy"
|
|
599
|
+
onError={(e) => {
|
|
600
|
+
console.error('Photo failed to load:', e);
|
|
601
|
+
}}
|
|
602
|
+
onMouseEnter={(e) => {
|
|
603
|
+
e.currentTarget.style.transform = 'scale(1.05)';
|
|
604
|
+
}}
|
|
605
|
+
onMouseLeave={(e) => {
|
|
606
|
+
e.currentTarget.style.transform = 'scale(1)';
|
|
607
|
+
}}
|
|
608
|
+
/>
|
|
609
|
+
) : (
|
|
610
|
+
<Image
|
|
611
|
+
source={{ uri: downloadUrl }}
|
|
612
|
+
style={styles.simplePhotoImage}
|
|
613
|
+
resizeMode="cover"
|
|
614
|
+
onError={(e) => {
|
|
615
|
+
console.error('Photo failed to load:', e);
|
|
616
|
+
}}
|
|
617
|
+
/>
|
|
618
|
+
)}
|
|
619
|
+
</View>
|
|
620
|
+
</TouchableOpacity>
|
|
621
|
+
);
|
|
622
|
+
}, [oxyServices, containerWidth]);
|
|
623
|
+
|
|
624
|
+
const renderJustifiedPhotoItem = useCallback((photo: FileMetadata, width: number, height: number, isLast: boolean) => {
|
|
625
|
+
const downloadUrl = oxyServices.getFileDownloadUrl(photo.id);
|
|
626
|
+
|
|
627
|
+
return (
|
|
628
|
+
<TouchableOpacity
|
|
629
|
+
key={photo.id}
|
|
630
|
+
style={[
|
|
631
|
+
styles.justifiedPhotoItem,
|
|
632
|
+
{
|
|
633
|
+
width,
|
|
634
|
+
height,
|
|
635
|
+
}
|
|
636
|
+
]}
|
|
637
|
+
onPress={() => handleFileOpen(photo)}
|
|
638
|
+
activeOpacity={0.8}
|
|
639
|
+
>
|
|
640
|
+
<View style={styles.justifiedPhotoContainer}>
|
|
641
|
+
{Platform.OS === 'web' ? (
|
|
642
|
+
<img
|
|
643
|
+
src={downloadUrl}
|
|
644
|
+
alt={photo.filename}
|
|
645
|
+
style={{
|
|
646
|
+
width: '100%',
|
|
647
|
+
height: '100%',
|
|
648
|
+
objectFit: 'cover',
|
|
649
|
+
borderRadius: 6,
|
|
650
|
+
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
|
|
651
|
+
}}
|
|
652
|
+
loading="lazy"
|
|
653
|
+
onError={(e) => {
|
|
654
|
+
console.error('Photo failed to load:', e);
|
|
655
|
+
}}
|
|
656
|
+
onMouseEnter={(e) => {
|
|
657
|
+
e.currentTarget.style.transform = 'scale(1.02)';
|
|
658
|
+
e.currentTarget.style.boxShadow = '0 8px 25px rgba(0,0,0,0.15)';
|
|
659
|
+
e.currentTarget.style.zIndex = '10';
|
|
660
|
+
}}
|
|
661
|
+
onMouseLeave={(e) => {
|
|
662
|
+
e.currentTarget.style.transform = 'scale(1)';
|
|
663
|
+
e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
|
|
664
|
+
e.currentTarget.style.zIndex = '1';
|
|
665
|
+
}}
|
|
666
|
+
/>
|
|
667
|
+
) : (
|
|
668
|
+
<Image
|
|
669
|
+
source={{ uri: downloadUrl }}
|
|
670
|
+
style={styles.justifiedPhotoImage}
|
|
671
|
+
resizeMode="cover"
|
|
672
|
+
onError={(e) => {
|
|
673
|
+
console.error('Photo failed to load:', e);
|
|
674
|
+
}}
|
|
675
|
+
/>
|
|
676
|
+
)}
|
|
677
|
+
</View>
|
|
678
|
+
</TouchableOpacity>
|
|
679
|
+
);
|
|
680
|
+
}, [oxyServices]);
|
|
681
|
+
|
|
682
|
+
useEffect(() => {
|
|
683
|
+
loadFiles();
|
|
684
|
+
}, [loadFiles]);
|
|
685
|
+
|
|
686
|
+
const renderFileItem = (file: FileMetadata) => {
|
|
687
|
+
const isImage = file.contentType.startsWith('image/');
|
|
688
|
+
const isPDF = file.contentType.includes('pdf');
|
|
689
|
+
const isVideo = file.contentType.startsWith('video/');
|
|
690
|
+
const isAudio = file.contentType.startsWith('audio/');
|
|
691
|
+
const hasPreview = isImage || isPDF || isVideo;
|
|
692
|
+
|
|
693
|
+
return (
|
|
694
|
+
<View
|
|
695
|
+
key={file.id}
|
|
696
|
+
style={[styles.fileItem, { backgroundColor: secondaryBackgroundColor, borderColor }]}
|
|
697
|
+
>
|
|
698
|
+
<TouchableOpacity
|
|
699
|
+
style={styles.fileContent}
|
|
700
|
+
onPress={() => handleFileOpen(file)}
|
|
701
|
+
>
|
|
702
|
+
{/* Preview Thumbnail */}
|
|
703
|
+
<View style={styles.filePreviewContainer}>
|
|
704
|
+
{hasPreview ? (
|
|
705
|
+
<View
|
|
706
|
+
style={styles.filePreview}
|
|
707
|
+
{...(Platform.OS === 'web' && {
|
|
708
|
+
onMouseEnter: () => setHoveredPreview(file.id),
|
|
709
|
+
onMouseLeave: () => setHoveredPreview(null),
|
|
710
|
+
})}
|
|
711
|
+
>
|
|
712
|
+
{isImage && (
|
|
713
|
+
Platform.OS === 'web' ? (
|
|
714
|
+
<img
|
|
715
|
+
src={oxyServices.getFileDownloadUrl(file.id)}
|
|
716
|
+
style={{
|
|
717
|
+
width: '100%',
|
|
718
|
+
height: '100%',
|
|
719
|
+
objectFit: 'cover',
|
|
720
|
+
borderRadius: 8,
|
|
721
|
+
transition: 'transform 0.2s ease',
|
|
722
|
+
transform: hoveredPreview === file.id ? 'scale(1.05)' : 'scale(1)',
|
|
723
|
+
}}
|
|
724
|
+
onError={(e) => {
|
|
725
|
+
// Show fallback icon if image fails to load
|
|
726
|
+
e.currentTarget.style.display = 'none';
|
|
727
|
+
const fallbackElement = e.currentTarget.parentElement?.querySelector('[data-fallback="true"]');
|
|
728
|
+
if (fallbackElement) {
|
|
729
|
+
(fallbackElement as HTMLElement).style.display = 'flex';
|
|
730
|
+
}
|
|
731
|
+
}}
|
|
732
|
+
/>
|
|
733
|
+
) : (
|
|
734
|
+
<Image
|
|
735
|
+
source={{ uri: oxyServices.getFileDownloadUrl(file.id) }}
|
|
736
|
+
style={styles.previewImage}
|
|
737
|
+
resizeMode="cover"
|
|
738
|
+
onError={() => {
|
|
739
|
+
// For React Native, you might want to set an error state
|
|
740
|
+
console.warn('Failed to load image preview for file:', file.id);
|
|
741
|
+
}}
|
|
742
|
+
/>
|
|
743
|
+
)
|
|
744
|
+
)}
|
|
745
|
+
{isPDF && (
|
|
746
|
+
<View style={styles.pdfPreview}>
|
|
747
|
+
<Ionicons name="document" size={32} color={primaryColor} />
|
|
748
|
+
<Text style={[styles.pdfLabel, { color: primaryColor }]}>PDF</Text>
|
|
749
|
+
</View>
|
|
750
|
+
)}
|
|
751
|
+
{isVideo && (
|
|
752
|
+
<View style={styles.videoPreview}>
|
|
753
|
+
<Ionicons name="play-circle" size={32} color={primaryColor} />
|
|
754
|
+
<Text style={[styles.videoLabel, { color: primaryColor }]}>VIDEO</Text>
|
|
755
|
+
</View>
|
|
756
|
+
)}
|
|
757
|
+
{/* Fallback icon (hidden by default for images) */}
|
|
758
|
+
<View
|
|
759
|
+
style={[styles.fallbackIcon, { display: isImage ? 'none' : 'flex' }]}
|
|
760
|
+
{...(Platform.OS === 'web' && { 'data-fallback': 'true' })}
|
|
761
|
+
>
|
|
762
|
+
<Ionicons
|
|
763
|
+
name={getFileIcon(file.contentType) as any}
|
|
764
|
+
size={32}
|
|
765
|
+
color={primaryColor}
|
|
766
|
+
/>
|
|
767
|
+
</View>
|
|
768
|
+
|
|
769
|
+
{/* Preview overlay for hover effect */}
|
|
770
|
+
{Platform.OS === 'web' && hoveredPreview === file.id && isImage && (
|
|
771
|
+
<View style={styles.previewOverlay}>
|
|
772
|
+
<Ionicons name="eye" size={24} color="#FFFFFF" />
|
|
773
|
+
</View>
|
|
774
|
+
)}
|
|
775
|
+
</View>
|
|
776
|
+
) : (
|
|
777
|
+
<View style={styles.fileIconContainer}>
|
|
778
|
+
<Ionicons
|
|
779
|
+
name={getFileIcon(file.contentType) as any}
|
|
780
|
+
size={32}
|
|
781
|
+
color={primaryColor}
|
|
782
|
+
/>
|
|
783
|
+
</View>
|
|
784
|
+
)}
|
|
785
|
+
</View>
|
|
786
|
+
|
|
787
|
+
<View style={styles.fileInfo}>
|
|
788
|
+
<Text style={[styles.fileName, { color: textColor }]} numberOfLines={1}>
|
|
789
|
+
{file.filename}
|
|
790
|
+
</Text>
|
|
791
|
+
<Text style={[styles.fileDetails, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
|
|
792
|
+
{formatFileSize(file.length)} • {new Date(file.uploadDate).toLocaleDateString()}
|
|
793
|
+
</Text>
|
|
794
|
+
{file.metadata?.description && (
|
|
795
|
+
<Text
|
|
796
|
+
style={[styles.fileDescription, { color: isDarkTheme ? '#AAAAAA' : '#888888' }]}
|
|
797
|
+
numberOfLines={2}
|
|
798
|
+
>
|
|
799
|
+
{file.metadata.description}
|
|
800
|
+
</Text>
|
|
801
|
+
)}
|
|
802
|
+
</View>
|
|
803
|
+
</TouchableOpacity>
|
|
804
|
+
|
|
805
|
+
<View style={styles.fileActions}>
|
|
806
|
+
{/* Preview button for supported files */}
|
|
807
|
+
{hasPreview && (
|
|
808
|
+
<TouchableOpacity
|
|
809
|
+
style={[styles.actionButton, { backgroundColor: isDarkTheme ? '#333333' : '#F0F0F0' }]}
|
|
810
|
+
onPress={() => handleFileOpen(file)}
|
|
811
|
+
>
|
|
812
|
+
<Ionicons name="eye" size={20} color={primaryColor} />
|
|
813
|
+
</TouchableOpacity>
|
|
814
|
+
)}
|
|
815
|
+
|
|
816
|
+
<TouchableOpacity
|
|
817
|
+
style={[styles.actionButton, { backgroundColor: isDarkTheme ? '#333333' : '#F0F0F0' }]}
|
|
818
|
+
onPress={() => handleFileDownload(file.id, file.filename)}
|
|
819
|
+
>
|
|
820
|
+
<Ionicons name="download" size={20} color={primaryColor} />
|
|
821
|
+
</TouchableOpacity>
|
|
822
|
+
|
|
823
|
+
{/* Always show delete button for debugging */}
|
|
824
|
+
<TouchableOpacity
|
|
825
|
+
style={[styles.actionButton, { backgroundColor: isDarkTheme ? '#400000' : '#FFEBEE' }]}
|
|
826
|
+
onPress={() => {
|
|
827
|
+
handleFileDelete(file.id, file.filename);
|
|
828
|
+
}}
|
|
829
|
+
disabled={deleting === file.id}
|
|
830
|
+
>
|
|
831
|
+
{deleting === file.id ? (
|
|
832
|
+
<ActivityIndicator size="small" color={dangerColor} />
|
|
833
|
+
) : (
|
|
834
|
+
<Ionicons name="trash" size={20} color={dangerColor} />
|
|
835
|
+
)}
|
|
836
|
+
</TouchableOpacity>
|
|
837
|
+
</View>
|
|
838
|
+
</View>
|
|
839
|
+
);
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
const renderPhotoGrid = useCallback(() => {
|
|
843
|
+
const photos = filteredFiles.filter(file => file.contentType.startsWith('image/'));
|
|
844
|
+
|
|
845
|
+
if (photos.length === 0) {
|
|
846
|
+
return (
|
|
847
|
+
<View style={styles.emptyState}>
|
|
848
|
+
<Ionicons name="images-outline" size={64} color={isDarkTheme ? '#666666' : '#CCCCCC'} />
|
|
849
|
+
<Text style={[styles.emptyStateTitle, { color: textColor }]}>No Photos Yet</Text>
|
|
850
|
+
<Text style={[styles.emptyStateDescription, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
|
|
851
|
+
{user?.id === targetUserId
|
|
852
|
+
? `Upload photos to get started. You can select multiple photos at once${Platform.OS === 'web' ? ' or drag & drop them here.' : '.'}`
|
|
853
|
+
: "This user hasn't uploaded any photos yet"
|
|
854
|
+
}
|
|
855
|
+
</Text>
|
|
856
|
+
{user?.id === targetUserId && (
|
|
857
|
+
<TouchableOpacity
|
|
858
|
+
style={[styles.emptyStateButton, { backgroundColor: primaryColor }]}
|
|
859
|
+
onPress={handleFileUpload}
|
|
860
|
+
disabled={uploading}
|
|
861
|
+
>
|
|
862
|
+
{uploading ? (
|
|
863
|
+
<ActivityIndicator size="small" color="#FFFFFF" />
|
|
864
|
+
) : (
|
|
865
|
+
<>
|
|
866
|
+
<Ionicons name="cloud-upload" size={20} color="#FFFFFF" />
|
|
867
|
+
<Text style={styles.emptyStateButtonText}>Upload Photos</Text>
|
|
868
|
+
</>
|
|
869
|
+
)}
|
|
870
|
+
</TouchableOpacity>
|
|
871
|
+
)}
|
|
872
|
+
</View>
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
return (
|
|
877
|
+
<ScrollView
|
|
878
|
+
style={styles.scrollView}
|
|
879
|
+
contentContainerStyle={styles.photoScrollContainer}
|
|
880
|
+
refreshControl={
|
|
881
|
+
<RefreshControl
|
|
882
|
+
refreshing={refreshing}
|
|
883
|
+
onRefresh={() => loadFiles(true)}
|
|
884
|
+
tintColor={primaryColor}
|
|
885
|
+
/>
|
|
886
|
+
}
|
|
887
|
+
showsVerticalScrollIndicator={false}
|
|
888
|
+
>
|
|
889
|
+
{loadingDimensions && (
|
|
890
|
+
<View style={styles.dimensionsLoadingIndicator}>
|
|
891
|
+
<ActivityIndicator size="small" color={primaryColor} />
|
|
892
|
+
<Text style={[styles.dimensionsLoadingText, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
|
|
893
|
+
Loading photo layout...
|
|
894
|
+
</Text>
|
|
895
|
+
</View>
|
|
896
|
+
)}
|
|
897
|
+
|
|
898
|
+
<JustifiedPhotoGrid
|
|
899
|
+
photos={photos}
|
|
900
|
+
photoDimensions={photoDimensions}
|
|
901
|
+
loadPhotoDimensions={loadPhotoDimensions}
|
|
902
|
+
createJustifiedRows={createJustifiedRows}
|
|
903
|
+
renderJustifiedPhotoItem={renderJustifiedPhotoItem}
|
|
904
|
+
renderSimplePhotoItem={renderPhotoItem}
|
|
905
|
+
textColor={textColor}
|
|
906
|
+
containerWidth={containerWidth}
|
|
907
|
+
/>
|
|
908
|
+
</ScrollView>
|
|
909
|
+
);
|
|
910
|
+
}, [filteredFiles, isDarkTheme, textColor, user?.id, targetUserId, uploading, primaryColor, handleFileUpload, refreshing, loadFiles, loadingDimensions, photoDimensions, loadPhotoDimensions, createJustifiedRows, renderJustifiedPhotoItem]);
|
|
911
|
+
|
|
912
|
+
// Separate component for the photo grid to optimize rendering
|
|
913
|
+
const JustifiedPhotoGrid = React.memo(({
|
|
914
|
+
photos,
|
|
915
|
+
photoDimensions,
|
|
916
|
+
loadPhotoDimensions,
|
|
917
|
+
createJustifiedRows,
|
|
918
|
+
renderJustifiedPhotoItem,
|
|
919
|
+
renderSimplePhotoItem,
|
|
920
|
+
textColor,
|
|
921
|
+
containerWidth
|
|
922
|
+
}: {
|
|
923
|
+
photos: FileMetadata[];
|
|
924
|
+
photoDimensions: {[key: string]: {width: number, height: number}};
|
|
925
|
+
loadPhotoDimensions: (photos: FileMetadata[]) => Promise<void>;
|
|
926
|
+
createJustifiedRows: (photos: FileMetadata[], containerWidth: number) => FileMetadata[][];
|
|
927
|
+
renderJustifiedPhotoItem: (photo: FileMetadata, width: number, height: number, isLast: boolean) => JSX.Element;
|
|
928
|
+
renderSimplePhotoItem: (photo: FileMetadata, index: number) => JSX.Element;
|
|
929
|
+
textColor: string;
|
|
930
|
+
containerWidth: number;
|
|
931
|
+
}) => {
|
|
932
|
+
// Load dimensions for new photos
|
|
933
|
+
React.useEffect(() => {
|
|
934
|
+
loadPhotoDimensions(photos);
|
|
935
|
+
}, [photos.map(p => p.id).join(','), loadPhotoDimensions]);
|
|
936
|
+
|
|
937
|
+
// Group photos by date
|
|
938
|
+
const photosByDate = React.useMemo(() => {
|
|
939
|
+
return photos.reduce((groups: {[key: string]: FileMetadata[]}, photo) => {
|
|
940
|
+
const date = new Date(photo.uploadDate).toDateString();
|
|
941
|
+
if (!groups[date]) {
|
|
942
|
+
groups[date] = [];
|
|
943
|
+
}
|
|
944
|
+
groups[date].push(photo);
|
|
945
|
+
return groups;
|
|
946
|
+
}, {});
|
|
947
|
+
}, [photos]);
|
|
948
|
+
|
|
949
|
+
const sortedDates = React.useMemo(() => {
|
|
950
|
+
return Object.keys(photosByDate).sort((a, b) =>
|
|
951
|
+
new Date(b).getTime() - new Date(a).getTime()
|
|
952
|
+
);
|
|
953
|
+
}, [photosByDate]);
|
|
954
|
+
|
|
955
|
+
return (
|
|
956
|
+
<>
|
|
957
|
+
{sortedDates.map(date => {
|
|
958
|
+
const dayPhotos = photosByDate[date];
|
|
959
|
+
const justifiedRows = createJustifiedRows(dayPhotos, containerWidth);
|
|
960
|
+
|
|
961
|
+
return (
|
|
962
|
+
<View key={date} style={styles.photoDateSection}>
|
|
963
|
+
<Text style={[styles.photoDateHeader, { color: textColor }]}>
|
|
964
|
+
{new Date(date).toLocaleDateString('en-US', {
|
|
965
|
+
weekday: 'long',
|
|
966
|
+
year: 'numeric',
|
|
967
|
+
month: 'long',
|
|
968
|
+
day: 'numeric'
|
|
969
|
+
})}
|
|
970
|
+
</Text>
|
|
971
|
+
<View style={styles.justifiedPhotoGrid}>
|
|
972
|
+
{justifiedRows.map((row, rowIndex) => {
|
|
973
|
+
// Calculate row height based on available width
|
|
974
|
+
const gap = 4;
|
|
975
|
+
let totalAspectRatio = 0;
|
|
976
|
+
|
|
977
|
+
// Calculate total aspect ratio for this row
|
|
978
|
+
row.forEach(photo => {
|
|
979
|
+
const dimensions = photoDimensions[photo.id];
|
|
980
|
+
const aspectRatio = dimensions ?
|
|
981
|
+
(dimensions.width / dimensions.height) :
|
|
982
|
+
1.33; // Default 4:3 ratio
|
|
983
|
+
totalAspectRatio += aspectRatio;
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
// Calculate the height that makes the row fill the available width
|
|
987
|
+
// Account for photoScrollContainer padding (32px total) and gaps between photos
|
|
988
|
+
const scrollContainerPadding = 32;
|
|
989
|
+
const availableWidth = containerWidth - scrollContainerPadding - (gap * (row.length - 1));
|
|
990
|
+
const calculatedHeight = availableWidth / totalAspectRatio;
|
|
991
|
+
|
|
992
|
+
// Clamp height for visual consistency
|
|
993
|
+
const rowHeight = Math.max(120, Math.min(calculatedHeight, 300));
|
|
994
|
+
|
|
995
|
+
return (
|
|
996
|
+
<View
|
|
997
|
+
key={`row-${rowIndex}`}
|
|
998
|
+
style={[
|
|
999
|
+
styles.justifiedPhotoRow,
|
|
1000
|
+
{
|
|
1001
|
+
height: rowHeight,
|
|
1002
|
+
maxWidth: containerWidth - 32, // Account for scroll container padding
|
|
1003
|
+
gap: 4, // Add horizontal gap between photos in row
|
|
1004
|
+
}
|
|
1005
|
+
]}
|
|
1006
|
+
>
|
|
1007
|
+
{row.map((photo, photoIndex) => {
|
|
1008
|
+
const dimensions = photoDimensions[photo.id];
|
|
1009
|
+
const aspectRatio = dimensions ?
|
|
1010
|
+
(dimensions.width / dimensions.height) :
|
|
1011
|
+
1.33; // Default 4:3 ratio
|
|
1012
|
+
|
|
1013
|
+
const photoWidth = rowHeight * aspectRatio;
|
|
1014
|
+
const isLast = photoIndex === row.length - 1;
|
|
1015
|
+
|
|
1016
|
+
return renderJustifiedPhotoItem(
|
|
1017
|
+
photo,
|
|
1018
|
+
photoWidth,
|
|
1019
|
+
rowHeight,
|
|
1020
|
+
isLast
|
|
1021
|
+
);
|
|
1022
|
+
})}
|
|
1023
|
+
</View>
|
|
1024
|
+
);
|
|
1025
|
+
})}
|
|
1026
|
+
</View>
|
|
1027
|
+
</View>
|
|
1028
|
+
);
|
|
1029
|
+
})}
|
|
1030
|
+
</>
|
|
1031
|
+
);
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
const renderPhotoItem = (photo: FileMetadata, index: number) => {
|
|
1035
|
+
const downloadUrl = oxyServices.getFileDownloadUrl(photo.id);
|
|
1036
|
+
|
|
1037
|
+
// Calculate photo item width based on actual container size from bottom sheet
|
|
1038
|
+
let itemsPerRow = 3; // Default for mobile
|
|
1039
|
+
if (containerWidth > 768) itemsPerRow = 6; // Tablet/Desktop
|
|
1040
|
+
else if (containerWidth > 480) itemsPerRow = 4; // Large mobile
|
|
1041
|
+
|
|
1042
|
+
// Account for the photoScrollContainer padding (16px on each side = 32px total)
|
|
1043
|
+
const scrollContainerPadding = 32; // Total horizontal padding from photoScrollContainer
|
|
1044
|
+
const gaps = (itemsPerRow - 1) * 4; // Gap between items
|
|
1045
|
+
const availableWidth = containerWidth - scrollContainerPadding;
|
|
1046
|
+
const itemWidth = (availableWidth - gaps) / itemsPerRow;
|
|
1047
|
+
|
|
1048
|
+
return (
|
|
1049
|
+
<TouchableOpacity
|
|
1050
|
+
key={photo.id}
|
|
1051
|
+
style={[
|
|
1052
|
+
styles.photoItem,
|
|
1053
|
+
{
|
|
1054
|
+
width: itemWidth,
|
|
1055
|
+
height: itemWidth,
|
|
1056
|
+
}
|
|
1057
|
+
]}
|
|
1058
|
+
onPress={() => handleFileOpen(photo)}
|
|
1059
|
+
activeOpacity={0.8}
|
|
1060
|
+
>
|
|
1061
|
+
<View style={styles.photoContainer}>
|
|
1062
|
+
{Platform.OS === 'web' ? (
|
|
1063
|
+
<img
|
|
1064
|
+
src={downloadUrl}
|
|
1065
|
+
alt={photo.filename}
|
|
1066
|
+
style={{
|
|
1067
|
+
width: '100%',
|
|
1068
|
+
height: '100%',
|
|
1069
|
+
objectFit: 'cover',
|
|
1070
|
+
borderRadius: 8,
|
|
1071
|
+
transition: 'transform 0.2s ease',
|
|
1072
|
+
}}
|
|
1073
|
+
loading="lazy"
|
|
1074
|
+
onError={(e) => {
|
|
1075
|
+
console.error('Photo failed to load:', e);
|
|
1076
|
+
// Could replace with placeholder image
|
|
1077
|
+
}}
|
|
1078
|
+
onMouseEnter={(e) => {
|
|
1079
|
+
e.currentTarget.style.transform = 'scale(1.02)';
|
|
1080
|
+
}}
|
|
1081
|
+
onMouseLeave={(e) => {
|
|
1082
|
+
e.currentTarget.style.transform = 'scale(1)';
|
|
1083
|
+
}}
|
|
1084
|
+
/>
|
|
1085
|
+
) : (
|
|
1086
|
+
<Image
|
|
1087
|
+
source={{ uri: downloadUrl }}
|
|
1088
|
+
style={styles.photoImage}
|
|
1089
|
+
resizeMode="cover"
|
|
1090
|
+
onError={(e) => {
|
|
1091
|
+
console.error('Photo failed to load:', e);
|
|
1092
|
+
}}
|
|
1093
|
+
/>
|
|
1094
|
+
)}
|
|
1095
|
+
</View>
|
|
1096
|
+
</TouchableOpacity>
|
|
1097
|
+
);
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
const renderFileDetailsModal = () => (
|
|
1101
|
+
<Modal
|
|
1102
|
+
visible={showFileDetails}
|
|
1103
|
+
animationType="slide"
|
|
1104
|
+
presentationStyle="pageSheet"
|
|
1105
|
+
onRequestClose={() => setShowFileDetails(false)}
|
|
1106
|
+
>
|
|
1107
|
+
<View style={[styles.modalContainer, { backgroundColor }]}>
|
|
1108
|
+
<View style={[styles.modalHeader, { borderBottomColor: borderColor }]}>
|
|
1109
|
+
<TouchableOpacity
|
|
1110
|
+
style={styles.modalCloseButton}
|
|
1111
|
+
onPress={() => setShowFileDetails(false)}
|
|
1112
|
+
>
|
|
1113
|
+
<Ionicons name="close" size={24} color={textColor} />
|
|
1114
|
+
</TouchableOpacity>
|
|
1115
|
+
<Text style={[styles.modalTitle, { color: textColor }]}>File Details</Text>
|
|
1116
|
+
<View style={styles.modalPlaceholder} />
|
|
1117
|
+
</View>
|
|
1118
|
+
|
|
1119
|
+
{selectedFile && (
|
|
1120
|
+
<ScrollView style={styles.modalContent}>
|
|
1121
|
+
<View style={[styles.fileDetailCard, { backgroundColor: secondaryBackgroundColor, borderColor }]}>
|
|
1122
|
+
<View style={styles.fileDetailIcon}>
|
|
1123
|
+
<Ionicons
|
|
1124
|
+
name={getFileIcon(selectedFile.contentType) as any}
|
|
1125
|
+
size={64}
|
|
1126
|
+
color={primaryColor}
|
|
1127
|
+
/>
|
|
1128
|
+
</View>
|
|
1129
|
+
|
|
1130
|
+
<Text style={[styles.fileDetailName, { color: textColor }]}>
|
|
1131
|
+
{selectedFile.filename}
|
|
1132
|
+
</Text>
|
|
1133
|
+
|
|
1134
|
+
<View style={styles.fileDetailInfo}>
|
|
1135
|
+
<View style={styles.detailRow}>
|
|
1136
|
+
<Text style={[styles.detailLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
|
|
1137
|
+
Size:
|
|
1138
|
+
</Text>
|
|
1139
|
+
<Text style={[styles.detailValue, { color: textColor }]}>
|
|
1140
|
+
{formatFileSize(selectedFile.length)}
|
|
1141
|
+
</Text>
|
|
1142
|
+
</View>
|
|
1143
|
+
|
|
1144
|
+
<View style={styles.detailRow}>
|
|
1145
|
+
<Text style={[styles.detailLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
|
|
1146
|
+
Type:
|
|
1147
|
+
</Text>
|
|
1148
|
+
<Text style={[styles.detailValue, { color: textColor }]}>
|
|
1149
|
+
{selectedFile.contentType}
|
|
1150
|
+
</Text>
|
|
1151
|
+
</View>
|
|
1152
|
+
|
|
1153
|
+
<View style={styles.detailRow}>
|
|
1154
|
+
<Text style={[styles.detailLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
|
|
1155
|
+
Uploaded:
|
|
1156
|
+
</Text>
|
|
1157
|
+
<Text style={[styles.detailValue, { color: textColor }]}>
|
|
1158
|
+
{new Date(selectedFile.uploadDate).toLocaleString()}
|
|
1159
|
+
</Text>
|
|
1160
|
+
</View>
|
|
1161
|
+
|
|
1162
|
+
{selectedFile.metadata?.description && (
|
|
1163
|
+
<View style={styles.detailRow}>
|
|
1164
|
+
<Text style={[styles.detailLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
|
|
1165
|
+
Description:
|
|
1166
|
+
</Text>
|
|
1167
|
+
<Text style={[styles.detailValue, { color: textColor }]}>
|
|
1168
|
+
{selectedFile.metadata.description}
|
|
1169
|
+
</Text>
|
|
1170
|
+
</View>
|
|
1171
|
+
)}
|
|
1172
|
+
</View>
|
|
1173
|
+
|
|
1174
|
+
<View style={styles.modalActions}>
|
|
1175
|
+
<TouchableOpacity
|
|
1176
|
+
style={[styles.modalActionButton, { backgroundColor: primaryColor }]}
|
|
1177
|
+
onPress={() => {
|
|
1178
|
+
handleFileDownload(selectedFile.id, selectedFile.filename);
|
|
1179
|
+
setShowFileDetails(false);
|
|
1180
|
+
}}
|
|
1181
|
+
>
|
|
1182
|
+
<Ionicons name="download" size={20} color="#FFFFFF" />
|
|
1183
|
+
<Text style={styles.modalActionText}>Download</Text>
|
|
1184
|
+
</TouchableOpacity>
|
|
1185
|
+
|
|
1186
|
+
{(user?.id === targetUserId) && (
|
|
1187
|
+
<TouchableOpacity
|
|
1188
|
+
style={[styles.modalActionButton, { backgroundColor: dangerColor }]}
|
|
1189
|
+
onPress={() => {
|
|
1190
|
+
setShowFileDetails(false);
|
|
1191
|
+
handleFileDelete(selectedFile.id, selectedFile.filename);
|
|
1192
|
+
}}
|
|
1193
|
+
>
|
|
1194
|
+
<Ionicons name="trash" size={20} color="#FFFFFF" />
|
|
1195
|
+
<Text style={styles.modalActionText}>Delete</Text>
|
|
1196
|
+
</TouchableOpacity>
|
|
1197
|
+
)}
|
|
1198
|
+
</View>
|
|
1199
|
+
</View>
|
|
1200
|
+
</ScrollView>
|
|
1201
|
+
)}
|
|
1202
|
+
</View>
|
|
1203
|
+
</Modal>
|
|
1204
|
+
);
|
|
1205
|
+
|
|
1206
|
+
const renderFileViewer = () => {
|
|
1207
|
+
if (!openedFile) return null;
|
|
1208
|
+
|
|
1209
|
+
const isImage = openedFile.contentType.startsWith('image/');
|
|
1210
|
+
const isText = openedFile.contentType.startsWith('text/') ||
|
|
1211
|
+
openedFile.contentType.includes('json') ||
|
|
1212
|
+
openedFile.contentType.includes('xml') ||
|
|
1213
|
+
openedFile.contentType.includes('javascript') ||
|
|
1214
|
+
openedFile.contentType.includes('typescript');
|
|
1215
|
+
const isPDF = openedFile.contentType.includes('pdf');
|
|
1216
|
+
const isVideo = openedFile.contentType.startsWith('video/');
|
|
1217
|
+
const isAudio = openedFile.contentType.startsWith('audio/');
|
|
1218
|
+
|
|
1219
|
+
return (
|
|
1220
|
+
<View style={[styles.fileViewerContainer, { backgroundColor }]}>
|
|
1221
|
+
{/* File Viewer Header */}
|
|
1222
|
+
<View style={[styles.fileViewerHeader, { borderBottomColor: borderColor }]}>
|
|
1223
|
+
<TouchableOpacity
|
|
1224
|
+
style={styles.backButton}
|
|
1225
|
+
onPress={handleCloseFile}
|
|
1226
|
+
>
|
|
1227
|
+
<Ionicons name="arrow-back" size={24} color={textColor} />
|
|
1228
|
+
</TouchableOpacity>
|
|
1229
|
+
<View style={styles.fileViewerTitleContainer}>
|
|
1230
|
+
<Text style={[styles.fileViewerTitle, { color: textColor }]} numberOfLines={1}>
|
|
1231
|
+
{openedFile.filename}
|
|
1232
|
+
</Text>
|
|
1233
|
+
<Text style={[styles.fileViewerSubtitle, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
|
|
1234
|
+
{formatFileSize(openedFile.length)} • {openedFile.contentType}
|
|
1235
|
+
</Text>
|
|
1236
|
+
</View>
|
|
1237
|
+
<View style={styles.fileViewerActions}>
|
|
1238
|
+
<TouchableOpacity
|
|
1239
|
+
style={[styles.actionButton, { backgroundColor: isDarkTheme ? '#333333' : '#F0F0F0' }]}
|
|
1240
|
+
onPress={() => handleFileDownload(openedFile.id, openedFile.filename)}
|
|
1241
|
+
>
|
|
1242
|
+
<Ionicons name="download" size={20} color={primaryColor} />
|
|
1243
|
+
</TouchableOpacity>
|
|
1244
|
+
<TouchableOpacity
|
|
1245
|
+
style={[
|
|
1246
|
+
styles.actionButton,
|
|
1247
|
+
{
|
|
1248
|
+
backgroundColor: showFileDetailsInViewer
|
|
1249
|
+
? primaryColor
|
|
1250
|
+
: (isDarkTheme ? '#333333' : '#F0F0F0')
|
|
1251
|
+
}
|
|
1252
|
+
]}
|
|
1253
|
+
onPress={() => setShowFileDetailsInViewer(!showFileDetailsInViewer)}
|
|
1254
|
+
>
|
|
1255
|
+
<Ionicons
|
|
1256
|
+
name={showFileDetailsInViewer ? "chevron-up" : "information-circle"}
|
|
1257
|
+
size={20}
|
|
1258
|
+
color={showFileDetailsInViewer ? "#FFFFFF" : primaryColor}
|
|
1259
|
+
/>
|
|
1260
|
+
</TouchableOpacity>
|
|
1261
|
+
</View>
|
|
1262
|
+
</View>
|
|
1263
|
+
|
|
1264
|
+
{/* File Details Section */}
|
|
1265
|
+
{showFileDetailsInViewer && (
|
|
1266
|
+
<View style={[styles.fileDetailsSection, { backgroundColor: secondaryBackgroundColor, borderColor }]}>
|
|
1267
|
+
<View style={styles.fileDetailsSectionHeader}>
|
|
1268
|
+
<Text style={[styles.fileDetailsSectionTitle, { color: textColor }]}>
|
|
1269
|
+
File Details
|
|
1270
|
+
</Text>
|
|
1271
|
+
<TouchableOpacity
|
|
1272
|
+
style={styles.fileDetailsSectionToggle}
|
|
1273
|
+
onPress={() => setShowFileDetailsInViewer(false)}
|
|
1274
|
+
>
|
|
1275
|
+
<Ionicons name="chevron-up" size={20} color={isDarkTheme ? '#BBBBBB' : '#666666'} />
|
|
1276
|
+
</TouchableOpacity>
|
|
1277
|
+
</View>
|
|
1278
|
+
|
|
1279
|
+
<View style={styles.fileDetailInfo}>
|
|
1280
|
+
<View style={styles.detailRow}>
|
|
1281
|
+
<Text style={[styles.detailLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
|
|
1282
|
+
File Name:
|
|
1283
|
+
</Text>
|
|
1284
|
+
<Text style={[styles.detailValue, { color: textColor }]}>
|
|
1285
|
+
{openedFile.filename}
|
|
1286
|
+
</Text>
|
|
1287
|
+
</View>
|
|
1288
|
+
|
|
1289
|
+
<View style={styles.detailRow}>
|
|
1290
|
+
<Text style={[styles.detailLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
|
|
1291
|
+
Size:
|
|
1292
|
+
</Text>
|
|
1293
|
+
<Text style={[styles.detailValue, { color: textColor }]}>
|
|
1294
|
+
{formatFileSize(openedFile.length)}
|
|
1295
|
+
</Text>
|
|
1296
|
+
</View>
|
|
1297
|
+
|
|
1298
|
+
<View style={styles.detailRow}>
|
|
1299
|
+
<Text style={[styles.detailLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
|
|
1300
|
+
Type:
|
|
1301
|
+
</Text>
|
|
1302
|
+
<Text style={[styles.detailValue, { color: textColor }]}>
|
|
1303
|
+
{openedFile.contentType}
|
|
1304
|
+
</Text>
|
|
1305
|
+
</View>
|
|
1306
|
+
|
|
1307
|
+
<View style={styles.detailRow}>
|
|
1308
|
+
<Text style={[styles.detailLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
|
|
1309
|
+
Uploaded:
|
|
1310
|
+
</Text>
|
|
1311
|
+
<Text style={[styles.detailValue, { color: textColor }]}>
|
|
1312
|
+
{new Date(openedFile.uploadDate).toLocaleString()}
|
|
1313
|
+
</Text>
|
|
1314
|
+
</View>
|
|
1315
|
+
|
|
1316
|
+
{openedFile.metadata?.description && (
|
|
1317
|
+
<View style={styles.detailRow}>
|
|
1318
|
+
<Text style={[styles.detailLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
|
|
1319
|
+
Description:
|
|
1320
|
+
</Text>
|
|
1321
|
+
<Text style={[styles.detailValue, { color: textColor }]}>
|
|
1322
|
+
{openedFile.metadata.description}
|
|
1323
|
+
</Text>
|
|
1324
|
+
</View>
|
|
1325
|
+
)}
|
|
1326
|
+
|
|
1327
|
+
<View style={styles.detailRow}>
|
|
1328
|
+
<Text style={[styles.detailLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
|
|
1329
|
+
File ID:
|
|
1330
|
+
</Text>
|
|
1331
|
+
<Text style={[styles.detailValue, { color: textColor, fontSize: 12, fontFamily: Platform.OS === 'web' ? 'monospace' : 'Courier' }]}>
|
|
1332
|
+
{openedFile.id}
|
|
1333
|
+
</Text>
|
|
1334
|
+
</View>
|
|
1335
|
+
</View>
|
|
1336
|
+
|
|
1337
|
+
<View style={styles.fileDetailsActions}>
|
|
1338
|
+
<TouchableOpacity
|
|
1339
|
+
style={[styles.fileDetailsActionButton, { backgroundColor: primaryColor }]}
|
|
1340
|
+
onPress={() => handleFileDownload(openedFile.id, openedFile.filename)}
|
|
1341
|
+
>
|
|
1342
|
+
<Ionicons name="download" size={16} color="#FFFFFF" />
|
|
1343
|
+
<Text style={styles.fileDetailsActionText}>Download</Text>
|
|
1344
|
+
</TouchableOpacity>
|
|
1345
|
+
|
|
1346
|
+
{(user?.id === targetUserId) && (
|
|
1347
|
+
<TouchableOpacity
|
|
1348
|
+
style={[styles.fileDetailsActionButton, { backgroundColor: dangerColor }]}
|
|
1349
|
+
onPress={() => {
|
|
1350
|
+
handleCloseFile();
|
|
1351
|
+
handleFileDelete(openedFile.id, openedFile.filename);
|
|
1352
|
+
}}
|
|
1353
|
+
>
|
|
1354
|
+
<Ionicons name="trash" size={16} color="#FFFFFF" />
|
|
1355
|
+
<Text style={styles.fileDetailsActionText}>Delete</Text>
|
|
1356
|
+
</TouchableOpacity>
|
|
1357
|
+
)}
|
|
1358
|
+
</View>
|
|
1359
|
+
</View>
|
|
1360
|
+
)}
|
|
1361
|
+
|
|
1362
|
+
{/* File Content */}
|
|
1363
|
+
<ScrollView
|
|
1364
|
+
style={[
|
|
1365
|
+
styles.fileViewerContent,
|
|
1366
|
+
showFileDetailsInViewer && styles.fileViewerContentWithDetails
|
|
1367
|
+
]}
|
|
1368
|
+
contentContainerStyle={styles.fileViewerContentContainer}
|
|
1369
|
+
>
|
|
1370
|
+
{loadingFileContent ? (
|
|
1371
|
+
<View style={styles.fileViewerLoading}>
|
|
1372
|
+
<ActivityIndicator size="large" color={primaryColor} />
|
|
1373
|
+
<Text style={[styles.fileViewerLoadingText, { color: textColor }]}>
|
|
1374
|
+
Loading file content...
|
|
1375
|
+
</Text>
|
|
1376
|
+
</View>
|
|
1377
|
+
) : isImage && fileContent ? (
|
|
1378
|
+
<View style={styles.imageContainer}>
|
|
1379
|
+
{Platform.OS === 'web' ? (
|
|
1380
|
+
<img
|
|
1381
|
+
src={fileContent}
|
|
1382
|
+
alt={openedFile.filename}
|
|
1383
|
+
style={{
|
|
1384
|
+
maxWidth: '100%',
|
|
1385
|
+
maxHeight: '80vh',
|
|
1386
|
+
objectFit: 'contain',
|
|
1387
|
+
borderRadius: 8,
|
|
1388
|
+
}}
|
|
1389
|
+
onError={(e) => {
|
|
1390
|
+
console.error('Image failed to load:', e);
|
|
1391
|
+
}}
|
|
1392
|
+
/>
|
|
1393
|
+
) : (
|
|
1394
|
+
<Image
|
|
1395
|
+
source={{ uri: fileContent }}
|
|
1396
|
+
style={{
|
|
1397
|
+
width: '100%',
|
|
1398
|
+
height: 400,
|
|
1399
|
+
resizeMode: 'contain',
|
|
1400
|
+
borderRadius: 8,
|
|
1401
|
+
}}
|
|
1402
|
+
onError={(e) => {
|
|
1403
|
+
console.error('Image failed to load:', e);
|
|
1404
|
+
}}
|
|
1405
|
+
/>
|
|
1406
|
+
)}
|
|
1407
|
+
</View>
|
|
1408
|
+
) : isText && fileContent ? (
|
|
1409
|
+
<View style={[styles.textContainer, { backgroundColor: secondaryBackgroundColor, borderColor }]}>
|
|
1410
|
+
<ScrollView style={{ flex: 1 }} nestedScrollEnabled>
|
|
1411
|
+
<Text style={[styles.textContent, { color: textColor }]}>
|
|
1412
|
+
{fileContent}
|
|
1413
|
+
</Text>
|
|
1414
|
+
</ScrollView>
|
|
1415
|
+
</View>
|
|
1416
|
+
) : isPDF && fileContent && Platform.OS === 'web' ? (
|
|
1417
|
+
<View style={styles.pdfContainer}>
|
|
1418
|
+
<iframe
|
|
1419
|
+
src={fileContent}
|
|
1420
|
+
width="100%"
|
|
1421
|
+
height="600px"
|
|
1422
|
+
style={{ border: 'none', borderRadius: 8 }}
|
|
1423
|
+
title={openedFile.filename}
|
|
1424
|
+
/>
|
|
1425
|
+
</View>
|
|
1426
|
+
) : isVideo && fileContent ? (
|
|
1427
|
+
<View style={styles.mediaContainer}>
|
|
1428
|
+
{Platform.OS === 'web' ? (
|
|
1429
|
+
<video
|
|
1430
|
+
controls
|
|
1431
|
+
style={{
|
|
1432
|
+
width: '100%',
|
|
1433
|
+
maxHeight: '70vh',
|
|
1434
|
+
borderRadius: 8,
|
|
1435
|
+
}}
|
|
1436
|
+
>
|
|
1437
|
+
<source src={fileContent} type={openedFile.contentType} />
|
|
1438
|
+
Your browser does not support the video tag.
|
|
1439
|
+
</video>
|
|
1440
|
+
) : (
|
|
1441
|
+
<Text style={[styles.unsupportedText, { color: textColor }]}>
|
|
1442
|
+
Video playback not supported on mobile
|
|
1443
|
+
</Text>
|
|
1444
|
+
)}
|
|
1445
|
+
</View>
|
|
1446
|
+
) : isAudio && fileContent ? (
|
|
1447
|
+
<View style={styles.mediaContainer}>
|
|
1448
|
+
{Platform.OS === 'web' ? (
|
|
1449
|
+
<audio
|
|
1450
|
+
controls
|
|
1451
|
+
style={{
|
|
1452
|
+
width: '100%',
|
|
1453
|
+
borderRadius: 8,
|
|
1454
|
+
}}
|
|
1455
|
+
>
|
|
1456
|
+
<source src={fileContent} type={openedFile.contentType} />
|
|
1457
|
+
Your browser does not support the audio tag.
|
|
1458
|
+
</audio>
|
|
1459
|
+
) : (
|
|
1460
|
+
<Text style={[styles.unsupportedText, { color: textColor }]}>
|
|
1461
|
+
Audio playback not supported on mobile
|
|
1462
|
+
</Text>
|
|
1463
|
+
)}
|
|
1464
|
+
</View>
|
|
1465
|
+
) : (
|
|
1466
|
+
<View style={styles.unsupportedFileContainer}>
|
|
1467
|
+
<Ionicons
|
|
1468
|
+
name={getFileIcon(openedFile.contentType) as any}
|
|
1469
|
+
size={64}
|
|
1470
|
+
color={isDarkTheme ? '#666666' : '#CCCCCC'}
|
|
1471
|
+
/>
|
|
1472
|
+
<Text style={[styles.unsupportedFileTitle, { color: textColor }]}>
|
|
1473
|
+
Preview Not Available
|
|
1474
|
+
</Text>
|
|
1475
|
+
<Text style={[styles.unsupportedFileDescription, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
|
|
1476
|
+
This file type cannot be previewed in the browser.{'\n'}
|
|
1477
|
+
Download the file to view its contents.
|
|
1478
|
+
</Text>
|
|
1479
|
+
<TouchableOpacity
|
|
1480
|
+
style={[styles.downloadButtonLarge, { backgroundColor: primaryColor }]}
|
|
1481
|
+
onPress={() => handleFileDownload(openedFile.id, openedFile.filename)}
|
|
1482
|
+
>
|
|
1483
|
+
<Ionicons name="download" size={20} color="#FFFFFF" />
|
|
1484
|
+
<Text style={styles.downloadButtonText}>Download File</Text>
|
|
1485
|
+
</TouchableOpacity>
|
|
1486
|
+
</View>
|
|
1487
|
+
)}
|
|
1488
|
+
</ScrollView>
|
|
1489
|
+
</View>
|
|
1490
|
+
);
|
|
1491
|
+
};
|
|
1492
|
+
|
|
1493
|
+
const renderEmptyState = () => (
|
|
1494
|
+
<View style={styles.emptyState}>
|
|
1495
|
+
<Ionicons name="folder-open-outline" size={64} color={isDarkTheme ? '#666666' : '#CCCCCC'} />
|
|
1496
|
+
<Text style={[styles.emptyStateTitle, { color: textColor }]}>No Files Yet</Text>
|
|
1497
|
+
<Text style={[styles.emptyStateDescription, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
|
|
1498
|
+
{user?.id === targetUserId
|
|
1499
|
+
? `Upload files to get started. You can select multiple files at once${Platform.OS === 'web' ? ' or drag & drop them here.' : '.'}`
|
|
1500
|
+
: "This user hasn't uploaded any files yet"
|
|
1501
|
+
}
|
|
1502
|
+
</Text>
|
|
1503
|
+
{user?.id === targetUserId && (
|
|
1504
|
+
<TouchableOpacity
|
|
1505
|
+
style={[styles.emptyStateButton, { backgroundColor: primaryColor }]}
|
|
1506
|
+
onPress={handleFileUpload}
|
|
1507
|
+
disabled={uploading}
|
|
1508
|
+
>
|
|
1509
|
+
{uploading ? (
|
|
1510
|
+
<ActivityIndicator size="small" color="#FFFFFF" />
|
|
1511
|
+
) : (
|
|
1512
|
+
<>
|
|
1513
|
+
<Ionicons name="cloud-upload" size={20} color="#FFFFFF" />
|
|
1514
|
+
<Text style={styles.emptyStateButtonText}>Upload Files</Text>
|
|
1515
|
+
</>
|
|
1516
|
+
)}
|
|
1517
|
+
</TouchableOpacity>
|
|
1518
|
+
)}
|
|
1519
|
+
</View>
|
|
1520
|
+
);
|
|
1521
|
+
|
|
1522
|
+
if (loading) {
|
|
1523
|
+
return (
|
|
1524
|
+
<View style={[styles.container, styles.centerContent, { backgroundColor }]}>
|
|
1525
|
+
<ActivityIndicator size="large" color={primaryColor} />
|
|
1526
|
+
<Text style={[styles.loadingText, { color: textColor }]}>Loading files...</Text>
|
|
1527
|
+
</View>
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
// If a file is opened, show the file viewer
|
|
1532
|
+
if (openedFile) {
|
|
1533
|
+
return (
|
|
1534
|
+
<>
|
|
1535
|
+
{renderFileViewer()}
|
|
1536
|
+
{renderFileDetailsModal()}
|
|
1537
|
+
</>
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
return (
|
|
1542
|
+
<View
|
|
1543
|
+
style={[
|
|
1544
|
+
styles.container,
|
|
1545
|
+
{ backgroundColor },
|
|
1546
|
+
isDragging && Platform.OS === 'web' && styles.dragOverlay
|
|
1547
|
+
]}
|
|
1548
|
+
{...(Platform.OS === 'web' && user?.id === targetUserId ? {
|
|
1549
|
+
onDragOver: handleDragOver,
|
|
1550
|
+
onDragLeave: handleDragLeave,
|
|
1551
|
+
onDrop: handleDrop,
|
|
1552
|
+
} : {})}
|
|
1553
|
+
>
|
|
1554
|
+
{/* Header */}
|
|
1555
|
+
<View style={[
|
|
1556
|
+
styles.header,
|
|
1557
|
+
{
|
|
1558
|
+
borderBottomColor: borderColor,
|
|
1559
|
+
backgroundColor: isDarkTheme ? '#1A1A1A' : '#FFFFFF',
|
|
1560
|
+
shadowColor: '#000000',
|
|
1561
|
+
shadowOffset: {
|
|
1562
|
+
width: 0,
|
|
1563
|
+
height: 2,
|
|
1564
|
+
},
|
|
1565
|
+
shadowOpacity: isDarkTheme ? 0.3 : 0.1,
|
|
1566
|
+
shadowRadius: 8,
|
|
1567
|
+
elevation: 4,
|
|
1568
|
+
}
|
|
1569
|
+
]}>
|
|
1570
|
+
<TouchableOpacity
|
|
1571
|
+
style={[
|
|
1572
|
+
styles.backButton,
|
|
1573
|
+
{
|
|
1574
|
+
backgroundColor: isDarkTheme ? '#2A2A2A' : '#F8F9FA',
|
|
1575
|
+
borderRadius: 12,
|
|
1576
|
+
}
|
|
1577
|
+
]}
|
|
1578
|
+
onPress={onClose || goBack}
|
|
1579
|
+
>
|
|
1580
|
+
<Ionicons name="arrow-back" size={22} color={textColor} />
|
|
1581
|
+
</TouchableOpacity>
|
|
1582
|
+
|
|
1583
|
+
<View style={styles.headerTitleContainer}>
|
|
1584
|
+
<Text style={[styles.headerTitle, { color: textColor }]}>
|
|
1585
|
+
{viewMode === 'photos' ? 'Photos' : 'File Management'}
|
|
1586
|
+
</Text>
|
|
1587
|
+
<Text style={[styles.headerSubtitle, { color: isDarkTheme ? '#AAAAAA' : '#666666' }]}>
|
|
1588
|
+
{filteredFiles.length} {filteredFiles.length === 1 ? 'item' : 'items'}
|
|
1589
|
+
</Text>
|
|
1590
|
+
</View>
|
|
1591
|
+
|
|
1592
|
+
<View style={styles.headerActions}>
|
|
1593
|
+
{/* View Mode Toggle */}
|
|
1594
|
+
<View style={[
|
|
1595
|
+
styles.viewModeToggle,
|
|
1596
|
+
{
|
|
1597
|
+
backgroundColor: isDarkTheme ? '#2A2A2A' : '#F8F9FA',
|
|
1598
|
+
borderWidth: 1,
|
|
1599
|
+
borderColor: isDarkTheme ? '#3A3A3A' : '#E8E9EA',
|
|
1600
|
+
shadowColor: '#000000',
|
|
1601
|
+
shadowOffset: {
|
|
1602
|
+
width: 0,
|
|
1603
|
+
height: 1,
|
|
1604
|
+
},
|
|
1605
|
+
shadowOpacity: isDarkTheme ? 0.3 : 0.05,
|
|
1606
|
+
shadowRadius: 4,
|
|
1607
|
+
elevation: 2,
|
|
1608
|
+
}
|
|
1609
|
+
]}>
|
|
1610
|
+
<TouchableOpacity
|
|
1611
|
+
style={[
|
|
1612
|
+
styles.viewModeButton,
|
|
1613
|
+
viewMode === 'all' && {
|
|
1614
|
+
backgroundColor: primaryColor,
|
|
1615
|
+
shadowColor: primaryColor,
|
|
1616
|
+
shadowOffset: {
|
|
1617
|
+
width: 0,
|
|
1618
|
+
height: 2,
|
|
1619
|
+
},
|
|
1620
|
+
shadowOpacity: 0.3,
|
|
1621
|
+
shadowRadius: 4,
|
|
1622
|
+
elevation: 3,
|
|
1623
|
+
}
|
|
1624
|
+
]}
|
|
1625
|
+
onPress={() => setViewMode('all')}
|
|
1626
|
+
>
|
|
1627
|
+
<Ionicons
|
|
1628
|
+
name="folder"
|
|
1629
|
+
size={18}
|
|
1630
|
+
color={viewMode === 'all' ? '#FFFFFF' : textColor}
|
|
1631
|
+
/>
|
|
1632
|
+
</TouchableOpacity>
|
|
1633
|
+
<TouchableOpacity
|
|
1634
|
+
style={[
|
|
1635
|
+
styles.viewModeButton,
|
|
1636
|
+
viewMode === 'photos' && {
|
|
1637
|
+
backgroundColor: primaryColor,
|
|
1638
|
+
shadowColor: primaryColor,
|
|
1639
|
+
shadowOffset: {
|
|
1640
|
+
width: 0,
|
|
1641
|
+
height: 2,
|
|
1642
|
+
},
|
|
1643
|
+
shadowOpacity: 0.3,
|
|
1644
|
+
shadowRadius: 4,
|
|
1645
|
+
elevation: 3,
|
|
1646
|
+
}
|
|
1647
|
+
]}
|
|
1648
|
+
onPress={() => setViewMode('photos')}
|
|
1649
|
+
>
|
|
1650
|
+
<Ionicons
|
|
1651
|
+
name="images"
|
|
1652
|
+
size={18}
|
|
1653
|
+
color={viewMode === 'photos' ? '#FFFFFF' : textColor}
|
|
1654
|
+
/>
|
|
1655
|
+
</TouchableOpacity>
|
|
1656
|
+
</View>
|
|
1657
|
+
|
|
1658
|
+
{user?.id === targetUserId && (
|
|
1659
|
+
<TouchableOpacity
|
|
1660
|
+
style={[
|
|
1661
|
+
styles.uploadButton,
|
|
1662
|
+
{
|
|
1663
|
+
backgroundColor: primaryColor,
|
|
1664
|
+
shadowColor: primaryColor,
|
|
1665
|
+
shadowOffset: {
|
|
1666
|
+
width: 0,
|
|
1667
|
+
height: 3,
|
|
1668
|
+
},
|
|
1669
|
+
shadowOpacity: 0.4,
|
|
1670
|
+
shadowRadius: 6,
|
|
1671
|
+
elevation: 5,
|
|
1672
|
+
transform: uploading ? [{ scale: 0.95 }] : [{ scale: 1 }],
|
|
1673
|
+
}
|
|
1674
|
+
]}
|
|
1675
|
+
onPress={handleFileUpload}
|
|
1676
|
+
disabled={uploading}
|
|
1677
|
+
>
|
|
1678
|
+
{uploading ? (
|
|
1679
|
+
<View style={styles.uploadProgress}>
|
|
1680
|
+
<ActivityIndicator size="small" color="#FFFFFF" />
|
|
1681
|
+
{uploadProgress && (
|
|
1682
|
+
<Text style={styles.uploadProgressText}>
|
|
1683
|
+
{uploadProgress.current}/{uploadProgress.total}
|
|
1684
|
+
</Text>
|
|
1685
|
+
)}
|
|
1686
|
+
</View>
|
|
1687
|
+
) : (
|
|
1688
|
+
<Ionicons name="add" size={26} color="#FFFFFF" />
|
|
1689
|
+
)}
|
|
1690
|
+
</TouchableOpacity>
|
|
1691
|
+
)}
|
|
1692
|
+
</View>
|
|
1693
|
+
</View>
|
|
1694
|
+
|
|
1695
|
+
{/* Search Bar */}
|
|
1696
|
+
{files.length > 0 && (viewMode === 'all' || files.some(f => f.contentType.startsWith('image/'))) && (
|
|
1697
|
+
<View style={[
|
|
1698
|
+
styles.searchContainer,
|
|
1699
|
+
{
|
|
1700
|
+
backgroundColor: isDarkTheme ? '#1A1A1A' : '#FFFFFF',
|
|
1701
|
+
borderColor: isDarkTheme ? '#3A3A3A' : '#E8E9EA',
|
|
1702
|
+
shadowColor: '#000000',
|
|
1703
|
+
shadowOffset: {
|
|
1704
|
+
width: 0,
|
|
1705
|
+
height: 1,
|
|
1706
|
+
},
|
|
1707
|
+
shadowOpacity: isDarkTheme ? 0.2 : 0.05,
|
|
1708
|
+
shadowRadius: 4,
|
|
1709
|
+
elevation: 2,
|
|
1710
|
+
}
|
|
1711
|
+
]}>
|
|
1712
|
+
<Ionicons name="search" size={22} color={isDarkTheme ? '#888888' : '#666666'} />
|
|
1713
|
+
<TextInput
|
|
1714
|
+
style={[styles.searchInput, { color: textColor }]}
|
|
1715
|
+
placeholder={viewMode === 'photos' ? 'Search photos...' : 'Search files...'}
|
|
1716
|
+
placeholderTextColor={isDarkTheme ? '#888888' : '#999999'}
|
|
1717
|
+
value={searchQuery}
|
|
1718
|
+
onChangeText={setSearchQuery}
|
|
1719
|
+
/>
|
|
1720
|
+
{searchQuery.length > 0 && (
|
|
1721
|
+
<TouchableOpacity
|
|
1722
|
+
onPress={() => setSearchQuery('')}
|
|
1723
|
+
style={styles.searchClearButton}
|
|
1724
|
+
>
|
|
1725
|
+
<Ionicons name="close-circle" size={22} color={isDarkTheme ? '#888888' : '#666666'} />
|
|
1726
|
+
</TouchableOpacity>
|
|
1727
|
+
)}
|
|
1728
|
+
</View>
|
|
1729
|
+
)}
|
|
1730
|
+
|
|
1731
|
+
{/* File Stats */}
|
|
1732
|
+
{files.length > 0 && (
|
|
1733
|
+
<View style={[
|
|
1734
|
+
styles.statsContainer,
|
|
1735
|
+
{
|
|
1736
|
+
backgroundColor: isDarkTheme ? '#1A1A1A' : '#FFFFFF',
|
|
1737
|
+
borderColor: isDarkTheme ? '#3A3A3A' : '#E8E9EA',
|
|
1738
|
+
shadowColor: '#000000',
|
|
1739
|
+
shadowOffset: {
|
|
1740
|
+
width: 0,
|
|
1741
|
+
height: 1,
|
|
1742
|
+
},
|
|
1743
|
+
shadowOpacity: isDarkTheme ? 0.2 : 0.05,
|
|
1744
|
+
shadowRadius: 4,
|
|
1745
|
+
elevation: 2,
|
|
1746
|
+
}
|
|
1747
|
+
]}>
|
|
1748
|
+
<View style={styles.statItem}>
|
|
1749
|
+
<Text style={[styles.statValue, { color: textColor }]}>{filteredFiles.length}</Text>
|
|
1750
|
+
<Text style={[styles.statLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
|
|
1751
|
+
{searchQuery.length > 0 ? 'Found' : (filteredFiles.length === 1 ? (viewMode === 'photos' ? 'Photo' : 'File') : (viewMode === 'photos' ? 'Photos' : 'Files'))}
|
|
1752
|
+
</Text>
|
|
1753
|
+
</View>
|
|
1754
|
+
<View style={styles.statItem}>
|
|
1755
|
+
<Text style={[styles.statValue, { color: textColor }]}>
|
|
1756
|
+
{formatFileSize(filteredFiles.reduce((total, file) => total + file.length, 0))}
|
|
1757
|
+
</Text>
|
|
1758
|
+
<Text style={[styles.statLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
|
|
1759
|
+
{searchQuery.length > 0 ? 'Size' : 'Total Size'}
|
|
1760
|
+
</Text>
|
|
1761
|
+
</View>
|
|
1762
|
+
{searchQuery.length > 0 && (
|
|
1763
|
+
<View style={styles.statItem}>
|
|
1764
|
+
<Text style={[styles.statValue, { color: textColor }]}>{files.length}</Text>
|
|
1765
|
+
<Text style={[styles.statLabel, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
|
|
1766
|
+
Total
|
|
1767
|
+
</Text>
|
|
1768
|
+
</View>
|
|
1769
|
+
)}
|
|
1770
|
+
</View>
|
|
1771
|
+
)}
|
|
1772
|
+
|
|
1773
|
+
{/* File List */}
|
|
1774
|
+
{viewMode === 'photos' ? (
|
|
1775
|
+
renderPhotoGrid()
|
|
1776
|
+
) : (
|
|
1777
|
+
<ScrollView
|
|
1778
|
+
style={styles.scrollView}
|
|
1779
|
+
contentContainerStyle={styles.scrollContainer}
|
|
1780
|
+
refreshControl={
|
|
1781
|
+
<RefreshControl
|
|
1782
|
+
refreshing={refreshing}
|
|
1783
|
+
onRefresh={() => loadFiles(true)}
|
|
1784
|
+
tintColor={primaryColor}
|
|
1785
|
+
/>
|
|
1786
|
+
}
|
|
1787
|
+
>
|
|
1788
|
+
{filteredFiles.length === 0 && searchQuery.length > 0 ? (
|
|
1789
|
+
<View style={styles.emptyState}>
|
|
1790
|
+
<Ionicons name="search" size={64} color={isDarkTheme ? '#666666' : '#CCCCCC'} />
|
|
1791
|
+
<Text style={[styles.emptyStateTitle, { color: textColor }]}>No Results Found</Text>
|
|
1792
|
+
<Text style={[styles.emptyStateDescription, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
|
|
1793
|
+
No files match your search for "{searchQuery}"
|
|
1794
|
+
</Text>
|
|
1795
|
+
<TouchableOpacity
|
|
1796
|
+
style={[styles.emptyStateButton, { backgroundColor: primaryColor }]}
|
|
1797
|
+
onPress={() => setSearchQuery('')}
|
|
1798
|
+
>
|
|
1799
|
+
<Ionicons name="refresh" size={20} color="#FFFFFF" />
|
|
1800
|
+
<Text style={styles.emptyStateButtonText}>Clear Search</Text>
|
|
1801
|
+
</TouchableOpacity>
|
|
1802
|
+
</View>
|
|
1803
|
+
) : filteredFiles.length === 0 ? renderEmptyState() : (
|
|
1804
|
+
<>
|
|
1805
|
+
{filteredFiles.map(renderFileItem)}
|
|
1806
|
+
</>
|
|
1807
|
+
)}
|
|
1808
|
+
</ScrollView>
|
|
1809
|
+
)}
|
|
1810
|
+
|
|
1811
|
+
{renderFileDetailsModal()}
|
|
1812
|
+
|
|
1813
|
+
{/* Drag and Drop Overlay */}
|
|
1814
|
+
{isDragging && Platform.OS === 'web' && (
|
|
1815
|
+
<View style={styles.dragDropOverlay}>
|
|
1816
|
+
<View style={styles.dragDropContent}>
|
|
1817
|
+
<Ionicons name="cloud-upload" size={64} color={primaryColor} />
|
|
1818
|
+
<Text style={[styles.dragDropTitle, { color: primaryColor }]}>
|
|
1819
|
+
Drop files to upload
|
|
1820
|
+
</Text>
|
|
1821
|
+
<Text style={[styles.dragDropSubtitle, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
|
|
1822
|
+
Release to upload{uploadProgress ? ` (${uploadProgress.current}/${uploadProgress.total})` : ' multiple files'}
|
|
1823
|
+
</Text>
|
|
1824
|
+
</View>
|
|
1825
|
+
</View>
|
|
1826
|
+
)}
|
|
1827
|
+
</View>
|
|
1828
|
+
);
|
|
1829
|
+
};
|
|
1830
|
+
|
|
1831
|
+
const styles = StyleSheet.create({
|
|
1832
|
+
container: {
|
|
1833
|
+
flex: 1,
|
|
1834
|
+
},
|
|
1835
|
+
dragOverlay: {
|
|
1836
|
+
backgroundColor: 'rgba(0, 122, 255, 0.1)',
|
|
1837
|
+
borderWidth: 2,
|
|
1838
|
+
borderColor: '#007AFF',
|
|
1839
|
+
borderStyle: 'dashed',
|
|
1840
|
+
},
|
|
1841
|
+
centerContent: {
|
|
1842
|
+
justifyContent: 'center',
|
|
1843
|
+
alignItems: 'center',
|
|
1844
|
+
},
|
|
1845
|
+
header: {
|
|
1846
|
+
flexDirection: 'row',
|
|
1847
|
+
alignItems: 'center',
|
|
1848
|
+
justifyContent: 'space-between',
|
|
1849
|
+
paddingHorizontal: 24,
|
|
1850
|
+
paddingVertical: 20,
|
|
1851
|
+
borderBottomWidth: 1,
|
|
1852
|
+
position: 'relative',
|
|
1853
|
+
},
|
|
1854
|
+
backButton: {
|
|
1855
|
+
padding: 12,
|
|
1856
|
+
borderRadius: 12,
|
|
1857
|
+
alignItems: 'center',
|
|
1858
|
+
justifyContent: 'center',
|
|
1859
|
+
minWidth: 44,
|
|
1860
|
+
minHeight: 44,
|
|
1861
|
+
},
|
|
1862
|
+
headerTitleContainer: {
|
|
1863
|
+
flex: 1,
|
|
1864
|
+
alignItems: 'center',
|
|
1865
|
+
justifyContent: 'center',
|
|
1866
|
+
marginHorizontal: 16,
|
|
1867
|
+
},
|
|
1868
|
+
headerTitle: {
|
|
1869
|
+
fontSize: 22,
|
|
1870
|
+
fontWeight: '700',
|
|
1871
|
+
fontFamily: fontFamilies.phuduBold,
|
|
1872
|
+
letterSpacing: -0.5,
|
|
1873
|
+
lineHeight: 28,
|
|
1874
|
+
},
|
|
1875
|
+
headerSubtitle: {
|
|
1876
|
+
fontSize: 13,
|
|
1877
|
+
fontWeight: '500',
|
|
1878
|
+
fontFamily: fontFamilies.phuduMedium,
|
|
1879
|
+
marginTop: 2,
|
|
1880
|
+
letterSpacing: 0.2,
|
|
1881
|
+
},
|
|
1882
|
+
uploadButton: {
|
|
1883
|
+
width: 44,
|
|
1884
|
+
height: 44,
|
|
1885
|
+
borderRadius: 22,
|
|
1886
|
+
alignItems: 'center',
|
|
1887
|
+
justifyContent: 'center',
|
|
1888
|
+
},
|
|
1889
|
+
uploadProgress: {
|
|
1890
|
+
alignItems: 'center',
|
|
1891
|
+
justifyContent: 'center',
|
|
1892
|
+
},
|
|
1893
|
+
uploadProgressText: {
|
|
1894
|
+
color: '#FFFFFF',
|
|
1895
|
+
fontSize: 10,
|
|
1896
|
+
fontWeight: '600',
|
|
1897
|
+
marginTop: 2,
|
|
1898
|
+
},
|
|
1899
|
+
searchContainer: {
|
|
1900
|
+
flexDirection: 'row',
|
|
1901
|
+
alignItems: 'center',
|
|
1902
|
+
paddingHorizontal: 20,
|
|
1903
|
+
paddingVertical: 16,
|
|
1904
|
+
marginHorizontal: 24,
|
|
1905
|
+
marginTop: 20,
|
|
1906
|
+
borderRadius: 16,
|
|
1907
|
+
borderWidth: 1,
|
|
1908
|
+
gap: 16,
|
|
1909
|
+
},
|
|
1910
|
+
searchInput: {
|
|
1911
|
+
flex: 1,
|
|
1912
|
+
fontSize: 16,
|
|
1913
|
+
fontFamily: fontFamilies.phudu,
|
|
1914
|
+
lineHeight: 20,
|
|
1915
|
+
},
|
|
1916
|
+
searchClearButton: {
|
|
1917
|
+
padding: 4,
|
|
1918
|
+
borderRadius: 12,
|
|
1919
|
+
alignItems: 'center',
|
|
1920
|
+
justifyContent: 'center',
|
|
1921
|
+
},
|
|
1922
|
+
searchIcon: {
|
|
1923
|
+
marginRight: 8,
|
|
1924
|
+
},
|
|
1925
|
+
statsContainer: {
|
|
1926
|
+
flexDirection: 'row',
|
|
1927
|
+
paddingHorizontal: 24,
|
|
1928
|
+
paddingVertical: 20,
|
|
1929
|
+
marginHorizontal: 24,
|
|
1930
|
+
marginTop: 20,
|
|
1931
|
+
borderRadius: 16,
|
|
1932
|
+
borderWidth: 1,
|
|
1933
|
+
},
|
|
1934
|
+
statItem: {
|
|
1935
|
+
flex: 1,
|
|
1936
|
+
alignItems: 'center',
|
|
1937
|
+
paddingVertical: 4,
|
|
1938
|
+
},
|
|
1939
|
+
statValue: {
|
|
1940
|
+
fontSize: 28,
|
|
1941
|
+
fontWeight: '800',
|
|
1942
|
+
fontFamily: fontFamilies.phuduBold,
|
|
1943
|
+
letterSpacing: -0.5,
|
|
1944
|
+
lineHeight: 32,
|
|
1945
|
+
},
|
|
1946
|
+
statLabel: {
|
|
1947
|
+
fontSize: 15,
|
|
1948
|
+
fontWeight: '500',
|
|
1949
|
+
fontFamily: fontFamilies.phuduMedium,
|
|
1950
|
+
marginTop: 4,
|
|
1951
|
+
letterSpacing: 0.3,
|
|
1952
|
+
},
|
|
1953
|
+
scrollView: {
|
|
1954
|
+
flex: 1,
|
|
1955
|
+
},
|
|
1956
|
+
scrollContainer: {
|
|
1957
|
+
padding: 20,
|
|
1958
|
+
},
|
|
1959
|
+
fileItem: {
|
|
1960
|
+
flexDirection: 'row',
|
|
1961
|
+
alignItems: 'center',
|
|
1962
|
+
padding: 16,
|
|
1963
|
+
marginBottom: 12,
|
|
1964
|
+
borderRadius: 12,
|
|
1965
|
+
borderWidth: 1,
|
|
1966
|
+
},
|
|
1967
|
+
fileContent: {
|
|
1968
|
+
flexDirection: 'row',
|
|
1969
|
+
alignItems: 'center',
|
|
1970
|
+
flex: 1,
|
|
1971
|
+
},
|
|
1972
|
+
fileIconContainer: {
|
|
1973
|
+
width: 50,
|
|
1974
|
+
height: 50,
|
|
1975
|
+
alignItems: 'center',
|
|
1976
|
+
justifyContent: 'center',
|
|
1977
|
+
marginRight: 12,
|
|
1978
|
+
},
|
|
1979
|
+
filePreviewContainer: {
|
|
1980
|
+
width: 60,
|
|
1981
|
+
height: 60,
|
|
1982
|
+
marginRight: 12,
|
|
1983
|
+
},
|
|
1984
|
+
filePreview: {
|
|
1985
|
+
width: '100%',
|
|
1986
|
+
height: '100%',
|
|
1987
|
+
borderRadius: 8,
|
|
1988
|
+
backgroundColor: '#F5F5F5',
|
|
1989
|
+
alignItems: 'center',
|
|
1990
|
+
justifyContent: 'center',
|
|
1991
|
+
overflow: 'hidden',
|
|
1992
|
+
position: 'relative',
|
|
1993
|
+
},
|
|
1994
|
+
previewImage: {
|
|
1995
|
+
width: '100%',
|
|
1996
|
+
height: '100%',
|
|
1997
|
+
borderRadius: 8,
|
|
1998
|
+
},
|
|
1999
|
+
pdfPreview: {
|
|
2000
|
+
alignItems: 'center',
|
|
2001
|
+
justifyContent: 'center',
|
|
2002
|
+
width: '100%',
|
|
2003
|
+
height: '100%',
|
|
2004
|
+
backgroundColor: '#FF6B6B20',
|
|
2005
|
+
},
|
|
2006
|
+
pdfLabel: {
|
|
2007
|
+
fontSize: 8,
|
|
2008
|
+
fontWeight: 'bold',
|
|
2009
|
+
marginTop: 2,
|
|
2010
|
+
},
|
|
2011
|
+
videoPreview: {
|
|
2012
|
+
alignItems: 'center',
|
|
2013
|
+
justifyContent: 'center',
|
|
2014
|
+
width: '100%',
|
|
2015
|
+
height: '100%',
|
|
2016
|
+
backgroundColor: '#4ECDC420',
|
|
2017
|
+
},
|
|
2018
|
+
videoLabel: {
|
|
2019
|
+
fontSize: 8,
|
|
2020
|
+
fontWeight: 'bold',
|
|
2021
|
+
marginTop: 2,
|
|
2022
|
+
},
|
|
2023
|
+
fallbackIcon: {
|
|
2024
|
+
position: 'absolute',
|
|
2025
|
+
top: 0,
|
|
2026
|
+
left: 0,
|
|
2027
|
+
right: 0,
|
|
2028
|
+
bottom: 0,
|
|
2029
|
+
alignItems: 'center',
|
|
2030
|
+
justifyContent: 'center',
|
|
2031
|
+
backgroundColor: '#F5F5F5',
|
|
2032
|
+
borderRadius: 8,
|
|
2033
|
+
},
|
|
2034
|
+
previewOverlay: {
|
|
2035
|
+
position: 'absolute',
|
|
2036
|
+
top: 0,
|
|
2037
|
+
left: 0,
|
|
2038
|
+
right: 0,
|
|
2039
|
+
bottom: 0,
|
|
2040
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
2041
|
+
alignItems: 'center',
|
|
2042
|
+
justifyContent: 'center',
|
|
2043
|
+
borderRadius: 8,
|
|
2044
|
+
},
|
|
2045
|
+
fileInfo: {
|
|
2046
|
+
flex: 1,
|
|
2047
|
+
},
|
|
2048
|
+
fileName: {
|
|
2049
|
+
fontSize: 16,
|
|
2050
|
+
fontWeight: '600',
|
|
2051
|
+
marginBottom: 4,
|
|
2052
|
+
},
|
|
2053
|
+
fileDetails: {
|
|
2054
|
+
fontSize: 14,
|
|
2055
|
+
marginBottom: 2,
|
|
2056
|
+
},
|
|
2057
|
+
fileDescription: {
|
|
2058
|
+
fontSize: 12,
|
|
2059
|
+
fontStyle: 'italic',
|
|
2060
|
+
},
|
|
2061
|
+
fileActions: {
|
|
2062
|
+
flexDirection: 'row',
|
|
2063
|
+
gap: 8,
|
|
2064
|
+
},
|
|
2065
|
+
actionButton: {
|
|
2066
|
+
width: 40,
|
|
2067
|
+
height: 40,
|
|
2068
|
+
borderRadius: 20,
|
|
2069
|
+
alignItems: 'center',
|
|
2070
|
+
justifyContent: 'center',
|
|
2071
|
+
},
|
|
2072
|
+
emptyState: {
|
|
2073
|
+
alignItems: 'center',
|
|
2074
|
+
paddingVertical: 60,
|
|
2075
|
+
paddingHorizontal: 40,
|
|
2076
|
+
},
|
|
2077
|
+
emptyStateTitle: {
|
|
2078
|
+
fontSize: 24,
|
|
2079
|
+
fontWeight: 'bold',
|
|
2080
|
+
fontFamily: fontFamilies.phuduBold,
|
|
2081
|
+
marginTop: 16,
|
|
2082
|
+
marginBottom: 8,
|
|
2083
|
+
},
|
|
2084
|
+
emptyStateDescription: {
|
|
2085
|
+
fontSize: 16,
|
|
2086
|
+
textAlign: 'center',
|
|
2087
|
+
lineHeight: 24,
|
|
2088
|
+
marginBottom: 32,
|
|
2089
|
+
},
|
|
2090
|
+
emptyStateButton: {
|
|
2091
|
+
flexDirection: 'row',
|
|
2092
|
+
alignItems: 'center',
|
|
2093
|
+
paddingHorizontal: 24,
|
|
2094
|
+
paddingVertical: 12,
|
|
2095
|
+
borderRadius: 24,
|
|
2096
|
+
gap: 8,
|
|
2097
|
+
},
|
|
2098
|
+
emptyStateButtonText: {
|
|
2099
|
+
color: '#FFFFFF',
|
|
2100
|
+
fontSize: 16,
|
|
2101
|
+
fontWeight: '600',
|
|
2102
|
+
},
|
|
2103
|
+
loadingText: {
|
|
2104
|
+
fontSize: 16,
|
|
2105
|
+
marginTop: 16,
|
|
2106
|
+
},
|
|
2107
|
+
|
|
2108
|
+
// Modal styles
|
|
2109
|
+
modalContainer: {
|
|
2110
|
+
flex: 1,
|
|
2111
|
+
},
|
|
2112
|
+
modalHeader: {
|
|
2113
|
+
flexDirection: 'row',
|
|
2114
|
+
alignItems: 'center',
|
|
2115
|
+
justifyContent: 'space-between',
|
|
2116
|
+
paddingHorizontal: 20,
|
|
2117
|
+
paddingVertical: 16,
|
|
2118
|
+
borderBottomWidth: 1,
|
|
2119
|
+
},
|
|
2120
|
+
modalCloseButton: {
|
|
2121
|
+
padding: 8,
|
|
2122
|
+
},
|
|
2123
|
+
modalTitle: {
|
|
2124
|
+
fontSize: 18,
|
|
2125
|
+
fontWeight: '600',
|
|
2126
|
+
fontFamily: fontFamilies.phuduSemiBold,
|
|
2127
|
+
},
|
|
2128
|
+
modalPlaceholder: {
|
|
2129
|
+
width: 40,
|
|
2130
|
+
},
|
|
2131
|
+
modalContent: {
|
|
2132
|
+
flex: 1,
|
|
2133
|
+
padding: 20,
|
|
2134
|
+
},
|
|
2135
|
+
fileDetailCard: {
|
|
2136
|
+
padding: 24,
|
|
2137
|
+
borderRadius: 16,
|
|
2138
|
+
borderWidth: 1,
|
|
2139
|
+
alignItems: 'center',
|
|
2140
|
+
},
|
|
2141
|
+
fileDetailIcon: {
|
|
2142
|
+
marginBottom: 16,
|
|
2143
|
+
},
|
|
2144
|
+
fileDetailName: {
|
|
2145
|
+
fontSize: 20,
|
|
2146
|
+
fontWeight: 'bold',
|
|
2147
|
+
fontFamily: fontFamilies.phuduBold,
|
|
2148
|
+
textAlign: 'center',
|
|
2149
|
+
marginBottom: 24,
|
|
2150
|
+
},
|
|
2151
|
+
fileDetailInfo: {
|
|
2152
|
+
width: '100%',
|
|
2153
|
+
marginBottom: 32,
|
|
2154
|
+
},
|
|
2155
|
+
detailRow: {
|
|
2156
|
+
flexDirection: 'row',
|
|
2157
|
+
justifyContent: 'space-between',
|
|
2158
|
+
alignItems: 'flex-start',
|
|
2159
|
+
marginBottom: 12,
|
|
2160
|
+
flexWrap: 'wrap',
|
|
2161
|
+
},
|
|
2162
|
+
detailLabel: {
|
|
2163
|
+
fontSize: 16,
|
|
2164
|
+
fontWeight: '500',
|
|
2165
|
+
flex: 1,
|
|
2166
|
+
minWidth: 100,
|
|
2167
|
+
},
|
|
2168
|
+
detailValue: {
|
|
2169
|
+
fontSize: 16,
|
|
2170
|
+
flex: 2,
|
|
2171
|
+
textAlign: 'right',
|
|
2172
|
+
},
|
|
2173
|
+
modalActions: {
|
|
2174
|
+
flexDirection: 'row',
|
|
2175
|
+
gap: 12,
|
|
2176
|
+
width: '100%',
|
|
2177
|
+
},
|
|
2178
|
+
modalActionButton: {
|
|
2179
|
+
flex: 1,
|
|
2180
|
+
flexDirection: 'row',
|
|
2181
|
+
alignItems: 'center',
|
|
2182
|
+
justifyContent: 'center',
|
|
2183
|
+
paddingVertical: 16,
|
|
2184
|
+
borderRadius: 12,
|
|
2185
|
+
gap: 8,
|
|
2186
|
+
},
|
|
2187
|
+
modalActionText: {
|
|
2188
|
+
color: '#FFFFFF',
|
|
2189
|
+
fontSize: 16,
|
|
2190
|
+
fontWeight: '600',
|
|
2191
|
+
},
|
|
2192
|
+
|
|
2193
|
+
// Drag and Drop styles
|
|
2194
|
+
dragDropOverlay: {
|
|
2195
|
+
position: 'absolute',
|
|
2196
|
+
top: 0,
|
|
2197
|
+
left: 0,
|
|
2198
|
+
right: 0,
|
|
2199
|
+
bottom: 0,
|
|
2200
|
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
2201
|
+
justifyContent: 'center',
|
|
2202
|
+
alignItems: 'center',
|
|
2203
|
+
zIndex: 1000,
|
|
2204
|
+
},
|
|
2205
|
+
dragDropContent: {
|
|
2206
|
+
alignItems: 'center',
|
|
2207
|
+
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
|
2208
|
+
padding: 40,
|
|
2209
|
+
borderRadius: 20,
|
|
2210
|
+
borderWidth: 3,
|
|
2211
|
+
borderColor: '#007AFF',
|
|
2212
|
+
borderStyle: 'dashed',
|
|
2213
|
+
},
|
|
2214
|
+
dragDropTitle: {
|
|
2215
|
+
fontSize: 24,
|
|
2216
|
+
fontWeight: 'bold',
|
|
2217
|
+
marginTop: 16,
|
|
2218
|
+
marginBottom: 8,
|
|
2219
|
+
},
|
|
2220
|
+
dragDropSubtitle: {
|
|
2221
|
+
fontSize: 16,
|
|
2222
|
+
textAlign: 'center',
|
|
2223
|
+
},
|
|
2224
|
+
|
|
2225
|
+
// File Viewer styles
|
|
2226
|
+
fileViewerContainer: {
|
|
2227
|
+
flex: 1,
|
|
2228
|
+
},
|
|
2229
|
+
fileViewerHeader: {
|
|
2230
|
+
flexDirection: 'row',
|
|
2231
|
+
alignItems: 'center',
|
|
2232
|
+
paddingHorizontal: 20,
|
|
2233
|
+
paddingVertical: 16,
|
|
2234
|
+
borderBottomWidth: 1,
|
|
2235
|
+
},
|
|
2236
|
+
fileViewerTitleContainer: {
|
|
2237
|
+
flex: 1,
|
|
2238
|
+
marginHorizontal: 16,
|
|
2239
|
+
},
|
|
2240
|
+
fileViewerTitle: {
|
|
2241
|
+
fontSize: 18,
|
|
2242
|
+
fontWeight: '600',
|
|
2243
|
+
fontFamily: fontFamilies.phuduSemiBold,
|
|
2244
|
+
marginBottom: 2,
|
|
2245
|
+
},
|
|
2246
|
+
fileViewerSubtitle: {
|
|
2247
|
+
fontSize: 14,
|
|
2248
|
+
},
|
|
2249
|
+
fileViewerActions: {
|
|
2250
|
+
flexDirection: 'row',
|
|
2251
|
+
gap: 8,
|
|
2252
|
+
},
|
|
2253
|
+
fileViewerContent: {
|
|
2254
|
+
flex: 1,
|
|
2255
|
+
},
|
|
2256
|
+
fileViewerContentWithDetails: {
|
|
2257
|
+
paddingBottom: 20,
|
|
2258
|
+
},
|
|
2259
|
+
fileViewerContentContainer: {
|
|
2260
|
+
flexGrow: 1,
|
|
2261
|
+
padding: 20,
|
|
2262
|
+
},
|
|
2263
|
+
fileViewerLoading: {
|
|
2264
|
+
flex: 1,
|
|
2265
|
+
justifyContent: 'center',
|
|
2266
|
+
alignItems: 'center',
|
|
2267
|
+
},
|
|
2268
|
+
fileViewerLoadingText: {
|
|
2269
|
+
fontSize: 16,
|
|
2270
|
+
marginTop: 16,
|
|
2271
|
+
},
|
|
2272
|
+
imageContainer: {
|
|
2273
|
+
alignItems: 'center',
|
|
2274
|
+
justifyContent: 'center',
|
|
2275
|
+
flex: 1,
|
|
2276
|
+
},
|
|
2277
|
+
textContainer: {
|
|
2278
|
+
flex: 1,
|
|
2279
|
+
borderRadius: 12,
|
|
2280
|
+
borderWidth: 1,
|
|
2281
|
+
padding: 16,
|
|
2282
|
+
minHeight: 200,
|
|
2283
|
+
maxHeight: '80%',
|
|
2284
|
+
},
|
|
2285
|
+
textContent: {
|
|
2286
|
+
fontSize: 14,
|
|
2287
|
+
fontFamily: Platform.OS === 'web' ? 'monospace' : 'Courier',
|
|
2288
|
+
lineHeight: 20,
|
|
2289
|
+
},
|
|
2290
|
+
unsupportedFileContainer: {
|
|
2291
|
+
flex: 1,
|
|
2292
|
+
justifyContent: 'center',
|
|
2293
|
+
alignItems: 'center',
|
|
2294
|
+
paddingVertical: 60,
|
|
2295
|
+
paddingHorizontal: 40,
|
|
2296
|
+
},
|
|
2297
|
+
unsupportedFileTitle: {
|
|
2298
|
+
fontSize: 24,
|
|
2299
|
+
fontWeight: 'bold',
|
|
2300
|
+
fontFamily: fontFamilies.phuduBold,
|
|
2301
|
+
marginTop: 16,
|
|
2302
|
+
marginBottom: 8,
|
|
2303
|
+
textAlign: 'center',
|
|
2304
|
+
},
|
|
2305
|
+
unsupportedFileDescription: {
|
|
2306
|
+
fontSize: 16,
|
|
2307
|
+
textAlign: 'center',
|
|
2308
|
+
lineHeight: 24,
|
|
2309
|
+
marginBottom: 32,
|
|
2310
|
+
},
|
|
2311
|
+
downloadButtonLarge: {
|
|
2312
|
+
flexDirection: 'row',
|
|
2313
|
+
alignItems: 'center',
|
|
2314
|
+
paddingHorizontal: 24,
|
|
2315
|
+
paddingVertical: 16,
|
|
2316
|
+
borderRadius: 24,
|
|
2317
|
+
gap: 8,
|
|
2318
|
+
},
|
|
2319
|
+
downloadButtonText: {
|
|
2320
|
+
color: '#FFFFFF',
|
|
2321
|
+
fontSize: 16,
|
|
2322
|
+
fontWeight: '600',
|
|
2323
|
+
},
|
|
2324
|
+
pdfContainer: {
|
|
2325
|
+
flex: 1,
|
|
2326
|
+
alignItems: 'center',
|
|
2327
|
+
justifyContent: 'center',
|
|
2328
|
+
},
|
|
2329
|
+
mediaContainer: {
|
|
2330
|
+
flex: 1,
|
|
2331
|
+
alignItems: 'center',
|
|
2332
|
+
justifyContent: 'center',
|
|
2333
|
+
padding: 20,
|
|
2334
|
+
},
|
|
2335
|
+
unsupportedText: {
|
|
2336
|
+
fontSize: 16,
|
|
2337
|
+
textAlign: 'center',
|
|
2338
|
+
fontStyle: 'italic',
|
|
2339
|
+
},
|
|
2340
|
+
|
|
2341
|
+
// File Details in Viewer styles
|
|
2342
|
+
fileDetailsSection: {
|
|
2343
|
+
margin: 16,
|
|
2344
|
+
marginTop: 0,
|
|
2345
|
+
padding: 20,
|
|
2346
|
+
borderRadius: 12,
|
|
2347
|
+
borderWidth: 1,
|
|
2348
|
+
},
|
|
2349
|
+
fileDetailsSectionTitle: {
|
|
2350
|
+
fontSize: 18,
|
|
2351
|
+
fontWeight: '600',
|
|
2352
|
+
fontFamily: fontFamilies.phuduSemiBold,
|
|
2353
|
+
flex: 1,
|
|
2354
|
+
},
|
|
2355
|
+
fileDetailsSectionHeader: {
|
|
2356
|
+
flexDirection: 'row',
|
|
2357
|
+
alignItems: 'center',
|
|
2358
|
+
justifyContent: 'space-between',
|
|
2359
|
+
marginBottom: 16,
|
|
2360
|
+
},
|
|
2361
|
+
fileDetailsSectionToggle: {
|
|
2362
|
+
padding: 4,
|
|
2363
|
+
},
|
|
2364
|
+
fileDetailsActions: {
|
|
2365
|
+
flexDirection: 'row',
|
|
2366
|
+
gap: 12,
|
|
2367
|
+
marginTop: 16,
|
|
2368
|
+
},
|
|
2369
|
+
fileDetailsActionButton: {
|
|
2370
|
+
flex: 1,
|
|
2371
|
+
flexDirection: 'row',
|
|
2372
|
+
alignItems: 'center',
|
|
2373
|
+
justifyContent: 'center',
|
|
2374
|
+
paddingVertical: 12,
|
|
2375
|
+
borderRadius: 8,
|
|
2376
|
+
gap: 6,
|
|
2377
|
+
},
|
|
2378
|
+
fileDetailsActionText: {
|
|
2379
|
+
color: '#FFFFFF',
|
|
2380
|
+
fontSize: 14,
|
|
2381
|
+
fontWeight: '600',
|
|
2382
|
+
},
|
|
2383
|
+
|
|
2384
|
+
// Header styles
|
|
2385
|
+
headerActions: {
|
|
2386
|
+
flexDirection: 'row',
|
|
2387
|
+
alignItems: 'center',
|
|
2388
|
+
gap: 16,
|
|
2389
|
+
},
|
|
2390
|
+
viewModeToggle: {
|
|
2391
|
+
flexDirection: 'row',
|
|
2392
|
+
borderRadius: 24,
|
|
2393
|
+
padding: 3,
|
|
2394
|
+
overflow: 'hidden',
|
|
2395
|
+
},
|
|
2396
|
+
viewModeButton: {
|
|
2397
|
+
paddingHorizontal: 14,
|
|
2398
|
+
paddingVertical: 10,
|
|
2399
|
+
borderRadius: 20,
|
|
2400
|
+
minWidth: 44,
|
|
2401
|
+
alignItems: 'center',
|
|
2402
|
+
justifyContent: 'center',
|
|
2403
|
+
marginHorizontal: 1,
|
|
2404
|
+
},
|
|
2405
|
+
|
|
2406
|
+
// Photo Grid styles
|
|
2407
|
+
photoScrollContainer: {
|
|
2408
|
+
padding: 16,
|
|
2409
|
+
},
|
|
2410
|
+
photoDateSection: {
|
|
2411
|
+
marginBottom: 24,
|
|
2412
|
+
},
|
|
2413
|
+
photoDateHeader: {
|
|
2414
|
+
fontSize: 18,
|
|
2415
|
+
fontWeight: '600',
|
|
2416
|
+
fontFamily: fontFamilies.phuduSemiBold,
|
|
2417
|
+
marginBottom: 12,
|
|
2418
|
+
paddingHorizontal: 4,
|
|
2419
|
+
},
|
|
2420
|
+
photoGrid: {
|
|
2421
|
+
flexDirection: 'row',
|
|
2422
|
+
flexWrap: 'wrap',
|
|
2423
|
+
gap: 4,
|
|
2424
|
+
justifyContent: 'flex-start',
|
|
2425
|
+
},
|
|
2426
|
+
photoItem: {
|
|
2427
|
+
borderRadius: 8,
|
|
2428
|
+
overflow: 'hidden',
|
|
2429
|
+
},
|
|
2430
|
+
photoContainer: {
|
|
2431
|
+
width: '100%',
|
|
2432
|
+
height: '100%',
|
|
2433
|
+
position: 'relative',
|
|
2434
|
+
borderRadius: 8,
|
|
2435
|
+
overflow: 'hidden',
|
|
2436
|
+
},
|
|
2437
|
+
photoImage: {
|
|
2438
|
+
width: '100%',
|
|
2439
|
+
height: '100%',
|
|
2440
|
+
},
|
|
2441
|
+
|
|
2442
|
+
// Justified Grid styles
|
|
2443
|
+
dimensionsLoadingIndicator: {
|
|
2444
|
+
flexDirection: 'row',
|
|
2445
|
+
alignItems: 'center',
|
|
2446
|
+
justifyContent: 'center',
|
|
2447
|
+
paddingVertical: 16,
|
|
2448
|
+
gap: 8,
|
|
2449
|
+
},
|
|
2450
|
+
dimensionsLoadingText: {
|
|
2451
|
+
fontSize: 14,
|
|
2452
|
+
fontStyle: 'italic',
|
|
2453
|
+
},
|
|
2454
|
+
justifiedPhotoGrid: {
|
|
2455
|
+
gap: 4,
|
|
2456
|
+
},
|
|
2457
|
+
justifiedPhotoRow: {
|
|
2458
|
+
flexDirection: 'row',
|
|
2459
|
+
},
|
|
2460
|
+
justifiedPhotoItem: {
|
|
2461
|
+
borderRadius: 6,
|
|
2462
|
+
overflow: 'hidden',
|
|
2463
|
+
position: 'relative',
|
|
2464
|
+
},
|
|
2465
|
+
justifiedPhotoContainer: {
|
|
2466
|
+
width: '100%',
|
|
2467
|
+
height: '100%',
|
|
2468
|
+
position: 'relative',
|
|
2469
|
+
borderRadius: 6,
|
|
2470
|
+
overflow: 'hidden',
|
|
2471
|
+
backgroundColor: '#F5F5F5',
|
|
2472
|
+
},
|
|
2473
|
+
justifiedPhotoImage: {
|
|
2474
|
+
width: '100%',
|
|
2475
|
+
height: '100%',
|
|
2476
|
+
borderRadius: 6,
|
|
2477
|
+
},
|
|
2478
|
+
|
|
2479
|
+
// Simple Photo Grid styles
|
|
2480
|
+
simplePhotoItem: {
|
|
2481
|
+
borderRadius: 8,
|
|
2482
|
+
overflow: 'hidden',
|
|
2483
|
+
backgroundColor: '#F5F5F5',
|
|
2484
|
+
},
|
|
2485
|
+
simplePhotoContainer: {
|
|
2486
|
+
width: '100%',
|
|
2487
|
+
height: '100%',
|
|
2488
|
+
position: 'relative',
|
|
2489
|
+
borderRadius: 8,
|
|
2490
|
+
overflow: 'hidden',
|
|
2491
|
+
},
|
|
2492
|
+
simplePhotoImage: {
|
|
2493
|
+
width: '100%',
|
|
2494
|
+
height: '100%',
|
|
2495
|
+
borderRadius: 8,
|
|
2496
|
+
},
|
|
2497
|
+
|
|
2498
|
+
// Loading skeleton styles
|
|
2499
|
+
photoSkeletonGrid: {
|
|
2500
|
+
flexDirection: 'row',
|
|
2501
|
+
flexWrap: 'wrap',
|
|
2502
|
+
gap: 4,
|
|
2503
|
+
marginTop: 20,
|
|
2504
|
+
},
|
|
2505
|
+
photoSkeletonItem: {
|
|
2506
|
+
width: '32%',
|
|
2507
|
+
aspectRatio: 1,
|
|
2508
|
+
borderRadius: 8,
|
|
2509
|
+
marginBottom: 4,
|
|
2510
|
+
},
|
|
2511
|
+
});
|
|
2512
|
+
|
|
2513
|
+
export default FileManagementScreen;
|