creates video voice memos from audio clips; with bluesky integration. trill.ptr.pet

feat: turn task items success into dropdown, improve style

ptr.pet dbd37d3f fe5cdac5

verified
+2 -1
src/App.tsx
··· 162 162 const Tasks = (props: TasksProps) => ( 163 163 <Stack 164 164 border="1px solid var(--colors-border-subtle)" 165 + borderBottomWidth="3px" 165 166 gap="1.5" 166 167 p="2" 167 168 rounded="sm" ··· 210 211 const Upload = (props: FileUpload.RootProps) => { 211 212 return ( 212 213 <FileUpload.Root maxFiles={100} {...props}> 213 - <FileUpload.Dropzone> 214 + <FileUpload.Dropzone borderBottomWidth="3px"> 214 215 <FileUpload.Label>drop your files here</FileUpload.Label> 215 216 <HStack alignItems="center"> 216 217 <FileUpload.Trigger
+59 -27
src/components/FileTask.tsx
··· 1 - import { CircleAlertIcon, DownloadIcon, SendIcon } from "lucide-solid"; 1 + import { 2 + CircleAlertIcon, 3 + DownloadIcon, 4 + EllipsisVerticalIcon, 5 + SendIcon, 6 + } from "lucide-solid"; 2 7 import { Stack } from "styled-system/jsx"; 3 8 import { IconButton } from "~/components/ui/icon-button"; 4 9 import { Spinner } from "~/components/ui/spinner"; ··· 9 14 10 15 import { TaskState } from "~/lib/task"; 11 16 import PostDialog from "./PostDialog"; 17 + import { Button } from "./ui/button"; 18 + import { Menu } from "./ui/menu"; 19 + import { createSignal } from "solid-js"; 12 20 13 21 const downloadFile = (blob: Blob, fileName: string) => { 14 22 const url = URL.createObjectURL(blob); ··· 23 31 }; 24 32 25 33 const Task = (process: TaskState, selectedAccount: Account | undefined) => { 34 + const [dialogOpen, setDialogOpen] = createSignal(false); 26 35 const statusError = (error: string) => ( 27 36 <Popover.Root> 28 37 <Popover.Trigger ··· 47 56 const statusSuccess = (result: Blob) => { 48 57 return ( 49 58 <> 50 - <IconButton 51 - color={{ _hover: "colorPalette.emphasized" }} 52 - onClick={() => 53 - downloadFile( 54 - result, 55 - process.file.name 56 - .split(".") 57 - .slice(0, -1) 58 - .join(".") 59 - .concat(".mp4"), 60 - ) 61 - } 62 - variant="ghost" 63 - > 64 - <DownloadIcon /> 65 - </IconButton> 66 59 <PostDialog 67 - trigger={(props) => ( 68 - <IconButton 69 - {...props} 70 - disabled={selectedAccount === undefined} 71 - color={{ _hover: "colorPalette.emphasized" }} 72 - variant="ghost" 73 - > 74 - <SendIcon /> 75 - </IconButton> 76 - )} 60 + openSignal={[dialogOpen, setDialogOpen]} 77 61 account={selectedAccount} 78 62 result={result} 79 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> 80 111 </> 81 112 ); 82 113 }; ··· 104 135 <Stack 105 136 direction="row" 106 137 border="1px solid var(--colors-border-muted)" 138 + borderBottomWidth="2px" 107 139 gap="2" 108 140 align="center" 109 141 rounded="sm"
+7 -5
src/components/MicRecorder.tsx
··· 99 99 mediaRecorder?.mimeType || mimeType || fallbackMimeType; 100 100 const fileExtension = usedMime.split("/")[1]?.split(";")[0] || "webm"; 101 101 const blob = new Blob(audioChunks, { type: usedMime }); 102 - const file = new File( 103 - [blob], 104 - `rec-${new Date().toISOString().replace(/:/g, "-")}.${fileExtension}`, 105 - { type: usedMime }, 106 - ); 102 + const fileDate = new Date() 103 + .toLocaleTimeString() 104 + .replace(/:/g, "-") 105 + .replace(/\s+/g, "_"); 106 + const file = new File([blob], `rec-${fileDate}.${fileExtension}`, { 107 + type: usedMime, 108 + }); 107 109 108 110 addTask(props.selectedAccount(), file); 109 111 audioChunks = [];
+3 -6
src/components/PostDialog.tsx
··· 1 - import { Component, createSignal } from "solid-js"; 1 + import { Component, createSignal, Signal } from "solid-js"; 2 2 3 3 import { SendIcon, XIcon } from "lucide-solid"; 4 4 import { Stack } from "styled-system/jsx"; ··· 16 16 import { Account } from "~/lib/accounts"; 17 17 18 18 const PostDialog = (props: { 19 - trigger: Component; 20 19 result: Blob; 21 20 account: Account | undefined; 21 + openSignal: Signal<boolean>; 22 22 }) => { 23 23 const [postContent, setPostContent] = createSignal<string>(""); 24 24 const [posting, setPosting] = createSignal(false); 25 - const [open, setOpen] = createSignal(false); 25 + const [open, setOpen] = props.openSignal; 26 26 27 27 return ( 28 28 <Dialog.Root open={open()} onOpenChange={(e) => setOpen(e.open)}> 29 - <Dialog.Trigger 30 - asChild={(triggerProps) => <props.trigger {...triggerProps()} />} 31 - /> 32 29 <Dialog.Backdrop /> 33 30 <Dialog.Positioner> 34 31 <Dialog.Content>
+16 -11
src/components/Settings.tsx
··· 224 224 }; 225 225 226 226 const Accounts = () => { 227 - const item = (account: Account) => ( 227 + const item = (account: Account, isLatest: boolean) => ( 228 228 <Stack 229 229 direction="row" 230 230 w="full" 231 231 px="2" 232 232 pb="2" 233 - borderBottom="1px solid var(--colors-border-muted)" 233 + borderBottom={ 234 + !isLatest ? "1px solid var(--colors-border-muted)" : undefined 235 + } 234 236 align="center" 235 237 > 236 238 {account.handle ? `@${account.handle}` : account.did} ··· 253 255 </Text> 254 256 } 255 257 > 256 - {item} 258 + {(acc, idx) => item(acc, idx() === accounts.length - 1)} 257 259 </For> 258 260 ); 259 261 return ( 260 262 <Stack> 261 263 <FormLabel>accounts</FormLabel> 262 - <Stack border="1px solid var(--colors-border-default)" rounded="xs"> 264 + <Stack 265 + border="1px solid var(--colors-border-default)" 266 + borderBottomWidth="3px" 267 + rounded="xs" 268 + > 263 269 <Stack 264 270 borderBottom="1px solid var(--colors-border-default)" 265 271 p="2" ··· 350 356 <Stack 351 357 gap="0" 352 358 border="1px solid var(--colors-border-default)" 359 + borderBottomWidth="3px" 353 360 rounded="xs" 354 361 > 355 362 <Box borderBottom="1px solid var(--colors-border-subtle)"> ··· 376 383 signal={[backgroundColor, setBackgroundColor]} 377 384 /> 378 385 </Stack> 379 - <Box borderBottom="1px solid var(--colors-border-muted)"> 380 - <SettingSelect 381 - label="frame rate" 382 - signal={[frameRate, setFrameRate]} 383 - collection={frameRateCollection} 384 - /> 385 - </Box> 386 + <SettingSelect 387 + label="frame rate" 388 + signal={[frameRate, setFrameRate]} 389 + collection={frameRateCollection} 390 + /> 386 391 </Stack> 387 392 </Stack> 388 393 </Stack>
+1
src/components/ui/menu.tsx
··· 1 + export * as Menu from './styled/menu'
+98
src/components/ui/styled/menu.tsx
··· 1 + import { type Assign, Menu } from '@ark-ui/solid' 2 + import type { ComponentProps } from 'solid-js' 3 + import { type MenuVariantProps, menu } from 'styled-system/recipes' 4 + import type { HTMLStyledProps } from 'styled-system/types' 5 + import { createStyleContext } from './utils/create-style-context' 6 + 7 + const { withRootProvider, withContext } = createStyleContext(menu) 8 + 9 + export type RootProviderProps = ComponentProps<typeof RootProvider> 10 + export const RootProvider = withRootProvider<Assign<Menu.RootProviderProps, MenuVariantProps>>( 11 + Menu.RootProvider, 12 + ) 13 + 14 + export type RootProps = ComponentProps<typeof Root> 15 + export const Root = withRootProvider<Assign<Menu.RootProps, MenuVariantProps>>(Menu.Root) 16 + 17 + export const Arrow = withContext<Assign<HTMLStyledProps<'div'>, Menu.ArrowBaseProps>>( 18 + Menu.Arrow, 19 + 'arrow', 20 + ) 21 + 22 + export const ArrowTip = withContext<Assign<HTMLStyledProps<'div'>, Menu.ArrowTipBaseProps>>( 23 + Menu.ArrowTip, 24 + 'arrowTip', 25 + ) 26 + 27 + export const CheckboxItem = withContext<Assign<HTMLStyledProps<'div'>, Menu.CheckboxItemBaseProps>>( 28 + Menu.CheckboxItem, 29 + 'item', 30 + ) 31 + 32 + export const Content = withContext<Assign<HTMLStyledProps<'div'>, Menu.ContentBaseProps>>( 33 + Menu.Content, 34 + 'content', 35 + ) 36 + 37 + export const ContextTrigger = withContext< 38 + Assign<HTMLStyledProps<'button'>, Menu.ContextTriggerBaseProps> 39 + >(Menu.ContextTrigger, 'contextTrigger') 40 + 41 + export const Indicator = withContext<Assign<HTMLStyledProps<'div'>, Menu.IndicatorBaseProps>>( 42 + Menu.Indicator, 43 + 'indicator', 44 + ) 45 + 46 + export const ItemGroupLabel = withContext< 47 + Assign<HTMLStyledProps<'div'>, Menu.ItemGroupLabelBaseProps> 48 + >(Menu.ItemGroupLabel, 'itemGroupLabel') 49 + 50 + export const ItemGroup = withContext<Assign<HTMLStyledProps<'div'>, Menu.ItemGroupBaseProps>>( 51 + Menu.ItemGroup, 52 + 'itemGroup', 53 + ) 54 + 55 + export const ItemIndicator = withContext< 56 + Assign<HTMLStyledProps<'div'>, Menu.ItemIndicatorBaseProps> 57 + >(Menu.ItemIndicator, 'itemIndicator') 58 + 59 + export const Item = withContext<Assign<HTMLStyledProps<'div'>, Menu.ItemBaseProps>>( 60 + Menu.Item, 61 + 'item', 62 + ) 63 + 64 + export const ItemText = withContext<Assign<HTMLStyledProps<'div'>, Menu.ItemTextBaseProps>>( 65 + Menu.ItemText, 66 + 'itemText', 67 + ) 68 + 69 + export const Positioner = withContext<Assign<HTMLStyledProps<'div'>, Menu.PositionerBaseProps>>( 70 + Menu.Positioner, 71 + 'positioner', 72 + ) 73 + 74 + export const RadioItemGroup = withContext< 75 + Assign<HTMLStyledProps<'div'>, Menu.RadioItemGroupBaseProps> 76 + >(Menu.RadioItemGroup, 'itemGroup') 77 + 78 + export const RadioItem = withContext<Assign<HTMLStyledProps<'div'>, Menu.RadioItemBaseProps>>( 79 + Menu.RadioItem, 80 + 'item', 81 + ) 82 + 83 + export const Separator = withContext<Assign<HTMLStyledProps<'hr'>, Menu.SeparatorBaseProps>>( 84 + Menu.Separator, 85 + 'separator', 86 + ) 87 + 88 + export const TriggerItem = withContext<Assign<HTMLStyledProps<'div'>, Menu.TriggerItemBaseProps>>( 89 + Menu.TriggerItem, 90 + 'triggerItem', 91 + ) 92 + 93 + export const Trigger = withContext<Assign<HTMLStyledProps<'button'>, Menu.TriggerBaseProps>>( 94 + Menu.Trigger, 95 + 'trigger', 96 + ) 97 + 98 + export { MenuContext as Context } from '@ark-ui/solid'
+5
src/index.css
··· 3 3 .lucide { 4 4 stroke-width: 3px; 5 5 } 6 + 7 + .input, 8 + .button--variant_outline { 9 + border-bottom-width: 2px; 10 + }
+5 -4
src/lib/render.ts
··· 336 336 drawBackground(); 337 337 338 338 if (pfpImg) { 339 - const pfpSize = Math.min(renderCanvas.width, renderCanvas.height) * 0.5; 340 - const pfpX = (renderCanvas.width - pfpSize) / 2; 341 - const pfpY = (renderCanvas.height - pfpSize) / 2; 339 + const centerX = renderCanvas.width / 2; 340 + const centerY = renderCanvas.height / 2; 341 + const baseRadius = 342 + Math.min(renderCanvas.width, renderCanvas.height) * 0.15; 342 343 343 - drawPfp(ctx, pfpImg, pfpX, pfpY, pfpSize); 344 + drawPfp(ctx, pfpImg, centerX, centerY, baseRadius); 344 345 } 345 346 346 347 await videoSource.add(0, duration);