diff --git a/src/common/utils/call-event.ts b/src/common/utils/call-event.ts index 70507f262..4a9d8403b 100644 --- a/src/common/utils/call-event.ts +++ b/src/common/utils/call-event.ts @@ -36,6 +36,7 @@ export interface EventName { } bookmarksChanged: Bookmark[] updatedPetSettings: { + instanceId?: string petName?: string petType: PetTypes background?: PetBackgroundId diff --git a/src/context/free-widget/use-widget-sync.ts b/src/context/free-widget/use-widget-sync.ts index d884a5cae..07a3a6dd5 100644 --- a/src/context/free-widget/use-widget-sync.ts +++ b/src/context/free-widget/use-widget-sync.ts @@ -4,10 +4,7 @@ import { setToStorage, watchStorage } from '@/common/storage' import { DEFAULT_COLS, DEFAULT_WIDGET_LAYOUT } from '@/layouts/widgets/layout-engine' import type { StoredWidget } from '@/layouts/widgets/layout-engine/types' import { applyInstanceIdMap, buildInstanceIdMap } from '@/layouts/widgets/instance-id' -import { - migrateWidgetLayoutIfNeeded, - replaceWidgetifyWithPetAndPhoto, -} from '@/layouts/widgets/migration' +import { migrateWidgetLayoutIfNeeded } from '@/layouts/widgets/migration' import { getUserWidgetsApi, syncUserWidgetsApi, @@ -164,11 +161,8 @@ export function useWidgetSync({ disabled: sw.disabled, })) - const { layout: replacedWidgets, hasChanged: wasReplaced } = - replaceWidgetifyWithPetAndPhoto(rawWidgets) - const fromSrv: StoredWidget[] = sanitizeLayout( - replacedWidgets, + rawWidgets, DEFAULT_COLS ) @@ -176,10 +170,6 @@ export function useWidgetSync({ setSavedLayout(fromSrv) applyRuntimeLayout(reflowForColumns(fromSrv, colsRef.current)) persistLayout(fromSrv) - - if (wasReplaced) { - triggerServerSync(fromSrv) - } } else { const localLayout = await migrateWidgetLayoutIfNeeded() if (localLayout && localLayout.length > 0) { diff --git a/src/layouts/widgets/migration.ts b/src/layouts/widgets/migration.ts index 90508f4da..bd2160b11 100644 --- a/src/layouts/widgets/migration.ts +++ b/src/layouts/widgets/migration.ts @@ -44,51 +44,11 @@ const topWidgets: StoredWidget[] = [ }, ] -export function replaceWidgetifyWithPetAndPhoto(widgets: StoredWidget[]): { - layout: StoredWidget[] - hasChanged: boolean -} { - let hasChanged = false - const result: StoredWidget[] = [] - - for (const widget of widgets) { - if (widget.id === ('widgetify' as any)) { - hasChanged = true - const col = widget.position?.col ?? 6 - const baseRow = widget.position?.row ?? 0 - - result.push({ - id: WidgetKeys.photo, - instanceId: 'photo-default', - position: { col, row: baseRow }, - size: { w: 2, h: 2 }, - widgetId: 'photo-default', - }) - - result.push({ - id: WidgetKeys.pet, - instanceId: 'pet-default', - position: { col, row: baseRow + 2 }, - size: { w: 2, h: 1 }, - widgetId: 'pet-default', - }) - } else { - result.push(widget) - } - } - - return { layout: result, hasChanged } -} - export async function migrateWidgetLayoutIfNeeded(): Promise { const existingStored = await getFromStorage('storedWidgets') if (Array.isArray(existingStored)) { - const { layout, hasChanged } = replaceWidgetifyWithPetAndPhoto(existingStored) - if (hasChanged) { - await setToStorage('storedWidgets', layout) - } - return layout + return existingStored } const { appearance, activeWidgets } = await getMultipleFromStorage([ diff --git a/src/layouts/widgets/pet/constants.ts b/src/layouts/widgets/pet/constants.ts index a32bdc9c0..e69576bbb 100644 --- a/src/layouts/widgets/pet/constants.ts +++ b/src/layouts/widgets/pet/constants.ts @@ -42,9 +42,15 @@ export const PET_SPECIES_LABEL: Record = { [PetTypes.CAT]: 'گربه', } -export const DEFAULT_PET_BACKGROUND: PetBackgroundId = 'forest' +export const DEFAULT_PET_BACKGROUND: PetBackgroundId = 'none' export const PET_BACKGROUNDS: Record = { + none: { + id: 'none', + label: 'بدون محیط', + image: null, + groundOffsetPx: 0, + }, forest: { id: 'forest', label: 'جنگل شب', diff --git a/src/layouts/widgets/pet/pet-setting.tsx b/src/layouts/widgets/pet/pet-setting.tsx index 004a80c3b..f8a8596fc 100644 --- a/src/layouts/widgets/pet/pet-setting.tsx +++ b/src/layouts/widgets/pet/pet-setting.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react' import { getFromStorage } from '@/common/storage' import { callEvent } from '@/common/utils/call-event' import { TextInput } from '@/components/ui' +import { useFreeWidgets } from '@/context/free-widget/free-widget.context' import { PetOptionTile } from './components/pet-option-tile' import { BASE_PET_OPTIONS, @@ -11,7 +12,7 @@ import { PET_PREVIEW, PET_SPECIES_LABEL, } from './constants' -import { type PetBackgroundId, PetTypes } from './types' +import { type PetBackgroundId, type PetMeta, PetTypes } from './types' import { getPetBackground } from './utils/get-pet-background' const PET_LIST = Object.keys(BASE_PET_OPTIONS.petOptions) as PetTypes[] @@ -21,13 +22,34 @@ const TIPS = [ 'برای غذا دادن، در محیط اطراف آن کلیک کنید', ] -export function PetSettings() { - const [petType, setPetType] = useState(PetTypes.DOG) - const [petName, setPetName] = useState('') - const [background, setBackground] = useState(DEFAULT_PET_BACKGROUND) +interface PetSettingsProps { + instanceId?: string + size?: { w: number; h: number } +} + +export function PetSettings({ instanceId }: PetSettingsProps = {}) { + const { runtimeLayout, updateWidgetSettings } = useFreeWidgets() + const targetWidget = instanceId + ? runtimeLayout.find((w) => w.instanceId === instanceId) + : null + const targetMeta = targetWidget?.meta as PetMeta | undefined + + const [petType, setPetType] = useState(targetMeta?.petType || PetTypes.DOG) + const [petName, setPetName] = useState(targetMeta?.petName || '') + const [background, setBackground] = useState( + targetMeta?.background || DEFAULT_PET_BACKGROUND + ) useEffect(() => { async function load() { + if (instanceId && targetMeta) { + const type = targetMeta.petType || PetTypes.DOG + setPetType(type) + setPetName(targetMeta.petName || BASE_PET_OPTIONS.petOptions[type].name) + setBackground(targetMeta.background || DEFAULT_PET_BACKGROUND) + return + } + const stored = await getFromStorage('pets') if (!stored?.petOptions) return @@ -38,26 +60,60 @@ export function PetSettings() { } load() - }, []) + }, [instanceId, targetMeta]) function onChangePetName(value: string) { setPetName(value) - callEvent('updatedPetSettings', { petName: value, petType }) + if (instanceId) { + updateWidgetSettings(instanceId, { + ...targetMeta, + petType, + petName: value, + background, + }) + } + callEvent('updatedPetSettings', { instanceId, petName: value, petType }) } async function onChangePetType(value: PetTypes) { const stored = await getFromStorage('pets') - setPetType(value) - setPetName( + const fallbackName = stored?.petOptions[value]?.name ?? BASE_PET_OPTIONS.petOptions[value].name - ) - callEvent('updatedPetSettings', { petType: value }) + + setPetType(value) + setPetName(fallbackName) + + if (instanceId) { + updateWidgetSettings(instanceId, { + ...targetMeta, + petType: value, + petName: fallbackName, + background, + }) + } + callEvent('updatedPetSettings', { + instanceId, + petType: value, + petName: fallbackName, + }) } function onChangeBackground(value: PetBackgroundId) { setBackground(value) - callEvent('updatedPetSettings', { petType, background: value }) + if (instanceId) { + updateWidgetSettings(instanceId, { + ...targetMeta, + petType, + petName, + background: value, + }) + } + callEvent('updatedPetSettings', { + instanceId, + petType, + background: value, + }) } const displayName = petName.trim() || BASE_PET_OPTIONS.petOptions[petType].name @@ -69,7 +125,7 @@ export function PetSettings() {
حیوان خانگی -
+
{PET_LIST.map((type) => (
{PET_BACKGROUND_LIST.map((item) => ( onChangeBackground(item.id)} >
+ className="w-full h-12 flex items-center justify-center bg-base-300/40" + style={ + item.image + ? { + backgroundImage: `url(${item.image})`, + backgroundSize: 'cover', + backgroundPosition: 'bottom center', + } + : undefined + } + >
))}
diff --git a/src/layouts/widgets/pet/pet.context.tsx b/src/layouts/widgets/pet/pet.context.tsx index 992919a32..646fd570c 100644 --- a/src/layouts/widgets/pet/pet.context.tsx +++ b/src/layouts/widgets/pet/pet.context.tsx @@ -10,7 +10,7 @@ import { import { getFromStorage, setToStorage } from '@/common/storage' import { listenEvent } from '@/common/utils/call-event' import { BASE_PET_OPTIONS, HUNGER_GAIN_STEPS } from './constants' -import { type PetHungerState, type PetSettings, PetTypes } from './types' +import { type PetHungerState, type PetMeta, type PetSettings, PetTypes } from './types' interface PetSettingsContextType extends PetSettings { getCurrentPetName: (petType: PetTypes) => string @@ -22,12 +22,55 @@ interface PetSettingsContextType extends PetSettings { const PetContext = createContext(undefined) -export function PetProvider({ children }: { children: React.ReactNode }) { +interface PetProviderProps { + children: React.ReactNode + meta?: PetMeta + instanceId?: string +} + +export function PetProvider({ children, meta, instanceId }: PetProviderProps) { const [settings, setSettings] = useState({ ...BASE_PET_OPTIONS, + petType: meta?.petType || BASE_PET_OPTIONS.petType, + background: meta?.background || BASE_PET_OPTIONS.background, + petOptions: { + ...BASE_PET_OPTIONS.petOptions, + ...(meta?.petType && meta?.petName + ? { + [meta.petType]: { + ...BASE_PET_OPTIONS.petOptions[meta.petType], + name: meta.petName, + }, + } + : {}), + }, }) const pendingPersistRef = useRef(null) + useEffect(() => { + if (meta) { + setSettings((prev) => { + const activeType = meta.petType || prev.petType || PetTypes.DOG + return { + ...prev, + petType: activeType, + background: meta.background ?? prev.background, + petOptions: { + ...prev.petOptions, + ...(meta.petName + ? { + [activeType]: { + ...prev.petOptions[activeType], + name: meta.petName, + }, + } + : {}), + }, + } + }) + } + }, [meta]) + useEffect(() => { const pending = pendingPersistRef.current if (!pending) return @@ -47,23 +90,56 @@ export function PetProvider({ children }: { children: React.ReactNode }) { setToStorage('pets', { ...BASE_PET_OPTIONS, }) - setSettings({ - ...BASE_PET_OPTIONS, - }) - } else { - setSettings({ + setSettings(() => ({ ...BASE_PET_OPTIONS, - ...storedPets, + petType: meta?.petType || BASE_PET_OPTIONS.petType, + background: meta?.background || BASE_PET_OPTIONS.background, petOptions: { ...BASE_PET_OPTIONS.petOptions, - ...(storedPets.petOptions || {}), + ...(meta?.petType && meta?.petName + ? { + [meta.petType]: { + ...BASE_PET_OPTIONS.petOptions[meta.petType], + name: meta.petName, + }, + } + : {}), }, + })) + } else { + setSettings((prev) => { + const mergedOptions = { + ...BASE_PET_OPTIONS.petOptions, + ...(storedPets.petOptions || {}), + } + const resolvedType = + meta?.petType || + storedPets.petType || + prev.petType || + PetTypes.DOG + if (meta?.petName) { + mergedOptions[resolvedType] = { + ...mergedOptions[resolvedType], + name: meta.petName, + } + } + return { + ...BASE_PET_OPTIONS, + ...storedPets, + petType: meta?.petType || storedPets.petType || prev.petType, + background: + meta?.background || + storedPets.background || + prev.background, + petOptions: mergedOptions, + } }) } } else { const initialSettings = { ...BASE_PET_OPTIONS, - petType: PetTypes.DOG, + petType: meta?.petType || PetTypes.DOG, + background: meta?.background || BASE_PET_OPTIONS.background, } setSettings(initialSettings) await setToStorage('pets', initialSettings) @@ -77,11 +153,15 @@ export function PetProvider({ children }: { children: React.ReactNode }) { return () => { cancelled = true } - }, []) + }, [meta]) useEffect(() => { const event = listenEvent('updatedPetSettings', (data) => { if (data) { + if (data.instanceId && instanceId && data.instanceId !== instanceId) { + return + } + setSettings((prevSettings) => { const newSettings = { ...prevSettings } @@ -107,7 +187,9 @@ export function PetProvider({ children }: { children: React.ReactNode }) { : newSettings.background, } - setToStorage('pets', updatedSettings) + if (!data.instanceId) { + setToStorage('pets', updatedSettings) + } return updatedSettings }) @@ -117,7 +199,7 @@ export function PetProvider({ children }: { children: React.ReactNode }) { return () => { event() } - }, []) + }, [instanceId]) const getCurrentPetName = useCallback( (petType: PetTypes) => settings.petOptions[petType]?.name ?? '', diff --git a/src/layouts/widgets/pet/pet.widget.tsx b/src/layouts/widgets/pet/pet.widget.tsx index d69face55..28d5e9be2 100644 --- a/src/layouts/widgets/pet/pet.widget.tsx +++ b/src/layouts/widgets/pet/pet.widget.tsx @@ -2,6 +2,7 @@ import { getPetBackground } from './utils/get-pet-background' import { PetProvider, usePetContext } from './pet.context' import { PetFactory } from './components/pet-factory' import { WidgetContainer } from '../widget-container' +import type { PetMeta } from './types' function PetScene() { const { background } = usePetContext() @@ -12,7 +13,7 @@ function PetScene() { className="relative w-full h-24 overflow-hidden" style={ { - backgroundImage: `url(${scene.image})`, + backgroundImage: scene.image ? `url(${scene.image})` : undefined, backgroundSize: 'auto 100%', backgroundPosition: 'bottom center', '--pet-ground': `${scene.groundOffsetPx}px`, @@ -24,9 +25,14 @@ function PetScene() { ) } -export function PetWidget() { +interface PetWidgetProps { + meta?: PetMeta + instanceId?: string +} + +export function PetWidget({ meta, instanceId }: PetWidgetProps = {}) { return ( - + diff --git a/src/layouts/widgets/pet/types.ts b/src/layouts/widgets/pet/types.ts index 59cf808aa..3d05534fc 100644 --- a/src/layouts/widgets/pet/types.ts +++ b/src/layouts/widgets/pet/types.ts @@ -10,12 +10,18 @@ export enum PetTypes { export type PetSpecies = 'dog' | 'chicken' | 'crab' | 'frog' | 'cat' -export type PetBackgroundId = 'forest' | 'autumn' | 'beach' +export type PetBackgroundId = 'none' | 'forest' | 'autumn' | 'beach' + +export interface PetMeta { + petType?: PetTypes + petName?: string + background?: PetBackgroundId +} export interface PetBackground { id: PetBackgroundId label: string - image: string + image: string | null groundOffsetPx: number } diff --git a/src/layouts/widgets/widget-registry.tsx b/src/layouts/widgets/widget-registry.tsx index e0732b03f..c8f3b8871 100644 --- a/src/layouts/widgets/widget-registry.tsx +++ b/src/layouts/widgets/widget-registry.tsx @@ -69,7 +69,9 @@ export const WIDGET_DEFINITIONS: Record = { defaultSize: { w: 2, h: 1 }, settingsTab: WidgetTabKeys.Pet, canDuplicate: true, - node: (_instanceId) => , + node: (instanceId, _size, meta) => ( + + ), }, [WidgetKeys.wigiPad]: { id: WidgetKeys.wigiPad,