├── .gitignore ├── README.md ├── app.json ├── app ├── _layout.tsx ├── home.tsx └── index.tsx ├── assets ├── fonts │ └── SpaceMono-Regular.ttf └── images │ ├── adaptive-icon.png │ ├── favicon.png │ ├── icon.png │ ├── partial-react-logo.png │ ├── react-logo.png │ ├── react-logo@2x.png │ ├── react-logo@3x.png │ └── splash.png ├── babel.config.js ├── components ├── Footer │ └── index.tsx ├── Header │ └── index.tsx ├── List │ └── index.tsx ├── ListFooter │ └── index.tsx └── ListHeader │ └── index.tsx ├── constants └── index.tsx ├── package-lock.json ├── package.json ├── tsconfig.json ├── types └── index.ts └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | 16 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb 17 | # The following patterns were generated by expo-cli 18 | 19 | expo-env.d.ts 20 | # @end expo-cli -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your Expo app 👋 2 | 3 | This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app). 4 | 5 | ## Get started 6 | 7 | 1. Install dependencies 8 | 9 | ```bash 10 | npm install 11 | ``` 12 | 13 | 2. Start the app 14 | 15 | ```bash 16 | npx expo start 17 | ``` 18 | 19 | In the output, you'll find options to open the app in a 20 | 21 | - [development build](https://docs.expo.dev/develop/development-builds/introduction/) 22 | - [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/) 23 | - [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/) 24 | - [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo 25 | 26 | You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction). 27 | 28 | ## Get a fresh project 29 | 30 | When you're ready, run: 31 | 32 | ```bash 33 | npm run reset-project 34 | ``` 35 | 36 | This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing. 37 | 38 | ## Learn more 39 | 40 | To learn more about developing your project with Expo, look at the following resources: 41 | 42 | - [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides). 43 | - [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web. 44 | 45 | ## Join the community 46 | 47 | Join our community of developers creating universal apps. 48 | 49 | - [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute. 50 | - [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions. 51 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "expo-drag-selection", 4 | "slug": "expo-drag-selection", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/images/icon.png", 8 | "scheme": "myapp", 9 | "userInterfaceStyle": "automatic", 10 | "splash": { 11 | "image": "./assets/images/splash.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "ios": { 16 | "supportsTablet": true, 17 | "infoPlist": { 18 | "UIViewControllerBasedStatusBarAppearance": true 19 | } 20 | }, 21 | "android": { 22 | "adaptiveIcon": { 23 | "foregroundImage": "./assets/images/adaptive-icon.png", 24 | "backgroundColor": "#ffffff" 25 | } 26 | }, 27 | "web": { 28 | "bundler": "metro", 29 | "output": "static", 30 | "favicon": "./assets/images/favicon.png" 31 | }, 32 | "plugins": [ 33 | ["expo-router"], 34 | [ 35 | "expo-media-library", 36 | { 37 | "photosPermission": "Allow $(PRODUCT_NAME) to access your photos.", 38 | "savePhotosPermission": "Allow $(PRODUCT_NAME) to save photos.", 39 | "isAccessMediaLocationEnabled": true 40 | } 41 | ] 42 | ], 43 | "experiments": { 44 | "typedRoutes": true 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | DarkTheme, 3 | DefaultTheme, 4 | ThemeProvider, 5 | } from "@react-navigation/native"; 6 | import { Stack } from "expo-router"; 7 | import { useColorScheme } from "react-native"; 8 | import { GestureHandlerRootView } from "react-native-gesture-handler"; 9 | import "react-native-reanimated"; 10 | 11 | export default function RootLayout() { 12 | const colorScheme = useColorScheme(); 13 | 14 | return ( 15 | 16 | 17 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /app/home.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | memo, 3 | useCallback, 4 | useEffect, 5 | useLayoutEffect, 6 | useRef, 7 | useState, 8 | } from "react"; 9 | import { RefreshControl, StyleSheet } from "react-native"; 10 | import { 11 | SafeAreaView, 12 | useSafeAreaInsets, 13 | } from "react-native-safe-area-context"; 14 | import Animated, { 15 | Extrapolation, 16 | interpolate, 17 | runOnJS, 18 | useAnimatedScrollHandler, 19 | useDerivedValue, 20 | useSharedValue, 21 | } from "react-native-reanimated"; 22 | import { useNavigation } from "expo-router"; 23 | import { COLUMNS, MIDDLE_MARGIN, EDGE_MARGIN } from "@/constants"; 24 | import ListHeaderComponent from "@/components/ListHeader"; 25 | import ListFooterComponent from "@/components/ListFooter"; 26 | import FooterComponent from "@/components/Footer"; 27 | import Header from "@/components/Header"; 28 | import List from "@/components/List"; 29 | import { IMAGE_TYPE, STATE_TYPE } from "@/types"; 30 | 31 | const Home = () => { 32 | const navigation = useNavigation(); 33 | const translationY = useSharedValue(-130); 34 | 35 | const toggleHeader = (isZero: boolean) => { 36 | if (isZero) { 37 | navigation.setOptions({ 38 | headerTintColor: undefined, 39 | }); 40 | } else { 41 | navigation.setOptions({ 42 | headerTintColor: "white", 43 | }); 44 | } 45 | }; 46 | 47 | useDerivedValue(() => { 48 | let translateValue = interpolate( 49 | translationY.value, 50 | [-125, -130], 51 | [1, 0], 52 | Extrapolation.CLAMP 53 | ); 54 | if (translateValue === 0) { 55 | runOnJS(toggleHeader)(true); 56 | } else { 57 | runOnJS(toggleHeader)(false); 58 | } 59 | }, []); 60 | 61 | const scrollHandler = useAnimatedScrollHandler((event) => { 62 | translationY.value = event.contentOffset.y; 63 | }); 64 | 65 | const { bottom } = useSafeAreaInsets(); 66 | 67 | const [state, setState] = useState({ 68 | imageData: [], 69 | refreshing: false, 70 | activeIndex: [], 71 | canSelect: false, 72 | }); 73 | 74 | useLayoutEffect(() => { 75 | navigation.setOptions({ 76 | title: "Wallpaper", 77 | headerRight: (props: { canGoBAck: boolean; tintColor: string }) => ( 78 |
82 | setState((prev) => ({ 83 | ...prev, 84 | canSelect: !prev.canSelect, 85 | activeIndex: [], 86 | })) 87 | } 88 | /> 89 | ), 90 | }); 91 | }, [navigation, state.canSelect]); 92 | 93 | useEffect(() => { 94 | getImages(); 95 | }, []); 96 | 97 | const onRefresh = () => { 98 | getImages(); 99 | }; 100 | 101 | const getImages = () => { 102 | setState((prev) => ({ ...prev, refreshing: true })); 103 | fetch("https://picsum.photos/v2/list?page=10&limit=100") 104 | .then((res) => res.json()) 105 | .then((res) => 106 | setState((prev) => ({ ...prev, imageData: res, refreshing: false })) 107 | ); 108 | }; 109 | 110 | const selectImage = (id: IMAGE_TYPE) => { 111 | if (state.canSelect) { 112 | if (state.activeIndex.includes(id)) { 113 | setState((prev) => ({ 114 | ...prev, 115 | activeIndex: prev.activeIndex.filter((i) => i !== id), 116 | })); 117 | } else { 118 | setState((prev) => ({ 119 | ...prev, 120 | activeIndex: [...prev.activeIndex, id], 121 | })); 122 | } 123 | } 124 | }; 125 | 126 | const renderItem = useCallback( 127 | (props: { item: IMAGE_TYPE; index: number }) => { 128 | return ( 129 | 134 | ); 135 | }, 136 | [state.activeIndex, state.canSelect] 137 | ); 138 | 139 | const listFooter = useCallback(() => { 140 | return ; 141 | }, [state.imageData]); 142 | 143 | return ( 144 | 145 | 146 | {state.imageData?.length > 0 && ( 147 | val.id} 160 | refreshControl={ 161 | 165 | } 166 | /> 167 | )} 168 | {state.activeIndex.length > 0 && ( 169 | 172 | setState((prev) => ({ 173 | ...prev, 174 | activeIndex: prev.activeIndex.filter((item) => item.id !== id), 175 | })) 176 | } 177 | /> 178 | )} 179 | 180 | ); 181 | }; 182 | 183 | export default memo(Home); 184 | 185 | const styles = StyleSheet.create({ 186 | flexOne: { 187 | flex: 1, 188 | }, 189 | listContainerStyle: { 190 | padding: EDGE_MARGIN, 191 | gap: MIDDLE_MARGIN, 192 | }, 193 | gap: { 194 | gap: MIDDLE_MARGIN, 195 | }, 196 | }); 197 | -------------------------------------------------------------------------------- /app/index.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, Text, View } from "react-native"; 2 | import React from "react"; 3 | import { Link } from "expo-router"; 4 | import { useTheme } from "@react-navigation/native"; 5 | 6 | const Home = () => { 7 | const theme = useTheme(); 8 | return ( 9 | 10 | 11 | Album 12 | 13 | Home 14 | 15 | ); 16 | }; 17 | 18 | export default Home; 19 | 20 | const styles = StyleSheet.create({ 21 | container: { 22 | flex: 1, 23 | justifyContent: "center", 24 | alignItems: "center", 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunabhverma/expo-selection/60b9429cfdaec1130dabf35067df767b9192a9b1/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunabhverma/expo-selection/60b9429cfdaec1130dabf35067df767b9192a9b1/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunabhverma/expo-selection/60b9429cfdaec1130dabf35067df767b9192a9b1/assets/images/favicon.png -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunabhverma/expo-selection/60b9429cfdaec1130dabf35067df767b9192a9b1/assets/images/icon.png -------------------------------------------------------------------------------- /assets/images/partial-react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunabhverma/expo-selection/60b9429cfdaec1130dabf35067df767b9192a9b1/assets/images/partial-react-logo.png -------------------------------------------------------------------------------- /assets/images/react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunabhverma/expo-selection/60b9429cfdaec1130dabf35067df767b9192a9b1/assets/images/react-logo.png -------------------------------------------------------------------------------- /assets/images/react-logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunabhverma/expo-selection/60b9429cfdaec1130dabf35067df767b9192a9b1/assets/images/react-logo@2x.png -------------------------------------------------------------------------------- /assets/images/react-logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunabhverma/expo-selection/60b9429cfdaec1130dabf35067df767b9192a9b1/assets/images/react-logo@3x.png -------------------------------------------------------------------------------- /assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunabhverma/expo-selection/60b9429cfdaec1130dabf35067df767b9192a9b1/assets/images/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /components/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import { LayoutChangeEvent, Pressable, StyleSheet } from "react-native"; 3 | import { useTheme } from "@react-navigation/native"; 4 | import { useSafeAreaInsets } from "react-native-safe-area-context"; 5 | import Animated, { 6 | FadeIn, 7 | FadeOut, 8 | LinearTransition, 9 | ZoomOut, 10 | useAnimatedStyle, 11 | useSharedValue, 12 | withTiming, 13 | } from "react-native-reanimated"; 14 | import { IMAGE_TYPE } from "@/types"; 15 | import { LinearGradient } from "expo-linear-gradient"; 16 | import { Image } from "expo-image"; 17 | 18 | const ITEM_GAP = 10; 19 | const CONTAINER_MARGIN = 20; 20 | const BORDER_RADIUS = 10; 21 | 22 | const FooterComponent = ({ 23 | activeImageIndex, 24 | toggleImage, 25 | }: { 26 | activeImageIndex: IMAGE_TYPE[]; 27 | toggleImage: (item: string) => void; 28 | }) => { 29 | const scrollViewRef = useRef(null); 30 | const previousContentWidth = useRef(0); 31 | const itemWidth = useSharedValue(0); 32 | const { colors } = useTheme(); 33 | const { bottom } = useSafeAreaInsets(); 34 | const scrollViewPadding = useSharedValue(10); 35 | const [data, setData] = useState([]); 36 | 37 | useEffect(() => { 38 | if (data.length !== activeImageIndex.length) { 39 | setData(activeImageIndex); 40 | if (data.length > activeImageIndex.length) { 41 | scrollViewPadding.value += itemWidth.value + ITEM_GAP; 42 | } 43 | } 44 | }, [activeImageIndex]); 45 | 46 | useEffect(() => { 47 | scrollViewPadding.value = withTiming(10); 48 | }, [data]); 49 | 50 | const onLayout = (e: LayoutChangeEvent) => { 51 | const { width } = e.nativeEvent.layout; 52 | itemWidth.value = width; 53 | }; 54 | 55 | const animatedPadding = useAnimatedStyle(() => ({ 56 | marginRight: scrollViewPadding.value, 57 | })); 58 | 59 | return ( 60 | 66 | 70 | { 75 | if (contentWidth > previousContentWidth.current) { 76 | scrollViewRef.current?.scrollToEnd({ animated: true }); 77 | } 78 | previousContentWidth.current = contentWidth; 79 | }} 80 | horizontal 81 | contentContainerStyle={styles.flexGrow} 82 | > 83 | 87 | {data.map((item) => ( 88 | 95 | toggleImage(item.id)}> 96 | 100 | 101 | 102 | ))} 103 | 104 | 105 | 106 | ); 107 | }; 108 | 109 | const styles = StyleSheet.create({ 110 | mainContainer: { 111 | position: "absolute", 112 | bottom: 0, 113 | width: "100%", 114 | zIndex: 10, 115 | shadowColor: "#000", 116 | shadowOffset: { 117 | width: 0, 118 | height: 10, 119 | }, 120 | shadowOpacity: 0.51, 121 | shadowRadius: 13.16, 122 | elevation: 20, 123 | }, 124 | flexGrow: { 125 | flexGrow: 1, 126 | }, 127 | rowContainer: { 128 | gap: ITEM_GAP, 129 | margin: CONTAINER_MARGIN, 130 | flexDirection: "row", 131 | }, 132 | imageStyle: { 133 | width: 50, 134 | aspectRatio: 1, 135 | borderWidth: 1, 136 | borderRadius: BORDER_RADIUS, 137 | }, 138 | }); 139 | 140 | export default FooterComponent; 141 | -------------------------------------------------------------------------------- /components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { 3 | Pressable, 4 | StyleSheet, 5 | View, 6 | Text, 7 | ViewStyle, 8 | useColorScheme, 9 | useWindowDimensions, 10 | } from "react-native"; 11 | import { Ionicons } from "@expo/vector-icons"; 12 | import { BlurView } from "expo-blur"; 13 | import Animated, { 14 | FadeIn, 15 | FadeOut, 16 | LinearTransition, 17 | } from "react-native-reanimated"; 18 | import { HEADER_PROPS } from "@/types"; 19 | 20 | const HeaderButton = ({ 21 | containerStyle, 22 | children, 23 | onPress, 24 | }: { 25 | containerStyle?: ViewStyle; 26 | children: React.ReactNode; 27 | onPress?: () => void; 28 | }) => { 29 | const tint = useColorScheme() || undefined; 30 | return ( 31 | 32 | 33 | 34 | {children} 35 | 36 | 37 | ); 38 | }; 39 | 40 | const Header = ({ tintColor, canSelect, toggleCanSelect }: HEADER_PROPS) => { 41 | const { width } = useWindowDimensions(); 42 | return ( 43 | 48 | 52 | 58 | toggleCanSelect()} 60 | containerStyle={styles.headerButtonContainer} 61 | > 62 | 63 | {canSelect ? "Cancel" : "Select"} 64 | 65 | 66 | 67 | {!canSelect && ( 68 | 73 | 74 | 79 | 80 | 81 | )} 82 | 83 | 84 | ); 85 | }; 86 | 87 | const styles = StyleSheet.create({ 88 | headerContainer: { 89 | flexDirection: "row", 90 | gap: 5, 91 | justifyContent: "flex-end", 92 | alignItems: "center", 93 | }, 94 | elevation: { 95 | zIndex: 1, 96 | }, 97 | headerButtonContainer: { 98 | backgroundColor: "rgba(100, 100, 100, 0.2)", 99 | borderRadius: 20, 100 | overflow: "hidden", 101 | height: 28, 102 | justifyContent: "center", 103 | alignItems: "center", 104 | paddingHorizontal: 10, 105 | }, 106 | buttonContainer: { 107 | backgroundColor: "rgba(100, 100, 100, 0.2)", 108 | borderRadius: 20, 109 | overflow: "hidden", 110 | height: 28, 111 | aspectRatio: 1, 112 | justifyContent: "center", 113 | alignItems: "center", 114 | }, 115 | text: { 116 | fontSize: 14, 117 | fontWeight: "500", 118 | }, 119 | }); 120 | 121 | export default Header; 122 | -------------------------------------------------------------------------------- /components/List/index.tsx: -------------------------------------------------------------------------------- 1 | import { IMAGE_WIDTH } from "@/constants"; 2 | import { LIST_PROPS } from "@/types"; 3 | import { useTheme } from "@react-navigation/native"; 4 | import { Image } from "expo-image"; 5 | import { Pressable, StyleSheet, Text, View } from "react-native"; 6 | import Animated, { 7 | LinearTransition, 8 | ZoomIn, 9 | ZoomOut, 10 | } from "react-native-reanimated"; 11 | 12 | const AnimatedImage = Animated.createAnimatedComponent(Image); 13 | 14 | const List = ({ item, selectImage, activeImageIndex }: LIST_PROPS) => { 15 | const theme = useTheme(); 16 | const isActive = activeImageIndex.includes(item); 17 | const findIndex = activeImageIndex.indexOf(item); 18 | 19 | return ( 20 | selectImage(item)}> 21 | 24 | {isActive && ( 25 | 30 | 31 | {findIndex + 1} 32 | 33 | 34 | )} 35 | 47 | 48 | 49 | ); 50 | }; 51 | 52 | const styles = StyleSheet.create({ 53 | itemContainer: { 54 | aspectRatio: 1, 55 | position: "relative", 56 | width: IMAGE_WIDTH, 57 | }, 58 | closeIcon: { 59 | right: 5, 60 | top: 5, 61 | backgroundColor: "dodgerblue", 62 | position: "absolute", 63 | zIndex: 1, 64 | minWidth: 25, 65 | height: 25, 66 | borderRadius: 50, 67 | borderWidth: 2, 68 | justifyContent: "center", 69 | alignItems: "center", 70 | }, 71 | numberStyle: { 72 | fontSize: 13, 73 | fontWeight: "500", 74 | }, 75 | imageStyle: { 76 | width: IMAGE_WIDTH, 77 | aspectRatio: 1, 78 | flex: 1, 79 | }, 80 | }); 81 | 82 | export default List; 83 | -------------------------------------------------------------------------------- /components/ListFooter/index.tsx: -------------------------------------------------------------------------------- 1 | import { MIDDLE_MARGIN } from "@/constants"; 2 | import { useTheme } from "@react-navigation/native"; 3 | import { Text, View } from "react-native"; 4 | 5 | const ListFooterComponent = ({ imageLength }: { imageLength: number }) => { 6 | const theme = useTheme(); 7 | return ( 8 | 15 | 23 | {imageLength} Wallpapers 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default ListFooterComponent; 30 | -------------------------------------------------------------------------------- /components/ListHeader/index.tsx: -------------------------------------------------------------------------------- 1 | import { useHeaderHeight } from "@react-navigation/elements"; 2 | import { LinearGradient } from "expo-linear-gradient"; 3 | import Animated, { 4 | Extrapolation, 5 | SharedValue, 6 | interpolate, 7 | useAnimatedStyle, 8 | withTiming, 9 | } from "react-native-reanimated"; 10 | 11 | const ListHeaderComponent = ({ 12 | translationY, 13 | }: { 14 | translationY: SharedValue; 15 | }) => { 16 | const headerSize = useHeaderHeight(); 17 | const animatedStyle = useAnimatedStyle(() => { 18 | let translateValue = interpolate( 19 | translationY.value, 20 | [-125, -130], 21 | [1, 0], 22 | Extrapolation.CLAMP 23 | ); 24 | return { 25 | opacity: withTiming(translateValue), 26 | }; 27 | }); 28 | 29 | return ( 30 | 43 | 47 | 48 | ); 49 | }; 50 | 51 | export default ListHeaderComponent; 52 | -------------------------------------------------------------------------------- /constants/index.tsx: -------------------------------------------------------------------------------- 1 | import { Dimensions } from "react-native"; 2 | const width = Dimensions.get("window").width; 3 | 4 | const COLUMN_SIZE = width / 3; 5 | const MIDDLE_MARGIN = 5; 6 | const EDGE_MARGIN = 0; 7 | const IMAGE_SPACING = (2 * MIDDLE_MARGIN + 2 * EDGE_MARGIN) / 3; 8 | const IMAGE_WIDTH = COLUMN_SIZE - IMAGE_SPACING; 9 | const COLUMNS = 3; 10 | 11 | export { 12 | COLUMNS, 13 | COLUMN_SIZE, 14 | MIDDLE_MARGIN, 15 | EDGE_MARGIN, 16 | IMAGE_SPACING, 17 | IMAGE_WIDTH, 18 | }; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expo-drag-selection", 3 | "main": "expo-router/entry", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "start": "expo start", 7 | "reset-project": "node ./scripts/reset-project.js", 8 | "android": "expo start --android", 9 | "ios": "expo start --ios", 10 | "web": "expo start --web", 11 | "test": "jest --watchAll", 12 | "lint": "expo lint" 13 | }, 14 | "jest": { 15 | "preset": "jest-expo" 16 | }, 17 | "dependencies": { 18 | "@expo/vector-icons": "^14.0.0", 19 | "@react-navigation/elements": "^1.3.30", 20 | "@react-navigation/native": "^6.0.2", 21 | "expo": "~51.0.2", 22 | "expo-blur": "~13.0.2", 23 | "expo-constants": "~16.0.1", 24 | "expo-font": "~12.0.4", 25 | "expo-image": "~1.12.9", 26 | "expo-linear-gradient": "~13.0.2", 27 | "expo-linking": "~6.3.1", 28 | "expo-media-library": "~16.0.3", 29 | "expo-router": "~3.5.11", 30 | "expo-splash-screen": "~0.27.4", 31 | "expo-status-bar": "~1.12.1", 32 | "expo-system-ui": "~3.0.4", 33 | "expo-web-browser": "~13.0.3", 34 | "react": "18.2.0", 35 | "react-dom": "18.2.0", 36 | "react-native": "0.74.1", 37 | "react-native-gesture-handler": "~2.16.1", 38 | "react-native-reanimated": "~3.10.1", 39 | "react-native-safe-area-context": "4.10.1", 40 | "react-native-screens": "3.31.1", 41 | "react-native-web": "~0.19.10" 42 | }, 43 | "devDependencies": { 44 | "@babel/core": "^7.20.0", 45 | "@types/jest": "^29.5.12", 46 | "@types/react": "~18.2.45", 47 | "@types/react-test-renderer": "^18.0.7", 48 | "jest": "^29.2.1", 49 | "jest-expo": "~51.0.1", 50 | "react-test-renderer": "18.2.0", 51 | "typescript": "~5.3.3" 52 | }, 53 | "private": true 54 | } 55 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "paths": { 6 | "@/*": [ 7 | "./*" 8 | ] 9 | } 10 | }, 11 | "include": [ 12 | "**/*.ts", 13 | "**/*.tsx", 14 | ".expo/types/**/*.ts", 15 | "expo-env.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /types/index.ts: -------------------------------------------------------------------------------- 1 | export interface IMAGE_TYPE { 2 | author: string; 3 | download_url: string; 4 | height: number; 5 | id: string; 6 | url: string; 7 | width: number; 8 | } 9 | 10 | export interface STATE_TYPE { 11 | imageData: IMAGE_TYPE[]; 12 | refreshing: boolean; 13 | activeIndex: IMAGE_TYPE[]; 14 | canSelect: boolean; 15 | } 16 | 17 | export interface LIST_PROPS { 18 | item: IMAGE_TYPE; 19 | index: number; 20 | selectImage: (item: IMAGE_TYPE) => void; 21 | activeImageIndex: IMAGE_TYPE[]; 22 | } 23 | 24 | export interface HEADER_PROPS { 25 | tintColor: string; 26 | canSelect: boolean; 27 | toggleCanSelect: () => void; 28 | } 29 | --------------------------------------------------------------------------------