@open-press/core 1.2.0 → 1.3.1
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 +2 -2
- package/engine/cli.mjs +1 -1
- package/engine/commands/_shared.mjs +10 -5
- package/engine/commands/deploy.mjs +19 -4
- package/engine/commands/typecheck.mjs +1 -1
- package/engine/document-export.mjs +1 -1
- package/engine/output/page-block.mjs +11 -2
- package/engine/output/public-assets.mjs +41 -6
- package/engine/output/static-server.mjs +84 -24
- package/engine/react/caption-numbering.mjs +2 -2
- package/engine/react/comment-marker.mjs +1 -2
- package/engine/react/document-entry.mjs +64 -11
- package/engine/react/document-export.d.mts +6 -0
- package/engine/react/document-export.mjs +158 -28
- package/engine/react/mdx-compile.mjs +4 -4
- package/engine/react/measurement-css.mjs +3 -3
- package/engine/react/page-folio.mjs +37 -0
- package/engine/react/pagination/allocator.mjs +4 -4
- package/engine/react/pipeline/frame-measurement.mjs +34 -16
- package/engine/react/press-tree-inspection.mjs +43 -13
- package/engine/react/project-asset-endpoint.mjs +45 -11
- package/engine/react/sources/heading-numbering.mjs +2 -2
- package/engine/react/sources/mdx-resolver.mjs +3 -3
- package/engine/react/style-discovery.mjs +60 -11
- package/engine/react/text-source-transform.mjs +18 -4
- package/engine/runtime/config.mjs +22 -22
- package/engine/runtime/file-utils.mjs +57 -13
- package/engine/runtime/inspection.mjs +40 -15
- package/engine/runtime/page-geometry.mjs +6 -6
- package/engine/runtime/source-text-tools.mjs +28 -4
- package/engine/runtime/source-workspace.mjs +6 -9
- package/engine/runtime/validation.mjs +42 -24
- package/package.json +1 -1
- package/src/openpress/app/OpenPressApp.tsx +10 -16
- package/src/openpress/app/OpenPressRuntime.tsx +29 -4
- package/src/openpress/app/WorkspaceGalleryPage.tsx +1 -1
- package/src/openpress/core/PageFolio.tsx +115 -0
- package/src/openpress/core/Press.tsx +5 -10
- package/src/openpress/core/Slide.tsx +11 -0
- package/src/openpress/core/index.tsx +4 -0
- package/src/openpress/core/types.ts +21 -13
- package/src/openpress/core/useSource.ts +1 -1
- package/src/openpress/document-model/workspaceManifestModel.ts +4 -9
- package/src/openpress/reader/PageThumbnailsPanel.tsx +28 -5
- package/src/openpress/reader/SlidePresentationPage.tsx +36 -19
- package/src/openpress/reader/SlidePublicPage.tsx +332 -0
- package/src/openpress/reader/index.ts +1 -0
- package/src/openpress/reader/pageViewportScaleModel.ts +5 -3
- package/src/openpress/reader/usePageViewportScale.ts +9 -5
- package/src/openpress/workbench/Workbench.tsx +46 -164
- package/src/openpress/workbench/actions/DeploymentControl.tsx +1 -1
- package/src/openpress/workbench/actions/ExportControl.tsx +267 -0
- package/src/openpress/workbench/actions/index.ts +1 -1
- package/src/openpress/workbench/actions/useDeploymentWorkbench.ts +7 -2
- package/src/openpress/workbench/hooks/useWorkbenchNavigation.ts +42 -0
- package/src/openpress/workbench/project/ProjectEntryPanel.tsx +2 -278
- package/src/openpress/workbench/shell/WorkbenchToolbarActions.tsx +206 -0
- package/src/styles/openpress/app-shell.css +0 -83
- package/src/styles/openpress/print-route.css +1 -3
- package/src/styles/openpress/project-preview-panel.css +5 -783
- package/src/styles/openpress/public-viewer.css +7 -249
- package/src/styles/openpress/reader-runtime.css +0 -274
- package/src/styles/openpress/slide-presenter.css +150 -0
- package/src/styles/openpress/slide-public-viewer.css +222 -0
- package/src/styles/openpress/workbench-dialog.css +267 -0
- package/src/styles/openpress/workbench-export.css +154 -0
- package/src/styles/openpress/workbench-inline-editor.css +128 -0
- package/src/styles/openpress/workbench-panels.css +0 -88
- package/src/styles/openpress/workbench-search.css +257 -0
- package/src/styles/openpress/workbench-toolbar.css +422 -0
- package/src/styles/openpress/workbench.css +34 -1263
- package/src/styles/openpress/workspace-gallery.css +0 -5
- package/src/styles/openpress.css +7 -1
- package/vite.config.ts +98 -25
- package/src/openpress/workbench/actions/ExportImageControl.tsx +0 -96
- package/src/styles/openpress/media-workspace.css +0 -230
|
@@ -4,10 +4,8 @@ import {
|
|
|
4
4
|
useState,
|
|
5
5
|
type CSSProperties,
|
|
6
6
|
} from "react";
|
|
7
|
-
import { ExternalLink, Home, MousePointer2, Play, Ruler } from "lucide-react";
|
|
8
7
|
import {
|
|
9
8
|
getProjectIdentity,
|
|
10
|
-
resolveAnchorPageIndex,
|
|
11
9
|
type DeploymentInfo,
|
|
12
10
|
type HtmlPageBlock,
|
|
13
11
|
type ReaderDocument,
|
|
@@ -34,14 +32,12 @@ import {
|
|
|
34
32
|
type InlineDocumentSourceTarget,
|
|
35
33
|
} from "./document";
|
|
36
34
|
import {
|
|
37
|
-
DeploymentControl,
|
|
38
|
-
ExportImageControl,
|
|
39
|
-
PageZoomControl,
|
|
40
|
-
SearchControl,
|
|
41
35
|
useDeploymentWorkbench,
|
|
42
36
|
} from "./actions";
|
|
43
37
|
import { PendingCommentsPanel, WorkbenchControlPanel, type WorkbenchPanel } from "./panels";
|
|
44
38
|
import { WorkbenchShell } from "./shell";
|
|
39
|
+
import { WorkbenchToolbarActions } from "./shell/WorkbenchToolbarActions";
|
|
40
|
+
import { useWorkbenchNavigation } from "./hooks/useWorkbenchNavigation";
|
|
45
41
|
import {
|
|
46
42
|
formatPageGeometrySpec,
|
|
47
43
|
formatInspectorSelection,
|
|
@@ -132,23 +128,13 @@ export function HtmlWorkbench({
|
|
|
132
128
|
onDocumentEdited: onDocumentRefresh,
|
|
133
129
|
});
|
|
134
130
|
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
reader.toggleRightPanel();
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
const selectWorkspaceAnchor = (anchorId: string, pageIndex?: number) => {
|
|
147
|
-
const targetPageIndex = resolveAnchorPageIndex(anchorPageMap, displayPages.length, anchorId, pageIndex);
|
|
148
|
-
if (targetPageIndex === null) return false;
|
|
149
|
-
selectWorkspacePage(targetPageIndex, { behavior: "smooth" });
|
|
150
|
-
return true;
|
|
151
|
-
};
|
|
131
|
+
const { selectWorkspaceAnchor, selectWorkspacePage } = useWorkbenchNavigation({
|
|
132
|
+
anchorPageMap,
|
|
133
|
+
pages: displayPages,
|
|
134
|
+
rightPanelOpen: reader.rightPanelOpen,
|
|
135
|
+
setPage: reader.setPage,
|
|
136
|
+
toggleRightPanel: reader.toggleRightPanel,
|
|
137
|
+
});
|
|
152
138
|
|
|
153
139
|
const comments = useInspectorComments({
|
|
154
140
|
workspaceMode,
|
|
@@ -244,145 +230,41 @@ export function HtmlWorkbench({
|
|
|
244
230
|
// don't rebuild the toolbar JSX. The toolbar depends on deploy/page/zoom
|
|
245
231
|
// state and inspector mode, but never on the composer draft text.
|
|
246
232
|
const toolbarActions = useMemo(() => (
|
|
247
|
-
|
|
248
|
-
{
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
aria-live="polite"
|
|
283
|
-
>
|
|
284
|
-
<span className="openpress-dev-pdf-status__spinner" aria-hidden="true" />
|
|
285
|
-
<span>{deployment.pdfStatusMessage}</span>
|
|
286
|
-
</span>
|
|
287
|
-
) : null}
|
|
288
|
-
</button>
|
|
289
|
-
<ExportImageControl
|
|
290
|
-
currentPageIndex={reader.currentPageIndex}
|
|
291
|
-
currentPageLabel={reader.currentPageLabel}
|
|
292
|
-
pressTitle={projectIdentity.name}
|
|
293
|
-
/>
|
|
294
|
-
</div>
|
|
295
|
-
<div className="openpress-workbench-toolbar__group openpress-workbench-toolbar__group--page" aria-label="頁面規格">
|
|
296
|
-
{isSlidePress && onOpenPresentation ? (
|
|
297
|
-
<button
|
|
298
|
-
type="button"
|
|
299
|
-
className="openpress-workbench-toolbar-action"
|
|
300
|
-
data-openpress-slide-present
|
|
301
|
-
data-openpress-toolbar-expanded="false"
|
|
302
|
-
data-openpress-toolbar-active="false"
|
|
303
|
-
aria-pressed="false"
|
|
304
|
-
title="進入放映模式"
|
|
305
|
-
aria-label="進入放映模式"
|
|
306
|
-
onClick={() => onOpenPresentation?.(reader.currentPageIndex)}
|
|
307
|
-
>
|
|
308
|
-
<Play aria-hidden="true" />
|
|
309
|
-
<span className="openpress-workbench-toolbar-action__label">放映</span>
|
|
310
|
-
</button>
|
|
311
|
-
) : null}
|
|
312
|
-
<button
|
|
313
|
-
type="button"
|
|
314
|
-
className="openpress-workbench-page-geometry"
|
|
315
|
-
data-openpress-page-geometry
|
|
316
|
-
title={pageGeometry.title}
|
|
317
|
-
aria-label={`頁面規格 ${pageGeometry.title}`}
|
|
318
|
-
>
|
|
319
|
-
<Ruler aria-hidden="true" />
|
|
320
|
-
<span className="openpress-workbench-page-geometry__label">{pageGeometry.label}</span>
|
|
321
|
-
<span className="openpress-workbench-page-geometry__dimensions">{pageGeometry.dimensions}</span>
|
|
322
|
-
</button>
|
|
323
|
-
<PageZoomControl
|
|
324
|
-
scaleMode={pageViewport.scaleMode}
|
|
325
|
-
scaleLabel={pageViewport.scaleLabel}
|
|
326
|
-
pageLayoutMode={pageLayoutMode}
|
|
327
|
-
onScaleModeChange={pageViewport.setScaleMode}
|
|
328
|
-
onPageLayoutModeChange={setPageLayoutMode}
|
|
329
|
-
/>
|
|
330
|
-
</div>
|
|
331
|
-
<div className="openpress-workbench-toolbar__group openpress-workbench-toolbar__group--right" aria-label="工作台狀態與發布">
|
|
332
|
-
{workspaceMode ? (
|
|
333
|
-
<SearchControl
|
|
334
|
-
sourceBlocksByPath={sourceBlocksByPath}
|
|
335
|
-
onSelectPage={selectWorkspacePage}
|
|
336
|
-
/>
|
|
337
|
-
) : null}
|
|
338
|
-
{workspaceMode && editStatusMessage ? (
|
|
339
|
-
<span
|
|
340
|
-
className="openpress-dev-edit-status openpress-dev-edit-status--toolbar"
|
|
341
|
-
data-openpress-edit-status={inlineEditStatus.state}
|
|
342
|
-
role="status"
|
|
343
|
-
aria-live="polite"
|
|
344
|
-
>
|
|
345
|
-
{inlineEditStatus.state === "saving" ? <span className="openpress-dev-edit-status__spinner" aria-hidden="true" /> : null}
|
|
346
|
-
<span>{editStatusMessage}</span>
|
|
347
|
-
</span>
|
|
348
|
-
) : null}
|
|
349
|
-
{workspaceMode ? (
|
|
350
|
-
<button
|
|
351
|
-
type="button"
|
|
352
|
-
className="openpress-workbench-toolbar-action"
|
|
353
|
-
data-openpress-inspector-toggle
|
|
354
|
-
data-openpress-inspector-active={inspector.inspectorMode ? "true" : "false"}
|
|
355
|
-
data-openpress-toolbar-expanded={inspectorToolbarExpanded ? "true" : "false"}
|
|
356
|
-
data-openpress-toolbar-active={inspectorToolbarExpanded ? "true" : "false"}
|
|
357
|
-
onClick={() => inspector.setInspectorMode(!inspector.inspectorMode)}
|
|
358
|
-
aria-pressed={inspector.inspectorMode}
|
|
359
|
-
title={inspector.inspectorMode ? "關閉註解" : "開啟註解"}
|
|
360
|
-
aria-label={inspector.inspectorMode ? "關閉註解" : "開啟註解"}
|
|
361
|
-
>
|
|
362
|
-
<MousePointer2 aria-hidden="true" />
|
|
363
|
-
<span className="openpress-workbench-toolbar-action__label">{inspector.inspectorMode ? "註解中" : "註解"}</span>
|
|
364
|
-
<span className="openpress-dev-inspector-status">{inspectorSelectionLabel}</span>
|
|
365
|
-
</button>
|
|
366
|
-
) : null}
|
|
367
|
-
{workspaceMode && inspector.inspectorMode ? (
|
|
368
|
-
<span
|
|
369
|
-
className="openpress-dev-inspector-status"
|
|
370
|
-
role="status"
|
|
371
|
-
aria-live="polite"
|
|
372
|
-
data-openpress-inspector-comment-status={comments.inspectorCommentStatus}
|
|
373
|
-
>
|
|
374
|
-
{comments.inspectorCommentStatusMessage}
|
|
375
|
-
</span>
|
|
376
|
-
) : null}
|
|
377
|
-
{deployment.localDeployEnabled ? (
|
|
378
|
-
<DeploymentControl
|
|
379
|
-
info={deployment.currentDeploymentInfo}
|
|
380
|
-
status={deployment.status}
|
|
381
|
-
onDeploy={deployment.handleDeploy}
|
|
382
|
-
/>
|
|
383
|
-
) : null}
|
|
384
|
-
</div>
|
|
385
|
-
</>
|
|
233
|
+
<WorkbenchToolbarActions
|
|
234
|
+
pages={displayPages}
|
|
235
|
+
currentPageIndex={reader.currentPageIndex}
|
|
236
|
+
pressTitle={projectIdentity.name}
|
|
237
|
+
theme={document.theme}
|
|
238
|
+
workspaceMode={workspaceMode}
|
|
239
|
+
sourceBlocksByPath={sourceBlocksByPath}
|
|
240
|
+
onSelectPage={selectWorkspacePage}
|
|
241
|
+
onBackToWorkspace={onBackToWorkspace}
|
|
242
|
+
isSlidePress={isSlidePress}
|
|
243
|
+
onOpenPresentation={onOpenPresentation}
|
|
244
|
+
pageGeometry={pageGeometry}
|
|
245
|
+
scaleMode={pageViewport.scaleMode}
|
|
246
|
+
scaleLabel={pageViewport.scaleLabel}
|
|
247
|
+
pageLayoutMode={pageLayoutMode}
|
|
248
|
+
onScaleModeChange={pageViewport.setScaleMode}
|
|
249
|
+
onPageLayoutModeChange={setPageLayoutMode}
|
|
250
|
+
inlineEditStatus={inlineEditStatus}
|
|
251
|
+
editStatusMessage={editStatusMessage}
|
|
252
|
+
inspectorMode={inspector.inspectorMode}
|
|
253
|
+
inspectorToolbarExpanded={inspectorToolbarExpanded}
|
|
254
|
+
inspectorSelectionLabel={inspectorSelectionLabel}
|
|
255
|
+
onInspectorModeChange={inspector.setInspectorMode}
|
|
256
|
+
inspectorCommentStatus={comments.inspectorCommentStatus}
|
|
257
|
+
inspectorCommentStatusMessage={comments.inspectorCommentStatusMessage}
|
|
258
|
+
deploymentInfo={deployment.currentDeploymentInfo}
|
|
259
|
+
deploymentStatus={deployment.status}
|
|
260
|
+
localDeployEnabled={deployment.localDeployEnabled}
|
|
261
|
+
onDeploy={deployment.handleDeploy}
|
|
262
|
+
onExportPdf={deployment.handleOpenWorkbenchPdf}
|
|
263
|
+
pdfDisabled={deployment.pdfButtonDisabled}
|
|
264
|
+
pdfLabel={deployment.pdfButtonText}
|
|
265
|
+
pdfStatusMessage={deployment.pdfStatusMessage}
|
|
266
|
+
pdfActionStatus={deployment.pdfActionStatus}
|
|
267
|
+
/>
|
|
386
268
|
), [
|
|
387
269
|
comments.inspectorCommentStatus,
|
|
388
270
|
comments.inspectorCommentStatusMessage,
|
|
@@ -394,8 +276,9 @@ export function HtmlWorkbench({
|
|
|
394
276
|
deployment.pdfButtonDisabled,
|
|
395
277
|
deployment.pdfButtonText,
|
|
396
278
|
deployment.pdfStatusMessage,
|
|
397
|
-
deployment.pdfToolbarExpanded,
|
|
398
279
|
deployment.status,
|
|
280
|
+
displayPages,
|
|
281
|
+
document.theme,
|
|
399
282
|
workspaceMode,
|
|
400
283
|
editStatusMessage,
|
|
401
284
|
inlineEditStatus.state,
|
|
@@ -416,7 +299,6 @@ export function HtmlWorkbench({
|
|
|
416
299
|
onBackToWorkspace,
|
|
417
300
|
onOpenPresentation,
|
|
418
301
|
reader.currentPageIndex,
|
|
419
|
-
reader.currentPageLabel,
|
|
420
302
|
projectIdentity.name,
|
|
421
303
|
]);
|
|
422
304
|
|
|
@@ -75,9 +75,9 @@ export function DeploymentControl({
|
|
|
75
75
|
className="openpress-workbench-toolbar-action"
|
|
76
76
|
data-openpress-deploy
|
|
77
77
|
data-openpress-deploy-status={kind}
|
|
78
|
+
data-openpress-deploy-state={status}
|
|
78
79
|
data-openpress-toolbar-expanded="false"
|
|
79
80
|
data-openpress-toolbar-active="false"
|
|
80
|
-
data-deploy-status={status}
|
|
81
81
|
aria-busy={busy ? "true" : "false"}
|
|
82
82
|
aria-label={buttonText}
|
|
83
83
|
title={description}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { useCallback, useEffect, useId, useRef, useState } from "react";
|
|
2
|
+
import { ChevronDown, Download, FileDown, FileText, Image as ImageIcon } from "lucide-react";
|
|
3
|
+
import { toPng } from "html-to-image";
|
|
4
|
+
import type { HtmlPageBlock, Theme } from "../../document-model";
|
|
5
|
+
import { PageThumbnails } from "../../reader";
|
|
6
|
+
import { WorkbenchDialog } from "../dialog";
|
|
7
|
+
|
|
8
|
+
type ExportDialog = "none" | "pdf" | "png";
|
|
9
|
+
type PngExportStatus = "idle" | "exporting" | "done" | "error";
|
|
10
|
+
|
|
11
|
+
export function ExportControl({
|
|
12
|
+
pages,
|
|
13
|
+
currentPageIndex,
|
|
14
|
+
pressTitle,
|
|
15
|
+
theme,
|
|
16
|
+
pdfHref,
|
|
17
|
+
onExportPdf,
|
|
18
|
+
pdfDisabled = false,
|
|
19
|
+
pdfLabel,
|
|
20
|
+
pdfStatusMessage,
|
|
21
|
+
pdfActionStatus,
|
|
22
|
+
}: {
|
|
23
|
+
pages: HtmlPageBlock[];
|
|
24
|
+
currentPageIndex: number;
|
|
25
|
+
pressTitle: string;
|
|
26
|
+
theme?: Theme;
|
|
27
|
+
pdfHref?: string;
|
|
28
|
+
onExportPdf?: () => void;
|
|
29
|
+
pdfDisabled?: boolean;
|
|
30
|
+
pdfLabel?: string;
|
|
31
|
+
pdfStatusMessage?: string | null;
|
|
32
|
+
pdfActionStatus?: string;
|
|
33
|
+
}) {
|
|
34
|
+
const menuId = useId();
|
|
35
|
+
const pdfTitleId = useId();
|
|
36
|
+
const pngTitleId = useId();
|
|
37
|
+
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
38
|
+
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
39
|
+
const [activeDialog, setActiveDialog] = useState<ExportDialog>("none");
|
|
40
|
+
const [pngPageIndex, setPngPageIndex] = useState(currentPageIndex);
|
|
41
|
+
const [selectedPngPageIndexes, setSelectedPngPageIndexes] = useState<Set<number>>(() => new Set());
|
|
42
|
+
const [pngStatus, setPngStatus] = useState<PngExportStatus>("idle");
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!dropdownOpen) return undefined;
|
|
46
|
+
const handlePointerDown = (event: PointerEvent) => {
|
|
47
|
+
if (event.target instanceof Node && rootRef.current?.contains(event.target)) return;
|
|
48
|
+
setDropdownOpen(false);
|
|
49
|
+
};
|
|
50
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
51
|
+
if (event.key === "Escape") setDropdownOpen(false);
|
|
52
|
+
};
|
|
53
|
+
window.addEventListener("pointerdown", handlePointerDown);
|
|
54
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
55
|
+
return () => {
|
|
56
|
+
window.removeEventListener("pointerdown", handlePointerDown);
|
|
57
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
58
|
+
};
|
|
59
|
+
}, [dropdownOpen]);
|
|
60
|
+
|
|
61
|
+
const openPdf = () => { setDropdownOpen(false); setActiveDialog("pdf"); };
|
|
62
|
+
const openPng = () => {
|
|
63
|
+
setDropdownOpen(false);
|
|
64
|
+
setPngPageIndex(currentPageIndex);
|
|
65
|
+
setSelectedPngPageIndexes(createAllPageIndexSet(pages));
|
|
66
|
+
setPngStatus("idle");
|
|
67
|
+
setActiveDialog("png");
|
|
68
|
+
};
|
|
69
|
+
const closeDialog = () => setActiveDialog("none");
|
|
70
|
+
|
|
71
|
+
const togglePngPage = (pageIndex: number) => {
|
|
72
|
+
setSelectedPngPageIndexes((current) => {
|
|
73
|
+
const next = new Set(current);
|
|
74
|
+
if (next.has(pageIndex)) next.delete(pageIndex);
|
|
75
|
+
else next.add(pageIndex);
|
|
76
|
+
return next;
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const selectAllPngPages = () => setSelectedPngPageIndexes(createAllPageIndexSet(pages));
|
|
81
|
+
const clearPngPages = () => setSelectedPngPageIndexes(new Set());
|
|
82
|
+
|
|
83
|
+
const handleExportPng = useCallback(async () => {
|
|
84
|
+
if (pngStatus === "exporting") return;
|
|
85
|
+
const pageIndexes = pages
|
|
86
|
+
.map((page) => page.pageNumber - 1)
|
|
87
|
+
.filter((pageIndex) => selectedPngPageIndexes.has(pageIndex));
|
|
88
|
+
if (pageIndexes.length === 0) return;
|
|
89
|
+
setPngStatus("exporting");
|
|
90
|
+
try {
|
|
91
|
+
const safeTitle = sanitizeFilename(pressTitle) || "openpress";
|
|
92
|
+
for (const pageIndex of pageIndexes) {
|
|
93
|
+
const pageEl = typeof window === "undefined"
|
|
94
|
+
? null
|
|
95
|
+
: window.document.querySelector<HTMLElement>(`[data-openpress-page-index="${pageIndex}"]`);
|
|
96
|
+
if (!pageEl) throw new Error(`找不到第 ${pageIndex + 1} 頁元素`);
|
|
97
|
+
const dataUrl = await toPng(pageEl, { pixelRatio: 2, cacheBust: true, backgroundColor: "#ffffff" });
|
|
98
|
+
const safePage = String(pageIndex + 1).padStart(2, "0");
|
|
99
|
+
const link = window.document.createElement("a");
|
|
100
|
+
link.href = dataUrl;
|
|
101
|
+
link.download = `${safeTitle}-${safePage}.png`;
|
|
102
|
+
window.document.body.appendChild(link);
|
|
103
|
+
link.click();
|
|
104
|
+
link.remove();
|
|
105
|
+
}
|
|
106
|
+
setPngStatus("done");
|
|
107
|
+
window.setTimeout(() => setPngStatus("idle"), 1600);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error("[openpress] PNG export failed", error);
|
|
110
|
+
setPngStatus("error");
|
|
111
|
+
window.setTimeout(() => setPngStatus("idle"), 2400);
|
|
112
|
+
}
|
|
113
|
+
}, [pages, pngStatus, pressTitle, selectedPngPageIndexes]);
|
|
114
|
+
|
|
115
|
+
const hasPdf = Boolean(pdfHref ?? onExportPdf);
|
|
116
|
+
const selectedPngCount = selectedPngPageIndexes.size;
|
|
117
|
+
const pngButtonLabel = pngStatus === "exporting" ? "匯出中…"
|
|
118
|
+
: pngStatus === "done" ? "已下載"
|
|
119
|
+
: pngStatus === "error" ? "匯出失敗"
|
|
120
|
+
: selectedPngCount === 0 ? "請選擇圖片"
|
|
121
|
+
: `匯出 ${selectedPngCount} 張`;
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div ref={rootRef} className="openpress-workbench-zoom-control-wrap" data-openpress-export-control>
|
|
125
|
+
<button
|
|
126
|
+
type="button"
|
|
127
|
+
className="openpress-workbench-zoom-control"
|
|
128
|
+
aria-label="匯出"
|
|
129
|
+
title="匯出"
|
|
130
|
+
aria-haspopup="menu"
|
|
131
|
+
aria-expanded={dropdownOpen}
|
|
132
|
+
aria-controls={dropdownOpen ? menuId : undefined}
|
|
133
|
+
onClick={() => setDropdownOpen((v) => !v)}
|
|
134
|
+
>
|
|
135
|
+
<FileDown aria-hidden="true" />
|
|
136
|
+
<span>匯出</span>
|
|
137
|
+
<ChevronDown className="openpress-workbench-zoom-control__chevron" aria-hidden="true" />
|
|
138
|
+
</button>
|
|
139
|
+
|
|
140
|
+
{dropdownOpen ? (
|
|
141
|
+
<div id={menuId} className="openpress-workbench-zoom-menu" role="menu" aria-label="匯出選項">
|
|
142
|
+
<div className="openpress-workbench-zoom-menu__section" role="group">
|
|
143
|
+
{hasPdf ? (
|
|
144
|
+
<button type="button" className="openpress-workbench-zoom-menu__item" role="menuitem" onClick={openPdf}>
|
|
145
|
+
<span className="openpress-workbench-zoom-menu__check" aria-hidden="true" />
|
|
146
|
+
<FileText aria-hidden="true" />
|
|
147
|
+
<span>PDF</span>
|
|
148
|
+
</button>
|
|
149
|
+
) : null}
|
|
150
|
+
<button type="button" className="openpress-workbench-zoom-menu__item" role="menuitem" onClick={openPng}>
|
|
151
|
+
<span className="openpress-workbench-zoom-menu__check" aria-hidden="true" />
|
|
152
|
+
<ImageIcon aria-hidden="true" />
|
|
153
|
+
<span>PNG 圖片</span>
|
|
154
|
+
</button>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
) : null}
|
|
158
|
+
|
|
159
|
+
{activeDialog === "pdf" ? (
|
|
160
|
+
<WorkbenchDialog
|
|
161
|
+
titleId={pdfTitleId}
|
|
162
|
+
eyebrow="匯出"
|
|
163
|
+
title="PDF"
|
|
164
|
+
closeLabel="關閉"
|
|
165
|
+
className="openpress-export-dialog"
|
|
166
|
+
onClose={closeDialog}
|
|
167
|
+
footer={
|
|
168
|
+
pdfHref ? (
|
|
169
|
+
<a
|
|
170
|
+
href={pdfHref}
|
|
171
|
+
target="_blank"
|
|
172
|
+
rel="noopener noreferrer"
|
|
173
|
+
className="openpress-export-dialog__action"
|
|
174
|
+
onClick={closeDialog}
|
|
175
|
+
>
|
|
176
|
+
<Download aria-hidden="true" />
|
|
177
|
+
<span>下載 PDF</span>
|
|
178
|
+
</a>
|
|
179
|
+
) : onExportPdf ? (
|
|
180
|
+
<button
|
|
181
|
+
type="button"
|
|
182
|
+
className="openpress-export-dialog__action"
|
|
183
|
+
disabled={pdfDisabled}
|
|
184
|
+
onClick={onExportPdf}
|
|
185
|
+
>
|
|
186
|
+
<Download aria-hidden="true" />
|
|
187
|
+
<span>{pdfLabel ?? "生成 PDF"}</span>
|
|
188
|
+
{pdfStatusMessage ? (
|
|
189
|
+
<span
|
|
190
|
+
className="openpress-dev-pdf-status"
|
|
191
|
+
data-openpress-pdf-status={pdfActionStatus}
|
|
192
|
+
role="status"
|
|
193
|
+
aria-live="polite"
|
|
194
|
+
>
|
|
195
|
+
<span className="openpress-dev-pdf-status__spinner" aria-hidden="true" />
|
|
196
|
+
<span>{pdfStatusMessage}</span>
|
|
197
|
+
</span>
|
|
198
|
+
) : null}
|
|
199
|
+
</button>
|
|
200
|
+
) : null
|
|
201
|
+
}
|
|
202
|
+
>
|
|
203
|
+
<div className="openpress-export-dialog__body">
|
|
204
|
+
<p className="openpress-export-dialog__summary">共 {pages.length} 頁</p>
|
|
205
|
+
</div>
|
|
206
|
+
</WorkbenchDialog>
|
|
207
|
+
) : null}
|
|
208
|
+
|
|
209
|
+
{activeDialog === "png" ? (
|
|
210
|
+
<WorkbenchDialog
|
|
211
|
+
titleId={pngTitleId}
|
|
212
|
+
eyebrow="匯出"
|
|
213
|
+
title="PNG 圖片"
|
|
214
|
+
closeLabel="關閉"
|
|
215
|
+
className="openpress-export-dialog openpress-export-png-dialog"
|
|
216
|
+
onClose={closeDialog}
|
|
217
|
+
footer={
|
|
218
|
+
<button
|
|
219
|
+
type="button"
|
|
220
|
+
className="openpress-export-dialog__action"
|
|
221
|
+
disabled={pngStatus === "exporting" || selectedPngCount === 0}
|
|
222
|
+
data-openpress-export-status={pngStatus}
|
|
223
|
+
onClick={handleExportPng}
|
|
224
|
+
>
|
|
225
|
+
<Download aria-hidden="true" />
|
|
226
|
+
<span>{pngButtonLabel}</span>
|
|
227
|
+
</button>
|
|
228
|
+
}
|
|
229
|
+
>
|
|
230
|
+
<div className="openpress-export-dialog__selection-bar">
|
|
231
|
+
<span>{selectedPngCount} / {pages.length} 張已選</span>
|
|
232
|
+
<div className="openpress-export-dialog__selection-actions">
|
|
233
|
+
<button type="button" onClick={selectAllPngPages}>全選</button>
|
|
234
|
+
<button type="button" onClick={clearPngPages}>清除</button>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
<div className="openpress-export-dialog__thumbs">
|
|
238
|
+
<PageThumbnails
|
|
239
|
+
pages={pages}
|
|
240
|
+
currentPageIndex={pngPageIndex}
|
|
241
|
+
selectedPageIndexes={selectedPngPageIndexes}
|
|
242
|
+
onTogglePage={(idx) => {
|
|
243
|
+
setPngPageIndex(idx);
|
|
244
|
+
togglePngPage(idx);
|
|
245
|
+
}}
|
|
246
|
+
onSelectPage={(idx) => setPngPageIndex(idx)}
|
|
247
|
+
theme={theme}
|
|
248
|
+
/>
|
|
249
|
+
</div>
|
|
250
|
+
</WorkbenchDialog>
|
|
251
|
+
) : null}
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function sanitizeFilename(value: string): string {
|
|
257
|
+
return value
|
|
258
|
+
.replace(/[\\/:*?"<>|]+/g, "-")
|
|
259
|
+
.replace(/\s+/g, "-")
|
|
260
|
+
.replace(/-+/g, "-")
|
|
261
|
+
.replace(/^-+|-+$/g, "")
|
|
262
|
+
.slice(0, 80);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function createAllPageIndexSet(pages: HtmlPageBlock[]) {
|
|
266
|
+
return new Set(pages.map((page) => page.pageNumber - 1));
|
|
267
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export * from "./deploymentStatusModel";
|
|
2
2
|
export * from "./DeploymentControl";
|
|
3
|
-
export * from "./
|
|
3
|
+
export * from "./ExportControl";
|
|
4
4
|
export * from "./PageZoomControl";
|
|
5
5
|
export * from "./SearchControl";
|
|
6
6
|
export * from "./useDeploymentWorkbench";
|
|
@@ -54,7 +54,12 @@ export function useDeploymentWorkbench({ deploymentInfo, pressSlug = null }: Use
|
|
|
54
54
|
}
|
|
55
55
|
setStatus("deploying");
|
|
56
56
|
try {
|
|
57
|
-
const
|
|
57
|
+
const requestBody = pressSlug ? { press: pressSlug } : {};
|
|
58
|
+
const response = await fetch("/__openpress/deploy", {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: { "Content-Type": "application/json" },
|
|
61
|
+
body: JSON.stringify(requestBody),
|
|
62
|
+
});
|
|
58
63
|
if (response.status === 404 || response.status === 405) {
|
|
59
64
|
setStatus("unavailable");
|
|
60
65
|
return;
|
|
@@ -96,7 +101,7 @@ export function useDeploymentWorkbench({ deploymentInfo, pressSlug = null }: Use
|
|
|
96
101
|
console.error("OpenPress deploy unavailable", error);
|
|
97
102
|
setStatus("unavailable");
|
|
98
103
|
}
|
|
99
|
-
}, [status, currentDeploymentInfo.configured]);
|
|
104
|
+
}, [status, currentDeploymentInfo.configured, pressSlug]);
|
|
100
105
|
|
|
101
106
|
const handleOpenLatestLocalPdf = useCallback(async () => {
|
|
102
107
|
if (pdfActionStatus === "generating") return;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { resolveAnchorPageIndex } from "../../document-model";
|
|
3
|
+
import { PUBLIC_DRAWER_BREAKPOINT, type DisplayPage } from "../../reader";
|
|
4
|
+
|
|
5
|
+
type SetPage = (pageIndex: number, options?: { behavior?: ScrollBehavior }) => void;
|
|
6
|
+
|
|
7
|
+
export function useWorkbenchNavigation({
|
|
8
|
+
anchorPageMap,
|
|
9
|
+
pages,
|
|
10
|
+
rightPanelOpen,
|
|
11
|
+
setPage,
|
|
12
|
+
toggleRightPanel,
|
|
13
|
+
}: {
|
|
14
|
+
anchorPageMap: Map<string, number>;
|
|
15
|
+
pages: DisplayPage[];
|
|
16
|
+
rightPanelOpen: boolean;
|
|
17
|
+
setPage: SetPage;
|
|
18
|
+
toggleRightPanel: () => void;
|
|
19
|
+
}) {
|
|
20
|
+
const selectWorkspacePage = useCallback((pageIndex: number, options?: { behavior?: ScrollBehavior }) => {
|
|
21
|
+
setPage(pageIndex, options);
|
|
22
|
+
if (
|
|
23
|
+
typeof window !== "undefined"
|
|
24
|
+
&& window.innerWidth < PUBLIC_DRAWER_BREAKPOINT
|
|
25
|
+
&& rightPanelOpen
|
|
26
|
+
) {
|
|
27
|
+
toggleRightPanel();
|
|
28
|
+
}
|
|
29
|
+
}, [rightPanelOpen, setPage, toggleRightPanel]);
|
|
30
|
+
|
|
31
|
+
const selectWorkspaceAnchor = useCallback((anchorId: string, pageIndex?: number) => {
|
|
32
|
+
const targetPageIndex = resolveAnchorPageIndex(anchorPageMap, pages.length, anchorId, pageIndex);
|
|
33
|
+
if (targetPageIndex === null) return false;
|
|
34
|
+
selectWorkspacePage(targetPageIndex, { behavior: "smooth" });
|
|
35
|
+
return true;
|
|
36
|
+
}, [anchorPageMap, pages.length, selectWorkspacePage]);
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
selectWorkspaceAnchor,
|
|
40
|
+
selectWorkspacePage,
|
|
41
|
+
};
|
|
42
|
+
}
|