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 lightboxmain
parent
2da2b0c9ed
commit
d0025877c4
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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: {}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
84
src/app.js
84
src/app.js
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 生成的原始图片,可进行无损去水印"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue