[READ ONLY MIRROR] Open Source TikTok alternative built on AT Protocol github.com/sprksocial/client
flutter atproto video dart
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

Commit

C3B 328fa55a 85491edc

+127 -43
+26
app/(auth)/Register.tsx
··· 1 + import { ThemedText } from '@/components/ThemedText'; 2 + import React from 'react'; 3 + import { View, StyleSheet } from 'react-native'; 4 + 5 + export default function Register() { 6 + 7 + const styles = StyleSheet.create({ 8 + container: { 9 + flex: 1, 10 + alignItems: 'center', 11 + justifyContent: 'center', 12 + backgroundColor: '#fff', 13 + }, 14 + text: { 15 + fontSize: 18, 16 + fontWeight: 'bold', 17 + color: '#333', 18 + }, 19 + }); 20 + 21 + return ( 22 + <View style={styles.container}> 23 + <ThemedText style={styles.text}>Hello from Register!</ThemedText> 24 + </View> 25 + ); 26 + }
app/(screens)/Login.tsx app/(auth)/Login.tsx
+2
app/(tabs)/index.tsx
··· 12 12 import { PostProps } from '@/types/Interfaces'; 13 13 import { fetchTrendingPosts } from '@/api/videoServices'; 14 14 import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; 15 + import { router } from 'expo-router'; 15 16 16 17 export default function HomeScreen() { 17 18 const flatListRef = useRef<FlatList<PostProps>>(null); ··· 54 55 55 56 loadContent(); 56 57 }, []); 58 + 57 59 58 60 return ( 59 61 <ThemedView >
+14 -13
app/_layout.tsx
··· 1 - //_layout.tsx 2 1 import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; 3 2 import { useFonts } from 'expo-font'; 4 3 import { Stack } from 'expo-router'; 5 4 import * as SplashScreen from 'expo-splash-screen'; 6 5 import { StatusBar } from 'expo-status-bar'; 7 - import { useEffect } from 'react'; 8 - import 'react-native-reanimated'; 6 + import { useEffect, useState } from 'react'; 7 + import { GestureHandlerRootView } from 'react-native-gesture-handler'; 9 8 10 9 import { useColorScheme } from '@/hooks/useColorScheme'; 11 - import { GestureHandlerRootView } from 'react-native-gesture-handler'; 12 10 13 11 SplashScreen.preventAutoHideAsync(); 14 12 ··· 29 27 } 30 28 31 29 return ( 32 - <GestureHandlerRootView> 33 - <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> 34 - <Stack> 35 - <Stack.Screen name="(tabs)" options={{ headerShown: false }} /> 36 - <Stack.Screen name="+not-found" /> 37 - </Stack> 38 - <StatusBar style="auto" /> 39 - </ThemeProvider> 30 + <GestureHandlerRootView style={{ flex: 1 }}> 31 + <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> 32 + <Stack screenOptions={{ headerShown: false }}> 33 + {/* Auth Screens */} 34 + <Stack.Screen name="(auth)/Login" /> 35 + <Stack.Screen name="(auth)/Register" /> 36 + {/* Main App Screens */} 37 + <Stack.Screen name="(tabs)" /> 38 + </Stack> 39 + <StatusBar style="auto" /> 40 + </ThemeProvider> 40 41 </GestureHandlerRootView> 41 42 ); 42 - } 43 + }
+82 -27
components/Video/VideoTop.tsx
··· 7 7 8 8 import { Switch } from 'react-native-gesture-handler'; 9 9 import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; 10 + import ActionButton from '../global/ActionButton'; 10 11 11 12 const VideoTop: React.FC = () => { 12 13 const height = Dimensions.get('screen').height; ··· 14 15 const colorScheme: 'light' | 'dark' = useColorScheme() as 'light' | 'dark'; 15 16 const [optionsHeight, setOptionsHeight] = useState(0); 16 17 17 - const [customForYou, setCustomForYou] = useState(false); 18 18 const [suggestiveContent, setSuggestiveContent] = useState(false); 19 19 const [nudity, setNudity] = useState(false); 20 20 const [violence, setViolence] = useState(false); ··· 137 137 paddingHorizontal: 10, 138 138 flexDirection: 'column', 139 139 alignItems: 'center', 140 - marginVertical: 10, 141 140 }, 142 141 topNav: { 143 142 top: height * 0.09, ··· 147 146 alignItems: 'center', 148 147 position: 'absolute', 149 148 zIndex: 1, 149 + gap: 10, 150 150 }, 151 151 optionsContent: { 152 152 width: '100%', ··· 174 174 }; 175 175 176 176 177 - const [customFeeds, setCustomFeeds] = useState([ 177 + type FeedWeights = { 178 + [key: string]: number; 179 + }; 180 + 181 + const [customFeeds, setCustomFeeds] = useState<Array<{ 182 + name: string; 183 + enabled: boolean; 184 + origin: string; 185 + weights: FeedWeights; 186 + }>>([ 178 187 { 179 188 name: 'For You', 180 189 enabled: true, 190 + origin: 'spark', 181 191 weights: { 182 192 comedy: 50, 183 193 news: 50, ··· 187 197 }, 188 198 { 189 199 name: 'Sports', 190 - enabled: true, 200 + enabled: false, 201 + origin: 'spark', 191 202 weights: { 192 203 football: 50, 193 204 basketball: 50, ··· 197 208 }, 198 209 { 199 210 name: 'Cute Cats', 200 - enabled: true, 211 + enabled: false, 212 + origin: 'spark', 201 213 weights: { 202 214 kittens: 50, 203 215 cats: 50, ··· 211 223 color: string, 212 224 title: string, 213 225 value: boolean, 214 - onValueChange: () => void) => { 226 + onValueChange: () => void, 227 + key: string, 228 + ) => { 215 229 return ( 216 - <View style={styles.feedOption}> 230 + <View style={styles.feedOption} key={key}> 217 231 <Switch value={value} 218 232 onValueChange={onValueChange} 219 233 trackColor={{ false: Colors[colorScheme ?? 'light'].underlineColor, true: color }} ··· 231 245 max: number, 232 246 step: number, 233 247 onValueChange: (value: number) => void, 234 - 248 + sliderKey: string 235 249 ) => { 236 250 return ( 237 - <View style={styles.feedWeight}> 251 + <View style={styles.feedWeight} key={sliderKey}> 238 252 <ThemedText>{title}</ThemedText> 239 253 <Slider 240 254 style={{ width: '100%', height: 40 }} ··· 250 264 ); 251 265 }; 252 266 267 + const generateFeedTab = ( 268 + title: string 269 + ) => { 270 + return ( 271 + <TouchableOpacity> 272 + <ThemedText type='defaultBold' darkColor={Colors.dark.text} lightColor={Colors.dark.text} style={styles.text}>{title}</ThemedText> 273 + </TouchableOpacity> 274 + ); 275 + }; 253 276 254 277 return ( 255 278 <View style={styles.container}> ··· 282 305 283 306 <View style={styles.feedOptions}> 284 307 <ThemedText type='subtitle' lightColor={Colors.dark.underlineColor} darkColor={Colors.light.underlineColor}>Filters</ThemedText> 285 - {generateSwitch("#0085FF", "Suggestive Content", suggestiveContent, () => setSuggestiveContent(!suggestiveContent))} 286 - {generateSwitch("#0085FF", "Nudity", nudity, () => setNudity(!nudity))} 287 - {generateSwitch("#0085FF", "Violence", violence, () => setViolence(!violence))} 308 + {generateSwitch("#0085FF", "Suggestive Content", suggestiveContent, () => setSuggestiveContent(!suggestiveContent), "suggestiveContent")} 309 + {generateSwitch("#0085FF", "Nudity", nudity, () => setNudity(!nudity), "nudity")} 310 + {generateSwitch("#0085FF", "Violence", violence, () => setViolence(!violence), "violence")} 288 311 </View> 312 + 289 313 <View style={styles.feedOptions}> 290 314 <ThemedText type='subtitle' lightColor={Colors.dark.underlineColor} darkColor={Colors.light.underlineColor}>Custom Feeds</ThemedText> 291 - { } 292 - {generateSwitch("#0085FF", "For You", customForYou, () => setCustomForYou(!customForYou))} 293 - <> 294 - {generateSlider("#0085FF", "Comedy", 50, 0, 100, 1, (value) => console.log(value))} 295 - {generateSlider("#0085FF", "News", 50, 0, 100, 1, (value) => console.log(value))} 296 - {generateSlider("#0085FF", "Trending", 50, 0, 100, 1, (value) => console.log(value))} 297 - {generateSlider("#0085FF", "Following", 50, 0, 100, 1, (value) => console.log(value))} 298 - </> 299 - <TouchableOpacity style={styles.addFeedOption} onPress={handleAddCustomFeed}> 300 - <Ionicons name="add" size={24} color={Colors.dark.text} /> 301 - <ThemedText style={{ color: Colors.dark.text }}>Add Custom Feed</ThemedText> 302 - </TouchableOpacity> 315 + { 316 + customFeeds.map((feed, index) => ( 317 + <> 318 + {generateSwitch("#0085FF", feed.name, feed.enabled, () => { 319 + const newFeeds = customFeeds.slice(); 320 + newFeeds[index].enabled = !newFeeds[index].enabled; 321 + setCustomFeeds(newFeeds); 322 + }, `customFeedSwitch_${index}`)} 323 + { 324 + feed.enabled && 325 + Object.keys(feed.weights).map((weightKey, weightIndex) => ( 326 + generateSlider( 327 + "#0085FF", 328 + weightKey, 329 + feed.weights[weightKey], 330 + 0, 331 + 100, 332 + 1, 333 + (value) => { 334 + const newFeeds = customFeeds.slice(); 335 + newFeeds[index].weights[weightKey] = value; 336 + setCustomFeeds(newFeeds); 337 + }, 338 + `customFeedSlider_${index}_${weightKey}` 339 + ) 340 + )) 341 + } 342 + </> 343 + )) 344 + } 345 + 346 + <ActionButton title='Add Custom Feed' onPress={handleAddCustomFeed} icon='add' width={'100%'}/> 303 347 </View> 348 + <View style={styles.feedOptions}> 349 + <ThemedText type='subtitle' lightColor={Colors.dark.underlineColor} darkColor={Colors.light.underlineColor}>Content Types</ThemedText> 350 + {generateSwitch("#0085FF", "Videos", true, () => {}, "videos")} 351 + {generateSwitch("#0085FF", "More Videos", true, () => {}, "moreVideos")} 352 + {generateSwitch("#0085FF", "Photos", true, () => {}, "photos")} 353 + {generateSwitch("#0085FF", "More Photos", true, () => {}, "morePhotos")} 354 + </View> 304 355 305 356 </ScrollView> 306 357 </SafeAreaView> 307 358 <View style={styles.topNav}> 308 - <TouchableOpacity> 309 - <ThemedText type='defaultBold' darkColor={Colors.dark.text} lightColor={Colors.dark.text} style={styles.text}>For You</ThemedText> 310 - </TouchableOpacity> 359 + { 360 + customFeeds.map((feed, index) => ( 361 + feed.enabled && 362 + generateFeedTab(feed.name) 363 + )) 364 + } 365 + 311 366 <TouchableOpacity style={styles.filtersButton} onPress={handleOpenOptions}> 312 367 <Ionicons name="filter" size={24} color={Colors.dark.text} lightColor={Colors.dark.text} /> 313 368 </TouchableOpacity>
+3 -3
components/global/ActionButton.tsx
··· 1 1 import { Colors } from '@/constants/Colors'; 2 2 import { Ionicons } from '@expo/vector-icons'; 3 3 import React from 'react'; 4 - import { TouchableOpacity, Text, StyleSheet, ActivityIndicator, useColorScheme } from 'react-native'; 4 + import { TouchableOpacity, Text, StyleSheet, ActivityIndicator, useColorScheme, DimensionValue } from 'react-native'; 5 5 6 6 interface ActionButtonProps { 7 7 title: string; ··· 9 9 type?: 'primary' | 'secondary' | 'outline' | 'disabled'; 10 10 icon?: string; 11 11 isLoading?: boolean; 12 - width?: number; 12 + width?: string | number; 13 13 } 14 14 15 15 const ActionButton: React.FC<ActionButtonProps> = ({ ··· 61 61 style={[ 62 62 styles.button, 63 63 styles[type], 64 - { width: width }, 64 + { width: width as DimensionValue }, 65 65 ]} 66 66 onPress={onPress} 67 67 disabled={type === 'disabled' || isLoading}