diff --git a/electron/main.js b/electron/main.js
index a110440..1bf1f1b 100644
--- a/electron/main.js
+++ b/electron/main.js
@@ -4,38 +4,107 @@ const fs = require('fs')
const ffmpegManager = require('./ffmpeg')
const deviceManager = require('./devices')
-// Inline tray icon as SVG → base64 DataURL (no external file needed)
-function makeTrayIcon() {
- const size = 16
- const svg = ``
- const dataUrl = `data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}`
- const img = nativeImage.createFromDataURL(dataUrl)
- if (process.platform === 'darwin') {
- img.setTemplateImage(true)
+// ─── Icon helpers ──────────────────────────────────────────────────────────────
+// Draw a pixel into an RGBA buffer
+function setPixel(buf, x, y, W, r, g, b, a = 255) {
+ if (x < 0 || y < 0 || x >= W || y >= W) return
+ const i = (y * W + x) * 4
+ buf[i] = r; buf[i+1] = g; buf[i+2] = b; buf[i+3] = a
+}
+
+// Anti-aliased circle helper: returns alpha 0-255 for a pixel at (px,py)
+// relative to circle center (cx,cy) with radius r
+function circleAlpha(px, py, cx, cy, r) {
+ const d = Math.sqrt((px - cx) ** 2 + (py - cy) ** 2)
+ if (d > r + 0.7) return 0
+ if (d < r - 0.7) return 255
+ return Math.round((r + 0.7 - d) / 1.4 * 255)
+}
+
+// Generate tray icon as raw RGBA buffer — works on all platforms
+// SVG DataURL does NOT work on Windows Electron tray, RGBA buffer does.
+function makeTrayIconBuffer(size = 16) {
+ const buf = Buffer.alloc(size * size * 4, 0)
+
+ for (let y = 0; y < size; y++) {
+ for (let x = 0; x < size; x++) {
+ // Rounded background
+ const cx = size / 2 - 0.5, cy = size / 2 - 0.5, r = size / 2 - 0.5
+ const dx = x - cx, dy = y - cy
+ if (dx * dx + dy * dy > r * r) continue // outside circle → transparent
+
+ // Dark background
+ setPixel(buf, x, y, size, 20, 20, 38, 255)
+
+ // Play triangle: points (3, 2), (3, 13), (12, 7.5)
+ const tL = 3, tT = 2, tB = size - 2, tR = size - 3
+ const midY = (tT + tB) / 2
+ // A point is inside the triangle if it satisfies all three half-plane tests
+ const cross1 = (tR - tL) * (y - tT) - (midY - tT) * (x - tL) // left-top to right
+ const cross2 = (tL - tR) * (y - midY) - (tT - midY) * (x - tR) // right to left-bot
+ const cross3 = (0) * (y - tB) - (tT - tB) * (x - tL) // left edge
+ // Simplified inside-triangle check
+ const inTri = x >= tL && x <= tR &&
+ y >= (tT + (midY - tT) / (tR - tL) * (x - tL) - 0.5) &&
+ y <= (tB - (tB - midY) / (tR - tL) * (x - tL) + 0.5)
+ if (inTri) {
+ setPixel(buf, x, y, size, 79, 142, 247, 255) // accent blue
+ continue
+ }
+
+ // Red live dot — top right quadrant
+ const dotCX = size * 0.78, dotCY = size * 0.22, dotR = size * 0.18
+ const da = circleAlpha(x, y, dotCX, dotCY, dotR)
+ if (da > 0) {
+ // Inner white
+ const innerA = circleAlpha(x, y, dotCX, dotCY, dotR * 0.5)
+ if (innerA > 128) setPixel(buf, x, y, size, 255, 255, 255, innerA)
+ else setPixel(buf, x, y, size, 239, 68, 68, da)
+ }
+ }
}
+ return buf
+}
+
+function makeTrayIcon() {
+ // 1. Try to load pre-generated file (faster, better quality)
+ const pngPaths = [
+ path.join(__dirname, '../assets/tray-icon.png'),
+ path.join(process.resourcesPath || '', '../assets/tray-icon.png')
+ ]
+ for (const p of pngPaths) {
+ if (fs.existsSync(p)) {
+ try {
+ const img = nativeImage.createFromPath(p)
+ if (!img.isEmpty()) {
+ if (process.platform === 'darwin') img.setTemplateImage(true)
+ return img
+ }
+ } catch {}
+ }
+ }
+
+ // 2. Fallback: create from raw RGBA buffer (always works, no file needed)
+ const size = process.platform === 'darwin' ? 22 : 16
+ const buf = makeTrayIconBuffer(size)
+ const img = nativeImage.createFromBuffer(buf, { width: size, height: size })
+ if (process.platform === 'darwin') img.setTemplateImage(true)
return img
}
-// Inline icon for main window (32x32)
function makeWindowIcon() {
- const svg = ``
- const dataUrl = `data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}`
- return nativeImage.createFromDataURL(dataUrl)
+ // Use pre-generated icon.png if available
+ const pngPath = path.join(__dirname, '../assets/icon.png')
+ if (fs.existsSync(pngPath)) {
+ try {
+ const img = nativeImage.createFromPath(pngPath)
+ if (!img.isEmpty()) return img
+ } catch {}
+ }
+ // Fallback: 32×32 RGBA buffer
+ const size = 32
+ const buf = makeTrayIconBuffer(size)
+ return nativeImage.createFromBuffer(buf, { width: size, height: size })
}
const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged
@@ -125,6 +194,29 @@ function createTray() {
})
}
+function confirmAndQuit() {
+ const activeCount = ffmpegManager.getActiveCount()
+ const detail = activeCount > 0
+ ? `Активных потоков: ${activeCount}. Все будут остановлены.`
+ : 'Приложение будет закрыто.'
+
+ const choice = dialog.showMessageBoxSync(mainWindow || undefined, {
+ type: 'question',
+ buttons: ['Выйти', 'Отмена'],
+ defaultId: 1,
+ cancelId: 1,
+ title: 'SRT Streamer — выход',
+ message: 'Вы уверены, что хотите выйти?',
+ detail
+ })
+
+ if (choice === 0) {
+ isQuitting = true
+ ffmpegManager.stopAllStreams()
+ app.quit()
+ }
+}
+
function showWindow() {
if (mainWindow) {
mainWindow.show()
@@ -138,17 +230,17 @@ function showWindow() {
function updateTrayMenu(activeCount = 0) {
if (!tray) return
const statusLabel = activeCount > 0
- ? `Active streams: ${activeCount}`
- : 'No active streams'
+ ? `▶ Активных потоков: ${activeCount}`
+ : '⬛ Нет активных потоков'
const contextMenu = Menu.buildFromTemplate([
{ label: 'SRT Streamer', enabled: false },
{ label: statusLabel, enabled: false },
{ type: 'separator' },
- { label: 'Show', click: () => showWindow() },
+ { label: '📂 Открыть', click: () => showWindow() },
{ type: 'separator' },
{
- label: 'Stop All Streams',
+ label: '⏹ Остановить все потоки',
enabled: activeCount > 0,
click: () => {
ffmpegManager.stopAllStreams()
@@ -156,27 +248,7 @@ function updateTrayMenu(activeCount = 0) {
}
},
{ type: 'separator' },
- {
- label: 'Quit',
- click: () => {
- const activeCount = ffmpegManager.getActiveCount()
- if (activeCount > 0) {
- const choice = dialog.showMessageBoxSync({
- type: 'question',
- buttons: ['Quit', 'Cancel'],
- defaultId: 1,
- cancelId: 1,
- title: 'Quit SRT Streamer',
- message: `${activeCount} stream(s) are active.`,
- detail: 'All running streams will be stopped. Are you sure you want to quit?'
- })
- if (choice !== 0) return
- }
- isQuitting = true
- ffmpegManager.stopAllStreams()
- app.quit()
- }
- }
+ { label: '✖ Выход', click: () => confirmAndQuit() }
])
tray.setContextMenu(contextMenu)
}