Spaces:
Running
Running
| {% extends "base.html" %} | |
| {% block title %}Drive Manager{% endblock %} | |
| {% block styles %} | |
| <style> | |
| /* Modern Drive Manager Styles */ | |
| .drive-header { | |
| background: linear-gradient(135deg, rgba(13,110,253,0.1) 0%, rgba(111,66,193,0.1) 100%); | |
| border-radius: 16px; | |
| padding: 24px 32px; | |
| margin-bottom: 24px; | |
| border: 1px solid var(--border-subtle); | |
| } | |
| .source-card { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border-subtle); | |
| border-radius: 12px; | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| overflow: hidden; | |
| } | |
| .source-card:hover { | |
| transform: translateY(-4px); | |
| border-color: var(--accent-primary); | |
| box-shadow: 0 12px 24px rgba(13, 110, 253, 0.2); | |
| } | |
| .source-card .card-body { | |
| padding: 20px; | |
| } | |
| .source-icon { | |
| width: 48px; | |
| height: 48px; | |
| border-radius: 12px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.5rem; | |
| margin-bottom: 12px; | |
| } | |
| .source-icon.folder { | |
| background: linear-gradient(135deg, rgba(255,193,7,0.2) 0%, rgba(255,152,0,0.2) 100%); | |
| color: #ffc107; | |
| } | |
| .source-icon.file { | |
| background: linear-gradient(135deg, rgba(220,53,69,0.2) 0%, rgba(255,87,34,0.2) 100%); | |
| color: #dc3545; | |
| } | |
| .recent-pdf-card { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border-subtle); | |
| border-radius: 12px; | |
| padding: 16px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| } | |
| .recent-pdf-card:hover { | |
| transform: translateY(-3px); | |
| border-color: var(--accent-primary); | |
| box-shadow: 0 8px 20px rgba(13, 110, 253, 0.25); | |
| } | |
| .my-drive-card { | |
| background: linear-gradient(135deg, rgba(255,193,7,0.1) 0%, rgba(255,152,0,0.05) 100%); | |
| border: 1px solid rgba(255,193,7,0.3); | |
| border-radius: 12px; | |
| padding: 20px 24px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| } | |
| .my-drive-card:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 20px rgba(255,193,7,0.2); | |
| } | |
| .action-btn { | |
| width: 36px; | |
| height: 36px; | |
| border-radius: 8px; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.2s ease; | |
| } | |
| .action-btn:hover { | |
| transform: scale(1.1); | |
| } | |
| .share-link-input { | |
| background: var(--bg-dark); | |
| border: 1px solid var(--border-subtle); | |
| border-radius: 8px; | |
| padding: 8px 12px; | |
| color: var(--text-secondary); | |
| font-family: monospace; | |
| font-size: 0.85rem; | |
| } | |
| .empty-state { | |
| text-align: center; | |
| padding: 60px 20px; | |
| color: var(--text-muted); | |
| } | |
| .empty-state i { | |
| font-size: 4rem; | |
| opacity: 0.3; | |
| margin-bottom: 16px; | |
| } | |
| /* Modal styles */ | |
| .modal-modern .modal-content { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border-subtle); | |
| border-radius: 16px; | |
| } | |
| .modal-modern .modal-header { | |
| border-bottom: 1px solid var(--border-subtle); | |
| padding: 20px 24px; | |
| } | |
| .modal-modern .modal-body { | |
| padding: 24px; | |
| } | |
| .modal-modern .modal-footer { | |
| border-top: 1px solid var(--border-subtle); | |
| padding: 16px 24px; | |
| } | |
| .section-title { | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| color: var(--text-muted); | |
| margin-bottom: 16px; | |
| } | |
| .toast-container { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| z-index: 1100; | |
| } | |
| </style> | |
| {% endblock %} | |
| {% block content %} | |
| <div class="container-fluid px-4 py-4"> | |
| <!-- Header --> | |
| <div class="drive-header d-flex justify-content-between align-items-center flex-wrap gap-3"> | |
| <div> | |
| <h1 class="h3 mb-1"><i class="bi bi-cloud-fill me-2"></i>Drive Sync Manager</h1> | |
| <p class="text-muted mb-0 small">Sync and manage Google Drive folders locally</p> | |
| </div> | |
| <div class="d-flex gap-2"> | |
| {% if not drive_connected %} | |
| <a href="/drive/connect" class="btn btn-outline-warning"> | |
| <i class="bi bi-google me-2"></i>Connect Drive | |
| </a> | |
| {% else %} | |
| <span class="badge bg-success d-flex align-items-center px-3 py-2"> | |
| <i class="bi bi-check-circle me-1"></i>Drive Connected | |
| </span> | |
| {% endif %} | |
| <button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addSourceModal"> | |
| <i class="bi bi-plus-lg me-2"></i>Add Source | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Recently Opened PDFs --> | |
| {% if recent_pdfs %} | |
| <div class="mb-4"> | |
| <div class="section-title"><i class="bi bi-clock-history me-2"></i>Recently Opened</div> | |
| <div class="row row-cols-1 row-cols-md-2 row-cols-lg-4 g-3"> | |
| {% for pdf in recent_pdfs %} | |
| <div class="col"> | |
| <div class="recent-pdf-card" onclick="location.href='/drive/api/open/{{ pdf.file_id }}'"> | |
| <div class="d-flex align-items-start gap-3"> | |
| <div class="source-icon file flex-shrink-0" style="width:40px;height:40px;font-size:1.2rem;"> | |
| <i class="bi bi-file-pdf-fill"></i> | |
| </div> | |
| <div class="flex-grow-1 min-w-0"> | |
| <h6 class="mb-1 text-truncate" title="{{ pdf.filename }}">{{ pdf.filename }}</h6> | |
| <small class="text-muted"><i class="bi bi-clock me-1"></i>{{ pdf.opened_at }}</small> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {% endfor %} | |
| </div> | |
| </div> | |
| {% endif %} | |
| <!-- My Drive Quick Access --> | |
| {% if drive_connected %} | |
| <div class="mb-4"> | |
| <div class="my-drive-card d-flex justify-content-between align-items-center" onclick="location.href='/drive/api/browse/root'"> | |
| <div class="d-flex align-items-center gap-3"> | |
| <div class="source-icon folder"> | |
| <i class="bi bi-google"></i> | |
| </div> | |
| <div> | |
| <h5 class="mb-0 text-warning">My Drive</h5> | |
| <small class="text-muted">Browse your personal Google Drive</small> | |
| </div> | |
| </div> | |
| <i class="bi bi-chevron-right text-muted fs-4"></i> | |
| </div> | |
| </div> | |
| {% endif %} | |
| <!-- Drive Sources --> | |
| <div class="section-title"><i class="bi bi-hdd-network me-2"></i>Synced Sources</div> | |
| <div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4" id="sources-grid"> | |
| {% for source in sources %} | |
| <div class="col"> | |
| <div class="source-card h-100"> | |
| <div class="card-body"> | |
| <div class="d-flex justify-content-between align-items-start mb-3"> | |
| <div class="source-icon {{ 'file' if source.source_type == 'file' else 'folder' }}"> | |
| <i class="bi {{ 'bi-file-earmark-arrow-down' if source.source_type == 'file' else 'bi-folder-fill' }}"></i> | |
| </div> | |
| <div class="dropdown"> | |
| <button class="btn btn-link text-muted p-0" data-bs-toggle="dropdown"> | |
| <i class="bi bi-three-dots-vertical fs-5"></i> | |
| </button> | |
| <ul class="dropdown-menu dropdown-menu-dark dropdown-menu-end"> | |
| <li><button class="dropdown-item sync-btn" data-id="{{ source.id }}"><i class="bi bi-arrow-repeat me-2"></i>Sync Now</button></li> | |
| <li><button class="dropdown-item" onclick="showShareModal({{ source.id }}, '{{ source.name }}')"><i class="bi bi-share me-2"></i>Get Share Links</button></li> | |
| <li><hr class="dropdown-divider"></li> | |
| <li><button class="dropdown-item text-danger delete-btn" data-id="{{ source.id }}"><i class="bi bi-trash me-2"></i>Delete</button></li> | |
| </ul> | |
| </div> | |
| </div> | |
| <div onclick="location.href='/drive/browse/{{ source.id }}'" style="cursor: pointer;"> | |
| <h5 class="mb-2">{{ source.name }}</h5> | |
| <p class="text-muted small text-truncate mb-3" title="{{ source.url }}">{{ source.url }}</p> | |
| <div class="d-flex justify-content-between align-items-center"> | |
| <small class="text-secondary"> | |
| <i class="bi bi-clock me-1"></i>{{ source.last_synced or 'Never synced' }} | |
| </small> | |
| <span class="badge bg-{{ 'success' if source.last_synced else 'secondary' }}"> | |
| {{ 'Synced' if source.last_synced else 'Pending' }} | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {% else %} | |
| <div class="col-12"> | |
| <div class="empty-state"> | |
| <i class="bi bi-cloud-slash d-block"></i> | |
| <h5>No drive sources added yet</h5> | |
| <p class="text-muted">Add a public Google Drive folder to get started</p> | |
| <button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addSourceModal"> | |
| <i class="bi bi-plus-lg me-2"></i>Add Source | |
| </button> | |
| </div> | |
| </div> | |
| {% endfor %} | |
| </div> | |
| </div> | |
| <!-- Add Source Modal --> | |
| <div class="modal fade modal-modern" id="addSourceModal" tabindex="-1"> | |
| <div class="modal-dialog"> | |
| <div class="modal-content text-white"> | |
| <div class="modal-header"> | |
| <h5 class="modal-title"><i class="bi bi-plus-circle me-2"></i>Add Drive Source</h5> | |
| <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button> | |
| </div> | |
| <div class="modal-body"> | |
| <form id="add-source-form"> | |
| <div class="mb-3"> | |
| <label class="form-label">Source Name</label> | |
| <input type="text" class="form-control" name="name" required placeholder="e.g. Question Papers 2024"> | |
| <div class="form-text">This will be the local folder name</div> | |
| </div> | |
| <div class="mb-3"> | |
| <label class="form-label">Google Drive URL</label> | |
| <input type="url" class="form-control" name="url" required placeholder="https://drive.google.com/drive/folders/..."> | |
| <div class="form-text">Must be a publicly accessible folder or file link</div> | |
| </div> | |
| </form> | |
| </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="save-source-btn"> | |
| <i class="bi bi-plus-lg me-1"></i>Add Source | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Share Links Modal --> | |
| <div class="modal fade modal-modern" id="shareModal" tabindex="-1"> | |
| <div class="modal-dialog modal-lg"> | |
| <div class="modal-content text-white"> | |
| <div class="modal-header"> | |
| <h5 class="modal-title"><i class="bi bi-share me-2"></i>Share Links</h5> | |
| <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button> | |
| </div> | |
| <div class="modal-body"> | |
| <p class="text-muted mb-4">Generate public links for sharing files without authentication</p> | |
| <!-- Direct PDF Link --> | |
| <div class="mb-4"> | |
| <label class="form-label fw-semibold"><i class="bi bi-file-pdf me-2"></i>Direct PDF Download Link</label> | |
| <p class="text-muted small mb-2">Anyone with this link can download PDFs directly</p> | |
| <div class="input-group"> | |
| <input type="text" class="form-control share-link-input" id="share-pdf-link" readonly> | |
| <button class="btn btn-outline-primary" onclick="copyLink('share-pdf-link')"> | |
| <i class="bi bi-clipboard"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Directory Listing Link --> | |
| <div class="mb-4"> | |
| <label class="form-label fw-semibold"><i class="bi bi-list-ul me-2"></i>Directory Listing (curl-friendly)</label> | |
| <p class="text-muted small mb-2">Returns a plain text list of all files - perfect for scripts</p> | |
| <div class="input-group"> | |
| <input type="text" class="form-control share-link-input" id="share-list-link" readonly> | |
| <button class="btn btn-outline-primary" onclick="copyLink('share-list-link')"> | |
| <i class="bi bi-clipboard"></i> | |
| </button> | |
| </div> | |
| <div class="mt-2"> | |
| <code class="small text-info">curl <span id="curl-example"></span></code> | |
| </div> | |
| </div> | |
| <!-- Access Token Info --> | |
| <div class="alert alert-warning border-0 small"> | |
| <i class="bi bi-shield-exclamation me-2"></i> | |
| These links include an access token. Share carefully - anyone with the link can access the files. | |
| </div> | |
| </div> | |
| <div class="modal-footer"> | |
| <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Toast Container --> | |
| <div class="toast-container"> | |
| <div class="toast align-items-center text-bg-success border-0" id="copyToast" role="alert"> | |
| <div class="d-flex"> | |
| <div class="toast-body"> | |
| <i class="bi bi-check-circle me-2"></i>Link copied to clipboard! | |
| </div> | |
| <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button> | |
| </div> | |
| </div> | |
| </div> | |
| {% endblock %} | |
| {% block scripts %} | |
| <script> | |
| // Add Source | |
| document.getElementById('save-source-btn').addEventListener('click', async () => { | |
| const form = document.getElementById('add-source-form'); | |
| if (!form.checkValidity()) { | |
| form.reportValidity(); | |
| return; | |
| } | |
| const formData = new FormData(form); | |
| try { | |
| const res = await fetch('/drive/add', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const data = await res.json(); | |
| if (data.success) { | |
| location.reload(); | |
| } else { | |
| alert('Error: ' + data.error); | |
| } | |
| } catch (e) { | |
| alert('Error adding source'); | |
| } | |
| }); | |
| // Sync | |
| document.querySelectorAll('.sync-btn').forEach(btn => { | |
| btn.addEventListener('click', async (e) => { | |
| e.stopPropagation(); | |
| const id = btn.dataset.id; | |
| btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Starting...'; | |
| btn.disabled = true; | |
| try { | |
| const res = await fetch(`/drive/sync/${id}`, { method: 'POST' }); | |
| const data = await res.json(); | |
| if (data.success) { | |
| showToast('Sync started in background'); | |
| } else { | |
| alert('Error: ' + data.error); | |
| } | |
| } catch (e) { | |
| alert('Request failed'); | |
| } finally { | |
| btn.innerHTML = '<i class="bi bi-arrow-repeat me-2"></i>Sync Now'; | |
| btn.disabled = false; | |
| } | |
| }); | |
| }); | |
| // Delete | |
| document.querySelectorAll('.delete-btn').forEach(btn => { | |
| btn.addEventListener('click', async (e) => { | |
| e.stopPropagation(); | |
| const id = btn.dataset.id; | |
| if (!confirm('Delete this source and all downloaded files?')) return; | |
| try { | |
| const res = await fetch(`/drive/delete/${id}`, { method: 'POST' }); | |
| const data = await res.json(); | |
| if (data.success) location.reload(); | |
| else alert('Error: ' + data.error); | |
| } catch (e) { | |
| alert('Request failed'); | |
| } | |
| }); | |
| }); | |
| // Share Modal | |
| async function showShareModal(sourceId, sourceName) { | |
| try { | |
| // Get the public access token | |
| const res = await fetch(`/drive/public/${sourceId}/token`); | |
| const data = await res.json(); | |
| if (!data.token) { | |
| alert('Error getting share token'); | |
| return; | |
| } | |
| const baseUrl = window.location.origin; | |
| const token = data.token; | |
| const pdfLink = `${baseUrl}/drive/public/${sourceId}/download?token=${token}`; | |
| const listLink = `${baseUrl}/drive/public/${sourceId}/list.txt?token=${token}`; | |
| document.getElementById('share-pdf-link').value = pdfLink; | |
| document.getElementById('share-list-link').value = listLink; | |
| document.getElementById('curl-example').textContent = listLink; | |
| new bootstrap.Modal(document.getElementById('shareModal')).show(); | |
| } catch (e) { | |
| alert('Error: ' + e.message); | |
| } | |
| } | |
| // Copy Link | |
| function copyLink(inputId) { | |
| const input = document.getElementById(inputId); | |
| input.select(); | |
| navigator.clipboard.writeText(input.value); | |
| showToast('Link copied to clipboard!'); | |
| } | |
| // Toast | |
| function showToast(message) { | |
| const toast = document.getElementById('copyToast'); | |
| toast.querySelector('.toast-body').innerHTML = `<i class="bi bi-check-circle me-2"></i>${message}`; | |
| new bootstrap.Toast(toast).show(); | |
| } | |
| </script> | |
| {% endblock %} | |