Skip to content

Commit

Permalink
feat(mobile): add reusable MediaCarousel component
Browse files Browse the repository at this point in the history
- Created a new MediaCarousel component to handle multiple media types in a scrollable view
- Implemented dynamic indicator for multi-image carousels
- Refactored EntryGridItem to use the new MediaCarousel component
- Exposed PreviewImageProps interface for better type compatibility

Signed-off-by: Innei <tukon479@gmail.com>
  • Loading branch information
Innei committed Feb 21, 2025
1 parent ae89d8d commit 48461f1
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 49 deletions.
106 changes: 106 additions & 0 deletions apps/mobile/src/components/ui/carousel/Carousel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { useEffect, useState } from "react"
import { ScrollView, View } from "react-native"
import Animated, {
interpolateColor,
useAnimatedStyle,
useSharedValue,
withSpring,
} from "react-native-reanimated"

import type { MediaModel } from "@/src/database/schemas/types"

import { ImageContextMenu } from "../image/ImageContextMenu"
import type { PreviewImageProps } from "../image/PreviewImage"
import { PreviewImage } from "../image/PreviewImage"

export const MediaCarousel = ({
media,
onPreview,
aspectRatio,
Accessory,
AccessoryProps,
}: {
media: MediaModel[]
onPreview: () => void
aspectRatio: number
} & Pick<PreviewImageProps, "Accessory" | "AccessoryProps">) => {
const [containerWidth, setContainerWidth] = useState(0)
const hasMany = media.length > 1

// const activeIndex = useSharedValue(0)
const [activeIndex, setActiveIndex] = useState(0)

return (
<View
onLayout={(e) => {
setContainerWidth(e.nativeEvent.layout.width)
}}
>
<ScrollView
onScroll={(e) => {
setActiveIndex(Math.round(e.nativeEvent.contentOffset.x / containerWidth))
}}
scrollEventThrottle={16}
scrollEnabled={hasMany}
horizontal
showsHorizontalScrollIndicator={false}
pagingEnabled
className="flex-1"
contentContainerClassName="flex-row"
style={{ aspectRatio }}
>
{media.map((m, index) => {
if (m.type === "photo") {
return (
<View key={index} className="relative" style={{ width: containerWidth }}>
<ImageContextMenu imageUrl={m.url}>
<PreviewImage
onPreview={onPreview}
imageUrl={m.url}
aspectRatio={m.width && m.height ? m.width / m.height : 1}
Accessory={Accessory}
AccessoryProps={AccessoryProps}
/>
</ImageContextMenu>
</View>
)
}

return (
<PreviewImage
key={index}
onPreview={() => {
// open player
}}
imageUrl={m.url}
aspectRatio={m.width && m.height ? m.width / m.height : 1}
/>
)
})}
</ScrollView>
{/* Indicators */}
{hasMany && (
<View className="absolute inset-x-0 bottom-0 flex-row items-center justify-center gap-1">
{media.map((_, index) => (
<Indicator key={index} index={index} activeIndex={activeIndex} />
))}
</View>
)}
</View>
)
}

const Indicator = ({ index, activeIndex }: { index: number; activeIndex: number }) => {
const activeValue = useSharedValue(0)
useEffect(() => {
activeValue.value = withSpring(index === activeIndex ? 1 : 0)
}, [activeIndex, activeValue, index])
const animatedStyle = useAnimatedStyle(() => ({
backgroundColor: interpolateColor(
activeValue.value,
[0, 1],
["rgba(0, 0, 0, 0.5)", "rgba(255, 255, 255, 0.9)"],
),
}))
return <Animated.View className="h-1 flex-1 rounded-sm" style={animatedStyle} />
}
2 changes: 1 addition & 1 deletion apps/mobile/src/components/ui/image/PreviewImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Pressable, View } from "react-native"

import { usePreviewImage } from "./PreviewPageProvider"

interface PreviewImageProps {
export interface PreviewImageProps {
imageUrl: string
blurhash?: string | undefined
aspectRatio: number
Expand Down
58 changes: 10 additions & 48 deletions apps/mobile/src/modules/entry-list/templates/EntryGridItem.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FeedViewType } from "@follow/constants"
import { uniqBy } from "es-toolkit/compat"
import { LinearGradient } from "expo-linear-gradient"
import { useEffect, useMemo, useState } from "react"
import { useEffect, useMemo } from "react"
import { ScrollView, Text, View } from "react-native"
import Animated, { useSharedValue, withTiming } from "react-native-reanimated"
import { useSafeAreaInsets } from "react-native-safe-area-context"
Expand All @@ -11,9 +11,9 @@ import {
EntryContentWebView,
setWebViewEntry,
} from "@/src/components/native/webview/EntryContentWebView"
import { MediaCarousel } from "@/src/components/ui/carousel/Carousel"
import { RelativeDateTime } from "@/src/components/ui/datetime/RelativeDateTime"
import { FeedIcon } from "@/src/components/ui/icon/feed-icon"
import { ImageContextMenu } from "@/src/components/ui/image/ImageContextMenu"
import { PreviewImage } from "@/src/components/ui/image/PreviewImage"
import { ItemPressable } from "@/src/components/ui/pressable/ItemPressable"
import type { MediaModel } from "@/src/database/schemas/types"
Expand Down Expand Up @@ -88,7 +88,7 @@ const MediaItems = ({
title: string
}) => {
const firstMedia = media[0]
const [containerWidth, setContainerWidth] = useState(0)

const uniqMedia = useMemo(() => {
return uniqBy(media, "url")
}, [media])
Expand Down Expand Up @@ -119,51 +119,13 @@ const MediaItems = ({
}

return (
<View
onLayout={({ nativeEvent }) => {
setContainerWidth(nativeEvent.layout.width)
}}
>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
pagingEnabled
className="flex-1"
contentContainerClassName="flex-row"
style={{ aspectRatio }}
>
{uniqMedia.map((m, index) => {
if (m.type === "photo") {
return (
<View key={index} className="relative" style={{ width: containerWidth }}>
<ImageContextMenu imageUrl={m.url}>
<PreviewImage
onPreview={onPreview}
imageUrl={m.url}
aspectRatio={m.width && m.height ? m.width / m.height : 1}
Accessory={EntryGridItemAccessory}
AccessoryProps={{
id: entryId,
}}
/>
</ImageContextMenu>
</View>
)
}

return (
<PreviewImage
key={index}
onPreview={() => {
// open player
}}
imageUrl={m.url}
aspectRatio={m.width && m.height ? m.width / m.height : 1}
/>
)
})}
</ScrollView>
</View>
<MediaCarousel
media={uniqMedia}
onPreview={onPreview}
aspectRatio={aspectRatio}
Accessory={EntryGridItemAccessory}
AccessoryProps={{ id: entryId }}
/>
)
}

Expand Down

0 comments on commit 48461f1

Please sign in to comment.