Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add PDF export functionality #3264

Merged
merged 4 commits into from
Mar 25, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/desktop/changelog/0.3.13.md
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@
## New Features

- New Action Language setting
- Export entry content to a PDF file

## Improvements

2 changes: 2 additions & 0 deletions apps/desktop/locales/app/ar-DZ.json
Original file line number Diff line number Diff line change
@@ -15,6 +15,8 @@
"discover.rss_url": "رابط RSS",
"discover.select_placeholder": "اختر",
"entry_actions.copy_link": "نسخ الرابط",
"entry_actions.exported_notify": "تم تصدير المقال كملف PDF.",
"entry_actions.export_as_pdf": "تصدير كملف PDF",
"entry_actions.failed_to_save_to_eagle": "فشل في الحفظ إلى Eagle.",
"entry_actions.failed_to_save_to_instapaper": "فشل في الحفظ إلى Instapaper.",
"entry_actions.failed_to_save_to_obsidian": "فشل في الحفظ إلى Obsidian.",
2 changes: 2 additions & 0 deletions apps/desktop/locales/app/ar-IQ.json
Original file line number Diff line number Diff line change
@@ -15,6 +15,8 @@
"discover.rss_url": "رابط RSS",
"discover.select_placeholder": "اختر",
"entry_actions.copy_link": "نسخ الرابط",
"entry_actions.exported_notify": "تم تصدير المقال كملف PDF.",
"entry_actions.export_as_pdf": "تصدير كملف PDF",
"entry_actions.failed_to_save_to_eagle": "فشل في الحفظ إلى Eagle.",
"entry_actions.failed_to_save_to_instapaper": "فشل في الحفظ إلى Instapaper.",
"entry_actions.failed_to_save_to_obsidian": "فشل في الحفظ إلى Obsidian.",
2 changes: 2 additions & 0 deletions apps/desktop/locales/app/ar-KW.json
Original file line number Diff line number Diff line change
@@ -15,6 +15,8 @@
"discover.rss_url": "رابط RSS",
"discover.select_placeholder": "اختر",
"entry_actions.copy_link": "نسخ الرابط",
"entry_actions.exported_notify": "تم تصدير المقال كملف PDF.",
"entry_actions.export_as_pdf": "تصدير كملف PDF",
"entry_actions.failed_to_save_to_eagle": "فشل في الحفظ إلى Eagle.",
"entry_actions.failed_to_save_to_instapaper": "فشل في الحفظ إلى Instapaper.",
"entry_actions.failed_to_save_to_obsidian": "فشل في الحفظ إلى Obsidian",
2 changes: 2 additions & 0 deletions apps/desktop/locales/app/ar-MA.json
Original file line number Diff line number Diff line change
@@ -15,6 +15,8 @@
"discover.rss_url": "رابط RSS",
"discover.select_placeholder": "اختر",
"entry_actions.copy_link": "نسخ الرابط",
"entry_actions.exported_notify": "تم تصدير المقال كملف PDF.",
"entry_actions.export_as_pdf": "تصدير كملف PDF",
"entry_actions.failed_to_save_to_eagle": "فشل الحفظ إلى Eagle.",
"entry_actions.failed_to_save_to_instapaper": "فشل الحفظ إلى Instapaper.",
"entry_actions.failed_to_save_to_obsidian": "فشل الحفظ في Obsidian",
2 changes: 2 additions & 0 deletions apps/desktop/locales/app/ar-SA.json
Original file line number Diff line number Diff line change
@@ -15,6 +15,8 @@
"discover.rss_url": "عنوان URL لخلاصة RSS",
"discover.select_placeholder": "اختيار",
"entry_actions.copy_link": "نسخ الرابط",
"entry_actions.exported_notify": "تم تصدير المقال كملف PDF.",
"entry_actions.export_as_pdf": "تصدير كملف PDF",
"entry_actions.failed_to_save_to_eagle": "فشل في الحفظ إلى Eagle.",
"entry_actions.failed_to_save_to_instapaper": "فشل في الحفظ إلى Instapaper.",
"entry_actions.failed_to_save_to_readwise": "فشل في الحفظ إلى Readwise.",
2 changes: 2 additions & 0 deletions apps/desktop/locales/app/ar-TN.json
Original file line number Diff line number Diff line change
@@ -15,6 +15,8 @@
"discover.rss_url": "رابط RSS",
"discover.select_placeholder": "اختر",
"entry_actions.copy_link": "نسخ الرابط",
"entry_actions.exported_notify": "تم تصدير المقال كملف PDF.",
"entry_actions.export_as_pdf": "تصدير كملف PDF",
"entry_actions.failed_to_save_to_eagle": "فشل في الحفظ إلى Eagle.",
"entry_actions.failed_to_save_to_instapaper": "فشل في الحفظ إلى Instapaper.",
"entry_actions.failed_to_save_to_readwise": "فشل في الحفظ إلى Readwise.",
2 changes: 2 additions & 0 deletions apps/desktop/locales/app/de.json
Original file line number Diff line number Diff line change
@@ -15,6 +15,8 @@
"discover.rss_url": "RSS-URL",
"discover.select_placeholder": "Auswählen",
"entry_actions.copy_link": "Link kopieren",
"entry_actions.exported_notify": "Artikel als PDF exportiert.",
"entry_actions.export_as_pdf": "Als PDF exportieren",
"entry_actions.failed_to_save_to_eagle": "Speichern in Eagle fehlgeschlagen.",
"entry_actions.failed_to_save_to_instapaper": "Speichern in Instapaper fehlgeschlagen.",
"entry_actions.failed_to_save_to_obsidian": "Speichern in Obsidian fehlgeschlagen",
2 changes: 2 additions & 0 deletions apps/desktop/locales/app/en.json
Original file line number Diff line number Diff line change
@@ -108,6 +108,8 @@
"entry_actions.image_gallery": "Image Gallery",
"entry_actions.copied_notify": "{{which}} copied to clipboard.",
"entry_actions.copy_link": "Copy Link",
"entry_actions.exported_notify": "Exported article as PDF.",
"entry_actions.export_as_pdf": "Export as PDF",
"entry_actions.copy_title": "Copy Title",
"entry_actions.delete": "Delete",
"entry_actions.deleted": "Deleted.",
2 changes: 2 additions & 0 deletions apps/desktop/locales/app/es.json
Original file line number Diff line number Diff line change
@@ -15,6 +15,8 @@
"discover.rss_url": "RSS URL",
"discover.select_placeholder": "Seleccionar",
"entry_actions.copy_link": "Copiar enlace",
"entry_actions.exported_notify": "Artículo exportado como PDF.",
"entry_actions.export_as_pdf": "Exportar como PDF",
"entry_actions.failed_to_save_to_obsidian": "Error al guardar en Obsidian",
"entry_actions.mark_as_read": "Marcar como leído",
"entry_actions.mark_as_unread": "Marcar como no leído",
2 changes: 2 additions & 0 deletions apps/desktop/locales/app/fi.json
Original file line number Diff line number Diff line change
@@ -15,6 +15,8 @@
"discover.rss_url": "RSS-URL",
"discover.select_placeholder": "Valitse",
"entry_actions.copy_link": "Kopioi linkki",
"entry_actions.exported_notify": "Artikkeli vietiin PDF-muodossa.",
"entry_actions.export_as_pdf": "Vie PDF:nä",
"entry_actions.failed_to_save_to_eagle": "Tallennus Eagleen epäonnistui.",
"entry_actions.failed_to_save_to_instapaper": "Tallennus Instapaperiin epäonnistui.",
"entry_actions.failed_to_save_to_readwise": "Tallennus Readwiseen epäonnistui.",
2 changes: 2 additions & 0 deletions apps/desktop/locales/app/fr.json
Original file line number Diff line number Diff line change
@@ -15,6 +15,8 @@
"discover.rss_url": "URL du RSS",
"discover.select_placeholder": "Sélectionner",
"entry_actions.copy_link": "Copier le lien",
"entry_actions.exported_notify": "Article exporté en PDF.",
"entry_actions.export_as_pdf": "Exporter en PDF",
"entry_actions.failed_to_save_to_eagle": "Échec de l'enregistrement sur Eagle.",
"entry_actions.failed_to_save_to_instapaper": "Échec de l'enregistrement sur Instapaper.",
"entry_actions.failed_to_save_to_readwise": "Échec de l'enregistrement sur Readwise.",
2 changes: 2 additions & 0 deletions apps/desktop/locales/app/it.json
Original file line number Diff line number Diff line change
@@ -15,6 +15,8 @@
"discover.rss_url": "URL RSS",
"discover.select_placeholder": "Seleziona",
"entry_actions.copy_link": "Copia link",
"entry_actions.exported_notify": "Articolo esportato come PDF.",
"entry_actions.export_as_pdf": "Esporta come PDF",
"entry_actions.failed_to_save_to_eagle": "Salvataggio su Eagle non riuscito.",
"entry_actions.failed_to_save_to_instapaper": "Salvataggio su Instapaper non riuscito.",
"entry_actions.failed_to_save_to_readwise": "Salvataggio su Readwise non riuscito.",
2 changes: 2 additions & 0 deletions apps/desktop/locales/app/ja.json
Original file line number Diff line number Diff line change
@@ -107,6 +107,8 @@
"discover.target.lists": "リスト",
"entry_actions.copied_notify": "{{which}} をクリップボードにコピーしました。",
"entry_actions.copy_link": "リンクをコピー",
"entry_actions.exported_notify": "記事をPDFとしてエクスポートしました。",
"entry_actions.export_as_pdf": "PDFとしてエクスポート",
"entry_actions.copy_title": "タイトルをコピー",
"entry_actions.delete": "削除",
"entry_actions.deleted": "削除済み",
2 changes: 2 additions & 0 deletions apps/desktop/locales/app/ko.json
Original file line number Diff line number Diff line change
@@ -107,6 +107,8 @@
"discover.target.lists": "목록",
"entry_actions.copied_notify": "{{which}}이(가) 클립보드에 복사되었습니다",
"entry_actions.copy_link": "링크 복사",
"entry_actions.exported_notify": "PDF로 기사 내보내기 완료.",
"entry_actions.export_as_pdf": "PDF로 내보내기",
"entry_actions.copy_title": "제목 복사",
"entry_actions.delete": "삭제",
"entry_actions.deleted": "삭제됨",
2 changes: 2 additions & 0 deletions apps/desktop/locales/app/pt.json
Original file line number Diff line number Diff line change
@@ -15,6 +15,8 @@
"discover.rss_url": "RSS URL",
"discover.select_placeholder": "選択",
"entry_actions.copy_link": "リンクをコピー",
"entry_actions.exported_notify": "Artigo exportado como PDF.",
"entry_actions.export_as_pdf": "Exportar como PDF",
"entry_actions.failed_to_save_to_eagle": "Eagle への保存に失敗しました。",
"entry_actions.failed_to_save_to_instapaper": "Instapaper への保存に失敗しました。",
"entry_actions.failed_to_save_to_readwise": "Readwise への保存に失敗しました。",
2 changes: 2 additions & 0 deletions apps/desktop/locales/app/ru.json
Original file line number Diff line number Diff line change
@@ -103,6 +103,8 @@
"discover.target.lists": "Списки",
"entry_actions.copied_notify": "{{which}} скопировано в буфер обмена.",
"entry_actions.copy_link": "Копировать ссылку",
"entry_actions.exported_notify": "Статья экспортирована в PDF.",
"entry_actions.export_as_pdf": "Экспортировать в PDF",
"entry_actions.copy_title": "Копировать заголовок",
"entry_actions.delete": "Удалить",
"entry_actions.deleted": "Удалено.",
2 changes: 2 additions & 0 deletions apps/desktop/locales/app/tr.json
Original file line number Diff line number Diff line change
@@ -28,6 +28,8 @@
"discover.target.label": "Arama hedefi",
"discover.target.lists": "Listeler",
"entry_actions.copy_link": "Bağlantıyı kopyala",
"entry_actions.exported_notify": "Makale PDF olarak dışa aktarıldı.",
"entry_actions.export_as_pdf": "PDF olarak dışa aktar",
"entry_actions.failed_to_save_to_eagle": "Eagle'a kaydetme başarısız oldu.",
"entry_actions.failed_to_save_to_instapaper": "Instapaper'a kaydetme başarısız oldu.",
"entry_actions.failed_to_save_to_readwise": "Readwise'a kaydetme başarısız oldu.",
2 changes: 2 additions & 0 deletions apps/desktop/locales/app/zh-CN.json
Original file line number Diff line number Diff line change
@@ -108,6 +108,8 @@
"entry_actions.image_gallery": "图片库",
"entry_actions.copied_notify": "{{which}}已复制到剪贴板。",
"entry_actions.copy_link": "复制链接",
"entry_actions.exported_notify": "文章已导出为 PDF。",
"entry_actions.export_as_pdf": "导出为 PDF",
"entry_actions.copy_title": "复制标题",
"entry_actions.delete": "删除",
"entry_actions.deleted": "已删除",
2 changes: 2 additions & 0 deletions apps/desktop/locales/app/zh-HK.json
Original file line number Diff line number Diff line change
@@ -108,6 +108,8 @@
"entry_actions.image_gallery": "圖片庫",
"entry_actions.copied_notify": "{{which}}已複製到剪貼簿",
"entry_actions.copy_link": "複製連結",
"entry_actions.exported_notify": "文章已匯出為 PDF。",
"entry_actions.export_as_pdf": "匯出為 PDF",
"entry_actions.copy_title": "複製標題",
"entry_actions.delete": "刪除",
"entry_actions.deleted": "已刪除",
2 changes: 2 additions & 0 deletions apps/desktop/locales/app/zh-TW.json
Original file line number Diff line number Diff line change
@@ -104,6 +104,8 @@
"discover.target.lists": "列表",
"entry_actions.copied_notify": "{{which}}已複製到剪貼簿",
"entry_actions.copy_link": "複製連結",
"entry_actions.exported_notify": "文章已匯出為 PDF。",
"entry_actions.export_as_pdf": "匯出為 PDF",
"entry_actions.copy_title": "複製標題",
"entry_actions.delete": "刪除",
"entry_actions.deleted": "已刪除。",
4 changes: 4 additions & 0 deletions apps/desktop/src/renderer/src/hooks/biz/useEntryActions.tsx
Original file line number Diff line number Diff line change
@@ -146,6 +146,10 @@ export const useEntryActions = ({ entryId, view }: { entryId: string; view?: Fee
hide: !entry?.entries.url,
shortcut: shortcuts.entry.copyLink.key,
},
{
id: COMMAND_ID.entry.exportAsPDF,
onClick: runCmdFn(COMMAND_ID.entry.exportAsPDF, [{ entryId }]),
},
{
id: COMMAND_ID.entry.openInBrowser,
onClick: runCmdFn(COMMAND_ID.entry.openInBrowser, [{ entryId }]),
19 changes: 19 additions & 0 deletions apps/desktop/src/renderer/src/modules/command/commands/entry.tsx
Original file line number Diff line number Diff line change
@@ -175,6 +175,25 @@ export const useRegisterEntryCommands = () => {
})
},
},
{
id: COMMAND_ID.entry.exportAsPDF,
label: t("entry_actions.export_as_pdf"),
icon: <i className="i-mgc-pdf-cute-re" />,
run: ({ entryId }) => {
const entry = useEntryStore.getState().flatMapEntries[entryId]

if (!entry) {
toast.error("Failed to export as pdf: entry is not available", { duration: 3000 })
return
}

window.print()

toast(t("entry_actions.exported_notify"), {
duration: 1000,
})
},
},
{
id: COMMAND_ID.entry.copyTitle,
label: t("entry_actions.copy_title"),
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ export const COMMAND_ID = {
read: "entry:read",
toggleAISummary: "entry:toggle-ai-summary",
toggleAITranslation: "entry:toggle-ai-translation",
exportAsPDF: "entry:export-as-pdf",
},
integration: {
saveToEagle: "integration:save-to-eagle",
Original file line number Diff line number Diff line change
@@ -25,6 +25,11 @@ export type CopyLinkCommand = Command<{
fn: (data: { entryId: string }) => void
}>

export type ExportAsPDFCommand = Command<{
id: typeof COMMAND_ID.entry.exportAsPDF
fn: (data: { entryId: string }) => void
}>

export type CopyTitleCommand = Command<{
id: typeof COMMAND_ID.entry.copyTitle
fn: (data: { entryId: string }) => void
@@ -65,6 +70,7 @@ export type EntryCommand =
| StarCommand
| DeleteCommand
| CopyLinkCommand
| ExportAsPDFCommand
| CopyTitleCommand
| OpenInBrowserCommand
| ViewSourceContentCommand
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@ export const DEFAULT_ACTION_ORDER: ToolbarActionOrder = {
[
COMMAND_ID.entry.copyLink,
COMMAND_ID.entry.openInBrowser,
COMMAND_ID.entry.exportAsPDF,
COMMAND_ID.entry.read,
] as string[]
).includes(id),
10 changes: 10 additions & 0 deletions apps/desktop/src/renderer/src/styles/main.css
Original file line number Diff line number Diff line change
@@ -2,3 +2,13 @@
@import "./additional.css";
@import "./scrollbar.css";
@import "./cursor.css";

@media print {
* {
overflow: visible !important;
page-break-after: avoid;
page-break-before: avoid;
break-inside: avoid;
height: max-content;
}
}
25 changes: 25 additions & 0 deletions apps/mobile/src/icons/pdf_cute_re.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"

interface PdfCuteReIconProps {
width?: number
height?: number
color?: string
}

export const PdfCuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: PdfCuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path
stroke={color}
strokeLinejoin="round"
strokeWidth={2}
d="M10.5 2.5a3 3 0 0 1 3 3v.6c0 .372 0 .557.025.713a2 2 0 0 0 1.662 1.662c.156.025.341.025.713.025h.6a3 3 0 0 1 3 3M12 12l-.074.468a6 6 0 0 1-2.156 3.734l-.368.298.442-.17a6 6 0 0 1 4.31 0l.444.17-.37-.298a6 6 0 0 1-2.155-3.733zm-1.036-9.5h-.296c-2.022 0-3.032 0-3.82.357a4 4 0 0 0-1.991 1.991C4.5 5.636 4.5 6.646 4.5 8.668V14c0 3.288 0 4.931.908 6.038a4 4 0 0 0 .554.554c1.107.908 2.75.908 6.038.908 3.287 0 4.931 0 6.038-.908.202-.166.388-.352.554-.554.908-1.107.908-2.75.908-6.038v-2.964c0-1.033 0-1.55-.082-2.042a6 6 0 0 0-1.033-2.492C18.095 6.095 17.73 5.73 17 5s-1.095-1.095-1.502-1.385a6 6 0 0 0-2.492-1.033c-.492-.082-1.009-.082-2.042-.082Z"
/>
</Svg>
)
}
1 change: 1 addition & 0 deletions icons/mgc/pdf_cute_re.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

Unchanged files with check annotations Beta

export const BlockError = (props: { error: any; message: string }) => {
useEffect(() => {
captureException(props.error)
}, [])

Check warning on line 7 in apps/desktop/src/renderer/src/components/ui/markdown/renderers/BlockErrorBoundary.tsx

GitHub Actions / Format, Lint and Typecheck (lts/*)

React Hook useEffect has a missing dependency: 'props.error'. Either include it or remove the dependency array
return (
<div className="center flex min-h-12 flex-col rounded bg-red-400 py-4 text-sm text-white dark:bg-red-800">
{props.message}
export const MarkdownRenderContainerRefContext = reactCreateContext<HTMLElement | null>(null)
export const MarkdownImageRecordContext = createContext<Record<string, MarkdownImage>>({})

Check warning on line 8 in apps/desktop/src/renderer/src/components/ui/markdown/context.tsx

GitHub Actions / Format, Lint and Typecheck (lts/*)

Fast refresh only works when a file only exports components. Move your React context(s) to a separate file
export const MarkdownRenderActionContext = reactCreateContext<MarkdownRenderActions>({
transformUrl: (url) => url ?? "",
speed?: number
}
export const mountLottie = (url: string, options: LottieOptions) => {

Check warning on line 38 in apps/desktop/src/renderer/src/components/ui/lottie-container/index.tsx

GitHub Actions / Format, Lint and Typecheck (lts/*)

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components
const { once = true, height, width, x, y, className, speed } = options
const Lottie: FC = () => {
return () => {
clearTimeout(timerRef.current)
}
}, [date, language])

Check warning on line 66 in apps/desktop/src/renderer/src/components/ui/datetime/index.tsx

GitHub Actions / Format, Lint and Typecheck (lts/*)

React Hook useEffect has a missing dependency: 'formatDateString'. Either include it or remove the dependency array
const formated = dayjs(date).format(formatTemplateStringShort)
if (formated === dateString) {
type Props = Component<
React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
>
const MacOSVibrancy: Props = ({ className, children, ...rest }) => (

Check warning on line 10 in apps/desktop/src/renderer/src/components/ui/background/WindowUnderBlur.tsx

GitHub Actions / Format, Lint and Typecheck (lts/*)

Fast refresh only works when a file only exports components. Move your component(s) to a separate file
<Focusable className={cn("bg-native/30 dark:bg-native/10", className)} {...rest}>
{children}
</Focusable>
)
const Noop: Props = ({ children, className, ...rest }) => (

Check warning on line 16 in apps/desktop/src/renderer/src/components/ui/background/WindowUnderBlur.tsx

GitHub Actions / Format, Lint and Typecheck (lts/*)

Fast refresh only works when a file only exports components. Move your component(s) to a separate file
<Focusable className={cn("bg-native", className)} {...rest}>
{children}
</Focusable>
resetError()
onceRef.current = true
}
}, [location.pathname])

Check warning on line 33 in apps/desktop/src/renderer/src/components/errors/helper.ts

GitHub Actions / Format, Lint and Typecheck (lts/*)

React Hook useEffect has a missing dependency: 'resetError'. Either include it or remove the dependency array. If 'resetError' changes too often, find the parent component that defines it and wrap that definition in useCallback
}
export const withErrorGrand = <T extends Error, S extends new (...args: any[]) => T>(
version: makeResults[0]?.packageJSON?.version,
files: [],
}
makeResults = makeResults.map((result) => {

Check warning on line 250 in apps/desktop/forge.config.cts

GitHub Actions / Format, Lint and Typecheck (lts/*)

Assignment to function parameter 'makeResults'
result.artifacts = result.artifacts.map((artifact) => {
if (artifactRegex.test(artifact)) {
const newArtifact = `${path.dirname(artifact)}/${
})
yml.releaseDate = new Date().toISOString()
const ymlPath = `${path.dirname(makeResults[0]?.artifacts?.[0]!)}/${

Check warning on line 282 in apps/desktop/forge.config.cts

GitHub Actions / Format, Lint and Typecheck (lts/*)

Optional chain expressions can return undefined by design - using a non-null assertion is unsafe and wrong
ymlMapsMap[makeResults[0]?.platform!]

Check warning on line 283 in apps/desktop/forge.config.cts

GitHub Actions / Format, Lint and Typecheck (lts/*)

Optional chain expressions can return undefined by design - using a non-null assertion is unsafe and wrong
}`
const ymlStr = yaml.dump(yml, {