Skip to content

Commit

Permalink
feat: added editing submenu to character list, adding card cloning
Browse files Browse the repository at this point in the history
  • Loading branch information
Vali-98 committed Sep 23, 2024
1 parent 576ad73 commit ffdddb8
Show file tree
Hide file tree
Showing 6 changed files with 273 additions and 68 deletions.
215 changes: 215 additions & 0 deletions app/components/CharacterMenu/CharacterEditPopup.tsx
Original file line number Diff line number Diff line change
@@ -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<void>
label: string
iconName: 'copy' | 'pencil' | 'trash'
warning?: boolean
}

const PopupOption: React.FC<PopupProps> = ({ onPress, label, iconName, warning = false }) => {
return (
<MenuOption>
<TouchableOpacity style={styles.popupButton} onPress={onPress}>
<FontAwesome
style={{ minWidth: 20 }}
name={iconName}
size={18}
color={Style.getColor(warning ? 'destructive-brand' : 'primary-text2')}
/>
<Text style={warning ? styles.optionLabelWarning : styles.optionLabel}>
{label}
</Text>
</TouchableOpacity>
</MenuOption>
)
}

const CharacterEditPopup: React.FC<CharacterEditPopupProps> = ({
characterInfo,
setNowLoading,
nowLoading,
}) => {
const [showMenu, setShowMenu] = useState<boolean>(false)
const menuRef: React.MutableRefObject<Menu | null> = 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 (
<Menu
ref={menuRef}
onOpen={() => setShowMenu(true)}
onClose={() => setShowMenu(false)}
renderer={Popover}
rendererProps={{
placement: 'left',
anchorStyle: styles.anchor,
openAnimationDuration: 150,
closeAnimationDuration: 0,
}}>
<MenuTrigger disabled={nowLoading}>
<AntDesign
style={styles.triggerButton}
color={Style.getColor(showMenu ? 'primary-text3' : 'primary-text2')}
name="edit"
size={26}
/>
</MenuTrigger>
<MenuOptions customStyles={menustyle}>
<PopupOption onPress={() => editCharacter()} label="Edit" iconName="pencil" />
<PopupOption
onPress={() => {
cloneCard()
}}
label="Clone"
iconName="copy"
/>
<PopupOption onPress={() => deleteCard()} label="Delete" iconName="trash" warning />
</MenuOptions>
</Menu>
)
}

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'),
},
}
2 changes: 1 addition & 1 deletion app/components/CharacterMenu/CharacterList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ const CharacterList: React.FC<CharacterListProps> = ({ showHeader }) => {

{characterList.length !== 0 && (
<FlatList
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
data={characterList}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item, index }) => (
Expand Down
48 changes: 17 additions & 31 deletions app/components/CharacterMenu/CharacterListing.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -46,27 +46,20 @@ const CharacterListing: React.FC<CharacterListingProps> = ({

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)
Expand Down Expand Up @@ -134,14 +127,11 @@ const CharacterListing: React.FC<CharacterListingProps> = ({
size={28}
/>
) : (
<TouchableOpacity
style={styles.secondaryButton}
onPress={async () => {
setCurrentCharacter(character.id, true)
}}
disabled={nowLoading}>
<AntDesign color={Style.getColor('primary-text2')} name="edit" size={26} />
</TouchableOpacity>
<CharacterEditPopup
characterInfo={character}
setNowLoading={setNowLoading}
nowLoading={nowLoading}
/>
)}
</View>
</View>
Expand Down Expand Up @@ -181,11 +171,6 @@ const styles = StyleSheet.create({
flex: 1,
},

secondaryButton: {
paddingHorizontal: 12,
paddingVertical: 20,
},

avatar: {
width: 48,
height: 48,
Expand All @@ -196,6 +181,7 @@ const styles = StyleSheet.create({

nametag: {
fontSize: 16,
fontWeight: '500',
color: Style.getColor('primary-text1'),
},

Expand Down
55 changes: 20 additions & 35 deletions app/components/CharacterMenu/CharacterNewMenu.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -55,15 +49,17 @@ const CharacterNewMenu: React.FC<CharacterNewMenuProps> = ({
}) => {
const menuRef: React.MutableRefObject<Menu | null> = 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) => ({
Expand Down Expand Up @@ -138,24 +134,13 @@ const CharacterNewMenu: React.FC<CharacterNewMenuProps> = ({
rendererProps={{ placement: 'bottom', anchorStyle: styles.anchor }}>
<MenuTrigger>
<View>
{!showMenu && (
<Animated.View style={styles.headerButtonContainer} entering={ZoomIn}>
<FontAwesome
name="plus"
size={28}
color={Style.getColor('primary-text1')}
/>
</Animated.View>
)}
{showMenu && (
<Animated.View style={styles.headerButtonContainer} entering={ZoomIn}>
<Ionicons
name="close"
size={28}
color={Style.getColor('primary-text1')}
/>
</Animated.View>
)}
<Animated.View style={styles.headerButtonContainer} entering={ZoomIn}>
<FontAwesome
name="plus"
size={28}
color={Style.getColor(showMenu ? 'primary-text2' : 'primary-text1')}
/>
</Animated.View>
</View>
</MenuTrigger>
<MenuOptions customStyles={menustyle}>
Expand Down
Loading

0 comments on commit ffdddb8

Please sign in to comment.