creates video voice memos from audio clips; with bluesky integration. trill.ptr.pet

feat: nah make it toggle to record on tap, hold otherwise

ptr.pet cb193787 cf4b246c

verified
Changed files
+56 -74
src
+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
··· 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
··· 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
··· 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 - };