feat: nah make it toggle to record on tap, hold otherwise
11234567891034567158910119 unmodified lines21222324333425262728234 unmodified linesExpand all26326426526627727827928026726826927027114 unmodified linesimport { createSignal, For } from "solid-js";import { For } from "solid-js";
import { CheckIcon, ChevronsUpDownIcon, ClipboardIcon, HeartIcon, MicIcon, Trash2Icon,} from "lucide-solid";import { CheckIcon, ChevronsUpDownIcon } from "lucide-solid";import { Button } from "./components/ui/button";import { Card } from "./components/ui/card";import { Stack, Box, StackProps, HStack, VStack } from "styled-system/jsx";import { FileUpload } from "./components/ui/file-upload";import { IconButton } from "./components/ui/icon-button";import { Text } from "./components/ui/text";
import { AtprotoDid } from "@atcute/lexicons/syntax";import {9 unmodified linesimport Task from "./components/FileTask";import Settings from "./components/Settings";import MicRecorder from "./components/MicRecorder";import { Link } from "./components/ui/link";import { css } from "styled-system/css";import { toggleToRecord } from "./lib/settings";
const App = () => { const collection = () => createListCollection({234 unmodified linesExpand all or pick file </Button> )} /> <MicRecorder selectedAccount={selectedAccount} holdToRecord={!toggleToRecord()} /> <MicRecorder selectedAccount={selectedAccount} /> {/*<IconButton size="sm" onClick={() => getAudioClipboard().then((file) => {14 unmodified lines7 unmodified lines89101112121314155 unmodified lines21222324252627282930314 unmodified lines36373839404142434412 unmodified lines5758596061626364656667686970717266 unmodified lines1391401411421431441451461471481498 unmodified lines1581591601611481621631641651661671681698 unmodified lines1781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142151 unmodified line21721821922017417517617717817918018118218318418518618718818919019119219319419519619722122222322422522622722811 unmodified lines7 unmodified linesimport { createTimeDifferenceFromNow } from "@solid-primitives/date";
type MicRecorderProps = { selectedAccount: () => AtprotoDid | undefined; holdToRecord?: boolean;};
const MicRecorder = (props: MicRecorderProps) => { const [isRecording, setIsRecording] = createSignal(false);5 unmodified lines let mediaRecorder: MediaRecorder | null = null; let mediaStream: MediaStream | null = null; let audioChunks: Blob[] = [];
// Flag to handle case where user releases hold before recording actually starts let stopRequestPending = false;
const isSafari = typeof navigator !== "undefined" && navigator.vendor && navigator.vendor.indexOf("Apple") > -1;4 unmodified lines const fallbackMimeType = isSafari ? "audio/mp4" : "audio/webm";
const startRecording = async () => { if (isRecording()) return; stopRequestPending = false;
try { audioChunks = [];
12 unmodified lines noiseSuppression: { ideal: true }, echoCancellation: { ideal: true }, }, });
// check if holding stopped while waiting for permission/stream if (stopRequestPending) { mediaStream.getTracks().forEach((track) => track.stop()); mediaStream = null; return; }
const audioTrack = mediaStream.getAudioTracks()[0] ?? null; if (!audioTrack) throw "no audio track found";
let mimeType = "";66 unmodified lines mediaRecorder.start();
setIsRecording(true); setRecordingStart(Date.now());
// delayed hold release if (stopRequestPending) stopRecording(); } catch (error) { console.error("error accessing microphone:", error); toaster.create({ title: "error starting recording",8 unmodified lines } };
const stopRecording = () => { if (!isRecording() || !mediaRecorder) return; if (!isRecording() || !mediaRecorder) { stopRequestPending = true; return; } if (mediaRecorder.state !== "inactive") mediaRecorder.stop(); setIsRecording(false); };
8 unmodified lines const secs = seconds % 60; return `${mins}:${secs.toString().padStart(2, "0")}`; };
let pressStartTime = 0; let startedSession = false;
const handlePointerDown = (e: PointerEvent) => { if (isRecording()) { stopRecording(); startedSession = false; } else { startRecording(); pressStartTime = Date.now(); startedSession = true; } };
const handlePointerUp = (e: PointerEvent) => { if (startedSession) { const duration = Date.now() - pressStartTime; if (duration >= 500) stopRecording();
startedSession = false; } };
const handlePointerLeave = (e: PointerEvent) => { if (startedSession && isRecording()) { stopRecording(); startedSession = false; } };
return ( <Popover.Root positioning={{ placement: "top" }} open={isRecording()}> <Popover.Anchor asChild={(anchorProps) => (1 unmodified line {...anchorProps()} size="md" variant={isRecording() ? "solid" : "subtle"} colorPalette={isRecording() ? "red" : undefined} onClick={ !props.holdToRecord ? () => (isRecording() ? stopRecording() : startRecording()) : undefined } onMouseDown={props.holdToRecord ? startRecording : undefined} onMouseUp={props.holdToRecord ? stopRecording : undefined} onMouseLeave={props.holdToRecord ? stopRecording : undefined} onTouchStart={ props.holdToRecord ? (e) => { e.preventDefault(); // Prevent mouse emulation startRecording(); } : undefined } onTouchEnd={ props.holdToRecord ? (e) => { e.preventDefault(); stopRecording(); } : undefined } onPointerDown={handlePointerDown} onPointerUp={handlePointerUp} onPointerLeave={handlePointerLeave} onContextMenu={(e) => e.preventDefault()} > {isRecording() ? <CircleStopIcon /> : <MicIcon />} </IconButton> )}11 unmodified lines32 unmodified lines333435363737394038394041309 unmodified linesExpand all35135235335435835936036136236336436536636736836937037137235535635735851 unmodified lines32 unmodified lines showVisualizer as showVisualizerSetting, backgroundColor as backgroundColorSetting, frameRate as frameRateSetting, useDominantColorAsBg as useDominantColorAsBgSetting, toggleToRecordSetting, Setting, toggleToRecord, setToggleToRecord,} from "~/lib/settings";import { handleResolver } from "~/lib/at";import { toaster } from "~/components/Toaster";import { createListCollection, Select } from "~/components/ui/select";309 unmodified linesExpand all <Drawer.Body> <Stack gap="4"> <Accounts /> <Stack> <FormLabel>user interface</FormLabel> <Stack gap="0" border="1px solid var(--colors-border-default)" borderBottomWidth="3px" rounded="xs" > <SettingCheckbox label="use toggle to record" setting={toggleToRecordSetting} signal={[toggleToRecord, setToggleToRecord]} /> </Stack> </Stack> <Stack> <FormLabel>processing</FormLabel> <Stack gap="0" border="1px solid var(--colors-border-default)"51 unmodified lines12123411 unmodified lines16171819222324252627282930313233import { createSignal } from "solid-js";
export const setting = <T>(key: string) => { return { get: () => { const value = localStorage.getItem(key);11 unmodified linesexport const showProfilePicture = setting<boolean>("showProfilePicture");export const useDominantColorAsBg = setting<boolean>("useDominantColorAsBg");export const backgroundColor = setting<string>("backgroundColor");export const frameRate = setting<number>("frameRate");
export const toggleToRecordSetting = setting<boolean>("toggleToRecord");const [_toggleToRecord, _setToggleToRecord] = createSignal<boolean>( toggleToRecordSetting.get() ?? false,);export const toggleToRecord = _toggleToRecord;export const setToggleToRecord = ( value: boolean | ((prev: boolean) => boolean),) => { const newAccounts = _setToggleToRecord(value); toggleToRecordSetting.set(newAccounts);};