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