mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-10 01:42:44 +08:00
refactor: rebuild desktop app with Tauri
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
.shell {
|
||||
--app-edge: 8px;
|
||||
--app-menu-width: 204px;
|
||||
--window-safe-top: var(--window-header-height);
|
||||
--window-menu-top: var(--window-safe-top);
|
||||
--window-corner-safe-bottom: 0px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--vscode-editor-background);
|
||||
}
|
||||
|
||||
.windows {
|
||||
--window-corner-safe-bottom: 6px;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import styles from "./AppFrame.module.scss";
|
||||
import { AppHeader } from "./AppHeader";
|
||||
|
||||
const currentPlatform = desktopPlatform();
|
||||
document.documentElement.dataset.platform = currentPlatform;
|
||||
|
||||
export function AppFrame() {
|
||||
const platform = currentPlatform;
|
||||
const platformClasses = platform === "windows" ? [styles.windows] : [];
|
||||
const nativeDesktop = "__TAURI_INTERNALS__" in window;
|
||||
|
||||
return (
|
||||
<div className={[styles.shell, ...platformClasses].filter(Boolean).join(" ")}>
|
||||
<AppHeader platform={platform} nativeDesktop={nativeDesktop} />
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type DesktopPlatform = "macos" | "windows" | "linux";
|
||||
|
||||
function desktopPlatform(): DesktopPlatform {
|
||||
// return "windows";
|
||||
const agent = navigator.userAgent;
|
||||
if (/Macintosh|Mac OS X/.test(agent)) return "macos";
|
||||
if (/Windows/.test(agent)) return "windows";
|
||||
return "linux";
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
@use "../styles/typography" as type;
|
||||
|
||||
.root {
|
||||
position: fixed;
|
||||
z-index: 12000;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
height: var(--window-header-height);
|
||||
background: transparent;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.dragLayer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.uiLayer {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 8px;
|
||||
padding-right: 0px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.identity {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
color: var(--vscode-foreground);
|
||||
font-size: type.$font-size-base;
|
||||
font-weight: 500;
|
||||
pointer-events: none;
|
||||
|
||||
img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import appIcon from "../../src-tauri/icons/32x32.png";
|
||||
import type { DesktopPlatform } from "./AppFrame";
|
||||
import { WindowControls } from "./WindowControls";
|
||||
import styles from "./AppHeader.module.scss";
|
||||
|
||||
type AppHeaderProps = {
|
||||
platform: DesktopPlatform;
|
||||
nativeDesktop: boolean;
|
||||
};
|
||||
|
||||
export function AppHeader({ platform, nativeDesktop }: AppHeaderProps) {
|
||||
const showNativeUi = nativeDesktop && platform !== "macos";
|
||||
|
||||
return <header className={styles.root}>
|
||||
<div className={styles.dragLayer} data-tauri-drag-region aria-hidden="true" />
|
||||
<div className={styles.uiLayer}>
|
||||
{showNativeUi && <>
|
||||
<div className={styles.identity} aria-label="Cursor BYOK">
|
||||
<img src={appIcon} alt="" />
|
||||
<span>Cursor 助手 v0.1.0</span>
|
||||
</div>
|
||||
<WindowControls />
|
||||
</>}
|
||||
</div>
|
||||
</header>;
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
@use "../styles/typography" as type;
|
||||
|
||||
.root {
|
||||
--app-region-gap: 16px;
|
||||
--app-toolbar-height: 36px;
|
||||
--app-page-max-width: 1280px;
|
||||
--app-page-padding: 18px;
|
||||
--app-content-left: calc(var(--app-edge) + var(--app-menu-width) );
|
||||
--app-content-top: calc(var(--window-safe-top) + var(--app-toolbar-height) + 10px );
|
||||
background: var(--vscode-editor-background);
|
||||
}
|
||||
|
||||
.menuCard {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
top: calc(var(--window-menu-top));
|
||||
bottom: var(--app-edge);
|
||||
left: var(--app-edge);
|
||||
width: var(--app-menu-width);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 6px;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.navigationList {
|
||||
--scroll-shadow-bottom-size: 28px;
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.navigationGroup {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: color-mix(in srgb, var(--vscode-descriptionForeground) 65%, transparent);
|
||||
font-size: type.$font-size-xs;
|
||||
font-weight: normal;
|
||||
padding: 5px 9px 2px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.navigationRow {
|
||||
flex: 0 0 36px;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
a {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 0 11px;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
|
||||
&:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
&[aria-current="page"] {
|
||||
color: var(--vscode-list-activeSelectionForeground);
|
||||
background: var(--vscode-list-activeSelectionBackground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dismissReason {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-top: 14px;
|
||||
font-size: type.$font-size-base;
|
||||
|
||||
textarea {
|
||||
min-height: 78px;
|
||||
padding: 8px 10px;
|
||||
resize: vertical;
|
||||
color: var(--vscode-input-foreground);
|
||||
background: var(--vscode-input-background);
|
||||
border: 1px solid var(--vscode-input-border, var(--vscode-sideBar-border));
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
|
||||
&:focus {
|
||||
border-color: var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
width: calc(100% - var(--app-content-left));
|
||||
height: 100%;
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
margin-left: var(--app-content-left);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.keepAliveContainer,
|
||||
.keepAlivePage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.actionRegion {
|
||||
position: absolute;
|
||||
z-index: 12500;
|
||||
top: var(--window-safe-top);
|
||||
right: max(var(--app-page-padding), calc((100% - var(--app-page-max-width)) / 2));
|
||||
height: calc(var(--app-content-top) - var(--window-safe-top));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-drag: none;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.actions {
|
||||
height: var(--app-toolbar-height);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 2px 2px;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.pageActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { IconifyIcon } from "@iconify/react/offline";
|
||||
import KeepAliveRouteOutlet from "keepalive-for-react-router";
|
||||
import { NavLink, useLocation } from "react-router-dom";
|
||||
import cursorIconUrl from "../assets/icons/cursor.svg";
|
||||
import { api } from "../api";
|
||||
import { AdMenu } from "../components/ads/AdMenu";
|
||||
import { FloatingAd } from "../components/ads/FloatingAd";
|
||||
import { AdActionType, type AdAction, type AdSlot } from "../components/ads/types";
|
||||
import { PageLayout } from "../components/layout/PageLayout";
|
||||
import { Card } from "../components/ui/Card";
|
||||
import controls from "../components/ui/Controls.module.scss";
|
||||
import { Icon } from "../components/ui/Icon";
|
||||
import { Modal } from "../components/ui/Modal";
|
||||
import { TooltipTrigger } from "../components/ui/TooltipTrigger";
|
||||
import { flatColorAreaChartIcon, flatColorOrganizationIcon, flatColorSalesPerformanceIcon, flatColorSettingsIcon, refreshIcon } from "../components/ui/icons";
|
||||
import { useMessage } from "../components/ui/message";
|
||||
import { VirtualList } from "../components/virtual/VirtualList";
|
||||
import { appStore, useAppStore } from "../store/appStore";
|
||||
import styles from "./AppLayout.module.scss";
|
||||
import { PageActionsTarget } from "./PageActions";
|
||||
|
||||
type MenuItem =
|
||||
| { kind: "page"; path: string; label: string; icon: IconifyIcon | string }
|
||||
| { kind: "group"; label: string };
|
||||
|
||||
const menuItems: MenuItem[] = [
|
||||
{ kind: "page", path: "/", label: "数据概览", icon: flatColorAreaChartIcon },
|
||||
{
|
||||
kind: "page",
|
||||
path: "/calls",
|
||||
label: "调用详细",
|
||||
icon: flatColorSalesPerformanceIcon,
|
||||
},
|
||||
{
|
||||
kind: "page",
|
||||
path: "/providers",
|
||||
label: "上游配置",
|
||||
icon: flatColorOrganizationIcon,
|
||||
},
|
||||
{ kind: "group", label: "Harness" },
|
||||
|
||||
{
|
||||
kind: "page",
|
||||
path: "/settings",
|
||||
label: "系统设置",
|
||||
icon: flatColorSettingsIcon,
|
||||
},
|
||||
{
|
||||
kind: "page",
|
||||
path: "/harness/cursor",
|
||||
label: "Cursor 设置",
|
||||
icon: cursorIconUrl,
|
||||
},
|
||||
];
|
||||
|
||||
const keptAlivePages = ["/", "/calls", "/providers", "/settings", "/harness/cursor"];
|
||||
const readAdStorageKey = "cursor-byok:read-ad-ids";
|
||||
const dismissedAdStorageKey = "cursor-byok:dismissed-ad-ids";
|
||||
|
||||
function loadStoredAdIds(key: string): Set<string> {
|
||||
try {
|
||||
const value: unknown = JSON.parse(localStorage.getItem(key) ?? "[]");
|
||||
return new Set(Array.isArray(value) ? value.filter((id): id is string => typeof id === "string") : []);
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
export function AppLayout() {
|
||||
const { busy } = useAppStore();
|
||||
const message = useMessage();
|
||||
const location = useLocation();
|
||||
const [actionTarget, setActionTarget] = useState<HTMLDivElement | null>(null);
|
||||
const [ads, setAds] = useState<AdSlot[]>([]);
|
||||
const [activeAd, setActiveAd] = useState<AdSlot | null>(null);
|
||||
const [dismissCandidate, setDismissCandidate] = useState<AdSlot | null>(null);
|
||||
const [dismissReason, setDismissReason] = useState("");
|
||||
const [readAdIds, setReadAdIds] = useState(() => loadStoredAdIds(readAdStorageKey));
|
||||
const [dismissedAdIds, setDismissedAdIds] = useState(() => loadStoredAdIds(dismissedAdStorageKey));
|
||||
const dismissedAdIdsRef = useRef(dismissedAdIds);
|
||||
dismissedAdIdsRef.current = dismissedAdIds;
|
||||
const adTriggers = useRef(new Map<string, HTMLButtonElement>());
|
||||
const closeAd = useCallback(() => setActiveAd(null), []);
|
||||
const visibleAds = ads.filter((ad) => ad.enabled && ad.placement === "menu" && !dismissedAdIds.has(ad.id));
|
||||
|
||||
useEffect(() => {
|
||||
let disposed = false;
|
||||
let pending = false;
|
||||
let lastRequestedAt = 0;
|
||||
const refreshAds = () => {
|
||||
const now = Date.now();
|
||||
if (pending || now - lastRequestedAt < 10_000) return;
|
||||
pending = true;
|
||||
lastRequestedAt = now;
|
||||
void api.ads(dismissedAdIdsRef.current)
|
||||
.then((runtime) => {
|
||||
if (disposed) return;
|
||||
setAds(runtime.slots);
|
||||
setActiveAd((current) => current
|
||||
? runtime.slots.find((ad) => ad.id === current.id) ?? null
|
||||
: null);
|
||||
})
|
||||
.catch(() => {
|
||||
if (disposed) return;
|
||||
setAds([]);
|
||||
setActiveAd(null);
|
||||
})
|
||||
.finally(() => { pending = false; });
|
||||
};
|
||||
const refreshVisibleAds = () => {
|
||||
if (document.visibilityState === "visible") refreshAds();
|
||||
};
|
||||
|
||||
refreshAds();
|
||||
window.addEventListener("focus", refreshAds);
|
||||
document.addEventListener("visibilitychange", refreshVisibleAds);
|
||||
return () => {
|
||||
disposed = true;
|
||||
window.removeEventListener("focus", refreshAds);
|
||||
document.removeEventListener("visibilitychange", refreshVisibleAds);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const openAd = useCallback((ad: AdSlot) => {
|
||||
setReadAdIds((current) => {
|
||||
if (current.has(ad.id)) return current;
|
||||
const next = new Set(current).add(ad.id);
|
||||
try {
|
||||
localStorage.setItem(readAdStorageKey, JSON.stringify([...next]));
|
||||
} catch {
|
||||
// Read state remains valid for the current session when storage is unavailable.
|
||||
}
|
||||
return next;
|
||||
});
|
||||
setActiveAd((current) => current?.id === ad.id ? null : ad);
|
||||
}, []);
|
||||
|
||||
const performAdAction = useCallback((action: AdAction) => {
|
||||
if (action.type !== AdActionType.OpenBrowser) return;
|
||||
void api.openExternalUrl(action.url)
|
||||
.catch((cause) => message(cause instanceof Error ? cause.message : String(cause)));
|
||||
}, [message]);
|
||||
|
||||
const openDismissAd = useCallback((ad: AdSlot) => {
|
||||
setDismissReason("");
|
||||
setDismissCandidate(ad);
|
||||
}, []);
|
||||
|
||||
const closeDismissAd = useCallback(() => {
|
||||
setDismissCandidate(null);
|
||||
setDismissReason("");
|
||||
}, []);
|
||||
|
||||
const dismissAd = useCallback(() => {
|
||||
if (!dismissCandidate) return;
|
||||
const dismissedId = dismissCandidate.id;
|
||||
const reason = dismissReason.trim();
|
||||
setDismissedAdIds((current) => {
|
||||
const next = new Set(current).add(dismissedId);
|
||||
try {
|
||||
localStorage.setItem(dismissedAdStorageKey, JSON.stringify([...next]));
|
||||
} catch {
|
||||
// Dismissed state remains valid for the current session when storage is unavailable.
|
||||
}
|
||||
return next;
|
||||
});
|
||||
setActiveAd((current) => current?.id === dismissedId ? null : current);
|
||||
setDismissCandidate(null);
|
||||
setDismissReason("");
|
||||
void api.dismissAd(dismissedId, reason).catch(() => {
|
||||
// Dismissal is intentionally fire-and-forget so reporting never blocks the UI.
|
||||
});
|
||||
}, [dismissCandidate, dismissReason]);
|
||||
|
||||
return <PageLayout className={styles.root}>
|
||||
<Card as="aside" className={styles.menuCard}>
|
||||
<nav className={styles.navigation} aria-label={t("主菜单")}>
|
||||
<VirtualList
|
||||
items={menuItems}
|
||||
itemKey={(item) => item.kind === "group" ? `group-${item.label}` : item.path}
|
||||
estimatedItemHeight={36}
|
||||
itemGap={3}
|
||||
className={`${styles.navigationList} scroll-shadow-bottom`}
|
||||
>
|
||||
{(item) => item.kind === "group"
|
||||
? <div className={styles.navigationGroup} key={`group-${item.label}`}>{item.label}</div>
|
||||
: <div className={styles.navigationRow} key={item.path}>
|
||||
<NavLink to={item.path} end={item.path === "/"}>
|
||||
{typeof item.icon === "string"
|
||||
? <Icon src={item.icon} size="1.3em" />
|
||||
: <Icon icon={item.icon} size="1.3em" />}
|
||||
<span>{item.label}</span>
|
||||
</NavLink>
|
||||
</div>}
|
||||
</VirtualList>
|
||||
<AdMenu ads={visibleAds} activeAdId={activeAd?.id} dismissingAdId={dismissCandidate?.id} readAdIds={readAdIds} triggerRefs={adTriggers} onOpen={openAd} onDismiss={openDismissAd} />
|
||||
</nav>
|
||||
</Card>
|
||||
{activeAd && <FloatingAd ad={activeAd} trigger={adTriggers.current.get(activeAd.id) ?? null} onClose={closeAd} onAction={performAdAction} />}
|
||||
<Modal
|
||||
id="dismiss-ad-dialog"
|
||||
open={dismissCandidate !== null}
|
||||
title={t("不再显示此广告")}
|
||||
closeLabel={t("取消")}
|
||||
submitLabel={t("确认")}
|
||||
onClose={closeDismissAd}
|
||||
onSubmit={() => void dismissAd()}
|
||||
>
|
||||
<p>{t("你确认不想再看到此广告吗?")}</p>
|
||||
<label className={styles.dismissReason}>
|
||||
<span>{t("原因(可选)")}</span>
|
||||
<textarea
|
||||
value={dismissReason}
|
||||
maxLength={2000}
|
||||
rows={4}
|
||||
placeholder={t("可以告诉我们原因")}
|
||||
onChange={(event) => setDismissReason(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
</Modal>
|
||||
<main className={styles.content}>
|
||||
<div className={styles.actionRegion}>
|
||||
<Card className={styles.actions}>
|
||||
{location.pathname !== "/" && <TooltipTrigger label={t("刷新")}><button className={controls.iconButton} aria-label={t("刷新")} disabled={busy} onClick={() => void appStore.refresh()}>
|
||||
<Icon className={busy ? controls.spin : ""} icon={refreshIcon} size="1.1em" />
|
||||
</button></TooltipTrigger>}
|
||||
<div ref={setActionTarget} className={styles.pageActions} />
|
||||
</Card>
|
||||
</div>
|
||||
<PageActionsTarget.Provider value={actionTarget}>
|
||||
<KeepAliveRouteOutlet
|
||||
activeCacheKey={location.pathname}
|
||||
include={keptAlivePages}
|
||||
max={keptAlivePages.length}
|
||||
enableActivity
|
||||
containerClassName={styles.keepAliveContainer}
|
||||
cacheNodeClassName={styles.keepAlivePage}
|
||||
/>
|
||||
</PageActionsTarget.Provider>
|
||||
</main>
|
||||
</PageLayout>;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createContext, useContext, type ReactNode } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useKeepAliveContext } from "keepalive-for-react";
|
||||
|
||||
export const PageActionsTarget = createContext<HTMLElement | null>(null);
|
||||
|
||||
export function PageActions({ children }: { children: ReactNode }) {
|
||||
const target = useContext(PageActionsTarget);
|
||||
const { active } = useKeepAliveContext();
|
||||
return active && target ? createPortal(children, target) : null;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
.root {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
pointer-events: auto;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 0px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
color: var(--vscode-foreground);
|
||||
background: var(--vscode-toolbar-hoverBackground, var(--vscode-list-hoverBackground));
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 1px solid var(--vscode-focusBorder);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Icon } from "../components/ui/Icon";
|
||||
import {
|
||||
windowCloseIcon,
|
||||
windowMaximizeIcon,
|
||||
windowMinimizeIcon,
|
||||
windowRestoreIcon,
|
||||
} from "../components/ui/icons";
|
||||
import styles from "./WindowControls.module.scss";
|
||||
|
||||
export function WindowControls() {
|
||||
const [maximized, setMaximized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const appWindow = getCurrentWindow();
|
||||
let disposed = false;
|
||||
let unlisten: (() => void) | undefined;
|
||||
|
||||
const updateMaximized = async () => {
|
||||
const next = await appWindow.isMaximized();
|
||||
if (!disposed) setMaximized(next);
|
||||
};
|
||||
|
||||
void updateMaximized();
|
||||
void appWindow.onResized(() => void updateMaximized()).then((stop) => {
|
||||
if (disposed) stop();
|
||||
else unlisten = stop;
|
||||
});
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
unlisten?.();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const appWindow = getCurrentWindow();
|
||||
const maximizeLabel = maximized ? t("还原窗口") : t("最大化窗口");
|
||||
|
||||
return <div className={styles.root}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.button}
|
||||
aria-label={t("最小化窗口")}
|
||||
title={t("最小化窗口")}
|
||||
onClick={() => void appWindow.minimize()}
|
||||
>
|
||||
<Icon icon={windowMinimizeIcon} size="1.1em" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.button}
|
||||
aria-label={maximizeLabel}
|
||||
title={maximizeLabel}
|
||||
onClick={() => void appWindow.toggleMaximize()}
|
||||
>
|
||||
<Icon icon={maximized ? windowRestoreIcon : windowMaximizeIcon} size="1.1em" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={[styles.button, styles.close].join(" ")}
|
||||
aria-label={t("关闭窗口")}
|
||||
title={t("关闭窗口")}
|
||||
onClick={() => void appWindow.close()}
|
||||
>
|
||||
<Icon icon={windowCloseIcon} size="1.1em" />
|
||||
</button>
|
||||
</div>;
|
||||
}
|
||||
Reference in New Issue
Block a user