@domicile-desktop/component-library 0.0.0-alpha-59d60d9540df → 0.0.0-alpha-75f1c331c3ad
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/README.md +4 -1
- package/package.json +6 -3
- package/src/PortalDialogs/CapturingIndicator.tsx +20 -19
- package/src/PortalDialogs/PortalDialogs.tsx +36 -15
- package/src/PortalDialogs/app-choices.ts +19 -61
- package/src/PortalDialogs/source-name.ts +14 -0
- package/src/useApps/useApps.ts +72 -0
- package/src/usePortalWallpaper/usePortalWallpaper.ts +20 -0
- package/src/PortalDialogs/app-name.ts +0 -3
package/README.md
CHANGED
|
@@ -30,7 +30,8 @@ Each export is `@domicile-desktop/component-library/<name>`.
|
|
|
30
30
|
| `list-walk` | Arrow-key movement of a highlight through a list while focus stays in an input. |
|
|
31
31
|
| `ModalDialog` | Modal dialog with `title`, `footer` and `trigger` props. |
|
|
32
32
|
| `ContextMenu` | Menu opened at a point, such as a right click's. |
|
|
33
|
-
| `PortalDialogs` | Every dialog applications ask for through the desktop portal. Mount one per shell with `host={domicile}`; it refuses kinds it cannot draw. See [PORTALS.md](/docs/architecture/PORTALS.md). |
|
|
33
|
+
| `PortalDialogs` | Every dialog applications ask for through the desktop portal. Mount one per shell with `host={domicile}`; it refuses kinds it cannot draw. `omitScreenCasts` leaves screen casts out of its indicator. See [PORTALS.md](/docs/architecture/PORTALS.md). |
|
|
34
|
+
| `source-name` | `sourceName`: how a screen cast's source is named. |
|
|
34
35
|
| `Popover` | Non-modal panel anchored to the control that opened it. |
|
|
35
36
|
| `Provider` | base-ui `DirectionProvider` wrapper. Every app roots its tree in it. |
|
|
36
37
|
| `Screen` | Renders its children once per selected display. |
|
|
@@ -46,7 +47,9 @@ Each export is `@domicile-desktop/component-library/<name>`.
|
|
|
46
47
|
| `Toaster` | Stack of toasts in a corner. The caller renders each card. |
|
|
47
48
|
| `ThemeProvider` | Light/dark theme state. Sets `data-theme` on `<html>`. |
|
|
48
49
|
| `ThemeSwitch` | Button that flips the theme. |
|
|
50
|
+
| `useApps` | Hook: names and icons of applications by app id, from their desktop entries. |
|
|
49
51
|
| `usePictureUrl` | Hook: a URL an `<img>` can show for a file read through the SDK's `system`. |
|
|
52
|
+
| `usePortalWallpaper` | Hook: the pictures applications set through the Wallpaper portal, as paths. |
|
|
50
53
|
| `theme-core`, `theme-source`, `standalone-theme-source` | Theme types and sources. Use the standalone source where there is no compositor, such as Storybook. |
|
|
51
54
|
| `control-sizes` | `Size` type and `SIZES` array for sized controls. |
|
|
52
55
|
| `spacing` | rem value of one spacing step, for runtime math. |
|
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-75f1c331c3ad",
|
|
6
|
+
"@domicile-desktop/system-apps": "0.0.0-alpha-75f1c331c3ad",
|
|
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.",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"./SlideOver": "./src/SlideOver/SlideOver.tsx",
|
|
57
57
|
"./Slider": "./src/Slider/Slider.tsx",
|
|
58
58
|
"./Switch": "./src/Switch/Switch.tsx",
|
|
59
|
+
"./source-name": "./src/PortalDialogs/source-name.ts",
|
|
59
60
|
"./spacing": "./src/spacing.ts",
|
|
60
61
|
"./standalone-theme-source": "./src/ThemeSwitch/standalone-theme-source.ts",
|
|
61
62
|
"./TabRail": "./src/TabRail/TabRail.tsx",
|
|
@@ -66,7 +67,9 @@
|
|
|
66
67
|
"./Toaster": "./src/Toaster/Toaster.tsx",
|
|
67
68
|
"./theme-core": "./src/ThemeSwitch/theme-core.ts",
|
|
68
69
|
"./theme-source": "./src/ThemeSwitch/theme-source.ts",
|
|
70
|
+
"./useApps": "./src/useApps/useApps.ts",
|
|
69
71
|
"./usePictureUrl": "./src/usePictureUrl/usePictureUrl.ts",
|
|
72
|
+
"./usePortalWallpaper": "./src/usePortalWallpaper/usePortalWallpaper.ts",
|
|
70
73
|
"./vite-shell": "./src/vite-shell.ts"
|
|
71
74
|
},
|
|
72
75
|
"files": [
|
|
@@ -99,5 +102,5 @@
|
|
|
99
102
|
"test:unit": "bun test"
|
|
100
103
|
},
|
|
101
104
|
"type": "module",
|
|
102
|
-
"version": "0.0.0-alpha-
|
|
105
|
+
"version": "0.0.0-alpha-75f1c331c3ad"
|
|
103
106
|
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
1
|
+
import type { Capturing } from "@domicile-desktop/sdk/portal";
|
|
2
|
+
import { CapturingKind } from "@domicile-desktop/sdk/portal";
|
|
3
3
|
import { css } from "../../styled-system/css";
|
|
4
4
|
import { flex, hstack } from "../../styled-system/patterns";
|
|
5
5
|
import { Button } from "../Button/Button";
|
|
6
6
|
import { useScreenRegion } from "../Screen/DisplayProvider";
|
|
7
|
-
import {
|
|
7
|
+
import type { App } from "../useApps/useApps";
|
|
8
8
|
import { deviceNames } from "./device-names";
|
|
9
|
+
import { sourceName } from "./source-name";
|
|
9
10
|
|
|
10
11
|
type Props = {
|
|
12
|
+
apps: (appId: string) => App;
|
|
11
13
|
sessions: readonly Capturing[];
|
|
12
14
|
stop: (id: number) => void;
|
|
13
15
|
screen: string | undefined;
|
|
@@ -17,14 +19,14 @@ type Props = {
|
|
|
17
19
|
* A row per session that records the screen or controls or captures input,
|
|
18
20
|
* naming its application and what it holds, with a button that ends it.
|
|
19
21
|
*/
|
|
20
|
-
export const CapturingIndicator = ({ screen, sessions, stop }: Props) => {
|
|
22
|
+
export const CapturingIndicator = ({ apps, screen, sessions, stop }: Props) => {
|
|
21
23
|
const region = useScreenRegion(screen);
|
|
22
24
|
return sessions.length === 0 ? undefined : (
|
|
23
25
|
<div className={regionStyles} style={region}>
|
|
24
26
|
<ul aria-label="Sharing and remote control" className={listStyles}>
|
|
25
27
|
{sessions.map((session) => (
|
|
26
28
|
<li className={itemStyles} key={session.id}>
|
|
27
|
-
<
|
|
29
|
+
<AppRow app={apps(session.appId)} session={session} />
|
|
28
30
|
<Button
|
|
29
31
|
onClick={() => {
|
|
30
32
|
stop(session.id);
|
|
@@ -62,6 +64,8 @@ const listStyles = flex({
|
|
|
62
64
|
position: "absolute",
|
|
63
65
|
});
|
|
64
66
|
|
|
67
|
+
const iconStyles = css({ blockSize: 4, flexShrink: 0, inlineSize: 4 });
|
|
68
|
+
|
|
65
69
|
const itemStyles = hstack({
|
|
66
70
|
backgroundColor: "card",
|
|
67
71
|
border: "1px solid {colors.warning}",
|
|
@@ -76,9 +80,18 @@ const itemStyles = hstack({
|
|
|
76
80
|
pointerEvents: "auto",
|
|
77
81
|
});
|
|
78
82
|
|
|
83
|
+
/** A session's application icon, when it has one, and what it holds. */
|
|
84
|
+
const AppRow = ({ app, session }: { app: App; session: Capturing }) => (
|
|
85
|
+
<>
|
|
86
|
+
{app.icon !== undefined && (
|
|
87
|
+
<img alt="" className={iconStyles} src={app.icon} />
|
|
88
|
+
)}
|
|
89
|
+
<span>{summary(session, app.name)}</span>
|
|
90
|
+
</>
|
|
91
|
+
);
|
|
92
|
+
|
|
79
93
|
/** What a session's row says. */
|
|
80
|
-
const summary = (session: Capturing): string => {
|
|
81
|
-
const name = appName(session.appId);
|
|
94
|
+
const summary = (session: Capturing, name: string): string => {
|
|
82
95
|
switch (session.kind) {
|
|
83
96
|
case CapturingKind.RemoteDesktop:
|
|
84
97
|
return `Remote control: ${name} — ${[
|
|
@@ -93,15 +106,3 @@ const summary = (session: Capturing): string => {
|
|
|
93
106
|
return `${session.wireKind}: ${name}`;
|
|
94
107
|
}
|
|
95
108
|
};
|
|
96
|
-
|
|
97
|
-
/** How a row names a recorded source. */
|
|
98
|
-
const sourceName = (source: Captured): string => {
|
|
99
|
-
switch (source.kind) {
|
|
100
|
-
case CapturedKind.Window:
|
|
101
|
-
return source.title === "" ? "Untitled window" : source.title;
|
|
102
|
-
case CapturedKind.Monitor:
|
|
103
|
-
return `Screen ${source.name}`;
|
|
104
|
-
case CapturedKind.Region:
|
|
105
|
-
return "Region";
|
|
106
|
-
}
|
|
107
|
-
};
|
|
@@ -3,6 +3,7 @@ import { fireGlobalShortcuts } from "@domicile-desktop/sdk/global-shortcuts";
|
|
|
3
3
|
import type { Capturing, PortalRequest } from "@domicile-desktop/sdk/portal";
|
|
4
4
|
import {
|
|
5
5
|
answerPortalRequest,
|
|
6
|
+
CapturingKind,
|
|
6
7
|
PortalAnswer,
|
|
7
8
|
PortalKind,
|
|
8
9
|
stopCapturing,
|
|
@@ -13,10 +14,10 @@ import type { System, SystemHost } from "@domicile-desktop/sdk/system";
|
|
|
13
14
|
import { system } from "@domicile-desktop/sdk/system";
|
|
14
15
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
15
16
|
import { listDirectory } from "../FilePicker/list-directory";
|
|
17
|
+
import { useApps } from "../useApps/useApps";
|
|
16
18
|
import { AccessDialog } from "./AccessDialog";
|
|
17
19
|
import { AccountDialog } from "./AccountDialog";
|
|
18
20
|
import { AppChooserDialog } from "./AppChooserDialog";
|
|
19
|
-
import { appName } from "./app-name";
|
|
20
21
|
import { CapturingIndicator } from "./CapturingIndicator";
|
|
21
22
|
import { FileChooserDialog } from "./FileChooserDialog";
|
|
22
23
|
import { GlobalShortcutsDialog } from "./GlobalShortcutsDialog";
|
|
@@ -41,6 +42,11 @@ type Props = {
|
|
|
41
42
|
* `DisplayProvider`; without it, a dialog is centered on the whole page.
|
|
42
43
|
*/
|
|
43
44
|
screen?: string | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Leave screen casts out of the capturing indicator, for a shell that shows
|
|
47
|
+
* them itself.
|
|
48
|
+
*/
|
|
49
|
+
omitScreenCasts?: boolean | undefined;
|
|
44
50
|
/** How dialogs read the desktop's files, for tests. */
|
|
45
51
|
systemOf?: typeof system | undefined;
|
|
46
52
|
/**
|
|
@@ -62,6 +68,7 @@ type Props = {
|
|
|
62
68
|
*/
|
|
63
69
|
export const PortalDialogs = ({
|
|
64
70
|
host,
|
|
71
|
+
omitScreenCasts = false,
|
|
65
72
|
screen,
|
|
66
73
|
screenOf,
|
|
67
74
|
shellChords = NONE,
|
|
@@ -74,6 +81,10 @@ export const PortalDialogs = ({
|
|
|
74
81
|
[files],
|
|
75
82
|
);
|
|
76
83
|
const [sessions, setSessions] = useState<readonly Capturing[]>([]);
|
|
84
|
+
const apps = useApps(files, [
|
|
85
|
+
...requests.map((request) => request.appId),
|
|
86
|
+
...sessions.map((session) => session.appId),
|
|
87
|
+
]);
|
|
77
88
|
|
|
78
89
|
useEffect(() => watchPortalRequests(host, setRequests), [host]);
|
|
79
90
|
useEffect(() => watchCapturing(host, setSessions), [host]);
|
|
@@ -91,8 +102,15 @@ export const PortalDialogs = ({
|
|
|
91
102
|
return (
|
|
92
103
|
<>
|
|
93
104
|
<CapturingIndicator
|
|
105
|
+
apps={apps}
|
|
94
106
|
screen={screen}
|
|
95
|
-
sessions={
|
|
107
|
+
sessions={
|
|
108
|
+
omitScreenCasts
|
|
109
|
+
? sessions.filter(
|
|
110
|
+
(session) => session.kind !== CapturingKind.ScreenCast,
|
|
111
|
+
)
|
|
112
|
+
: sessions
|
|
113
|
+
}
|
|
96
114
|
stop={(id) => {
|
|
97
115
|
stopCapturing(host, id);
|
|
98
116
|
}}
|
|
@@ -102,6 +120,7 @@ export const PortalDialogs = ({
|
|
|
102
120
|
answer={(answer) => {
|
|
103
121
|
answerPortalRequest(host, shown.id, answer);
|
|
104
122
|
}}
|
|
123
|
+
asker={apps(shown.appId).name}
|
|
105
124
|
key={shown.id}
|
|
106
125
|
list={list}
|
|
107
126
|
request={shown}
|
|
@@ -117,6 +136,7 @@ export const PortalDialogs = ({
|
|
|
117
136
|
/** The dialog for `request`'s kind. */
|
|
118
137
|
const Dialog = ({
|
|
119
138
|
answer,
|
|
139
|
+
asker,
|
|
120
140
|
list,
|
|
121
141
|
request,
|
|
122
142
|
screen,
|
|
@@ -124,6 +144,7 @@ const Dialog = ({
|
|
|
124
144
|
system: files,
|
|
125
145
|
}: {
|
|
126
146
|
answer: (answer: PortalAnswer) => void;
|
|
147
|
+
asker: string;
|
|
127
148
|
list: (path: string) => Promise<readonly string[]>;
|
|
128
149
|
request: PortalRequest;
|
|
129
150
|
screen: string | undefined;
|
|
@@ -135,7 +156,7 @@ const Dialog = ({
|
|
|
135
156
|
return (
|
|
136
157
|
<AccessDialog
|
|
137
158
|
answer={answer}
|
|
138
|
-
asker={
|
|
159
|
+
asker={asker}
|
|
139
160
|
body={request.body}
|
|
140
161
|
screen={screen}
|
|
141
162
|
/>
|
|
@@ -144,7 +165,7 @@ const Dialog = ({
|
|
|
144
165
|
return (
|
|
145
166
|
<RemoteDesktopDialog
|
|
146
167
|
answer={answer}
|
|
147
|
-
asker={
|
|
168
|
+
asker={asker}
|
|
148
169
|
body={request.body}
|
|
149
170
|
screen={screen}
|
|
150
171
|
/>
|
|
@@ -153,7 +174,7 @@ const Dialog = ({
|
|
|
153
174
|
return (
|
|
154
175
|
<InputCaptureDialog
|
|
155
176
|
answer={answer}
|
|
156
|
-
asker={
|
|
177
|
+
asker={asker}
|
|
157
178
|
body={request.body}
|
|
158
179
|
screen={screen}
|
|
159
180
|
/>
|
|
@@ -162,7 +183,7 @@ const Dialog = ({
|
|
|
162
183
|
return (
|
|
163
184
|
<AppChooserDialog
|
|
164
185
|
answer={answer}
|
|
165
|
-
asker={
|
|
186
|
+
asker={asker}
|
|
166
187
|
body={request.body}
|
|
167
188
|
screen={screen}
|
|
168
189
|
system={files}
|
|
@@ -181,7 +202,7 @@ const Dialog = ({
|
|
|
181
202
|
return (
|
|
182
203
|
<AccountDialog
|
|
183
204
|
answer={answer}
|
|
184
|
-
asker={
|
|
205
|
+
asker={asker}
|
|
185
206
|
body={request.body}
|
|
186
207
|
screen={screen}
|
|
187
208
|
/>
|
|
@@ -190,7 +211,7 @@ const Dialog = ({
|
|
|
190
211
|
return (
|
|
191
212
|
<GlobalShortcutsDialog
|
|
192
213
|
answer={answer}
|
|
193
|
-
asker={
|
|
214
|
+
asker={asker}
|
|
194
215
|
body={request.body}
|
|
195
216
|
screen={screen}
|
|
196
217
|
shellChords={shellChords}
|
|
@@ -200,7 +221,7 @@ const Dialog = ({
|
|
|
200
221
|
return (
|
|
201
222
|
<WallpaperDialog
|
|
202
223
|
answer={answer}
|
|
203
|
-
asker={
|
|
224
|
+
asker={asker}
|
|
204
225
|
body={request.body}
|
|
205
226
|
files={files}
|
|
206
227
|
screen={screen}
|
|
@@ -210,7 +231,7 @@ const Dialog = ({
|
|
|
210
231
|
return (
|
|
211
232
|
<LauncherDialog
|
|
212
233
|
answer={answer}
|
|
213
|
-
asker={
|
|
234
|
+
asker={asker}
|
|
214
235
|
body={request.body}
|
|
215
236
|
screen={screen}
|
|
216
237
|
/>
|
|
@@ -219,7 +240,7 @@ const Dialog = ({
|
|
|
219
240
|
return (
|
|
220
241
|
<UsbDialog
|
|
221
242
|
answer={answer}
|
|
222
|
-
asker={
|
|
243
|
+
asker={asker}
|
|
223
244
|
body={request.body}
|
|
224
245
|
screen={screen}
|
|
225
246
|
/>
|
|
@@ -228,7 +249,7 @@ const Dialog = ({
|
|
|
228
249
|
return (
|
|
229
250
|
<ScreenCastDialog
|
|
230
251
|
answer={answer}
|
|
231
|
-
asker={
|
|
252
|
+
asker={asker}
|
|
232
253
|
body={request.body}
|
|
233
254
|
screen={screen}
|
|
234
255
|
/>
|
|
@@ -237,7 +258,7 @@ const Dialog = ({
|
|
|
237
258
|
return (
|
|
238
259
|
<PrintDialog
|
|
239
260
|
answer={answer}
|
|
240
|
-
asker={
|
|
261
|
+
asker={asker}
|
|
241
262
|
body={request.body}
|
|
242
263
|
screen={screen}
|
|
243
264
|
/>
|
|
@@ -246,7 +267,7 @@ const Dialog = ({
|
|
|
246
267
|
return (
|
|
247
268
|
<ScreenshotDialog
|
|
248
269
|
answer={answer}
|
|
249
|
-
asker={
|
|
270
|
+
asker={asker}
|
|
250
271
|
body={request.body}
|
|
251
272
|
screen={screen}
|
|
252
273
|
/>
|
|
@@ -255,7 +276,7 @@ const Dialog = ({
|
|
|
255
276
|
return (
|
|
256
277
|
<PickColorDialog
|
|
257
278
|
answer={answer}
|
|
258
|
-
asker={
|
|
279
|
+
asker={asker}
|
|
259
280
|
body={request.body}
|
|
260
281
|
screen={screen}
|
|
261
282
|
/>
|
|
@@ -1,29 +1,16 @@
|
|
|
1
1
|
// The applications an app chooser offers, described from their desktop
|
|
2
2
|
// entries, and which to pick first.
|
|
3
3
|
|
|
4
|
-
import type {
|
|
5
|
-
import {
|
|
4
|
+
import type { Result } from "@cprussin/option-result";
|
|
5
|
+
import { Ok } from "@cprussin/option-result";
|
|
6
6
|
import type { System, SystemError } from "@domicile-desktop/sdk/system";
|
|
7
|
-
import type {
|
|
8
|
-
import {
|
|
9
|
-
import { dataDirs } from "@domicile-desktop/system-apps/data-dirs";
|
|
10
|
-
import type { DesktopEntry } from "@domicile-desktop/system-apps/desktop-entry";
|
|
11
|
-
import { installedApps } from "@domicile-desktop/system-apps/installed";
|
|
7
|
+
import type { DescribedApp } from "@domicile-desktop/system-apps/describe-apps";
|
|
8
|
+
import { describeApps } from "@domicile-desktop/system-apps/describe-apps";
|
|
12
9
|
import { defaultApps } from "@domicile-desktop/system-apps/mime-apps";
|
|
13
10
|
|
|
14
|
-
/** One application offered. */
|
|
15
|
-
export type AppChoice = {
|
|
16
|
-
/** Its desktop file ID without `.desktop`, as the portal names it. */
|
|
17
|
-
id: string;
|
|
18
|
-
/** Its entry's `Name`, or `id` when it has no entry to read. */
|
|
19
|
-
name: string;
|
|
20
|
-
/** A `data:` URL, or `undefined` for none. */
|
|
21
|
-
icon: string | undefined;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
11
|
/** The offered applications, described, and the defaults for the type. */
|
|
25
12
|
export type DescribedChoices = {
|
|
26
|
-
apps:
|
|
13
|
+
apps: DescribedApp[];
|
|
27
14
|
/** `mimeapps.list`'s defaults, desktop file IDs without `.desktop`. */
|
|
28
15
|
defaults: string[];
|
|
29
16
|
};
|
|
@@ -36,32 +23,20 @@ export const describeChoices = async (
|
|
|
36
23
|
system: System,
|
|
37
24
|
choices: readonly string[],
|
|
38
25
|
contentType: string | undefined,
|
|
39
|
-
): Promise<Result<DescribedChoices, SystemError>> =>
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
);
|
|
54
|
-
const defaults =
|
|
55
|
-
contentType === undefined
|
|
56
|
-
? Ok<string[], SystemError>([])
|
|
57
|
-
: await defaultApps(system, contentType);
|
|
58
|
-
return (await apps).andThen((found) =>
|
|
59
|
-
defaults.map((ids) => ({
|
|
60
|
-
apps: found,
|
|
61
|
-
defaults: ids.map(withoutSuffix),
|
|
62
|
-
})),
|
|
63
|
-
);
|
|
64
|
-
});
|
|
26
|
+
): Promise<Result<DescribedChoices, SystemError>> => {
|
|
27
|
+
const [apps, defaults] = await Promise.all([
|
|
28
|
+
describeApps(system, choices),
|
|
29
|
+
contentType === undefined
|
|
30
|
+
? Ok<string[], SystemError>([])
|
|
31
|
+
: defaultApps(system, contentType),
|
|
32
|
+
]);
|
|
33
|
+
return apps.andThen((found) =>
|
|
34
|
+
defaults.map((ids) => ({
|
|
35
|
+
apps: found,
|
|
36
|
+
defaults: ids.map(withoutSuffix),
|
|
37
|
+
})),
|
|
38
|
+
);
|
|
39
|
+
};
|
|
65
40
|
|
|
66
41
|
/**
|
|
67
42
|
* The application to pick first: the last one chosen, else the first default
|
|
@@ -76,21 +51,4 @@ export const preselected = (
|
|
|
76
51
|
(id) => id !== undefined && choices.includes(id),
|
|
77
52
|
);
|
|
78
53
|
|
|
79
|
-
/** Choice `id`, named and drawn by its `entry` when it has one. */
|
|
80
|
-
const described = async (
|
|
81
|
-
icons: IconLookup,
|
|
82
|
-
id: string,
|
|
83
|
-
entry: DesktopEntry | undefined,
|
|
84
|
-
): Promise<Result<AppChoice, SystemError>> =>
|
|
85
|
-
entry === undefined
|
|
86
|
-
? Ok({ icon: undefined, id, name: id })
|
|
87
|
-
: (entry.icon === undefined
|
|
88
|
-
? Ok<Option<string>, SystemError>(None())
|
|
89
|
-
: await icons(entry.icon)
|
|
90
|
-
).map((icon) => ({
|
|
91
|
-
icon: icon.match({ None: () => undefined, Some: (url) => url }),
|
|
92
|
-
id,
|
|
93
|
-
name: entry.name,
|
|
94
|
-
}));
|
|
95
|
-
|
|
96
54
|
const withoutSuffix = (id: string): string => id.replace(/\.desktop$/, "");
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Captured } from "@domicile-desktop/sdk/portal";
|
|
2
|
+
import { CapturedKind } from "@domicile-desktop/sdk/portal";
|
|
3
|
+
|
|
4
|
+
/** How the shell names a source a screen cast records. */
|
|
5
|
+
export const sourceName = (source: Captured): string => {
|
|
6
|
+
switch (source.kind) {
|
|
7
|
+
case CapturedKind.Window:
|
|
8
|
+
return source.title === "" ? "Untitled window" : source.title;
|
|
9
|
+
case CapturedKind.Monitor:
|
|
10
|
+
return `Screen ${source.name}`;
|
|
11
|
+
case CapturedKind.Region:
|
|
12
|
+
return "Region";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { System } from "@domicile-desktop/sdk/system";
|
|
2
|
+
import type { DescribedApp } from "@domicile-desktop/system-apps/describe-apps";
|
|
3
|
+
import { describeApps } from "@domicile-desktop/system-apps/describe-apps";
|
|
4
|
+
import { useCallback, useEffect, useState } from "react";
|
|
5
|
+
|
|
6
|
+
/** An application as a dialog or indicator shows it. */
|
|
7
|
+
export type App = {
|
|
8
|
+
name: string;
|
|
9
|
+
/** A `data:` URL, or `undefined` for none. */
|
|
10
|
+
icon: string | undefined;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Names each application by its desktop entry, with its icon, reading the
|
|
15
|
+
* entries for `ids` again when they change. An id with no entry, or not read
|
|
16
|
+
* yet, is named by itself; the empty id is "An application".
|
|
17
|
+
*/
|
|
18
|
+
export const useApps = (
|
|
19
|
+
system: System,
|
|
20
|
+
ids: readonly string[],
|
|
21
|
+
): ((appId: string) => App) => {
|
|
22
|
+
const [described, setDescribed] = useState<ReadonlyMap<string, DescribedApp>>(
|
|
23
|
+
new Map(),
|
|
24
|
+
);
|
|
25
|
+
// The array is new on every push; its contents are what change.
|
|
26
|
+
const key = [...new Set(ids.filter((id) => id !== ""))].toSorted().join("\n");
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (key === "") {
|
|
30
|
+
return undefined;
|
|
31
|
+
} else {
|
|
32
|
+
let current = true;
|
|
33
|
+
describeApps(system, key.split("\n"))
|
|
34
|
+
.then((result) => {
|
|
35
|
+
result.match({
|
|
36
|
+
Err: (error) => {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`could not read the applications: ${error.message}`,
|
|
39
|
+
);
|
|
40
|
+
},
|
|
41
|
+
Ok: (found) => {
|
|
42
|
+
if (current) {
|
|
43
|
+
setDescribed(new Map(found.map((app) => [app.id, app])));
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
})
|
|
48
|
+
.catch((error: unknown) => {
|
|
49
|
+
// biome-ignore lint/suspicious/noConsole: applications stay named by their ids; say why
|
|
50
|
+
console.error("Could not name the applications", error);
|
|
51
|
+
});
|
|
52
|
+
return () => {
|
|
53
|
+
current = false;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}, [system, key]);
|
|
57
|
+
|
|
58
|
+
return useCallback((appId) => app(described, appId), [described]);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** `appId` as `described` names it. */
|
|
62
|
+
const app = (
|
|
63
|
+
described: ReadonlyMap<string, DescribedApp>,
|
|
64
|
+
appId: string,
|
|
65
|
+
): App => {
|
|
66
|
+
const entry = described.get(appId);
|
|
67
|
+
if (entry === undefined) {
|
|
68
|
+
return { icon: undefined, name: appId === "" ? "An application" : appId };
|
|
69
|
+
} else {
|
|
70
|
+
return { icon: entry.icon, name: entry.name };
|
|
71
|
+
}
|
|
72
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PortalHost, PortalWallpaper } from "@domicile-desktop/sdk/portal";
|
|
2
|
+
import { watchPortalWallpaper } from "@domicile-desktop/sdk/portal";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
const NONE_SET: PortalWallpaper = {
|
|
6
|
+
background: undefined,
|
|
7
|
+
lockscreen: undefined,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The pictures applications set through the Wallpaper portal, as paths. See
|
|
12
|
+
* `docs/architecture/PORTALS.md`.
|
|
13
|
+
*/
|
|
14
|
+
export const usePortalWallpaper = (host: PortalHost): PortalWallpaper => {
|
|
15
|
+
const [wallpaper, setWallpaper] = useState(NONE_SET);
|
|
16
|
+
|
|
17
|
+
useEffect(() => watchPortalWallpaper(host, setWallpaper), [host]);
|
|
18
|
+
|
|
19
|
+
return wallpaper;
|
|
20
|
+
};
|