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";