creates video voice memos from audio clips; with bluesky integration.
trill.ptr.pet
1import { Component, createSignal, Signal } from "solid-js";
2
3import { CaptionsIcon, SendIcon, XIcon } from "lucide-solid";
4import { Stack } from "styled-system/jsx";
5import { IconButton } from "~/components/ui/icon-button";
6import { Spinner } from "~/components/ui/spinner";
7import { Text } from "~/components/ui/text";
8import { Link } from "~/components/ui/link";
9
10import { parseCanonicalResourceUri } from "@atcute/lexicons/syntax";
11import { css } from "styled-system/css";
12import { sendPost } from "~/lib/at";
13import { toaster } from "~/components/Toaster";
14import { Dialog } from "~/components/ui/dialog";
15import { Textarea } from "~/components/ui/textarea";
16import { Account } from "~/lib/accounts";
17import { Popover } from "./ui/popover";
18
19const PostDialog = (props: {
20 result: Blob;
21 account: Account | undefined;
22 openSignal: Signal<boolean>;
23 initialAltText?: string;
24}) => {
25 const [postContent, setPostContent] = createSignal<string>("");
26 const [altText, setAltText] = createSignal<string>(
27 props.initialAltText ?? "",
28 );
29 const [posting, setPosting] = createSignal(false);
30 const [open, setOpen] = props.openSignal;
31
32 return (
33 <Dialog.Root open={open()} onOpenChange={(e) => setOpen(e.open)}>
34 <Dialog.Backdrop />
35 <Dialog.Positioner>
36 <Dialog.Content>
37 <Stack>
38 <Stack gap="0">
39 <video
40 class={css({ maxW: "sm", roundedTop: "xs" })}
41 controls
42 src={URL.createObjectURL(props.result)}
43 ></video>
44 <Textarea
45 placeholder="enter text content..."
46 id="post-content"
47 value={postContent()}
48 onChange={(e) => setPostContent(e.target.value)}
49 rows={2}
50 resize="none"
51 border="none"
52 borderTop="1px solid var(--colors-border-muted)"
53 boxShadow={{ base: "none", _focus: "none" }}
54 />
55 </Stack>
56 <Stack
57 borderTop="1px solid var(--colors-border-muted)"
58 gap="2"
59 p="3"
60 direction="row"
61 align="center"
62 >
63 <Stack direction="row" align="center">
64 <Dialog.Title>
65 post to {props.account?.handle ?? props.account?.did}
66 </Dialog.Title>
67 </Stack>
68 <div class={css({ flexGrow: 1 })} />
69 {posting() ? (
70 <Spinner
71 borderLeftColor="bg.emphasized"
72 borderBottomColor="bg.emphasized"
73 borderWidth="4px"
74 size="sm"
75 />
76 ) : (
77 <Dialog.CloseTrigger
78 asChild={(closeTriggerProps) => (
79 <IconButton
80 {...closeTriggerProps()}
81 aria-label="Close Dialog"
82 variant="ghost"
83 size="sm"
84 >
85 <XIcon />
86 </IconButton>
87 )}
88 />
89 )}
90 <Popover.Root>
91 <Popover.Trigger
92 asChild={(triggerProps) => (
93 <IconButton
94 {...triggerProps()}
95 variant={altText() ? "solid" : "ghost"}
96 size="sm"
97 >
98 <CaptionsIcon />
99 </IconButton>
100 )}
101 />
102 <Popover.Positioner>
103 <Popover.Content width="sm">
104 <Popover.Arrow />
105 <Stack gap="2">
106 <Popover.Title>video alt text</Popover.Title>
107 <Textarea
108 value={altText()}
109 onInput={(e) => setAltText(e.currentTarget.value)}
110 placeholder="describe the video content..."
111 rows={4}
112 />
113 </Stack>
114 </Popover.Content>
115 </Popover.Positioner>
116 </Popover.Root>
117 <IconButton
118 disabled={posting()}
119 onClick={() => {
120 setPosting(true);
121 sendPost(
122 props.account?.did!,
123 props.result,
124 postContent(),
125 altText(),
126 )
127 .then((result) => {
128 const parsedUri = parseCanonicalResourceUri(result.uri);
129 if (!parsedUri.ok) throw "failed to parse atproto uri";
130 const { repo, rkey } = parsedUri.value;
131 toaster.create({
132 title: "post sent",
133 description: (
134 <>
135 <Text>view post </Text>
136 <Link
137 href={`https://bsky.app/profile/${repo}/post/${rkey}`}
138 color={{
139 base: "colorPalette.text",
140 _hover: "colorPalette.emphasized",
141 }}
142 textDecoration={{ _hover: "underline" }}
143 >
144 here
145 </Link>
146 </>
147 ),
148 type: "success",
149 });
150 setOpen(false);
151 })
152 .catch((error) => {
153 toaster.create({
154 title: "send post failed",
155 description: error,
156 type: "error",
157 });
158 })
159 .finally(() => {
160 setPosting(false);
161 });
162 }}
163 variant="ghost"
164 size="sm"
165 >
166 <SendIcon />
167 </IconButton>
168 </Stack>
169 </Stack>
170 </Dialog.Content>
171 </Dialog.Positioner>
172 </Dialog.Root>
173 );
174};
175
176export default PostDialog;