From ffdddb8f426466f4e7dfc702a228d317ba6fea76 Mon Sep 17 00:00:00 2001 From: Vali98 Date: Mon, 23 Sep 2024 22:57:22 +0800 Subject: [PATCH] feat: added editing submenu to character list, adding card cloning --- .../CharacterMenu/CharacterEditPopup.tsx | 215 ++++++++++++++++++ .../CharacterMenu/CharacterList.tsx | 2 +- .../CharacterMenu/CharacterListing.tsx | 48 ++-- .../CharacterMenu/CharacterNewMenu.tsx | 55 ++--- app/constants/Characters.ts | 19 ++ app/constants/Style.ts | 2 +- 6 files changed, 273 insertions(+), 68 deletions(-) create mode 100644 app/components/CharacterMenu/CharacterEditPopup.tsx diff --git a/app/components/CharacterMenu/CharacterEditPopup.tsx b/app/components/CharacterMenu/CharacterEditPopup.tsx new file mode 100644 index 0000000..0231ec6 --- /dev/null +++ b/app/components/CharacterMenu/CharacterEditPopup.tsx @@ -0,0 +1,215 @@ +import { AntDesign, FontAwesome } from '@expo/vector-icons' +import { Characters, Style } from '@globals' +import { useFocusEffect, useRouter } from 'expo-router' +import React, { useRef, useState } from 'react' +import { StyleSheet, TouchableOpacity, Text, BackHandler, Alert } from 'react-native' +import { + Menu, + MenuOption, + MenuOptions, + MenuOptionsCustomStyle, + MenuTrigger, + renderers, +} from 'react-native-popup-menu' + +const { Popover } = renderers + +type CharInfo = { + name: string + id: number + image_id: number + last_modified: number + tags: string[] + latestSwipe?: string + latestName?: string + latestChat?: number +} + +type CharacterEditPopupProps = { + characterInfo: CharInfo + nowLoading: boolean + setNowLoading: (b: boolean) => void +} + +type PopupProps = { + onPress: () => void | Promise + label: string + iconName: 'copy' | 'pencil' | 'trash' + warning?: boolean +} + +const PopupOption: React.FC = ({ onPress, label, iconName, warning = false }) => { + return ( + + + + + {label} + + + + ) +} + +const CharacterEditPopup: React.FC = ({ + characterInfo, + setNowLoading, + nowLoading, +}) => { + const [showMenu, setShowMenu] = useState(false) + const menuRef: React.MutableRefObject = useRef(null) + const router = useRouter() + + const { setCurrentCard, unloadCard } = Characters.useCharacterCard((state) => ({ + setCurrentCard: state.setCard, + unloadCard: state.unloadCard, + })) + + const deleteCard = () => { + Alert.alert( + `Delete Character`, + `Are you sure you want to delete '${characterInfo.name}'? This cannot be undone.`, + [ + { text: 'Cancel', onPress: () => {}, style: 'cancel' }, + { + text: 'Confirm', + onPress: () => { + Characters.db.mutate.deleteCard(characterInfo.id ?? -1) + unloadCard() + }, + style: 'destructive', + }, + ], + { cancelable: true } + ) + } + + const cloneCard = () => { + Alert.alert( + `Clone Character`, + `Are you sure you want to clone '${characterInfo.name}'?`, + [ + { text: 'Cancel', onPress: () => {}, style: 'cancel' }, + { + text: 'Confirm', + onPress: async () => { + setNowLoading(true) + await Characters.db.mutate.duplicateCard(characterInfo.id) + menuRef.current?.close() + setNowLoading(false) + }, + style: 'destructive', + }, + ], + { cancelable: true } + ) + } + + const editCharacter = async () => { + if (nowLoading) return + setNowLoading(true) + await setCurrentCard(characterInfo.id) + setNowLoading(false) + menuRef.current?.close() + router.push('/CharInfo') + } + + const backAction = () => { + if (!menuRef.current || !menuRef.current?.isOpen()) return false + menuRef.current?.close() + return true + } + + useFocusEffect(() => { + BackHandler.removeEventListener('hardwareBackPress', backAction) + const handler = BackHandler.addEventListener('hardwareBackPress', backAction) + return () => handler.remove() + }) + + return ( + setShowMenu(true)} + onClose={() => setShowMenu(false)} + renderer={Popover} + rendererProps={{ + placement: 'left', + anchorStyle: styles.anchor, + openAnimationDuration: 150, + closeAnimationDuration: 0, + }}> + + + + + editCharacter()} label="Edit" iconName="pencil" /> + { + cloneCard() + }} + label="Clone" + iconName="copy" + /> + deleteCard()} label="Delete" iconName="trash" warning /> + + + ) +} + +export default CharacterEditPopup + +const styles = StyleSheet.create({ + anchor: { + backgroundColor: Style.getColor('primary-surface3'), + padding: 4, + }, + + popupButton: { + flexDirection: 'row', + alignItems: 'center', + columnGap: 12, + paddingVertical: 12, + paddingRight: 32, + paddingLeft: 12, + borderRadius: 12, + }, + + headerButtonContainer: { + flexDirection: 'row', + }, + + optionLabel: { + color: Style.getColor('primary-text1'), + }, + + optionLabelWarning: { + fontWeight: '500', + color: '#d2574b', + }, + + triggerButton: { + paddingHorizontal: 12, + paddingVertical: 20, + }, +}) + +const menustyle: MenuOptionsCustomStyle = { + optionsContainer: { + backgroundColor: Style.getColor('primary-surface3'), + padding: 4, + borderRadius: 12, + }, + optionsWrapper: { + backgroundColor: Style.getColor('primary-surface3'), + }, +} diff --git a/app/components/CharacterMenu/CharacterList.tsx b/app/components/CharacterMenu/CharacterList.tsx index 725781b..757c05c 100644 --- a/app/components/CharacterMenu/CharacterList.tsx +++ b/app/components/CharacterMenu/CharacterList.tsx @@ -221,7 +221,7 @@ const CharacterList: React.FC = ({ showHeader }) => { {characterList.length !== 0 && ( item.id.toString()} renderItem={({ item, index }) => ( diff --git a/app/components/CharacterMenu/CharacterListing.tsx b/app/components/CharacterMenu/CharacterListing.tsx index be9eeb8..b876c0c 100644 --- a/app/components/CharacterMenu/CharacterListing.tsx +++ b/app/components/CharacterMenu/CharacterListing.tsx @@ -1,8 +1,8 @@ -import { AntDesign } from '@expo/vector-icons' import { Characters, Chats, Logger, Style } from '@globals' -import { router } from 'expo-router' import { View, Text, Image, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native' +import CharacterEditPopup from './CharacterEditPopup' + type CharacterListingProps = { index: number character: CharInfo @@ -46,27 +46,20 @@ const CharacterListing: React.FC = ({ const loadChat = Chats.useChat((state) => state.load) - const setCurrentCharacter = async (charId: number, edit: boolean = false) => { + const setCurrentCharacter = async (charId: number) => { if (nowLoading) return - try { setNowLoading(true) await setCurrentCard(charId) - - if (edit) { - router.push('/CharInfo') - } else { - let chatId = character.latestChat - if (!chatId) { - chatId = await Chats.db.mutate.createChat(charId) - } - if (!chatId) { - Logger.log('Chat creation backup has failed! Please report.', true) - return - } - await loadChat(chatId) + let chatId = character.latestChat + if (!chatId) { + chatId = await Chats.db.mutate.createChat(charId) } - + if (!chatId) { + Logger.log('Chat creation backup has failed! Please report.', true) + return + } + await loadChat(chatId) setNowLoading(false) } catch (error) { Logger.log(`Couldn't load character: ${error}`, true) @@ -134,14 +127,11 @@ const CharacterListing: React.FC = ({ size={28} /> ) : ( - { - setCurrentCharacter(character.id, true) - }} - disabled={nowLoading}> - - + )} @@ -181,11 +171,6 @@ const styles = StyleSheet.create({ flex: 1, }, - secondaryButton: { - paddingHorizontal: 12, - paddingVertical: 20, - }, - avatar: { width: 48, height: 48, @@ -196,6 +181,7 @@ const styles = StyleSheet.create({ nametag: { fontSize: 16, + fontWeight: '500', color: Style.getColor('primary-text1'), }, diff --git a/app/components/CharacterMenu/CharacterNewMenu.tsx b/app/components/CharacterMenu/CharacterNewMenu.tsx index fcfd2e7..4833193 100644 --- a/app/components/CharacterMenu/CharacterNewMenu.tsx +++ b/app/components/CharacterMenu/CharacterNewMenu.tsx @@ -1,8 +1,8 @@ import TextBoxModal from '@components/TextBoxModal' -import { FontAwesome, Ionicons } from '@expo/vector-icons' +import { FontAwesome } from '@expo/vector-icons' import { Characters, Chats, Logger, Style } from '@globals' -import { useRouter } from 'expo-router' -import { useEffect, useRef, useState } from 'react' +import { useFocusEffect, useRouter } from 'expo-router' +import { useRef, useState } from 'react' import { StyleSheet, TouchableOpacity, Text, BackHandler, View } from 'react-native' import { Menu, @@ -12,13 +12,7 @@ import { MenuTrigger, renderers, } from 'react-native-popup-menu' -import Animated, { - Easing, - SlideInRight, - SlideOutRight, - ZoomIn, - ZoomOut, -} from 'react-native-reanimated' +import Animated, { ZoomIn } from 'react-native-reanimated' import { useShallow } from 'zustand/react/shallow' const { Popover } = renderers @@ -55,15 +49,17 @@ const CharacterNewMenu: React.FC = ({ }) => { const menuRef: React.MutableRefObject = useRef(null) - useEffect(() => { - const backAction = () => { - if (!menuRef.current || !menuRef.current?.isOpen()) return false - menuRef.current?.close() - return true - } + const backAction = () => { + if (!menuRef.current || !menuRef.current?.isOpen()) return false + menuRef.current?.close() + return true + } + + useFocusEffect(() => { + BackHandler.removeEventListener('hardwareBackPress', backAction) const handler = BackHandler.addEventListener('hardwareBackPress', backAction) return () => handler.remove() - }, []) + }) const { setCurrentCard } = Characters.useCharacterCard( useShallow((state) => ({ @@ -138,24 +134,13 @@ const CharacterNewMenu: React.FC = ({ rendererProps={{ placement: 'bottom', anchorStyle: styles.anchor }}> - {!showMenu && ( - - - - )} - {showMenu && ( - - - - )} + + + diff --git a/app/constants/Characters.ts b/app/constants/Characters.ts index 0e0e088..0e74877 100644 --- a/app/constants/Characters.ts +++ b/app/constants/Characters.ts @@ -176,6 +176,7 @@ export namespace Characters { export const card = async (charId: number): Promise => { const data = await database.query.characters.findFirst({ where: eq(characters.id, charId), + columns: { id: false }, with: { tags: { columns: { @@ -436,6 +437,24 @@ export namespace Characters { }) if (image_id) await copyImage(imageuri, image_id) } + + export const duplicateCard = async (charId: number) => { + const card = await db.query.card(charId) + + if (!card) { + Logger.log('Failed to copy card: Card does not exit', true) + return + } + const cacheLoc = `${FS.cacheDirectory}${card.data.image_id}` + await FS.copyAsync({ + from: getImageDir(card.data.image_id), + to: cacheLoc, + }) + card.data.last_modified = new Date().getTime() + await createCharacter(card, cacheLoc) + .then(() => Logger.log(`Card cloned: ${card.data.name}`)) + .catch((e) => Logger.log(`Failed to clone card: ${e}`)) + } } } diff --git a/app/constants/Style.ts b/app/constants/Style.ts index 39526bc..e7dc428 100644 --- a/app/constants/Style.ts +++ b/app/constants/Style.ts @@ -159,7 +159,7 @@ export namespace Style { }, accent: { h: 180, s: 80, l: 50 }, warning: { h: 50, s: 60, l: 50 }, - destructive: { h: 5, s: 60, l: 50 }, + destructive: { h: 5, s: 150, l: 82 }, confirm: { h: 140, s: 60, l: 50 }, }, setColor: (colors: ColorTypes) => set((state) => ({ ...state, colors: colors })),