Skip to content

Commit f272d28

Browse files
authored
refactor: percentage branded type with controls (#2357)
* refactor: percentage branded type with controls Signed-off-by: Adam Setch <adam.setch@outlook.com> * refactor: percentage branded type with controls Signed-off-by: Adam Setch <adam.setch@outlook.com> --------- Signed-off-by: Adam Setch <adam.setch@outlook.com>
1 parent 4b5ed0f commit f272d28

File tree

11 files changed

+213
-46
lines changed

11 files changed

+213
-46
lines changed

src/renderer/__mocks__/state-mocks.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
type Link,
1313
type NotificationSettingsState,
1414
OpenPreference,
15+
type Percentage,
1516
type SettingsState,
1617
type SystemSettingsState,
1718
Theme,
@@ -81,7 +82,7 @@ export const mockToken = 'token-123-456' as Token;
8182
const mockAppearanceSettings: AppearanceSettingsState = {
8283
theme: Theme.SYSTEM,
8384
increaseContrast: false,
84-
zoomPercentage: 100,
85+
zoomPercentage: 100 as Percentage,
8586
showAccountHeader: false,
8687
wrapNotificationTitle: false,
8788
};
@@ -111,7 +112,7 @@ const mockSystemSettings: SystemSettingsState = {
111112
keyboardShortcut: true,
112113
showNotifications: true,
113114
playSound: true,
114-
notificationVolume: 20,
115+
notificationVolume: 20 as Percentage,
115116
openAtStartup: false,
116117
};
117118

src/renderer/components/settings/AppearanceSettings.tsx

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ import {
1818
import { AppContext } from '../../context/App';
1919
import { Theme } from '../../types';
2020
import { hasMultipleAccounts } from '../../utils/auth/utils';
21-
import { zoomLevelToPercentage, zoomPercentageToLevel } from '../../utils/zoom';
21+
import {
22+
canDecreaseZoom,
23+
canIncreaseZoom,
24+
decreaseZoom,
25+
increaseZoom,
26+
zoomLevelToPercentage,
27+
} from '../../utils/zoom';
2228
import { Checkbox } from '../fields/Checkbox';
2329
import { FieldLabel } from '../fields/FieldLabel';
2430
import { Title } from '../primitives/Title';
@@ -117,13 +123,9 @@ export const AppearanceSettings: FC = () => {
117123
<IconButton
118124
aria-label="Zoom out"
119125
data-testid="settings-zoom-out"
126+
disabled={!canDecreaseZoom(zoomPercentage)}
120127
icon={ZoomOutIcon}
121-
onClick={() =>
122-
zoomPercentage > 0 &&
123-
window.gitify.zoom.setLevel(
124-
zoomPercentageToLevel(zoomPercentage - 10),
125-
)
126-
}
128+
onClick={() => decreaseZoom(zoomPercentage)}
127129
size="small"
128130
unsafeDisableTooltip={true}
129131
/>
@@ -135,13 +137,9 @@ export const AppearanceSettings: FC = () => {
135137
<IconButton
136138
aria-label="Zoom in"
137139
data-testid="settings-zoom-in"
140+
disabled={!canIncreaseZoom(zoomPercentage)}
138141
icon={ZoomInIcon}
139-
onClick={() =>
140-
zoomPercentage < 120 &&
141-
window.gitify.zoom.setLevel(
142-
zoomPercentageToLevel(zoomPercentage + 10),
143-
)
144-
}
142+
onClick={() => increaseZoom(zoomPercentage)}
145143
size="small"
146144
unsafeDisableTooltip={true}
147145
/>

src/renderer/components/settings/SystemSettings.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event';
33

44
import { mockAuth, mockSettings } from '../../__mocks__/state-mocks';
55
import { AppContext } from '../../context/App';
6+
import type { Percentage } from '../../types';
67
import { SystemSettings } from './SystemSettings';
78

89
describe('renderer/components/settings/SystemSettings.tsx', () => {
@@ -155,7 +156,7 @@ describe('renderer/components/settings/SystemSettings.tsx', () => {
155156
auth: mockAuth,
156157
settings: {
157158
...mockSettings,
158-
notificationVolume: 30,
159+
notificationVolume: 30 as Percentage,
159160
},
160161
updateSetting,
161162
}}

src/renderer/components/settings/SystemSettings.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ import { APPLICATION } from '../../../shared/constants';
88
import { AppContext } from '../../context/App';
99
import { defaultSettings } from '../../context/defaults';
1010
import { OpenPreference } from '../../types';
11+
import {
12+
canDecreaseVolume,
13+
canIncreaseVolume,
14+
decreaseVolume,
15+
increaseVolume,
16+
} from '../../utils/notifications/sound';
1117
import { Checkbox } from '../fields/Checkbox';
1218
import { RadioGroup } from '../fields/RadioGroup';
1319
import { VolumeDownIcon } from '../icons/VolumeDownIcon';
@@ -104,13 +110,13 @@ export const SystemSettings: FC = () => {
104110
<IconButton
105111
aria-label="Volume down"
106112
data-testid="settings-volume-down"
113+
disabled={!canDecreaseVolume(settings.notificationVolume)}
107114
icon={VolumeDownIcon}
108115
onClick={() => {
109-
const newVolume = Math.max(
110-
settings.notificationVolume - 10,
111-
10,
116+
updateSetting(
117+
'notificationVolume',
118+
decreaseVolume(settings.notificationVolume),
112119
);
113-
updateSetting('notificationVolume', newVolume);
114120
}}
115121
size="small"
116122
unsafeDisableTooltip={true}
@@ -123,13 +129,13 @@ export const SystemSettings: FC = () => {
123129
<IconButton
124130
aria-label="Volume up"
125131
data-testid="settings-volume-up"
132+
disabled={!canIncreaseVolume(settings.notificationVolume)}
126133
icon={VolumeUpIcon}
127134
onClick={() => {
128-
const newVolume = Math.min(
129-
settings.notificationVolume + 10,
130-
100,
135+
updateSetting(
136+
'notificationVolume',
137+
increaseVolume(settings.notificationVolume),
131138
);
132-
updateSetting('notificationVolume', newVolume);
133139
}}
134140
size="small"
135141
unsafeDisableTooltip={true}

src/renderer/context/defaults.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
GroupBy,
88
type NotificationSettingsState,
99
OpenPreference,
10+
type Percentage,
1011
type SettingsState,
1112
type SystemSettingsState,
1213
Theme,
@@ -20,7 +21,7 @@ export const defaultAuth: AuthState = {
2021
const defaultAppearanceSettings: AppearanceSettingsState = {
2122
theme: Theme.SYSTEM,
2223
increaseContrast: false,
23-
zoomPercentage: 100,
24+
zoomPercentage: 100 as Percentage,
2425
showAccountHeader: false,
2526
wrapNotificationTitle: false,
2627
};
@@ -50,7 +51,7 @@ const defaultSystemSettings: SystemSettingsState = {
5051
keyboardShortcut: true,
5152
showNotifications: true,
5253
playSound: true,
53-
notificationVolume: 20,
54+
notificationVolume: 20 as Percentage,
5455
openAtStartup: false,
5556
};
5657

src/renderer/hooks/useNotifications.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export const useNotifications = (): NotificationsState => {
101101

102102
if (diffNotifications.length > 0) {
103103
if (state.settings.playSound) {
104-
raiseSoundNotification(state.settings.notificationVolume / 100);
104+
raiseSoundNotification(state.settings.notificationVolume);
105105
}
106106

107107
if (state.settings.showNotifications) {

src/renderer/types.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export type SearchToken = Branded<string, 'SearchToken'>;
3636

3737
export type Status = 'loading' | 'success' | 'error';
3838

39+
export type Percentage = Branded<number, 'Percentage'>;
40+
3941
export interface Account {
4042
method: AuthMethod;
4143
platform: PlatformType;
@@ -53,6 +55,7 @@ export type SettingsValue =
5355
| FilterValue[]
5456
| GroupBy
5557
| OpenPreference
58+
| Percentage
5659
| Theme;
5760

5861
export type FilterValue =
@@ -71,7 +74,7 @@ export type SettingsState = AppearanceSettingsState &
7174
export interface AppearanceSettingsState {
7275
theme: Theme;
7376
increaseContrast: boolean;
74-
zoomPercentage: number;
77+
zoomPercentage: Percentage;
7578
showAccountHeader: boolean;
7679
wrapNotificationTitle: boolean;
7780
}
@@ -101,7 +104,7 @@ export interface SystemSettingsState {
101104
keyboardShortcut: boolean;
102105
showNotifications: boolean;
103106
playSound: boolean;
104-
notificationVolume: number;
107+
notificationVolume: Percentage;
105108
openAtStartup: boolean;
106109
}
107110

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { Percentage } from '../../types';
2+
import {
3+
canDecreaseVolume,
4+
canIncreaseVolume,
5+
decreaseVolume,
6+
increaseVolume,
7+
volumePercentageToLevel,
8+
} from './sound';
9+
10+
describe('renderer/utils/notifications/sound.ts', () => {
11+
it('should convert percentage to sound level', () => {
12+
expect(volumePercentageToLevel(100 as Percentage)).toBe(1);
13+
expect(volumePercentageToLevel(50 as Percentage)).toBe(0.5);
14+
expect(volumePercentageToLevel(0 as Percentage)).toBe(0);
15+
});
16+
17+
it('can decrease volume percentage', () => {
18+
expect(canDecreaseVolume(-10 as Percentage)).toBe(false);
19+
expect(canDecreaseVolume(0 as Percentage)).toBe(false);
20+
expect(canDecreaseVolume(10 as Percentage)).toBe(true);
21+
expect(canDecreaseVolume(100 as Percentage)).toBe(true);
22+
});
23+
24+
it('should decrease volume by step amount', () => {
25+
expect(decreaseVolume(100 as Percentage)).toBe(90);
26+
expect(decreaseVolume(50 as Percentage)).toBe(40);
27+
expect(decreaseVolume(0 as Percentage)).toBe(0);
28+
expect(decreaseVolume(-10 as Percentage)).toBe(0);
29+
});
30+
31+
it('can increase volume percentage', () => {
32+
expect(canIncreaseVolume(10 as Percentage)).toBe(true);
33+
expect(canIncreaseVolume(90 as Percentage)).toBe(true);
34+
expect(canIncreaseVolume(100 as Percentage)).toBe(false);
35+
expect(canIncreaseVolume(110 as Percentage)).toBe(false);
36+
});
37+
38+
it('should increase volume by step amount', () => {
39+
expect(increaseVolume(0 as Percentage)).toBe(10);
40+
expect(increaseVolume(50 as Percentage)).toBe(60);
41+
expect(increaseVolume(100 as Percentage)).toBe(100);
42+
expect(increaseVolume(110 as Percentage)).toBe(100);
43+
});
44+
});
Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,56 @@
1-
export async function raiseSoundNotification(volume: number) {
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+
export async function raiseSoundNotification(volume: Percentage) {
28
const path = await globalThis.gitify.notificationSoundPath();
39

410
const audio = new Audio(path);
5-
audio.volume = volume;
11+
audio.volume = volumePercentageToLevel(volume);
612
audio.play();
713
}
14+
15+
/**
16+
* Convert volume percentage (0-100) to level (0.0-1.0)
17+
*/
18+
export function volumePercentageToLevel(percentage: Percentage): number {
19+
return percentage / 100;
20+
}
21+
22+
/**
23+
* Returns true if can decrease volume percentage further
24+
*/
25+
export function canDecreaseVolume(volumePercentage: Percentage) {
26+
return volumePercentage - VOLUME_STEP >= MINIMUM_VOLUME_PERCENTAGE;
27+
}
28+
29+
/**
30+
* Returns true if can increase volume percentage further
31+
*/
32+
export function canIncreaseVolume(volumePercentage: Percentage) {
33+
return volumePercentage + VOLUME_STEP <= MAXIMUM_VOLUME_PERCENTAGE;
34+
}
35+
36+
/**
37+
* Decrease volume by step amount
38+
*/
39+
export function decreaseVolume(volume: Percentage) {
40+
if (canDecreaseVolume(volume)) {
41+
return volume - VOLUME_STEP;
42+
}
43+
44+
return MINIMUM_VOLUME_PERCENTAGE;
45+
}
46+
47+
/**
48+
* Increase volume by step amount
49+
*/
50+
export function increaseVolume(volume: Percentage) {
51+
if (canIncreaseVolume(volume)) {
52+
return volume + VOLUME_STEP;
53+
}
54+
55+
return MAXIMUM_VOLUME_PERCENTAGE;
56+
}

src/renderer/utils/zoom.test.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1-
import { zoomLevelToPercentage, zoomPercentageToLevel } from './zoom';
1+
import type { Percentage } from '../types';
2+
import {
3+
canDecreaseZoom,
4+
canIncreaseZoom,
5+
zoomLevelToPercentage,
6+
zoomPercentageToLevel,
7+
} from './zoom';
28

39
describe('renderer/utils/zoom.ts', () => {
410
it('should convert percentage to zoom level', () => {
5-
expect(zoomPercentageToLevel(100)).toBe(0);
6-
expect(zoomPercentageToLevel(50)).toBe(-1);
7-
expect(zoomPercentageToLevel(0)).toBe(-2);
8-
expect(zoomPercentageToLevel(150)).toBe(1);
11+
expect(zoomPercentageToLevel(100 as Percentage)).toBe(0);
12+
expect(zoomPercentageToLevel(50 as Percentage)).toBe(-1);
13+
expect(zoomPercentageToLevel(0 as Percentage)).toBe(-2);
14+
expect(zoomPercentageToLevel(150 as Percentage)).toBe(1);
915

10-
expect(zoomPercentageToLevel(undefined)).toBe(0);
16+
expect(zoomPercentageToLevel(undefined as unknown as Percentage)).toBe(0);
1117
});
1218

1319
it('should convert zoom level to percentage', () => {
@@ -18,4 +24,17 @@ describe('renderer/utils/zoom.ts', () => {
1824

1925
expect(zoomLevelToPercentage(undefined)).toBe(100);
2026
});
27+
28+
it('can decrease zoom percentage', () => {
29+
expect(canDecreaseZoom(-10 as Percentage)).toBe(false);
30+
expect(canDecreaseZoom(0 as Percentage)).toBe(false);
31+
expect(canDecreaseZoom(10 as Percentage)).toBe(true);
32+
});
33+
34+
it('can increase zoom percentage', () => {
35+
expect(canIncreaseZoom(10 as Percentage)).toBe(true);
36+
expect(canIncreaseZoom(110 as Percentage)).toBe(true);
37+
expect(canIncreaseZoom(120 as Percentage)).toBe(false);
38+
expect(canIncreaseZoom(150 as Percentage)).toBe(false);
39+
});
2140
});

0 commit comments

Comments
 (0)