feat(about): add support information
This commit is contained in:
1 parent
f21b05d996
commit
8bc14d7406
9 files changed
+247
-1
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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::<OSVERSIONINFOW>() 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<String> {
|
||||
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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<AppSupportInfo>({
|
||||
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<AppSupportInfo>("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 (
|
||||
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
||||
<DialogContent className="w-[min(320px,calc(100vw-2rem))] sm:max-w-[320px] flex flex-col items-center p-6 gap-4">
|
||||
<DialogContent className="w-[min(440px,calc(100vw-2rem))] sm:max-w-[440px] flex flex-col items-center p-6 gap-4">
|
||||
<DialogHeader className="items-center">
|
||||
<NyaTermLogo className="w-24 h-24 object-contain" />
|
||||
<DialogTitle className="text-lg">{appName}</DialogTitle>
|
||||
@@ -43,6 +83,31 @@ export default function AboutDialog({ open, onClose }: AboutDialogProps) {
|
||||
{t("about.description")}
|
||||
</p>
|
||||
|
||||
<section className="w-full rounded-md border bg-muted/20 p-3" aria-labelledby="support-info-title">
|
||||
<div className="flex items-center justify-between gap-3 mb-2">
|
||||
<h3 id="support-info-title" className="text-sm font-medium">{t("about.supportInfo")}</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 gap-1.5 px-2 text-xs"
|
||||
onClick={() => void copySupportInfo()}
|
||||
title={t("about.copySupportInfo")}
|
||||
aria-label={t("about.copySupportInfo")}
|
||||
>
|
||||
{copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
|
||||
{copied ? t("about.supportInfoCopiedShort") : t("about.copySupportInfo")}
|
||||
</Button>
|
||||
</div>
|
||||
<dl className="grid grid-cols-[minmax(0,auto)_1fr] gap-x-4 gap-y-1.5 text-xs">
|
||||
{supportRows.map(([label, value]) => (
|
||||
<div className="contents" key={label}>
|
||||
<dt className="text-muted-foreground">{label}</dt>
|
||||
<dd className="min-w-0 break-words text-right font-mono">{value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<div className="flex gap-3 w-full pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
{
|
||||
"about": {
|
||||
"architecture": "Architecture",
|
||||
"close": "Close",
|
||||
"copySupportInfo": "Copy all",
|
||||
"description": "A desktop client for SSH-centric operations and mixed terminal workflows.",
|
||||
"installed": "Installed",
|
||||
"issues": "Issues",
|
||||
"operatingSystem": "Operating system",
|
||||
"portable": "Portable",
|
||||
"runtime": "Runtime",
|
||||
"supportInfo": "Support information",
|
||||
"supportInfoCopied": "Support information copied",
|
||||
"supportInfoCopiedShort": "Copied",
|
||||
"supportInfoCopyFailed": "Failed to copy support information",
|
||||
"unknown": "Unknown",
|
||||
"version": "Version",
|
||||
"website": "Website"
|
||||
},
|
||||
"activeSessions": {
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
{
|
||||
"about": {
|
||||
"architecture": "시스템 아키텍처",
|
||||
"close": "닫기",
|
||||
"copySupportInfo": "전체 복사",
|
||||
"description": "SSH 중심 작업과 다양한 터미널 워크플로를 위한 데스크톱 클라이언트입니다.",
|
||||
"installed": "설치 버전",
|
||||
"issues": "이슈",
|
||||
"operatingSystem": "운영 체제",
|
||||
"portable": "포터블 버전",
|
||||
"runtime": "실행 환경",
|
||||
"supportInfo": "지원 정보",
|
||||
"supportInfoCopied": "지원 정보가 복사되었습니다",
|
||||
"supportInfoCopiedShort": "복사됨",
|
||||
"supportInfoCopyFailed": "지원 정보를 복사하지 못했습니다",
|
||||
"unknown": "알 수 없음",
|
||||
"version": "소프트웨어 버전",
|
||||
"website": "웹사이트"
|
||||
},
|
||||
"activeSessions": {
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
{
|
||||
"about": {
|
||||
"architecture": "系统架构",
|
||||
"close": "关闭",
|
||||
"copySupportInfo": "复制全部",
|
||||
"description": "用于 SSH、串口和本地命令行工作的桌面终端客户端。",
|
||||
"installed": "安装版",
|
||||
"issues": "问题反馈",
|
||||
"operatingSystem": "操作系统",
|
||||
"portable": "便携版",
|
||||
"runtime": "运行环境",
|
||||
"supportInfo": "支持信息",
|
||||
"supportInfoCopied": "支持信息已复制",
|
||||
"supportInfoCopiedShort": "已复制",
|
||||
"supportInfoCopyFailed": "支持信息复制失败",
|
||||
"unknown": "未知",
|
||||
"version": "软件版本",
|
||||
"website": "官网"
|
||||
},
|
||||
"activeSessions": {
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
{
|
||||
"about": {
|
||||
"architecture": "系統架構",
|
||||
"close": "關閉",
|
||||
"copySupportInfo": "複製全部",
|
||||
"description": "用於 SSH、序列埠和本機命令列工作的桌面終端客戶端。",
|
||||
"installed": "安裝版",
|
||||
"issues": "問題回報",
|
||||
"operatingSystem": "作業系統",
|
||||
"portable": "便攜版",
|
||||
"runtime": "執行環境",
|
||||
"supportInfo": "支援資訊",
|
||||
"supportInfoCopied": "支援資訊已複製",
|
||||
"supportInfoCopiedShort": "已複製",
|
||||
"supportInfoCopyFailed": "支援資訊複製失敗",
|
||||
"unknown": "未知",
|
||||
"version": "軟體版本",
|
||||
"website": "官方網站"
|
||||
},
|
||||
"activeSessions": {
|
||||
|
||||
Vendored
+6
@@ -12,6 +12,12 @@ export interface AppRuntimeInfo {
|
||||
portableMarkerPath?: string | null;
|
||||
}
|
||||
|
||||
export interface AppSupportInfo {
|
||||
os: string;
|
||||
architecture: string;
|
||||
runtime: "portable" | "installed";
|
||||
}
|
||||
|
||||
/** AI Agent command execution wrapper profile. */
|
||||
export type AIExecutionProfile = "auto" | "posix" | "powershell" | "cmd" | "send_only" | "disabled";
|
||||
|
||||
|
||||
Reference in new issue
Block a user