creates video voice memos from audio clips; with bluesky integration.
trill.ptr.pet
1import {
2 CircleAlertIcon,
3 DownloadIcon,
4 EllipsisVerticalIcon,
5 SendIcon,
6} from "lucide-solid";
7import { Stack } from "styled-system/jsx";
8import { IconButton } from "~/components/ui/icon-button";
9import { Spinner } from "~/components/ui/spinner";
10import { Popover } from "~/components/ui/popover";
11
12import { css } from "styled-system/css";
13import { Account } from "~/lib/accounts";
14
15import { TaskState } from "~/lib/task";
16import PostDialog from "./PostDialog";
17import { Button } from "./ui/button";
18import { Menu } from "./ui/menu";
19import { createSignal } from "solid-js";
20
21const downloadFile = (blob: Blob, fileName: string) => {
22 const url = URL.createObjectURL(blob);
23 const a = document.createElement("a");
24 a.href = url;
25 // handle file names with periods in them
26 a.download = fileName;
27 document.body.appendChild(a);
28 a.click();
29 document.body.removeChild(a);
30 URL.revokeObjectURL(url);
31};
32
33const Task = (process: TaskState, selectedAccount: Account | undefined) => {
34 const [dialogOpen, setDialogOpen] = createSignal(false);
35 const statusError = (error: string) => (
36 <Popover.Root>
37 <Popover.Trigger
38 asChild={(triggerProps) => (
39 <IconButton
40 {...triggerProps()}
41 color={{
42 base: "red",
43 _hover: "red.emphasized",
44 }}
45 variant="ghost"
46 >
47 <CircleAlertIcon />
48 </IconButton>
49 )}
50 />
51 <Popover.Positioner>
52 <Popover.Content>error processing file: {error}</Popover.Content>
53 </Popover.Positioner>
54 </Popover.Root>
55 );
56 const statusSuccess = (result: Blob) => {
57 return (
58 <>
59 <PostDialog
60 openSignal={[dialogOpen, setDialogOpen]}
61 account={selectedAccount}
62 result={result}
63 />
64 <Menu.Root
65 positioning={{ placement: "bottom-start", strategy: "fixed" }}
66 >
67 <Menu.Trigger
68 asChild={(triggerProps) => (
69 <IconButton {...triggerProps()} variant="ghost">
70 <EllipsisVerticalIcon />
71 </IconButton>
72 )}
73 />
74 <Menu.Positioner>
75 <Menu.Content>
76 <Menu.ItemGroup>
77 <Button
78 color={{ _hover: "colorPalette.emphasized" }}
79 onClick={() =>
80 downloadFile(
81 result,
82 process.file.name
83 .split(".")
84 .slice(0, -1)
85 .join(".")
86 .concat(".mp4"),
87 )
88 }
89 variant="ghost"
90 display="flex"
91 justifyContent="space-between"
92 alignItems="center"
93 >
94 download <DownloadIcon />
95 </Button>
96 <Button
97 onClick={() => setDialogOpen(!dialogOpen())}
98 disabled={selectedAccount === undefined}
99 color={{ _hover: "colorPalette.emphasized" }}
100 variant="ghost"
101 display="flex"
102 justifyContent="space-between"
103 alignItems="center"
104 >
105 post to bsky <SendIcon />
106 </Button>
107 </Menu.ItemGroup>
108 </Menu.Content>
109 </Menu.Positioner>
110 </Menu.Root>
111 </>
112 );
113 };
114 const statusProcessing = () => (
115 <Spinner
116 borderLeftColor="bg.emphasized"
117 borderBottomColor="bg.emphasized"
118 borderWidth="4px"
119 m="2"
120 />
121 );
122
123 const status = () => {
124 switch (process.status) {
125 case "success":
126 return statusSuccess(process.result);
127 case "processing":
128 return statusProcessing();
129 default:
130 return statusError(process.error);
131 }
132 };
133
134 return (
135 <Stack
136 direction="row"
137 border="1px solid var(--colors-border-muted)"
138 borderBottomWidth="2px"
139 gap="2"
140 align="center"
141 rounded="sm"
142 >
143 <span
144 class={css({
145 overflow: "hidden",
146 textOverflow: "ellipsis",
147 whiteSpace: "nowrap",
148 pl: 2,
149 })}
150 >
151 {process.file.name}
152 </span>
153 <div class={css({ flexGrow: 1 })}></div>
154 <Stack direction="row" gap="0" flexShrink="0" align="center">
155 {status()}
156 </Stack>
157 </Stack>
158 );
159};
160
161export default Task;