aboutsummaryrefslogtreecommitdiffstats
path: root/src/lib/Tray.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/Tray.ts')
-rw-r--r--src/lib/Tray.ts265
1 files changed, 265 insertions, 0 deletions
diff --git a/src/lib/Tray.ts b/src/lib/Tray.ts
new file mode 100644
index 000000000..8e489edde
--- /dev/null
+++ b/src/lib/Tray.ts
@@ -0,0 +1,265 @@
1import {
2 app,
3 Menu,
4 nativeImage,
5 nativeTheme,
6 systemPreferences,
7 Tray,
8 ipcMain,
9 BrowserWindow,
10 NativeImage,
11} from 'electron';
12import { join } from 'path';
13import macosVersion from 'macos-version';
14import { isMac, isWindows, isLinux } from '../environment';
15
16const FILE_EXTENSION = isWindows ? 'ico' : 'png';
17const INDICATOR_TRAY_PLAIN = 'tray';
18const INDICATOR_TRAY_UNREAD = 'tray-unread';
19const INDICATOR_TRAY_INDIRECT = 'tray-indirect';
20
21// TODO: Need to support i18n for a lot of the hard-coded strings in this file
22export default class TrayIcon {
23 trayIcon: Tray | null = null;
24
25 indicator: string | number = 0;
26
27 themeChangeSubscriberId: number | null = null;
28
29 trayMenu: Menu | null = null;
30
31 visible = false;
32
33 isAppMuted = false;
34
35 mainWindow: BrowserWindow | null = null;
36
37 constructor() {
38 ipcMain.on('initialAppSettings', (_, appSettings) => {
39 this._updateTrayMenu(appSettings);
40 });
41 ipcMain.on('updateAppSettings', (_, appSettings) => {
42 this._updateTrayMenu(appSettings);
43 });
44
45 const [firstWindow] = BrowserWindow.getAllWindows();
46 this.mainWindow = firstWindow;
47
48 // listen to window events to be able to set correct string
49 // to tray menu ('Hide Ferdium' / 'Show Ferdium')
50 this.mainWindow.on('hide', () => {
51 this._updateTrayMenu(null);
52 });
53 this.mainWindow.on('restore', () => {
54 this._updateTrayMenu(null);
55 });
56 this.mainWindow.on('minimize', () => {
57 this._updateTrayMenu(null);
58 });
59 this.mainWindow.on('show', () => {
60 this._updateTrayMenu(null);
61 });
62 this.mainWindow.on('focus', () => {
63 this._updateTrayMenu(null);
64 });
65 this.mainWindow.on('blur', () => {
66 this._updateTrayMenu(null);
67 });
68 }
69
70 trayMenuTemplate(tray) {
71 return [
72 {
73 label:
74 tray.mainWindow.isVisible() && tray.mainWindow.isFocused()
75 ? 'Hide Ferdium'
76 : 'Show Ferdium',
77 click() {
78 tray._toggleWindow();
79 },
80 },
81 {
82 label: tray.isAppMuted
83 ? 'Enable Notifications && Audio'
84 : 'Disable Notifications && Audio',
85 click() {
86 if (!tray.mainWindow) return;
87 tray.mainWindow.webContents.send('muteApp');
88 },
89 },
90 {
91 label: 'Quit Ferdium',
92 click() {
93 app.quit();
94 },
95 },
96 ];
97 }
98
99 _updateTrayMenu(appSettings): void {
100 if (!this.trayIcon) return;
101
102 if (appSettings && appSettings.type === 'app') {
103 this.isAppMuted = appSettings.data.isAppMuted; // save current state after a change
104 }
105
106 this.trayMenu = Menu.buildFromTemplate(this.trayMenuTemplate(this));
107 if (isLinux) {
108 this.trayIcon.setContextMenu(this.trayMenu);
109 }
110 }
111
112 show(): void {
113 this.visible = true;
114 this._show();
115 }
116
117 _show(): void {
118 if (this.trayIcon) {
119 return;
120 }
121
122 this.trayIcon = new Tray(this._getAsset('tray', INDICATOR_TRAY_PLAIN));
123 this.trayIcon.setToolTip('Ferdium');
124
125 this.trayMenu = Menu.buildFromTemplate(this.trayMenuTemplate(this));
126 if (isLinux) {
127 this.trayIcon.setContextMenu(this.trayMenu);
128 }
129
130 this.trayIcon.on('click', () => {
131 this._toggleWindow();
132 });
133
134 if (isMac || isWindows) {
135 this.trayIcon.on('right-click', () => {
136 if (this.trayIcon && this.trayMenu) {
137 this.trayIcon.popUpContextMenu(this.trayMenu);
138 }
139 });
140 }
141
142 if (isMac) {
143 this.themeChangeSubscriberId = systemPreferences.subscribeNotification(
144 'AppleInterfaceThemeChangedNotification',
145 () => {
146 this._refreshIcon();
147 },
148 );
149 }
150 }
151
152 _toggleWindow(): void {
153 const [mainWindow] = BrowserWindow.getAllWindows();
154 if (!mainWindow) {
155 return;
156 }
157
158 if (mainWindow.isMinimized()) {
159 mainWindow.restore();
160 } else if (mainWindow.isVisible() && mainWindow.isFocused()) {
161 if (isMac && mainWindow.isFullScreen()) {
162 mainWindow.once('show', () => mainWindow?.setFullScreen(true));
163 mainWindow.once('leave-full-screen', () => mainWindow?.hide());
164 mainWindow.setFullScreen(false);
165 } else {
166 mainWindow.hide();
167 }
168 } else {
169 mainWindow.show();
170 mainWindow.focus();
171 }
172 }
173
174 hide(): void {
175 this.visible = false;
176 this._hide();
177 }
178
179 _hide(): void {
180 if (!this.trayIcon) return;
181
182 this.trayIcon.destroy();
183 this.trayIcon = null;
184
185 if (isMac && this.themeChangeSubscriberId) {
186 systemPreferences.unsubscribeNotification(this.themeChangeSubscriberId);
187 this.themeChangeSubscriberId = null;
188 }
189 }
190
191 recreateIfVisible(): void {
192 if (this.visible) {
193 this._hide();
194 setTimeout(() => {
195 if (this.visible) {
196 this._show();
197 }
198 }, 100);
199 }
200 }
201
202 setIndicator(indicator: string | number): void {
203 this.indicator = indicator;
204 this._refreshIcon();
205 }
206
207 _getAssetFromIndicator(indicator: string | number): string {
208 let assetFromIndicator = INDICATOR_TRAY_PLAIN;
209 if (indicator === '•') {
210 assetFromIndicator = INDICATOR_TRAY_INDIRECT;
211 }
212 if (indicator !== 0) {
213 assetFromIndicator = INDICATOR_TRAY_UNREAD;
214 }
215 return assetFromIndicator;
216 }
217
218 _refreshIcon(): void {
219 if (!this.trayIcon) {
220 return;
221 }
222
223 this.trayIcon.setImage(
224 this._getAsset('tray', this._getAssetFromIndicator(this.indicator)),
225 );
226
227 if (isMac && !macosVersion.isGreaterThanOrEqualTo('11')) {
228 this.trayIcon.setPressedImage(
229 this._getAsset(
230 'tray',
231 `${this._getAssetFromIndicator(this.indicator)}-active`,
232 ),
233 );
234 }
235 }
236
237 _getAsset(type, asset): NativeImage {
238 const { platform } = process;
239 let platformPath: string = platform;
240
241 if (isMac && macosVersion.isGreaterThanOrEqualTo('11')) {
242 platformPath = `${platform}-20`;
243 } else if (isMac && nativeTheme.shouldUseDarkColors) {
244 platformPath = `${platform}-dark`;
245 }
246
247 const trayImg = nativeImage.createFromPath(
248 join(
249 __dirname,
250 '..',
251 'assets',
252 'images',
253 type,
254 platformPath,
255 `${asset}.${FILE_EXTENSION}`,
256 ),
257 );
258
259 if (isMac && macosVersion.isGreaterThanOrEqualTo('11')) {
260 trayImg.setTemplateImage(true);
261 }
262
263 return trayImg;
264 }
265}