574 lines
16 KiB
PHP
574 lines
16 KiB
PHP
<style>
|
|
.s3-page {
|
|
min-height: calc(100vh - 130px);
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
padding: 2.75rem 0 3rem;
|
|
box-sizing: border-box;
|
|
}
|
|
.s3-browser {
|
|
--s3-primary: #02a8b5;
|
|
--s3-primary-deep: #028f9a;
|
|
--s3-ink: #333333;
|
|
--s3-muted: #6c757d;
|
|
--s3-line: #e3eef0;
|
|
--s3-soft: #f5ffff;
|
|
--s3-white: #ffffff;
|
|
font-family: "Poppins", system-ui, sans-serif;
|
|
color: var(--s3-ink);
|
|
width: 100%;
|
|
}
|
|
|
|
.s3-browser .s3-header {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 1rem;
|
|
margin-bottom: 1.25rem;
|
|
padding-top: 0.35rem;
|
|
}
|
|
.s3-browser .s3-header h3 {
|
|
margin: 0;
|
|
font-size: 1.45rem;
|
|
font-weight: 600;
|
|
color: var(--s3-ink);
|
|
}
|
|
.s3-browser .s3-header p {
|
|
margin: 0.3rem 0 0;
|
|
color: var(--s3-muted);
|
|
font-size: 0.9rem;
|
|
}
|
|
.s3-browser .bucket-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
background: var(--s3-soft);
|
|
border: 1px solid #b2ebf2;
|
|
border-radius: 999px;
|
|
padding: 0.4rem 0.85rem;
|
|
color: var(--s3-ink);
|
|
font-size: 0.82rem;
|
|
white-space: nowrap;
|
|
}
|
|
.s3-browser .bucket-pill i {
|
|
color: var(--s3-primary);
|
|
}
|
|
|
|
.s3-browser .panel,
|
|
.s3-browser .table-wrap {
|
|
background: var(--s3-white);
|
|
border: 1px solid var(--s3-line);
|
|
border-radius: 12px;
|
|
box-shadow: 0 1px 2px rgba(2, 168, 181, 0.05);
|
|
}
|
|
.s3-browser .panel {
|
|
padding: 1rem 1.1rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.s3-browser .toolbar-grid {
|
|
display: grid;
|
|
grid-template-columns: minmax(200px, 1.1fr) minmax(240px, 2.4fr) 120px;
|
|
gap: 1rem;
|
|
align-items: end;
|
|
}
|
|
.s3-browser .toolbar-field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0;
|
|
margin: 0;
|
|
}
|
|
.s3-browser .toolbar label {
|
|
display: block;
|
|
font-size: 0.8rem;
|
|
color: var(--s3-muted);
|
|
margin-bottom: 0.4rem;
|
|
font-weight: 500;
|
|
line-height: 1.2;
|
|
min-height: 1.2em;
|
|
}
|
|
.s3-browser .toolbar .control-row {
|
|
display: flex;
|
|
align-items: stretch;
|
|
min-height: 42px;
|
|
}
|
|
.s3-browser .toolbar .control-row > .form-control,
|
|
.s3-browser .toolbar .control-row > .select2-container,
|
|
.s3-browser .toolbar .control-row > .btn-search {
|
|
width: 100%;
|
|
}
|
|
.s3-browser .form-control,
|
|
.s3-browser .custom-select {
|
|
border-radius: 8px !important;
|
|
border: 1px solid #d7e6e8 !important;
|
|
height: 42px;
|
|
color: var(--s3-ink) !important;
|
|
background: #fff !important;
|
|
margin: 0;
|
|
}
|
|
.s3-browser .form-control:focus,
|
|
.s3-browser .custom-select:focus {
|
|
border-color: var(--s3-primary) !important;
|
|
box-shadow: 0 0 0 0.15rem rgba(2, 168, 181, 0.15) !important;
|
|
}
|
|
|
|
.s3-browser .btn-search {
|
|
height: 42px;
|
|
width: 100%;
|
|
border-radius: 8px;
|
|
background: var(--s3-primary);
|
|
color: #fff;
|
|
border: none;
|
|
font-weight: 600;
|
|
padding: 0 1rem;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.s3-browser .btn-search:hover {
|
|
background: var(--s3-primary-deep);
|
|
color: #fff;
|
|
}
|
|
.s3-browser .btn-ghost {
|
|
border-radius: 8px;
|
|
border: 1px solid #d7e6e8;
|
|
background: #fff;
|
|
color: var(--s3-ink);
|
|
}
|
|
.s3-browser .btn-ghost:hover {
|
|
background: var(--s3-soft);
|
|
color: var(--s3-ink);
|
|
border-color: #b2ebf2;
|
|
}
|
|
.s3-browser .btn-next {
|
|
border-radius: 8px;
|
|
border: none;
|
|
background: var(--s3-primary);
|
|
color: #fff;
|
|
font-weight: 600;
|
|
}
|
|
.s3-browser .btn-next:hover {
|
|
background: var(--s3-primary-deep);
|
|
color: #fff;
|
|
}
|
|
.s3-browser .btn-next:disabled,
|
|
.s3-browser .btn-ghost:disabled,
|
|
.s3-browser .btn-search:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.s3-browser .table-wrap {
|
|
overflow: hidden;
|
|
}
|
|
.s3-browser table {
|
|
margin: 0;
|
|
}
|
|
.s3-browser thead th {
|
|
background: var(--s3-soft);
|
|
border-bottom: 1px solid var(--s3-line);
|
|
border-top: 0;
|
|
color: var(--s3-muted);
|
|
font-size: 0.78rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.03em;
|
|
padding: 0.85rem 1rem;
|
|
}
|
|
.s3-browser tbody td {
|
|
vertical-align: middle;
|
|
border-top: 1px solid var(--s3-line);
|
|
padding: 0.85rem 1rem;
|
|
color: var(--s3-ink);
|
|
font-size: 0.9rem;
|
|
background: #fff;
|
|
}
|
|
.s3-browser tbody tr:hover td {
|
|
background: #f7fcfc;
|
|
}
|
|
|
|
.s3-browser .file-cell {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 0.7rem;
|
|
}
|
|
.s3-browser .file-icon {
|
|
width: 34px;
|
|
height: 34px;
|
|
border-radius: 8px;
|
|
background: #e0f7fa;
|
|
color: var(--s3-primary);
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
font-size: 1.05rem;
|
|
}
|
|
.s3-browser .file-name {
|
|
font-weight: 500;
|
|
word-break: break-all;
|
|
color: var(--s3-ink);
|
|
line-height: 1.3;
|
|
}
|
|
.s3-browser .file-key {
|
|
display: block;
|
|
color: var(--s3-muted);
|
|
font-size: 0.78rem;
|
|
margin-top: 0.15rem;
|
|
}
|
|
.s3-browser .badge-soft {
|
|
background: #e0f7fa;
|
|
color: #027a84;
|
|
border-radius: 999px;
|
|
font-weight: 500;
|
|
padding: 0.28rem 0.65rem;
|
|
font-size: 0.78rem;
|
|
}
|
|
|
|
.s3-browser .pager {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 0.75rem;
|
|
padding: 0.85rem 1rem;
|
|
border-top: 1px solid var(--s3-line);
|
|
background: #fafefe;
|
|
}
|
|
.s3-browser .pager .meta {
|
|
color: var(--s3-muted);
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.s3-browser .empty-state,
|
|
.s3-browser .loading-state {
|
|
text-align: center;
|
|
padding: 3rem 1rem;
|
|
color: var(--s3-muted);
|
|
}
|
|
.s3-browser .empty-state i,
|
|
.s3-browser .loading-state i {
|
|
font-size: 2rem;
|
|
color: var(--s3-primary);
|
|
display: block;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
.s3-browser .alert-local {
|
|
border-radius: 10px;
|
|
border: 1px solid #ffe082;
|
|
background: #fff8e1;
|
|
color: #8d6e00;
|
|
padding: 0.75rem 1rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.s3-browser .select2-container {
|
|
width: 100% !important;
|
|
}
|
|
.s3-browser .select2-container .select2-selection--single {
|
|
height: 42px !important;
|
|
border: 1px solid #d7e6e8 !important;
|
|
border-radius: 8px !important;
|
|
padding: 0.35rem 0.5rem;
|
|
background: #fff !important;
|
|
}
|
|
.s3-browser .select2-container--default.select2-container--focus .select2-selection--single {
|
|
border-color: var(--s3-primary) !important;
|
|
}
|
|
.s3-browser .select2-container--default .select2-selection--single .select2-selection__rendered {
|
|
line-height: 1.85 !important;
|
|
padding-left: 0.1rem;
|
|
color: var(--s3-ink) !important;
|
|
}
|
|
.s3-browser .select2-container--default .select2-selection--single .select2-selection__placeholder {
|
|
color: #adb5bd !important;
|
|
}
|
|
.s3-browser .select2-container--default .select2-selection--single .select2-selection__arrow {
|
|
height: 40px !important;
|
|
top: 1px !important;
|
|
}
|
|
.s3-browser .select2-dropdown {
|
|
border-radius: 8px;
|
|
border-color: #d7e6e8;
|
|
overflow: hidden;
|
|
}
|
|
.s3-browser .select2-search--dropdown .select2-search__field {
|
|
border-radius: 6px;
|
|
border-color: #d7e6e8;
|
|
}
|
|
.s3-browser .select2-container--default .select2-results__option--highlighted.select2-results__option--selectable {
|
|
background-color: var(--s3-primary) !important;
|
|
}
|
|
|
|
@media (max-width: 767.98px) {
|
|
.s3-browser .s3-header {
|
|
flex-direction: column;
|
|
}
|
|
.s3-browser .toolbar-grid {
|
|
grid-template-columns: 1fr;
|
|
gap: 0.85rem;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<div class="s3-page">
|
|
<div class="s3-browser">
|
|
<div class="s3-header">
|
|
<div>
|
|
<h3>File browser</h3>
|
|
<p>Browse upload objects by folder. Results load 10 at a time.</p>
|
|
</div>
|
|
<div class="bucket-pill">
|
|
<i class="mdi mdi-database"></i>
|
|
<span id="s3-bucket-label"><?= esc($bucket ?? 'Bucket not configured') ?></span>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if (empty($uses_s3)): ?>
|
|
<div class="alert-local">
|
|
FILE_STORAGE_DRIVER is not set to <strong>s3</strong>, or the upload bucket is missing. Listing is disabled.
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="panel toolbar">
|
|
<div class="toolbar-grid">
|
|
<div class="toolbar-field">
|
|
<label for="s3-folder">Folder</label>
|
|
<div class="control-row">
|
|
<select id="s3-folder" class="form-control custom-select" data-placeholder="Search or select folder" <?= empty($uses_s3) ? 'disabled' : '' ?>>
|
|
<option value="">Select folder</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="toolbar-field">
|
|
<label for="s3-search">File name</label>
|
|
<div class="control-row">
|
|
<input type="text" id="s3-search" class="form-control" placeholder="Search file name…" <?= empty($uses_s3) ? 'disabled' : '' ?>>
|
|
</div>
|
|
</div>
|
|
<div class="toolbar-field">
|
|
<label for="s3-search-btn"> </label>
|
|
<div class="control-row">
|
|
<button type="button" id="s3-search-btn" class="btn btn-search" <?= empty($uses_s3) ? 'disabled' : '' ?>>
|
|
Search
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-wrap">
|
|
<div class="table-responsive">
|
|
<table class="table mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:48%">File name</th>
|
|
<th>Size</th>
|
|
<th>Last modified</th>
|
|
<th class="text-right">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="s3-file-rows">
|
|
<tr>
|
|
<td colspan="4">
|
|
<div class="empty-state">
|
|
<i class="mdi mdi-folder-open-outline"></i>
|
|
Select a folder to load files
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="pager">
|
|
<div class="meta" id="s3-page-meta">Page —</div>
|
|
<div class="d-flex" style="gap:0.5rem;">
|
|
<button type="button" class="btn btn-sm btn-ghost" id="s3-prev-btn" disabled>Previous</button>
|
|
<button type="button" class="btn btn-sm btn-next" id="s3-next-btn" disabled>Next</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
const USES_S3 = <?= ! empty($uses_s3) ? 'true' : 'false' ?>;
|
|
const LIST_URL = <?= json_encode($list_url ?? '') ?>;
|
|
const FOLDERS_URL = <?= json_encode($folders_url ?? '') ?>;
|
|
const PER_PAGE = 10;
|
|
|
|
let page = 1;
|
|
let searchTimer = null;
|
|
|
|
const elFolder = document.getElementById('s3-folder');
|
|
const elSearch = document.getElementById('s3-search');
|
|
const elRows = document.getElementById('s3-file-rows');
|
|
const elMeta = document.getElementById('s3-page-meta');
|
|
const elPrev = document.getElementById('s3-prev-btn');
|
|
const elNext = document.getElementById('s3-next-btn');
|
|
const elBucket = document.getElementById('s3-bucket-label');
|
|
|
|
function escapeHtml(str) {
|
|
return String(str)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function fileIconClass(name) {
|
|
const ext = String(name).split('.').pop().toLowerCase();
|
|
if (['xls', 'xlsx', 'csv', 'ods'].indexOf(ext) !== -1) return 'mdi mdi-file-excel';
|
|
if (['pdf'].indexOf(ext) !== -1) return 'mdi mdi-file-pdf-box';
|
|
if (['png', 'jpg', 'jpeg', 'gif', 'webp'].indexOf(ext) !== -1) return 'mdi mdi-file-image';
|
|
if (['zip', 'rar', '7z'].indexOf(ext) !== -1) return 'mdi mdi-zip-box';
|
|
return 'mdi mdi-file-document-outline';
|
|
}
|
|
|
|
function setLoading() {
|
|
elRows.innerHTML = '<tr><td colspan="4"><div class="loading-state"><i class="mdi mdi-loading mdi-spin"></i>Loading files…</div></td></tr>';
|
|
elPrev.disabled = true;
|
|
elNext.disabled = true;
|
|
}
|
|
|
|
function setEmpty(message) {
|
|
elRows.innerHTML = '<tr><td colspan="4"><div class="empty-state"><i class="mdi mdi-file-search-outline"></i>' + escapeHtml(message) + '</div></td></tr>';
|
|
}
|
|
|
|
function renderRows(files) {
|
|
if (!files || !files.length) {
|
|
setEmpty('No files found for this folder / search');
|
|
return;
|
|
}
|
|
elRows.innerHTML = files.map(function (f) {
|
|
const download = f.download_url
|
|
? '<a class="btn btn-sm btn-ghost" href="' + escapeHtml(f.download_url) + '" target="_blank" rel="noopener">Download</a>'
|
|
: '<span class="text-muted">—</span>';
|
|
return '<tr>' +
|
|
'<td><div class="file-cell">' +
|
|
'<span class="file-icon"><i class="' + fileIconClass(f.file_name) + '"></i></span>' +
|
|
'<div><div class="file-name">' + escapeHtml(f.file_name) + '</div>' +
|
|
'<span class="file-key">' + escapeHtml(f.key) + '</span></div>' +
|
|
'</div></td>' +
|
|
'<td><span class="badge-soft">' + escapeHtml(f.size_label || '') + '</span></td>' +
|
|
'<td>' + escapeHtml(f.last_modified || '') + '</td>' +
|
|
'<td class="text-right">' + download + '</td>' +
|
|
'</tr>';
|
|
}).join('');
|
|
}
|
|
|
|
function initFolderSelect() {
|
|
if (typeof jQuery === 'undefined' || !jQuery.fn.select2) {
|
|
return;
|
|
}
|
|
const $folder = jQuery('#s3-folder');
|
|
if ($folder.hasClass('select2-hidden-accessible')) {
|
|
$folder.select2('destroy');
|
|
}
|
|
$folder.select2({
|
|
placeholder: 'Search or select folder',
|
|
allowClear: true,
|
|
width: '100%',
|
|
dropdownParent: jQuery('.s3-browser')
|
|
});
|
|
$folder.off('change.s3browser').on('change.s3browser', function () {
|
|
page = 1;
|
|
loadFiles();
|
|
});
|
|
}
|
|
|
|
function loadFolders() {
|
|
if (!USES_S3) return;
|
|
fetch(FOLDERS_URL, { credentials: 'same-origin' })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (data) {
|
|
if (data.bucket) elBucket.textContent = data.bucket;
|
|
const folders = data.folders || [];
|
|
elFolder.innerHTML = '<option value="">Select folder</option>' +
|
|
folders.map(function (f) {
|
|
return '<option value="' + escapeHtml(f) + '">' + escapeHtml(f) + '</option>';
|
|
}).join('');
|
|
initFolderSelect();
|
|
})
|
|
.catch(function () {
|
|
setEmpty('Failed to load folders');
|
|
});
|
|
}
|
|
|
|
function loadFiles() {
|
|
if (!USES_S3) return;
|
|
const folder = elFolder.value;
|
|
if (!folder) {
|
|
setEmpty('Select a folder to load files');
|
|
elMeta.textContent = 'Page —';
|
|
elPrev.disabled = true;
|
|
elNext.disabled = true;
|
|
return;
|
|
}
|
|
|
|
setLoading();
|
|
const params = new URLSearchParams({
|
|
folder: folder,
|
|
search: elSearch.value.trim(),
|
|
page: String(page),
|
|
per_page: String(PER_PAGE)
|
|
});
|
|
|
|
fetch(LIST_URL + '?' + params.toString(), { credentials: 'same-origin' })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (data) {
|
|
if (!data.success) {
|
|
setEmpty(data.message || 'Failed to load files');
|
|
elMeta.textContent = 'Page —';
|
|
return;
|
|
}
|
|
renderRows(data.files || []);
|
|
const noun = (data.count === 1) ? 'file' : 'files';
|
|
elMeta.textContent = 'Page ' + data.page + ' · showing ' + (data.count || 0) + ' ' + noun;
|
|
elPrev.disabled = !data.has_previous;
|
|
elNext.disabled = !data.has_more;
|
|
})
|
|
.catch(function () {
|
|
setEmpty('Failed to load files');
|
|
});
|
|
}
|
|
|
|
document.getElementById('s3-search-btn').addEventListener('click', function () {
|
|
page = 1;
|
|
loadFiles();
|
|
});
|
|
|
|
elSearch.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
page = 1;
|
|
loadFiles();
|
|
}
|
|
});
|
|
|
|
elSearch.addEventListener('input', function () {
|
|
clearTimeout(searchTimer);
|
|
searchTimer = setTimeout(function () {
|
|
page = 1;
|
|
loadFiles();
|
|
}, 400);
|
|
});
|
|
|
|
elPrev.addEventListener('click', function () {
|
|
if (page > 1) {
|
|
page -= 1;
|
|
loadFiles();
|
|
}
|
|
});
|
|
|
|
elNext.addEventListener('click', function () {
|
|
page += 1;
|
|
loadFiles();
|
|
});
|
|
|
|
loadFolders();
|
|
})();
|
|
</script>
|