import { useRef, useState, useEffect } from 'react';
function FileList({ files, setFiles, dataChannels, dataChannelsRef, useFallback, socket, code, socketId, localFilesRef, downloadStates, setDownloadStates, cancelDownload, cancelRequestsRef, downloadCounts, handleDeleteFile, SERVER_URL }) {
const fileInputRef = useRef(null);
const thumbnailUrlsRef = useRef({}); // New ref to cache thumbnail URLs
const generateThumbnail = (file, maxSize = 256) => {
return new Promise((resolve) => {
if (!file.type.startsWith('image/')) {
resolve(null);
return;
}
const img = new Image();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
img.onload = () => {
const { width, height } = img;
const scale = Math.min(maxSize / width, maxSize / height, 1);
canvas.width = width * scale;
canvas.height = height * scale;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
canvas.toBlob((blob) => {
resolve(blob);
}, 'image/jpeg', 0.7); // 70% quality JPEG
};
img.src = URL.createObjectURL(file);
});
};
const handleFileSelect = async () => {
const inputFiles = fileInputRef.current.files;
if (!inputFiles || inputFiles.length === 0) return;
const fileList = Array.from(inputFiles).map(file => ({
name: file.name,
size: file.size,
peerId: socketId,
thumbnail: file.type.startsWith('image/') ? true : false,
}));
if (useFallback) {
const formData = new FormData();
Array.from(inputFiles).forEach(file => formData.append('files', file));
try {
const response = await fetch(`${SERVER_URL}/api/upload/${code}`, {
method: 'POST',
body: formData,
headers: { 'x-socket-id': socketId },
});
if (!response.ok) throw new Error('File upload failed');
console.log('Files uploaded to server:', fileList);
} catch (err) {
console.error('Error uploading files:', err);
}
} else {
// Generate thumbnails for all image files
const thumbnails = await Promise.all(
Array.from(inputFiles).map(async (file) => ({
name: file.name,
thumbnail: file.type.startsWith('image/') ? await generateThumbnail(file) : null,
}))
);
localFilesRef.current = {
...localFilesRef.current,
...Object.fromEntries(Array.from(inputFiles).map(file => [file.name, file])),
...Object.fromEntries(
thumbnails
.filter(({ thumbnail }) => thumbnail)
.map(({ name, thumbnail }) => [`${name}_thumbnail`, thumbnail])
),
};
// Create and cache thumbnail URLs once here
thumbnails.forEach(({ name, thumbnail }) => {
if (thumbnail) {
const url = URL.createObjectURL(thumbnail);
thumbnailUrlsRef.current[name] = url;
console.log(`Cached thumbnail URL for ${name}: ${url}`);
}
});
console.log(`Stored files in localFilesRef:`, Object.keys(localFilesRef.current));
// Emit file-list to all peers
socket.emit('file-list', { code, files: fileList });
console.log('Shared file list:', fileList);
}
setFiles((prev) => [...prev, ...fileList]);
fileInputRef.current.value = '';
};
const handleDownload = async (file) => {
if (cancelRequestsRef.current.has(file.name)) {
cancelRequestsRef.current.delete(file.name);
console.log(`Cleared previous cancel for ${file.name}, starting new request to ${file.peerId}`);
}
if (downloadStates[file.name]?.status === 'downloading') {
cancelDownload(file.name, file.peerId);
console.log(`Canceling download for ${file.name}`);
return;
}
setDownloadStates((prev) => ({
...prev,
[file.name]: { status: 'downloading', progress: 0, total: file.size, peerId: file.peerId },
}));
console.log(`Starting download for ${file.name}, downloadStates:`, { ...downloadStates, [file.name]: { status: 'downloading', progress: 0 }});
if (useFallback) {
socket.emit('download-start-fallback', { code, fileName: file.name });
const link = document.createElement('a');
link.href = `${SERVER_URL}/uploads/${file.path.split('/').pop()}`;
link.download = file.name;
link.click();
console.log(`Downloading file via server: ${file.name}`);
setDownloadStates((prev) => ({
...prev,
[file.name]: { status: 'completed', progress: 100 },
}));
socket.emit('download-end-fallback', { code, fileName: file.name });
} else if (file.peerId === socketId) {
const localFile = localFilesRef.current[file.name] || files.find(f => f.name === file.name)?.file;
if (localFile) {
socket.emit('download-start-fallback', { code, fileName: file.name });
const url = URL.createObjectURL(localFile);
const link = document.createElement('a');
link.href = url;
link.download = file.name;
link.click();
URL.revokeObjectURL(url);
console.log(`Downloaded local file: ${file.name}`);
setDownloadStates((prev) => ({
...prev,
[file.name]: { status: 'saved', progress: 100 },
}));
socket.emit('download-end-fallback', { code, fileName: file.name });
} else {
console.error(`Local file not found: ${file.name}`);
}
} else {
if (dataChannels[file.peerId]?.readyState === 'open') {
//socket.emit('request-file', { code, fileName: file.name, to: file.peerId });
dataChannelsRef.current[file.peerId].send(JSON.stringify({
type: 'request-file',
fileName: file.name,
}));
console.log(`Requested file ${file.name} from ${file.peerId}`);
} else {
console.error(`Data channel not open for peer ${file.peerId}`);
}
}
};
const getFileIcon = (name) => {
const ext = name.split('.').pop().toLowerCase();
const iconMap = {
exe: 'executable.png',
bin: 'executable.png',
dll: 'executable.png',
jpg: 'picture.png',
jpeg: 'picture.png',
png: 'picture.png',
gif: 'picture.png',
mp3: 'audio.png',
wav: 'audio.png',
ogg: 'audio.png',
mp4: 'video.png',
mkv: 'video.png',
avi: 'video.png',
mov: 'video.png',
wma: 'video.png',
pdf: 'unknown.png',
doc: 'unknown.png',
docx: 'unknown.png',
txt: 'text.png',
log: 'text.png',
bat: 'script.png',
sh: 'script.png',
html: 'html.png',
xml: 'xml.png',
zip: 'archive.png',
gz: 'archive.png',
bz: 'archive.png',
};
return `/imgs/${iconMap[ext] || 'unknown.png'}`;
};
const formatFileSize = (size) => {
if (!size) return 'Unknown';
if (size < 1024) return `${size} B`;
if (size < 1024 * 1024) return `${(size / 1024).toFixed(2)} KB`;
if (size < 1024 * 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(2)} MB`;
return `${(size / (1024 * 1024 * 1024)).toFixed(2)} GB`;
};
// Generate or retrieve thumbnail URL
const getThumbnailUrl = (file) => {
if (!file.thumbnail) {
console.log(`No thumbnail flag for ${file.name}`);
return null;
}
// Check if URL is already cached
if (thumbnailUrlsRef.current[file.name]) {
//console.log(`Returning cached thumbnail URL for ${file.name}: ${thumbnailUrlsRef.current[file.name]}`);
return thumbnailUrlsRef.current[file.name];
}
// Check if thumbnail exists in localFilesRef (local or remote)
const thumbnail = localFilesRef.current[`${file.name}_thumbnail`];
if (thumbnail) {
const url = URL.createObjectURL(thumbnail);
thumbnailUrlsRef.current[file.name] = url;
console.log(`Generated and cached thumbnail URL for ${file.name}: ${url}`);
return url;
}
console.log(`No thumbnail available for ${file.name}`);
return null;
};
// Cleanup: Revoke all thumbnail URLs on unmount
useEffect(() => {
return () => {
Object.values(thumbnailUrlsRef.current).forEach((url) => {
if (url) URL.revokeObjectURL(url);
});
thumbnailUrlsRef.current = {};
console.log('Revoked all thumbnail URLs on unmount');
};
}, []);
return (
<div>
<h2 className="text-xl font-semibold mb-2">Files</h2>
<input
type="file"
multiple
ref={fileInputRef}
onChange={handleFileSelect}
className="mb-2"
/>
<ul className="space-y-2">
{files.map((file, index) => {
const buttonClass =
downloadStates[file.name]?.status === 'downloading'
? 'bg-yellow-500 text-black'
: downloadStates[file.name]?.status === 'completed'
? 'bg-emerald-500 text-white'
: 'bg-purple-900 text-pink-100';
return (
<li key={index} className="flex items-center space-x-2">
{file.thumbnail && (
<img
//src={getThumbnailUrl(file) || 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'} // 1x1 transparent GIF
src={getThumbnailUrl(file) || getFileIcon(file.name)}
alt={`${file.name} preview`}
className="w-16 h-16 object-cover mr-2"
onError={() => {
console.log(`Thumbnail failed to load for ${file.name}, requesting...`);
}}
/>
) || (
<img
src={getFileIcon(file.name)}
alt="file icon"
className="w-6 h-6"
onError={(e) => (e.target.style.display = 'none')}
/>
)}
<span>({formatFileSize(file.size)})</span>
{file.peerId !== socketId && (
<button
onClick={() => handleDownload(file)}
className={`px-4 py-2 rounded ${
downloadStates[file.name]?.status === 'saved'
? 'bg-green-500 text-white'
: downloadStates[file.name]?.status === 'downloading'
? 'bg-yellow-500 text-black'
: 'bg-purple-600 text-darkPurple hover:bg-purple-500'
}`}
>
{downloadStates[file.name]?.status === 'saved'
? 'Saved ✓'
: downloadStates[file.name]?.status === 'downloading'
? `Cancel ${(downloadStates[file.name]?.progress || 0).toFixed(2)}%`
: 'Download'}
</button>
)}
{file.peerId === socketId && (
<>
<button
onClick={() => handleDeleteFile(file.name)}
className="bg-rose-800 text-darkPurple px-4 py-2 rounded hover:bg-rose-700"
>
Delete
</button>
</>
)}
{downloadCounts[file.name] > 0 && (
<span>{downloadCounts[file.name]} downloading</span>
)}
<span>{file.name}</span>
</li>
);
})}
</ul>
</div>
);
}
export default FileList;
Top