update('network')}
- >
-
{t('virtualDevice.network')}
-
+
+
+ {t('settings.device.network')}
+ {t('settings.device.networkDesc')}
+
+
+
update('network')}
+ />
>
);
diff --git a/web/src/pages/desktop/menu/settings/device/wifi.tsx b/web/src/pages/desktop/menu/settings/device/wifi.tsx
new file mode 100644
index 0000000..5503f9c
--- /dev/null
+++ b/web/src/pages/desktop/menu/settings/device/wifi.tsx
@@ -0,0 +1,45 @@
+import { useEffect, useState } from 'react';
+import { Button } from 'antd';
+import { useTranslation } from 'react-i18next';
+
+import * as api from '@/api/network.ts';
+
+export const Wifi = () => {
+ const { t } = useTranslation();
+
+ const [isSupported, setIsSupported] = useState(false);
+
+ useEffect(() => {
+ api.getWiFi().then((rsp) => {
+ if (rsp.code !== 0) {
+ console.log(rsp.msg);
+ return;
+ }
+
+ setIsSupported(rsp.data?.supported);
+ });
+ }, []);
+
+ function open() {
+ window.open('/#/wifi', '_blank');
+ }
+
+ return (
+ <>
+ {isSupported && (
+
+
+ {t('settings.device.wifi.title')}
+
+ {t('settings.device.wifi.description')}
+
+
+
+
+
+ )}
+ >
+ );
+};
diff --git a/web/src/pages/desktop/menu/settings/index.tsx b/web/src/pages/desktop/menu/settings/index.tsx
index c8cd845..9d15bf9 100644
--- a/web/src/pages/desktop/menu/settings/index.tsx
+++ b/web/src/pages/desktop/menu/settings/index.tsx
@@ -1,67 +1,154 @@
import { useEffect, useState } from 'react';
-import { Badge, Divider, Popover } from 'antd';
-import { useAtom } from 'jotai';
-import { SettingsIcon } from 'lucide-react';
+import { Badge, Modal } from 'antd';
+import clsx from 'clsx';
+import {
+ BadgeInfoIcon,
+ CircleArrowUpIcon,
+ PaletteIcon,
+ SettingsIcon,
+ SmartphoneIcon,
+ UserRoundIcon
+} from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import semver from 'semver';
-import { getWiFi } from '@/api/network.ts';
-import { isSettingsOpenAtom } from '@/jotai/settings.ts';
+import * as api from '@/api/application.ts';
+import * as ls from '@/lib/localstorage.ts';
+import { Tailscale as TailscaleIcon } from '@/components/icons/tailscale';
-import { About } from './about.tsx';
-import { Language } from './language.tsx';
-import { Logout } from './logout.tsx';
-import { Password } from './password.tsx';
+import { About } from './about';
+import { Account } from './account';
+import { Appearance } from './appearance';
+import { Device } from './device';
import { Tailscale } from './tailscale';
-import { Update } from './update.tsx';
-import { VirtualDevices } from './virtual-devices.tsx';
-import { Wifi } from './wifi.tsx';
+import { Update } from './update';
export const Settings = () => {
- const [isWifiSupported, setIsWifiSupported] = useState(false);
- const [isSettingsOpen, setIsSettingsOpen] = useAtom(isSettingsOpenAtom);
- const [isBadgeVisible, setIsBadgeVisible] = useState(false);
+ const { t } = useTranslation();
+
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [isLocked, setIsLocked] = useState(false);
+ const [currentTab, setCurrentTab] = useState('about');
+
+ const [isUpdateAvailable, setIsUpdateAvailable] = useState(false);
+
+ const tabs = [
+ { id: 'about', icon:
, component:
},
+ { id: 'appearance', icon:
, component:
},
+ { id: 'device', icon:
, component:
},
+ {
+ id: 'tailscale',
+ icon:
,
+ component:
+ },
+ {
+ id: 'update',
+ icon:
,
+ component:
+ },
+ { id: 'account', icon:
, component:
}
+ ];
useEffect(() => {
- getWiFi().then((rsp) => {
- if (rsp.code === 0 && rsp.data.supported) {
- setIsWifiSupported(true);
- }
- });
+ const skip = ls.getSkipUpdate();
+ if (!skip) {
+ checkForUpdates();
+ }
}, []);
- const content = (
- <>
-
-
-
-
+ function checkForUpdates() {
+ api.getVersion().then((rsp: any) => {
+ if (rsp.code !== 0) {
+ return;
+ }
-
-
+ if (semver.gt(rsp.data.latest, rsp.data.current)) {
+ setIsUpdateAvailable(true);
+ }
+ });
+ }
-
- {isWifiSupported &&
}
-
+ function changeTab(tab: string) {
+ if (isLocked) {
+ return;
+ }
-
-
- >
- );
+ setCurrentTab(tab);
+
+ if (isUpdateAvailable && tab === 'update') {
+ setIsUpdateAvailable(false);
+ ls.setSkipUpdate(true);
+ }
+ }
+
+ function closeModal() {
+ if (isLocked) {
+ return;
+ }
+
+ setIsModalOpen(false);
+ setCurrentTab('about');
+ }
return (
<>
-
setIsModalOpen(true)}
>
-
+
+
+
+
+
{t('settings.title')}
+
+
+ {tabs.map((tab) => (
+
changeTab(tab.id)}
+ >
+
{tab.icon}
+
+ {isUpdateAvailable && tab.id === 'update' ? (
+
+
+ {t(`settings.${tab.id}.title`)}
+
+
+ ) : (
+
+ {t(`settings.${tab.id}.title`)}
+
+ )}
+
+ ))}
+
+
+
+
+ <>{tabs.find((tab) => tab.id === currentTab)?.component}>
+
+
-
+
>
);
};
diff --git a/web/src/pages/desktop/menu/settings/language.tsx b/web/src/pages/desktop/menu/settings/language.tsx
deleted file mode 100644
index 9871491..0000000
--- a/web/src/pages/desktop/menu/settings/language.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import { Popover } from 'antd';
-import clsx from 'clsx';
-import { LanguagesIcon } from 'lucide-react';
-import { useTranslation } from 'react-i18next';
-
-import languages from '@/i18n/languages.ts';
-import { setLanguage } from '@/lib/localstorage.ts';
-
-export const Language = () => {
- const { t, i18n } = useTranslation();
-
- function changeLanguage(lng: string) {
- if (i18n.language === lng) return;
-
- i18n.changeLanguage(lng);
- setLanguage(lng);
- }
-
- const content = (
- <>
- {languages.map((lng) => (
-
changeLanguage(lng.key)}
- >
- {lng.name}
-
- ))}
- >
- );
-
- return (
-
-
-
- {t('language')}
-
-
- );
-};
diff --git a/web/src/pages/desktop/menu/settings/logout.tsx b/web/src/pages/desktop/menu/settings/logout.tsx
deleted file mode 100644
index a03526a..0000000
--- a/web/src/pages/desktop/menu/settings/logout.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { useTranslation } from 'react-i18next';
-import { useNavigate } from 'react-router-dom';
-
-import { removeToken } from '@/lib/cookie.ts';
-
-export const Logout = () => {
- const navigate = useNavigate();
- const { t } = useTranslation();
-
- function logout() {
- removeToken();
- navigate('/auth/login');
- }
-
- return (
-
- {t('logout')}
-
- );
-};
diff --git a/web/src/pages/desktop/menu/settings/password.tsx b/web/src/pages/desktop/menu/settings/password.tsx
deleted file mode 100644
index 8cdb9d7..0000000
--- a/web/src/pages/desktop/menu/settings/password.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { useTranslation } from 'react-i18next';
-import { useNavigate } from 'react-router-dom';
-
-export const Password = () => {
- const navigate = useNavigate();
- const { t } = useTranslation();
-
- function changePassword() {
- navigate('/auth/password');
- }
-
- return (
-
- {t('changePassword')}
-
- );
-};
diff --git a/web/src/pages/desktop/menu/settings/tailscale/device.tsx b/web/src/pages/desktop/menu/settings/tailscale/device.tsx
index 894c1d8..01d70c3 100644
--- a/web/src/pages/desktop/menu/settings/tailscale/device.tsx
+++ b/web/src/pages/desktop/menu/settings/tailscale/device.tsx
@@ -1,105 +1,96 @@
import { useEffect, useState } from 'react';
import { LogoutOutlined } from '@ant-design/icons';
-import { Button, Divider, Switch } from 'antd';
+import { Button, Switch } from 'antd';
import { useTranslation } from 'react-i18next';
-import { logoutTailscale, updateTailscaleStatus } from '@/api/network.ts';
+import * as api from '@/api/network.ts';
type DeviceProps = {
- status: string;
- name: string;
- ip: string;
- account: string;
- setStatus: (status: 'notLogin') => void;
+ status: any;
+ onLogout: () => void;
};
-export const Device = ({ status, name, ip, account, setStatus }: DeviceProps) => {
+export const Device = ({ status, onLogout }: DeviceProps) => {
const { t } = useTranslation();
const [isRunning, setIsRunning] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
- const [showLogoutConfirmation, setShowLogoutConfirmation] = useState(false);
- const [isLoggingOut, setIsLoggingOut] = useState(false);
+ const [isConfirmation, setIsConfirmation] = useState(false);
+ const [isLogging, setIsLogging] = useState(false);
const [errMsg, setErrMsg] = useState('');
useEffect(() => {
- setIsRunning(status === 'running');
- }, []);
+ setIsRunning(status.state === 'running');
+ }, [status]);
- function updateStatus() {
+ async function update() {
if (isUpdating) return;
setIsUpdating(true);
- const command = isRunning ? 'down' : 'up';
+ try {
+ const rsp = isRunning ? await api.downTailscale() : await api.upTailscale();
+ if (rsp.code !== 0) {
+ setErrMsg(rsp.msg);
+ return;
+ }
- updateTailscaleStatus(command)
- .then((rsp) => {
- if (rsp.code !== 0) {
- setErrMsg(rsp.msg);
- return;
- }
-
- setIsRunning(rsp.data.status === 'running');
- })
- .finally(() => {
- setIsUpdating(false);
- });
+ setIsRunning(!isRunning);
+ } finally {
+ setIsUpdating(false);
+ }
}
- function logout() {
- if (isLoggingOut) return;
- setIsLoggingOut(true);
+ async function logout() {
+ if (isLogging) return;
+ setIsLogging(true);
- logoutTailscale()
+ api
+ .logoutTailscale()
.then((rsp) => {
if (rsp.code !== 0) {
setErrMsg(rsp.msg);
return;
}
- setStatus('notLogin');
+ onLogout();
})
.finally(() => {
- setIsLoggingOut(false);
+ setIsLogging(false);
});
}
return (
-
-
-
{t('tailscale.enable')}
-
+
+
+ {t('settings.tailscale.enable')}
+
-
-
-
{t('tailscale.deviceName')}
-
{name}
+
+ {t('settings.tailscale.deviceName')}
+ {status.name}
-
-
-
{t('tailscale.deviceIP')}
-
{ip}
+
+ {t('settings.tailscale.deviceIP')}
+ {status.ip}
-
-
-
{t('tailscale.account')}
-
{account}
+
+ {t('settings.tailscale.account')}
+ {status.account}
-
-
- {!showLogoutConfirmation ? (
+
+ {!isConfirmation ? (
}
- onClick={() => setShowLogoutConfirmation(true)}
+ onClick={() => setIsConfirmation(true)}
>
- {t('tailscale.logout')}
+ {t('settings.tailscale.logout')}
) : (
)}
diff --git a/web/src/pages/desktop/menu/settings/tailscale/index.tsx b/web/src/pages/desktop/menu/settings/tailscale/index.tsx
index 4a8764f..483e9fe 100644
--- a/web/src/pages/desktop/menu/settings/tailscale/index.tsx
+++ b/web/src/pages/desktop/menu/settings/tailscale/index.tsx
@@ -1,52 +1,51 @@
-import { useState } from 'react';
-import { Modal } from 'antd';
-import { useSetAtom } from 'jotai';
+import { useEffect, useState } from 'react';
+import { Divider } from 'antd';
import { LoaderCircleIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { getTailscaleStatus } from '@/api/network.ts';
-import { isSettingsOpenAtom } from '@/jotai/settings.ts';
+import * as api from '@/api/network.ts';
import { Device } from './device.tsx';
import { Install } from './install.tsx';
import { Login } from './login.tsx';
-type Status = 'notInstall' | 'notLogin' | 'stopped' | 'running';
+type State = 'notInstall' | 'notLogin' | 'stopped' | 'running';
-export const Tailscale = () => {
+type Status = {
+ state: State;
+ name: string;
+ ip: string;
+ account: string;
+};
+
+type TailscaleProps = {
+ setIsLocked: (isLocked: boolean) => void;
+};
+
+export const Tailscale = ({ setIsLocked }: TailscaleProps) => {
const { t } = useTranslation();
- const setIsSettingsOpen = useSetAtom(isSettingsOpenAtom);
- const [isModalOpen, setIsModalOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
- const [status, setStatus] = useState
('notInstall');
- const [name, setName] = useState('');
- const [ip, setIp] = useState('');
- const [account, setAccount] = useState('');
+ const [status, setStatus] = useState();
const [errMsg, setErrMsg] = useState('');
- function openModal() {
+ useEffect(() => {
getStatus();
-
- setIsModalOpen(true);
- setIsSettingsOpen(false);
- }
+ }, []);
function getStatus() {
if (isLoading) return;
setIsLoading(true);
- getTailscaleStatus()
+ api
+ .getTailscaleStatus()
.then((rsp) => {
if (rsp.code !== 0) {
setErrMsg(rsp.msg);
return;
}
- setStatus(rsp.data.status);
- setName(rsp.data.name);
- setIp(rsp.data.ip);
- setAccount(rsp.data.account);
+ setStatus(rsp.data);
})
.finally(() => {
setIsLoading(false);
@@ -55,39 +54,29 @@ export const Tailscale = () => {
return (
<>
-
- Tailscale
-
+ {t('settings.tailscale.title')}
+
- setIsModalOpen(false)}
- >
-
- {isLoading ? (
-
-
- {t('tailscale.loading')}
-
- ) : status === 'notInstall' ? (
-
- ) : status === 'notLogin' ? (
-
- ) : (
-
+ {isLoading ? (
+
+
+ {t('settings.tailscale.loading')}
+
+ ) : (
+ <>
+ {status?.state === 'notInstall' && (
+
)}
- {errMsg &&
{errMsg}}
-
-
+ {status?.state === 'notLogin' && }
+
+ {(status?.state === 'stopped' || status?.state === 'running') && (
+
+ )}
+
+ {errMsg && {errMsg}
}
+ >
+ )}
>
);
};
diff --git a/web/src/pages/desktop/menu/settings/tailscale/install.tsx b/web/src/pages/desktop/menu/settings/tailscale/install.tsx
index 1708654..9eaa505 100644
--- a/web/src/pages/desktop/menu/settings/tailscale/install.tsx
+++ b/web/src/pages/desktop/menu/settings/tailscale/install.tsx
@@ -1,81 +1,82 @@
import { useState } from 'react';
-import { DownloadOutlined } from '@ant-design/icons';
-import { Button, Card } from 'antd';
+import { InboxOutlined, InfoCircleOutlined } from '@ant-design/icons';
+import { Button, Card, Result } from 'antd';
import { useTranslation } from 'react-i18next';
-import { installTailscale } from '@/api/network.ts';
+import * as api from '@/api/network.ts';
type InstallProps = {
+ setIsLocked: (setIsLocked: boolean) => void;
onSuccess: () => void;
};
-export const Install = ({ onSuccess }: InstallProps) => {
+export const Install = ({ setIsLocked, onSuccess }: InstallProps) => {
const { t } = useTranslation();
- const [isInstalling, setIsInstalling] = useState(false);
- const [isFailed, setIsFailed] = useState(false);
+ const [state, setState] = useState('');
function install() {
- if (isInstalling) return;
- setIsInstalling(true);
+ if (state === 'installing') return;
+ setState('installing');
+ setIsLocked(true);
- installTailscale()
+ api
+ .installTailscale()
.then((rsp) => {
if (rsp.code !== 0) {
- setIsFailed(true);
+ setState('failed');
return;
}
onSuccess();
})
.finally(() => {
- setIsInstalling(false);
+ setState('');
+ setIsLocked(false);
});
}
return (
<>
- {!isFailed ? (
-
- {t('tailscale.notInstall')}
-
- }
- loading={isInstalling}
- onClick={install}
- >
- {isInstalling ? t('tailscale.installing') : t('tailscale.install')}
-
-
+ {state !== 'failed' ? (
+ }
+ subTitle={t('settings.tailscale.notInstall')}
+ extra={
+
+ }
+ />
) : (
-
-
- {t('tailscale.failed')}
- {t('tailscale.retry')}
-
-
-
-
- -
- {t('tailscale.download')}
-
- {t('tailscale.package')}
-
- {t('tailscale.unzip')}
-
- - {t('tailscale.upTailscale')}
- - {t('tailscale.upTailscaled')}
- - {t('tailscale.refresh')}
-
-
-
+ }
+ extra={
+
+
+ -
+ {t('settings.tailscale.download')}
+
+ {t('settings.tailscale.package')}
+
+ {t('settings.tailscale.unzip')}
+
+ - {t('settings.tailscale.upTailscale')}
+ - {t('settings.tailscale.upTailscaled')}
+ - {t('settings.tailscale.refresh')}
+
+
+ }
+ />
)}
>
);
diff --git a/web/src/pages/desktop/menu/settings/tailscale/login.tsx b/web/src/pages/desktop/menu/settings/tailscale/login.tsx
index d1501fe..2413d30 100644
--- a/web/src/pages/desktop/menu/settings/tailscale/login.tsx
+++ b/web/src/pages/desktop/menu/settings/tailscale/login.tsx
@@ -3,7 +3,7 @@ import { UserSwitchOutlined } from '@ant-design/icons';
import { Button, Card } from 'antd';
import { useTranslation } from 'react-i18next';
-import { loginTailscale } from '@/api/network.ts';
+import * as api from '@/api/network.ts';
type LoginProps = {
onSuccess: () => void;
@@ -12,66 +12,64 @@ type LoginProps = {
export const Login = ({ onSuccess }: LoginProps) => {
const { t } = useTranslation();
- const [isLogging, setIsLogging] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
const [loginUrl, setLoginUrl] = useState('');
const [errMsg, setErrMsg] = useState('');
function login() {
- if (isLogging) return;
- setIsLogging(true);
+ if (isLoading) return;
+ setIsLoading(true);
- loginTailscale()
+ api
+ .loginTailscale()
.then((rsp) => {
if (rsp.code !== 0) {
setErrMsg(rsp.msg);
return;
}
- // already logged in
- if (rsp.data.status === 'running') {
+ const url = rsp.data.url;
+ if (!url) {
onSuccess();
return;
}
- const url = rsp.data.url;
- if (!url) return;
-
setLoginUrl(url);
window.open(url, '_blank');
setTimeout(() => setLoginUrl(''), 10 * 60 * 1000);
})
.finally(() => {
- setIsLogging(false);
+ setIsLoading(false);
});
}
return (
-
-
{t('tailscale.notLogin')}
+
+
{t('settings.tailscale.notLogin')}
- {!loginUrl ? (
+ {loginUrl === '' ? (
}
- loading={isLogging}
+ loading={isLoading}
onClick={login}
>
- {t('tailscale.login')}
+ {t('settings.tailscale.login')}
) : (
- <>
+
- {t('tailscale.urlPeriod')}
+ {t('settings.tailscale.urlPeriod')}
- >
+
)}
{errMsg &&
{errMsg}}
diff --git a/web/src/pages/desktop/menu/settings/update.tsx b/web/src/pages/desktop/menu/settings/update.tsx
deleted file mode 100644
index 6d316a5..0000000
--- a/web/src/pages/desktop/menu/settings/update.tsx
+++ /dev/null
@@ -1,174 +0,0 @@
-import { useEffect, useState } from 'react';
-import { Badge, Button, Modal, Spin } from 'antd';
-import { useSetAtom } from 'jotai';
-import { useTranslation } from 'react-i18next';
-import semver from 'semver';
-
-import * as api from '@/api/application.ts';
-import { getSkipUpdate, setSkipUpdate } from '@/lib/localstorage.ts';
-import { isSettingsOpenAtom } from '@/jotai/settings.ts';
-
-type UpdateProps = {
- setIsBadgeVisible: (isBadgeVisible: boolean) => void;
-};
-
-export const Update = ({ setIsBadgeVisible }: UpdateProps) => {
- const { t } = useTranslation();
- const setIsSettingsOpen = useSetAtom(isSettingsOpenAtom);
-
- const [isModalOpen, setIsModalOpen] = useState(false);
- const [isLoading, setIsLoading] = useState(false);
- const [current, setCurrent] = useState('');
- const [latest, setLatest] = useState('');
- const [isLatest, setIsLatest] = useState(true);
- const [isUpdating, setIsUpdating] = useState(false);
- const [errMsg, setErrMsg] = useState('');
-
- useEffect(() => {
- const skip = getSkipUpdate();
- if (!skip) {
- getVersion();
- }
- }, []);
-
- function showModal() {
- getVersion();
-
- setErrMsg('');
- setIsModalOpen(true);
-
- setIsSettingsOpen(false);
- }
-
- function getVersion() {
- if (isLoading) return;
- setIsLoading(true);
-
- api
- .getVersion()
- .then((rsp: any) => {
- if (rsp.code !== 0) {
- setErrMsg(t('update.queryFailed'));
- return;
- }
-
- setCurrent(rsp.data.current);
- setLatest(rsp.data.latest);
-
- if (semver.gt(rsp.data.latest, rsp.data.current)) {
- setIsLatest(false);
- setIsBadgeVisible(true);
- }
- })
- .catch(() => {
- setErrMsg(t('update.queryFailed'));
- })
- .finally(() => {
- setIsLoading(false);
- });
- }
-
- function confirm() {
- if (isLatest || !latest) {
- setIsModalOpen(false);
- return;
- }
-
- if (isUpdating) return;
- setIsUpdating(true);
-
- api
- .update()
- .then((rsp: any) => {
- if (rsp.code !== 0) {
- setErrMsg(t('update.updateFailed'));
- }
- })
- .finally(() => {
- setTimeout(() => {
- setIsUpdating(false);
- setErrMsg('');
-
- window.location.reload();
- }, 6000);
- });
- }
-
- function cancel() {
- setIsBadgeVisible(false);
-
- setIsLatest(true);
- setIsModalOpen(false);
- setSkipUpdate(true);
- }
-
- return (
- <>
-
-
- {t('update.title')}
-
-
-
-
- {isUpdating ? (
-
-
- {t('update.updating')}
-
- ) : (
-
- {isLoading ? (
-
- ) : (
- <>
- {errMsg !== '' ? (
-
{errMsg}
- ) : (
- <>
- {isLatest ? (
-
- {current}
- {t('update.isLatest')}
-
- ) : (
-
-
- {current}
- {'->'}
- {latest}
-
-
{t('update.available')}
-
- )}
- >
- )}
- >
- )}
-
-
-
- {!isLatest && (
-
- )}
-
-
- )}
-
- >
- );
-};
diff --git a/web/src/pages/desktop/menu/settings/update/index.tsx b/web/src/pages/desktop/menu/settings/update/index.tsx
new file mode 100644
index 0000000..da6d9c1
--- /dev/null
+++ b/web/src/pages/desktop/menu/settings/update/index.tsx
@@ -0,0 +1,119 @@
+import { useEffect, useState } from 'react';
+import { LoadingOutlined, RocketOutlined, SmileOutlined } from '@ant-design/icons';
+import { Button, Divider, Result, Spin } from 'antd';
+import { useTranslation } from 'react-i18next';
+import semver from 'semver';
+
+import * as api from '@/api/application.ts';
+
+type UpdateProps = {
+ setIsLocked: (isClosable: boolean) => void;
+};
+
+type Status = 'loading' | 'updating' | 'outdated' | 'latest' | 'failed';
+
+export const Update = ({ setIsLocked }: UpdateProps) => {
+ const { t } = useTranslation();
+
+ const [status, setStatus] = useState
('loading');
+ const [currentVersion, setCurrentVersion] = useState('');
+ const [latestVersion, setLatestVersion] = useState('');
+ const [errMsg, setErrMsg] = useState('');
+
+ useEffect(() => {
+ checkForUpdates();
+ }, []);
+
+ function checkForUpdates() {
+ setStatus('loading');
+
+ api
+ .getVersion()
+ .then((rsp: any) => {
+ if (rsp.code !== 0) {
+ setStatus('failed');
+ setErrMsg(t('settings.update.queryFailed'));
+ return;
+ }
+
+ setCurrentVersion(rsp.data.current);
+ setLatestVersion(rsp.data.latest);
+
+ const isLatest = semver.gte(rsp.data.current, rsp.data.latest);
+ setStatus(isLatest ? 'latest' : 'outdated');
+ })
+ .catch(() => {
+ setStatus('failed');
+ setErrMsg(t('settings.update.queryFailed'));
+ });
+ }
+
+ function update() {
+ if (status !== 'outdated') return;
+
+ setIsLocked(true);
+ setStatus('updating');
+
+ api
+ .update()
+ .then((rsp: any) => {
+ if (rsp.code !== 0) {
+ setStatus('failed');
+ setErrMsg(t('settings.update.updateFailed'));
+ }
+ })
+ .finally(() => {
+ setTimeout(() => {
+ setIsLocked(false);
+ setErrMsg('');
+
+ window.location.reload();
+ }, 6000);
+ });
+ }
+
+ return (
+ <>
+ {t('settings.update.title')}
+
+
+ {status === 'loading' && (
+
+ } size="large" />
+
+ )}
+
+ {status === 'updating' && (
+
+
+ {t('settings.update.updating')}
+
+ )}
+
+ {status === 'latest' && (
+ }
+ title={currentVersion}
+ subTitle={t('settings.update.isLatest')}
+ />
+ )}
+
+ {status === 'outdated' && (
+ }
+ title={`${currentVersion} -> ${latestVersion}`}
+ subTitle={t('settings.update.available')}
+ extra={[
+
+ ]}
+ />
+ )}
+
+ {status === 'failed' && }
+ >
+ );
+};
diff --git a/web/src/pages/desktop/menu/settings/wifi.tsx b/web/src/pages/desktop/menu/settings/wifi.tsx
deleted file mode 100644
index 4dd5e76..0000000
--- a/web/src/pages/desktop/menu/settings/wifi.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-export const Wifi = () => {
- function open() {
- window.open('/#/wifi', '_blank');
- }
-
- return (
-
- Wi-Fi
-
- );
-};
diff --git a/web/src/pages/desktop/menu/terminal/index.tsx b/web/src/pages/desktop/menu/terminal/index.tsx
index c7af9b0..7f65584 100644
--- a/web/src/pages/desktop/menu/terminal/index.tsx
+++ b/web/src/pages/desktop/menu/terminal/index.tsx
@@ -28,10 +28,11 @@ export const Terminal = () => {
content={content}
placement="bottomLeft"
trigger="click"
+ arrow={false}
open={isPopoverOpen}
onOpenChange={setIsPopoverOpen}
>
-
+
diff --git a/web/src/pages/desktop/menu/terminal/nanokvm.tsx b/web/src/pages/desktop/menu/terminal/nanokvm.tsx
index 1ad2c2c..592ac0f 100644
--- a/web/src/pages/desktop/menu/terminal/nanokvm.tsx
+++ b/web/src/pages/desktop/menu/terminal/nanokvm.tsx
@@ -15,7 +15,7 @@ export const Nanokvm = ({ setIsPopoverOpen }: NanokvmProps) => {
return (
diff --git a/web/src/pages/desktop/menu/terminal/serial-port.tsx b/web/src/pages/desktop/menu/terminal/serial-port.tsx
index c9c192c..60848c0 100644
--- a/web/src/pages/desktop/menu/terminal/serial-port.tsx
+++ b/web/src/pages/desktop/menu/terminal/serial-port.tsx
@@ -56,7 +56,7 @@ export const SerialPort = ({ setIsPopoverOpen }: SerialPortProps) => {
return (
<>
diff --git a/web/src/pages/desktop/menu/wol.tsx b/web/src/pages/desktop/menu/wol.tsx
deleted file mode 100644
index b1a5c84..0000000
--- a/web/src/pages/desktop/menu/wol.tsx
+++ /dev/null
@@ -1,159 +0,0 @@
-import { ChangeEvent, useRef, useState } from 'react';
-import { Button, Divider, Input, List, Popover } from 'antd';
-import type { InputRef } from 'antd';
-import clsx from 'clsx';
-import { useSetAtom } from 'jotai';
-import { NetworkIcon, SendIcon, Trash2Icon } from 'lucide-react';
-import { useTranslation } from 'react-i18next';
-
-import { deleteWolMac, getWolMacs, wol } from '@/api/network.ts';
-import { isKeyboardEnableAtom } from '@/jotai/keyboard.ts';
-
-export const Wol = () => {
- const { t } = useTranslation();
- const setIsKeyboardEnable = useSetAtom(isKeyboardEnableAtom);
-
- const [isPopoverOpen, setIsPopoverOpen] = useState(false);
-
- const [input, setInput] = useState('');
- const [status, setStatus] = useState('');
- const [log, setLog] = useState('');
-
- const [macList, setMacList] = useState
([]);
-
- const inputRef = useRef(null);
-
- function handleOpenChange(open: boolean) {
- if (open) {
- getMacs();
-
- setIsKeyboardEnable(false);
- } else {
- setInput('');
- setStatus('');
- setLog('');
-
- setIsKeyboardEnable(true);
- }
-
- setIsPopoverOpen(open);
- }
-
- function handleChange(e: ChangeEvent) {
- setInput(e.target.value);
- }
-
- function getMacs() {
- getWolMacs().then((rsp) => {
- if (rsp.code !== 0) {
- console.log(rsp.msg);
- return;
- }
-
- setMacList(rsp.data.macs.filter((mac: string) => mac !== ''));
- });
- }
-
- function deleteMac(mac: string) {
- deleteWolMac(mac).then((rsp) => {
- if (rsp.code === 0) {
- setMacList(macList.filter((item) => item !== mac));
- }
- });
- }
-
- function wake(mac?: string) {
- if (status === 'loading') return;
-
- const value = mac ? mac : input;
- if (!value) return;
-
- setStatus('loading');
- setLog(t('wol.sending'));
-
- wol(value)
- .then((rsp) => {
- if (rsp.code !== 0) {
- setStatus('failed');
- setLog(rsp.msg);
- return;
- }
-
- setStatus('success');
- setLog(t('wol.sent'));
- })
- .catch(() => {
- setStatus('failed');
- });
- }
-
- const content = (
-
-
- Wake-on-LAN
-
-
-
-
-
{t('wol.input')}
-
-
-
-
-
-
- {status && (
-
- {log}
-
- )}
-
-
-
(
-
- {mac}
-
-
wake(mac)}
- >
-
-
-
deleteMac(mac)}
- >
-
-
-
-
- )}
- />
-
- );
-
- return (
-
-
-
-
-
- );
-};
diff --git a/web/src/pages/desktop/menu-phone/wol.tsx b/web/src/pages/desktop/menu/wol/index.tsx
similarity index 88%
rename from web/src/pages/desktop/menu-phone/wol.tsx
rename to web/src/pages/desktop/menu/wol/index.tsx
index 662fe1d..ca58d08 100644
--- a/web/src/pages/desktop/menu-phone/wol.tsx
+++ b/web/src/pages/desktop/menu/wol/index.tsx
@@ -5,12 +5,15 @@ import clsx from 'clsx';
import { useSetAtom } from 'jotai';
import { NetworkIcon, SendIcon, Trash2Icon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
+import { useMediaQuery } from 'react-responsive';
import { deleteWolMac, getWolMacs, wol } from '@/api/network.ts';
import { isKeyboardEnableAtom } from '@/jotai/keyboard.ts';
export const Wol = () => {
const { t } = useTranslation();
+ const isBigScreen = useMediaQuery({ minWidth: 640 });
+
const setIsKeyboardEnable = useSetAtom(isKeyboardEnableAtom);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
@@ -90,7 +93,7 @@ export const Wol = () => {
const content = (
- Wake-on-LAN
+ {t('wol.title')}
@@ -123,7 +126,7 @@ export const Wol = () => {
renderItem={(mac) => (
{mac}
-
+
wake(mac)}
@@ -146,13 +149,14 @@ export const Wol = () => {
return (
-
-
+
+
);
diff --git a/web/src/pages/desktop/mouse/relative.tsx b/web/src/pages/desktop/mouse/relative.tsx
index bb8fba5..a527e42 100644
--- a/web/src/pages/desktop/mouse/relative.tsx
+++ b/web/src/pages/desktop/mouse/relative.tsx
@@ -24,6 +24,7 @@ export const Relative = () => {
if (!canvas) return;
messageApi.open({
+ key: 'no_mouse_relative',
type: 'info',
content: t('mouse.requestPointer'),
duration: 3,
diff --git a/web/src/pages/desktop/notification.tsx b/web/src/pages/desktop/notification.tsx
index e2741c1..4c06eef 100644
--- a/web/src/pages/desktop/notification.tsx
+++ b/web/src/pages/desktop/notification.tsx
@@ -23,11 +23,16 @@ export const Notification = () => {
}, []);
function openNotification() {
- api.info({
+ api.warning({
+ key: 'no_change_password',
message: t('auth.changePassword'),
description: t('auth.changePasswordDesc'),
placement: 'topRight',
- btn:
,
+ btn: (
+
+ ),
duration: null,
onClose: () => setSkipModifyPassword(true)
});
diff --git a/web/src/pages/desktop/screen/h264.tsx b/web/src/pages/desktop/screen/h264.tsx
index fc88086..f40a104 100644
--- a/web/src/pages/desktop/screen/h264.tsx
+++ b/web/src/pages/desktop/screen/h264.tsx
@@ -1,5 +1,4 @@
import { useEffect, useState } from 'react';
-import { LoadingOutlined } from '@ant-design/icons';
import { Spin } from 'antd';
import clsx from 'clsx';
import { useAtomValue } from 'jotai';
@@ -20,7 +19,11 @@ export const H264 = () => {
const videoElement = document.getElementById('screen') as HTMLVideoElement;
const pc = new RTCPeerConnection({
- iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
+ iceServers: [
+ {
+ urls: ['stun:stun.l.google.com:19302', 'stun:turn.cloudflare.com:3478']
+ }
+ ]
});
pc.ontrack = function (event) {
@@ -85,23 +88,23 @@ export const H264 = () => {
setTimeout(() => {
setIsLoading(false);
- }, 10 * 1000);
+ }, 15 * 1000);
return () => {
- heartbeatTimer && clearInterval(heartbeatTimer);
+ if (heartbeatTimer) {
+ clearInterval(heartbeatTimer);
+ }
ws.close();
pc.close();
};
}, []);
return (
- <>
- {isLoading &&
} size="large" fullscreen />}
-
+
- >
+
);
};
diff --git a/web/src/pages/desktop/screen/mjpeg.tsx b/web/src/pages/desktop/screen/mjpeg.tsx
index 2ac9edb..743b17d 100644
--- a/web/src/pages/desktop/screen/mjpeg.tsx
+++ b/web/src/pages/desktop/screen/mjpeg.tsx
@@ -22,7 +22,7 @@ export const Mjpeg = () => {
{
const { t } = useTranslation();
- const [token, setToken] = useState(`t=${encrypt('root')}`);
+ const [token, setToken] = useState('u=root');
return (
<>
diff --git a/web/src/pages/terminal/login.tsx b/web/src/pages/terminal/login.tsx
index 1461d2f..f4ea09e 100644
--- a/web/src/pages/terminal/login.tsx
+++ b/web/src/pages/terminal/login.tsx
@@ -16,8 +16,7 @@ export const Login = ({ setToken }: LoginProps) => {
const username = values.username;
const password = encrypt(values.password);
- const token = username === 'root' ? `t=${password}` : `u=${values.username}&t=${password}`;
- setToken(token);
+ setToken(`u=${username}&t=${password}`);
}
return (
diff --git a/web/src/pages/auth/wifi/index.tsx b/web/src/pages/wifi/index.tsx
similarity index 78%
rename from web/src/pages/auth/wifi/index.tsx
rename to web/src/pages/wifi/index.tsx
index e44c93c..481665b 100644
--- a/web/src/pages/auth/wifi/index.tsx
+++ b/web/src/pages/wifi/index.tsx
@@ -1,28 +1,29 @@
import { useState } from 'react';
import { CheckOutlined, LockOutlined, WifiOutlined } from '@ant-design/icons';
import { Button, Form, Input } from 'antd';
+import { useTranslation } from 'react-i18next';
-import { connectWifi } from '@/api/auth.ts';
+import { connectWifi } from '@/api/network.ts';
import { Head } from '@/components/head.tsx';
type State = '' | 'loading' | 'success' | 'failed';
export const Wifi = () => {
+ const { t } = useTranslation();
+
const [state, setState] = useState('');
function connect(values: any) {
if (state === 'loading' || state === 'success') return;
- setState('loading');
-
const ssid = values.ssid;
const password = values.password;
-
if (!ssid || !password) return;
+ setState('loading');
+
connectWifi(ssid, password)
.then((rsp) => {
- console.log(rsp);
setState(rsp.code === 0 ? 'success' : 'failed');
})
.catch(() => {
@@ -32,7 +33,7 @@ export const Wifi = () => {
return (
<>
-
+
@@ -56,7 +59,7 @@ export const Wifi = () => {
{state === 'success' ? (
}>
- Finished
+ {t('wifi.finishBtn')}
) : (
)}
@@ -73,14 +76,10 @@ export const Wifi = () => {
{state === 'success' && (
-
- Please check the network status of NanoKVM and visit the new IP address.
-
+ {t('wifi.success')}
)}
- {state === 'failed' && (
- Operation failed, please try again.
- )}
+ {state === 'failed' && {t('wifi.failed')} }
>
diff --git a/web/src/router.tsx b/web/src/router.tsx
index 56a48c3..a7135e5 100644
--- a/web/src/router.tsx
+++ b/web/src/router.tsx
@@ -45,7 +45,7 @@ export const router = createHashRouter([
{
path: '/wifi',
lazy: async () => {
- const { Wifi } = await import('./pages/auth/wifi');
+ const { Wifi } = await import('./pages/wifi');
return { Component: Wifi };
}
}