diff --git a/assets/icons/svg/cancel.svg b/assets/icons/svg/cancel.svg
new file mode 100644
index 0000000..982309c
--- /dev/null
+++ b/assets/icons/svg/cancel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/svg/download.svg b/assets/icons/svg/download.svg
new file mode 100644
index 0000000..c79fa79
--- /dev/null
+++ b/assets/icons/svg/download.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/components/ask/selectModel.tsx b/components/ask/selectModel.tsx
index d6f8cd0..3b91652 100644
--- a/components/ask/selectModel.tsx
+++ b/components/ask/selectModel.tsx
@@ -4,10 +4,14 @@ 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, GestureDetector } from 'react-native-gesture-handler';
+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';
+
interface SelectModelProps {
modalDetailsVisible: { visible: boolean, content: any };
setModalDetailsVisible: React.Dispatch>;
@@ -17,6 +21,7 @@ interface SelectModelProps {
t: TFunction;
}
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!`);
@@ -40,13 +45,13 @@ const SelectModel = ({ modalDetailsVisible, setModalDetailsVisible, insets, setS
{t('ask.selectPhoto', { ns: 'ask' })}
-
+
item.id}
showsVerticalScrollIndicator={false}
- contentContainerStyle={detailsStyles.flatListContent}
+ contentContainerStyle={detailsStyles.gridContainer}
initialNumToRender={12}
maxToRenderPerBatch={12}
updateCellsBatchingPeriod={50}
@@ -63,20 +68,43 @@ const SelectModel = ({ modalDetailsVisible, setModalDetailsVisible, insets, setS
: [...prev, itemId]
);
};
+
return (
-
-
-
+
+
+
+ {isSelected && (
- {selectedImages?.map((image, index) => {
- if (image === item.id || image === item.video?.id) {
- return index + 1
- }
- })}
+ {selectedImages.indexOf(itemId) + 1}
+ )}
+ ,
+ label: "保存",
+ onPress: () => console.log('保存图片'),
+ 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,
+ }}
+ >
console.log('Image loaded successfully')}
loadingIndicatorSource={require('@/assets/images/png/placeholder.png')}
/>
-
- {isSelected && }
-
-
-
-
+
+
+ {
+ setSelectedImages(prev =>
+ isSelected
+ ? prev.filter(id => id !== itemId)
+ : [...prev, itemId]
+ );
+ }}
+ activeOpacity={0.8}
+ >
+ {isSelected && }
+
+
+
);
}}
/>
@@ -127,6 +162,11 @@ const SelectModel = ({ modalDetailsVisible, setModalDetailsVisible, insets, setS
const detailsStyles = StyleSheet.create({
+ gridContainer: {
+ flex: 1,
+ paddingHorizontal: 8,
+ paddingTop: 8,
+ },
gridItemContainer: {
width: '33.33%',
aspectRatio: 1,
@@ -199,6 +239,7 @@ const detailsStyles = StyleSheet.create({
image: {
width: '100%',
height: '100%',
+ resizeMode: 'cover',
},
circleMarker: {
position: 'absolute',
diff --git a/components/download/qrCode.tsx b/components/download/qrCode.tsx
index 36d2531..74e435e 100644
--- a/components/download/qrCode.tsx
+++ b/components/download/qrCode.tsx
@@ -1,9 +1,9 @@
+import i18n from '@/i18n';
+import { PermissionService } from '@/lib/PermissionService';
import * as Haptics from 'expo-haptics';
import * as MediaLibrary from 'expo-media-library';
import React, { useRef } from 'react';
import { StyleSheet, TouchableOpacity, View } from 'react-native';
-import i18n from '@/i18n';
-import { PermissionService } from '@/lib/PermissionService';
import QRCode from 'react-native-qrcode-svg';
import { captureRef } from 'react-native-view-shot';
diff --git a/components/gusture/contextMenu.tsx b/components/gusture/contextMenu.tsx
new file mode 100644
index 0000000..5fcaa2f
--- /dev/null
+++ b/components/gusture/contextMenu.tsx
@@ -0,0 +1,174 @@
+import React, { useRef, useState } from 'react';
+import {
+ Dimensions,
+ Modal,
+ StyleProp,
+ StyleSheet,
+ Text,
+ TextStyle,
+ TouchableOpacity,
+ TouchableWithoutFeedback,
+ View,
+ ViewStyle
+} from 'react-native';
+import { Gesture, GestureDetector } from 'react-native-gesture-handler';
+import { runOnJS } from 'react-native-reanimated';
+
+interface MenuItem {
+ label: string;
+ svg?: React.ReactNode;
+ onPress: () => void;
+ textStyle?: StyleProp;
+}
+
+interface ContextMenuProps {
+ children: React.ReactNode;
+ items: MenuItem[];
+ menuStyle?: StyleProp;
+ menuItemStyle?: StyleProp;
+ menuTextStyle?: StyleProp;
+ dividerStyle?: StyleProp;
+ onOpen?: () => void;
+ onClose?: () => void;
+ longPressDuration?: number;
+ activeOpacity?: number;
+}
+
+const ContextMenu: React.FC = ({
+ children,
+ items,
+ menuStyle,
+ menuItemStyle,
+ menuTextStyle,
+ dividerStyle,
+ onOpen,
+ onClose,
+ longPressDuration = 500,
+ activeOpacity = 0.8,
+}) => {
+ const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
+ const [menuVisible, setMenuVisible] = useState(false);
+ const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
+ const containerRef = useRef(null);
+
+ const showMenu = (x: number, y: number) => {
+ setMenuPosition({ x, y });
+ setMenuVisible(true);
+ onOpen?.();
+ };
+
+ const hideMenu = () => {
+ setMenuVisible(false);
+ onClose?.();
+ };
+
+ const handleItemPress = (onPress: () => void) => {
+ onPress();
+ hideMenu();
+ };
+
+ const gesture = Gesture.LongPress()
+ .minDuration(longPressDuration)
+ .onStart((e) => {
+ const absoluteX = e.absoluteX;
+ const absoluteY = e.absoluteY;
+ runOnJS(showMenu)(absoluteX, absoluteY);
+ });
+
+ return (
+ <>
+
+
+
+ {children}
+
+
+
+
+
+
+
+
+
+ screenWidth / 2 ? menuPosition.x - 150 : menuPosition.x,
+ screenWidth - 160
+ ),
+ },
+ menuStyle,
+ ]}
+ onStartShouldSetResponder={() => true}
+ >
+ {items.map((item, index) => (
+
+ handleItemPress(item.onPress)}
+ activeOpacity={activeOpacity}
+ >
+ {item.svg}
+
+ {item.label}
+
+
+ {index < items.length - 1 && (
+
+ )}
+
+ ))}
+
+
+ >
+ );
+};
+
+const styles = StyleSheet.create({
+ modalOverlay: {
+ ...StyleSheet.absoluteFillObject,
+ backgroundColor: 'rgba(0, 0, 0, 0.1)',
+ },
+ menu: {
+ backgroundColor: 'white',
+ borderRadius: 8,
+ minWidth: 100,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.3,
+ shadowRadius: 6,
+ elevation: 10,
+ zIndex: 1000,
+ },
+ menuItem: {
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ minWidth: 100,
+ flexDirection: 'row',
+ gap: 4,
+ alignItems: 'center'
+ },
+ menuText: {
+ fontSize: 16,
+ color: '#333',
+ },
+ divider: {
+ height: 1,
+ backgroundColor: '#f0f0f0',
+ marginHorizontal: 8,
+ },
+});
+
+export default ContextMenu;
\ No newline at end of file
diff --git a/provider.tsx b/provider.tsx
index 6535122..5e20661 100644
--- a/provider.tsx
+++ b/provider.tsx
@@ -1,7 +1,9 @@
import { I18nextProvider } from "react-i18next";
import { Platform } from 'react-native';
+import 'react-native-gesture-handler';
import { GestureHandlerRootView } from "react-native-gesture-handler";
+import 'react-native-reanimated';
import Toast, { BaseToast, ErrorToast, ToastConfig } from 'react-native-toast-message';
import { Provider as ReduxProvider } from "react-redux";
import { AuthProvider } from "./contexts/auth-context";