From 02582c6b06227e129e79ffacc0dbb93ffa072a46 Mon Sep 17 00:00:00 2001 From: pika Date: Mon, 14 Apr 2025 22:56:18 +0200 Subject: [PATCH] wip --- app/auth/__pycache__/routes.cpython-313.pyc | Bin 4564 -> 4564 bytes app/routes.py | 60 ++++++- app/templates/base.html | 113 ++++++++++++- app/templates/category_edit.html | 175 +++++++++++++++++++- 4 files changed, 336 insertions(+), 12 deletions(-) diff --git a/app/auth/__pycache__/routes.cpython-313.pyc b/app/auth/__pycache__/routes.cpython-313.pyc index 53ee881fdc2c8093d0b662ba3f39f679ef8bafa5..6f6f9cd63e9b7df09a640c2262bec79f12e64b3c 100644 GIT binary patch delta 19 Zcmcbjd_|e-GcPX}0}${RZR9#72mm=Z1t$Oi delta 19 Zcmcbjd_|e-GcPX}0}!k%+{krG5CA;R1;79R diff --git a/app/routes.py b/app/routes.py index dc61b53..b33504d 100644 --- a/app/routes.py +++ b/app/routes.py @@ -10,6 +10,36 @@ from sqlalchemy import or_ main = Blueprint('main', __name__) +# Helper function to convert categories to a hierarchical dictionary +def build_category_hierarchy(categories): + categories_dict = [] + + # Start with root categories (parent_id is None) + root_categories = [c for c in categories if c.parent_id is None] + + # Helper function to convert category and its children to dict + def category_to_dict(category): + cat_dict = { + 'id': category.id, + 'name': category.name, + 'icon': category.icon, + 'is_root': getattr(category, 'is_root', False), + 'parent_id': category.parent_id, + 'children': [] + } + + # Find and add children + for child in [c for c in categories if c.parent_id == category.id]: + cat_dict['children'].append(category_to_dict(child)) + + return cat_dict + + # Convert all root categories and their children + for category in root_categories: + categories_dict.append(category_to_dict(category)) + + return categories_dict + @main.route('/') @login_required def index(): @@ -46,7 +76,11 @@ def edit_document(doc_id): document = Document.query.filter_by(id=doc_id, user_id=current_user.id).first_or_404() categories = Category.query.filter_by(user_id=current_user.id).all() tags = Tag.query.filter_by(user_id=current_user.id).all() - return render_template('document_edit.html', document=document, categories=categories, tags=tags) + + # Convert categories to dictionaries for JSON serialization + categories_dict = build_category_hierarchy(categories) + + return render_template('document_edit.html', document=document, categories=categories_dict, tags=tags) @main.route('/document/new', methods=['GET']) @login_required @@ -79,12 +113,10 @@ def new_document(): user_id=current_user.id ) - # Make sure all categories include their children for the hierarchical dropdown - for category in categories: - if hasattr(category, 'to_dict'): - setattr(category, '_serialized', category.to_dict()) + # Convert categories to dictionaries for JSON serialization + categories_dict = build_category_hierarchy(categories) - return render_template('document_edit.html', document=document, categories=categories, tags=tags, preselected_category_id=preselected_category_id) + return render_template('document_edit.html', document=document, categories=categories_dict, tags=tags, preselected_category_id=preselected_category_id) @main.route('/api/document', methods=['POST']) @login_required @@ -147,6 +179,22 @@ def delete_document(doc_id): db.session.commit() return jsonify({'success': True}) +@main.route('/api/document/', methods=['PATCH']) +@login_required +def update_document(doc_id): + """Update a document's properties (like category)""" + document = Document.query.filter_by(id=doc_id, user_id=current_user.id).first_or_404() + data = request.json + + # Update category if provided + if 'category_id' in data and data['category_id']: + # Verify the category exists and belongs to user + category = Category.query.filter_by(id=data['category_id'], user_id=current_user.id).first_or_404() + document.category_id = category.id + + db.session.commit() + return jsonify(document.to_dict()) + @main.route('/api/category', methods=['POST']) @login_required def save_category(): diff --git a/app/templates/base.html b/app/templates/base.html index f773f9e..5464817 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -3,6 +3,7 @@ + {% block title %}Vim Docs{% endblock %} @@ -288,13 +289,98 @@ } }); - // Add children - if (category.children && category.children.length > 0) { + // If this category has documents or child categories, add them + if ((category.documents && category.documents.length > 0) || + (category.children && category.children.length > 0)) { + const childrenUl = document.createElement('ul'); childrenUl.className = 'ml-2 pl-2 border-l border-gray-700 my-1'; - category.children.forEach(child => { - childrenUl.appendChild(createCategoryItem(child)); + // Add documents first + if (category.documents && category.documents.length > 0) { + // Sort documents by name + const docs = [...category.documents]; + docs.sort((a, b) => a.title.localeCompare(b.title)); + + // Add document items + docs.forEach(docId => { + // We need to fetch the document title since we only have IDs + fetch(`/api/document/${docId}`) + .then(response => response.json()) + .then(doc => { + const docLi = document.createElement('li'); + const docLink = document.createElement('a'); + docLink.href = `/document/${doc.id}`; + docLink.className = 'flex items-center py-1 px-2 text-gray-400 hover:text-primary rounded transition-colors'; + docLink.innerHTML = ` ${doc.title}`; + + // Add drag functionality + docLink.draggable = true; + docLink.dataset.docId = doc.id; + docLink.addEventListener('dragstart', function(e) { + e.dataTransfer.setData('text/plain', JSON.stringify({ + type: 'document', + id: doc.id, + title: doc.title + })); + }); + + docLi.appendChild(docLink); + childrenUl.appendChild(docLi); + }) + .catch(error => console.error('Error fetching document:', error)); + }); + } + + // Add child categories after documents + if (category.children && category.children.length > 0) { + category.children.forEach(child => { + childrenUl.appendChild(createCategoryItem(child)); + }); + } + + // Add drop capability to the category + li.addEventListener('dragover', function(e) { + e.preventDefault(); + li.classList.add('bg-gray-700/30'); // Visual feedback + }); + + li.addEventListener('dragleave', function() { + li.classList.remove('bg-gray-700/30'); + }); + + li.addEventListener('drop', function(e) { + e.preventDefault(); + li.classList.remove('bg-gray-700/30'); + + try { + const data = JSON.parse(e.dataTransfer.getData('text/plain')); + + if (data.type === 'document') { + // Handle document drop - move to this category + fetch(`/api/document/${data.id}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCsrfToken() + }, + body: JSON.stringify({ + category_id: category.id + }) + }) + .then(response => { + if (response.ok) { + // Show success notification + showNotification(`Moved "${data.title}" to ${category.name}`); + // Refresh the category tree + loadCategories(); + } + }) + .catch(error => console.error('Error moving document:', error)); + } + } catch (error) { + console.error('Error processing drop:', error); + } }); li.appendChild(childrenUl); @@ -303,6 +389,25 @@ return li; } + // Helper function to show notifications + function showNotification(message) { + const notification = document.createElement('div'); + notification.className = 'fixed bottom-4 right-4 bg-primary/90 text-white px-4 py-2 rounded-md shadow-lg transform translate-y-0 opacity-100 transition-all duration-300 flex items-center'; + notification.innerHTML = `${message}`; + + document.body.appendChild(notification); + + setTimeout(() => { + notification.classList.add('translate-y-16', 'opacity-0'); + setTimeout(() => notification.remove(), 300); + }, 3000); + } + + // Helper function to get CSRF token from meta tag + function getCsrfToken() { + return document.querySelector('meta[name="csrf-token"]').getAttribute('content'); + } + function setupSearch() { const searchInput = document.getElementById('search-input'); const searchResults = document.getElementById('search-results'); diff --git a/app/templates/category_edit.html b/app/templates/category_edit.html index fb93c97..4fe069c 100644 --- a/app/templates/category_edit.html +++ b/app/templates/category_edit.html @@ -22,8 +22,54 @@
- +
+
+
+ +
+ + +
+ + + +

Example: mdi-folder-outline, mdi-code-tags, mdi-book-open-page-variant

@@ -188,6 +234,131 @@ alert('Error saving category: ' + error.message); }); }); + + // Icon Preview Functionality + const iconPreview = document.getElementById('icon-preview'); + + // Update icon preview when input changes + function updateIconPreview() { + const iconClass = iconInput.value.trim(); + + // Remove all mdi classes + iconPreview.className = ''; + + // Add the mdi base class and the selected icon class + iconPreview.classList.add('mdi'); + + // If the value doesn't start with mdi-, add it + if (iconClass.startsWith('mdi-')) { + iconPreview.classList.add(iconClass); + } else if (iconClass) { + iconPreview.classList.add('mdi-' + iconClass.replace('mdi-', '')); + } else { + iconPreview.classList.add('mdi-folder-outline'); + } + } + + // Initial preview + updateIconPreview(); + + // Update preview on input change + iconInput.addEventListener('input', updateIconPreview); + + // Icon Search Modal + const iconSearchBtn = document.getElementById('icon-search-btn'); + const iconSearchModal = document.getElementById('icon-search-modal'); + const closeIconModal = document.getElementById('close-icon-modal'); + const iconSearchInput = document.getElementById('icon-search-input'); + const iconsGrid = document.getElementById('icons-grid'); + + // Common Material Design Icons + const commonIcons = [ + 'mdi-folder-outline', 'mdi-folder', 'mdi-folder-open', 'mdi-folder-plus', + 'mdi-file-document-outline', 'mdi-file-document', 'mdi-file', + 'mdi-note-outline', 'mdi-note', 'mdi-notebook', 'mdi-notebook-outline', + 'mdi-code-tags', 'mdi-code-braces', 'mdi-console', 'mdi-database', + 'mdi-server', 'mdi-desktop-tower', 'mdi-monitor', 'mdi-laptop', + 'mdi-home', 'mdi-account', 'mdi-cog', 'mdi-star', 'mdi-heart', + 'mdi-book', 'mdi-book-open', 'mdi-bookshelf', 'mdi-bookmark', + 'mdi-alert', 'mdi-information', 'mdi-help-circle', 'mdi-check-circle', + 'mdi-tools', 'mdi-wrench', 'mdi-hammer', 'mdi-screwdriver', + 'mdi-shield', 'mdi-lock', 'mdi-key', 'mdi-wifi', 'mdi-web', + 'mdi-link', 'mdi-github', 'mdi-git', 'mdi-docker', 'mdi-language-python', + 'mdi-language-javascript', 'mdi-language-html5', 'mdi-language-css3', + 'mdi-chart-bar', 'mdi-chart-line', 'mdi-chart-pie', 'mdi-chart-bubble', + 'mdi-movie', 'mdi-music', 'mdi-image', 'mdi-camera', 'mdi-video', + 'mdi-atom', 'mdi-microscope', 'mdi-flask', 'mdi-test-tube', + 'mdi-robot', 'mdi-brain', 'mdi-bug', 'mdi-rocket', 'mdi-satellite', + 'mdi-car', 'mdi-airplane', 'mdi-train', 'mdi-bike', 'mdi-walk', + 'mdi-food', 'mdi-coffee', 'mdi-beer', 'mdi-pizza', 'mdi-cake', + 'mdi-currency-usd', 'mdi-cash', 'mdi-credit-card', 'mdi-bank', + 'mdi-message', 'mdi-email', 'mdi-chat', 'mdi-forum', 'mdi-comment', + 'mdi-calendar', 'mdi-clock', 'mdi-alarm', 'mdi-timer', 'mdi-watch', + 'mdi-weather-sunny', 'mdi-weather-night', 'mdi-weather-rainy', 'mdi-weather-snowy', + 'mdi-map', 'mdi-map-marker', 'mdi-compass', 'mdi-earth', 'mdi-directions', + 'mdi-shopping', 'mdi-cart', 'mdi-store', 'mdi-tag', 'mdi-sale', + 'mdi-palette', 'mdi-brush', 'mdi-format-paint', 'mdi-pencil', 'mdi-pen', + 'mdi-printer', 'mdi-scanner', 'mdi-fax', 'mdi-file-pdf', 'mdi-file-excel', + 'mdi-file-word', 'mdi-file-powerpoint', 'mdi-file-image', 'mdi-file-video', + 'mdi-file-music', 'mdi-file-xml', 'mdi-file-code', 'mdi-zip-box' + ]; + + // Populate the icons grid + function populateIconsGrid(searchTerm = '') { + iconsGrid.innerHTML = ''; + + const filteredIcons = searchTerm + ? commonIcons.filter(icon => icon.toLowerCase().includes(searchTerm.toLowerCase())) + : commonIcons; + + filteredIcons.forEach(icon => { + const iconDiv = document.createElement('div'); + iconDiv.className = 'cursor-pointer p-2 bg-gray-700 rounded-md flex flex-col items-center hover:bg-gray-600'; + + const iconEl = document.createElement('i'); + iconEl.className = 'mdi ' + icon + ' text-2xl mb-1'; + + const iconName = document.createElement('div'); + iconName.className = 'text-xs text-gray-400 truncate w-full text-center'; + iconName.textContent = icon.replace('mdi-', ''); + + iconDiv.appendChild(iconEl); + iconDiv.appendChild(iconName); + + // Select icon when clicked + iconDiv.addEventListener('click', function() { + iconInput.value = icon; + updateIconPreview(); + iconSearchModal.classList.add('hidden'); + }); + + iconsGrid.appendChild(iconDiv); + }); + } + + // Open modal + iconSearchBtn.addEventListener('click', function() { + populateIconsGrid(); + iconSearchModal.classList.remove('hidden'); + iconSearchInput.focus(); + }); + + // Close modal + closeIconModal.addEventListener('click', function() { + iconSearchModal.classList.add('hidden'); + }); + + // Click outside to close + iconSearchModal.addEventListener('click', function(e) { + if (e.target === iconSearchModal) { + iconSearchModal.classList.add('hidden'); + } + }); + + // Search functionality + iconSearchInput.addEventListener('input', function() { + populateIconsGrid(this.value); + }); }); {% endblock %} \ No newline at end of file