From f740676cf08a2947206756a358e5e8d46d12ebd5 Mon Sep 17 00:00:00 2001 From: Vali98 Date: Sun, 13 Oct 2024 18:15:01 +0800 Subject: [PATCH] feat: added custom Alert component --- app/_layout.tsx | 2 + app/components/Alert.tsx | 159 ++++++++++++++++++ .../CharacterMenu/CharacterEditPopup.tsx | 46 ++++- .../ChatMenu/ChatWindow/EditorModal.tsx | 2 +- app/components/FadeBackdrop.tsx | 24 +++ app/constants/Characters.ts | 67 ++++++-- 6 files changed, 279 insertions(+), 21 deletions(-) create mode 100644 app/components/Alert.tsx create mode 100644 app/components/FadeBackdrop.tsx diff --git a/app/_layout.tsx b/app/_layout.tsx index dbbc72b..a85bfec 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,3 +1,4 @@ +import { AlertBox } from '@components/Alert' import { db, rawdb } from '@db' import { Style, initializeApp, startupApp } from '@globals' import { useMigrations } from 'drizzle-orm/expo-sqlite/migrator' @@ -45,6 +46,7 @@ const Layout = () => { {__DEV__ && } + void | Promise + type?: 'warning' | 'default' +} + +type AlertProps = { + title: string + description: string + buttons: AlertButtonProps[] + alignButtons?: 'left' | 'right' +} + +type AlertState = { + visible: boolean + props: AlertProps + hide: () => void + show: (props: AlertProps) => void +} + +export namespace Alert { + export const alert = (props: AlertProps) => { + useAlert.getState().show(props) + } +} + +const useAlert = create()((set, get) => ({ + visible: false, + props: { + title: 'Are You Sure?', + description: 'LIke `sure` sure?', + buttons: [ + { label: 'Cancel', onPress: () => {}, type: 'default' }, + { label: 'Confirm', onPress: () => {}, type: 'warning' }, + ], + alignButtons: 'right', + }, + hide: () => { + set((state) => ({ ...state, visible: false })) + }, + show: (props: AlertProps) => { + set((state) => ({ ...state, visible: true, props: props })) + }, +})) + +type AlertProviderProps = { + children: ReactNode +} + +const AlertButton: React.FC = ({ label, onPress, type = 'default' }) => { + return ( + { + onPress && onPress() + useAlert.getState().hide() + }}> + {label} + + ) +} + +export const AlertBox = () => { + const { visible, props } = useAlert((state) => ({ visible: state.visible, props: state.props })) + + return ( + + { + useAlert.getState().hide() + }}> + + + {props.title} + {props.description} + + {props.buttons.map((item, index) => ( + + ))} + + + + + + ) +} + +export const AlertProvider: React.FC = ({ children }) => { + return ( + + + {children} + + ) +} + +const styles = StyleSheet.create({ + modal: { + flex: 1, + }, + textBoxContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + + textBox: { + backgroundColor: Style.getColor('primary-surface2'), + paddingHorizontal: 32, + paddingBottom: 16, + paddingTop: 24, + borderRadius: 16, + width: '90%', + }, + + title: { + color: Style.getColor('primary-text1'), + fontSize: 20, + fontWeight: '500', + marginBottom: 12, + }, + + description: { + color: Style.getColor('primary-text1'), + marginBottom: 12, + fontSize: 16, + }, + + buttonContainer: { + flexDirection: 'row', + columnGap: 24, + justifyContent: 'flex-end', + alignItems: 'center', + }, + + button: { + paddingHorizontal: 4, + paddingVertical: 8, + color: Style.getColor('primary-text2'), + }, + + buttonWarning: { + paddingHorizontal: 4, + paddingVertical: 8, + color: '#d2574b', + }, +}) + +const buttonStyleMap = { + warning: styles.buttonWarning, + default: styles.button, +} diff --git a/app/components/CharacterMenu/CharacterEditPopup.tsx b/app/components/CharacterMenu/CharacterEditPopup.tsx index 054d37e..42734a9 100644 --- a/app/components/CharacterMenu/CharacterEditPopup.tsx +++ b/app/components/CharacterMenu/CharacterEditPopup.tsx @@ -1,9 +1,10 @@ +import { Alert } from '@components/Alert' import { CharInfo } from '@constants/Characters' 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 { StyleSheet, TouchableOpacity, Text, BackHandler } from 'react-native' import { Menu, MenuOption, @@ -61,6 +62,24 @@ const CharacterEditPopup: React.FC = ({ })) const deleteCard = () => { + Alert.alert({ + title: 'Delete Chracter', + description: `Are you sure you want to delete '${characterInfo.name}'? This cannot be undone.`, + buttons: [ + { + label: 'Cancel', + }, + { + label: 'Delete Character', + onPress: async () => { + Characters.db.mutate.deleteCard(characterInfo.id ?? -1) + }, + type: 'warning', + }, + ], + }) + + /* Alert.alert( `Delete Character`, `Are you sure you want to delete '${characterInfo.name}'? This cannot be undone.`, @@ -76,11 +95,30 @@ const CharacterEditPopup: React.FC = ({ }, ], { cancelable: true } - ) + )*/ } const cloneCard = () => { - Alert.alert( + Alert.alert({ + title: 'Clone Character', + description: `Are you sure you want to clone '${characterInfo.name}'?`, + buttons: [ + { + label: 'Cancel', + }, + { + label: 'Clone Character', + onPress: async () => { + setNowLoading(true) + await Characters.db.mutate.duplicateCard(characterInfo.id) + menuRef.current?.close() + setNowLoading(false) + }, + }, + ], + }) + + /* Alert.alert( `Clone Character`, `Are you sure you want to clone '${characterInfo.name}'?`, [ @@ -97,7 +135,7 @@ const CharacterEditPopup: React.FC = ({ }, ], { cancelable: true } - ) + ) */ } const editCharacter = async () => { diff --git a/app/components/ChatMenu/ChatWindow/EditorModal.tsx b/app/components/ChatMenu/ChatWindow/EditorModal.tsx index e3513a4..43370e8 100644 --- a/app/components/ChatMenu/ChatWindow/EditorModal.tsx +++ b/app/components/ChatMenu/ChatWindow/EditorModal.tsx @@ -45,7 +45,7 @@ type EditorProps = { } type FadeScreenProps = { - handleOverlayClick: (e: GestureResponderEvent) => void + handleOverlayClick?: (e: GestureResponderEvent) => void children: ReactElement } const FadeScreen: React.FC = ({ handleOverlayClick, children }) => { diff --git a/app/components/FadeBackdrop.tsx b/app/components/FadeBackdrop.tsx new file mode 100644 index 0000000..3560755 --- /dev/null +++ b/app/components/FadeBackdrop.tsx @@ -0,0 +1,24 @@ +import { ReactNode } from 'react' +import { GestureResponderEvent, TouchableOpacity } from 'react-native' + +type FadeScreenProps = { + handleOverlayClick?: (e: GestureResponderEvent) => void + children: ReactNode +} + +const FadeBackrop: React.FC = ({ handleOverlayClick, children }) => { + return ( + + {children} + + ) +} + +export default FadeBackrop diff --git a/app/constants/Characters.ts b/app/constants/Characters.ts index 89f07b5..455b633 100644 --- a/app/constants/Characters.ts +++ b/app/constants/Characters.ts @@ -10,9 +10,11 @@ import { tags, } from 'db/schema' import { desc, eq, inArray, notInArray } from 'drizzle-orm' +import { useLiveQuery } from 'drizzle-orm/expo-sqlite' import { randomUUID } from 'expo-crypto' import * as DocumentPicker from 'expo-document-picker' import * as FS from 'expo-file-system' +import { useEffect } from 'react' import { create } from 'zustand' import { createJSONStorage, persist } from 'zustand/middleware' @@ -45,6 +47,7 @@ type CharacterCardState = { card?: CharacterCardV2 tokenCache: CharacterTokenCache | undefined id: number | undefined + updateCard: (card: CharacterCardV2) => void setCard: (id: number) => Promise unloadCard: () => void getImage: () => string @@ -74,6 +77,9 @@ export namespace Characters { tokenCache: undefined, })) }, + updateCard: (card: CharacterCardV2) => { + set((state) => ({ ...state, card: card })) + }, getImage: () => { return getImageDir(get().card?.data.image_id ?? 0) }, @@ -139,6 +145,9 @@ export namespace Characters { set((state) => ({ ...state, card: card, id: id, tokenCache: undefined })) return card?.data.name }, + updateCard: (card: CharacterCardV2) => { + set((state) => ({ ...state, card: card })) + }, unloadCard: () => { set((state) => ({ ...state, @@ -185,10 +194,9 @@ export namespace Characters { export namespace db { export namespace query { - export const card = async (charId: number): Promise => { - const data = await database.query.characters.findFirst({ + export const cardQuery = (charId: number) => { + return database.query.characters.findFirst({ where: eq(characters.id, charId), - columns: { id: false }, with: { tags: { columns: { @@ -201,19 +209,30 @@ export namespace Characters { alternate_greetings: true, }, }) - if (data) - return { - spec: 'chara_card_v2', - spec_version: '2.0', - data: { - ...data, - last_modified: data?.last_modified ?? 0, // assume this never actually fails - tags: data.tags.map((item) => item.tag.tag), - alternate_greetings: data.alternate_greetings.map( - (item) => item.greeting - ), - }, - } + } + + export const card = async (charId: number): Promise => { + const data = await cardQuery(charId) + return cardEntryToCV2(data) + } + + type CardEntry = Awaited> + + export const cardEntryToCV2 = (data: CardEntry): CharacterCardV2 | undefined => { + if (!data) return + + const { id, ...rest } = data + + return { + spec: 'chara_card_v2', + spec_version: '2.0', + data: { + ...rest, + last_modified: rest?.last_modified ?? 0, // assume this never actually fails + tags: rest.tags.map((item) => item.tag.tag), + alternate_greetings: rest.alternate_greetings.map((item) => item.greeting), + }, + } } export const cardList = async ( @@ -638,6 +657,22 @@ export namespace Characters { if (!fileinfo.exists) await copyFileRes(resName, cardDefaultDir) await createCharacterFromImage(cardDefaultDir) } + + export const useCharacterUpdater = () => { + const { id, updateCard } = useCharacterCard((state) => ({ + id: state.id, + updateCard: state.updateCard, + })) + + const { data } = useLiveQuery(db.query.cardQuery(id ?? -1)) + + useEffect(() => { + if (id && id === data?.id) { + const card = db.query.cardEntryToCV2(data) + if (card) updateCard(card) + } + }, [data]) + } } export type CharacterCardV2Data = {