Skip to content

Commit

Permalink
feat: 主要功能全部完成
Browse files Browse the repository at this point in the history
  • Loading branch information
LeafYeeXYZ committed Aug 27, 2024
1 parent 2741e31 commit 3c03eca
Show file tree
Hide file tree
Showing 14 changed files with 209 additions and 37 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

相比于 LaTeX, 本软件更加简单易用, 省去了复杂的配置和学习成本, 但仍然能够满足大部分学术论文的排版需求; 相比于 Word, 本软件完全接管了格式调整的工作, 你只需选择指定的论文模板 (目前仅支持心理学报格式), 专心写作即可

如果你有一定的计算机基础, 也推荐您使用命令行程序 [MarkdownPaper](https://github.com/LeafYeeXYZ/MarkdownPaper), 它的处理逻辑与本软件一致, 但更加灵活, 您可以使用任何您喜欢的编辑器撰写学术论文
如果你有一定的计算机基础, 也推荐您使用命令行程序 [MarkdownPaper](https://github.com/LeafYeeXYZ/MarkdownPaper), 它的处理逻辑与本软件基本一致, 但更加灵活和强大 (如支持导出 DOCX 文件), 您还可以使用任何您喜欢的编辑器撰写学术论文

## 使用方法

Expand Down
Binary file modified bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default defineConfig({
renderer: {
resolve: {
alias: {
'@renderer': resolve('src/renderer/src')
'@lib': resolve('lib')
}
},
plugins: [react()]
Expand Down
File renamed without changes.
14 changes: 14 additions & 0 deletions resources/lib/theme.ts → lib/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
import type { PDFOptions } from 'puppeteer'

export interface MarkdownPaperTheme {
/**
* 主题名称
*/
themeName: string
/**
* css 样式
* 不含 \<style>\</style>
Expand Down Expand Up @@ -33,7 +37,17 @@ export interface MarkdownPaperTheme {
pdfOptions: PDFOptions
}

export const getTheme = (themeName: string): MarkdownPaperTheme => {
switch (themeName) {
case 'aps':
return APS
default:
return APS
}
}

export const APS: MarkdownPaperTheme = {
themeName: 'aps',
css: `
* {
font-family: 'Times', 'Times New Roman', '宋体', 'SimSun', '华文宋体', 'STSong'; /* 所有数字和英文字体都用 Times New Roman */
Expand Down
79 changes: 78 additions & 1 deletion src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ import { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'
import fs from 'node:fs/promises'
import { readFileSync } from 'node:fs'
import path from 'node:path'
import puppeteer from 'puppeteer'
import { mdToHtml } from '../../lib/render'
import { getTheme } from '../../lib/theme'

function createWindow(): void {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 1000,
width: 905,
height: 670,
minWidth: 768,
minHeight: 512,
Expand Down Expand Up @@ -98,6 +102,79 @@ app.whenReady().then(() => {
await fs.writeFile(path.resolve(filepath, filename), content, 'utf-8')
}
)
ipcMain.handle(
'createTextFile',
async (_, content: string, ext: string, defaultName: string): Promise<boolean> => {
// 弹出文件选择框
const { filePath, canceled } = await dialog.showSaveDialog({
title: '导出论文',
filters: [{ name: '文本文件', extensions: [ext] }],
defaultPath: defaultName + '.' + ext,
showsTagField: false,
properties: ['createDirectory']
})
if (canceled) {
return false
} else {
await fs.writeFile(filePath, content, 'utf-8')
return true
}
}
)
ipcMain.handle(
'embedImageIntoHtml',
async (_, html: string, filepath: string): Promise<string> => {
return html.replace(/<img src="(.+?)"/g, (match, p1) => {
if (p1.startsWith('http')) return match
try {
const url = path.resolve(filepath, decodeURI(p1))
const data = readFileSync(url).toString('base64')
return `<img src="data:image/${path.extname(p1).replace('.', '')};base64,${data}"`
} catch (_) {
return match
}
})
}
)
ipcMain.handle(
'createPdf',
async (
_,
markdown: string,
themeName: string = 'aps',
filepath: string,
filename: string
): Promise<boolean> => {
const { filePath, canceled } = await dialog.showSaveDialog({
title: '导出论文',
filters: [{ name: 'PDF 文件', extensions: ['pdf'] }],
defaultPath: filename.split('.')[0] + '.pdf',
showsTagField: false,
properties: ['createDirectory']
})
if (canceled) {
return false
}
const dist = path.resolve(filePath)
const browser = await puppeteer.launch()
const page = await browser.newPage()
const theme = getTheme(themeName)
const html = (await mdToHtml(markdown, theme)).replace(/<img src="(.+?)"/g, (match, p1) => {
if (p1.startsWith('http')) return match
try {
const url = path.resolve(filepath, decodeURI(p1))
const data = readFileSync(url).toString('base64')
return `<img src="data:image/${path.extname(p1).replace('.', '')};base64,${data}"`
} catch (_) {
return match
}
})
await page.setContent(html)
await page.pdf({ path: dist, ...theme.pdfOptions })
await browser.close()
return true
}
)

createWindow()

Expand Down
8 changes: 8 additions & 0 deletions src/preload/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ declare global {
newPaper: () => Promise<{ filepath: string; filename: string }>
openPaper: () => Promise<{ filepath: string; filename: string; content: string }>
savePaper: (filepath: string, filename: string, content: string) => Promise<void>
createTextFile: (content: string, ext: string, defaultName: string) => Promise<boolean>
embedImageIntoHtml: (html: string, filepath: string) => Promise<string>
createPdf: (
markdown: string,
themeName: string,
filepath: string,
filename: string
) => Promise<boolean>
}
}
}
30 changes: 22 additions & 8 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,31 @@ import { electronAPI } from '@electron-toolkit/preload'

// Custom APIs for renderer
const api = {
openBrowser: async (url: string): Promise<void> => {
await electronAPI.ipcRenderer.invoke('openBrowser', url)
openBrowser: (url: string): Promise<void> => {
return electronAPI.ipcRenderer.invoke('openBrowser', url)
},
newPaper: async (): Promise<{ filepath: string; filename: string }> => {
return await electronAPI.ipcRenderer.invoke('newPaper')
newPaper: (): Promise<{ filepath: string; filename: string }> => {
return electronAPI.ipcRenderer.invoke('newPaper')
},
openPaper: async (): Promise<{ filepath: string; filename: string; content: string }> => {
return await electronAPI.ipcRenderer.invoke('openPaper')
openPaper: (): Promise<{ filepath: string; filename: string; content: string }> => {
return electronAPI.ipcRenderer.invoke('openPaper')
},
savePaper: async (filepath: string, filename: string, content: string): Promise<void> => {
await electronAPI.ipcRenderer.invoke('savePaper', filepath, filename, content)
savePaper: (filepath: string, filename: string, content: string): Promise<void> => {
return electronAPI.ipcRenderer.invoke('savePaper', filepath, filename, content)
},
createTextFile: (content: string, ext: string, defaultName: string): Promise<boolean> => {
return electronAPI.ipcRenderer.invoke('createTextFile', content, ext, defaultName)
},
embedImageIntoHtml: (html: string, filepath: string): Promise<string> => {
return electronAPI.ipcRenderer.invoke('embedImageIntoHtml', html, filepath)
},
createPdf: (
markdown: string,
themeName: string,
filepath: string,
filename: string
): Promise<boolean> => {
return electronAPI.ipcRenderer.invoke('createPdf', markdown, themeName, filepath, filename)
}
}

Expand Down
10 changes: 6 additions & 4 deletions src/renderer/src/components/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Input } from 'antd'
import { useStore } from '../lib/useStore'

export default function Editor(): JSX.Element {
const { setMarkdown, filepath, filename, markdown } = useStore()
const { setMarkdown, filepath, filename, markdown, disabled } = useStore()
return (
<div className="p-2 pl-1 pt-0 w-full h-full overflow-hidden">
<div
Expand All @@ -11,12 +11,14 @@ export default function Editor(): JSX.Element {
>
<div className="w-full h-8 p-2 mb-2 border rounded-md shadow-sm bg-gray-50 text-xs text-gray-500 text-ellipsis overflow-hidden text-nowrap">
<span className="font-bold">
当前文件: <span className="font-normal">{filename || '...'}</span>
当前文件:&nbsp;&nbsp;
<span className="font-normal">{filename || '...'}</span>
</span>
</div>
<div className="w-full h-8 p-2 mb-2 border rounded-md shadow-sm bg-gray-50 text-xs text-gray-500 text-ellipsis overflow-hidden text-nowrap">
<span className="font-bold">
存放位置: <span className="font-normal">{filepath || '...'}</span>
存放位置:&nbsp;&nbsp;
<span className="font-normal">{filepath || '...'}</span>
</span>
</div>
<Input.TextArea
Expand All @@ -28,7 +30,7 @@ export default function Editor(): JSX.Element {
autoCapitalize="off"
spellCheck={false}
onChange={(e) => setMarkdown(e.target.value)}
disabled={filepath === '' || filename === ''}
disabled={filepath === '' || filename === '' || disabled}
value={markdown}
/>
</div>
Expand Down
12 changes: 7 additions & 5 deletions src/renderer/src/components/Preview.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { mdToHtml } from '../../../../resources/lib/render'
import { mdToHtml } from '../../../../lib/render'
import { useStore } from '../lib/useStore'
import { useState, useEffect } from 'react'

export default function Preview(): JSX.Element {
const { markdown, theme } = useStore()
const { markdown, theme, filepath } = useStore()
const [html, setHtml] = useState('')
useEffect(() => {
mdToHtml(markdown, theme).then(setHtml)
}, [markdown, theme])
mdToHtml(markdown, theme)
.then((html) => window.api.embedImageIntoHtml(html, filepath))
.then((html) => setHtml(html))
}, [markdown, theme, filepath])
return (
<div className="p-2 pr-1 pt-0 w-full h-full overflow-hidden">
<div
Expand All @@ -17,7 +19,7 @@ export default function Preview(): JSX.Element {
{markdown ? (
<iframe className="w-full h-full p-8" srcDoc={html} sandbox="" />
) : (
<span className="text-xs text-gray-300 block p-2">内容预览</span>
<span className="text-xs text-gray-300 block p-2">论文内容预览</span>
)}
</div>
</div>
Expand Down
82 changes: 68 additions & 14 deletions src/renderer/src/components/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,42 @@ import {
QuestionCircleOutlined,
FileMarkdownOutlined,
FilePdfOutlined,
FileTextOutlined,
FileWordOutlined
FileTextOutlined
} from '@ant-design/icons'
import { useStore } from '../lib/useStore'
import { mdToHtml } from '../../../../lib/render'
import { flushSync } from 'react-dom'

export default function Toolbar(): JSX.Element {
const { filepath, filename, setFilename, setFilepath, setMarkdown, messageApi, markdown } =
useStore()
const {
filepath,
filename,
setFilename,
setFilepath,
setMarkdown,
messageApi,
markdown,
theme,
disabled,
setDisabled
} = useStore()
return (
<div className="h-full w-full flex justify-start items-center gap-2 px-2">
<Button
type={filepath && filename ? 'default' : 'primary'}
disabled={disabled}
onClick={async () => {
const { filename, filepath } = await window.api.newPaper()
setFilename(filename)
setFilepath(filepath)
setMarkdown('')
}}
>
<FileAddOutlined /> 新建
</Button>
<Button
type={filepath && filename ? 'default' : 'primary'}
disabled={disabled}
onClick={async () => {
const { filename, filepath, content } = await window.api.openPaper()
setFilename(filename)
Expand All @@ -38,7 +52,7 @@ export default function Toolbar(): JSX.Element {
<FolderOpenOutlined /> 打开
</Button>
<Button
disabled={!filepath || !filename || !markdown.length || !messageApi}
disabled={!filepath || !filename || !markdown.length || !messageApi || disabled}
onClick={async () => {
await window.api
.savePaper(filepath, filename, markdown)
Expand All @@ -49,27 +63,67 @@ export default function Toolbar(): JSX.Element {
<SaveOutlined /> 保存
</Button>
<Button
onClick={() => {
window.api.openBrowser('https://github.com/LeafYeeXYZ/EasyPaper')
disabled={disabled}
onClick={async () => {
await window.api.openBrowser('https://github.com/LeafYeeXYZ/EasyPaper')
}}
>
<QuestionCircleOutlined /> 帮助
</Button>
<p className="text-xs px-1 font-bold text-nowrap text-gray-600">导出:</p>
<Button disabled>
<Button
disabled={!filepath || !filename || !markdown.length || !messageApi || disabled}
onClick={async () => {
await window.api
.createTextFile(markdown, 'md', filename.split('.')[0])
.then((res) => res && messageApi!.success('导出成功'))
.catch(() => messageApi!.error('导出失败'))
}}
>
<FileMarkdownOutlined /> Markdown
</Button>
<Button disabled>
<Button
disabled={!filepath || !filename || !markdown.length || !messageApi || disabled}
onClick={async () => {
flushSync(() => setDisabled(true))
messageApi!.open({
content: '导出中...',
type: 'loading',
duration: 0,
key: 'exporting'
})
await window.api
.createPdf(markdown, theme.themeName, filepath, filename)
.then((res) => {
messageApi!.destroy()
res && messageApi!.success('导出成功')
})
.catch(() => {
messageApi!.destroy()
messageApi!.error('导出失败')
})
.finally(() => setDisabled(false))
}}
>
<FilePdfOutlined /> PDF
</Button>
<Button disabled>
<Button
disabled={!filepath || !filename || !markdown.length || !messageApi || disabled}
onClick={async () => {
const html = await window.api.embedImageIntoHtml(
await mdToHtml(markdown, theme),
filepath
)
await window.api
.createTextFile(html, 'html', filename.split('.')[0])
.then((res) => res && messageApi!.success('导出成功'))
.catch(() => messageApi!.error('导出失败'))
}}
>
<FileTextOutlined /> HTML
</Button>
<Button disabled>
<FileWordOutlined /> Word
</Button>
<p className="text-xs px-1 font-bold text-nowrap text-gray-600">论文格式:</p>
<Select defaultValue="aps">
<Select defaultValue="aps" disabled={disabled}>
<Select.Option value="aps">心理学报</Select.Option>
</Select>
&nbsp;
Expand Down
Loading

0 comments on commit 3c03eca

Please sign in to comment.