Spaces:
Running
Running
| {% 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 ; | |
| } | |
| .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 %} | |