Skip to content

Commit 3b54522

Browse files
author
Garrett Downs
committed
add playback speed setting
1 parent e44783a commit 3b54522

9 files changed

Lines changed: 76 additions & 18 deletions

File tree

src/contexts/SettingsProvider.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,26 @@ const defaultSettings: Settings = {
2323
accentColor: 'ec5817',
2424
notificationType: NotificationType.EpisodeInfo,
2525
notificationAction: NotificationAction.ViewPlayer,
26+
playbackSpeed: 1,
2627
};
2728

2829
type SettingsContextValue = {
2930
settings: Settings;
3031
setSettings: (settings: Settings) => void;
32+
setSetting: <T extends keyof Settings>(
33+
settingsKey: keyof Settings,
34+
val: Settings[T]
35+
) => void;
3136
};
3237

3338
const defaultValue: SettingsContextValue = {
3439
settings: defaultSettings,
3540
setSettings: (settings) => {
3641
console.log('default', settings);
3742
},
43+
setSetting: (settingsKey) => {
44+
console.log('default', settingsKey);
45+
},
3846
};
3947

4048
const SettingsContext = createContext<SettingsContextValue>(defaultValue);
@@ -54,11 +62,22 @@ export function SettingsProvider(props: SettingsProviderProps): VNode {
5462
setSettingsInternal(val);
5563
}
5664

65+
function setSetting<T extends keyof Settings>(
66+
key: T,
67+
val: Settings[T]
68+
): void {
69+
setSettings({
70+
...settings,
71+
[key]: val,
72+
});
73+
}
74+
5775
return (
5876
<SettingsContext.Provider
5977
value={{
6078
settings,
6179
setSettings,
80+
setSetting,
6281
}}
6382
>
6483
{props.children}

src/contexts/playerContext.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ export function PlayerProvider(props: ComponentBaseProps): VNode {
5959
const { showToast } = useToast();
6060
const { settings } = useSettings();
6161

62+
useEffect(() => {
63+
audioRef.playbackRate = settings.playbackSpeed;
64+
// eslint-disable-next-line react-hooks/exhaustive-deps
65+
}, [settings.playbackSpeed]);
66+
6267
function getStatus(): PlaybackStatus {
6368
return {
6469
playing: !audioRef.paused,

src/hooks/useDpad.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export function useDpad<T>({
3333
stopPropagation: true,
3434
scrollIntoView: true,
3535
disabled: false,
36-
mode: 'updownleftright',
36+
mode: 'updown',
3737
...options,
3838
};
3939

@@ -89,6 +89,7 @@ export function useDpad<T>({
8989

9090
if (options.stopPropagation) {
9191
ev.stopPropagation();
92+
ev.stopImmediatePropagation();
9293
ev.preventDefault();
9394
}
9495

src/models/Settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,5 @@ export type Settings = {
3333
accentColor: string;
3434
notificationType: NotificationType;
3535
notificationAction: NotificationAction;
36+
playbackSpeed: number;
3637
};

src/routes/AppSettings.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,10 @@ type SelectMenu = {
2323
};
2424

2525
export default function AppSettings(): VNode {
26-
const { settings, setSettings } = useSettings();
26+
const { settings, setSettings, setSetting } = useSettings();
2727
const [selectMenu, setSelectMenu] = useState<SelectMenu>();
2828
const accentColorRef = useRef<HTMLInputElement>(null);
2929

30-
function saveSetting(key: keyof Settings, value: any): void {
31-
setSettings({
32-
...settings,
33-
[key]: value,
34-
});
35-
}
36-
3730
function handleClick(id: keyof Settings): void {
3831
switch (id) {
3932
case 'theme':
@@ -89,7 +82,7 @@ export default function AppSettings(): VNode {
8982
});
9083
break;
9184
case 'accentColor':
92-
saveSetting('accentColor', accentColorRef.current?.value);
85+
setSetting('accentColor', accentColorRef.current?.value || '');
9386
break;
9487
}
9588
}
@@ -106,7 +99,10 @@ export default function AppSettings(): VNode {
10699
},
107100
});
108101

109-
function handleSettingSelect(key: keyof Settings, value: string): void {
102+
function handleSettingSelect<T extends keyof Settings>(
103+
key: T,
104+
value: Settings[T]
105+
): void {
110106
if (key === 'theme') {
111107
// We want to use the theme's original accent color
112108
const theme = themes.find((a) => a.id === value) as ThemeConfig;
@@ -116,7 +112,7 @@ export default function AppSettings(): VNode {
116112
accentColor: theme.values.appAccentColor.value.slice(1),
117113
});
118114
} else {
119-
saveSetting(key, value);
115+
setSetting(key, value);
120116
}
121117

122118
setSelectMenu(undefined);

src/routes/Player.module.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@
5959
flex: 1;
6060
}
6161

62+
.speed {
63+
padding: 0 5px;
64+
font-weight: 600;
65+
}
66+
.speed[data-selected] {
67+
color: var(--accent-text-color);
68+
}
69+
6270
.chevronDown {
6371
border-right: 2px solid;
6472
border-bottom: 2px solid;

src/routes/Player.tsx

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ import { Core } from '../services/core';
1212
import { ListItem, MenuOption, View } from '../ui-components';
1313
import { ifClass, joinClasses } from '../utils/classes';
1414
import styles from './Player.module.css';
15+
import { useSettings } from '../contexts/SettingsProvider';
16+
import { clamp } from '../utils/clamp';
1517

1618
export default function Player(): VNode {
1719
const [chapters, setChapters] = useState<Chapter[] | null>(null);
1820
const [podcast, setPodcast] = useState<Podcast | null>(null);
1921
const [browsingChapters, setBrowsingChapters] = useState(false);
22+
const [changingSpeed, setChangingSpeed] = useState(false);
2023
const [status, setStatus] = useState<PlaybackStatus>({
2124
playing: false,
2225
currentTime: 0,
@@ -25,6 +28,7 @@ export default function Player(): VNode {
2528
const [accentColor, setAccentColor] = useState<string>();
2629

2730
const player = usePlayer();
31+
const { settings, setSetting } = useSettings();
2832

2933
useEffect(() => {
3034
const episode = player.episode;
@@ -98,6 +102,18 @@ export default function Player(): VNode {
98102
case 'minus30':
99103
newStatus = player.jump(-30);
100104
break;
105+
case 'speedDown':
106+
setSetting(
107+
'playbackSpeed',
108+
clamp(Math.round((settings.playbackSpeed - 0.2) * 100) / 100, 0.6, 4)
109+
);
110+
break;
111+
case 'speedUp':
112+
setSetting(
113+
'playbackSpeed',
114+
clamp(Math.round((settings.playbackSpeed + 0.2) * 100) / 100, 0.6, 4)
115+
);
116+
break;
101117
case 'detail':
102118
route(`/episode/${player.episode.id}`);
103119
break;
@@ -115,8 +131,6 @@ export default function Player(): VNode {
115131
? { id: 'pause', label: 'Pause' }
116132
: { id: 'play', label: 'Play' },
117133
{ id: 'stop', label: 'Stop' },
118-
{ id: 'plus30', label: '+30 seconds' },
119-
{ id: 'minus30', label: '-30 seconds' },
120134
{ id: 'detail', label: 'View episode detail' },
121135
]
122136
: [];
@@ -132,14 +146,19 @@ export default function Player(): VNode {
132146
useDpad({
133147
priority: SelectablePriority.Low,
134148
onEnter: handleClick,
135-
onChange: (itemId) => setBrowsingChapters(!!itemId?.startsWith('chapter')),
136-
options: { mode: 'updown' },
149+
onChange: (itemId) => {
150+
setChangingSpeed(itemId === 'speed');
151+
setBrowsingChapters(!!itemId?.startsWith('chapter'));
152+
},
153+
options: { stopPropagation: false },
137154
});
138155

139156
useNavKeys(
140157
{
141-
ArrowLeft: () => handleAction('minus30'),
142-
ArrowRight: () => handleAction('plus30'),
158+
ArrowLeft: () =>
159+
changingSpeed ? handleAction('speedDown') : handleAction('minus30'),
160+
ArrowRight: () =>
161+
changingSpeed ? handleAction('speedUp') : handleAction('plus30'),
143162
Enter: () => {
144163
if (!player.episode) {
145164
return;
@@ -195,6 +214,11 @@ export default function Player(): VNode {
195214
{`${formatTime(status.duration)}`}
196215
</span>
197216
<div className={styles.spacer} />
217+
<span
218+
className={styles.speed}
219+
data-selectable-priority={SelectablePriority.Low}
220+
data-selectable-id="speed"
221+
>{`${settings.playbackSpeed}x`}</span>
198222
{chapters && chapters.length > 0 ? (
199223
<div className={styles.chevronDown} />
200224
) : null}

src/routes/Podcasts.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export default function Podcasts({ selectedItemId }: Props): VNode {
5050
route(`/podcasts/`, true);
5151
}
5252
},
53+
options: { mode: 'updownleftright' },
5354
});
5455

5556
async function seedData(): Promise<void> {

src/utils/clamp.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function clamp(num: number, min: number, max: number): number {
2+
return Math.min(Math.max(num, min), max);
3+
}

0 commit comments

Comments
 (0)