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
···
-
import { createSignal, For } from "solid-js";
-
import {
-
CheckIcon,
-
ChevronsUpDownIcon,
-
ClipboardIcon,
-
HeartIcon,
-
MicIcon,
-
Trash2Icon,
-
} 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 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 = () =>
···
</Button>
)}
/>
-
<MicRecorder
-
selectedAccount={selectedAccount}
-
holdToRecord={!toggleToRecord()}
-
/>
{/*<IconButton
size="sm"
onClick={() =>
···
+
import { For } from "solid-js";
+
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 { Text } from "./components/ui/text";
import { AtprotoDid } from "@atcute/lexicons/syntax";
···
import Settings from "./components/Settings";
import MicRecorder from "./components/MicRecorder";
import { Link } from "./components/ui/link";
const App = () => {
const collection = () =>
···
</Button>
)}
/>
+
<MicRecorder selectedAccount={selectedAccount} />
{/*<IconButton
size="sm"
onClick={() =>
+53 -26
src/components/MicRecorder.tsx
···
type MicRecorderProps = {
selectedAccount: () => AtprotoDid | undefined;
-
holdToRecord?: boolean;
};
const MicRecorder = (props: MicRecorderProps) => {
···
let mediaStream: MediaStream | null = null;
let audioChunks: Blob[] = [];
const isSafari =
typeof navigator !== "undefined" &&
navigator.vendor &&
···
const startRecording = async () => {
if (isRecording()) return;
try {
audioChunks = [];
···
echoCancellation: { ideal: true },
},
});
const audioTrack = mediaStream.getAudioTracks()[0] ?? null;
if (!audioTrack) throw "no audio track found";
···
setIsRecording(true);
setRecordingStart(Date.now());
} catch (error) {
console.error("error accessing microphone:", error);
toaster.create({
···
};
const stopRecording = () => {
-
if (!isRecording() || !mediaRecorder) return;
if (mediaRecorder.state !== "inactive") mediaRecorder.stop();
setIsRecording(false);
};
···
return `${mins}:${secs.toString().padStart(2, "0")}`;
};
return (
<Popover.Root positioning={{ placement: "top" }} open={isRecording()}>
<Popover.Anchor
···
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
-
}
>
{isRecording() ? <CircleStopIcon /> : <MicIcon />}
</IconButton>
···
type MicRecorderProps = {
selectedAccount: () => AtprotoDid | undefined;
};
const MicRecorder = (props: MicRecorderProps) => {
···
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 &&
···
const startRecording = async () => {
if (isRecording()) return;
+
stopRequestPending = false;
try {
audioChunks = [];
···
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";
···
setIsRecording(true);
setRecordingStart(Date.now());
+
+
// delayed hold release
+
if (stopRequestPending) stopRecording();
} catch (error) {
console.error("error accessing microphone:", error);
toaster.create({
···
};
const stopRecording = () => {
+
if (!isRecording() || !mediaRecorder) {
+
stopRequestPending = true;
+
return;
+
}
if (mediaRecorder.state !== "inactive") mediaRecorder.stop();
setIsRecording(false);
};
···
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
···
size="md"
variant={isRecording() ? "solid" : "subtle"}
colorPalette={isRecording() ? "red" : undefined}
+
onPointerDown={handlePointerDown}
+
onPointerUp={handlePointerUp}
+
onPointerLeave={handlePointerLeave}
+
onContextMenu={(e) => e.preventDefault()}
>
{isRecording() ? <CircleStopIcon /> : <MicIcon />}
</IconButton>
-18
src/components/Settings.tsx
···
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";
···
<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
···
backgroundColor as backgroundColorSetting,
frameRate as frameRateSetting,
useDominantColorAsBg as useDominantColorAsBgSetting,
Setting,
} from "~/lib/settings";
import { handleResolver } from "~/lib/at";
import { toaster } from "~/components/Toaster";
···
<Drawer.Body>
<Stack gap="4">
<Accounts />
<Stack>
<FormLabel>processing</FormLabel>
<Stack
-14
src/lib/settings.ts
···
-
import { createSignal } from "solid-js";
-
export const setting = <T>(key: string) => {
return {
get: () => {
···
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);
-
};
···
export const setting = <T>(key: string) => {
return {
get: () => {
···
export const useDominantColorAsBg = setting<boolean>("useDominantColorAsBg");
export const backgroundColor = setting<string>("backgroundColor");
export const frameRate = setting<number>("frameRate");