UPDATE: Add muti language

This commit is contained in:
2026-04-03 18:37:01 +07:00
parent 2b0c82bd7e
commit 45d345d7cb
24 changed files with 2451 additions and 430 deletions

View File

@@ -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" />

View File

@@ -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>
)
}

View File

@@ -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>
)
}
}

View 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;

View File

@@ -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>

View File

@@ -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
View 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
},
});

View File

@@ -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' {

View File

@@ -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>
);
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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" }
]}
/>

View File

@@ -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')!