diff --git a/src/app/(dashboard)/dashboard/profile/page.js b/src/app/(dashboard)/dashboard/profile/page.js
index 5ec301e3..e5e40642 100644
--- a/src/app/(dashboard)/dashboard/profile/page.js
+++ b/src/app/(dashboard)/dashboard/profile/page.js
@@ -1,13 +1,32 @@
"use client";
import { useState, useEffect, useRef } from "react";
+import { useRouter } from "next/navigation";
import { Card, Button, Toggle, Input } from "@/shared/components";
+import { ConfirmModal } from "@/shared/components/Modal";
+import LanguageSwitcher from "@/shared/components/LanguageSwitcher";
import { useTheme } from "@/shared/hooks/useTheme";
import { cn } from "@/shared/utils/cn";
import { APP_CONFIG } from "@/shared/constants/config";
+import { LOCALE_COOKIE, normalizeLocale } from "@/i18n/config";
+import { LOCALE_FLAGS } from "@/shared/constants/locales";
+
+function getLocaleFromCookie() {
+ if (typeof document === "undefined") return "en";
+ const cookie = document.cookie
+ .split(";")
+ .find((c) => c.trim().startsWith(`${LOCALE_COOKIE}=`));
+ const value = cookie ? decodeURIComponent(cookie.split("=")[1]) : "en";
+ return normalizeLocale(value);
+}
export default function ProfilePage() {
+ const router = useRouter();
const { theme, setTheme, isDark } = useTheme();
+ const [locale, setLocale] = useState("en");
+ const [langOpen, setLangOpen] = useState(false);
+ const [shutdownOpen, setShutdownOpen] = useState(false);
+ const [isShuttingDown, setIsShuttingDown] = useState(false);
const [settings, setSettings] = useState({ fallbackStrategy: "fill-first" });
const [loading, setLoading] = useState(true);
const [passwords, setPasswords] = useState({ current: "", new: "", confirm: "" });
@@ -39,6 +58,10 @@ export default function ProfilePage() {
const [proxyLoading, setProxyLoading] = useState(false);
const [proxyTestLoading, setProxyTestLoading] = useState(false);
+ useEffect(() => {
+ setLocale(getLocaleFromCookie());
+ }, [langOpen]);
+
useEffect(() => {
fetch("/api/settings")
.then((res) => res.json())
@@ -515,6 +538,29 @@ export default function ProfilePage() {
const observabilityEnabled = settings.enableObservability === true;
+ const handleShutdown = async () => {
+ setIsShuttingDown(true);
+ try {
+ await fetch("/api/version/shutdown", { method: "POST" });
+ } catch (e) {
+ // Expected to fail as server shuts down; ignore error
+ }
+ setIsShuttingDown(false);
+ setShutdownOpen(false);
+ };
+
+ const handleLogout = async () => {
+ try {
+ const res = await fetch("/api/auth/logout", { method: "POST" });
+ if (res.ok) {
+ router.push("/login");
+ router.refresh();
+ }
+ } catch (err) {
+ console.error("Failed to logout:", err);
+ }
+ };
+
return (
@@ -593,6 +639,24 @@ export default function ProfilePage() {
+ {/* Language */}
+
+
+
+ language
+
+
Language
+
+
+
+
{/* Security */}
@@ -1024,12 +1088,53 @@ export default function ProfilePage() {
+ {/* Account actions */}
+
+
+
+
+
{/* App Info */}
{APP_CONFIG.name} v{APP_CONFIG.version}
Local Mode - All data stored on your machine
+
+ {
+ setLangOpen(false);
+ setLocale(next);
+ }}
+ />
+ setShutdownOpen(false)}
+ onConfirm={handleShutdown}
+ title="Close Proxy"
+ message="Are you sure you want to close the proxy server?"
+ confirmText="Close"
+ cancelText="Cancel"
+ variant="danger"
+ loading={isShuttingDown}
+ />
);
}
diff --git a/src/shared/components/Header.js b/src/shared/components/Header.js
index de1bc052..f8427aa2 100644
--- a/src/shared/components/Header.js
+++ b/src/shared/components/Header.js
@@ -6,6 +6,7 @@ import Link from "next/link";
import PropTypes from "prop-types";
import ProviderIcon from "@/shared/components/ProviderIcon";
import HeaderMenu from "@/shared/components/HeaderMenu";
+import HeaderLanguage from "@/shared/components/HeaderLanguage";
import ThemeToggle from "@/shared/components/ThemeToggle";
import DonateModal from "@/shared/components/DonateModal";
import { useHeaderSearchStore } from "@/store/headerSearchStore";
@@ -315,6 +316,7 @@ export default function Header({ onMenuClick, showMenuButton = true }) {
Donate
+
setDonateOpen(false)} />
diff --git a/src/shared/components/HeaderLanguage.js b/src/shared/components/HeaderLanguage.js
new file mode 100644
index 00000000..0e24d3c7
--- /dev/null
+++ b/src/shared/components/HeaderLanguage.js
@@ -0,0 +1,46 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { LOCALE_COOKIE, normalizeLocale } from "@/i18n/config";
+import { LOCALE_FLAGS } from "@/shared/constants/locales";
+import LanguageSwitcher from "./LanguageSwitcher";
+
+function getLocaleFromCookie() {
+ if (typeof document === "undefined") return "en";
+ const cookie = document.cookie
+ .split(";")
+ .find((c) => c.trim().startsWith(`${LOCALE_COOKIE}=`));
+ const value = cookie ? decodeURIComponent(cookie.split("=")[1]) : "en";
+ return normalizeLocale(value);
+}
+
+export default function HeaderLanguage() {
+ const [open, setOpen] = useState(false);
+ const [locale, setLocale] = useState("en");
+
+ useEffect(() => {
+ setLocale(getLocaleFromCookie());
+ }, [open]);
+
+ return (
+ <>
+
+
+ {
+ setOpen(false);
+ setLocale(next);
+ }}
+ />
+ >
+ );
+}
diff --git a/src/shared/components/HeaderMenu.js b/src/shared/components/HeaderMenu.js
index 1f36be12..54ba6763 100644
--- a/src/shared/components/HeaderMenu.js
+++ b/src/shared/components/HeaderMenu.js
@@ -2,56 +2,9 @@
import { useState, useEffect, useRef } from "react";
import PropTypes from "prop-types";
-import { LOCALE_COOKIE, normalizeLocale } from "@/i18n/config";
import { useTheme } from "@/shared/hooks/useTheme";
import ChangelogModal from "./ChangelogModal";
-import NineRemotePromoModal from "./NineRemotePromoModal";
-import LanguageSwitcher from "./LanguageSwitcher";
-
-const LOCALE_INFO = {
- "en": { name: "English", flag: "๐บ๐ธ" },
- "vi": { name: "Tiแบฟng Viแปt", flag: "๐ป๐ณ" },
- "zh-CN": { name: "็ฎไฝไธญๆ", flag: "๐จ๐ณ" },
- "zh-TW": { name: "็น้ซไธญๆ", flag: "๐น๐ผ" },
- "ja": { name: "ๆฅๆฌ่ช", flag: "๐ฏ๐ต" },
- "pt-BR": { name: "Portuguรชs (BR)", flag: "๐ง๐ท" },
- "pt-PT": { name: "Portuguรชs (PT)", flag: "๐ต๐น" },
- "ko": { name: "ํ๊ตญ์ด", flag: "๐ฐ๐ท" },
- "es": { name: "Espaรฑol", flag: "๐ช๐ธ" },
- "de": { name: "Deutsch", flag: "๐ฉ๐ช" },
- "fr": { name: "Franรงais", flag: "๐ซ๐ท" },
- "he": { name: "ืขืืจืืช", flag: "๐ฎ๐ฑ" },
- "ar": { name: "ุงูุนุฑุจูุฉ", flag: "๐ธ๐ฆ" },
- "ru": { name: "ะ ัััะบะธะน", flag: "๐ท๐บ" },
- "pl": { name: "Polski", flag: "๐ต๐ฑ" },
- "cs": { name: "ฤeลกtina", flag: "๐จ๐ฟ" },
- "nl": { name: "Nederlands", flag: "๐ณ๐ฑ" },
- "tr": { name: "Tรผrkรงe", flag: "๐น๐ท" },
- "uk": { name: "ะฃะบัะฐัะฝััะบะฐ", flag: "๐บ๐ฆ" },
- "tl": { name: "Tagalog", flag: "๐ต๐ญ" },
- "id": { name: "Indonesia", flag: "๐ฎ๐ฉ" },
- "th": { name: "เนเธเธข", flag: "๐น๐ญ" },
- "hi": { name: "เคนเคฟเคจเฅเคฆเฅ", flag: "๐ฎ๐ณ" },
- "bn": { name: "เฆฌเฆพเฆเฆฒเฆพ", flag: "๐ง๐ฉ" },
- "ur": { name: "ุงุฑุฏู", flag: "๐ต๐ฐ" },
- "ro": { name: "Romรขnฤ", flag: "๐ท๐ด" },
- "sv": { name: "Svenska", flag: "๐ธ๐ช" },
- "it": { name: "Italiano", flag: "๐ฎ๐น" },
- "el": { name: "ฮฮปฮปฮทฮฝฮนฮบฮฌ", flag: "๐ฌ๐ท" },
- "hu": { name: "Magyar", flag: "๐ญ๐บ" },
- "fi": { name: "Suomi", flag: "๐ซ๐ฎ" },
- "da": { name: "Dansk", flag: "๐ฉ๐ฐ" },
- "no": { name: "Norsk", flag: "๐ณ๐ด" },
-};
-
-function getLocaleFromCookie() {
- if (typeof document === "undefined") return "en";
- const cookie = document.cookie
- .split(";")
- .find((c) => c.trim().startsWith(`${LOCALE_COOKIE}=`));
- const value = cookie ? decodeURIComponent(cookie.split("=")[1]) : "en";
- return normalizeLocale(value);
-}
+import { ConfirmModal } from "./Modal";
function MenuItem({ icon, label, onClick, trailing, danger }) {
return (
@@ -83,15 +36,21 @@ MenuItem.propTypes = {
export default function HeaderMenu({ onLogout }) {
const [isOpen, setIsOpen] = useState(false);
const [changelogOpen, setChangelogOpen] = useState(false);
- const [remoteOpen, setRemoteOpen] = useState(false);
- const [langOpen, setLangOpen] = useState(false);
- const [locale, setLocale] = useState("en");
+ const [shutdownOpen, setShutdownOpen] = useState(false);
+ const [isShuttingDown, setIsShuttingDown] = useState(false);
const { toggleTheme, isDark } = useTheme();
const menuRef = useRef(null);
- useEffect(() => {
- setLocale(getLocaleFromCookie());
- }, [langOpen]);
+ const handleShutdown = async () => {
+ setIsShuttingDown(true);
+ try {
+ await fetch("/api/version/shutdown", { method: "POST" });
+ } catch (e) {
+ // Expected to fail as server shuts down; ignore error
+ }
+ setIsShuttingDown(false);
+ setShutdownOpen(false);
+ };
useEffect(() => {
const handleClickOutside = (e) => {
@@ -125,21 +84,16 @@ export default function HeaderMenu({ onLogout }) {
label="Change Log"
onClick={() => { close(); setChangelogOpen(true); }}
/>
-