Report-Generator / templates /pdf_manager.html
t
feat: add PDF viewer selection setting and fix drive copy links
a90d76a
{% 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) !important;
}
.no-caret::after {
display: none !important;
}
.ts-dropdown {
z-index: 1060 !important;
}
</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">&laquo; Previous</button>
<span id="edit-carousel-indicator"></span>
<button class="btn btn-outline-secondary" id="edit-next-btn">Next &raquo;</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>&uarr;</kbd> / <kbd>&darr;</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 %}