feat: 图片视频长按保存

This commit is contained in:
jinyaqiu 2025-07-31 11:34:17 +08:00
parent 0644d636d7
commit a250529de3
4 changed files with 134 additions and 23 deletions

View File

@ -12,7 +12,7 @@
"supportsTablet": true,
"infoPlist": {
"NSPhotoLibraryUsageDescription": "允许访问照片库以便模型使用您照片库中的素材进行视频创作”例如上传您参加音乐节的现场图生成一个音乐节体验Vlog",
"NSPhotoLibraryAddUsageDescription": "需要保存图片到相册",
"NSPhotoLibraryAddUsageDescription": "App需要访问相册来保存图片",
"NSLocationWhenInUseUsageDescription": "允许获取位置信息以便模型使用您的位置信息进行个性化创作”例如上传您去欧洲旅游的位置信息结合在当地拍摄的照片生成一个欧洲旅行攻略Vlog",
"ITSAppUsesNonExemptEncryption": false,
"UIBackgroundModes": [

View File

@ -1,4 +1,6 @@
import CancelSvg from '@/assets/icons/svg/cancel.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 { Message, Video } from "@/types/ask";
import { MaterialItem } from "@/types/personal-info";
@ -12,11 +14,12 @@ import {
TouchableOpacity,
View
} from 'react-native';
import ContextMenu from "../gusture/contextMenu";
import { ThemedText } from "../ThemedText";
import SelectModel from "./selectModel";
import SingleContentModel from "./singleContentModel";
import TypewriterText from "./typewriterText";
import { mergeArrays } from "./utils";
import { mergeArrays, saveMediaToGallery } from "./utils";
interface RenderMessageProps {
insets: { top: number };
@ -69,16 +72,50 @@ const MessageItem = ({ t, insets, item, sessionId, setModalVisible, modalVisible
marginBottom: 8,
}}
>
<Image
source={{ uri: image?.preview_file_info?.url || image.video?.preview_file_info?.url }}
style={{
width: '100%',
height: '100%',
borderRadius: 12,
<ContextMenu
items={[
{
svg: <DownloadSvg width={20} height={20} />,
label: "保存",
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>
))}
</View>

View File

@ -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 ReturnArrow from "@/assets/icons/svg/returnArrow.svg";
import YesSvg from "@/assets/icons/svg/yes.svg";
import { TFunction } from "i18next";
import React from "react";
import { FlatList, Image, Modal, StyleSheet, TouchableOpacity, View } from "react-native";
import { Gesture } from 'react-native-gesture-handler';
import ContextMenu from "../gusture/contextMenu";
import { ThemedText } from "../ThemedText";
import { mergeArrays } from "./utils";
import CancelSvg from '@/assets/icons/svg/cancel.svg';
import DownloadSvg from '@/assets/icons/svg/download.svg';
import { mergeArrays, saveMediaToGallery } from "./utils";
interface SelectModelProps {
modalDetailsVisible: { visible: boolean, content: any };
@ -22,11 +20,6 @@ interface 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 (
<Modal
animationType="fade"
@ -83,7 +76,12 @@ const SelectModel = ({ modalDetailsVisible, setModalDetailsVisible, insets, setS
{
svg: <DownloadSvg width={20} height={20} />,
label: "保存",
onPress: () => console.log('保存图片'),
onPress: () => {
const imageUrl = item?.file_info?.url || item.video?.file_info?.url;
if (imageUrl) {
saveMediaToGallery(imageUrl);
}
},
textStyle: { color: '#4C320C' }
},
{
@ -156,7 +154,7 @@ const SelectModel = ({ modalDetailsVisible, setModalDetailsVisible, insets, setS
</TouchableOpacity>
</View>
</View>
</Modal>
</Modal >
)
}

View File

@ -1,6 +1,9 @@
import { fetchApi } from "@/lib/server-api-util";
import { Message } from "@/types/ask";
import * as FileSystem from 'expo-file-system';
import * as MediaLibrary from 'expo-media-library';
import { useCallback } from "react";
import { Alert } from 'react-native';
// 实现一个函数,从两个数组中轮流插入新数组
export const mergeArrays = (arr1: any[], arr2: any[]) => {
@ -49,4 +52,77 @@ export const getConversation = async ({
// console.error('Error in getConversation:', error);
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);
}
}
};