release: 发布 v0.5.1
新增能力:内置 4 套红头、3 套正式文档和 3 套标书主题,支持主题推荐页边距、页面装饰、页眉页脚和页码;增加结构化公文、项目报告及标书 Front Matter,内置 Fandol 中文字体、两份教程和 14 份主题示例。 桌面工作流:重组更多菜单,增加新建、保存、另存为快捷键和未保存确认;另存为后跟随新路径,主题示例自动切换主题,Desktop 发行包完整携带教程、示例与字体。 问题修复:修正红头标题居中与正式文档字体;围栏代码按正文宽度自动换行,长内容安全断行,至少两行即可在当前页分页,并统一保留 8px 左侧内容留白;Docker Web 镜像正确打包 samples。 兼容与部署:未声明主题推荐设置时继续使用 16mm 默认页边距;Web 隐藏不适用的另存为。正式镜像 yixiong/md-to-pdf:v0.5.1 内容 ID 为 sha256:72f519a69bfd6cb2f6df30c2be938e58ceb6f20e4748a32551874de3d436b276,Compose 健康运行。 验证结果:最终修复前 65 个测试文件、286 项测试通过,最终代码块与主题定向测试 27 项通过;全项目类型检查、生产构建和 git diff --check 通过。容器检出 14 套主题、17 份 Markdown,代码块回归 PDF 为 2 页且无越界。NSIS SHA-256 为 676CCE73D782B0B15AC6BC68F253CFBC4740383583E4DAA98F746EDF9999C5CC,ZIP SHA-256 为 82BF6ADF1200604F145CC86FA3ED193955CF6741EBEE3DF8953483515CFF621F。
This commit is contained in:
+485
-37
@@ -59,6 +59,18 @@ import {
|
||||
useThemeResources
|
||||
} from "./use-theme-resources";
|
||||
import { sampleMarkdown } from "./sample-markdown";
|
||||
import { groupThemesByCategory } from "./theme-groups";
|
||||
import { getBuiltinThemeSample } from "./builtin-theme-samples";
|
||||
import {
|
||||
builtinTutorials,
|
||||
type BuiltinTutorial
|
||||
} from "./builtin-tutorials";
|
||||
import { DocumentActionDialog } from "./DocumentActionDialog";
|
||||
import { getDocumentShortcut } from "./document-shortcuts";
|
||||
import {
|
||||
applyThemeDefaults,
|
||||
selectTheme
|
||||
} from "./theme-margins";
|
||||
|
||||
const PrecisePdfPreview = lazy(async () => {
|
||||
const module = await import("./PrecisePdfPreview");
|
||||
@@ -67,18 +79,48 @@ const PrecisePdfPreview = lazy(async () => {
|
||||
|
||||
type PreviewMode = "quick" | "precise" | "continuous";
|
||||
type DocumentKind = "sample" | "new" | "file";
|
||||
type PendingDocumentAction =
|
||||
| {
|
||||
type: "new-current";
|
||||
}
|
||||
| {
|
||||
type: "close";
|
||||
systemRequest: boolean;
|
||||
};
|
||||
type DocumentDialogState =
|
||||
| {
|
||||
type: "new-location";
|
||||
}
|
||||
| {
|
||||
type: "unsaved";
|
||||
action: PendingDocumentAction;
|
||||
};
|
||||
const noImageResources: never[] = [];
|
||||
const startsWithBlankMarkdown =
|
||||
Boolean(window.mdToPdfDesktop) &&
|
||||
new URLSearchParams(window.location.search).get("new") === "1";
|
||||
const initialMarkdown = startsWithBlankMarkdown ? "" : sampleMarkdown;
|
||||
const initialFileName = startsWithBlankMarkdown ? "" : "示例文档.md";
|
||||
const initialDocumentKind: DocumentKind = startsWithBlankMarkdown
|
||||
? "new"
|
||||
: "sample";
|
||||
|
||||
export function App() {
|
||||
const [markdown, setMarkdown] = useState(sampleMarkdown);
|
||||
const [fileName, setFileName] = useState("示例文档.md");
|
||||
const [markdown, setMarkdown] = useState(initialMarkdown);
|
||||
const [fileName, setFileName] = useState(initialFileName);
|
||||
const [documentKind, setDocumentKind] =
|
||||
useState<DocumentKind>("sample");
|
||||
const [savedMarkdown, setSavedMarkdown] = useState(sampleMarkdown);
|
||||
useState<DocumentKind>(initialDocumentKind);
|
||||
const [savedMarkdown, setSavedMarkdown] = useState(initialMarkdown);
|
||||
const [exportConfig, setExportConfig] =
|
||||
useState<ExportConfig>(loadExportConfig);
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
const [documentDialog, setDocumentDialog] =
|
||||
useState<DocumentDialogState>();
|
||||
const [documentActionBusy, setDocumentActionBusy] = useState(false);
|
||||
const [moreMenuOpen, setMoreMenuOpen] = useState(false);
|
||||
const [moreSubmenuOpen, setMoreSubmenuOpen] = useState<
|
||||
"tutorials" | "samples" | undefined
|
||||
>();
|
||||
const [themeRefreshKey, setThemeRefreshKey] = useState(0);
|
||||
const [sourcePanelCollapsed, setSourcePanelCollapsed] = useState(false);
|
||||
const [status, setStatus] = useState("正在准备预览…");
|
||||
@@ -186,21 +228,60 @@ export function App() {
|
||||
);
|
||||
}, [documentDirty]);
|
||||
|
||||
useEffect(() => {
|
||||
const bridge = window.mdToPdfDesktop;
|
||||
if (!bridge) {
|
||||
return;
|
||||
}
|
||||
return bridge.onWindowCloseRequested(() => {
|
||||
requestDocumentClose(true);
|
||||
});
|
||||
}, [documentDialog, documentDirty]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
const shortcut = getDocumentShortcut(
|
||||
event,
|
||||
Boolean(window.mdToPdfDesktop)
|
||||
);
|
||||
if (!shortcut) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
if (documentDialog) {
|
||||
return;
|
||||
}
|
||||
if (shortcut === "save") {
|
||||
void handleSaveMarkdown();
|
||||
} else if (shortcut === "save-as") {
|
||||
void handleSaveMarkdownAs();
|
||||
} else if (shortcut === "new") {
|
||||
requestNewMarkdown();
|
||||
} else {
|
||||
requestDocumentClose(false);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [documentDialog, documentDirty, fileName, markdown]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!moreMenuOpen) {
|
||||
return;
|
||||
}
|
||||
const handlePointerDown = (event: PointerEvent) => {
|
||||
if (
|
||||
event.target instanceof Node &&
|
||||
!moreMenuRef.current?.contains(event.target)
|
||||
) {
|
||||
if (!(event.target instanceof Node)) {
|
||||
return;
|
||||
}
|
||||
if (!moreMenuRef.current?.contains(event.target)) {
|
||||
setMoreMenuOpen(false);
|
||||
setMoreSubmenuOpen(undefined);
|
||||
}
|
||||
};
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
setMoreMenuOpen(false);
|
||||
setMoreSubmenuOpen(undefined);
|
||||
}
|
||||
};
|
||||
window.addEventListener("pointerdown", handlePointerDown);
|
||||
@@ -214,19 +295,18 @@ export function App() {
|
||||
const handleThemesLoaded = useCallback(
|
||||
(availableThemes: ThemeSummary[], loadedRefreshKey: number) => {
|
||||
setExportConfig((currentConfig) => {
|
||||
if (
|
||||
availableThemes.some(
|
||||
(theme) => theme.id === currentConfig.themeId
|
||||
)
|
||||
) {
|
||||
return currentConfig;
|
||||
const currentTheme = availableThemes.find(
|
||||
(theme) => theme.id === currentConfig.themeId
|
||||
);
|
||||
if (currentTheme) {
|
||||
return applyThemeDefaults(currentConfig, currentTheme);
|
||||
}
|
||||
const preferredTheme =
|
||||
availableThemes.find((theme) => theme.id === "typora-github") ??
|
||||
availableThemes.find((theme) => theme.id === "typora-like") ??
|
||||
availableThemes[0];
|
||||
return preferredTheme
|
||||
? { ...currentConfig, themeId: preferredTheme.id }
|
||||
? selectTheme(currentConfig, preferredTheme)
|
||||
: currentConfig;
|
||||
});
|
||||
if (themeRefreshTargetRef.current === loadedRefreshKey) {
|
||||
@@ -268,6 +348,17 @@ export function App() {
|
||||
pdfError;
|
||||
const themeId = exportConfig.themeId;
|
||||
const selectedTheme = themes.find((theme) => theme.id === themeId);
|
||||
const themeGroups = useMemo(
|
||||
() => groupThemesByCategory(themes),
|
||||
[themes]
|
||||
);
|
||||
const sampleThemeGroups = useMemo(
|
||||
() =>
|
||||
groupThemesByCategory(
|
||||
themes.filter((theme) => getBuiltinThemeSample(theme.id))
|
||||
),
|
||||
[themes]
|
||||
);
|
||||
const effectiveFileName =
|
||||
documentKind === "new" || !fileName
|
||||
? createMarkdownFileName(markdown)
|
||||
@@ -806,11 +897,29 @@ export function App() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleNewMarkdown() {
|
||||
function requestNewMarkdown() {
|
||||
setMoreMenuOpen(false);
|
||||
if (!confirmDiscardChanges()) {
|
||||
setMoreSubmenuOpen(undefined);
|
||||
if (window.mdToPdfDesktop) {
|
||||
setDocumentDialog({ type: "new-location" });
|
||||
return;
|
||||
}
|
||||
requestCurrentWindowNew();
|
||||
}
|
||||
|
||||
function requestCurrentWindowNew() {
|
||||
if (documentDirty) {
|
||||
setDocumentDialog({
|
||||
type: "unsaved",
|
||||
action: { type: "new-current" }
|
||||
});
|
||||
return;
|
||||
}
|
||||
setDocumentDialog(undefined);
|
||||
void createMarkdownInCurrentWindow();
|
||||
}
|
||||
|
||||
async function createMarkdownInCurrentWindow() {
|
||||
try {
|
||||
setAppError("");
|
||||
await window.mdToPdfDesktop?.startNewMarkdown();
|
||||
@@ -827,18 +936,193 @@ export function App() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveMarkdown() {
|
||||
async function createMarkdownInNewWindow() {
|
||||
setDocumentDialog(undefined);
|
||||
try {
|
||||
setAppError("");
|
||||
await window.mdToPdfDesktop?.createNewWindow();
|
||||
setStatus("已在新窗口创建空白 Markdown");
|
||||
} catch (reason) {
|
||||
setAppError(
|
||||
reason instanceof Error
|
||||
? reason.message
|
||||
: "无法创建新的 Markdown 窗口"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function requestDocumentClose(systemRequest: boolean) {
|
||||
const bridge = window.mdToPdfDesktop;
|
||||
if (!bridge) {
|
||||
return;
|
||||
}
|
||||
if (documentDialog) {
|
||||
if (systemRequest) {
|
||||
void bridge.resolveWindowClose(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (documentDirty) {
|
||||
setDocumentDialog({
|
||||
type: "unsaved",
|
||||
action: {
|
||||
type: "close",
|
||||
systemRequest
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
void bridge.resolveWindowClose(true);
|
||||
}
|
||||
|
||||
async function completePendingDocumentAction(
|
||||
action: PendingDocumentAction
|
||||
) {
|
||||
setDocumentDialog(undefined);
|
||||
if (action.type === "new-current") {
|
||||
await createMarkdownInCurrentWindow();
|
||||
return;
|
||||
}
|
||||
await window.mdToPdfDesktop?.resolveWindowClose(true);
|
||||
}
|
||||
|
||||
function cancelDocumentDialog() {
|
||||
const currentDialog = documentDialog;
|
||||
setDocumentDialog(undefined);
|
||||
if (
|
||||
currentDialog?.type === "unsaved" &&
|
||||
currentDialog.action.type === "close" &&
|
||||
currentDialog.action.systemRequest
|
||||
) {
|
||||
void window.mdToPdfDesktop?.resolveWindowClose(false);
|
||||
}
|
||||
}
|
||||
|
||||
function discardPendingDocumentChanges() {
|
||||
if (documentDialog?.type !== "unsaved") {
|
||||
return;
|
||||
}
|
||||
void completePendingDocumentAction(documentDialog.action);
|
||||
}
|
||||
|
||||
async function saveBeforePendingDocumentAction() {
|
||||
if (documentDialog?.type !== "unsaved" || documentActionBusy) {
|
||||
return;
|
||||
}
|
||||
const action = documentDialog.action;
|
||||
setDocumentActionBusy(true);
|
||||
try {
|
||||
if (await saveMarkdown()) {
|
||||
await completePendingDocumentAction(action);
|
||||
}
|
||||
} finally {
|
||||
setDocumentActionBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleOpenThemeSample(theme: ThemeSummary) {
|
||||
const sample = getBuiltinThemeSample(theme.id);
|
||||
if (!sample) {
|
||||
setAppError(`找不到“${theme.name}”的内置示例`);
|
||||
return;
|
||||
}
|
||||
setMoreMenuOpen(false);
|
||||
setMoreSubmenuOpen(undefined);
|
||||
if (!confirmDiscardChanges()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setAppError("");
|
||||
await window.mdToPdfDesktop?.startNewMarkdown();
|
||||
setMarkdown(sample.markdown);
|
||||
setFileName(sample.fileName);
|
||||
setDocumentKind("sample");
|
||||
setSavedMarkdown(sample.markdown);
|
||||
setExportConfig((currentConfig) =>
|
||||
selectTheme(
|
||||
{
|
||||
...currentConfig,
|
||||
paper: {
|
||||
...currentConfig.paper,
|
||||
marginMode: "theme"
|
||||
},
|
||||
pageDecorationsMode: "theme"
|
||||
},
|
||||
theme
|
||||
)
|
||||
);
|
||||
setStatus(
|
||||
`已打开“${theme.name}”主题示例,可编辑后另存为 Markdown`
|
||||
);
|
||||
window.requestAnimationFrame(() => editorRef.current?.focus());
|
||||
} catch (reason) {
|
||||
setAppError(
|
||||
reason instanceof Error ? reason.message : "无法打开主题示例"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleOpenTutorial(tutorial: BuiltinTutorial) {
|
||||
const theme = themes.find(
|
||||
(availableTheme) => availableTheme.id === tutorial.themeId
|
||||
);
|
||||
if (!theme) {
|
||||
setAppError(`找不到教程需要的主题:${tutorial.themeId}`);
|
||||
return;
|
||||
}
|
||||
setMoreMenuOpen(false);
|
||||
setMoreSubmenuOpen(undefined);
|
||||
if (!confirmDiscardChanges()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setAppError("");
|
||||
await window.mdToPdfDesktop?.startNewMarkdown();
|
||||
setMarkdown(tutorial.markdown);
|
||||
setFileName(tutorial.fileName);
|
||||
setDocumentKind("sample");
|
||||
setSavedMarkdown(tutorial.markdown);
|
||||
setExportConfig((currentConfig) =>
|
||||
selectTheme(
|
||||
{
|
||||
...currentConfig,
|
||||
paper: {
|
||||
...currentConfig.paper,
|
||||
marginMode: "theme"
|
||||
},
|
||||
pageDecorationsMode: "theme"
|
||||
},
|
||||
theme
|
||||
)
|
||||
);
|
||||
setStatus(
|
||||
`已打开“${tutorial.name}”,可编辑后另存为 Markdown`
|
||||
);
|
||||
window.requestAnimationFrame(() => editorRef.current?.focus());
|
||||
} catch (reason) {
|
||||
setAppError(
|
||||
reason instanceof Error ? reason.message : "无法打开内置教程"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveMarkdown(forceSaveAs = false) {
|
||||
const suggestedName = ensureMarkdownFileName(fileName, markdown);
|
||||
try {
|
||||
setAppError("");
|
||||
const bridge = window.mdToPdfDesktop;
|
||||
let savedFileName = suggestedName;
|
||||
if (bridge) {
|
||||
const saved = await bridge.saveMarkdown(suggestedName, markdown);
|
||||
const saved = forceSaveAs
|
||||
? await bridge.saveMarkdownAs(suggestedName, markdown)
|
||||
: await bridge.saveMarkdown(suggestedName, markdown);
|
||||
if (!saved) {
|
||||
setStatus("已取消 Markdown 保存");
|
||||
return;
|
||||
setStatus(
|
||||
forceSaveAs
|
||||
? "已取消 Markdown 另存为"
|
||||
: "已取消 Markdown 保存"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
savedFileName = saved.fileName;
|
||||
} else {
|
||||
@@ -847,16 +1131,39 @@ export function App() {
|
||||
setFileName(savedFileName);
|
||||
setDocumentKind("file");
|
||||
setSavedMarkdown(markdown);
|
||||
setStatus(`Markdown 已保存:${savedFileName}`);
|
||||
setStatus(
|
||||
forceSaveAs
|
||||
? `Markdown 已另存为:${savedFileName}`
|
||||
: `Markdown 已保存:${savedFileName}`
|
||||
);
|
||||
return true;
|
||||
} catch (reason) {
|
||||
setAppError(
|
||||
reason instanceof Error ? reason.message : "Markdown 保存失败"
|
||||
reason instanceof Error
|
||||
? reason.message
|
||||
: forceSaveAs
|
||||
? "Markdown 另存为失败"
|
||||
: "Markdown 保存失败"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveMarkdown() {
|
||||
setMoreMenuOpen(false);
|
||||
setMoreSubmenuOpen(undefined);
|
||||
return saveMarkdown();
|
||||
}
|
||||
|
||||
async function handleSaveMarkdownAs() {
|
||||
setMoreMenuOpen(false);
|
||||
setMoreSubmenuOpen(undefined);
|
||||
return saveMarkdown(true);
|
||||
}
|
||||
|
||||
async function handleOpenThemeDirectory() {
|
||||
setMoreMenuOpen(false);
|
||||
setMoreSubmenuOpen(undefined);
|
||||
try {
|
||||
setAppError("");
|
||||
const themeDirectory =
|
||||
@@ -875,6 +1182,7 @@ export function App() {
|
||||
|
||||
async function handleRefreshThemes() {
|
||||
setMoreMenuOpen(false);
|
||||
setMoreSubmenuOpen(undefined);
|
||||
try {
|
||||
setAppError("");
|
||||
await window.mdToPdfDesktop?.refreshThemes();
|
||||
@@ -1011,7 +1319,10 @@ export function App() {
|
||||
aria-controls="document-actions-menu"
|
||||
aria-expanded={moreMenuOpen}
|
||||
aria-haspopup="menu"
|
||||
onClick={() => setMoreMenuOpen((open) => !open)}
|
||||
onClick={() => {
|
||||
setMoreMenuOpen((open) => !open);
|
||||
setMoreSubmenuOpen(undefined);
|
||||
}}
|
||||
>
|
||||
更多 ▾
|
||||
</button>
|
||||
@@ -1023,20 +1334,129 @@ export function App() {
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="menu-command"
|
||||
role="menuitem"
|
||||
onClick={() => void handleNewMarkdown()}
|
||||
onClick={requestNewMarkdown}
|
||||
>
|
||||
新建 Markdown
|
||||
<span>新建文件</span>
|
||||
{window.mdToPdfDesktop ? <kbd>Ctrl+N</kbd> : null}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="menu-command"
|
||||
role="menuitem"
|
||||
onClick={() => void handleSaveMarkdown()}
|
||||
>
|
||||
保存 Markdown
|
||||
<span>保存文件</span>
|
||||
<kbd>Ctrl+S</kbd>
|
||||
</button>
|
||||
{window.mdToPdfDesktop ? (
|
||||
<button
|
||||
type="button"
|
||||
className="menu-command"
|
||||
role="menuitem"
|
||||
onClick={() => void handleSaveMarkdownAs()}
|
||||
>
|
||||
<span>另存为文件</span>
|
||||
<kbd>Ctrl+Shift+S</kbd>
|
||||
</button>
|
||||
) : null}
|
||||
<div className="menu-separator" role="separator" />
|
||||
<div
|
||||
className="more-menu-submenu-item"
|
||||
onPointerEnter={() =>
|
||||
setMoreSubmenuOpen("tutorials")
|
||||
}
|
||||
onPointerLeave={() => setMoreSubmenuOpen(undefined)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="submenu-trigger"
|
||||
role="menuitem"
|
||||
aria-controls="tutorials-menu"
|
||||
aria-expanded={moreSubmenuOpen === "tutorials"}
|
||||
aria-haspopup="menu"
|
||||
onFocus={() => setMoreSubmenuOpen("tutorials")}
|
||||
onClick={() => setMoreSubmenuOpen("tutorials")}
|
||||
>
|
||||
<span>使用教程</span>
|
||||
<span aria-hidden="true">›</span>
|
||||
</button>
|
||||
{moreSubmenuOpen === "tutorials" ? (
|
||||
<div
|
||||
id="tutorials-menu"
|
||||
className="more-menu-submenu"
|
||||
role="menu"
|
||||
aria-label="使用教程"
|
||||
>
|
||||
{builtinTutorials.map((tutorial) => (
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
key={tutorial.id}
|
||||
onClick={() =>
|
||||
void handleOpenTutorial(tutorial)
|
||||
}
|
||||
>
|
||||
{tutorial.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
className="more-menu-submenu-item"
|
||||
onPointerEnter={() => setMoreSubmenuOpen("samples")}
|
||||
onPointerLeave={() => setMoreSubmenuOpen(undefined)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="submenu-trigger"
|
||||
role="menuitem"
|
||||
aria-controls="theme-samples-menu"
|
||||
aria-expanded={moreSubmenuOpen === "samples"}
|
||||
aria-haspopup="menu"
|
||||
onFocus={() => setMoreSubmenuOpen("samples")}
|
||||
onClick={() => setMoreSubmenuOpen("samples")}
|
||||
>
|
||||
<span>主题示例</span>
|
||||
<span aria-hidden="true">›</span>
|
||||
</button>
|
||||
{moreSubmenuOpen === "samples" ? (
|
||||
<div
|
||||
id="theme-samples-menu"
|
||||
className="more-menu-submenu"
|
||||
role="menu"
|
||||
aria-label="主题示例"
|
||||
>
|
||||
{sampleThemeGroups.map((group) => (
|
||||
<section
|
||||
className="sample-menu-group"
|
||||
key={group.category}
|
||||
>
|
||||
<span className="sample-menu-group-label">
|
||||
{group.label}
|
||||
</span>
|
||||
{group.themes.map((theme) => (
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
key={theme.id}
|
||||
onClick={() =>
|
||||
void handleOpenThemeSample(theme)
|
||||
}
|
||||
>
|
||||
{theme.name}
|
||||
</button>
|
||||
))}
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{window.mdToPdfDesktop ? (
|
||||
<>
|
||||
<div className="menu-separator" role="separator" />
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
@@ -1175,20 +1595,31 @@ export function App() {
|
||||
<select
|
||||
aria-label="预览主题"
|
||||
value={themeId}
|
||||
onChange={(event) =>
|
||||
setExportConfig((currentConfig) => ({
|
||||
...currentConfig,
|
||||
themeId: event.target.value
|
||||
}))
|
||||
}
|
||||
onChange={(event) => {
|
||||
const nextTheme = themes.find(
|
||||
(theme) => theme.id === event.target.value
|
||||
);
|
||||
if (nextTheme) {
|
||||
setExportConfig((currentConfig) =>
|
||||
selectTheme(currentConfig, nextTheme)
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{themes.length === 0 ? (
|
||||
<option value={themeId}>正在加载主题…</option>
|
||||
) : (
|
||||
themes.map((theme) => (
|
||||
<option key={theme.id} value={theme.id}>
|
||||
{theme.name}
|
||||
</option>
|
||||
themeGroups.map((group) => (
|
||||
<optgroup
|
||||
key={group.category}
|
||||
label={group.label}
|
||||
>
|
||||
{group.themes.map((theme) => (
|
||||
<option key={theme.id} value={theme.id}>
|
||||
{theme.name}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
@@ -1280,11 +1711,28 @@ export function App() {
|
||||
{settingsOpen ? (
|
||||
<ExportSettingsDrawer
|
||||
config={exportConfig}
|
||||
theme={selectedTheme}
|
||||
onChange={setExportConfig}
|
||||
onClose={() => setSettingsOpen(false)}
|
||||
onReset={() => setExportConfig(resetExportConfig())}
|
||||
/>
|
||||
) : null}
|
||||
{documentDialog?.type === "new-location" ? (
|
||||
<DocumentActionDialog
|
||||
type="new-location"
|
||||
onCurrentWindow={requestCurrentWindowNew}
|
||||
onNewWindow={() => void createMarkdownInNewWindow()}
|
||||
onCancel={cancelDocumentDialog}
|
||||
/>
|
||||
) : documentDialog?.type === "unsaved" ? (
|
||||
<DocumentActionDialog
|
||||
type="unsaved"
|
||||
busy={documentActionBusy}
|
||||
onSave={() => void saveBeforePendingDocumentAction()}
|
||||
onDiscard={discardPendingDocumentChanges}
|
||||
onCancel={cancelDocumentDialog}
|
||||
/>
|
||||
) : null}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user