commit
This commit is contained in:
parent
52a2ae9a31
commit
d563f7244d
@ -1,3 +1,17 @@
|
||||
module github.com/scanner/backend
|
||||
|
||||
go 1.20
|
||||
go 1.22
|
||||
|
||||
toolchain go1.23.7
|
||||
|
||||
require (
|
||||
github.com/hhrutter/lzw v1.0.0 // indirect
|
||||
github.com/hhrutter/tiff v1.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/pdfcpu/pdfcpu v0.9.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
golang.org/x/image v0.21.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
23
backend/go.sum
Normal file
23
backend/go.sum
Normal file
@ -0,0 +1,23 @@
|
||||
github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0=
|
||||
github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo=
|
||||
github.com/hhrutter/tiff v1.0.1 h1:MIus8caHU5U6823gx7C6jrfoEvfSTGtEFRiM8/LOzC0=
|
||||
github.com/hhrutter/tiff v1.0.1/go.mod h1:zU/dNgDm0cMIa8y8YwcYBeuEEveI4B0owqHyiPpJPHc=
|
||||
github.com/jung-kurt/gofpdf/v2 v2.0.0/go.mod h1:59m4YstrsXqnwOx8NOVIk6k0XqfrRPkiunj4eAevyaY=
|
||||
github.com/jung-kurt/gofpdf/v2 v2.17.3 h1:otZXZby2gXJ7uU6pzprXHq/R57lsHLi0WtH79VabWxY=
|
||||
github.com/jung-kurt/gofpdf/v2 v2.17.3/go.mod h1:Qx8ZNg4cNsO5i6uLDiBngnm+ii/FjtAqjRNO6drsoYU=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/pdfcpu/pdfcpu v0.9.1 h1:q8/KlBdHjkE7ZJU4ofhKG5Rjf7M6L324CVM6BMDySao=
|
||||
github.com/pdfcpu/pdfcpu v0.9.1/go.mod h1:fVfOloBzs2+W2VJCCbq60XIxc3yJHAZ0Gahv1oO0gyI=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
|
||||
golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
57
backend/handlers/file_handlers.go
Normal file
57
backend/handlers/file_handlers.go
Normal file
@ -0,0 +1,57 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/scanner/backend/middleware"
|
||||
"github.com/scanner/backend/models"
|
||||
"github.com/scanner/backend/services"
|
||||
)
|
||||
|
||||
type FileHandler struct {
|
||||
fileConverter *services.FileConverter
|
||||
}
|
||||
|
||||
func NewFileHandler(fileConverter *services.FileConverter) *FileHandler {
|
||||
return &FileHandler{fileConverter: fileConverter}
|
||||
}
|
||||
|
||||
func (h *FileHandler) MergeFiles(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
middleware.ApplyCors(w, r)
|
||||
|
||||
var mergeRequest models.MergeRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&mergeRequest)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if len(mergeRequest.Filenames) < 2 {
|
||||
http.Error(w, "At least two files are required for merging", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
files := []string{}
|
||||
for _, filename := range mergeRequest.Filenames {
|
||||
files = append(files, "/home/backend/var/documents/"+filename)
|
||||
}
|
||||
|
||||
mergedFilename, err := h.fileConverter.MergeFiles(files)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to merge files: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]string{
|
||||
"mergedFile": "/files/" + mergedFilename,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
@ -12,9 +12,11 @@ import (
|
||||
func main() {
|
||||
// Initialize services
|
||||
scannerService := services.NewScannerService()
|
||||
fileConverter := services.NewFileConverter()
|
||||
|
||||
// Initialize handlers
|
||||
scanHandler := handlers.NewScanHandler(scannerService)
|
||||
fileHandler := handlers.NewFileHandler(fileConverter)
|
||||
|
||||
// Set up routes with middleware
|
||||
mux := http.NewServeMux()
|
||||
@ -24,6 +26,7 @@ func main() {
|
||||
mux.HandleFunc("/scan/abort", scanHandler.AbortScan)
|
||||
mux.HandleFunc("/scan/status", scanHandler.GetScanStatus)
|
||||
mux.HandleFunc("/documents", scanHandler.GetDocuments)
|
||||
mux.HandleFunc("/documents/merge", fileHandler.MergeFiles)
|
||||
|
||||
fileServer := http.FileServer(http.Dir("/home/backend/var/documents"))
|
||||
mux.Handle("/files/", http.StripPrefix("/files", fileServer))
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
package models
|
||||
|
||||
// Document represents a scanned document
|
||||
type Document struct {
|
||||
Filename string `json:"filename"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type MergeRequest struct {
|
||||
Filenames []string `json:"filenames"`
|
||||
}
|
||||
|
||||
37
backend/services/file_converter.go
Normal file
37
backend/services/file_converter.go
Normal file
@ -0,0 +1,37 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/pdfcpu/pdfcpu/pkg/api"
|
||||
)
|
||||
|
||||
type FileConverter struct {
|
||||
}
|
||||
|
||||
func NewFileConverter() *FileConverter {
|
||||
return &FileConverter{}
|
||||
}
|
||||
|
||||
func (c *FileConverter) MergeFiles(filenames []string) (string, error) {
|
||||
timestamp := time.Now().Format("20060102-150405")
|
||||
mergedFilename := "merged-" + timestamp + ".pdf"
|
||||
mergedFile := "/home/backend/var/documents/" + mergedFilename
|
||||
|
||||
outputDir := filepath.Dir(mergedFile)
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_ = os.Remove(mergedFile)
|
||||
|
||||
config := api.LoadConfiguration()
|
||||
|
||||
if err := api.MergeCreateFile(filenames, mergedFile, false, config); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return mergedFilename, nil
|
||||
}
|
||||
@ -42,7 +42,7 @@ func listDevices() ([]string, error) {
|
||||
}
|
||||
|
||||
func startScan(outputFile string) (string, error) {
|
||||
result, err := scanImage("-d", "escl:https://printer:443", "--format", "png", "--output-file", outputFile)
|
||||
result, err := scanImage("-d", "escl:https://printer:443", "--format", "pdf", "--output-file", outputFile)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error scanning: %v", err)
|
||||
}
|
||||
|
||||
@ -9,12 +9,10 @@ import (
|
||||
"github.com/scanner/backend/models"
|
||||
)
|
||||
|
||||
// ScannerService handles scanner operations
|
||||
type ScannerService struct {
|
||||
state models.ScannerState
|
||||
}
|
||||
|
||||
// NewScannerService creates a new scanner service
|
||||
func NewScannerService() *ScannerService {
|
||||
return &ScannerService{
|
||||
state: models.ScannerState{
|
||||
@ -23,7 +21,6 @@ func NewScannerService() *ScannerService {
|
||||
}
|
||||
}
|
||||
|
||||
// GetScanStatus returns the current scan status
|
||||
func (s *ScannerService) IsScanning() bool {
|
||||
s.state.Mu.Lock()
|
||||
state := s.state.IsScanning
|
||||
@ -32,7 +29,6 @@ func (s *ScannerService) IsScanning() bool {
|
||||
return state
|
||||
}
|
||||
|
||||
// StartScan begins a new scanning process
|
||||
func (s *ScannerService) StartScan() (bool, error) {
|
||||
s.state.Mu.Lock()
|
||||
defer s.state.Mu.Unlock()
|
||||
@ -47,7 +43,6 @@ func (s *ScannerService) StartScan() (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// AbortScan stops an in-progress scan
|
||||
func (s *ScannerService) AbortScan() (bool, error) {
|
||||
s.state.Mu.Lock()
|
||||
defer s.state.Mu.Unlock()
|
||||
@ -60,7 +55,6 @@ func (s *ScannerService) AbortScan() (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// GetDocuments returns all scanned documents
|
||||
func (s *ScannerService) GetDocuments() []models.Document {
|
||||
const outputDir = "/home/backend/var/documents"
|
||||
|
||||
@ -76,7 +70,7 @@ func (s *ScannerService) GetDocuments() []models.Document {
|
||||
}
|
||||
|
||||
filename := file.Name()
|
||||
if !strings.HasPrefix(filename, "scan_") || !strings.HasSuffix(filename, ".jpg") {
|
||||
if !strings.HasSuffix(filename, ".pdf") {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -100,7 +94,7 @@ func (s *ScannerService) scan() {
|
||||
|
||||
outputDir := "/home/backend/var/documents"
|
||||
timestamp := time.Now().Format("20060102150405")
|
||||
outputFile := fmt.Sprintf("%s/scan_%s.jpg", outputDir, timestamp)
|
||||
outputFile := fmt.Sprintf("%s/scan_%s.pdf", outputDir, timestamp)
|
||||
|
||||
_, err := startScan(outputFile)
|
||||
|
||||
|
||||
32
docker-compose.server.yml
Normal file
32
docker-compose.server.yml
Normal file
@ -0,0 +1,32 @@
|
||||
services:
|
||||
backend:
|
||||
hostname: scanner-backend
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "8089:80"
|
||||
extra_hosts:
|
||||
- "printer:192.168.178.36"
|
||||
networks:
|
||||
- printer_network
|
||||
- proxy
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
|
||||
frontend:
|
||||
hostname: scanner-frontend
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "8090:80"
|
||||
networks:
|
||||
- printer_network
|
||||
- proxy
|
||||
|
||||
networks:
|
||||
printer_network:
|
||||
driver: bridge
|
||||
proxy:
|
||||
external: true
|
||||
@ -5,76 +5,251 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document Scanner</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
: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: 20px;
|
||||
gap: var(--spacing);
|
||||
background-color: white;
|
||||
border-radius: var(--radius);
|
||||
padding: var(--spacing);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background-color: #4285f4;
|
||||
border-radius: var(--radius);
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
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: #3367d6;
|
||||
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(200px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.document-card {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
box-shadow: var(--shadow);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
.document-card img {
|
||||
|
||||
.document-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 15px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.document-card .pdf-thumbnail {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
object-fit: cover;
|
||||
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: 10px;
|
||||
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: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
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">
|
||||
<h1>Document Scanner</h1>
|
||||
|
||||
<div id="status" class="status idle">Scanner is idle</div>
|
||||
<div class="container">
|
||||
<div id="status" class="status idle">Scanner ist bereit</div>
|
||||
|
||||
<div class="controls">
|
||||
<button id="startBtn">Start Scanning</button>
|
||||
@ -82,17 +257,33 @@
|
||||
</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">×</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;
|
||||
@ -134,28 +325,136 @@
|
||||
}
|
||||
}
|
||||
|
||||
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">
|
||||
<img src="${doc.url}" alt="${doc.filename}">
|
||||
<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);
|
||||
@ -180,9 +479,13 @@
|
||||
|
||||
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>
|
||||
</html>
|
||||
@ -8,7 +8,7 @@ server {
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://backend:80/;
|
||||
proxy_pass http://scanner-backend:80/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user