Merge pull request #198 from gamosoft/features/suggestions
Features/suggestions
This commit is contained in:
commit
7590e6e712
|
|
@ -62,7 +62,7 @@ NoteDiscovery is a **lightweight, self-hosted note-taking application** that put
|
||||||
- 📱 **Responsive** - Works on desktop, tablet, and mobile
|
- 📱 **Responsive** - Works on desktop, tablet, and mobile
|
||||||
- 📂 **Simple Storage** - Plain markdown files in folders
|
- 📂 **Simple Storage** - Plain markdown files in folders
|
||||||
- 🧮 **Math Support** - LaTeX/MathJax for beautiful equations
|
- 🧮 **Math Support** - LaTeX/MathJax for beautiful equations
|
||||||
- 📄 **HTML Export** - Share notes as standalone HTML files
|
- 📄 **HTML Export & Print** - Export notes as standalone HTML or print
|
||||||
- 🕸️ **Graph View** - Interactive visualization of connected notes
|
- 🕸️ **Graph View** - Interactive visualization of connected notes
|
||||||
- ⭐ **Favorites** - Star your most-used notes for instant access
|
- ⭐ **Favorites** - Star your most-used notes for instant access
|
||||||
- 📑 **Outline Panel** - Navigate headings with click-to-jump TOC
|
- 📑 **Outline Panel** - Navigate headings with click-to-jump TOC
|
||||||
|
|
|
||||||
|
|
@ -300,7 +300,8 @@ 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.
|
||||||
|
|
@ -311,6 +312,7 @@ def generate_export_html(
|
||||||
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>
|
||||||
|
|
@ -348,8 +368,8 @@ def generate_export_html(
|
||||||
<script>
|
<script>
|
||||||
MathJax = {{
|
MathJax = {{
|
||||||
tex: {{
|
tex: {{
|
||||||
inlineMath: [['$', '$']],
|
inlineMath: [['\\\\(', '\\\\)'], ['$', '$']],
|
||||||
displayMath: [['$$', '$$']],
|
displayMath: [['\\\\[', '\\\\]'], ['$$', '$$']],
|
||||||
processEscapes: true,
|
processEscapes: true,
|
||||||
processEnvironments: true
|
processEnvironments: true
|
||||||
}},
|
}},
|
||||||
|
|
@ -667,13 +687,72 @@ 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
|
||||||
|
marked.use({{
|
||||||
|
extensions: [{{
|
||||||
|
name: 'protectLatexMath',
|
||||||
|
level: 'inline',
|
||||||
|
start(src) {{ return src.match(/\\\\[\\(\\[]/)?.index; }},
|
||||||
|
tokenizer(src) {{
|
||||||
|
const match = src.match(/^(\\\\[\\(\\[])([\\s\\S]*?)(\\\\[\\)\\]])/);
|
||||||
|
if (match) {{
|
||||||
|
return {{ type: 'html', raw: match[0], text: match[0] }};
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}]
|
||||||
|
}});
|
||||||
|
|
||||||
// Configure marked
|
// Configure marked
|
||||||
marked.setOptions({{
|
marked.setOptions({{
|
||||||
gfm: true,
|
gfm: true,
|
||||||
|
|
@ -691,6 +770,11 @@ def generate_export_html(
|
||||||
const safeHtml = DOMPurify.sanitize(rawHtml);
|
const safeHtml = DOMPurify.sanitize(rawHtml);
|
||||||
document.getElementById('content').innerHTML = safeHtml;
|
document.getElementById('content').innerHTML = safeHtml;
|
||||||
|
|
||||||
|
// Typeset math after content is inserted
|
||||||
|
if (typeof MathJax !== 'undefined' && MathJax.typeset) {{
|
||||||
|
MathJax.typeset();
|
||||||
|
}}
|
||||||
|
|
||||||
// Add copy buttons to code blocks
|
// Add copy buttons to code blocks
|
||||||
document.querySelectorAll('.markdown-preview pre').forEach(pre => {{
|
document.querySelectorAll('.markdown-preview pre').forEach(pre => {{
|
||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
|
|
|
||||||
|
|
@ -1228,6 +1228,95 @@ async def remove_note(request: Request, note_path: str):
|
||||||
raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to delete note"))
|
raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to delete note"))
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.get("/export/{note_path:path}", tags=["Export"])
|
||||||
|
@limiter.limit("30/minute")
|
||||||
|
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.
|
||||||
|
|
||||||
|
The HTML includes all necessary CSS, MathJax, Mermaid, and syntax highlighting
|
||||||
|
for offline viewing. Images are embedded as base64.
|
||||||
|
|
||||||
|
Query Parameters:
|
||||||
|
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:
|
||||||
|
HTML file (download or inline based on download parameter)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
notes_dir = Path(config['storage']['notes_dir'])
|
||||||
|
|
||||||
|
# Read note content
|
||||||
|
content = get_note_content(str(notes_dir), note_path)
|
||||||
|
if content is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Note not found")
|
||||||
|
|
||||||
|
# Run on_note_load hook (can transform content, e.g., decrypt)
|
||||||
|
transformed_content = plugin_manager.run_hook('on_note_load', note_path=note_path, content=content)
|
||||||
|
if transformed_content is not None:
|
||||||
|
content = transformed_content
|
||||||
|
|
||||||
|
# Strip YAML frontmatter (like the preview does)
|
||||||
|
content = strip_frontmatter(content)
|
||||||
|
|
||||||
|
# Get note folder for resolving relative image paths
|
||||||
|
note_file_path = notes_dir / note_path
|
||||||
|
note_folder = note_file_path.parent
|
||||||
|
|
||||||
|
# Embed images as base64
|
||||||
|
content_with_images = embed_images_as_base64(content, note_folder, notes_dir)
|
||||||
|
|
||||||
|
# Convert wikilinks to decorative HTML links
|
||||||
|
content_with_links = convert_wikilinks_to_html(content_with_images)
|
||||||
|
|
||||||
|
# Get theme CSS
|
||||||
|
themes_dir = Path(__file__).parent.parent / "themes"
|
||||||
|
theme_name = theme or 'light'
|
||||||
|
theme_css = get_theme_css(str(themes_dir), theme_name)
|
||||||
|
if not theme_css:
|
||||||
|
theme_css = get_theme_css(str(themes_dir), "light")
|
||||||
|
theme_name = "light"
|
||||||
|
|
||||||
|
# Strip data-theme selector
|
||||||
|
theme_css = theme_css.replace(f':root[data-theme="{theme_name}"]', ':root')
|
||||||
|
theme_css = theme_css.replace(':root[data-theme="light"]', ':root')
|
||||||
|
theme_css = theme_css.replace(':root[data-theme="dark"]', ':root')
|
||||||
|
|
||||||
|
# Determine if dark theme
|
||||||
|
is_dark = 'dark' in theme_name.lower() or theme_name in ['dracula', 'nord', 'monokai', 'cobalt2', 'gruvbox-dark']
|
||||||
|
|
||||||
|
# Get note title
|
||||||
|
title = Path(note_path).stem
|
||||||
|
|
||||||
|
# Generate HTML (show print button only when not downloading)
|
||||||
|
html_content = generate_export_html(
|
||||||
|
title=title,
|
||||||
|
content=content_with_links,
|
||||||
|
theme_css=theme_css,
|
||||||
|
is_dark=is_dark,
|
||||||
|
show_print_button=not download
|
||||||
|
)
|
||||||
|
|
||||||
|
# Return as downloadable file or inline (for print preview)
|
||||||
|
if download:
|
||||||
|
filename = f"{title}.html"
|
||||||
|
return Response(
|
||||||
|
content=html_content,
|
||||||
|
media_type="text/html",
|
||||||
|
headers={
|
||||||
|
"Content-Disposition": f'attachment; filename="{filename}"'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Return inline for browser display (print preview)
|
||||||
|
return HTMLResponse(content=html_content)
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to export note"))
|
||||||
|
|
||||||
|
|
||||||
@api_router.get("/search", tags=["Search"])
|
@api_router.get("/search", tags=["Search"])
|
||||||
async def search(
|
async def search(
|
||||||
q: str,
|
q: str,
|
||||||
|
|
|
||||||
|
|
@ -444,6 +444,55 @@ Returns the relationship graph between notes with link detection.
|
||||||
- **Markdown links** - `[text](note.md)` standard internal links
|
- **Markdown links** - `[text](note.md)` standard internal links
|
||||||
- **Edge types** - `"wikilink"` or `"markdown"` to distinguish link source
|
- **Edge types** - `"wikilink"` or `"markdown"` to distinguish link source
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📤 Export
|
||||||
|
|
||||||
|
### Export Note as HTML
|
||||||
|
```http
|
||||||
|
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.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|------|-------------|
|
||||||
|
| `note_path` | path | Path to the note (e.g., `folder/note.md`) |
|
||||||
|
| `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:**
|
||||||
|
- `download=true`: Returns an HTML file with `Content-Disposition: attachment` header
|
||||||
|
- `download=false`: Returns inline HTML for browser display (print preview mode)
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Fully self-contained HTML with embedded CSS
|
||||||
|
- Images converted to base64 data URLs
|
||||||
|
- MathJax for LaTeX math rendering (supports `$...$`, `$$...$$`, `\(...\)`, `\[...\]`)
|
||||||
|
- Mermaid.js for diagram rendering
|
||||||
|
- Highlight.js for syntax highlighting
|
||||||
|
- Wikilinks converted to decorative spans
|
||||||
|
- YAML frontmatter stripped
|
||||||
|
- Responsive design with print support
|
||||||
|
- Print toolbar with Print/Close buttons (preview mode only)
|
||||||
|
|
||||||
|
**Rate Limit:** 30 requests/minute
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
# Export with default theme (downloads file)
|
||||||
|
curl -O http://localhost:8000/api/export/notes/Welcome.md
|
||||||
|
|
||||||
|
# Export with dark theme (downloads file)
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## ⚙️ System
|
## ⚙️ System
|
||||||
|
|
||||||
### Get Config
|
### Get Config
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@
|
||||||
- **Copy code blocks** - One-click copy button on hover
|
- **Copy code blocks** - One-click copy button on hover
|
||||||
- **LaTeX/Math rendering** - Beautiful mathematical equations with MathJax (see [MATHJAX.md](MATHJAX.md))
|
- **LaTeX/Math rendering** - Beautiful mathematical equations with MathJax (see [MATHJAX.md](MATHJAX.md))
|
||||||
- **Mermaid diagrams** - Create flowcharts, sequence diagrams, and more (see [MERMAID.md](MERMAID.md))
|
- **Mermaid diagrams** - Create flowcharts, sequence diagrams, and more (see [MERMAID.md](MERMAID.md))
|
||||||
- **HTML Export** - Export notes as standalone HTML files with embedded images
|
|
||||||
- **Public Sharing** - Share notes via token-based URLs with optional QR code for mobile (see [SHARING.md](SHARING.md))
|
- **Public Sharing** - Share notes via token-based URLs with optional QR code for mobile (see [SHARING.md](SHARING.md))
|
||||||
|
|
||||||
### Media Support
|
### Media Support
|
||||||
|
|
@ -32,6 +31,14 @@
|
||||||
- **Rename anything** - Files and folders, instantly
|
- **Rename anything** - Files and folders, instantly
|
||||||
- **Visual tree view** - Expandable/collapsible navigation
|
- **Visual tree view** - Expandable/collapsible navigation
|
||||||
- **Hide system folders** - Toggle to hide `_attachments`, `_templates` and other underscore-prefixed folders from sidebar
|
- **Hide system folders** - Toggle to hide `_attachments`, `_templates` and other underscore-prefixed folders from sidebar
|
||||||
|
- **Tab inserts tab** - Toggle to make Tab key insert a tab character in the editor instead of changing focus
|
||||||
|
|
||||||
|
### Export & Print
|
||||||
|
- **HTML Export** - Download notes as standalone HTML files with all styling, images, diagrams, and math embedded
|
||||||
|
- **Print Preview** - Open note in new tab with Print/Close buttons for easy printing
|
||||||
|
- **Self-contained** - Exported files work offline with no dependencies
|
||||||
|
- **Theme-aware** - Export uses your current theme for consistent appearance
|
||||||
|
- **Full rendering** - MathJax equations, Mermaid diagrams, and syntax highlighting included
|
||||||
|
|
||||||
## 🔗 Linking & Discovery
|
## 🔗 Linking & Discovery
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,18 @@ NoteDiscovery supports **LaTeX mathematical notation** powered by MathJax 3. Wri
|
||||||
|
|
||||||
## Syntax Overview
|
## Syntax Overview
|
||||||
|
|
||||||
### Inline Math (within text)
|
| Delimiter | Type | Behavior |
|
||||||
Use `$...$` for inline equations:
|
|-----------|------|----------|
|
||||||
|
| `$...$` | Inline | Flows with text, not centered |
|
||||||
|
| `\(...\)` | Inline | Same as `$...$` (LaTeX standard) |
|
||||||
|
| `$$...$$` | Display | Own paragraph, centered, larger |
|
||||||
|
| `\[...\]` | Display | Same as `$$...$$` (LaTeX standard) |
|
||||||
|
|
||||||
- `$E = mc^2$` renders as: $E = mc^2$
|
### Inline Math (within text)
|
||||||
- `$x^2 + y^2 = r^2$` renders as: $x^2 + y^2 = r^2$
|
Inline math flows with your text: `$E = mc^2$` renders as $E = mc^2$
|
||||||
|
|
||||||
### Display Math (centered, on its own line)
|
### Display Math (centered, on its own line)
|
||||||
Use `$$...$$` for display equations:
|
Display math gets its own centered paragraph:
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
$$
|
$$
|
||||||
|
|
@ -19,6 +23,13 @@ x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}
|
||||||
$$
|
$$
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Or using LaTeX-style delimiters:
|
||||||
|
```markdown
|
||||||
|
\[
|
||||||
|
x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}
|
||||||
|
\]
|
||||||
|
```
|
||||||
|
|
||||||
$$
|
$$
|
||||||
x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}
|
x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}
|
||||||
$$
|
$$
|
||||||
|
|
|
||||||
344
frontend/app.js
344
frontend/app.js
|
|
@ -20,6 +20,7 @@ const LOCAL_SETTINGS = {
|
||||||
favoritesExpanded: { key: 'favoritesExpanded', type: 'boolean', default: true },
|
favoritesExpanded: { key: 'favoritesExpanded', type: 'boolean', default: true },
|
||||||
tagsExpanded: { key: 'tagsExpanded', type: 'boolean', default: false },
|
tagsExpanded: { key: 'tagsExpanded', type: 'boolean', default: false },
|
||||||
hideUnderscoreFolders: { key: 'hideUnderscoreFolders', type: 'boolean', default: false },
|
hideUnderscoreFolders: { key: 'hideUnderscoreFolders', type: 'boolean', default: false },
|
||||||
|
tabInsertsTab: { key: 'tabInsertsTab', type: 'boolean', default: false },
|
||||||
// Number settings with validation
|
// Number settings with validation
|
||||||
sidebarWidth: { key: 'sidebarWidth', type: 'number', default: CONFIG.DEFAULT_SIDEBAR_WIDTH, min: 200, max: 600 },
|
sidebarWidth: { key: 'sidebarWidth', type: 'number', default: CONFIG.DEFAULT_SIDEBAR_WIDTH, min: 200, max: 600 },
|
||||||
editorWidth: { key: 'editorWidth', type: 'number', default: 50, min: 20, max: 80 },
|
editorWidth: { key: 'editorWidth', type: 'number', default: 50, min: 20, max: 80 },
|
||||||
|
|
@ -215,6 +216,9 @@ function noteApp() {
|
||||||
// Read synchronously to prevent flash on initial render
|
// Read synchronously to prevent flash on initial render
|
||||||
hideUnderscoreFolders: localStorage.getItem('hideUnderscoreFolders') === 'true',
|
hideUnderscoreFolders: localStorage.getItem('hideUnderscoreFolders') === 'true',
|
||||||
|
|
||||||
|
// Tab key inserts tab character instead of changing focus
|
||||||
|
tabInsertsTab: localStorage.getItem('tabInsertsTab') === 'true',
|
||||||
|
|
||||||
// Icon rail / panel state
|
// Icon rail / panel state
|
||||||
activePanel: 'files', // 'files', 'search', 'tags', 'settings'
|
activePanel: 'files', // 'files', 'search', 'tags', 'settings'
|
||||||
|
|
||||||
|
|
@ -827,6 +831,27 @@ function noteApp() {
|
||||||
localStorage.setItem('hideUnderscoreFolders', this.hideUnderscoreFolders);
|
localStorage.setItem('hideUnderscoreFolders', this.hideUnderscoreFolders);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Tab inserts tab toggle (Tab key inserts tab character instead of changing focus)
|
||||||
|
toggleTabInsertsTab() {
|
||||||
|
this.tabInsertsTab = !this.tabInsertsTab;
|
||||||
|
localStorage.setItem('tabInsertsTab', this.tabInsertsTab);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Handle Tab key in editor (inserts tab if setting enabled)
|
||||||
|
handleTabKey(event) {
|
||||||
|
if (!this.tabInsertsTab) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
const textarea = event.target;
|
||||||
|
const start = textarea.selectionStart;
|
||||||
|
const end = textarea.selectionEnd;
|
||||||
|
this.noteContent = this.noteContent.substring(0, start) + '\t' + this.noteContent.substring(end);
|
||||||
|
this.$nextTick(() => {
|
||||||
|
textarea.selectionStart = textarea.selectionEnd = start + 1;
|
||||||
|
});
|
||||||
|
this.autoSave();
|
||||||
|
},
|
||||||
|
|
||||||
// Update syntax highlight overlay (debounced, called on input)
|
// Update syntax highlight overlay (debounced, called on input)
|
||||||
updateSyntaxHighlight() {
|
updateSyntaxHighlight() {
|
||||||
if (!this.syntaxHighlightEnabled) return;
|
if (!this.syntaxHighlightEnabled) return;
|
||||||
|
|
@ -4139,6 +4164,26 @@ function noteApp() {
|
||||||
return codeBlocks[parseInt(index)];
|
return codeBlocks[parseInt(index)];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Protect LaTeX \(...\) and \[...\] delimiters from marked.js escaping
|
||||||
|
marked.use({
|
||||||
|
extensions: [{
|
||||||
|
name: 'protectLatexMath',
|
||||||
|
level: 'inline',
|
||||||
|
start(src) { return src.match(/\\[\(\[]/)?.index; },
|
||||||
|
tokenizer(src) {
|
||||||
|
// Match \(...\) or \[...\]
|
||||||
|
const match = src.match(/^(\\[\(\[])([\s\S]*?)(\\[\)\]])/);
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
type: 'html',
|
||||||
|
raw: match[0],
|
||||||
|
text: match[0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
// Configure marked with syntax highlighting
|
// Configure marked with syntax highlighting
|
||||||
marked.setOptions({
|
marked.setOptions({
|
||||||
breaks: true,
|
breaks: true,
|
||||||
|
|
@ -5020,7 +5065,7 @@ function noteApp() {
|
||||||
}, CONFIG.SCROLL_SYNC_DELAY);
|
}, CONFIG.SCROLL_SYNC_DELAY);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Export current note as HTML
|
// Export current note as HTML via backend API
|
||||||
async exportToHTML() {
|
async exportToHTML() {
|
||||||
if (!this.currentNote || !this.noteContent) {
|
if (!this.currentNote || !this.noteContent) {
|
||||||
alert(this.t('notes.no_content'));
|
alert(this.t('notes.no_content'));
|
||||||
|
|
@ -5028,278 +5073,39 @@ function noteApp() {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the note name without extension
|
// Build API URL with current theme
|
||||||
const noteName = this.currentNoteName || 'note';
|
|
||||||
|
|
||||||
// Get current rendered HTML (this already has markdown converted and will have LaTeX delimiters)
|
|
||||||
let renderedHTML = this.renderedMarkdown;
|
|
||||||
|
|
||||||
// Convert non-image media (audio, video, PDF) to placeholders first
|
|
||||||
// These shouldn't be embedded as base64 (too large)
|
|
||||||
// Use CSS variables with fallbacks for theme-aware styling (matches backend export.py)
|
|
||||||
const mediaPlaceholder = (type, name) => {
|
|
||||||
const icons = { audio: '🎵', video: '🎬', document: '📄' };
|
|
||||||
const labels = { audio: 'Audio file', video: 'Video file', document: 'PDF document' };
|
|
||||||
const icon = icons[type] || '📎';
|
|
||||||
const label = labels[type] || 'Media file';
|
|
||||||
return `<div style="margin:1.5rem 0;padding:1.5rem;background:linear-gradient(135deg,var(--bg-tertiary,#f8f9fa) 0%,var(--bg-secondary,#e9ecef) 100%);border:1px solid var(--border-primary,#dee2e6);border-radius:0.5rem;display:flex;align-items:center;gap:1rem;">
|
|
||||||
<span style="font-size:2rem;">${icon}</span>
|
|
||||||
<div>
|
|
||||||
<div style="font-weight:600;color:var(--text-primary,#212529);">${name}</div>
|
|
||||||
<div style="font-size:0.875rem;color:var(--text-secondary,#6c757d);">${label} — not available in exported view</div>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Replace audio embeds with placeholders
|
|
||||||
renderedHTML = renderedHTML.replace(
|
|
||||||
/<div class="media-embed media-audio">.*?<audio[^>]*src="[^"]*\/([^"\/]+)"[^>]*>.*?<\/div>/gs,
|
|
||||||
(match, filename) => mediaPlaceholder('audio', decodeURIComponent(filename).replace(/\.[^.]+$/, ''))
|
|
||||||
);
|
|
||||||
|
|
||||||
// Replace video embeds with placeholders
|
|
||||||
renderedHTML = renderedHTML.replace(
|
|
||||||
/<div class="media-embed media-video">.*?<video[^>]*src="[^"]*\/([^"\/]+)"[^>]*>.*?<\/div>/gs,
|
|
||||||
(match, filename) => mediaPlaceholder('video', decodeURIComponent(filename).replace(/\.[^.]+$/, ''))
|
|
||||||
);
|
|
||||||
|
|
||||||
// Replace PDF embeds with placeholders
|
|
||||||
renderedHTML = renderedHTML.replace(
|
|
||||||
/<div class="media-embed media-pdf">.*?<iframe[^>]*src="[^"]*\/([^"\/]+)"[^>]*>.*?<\/div>/gs,
|
|
||||||
(match, filename) => mediaPlaceholder('document', decodeURIComponent(filename).replace(/\.[^.]+$/, ''))
|
|
||||||
);
|
|
||||||
|
|
||||||
// Embed local images as base64 for fully self-contained HTML
|
|
||||||
// Handle both /api/media/ and legacy /api/images/ paths
|
|
||||||
const imgRegex = /src="\/api\/(?:media|images)\/([^"]+)"/g;
|
|
||||||
const imgMatches = [...renderedHTML.matchAll(imgRegex)];
|
|
||||||
|
|
||||||
for (const match of imgMatches) {
|
|
||||||
const encodedPath = match[1];
|
|
||||||
// Skip non-image files (already handled above)
|
|
||||||
const ext = encodedPath.split('.').pop().toLowerCase();
|
|
||||||
if (!['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Fetch the image
|
|
||||||
const imgResponse = await fetch(`/api/media/${encodedPath}`);
|
|
||||||
if (imgResponse.ok) {
|
|
||||||
const blob = await imgResponse.blob();
|
|
||||||
// Convert to base64 data URL
|
|
||||||
const base64 = await new Promise((resolve) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onloadend = () => resolve(reader.result);
|
|
||||||
reader.readAsDataURL(blob);
|
|
||||||
});
|
|
||||||
// Replace the src with base64 data URL
|
|
||||||
renderedHTML = renderedHTML.replace(match[0], `src="${base64}"`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(`Failed to embed image: ${encodedPath}`, e);
|
|
||||||
// Fall back to relative path
|
|
||||||
const decodedPath = decodeURIComponent(encodedPath);
|
|
||||||
renderedHTML = renderedHTML.replace(match[0], `src="${decodedPath}"`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current theme CSS
|
|
||||||
const currentTheme = this.currentTheme || 'light';
|
const currentTheme = this.currentTheme || 'light';
|
||||||
const themeResponse = await fetch(`/api/themes/${currentTheme}`);
|
const encodedPath = this.currentNote.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||||
const themeText = await themeResponse.text();
|
const url = `/api/export/${encodedPath}?theme=${encodeURIComponent(currentTheme)}`;
|
||||||
|
|
||||||
// Check if response is JSON or plain CSS
|
// Fetch the exported HTML from backend
|
||||||
let themeCss;
|
const response = await fetch(url);
|
||||||
try {
|
if (!response.ok) {
|
||||||
const themeJson = JSON.parse(themeText);
|
const error = await response.json().catch(() => ({ detail: 'Export failed' }));
|
||||||
// If it's JSON, extract the css field
|
throw new Error(error.detail || 'Export failed');
|
||||||
themeCss = themeJson.css || themeText;
|
|
||||||
} catch (e) {
|
|
||||||
// If it's not JSON, use it as-is
|
|
||||||
themeCss = themeText;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Theme CSS uses :root[data-theme="..."] selector, but we need plain :root for export
|
// Get filename from Content-Disposition header or use note name
|
||||||
// Strip the data-theme attribute selector so variables apply globally
|
let filename = (this.currentNoteName || 'note') + '.html';
|
||||||
themeCss = themeCss.replace(/:root\[data-theme="[^"]+"\]/g, ':root');
|
const contentDisposition = response.headers.get('Content-Disposition');
|
||||||
|
if (contentDisposition) {
|
||||||
// Get highlight.js theme URL from current page
|
const match = contentDisposition.match(/filename="([^"]+)"/);
|
||||||
const highlightLinkElement = document.getElementById('highlight-theme');
|
if (match) {
|
||||||
if (!highlightLinkElement || !highlightLinkElement.href) {
|
filename = match[1];
|
||||||
console.warn('Could not detect highlight.js theme, export may not match preview exactly');
|
|
||||||
}
|
|
||||||
const highlightTheme = highlightLinkElement ? highlightLinkElement.href : '';
|
|
||||||
|
|
||||||
// Extract all markdown preview styles from current page
|
|
||||||
let markdownStyles = '';
|
|
||||||
const styleSheets = Array.from(document.styleSheets);
|
|
||||||
|
|
||||||
for (const sheet of styleSheets) {
|
|
||||||
try {
|
|
||||||
// Skip external stylesheets (CDN resources) to avoid CORS errors
|
|
||||||
// We link them directly in the exported HTML anyway
|
|
||||||
if (sheet.href && (sheet.href.startsWith('http://') || sheet.href.startsWith('https://'))) {
|
|
||||||
const currentOrigin = window.location.origin;
|
|
||||||
const sheetURL = new URL(sheet.href);
|
|
||||||
if (sheetURL.origin !== currentOrigin) {
|
|
||||||
// Skip cross-origin stylesheets (they're linked directly in export)
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const rules = Array.from(sheet.cssRules || []);
|
|
||||||
for (const rule of rules) {
|
|
||||||
const cssText = rule.cssText;
|
|
||||||
// Include rules that target markdown-preview, mjx-container, or mermaid-rendered
|
|
||||||
if (cssText.includes('.markdown-preview') ||
|
|
||||||
cssText.includes('mjx-container') ||
|
|
||||||
cssText.includes('.MathJax') ||
|
|
||||||
cssText.includes('.mermaid-rendered')) {
|
|
||||||
markdownStyles += cssText + '\n';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Gracefully skip stylesheets that can't be accessed
|
|
||||||
// (This should rarely happen now that we skip external stylesheets)
|
|
||||||
console.debug('Skipping stylesheet:', sheet.href);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create standalone HTML document with MathJax
|
// Download as blob
|
||||||
const htmlDocument = `<!DOCTYPE html>
|
const blob = await response.blob();
|
||||||
<html lang="en">
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>${noteName}</title>
|
|
||||||
|
|
||||||
<!-- Highlight.js for code syntax highlighting (v11.9.0) -->
|
|
||||||
${highlightTheme ? `<link rel="stylesheet" href="${highlightTheme}">` : '<!-- No highlight.js theme detected -->'}
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
||||||
|
|
||||||
<!-- MathJax for LaTeX math rendering (v3.2.2) -->
|
|
||||||
<script>
|
|
||||||
MathJax = {
|
|
||||||
tex: {
|
|
||||||
inlineMath: [['$', '$']],
|
|
||||||
displayMath: [['$$', '$$']],
|
|
||||||
processEscapes: true,
|
|
||||||
processEnvironments: true
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code']
|
|
||||||
},
|
|
||||||
startup: {
|
|
||||||
pageReady: () => {
|
|
||||||
return MathJax.startup.defaultPageReady().then(() => {
|
|
||||||
// Highlight code blocks after MathJax is done (exclude diagram renderers)
|
|
||||||
document.querySelectorAll('pre code:not(.language-mermaid)').forEach((block) => {
|
|
||||||
hljs.highlightElement(block);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/tex-mml-chtml.js"></script>
|
|
||||||
|
|
||||||
<!-- Mermaid.js for diagrams -->
|
|
||||||
<script type="module">
|
|
||||||
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11.12.2/dist/mermaid.esm.min.mjs';
|
|
||||||
const isDark = ${this.getThemeType() === 'dark'};
|
|
||||||
mermaid.initialize({
|
|
||||||
startOnLoad: false,
|
|
||||||
theme: isDark ? 'dark' : 'default',
|
|
||||||
securityLevel: 'strict',
|
|
||||||
fontFamily: 'inherit',
|
|
||||||
flowchart: { useMaxWidth: true },
|
|
||||||
sequence: { useMaxWidth: true },
|
|
||||||
gantt: { useMaxWidth: true },
|
|
||||||
state: { useMaxWidth: true },
|
|
||||||
er: { useMaxWidth: true },
|
|
||||||
pie: { useMaxWidth: true },
|
|
||||||
mindmap: { useMaxWidth: true },
|
|
||||||
gitGraph: { useMaxWidth: true }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Render any Mermaid code blocks
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
|
||||||
const mermaidBlocks = document.querySelectorAll('pre code.language-mermaid');
|
|
||||||
for (let i = 0; i < mermaidBlocks.length; i++) {
|
|
||||||
const block = mermaidBlocks[i];
|
|
||||||
const pre = block.parentElement;
|
|
||||||
try {
|
|
||||||
const code = block.textContent;
|
|
||||||
const id = 'mermaid-diagram-' + i;
|
|
||||||
const { svg } = await mermaid.render(id, code);
|
|
||||||
const container = document.createElement('div');
|
|
||||||
container.className = 'mermaid-rendered';
|
|
||||||
container.style.cssText = 'background-color: transparent; padding: 20px; text-align: center; overflow-x: auto;';
|
|
||||||
container.innerHTML = svg;
|
|
||||||
pre.parentElement.replaceChild(container, pre);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Mermaid rendering error:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Theme CSS */
|
|
||||||
${themeCss}
|
|
||||||
|
|
||||||
/* Base styles */
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
||||||
margin: 0;
|
|
||||||
padding: 2rem;
|
|
||||||
max-width: 900px;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
background-color: var(--bg-primary);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Markdown preview styles extracted from current page */
|
|
||||||
${markdownStyles}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
body {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
body {
|
|
||||||
padding: 0.5in;
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="markdown-preview">
|
|
||||||
${renderedHTML}
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>`;
|
|
||||||
|
|
||||||
// Create blob and download
|
|
||||||
const blob = new Blob([htmlDocument], { type: 'text/html;charset=utf-8' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = blobUrl;
|
||||||
a.download = `${noteName}.html`;
|
a.download = filename;
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(blobUrl);
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -5308,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;
|
||||||
|
|
|
||||||
|
|
@ -70,8 +70,8 @@
|
||||||
<script>
|
<script>
|
||||||
MathJax = {
|
MathJax = {
|
||||||
tex: {
|
tex: {
|
||||||
inlineMath: [['$', '$']],
|
inlineMath: [['\\(', '\\)'], ['$', '$']],
|
||||||
displayMath: [['$$', '$$']],
|
displayMath: [['\\[', '\\]'], ['$$', '$$']],
|
||||||
processEscapes: true,
|
processEscapes: true,
|
||||||
processEnvironments: true
|
processEnvironments: true
|
||||||
},
|
},
|
||||||
|
|
@ -2038,6 +2038,24 @@
|
||||||
<p class="text-xs mt-1" style="color: var(--text-tertiary);" x-text="t('settings.hide_underscore_folders_desc')"></p>
|
<p class="text-xs mt-1" style="color: var(--text-tertiary);" x-text="t('settings.hide_underscore_folders_desc')"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab inserts tab -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="flex items-center justify-between cursor-pointer">
|
||||||
|
<span class="text-xs font-medium" style="color: var(--text-secondary);" x-text="t('settings.tab_inserts_tab')"></span>
|
||||||
|
<div
|
||||||
|
@click="toggleTabInsertsTab()"
|
||||||
|
class="relative w-10 h-5 rounded-full transition-colors cursor-pointer"
|
||||||
|
:style="tabInsertsTab ? 'background-color: var(--accent-primary)' : 'background-color: var(--bg-tertiary)'"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute top-0.5 left-0.5 w-4 h-4 rounded-full transition-transform"
|
||||||
|
:style="'background-color: var(--bg-primary);' + (tabInsertsTab ? ' transform: translateX(20px);' : '')"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<p class="text-xs mt-1" style="color: var(--text-tertiary);" x-text="t('settings.tab_inserts_tab_desc')"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Logout (if auth enabled) - uses authEnabled from main app state -->
|
<!-- Logout (if auth enabled) - uses authEnabled from main app state -->
|
||||||
<div x-show="authEnabled" class="mb-4">
|
<div x-show="authEnabled" class="mb-4">
|
||||||
<label class="block text-xs font-medium mb-2" style="color: var(--text-secondary);" x-text="t('settings.account')"></label>
|
<label class="block text-xs font-medium mb-2" style="color: var(--text-secondary);" x-text="t('settings.account')"></label>
|
||||||
|
|
@ -2524,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()"
|
||||||
|
|
@ -2644,6 +2674,7 @@
|
||||||
x-model="noteContent"
|
x-model="noteContent"
|
||||||
@input="autoSave(); updateSyntaxHighlight()"
|
@input="autoSave(); updateSyntaxHighlight()"
|
||||||
@scroll="syncOverlayScroll()"
|
@scroll="syncOverlayScroll()"
|
||||||
|
@keydown.tab="handleTabKey($event)"
|
||||||
@drop="onEditorDrop($event)"
|
@drop="onEditorDrop($event)"
|
||||||
@dragover.prevent="onEditorDragOver($event)"
|
@dragover.prevent="onEditorDragOver($event)"
|
||||||
@dragenter="onEditorDragEnter($event)"
|
@dragenter="onEditorDragEnter($event)"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -250,7 +251,9 @@
|
||||||
"readable_line_length": "Lesbare Zeilenlänge",
|
"readable_line_length": "Lesbare Zeilenlänge",
|
||||||
"readable_line_length_desc": "Vorschaubreite für bessere Lesbarkeit begrenzen",
|
"readable_line_length_desc": "Vorschaubreite für bessere Lesbarkeit begrenzen",
|
||||||
"hide_underscore_folders": "Systemordner ausblenden",
|
"hide_underscore_folders": "Systemordner ausblenden",
|
||||||
"hide_underscore_folders_desc": "_attachments, _templates und andere Unterstrich-Ordner ausblenden"
|
"hide_underscore_folders_desc": "_attachments, _templates und andere Unterstrich-Ordner ausblenden",
|
||||||
|
"tab_inserts_tab": "Tab-Taste fügt Tab ein",
|
||||||
|
"tab_inserts_tab_desc": "Tab drücken, um ein Tabulatorzeichen einzufügen statt den Fokus zu wechseln"
|
||||||
},
|
},
|
||||||
|
|
||||||
"homepage": {
|
"homepage": {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -249,7 +250,9 @@
|
||||||
"readable_line_length": "Readable line length",
|
"readable_line_length": "Readable line length",
|
||||||
"readable_line_length_desc": "Limit preview width for easier reading",
|
"readable_line_length_desc": "Limit preview width for easier reading",
|
||||||
"hide_underscore_folders": "Hide system folders",
|
"hide_underscore_folders": "Hide system folders",
|
||||||
"hide_underscore_folders_desc": "Hide _attachments, _templates and other underscore folders from sidebar"
|
"hide_underscore_folders_desc": "Hide _attachments, _templates and other underscore folders from sidebar",
|
||||||
|
"tab_inserts_tab": "Tab key inserts tab",
|
||||||
|
"tab_inserts_tab_desc": "Press Tab to insert a tab character instead of changing focus"
|
||||||
},
|
},
|
||||||
|
|
||||||
"homepage": {
|
"homepage": {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -250,7 +251,9 @@
|
||||||
"readable_line_length": "Readable line length",
|
"readable_line_length": "Readable line length",
|
||||||
"readable_line_length_desc": "Limit preview width for easier reading",
|
"readable_line_length_desc": "Limit preview width for easier reading",
|
||||||
"hide_underscore_folders": "Hide system folders",
|
"hide_underscore_folders": "Hide system folders",
|
||||||
"hide_underscore_folders_desc": "Hide _attachments, _templates and other underscore folders from sidebar"
|
"hide_underscore_folders_desc": "Hide _attachments, _templates and other underscore folders from sidebar",
|
||||||
|
"tab_inserts_tab": "Tab key inserts tab",
|
||||||
|
"tab_inserts_tab_desc": "Press Tab to insert a tab character instead of changing focus"
|
||||||
},
|
},
|
||||||
|
|
||||||
"homepage": {
|
"homepage": {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -250,7 +251,9 @@
|
||||||
"readable_line_length": "Longitud de línea legible",
|
"readable_line_length": "Longitud de línea legible",
|
||||||
"readable_line_length_desc": "Limitar el ancho de vista previa para facilitar la lectura",
|
"readable_line_length_desc": "Limitar el ancho de vista previa para facilitar la lectura",
|
||||||
"hide_underscore_folders": "Ocultar carpetas de sistema",
|
"hide_underscore_folders": "Ocultar carpetas de sistema",
|
||||||
"hide_underscore_folders_desc": "Ocultar _attachments, _templates y otras carpetas con guion bajo"
|
"hide_underscore_folders_desc": "Ocultar _attachments, _templates y otras carpetas con guion bajo",
|
||||||
|
"tab_inserts_tab": "Tabulador inserta tabulación",
|
||||||
|
"tab_inserts_tab_desc": "Pulsa Tab para insertar un tabulador en lugar de cambiar el foco"
|
||||||
},
|
},
|
||||||
|
|
||||||
"homepage": {
|
"homepage": {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -250,7 +251,9 @@
|
||||||
"readable_line_length": "Longueur de ligne lisible",
|
"readable_line_length": "Longueur de ligne lisible",
|
||||||
"readable_line_length_desc": "Limiter la largeur de l'aperçu pour une lecture plus facile",
|
"readable_line_length_desc": "Limiter la largeur de l'aperçu pour une lecture plus facile",
|
||||||
"hide_underscore_folders": "Masquer les dossiers système",
|
"hide_underscore_folders": "Masquer les dossiers système",
|
||||||
"hide_underscore_folders_desc": "Masquer _attachments, _templates et autres dossiers préfixés"
|
"hide_underscore_folders_desc": "Masquer _attachments, _templates et autres dossiers préfixés",
|
||||||
|
"tab_inserts_tab": "Tab insère une tabulation",
|
||||||
|
"tab_inserts_tab_desc": "Appuyez sur Tab pour insérer une tabulation au lieu de changer le focus"
|
||||||
},
|
},
|
||||||
|
|
||||||
"homepage": {
|
"homepage": {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -250,7 +251,9 @@
|
||||||
"readable_line_length": "Olvasható sorhossz",
|
"readable_line_length": "Olvasható sorhossz",
|
||||||
"readable_line_length_desc": "Korlátozd az előnézeti szélességet a könnyebb olvasás érdekében",
|
"readable_line_length_desc": "Korlátozd az előnézeti szélességet a könnyebb olvasás érdekében",
|
||||||
"hide_underscore_folders": "Rendszermappák elrejtése",
|
"hide_underscore_folders": "Rendszermappák elrejtése",
|
||||||
"hide_underscore_folders_desc": "_attachments, _templates és más aláhúzásos mappák elrejtése"
|
"hide_underscore_folders_desc": "_attachments, _templates és más aláhúzásos mappák elrejtése",
|
||||||
|
"tab_inserts_tab": "Tab billentyű tabulátort szúr be",
|
||||||
|
"tab_inserts_tab_desc": "Nyomja meg a Tab-ot tabulátor beszúrásához a fókuszváltás helyett"
|
||||||
},
|
},
|
||||||
|
|
||||||
"homepage": {
|
"homepage": {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -249,7 +250,9 @@
|
||||||
"readable_line_length": "Larghezza riga leggibile",
|
"readable_line_length": "Larghezza riga leggibile",
|
||||||
"readable_line_length_desc": "Limita la larghezza dell'anteprima per una lettura più facile",
|
"readable_line_length_desc": "Limita la larghezza dell'anteprima per una lettura più facile",
|
||||||
"hide_underscore_folders": "Nascondi cartelle di sistema",
|
"hide_underscore_folders": "Nascondi cartelle di sistema",
|
||||||
"hide_underscore_folders_desc": "Nascondi _attachments, _templates e altre cartelle con underscore"
|
"hide_underscore_folders_desc": "Nascondi _attachments, _templates e altre cartelle con underscore",
|
||||||
|
"tab_inserts_tab": "Tab inserisce tabulazione",
|
||||||
|
"tab_inserts_tab_desc": "Premi Tab per inserire una tabulazione invece di cambiare focus"
|
||||||
},
|
},
|
||||||
|
|
||||||
"homepage": {
|
"homepage": {
|
||||||
|
|
|
||||||
|
|
@ -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": "お気に入りから削除"
|
||||||
|
|
@ -249,7 +250,9 @@
|
||||||
"readable_line_length": "読みやすい行幅",
|
"readable_line_length": "読みやすい行幅",
|
||||||
"readable_line_length_desc": "プレビューの幅を制限して読みやすくする",
|
"readable_line_length_desc": "プレビューの幅を制限して読みやすくする",
|
||||||
"hide_underscore_folders": "システムフォルダを非表示",
|
"hide_underscore_folders": "システムフォルダを非表示",
|
||||||
"hide_underscore_folders_desc": "_attachments、_templatesなどのフォルダをサイドバーから非表示"
|
"hide_underscore_folders_desc": "_attachments、_templatesなどのフォルダをサイドバーから非表示",
|
||||||
|
"tab_inserts_tab": "Tabキーでタブを挿入",
|
||||||
|
"tab_inserts_tab_desc": "Tabキーでフォーカス移動ではなくタブ文字を挿入"
|
||||||
},
|
},
|
||||||
|
|
||||||
"homepage": {
|
"homepage": {
|
||||||
|
|
|
||||||
|
|
@ -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": "Удалить из избранного"
|
||||||
|
|
@ -249,7 +250,9 @@
|
||||||
"readable_line_length": "Удобная длина строки",
|
"readable_line_length": "Удобная длина строки",
|
||||||
"readable_line_length_desc": "Ограничить ширину предпросмотра для удобства чтения",
|
"readable_line_length_desc": "Ограничить ширину предпросмотра для удобства чтения",
|
||||||
"hide_underscore_folders": "Скрыть системные папки",
|
"hide_underscore_folders": "Скрыть системные папки",
|
||||||
"hide_underscore_folders_desc": "Скрыть _attachments, _templates и другие папки с подчёркиванием"
|
"hide_underscore_folders_desc": "Скрыть _attachments, _templates и другие папки с подчёркиванием",
|
||||||
|
"tab_inserts_tab": "Tab вставляет табуляцию",
|
||||||
|
"tab_inserts_tab_desc": "Нажмите Tab для вставки табуляции вместо переключения фокуса"
|
||||||
},
|
},
|
||||||
|
|
||||||
"homepage": {
|
"homepage": {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -249,7 +250,9 @@
|
||||||
"readable_line_length": "Berljiva dolžina vrstice",
|
"readable_line_length": "Berljiva dolžina vrstice",
|
||||||
"readable_line_length_desc": "Omejite širino predogleda za lažje branje",
|
"readable_line_length_desc": "Omejite širino predogleda za lažje branje",
|
||||||
"hide_underscore_folders": "Skrij sistemske mape",
|
"hide_underscore_folders": "Skrij sistemske mape",
|
||||||
"hide_underscore_folders_desc": "Skrij _attachments, _templates in druge mape s podčrtajem"
|
"hide_underscore_folders_desc": "Skrij _attachments, _templates in druge mape s podčrtajem",
|
||||||
|
"tab_inserts_tab": "Tipka Tab vstavi tabulator",
|
||||||
|
"tab_inserts_tab_desc": "Pritisnite Tab za vstavljanje tabulatorja namesto premikanja fokusa"
|
||||||
},
|
},
|
||||||
|
|
||||||
"homepage": {
|
"homepage": {
|
||||||
|
|
|
||||||
|
|
@ -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": "从收藏夹移除"
|
||||||
|
|
@ -249,7 +250,9 @@
|
||||||
"readable_line_length": "可读行宽",
|
"readable_line_length": "可读行宽",
|
||||||
"readable_line_length_desc": "限制预览宽度以便于阅读",
|
"readable_line_length_desc": "限制预览宽度以便于阅读",
|
||||||
"hide_underscore_folders": "隐藏系统文件夹",
|
"hide_underscore_folders": "隐藏系统文件夹",
|
||||||
"hide_underscore_folders_desc": "从侧边栏隐藏 _attachments、_templates 等下划线文件夹"
|
"hide_underscore_folders_desc": "从侧边栏隐藏 _attachments、_templates 等下划线文件夹",
|
||||||
|
"tab_inserts_tab": "Tab键插入制表符",
|
||||||
|
"tab_inserts_tab_desc": "按Tab键插入制表符而不是切换焦点"
|
||||||
},
|
},
|
||||||
|
|
||||||
"homepage": {
|
"homepage": {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue