feat: 长按保存
This commit is contained in:
parent
e74622b009
commit
0644d636d7
1
assets/icons/svg/cancel.svg
Normal file
1
assets/icons/svg/cancel.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-x-icon lucide-circle-x"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>
|
||||
|
After Width: | Height: | Size: 312 B |
1
assets/icons/svg/download.svg
Normal file
1
assets/icons/svg/download.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-down-to-line-icon lucide-arrow-down-to-line"><path d="M12 17V3"/><path d="m6 11 6 6 6-6"/><path d="M19 21H5"/></svg>
|
||||
|
After Width: | Height: | Size: 324 B |
@ -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<React.SetStateAction<{ visible: boolean, content: any }>>;
|
||||
@ -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
|
||||
<ThemedText style={detailsStyles.headerText}>{t('ask.selectPhoto', { ns: 'ask' })}</ThemedText>
|
||||
<FolderSvg />
|
||||
</View>
|
||||
<View style={{ overflow: 'scroll', height: "100%" }}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<FlatList
|
||||
data={mergeArrays(modalDetailsVisible?.content?.image_material_infos || [], modalDetailsVisible?.content?.video_material_infos || [])}
|
||||
numColumns={3}
|
||||
keyExtractor={(item) => 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 (
|
||||
<TouchableOpacity
|
||||
style={detailsStyles.gridItemContainer}
|
||||
key={item.id}
|
||||
>
|
||||
<GestureDetector gesture={longPressGesture}>
|
||||
<View style={detailsStyles.gridItem}>
|
||||
<View style={detailsStyles.gridItemContainer} key={itemId}>
|
||||
|
||||
<View style={detailsStyles.gridItem}>
|
||||
{isSelected && (
|
||||
<ThemedText style={detailsStyles.imageNumber}>
|
||||
{selectedImages?.map((image, index) => {
|
||||
if (image === item.id || image === item.video?.id) {
|
||||
return index + 1
|
||||
}
|
||||
})}
|
||||
{selectedImages.indexOf(itemId) + 1}
|
||||
</ThemedText>
|
||||
)}
|
||||
<ContextMenu
|
||||
items={[
|
||||
{
|
||||
svg: <DownloadSvg width={20} height={20} />,
|
||||
label: "保存",
|
||||
onPress: () => console.log('保存图片'),
|
||||
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,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: item?.preview_file_info?.url || item.video?.preview_file_info?.url }}
|
||||
style={detailsStyles.image}
|
||||
@ -84,19 +112,26 @@ const SelectModel = ({ modalDetailsVisible, setModalDetailsVisible, insets, setS
|
||||
onLoad={() => console.log('Image loaded successfully')}
|
||||
loadingIndicatorSource={require('@/assets/images/png/placeholder.png')}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
detailsStyles.circleMarker,
|
||||
isSelected && detailsStyles.circleMarkerSelected
|
||||
]}
|
||||
onPress={toggleSelection}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{isSelected && <YesSvg width={16} height={16} />}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</GestureDetector>
|
||||
</TouchableOpacity>
|
||||
</ContextMenu>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
detailsStyles.circleMarker,
|
||||
isSelected && detailsStyles.circleMarkerSelected
|
||||
]}
|
||||
onPress={() => {
|
||||
setSelectedImages(prev =>
|
||||
isSelected
|
||||
? prev.filter(id => id !== itemId)
|
||||
: [...prev, itemId]
|
||||
);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{isSelected && <YesSvg width={16} height={16} />}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@ -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',
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
174
components/gusture/contextMenu.tsx
Normal file
174
components/gusture/contextMenu.tsx
Normal file
@ -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<TextStyle>;
|
||||
}
|
||||
|
||||
interface ContextMenuProps {
|
||||
children: React.ReactNode;
|
||||
items: MenuItem[];
|
||||
menuStyle?: StyleProp<ViewStyle>;
|
||||
menuItemStyle?: StyleProp<ViewStyle>;
|
||||
menuTextStyle?: StyleProp<TextStyle>;
|
||||
dividerStyle?: StyleProp<ViewStyle>;
|
||||
onOpen?: () => void;
|
||||
onClose?: () => void;
|
||||
longPressDuration?: number;
|
||||
activeOpacity?: number;
|
||||
}
|
||||
|
||||
const ContextMenu: React.FC<ContextMenuProps> = ({
|
||||
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<View>(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 (
|
||||
<>
|
||||
<View ref={containerRef} collapsable={false} style={{ flex: 1 }}>
|
||||
<GestureDetector gesture={gesture}>
|
||||
<View style={{ flex: 1 }}>
|
||||
{children}
|
||||
</View>
|
||||
</GestureDetector>
|
||||
</View>
|
||||
|
||||
<Modal
|
||||
visible={menuVisible}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={hideMenu}
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={hideMenu}>
|
||||
<View style={styles.modalOverlay} />
|
||||
</TouchableWithoutFeedback>
|
||||
|
||||
<View
|
||||
style={[
|
||||
styles.menu,
|
||||
{
|
||||
position: 'absolute',
|
||||
top: Math.min(
|
||||
menuPosition.y,
|
||||
screenHeight - 300
|
||||
),
|
||||
left: Math.min(
|
||||
menuPosition.x > screenWidth / 2 ? menuPosition.x - 150 : menuPosition.x,
|
||||
screenWidth - 160
|
||||
),
|
||||
},
|
||||
menuStyle,
|
||||
]}
|
||||
onStartShouldSetResponder={() => true}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<React.Fragment key={item.label}>
|
||||
<TouchableOpacity
|
||||
style={[styles.menuItem, menuItemStyle]}
|
||||
onPress={() => handleItemPress(item.onPress)}
|
||||
activeOpacity={activeOpacity}
|
||||
>
|
||||
{item.svg}
|
||||
<Text style={[styles.menuText, menuTextStyle, item.textStyle]}>
|
||||
{item.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
{index < items.length - 1 && (
|
||||
<View style={[styles.divider, dividerStyle]} />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</View>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
@ -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";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user