import { Tray, Menu, nativeImage, app } from 'electron' import { join } from 'node:path' import { showMainWindow } from './windows' import { forceCheck } from './scheduler' import { broadcastState, snoozeAll } from './state-actions' import { getSettings, updateSettings } from './store' import type { Language } from '@shared/types' let tray: Tray | null = null /** * Minimal tray-side localisation. The renderer's full i18n dict lives in * `src/renderer/src/i18n/dict.ts` and isn't reachable from the main process * tsconfig, so we keep the 5 strings the tray actually uses here. */ const TRAY_STRINGS: Record> = { ru: { open: 'Открыть', pause: 'Пауза напоминаний', resume: 'Возобновить напоминания', snooze15: 'Отложить все на 15 мин', quit: 'Выход' }, en: { open: 'Open', pause: 'Pause reminders', resume: 'Resume reminders', snooze15: 'Snooze all 15 min', quit: 'Quit' } } function trayLabel(key: string): string { // getSettings reads from cache; if the store hasn't loaded yet (very early // boot) it lazily reads from disk. Defaults to 'ru' if anything goes wrong. let lang: Language = 'ru' try { lang = getSettings().language ?? 'ru' } catch { /* keep default */ } return TRAY_STRINGS[lang]?.[key] ?? TRAY_STRINGS.ru[key] ?? key } function resolveTrayIcon(): Electron.NativeImage { // Try resources/, fallback to a transparent 16x16 if missing during dev. const candidates = [ join(process.resourcesPath, 'tray.png'), join(__dirname, '../../resources/tray.png'), join(app.getAppPath(), 'resources/tray.png') ] for (const p of candidates) { const img = nativeImage.createFromPath(p) if (!img.isEmpty()) return img } return nativeImage.createEmpty() } export function createTray(): Tray { if (tray) return tray const icon = resolveTrayIcon() tray = new Tray(icon) tray.setToolTip('Exercise Reminder') refreshMenu() tray.on('click', () => showMainWindow()) tray.on('double-click', () => showMainWindow()) return tray } export function refreshMenu(): void { if (!tray) return // Single source of truth — settings.globalEnabled. Раньше tray держал // отдельный scheduler-local `paused` flag, который не синхронизировался // с Dashboard'ом (там кнопка читает globalEnabled). Теперь оба пути // правят одно поле. const paused = !getSettings().globalEnabled const menu = Menu.buildFromTemplate([ { label: trayLabel('open'), click: () => showMainWindow() }, { type: 'separator' }, { label: paused ? trayLabel('resume') : trayLabel('pause'), click: () => { updateSettings({ globalEnabled: paused }) // toggle broadcastState() // чтобы Dashboard перерисовал кнопку сразу refreshMenu() if (paused) forceCheck() // resuming — догнать пропущенные fires } }, { label: trayLabel('snooze15'), click: () => snoozeAll(15) }, { type: 'separator' }, { label: trayLabel('quit'), click: () => { app.quit() } } ]) tray.setContextMenu(menu) } export function destroyTray(): void { if (tray) { tray.destroy() tray = null } }