diff --git a/src/components/common/PanoramaBackground.tsx b/src/components/common/PanoramaBackground.tsx index 91d52d0..a55828b 100644 --- a/src/components/common/PanoramaBackground.tsx +++ b/src/components/common/PanoramaBackground.tsx @@ -10,7 +10,10 @@ const PanoramaBackground = React.memo(({ profile, isDay }: PanoramaProps) => { const { isWindowVisible } = useUI(); const baseId = profile; const profileId = baseId ? baseId : "vanilla_tu19"; - const currentPanorama = `/panorama/${profileId}_Panorama_Background_${isDay ? "Day" : "Night"}.png`; + const isCustomUrl = profile.startsWith("data:") || profile.startsWith("blob:"); + const currentPanorama = isCustomUrl + ? profile + : `/panorama/${profileId}_Panorama_Background_${isDay ? "Day" : "Night"}.png`; const [bgWidth, setBgWidth] = useState(null); const containerRef = useRef(null); diff --git a/src/components/modals/CustomizeModal.tsx b/src/components/modals/CustomizeModal.tsx new file mode 100644 index 0000000..5febcb1 --- /dev/null +++ b/src/components/modals/CustomizeModal.tsx @@ -0,0 +1,246 @@ +import { useState, useEffect } from "react"; +import { motion } from "framer-motion"; +import { TauriService } from "../../services/TauriService"; + +function isFilePath(value: string): boolean { + return value.startsWith("/") && !value.startsWith("/images/") && !value.startsWith("/panorama/"); +} + +async function resolvePath(value: string | undefined): Promise { + if (!value) return undefined; + if (isFilePath(value)) { + try { + return await TauriService.readScreenshotAsDataUrl(value); + } catch { + return value; + } + } + return value; +} + +export default function CustomizeModal({ + isOpen, + onClose, + playPressSound, + playBackSound, + editionName, + currentTitleImage, + currentPanorama, + onSave, +}: { + isOpen: boolean; + onClose: () => void; + playPressSound: (s?: string) => void; + playBackSound: (s?: string) => void; + editionName: string; + currentTitleImage?: string; + currentPanorama?: string; + onSave: (updates: { titleImage?: string; panorama?: string }) => void; +}) { + const [titleImage, setTitleImage] = useState(currentTitleImage || ""); + const [panorama, setPanorama] = useState(currentPanorama || ""); + const [focusIndex, setFocusIndex] = useState(0); + + useEffect(() => { + if (isOpen) { + setTitleImage(currentTitleImage || ""); + setPanorama(currentPanorama || ""); + setFocusIndex(0); + } + }, [isOpen, currentTitleImage, currentPanorama]); + + useEffect(() => { + if (!isOpen) return; + const handleKey = (e: KeyboardEvent) => { + if (e.key === "Escape") { + playBackSound("close_click.wav"); + onClose(); + } else if (e.key === "Enter") { + if (focusIndex === 4) { + playBackSound("close_click.wav"); + onClose(); + } else if (focusIndex === 5) { + handleSave(); + } + } else if (e.key === "ArrowDown" || e.key === "Tab") { + e.preventDefault(); + setFocusIndex((prev) => (prev + 1) % 6); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + setFocusIndex((prev) => (prev - 1 + 6) % 6); + } + }; + window.addEventListener("keydown", handleKey); + return () => window.removeEventListener("keydown", handleKey); + }, [isOpen, focusIndex, titleImage, panorama]); + + const handleSave = async () => { + playPressSound("save_click.wav"); + const [resolvedTitle, resolvedPanorama] = await Promise.all([ + resolvePath(titleImage || undefined), + resolvePath(panorama || undefined), + ]); + onSave({ + titleImage: resolvedTitle, + panorama: resolvedPanorama, + }); + onClose(); + }; + + const handlePickFile = async (field: "titleImage" | "panorama") => { + try { + const path = await TauriService.pickFile( + field === "titleImage" ? "Select Title Image" : "Select Panorama Background", + ["png", "jpg", "jpeg", "bmp"], + ); + if (path) { + if (field === "titleImage") setTitleImage(path); + else setPanorama(path); + playPressSound(); + } + } catch (e) { + if (e !== "CANCELED") console.error(e); + } + }; + + const handleReset = (field: "titleImage" | "panorama") => { + playPressSound(); + if (field === "titleImage") setTitleImage(""); + else setPanorama(""); + }; + + if (!isOpen) return null; + + const pickerSection = ( + field: "titleImage" | "panorama", + value: string, + idx: number, + resetIdx: number, + ) => ( +
+ +
+ + {value && ( + + )} +
+ {value && ( +
+ Preview { + (e.target as HTMLImageElement).style.display = "none"; + }} + /> + + {value.split("/").pop() || value} + +
+ )} +
+ ); + + return ( + +
+

+ Customize +

+

+ {editionName} +

+ +
+ {pickerSection("titleImage", titleImage, 0, 1)} + {pickerSection("panorama", panorama, 2, 3)} +
+ +
+ + +
+
+
+ ); +} diff --git a/src/components/views/VersionsView.tsx b/src/components/views/VersionsView.tsx index defb1db..5e8c39b 100644 --- a/src/components/views/VersionsView.tsx +++ b/src/components/views/VersionsView.tsx @@ -5,6 +5,7 @@ import CustomTUModal from "../modals/CustomTUModal"; import SetUidModal from "../modals/SetUidModal"; import ImportWorldModal from "../modals/ImportWorldModal"; import PlaytimeModal from "../modals/PlaytimeModal"; +import CustomizeModal from "../modals/CustomizeModal"; import { useUI, useConfig, @@ -77,6 +78,8 @@ const VersionsView = memo(function VersionsView() { updatesAvailable, addToSteam, cycleBranch, + customizations, + updateCustomization, } = useGame(); const { isDayTime } = useConfig(); const [focusIndex, setFocusIndex] = useState(0); @@ -89,6 +92,8 @@ const VersionsView = memo(function VersionsView() { const [importWorldTarget, setImportWorldTarget] = useState<{ id: string; name: string } | null>(null); const [isPlaytimeModalOpen, setIsPlaytimeModalOpen] = useState(false); const [playtimeTarget, setPlaytimeTarget] = useState<{ id: string; name: string } | null>(null); + const [isCustomizeModalOpen, setIsCustomizeModalOpen] = useState(false); + const [customizeTarget, setCustomizeTarget] = useState(null); const [playtimeMap, setPlaytimeMap] = useState>({}); const [initialPath, setInitialPath] = useState(""); const [hoveredBtn, setHoveredBtn] = useState<{ @@ -673,6 +678,29 @@ const VersionsView = memo(function VersionsView() { Restore +