UPDATE: Extra Setting for FF GO
All checks were successful
Gitea Auto Deploy / Deploy-Container (push) Successful in 2m24s

This commit is contained in:
2025-10-02 23:27:42 +07:00
parent 91ce8320d1
commit ab664eb8ea
22 changed files with 728 additions and 430 deletions

View File

@@ -248,6 +248,11 @@
"downRoll": "减少副属性",
"actions": "操作",
"avatars": "头像",
"quickView": "快速预览"
"quickView": "快速预览",
"extraSetting": "额外设置",
"disableCensorship": "禁用审查",
"hideUI": "隐藏界面",
"theoryCraftMode": "理论研究模式",
"cycleCount": "循环次数"
}
}

View File

@@ -249,6 +249,11 @@
"downRoll": "Down Roll",
"actions": "Actions",
"avatars": "Avatars",
"quickView": "Quick View"
"quickView": "Quick View",
"extraSetting": "Extra Settings",
"disableCensorship": "Disable Censorship",
"hideUI": "Hide UI",
"theoryCraftMode": "Theorycraft Mode",
"cycleCount": "Cycle Count"
}
}

View File

@@ -248,6 +248,11 @@
"downRoll": "サブステータスを減らす",
"actions": "アクション",
"avatars": "アバター",
"quickView": "クイックビュー"
"quickView": "クイックビュー",
"extraSetting": "追加設定",
"disableCensorship": "検閲を無効化",
"hideUI": "UIを非表示",
"theoryCraftMode": "シアリークラフトモード",
"cycleCount": "サイクル数"
}
}

View File

@@ -248,6 +248,11 @@
"downRoll": "부옵션 감소",
"actions": "동작",
"avatars": "아바타",
"quickView": "빠른 조회"
"quickView": "빠른 조회",
"extraSetting": "추가 설정",
"disableCensorship": "검열 비활성화",
"hideUI": "UI 숨기기",
"theoryCraftMode": "이론 제작 모드",
"cycleCount": "사이클 수"
}
}

View File

@@ -248,6 +248,11 @@
"downRoll": "Giảm dòng",
"actions": "Hành động",
"avatars": "Nhân vật",
"quickView": "Xem nhanh"
"quickView": "Xem nhanh",
"extraSetting": "Cài đặt bổ sung",
"disableCensorship": "Tắt kiểm duyệt",
"hideUI": "Ẩn giao diện",
"theoryCraftMode": "Chế độ Theorycraft",
"cycleCount": "Số vòng"
}
}

View File

@@ -248,6 +248,11 @@
"downRoll": "减少副属性",
"actions": "操作",
"avatars": "头像",
"quickView": "快速预览"
"quickView": "快速预览",
"extraSetting": "额外设置",
"disableCensorship": "禁用审查",
"hideUI": "隐藏界面",
"theoryCraftMode": "理论研究模式",
"cycleCount": "循环次数"
}
}

View File

@@ -38,7 +38,7 @@ export default function ActionBar() {
const [profileName, setProfileName] = useState("");
const [formState, setFormState] = useState("EDIT");
const [profileEdit, setProfileEdit] = useState(-1);
const { isConnectPS, setIsConnectPS } = useGlobalStore()
const { isConnectPS } = useGlobalStore()
const profileCurrent = useMemo(() => {
if (!avatarSelected) return null;
@@ -157,20 +157,71 @@ export default function ActionBar() {
toast.success(transI18n("syncSuccess"))
} else {
toast.error(`${transI18n("syncFailed")}: ${res.message}`)
setIsConnectPS(false)
}
} else {
const res = await connectToPS()
if (res.success) {
toast.success(transI18n("connectedSuccess"))
setIsConnectPS(true)
} else {
toast.error(`${transI18n("connectedFailed")}: ${res.message}`)
setIsConnectPS(false)
}
}
}
const modalConfigs = [
{
id: "update_profile_modal",
title: formState === "CREATE" ? transI18n("createNewProfile") : transI18n("editProfile"),
onClose: () => {
setIsOpenCreateProfile(false)
handleCloseModal("update_profile_modal")
},
content: (
<div className="px-6 space-y-4">
<div className="form-control">
<label className="label">
<span className="label-text text-primary font-semibold text-lg">
{transI18n("profileName")}
</span>
</label>
<input
type="text"
placeholder={transI18n("placeholderProfileName")}
className="input input-warning mt-1 w-full"
value={profileName}
onChange={(e) => setProfileName(e.target.value)}
/>
</div>
<div className="modal-action">
<button className="btn btn-success btn-sm sm:btn-md" onClick={handleUpdateProfile}>
{formState === "CREATE" ? transI18n("create") : transI18n("update")}
</button>
</div>
</div>
)
},
{
id: "copy_profile_modal",
title: transI18n("copyProfiles").toUpperCase(),
onClose: () => {
setIsOpenCopy(false)
handleCloseModal("copy_profile_modal")
},
content: <CopyImport />
},
{
id: "avatars_modal",
title: transI18n("avatars").toUpperCase(),
onClose: () => {
setIsOpenAvatars(false)
handleCloseModal("avatars_modal")
},
content: <AvatarBar onClose={() => { setIsOpenAvatars(false); handleCloseModal("avatars_modal") }} />
}
]
return (
<div className="w-full px-4 pb-4 bg-base-200">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 items-center justify-items-center">
@@ -376,96 +427,28 @@ export default function ActionBar() {
</button>
</div>
<dialog id="update_profile_modal" className="modal">
{modalConfigs.map(({ id, title, onClose, content }) => (
<dialog key={id} id={id} className="modal">
<div className="modal-box w-11/12 max-w-7xl max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="sticky top-0 z-10">
<motion.button
whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }}
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
onClick={() => {
setIsOpenCreateProfile(false)
handleCloseModal("update_profile_modal")
}}
>
</motion.button>
</div>
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-400">
{formState === "CREATE" ? transI18n("createNewProfile") : transI18n("editProfile")}
</h3>
</div>
<div className="px-6 space-y-4">
<div className="form-control">
<label className="label">
<span className="label-text text-primary font-semibold text-lg">{transI18n("profileName")}</span>
</label>
<input type="text" placeholder={transI18n("placeholderProfileName")} className="input input-warning mt-1 w-full"
value={profileName}
onChange={(e) => setProfileName(e.target.value)}
/>
</div>
<div className="modal-action">
<button className="btn btn-success btn-sm sm:btn-md" onClick={handleUpdateProfile}>
{formState === "CREATE" ? transI18n("create") : transI18n("update")}
</button>
</div>
</div>
</div>
</dialog>
<dialog id="copy_profile_modal" className="modal">
<div className="modal-box w-11/12 max-w-7xl max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="sticky top-0 z-10">
<motion.button
whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }}
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
onClick={() => {
setIsOpenCopy(false)
handleCloseModal("copy_profile_modal")
}}
onClick={onClose}
>
</motion.button>
</div>
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-400">
{transI18n("copyProfiles").toUpperCase()}
{title}
</h3>
</div>
<CopyImport />
</div>
</dialog>
<dialog id="avatars_modal" className="modal">
<div className="modal-box w-11/12 max-w-7xl max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="sticky top-0 z-10">
<motion.button
whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }}
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
onClick={() => {
setIsOpenAvatars(false)
handleCloseModal("avatars_modal")
}}
>
</motion.button>
</div>
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-400">
{transI18n("avatars").toUpperCase()}
</h3>
</div>
<AvatarBar onClose={() => {setIsOpenAvatars(false); handleCloseModal("avatars_modal")}} />
{content}
</div>
</dialog>
))}
</div>

View File

@@ -0,0 +1,167 @@
"use client"
import { connectToPS, syncDataToPS } from "@/helper"
import useConnectStore from "@/stores/connectStore"
import useGlobalStore from "@/stores/globalStore"
import { useTranslations } from "next-intl"
import { useState } from "react"
export default function ConnectBar() {
const transI18n = useTranslations("DataPage")
const [message, setMessage] = useState({ text: '', type: '' });
const {
connectionType,
privateType,
serverUrl,
username,
password,
setConnectionType,
setPrivateType,
setServerUrl,
setUsername,
setPassword
} = useConnectStore()
const { isConnectPS } = useGlobalStore()
return (
<div className="px-6 py-4">
{/* Select connection type */}
<div className="form-control grid grid-cols-1 w-full mb-6">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("connectionType")}</span>
</label>
<select
className="select w-full select-bordered border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={connectionType}
onChange={(e) => setConnectionType(e.target.value)}
>
<option value="FireflyGo">FireflyGo</option>
<option value="Other">{transI18n("other")}</option>
</select>
</div>
{/* Show host/port if Other */}
{connectionType === "Other" && (
<div className="flex flex-col md:space-x-4 mb-6 gap-2">
<div className="form-control w-full mb-4 md:mb-0">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("serverUrl")}</span>
</label>
<input
type="text"
placeholder={transI18n("placeholderServerUrl")}
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
/>
</div>
<div className="form-control w-full mb-4 md:mb-0">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("privateType")}</span>
</label>
<select
className="select w-full select-bordered border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={privateType}
onChange={(e) => setPrivateType(e.target.value)}
>
<option value="Local">{transI18n("local")}</option>
<option value="Server">{transI18n("server")}</option>
</select>
</div>
<div className="form-control w-full mb-4 md:mb-0">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("username")}</span>
</label>
<input
type="text"
placeholder={transI18n("placeholderUsername")}
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className="form-control w-full mb-4 md:mb-0">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("password")}</span>
</label>
<input
type="password"
placeholder={transI18n("placeholderPassword")}
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
</div>
)}
{message.text && (
<div className={`alert ${message.type === 'success' ? 'alert-success' :
message.type === 'error' ? 'alert-error' : 'alert-info'
} mb-6`}>
<span>{message.text}</span>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6 mb-2">
{/* Status */}
<div className="flex items-center justify-center md:justify-start">
<span className="text-md mr-2">{transI18n("status")}:</span>
<span
className={`badge ${isConnectPS ? "badge-success" : "badge-error"
} badge-lg`}
>
{isConnectPS ? transI18n("connected") : transI18n("unconnected")}
</span>
</div>
{/* Buttons */}
<div className="flex flex-col sm:flex-row gap-2 w-full justify-center md:justify-end">
<button
onClick={async () => {
const response = await connectToPS();
if (response.success) {
setMessage({
type: "success",
text: transI18n("connectedSuccess"),
});
} else {
setMessage({
type: "error",
text: response.message,
});
}
}}
className="btn btn-primary w-full sm:w-auto"
>
{transI18n("connectPs")}
</button>
{isConnectPS && (
<button
onClick={async () => {
const response = await syncDataToPS();
if (response.success) {
setMessage({
type: "success",
text: transI18n("syncSuccess"),
});
} else {
setMessage({
type: "error",
text: `${transI18n("syncFailed")}: ${response.message}`,
});
}
}}
className="btn btn-success w-full sm:w-auto"
>
{transI18n("sync")}
</button>
)}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,120 @@
'use client'
import { motion } from "framer-motion"
import { EyeOff, Eye, Hammer, RefreshCw, ShieldBan } from "lucide-react"
import useGlobalStore from '@/stores/globalStore';
import { useTranslations } from "next-intl";
export default function ExtraSettingBar() {
const { extraData, setExtraData } = useGlobalStore()
const transI18n = useTranslations("DataPage")
return (
<div className="px-4 sm:px-6 py-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Theorycraft Mode */}
{extraData?.theory_craft?.mode !== undefined && (
<motion.div
whileHover={{ scale: 1.02 }}
className="form-control bg-base-200 p-4 rounded-xl shadow"
>
<label className="flex flex-wrap items-center label cursor-pointer justify-start gap-3">
<Hammer className="text-primary" size={20}/>
<input
type="checkbox"
className="toggle toggle-primary"
checked={extraData?.theory_craft?.mode}
onChange={(e) =>
setExtraData({
...extraData,
theory_craft: { ...extraData?.theory_craft, mode: e.target.checked }
})
}
/>
<span className="label-text font-semibold">{transI18n("theoryCraftMode")}</span>
</label>
</motion.div>
)}
{/* Cycle Count */}
{extraData?.theory_craft?.mode && (
<motion.div
whileHover={{ scale: 1.02 }}
className="form-control bg-base-200 p-4 rounded-xl shadow"
>
<label className="flex flex-wrap items-center label justify-start gap-3">
<RefreshCw className="text-info" size={20}/>
<span className="label-text font-semibold">{transI18n("cycleCount")}</span>
<input
type="number"
className="input input-bordered"
value={extraData?.theory_craft?.cycle_count}
onChange={(e) =>
setExtraData({
...extraData,
theory_craft: {
...extraData?.theory_craft,
cycle_count: parseInt(e.target.value) || 1
}
})
}
min="1"
/>
</label>
</motion.div>
)}
{/* Hidden UI */}
{extraData?.setting?.hide_ui !== undefined && (
<motion.div
whileHover={{ scale: 1.02 }}
className="form-control bg-base-200 p-4 rounded-xl shadow"
>
<label className="flex flex-wrap items-center label cursor-pointer justify-start gap-3">
{extraData?.setting?.hide_ui
? <EyeOff className="text-warning" size={20}/>
: <Eye className="text-success" size={20}/>
}
<input
type="checkbox"
className="toggle toggle-secondary"
checked={extraData?.setting?.hide_ui}
onChange={(e) =>
setExtraData({
...extraData,
setting: { ...extraData?.setting, hide_ui: e.target.checked }
})
}
/>
<span className="label-text font-semibold">{transI18n("hideUI")}</span>
</label>
</motion.div>
)}
{/* Censorship */}
{extraData?.setting?.censorship !== undefined && (
<motion.div
whileHover={{ scale: 1.02 }}
className="form-control bg-base-200 p-4 rounded-xl shadow"
>
<label className="flex flex-wrap items-center label cursor-pointer justify-start gap-3">
<ShieldBan className="text-error" size={20}/>
<input
type="checkbox"
className="toggle toggle-accent"
checked={extraData?.setting?.censorship}
onChange={(e) =>
setExtraData({
...extraData,
setting: { ...extraData?.setting, censorship: e.target.checked }
})
}
/>
<span className="label-text font-semibold">{transI18n("disableCensorship")}</span>
</label>
</motion.div>
)}
</div>
</div>
);
}

View File

@@ -1,5 +1,5 @@
"use client"
import { connectToPS, downloadJson, syncDataToPS } from "@/helper";
import { downloadJson } from "@/helper";
import { converterToFreeSRJson } from "@/helper/converterToFreeSRJson";
import { useChangeTheme } from "@/hooks/useChangeTheme";
import { listCurrentLanguage } from "@/constant/constant";
@@ -15,10 +15,11 @@ import useModelStore from "@/stores/modelStore";
import FreeSRImport from "../importBar/freesr";
import { toast } from "react-toastify";
import { micsSchema } from "@/zod";
import useConnectStore from "@/stores/connectStore";
import useGlobalStore from "@/stores/globalStore";
import MonsterBar from "../monsterBar";
import Image from "next/image";
import ConnectBar from "../connectBar";
import ExtraSettingBar from "../extraSettingBar";
const themes = [
{ label: "Winter" },
@@ -55,24 +56,14 @@ export default function Header() {
setIsOpenMonster,
isOpenMonster,
setIsOpenConnect,
isOpenConnect
isOpenConnect,
setIsOpenExtra,
isOpenExtra
} = useModelStore()
const [message, setMessage] = useState({ text: '', type: '' });
const [importModal, setImportModal] = useState("enka");
const {
connectionType,
privateType,
serverUrl,
username,
password,
setConnectionType,
setPrivateType,
setServerUrl,
setUsername,
setPassword
} = useConnectStore()
const { isConnectPS, setIsConnectPS } = useGlobalStore()
const { isConnectPS, extraData } = useGlobalStore()
useEffect(() => {
@@ -134,17 +125,22 @@ export default function Header() {
handleCloseModal("connect_modal");
return;
}
if (!isOpenExtra) {
handleCloseModal("extra_modal");
return;
}
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
handleCloseModal("connect_modal");
handleCloseModal("import_modal");
handleCloseModal("monster_modal");
handleCloseModal("extra_modal");
}
};
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenImport, isOpenMonster, isOpenConnect]);
}, [isOpenImport, isOpenMonster, isOpenConnect, isOpenExtra]);
const handleImportDatabase = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -183,6 +179,56 @@ export default function Header() {
};
const modalConfigs = [
{
id: "connect_modal",
title: transI18n("psConnection"),
isOpen: isOpenConnect,
onClose: () => {
setIsOpenConnect(false)
handleCloseModal("connect_modal")
},
content: <ConnectBar />
},
{
id: "import_modal",
title: transI18n("importSetting"),
isOpen: isOpenImport,
onClose: () => {
setIsOpenImport(false)
handleCloseModal("import_modal")
},
content: (
<>
{importModal === "enka" && <EnkaImport />}
{importModal === "freesr" && <FreeSRImport />}
</>
)
},
{
id: "monster_modal",
title: transI18n("monsterSetting"),
isOpen: isOpenMonster,
onClose: () => {
setIsOpenMonster(false)
handleCloseModal("monster_modal")
},
content: <MonsterBar />
},
{
id: "extra_modal",
title: transI18n("extraSetting"),
isOpen: isOpenExtra,
onClose: () => {
setIsOpenExtra(false)
handleCloseModal("extra_modal")
},
content: <ExtraSettingBar />
}
]
return (
<div className="navbar bg-base-100 shadow-md sticky top-0 z-50 px-3 py-1">
<div className="navbar-start">
@@ -279,6 +325,20 @@ export default function Header() {
{transI18n("monsterSetting")}
</button>
</li>
{extraData && (
<li>
<button
onClick={() => {
setIsOpenExtra(true)
handleShow("extra_modal")
}}
className="disabled px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
>
{transI18n("extraSetting")}
</button>
</li>
)}
</ul>
</div>
@@ -386,6 +446,19 @@ export default function Header() {
{transI18n("monsterSetting")}
</button>
</li>
{extraData && (
<li>
<button
onClick={() => {
setIsOpenExtra(true)
handleShow("extra_modal")
}}
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
>
{transI18n("extraSetting")}
</button>
</li>
)}
</ul>
</div>
@@ -495,17 +568,15 @@ export default function Header() {
</Link>
</div>
<dialog id="connect_modal" className="modal">
{modalConfigs.map(({ id, title, onClose, content }) => (
<dialog key={id} id={id} className="modal">
<div className="modal-box w-11/12 max-w-7xl max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="sticky top-0 z-10">
<motion.button
whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }}
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
onClick={() => {
setIsOpenConnect(false)
handleCloseModal("connect_modal")
}}
onClick={onClose}
>
</motion.button>
@@ -513,207 +584,14 @@ export default function Header() {
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-400">
{"PS Connection"}
{title}
</h3>
</div>
<div className="px-6 py-4">
{/* Select connection type */}
<div className="form-control grid grid-cols-1 w-full mb-6">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("connectionType")}</span>
</label>
<select
className="select w-full select-bordered border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={connectionType}
onChange={(e) => setConnectionType(e.target.value)}
>
<option value="FireflyGo">FireflyGo</option>
<option value="Other">{transI18n("other")}</option>
</select>
</div>
{/* Show host/port if Other */}
{connectionType === "Other" && (
<div className="flex flex-col md:space-x-4 mb-6 gap-2">
<div className="form-control w-full mb-4 md:mb-0">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("serverUrl")}</span>
</label>
<input
type="text"
placeholder={transI18n("placeholderServerUrl")}
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
/>
</div>
<div className="form-control w-full mb-4 md:mb-0">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("privateType")}</span>
</label>
<select
className="select w-full select-bordered border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={privateType}
onChange={(e) => setPrivateType(e.target.value)}
>
<option value="Local">{transI18n("local")}</option>
<option value="Server">{transI18n("server")}</option>
</select>
</div>
<div className="form-control w-full mb-4 md:mb-0">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("username")}</span>
</label>
<input
type="text"
placeholder={transI18n("placeholderUsername")}
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className="form-control w-full mb-4 md:mb-0">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("password")}</span>
</label>
<input
type="password"
placeholder={transI18n("placeholderPassword")}
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
</div>
)}
{message.text && (
<div className={`alert ${message.type === 'success' ? 'alert-success' :
message.type === 'error' ? 'alert-error' : 'alert-info'
} mb-6`}>
<span>{message.text}</span>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6 mb-2">
{/* Status */}
<div className="flex items-center justify-center md:justify-start">
<span className="text-md mr-2">{transI18n("status")}:</span>
<span
className={`badge ${isConnectPS ? "badge-success" : "badge-error"
} badge-lg`}
>
{isConnectPS ? transI18n("connected") : transI18n("unconnected")}
</span>
</div>
{/* Buttons */}
<div className="flex flex-col sm:flex-row gap-2 w-full justify-center md:justify-end">
<button
onClick={async () => {
const response = await connectToPS();
if (response.success) {
setIsConnectPS(true);
setMessage({
type: "success",
text: transI18n("connectedSuccess"),
});
} else {
setIsConnectPS(false);
setMessage({
type: "error",
text: response.message,
});
}
}}
className="btn btn-primary w-full sm:w-auto"
>
{transI18n("connectPs")}
</button>
{isConnectPS && (
<button
onClick={async () => {
const response = await syncDataToPS();
if (response.success) {
setMessage({
type: "success",
text: transI18n("syncSuccess"),
});
} else {
setMessage({
type: "error",
text: `${transI18n("syncFailed")}: ${response.message}`,
});
}
}}
className="btn btn-success w-full sm:w-auto"
>
{transI18n("sync")}
</button>
)}
</div>
</div>
</div>
{content}
</div>
</dialog>
<dialog id="import_modal" className="modal">
<div className="modal-box w-11/12 max-w-7xl max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="sticky top-0 z-10">
<motion.button
whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }}
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
onClick={() => {
handleCloseModal("import_modal")
setIsOpenImport(false)
}}
>
</motion.button>
</div>
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-400">
{transI18n("importSetting")}
</h3>
</div>
{importModal === "enka" && <EnkaImport />}
{importModal === "freesr" && <FreeSRImport />}
</div>
</dialog>
<dialog id="monster_modal" className="modal">
<div className="modal-box w-11/12 max-w-7xl max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="sticky top-0 z-10">
<motion.button
whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }}
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
onClick={() => {
handleCloseModal("monster_modal")
setIsOpenMonster(false)
}}
>
</motion.button>
</div>
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-400">
{transI18n("monsterSetting")}
</h3>
</div>
<MonsterBar />
</div>
</dialog>
))}
</div>
)
}

View File

@@ -20,6 +20,7 @@ import { MonsterBasic } from "@/types";
import cloneDeep from 'lodash/cloneDeep'
import { useTranslations } from "next-intl";
import { listCurrentLanguageApi } from "@/constant/constant";
import useGlobalStore from "@/stores/globalStore";
export default function CeBar() {
@@ -33,6 +34,7 @@ export default function CeBar() {
const [showSearchStage, setShowSearchStage] = useState(false)
const [stageSearchTerm, setStageSearchTerm] = useState("")
const [stagePage, setStagePage] = useState(1)
const { extraData, setExtraData } = useGlobalStore()
const pageSize = 30
@@ -110,6 +112,32 @@ export default function CeBar() {
setStagePage(1)
}, [stageSearchTerm])
useEffect(() => {
if (!ce_config) return
if (!extraData || !extraData.theory_craft?.mode) return
const newExtraData = cloneDeep(extraData)
if (!newExtraData.theory_craft.hp) {
newExtraData.theory_craft.hp = {}
}
for (let i = 0; i < ce_config.monsters.length; i++) {
const waveKey = (i + 1).toString()
if (!newExtraData.theory_craft.hp[waveKey]) {
newExtraData.theory_craft.hp[waveKey] = []
}
for (let j = 0; j < ce_config.monsters[i].length; j++) {
if (newExtraData.theory_craft.hp[waveKey][j] === undefined) {
newExtraData.theory_craft.hp[waveKey][j] = 0
}
}
}
setExtraData(newExtraData)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ce_config])
return (
<div className="z-4 py-8 h-full w-full" onClick={() => {
@@ -337,13 +365,13 @@ export default function CeBar() {
</div>
<div className="text-center flex flex-col items-center justify-center">
<div className="text-sm font-medium">
{getLocaleName(locale, listMonsterDetail.find((monster) => monster.id === member.monster_id.toString())) } {`(${member.monster_id})`}
{getLocaleName(locale, listMonsterDetail.find((monster) => monster.id === member.monster_id.toString()))} {`(${member.monster_id})`}
</div>
<div className="flex items-center gap-1 mt-1">
<span className="text-sm">Lv.</span>
<div className="flex items-center gap-1 mt-1 mx-2">
<span className="text-sm">LV.</span>
<input
type="number"
className="w-16 text-center input input-sm"
className="text-center input input-sm"
value={member.level}
onChange={(e) => {
@@ -357,6 +385,31 @@ export default function CeBar() {
}}
/>
</div>
{(extraData?.theory_craft?.mode === true && (
<div className="flex items-center gap-1 mt-1 mx-2">
<span className="text-sm">HP</span>
<input
type="number"
className="text-center input input-sm"
value={extraData?.theory_craft?.hp?.[(waveIndex + 1).toString()]?.[memberIndex] || 0}
onChange={(e) => {
const val = Number(e.target.value)
if (isNaN(val) || val < 0) return
const newData = cloneDeep(extraData)
if (!newData?.theory_craft?.hp?.[(waveIndex + 1).toString()]) {
newData.theory_craft.hp[(waveIndex + 1).toString()] = []
}
newData.theory_craft.hp[(waveIndex + 1).toString()][memberIndex] = val
setExtraData(newData)
}}
/>
</div>
))}
</div>
</div>
@@ -430,7 +483,7 @@ export default function CeBar() {
<div className="relative w-8 h-8 rounded-full overflow-hidden flex-shrink-0 border border-white/10 shadow-sm">
{listMonsterDetail.find((monster2) => monster2.id === monster.id)?.icon?.split("/")?.pop()?.replace(".png", "") && (
<Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonsterDetail.find((monster2) => monster2.id ===monster.id)?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonsterDetail.find((monster2) => monster2.id === monster.id)?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
alt="Enemy Icon"
width={376}
height={512}

View File

@@ -150,6 +150,27 @@ export default function RelicsInfo() {
return listEffects;
}, [avatars, avatarSelected]);
const modalConfigs = [
{
id: "action_detail_modal",
title: null, // không có title
onClose: () => {
setIsOpenRelic(false)
handleCloseModal("action_detail_modal")
},
content: <RelicMaker />
},
{
id: "quick_view_modal",
title: transI18n("quickView").toUpperCase(),
onClose: () => {
setIsOpenQuickView(false)
handleCloseModal("quick_view_modal")
},
content: <QuickView />
}
]
return (
<div className="max-h-[77vh] min-h-[50vh] overflow-y-scroll overflow-x-hidden">
@@ -316,49 +337,33 @@ export default function RelicsInfo() {
</div>
</div>
<dialog id="action_detail_modal" className="modal">
{modalConfigs.map(({ id, title, onClose, content }) => (
<dialog key={id} id={id} className="modal">
<div className="modal-box w-11/12 max-w-7xl max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="sticky top-0 z-10">
<motion.button
whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }}
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
onClick={() => {
setIsOpenRelic(false)
handleCloseModal("action_detail_modal")
}}
onClick={onClose}
>
</motion.button>
</div>
<RelicMaker />
</div>
</dialog>
<dialog id="quick_view_modal" className="modal">
<div className="modal-box w-11/12 max-w-7xl max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="sticky top-0 z-10">
<motion.button
whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }}
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
onClick={() => {
setIsOpenQuickView(false)
handleCloseModal("quick_view_modal")
}}
>
</motion.button>
</div>
{title && (
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-400">
{transI18n("quickView").toUpperCase()}
{title}
</h3>
</div>
<QuickView />
)}
{content}
</div>
</dialog>
))}
</div>
);
}

View File

@@ -4,6 +4,7 @@ import useConnectStore from "@/stores/connectStore"
import useUserDataStore from "@/stores/userDataStore"
import { converterToFreeSRJson } from "./converterToFreeSRJson"
import { pSResponseSchema } from "@/zod"
import useGlobalStore from "@/stores/globalStore"
export const connectToPS = async (): Promise<{ success: boolean, message: string }> => {
const {
@@ -13,6 +14,7 @@ export const connectToPS = async (): Promise<{ success: boolean, message: string
username,
password
} = useConnectStore.getState()
const {setExtraData, setIsConnectPS} = useGlobalStore.getState()
let urlQuery = serverUrl
if (!urlQuery.startsWith("http://") && !urlQuery.startsWith("https://")) {
@@ -36,10 +38,14 @@ export const connectToPS = async (): Promise<{ success: boolean, message: string
}
const response = await SendDataToServer(username, password, urlQuery, null)
if (typeof response === "string") {
setIsConnectPS(false)
return { success: false, message: response }
} else if (response.status != 200) {
setIsConnectPS(false)
return { success: false, message: response.message }
} else {
setIsConnectPS(true)
setExtraData(response?.extra_data)
return { success: true, message: "" }
}
}
@@ -53,6 +59,9 @@ export const syncDataToPS = async (): Promise<{ success: boolean, message: strin
password
} = useConnectStore.getState()
const {extraData, setIsConnectPS, setExtraData} = useGlobalStore.getState()
const {avatars, battle_type, moc_config, pf_config, as_config, ce_config, peak_config} = useUserDataStore.getState()
const data = converterToFreeSRJson(avatars, battle_type, moc_config, pf_config, as_config, ce_config, peak_config)
@@ -76,12 +85,16 @@ export const syncDataToPS = async (): Promise<{ success: boolean, message: strin
return { success: true, message: "" }
}
}
const response = await SendDataToServer(username, password, urlQuery, data)
const response = await SendDataToServer(username, password, urlQuery, data, extraData)
if (typeof response === "string") {
setIsConnectPS(false)
return { success: false, message: response }
} else if (response.status != 200) {
setIsConnectPS(false)
return { success: false, message: response.message }
} else {
setIsConnectPS(true)
setExtraData(response?.extra_data)
return { success: true, message: "" }
}
}

View File

@@ -3,6 +3,7 @@
import { AffixDetail, ASDetail, CharacterDetail, ConfigMaze, FreeSRJson, LightConeDetail, MocDetail, MonsterDetail, PeakDetail, PFDetail, PSResponse, RelicDetail } from "@/types";
import axios from 'axios';
import { pSResponseSchema } from "@/zod";
import { ExtraData } from "@/types";
export async function getConfigMazeApi(): Promise<ConfigMaze> {
try {
@@ -236,11 +237,15 @@ export async function fetchMonsterByIdNative(ids: string, locale: string): Promi
}
}
export async function SendDataToServer(username: string, password: string, serverUrl: string, data: FreeSRJson | null): Promise<PSResponse | string> {
export async function SendDataToServer(
username: string,
password: string,
serverUrl: string,
data: FreeSRJson | null,
extraData?: ExtraData
): Promise<PSResponse | string> {
try {
const response = await axios.post(`${serverUrl}`, { username, password, data })
const response = await axios.post(`${serverUrl}`, { username, password, data, extra_data: extraData })
const parsed = pSResponseSchema.safeParse(response.data)
if (!parsed.success) {
return "Invalid response schema";
@@ -251,7 +256,7 @@ export async function SendDataToServer(username: string, password: string, serve
}
}
export async function SendDataThroughProxy({data}: {data: any}) {
export async function SendDataThroughProxy({ data }: { data: any }) {
try {
const response = await axios.post(`/api/proxy`, { ...data })
return response.data;

View File

@@ -1,12 +1,17 @@
import { create } from 'zustand'
import { ExtraData } from '@/types'
interface GlobalState {
isConnectPS: boolean;
extraData?: ExtraData;
setExtraData: (newExtraData: ExtraData | undefined) => void;
setIsConnectPS: (newIsConnectPS: boolean) => void;
}
const useGlobalStore = create<GlobalState>((set) => ({
isConnectPS: false,
extraData: undefined,
setExtraData: (newExtraData: ExtraData | undefined) => set({ extraData: newExtraData }),
setIsConnectPS: (newIsConnectPS: boolean) => set({ isConnectPS: newIsConnectPS }),
}));

View File

@@ -12,6 +12,8 @@ interface ModelState {
isOpenConnect: boolean;
isOpenAvatars: boolean;
isOpenQuickView: boolean;
isOpenExtra: boolean;
setIsOpenExtra: (newIsOpenExtra: boolean) => void;
setIsOpenQuickView: (newIsOpenQuickView: boolean) => void;
setIsOpenAvatars: (newIsOpenAvatars: boolean) => void;
setIsOpenConnect: (newIsOpenConnect: boolean) => void;
@@ -35,6 +37,8 @@ const useModelStore = create<ModelState>((set) => ({
isOpenConnect: false,
isOpenAvatars: false,
isOpenQuickView: false,
isOpenExtra: false,
setIsOpenExtra: (newIsOpenExtra: boolean) => set({ isOpenExtra: newIsOpenExtra }),
setIsOpenQuickView: (newIsOpenQuickView: boolean) => set({ isOpenQuickView: newIsOpenQuickView }),
setIsOpenAvatars: (newIsOpenAvatars: boolean) => set({ isOpenAvatars: newIsOpenAvatars }),
setIsOpenConnect: (newIsOpenConnect: boolean) => set({ isOpenConnect: newIsOpenConnect }),

13
src/types/extraData.ts Normal file
View File

@@ -0,0 +1,13 @@
export interface ExtraData {
theory_craft: {
hp: Record<string, number[]>
cycle_count: number
mode: boolean
}
setting: {
censorship: boolean
cm: boolean
first_person: boolean
hide_ui: boolean
};
}

View File

@@ -19,3 +19,4 @@ export * from "./mocDetail"
export * from "./monsterValue"
export * from "./peakDetail"
export * from "./monsterDetail"
export * from "./extraData"

View File

@@ -1,3 +1,5 @@
import { ExtraData } from "./extraData";
export interface SubAffix {
sub_affix_id: number;
count: number;
@@ -81,5 +83,6 @@ export interface FreeSRJson {
export interface PSResponse {
status: number;
message: string;
extra_data?: ExtraData
}

15
src/zod/extraData.zod.ts Normal file
View File

@@ -0,0 +1,15 @@
import { z } from "zod";
export const extraDataSchema = z.object({
theory_craft: z.object({
hp: z.record(z.string(), z.array(z.number())),
cycle_count: z.number(),
mode: z.boolean(),
}),
setting: z.object({
censorship: z.boolean(),
cm: z.boolean(),
first_person: z.boolean(),
hide_ui: z.boolean(),
}),
});

View File

@@ -11,3 +11,4 @@ export * from "./mics.zod";
export * from "./relicBasic.zod";
export * from "./relicDetail.zod";
export * from "./srtools.zod";
export * from "./extraData.zod";

View File

@@ -1,5 +1,6 @@
// Generated by ts-to-zod
import { z } from "zod";
import { extraDataSchema } from "./extraData.zod";
export const subAffixSchema = z.object({
sub_affix_id: z.number(),
@@ -87,4 +88,5 @@ export const freeSRJsonSchema = z.object({
export const pSResponseSchema = z.object({
status: z.number(),
message: z.string(),
extra_data: extraDataSchema.optional(),
});