feat: 图片视频长按保存
This commit is contained in:
parent
0644d636d7
commit
a250529de3
2
app.json
2
app.json
@ -12,7 +12,7 @@
|
|||||||
"supportsTablet": true,
|
"supportsTablet": true,
|
||||||
"infoPlist": {
|
"infoPlist": {
|
||||||
"NSPhotoLibraryUsageDescription": "允许访问照片库,以便模型使用您照片库中的素材进行视频创作”(例如:上传您参加音乐节的现场图,生成一个音乐节体验Vlog",
|
"NSPhotoLibraryUsageDescription": "允许访问照片库,以便模型使用您照片库中的素材进行视频创作”(例如:上传您参加音乐节的现场图,生成一个音乐节体验Vlog",
|
||||||
"NSPhotoLibraryAddUsageDescription": "需要保存图片到相册",
|
"NSPhotoLibraryAddUsageDescription": "App需要访问相册来保存图片",
|
||||||
"NSLocationWhenInUseUsageDescription": "允许获取位置信息,以便模型使用您的位置信息进行个性化创作”(例如:上传您去欧洲旅游的位置信息,结合在当地拍摄的照片,生成一个欧洲旅行攻略Vlog)",
|
"NSLocationWhenInUseUsageDescription": "允许获取位置信息,以便模型使用您的位置信息进行个性化创作”(例如:上传您去欧洲旅游的位置信息,结合在当地拍摄的照片,生成一个欧洲旅行攻略Vlog)",
|
||||||
"ITSAppUsesNonExemptEncryption": false,
|
"ITSAppUsesNonExemptEncryption": false,
|
||||||
"UIBackgroundModes": [
|
"UIBackgroundModes": [
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
|
import CancelSvg from '@/assets/icons/svg/cancel.svg';
|
||||||
import ChatSvg from "@/assets/icons/svg/chat.svg";
|
import ChatSvg from "@/assets/icons/svg/chat.svg";
|
||||||
|
import DownloadSvg from '@/assets/icons/svg/download.svg';
|
||||||
import MoreSvg from "@/assets/icons/svg/more.svg";
|
import MoreSvg from "@/assets/icons/svg/more.svg";
|
||||||
import { Message, Video } from "@/types/ask";
|
import { Message, Video } from "@/types/ask";
|
||||||
import { MaterialItem } from "@/types/personal-info";
|
import { MaterialItem } from "@/types/personal-info";
|
||||||
@ -12,11 +14,12 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View
|
View
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import ContextMenu from "../gusture/contextMenu";
|
||||||
import { ThemedText } from "../ThemedText";
|
import { ThemedText } from "../ThemedText";
|
||||||
import SelectModel from "./selectModel";
|
import SelectModel from "./selectModel";
|
||||||
import SingleContentModel from "./singleContentModel";
|
import SingleContentModel from "./singleContentModel";
|
||||||
import TypewriterText from "./typewriterText";
|
import TypewriterText from "./typewriterText";
|
||||||
import { mergeArrays } from "./utils";
|
import { mergeArrays, saveMediaToGallery } from "./utils";
|
||||||
|
|
||||||
interface RenderMessageProps {
|
interface RenderMessageProps {
|
||||||
insets: { top: number };
|
insets: { top: number };
|
||||||
@ -69,16 +72,50 @@ const MessageItem = ({ t, insets, item, sessionId, setModalVisible, modalVisible
|
|||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Image
|
<ContextMenu
|
||||||
source={{ uri: image?.preview_file_info?.url || image.video?.preview_file_info?.url }}
|
items={[
|
||||||
style={{
|
{
|
||||||
width: '100%',
|
svg: <DownloadSvg width={20} height={20} />,
|
||||||
height: '100%',
|
label: "保存",
|
||||||
borderRadius: 12,
|
onPress: () => {
|
||||||
|
const imageUrl = image?.preview_file_info?.url || image.video?.preview_file_info?.url;
|
||||||
|
if (imageUrl) {
|
||||||
|
saveMediaToGallery(imageUrl);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
textStyle: { color: '#4C320C' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
svg: <CancelSvg width={20} height={20} color='red' />,
|
||||||
|
label: "取消",
|
||||||
|
onPress: () => console.log('取消'),
|
||||||
|
textStyle: { color: 'red' }
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
menuStyle={{
|
||||||
|
backgroundColor: 'white',
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 8,
|
||||||
|
minWidth: 150,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.25,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 5,
|
||||||
}}
|
}}
|
||||||
resizeMode="cover"
|
>
|
||||||
loadingIndicatorSource={require('@/assets/images/png/placeholder.png')}
|
<Image
|
||||||
/>
|
source={{ uri: image?.preview_file_info?.url || image.video?.preview_file_info?.url }}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
borderRadius: 12,
|
||||||
|
}}
|
||||||
|
resizeMode="cover"
|
||||||
|
loadingIndicatorSource={require('@/assets/images/png/placeholder.png')}
|
||||||
|
/>
|
||||||
|
</ContextMenu>
|
||||||
|
|
||||||
</Pressable>
|
</Pressable>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@ -1,16 +1,14 @@
|
|||||||
|
import CancelSvg from '@/assets/icons/svg/cancel.svg';
|
||||||
|
import DownloadSvg from '@/assets/icons/svg/download.svg';
|
||||||
import FolderSvg from "@/assets/icons/svg/folder.svg";
|
import FolderSvg from "@/assets/icons/svg/folder.svg";
|
||||||
import ReturnArrow from "@/assets/icons/svg/returnArrow.svg";
|
import ReturnArrow from "@/assets/icons/svg/returnArrow.svg";
|
||||||
import YesSvg from "@/assets/icons/svg/yes.svg";
|
import YesSvg from "@/assets/icons/svg/yes.svg";
|
||||||
import { TFunction } from "i18next";
|
import { TFunction } from "i18next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { FlatList, Image, Modal, StyleSheet, TouchableOpacity, View } from "react-native";
|
import { FlatList, Image, Modal, StyleSheet, TouchableOpacity, View } from "react-native";
|
||||||
import { Gesture } from 'react-native-gesture-handler';
|
|
||||||
import ContextMenu from "../gusture/contextMenu";
|
import ContextMenu from "../gusture/contextMenu";
|
||||||
import { ThemedText } from "../ThemedText";
|
import { ThemedText } from "../ThemedText";
|
||||||
import { mergeArrays } from "./utils";
|
import { mergeArrays, saveMediaToGallery } from "./utils";
|
||||||
|
|
||||||
import CancelSvg from '@/assets/icons/svg/cancel.svg';
|
|
||||||
import DownloadSvg from '@/assets/icons/svg/download.svg';
|
|
||||||
|
|
||||||
interface SelectModelProps {
|
interface SelectModelProps {
|
||||||
modalDetailsVisible: { visible: boolean, content: any };
|
modalDetailsVisible: { visible: boolean, content: any };
|
||||||
@ -22,11 +20,6 @@ interface SelectModelProps {
|
|||||||
}
|
}
|
||||||
const SelectModel = ({ modalDetailsVisible, setModalDetailsVisible, insets, setSelectedImages, selectedImages, t }: SelectModelProps) => {
|
const SelectModel = ({ modalDetailsVisible, setModalDetailsVisible, insets, setSelectedImages, selectedImages, t }: SelectModelProps) => {
|
||||||
|
|
||||||
const longPressGesture = Gesture.LongPress().onEnd((e, success) => {
|
|
||||||
if (success) {
|
|
||||||
console.log(`Long pressed for ${e.duration} ms!`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
animationType="fade"
|
animationType="fade"
|
||||||
@ -83,7 +76,12 @@ const SelectModel = ({ modalDetailsVisible, setModalDetailsVisible, insets, setS
|
|||||||
{
|
{
|
||||||
svg: <DownloadSvg width={20} height={20} />,
|
svg: <DownloadSvg width={20} height={20} />,
|
||||||
label: "保存",
|
label: "保存",
|
||||||
onPress: () => console.log('保存图片'),
|
onPress: () => {
|
||||||
|
const imageUrl = item?.file_info?.url || item.video?.file_info?.url;
|
||||||
|
if (imageUrl) {
|
||||||
|
saveMediaToGallery(imageUrl);
|
||||||
|
}
|
||||||
|
},
|
||||||
textStyle: { color: '#4C320C' }
|
textStyle: { color: '#4C320C' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -156,7 +154,7 @@ const SelectModel = ({ modalDetailsVisible, setModalDetailsVisible, insets, setS
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal >
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import { fetchApi } from "@/lib/server-api-util";
|
import { fetchApi } from "@/lib/server-api-util";
|
||||||
import { Message } from "@/types/ask";
|
import { Message } from "@/types/ask";
|
||||||
|
import * as FileSystem from 'expo-file-system';
|
||||||
|
import * as MediaLibrary from 'expo-media-library';
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
import { Alert } from 'react-native';
|
||||||
|
|
||||||
// 实现一个函数,从两个数组中轮流插入新数组
|
// 实现一个函数,从两个数组中轮流插入新数组
|
||||||
export const mergeArrays = (arr1: any[], arr2: any[]) => {
|
export const mergeArrays = (arr1: any[], arr2: any[]) => {
|
||||||
@ -49,4 +52,77 @@ export const getConversation = async ({
|
|||||||
// console.error('Error in getConversation:', error);
|
// console.error('Error in getConversation:', error);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 图片 视频 保存到本地
|
||||||
|
export const saveMediaToGallery = async (mediaUrl: string) => {
|
||||||
|
// 声明 fileUri 变量以便在 finally 块中使用
|
||||||
|
let fileUri: string | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 首先请求权限
|
||||||
|
const { status } = await MediaLibrary.requestPermissionsAsync();
|
||||||
|
|
||||||
|
if (status !== 'granted') {
|
||||||
|
Alert.alert('需要相册权限', '请允许应用访问相册以保存媒体文件');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件扩展名
|
||||||
|
const fileExtension = mediaUrl.split('.').pop()?.toLowerCase() || 'mp4';
|
||||||
|
const isVideo = ['mp4', 'mov', 'avi', 'mkv'].includes(fileExtension);
|
||||||
|
const fileName = `temp_${Date.now()}.${fileExtension}`;
|
||||||
|
fileUri = `${FileSystem.documentDirectory}${fileName}`;
|
||||||
|
|
||||||
|
// 下载文件
|
||||||
|
console.log('开始下载文件:', mediaUrl);
|
||||||
|
const downloadResumable = FileSystem.createDownloadResumable(
|
||||||
|
mediaUrl,
|
||||||
|
fileUri,
|
||||||
|
{},
|
||||||
|
(downloadProgress) => {
|
||||||
|
const progress = downloadProgress.totalBytesWritten / (downloadProgress.totalBytesExpectedToWrite || 1);
|
||||||
|
console.log(`下载进度: ${Math.round(progress * 100)}%`);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const downloadResult = await downloadResumable.downloadAsync();
|
||||||
|
|
||||||
|
if (!downloadResult) {
|
||||||
|
throw new Error('下载失败: 下载被取消或发生错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { uri } = downloadResult;
|
||||||
|
console.log('文件下载完成,准备保存到相册:', uri);
|
||||||
|
|
||||||
|
// 保存到相册
|
||||||
|
const asset = await MediaLibrary.createAssetAsync(uri);
|
||||||
|
await MediaLibrary.createAlbumAsync(
|
||||||
|
'Memowake',
|
||||||
|
asset,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
Alert.alert(
|
||||||
|
'保存成功',
|
||||||
|
isVideo ? '视频已保存到相册' : '图片已保存到相册'
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存失败:', error);
|
||||||
|
Alert.alert(
|
||||||
|
'保存失败',
|
||||||
|
error instanceof Error ? error.message : '保存媒体文件时出错,请重试'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
// 清理临时文件
|
||||||
|
try {
|
||||||
|
if (fileUri) {
|
||||||
|
await FileSystem.deleteAsync(fileUri, { idempotent: true }).catch(console.warn);
|
||||||
|
}
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.warn('清理临时文件时出错:', cleanupError);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user