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;