mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-07 16:41:30 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59278b2c4b | ||
|
|
9b05103928 | ||
|
|
a129b9670c | ||
|
|
434f861978 |
No files matched your search
@@ -312,8 +312,11 @@ export const enCoreMessages: Messages = {
|
||||
'settings.globalHotkey.toggleWindowDesc': 'Press a key combination to set a global shortcut for showing/hiding the window.',
|
||||
'settings.globalHotkey.notSet': 'Not set',
|
||||
'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.closeToTray': 'Keep Running When Window Closes',
|
||||
'settings.globalHotkey.closeToTrayDesc': 'When enabled, closing the window hides it while the app keeps running. Tray icon visibility is set separately.',
|
||||
'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.showTrayIconHiddenHint': 'Tray icon hidden. Launch the app again or use the global toggle hotkey to bring the window back, then re-enable this setting to restore the icon.',
|
||||
'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).',
|
||||
|
||||
@@ -227,8 +227,11 @@ export const esCoreMessages: Messages = {
|
||||
'settings.globalHotkey.toggleWindowDesc': 'Presiona una combinación de teclas para definir un atajo global que muestre u oculte la ventana.',
|
||||
'settings.globalHotkey.notSet': 'No configurada',
|
||||
'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.closeToTray': 'Seguir en segundo plano al cerrar la ventana',
|
||||
'settings.globalHotkey.closeToTrayDesc': 'Al cerrar la ventana, se oculta y la aplicación sigue activa. La visibilidad del icono de la bandeja se configura aparte.',
|
||||
'settings.globalHotkey.showTrayIcon': 'Mostrar 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.showTrayIconHiddenHint': 'Icono de la bandeja oculto. Vuelve a iniciar la aplicación o usa el atajo global para recuperar la ventana y vuelve a activar este ajuste para restaurar el icono.',
|
||||
'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).',
|
||||
|
||||
@@ -296,8 +296,11 @@ export const ruCoreMessages: Messages = {
|
||||
'settings.globalHotkey.toggleWindowDesc': 'Нажмите сочетание клавиш, чтобы задать глобальную горячую клавишу для показа или скрытия окна.',
|
||||
'settings.globalHotkey.notSet': 'Не задано',
|
||||
'settings.globalHotkey.reset': 'Сбросить по умолчанию',
|
||||
'settings.globalHotkey.closeToTray': 'Сворачивать в системный трей',
|
||||
'settings.globalHotkey.closeToTrayDesc': 'Если включено, при закрытии окно будет сворачиваться в системный трей вместо выхода из приложения.',
|
||||
'settings.globalHotkey.closeToTray': 'Оставлять приложение в фоне при закрытии окна',
|
||||
'settings.globalHotkey.closeToTrayDesc': 'При закрытии окно скрывается, а приложение продолжает работать. Отображение значка в трее настраивается отдельно.',
|
||||
'settings.globalHotkey.showTrayIcon': 'Показывать значок в системном трее',
|
||||
'settings.globalHotkey.showTrayIconDesc': 'Если отключено, значок в трее скрыт, но приложение продолжает работать в фоне.',
|
||||
'settings.globalHotkey.showTrayIconHiddenHint': 'Значок в трее скрыт. Запустите приложение снова или используйте глобальную горячую клавишу, чтобы вернуть окно, затем снова включите этот параметр, чтобы восстановить значок.',
|
||||
'settings.globalHotkey.enabled': 'Включить глобальную горячую клавишу',
|
||||
'settings.globalHotkey.enabledDesc': 'Регистрировать системные сочетания клавиш. Когда отключено, все глобальные горячие клавиши снимаются с регистрации.',
|
||||
'settings.globalHotkey.hint': 'Глобальная горячая клавиша работает на уровне всей системы и позволяет быстро показывать или скрывать окно (терминал в стиле Quake).',
|
||||
|
||||
@@ -316,8 +316,11 @@ export const zhCNCoreMessages: Messages = {
|
||||
'settings.globalHotkey.toggleWindowDesc': '按下组合键以设置显示/隐藏窗口的全局快捷键。',
|
||||
'settings.globalHotkey.notSet': '未设置',
|
||||
'settings.globalHotkey.reset': '恢复默认',
|
||||
'settings.globalHotkey.closeToTray': '关闭时最小化到托盘',
|
||||
'settings.globalHotkey.closeToTrayDesc': '启用后,关闭窗口将最小化到系统托盘而不是退出程序。',
|
||||
'settings.globalHotkey.closeToTray': '关闭窗口后在后台运行',
|
||||
'settings.globalHotkey.closeToTrayDesc': '启用后,关闭窗口会隐藏窗口,应用继续在后台运行。托盘图标可单独开关。',
|
||||
'settings.globalHotkey.showTrayIcon': '显示系统托盘图标',
|
||||
'settings.globalHotkey.showTrayIconDesc': '关闭后,托盘图标将被隐藏,但应用仍会在后台正常运行。',
|
||||
'settings.globalHotkey.showTrayIconHiddenHint': '托盘图标已隐藏。再次启动应用或使用全局显示/隐藏快捷键即可找回窗口,然后重新打开此设置即可恢复图标。',
|
||||
'settings.globalHotkey.enabled': '启用全局快捷键',
|
||||
'settings.globalHotkey.enabledDesc': '注册系统级键盘快捷键。禁用后将取消所有全局快捷键注册。',
|
||||
'settings.globalHotkey.hint': '全局快捷键在系统范围内工作,可快速显示或隐藏窗口(下拉式终端风格)。',
|
||||
|
||||
@@ -318,8 +318,11 @@ export const zhTWCoreMessages: Messages = {
|
||||
'settings.globalHotkey.toggleWindowDesc': '按下組合鍵以設定顯示/隱藏視窗的全域快速鍵。',
|
||||
'settings.globalHotkey.notSet': '未設定',
|
||||
'settings.globalHotkey.reset': '恢復預設',
|
||||
'settings.globalHotkey.closeToTray': '關閉時最小化到系統匣',
|
||||
'settings.globalHotkey.closeToTrayDesc': '啟用後,關閉視窗將最小化到系統匣而不是結束程式。',
|
||||
'settings.globalHotkey.closeToTray': '關閉視窗後在背景執行',
|
||||
'settings.globalHotkey.closeToTrayDesc': '啟用後,關閉視窗會隱藏視窗,應用程式繼續在背景執行。系統匣圖示可另外開關。',
|
||||
'settings.globalHotkey.showTrayIcon': '顯示系統匣圖示',
|
||||
'settings.globalHotkey.showTrayIconDesc': '關閉後,系統匣圖示將被隱藏,但應用程式仍會在背景正常執行。',
|
||||
'settings.globalHotkey.showTrayIconHiddenHint': '系統匣圖示已隱藏。再次啟動應用程式或使用全域顯示/隱藏快捷鍵即可找回視窗,然後重新開啟此設定即可恢復圖示。',
|
||||
'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,
|
||||
@@ -173,6 +176,24 @@ export function useSystemSettingsEffects({
|
||||
notifySettingsChanged(STORAGE_KEY_CLOSE_TO_TRAY, closeToTray);
|
||||
}, [enabled, closeToTray, notifySettingsChanged, persistMountedRef]);
|
||||
|
||||
// Persist and sync the show-tray-icon setting. Runs after the close-to-tray
|
||||
// effect above (declaration order) so a hidden icon destroys the tray that
|
||||
// close-to-tray's mount-time push may have just created.
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
// Update main process tray visibility (needed on mount)
|
||||
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]);
|
||||
|
||||
// Hydrate auto-launch from the main process on mount — the OS login item
|
||||
// is the real source of truth (the user may have toggled it outside the
|
||||
// app), localStorage is only an optimistic cache for first paint.
|
||||
|
||||
@@ -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,13 @@ export const useSettingsState = (options: { enableSettingsSync?: boolean; enable
|
||||
if (stored === null) return true;
|
||||
return stored === 'true';
|
||||
});
|
||||
// Preserve the old tray visibility for existing users until they set this
|
||||
// independently. Fresh installs still default to showing the icon.
|
||||
const [showTrayIcon, setShowTrayIcon] = useState<boolean>(() => {
|
||||
const stored = readStoredString(STORAGE_KEY_SHOW_TRAY_ICON);
|
||||
if (stored === null) return closeToTray;
|
||||
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 +1799,7 @@ export const useSettingsState = (options: { enableSettingsSync?: boolean; enable
|
||||
toggleWindowHotkey,
|
||||
globalHotkeyEnabled,
|
||||
closeToTray,
|
||||
showTrayIcon,
|
||||
autoLaunchEnabled,
|
||||
windowOpacityRecord,
|
||||
windowOpacityMutationSourceRef,
|
||||
@@ -2156,6 +2165,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,
|
||||
@@ -1313,6 +1317,23 @@ const SettingsSystemTab: React.FC<SettingsSystemTabProps> = ({
|
||||
onChange={setCloseToTray}
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
{/* Show System Tray Icon */}
|
||||
<SettingRow
|
||||
anchorId="system-show-tray-icon"
|
||||
label={t("settings.globalHotkey.showTrayIcon")}
|
||||
description={t("settings.globalHotkey.showTrayIconDesc")}
|
||||
>
|
||||
<Toggle
|
||||
checked={showTrayIcon}
|
||||
onChange={setShowTrayIcon}
|
||||
/>
|
||||
</SettingRow>
|
||||
{!showTrayIcon && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("settings.globalHotkey.showTrayIconHiddenHint")}
|
||||
</p>
|
||||
)}
|
||||
</SettingCard>
|
||||
|
||||
<SettingHint>
|
||||
|
||||
@@ -961,6 +961,13 @@ export const SETTINGS_SEARCH_CATALOG: readonly SettingsSearchEntry[] = [
|
||||
descriptionKey: "settings.globalHotkey.closeToTrayDesc",
|
||||
sectionKey: "settings.globalHotkey.title",
|
||||
},
|
||||
{
|
||||
id: "system-show-tray-icon",
|
||||
tab: "system",
|
||||
labelKey: "settings.globalHotkey.showTrayIcon",
|
||||
descriptionKey: "settings.globalHotkey.showTrayIconDesc",
|
||||
sectionKey: "settings.globalHotkey.title",
|
||||
},
|
||||
|
||||
// Plugins
|
||||
{
|
||||
|
||||
@@ -19,6 +19,11 @@ let sendWhenRendererReady = null;
|
||||
let getSystemMenuMainWindow = null;
|
||||
let tray = null;
|
||||
let closeToTray = false;
|
||||
// User preference for whether the tray/menu bar icon is shown at all.
|
||||
// Independent of close-to-tray: the app keeps running in the background with
|
||||
// the icon hidden, and window close still hides (not quits) when close-to-tray
|
||||
// was explicitly enabled.
|
||||
let showTrayIcon = true;
|
||||
let windowsTrayScaleListener = null;
|
||||
let currentHotkey = null;
|
||||
let hotkeyEnabled = false;
|
||||
@@ -1292,18 +1297,12 @@ function setCloseToTray(enabled) {
|
||||
closeToTray = !!enabled;
|
||||
|
||||
if (closeToTray) {
|
||||
// Create tray if it doesn't exist
|
||||
if (!tray) {
|
||||
// Tray visibility is controlled separately by showTrayIcon.
|
||||
if (!tray && showTrayIcon) {
|
||||
createTray();
|
||||
}
|
||||
} else {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, enabled: closeToTray };
|
||||
@@ -1321,18 +1320,45 @@ function pinTrayForHiddenLaunch() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Release the hidden-launch tray pin once its window has been shown. The
|
||||
* hidden icon preference can now take effect safely.
|
||||
*/
|
||||
function releaseHiddenLaunchTrayPin() {
|
||||
if (!hiddenLaunchTrayPinned) return;
|
||||
hiddenLaunchTrayPinned = false;
|
||||
if (!closeToTray) {
|
||||
if (!showTrayIcon) {
|
||||
destroyTray();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show or hide the tray icon without changing close-to-tray behavior. The
|
||||
* app keeps running in the background while the icon is hidden; close-to-tray
|
||||
* still hides the window on close.
|
||||
*/
|
||||
function setShowTrayIcon(enabled) {
|
||||
showTrayIcon = !!enabled;
|
||||
|
||||
if (showTrayIcon) {
|
||||
if (!tray) {
|
||||
createTray();
|
||||
}
|
||||
} else if (!hiddenLaunchTrayPinned) {
|
||||
// A hidden auto-launch cold start keeps its safety pin until its window
|
||||
// is shown once, so a trayless zombie never appears without consent.
|
||||
destroyTray();
|
||||
}
|
||||
|
||||
return { success: true, enabled: showTrayIcon };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the tray icon is currently meant to be shown
|
||||
*/
|
||||
function isShowTrayIconEnabled() {
|
||||
return showTrayIcon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if close-to-tray is enabled
|
||||
*/
|
||||
@@ -1354,7 +1380,10 @@ function getHotkeyStatus() {
|
||||
* Handle window close event - hide to tray instead of closing
|
||||
*/
|
||||
function handleWindowClose(event, win) {
|
||||
if (closeToTray && tray) {
|
||||
// With the tray icon hidden by preference there is no `tray` object, but a
|
||||
// user who enabled close-to-tray still expects the window to hide (app
|
||||
// stays in the background), not to quit.
|
||||
if (closeToTray && (tray || !showTrayIcon)) {
|
||||
event.preventDefault();
|
||||
hideWindowRespectingMacFullscreen(win);
|
||||
return true; // Prevented close
|
||||
@@ -1392,6 +1421,16 @@ function registerHandlers(ipcMain) {
|
||||
return { enabled: closeToTray };
|
||||
});
|
||||
|
||||
// Show/hide the tray icon itself (independent of close-to-tray)
|
||||
ipcMain.handle("netcatty:tray:setShowTrayIcon", async (_event, { enabled }) => {
|
||||
return setShowTrayIcon(enabled);
|
||||
});
|
||||
|
||||
// Get show-tray-icon status
|
||||
ipcMain.handle("netcatty:tray:isShowTrayIcon", async () => {
|
||||
return { enabled: showTrayIcon };
|
||||
});
|
||||
|
||||
// Update tray menu data
|
||||
ipcMain.handle("netcatty:tray:updateMenuData", async (_event, data) => {
|
||||
setTrayMenuData(data);
|
||||
|
||||
@@ -1705,7 +1705,7 @@ test("a hidden-launch tray pin survives close-to-tray being turned off", async (
|
||||
}
|
||||
});
|
||||
|
||||
test("releasing the hidden-launch tray pin destroys the tray if close-to-tray is off", async () => {
|
||||
test("releasing the hidden-launch tray pin keeps the visible tray when close-to-tray is off", async () => {
|
||||
const bridge = loadBridge();
|
||||
const electronModule = createElectronStub();
|
||||
bridge.init({ electronModule, getMainWindow: () => null });
|
||||
@@ -1719,7 +1719,27 @@ test("releasing the hidden-launch tray pin destroys the tray if close-to-tray is
|
||||
|
||||
bridge.releaseHiddenLaunchTrayPin();
|
||||
|
||||
assert.equal(bridge.getTray(), null, "once the window is shown, close-to-tray=off should win");
|
||||
assert.notEqual(bridge.getTray(), null, "show-tray-icon=on keeps the tray visible");
|
||||
} finally {
|
||||
bridge.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("releasing the hidden-launch tray pin hides the icon when requested", async () => {
|
||||
const bridge = loadBridge();
|
||||
const electronModule = createElectronStub();
|
||||
bridge.init({ electronModule, getMainWindow: () => null });
|
||||
const ipcMain = createIpcMainStub();
|
||||
bridge.registerHandlers(ipcMain);
|
||||
|
||||
try {
|
||||
bridge.pinTrayForHiddenLaunch();
|
||||
await ipcMain.handlers.get("netcatty:tray:setShowTrayIcon")(null, { enabled: false });
|
||||
assert.notEqual(bridge.getTray(), null, "the hidden launch keeps its recovery icon");
|
||||
|
||||
bridge.releaseHiddenLaunchTrayPin();
|
||||
|
||||
assert.equal(bridge.getTray(), null);
|
||||
} finally {
|
||||
bridge.cleanup();
|
||||
}
|
||||
@@ -1757,7 +1777,7 @@ test("releaseHiddenLaunchTrayPin is a no-op when the pin was never set", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("setCloseToTray(false) still destroys an unpinned tray as before", async () => {
|
||||
test("setCloseToTray(false) keeps a visible tray without hiding the window on close", async () => {
|
||||
const bridge = loadBridge();
|
||||
const electronModule = createElectronStub();
|
||||
const { ipcMain } = await enableCloseToTray(bridge, electronModule);
|
||||
@@ -1765,6 +1785,92 @@ test("setCloseToTray(false) still destroys an unpinned tray as before", async ()
|
||||
try {
|
||||
assert.notEqual(bridge.getTray(), null);
|
||||
await ipcMain.handlers.get("netcatty:tray:setCloseToTray")(null, { enabled: false });
|
||||
assert.notEqual(bridge.getTray(), null);
|
||||
assert.equal(bridge.handleWindowClose({ preventDefault() { assert.fail("close was prevented"); } }, new FakeWindow()), false);
|
||||
} finally {
|
||||
bridge.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("setShowTrayIcon(false) destroys the tray even when close-to-tray stays 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);
|
||||
} finally {
|
||||
bridge.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("handleWindowClose still hides to tray after the icon was hidden by preference", async () => {
|
||||
await withPlatform("darwin", 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);
|
||||
|
||||
const win = new FakeWindow({ fullscreen: false });
|
||||
let prevented = false;
|
||||
const result = bridge.handleWindowClose({ preventDefault() { prevented = true; } }, win);
|
||||
|
||||
assert.equal(result, true);
|
||||
assert.equal(prevented, true);
|
||||
assert.equal(win.hideCalls, 1);
|
||||
} finally {
|
||||
bridge.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("setShowTrayIcon(true) restores 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);
|
||||
} finally {
|
||||
bridge.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("setShowTrayIcon(true) creates a tray when close-to-tray is off", async () => {
|
||||
const bridge = loadBridge();
|
||||
const electronModule = createElectronStub();
|
||||
bridge.init({ electronModule, getMainWindow: () => null });
|
||||
const ipcMain = createIpcMainStub();
|
||||
bridge.registerHandlers(ipcMain);
|
||||
|
||||
try {
|
||||
await ipcMain.handlers.get("netcatty:tray:setCloseToTray")(null, { enabled: false });
|
||||
await ipcMain.handlers.get("netcatty:tray:setShowTrayIcon")(null, { enabled: true });
|
||||
assert.notEqual(bridge.getTray(), null);
|
||||
} finally {
|
||||
bridge.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("setCloseToTray(true) does not recreate the tray while the icon is hidden by preference", async () => {
|
||||
const bridge = loadBridge();
|
||||
const electronModule = createElectronStub();
|
||||
bridge.init({ electronModule, getMainWindow: () => null });
|
||||
const ipcMain = createIpcMainStub();
|
||||
bridge.registerHandlers(ipcMain);
|
||||
|
||||
try {
|
||||
await ipcMain.handlers.get("netcatty:tray:setShowTrayIcon")(null, { enabled: false });
|
||||
await ipcMain.handlers.get("netcatty:tray:setCloseToTray")(null, { enabled: true });
|
||||
assert.equal(bridge.getTray(), null);
|
||||
} finally {
|
||||
bridge.cleanup();
|
||||
|
||||
@@ -1557,6 +1557,10 @@ function createPreloadApi(ctx) {
|
||||
ipcRenderer.invoke("netcatty:tray:setCloseToTray", { enabled }),
|
||||
isCloseToTray: () =>
|
||||
ipcRenderer.invoke("netcatty:tray:isCloseToTray"),
|
||||
setShowTrayIcon: (enabled) =>
|
||||
ipcRenderer.invoke("netcatty:tray:setShowTrayIcon", { enabled }),
|
||||
isShowTrayIcon: () =>
|
||||
ipcRenderer.invoke("netcatty:tray:isShowTrayIcon"),
|
||||
|
||||
// Auto Launch at system login (hidden to tray)
|
||||
getAutoLaunch: () =>
|
||||
|
||||
@@ -171,6 +171,8 @@ 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';
|
||||
/** Whether the system tray / menu bar icon is shown. Default: shown. */
|
||||
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). */
|
||||
|
||||
+2
@@ -65,6 +65,8 @@ 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 }>;
|
||||
isShowTrayIcon?(): Promise<{ 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