Report-Generator / templates /drive_manager.html
t
feat: modernize drive sync UI and add public access links
b564de3
{% 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 %}