diff --git a/frontend/interface/ipc/useNyanpasu.ts b/frontend/interface/ipc/useNyanpasu.ts index ece0ba7a73..5a2f533ed6 100644 --- a/frontend/interface/ipc/useNyanpasu.ts +++ b/frontend/interface/ipc/useNyanpasu.ts @@ -1,5 +1,13 @@ import useSWR from "swr"; -import { getNyanpasuConfig, patchNyanpasuConfig, VergeConfig } from "@/service"; +import { + getNyanpasuConfig, + patchNyanpasuConfig, + VergeConfig, + getCoreVersion, + setClashCore as setClashCoreWithTauri, + restartSidecar, +} from "@/service"; +import { fetchCoreVersion, fetchLatestCore } from "@/service/core"; /** * useNyanpasu with swr. @@ -30,10 +38,30 @@ export const useNyanpasu = (options?: { } }; + const getClashCore = useSWR("getClashCore", fetchCoreVersion); + + const setClashCore = async ( + clashCore: Required["clash_core"], + ) => { + await setClashCoreWithTauri(clashCore); + + // timeout for restart clash core. + setTimeout(() => { + getClashCore.mutate(); + }, 100); + }; + + const getLatestCore = useSWR("getLatestCore", fetchLatestCore); + return { nyanpasuConfig: data, isLoading: !data && !error, isError: error, setNyanpasuConfig, + getCoreVersion, + getClashCore, + setClashCore, + restartSidecar, + getLatestCore, }; }; diff --git a/frontend/interface/service/core.ts b/frontend/interface/service/core.ts new file mode 100644 index 0000000000..56130a7b6c --- /dev/null +++ b/frontend/interface/service/core.ts @@ -0,0 +1,75 @@ +import { fetchLatestCoreVersions, getCoreVersion } from "./tauri"; +import { VergeConfig } from "./types"; + +export type ClashCore = Required["clash_core"]; + +export interface Core { + name: string; + core: ClashCore; + version?: string; + latest?: string; +} + +export const VALID_CORE: Core[] = [ + { name: "Clash Premium", core: "clash" }, + { name: "Mihomo", core: "mihomo" }, + { name: "Mihomo Alpha", core: "mihomo-alpha" }, + { name: "Clash Rust", core: "clash-rs" }, +]; + +export const fetchCoreVersion = async () => { + return await Promise.all( + VALID_CORE.map(async (item) => { + const version = await getCoreVersion(item.core); + return { ...item, version }; + }), + ); +}; + +export const fetchLatestCore = async () => { + const results = await fetchLatestCoreVersions(); + + const cores = VALID_CORE.map((item) => { + if (item.core == "clash") { + return { + ...item, + latest: `n${results["clash_premium"]}`, + }; + } else { + return { + ...item, + latest: results[item.core.replace(/-/g, "_") as keyof typeof results], + }; + } + }); + + return cores; +}; + +export enum SupportedArch { + // blocked by clash-rs + // WindowsX86 = "windows-x86", + WindowsX86_64 = "windows-x86_64", + // blocked by clash-rs#212 + // WindowsArm64 = "windows-arm64", + LinuxAarch64 = "linux-aarch64", + LinuxAmd64 = "linux-amd64", + DarwinArm64 = "darwin-arm64", + DarwinX64 = "darwin-x64", +} + +export enum SupportedCore { + Mihomo = "mihomo", + MihomoAlpha = "mihomo_alpha", + ClashRs = "clash_rs", + ClashPremium = "clash_premium", +} + +export type ArchMapping = { [key in SupportedArch]: string }; + +export interface ManifestVersion { + manifest_version: number; + latest: { [K in SupportedCore]: string }; + arch_template: { [K in SupportedCore]: ArchMapping }; + updated_at: string; // ISO 8601 +} diff --git a/frontend/interface/service/index.ts b/frontend/interface/service/index.ts index 257a6f7b86..e16da452ab 100644 --- a/frontend/interface/service/index.ts +++ b/frontend/interface/service/index.ts @@ -1,3 +1,4 @@ export * from "./types"; export * from "./tauri"; export * from "./clash"; +export * from "./core"; diff --git a/frontend/interface/service/tauri.ts b/frontend/interface/service/tauri.ts index ff1620af24..d2d1ee121e 100644 --- a/frontend/interface/service/tauri.ts +++ b/frontend/interface/service/tauri.ts @@ -1,5 +1,6 @@ import { invoke } from "@tauri-apps/api/tauri"; import { ClashConfig, ClashInfo, VergeConfig, Profile } from "./types"; +import { ManifestVersion } from "./core"; export const getNyanpasuConfig = async () => { return await invoke("get_verge_config"); @@ -35,3 +36,23 @@ export const setProfiles = async (payload: { export const setProfilesConfig = async (profiles: Profile.Config) => { return await invoke("patch_profiles_config", { profiles }); }; + +export const getCoreVersion = async ( + coreType: Required["clash_core"], +) => { + return await invoke("get_core_version", { coreType }); +}; + +export const setClashCore = async ( + clashCore: Required["clash_core"], +) => { + return await invoke("change_clash_core", { clashCore }); +}; + +export const restartSidecar = async () => { + return await invoke("restart_sidecar"); +}; + +export const fetchLatestCoreVersions = async () => { + return await invoke("fetch_latest_core_versions"); +}; diff --git a/frontend/nyanpasu/src/assets/image/core/clash-rs.png b/frontend/nyanpasu/src/assets/image/core/clash-rs.png new file mode 100644 index 0000000000..60687590cb Binary files /dev/null and b/frontend/nyanpasu/src/assets/image/core/clash-rs.png differ diff --git a/frontend/nyanpasu/src/assets/image/core/clash.meta.png b/frontend/nyanpasu/src/assets/image/core/clash.meta.png new file mode 100644 index 0000000000..1f6323dad3 Binary files /dev/null and b/frontend/nyanpasu/src/assets/image/core/clash.meta.png differ diff --git a/frontend/nyanpasu/src/assets/image/core/clash.png b/frontend/nyanpasu/src/assets/image/core/clash.png new file mode 100644 index 0000000000..c624b94d23 Binary files /dev/null and b/frontend/nyanpasu/src/assets/image/core/clash.png differ diff --git a/frontend/nyanpasu/src/components/setting/modules/clash-core.tsx b/frontend/nyanpasu/src/components/setting/modules/clash-core.tsx new file mode 100644 index 0000000000..165477fb5a --- /dev/null +++ b/frontend/nyanpasu/src/components/setting/modules/clash-core.tsx @@ -0,0 +1,100 @@ +import ListItem from "@mui/material/ListItem"; +import ListItemButton from "@mui/material/ListItemButton"; +import { Item } from "./clash-web"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { alpha, useTheme } from "@mui/material/styles"; +import { ClashCore, Core } from "@nyanpasu/interface"; +import Clash from "@/assets/image/core/clash.png"; +import ClashMeta from "@/assets/image/core/clash.meta.png"; +import ClashRs from "@/assets/image/core/clash-rs.png"; +import { FiberManualRecord } from "@mui/icons-material"; + +export const getImage = (core: ClashCore) => { + switch (core) { + case "mihomo": + case "mihomo-alpha": { + return ClashMeta; + } + + case "clash-rs": { + return ClashRs; + } + + default: { + return Clash; + } + } +}; + +export interface ClashCoreItemProps { + selected: boolean; + data: Core; + onClick: (core: ClashCore) => void; +} + +/** + * @example + * changeClashCore(item.core)} + /> + * + * `Design for Clash Core used.` + * + * @author keiko233 + * @copyright LibNyanpasu org. 2024 + */ +export const ClashCoreItem = ({ + selected, + data, + onClick, +}: ClashCoreItemProps) => { + const { palette } = useTheme(); + + const newVersion = data.latest ? data.latest !== data.version : false; + + return ( + + onClick(data.core)} + > + + + + + + + {data.name} + + {newVersion && ( + + )} + + + {data.version} + + {newVersion && ( + + New Version: {data.latest} + + )} + + + + + + ); +}; diff --git a/frontend/nyanpasu/src/components/setting/setting-clash-core.tsx b/frontend/nyanpasu/src/components/setting/setting-clash-core.tsx new file mode 100644 index 0000000000..292b04ad93 --- /dev/null +++ b/frontend/nyanpasu/src/components/setting/setting-clash-core.tsx @@ -0,0 +1,182 @@ +import { BaseCard, ExpandMore } from "@nyanpasu/ui"; +import { useTranslation } from "react-i18next"; +import { useMemo, useState } from "react"; +import { Box, List, ListItem, Tooltip } from "@mui/material"; +import { ClashCore, useClash, useNyanpasu } from "@nyanpasu/interface"; +import { useLockFn, useReactive } from "ahooks"; +import { useMessage } from "@/hooks/use-notification"; +import LoadingButton from "@mui/lab/LoadingButton"; +import { motion } from "framer-motion"; +import { ClashCoreItem } from "./modules/clash-core"; + +export const SettingClashCore = () => { + const { t } = useTranslation(); + + const loading = useReactive({ + mask: false, + restart: false, + }); + + const [expand, setExpand] = useState(false); + + const { + nyanpasuConfig, + setClashCore, + getClashCore, + restartSidecar, + getLatestCore, + } = useNyanpasu(); + + const { getVersion, deleteConnections } = useClash(); + + const version = useMemo(() => { + const data = getVersion.data; + + return data?.premium + ? `${data.version} Premium` + : data?.meta + ? `${data.version} Meta` + : data?.version || "-"; + }, [getVersion.data, nyanpasuConfig]); + + const changeClashCore = useLockFn(async (core: ClashCore) => { + try { + loading.mask = true; + + await deleteConnections(); + + await setClashCore(core); + + useMessage(`Successfully switch to ${core}`, { + type: "info", + title: t("Success"), + }); + } catch (e) { + useMessage( + "Switching failed, please check log and modify your profile file.", + { + type: "error", + title: t("Error"), + }, + ); + } finally { + loading.mask = false; + } + }); + + const handleRestart = useLockFn(async () => { + try { + loading.restart = true; + + await restartSidecar(); + + useMessage(t("Successfully restart core"), { + type: "info", + title: t("Success"), + }); + } catch (e) { + useMessage("Restart failed, please check log.", { + type: "error", + title: t("Error"), + }); + } finally { + loading.restart = false; + } + }); + + const handleCheckUpdates = useLockFn(async () => { + try { + await getLatestCore.mutate(); + } catch (e) { + useMessage("Fetch failed, please check your internet connection.", { + type: "error", + title: t("Error"), + }); + } + }); + + const mergeCores = useMemo(() => { + return getClashCore.data?.map((item) => { + const latest = getLatestCore.data?.find( + (i) => i.core == item.core, + )?.latest; + + return { + ...item, + latest, + }; + }); + }, [getClashCore.data, getLatestCore.data]); + + return ( + {version}} + > + + {mergeCores?.map((item, index) => { + const show = expand || item.core == nyanpasuConfig?.clash_core; + + return ( + + changeClashCore(item.core)} + /> + + ); + })} + + + + + {t("Restart")} + + + + {t("Check Updates")} + + + + + setExpand(!expand)} /> + + + + + ); +}; + +export default SettingClashCore;