2026-01-18 02:22:05 -06:00
|
|
|
<!doctype html>
|
2025-12-14 01:54:40 -06:00
|
|
|
<html lang="en">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8" />
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
2026-01-18 02:22:05 -06:00
|
|
|
<title>Media Library - Sky Art Shop Admin</title>
|
2025-12-24 00:13:23 -06:00
|
|
|
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
|
|
|
|
|
2025-12-14 01:54:40 -06:00
|
|
|
<link
|
|
|
|
|
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
|
|
|
|
|
rel="stylesheet"
|
|
|
|
|
/>
|
|
|
|
|
<link
|
|
|
|
|
rel="stylesheet"
|
|
|
|
|
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
|
|
|
|
|
/>
|
|
|
|
|
<link rel="stylesheet" href="/admin/css/admin-style.css" />
|
2026-01-18 02:22:05 -06:00
|
|
|
<link rel="stylesheet" href="/admin/css/media-library.css" />
|
2025-12-19 20:44:46 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
<style>
|
|
|
|
|
/* Page-specific styles for standalone media library page */
|
|
|
|
|
.page-header {
|
|
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
2025-12-24 00:13:23 -06:00
|
|
|
color: white;
|
2026-01-18 02:22:05 -06:00
|
|
|
padding: 2rem;
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
margin-bottom: 1.5rem;
|
|
|
|
|
box-shadow: 0 10px 40px rgba(102, 126, 234, 0.3);
|
2025-12-24 00:13:23 -06:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
.page-header h1 {
|
|
|
|
|
font-size: 1.75rem;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
margin-bottom: 0.25rem;
|
2025-12-19 20:44:46 -06:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
.page-header p {
|
|
|
|
|
opacity: 0.9;
|
2025-12-19 20:44:46 -06:00
|
|
|
margin: 0;
|
2025-12-14 01:54:40 -06:00
|
|
|
}
|
2025-12-19 20:44:46 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
.media-library-page {
|
2025-12-24 00:13:23 -06:00
|
|
|
background: white;
|
2026-01-18 02:22:05 -06:00
|
|
|
border-radius: 16px;
|
|
|
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
|
|
|
|
overflow: hidden;
|
2025-12-24 00:13:23 -06:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
.media-library-page .media-library-body {
|
|
|
|
|
max-height: none;
|
2025-12-24 00:13:23 -06:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
.stats-bar {
|
2025-12-24 00:13:23 -06:00
|
|
|
display: flex;
|
2026-01-18 02:22:05 -06:00
|
|
|
gap: 1.5rem;
|
|
|
|
|
margin-top: 1rem;
|
2025-12-24 00:13:23 -06:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
.stat-item {
|
2025-12-24 00:13:23 -06:00
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
2026-01-18 02:22:05 -06:00
|
|
|
gap: 0.5rem;
|
|
|
|
|
padding: 0.5rem 1rem;
|
|
|
|
|
background: rgba(255, 255, 255, 0.15);
|
2025-12-24 00:13:23 -06:00
|
|
|
border-radius: 20px;
|
2026-01-18 02:22:05 -06:00
|
|
|
font-size: 0.875rem;
|
2025-12-24 00:13:23 -06:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
.stat-item i {
|
|
|
|
|
font-size: 1rem;
|
2025-12-24 00:13:23 -06:00
|
|
|
}
|
2025-12-14 01:54:40 -06:00
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<!-- Sidebar -->
|
|
|
|
|
<div class="sidebar">
|
2026-01-18 02:22:05 -06:00
|
|
|
<div class="sidebar-brand">Sky Art Shop</div>
|
2025-12-14 01:54:40 -06:00
|
|
|
<ul class="sidebar-menu">
|
|
|
|
|
<li>
|
2026-01-01 22:24:30 -06:00
|
|
|
<a href="/admin/dashboard"
|
2025-12-14 01:54:40 -06:00
|
|
|
><i class="bi bi-speedometer2"></i> Dashboard</a
|
|
|
|
|
>
|
|
|
|
|
</li>
|
|
|
|
|
<li>
|
2026-01-01 22:24:30 -06:00
|
|
|
<a href="/admin/homepage"
|
2025-12-14 01:54:40 -06:00
|
|
|
><i class="bi bi-house"></i> Homepage Editor</a
|
|
|
|
|
>
|
|
|
|
|
</li>
|
|
|
|
|
<li>
|
2026-01-01 22:24:30 -06:00
|
|
|
<a href="/admin/products"><i class="bi bi-box"></i> Products</a>
|
2025-12-14 01:54:40 -06:00
|
|
|
</li>
|
|
|
|
|
<li>
|
2026-01-18 02:22:05 -06:00
|
|
|
<a href="/admin/portfolio"><i class="bi bi-easel"></i> Portfolio</a>
|
2025-12-14 01:54:40 -06:00
|
|
|
</li>
|
|
|
|
|
<li>
|
2026-01-01 22:24:30 -06:00
|
|
|
<a href="/admin/blog"><i class="bi bi-newspaper"></i> Blog</a>
|
2025-12-14 01:54:40 -06:00
|
|
|
</li>
|
|
|
|
|
<li>
|
2026-01-01 22:24:30 -06:00
|
|
|
<a href="/admin/pages"
|
2025-12-14 01:54:40 -06:00
|
|
|
><i class="bi bi-file-text"></i> Custom Pages</a
|
|
|
|
|
>
|
|
|
|
|
</li>
|
2026-01-18 02:22:05 -06:00
|
|
|
<li class="active">
|
|
|
|
|
<a href="/admin/media-library"
|
2025-12-14 01:54:40 -06:00
|
|
|
><i class="bi bi-images"></i> Media Library</a
|
|
|
|
|
>
|
|
|
|
|
</li>
|
|
|
|
|
<li>
|
2026-01-01 22:24:30 -06:00
|
|
|
<a href="/admin/menu"><i class="bi bi-list"></i> Menu</a>
|
2025-12-14 01:54:40 -06:00
|
|
|
</li>
|
|
|
|
|
<li>
|
2026-01-01 22:24:30 -06:00
|
|
|
<a href="/admin/settings"><i class="bi bi-gear"></i> Settings</a>
|
2025-12-14 01:54:40 -06:00
|
|
|
</li>
|
|
|
|
|
<li>
|
2026-01-01 22:24:30 -06:00
|
|
|
<a href="/admin/users"><i class="bi bi-people"></i> Users</a>
|
2025-12-14 01:54:40 -06:00
|
|
|
</li>
|
2026-01-18 02:22:05 -06:00
|
|
|
<li>
|
|
|
|
|
<a href="/admin/customers"
|
|
|
|
|
><i class="bi bi-person-hearts"></i> Customers</a
|
|
|
|
|
>
|
|
|
|
|
</li>
|
2025-12-14 01:54:40 -06:00
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Main Content -->
|
|
|
|
|
<div class="main-content">
|
2026-01-18 02:22:05 -06:00
|
|
|
<!-- Page Header -->
|
|
|
|
|
<div class="page-header">
|
|
|
|
|
<h1><i class="bi bi-images"></i> Media Library</h1>
|
|
|
|
|
<p>Manage all your images and files in one place</p>
|
|
|
|
|
<div class="stats-bar" id="statsBar">
|
|
|
|
|
<div class="stat-item">
|
|
|
|
|
<i class="bi bi-folder"></i>
|
|
|
|
|
<span id="folderCount">0 folders</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-item">
|
|
|
|
|
<i class="bi bi-image"></i>
|
|
|
|
|
<span id="fileCount">0 files</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-item">
|
|
|
|
|
<i class="bi bi-hdd"></i>
|
|
|
|
|
<span id="totalSize">0 MB used</span>
|
2025-12-24 00:13:23 -06:00
|
|
|
</div>
|
2025-12-14 01:54:40 -06:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
<!-- Media Library Container -->
|
|
|
|
|
<div class="media-library-page">
|
2025-12-19 20:44:46 -06:00
|
|
|
<!-- Toolbar -->
|
2026-01-18 02:22:05 -06:00
|
|
|
<div
|
|
|
|
|
class="media-library-toolbar"
|
|
|
|
|
style="border-top-left-radius: 16px; border-top-right-radius: 16px"
|
|
|
|
|
>
|
2025-12-19 20:44:46 -06:00
|
|
|
<div class="toolbar-left">
|
2026-01-18 02:22:05 -06:00
|
|
|
<button type="button" class="btn btn-primary" id="uploadBtn">
|
|
|
|
|
<i class="bi bi-cloud-upload"></i> Upload Files
|
2025-12-24 00:13:23 -06:00
|
|
|
</button>
|
2026-01-18 02:22:05 -06:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="btn btn-outline-secondary"
|
|
|
|
|
id="newFolderBtn"
|
2025-12-14 01:54:40 -06:00
|
|
|
>
|
2025-12-19 20:44:46 -06:00
|
|
|
<i class="bi bi-folder-plus"></i> New Folder
|
|
|
|
|
</button>
|
2026-01-18 02:22:05 -06:00
|
|
|
<div class="divider"></div>
|
|
|
|
|
<div class="search-box">
|
|
|
|
|
<i class="bi bi-search"></i>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="Search files and folders..."
|
|
|
|
|
id="searchInput"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="toolbar-right">
|
|
|
|
|
<div class="view-toggle btn-group" role="group">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="btn btn-outline-secondary btn-sm active"
|
|
|
|
|
data-view="grid"
|
|
|
|
|
title="Grid view"
|
|
|
|
|
>
|
|
|
|
|
<i class="bi bi-grid-3x3-gap-fill"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="btn btn-outline-secondary btn-sm"
|
|
|
|
|
data-view="list"
|
|
|
|
|
title="List view"
|
|
|
|
|
>
|
|
|
|
|
<i class="bi bi-list-ul"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<select
|
|
|
|
|
class="form-select form-select-sm"
|
|
|
|
|
id="sortSelect"
|
|
|
|
|
style="width: auto"
|
|
|
|
|
>
|
|
|
|
|
<option value="date">Newest First</option>
|
|
|
|
|
<option value="date-asc">Oldest First</option>
|
|
|
|
|
<option value="name">Name (A-Z)</option>
|
|
|
|
|
<option value="name-desc">Name (Z-A)</option>
|
|
|
|
|
<option value="size">Size (Largest)</option>
|
|
|
|
|
<option value="size-asc">Size (Smallest)</option>
|
|
|
|
|
</select>
|
|
|
|
|
<span
|
|
|
|
|
class="selected-count"
|
|
|
|
|
id="selectedCount"
|
2025-12-14 01:54:40 -06:00
|
|
|
style="display: none"
|
|
|
|
|
>
|
2026-01-18 02:22:05 -06:00
|
|
|
<span class="count">0</span> selected
|
|
|
|
|
</span>
|
2025-12-14 01:54:40 -06:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
<!-- Breadcrumb -->
|
|
|
|
|
<div
|
|
|
|
|
style="
|
|
|
|
|
padding: 0.75rem 1.5rem;
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
border-bottom: 1px solid #e2e8f0;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
"
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="btn btn-outline-secondary btn-sm"
|
|
|
|
|
id="backBtn"
|
|
|
|
|
style="display: none"
|
|
|
|
|
>
|
|
|
|
|
<i class="bi bi-arrow-left"></i> Back
|
|
|
|
|
</button>
|
|
|
|
|
<nav aria-label="breadcrumb">
|
|
|
|
|
<ol class="breadcrumb mb-0" id="breadcrumb">
|
|
|
|
|
<li class="breadcrumb-item active">Root</li>
|
|
|
|
|
</ol>
|
|
|
|
|
</nav>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Upload Drop Zone -->
|
|
|
|
|
<div
|
|
|
|
|
class="upload-drop-zone"
|
|
|
|
|
id="uploadDropZone"
|
|
|
|
|
style="margin: 1rem 1.5rem"
|
|
|
|
|
>
|
|
|
|
|
<div class="drop-zone-content">
|
|
|
|
|
<i class="bi bi-cloud-arrow-up"></i>
|
|
|
|
|
<p>
|
|
|
|
|
Drag & drop files here or <span class="browse-link">browse</span>
|
|
|
|
|
</p>
|
|
|
|
|
<small
|
|
|
|
|
>Supports: JPG, PNG, GIF, WebP (Max 60MB each, up to 10
|
|
|
|
|
files)</small
|
|
|
|
|
>
|
|
|
|
|
</div>
|
2025-12-14 01:54:40 -06:00
|
|
|
<input
|
|
|
|
|
type="file"
|
|
|
|
|
id="fileInput"
|
|
|
|
|
multiple
|
2026-01-18 02:22:05 -06:00
|
|
|
accept="image/*,.jpg,.jpeg,.png,.gif,.webp,.bmp,.tiff,.tif,.svg,.ico,.avif,.heic,.heif"
|
|
|
|
|
hidden
|
2025-12-14 01:54:40 -06:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
<!-- Upload Progress -->
|
|
|
|
|
<div class="upload-progress" id="uploadProgress" style="display: none">
|
|
|
|
|
<div class="progress-header">
|
|
|
|
|
<span>Uploading...</span>
|
|
|
|
|
<span class="progress-percent" id="uploadPercent">0%</span>
|
|
|
|
|
</div>
|
2025-12-19 20:44:46 -06:00
|
|
|
<div class="progress">
|
2025-12-14 01:54:40 -06:00
|
|
|
<div
|
|
|
|
|
class="progress-bar progress-bar-striped progress-bar-animated"
|
2026-01-18 02:22:05 -06:00
|
|
|
id="uploadProgressBar"
|
|
|
|
|
role="progressbar"
|
|
|
|
|
></div>
|
2025-12-14 01:54:40 -06:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
<!-- Media Content -->
|
|
|
|
|
<div
|
|
|
|
|
class="media-library-body"
|
|
|
|
|
style="min-height: 400px; max-height: none"
|
|
|
|
|
>
|
|
|
|
|
<div class="media-content grid-view" id="mediaContent">
|
|
|
|
|
<div class="loading-spinner">
|
|
|
|
|
<div class="spinner-border text-primary" role="status">
|
|
|
|
|
<span class="visually-hidden">Loading...</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-19 20:44:46 -06:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
<!-- Footer Actions -->
|
|
|
|
|
<div
|
|
|
|
|
class="media-library-footer"
|
|
|
|
|
style="
|
|
|
|
|
border-bottom-left-radius: 16px;
|
|
|
|
|
border-bottom-right-radius: 16px;
|
|
|
|
|
"
|
|
|
|
|
>
|
|
|
|
|
<div class="footer-left">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="btn btn-outline-danger"
|
|
|
|
|
id="deleteSelectedBtn"
|
|
|
|
|
style="display: none"
|
|
|
|
|
>
|
|
|
|
|
<i class="bi bi-trash"></i> Delete Selected
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="footer-right">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="btn btn-outline-secondary"
|
|
|
|
|
id="refreshBtn"
|
|
|
|
|
>
|
|
|
|
|
<i class="bi bi-arrow-clockwise"></i> Refresh
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-24 00:13:23 -06:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
<!-- Rename Modal -->
|
|
|
|
|
<div class="modal fade" id="renameModal" tabindex="-1">
|
|
|
|
|
<div class="modal-dialog modal-dialog-centered">
|
2025-12-24 00:13:23 -06:00
|
|
|
<div class="modal-content">
|
2026-01-18 02:22:05 -06:00
|
|
|
<div class="modal-header">
|
|
|
|
|
<h5 class="modal-title"><i class="bi bi-pencil"></i> Rename</h5>
|
2025-12-24 00:13:23 -06:00
|
|
|
<button
|
|
|
|
|
type="button"
|
2026-01-18 02:22:05 -06:00
|
|
|
class="btn-close"
|
2025-12-24 00:13:23 -06:00
|
|
|
data-bs-dismiss="modal"
|
|
|
|
|
></button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-body">
|
2026-01-18 02:22:05 -06:00
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
class="form-control"
|
|
|
|
|
id="renameInput"
|
|
|
|
|
placeholder="Enter new name"
|
|
|
|
|
/>
|
|
|
|
|
<input type="hidden" id="renameItemId" />
|
|
|
|
|
<input type="hidden" id="renameItemType" />
|
2025-12-24 00:13:23 -06:00
|
|
|
</div>
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="btn btn-secondary"
|
|
|
|
|
data-bs-dismiss="modal"
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</button>
|
2026-01-18 02:22:05 -06:00
|
|
|
<button type="button" class="btn btn-primary" id="confirmRenameBtn">
|
|
|
|
|
Rename
|
2025-12-24 00:13:23 -06:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-12-19 20:44:46 -06:00
|
|
|
<!-- Create Folder Modal -->
|
|
|
|
|
<div class="modal fade" id="createFolderModal" tabindex="-1">
|
2026-01-18 02:22:05 -06:00
|
|
|
<div class="modal-dialog modal-dialog-centered">
|
2025-12-19 20:44:46 -06:00
|
|
|
<div class="modal-content">
|
|
|
|
|
<div class="modal-header">
|
2026-01-18 02:22:05 -06:00
|
|
|
<h5 class="modal-title">
|
|
|
|
|
<i class="bi bi-folder-plus"></i> Create New Folder
|
|
|
|
|
</h5>
|
2025-12-19 20:44:46 -06:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="btn-close"
|
|
|
|
|
data-bs-dismiss="modal"
|
|
|
|
|
></button>
|
2025-12-14 01:54:40 -06:00
|
|
|
</div>
|
2025-12-19 20:44:46 -06:00
|
|
|
<div class="modal-body">
|
2026-01-18 02:22:05 -06:00
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
class="form-control"
|
|
|
|
|
id="newFolderInput"
|
|
|
|
|
placeholder="Folder name"
|
|
|
|
|
/>
|
2025-12-14 01:54:40 -06:00
|
|
|
</div>
|
2025-12-19 20:44:46 -06:00
|
|
|
<div class="modal-footer">
|
2025-12-14 01:54:40 -06:00
|
|
|
<button
|
2025-12-19 20:44:46 -06:00
|
|
|
type="button"
|
|
|
|
|
class="btn btn-secondary"
|
|
|
|
|
data-bs-dismiss="modal"
|
2025-12-14 01:54:40 -06:00
|
|
|
>
|
2025-12-19 20:44:46 -06:00
|
|
|
Cancel
|
|
|
|
|
</button>
|
2026-01-18 02:22:05 -06:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="btn btn-primary"
|
|
|
|
|
id="confirmCreateFolderBtn"
|
|
|
|
|
>
|
2025-12-19 20:44:46 -06:00
|
|
|
Create Folder
|
2025-12-14 01:54:40 -06:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
<!-- Image Preview Overlay -->
|
|
|
|
|
<div class="image-preview-overlay" id="imagePreviewOverlay">
|
|
|
|
|
<button class="preview-close" id="previewClose">×</button>
|
|
|
|
|
<img
|
|
|
|
|
src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs="
|
|
|
|
|
alt="Preview"
|
|
|
|
|
id="previewImage"
|
|
|
|
|
/>
|
|
|
|
|
<div class="preview-info">
|
|
|
|
|
<span class="preview-filename" id="previewFilename"></span>
|
|
|
|
|
<span class="preview-size" id="previewSize"></span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-12-14 01:54:40 -06:00
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
2026-01-18 02:22:05 -06:00
|
|
|
<script src="/admin/js/auth.js"></script>
|
2025-12-14 01:54:40 -06:00
|
|
|
<script>
|
2026-01-18 02:22:05 -06:00
|
|
|
// State
|
|
|
|
|
let currentFolder = null;
|
|
|
|
|
let folders = [];
|
|
|
|
|
let files = [];
|
|
|
|
|
let selectedItems = [];
|
|
|
|
|
let viewMode = localStorage.getItem("mediaLibraryView") || "grid";
|
|
|
|
|
let sortBy = localStorage.getItem("mediaLibrarySort") || "date";
|
|
|
|
|
let searchQuery = "";
|
|
|
|
|
let draggedItem = null;
|
|
|
|
|
|
|
|
|
|
// Initialize
|
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
|
|
|
initViewMode();
|
|
|
|
|
loadContent();
|
|
|
|
|
bindEvents();
|
|
|
|
|
});
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
function initViewMode() {
|
|
|
|
|
const content = document.getElementById("mediaContent");
|
|
|
|
|
content.classList.remove("grid-view", "list-view");
|
|
|
|
|
content.classList.add(viewMode === "list" ? "list-view" : "grid-view");
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
document.querySelectorAll(".view-toggle button").forEach((btn) => {
|
|
|
|
|
btn.classList.toggle("active", btn.dataset.view === viewMode);
|
|
|
|
|
});
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
document.getElementById("sortSelect").value = sortBy;
|
2025-12-14 01:54:40 -06:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
function bindEvents() {
|
|
|
|
|
// View toggle
|
|
|
|
|
document.querySelectorAll(".view-toggle button").forEach((btn) => {
|
|
|
|
|
btn.addEventListener("click", () => {
|
|
|
|
|
viewMode = btn.dataset.view;
|
|
|
|
|
localStorage.setItem("mediaLibraryView", viewMode);
|
|
|
|
|
initViewMode();
|
2025-12-19 20:44:46 -06:00
|
|
|
});
|
2026-01-18 02:22:05 -06:00
|
|
|
});
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
// Sort
|
|
|
|
|
document
|
|
|
|
|
.getElementById("sortSelect")
|
|
|
|
|
.addEventListener("change", (e) => {
|
|
|
|
|
sortBy = e.target.value;
|
|
|
|
|
localStorage.setItem("mediaLibrarySort", sortBy);
|
|
|
|
|
renderContent();
|
|
|
|
|
});
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
// Search
|
|
|
|
|
document
|
|
|
|
|
.getElementById("searchInput")
|
|
|
|
|
.addEventListener("input", (e) => {
|
|
|
|
|
searchQuery = e.target.value.toLowerCase();
|
|
|
|
|
renderContent();
|
|
|
|
|
});
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
// Upload button
|
|
|
|
|
document.getElementById("uploadBtn").addEventListener("click", () => {
|
|
|
|
|
document.getElementById("fileInput").click();
|
|
|
|
|
});
|
2025-12-19 20:44:46 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
// File input
|
|
|
|
|
document.getElementById("fileInput").addEventListener("change", (e) => {
|
|
|
|
|
if (e.target.files.length > 0) {
|
|
|
|
|
uploadFiles(e.target.files);
|
2025-12-19 20:44:46 -06:00
|
|
|
}
|
2026-01-18 02:22:05 -06:00
|
|
|
});
|
2025-12-19 20:44:46 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
// Drop zone
|
|
|
|
|
const dropZone = document.getElementById("uploadDropZone");
|
|
|
|
|
dropZone.addEventListener("dragover", (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
dropZone.classList.add("dragover");
|
|
|
|
|
});
|
|
|
|
|
dropZone.addEventListener("dragleave", () => {
|
|
|
|
|
dropZone.classList.remove("dragover");
|
|
|
|
|
});
|
|
|
|
|
dropZone.addEventListener("drop", (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
dropZone.classList.remove("dragover");
|
|
|
|
|
if (e.dataTransfer.files.length > 0) {
|
|
|
|
|
uploadFiles(e.dataTransfer.files);
|
2025-12-19 20:44:46 -06:00
|
|
|
}
|
2026-01-18 02:22:05 -06:00
|
|
|
});
|
|
|
|
|
dropZone.querySelector(".browse-link").addEventListener("click", () => {
|
|
|
|
|
document.getElementById("fileInput").click();
|
|
|
|
|
});
|
2025-12-19 20:44:46 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
// New folder
|
|
|
|
|
document
|
|
|
|
|
.getElementById("newFolderBtn")
|
|
|
|
|
.addEventListener("click", () => {
|
|
|
|
|
document.getElementById("newFolderInput").value = "";
|
|
|
|
|
new bootstrap.Modal(
|
|
|
|
|
document.getElementById("createFolderModal"),
|
|
|
|
|
).show();
|
2025-12-14 01:54:40 -06:00
|
|
|
});
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
document
|
|
|
|
|
.getElementById("confirmCreateFolderBtn")
|
|
|
|
|
.addEventListener("click", createFolder);
|
|
|
|
|
document
|
|
|
|
|
.getElementById("newFolderInput")
|
|
|
|
|
.addEventListener("keypress", (e) => {
|
|
|
|
|
if (e.key === "Enter") createFolder();
|
|
|
|
|
});
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
// Rename
|
|
|
|
|
document
|
|
|
|
|
.getElementById("confirmRenameBtn")
|
|
|
|
|
.addEventListener("click", confirmRename);
|
|
|
|
|
document
|
|
|
|
|
.getElementById("renameInput")
|
|
|
|
|
.addEventListener("keypress", (e) => {
|
|
|
|
|
if (e.key === "Enter") confirmRename();
|
|
|
|
|
});
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
// Delete selected
|
|
|
|
|
document
|
|
|
|
|
.getElementById("deleteSelectedBtn")
|
|
|
|
|
.addEventListener("click", deleteSelected);
|
2025-12-14 01:54:40 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
// Back button
|
|
|
|
|
document
|
|
|
|
|
.getElementById("backBtn")
|
|
|
|
|
.addEventListener("click", navigateBack);
|
|
|
|
|
|
|
|
|
|
// Refresh
|
|
|
|
|
document
|
|
|
|
|
.getElementById("refreshBtn")
|
|
|
|
|
.addEventListener("click", loadContent);
|
|
|
|
|
|
|
|
|
|
// Preview close
|
|
|
|
|
document
|
|
|
|
|
.getElementById("previewClose")
|
|
|
|
|
.addEventListener("click", closePreview);
|
|
|
|
|
document
|
|
|
|
|
.getElementById("imagePreviewOverlay")
|
|
|
|
|
.addEventListener("click", (e) => {
|
|
|
|
|
if (e.target.id === "imagePreviewOverlay") closePreview();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.addEventListener("keydown", (e) => {
|
|
|
|
|
if (e.key === "Escape") closePreview();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadContent() {
|
|
|
|
|
try {
|
|
|
|
|
const [foldersRes, filesRes] = await Promise.all([
|
|
|
|
|
fetch("/api/admin/folders", { credentials: "include" }),
|
|
|
|
|
fetch(
|
|
|
|
|
`/api/admin/uploads${
|
|
|
|
|
currentFolder
|
|
|
|
|
? `?folder_id=${currentFolder}`
|
|
|
|
|
: "?folder_id=null"
|
|
|
|
|
}`,
|
|
|
|
|
{ credentials: "include" },
|
|
|
|
|
),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const foldersData = await foldersRes.json();
|
|
|
|
|
const filesData = await filesRes.json();
|
|
|
|
|
|
|
|
|
|
folders = foldersData.folders || [];
|
|
|
|
|
files = filesData.files || [];
|
|
|
|
|
|
|
|
|
|
updateStats();
|
|
|
|
|
renderContent();
|
|
|
|
|
updateBreadcrumb();
|
2025-12-14 01:54:40 -06:00
|
|
|
} catch (error) {
|
2026-01-18 02:22:05 -06:00
|
|
|
console.error("Error loading media:", error);
|
|
|
|
|
showToast("Failed to load media", "error");
|
2025-12-14 01:54:40 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
function updateStats() {
|
|
|
|
|
// Count all folders
|
|
|
|
|
document.getElementById("folderCount").textContent = `${
|
|
|
|
|
folders.length
|
|
|
|
|
} folder${folders.length !== 1 ? "s" : ""}`;
|
|
|
|
|
|
|
|
|
|
// Need to get total file count
|
|
|
|
|
fetch("/api/admin/uploads", { credentials: "include" })
|
|
|
|
|
.then((res) => res.json())
|
|
|
|
|
.then((data) => {
|
|
|
|
|
const allFiles = data.files || [];
|
|
|
|
|
document.getElementById("fileCount").textContent = `${
|
|
|
|
|
allFiles.length
|
|
|
|
|
} file${allFiles.length !== 1 ? "s" : ""}`;
|
|
|
|
|
|
|
|
|
|
const totalBytes = allFiles.reduce(
|
|
|
|
|
(sum, f) => sum + (f.size || 0),
|
|
|
|
|
0,
|
|
|
|
|
);
|
|
|
|
|
document.getElementById("totalSize").textContent =
|
|
|
|
|
formatFileSize(totalBytes) + " used";
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderContent() {
|
|
|
|
|
const content = document.getElementById("mediaContent");
|
2025-12-14 01:54:40 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
// Filter folders for current directory
|
|
|
|
|
let filteredFolders = folders.filter((f) =>
|
|
|
|
|
currentFolder ? f.parentId === currentFolder : f.parentId === null,
|
2025-12-19 20:44:46 -06:00
|
|
|
);
|
2026-01-18 02:22:05 -06:00
|
|
|
let filteredFiles = files;
|
|
|
|
|
|
|
|
|
|
// Apply search
|
|
|
|
|
if (searchQuery) {
|
|
|
|
|
filteredFolders = filteredFolders.filter((f) =>
|
|
|
|
|
f.name.toLowerCase().includes(searchQuery),
|
|
|
|
|
);
|
|
|
|
|
filteredFiles = filteredFiles.filter(
|
|
|
|
|
(f) =>
|
|
|
|
|
f.originalName.toLowerCase().includes(searchQuery) ||
|
|
|
|
|
f.filename.toLowerCase().includes(searchQuery),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sort files
|
|
|
|
|
filteredFiles = sortFiles(filteredFiles);
|
2025-12-19 20:44:46 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
if (filteredFolders.length === 0 && filteredFiles.length === 0) {
|
|
|
|
|
content.innerHTML = `
|
2025-12-19 20:44:46 -06:00
|
|
|
<div class="empty-state">
|
2026-01-18 02:22:05 -06:00
|
|
|
<i class="bi bi-folder2-open"></i>
|
|
|
|
|
<h5>${
|
|
|
|
|
searchQuery ? "No results found" : "This folder is empty"
|
|
|
|
|
}</h5>
|
|
|
|
|
<p>${
|
|
|
|
|
searchQuery
|
|
|
|
|
? "Try a different search term"
|
|
|
|
|
: "Upload files or create a folder to get started"
|
|
|
|
|
}</p>
|
2025-12-19 20:44:46 -06:00
|
|
|
</div>
|
|
|
|
|
`;
|
2025-12-14 01:54:40 -06:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 20:44:46 -06:00
|
|
|
let html = "";
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
// Folders
|
|
|
|
|
filteredFolders.forEach((folder) => {
|
|
|
|
|
const isSelected = selectedItems.some(
|
|
|
|
|
(s) => s.type === "folder" && s.id === folder.id,
|
|
|
|
|
);
|
2025-12-19 20:44:46 -06:00
|
|
|
html += `
|
2026-01-18 02:22:05 -06:00
|
|
|
<div class="media-item folder-item ${isSelected ? "selected" : ""}"
|
|
|
|
|
data-type="folder" data-id="${
|
|
|
|
|
folder.id
|
|
|
|
|
}" data-name="${escapeHtml(folder.name)}"
|
|
|
|
|
draggable="true">
|
|
|
|
|
<div class="item-checkbox">
|
|
|
|
|
<input type="checkbox" ${isSelected ? "checked" : ""}>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="item-icon">
|
2025-12-19 20:44:46 -06:00
|
|
|
<i class="bi bi-folder-fill"></i>
|
2026-01-18 02:22:05 -06:00
|
|
|
</div>
|
|
|
|
|
<div class="item-info">
|
|
|
|
|
<span class="item-name">${escapeHtml(folder.name)}</span>
|
|
|
|
|
<span class="item-meta">${folder.fileCount || 0} files</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="item-actions">
|
|
|
|
|
<button class="btn-action" data-action="rename" title="Rename">
|
|
|
|
|
<i class="bi bi-pencil"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<button class="btn-action btn-danger" data-action="delete" title="Delete">
|
|
|
|
|
<i class="bi bi-trash"></i>
|
|
|
|
|
</button>
|
2025-12-19 20:44:46 -06:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
2026-01-18 02:22:05 -06:00
|
|
|
});
|
2025-12-19 20:44:46 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
// Files
|
|
|
|
|
filteredFiles.forEach((file) => {
|
|
|
|
|
const isSelected = selectedItems.some(
|
|
|
|
|
(s) => s.type === "file" && s.id === file.id,
|
|
|
|
|
);
|
2025-12-19 20:44:46 -06:00
|
|
|
html += `
|
2026-01-18 02:22:05 -06:00
|
|
|
<div class="media-item file-item ${isSelected ? "selected" : ""}"
|
|
|
|
|
data-type="file" data-id="${file.id}" data-path="${file.path}"
|
|
|
|
|
data-name="${escapeHtml(file.originalName)}" data-size="${
|
|
|
|
|
file.size
|
|
|
|
|
}"
|
|
|
|
|
draggable="true">
|
|
|
|
|
<div class="item-checkbox">
|
|
|
|
|
<input type="checkbox" ${isSelected ? "checked" : ""}>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="item-preview">
|
|
|
|
|
<img src="${file.path}" alt="${escapeHtml(
|
|
|
|
|
file.originalName,
|
|
|
|
|
)}" loading="lazy">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="item-info">
|
|
|
|
|
<span class="item-name" title="${escapeHtml(
|
|
|
|
|
file.originalName,
|
|
|
|
|
)}">${escapeHtml(file.originalName)}</span>
|
|
|
|
|
<span class="item-meta">${formatFileSize(file.size)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="item-actions">
|
|
|
|
|
<button class="btn-action" data-action="preview" title="Preview">
|
|
|
|
|
<i class="bi bi-eye"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<button class="btn-action" data-action="rename" title="Rename">
|
|
|
|
|
<i class="bi bi-pencil"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<button class="btn-action btn-danger" data-action="delete" title="Delete">
|
|
|
|
|
<i class="bi bi-trash"></i>
|
|
|
|
|
</button>
|
2025-12-19 20:44:46 -06:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
2025-12-24 00:13:23 -06:00
|
|
|
});
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
content.innerHTML = html;
|
|
|
|
|
bindItemEvents();
|
|
|
|
|
}
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
function bindItemEvents() {
|
|
|
|
|
document.querySelectorAll(".media-item").forEach((item) => {
|
|
|
|
|
// Click
|
2025-12-24 00:13:23 -06:00
|
|
|
item.addEventListener("click", (e) => {
|
2026-01-18 02:22:05 -06:00
|
|
|
if (
|
|
|
|
|
e.target.closest(".item-actions") ||
|
|
|
|
|
e.target.closest(".item-checkbox")
|
|
|
|
|
)
|
2025-12-24 00:13:23 -06:00
|
|
|
return;
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
const type = item.dataset.type;
|
|
|
|
|
const id = parseInt(item.dataset.id);
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
if (type === "folder") {
|
|
|
|
|
currentFolder = id;
|
|
|
|
|
loadContent();
|
|
|
|
|
return;
|
2025-12-24 00:13:23 -06:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
toggleSelection(item);
|
2025-12-24 00:13:23 -06:00
|
|
|
});
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
// Checkbox
|
|
|
|
|
item
|
|
|
|
|
.querySelector(".item-checkbox input")
|
|
|
|
|
.addEventListener("change", (e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
toggleSelection(item);
|
2025-12-24 00:13:23 -06:00
|
|
|
});
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
// Actions
|
|
|
|
|
item.querySelectorAll(".btn-action").forEach((btn) => {
|
|
|
|
|
btn.addEventListener("click", (e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
const action = btn.dataset.action;
|
|
|
|
|
const type = item.dataset.type;
|
|
|
|
|
const id = parseInt(item.dataset.id);
|
|
|
|
|
const name = item.dataset.name;
|
|
|
|
|
|
|
|
|
|
switch (action) {
|
|
|
|
|
case "preview":
|
|
|
|
|
showPreview(item.dataset.path, name, item.dataset.size);
|
|
|
|
|
break;
|
|
|
|
|
case "rename":
|
|
|
|
|
showRenameModal(id, type, name);
|
|
|
|
|
break;
|
|
|
|
|
case "delete":
|
|
|
|
|
deleteItem(id, type, name);
|
|
|
|
|
break;
|
|
|
|
|
}
|
2025-12-24 00:13:23 -06:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
// Drag
|
|
|
|
|
item.addEventListener("dragstart", () => {
|
|
|
|
|
draggedItem = {
|
|
|
|
|
type: item.dataset.type,
|
|
|
|
|
id: parseInt(item.dataset.id),
|
|
|
|
|
};
|
|
|
|
|
item.classList.add("dragging");
|
2025-12-24 00:13:23 -06:00
|
|
|
});
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
item.addEventListener("dragend", () => {
|
|
|
|
|
item.classList.remove("dragging");
|
|
|
|
|
draggedItem = null;
|
2025-12-24 00:13:23 -06:00
|
|
|
});
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
if (item.dataset.type === "folder") {
|
|
|
|
|
item.addEventListener("dragover", (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (draggedItem && draggedItem.type === "file") {
|
|
|
|
|
item.classList.add("drag-over");
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
item.addEventListener("dragleave", () => {
|
|
|
|
|
item.classList.remove("drag-over");
|
|
|
|
|
});
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
item.addEventListener("drop", (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
item.classList.remove("drag-over");
|
|
|
|
|
if (draggedItem && draggedItem.type === "file") {
|
|
|
|
|
moveFileToFolder(draggedItem.id, parseInt(item.dataset.id));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-12-24 00:13:23 -06:00
|
|
|
});
|
2025-12-19 20:44:46 -06:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
function toggleSelection(item) {
|
|
|
|
|
const type = item.dataset.type;
|
|
|
|
|
const id = parseInt(item.dataset.id);
|
|
|
|
|
const path = item.dataset.path;
|
|
|
|
|
const name = item.dataset.name;
|
2025-12-19 20:44:46 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
const existingIndex = selectedItems.findIndex(
|
|
|
|
|
(s) => s.type === type && s.id === id,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (existingIndex >= 0) {
|
|
|
|
|
selectedItems.splice(existingIndex, 1);
|
|
|
|
|
item.classList.remove("selected");
|
|
|
|
|
item.querySelector(".item-checkbox input").checked = false;
|
2025-12-14 01:54:40 -06:00
|
|
|
} else {
|
2026-01-18 02:22:05 -06:00
|
|
|
selectedItems.push({ type, id, path, name });
|
|
|
|
|
item.classList.add("selected");
|
|
|
|
|
item.querySelector(".item-checkbox input").checked = true;
|
2025-12-14 01:54:40 -06:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
updateSelectedCount();
|
2025-12-14 01:54:40 -06:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
function updateSelectedCount() {
|
|
|
|
|
const count = selectedItems.length;
|
2025-12-14 01:54:40 -06:00
|
|
|
const countEl = document.getElementById("selectedCount");
|
|
|
|
|
const deleteBtn = document.getElementById("deleteSelectedBtn");
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
if (count > 0) {
|
|
|
|
|
countEl.style.display = "inline-flex";
|
|
|
|
|
countEl.querySelector(".count").textContent = count;
|
|
|
|
|
deleteBtn.style.display = "inline-flex";
|
2025-12-14 01:54:40 -06:00
|
|
|
} else {
|
|
|
|
|
countEl.style.display = "none";
|
2026-01-18 02:22:05 -06:00
|
|
|
deleteBtn.style.display = "none";
|
2025-12-24 00:13:23 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
function sortFiles(filesList) {
|
|
|
|
|
const sorted = [...filesList];
|
2025-12-19 20:44:46 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
switch (sortBy) {
|
|
|
|
|
case "date":
|
|
|
|
|
sorted.sort(
|
|
|
|
|
(a, b) => new Date(b.uploadDate) - new Date(a.uploadDate),
|
|
|
|
|
);
|
|
|
|
|
break;
|
|
|
|
|
case "date-asc":
|
|
|
|
|
sorted.sort(
|
|
|
|
|
(a, b) => new Date(a.uploadDate) - new Date(b.uploadDate),
|
|
|
|
|
);
|
|
|
|
|
break;
|
|
|
|
|
case "name":
|
|
|
|
|
sorted.sort((a, b) => a.originalName.localeCompare(b.originalName));
|
|
|
|
|
break;
|
|
|
|
|
case "name-desc":
|
|
|
|
|
sorted.sort((a, b) => b.originalName.localeCompare(a.originalName));
|
|
|
|
|
break;
|
|
|
|
|
case "size":
|
|
|
|
|
sorted.sort((a, b) => b.size - a.size);
|
|
|
|
|
break;
|
|
|
|
|
case "size-asc":
|
|
|
|
|
sorted.sort((a, b) => a.size - b.size);
|
|
|
|
|
break;
|
2025-12-14 01:54:40 -06:00
|
|
|
}
|
2025-12-19 20:44:46 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
return sorted;
|
2025-12-19 20:44:46 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateBreadcrumb() {
|
|
|
|
|
const breadcrumb = document.getElementById("breadcrumb");
|
2026-01-18 02:22:05 -06:00
|
|
|
const backBtn = document.getElementById("backBtn");
|
|
|
|
|
let html = `<li class="breadcrumb-item"><a href="#" onclick="navigateToFolder(null); return false;">Root</a></li>`;
|
2025-12-19 20:44:46 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
// Show/hide back button
|
|
|
|
|
if (currentFolder) {
|
|
|
|
|
backBtn.style.display = "inline-flex";
|
|
|
|
|
} else {
|
|
|
|
|
backBtn.style.display = "none";
|
2025-12-19 20:44:46 -06:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
if (currentFolder) {
|
|
|
|
|
const folder = folders.find((f) => f.id === currentFolder);
|
|
|
|
|
if (folder) {
|
|
|
|
|
const pathParts = folder.path.split("/").filter((p) => p);
|
|
|
|
|
let currentPath = "";
|
|
|
|
|
|
|
|
|
|
pathParts.forEach((part, index) => {
|
|
|
|
|
currentPath += "/" + part;
|
|
|
|
|
const pathFolder = folders.find((f) => f.path === currentPath);
|
|
|
|
|
const isLast = index === pathParts.length - 1;
|
|
|
|
|
|
|
|
|
|
if (isLast) {
|
|
|
|
|
html += `<li class="breadcrumb-item active">${escapeHtml(
|
|
|
|
|
part,
|
|
|
|
|
)}</li>`;
|
|
|
|
|
} else if (pathFolder) {
|
|
|
|
|
html += `<li class="breadcrumb-item"><a href="#" onclick="navigateToFolder(${
|
|
|
|
|
pathFolder.id
|
|
|
|
|
}); return false;">${escapeHtml(part)}</a></li>`;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-12-24 00:13:23 -06:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
breadcrumb.innerHTML = html;
|
2025-12-19 20:44:46 -06:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
function navigateBack() {
|
|
|
|
|
if (!currentFolder) return;
|
|
|
|
|
|
|
|
|
|
const currentFolderObj = folders.find((f) => f.id === currentFolder);
|
|
|
|
|
if (currentFolderObj && currentFolderObj.parentId) {
|
|
|
|
|
currentFolder = currentFolderObj.parentId;
|
2025-12-24 00:13:23 -06:00
|
|
|
} else {
|
2026-01-18 02:22:05 -06:00
|
|
|
currentFolder = null;
|
2025-12-24 00:13:23 -06:00
|
|
|
}
|
2025-12-14 01:54:40 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
loadContent();
|
2025-12-14 01:54:40 -06:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
function navigateToFolder(folderId) {
|
|
|
|
|
currentFolder = folderId;
|
|
|
|
|
loadContent();
|
2025-12-14 01:54:40 -06:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
async function uploadFiles(fileList) {
|
|
|
|
|
const progressEl = document.getElementById("uploadProgress");
|
|
|
|
|
const progressBar = document.getElementById("uploadProgressBar");
|
|
|
|
|
const progressPercent = document.getElementById("uploadPercent");
|
2025-12-19 20:44:46 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
progressEl.style.display = "block";
|
|
|
|
|
progressBar.style.width = "0%";
|
2025-12-14 01:54:40 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
const formData = new FormData();
|
|
|
|
|
Array.from(fileList).forEach((file) => formData.append("files", file));
|
|
|
|
|
if (currentFolder) {
|
|
|
|
|
formData.append("folder_id", currentFolder);
|
2025-12-19 20:44:46 -06:00
|
|
|
}
|
|
|
|
|
|
2025-12-14 01:54:40 -06:00
|
|
|
try {
|
2026-01-18 02:22:05 -06:00
|
|
|
const response = await fetch("/api/admin/upload", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: formData,
|
|
|
|
|
credentials: "include",
|
2025-12-14 01:54:40 -06:00
|
|
|
});
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
const data = await response.json();
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
if (data.success) {
|
|
|
|
|
progressBar.style.width = "100%";
|
|
|
|
|
progressPercent.textContent = "100%";
|
|
|
|
|
showToast(
|
|
|
|
|
`${data.files.length} file(s) uploaded successfully`,
|
|
|
|
|
"success",
|
|
|
|
|
);
|
|
|
|
|
await loadContent();
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error(data.message || "Upload failed");
|
|
|
|
|
}
|
2025-12-14 01:54:40 -06:00
|
|
|
} catch (error) {
|
2026-01-18 02:22:05 -06:00
|
|
|
console.error("Upload error:", error);
|
|
|
|
|
showToast(error.message || "Upload failed", "error");
|
|
|
|
|
} finally {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
progressEl.style.display = "none";
|
|
|
|
|
progressBar.style.width = "0%";
|
|
|
|
|
}, 1000);
|
2025-12-14 01:54:40 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
document.getElementById("fileInput").value = "";
|
|
|
|
|
}
|
2025-12-19 20:44:46 -06:00
|
|
|
}
|
2025-12-14 01:54:40 -06:00
|
|
|
|
2025-12-19 20:44:46 -06:00
|
|
|
async function createFolder() {
|
2026-01-18 02:22:05 -06:00
|
|
|
const name = document.getElementById("newFolderInput").value.trim();
|
2025-12-19 20:44:46 -06:00
|
|
|
|
|
|
|
|
if (!name) {
|
2026-01-18 02:22:05 -06:00
|
|
|
showToast("Please enter a folder name", "error");
|
2025-12-19 20:44:46 -06:00
|
|
|
return;
|
|
|
|
|
}
|
2025-12-14 01:54:40 -06:00
|
|
|
|
|
|
|
|
try {
|
2025-12-19 20:44:46 -06:00
|
|
|
const response = await fetch("/api/admin/folders", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
2026-01-18 02:22:05 -06:00
|
|
|
body: JSON.stringify({ name, parent_id: currentFolder }),
|
2025-12-14 01:54:40 -06:00
|
|
|
credentials: "include",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
2025-12-19 20:44:46 -06:00
|
|
|
|
2025-12-14 01:54:40 -06:00
|
|
|
if (data.success) {
|
2026-01-18 02:22:05 -06:00
|
|
|
bootstrap.Modal.getInstance(
|
|
|
|
|
document.getElementById("createFolderModal"),
|
|
|
|
|
).hide();
|
|
|
|
|
showToast("Folder created successfully", "success");
|
|
|
|
|
await loadContent();
|
2025-12-19 20:44:46 -06:00
|
|
|
} else {
|
2026-01-18 02:22:05 -06:00
|
|
|
throw new Error(data.error || "Failed to create folder");
|
2025-12-14 01:54:40 -06:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2026-01-18 02:22:05 -06:00
|
|
|
console.error("Create folder error:", error);
|
|
|
|
|
showToast(error.message || "Failed to create folder", "error");
|
2025-12-14 01:54:40 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
function showRenameModal(id, type, currentName) {
|
|
|
|
|
document.getElementById("renameItemId").value = id;
|
|
|
|
|
document.getElementById("renameItemType").value = type;
|
2025-12-14 01:54:40 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
let displayName = currentName;
|
|
|
|
|
if (type === "file") {
|
|
|
|
|
const lastDot = currentName.lastIndexOf(".");
|
|
|
|
|
if (lastDot > 0) {
|
|
|
|
|
displayName = currentName.substring(0, lastDot);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-14 01:54:40 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
document.getElementById("renameInput").value = displayName;
|
|
|
|
|
new bootstrap.Modal(document.getElementById("renameModal")).show();
|
2025-12-14 01:54:40 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
setTimeout(() => document.getElementById("renameInput").select(), 100);
|
2025-12-24 00:13:23 -06:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
async function confirmRename() {
|
|
|
|
|
const id = parseInt(document.getElementById("renameItemId").value);
|
|
|
|
|
const type = document.getElementById("renameItemType").value;
|
|
|
|
|
const newName = document.getElementById("renameInput").value.trim();
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
if (!newName) {
|
|
|
|
|
showToast("Please enter a name", "error");
|
|
|
|
|
return;
|
2025-12-24 00:13:23 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2026-01-18 02:22:05 -06:00
|
|
|
const endpoint =
|
|
|
|
|
type === "folder"
|
|
|
|
|
? `/api/admin/folders/${id}/rename`
|
|
|
|
|
: `/api/admin/uploads/${id}/rename`;
|
|
|
|
|
|
|
|
|
|
const response = await fetch(endpoint, {
|
2025-12-24 00:13:23 -06:00
|
|
|
method: "PATCH",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
2026-01-18 02:22:05 -06:00
|
|
|
body: JSON.stringify({ newName }),
|
2025-12-24 00:13:23 -06:00
|
|
|
credentials: "include",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
2026-01-18 02:22:05 -06:00
|
|
|
|
2025-12-24 00:13:23 -06:00
|
|
|
if (data.success) {
|
2026-01-18 02:22:05 -06:00
|
|
|
bootstrap.Modal.getInstance(
|
|
|
|
|
document.getElementById("renameModal"),
|
|
|
|
|
).hide();
|
|
|
|
|
showToast("Renamed successfully", "success");
|
|
|
|
|
await loadContent();
|
2025-12-24 00:13:23 -06:00
|
|
|
} else {
|
2026-01-18 02:22:05 -06:00
|
|
|
throw new Error(data.error || "Failed to rename");
|
2025-12-24 00:13:23 -06:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2026-01-18 02:22:05 -06:00
|
|
|
console.error("Rename error:", error);
|
|
|
|
|
showToast(error.message || "Failed to rename", "error");
|
2025-12-14 01:54:40 -06:00
|
|
|
}
|
2025-12-19 20:44:46 -06:00
|
|
|
}
|
2025-12-14 01:54:40 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
async function deleteItem(id, type, name) {
|
|
|
|
|
if (!confirm(`Are you sure you want to delete "${name}"?`)) return;
|
2025-12-19 20:44:46 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
try {
|
|
|
|
|
const endpoint =
|
|
|
|
|
type === "folder"
|
|
|
|
|
? `/api/admin/folders/${id}?delete_contents=true`
|
|
|
|
|
: `/api/admin/uploads/id/${id}`;
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
const response = await fetch(endpoint, {
|
|
|
|
|
method: "DELETE",
|
|
|
|
|
credentials: "include",
|
|
|
|
|
});
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
const data = await response.json();
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
if (data.success) {
|
|
|
|
|
showToast("Deleted successfully", "success");
|
|
|
|
|
selectedItems = selectedItems.filter(
|
|
|
|
|
(s) => !(s.type === type && s.id === id),
|
|
|
|
|
);
|
|
|
|
|
updateSelectedCount();
|
|
|
|
|
await loadContent();
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error(data.error || "Failed to delete");
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Delete error:", error);
|
|
|
|
|
showToast(error.message || "Failed to delete", "error");
|
2025-12-24 00:13:23 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
async function deleteSelected() {
|
|
|
|
|
const fileIds = selectedItems
|
|
|
|
|
.filter((s) => s.type === "file")
|
|
|
|
|
.map((s) => s.id);
|
|
|
|
|
const folderIds = selectedItems
|
|
|
|
|
.filter((s) => s.type === "folder")
|
|
|
|
|
.map((s) => s.id);
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
const total = fileIds.length + folderIds.length;
|
|
|
|
|
if (total === 0) return;
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
if (!confirm(`Are you sure you want to delete ${total} item(s)?`))
|
|
|
|
|
return;
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
try {
|
|
|
|
|
for (const folderId of folderIds) {
|
|
|
|
|
await fetch(`/api/admin/folders/${folderId}?delete_contents=true`, {
|
|
|
|
|
method: "DELETE",
|
|
|
|
|
credentials: "include",
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
if (fileIds.length > 0) {
|
|
|
|
|
await fetch("/api/admin/uploads/bulk-delete", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
body: JSON.stringify({ file_ids: fileIds }),
|
|
|
|
|
credentials: "include",
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
showToast(`${total} item(s) deleted`, "success");
|
|
|
|
|
selectedItems = [];
|
|
|
|
|
updateSelectedCount();
|
|
|
|
|
await loadContent();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Bulk delete error:", error);
|
|
|
|
|
showToast("Failed to delete some items", "error");
|
2025-12-24 00:13:23 -06:00
|
|
|
}
|
2026-01-18 02:22:05 -06:00
|
|
|
}
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
async function moveFileToFolder(fileId, folderId) {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch("/api/admin/uploads/move", {
|
|
|
|
|
method: "PATCH",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
body: JSON.stringify({ file_ids: [fileId], folder_id: folderId }),
|
|
|
|
|
credentials: "include",
|
2025-12-24 00:13:23 -06:00
|
|
|
});
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
const data = await response.json();
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
if (data.success) {
|
|
|
|
|
showToast("File moved successfully", "success");
|
|
|
|
|
await loadContent();
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error(data.error || "Failed to move file");
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Move error:", error);
|
|
|
|
|
showToast(error.message || "Failed to move file", "error");
|
2025-12-24 00:13:23 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
function showPreview(path, name, size) {
|
|
|
|
|
const overlay = document.getElementById("imagePreviewOverlay");
|
|
|
|
|
document.getElementById("previewImage").src = path;
|
|
|
|
|
document.getElementById("previewFilename").textContent = name;
|
|
|
|
|
document.getElementById("previewSize").textContent = formatFileSize(
|
|
|
|
|
parseInt(size),
|
|
|
|
|
);
|
|
|
|
|
overlay.classList.add("active");
|
2025-12-24 00:13:23 -06:00
|
|
|
document.body.style.overflow = "hidden";
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
function closePreview() {
|
|
|
|
|
const overlay = document.getElementById("imagePreviewOverlay");
|
|
|
|
|
overlay.classList.remove("active");
|
2025-12-24 00:13:23 -06:00
|
|
|
document.body.style.overflow = "";
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
function showToast(message, type = "info") {
|
|
|
|
|
const toast = document.createElement("div");
|
|
|
|
|
toast.className = `media-toast ${type}`;
|
|
|
|
|
toast.innerHTML = `
|
|
|
|
|
<i class="bi bi-${
|
|
|
|
|
type === "success"
|
|
|
|
|
? "check-circle"
|
|
|
|
|
: type === "error"
|
|
|
|
|
? "exclamation-circle"
|
|
|
|
|
: "info-circle"
|
|
|
|
|
}"></i>
|
|
|
|
|
<span>${escapeHtml(message)}</span>
|
|
|
|
|
`;
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
document.body.appendChild(toast);
|
|
|
|
|
setTimeout(() => toast.classList.add("show"), 10);
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
setTimeout(() => {
|
|
|
|
|
toast.classList.remove("show");
|
|
|
|
|
setTimeout(() => toast.remove(), 300);
|
|
|
|
|
}, 3000);
|
|
|
|
|
}
|
2025-12-24 00:13:23 -06:00
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
function formatFileSize(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 escapeHtml(text) {
|
|
|
|
|
const div = document.createElement("div");
|
|
|
|
|
div.textContent = text;
|
|
|
|
|
return div.innerHTML;
|
|
|
|
|
}
|
2025-12-14 01:54:40 -06:00
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|