@open-press/core 1.2.0 → 1.2.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/engine/cli.mjs +1 -1
- package/engine/commands/_shared.mjs +10 -5
- package/engine/commands/deploy.mjs +19 -4
- package/engine/output/static-server.mjs +16 -9
- package/package.json +1 -1
- package/src/openpress/app/OpenPressApp.tsx +4 -1
- package/src/openpress/app/OpenPressRuntime.tsx +26 -1
- 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 +16 -9
- package/src/openpress/workbench/actions/ExportImageControl.tsx +0 -96
- package/src/styles/openpress/media-workspace.css +0 -230
package/engine/cli.mjs
CHANGED
|
@@ -87,7 +87,7 @@ Commands:
|
|
|
87
87
|
typecheck
|
|
88
88
|
image [--output <outputDir>] [--press <slug>] [--pages <selector>] [--no-build] [--dry-run]
|
|
89
89
|
pdf [--output <outputDir>/<pdf.filename>] [--press <slug>] [--no-build] [--dry-run]
|
|
90
|
-
deploy --confirm [--dry-run]
|
|
90
|
+
deploy --confirm [--press <slug>] [--dry-run]
|
|
91
91
|
doctor [--json] [--no-cache] # version + skill staleness check
|
|
92
92
|
upgrade [--dry-run] [--no-deps] [--no-skills] [--json] # apply updates; agent-driven
|
|
93
93
|
migrate [--dry-run] [--no-deps] [--no-skills] [--json] # alias for upgrade; reads migration notes
|
|
@@ -4,7 +4,7 @@ import fs from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { captureUrlPagesToPng, printUrlToPdf, stopChildProcess, waitForPrintReady } from "../output/chrome-pdf.mjs";
|
|
7
|
-
import { loadConfig
|
|
7
|
+
import { loadConfig } from "../runtime/config.mjs";
|
|
8
8
|
import { exportDocument } from "../document-export.mjs";
|
|
9
9
|
import { optimizePdfMediaForStaticRoot } from "../output/pdf-media.mjs";
|
|
10
10
|
|
|
@@ -142,7 +142,7 @@ function pressPrintUrl(host, port, slug) {
|
|
|
142
142
|
return `http://${host}:${port}/${normalized}?print=1`;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
function pressSuffixedFilename(baseFilename, slug) {
|
|
145
|
+
export function pressSuffixedFilename(baseFilename, slug) {
|
|
146
146
|
const normalized = (slug ?? "").replace(/^\/+|\/+$/g, "");
|
|
147
147
|
if (!normalized) return baseFilename;
|
|
148
148
|
const ext = path.extname(baseFilename);
|
|
@@ -150,6 +150,10 @@ function pressSuffixedFilename(baseFilename, slug) {
|
|
|
150
150
|
return `${stem}-${normalized}${ext}`;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
export function publicPdfHrefForFilename(filename) {
|
|
154
|
+
return `/${filename}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
153
157
|
export async function buildReactPdf({
|
|
154
158
|
root,
|
|
155
159
|
config,
|
|
@@ -294,18 +298,19 @@ export function startStaticServer(root, config, host, port) {
|
|
|
294
298
|
});
|
|
295
299
|
}
|
|
296
300
|
|
|
297
|
-
export async function writePdfStageDeployConfig(root, source, config) {
|
|
301
|
+
export async function writePdfStageDeployConfig(root, source, config, { pdfFilename = config.pdf.filename } = {}) {
|
|
298
302
|
const deployRoot = path.resolve(root, source);
|
|
299
303
|
const openpressDir = path.join(deployRoot, "openpress");
|
|
304
|
+
const pdfHref = publicPdfHrefForFilename(pdfFilename);
|
|
300
305
|
await fs.mkdir(openpressDir, { recursive: true });
|
|
301
306
|
await fs.writeFile(
|
|
302
307
|
path.join(openpressDir, "deploy.json"),
|
|
303
|
-
`${JSON.stringify({ pdf:
|
|
308
|
+
`${JSON.stringify({ pdf: pdfHref, deployed_at: new Date().toISOString() }, null, 2)}\n`,
|
|
304
309
|
"utf8",
|
|
305
310
|
);
|
|
306
311
|
await fs.writeFile(
|
|
307
312
|
path.join(deployRoot, "_headers"),
|
|
308
|
-
`${
|
|
313
|
+
`${pdfHref}\n Content-Type: application/pdf\n Content-Disposition: inline; filename="${pdfFilename}"\n`,
|
|
309
314
|
"utf8",
|
|
310
315
|
);
|
|
311
316
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { deploySync } from "../output/deploy-sync.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
buildReactPdf,
|
|
5
|
+
formatOpenPressCommand,
|
|
6
|
+
pressSuffixedFilename,
|
|
7
|
+
runCommand,
|
|
8
|
+
writePdfStageDeployConfig,
|
|
9
|
+
} from "./_shared.mjs";
|
|
4
10
|
|
|
5
11
|
export async function run({ root, config, options, recurse }) {
|
|
6
12
|
if (config.deploy.requiresConfirmation === true && !options.confirm) {
|
|
@@ -10,11 +16,15 @@ export async function run({ root, config, options, recurse }) {
|
|
|
10
16
|
const source = config.deploy.source;
|
|
11
17
|
const projectName = config.deploy.projectName;
|
|
12
18
|
const commitDirty = config.deploy.commitDirty;
|
|
19
|
+
const pressSlug = normalizePressSlug(options.press);
|
|
20
|
+
const pdfFilename = pressSuffixedFilename(config.pdf.filename, pressSlug);
|
|
21
|
+
const pdfArgs = ["pdf", ".", "--output", `${source}/${pdfFilename}`];
|
|
22
|
+
if (pressSlug) pdfArgs.push("--press", pressSlug);
|
|
13
23
|
if (options.dryRun) {
|
|
14
24
|
console.log("OpenPress deploy dry run");
|
|
15
25
|
console.log(`Command: ${formatOpenPressCommand(["render", ".", "--renderer", "react"])}`);
|
|
16
26
|
console.log(`Step: deploy-sync (copy ${config.outputDir} → ${source})`);
|
|
17
|
-
console.log(`Command: ${formatOpenPressCommand(
|
|
27
|
+
console.log(`Command: ${formatOpenPressCommand(pdfArgs)}`);
|
|
18
28
|
console.log(`Step: write ${source}/openpress/deploy.json with deployment metadata`);
|
|
19
29
|
console.log(`Command: npx wrangler pages deploy ${source}${projectName ? ` --project-name=${projectName}` : ""}${commitDirty ? " --commit-dirty=true" : ""}`);
|
|
20
30
|
return 0;
|
|
@@ -22,10 +32,15 @@ export async function run({ root, config, options, recurse }) {
|
|
|
22
32
|
const renderCode = await recurse("render", [root, "--renderer", "react"]);
|
|
23
33
|
if (renderCode !== 0) return renderCode;
|
|
24
34
|
await deploySync(root, config.outputDir, source);
|
|
25
|
-
await buildReactPdf({ root, config, outPath: path.resolve(root, source,
|
|
26
|
-
await writePdfStageDeployConfig(root, source, config);
|
|
35
|
+
await buildReactPdf({ root, config, outPath: path.resolve(root, source, pdfFilename), noBuild: true, recurse, pressSlug });
|
|
36
|
+
await writePdfStageDeployConfig(root, source, config, { pdfFilename });
|
|
27
37
|
const wranglerArgs = ["wrangler", "pages", "deploy", source];
|
|
28
38
|
if (projectName) wranglerArgs.push(`--project-name=${projectName}`);
|
|
29
39
|
if (commitDirty) wranglerArgs.push("--commit-dirty=true");
|
|
30
40
|
return runCommand("npx", wranglerArgs, root);
|
|
31
41
|
}
|
|
42
|
+
|
|
43
|
+
function normalizePressSlug(value) {
|
|
44
|
+
if (typeof value !== "string") return "";
|
|
45
|
+
return value.trim().replace(/^\/+|\/+$/g, "");
|
|
46
|
+
}
|
|
@@ -283,6 +283,11 @@ async function handleDeployRequest(req, res) {
|
|
|
283
283
|
return;
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
+
const body = await readJsonBody(req);
|
|
287
|
+
const slug = normalizePressSlug(body?.press);
|
|
288
|
+
const command = slug ? `open-press deploy . --confirm --press ${slug}` : "open-press deploy . --confirm";
|
|
289
|
+
const pdfFilename = pressFilename(config.pdf.filename, slug);
|
|
290
|
+
|
|
286
291
|
if (!isDeployConfigured()) {
|
|
287
292
|
writeJson(res, 400, {
|
|
288
293
|
ok: false,
|
|
@@ -292,15 +297,15 @@ async function handleDeployRequest(req, res) {
|
|
|
292
297
|
deploy_adapter: config.deploy.adapter,
|
|
293
298
|
deploy_source: config.deploy.source,
|
|
294
299
|
deploy_project_name: config.deploy.projectName,
|
|
295
|
-
command
|
|
300
|
+
command,
|
|
296
301
|
});
|
|
297
302
|
return;
|
|
298
303
|
}
|
|
299
304
|
|
|
300
|
-
const result = await runDeploy();
|
|
305
|
+
const result = await runDeploy(slug);
|
|
301
306
|
const deployedUrl = extractDeployUrl(result.stdout);
|
|
302
307
|
if (result.code === 0 && deployedUrl) {
|
|
303
|
-
await writeDeploymentPublicUrl(deployedUrl);
|
|
308
|
+
await writeDeploymentPublicUrl(deployedUrl, pdfFilename);
|
|
304
309
|
}
|
|
305
310
|
const deploymentInfo = await readDeploymentInfo();
|
|
306
311
|
const publicUrl = deployedUrl ?? deploymentInfo.public_url;
|
|
@@ -308,10 +313,10 @@ async function handleDeployRequest(req, res) {
|
|
|
308
313
|
ok: result.code === 0,
|
|
309
314
|
code: result.code,
|
|
310
315
|
deployed_at: deploymentInfo.deployed_at,
|
|
311
|
-
pdf: deployedUrl ? `${deployedUrl}/${
|
|
316
|
+
pdf: deployedUrl ? `${deployedUrl}/${pdfFilename}` : deploymentInfo.pdf,
|
|
312
317
|
public_url: publicUrl,
|
|
313
318
|
dirty: false,
|
|
314
|
-
command
|
|
319
|
+
command,
|
|
315
320
|
stdout: result.stdout,
|
|
316
321
|
stderr: result.stderr,
|
|
317
322
|
});
|
|
@@ -419,9 +424,11 @@ function runLocalPdfExport(slug = "") {
|
|
|
419
424
|
});
|
|
420
425
|
}
|
|
421
426
|
|
|
422
|
-
function runDeploy() {
|
|
427
|
+
function runDeploy(slug = "") {
|
|
428
|
+
const cliArgs = [CLI_ENTRY, "deploy", ".", "--confirm"];
|
|
429
|
+
if (slug) cliArgs.push("--press", slug);
|
|
423
430
|
return new Promise((resolve) => {
|
|
424
|
-
const child = spawn("node",
|
|
431
|
+
const child = spawn("node", cliArgs, {
|
|
425
432
|
cwd: workspace,
|
|
426
433
|
shell: false,
|
|
427
434
|
});
|
|
@@ -485,7 +492,7 @@ async function readDeploymentInfo() {
|
|
|
485
492
|
}
|
|
486
493
|
}
|
|
487
494
|
|
|
488
|
-
async function writeDeploymentPublicUrl(publicUrl) {
|
|
495
|
+
async function writeDeploymentPublicUrl(publicUrl, pdfFilename = config.pdf.filename) {
|
|
489
496
|
let deployConfig = {};
|
|
490
497
|
try {
|
|
491
498
|
deployConfig = JSON.parse(await fs.readFile(config.paths.deployMetadata, "utf8"));
|
|
@@ -495,7 +502,7 @@ async function writeDeploymentPublicUrl(publicUrl) {
|
|
|
495
502
|
await fs.mkdir(path.dirname(config.paths.deployMetadata), { recursive: true });
|
|
496
503
|
await fs.writeFile(
|
|
497
504
|
config.paths.deployMetadata,
|
|
498
|
-
`${JSON.stringify({ ...deployConfig, pdf: `${publicUrl}/${
|
|
505
|
+
`${JSON.stringify({ ...deployConfig, pdf: `${publicUrl}/${pdfFilename}`, public_url: publicUrl }, null, 2)}\n`,
|
|
499
506
|
"utf8",
|
|
500
507
|
);
|
|
501
508
|
}
|
package/package.json
CHANGED
|
@@ -230,7 +230,10 @@ export function OpenPressApp() {
|
|
|
230
230
|
const presentationSlug = state.activeSlug || currentRouteFromLocation().slug;
|
|
231
231
|
const openPresentation = state.document.meta.type === "slides" && presentationSlug
|
|
232
232
|
? (pageIndex: number) => {
|
|
233
|
-
|
|
233
|
+
const slug = normalizeSlug(presentationSlug);
|
|
234
|
+
const pathname = slug ? `/${slug}` : "/";
|
|
235
|
+
const hash = `#page-${String(pageIndex + 1).padStart(2, "0")}`;
|
|
236
|
+
window.open(`${pathname}?fullscreen=1${hash}`, "_blank", "noopener,noreferrer");
|
|
234
237
|
}
|
|
235
238
|
: undefined;
|
|
236
239
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useMemo, useState, type CSSProperties } from "react";
|
|
2
|
-
import { PrintDocument, PublicViewer, SlidePresentationPage } from "../reader";
|
|
2
|
+
import { PrintDocument, PublicViewer, SlidePublicViewer, SlidePresentationPage } from "../reader";
|
|
3
3
|
import { isPresentationModeLocation, isPrintModeLocation, isWorkspaceModeLocation } from "../shared";
|
|
4
4
|
import { HtmlWorkbench } from "../workbench";
|
|
5
5
|
import type {
|
|
@@ -82,6 +82,20 @@ export function OpenPressRuntime({
|
|
|
82
82
|
);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
if (!workspaceMode && document.meta.type === "slides") {
|
|
86
|
+
const slideDeploymentInfo = activeSlug
|
|
87
|
+
? { ...deploymentInfo, pdf: resolvePressPdfUrl(deploymentInfo.pdf, activeSlug) }
|
|
88
|
+
: deploymentInfo;
|
|
89
|
+
return (
|
|
90
|
+
<SlidePublicViewer
|
|
91
|
+
document={document}
|
|
92
|
+
pages={htmlPages}
|
|
93
|
+
style={style}
|
|
94
|
+
deploymentInfo={slideDeploymentInfo}
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
85
99
|
if (!workspaceMode) {
|
|
86
100
|
return <PublicViewer document={document} pages={htmlPages} style={style} deploymentInfo={deploymentInfo} />;
|
|
87
101
|
}
|
|
@@ -164,6 +178,17 @@ function useLocationVersion() {
|
|
|
164
178
|
return version;
|
|
165
179
|
}
|
|
166
180
|
|
|
181
|
+
function resolvePressPdfUrl(basePdfUrl: string | undefined, slug: string): string | undefined {
|
|
182
|
+
if (!basePdfUrl) return undefined;
|
|
183
|
+
const lastSlash = basePdfUrl.lastIndexOf("/");
|
|
184
|
+
const dir = lastSlash >= 0 ? basePdfUrl.slice(0, lastSlash + 1) : "";
|
|
185
|
+
const filename = lastSlash >= 0 ? basePdfUrl.slice(lastSlash + 1) : basePdfUrl;
|
|
186
|
+
const dot = filename.lastIndexOf(".");
|
|
187
|
+
const stem = dot >= 0 ? filename.slice(0, dot) : filename;
|
|
188
|
+
const ext = dot >= 0 ? filename.slice(dot) : "";
|
|
189
|
+
return `${dir}${stem}-${slug}${ext}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
167
192
|
function themeToCssVariables(theme?: Theme) {
|
|
168
193
|
const style: CSSProperties & Record<`--${string}`, string> = {
|
|
169
194
|
"--openpress-font-family": theme?.fontFamily ?? "'Noto Sans TC', 'PingFang TC', sans-serif",
|
|
@@ -13,11 +13,15 @@ export function PageThumbnails({
|
|
|
13
13
|
pages,
|
|
14
14
|
currentPageIndex,
|
|
15
15
|
onSelectPage,
|
|
16
|
+
selectedPageIndexes,
|
|
17
|
+
onTogglePage,
|
|
16
18
|
theme,
|
|
17
19
|
}: {
|
|
18
20
|
pages: HtmlPageBlock[];
|
|
19
21
|
currentPageIndex: number;
|
|
20
22
|
onSelectPage: (pageIndex: number, options?: { behavior?: ScrollBehavior }) => void;
|
|
23
|
+
selectedPageIndexes?: ReadonlySet<number>;
|
|
24
|
+
onTogglePage?: (pageIndex: number) => void;
|
|
21
25
|
theme?: Theme;
|
|
22
26
|
}) {
|
|
23
27
|
const pageWidthPx = parsePxLength(theme?.pageWidth) ?? FALLBACK_PAGE_WIDTH_PX;
|
|
@@ -40,7 +44,15 @@ export function PageThumbnails({
|
|
|
40
44
|
page={page}
|
|
41
45
|
index={index}
|
|
42
46
|
active={index === currentPageIndex}
|
|
43
|
-
|
|
47
|
+
selected={selectedPageIndexes?.has(index) ?? false}
|
|
48
|
+
selectionMode={Boolean(selectedPageIndexes && onTogglePage)}
|
|
49
|
+
onClick={() => {
|
|
50
|
+
if (selectedPageIndexes && onTogglePage) {
|
|
51
|
+
onTogglePage(index);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
onSelectPage(index, { behavior: "smooth" });
|
|
55
|
+
}}
|
|
44
56
|
pageWidthPx={pageWidthPx}
|
|
45
57
|
pageHeightPx={pageHeightPx}
|
|
46
58
|
aspectRatio={aspectRatio}
|
|
@@ -55,6 +67,8 @@ function ThumbnailCard({
|
|
|
55
67
|
page,
|
|
56
68
|
index,
|
|
57
69
|
active,
|
|
70
|
+
selected,
|
|
71
|
+
selectionMode,
|
|
58
72
|
onClick,
|
|
59
73
|
pageWidthPx,
|
|
60
74
|
pageHeightPx,
|
|
@@ -63,6 +77,8 @@ function ThumbnailCard({
|
|
|
63
77
|
page: HtmlPageBlock;
|
|
64
78
|
index: number;
|
|
65
79
|
active: boolean;
|
|
80
|
+
selected: boolean;
|
|
81
|
+
selectionMode: boolean;
|
|
66
82
|
onClick: () => void;
|
|
67
83
|
pageWidthPx: number;
|
|
68
84
|
pageHeightPx: number;
|
|
@@ -92,7 +108,7 @@ function ThumbnailCard({
|
|
|
92
108
|
cardRef.current?.scrollIntoView({ block: "nearest" });
|
|
93
109
|
}, [active]);
|
|
94
110
|
|
|
95
|
-
const className = `openpress-thumb-card${active ? " is-active" : ""}`;
|
|
111
|
+
const className = `openpress-thumb-card${active ? " is-active" : ""}${selected ? " is-selected" : ""}`;
|
|
96
112
|
// Wrap the page HTML using the same class structure as the main
|
|
97
113
|
// reader (`.openpress-html-page > .openpress-html-page__html`) so
|
|
98
114
|
// section-scoped CSS that targets those classes still applies in
|
|
@@ -124,12 +140,14 @@ function ThumbnailCard({
|
|
|
124
140
|
return (
|
|
125
141
|
<div
|
|
126
142
|
ref={cardRef}
|
|
127
|
-
role="button"
|
|
143
|
+
role={selectionMode ? "checkbox" : "button"}
|
|
128
144
|
tabIndex={0}
|
|
129
145
|
className={className}
|
|
130
146
|
data-openpress-thumb-index={index}
|
|
131
|
-
|
|
132
|
-
aria-
|
|
147
|
+
data-openpress-thumb-selected={selectionMode ? (selected ? "true" : "false") : undefined}
|
|
148
|
+
aria-label={selectionMode ? `選取第 ${index + 1} 頁:${pageTitle}` : `前往第 ${index + 1} 頁:${pageTitle}`}
|
|
149
|
+
aria-checked={selectionMode ? selected : undefined}
|
|
150
|
+
aria-current={!selectionMode && active ? "page" : undefined}
|
|
133
151
|
onClick={onClick}
|
|
134
152
|
onKeyDown={(event) => {
|
|
135
153
|
if (event.key === "Enter" || event.key === " ") {
|
|
@@ -138,6 +156,11 @@ function ThumbnailCard({
|
|
|
138
156
|
}
|
|
139
157
|
}}
|
|
140
158
|
>
|
|
159
|
+
{selectionMode ? (
|
|
160
|
+
<span className="openpress-thumb-card__check" aria-hidden="true">
|
|
161
|
+
{selected ? "✓" : ""}
|
|
162
|
+
</span>
|
|
163
|
+
) : null}
|
|
141
164
|
<div className="openpress-thumb-card__surface" ref={surfaceRef} style={{ aspectRatio }}>
|
|
142
165
|
<div className="openpress-thumb-card__frame" style={frameStyle}>
|
|
143
166
|
<div className={pageClass} style={pageStyle} data-openpress-thumb-page="true">
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type MouseEvent as ReactMouseEvent } from "react";
|
|
2
|
-
import { Maximize2, X } from "lucide-react";
|
|
2
|
+
import { Download, Maximize2, X } from "lucide-react";
|
|
3
3
|
import { createPageObjectEntityId } from "../document-model";
|
|
4
|
-
import type { HtmlPageBlock, ReaderDocument } from "../document-model";
|
|
4
|
+
import type { DeploymentInfo, HtmlPageBlock, ReaderDocument } from "../document-model";
|
|
5
5
|
import { pageIndexFromHash, replacePageRoute } from "./readerPageRoute";
|
|
6
6
|
import { clampReaderPageIndex, formatReaderPageNumber, normalizeReaderPageCount } from "./readerStateModel";
|
|
7
7
|
import { usePageViewportScale } from "./usePageViewportScale";
|
|
@@ -13,11 +13,13 @@ export function SlidePresentationPage({
|
|
|
13
13
|
pages,
|
|
14
14
|
style,
|
|
15
15
|
onExitPresentation,
|
|
16
|
+
deploymentInfo,
|
|
16
17
|
}: {
|
|
17
18
|
document: ReaderDocument;
|
|
18
19
|
pages: HtmlPageBlock[];
|
|
19
20
|
style: CSSProperties;
|
|
20
21
|
onExitPresentation?: (pageIndex: number) => void;
|
|
22
|
+
deploymentInfo?: DeploymentInfo;
|
|
21
23
|
}) {
|
|
22
24
|
const sourceContainerRef = useRef<HTMLDivElement | null>(null);
|
|
23
25
|
const stageRef = useRef<HTMLElement | null>(null);
|
|
@@ -35,6 +37,8 @@ export function SlidePresentationPage({
|
|
|
35
37
|
pageContainerRef: sourceContainerRef,
|
|
36
38
|
pageCount: pages.length,
|
|
37
39
|
layoutMode: "single",
|
|
40
|
+
initialScaleMode: "fit-page",
|
|
41
|
+
maxFitScale: Infinity,
|
|
38
42
|
});
|
|
39
43
|
const currentPage = pages[clampReaderPageIndex(currentPageIndex, normalizedPageCount)];
|
|
40
44
|
const currentPageLabel = formatReaderPageNumber(currentPageIndex + 1);
|
|
@@ -66,13 +70,6 @@ export function SlidePresentationPage({
|
|
|
66
70
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
67
71
|
if (isEditableTarget(event.target)) return;
|
|
68
72
|
if (event.key === "Escape") {
|
|
69
|
-
// Esc is reserved for exiting browser fullscreen. The chrome HUD
|
|
70
|
-
// already exposes explicit "re-enter fullscreen" and "close"
|
|
71
|
-
// buttons; navigating out of the presenter from a stray keystroke
|
|
72
|
-
// would yank the user back to the workspace shell unexpectedly
|
|
73
|
-
// (and racily, since the same Esc that triggered the browser's
|
|
74
|
-
// fullscreen exit is also delivered to this handler with
|
|
75
|
-
// fullscreenElement already null).
|
|
76
73
|
const activeDocument = globalThis.document;
|
|
77
74
|
if (activeDocument.fullscreenElement && activeDocument.exitFullscreen) {
|
|
78
75
|
event.preventDefault();
|
|
@@ -183,6 +180,11 @@ export function SlidePresentationPage({
|
|
|
183
180
|
</section>
|
|
184
181
|
|
|
185
182
|
<div className="openpress-slide-presenter__hud" aria-label="放映控制">
|
|
183
|
+
{deploymentInfo && document.meta.title ? (
|
|
184
|
+
<span className="openpress-slide-presenter__title" aria-label="簡報標題">
|
|
185
|
+
{document.meta.title}
|
|
186
|
+
</span>
|
|
187
|
+
) : null}
|
|
186
188
|
<span
|
|
187
189
|
className="openpress-slide-presenter__progress"
|
|
188
190
|
data-openpress-present-progress
|
|
@@ -200,16 +202,31 @@ export function SlidePresentationPage({
|
|
|
200
202
|
>
|
|
201
203
|
<Maximize2 aria-hidden="true" />
|
|
202
204
|
</button>
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
205
|
+
{deploymentInfo?.pdf ? (
|
|
206
|
+
<a
|
|
207
|
+
href={deploymentInfo.pdf}
|
|
208
|
+
target="_blank"
|
|
209
|
+
rel="noopener noreferrer"
|
|
210
|
+
className="openpress-slide-presenter__button"
|
|
211
|
+
data-openpress-present-pdf
|
|
212
|
+
aria-label="下載 PDF"
|
|
213
|
+
title="下載 PDF"
|
|
214
|
+
>
|
|
215
|
+
<Download aria-hidden="true" />
|
|
216
|
+
</a>
|
|
217
|
+
) : null}
|
|
218
|
+
{onExitPresentation ? (
|
|
219
|
+
<button
|
|
220
|
+
type="button"
|
|
221
|
+
className="openpress-slide-presenter__button"
|
|
222
|
+
data-openpress-present-exit
|
|
223
|
+
onClick={() => onExitPresentation(currentPageIndex)}
|
|
224
|
+
aria-label="回到工作台"
|
|
225
|
+
title="回到工作台"
|
|
226
|
+
>
|
|
227
|
+
<X aria-hidden="true" />
|
|
228
|
+
</button>
|
|
229
|
+
) : null}
|
|
213
230
|
</div>
|
|
214
231
|
</main>
|
|
215
232
|
);
|