diff --git a/app/src/components/settings/panels/MascotPanel.tsx b/app/src/components/settings/panels/MascotPanel.tsx index c9e105f0c..2c72de375 100644 --- a/app/src/components/settings/panels/MascotPanel.tsx +++ b/app/src/components/settings/panels/MascotPanel.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef, useState } from 'react'; +import { CustomGifMascot } from '../../../features/human/Mascot'; import { BackendMascot } from '../../../features/human/Mascot/backend/BackendMascot'; import type { MascotDetail, MascotSummary } from '../../../features/human/Mascot/backend/types'; import { getMascotPalette, type MascotColor } from '../../../features/human/Mascot/mascotPalette'; @@ -9,13 +10,16 @@ import { fetchMascotList, getCachedMascotDetail } from '../../../services/mascot import { useAppDispatch, useAppSelector } from '../../../store/hooks'; import { DEFAULT_MASCOT_COLOR, + isCustomMascotGifUrl, type MascotVoiceGender, + selectCustomMascotGifUrl, selectEffectiveMascotVoiceId, selectMascotColor, selectMascotVoiceGender, selectMascotVoiceId, selectMascotVoiceUseLocaleDefault, selectSelectedMascotId, + setCustomMascotGifUrl, setMascotColor, setMascotVoiceGender, setMascotVoiceId, @@ -52,6 +56,7 @@ const MascotPanel = () => { const dispatch = useAppDispatch(); const storedColor = useAppSelector(selectMascotColor); const selectedMascotId = useAppSelector(selectSelectedMascotId); + const customMascotGifUrl = useAppSelector(selectCustomMascotGifUrl); const storedVoiceId = useAppSelector(selectMascotVoiceId); const voiceGender = useAppSelector(selectMascotVoiceGender); const useLocaleDefault = useAppSelector(selectMascotVoiceUseLocaleDefault); @@ -64,6 +69,8 @@ const MascotPanel = () => { const [backendListError, setBackendListError] = useState(null); const [activeDetail, setActiveDetail] = useState(null); const [detailError, setDetailError] = useState(null); + const [customGifDraft, setCustomGifDraft] = useState(customMascotGifUrl ?? ''); + const [customGifError, setCustomGifError] = useState(null); // Voice picker state — paste-mode is sticky because we can't derive it // from the stored value alone (a curated preset id and "user is @@ -138,6 +145,35 @@ const MascotPanel = () => { const handleSelectBackend = (id: string | null) => { dispatch(setSelectedMascotId(id)); + setCustomGifError(null); + if (id == null) { + setCustomGifDraft(''); + dispatch(setCustomMascotGifUrl(null)); + } else { + setCustomGifDraft(''); + } + }; + + const onSaveCustomGif = () => { + const trimmed = customGifDraft.trim(); + setCustomGifDraft(trimmed); + if (trimmed.length === 0) { + setCustomGifError(null); + dispatch(setCustomMascotGifUrl(null)); + return; + } + if (!isCustomMascotGifUrl(trimmed)) { + setCustomGifError(t('settings.mascot.customGifError')); + return; + } + setCustomGifError(null); + dispatch(setCustomMascotGifUrl(trimmed)); + }; + + const onResetCustomGif = () => { + setCustomGifDraft(''); + setCustomGifError(null); + dispatch(setCustomMascotGifUrl(null)); }; // Filter the menu to colors the asset pipeline currently supports — guards @@ -455,6 +491,56 @@ const MascotPanel = () => {

{t('settings.mascot.characterHeading')}

+
+ + {customGifError && ( +

+ {customGifError} +

+ )} + {customMascotGifUrl && ( +
+
+ +
+
+ )} +
{backendListError && (

@@ -477,14 +563,14 @@ const MascotPanel = () => {