implement base of image picker

This commit is contained in:
Hailey
2024-10-27 00:29:38 -07:00
parent 2b53ef2541
commit 3ef867d894
4 changed files with 263 additions and 21 deletions
@@ -10,7 +10,9 @@ Pod::Spec.new do |s|
s.static_framework = true
s.dependency 'ExpoModulesCore'
s.dependency 'SDWebImage', '~> 5.19.1'
s.dependency 'SDWebImageWebPCoder', '~> 0.14.6'
# Swift/Objective-C compatibility
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
@@ -42,5 +42,9 @@ public class ExpoPlatformInfoModule: Module {
}
}
}
AsyncFunction("isAnimatedImage") { (url: URL) in
return ImageInfo.isAnimatedImage(url: url)
}
}
}
@@ -0,0 +1,22 @@
//
// IsAnimatedImage.swift
// Pods
//
// Created by Hailey on 10/27/24.
//
import SDWebImage
class ImageInfo {
static func isAnimatedImage(url: URL) -> Bool {
var path: String
if #available(iOS 16.0, *) {
path = url.path()
} else {
path = url.path
}
let image = SDAnimatedImage(contentsOfFile: path)
return image?.sd_isAnimated ?? false
}
}
+234 -20
View File
@@ -1,6 +1,11 @@
/* eslint-disable react-native-a11y/has-valid-accessibility-ignores-invert-colors */
import React, {useCallback} from 'react'
import {msg} from '@lingui/macro'
import {Dimensions, View} from 'react-native'
import * as FS from 'expo-file-system'
import {Image} from 'expo-image'
import {Asset, AssetInfo, MediaType, PagedInfo} from 'expo-media-library'
import * as MediaLibrary from 'expo-media-library'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {usePhotoLibraryPermission} from '#/lib/hooks/usePermissions'
@@ -8,9 +13,13 @@ import {openPicker} from '#/lib/media/picker'
import {isNative} from '#/platform/detection'
import {ComposerImage, createComposerImage} from '#/state/gallery'
import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {useDialogControl} from '#/components/Dialog'
import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
import {Image_Stroke2_Corner0_Rounded as Image} from '#/components/icons/Image'
import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
import {Text} from '#/components/Typography'
type Props = {
size: number
@@ -24,24 +33,32 @@ export function SelectPhotoBtn({size, disabled, onAdd}: Props) {
const t = useTheme()
const sheetWrapper = useSheetWrapper()
const control = useDialogControl()
const onPressSelectPhotos = useCallback(async () => {
if (isNative && !(await requestPhotoAccessIfNeeded())) {
return
}
control.open()
const images = await sheetWrapper(
openPicker({
selectionLimit: 4 - size,
allowsMultipleSelection: true,
}),
)
const results = await Promise.all(
images.map(img => createComposerImage(img)),
)
onAdd(results)
}, [requestPhotoAccessIfNeeded, size, onAdd, sheetWrapper])
// if (isNative && !(await requestPhotoAccessIfNeeded())) {
// return
// }
//
// const images = await sheetWrapper(
// openPicker({
// selectionLimit: 4 - size,
// allowsMultipleSelection: true,
// }),
// )
//
// if (images.length === 0) {
// return
// }
//
// const results = await Promise.all(
// images.map(img => createComposerImage(img)),
// )
//
// onAdd(results)
}, [control])
return (
<Button
@@ -54,7 +71,204 @@ export function SelectPhotoBtn({size, disabled, onAdd}: Props) {
shape="round"
color="primary"
disabled={disabled}>
<Image size="lg" style={disabled && t.atoms.text_contrast_low} />
<ImageIcon size="lg" style={disabled && t.atoms.text_contrast_low} />
<Dialog.Outer
control={control}
nativeOptions={{minHeight: Dimensions.get('window').height}}>
<ImagePickerView control={control} />
</Dialog.Outer>
</Button>
)
}
function ImagePickerView({control}: {control: Dialog.DialogControlProps}) {
const [isLoading, setIsLoading] = React.useState(true)
const [pages, setPages] = React.useState<PagedInfo<Asset>[]>([])
const [selections, setSelections] = React.useState<AssetInfo[]>([])
const canPickMore = React.useMemo(() => {
if (selections.length === 0) {
return true
}
if (selections[0].mediaType === MediaType.video) {
return false
}
if (selections[0].mediaType === MediaType.photo && selections[0]) {
return selections.length < 4
}
}, [selections])
React.useEffect(() => {
;(async () => {
const items = await MediaLibrary.getAssetsAsync({
first: 30,
mediaType: [MediaType.video, MediaType.photo],
})
setPages([items])
setIsLoading(false)
})()
}, [])
const items = pages.flatMap(page => page.assets)
const slices = items.reduce<Asset[][]>((acc, item, i) => {
if (i % 3 === 0) {
acc.push([item])
} else {
acc[acc.length - 1].push(item)
}
return acc
}, [])
return (
<Dialog.ScrollableInner
label=""
contentContainerStyle={[a.px_0, a.pt_0]}
header={
<Dialog.Header
renderLeft={() => <CancelButton onPress={() => control.close()} />}
renderRight={() => <SaveButton onPress={() => {}} disabled={true} />}
style={[a.pb_sm]}>
<Dialog.HeaderText>
<Trans>Edit profile</Trans>
</Dialog.HeaderText>
</Dialog.Header>
}>
<View style={[a.flex_row, a.flex_wrap, a.gap_xs, a.justify_center]}>
{slices.map((slice, i) => (
<ImagePickerRow key={i} items={slice} isFirstRow={i === 0} />
))}
</View>
</Dialog.ScrollableInner>
)
}
function ImagePickerRow({
items,
isFirstRow,
}: {
items: Asset[]
isFirstRow: boolean
}) {
return (
<View style={[a.flex_row, a.flex_wrap, a.gap_xs]}>
{items.map((item, i) => (
<ImagePickerItem
key={item.id}
item={item}
index={i}
isFirstRow={isFirstRow}
/>
))}
</View>
)
}
const IMAGE_WIDTH = Dimensions.get('window').width / 3 - 8
function ImagePickerItem({
item,
index,
isFirstRow,
}: {
item: Asset
index: number
isFirstRow: boolean
}) {
const isVideo = item.mediaType === MediaType.video
return (
<View style={[{aspectRatio: 1, width: IMAGE_WIDTH}]}>
<Image
source={{uri: item.uri}}
style={[
a.flex_1,
a.overflow_hidden,
a.rounded_xs,
isFirstRow && [
index === 0
? {borderTopLeftRadius: 12}
: index === 2
? {borderTopRightRadius: 12}
: null,
],
]}
/>
{isVideo ? (
<View
style={[
a.absolute,
{
right: 0,
bottom: 0,
},
]}>
<View
style={[
a.flex_row,
a.rounded_xs,
a.gap_xs,
a.align_center,
a.mr_xs,
a.mb_xs,
{backgroundColor: 'rgba(0,0,0,0.5)', padding: 4},
]}>
<PlayIcon
width={12}
style={[a.relative, a.z_10, {color: 'white'}]}
/>
<Text style={[a.font_bold, {color: 'white'}]}>
{Math.floor(item.duration / 60)}:
{Math.floor(item.duration % 60)
.toString()
.padStart(2, '0')}
</Text>
</View>
</View>
) : null}
</View>
)
}
function SaveButton({
onPress,
disabled,
}: {
onPress: () => void
disabled: boolean
}) {
const {_} = useLingui()
return (
<Button
label={_(msg`Select`)}
onPress={onPress}
size="small"
color="primary"
variant="ghost"
style={[a.rounded_full]}
disabled={disabled}>
<ButtonText style={[a.text_md]}>
<Trans>Select</Trans>
</ButtonText>
</Button>
)
}
function CancelButton({onPress}: {onPress: () => void}) {
const {_} = useLingui()
return (
<Button
label={_(msg`Cancel`)}
onPress={onPress}
size="small"
color="primary"
variant="ghost"
style={[a.rounded_full]}>
<ButtonText style={[a.text_md]}>
<Trans>Cancel</Trans>
</ButtonText>
</Button>
)
}