Skip to content

Commit c9cb77a

Browse files
authored
refactor(utils/system): alignment and jsdocs (#2735)
Signed-off-by: Adam Setch <adam.setch@outlook.com>
1 parent e0da133 commit c9cb77a

File tree

8 files changed

+161
-106
lines changed

8 files changed

+161
-106
lines changed

src/renderer/components/settings/SystemSettings.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
canIncreaseVolume,
2020
decreaseVolume,
2121
increaseVolume,
22-
} from '../../utils/system/audio';
22+
} from '../../utils/ui/volume';
2323
import { VolumeDownIcon } from '../icons/VolumeDownIcon';
2424
import { VolumeUpIcon } from '../icons/VolumeUpIcon';
2525

Lines changed: 8 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,16 @@
11
import type { Percentage } from '../../types';
22

3-
import {
4-
canDecreaseVolume,
5-
canIncreaseVolume,
6-
decreaseVolume,
7-
increaseVolume,
8-
volumePercentageToLevel,
9-
} from './audio';
3+
import { raiseSoundNotification } from './audio';
104

115
describe('renderer/utils/system/audio.ts', () => {
12-
it('should convert percentage to sound level', () => {
13-
expect(volumePercentageToLevel(100 as Percentage)).toBe(1);
14-
expect(volumePercentageToLevel(50 as Percentage)).toBe(0.5);
15-
expect(volumePercentageToLevel(0 as Percentage)).toBe(0);
16-
});
17-
18-
it('can decrease volume percentage', () => {
19-
expect(canDecreaseVolume(-10 as Percentage)).toBe(false);
20-
expect(canDecreaseVolume(0 as Percentage)).toBe(false);
21-
expect(canDecreaseVolume(10 as Percentage)).toBe(true);
22-
expect(canDecreaseVolume(100 as Percentage)).toBe(true);
23-
});
6+
describe('raiseSoundNotification', () => {
7+
it('should play sound at correct volume', async () => {
8+
const audioPlaySpy = vi.spyOn(Audio.prototype, 'play');
249

25-
it('should decrease volume by step amount', () => {
26-
expect(decreaseVolume(100 as Percentage)).toBe(90);
27-
expect(decreaseVolume(50 as Percentage)).toBe(40);
28-
expect(decreaseVolume(0 as Percentage)).toBe(0);
29-
expect(decreaseVolume(-10 as Percentage)).toBe(0);
30-
});
31-
32-
it('can increase volume percentage', () => {
33-
expect(canIncreaseVolume(10 as Percentage)).toBe(true);
34-
expect(canIncreaseVolume(90 as Percentage)).toBe(true);
35-
expect(canIncreaseVolume(100 as Percentage)).toBe(false);
36-
expect(canIncreaseVolume(110 as Percentage)).toBe(false);
37-
});
10+
await raiseSoundNotification(50 as Percentage);
3811

39-
it('should increase volume by step amount', () => {
40-
expect(increaseVolume(0 as Percentage)).toBe(10);
41-
expect(increaseVolume(50 as Percentage)).toBe(60);
42-
expect(increaseVolume(100 as Percentage)).toBe(100);
43-
expect(increaseVolume(110 as Percentage)).toBe(100);
12+
expect(window.gitify.notificationSoundPath).toHaveBeenCalled();
13+
expect(audioPlaySpy).toHaveBeenCalled();
14+
});
4415
});
4516
});

src/renderer/utils/system/audio.ts

Lines changed: 20 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,33 @@
11
import type { Percentage } from '../../types';
22

3-
const MINIMUM_VOLUME_PERCENTAGE = 0 as Percentage;
4-
const MAXIMUM_VOLUME_PERCENTAGE = 100 as Percentage;
5-
const VOLUME_STEP = 10 as Percentage;
3+
import { rendererLogError } from '../core/logger';
4+
import { volumePercentageToLevel } from '../ui/volume';
5+
6+
// Cache audio instance to avoid re-creating elements on every notification.
7+
let cachedAudio: HTMLAudioElement | null = null;
68

79
/**
8-
* Play the user's configured notification sound at the given volume.
9-
*
10-
* Resolves the notification sound file path from the main process, then
11-
* plays it via the Web Audio API.
10+
* Plays the notification sound at the specified volume.
11+
* The audio element is lazily created and cached to avoid re-creating it on every call.
12+
* The cache is cleared if playback fails so the next call can retry.
1213
*
13-
* @param volume - The playback volume as a percentage (0–100).
14+
* @param volume - The volume level to play the sound at, as a percentage (`0`–`100`).
1415
*/
1516
export async function raiseSoundNotification(volume: Percentage) {
16-
const path = await window.gitify.notificationSoundPath();
17-
18-
const audio = new Audio(path);
19-
audio.volume = volumePercentageToLevel(volume);
20-
audio.play();
21-
}
22-
23-
/**
24-
* Convert volume percentage (0-100) to level (0.0-1.0)
25-
*/
26-
export function volumePercentageToLevel(percentage: Percentage): number {
27-
return percentage / 100;
28-
}
17+
if (!cachedAudio) {
18+
const path = await window.gitify.notificationSoundPath();
2919

30-
/**
31-
* Returns true if can decrease volume percentage further
32-
*/
33-
export function canDecreaseVolume(volumePercentage: Percentage) {
34-
return volumePercentage - VOLUME_STEP >= MINIMUM_VOLUME_PERCENTAGE;
35-
}
36-
37-
/**
38-
* Returns true if can increase volume percentage further
39-
*/
40-
export function canIncreaseVolume(volumePercentage: Percentage) {
41-
return volumePercentage + VOLUME_STEP <= MAXIMUM_VOLUME_PERCENTAGE;
42-
}
43-
44-
/**
45-
* Decrease the volume by one step, clamped to the minimum.
46-
*
47-
* @param volume - The current volume percentage.
48-
* @returns The new volume percentage after decrement, or the minimum if already at the floor.
49-
*/
50-
export function decreaseVolume(volume: Percentage) {
51-
if (canDecreaseVolume(volume)) {
52-
return volume - VOLUME_STEP;
20+
cachedAudio = new Audio(path);
5321
}
5422

55-
return MINIMUM_VOLUME_PERCENTAGE;
56-
}
23+
const audio = cachedAudio;
5724

58-
/**
59-
* Increase the volume by one step, clamped to the maximum.
60-
*
61-
* @param volume - The current volume percentage.
62-
* @returns The new volume percentage after increment, or the maximum if already at the ceiling.
63-
*/
64-
export function increaseVolume(volume: Percentage) {
65-
if (canIncreaseVolume(volume)) {
66-
return volume + VOLUME_STEP;
67-
}
25+
audio.volume = volumePercentageToLevel(volume);
6826

69-
return MAXIMUM_VOLUME_PERCENTAGE;
27+
try {
28+
await audio.play();
29+
} catch (err) {
30+
rendererLogError('audio', 'Failed to play notification sound:', err);
31+
cachedAudio = null;
32+
}
7033
}

src/renderer/utils/system/comms.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,36 +28,36 @@ export function openExternalLink(url: Link): void {
2828
}
2929

3030
/**
31-
* Return the application version string from the main process.
31+
* Returns the current application version string.
3232
*
33-
* @returns The version string (e.g. `"5.12.0"`).
33+
* @returns Promise resolving to the app version (e.g. `"6.18.0"`).
3434
*/
3535
export async function getAppVersion(): Promise<string> {
3636
return await window.gitify.app.version();
3737
}
3838

3939
/**
40-
* Encrypt a plaintext value using Electron's safe storage.
40+
* Encrypts a plaintext string using the native Electron encryption bridge.
4141
*
4242
* @param value - The plaintext string to encrypt.
43-
* @returns The encrypted string.
43+
* @returns Promise resolving to the encrypted string.
4444
*/
4545
export async function encryptValue(value: string): Promise<string> {
4646
return await window.gitify.encryptValue(value);
4747
}
4848

4949
/**
50-
* Decrypt an encrypted value using Electron's safe storage.
50+
* Decrypts a previously encrypted string using the native Electron decryption bridge.
5151
*
5252
* @param value - The encrypted string to decrypt.
53-
* @returns The plaintext string.
53+
* @returns Promise resolving to the decrypted plaintext string.
5454
*/
5555
export async function decryptValue(value: string): Promise<string> {
5656
return await window.gitify.decryptValue(value);
5757
}
5858

5959
/**
60-
* Quit the Electron application.
60+
* Quit the application.
6161
*/
6262
export function quitApp(): void {
6363
window.gitify.app.quit();
@@ -78,7 +78,7 @@ export function hideWindow(): void {
7878
}
7979

8080
/**
81-
* Enable or disable launching the application at system login.
81+
* Enables or disables auto-launch of the application on system startup.
8282
*
8383
* @param value - `true` to enable auto-launch, `false` to disable.
8484
*/
@@ -105,9 +105,9 @@ export function setUseUnreadActiveIcon(value: boolean): void {
105105
}
106106

107107
/**
108-
* Register or unregister the global keyboard shortcut for the application.
108+
* Registers or unregisters the global keyboard shortcut to toggle the application window.
109109
*
110-
* @param keyboardShortcut - `true` to enable the shortcut, `false` to disable.
110+
* @param keyboardShortcut - `true` to register the shortcut, `false` to unregister.
111111
*/
112112
export function setKeyboardShortcut(keyboardShortcut: boolean): void {
113113
window.gitify.setKeyboardShortcut(keyboardShortcut);
@@ -133,6 +133,11 @@ export function updateTrayTitle(title: string): void {
133133
window.gitify.tray.updateTitle(title);
134134
}
135135

136+
/**
137+
* Copies the specified text to the system clipboard.
138+
*
139+
* @param text - The text to copy to the clipboard.
140+
*/
136141
export async function copyToClipboard(text: string): Promise<void> {
137142
await navigator.clipboard.writeText(text);
138143
}

src/renderer/utils/system/native.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ describe('renderer/utils/system/native.ts', () => {
2121
mockSingleAccountNotifications[0].notifications,
2222
);
2323

24-
// wait for async native handling (generateGitHubWebUrl) to complete
2524
await waitFor(() =>
2625
expect(window.gitify.raiseNativeNotification).toHaveBeenCalledTimes(1),
2726
);

src/renderer/utils/system/native.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ import type { GitifyNotification } from '../../types';
44

55
import { generateGitHubWebUrl } from '../notifications/url';
66

7+
/**
8+
* Raises a native OS notification.
9+
*
10+
* For a single notification, the message is used as the title (non-Windows) and the
11+
* formatted footer text is used as the body. For multiple notifications, a generic count
12+
* summary is shown instead.
13+
*
14+
* @param notifications - The notifications to surface as a native OS notification.
15+
*/
716
export async function raiseNativeNotification(
817
notifications: GitifyNotification[],
918
) {
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { Percentage } from '../../types';
2+
3+
import {
4+
canDecreaseVolume,
5+
canIncreaseVolume,
6+
decreaseVolume,
7+
increaseVolume,
8+
volumePercentageToLevel,
9+
} from './volume';
10+
11+
describe('renderer/utils/ui/volume.ts', () => {
12+
it('should convert percentage to sound level', () => {
13+
expect(volumePercentageToLevel(100 as Percentage)).toBe(1);
14+
expect(volumePercentageToLevel(50 as Percentage)).toBe(0.5);
15+
expect(volumePercentageToLevel(0 as Percentage)).toBe(0);
16+
});
17+
18+
it('can decrease volume percentage', () => {
19+
expect(canDecreaseVolume(-10 as Percentage)).toBe(false);
20+
expect(canDecreaseVolume(0 as Percentage)).toBe(false);
21+
expect(canDecreaseVolume(10 as Percentage)).toBe(true);
22+
expect(canDecreaseVolume(100 as Percentage)).toBe(true);
23+
});
24+
25+
it('should decrease volume by step amount', () => {
26+
expect(decreaseVolume(100 as Percentage)).toBe(90);
27+
expect(decreaseVolume(50 as Percentage)).toBe(40);
28+
expect(decreaseVolume(0 as Percentage)).toBe(0);
29+
expect(decreaseVolume(-10 as Percentage)).toBe(0);
30+
});
31+
32+
it('can increase volume percentage', () => {
33+
expect(canIncreaseVolume(10 as Percentage)).toBe(true);
34+
expect(canIncreaseVolume(90 as Percentage)).toBe(true);
35+
expect(canIncreaseVolume(100 as Percentage)).toBe(false);
36+
expect(canIncreaseVolume(110 as Percentage)).toBe(false);
37+
});
38+
39+
it('should increase volume by step amount', () => {
40+
expect(increaseVolume(0 as Percentage)).toBe(10);
41+
expect(increaseVolume(50 as Percentage)).toBe(60);
42+
expect(increaseVolume(100 as Percentage)).toBe(100);
43+
expect(increaseVolume(110 as Percentage)).toBe(100);
44+
});
45+
});

src/renderer/utils/ui/volume.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type { Percentage } from '../../types';
2+
3+
const MINIMUM_VOLUME_PERCENTAGE = 0 as Percentage;
4+
const MAXIMUM_VOLUME_PERCENTAGE = 100 as Percentage;
5+
const VOLUME_STEP = 10 as Percentage;
6+
7+
/**
8+
* Convert volume percentage (0-100) to level (0.0-1.0).
9+
*
10+
* @param percentage - Volume percentage in the range `0`–`100`.
11+
* @returns Volume level in the range `0.0`–`1.0`.
12+
*/
13+
export function volumePercentageToLevel(percentage: Percentage): number {
14+
return percentage / 100;
15+
}
16+
17+
/**
18+
* Returns `true` if the volume can be decreased by one step.
19+
*
20+
* @param volumePercentage - Current volume percentage.
21+
* @returns `true` if decreasing by one step would remain at or above the minimum, `false` otherwise.
22+
*/
23+
export function canDecreaseVolume(volumePercentage: Percentage) {
24+
return volumePercentage - VOLUME_STEP >= MINIMUM_VOLUME_PERCENTAGE;
25+
}
26+
27+
/**
28+
* Returns `true` if the volume can be increased by one step.
29+
*
30+
* @param volumePercentage - Current volume percentage.
31+
* @returns `true` if increasing by one step would remain at or below the maximum, `false` otherwise.
32+
*/
33+
export function canIncreaseVolume(volumePercentage: Percentage) {
34+
return volumePercentage + VOLUME_STEP <= MAXIMUM_VOLUME_PERCENTAGE;
35+
}
36+
37+
/**
38+
* Decreases the volume by one step, clamping to the minimum.
39+
*
40+
* @param volume - Current volume percentage.
41+
* @returns The new volume percentage after decreasing, or `0` if already at minimum.
42+
*/
43+
export function decreaseVolume(volume: Percentage): Percentage {
44+
if (canDecreaseVolume(volume)) {
45+
return (volume - VOLUME_STEP) as Percentage;
46+
}
47+
48+
return MINIMUM_VOLUME_PERCENTAGE;
49+
}
50+
51+
/**
52+
* Increases the volume by one step, clamping to the maximum.
53+
*
54+
* @param volume - Current volume percentage.
55+
* @returns The new volume percentage after increasing, or `100` if already at maximum.
56+
*/
57+
export function increaseVolume(volume: Percentage): Percentage {
58+
if (canIncreaseVolume(volume)) {
59+
return (volume + VOLUME_STEP) as Percentage;
60+
}
61+
62+
return MAXIMUM_VOLUME_PERCENTAGE;
63+
}

0 commit comments

Comments
 (0)