mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-03 12:32:49 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e91b6b808 | ||
|
|
5919dcdc22 | ||
|
|
dad6407308 |
No files matched your search
@@ -538,7 +538,7 @@ Highlight the focused split pane:
|
||||
'settings.appearance.windowOpacity': 'Window Opacity',
|
||||
'settings.appearance.windowOpacity.desc': 'Adjust the transparency of the entire application window. Lower values also fade terminal text. Some Linux desktop environments may not support this.',
|
||||
'settings.appearance.appIcon': 'App Icon',
|
||||
'settings.appearance.appIcon.desc': 'Choose a dock/taskbar icon style to make Netcatty easier to spot when switching apps. Applies at runtime; the installed app bundle icon is unchanged.',
|
||||
'settings.appearance.appIcon.desc': 'Choose a dock/taskbar icon style to make Netcatty easier to spot when switching apps. On Windows, the notification area icon uses the same style. Applies at runtime; the installed app bundle icon is unchanged.',
|
||||
'settings.appearance.appIcon.original': 'Original',
|
||||
'settings.appearance.appIcon.bright': 'Bright',
|
||||
'settings.appearance.appIcon.dark': 'Dark',
|
||||
|
||||
@@ -453,7 +453,7 @@ Resalta el panel dividido enfocado:
|
||||
'settings.appearance.windowOpacity': 'Opacidad de la ventana',
|
||||
'settings.appearance.windowOpacity.desc': 'Ajusta la transparencia de toda la ventana de la aplicación. Los valores más bajos también atenúan el texto de la terminal. Algunos entornos de escritorio de Linux pueden no admitir esto.',
|
||||
'settings.appearance.appIcon': 'Ícono de la aplicación',
|
||||
'settings.appearance.appIcon.desc': 'Elige un estilo de ícono para el dock o la barra de tareas que haga más fácil distinguir Netcatty al cambiar de aplicación. Se aplica en tiempo de ejecución; el ícono del paquete instalado no cambia.',
|
||||
'settings.appearance.appIcon.desc': 'Elige un estilo de ícono para el dock o la barra de tareas que haga más fácil distinguir Netcatty al cambiar de aplicación. En Windows, el icono del área de notificación usa el mismo estilo. Se aplica en tiempo de ejecución; el ícono del paquete instalado no cambia.',
|
||||
'settings.appearance.appIcon.original': 'Original',
|
||||
'settings.appearance.appIcon.bright': 'Brillante',
|
||||
'settings.appearance.appIcon.dark': 'Oscuro',
|
||||
|
||||
@@ -507,7 +507,7 @@ export const ruCoreMessages: Messages = {
|
||||
'settings.appearance.windowOpacity': 'Прозрачность окна',
|
||||
'settings.appearance.windowOpacity.desc': 'Настройте прозрачность всего окна приложения. При низких значениях текст терминала тоже бледнеет. В некоторых средах Linux это может не поддерживаться.',
|
||||
'settings.appearance.appIcon': 'Значок приложения',
|
||||
'settings.appearance.appIcon.desc': 'Выберите стиль значка для Dock/панели задач, чтобы Netcatty было проще найти при переключении приложений. Применяется во время работы; значок установленного пакета не меняется.',
|
||||
'settings.appearance.appIcon.desc': 'Выберите стиль значка для Dock/панели задач, чтобы Netcatty было проще найти при переключении приложений. В Windows значок области уведомлений использует тот же стиль. Применяется во время работы; значок установленного пакета не меняется.',
|
||||
'settings.appearance.appIcon.original': 'Оригинал',
|
||||
'settings.appearance.appIcon.bright': 'Яркий',
|
||||
'settings.appearance.appIcon.dark': 'Тёмный',
|
||||
|
||||
@@ -542,7 +542,7 @@ export const zhCNCoreMessages: Messages = {
|
||||
'settings.appearance.windowOpacity': '窗口透明度',
|
||||
'settings.appearance.windowOpacity.desc': '调节整个应用窗口的透明度,方便叠在其他内容上方。较低时终端文字也会变淡;部分 Linux 桌面环境可能不支持。',
|
||||
'settings.appearance.appIcon': '应用图标',
|
||||
'settings.appearance.appIcon.desc': '选择 Dock/任务栏图标风格,切换应用时更容易找到 Netcatty。运行时生效,安装包内的图标不会改变。',
|
||||
'settings.appearance.appIcon.desc': '选择 Dock/任务栏图标风格,切换应用时更容易找到 Netcatty。Windows 通知区域图标会使用同一套风格。运行时生效,安装包内的图标不会改变。',
|
||||
'settings.appearance.appIcon.original': '原始',
|
||||
'settings.appearance.appIcon.bright': '明亮',
|
||||
'settings.appearance.appIcon.dark': '深色',
|
||||
|
||||
@@ -544,7 +544,7 @@ export const zhTWCoreMessages: Messages = {
|
||||
'settings.appearance.windowOpacity': '視窗透明度',
|
||||
'settings.appearance.windowOpacity.desc': '調節整個應用程式視窗的透明度,方便疊在其他內容上方。較低時終端文字也會變淡;部分 Linux 桌面環境可能不支援。',
|
||||
'settings.appearance.appIcon': '應用程式圖示',
|
||||
'settings.appearance.appIcon.desc': '選擇 Dock/工作列圖示風格,切換應用程式時更容易找到 Netcatty。執行時生效,安裝檔內的圖示不會改變。',
|
||||
'settings.appearance.appIcon.desc': '選擇 Dock/工作列圖示風格,切換應用程式時更容易找到 Netcatty。Windows 通知區域圖示會使用同一套風格。執行時生效,安裝檔內的圖示不會改變。',
|
||||
'settings.appearance.appIcon.original': '原始',
|
||||
'settings.appearance.appIcon.bright': '明亮',
|
||||
'settings.appearance.appIcon.dark': '深色',
|
||||
|
||||
@@ -16,12 +16,20 @@ test("SFTP side panel rebinds after same-tab SSH start-over", () => {
|
||||
sidePanelSource,
|
||||
/previousStatus:\s*lastSourceSessionStatusRef\.current/,
|
||||
);
|
||||
// Reuse only after SSH is connected; linked id may arrive while reconnecting.
|
||||
// Reuse the terminal transport only after SSH is connected. A linked id can
|
||||
// arrive while reconnecting. Single-channel bastions dial a separate login.
|
||||
assert.match(
|
||||
sidePanelSource,
|
||||
/activeSessionStatus === "connected" \? \(activeSessionId \?\? undefined\) : undefined/,
|
||||
/const reuseTerminalTransport = !dedicatedSftpLogin && \(\s*Boolean\(pendingStrictSourceSessionId\) \|\| activeSessionStatus === "connected"\s*\);/,
|
||||
);
|
||||
assert.match(
|
||||
sidePanelSource,
|
||||
/sourceSessionId: reuseTerminalTransport\s*\?\s*\(pendingStrictSourceSessionId \?\? \(activeSessionId \?\? undefined\)\)\s*: undefined/,
|
||||
);
|
||||
assert.match(
|
||||
sidePanelSource,
|
||||
/requireSourceSessionReuse: reuseTerminalTransport && Boolean\(pendingStrictSourceSessionId\)/,
|
||||
);
|
||||
assert.match(sidePanelSource, /requireSourceSessionReuse:\s*Boolean\(pendingStrictSourceSessionId\)/);
|
||||
assert.match(sidePanelSource, /resolveSftpSidePanelTrackedSourceStatusUpdate\(/);
|
||||
assert.match(
|
||||
sidePanelSource,
|
||||
|
||||
@@ -19,6 +19,7 @@ let sendWhenRendererReady = null;
|
||||
let getSystemMenuMainWindow = null;
|
||||
let tray = null;
|
||||
let closeToTray = false;
|
||||
let windowsTrayScaleListener = null;
|
||||
let currentHotkey = null;
|
||||
let hotkeyEnabled = false;
|
||||
// True while a hidden auto-launch cold start has no visible window yet.
|
||||
@@ -572,6 +573,155 @@ function toggleTrayPanel(eventBounds) {
|
||||
}
|
||||
}
|
||||
|
||||
function windowsSmallIconPx() {
|
||||
let scale = 1;
|
||||
try {
|
||||
const reported = electronModule?.screen?.getPrimaryDisplay?.()?.scaleFactor;
|
||||
if (typeof reported === "number" && Number.isFinite(reported) && reported > 0) {
|
||||
scale = reported;
|
||||
}
|
||||
} catch {
|
||||
scale = 1;
|
||||
}
|
||||
// SM_CXSMICON is 16px at 100% and scales with the system DPI.
|
||||
return Math.max(16, Math.round(16 * scale));
|
||||
}
|
||||
|
||||
function loadPackagedTrayImage() {
|
||||
const { nativeImage } = electronModule;
|
||||
const iconPath = resolveTrayIconPath();
|
||||
if (!iconPath || !nativeImage?.createFromPath) return null;
|
||||
return nativeImage.createFromPath(iconPath);
|
||||
}
|
||||
|
||||
// Variant PNGs keep the Apple-style transparent margin (about 6% per side).
|
||||
// The packaged tray ico is full-bleed, so leaving that margin in place makes
|
||||
// every other style look smaller in the same 16px slot. Crop to the opaque
|
||||
// artwork, then scale.
|
||||
function cropTransparentMargin(image) {
|
||||
if (!image?.getSize || !image?.toBitmap || !image?.crop) return image;
|
||||
let size;
|
||||
let bitmap;
|
||||
try {
|
||||
size = image.getSize(1);
|
||||
bitmap = image.toBitmap({ scaleFactor: 1 });
|
||||
} catch {
|
||||
return image;
|
||||
}
|
||||
const width = size?.width;
|
||||
const height = size?.height;
|
||||
if (!width || !height || bitmap?.length !== width * height * 4) return image;
|
||||
|
||||
let minX = width;
|
||||
let minY = height;
|
||||
let maxX = -1;
|
||||
let maxY = -1;
|
||||
for (let y = 0; y < height; y += 1) {
|
||||
const row = y * width * 4;
|
||||
for (let x = 0; x < width; x += 1) {
|
||||
if (bitmap[row + x * 4 + 3] <= 16) continue;
|
||||
if (x < minX) minX = x;
|
||||
if (y < minY) minY = y;
|
||||
if (x > maxX) maxX = x;
|
||||
if (y > maxY) maxY = y;
|
||||
}
|
||||
}
|
||||
if (maxX < minX || maxY < minY) return image;
|
||||
|
||||
const pad = 1;
|
||||
const boxLeft = Math.max(0, minX - pad);
|
||||
const boxTop = Math.max(0, minY - pad);
|
||||
const boxRight = Math.min(width - 1, maxX + pad);
|
||||
const boxBottom = Math.min(height - 1, maxY + pad);
|
||||
let side = Math.max(boxRight - boxLeft + 1, boxBottom - boxTop + 1);
|
||||
side = Math.min(side, width, height);
|
||||
let left = Math.round((boxLeft + boxRight) / 2 - (side - 1) / 2);
|
||||
let top = Math.round((boxTop + boxBottom) / 2 - (side - 1) / 2);
|
||||
left = Math.max(0, Math.min(left, width - side));
|
||||
top = Math.max(0, Math.min(top, height - side));
|
||||
if (left === 0 && top === 0 && side === width && side === height) return image;
|
||||
|
||||
try {
|
||||
const cropped = image.crop({ x: left, y: top, width: side, height: side });
|
||||
if (!cropped || cropped.isEmpty?.()) return image;
|
||||
return cropped;
|
||||
} catch {
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
||||
// Windows Tray::SetImage asks NativeImage for an HICON at SM_CXSMICON.
|
||||
// An .ico path keeps that lookup. A variant PNG is one 1024px bitmap, and
|
||||
// GetHICON would pass the whole bitmap through, so scale it down first.
|
||||
function loadWindowsTrayImage() {
|
||||
const { nativeImage } = electronModule;
|
||||
let variant = "original";
|
||||
let variantPath = null;
|
||||
try {
|
||||
const appIconManager = require("./appIconManager.cjs");
|
||||
variant = appIconManager.getAppIconVariant();
|
||||
if (variant !== "original") {
|
||||
variantPath = appIconManager.getAppIconPath();
|
||||
}
|
||||
} catch {
|
||||
variant = "original";
|
||||
}
|
||||
|
||||
if (variant === "original" || !variantPath || !fs.existsSync(variantPath)) {
|
||||
return loadPackagedTrayImage();
|
||||
}
|
||||
|
||||
try {
|
||||
const source = nativeImage.createFromBuffer
|
||||
? nativeImage.createFromBuffer(fs.readFileSync(variantPath))
|
||||
: nativeImage.createFromPath(variantPath);
|
||||
if (!source || source.isEmpty?.()) return loadPackagedTrayImage();
|
||||
const artwork = cropTransparentMargin(source);
|
||||
const size = windowsSmallIconPx();
|
||||
const sized = artwork?.resize
|
||||
? artwork.resize({ width: size, height: size, quality: "best" })
|
||||
: artwork;
|
||||
if (!sized || sized.isEmpty?.()) return loadPackagedTrayImage();
|
||||
return sized;
|
||||
} catch {
|
||||
return loadPackagedTrayImage();
|
||||
}
|
||||
}
|
||||
|
||||
function applyWindowsTrayImage() {
|
||||
if (process.platform !== "win32" || !tray || !electronModule) return false;
|
||||
const image = loadWindowsTrayImage();
|
||||
if (!image || !tray.setImage) return false;
|
||||
try {
|
||||
tray.setImage(image);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function bindWindowsTrayScaleListener() {
|
||||
if (process.platform !== "win32" || windowsTrayScaleListener) return;
|
||||
const screen = electronModule?.screen;
|
||||
if (!screen?.on) return;
|
||||
windowsTrayScaleListener = () => {
|
||||
applyWindowsTrayImage();
|
||||
};
|
||||
screen.on("display-metrics-changed", windowsTrayScaleListener);
|
||||
}
|
||||
|
||||
function unbindWindowsTrayScaleListener() {
|
||||
const screen = electronModule?.screen;
|
||||
if (windowsTrayScaleListener && screen?.removeListener) {
|
||||
try {
|
||||
screen.removeListener("display-metrics-changed", windowsTrayScaleListener);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
windowsTrayScaleListener = null;
|
||||
}
|
||||
|
||||
function resolveTrayIconPath() {
|
||||
const { app } = electronModule;
|
||||
|
||||
@@ -858,14 +1008,16 @@ function createTray() {
|
||||
// Load the tray icon
|
||||
let trayIcon;
|
||||
const resolvedIconPath = resolveTrayIconPath();
|
||||
if (resolvedIconPath) {
|
||||
if (process.platform === "win32") {
|
||||
// Original uses the multi-size .ico. Any other app-icon choice is
|
||||
// drawn from that variant, scaled to the notification-area slot.
|
||||
trayIcon = loadWindowsTrayImage();
|
||||
bindWindowsTrayScaleListener();
|
||||
} else if (resolvedIconPath) {
|
||||
trayIcon = nativeImage.createFromPath(resolvedIconPath);
|
||||
if (process.platform === "darwin") {
|
||||
trayIcon = trayIcon.resize({ width: 16, height: 16 });
|
||||
trayIcon.setTemplateImage(true);
|
||||
} else if (process.platform === "win32") {
|
||||
// The .ico already carries 16/20/24/32/40/48/64 — Windows picks the
|
||||
// right size per DPI scale on its own. Do not resize.
|
||||
} else {
|
||||
// Linux: attach the @2x representation so the shell can pick the
|
||||
// right pixel size on HiDPI. Leaving the base at its native size
|
||||
@@ -1311,6 +1463,7 @@ function registerHandlers(ipcMain) {
|
||||
*/
|
||||
function cleanup() {
|
||||
unregisterGlobalHotkey();
|
||||
unbindWindowsTrayScaleListener();
|
||||
destroyTray();
|
||||
pendingPortForwardToggles = [];
|
||||
pendingHostConnections = [];
|
||||
@@ -1354,6 +1507,8 @@ module.exports = {
|
||||
clearPendingFullscreenHide,
|
||||
cleanup,
|
||||
createTray,
|
||||
updateTrayIcon: applyWindowsTrayImage,
|
||||
__cropTransparentMarginForTests: cropTransparentMargin,
|
||||
pinTrayForHiddenLaunch,
|
||||
releaseHiddenLaunchTrayPin,
|
||||
getTray: () => tray,
|
||||
|
||||
@@ -1822,3 +1822,251 @@ test("tray panel forwarding waits for a newly created main renderer", async () =
|
||||
assert.equal(delivered, true);
|
||||
});
|
||||
});
|
||||
|
||||
function createRecordingTrayElectron(scaleHolder) {
|
||||
const electronModule = createElectronStub();
|
||||
const resizes = [];
|
||||
const crops = [];
|
||||
let scaleListener = null;
|
||||
const variantWidth = 10;
|
||||
const variantHeight = 10;
|
||||
const variantBitmap = Buffer.alloc(variantWidth * variantHeight * 4);
|
||||
for (let y = 2; y <= 7; y += 1) {
|
||||
for (let x = 2; x <= 7; x += 1) {
|
||||
variantBitmap[(y * variantWidth + x) * 4 + 3] = 255;
|
||||
}
|
||||
}
|
||||
class RecordingTray {
|
||||
constructor(image) {
|
||||
this.image = image;
|
||||
this.handlers = new Map();
|
||||
this.setImageCalls = [];
|
||||
}
|
||||
|
||||
setToolTip() {}
|
||||
setContextMenu(menu) {
|
||||
this.contextMenu = menu;
|
||||
}
|
||||
setImage(image) {
|
||||
this.image = image;
|
||||
this.setImageCalls.push(image);
|
||||
}
|
||||
destroy() {}
|
||||
on(eventName, handler) {
|
||||
this.handlers.set(eventName, handler);
|
||||
}
|
||||
}
|
||||
electronModule.Tray = RecordingTray;
|
||||
electronModule.screen = {
|
||||
getPrimaryDisplay() {
|
||||
return { scaleFactor: scaleHolder.scale };
|
||||
},
|
||||
on(eventName, handler) {
|
||||
if (eventName === "display-metrics-changed") scaleListener = handler;
|
||||
},
|
||||
removeListener(eventName, handler) {
|
||||
if (eventName === "display-metrics-changed" && scaleListener === handler) {
|
||||
scaleListener = null;
|
||||
}
|
||||
},
|
||||
};
|
||||
electronModule.nativeImage = {
|
||||
createFromPath(filePath) {
|
||||
return {
|
||||
kind: "path",
|
||||
filePath,
|
||||
resize(opts) {
|
||||
resizes.push(opts);
|
||||
return {
|
||||
kind: "resized-path",
|
||||
filePath,
|
||||
...opts,
|
||||
setTemplateImage() {},
|
||||
addRepresentation() {},
|
||||
isEmpty() {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
},
|
||||
setTemplateImage() {},
|
||||
addRepresentation() {},
|
||||
isEmpty() {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
},
|
||||
createFromBuffer(buffer) {
|
||||
return {
|
||||
kind: "buffer",
|
||||
bytes: buffer.length,
|
||||
getSize() {
|
||||
return { width: variantWidth, height: variantHeight };
|
||||
},
|
||||
toBitmap() {
|
||||
return variantBitmap;
|
||||
},
|
||||
crop(rect) {
|
||||
crops.push(rect);
|
||||
return {
|
||||
kind: "cropped",
|
||||
rect,
|
||||
isEmpty() {
|
||||
return false;
|
||||
},
|
||||
resize(opts) {
|
||||
resizes.push(opts);
|
||||
return {
|
||||
kind: "resized-buffer",
|
||||
bytes: buffer.length,
|
||||
rect,
|
||||
...opts,
|
||||
isEmpty() {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
resize(opts) {
|
||||
resizes.push(opts);
|
||||
return {
|
||||
kind: "resized-buffer",
|
||||
bytes: buffer.length,
|
||||
...opts,
|
||||
isEmpty() {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
},
|
||||
isEmpty() {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
},
|
||||
createEmpty() {
|
||||
return { kind: "empty" };
|
||||
},
|
||||
};
|
||||
return {
|
||||
electronModule,
|
||||
resizes,
|
||||
crops,
|
||||
fireScaleChange() {
|
||||
scaleListener?.();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function applyIconVariant(variant) {
|
||||
const appIconManager = require("./appIconManager.cjs");
|
||||
return appIconManager.applyAppIconVariant(variant, {
|
||||
app: { isPackaged: false },
|
||||
BrowserWindow: { getAllWindows: () => [] },
|
||||
nativeImage: {
|
||||
createFromBuffer: () => ({}),
|
||||
createFromPath: () => ({}),
|
||||
},
|
||||
appPath: process.cwd(),
|
||||
isMac: false,
|
||||
});
|
||||
}
|
||||
|
||||
test("windows tray follows the selected app icon and keeps the ico for original", async () => {
|
||||
await withPlatform("win32", async () => {
|
||||
const appIconManager = require("./appIconManager.cjs");
|
||||
const bridge = loadBridge();
|
||||
const scaleHolder = { scale: 1.5 };
|
||||
const { electronModule, resizes, crops, fireScaleChange } = createRecordingTrayElectron(scaleHolder);
|
||||
try {
|
||||
appIconManager.initializeAppIconManager(process.cwd(), { preferPublic: true, isMac: false });
|
||||
await enableCloseToTray(bridge, electronModule);
|
||||
const trayInstance = bridge.getTray();
|
||||
assert.match(trayInstance.image.filePath, /tray-icon\.ico$/);
|
||||
assert.equal(resizes.length, 0);
|
||||
|
||||
assert.equal(applyIconVariant("bright"), true);
|
||||
assert.equal(bridge.updateTrayIcon(), true);
|
||||
assert.equal(trayInstance.image.kind, "resized-buffer");
|
||||
assert.deepEqual(crops[0], { x: 1, y: 1, width: 8, height: 8 });
|
||||
assert.deepEqual(resizes.at(-1), { width: 24, height: 24, quality: "best" });
|
||||
|
||||
scaleHolder.scale = 2;
|
||||
fireScaleChange();
|
||||
assert.deepEqual(resizes.at(-1), { width: 32, height: 32, quality: "best" });
|
||||
|
||||
assert.equal(applyIconVariant("original"), true);
|
||||
assert.equal(bridge.updateTrayIcon(), true);
|
||||
assert.match(trayInstance.image.filePath, /tray-icon\.ico$/);
|
||||
} finally {
|
||||
bridge.cleanup();
|
||||
appIconManager.initializeAppIconManager(process.cwd(), { preferPublic: true, isMac: false });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("non-windows trays ignore the app icon variant", async () => {
|
||||
await withPlatform("darwin", async () => {
|
||||
const appIconManager = require("./appIconManager.cjs");
|
||||
const bridge = loadBridge();
|
||||
const { electronModule } = createRecordingTrayElectron({ scale: 1 });
|
||||
try {
|
||||
appIconManager.initializeAppIconManager(process.cwd(), { preferPublic: true, isMac: false });
|
||||
await enableCloseToTray(bridge, electronModule);
|
||||
const trayInstance = bridge.getTray();
|
||||
assert.match(trayInstance.image.filePath, /tray-iconTemplate\.png$/);
|
||||
assert.equal(applyIconVariant("bright"), true);
|
||||
assert.equal(bridge.updateTrayIcon(), false);
|
||||
assert.equal(trayInstance.setImageCalls.length, 0);
|
||||
assert.match(trayInstance.image.filePath, /tray-iconTemplate\.png$/);
|
||||
} finally {
|
||||
bridge.cleanup();
|
||||
appIconManager.initializeAppIconManager(process.cwd(), { preferPublic: true, isMac: false });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function bitmapWithOpaqueRect(width, height, opaque) {
|
||||
const bitmap = Buffer.alloc(width * height * 4);
|
||||
for (let y = opaque.y; y < opaque.y + opaque.height; y += 1) {
|
||||
for (let x = opaque.x; x < opaque.x + opaque.width; x += 1) {
|
||||
bitmap[(y * width + x) * 4 + 3] = 255;
|
||||
}
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
test("tray icon crop drops the transparent margin and keeps a full-bleed image", () => {
|
||||
const bridge = loadBridge();
|
||||
const crops = [];
|
||||
const marginImage = {
|
||||
getSize() {
|
||||
return { width: 10, height: 10 };
|
||||
},
|
||||
toBitmap() {
|
||||
return bitmapWithOpaqueRect(10, 10, { x: 2, y: 2, width: 6, height: 6 });
|
||||
},
|
||||
crop(rect) {
|
||||
crops.push(rect);
|
||||
return { cropped: true, rect, isEmpty() { return false; } };
|
||||
},
|
||||
};
|
||||
const cropped = bridge.__cropTransparentMarginForTests(marginImage);
|
||||
assert.equal(cropped.cropped, true);
|
||||
assert.deepEqual(crops, [{ x: 1, y: 1, width: 8, height: 8 }]);
|
||||
|
||||
let cropCalls = 0;
|
||||
const fullBleed = {
|
||||
getSize() {
|
||||
return { width: 4, height: 4 };
|
||||
},
|
||||
toBitmap() {
|
||||
return bitmapWithOpaqueRect(4, 4, { x: 0, y: 0, width: 4, height: 4 });
|
||||
},
|
||||
crop() {
|
||||
cropCalls += 1;
|
||||
return { isEmpty() { return false; } };
|
||||
},
|
||||
};
|
||||
assert.equal(bridge.__cropTransparentMarginForTests(fullBleed), fullBleed);
|
||||
assert.equal(cropCalls, 0);
|
||||
});
|
||||
@@ -1171,13 +1171,21 @@ function registerWindowHandlers(ipcMain, nativeTheme) {
|
||||
ipcMain.handle("netcatty:setAppIconVariant", (_event, variant) => {
|
||||
const { app, BrowserWindow, nativeImage } = require("electron");
|
||||
const appIconManager = require("./appIconManager.cjs");
|
||||
return appIconManager.applyAppIconVariant(variant, {
|
||||
const applied = appIconManager.applyAppIconVariant(variant, {
|
||||
app,
|
||||
BrowserWindow,
|
||||
nativeImage,
|
||||
appPath: app.getAppPath(),
|
||||
isMac: process.platform === "darwin",
|
||||
});
|
||||
if (applied && process.platform === "win32") {
|
||||
try {
|
||||
getGlobalShortcutBridge().updateTrayIcon?.();
|
||||
} catch {
|
||||
// The tray is optional; the taskbar icon still updated.
|
||||
}
|
||||
}
|
||||
return applied;
|
||||
});
|
||||
|
||||
ipcMain.handle("netcatty:setLanguage", (_event, language) => {
|
||||
|
||||
Reference in new issue
Block a user