@oxyhq/services 5.13.4 → 5.13.10
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/lib/commonjs/core/HttpClient.js +1 -1
- package/lib/commonjs/core/HttpClient.js.map +1 -1
- package/lib/commonjs/core/OxyServices.js +83 -30
- package/lib/commonjs/core/OxyServices.js.map +1 -1
- package/lib/commonjs/core/index.js +0 -7
- package/lib/commonjs/core/index.js.map +1 -1
- package/lib/commonjs/i18n/locales/en-US.json +222 -6
- package/lib/commonjs/index.js +0 -7
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/lib/sonner.js.map +1 -1
- package/lib/commonjs/ui/components/GroupedItem.js +24 -22
- package/lib/commonjs/ui/components/GroupedItem.js.map +1 -1
- package/lib/commonjs/ui/components/OxyProvider.js +35 -14
- package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
- package/lib/commonjs/ui/navigation/routes.js +36 -1
- package/lib/commonjs/ui/navigation/routes.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountOverviewScreen.js +150 -5
- package/lib/commonjs/ui/screens/AccountOverviewScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js +475 -319
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountVerificationScreen.js +217 -0
- package/lib/commonjs/ui/screens/AccountVerificationScreen.js.map +1 -0
- package/lib/commonjs/ui/screens/FileManagementScreen.js +911 -213
- package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/HelpSupportScreen.js +131 -0
- package/lib/commonjs/ui/screens/HelpSupportScreen.js.map +1 -0
- package/lib/commonjs/ui/screens/HistoryViewScreen.js +258 -0
- package/lib/commonjs/ui/screens/HistoryViewScreen.js.map +1 -0
- package/lib/commonjs/ui/screens/LegalDocumentsScreen.js +211 -0
- package/lib/commonjs/ui/screens/LegalDocumentsScreen.js.map +1 -0
- package/lib/commonjs/ui/screens/PremiumSubscriptionScreen.js +0 -1
- package/lib/commonjs/ui/screens/PremiumSubscriptionScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/PrivacySettingsScreen.js +307 -0
- package/lib/commonjs/ui/screens/PrivacySettingsScreen.js.map +1 -0
- package/lib/commonjs/ui/screens/ProfileScreen.js +1 -7
- package/lib/commonjs/ui/screens/ProfileScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/SavesCollectionsScreen.js +205 -0
- package/lib/commonjs/ui/screens/SavesCollectionsScreen.js.map +1 -0
- package/lib/commonjs/ui/screens/SearchSettingsScreen.js +239 -0
- package/lib/commonjs/ui/screens/SearchSettingsScreen.js.map +1 -0
- package/lib/commonjs/ui/screens/SignInScreen.js +14 -29
- package/lib/commonjs/ui/screens/SignInScreen.js.map +1 -1
- package/lib/commonjs/utils/asyncUtils.js +1 -0
- package/lib/commonjs/utils/asyncUtils.js.map +1 -1
- package/lib/commonjs/utils/cache.js +4 -4
- package/lib/commonjs/utils/cache.js.map +1 -1
- package/lib/commonjs/utils/index.js +0 -6
- package/lib/commonjs/utils/index.js.map +1 -1
- package/lib/module/core/HttpClient.js +1 -1
- package/lib/module/core/HttpClient.js.map +1 -1
- package/lib/module/core/OxyServices.js +82 -29
- package/lib/module/core/OxyServices.js.map +1 -1
- package/lib/module/core/index.js +1 -1
- package/lib/module/core/index.js.map +1 -1
- package/lib/module/i18n/locales/en-US.json +222 -6
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/lib/sonner.js.map +1 -1
- package/lib/module/ui/components/GroupedItem.js +24 -22
- package/lib/module/ui/components/GroupedItem.js.map +1 -1
- package/lib/module/ui/components/OxyProvider.js +40 -17
- package/lib/module/ui/components/OxyProvider.js.map +1 -1
- package/lib/module/ui/navigation/routes.js +36 -1
- package/lib/module/ui/navigation/routes.js.map +1 -1
- package/lib/module/ui/screens/AccountOverviewScreen.js +151 -6
- package/lib/module/ui/screens/AccountOverviewScreen.js.map +1 -1
- package/lib/module/ui/screens/AccountSettingsScreen.js +475 -319
- package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/module/ui/screens/AccountVerificationScreen.js +212 -0
- package/lib/module/ui/screens/AccountVerificationScreen.js.map +1 -0
- package/lib/module/ui/screens/FileManagementScreen.js +913 -212
- package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/module/ui/screens/HelpSupportScreen.js +126 -0
- package/lib/module/ui/screens/HelpSupportScreen.js.map +1 -0
- package/lib/module/ui/screens/HistoryViewScreen.js +253 -0
- package/lib/module/ui/screens/HistoryViewScreen.js.map +1 -0
- package/lib/module/ui/screens/LegalDocumentsScreen.js +206 -0
- package/lib/module/ui/screens/LegalDocumentsScreen.js.map +1 -0
- package/lib/module/ui/screens/PremiumSubscriptionScreen.js +0 -1
- package/lib/module/ui/screens/PremiumSubscriptionScreen.js.map +1 -1
- package/lib/module/ui/screens/PrivacySettingsScreen.js +302 -0
- package/lib/module/ui/screens/PrivacySettingsScreen.js.map +1 -0
- package/lib/module/ui/screens/ProfileScreen.js +1 -7
- package/lib/module/ui/screens/ProfileScreen.js.map +1 -1
- package/lib/module/ui/screens/SavesCollectionsScreen.js +200 -0
- package/lib/module/ui/screens/SavesCollectionsScreen.js.map +1 -0
- package/lib/module/ui/screens/SearchSettingsScreen.js +234 -0
- package/lib/module/ui/screens/SearchSettingsScreen.js.map +1 -0
- package/lib/module/ui/screens/SignInScreen.js +14 -29
- package/lib/module/ui/screens/SignInScreen.js.map +1 -1
- package/lib/module/utils/asyncUtils.js +1 -0
- package/lib/module/utils/asyncUtils.js.map +1 -1
- package/lib/module/utils/cache.js +3 -3
- package/lib/module/utils/cache.js.map +1 -1
- package/lib/module/utils/index.js +1 -1
- package/lib/module/utils/index.js.map +1 -1
- package/lib/typescript/core/OxyServices.d.ts +30 -24
- package/lib/typescript/core/OxyServices.d.ts.map +1 -1
- package/lib/typescript/core/index.d.ts +1 -1
- package/lib/typescript/core/index.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +1 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/lib/sonner.d.ts +1 -0
- package/lib/typescript/lib/sonner.d.ts.map +1 -1
- package/lib/typescript/types/expo-document-picker.d.ts +36 -0
- package/lib/typescript/ui/components/GroupedItem.d.ts.map +1 -1
- package/lib/typescript/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/ui/navigation/routes.d.ts +1 -1
- package/lib/typescript/ui/navigation/routes.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AccountOverviewScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AccountVerificationScreen.d.ts +5 -0
- package/lib/typescript/ui/screens/AccountVerificationScreen.d.ts.map +1 -0
- package/lib/typescript/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/HelpSupportScreen.d.ts +5 -0
- package/lib/typescript/ui/screens/HelpSupportScreen.d.ts.map +1 -0
- package/lib/typescript/ui/screens/HistoryViewScreen.d.ts +5 -0
- package/lib/typescript/ui/screens/HistoryViewScreen.d.ts.map +1 -0
- package/lib/typescript/ui/screens/LegalDocumentsScreen.d.ts +5 -0
- package/lib/typescript/ui/screens/LegalDocumentsScreen.d.ts.map +1 -0
- package/lib/typescript/ui/screens/PremiumSubscriptionScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/PrivacySettingsScreen.d.ts +5 -0
- package/lib/typescript/ui/screens/PrivacySettingsScreen.d.ts.map +1 -0
- package/lib/typescript/ui/screens/ProfileScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/SavesCollectionsScreen.d.ts +5 -0
- package/lib/typescript/ui/screens/SavesCollectionsScreen.d.ts.map +1 -0
- package/lib/typescript/ui/screens/SearchSettingsScreen.d.ts +5 -0
- package/lib/typescript/ui/screens/SearchSettingsScreen.d.ts.map +1 -0
- package/lib/typescript/ui/screens/SignInScreen.d.ts.map +1 -1
- package/lib/typescript/utils/asyncUtils.d.ts.map +1 -1
- package/lib/typescript/utils/cache.d.ts +3 -3
- package/lib/typescript/utils/cache.d.ts.map +1 -1
- package/lib/typescript/utils/index.d.ts +1 -1
- package/lib/typescript/utils/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/HttpClient.ts +1 -1
- package/src/core/OxyServices.ts +80 -30
- package/src/core/index.ts +1 -1
- package/src/i18n/locales/en-US.json +222 -6
- package/src/index.ts +1 -1
- package/src/lib/sonner.ts +1 -0
- package/src/types/expo-document-picker.d.ts +36 -0
- package/src/ui/components/GroupedItem.tsx +23 -21
- package/src/ui/components/OxyProvider.tsx +33 -11
- package/src/ui/navigation/routes.ts +42 -0
- package/src/ui/screens/AccountOverviewScreen.tsx +175 -5
- package/src/ui/screens/AccountSettingsScreen.tsx +521 -360
- package/src/ui/screens/AccountVerificationScreen.tsx +235 -0
- package/src/ui/screens/FileManagementScreen.tsx +934 -208
- package/src/ui/screens/HelpSupportScreen.tsx +143 -0
- package/src/ui/screens/HistoryViewScreen.tsx +280 -0
- package/src/ui/screens/LegalDocumentsScreen.tsx +220 -0
- package/src/ui/screens/PremiumSubscriptionScreen.tsx +0 -1
- package/src/ui/screens/PrivacySettingsScreen.tsx +332 -0
- package/src/ui/screens/ProfileScreen.tsx +1 -8
- package/src/ui/screens/SavesCollectionsScreen.tsx +222 -0
- package/src/ui/screens/SearchSettingsScreen.tsx +219 -0
- package/src/ui/screens/SignInScreen.tsx +19 -35
- package/src/utils/asyncUtils.ts +1 -0
- package/src/utils/cache.ts +3 -3
- package/src/utils/index.ts +1 -1
- package/lib/commonjs/ui/components/StepBasedScreen.README.md +0 -337
- package/lib/commonjs/ui/components/internal/TextField.md +0 -436
- package/lib/commonjs/ui/styles/FONTS.md +0 -126
- package/lib/module/ui/components/StepBasedScreen.README.md +0 -337
- package/lib/module/ui/components/internal/TextField.md +0 -436
- package/lib/module/ui/styles/FONTS.md +0 -126
- package/src/ui/components/StepBasedScreen.README.md +0 -337
- package/src/ui/components/internal/TextField.md +0 -436
- package/src/ui/styles/FONTS.md +0 -126
|
@@ -12,6 +12,9 @@ import {
|
|
|
12
12
|
Modal,
|
|
13
13
|
TextInput,
|
|
14
14
|
Image, // kept for Image.getSize only
|
|
15
|
+
Animated,
|
|
16
|
+
Easing,
|
|
17
|
+
Alert,
|
|
15
18
|
} from 'react-native';
|
|
16
19
|
import { Image as ExpoImage } from 'expo-image';
|
|
17
20
|
import type { BaseScreenProps } from '../navigation/types';
|
|
@@ -92,18 +95,6 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
92
95
|
linkContext,
|
|
93
96
|
}) => {
|
|
94
97
|
const { user, oxyServices } = useOxy();
|
|
95
|
-
|
|
96
|
-
// Debug: log the actual container width
|
|
97
|
-
useEffect(() => {
|
|
98
|
-
console.log('[FileManagementScreen] Container width (full):', containerWidth);
|
|
99
|
-
// Padding structure:
|
|
100
|
-
// - containerWidth = full bottom sheet container width (measured from OxyProvider)
|
|
101
|
-
// - photoScrollContainer adds padding: 16 (32px total horizontal padding)
|
|
102
|
-
// - Available content width = containerWidth - 32
|
|
103
|
-
const availableContentWidth = containerWidth - 32;
|
|
104
|
-
console.log('[FileManagementScreen] Available content width:', availableContentWidth);
|
|
105
|
-
console.log('[FileManagementScreen] Spacing fix applied: 4px uniform gap both horizontal and vertical');
|
|
106
|
-
}, [containerWidth]);
|
|
107
98
|
const files = useFiles();
|
|
108
99
|
const uploading = useUploadingStore();
|
|
109
100
|
const uploadProgress = useUploadAggregateProgress();
|
|
@@ -118,24 +109,63 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
118
109
|
const [fileContent, setFileContent] = useState<string | null>(null);
|
|
119
110
|
const [loadingFileContent, setLoadingFileContent] = useState(false);
|
|
120
111
|
const [showFileDetailsInViewer, setShowFileDetailsInViewer] = useState(false);
|
|
121
|
-
const [viewMode, setViewMode] = useState<'all' | 'photos'>('all');
|
|
112
|
+
const [viewMode, setViewMode] = useState<'all' | 'photos' | 'videos' | 'documents' | 'audio'>('all');
|
|
122
113
|
const [searchQuery, setSearchQuery] = useState('');
|
|
123
|
-
|
|
114
|
+
const [sortBy, setSortBy] = useState<'date' | 'size' | 'name' | 'type'>('date');
|
|
115
|
+
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
|
|
116
|
+
const [pendingFiles, setPendingFiles] = useState<Array<{ file: File | Blob; preview?: string; size: number; name: string; type: string }>>([]);
|
|
117
|
+
const [showUploadPreview, setShowUploadPreview] = useState(false);
|
|
118
|
+
// Derived filtered and sorted files (avoid setState loops)
|
|
124
119
|
const filteredFiles = useMemo(() => {
|
|
125
120
|
let filteredByMode = files;
|
|
126
121
|
if (viewMode === 'photos') {
|
|
127
122
|
filteredByMode = files.filter(file => file.contentType.startsWith('image/'));
|
|
123
|
+
} else if (viewMode === 'videos') {
|
|
124
|
+
filteredByMode = files.filter(file => file.contentType.startsWith('video/'));
|
|
125
|
+
} else if (viewMode === 'documents') {
|
|
126
|
+
filteredByMode = files.filter(file =>
|
|
127
|
+
file.contentType.includes('pdf') ||
|
|
128
|
+
file.contentType.includes('document') ||
|
|
129
|
+
file.contentType.includes('text') ||
|
|
130
|
+
file.contentType.includes('msword') ||
|
|
131
|
+
file.contentType.includes('excel') ||
|
|
132
|
+
file.contentType.includes('spreadsheet') ||
|
|
133
|
+
file.contentType.includes('presentation') ||
|
|
134
|
+
file.contentType.includes('powerpoint')
|
|
135
|
+
);
|
|
136
|
+
} else if (viewMode === 'audio') {
|
|
137
|
+
filteredByMode = files.filter(file => file.contentType.startsWith('audio/'));
|
|
128
138
|
}
|
|
129
|
-
|
|
130
|
-
|
|
139
|
+
|
|
140
|
+
let filtered = filteredByMode;
|
|
141
|
+
if (searchQuery.trim()) {
|
|
142
|
+
const query = searchQuery.toLowerCase();
|
|
143
|
+
filtered = filteredByMode.filter(file =>
|
|
144
|
+
file.filename.toLowerCase().includes(query) ||
|
|
145
|
+
file.contentType.toLowerCase().includes(query) ||
|
|
146
|
+
(file.metadata?.description && file.metadata.description.toLowerCase().includes(query))
|
|
147
|
+
);
|
|
131
148
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
(
|
|
137
|
-
|
|
138
|
-
|
|
149
|
+
|
|
150
|
+
// Sort files
|
|
151
|
+
const sorted = [...filtered].sort((a, b) => {
|
|
152
|
+
let comparison = 0;
|
|
153
|
+
if (sortBy === 'date') {
|
|
154
|
+
const dateA = new Date(a.uploadDate || 0).getTime();
|
|
155
|
+
const dateB = new Date(b.uploadDate || 0).getTime();
|
|
156
|
+
comparison = dateA - dateB;
|
|
157
|
+
} else if (sortBy === 'size') {
|
|
158
|
+
comparison = (a.length || 0) - (b.length || 0);
|
|
159
|
+
} else if (sortBy === 'name') {
|
|
160
|
+
comparison = (a.filename || '').localeCompare(b.filename || '');
|
|
161
|
+
} else if (sortBy === 'type') {
|
|
162
|
+
comparison = (a.contentType || '').localeCompare(b.contentType || '');
|
|
163
|
+
}
|
|
164
|
+
return sortOrder === 'asc' ? comparison : -comparison;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return sorted;
|
|
168
|
+
}, [files, searchQuery, viewMode, sortBy, sortOrder]);
|
|
139
169
|
const [isDragging, setIsDragging] = useState(false);
|
|
140
170
|
const [photoDimensions, setPhotoDimensions] = useState<{ [key: string]: { width: number, height: number } }>({});
|
|
141
171
|
const [loadingDimensions, setLoadingDimensions] = useState(false);
|
|
@@ -144,6 +174,11 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
144
174
|
const MIN_BANNER_MS = 600;
|
|
145
175
|
// Selection state
|
|
146
176
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set(initialSelectedIds));
|
|
177
|
+
const [lastSelectedFileId, setLastSelectedFileId] = useState<string | null>(null);
|
|
178
|
+
const scrollViewRef = useRef<ScrollView>(null);
|
|
179
|
+
const photoScrollViewRef = useRef<ScrollView>(null);
|
|
180
|
+
const itemRefs = useRef<Map<string, number>>(new Map()); // Track item positions
|
|
181
|
+
const containerRef = useRef<View>(null); // Ref for drag and drop container
|
|
147
182
|
useEffect(() => {
|
|
148
183
|
if (initialSelectedIds && initialSelectedIds.length) {
|
|
149
184
|
setSelectedIds(new Set(initialSelectedIds));
|
|
@@ -151,7 +186,8 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
151
186
|
}, [initialSelectedIds]);
|
|
152
187
|
|
|
153
188
|
const toggleSelect = useCallback(async (file: FileMetadata) => {
|
|
154
|
-
|
|
189
|
+
// Allow selection in regular mode for bulk operations
|
|
190
|
+
// if (!selectMode) return;
|
|
155
191
|
if (disabledMimeTypes.length) {
|
|
156
192
|
const blocked = disabledMimeTypes.some(mt => file.contentType === mt || file.contentType.startsWith(mt.endsWith('/') ? mt : mt + '/'));
|
|
157
193
|
if (blocked) {
|
|
@@ -165,12 +201,13 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
165
201
|
if (fileVisibility !== defaultVisibility) {
|
|
166
202
|
try {
|
|
167
203
|
await oxyServices.assetUpdateVisibility(file.id, defaultVisibility);
|
|
168
|
-
console.log(`Updated file ${file.id} visibility from ${fileVisibility} to ${defaultVisibility}`);
|
|
169
204
|
} catch (error) {
|
|
170
|
-
console.error('Failed to update file visibility:', error);
|
|
171
205
|
// Continue anyway - selection shouldn't fail if visibility update fails
|
|
172
206
|
}
|
|
173
207
|
}
|
|
208
|
+
|
|
209
|
+
// Track the selected file for scrolling
|
|
210
|
+
setLastSelectedFileId(file.id);
|
|
174
211
|
|
|
175
212
|
// Link file to entity if linkContext is provided
|
|
176
213
|
if (linkContext) {
|
|
@@ -183,9 +220,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
183
220
|
defaultVisibility,
|
|
184
221
|
(linkContext as any).webhookUrl
|
|
185
222
|
);
|
|
186
|
-
console.log(`Linked file ${file.id} to ${linkContext.app}/${linkContext.entityType}/${linkContext.entityId}`);
|
|
187
223
|
} catch (error) {
|
|
188
|
-
console.error('Failed to link file:', error);
|
|
189
224
|
// Continue anyway - selection shouldn't fail if linking fails
|
|
190
225
|
}
|
|
191
226
|
}
|
|
@@ -228,9 +263,8 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
228
263
|
if (fileVisibility !== defaultVisibility) {
|
|
229
264
|
try {
|
|
230
265
|
await oxyServices.assetUpdateVisibility(file.id, defaultVisibility);
|
|
231
|
-
console.log(`Updated file ${file.id} visibility from ${fileVisibility} to ${defaultVisibility}`);
|
|
232
266
|
} catch (error) {
|
|
233
|
-
|
|
267
|
+
// Visibility update failed, continue with selection
|
|
234
268
|
}
|
|
235
269
|
}
|
|
236
270
|
|
|
@@ -245,9 +279,8 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
245
279
|
defaultVisibility,
|
|
246
280
|
(linkContext as any).webhookUrl
|
|
247
281
|
);
|
|
248
|
-
console.log(`Linked file ${file.id} to ${linkContext.app}/${linkContext.entityType}/${linkContext.entityId}`);
|
|
249
282
|
} catch (error) {
|
|
250
|
-
|
|
283
|
+
// File linking failed, continue with selection
|
|
251
284
|
}
|
|
252
285
|
}
|
|
253
286
|
});
|
|
@@ -378,7 +411,6 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
378
411
|
}));
|
|
379
412
|
}
|
|
380
413
|
} catch (error: any) {
|
|
381
|
-
console.error('Failed to load files:', error);
|
|
382
414
|
toast.error(error.message || 'Failed to load files');
|
|
383
415
|
} finally {
|
|
384
416
|
setLoading(false);
|
|
@@ -465,7 +497,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
465
497
|
setPhotoDimensions(newDimensions);
|
|
466
498
|
}
|
|
467
499
|
} catch (error) {
|
|
468
|
-
|
|
500
|
+
// Photo dimensions loading failed, continue without dimensions
|
|
469
501
|
} finally {
|
|
470
502
|
setLoadingDimensions(false);
|
|
471
503
|
}
|
|
@@ -554,67 +586,171 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
554
586
|
// Silent background refresh to ensure metadata/variants updated
|
|
555
587
|
setTimeout(() => { loadFiles('silent'); }, 1200);
|
|
556
588
|
} catch (error: any) {
|
|
557
|
-
console.error('Upload error:', error);
|
|
558
589
|
toast.error(error.message || 'Failed to upload files');
|
|
559
590
|
} finally {
|
|
560
591
|
storeSetUploadProgress(null);
|
|
561
592
|
}
|
|
562
593
|
};
|
|
563
594
|
|
|
564
|
-
const
|
|
595
|
+
const handleFileSelection = useCallback(async (selectedFiles: File[] | any[]) => {
|
|
596
|
+
const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
|
|
597
|
+
const processedFiles: Array<{ file: File | Blob; preview?: string; size: number; name: string; type: string }> = [];
|
|
598
|
+
|
|
599
|
+
for (const file of selectedFiles) {
|
|
600
|
+
// Validate file size
|
|
601
|
+
if (file.size > MAX_FILE_SIZE) {
|
|
602
|
+
toast.error(`"${file.name}" is too large. Maximum file size is ${formatFileSize(MAX_FILE_SIZE)}`);
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Generate preview for images
|
|
607
|
+
let preview: string | undefined;
|
|
608
|
+
if (file.type.startsWith('image/')) {
|
|
609
|
+
preview = URL.createObjectURL(file);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
processedFiles.push({
|
|
613
|
+
file,
|
|
614
|
+
preview,
|
|
615
|
+
size: file.size,
|
|
616
|
+
name: file.name,
|
|
617
|
+
type: file.type
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (processedFiles.length === 0) return;
|
|
622
|
+
|
|
623
|
+
// Show preview modal for user to review files before upload
|
|
624
|
+
setPendingFiles(processedFiles);
|
|
625
|
+
setShowUploadPreview(true);
|
|
626
|
+
}, []);
|
|
627
|
+
|
|
628
|
+
const handleConfirmUpload = async () => {
|
|
629
|
+
if (pendingFiles.length === 0) return;
|
|
630
|
+
|
|
631
|
+
setShowUploadPreview(false);
|
|
632
|
+
uploadStartRef.current = Date.now();
|
|
633
|
+
storeSetUploading(true);
|
|
634
|
+
storeSetUploadProgress(null);
|
|
635
|
+
|
|
565
636
|
try {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
637
|
+
const filesToUpload = pendingFiles.map(pf => pf.file as File);
|
|
638
|
+
storeSetUploadProgress({ current: 0, total: filesToUpload.length });
|
|
639
|
+
await processFileUploads(filesToUpload);
|
|
640
|
+
|
|
641
|
+
// Cleanup preview URLs
|
|
642
|
+
pendingFiles.forEach(pf => {
|
|
643
|
+
if (pf.preview) {
|
|
644
|
+
URL.revokeObjectURL(pf.preview);
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
setPendingFiles([]);
|
|
648
|
+
endUpload();
|
|
649
|
+
} catch (error: any) {
|
|
650
|
+
toast.error(error.message || 'Failed to upload files');
|
|
651
|
+
endUpload();
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
const handleCancelUpload = () => {
|
|
656
|
+
// Cleanup preview URLs
|
|
657
|
+
pendingFiles.forEach(pf => {
|
|
658
|
+
if (pf.preview) {
|
|
659
|
+
URL.revokeObjectURL(pf.preview);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
setPendingFiles([]);
|
|
663
|
+
setShowUploadPreview(false);
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
const removePendingFile = (index: number) => {
|
|
667
|
+
const file = pendingFiles[index];
|
|
668
|
+
if (file.preview) {
|
|
669
|
+
URL.revokeObjectURL(file.preview);
|
|
670
|
+
}
|
|
671
|
+
const updated = pendingFiles.filter((_, i) => i !== index);
|
|
672
|
+
setPendingFiles(updated);
|
|
673
|
+
if (updated.length === 0) {
|
|
674
|
+
setShowUploadPreview(false);
|
|
675
|
+
}
|
|
676
|
+
};
|
|
569
677
|
|
|
678
|
+
const handleFileUpload = async () => {
|
|
679
|
+
try {
|
|
570
680
|
if (Platform.OS === 'web') {
|
|
571
|
-
//
|
|
681
|
+
// Enhanced web file picker
|
|
572
682
|
const input = document.createElement('input');
|
|
573
683
|
input.type = 'file';
|
|
574
684
|
input.multiple = true;
|
|
575
685
|
input.accept = '*/*';
|
|
576
|
-
|
|
686
|
+
|
|
577
687
|
const cancellationTimer = setTimeout(() => {
|
|
578
688
|
const state = useFileStore.getState();
|
|
579
689
|
if (state.uploading && uploadStartRef.current && !state.uploadProgress) {
|
|
580
|
-
// No selection happened; treat as cancel
|
|
581
690
|
endUpload();
|
|
582
691
|
}
|
|
583
|
-
}, 1500);
|
|
692
|
+
}, 1500);
|
|
584
693
|
|
|
585
694
|
input.onchange = async (e: any) => {
|
|
586
695
|
clearTimeout(cancellationTimer);
|
|
587
696
|
const selectedFiles = Array.from(e.target.files || []) as File[];
|
|
588
697
|
if (selectedFiles.length === 0) {
|
|
589
|
-
// User explicitly canceled (some browsers still fire onchange with empty list)
|
|
590
698
|
endUpload();
|
|
591
699
|
return;
|
|
592
700
|
}
|
|
593
|
-
|
|
594
|
-
await processFileUploads(selectedFiles);
|
|
595
|
-
endUpload();
|
|
701
|
+
await handleFileSelection(selectedFiles);
|
|
596
702
|
};
|
|
597
703
|
|
|
598
704
|
input.click();
|
|
599
705
|
} else {
|
|
600
|
-
// Mobile
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
706
|
+
// Mobile file picker with expo-document-picker
|
|
707
|
+
try {
|
|
708
|
+
// Dynamically import to avoid breaking if not installed
|
|
709
|
+
const DocumentPicker = await import('expo-document-picker').catch(() => null);
|
|
710
|
+
|
|
711
|
+
if (!DocumentPicker) {
|
|
712
|
+
toast.error('File picker not available. Please install expo-document-picker');
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const result = await DocumentPicker.getDocumentAsync({
|
|
717
|
+
type: '*/*',
|
|
718
|
+
multiple: true,
|
|
719
|
+
copyToCacheDirectory: true,
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
if (result.canceled) {
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Convert expo document picker results to File-like objects
|
|
727
|
+
const files: File[] = [];
|
|
728
|
+
for (const doc of result.assets) {
|
|
729
|
+
if (doc.file) {
|
|
730
|
+
// expo-document-picker provides a File-like object
|
|
731
|
+
files.push(doc.file as File);
|
|
732
|
+
} else if (doc.uri) {
|
|
733
|
+
// Fallback: fetch and create Blob
|
|
734
|
+
const response = await fetch(doc.uri);
|
|
735
|
+
const blob = await response.blob();
|
|
736
|
+
const file = new File([blob], doc.name || 'file', { type: doc.mimeType || 'application/octet-stream' });
|
|
737
|
+
files.push(file);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (files.length > 0) {
|
|
742
|
+
await handleFileSelection(files);
|
|
743
|
+
}
|
|
744
|
+
} catch (error: any) {
|
|
745
|
+
if (error.message?.includes('expo-document-picker')) {
|
|
746
|
+
toast.error('File picker not available. Please install expo-document-picker');
|
|
747
|
+
} else {
|
|
748
|
+
toast.error(error.message || 'Failed to select files');
|
|
749
|
+
}
|
|
608
750
|
}
|
|
609
751
|
}
|
|
610
752
|
} catch (error: any) {
|
|
611
|
-
toast.error(error.message || 'Failed to
|
|
612
|
-
} finally {
|
|
613
|
-
// IMPORTANT: Do NOT call endUpload here.
|
|
614
|
-
// We only want to hide the banner after the actual upload(s) complete.
|
|
615
|
-
// The input.onchange handler invokes processFileUploads then calls endUpload().
|
|
616
|
-
// Calling endUpload here caused the banner to disappear while files were still uploading.
|
|
617
|
-
storeSetUploadProgress(null); // keep clearing any stale progress
|
|
753
|
+
toast.error(error.message || 'Failed to open file picker');
|
|
618
754
|
}
|
|
619
755
|
};
|
|
620
756
|
|
|
@@ -623,18 +759,12 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
623
759
|
const confirmed = window.confirm(`Are you sure you want to delete "${filename}"? This action cannot be undone.`);
|
|
624
760
|
|
|
625
761
|
if (!confirmed) {
|
|
626
|
-
console.log('Delete cancelled by user');
|
|
627
762
|
return;
|
|
628
763
|
}
|
|
629
764
|
|
|
630
765
|
try {
|
|
631
|
-
console.log('Deleting file:', { fileId, filename });
|
|
632
|
-
console.log('Target user ID:', targetUserId);
|
|
633
|
-
console.log('Current user ID:', user?.id);
|
|
634
766
|
storeSetDeleting(fileId);
|
|
635
|
-
|
|
636
|
-
const result = await oxyServices.deleteFile(fileId);
|
|
637
|
-
console.log('Delete result:', result);
|
|
767
|
+
await oxyServices.deleteFile(fileId);
|
|
638
768
|
|
|
639
769
|
toast.success('File deleted successfully');
|
|
640
770
|
|
|
@@ -644,8 +774,6 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
644
774
|
// Silent background reconcile
|
|
645
775
|
setTimeout(() => loadFiles('silent'), 800);
|
|
646
776
|
} catch (error: any) {
|
|
647
|
-
console.error('Delete error:', error);
|
|
648
|
-
console.error('Error details:', error.response?.data || error.message);
|
|
649
777
|
|
|
650
778
|
// Provide specific error messages
|
|
651
779
|
if (error.message?.includes('File not found') || error.message?.includes('404')) {
|
|
@@ -662,88 +790,155 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
662
790
|
}
|
|
663
791
|
};
|
|
664
792
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
793
|
+
const handleBulkDelete = useCallback(async () => {
|
|
794
|
+
if (selectedIds.size === 0) return;
|
|
795
|
+
|
|
796
|
+
const fileMap: Record<string, FileMetadata> = {};
|
|
797
|
+
files.forEach(f => { fileMap[f.id] = f; });
|
|
798
|
+
const selectedFiles = Array.from(selectedIds).map(id => fileMap[id]).filter(Boolean);
|
|
799
|
+
|
|
800
|
+
const confirmed = window.confirm(
|
|
801
|
+
`Are you sure you want to delete ${selectedFiles.length} file(s)? This action cannot be undone.`
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
if (!confirmed) return;
|
|
805
|
+
|
|
806
|
+
try {
|
|
807
|
+
const deletePromises = Array.from(selectedIds).map(async (fileId) => {
|
|
808
|
+
try {
|
|
809
|
+
await oxyServices.deleteFile(fileId);
|
|
810
|
+
useFileStore.getState().removeFile(fileId);
|
|
811
|
+
return { success: true, fileId };
|
|
812
|
+
} catch (error: any) {
|
|
813
|
+
return { success: false, fileId, error };
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
const results = await Promise.allSettled(deletePromises);
|
|
818
|
+
const successful = results.filter(r => r.status === 'fulfilled' && r.value.success).length;
|
|
819
|
+
const failed = results.length - successful;
|
|
820
|
+
|
|
821
|
+
if (successful > 0) {
|
|
822
|
+
toast.success(`${successful} file(s) deleted successfully`);
|
|
823
|
+
}
|
|
824
|
+
if (failed > 0) {
|
|
825
|
+
toast.error(`${failed} file(s) failed to delete`);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
setSelectedIds(new Set());
|
|
829
|
+
setTimeout(() => loadFiles('silent'), 800);
|
|
830
|
+
} catch (error: any) {
|
|
831
|
+
toast.error(error.message || 'Failed to delete files');
|
|
677
832
|
}
|
|
678
|
-
};
|
|
833
|
+
}, [selectedIds, files, oxyServices, loadFiles]);
|
|
679
834
|
|
|
680
|
-
const
|
|
681
|
-
if (
|
|
682
|
-
|
|
683
|
-
|
|
835
|
+
const handleBulkVisibilityChange = useCallback(async (visibility: 'private' | 'public' | 'unlisted') => {
|
|
836
|
+
if (selectedIds.size === 0) return;
|
|
837
|
+
|
|
838
|
+
try {
|
|
839
|
+
const updatePromises = Array.from(selectedIds).map(async (fileId) => {
|
|
840
|
+
try {
|
|
841
|
+
await oxyServices.assetUpdateVisibility(fileId, visibility);
|
|
842
|
+
return { success: true, fileId };
|
|
843
|
+
} catch (error: any) {
|
|
844
|
+
return { success: false, fileId, error };
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
const results = await Promise.allSettled(updatePromises);
|
|
849
|
+
const successful = results.filter(r => r.status === 'fulfilled' && r.value.success).length;
|
|
850
|
+
const failed = results.length - successful;
|
|
851
|
+
|
|
852
|
+
if (successful > 0) {
|
|
853
|
+
toast.success(`${successful} file(s) visibility updated to ${visibility}`);
|
|
854
|
+
// Update file metadata in store
|
|
855
|
+
Array.from(selectedIds).forEach(fileId => {
|
|
856
|
+
useFileStore.getState().updateFile(fileId, {
|
|
857
|
+
metadata: { ...files.find(f => f.id === fileId)?.metadata, visibility }
|
|
858
|
+
} as any);
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
if (failed > 0) {
|
|
862
|
+
toast.error(`${failed} file(s) failed to update visibility`);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
setTimeout(() => loadFiles('silent'), 800);
|
|
866
|
+
} catch (error: any) {
|
|
867
|
+
toast.error(error.message || 'Failed to update visibility');
|
|
684
868
|
}
|
|
685
|
-
};
|
|
869
|
+
}, [selectedIds, oxyServices, files, loadFiles]);
|
|
686
870
|
|
|
687
|
-
// Global drag listeners (web) to
|
|
871
|
+
// Global drag listeners (web) - attach to document for reliable drag and drop
|
|
688
872
|
useEffect(() => {
|
|
689
873
|
if (Platform.OS !== 'web' || user?.id !== targetUserId) return;
|
|
690
|
-
|
|
691
|
-
|
|
874
|
+
|
|
875
|
+
let dragCounter = 0; // Track drag enter/leave to handle nested elements
|
|
876
|
+
|
|
877
|
+
const onDragEnter = (e: DragEvent) => {
|
|
878
|
+
dragCounter++;
|
|
879
|
+
if (e?.dataTransfer?.types?.includes('Files')) {
|
|
880
|
+
e.preventDefault();
|
|
881
|
+
e.stopPropagation();
|
|
882
|
+
setIsDragging(true);
|
|
883
|
+
}
|
|
692
884
|
};
|
|
693
|
-
|
|
885
|
+
|
|
886
|
+
const onDragOver = (e: DragEvent) => {
|
|
694
887
|
if (e?.dataTransfer?.types?.includes('Files')) {
|
|
695
888
|
e.preventDefault();
|
|
889
|
+
e.stopPropagation();
|
|
890
|
+
// Keep dragging state true while over document
|
|
696
891
|
setIsDragging(true);
|
|
697
892
|
}
|
|
698
893
|
};
|
|
699
|
-
|
|
894
|
+
|
|
895
|
+
const onDrop = async (e: DragEvent) => {
|
|
896
|
+
dragCounter = 0;
|
|
897
|
+
setIsDragging(false);
|
|
898
|
+
|
|
700
899
|
if (e?.dataTransfer?.files?.length) {
|
|
701
900
|
e.preventDefault();
|
|
702
|
-
|
|
901
|
+
e.stopPropagation();
|
|
902
|
+
|
|
903
|
+
try {
|
|
904
|
+
const files = Array.from(e.dataTransfer.files) as File[];
|
|
905
|
+
if (files.length > 0) {
|
|
906
|
+
await handleFileSelection(files);
|
|
907
|
+
}
|
|
908
|
+
} catch (error: any) {
|
|
909
|
+
toast.error(error.message || 'Failed to upload files');
|
|
910
|
+
}
|
|
703
911
|
}
|
|
704
912
|
};
|
|
705
|
-
|
|
706
|
-
|
|
913
|
+
|
|
914
|
+
const onDragLeave = (e: DragEvent) => {
|
|
915
|
+
dragCounter--;
|
|
916
|
+
// Only hide drag overlay if we're actually leaving the document (drag counter reaches 0)
|
|
917
|
+
if (dragCounter === 0) {
|
|
918
|
+
setIsDragging(false);
|
|
919
|
+
}
|
|
707
920
|
};
|
|
708
|
-
|
|
709
|
-
document
|
|
710
|
-
document.addEventListener('
|
|
711
|
-
document.addEventListener('
|
|
921
|
+
|
|
922
|
+
// Attach to document for global drag detection
|
|
923
|
+
document.addEventListener('dragenter', onDragEnter, false);
|
|
924
|
+
document.addEventListener('dragover', onDragOver, false);
|
|
925
|
+
document.addEventListener('drop', onDrop, false);
|
|
926
|
+
document.addEventListener('dragleave', onDragLeave, false);
|
|
927
|
+
|
|
712
928
|
return () => {
|
|
713
|
-
document.removeEventListener('dragenter',
|
|
714
|
-
document.removeEventListener('dragover',
|
|
715
|
-
document.removeEventListener('drop',
|
|
716
|
-
document.removeEventListener('dragleave',
|
|
929
|
+
document.removeEventListener('dragenter', onDragEnter, false);
|
|
930
|
+
document.removeEventListener('dragover', onDragOver, false);
|
|
931
|
+
document.removeEventListener('drop', onDrop, false);
|
|
932
|
+
document.removeEventListener('dragleave', onDragLeave, false);
|
|
717
933
|
};
|
|
718
|
-
}, [user?.id, targetUserId]);
|
|
934
|
+
}, [user?.id, targetUserId, handleFileSelection]);
|
|
719
935
|
|
|
720
|
-
const handleDrop = async (e: any) => {
|
|
721
|
-
if (Platform.OS === 'web' && user?.id === targetUserId) {
|
|
722
|
-
e.preventDefault();
|
|
723
|
-
setIsDragging(false);
|
|
724
|
-
uploadStartRef.current = Date.now();
|
|
725
|
-
storeSetUploading(true);
|
|
726
|
-
|
|
727
|
-
try {
|
|
728
|
-
const files = Array.from(e.dataTransfer.files) as File[];
|
|
729
|
-
if (files.length > 0) storeSetUploadProgress({ current: 0, total: files.length });
|
|
730
|
-
await processFileUploads(files);
|
|
731
|
-
} catch (error: any) {
|
|
732
|
-
toast.error(error.message || 'Failed to upload files');
|
|
733
|
-
} finally {
|
|
734
|
-
endUpload();
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
};
|
|
738
936
|
|
|
739
937
|
const handleFileDownload = async (fileId: string, filename: string) => {
|
|
740
938
|
try {
|
|
741
939
|
if (Platform.OS === 'web') {
|
|
742
|
-
console.log('Downloading file:', { fileId, filename });
|
|
743
|
-
|
|
744
940
|
// Use the public download URL method
|
|
745
941
|
const downloadUrl = oxyServices.getFileDownloadUrl(fileId);
|
|
746
|
-
console.log('Download URL:', downloadUrl);
|
|
747
942
|
|
|
748
943
|
try {
|
|
749
944
|
// Method 1: Try simple link download first
|
|
@@ -757,7 +952,6 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
757
952
|
|
|
758
953
|
toast.success('File download started');
|
|
759
954
|
} catch (linkError) {
|
|
760
|
-
console.warn('Link download failed, trying fetch method:', linkError);
|
|
761
955
|
|
|
762
956
|
// Method 2: Fallback to authenticated download
|
|
763
957
|
const blob = await oxyServices.getFileContentAsBlob(fileId);
|
|
@@ -779,7 +973,6 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
779
973
|
toast.info('File download not implemented for mobile yet');
|
|
780
974
|
}
|
|
781
975
|
} catch (error: any) {
|
|
782
|
-
console.error('Download error:', error);
|
|
783
976
|
toast.error(error.message || 'Failed to download file');
|
|
784
977
|
}
|
|
785
978
|
};
|
|
@@ -837,7 +1030,6 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
837
1030
|
setFileContent(content);
|
|
838
1031
|
}
|
|
839
1032
|
} catch (error: any) {
|
|
840
|
-
console.error('Failed to load file content:', error);
|
|
841
1033
|
if (error.message?.includes('404') || error.message?.includes('not found')) {
|
|
842
1034
|
toast.error('File not found. It may have been deleted.');
|
|
843
1035
|
} else {
|
|
@@ -850,7 +1042,6 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
850
1042
|
setFileContent(null);
|
|
851
1043
|
}
|
|
852
1044
|
} catch (error: any) {
|
|
853
|
-
console.error('Failed to open file:', error);
|
|
854
1045
|
toast.error(error.message || 'Failed to open file');
|
|
855
1046
|
} finally {
|
|
856
1047
|
setLoadingFileContent(false);
|
|
@@ -905,8 +1096,8 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
905
1096
|
contentFit="cover"
|
|
906
1097
|
transition={120}
|
|
907
1098
|
cachePolicy="memory-disk"
|
|
908
|
-
onError={(
|
|
909
|
-
|
|
1099
|
+
onError={() => {
|
|
1100
|
+
// Photo failed to load, will show placeholder
|
|
910
1101
|
}}
|
|
911
1102
|
accessibilityLabel={photo.filename}
|
|
912
1103
|
/>
|
|
@@ -945,8 +1136,8 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
945
1136
|
contentFit="cover"
|
|
946
1137
|
transition={120}
|
|
947
1138
|
cachePolicy="memory-disk"
|
|
948
|
-
onError={(
|
|
949
|
-
|
|
1139
|
+
onError={() => {
|
|
1140
|
+
// Photo failed to load, will show placeholder
|
|
950
1141
|
}}
|
|
951
1142
|
accessibilityLabel={photo.filename}
|
|
952
1143
|
/>
|
|
@@ -1005,8 +1196,8 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
1005
1196
|
contentFit="cover"
|
|
1006
1197
|
transition={120}
|
|
1007
1198
|
cachePolicy="memory-disk"
|
|
1008
|
-
onError={(
|
|
1009
|
-
|
|
1199
|
+
onError={() => {
|
|
1200
|
+
// Image preview failed to load
|
|
1010
1201
|
}}
|
|
1011
1202
|
accessibilityLabel={file.filename}
|
|
1012
1203
|
/>
|
|
@@ -1129,10 +1320,15 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
1129
1320
|
|
|
1130
1321
|
// GroupedSection-based file items (for 'all' view) replacing legacy flat list look
|
|
1131
1322
|
const groupedFileItems = useMemo(() => {
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1323
|
+
// filteredFiles is already sorted, so just use it directly
|
|
1324
|
+
const sortedFiles = filteredFiles;
|
|
1325
|
+
|
|
1326
|
+
// Store file positions for scrolling
|
|
1327
|
+
sortedFiles.forEach((file, index) => {
|
|
1328
|
+
itemRefs.current.set(file.id, index);
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
return sortedFiles.map((file) => {
|
|
1136
1332
|
const isImage = file.contentType.startsWith('image/');
|
|
1137
1333
|
const isVideo = file.contentType.startsWith('video/');
|
|
1138
1334
|
const hasPreview = isImage || isVideo;
|
|
@@ -1147,13 +1343,29 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
1147
1343
|
title: file.filename,
|
|
1148
1344
|
subtitle: `${formatFileSize(file.length)} • ${new Date(file.uploadDate).toLocaleDateString()}`,
|
|
1149
1345
|
theme: theme as 'light' | 'dark',
|
|
1150
|
-
onPress: () =>
|
|
1346
|
+
onPress: () => {
|
|
1347
|
+
// Support selection in regular mode with long press or if already selecting
|
|
1348
|
+
if (!selectMode && selectedIds.size > 0) {
|
|
1349
|
+
// If already in selection mode (some files selected), toggle selection
|
|
1350
|
+
toggleSelect(file);
|
|
1351
|
+
} else {
|
|
1352
|
+
handleFileOpen(file);
|
|
1353
|
+
}
|
|
1354
|
+
},
|
|
1355
|
+
onLongPress: !selectMode ? () => {
|
|
1356
|
+
// Enable selection mode on long press
|
|
1357
|
+
if (selectedIds.size === 0) {
|
|
1358
|
+
setSelectedIds(new Set([file.id]));
|
|
1359
|
+
} else {
|
|
1360
|
+
toggleSelect(file);
|
|
1361
|
+
}
|
|
1362
|
+
} : undefined,
|
|
1151
1363
|
showChevron: false,
|
|
1152
1364
|
dense: true,
|
|
1153
1365
|
multiRow: !!file.metadata?.description,
|
|
1154
|
-
selected: selectMode && isSelected,
|
|
1155
|
-
// Hide action buttons when selecting
|
|
1156
|
-
customContent: !selectMode ? (
|
|
1366
|
+
selected: (selectMode || selectedIds.size > 0) && isSelected,
|
|
1367
|
+
// Hide action buttons when selecting (in selectMode or bulk operations mode)
|
|
1368
|
+
customContent: (!selectMode && selectedIds.size === 0) ? (
|
|
1157
1369
|
<View style={styles.groupedActions}>
|
|
1158
1370
|
{(isImage || isVideo || file.contentType.includes('pdf')) && (
|
|
1159
1371
|
<TouchableOpacity
|
|
@@ -1191,6 +1403,92 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
1191
1403
|
});
|
|
1192
1404
|
}, [filteredFiles, theme, themeStyles, deleting, handleFileDownload, handleFileDelete, handleFileOpen, getSafeDownloadUrl, selectMode, selectedIds]);
|
|
1193
1405
|
|
|
1406
|
+
// Scroll to selected file after selection
|
|
1407
|
+
useEffect(() => {
|
|
1408
|
+
if (lastSelectedFileId && selectMode) {
|
|
1409
|
+
if (viewMode === 'all' && scrollViewRef.current) {
|
|
1410
|
+
// Find the index of the selected file
|
|
1411
|
+
const itemIndex = itemRefs.current.get(lastSelectedFileId);
|
|
1412
|
+
|
|
1413
|
+
if (itemIndex !== undefined && itemIndex >= 0) {
|
|
1414
|
+
// Estimate item height (GroupedItem with dense mode is approximately 60-70px)
|
|
1415
|
+
// Account for description rows which add extra height
|
|
1416
|
+
const baseItemHeight = 65;
|
|
1417
|
+
const descriptionHeight = 30; // Approximate height for description
|
|
1418
|
+
// Use filteredFiles which is already sorted according to user's selection
|
|
1419
|
+
const sortedFiles = filteredFiles;
|
|
1420
|
+
|
|
1421
|
+
// Calculate total height up to this item
|
|
1422
|
+
let scrollPosition = 0;
|
|
1423
|
+
for (let i = 0; i <= itemIndex && i < sortedFiles.length; i++) {
|
|
1424
|
+
const file = sortedFiles[i];
|
|
1425
|
+
scrollPosition += baseItemHeight;
|
|
1426
|
+
if (file.metadata?.description) {
|
|
1427
|
+
scrollPosition += descriptionHeight;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// Add header, controls, search, and stats height (approximately 250px)
|
|
1432
|
+
const headerHeight = 250;
|
|
1433
|
+
const finalScrollPosition = headerHeight + scrollPosition - 150; // Offset to show item near top
|
|
1434
|
+
|
|
1435
|
+
// Use requestAnimationFrame to ensure DOM is updated before scrolling
|
|
1436
|
+
requestAnimationFrame(() => {
|
|
1437
|
+
requestAnimationFrame(() => {
|
|
1438
|
+
scrollViewRef.current?.scrollTo({
|
|
1439
|
+
y: Math.max(0, finalScrollPosition),
|
|
1440
|
+
animated: true,
|
|
1441
|
+
});
|
|
1442
|
+
});
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
} else if (viewMode === 'photos' && photoScrollViewRef.current) {
|
|
1446
|
+
// For photo grid, find the photo index
|
|
1447
|
+
const photos = filteredFiles.filter(file => file.contentType.startsWith('image/'));
|
|
1448
|
+
const photoIndex = photos.findIndex(p => p.id === lastSelectedFileId);
|
|
1449
|
+
|
|
1450
|
+
if (photoIndex >= 0) {
|
|
1451
|
+
// Estimate photo item height based on grid layout
|
|
1452
|
+
// Calculate items per row
|
|
1453
|
+
let itemsPerRow = 3;
|
|
1454
|
+
if (containerWidth > 768) itemsPerRow = 6;
|
|
1455
|
+
else if (containerWidth > 480) itemsPerRow = 4;
|
|
1456
|
+
|
|
1457
|
+
const scrollContainerPadding = 32;
|
|
1458
|
+
const gaps = (itemsPerRow - 1) * 4;
|
|
1459
|
+
const availableWidth = containerWidth - scrollContainerPadding;
|
|
1460
|
+
const itemWidth = (availableWidth - gaps) / itemsPerRow;
|
|
1461
|
+
|
|
1462
|
+
// Calculate row and approximate scroll position
|
|
1463
|
+
const row = Math.floor(photoIndex / itemsPerRow);
|
|
1464
|
+
const headerHeight = 250;
|
|
1465
|
+
const finalScrollPosition = headerHeight + (row * (itemWidth + 4)) - 150;
|
|
1466
|
+
|
|
1467
|
+
// Use requestAnimationFrame to ensure DOM is updated before scrolling
|
|
1468
|
+
requestAnimationFrame(() => {
|
|
1469
|
+
requestAnimationFrame(() => {
|
|
1470
|
+
photoScrollViewRef.current?.scrollTo({
|
|
1471
|
+
y: Math.max(0, finalScrollPosition),
|
|
1472
|
+
animated: true,
|
|
1473
|
+
});
|
|
1474
|
+
});
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
}, [lastSelectedFileId, selectMode, viewMode, filteredFiles, containerWidth]);
|
|
1480
|
+
|
|
1481
|
+
// Clear selected file ID after scroll animation completes
|
|
1482
|
+
useEffect(() => {
|
|
1483
|
+
if (lastSelectedFileId && scrollViewRef.current) {
|
|
1484
|
+
const timeoutId = setTimeout(() => {
|
|
1485
|
+
setLastSelectedFileId(null);
|
|
1486
|
+
}, 600); // Allow time for scroll animation to complete
|
|
1487
|
+
|
|
1488
|
+
return () => clearTimeout(timeoutId);
|
|
1489
|
+
}
|
|
1490
|
+
}, [lastSelectedFileId]);
|
|
1491
|
+
|
|
1194
1492
|
const renderPhotoItem = (photo: FileMetadata, index: number) => {
|
|
1195
1493
|
const downloadUrl = getSafeDownloadUrl(photo, 'thumb');
|
|
1196
1494
|
|
|
@@ -1225,8 +1523,8 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
1225
1523
|
contentFit="cover"
|
|
1226
1524
|
transition={120}
|
|
1227
1525
|
cachePolicy="memory-disk"
|
|
1228
|
-
onError={(
|
|
1229
|
-
|
|
1526
|
+
onError={() => {
|
|
1527
|
+
// Image preview failed to load
|
|
1230
1528
|
}}
|
|
1231
1529
|
accessibilityLabel={photo.filename}
|
|
1232
1530
|
/>
|
|
@@ -1270,6 +1568,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
1270
1568
|
|
|
1271
1569
|
return (
|
|
1272
1570
|
<ScrollView
|
|
1571
|
+
ref={photoScrollViewRef}
|
|
1273
1572
|
style={styles.scrollView}
|
|
1274
1573
|
contentContainerStyle={styles.photoScrollContainer}
|
|
1275
1574
|
refreshControl={
|
|
@@ -1621,8 +1920,8 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
1621
1920
|
contentFit="contain"
|
|
1622
1921
|
transition={120}
|
|
1623
1922
|
cachePolicy="memory-disk"
|
|
1624
|
-
onError={(
|
|
1625
|
-
|
|
1923
|
+
onError={() => {
|
|
1924
|
+
// Image failed to load
|
|
1626
1925
|
}}
|
|
1627
1926
|
accessibilityLabel={openedFile.filename}
|
|
1628
1927
|
/>
|
|
@@ -1741,13 +2040,190 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
1741
2040
|
</View>
|
|
1742
2041
|
);
|
|
1743
2042
|
|
|
1744
|
-
|
|
2043
|
+
// Professional Skeleton Loading Component with Advanced Shimmer Effect
|
|
2044
|
+
const SkeletonLoader = React.memo(() => {
|
|
2045
|
+
const shimmerAnim = useRef(new Animated.Value(0)).current;
|
|
2046
|
+
const skeletonContainerWidth = containerWidth || 400;
|
|
2047
|
+
|
|
2048
|
+
useEffect(() => {
|
|
2049
|
+
const shimmer = Animated.loop(
|
|
2050
|
+
Animated.timing(shimmerAnim, {
|
|
2051
|
+
toValue: 1,
|
|
2052
|
+
duration: 2000,
|
|
2053
|
+
easing: Easing.linear,
|
|
2054
|
+
useNativeDriver: true,
|
|
2055
|
+
})
|
|
2056
|
+
);
|
|
2057
|
+
shimmer.start();
|
|
2058
|
+
return () => shimmer.stop();
|
|
2059
|
+
}, [shimmerAnim]);
|
|
2060
|
+
|
|
2061
|
+
// Create a sweeping shimmer effect
|
|
2062
|
+
const shimmerTranslateX = shimmerAnim.interpolate({
|
|
2063
|
+
inputRange: [0, 1],
|
|
2064
|
+
outputRange: [-skeletonContainerWidth * 2, skeletonContainerWidth * 2],
|
|
2065
|
+
});
|
|
2066
|
+
|
|
2067
|
+
const SkeletonBox = ({ width, height, borderRadius = 8, style, delay = 0 }: { width: number | string; height: number; borderRadius?: number; style?: any; delay?: number }) => {
|
|
2068
|
+
const delayedTranslateX = shimmerAnim.interpolate({
|
|
2069
|
+
inputRange: [0, 1],
|
|
2070
|
+
outputRange: [-skeletonContainerWidth * 2 + delay, skeletonContainerWidth * 2 + delay],
|
|
2071
|
+
});
|
|
2072
|
+
|
|
2073
|
+
return (
|
|
2074
|
+
<View
|
|
2075
|
+
style={[
|
|
2076
|
+
{
|
|
2077
|
+
width,
|
|
2078
|
+
height,
|
|
2079
|
+
borderRadius,
|
|
2080
|
+
backgroundColor: themeStyles.isDarkTheme ? '#1E1E1E' : '#F5F5F5',
|
|
2081
|
+
overflow: 'hidden',
|
|
2082
|
+
position: 'relative',
|
|
2083
|
+
},
|
|
2084
|
+
style,
|
|
2085
|
+
]}
|
|
2086
|
+
>
|
|
2087
|
+
{/* Base background */}
|
|
2088
|
+
<View
|
|
2089
|
+
style={{
|
|
2090
|
+
position: 'absolute',
|
|
2091
|
+
top: 0,
|
|
2092
|
+
left: 0,
|
|
2093
|
+
right: 0,
|
|
2094
|
+
bottom: 0,
|
|
2095
|
+
backgroundColor: themeStyles.isDarkTheme ? '#1E1E1E' : '#F5F5F5',
|
|
2096
|
+
}}
|
|
2097
|
+
/>
|
|
2098
|
+
{/* Shimmer gradient effect */}
|
|
2099
|
+
<Animated.View
|
|
2100
|
+
style={{
|
|
2101
|
+
position: 'absolute',
|
|
2102
|
+
top: 0,
|
|
2103
|
+
left: 0,
|
|
2104
|
+
width: '100%',
|
|
2105
|
+
height: '100%',
|
|
2106
|
+
transform: [{ translateX: delayedTranslateX }],
|
|
2107
|
+
}}
|
|
2108
|
+
>
|
|
2109
|
+
<View
|
|
2110
|
+
style={{
|
|
2111
|
+
width: skeletonContainerWidth,
|
|
2112
|
+
height: '100%',
|
|
2113
|
+
backgroundColor: themeStyles.isDarkTheme
|
|
2114
|
+
? 'rgba(255, 255, 255, 0.08)'
|
|
2115
|
+
: 'rgba(255, 255, 255, 0.8)',
|
|
2116
|
+
shadowColor: themeStyles.isDarkTheme ? '#000' : '#FFF',
|
|
2117
|
+
shadowOffset: { width: 0, height: 0 },
|
|
2118
|
+
shadowOpacity: 0.3,
|
|
2119
|
+
shadowRadius: 10,
|
|
2120
|
+
}}
|
|
2121
|
+
/>
|
|
2122
|
+
</Animated.View>
|
|
2123
|
+
</View>
|
|
2124
|
+
);
|
|
2125
|
+
};
|
|
2126
|
+
|
|
2127
|
+
// Skeleton file item matching GroupedSection structure
|
|
2128
|
+
const SkeletonFileItem = ({ index }: { index: number }) => (
|
|
2129
|
+
<View
|
|
2130
|
+
style={[
|
|
2131
|
+
{
|
|
2132
|
+
flexDirection: 'row',
|
|
2133
|
+
alignItems: 'center',
|
|
2134
|
+
paddingHorizontal: 16,
|
|
2135
|
+
paddingVertical: 12,
|
|
2136
|
+
backgroundColor: themeStyles.isDarkTheme ? '#121212' : '#FFFFFF',
|
|
2137
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
2138
|
+
borderBottomColor: themeStyles.borderColor,
|
|
2139
|
+
},
|
|
2140
|
+
]}
|
|
2141
|
+
>
|
|
2142
|
+
{/* Icon/Image skeleton */}
|
|
2143
|
+
<SkeletonBox width={44} height={44} borderRadius={8} delay={index * 50} />
|
|
2144
|
+
|
|
2145
|
+
{/* Content skeleton */}
|
|
2146
|
+
<View style={{ flex: 1, marginLeft: 12, justifyContent: 'center' }}>
|
|
2147
|
+
<SkeletonBox
|
|
2148
|
+
width={index % 3 === 0 ? '85%' : index % 3 === 1 ? '70%' : '90%'}
|
|
2149
|
+
height={16}
|
|
2150
|
+
style={{ marginBottom: 8 }}
|
|
2151
|
+
delay={index * 50 + 20}
|
|
2152
|
+
/>
|
|
2153
|
+
<SkeletonBox
|
|
2154
|
+
width={index % 2 === 0 ? '50%' : '60%'}
|
|
2155
|
+
height={12}
|
|
2156
|
+
delay={index * 50 + 40}
|
|
2157
|
+
/>
|
|
2158
|
+
</View>
|
|
2159
|
+
</View>
|
|
2160
|
+
);
|
|
2161
|
+
|
|
1745
2162
|
return (
|
|
1746
|
-
<View style={[styles.container,
|
|
1747
|
-
|
|
1748
|
-
<
|
|
2163
|
+
<View style={[styles.container, { backgroundColor }]}>
|
|
2164
|
+
{/* Header Skeleton */}
|
|
2165
|
+
<View style={[styles.header, { borderBottomColor: themeStyles.borderColor, borderBottomWidth: StyleSheet.hairlineWidth }]}>
|
|
2166
|
+
<SkeletonBox width={44} height={44} borderRadius={12} />
|
|
2167
|
+
<View style={[styles.headerTitleContainer, { flex: 1 }]}>
|
|
2168
|
+
<SkeletonBox width={140} height={20} style={{ marginBottom: 6 }} />
|
|
2169
|
+
<SkeletonBox width={100} height={14} />
|
|
2170
|
+
</View>
|
|
2171
|
+
<SkeletonBox width={44} height={44} borderRadius={12} />
|
|
2172
|
+
</View>
|
|
2173
|
+
|
|
2174
|
+
{/* Controls Bar Skeleton */}
|
|
2175
|
+
<View style={styles.controlsBar}>
|
|
2176
|
+
<SkeletonBox width={100} height={36} borderRadius={18} />
|
|
2177
|
+
<SkeletonBox width={44} height={44} borderRadius={22} />
|
|
2178
|
+
</View>
|
|
2179
|
+
|
|
2180
|
+
{/* Search Bar Skeleton */}
|
|
2181
|
+
<View style={[styles.searchContainer, {
|
|
2182
|
+
backgroundColor: themeStyles.isDarkTheme ? '#1A1A1A' : '#FFFFFF',
|
|
2183
|
+
borderColor: themeStyles.borderColor,
|
|
2184
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
2185
|
+
}]}>
|
|
2186
|
+
<SkeletonBox width="100%" height={44} borderRadius={12} />
|
|
2187
|
+
</View>
|
|
2188
|
+
|
|
2189
|
+
{/* Stats Container Skeleton */}
|
|
2190
|
+
<View style={[styles.statsContainer, {
|
|
2191
|
+
backgroundColor: themeStyles.isDarkTheme ? '#1A1A1A' : '#FFFFFF',
|
|
2192
|
+
borderColor: themeStyles.borderColor,
|
|
2193
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
2194
|
+
}]}>
|
|
2195
|
+
{[1, 2, 3].map((i) => (
|
|
2196
|
+
<View key={i} style={styles.statItem}>
|
|
2197
|
+
<SkeletonBox width={50} height={20} style={{ marginBottom: 4 }} delay={i * 30} />
|
|
2198
|
+
<SkeletonBox width={40} height={14} delay={i * 30 + 15} />
|
|
2199
|
+
</View>
|
|
2200
|
+
))}
|
|
2201
|
+
</View>
|
|
2202
|
+
|
|
2203
|
+
{/* File List Skeleton - Matching GroupedSection */}
|
|
2204
|
+
<ScrollView
|
|
2205
|
+
style={styles.scrollView}
|
|
2206
|
+
contentContainerStyle={styles.scrollContainer}
|
|
2207
|
+
showsVerticalScrollIndicator={false}
|
|
2208
|
+
>
|
|
2209
|
+
<View style={{
|
|
2210
|
+
backgroundColor: themeStyles.isDarkTheme ? '#121212' : '#FFFFFF',
|
|
2211
|
+
borderRadius: 12,
|
|
2212
|
+
overflow: 'hidden',
|
|
2213
|
+
marginHorizontal: 16,
|
|
2214
|
+
marginTop: 8,
|
|
2215
|
+
}}>
|
|
2216
|
+
{[1, 2, 3, 4, 5, 6, 7, 8].map((i) => (
|
|
2217
|
+
<SkeletonFileItem key={i} index={i} />
|
|
2218
|
+
))}
|
|
2219
|
+
</View>
|
|
2220
|
+
</ScrollView>
|
|
1749
2221
|
</View>
|
|
1750
2222
|
);
|
|
2223
|
+
});
|
|
2224
|
+
|
|
2225
|
+
if (loading) {
|
|
2226
|
+
return <SkeletonLoader />;
|
|
1751
2227
|
}
|
|
1752
2228
|
|
|
1753
2229
|
// If a file is opened, show the file viewer
|
|
@@ -1762,16 +2238,11 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
1762
2238
|
|
|
1763
2239
|
return (
|
|
1764
2240
|
<View
|
|
2241
|
+
ref={containerRef}
|
|
1765
2242
|
style={[
|
|
1766
2243
|
styles.container,
|
|
1767
2244
|
isDragging && Platform.OS === 'web' && styles.dragOverlay
|
|
1768
2245
|
]}
|
|
1769
|
-
{...(Platform.OS === 'web' && user?.id === targetUserId ? {
|
|
1770
|
-
onDragOver: handleDragOver,
|
|
1771
|
-
onDragEnter: handleDragEnter,
|
|
1772
|
-
onDragLeave: handleDragLeave,
|
|
1773
|
-
onDrop: handleDrop,
|
|
1774
|
-
} : {})}
|
|
1775
2246
|
>
|
|
1776
2247
|
<Header
|
|
1777
2248
|
title={selectMode ? (multiSelect ? `${selectedIds.size}${maxSelection ? '/' + maxSelection : ''} Selected` : 'Select a File') : (viewMode === 'photos' ? 'Photos' : 'File Management')}
|
|
@@ -1789,6 +2260,36 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
1789
2260
|
onPress: confirmMultiSelection,
|
|
1790
2261
|
disabled: selectedIds.size === 0,
|
|
1791
2262
|
}
|
|
2263
|
+
] : !selectMode && selectedIds.size > 0 ? [
|
|
2264
|
+
{
|
|
2265
|
+
key: 'clear',
|
|
2266
|
+
text: 'Clear',
|
|
2267
|
+
onPress: () => setSelectedIds(new Set()),
|
|
2268
|
+
},
|
|
2269
|
+
{
|
|
2270
|
+
key: 'delete',
|
|
2271
|
+
text: `Delete (${selectedIds.size})`,
|
|
2272
|
+
onPress: handleBulkDelete,
|
|
2273
|
+
icon: 'trash',
|
|
2274
|
+
},
|
|
2275
|
+
{
|
|
2276
|
+
key: 'visibility',
|
|
2277
|
+
text: 'Visibility',
|
|
2278
|
+
onPress: () => {
|
|
2279
|
+
// Show visibility options menu
|
|
2280
|
+
Alert.alert(
|
|
2281
|
+
'Change Visibility',
|
|
2282
|
+
`Change visibility for ${selectedIds.size} file(s)?`,
|
|
2283
|
+
[
|
|
2284
|
+
{ text: 'Cancel', style: 'cancel' },
|
|
2285
|
+
{ text: 'Private', onPress: () => handleBulkVisibilityChange('private') },
|
|
2286
|
+
{ text: 'Public', onPress: () => handleBulkVisibilityChange('public') },
|
|
2287
|
+
{ text: 'Unlisted', onPress: () => handleBulkVisibilityChange('unlisted') },
|
|
2288
|
+
]
|
|
2289
|
+
);
|
|
2290
|
+
},
|
|
2291
|
+
icon: 'eye',
|
|
2292
|
+
}
|
|
1792
2293
|
] : undefined}
|
|
1793
2294
|
onBack={onClose || goBack}
|
|
1794
2295
|
theme={theme}
|
|
@@ -1799,41 +2300,119 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
1799
2300
|
/>
|
|
1800
2301
|
|
|
1801
2302
|
<View style={styles.controlsBar}>
|
|
1802
|
-
<
|
|
1803
|
-
|
|
1804
|
-
{
|
|
2303
|
+
<ScrollView
|
|
2304
|
+
horizontal
|
|
2305
|
+
showsHorizontalScrollIndicator={false}
|
|
2306
|
+
style={styles.viewModeScroll}
|
|
2307
|
+
>
|
|
2308
|
+
<View style={[
|
|
2309
|
+
styles.viewModeToggle,
|
|
2310
|
+
{
|
|
2311
|
+
backgroundColor: themeStyles.isDarkTheme ? '#181818' : '#FFFFFF',
|
|
2312
|
+
borderWidth: 1,
|
|
2313
|
+
borderColor: themeStyles.isDarkTheme ? '#2A2A2A' : '#E8E9EA',
|
|
2314
|
+
}
|
|
2315
|
+
]}>
|
|
2316
|
+
<TouchableOpacity
|
|
2317
|
+
style={[
|
|
2318
|
+
styles.viewModeButton,
|
|
2319
|
+
viewMode === 'all' && { backgroundColor: themeStyles.primaryColor }
|
|
2320
|
+
]}
|
|
2321
|
+
onPress={() => setViewMode('all')}
|
|
2322
|
+
>
|
|
2323
|
+
<Ionicons
|
|
2324
|
+
name="folder"
|
|
2325
|
+
size={18}
|
|
2326
|
+
color={viewMode === 'all' ? '#FFFFFF' : themeStyles.textColor}
|
|
2327
|
+
/>
|
|
2328
|
+
</TouchableOpacity>
|
|
2329
|
+
<TouchableOpacity
|
|
2330
|
+
style={[
|
|
2331
|
+
styles.viewModeButton,
|
|
2332
|
+
viewMode === 'photos' && { backgroundColor: themeStyles.primaryColor }
|
|
2333
|
+
]}
|
|
2334
|
+
onPress={() => setViewMode('photos')}
|
|
2335
|
+
>
|
|
2336
|
+
<Ionicons
|
|
2337
|
+
name="images"
|
|
2338
|
+
size={18}
|
|
2339
|
+
color={viewMode === 'photos' ? '#FFFFFF' : themeStyles.textColor}
|
|
2340
|
+
/>
|
|
2341
|
+
</TouchableOpacity>
|
|
2342
|
+
<TouchableOpacity
|
|
2343
|
+
style={[
|
|
2344
|
+
styles.viewModeButton,
|
|
2345
|
+
viewMode === 'videos' && { backgroundColor: themeStyles.primaryColor }
|
|
2346
|
+
]}
|
|
2347
|
+
onPress={() => setViewMode('videos')}
|
|
2348
|
+
>
|
|
2349
|
+
<Ionicons
|
|
2350
|
+
name="videocam"
|
|
2351
|
+
size={18}
|
|
2352
|
+
color={viewMode === 'videos' ? '#FFFFFF' : themeStyles.textColor}
|
|
2353
|
+
/>
|
|
2354
|
+
</TouchableOpacity>
|
|
2355
|
+
<TouchableOpacity
|
|
2356
|
+
style={[
|
|
2357
|
+
styles.viewModeButton,
|
|
2358
|
+
viewMode === 'documents' && { backgroundColor: themeStyles.primaryColor }
|
|
2359
|
+
]}
|
|
2360
|
+
onPress={() => setViewMode('documents')}
|
|
2361
|
+
>
|
|
2362
|
+
<Ionicons
|
|
2363
|
+
name="document-text"
|
|
2364
|
+
size={18}
|
|
2365
|
+
color={viewMode === 'documents' ? '#FFFFFF' : themeStyles.textColor}
|
|
2366
|
+
/>
|
|
2367
|
+
</TouchableOpacity>
|
|
2368
|
+
<TouchableOpacity
|
|
2369
|
+
style={[
|
|
2370
|
+
styles.viewModeButton,
|
|
2371
|
+
viewMode === 'audio' && { backgroundColor: themeStyles.primaryColor }
|
|
2372
|
+
]}
|
|
2373
|
+
onPress={() => setViewMode('audio')}
|
|
2374
|
+
>
|
|
2375
|
+
<Ionicons
|
|
2376
|
+
name="musical-notes"
|
|
2377
|
+
size={18}
|
|
2378
|
+
color={viewMode === 'audio' ? '#FFFFFF' : themeStyles.textColor}
|
|
2379
|
+
/>
|
|
2380
|
+
</TouchableOpacity>
|
|
2381
|
+
</View>
|
|
2382
|
+
</ScrollView>
|
|
2383
|
+
<TouchableOpacity
|
|
2384
|
+
style={[styles.sortButton, {
|
|
1805
2385
|
backgroundColor: themeStyles.isDarkTheme ? '#181818' : '#FFFFFF',
|
|
1806
|
-
borderWidth: 1,
|
|
1807
2386
|
borderColor: themeStyles.isDarkTheme ? '#2A2A2A' : '#E8E9EA',
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
]
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
</
|
|
2387
|
+
}]}
|
|
2388
|
+
onPress={() => {
|
|
2389
|
+
// Cycle through sort options: date -> size -> name -> type -> date
|
|
2390
|
+
const sortOrder: Array<'date' | 'size' | 'name' | 'type'> = ['date', 'size', 'name', 'type'];
|
|
2391
|
+
const currentIndex = sortOrder.indexOf(sortBy);
|
|
2392
|
+
const nextIndex = (currentIndex + 1) % sortOrder.length;
|
|
2393
|
+
setSortBy(sortOrder[nextIndex]);
|
|
2394
|
+
// Toggle order when cycling back to date
|
|
2395
|
+
if (nextIndex === 0) {
|
|
2396
|
+
setSortOrder(prev => prev === 'asc' ? 'desc' : 'asc');
|
|
2397
|
+
}
|
|
2398
|
+
}}
|
|
2399
|
+
>
|
|
2400
|
+
<Ionicons
|
|
2401
|
+
name={sortOrder === 'asc' ? 'arrow-up' : 'arrow-down'}
|
|
2402
|
+
size={18}
|
|
2403
|
+
color={themeStyles.textColor}
|
|
2404
|
+
/>
|
|
2405
|
+
<Ionicons
|
|
2406
|
+
name={
|
|
2407
|
+
sortBy === 'date' ? 'calendar' :
|
|
2408
|
+
sortBy === 'size' ? 'resize' :
|
|
2409
|
+
sortBy === 'name' ? 'text' : 'document'
|
|
2410
|
+
}
|
|
2411
|
+
size={16}
|
|
2412
|
+
color={themeStyles.textColor}
|
|
2413
|
+
style={{ marginLeft: 4 }}
|
|
2414
|
+
/>
|
|
2415
|
+
</TouchableOpacity>
|
|
1837
2416
|
{user?.id === targetUserId && (!selectMode || (selectMode && allowUploadInSelectMode)) && (
|
|
1838
2417
|
<TouchableOpacity
|
|
1839
2418
|
style={[styles.uploadButton, { backgroundColor: themeStyles.primaryColor }]}
|
|
@@ -1923,6 +2502,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
1923
2502
|
renderPhotoGrid()
|
|
1924
2503
|
) : (
|
|
1925
2504
|
<ScrollView
|
|
2505
|
+
ref={scrollViewRef}
|
|
1926
2506
|
style={styles.scrollView}
|
|
1927
2507
|
contentContainerStyle={styles.scrollContainer}
|
|
1928
2508
|
refreshControl={
|
|
@@ -1990,16 +2570,21 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
1990
2570
|
{/* Selection bar removed; actions are now in header */}
|
|
1991
2571
|
{/* Global loadingMore bar removed; now inline in scroll areas */}
|
|
1992
2572
|
|
|
1993
|
-
{/* Drag and Drop Overlay */}
|
|
2573
|
+
{/* Drag and Drop Overlay - Enhanced */}
|
|
1994
2574
|
{isDragging && Platform.OS === 'web' && (
|
|
1995
2575
|
<View style={styles.dragDropOverlay}>
|
|
1996
|
-
<View style={styles.dragDropContent
|
|
1997
|
-
|
|
2576
|
+
<View style={[styles.dragDropContent, {
|
|
2577
|
+
backgroundColor: themeStyles.isDarkTheme ? 'rgba(30, 30, 30, 0.98)' : 'rgba(255, 255, 255, 0.98)',
|
|
2578
|
+
borderColor: themeStyles.primaryColor,
|
|
2579
|
+
}]}>
|
|
2580
|
+
<View style={[styles.dragDropIconContainer, { backgroundColor: `${themeStyles.primaryColor}15` }]}>
|
|
2581
|
+
<Ionicons name="cloud-upload" size={64} color={themeStyles.primaryColor} />
|
|
2582
|
+
</View>
|
|
1998
2583
|
<Text style={[styles.dragDropTitle, { color: themeStyles.primaryColor }]}>
|
|
1999
2584
|
Drop files to upload
|
|
2000
2585
|
</Text>
|
|
2001
2586
|
<Text style={[styles.dragDropSubtitle, { color: themeStyles.isDarkTheme ? '#BBBBBB' : '#666666' }]}>
|
|
2002
|
-
Release to upload
|
|
2587
|
+
Release to upload multiple files
|
|
2003
2588
|
</Text>
|
|
2004
2589
|
</View>
|
|
2005
2590
|
</View>
|
|
@@ -2499,36 +3084,151 @@ const styles = StyleSheet.create({
|
|
|
2499
3084
|
fontWeight: '600',
|
|
2500
3085
|
},
|
|
2501
3086
|
|
|
2502
|
-
// Drag and Drop styles
|
|
3087
|
+
// Drag and Drop styles - Enhanced
|
|
2503
3088
|
dragDropOverlay: {
|
|
2504
3089
|
position: 'absolute',
|
|
2505
3090
|
top: 0,
|
|
2506
3091
|
left: 0,
|
|
2507
3092
|
right: 0,
|
|
2508
3093
|
bottom: 0,
|
|
2509
|
-
backgroundColor: 'rgba(0, 0, 0, 0.
|
|
3094
|
+
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
|
2510
3095
|
justifyContent: 'center',
|
|
2511
3096
|
alignItems: 'center',
|
|
2512
3097
|
zIndex: 1000,
|
|
2513
3098
|
},
|
|
2514
3099
|
dragDropContent: {
|
|
2515
3100
|
alignItems: 'center',
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
borderWidth: 1,
|
|
2520
|
-
borderColor: '#66AFFF',
|
|
3101
|
+
padding: 32,
|
|
3102
|
+
borderRadius: 20,
|
|
3103
|
+
borderWidth: 3,
|
|
2521
3104
|
borderStyle: 'dashed',
|
|
3105
|
+
minWidth: 280,
|
|
3106
|
+
shadowColor: '#000',
|
|
3107
|
+
shadowOpacity: 0.3,
|
|
3108
|
+
shadowRadius: 20,
|
|
3109
|
+
shadowOffset: { width: 0, height: 10 },
|
|
3110
|
+
elevation: 10,
|
|
3111
|
+
},
|
|
3112
|
+
dragDropIconContainer: {
|
|
3113
|
+
padding: 24,
|
|
3114
|
+
borderRadius: 50,
|
|
3115
|
+
marginBottom: 16,
|
|
2522
3116
|
},
|
|
2523
3117
|
dragDropTitle: {
|
|
2524
|
-
fontSize:
|
|
2525
|
-
fontWeight: '
|
|
2526
|
-
|
|
2527
|
-
marginBottom:
|
|
3118
|
+
fontSize: 24,
|
|
3119
|
+
fontWeight: '700',
|
|
3120
|
+
fontFamily: fontFamilies.phuduBold,
|
|
3121
|
+
marginBottom: 8,
|
|
2528
3122
|
},
|
|
2529
3123
|
dragDropSubtitle: {
|
|
2530
3124
|
fontSize: 16,
|
|
2531
|
-
|
|
3125
|
+
fontWeight: '500',
|
|
3126
|
+
fontFamily: fontFamilies.phuduMedium,
|
|
3127
|
+
},
|
|
3128
|
+
// Upload Preview Modal styles
|
|
3129
|
+
uploadPreviewContainer: {
|
|
3130
|
+
flex: 1,
|
|
3131
|
+
},
|
|
3132
|
+
uploadPreviewHeader: {
|
|
3133
|
+
flexDirection: 'row',
|
|
3134
|
+
alignItems: 'center',
|
|
3135
|
+
justifyContent: 'space-between',
|
|
3136
|
+
paddingHorizontal: 16,
|
|
3137
|
+
paddingVertical: 16,
|
|
3138
|
+
borderBottomWidth: 1,
|
|
3139
|
+
},
|
|
3140
|
+
uploadPreviewTitle: {
|
|
3141
|
+
fontSize: 20,
|
|
3142
|
+
fontWeight: '700',
|
|
3143
|
+
fontFamily: fontFamilies.phuduBold,
|
|
3144
|
+
},
|
|
3145
|
+
uploadPreviewList: {
|
|
3146
|
+
flex: 1,
|
|
3147
|
+
padding: 16,
|
|
3148
|
+
},
|
|
3149
|
+
uploadPreviewItem: {
|
|
3150
|
+
flexDirection: 'row',
|
|
3151
|
+
alignItems: 'center',
|
|
3152
|
+
padding: 12,
|
|
3153
|
+
borderRadius: 12,
|
|
3154
|
+
borderWidth: 1,
|
|
3155
|
+
marginBottom: 12,
|
|
3156
|
+
gap: 12,
|
|
3157
|
+
},
|
|
3158
|
+
uploadPreviewThumbnail: {
|
|
3159
|
+
width: 60,
|
|
3160
|
+
height: 60,
|
|
3161
|
+
borderRadius: 8,
|
|
3162
|
+
},
|
|
3163
|
+
uploadPreviewIconContainer: {
|
|
3164
|
+
width: 60,
|
|
3165
|
+
height: 60,
|
|
3166
|
+
borderRadius: 8,
|
|
3167
|
+
alignItems: 'center',
|
|
3168
|
+
justifyContent: 'center',
|
|
3169
|
+
},
|
|
3170
|
+
uploadPreviewInfo: {
|
|
3171
|
+
flex: 1,
|
|
3172
|
+
minWidth: 0,
|
|
3173
|
+
},
|
|
3174
|
+
uploadPreviewName: {
|
|
3175
|
+
fontSize: 16,
|
|
3176
|
+
fontWeight: '600',
|
|
3177
|
+
fontFamily: fontFamilies.phuduSemiBold,
|
|
3178
|
+
marginBottom: 4,
|
|
3179
|
+
},
|
|
3180
|
+
uploadPreviewMeta: {
|
|
3181
|
+
fontSize: 13,
|
|
3182
|
+
fontFamily: fontFamilies.phudu,
|
|
3183
|
+
},
|
|
3184
|
+
uploadPreviewRemove: {
|
|
3185
|
+
padding: 4,
|
|
3186
|
+
},
|
|
3187
|
+
uploadPreviewFooter: {
|
|
3188
|
+
padding: 16,
|
|
3189
|
+
borderTopWidth: 1,
|
|
3190
|
+
},
|
|
3191
|
+
uploadPreviewStats: {
|
|
3192
|
+
flexDirection: 'row',
|
|
3193
|
+
justifyContent: 'space-between',
|
|
3194
|
+
marginBottom: 16,
|
|
3195
|
+
},
|
|
3196
|
+
uploadPreviewStatsText: {
|
|
3197
|
+
fontSize: 15,
|
|
3198
|
+
fontWeight: '600',
|
|
3199
|
+
fontFamily: fontFamilies.phuduSemiBold,
|
|
3200
|
+
},
|
|
3201
|
+
uploadPreviewActions: {
|
|
3202
|
+
flexDirection: 'row',
|
|
3203
|
+
gap: 12,
|
|
3204
|
+
},
|
|
3205
|
+
uploadPreviewCancelButton: {
|
|
3206
|
+
flex: 1,
|
|
3207
|
+
paddingVertical: 14,
|
|
3208
|
+
borderRadius: 12,
|
|
3209
|
+
borderWidth: 1,
|
|
3210
|
+
alignItems: 'center',
|
|
3211
|
+
justifyContent: 'center',
|
|
3212
|
+
},
|
|
3213
|
+
uploadPreviewCancelText: {
|
|
3214
|
+
fontSize: 16,
|
|
3215
|
+
fontWeight: '600',
|
|
3216
|
+
fontFamily: fontFamilies.phuduSemiBold,
|
|
3217
|
+
},
|
|
3218
|
+
uploadPreviewConfirmButton: {
|
|
3219
|
+
flex: 2,
|
|
3220
|
+
flexDirection: 'row',
|
|
3221
|
+
alignItems: 'center',
|
|
3222
|
+
justifyContent: 'center',
|
|
3223
|
+
paddingVertical: 14,
|
|
3224
|
+
borderRadius: 12,
|
|
3225
|
+
gap: 8,
|
|
3226
|
+
},
|
|
3227
|
+
uploadPreviewConfirmText: {
|
|
3228
|
+
color: '#FFFFFF',
|
|
3229
|
+
fontSize: 16,
|
|
3230
|
+
fontWeight: '600',
|
|
3231
|
+
fontFamily: fontFamilies.phuduSemiBold,
|
|
2532
3232
|
},
|
|
2533
3233
|
|
|
2534
3234
|
// File Viewer styles
|
|
@@ -2705,6 +3405,10 @@ const styles = StyleSheet.create({
|
|
|
2705
3405
|
paddingBottom: 4,
|
|
2706
3406
|
gap: 12,
|
|
2707
3407
|
},
|
|
3408
|
+
viewModeScroll: {
|
|
3409
|
+
flex: 1,
|
|
3410
|
+
maxWidth: '80%',
|
|
3411
|
+
},
|
|
2708
3412
|
viewModeToggle: {
|
|
2709
3413
|
flexDirection: 'row',
|
|
2710
3414
|
borderRadius: 24,
|
|
@@ -2720,6 +3424,16 @@ const styles = StyleSheet.create({
|
|
|
2720
3424
|
justifyContent: 'center',
|
|
2721
3425
|
marginHorizontal: 1,
|
|
2722
3426
|
},
|
|
3427
|
+
sortButton: {
|
|
3428
|
+
flexDirection: 'row',
|
|
3429
|
+
alignItems: 'center',
|
|
3430
|
+
justifyContent: 'center',
|
|
3431
|
+
paddingHorizontal: 12,
|
|
3432
|
+
paddingVertical: 10,
|
|
3433
|
+
borderRadius: 20,
|
|
3434
|
+
borderWidth: 1,
|
|
3435
|
+
minWidth: 44,
|
|
3436
|
+
},
|
|
2723
3437
|
|
|
2724
3438
|
// Photo Grid styles
|
|
2725
3439
|
photoScrollContainer: {
|
|
@@ -2826,6 +3540,18 @@ const styles = StyleSheet.create({
|
|
|
2826
3540
|
borderRadius: 8,
|
|
2827
3541
|
marginBottom: 4,
|
|
2828
3542
|
},
|
|
3543
|
+
skeletonFileItem: {
|
|
3544
|
+
flexDirection: 'row',
|
|
3545
|
+
alignItems: 'center',
|
|
3546
|
+
paddingHorizontal: 16,
|
|
3547
|
+
paddingVertical: 12,
|
|
3548
|
+
borderBottomWidth: 1,
|
|
3549
|
+
gap: 12,
|
|
3550
|
+
},
|
|
3551
|
+
skeletonFileInfo: {
|
|
3552
|
+
flex: 1,
|
|
3553
|
+
justifyContent: 'center',
|
|
3554
|
+
},
|
|
2829
3555
|
});
|
|
2830
3556
|
|
|
2831
3557
|
export default FileManagementScreen;
|