feat: Add non-gemini generated image tips, and add a image lightbox (#4)

* wip: detect image not be made with gemini

* wip: multi image list style

* feat: add image lightbox
main
Jad 2025-12-22 23:49:09 +08:00
parent 2da2b0c9ed
commit d0025877c4
7 changed files with 137 additions and 55 deletions

View File

@ -15,7 +15,9 @@
"serve": "npx serve dist" "serve": "npx serve dist"
}, },
"dependencies": { "dependencies": {
"jszip": "^3.10.1" "exifr": "^7.1.3",
"jszip": "^3.10.1",
"medium-zoom": "^1.1.0"
}, },
"devDependencies": { "devDependencies": {
"esbuild": "^0.24.0" "esbuild": "^0.24.0"

View File

@ -8,9 +8,15 @@ importers:
.: .:
dependencies: dependencies:
exifr:
specifier: ^7.1.3
version: 7.1.3
jszip: jszip:
specifier: ^3.10.1 specifier: ^3.10.1
version: 3.10.1 version: 3.10.1
medium-zoom:
specifier: ^1.1.0
version: 1.1.0
devDependencies: devDependencies:
esbuild: esbuild:
specifier: ^0.24.0 specifier: ^0.24.0
@ -176,6 +182,9 @@ packages:
engines: {node: '>=18'} engines: {node: '>=18'}
hasBin: true hasBin: true
exifr@7.1.3:
resolution: {integrity: sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw==}
immediate@3.0.6: immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
@ -191,6 +200,9 @@ packages:
lie@3.3.0: lie@3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
medium-zoom@1.1.0:
resolution: {integrity: sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ==}
pako@1.0.11: pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
@ -319,6 +331,8 @@ snapshots:
'@esbuild/win32-ia32': 0.24.2 '@esbuild/win32-ia32': 0.24.2
'@esbuild/win32-x64': 0.24.2 '@esbuild/win32-x64': 0.24.2
exifr@7.1.3: {}
immediate@3.0.6: {} immediate@3.0.6: {}
inherits@2.0.4: {} inherits@2.0.4: {}
@ -336,6 +350,8 @@ snapshots:
dependencies: dependencies:
immediate: 3.0.6 immediate: 3.0.6
medium-zoom@1.1.0: {}
pako@1.0.11: {} pako@1.0.11: {}
process-nextick-args@2.0.1: {} process-nextick-args@2.0.1: {}

View File

@ -19,6 +19,10 @@
primary: '#10B981', primary: '#10B981',
'primary-hover': '#059669', 'primary-hover': '#059669',
dark: '#1F2937', dark: '#1F2937',
success: '#10B981',
warn: '#F59E0B',
err: '#EF4444',
info: '#3B82F6',
}, },
boxShadow: { boxShadow: {
'soft': '0 4px 20px -2px rgba(16, 185, 129, 0.1)', 'soft': '0 4px 20px -2px rgba(16, 185, 129, 0.1)',
@ -65,6 +69,14 @@
} }
body.loading { opacity: 0; } body.loading { opacity: 0; }
body { transition: opacity 0s; } body { transition: opacity 0s; }
.medium-zoom-overlay,
.medium-zoom-image--opened {
z-index: 999;
}
.medium-zoom-overlay {
backdrop-filter: blur(4px);
}
</style> </style>
</head> </head>
<body class="bg-white text-gray-800 antialiased selection:bg-primary selection:text-white flex flex-col min-h-screen loading"> <body class="bg-white text-gray-800 antialiased selection:bg-primary selection:text-white flex flex-col min-h-screen loading">
@ -103,7 +115,7 @@
<div id="uploadArea" class="group relative flex flex-col items-center justify-center w-full h-64 border-2 border-dashed border-emerald-200 rounded-xl bg-emerald-50/30 hover:bg-emerald-50 hover:border-emerald-400 transition-all cursor-pointer"> <div id="uploadArea" class="group relative flex flex-col items-center justify-center w-full h-64 border-2 border-dashed border-emerald-200 rounded-xl bg-emerald-50/30 hover:bg-emerald-50 hover:border-emerald-400 transition-all cursor-pointer">
<div class="flex flex-col items-center justify-center pt-5 pb-6"> <div class="flex flex-col items-center justify-center pt-5 pb-6">
<div class="w-16 h-16 bg-white rounded-full flex items-center justify-center shadow-sm mb-4 group-hover:scale-110 transition-transform duration-300"> <div class="w-16 h-16 bg-white rounded-full flex items-center justify-center shadow-sm mb-4 group-hover:scale-110 transition-transform duration-300">
<svg class="w-8 h-8 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path></svg> <svg class="w-8 h-8 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M 22.498 20.365 L 22.498 4.247 C 22.498 2.983 21.461 1.946 20.197 1.946 L 4.079 1.946 C 2.815 1.946 1.778 2.983 1.778 4.247 L 1.778 20.365 C 1.778 21.629 2.815 22.666 4.079 22.666 L 20.197 22.666 C 21.461 22.666 22.498 21.629 22.498 20.365 Z M 8.111 14.032 L 10.987 17.483 L 15.014 12.306 L 20.197 19.214 L 4.079 19.214 L 8.111 14.032 Z" fill="currentColor"></path></svg>
</div> </div>
<p class="mb-2 text-lg font-medium text-gray-700" data-i18n="upload.text">点击选择 或 拖拽图片至此</p> <p class="mb-2 text-lg font-medium text-gray-700" data-i18n="upload.text">点击选择 或 拖拽图片至此</p>
<p class="text-sm text-gray-400" data-i18n="upload.hint">支持 JPG, PNG, WebP</p> <p class="text-sm text-gray-400" data-i18n="upload.hint">支持 JPG, PNG, WebP</p>
@ -140,8 +152,8 @@
</h3> </h3>
<span id="originalInfo" class="text-xs text-gray-400 font-mono"></span> <span id="originalInfo" class="text-xs text-gray-400 font-mono"></span>
</div> </div>
<div class="p-4 bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgZmlsbD0iI2Y5ZmRmZCI+PHJlY3Qgd2lkdGg9IjEwIiBoZWlnaHQ9IjEwIiBmaWxsPSIjZjJmMmYyIi8+PHJlY3QgeD0iMTAiIHk9IjEwIiB3aWR0aD0iMTAiIGhlaWdodD0iMTAiIGZpbGw9IiNmMmYyZjIiLz48L3N2Zz4=')]"> <div class="h-[200px] md:h-[500px] p-4 bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgZmlsbD0iI2Y5ZmRmZCI+PHJlY3Qgd2lkdGg9IjEwIiBoZWlnaHQ9IjEwIiBmaWxsPSIjZjJmMmYyIi8+PHJlY3QgeD0iMTAiIHk9IjEwIiB3aWR0aD0iMTAiIGhlaWdodD0iMTAiIGZpbGw9IiNmMmYyZjIiLz48L3N2Zz4=')]">
<canvas id="originalCanvas" class="max-w-full h-auto mx-auto rounded-lg shadow-sm block"></canvas> <img id="originalImage" class="max-w-full max-h-full mx-auto rounded-lg shadow-sm block" data-zoomable />
</div> </div>
</div> </div>
@ -152,8 +164,8 @@
</h3> </h3>
<span id="processedInfo" class="text-xs text-emerald-600 font-mono"></span> <span id="processedInfo" class="text-xs text-emerald-600 font-mono"></span>
</div> </div>
<div class="p-4 bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgZmlsbD0iI2Y5ZmRmZCI+PHJlY3Qgd2lkdGg9IjEwIiBoZWlnaHQ9IjEwIiBmaWxsPSIjZjJmMmYyIi8+PHJlY3QgeD0iMTAiIHk9IjEwIiB3aWR0aD0iMTAiIGhlaWdodD0iMTAiIGZpbGw9IiNmMmYyZjIiLz48L3N2Zz4=')]"> <div class="h-[200px] md:h-[500px] p-4 bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgZmlsbD0iI2Y5ZmRmZCI+PHJlY3Qgd2lkdGg9IjEwIiBoZWlnaHQ9IjEwIiBmaWxsPSIjZjJmMmYyIi8+PHJlY3QgeD0iMTAiIHk9IjEwIiB3aWR0aD0iMTAiIGhlaWdodD0iMTAiIGZpbGw9IiNmMmYyZjIiLz48L3N2Zz4=')]">
<img id="processedImage" class="max-w-full h-auto mx-auto rounded-lg shadow-sm block" /> <img id="processedImage" class="max-w-full max-h-full mx-auto rounded-lg shadow-sm block" data-zoomable />
</div> </div>
</div> </div>
</div> </div>
@ -174,7 +186,7 @@
</button> </button>
</div> </div>
<div id="statusMessage" class="mt-6 text-sm text-center text-gray-500 min-h-[1.25rem]"></div> <div id="statusMessage" class="mt-6 text-sm text-gray-500 min-h-[1.25rem]"></div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,11 +1,14 @@
import { WatermarkEngine } from './core/watermarkEngine.js'; import { WatermarkEngine } from './core/watermarkEngine.js';
import i18n from './i18n.js'; import i18n from './i18n.js';
import { loadImage, checkOriginal, getOriginalStatus, setStatusMessage, showLoading, hideLoading } from './utils.js';
import JSZip from 'jszip'; import JSZip from 'jszip';
import mediumZoom from 'medium-zoom';
// global state // global state
let engine = null; let engine = null;
let imageQueue = []; let imageQueue = [];
let processedCount = 0; let processedCount = 0;
let zoom = null;
// dom elements references // dom elements references
const uploadArea = document.getElementById('uploadArea'); const uploadArea = document.getElementById('uploadArea');
@ -15,15 +18,13 @@ const multiPreview = document.getElementById('multiPreview');
const imageList = document.getElementById('imageList'); const imageList = document.getElementById('imageList');
const progressText = document.getElementById('progressText'); const progressText = document.getElementById('progressText');
const downloadAllBtn = document.getElementById('downloadAllBtn'); const downloadAllBtn = document.getElementById('downloadAllBtn');
const loadingOverlay = document.getElementById('loadingOverlay'); const originalImage = document.getElementById('originalImage');
const originalCanvas = document.getElementById('originalCanvas');
const processedSection = document.getElementById('processedSection'); const processedSection = document.getElementById('processedSection');
const processedImage = document.getElementById('processedImage'); const processedImage = document.getElementById('processedImage');
const originalInfo = document.getElementById('originalInfo'); const originalInfo = document.getElementById('originalInfo');
const processedInfo = document.getElementById('processedInfo'); const processedInfo = document.getElementById('processedInfo');
const downloadBtn = document.getElementById('downloadBtn'); const downloadBtn = document.getElementById('downloadBtn');
const resetBtn = document.getElementById('resetBtn'); const resetBtn = document.getElementById('resetBtn');
const statusMessage = document.getElementById('statusMessage');
/** /**
* initialize the application * initialize the application
@ -38,6 +39,12 @@ async function init() {
hideLoading(); hideLoading();
setupEventListeners(); setupEventListeners();
zoom = mediumZoom('[data-zoomable]', {
margin: 24,
scrollOffset: 0,
background: 'rgba(255, 255, 255, .6)',
})
} catch (error) { } catch (error) {
hideLoading(); hideLoading();
console.error('初始化错误:', error); console.error('初始化错误:', error);
@ -136,16 +143,17 @@ async function processSingle(item) {
const img = await loadImage(item.file); const img = await loadImage(item.file);
item.originalImg = img; item.originalImg = img;
originalCanvas.width = img.width; const { is_google, is_original } = await checkOriginal(item.file);
originalCanvas.height = img.height; const status = getOriginalStatus({ is_google, is_original });
originalCanvas.getContext('2d').drawImage(img, 0, 0); setStatusMessage(status, is_google && is_original ? 'success' : 'warn');
originalImage.src = img.src;
// update original image info
const watermarkInfo = engine.getWatermarkInfo(img.width, img.height); const watermarkInfo = engine.getWatermarkInfo(img.width, img.height);
originalInfo.innerHTML = ` originalInfo.innerHTML = `
<strong>${i18n.t('info.size')}</strong>${img.width} × ${img.height} px<br> <p>${i18n.t('info.size')}: ${img.width}×${img.height}</p>
<strong>${i18n.t('info.watermark')}</strong>${watermarkInfo.size}×${watermarkInfo.size} px<br> <p>${i18n.t('info.watermark')}: ${watermarkInfo.size}×${watermarkInfo.size}</p>
<strong>${i18n.t('info.position')}</strong>(${watermarkInfo.position.x}, ${watermarkInfo.position.y}) <p>${i18n.t('info.position')}: (${watermarkInfo.position.x},${watermarkInfo.position.y})</p>
`; `;
const result = await engine.removeWatermarkFromImage(img); const result = await engine.removeWatermarkFromImage(img);
@ -158,10 +166,13 @@ async function processSingle(item) {
downloadBtn.onclick = () => downloadImage(item); downloadBtn.onclick = () => downloadImage(item);
processedInfo.innerHTML = ` processedInfo.innerHTML = `
<strong>${i18n.t('info.size')}</strong>${img.width} × ${img.height} px<br> <p>${i18n.t('info.size')}: ${img.width}×${img.height}</p>
<strong>${i18n.t('info.status')}</strong>${i18n.t('info.removed')} <p>${i18n.t('info.status')}: ${i18n.t('info.removed')}</p>
`; `;
zoom.detach();
zoom.attach('[data-zoomable]');
processedSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); processedSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -171,20 +182,20 @@ async function processSingle(item) {
function createImageCard(item) { function createImageCard(item) {
const card = document.createElement('div'); const card = document.createElement('div');
card.id = `card-${item.id}`; card.id = `card-${item.id}`;
card.className = 'bg-white md:h-[130px] rounded-xl shadow-card border border-gray-100 overflow-hidden'; card.className = 'bg-white md:h-[140px] rounded-xl shadow-card border border-gray-100 overflow-hidden';
card.innerHTML = ` card.innerHTML = `
<div class="flex flex-wrap h-full relative"> <div class="flex flex-wrap h-full">
<div class="w-full md:w-auto h-full flex"> <div class="w-full md:w-auto h-full flex border-b border-gray-100">
<div class="w-24 md:w-48 flex-shrink-0 bg-gray-50 p-2 flex items-center justify-center"> <div class="w-24 md:w-48 flex-shrink-0 bg-gray-50 p-2 flex items-center justify-center">
<img id="result-${item.id}" class="max-w-full max-h-full rounded"></img> <img id="result-${item.id}" class="max-w-full max-h-24 md:max-h-full rounded" data-zoomable />
</div> </div>
<div class="flex-1 p-4 flex flex-col min-w-0"> <div class="flex-1 p-4 flex flex-col min-w-0">
<h4 class="font-semibold text-gray-900 mb-2 truncate">${item.name}</h4> <h4 class="font-semibold text-sm text-gray-900 mb-2 truncate">${item.name}</h4>
<div class="text-xs text-gray-500" id="status-${item.id}">${i18n.t('status.pending')}</div> <div class="text-xs text-gray-500" id="status-${item.id}">${i18n.t('status.pending')}</div>
</div> </div>
</div> </div>
<div class="absolute bottom-0 right-0 md:static w-auto ml-auto flex-shrink-0 p-2 md:p-4 flex items-center justify-center"> <div class="w-full md:w-auto ml-auto flex-shrink-0 p-2 md:p-4 flex items-center justify-center">
<button id="download-${item.id}" class="px-4 py-2 bg-gray-900 hover:bg-gray-800 text-white rounded-lg text-sm hidden">${i18n.t('btn.download')}</button> <button id="download-${item.id}" class="px-4 py-2 bg-gray-900 hover:bg-gray-800 text-white rounded-lg text-xs md:text-sm hidden">${i18n.t('btn.download')}</button>
</div> </div>
</div> </div>
`; `;
@ -196,6 +207,7 @@ async function processQueue() {
const img = await loadImage(item.file); const img = await loadImage(item.file);
item.originalImg = img; item.originalImg = img;
document.getElementById(`result-${item.id}`).src = img.src; document.getElementById(`result-${item.id}`).src = img.src;
zoom.attach(`#result-${item.id}`);
} }
for (const item of imageQueue) { for (const item of imageQueue) {
@ -213,9 +225,13 @@ async function processQueue() {
item.status = 'completed'; item.status = 'completed';
const watermarkInfo = engine.getWatermarkInfo(item.originalImg.width, item.originalImg.height); const watermarkInfo = engine.getWatermarkInfo(item.originalImg.width, item.originalImg.height);
updateStatus(item.id, `<strong>${i18n.t('info.size')}</strong>${item.originalImg.width} × ${item.originalImg.height} px<br> const { is_google, is_original } = await checkOriginal(item.originalImg);
<strong>${i18n.t('info.watermark')}</strong>${watermarkInfo.size}×${watermarkInfo.size} px<br> const originalStatus = getOriginalStatus({ is_google, is_original });
<strong>${i18n.t('info.position')}</strong>(${watermarkInfo.position.x}, ${watermarkInfo.position.y})`, true);
updateStatus(item.id, `<p>${i18n.t('info.size')}: ${item.originalImg.width}×${item.originalImg.height}</p>
<p>${i18n.t('info.watermark')}: ${watermarkInfo.size}×${watermarkInfo.size}</p>
<p>${i18n.t('info.position')}: (${watermarkInfo.position.x},${watermarkInfo.position.y})</p>
<p class="inline-block mt-1 text-xs md:text-sm ${is_google && is_original ? 'hidden' : 'text-warn'}">${originalStatus}</p>`, true);
const downloadBtn = document.getElementById(`download-${item.id}`); const downloadBtn = document.getElementById(`download-${item.id}`);
downloadBtn.classList.remove('hidden'); downloadBtn.classList.remove('hidden');
@ -235,20 +251,6 @@ async function processQueue() {
} }
} }
function loadImage(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = e.target.result;
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
function updateStatus(id, text, isHtml = false) { function updateStatus(id, text, isHtml = false) {
const el = document.getElementById(`status-${id}`); const el = document.getElementById(`status-${id}`);
if (el) el.innerHTML = isHtml ? text : text.replace(/\n/g, '<br>'); if (el) el.innerHTML = isHtml ? text : text.replace(/\n/g, '<br>');
@ -288,14 +290,4 @@ async function downloadAll() {
a.click(); a.click();
} }
function showLoading(text = null) {
loadingOverlay.style.display = 'flex';
const textEl = loadingOverlay.querySelector('p');
if (textEl && text) textEl.textContent = text;
}
function hideLoading() {
loadingOverlay.style.display = 'none';
}
init(); init();

View File

@ -39,5 +39,8 @@
"info.watermark": "Detected Watermark", "info.watermark": "Detected Watermark",
"info.position": "Position", "info.position": "Position",
"info.status": "Status", "info.status": "Status",
"info.removed": "Watermark Removed" "info.removed": "Watermark Removed",
"original.not_gemini": "⚠️ This image may not be made with Gemini. Lossless processing unavailable.",
"original.not_original": "⚠️ This image may not be original size. Lossless processing unavailable.",
"original.pass": "✅ This image made with Gemini. Lossless processing supported."
} }

View File

@ -39,5 +39,8 @@
"info.watermark": "检测到的水印", "info.watermark": "检测到的水印",
"info.position": "位置", "info.position": "位置",
"info.status": "状态", "info.status": "状态",
"info.removed": "水印已移除" "info.removed": "水印已移除",
"original.not_gemini": "⚠️ 此图片非 Gemini 生成的原始图片,可能无法进行无损去水印",
"original.not_original": "⚠️ 此图片非原始尺寸,可能无法进行无损去水印",
"original.pass": "✅ 此图片为 Gemini 生成的原始图片,可进行无损去水印"
} }

54
src/utils.js 100644
View File

@ -0,0 +1,54 @@
import exifr from 'exifr';
import i18n from './i18n.js';
export function loadImage(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = e.target.result;
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
export async function checkOriginal(file) {
try {
const exif = await exifr.parse(file, { xmp: true });
return {
is_google: exif?.Credit === 'Made with Google AI',
is_original: ['ImageWidth', 'ImageHeight'].every(key => exif?.[key])
}
} catch {
return { is_google: false, is_original: false };
}
}
export function getOriginalStatus({ is_google, is_original }) {
if (!is_google) return i18n.t('original.not_gemini');
if (!is_original) return i18n.t('original.not_original');
return '';
}
const statusMessage = document.getElementById('statusMessage');
export function setStatusMessage(message = '', type = '') {
statusMessage.textContent = message;
statusMessage.style.display = message ? 'block' : 'none';
const colorMap = { warn: 'text-warn', success: 'text-success' };
statusMessage.classList.remove(...Object.values(colorMap));
if (colorMap[type]) statusMessage.classList.add(colorMap[type]);
}
const loadingOverlay = document.getElementById('loadingOverlay');
export function showLoading(text = null) {
loadingOverlay.style.display = 'flex';
const textEl = loadingOverlay.querySelector('p');
if (textEl && text) textEl.textContent = text;
}
export function hideLoading() {
loadingOverlay.style.display = 'none';
}