Added Cloudflare R2 media storage support
Build and Publish / Build and Publish Docker Image (push) Successful in 1m44s

This commit is contained in:
Aleksandr Bogomiakov
2026-08-09 01:05:47 +01:00
parent 9ee5048a43
commit 15c9528f47
14 changed files with 885 additions and 116 deletions
+72 -5
View File
@@ -21,6 +21,7 @@
<input class="input" type="file" id="uploadFiles" name="files" multiple accept="image/*,video/*" required>
</div>
<p id="fileCount" style="font-size:0.8rem;color:#888;margin-top:0.3rem;"></p>
<div id="uploadQueue" class="upload-queue"></div>
</div>
<div class="field">
@@ -49,6 +50,7 @@
var form = document.getElementById('uploadForm');
var filesInput = document.getElementById('uploadFiles');
var fileCount = document.getElementById('fileCount');
var queue = document.getElementById('uploadQueue');
var progress = document.getElementById('uploadProgress');
var bar = document.getElementById('uploadBar');
var percent = document.getElementById('uploadPercent');
@@ -57,9 +59,41 @@
filesInput.addEventListener('change', function() {
var n = this.files.length;
fileCount.textContent = n > 0 ? ('Выбрано файлов: ' + n) : '';
fileCount.textContent = n > 0 ? ('{{ t.media_files_selected }}: ' + n) : '';
queue.replaceChildren();
Array.from(this.files).forEach(function(file) {
var item = document.createElement('div');
item.className = 'upload-queue-item';
var icon = document.createElement('span');
icon.className = 'upload-queue-icon';
icon.textContent = file.type.indexOf('video/') === 0 ? '🎬' : '🖼️';
var details = document.createElement('div');
var name = document.createElement('div');
name.className = 'upload-queue-name';
name.textContent = file.name;
var state = document.createElement('div');
state.className = 'upload-queue-state';
state.textContent = '0%';
var track = document.createElement('div');
track.className = 'upload-queue-track';
var itemBar = document.createElement('div');
itemBar.className = 'upload-queue-bar';
track.appendChild(itemBar);
details.append(name, state, track);
item.append(icon, details);
queue.appendChild(item);
});
});
function updateQueue(state, progressValue, processing) {
queue.querySelectorAll('.upload-queue-item').forEach(function(item) {
item.querySelector('.upload-queue-state').textContent = state;
var itemBar = item.querySelector('.upload-queue-bar');
itemBar.classList.toggle('is-processing', processing);
if (!processing) itemBar.style.width = progressValue + '%';
});
}
form.addEventListener('submit', function(e) {
e.preventDefault();
if (!filesInput.files.length) return;
@@ -69,7 +103,9 @@
progress.style.display = 'block';
submitBtn.disabled = true;
submitBtn.textContent = 'Загрузка...';
submitBtn.textContent = '{{ t.media_upload_sending }}';
statusText.textContent = '{{ t.media_upload_sending }}';
bar.classList.remove('is-processing');
bar.style.width = '0%';
percent.textContent = '0%';
@@ -78,24 +114,42 @@
var pct = Math.round(ev.loaded / ev.total * 100);
bar.style.width = pct + '%';
percent.textContent = pct + '%';
if (pct === 100) statusText.textContent = 'Обработка...';
updateQueue(pct + '%', pct, false);
if (pct === 100) {
statusText.textContent = '{{ t.media_upload_processing }}';
percent.textContent = '•••';
bar.classList.add('is-processing');
updateQueue('{{ t.media_upload_processing }}', 100, true);
}
});
xhr.upload.addEventListener('load', function() {
statusText.textContent = '{{ t.media_upload_processing }}';
percent.textContent = '•••';
bar.classList.add('is-processing');
updateQueue('{{ t.media_upload_processing }}', 100, true);
});
xhr.addEventListener('load', function() {
if (xhr.status >= 200 && xhr.status < 400) {
bar.style.width = '100%';
bar.classList.remove('is-processing');
percent.textContent = '100%';
statusText.textContent = 'Готово!';
statusText.textContent = '{{ t.media_upload_done }}';
updateQueue('{{ t.media_upload_done }}', 100, false);
setTimeout(function() { window.location.href = xhr.responseURL || '/admin/media'; }, 300);
} else {
bar.classList.remove('is-processing');
statusText.textContent = 'Ошибка загрузки (' + xhr.status + ')';
updateQueue('Ошибка загрузки', 0, false);
submitBtn.disabled = false;
submitBtn.textContent = '{{ t.media_upload }}';
}
});
xhr.addEventListener('error', function() {
statusText.textContent = 'Ошибка соединения';
bar.classList.remove('is-processing');
statusText.textContent = '{{ t.media_upload_connection_error }}';
updateQueue('{{ t.media_upload_connection_error }}', 0, false);
submitBtn.disabled = false;
submitBtn.textContent = '{{ t.media_upload }}';
});
@@ -106,4 +160,17 @@
})();
</script>
</div>
<style>
.upload-queue { display:flex; flex-direction:column; gap:.4rem; margin-top:.65rem; }
.upload-queue:empty { display:none; }
.upload-queue-item { display:grid; grid-template-columns:34px minmax(0,1fr); gap:.55rem; align-items:center; padding:.45rem .55rem; background:#f7f6ff; border-radius:8px; }
.upload-queue-icon { width:34px; height:34px; display:flex; align-items:center; justify-content:center; border-radius:7px; background:#ebe8ff; font-size:1.05rem; }
.upload-queue-name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:.78rem; color:#494467; }
.upload-queue-state { font-size:.68rem; color:#8a84a5; }
.upload-queue-track { height:3px; overflow:hidden; border-radius:99px; background:#dedbea; margin-top:.25rem; }
.upload-queue-bar { height:100%; width:0; background:#7567e8; transition:width .2s; }
#uploadBar.is-processing, .upload-queue-bar.is-processing { width:35% !important; animation:upload-processing 1.15s ease-in-out infinite; }
@keyframes upload-processing { from { transform:translateX(-110%); } to { transform:translateX(300%); } }
</style>
{% endblock %}