import { useEffect, useRef, useState } from "react"; import { useShellStore } from "../app/shellStore"; import { retryCloudSave, useCloudSaveStatus } from "../app/onlineAccountClient"; import { useCombatStore } from "../game/combatStore"; import { abilityAtLevel, defaultActionBarForCharacter } from "../game/abilityCatalog"; import { useGameStore } from "../game/store"; import { BAR_COLUMNS, HUD_ELEMENTS, actionKeyForEvent, bindDesktopSlot, defaultUiLayout, exportUiPreferences, importUiPreferences, type HudElementId, type UiDevice, resolveHudScreen, type HudScreen, } from "../game/uiLayout"; import { INPUT_ACTION_BY_ID, conflictingInputActions } from "../input/keybinds"; import { profileInputBindings } from "../game/keybindPreferences"; import { useUiLayoutStore } from "../game/uiLayoutStore"; import { AbilityTooltipTarget } from "./AbilityTooltip"; import { currentDisplayLayout } from "../platform/displayLayout"; export function UiLayoutEditor() { const open = useGameStore((state) => state.overlay === "ui-layout"); const ui = useUiLayoutStore(); const session = useShellStore((state) => state.session); const cloudSave = useCloudSaveStatus(); const abilities = useCombatStore((state) => state.abilities); const level = useCombatStore((state) => state.level); const talentRanks = useCombatStore((state) => state.talentRanks); const [tab, setTab] = useState(ui.device); const [name, setName] = useState("My UI"); const [slot, setSlot] = useState(0); const [message, setMessage] = useState(""); const [collapsed, setCollapsed] = useState(false); const [dockRight, setDockRight] = useState(false); const [search, setSearch] = useState(""); const [pendingImport, setPendingImport] = useState | null>(null); const [transferText, setTransferText] = useState(null); const file = useRef(null); const wasOpen = useRef(false); useEffect(() => { if (open && (!wasOpen.current || !useUiLayoutStore.getState().draft)) { useUiLayoutStore.getState().begin(); setTab(useUiLayoutStore.getState().device); setMessage(""); } if (!open && wasOpen.current) useUiLayoutStore.getState().finish(false); wasOpen.current = open; }, [open, ui.draft !== null]); useEffect(() => { if (!open) return; const onKey = (event: KeyboardEvent) => { const state = useUiLayoutStore.getState(); if (event.target instanceof Element && event.target.closest("input, textarea, select, [contenteditable='true']")) return; if (event.code === "Escape") { event.preventDefault(); event.stopImmediatePropagation(); if (state.selectedButton) useUiLayoutStore.setState({ selectedButton: null }); else { state.finish(false); useGameStore.getState().openOverlay("pause"); } return; } if (state.mode !== "bindings" || !state.selectedButton) return; event.preventDefault(); event.stopImmediatePropagation(); if (event.repeat) return; const key = event.code === "Backspace" || event.code === "Delete" ? null : actionKeyForEvent(event); if (!key && event.code !== "Backspace" && event.code !== "Delete") return; const { bar, slot: button } = state.selectedButton; const bindings = profileInputBindings(state.preferences, "pc"); const conflicts = conflictingInputActions(bindings, "pc", `bar:${bar}:${button}`, key).filter((id) => !id.startsWith("bar:")); if (conflicts.length) { setMessage(`That key belongs to ${conflicts.map((id) => INPUT_ACTION_BY_ID[id].label).join(", ")}. Change it in Options → Keybinds first. Your existing bindings were kept.`); useUiLayoutStore.setState({ selectedButton: null }); return; } state.updateDraft((draft) => bindDesktopSlot(draft, bar, button, key)); setMessage(key ? `Bound bar ${bar + 1}, button ${button + 1}. Any previous assignment was cleared.` : "Keybind cleared."); useUiLayoutStore.setState({ selectedButton: null }); }; window.addEventListener("keydown", onKey, true); return () => window.removeEventListener("keydown", onKey, true); }, [open]); if (!open || !ui.draft) return null; const draft = ui.draft; const dualScreen = currentDisplayLayout() !== "single"; const attempt = (run: () => void) => { try { run(); } catch (error) { setMessage(error instanceof Error ? error.message : "Could not save this UI."); } }; const saveAndClose = () => attempt(() => { ui.finish(true); useGameStore.getState().closeOverlay(); }); const validName = name.trim().length > 0; const chooseSlot = (device: UiDevice, index: number) => { setTab(device); setSlot(index); setName(ui.preferences.slots[device][index]?.name ?? "My UI"); }; const selected = ui.selectedButton; const setAbility = (abilityId: string | null) => { if (!selected) return; ui.updateDraft((layout) => ({ ...layout, bars: layout.bars.map((bar, index) => index !== selected.bar ? bar : { ...bar, slots: bar.slots.map((button, index) => index === selected.slot ? { ...button, abilityId } : button), }) })); }; return