+3
-16
src/App.tsx
+3
-16
src/App.tsx
···
1
-
import { createSignal, For } from "solid-js";
1
+
import { For } from "solid-js";
2
2
3
-
import {
4
-
CheckIcon,
5
-
ChevronsUpDownIcon,
6
-
ClipboardIcon,
7
-
HeartIcon,
8
-
MicIcon,
9
-
Trash2Icon,
10
-
} from "lucide-solid";
3
+
import { CheckIcon, ChevronsUpDownIcon } from "lucide-solid";
11
4
import { Button } from "./components/ui/button";
12
5
import { Card } from "./components/ui/card";
13
6
import { Stack, Box, StackProps, HStack, VStack } from "styled-system/jsx";
14
7
import { FileUpload } from "./components/ui/file-upload";
15
-
import { IconButton } from "./components/ui/icon-button";
16
8
import { Text } from "./components/ui/text";
17
9
18
10
import { AtprotoDid } from "@atcute/lexicons/syntax";
···
30
22
import Settings from "./components/Settings";
31
23
import MicRecorder from "./components/MicRecorder";
32
24
import { Link } from "./components/ui/link";
33
-
import { css } from "styled-system/css";
34
-
import { toggleToRecord } from "./lib/settings";
35
25
36
26
const App = () => {
37
27
const collection = () =>
···
274
264
</Button>
275
265
)}
276
266
/>
277
-
<MicRecorder
278
-
selectedAccount={selectedAccount}
279
-
holdToRecord={!toggleToRecord()}
280
-
/>
267
+
<MicRecorder selectedAccount={selectedAccount} />
281
268
{/*<IconButton
282
269
size="sm"
283
270
onClick={() =>
+53
-26
src/components/MicRecorder.tsx
+53
-26
src/components/MicRecorder.tsx
···
9
9
10
10
type MicRecorderProps = {
11
11
selectedAccount: () => AtprotoDid | undefined;
12
-
holdToRecord?: boolean;
13
12
};
14
13
15
14
const MicRecorder = (props: MicRecorderProps) => {
···
23
22
let mediaStream: MediaStream | null = null;
24
23
let audioChunks: Blob[] = [];
25
24
25
+
// Flag to handle case where user releases hold before recording actually starts
26
+
let stopRequestPending = false;
27
+
26
28
const isSafari =
27
29
typeof navigator !== "undefined" &&
28
30
navigator.vendor &&
···
35
37
36
38
const startRecording = async () => {
37
39
if (isRecording()) return;
40
+
stopRequestPending = false;
38
41
39
42
try {
40
43
audioChunks = [];
···
55
58
echoCancellation: { ideal: true },
56
59
},
57
60
});
61
+
62
+
// check if holding stopped while waiting for permission/stream
63
+
if (stopRequestPending) {
64
+
mediaStream.getTracks().forEach((track) => track.stop());
65
+
mediaStream = null;
66
+
return;
67
+
}
68
+
58
69
const audioTrack = mediaStream.getAudioTracks()[0] ?? null;
59
70
if (!audioTrack) throw "no audio track found";
60
71
···
129
140
130
141
setIsRecording(true);
131
142
setRecordingStart(Date.now());
143
+
144
+
// delayed hold release
145
+
if (stopRequestPending) stopRecording();
132
146
} catch (error) {
133
147
console.error("error accessing microphone:", error);
134
148
toaster.create({
···
145
159
};
146
160
147
161
const stopRecording = () => {
148
-
if (!isRecording() || !mediaRecorder) return;
162
+
if (!isRecording() || !mediaRecorder) {
163
+
stopRequestPending = true;
164
+
return;
165
+
}
149
166
if (mediaRecorder.state !== "inactive") mediaRecorder.stop();
150
167
setIsRecording(false);
151
168
};
···
162
179
return `${mins}:${secs.toString().padStart(2, "0")}`;
163
180
};
164
181
182
+
let pressStartTime = 0;
183
+
let startedSession = false;
184
+
185
+
const handlePointerDown = (e: PointerEvent) => {
186
+
if (isRecording()) {
187
+
stopRecording();
188
+
startedSession = false;
189
+
} else {
190
+
startRecording();
191
+
pressStartTime = Date.now();
192
+
startedSession = true;
193
+
}
194
+
};
195
+
196
+
const handlePointerUp = (e: PointerEvent) => {
197
+
if (startedSession) {
198
+
const duration = Date.now() - pressStartTime;
199
+
if (duration >= 500) stopRecording();
200
+
201
+
startedSession = false;
202
+
}
203
+
};
204
+
205
+
const handlePointerLeave = (e: PointerEvent) => {
206
+
if (startedSession && isRecording()) {
207
+
stopRecording();
208
+
startedSession = false;
209
+
}
210
+
};
211
+
165
212
return (
166
213
<Popover.Root positioning={{ placement: "top" }} open={isRecording()}>
167
214
<Popover.Anchor
···
171
218
size="md"
172
219
variant={isRecording() ? "solid" : "subtle"}
173
220
colorPalette={isRecording() ? "red" : undefined}
174
-
onClick={
175
-
!props.holdToRecord
176
-
? () => (isRecording() ? stopRecording() : startRecording())
177
-
: undefined
178
-
}
179
-
onMouseDown={props.holdToRecord ? startRecording : undefined}
180
-
onMouseUp={props.holdToRecord ? stopRecording : undefined}
181
-
onMouseLeave={props.holdToRecord ? stopRecording : undefined}
182
-
onTouchStart={
183
-
props.holdToRecord
184
-
? (e) => {
185
-
e.preventDefault(); // Prevent mouse emulation
186
-
startRecording();
187
-
}
188
-
: undefined
189
-
}
190
-
onTouchEnd={
191
-
props.holdToRecord
192
-
? (e) => {
193
-
e.preventDefault();
194
-
stopRecording();
195
-
}
196
-
: undefined
197
-
}
221
+
onPointerDown={handlePointerDown}
222
+
onPointerUp={handlePointerUp}
223
+
onPointerLeave={handlePointerLeave}
224
+
onContextMenu={(e) => e.preventDefault()}
198
225
>
199
226
{isRecording() ? <CircleStopIcon /> : <MicIcon />}
200
227
</IconButton>
-18
src/components/Settings.tsx
-18
src/components/Settings.tsx
···
34
34
backgroundColor as backgroundColorSetting,
35
35
frameRate as frameRateSetting,
36
36
useDominantColorAsBg as useDominantColorAsBgSetting,
37
-
toggleToRecordSetting,
38
37
Setting,
39
-
toggleToRecord,
40
-
setToggleToRecord,
41
38
} from "~/lib/settings";
42
39
import { handleResolver } from "~/lib/at";
43
40
import { toaster } from "~/components/Toaster";
···
354
351
<Drawer.Body>
355
352
<Stack gap="4">
356
353
<Accounts />
357
-
<Stack>
358
-
<FormLabel>user interface</FormLabel>
359
-
<Stack
360
-
gap="0"
361
-
border="1px solid var(--colors-border-default)"
362
-
borderBottomWidth="3px"
363
-
rounded="xs"
364
-
>
365
-
<SettingCheckbox
366
-
label="use toggle to record"
367
-
setting={toggleToRecordSetting}
368
-
signal={[toggleToRecord, setToggleToRecord]}
369
-
/>
370
-
</Stack>
371
-
</Stack>
372
354
<Stack>
373
355
<FormLabel>processing</FormLabel>
374
356
<Stack
-14
src/lib/settings.ts
-14
src/lib/settings.ts
···
1
-
import { createSignal } from "solid-js";
2
-
3
1
export const setting = <T>(key: string) => {
4
2
return {
5
3
get: () => {
···
19
17
export const useDominantColorAsBg = setting<boolean>("useDominantColorAsBg");
20
18
export const backgroundColor = setting<string>("backgroundColor");
21
19
export const frameRate = setting<number>("frameRate");
22
-
23
-
export const toggleToRecordSetting = setting<boolean>("toggleToRecord");
24
-
const [_toggleToRecord, _setToggleToRecord] = createSignal<boolean>(
25
-
toggleToRecordSetting.get() ?? false,
26
-
);
27
-
export const toggleToRecord = _toggleToRecord;
28
-
export const setToggleToRecord = (
29
-
value: boolean | ((prev: boolean) => boolean),
30
-
) => {
31
-
const newAccounts = _setToggleToRecord(value);
32
-
toggleToRecordSetting.set(newAccounts);
33
-
};