Added Cloudflare R2 media storage support
Build and Publish / Build and Publish Docker Image (push) Successful in 1m44s
Build and Publish / Build and Publish Docker Image (push) Successful in 1m44s
This commit is contained in:
+12
-10
@@ -28,12 +28,16 @@
|
||||
<div class="media-card">
|
||||
{% if item.media.file_type == "photo" %}
|
||||
<a href="{{ item.media.url }}" data-lightbox="photo">
|
||||
<img src="{{ item.media.thumbnail_url }}" alt="" loading="lazy">
|
||||
<span class="photo-thumb media-loading-frame is-loading">
|
||||
<img src="{{ item.media.thumbnail_url }}" alt="" loading="lazy" data-media-load>
|
||||
</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ item.media.url }}" data-lightbox="video">
|
||||
<div class="video-thumb">
|
||||
<video src="{{ item.media.url }}#t=0.1" preload="metadata" muted playsinline></video>
|
||||
<div class="video-thumb media-loading-frame{% if !item.media.thumbnail_url.is_empty() %} is-loading{% endif %}" data-video-preview>
|
||||
{% if !item.media.thumbnail_url.is_empty() %}
|
||||
<img class="video-preview-sprite" src="{{ item.media.thumbnail_url }}" alt="" loading="lazy" data-media-load data-preview-frames="4">
|
||||
{% endif %}
|
||||
<span class="video-play">▶</span>
|
||||
</div>
|
||||
</a>
|
||||
@@ -88,18 +92,16 @@
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.media-card .photo-thumb {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
}
|
||||
.media-card .video-thumb {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
background: #111;
|
||||
}
|
||||
.media-card .video-thumb video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
.media-card .video-play {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
@@ -132,7 +134,7 @@
|
||||
.media-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
}
|
||||
.media-card img, .media-card .video-thumb {
|
||||
.media-card img, .media-card .photo-thumb, .media-card .video-thumb {
|
||||
height: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -114,12 +114,16 @@
|
||||
<div class="visit-media-item">
|
||||
{% if m.file_type == "photo" %}
|
||||
<a href="{{ m.url }}" data-lightbox="photo">
|
||||
<img src="{{ m.thumbnail_url }}" alt="" loading="lazy">
|
||||
<span class="photo-thumb media-loading-frame is-loading">
|
||||
<img src="{{ m.thumbnail_url }}" alt="" loading="lazy" data-media-load>
|
||||
</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ m.url }}" data-lightbox="video">
|
||||
<div class="video-thumb-sm">
|
||||
<video src="{{ m.url }}#t=0.1" preload="metadata" muted playsinline></video>
|
||||
<div class="video-thumb-sm media-loading-frame{% if !m.thumbnail_url.is_empty() %} is-loading{% endif %}" data-video-preview>
|
||||
{% if !m.thumbnail_url.is_empty() %}
|
||||
<img class="video-preview-sprite" src="{{ m.thumbnail_url }}" alt="" loading="lazy" data-media-load data-preview-frames="4">
|
||||
{% endif %}
|
||||
<span class="video-play">▶</span>
|
||||
</div>
|
||||
</a>
|
||||
@@ -163,6 +167,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">
|
||||
<label class="label">{{ t.media_caption }}</label>
|
||||
@@ -238,18 +243,16 @@
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.visit-media-item .photo-thumb {
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
}
|
||||
.visit-media-item .video-thumb-sm {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
background: #111;
|
||||
}
|
||||
.visit-media-item .video-thumb-sm video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
.visit-media-item .video-play {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
@@ -297,6 +300,16 @@
|
||||
max-width: 420px;
|
||||
box-shadow: 0 4px 24px rgba(0,0,0,0.15);
|
||||
}
|
||||
.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>
|
||||
|
||||
<script>
|
||||
@@ -316,6 +329,7 @@ document.querySelectorAll('.status-btn').forEach(function(btn) {
|
||||
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');
|
||||
@@ -336,9 +350,41 @@ document.querySelectorAll('.status-btn').forEach(function(btn) {
|
||||
// Show selected file count
|
||||
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 + '%';
|
||||
});
|
||||
}
|
||||
|
||||
// Submit via XHR for progress tracking
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
@@ -350,7 +396,9 @@ document.querySelectorAll('.status-btn').forEach(function(btn) {
|
||||
// Show progress bar, disable submit
|
||||
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%';
|
||||
|
||||
@@ -359,25 +407,43 @@ document.querySelectorAll('.status-btn').forEach(function(btn) {
|
||||
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);
|
||||
// Reload page to show uploaded media
|
||||
setTimeout(function() { window.location.reload(); }, 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 }}';
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user