UPDATE: Add muti language
This commit is contained in:
@@ -4,7 +4,9 @@ import { useState, useRef } from 'react'
|
||||
import { X, Image as ImageIcon, Plus, Upload, Check } from 'lucide-react'
|
||||
import useSettingStore from '@/stores/settingStore'
|
||||
import Cropper from 'react-easy-crop'
|
||||
import getCroppedImg from '@/utils/cropImage'
|
||||
import getCroppedImg from '@/utils/cropImage'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from "react-toastify"
|
||||
|
||||
const initialImages = {
|
||||
"bg-1": "bg-1.jpeg",
|
||||
@@ -30,14 +32,26 @@ export const BackgroundSelector = () => {
|
||||
const [croppedAreaPixels, setCroppedAreaPixels] = useState<any>(null)
|
||||
const { background, setBackground, extraBackgrounds, setExtraBackgrounds } = useSettingStore()
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const { t } = useTranslation()
|
||||
const handleSelect = (img: string) => {
|
||||
setIsOpen(false)
|
||||
setBackground(img)
|
||||
}
|
||||
|
||||
const isImageUrl = (url: string) => {
|
||||
return /^https?:\/\/.+\.(jpg|jpeg|png|webp|gif)(\?.*)?$/i.test(url)
|
||||
}
|
||||
const handleAddUrl = () => {
|
||||
if (!newUrl.trim()) return setCroppingImage(newUrl)
|
||||
const url = newUrl.trim()
|
||||
if (!url) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!isImageUrl(url)) {
|
||||
toast.error(t("background.invalid_url"))
|
||||
return
|
||||
}
|
||||
|
||||
setCroppingImage(url)
|
||||
setNewUrl('')
|
||||
}
|
||||
|
||||
@@ -61,8 +75,8 @@ export const BackgroundSelector = () => {
|
||||
const allBackgrounds = [...extraBackgrounds, ...Object.values(initialImages)]
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-4">
|
||||
<div className="tooltip tooltip-right" data-tip="Select Background">
|
||||
<div className="flex flex-col items-center justify-center gap-4 w-full">
|
||||
<div className="tooltip tooltip-right" data-tip={t("background.select_bg")}>
|
||||
<button
|
||||
className="group btn btn-primary btn-circle flex items-center justify-center shadow-md transition-all duration-300 hover:scale-110 hover:shadow-lg hover:bg-primary/80"
|
||||
onClick={() => setIsOpen(true)}
|
||||
@@ -78,19 +92,19 @@ export const BackgroundSelector = () => {
|
||||
<X size={20} />
|
||||
</button>
|
||||
|
||||
<h2 className="text-lg font-semibold mb-4">Choose Background</h2>
|
||||
<h2 className="text-lg font-semibold mb-4">{t("background.choose_bg")}</h2>
|
||||
|
||||
{/* Add via URL */}
|
||||
<div className="flex gap-2 mb-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Paste image URL (https://...)"
|
||||
placeholder={t("background.paste_url")}
|
||||
className="input input-bordered w-full text-info"
|
||||
value={newUrl}
|
||||
onChange={(e) => setNewUrl(e.target.value)}
|
||||
/>
|
||||
<button className="btn btn-success flex items-center gap-1" onClick={handleAddUrl}>
|
||||
<Plus size={16} /> Add
|
||||
<Plus size={16} /> {t("background.add")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -100,7 +114,7 @@ export const BackgroundSelector = () => {
|
||||
className="btn btn-warning flex items-center gap-1"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<Upload size={16} /> Upload from computer
|
||||
<Upload size={16} /> {t("background.upload_from_computer")}
|
||||
</button>
|
||||
<input
|
||||
type="file"
|
||||
@@ -116,9 +130,9 @@ export const BackgroundSelector = () => {
|
||||
</div>
|
||||
|
||||
{/* Crop Modal */}
|
||||
{croppingImage && (
|
||||
{(croppingImage != null && croppingImage != "") && (
|
||||
<div className="fixed inset-0 z-60 flex flex-col items-center justify-center bg-black/70 p-4">
|
||||
<div className="relative w-full max-w-3xl h-[400px] bg-gray-800 rounded-lg">
|
||||
<div className="relative w-full max-w-5xl h-150 bg-gray-800 rounded-lg">
|
||||
<Cropper
|
||||
image={croppingImage}
|
||||
crop={crop}
|
||||
@@ -132,7 +146,7 @@ export const BackgroundSelector = () => {
|
||||
className="absolute bottom-4 left-1/2 -translate-x-1/2 btn btn-success"
|
||||
onClick={handleCropComplete}
|
||||
>
|
||||
<Check size={20} /> Done
|
||||
<Check size={20} /> {t("background.done")}
|
||||
</button>
|
||||
<button
|
||||
className="absolute top-2 right-2 btn btn-ghost btn-circle"
|
||||
@@ -150,9 +164,8 @@ export const BackgroundSelector = () => {
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className={`relative rounded-lg overflow-hidden cursor-pointer border-2 transition-all duration-200 ${
|
||||
value === background ? 'border-blue-500' : 'border-transparent hover:border-gray-500'
|
||||
}`}
|
||||
className={`relative rounded-lg overflow-hidden cursor-pointer border-2 transition-all duration-200 ${value === background ? 'border-blue-500' : 'border-transparent hover:border-gray-500'
|
||||
}`}
|
||||
onClick={() => handleSelect(value)}
|
||||
>
|
||||
<img src={value} alt={`bg-${i}`} loading="lazy" className="w-full h-28 object-cover" />
|
||||
|
||||
@@ -2,6 +2,7 @@ import { motion } from "motion/react"
|
||||
import { AppService } from "@bindings/firefly-launcher/internal/app-service"
|
||||
import { toast } from "react-toastify"
|
||||
import useSettingStore from "@/stores/settingStore"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
export default function CloseModal({
|
||||
isOpen,
|
||||
@@ -12,72 +13,73 @@ export default function CloseModal({
|
||||
}) {
|
||||
if (!isOpen) return null
|
||||
const { closingOption, setClosingOption } = useSettingStore()
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 h-full flex items-center justify-center bg-black/40 backdrop-blur-sm">
|
||||
<div className="relative w-[90%] max-w-2xl bg-base-100 text-base-content rounded-xl border border-purple-500/50 shadow-lg shadow-purple-500/20">
|
||||
<div className="border-b border-purple-500/30 px-6 py-4 mb-4 flex justify-between items-center">
|
||||
<h3 className="font-bold text-xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-600">
|
||||
Confirm Action
|
||||
</h3>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1, rotate: 90 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="btn btn-circle btn-md btn-error absolute right-3 top-3"
|
||||
onClick={onClose}
|
||||
>
|
||||
✕
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
<div className="px-6 pt-2 pb-6">
|
||||
<p className="mb-4 text-lg">
|
||||
Do you want to minimize the application to the system tray or close the application?
|
||||
</p>
|
||||
|
||||
<div className="flex items-center mb-4">
|
||||
<input
|
||||
id="dontAskAgain"
|
||||
type="checkbox"
|
||||
className="checkbox checkbox-sm mr-2"
|
||||
checked={!closingOption.isAsk}
|
||||
onChange={(e) => setClosingOption({ isMinimize: closingOption.isMinimize, isAsk: !e.target.checked })}
|
||||
/>
|
||||
<label htmlFor="dontAskAgain" className="text-sm font-semibold text-accent">
|
||||
Do not ask me again
|
||||
</label>
|
||||
<div className="relative w-[90%] max-w-2xl bg-base-100 text-base-content rounded-xl border border-purple-500/50 shadow-lg shadow-purple-500/20">
|
||||
<div className="border-b border-purple-500/30 px-6 py-4 mb-4 flex justify-between items-center">
|
||||
<h3 className="font-bold text-xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-600">
|
||||
{t("close.title")}
|
||||
</h3>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1, rotate: 90 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="btn btn-circle btn-md btn-error absolute right-3 top-3"
|
||||
onClick={onClose}
|
||||
>
|
||||
✕
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 justify-end gap-3">
|
||||
<button
|
||||
className="btn btn-warning"
|
||||
onClick={async () => {
|
||||
onClose()
|
||||
const [success, message] = await AppService.HideApp()
|
||||
if (!success) toast.error(message)
|
||||
if (!closingOption.isAsk) {
|
||||
setClosingOption({ isMinimize: true, isAsk: false })
|
||||
}
|
||||
}}
|
||||
>
|
||||
Minimize
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-error btn-outline"
|
||||
onClick={async () => {
|
||||
onClose()
|
||||
const [success, message] = await AppService.CloseApp()
|
||||
if (!success) toast.error(message)
|
||||
if (!closingOption.isAsk) {
|
||||
setClosingOption({ isMinimize: false, isAsk: false })
|
||||
}
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<div className="px-6 pt-2 pb-6">
|
||||
<p className="mb-4 text-lg">
|
||||
{t("close.description")}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center mb-4">
|
||||
<input
|
||||
id="dontAskAgain"
|
||||
type="checkbox"
|
||||
className="checkbox checkbox-sm mr-2"
|
||||
checked={!closingOption.isAsk}
|
||||
onChange={(e) => setClosingOption({ isMinimize: closingOption.isMinimize, isAsk: !e.target.checked })}
|
||||
/>
|
||||
<label htmlFor="dontAskAgain" className="text-sm font-semibold text-accent">
|
||||
{t("close.dont_ask")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 justify-end gap-3">
|
||||
<button
|
||||
className="btn btn-warning"
|
||||
onClick={async () => {
|
||||
onClose()
|
||||
const [success, message] = await AppService.HideApp()
|
||||
if (!success) toast.error(message)
|
||||
if (!closingOption.isAsk) {
|
||||
setClosingOption({ isMinimize: true, isAsk: false })
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("close.minimize")}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-error btn-outline"
|
||||
onClick={async () => {
|
||||
onClose()
|
||||
const [success, message] = await AppService.CloseApp()
|
||||
if (!success) toast.error(message)
|
||||
if (!closingOption.isAsk) {
|
||||
setClosingOption({ isMinimize: false, isAsk: false })
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("close.close")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,26 +2,42 @@ import { Link } from "@tanstack/react-router";
|
||||
import useModalStore from "@/stores/modalStore";
|
||||
import { Blend, BookOpen, Diff, Home, Info, Languages, Minus, Puzzle, Settings, TrendingUpDown, Wrench, X } from "lucide-react";
|
||||
import { AppService } from "@bindings/firefly-launcher/internal/app-service";
|
||||
import LanguageSwitcher from "../languageSwitcher";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useSettingStore from "@/stores/settingStore";
|
||||
|
||||
export default function Header() {
|
||||
const { setIsOpenSettingModal } = useModalStore()
|
||||
const { setIsOpenSettingModal, setIsOpenCloseModal } = useModalStore()
|
||||
const { closingOption } = useSettingStore()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const controlButtons = [
|
||||
{
|
||||
icon: <Settings className="w-5 h-5" />,
|
||||
action: () => setIsOpenSettingModal(true),
|
||||
tip: "Settings",
|
||||
tip: t("header.settings"),
|
||||
},
|
||||
{
|
||||
icon: <Minus className="w-5 h-5" />,
|
||||
action: () => AppService.MinimizeApp(),
|
||||
tip: "Minimize",
|
||||
tip: t("header.minimize"),
|
||||
},
|
||||
{
|
||||
icon: <X className="w-5 h-5" />,
|
||||
action: () => AppService.CloseApp(),
|
||||
tip: "Close",
|
||||
action: () => {
|
||||
if (closingOption.isAsk) {
|
||||
setIsOpenCloseModal(true)
|
||||
return
|
||||
}
|
||||
if (closingOption.isMinimize) {
|
||||
AppService.HideApp()
|
||||
return
|
||||
}
|
||||
AppService.CloseApp()
|
||||
},
|
||||
tip: t("header.close"),
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
return (
|
||||
@@ -42,26 +58,26 @@ export default function Header() {
|
||||
tabIndex={0}
|
||||
className="menu menu-sm dropdown-content bg-black/50 backdrop-blur-md rounded-box z-1 mt-3 w-52 p-2 shadow"
|
||||
>
|
||||
<li><Link to="/">Home</Link></li>
|
||||
<li><Link to="/">{t("header.home")}</Link></li>
|
||||
|
||||
<li>
|
||||
<a>Tools</a>
|
||||
<a>{t("header.tools")}</a>
|
||||
<ul className="p-2">
|
||||
<li><Link to="/language">Language</Link></li>
|
||||
<li><Link to="/diff">Diff</Link></li>
|
||||
<li><Link to="/language">{t("header.language")}</Link></li>
|
||||
<li><Link to="/diff">{t("header.client_update")}</Link></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a>Plugins</a>
|
||||
<a>{t("header.plugins")}</a>
|
||||
<ul className="p-2">
|
||||
<li><Link to="/analysis">Analysis (Veritas)</Link></li>
|
||||
<li><Link to="/srtools">SrTools</Link></li>
|
||||
<li><Link to="/analysis">{t("header.analysis")}</Link></li>
|
||||
<li><Link to="/srtools">{t("header.firefly_tools")}</Link></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li><Link to="/howto">How to?</Link></li>
|
||||
<li><Link to="/about">About</Link></li>
|
||||
<li><Link to="/howto">{t("header.how_to")}</Link></li>
|
||||
<li><Link to="/about">{t("header.about")}</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -71,11 +87,11 @@ export default function Header() {
|
||||
<div className="flex flex-col justify-center items-start">
|
||||
<h1 className="text-xl font-bold">
|
||||
<span className="text-emerald-500">Firefly </span>
|
||||
<span className="bg-clip-text text-transparent bg-gradient-to-r from-emerald-400 via-orange-500 to-red-500">
|
||||
<span className="bg-clip-text text-transparent bg-linear-to-r from-emerald-400 via-orange-500 to-red-500">
|
||||
Launcher
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-white text-sm">By Firefly Shelter</p>
|
||||
<p className="text-white text-sm">{t("header.by")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
@@ -89,24 +105,24 @@ export default function Header() {
|
||||
<ul className="menu menu-horizontal px-1 gap-4 text-white">
|
||||
<li>
|
||||
<Link to="/" className="flex items-center gap-2 hover:text-cyan-300">
|
||||
<Home size={18} /> Home
|
||||
<Home size={18} /> {t("header.home")}
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<details>
|
||||
<summary className="flex items-center gap-2 cursor-pointer hover:text-cyan-300">
|
||||
<Wrench size={18} /> Tools
|
||||
<summary className="flex items-center gap-2 cursor-pointer hover:text-cyan-300 w-full">
|
||||
<Wrench size={18} /> {t("header.tools")}
|
||||
</summary>
|
||||
<ul className="p-2 bg-black/75 text-white rounded-lg">
|
||||
<ul className="p-2 bg-black/75 text-white rounded-lg min-w-40 whitespace-nowrap">
|
||||
<li>
|
||||
<Link to="/language" className="flex items-center gap-2 hover:text-cyan-300">
|
||||
<Languages size={18} /> Language
|
||||
<Languages size={18} /> {t("header.language")}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/diff" className="flex items-center gap-2 hover:text-cyan-300">
|
||||
<Diff size={18} /> Client update
|
||||
<Diff size={18} /> {t("header.client_update")}
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -115,18 +131,18 @@ export default function Header() {
|
||||
|
||||
<li>
|
||||
<details>
|
||||
<summary className="flex items-center gap-2 cursor-pointer hover:text-cyan-300">
|
||||
<Puzzle size={18} /> Plugins
|
||||
<summary className="flex items-center gap-2 cursor-pointer hover:text-cyan-300 w-full">
|
||||
<Puzzle size={18} /> {t("header.plugins")}
|
||||
</summary>
|
||||
<ul className="p-2 bg-black/75 text-white rounded-lg">
|
||||
<ul className="p-2 bg-black/75 text-white rounded-lg min-w-40 whitespace-nowrap">
|
||||
<li>
|
||||
<Link to="/analysis" className="flex items-center gap-2 hover:text-cyan-300">
|
||||
<TrendingUpDown size={18} /> Analysis (Veritas)
|
||||
<TrendingUpDown size={18} /> {t("header.analysis")}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/srtools" className="flex items-center gap-2 hover:text-cyan-300">
|
||||
<Blend size={18} /> Firefly Tools
|
||||
<Blend size={18} /> {t("header.firefly_tools")}
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -135,13 +151,13 @@ export default function Header() {
|
||||
|
||||
<li>
|
||||
<Link to="/howto" className="flex items-center gap-2 hover:text-cyan-300">
|
||||
<BookOpen size={18} /> How to?
|
||||
<BookOpen size={18} /> {t("header.how_to")}
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Link to="/about" className="flex items-center gap-2 hover:text-cyan-300">
|
||||
<Info size={18} /> About
|
||||
<Info size={18} /> {t("header.about")}
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -150,9 +166,10 @@ export default function Header() {
|
||||
{/* RIGHT */}
|
||||
<div
|
||||
className="navbar-end flex gap-2 z-52"
|
||||
style={{ '--wails-draggable': 'no-drag' } as any}
|
||||
|
||||
>
|
||||
<div className="flex items-center gap-2 bg-black/40 backdrop-blur-sm rounded-lg">
|
||||
<div className="flex items-center gap-2 bg-black/40 backdrop-blur-sm rounded-lg" style={{ '--wails-draggable': 'no-drag' } as any}>
|
||||
<LanguageSwitcher />
|
||||
{controlButtons.map((btn, i) => (
|
||||
<div key={i} className="tooltip tooltip-bottom" data-tip={btn.tip}>
|
||||
<button
|
||||
@@ -167,4 +184,4 @@ export default function Header() {
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
110
frontend/src/components/languageSwitcher/index.tsx
Normal file
110
frontend/src/components/languageSwitcher/index.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const languages = [
|
||||
{ code: "en", label: "EN", native: "English" },
|
||||
{ code: "vi", label: "VI", native: "Tiếng Việt" },
|
||||
{ code: "ja", label: "JA", native: "日本語" },
|
||||
{ code: "ko", label: "KO", native: "한국어" },
|
||||
{ code: "zh", label: "ZH", native: "中文" },
|
||||
];
|
||||
|
||||
const LanguageSwitcher = () => {
|
||||
const { i18n, t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const current = languages.find((l) => l.code === i18n.language) ?? languages[0];
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handler);
|
||||
return () => document.removeEventListener("mousedown", handler);
|
||||
}, []);
|
||||
|
||||
const select = (code: string) => {
|
||||
i18n.changeLanguage(code);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={ref} className="relative">
|
||||
{/* Trigger */}
|
||||
<button
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="
|
||||
btn btn-ghost px-2
|
||||
flex items-center gap-1
|
||||
text-sm font-semibold tracking-wider
|
||||
text-white/80+
|
||||
select-none
|
||||
tooltip tooltip-bottom
|
||||
"
|
||||
data-tip={t("header.language")}
|
||||
>
|
||||
<span className="uppercase">{current.label}</span>
|
||||
{/* Chevron */}
|
||||
<svg
|
||||
className={`w-3 h-3 text-white/50 transition-transform duration-200 ${open ? "rotate-180" : ""}`}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2.5}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Dropdown */}
|
||||
{open && (
|
||||
<ul
|
||||
className="
|
||||
absolute right-0 mt-2 w-32
|
||||
bg-black/60 backdrop-blur-md
|
||||
border border-white/10 rounded-xl
|
||||
shadow-2xl shadow-black/50
|
||||
overflow-hidden z-999
|
||||
py-1
|
||||
animate-in fade-in slide-in-from-top-2 duration-150
|
||||
"
|
||||
>
|
||||
{languages.map((lang) => {
|
||||
const isActive = lang.code === i18n.language;
|
||||
return (
|
||||
<li key={lang.code}>
|
||||
<button
|
||||
onClick={() => select(lang.code)}
|
||||
className={`
|
||||
w-full flex items-center gap-3 px-4 py-2.5 text-sm
|
||||
transition-colors duration-150
|
||||
${isActive
|
||||
? "bg-emerald-500/20 text-emerald-400"
|
||||
: "text-white/70 hover:bg-white/8 hover:text-white"
|
||||
}
|
||||
`}
|
||||
>
|
||||
<span className="flex-1 text-left">{lang.native}</span>
|
||||
{isActive && (
|
||||
<svg className="w-3.5 h-3.5 text-emerald-400 shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageSwitcher;
|
||||
@@ -3,6 +3,7 @@ import useModalStore from "@/stores/modalStore"
|
||||
import useSettingStore from "@/stores/settingStore"
|
||||
import useLauncherStore from "@/stores/launcherStore"
|
||||
import { toast } from "react-toastify"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
export default function SettingModal({
|
||||
isOpen,
|
||||
@@ -12,7 +13,7 @@ export default function SettingModal({
|
||||
onClose: () => void
|
||||
}) {
|
||||
if (!isOpen) return null
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { setIsOpenSelfUpdateModal } = useModalStore()
|
||||
const { closingOption, setClosingOption, serverVersion,
|
||||
proxyVersion, } = useSettingStore()
|
||||
@@ -20,7 +21,7 @@ export default function SettingModal({
|
||||
const CheckUpdate = async () => {
|
||||
const launcherData = await CheckUpdateLauncher()
|
||||
if (!launcherData.isUpdate) {
|
||||
toast.success("Launcher is already up to date")
|
||||
toast.success(t("setting.launcher_update_success"))
|
||||
return
|
||||
}
|
||||
setUpdateData({
|
||||
@@ -37,8 +38,8 @@ export default function SettingModal({
|
||||
<div className="relative w-[90%] max-w-md bg-base-100 text-base-content rounded-2xl border border-purple-500/30 shadow-2xl shadow-purple-500/30 p-6">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h3 className="font-extrabold text-2xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-500">
|
||||
Settings
|
||||
<h3 className="font-extrabold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-500">
|
||||
{t("setting.title")}
|
||||
</h3>
|
||||
<button
|
||||
className="btn btn-circle btn-sm bg-red-600 hover:bg-red-700 text-white border-none shadow-lg"
|
||||
@@ -52,21 +53,21 @@ export default function SettingModal({
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Section 1: Launcher Update */}
|
||||
<div className="p-4 bg-base-200 rounded-xl border border-purple-300 shadow-sm">
|
||||
<h4 className="font-bold text-lg mb-2">Launcher Update</h4>
|
||||
<h4 className="font-bold text-lg mb-2">{t("setting.launcher_update_title")}</h4>
|
||||
<p className="text-sm text-info mb-3">
|
||||
Check if your launcher is up to date.
|
||||
{t("setting.launcher_update_desc")}
|
||||
</p>
|
||||
<button
|
||||
className="btn btn-primary bg-gradient-to-r from-orange-500 to-red-500 hover:from-orange-400 hover:to-red-500 text-white shadow-md hover:shadow-lg transition-all duration-200"
|
||||
className="btn btn-primary bg-linear-to-r from-orange-500 to-red-500 hover:from-orange-400 hover:to-red-500 text-white shadow-md hover:shadow-lg transition-all duration-200"
|
||||
onClick={CheckUpdate}
|
||||
>
|
||||
Check for Launcher Updates
|
||||
{t("setting.launcher_update_btn")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Section 2: Closing Option */}
|
||||
<div className="p-4 bg-base-200 rounded-xl border border-purple-300 shadow-sm">
|
||||
<h4 className="font-bold text-lg mb-2">Closing Options</h4>
|
||||
<h4 className="font-bold text-lg mb-2">{t("setting.closing_options_title")}</h4>
|
||||
<label className="flex items-start gap-3 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -81,12 +82,10 @@ export default function SettingModal({
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-base font-medium text-info">
|
||||
Set do not ask again
|
||||
{t("setting.set_dont_ask_again")}
|
||||
</span>
|
||||
<span className="text-sm text-accent">
|
||||
Next time you close the app, it will automatically{" "}
|
||||
{closingOption.isMinimize ? "minimize to system tray" : "quit the app"}{" "}
|
||||
without asking.
|
||||
{t('setting.closing_auto_desc', { action: closingOption.isMinimize ? t('setting.action_minimize') : t('setting.action_quit') })}
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
@@ -95,18 +94,18 @@ export default function SettingModal({
|
||||
|
||||
{/* Section 3: Launcher Version */}
|
||||
<div className="p-4 bg-base-200 rounded-xl border border-purple-300 shadow-sm">
|
||||
<h4 className="font-bold text-lg mb-2">Version</h4>
|
||||
<h4 className="font-bold text-lg mb-2">{t("setting.version_label")}</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<p className="text-base text-info">
|
||||
Server: {serverVersion}
|
||||
</p>
|
||||
<p className="text-base text-info">
|
||||
Proxy: {proxyVersion}
|
||||
</p>
|
||||
<p className="text-base text-info">
|
||||
{t("setting.server_label")}: {serverVersion}
|
||||
</p>
|
||||
<p className="text-base text-info">
|
||||
{t("setting.proxy_label")}: {proxyVersion}
|
||||
</p>
|
||||
|
||||
<p className="text-base text-info">
|
||||
Launcher: {launcherVersion}
|
||||
</p>
|
||||
<p className="text-base text-info">
|
||||
{t("setting.launcher_label")}: {launcherVersion}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function UpdateModal({ isOpen, title, message, buttons, onClose }
|
||||
</motion.button>
|
||||
|
||||
<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">
|
||||
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
@@ -46,7 +46,7 @@ export default function UpdateModal({ isOpen, title, message, buttons, onClose }
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className={`btn ${
|
||||
btn.variant === "primary"
|
||||
? "btn-primary bg-gradient-to-r from-orange-200 to-red-400 border-none"
|
||||
? "btn-primary bg-linear-to-r from-orange-200 to-red-400 border-none"
|
||||
: btn.variant === "error"
|
||||
? "btn-error"
|
||||
: "btn-outline btn-error"
|
||||
|
||||
18
frontend/src/i18n.ts
Normal file
18
frontend/src/i18n.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
import httpBackend from "i18next-http-backend";
|
||||
i18n
|
||||
.use(httpBackend)
|
||||
.use(LanguageDetector) // detects browser language
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: "en", //fallback language
|
||||
debug: true,
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
backend: {
|
||||
loadPath: `/locales/{{lng}}.json`, //path of the languages
|
||||
},
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import './styles/index.css'
|
||||
import "../runtime.js"
|
||||
import "@wailsio/runtime";
|
||||
import { routeTree } from './routeTree.gen.js'
|
||||
import "./i18n";
|
||||
|
||||
const router = createRouter({ routeTree })
|
||||
declare module '@tanstack/react-router' {
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function AboutPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-base-200 flex items-center justify-center p-6">
|
||||
<div className="w-full bg-base-100 shadow-xl rounded-2xl p-8 space-y-6">
|
||||
<h1 className="text-4xl font-bold text-primary text-center">About</h1>
|
||||
<h1 className="text-4xl font-bold text-primary text-center">{t("about.title")}</h1>
|
||||
|
||||
<div className="space-y-4">
|
||||
<p className="text-lg leading-relaxed">
|
||||
Hello! We are <span className="font-semibold text-error">Firefly Shelter</span>, a developer team passionate about building useful tools and improving user experiences.
|
||||
{t("about.p1_pre")}<span className="font-semibold text-error">Firefly Shelter</span>{t("about.p1_post")}
|
||||
</p>
|
||||
<p className="text-lg leading-relaxed">
|
||||
I created a lightweight and modern <span className="font-semibold text-success">Game Launcher</span> to help users easily launch and manage their games with better performance and simplicity.
|
||||
{t("about.p2_pre")}<span className="font-semibold text-success">{t("about.p2_highlight")}</span>{t("about.p2_post")}
|
||||
</p>
|
||||
<p className="text-lg leading-relaxed">
|
||||
The launcher is built using <span className="font-mono text-info">Go + Wails3</span>, with a clean and responsive interface styled with <span className="text-accent">Tailwind CSS</span> and <span className="text-accent">DaisyUI</span>.
|
||||
{t("about.p3_pre")}<span className="font-mono text-info">Go + Wails3</span>{t("about.p3_mid")}<span className="text-accent">Tailwind CSS</span>{t("about.p3_and")}<span className="text-accent">DaisyUI</span>{t("about.p3_post")}
|
||||
</p>
|
||||
<p className="text-lg leading-relaxed">
|
||||
My goal is to make tools that are fast, efficient, and enjoyable to use — and this launcher is just the beginning.
|
||||
{t("about.p4")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center pt-4">
|
||||
<Link to="/" className="btn btn-primary btn-wide">Back to Home</Link>
|
||||
<Link to="/" className="btn btn-primary btn-wide">{t("about.btn_back")}</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,18 +1,20 @@
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function AnalysisPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-base-200 flex items-center justify-center p-6">
|
||||
<div className="w-full bg-base-100 shadow-xl rounded-2xl p-8 space-y-8">
|
||||
<h1 className="text-4xl font-bold text-primary text-center">
|
||||
Firefly Analysis & Veritas Plugin
|
||||
{t("analysis.title")}
|
||||
</h1>
|
||||
|
||||
{/* About Veritas Section */}
|
||||
<div className="bg-green-50 border-l-4 border-green-400 p-6 rounded-r-lg">
|
||||
<h2 className="text-2xl font-bold text-green-800 flex items-center gap-2 mb-4">
|
||||
<span>🔬</span>
|
||||
<span>About Veritas</span>
|
||||
<span>{t("analysis.sect1_title")}</span>
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4 text-green-700">
|
||||
@@ -20,17 +22,19 @@ export default function AnalysisPage() {
|
||||
<div className="text-green-600 text-lg">⚡</div>
|
||||
<div>
|
||||
<p className="mb-2">
|
||||
<span className="font-semibold text-success">Veritas</span> is a powerful{" "}
|
||||
<span className="text-info font-mono bg-blue-100 px-2 py-1 rounded">Damage Logger</span> designed for analyzing damage in real-time during gameplay.
|
||||
<span className="font-semibold text-success">{t("analysis.sect1_p1_pre")}</span>
|
||||
{t("analysis.sect1_p1_mid")}
|
||||
<span className="text-info font-mono bg-blue-100 px-2 py-1 rounded">{t("analysis.sect1_p1_highlight")}</span>
|
||||
{t("analysis.sect1_p1_post")}
|
||||
</p>
|
||||
<p className="text-sm">It's lightweight, fast, and easy to use for comprehensive damage analysis.</p>
|
||||
<p className="text-sm">{t("analysis.sect1_p2")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-green-200 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-green-600 text-lg">📁</span>
|
||||
<span className="font-semibold text-green-800">GitHub Repository</span>
|
||||
<span className="font-semibold text-green-800">{t("analysis.sect1_github")}</span>
|
||||
</div>
|
||||
<a
|
||||
href="https://github.com/hessiser/veritas"
|
||||
@@ -44,23 +48,22 @@ export default function AnalysisPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Web Analysis Tools Section */}
|
||||
<div className="bg-blue-50 border-l-4 border-blue-400 p-6 rounded-r-lg">
|
||||
<h2 className="text-2xl font-bold text-blue-800 flex items-center gap-2 mb-4">
|
||||
<span>🌐</span>
|
||||
<span>Web Analysis Tools</span>
|
||||
<span>{t("analysis.sect2_title")}</span>
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<p className="text-blue-700">
|
||||
Use these web applications for real-time damage analysis with Veritas:
|
||||
{t("analysis.sect2_desc")}
|
||||
</p>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="bg-white border border-blue-200 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-blue-600 text-lg">🏆</span>
|
||||
<span className="font-semibold text-blue-800">Master Website</span>
|
||||
<span className="font-semibold text-blue-800">{t("analysis.sect2_master")}</span>
|
||||
</div>
|
||||
<a
|
||||
href="https://sranalysis.punklorde.org"
|
||||
@@ -75,7 +78,7 @@ export default function AnalysisPage() {
|
||||
<div className="bg-white border border-blue-200 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-blue-600 text-lg">🔄</span>
|
||||
<span className="font-semibold text-blue-800">Backup Website</span>
|
||||
<span className="font-semibold text-blue-800">{t("analysis.sect2_backup")}</span>
|
||||
</div>
|
||||
<a
|
||||
href="https://firefly-sranalysis.vercel.app/"
|
||||
@@ -83,7 +86,7 @@ export default function AnalysisPage() {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
https://firefly-sranalysis.vercel.app/
|
||||
https://firefly-sranalysis.vercel.app
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -92,34 +95,33 @@ export default function AnalysisPage() {
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="text-yellow-600 text-lg">💡</div>
|
||||
<p className="text-yellow-800 text-sm">
|
||||
<strong>Tip:</strong> If your country has issues loading from the master site, please use the backup site instead.
|
||||
<strong>{t("analysis.sect2_tip_pre")}</strong>{t("analysis.sect2_tip_post")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Installation Instructions */}
|
||||
<div className="bg-red-50 border-l-4 border-red-400 p-6 rounded-r-lg">
|
||||
<h2 className="text-2xl font-bold text-red-800 flex items-center gap-2 mb-4">
|
||||
<span>⚠️</span>
|
||||
<span>Installation Instructions</span>
|
||||
<span>{t("analysis.sect3_title")}</span>
|
||||
</h2>
|
||||
|
||||
<div className="bg-white border border-red-200 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-red-600 text-xl">📋</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-red-800 mb-2">Important Setup Step</h3>
|
||||
<h3 className="font-semibold text-red-800 mb-2">{t("analysis.sect3_subtitle")}</h3>
|
||||
<p className="text-red-700 mb-2">
|
||||
After downloading Veritas, you must rename the file for it to work properly:
|
||||
{t("analysis.sect3_desc")}
|
||||
</p>
|
||||
<div className="bg-red-100 p-3 rounded-lg">
|
||||
<p className="text-red-800 font-mono text-sm">
|
||||
Rename: <code className="bg-red-200 px-1 py-0.5 rounded">veritas.dll</code> → <code className="bg-red-200 px-1 py-0.5 rounded">astrolabe.dll</code>
|
||||
{t("analysis.sect3_rename")}<code className="bg-red-200 px-1 py-0.5 rounded">veritas.dll</code> → <code className="bg-red-200 px-1 py-0.5 rounded">astrolabe.dll</code>
|
||||
</p>
|
||||
<p className="text-red-800 text-sm mt-1">
|
||||
Then place <code className="bg-red-200 px-1 py-0.5 rounded">astrolabe.dll</code> into your game directory.
|
||||
{t("analysis.sect3_place")}<code className="bg-red-200 px-1 py-0.5 rounded">astrolabe.dll</code>{t("analysis.sect3_place_post")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -127,62 +129,59 @@ export default function AnalysisPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Usage Instructions */}
|
||||
<div className="bg-purple-50 border-l-4 border-purple-400 p-6 rounded-r-lg">
|
||||
<h2 className="text-2xl font-bold text-purple-800 flex items-center gap-2 mb-4">
|
||||
<span>🛠️</span>
|
||||
<span>How to Use Web App</span>
|
||||
<span>{t("analysis.sect4_title")}</span>
|
||||
</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Firefly GO Local */}
|
||||
<div className="bg-white border border-purple-200 rounded-lg p-4">
|
||||
<h3 className="font-semibold text-purple-800 mb-3 flex items-center gap-2">
|
||||
<span className="text-purple-600">🚀</span>
|
||||
<span>For Firefly GO Local</span>
|
||||
<span>{t("analysis.sect4_sub1")}</span>
|
||||
</h3>
|
||||
<div className="space-y-2 text-purple-700">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="font-medium min-w-[20px] text-purple-600">1.</span>
|
||||
<p>Launch the <span className="font-semibold">game</span> and your <span className="font-semibold">Firefly GO Server (PS)</span>.</p>
|
||||
<span className="font-medium min-w-5 text-purple-600">1.</span>
|
||||
<p>{t("analysis.sect4_sub1_step1_pre")}<span className="font-semibold">{t("analysis.sect4_sub1_step1_game")}</span>{t("analysis.sect4_sub1_step1_mid")}<span className="font-semibold">{t("analysis.sect4_sub1_step1_server")}</span>{t("analysis.sect4_sub1_step1_post")}</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="font-medium min-w-[20px] text-purple-600">2.</span>
|
||||
<p>Open one of the web analysis tools.</p>
|
||||
<span className="font-medium min-w-5 text-purple-600">2.</span>
|
||||
<p>{t("analysis.sect4_sub1_step2")}</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="font-medium min-w-[20px] text-purple-600">3.</span>
|
||||
<p>Go to <strong>Connection Settings</strong> → select <strong>Connection Type: PS</strong> → click <strong>Connect</strong>.</p>
|
||||
<span className="font-medium min-w-5 text-purple-600">3.</span>
|
||||
<p>{t("analysis.sect4_sub1_step3_pre")}<strong>{t("analysis.sect4_sub1_step3_conn")}</strong>{t("analysis.sect4_sub1_step3_mid1")}<strong>{t("analysis.sect4_sub1_step3_type")}</strong>{t("analysis.sect4_sub1_step3_mid2")}<strong>{t("analysis.sect4_sub1_step3_btn")}</strong>{t("analysis.sect4_sub1_step3_post")}</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="font-medium min-w-[20px] text-purple-600">4.</span>
|
||||
<p>Once connected, play the game. The tool will automatically analyze in the background.</p>
|
||||
<span className="font-medium min-w-5 text-purple-600">4.</span>
|
||||
<p>{t("analysis.sect4_sub1_step4")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Other Private Servers */}
|
||||
<div className="bg-white border border-purple-200 rounded-lg p-4">
|
||||
<h3 className="font-semibold text-purple-800 mb-3 flex items-center gap-2">
|
||||
<span className="text-purple-600">🌐</span>
|
||||
<span>For Other Private Servers</span>
|
||||
<span>{t("analysis.sect4_sub2")}</span>
|
||||
</h3>
|
||||
<div className="space-y-2 text-purple-700">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="font-medium min-w-[20px] text-purple-600">1.</span>
|
||||
<p>Launch the <span className="font-semibold">game</span> and your <span className="font-semibold">Private Server</span>.</p>
|
||||
<span className="font-medium min-w-5 text-purple-600">1.</span>
|
||||
<p>{t("analysis.sect4_sub2_step1_pre")}<span className="font-semibold">{t("analysis.sect4_sub2_step1_game")}</span>{t("analysis.sect4_sub2_step1_mid")}<span className="font-semibold">{t("analysis.sect4_sub2_step1_server")}</span>{t("analysis.sect4_sub2_step1_post")}</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="font-medium min-w-[20px] text-purple-600">2.</span>
|
||||
<p>Open one of the web analysis tools.</p>
|
||||
<span className="font-medium min-w-5 text-purple-600">2.</span>
|
||||
<p>{t("analysis.sect4_sub2_step2")}</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="font-medium min-w-[20px] text-purple-600">3.</span>
|
||||
<p>Go to <strong>Connection Settings</strong> → select <strong>Connection Type: Native</strong> → click <strong>Connect</strong>.</p>
|
||||
<span className="font-medium min-w-5 text-purple-600">3.</span>
|
||||
<p>{t("analysis.sect4_sub2_step3_pre")}<strong>{t("analysis.sect4_sub2_step3_conn")}</strong>{t("analysis.sect4_sub2_step3_mid1")}<strong>{t("analysis.sect4_sub2_step3_type")}</strong>{t("analysis.sect4_sub2_step3_mid2")}<strong>{t("analysis.sect4_sub2_step3_btn")}</strong>{t("analysis.sect4_sub2_step3_post")}</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="font-medium min-w-[20px] text-purple-600">4.</span>
|
||||
<p>Once connected, play the game normally.</p>
|
||||
<span className="font-medium min-w-5 text-purple-600">4.</span>
|
||||
<p>{t("analysis.sect4_sub2_step4")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -190,7 +189,7 @@ export default function AnalysisPage() {
|
||||
</div>
|
||||
|
||||
<div className="text-center pt-6">
|
||||
<Link to="/" className="btn btn-primary btn-wide">Back to Home</Link>
|
||||
<Link to="/" className="btn btn-primary btn-wide">{t("analysis.btn_back")}</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,17 +2,18 @@ import useSettingStore from "@/stores/settingStore"
|
||||
import { Check, Folder, File, X, Settings } from "lucide-react"
|
||||
import { useEffect } from "react"
|
||||
import { toast } from "react-toastify"
|
||||
import { DiffService} from "@bindings/firefly-launcher/internal/diff-service"
|
||||
import { DiffService } from "@bindings/firefly-launcher/internal/diff-service"
|
||||
import { FSService } from "@bindings/firefly-launcher/internal/fs-service"
|
||||
import { motion } from "motion/react"
|
||||
import useDiffStore from "@/stores/diffStore"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
export default function DiffPage() {
|
||||
const { gameDir, setGameDir } = useSettingStore()
|
||||
const {
|
||||
isLoading,
|
||||
setIsLoading,
|
||||
folderCheckResult,
|
||||
const {
|
||||
isLoading,
|
||||
setIsLoading,
|
||||
folderCheckResult,
|
||||
setFolderCheckResult,
|
||||
diffDir,
|
||||
setDiffDir,
|
||||
@@ -29,6 +30,7 @@ export default function DiffPage() {
|
||||
messageUpdate,
|
||||
setMessageUpdate
|
||||
} = useDiffStore()
|
||||
const { t } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
const getLanguage = async () => {
|
||||
@@ -50,7 +52,7 @@ export default function DiffPage() {
|
||||
|
||||
const handlePickGameFolder = async () => {
|
||||
try {
|
||||
setIsLoading({game: true, diff: false})
|
||||
setIsLoading({ game: true, diff: false })
|
||||
const basePath = await FSService.PickFolder()
|
||||
if (basePath) {
|
||||
setGameDir(basePath)
|
||||
@@ -61,28 +63,28 @@ export default function DiffPage() {
|
||||
setFolderCheckResult(exists ? 'success' : 'error')
|
||||
setGameDir(exists ? basePath : '')
|
||||
if (!exists) {
|
||||
toast.error('Game directory not found. Please select the correct folder.')
|
||||
toast.error(t("diff.toast_game_dir_not_found"))
|
||||
}
|
||||
} else {
|
||||
toast.error('No folder path selected')
|
||||
toast.error(t("diff.toast_no_folder_selected"))
|
||||
setFolderCheckResult('error')
|
||||
setGameDir('')
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast.error('PickFolder error:', err)
|
||||
toast.error(t("diff.toast_pick_folder_error"), err)
|
||||
setFolderCheckResult('error')
|
||||
} finally {
|
||||
setIsLoading({game: false, diff: false})
|
||||
setIsLoading({ game: false, diff: false })
|
||||
}
|
||||
}
|
||||
|
||||
const handlePickDiffFile = async () => {
|
||||
try {
|
||||
setIsLoading({game: false, diff: true})
|
||||
setIsLoading({ game: false, diff: true })
|
||||
const basePath = await FSService.PickFile("")
|
||||
if (basePath) {
|
||||
if (!basePath.endsWith(".7z") && !basePath.endsWith(".zip") && !basePath.endsWith(".rar")) {
|
||||
toast.error('Not valid file type')
|
||||
toast.error(t("diff.toast_invalid_file_type"))
|
||||
setDiffCheckResult('error')
|
||||
setDiffDir('')
|
||||
return
|
||||
@@ -90,15 +92,15 @@ export default function DiffPage() {
|
||||
setDiffDir(basePath)
|
||||
setDiffCheckResult('success')
|
||||
} else {
|
||||
toast.error('No file path selected')
|
||||
toast.error(t("diff.toast_no_file_selected"))
|
||||
setDiffCheckResult('error')
|
||||
setDiffDir('')
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast.error('PickFile error:', err)
|
||||
toast.error(t("diff.toast_pick_file_error"), err)
|
||||
setDiffCheckResult('error')
|
||||
} finally {
|
||||
setIsLoading({game: false, diff: false})
|
||||
setIsLoading({ game: false, diff: false })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,25 +112,25 @@ export default function DiffPage() {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
setIsDiffLoading(true)
|
||||
|
||||
|
||||
if (!gameDir || !diffDir) {
|
||||
toast.error('Please select game directory and diff file')
|
||||
toast.error(t("diff.toast_select_both"))
|
||||
return
|
||||
}
|
||||
|
||||
setStageType('Check Type HDiff')
|
||||
|
||||
setStageType(t("diff.stage_check_type"))
|
||||
setProgressUpdate(0)
|
||||
setMaxProgressUpdate(1)
|
||||
|
||||
|
||||
const [isOk, validType, errorType] = await DiffService.CheckTypeHDiff(diffDir)
|
||||
if (!handleResult(isOk, errorType)) return
|
||||
setProgressUpdate(1)
|
||||
|
||||
|
||||
if (['hdiffmap.json', 'hdifffiles.txt', 'hdifffiles.json'].includes(validType)) {
|
||||
setStageType('Version Validate')
|
||||
setStageType(t("diff.stage_version_validate"))
|
||||
setProgressUpdate(0)
|
||||
setMaxProgressUpdate(1)
|
||||
const [validVersion, errorVersion] = await DiffService.VersionValidate(gameDir, diffDir)
|
||||
@@ -136,37 +138,37 @@ export default function DiffPage() {
|
||||
setProgressUpdate(1)
|
||||
}
|
||||
|
||||
setStageType('Data Extract')
|
||||
setStageType(t("diff.stage_data_extract"))
|
||||
const [validData, errorData] = await DiffService.DataExtract(gameDir, diffDir)
|
||||
if (!handleResult(validData, errorData)) return
|
||||
|
||||
setStageType('Cut Data')
|
||||
|
||||
setStageType(t("diff.stage_cut_data"))
|
||||
setMessageUpdate('')
|
||||
const [validCut, errorCut] = await DiffService.CutData(gameDir)
|
||||
if (!handleResult(validCut, errorCut)) return
|
||||
|
||||
|
||||
switch (validType) {
|
||||
case 'hdifffiles.txt':
|
||||
case 'hdiffmap.json':
|
||||
case 'hdifffiles.json': {
|
||||
setStageType('Patch Data')
|
||||
setStageType(t("diff.stage_patch_data"))
|
||||
const [validPatch, errorPatch] = await DiffService.HDiffPatchData(gameDir)
|
||||
if (!handleResult(validPatch, errorPatch)) return
|
||||
|
||||
setStageType('Delete old files')
|
||||
|
||||
setStageType(t("diff.stage_delete_old_files"))
|
||||
const [validDelete, errorDelete] = await DiffService.DeleteFiles(gameDir)
|
||||
if (!handleResult(validDelete, errorDelete)) return
|
||||
break
|
||||
}
|
||||
case 'manifest': {
|
||||
setStageType('Patch Data')
|
||||
setStageType(t("diff.stage_patch_data"))
|
||||
const [validPatch, errorPatch] = await DiffService.LDiffPatchData(gameDir)
|
||||
if (!handleResult(validPatch, errorPatch)) return
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
toast.success('Update game completed')
|
||||
|
||||
toast.success(t("diff.toast_update_completed"))
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
toast.error(`PickFile error: ${err}`)
|
||||
@@ -174,7 +176,7 @@ export default function DiffPage() {
|
||||
setIsDiffLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="p-2 mx-4">
|
||||
@@ -182,9 +184,9 @@ export default function DiffPage() {
|
||||
{/* Header */}
|
||||
<div className="text-center mb-2">
|
||||
<h1 className="text-4xl font-bold mb-2">
|
||||
🎮 Game Update by Hdiffz
|
||||
{t("diff.header_title")}
|
||||
</h1>
|
||||
<p className="">Help you update game with hdiffz</p>
|
||||
<p className="">{t("diff.header_desc")}</p>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
@@ -194,7 +196,7 @@ export default function DiffPage() {
|
||||
<div className="pb-2">
|
||||
<h2 className="text-2xl font-semibold mb-4 flex items-center gap-2">
|
||||
<Folder className="text-primary" size={24} />
|
||||
Game Directory
|
||||
{t("diff.game_dir_title")}
|
||||
</h2>
|
||||
|
||||
<div className="space-y-1">
|
||||
@@ -205,7 +207,7 @@ export default function DiffPage() {
|
||||
className="btn btn-primary"
|
||||
>
|
||||
<Folder size={20} />
|
||||
{isLoading.game ? 'Selecting...' : 'Select Game Folder'}
|
||||
{isLoading.game ? t("diff.btn_selecting") : t("diff.btn_select_game")}
|
||||
</button>
|
||||
|
||||
{gameDir && (
|
||||
@@ -224,12 +226,12 @@ export default function DiffPage() {
|
||||
{folderCheckResult === 'success' ? (
|
||||
<>
|
||||
<Check size={20} />
|
||||
<span>Valid game directory found!</span>
|
||||
<span>{t("diff.game_dir_valid")}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<X size={20} />
|
||||
<span>Game directory not found. Please select the correct folder.</span>
|
||||
<span>{t("diff.game_dir_invalid")}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -241,7 +243,7 @@ export default function DiffPage() {
|
||||
<div className="pb-2">
|
||||
<h2 className="text-2xl font-semibold mb-4 flex items-center gap-2">
|
||||
<File className="text-primary" size={24} />
|
||||
Diff file Directory
|
||||
{t("diff.diff_file_title")}
|
||||
</h2>
|
||||
|
||||
<div className="space-y-1">
|
||||
@@ -252,7 +254,7 @@ export default function DiffPage() {
|
||||
className="btn btn-primary"
|
||||
>
|
||||
<File size={20} />
|
||||
{isLoading.diff ? 'Selecting...' : 'Select Diff file Folder'}
|
||||
{isLoading.diff ? t("diff.btn_selecting") : t("diff.btn_select_diff")}
|
||||
</button>
|
||||
|
||||
{diffDir && (
|
||||
@@ -271,12 +273,12 @@ export default function DiffPage() {
|
||||
{diffCheckResult === 'success' ? (
|
||||
<>
|
||||
<Check size={20} />
|
||||
<span>Valid diff file found!</span>
|
||||
<span>{t("diff.diff_file_valid")}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<X size={20} />
|
||||
<span>Diff file not found. Please select the correct file.</span>
|
||||
<span>{t("diff.diff_file_invalid")}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -288,10 +290,10 @@ export default function DiffPage() {
|
||||
<button
|
||||
onClick={handleUpdateGame}
|
||||
disabled={!diffDir || !gameDir || isLoading.game || isLoading.diff}
|
||||
className="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 disabled:from-gray-400 disabled:to-gray-500 text-white px-8 py-3 rounded-lg font-medium transition-all duration-200 flex items-center gap-2 shadow-lg hover:shadow-xl disabled:cursor-not-allowed cursor-pointer"
|
||||
className="bg-linear-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 disabled:from-gray-400 disabled:to-gray-500 text-white px-8 py-3 rounded-lg font-medium transition-all duration-200 flex items-center gap-2 shadow-lg hover:shadow-xl disabled:cursor-not-allowed cursor-pointer"
|
||||
>
|
||||
<Settings size={20} />
|
||||
{isDiffLoading ? 'Updating...' : 'Update Game'}
|
||||
{isDiffLoading ? t("diff.btn_updating") : t("diff.btn_update_game") }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -300,8 +302,8 @@ export default function DiffPage() {
|
||||
<div className="fixed inset-0 z-50 h-full flex items-center justify-center bg-black/40 backdrop-blur-sm">
|
||||
<div className="relative w-[90%] max-w-5xl bg-base-100 text-base-content rounded-xl border border-purple-500/50 shadow-lg shadow-purple-500/20">
|
||||
<div className="border-b border-purple-500/30 px-6 py-4 mb-4 text-center">
|
||||
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-400">
|
||||
Update Game
|
||||
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
|
||||
{t("diff.modal_update_title")}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -311,20 +313,20 @@ export default function DiffPage() {
|
||||
<div className="flex justify-center items-center text-sm text-white/80">
|
||||
<span className="font-bold text-lg text-accent">{stageType}:</span>
|
||||
<div className="flex items-center gap-4 ml-2">
|
||||
{stageType !== 'Cut Data' && <span className="text-white font-bold">{progressUpdate.toFixed(0)} / {maxProgressUpdate.toFixed(0)}</span>}
|
||||
{stageType === 'Cut Data' && <span className="text-white font-bold truncate max-w-full overflow-hidden whitespace-nowrap">{messageUpdate}</span>}
|
||||
{stageType !== 'Cut Data' && <span className="text-white font-bold">{progressUpdate.toFixed(0)} / {maxProgressUpdate.toFixed(0)}</span>}
|
||||
{stageType === 'Cut Data' && <span className="text-white font-bold truncate max-w-full overflow-hidden whitespace-nowrap">{messageUpdate}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full bg-white/20 rounded-full h-2 overflow-hidden">
|
||||
<motion.div
|
||||
className="h-full bg-gradient-to-r from-cyan-400 to-blue-500 rounded-full"
|
||||
className="h-full bg-linear-to-r from-cyan-400 to-blue-500 rounded-full"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${(progressUpdate/maxProgressUpdate)*100}%` }}
|
||||
animate={{ width: `${(progressUpdate / maxProgressUpdate) * 100}%` }}
|
||||
transition={{ duration: 0.3 }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center text-lg text-white/60">
|
||||
Please wait...
|
||||
{t("diff.status_wait")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -336,13 +338,13 @@ export default function DiffPage() {
|
||||
|
||||
{/* Instructions */}
|
||||
<div className="bg-info/5 rounded-lg p-4 border border-info/30 mt-6">
|
||||
<h3 className="font-medium text-error mb-2">📋 Instructions:</h3>
|
||||
<h3 className="font-medium text-error mb-2">{t("diff.inst_title")}</h3>
|
||||
<ol className="text-sm text-error space-y-1">
|
||||
<li>1. Click "Select Game Folder" and choose your game's root directory</li>
|
||||
<li>2. Wait for the system to validate the game directory</li>
|
||||
<li>3. Click "Select Diff file Folder" and choose your diff file's root directory</li>
|
||||
<li>4. Wait for the system to validate the diff file directory</li>
|
||||
<li>5. Click "Update Game" to save your changes</li>
|
||||
<li>{t("diff.inst_step_1")}</li>
|
||||
<li>{t("diff.inst_step_2")}</li>
|
||||
<li>{t("diff.inst_step_3")}</li>
|
||||
<li>{t("diff.inst_step_4")}</li>
|
||||
<li>{t("diff.inst_step_5")}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function FireflyToolsPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-base-200 flex items-center justify-center p-6">
|
||||
<div className="w-full bg-base-100 shadow-xl rounded-2xl p-8 space-y-8">
|
||||
<h1 className="text-4xl font-bold text-primary text-center">Firefly Tools</h1>
|
||||
<h1 className="text-4xl font-bold text-primary text-center">{t("fireflytools.title")}</h1>
|
||||
|
||||
{/* Section 1: About SR Tools */}
|
||||
<div className="bg-blue-50 border-l-4 border-blue-400 p-6 rounded-r-lg">
|
||||
<h2 className="text-2xl font-bold text-blue-800 flex items-center gap-2 mb-4">
|
||||
<span>ℹ️</span>
|
||||
<span>About Firefly Tools</span>
|
||||
<span>{t("fireflytools.sect1_title")}</span>
|
||||
</h2>
|
||||
<div className="space-y-3 text-blue-700">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-blue-600 text-lg">🏠</div>
|
||||
<p>
|
||||
This site is a another version of {" "}
|
||||
<span className="font-semibold text-success">Firefly Tools {" "}</span>
|
||||
developed by {" "}
|
||||
<span className="font-semibold text-accent">Firefly Shelter</span>
|
||||
{t("fireflytools.sect1_p1_pre")}
|
||||
<span className="font-semibold text-success">{t("fireflytools.sect1_p1_tool")}</span>
|
||||
{t("fireflytools.sect1_p1_mid")}
|
||||
<span className="font-semibold text-accent">{t("fireflytools.sect1_p1_author")}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white border border-blue-200 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-blue-600 text-lg">🏆</span>
|
||||
<span className="font-semibold text-blue-800">Master Website</span>
|
||||
<span className="font-semibold text-blue-800">{t("fireflytools.sect1_master_site")}</span>
|
||||
</div>
|
||||
<a
|
||||
href="https://srtools.punklorde.org"
|
||||
@@ -38,11 +40,11 @@ export default function FireflyToolsPage() {
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-blue-600 text-lg">👨💻</div>
|
||||
<p>The original tool was created by a third-party developer named <span className="font-semibold text-accent">Amazing</span>. This version is directly based on that work, without modification to core logic.</p>
|
||||
<p>{t("fireflytools.sect1_p2_pre")}<span className="font-semibold text-accent">{t("fireflytools.sect1_p2_author")}</span>{t("fireflytools.sect1_p2_post")}</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-blue-600 text-lg">🔗</div>
|
||||
<p>There is also a more modern version by the same author available at{" "}
|
||||
<p>{t("fireflytools.sect1_p3_pre")}
|
||||
<a
|
||||
href="https://srtools.neonteam.dev"
|
||||
className="link link-accent"
|
||||
@@ -56,75 +58,73 @@ export default function FireflyToolsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 2: Main Features */}
|
||||
<div className="bg-green-50 border-l-4 border-green-400 p-6 rounded-r-lg">
|
||||
<h2 className="text-2xl font-bold text-green-800 flex items-center gap-2 mb-4">
|
||||
<span>🔧</span>
|
||||
<span>Main Features</span>
|
||||
<span>{t("fireflytools.sect2_title")}</span>
|
||||
</h2>
|
||||
<div className="space-y-3 text-green-700">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-green-600 text-lg">⚙️</div>
|
||||
<p>Configure characters, light cones, relics, traces, and eidolons easily in your browser.</p>
|
||||
<p>{t("fireflytools.sect2_feat1")}</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-green-600 text-lg">🔌</div>
|
||||
<p>Instantly apply setups to <span className="font-semibold text-accent">Firefly GO Server</span> using <span className="font-semibold">Connect PS</span> — no manual file uploads required.</p>
|
||||
<p>{t("fireflytools.sect2_feat2_pre")}<span className="font-semibold text-accent">{t("fireflytools.sect2_feat2_server")}</span>{t("fireflytools.sect2_feat2_mid")}<span className="font-semibold">{t("fireflytools.sect2_feat2_conn")}</span>{t("fireflytools.sect2_feat2_post")}</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-green-600 text-2xl">✨</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-green-800 text-lg">Extra Settings</h4>
|
||||
<h4 className="font-semibold text-green-800 text-lg">{t("fireflytools.sect2_feat3_title")}</h4>
|
||||
<p className="text-green-700 mt-1">
|
||||
Enhance your <span className="font-semibold text-accent">Firefly GO Server</span> experience with extra features:
|
||||
{t("fireflytools.sect2_feat3_desc_pre")}<span className="font-semibold text-accent">{t("fireflytools.sect2_feat3_desc_server")}</span>{t("fireflytools.sect2_feat3_desc_post")}
|
||||
</p>
|
||||
<ul className="list-disc list-inside mt-2 space-y-1 text-green-700">
|
||||
<li>🎭 <span className="font-medium">Hidden Game UI</span> — remove the entire game interface.</li>
|
||||
<li>🚫 <span className="font-medium">Disable Censorship</span> — get rid of Lens Flare censor 💀.</li>
|
||||
<li>🧪 <span className="font-medium">Theorycraft Mode</span> — configure HP, cycles, and more via the web.</li>
|
||||
<li>🎭 <span className="font-medium">{t("fireflytools.sect2_feat3_ui")}</span> {t("fireflytools.sect2_feat3_ui_desc")}</li>
|
||||
<li>🚫 <span className="font-medium">{t("fireflytools.sect2_feat3_censor")}</span> {t("fireflytools.sect2_feat3_censor_desc")}</li>
|
||||
<li>🧪 <span className="font-medium">{t("fireflytools.sect2_feat3_tc")}</span> {t("fireflytools.sect2_feat3_tc_desc")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-green-600 text-lg">📂</div>
|
||||
<p>Export and import full builds using <code className="bg-gray-200 px-2 py-1 rounded text-sm">freesr-data.json</code>.</p>
|
||||
<p>{t("fireflytools.sect2_feat4_pre")}<code className="bg-gray-200 px-2 py-1 rounded text-sm">freesr-data.json</code>{t("fireflytools.sect2_feat4_post")}</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-green-600 text-lg">⚡</div>
|
||||
<p>Fast testing workflow — no sync cooldowns, instant in-game updates.</p>
|
||||
<p>{t("fireflytools.sect2_feat5")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 3: Getting Started */}
|
||||
<div className="bg-purple-50 border-l-4 border-purple-400 p-6 rounded-r-lg">
|
||||
<h2 className="text-2xl font-bold text-purple-800 flex items-center gap-2 mb-4">
|
||||
<span>🚀</span>
|
||||
<span>Getting Started</span>
|
||||
<span>{t("fireflytools.sect3_title")}</span>
|
||||
</h2>
|
||||
<div className="space-y-3 text-purple-700">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-purple-600 text-lg">1️⃣</div>
|
||||
<p>Access the tool through your browser at the self-hosted instance.</p>
|
||||
<p>{t("fireflytools.sect3_step1")}</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-purple-600 text-lg">2️⃣</div>
|
||||
<p>Configure your character builds with the intuitive web interface.</p>
|
||||
<p>{t("fireflytools.sect3_step2")}</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-purple-600 text-lg">3️⃣</div>
|
||||
<p>Use <span className="font-semibold">Connect PS</span> feature to instantly sync with your private server.</p>
|
||||
<p>{t("fireflytools.sect3_step3_pre")}<span className="font-semibold">{t("fireflytools.sect3_step3_bold")}</span>{t("fireflytools.sect3_step3_post")}</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-purple-600 text-lg">4️⃣</div>
|
||||
<p>Test your builds in-game with real-time updates and modifications.</p>
|
||||
<p>{t("fireflytools.sect3_step4")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center pt-4">
|
||||
<Link to="/" className="btn btn-primary btn-wide">Back to Home</Link>
|
||||
<Link to="/" className="btn btn-primary btn-wide">{t("fireflytools.btn_back")}</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,49 +1,48 @@
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function HowToPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-base-200 flex items-center justify-center p-6">
|
||||
<div className=" w-full bg-base-100 shadow-xl rounded-2xl p-8 space-y-8">
|
||||
<h1 className="text-4xl font-bold text-primary text-center">How to Use</h1>
|
||||
<h1 className="text-4xl font-bold text-primary text-center">{t("howto.title")}</h1>
|
||||
|
||||
{/* Section 1: Launcher Features */}
|
||||
<div className="bg-green-50 border-l-4 border-green-400 p-6 rounded-r-lg">
|
||||
<h2 className="text-2xl font-bold text-green-800 flex items-center gap-2 mb-4">
|
||||
<span>🚀</span>
|
||||
<span>Using the Launcher Features</span>
|
||||
<span>{t("howto.sect1_title")}</span>
|
||||
</h2>
|
||||
<div className="space-y-3 text-green-700">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-green-600 text-lg">🔄</div>
|
||||
<p>Automatically update <span className="font-semibold text-amber-600">Firefly Go</span> and proxy tools when launching.</p>
|
||||
<p>{t("howto.sect1_auto_update_pre")} <span className="font-semibold text-amber-600">Firefly Go</span> {t("howto.sect1_auto_update_post")}</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-green-600 text-lg">🎮</div>
|
||||
<p>Launch the game directly through the launcher with correct parameters and runtime environment.</p>
|
||||
<p>{t("howto.sect1_launch_game")}</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-green-600 text-lg">🌐</div>
|
||||
<p>Support switching in-game language (e.g., EN, JP, ZH, KR) via{" "}
|
||||
<Link to="/language" className="link link-info font-mono">Language Tools</Link>
|
||||
<p>{t("howto.sect1_lang_pre")} <Link to="/language" className="link link-info font-mono">{t("howto.sect1_lang_link")}</Link>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-green-600 text-2xl">📦</div>
|
||||
<div>
|
||||
<p className="text-green-800 font-semibold">
|
||||
Patch & Update Game Files
|
||||
{t("howto.sect1_patch_title")}
|
||||
</p>
|
||||
<p className="text-green-700">
|
||||
Use the{" "}
|
||||
<Link to="/diff" className="link link-info font-mono">Diff Tool</Link>{" "}
|
||||
(<span className="font-medium">DiffPatch</span>) for fast & lightweight incremental updates.
|
||||
{t("howto.sect1_patch_desc_1_pre")} <Link to="/diff" className="link link-info font-mono">{t("howto.sect1_patch_desc_1_link")}</Link> {t("howto.sect1_patch_desc_1_post")}
|
||||
</p>
|
||||
<p className="text-green-700 mt-1">
|
||||
Supports <span className="font-semibold">Hdiff</span>, <span className="font-semibold">Ldiff</span>, and custom diff formats.
|
||||
{t("howto.sect1_patch_desc_2_pre")} <span className="font-semibold">Hdiff</span>, <span className="font-semibold">Ldiff</span>{t("howto.sect1_patch_desc_2_post")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -51,12 +50,11 @@ export default function HowToPage() {
|
||||
<div className="bg-blue-50 border-l-4 border-blue-400 p-6 rounded-r-lg">
|
||||
<h2 className="text-2xl font-bold text-blue-800 flex items-center gap-2 mb-4">
|
||||
<span>📜</span>
|
||||
<span>FireflyGo Chat Commands</span>
|
||||
<span>{t("howto.sect2_title")}</span>
|
||||
</h2>
|
||||
|
||||
<p className="text-blue-700 mb-4">
|
||||
Below are in-game chat commands you can use. Some commands require you to enable{" "}
|
||||
<span className="font-semibold text-accent">Theorycraft Mode</span>.
|
||||
{t("howto.sect2_desc_pre")} <span className="font-semibold text-accent">{t("howto.sect2_desc_bold")}</span>{t("howto.sect2_desc_post")}
|
||||
</p>
|
||||
|
||||
{/* Theorycraft Mode Warning */}
|
||||
@@ -64,8 +62,8 @@ export default function HowToPage() {
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-red-600 text-xl">🔒</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-red-800 mb-2">Theorycraft Mode Required</h3>
|
||||
<p className="text-red-700 mb-2">The following commands are only available when <strong>Theorycraft Mode</strong> is enabled:</p>
|
||||
<h3 className="font-semibold text-red-800 mb-2">{t("howto.sect2_tc_req_title")}</h3>
|
||||
<p className="text-red-700 mb-2">{t("howto.sect2_tc_req_desc_pre")} <strong>{t("howto.sect2_tc_req_desc_bold")}</strong> {t("howto.sect2_tc_req_desc_post")}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<code className="bg-red-100 px-2 py-1 rounded text-sm text-red-800">/cycle</code>
|
||||
<code className="bg-red-100 px-2 py-1 rounded text-sm text-red-800">/hp</code>
|
||||
@@ -79,36 +77,34 @@ export default function HowToPage() {
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-blue-600 text-lg">✨</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-blue-800 mb-1">Extra Settings</h4>
|
||||
<h4 className="font-semibold text-blue-800 mb-1">{t("howto.sect2_extra_title")}</h4>
|
||||
|
||||
<div className="space-y-4 text-blue-700 text-sm">
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<h5 className="font-semibold text-blue-800 flex items-center gap-2">
|
||||
🎭 Hidden UI
|
||||
🎭 {t("howto.sect2_hidden_ui_title")}
|
||||
</h5>
|
||||
<p className="mt-1">
|
||||
Instantly hides the entire game UI — often used in DIM showcase videos.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<h5 className="font-semibold text-blue-800 flex items-center gap-2">
|
||||
🚫 Disable Censorship
|
||||
</h5>
|
||||
<p className="mt-1">
|
||||
Remove the Lens Flare censor effect 💀 for a cleaner experience.
|
||||
{t("howto.sect2_hidden_ui_desc")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<h5 className="font-semibold text-blue-800 flex items-center gap-2">
|
||||
🧪 Theorycraft Mode
|
||||
🚫 {t("howto.sect2_censor_title")}
|
||||
</h5>
|
||||
<p className="mt-1">
|
||||
No need to type chat commands anymore — configure everything through the
|
||||
web: adjust monster HP, set cycles, view logs, and more.
|
||||
{t("howto.sect2_censor_desc")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<h5 className="font-semibold text-blue-800 flex items-center gap-2">
|
||||
🧪 {t("howto.sect2_tc_title")}
|
||||
</h5>
|
||||
<p className="mt-1">
|
||||
{t("howto.sect2_tc_desc")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -120,7 +116,7 @@ export default function HowToPage() {
|
||||
title="Extra Settings Tutorial"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
className="rounded-lg w-full h-[300px]"
|
||||
className="rounded-lg w-full h-75"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
@@ -129,17 +125,17 @@ export default function HowToPage() {
|
||||
|
||||
{/* Commands List */}
|
||||
<div className="space-y-4 mt-4">
|
||||
<h3 className="text-lg font-semibold text-blue-800">Available Commands:</h3>
|
||||
<h3 className="text-lg font-semibold text-blue-800">{t("howto.sect2_cmd_title")}</h3>
|
||||
|
||||
{/* Theorycraft Toggle */}
|
||||
<div className="bg-white border border-blue-200 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-blue-600 text-lg">⚙️</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-blue-800 mb-1">Theorycraft Mode</h4>
|
||||
<h4 className="font-semibold text-blue-800 mb-1">{t("howto.sect2_cmd_tc")}</h4>
|
||||
<div className="space-y-1 text-blue-700">
|
||||
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/theorycraft 1</code> — Enable Theorycraft Mode</p>
|
||||
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/theorycraft 0</code> — Disable Theorycraft Mode</p>
|
||||
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/theorycraft 1</code> {t("howto.sect2_cmd_tc_enable")}</p>
|
||||
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/theorycraft 0</code> {t("howto.sect2_cmd_tc_disable")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -150,11 +146,11 @@ export default function HowToPage() {
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-blue-600 text-lg">🔄</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-blue-800 mb-1">Cycle Control <span className="text-red-600 text-sm">(Theorycraft only)</span></h4>
|
||||
<h4 className="font-semibold text-blue-800 mb-1">{t("howto.sect2_cmd_cycle")} <span className="text-red-600 text-sm">{t("howto.sect2_cmd_tc_only")}</span></h4>
|
||||
<div className="space-y-1 text-blue-700">
|
||||
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/cycle N</code> — Set cycle count in battle</p>
|
||||
<p className="text-sm">Example: <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/cycle 30</code> sets battle to 30 cycles</p>
|
||||
<p className="text-sm"><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/cycle 0</code> disables custom cycle</p>
|
||||
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/cycle N</code> {t("howto.sect2_cmd_cycle_desc")}</p>
|
||||
<p className="text-sm">{t("howto.sect2_cmd_cycle_ex1_pre")} <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/cycle 30</code> {t("howto.sect2_cmd_cycle_ex1_post")}</p>
|
||||
<p className="text-sm"><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/cycle 0</code> {t("howto.sect2_cmd_cycle_ex2_post")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -166,37 +162,36 @@ export default function HowToPage() {
|
||||
<div className="text-blue-600 text-lg">❤️</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-blue-800 mb-1">
|
||||
HP Override <span className="text-red-600 text-sm">(Theorycraft only)</span>
|
||||
{t("howto.sect2_cmd_hp")} <span className="text-red-600 text-sm">{t("howto.sect2_cmd_tc_only")}</span>
|
||||
</h4>
|
||||
<div className="space-y-2 text-blue-700 text-sm">
|
||||
<p>
|
||||
<code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp N</code> — Set monster HP (only available in Theorycraft mode)
|
||||
<code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp N</code> {t("howto.sect2_cmd_hp_desc1")}
|
||||
</p>
|
||||
<p>
|
||||
<code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp 0</code> — Disable the set HP feature
|
||||
<code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp 0</code> {t("howto.sect2_cmd_hp_desc2")}
|
||||
</p>
|
||||
<p>
|
||||
<code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp Wave V1 V2 ...</code> — Set HP for each monster in a specific wave
|
||||
<code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp Wave V1 V2 ...</code> {t("howto.sect2_cmd_hp_desc3")}
|
||||
</p>
|
||||
<p className="ml-4">
|
||||
Example: <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp 1 2000000 3000000</code> sets wave 1 monster1 HP=2,000,000 and monster2 HP=3,000,000
|
||||
{t("howto.sect2_cmd_hp_ex_pre")} <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp 1 2000000 3000000</code> {t("howto.sect2_cmd_hp_ex_post")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Log Command */}
|
||||
<div className="bg-white border border-blue-200 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-blue-600 text-lg">📝</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-blue-800 mb-1">Battle Log <span className="text-red-600 text-sm">(Theorycraft only)</span></h4>
|
||||
<h4 className="font-semibold text-blue-800 mb-1">{t("howto.sect2_cmd_log")} <span className="text-red-600 text-sm">{t("howto.sect2_cmd_tc_only")}</span></h4>
|
||||
<div className="space-y-1 text-blue-700">
|
||||
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/log 1</code> — Enable battle log output</p>
|
||||
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/log 0</code> — Disable battle log</p>
|
||||
<p className="text-sm">Output will be written as <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">.json</code></p>
|
||||
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/log 1</code> {t("howto.sect2_cmd_log_desc1")}</p>
|
||||
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/log 0</code> {t("howto.sect2_cmd_log_desc2")}</p>
|
||||
<p className="text-sm">{t("howto.sect2_cmd_log_out_pre")} <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">.json</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -207,11 +202,11 @@ export default function HowToPage() {
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-blue-600 text-lg">⏭️</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-blue-800 mb-1">Skip Nodes</h4>
|
||||
<h4 className="font-semibold text-blue-800 mb-1">{t("howto.sect2_cmd_skip")}</h4>
|
||||
<div className="space-y-1 text-blue-700">
|
||||
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/skip N</code> — Skip nodes in MOC / AS / Pure Fiction</p>
|
||||
<p className="text-sm">Example: <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/skip 2</code> skips node 2</p>
|
||||
<p className="text-sm"><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/skip 0</code> disables skipping</p>
|
||||
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/skip N</code> {t("howto.sect2_cmd_skip_desc")}</p>
|
||||
<p className="text-sm">{t("howto.sect2_cmd_skip_ex1_pre")} <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/skip 2</code> {t("howto.sect2_cmd_skip_ex1_post")}</p>
|
||||
<p className="text-sm"><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/skip 0</code> {t("howto.sect2_cmd_skip_ex2_post")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -222,11 +217,11 @@ export default function HowToPage() {
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-blue-600 text-lg">🔄</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-blue-800 mb-1">Character Path Switch</h4>
|
||||
<h4 className="font-semibold text-blue-800 mb-1">{t("howto.sect2_cmd_id")}</h4>
|
||||
<div className="space-y-1 text-blue-700">
|
||||
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/id CHAR_ID</code> — Switch path for multi-form characters</p>
|
||||
<p className="text-sm">Example: <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/id 8008</code> to change MC (Trailblazer) form</p>
|
||||
<p className="text-sm">Works with IDs like <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">8001 → 8008</code>, <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">1001 → 1224</code></p>
|
||||
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/id CHAR_ID</code> {t("howto.sect2_cmd_id_desc")}</p>
|
||||
<p className="text-sm">{t("howto.sect2_cmd_id_ex1_pre")} <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/id 8008</code> {t("howto.sect2_cmd_id_ex1_post")}</p>
|
||||
<p className="text-sm">{t("howto.sect2_cmd_id_ex2_pre")} <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">8001 → 8008</code>, <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">1001 → 1224</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -237,9 +232,9 @@ export default function HowToPage() {
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-blue-600 text-lg">🔄</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-blue-800 mb-1">Refresh Data</h4>
|
||||
<h4 className="font-semibold text-blue-800 mb-1">{t("howto.sect2_cmd_update")}</h4>
|
||||
<div className="space-y-1 text-blue-700">
|
||||
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/update</code> — Refresh server data from current <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">freesr-data.json</code></p>
|
||||
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/update</code> {t("howto.sect2_cmd_update_desc_pre")} <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">freesr-data.json</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -251,7 +246,7 @@ export default function HowToPage() {
|
||||
<div className="bg-gray-50 border-l-4 border-gray-400 p-6 rounded-r-lg">
|
||||
<h2 className="text-2xl font-bold text-gray-800 flex items-center gap-2 mb-4">
|
||||
<span>📌</span>
|
||||
<span>Other Notes</span>
|
||||
<span>{t("howto.sect3_title")}</span>
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
@@ -260,9 +255,9 @@ export default function HowToPage() {
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-yellow-600 text-xl">⚠️</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-yellow-800 mb-1">Administrator Rights</h3>
|
||||
<h3 className="font-semibold text-yellow-800 mb-1">{t("howto.sect3_admin_title")}</h3>
|
||||
<p className="text-yellow-700">
|
||||
Always run the launcher as Administrator for file permission access.
|
||||
{t("howto.sect3_admin_desc")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -273,10 +268,9 @@ export default function HowToPage() {
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-blue-600 text-xl">💾</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-blue-800 mb-1">Backup Data</h3>
|
||||
<h3 className="font-semibold text-blue-800 mb-1">{t("howto.sect3_backup_title")}</h3>
|
||||
<p className="text-blue-700">
|
||||
Backup your <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">config.json</code> and{' '}
|
||||
<code className="bg-blue-100 px-1 py-0.5 rounded text-sm">freesr-data.json</code> regularly.
|
||||
{t("howto.sect3_backup_desc_pre")} <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">config.json</code> {t("howto.sect3_backup_desc_mid")} <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">freesr-data.json</code> {t("howto.sect3_backup_desc_post")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -287,21 +281,21 @@ export default function HowToPage() {
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-green-600 text-xl">🎵</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-green-800 mb-2">Enable Voice Packs in Beta Client</h3>
|
||||
<h3 className="font-semibold text-green-800 mb-2">{t("howto.sect3_voice_title")}</h3>
|
||||
<div className="space-y-3 text-green-700">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="font-medium min-w-[20px] text-green-600">1.</span>
|
||||
<span className="font-medium min-w-5 text-green-600">1.</span>
|
||||
<div>
|
||||
<p className="mb-1">Copy the desired voice folder (e.g., <code className="bg-green-100 px-1 py-0.5 rounded text-sm">Japanese</code>, <code className="bg-green-100 px-1 py-0.5 rounded text-sm">English</code>) from:</p>
|
||||
<p className="mb-1">{t("howto.sect3_voice_step1_pre")} <code className="bg-green-100 px-1 py-0.5 rounded text-sm">Japanese</code>, <code className="bg-green-100 px-1 py-0.5 rounded text-sm">English</code>{t("howto.sect3_voice_step1_mid")}</p>
|
||||
<code className="block bg-green-100 px-2 py-1 rounded text-sm mt-1">
|
||||
Star Rail\Games\StarRail_Data\Persistent\Audio\AudioPackage\Windows
|
||||
</code>
|
||||
<p className="mt-1">to the beta folder by clicking <strong>"Open Voice Folder"</strong> on the Home tab.</p>
|
||||
<p className="mt-1">{t("howto.sect3_voice_step1_post_pre")} <strong>{t("howto.sect3_voice_step1_post_bold")}</strong> {t("howto.sect3_voice_step1_post_post")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="font-medium min-w-[20px] text-green-600">2.</span>
|
||||
<p>When launching the game for the first time, it may delete the voice folder. If so, repeat step 1 to restore it.</p>
|
||||
<span className="font-medium min-w-5 text-green-600">2.</span>
|
||||
<p>{t("howto.sect3_voice_step2")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -311,7 +305,7 @@ export default function HowToPage() {
|
||||
</div>
|
||||
|
||||
<div className="text-center pt-4">
|
||||
<Link to="/" className="btn btn-primary btn-wide">Back to Home</Link>
|
||||
<Link to="/" className="btn btn-primary btn-wide">{t("howto.btn_back")}</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { FSService } from '@bindings/firefly-launcher/internal/fs-service'
|
||||
import { LanguageService } from '@bindings/firefly-launcher/internal/language-service'
|
||||
import { toast } from 'react-toastify'
|
||||
import useSettingStore from '@/stores/settingStore'
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
export default function LanguagePage() {
|
||||
const { gameDir, setGameDir } = useSettingStore()
|
||||
@@ -24,6 +25,7 @@ export default function LanguagePage() {
|
||||
{ value: 'jp', label: 'Japanese', flag: '🇯🇵' },
|
||||
{ value: 'kr', label: 'Korean', flag: '🇰🇷' }
|
||||
]
|
||||
const { t } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
const getLanguage = async () => {
|
||||
@@ -78,15 +80,15 @@ export default function LanguagePage() {
|
||||
setFolderCheckResult(exists ? 'success' : 'error')
|
||||
setGameDir(exists ? basePath : "")
|
||||
if (!exists) {
|
||||
toast.error('Game directory not found. Please select the correct folder.')
|
||||
toast.error(t("diff.toast_game_dir_not_found"))
|
||||
}
|
||||
} else {
|
||||
toast.error('No folder path selected')
|
||||
toast.error(t("language.toast_no_folder_selected"))
|
||||
setFolderCheckResult('error')
|
||||
setGameDir('')
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast.error('PickFolder error:', err)
|
||||
toast.error(t("language.toast_pick_folder_error"), err)
|
||||
setFolderCheckResult('error')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
@@ -95,7 +97,7 @@ export default function LanguagePage() {
|
||||
|
||||
const handleSetLanguage = async () => {
|
||||
if (!gameDir) {
|
||||
toast.error('No folder path selected')
|
||||
toast.error(t("language.toast_no_folder_selected"))
|
||||
return
|
||||
}
|
||||
try {
|
||||
@@ -106,7 +108,7 @@ export default function LanguagePage() {
|
||||
selectedVoiceLang
|
||||
)
|
||||
if (ok) {
|
||||
toast.success('Language set successfully')
|
||||
toast.success(t("language.toast_set_language_success"))
|
||||
setTextLang(selectedTextLang)
|
||||
setVoiceLang(selectedVoiceLang)
|
||||
}
|
||||
@@ -115,7 +117,7 @@ export default function LanguagePage() {
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
toast.error('SetLanguage error:', err)
|
||||
toast.error(t("language.toast_set_language_error"), err)
|
||||
} finally {
|
||||
setIsSettingLanguage(false)
|
||||
}
|
||||
@@ -132,9 +134,9 @@ export default function LanguagePage() {
|
||||
{/* Header */}
|
||||
<div className="text-center mb-2">
|
||||
<h1 className="text-4xl font-bold mb-2">
|
||||
🎮 Game Language Manager
|
||||
{t("language.header_title")}
|
||||
</h1>
|
||||
<p className="">Manage text and voice language settings for your game</p>
|
||||
<p className="">{t("language.header_desc")}</p>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
@@ -144,7 +146,7 @@ export default function LanguagePage() {
|
||||
<div className="pb-2">
|
||||
<h2 className="text-2xl font-semibold mb-4 flex items-center gap-2">
|
||||
<Folder className="text-primary" size={24} />
|
||||
Game Directory
|
||||
{t("language.game_dir_title")}
|
||||
</h2>
|
||||
|
||||
<div className="space-y-1">
|
||||
@@ -155,7 +157,7 @@ export default function LanguagePage() {
|
||||
className="btn btn-primary"
|
||||
>
|
||||
<Folder size={20} />
|
||||
{isLoading ? 'Selecting...' : 'Select Game Folder'}
|
||||
{isLoading ? t("language.btn_selecting") : t("language.btn_select_game") }
|
||||
</button>
|
||||
|
||||
{gameDir && (
|
||||
@@ -174,12 +176,12 @@ export default function LanguagePage() {
|
||||
{folderCheckResult === 'success' ? (
|
||||
<>
|
||||
<Check size={20} />
|
||||
<span>Valid game directory found!</span>
|
||||
<span>{t("language.game_dir_valid")}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<X size={20} />
|
||||
<span>Game directory not found. Please select the correct folder.</span>
|
||||
<span>{t("language.game_dir_invalid")}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -192,14 +194,14 @@ export default function LanguagePage() {
|
||||
<div className="pb-2">
|
||||
<h2 className="text-2xl font-semibold mb-4 flex items-center gap-2">
|
||||
<Globe className="text-primary" size={24} />
|
||||
Current Languages
|
||||
{t("language.current_languages_title")}
|
||||
</h2>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="bg-success/5 rounded-lg p-2 border border-success/30">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Globe size={20} className="text-success" />
|
||||
<span className="font-bold text-success">Text Language</span>
|
||||
<span className="font-bold text-success">{t("language.text_language")}</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-success">
|
||||
{getLanguageLabel(textLang)}
|
||||
@@ -209,7 +211,7 @@ export default function LanguagePage() {
|
||||
<div className="bg-warning/5 rounded-lg p-2 border border-warning/30">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Mic size={20} className="text-accent" />
|
||||
<span className="font-bold text-accent">Voice Language</span>
|
||||
<span className="font-bold text-accent">{t("language.voice_language")}</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-accent">
|
||||
{getLanguageLabel(voiceLang)}
|
||||
@@ -224,22 +226,21 @@ export default function LanguagePage() {
|
||||
}`}>
|
||||
<h2 className="text-2xl font-semibold mb-4 flex items-center gap-2">
|
||||
<Settings className="text-primary" size={24} />
|
||||
Language Settings
|
||||
{t("language.language_settings_title")}
|
||||
</h2>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{/* Text Language */}
|
||||
<div className="space-y-3">
|
||||
<label className="flex text-sm font-medium text-success items-center gap-2">
|
||||
<Globe size={16} />
|
||||
Text Language
|
||||
{t("language.text_language")}
|
||||
</label>
|
||||
<select
|
||||
value={selectedTextLang}
|
||||
onChange={(e) => setSelectedTextLang(e.target.value)}
|
||||
className="w-full select select-success"
|
||||
>
|
||||
<option value="">Select text language...</option>
|
||||
<option value="">{t("language.select_text_placeholder")}</option>
|
||||
{languageOptions.map(lang => (
|
||||
<option key={lang.value} value={lang.value}>
|
||||
{lang.flag} {lang.label}
|
||||
@@ -248,18 +249,17 @@ export default function LanguagePage() {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Voice Language */}
|
||||
<div className="space-y-3">
|
||||
<label className="flex text-sm font-medium text-accent items-center gap-2">
|
||||
<Mic size={16} />
|
||||
Voice Language
|
||||
{t("language.voice_language")}
|
||||
</label>
|
||||
<select
|
||||
value={selectedVoiceLang}
|
||||
onChange={(e) => setSelectedVoiceLang(e.target.value)}
|
||||
className="w-full select select-warning"
|
||||
>
|
||||
<option value="">Select voice language...</option>
|
||||
<option value="">{t("language.select_voice_placeholder")}.</option>
|
||||
{languageOptions.map(lang => (
|
||||
<option key={lang.value} value={lang.value}>
|
||||
{lang.flag} {lang.label}
|
||||
@@ -274,22 +274,22 @@ export default function LanguagePage() {
|
||||
<button
|
||||
onClick={handleSetLanguage}
|
||||
disabled={!selectedTextLang || !selectedVoiceLang || isSettingLanguage}
|
||||
className="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 disabled:from-gray-400 disabled:to-gray-500 text-white px-8 py-3 rounded-lg font-medium transition-all duration-200 flex items-center gap-2 shadow-lg hover:shadow-xl disabled:cursor-not-allowed cursor-pointer"
|
||||
className="bg-linear-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 disabled:from-gray-400 disabled:to-gray-500 text-white px-8 py-3 rounded-lg font-medium transition-all duration-200 flex items-center gap-2 shadow-lg hover:shadow-xl disabled:cursor-not-allowed cursor-pointer"
|
||||
>
|
||||
<Settings size={20} />
|
||||
{isSettingLanguage ? 'Applying...' : 'Apply Language Settings'}
|
||||
{isSettingLanguage ? t("language.btn_applying") : t("language.btn_apply")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Instructions */}
|
||||
<div className="bg-info/5 rounded-lg p-4 border border-info/30 mt-6">
|
||||
<h3 className="font-medium text-error mb-2">📋 Instructions:</h3>
|
||||
<h3 className="font-medium text-error mb-2">{t("language.inst_title")}</h3>
|
||||
<ol className="text-sm text-error space-y-1">
|
||||
<li>1. Click "Select Game Folder" and choose your game's root directory</li>
|
||||
<li>2. Wait for the system to validate the game directory</li>
|
||||
<li>3. Select your preferred text and voice languages</li>
|
||||
<li>4. Click "Apply Language Settings" to save your changes</li>
|
||||
<li>{t("language.inst_step_1")}</li>
|
||||
<li>{t("language.inst_step_2")}</li>
|
||||
<li>{t("language.inst_step_3")}</li>
|
||||
<li>{t("language.inst_step_4")}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,10 +12,11 @@ import { Link } from '@tanstack/react-router';
|
||||
import { CheckUpdateLauncher, CheckUpdateProxy, CheckUpdateServer, sleep, UpdateLauncher, UpdateProxy, UpdateServer } from '@/helper';
|
||||
import UpdateModal from '@/components/updateModal';
|
||||
import { BackgroundSelector } from '@/components/backgroudModal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
export default function LauncherPage() {
|
||||
const {
|
||||
const {
|
||||
gamePath,
|
||||
setGamePath,
|
||||
setGameDir,
|
||||
@@ -26,7 +27,7 @@ export default function LauncherPage() {
|
||||
proxyVersion,
|
||||
background
|
||||
} = useSettingStore()
|
||||
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
isOpenDownloadDataModal,
|
||||
isOpenUpdateDataModal,
|
||||
@@ -156,17 +157,17 @@ export default function LauncherPage() {
|
||||
const fullPath = `${folderPath}/StarRail_Data/StreamingAssets/DesignData/Windows`
|
||||
const exists = await FSService.DirExists(fullPath)
|
||||
if (!exists) {
|
||||
toast.error('Game directory not found. Please select the correct folder.')
|
||||
toast.error(t("home.error_game_dir"))
|
||||
} else {
|
||||
setGamePath(basePath)
|
||||
setGameDir(folderPath)
|
||||
toast.success('Game path set successfully')
|
||||
toast.success(t("home.game_path_success"))
|
||||
}
|
||||
} else {
|
||||
toast.error('Not valid file type')
|
||||
toast.error(t("home.error_file_type"))
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast.error('PickFolder error:', err)
|
||||
toast.error(t("home.toast_pick_folder_error"), err)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
@@ -184,7 +185,7 @@ export default function LauncherPage() {
|
||||
if (!proxyRunning && !gamePath.endsWith("launcher.exe")) {
|
||||
const [resultProxy, error] = await FSService.StartWithConsole(proxyPath)
|
||||
if (!resultProxy) {
|
||||
toast.error('Failed to start proxy: ' + error)
|
||||
toast.error(t("home.toast_start_proxy_failed") + error)
|
||||
return
|
||||
}
|
||||
setProxyRunning(true)
|
||||
@@ -194,7 +195,7 @@ export default function LauncherPage() {
|
||||
if (!serverRunning) {
|
||||
const [resultServer, error] = await FSService.StartWithConsole(serverPath)
|
||||
if (!resultServer) {
|
||||
toast.error('Failed to start server: ' + error)
|
||||
toast.error(t("home.toast_start_server_failed") + error)
|
||||
return
|
||||
}
|
||||
setServerRunning(true)
|
||||
@@ -204,13 +205,13 @@ export default function LauncherPage() {
|
||||
if (gamePath.endsWith("launcher.exe")) {
|
||||
const [resultGame, error] = await FSService.StartWithConsole(gamePath)
|
||||
if (!resultGame) {
|
||||
toast.error('Failed to start game: ' + error)
|
||||
toast.error(t("home.toast_start_game_failed") + error)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
const [resultGame, error] = await FSService.StartApp(gamePath)
|
||||
if (!resultGame) {
|
||||
toast.error('Failed to start game: ' + error)
|
||||
toast.error(t("home.toast_start_game_failed") + error)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -218,7 +219,7 @@ export default function LauncherPage() {
|
||||
|
||||
} catch (err: any) {
|
||||
console.log(err)
|
||||
toast.error('StartGame error:', err)
|
||||
toast.error(t("home.toast_start_game_error"), err)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
@@ -276,7 +277,7 @@ export default function LauncherPage() {
|
||||
|
||||
{/* Header */}
|
||||
<header className="hidden sm:flex fixed z-10 items-center justify-between py-6 px-4 ">
|
||||
<div className="text-2xl font-bold text-white bg-gray-500/5 rounded-full p-1">Firefly GO</div>
|
||||
<div className="text-2xl font-bold text-white bg-gray-500/5 rounded-full p-1">{t("home.header_title")}</div>
|
||||
</header>
|
||||
|
||||
<div className="hidden sm:flex fixed top-1/4 right-4 z-10 flex-col space-y-3 bg-white/5 rounded-xl p-3 shadow-lg">
|
||||
@@ -296,7 +297,7 @@ export default function LauncherPage() {
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="tooltip tooltip-left" data-tip="How to use all tools & commands">
|
||||
<div className="tooltip tooltip-left" data-tip={t("home.tooltip_how_to")}>
|
||||
<Link
|
||||
to="/howto"
|
||||
className="btn btn-warning btn-circle"
|
||||
@@ -312,7 +313,7 @@ export default function LauncherPage() {
|
||||
|
||||
{/* Bottom Panel */}
|
||||
{serverReady && proxyReady && !isDownloading && (
|
||||
<div className="fixed bottom-0 right-0 p-8 z-10">
|
||||
<div className="fixed bottom-0 right-0 p-8 z-2">
|
||||
<div className="flex flex-wrap items-center justify-center gap-2">
|
||||
{gamePath === "" ? (
|
||||
<button
|
||||
@@ -320,7 +321,7 @@ export default function LauncherPage() {
|
||||
onClick={handlePickFile}
|
||||
>
|
||||
<FolderOpen className="w-6 h-6" />
|
||||
{isLoading ? 'Selecting...' : 'Select Game file'}
|
||||
{isLoading ? t("home.btn_selecting") : t("home.btn_select_game")}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
@@ -328,7 +329,7 @@ export default function LauncherPage() {
|
||||
onClick={handleStartGame}
|
||||
>
|
||||
<Play className="w-6 h-6" />
|
||||
{isLoading ? 'Selecting...' : gameRunning ? 'Game is running' : 'Start Game'}
|
||||
{isLoading ? t("home.btn_selecting") : gameRunning ? t("home.btn_game_running") : t("home.btn_start_game")}
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -337,7 +338,7 @@ export default function LauncherPage() {
|
||||
<Menu className="w-6 h-6" />
|
||||
</div>
|
||||
<ul tabIndex={0} className="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm">
|
||||
<li><button onClick={handlePickFile}>Change Game Path</button></li>
|
||||
<li><button onClick={handlePickFile}>{t("home.menu_change_path")}</button></li>
|
||||
<li>
|
||||
<button
|
||||
onClick={async () => {
|
||||
@@ -357,29 +358,44 @@ export default function LauncherPage() {
|
||||
setIsOpenUpdateDataModal(true)
|
||||
return
|
||||
}
|
||||
toast.success("No updates available")
|
||||
toast.success(t("home.no_updates"))
|
||||
}}>
|
||||
Check for Updates Server & Proxy
|
||||
{t("home.menu_check_update")}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
|
||||
</li>
|
||||
<li><button disabled={!serverPath} onClick={() => {
|
||||
if (serverPath) {
|
||||
FSService.OpenFolder("./server")
|
||||
}
|
||||
}}>Open server folder</button></li>
|
||||
<li><button disabled={!proxyPath} onClick={() => {
|
||||
if (proxyPath) {
|
||||
FSService.OpenFolder("./proxy")
|
||||
}
|
||||
}}>Open proxy folder</button></li>
|
||||
<li><button disabled={!gameDir} onClick={() => {
|
||||
if (gameDir) {
|
||||
FSService.OpenFolder(gameDir + "/StarRail_Data/Persistent/Audio/AudioPackage/Windows")
|
||||
}
|
||||
}}>Open voice folder</button></li>
|
||||
<li>
|
||||
<button disabled={!serverPath} onClick={() => {
|
||||
if (serverPath) {
|
||||
FSService.OpenFolder("./server")
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("home.menu_open_server")}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button disabled={!proxyPath} onClick={() => {
|
||||
if (proxyPath) {
|
||||
FSService.OpenFolder("./proxy")
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("home.menu_open_proxy")}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button disabled={!gameDir} onClick={() => {
|
||||
if (gameDir) {
|
||||
FSService.OpenFolder(gameDir + "/StarRail_Data/Persistent/Audio/AudioPackage/Windows")
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("home.menu_open_voice")}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
@@ -412,7 +428,7 @@ export default function LauncherPage() {
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center text-xs text-white/60">
|
||||
{progressDownload < 100 ? 'Please wait...' : 'Complete!'}
|
||||
{progressDownload < 100 ? t("home.status_wait") : t("home.status_complete")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -431,16 +447,16 @@ export default function LauncherPage() {
|
||||
: "text-red-200 text-xl"
|
||||
}`}
|
||||
>
|
||||
{downloadType === "update:launcher:downloading" && "Updating launcher"}
|
||||
{downloadType === "update:launcher:success" && "Launcher updated successfully, auto closing after 5s"}
|
||||
{downloadType === "update:launcher:failed" && "Launcher update failed, auto closing after 5s"}
|
||||
{downloadType === "update:launcher:downloading" && t("home.status_updating_launcher")}
|
||||
{downloadType === "update:launcher:success" && t("home.status_update_success")}
|
||||
{downloadType === "update:launcher:failed" && t("home.status_update_failed")}
|
||||
<span className="dot-animation ml-1"></span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-xs text-white/60">
|
||||
{progressDownload < 100 ? "Please wait..." : "Complete!"}
|
||||
{progressDownload < 100 ? t("home.status_wait") : t("home.status_complete")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -461,41 +477,41 @@ export default function LauncherPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="hidden md:block fixed bottom-4 left-5 z-10">
|
||||
<div className="hidden md:block fixed bottom-4 left-5 z-10">
|
||||
|
||||
<img src="/heart-hsr.gif" alt="firefly animation" className="rounded-lg w-24 h-24" />
|
||||
<img src="/heart-hsr.gif" alt="firefly animation" className="rounded-lg w-24 h-24" />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{/* Modal */}
|
||||
<UpdateModal
|
||||
isOpen={isOpenUpdateDataModal}
|
||||
onClose={() => setIsOpenUpdateDataModal(false)}
|
||||
title="Update Data"
|
||||
message="Do you want to update data server and proxy?"
|
||||
title={t("home.modal_update_title")}
|
||||
message={t("home.modal_update_msg")}
|
||||
buttons={[
|
||||
{ text: "No", onClick: () => setIsOpenUpdateDataModal(false), variant: "outline" },
|
||||
{ text: "Yes", onClick: async () => { setIsOpenUpdateDataModal(false); await handlerUpdateData() }, variant: "primary" }
|
||||
{ text: t("home.btn_no"), onClick: () => setIsOpenUpdateDataModal(false), variant: "outline" },
|
||||
{ text: t("home.btn_yes"), onClick: async () => { setIsOpenUpdateDataModal(false); await handlerUpdateData() }, variant: "primary" }
|
||||
]}
|
||||
/>
|
||||
|
||||
<UpdateModal
|
||||
isOpen={isOpenDownloadDataModal}
|
||||
onClose={() => setIsOpenDownloadDataModal(false)}
|
||||
title="Download Data"
|
||||
message="Data server and proxy download required"
|
||||
title={t("home.modal_download_title")}
|
||||
message={t("home.modal_download_msg")}
|
||||
buttons={[
|
||||
{ text: "Download", onClick: async () => { setIsOpenDownloadDataModal(false); await handlerUpdateData() }, variant: "primary" }
|
||||
{ text: t("home.btn_download"), onClick: async () => { setIsOpenDownloadDataModal(false); await handlerUpdateData() }, variant: "primary" }
|
||||
]}
|
||||
/>
|
||||
|
||||
<UpdateModal
|
||||
isOpen={isOpenSelfUpdateModal}
|
||||
onClose={() => setIsOpenSelfUpdateModal(false)}
|
||||
title="Update Launcher"
|
||||
message="Do you want to update launcher?"
|
||||
title={t("home.modal_self_update_title")}
|
||||
message={t("home.modal_self_update_msg")}
|
||||
buttons={[
|
||||
{ text: "No", onClick: () => setIsOpenSelfUpdateModal(false), variant: "outline" },
|
||||
{ text: "Yes", onClick: async () => { setIsOpenSelfUpdateModal(false); await handlerUpdateData() }, variant: "primary" }
|
||||
{ text: t("home.btn_no"), onClick: () => setIsOpenSelfUpdateModal(false), variant: "outline" },
|
||||
{ text: t("home.btn_yes"), onClick: async () => { setIsOpenSelfUpdateModal(false); await handlerUpdateData() }, variant: "primary" }
|
||||
]}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
export default function getCroppedImg(imageSrc: string, crop: any): Promise<string> {
|
||||
const image = new Image()
|
||||
image.crossOrigin = "anonymous"
|
||||
image.src = imageSrc
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')!
|
||||
|
||||
Reference in New Issue
Block a user