working folder creation
This commit is contained in:
parent
ea3e92b8b7
commit
b9a82af12f
11 changed files with 2791 additions and 1552 deletions
|
@ -2,225 +2,617 @@
|
|||
|
||||
{% block title %}File Browser - Flask Files{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="file-browser">
|
||||
<div class="browser-header">
|
||||
<h2>File Browser</h2>
|
||||
<!-- <div class="browser-actions"> -->
|
||||
<!-- <a href="{{ url_for('files.upload', folder=current_folder.id if current_folder else None) }}"
|
||||
class="btn primary">
|
||||
<i class="fas fa-cloud-upload-alt"></i> Upload
|
||||
</a> -->
|
||||
<!-- <button class="btn" id="new-folder-btn">
|
||||
<i class="fas fa-folder-plus"></i> New Folder
|
||||
</button> -->
|
||||
<!-- </div> -->
|
||||
</div>
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.browser-container {
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
<div class="path-nav">
|
||||
<a href="{{ url_for('files.browser') }}" class="path-item">
|
||||
<i class="fas fa-home"></i> Home
|
||||
</a>
|
||||
{% for folder in breadcrumbs %}
|
||||
<span class="path-separator">/</span>
|
||||
<a href="{{ url_for('files.browser', folder=folder.id) }}" class="path-item">{{ folder.name }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
.browser-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
{% if folders or files %}
|
||||
<div class="files-container">
|
||||
{% if folders %}
|
||||
<div class="folder-section">
|
||||
<h3>Folders</h3>
|
||||
<div class="files-list">
|
||||
{% for folder in folders %}
|
||||
<div class="file-item folder">
|
||||
<a href="{{ url_for('files.browser', folder=folder.id) }}" class="file-link">
|
||||
<div class="file-icon">
|
||||
<i class="fas fa-folder fa-2x"></i>
|
||||
</div>
|
||||
<div class="file-name">{{ folder.name }}</div>
|
||||
</a>
|
||||
<div class="file-actions">
|
||||
<button class="action-btn rename" data-id="{{ folder.id }}" title="Rename">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="action-btn delete" data-id="{{ folder.id }}" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
.browser-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
{% if files %}
|
||||
<div class="file-section">
|
||||
<h3>Files</h3>
|
||||
<div class="files-list">
|
||||
{% for file in files %}
|
||||
<div class="file-item">
|
||||
<div class="file-link">
|
||||
<div class="file-icon">
|
||||
{% if file.name.endswith('.pdf') %}<i class="fas fa-file-pdf fa-2x"></i>
|
||||
{% elif file.name.endswith(('.jpg', '.jpeg', '.png', '.gif')) %}<i
|
||||
class="fas fa-file-image fa-2x"></i>
|
||||
{% elif file.name.endswith(('.mp3', '.wav', '.flac')) %}<i
|
||||
class="fas fa-file-audio fa-2x"></i>
|
||||
{% elif file.name.endswith(('.mp4', '.mov', '.avi')) %}<i
|
||||
class="fas fa-file-video fa-2x"></i>
|
||||
{% elif file.name.endswith(('.doc', '.docx')) %}<i class="fas fa-file-word fa-2x"></i>
|
||||
{% elif file.name.endswith(('.xls', '.xlsx')) %}<i class="fas fa-file-excel fa-2x"></i>
|
||||
{% elif file.name.endswith(('.ppt', '.pptx')) %}<i class="fas fa-file-powerpoint fa-2x"></i>
|
||||
{% elif file.name.endswith('.zip') %}<i class="fas fa-file-archive fa-2x"></i>
|
||||
{% else %}<i class="fas fa-file fa-2x"></i>{% endif %}
|
||||
</div>
|
||||
<div class="file-details">
|
||||
<div class="file-name">{{ file.name }}</div>
|
||||
<div class="file-meta">
|
||||
<span class="file-size">{{ (file.size / 1024)|round(1) }} KB</span>
|
||||
<span class="file-date">{{ file.updated_at.strftime('%b %d, %Y') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-actions">
|
||||
<a href="#" class="action-btn download" data-id="{{ file.id }}" title="Download">
|
||||
<i class="fas fa-download"></i>
|
||||
</a>
|
||||
<a href="#" class="action-btn share" data-id="{{ file.id }}" title="Share">
|
||||
<i class="fas fa-share-alt"></i>
|
||||
</a>
|
||||
<button class="action-btn rename" data-id="{{ file.id }}" title="Rename">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="action-btn delete" data-id="{{ file.id }}" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<i class="fas fa-folder-open fa-3x"></i>
|
||||
</div>
|
||||
<p>This folder is empty</p>
|
||||
<p>Upload files or create a new folder to get started</p>
|
||||
<div class="empty-actions">
|
||||
<a href="{{ url_for('files.upload', folder=current_folder.id if current_folder else None) }}"
|
||||
class="btn primary">
|
||||
<i class=" fas fa-cloud-upload-alt"></i> Upload
|
||||
</a>
|
||||
<button class="btn" id="empty-new-folder-btn">
|
||||
<i class="fas fa-folder-plus"></i> New Folder
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
.browser-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.breadcrumbs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1.5rem;
|
||||
background: var(--bg-light);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
margin: 0 0.5rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-sm);
|
||||
overflow: hidden;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
border: none;
|
||||
background: var(--card-bg);
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.view-btn.active {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
margin-bottom: 1rem;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-bar input {
|
||||
width: 100%;
|
||||
padding: 0.5rem 1rem 0.5rem 2.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background: var(--bg-light);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 0.75rem;
|
||||
top: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sort-dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sort-dropdown-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background: var(--bg-light);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sort-dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-sm);
|
||||
min-width: 200px;
|
||||
z-index: 10;
|
||||
box-shadow: var(--shadow-md);
|
||||
display: none;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.sort-dropdown-menu.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sort-option {
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sort-option:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.sort-option.active {
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.files-container {
|
||||
min-height: 200px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loading-indicator {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(var(--card-bg-rgb), 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 5;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.loading-indicator.show {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid rgba(var(--primary-color-rgb), 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: var(--primary-color);
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Grid view */
|
||||
.files-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.folder-item,
|
||||
.file-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
border-radius: var(--border-radius-sm);
|
||||
border: 1px solid var(--border-color);
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.folder-item:hover,
|
||||
.file-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-sm);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 0.75rem;
|
||||
text-align: center;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.folder-item .item-icon {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
word-break: break-word;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.item-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* List view */
|
||||
.files-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.list-view .folder-item,
|
||||
.list-view .file-item {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.list-view .item-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0;
|
||||
margin-right: 1rem;
|
||||
width: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.list-view .item-info {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.list-view .item-name {
|
||||
margin-bottom: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.list-view .item-details {
|
||||
margin-left: auto;
|
||||
min-width: 200px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.list-view .item-date {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
/* Empty folder state */
|
||||
.empty-folder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 3rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.empty-message h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.empty-message p {
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.empty-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Context menu */
|
||||
.context-menu {
|
||||
position: fixed;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-sm);
|
||||
box-shadow: var(--shadow-md);
|
||||
z-index: 100;
|
||||
min-width: 180px;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.context-menu-item {
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.context-menu-item:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.context-menu-item.danger {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.browser-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.browser-actions {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.files-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
}
|
||||
|
||||
.list-view .item-info {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.list-view .item-details {
|
||||
margin-left: 0;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal fixes - ensure they're hidden by default */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
overflow: auto;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal.visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--border-radius);
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
margin: 2rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="{{ url_for('static', filename='js/upload.js') }}"></script>
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="browser-container">
|
||||
<div class="browser-header">
|
||||
<div class="browser-title">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
<h2>{% if current_folder %}{{ current_folder.name }}{% else %}My Files{% endif %}</h2>
|
||||
</div>
|
||||
<div class="browser-actions">
|
||||
<input type="text" id="search-input" class="search-input" placeholder="Search files...">
|
||||
<button id="search-btn" class="btn">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
<a href="{% if current_folder %}{{ url_for('files.upload', folder_id=current_folder.id) }}{% else %}{{ url_for('files.upload') }}{% endif %}"
|
||||
class="btn primary">
|
||||
<i class="fas fa-upload"></i> Upload
|
||||
</a>
|
||||
<button id="new-folder-btn" class="btn secondary">
|
||||
<i class="fas fa-folder-plus"></i> New Folder
|
||||
</button>
|
||||
<div class="view-toggle">
|
||||
<button id="grid-view-btn" class="view-btn active" title="Grid View">
|
||||
<i class="fas fa-th"></i>
|
||||
</button>
|
||||
<button id="list-view-btn" class="view-btn" title="List View">
|
||||
<i class="fas fa-list"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="breadcrumbs">
|
||||
<a href="{{ url_for('files.browser') }}" class="breadcrumb-item">
|
||||
<i class="fas fa-home"></i> Home
|
||||
</a>
|
||||
{% if breadcrumbs %}
|
||||
{% for folder in breadcrumbs %}
|
||||
<span class="breadcrumb-separator">/</span>
|
||||
<a href="{{ url_for('files.browser', folder_id=folder.id) }}" class="breadcrumb-item">
|
||||
{{ folder.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div id="files-container" class="files-container grid-view">
|
||||
{% include 'files/partials/folder_contents.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New Folder Modal -->
|
||||
<div id="new-folder-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>Create New Folder</h3>
|
||||
<button class="modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="new-folder-form" action="{{ url_for('files.create_folder') }}" method="post">
|
||||
<div class="form-group">
|
||||
<label for="folder-name">Folder Name</label>
|
||||
<input type="text" id="folder-name" name="name" required>
|
||||
<input type="hidden" id="parent-folder-id" name="parent_id"
|
||||
value="{% if current_folder %}{{ current_folder.id }}{% endif %}">
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn secondary modal-cancel">Cancel</button>
|
||||
<button type="submit" class="btn primary">Create Folder</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File Actions Modal -->
|
||||
<div id="file-actions-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="file-name-header">File Actions</h3>
|
||||
<button class="modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="file-actions-list">
|
||||
<a id="download-action" href="#" class="file-action">
|
||||
<i class="fas fa-download"></i> Download
|
||||
</a>
|
||||
<a id="share-action" href="#" class="file-action">
|
||||
<i class="fas fa-share-alt"></i> Share
|
||||
</a>
|
||||
<button id="rename-action" class="file-action">
|
||||
<i class="fas fa-edit"></i> Rename
|
||||
</button>
|
||||
<button id="delete-action" class="file-action dangerous">
|
||||
<i class="fas fa-trash-alt"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rename Modal -->
|
||||
<div id="rename-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>Rename Item</h3>
|
||||
<button class="modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="rename-form">
|
||||
<div class="form-group">
|
||||
<label for="new-name">New Name</label>
|
||||
<input type="text" id="new-name" name="name" required>
|
||||
<input type="hidden" id="rename-item-id" name="item_id">
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn secondary modal-cancel">Cancel</button>
|
||||
<button type="submit" class="btn primary">Rename</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div id="delete-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>Confirm Deletion</h3>
|
||||
<button class="modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="delete-confirmation-message">Are you sure you want to delete this item? This action cannot be undone.
|
||||
</p>
|
||||
<div class="form-actions">
|
||||
<button class="btn secondary modal-cancel">Cancel</button>
|
||||
<button id="confirm-delete" class="btn dangerous">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// New folder functionality
|
||||
// Setup modals on page load
|
||||
setupModals();
|
||||
|
||||
// Initialize variables
|
||||
const filesContainer = document.getElementById('files-container');
|
||||
const gridViewBtn = document.getElementById('grid-view-btn');
|
||||
const listViewBtn = document.getElementById('list-view-btn');
|
||||
const newFolderBtn = document.getElementById('new-folder-btn');
|
||||
const emptyNewFolderBtn = document.getElementById('empty-new-folder-btn');
|
||||
let selectedItemId = null;
|
||||
|
||||
function showNewFolderPrompt() {
|
||||
const folderName = prompt('Enter folder name:');
|
||||
if (folderName) {
|
||||
createNewFolder(folderName);
|
||||
// Button event listeners
|
||||
if (gridViewBtn && listViewBtn) {
|
||||
gridViewBtn.addEventListener('click', function () {
|
||||
filesContainer.className = 'files-container grid-view';
|
||||
gridViewBtn.classList.add('active');
|
||||
listViewBtn.classList.remove('active');
|
||||
|
||||
// Save preference
|
||||
localStorage.setItem('view_preference', 'grid');
|
||||
});
|
||||
|
||||
listViewBtn.addEventListener('click', function () {
|
||||
filesContainer.className = 'files-container list-view';
|
||||
listViewBtn.classList.add('active');
|
||||
gridViewBtn.classList.remove('active');
|
||||
|
||||
// Save preference
|
||||
localStorage.setItem('view_preference', 'list');
|
||||
});
|
||||
}
|
||||
|
||||
// Apply saved view preference
|
||||
const savedView = localStorage.getItem('view_preference');
|
||||
if (savedView === 'list') {
|
||||
filesContainer.className = 'files-container list-view';
|
||||
if (listViewBtn && gridViewBtn) {
|
||||
listViewBtn.classList.add('active');
|
||||
gridViewBtn.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
function createNewFolder(name) {
|
||||
const formData = new FormData();
|
||||
formData.append('name', name);
|
||||
|
||||
{% if current_folder %}
|
||||
formData.append('parent_id', '{{ current_folder.id }}');
|
||||
{% endif %}
|
||||
|
||||
fetch('/files/create_folder', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(data.error || 'Failed to create folder');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('An error occurred while creating the folder');
|
||||
});
|
||||
}
|
||||
|
||||
// New folder button
|
||||
if (newFolderBtn) {
|
||||
newFolderBtn.addEventListener('click', showNewFolderPrompt);
|
||||
newFolderBtn.addEventListener('click', function () {
|
||||
openModal('new-folder-modal');
|
||||
document.getElementById('folder-name').focus();
|
||||
});
|
||||
}
|
||||
|
||||
if (emptyNewFolderBtn) {
|
||||
emptyNewFolderBtn.addEventListener('click', showNewFolderPrompt);
|
||||
// Setup file item event listeners
|
||||
function setupFileListeners() {
|
||||
// ... your existing file listeners ...
|
||||
}
|
||||
|
||||
// Setup dismiss
|
||||
const closeBtn = alert.querySelector('.close');
|
||||
closeBtn.addEventListener('click', function () {
|
||||
alert.classList.add('fade-out');
|
||||
setTimeout(() => {
|
||||
alert.remove();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Auto dismiss
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.classList.add('fade-out');
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.remove();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}, 5000);
|
||||
// Initial setup
|
||||
setupFileListeners();
|
||||
});
|
||||
|
||||
// Helper functions
|
||||
function formatSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return 'Unknown';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
function escapeHtml(unsafe) {
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -1,5 +1,5 @@
|
|||
{% if folders or files %}
|
||||
<div class="files-grid grid-view">
|
||||
<div class="files-grid">
|
||||
{% if folders %}
|
||||
{% for folder in folders %}
|
||||
<a href="{{ url_for('files.browser', folder_id=folder.id) }}" class="folder-item" data-id="{{ folder.id }}">
|
||||
|
@ -9,8 +9,8 @@
|
|||
<div class="item-info">
|
||||
<div class="item-name">{{ folder.name }}</div>
|
||||
<div class="item-details">
|
||||
<span class="item-count">{{ folder.files.count() }} items</span>
|
||||
<span class="item-date">{{ folder.created_at.strftime('%Y-%m-%d') }}</span>
|
||||
<span class="item-count">{{ folder.children.count() }} items</span>
|
||||
<span class="item-date">{{ folder.updated_at.strftime('%Y-%m-%d') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
@ -27,7 +27,7 @@
|
|||
<div class="item-name">{{ file.name }}</div>
|
||||
<div class="item-details">
|
||||
<span class="item-size">{{ format_size(file.size) }}</span>
|
||||
<span class="item-date">{{ file.created_at.strftime('%Y-%m-%d') }}</span>
|
||||
<span class="item-date">{{ file.updated_at.strftime('%Y-%m-%d') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
@ -43,5 +43,14 @@
|
|||
<h3>This folder is empty</h3>
|
||||
<p>Upload files or create a folder to get started</p>
|
||||
</div>
|
||||
<div class="empty-actions">
|
||||
<a href="{{ url_for('files.upload', folder_id=current_folder.id if current_folder else None) }}"
|
||||
class="btn primary">
|
||||
<i class="fas fa-upload"></i> Upload
|
||||
</a>
|
||||
<button class="btn" id="empty-new-folder-btn">
|
||||
<i class="fas fa-folder-plus"></i> New Folder
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
|
@ -3,562 +3,222 @@
|
|||
{% block title %}Upload Files - Flask Files{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/upload.css') }}">
|
||||
<style>
|
||||
.upload-container {
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.upload-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.dropzone {
|
||||
border: 2px dashed var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dropzone.highlight {
|
||||
border-color: var(--primary-color);
|
||||
background-color: var(--primary-color-light);
|
||||
}
|
||||
|
||||
.dropzone-icon {
|
||||
font-size: 3rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.file-inputs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
display: none;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
height: 8px;
|
||||
background-color: var(--bg-light);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background-color: var(--primary-color);
|
||||
width: 0%;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.upload-files-list {
|
||||
margin-top: 1.5rem;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.file-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.file-item-icon {
|
||||
margin-right: 1rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.file-item-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.file-item-name {
|
||||
margin-bottom: 0.25rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.file-item-meta {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.file-item-status {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.status-uploading {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.status-success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.status-error {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="upload-container">
|
||||
<div class="upload-header">
|
||||
<h2>Upload Files</h2>
|
||||
<div class="upload-location">
|
||||
<span>Uploading to:</span>
|
||||
{% if parent_folder %}
|
||||
<a href="{{ url_for('files.browser', folder_id=parent_folder.id) }}">{{ parent_folder.name }}</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('files.browser') }}">Root</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="upload-dropzone" id="dropzone">
|
||||
<div class="upload-icon-wrapper">
|
||||
<i class="fas fa-cloud-upload-alt upload-icon"></i>
|
||||
</div>
|
||||
<div class="upload-text">
|
||||
<p class="upload-primary-text">Drag & drop files or folders here</p>
|
||||
<p class="upload-secondary-text">or</p>
|
||||
<div class="upload-buttons">
|
||||
<label class="btn primary">
|
||||
<i class="fas fa-file"></i> Select Files
|
||||
<input type="file" name="files[]" multiple id="file-input" style="display: none">
|
||||
</label>
|
||||
<label class="btn">
|
||||
<i class="fas fa-folder"></i> Select Folder
|
||||
<input type="file" name="folders[]" webkitdirectory directory id="folder-input"
|
||||
style="display: none">
|
||||
</label>
|
||||
</div>
|
||||
<p class="upload-hint">Files will upload automatically when dropped or selected</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="upload-progress-container" id="progress-container" style="display: none;">
|
||||
<h3>Upload Progress</h3>
|
||||
<div class="progress-overall">
|
||||
<div class="progress-header">
|
||||
<span>Overall Progress</span>
|
||||
<span id="progress-percentage">0%</span>
|
||||
</div>
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar" id="progress-bar"></div>
|
||||
<div class="container">
|
||||
<div class="upload-container">
|
||||
<div class="upload-header">
|
||||
<h2>Upload Files</h2>
|
||||
<div>
|
||||
<a href="{{ url_for('files.browser', folder_id=parent_folder.id if parent_folder else None) }}"
|
||||
class="btn secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back to Files
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="upload-stats">
|
||||
<div class="stat-item">
|
||||
<i class="fas fa-tachometer-alt"></i>
|
||||
<span id="upload-speed">0 KB/s</span>
|
||||
<div class="upload-tabs">
|
||||
<button class="upload-tab active" data-tab="file-upload-tab">
|
||||
<i class="fas fa-file-upload"></i> File Upload
|
||||
</button>
|
||||
<button class="upload-tab" data-tab="folder-upload-tab">
|
||||
<i class="fas fa-folder-upload"></i> Folder Upload
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-content active" id="file-upload-tab">
|
||||
<div id="file-dropzone" class="dropzone">
|
||||
<div class="dropzone-icon">
|
||||
<i class="fas fa-cloud-upload-alt"></i>
|
||||
</div>
|
||||
<h3>Drag & Drop Files Here</h3>
|
||||
<p>Or click to browse your device</p>
|
||||
<button id="file-select-btn" class="btn primary">Select Files</button>
|
||||
<form id="file-upload-form" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="folder_id"
|
||||
value="{% if parent_folder %}{{ parent_folder.id }}{% endif %}">
|
||||
<input type="file" id="file-input" name="files[]" multiple style="display: none;">
|
||||
</form>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<i class="fas fa-file-upload"></i>
|
||||
<span id="uploaded-size">0 KB / 0 KB</span>
|
||||
|
||||
<div class="upload-progress">
|
||||
<div class="progress-info">
|
||||
<div class="progress-text">Upload Progress</div>
|
||||
<div class="progress-percentage" id="progress-percentage">0%</div>
|
||||
</div>
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar" id="progress-bar"></div>
|
||||
</div>
|
||||
<div class="progress-details">
|
||||
<div class="progress-speed" id="upload-speed">0 KB/s</div>
|
||||
<div class="progress-size" id="uploaded-size">0 KB / 0 KB</div>
|
||||
<div class="progress-time" id="time-remaining">Calculating...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span id="time-remaining">calculating...</span>
|
||||
|
||||
<div id="file-list" class="file-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="folder-upload-tab">
|
||||
<div id="folder-dropzone" class="dropzone">
|
||||
<div class="dropzone-icon">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
</div>
|
||||
<h3>Drag & Drop a Folder Here</h3>
|
||||
<p>Or click to browse your device</p>
|
||||
<button id="folder-select-btn" class="btn primary">Select Folder</button>
|
||||
<form id="folder-upload-form" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="folder_id"
|
||||
value="{% if parent_folder %}{{ parent_folder.id }}{% endif %}">
|
||||
<input type="file" id="folder-input" name="files[]" webkitdirectory directory multiple
|
||||
style="display: none;">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="upload-progress">
|
||||
<div class="progress-info">
|
||||
<div class="progress-text">Upload Progress</div>
|
||||
<div class="progress-percentage" id="folder-progress-percentage">0%</div>
|
||||
</div>
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar" id="folder-progress-bar"></div>
|
||||
</div>
|
||||
<div class="progress-details">
|
||||
<div class="progress-speed" id="folder-upload-speed">0 KB/s</div>
|
||||
<div class="progress-size" id="folder-uploaded-size">0 KB / 0 KB</div>
|
||||
<div class="progress-time" id="folder-time-remaining">Calculating...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="folder-file-list" class="file-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="upload-list" id="upload-list">
|
||||
<h3>Files (<span id="file-count">0</span>)</h3>
|
||||
<div id="file-items" class="file-items"></div>
|
||||
<div id="empty-message" class="empty-message">
|
||||
<p>No files selected yet</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="upload-actions">
|
||||
<a href="{{ url_for('files.browser', folder_id=parent_folder.id if parent_folder else None) }}" class="btn">
|
||||
Cancel
|
||||
</a>
|
||||
<button id="clear-button" class="btn" disabled>Clear All</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// DOM Elements
|
||||
const dropzone = document.getElementById('dropzone');
|
||||
const fileInput = document.getElementById('file-input');
|
||||
const folderInput = document.getElementById('folder-input');
|
||||
const uploadList = document.getElementById('file-items');
|
||||
const emptyMessage = document.getElementById('empty-message');
|
||||
const fileCount = document.getElementById('file-count');
|
||||
const progressContainer = document.getElementById('progress-container');
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
const progressPercentage = document.getElementById('progress-percentage');
|
||||
const uploadSpeed = document.getElementById('upload-speed');
|
||||
const uploadedSize = document.getElementById('uploaded-size');
|
||||
const timeRemaining = document.getElementById('time-remaining');
|
||||
const clearButton = document.getElementById('clear-button');
|
||||
|
||||
// Upload tracking
|
||||
let uploadQueue = [];
|
||||
let currentUploads = 0;
|
||||
let totalUploaded = 0;
|
||||
let totalSize = 0;
|
||||
let uploadStartTime = 0;
|
||||
let lastUploadedBytes = 0;
|
||||
let uploadUpdateInterval = null;
|
||||
const MAX_CONCURRENT_UPLOADS = 3;
|
||||
const folderId = {{ parent_folder.id if parent_folder else 'null' }
|
||||
};
|
||||
});
|
||||
|
||||
// Setup event listeners
|
||||
dropzone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
this.classList.add('highlight');
|
||||
});
|
||||
|
||||
dropzone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
this.classList.remove('highlight');
|
||||
});
|
||||
|
||||
dropzone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
this.classList.remove('highlight');
|
||||
|
||||
// Handle dropped files
|
||||
const items = e.dataTransfer.items;
|
||||
if (items && items.length > 0) {
|
||||
// Check if this is a folder drop from file explorer
|
||||
const containsDirectories = Array.from(items).some(item => {
|
||||
return item.webkitGetAsEntry && item.webkitGetAsEntry().isDirectory;
|
||||
});
|
||||
|
||||
if (containsDirectories) {
|
||||
processDroppedItems(items);
|
||||
} else {
|
||||
handleFiles(e.dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', function () {
|
||||
handleFiles(this.files);
|
||||
});
|
||||
|
||||
folderInput.addEventListener('change', function () {
|
||||
handleFolderUpload(this.files);
|
||||
});
|
||||
|
||||
clearButton.addEventListener('click', function () {
|
||||
resetUploadState();
|
||||
});
|
||||
|
||||
function processDroppedItems(items) {
|
||||
const fileList = [];
|
||||
let pendingDirectories = 0;
|
||||
|
||||
function traverseFileTree(entry, path = '') {
|
||||
if (entry.isFile) {
|
||||
entry.file(file => {
|
||||
file.relativePath = path + file.name;
|
||||
fileList.push(file);
|
||||
if (pendingDirectories === 0 && entry.isFile) {
|
||||
handleFiles(fileList);
|
||||
}
|
||||
});
|
||||
} else if (entry.isDirectory) {
|
||||
pendingDirectories++;
|
||||
const dirReader = entry.createReader();
|
||||
const readEntries = () => {
|
||||
dirReader.readEntries(entries => {
|
||||
if (entries.length > 0) {
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
traverseFileTree(entries[i], path + entry.name + '/');
|
||||
}
|
||||
readEntries(); // Continue reading if there might be more entries
|
||||
} else {
|
||||
pendingDirectories--;
|
||||
if (pendingDirectories === 0) {
|
||||
handleFiles(fileList);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
readEntries();
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const entry = items[i].webkitGetAsEntry();
|
||||
if (entry) {
|
||||
traverseFileTree(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleFiles(files) {
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
clearButton.disabled = false;
|
||||
emptyMessage.style.display = 'none';
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
|
||||
// Add to queue
|
||||
uploadQueue.push({
|
||||
file: file,
|
||||
relativePath: file.relativePath || null,
|
||||
status: 'queued',
|
||||
progress: 0
|
||||
});
|
||||
|
||||
totalSize += file.size;
|
||||
|
||||
// Add to UI
|
||||
const fileItem = document.createElement('div');
|
||||
fileItem.className = 'file-item';
|
||||
fileItem.dataset.index = uploadQueue.length - 1;
|
||||
|
||||
// Determine icon based on file type
|
||||
const fileIcon = getFileIcon(file.name);
|
||||
|
||||
fileItem.innerHTML = `
|
||||
<div class="file-icon">
|
||||
<i class="fas ${fileIcon}"></i>
|
||||
</div>
|
||||
<div class="file-info">
|
||||
<div class="file-name">${file.name}</div>
|
||||
<div class="file-path">${file.relativePath || 'No path'}</div>
|
||||
<div class="file-size">${formatSize(file.size)}</div>
|
||||
<div class="file-progress">
|
||||
<div class="progress-bar-small" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-status">
|
||||
<span class="status-indicator queued">Queued</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
uploadList.appendChild(fileItem);
|
||||
}
|
||||
|
||||
fileCount.textContent = uploadQueue.length;
|
||||
|
||||
// Start upload process
|
||||
startUpload();
|
||||
}
|
||||
|
||||
function handleFolderUpload(files) {
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const fileArray = Array.from(files);
|
||||
for (let i = 0; i < fileArray.length; i++) {
|
||||
const file = fileArray[i];
|
||||
file.relativePath = file.webkitRelativePath;
|
||||
}
|
||||
|
||||
handleFiles(fileArray);
|
||||
}
|
||||
|
||||
function startUpload() {
|
||||
if (uploadQueue.length === 0 || currentUploads >= MAX_CONCURRENT_UPLOADS) return;
|
||||
|
||||
if (currentUploads === 0) {
|
||||
// First upload - initialize tracking
|
||||
uploadStartTime = Date.now();
|
||||
lastUploadedBytes = 0;
|
||||
progressContainer.style.display = 'block';
|
||||
|
||||
// Start progress update interval
|
||||
uploadUpdateInterval = setInterval(updateUploadStats, 500);
|
||||
}
|
||||
|
||||
// Find next queued file
|
||||
const nextIndex = uploadQueue.findIndex(item => item.status === 'queued');
|
||||
if (nextIndex === -1) return;
|
||||
|
||||
// Start uploading this file
|
||||
uploadQueue[nextIndex].status = 'uploading';
|
||||
currentUploads++;
|
||||
|
||||
// Update UI
|
||||
const fileItem = document.querySelector(`.file-item[data-index="${nextIndex}"]`);
|
||||
const statusIndicator = fileItem.querySelector('.status-indicator');
|
||||
statusIndicator.className = 'status-indicator uploading';
|
||||
statusIndicator.textContent = 'Uploading';
|
||||
|
||||
// Create FormData
|
||||
const formData = new FormData();
|
||||
formData.append('file', uploadQueue[nextIndex].file);
|
||||
formData.append('folder_id', folderId || '');
|
||||
|
||||
if (uploadQueue[nextIndex].relativePath) {
|
||||
formData.append('relative_path', uploadQueue[nextIndex].relativePath);
|
||||
}
|
||||
|
||||
// Create and configure XHR
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.upload.addEventListener('progress', function (e) {
|
||||
if (e.lengthComputable) {
|
||||
const percentComplete = Math.round((e.loaded / e.total) * 100);
|
||||
|
||||
// Update file progress
|
||||
uploadQueue[nextIndex].progress = percentComplete;
|
||||
|
||||
// Update file UI
|
||||
const progressBar = fileItem.querySelector('.progress-bar-small');
|
||||
progressBar.style.width = percentComplete + '%';
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('load', function () {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
|
||||
if (response.success) {
|
||||
// Upload successful
|
||||
uploadQueue[nextIndex].status = 'complete';
|
||||
statusIndicator.className = 'status-indicator complete';
|
||||
statusIndicator.textContent = 'Complete';
|
||||
|
||||
totalUploaded += uploadQueue[nextIndex].file.size;
|
||||
} else {
|
||||
// Upload failed on server
|
||||
uploadQueue[nextIndex].status = 'error';
|
||||
statusIndicator.className = 'status-indicator error';
|
||||
statusIndicator.textContent = 'Error: ' + (response.error || 'Server Error');
|
||||
}
|
||||
} catch (e) {
|
||||
// JSON parse error
|
||||
uploadQueue[nextIndex].status = 'error';
|
||||
statusIndicator.className = 'status-indicator error';
|
||||
statusIndicator.textContent = 'Error: Invalid response';
|
||||
}
|
||||
} else {
|
||||
// HTTP error
|
||||
uploadQueue[nextIndex].status = 'error';
|
||||
statusIndicator.className = 'status-indicator error';
|
||||
statusIndicator.textContent = 'Error: ' + xhr.status;
|
||||
}
|
||||
|
||||
// One upload completed
|
||||
currentUploads--;
|
||||
|
||||
// Check if all uploads complete
|
||||
if (uploadQueue.every(item => item.status !== 'queued' && item.status !== 'uploading')) {
|
||||
// All uploads complete
|
||||
clearInterval(uploadUpdateInterval);
|
||||
|
||||
// Show completion notification
|
||||
const successCount = uploadQueue.filter(item => item.status === 'complete').length;
|
||||
const errorCount = uploadQueue.filter(item => item.status === 'error').length;
|
||||
|
||||
// Show notification message
|
||||
if (errorCount === 0) {
|
||||
showNotification(`Successfully uploaded ${successCount} files`, 'success');
|
||||
} else {
|
||||
showNotification(`Uploaded ${successCount} files, ${errorCount} failed`, 'warning');
|
||||
}
|
||||
} else {
|
||||
// Start next upload
|
||||
startUpload();
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('error', function () {
|
||||
// Network error
|
||||
uploadQueue[nextIndex].status = 'error';
|
||||
statusIndicator.className = 'status-indicator error';
|
||||
statusIndicator.textContent = 'Error: Network error';
|
||||
|
||||
currentUploads--;
|
||||
startUpload(); // Try the next file
|
||||
});
|
||||
|
||||
// Send the request
|
||||
xhr.open('POST', '/files/upload_file');
|
||||
xhr.send(formData);
|
||||
|
||||
// Try to start more uploads if possible
|
||||
startUpload();
|
||||
}
|
||||
|
||||
function updateUploadStats() {
|
||||
if (currentUploads === 0) return;
|
||||
|
||||
// Calculate overall progress
|
||||
let totalProgress = 0;
|
||||
|
||||
uploadQueue.forEach(item => {
|
||||
if (item.status === 'complete') {
|
||||
totalProgress += item.file.size;
|
||||
} else if (item.status === 'uploading') {
|
||||
totalProgress += (item.file.size * (item.progress / 100));
|
||||
}
|
||||
});
|
||||
|
||||
const overallPercentage = Math.round((totalProgress / totalSize) * 100);
|
||||
|
||||
// Update progress bar
|
||||
progressBar.style.width = overallPercentage + '%';
|
||||
progressPercentage.textContent = overallPercentage + '%';
|
||||
|
||||
// Calculate upload speed
|
||||
const elapsed = (Date.now() - uploadStartTime) / 1000; // seconds
|
||||
const bytesPerSecond = totalProgress / elapsed;
|
||||
|
||||
uploadSpeed.textContent = formatSize(bytesPerSecond) + '/s';
|
||||
|
||||
// Update uploaded size
|
||||
uploadedSize.textContent = `${formatSize(totalProgress)} / ${formatSize(totalSize)}`;
|
||||
|
||||
// Calculate time remaining
|
||||
const remainingBytes = totalSize - totalProgress;
|
||||
if (bytesPerSecond > 0) {
|
||||
const secondsRemaining = Math.round(remainingBytes / bytesPerSecond);
|
||||
timeRemaining.textContent = formatTime(secondsRemaining);
|
||||
} else {
|
||||
timeRemaining.textContent = 'calculating...';
|
||||
}
|
||||
}
|
||||
|
||||
function resetUploadState() {
|
||||
// Reset all upload tracking variables
|
||||
uploadQueue = [];
|
||||
currentUploads = 0;
|
||||
totalUploaded = 0;
|
||||
totalSize = 0;
|
||||
clearInterval(uploadUpdateInterval);
|
||||
|
||||
// Reset UI
|
||||
uploadList.innerHTML = '';
|
||||
emptyMessage.style.display = 'block';
|
||||
progressContainer.style.display = 'none';
|
||||
fileCount.textContent = '0';
|
||||
progressBar.style.width = '0%';
|
||||
progressPercentage.textContent = '0%';
|
||||
uploadSpeed.textContent = '0 KB/s';
|
||||
uploadedSize.textContent = '0 KB / 0 KB';
|
||||
timeRemaining.textContent = 'calculating...';
|
||||
clearButton.disabled = true;
|
||||
}
|
||||
|
||||
function showNotification(message, type) {
|
||||
// Create alerts container if it doesn't exist
|
||||
let alertsContainer = document.querySelector('.alerts');
|
||||
if (!alertsContainer) {
|
||||
alertsContainer = document.createElement('div');
|
||||
alertsContainer.className = 'alerts';
|
||||
document.body.appendChild(alertsContainer);
|
||||
}
|
||||
|
||||
// Create alert
|
||||
const alert = document.createElement('div');
|
||||
alert.className = `alert ${type || 'info'}`;
|
||||
alert.innerHTML = `
|
||||
<div class="alert-content">${message}</div>
|
||||
<button class="close" aria-label="Close">×</button>
|
||||
`;
|
||||
|
||||
// Add to container
|
||||
alertsContainer.appendChild(alert);
|
||||
|
||||
// Setup dismiss
|
||||
const closeBtn = alert.querySelector('.close');
|
||||
closeBtn.addEventListener('click', function () {
|
||||
alert.classList.add('fade-out');
|
||||
setTimeout(() => {
|
||||
alert.remove();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Auto dismiss
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.classList.add('fade-out');
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.remove();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function formatTime(seconds) {
|
||||
if (seconds < 60) {
|
||||
return seconds + ' seconds';
|
||||
} else if (seconds < 3600) {
|
||||
return Math.floor(seconds / 60) + ' minutes';
|
||||
} else {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
return hours + ' hours ' + minutes + ' minutes';
|
||||
}
|
||||
}
|
||||
|
||||
function getFileIcon(fileName) {
|
||||
const extension = fileName.split('.').pop().toLowerCase();
|
||||
|
||||
// Images
|
||||
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp'].includes(extension)) {
|
||||
return 'fa-file-image';
|
||||
}
|
||||
// Videos
|
||||
else if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm'].includes(extension)) {
|
||||
return 'fa-file-video';
|
||||
}
|
||||
// Audio
|
||||
else if (['mp3', 'wav', 'ogg', 'flac', 'm4a'].includes(extension)) {
|
||||
return 'fa-file-audio';
|
||||
}
|
||||
// Documents
|
||||
else if (['doc', 'docx', 'dot', 'dotx'].includes(extension)) {
|
||||
return 'fa-file-word';
|
||||
}
|
||||
else if (['xls', 'xlsx', 'csv'].includes(extension)) {
|
||||
return 'fa-file-excel';
|
||||
}
|
||||
else if (['ppt', 'pptx'].includes(extension)) {
|
||||
return 'fa-file-powerpoint';
|
||||
}
|
||||
else if (['pdf'].includes(extension)) {
|
||||
return 'fa-file-pdf';
|
||||
}
|
||||
// Archives
|
||||
else if (['zip', 'rar', '7z', 'tar', 'gz', 'bz2'].includes(extension)) {
|
||||
return 'fa-file-archive';
|
||||
}
|
||||
// Text
|
||||
else if (['txt', 'rtf', 'md', 'log'].includes(extension)) {
|
||||
return 'fa-file-alt';
|
||||
}
|
||||
// Code
|
||||
else if (['html', 'css', 'js', 'php', 'py', 'java', 'c', 'cpp', 'h', 'xml', 'json', 'sql'].includes(extension)) {
|
||||
return 'fa-file-code';
|
||||
}
|
||||
|
||||
return 'fa-file';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/upload.js') }}"></script>
|
||||
{% endblock %}
|
Loading…
Add table
Add a link
Reference in a new issue