Add File Download for notes.

This commit is contained in:
2025-07-06 22:46:31 +02:00
parent 9113dc1a69
commit f4b8664fd5
3 changed files with 527 additions and 6 deletions

View File

@@ -22,6 +22,38 @@
</div>
<div class="note-actions">
<div class="dropdown" style="display: inline-block;">
<button class="btn btn-success dropdown-toggle" type="button" id="downloadDropdown" data-toggle="dropdown">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16" style="vertical-align: -2px; margin-right: 4px;">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>
Download
</button>
<div class="dropdown-menu">
<a class="dropdown-item" href="{{ url_for('download_note', slug=note.slug, format='md') }}">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16" style="margin-right: 8px;">
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/>
<path d="M4.5 12.5A.5.5 0 0 1 5 12h3a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm0-2A.5.5 0 0 1 5 10h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm1.639-3.708 1.33.886 1.854-1.855a.25.25 0 0 1 .289-.047l1.888.974V8.5a.5.5 0 0 1-.5.5H5a.5.5 0 0 1-.5-.5V7s1.54-1.274 1.639-1.208zM6.25 6a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5z"/>
</svg>
Markdown (.md)
</a>
<a class="dropdown-item" href="{{ url_for('download_note', slug=note.slug, format='html') }}">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16" style="margin-right: 8px;">
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/>
<path d="M8.5 6.5a.5.5 0 0 0-1 0V8H6a.5.5 0 0 0 0 1h1.5v1.5a.5.5 0 0 0 1 0V9H10a.5.5 0 0 0 0-1H8.5V6.5z"/>
</svg>
HTML (.html)
</a>
<a class="dropdown-item" href="{{ url_for('download_note', slug=note.slug, format='txt') }}">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16" style="margin-right: 8px;">
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/>
<path d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5zM5 9.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z"/>
</svg>
Plain Text (.txt)
</a>
</div>
</div>
<a href="{{ url_for('view_note_mindmap', slug=note.slug) }}" class="btn btn-info">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16" style="vertical-align: -2px; margin-right: 4px;">
<circle cx="8" cy="8" r="2"/>
@@ -452,6 +484,64 @@
font-weight: 500;
}
/* Dropdown styles */
.dropdown {
position: relative;
}
.dropdown-toggle::after {
content: "";
display: inline-block;
margin-left: 0.255em;
vertical-align: 0.255em;
border-top: 0.3em solid;
border-right: 0.3em solid transparent;
border-bottom: 0;
border-left: 0.3em solid transparent;
}
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
display: none;
min-width: 200px;
padding: 0.5rem 0;
margin: 0.125rem 0 0;
background-color: white;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.dropdown-menu.show {
display: block;
}
.dropdown-item {
display: flex;
align-items: center;
width: 100%;
padding: 0.5rem 1rem;
color: #212529;
text-align: inherit;
text-decoration: none;
white-space: nowrap;
background-color: transparent;
border: 0;
}
.dropdown-item:hover {
background-color: #f8f9fa;
color: #212529;
text-decoration: none;
}
.dropdown-item svg {
flex-shrink: 0;
}
/* Modal styles */
.modal-content {
max-width: 500px;
@@ -486,8 +576,22 @@
</style>
<script>
{% if note.can_user_edit(g.user) %}
document.addEventListener('DOMContentLoaded', function() {
// Download dropdown functionality
const downloadBtn = document.getElementById('downloadDropdown');
const downloadMenu = downloadBtn.nextElementSibling;
downloadBtn.addEventListener('click', function(e) {
e.stopPropagation();
downloadMenu.classList.toggle('show');
});
// Close dropdown when clicking outside
document.addEventListener('click', function() {
downloadMenu.classList.remove('show');
});
{% if note.can_user_edit(g.user) %}
const modal = document.getElementById('link-modal');
const addLinkBtn = document.getElementById('add-link-btn');
const closeBtn = modal.querySelector('.close');
@@ -582,8 +686,8 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
});
{% endif %}
});
{% endif %}
</script>
{% endblock %}

View File

@@ -258,6 +258,12 @@
<path d="M6.5 6.5L4 4M9.5 6.5L12 4M6.5 9.5L4 12M9.5 9.5L12 12" stroke="currentColor" fill="none"/>
</svg>
</a>
<a href="{{ url_for('download_note', slug=note.slug, format='md') }}" class="btn-action" title="Download as Markdown">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>
</a>
{% if note.can_user_edit(g.user) %}
<a href="{{ url_for('edit_note', slug=note.slug) }}" class="btn-action" title="Edit">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
@@ -348,6 +354,12 @@
<path d="M6.5 6.5L4 4M9.5 6.5L12 4M6.5 9.5L4 12M9.5 9.5L12 12" stroke="currentColor" fill="none"/>
</svg>
</a>
<a href="{{ url_for('download_note', slug=note.slug, format='md') }}" class="btn-action" title="Download as Markdown">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>
</a>
{% if note.can_user_edit(g.user) %}
<a href="{{ url_for('edit_note', slug=note.slug) }}" class="btn-action" title="Edit">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
@@ -379,6 +391,13 @@
</div> <!-- End notes-layout -->
</div> <!-- End notes-list-container -->
<!-- Folder Download Dropdown (Hidden, positioned dynamically) -->
<div id="folder-download-dropdown" class="folder-download-dropdown">
<a href="#" data-format="md">Download as Markdown</a>
<a href="#" data-format="html">Download as HTML</a>
<a href="#" data-format="txt">Download as Text</a>
</div>
<style>
/* Notes list specific styles */
.notes-list-container {
@@ -544,6 +563,55 @@
margin-left: 0.5rem;
}
.folder-download-btn {
margin-left: 0.5rem;
opacity: 0;
transition: opacity 0.2s;
cursor: pointer;
padding: 0.2rem;
border-radius: 3px;
}
.folder-content:hover .folder-download-btn {
opacity: 0.7;
}
.folder-download-btn:hover {
opacity: 1 !important;
background: rgba(0, 0, 0, 0.1);
}
/* Folder download dropdown */
.folder-download-dropdown {
position: absolute;
right: 0;
top: 100%;
z-index: 1000;
min-width: 150px;
background: white;
border: 1px solid #dee2e6;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: none;
}
.folder-download-dropdown.show {
display: block;
}
.folder-download-dropdown a {
display: block;
padding: 0.5rem 1rem;
color: #333;
text-decoration: none;
font-size: 0.9rem;
transition: background 0.2s;
}
.folder-download-dropdown a:hover {
background: #f8f9fa;
}
.folder-children {
margin-left: 1.5rem;
display: none;
@@ -1026,7 +1094,7 @@
}
.column-actions {
width: 160px;
width: 200px;
}
.note-actions {
@@ -1460,6 +1528,9 @@ document.addEventListener('DOMContentLoaded', function() {
// Enable drag and drop for notes
enableDragAndDrop();
// Setup folder download dropdown
setupFolderDownload();
});
// Folder tree functions
@@ -1784,6 +1855,63 @@ function addTagsToNotes() {
alert('Error adding tags to notes');
});
}
// Folder download functions
function setupFolderDownload() {
const dropdown = document.getElementById('folder-download-dropdown');
// Close dropdown when clicking outside
document.addEventListener('click', function(e) {
if (!e.target.closest('.folder-download-btn') && !dropdown.contains(e.target)) {
dropdown.classList.remove('show');
}
});
// Setup download links
dropdown.querySelectorAll('a').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const format = this.getAttribute('data-format');
const folder = dropdown.getAttribute('data-folder');
if (folder) {
downloadFolder(folder, format);
}
dropdown.classList.remove('show');
});
});
}
function showFolderDownloadMenu(event, folderPath) {
event.stopPropagation();
const dropdown = document.getElementById('folder-download-dropdown');
const btn = event.currentTarget;
// Position dropdown near the button
const rect = btn.getBoundingClientRect();
dropdown.style.position = 'fixed';
dropdown.style.left = rect.left + 'px';
dropdown.style.top = (rect.bottom + 5) + 'px';
// Store the folder path
dropdown.setAttribute('data-folder', folderPath);
// Show dropdown
dropdown.classList.add('show');
}
function downloadFolder(folderPath, format) {
// Encode the folder path for URL
const encodedPath = encodeURIComponent(folderPath);
const url = `/notes/folder/${encodedPath}/download/${format}`;
// Create a temporary link and click it
const link = document.createElement('a');
link.href = url;
link.download = '';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
</script>
{% endblock %}
@@ -1791,13 +1919,21 @@ function addTagsToNotes() {
{% macro render_folder_tree(tree, level=0) %}
{% for folder, children in tree.items() %}
<div class="folder-item {% if children %}has-children{% endif %}" data-folder="{{ folder }}">
<div class="folder-content {% if folder_filter == folder %}active{% endif %}" onclick="filterByFolder('{{ folder }}')">
<div class="folder-content {% if folder_filter == folder %}active{% endif %}">
{% if children %}
<span onclick="toggleFolder(event, '{{ folder }}')" style="position: absolute; left: -15px; cursor: pointer;"></span>
{% endif %}
<span class="folder-icon">📁</span>
<span class="folder-name">{{ folder.split('/')[-1] }}</span>
<span class="folder-icon" onclick="filterByFolder('{{ folder }}')" style="cursor: pointer;">📁</span>
<span class="folder-name" onclick="filterByFolder('{{ folder }}')" style="cursor: pointer;">{{ folder.split('/')[-1] }}</span>
<span class="folder-count">({{ folder_counts.get(folder, 0) }})</span>
{% if folder_counts.get(folder, 0) > 0 %}
<div class="folder-download-btn" onclick="showFolderDownloadMenu(event, '{{ folder }}')">
<svg width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>
</div>
{% endif %}
</div>
{% if children %}
<div class="folder-children">