Files
sigma-file-manager/src-tauri/assets/lan_share/lan_share_stream.html
T

906 lines
28 KiB
HTML

<!-- SPDX-License-Identifier: GPL-3.0-or-later
License: GNU GPLv3 or later. See the license file in the project root for more information.
Copyright © 2021 - present Aleksey Hoffman. All rights reserved.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sigma File Manager — Media Hub</title>
<link rel="icon" href="/app-icon.png" type="image/png">
<style>
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg: #16181d;
--surface: #1e2026;
--surface-hover: #262930;
--surface-active: #2c2f38;
--border: #2e3138;
--text: #e0e0e4;
--text-muted: #8b8d94;
--accent: #7c8aff;
--accent-hover: #9aa4ff;
--radius: 8px;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
line-height: 1.5;
}
.app {
max-width: 1200px;
margin: 0 auto;
padding: 24px 16px;
}
.header {
display: flex;
align-items: center;
gap: 12px;
padding-bottom: 20px;
border-bottom: 1px solid var(--border);
margin-bottom: 20px;
}
.header-brand {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.header-icon {
width: 40px;
height: 40px;
flex-shrink: 0;
border-radius: 8px;
}
.header-logo {
font-size: 1.125rem;
font-weight: 700;
letter-spacing: -0.02em;
}
.header-badge {
padding: 3px 10px;
border: 1px solid var(--border);
border-radius: 100px;
font-size: 0.75rem;
color: var(--text-muted);
}
body.viewer-open {
overflow: hidden;
}
.viewer-overlay {
position: fixed;
inset: 0;
z-index: 1000;
display: none;
flex-direction: column;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
}
.viewer-overlay--open {
display: flex;
}
.viewer-overlay__toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
gap: 4px;
flex-shrink: 0;
}
.viewer-overlay__nav {
display: flex;
align-items: center;
gap: 2px;
}
.viewer-overlay__btn {
all: unset;
display: inline-flex;
width: 36px;
height: 36px;
align-items: center;
justify-content: center;
border-radius: 6px;
color: rgba(255, 255, 255, 0.7);
cursor: pointer;
font-size: 1.125rem;
transition: background 100ms, color 100ms;
user-select: none;
}
.viewer-overlay__btn:hover {
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
.viewer-overlay__counter {
color: rgba(255, 255, 255, 0.5);
font-size: 0.75rem;
padding: 0 6px;
user-select: none;
}
.viewer-overlay__content {
position: relative;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
min-height: 0;
overflow: hidden;
padding: 16px;
touch-action: none;
}
.viewer-overlay__content img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
transform-origin: center center;
will-change: transform;
}
.viewer-overlay__content video {
max-width: 100%;
max-height: 100%;
}
.viewer-overlay__content audio {
width: 80%;
max-width: 500px;
}
.viewer-overlay__audio-wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 32px 16px;
width: 100%;
}
.viewer-overlay__audio-icon {
font-size: 2.5rem;
opacity: 0.6;
}
.viewer-overlay__audio-name {
font-size: 0.875rem;
font-weight: 600;
text-align: center;
word-break: break-word;
}
.viewer-overlay__filename {
padding: 8px 16px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.5);
font-size: 0.8125rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
flex-shrink: 0;
}
.breadcrumb {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 16px;
font-size: 0.875rem;
}
.breadcrumb-segment {
cursor: pointer;
color: var(--accent);
padding: 2px 6px;
border-radius: 4px;
background: none;
border: none;
font: inherit;
}
.breadcrumb-segment:hover {
color: var(--accent-hover);
background: var(--surface);
}
.breadcrumb-separator {
color: var(--text-muted);
user-select: none;
}
.breadcrumb-current {
color: var(--text);
cursor: default;
}
.breadcrumb-current:hover {
background: none;
color: var(--text);
}
.media-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 12px;
}
.media-card {
display: flex;
flex-direction: column;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--surface);
overflow: hidden;
cursor: pointer;
transition: border-color 120ms, background 120ms;
}
.media-card:hover {
border-color: var(--accent);
background: var(--surface-hover);
}
.media-card--active {
border-color: var(--accent);
background: var(--surface-active);
}
.media-card__preview {
position: relative;
width: 100%;
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.15);
overflow: hidden;
}
.media-card__preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.media-card__preview-icon {
font-size: 2rem;
opacity: 0.5;
}
.media-card__preview-badge {
position: absolute;
top: 6px;
right: 6px;
padding: 1px 6px;
border-radius: 4px;
background: rgba(0, 0, 0, 0.6);
font-size: 0.625rem;
font-weight: 600;
text-transform: uppercase;
color: var(--text);
letter-spacing: 0.04em;
}
.media-card__info {
padding: 8px 10px;
}
.media-card__name {
font-size: 0.75rem;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.3;
}
.media-card__meta {
font-size: 0.6875rem;
color: var(--text-muted);
margin-top: 2px;
}
.media-card--dir .media-card__preview {
background: rgba(124, 138, 255, 0.06);
}
.media-card--dir .media-card__preview-icon {
color: var(--accent);
opacity: 0.7;
}
.empty-state {
text-align: center;
padding: 48px 0;
color: var(--text-muted);
font-size: 0.9375rem;
}
.loading {
text-align: center;
padding: 48px 0;
color: var(--text-muted);
}
@media (max-width: 600px) {
.app {
padding: 16px 12px;
}
.media-grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 8px;
}
}
</style>
</head>
<body>
<div class="app" id="app">
<div class="header">
<div class="header-brand">
<img class="header-icon" src="/app-icon.png" width="40" height="40" alt="" decoding="async">
<div class="header-logo">Sigma File Manager</div>
</div>
<div class="header-badge">Media Hub</div>
</div>
<div class="breadcrumb" id="breadcrumb"></div>
<div id="content">
<div class="loading">Loading...</div>
</div>
</div>
<div class="viewer-overlay" id="viewerOverlay">
<div class="viewer-overlay__toolbar">
<div class="viewer-overlay__nav">
<button type="button" class="viewer-overlay__btn" id="prevBtn" title="Previous">&#9664;</button>
<span class="viewer-overlay__counter" id="viewerCounter"></span>
<button type="button" class="viewer-overlay__btn" id="nextBtn" title="Next">&#9654;</button>
</div>
<button type="button" class="viewer-overlay__btn" id="closeBtn" title="Close">&#10005;</button>
</div>
<div class="viewer-overlay__content" id="viewerContent"></div>
<div class="viewer-overlay__filename" id="viewerFilename"></div>
</div>
<script>
const IMAGE_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "ico", "avif"];
const VIDEO_EXTENSIONS = ["mp4", "webm", "ogv", "mov"];
const AUDIO_EXTENSIONS = ["mp3", "wav", "ogg", "flac", "aac", "m4a", "opus"];
let currentPath = "";
let allEntries = [];
let mediaFileList = [];
let currentMediaIndex = -1;
let shareRootLabel = "Shared";
const contentEl = document.getElementById("content");
const breadcrumbEl = document.getElementById("breadcrumb");
const viewerOverlay = document.getElementById("viewerOverlay");
const viewerContent = document.getElementById("viewerContent");
const viewerFilename = document.getElementById("viewerFilename");
const viewerCounter = document.getElementById("viewerCounter");
const prevBtn = document.getElementById("prevBtn");
const nextBtn = document.getElementById("nextBtn");
const closeBtn = document.getElementById("closeBtn");
const apiOrigin = window.location.origin;
function getExtension (name) {
const dotIndex = name.lastIndexOf(".");
if (dotIndex < 0) return "";
return name.slice(dotIndex + 1).toLowerCase();
}
function getMediaType (name) {
const extension = getExtension(name);
if (IMAGE_EXTENSIONS.includes(extension)) return "image";
if (VIDEO_EXTENSIONS.includes(extension)) return "video";
if (AUDIO_EXTENSIONS.includes(extension)) return "audio";
return null;
}
function formatBytes (bytes) {
if (bytes === 0) return "0 B";
const units = ["B", "KB", "MB", "GB", "TB"];
const unitIndex = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, unitIndex)).toFixed(unitIndex > 0 ? 1 : 0) + " " + units[unitIndex];
}
function escapeHtml (text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
function buildFileUrl (entryPath) {
return apiOrigin + "/files/" + encodeURIComponent(entryPath);
}
function entryListPathSegment (entry) {
if (entry.fileId != null && entry.fileId !== "") {
return String(entry.fileId);
}
return entry.name;
}
function buildEntryRelativePath (entry) {
const segment = entryListPathSegment(entry);
return currentPath ? currentPath + "/" + segment : segment;
}
async function fetchListing (path) {
currentPath = path || "";
currentMediaIndex = -1;
closeViewer();
const queryPath = path ? "?path=" + encodeURIComponent(path) : "";
try {
const response = await fetch(apiOrigin + "/api/list" + queryPath);
if (!response.ok) throw new Error("HTTP " + response.status);
const data = await response.json();
allEntries = data.entries;
if (typeof data.shareName === "string" && data.shareName.length > 0) {
shareRootLabel = data.shareName;
}
mediaFileList = allEntries
.filter(entry => !entry.is_dir && getMediaType(entry.name))
.map(entry => {
const filePath = buildEntryRelativePath(entry);
return { path: filePath, name: entry.name, type: getMediaType(entry.name) };
});
renderBreadcrumb(data.currentPath, data.canGoUp);
renderGrid(allEntries);
} catch (error) {
contentEl.innerHTML = '<div class="empty-state">Failed to load directory listing</div>';
}
}
function renderBreadcrumb (pathStr, canGoUp) {
breadcrumbEl.innerHTML = "";
const rootBtn = document.createElement("button");
rootBtn.className = "breadcrumb-segment" + (!pathStr ? " breadcrumb-current" : "");
rootBtn.textContent = shareRootLabel;
rootBtn.onclick = () => fetchListing("");
breadcrumbEl.appendChild(rootBtn);
if (pathStr) {
const segments = pathStr.split("/").filter(Boolean);
segments.forEach((segment, index) => {
const separator = document.createElement("span");
separator.className = "breadcrumb-separator";
separator.textContent = "/";
breadcrumbEl.appendChild(separator);
const isLast = index === segments.length - 1;
const btn = document.createElement("button");
btn.className = "breadcrumb-segment" + (isLast ? " breadcrumb-current" : "");
btn.textContent = segment;
if (!isLast) {
const targetPath = segments.slice(0, index + 1).join("/");
btn.onclick = () => fetchListing(targetPath);
}
breadcrumbEl.appendChild(btn);
});
}
}
function renderGrid (entries) {
const dirs = entries.filter(entry => entry.is_dir);
const mediaFiles = entries.filter(entry => !entry.is_dir && getMediaType(entry.name));
if (!dirs.length && !mediaFiles.length) {
contentEl.innerHTML = '<div class="empty-state">No media files in this directory</div>';
return;
}
let html = '<div class="media-grid">';
for (const dir of dirs) {
const dirPath = currentPath ? currentPath + "/" + dir.name : dir.name;
html += '<div class="media-card media-card--dir" data-dir="' + escapeHtml(dirPath) + '">'
+ '<div class="media-card__preview"><div class="media-card__preview-icon">&#128193;</div></div>'
+ '<div class="media-card__info">'
+ '<div class="media-card__name">' + escapeHtml(dir.name) + '</div>'
+ '<div class="media-card__meta">Folder</div>'
+ '</div></div>';
}
for (const file of mediaFiles) {
const filePath = buildEntryRelativePath(file);
const mediaType = getMediaType(file.name);
const fileUrl = buildFileUrl(filePath);
const extension = getExtension(file.name).toUpperCase();
let previewHtml;
if (mediaType === "image") {
previewHtml = '<img src="' + fileUrl + '" alt="" loading="lazy" decoding="async">';
} else if (mediaType === "video") {
previewHtml = '<div class="media-card__preview-icon">&#127916;</div>';
} else {
previewHtml = '<div class="media-card__preview-icon">&#127925;</div>';
}
html += '<div class="media-card" data-file="' + escapeHtml(filePath) + '" data-type="' + mediaType + '" data-name="' + escapeHtml(file.name) + '">'
+ '<div class="media-card__preview">' + previewHtml
+ '<div class="media-card__preview-badge">' + extension + '</div>'
+ '</div>'
+ '<div class="media-card__info">'
+ '<div class="media-card__name">' + escapeHtml(file.name) + '</div>'
+ '<div class="media-card__meta">' + formatBytes(file.size) + '</div>'
+ '</div></div>';
}
html += '</div>';
contentEl.innerHTML = html;
contentEl.querySelectorAll(".media-card[data-dir]").forEach(card => {
card.addEventListener("click", () => {
fetchListing(card.dataset.dir);
});
});
contentEl.querySelectorAll(".media-card[data-file]").forEach(card => {
card.addEventListener("click", () => {
const filePath = card.dataset.file;
const mediaIndex = mediaFileList.findIndex(media => media.path === filePath);
if (mediaIndex >= 0) {
navigateToMedia(mediaIndex);
}
});
});
}
let imageScale = 1;
let imageTranslateX = 0;
let imageTranslateY = 0;
let lastPinchDist = 0;
let lastPinchCenterX = 0;
let lastPinchCenterY = 0;
let isPinching = false;
let isPanning = false;
let panLastX = 0;
let panLastY = 0;
function applyImageTransform () {
const img = viewerContent.querySelector("img");
if (img) {
img.style.transform = "translate(" + imageTranslateX + "px, " + imageTranslateY + "px) scale(" + imageScale + ")";
}
}
function clampTranslation () {
const img = viewerContent.querySelector("img");
if (!img) return;
const rect = viewerContent.getBoundingClientRect();
const scaledW = img.naturalWidth > 0
? Math.min(img.clientWidth, rect.width) * imageScale
: rect.width * imageScale;
const scaledH = img.naturalHeight > 0
? Math.min(img.clientHeight, rect.height) * imageScale
: rect.height * imageScale;
const maxPanX = Math.max(0, (scaledW - rect.width) / 2);
const maxPanY = Math.max(0, (scaledH - rect.height) / 2);
imageTranslateX = Math.max(-maxPanX, Math.min(maxPanX, imageTranslateX));
imageTranslateY = Math.max(-maxPanY, Math.min(maxPanY, imageTranslateY));
}
function resetImageZoom () {
imageScale = 1;
imageTranslateX = 0;
imageTranslateY = 0;
const img = viewerContent.querySelector("img");
if (img) img.style.transform = "";
}
let isAnimating = false;
function loadMediaContent (index) {
resetImageZoom();
currentMediaIndex = index;
const media = mediaFileList[index];
const fileUrl = buildFileUrl(media.path);
if (media.type === "image") {
viewerContent.innerHTML = '<img src="' + fileUrl + '" alt="' + escapeHtml(media.name) + '">';
} else if (media.type === "video") {
viewerContent.innerHTML = '<video src="' + fileUrl + '" controls playsinline></video>';
} else if (media.type === "audio") {
viewerContent.innerHTML = '<div class="viewer-overlay__audio-wrap">'
+ '<div class="viewer-overlay__audio-icon">&#127925;</div>'
+ '<div class="viewer-overlay__audio-name">' + escapeHtml(media.name) + '</div>'
+ '<audio src="' + fileUrl + '" controls></audio>'
+ '</div>';
}
viewerFilename.textContent = media.name;
viewerCounter.textContent = (index + 1) + " / " + mediaFileList.length;
highlightActiveCard(media.path);
}
function navigateToMedia (index) {
if (index < 0 || index >= mediaFileList.length) return;
viewerOverlay.classList.add("viewer-overlay--open");
document.body.classList.add("viewer-open");
loadMediaContent(index);
}
function navigateWithAnimation (targetIndex, direction) {
if (isAnimating) return;
if (targetIndex < 0 || targetIndex >= mediaFileList.length) return;
isAnimating = true;
var containerWidth = viewerContent.offsetWidth;
var slideOutX = direction === "next" ? -containerWidth : containerWidth;
var slideInX = direction === "next" ? containerWidth : -containerWidth;
viewerContent.style.transition = "transform 200ms ease-out";
viewerContent.style.transform = "translateX(" + slideOutX + "px)";
function afterSlideOut () {
viewerContent.removeEventListener("transitionend", afterSlideOut);
loadMediaContent(targetIndex);
viewerContent.style.transition = "none";
viewerContent.style.transform = "translateX(" + slideInX + "px)";
void viewerContent.offsetHeight;
viewerContent.style.transition = "transform 200ms ease-out";
viewerContent.style.transform = "translateX(0)";
function afterSlideIn () {
viewerContent.removeEventListener("transitionend", afterSlideIn);
viewerContent.style.transition = "";
viewerContent.style.transform = "";
isAnimating = false;
}
viewerContent.addEventListener("transitionend", afterSlideIn);
}
viewerContent.addEventListener("transitionend", afterSlideOut);
}
function commitSwipeAnimation (targetIndex, direction, currentDragX) {
if (isAnimating) return;
isAnimating = true;
var containerWidth = viewerContent.offsetWidth;
var slideOutX = direction === "next" ? -containerWidth : containerWidth;
var slideInX = direction === "next" ? containerWidth : -containerWidth;
var remaining = containerWidth - Math.abs(currentDragX);
var outDuration = Math.max(80, Math.round(200 * (remaining / containerWidth)));
viewerContent.style.transition = "transform " + outDuration + "ms ease-out";
viewerContent.style.transform = "translateX(" + slideOutX + "px)";
function afterSlideOut () {
viewerContent.removeEventListener("transitionend", afterSlideOut);
loadMediaContent(targetIndex);
viewerContent.style.transition = "none";
viewerContent.style.transform = "translateX(" + slideInX + "px)";
void viewerContent.offsetHeight;
viewerContent.style.transition = "transform 200ms ease-out";
viewerContent.style.transform = "translateX(0)";
function afterSlideIn () {
viewerContent.removeEventListener("transitionend", afterSlideIn);
viewerContent.style.transition = "";
viewerContent.style.transform = "";
isAnimating = false;
}
viewerContent.addEventListener("transitionend", afterSlideIn);
}
viewerContent.addEventListener("transitionend", afterSlideOut);
}
function cancelSwipeDrag () {
viewerContent.style.transition = "transform 180ms ease-out";
viewerContent.style.transform = "translateX(0)";
function afterSnap () {
viewerContent.removeEventListener("transitionend", afterSnap);
viewerContent.style.transition = "";
viewerContent.style.transform = "";
}
viewerContent.addEventListener("transitionend", afterSnap);
}
function highlightActiveCard (filePath) {
contentEl.querySelectorAll(".media-card--active").forEach(card => card.classList.remove("media-card--active"));
const activeCard = contentEl.querySelector('.media-card[data-file="' + CSS.escape(filePath) + '"]');
if (activeCard) {
activeCard.classList.add("media-card--active");
activeCard.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
}
function navigatePrev () {
if (currentMediaIndex > 0) {
navigateWithAnimation(currentMediaIndex - 1, "prev");
}
}
function navigateNext () {
if (currentMediaIndex < mediaFileList.length - 1) {
navigateWithAnimation(currentMediaIndex + 1, "next");
}
}
function closeViewer () {
resetImageZoom();
viewerOverlay.classList.remove("viewer-overlay--open");
document.body.classList.remove("viewer-open");
viewerContent.innerHTML = "";
currentMediaIndex = -1;
contentEl.querySelectorAll(".media-card--active").forEach(card => card.classList.remove("media-card--active"));
}
prevBtn.addEventListener("click", navigatePrev);
nextBtn.addEventListener("click", navigateNext);
closeBtn.addEventListener("click", closeViewer);
document.addEventListener("keydown", (event) => {
if (currentMediaIndex < 0) return;
if (event.key === "ArrowLeft") { event.preventDefault(); navigatePrev(); }
else if (event.key === "ArrowRight") { event.preventDefault(); navigateNext(); }
else if (event.key === "Escape") { closeViewer(); }
});
let swipeStartX = 0;
let swipeStartY = 0;
let swipeDragX = 0;
let swipeDragging = false;
let swipeDirectionLocked = false;
let swipeIsHorizontal = false;
const SWIPE_THRESHOLD = 50;
const DIRECTION_LOCK_THRESHOLD = 8;
viewerContent.addEventListener("touchstart", (event) => {
if (isAnimating) return;
if (event.touches.length === 2) {
isPinching = true;
isPanning = false;
swipeDragging = false;
lastPinchDist = Math.hypot(
event.touches[0].clientX - event.touches[1].clientX,
event.touches[0].clientY - event.touches[1].clientY
);
lastPinchCenterX = (event.touches[0].clientX + event.touches[1].clientX) / 2;
lastPinchCenterY = (event.touches[0].clientY + event.touches[1].clientY) / 2;
} else if (event.touches.length === 1) {
swipeStartX = event.touches[0].clientX;
swipeStartY = event.touches[0].clientY;
swipeDragX = 0;
swipeDirectionLocked = false;
swipeIsHorizontal = false;
if (imageScale > 1.05) {
isPanning = true;
panLastX = event.touches[0].clientX;
panLastY = event.touches[0].clientY;
}
}
}, { passive: true });
viewerContent.addEventListener("touchmove", (event) => {
if (isAnimating) return;
if (event.touches.length === 2) {
isPinching = true;
const dist = Math.hypot(
event.touches[0].clientX - event.touches[1].clientX,
event.touches[0].clientY - event.touches[1].clientY
);
const centerX = (event.touches[0].clientX + event.touches[1].clientX) / 2;
const centerY = (event.touches[0].clientY + event.touches[1].clientY) / 2;
const prevScale = imageScale;
imageScale = Math.max(1, Math.min(5, imageScale * (dist / lastPinchDist)));
const rect = viewerContent.getBoundingClientRect();
const originX = centerX - rect.left - rect.width / 2;
const originY = centerY - rect.top - rect.height / 2;
const scaleFactor = imageScale / prevScale;
imageTranslateX = originX - scaleFactor * (originX - imageTranslateX);
imageTranslateY = originY - scaleFactor * (originY - imageTranslateY);
imageTranslateX += centerX - lastPinchCenterX;
imageTranslateY += centerY - lastPinchCenterY;
lastPinchDist = dist;
lastPinchCenterX = centerX;
lastPinchCenterY = centerY;
clampTranslation();
applyImageTransform();
event.preventDefault();
} else if (event.touches.length === 1 && isPanning && !isPinching) {
const touchX = event.touches[0].clientX;
const touchY = event.touches[0].clientY;
imageTranslateX += touchX - panLastX;
imageTranslateY += touchY - panLastY;
panLastX = touchX;
panLastY = touchY;
clampTranslation();
applyImageTransform();
event.preventDefault();
} else if (event.touches.length === 1 && !isPanning && !isPinching && imageScale <= 1.05) {
const deltaX = event.touches[0].clientX - swipeStartX;
const deltaY = event.touches[0].clientY - swipeStartY;
if (!swipeDirectionLocked) {
if (Math.abs(deltaX) > DIRECTION_LOCK_THRESHOLD || Math.abs(deltaY) > DIRECTION_LOCK_THRESHOLD) {
swipeDirectionLocked = true;
swipeIsHorizontal = Math.abs(deltaX) >= Math.abs(deltaY);
}
}
if (swipeDirectionLocked && swipeIsHorizontal) {
swipeDragging = true;
swipeDragX = deltaX;
viewerContent.style.transition = "none";
viewerContent.style.transform = "translateX(" + deltaX + "px)";
event.preventDefault();
}
}
}, { passive: false });
viewerContent.addEventListener("touchend", (event) => {
if (event.touches.length < 2 && isPinching) {
isPinching = false;
if (imageScale < 1.05) {
resetImageZoom();
}
return;
}
if (event.touches.length === 0) {
if (isPanning) {
isPanning = false;
return;
}
if (swipeDragging) {
swipeDragging = false;
var dragX = swipeDragX;
swipeDragX = 0;
if (Math.abs(dragX) >= SWIPE_THRESHOLD) {
if (dragX < 0 && currentMediaIndex < mediaFileList.length - 1) {
commitSwipeAnimation(currentMediaIndex + 1, "next", dragX);
} else if (dragX > 0 && currentMediaIndex > 0) {
commitSwipeAnimation(currentMediaIndex - 1, "prev", dragX);
} else {
cancelSwipeDrag();
}
} else {
cancelSwipeDrag();
}
return;
}
}
}, { passive: true });
fetchListing("");
</script>
</body>
</html>