scanner/frontend/public/index.html
2025-04-24 20:44:45 +02:00

491 lines
15 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document Scanner</title>
<style>
:root {
--primary-color: #4285f4;
--primary-hover: #3367d6;
--light-gray: #f5f5f5;
--border-color: #e0e0e0;
--shadow: 0 4px 6px rgba(0,0,0,0.1);
--radius: 8px;
--spacing: 24px;
}
body {
font-family: 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
max-width: 1000px;
margin: 0 auto;
padding: 30px;
background-color: #fafafa;
color: #333;
line-height: 1.6;
}
h1, h2 {
color: #1a1a1a;
font-weight: 600;
}
h1 {
font-size: 2.5rem;
margin-bottom: var(--spacing);
border-bottom: 2px solid var(--primary-color);
padding-bottom: 10px;
}
h2 {
font-size: 1.8rem;
margin-top: var(--spacing);
margin-bottom: calc(var(--spacing) / 2);
}
.container {
display: flex;
flex-direction: column;
gap: var(--spacing);
background-color: white;
border-radius: var(--radius);
padding: var(--spacing);
box-shadow: var(--shadow);
}
.controls {
display: flex;
gap: 15px;
}
button {
padding: 12px 24px;
border: none;
border-radius: var(--radius);
background-color: var(--primary-color);
color: white;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 500;
font-size: 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
button:hover {
background-color: var(--primary-hover);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
button:active {
transform: translateY(0);
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
box-shadow: none;
transform: none;
}
.document-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 20px;
}
.document-card {
position: relative;
border: 1px solid var(--border-color);
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--shadow);
transition: transform 0.3s ease, box-shadow 0.3s ease;
cursor: pointer;
}
.document-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 15px rgba(0,0,0,0.15);
}
.document-card .pdf-thumbnail {
width: 100%;
height: 180px;
display: flex;
justify-content: center;
align-items: center;
background-color: #f8f9fa;
border-bottom: 1px solid var(--border-color);
}
.document-card .pdf-icon {
font-size: 60px;
color: #e74c3c;
}
.document-card .info {
padding: 15px;
background-color: white;
}
.document-card h3 {
margin: 0;
font-size: 1.1rem;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.checkbox-container {
position: absolute;
top: 10px;
left: 10px;
z-index: 5;
}
.merge-controls {
display: flex;
gap: 15px;
margin-bottom: 15px;
}
.status {
margin-bottom: 15px;
padding: 15px;
border-radius: var(--radius);
font-weight: 500;
display: flex;
align-items: center;
position: relative;
}
.status::before {
content: '';
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 10px;
}
.status.scanning {
background-color: #e6f7ff;
border: 1px solid #91d5ff;
color: #0050b3;
}
.status.scanning::before {
background-color: #1890ff;
animation: pulse 1.5s infinite;
}
.status.idle {
background-color: #f6ffed;
border: 1px solid #b7eb8f;
color: #389e0d;
}
.status.idle::before {
background-color: #52c41a;
}
.lightbox {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
z-index: 1000;
justify-content: center;
align-items: center;
}
.lightbox.open {
display: flex;
}
.lightbox-content {
width: 90%;
height: 90%;
position: relative;
}
.lightbox-content iframe {
width: 100%;
height: 100%;
border: none;
background-color: white;
}
.close-lightbox {
position: absolute;
top: -40px;
right: 0;
color: white;
font-size: 30px;
cursor: pointer;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
@media (max-width: 768px) {
body {
padding: 15px;
}
.document-grid {
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
}
}
</style>
</head>
<body>
<div class="container">
<div id="status" class="status idle">Scanner ist bereit</div>
<div class="controls">
<button id="startBtn">Start Scanning</button>
<button id="abortBtn" disabled>Abort Scanning</button>
</div>
<h2>Documents</h2>
<div class="merge-controls">
<button id="mergeSelectedBtn" disabled>Merge Selected</button>
<button id="mergeAllBtn" disabled>Merge All</button>
</div>
<div id="documents" class="document-grid">
<p>No documents scanned yet.</p>
</div>
</div>
<div id="pdfLightbox" class="lightbox">
<div class="lightbox-content">
<span class="close-lightbox">&times;</span>
<iframe id="pdfViewer"></iframe>
</div>
</div>
<script>
const API_URL = `/api`;
const startBtn = document.getElementById('startBtn');
const abortBtn = document.getElementById('abortBtn');
const mergeSelectedBtn = document.getElementById('mergeSelectedBtn');
const mergeAllBtn = document.getElementById('mergeAllBtn');
const statusEl = document.getElementById('status');
const documentsEl = document.getElementById('documents');
const pdfLightbox = document.getElementById('pdfLightbox');
const pdfViewer = document.getElementById('pdfViewer');
const closeLightbox = document.querySelector('.close-lightbox');
function setScannerState(scanning) {
startBtn.disabled = scanning;
abortBtn.disabled = !scanning;
if (scanning) {
statusEl.textContent = 'Scanner is running...';
statusEl.className = 'status scanning';
} else {
statusEl.textContent = 'Scanner is idle';
statusEl.className = 'status idle';
}
}
async function startScanning() {
startBtn.disabled = true;
try {
const response = await fetch(`${API_URL}/scan/start`, {
method: 'POST'
});
} catch (error) {
console.error('Error starting scan:', error);
alert('Failed to connect to scanner service.');
startBtn.disabled = false;
}
}
async function abortScanning() {
abortBtn.disabled = true;
try {
const response = await fetch(`${API_URL}/scan/abort`, {
method: 'POST'
});
} catch (error) {
console.error('Error aborting scan:', error);
alert('Failed to connect to scanner service.');
abortBtn.disabled = false;
}
}
async function mergeDocuments(filenames) {
try {
const response = await fetch(`${API_URL}/documents/merge`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ filenames })
});
if (!response.ok) {
throw new Error('Failed to merge documents');
}
const result = await response.json();
if (result.mergedFile) {
openPdfViewer(result.mergedFile);
}
} catch (error) {
console.error('Error merging documents:', error);
alert('Failed to merge documents.');
}
}
function mergeSelected() {
const selectedCheckboxes = document.querySelectorAll('.document-checkbox:checked');
if (selectedCheckboxes.length < 2) {
alert('Please select at least 2 documents to merge.');
return;
}
const filenames = Array.from(selectedCheckboxes).map(checkbox =>
checkbox.closest('.document-card').getAttribute('data-filename')
);
mergeDocuments(filenames);
}
function mergeAll() {
const allDocumentCards = document.querySelectorAll('.document-card');
if (allDocumentCards.length < 2) {
alert('Need at least 2 documents to merge.');
return;
}
const filenames = Array.from(allDocumentCards).map(card =>
card.getAttribute('data-filename')
);
mergeDocuments(filenames);
}
function updateMergeButtons() {
const documentCount = document.querySelectorAll('.document-card').length;
mergeAllBtn.disabled = documentCount < 2;
const selectedCount = document.querySelectorAll('.document-checkbox:checked').length;
mergeSelectedBtn.disabled = selectedCount < 2;
}
function openPdfViewer(pdfUrl) {
pdfViewer.src = pdfUrl;
pdfLightbox.classList.add('open');
}
function closePdfViewer() {
pdfLightbox.classList.remove('open');
pdfViewer.src = '';
}
function handleDocumentClick(event) {
const checkbox = event.target.closest('.document-checkbox');
if (checkbox) {
updateMergeButtons();
return;
}
const card = event.target.closest('.document-card');
if (card) {
const pdfUrl = card.getAttribute('data-url');
openPdfViewer(pdfUrl);
}
}
function pollDocuments() {
fetch(`${API_URL}/documents`)
.then(response => response.json())
.then(documents => {
if (documents.length === 0) {
documentsEl.innerHTML = '<p>No documents scanned yet.</p>';
mergeAllBtn.disabled = true;
mergeSelectedBtn.disabled = true;
setTimeout(() => {
pollDocuments();
}, 2000);
return;
}
const existingCards = Array.from(documentsEl.querySelectorAll('.document-card')) || [];
const existingFilenames = existingCards.map(card =>
card.getAttribute('data-filename')
);
if (existingFilenames.length !== documents.length ||
!documents.every(doc => existingFilenames.includes(doc.filename))) {
documentsEl.innerHTML = '';
} else {
setTimeout(() => {
pollDocuments();
}, 2000);
return;
}
documentsEl.innerHTML = documents.map(doc => `
<div class="document-card" data-filename="${doc.filename}" data-url="${doc.url}">
<div class="checkbox-container">
<input type="checkbox" class="document-checkbox">
</div>
<div class="pdf-thumbnail">
<div class="pdf-icon">PDF</div>
</div>
<div class="info">
<h3>${doc.filename}</h3>
</div>
</div>
`).join('');
updateMergeButtons();
setTimeout(() => {
pollDocuments();
}, 2000);
})
.catch(error => {
console.error('Error fetching documents:', error);
documentsEl.innerHTML = '<p>Error loading documents.</p>';
});
}
function pollStatus() {
fetch(`${API_URL}/scan/status`)
.then(response => response.json())
.then(data => {
setScannerState(data.isScanning);
setTimeout(() => {
pollStatus();
}, 1000);
});
}
startBtn.addEventListener('click', startScanning);
abortBtn.addEventListener('click', abortScanning);
mergeSelectedBtn.addEventListener('click', mergeSelected);
mergeAllBtn.addEventListener('click', mergeAll);
documentsEl.addEventListener('click', handleDocumentClick);
closeLightbox.addEventListener('click', closePdfViewer);
pollStatus();
pollDocuments();
</script>
</body>
</html>