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
···
const Tasks = (props: TasksProps) => (
<Stack
border="1px solid var(--colors-border-subtle)"
+
borderBottomWidth="3px"
gap="1.5"
p="2"
rounded="sm"
···
const Upload = (props: FileUpload.RootProps) => {
return (
<FileUpload.Root maxFiles={100} {...props}>
-
<FileUpload.Dropzone>
+
<FileUpload.Dropzone borderBottomWidth="3px">
<FileUpload.Label>drop your files here</FileUpload.Label>
<HStack alignItems="center">
<FileUpload.Trigger
+59 -27
src/components/FileTask.tsx
···
-
import { CircleAlertIcon, DownloadIcon, SendIcon } from "lucide-solid";
+
import {
+
CircleAlertIcon,
+
DownloadIcon,
+
EllipsisVerticalIcon,
+
SendIcon,
+
} from "lucide-solid";
import { Stack } from "styled-system/jsx";
import { IconButton } from "~/components/ui/icon-button";
import { Spinner } from "~/components/ui/spinner";
···
import { TaskState } from "~/lib/task";
import PostDialog from "./PostDialog";
+
import { Button } from "./ui/button";
+
import { Menu } from "./ui/menu";
+
import { createSignal } from "solid-js";
const downloadFile = (blob: Blob, fileName: string) => {
const url = URL.createObjectURL(blob);
···
};
const Task = (process: TaskState, selectedAccount: Account | undefined) => {
+
const [dialogOpen, setDialogOpen] = createSignal(false);
const statusError = (error: string) => (
<Popover.Root>
<Popover.Trigger
···
const statusSuccess = (result: Blob) => {
return (
<>
-
<IconButton
-
color={{ _hover: "colorPalette.emphasized" }}
-
onClick={() =>
-
downloadFile(
-
result,
-
process.file.name
-
.split(".")
-
.slice(0, -1)
-
.join(".")
-
.concat(".mp4"),
-
)
-
}
-
variant="ghost"
-
>
-
<DownloadIcon />
-
</IconButton>
<PostDialog
-
trigger={(props) => (
-
<IconButton
-
{...props}
-
disabled={selectedAccount === undefined}
-
color={{ _hover: "colorPalette.emphasized" }}
-
variant="ghost"
-
>
-
<SendIcon />
-
</IconButton>
-
)}
+
openSignal={[dialogOpen, setDialogOpen]}
account={selectedAccount}
result={result}
/>
+
<Menu.Root
+
positioning={{ placement: "bottom-start", strategy: "fixed" }}
+
>
+
<Menu.Trigger
+
asChild={(triggerProps) => (
+
<IconButton {...triggerProps()} variant="ghost">
+
<EllipsisVerticalIcon />
+
</IconButton>
+
)}
+
/>
+
<Menu.Positioner>
+
<Menu.Content>
+
<Menu.ItemGroup>
+
<Button
+
color={{ _hover: "colorPalette.emphasized" }}
+
onClick={() =>
+
downloadFile(
+
result,
+
process.file.name
+
.split(".")
+
.slice(0, -1)
+
.join(".")
+
.concat(".mp4"),
+
)
+
}
+
variant="ghost"
+
display="flex"
+
justifyContent="space-between"
+
alignItems="center"
+
>
+
download <DownloadIcon />
+
</Button>
+
<Button
+
onClick={() => setDialogOpen(!dialogOpen())}
+
disabled={selectedAccount === undefined}
+
color={{ _hover: "colorPalette.emphasized" }}
+
variant="ghost"
+
display="flex"
+
justifyContent="space-between"
+
alignItems="center"
+
>
+
post to bsky <SendIcon />
+
</Button>
+
</Menu.ItemGroup>
+
</Menu.Content>
+
</Menu.Positioner>
+
</Menu.Root>
</>
);
};
···
<Stack
direction="row"
border="1px solid var(--colors-border-muted)"
+
borderBottomWidth="2px"
gap="2"
align="center"
rounded="sm"
+7 -5
src/components/MicRecorder.tsx
···
mediaRecorder?.mimeType || mimeType || fallbackMimeType;
const fileExtension = usedMime.split("/")[1]?.split(";")[0] || "webm";
const blob = new Blob(audioChunks, { type: usedMime });
-
const file = new File(
-
[blob],
-
`rec-${new Date().toISOString().replace(/:/g, "-")}.${fileExtension}`,
-
{ type: usedMime },
-
);
+
const fileDate = new Date()
+
.toLocaleTimeString()
+
.replace(/:/g, "-")
+
.replace(/\s+/g, "_");
+
const file = new File([blob], `rec-${fileDate}.${fileExtension}`, {
+
type: usedMime,
+
});
addTask(props.selectedAccount(), file);
audioChunks = [];
+3 -6
src/components/PostDialog.tsx
···
-
import { Component, createSignal } from "solid-js";
+
import { Component, createSignal, Signal } from "solid-js";
import { SendIcon, XIcon } from "lucide-solid";
import { Stack } from "styled-system/jsx";
···
import { Account } from "~/lib/accounts";
const PostDialog = (props: {
-
trigger: Component;
result: Blob;
account: Account | undefined;
+
openSignal: Signal<boolean>;
}) => {
const [postContent, setPostContent] = createSignal<string>("");
const [posting, setPosting] = createSignal(false);
-
const [open, setOpen] = createSignal(false);
+
const [open, setOpen] = props.openSignal;
return (
<Dialog.Root open={open()} onOpenChange={(e) => setOpen(e.open)}>
-
<Dialog.Trigger
-
asChild={(triggerProps) => <props.trigger {...triggerProps()} />}
-
/>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
+16 -11
src/components/Settings.tsx
···
};
const Accounts = () => {
-
const item = (account: Account) => (
+
const item = (account: Account, isLatest: boolean) => (
<Stack
direction="row"
w="full"
px="2"
pb="2"
-
borderBottom="1px solid var(--colors-border-muted)"
+
borderBottom={
+
!isLatest ? "1px solid var(--colors-border-muted)" : undefined
+
}
align="center"
>
{account.handle ? `@${account.handle}` : account.did}
···
</Text>
}
>
-
{item}
+
{(acc, idx) => item(acc, idx() === accounts.length - 1)}
</For>
);
return (
<Stack>
<FormLabel>accounts</FormLabel>
-
<Stack border="1px solid var(--colors-border-default)" rounded="xs">
+
<Stack
+
border="1px solid var(--colors-border-default)"
+
borderBottomWidth="3px"
+
rounded="xs"
+
>
<Stack
borderBottom="1px solid var(--colors-border-default)"
p="2"
···
<Stack
gap="0"
border="1px solid var(--colors-border-default)"
+
borderBottomWidth="3px"
rounded="xs"
>
<Box borderBottom="1px solid var(--colors-border-subtle)">
···
signal={[backgroundColor, setBackgroundColor]}
/>
</Stack>
-
<Box borderBottom="1px solid var(--colors-border-muted)">
-
<SettingSelect
-
label="frame rate"
-
signal={[frameRate, setFrameRate]}
-
collection={frameRateCollection}
-
/>
-
</Box>
+
<SettingSelect
+
label="frame rate"
+
signal={[frameRate, setFrameRate]}
+
collection={frameRateCollection}
+
/>
</Stack>
</Stack>
</Stack>
+1
src/components/ui/menu.tsx
···
+
export * as Menu from './styled/menu'
+98
src/components/ui/styled/menu.tsx
···
+
import { type Assign, Menu } from '@ark-ui/solid'
+
import type { ComponentProps } from 'solid-js'
+
import { type MenuVariantProps, menu } from 'styled-system/recipes'
+
import type { HTMLStyledProps } from 'styled-system/types'
+
import { createStyleContext } from './utils/create-style-context'
+
+
const { withRootProvider, withContext } = createStyleContext(menu)
+
+
export type RootProviderProps = ComponentProps<typeof RootProvider>
+
export const RootProvider = withRootProvider<Assign<Menu.RootProviderProps, MenuVariantProps>>(
+
Menu.RootProvider,
+
)
+
+
export type RootProps = ComponentProps<typeof Root>
+
export const Root = withRootProvider<Assign<Menu.RootProps, MenuVariantProps>>(Menu.Root)
+
+
export const Arrow = withContext<Assign<HTMLStyledProps<'div'>, Menu.ArrowBaseProps>>(
+
Menu.Arrow,
+
'arrow',
+
)
+
+
export const ArrowTip = withContext<Assign<HTMLStyledProps<'div'>, Menu.ArrowTipBaseProps>>(
+
Menu.ArrowTip,
+
'arrowTip',
+
)
+
+
export const CheckboxItem = withContext<Assign<HTMLStyledProps<'div'>, Menu.CheckboxItemBaseProps>>(
+
Menu.CheckboxItem,
+
'item',
+
)
+
+
export const Content = withContext<Assign<HTMLStyledProps<'div'>, Menu.ContentBaseProps>>(
+
Menu.Content,
+
'content',
+
)
+
+
export const ContextTrigger = withContext<
+
Assign<HTMLStyledProps<'button'>, Menu.ContextTriggerBaseProps>
+
>(Menu.ContextTrigger, 'contextTrigger')
+
+
export const Indicator = withContext<Assign<HTMLStyledProps<'div'>, Menu.IndicatorBaseProps>>(
+
Menu.Indicator,
+
'indicator',
+
)
+
+
export const ItemGroupLabel = withContext<
+
Assign<HTMLStyledProps<'div'>, Menu.ItemGroupLabelBaseProps>
+
>(Menu.ItemGroupLabel, 'itemGroupLabel')
+
+
export const ItemGroup = withContext<Assign<HTMLStyledProps<'div'>, Menu.ItemGroupBaseProps>>(
+
Menu.ItemGroup,
+
'itemGroup',
+
)
+
+
export const ItemIndicator = withContext<
+
Assign<HTMLStyledProps<'div'>, Menu.ItemIndicatorBaseProps>
+
>(Menu.ItemIndicator, 'itemIndicator')
+
+
export const Item = withContext<Assign<HTMLStyledProps<'div'>, Menu.ItemBaseProps>>(
+
Menu.Item,
+
'item',
+
)
+
+
export const ItemText = withContext<Assign<HTMLStyledProps<'div'>, Menu.ItemTextBaseProps>>(
+
Menu.ItemText,
+
'itemText',
+
)
+
+
export const Positioner = withContext<Assign<HTMLStyledProps<'div'>, Menu.PositionerBaseProps>>(
+
Menu.Positioner,
+
'positioner',
+
)
+
+
export const RadioItemGroup = withContext<
+
Assign<HTMLStyledProps<'div'>, Menu.RadioItemGroupBaseProps>
+
>(Menu.RadioItemGroup, 'itemGroup')
+
+
export const RadioItem = withContext<Assign<HTMLStyledProps<'div'>, Menu.RadioItemBaseProps>>(
+
Menu.RadioItem,
+
'item',
+
)
+
+
export const Separator = withContext<Assign<HTMLStyledProps<'hr'>, Menu.SeparatorBaseProps>>(
+
Menu.Separator,
+
'separator',
+
)
+
+
export const TriggerItem = withContext<Assign<HTMLStyledProps<'div'>, Menu.TriggerItemBaseProps>>(
+
Menu.TriggerItem,
+
'triggerItem',
+
)
+
+
export const Trigger = withContext<Assign<HTMLStyledProps<'button'>, Menu.TriggerBaseProps>>(
+
Menu.Trigger,
+
'trigger',
+
)
+
+
export { MenuContext as Context } from '@ark-ui/solid'
+5
src/index.css
···
.lucide {
stroke-width: 3px;
}
+
+
.input,
+
.button--variant_outline {
+
border-bottom-width: 2px;
+
}
+5 -4
src/lib/render.ts
···
drawBackground();
if (pfpImg) {
-
const pfpSize = Math.min(renderCanvas.width, renderCanvas.height) * 0.5;
-
const pfpX = (renderCanvas.width - pfpSize) / 2;
-
const pfpY = (renderCanvas.height - pfpSize) / 2;
+
const centerX = renderCanvas.width / 2;
+
const centerY = renderCanvas.height / 2;
+
const baseRadius =
+
Math.min(renderCanvas.width, renderCanvas.height) * 0.15;
-
drawPfp(ctx, pfpImg, pfpX, pfpY, pfpSize);
+
drawPfp(ctx, pfpImg, centerX, centerY, baseRadius);
}
await videoSource.add(0, duration);