diff --git a/app.json b/app.json
index 426908f..e201da3 100644
--- a/app.json
+++ b/app.json
@@ -12,7 +12,7 @@
"supportsTablet": true,
"infoPlist": {
"NSPhotoLibraryUsageDescription": "允许访问照片库,以便模型使用您照片库中的素材进行视频创作”(例如:上传您参加音乐节的现场图,生成一个音乐节体验Vlog",
- "NSPhotoLibraryAddUsageDescription": "需要保存图片到相册",
+ "NSPhotoLibraryAddUsageDescription": "App需要访问相册来保存图片",
"NSLocationWhenInUseUsageDescription": "允许获取位置信息,以便模型使用您的位置信息进行个性化创作”(例如:上传您去欧洲旅游的位置信息,结合在当地拍摄的照片,生成一个欧洲旅行攻略Vlog)",
"ITSAppUsesNonExemptEncryption": false,
"UIBackgroundModes": [
diff --git a/components/ask/aiChat.tsx b/components/ask/aiChat.tsx
index 5ecd81b..de470a9 100644
--- a/components/ask/aiChat.tsx
+++ b/components/ask/aiChat.tsx
@@ -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,
}}
>
- ,
+ label: "保存",
+ onPress: () => {
+ const imageUrl = image?.preview_file_info?.url || image.video?.preview_file_info?.url;
+ if (imageUrl) {
+ saveMediaToGallery(imageUrl);
+ }
+ },
+ textStyle: { color: '#4C320C' }
+ },
+ {
+ svg: ,
+ 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')}
- />
+ >
+
+
+
))}
diff --git a/components/ask/selectModel.tsx b/components/ask/selectModel.tsx
index 3b91652..d5c2dea 100644
--- a/components/ask/selectModel.tsx
+++ b/components/ask/selectModel.tsx
@@ -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 (
,
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
-
+
)
}
diff --git a/components/ask/utils.ts b/components/ask/utils.ts
index 341dca7..945e3a4 100644
--- a/components/ask/utils.ts
+++ b/components/ask/utils.ts
@@ -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);
+ }
+ }
};
\ No newline at end of file