Report-Generator / templates /drive_browser.html
t
feat: add public token links for Drive API files and breadcrumb navigation
abda79a
{% extends "base.html" %}
{% block title %}{{ source.name }} - Drive Browser{% endblock %}
{% block styles %}
<style>
/* Modern Drive Browser Styles */
.browser-header {
background: var(--bg-card);
border: 1px solid var(--border-subtle);
border-radius: 12px;
padding: 16px 20px;
margin-bottom: 20px;
}
.breadcrumb {
margin-bottom: 0;
padding: 0;
background: transparent;
}
.breadcrumb-item a {
color: var(--accent-primary);
text-decoration: none;
}
.breadcrumb-item a:hover {
text-decoration: underline;
}
.breadcrumb-item.active {
color: var(--text-primary);
}
.view-toggle .btn {
padding: 8px 12px;
border-radius: 8px;
}
.view-toggle .btn.active {
background: var(--accent-primary);
border-color: var(--accent-primary);
color: white;
}
/* File Cards */
.file-card {
background: var(--bg-card);
border: 1px solid var(--border-subtle);
border-radius: 12px;
transition: all 0.2s ease;
cursor: pointer;
overflow: hidden;
}
.file-card:hover {
transform: translateY(-4px);
border-color: var(--accent-primary);
box-shadow: 0 8px 20px rgba(13, 110, 253, 0.2);
}
.file-card .card-body {
padding: 20px;
text-align: center;
position: relative;
}
.file-card .copy-btn {
position: absolute;
top: 8px;
right: 8px;
width: 28px;
height: 28px;
border-radius: 6px;
background: rgba(255,255,255,0.1);
border: none;
color: var(--text-muted);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: all 0.2s ease;
z-index: 10;
}
.file-card:hover .copy-btn {
opacity: 1;
}
.file-card .copy-btn:hover {
background: var(--accent-primary);
color: white;
transform: scale(1.1);
}
.file-card .copy-btn.copied {
background: #198754;
color: white;
}
.file-icon {
width: 64px;
height: 64px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
margin: 0 auto 12px;
}
.file-icon.folder {
background: linear-gradient(135deg, rgba(255,193,7,0.2) 0%, rgba(255,152,0,0.2) 100%);
color: #ffc107;
}
.file-icon.pdf {
background: linear-gradient(135deg, rgba(220,53,69,0.2) 0%, rgba(255,87,34,0.2) 100%);
color: #dc3545;
}
.file-icon.image {
background: linear-gradient(135deg, rgba(13,202,240,0.2) 0%, rgba(32,201,151,0.2) 100%);
color: #0dcaf0;
}
.file-icon.file {
background: linear-gradient(135deg, rgba(108,117,125,0.2) 0%, rgba(73,80,87,0.2) 100%);
color: #6c757d;
}
.file-icon.slides {
background: linear-gradient(135deg, rgba(251,188,5,0.2) 0%, rgba(234,88,12,0.2) 100%);
color: #f59e0b;
}
.file-icon.sheet {
background: linear-gradient(135deg, rgba(34,197,94,0.2) 0%, rgba(22,163,74,0.2) 100%);
color: #22c55e;
}
.file-icon.doc {
background: linear-gradient(135deg, rgba(59,130,246,0.2) 0%, rgba(37,99,235,0.2) 100%);
color: #3b82f6;
}
.file-icon.video {
background: linear-gradient(135deg, rgba(168,85,247,0.2) 0%, rgba(139,92,246,0.2) 100%);
color: #a855f7;
}
.file-name {
font-size: 0.9rem;
font-weight: 500;
word-break: break-word;
line-height: 1.3;
}
/* List View */
.list-view {
display: none;
}
.list-view.active {
display: block;
}
.grid-view {
display: flex;
}
.grid-view.hidden {
display: none !important;
}
.file-row {
background: var(--bg-card);
border: 1px solid var(--border-subtle);
border-radius: 8px;
margin-bottom: 8px;
padding: 12px 16px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 12px;
}
.file-row:hover {
background: rgba(13, 110, 253, 0.1);
border-color: var(--accent-primary);
}
.file-row .file-icon {
width: 40px;
height: 40px;
font-size: 1.2rem;
margin: 0;
flex-shrink: 0;
}
.file-row .file-name {
flex-grow: 1;
text-align: left;
}
.file-row .copy-btn {
width: 32px;
height: 32px;
border-radius: 6px;
background: rgba(255,255,255,0.1);
border: none;
color: var(--text-muted);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
flex-shrink: 0;
}
.file-row .copy-btn:hover {
background: var(--accent-primary);
color: white;
}
.file-row .copy-btn.copied {
background: #198754;
color: white;
}
/* Action buttons for files */
.file-actions {
display: flex;
gap: 4px;
flex-shrink: 0;
}
.action-btn-sm {
width: 28px;
height: 28px;
border-radius: 6px;
background: rgba(255,255,255,0.1);
border: none;
color: var(--text-muted);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
font-size: 0.85rem;
}
.action-btn-sm:hover {
background: var(--accent-primary);
color: white;
}
.action-btn-sm.google-btn:hover {
background: #4285f4;
}
/* Loading more indicator */
.loading-more {
display: none;
text-align: center;
padding: 20px;
color: var(--text-muted);
}
.loading-more.active {
display: block;
}
.loading-more .spinner-border {
width: 1.5rem;
height: 1.5rem;
}
/* Skeleton loader */
.skeleton-card {
background: var(--bg-card);
border: 1px solid var(--border-subtle);
border-radius: 12px;
padding: 20px;
text-align: center;
}
.skeleton-icon {
width: 64px;
height: 64px;
border-radius: 16px;
background: linear-gradient(90deg, var(--bg-elevated) 25%, var(--bg-card) 50%, var(--bg-elevated) 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
margin: 0 auto 12px;
}
.skeleton-text {
height: 14px;
border-radius: 4px;
background: linear-gradient(90deg, var(--bg-elevated) 25%, var(--bg-card) 50%, var(--bg-elevated) 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
width: 70%;
margin: 0 auto;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* Empty State */
.empty-state {
text-align: center;
padding: 80px 20px;
color: var(--text-muted);
}
.empty-state i {
font-size: 4rem;
opacity: 0.3;
margin-bottom: 16px;
}
/* Loader */
.loader-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.8);
z-index: 9999;
align-items: center;
justify-content: center;
flex-direction: column;
color: white;
}
.loader-overlay.active {
display: flex;
}
.selection-bar {
background: var(--bg-card);
border: 1px solid var(--border-subtle);
border-radius: 8px;
padding: 8px 16px;
margin-bottom: 16px;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid px-4 py-4">
<!-- Header with Breadcrumb -->
<div class="browser-header d-flex justify-content-between align-items-center flex-wrap gap-3">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/drive_manager"><i class="bi bi-cloud me-1"></i>Drives</a></li>
{% if is_api %}
<li class="breadcrumb-item"><a href="/drive/api/browse/root?title=My Drive">My Drive</a></li>
{% for crumb in breadcrumbs %}
<li class="breadcrumb-item"><a href="/drive/api/browse/{{ crumb.id }}?title={{ crumb.name }}&breadcrumbs={{ crumb.trail | tojson | urlencode }}">{{ crumb.name }}</a></li>
{% endfor %}
{% if folder_id != 'root' and not breadcrumbs %}
<li class="breadcrumb-item active">{{ source.name }}</li>
{% endif %}
{% else %}
<li class="breadcrumb-item"><a href="/drive/browse/{{ source.id }}">{{ source.name }}</a></li>
{% for crumb in breadcrumbs %}
<li class="breadcrumb-item active">{{ crumb.name }}</li>
{% endfor %}
{% endif %}
</ol>
</nav>
<div class="d-flex gap-2 align-items-center">
<div class="btn-group view-toggle" role="group">
<button type="button" class="btn btn-outline-secondary active" id="btn-grid" onclick="switchView('grid')">
<i class="bi bi-grid-3x3-gap-fill"></i>
</button>
<button type="button" class="btn btn-outline-secondary" id="btn-list" onclick="switchView('list')">
<i class="bi bi-list"></i>
</button>
</div>
</div>
</div>
<!-- Selection Bar -->
<div class="selection-bar d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center gap-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="select-all">
<label class="form-check-label small" for="select-all">Select All</label>
</div>
<span class="text-muted small"><span id="item-count">{{ items|length }}</span> items</span>
</div>
<div id="bulk-actions" style="display: none;">
<span class="text-muted me-2 small"><span id="selected-count">0</span> selected</span>
</div>
</div>
<!-- Grid View -->
<div class="row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-lg-5 row-cols-xl-6 g-3 grid-view" id="grid-view">
{% for item in items %}
{% if item.type == 'folder' %}
{% set link = '/drive/api/browse/' + item.path if is_api else '/drive/browse/' + source.id|string + '/' + item.path %}
{% set raw_link = '' %}
{% set icon_class = 'folder' %}
{% set icon = 'bi-folder-fill' %}
{% set google_link = '' %}
{% elif item.type == 'pdf' %}
{% set link = '/drive/api/open/' + item.path if is_api else '/drive/file/' + source.id|string + '/' + item.path %}
{% set raw_link = '/drive/api/download/' + item.path if is_api else '/drive/raw/' + source.id|string + '/' + item.path %}
{% set icon_class = 'pdf' %}
{% set icon = 'bi-file-earmark-pdf-fill' %}
{% set google_link = '' %}
{% elif item.type == 'image' %}
{% set link = '/drive/api/open/' + item.path if is_api else '/drive/file/' + source.id|string + '/' + item.path %}
{% set raw_link = '/drive/api/download/' + item.path if is_api else '/drive/raw/' + source.id|string + '/' + item.path %}
{% set icon_class = 'image' %}
{% set icon = 'bi-file-earmark-image-fill' %}
{% set google_link = '' %}
{% elif item.type == 'slides' %}
{% set link = '/drive/api/open/' + item.path if is_api else '/drive/file/' + source.id|string + '/' + item.path %}
{% set raw_link = '' %}
{% set icon_class = 'slides' %}
{% set icon = 'bi-file-earmark-slides-fill' %}
{% set google_link = 'https://docs.google.com/presentation/d/' + item.path + '/edit' if is_api else '' %}
{% elif item.type == 'sheet' %}
{% set link = '/drive/api/open/' + item.path if is_api else '/drive/file/' + source.id|string + '/' + item.path %}
{% set raw_link = '' %}
{% set icon_class = 'sheet' %}
{% set icon = 'bi-file-earmark-spreadsheet-fill' %}
{% set google_link = 'https://docs.google.com/spreadsheets/d/' + item.path + '/edit' if is_api else '' %}
{% elif item.type == 'doc' %}
{% set link = '/drive/api/open/' + item.path if is_api else '/drive/file/' + source.id|string + '/' + item.path %}
{% set raw_link = '' %}
{% set icon_class = 'doc' %}
{% set icon = 'bi-file-earmark-word-fill' %}
{% set google_link = 'https://docs.google.com/document/d/' + item.path + '/edit' if is_api else '' %}
{% elif item.type == 'video' %}
{% set link = '/drive/api/open/' + item.path if is_api else '/drive/file/' + source.id|string + '/' + item.path %}
{% set raw_link = '' %}
{% set icon_class = 'video' %}
{% set icon = 'bi-file-earmark-play-fill' %}
{% set google_link = '' %}
{% else %}
{% set link = '/drive/api/open/' + item.path if is_api else '/drive/file/' + source.id|string + '/' + item.path %}
{% set raw_link = '/drive/api/download/' + item.path if is_api else '/drive/raw/' + source.id|string + '/' + item.path %}
{% set icon_class = 'file' %}
{% set icon = 'bi-file-earmark-fill' %}
{% set google_link = '' %}
{% endif %}
<div class="col">
<div class="file-card" onclick="navigate('{{ link }}')" data-file-type="{{ item.type }}">
<div class="card-body">
{% if item.type != 'folder' %}
<div class="file-actions" style="position: absolute; top: 8px; right: 8px; opacity: 0; transition: opacity 0.2s;">
{% if raw_link %}
<button class="action-btn-sm" onclick="event.stopPropagation(); copyFileLink('{{ raw_link }}', this)" title="Copy download link">
<i class="bi bi-link-45deg"></i>
</button>
{% endif %}
{% if google_link %}
<a href="{{ google_link }}" target="_blank" class="action-btn-sm google-btn" onclick="event.stopPropagation()" title="Open in Google">
<i class="bi bi-google"></i>
</a>
{% endif %}
</div>
{% endif %}
<div class="file-icon {{ icon_class }}">
<i class="bi {{ icon }}"></i>
</div>
<div class="file-name text-truncate" title="{{ item.name }}">{{ item.name }}</div>
</div>
</div>
</div>
{% else %}
<div class="col-12">
<div class="empty-state">
<i class="bi bi-folder2-open d-block"></i>
<h5>Empty Folder</h5>
<p class="text-muted">This folder is empty or not synced yet</p>
<a href="/drive_manager" class="btn btn-outline-primary">
<i class="bi bi-arrow-left me-1"></i>Back to Drive Manager
</a>
</div>
</div>
{% endfor %}
</div>
<!-- List View -->
<div class="list-view" id="list-view">
{% for item in items %}
{% if item.type == 'folder' %}
{% set link = '/drive/api/browse/' + item.path if is_api else '/drive/browse/' + source.id|string + '/' + item.path %}
{% set raw_link = '' %}
{% set icon_class = 'folder' %}
{% set icon = 'bi-folder-fill' %}
{% set google_link = '' %}
{% elif item.type == 'pdf' %}
{% set link = '/drive/api/open/' + item.path if is_api else '/drive/file/' + source.id|string + '/' + item.path %}
{% set raw_link = '/drive/api/download/' + item.path if is_api else '/drive/raw/' + source.id|string + '/' + item.path %}
{% set icon_class = 'pdf' %}
{% set icon = 'bi-file-earmark-pdf-fill' %}
{% set google_link = '' %}
{% elif item.type == 'image' %}
{% set link = '/drive/api/open/' + item.path if is_api else '/drive/file/' + source.id|string + '/' + item.path %}
{% set raw_link = '/drive/api/download/' + item.path if is_api else '/drive/raw/' + source.id|string + '/' + item.path %}
{% set icon_class = 'image' %}
{% set icon = 'bi-file-earmark-image-fill' %}
{% set google_link = '' %}
{% elif item.type == 'slides' %}
{% set link = '/drive/api/open/' + item.path if is_api else '/drive/file/' + source.id|string + '/' + item.path %}
{% set raw_link = '' %}
{% set icon_class = 'slides' %}
{% set icon = 'bi-file-earmark-slides-fill' %}
{% set google_link = 'https://docs.google.com/presentation/d/' + item.path + '/edit' if is_api else '' %}
{% elif item.type == 'sheet' %}
{% set link = '/drive/api/open/' + item.path if is_api else '/drive/file/' + source.id|string + '/' + item.path %}
{% set raw_link = '' %}
{% set icon_class = 'sheet' %}
{% set icon = 'bi-file-earmark-spreadsheet-fill' %}
{% set google_link = 'https://docs.google.com/spreadsheets/d/' + item.path + '/edit' if is_api else '' %}
{% elif item.type == 'doc' %}
{% set link = '/drive/api/open/' + item.path if is_api else '/drive/file/' + source.id|string + '/' + item.path %}
{% set raw_link = '' %}
{% set icon_class = 'doc' %}
{% set icon = 'bi-file-earmark-word-fill' %}
{% set google_link = 'https://docs.google.com/document/d/' + item.path + '/edit' if is_api else '' %}
{% elif item.type == 'video' %}
{% set link = '/drive/api/open/' + item.path if is_api else '/drive/file/' + source.id|string + '/' + item.path %}
{% set raw_link = '' %}
{% set icon_class = 'video' %}
{% set icon = 'bi-file-earmark-play-fill' %}
{% set google_link = '' %}
{% else %}
{% set link = '/drive/api/open/' + item.path if is_api else '/drive/file/' + source.id|string + '/' + item.path %}
{% set raw_link = '/drive/api/download/' + item.path if is_api else '/drive/raw/' + source.id|string + '/' + item.path %}
{% set icon_class = 'file' %}
{% set icon = 'bi-file-earmark-fill' %}
{% set google_link = '' %}
{% endif %}
<div class="file-row" onclick="navigate('{{ link }}')">
<div class="file-icon {{ icon_class }}">
<i class="bi {{ icon }}"></i>
</div>
<div class="file-name">{{ item.name }}</div>
<span class="badge bg-secondary">{{ item.type }}</span>
{% if item.type != 'folder' %}
<div class="file-actions">
{% if raw_link %}
<button class="action-btn-sm" onclick="event.stopPropagation(); copyFileLink('{{ raw_link }}', this)" title="Copy download link">
<i class="bi bi-link-45deg"></i>
</button>
{% endif %}
{% if google_link %}
<a href="{{ google_link }}" target="_blank" class="action-btn-sm google-btn" onclick="event.stopPropagation()" title="Open in Google">
<i class="bi bi-google"></i>
</a>
{% endif %}
</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
<!-- Loading More Indicator -->
<div class="loading-more" id="loadingMore">
<div class="spinner-border text-primary" role="status"></div>
<div class="mt-2">Loading more files...</div>
</div>
<!-- Loader Overlay -->
<div class="loader-overlay" id="loader">
<div class="spinner-border text-primary mb-3" style="width: 3rem; height: 3rem;"></div>
<div class="fs-5">Opening file...</div>
</div>
<!-- Toast -->
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 1100">
<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!
</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>
// Configuration
const CONFIG = {
isApi: {{ 'true' if is_api else 'false' }},
isLocal: {{ 'true' if is_local is defined and is_local else 'false' }},
lazyLoad: {{ 'true' if lazy_load is defined and lazy_load else 'false' }},
sourceId: '{{ source.id }}',
folderId: '{{ folder_id if folder_id is defined else "" }}',
sourceName: '{{ source.name }}',
subpath: '{{ current_subpath if current_subpath is defined else "" }}',
breadcrumbs: {{ breadcrumbs | tojson }}
};
// Lazy loading state
const lazyState = {
nextToken: null,
offset: 0,
loading: false,
hasMore: true,
itemCount: 0
};
// Build folder URL with breadcrumb tracking for API
function buildFolderUrl(folderId, folderName) {
if (CONFIG.isApi) {
// Build new breadcrumb trail
const newTrail = [...CONFIG.breadcrumbs, { id: CONFIG.folderId, name: CONFIG.sourceName }];
const breadcrumbsParam = encodeURIComponent(JSON.stringify(newTrail));
return `/drive/api/browse/${folderId}?title=${encodeURIComponent(folderName)}&breadcrumbs=${breadcrumbsParam}`;
}
return `/drive/browse/${CONFIG.sourceId}/${folderId}`;
}
// Navigation with loader
function navigate(url) {
document.getElementById('loader').classList.add('active');
window.location.href = url;
}
// Hide loader on back navigation
window.addEventListener('pageshow', () => {
document.getElementById('loader').classList.remove('active');
});
// View switching
function switchView(view) {
const gridView = document.getElementById('grid-view');
const listView = document.getElementById('list-view');
const btnGrid = document.getElementById('btn-grid');
const btnList = document.getElementById('btn-list');
if (view === 'list') {
gridView.classList.add('hidden');
listView.classList.add('active');
btnList.classList.add('active');
btnGrid.classList.remove('active');
} else {
gridView.classList.remove('hidden');
listView.classList.remove('active');
btnGrid.classList.add('active');
btnList.classList.remove('active');
}
localStorage.setItem('driveViewMode', view);
}
// Copy file link - for API files, get public token first
async function copyFileLink(path, btn) {
let urlToCopy = window.location.origin + path;
// For API files, get the public token URL
if (CONFIG.isApi && path.includes('/drive/api/download/')) {
const fileId = path.split('/drive/api/download/')[1];
try {
const response = await fetch(`/drive/api/token/${fileId}`);
if (response.ok) {
const data = await response.json();
urlToCopy = data.public_url;
}
} catch (e) {
console.error('Failed to get public token:', e);
}
}
navigator.clipboard.writeText(urlToCopy).then(() => {
const originalHtml = btn.innerHTML;
btn.innerHTML = '<i class="bi bi-check"></i>';
btn.style.background = '#198754';
btn.style.color = 'white';
const toast = document.getElementById('copyToast');
new bootstrap.Toast(toast).show();
setTimeout(() => {
btn.innerHTML = originalHtml;
btn.style.background = '';
btn.style.color = '';
}, 2000);
});
}
// Create file card HTML
function createFileCard(item) {
const typeConfig = {
folder: { icon: 'bi-folder-fill', class: 'folder' },
pdf: { icon: 'bi-file-earmark-pdf-fill', class: 'pdf' },
image: { icon: 'bi-file-earmark-image-fill', class: 'image' },
slides: { icon: 'bi-file-earmark-slides-fill', class: 'slides' },
sheet: { icon: 'bi-file-earmark-spreadsheet-fill', class: 'sheet' },
doc: { icon: 'bi-file-earmark-word-fill', class: 'doc' },
video: { icon: 'bi-file-earmark-play-fill', class: 'video' },
file: { icon: 'bi-file-earmark-fill', class: 'file' }
};
const cfg = typeConfig[item.type] || typeConfig.file;
let link, rawLink = '', googleLink = '';
if (CONFIG.isApi) {
link = item.type === 'folder'
? buildFolderUrl(item.path, item.name)
: `/drive/api/open/${item.path}`;
// For API files, use download endpoint for copy link
if (item.type !== 'folder' && !['slides', 'sheet', 'doc', 'video'].includes(item.type)) {
rawLink = `/drive/api/download/${item.path}`;
}
if (item.type === 'slides') googleLink = `https://docs.google.com/presentation/d/${item.path}/edit`;
else if (item.type === 'sheet') googleLink = `https://docs.google.com/spreadsheets/d/${item.path}/edit`;
else if (item.type === 'doc') googleLink = `https://docs.google.com/document/d/${item.path}/edit`;
} else {
link = item.type === 'folder'
? `/drive/browse/${CONFIG.sourceId}/${item.path}`
: `/drive/file/${CONFIG.sourceId}/${item.path}`;
if (item.type !== 'folder' && !['slides', 'sheet', 'doc', 'video'].includes(item.type)) {
rawLink = `/drive/raw/${CONFIG.sourceId}/${item.path}`;
}
}
let actionsHtml = '';
if (item.type !== 'folder') {
actionsHtml = '<div class="file-actions" style="position: absolute; top: 8px; right: 8px; opacity: 0; transition: opacity 0.2s;">';
if (rawLink) {
actionsHtml += `<button class="action-btn-sm" onclick="event.stopPropagation(); copyFileLink('${rawLink}', this)" title="Copy download link"><i class="bi bi-link-45deg"></i></button>`;
}
if (googleLink) {
actionsHtml += `<a href="${googleLink}" target="_blank" class="action-btn-sm google-btn" onclick="event.stopPropagation()" title="Open in Google"><i class="bi bi-google"></i></a>`;
}
actionsHtml += '</div>';
}
return `
<div class="col">
<div class="file-card" onclick="navigate('${link}')" data-file-type="${item.type}">
<div class="card-body">
${actionsHtml}
<div class="file-icon ${cfg.class}">
<i class="bi ${cfg.icon}"></i>
</div>
<div class="file-name text-truncate" title="${item.name}">${item.name}</div>
</div>
</div>
</div>
`;
}
// Create list row HTML
function createFileRow(item) {
const typeConfig = {
folder: { icon: 'bi-folder-fill', class: 'folder' },
pdf: { icon: 'bi-file-earmark-pdf-fill', class: 'pdf' },
image: { icon: 'bi-file-earmark-image-fill', class: 'image' },
slides: { icon: 'bi-file-earmark-slides-fill', class: 'slides' },
sheet: { icon: 'bi-file-earmark-spreadsheet-fill', class: 'sheet' },
doc: { icon: 'bi-file-earmark-word-fill', class: 'doc' },
video: { icon: 'bi-file-earmark-play-fill', class: 'video' },
file: { icon: 'bi-file-earmark-fill', class: 'file' }
};
const cfg = typeConfig[item.type] || typeConfig.file;
let link, rawLink = '', googleLink = '';
if (CONFIG.isApi) {
link = item.type === 'folder' ? `/drive/api/browse/${item.path}` : `/drive/api/open/${item.path}`;
// For API files, use download endpoint for copy link
if (item.type !== 'folder' && !['slides', 'sheet', 'doc', 'video'].includes(item.type)) {
rawLink = `/drive/api/download/${item.path}`;
}
if (item.type === 'slides') googleLink = `https://docs.google.com/presentation/d/${item.path}/edit`;
else if (item.type === 'sheet') googleLink = `https://docs.google.com/spreadsheets/d/${item.path}/edit`;
else if (item.type === 'doc') googleLink = `https://docs.google.com/document/d/${item.path}/edit`;
} else {
link = item.type === 'folder' ? `/drive/browse/${CONFIG.sourceId}/${item.path}` : `/drive/file/${CONFIG.sourceId}/${item.path}`;
if (item.type !== 'folder' && !['slides', 'sheet', 'doc', 'video'].includes(item.type)) {
rawLink = `/drive/raw/${CONFIG.sourceId}/${item.path}`;
}
}
let actionsHtml = '';
if (item.type !== 'folder') {
actionsHtml = '<div class="file-actions">';
if (rawLink) {
actionsHtml += `<button class="action-btn-sm" onclick="event.stopPropagation(); copyFileLink('${rawLink}', this)" title="Copy download link"><i class="bi bi-link-45deg"></i></button>`;
}
if (googleLink) {
actionsHtml += `<a href="${googleLink}" target="_blank" class="action-btn-sm google-btn" onclick="event.stopPropagation()" title="Open in Google"><i class="bi bi-google"></i></a>`;
}
actionsHtml += '</div>';
}
return `
<div class="file-row" onclick="navigate('${link}')">
<div class="file-icon ${cfg.class}">
<i class="bi ${cfg.icon}"></i>
</div>
<div class="file-name">${item.name}</div>
<span class="badge bg-secondary">${item.type}</span>
${actionsHtml}
</div>
`;
}
// Load more files
async function loadMoreFiles() {
if (lazyState.loading || !lazyState.hasMore) return;
lazyState.loading = true;
document.getElementById('loadingMore').classList.add('active');
try {
let url;
if (CONFIG.isApi) {
url = `/drive/api/files/${CONFIG.folderId}`;
if (lazyState.nextToken) url += `?page_token=${lazyState.nextToken}`;
} else {
url = `/drive/local/files/${CONFIG.sourceId}?offset=${lazyState.offset}&limit=50`;
if (CONFIG.subpath) url += `&path=${encodeURIComponent(CONFIG.subpath)}`;
}
const response = await fetch(url);
const data = await response.json();
if (data.error) {
console.error(data.error);
return;
}
const gridView = document.getElementById('grid-view');
const listView = document.getElementById('list-view');
data.items.forEach(item => {
gridView.insertAdjacentHTML('beforeend', createFileCard(item));
listView.insertAdjacentHTML('beforeend', createFileRow(item));
lazyState.itemCount++;
});
// Update item count
document.getElementById('item-count').textContent = lazyState.itemCount;
// Re-attach hover listeners for new cards
attachHoverListeners();
// Update pagination state
if (CONFIG.isApi) {
lazyState.nextToken = data.next_token;
lazyState.hasMore = data.has_more;
} else {
lazyState.offset += data.items.length;
lazyState.hasMore = data.has_more;
}
} catch (error) {
console.error('Failed to load files:', error);
} finally {
lazyState.loading = false;
document.getElementById('loadingMore').classList.remove('active');
}
}
// Attach hover listeners for file actions
function attachHoverListeners() {
document.querySelectorAll('.file-card').forEach(card => {
const actions = card.querySelector('.file-actions');
if (actions && !card.dataset.hoverAttached) {
card.addEventListener('mouseenter', () => actions.style.opacity = '1');
card.addEventListener('mouseleave', () => actions.style.opacity = '0');
card.dataset.hoverAttached = 'true';
}
});
}
// Infinite scroll
function setupInfiniteScroll() {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && lazyState.hasMore) {
loadMoreFiles();
}
}, { rootMargin: '200px' });
observer.observe(document.getElementById('loadingMore'));
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
const savedView = localStorage.getItem('driveViewMode') || 'grid';
switchView(savedView);
attachHoverListeners();
// Start lazy loading if enabled
if (CONFIG.lazyLoad) {
setupInfiniteScroll();
loadMoreFiles();
}
});
</script>
{% endblock %}