feat: fixes and formatter

This commit is contained in:
2026-01-12 16:01:32 +01:00
parent 85c6084351
commit 11e632acff
353 changed files with 23315 additions and 27361 deletions
+187 -221
View File
@@ -1,234 +1,217 @@
import { useIsFocused, useRoute } from "@react-navigation/native";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
FlatList,
Image,
Pressable,
StyleSheet,
View,
useWindowDimensions,
} from "react-native";
import { icons } from "../../assets";
import { projectsRef } from "../../config/firebase";
import useDataFromRef from "../../hooks/useDataFromRef";
import { useUser } from "../../providers/UserDataProvider";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import PlaybackItem from "./components/PlaybackItem.web";
import { useIsFocused, useRoute } from '@react-navigation/native'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { FlatList, Image, Pressable, StyleSheet, View, useWindowDimensions } from 'react-native'
import { icons } from '../../assets'
import { projectsRef } from '../../config/firebase'
import useDataFromRef from '../../hooks/useDataFromRef'
import { useUser } from '../../providers/UserDataProvider'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import PlaybackItem from './components/PlaybackItem.web'
const Playbacks = () => {
const { height: windowHeight } = useWindowDimensions();
const [activeIndex, setActiveIndex] = useState(0);
const route = useRoute();
const focusProjectId =
route?.params?.projectId || route?.params?.focusId || null;
const userCache = useRef(new Map());
const { getUserByUid } = useUser() || {};
const isFocused = useIsFocused();
const { height: windowHeight } = useWindowDimensions()
const [activeIndex, setActiveIndex] = useState(0)
const route = useRoute()
const focusProjectId = route?.params?.projectId || route?.params?.focusId || null
const userCache = useRef(new Map())
const { getUserByUid } = useUser() || {}
const isFocused = useIsFocused()
const {
data: rawPlaybacks = [],
loadMore,
hasMore,
loading,
} = useDataFromRef({
ref: projectsRef.where("playbackUrl", "!=", null),
ref: projectsRef.where('playbackUrl', '!=', null),
simpleRef: false,
listener: false,
usePagination: true,
batchSize: 6,
});
})
const focusIndex = useMemo(() => {
if (!focusProjectId) return -1;
return rawPlaybacks.findIndex((p) => p?.id === focusProjectId);
}, [focusProjectId, rawPlaybacks]);
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]);
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 activeTitle =
typeof activePlayback?.title === "string"
? activePlayback.title.trim()
: "";
const activePlayback = playbacks?.[activeIndex] || null
const activeVideoUrl = activePlayback?.playbackUrl || null
const activeTitle = typeof activePlayback?.title === 'string' ? activePlayback.title.trim() : ''
const activeCreatorName = activePlayback?.userName;
const backgroundVideoRef = useRef(null);
const lastActiveIndexRef = useRef(0);
const backgroundSeekPendingRef = useRef(false);
const listRef = useRef(null);
const activeCreatorName = activePlayback?.userName
const backgroundVideoRef = useRef(null)
const lastActiveIndexRef = useRef(0)
const backgroundSeekPendingRef = useRef(false)
const listRef = useRef(null)
useEffect(() => {
const total = playbacks?.length || 0;
const total = playbacks?.length || 0
if (total <= 0) {
if (activeIndex !== 0) {
lastActiveIndexRef.current = 0;
setActiveIndex(0);
lastActiveIndexRef.current = 0
setActiveIndex(0)
}
return;
return
}
const maxIndex = total - 1;
const maxIndex = total - 1
if (activeIndex > maxIndex) {
lastActiveIndexRef.current = maxIndex;
setActiveIndex(maxIndex);
lastActiveIndexRef.current = maxIndex
setActiveIndex(maxIndex)
}
}, [playbacks?.length, activeIndex]);
}, [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 (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?.();
loadMore?.()
}
},
[playbacks?.length, loadMore],
);
[playbacks?.length, loadMore]
)
const syncBackgroundVideo = useCallback(({ currentTime, isPlaying }) => {
const bg = backgroundVideoRef.current;
if (!bg) return;
const bg = backgroundVideoRef.current
if (!bg) return
if (typeof currentTime === "number" && Number.isFinite(currentTime)) {
if (typeof currentTime === 'number' && Number.isFinite(currentTime)) {
const applyTime = () => {
backgroundSeekPendingRef.current = false;
const diff = Math.abs((bg.currentTime || 0) - currentTime);
backgroundSeekPendingRef.current = false
const diff = Math.abs((bg.currentTime || 0) - currentTime)
if (diff > 0.25) {
try {
bg.currentTime = currentTime;
bg.currentTime = currentTime
} catch (_e) {}
}
};
}
if (bg.readyState >= 1) applyTime();
if (bg.readyState >= 1) applyTime()
else if (!backgroundSeekPendingRef.current) {
backgroundSeekPendingRef.current = true;
const handler = () => applyTime();
bg.addEventListener("loadeddata", handler, { once: true });
backgroundSeekPendingRef.current = true
const handler = () => applyTime()
bg.addEventListener('loadeddata', handler, { once: true })
}
}
if (isPlaying === true) {
if (bg.paused) {
bg.play().catch(() => {});
bg.play().catch(() => {})
}
} else if (isPlaying === false) {
try {
if (!bg.paused) bg.pause();
if (!bg.paused) bg.pause()
} catch (_e) {}
}
}, []);
}, [])
useEffect(() => {
const bg = backgroundVideoRef.current;
if (!bg) return;
const bg = backgroundVideoRef.current
if (!bg) return
if (!activeVideoUrl) {
backgroundSeekPendingRef.current = false;
backgroundSeekPendingRef.current = false
try {
bg.pause();
bg.pause()
} catch (_e) {}
bg.removeAttribute?.("src");
bg.load?.();
return;
bg.removeAttribute?.('src')
bg.load?.()
return
}
backgroundSeekPendingRef.current = false;
backgroundSeekPendingRef.current = false
const setSrcIfNeeded = () => {
const attrSrc = bg.getAttribute("src");
const attrSrc = bg.getAttribute('src')
if (attrSrc !== activeVideoUrl) {
bg.setAttribute("src", activeVideoUrl);
bg.setAttribute('src', activeVideoUrl)
try {
bg.load();
bg.load()
} catch (_e) {}
}
};
}
const startPlayback = () => {
bg.play().catch(() => {});
};
bg.play().catch(() => {})
}
setSrcIfNeeded();
setSrcIfNeeded()
if (bg.readyState >= 2) {
startPlayback();
return;
startPlayback()
return
}
const handleLoaded = () => {
startPlayback();
};
bg.addEventListener("loadeddata", handleLoaded, { once: true });
startPlayback()
}
bg.addEventListener('loadeddata', handleLoaded, { once: true })
return () => {
bg.removeEventListener("loadeddata", handleLoaded);
};
}, [activeVideoUrl]);
bg.removeEventListener('loadeddata', handleLoaded)
}
}, [activeVideoUrl])
const triedLoadMoreRef = useRef(0);
const hasAppliedFocusRef = useRef(false);
const focusLoadAttemptsRef = useRef(0);
const triedLoadMoreRef = useRef(0)
const hasAppliedFocusRef = useRef(false)
const focusLoadAttemptsRef = useRef(0)
useEffect(() => {
hasAppliedFocusRef.current = false;
focusLoadAttemptsRef.current = 0;
}, [focusProjectId]);
hasAppliedFocusRef.current = false
focusLoadAttemptsRef.current = 0
}, [focusProjectId])
useEffect(() => {
if (!focusProjectId) return;
if (!focusProjectId) return
if (focusIndex >= 0 && !hasAppliedFocusRef.current) {
hasAppliedFocusRef.current = true;
lastActiveIndexRef.current = 0;
setActiveIndex(0);
hasAppliedFocusRef.current = true
lastActiveIndexRef.current = 0
setActiveIndex(0)
requestAnimationFrame(() => {
try {
listRef.current?.scrollToOffset?.({ offset: 0, animated: false });
listRef.current?.scrollToOffset?.({ offset: 0, animated: false })
} catch (_e) {}
});
return;
})
return
}
if (loadMore && focusLoadAttemptsRef.current < 6) {
focusLoadAttemptsRef.current += 1;
loadMore();
focusLoadAttemptsRef.current += 1
loadMore()
}
}, [focusIndex, focusProjectId, loadMore, playbacks]);
}, [focusIndex, focusProjectId, loadMore, playbacks])
// === 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);
const y = e?.nativeEvent?.contentOffset?.y || 0
const idx = Math.round(y / windowHeight)
handleActiveIndexChange(idx)
} catch (_e) {}
},
[windowHeight, handleActiveIndexChange],
);
[windowHeight, handleActiveIndexChange]
)
const onScroll = useCallback(
(e) => {
try {
const y = e?.nativeEvent?.contentOffset?.y || 0;
const idx = Math.round(y / windowHeight);
handleActiveIndexChange(idx);
const y = e?.nativeEvent?.contentOffset?.y || 0
const idx = Math.round(y / windowHeight)
handleActiveIndexChange(idx)
} catch (_e) {}
},
[windowHeight, handleActiveIndexChange],
);
[windowHeight, handleActiveIndexChange]
)
const getItemLayout = useCallback(
(_data, index) => ({
@@ -236,90 +219,80 @@ const Playbacks = () => {
offset: windowHeight * index,
index,
}),
[windowHeight],
);
[windowHeight]
)
const total = playbacks.length;
const total = playbacks.length
const indicatorItems = useMemo(() => {
if (total <= 0) return [];
const maxVisible = 7;
let start = 0;
let end = total - 1;
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;
const half = Math.floor(maxVisible / 2)
start = activeIndex - half
end = start + maxVisible - 1
if (start < 0) {
end += -start;
start = 0;
end += -start
start = 0
}
if (end >= total) {
const overshoot = end - (total - 1);
start = Math.max(0, start - overshoot);
end = total - 1;
const overshoot = end - (total - 1)
start = Math.max(0, start - overshoot)
end = total - 1
}
}
const items = [];
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]);
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 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]);
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 (!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?.();
loadMore?.()
}
return;
return
}
const targetOffset = targetIndex * windowHeight;
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);
handleActiveIndexChange(targetIndex)
},
[
activeIndex,
handleActiveIndexChange,
hasMore,
loadMore,
playbacks.length,
windowHeight,
],
);
[activeIndex, handleActiveIndexChange, hasMore, loadMore, playbacks.length, windowHeight]
)
return (
<View style={styles.screen}>
@@ -335,13 +308,13 @@ const Playbacks = () => {
playsInline
preload="auto"
style={{
position: "absolute",
position: 'absolute',
inset: 0,
width: "100%",
height: "100%",
objectFit: "cover",
filter: "blur(28px) saturate(120%) brightness(55%)",
transform: "scale(1.1)",
width: '100%',
height: '100%',
objectFit: 'cover',
filter: 'blur(28px) saturate(120%) brightness(55%)',
transform: 'scale(1.1)',
}}
/>
<View style={styles.backgroundOverlay} />
@@ -352,7 +325,7 @@ const Playbacks = () => {
data={playbacks}
keyExtractor={(item, index) => String(item?.id || index)}
renderItem={({ item, index }) => (
<View style={{ width: "100%", height: windowHeight }}>
<View style={{ width: '100%', height: windowHeight }}>
<PlaybackItem
item={item}
userCache={userCache}
@@ -398,16 +371,12 @@ const Playbacks = () => {
styles.indicatorDot,
item.isActive && styles.indicatorDotActive,
item.isPast && styles.indicatorDotPast,
index < indicatorItems.length - 1
? styles.indicatorDotSpacing
: null,
index < indicatorItems.length - 1 ? styles.indicatorDotSpacing : null,
]}
/>
))}
{canScrollDown && hasMore && (
<View
style={[styles.indicatorMoreDot, styles.indicatorDotSpacing]}
/>
<View style={[styles.indicatorMoreDot, styles.indicatorDotSpacing]} />
)}
</View>
{/* {indicatorLabel ? (
@@ -422,53 +391,50 @@ const Playbacks = () => {
<Image
source={icons.chevronDown}
resizeMode="contain"
style={[
styles.indicatorArrow,
!canScrollDown && styles.indicatorArrowDisabled,
]}
style={[styles.indicatorArrow, !canScrollDown && styles.indicatorArrowDisabled]}
/>
</Pressable>
</View>
)}
</View>
// </Page>
);
};
)
}
export default Playbacks;
export default Playbacks
const styles = StyleSheet.create({
screen: {
flex: 1,
},
backgroundVideoWrapper: {
height: "100%",
width: "100%",
position: "absolute",
height: '100%',
width: '100%',
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
overflow: "hidden",
overflow: 'hidden',
zIndex: -1,
backgroundColor: "#060606",
backgroundColor: '#060606',
},
backgroundOverlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: "rgba(2, 2, 2, 0.35)",
backgroundColor: 'rgba(2, 2, 2, 0.35)',
},
topOverlayContainer: {
position: "absolute",
position: 'absolute',
top: 32,
left: 24,
right: 24,
zIndex: 10,
},
topOverlayContent: {
alignSelf: "flex-start",
alignSelf: 'flex-start',
paddingVertical: 12,
paddingHorizontal: 18,
borderRadius: 18,
backgroundColor: "rgba(0,0,0,0.45)",
backgroundColor: 'rgba(0,0,0,0.45)',
gap: 4,
},
topOverlayTitle: {
@@ -483,12 +449,12 @@ const styles = StyleSheet.create({
opacity: 0.9,
},
indicatorContainer: {
position: "absolute",
position: 'absolute',
left: 20,
top: 0,
bottom: 0,
justifyContent: "center",
alignItems: "center",
justifyContent: 'center',
alignItems: 'center',
gap: 16,
},
indicatorArrow: {
@@ -502,22 +468,22 @@ const styles = StyleSheet.create({
paddingHorizontal: 8,
},
indicatorArrowUp: {
transform: [{ rotate: "180deg" }],
transform: [{ rotate: '180deg' }],
},
indicatorArrowDisabled: {
opacity: 0.25,
},
indicatorDotsWrapper: {
alignItems: "center",
alignItems: 'center',
},
indicatorDot: {
width: 6,
height: 12,
borderRadius: 3,
backgroundColor: "rgba(255,255,255,0.3)",
backgroundColor: 'rgba(255,255,255,0.3)',
},
indicatorDotPast: {
backgroundColor: "rgba(255,255,255,0.55)",
backgroundColor: 'rgba(255,255,255,0.55)',
},
indicatorDotActive: {
backgroundColor: Palette.white,
@@ -537,6 +503,6 @@ const styles = StyleSheet.create({
color: Palette.white,
fontSize: 12,
fontFamily: FONT_FAMILY.InterMedium,
textAlign: "center",
textAlign: 'center',
},
});
})