added print preview option

This commit is contained in:
Gamosoft 2026-04-06 12:58:38 +02:00
parent 96b5fd8cfc
commit 80797f9cfe
16 changed files with 139 additions and 24 deletions

View File

@ -300,18 +300,20 @@ def generate_export_html(
title: str, title: str,
content: str, content: str,
theme_css: str, theme_css: str,
is_dark: bool = False is_dark: bool = False,
show_print_button: bool = False
) -> str: ) -> str:
""" """
Generate a standalone HTML document for a note. Generate a standalone HTML document for a note.
Uses marked.js for client-side markdown rendering. Uses marked.js for client-side markdown rendering.
Args: Args:
title: The note title (for <title> and display) title: The note title (for <title> and display)
content: Raw markdown content (images should already be base64 embedded) content: Raw markdown content (images should already be base64 embedded)
theme_css: CSS content for theming theme_css: CSS content for theming
is_dark: Whether using a dark theme (for Mermaid/Highlight.js) is_dark: Whether using a dark theme (for Mermaid/Highlight.js)
show_print_button: Whether to show a print button (for preview mode)
Returns: Returns:
Complete HTML document as string Complete HTML document as string
""" """
@ -327,6 +329,24 @@ def generate_export_html(
highlight_theme = 'github-dark' if is_dark else 'github' highlight_theme = 'github-dark' if is_dark else 'github'
mermaid_theme = 'dark' if is_dark else 'default' mermaid_theme = 'dark' if is_dark else 'default'
# Print toolbar HTML (only shown in preview mode)
print_toolbar_html = '''
<div class="print-toolbar">
<button onclick="window.print()" title="Print">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"></path>
</svg>
Print
</button>
<button onclick="window.close()" title="Close">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
Close
</button>
</div>
''' if show_print_button else ''
html = f'''<!DOCTYPE html> html = f'''<!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@ -667,12 +687,56 @@ def generate_export_html(
padding: 0.5in; padding: 0.5in;
max-width: none; max-width: none;
}} }}
.print-toolbar {{
display: none !important;
}}
}}
/* Print toolbar (only shown in preview mode) */
.print-toolbar {{
position: fixed;
top: 1rem;
right: 1rem;
z-index: 1000;
display: flex;
gap: 0.5rem;
background: var(--bg-secondary, #f8f9fa);
padding: 0.5rem;
border-radius: 0.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
border: 1px solid var(--border-primary, #dee2e6);
}}
.print-toolbar button {{
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border-primary, #dee2e6);
border-radius: 0.375rem;
background: var(--bg-primary, #ffffff);
color: var(--text-primary, #333333);
cursor: pointer;
font-size: 0.875rem;
font-family: inherit;
transition: background-color 0.15s, border-color 0.15s;
}}
.print-toolbar button:hover {{
background: var(--bg-tertiary, #e9ecef);
border-color: var(--accent-primary, #0366d6);
}}
.print-toolbar button svg {{
width: 1rem;
height: 1rem;
}} }}
</style> </style>
</head> </head>
<body> <body>
{print_toolbar_html}
<div class="markdown-preview" id="content"></div> <div class="markdown-preview" id="content"></div>
<script> <script>
// Protect LaTeX delimiters \\(...\\) and \\[...\\] from marked.js escaping // Protect LaTeX delimiters \\(...\\) and \\[...\\] from marked.js escaping
marked.use({{ marked.use({{

View File

@ -1230,18 +1230,19 @@ async def remove_note(request: Request, note_path: str):
@api_router.get("/export/{note_path:path}", tags=["Export"]) @api_router.get("/export/{note_path:path}", tags=["Export"])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def export_note_to_html(request: Request, note_path: str, theme: Optional[str] = None): async def export_note_to_html(request: Request, note_path: str, theme: Optional[str] = None, download: bool = True):
""" """
Export a note as a standalone HTML file. Export a note as a standalone HTML file.
The HTML includes all necessary CSS, MathJax, Mermaid, and syntax highlighting The HTML includes all necessary CSS, MathJax, Mermaid, and syntax highlighting
for offline viewing. Images are embedded as base64. for offline viewing. Images are embedded as base64.
Query Parameters: Query Parameters:
theme: Optional theme name (defaults to current theme or 'light') theme: Optional theme name (defaults to current theme or 'light')
download: If true (default), returns as file download. If false, displays in browser with print button.
Returns: Returns:
HTML file download HTML file (download or inline based on download parameter)
""" """
try: try:
notes_dir = Path(config['storage']['notes_dir']) notes_dir = Path(config['storage']['notes_dir'])
@ -1288,23 +1289,28 @@ async def export_note_to_html(request: Request, note_path: str, theme: Optional[
# Get note title # Get note title
title = Path(note_path).stem title = Path(note_path).stem
# Generate HTML # Generate HTML (show print button only when not downloading)
html_content = generate_export_html( html_content = generate_export_html(
title=title, title=title,
content=content_with_links, content=content_with_links,
theme_css=theme_css, theme_css=theme_css,
is_dark=is_dark is_dark=is_dark,
show_print_button=not download
) )
# Return as downloadable file # Return as downloadable file or inline (for print preview)
filename = f"{title}.html" if download:
return Response( filename = f"{title}.html"
content=html_content, return Response(
media_type="text/html", content=html_content,
headers={ media_type="text/html",
"Content-Disposition": f'attachment; filename="{filename}"' headers={
} "Content-Disposition": f'attachment; filename="{filename}"'
) }
)
else:
# Return inline for browser display (print preview)
return HTMLResponse(content=html_content)
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:

View File

@ -450,7 +450,7 @@ Returns the relationship graph between notes with link detection.
### Export Note as HTML ### Export Note as HTML
```http ```http
GET /api/export/{note_path}?theme={theme_name} GET /api/export/{note_path}?theme={theme_name}&download={true|false}
``` ```
Exports a note as a standalone HTML file with all dependencies embedded for offline viewing. Exports a note as a standalone HTML file with all dependencies embedded for offline viewing.
@ -460,9 +460,11 @@ Exports a note as a standalone HTML file with all dependencies embedded for offl
|-----------|------|-------------| |-----------|------|-------------|
| `note_path` | path | Path to the note (e.g., `folder/note.md`) | | `note_path` | path | Path to the note (e.g., `folder/note.md`) |
| `theme` | query (optional) | Theme name for styling (defaults to `light`) | | `theme` | query (optional) | Theme name for styling (defaults to `light`) |
| `download` | query (optional) | If `true` (default), returns as file download. If `false`, displays in browser with Print/Close buttons for print preview |
**Response:** **Response:**
Returns an HTML file download with `Content-Disposition: attachment` header. - `download=true`: Returns an HTML file with `Content-Disposition: attachment` header
- `download=false`: Returns inline HTML for browser display (print preview mode)
**Features:** **Features:**
- Fully self-contained HTML with embedded CSS - Fully self-contained HTML with embedded CSS
@ -473,16 +475,20 @@ Returns an HTML file download with `Content-Disposition: attachment` header.
- Wikilinks converted to decorative spans - Wikilinks converted to decorative spans
- YAML frontmatter stripped - YAML frontmatter stripped
- Responsive design with print support - Responsive design with print support
- Print toolbar with Print/Close buttons (preview mode only)
**Rate Limit:** 30 requests/minute **Rate Limit:** 30 requests/minute
**Example:** **Example:**
```bash ```bash
# Export with default theme # Export with default theme (downloads file)
curl -O http://localhost:8000/api/export/notes/Welcome.md curl -O http://localhost:8000/api/export/notes/Welcome.md
# Export with dark theme # Export with dark theme (downloads file)
curl -O "http://localhost:8000/api/export/docs/API.md?theme=dracula" curl -O "http://localhost:8000/api/export/docs/API.md?theme=dracula"
# Print preview (open in browser)
# http://localhost:8000/api/export/docs/API.md?theme=light&download=false
``` ```
--- ---

View File

@ -5114,6 +5114,22 @@ function noteApp() {
} }
}, },
// Open print preview in new window
printPreview() {
if (!this.currentNote || !this.noteContent) {
alert(this.t('notes.no_content'));
return;
}
// Build API URL with current theme and download=false for inline display
const currentTheme = this.currentTheme || 'light';
const encodedPath = this.currentNote.split('/').map(s => encodeURIComponent(s)).join('/');
const url = `/api/export/${encodedPath}?theme=${encodeURIComponent(currentTheme)}&download=false`;
// Open in new window/tab
window.open(url, '_blank');
},
// Copy current note link to clipboard // Copy current note link to clipboard
async copyNoteLink() { async copyNoteLink() {
if (!this.currentNote) return; if (!this.currentNote) return;

View File

@ -2542,6 +2542,18 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg> </svg>
</button> </button>
<button
@click="printPreview()"
class="p-2 transition"
style="color: var(--text-secondary);"
:title="t('toolbar.print_preview')"
@mouseenter="$el.style.backgroundColor = 'var(--bg-secondary)'"
@mouseleave="$el.style.backgroundColor = 'transparent'"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"></path>
</svg>
</button>
<div style="width: 1px; height: 20px; background-color: var(--border-primary);"></div> <div style="width: 1px; height: 20px; background-color: var(--border-primary);"></div>
<button <button
@click="copyNoteLink()" @click="copyNoteLink()"

View File

@ -105,6 +105,7 @@
"delete_note": "Notiz löschen", "delete_note": "Notiz löschen",
"delete_image": "Bild löschen", "delete_image": "Bild löschen",
"export_html": "Als HTML exportieren", "export_html": "Als HTML exportieren",
"print_preview": "Druckvorschau",
"copy_link": "Link in Zwischenablage kopieren", "copy_link": "Link in Zwischenablage kopieren",
"add_favorite": "Zu Favoriten hinzufügen", "add_favorite": "Zu Favoriten hinzufügen",
"remove_favorite": "Aus Favoriten entfernen" "remove_favorite": "Aus Favoriten entfernen"

View File

@ -104,6 +104,7 @@
"delete_note": "Delete note", "delete_note": "Delete note",
"delete_image": "Delete image", "delete_image": "Delete image",
"export_html": "Export as HTML", "export_html": "Export as HTML",
"print_preview": "Print preview",
"copy_link": "Copy link to clipboard", "copy_link": "Copy link to clipboard",
"add_favorite": "Add to favourites", "add_favorite": "Add to favourites",
"remove_favorite": "Remove from favourites" "remove_favorite": "Remove from favourites"

View File

@ -105,6 +105,7 @@
"delete_note": "Delete note", "delete_note": "Delete note",
"delete_image": "Delete image", "delete_image": "Delete image",
"export_html": "Export as HTML", "export_html": "Export as HTML",
"print_preview": "Print preview",
"copy_link": "Copy link to clipboard", "copy_link": "Copy link to clipboard",
"add_favorite": "Add to favorites", "add_favorite": "Add to favorites",
"remove_favorite": "Remove from favorites" "remove_favorite": "Remove from favorites"

View File

@ -105,6 +105,7 @@
"delete_note": "Eliminar nota", "delete_note": "Eliminar nota",
"delete_image": "Eliminar imagen", "delete_image": "Eliminar imagen",
"export_html": "Exportar como HTML", "export_html": "Exportar como HTML",
"print_preview": "Vista previa de impresión",
"copy_link": "Copiar enlace al portapapeles", "copy_link": "Copiar enlace al portapapeles",
"add_favorite": "Añadir a favoritos", "add_favorite": "Añadir a favoritos",
"remove_favorite": "Quitar de favoritos" "remove_favorite": "Quitar de favoritos"

View File

@ -105,6 +105,7 @@
"delete_note": "Supprimer la note", "delete_note": "Supprimer la note",
"delete_image": "Supprimer l'image", "delete_image": "Supprimer l'image",
"export_html": "Exporter en HTML", "export_html": "Exporter en HTML",
"print_preview": "Aperçu avant impression",
"copy_link": "Copier le lien dans le presse-papiers", "copy_link": "Copier le lien dans le presse-papiers",
"add_favorite": "Ajouter aux favoris", "add_favorite": "Ajouter aux favoris",
"remove_favorite": "Retirer des favoris" "remove_favorite": "Retirer des favoris"

View File

@ -105,6 +105,7 @@
"delete_note": "Jegyzet törlése", "delete_note": "Jegyzet törlése",
"delete_image": "Kép törlése", "delete_image": "Kép törlése",
"export_html": "Exportálás HTML-ként", "export_html": "Exportálás HTML-ként",
"print_preview": "Nyomtatási előnézet",
"copy_link": "Link másolása", "copy_link": "Link másolása",
"add_favorite": "Hozzáadás a kedvencekhez", "add_favorite": "Hozzáadás a kedvencekhez",
"remove_favorite": "Eltávolítás a kedvencek közül" "remove_favorite": "Eltávolítás a kedvencek közül"

View File

@ -104,6 +104,7 @@
"delete_note": "Elimina nota", "delete_note": "Elimina nota",
"delete_image": "Elimina immagine", "delete_image": "Elimina immagine",
"export_html": "Esporta come HTML", "export_html": "Esporta come HTML",
"print_preview": "Anteprima di stampa",
"copy_link": "Copia link negli appunti", "copy_link": "Copia link negli appunti",
"add_favorite": "Aggiungi ai preferiti", "add_favorite": "Aggiungi ai preferiti",
"remove_favorite": "Rimuovi dai preferiti" "remove_favorite": "Rimuovi dai preferiti"

View File

@ -104,6 +104,7 @@
"delete_note": "ノートを削除", "delete_note": "ノートを削除",
"delete_image": "画像を削除", "delete_image": "画像を削除",
"export_html": "HTMLとしてエクスポート", "export_html": "HTMLとしてエクスポート",
"print_preview": "印刷プレビュー",
"copy_link": "リンクをクリップボードにコピー", "copy_link": "リンクをクリップボードにコピー",
"add_favorite": "お気に入りに追加", "add_favorite": "お気に入りに追加",
"remove_favorite": "お気に入りから削除" "remove_favorite": "お気に入りから削除"

View File

@ -104,6 +104,7 @@
"delete_note": "Удалить заметку", "delete_note": "Удалить заметку",
"delete_image": "Удалить изображение", "delete_image": "Удалить изображение",
"export_html": "Экспорт в HTML", "export_html": "Экспорт в HTML",
"print_preview": "Предварительный просмотр печати",
"copy_link": "Копировать ссылку", "copy_link": "Копировать ссылку",
"add_favorite": "Добавить в избранное", "add_favorite": "Добавить в избранное",
"remove_favorite": "Удалить из избранного" "remove_favorite": "Удалить из избранного"

View File

@ -104,6 +104,7 @@
"delete_note": "Izbriši zapis", "delete_note": "Izbriši zapis",
"delete_image": "Izbriši sliko", "delete_image": "Izbriši sliko",
"export_html": "Izvozi kot HTML", "export_html": "Izvozi kot HTML",
"print_preview": "Predogled tiskanja",
"copy_link": "Kopiraj povezavo v odložišče", "copy_link": "Kopiraj povezavo v odložišče",
"add_favorite": "Dodaj med priljubljene", "add_favorite": "Dodaj med priljubljene",
"remove_favorite": "Odstrani iz priljubljenih" "remove_favorite": "Odstrani iz priljubljenih"

View File

@ -104,6 +104,7 @@
"delete_note": "删除笔记", "delete_note": "删除笔记",
"delete_image": "删除图片", "delete_image": "删除图片",
"export_html": "导出为 HTML", "export_html": "导出为 HTML",
"print_preview": "打印预览",
"copy_link": "复制链接到剪贴板", "copy_link": "复制链接到剪贴板",
"add_favorite": "添加到收藏夹", "add_favorite": "添加到收藏夹",
"remove_favorite": "从收藏夹移除" "remove_favorite": "从收藏夹移除"