Spaces:
Running
Running
| {% extends "base.html" %} | |
| {% block title %}PDF Manager - DocuPDF{% endblock %} | |
| {% block styles %} | |
| <style> | |
| .table-container { | |
| max-height: 60vh; | |
| overflow-y: auto; | |
| border: 1px solid var(--border-subtle); | |
| border-radius: .25rem; | |
| } | |
| .filename-col { | |
| max-width: 250px; | |
| white-space: normal; | |
| word-wrap: break-word; | |
| } | |
| .pdf-filename { | |
| word-break: break-all; | |
| white-space: normal; | |
| display: inline-block; | |
| max-width: 100%; | |
| } | |
| .folder-row, .pdf-row { | |
| cursor: pointer; | |
| } | |
| #folder-tree-move .list-group-item { | |
| cursor: pointer; | |
| } | |
| #folder-tree-move .list-group-item.active { | |
| background-color: var(--accent-primary); | |
| border-color: var(--accent-primary); | |
| } | |
| .editable-input { | |
| width: 100%; | |
| background-color: var(--bg-elevated); | |
| color: #fff; | |
| border: 1px solid var(--accent-primary); | |
| border-radius: .25rem; | |
| padding: .375rem .75rem; | |
| } | |
| .table-hover .highlighted-row { | |
| background-color: var(--accent-primary) ; | |
| } | |
| .no-caret::after { | |
| display: none ; | |
| } | |
| .ts-dropdown { | |
| z-index: 1060 ; | |
| } | |
| </style> | |
| {% endblock %} | |
| {% block content %} | |
| <div class="container-fluid mt-4" style="width: 90%; margin: auto;"> | |
| <!-- Header & Actions --> | |
| <div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-4"> | |
| <h1 class="mb-0">PDF Manager</h1> | |
| <div class="d-flex flex-wrap gap-2 align-items-center"> | |
| <form action="/pdf_manager" method="get" class="d-flex gap-2"> | |
| <input type="text" name="search" class="form-control bg-dark text-white border-secondary" placeholder="Search..." value="{{ request.args.get('search', '') }}"> | |
| <button type="submit" class="btn btn-outline-secondary"><i class="bi bi-search"></i></button> | |
| {% if request.args.get('search') %} | |
| <a href="/pdf_manager" class="btn btn-outline-danger"><i class="bi bi-x-lg"></i></a> | |
| {% endif %} | |
| </form> | |
| {% if all_view %} | |
| <a href="/pdf_manager" class="btn btn-info">Show Folders</a> | |
| {% else %} | |
| <a href="/pdf_manager?view=all" class="btn btn-info">Show All PDFs</a> | |
| {% endif %} | |
| <button class="btn btn-primary" id="create-folder-btn">Create Folder</button> | |
| <a href="/upload_final_pdf" class="btn btn-success">Upload</a> | |
| <button class="btn btn-outline-info" data-bs-toggle="modal" data-bs-target="#shortcutsModal">?</button> | |
| </div> | |
| </div> | |
| <!-- Bulk Actions --> | |
| <div class="d-flex flex-wrap justify-content-end align-items-center gap-2 mb-3"> | |
| <div class="form-check me-3"> | |
| <input type="checkbox" class="form-check-input" id="select-all-checkbox"> | |
| <label class="form-check-label" for="select-all-checkbox">Select All</label> | |
| </div> | |
| <button class="btn btn-info" id="bulk-persist-btn" disabled>Toggle Persist</button> | |
| <button class="btn btn-primary" id="bulk-merge-btn" disabled>Merge Selected</button> | |
| <button class="btn btn-success" id="bulk-download-btn" disabled>Download Selected</button> | |
| <button class="btn btn-warning" id="bulk-move-btn" disabled>Move Selected</button> | |
| <button class="btn btn-warning" id="bulk-edit-btn" disabled>Edit</button> | |
| <button class="btn btn-primary" id="bulk-rename-btn" disabled>Rename</button> | |
| <button class="btn btn-danger" id="bulk-delete-btn" disabled>Delete Selected</button> | |
| <button class="btn btn-secondary" id="bulk-print-btn" disabled>Print Selected</button> | |
| </div> | |
| {% if not all_view %} | |
| <!-- Breadcrumbs --> | |
| <nav aria-label="breadcrumb" class="mb-3"> | |
| <ol class="breadcrumb"> | |
| <li class="breadcrumb-item"><a href="/pdf_manager">Home</a></li> | |
| {% for item in breadcrumbs %} | |
| <li class="breadcrumb-item"><a href="/pdf_manager/browse/{{ item.path }}">{{ item.name }}</a></li> | |
| {% endfor %} | |
| </ol> | |
| </nav> | |
| {% endif %} | |
| <div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 g-4"> | |
| {% if not all_view %} | |
| <!-- Folders --> | |
| {% for folder in subfolders %} | |
| <div class="col"> | |
| <div class="card h-100 bg-dark text-white border-secondary folder-card" data-path="{{ (breadcrumbs|map(attribute='path')|list)|last if breadcrumbs else '' }}/{{ folder.name }}"> | |
| <div class="card-body text-center"> | |
| <i class="bi bi-folder-fill fs-1 text-primary"></i> | |
| <h5 class="card-title mt-2">{{ folder.name }}</h5> | |
| <p class="card-text text-muted">{{ folder.created_at.strftime('%Y-%m-%d %I:%M %p') }}</p> | |
| </div> | |
| <div class="card-footer d-flex justify-content-between align-items-center"> | |
| <input type="checkbox" class="form-check-input item-checkbox" data-item-type="folder" value="{{ folder.id }}"> | |
| <div class="dropdown"> | |
| <button class="btn btn-sm btn-outline-secondary dropdown-toggle no-caret" type="button" data-bs-toggle="dropdown"> | |
| <i class="bi bi-three-dots-vertical"></i> | |
| </button> | |
| <ul class="dropdown-menu dropdown-menu-dark dropdown-menu-end"> | |
| <li><a class="dropdown-item rename-single-btn" href="#" data-id="{{ folder.id }}" data-type="folder"><i class="bi bi-pencil-square me-2"></i> Rename</a></li> | |
| <li><hr class="dropdown-divider"></li> | |
| <li><a class="dropdown-item text-danger delete-item-btn" href="#" data-item-type="folder" data-id="{{ folder.id }}"><i class="bi bi-trash me-2"></i> Delete</a></li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {% endfor %} | |
| {% endif %} | |
| <!-- PDFs --> | |
| {% for pdf in pdfs %} | |
| <div class="col"> | |
| <div class="card h-100 bg-dark text-white border-secondary pdf-card"> | |
| <div class="card-header d-flex justify-content-between align-items-center"> | |
| <div> | |
| <i class="bi bi-file-earmark-text me-2"></i> | |
| {% set viewer_url = '/view_pdf_v2/' if current_user.pdf_viewer == 'adobe' else ('/viewpdflegacy/' ~ pdf.id if current_user.pdf_viewer == 'legacy' else '/view_pdf/') %} | |
| {% set pdf_path = '' if current_user.pdf_viewer == 'legacy' else pdf.filename %} | |
| <a href="{{ viewer_url }}{{ pdf_path }}" target="_blank" class="text-white text-decoration-none"> | |
| <span class="fw-bold pdf-filename">{{ pdf.filename }}</span> | |
| </a> | |
| </div> | |
| <div class="form-check"> | |
| <input type="checkbox" class="form-check-input item-checkbox" data-item-type="pdf" value="{{ pdf.id }}" style="z-index: 2; position: relative;"> | |
| </div> | |
| </div> | |
| <div class="card-body"> | |
| <p class="card-text small text-muted"> | |
| Subject: {{ pdf.subject or 'No subject' }} | |
| </p> | |
| <p class="card-text"> | |
| {% if pdf.tags %} | |
| {% for tag in pdf.tags.split(',') %} | |
| <a href="{{ url_for('main.pdf_manager', search=tag.strip()) }}" class="badge bg-secondary text-decoration-none">{{ tag.strip() }}</a> | |
| {% endfor %} | |
| {% endif %} | |
| </p> | |
| </div> | |
| <div class="card-footer d-flex justify-content-between align-items-center"> | |
| <small class="text-muted">{{ pdf.created_at.strftime('%Y-%m-%d %I:%M %p') }}</small> | |
| <div class="d-flex align-items-center gap-2"> | |
| {% if pdf.persist %}<i class="bi bi-pin-angle-fill text-primary" title="Persisted"></i>{% endif %} | |
| <a href="intent://{{ request.host }}{{ url_for('main.download_file', filename=pdf.filename) }}#Intent;scheme={{ request.scheme }};action=android.intent.action.VIEW;type=application/pdf;end" | |
| class="btn btn-sm btn-outline-info" title="Open in external app" target="_blank"> | |
| <i class="bi bi-box-arrow-up-right"></i> | |
| </a> | |
| <a href="{{ url_for('main.download_file', filename=pdf.filename) }}" class="btn btn-sm btn-outline-success" title="Download"> | |
| <i class="bi bi-download"></i> | |
| </a> | |
| <div class="dropdown"> | |
| <button class="btn btn-sm btn-outline-secondary dropdown-toggle no-caret" type="button" data-bs-toggle="dropdown"> | |
| <i class="bi bi-three-dots-vertical"></i> | |
| </button> | |
| <ul class="dropdown-menu dropdown-menu-dark dropdown-menu-end"> | |
| <li><a class="dropdown-item edit-single-btn" href="#" data-id="{{ pdf.id }}"><i class="bi bi-info-circle me-2"></i> Details</a></li> | |
| <li><hr class="dropdown-divider"></li> | |
| <li><a class="dropdown-item text-danger delete-item-btn" href="#" data-item-type="pdf" data-id="{{ pdf.id }}"><i class="bi bi-trash me-2"></i> Delete</a></li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {% endfor %} | |
| </div> | |
| <!-- Modals --><!-- Create Folder Modal --> | |
| <div class="modal fade" id="createFolderModal" tabindex="-1"> | |
| <div class="modal-dialog"> | |
| <div class="modal-content bg-dark text-white"> | |
| <div class="modal-header"> | |
| <h5 class="modal-title">Create New Folder</h5> | |
| <button type="button" class="btn-close" data-bs-dismiss="modal"></button> | |
| </div> | |
| <div class="modal-body"> | |
| <label for="newFolderName" class="form-label">Folder Name</label> | |
| <input type="text" class="form-control" id="newFolderName"> | |
| </div> | |
| <div class="modal-footer"> | |
| <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> | |
| <button type="button" class="btn btn-primary" id="create-folder-confirm-btn">Create</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Move Modal --> | |
| <div class="modal fade" id="moveModal" tabindex="-1"> | |
| <div class="modal-dialog modal-lg"> | |
| <div class="modal-content bg-dark text-white"> | |
| <div class="modal-header"> | |
| <h5 class="modal-title">Move Selected Files</h5> | |
| <button type="button" class="btn-close" data-bs-dismiss="modal"></button> | |
| </div> | |
| <div class="modal-body"> | |
| <p>Select destination folder:</p> | |
| <div id="folder-tree-move" class="list-group" style="max-height: 300px; overflow-y: auto;"></div> | |
| <hr> | |
| <div class="input-group mb-3"> | |
| <input type="text" class="form-control" placeholder="New subfolder name" id="new-subfolder-name"> | |
| <button class="btn btn-outline-secondary" type="button" id="create-subfolder-btn">Create Subfolder</button> | |
| </div> | |
| </div> | |
| <div class="modal-footer"> | |
| <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> | |
| <button type="button" class="btn btn-primary" id="move-confirm-btn">Move Here</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Edit Modal --> | |
| <div class="modal fade" id="editModal" tabindex="-1"> | |
| <div class="modal-dialog modal-lg"> | |
| <div class="modal-content bg-dark text-white"> | |
| <div class="modal-header"> | |
| <h5 class="modal-title">Edit PDF Details</h5> | |
| <button type="button" class="btn-close" data-bs-dismiss="modal"></button> | |
| </div> | |
| <div class="modal-body"> | |
| <!-- Carousel Navigation (visible in bulk edit) --> | |
| <div id="edit-carousel-nav" class="d-flex justify-content-between align-items-center mb-3" style="display: none;"> | |
| <button class="btn btn-outline-secondary" id="edit-prev-btn">« Previous</button> | |
| <span id="edit-carousel-indicator"></span> | |
| <button class="btn btn-outline-secondary" id="edit-next-btn">Next »</button> | |
| </div> | |
| <input type="hidden" id="edit-pdf-id"> | |
| <div class="mb-3"> | |
| <label for="edit-subject" class="form-label">Subject</label> | |
| <input type="text" class="form-control" id="edit-subject"> | |
| </div> | |
| <div class="mb-3"> | |
| <label for="edit-tags" class="form-label">Tags (comma-separated)</label> | |
| <input type="text" class="form-control" id="edit-tags"> | |
| </div> | |
| <div class="mb-3"> | |
| <label for="edit-notes" class="form-label">Notes</label> | |
| <textarea class="form-control" id="edit-notes" rows="3"></textarea> | |
| </div> | |
| <div class="mb-3"> | |
| <label for="edit-filename" class="form-label">Filename</label> | |
| <input type="text" class="form-control" id="edit-filename" readonly> | |
| </div> | |
| </div> | |
| <div class="modal-footer"> | |
| <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> | |
| <button type="button" class="btn btn-primary" id="save-edit-btn">Save Changes</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Shortcuts Modal --> | |
| <div class="modal fade" id="shortcutsModal" tabindex="-1"> | |
| <div class="modal-dialog"> | |
| <div class="modal-content bg-dark text-white"> | |
| <div class="modal-header"> | |
| <h5 class="modal-title">Keyboard Shortcuts</h5> | |
| <button type="button" class="btn-close" data-bs-dismiss="modal"></button> | |
| </div> | |
| <div class="modal-body"> | |
| <ul class="list-group"> | |
| <li class="list-group-item bg-dark text-white d-flex justify-content-between"><span>Navigate up/down</span><div><kbd>j</kbd> / <kbd>k</kbd> or <kbd>↑</kbd> / <kbd>↓</kbd></div></li> | |
| <li class="list-group-item bg-dark text-white d-flex justify-content-between"><span>Select/Deselect item</span><kbd>x</kbd></li> | |
| <li class="list-group-item bg-dark text-white d-flex justify-content-between"><span>Toggle select all</span><kbd>s</kbd></li> | |
| <li class="list-group-item bg-dark text-white d-flex justify-content-between"><span>Open folder / View PDF</span><kbd>Enter</kbd></li> | |
| <li class="list-group-item bg-dark text-white d-flex justify-content-between"><span>Edit selected</span><kbd>e</kbd></li> | |
| <li class="list-group-item bg-dark text-white d-flex justify-content-between"><span>Delete selected</span><kbd>d</kbd> or <kbd>Del</kbd></li> | |
| <li class="list-group-item bg-dark text-white d-flex justify-content-between"><span>Toggle persist for selected</span><kbd>p</kbd></li> | |
| <li class="list-group-item bg-dark text-white d-flex justify-content-between"><span>Show this help</span><kbd>?</kbd></li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {% endblock %} | |
| {% block scripts %} | |
| <script> | |
| document.addEventListener('DOMContentLoaded', () => { | |
| let allSubjects = []; | |
| let allTags = []; | |
| let subjectSelect, tagsSelect; | |
| async function fetchSubjectsAndTags() { | |
| try { | |
| const response = await fetch('/get_all_subjects_and_tags'); | |
| const data = await response.json(); | |
| allSubjects = data.subjects.map(s => ({value: s, text: s})); | |
| allTags = data.tags.map(t => ({value: t, text: t})); | |
| } catch (error) { | |
| console.error('Failed to fetch subjects and tags:', error); | |
| } | |
| } | |
| const currentFolderId = {{ current_folder_id|tojson }}; | |
| const folderTree = {{ folder_tree|tojson }}; | |
| let selectedFolderId = null; | |
| const bulkRenameBtn = document.getElementById('bulk-rename-btn'); | |
| const bulkEditBtn = document.getElementById('bulk-edit-btn'); | |
| const bulkMergeBtn = document.getElementById('bulk-merge-btn'); | |
| const bulkDeleteBtn = document.getElementById('bulk-delete-btn'); | |
| const selectAllCheckbox = document.getElementById('select-all-checkbox'); | |
| const itemCheckboxes = document.querySelectorAll('.item-checkbox'); | |
| function getSelectedItems(type = null) { | |
| let items = Array.from(itemCheckboxes).filter(cb => cb.checked); | |
| if (type) { | |
| items = items.filter(cb => cb.dataset.itemType === type); | |
| } | |
| return items; | |
| } | |
| function updateBulkButtons() { | |
| const selectedItems = getSelectedItems(); | |
| const selectedCount = selectedItems.length; | |
| const selectedPdfs = getSelectedItems('pdf'); | |
| const selectedFolders = getSelectedItems('folder'); | |
| document.getElementById('bulk-persist-btn').disabled = selectedCount === 0; | |
| document.getElementById('bulk-download-btn').disabled = selectedPdfs.length === 0; | |
| document.getElementById('bulk-move-btn').disabled = selectedCount === 0; | |
| document.getElementById('bulk-print-btn').disabled = selectedPdfs.length === 0; | |
| bulkDeleteBtn.disabled = selectedCount === 0; | |
| bulkMergeBtn.disabled = selectedPdfs.length < 2; | |
| bulkRenameBtn.disabled = selectedCount !== 1; | |
| bulkEditBtn.disabled = selectedPdfs.length === 0; | |
| } | |
| selectAllCheckbox.addEventListener('change', e => { | |
| itemCheckboxes.forEach(cb => cb.checked = e.target.checked); | |
| updateBulkButtons(); | |
| }); | |
| itemCheckboxes.forEach(cb => { | |
| cb.addEventListener('change', updateBulkButtons); | |
| }); | |
| // Single Deletion | |
| document.querySelectorAll('.delete-item-btn').forEach(btn => { | |
| btn.addEventListener('click', async (e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| const itemType = btn.dataset.itemType; | |
| const itemId = btn.dataset.id; | |
| if (!confirm(`Are you sure you want to delete this ${itemType}?`)) return; | |
| const url = itemType === 'folder' ? `/delete_folder/${itemId}` : `/delete_generated_pdf/${itemId}`; | |
| const response = await fetch(url, { method: 'DELETE' }); | |
| if (response.ok) { | |
| location.reload(); | |
| } else { | |
| alert('Failed to delete item.'); | |
| } | |
| }); | |
| }); | |
| // Single Rename | |
| document.querySelectorAll('.rename-single-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| const card = btn.closest('.card'); | |
| const titleElement = card.querySelector('.card-title') || card.querySelector('.fw-bold'); | |
| const currentName = titleElement.textContent.trim(); | |
| const itemType = btn.dataset.type || 'pdf'; | |
| const itemId = btn.dataset.id; | |
| titleElement.innerHTML = `<input type="text" class="editable-input" value="${currentName}" />`; | |
| const input = titleElement.querySelector('input'); | |
| input.focus(); | |
| input.select(); | |
| const saveChanges = async () => { | |
| const newName = input.value; | |
| if (newName && newName !== currentName) { | |
| const response = await fetch('/rename_item', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| item_type: itemType, | |
| item_id: itemId, | |
| new_name: newName | |
| }) | |
| }); | |
| if (response.ok) location.reload(); | |
| else { | |
| alert('Failed to rename.'); | |
| titleElement.textContent = currentName; | |
| } | |
| } else { | |
| titleElement.textContent = currentName; | |
| } | |
| }; | |
| input.addEventListener('blur', saveChanges); | |
| input.addEventListener('keydown', ev => { | |
| if (ev.key === 'Enter') { | |
| ev.preventDefault(); | |
| saveChanges(); | |
| } | |
| if (ev.key === 'Escape') titleElement.textContent = currentName; | |
| }); | |
| }); | |
| }); | |
| // Single Edit | |
| document.querySelectorAll('.folder-card').forEach(card => { | |
| card.addEventListener('click', e => { | |
| // If the click target is the folder name link, let the link handle it | |
| if (e.target.closest('.card-title a')) { | |
| return; | |
| } | |
| // Otherwise, toggle the checkbox | |
| const checkbox = card.querySelector('.item-checkbox'); | |
| if (checkbox) { | |
| checkbox.checked = !checkbox.checked; | |
| checkbox.dispatchEvent(new Event('change')); | |
| } | |
| }); | |
| }); | |
| // Make the folder name itself a clickable link | |
| document.querySelectorAll('.folder-card .card-title').forEach(title => { | |
| const card = title.closest('.folder-card'); | |
| const folderPath = card.dataset.path; | |
| title.innerHTML = `<a href="/pdf_manager/browse/${folderPath}" class="text-white text-decoration-none">${title.textContent}</a>`; | |
| }); | |
| document.querySelectorAll('.pdf-card').forEach(card => { | |
| card.addEventListener('click', e => { | |
| // If the click target is the link itself or a child of the link, let the link handle it | |
| if (e.target.closest('a.text-decoration-none')) { | |
| return; | |
| } | |
| // Otherwise, toggle the checkbox | |
| const checkbox = card.querySelector('.item-checkbox'); | |
| if (checkbox) { | |
| checkbox.checked = !checkbox.checked; | |
| checkbox.dispatchEvent(new Event('change')); | |
| } | |
| }); | |
| }); | |
| document.querySelectorAll('.pdf-card .form-check-input').forEach(checkbox => { | |
| checkbox.addEventListener('click', e => { | |
| e.stopPropagation(); // Prevent the card's link from firing | |
| }); | |
| }); | |
| bulkRenameBtn.addEventListener('click', () => { | |
| const selectedItems = getSelectedItems(); | |
| if (selectedItems.length !== 1) return; | |
| const checkbox = selectedItems[0]; | |
| const card = checkbox.closest('.card'); | |
| const titleElement = card.querySelector('.card-title') || card.querySelector('.fw-bold'); | |
| const currentName = titleElement.textContent.trim(); | |
| titleElement.innerHTML = `<input type="text" class="editable-input" value="${currentName}" />`; | |
| const input = titleElement.querySelector('input'); | |
| input.focus(); | |
| input.select(); | |
| const saveChanges = async () => { | |
| const newName = input.value; | |
| if (newName && newName !== currentName) { | |
| const response = await fetch('/rename_item', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| item_type: checkbox.dataset.itemType, | |
| item_id: checkbox.value, | |
| new_name: newName | |
| }) | |
| }); | |
| if (response.ok) { | |
| location.reload(); | |
| } else { | |
| alert('Failed to rename.'); | |
| titleElement.textContent = currentName; | |
| } | |
| } else { | |
| titleElement.textContent = currentName; | |
| } | |
| }; | |
| input.addEventListener('blur', saveChanges); | |
| input.addEventListener('keydown', e => { | |
| if (e.key === 'Enter') { | |
| e.preventDefault(); | |
| saveChanges(); | |
| } | |
| if (e.key === 'Escape') titleElement.textContent = currentName; | |
| }); | |
| }); | |
| bulkDeleteBtn.addEventListener('click', async () => { | |
| const selectedItems = getSelectedItems(); | |
| if (selectedItems.length === 0) return; | |
| if (!confirm(`Are you sure you want to delete ${selectedItems.length} selected item(s)?`)) return; | |
| const foldersToDelete = getSelectedItems('folder').map(cb => cb.value); | |
| const pdfsToDelete = getSelectedItems('pdf').map(cb => cb.value); | |
| let allSucceeded = true; | |
| if (foldersToDelete.length > 0) { | |
| for (const folderId of foldersToDelete) { | |
| const response = await fetch(`/delete_folder/${folderId}`, { method: 'DELETE' }); | |
| if (!response.ok) allSucceeded = false; | |
| } | |
| } | |
| if (pdfsToDelete.length > 0) { | |
| const response = await fetch('/bulk_delete_pdfs', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ ids: pdfsToDelete }) | |
| }); | |
| if (!response.ok) allSucceeded = false; | |
| } | |
| if (allSucceeded) { | |
| location.reload(); | |
| } else { | |
| alert('An error occurred during deletion.'); | |
| } | |
| }); | |
| bulkMergeBtn.addEventListener('click', async () => { | |
| const selectedPdfIds = getSelectedItems('pdf').map(cb => cb.value); | |
| if (selectedPdfIds.length < 2) { | |
| alert('Please select at least two PDFs to merge.'); | |
| return; | |
| } | |
| const response = await fetch('/merge_pdfs', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ pdf_ids: selectedPdfIds }) | |
| }); | |
| if (response.ok) location.reload(); | |
| else alert('Failed to merge PDFs.'); | |
| }); | |
| const editModalEl = document.getElementById('editModal'); | |
| const editModal = new bootstrap.Modal(editModalEl); | |
| let editData = []; | |
| let currentEditIndex = 0; | |
| function initializeTomSelect() { | |
| if (subjectSelect) subjectSelect.destroy(); | |
| if (tagsSelect) tagsSelect.destroy(); | |
| subjectSelect = new TomSelect('#edit-subject',{ create: true, persist: false, options: allSubjects, dropdownParent: 'body' }); | |
| tagsSelect = new TomSelect('#edit-tags',{ persist: false, createOnBlur: true, create: true, plugins: ['remove_button'], options: allTags, dropdownParent: 'body' }); | |
| } | |
| function populateEditModal(index) { | |
| const pdf = editData[index]; | |
| document.getElementById('edit-pdf-id').value = pdf.id; | |
| subjectSelect.setValue(pdf.subject || ''); | |
| tagsSelect.setValue((pdf.tags || '').split(',').map(t => t.trim()).filter(t => t)); | |
| document.getElementById('edit-notes').value = pdf.notes || ''; | |
| document.getElementById('edit-filename').value = pdf.filename || ''; | |
| if (editData.length > 1) { | |
| document.getElementById('edit-carousel-nav').style.display = 'flex'; | |
| document.getElementById('edit-carousel-indicator').textContent = `${index + 1} / ${editData.length}`; | |
| } else { | |
| document.getElementById('edit-carousel-nav').style.display = 'none'; | |
| } | |
| } | |
| bulkEditBtn.addEventListener('click', async () => { | |
| const selectedItems = getSelectedItems('pdf'); | |
| if (selectedItems.length === 0) return; | |
| const ids = selectedItems.map(item => item.value); | |
| await openEditModal(ids); | |
| }); | |
| document.getElementById('edit-prev-btn').addEventListener('click', () => { | |
| if (currentEditIndex > 0) { | |
| currentEditIndex--; | |
| populateEditModal(currentEditIndex); | |
| } | |
| }); | |
| document.getElementById('edit-next-btn').addEventListener('click', () => { | |
| if (currentEditIndex < editData.length - 1) { | |
| currentEditIndex++; | |
| populateEditModal(currentEditIndex); | |
| } | |
| }); | |
| document.getElementById('save-edit-btn').addEventListener('click', async () => { | |
| try { | |
| const pdfId = document.getElementById('edit-pdf-id').value; | |
| const updatedData = { | |
| subject: subjectSelect.getValue(), | |
| tags: tagsSelect.getValue(), | |
| notes: document.getElementById('edit-notes').value, | |
| }; | |
| const response = await fetch(`/update_pdf_details/${pdfId}`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(updatedData) | |
| }); | |
| if (response.ok) { | |
| editData[currentEditIndex] = { ...editData[currentEditIndex], ...updatedData }; | |
| if (currentEditIndex === editData.length - 1) { | |
| editModal.hide(); | |
| location.reload(); | |
| } else { | |
| currentEditIndex++; | |
| populateEditModal(currentEditIndex); | |
| } | |
| } else { | |
| alert(`Failed to save changes: ${(await response.json()).error}`); | |
| } | |
| } catch (error) { | |
| alert(`An error occurred: ${error}`); | |
| } | |
| }); | |
| function renderFolderTree(folders, container, level = 0) { | |
| const ul = document.createElement('ul'); | |
| ul.className = 'list-group'; | |
| if (level > 0) ul.style.display = 'none'; | |
| container.appendChild(ul); | |
| folders.forEach(folder => { | |
| const li = document.createElement('li'); | |
| li.className = 'list-group-item list-group-item-action bg-dark text-white'; | |
| li.style.paddingLeft = `${level * 20 + 12}px`; | |
| li.dataset.folderId = folder.id; | |
| let icon = folder.children && folder.children.length > 0 ? '<i class="bi bi-chevron-right"></i>' : '<i class="bi bi-folder"></i>'; | |
| li.innerHTML = `${icon} ${folder.name}`; | |
| li.addEventListener('click', e => { | |
| e.stopPropagation(); | |
| document.querySelectorAll('#folder-tree-move .list-group-item').forEach(item => item.classList.remove('active')); | |
| li.classList.add('active'); | |
| selectedFolderId = folder.id; | |
| const childUl = li.querySelector('ul'); | |
| if (childUl) { | |
| const iconEl = li.querySelector('i'); | |
| if (childUl.style.display === 'none') { | |
| childUl.style.display = 'block'; | |
| iconEl.classList.replace('bi-chevron-right', 'bi-chevron-down'); | |
| } else { | |
| childUl.style.display = 'none'; | |
| iconEl.classList.replace('bi-chevron-down', 'bi-chevron-right'); | |
| } | |
| } | |
| }); | |
| ul.appendChild(li); | |
| if (folder.children) renderFolderTree(folder.children, li, level + 1); | |
| }); | |
| } | |
| document.getElementById('bulk-move-btn').addEventListener('click', () => { | |
| const folderTreeContainer = document.getElementById('folder-tree-move'); | |
| folderTreeContainer.innerHTML = ''; | |
| const rootItem = document.createElement('a'); | |
| rootItem.href = '#'; | |
| rootItem.className = 'list-group-item list-group-item-action bg-dark text-white'; | |
| rootItem.dataset.folderId = 'null'; | |
| rootItem.innerHTML = `<i class="bi bi-house-door"></i> Root`; | |
| rootItem.addEventListener('click', e => { | |
| e.preventDefault(); | |
| document.querySelectorAll('#folder-tree-move .list-group-item').forEach(item => item.classList.remove('active')); | |
| rootItem.classList.add('active'); | |
| selectedFolderId = null; | |
| }); | |
| folderTreeContainer.appendChild(rootItem); | |
| renderFolderTree(folderTree, folderTreeContainer); | |
| new bootstrap.Modal(document.getElementById('moveModal')).show(); | |
| }); | |
| document.getElementById('create-subfolder-btn').addEventListener('click', async () => { | |
| const newFolderName = document.getElementById('new-subfolder-name').value; | |
| if (!newFolderName) return alert('Please enter a name for the new subfolder.'); | |
| const response = await fetch('/create_folder', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ new_folder_name: newFolderName, parent_id: selectedFolderId }) | |
| }); | |
| if (response.ok) location.reload(); | |
| else alert('Failed to create subfolder.'); | |
| }); | |
| document.getElementById('move-confirm-btn').addEventListener('click', async () => { | |
| const pdfIdsToMove = getSelectedItems('pdf').map(cb => cb.value); | |
| if(pdfIdsToMove.length === 0) return alert("No PDFs selected to move."); | |
| const response = await fetch('/bulk_move_pdfs', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ ids: pdfIdsToMove, target_folder_id: selectedFolderId }) | |
| }); | |
| if (response.ok) location.reload(); | |
| else alert('Failed to move files.'); | |
| }); | |
| document.getElementById('create-folder-btn').addEventListener('click', () => new bootstrap.Modal(document.getElementById('createFolderModal')).show()); | |
| document.getElementById('create-folder-confirm-btn').addEventListener('click', async () => { | |
| const newFolderName = document.getElementById('newFolderName').value; | |
| const response = await fetch('/create_folder', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ new_folder_name: newFolderName, parent_id: currentFolderId }) | |
| }); | |
| if (response.ok) location.reload(); | |
| else alert('Failed to create folder.'); | |
| }); | |
| document.getElementById('bulk-persist-btn').addEventListener('click', async () => { | |
| const selectedIds = getSelectedItems().map(cb => cb.value); | |
| const response = await fetch('/bulk_toggle_persist', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ ids: selectedIds }) | |
| }); | |
| if (response.ok) location.reload(); | |
| else alert('Failed to toggle persistence.'); | |
| }); | |
| document.getElementById('bulk-download-btn').addEventListener('click', async () => { | |
| const selectedIds = getSelectedItems('pdf').map(cb => cb.value); | |
| const response = await fetch('/bulk_download_pdfs', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ ids: selectedIds }) | |
| }); | |
| if (response.ok) { | |
| const blob = await response.blob(); | |
| const url = window.URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.style.display = 'none'; | |
| a.href = url; | |
| a.download = 'pdfs.zip'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| window.URL.revokeObjectURL(url); | |
| } else { | |
| alert('Failed to download PDFs.'); | |
| } | |
| }); | |
| document.getElementById('bulk-print-btn').addEventListener('click', async (e) => { | |
| e.preventDefault(); | |
| const selectedItems = getSelectedItems('pdf'); | |
| if (selectedItems.length === 0) { | |
| alert('Please select at least one PDF to print.'); | |
| return; | |
| } | |
| const formData = new FormData(); | |
| selectedItems.forEach(item => { | |
| formData.append('pdf_ids', item.value); | |
| }); | |
| try { | |
| const response = await fetch('/print_pdfs', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const result = await response.json(); | |
| if (result.success) { | |
| window.open(result.url, '_blank'); | |
| } else { | |
| alert(`Failed to generate PDF for printing: ${result.error}`); | |
| } | |
| } catch (error) { | |
| console.error('Error printing PDFs:', error); | |
| alert('An unexpected error occurred while printing.'); | |
| } | |
| }); | |
| async function openEditModal(ids) { | |
| await fetchSubjectsAndTags(); | |
| editData = []; | |
| for (const id of ids) { | |
| const response = await fetch(`/get_pdf_details/${id}`); | |
| if (response.ok) editData.push(await response.json()); | |
| } | |
| if (editData.length > 0) { | |
| currentEditIndex = 0; | |
| initializeTomSelect(); | |
| populateEditModal(currentEditIndex); | |
| editModal.show(); | |
| } | |
| } | |
| // Single Edit | |
| document.querySelectorAll('.edit-single-btn').forEach(btn => { | |
| btn.addEventListener('click', async (e) => { | |
| e.preventDefault(); | |
| const id = btn.dataset.id; | |
| await openEditModal([id]); | |
| }); | |
| }); | |
| updateBulkButtons(); | |
| const cards = Array.from(document.querySelectorAll('.col')); | |
| let currentRowIndex = -1; | |
| const shortcutsModal = new bootstrap.Modal(document.getElementById('shortcutsModal')); | |
| function highlightRow(index) { | |
| cards.forEach(card => card.querySelector('.card')?.classList.remove('border-primary')); | |
| if (index >= 0 && index < cards.length) { | |
| const card = cards[index].querySelector('.card'); | |
| card.classList.add('border-primary'); | |
| card.scrollIntoView({ block: 'center', behavior: 'smooth' }); | |
| } | |
| } | |
| document.addEventListener('keydown', (e) => { | |
| if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; | |
| const isModalOpen = document.querySelector('.modal.show'); | |
| if (isModalOpen && e.key !== 'Escape') { | |
| if (e.key === '?') shortcutsModal.hide(); | |
| return; | |
| } | |
| switch (e.key) { | |
| case 'j': case 'ArrowDown': | |
| e.preventDefault(); | |
| if (currentRowIndex < cards.length - 1) { | |
| currentRowIndex++; | |
| highlightRow(currentRowIndex); | |
| } | |
| break; | |
| case 'k': case 'ArrowUp': | |
| e.preventDefault(); | |
| if (currentRowIndex > 0) { | |
| currentRowIndex--; | |
| highlightRow(currentRowIndex); | |
| } | |
| break; | |
| case 'x': | |
| e.preventDefault(); | |
| if (currentRowIndex >= 0) { | |
| const checkbox = cards[currentRowIndex].querySelector('.item-checkbox'); | |
| if (checkbox) { | |
| checkbox.checked = !checkbox.checked; | |
| checkbox.dispatchEvent(new Event('change')); | |
| } | |
| } | |
| break; | |
| case 's': | |
| e.preventDefault(); | |
| selectAllCheckbox.checked = !selectAllCheckbox.checked; | |
| selectAllCheckbox.dispatchEvent(new Event('change')); | |
| break; | |
| case 'Enter': | |
| e.preventDefault(); | |
| if (currentRowIndex >= 0) { | |
| const card = cards[currentRowIndex].querySelector('.card'); | |
| if (card.classList.contains('folder-card')) { | |
| window.location.href = `/pdf_manager/browse/${card.dataset.path}`; | |
| } else { | |
| const link = card.querySelector('.card-header a.text-white'); | |
| if (link) link.click(); | |
| } | |
| } | |
| break; | |
| case 'e': | |
| e.preventDefault(); | |
| if (!bulkEditBtn.disabled) bulkEditBtn.click(); | |
| break; | |
| case 'd': case 'Delete': | |
| e.preventDefault(); | |
| if (!bulkDeleteBtn.disabled) bulkDeleteBtn.click(); | |
| break; | |
| case 'p': | |
| e.preventDefault(); | |
| const persistBtn = document.getElementById('bulk-persist-btn'); | |
| if (!persistBtn.disabled) persistBtn.click(); | |
| break; | |
| case '?': | |
| e.preventDefault(); | |
| shortcutsModal.show(); | |
| break; | |
| } | |
| }); | |
| if (cards.length > 0) { | |
| currentRowIndex = 0; | |
| highlightRow(currentRowIndex); | |
| } | |
| fetchSubjectsAndTags(); | |
| }); | |
| </script> | |
| {% endblock %} | |