Live video on the AT Protocol

add a gradient thingy

+186 -75
+16 -15
js/app/components/mobile/desktop-ui.tsx
··· 10 10 View, 11 11 zero, 12 12 } from "@streamplace/components"; 13 + import { AnimatedGradient } from "components/ui/gradient"; 13 14 import React, { useCallback, useEffect, useRef, useState } from "react"; 14 15 import { Platform } from "react-native"; 15 16 import { Gesture, GestureDetector } from "react-native-gesture-handler"; ··· 228 229 layout.position.absolute, 229 230 position.bottom[0], 230 231 w.percent[100], 231 - { 232 - backgroundColor: "rgba(0, 0, 0, 0.6)", 233 - paddingHorizontal: 16, 234 - paddingVertical: 2, 235 - paddingBottom: 2, 236 - }, 237 232 animatedFadeStyle, 238 233 ]} 239 234 > 240 - <BottomControlBar 241 - ingest={ingest} 242 - pipSupported={pipSupported} 243 - pipActive={pipActive} 244 - onHandlePip={handlePip} 245 - dropdownPortalContainer={fullscreen && portalContainerID} 246 - showChat={isChatOpen || false} 247 - setShowChat={setIsChatOpen || undefined} 248 - /> 235 + <AnimatedGradient 236 + fromColor="#00000080" 237 + toColor="#000000" 238 + opacityColor1={0} 239 + > 240 + <BottomControlBar 241 + ingest={ingest} 242 + pipSupported={pipSupported} 243 + pipActive={pipActive} 244 + onHandlePip={handlePip} 245 + dropdownPortalContainer={fullscreen && portalContainerID} 246 + showChat={isChatOpen || false} 247 + setShowChat={setIsChatOpen || undefined} 248 + /> 249 + </AnimatedGradient> 249 250 </Animated.View> 250 251 251 252 {isSelfAndNotLive && (
+93 -60
js/app/components/mobile/desktop-ui/bottom-controls.tsx
··· 17 17 PictureInPicture2, 18 18 } from "lucide-react-native"; 19 19 import { Platform, Pressable } from "react-native"; 20 + import { Mu } from "./mu"; 20 21 import { VolumeSlider } from "./volume-slider"; 21 22 22 - import { Mu } from "./mu"; 23 - 24 - const { gap, layout, p, r, py, px } = zero; 23 + const { gap, layout, p, r, px } = zero; 25 24 26 25 interface BottomControlBarProps { 27 26 ingest: string | null; ··· 33 32 setShowChat?: (show: boolean) => void; 34 33 } 35 34 35 + function PipButton({ 36 + pipActive, 37 + onHandlePip, 38 + }: { 39 + pipActive: boolean; 40 + onHandlePip: () => void; 41 + }) { 42 + const { theme } = useTheme(); 43 + if (Platform.OS !== "web") return null; 44 + return ( 45 + <Pressable onPress={onHandlePip} disabled={pipActive}> 46 + <View style={{ opacity: pipActive ? 0.5 : 1 }}> 47 + <PictureInPicture2 color={theme.colors.text} /> 48 + </View> 49 + </Pressable> 50 + ); 51 + } 52 + 53 + function DanmuButton() { 54 + const { theme } = useTheme(); 55 + const danmuUnlocked = useDanmuUnlocked(); 56 + const danmuEnabled = useDanmuEnabled(); 57 + const setDanmuEnabled = useSetDanmuEnabled(); 58 + if (!danmuUnlocked) return null; 59 + return ( 60 + <Pressable 61 + onPress={() => setDanmuEnabled(!danmuEnabled)} 62 + style={[px[2], r[1]]} 63 + > 64 + <Mu 65 + size={22} 66 + color={theme.colors.text} 67 + style={{ opacity: danmuEnabled ? 1 : 0.5 }} 68 + /> 69 + </Pressable> 70 + ); 71 + } 72 + 73 + function ContextMenuButton({ 74 + dropdownPortalContainer, 75 + }: { 76 + dropdownPortalContainer?: any; 77 + }) { 78 + return ( 79 + <PlayerUI.ContextMenu dropdownPortalContainer={dropdownPortalContainer} /> 80 + ); 81 + } 82 + 83 + function FullscreenButton() { 84 + const { theme } = useTheme(); 85 + const fullscreen = usePlayerStore((state) => state.fullscreen); 86 + const setFullscreen = usePlayerStore((state) => state.setFullscreen); 87 + if (Platform.OS !== "web") return null; 88 + return ( 89 + <Pressable onPress={() => setFullscreen(!fullscreen)} style={[p[2], r[1]]}> 90 + {fullscreen ? ( 91 + <Minimize color={theme.colors.text} /> 92 + ) : ( 93 + <Fullscreen color={theme.colors.text} /> 94 + )} 95 + </Pressable> 96 + ); 97 + } 98 + 99 + function CollapseChatButton({ 100 + showChat, 101 + setShowChat, 102 + }: { 103 + showChat: boolean; 104 + setShowChat: (show: boolean) => void; 105 + }) { 106 + if (Platform.OS === "web") return null; 107 + return ( 108 + <Button variant="outline" size="sm" onPress={() => setShowChat(!showChat)}> 109 + {showChat ? ( 110 + <ChevronRight color="white" size={16} /> 111 + ) : ( 112 + <ChevronLeft color="white" size={16} /> 113 + )} 114 + </Button> 115 + ); 116 + } 117 + 36 118 export function BottomControlBar({ 37 119 ingest, 38 120 pipSupported, ··· 42 124 showChat, 43 125 setShowChat, 44 126 }: BottomControlBarProps) { 45 - let { theme } = useTheme(); 46 - const fullscreen = usePlayerStore((state) => state.fullscreen); 47 - const setFullscreen = usePlayerStore((state) => state.setFullscreen); 48 - const danmuUnlocked = useDanmuUnlocked(); 49 - const danmuEnabled = useDanmuEnabled(); 50 - const setDanmuEnabled = useSetDanmuEnabled(); 51 - 52 127 return ( 53 128 <View 54 129 style={[ 55 130 layout.flex.row, 56 131 layout.flex.spaceBetween, 57 132 layout.flex.alignCenter, 133 + zero.px[4], 58 134 ]} 59 135 > 60 136 <View style={[layout.flex.row, layout.flex.alignCenter, gap.all[4]]}> ··· 62 138 </View> 63 139 64 140 <View style={[layout.flex.row, layout.flex.alignCenter, gap.all[3]]}> 65 - {Platform.OS === "web" && pipSupported && ( 66 - <Pressable onPress={onHandlePip} disabled={pipActive}> 67 - <View style={{ opacity: pipActive ? 0.5 : 1 }}> 68 - <PictureInPicture2 color={theme.colors.text} /> 69 - </View> 70 - </Pressable> 71 - )} 72 - {danmuUnlocked && ( 73 - <Pressable 74 - onPress={() => { 75 - setDanmuEnabled(!danmuEnabled); 76 - }} 77 - style={[px[0], r[1]]} 78 - > 79 - <Mu 80 - size={22} 81 - color={theme.colors.text} 82 - style={{ opacity: danmuEnabled ? 1 : 0.5 }} 83 - /> 84 - </Pressable> 141 + {pipSupported && ( 142 + <PipButton pipActive={pipActive} onHandlePip={onHandlePip} /> 85 143 )} 144 + <DanmuButton /> 86 145 {ingest === null && ( 87 - <PlayerUI.ContextMenu 146 + <ContextMenuButton 88 147 dropdownPortalContainer={dropdownPortalContainer} 89 148 /> 90 149 )} 91 - {Platform.OS === "web" && ( 92 - <Pressable 93 - onPress={() => { 94 - setFullscreen(!fullscreen); 95 - }} 96 - style={[p[2], r[1]]} 97 - > 98 - {fullscreen ? ( 99 - <Minimize color={theme.colors.text} /> 100 - ) : ( 101 - <Fullscreen color={theme.colors.text} /> 102 - )} 103 - </Pressable> 104 - )} 105 - {/* if not web, then add the collapse chat controls here */} 106 - {Platform.OS !== "web" && setShowChat && ( 107 - <Button 108 - variant="outline" 109 - size="sm" 110 - onPress={() => { 111 - setShowChat(!showChat); 112 - }} 113 - > 114 - {showChat ? ( 115 - <ChevronRight color="white" size={16} /> 116 - ) : ( 117 - <ChevronLeft color="white" size={16} /> 118 - )} 119 - </Button> 150 + <FullscreenButton /> 151 + {setShowChat && ( 152 + <CollapseChatButton showChat={showChat} setShowChat={setShowChat} /> 120 153 )} 121 154 </View> 122 155 </View>
+77
js/app/components/ui/gradient.tsx
··· 1 + // Source - https://stackoverflow.com/a/74182982 2 + // Posted by TOPKAT, modified by community. See post 'Timeline' for change history 3 + // Retrieved 2026-02-18, License - CC BY-SA 4.0 4 + 5 + import { DimensionValue, StyleSheet, View, ViewProps } from "react-native"; 6 + import Animated from "react-native-reanimated"; 7 + import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg"; 8 + 9 + type GradientProps = { 10 + fromColor: string; 11 + toColor: string; 12 + children?: any; 13 + height?: DimensionValue; 14 + opacityColor1?: number; 15 + opacityColor2?: number; 16 + } & ViewProps; 17 + 18 + function Gradient({ 19 + children, 20 + fromColor, 21 + toColor, 22 + height = "100%", 23 + opacityColor1 = 1, 24 + opacityColor2 = 1, 25 + ...otherViewProps 26 + }: GradientProps) { 27 + const gradientUniqueId = `grad${fromColor}+${toColor}`.replace( 28 + /[^a-zA-Z0-9 ]/g, 29 + "", 30 + ); 31 + return ( 32 + <> 33 + <View 34 + style={[ 35 + { 36 + ...StyleSheet.absoluteFillObject, 37 + height, 38 + zIndex: -1, 39 + top: 0, 40 + left: 0, 41 + }, 42 + otherViewProps.style, 43 + ]} 44 + {...otherViewProps} 45 + > 46 + <Svg height="100%" width="100%" style={StyleSheet.absoluteFillObject}> 47 + <Defs> 48 + <LinearGradient 49 + id={gradientUniqueId} 50 + x1="0%" 51 + y1="0%" 52 + x2="0%" 53 + y2="100%" 54 + > 55 + <Stop 56 + offset="0" 57 + stopColor={fromColor} 58 + stopOpacity={opacityColor1} 59 + /> 60 + <Stop 61 + offset="1" 62 + stopColor={toColor} 63 + stopOpacity={opacityColor2} 64 + /> 65 + </LinearGradient> 66 + </Defs> 67 + <Rect width="100%" height="100%" fill={`url(#${gradientUniqueId})`} /> 68 + </Svg> 69 + </View> 70 + {children} 71 + </> 72 + ); 73 + } 74 + 75 + export const AnimatedGradient = Animated.createAnimatedComponent(Gradient); 76 + 77 + export default Gradient;