完善选择媒体组件功能

This commit is contained in:
yezian 2024-09-27 16:04:56 +08:00
parent 354811a078
commit 74860351e8
6 changed files with 1246 additions and 1685 deletions

View File

@ -1,5 +1,5 @@
import { View, TouchableOpacity, Image } from "react-native";
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useCallback } from "react";
import { useTailwind } from "tailwind-rn";
import { Icon } from "@rneui/themed";
import Toast from "react-native-toast-message";
@ -7,6 +7,7 @@ import { DraggableGrid } from "react-native-draggable-grid";
import { useImageViewer } from "../../context/ImageViewProvider";
import VideoModal from "../VideoModal";
import * as ImagePicker from "expo-image-picker";
import * as VideoThumbnails from "expo-video-thumbnails";
/*
props格式
@ -31,6 +32,16 @@ export default function MediaPicker({
const [showVideo, setShowVideo] = useState(false);
const [videoUrl, setVideoUrl] = useState("");
//
const generateThumbnail = useCallback(async (uri) => {
try {
const videoCover = await VideoThumbnails.getThumbnailAsync(uri);
return videoCover;
} catch (e) {
console.warn(e);
}
}, []);
//
const pickMedia = async () => {
let mediaType;
@ -49,6 +60,12 @@ export default function MediaPicker({
});
if (!result.canceled) {
for (let i = 0; i < result.assets.length; i++) {
if (result.assets[i].duration > 0) {
const videoCover = await generateThumbnail(result.assets[i].uri);
result.assets[i].cover = videoCover.uri;
}
}
setNewAssets(result.assets);
}
};
@ -111,8 +128,11 @@ export default function MediaPicker({
}}
key={item.key}
>
<Image src={item.uri} style={tailwind("w-full h-full rounded")} />
<View style={tailwind("absolute top-2 right-2")}>
<Image
src={item.cover ? item.cover : item.uri}
style={tailwind("w-full h-full rounded")}
/>
<View style={{ zIndex: 999, ...tailwind("absolute top-2 right-2") }}>
<Icon
type="ionicon"
name="close"

View File

@ -1,351 +0,0 @@
import {
View,
Text,
TouchableOpacity,
Image,
Modal,
Platform,
} from "react-native";
import React, { useState, useEffect } from "react";
import { useTailwind } from "tailwind-rn";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { ImagePicker } from "expo-image-multiple-picker";
import { Icon, Badge } from "@rneui/themed";
import * as MediaLibrary from "expo-media-library";
import * as Linking from "expo-linking";
import MyModal from "../MyModal";
/*
props格式
visible 控制modal可见性
setVisible 控制modal可见性
type 选择的媒体类型 "image"|"video"|"mix"
maxCount 最大选择数量
setAssets 向父组件返回媒体
*/
export default function MediaPickerModal({
visible,
setVisible,
type,
maxCount,
setAssets,
}) {
const tailwind = useTailwind();
const insets = useSafeAreaInsets();
const [album, setAlbum] = useState({ title: "全部" });
//
const [requestModalVisible, setRequestModalVisible] = useState(false);
//
const [overlayVisible, setOverlayVisible] = useState(false);
//
const [permissionStatus, setPermissionStatus] = useState();
//
useEffect(() => {
if (visible) {
async function checkMediaLibraryPermissions() {
//
// :
const { status } = await MediaLibrary.getPermissionsAsync();
if (status === "granted") {
// ,
setPermissionStatus("granted");
return;
}
if (status === "undetermined") {
setRequestModalVisible(true);
return;
}
if (status === "denied") {
requestMediaLibraryPermissions();
}
}
checkMediaLibraryPermissions();
}
}, [visible]);
async function requestMediaLibraryPermissions() {
const permission = await MediaLibrary.requestPermissionsAsync();
if (permission.status === "denied") {
// ,
setPermissionStatus("denied");
setVisible(false);
setOverlayVisible(true);
return;
}
if (permission.status === "granted") {
// ,
setPermissionStatus("granted");
}
}
//iosassetsph://file://
const changeToLocalUri = async (assets) => {
const editedAssets = await Promise.all(
assets.map(async (item) => {
const info = await MediaLibrary.getAssetInfoAsync(item.id);
const uri = info.localUri;
if (Platform.OS === "ios") {
return { ...item, old_uri: item.uri, uri };
}
return { ...item, old_uri: item.uri };
})
);
return editedAssets;
};
//
function formatDuration(duration) {
let minutes = Math.floor(duration / 60);
let seconds = Math.round(duration % 60);
if (seconds < 10) {
seconds = "0" + seconds;
}
return `${minutes}:${seconds}`;
}
//header
const ImagePickerHeader = (props) => {
return (
<View
style={{
paddingTop: insets.top,
height: 100,
...tailwind(
"px-4 bg-[#07050A] flex-row justify-between items-center"
),
}}
>
{props.view == "album" && (
<>
<View style={tailwind("w-10")}></View>
<Text
style={tailwind("text-lg text-center text-white font-medium")}
>
选择相册
</Text>
<TouchableOpacity
onPress={() => {
setVisible(false);
}}
style={tailwind("flex-row items-center")}
>
<Text
style={{
color: "#FF669E",
...tailwind("text-lg font-semibold ml-1"),
}}
>
取消
</Text>
</TouchableOpacity>
</>
)}
{props.view == "gallery" && (
<>
<TouchableOpacity onPress={props.goToAlbum}>
<Icon
type="ionicon"
name="chevron-back"
color="white"
size={32}
/>
</TouchableOpacity>
<Text
style={tailwind(
"text-lg text-center text-white font-medium w-1/2"
)}
numberOfLines={1}
ellipsizeMode="tail"
>
{props.album.title}
</Text>
{props.imagesPicked > 0 ? (
<TouchableOpacity
onPress={props.save}
style={tailwind("flex-row items-center")}
>
<Badge
value={props.imagesPicked}
badgeStyle={{
backgroundColor: "#FF669E",
borderColor: "#FF669E",
}}
textStyle={tailwind("text-white font-medium")}
/>
<Text
style={{
color: "#FF669E",
...tailwind("text-lg font-semibold ml-1"),
}}
>
完成
</Text>
</TouchableOpacity>
) : (
<TouchableOpacity
onPress={() => {
setVisible(false);
}}
style={tailwind("flex-row items-center")}
>
<Text
style={{
color: "#FF669E",
...tailwind("text-lg font-semibold ml-1"),
}}
>
取消
</Text>
</TouchableOpacity>
)}
</>
)}
</View>
);
};
//check
const ImagePickerCheck = () => {
return (
<View
style={{
width: "100%",
height: "100%",
alignItems: "center",
justifyContent: "center",
backgroundColor: "rgba(0,0,0,0.6)",
}}
>
<Icon type="ionicon" name="checkmark" color="white" size={32} />
</View>
);
};
//album
const ImagePickerAlbum = (props) => {
return (
<TouchableOpacity
onPress={() => props.goToGallery(props.album)}
style={{ flex: 1, height: 200 }}
>
<Image
source={{ uri: props.thumb.uri }}
style={{ width: "100%", height: "100%" }}
blurRadius={10}
></Image>
<View
style={{
position: "absolute",
width: "100%",
height: "100%",
backgroundColor: "rgba(0,0,0,0.2)",
justifyContent: "flex-end",
}}
>
<View style={{ padding: 5, flexDirection: "row" }}>
<Icon type="ionicon" name="folder-open" color="white" size={16} />
<Text
style={{
color: "white",
fontSize: 16,
marginLeft: 5,
}}
>
{props.album.title}
</Text>
</View>
</View>
</TouchableOpacity>
);
};
//video
const ImagePickerVideo = (props) => {
return (
<View style={tailwind("flex-1 justify-end")}>
<View
style={tailwind(
"flex-row justify-between items-center bg-black px-2"
)}
>
<Icon type="ionicon" name="videocam" color="white" size={16} />
<Text numberOfLines={1} style={tailwind("text-xs text-white")}>
{formatDuration(props.duration)}
</Text>
</View>
</View>
);
};
return (
<>
<Modal
visible={visible && permissionStatus == "granted"}
transparent={true}
statusBarTranslucent
animationType="slide"
>
<View
style={{
...tailwind("bg-[#13121F] flex-1"),
}}
>
<ImagePicker
theme={{
header: ImagePickerHeader,
check: ImagePickerCheck,
album: ImagePickerAlbum,
video: ImagePickerVideo,
}}
onSave={async (assets) => {
setVisible(false);
const editedAssets = await changeToLocalUri(assets);
setAssets(editedAssets);
}}
onCancel={() => {
setVisible(false);
}}
galleryColumns={3}
albumColumns={2}
multiple
onSelectAlbum={(album) => setAlbum(album)}
selectedAlbum={album}
limit={maxCount}
video={type === "video" || type === "mix"}
image={type === "image" || type === "mix"}
/>
</View>
</Modal>
<MyModal
visible={requestModalVisible}
setVisible={setRequestModalVisible}
title="权限申请"
content="该功能需要开启存储权限,用于选择上传媒体文件"
confirmText="继续"
custom={() => {
setRequestModalVisible(false);
requestMediaLibraryPermissions();
}}
customText="继续"
/>
<MyModal
visible={overlayVisible}
setVisible={setOverlayVisible}
title="未获得相册权限"
content="请先前往设置打开相关权限后重试"
confirmText="去设置"
cancel={() => {
setOverlayVisible(false);
}}
confirm={() => {
Linking.openSettings();
setOverlayVisible(false);
}}
/>
</>
);
}

View File

@ -44,7 +44,6 @@
"expo-file-system": "~17.0.1",
"expo-image": "~1.12.15",
"expo-image-manipulator": "~12.0.5",
"expo-image-multiple-picker": "^4.8.3",
"expo-image-picker": "~15.0.7",
"expo-intent-launcher": "~11.0.1",
"expo-linear-gradient": "~13.0.2",

View File

@ -7,13 +7,12 @@ import {
Platform,
ActivityIndicator,
} from "react-native";
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useCallback } from "react";
import { useTailwind } from "tailwind-rn";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Divider, Icon, Button } from "@rneui/themed";
import { Image } from "expo-image";
import { Formik } from "formik";
import MediaPickerModal from "../../components/MediaPickerModal";
import MediaPicker from "../../components/MediaPicker";
import Toast from "react-native-toast-message";
import { get } from "../../utils/storeInfo";
@ -21,6 +20,8 @@ import { multiUpload } from "../../utils/upload";
import baseRequest from "../../utils/baseRequest";
import { generateSignature } from "../../utils/crypto";
import MediaGrid from "../../components/MediaGrid";
import * as ImagePicker from "expo-image-picker";
import * as VideoThumbnails from "expo-video-thumbnails";
const blurhash = "LcKUTa%gOYWBYRt6xuoJo~s8V@fk";
@ -28,16 +29,52 @@ export default function EditStreamerMedia({ navigation, route }) {
const tailwind = useTailwind();
const insets = useSafeAreaInsets();
//Modal
const [displayImagePickerModalVisible, setDisplayImagePickerModalVisible] =
useState(false);
//
const [displayImage, setDisplayImage] = useState([]);
//Modal
const [displayVideoPickerModalVisible, setDisplayVideoPickerModalVisible] =
useState(false);
//
const pickDisPlayImage = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
quality: 1,
});
if (!result.canceled) {
setDisplayImage(result.assets);
}
};
//
const [displayVideo, setDisplayVideo] = useState([]);
//
const generateThumbnail = useCallback(async (uri) => {
try {
const videoCover = await VideoThumbnails.getThumbnailAsync(uri);
return videoCover;
} catch (e) {
console.warn(e);
}
}, []);
//
const pickDisPlayVideo = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Videos,
quality: 1,
});
if (!result.canceled) {
for (let i = 0; i < result.assets.length; i++) {
if (result.assets[i].duration > 0) {
const videoCover = await generateThumbnail(result.assets[i].uri);
result.assets[i].cover = videoCover.uri;
}
}
setDisplayVideo(result.assets);
}
};
//
const [photos, setPhotos] = useState([]);
//
@ -89,7 +126,7 @@ export default function EditStreamerMedia({ navigation, route }) {
{
notChanged: true,
id: { video_ids: streamerData.data.streamer.shorts.video_ids },
old_uri:
cover:
streamerData?.data?.streamer?.shorts?.videos[0]?.cover_urls[0],
},
]);
@ -239,7 +276,7 @@ export default function EditStreamerMedia({ navigation, route }) {
封面图
</Text>
<TouchableOpacity
onPress={() => setDisplayImagePickerModalVisible(true)}
onPress={() => pickDisPlayImage()}
style={tailwind("flex-row items-center")}
>
{displayImage.length !== 0 ? (
@ -264,13 +301,6 @@ export default function EditStreamerMedia({ navigation, route }) {
color="white"
/>
</TouchableOpacity>
<MediaPickerModal
visible={displayImagePickerModalVisible}
setVisible={setDisplayImagePickerModalVisible}
type="image"
maxCount={1}
setAssets={setDisplayImage}
/>
</View>
<Divider style={tailwind("my-2")} />
<View style={tailwind("flex-row justify-between items-center")}>
@ -279,13 +309,13 @@ export default function EditStreamerMedia({ navigation, route }) {
展示视频
</Text>
<TouchableOpacity
onPress={() => setDisplayVideoPickerModalVisible(true)}
onPress={() => pickDisPlayVideo()}
style={tailwind("flex-row items-center")}
>
{displayVideo.length !== 0 ? (
<Image
style={tailwind("w-12 h-16 rounded-lg")}
source={displayVideo[0].old_uri}
source={displayVideo[0].cover}
placeholder={blurhash}
contentFit="cover"
transition={1000}
@ -304,13 +334,6 @@ export default function EditStreamerMedia({ navigation, route }) {
color="white"
/>
</TouchableOpacity>
<MediaPickerModal
visible={displayVideoPickerModalVisible}
setVisible={setDisplayVideoPickerModalVisible}
type="video"
maxCount={1}
setAssets={setDisplayVideo}
/>
</View>
<Divider style={tailwind("my-2")} />
<View>

View File

@ -15,7 +15,6 @@ import { Divider, Icon, Button, CheckBox } from "@rneui/themed";
import { Image } from "expo-image";
import { Formik } from "formik";
import Picker from "../../../components/Picker";
import MediaPickerModal from "../../../components/MediaPickerModal";
import MediaPicker from "../../../components/MediaPicker";
import Toast from "react-native-toast-message";
import { get } from "../../../utils/storeInfo";
@ -23,6 +22,8 @@ import { multiUpload } from "../../../utils/upload";
import baseRequest from "../../../utils/baseRequest";
import { generateSignature } from "../../../utils/crypto";
import MediaGrid from "../../../components/MediaGrid";
import * as ImagePicker from "expo-image-picker";
import * as VideoThumbnails from "expo-video-thumbnails";
const blurhash = "LcKUTa%gOYWBYRt6xuoJo~s8V@fk";
@ -31,14 +32,51 @@ export default function CompleteStreamerInformation({ navigation, route }) {
const insets = useSafeAreaInsets();
//Modal
const [displayImagePickerModalVisible, setDisplayImagePickerModalVisible] =
useState(false);
const [displayImage, setDisplayImage] = useState([]);
//Modal
const [displayVideoPickerModalVisible, setDisplayVideoPickerModalVisible] =
useState(false);
//
const pickDisPlayImage = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
quality: 1,
});
if (!result.canceled) {
setDisplayImage(result.assets);
}
};
//
const [displayVideo, setDisplayVideo] = useState([]);
//
const generateThumbnail = useCallback(async (uri) => {
try {
const videoCover = await VideoThumbnails.getThumbnailAsync(uri);
return videoCover;
} catch (e) {
console.warn(e);
}
}, []);
//
const pickDisPlayVideo = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Videos,
quality: 1,
});
if (!result.canceled) {
for (let i = 0; i < result.assets.length; i++) {
if (result.assets[i].duration > 0) {
const videoCover = await generateThumbnail(result.assets[i].uri);
result.assets[i].cover = videoCover.uri;
}
}
setDisplayVideo(result.assets);
}
};
const [wechatInputShow, setWechatInputShow] = useState(1);
//
@ -234,7 +272,7 @@ export default function CompleteStreamerInformation({ navigation, route }) {
{
notChanged: true,
id: { video_ids: _data.data.list[0].shorts?.video_ids },
old_uri: _data.data.list[0].shorts?.videos[0]?.cover_urls[0],
cover: _data.data.list[0].shorts?.videos[0]?.cover_urls[0],
},
]);
setOldPhotos(_data.data.list[0].album.images);
@ -748,7 +786,7 @@ export default function CompleteStreamerInformation({ navigation, route }) {
封面图
</Text>
<TouchableOpacity
onPress={() => setDisplayImagePickerModalVisible(true)}
onPress={() => pickDisPlayImage()}
style={tailwind("flex-row items-center")}
>
{displayImage.length !== 0 ? (
@ -773,13 +811,6 @@ export default function CompleteStreamerInformation({ navigation, route }) {
color="white"
/>
</TouchableOpacity>
<MediaPickerModal
visible={displayImagePickerModalVisible}
setVisible={setDisplayImagePickerModalVisible}
type="image"
maxCount={1}
setAssets={setDisplayImage}
/>
</View>
<Divider style={tailwind("my-2")} />
<View style={tailwind("flex-row justify-between items-center")}>
@ -788,13 +819,13 @@ export default function CompleteStreamerInformation({ navigation, route }) {
展示视频
</Text>
<TouchableOpacity
onPress={() => setDisplayVideoPickerModalVisible(true)}
onPress={() => pickDisPlayVideo()}
style={tailwind("flex-row items-center")}
>
{displayVideo.length !== 0 ? (
<Image
style={tailwind("w-12 h-16 rounded-lg")}
source={displayVideo[0].old_uri}
source={displayVideo[0].cover}
placeholder={blurhash}
contentFit="cover"
transition={1000}
@ -813,13 +844,6 @@ export default function CompleteStreamerInformation({ navigation, route }) {
color="white"
/>
</TouchableOpacity>
<MediaPickerModal
visible={displayVideoPickerModalVisible}
setVisible={setDisplayVideoPickerModalVisible}
type="video"
maxCount={1}
setAssets={setDisplayVideo}
/>
</View>
<Divider style={tailwind("my-2")} />
<View>

2406
yarn.lock

File diff suppressed because it is too large Load Diff