@stevecastaneda/paseo-progress 1.14.2 → 1.14.4
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 +28 -17
- package/client/attachments.tsx +122 -28
- package/client/blocks.tsx +33 -8
- package/client/chat-row-view.tsx +19 -6
- package/client/chat-row.ts +13 -2
- package/client/dashboard-query.ts +9 -4
- package/client/deliverables.tsx +82 -17
- package/client/dialog-stack.tsx +104 -42
- package/client/directory.ts +1 -3
- package/client/errors.ts +6 -1
- package/client/history.tsx +233 -79
- package/client/i18n.ts +10 -5
- package/client/launcher.tsx +57 -15
- package/client/locales/en.ts +28 -22
- package/client/locales/zh-CN.ts +18 -17
- package/client/messages.ts +1 -0
- package/client/motion.tsx +38 -14
- package/client/panel-opened.ts +3 -1
- package/client/panel.tsx +442 -160
- package/client/pdf.tsx +18 -4
- package/client/pieces.ts +6 -1
- package/client/pill-popover.tsx +108 -23
- package/client/pills.tsx +35 -12
- package/client/preview.tsx +265 -79
- package/client/questions.tsx +173 -52
- package/client/row.tsx +14 -3
- package/client/section-title.tsx +11 -2
- package/client/settings.tsx +76 -24
- package/client/show-more.tsx +23 -6
- package/client/spinner.tsx +11 -7
- package/client/status-badge.tsx +9 -7
- package/client/tabs.tsx +98 -20
- package/client/ticket-dialog.tsx +241 -73
- package/client/time-bar.tsx +5 -2
- package/client/video.tsx +15 -6
- package/client/whats-new.tsx +94 -36
- package/client/when.tsx +2 -1
- package/client/write-up.tsx +79 -12
- package/dist/server/cli.js +199 -101
- package/index.client.tsx +28 -13
- package/index.server.ts +19 -1
- package/package.json +15 -14
- package/server/cli.ts +72 -32
- package/server/dashboard.ts +9 -13
- package/server/launcher.ts +33 -17
- package/server/lock.ts +14 -7
- package/server/open.ts +32 -98
- package/server/progress-file.ts +6 -6
- package/server/question-mode.ts +12 -6
- package/server/skill.ts +22 -16
- package/server/store.ts +26 -41
- package/server/system.ts +113 -0
- package/server/tool.ts +89 -44
- package/server/whats-new.ts +25 -13
- package/shared/attachments.ts +40 -8
- package/shared/blocks.ts +9 -6
- package/shared/dashboard.ts +46 -29
- package/shared/events.ts +12 -10
- package/shared/markdown.ts +16 -8
- package/shared/question-modes.ts +8 -5
- package/shared/rpc.ts +9 -3
- package/shared/show.ts +20 -8
- package/shared/ticket-story.ts +1 -3
- package/skills/paseo-progress/SKILL.md +7 -0
- package/tsconfig.json +3 -10
- package/client/page-opener.ts +0 -9
- package/client/page-screen.tsx +0 -97
package/client/preview.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { Icon, Markdown, ScrollView } from "@getpaseo/plugin/client/ui";
|
|
|
3
3
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
4
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
5
5
|
import { ActivityIndicator, Image, type LayoutChangeEvent, Platform, Pressable, Text, useWindowDimensions, View, type ViewStyle } from "react-native";
|
|
6
|
-
import { ATTACHMENT_ROW, type Attachment, AttachmentList, isPreviewable } from "./attachments";
|
|
6
|
+
import { ATTACHMENT_ROW, type Attachment, AttachmentList, isHostOpenable, isPreviewable } from "./attachments";
|
|
7
7
|
import { errorText } from "./errors";
|
|
8
8
|
import { folderAttachments, isFolder, isPieced, PDF_MIME_TYPE, previewKind } from "../shared/attachments";
|
|
9
9
|
import { canOpenOnHost, previewDeliverable, readDeliverablePiece } from "../shared/rpc";
|
|
@@ -41,7 +41,22 @@ type PreviewProps = {
|
|
|
41
41
|
|
|
42
42
|
// `onLong` says when the file is too tall for the dialog, so the host turns off
|
|
43
43
|
// its own scrolling and the file scrolls with the actions pinned below.
|
|
44
|
-
export function PreviewContent({
|
|
44
|
+
export function PreviewContent({
|
|
45
|
+
colors,
|
|
46
|
+
attachment,
|
|
47
|
+
workspaceId,
|
|
48
|
+
workspaceDirectory,
|
|
49
|
+
onOpenOnHost,
|
|
50
|
+
onCopyPath,
|
|
51
|
+
onBack,
|
|
52
|
+
gallery,
|
|
53
|
+
onOpenEntry,
|
|
54
|
+
onLong,
|
|
55
|
+
ownScroll,
|
|
56
|
+
fullScreen,
|
|
57
|
+
leftFullScreen,
|
|
58
|
+
onFullScreen,
|
|
59
|
+
}: PreviewProps & {
|
|
45
60
|
onLong(long: boolean): void;
|
|
46
61
|
ownScroll: boolean;
|
|
47
62
|
fullScreen: boolean;
|
|
@@ -53,11 +68,12 @@ export function PreviewContent({ colors, attachment, workspaceId, workspaceDirec
|
|
|
53
68
|
const readPiece = useRpc(readDeliverablePiece);
|
|
54
69
|
const checkHost = useRpc(canOpenOnHost);
|
|
55
70
|
const queryClient = useQueryClient();
|
|
56
|
-
const canOpen =
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
71
|
+
const canOpen =
|
|
72
|
+
useQuery({
|
|
73
|
+
queryKey: ["progress", "can-open", workspaceId],
|
|
74
|
+
queryFn: () => checkHost({ workspaceId, workspaceDirectory }),
|
|
75
|
+
staleTime: Infinity,
|
|
76
|
+
}).data?.canOpen === true && isHostOpenable(attachment);
|
|
61
77
|
const loadingKey = (target: Attachment) => ["progress", "preview-loading", workspaceId, target.ref, target.path];
|
|
62
78
|
const previewQuery = (target: Attachment) => ({
|
|
63
79
|
queryKey: ["progress", "preview", workspaceId, target.ref, target.path],
|
|
@@ -67,7 +83,7 @@ export function PreviewContent({ colors, attachment, workspaceId, workspaceDirec
|
|
|
67
83
|
// and so on, so a tall image made the dialog jump between two sizes until closed.
|
|
68
84
|
queryFn: async () => {
|
|
69
85
|
const data = await fetchPreview({ workspaceId, workspaceDirectory, ref: target.ref });
|
|
70
|
-
if (data.kind === "image") return { ...data, ...await imageSize(data.dataUri) };
|
|
86
|
+
if (data.kind === "image") return { ...data, ...(await imageSize(data.dataUri)) };
|
|
71
87
|
if (data.kind !== "video" && data.kind !== "pdf") return data;
|
|
72
88
|
queryClient.setQueryData(loadingKey(target), 0);
|
|
73
89
|
const blob = await loadPieces({
|
|
@@ -162,11 +178,15 @@ export function PreviewContent({ colors, attachment, workspaceId, workspaceDirec
|
|
|
162
178
|
const long = data?.kind === "markdown" ? (stepping ? lastLong.current : certainlyLong || latched) : estimatedHeight(data, aspectRatio, maxMediaHeight) > room;
|
|
163
179
|
lastLong.current = long;
|
|
164
180
|
useEffect(() => onLong(long), [long]);
|
|
165
|
-
const measure =
|
|
166
|
-
|
|
167
|
-
|
|
181
|
+
const measure =
|
|
182
|
+
!long && !stepping
|
|
183
|
+
? (event: LayoutChangeEvent) => {
|
|
184
|
+
if (event.nativeEvent.layout.height > room) queryClient.setQueryData(longKey, true);
|
|
185
|
+
}
|
|
186
|
+
: undefined;
|
|
168
187
|
const fileKind = previewKind(attachment.path ?? "");
|
|
169
|
-
const piecesLoading =
|
|
188
|
+
const piecesLoading =
|
|
189
|
+
isPieced(fileKind) && preview.isFetching && loading !== undefined && loading < 1 ? m.loadingPieces(fileKind, Math.floor(loading * 100)) : null;
|
|
170
190
|
const caption = { color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 } as const;
|
|
171
191
|
const loadingState = (
|
|
172
192
|
<View style={{ alignItems: "center", gap: 8, padding: 24 }}>
|
|
@@ -174,7 +194,12 @@ export function PreviewContent({ colors, attachment, workspaceId, workspaceDirec
|
|
|
174
194
|
{piecesLoading ? <Text style={[caption, { fontVariant: ["tabular-nums"] }]}>{piecesLoading}</Text> : null}
|
|
175
195
|
</View>
|
|
176
196
|
);
|
|
177
|
-
const truncatedNote = (paddingTop: number) =>
|
|
197
|
+
const truncatedNote = (paddingTop: number) => (
|
|
198
|
+
<Text style={{ color: colors.foregroundMuted, fontSize: 11, lineHeight: 16, paddingTop }}>
|
|
199
|
+
{m.truncatedText}
|
|
200
|
+
{canOpen ? m.openRest : ""}
|
|
201
|
+
</Text>
|
|
202
|
+
);
|
|
178
203
|
const content = (
|
|
179
204
|
<>
|
|
180
205
|
{preview.isPending ? loadingState : null}
|
|
@@ -192,20 +217,35 @@ export function PreviewContent({ colors, attachment, workspaceId, workspaceDirec
|
|
|
192
217
|
</View>
|
|
193
218
|
) : null}
|
|
194
219
|
{/* Straight on the dialog: Paseo's code blocks and tables bring their own fills. */}
|
|
195
|
-
{preview.data?.kind === "markdown" ? (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
220
|
+
{preview.data?.kind === "markdown" ? (
|
|
221
|
+
certainlyLong && !ownScroll ? (
|
|
222
|
+
loadingState
|
|
223
|
+
) : (
|
|
224
|
+
<View onLayout={measure} style={{ gap: 12, opacity: stepping ? 0.6 : 1 }}>
|
|
225
|
+
<Markdown>{preview.data.markdown}</Markdown>
|
|
226
|
+
{preview.data.truncated ? truncatedNote(0) : null}
|
|
227
|
+
</View>
|
|
228
|
+
)
|
|
229
|
+
) : null}
|
|
201
230
|
{preview.data?.kind === "folder" ? (
|
|
202
|
-
<FolderList
|
|
203
|
-
|
|
231
|
+
<FolderList
|
|
232
|
+
colors={colors}
|
|
233
|
+
folder={attachment}
|
|
234
|
+
entries={folderAttachments(attachment, preview.data.entries)}
|
|
235
|
+
truncated={preview.data.truncated}
|
|
236
|
+
dimmed={stepping}
|
|
237
|
+
root={workspaceDirectory}
|
|
238
|
+
onOpen={onOpenEntry}
|
|
239
|
+
/>
|
|
204
240
|
) : null}
|
|
205
241
|
{preview.data?.kind === "image" ? (
|
|
206
242
|
// The Full screen button is the keyboard's way in, so the image isn't a second tab stop.
|
|
207
|
-
<Pressable
|
|
208
|
-
|
|
243
|
+
<Pressable
|
|
244
|
+
disabled={!canFullScreen}
|
|
245
|
+
focusable={false}
|
|
246
|
+
onPress={() => onFullScreen(true)}
|
|
247
|
+
style={{ width: "100%", cursor: canFullScreen ? ("zoom-in" as unknown as ViewStyle["cursor"]) : undefined }}
|
|
248
|
+
>
|
|
209
249
|
<Image accessibilityLabel={attachment.title} source={{ uri: preview.data.dataUri }} resizeMode="contain" style={mediaStyle} />
|
|
210
250
|
</Pressable>
|
|
211
251
|
) : null}
|
|
@@ -216,33 +256,48 @@ export function PreviewContent({ colors, attachment, workspaceId, workspaceDirec
|
|
|
216
256
|
// The path as a caption under the file, where it has the full width, with Copy
|
|
217
257
|
// beside it so it can go into a message without leaving the dialog. A long
|
|
218
258
|
// file scrolls above it, so it stays in view with the actions.
|
|
219
|
-
const path =
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
<
|
|
224
|
-
{
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
259
|
+
const path =
|
|
260
|
+
!preview.isPending && piecesLoading ? (
|
|
261
|
+
<Text style={[caption, { fontVariant: ["tabular-nums"] }]}>{piecesLoading}</Text>
|
|
262
|
+
) : (
|
|
263
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
|
264
|
+
<Text selectable numberOfLines={1} ellipsizeMode="middle" style={[caption, { flex: 1, minWidth: 0 }]}>
|
|
265
|
+
{attachment.path}
|
|
266
|
+
</Text>
|
|
267
|
+
{/* Optical: the 22px button centers on the 17px caption line without growing it. */}
|
|
268
|
+
<PressScale
|
|
269
|
+
accessibilityRole="button"
|
|
270
|
+
accessibilityLabel={m.copyItemPath(attachment.title)}
|
|
271
|
+
hitSlop={6}
|
|
272
|
+
outerStyle={{ marginVertical: -3 }}
|
|
273
|
+
onPress={onCopyPath}
|
|
274
|
+
style={({ pressed, hovered }) => ({
|
|
275
|
+
padding: 4,
|
|
276
|
+
borderRadius: 4,
|
|
277
|
+
backgroundColor: pressed ? colors.border : hovered ? colors.surface2 : "transparent",
|
|
278
|
+
})}
|
|
279
|
+
>
|
|
280
|
+
<Icon name="Copy" size={14} color={colors.foregroundMuted} />
|
|
281
|
+
</PressScale>
|
|
282
|
+
</View>
|
|
283
|
+
);
|
|
234
284
|
// Back on the left, Previous and Next in the middle, Open on the right.
|
|
235
285
|
const actions = (
|
|
236
286
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
|
|
237
|
-
<View style={{ flex: 1, alignItems: "flex-start" }}>
|
|
238
|
-
{onBack ? <BackButton colors={colors} onPress={onBack} /> : null}
|
|
239
|
-
</View>
|
|
287
|
+
<View style={{ flex: 1, alignItems: "flex-start" }}>{onBack ? <BackButton colors={colors} onPress={onBack} /> : null}</View>
|
|
240
288
|
{canStep ? <Stepper colors={colors} index={index} count={items.length} onStep={step} nextRef={nextButton} /> : null}
|
|
241
289
|
<View style={{ flex: 1, flexDirection: "row", alignItems: "center", justifyContent: "flex-end", gap: 8 }}>
|
|
242
290
|
{canFullScreen ? (
|
|
243
291
|
// An inline outline beats Paseo's :focus-visible ring.
|
|
244
|
-
<DialogButton
|
|
245
|
-
|
|
292
|
+
<DialogButton
|
|
293
|
+
ref={fullScreenButton}
|
|
294
|
+
colors={colors}
|
|
295
|
+
icon="Maximize2"
|
|
296
|
+
accessibilityLabel={m.fullScreen}
|
|
297
|
+
onPress={() => onFullScreen(true)}
|
|
298
|
+
outerStyle={quiet ? { outlineWidth: 0 } : undefined}
|
|
299
|
+
onBlur={() => setQuiet(false)}
|
|
300
|
+
/>
|
|
246
301
|
) : null}
|
|
247
302
|
{canOpen ? <DialogButton colors={colors} icon="ExternalLink" label={m.open} accessibilityLabel={m.openDefault} onPress={onOpenOnHost} /> : null}
|
|
248
303
|
</View>
|
|
@@ -251,10 +306,17 @@ export function PreviewContent({ colors, attachment, workspaceId, workspaceDirec
|
|
|
251
306
|
const layerShown = fullScreen && showsLayer;
|
|
252
307
|
// An SVG stays sharp at any size, so only a bitmap stops at its own pixels. An
|
|
253
308
|
// icon's SVG can say it's 24px wide.
|
|
254
|
-
const fullScreenLayer =
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
309
|
+
const fullScreenLayer =
|
|
310
|
+
layerShown && preview.data?.kind === "image" ? (
|
|
311
|
+
<FullScreenImage
|
|
312
|
+
title={attachment.title}
|
|
313
|
+
uri={preview.data.dataUri}
|
|
314
|
+
ratio={aspectRatio}
|
|
315
|
+
dimmed={stepping}
|
|
316
|
+
onExit={() => onFullScreen(false)}
|
|
317
|
+
naturalWidth={preview.data.dataUri.startsWith("data:image/svg+xml") ? null : preview.data.width}
|
|
318
|
+
/>
|
|
319
|
+
) : null;
|
|
258
320
|
// Paseo's focus trap counts the controls under the layer, so Shift+Tab would
|
|
259
321
|
// reach a hidden Open. Inert takes them out until full screen ends.
|
|
260
322
|
const body = useRef<View>(null);
|
|
@@ -268,7 +330,9 @@ export function PreviewContent({ colors, attachment, workspaceId, workspaceDirec
|
|
|
268
330
|
<>
|
|
269
331
|
{long ? (
|
|
270
332
|
<View ref={body} style={{ flex: 1 }}>
|
|
271
|
-
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ gap: 12 }}>
|
|
333
|
+
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ gap: 12 }}>
|
|
334
|
+
{content}
|
|
335
|
+
</ScrollView>
|
|
272
336
|
<View style={{ marginTop: 8 }}>{path}</View>
|
|
273
337
|
{actions}
|
|
274
338
|
</View>
|
|
@@ -289,9 +353,33 @@ export function PreviewContent({ colors, attachment, workspaceId, workspaceDirec
|
|
|
289
353
|
const FULL_SCREEN_MARGIN = 24;
|
|
290
354
|
const FULL_SCREEN_BAR = 52;
|
|
291
355
|
|
|
356
|
+
function barButton({ pressed, hovered }: { pressed: boolean; hovered: boolean }) {
|
|
357
|
+
return {
|
|
358
|
+
height: 36,
|
|
359
|
+
alignItems: "center",
|
|
360
|
+
justifyContent: "center",
|
|
361
|
+
borderRadius: 6,
|
|
362
|
+
backgroundColor: pressed ? "rgba(255, 255, 255, 0.16)" : hovered ? "rgba(255, 255, 255, 0.1)" : "transparent",
|
|
363
|
+
} as const;
|
|
364
|
+
}
|
|
365
|
+
|
|
292
366
|
// The image over the whole window, never past its own pixels. It fits whole, or a
|
|
293
367
|
// long one can fill the width and scroll.
|
|
294
|
-
function FullScreenImage({
|
|
368
|
+
function FullScreenImage({
|
|
369
|
+
title,
|
|
370
|
+
uri,
|
|
371
|
+
naturalWidth,
|
|
372
|
+
ratio,
|
|
373
|
+
dimmed,
|
|
374
|
+
onExit,
|
|
375
|
+
}: {
|
|
376
|
+
title: string;
|
|
377
|
+
uri: string;
|
|
378
|
+
naturalWidth: number | null;
|
|
379
|
+
ratio: number;
|
|
380
|
+
dimmed: boolean;
|
|
381
|
+
onExit(): void;
|
|
382
|
+
}) {
|
|
295
383
|
const { messages: m } = useProgressLanguage();
|
|
296
384
|
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
|
|
297
385
|
const closeButton = useRef<View>(null);
|
|
@@ -317,36 +405,62 @@ function FullScreenImage({ title, uri, naturalWidth, ratio, dimmed, onExit }: {
|
|
|
317
405
|
scrolled.current = scrolls;
|
|
318
406
|
}, [scrolls, uri]);
|
|
319
407
|
const light = "rgba(255, 255, 255, 0.85)";
|
|
320
|
-
const barButton = ({ pressed, hovered }: { pressed: boolean; hovered: boolean }) => ({
|
|
321
|
-
height: 36, alignItems: "center", justifyContent: "center", borderRadius: 6,
|
|
322
|
-
backgroundColor: pressed ? "rgba(255, 255, 255, 0.16)" : hovered ? "rgba(255, 255, 255, 0.1)" : "transparent",
|
|
323
|
-
}) as const;
|
|
324
408
|
// A press on the image stops there, so only the black around it exits.
|
|
325
409
|
// The fit button is the keyboard's way to the toggle, so the image isn't a tab stop.
|
|
326
410
|
const area = (
|
|
327
|
-
<Pressable
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
411
|
+
<Pressable
|
|
412
|
+
accessible={false}
|
|
413
|
+
focusable={false}
|
|
414
|
+
onPress={onExit}
|
|
415
|
+
style={{ flexGrow: 1, alignItems: "center", justifyContent: "center", padding: FULL_SCREEN_MARGIN }}
|
|
416
|
+
>
|
|
417
|
+
<Pressable
|
|
418
|
+
accessible={false}
|
|
419
|
+
focusable={false}
|
|
420
|
+
onPress={long ? toggle : () => {}}
|
|
421
|
+
style={{ cursor: (long ? (scrolls ? "zoom-out" : "zoom-in") : "default") as unknown as ViewStyle["cursor"] }}
|
|
422
|
+
>
|
|
423
|
+
<Image
|
|
424
|
+
accessibilityLabel={title}
|
|
425
|
+
source={{ uri }}
|
|
426
|
+
resizeMode="contain"
|
|
427
|
+
style={{
|
|
428
|
+
width: scrolls ? widthFit : screenFit,
|
|
429
|
+
aspectRatio: ratio,
|
|
430
|
+
borderRadius: 6,
|
|
431
|
+
borderWidth: 1,
|
|
432
|
+
borderColor: "rgba(255, 255, 255, 0.1)",
|
|
433
|
+
opacity: dimmed ? 0.6 : 1,
|
|
434
|
+
}}
|
|
435
|
+
/>
|
|
333
436
|
</Pressable>
|
|
334
437
|
</Pressable>
|
|
335
438
|
);
|
|
336
439
|
return (
|
|
337
440
|
<View style={{ position: "fixed" as unknown as ViewStyle["position"], top: 0, right: 0, bottom: 0, left: 0, zIndex: 1000, backgroundColor: "black" }}>
|
|
338
441
|
<View style={{ height: FULL_SCREEN_BAR, flexDirection: "row", alignItems: "center", gap: 12, paddingStart: FULL_SCREEN_MARGIN, paddingEnd: 12 }}>
|
|
339
|
-
<Text numberOfLines={1} style={{ flex: 1, minWidth: 0, color: light, fontSize: 13, lineHeight: 18 }}>
|
|
442
|
+
<Text numberOfLines={1} style={{ flex: 1, minWidth: 0, color: light, fontSize: 13, lineHeight: 18 }}>
|
|
443
|
+
{title}
|
|
444
|
+
</Text>
|
|
340
445
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
341
446
|
{long ? (
|
|
342
|
-
<PressScale
|
|
343
|
-
|
|
447
|
+
<PressScale
|
|
448
|
+
ref={fitButton}
|
|
449
|
+
accessibilityRole="button"
|
|
450
|
+
onPress={toggle}
|
|
451
|
+
style={(state) => ({ ...barButton(state), flexDirection: "row", gap: 6, paddingHorizontal: 10 })}
|
|
452
|
+
>
|
|
344
453
|
<Icon name={scrolls ? "Shrink" : "MoveHorizontal"} size={16} color={light} />
|
|
345
454
|
<Text style={{ color: light, fontSize: 13, lineHeight: 18 }}>{scrolls ? m.fitToScreen : m.fitToWidth}</Text>
|
|
346
455
|
</PressScale>
|
|
347
456
|
) : null}
|
|
348
|
-
<PressScale
|
|
349
|
-
|
|
457
|
+
<PressScale
|
|
458
|
+
ref={closeButton}
|
|
459
|
+
accessibilityRole="button"
|
|
460
|
+
accessibilityLabel={m.exitFullScreen}
|
|
461
|
+
onPress={onExit}
|
|
462
|
+
style={(state) => ({ ...barButton(state), width: 36 })}
|
|
463
|
+
>
|
|
350
464
|
<Icon name="X" size={18} color={light} />
|
|
351
465
|
</PressScale>
|
|
352
466
|
</View>
|
|
@@ -354,8 +468,12 @@ function FullScreenImage({ title, uri, naturalWidth, ratio, dimmed, onExit }: {
|
|
|
354
468
|
{/* Keyed so the next image starts at its top. The fit button stays the visible
|
|
355
469
|
keyboard control, so the scroll area draws no focus ring. */}
|
|
356
470
|
{scrolls ? (
|
|
357
|
-
<ScrollView key={uri} ref={scrollArea} focusable style={{ flex: 1, outlineWidth: 0 }} contentContainerStyle={{ flexGrow: 1 }}>
|
|
358
|
-
|
|
471
|
+
<ScrollView key={uri} ref={scrollArea} focusable style={{ flex: 1, outlineWidth: 0 }} contentContainerStyle={{ flexGrow: 1 }}>
|
|
472
|
+
{area}
|
|
473
|
+
</ScrollView>
|
|
474
|
+
) : (
|
|
475
|
+
area
|
|
476
|
+
)}
|
|
359
477
|
</View>
|
|
360
478
|
);
|
|
361
479
|
}
|
|
@@ -378,7 +496,11 @@ const CHARS_PER_LINE = Math.floor((CONTENT_WIDTH - 20) / 7.2);
|
|
|
378
496
|
const FOLDER_ROW_HEIGHT = ATTACHMENT_ROW.lineHeight + ATTACHMENT_ROW.paddingVertical * 2 + ATTACHMENT_ROW.gap;
|
|
379
497
|
|
|
380
498
|
// Roughly how tall the preview draws, including wrapped lines.
|
|
381
|
-
function estimatedHeight(
|
|
499
|
+
function estimatedHeight(
|
|
500
|
+
data: { kind: "image" | "video" | "pdf" } | { kind: "text"; text: string } | { kind: "folder"; entries: unknown[]; truncated: boolean } | undefined,
|
|
501
|
+
aspectRatio: number,
|
|
502
|
+
maxMediaHeight: number,
|
|
503
|
+
): number {
|
|
382
504
|
if (!data) return 0;
|
|
383
505
|
if (data.kind === "folder") return Math.max(1, data.entries.length + (data.truncated ? 1 : 0)) * FOLDER_ROW_HEIGHT;
|
|
384
506
|
if (data.kind === "pdf") return maxMediaHeight;
|
|
@@ -398,13 +520,29 @@ export function markdownFloor(markdown: string): number {
|
|
|
398
520
|
return lines * MIN_MARKDOWN_LINE;
|
|
399
521
|
}
|
|
400
522
|
|
|
401
|
-
function FolderList({
|
|
523
|
+
function FolderList({
|
|
524
|
+
colors,
|
|
525
|
+
folder,
|
|
526
|
+
entries,
|
|
527
|
+
truncated,
|
|
528
|
+
dimmed,
|
|
529
|
+
root,
|
|
530
|
+
onOpen,
|
|
531
|
+
}: {
|
|
532
|
+
colors: Colors;
|
|
533
|
+
folder: Attachment;
|
|
534
|
+
entries: Attachment[];
|
|
535
|
+
truncated: boolean;
|
|
536
|
+
dimmed: boolean;
|
|
537
|
+
root: string;
|
|
538
|
+
onOpen(attachment: Attachment, group: Attachment[]): void;
|
|
539
|
+
}) {
|
|
402
540
|
const { messages: m } = useProgressLanguage();
|
|
403
541
|
const caption = { color: colors.foregroundMuted, fontSize: 12, lineHeight: 17 } as const;
|
|
404
542
|
if (!entries.length) return <Text style={caption}>{m.emptyFolder(folder.title)}</Text>;
|
|
405
543
|
return (
|
|
406
544
|
<View style={{ gap: 8, opacity: dimmed ? 0.6 : 1 }}>
|
|
407
|
-
<AttachmentList colors={colors} attachments={entries} onOpen={onOpen} />
|
|
545
|
+
<AttachmentList colors={colors} attachments={entries} root={root} onOpen={onOpen} />
|
|
408
546
|
{truncated ? <Text style={caption}>{m.firstEntries(entries.length)}</Text> : null}
|
|
409
547
|
</View>
|
|
410
548
|
);
|
|
@@ -414,8 +552,11 @@ function FolderList({ colors, folder, entries, truncated, dimmed, onOpen }: { co
|
|
|
414
552
|
// Read from the data because onLoad doesn't report a size for data URIs on every platform.
|
|
415
553
|
function imageSize(uri: string): Promise<{ width: number | null; ratio: number | null }> {
|
|
416
554
|
return new Promise((resolve) => {
|
|
417
|
-
Image.getSize(
|
|
418
|
-
|
|
555
|
+
Image.getSize(
|
|
556
|
+
uri,
|
|
557
|
+
(width, height) => resolve(width && height ? { width, ratio: width / height } : { width: null, ratio: null }),
|
|
558
|
+
() => resolve({ width: null, ratio: null }),
|
|
559
|
+
);
|
|
419
560
|
});
|
|
420
561
|
}
|
|
421
562
|
|
|
@@ -427,14 +568,45 @@ export function roomForContent(windowHeight: number): number {
|
|
|
427
568
|
}
|
|
428
569
|
|
|
429
570
|
// A dialog footer button: 13px label and 14px icon, 30px tall. Icon-only is a 30px square.
|
|
430
|
-
export function DialogButton({
|
|
571
|
+
export function DialogButton({
|
|
572
|
+
colors,
|
|
573
|
+
icon,
|
|
574
|
+
label,
|
|
575
|
+
accessibilityLabel,
|
|
576
|
+
onPress,
|
|
577
|
+
onBlur,
|
|
578
|
+
outerStyle,
|
|
579
|
+
ref,
|
|
580
|
+
}: {
|
|
581
|
+
colors: Colors;
|
|
582
|
+
icon: string;
|
|
583
|
+
label?: string;
|
|
584
|
+
accessibilityLabel?: string;
|
|
585
|
+
onPress(): void;
|
|
586
|
+
onBlur?(): void;
|
|
587
|
+
outerStyle?: ViewStyle;
|
|
588
|
+
ref?: React.Ref<View>;
|
|
589
|
+
}) {
|
|
431
590
|
return (
|
|
432
|
-
<PressScale
|
|
591
|
+
<PressScale
|
|
592
|
+
ref={ref}
|
|
593
|
+
accessibilityRole="button"
|
|
594
|
+
accessibilityLabel={accessibilityLabel ?? label}
|
|
595
|
+
onPress={onPress}
|
|
596
|
+
onBlur={onBlur}
|
|
597
|
+
outerStyle={outerStyle}
|
|
433
598
|
style={({ pressed }) => ({
|
|
434
|
-
flexDirection: "row",
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
599
|
+
flexDirection: "row",
|
|
600
|
+
alignItems: "center",
|
|
601
|
+
gap: 6,
|
|
602
|
+
paddingVertical: label ? 6 : 8,
|
|
603
|
+
paddingLeft: label ? 8 : 8,
|
|
604
|
+
paddingRight: label ? 10 : 8,
|
|
605
|
+
borderRadius: 6,
|
|
606
|
+
...raised(colors),
|
|
607
|
+
backgroundColor: pressed ? colors.surface1 : colors.surface2,
|
|
608
|
+
})}
|
|
609
|
+
>
|
|
438
610
|
<Icon name={icon} size={14} color={colors.foreground} />
|
|
439
611
|
{label ? <Text style={{ color: colors.foreground, fontSize: 13, lineHeight: 18 }}>{label}</Text> : null}
|
|
440
612
|
</PressScale>
|
|
@@ -448,12 +620,26 @@ export function BackButton({ colors, onPress }: { colors: Colors; onPress(): voi
|
|
|
448
620
|
}
|
|
449
621
|
|
|
450
622
|
// Previous and next through a gallery, with "2 of 3" between.
|
|
451
|
-
function Stepper({
|
|
623
|
+
function Stepper({
|
|
624
|
+
colors,
|
|
625
|
+
index,
|
|
626
|
+
count,
|
|
627
|
+
onStep,
|
|
628
|
+
nextRef,
|
|
629
|
+
}: {
|
|
630
|
+
colors: Colors;
|
|
631
|
+
index: number;
|
|
632
|
+
count: number;
|
|
633
|
+
onStep(offset: number): void;
|
|
634
|
+
nextRef: React.Ref<View>;
|
|
635
|
+
}) {
|
|
452
636
|
const { messages: m } = useProgressLanguage();
|
|
453
637
|
return (
|
|
454
638
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
|
|
455
639
|
<DialogButton colors={colors} icon="ChevronLeft" accessibilityLabel={m.previousFile} onPress={() => onStep(-1)} />
|
|
456
|
-
<Text style={{ minWidth: 44, textAlign: "center", color: colors.foregroundMuted, fontSize: 13, lineHeight: 18, fontVariant: ["tabular-nums"] }}>
|
|
640
|
+
<Text style={{ minWidth: 44, textAlign: "center", color: colors.foregroundMuted, fontSize: 13, lineHeight: 18, fontVariant: ["tabular-nums"] }}>
|
|
641
|
+
{m.galleryPosition(index + 1, count)}
|
|
642
|
+
</Text>
|
|
457
643
|
<DialogButton ref={nextRef} colors={colors} icon="ChevronRight" accessibilityLabel={m.nextFile} onPress={() => onStep(1)} />
|
|
458
644
|
</View>
|
|
459
645
|
);
|