mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-10 16:21:22 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb54d10df6 |
No files matched your search
@@ -314,6 +314,8 @@ export const enCoreMessages: Messages = {
|
||||
'settings.globalHotkey.reset': 'Reset to default',
|
||||
'settings.globalHotkey.closeToTray': 'Close to System Tray',
|
||||
'settings.globalHotkey.closeToTrayDesc': 'When enabled, closing the window will minimize to the system tray instead of quitting.',
|
||||
'settings.globalHotkey.showTrayIcon': 'Show System Tray Icon',
|
||||
'settings.globalHotkey.showTrayIconDesc': 'When disabled, the tray icon is hidden but the app keeps running in the background.',
|
||||
'settings.globalHotkey.enabled': 'Enable Global Hotkey',
|
||||
'settings.globalHotkey.enabledDesc': 'Register system-wide keyboard shortcuts. When disabled, all global hotkeys are unregistered.',
|
||||
'settings.globalHotkey.hint': 'Global hotkey works system-wide to quickly show or hide the window (Quake-style terminal).',
|
||||
|
||||
@@ -229,6 +229,8 @@ export const esCoreMessages: Messages = {
|
||||
'settings.globalHotkey.reset': 'Restablecer al valor predeterminado',
|
||||
'settings.globalHotkey.closeToTray': 'Cerrar a la bandeja del sistema',
|
||||
'settings.globalHotkey.closeToTrayDesc': 'Cuando está activado, cerrar la ventana la minimiza a la bandeja del sistema en lugar de salir.',
|
||||
'settings.globalHotkey.showTrayIcon': 'Mostrar el icono de la bandeja del sistema',
|
||||
'settings.globalHotkey.showTrayIconDesc': 'Cuando está desactivado, el icono de la bandeja se oculta pero la aplicación sigue ejecutándose en segundo plano.',
|
||||
'settings.globalHotkey.enabled': 'Activar tecla de acceso global',
|
||||
'settings.globalHotkey.enabledDesc': 'Registra atajos de teclado a nivel de todo el sistema. Cuando está desactivado, todas las teclas de acceso global se desregistran.',
|
||||
'settings.globalHotkey.hint': 'La tecla de acceso global funciona en todo el sistema para mostrar u ocultar la ventana rápidamente (terminal estilo Quake).',
|
||||
|
||||
@@ -298,6 +298,8 @@ export const ruCoreMessages: Messages = {
|
||||
'settings.globalHotkey.reset': 'Сбросить по умолчанию',
|
||||
'settings.globalHotkey.closeToTray': 'Сворачивать в системный трей',
|
||||
'settings.globalHotkey.closeToTrayDesc': 'Если включено, при закрытии окно будет сворачиваться в системный трей вместо выхода из приложения.',
|
||||
'settings.globalHotkey.showTrayIcon': 'Показывать значок в системном трее',
|
||||
'settings.globalHotkey.showTrayIconDesc': "Если отключено, значок в трее скрывается, но приложение продолжает работать в фоне.",
|
||||
'settings.globalHotkey.enabled': 'Включить глобальную горячую клавишу',
|
||||
'settings.globalHotkey.enabledDesc': 'Регистрировать системные сочетания клавиш. Когда отключено, все глобальные горячие клавиши снимаются с регистрации.',
|
||||
'settings.globalHotkey.hint': 'Глобальная горячая клавиша работает на уровне всей системы и позволяет быстро показывать или скрывать окно (терминал в стиле Quake).',
|
||||
|
||||
@@ -318,6 +318,8 @@ export const zhCNCoreMessages: Messages = {
|
||||
'settings.globalHotkey.reset': '恢复默认',
|
||||
'settings.globalHotkey.closeToTray': '关闭时最小化到托盘',
|
||||
'settings.globalHotkey.closeToTrayDesc': '启用后,关闭窗口将最小化到系统托盘而不是退出程序。',
|
||||
'settings.globalHotkey.showTrayIcon': '显示系统托盘图标',
|
||||
'settings.globalHotkey.showTrayIconDesc': '关闭后,将隐藏系统托盘中的应用图标,应用仍会在后台正常运行。',
|
||||
'settings.globalHotkey.enabled': '启用全局快捷键',
|
||||
'settings.globalHotkey.enabledDesc': '注册系统级键盘快捷键。禁用后将取消所有全局快捷键注册。',
|
||||
'settings.globalHotkey.hint': '全局快捷键在系统范围内工作,可快速显示或隐藏窗口(下拉式终端风格)。',
|
||||
|
||||
@@ -320,6 +320,8 @@ export const zhTWCoreMessages: Messages = {
|
||||
'settings.globalHotkey.reset': '恢復預設',
|
||||
'settings.globalHotkey.closeToTray': '關閉時最小化到系統匣',
|
||||
'settings.globalHotkey.closeToTrayDesc': '啟用後,關閉視窗將最小化到系統匣而不是結束程式。',
|
||||
'settings.globalHotkey.showTrayIcon': '顯示系統匣圖示',
|
||||
'settings.globalHotkey.showTrayIconDesc': '停用後,將隱藏系統匣中的應用圖示,應用程式仍會在背景正常執行。',
|
||||
'settings.globalHotkey.enabled': '啟用全域快速鍵',
|
||||
'settings.globalHotkey.enabledDesc': '註冊系統級鍵盤快速鍵。停用後將取消所有全域快速鍵註冊。',
|
||||
'settings.globalHotkey.hint': '全域快速鍵在系統範圍內工作,可快速顯示或隱藏視窗(下拉式終端風格)。',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useRef, type MutableRefObject } from 'react';
|
||||
import {
|
||||
STORAGE_KEY_AUTO_UPDATE_ENABLED,
|
||||
STORAGE_KEY_CLOSE_TO_TRAY,
|
||||
STORAGE_KEY_SHOW_TRAY_ICON,
|
||||
STORAGE_KEY_AUTO_LAUNCH_ENABLED,
|
||||
STORAGE_KEY_GLOBAL_HOTKEY_ENABLED,
|
||||
STORAGE_KEY_TOGGLE_WINDOW_HOTKEY,
|
||||
@@ -42,6 +43,7 @@ interface UseSystemSettingsEffectsParams {
|
||||
toggleWindowHotkey: string;
|
||||
globalHotkeyEnabled: boolean;
|
||||
closeToTray: boolean;
|
||||
showTrayIcon: boolean;
|
||||
autoLaunchEnabled: boolean;
|
||||
windowOpacityRecord: WindowOpacityRecord;
|
||||
windowOpacityMutationSourceRef: MutableRefObject<WindowOpacityMutationSource>;
|
||||
@@ -62,6 +64,7 @@ export function useSystemSettingsEffects({
|
||||
toggleWindowHotkey,
|
||||
globalHotkeyEnabled,
|
||||
closeToTray,
|
||||
showTrayIcon,
|
||||
autoLaunchEnabled,
|
||||
windowOpacityRecord,
|
||||
windowOpacityMutationSourceRef,
|
||||
@@ -157,6 +160,23 @@ export function useSystemSettingsEffects({
|
||||
notifySettingsChanged(STORAGE_KEY_GLOBAL_HOTKEY_ENABLED, globalHotkeyEnabled);
|
||||
}, [enabled, globalHotkeyEnabled, notifySettingsChanged, persistMountedRef]);
|
||||
|
||||
// Persist and sync the show-tray-icon setting. The tray itself is managed
|
||||
// in the main process: hiding the icon keeps the app running in the
|
||||
// background, only the icon's visibility changes.
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
const bridge = netcattyBridge.get();
|
||||
if (bridge?.setShowTrayIcon) {
|
||||
bridge.setShowTrayIcon(showTrayIcon).catch((err) => {
|
||||
console.warn('[SystemTray] Failed to set show-tray-icon:', err);
|
||||
});
|
||||
}
|
||||
localStorageAdapter.writeString(STORAGE_KEY_SHOW_TRAY_ICON, showTrayIcon ? 'true' : 'false');
|
||||
// Skip IPC on initial mount
|
||||
if (!persistMountedRef.current) return;
|
||||
notifySettingsChanged(STORAGE_KEY_SHOW_TRAY_ICON, showTrayIcon);
|
||||
}, [enabled, showTrayIcon, notifySettingsChanged, persistMountedRef]);
|
||||
|
||||
// Persist and sync close to tray setting
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
|
||||
@@ -67,6 +67,7 @@ import {
|
||||
STORAGE_KEY_EXPLORER_CONTEXT_MENU_ENABLED,
|
||||
STORAGE_KEY_TOGGLE_WINDOW_HOTKEY,
|
||||
STORAGE_KEY_CLOSE_TO_TRAY,
|
||||
STORAGE_KEY_SHOW_TRAY_ICON,
|
||||
STORAGE_KEY_AUTO_LAUNCH_ENABLED,
|
||||
STORAGE_KEY_HTTP_NETWORK_PROXY,
|
||||
STORAGE_KEY_GLOBAL_HOTKEY_ENABLED,
|
||||
@@ -550,6 +551,12 @@ export const useSettingsState = (options: { enableSettingsSync?: boolean; enable
|
||||
if (stored === null) return true;
|
||||
return stored === 'true';
|
||||
});
|
||||
const [showTrayIcon, setShowTrayIcon] = useState<boolean>(() => {
|
||||
const stored = readStoredString(STORAGE_KEY_SHOW_TRAY_ICON);
|
||||
// Default to true so the tray icon keeps its current always-visible behavior
|
||||
if (stored === null) return true;
|
||||
return stored === 'true';
|
||||
});
|
||||
// Optimistic cache for first paint; useSystemSettingsEffects hydrates this
|
||||
// from app.getLoginItemSettings() (the real source of truth) on mount.
|
||||
const [autoLaunchEnabled, setAutoLaunchEnabled] = useState<boolean>(() => {
|
||||
@@ -1791,6 +1798,7 @@ export const useSettingsState = (options: { enableSettingsSync?: boolean; enable
|
||||
toggleWindowHotkey,
|
||||
globalHotkeyEnabled,
|
||||
closeToTray,
|
||||
showTrayIcon,
|
||||
autoLaunchEnabled,
|
||||
windowOpacityRecord,
|
||||
windowOpacityMutationSourceRef,
|
||||
@@ -2156,6 +2164,8 @@ export const useSettingsState = (options: { enableSettingsSync?: boolean; enable
|
||||
setToggleWindowHotkey,
|
||||
closeToTray,
|
||||
setCloseToTray,
|
||||
showTrayIcon,
|
||||
setShowTrayIcon,
|
||||
autoLaunchEnabled,
|
||||
setAutoLaunchEnabled,
|
||||
autoLaunchSupported,
|
||||
|
||||
@@ -677,6 +677,8 @@ const SettingsPageContent: React.FC<{ settings: SettingsState; appLock?: AppLock
|
||||
setToggleWindowHotkey={settings.setToggleWindowHotkey}
|
||||
closeToTray={settings.closeToTray}
|
||||
setCloseToTray={settings.setCloseToTray}
|
||||
showTrayIcon={settings.showTrayIcon}
|
||||
setShowTrayIcon={settings.setShowTrayIcon}
|
||||
autoLaunchEnabled={settings.autoLaunchEnabled}
|
||||
setAutoLaunchEnabled={settings.setAutoLaunchEnabled}
|
||||
autoLaunchSupported={settings.autoLaunchSupported}
|
||||
|
||||
@@ -121,6 +121,8 @@ interface SettingsSystemTabProps {
|
||||
setToggleWindowHotkey: (hotkey: string) => void;
|
||||
closeToTray: boolean;
|
||||
setCloseToTray: (enabled: boolean) => void;
|
||||
showTrayIcon: boolean;
|
||||
setShowTrayIcon: (enabled: boolean) => void;
|
||||
autoLaunchEnabled: boolean;
|
||||
setAutoLaunchEnabled: (enabled: boolean) => void;
|
||||
autoLaunchSupported: boolean;
|
||||
@@ -173,6 +175,8 @@ const SettingsSystemTab: React.FC<SettingsSystemTabProps> = ({
|
||||
setToggleWindowHotkey,
|
||||
closeToTray,
|
||||
setCloseToTray,
|
||||
showTrayIcon,
|
||||
setShowTrayIcon,
|
||||
autoLaunchEnabled,
|
||||
setAutoLaunchEnabled,
|
||||
autoLaunchSupported,
|
||||
@@ -1302,6 +1306,18 @@ const SettingsSystemTab: React.FC<SettingsSystemTabProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Show Tray Icon */}
|
||||
<SettingRow
|
||||
anchorId="system-show-tray-icon"
|
||||
label={t("settings.globalHotkey.showTrayIcon")}
|
||||
description={t("settings.globalHotkey.showTrayIconDesc")}
|
||||
>
|
||||
<Toggle
|
||||
checked={showTrayIcon}
|
||||
onChange={setShowTrayIcon}
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
{/* Close to Tray */}
|
||||
<SettingRow
|
||||
anchorId="system-close-to-tray"
|
||||
|
||||
@@ -954,6 +954,13 @@ export const SETTINGS_SEARCH_CATALOG: readonly SettingsSearchEntry[] = [
|
||||
descriptionKey: "settings.globalHotkey.toggleWindowDesc",
|
||||
sectionKey: "settings.globalHotkey.title",
|
||||
},
|
||||
{
|
||||
id: "system-show-tray-icon",
|
||||
tab: "system",
|
||||
labelKey: "settings.globalHotkey.showTrayIcon",
|
||||
descriptionKey: "settings.globalHotkey.showTrayIconDesc",
|
||||
sectionKey: "settings.globalHotkey.title",
|
||||
},
|
||||
{
|
||||
id: "system-close-to-tray",
|
||||
tab: "system",
|
||||
|
||||
@@ -19,6 +19,9 @@ let sendWhenRendererReady = null;
|
||||
let getSystemMenuMainWindow = null;
|
||||
let tray = null;
|
||||
let closeToTray = false;
|
||||
// Separate user preference: whether the tray icon itself is shown at all.
|
||||
// Defaults to true so existing behavior (a visible tray icon) is unchanged.
|
||||
let showTrayIcon = true;
|
||||
let currentHotkey = null;
|
||||
let hotkeyEnabled = false;
|
||||
// True while a hidden auto-launch cold start has no visible window yet.
|
||||
@@ -1133,50 +1136,79 @@ function destroyTray() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconcile the tray's existence with the current preferences:
|
||||
* - showTrayIcon=false always wins: the user asked for no tray icon, so the
|
||||
* tray is destroyed even if close-to-tray or the hidden-launch pin is on.
|
||||
* - Otherwise the tray exists when close-to-tray is enabled, or while a
|
||||
* hidden auto-launch cold start has pinned it.
|
||||
*/
|
||||
function syncTrayVisibility() {
|
||||
if (!showTrayIcon) {
|
||||
destroyTray();
|
||||
return;
|
||||
}
|
||||
if (closeToTray) {
|
||||
if (!tray) {
|
||||
createTray();
|
||||
}
|
||||
return;
|
||||
}
|
||||
// A hidden auto-launch cold start pins the tray regardless of this
|
||||
// preference until its window is actually shown once — otherwise a user
|
||||
// with close-to-tray off would get a windowless, trayless zombie process.
|
||||
if (!hiddenLaunchTrayPinned) {
|
||||
destroyTray();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set close-to-tray behavior
|
||||
*/
|
||||
function setCloseToTray(enabled) {
|
||||
closeToTray = !!enabled;
|
||||
|
||||
if (closeToTray) {
|
||||
// Create tray if it doesn't exist
|
||||
if (!tray) {
|
||||
createTray();
|
||||
}
|
||||
} else {
|
||||
if (!closeToTray) {
|
||||
clearPendingFullscreenHide(getMainWindow());
|
||||
// A hidden auto-launch cold start pins the tray regardless of this
|
||||
// preference until its window is actually shown once — otherwise a user
|
||||
// with close-to-tray off would get a windowless, trayless zombie process.
|
||||
if (!hiddenLaunchTrayPinned) {
|
||||
destroyTray();
|
||||
}
|
||||
}
|
||||
syncTrayVisibility();
|
||||
|
||||
return { success: true, enabled: closeToTray };
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the system tray icon is shown at all. The app keeps running
|
||||
* in the background either way; only the icon's visibility changes.
|
||||
*/
|
||||
function setShowTrayIcon(enabled) {
|
||||
showTrayIcon = !!enabled;
|
||||
syncTrayVisibility();
|
||||
return { success: true, enabled: showTrayIcon };
|
||||
}
|
||||
|
||||
/**
|
||||
* Force-create the tray for a hidden auto-launch cold start and keep it
|
||||
* alive even if close-to-tray is later disabled, until the pin is released.
|
||||
* An explicit "hide tray icon" preference still wins — the pin only exists
|
||||
* to avoid an unreachable zombie, not to override the user's choice.
|
||||
*/
|
||||
function pinTrayForHiddenLaunch() {
|
||||
hiddenLaunchTrayPinned = true;
|
||||
if (!tray) {
|
||||
if (!tray && showTrayIcon) {
|
||||
createTray();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the hidden-launch tray pin once its window has been shown. If the
|
||||
* user's close-to-tray preference is off, the tray is destroyed now instead
|
||||
* of lingering until the next close-to-tray toggle.
|
||||
* user's close-to-tray preference is off (or they opted out of the tray
|
||||
* icon entirely), the tray is destroyed now instead of lingering until the
|
||||
* next close-to-tray toggle.
|
||||
*/
|
||||
function releaseHiddenLaunchTrayPin() {
|
||||
if (!hiddenLaunchTrayPinned) return;
|
||||
hiddenLaunchTrayPinned = false;
|
||||
if (!closeToTray) {
|
||||
if (!closeToTray || !showTrayIcon) {
|
||||
destroyTray();
|
||||
}
|
||||
}
|
||||
@@ -1240,6 +1272,11 @@ function registerHandlers(ipcMain) {
|
||||
return { enabled: closeToTray };
|
||||
});
|
||||
|
||||
// Set whether the system tray icon is shown at all
|
||||
ipcMain.handle("netcatty:tray:setShowTrayIcon", async (_event, { enabled }) => {
|
||||
return setShowTrayIcon(enabled);
|
||||
});
|
||||
|
||||
// Update tray menu data
|
||||
ipcMain.handle("netcatty:tray:updateMenuData", async (_event, data) => {
|
||||
setTrayMenuData(data);
|
||||
@@ -1357,6 +1394,7 @@ module.exports = {
|
||||
pinTrayForHiddenLaunch,
|
||||
releaseHiddenLaunchTrayPin,
|
||||
getTray: () => tray,
|
||||
setShowTrayIcon,
|
||||
getTrayPanelWindow: () => trayPanelWindow,
|
||||
// Test helpers
|
||||
__flushPendingPortForwardTogglesForTests: flushPendingPortForwardToggles,
|
||||
|
||||
@@ -1771,6 +1771,55 @@ test("setCloseToTray(false) still destroys an unpinned tray as before", async ()
|
||||
}
|
||||
});
|
||||
|
||||
test("setShowTrayIcon(false) hides the tray even with close-to-tray on", async () => {
|
||||
const bridge = loadBridge();
|
||||
const electronModule = createElectronStub();
|
||||
const { ipcMain } = await enableCloseToTray(bridge, electronModule);
|
||||
|
||||
try {
|
||||
assert.notEqual(bridge.getTray(), null);
|
||||
const result = await ipcMain.handlers.get("netcatty:tray:setShowTrayIcon")(null, { enabled: false });
|
||||
assert.deepEqual(result, { success: true, enabled: false });
|
||||
assert.equal(bridge.getTray(), null, "the hide-tray-icon preference must win over close-to-tray");
|
||||
} finally {
|
||||
bridge.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("setShowTrayIcon(true) recreates the tray when close-to-tray is on", async () => {
|
||||
const bridge = loadBridge();
|
||||
const electronModule = createElectronStub();
|
||||
const { ipcMain } = await enableCloseToTray(bridge, electronModule);
|
||||
|
||||
try {
|
||||
await ipcMain.handlers.get("netcatty:tray:setShowTrayIcon")(null, { enabled: false });
|
||||
assert.equal(bridge.getTray(), null);
|
||||
await ipcMain.handlers.get("netcatty:tray:setShowTrayIcon")(null, { enabled: true });
|
||||
assert.notEqual(bridge.getTray(), null, "re-enabling the icon must restore the tray");
|
||||
} finally {
|
||||
bridge.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("pinTrayForHiddenLaunch respects the hidden tray icon preference", () => {
|
||||
const bridge = loadBridge();
|
||||
const electronModule = createElectronStub();
|
||||
bridge.init({ electronModule, getMainWindow: () => null });
|
||||
const ipcMain = createIpcMainStub();
|
||||
bridge.registerHandlers(ipcMain);
|
||||
|
||||
return ipcMain.handlers.get("netcatty:tray:setShowTrayIcon")(null, { enabled: false }).then(async () => {
|
||||
try {
|
||||
bridge.pinTrayForHiddenLaunch();
|
||||
assert.equal(bridge.getTray(), null, "an explicit hide-tray-icon choice must not be overridden by the hidden-launch pin");
|
||||
bridge.releaseHiddenLaunchTrayPin();
|
||||
assert.equal(bridge.getTray(), null);
|
||||
} finally {
|
||||
bridge.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("tray panel forwarding start reaches the main renderer coordinator", async () => {
|
||||
await withPlatform("darwin", async () => {
|
||||
const bridge = loadBridge();
|
||||
|
||||
@@ -1557,6 +1557,8 @@ function createPreloadApi(ctx) {
|
||||
ipcRenderer.invoke("netcatty:tray:setCloseToTray", { enabled }),
|
||||
isCloseToTray: () =>
|
||||
ipcRenderer.invoke("netcatty:tray:isCloseToTray"),
|
||||
setShowTrayIcon: (enabled) =>
|
||||
ipcRenderer.invoke("netcatty:tray:setShowTrayIcon", { enabled }),
|
||||
|
||||
// Auto Launch at system login (hidden to tray)
|
||||
getAutoLaunch: () =>
|
||||
|
||||
@@ -171,6 +171,7 @@ export const STORAGE_KEY_MANAGED_SOURCES = 'netcatty_managed_sources_v1';
|
||||
// Global Toggle Window Settings (Quake Mode)
|
||||
export const STORAGE_KEY_TOGGLE_WINDOW_HOTKEY = 'netcatty_toggle_window_hotkey_v1';
|
||||
export const STORAGE_KEY_CLOSE_TO_TRAY = 'netcatty_close_to_tray_v1';
|
||||
export const STORAGE_KEY_SHOW_TRAY_ICON = 'netcatty_show_tray_icon_v1';
|
||||
/** Optimistic cache only — the source of truth is app.getLoginItemSettings() in the main process. */
|
||||
export const STORAGE_KEY_AUTO_LAUNCH_ENABLED = 'netcatty_auto_launch_enabled_v1';
|
||||
/** App-level HTTP(S) proxy for cloud sync / AI (not SSH ProxyJump). */
|
||||
|
||||
+1
@@ -65,6 +65,7 @@ declare global {
|
||||
// System Tray / Close to Tray
|
||||
setCloseToTray?(enabled: boolean): Promise<{ success: boolean; enabled: boolean }>;
|
||||
isCloseToTray?(): Promise<{ enabled: boolean }>;
|
||||
setShowTrayIcon?(enabled: boolean): Promise<{ success: boolean; enabled: boolean }>;
|
||||
|
||||
// Auto Launch at system login (hidden to tray)
|
||||
getAutoLaunch?(): Promise<{ success: boolean; enabled: boolean; supported: boolean }>;
|
||||
|
||||
Reference in new issue
Block a user