tangled
alpha
login
or
join now
ptr.pet
/
trill
23
fork
atom
creates video voice memos from audio clips; with bluesky integration.
trill.wisp.place
23
fork
atom
overview
issues
2
pulls
pipelines
feat: turn task items success into dropdown, improve style
ptr.pet
4 months ago
dbd37d3f
fe5cdac5
verified
This commit was signed with the committer's
known signature
.
ptr.pet
SSH Key Fingerprint:
SHA256:Abmvag+juovVufZTxyWY8KcVgrznxvBjQpJesv071Aw=
+196
-54
9 changed files
expand all
collapse all
unified
split
src
App.tsx
components
FileTask.tsx
MicRecorder.tsx
PostDialog.tsx
Settings.tsx
ui
menu.tsx
styled
menu.tsx
index.css
lib
render.ts
+2
-1
src/App.tsx
···
162
162
const Tasks = (props: TasksProps) => (
163
163
<Stack
164
164
border="1px solid var(--colors-border-subtle)"
165
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
213
-
<FileUpload.Dropzone>
214
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
1
-
import { CircleAlertIcon, DownloadIcon, SendIcon } from "lucide-solid";
1
1
+
import {
2
2
+
CircleAlertIcon,
3
3
+
DownloadIcon,
4
4
+
EllipsisVerticalIcon,
5
5
+
SendIcon,
6
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
17
+
import { Button } from "./ui/button";
18
18
+
import { Menu } from "./ui/menu";
19
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
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
50
-
<IconButton
51
51
-
color={{ _hover: "colorPalette.emphasized" }}
52
52
-
onClick={() =>
53
53
-
downloadFile(
54
54
-
result,
55
55
-
process.file.name
56
56
-
.split(".")
57
57
-
.slice(0, -1)
58
58
-
.join(".")
59
59
-
.concat(".mp4"),
60
60
-
)
61
61
-
}
62
62
-
variant="ghost"
63
63
-
>
64
64
-
<DownloadIcon />
65
65
-
</IconButton>
66
59
<PostDialog
67
67
-
trigger={(props) => (
68
68
-
<IconButton
69
69
-
{...props}
70
70
-
disabled={selectedAccount === undefined}
71
71
-
color={{ _hover: "colorPalette.emphasized" }}
72
72
-
variant="ghost"
73
73
-
>
74
74
-
<SendIcon />
75
75
-
</IconButton>
76
76
-
)}
60
60
+
openSignal={[dialogOpen, setDialogOpen]}
77
61
account={selectedAccount}
78
62
result={result}
79
63
/>
64
64
+
<Menu.Root
65
65
+
positioning={{ placement: "bottom-start", strategy: "fixed" }}
66
66
+
>
67
67
+
<Menu.Trigger
68
68
+
asChild={(triggerProps) => (
69
69
+
<IconButton {...triggerProps()} variant="ghost">
70
70
+
<EllipsisVerticalIcon />
71
71
+
</IconButton>
72
72
+
)}
73
73
+
/>
74
74
+
<Menu.Positioner>
75
75
+
<Menu.Content>
76
76
+
<Menu.ItemGroup>
77
77
+
<Button
78
78
+
color={{ _hover: "colorPalette.emphasized" }}
79
79
+
onClick={() =>
80
80
+
downloadFile(
81
81
+
result,
82
82
+
process.file.name
83
83
+
.split(".")
84
84
+
.slice(0, -1)
85
85
+
.join(".")
86
86
+
.concat(".mp4"),
87
87
+
)
88
88
+
}
89
89
+
variant="ghost"
90
90
+
display="flex"
91
91
+
justifyContent="space-between"
92
92
+
alignItems="center"
93
93
+
>
94
94
+
download <DownloadIcon />
95
95
+
</Button>
96
96
+
<Button
97
97
+
onClick={() => setDialogOpen(!dialogOpen())}
98
98
+
disabled={selectedAccount === undefined}
99
99
+
color={{ _hover: "colorPalette.emphasized" }}
100
100
+
variant="ghost"
101
101
+
display="flex"
102
102
+
justifyContent="space-between"
103
103
+
alignItems="center"
104
104
+
>
105
105
+
post to bsky <SendIcon />
106
106
+
</Button>
107
107
+
</Menu.ItemGroup>
108
108
+
</Menu.Content>
109
109
+
</Menu.Positioner>
110
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
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
102
-
const file = new File(
103
103
-
[blob],
104
104
-
`rec-${new Date().toISOString().replace(/:/g, "-")}.${fileExtension}`,
105
105
-
{ type: usedMime },
106
106
-
);
102
102
+
const fileDate = new Date()
103
103
+
.toLocaleTimeString()
104
104
+
.replace(/:/g, "-")
105
105
+
.replace(/\s+/g, "_");
106
106
+
const file = new File([blob], `rec-${fileDate}.${fileExtension}`, {
107
107
+
type: usedMime,
108
108
+
});
107
109
108
110
addTask(props.selectedAccount(), file);
109
111
audioChunks = [];
+3
-6
src/components/PostDialog.tsx
···
1
1
-
import { Component, createSignal } from "solid-js";
1
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
19
-
trigger: Component;
20
19
result: Blob;
21
20
account: Account | undefined;
21
21
+
openSignal: Signal<boolean>;
22
22
}) => {
23
23
const [postContent, setPostContent] = createSignal<string>("");
24
24
const [posting, setPosting] = createSignal(false);
25
25
-
const [open, setOpen] = createSignal(false);
25
25
+
const [open, setOpen] = props.openSignal;
26
26
27
27
return (
28
28
<Dialog.Root open={open()} onOpenChange={(e) => setOpen(e.open)}>
29
29
-
<Dialog.Trigger
30
30
-
asChild={(triggerProps) => <props.trigger {...triggerProps()} />}
31
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
227
-
const item = (account: Account) => (
227
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
233
-
borderBottom="1px solid var(--colors-border-muted)"
233
233
+
borderBottom={
234
234
+
!isLatest ? "1px solid var(--colors-border-muted)" : undefined
235
235
+
}
234
236
align="center"
235
237
>
236
238
{account.handle ? `@${account.handle}` : account.did}
···
253
255
</Text>
254
256
}
255
257
>
256
256
-
{item}
258
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
262
-
<Stack border="1px solid var(--colors-border-default)" rounded="xs">
264
264
+
<Stack
265
265
+
border="1px solid var(--colors-border-default)"
266
266
+
borderBottomWidth="3px"
267
267
+
rounded="xs"
268
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
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
379
-
<Box borderBottom="1px solid var(--colors-border-muted)">
380
380
-
<SettingSelect
381
381
-
label="frame rate"
382
382
-
signal={[frameRate, setFrameRate]}
383
383
-
collection={frameRateCollection}
384
384
-
/>
385
385
-
</Box>
386
386
+
<SettingSelect
387
387
+
label="frame rate"
388
388
+
signal={[frameRate, setFrameRate]}
389
389
+
collection={frameRateCollection}
390
390
+
/>
386
391
</Stack>
387
392
</Stack>
388
393
</Stack>
+1
src/components/ui/menu.tsx
···
1
1
+
export * as Menu from './styled/menu'
+98
src/components/ui/styled/menu.tsx
···
1
1
+
import { type Assign, Menu } from '@ark-ui/solid'
2
2
+
import type { ComponentProps } from 'solid-js'
3
3
+
import { type MenuVariantProps, menu } from 'styled-system/recipes'
4
4
+
import type { HTMLStyledProps } from 'styled-system/types'
5
5
+
import { createStyleContext } from './utils/create-style-context'
6
6
+
7
7
+
const { withRootProvider, withContext } = createStyleContext(menu)
8
8
+
9
9
+
export type RootProviderProps = ComponentProps<typeof RootProvider>
10
10
+
export const RootProvider = withRootProvider<Assign<Menu.RootProviderProps, MenuVariantProps>>(
11
11
+
Menu.RootProvider,
12
12
+
)
13
13
+
14
14
+
export type RootProps = ComponentProps<typeof Root>
15
15
+
export const Root = withRootProvider<Assign<Menu.RootProps, MenuVariantProps>>(Menu.Root)
16
16
+
17
17
+
export const Arrow = withContext<Assign<HTMLStyledProps<'div'>, Menu.ArrowBaseProps>>(
18
18
+
Menu.Arrow,
19
19
+
'arrow',
20
20
+
)
21
21
+
22
22
+
export const ArrowTip = withContext<Assign<HTMLStyledProps<'div'>, Menu.ArrowTipBaseProps>>(
23
23
+
Menu.ArrowTip,
24
24
+
'arrowTip',
25
25
+
)
26
26
+
27
27
+
export const CheckboxItem = withContext<Assign<HTMLStyledProps<'div'>, Menu.CheckboxItemBaseProps>>(
28
28
+
Menu.CheckboxItem,
29
29
+
'item',
30
30
+
)
31
31
+
32
32
+
export const Content = withContext<Assign<HTMLStyledProps<'div'>, Menu.ContentBaseProps>>(
33
33
+
Menu.Content,
34
34
+
'content',
35
35
+
)
36
36
+
37
37
+
export const ContextTrigger = withContext<
38
38
+
Assign<HTMLStyledProps<'button'>, Menu.ContextTriggerBaseProps>
39
39
+
>(Menu.ContextTrigger, 'contextTrigger')
40
40
+
41
41
+
export const Indicator = withContext<Assign<HTMLStyledProps<'div'>, Menu.IndicatorBaseProps>>(
42
42
+
Menu.Indicator,
43
43
+
'indicator',
44
44
+
)
45
45
+
46
46
+
export const ItemGroupLabel = withContext<
47
47
+
Assign<HTMLStyledProps<'div'>, Menu.ItemGroupLabelBaseProps>
48
48
+
>(Menu.ItemGroupLabel, 'itemGroupLabel')
49
49
+
50
50
+
export const ItemGroup = withContext<Assign<HTMLStyledProps<'div'>, Menu.ItemGroupBaseProps>>(
51
51
+
Menu.ItemGroup,
52
52
+
'itemGroup',
53
53
+
)
54
54
+
55
55
+
export const ItemIndicator = withContext<
56
56
+
Assign<HTMLStyledProps<'div'>, Menu.ItemIndicatorBaseProps>
57
57
+
>(Menu.ItemIndicator, 'itemIndicator')
58
58
+
59
59
+
export const Item = withContext<Assign<HTMLStyledProps<'div'>, Menu.ItemBaseProps>>(
60
60
+
Menu.Item,
61
61
+
'item',
62
62
+
)
63
63
+
64
64
+
export const ItemText = withContext<Assign<HTMLStyledProps<'div'>, Menu.ItemTextBaseProps>>(
65
65
+
Menu.ItemText,
66
66
+
'itemText',
67
67
+
)
68
68
+
69
69
+
export const Positioner = withContext<Assign<HTMLStyledProps<'div'>, Menu.PositionerBaseProps>>(
70
70
+
Menu.Positioner,
71
71
+
'positioner',
72
72
+
)
73
73
+
74
74
+
export const RadioItemGroup = withContext<
75
75
+
Assign<HTMLStyledProps<'div'>, Menu.RadioItemGroupBaseProps>
76
76
+
>(Menu.RadioItemGroup, 'itemGroup')
77
77
+
78
78
+
export const RadioItem = withContext<Assign<HTMLStyledProps<'div'>, Menu.RadioItemBaseProps>>(
79
79
+
Menu.RadioItem,
80
80
+
'item',
81
81
+
)
82
82
+
83
83
+
export const Separator = withContext<Assign<HTMLStyledProps<'hr'>, Menu.SeparatorBaseProps>>(
84
84
+
Menu.Separator,
85
85
+
'separator',
86
86
+
)
87
87
+
88
88
+
export const TriggerItem = withContext<Assign<HTMLStyledProps<'div'>, Menu.TriggerItemBaseProps>>(
89
89
+
Menu.TriggerItem,
90
90
+
'triggerItem',
91
91
+
)
92
92
+
93
93
+
export const Trigger = withContext<Assign<HTMLStyledProps<'button'>, Menu.TriggerBaseProps>>(
94
94
+
Menu.Trigger,
95
95
+
'trigger',
96
96
+
)
97
97
+
98
98
+
export { MenuContext as Context } from '@ark-ui/solid'
+5
src/index.css
···
3
3
.lucide {
4
4
stroke-width: 3px;
5
5
}
6
6
+
7
7
+
.input,
8
8
+
.button--variant_outline {
9
9
+
border-bottom-width: 2px;
10
10
+
}
+5
-4
src/lib/render.ts
···
336
336
drawBackground();
337
337
338
338
if (pfpImg) {
339
339
-
const pfpSize = Math.min(renderCanvas.width, renderCanvas.height) * 0.5;
340
340
-
const pfpX = (renderCanvas.width - pfpSize) / 2;
341
341
-
const pfpY = (renderCanvas.height - pfpSize) / 2;
339
339
+
const centerX = renderCanvas.width / 2;
340
340
+
const centerY = renderCanvas.height / 2;
341
341
+
const baseRadius =
342
342
+
Math.min(renderCanvas.width, renderCanvas.height) * 0.15;
342
343
343
343
-
drawPfp(ctx, pfpImg, pfpX, pfpY, pfpSize);
344
344
+
drawPfp(ctx, pfpImg, centerX, centerY, baseRadius);
344
345
}
345
346
346
347
await videoSource.add(0, duration);