@domicile-desktop/component-library 0.0.0-alpha-ddde2bfbbaad → 0.0.0-alpha-e1457b57e6b2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -3
- package/src/AppChooser/AppChooser.tsx +186 -0
- package/src/PortalDialogs/AccountDialog.tsx +65 -36
- package/src/PortalDialogs/AppChooserDialog.tsx +18 -157
- package/src/PortalDialogs/PortalDialogs.tsx +19 -1
- package/src/PortalDialogs/ScreenCastDialog.tsx +33 -11
- /package/src/{PortalDialogs → AppChooser}/app-choices.ts +0 -0
- /package/src/{PortalDialogs → AppChooser}/useDescribedChoices.ts +0 -0
package/package.json
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"dependencies": {
|
|
3
3
|
"@base-ui/react": "^1.8.0",
|
|
4
4
|
"@cprussin/option-result": "^2.0.1",
|
|
5
|
-
"@domicile-desktop/sdk": "0.0.0-alpha-
|
|
6
|
-
"@domicile-desktop/system-apps": "0.0.0-alpha-
|
|
5
|
+
"@domicile-desktop/sdk": "0.0.0-alpha-e1457b57e6b2",
|
|
6
|
+
"@domicile-desktop/system-apps": "0.0.0-alpha-e1457b57e6b2",
|
|
7
7
|
"@phosphor-icons/react": "^2.1.10"
|
|
8
8
|
},
|
|
9
9
|
"description": "React components and the Panda CSS design system that Domicile's shells are built from.",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"exports": {
|
|
32
32
|
"./Accordion": "./src/Accordion/Accordion.tsx",
|
|
33
|
+
"./AppChooser": "./src/AppChooser/AppChooser.tsx",
|
|
33
34
|
"./Autocomplete": "./src/Autocomplete/Autocomplete.tsx",
|
|
34
35
|
"./Avatar": "./src/Avatar/Avatar.tsx",
|
|
35
36
|
"./Button": "./src/Button/Button.tsx",
|
|
@@ -102,5 +103,5 @@
|
|
|
102
103
|
"test:unit": "bun test"
|
|
103
104
|
},
|
|
104
105
|
"type": "module",
|
|
105
|
-
"version": "0.0.0-alpha-
|
|
106
|
+
"version": "0.0.0-alpha-e1457b57e6b2"
|
|
106
107
|
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { System } from "@domicile-desktop/sdk/system";
|
|
2
|
+
import { AppWindowIcon } from "@phosphor-icons/react/dist/ssr/AppWindow";
|
|
3
|
+
import { useId, useState } from "react";
|
|
4
|
+
import { css } from "../../styled-system/css";
|
|
5
|
+
import { flex, hstack } from "../../styled-system/patterns";
|
|
6
|
+
import { Button } from "../Button/Button";
|
|
7
|
+
import { keepInView, stepOf, steppedTo } from "../list-walk";
|
|
8
|
+
import { ModalDialog } from "../ModalDialog/ModalDialog";
|
|
9
|
+
import { preselected } from "./app-choices";
|
|
10
|
+
import { useDescribedChoices } from "./useDescribedChoices";
|
|
11
|
+
|
|
12
|
+
const ICON_SIZE = 24;
|
|
13
|
+
|
|
14
|
+
type Props = {
|
|
15
|
+
/** Desktop file IDs without `.desktop`, in the order offered. */
|
|
16
|
+
choices: readonly string[];
|
|
17
|
+
/** The type opened, whose defaults start picked; none when `undefined`. */
|
|
18
|
+
contentType: string | undefined;
|
|
19
|
+
/** A line under the title, such as who asks. */
|
|
20
|
+
description?: string | undefined;
|
|
21
|
+
/** The application chosen last time, which starts picked. */
|
|
22
|
+
lastChoice?: string | undefined;
|
|
23
|
+
onCancel: () => void;
|
|
24
|
+
/** Called with the ID of the application to open with. */
|
|
25
|
+
onChoose: (id: string) => void;
|
|
26
|
+
screen: string | undefined;
|
|
27
|
+
/** Where the applications' desktop entries are read. */
|
|
28
|
+
system: System;
|
|
29
|
+
title: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Pick an application to open a file or URI with. The last choice, else the
|
|
34
|
+
* type's default, else the first offered, starts picked. Dismissing it
|
|
35
|
+
* cancels.
|
|
36
|
+
*/
|
|
37
|
+
export const AppChooser = ({
|
|
38
|
+
choices,
|
|
39
|
+
contentType,
|
|
40
|
+
description,
|
|
41
|
+
lastChoice,
|
|
42
|
+
onCancel,
|
|
43
|
+
onChoose,
|
|
44
|
+
screen,
|
|
45
|
+
system,
|
|
46
|
+
title,
|
|
47
|
+
}: Props) => {
|
|
48
|
+
const listId = useId();
|
|
49
|
+
const described = useDescribedChoices(system, choices, contentType);
|
|
50
|
+
const [picked, setPicked] = useState<string | undefined>();
|
|
51
|
+
const apps = described?.apps ?? [];
|
|
52
|
+
const first =
|
|
53
|
+
described === undefined
|
|
54
|
+
? undefined
|
|
55
|
+
: preselected(choices, lastChoice, described.defaults);
|
|
56
|
+
// A pick the caller has since taken away no longer counts.
|
|
57
|
+
const chosen =
|
|
58
|
+
picked !== undefined && choices.includes(picked) ? picked : first;
|
|
59
|
+
const at = apps.findIndex(({ id }) => id === chosen);
|
|
60
|
+
const choose = (id: string | undefined) => {
|
|
61
|
+
if (id !== undefined) {
|
|
62
|
+
onChoose(id);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
return (
|
|
66
|
+
<ModalDialog
|
|
67
|
+
closeButton={false}
|
|
68
|
+
footer={
|
|
69
|
+
<>
|
|
70
|
+
<Button onClick={onCancel} variant="outline">
|
|
71
|
+
Cancel
|
|
72
|
+
</Button>
|
|
73
|
+
<Button
|
|
74
|
+
disabled={chosen === undefined}
|
|
75
|
+
onClick={() => {
|
|
76
|
+
choose(chosen);
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
Open
|
|
80
|
+
</Button>
|
|
81
|
+
</>
|
|
82
|
+
}
|
|
83
|
+
onOpenChange={(open) => {
|
|
84
|
+
if (!open) {
|
|
85
|
+
onCancel();
|
|
86
|
+
}
|
|
87
|
+
}}
|
|
88
|
+
open
|
|
89
|
+
screen={screen}
|
|
90
|
+
title={title}
|
|
91
|
+
>
|
|
92
|
+
{description !== undefined && (
|
|
93
|
+
<p className={descriptionStyles}>{description}</p>
|
|
94
|
+
)}
|
|
95
|
+
<div
|
|
96
|
+
aria-activedescendant={at === -1 ? undefined : optionId(listId, at)}
|
|
97
|
+
aria-label="Applications"
|
|
98
|
+
className={listStyles}
|
|
99
|
+
onKeyDown={(event) => {
|
|
100
|
+
const step = stepOf(event);
|
|
101
|
+
const next = apps[steppedTo(at, step ?? 0, apps.length)];
|
|
102
|
+
if (event.key === "Enter") {
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
choose(chosen);
|
|
105
|
+
} else if (step !== undefined && next !== undefined) {
|
|
106
|
+
event.preventDefault();
|
|
107
|
+
setPicked(next.id);
|
|
108
|
+
}
|
|
109
|
+
}}
|
|
110
|
+
role="listbox"
|
|
111
|
+
tabIndex={0}
|
|
112
|
+
>
|
|
113
|
+
{apps.map((app, index) => (
|
|
114
|
+
// biome-ignore lint/a11y/useKeyWithClickEvents: the listbox handles the keyboard via `aria-activedescendant`
|
|
115
|
+
<div
|
|
116
|
+
aria-selected={app.id === chosen}
|
|
117
|
+
className={optionStyles}
|
|
118
|
+
id={optionId(listId, index)}
|
|
119
|
+
key={app.id}
|
|
120
|
+
onClick={() => {
|
|
121
|
+
setPicked(app.id);
|
|
122
|
+
}}
|
|
123
|
+
onDoubleClick={() => {
|
|
124
|
+
choose(app.id);
|
|
125
|
+
}}
|
|
126
|
+
ref={app.id === chosen ? keepInView : undefined}
|
|
127
|
+
role="option"
|
|
128
|
+
tabIndex={-1}
|
|
129
|
+
>
|
|
130
|
+
{app.icon === undefined ? (
|
|
131
|
+
<AppWindowIcon size={ICON_SIZE} />
|
|
132
|
+
) : (
|
|
133
|
+
<img
|
|
134
|
+
alt=""
|
|
135
|
+
className={iconStyles}
|
|
136
|
+
height={ICON_SIZE}
|
|
137
|
+
src={app.icon}
|
|
138
|
+
width={ICON_SIZE}
|
|
139
|
+
/>
|
|
140
|
+
)}
|
|
141
|
+
<span className={nameStyles}>{app.name}</span>
|
|
142
|
+
</div>
|
|
143
|
+
))}
|
|
144
|
+
</div>
|
|
145
|
+
</ModalDialog>
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const descriptionStyles = css({ color: "muted", fontSize: "sm", margin: 0 });
|
|
150
|
+
|
|
151
|
+
const listStyles = flex({
|
|
152
|
+
direction: "column",
|
|
153
|
+
gap: 0.5,
|
|
154
|
+
marginBlockStart: 3,
|
|
155
|
+
maxBlockSize: 80,
|
|
156
|
+
overflowY: "auto",
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// As the file picker's rows.
|
|
160
|
+
const optionStyles = hstack({
|
|
161
|
+
"&[aria-selected=true]": {
|
|
162
|
+
backgroundImage:
|
|
163
|
+
"linear-gradient(to right, color-mix(in oklab, {colors.accent} 30%, transparent), color-mix(in oklab, {colors.accent} 8%, transparent))",
|
|
164
|
+
},
|
|
165
|
+
borderRadius: "md",
|
|
166
|
+
color: "foreground",
|
|
167
|
+
cursor: "pointer",
|
|
168
|
+
gap: 3,
|
|
169
|
+
paddingBlock: 1.5,
|
|
170
|
+
paddingInline: 2,
|
|
171
|
+
transition: "background-color {durations.fast} {easings.out}",
|
|
172
|
+
userSelect: "none",
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const iconStyles = css({ flexShrink: 0, objectFit: "contain" });
|
|
176
|
+
|
|
177
|
+
const nameStyles = css({
|
|
178
|
+
fontSize: "sm",
|
|
179
|
+
fontWeight: "medium",
|
|
180
|
+
overflow: "hidden",
|
|
181
|
+
textOverflow: "ellipsis",
|
|
182
|
+
whiteSpace: "nowrap",
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const optionId = (listId: string, index: number): string =>
|
|
186
|
+
`${listId}-${String(index)}`;
|
|
@@ -1,57 +1,86 @@
|
|
|
1
1
|
import type { AccountBody, PortalAnswer } from "@domicile-desktop/sdk/portal";
|
|
2
2
|
import { PortalAnswer as Answer } from "@domicile-desktop/sdk/portal";
|
|
3
|
+
import type { System } from "@domicile-desktop/sdk/system";
|
|
3
4
|
import { css } from "../../styled-system/css";
|
|
5
|
+
import { hstack } from "../../styled-system/patterns";
|
|
4
6
|
import { Button } from "../Button/Button";
|
|
5
7
|
import { ModalDialog } from "../ModalDialog/ModalDialog";
|
|
8
|
+
import { usePictureUrl } from "../usePictureUrl/usePictureUrl";
|
|
6
9
|
|
|
7
10
|
type Props = {
|
|
8
11
|
answer: (answer: PortalAnswer) => void;
|
|
9
12
|
/** Who asks, as the dialog names them. */
|
|
10
13
|
asker: string;
|
|
11
14
|
body: AccountBody;
|
|
15
|
+
/** Reads the user's picture. */
|
|
16
|
+
files: Pick<System, "readFile">;
|
|
12
17
|
screen: string | undefined;
|
|
13
18
|
};
|
|
14
19
|
|
|
15
20
|
/**
|
|
16
|
-
* Whether an application may have the user's name and picture
|
|
17
|
-
* keeps them.
|
|
21
|
+
* Whether an application may have the user's name and picture, showing both.
|
|
22
|
+
* Dismissing it keeps them.
|
|
18
23
|
*/
|
|
19
|
-
export const AccountDialog = ({
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
24
|
+
export const AccountDialog = ({
|
|
25
|
+
answer,
|
|
26
|
+
asker,
|
|
27
|
+
body,
|
|
28
|
+
files,
|
|
29
|
+
screen,
|
|
30
|
+
}: Props) => {
|
|
31
|
+
const picture = usePictureUrl(files, body.image);
|
|
32
|
+
return (
|
|
33
|
+
<ModalDialog
|
|
34
|
+
closeButton={false}
|
|
35
|
+
footer={
|
|
36
|
+
<>
|
|
37
|
+
<Button
|
|
38
|
+
onClick={() => {
|
|
39
|
+
answer(Answer.Canceled());
|
|
40
|
+
}}
|
|
41
|
+
variant="outline"
|
|
42
|
+
>
|
|
43
|
+
Don't share
|
|
44
|
+
</Button>
|
|
45
|
+
<Button
|
|
46
|
+
onClick={() => {
|
|
47
|
+
answer(Answer.Access());
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
Share
|
|
51
|
+
</Button>
|
|
52
|
+
</>
|
|
44
53
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
onOpenChange={(open) => {
|
|
55
|
+
if (!open) {
|
|
56
|
+
answer(Answer.Canceled());
|
|
57
|
+
}
|
|
58
|
+
}}
|
|
59
|
+
open
|
|
60
|
+
screen={screen}
|
|
61
|
+
title="Share your name and picture?"
|
|
62
|
+
>
|
|
63
|
+
<p className={askerStyles}>{`${asker} asks`}</p>
|
|
64
|
+
{body.reason !== undefined && <p className={textStyles}>{body.reason}</p>}
|
|
65
|
+
<div className={userStyles}>
|
|
66
|
+
{picture !== undefined && (
|
|
67
|
+
<img alt={body.name} className={pictureStyles} src={picture} />
|
|
68
|
+
)}
|
|
69
|
+
<span className={textStyles}>{body.name}</span>
|
|
70
|
+
</div>
|
|
71
|
+
</ModalDialog>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
54
74
|
|
|
55
75
|
const askerStyles = css({ color: "muted", fontSize: "sm", margin: 0 });
|
|
56
76
|
|
|
57
77
|
const textStyles = css({ color: "foreground", margin: 0 });
|
|
78
|
+
|
|
79
|
+
const userStyles = hstack({ gap: 3, marginBlockStart: 3 });
|
|
80
|
+
|
|
81
|
+
const pictureStyles = css({
|
|
82
|
+
blockSize: 10,
|
|
83
|
+
borderRadius: "full",
|
|
84
|
+
inlineSize: 10,
|
|
85
|
+
objectFit: "cover",
|
|
86
|
+
});
|
|
@@ -4,17 +4,7 @@ import type {
|
|
|
4
4
|
} from "@domicile-desktop/sdk/portal";
|
|
5
5
|
import { PortalAnswer as Answer } from "@domicile-desktop/sdk/portal";
|
|
6
6
|
import type { System } from "@domicile-desktop/sdk/system";
|
|
7
|
-
import {
|
|
8
|
-
import { useId, useState } from "react";
|
|
9
|
-
import { css } from "../../styled-system/css";
|
|
10
|
-
import { flex, hstack } from "../../styled-system/patterns";
|
|
11
|
-
import { Button } from "../Button/Button";
|
|
12
|
-
import { keepInView, stepOf, steppedTo } from "../list-walk";
|
|
13
|
-
import { ModalDialog } from "../ModalDialog/ModalDialog";
|
|
14
|
-
import { preselected } from "./app-choices";
|
|
15
|
-
import { useDescribedChoices } from "./useDescribedChoices";
|
|
16
|
-
|
|
17
|
-
const ICON_SIZE = 24;
|
|
7
|
+
import { AppChooser } from "../AppChooser/AppChooser";
|
|
18
8
|
|
|
19
9
|
type Props = {
|
|
20
10
|
answer: (answer: PortalAnswer) => void;
|
|
@@ -26,162 +16,33 @@ type Props = {
|
|
|
26
16
|
system: System;
|
|
27
17
|
};
|
|
28
18
|
|
|
29
|
-
/**
|
|
30
|
-
* Pick an application to open a file or URI with. The last choice, else the
|
|
31
|
-
* type's default, starts picked. Dismissing it cancels.
|
|
32
|
-
*/
|
|
19
|
+
/** The AppChooser portal's {@link AppChooser}. Dismissing it cancels. */
|
|
33
20
|
export const AppChooserDialog = ({
|
|
34
21
|
answer,
|
|
35
22
|
asker,
|
|
36
23
|
body,
|
|
37
24
|
screen,
|
|
38
25
|
system,
|
|
39
|
-
}: Props) =>
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const chosen =
|
|
50
|
-
picked !== undefined && body.choices.includes(picked) ? picked : first;
|
|
51
|
-
const at = apps.findIndex(({ id }) => id === chosen);
|
|
52
|
-
const choose = (id: string | undefined) => {
|
|
53
|
-
if (id !== undefined) {
|
|
26
|
+
}: Props) => (
|
|
27
|
+
<AppChooser
|
|
28
|
+
choices={body.choices}
|
|
29
|
+
contentType={body.contentType}
|
|
30
|
+
description={`${asker} asks`}
|
|
31
|
+
lastChoice={body.lastChoice}
|
|
32
|
+
onCancel={() => {
|
|
33
|
+
answer(Answer.Canceled());
|
|
34
|
+
}}
|
|
35
|
+
onChoose={(id) => {
|
|
54
36
|
answer(Answer.AppChooser(id));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
<>
|
|
62
|
-
<Button
|
|
63
|
-
onClick={() => {
|
|
64
|
-
answer(Answer.Canceled());
|
|
65
|
-
}}
|
|
66
|
-
variant="outline"
|
|
67
|
-
>
|
|
68
|
-
Cancel
|
|
69
|
-
</Button>
|
|
70
|
-
<Button
|
|
71
|
-
disabled={chosen === undefined}
|
|
72
|
-
onClick={() => {
|
|
73
|
-
choose(chosen);
|
|
74
|
-
}}
|
|
75
|
-
>
|
|
76
|
-
Open
|
|
77
|
-
</Button>
|
|
78
|
-
</>
|
|
79
|
-
}
|
|
80
|
-
onOpenChange={(open) => {
|
|
81
|
-
if (!open) {
|
|
82
|
-
answer(Answer.Canceled());
|
|
83
|
-
}
|
|
84
|
-
}}
|
|
85
|
-
open
|
|
86
|
-
screen={screen}
|
|
87
|
-
title={titleOf(body)}
|
|
88
|
-
>
|
|
89
|
-
<p className={askerStyles}>{`${asker} asks`}</p>
|
|
90
|
-
<div
|
|
91
|
-
aria-activedescendant={at === -1 ? undefined : optionId(listId, at)}
|
|
92
|
-
aria-label="Applications"
|
|
93
|
-
className={listStyles}
|
|
94
|
-
onKeyDown={(event) => {
|
|
95
|
-
const step = stepOf(event);
|
|
96
|
-
const next = apps[steppedTo(at, step ?? 0, apps.length)];
|
|
97
|
-
if (event.key === "Enter") {
|
|
98
|
-
event.preventDefault();
|
|
99
|
-
choose(chosen);
|
|
100
|
-
} else if (step !== undefined && next !== undefined) {
|
|
101
|
-
event.preventDefault();
|
|
102
|
-
setPicked(next.id);
|
|
103
|
-
}
|
|
104
|
-
}}
|
|
105
|
-
role="listbox"
|
|
106
|
-
tabIndex={0}
|
|
107
|
-
>
|
|
108
|
-
{apps.map((app, index) => (
|
|
109
|
-
// biome-ignore lint/a11y/useKeyWithClickEvents: the listbox handles the keyboard via `aria-activedescendant`
|
|
110
|
-
<div
|
|
111
|
-
aria-selected={app.id === chosen}
|
|
112
|
-
className={optionStyles}
|
|
113
|
-
id={optionId(listId, index)}
|
|
114
|
-
key={app.id}
|
|
115
|
-
onClick={() => {
|
|
116
|
-
setPicked(app.id);
|
|
117
|
-
}}
|
|
118
|
-
onDoubleClick={() => {
|
|
119
|
-
choose(app.id);
|
|
120
|
-
}}
|
|
121
|
-
ref={app.id === chosen ? keepInView : undefined}
|
|
122
|
-
role="option"
|
|
123
|
-
tabIndex={-1}
|
|
124
|
-
>
|
|
125
|
-
{app.icon === undefined ? (
|
|
126
|
-
<AppWindowIcon size={ICON_SIZE} />
|
|
127
|
-
) : (
|
|
128
|
-
<img
|
|
129
|
-
alt=""
|
|
130
|
-
className={iconStyles}
|
|
131
|
-
height={ICON_SIZE}
|
|
132
|
-
src={app.icon}
|
|
133
|
-
width={ICON_SIZE}
|
|
134
|
-
/>
|
|
135
|
-
)}
|
|
136
|
-
<span className={nameStyles}>{app.name}</span>
|
|
137
|
-
</div>
|
|
138
|
-
))}
|
|
139
|
-
</div>
|
|
140
|
-
</ModalDialog>
|
|
141
|
-
);
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
const askerStyles = css({ color: "muted", fontSize: "sm", margin: 0 });
|
|
145
|
-
|
|
146
|
-
const listStyles = flex({
|
|
147
|
-
direction: "column",
|
|
148
|
-
gap: 0.5,
|
|
149
|
-
marginBlockStart: 3,
|
|
150
|
-
maxBlockSize: 80,
|
|
151
|
-
overflowY: "auto",
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// As the file picker's rows.
|
|
155
|
-
const optionStyles = hstack({
|
|
156
|
-
"&[aria-selected=true]": {
|
|
157
|
-
backgroundImage:
|
|
158
|
-
"linear-gradient(to right, color-mix(in oklab, {colors.accent} 30%, transparent), color-mix(in oklab, {colors.accent} 8%, transparent))",
|
|
159
|
-
},
|
|
160
|
-
borderRadius: "md",
|
|
161
|
-
color: "foreground",
|
|
162
|
-
cursor: "pointer",
|
|
163
|
-
gap: 3,
|
|
164
|
-
paddingBlock: 1.5,
|
|
165
|
-
paddingInline: 2,
|
|
166
|
-
transition: "background-color {durations.fast} {easings.out}",
|
|
167
|
-
userSelect: "none",
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
const iconStyles = css({ flexShrink: 0, objectFit: "contain" });
|
|
171
|
-
|
|
172
|
-
const nameStyles = css({
|
|
173
|
-
fontSize: "sm",
|
|
174
|
-
fontWeight: "medium",
|
|
175
|
-
overflow: "hidden",
|
|
176
|
-
textOverflow: "ellipsis",
|
|
177
|
-
whiteSpace: "nowrap",
|
|
178
|
-
});
|
|
37
|
+
}}
|
|
38
|
+
screen={screen}
|
|
39
|
+
system={system}
|
|
40
|
+
title={titleOf(body)}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
179
43
|
|
|
180
44
|
/** What the dialog opens, as its title names it. */
|
|
181
45
|
const titleOf = ({ filename, uri }: AppChooserBody): string => {
|
|
182
46
|
const opened = filename ?? uri;
|
|
183
47
|
return opened === undefined ? "Open with" : `Open ${opened} with`;
|
|
184
48
|
};
|
|
185
|
-
|
|
186
|
-
const optionId = (listId: string, index: number): string =>
|
|
187
|
-
`${listId}-${String(index)}`;
|
|
@@ -4,6 +4,7 @@ import type { Capturing, PortalRequest } from "@domicile-desktop/sdk/portal";
|
|
|
4
4
|
import {
|
|
5
5
|
answerPortalRequest,
|
|
6
6
|
CapturingKind,
|
|
7
|
+
CastSourceKind,
|
|
7
8
|
PortalAnswer,
|
|
8
9
|
PortalKind,
|
|
9
10
|
stopCapturing,
|
|
@@ -14,6 +15,7 @@ import type { System, SystemHost } from "@domicile-desktop/sdk/system";
|
|
|
14
15
|
import { system } from "@domicile-desktop/sdk/system";
|
|
15
16
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
16
17
|
import { listDirectory } from "../FilePicker/list-directory";
|
|
18
|
+
import type { App } from "../useApps/useApps";
|
|
17
19
|
import { useApps } from "../useApps/useApps";
|
|
18
20
|
import { AccessDialog } from "./AccessDialog";
|
|
19
21
|
import { AccountDialog } from "./AccountDialog";
|
|
@@ -82,7 +84,7 @@ export const PortalDialogs = ({
|
|
|
82
84
|
);
|
|
83
85
|
const [sessions, setSessions] = useState<readonly Capturing[]>([]);
|
|
84
86
|
const apps = useApps(files, [
|
|
85
|
-
...requests.
|
|
87
|
+
...requests.flatMap(appIds),
|
|
86
88
|
...sessions.map((session) => session.appId),
|
|
87
89
|
]);
|
|
88
90
|
|
|
@@ -120,6 +122,7 @@ export const PortalDialogs = ({
|
|
|
120
122
|
answer={(answer) => {
|
|
121
123
|
answerPortalRequest(host, shown.id, answer);
|
|
122
124
|
}}
|
|
125
|
+
apps={apps}
|
|
123
126
|
asker={apps(shown.appId).name}
|
|
124
127
|
key={shown.id}
|
|
125
128
|
list={list}
|
|
@@ -136,6 +139,7 @@ export const PortalDialogs = ({
|
|
|
136
139
|
/** The dialog for `request`'s kind. */
|
|
137
140
|
const Dialog = ({
|
|
138
141
|
answer,
|
|
142
|
+
apps,
|
|
139
143
|
asker,
|
|
140
144
|
list,
|
|
141
145
|
request,
|
|
@@ -144,6 +148,7 @@ const Dialog = ({
|
|
|
144
148
|
system: files,
|
|
145
149
|
}: {
|
|
146
150
|
answer: (answer: PortalAnswer) => void;
|
|
151
|
+
apps: (appId: string) => App;
|
|
147
152
|
asker: string;
|
|
148
153
|
list: (path: string) => Promise<readonly string[]>;
|
|
149
154
|
request: PortalRequest;
|
|
@@ -204,6 +209,7 @@ const Dialog = ({
|
|
|
204
209
|
answer={answer}
|
|
205
210
|
asker={asker}
|
|
206
211
|
body={request.body}
|
|
212
|
+
files={files}
|
|
207
213
|
screen={screen}
|
|
208
214
|
/>
|
|
209
215
|
);
|
|
@@ -249,6 +255,7 @@ const Dialog = ({
|
|
|
249
255
|
return (
|
|
250
256
|
<ScreenCastDialog
|
|
251
257
|
answer={answer}
|
|
258
|
+
apps={apps}
|
|
252
259
|
asker={asker}
|
|
253
260
|
body={request.body}
|
|
254
261
|
screen={screen}
|
|
@@ -300,6 +307,17 @@ const screenFor = (
|
|
|
300
307
|
return parents ?? screen;
|
|
301
308
|
};
|
|
302
309
|
|
|
310
|
+
/** The applications `request` names: who asks, and windows it may record. */
|
|
311
|
+
const appIds = (request: PortalRequest): string[] =>
|
|
312
|
+
request.kind === PortalKind.ScreenCast
|
|
313
|
+
? [
|
|
314
|
+
request.appId,
|
|
315
|
+
...request.body.sources.flatMap((source) =>
|
|
316
|
+
source.kind === CastSourceKind.Window ? [source.appId] : [],
|
|
317
|
+
),
|
|
318
|
+
]
|
|
319
|
+
: [request.appId];
|
|
320
|
+
|
|
303
321
|
/** Whether `request` is a question this draws a dialog for. */
|
|
304
322
|
const isAsked = (request: PortalRequest): boolean => {
|
|
305
323
|
switch (request.kind) {
|
|
@@ -17,10 +17,13 @@ import { css } from "../../styled-system/css";
|
|
|
17
17
|
import { Button } from "../Button/Button";
|
|
18
18
|
import { ModalDialog } from "../ModalDialog/ModalDialog";
|
|
19
19
|
import { Tabs } from "../Tabs/Tabs";
|
|
20
|
+
import type { App } from "../useApps/useApps";
|
|
20
21
|
import { RegionPicker } from "./RegionPicker";
|
|
21
22
|
|
|
22
23
|
type Props = {
|
|
23
24
|
answer: (answer: PortalAnswer) => void;
|
|
25
|
+
/** Names a window's application by its Wayland app id. */
|
|
26
|
+
apps: (appId: string) => App;
|
|
24
27
|
/** Who asks, as the dialog names them. */
|
|
25
28
|
asker: string;
|
|
26
29
|
body: ScreenCastBody;
|
|
@@ -33,7 +36,13 @@ type Props = {
|
|
|
33
36
|
* asks for one. Picks one, or several when the application asks for more.
|
|
34
37
|
* Dismissing it cancels.
|
|
35
38
|
*/
|
|
36
|
-
export const ScreenCastDialog = ({
|
|
39
|
+
export const ScreenCastDialog = ({
|
|
40
|
+
answer,
|
|
41
|
+
apps,
|
|
42
|
+
asker,
|
|
43
|
+
body,
|
|
44
|
+
screen,
|
|
45
|
+
}: Props) => {
|
|
37
46
|
const [picked, setPicked] = useState<readonly string[]>([]);
|
|
38
47
|
const [drawing, setDrawing] = useState(false);
|
|
39
48
|
const windows = body.sources.filter(
|
|
@@ -72,8 +81,8 @@ export const ScreenCastDialog = ({ answer, asker, body, screen }: Props) => {
|
|
|
72
81
|
key={sourceKey(source)}
|
|
73
82
|
value={sourceKey(source)}
|
|
74
83
|
>
|
|
75
|
-
<SourceIcon source={source} />
|
|
76
|
-
<span>{sourceName(source)}</span>
|
|
84
|
+
<SourceIcon app={appOf(apps, source)} source={source} />
|
|
85
|
+
<span>{sourceName(source, appOf(apps, source))}</span>
|
|
77
86
|
</Toggle>
|
|
78
87
|
))}
|
|
79
88
|
</ToggleGroup>
|
|
@@ -152,13 +161,19 @@ export const ScreenCastDialog = ({ answer, asker, body, screen }: Props) => {
|
|
|
152
161
|
};
|
|
153
162
|
|
|
154
163
|
/** A window's application icon, a window outline without one, or a screen. */
|
|
155
|
-
const SourceIcon = ({
|
|
164
|
+
const SourceIcon = ({
|
|
165
|
+
app,
|
|
166
|
+
source,
|
|
167
|
+
}: {
|
|
168
|
+
app: App | undefined;
|
|
169
|
+
source: CastSource;
|
|
170
|
+
}) => {
|
|
156
171
|
switch (source.kind) {
|
|
157
172
|
case CastSourceKind.Window:
|
|
158
|
-
return
|
|
173
|
+
return app?.icon === undefined ? (
|
|
159
174
|
<AppWindowIcon aria-hidden className={iconStyles} />
|
|
160
175
|
) : (
|
|
161
|
-
<img alt="" className={iconStyles} src={
|
|
176
|
+
<img alt="" className={iconStyles} src={app.icon} />
|
|
162
177
|
);
|
|
163
178
|
case CastSourceKind.Monitor:
|
|
164
179
|
case CastSourceKind.Region:
|
|
@@ -226,14 +241,21 @@ const sourceKey = (source: CastSource): string => {
|
|
|
226
241
|
}
|
|
227
242
|
};
|
|
228
243
|
|
|
229
|
-
/**
|
|
230
|
-
const
|
|
244
|
+
/** A window's application, or `undefined` for a screen or a window with no app id. */
|
|
245
|
+
const appOf = (
|
|
246
|
+
apps: (appId: string) => App,
|
|
247
|
+
source: CastSource,
|
|
248
|
+
): App | undefined =>
|
|
249
|
+
source.kind === CastSourceKind.Window && source.appId !== ""
|
|
250
|
+
? apps(source.appId)
|
|
251
|
+
: undefined;
|
|
252
|
+
|
|
253
|
+
/** How the picker names a source, a window by its `app` too. */
|
|
254
|
+
const sourceName = (source: CastSource, app: App | undefined): string => {
|
|
231
255
|
switch (source.kind) {
|
|
232
256
|
case CastSourceKind.Window: {
|
|
233
257
|
const title = source.title === "" ? "Untitled window" : source.title;
|
|
234
|
-
return
|
|
235
|
-
? title
|
|
236
|
-
: `${source.appName}: ${title}`;
|
|
258
|
+
return app === undefined ? title : `${app.name}: ${title}`;
|
|
237
259
|
}
|
|
238
260
|
case CastSourceKind.Monitor:
|
|
239
261
|
return source.description === ""
|
|
File without changes
|
|
File without changes
|