diff --git a/frontend/css/style.css b/frontend/css/style.css index bc55409..ed19134 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -129,6 +129,56 @@ html, body { background: white; color: rgba(44, 44, 44, 0.9); } + +/* Viewer Info Overlay */ +#viewer-info-overlay { + position: fixed; + bottom: 15px; + right: 15px; + background: rgba(30, 30, 30, 0.5); /* Nền xám tối transparent 0.5 - Tăng độ rõ */ + border: 1px solid rgba(255, 255, 255, 0.5); /* Viền trắng mờ - Tăng độ rõ */ + border-radius: 10px; + padding: 15px 20px; + max-width: 300px; + color: #fff; + font-size: 14px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4); + z-index: 3002; /* Trên viewer, dưới các modal */ + pointer-events: none; /* Không chặn tương tác chuột với viewer */ + opacity: 0; /* Khởi tạo ẩn */ + transform: translateY(20px); /* Hiệu ứng trượt lên */ + transition: opacity 0.3s ease-out, transform 0.3s ease-out; +} + +#viewer-info-overlay.show { + opacity: 1; + transform: translateY(0); +} + +#viewer-info-overlay .info-content h4 { + font-size: 18px; + margin-bottom: 8px; + color: #00d4ff; /* Màu xanh nổi bật cho tiêu đề */ + text-shadow: 0 1px 3px rgba(0,0,0,0.5); +} + +#viewer-info-overlay .info-content p { + font-size: 13px; + color: #ccc; + margin-bottom: 10px; + line-height: 1.4; +} + +#viewer-info-overlay .info-meta { + display: flex; + flex-direction: column; + gap: 5px; + font-size: 12px; + color: #aaa; + border-top: 1px solid rgba(255, 255, 255, 0.1); + padding-top: 10px; + margin-top: 10px; +} /* Modal Overlay */ .modal-overlay { display: none; diff --git a/frontend/js/main_map.js b/frontend/js/main_map.js index 981f84f..c8ffb06 100644 --- a/frontend/js/main_map.js +++ b/frontend/js/main_map.js @@ -369,6 +369,14 @@ function formatSystemDate(dateString) { } } +/** + * Kiểm tra xem người dùng có đang dùng thiết bị di động hay không + */ +function isMobileDevice() { + return (window.innerWidth <= 768) || + (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)); +} + /** * Initializes the full-screen Leaflet Map */ @@ -388,7 +396,11 @@ function initMap() { if (isNaN(startZoom)) startZoom = 13; // Khởi tạo bản đồ với zoomControl và tắt attribution mặc định của Leaflet - map = L.map('map', { zoomControl: true, attributionControl: false }).setView([startLat, startLng], startZoom); + map = L.map('map', { + zoomControl: !isMobileDevice(), // Ẩn nút +/- trên mobile để lấy thêm không gian hiển thị + attributionControl: false, + tap: true // Hỗ trợ click trên thiết bị cảm ứng tốt hơn + }).setView([startLat, startLng], startZoom); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, @@ -1056,7 +1068,7 @@ async function loadScenes(urlToken = null) { let thumbUrl = `${API_BASE_URL}/assets/view/${assetId}`; if (token) thumbUrl += `?token=${token}`; else if (scene.privacy === 'shared' && scene.shareToken) thumbUrl += `?token=${scene.shareToken}`; - thumbHtml = `${sceneName}`; + thumbHtml = `${sceneName}`; } const calloutIcon = L.divIcon({ @@ -1423,6 +1435,9 @@ async function openScene(sceneId, privacy, shareToken, force = false, initialPit // Initialize 3D Viewer with secure, referer-protected image stream initPanoramaViewer(secureImageUrl, hotspots || [], sceneOwnerId, initialPitch, initialYaw); + // Hiển thị thông tin overlay góc màn hình + updateViewerInfoOverlay(scene); + // Sau khi mở thành công từ URL trực tiếp, xóa tham số để làm sạch thanh địa chỉ (URL chuyên nghiệp) const urlParams = new URLSearchParams(window.location.search); if (urlParams.has('sceneId') || window.location.pathname.includes('/api/share/')) { @@ -1453,6 +1468,72 @@ async function openScene(sceneId, privacy, shareToken, force = false, initialPit } } +/** + * Cập nhật overlay thông tin cảnh đang xem (Góc dưới bên trái) + */ +async function updateViewerInfoOverlay(scene) { + if (!scene) { + console.warn("updateViewerInfoOverlay: Scene object is null or undefined. Cannot display info."); + return; + } + + let overlay = document.getElementById('viewer-info-overlay'); + if (!overlay) { + overlay = document.createElement('div'); + overlay.id = 'viewer-info-overlay'; + // Append to body to allow independent positioning and animation + document.body.appendChild(overlay); + } + + const lang = systemSettings.language || 'vi'; + const name = scene.name || scene.title || (lang === 'vi' ? "Cảnh không tên" : "Untitled Scene"); + const desc = scene.description || ""; + const author = scene.createdBy?.username || (lang === 'vi' ? "Ẩn danh" : "Anonymous"); + const date = formatSystemDate(scene.createdAt); + + // Lấy tọa độ để truy vấn địa chỉ + const lat = scene.gps?.lat || scene.lat; + const lng = scene.gps?.lng || scene.lng; + let locationText = lang === 'vi' ? "Đang xác định vị trí..." : "Locating..."; + + overlay.innerHTML = ` +
+

${name}

+ ${desc ? `

${desc}

` : ''} +
+ 👤 ${author} + 📸 ${lang === 'vi' ? 'Ngày chụp' : 'Date taken'}: ${date} + 📍 ${locationText} +
+
+ `; + overlay.classList.add('show'); // Make it visible with transition + + // Thực hiện Reverse Geocoding để lấy địa chỉ từ tọa độ + try { + const res = await fetch(`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${lat}&lon=${lng}`); + const data = await res.json(); + const address = data.display_name || `${lat.toFixed(4)}, ${lng.toFixed(4)}`; + const addrElem = document.getElementById('overlay-address'); + if (addrElem) addrElem.innerText = `📍 ${address}`; + } catch (e) { + const addrElem = document.getElementById('overlay-address'); + if (addrElem) addrElem.innerText = `📍 ${lat.toFixed(4)}, ${lng.toFixed(4)}`; + } +} + +/** + * Ẩn overlay thông tin cảnh đang xem + */ +window.hideViewerInfoOverlay = function() { + const overlay = document.getElementById('viewer-info-overlay'); + if (overlay) { + overlay.classList.remove('show'); // Hide it with transition + // Optionally remove from DOM after transition if not needed + // setTimeout(() => overlay.remove(), 300); // Match CSS transition duration + } +} + /** * Khôi phục Scene đang xem từ localStorage sau khi reload trang */ @@ -1951,7 +2032,7 @@ async function loadMyTours() { if (assetId) { let thumbUrl = `${API_BASE_URL}/assets/view/${assetId}`; if (token) thumbUrl += `?token=${token}`; - card.style.backgroundImage = `url('${thumbUrl}')`; + card.innerHTML = ``; } else { card.style.backgroundColor = '#1a1a1a'; } diff --git a/frontend/js/viewer360.js b/frontend/js/viewer360.js index 5edffeb..f3e4267 100644 --- a/frontend/js/viewer360.js +++ b/frontend/js/viewer360.js @@ -144,6 +144,11 @@ function initPanoramaViewer(imageUrl, hotspots = [], ownerId = null, initialPitc function closeViewer() { document.getElementById('viewer-container').style.display = 'none'; + // Ẩn overlay thông tin cảnh nếu có + if (typeof window.hideViewerInfoOverlay === 'function') { + window.hideViewerInfoOverlay(); + } + // Xóa trạng thái Scene đang hoạt động khi đóng viewer localStorage.removeItem('activeSceneId'); localStorage.removeItem('activeScenePrivacy');