From 8bc14d74065e05f7a95cfdd66c8d14cca0a345b3 Mon Sep 17 00:00:00 2001 From: 233 <85852251+wfion@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:54:19 +0800 Subject: [PATCH] feat(about): add support information --- src-tauri/Cargo.toml | 2 + src-tauri/src/cmd/app.rs | 124 ++++++++++++++++++++++ src-tauri/src/lib.rs | 1 + src/components/dialog/app/AboutDialog.tsx | 67 +++++++++++- src/i18n/locales/en.json | 12 +++ src/i18n/locales/ko.json | 12 +++ src/i18n/locales/zh-CN.json | 12 +++ src/i18n/locales/zh-TW.json | 12 +++ src/types/global.d.ts | 6 ++ 9 files changed, 247 insertions(+), 1 deletion(-) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 8275f188..2295d1d0 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -193,9 +193,11 @@ windows-sys = { version = "0.61", features = [ "Win32_System_Threading", ] } windows = { version = "0.61", features = [ + "Wdk_System_SystemServices", "Win32_Foundation", "Win32_Graphics_Gdi", "Win32_System_Threading", + "Win32_System_SystemInformation", "Win32_System_DataExchange", "Win32_System_Memory", "Win32_UI_Shell", diff --git a/src-tauri/src/cmd/app.rs b/src-tauri/src/cmd/app.rs index 6893d4b8..7a6a24bb 100644 --- a/src-tauri/src/cmd/app.rs +++ b/src-tauri/src/cmd/app.rs @@ -103,6 +103,130 @@ pub fn get_app_runtime_info( state.info() } +#[derive(Clone, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AppSupportInfo { + os: String, + architecture: String, + runtime: String, +} + +#[tauri::command] +pub fn get_support_info(state: tauri::State<'_, crate::runtime::AppRuntime>) -> AppSupportInfo { + AppSupportInfo { + os: operating_system_label(), + architecture: std::env::consts::ARCH.to_string(), + runtime: support_runtime_label(state.portable()).to_string(), + } +} + +fn support_runtime_label(portable: bool) -> &'static str { + if portable { "portable" } else { "installed" } +} + +fn operating_system_label() -> String { + #[cfg(target_os = "windows")] + { + return windows_version_label(); + } + + #[cfg(target_os = "macos")] + { + return command_output("sw_vers", &["-productName", "-productVersion"]) + .unwrap_or_else(|| "macOS".to_string()); + } + + #[cfg(target_os = "linux")] + { + if let Ok(contents) = std::fs::read_to_string("/etc/os-release") { + let name = contents + .lines() + .find_map(|line| line.strip_prefix("PRETTY_NAME=")) + .map(|value| value.trim_matches('"').to_string()); + if let Some(name) = name { + return name; + } + } + return "Linux".to_string(); + } + + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] + { + std::env::consts::OS.to_string() + } +} + +#[cfg(target_os = "windows")] +fn windows_version_label() -> String { + use windows::Wdk::System::SystemServices::RtlGetVersion; + use windows::Win32::System::SystemInformation::OSVERSIONINFOW; + + let mut version = OSVERSIONINFOW { + dwOSVersionInfoSize: std::mem::size_of::() as u32, + ..Default::default() + }; + + // RtlGetVersion queries the current Windows version in-process, avoiding a console process. + if unsafe { RtlGetVersion(&mut version) }.is_ok() { + windows_version_label_from_parts( + version.dwMajorVersion, + version.dwMinorVersion, + version.dwBuildNumber, + ) + } else { + "Windows".to_string() + } +} + +#[cfg(any(target_os = "windows", test))] +fn windows_version_label_from_parts(major: u32, minor: u32, build: u32) -> String { + let name = match (major, minor, build) { + (10, 0, 22_000..) => "Windows 11", + (10, 0, _) => "Windows 10", + (6, 3, _) => "Windows 8.1", + (6, 2, _) => "Windows 8", + (6, 1, _) => "Windows 7", + _ => "Windows", + }; + format!("{name} ({major}.{minor}.{build})") +} + +#[cfg(target_os = "macos")] +fn command_output(program: &str, args: &[&str]) -> Option { + let output = std::process::Command::new(program) + .args(args) + .output() + .ok()?; + if !output.status.success() { + return None; + } + let value = String::from_utf8(output.stdout).ok()?.trim().to_string(); + (!value.is_empty()).then_some(value) +} + +#[cfg(test)] +mod tests { + use super::{support_runtime_label, windows_version_label_from_parts}; + + #[test] + fn support_runtime_label_matches_installation_mode() { + assert_eq!(support_runtime_label(true), "portable"); + assert_eq!(support_runtime_label(false), "installed"); + } + + #[test] + fn windows_version_label_uses_native_version_parts() { + assert_eq!( + windows_version_label_from_parts(10, 0, 22_631), + "Windows 11 (10.0.22631)" + ); + assert_eq!( + windows_version_label_from_parts(10, 0, 19_045), + "Windows 10 (10.0.19045)" + ); + } +} + #[tauri::command] pub fn get_app_lock_state(state: tauri::State<'_, AppLockState>) -> bool { state.is_locked() diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index fb6c2d53..012383bd 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -115,6 +115,7 @@ pub fn run() { cmd::app::open_download_dir, cmd::app::open_log_dir, cmd::app::get_app_runtime_info, + cmd::app::get_support_info, cmd::app::get_app_lock_state, cmd::app::set_app_lock_state, cmd::app::open_child_window, diff --git a/src/components/dialog/app/AboutDialog.tsx b/src/components/dialog/app/AboutDialog.tsx index 26186570..2c751b9b 100644 --- a/src/components/dialog/app/AboutDialog.tsx +++ b/src/components/dialog/app/AboutDialog.tsx @@ -1,7 +1,9 @@ import { getName, getVersion } from "@tauri-apps/api/app"; import { openUrl } from "@tauri-apps/plugin-opener"; +import { Check, Copy } from "lucide-react"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -12,6 +14,9 @@ import { } from "@/components/ui/dialog"; import pkg from "../../../../package.json"; import NyaTermLogo from "../../NyaTermLogo"; +import { writeClipboardText } from "@/lib/clipboard"; +import { invoke } from "@/lib/invoke"; +import type { AppSupportInfo } from "@/types/global"; interface AboutDialogProps { open: boolean; @@ -22,17 +27,52 @@ export default function AboutDialog({ open, onClose }: AboutDialogProps) { const { t } = useTranslation(); const [appName, setAppName] = useState("NyaTerm"); const [appVersion, setAppVersion] = useState("0.1.0"); + const [supportInfo, setSupportInfo] = useState({ + os: "unknown", + architecture: "unknown", + runtime: "installed", + }); + const [copied, setCopied] = useState(false); useEffect(() => { if (open) { getName().then(setAppName).catch(console.error); getVersion().then(setAppVersion).catch(console.error); + invoke("get_support_info") + .then(setSupportInfo) + .catch(() => undefined); + setCopied(false); } }, [open]); + const copySupportInfo = async () => { + const text = [ + "NyaTerm Support Information", + `Version: ${appVersion}`, + `Operating System: ${supportInfo.os}`, + `Architecture: ${supportInfo.architecture}`, + `Runtime: ${supportInfo.runtime === "portable" ? t("about.portable") : t("about.installed")}`, + ].join("\n"); + try { + await writeClipboardText(text); + setCopied(true); + toast.success(t("about.supportInfoCopied")); + window.setTimeout(() => setCopied(false), 1800); + } catch { + toast.error(t("about.supportInfoCopyFailed")); + } + }; + + const supportRows = [ + [t("about.version"), appVersion], + [t("about.operatingSystem"), supportInfo.os === "unknown" ? t("about.unknown") : supportInfo.os], + [t("about.architecture"), supportInfo.architecture === "unknown" ? t("about.unknown") : supportInfo.architecture], + [t("about.runtime"), supportInfo.runtime === "portable" ? t("about.portable") : t("about.installed")], + ]; + return ( !v && onClose()}> - + {appName} @@ -43,6 +83,31 @@ export default function AboutDialog({ open, onClose }: AboutDialogProps) { {t("about.description")}

+
+
+

{t("about.supportInfo")}

+ +
+
+ {supportRows.map(([label, value]) => ( +
+
{label}
+
{value}
+
+ ))} +
+
+