/** * @license * SPDX-License-Identifier: Apache-2.0 */ import React, { useState, useCallback, useMemo, useEffect } from 'react'; import { Grid3X3, Play, Layers, ChevronLeft, ChevronRight, X, FolderOpen, Heart, MessageCircle, Bookmark, MoreHorizontal, Loader2, Volume2, VolumeX } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; import { clsx, type ClassValue } from 'clsx'; import { twMerge } from 'tailwind-merge'; import { format, parseISO } from 'date-fns'; // @ts-ignore import { XzReadableStream } from 'xz-decompress'; // --- Utilities --- function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } // --- Types --- interface MediaFile { name: string; url: string; type: 'image' | 'video'; index: number; } interface Post { id: string; date: string; username: string; caption: string; media: MediaFile[]; thumbnail: string; isStory?: boolean; } /** * Common interface for both local File objects and remote server-side files. */ interface ArchiveFile { name: string; webkitRelativePath: string; size: number; text(): Promise; arrayBuffer(): Promise; stream(): ReadableStream; url?: string; } class LocalArchiveFile implements ArchiveFile { constructor(private file: File) {} get name() { return this.file.name; } get webkitRelativePath() { return this.file.webkitRelativePath; } get size() { return this.file.size; } text() { return this.file.text(); } arrayBuffer() { return this.file.arrayBuffer(); } stream() { return this.file.stream(); } } class RemoteArchiveFile implements ArchiveFile { constructor( public name: string, public webkitRelativePath: string, public size: number, public url: string ) {} async text() { const res = await fetch(this.url); return res.text(); } async arrayBuffer() { const res = await fetch(this.url); return res.arrayBuffer(); } stream() { const transform = new TransformStream(); fetch(this.url).then(res => { if (res.body) res.body.pipeTo(transform.writable); else transform.writable.getWriter().close(); }); return transform.readable; } } interface ServerArchive { name: string; thumbnail: string; path: string; fileCount: number; } // --- Components --- const ArchiveDashboard = ({ archives, onSelect, onLocalSelect, isScanning }: { archives: ServerArchive[]; onSelect: (archive: ServerArchive) => void; onLocalSelect: () => void; isScanning: boolean; }) => { return (

Your Archives

Select a hosted archive to browse or upload a local directory from your computer.

{/* Local Upload Card */} {/* Server Archives */} {archives.map((archive) => ( ))}
); }; const StoryViewer = ({ stories, onClose, profilePic }: { stories: Post[]; onClose: () => void; profilePic: string | null; }) => { const [currentStoryIndex, setCurrentStoryIndex] = useState(0); const [progress, setProgress] = useState(0); const [isMuted, setIsMuted] = useState(false); const videoRef = React.useRef(null); const story = stories[currentStoryIndex]; useEffect(() => { setProgress(0); let duration = 5000; // Default 5s for images const interval = 50; const updateProgress = () => { if (story.media[0].type === 'video' && videoRef.current) { const currentTime = videoRef.current.currentTime; const totalTime = videoRef.current.duration; if (totalTime) { setProgress((currentTime / totalTime) * 100); } } else { setProgress(prev => { const step = (interval / duration) * 100; if (prev >= 100) return 100; return prev + step; }); } }; const timer = setInterval(() => { updateProgress(); }, interval); return () => clearInterval(timer); }, [currentStoryIndex, story.media]); useEffect(() => { if (progress >= 100) { if (currentStoryIndex < stories.length - 1) { setCurrentStoryIndex(prev => prev + 1); } else { onClose(); } } }, [progress, currentStoryIndex, stories.length, onClose]); const nextStory = () => { if (currentStoryIndex < stories.length - 1) { setCurrentStoryIndex(prev => prev + 1); } else { onClose(); } }; const prevStory = () => { if (currentStoryIndex > 0) { setCurrentStoryIndex(prev => prev - 1); } }; return ( {/* Background Blur */}
{/* Navigation Arrows (Desktop) */} {/* Main Container */}
e.stopPropagation()} > {/* Progress Bars */}
100 ? '1px' : (stories.length > 50 ? '2px' : '4px') }} > {stories.map((_, i) => (
))}
{/* Header */}
{profilePic ? ( ) : ( {story.username[0]} )}
{story.username} {format(parseISO(story.date), 'MMM d')}
{story.media[0].type === 'video' && ( )}
{/* Media */}
{story.media[0].type === 'video' ? (
{/* Interaction Areas */}
{/* Caption Overlay */} {story.caption && (
{story.caption}
)}
); }; // --- Cache for video thumbnails to prevent redundant processing --- const thumbnailCache = new Map(); const VideoThumbnail = ({ url, className }: { url: string; className?: string }) => { const [thumbnail, setThumbnail] = useState(thumbnailCache.get(url) || null); const [isInView, setIsInView] = useState(false); const containerRef = React.useRef(null); useEffect(() => { if (thumbnail || !containerRef.current) return; const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setIsInView(true); observer.disconnect(); } }, { rootMargin: '200px' } ); observer.observe(containerRef.current); return () => observer.disconnect(); }, [thumbnail]); useEffect(() => { if (thumbnail || !isInView) return; const video = document.createElement('video'); video.src = `${url}#t=0.1`; video.preload = 'metadata'; video.muted = true; video.playsInline = true; const captureFrame = () => { try { const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; const ctx = canvas.getContext('2d'); if (ctx && video.videoWidth > 0) { ctx.drawImage(video, 0, 0, canvas.width, canvas.height); const dataUrl = canvas.toDataURL('image/jpeg', 0.6); thumbnailCache.set(url, dataUrl); setThumbnail(dataUrl); } } catch (err) { console.error('Failed to capture video frame:', err); } finally { cleanup(); } }; const handleLoadedMetadata = () => { video.currentTime = 0.1; }; const handleSeeked = () => { captureFrame(); }; const handleError = () => { cleanup(); }; const cleanup = () => { video.removeEventListener('loadedmetadata', handleLoadedMetadata); video.removeEventListener('seeked', handleSeeked); video.removeEventListener('error', handleError); video.src = ''; video.load(); }; video.addEventListener('loadedmetadata', handleLoadedMetadata); video.addEventListener('seeked', handleSeeked); video.addEventListener('error', handleError); const timeout = setTimeout(() => { if (!thumbnailCache.has(url)) { cleanup(); } }, 5000); return () => { clearTimeout(timeout); cleanup(); }; }, [url, thumbnail, isInView]); if (!thumbnail) { return (
); } return ( ); }; const MediaRenderer = ({ file, className, isFullView }: { file: MediaFile; className?: string; isFullView?: boolean }) => { const [isMuted, setIsMuted] = useState(false); const sizingClass = isFullView ? "w-full h-auto block" : "w-full h-full object-cover"; const mediaStyle = { transform: 'translateZ(0)' }; if (file.type === 'video') { return (
); } return ( ); }; const PostModal = ({ post, onClose, onNextPost, onPrevPost, hasNextPost, hasPrevPost, profilePic }: { post: Post; onClose: () => void; onNextPost?: () => void; onPrevPost?: () => void; hasNextPost?: boolean; hasPrevPost?: boolean; profilePic: string | null; }) => { const [currentIndex, setCurrentIndex] = useState(0); const [direction, setDirection] = useState(0); useEffect(() => { setCurrentIndex(0); }, [post.id]); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'ArrowRight') { if (onNextPost) { e.preventDefault(); onNextPost(); } } else if (e.key === 'ArrowLeft') { if (onPrevPost) { e.preventDefault(); onPrevPost(); } } else if (e.key === '.') { if (currentIndex < post.media.length - 1) { e.preventDefault(); paginate(1); } } else if (e.key === ',') { if (currentIndex > 0) { e.preventDefault(); paginate(-1); } } else if (e.key === 'Escape') { onClose(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [onNextPost, onPrevPost, currentIndex, post.media.length, onClose]); const paginate = (newDirection: number) => { const nextIndex = currentIndex + newDirection; if (nextIndex >= 0 && nextIndex < post.media.length) { setDirection(newDirection); setCurrentIndex(nextIndex); } }; const variants = { enter: (direction: number) => ({ x: direction > 0 ? '100%' : '-100%', }), center: { zIndex: 1, x: 0, }, exit: (direction: number) => ({ zIndex: 0, x: direction < 0 ? '100%' : '-100%', }) }; const swipeConfidenceThreshold = 15000; const interPostSwipeThreshold = 40000; const swipePower = (offset: number, velocity: number) => { return Math.abs(offset) * velocity; }; return (
{hasPrevPost && onPrevPost && ( )} {hasNextPost && onNextPost && ( )} { if (offset.y > 200 || velocity.y > 800) { onClose(); } }} className="bg-black flex flex-col md:flex-row w-full max-w-6xl h-auto md:rounded-sm overflow-hidden shadow-2xl relative" onClick={e => e.stopPropagation()} >
{ const swipe = swipePower(offset.x, velocity.x); if (swipe < -swipeConfidenceThreshold) { if (currentIndex < post.media.length - 1) { paginate(1); } else if (hasNextPost && onNextPost && swipe < -interPostSwipeThreshold) { onNextPost(); } } else if (swipe > swipeConfidenceThreshold) { if (currentIndex > 0) { paginate(-1); } else if (hasPrevPost && onPrevPost && swipe > interPostSwipeThreshold) { onPrevPost(); } } }} className="col-start-1 row-start-1 w-full flex items-center justify-center cursor-grab active:cursor-grabbing relative" >
{post.media.length > 1 && ( <> {currentIndex > 0 && ( )} {currentIndex < post.media.length - 1 && ( )}
{post.media.map((_, i) => (
))}
)}
{profilePic ? ( ) : ( {post.username[0]} )}
{post.username}
{profilePic ? ( ) : ( {post.username[0]} )}
{post.username} {post.caption}
{format(parseISO(post.date), 'MMMM d, yyyy')}
Archived Post {post.id}
); }; export default function App() { const [allPosts, setAllPosts] = useState([]); const [allStories, setAllStories] = useState([]); const [showStoryViewer, setShowStoryViewer] = useState(false); const [visiblePostsCount, setVisiblePostsCount] = useState(90); const [isScanning, setIsScanning] = useState(false); const [selectedPost, setSelectedPost] = useState(null); const [username, setUsername] = useState(''); const [fullName, setFullName] = useState(''); const [bio, setBio] = useState(''); const [followerCount, setFollowerCount] = useState(0); const [followingCount, setFollowingCount] = useState(0); const [externalUrl, setExternalUrl] = useState(''); const [profilePic, setProfilePic] = useState(null); const [gridAspectRatio, setGridAspectRatio] = useState<'1:1' | '3:4'>('1:1'); const [gridOffset, setGridOffset] = useState(0); const [activeTab, setActiveTab] = useState<'posts' | 'reels' | 'saved'>('posts'); const [serverArchives, setServerArchives] = useState([]); const [isServerMode, setIsServerMode] = useState(false); const [currentArchive, setCurrentArchive] = useState(null); const fileInputRef = React.useRef(null); const profilePicInputRef = React.useRef(null); useEffect(() => { fetch('/api/archives') .then(res => res.json()) .then(data => { if (Array.isArray(data) && data.length > 0) { setServerArchives(data); setIsServerMode(true); } }) .catch(() => { setIsServerMode(false); }); }, []); const filteredPosts = useMemo(() => { if (activeTab === 'reels') { return allPosts.filter(p => p.media.length === 1 && p.media[0].type === 'video'); } if (activeTab === 'posts') { return allPosts.filter(p => !(p.media.length === 1 && p.media[0].type === 'video')); } return []; }, [allPosts, activeTab]); const handleTabChange = (tab: 'posts' | 'reels' | 'saved') => { setActiveTab(tab); setVisiblePostsCount(90); }; const visiblePosts = useMemo(() => { return filteredPosts.slice(0, visiblePostsCount); }, [filteredPosts, visiblePostsCount]); const postIndex = useMemo(() => { if (!selectedPost) return -1; return filteredPosts.findIndex(p => p.id === selectedPost.id); }, [selectedPost, filteredPosts]); const onNextPost = useCallback(() => { if (postIndex < filteredPosts.length - 1) { setSelectedPost(filteredPosts[postIndex + 1]); } }, [postIndex, filteredPosts]); const onPrevPost = useCallback(() => { if (postIndex > 0) { setSelectedPost(filteredPosts[postIndex - 1]); } }, [postIndex, filteredPosts]); const handleProfilePicChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { const url = URL.createObjectURL(file); setProfilePic(url); } }; const handleFiles = async (files: ArchiveFile[]) => { if (!files || files.length === 0) return; setIsScanning(true); setProfilePic(null); setGridOffset(0); console.log(`Starting scan of ${files.length} files...`); await new Promise(resolve => setTimeout(resolve, 100)); const parseXZFile = async (file: ArchiveFile) => { try { const decompressedStream = new XzReadableStream(file.stream()); const response = new Response(decompressedStream); return await response.json(); } catch (e) { console.error("Error decompressing XZ file:", file.name, e); return null; } }; try { const postsMap = new Map>(); const mediaFilesMap = new Map(); const exportRegex = /^(\d{4}-\d{2}-\d{2})_(.+?) - (.+?)(?: - (\d+))?(?: - (story))?\.(.+)$/; const instaloaderRegex = /^(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}_UTC)(?:_(\d+))?(?:_(story))?\.(.+)$/; const checkIsStory = (obj: any): boolean => { if (!obj) return false; const typeName = obj.__typename || obj.typename || ""; const nodeType = obj.node_type || ""; const productType = obj.product_type || ""; return ( obj.is_story === true || obj.is_reel_media === true || typeName.includes('Story') || obj.audience === "MediaAudience.DEFAULT" || nodeType === "StoryItem" || productType === "story" || typeName === "GraphStoryVideo" || typeName === "GraphStoryImage" ); }; let detectedUsername = ''; let format: 'export' | 'instaloader' | 'json' | 'unknown' = 'unknown'; let jsonFiles: ArchiveFile[] = []; for (const file of files) { const lowerName = file.name.toLowerCase(); if (lowerName.endsWith('.json') || lowerName.endsWith('.json.xz')) { jsonFiles.push(file); if (lowerName.includes('posts_1') || lowerName.includes('reels_1') || lowerName.includes('stories_1')) { format = 'json'; } else if (format === 'unknown' && (lowerName.includes('story') || lowerName.includes('post'))) { format = 'json'; } continue; } if (lowerName.includes('_profile_pic.jpg')) { const blob = new Blob([await file.arrayBuffer()], { type: 'image/jpeg' }); const url = file.url || URL.createObjectURL(blob); setProfilePic(url); if (!detectedUsername && file.webkitRelativePath) { const parts = file.webkitRelativePath.split(/[/\\]/); if (parts.length > 1) { detectedUsername = parts[0]; setUsername(detectedUsername); } } if (format === 'unknown') format = 'instaloader'; continue; } const exportMatch = file.name.match(exportRegex); if (exportMatch) { if (!detectedUsername) { detectedUsername = exportMatch[2]; setUsername(detectedUsername); } format = 'export'; } const loaderMatch = file.name.match(instaloaderRegex); if (loaderMatch && format === 'unknown') { format = 'instaloader'; if (!detectedUsername && file.webkitRelativePath) { const parts = file.webkitRelativePath.split(/[/\\]/); if (parts.length > 1) { detectedUsername = parts[0]; setUsername(detectedUsername); } } } if (['jpg', 'jpeg', 'png', 'webp', 'mp4'].some(ext => lowerName.endsWith(ext))) { const key = file.webkitRelativePath || file.name; mediaFilesMap.set(key, file); } } if (format === 'json' || format === 'instaloader') { for (const jsonFile of jsonFiles) { try { const data = jsonFile.name.endsWith('.xz') ? await parseXZFile(jsonFile) : JSON.parse(await jsonFile.text()); if (!data) continue; if (data.node && (data.instaloader?.node_type === 'Profile' || data.node.__typename === 'User')) { const node = data.node; const iphone = node.iphone_struct || {}; setUsername(node.username || ''); setFullName(node.full_name || ''); setBio(node.biography || iphone.biography || ''); setExternalUrl(node.external_url || ''); setFollowerCount(node.edge_followed_by?.count || iphone.follower_count || 0); setFollowingCount(node.edge_follow?.count || iphone.following_count || 0); continue; } const items = Array.isArray(data) ? data : (data.media || [data]); const isStoriesFile = jsonFile.name.toLowerCase().includes('stories'); for (const [idx, item] of items.entries()) { const mediaList = item.media || [item]; const postId = item.node?.id || item.id || item.title || `post_${idx}_${Date.now()}`; const date = item.creation_timestamp ? new Date(item.creation_timestamp * 1000).toISOString().split('T')[0] : item.node?.taken_at_timestamp ? new Date(item.node.taken_at_timestamp * 1000).toISOString().split('T')[0] : new Date().toISOString().split('T')[0]; const isStory = isStoriesFile || checkIsStory(item) || checkIsStory(item.node) || checkIsStory(data.instaloader) || checkIsStory(item.node?.iphone_struct) || checkIsStory(item.iphone_struct) || (item.media && Array.isArray(item.media) && item.media.some((m: any) => checkIsStory(m))); const post: Partial = { id: postId, date, username: detectedUsername || 'archived_user', caption: item.title || item.node?.edge_media_to_caption?.edges?.[0]?.node?.text || item.node?.caption?.text || '', media: [], isStory, }; for (const [mIdx, m] of mediaList.entries()) { const uri = m.uri; let matchedFile: ArchiveFile | undefined; if (uri) { for (const [path, f] of mediaFilesMap.entries()) { if (path.endsWith(uri) || uri.endsWith(path)) { matchedFile = f; break; } } } if (!matchedFile) { const id = item.node?.id || item.id; if (id) { for (const [path, f] of mediaFilesMap.entries()) { if (f.name.includes(id)) { matchedFile = f; break; } } } } if (!matchedFile) { const jsonBase = jsonFile.name.substring(0, jsonFile.name.lastIndexOf('.')); for (const ext of ['mp4', 'jpg', 'jpeg', 'png', 'webp']) { const possibleName = `${jsonBase}.${ext}`; for (const [path, f] of mediaFilesMap.entries()) { if (f.name.toLowerCase() === possibleName.toLowerCase()) { matchedFile = f; break; } } if (matchedFile) break; } } if (matchedFile) { const blob = new Blob([await matchedFile.arrayBuffer()], { type: matchedFile.name.endsWith('mp4') ? 'video/mp4' : 'image/jpeg' }); const url = matchedFile.url || URL.createObjectURL(blob); const type = matchedFile.name.toLowerCase().endsWith('mp4') ? 'video' : 'image'; const existingMedia = post.media!.find(media => media.index === mIdx + 1); if (existingMedia) { if (type === 'video' && existingMedia.type === 'image') { post.media = post.media!.map(media => media.index === mIdx + 1 ? { name: matchedFile!.name, url, type, index: mIdx + 1 } : media); } } else { post.media!.push({ name: matchedFile.name, url, type, index: mIdx + 1 }); } } } if (post.media!.length > 0) { postsMap.set(postId, post); } } } catch (e) { console.error("Error parsing JSON file:", jsonFile.name, e); } } } if (format !== 'json') { const CHUNK_SIZE = 100; for (let i = 0; i < files.length; i += CHUNK_SIZE) { const end = Math.min(i + CHUNK_SIZE, files.length); for (let j = i; j < end; j++) { const file = files[j]; const lowerName = file.name.toLowerCase(); if (detectedUsername && lowerName === `${detectedUsername.toLowerCase()}.jpg`) { const blob = new Blob([await file.arrayBuffer()], { type: 'image/jpeg' }); const url = file.url || URL.createObjectURL(blob); setProfilePic(url); continue; } let postId = ''; let date = ''; let user = detectedUsername || 'archived_user'; let index = 1; let ext = ''; let isStory = lowerName.includes('story') || file.webkitRelativePath.toLowerCase().includes('stories'); if (format === 'export') { const match = file.name.match(exportRegex); if (!match) continue; const [_, dateMatch, userMatch, postIdMatch, indexStrMatch, storyMatch, extMatch] = match; date = dateMatch; user = userMatch; postId = postIdMatch; index = indexStrMatch ? parseInt(indexStrMatch, 10) : 1; if (storyMatch) isStory = true; ext = extMatch; } else if (format === 'instaloader') { const match = file.name.match(instaloaderRegex); if (!match) continue; const [_, postIdMatch, indexStrMatch, storyMatch, extMatch] = match; postId = postIdMatch; date = postIdMatch.split('_')[0]; index = indexStrMatch ? parseInt(indexStrMatch, 10) : 1; if (storyMatch) isStory = true; ext = extMatch; } else { continue; } let post = postsMap.get(postId); if (!post) { post = { id: postId, date, username: user, caption: '', media: [], isStory, }; postsMap.set(postId, post); } else if (isStory) { post.isStory = true; } const lowerExt = ext.toLowerCase(); if (lowerExt === 'txt') { post.caption = await file.text(); } else if (lowerExt === 'json' || lowerName.endsWith('.json.xz')) { try { const data = lowerName.endsWith('.xz') ? await parseXZFile(file) : JSON.parse(await file.text()); if (!data) continue; const node = data.node || data; const iphone = node.iphone_struct || {}; const captionText = node.edge_media_to_caption?.edges?.[0]?.node?.text || node.caption?.text || iphone.caption?.text || ""; if (captionText) post.caption = captionText; if (checkIsStory(data) || checkIsStory(node) || checkIsStory(data.instaloader) || checkIsStory(iphone)) { post.isStory = true; } if (data.node && (data.instaloader?.node_type === 'Profile' || data.node.__typename === 'User')) { const n = data.node; const iph = n.iphone_struct || {}; setUsername(n.username || ''); setFullName(n.full_name || ''); setBio(n.biography || iph.biography || ''); setExternalUrl(n.external_url || ''); setFollowerCount(n.edge_followed_by?.count || iph.follower_count || 0); setFollowingCount(n.edge_follow?.count || iph.following_count || 0); } } catch (e) {} } else if (['jpg', 'jpeg', 'png', 'webp', 'mp4'].includes(lowerExt)) { const blob = new Blob([await file.arrayBuffer()], { type: lowerExt === 'mp4' ? 'video/mp4' : 'image/jpeg' }); const url = file.url || URL.createObjectURL(blob); const type = lowerExt === 'mp4' ? 'video' : 'image'; const existingMedia = post.media!.find(m => m.index === index); if (existingMedia) { if (type === 'video' && existingMedia.type === 'image') { post.media = post.media!.map(m => m.index === index ? { name: file.name, url, type, index } : m); } } else { post.media!.push({ name: file.name, url, type, index }); } } } await new Promise(resolve => setTimeout(resolve, 0)); } } const allItems = Array.from(postsMap.values()) .filter(p => p.media && p.media.length > 0) .map(p => { const sortedMedia = p.media!.sort((a, b) => a.index - b.index); return { ...p, media: sortedMedia, thumbnail: sortedMedia[0].url, } as Post; }); const posts = allItems.filter(p => !p.isStory).sort((a, b) => b.date.localeCompare(a.date)); const stories = allItems.filter(p => p.isStory).sort((a, b) => a.date.localeCompare(b.date)); setAllPosts(posts); setAllStories(stories); setVisiblePostsCount(90); } catch (err) { console.error('Error processing files:', err); } finally { setIsScanning(false); } }; const loadServerArchive = async (archive: ServerArchive) => { setIsScanning(true); setCurrentArchive(archive); try { const res = await fetch(`/api/archives/${archive.name}/files`); const filePaths: string[] = await res.json(); const archiveFiles = filePaths.map(p => { const name = p.split(/[/\\]/).pop() || p; return new RemoteArchiveFile(name, p, 0, `/archives/${archive.name}/${p}`); }); await handleFiles(archiveFiles); } catch (err) { console.error('Failed to load server archive:', err); } finally { setIsScanning(false); } }; const handleLocalFiles = (files: FileList | null) => { if (!files) return; const archiveFiles = Array.from(files).map(f => new LocalArchiveFile(f)); handleFiles(archiveFiles); }; const triggerFileSelect = () => { fileInputRef.current?.click(); }; const loadMore = () => { setVisiblePostsCount(prev => prev + 90); }; return (
{/* Hidden Input for Directory Selection */} handleLocalFiles(e.target.files)} /> {/* Navigation */}
{allPosts.length === 0 && !isScanning ? ( isServerMode ? ( ) : (

No Archive Loaded

Select the directory containing your Instagram archive files to start browsing.

) ) : isScanning ? (
Scanning Archive...

Parsing media and metadata

) : (
{/* Profile Header */}
0 ? "bg-gradient-to-tr from-yellow-400 via-red-500 to-purple-600" : "bg-gray-200" )} onClick={() => allStories.length > 0 && setShowStoryViewer(true)} >
{profilePic ? ( {username} setProfilePic(null)} referrerPolicy="no-referrer" /> ) : ( {username[0]} )}

{username}

{allPosts.length} posts
{followerCount.toLocaleString()} followers
{followingCount.toLocaleString()} following
{fullName || `@${username}`}
{bio || 'Archived profile viewer for local files.'}
{externalUrl && ( {externalUrl.replace(/^https?:\/\/(www\.)?/, '')} )}
{/* Tabs */}
{/* Grid */}
{activeTab === 'posts' && Array.from({ length: gridOffset }).map((_, i) => (
Blank
))} {visiblePosts.map((post) => ( setSelectedPost(post)} className={cn( "relative group cursor-pointer overflow-hidden bg-gray-200 transition-all duration-300", activeTab === 'reels' ? "aspect-[9/16]" : (gridAspectRatio === '1:1' ? "aspect-square" : "aspect-[3/4]") )} > {post.media[0].type === 'video' ? ( ) : ( )} {/* Icons */}
{post.media.length > 1 && (
)} {post.media.some(m => m.type === 'video') && (
)}
{/* Hover Overlay */}
-
-
))}
{filteredPosts.length > visiblePostsCount && (
)}
)}
{/* Post Modal */} {selectedPost && ( setSelectedPost(null)} onNextPost={onNextPost} onPrevPost={onPrevPost} hasNextPost={postIndex < filteredPosts.length - 1} hasPrevPost={postIndex > 0} profilePic={profilePic} /> )} {/* Story Viewer */} {showStoryViewer && allStories.length > 0 && ( setShowStoryViewer(false)} profilePic={profilePic} /> )} {/* Footer */}
); }