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 }}';
|
||||
});
|
||||
|
||||
@@ -57,6 +57,9 @@
|
||||
.media-row img {
|
||||
width: 80px; height: 60px; object-fit: cover; border-radius: 6px;
|
||||
}
|
||||
.media-row .media-thumb-frame {
|
||||
width: 80px; height: 60px; border-radius: 6px;
|
||||
}
|
||||
.media-row .vid-thumb {
|
||||
position: relative; width: 80px; height: 60px; border-radius: 6px;
|
||||
overflow: hidden; background: #111;
|
||||
@@ -192,12 +195,16 @@
|
||||
{% for m in &pv.media %}
|
||||
{% if m.file_type == "photo" %}
|
||||
<a href="{{ m.url }}" data-lightbox="photo">
|
||||
<img src="{{ m.thumbnail_url }}" alt="" loading="lazy">
|
||||
<span class="media-thumb-frame 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="vid-thumb">
|
||||
<video src="{{ m.url }}#t=0.1" preload="metadata" muted playsinline></video>
|
||||
<div class="vid-thumb 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>
|
||||
|
||||
@@ -1,49 +1,164 @@
|
||||
<div class="lightbox-overlay" id="lightbox" onclick="closeLightbox(event)">
|
||||
<button class="lightbox-close" onclick="closeLightbox(event)">×</button>
|
||||
<img id="lightboxImg" src="" alt="">
|
||||
<video id="lightboxVideo" controls style="display:none;"></video>
|
||||
<button class="lightbox-close" type="button" onclick="closeLightbox(event)">×</button>
|
||||
<div class="lightbox-stage" id="lightboxStage">
|
||||
<span class="lightbox-loader" aria-hidden="true"></span>
|
||||
<img id="lightboxImg" src="" alt="">
|
||||
<video id="lightboxVideo" controls playsinline preload="auto" style="display:none;"></video>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.media-loading-frame {
|
||||
position: relative; display: block; overflow: hidden; background: #eceaf5;
|
||||
}
|
||||
.media-loading-frame::after, .lightbox-loader {
|
||||
content: ""; position: absolute; z-index: 3; top: 50%; left: 50%;
|
||||
width: 22px; height: 22px; margin: -11px 0 0 -11px;
|
||||
border: 3px solid rgba(124,108,255,.22); border-top-color: #7c6cff;
|
||||
border-radius: 50%; animation: media-spinner .75s linear infinite;
|
||||
}
|
||||
.media-loading-frame:not(.is-loading)::after { display: none; }
|
||||
.media-loading-frame img[data-media-load] { opacity: 0; transition: opacity .18s ease; }
|
||||
.media-loading-frame.is-loaded img[data-media-load] { opacity: 1; }
|
||||
.media-loading-frame.is-error::after {
|
||||
display: block; content: "!"; width: 24px; height: 24px; margin: -12px 0 0 -12px;
|
||||
border: 0; animation: none; color: #9b93bb; font-weight: 700; text-align: center;
|
||||
}
|
||||
.video-preview-sprite {
|
||||
display: block !important; width: 400% !important; max-width: none !important;
|
||||
height: 100% !important; object-fit: fill !important;
|
||||
transform: translateX(0); transition: transform .08s linear;
|
||||
}
|
||||
@keyframes media-spinner { to { transform: rotate(360deg); } }
|
||||
|
||||
.lightbox-overlay {
|
||||
display:none; position:fixed; inset:0; z-index:200;
|
||||
background:rgba(0,0,0,0.85); align-items:center; justify-content:center;
|
||||
background:rgba(9,8,18,.9); align-items:center; justify-content:center;
|
||||
padding: 1rem;
|
||||
}
|
||||
.lightbox-overlay.is-open { display:flex; }
|
||||
.lightbox-overlay img, .lightbox-overlay video {
|
||||
max-width:92vw; max-height:88vh; border-radius:8px; object-fit:contain;
|
||||
.lightbox-stage {
|
||||
position: relative; display: flex; align-items: center; justify-content: center;
|
||||
min-width: 96px; min-height: 96px; max-width: 94vw; max-height: 90vh;
|
||||
}
|
||||
.lightbox-stage img, .lightbox-stage video {
|
||||
max-width:92vw; max-height:88vh; border-radius:10px; object-fit:contain;
|
||||
background:#090909; box-shadow:0 18px 60px rgba(0,0,0,.42);
|
||||
}
|
||||
.lightbox-stage.is-loading img, .lightbox-stage.is-loading video { opacity: .18; }
|
||||
.lightbox-stage:not(.is-loading) .lightbox-loader { display: none; }
|
||||
.lightbox-stage.is-error .lightbox-loader {
|
||||
display: block; animation: none; border: 0; color: white;
|
||||
}
|
||||
.lightbox-stage.is-error .lightbox-loader::after { content: "!"; font-size: 2rem; }
|
||||
.lightbox-close {
|
||||
position:absolute; top:0.75rem; right:1rem; background:none; border:none;
|
||||
position:absolute; top:0.75rem; right:1rem; background:rgba(0,0,0,.25); border:none;
|
||||
color:#fff; font-size:2.2rem; cursor:pointer; line-height:1; z-index:201;
|
||||
width:44px; height:44px; border-radius:50%;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function() {
|
||||
function finishThumbnail(image, loaded) {
|
||||
var frame = image.closest('.media-loading-frame');
|
||||
if (!frame) return;
|
||||
frame.classList.remove('is-loading');
|
||||
frame.classList.add(loaded ? 'is-loaded' : 'is-error');
|
||||
}
|
||||
|
||||
document.querySelectorAll('img[data-media-load]').forEach(function(image) {
|
||||
image.addEventListener('load', function() { finishThumbnail(image, true); });
|
||||
image.addEventListener('error', function() { finishThumbnail(image, false); });
|
||||
if (image.complete) finishThumbnail(image, image.naturalWidth > 0);
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-video-preview]').forEach(function(preview) {
|
||||
var sprite = preview.querySelector('[data-preview-frames]');
|
||||
if (!sprite) return;
|
||||
var timer;
|
||||
var frame = 0;
|
||||
var frames = Number(sprite.dataset.previewFrames) || 4;
|
||||
function showFrame() {
|
||||
sprite.style.transform = 'translateX(-' + (frame * 100 / frames) + '%)';
|
||||
}
|
||||
function start() {
|
||||
if (timer || !preview.classList.contains('is-loaded')) return;
|
||||
frame = 1;
|
||||
showFrame();
|
||||
timer = window.setInterval(function() {
|
||||
frame = (frame + 1) % frames;
|
||||
showFrame();
|
||||
}, 650);
|
||||
}
|
||||
function stop() {
|
||||
window.clearInterval(timer);
|
||||
timer = null;
|
||||
frame = 0;
|
||||
showFrame();
|
||||
}
|
||||
preview.addEventListener('pointerenter', start);
|
||||
preview.addEventListener('pointerleave', stop);
|
||||
preview.closest('a').addEventListener('focus', start);
|
||||
preview.closest('a').addEventListener('blur', stop);
|
||||
});
|
||||
})();
|
||||
|
||||
function setLightboxState(state) {
|
||||
var stage = document.getElementById('lightboxStage');
|
||||
stage.classList.remove('is-loading', 'is-error');
|
||||
if (state) stage.classList.add(state);
|
||||
}
|
||||
function openLightbox(url, isVideo) {
|
||||
var lb = document.getElementById('lightbox');
|
||||
var img = document.getElementById('lightboxImg');
|
||||
var vid = document.getElementById('lightboxVideo');
|
||||
setLightboxState('is-loading');
|
||||
lb.classList.add('is-open');
|
||||
if (isVideo) {
|
||||
img.style.display = 'none';
|
||||
img.removeAttribute('src');
|
||||
vid.style.display = '';
|
||||
vid.src = url;
|
||||
vid.load();
|
||||
var playback = vid.play();
|
||||
if (playback) playback.catch(function() {});
|
||||
} else {
|
||||
vid.style.display = 'none';
|
||||
vid.pause && vid.pause(); vid.src = '';
|
||||
vid.pause();
|
||||
vid.removeAttribute('src');
|
||||
vid.load();
|
||||
img.style.display = '';
|
||||
img.src = url;
|
||||
}
|
||||
lb.classList.add('is-open');
|
||||
}
|
||||
function closeLightbox(e) {
|
||||
if (e && e.target !== document.getElementById('lightbox') && e.target.className !== 'lightbox-close') return;
|
||||
function closeLightbox(event) {
|
||||
var lb = document.getElementById('lightbox');
|
||||
if (event && event.target !== lb && !event.target.closest('.lightbox-close')) return;
|
||||
lb.classList.remove('is-open');
|
||||
var vid = document.getElementById('lightboxVideo');
|
||||
vid.pause && vid.pause(); vid.src = '';
|
||||
vid.pause();
|
||||
vid.removeAttribute('src');
|
||||
vid.load();
|
||||
var img = document.getElementById('lightboxImg');
|
||||
img.removeAttribute('src');
|
||||
setLightboxState(null);
|
||||
}
|
||||
document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closeLightbox(null); });
|
||||
document.addEventListener('click', function(e) {
|
||||
var a = e.target.closest('[data-lightbox]');
|
||||
if (a) { e.preventDefault(); openLightbox(a.href, a.dataset.lightbox === 'video'); }
|
||||
document.getElementById('lightboxImg').addEventListener('load', function() { setLightboxState(null); });
|
||||
document.getElementById('lightboxImg').addEventListener('error', function() { setLightboxState('is-error'); });
|
||||
var lightboxVideo = document.getElementById('lightboxVideo');
|
||||
['loadeddata', 'canplay', 'playing'].forEach(function(name) {
|
||||
lightboxVideo.addEventListener(name, function() { setLightboxState(null); });
|
||||
});
|
||||
['waiting', 'stalled', 'seeking'].forEach(function(name) {
|
||||
lightboxVideo.addEventListener(name, function() { setLightboxState('is-loading'); });
|
||||
});
|
||||
lightboxVideo.addEventListener('error', function() { setLightboxState('is-error'); });
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape') closeLightbox(null);
|
||||
});
|
||||
document.addEventListener('click', function(event) {
|
||||
var link = event.target.closest('[data-lightbox]');
|
||||
if (!link) return;
|
||||
event.preventDefault();
|
||||
openLightbox(link.href, link.dataset.lightbox === 'video');
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user