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"
|
||||
|
||||
Reference in New Issue
Block a user