import { useIsFocused, useRoute } from '@react-navigation/native' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { FlatList, Image, Pressable, StyleSheet, Text, View, useWindowDimensions, } from 'react-native' import { icons } from '../../assets' import { projectsRef } from '../../config/firebase' import useDataFromRef from '../../hooks/useDataFromRef' import usePlayer from '../../hooks/usePlayer' import { useUser } from '../../providers/UserDataProvider' import { Palette } from '../../styles' import { FONT_FAMILY } from '../../styles/Fonts' import PlaybackItem from './components/PlaybackItem.web' import PlaybackCharts from './components/PlaybackCharts' const Playbacks = () => { const { height: windowHeight } = useWindowDimensions() const [activeIndex, setActiveIndex] = useState(0) const route = useRoute() const { stop } = usePlayer() || {} const wasFocusedRef = useRef(false) const initialFocusProjectId = route?.params?.projectId || route?.params?.focusId || null const [focusProjectId, setFocusProjectId] = useState(initialFocusProjectId) const [viewMode, setViewMode] = useState(initialFocusProjectId ? 'feed' : 'charts') const userCache = useRef(new Map()) const { getUserByUid } = useUser() || {} const isFocused = useIsFocused() const { data: rawPlaybacks = [], loadMore, hasMore, loading, } = useDataFromRef({ ref: projectsRef.where('playbackUrl', '!=', null), format: (docs) => docs.filter((item) => item?.playbackPublishedOnMusicLand !== false), simpleRef: false, listener: false, usePagination: true, batchSize: 6, }) const focusIndex = useMemo(() => { if (!focusProjectId) return -1 return rawPlaybacks.findIndex((p) => p?.id === focusProjectId) }, [focusProjectId, rawPlaybacks]) const playbacks = useMemo(() => { if (focusIndex < 0) return rawPlaybacks const target = rawPlaybacks[focusIndex] const before = rawPlaybacks.slice(0, focusIndex) const after = rawPlaybacks.slice(focusIndex + 1) return [target, ...after, ...before] }, [focusIndex, rawPlaybacks]) const activePlayback = playbacks?.[activeIndex] || null const activeVideoUrl = activePlayback?.playbackUrl || null const backgroundVideoRef = useRef(null) const lastActiveIndexRef = useRef(0) const backgroundSeekPendingRef = useRef(false) const listRef = useRef(null) useEffect(() => { if (isFocused && !wasFocusedRef.current) { stop?.().catch(() => {}) } wasFocusedRef.current = isFocused }, [isFocused, stop]) useEffect(() => { const total = playbacks?.length || 0 if (total <= 0) { if (activeIndex !== 0) { lastActiveIndexRef.current = 0 setActiveIndex(0) } return } const maxIndex = total - 1 if (activeIndex > maxIndex) { lastActiveIndexRef.current = maxIndex setActiveIndex(maxIndex) } }, [playbacks?.length, activeIndex]) const handleActiveIndexChange = useCallback( (rawIndex) => { if (Number.isNaN(rawIndex)) return const total = playbacks?.length || 0 const maxIndex = Math.max(0, total - 1) const clamped = Math.max(0, Math.min(rawIndex, maxIndex)) if (clamped === lastActiveIndexRef.current) return lastActiveIndexRef.current = clamped setActiveIndex((prev) => (prev === clamped ? prev : clamped)) if (total > 0 && clamped >= Math.max(0, total - 6)) { loadMore?.() } }, [playbacks?.length, loadMore] ) const syncBackgroundVideo = useCallback(({ currentTime, isPlaying }) => { const bg = backgroundVideoRef.current if (!bg) return if (typeof currentTime === 'number' && Number.isFinite(currentTime)) { const applyTime = () => { backgroundSeekPendingRef.current = false const diff = Math.abs((bg.currentTime || 0) - currentTime) if (diff > 0.25) { try { bg.currentTime = currentTime } catch (_e) {} } } if (bg.readyState >= 1) applyTime() else if (!backgroundSeekPendingRef.current) { backgroundSeekPendingRef.current = true const handler = () => applyTime() bg.addEventListener('loadeddata', handler, { once: true }) } } if (isPlaying === true) { if (bg.paused) { bg.play().catch(() => {}) } } else if (isPlaying === false) { try { if (!bg.paused) bg.pause() } catch (_e) {} } }, []) useEffect(() => { const bg = backgroundVideoRef.current if (!bg) return if (!activeVideoUrl) { backgroundSeekPendingRef.current = false try { bg.pause() } catch (_e) {} bg.removeAttribute?.('src') bg.load?.() return } backgroundSeekPendingRef.current = false const setSrcIfNeeded = () => { const attrSrc = bg.getAttribute('src') if (attrSrc !== activeVideoUrl) { bg.setAttribute('src', activeVideoUrl) try { bg.load() } catch (_e) {} } } const startPlayback = () => { bg.play().catch(() => {}) } setSrcIfNeeded() if (bg.readyState >= 2) { startPlayback() return } const handleLoaded = () => { startPlayback() } bg.addEventListener('loadeddata', handleLoaded, { once: true }) return () => { bg.removeEventListener('loadeddata', handleLoaded) } }, [activeVideoUrl]) const hasAppliedFocusRef = useRef(false) const focusLoadAttemptsRef = useRef(0) useEffect(() => { hasAppliedFocusRef.current = false focusLoadAttemptsRef.current = 0 }, [focusProjectId]) useEffect(() => { if (!focusProjectId) return if (focusIndex >= 0 && !hasAppliedFocusRef.current) { hasAppliedFocusRef.current = true lastActiveIndexRef.current = 0 setActiveIndex(0) requestAnimationFrame(() => { try { listRef.current?.scrollToOffset?.({ offset: 0, animated: false }) } catch (_e) {} }) return } if (loadMore && focusLoadAttemptsRef.current < 6) { focusLoadAttemptsRef.current += 1 loadMore() } }, [focusIndex, focusProjectId, loadMore, playbacks]) useEffect(() => { const nextFocusId = route?.params?.projectId || route?.params?.focusId || null setFocusProjectId(nextFocusId) if (nextFocusId) { setViewMode('feed') } }, [route?.params?.focusId, route?.params?.projectId]) useEffect(() => { if (viewMode !== 'feed') { return } lastActiveIndexRef.current = 0 setActiveIndex(0) requestAnimationFrame(() => { try { listRef.current?.scrollToOffset?.({ offset: 0, animated: false }) } catch (_e) {} }) }, [viewMode]) // === FlatList (one real page per item) === // keep active index in sync with scroll const onMomentumScrollEnd = useCallback( (e) => { try { const y = e?.nativeEvent?.contentOffset?.y || 0 const idx = Math.round(y / windowHeight) handleActiveIndexChange(idx) } catch (_e) {} }, [windowHeight, handleActiveIndexChange] ) const onScroll = useCallback( (e) => { try { const y = e?.nativeEvent?.contentOffset?.y || 0 const idx = Math.round(y / windowHeight) handleActiveIndexChange(idx) } catch (_e) {} }, [windowHeight, handleActiveIndexChange] ) const getItemLayout = useCallback( (_data, index) => ({ length: windowHeight, offset: windowHeight * index, index, }), [windowHeight] ) const total = playbacks.length const indicatorItems = useMemo(() => { if (total <= 0) return [] const maxVisible = 7 let start = 0 let end = total - 1 if (total > maxVisible) { const half = Math.floor(maxVisible / 2) start = activeIndex - half end = start + maxVisible - 1 if (start < 0) { end += -start start = 0 } if (end >= total) { const overshoot = end - (total - 1) start = Math.max(0, start - overshoot) end = total - 1 } } const items = [] for (let i = start; i <= end; i += 1) { items.push({ key: `indicator-${i}`, isActive: i === activeIndex, isPast: i < activeIndex, }) } return items }, [total, activeIndex]) const canScrollUp = total > 0 && activeIndex > 0 const atLastLoaded = total > 0 && activeIndex >= total - 1 const canScrollDown = total > 0 && (!atLastLoaded || hasMore) const showIndicators = total > 0 const indicatorLabel = useMemo(() => { if (total <= 0) return null if (hasMore || loading) return `${activeIndex + 1}+` return `${activeIndex + 1}/${total}` }, [total, activeIndex, hasMore, loading]) const scrollToPlayback = useCallback( (direction) => { if (!direction || !listRef.current) return const totalItems = playbacks.length if (totalItems <= 0) return const targetIndex = Math.max(0, Math.min(activeIndex + direction, totalItems - 1)) if (targetIndex === activeIndex) { if (direction > 0 && hasMore) { loadMore?.() } return } const targetOffset = targetIndex * windowHeight try { listRef.current.scrollToOffset({ offset: targetOffset, animated: true, }) } catch (_e) { try { listRef.current.scrollToIndex({ index: targetIndex, animated: true, }) } catch (__e) {} } handleActiveIndexChange(targetIndex) }, [activeIndex, handleActiveIndexChange, hasMore, loadMore, playbacks.length, windowHeight] ) const showFeed = viewMode === 'feed' const openFeedForPlayback = useCallback((project) => { const nextId = project && typeof project === 'object' ? project?.id || project?.projectId || null : project || null if (!nextId) { return } setFocusProjectId(nextId) setViewMode('feed') }, []) if (!showFeed) { return } return ( {activeVideoUrl ? (