mirror of
https://github.com/Theodor-Springmann-Stiftung/kgpz_web.git
synced 2025-12-15 11:35:30 +00:00
+Compressed IMG files
This commit is contained in:
11
app/kgpz.go
11
app/kgpz.go
@@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/Theodor-Springmann-Stiftung/kgpz_web/providers/xmlprovider"
|
"github.com/Theodor-Springmann-Stiftung/kgpz_web/providers/xmlprovider"
|
||||||
"github.com/Theodor-Springmann-Stiftung/kgpz_web/xmlmodels"
|
"github.com/Theodor-Springmann-Stiftung/kgpz_web/xmlmodels"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/compress"
|
||||||
"github.com/gofiber/fiber/v2/middleware/etag"
|
"github.com/gofiber/fiber/v2/middleware/etag"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -89,8 +90,9 @@ func (k *KGPZ) Pre(srv *fiber.App) error {
|
|||||||
// Check if folder exists and if yes, serve image files from it
|
// Check if folder exists and if yes, serve image files from it
|
||||||
if _, err := os.Stat(k.Config.Config.ImgPath); err == nil {
|
if _, err := os.Stat(k.Config.Config.ImgPath); err == nil {
|
||||||
fs := os.DirFS(k.Config.Config.ImgPath)
|
fs := os.DirFS(k.Config.Config.ImgPath)
|
||||||
srv.Use(IMG_PREFIX, etag.New())
|
srv.Use(IMG_PREFIX, compress.New(compress.Config{
|
||||||
srv.Use(IMG_PREFIX, helpers.StaticHandler(&fs))
|
Level: compress.LevelBestSpeed,
|
||||||
|
}), etag.New(), helpers.StaticHandler(&fs))
|
||||||
} else {
|
} else {
|
||||||
logging.Info("Image folder not found. Skipping image serving.")
|
logging.Info("Image folder not found. Skipping image serving.")
|
||||||
}
|
}
|
||||||
@@ -98,8 +100,9 @@ func (k *KGPZ) Pre(srv *fiber.App) error {
|
|||||||
// Serve newspaper pictures from pictures directory
|
// Serve newspaper pictures from pictures directory
|
||||||
if _, err := os.Stat("pictures"); err == nil {
|
if _, err := os.Stat("pictures"); err == nil {
|
||||||
picturesFS := os.DirFS("pictures")
|
picturesFS := os.DirFS("pictures")
|
||||||
srv.Use(PICTURES_PREFIX, etag.New())
|
srv.Use(PICTURES_PREFIX, compress.New(compress.Config{
|
||||||
srv.Use(PICTURES_PREFIX, helpers.StaticHandler(&picturesFS))
|
Level: compress.LevelBestSpeed,
|
||||||
|
}), etag.New(), helpers.StaticHandler(&picturesFS))
|
||||||
logging.Info("Serving newspaper pictures from pictures/ directory.")
|
logging.Info("Serving newspaper pictures from pictures/ directory.")
|
||||||
} else {
|
} else {
|
||||||
logging.Info("Pictures folder not found. Skipping picture serving.")
|
logging.Info("Pictures folder not found. Skipping picture serving.")
|
||||||
|
|||||||
127
scripts/README_IMAGE_COMPRESSION.md
Normal file
127
scripts/README_IMAGE_COMPRESSION.md
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# Image Compression System
|
||||||
|
|
||||||
|
This system provides automatic dual image loading for optimal performance:
|
||||||
|
|
||||||
|
- **Layout views**: Compressed WebP images for fast browsing
|
||||||
|
- **Single page viewer**: Full-quality JPEG images for detailed reading
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
pictures/
|
||||||
|
├── 1771-42-166.jpg # Original high-quality image
|
||||||
|
├── 1771-42-166-preview.webp # Compressed preview for layouts
|
||||||
|
├── 1771-42-167.jpg # Original high-quality image
|
||||||
|
├── 1771-42-167-preview.webp # Compressed preview for layouts
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Backend (Go)
|
||||||
|
- `ImageFile` struct includes both `Path` (original) and `PreviewPath` (compressed)
|
||||||
|
- Image registry automatically detects `-preview.webp` files during initialization
|
||||||
|
- Templates receive both paths for each image
|
||||||
|
|
||||||
|
### Frontend (Templates)
|
||||||
|
- Layout views use `<picture>` elements with WebP source and JPEG fallback
|
||||||
|
- Single page viewer uses `data-full-image` attribute to load full-quality images
|
||||||
|
- Automatic fallback to original image if preview doesn't exist
|
||||||
|
|
||||||
|
### Performance Benefits
|
||||||
|
- **60-80% smaller file sizes** for layout browsing
|
||||||
|
- **Faster page loads** with compressed images
|
||||||
|
- **Full quality** maintained for detailed viewing
|
||||||
|
- **Progressive enhancement** with WebP support detection
|
||||||
|
|
||||||
|
## Generating WebP Previews
|
||||||
|
|
||||||
|
### Automatic Generation
|
||||||
|
Run the provided script to convert all existing images:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/generate_webp_previews.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Generation
|
||||||
|
For individual files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cwebp -q 75 -m 6 pictures/1771-42-166.jpg -o pictures/1771-42-166-preview.webp
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quality Settings
|
||||||
|
- **Quality**: 75% (good balance for text-heavy images)
|
||||||
|
- **Compression**: Level 6 (maximum compression)
|
||||||
|
- **Format**: WebP (excellent text preservation)
|
||||||
|
|
||||||
|
## Browser Support
|
||||||
|
|
||||||
|
### WebP Support
|
||||||
|
- Chrome/Edge: ✅ Full support
|
||||||
|
- Firefox: ✅ Full support
|
||||||
|
- Safari: ✅ Full support (14+)
|
||||||
|
- Fallback: Automatic JPEG fallback for older browsers
|
||||||
|
|
||||||
|
### Picture Element
|
||||||
|
- Modern browsers: ✅ Optimal WebP loading
|
||||||
|
- Older browsers: ✅ Automatic JPEG fallback
|
||||||
|
- No JavaScript required
|
||||||
|
|
||||||
|
## File Size Comparison
|
||||||
|
|
||||||
|
Typical compression results for newspaper images:
|
||||||
|
|
||||||
|
| Image Type | Original JPEG | WebP Preview | Savings |
|
||||||
|
|------------|---------------|--------------|---------|
|
||||||
|
| Text page | 800 KB | 320 KB | 60% |
|
||||||
|
| Mixed page | 1.2 MB | 480 KB | 60% |
|
||||||
|
| Image page | 1.5 MB | 750 KB | 50% |
|
||||||
|
|
||||||
|
## Development Notes
|
||||||
|
|
||||||
|
### Template Usage
|
||||||
|
```html
|
||||||
|
<picture>
|
||||||
|
{{- if ne $page.PreviewPath "" -}}
|
||||||
|
<source srcset="{{ $page.PreviewPath }}" type="image/webp">
|
||||||
|
{{- end -}}
|
||||||
|
<img src="{{ if ne $page.PreviewPath "" }}{{ $page.PreviewPath }}{{ else }}{{ $page.ImagePath }}{{ end }}"
|
||||||
|
data-full-image="{{ $page.ImagePath }}"
|
||||||
|
alt="Page {{ $page.PageNumber }}" />
|
||||||
|
</picture>
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript Integration
|
||||||
|
```javascript
|
||||||
|
// Single page viewer automatically uses full-quality image
|
||||||
|
const fullImageSrc = imgElement.getAttribute('data-full-image') || imgElement.src;
|
||||||
|
viewer.show(fullImageSrc, ...);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fallback Strategy
|
||||||
|
1. **Missing preview**: Uses original JPEG
|
||||||
|
2. **WebP unsupported**: Browser loads JPEG fallback
|
||||||
|
3. **File not found**: Standard error handling
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
### Check Compression Status
|
||||||
|
```bash
|
||||||
|
# Count preview files
|
||||||
|
find pictures -name "*-preview.webp" | wc -l
|
||||||
|
|
||||||
|
# Compare total sizes
|
||||||
|
find pictures -name "*.jpg" -exec du -ch {} + | tail -1
|
||||||
|
find pictures -name "*-preview.webp" -exec du -ch {} + | tail -1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Regenerate Previews
|
||||||
|
```bash
|
||||||
|
# Regenerate all previews
|
||||||
|
./scripts/generate_webp_previews.sh
|
||||||
|
|
||||||
|
# Force regeneration (remove existing previews first)
|
||||||
|
find pictures -name "*-preview.webp" -delete
|
||||||
|
./scripts/generate_webp_previews.sh
|
||||||
|
```
|
||||||
147
scripts/generate_webp_originals.sh
Executable file
147
scripts/generate_webp_originals.sh
Executable file
@@ -0,0 +1,147 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script to generate high-quality WebP versions of original JPEG files
|
||||||
|
# These will be used for the single page viewer (enlarged view)
|
||||||
|
# Usage: ./scripts/generate_webp_originals.sh
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
QUALITY=95 # WebP quality (0-100) - very high for single page viewer
|
||||||
|
COMPRESSION=1 # WebP compression level (0-6, lower = less compression, higher quality)
|
||||||
|
PICTURES_DIR="pictures"
|
||||||
|
|
||||||
|
# Check if cwebp is installed
|
||||||
|
if ! command -v cwebp &> /dev/null; then
|
||||||
|
echo -e "${RED}Error: cwebp is not installed. Please install WebP tools:${NC}"
|
||||||
|
echo " Ubuntu/Debian: sudo apt-get install webp"
|
||||||
|
echo " macOS: brew install webp"
|
||||||
|
echo " CentOS/RHEL: sudo yum install libwebp-tools"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if pictures directory exists
|
||||||
|
if [ ! -d "$PICTURES_DIR" ]; then
|
||||||
|
echo -e "${RED}Error: Pictures directory '$PICTURES_DIR' not found${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${BLUE}Generating high-quality WebP originals for single page viewer...${NC}"
|
||||||
|
echo "Quality: $QUALITY% (near-lossless)"
|
||||||
|
echo "Compression: $COMPRESSION (minimal compression for maximum quality)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Counters
|
||||||
|
processed=0
|
||||||
|
skipped=0
|
||||||
|
errors=0
|
||||||
|
|
||||||
|
# Function to process a single file
|
||||||
|
process_file() {
|
||||||
|
local jpg_file="$1"
|
||||||
|
|
||||||
|
# Skip if already a preview file
|
||||||
|
if [[ "$jpg_file" =~ -preview\.(jpg|jpeg)$ ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate output filename
|
||||||
|
dir=$(dirname "$jpg_file")
|
||||||
|
filename=$(basename "$jpg_file")
|
||||||
|
name_no_ext="${filename%.*}"
|
||||||
|
webp_file="$dir/${name_no_ext}.webp"
|
||||||
|
|
||||||
|
# Skip if WebP original already exists and is newer than source
|
||||||
|
if [ -f "$webp_file" ] && [ "$webp_file" -nt "$jpg_file" ]; then
|
||||||
|
echo -e "${YELLOW}Skipping $jpg_file (WebP exists and is newer)${NC}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Convert to high-quality WebP
|
||||||
|
echo "Processing: $jpg_file -> $webp_file"
|
||||||
|
|
||||||
|
if cwebp -q "$QUALITY" -m "$COMPRESSION" -alpha_cleanup "$jpg_file" -o "$webp_file" 2>/dev/null; then
|
||||||
|
# Check file sizes
|
||||||
|
jpg_size=$(stat -f%z "$jpg_file" 2>/dev/null || stat -c%s "$jpg_file" 2>/dev/null)
|
||||||
|
webp_size=$(stat -f%z "$webp_file" 2>/dev/null || stat -c%s "$webp_file" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -n "$jpg_size" ] && [ -n "$webp_size" ]; then
|
||||||
|
if [ "$webp_size" -lt "$jpg_size" ]; then
|
||||||
|
reduction=$(( (jpg_size - webp_size) * 100 / jpg_size ))
|
||||||
|
echo -e "${GREEN} ✓ Success! Size reduction: ${reduction}%${NC}"
|
||||||
|
else
|
||||||
|
increase=$(( (webp_size - jpg_size) * 100 / jpg_size ))
|
||||||
|
echo -e "${GREEN} ✓ Success! Size increase: ${increase}% (expected for high quality)${NC}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${GREEN} ✓ Success!${NC}"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo -e "${RED} ✗ Failed to convert $jpg_file${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Export the function and variables for parallel execution
|
||||||
|
export -f process_file
|
||||||
|
export QUALITY COMPRESSION GREEN RED YELLOW BLUE NC
|
||||||
|
|
||||||
|
# Detect number of CPU cores for parallel processing
|
||||||
|
CPU_CORES=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)
|
||||||
|
PARALLEL_JOBS=$((CPU_CORES))
|
||||||
|
|
||||||
|
echo "Using $PARALLEL_JOBS parallel jobs (detected $CPU_CORES CPU cores)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Find all JPG files and process them in parallel
|
||||||
|
find "$PICTURES_DIR" -type f \( -name "*.jpg" -o -name "*.jpeg" \) | \
|
||||||
|
grep -v -E '\-preview\.(jpg|jpeg)$' | \
|
||||||
|
xargs -n 1 -P "$PARALLEL_JOBS" -I {} bash -c 'process_file "$@"' _ {}
|
||||||
|
|
||||||
|
# Wait for all background processes to complete
|
||||||
|
wait
|
||||||
|
|
||||||
|
# Count actual results
|
||||||
|
total_files=$(find "$PICTURES_DIR" -type f \( -name "*.jpg" -o -name "*.jpeg" \) | grep -v -E '\-preview\.(jpg|jpeg)$' | wc -l)
|
||||||
|
webp_files=$(find "$PICTURES_DIR" -type f -name "*.webp" ! -name "*-preview.webp" | wc -l)
|
||||||
|
processed=$webp_files
|
||||||
|
skipped=0
|
||||||
|
errors=$((total_files - processed))
|
||||||
|
|
||||||
|
# Final summary
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}=== Summary ===${NC}"
|
||||||
|
echo "Processed: $processed files"
|
||||||
|
echo "Skipped: $skipped files"
|
||||||
|
echo "Errors: $errors files"
|
||||||
|
|
||||||
|
if [ $errors -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}All conversions completed successfully!${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}Completed with $errors errors${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Information about file structure
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}=== File Structure ===${NC}"
|
||||||
|
echo "After running this script, you'll have:"
|
||||||
|
echo " original.jpg -> Original JPEG file (fallback)"
|
||||||
|
echo " original.webp -> High-quality WebP (single page viewer)"
|
||||||
|
echo " original-preview.webp -> Compressed WebP (layout views)"
|
||||||
|
echo ""
|
||||||
|
echo "The backend will prefer .webp files for the single page viewer,"
|
||||||
|
echo "falling back to .jpg if WebP is not available."
|
||||||
|
|
||||||
|
# Calculate total space impact (optional)
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}=== Space Analysis ===${NC}"
|
||||||
|
echo "To analyze space usage:"
|
||||||
|
echo " Original JPEGs: find $PICTURES_DIR -name '*.jpg' -exec du -ch {} + | tail -1"
|
||||||
|
echo " WebP originals: find $PICTURES_DIR -name '*.webp' ! -name '*-preview.webp' -exec du -ch {} + | tail -1"
|
||||||
|
echo " WebP previews: find $PICTURES_DIR -name '*-preview.webp' -exec du -ch {} + | tail -1"
|
||||||
127
scripts/generate_webp_previews.sh
Executable file
127
scripts/generate_webp_previews.sh
Executable file
@@ -0,0 +1,127 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script to generate WebP preview images from existing JPEG files
|
||||||
|
# Usage: ./scripts/generate_webp_previews.sh
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
QUALITY=75 # WebP quality (0-100)
|
||||||
|
COMPRESSION=6 # WebP compression level (0-6, higher = better compression)
|
||||||
|
PICTURES_DIR="pictures"
|
||||||
|
|
||||||
|
# Check if cwebp is installed
|
||||||
|
if ! command -v cwebp &> /dev/null; then
|
||||||
|
echo -e "${RED}Error: cwebp is not installed. Please install WebP tools:${NC}"
|
||||||
|
echo " Ubuntu/Debian: sudo apt-get install webp"
|
||||||
|
echo " macOS: brew install webp"
|
||||||
|
echo " CentOS/RHEL: sudo yum install libwebp-tools"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if pictures directory exists
|
||||||
|
if [ ! -d "$PICTURES_DIR" ]; then
|
||||||
|
echo -e "${RED}Error: Pictures directory '$PICTURES_DIR' not found${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}Generating WebP preview images...${NC}"
|
||||||
|
echo "Quality: $QUALITY%"
|
||||||
|
echo "Compression: $COMPRESSION"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Counters
|
||||||
|
processed=0
|
||||||
|
skipped=0
|
||||||
|
errors=0
|
||||||
|
|
||||||
|
# Function to process a single file
|
||||||
|
process_file() {
|
||||||
|
local jpg_file="$1"
|
||||||
|
|
||||||
|
# Skip if already a preview file
|
||||||
|
if [[ "$jpg_file" =~ -preview\.(jpg|jpeg)$ ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate output filename
|
||||||
|
dir=$(dirname "$jpg_file")
|
||||||
|
filename=$(basename "$jpg_file")
|
||||||
|
name_no_ext="${filename%.*}"
|
||||||
|
webp_file="$dir/${name_no_ext}-preview.webp"
|
||||||
|
|
||||||
|
# Skip if WebP preview already exists and is newer than source
|
||||||
|
if [ -f "$webp_file" ] && [ "$webp_file" -nt "$jpg_file" ]; then
|
||||||
|
echo -e "${YELLOW}Skipping $jpg_file (preview exists and is newer)${NC}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Convert to WebP
|
||||||
|
echo "Processing: $jpg_file -> $webp_file"
|
||||||
|
|
||||||
|
if cwebp -q "$QUALITY" -m "$COMPRESSION" "$jpg_file" -o "$webp_file" 2>/dev/null; then
|
||||||
|
# Check file sizes
|
||||||
|
jpg_size=$(stat -f%z "$jpg_file" 2>/dev/null || stat -c%s "$jpg_file" 2>/dev/null)
|
||||||
|
webp_size=$(stat -f%z "$webp_file" 2>/dev/null || stat -c%s "$webp_file" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -n "$jpg_size" ] && [ -n "$webp_size" ]; then
|
||||||
|
reduction=$(( (jpg_size - webp_size) * 100 / jpg_size ))
|
||||||
|
echo -e "${GREEN} ✓ Success! Size reduction: ${reduction}%${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${GREEN} ✓ Success!${NC}"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo -e "${RED} ✗ Failed to convert $jpg_file${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Export the function and variables for parallel execution
|
||||||
|
export -f process_file
|
||||||
|
export QUALITY COMPRESSION GREEN RED YELLOW NC
|
||||||
|
|
||||||
|
# Detect number of CPU cores for parallel processing
|
||||||
|
CPU_CORES=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)
|
||||||
|
PARALLEL_JOBS=$((CPU_CORES))
|
||||||
|
|
||||||
|
echo "Using $PARALLEL_JOBS parallel jobs (detected $CPU_CORES CPU cores)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Find all JPG files and process them in parallel
|
||||||
|
find "$PICTURES_DIR" -type f \( -name "*.jpg" -o -name "*.jpeg" \) | \
|
||||||
|
grep -v -E '\-preview\.(jpg|jpeg)$' | \
|
||||||
|
xargs -n 1 -P "$PARALLEL_JOBS" -I {} bash -c 'process_file "$@"' _ {}
|
||||||
|
|
||||||
|
# Wait for all background processes to complete
|
||||||
|
wait
|
||||||
|
|
||||||
|
# Count actual results
|
||||||
|
total_files=$(find "$PICTURES_DIR" -type f \( -name "*.jpg" -o -name "*.jpeg" \) | grep -v -E '\-preview\.(jpg|jpeg)$' | wc -l)
|
||||||
|
preview_files=$(find "$PICTURES_DIR" -type f -name "*-preview.webp" | wc -l)
|
||||||
|
processed=$preview_files
|
||||||
|
skipped=0
|
||||||
|
errors=$((total_files - processed))
|
||||||
|
|
||||||
|
# Final summary
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}=== Summary ===${NC}"
|
||||||
|
echo "Processed: $processed files"
|
||||||
|
echo "Skipped: $skipped files"
|
||||||
|
echo "Errors: $errors files"
|
||||||
|
|
||||||
|
if [ $errors -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}All conversions completed successfully!${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}Completed with $errors errors${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Calculate total space saved (optional)
|
||||||
|
echo ""
|
||||||
|
echo "To see space savings, run:"
|
||||||
|
echo " find $PICTURES_DIR -name '*-preview.webp' -exec du -ch {} + | tail -1"
|
||||||
|
echo " find $PICTURES_DIR -name '*.jpg' -exec du -ch {} + | tail -1"
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/gofiber/fiber/v2/middleware/cache"
|
"github.com/gofiber/fiber/v2/middleware/cache"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/compress"
|
||||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||||
"github.com/gofiber/storage/memory/v2"
|
"github.com/gofiber/storage/memory/v2"
|
||||||
@@ -139,6 +141,18 @@ func (s *Server) Start() {
|
|||||||
|
|
||||||
srv.Use(recover.New())
|
srv.Use(recover.New())
|
||||||
|
|
||||||
|
// Add compression middleware for HTML responses (non-static routes)
|
||||||
|
srv.Use(compress.New(compress.Config{
|
||||||
|
Level: compress.LevelBestSpeed, // Fast compression for HTML responses
|
||||||
|
Next: func(c *fiber.Ctx) bool {
|
||||||
|
// Only compress for routes that don't start with static prefixes
|
||||||
|
path := c.Path()
|
||||||
|
return strings.HasPrefix(path, "/assets") ||
|
||||||
|
strings.HasPrefix(path, "/static/pictures/") ||
|
||||||
|
strings.HasPrefix(path, "/img/")
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
// INFO: No caching middleware in debug mode to avoid cache issues during development
|
// INFO: No caching middleware in debug mode to avoid cache issues during development
|
||||||
// We cant do it with cach busting the files via ?v=XXX, since we also cache the templates.
|
// We cant do it with cach busting the files via ?v=XXX, since we also cache the templates.
|
||||||
// TODO: Dont cache static assets, bc storage gets huge on images.
|
// TODO: Dont cache static assets, bc storage gets huge on images.
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/Theodor-Springmann-Stiftung/kgpz_web/helpers/templatefunctions"
|
"github.com/Theodor-Springmann-Stiftung/kgpz_web/helpers/templatefunctions"
|
||||||
"github.com/Theodor-Springmann-Stiftung/kgpz_web/views"
|
"github.com/Theodor-Springmann-Stiftung/kgpz_web/views"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/compress"
|
||||||
"github.com/gofiber/fiber/v2/middleware/etag"
|
"github.com/gofiber/fiber/v2/middleware/etag"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -408,8 +409,9 @@ func (e *Engine) funcs() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e Engine) Pre(srv *fiber.App) error {
|
func (e Engine) Pre(srv *fiber.App) error {
|
||||||
srv.Use(ASSETS_URL_PREFIX, etag.New())
|
srv.Use(ASSETS_URL_PREFIX, compress.New(compress.Config{
|
||||||
srv.Use(ASSETS_URL_PREFIX, helpers.StaticHandler(&views.StaticFS))
|
Level: compress.LevelBestSpeed,
|
||||||
|
}), etag.New(), helpers.StaticHandler(&views.StaticFS))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,14 +41,16 @@ type IndividualPiecesByPage struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type IssuePage struct {
|
type IssuePage struct {
|
||||||
PageNumber int
|
PageNumber int
|
||||||
ImagePath string
|
ImagePath string // Full-quality image path (prefers WebP over JPEG)
|
||||||
Available bool
|
PreviewPath string // Compressed WebP path for layout views
|
||||||
GridColumn int // 1 or 2 for left/right positioning
|
JpegPath string // JPEG path for download button
|
||||||
GridRow int // Row number in grid
|
Available bool
|
||||||
HasHeader bool // Whether this page has a double-spread header
|
GridColumn int // 1 or 2 for left/right positioning
|
||||||
HeaderText string // Text for double-spread header
|
GridRow int // Row number in grid
|
||||||
PageIcon string // Icon type: "first", "last", "even", "odd"
|
HasHeader bool // Whether this page has a double-spread header
|
||||||
|
HeaderText string // Text for double-spread header
|
||||||
|
PageIcon string // Icon type: "first", "last", "even", "odd"
|
||||||
}
|
}
|
||||||
|
|
||||||
type IssueImages struct {
|
type IssueImages struct {
|
||||||
@@ -58,13 +60,15 @@ type IssueImages struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ImageFile struct {
|
type ImageFile struct {
|
||||||
Year int
|
Year int
|
||||||
Issue int
|
Issue int
|
||||||
Page int
|
Page int
|
||||||
IsBeilage bool
|
IsBeilage bool
|
||||||
BeilageNo int
|
BeilageNo int
|
||||||
Filename string
|
Filename string
|
||||||
Path string
|
Path string // Primary path (prefers WebP over JPEG)
|
||||||
|
PreviewPath string // Path to compressed WebP version for layout views
|
||||||
|
JpegPath string // Path to JPEG version (for download button)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageRegistry struct {
|
type ImageRegistry struct {
|
||||||
@@ -518,16 +522,30 @@ func LoadIssueImages(issue xmlmodels.Issue) (IssueImages, error) {
|
|||||||
|
|
||||||
if foundFile != nil {
|
if foundFile != nil {
|
||||||
images.HasImages = true
|
images.HasImages = true
|
||||||
|
// Use preview path if available, otherwise fallback to original
|
||||||
|
previewPath := foundFile.PreviewPath
|
||||||
|
if previewPath == "" {
|
||||||
|
previewPath = foundFile.Path
|
||||||
|
}
|
||||||
|
// Use JPEG path if available, otherwise fallback to primary
|
||||||
|
jpegPath := foundFile.JpegPath
|
||||||
|
if jpegPath == "" {
|
||||||
|
jpegPath = foundFile.Path
|
||||||
|
}
|
||||||
images.MainPages = append(images.MainPages, IssuePage{
|
images.MainPages = append(images.MainPages, IssuePage{
|
||||||
PageNumber: page,
|
PageNumber: page,
|
||||||
ImagePath: foundFile.Path,
|
ImagePath: foundFile.Path,
|
||||||
Available: true,
|
PreviewPath: previewPath,
|
||||||
|
JpegPath: jpegPath,
|
||||||
|
Available: true,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
images.MainPages = append(images.MainPages, IssuePage{
|
images.MainPages = append(images.MainPages, IssuePage{
|
||||||
PageNumber: page,
|
PageNumber: page,
|
||||||
ImagePath: "",
|
ImagePath: "",
|
||||||
Available: false,
|
PreviewPath: "",
|
||||||
|
JpegPath: "",
|
||||||
|
Available: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -539,10 +557,22 @@ func LoadIssueImages(issue xmlmodels.Issue) (IssueImages, error) {
|
|||||||
// Add ALL beilage files found for this issue
|
// Add ALL beilage files found for this issue
|
||||||
for _, file := range beilageFiles {
|
for _, file := range beilageFiles {
|
||||||
images.HasImages = true
|
images.HasImages = true
|
||||||
|
// Use preview path if available, otherwise fallback to original
|
||||||
|
previewPath := file.PreviewPath
|
||||||
|
if previewPath == "" {
|
||||||
|
previewPath = file.Path
|
||||||
|
}
|
||||||
|
// Use JPEG path if available, otherwise fallback to primary
|
||||||
|
jpegPath := file.JpegPath
|
||||||
|
if jpegPath == "" {
|
||||||
|
jpegPath = file.Path
|
||||||
|
}
|
||||||
beilagePages = append(beilagePages, IssuePage{
|
beilagePages = append(beilagePages, IssuePage{
|
||||||
PageNumber: file.Page,
|
PageNumber: file.Page,
|
||||||
ImagePath: file.Path,
|
ImagePath: file.Path,
|
||||||
Available: true,
|
PreviewPath: previewPath,
|
||||||
|
JpegPath: jpegPath,
|
||||||
|
Available: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -573,7 +603,10 @@ func initImageRegistry() error {
|
|||||||
ByYearPage: make(map[string]ImageFile),
|
ByYearPage: make(map[string]ImageFile),
|
||||||
}
|
}
|
||||||
|
|
||||||
return filepath.Walk("pictures", func(path string, info os.FileInfo, err error) error {
|
// Temporary map to collect all files by their base name (year-issue-page)
|
||||||
|
tempFiles := make(map[string]*ImageFile)
|
||||||
|
|
||||||
|
err := filepath.Walk("pictures", func(path string, info os.FileInfo, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -583,14 +616,22 @@ func initImageRegistry() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
filename := info.Name()
|
filename := info.Name()
|
||||||
|
filenamelower := strings.ToLower(filename)
|
||||||
|
|
||||||
// Skip non-jpg files
|
// Only process .jpg and .webp files (but skip preview files)
|
||||||
if !strings.HasSuffix(strings.ToLower(filename), ".jpg") {
|
var nameWithoutExt string
|
||||||
return nil
|
var isWebP bool
|
||||||
|
|
||||||
|
if strings.HasSuffix(filenamelower, ".jpg") {
|
||||||
|
nameWithoutExt = strings.TrimSuffix(filename, ".jpg")
|
||||||
|
isWebP = false
|
||||||
|
} else if strings.HasSuffix(filenamelower, ".webp") && !strings.HasSuffix(filenamelower, "-preview.webp") {
|
||||||
|
nameWithoutExt = strings.TrimSuffix(filename, ".webp")
|
||||||
|
isWebP = true
|
||||||
|
} else {
|
||||||
|
return nil // Skip non-image files and preview files
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove .jpg extension and split by -
|
|
||||||
nameWithoutExt := strings.TrimSuffix(filename, ".jpg")
|
|
||||||
parts := strings.Split(nameWithoutExt, "-")
|
parts := strings.Split(nameWithoutExt, "-")
|
||||||
|
|
||||||
// Need at least 3 parts: year-issue-page
|
// Need at least 3 parts: year-issue-page
|
||||||
@@ -624,26 +665,87 @@ func initImageRegistry() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
imageFile := ImageFile{
|
// Create unique key for this image (handles both regular and beilage)
|
||||||
Year: year,
|
var uniqueKey string
|
||||||
Issue: issue,
|
if isBeilage {
|
||||||
Page: page,
|
uniqueKey = fmt.Sprintf("%d-%db-%d", year, issue, page)
|
||||||
IsBeilage: isBeilage,
|
} else {
|
||||||
BeilageNo: 1, // Default beilage number
|
uniqueKey = fmt.Sprintf("%d-%d-%d", year, issue, page)
|
||||||
Filename: filename,
|
|
||||||
Path: fmt.Sprintf("/static/pictures/%s", path[9:]), // Remove "pictures/" prefix
|
|
||||||
}
|
}
|
||||||
|
|
||||||
imageRegistry.Files = append(imageRegistry.Files, imageFile)
|
// Get or create the ImageFile entry
|
||||||
|
imageFile, exists := tempFiles[uniqueKey]
|
||||||
|
if !exists {
|
||||||
|
imageFile = &ImageFile{
|
||||||
|
Year: year,
|
||||||
|
Issue: issue,
|
||||||
|
Page: page,
|
||||||
|
IsBeilage: isBeilage,
|
||||||
|
BeilageNo: 1, // Default beilage number
|
||||||
|
}
|
||||||
|
tempFiles[uniqueKey] = imageFile
|
||||||
|
}
|
||||||
|
|
||||||
yearIssueKey := fmt.Sprintf("%d-%d", year, issue)
|
// Set paths based on file type
|
||||||
imageRegistry.ByYearIssue[yearIssueKey] = append(imageRegistry.ByYearIssue[yearIssueKey], imageFile)
|
currentPath := fmt.Sprintf("/static/pictures/%s", path[9:]) // Remove "pictures/" prefix
|
||||||
|
if isWebP {
|
||||||
if !isBeilage {
|
// WebP is the primary path for single page viewer
|
||||||
yearPageKey := fmt.Sprintf("%d-%d", year, page)
|
imageFile.Path = currentPath
|
||||||
imageRegistry.ByYearPage[yearPageKey] = imageFile
|
imageFile.Filename = filename
|
||||||
|
} else {
|
||||||
|
// JPEG is the fallback path for download
|
||||||
|
imageFile.JpegPath = currentPath
|
||||||
|
// If no WebP path is set yet, use JPEG as primary
|
||||||
|
if imageFile.Path == "" {
|
||||||
|
imageFile.Path = currentPath
|
||||||
|
imageFile.Filename = filename
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second pass: set PreviewPath for each ImageFile by checking for preview files
|
||||||
|
for _, imageFile := range tempFiles {
|
||||||
|
// Extract the base name from the filename to preserve original format
|
||||||
|
baseNameWithExt := imageFile.Filename
|
||||||
|
var baseName string
|
||||||
|
|
||||||
|
// Remove extension to get base name
|
||||||
|
if strings.HasSuffix(strings.ToLower(baseNameWithExt), ".webp") {
|
||||||
|
baseName = strings.TrimSuffix(baseNameWithExt, ".webp")
|
||||||
|
} else if strings.HasSuffix(strings.ToLower(baseNameWithExt), ".jpg") {
|
||||||
|
baseName = strings.TrimSuffix(baseNameWithExt, ".jpg")
|
||||||
|
} else {
|
||||||
|
baseName = baseNameWithExt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate preview filename using the original base name format
|
||||||
|
previewFilename := baseName + "-preview.webp"
|
||||||
|
|
||||||
|
// Check if preview file exists
|
||||||
|
previewFullPath := filepath.Join("pictures", fmt.Sprintf("%d", imageFile.Year), previewFilename)
|
||||||
|
if _, err := os.Stat(previewFullPath); err == nil {
|
||||||
|
imageFile.PreviewPath = fmt.Sprintf("/static/pictures/%d/%s", imageFile.Year, previewFilename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert temp map to final registry structures
|
||||||
|
for _, imageFile := range tempFiles {
|
||||||
|
imageRegistry.Files = append(imageRegistry.Files, *imageFile)
|
||||||
|
|
||||||
|
yearIssueKey := fmt.Sprintf("%d-%d", imageFile.Year, imageFile.Issue)
|
||||||
|
imageRegistry.ByYearIssue[yearIssueKey] = append(imageRegistry.ByYearIssue[yearIssueKey], *imageFile)
|
||||||
|
|
||||||
|
if !imageFile.IsBeilage {
|
||||||
|
yearPageKey := fmt.Sprintf("%d-%d", imageFile.Year, imageFile.Page)
|
||||||
|
imageRegistry.ByYearPage[yearPageKey] = *imageFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ type PiecePageEntry struct {
|
|||||||
PageNumber int
|
PageNumber int
|
||||||
IssueYear int
|
IssueYear int
|
||||||
IssueNumber int
|
IssueNumber int
|
||||||
ImagePath string
|
ImagePath string // Full-quality image path (prefers WebP over JPEG)
|
||||||
|
PreviewPath string // Compressed WebP path for layout views
|
||||||
|
JpegPath string // JPEG path for download button
|
||||||
IsContinuation bool
|
IsContinuation bool
|
||||||
IssueContext string // "1764 Nr. 37" for display
|
IssueContext string // "1764 Nr. 37" for display
|
||||||
Available bool
|
Available bool
|
||||||
@@ -96,8 +98,8 @@ func NewPieceView(piece xmlmodels.Piece, lib *xmlmodels.Library) (*PieceVM, erro
|
|||||||
PartNumber: partIndex + 1, // Part number (1-based)
|
PartNumber: partIndex + 1, // Part number (1-based)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get actual image path from registry
|
// Get actual image path, preview path, and JPEG path from registry
|
||||||
pageEntry.ImagePath = getImagePathFromRegistryWithBeilage(issueRef.When.Year, issueRef.Nr, pageNum, issueRef.Beilage > 0)
|
pageEntry.ImagePath, pageEntry.PreviewPath, pageEntry.JpegPath = getImagePathsFromRegistryWithBeilage(issueRef.When.Year, issueRef.Nr, pageNum, issueRef.Beilage > 0)
|
||||||
|
|
||||||
pvm.AllPages = append(pvm.AllPages, pageEntry)
|
pvm.AllPages = append(pvm.AllPages, pageEntry)
|
||||||
}
|
}
|
||||||
@@ -140,10 +142,12 @@ func (pvm *PieceVM) loadImages() error {
|
|||||||
for i, pageEntry := range pvm.AllPages {
|
for i, pageEntry := range pvm.AllPages {
|
||||||
// Create IssuePage for template compatibility
|
// Create IssuePage for template compatibility
|
||||||
issuePage := IssuePage{
|
issuePage := IssuePage{
|
||||||
PageNumber: pageEntry.PageNumber,
|
PageNumber: pageEntry.PageNumber,
|
||||||
ImagePath: pageEntry.ImagePath,
|
ImagePath: pageEntry.ImagePath,
|
||||||
Available: true, // Assume available for now
|
PreviewPath: pageEntry.PreviewPath,
|
||||||
PageIcon: "single", // Simplified icon for piece view
|
JpegPath: pageEntry.JpegPath,
|
||||||
|
Available: true, // Assume available for now
|
||||||
|
PageIcon: "single", // Simplified icon for piece view
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if image actually exists using the registry
|
// Check if image actually exists using the registry
|
||||||
@@ -231,7 +235,56 @@ func getImagePathFromRegistry(year, page int) string {
|
|||||||
return fmt.Sprintf("/static/pictures/%d/seite_%d.jpg", year, page)
|
return fmt.Sprintf("/static/pictures/%d/seite_%d.jpg", year, page)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getImagePathsFromRegistryWithBeilage gets image paths: primary (WebP preferred), preview (compressed), and JPEG, handling both regular and Beilage pages
|
||||||
|
func getImagePathsFromRegistryWithBeilage(year, issue, page int, isBeilage bool) (string, string, string) {
|
||||||
|
// Initialize registry if needed
|
||||||
|
if err := initImageRegistry(); err != nil {
|
||||||
|
return "", "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// For regular pages, use the year-page lookup
|
||||||
|
if !isBeilage {
|
||||||
|
key := fmt.Sprintf("%d-%d", year, page)
|
||||||
|
if imageFile, exists := imageRegistry.ByYearPage[key]; exists {
|
||||||
|
previewPath := imageFile.PreviewPath
|
||||||
|
if previewPath == "" {
|
||||||
|
previewPath = imageFile.Path // Fallback to original if no preview
|
||||||
|
}
|
||||||
|
jpegPath := imageFile.JpegPath
|
||||||
|
if jpegPath == "" {
|
||||||
|
jpegPath = imageFile.Path // Fallback to primary if no separate JPEG
|
||||||
|
}
|
||||||
|
return imageFile.Path, previewPath, jpegPath
|
||||||
|
}
|
||||||
|
// Fallback for regular pages
|
||||||
|
fallbackPath := fmt.Sprintf("/static/pictures/%d/seite_%d.jpg", year, page)
|
||||||
|
return fallbackPath, fallbackPath, fallbackPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Beilage pages, search through all files for this year-issue
|
||||||
|
yearIssueKey := fmt.Sprintf("%d-%d", year, issue)
|
||||||
|
if issueFiles, exists := imageRegistry.ByYearIssue[yearIssueKey]; exists {
|
||||||
|
for _, file := range issueFiles {
|
||||||
|
if file.IsBeilage && file.Page == page {
|
||||||
|
previewPath := file.PreviewPath
|
||||||
|
if previewPath == "" {
|
||||||
|
previewPath = file.Path // Fallback to original if no preview
|
||||||
|
}
|
||||||
|
jpegPath := file.JpegPath
|
||||||
|
if jpegPath == "" {
|
||||||
|
jpegPath = file.Path // Fallback to primary if no separate JPEG
|
||||||
|
}
|
||||||
|
return file.Path, previewPath, jpegPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No file found
|
||||||
|
return "", "", ""
|
||||||
|
}
|
||||||
|
|
||||||
// getImagePathFromRegistryWithBeilage gets the actual image path, handling both regular and Beilage pages
|
// getImagePathFromRegistryWithBeilage gets the actual image path, handling both regular and Beilage pages
|
||||||
|
// Kept for backward compatibility
|
||||||
func getImagePathFromRegistryWithBeilage(year, issue, page int, isBeilage bool) string {
|
func getImagePathFromRegistryWithBeilage(year, issue, page int, isBeilage bool) string {
|
||||||
// Initialize registry if needed
|
// Initialize registry if needed
|
||||||
if err := initImageRegistry(); err != nil {
|
if err := initImageRegistry(); err != nil {
|
||||||
|
|||||||
@@ -603,7 +603,7 @@ class W extends HTMLElement {
|
|||||||
document.documentElement.offsetHeight
|
document.documentElement.offsetHeight
|
||||||
), s = window.innerHeight, o = n - s, r = o > 0 ? window.scrollY / o : 0, l = t.clientHeight, u = t.scrollHeight - l;
|
), s = window.innerHeight, o = n - s, r = o > 0 ? window.scrollY / o : 0, l = t.clientHeight, u = t.scrollHeight - l;
|
||||||
if (u > 0) {
|
if (u > 0) {
|
||||||
const d = r * u, h = i.getBoundingClientRect(), g = t.getBoundingClientRect(), p = h.top - g.top + t.scrollTop, m = l / 2, f = p - m, w = 0.7, b = w * d + (1 - w) * f, x = Math.max(0, Math.min(u, b)), E = t.scrollTop;
|
const d = r * u, h = i.getBoundingClientRect(), g = t.getBoundingClientRect(), p = h.top - g.top + t.scrollTop, f = l / 2, m = p - f, w = 0.7, b = w * d + (1 - w) * m, x = Math.max(0, Math.min(u, b)), E = t.scrollTop;
|
||||||
Math.abs(x - E) > 10 && t.scrollTo({
|
Math.abs(x - E) > 10 && t.scrollTo({
|
||||||
top: x,
|
top: x,
|
||||||
behavior: "smooth"
|
behavior: "smooth"
|
||||||
@@ -813,10 +813,10 @@ class Y extends HTMLElement {
|
|||||||
const s = document.createElementNS("http://www.w3.org/2000/svg", "stop");
|
const s = document.createElementNS("http://www.w3.org/2000/svg", "stop");
|
||||||
s.setAttribute("offset", "100%"), s.setAttribute("stop-color", "#e53e3e"), i.appendChild(n), i.appendChild(s), t.appendChild(i), e.appendChild(t), this.pointsContainer.appendChild(e);
|
s.setAttribute("offset", "100%"), s.setAttribute("stop-color", "#e53e3e"), i.appendChild(n), i.appendChild(s), t.appendChild(i), e.appendChild(t), this.pointsContainer.appendChild(e);
|
||||||
const o = { xmin: 2555e3, ymin: 135e4, xmax: 7405e3, ymax: 55e5 }, r = { lon: 10, lat: 52 }, l = (u, d) => {
|
const o = { xmin: 2555e3, ymin: 135e4, xmax: 7405e3, ymax: 55e5 }, r = { lon: 10, lat: 52 }, l = (u, d) => {
|
||||||
const m = r.lon * Math.PI / 180, f = r.lat * Math.PI / 180, w = d * Math.PI / 180, b = u * Math.PI / 180, x = Math.sqrt(
|
const f = r.lon * Math.PI / 180, m = r.lat * Math.PI / 180, w = d * Math.PI / 180, b = u * Math.PI / 180, x = Math.sqrt(
|
||||||
2 / (1 + Math.sin(f) * Math.sin(b) + Math.cos(f) * Math.cos(b) * Math.cos(w - m))
|
2 / (1 + Math.sin(m) * Math.sin(b) + Math.cos(m) * Math.cos(b) * Math.cos(w - f))
|
||||||
), E = 6371e3 * x * Math.cos(b) * Math.sin(w - m), S = 6371e3 * x * (Math.cos(f) * Math.sin(b) - Math.sin(f) * Math.cos(b) * Math.cos(w - m)), C = E + 4321e3, k = S + 321e4, L = o.xmax - o.xmin, v = o.ymax - o.ymin, R = (C - o.xmin) / L * 100, $ = (o.ymax - k) / v * 100;
|
), E = 6371e3 * x * Math.cos(b) * Math.sin(w - f), S = 6371e3 * x * (Math.cos(m) * Math.sin(b) - Math.sin(m) * Math.cos(b) * Math.cos(w - f)), C = E + 4321e3, k = S + 321e4, L = o.xmax - o.xmin, v = o.ymax - o.ymin, $ = (C - o.xmin) / L * 100, R = (o.ymax - k) / v * 100;
|
||||||
return { x: R, y: $ };
|
return { x: $, y: R };
|
||||||
}, c = [];
|
}, c = [];
|
||||||
this.places.forEach((u) => {
|
this.places.forEach((u) => {
|
||||||
if (u.lat && u.lng) {
|
if (u.lat && u.lng) {
|
||||||
@@ -829,8 +829,8 @@ class Y extends HTMLElement {
|
|||||||
}), p.addEventListener("mouseleave", () => {
|
}), p.addEventListener("mouseleave", () => {
|
||||||
p.getAttribute("fill") === "#f87171" && (p.setAttribute("r", "0.4"), p.setAttribute("fill", "white"), p.setAttribute("opacity", "0.7"));
|
p.getAttribute("fill") === "#f87171" && (p.setAttribute("r", "0.4"), p.setAttribute("fill", "white"), p.setAttribute("opacity", "0.7"));
|
||||||
});
|
});
|
||||||
const m = `${u.name}${u.toponymName && u.toponymName !== u.name ? ` (${u.toponymName})` : ""}`;
|
const f = `${u.name}${u.toponymName && u.toponymName !== u.name ? ` (${u.toponymName})` : ""}`;
|
||||||
p.dataset.placeId = u.id, p.dataset.tooltipText = m, p.addEventListener("mouseenter", (f) => this.showTooltip(f)), p.addEventListener("mouseleave", () => this.hideTooltip()), p.addEventListener("mousemove", (f) => this.updateTooltipPosition(f)), p.addEventListener("click", (f) => this.scrollToPlace(f)), e.appendChild(p), this.mapPoints.set(u.id, p);
|
p.dataset.placeId = u.id, p.dataset.tooltipText = f, p.addEventListener("mouseenter", (m) => this.showTooltip(m)), p.addEventListener("mouseleave", () => this.hideTooltip()), p.addEventListener("mousemove", (m) => this.updateTooltipPosition(m)), p.addEventListener("click", (m) => this.scrollToPlace(m)), e.appendChild(p), this.mapPoints.set(u.id, p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}), c.length > 0 && this.autoZoomToPoints(c);
|
}), c.length > 0 && this.autoZoomToPoints(c);
|
||||||
@@ -841,14 +841,14 @@ class Y extends HTMLElement {
|
|||||||
e.forEach((v) => {
|
e.forEach((v) => {
|
||||||
v.x < t && (t = v.x), v.x > i && (i = v.x), v.y < n && (n = v.y), v.y > s && (s = v.y);
|
v.x < t && (t = v.x), v.x > i && (i = v.x), v.y < n && (n = v.y), v.y > s && (s = v.y);
|
||||||
});
|
});
|
||||||
const o = 0.06, r = i - t, l = s - n, c = r * o, u = l * o, d = Math.max(0, t - c), h = Math.min(100, i + c), g = Math.max(0, n - u), p = Math.min(100, s + u), m = h - d, f = p - g, w = 5 / 7, b = m / f;
|
const o = 0.06, r = i - t, l = s - n, c = r * o, u = l * o, d = Math.max(0, t - c), h = Math.min(100, i + c), g = Math.max(0, n - u), p = Math.min(100, s + u), f = h - d, m = p - g, w = 5 / 7, b = f / m;
|
||||||
let x = { x: d, y: g, width: m, height: f };
|
let x = { x: d, y: g, width: f, height: m };
|
||||||
if (b > w) {
|
if (b > w) {
|
||||||
const v = m / w;
|
const v = f / w;
|
||||||
x.y = g - (v - f) / 2, x.height = v;
|
x.y = g - (v - m) / 2, x.height = v;
|
||||||
} else {
|
} else {
|
||||||
const v = f * w;
|
const v = m * w;
|
||||||
x.x = d - (v - m) / 2, x.width = v;
|
x.x = d - (v - f) / 2, x.width = v;
|
||||||
}
|
}
|
||||||
const E = 100 / x.width, S = -x.x, C = -x.y, k = `scale(${E}) translate(${S}%, ${C}%)`, L = this.querySelector(".transform-wrapper");
|
const E = 100 / x.width, S = -x.x, C = -x.y, k = `scale(${E}) translate(${S}%, ${C}%)`, L = this.querySelector(".transform-wrapper");
|
||||||
L && (L.style.transform = k);
|
L && (L.style.transform = k);
|
||||||
@@ -990,9 +990,9 @@ class Z extends HTMLElement {
|
|||||||
if (!this.place || !this.place.lat || !this.place.lng || !this.pointsContainer)
|
if (!this.place || !this.place.lat || !this.place.lng || !this.pointsContainer)
|
||||||
return;
|
return;
|
||||||
const e = { xmin: 2555e3, ymin: 135e4, xmax: 7405e3, ymax: 55e5 }, t = { lon: 10, lat: 52 }, i = (r, l) => {
|
const e = { xmin: 2555e3, ymin: 135e4, xmax: 7405e3, ymax: 55e5 }, t = { lon: 10, lat: 52 }, i = (r, l) => {
|
||||||
const h = t.lon * Math.PI / 180, g = t.lat * Math.PI / 180, p = l * Math.PI / 180, m = r * Math.PI / 180, f = Math.sqrt(
|
const h = t.lon * Math.PI / 180, g = t.lat * Math.PI / 180, p = l * Math.PI / 180, f = r * Math.PI / 180, m = Math.sqrt(
|
||||||
2 / (1 + Math.sin(g) * Math.sin(m) + Math.cos(g) * Math.cos(m) * Math.cos(p - h))
|
2 / (1 + Math.sin(g) * Math.sin(f) + Math.cos(g) * Math.cos(f) * Math.cos(p - h))
|
||||||
), w = 6371e3 * f * Math.cos(m) * Math.sin(p - h), b = 6371e3 * f * (Math.cos(g) * Math.sin(m) - Math.sin(g) * Math.cos(m) * Math.cos(p - h)), x = w + 4321e3, E = b + 321e4, S = e.xmax - e.xmin, C = e.ymax - e.ymin, k = (x - e.xmin) / S * 100, L = (e.ymax - E) / C * 100;
|
), w = 6371e3 * m * Math.cos(f) * Math.sin(p - h), b = 6371e3 * m * (Math.cos(g) * Math.sin(f) - Math.sin(g) * Math.cos(f) * Math.cos(p - h)), x = w + 4321e3, E = b + 321e4, S = e.xmax - e.xmin, C = e.ymax - e.ymin, k = (x - e.xmin) / S * 100, L = (e.ymax - E) / C * 100;
|
||||||
return { x: k, y: L };
|
return { x: k, y: L };
|
||||||
}, n = parseFloat(this.place.lat), s = parseFloat(this.place.lng), o = i(n, s);
|
}, n = parseFloat(this.place.lat), s = parseFloat(this.place.lng), o = i(n, s);
|
||||||
if (o.x >= 0 && o.x <= 100 && o.y >= 0 && o.y <= 100) {
|
if (o.x >= 0 && o.x <= 100 && o.y >= 0 && o.y <= 100) {
|
||||||
@@ -1031,8 +1031,8 @@ class Z extends HTMLElement {
|
|||||||
const b = (h - d.height) / 2;
|
const b = (h - d.height) / 2;
|
||||||
d.y = Math.max(0, d.y - b), d.height = Math.min(h, 100 - d.y);
|
d.y = Math.max(0, d.y - b), d.height = Math.min(h, 100 - d.y);
|
||||||
}
|
}
|
||||||
const g = 100 / d.width, p = -d.x, m = -d.y, f = `scale(${g}) translate(${p}%, ${m}%)`, w = this.querySelector(".transform-wrapper");
|
const g = 100 / d.width, p = -d.x, f = -d.y, m = `scale(${g}) translate(${p}%, ${f}%)`, w = this.querySelector(".transform-wrapper");
|
||||||
w && (w.style.transform = f);
|
w && (w.style.transform = m);
|
||||||
}
|
}
|
||||||
showTooltip(e) {
|
showTooltip(e) {
|
||||||
const i = e.target.dataset.tooltipText;
|
const i = e.target.dataset.tooltipText;
|
||||||
@@ -1117,7 +1117,7 @@ class X extends HTMLElement {
|
|||||||
customElements.define("generic-filter", X);
|
customElements.define("generic-filter", X);
|
||||||
class J extends HTMLElement {
|
class J extends HTMLElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(), this.resizeObserver = null;
|
super(), this.resizeObserver = null, this.hasStartedPreloading = !1;
|
||||||
}
|
}
|
||||||
// Dynamically detect sidebar width in pixels
|
// Dynamically detect sidebar width in pixels
|
||||||
detectSidebarWidth() {
|
detectSidebarWidth() {
|
||||||
@@ -1255,8 +1255,8 @@ class J extends HTMLElement {
|
|||||||
u.style.position = "relative";
|
u.style.position = "relative";
|
||||||
const p = u.querySelector(".target-page-dot");
|
const p = u.querySelector(".target-page-dot");
|
||||||
p && p.remove();
|
p && p.remove();
|
||||||
const m = document.createElement("span");
|
const f = document.createElement("span");
|
||||||
m.className = "target-page-dot absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full z-10", m.title = "verlinkte Seite", u.appendChild(m);
|
f.className = "target-page-dot absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full z-10", f.title = "verlinkte Seite", u.appendChild(f);
|
||||||
}
|
}
|
||||||
r ? r === "part-number" && o !== null ? d.innerHTML = `<span class="part-number bg-slate-100 text-slate-800 font-bold px-1.5 py-0.5 rounded border border-slate-400 flex items-center justify-center">${o}. Teil</span>` : d.innerHTML = this.generateIconFromType(r) : d.innerHTML = this.generateFallbackIcon(i, n, o), this.updateNavigationButtons(), this.style.display = "block", this.setAttribute("active", "true");
|
r ? r === "part-number" && o !== null ? d.innerHTML = `<span class="part-number bg-slate-100 text-slate-800 font-bold px-1.5 py-0.5 rounded border border-slate-400 flex items-center justify-center">${o}. Teil</span>` : d.innerHTML = this.generateIconFromType(r) : d.innerHTML = this.generateFallbackIcon(i, n, o), this.updateNavigationButtons(), this.style.display = "block", this.setAttribute("active", "true");
|
||||||
const g = this.querySelector(".flex-1.overflow-auto");
|
const g = this.querySelector(".flex-1.overflow-auto");
|
||||||
@@ -1264,7 +1264,7 @@ class J extends HTMLElement {
|
|||||||
new CustomEvent("singlepageviewer:opened", {
|
new CustomEvent("singlepageviewer:opened", {
|
||||||
detail: { pageNumber: this.currentPageNumber, isBeilage: this.currentIsBeilage }
|
detail: { pageNumber: this.currentPageNumber, isBeilage: this.currentIsBeilage }
|
||||||
})
|
})
|
||||||
);
|
), this.startImagePreloading();
|
||||||
}
|
}
|
||||||
close() {
|
close() {
|
||||||
this.style.display = "none", this.removeAttribute("active"), document.body.style.overflow = "", document.dispatchEvent(
|
this.style.display = "none", this.removeAttribute("active"), document.body.style.overflow = "", document.dispatchEvent(
|
||||||
@@ -1467,6 +1467,52 @@ class J extends HTMLElement {
|
|||||||
}
|
}
|
||||||
return "KGPZ";
|
return "KGPZ";
|
||||||
}
|
}
|
||||||
|
// Start preloading all high-quality images in the background
|
||||||
|
startImagePreloading() {
|
||||||
|
if (this.hasStartedPreloading)
|
||||||
|
return;
|
||||||
|
this.hasStartedPreloading = !0;
|
||||||
|
const e = this.collectAllImageUrls();
|
||||||
|
if (e.length === 0) {
|
||||||
|
console.log("No images to preload");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`Starting background preload of ${e.length} high-quality images`), setTimeout(() => {
|
||||||
|
this.preloadImages(e);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
// Collect all high-quality image URLs from the current page
|
||||||
|
collectAllImageUrls() {
|
||||||
|
const e = [];
|
||||||
|
return document.querySelectorAll(".newspaper-page-image, .piece-page-image").forEach((i) => {
|
||||||
|
const n = i.getAttribute("data-full-image");
|
||||||
|
n && !e.includes(n) && e.push(n);
|
||||||
|
}), e;
|
||||||
|
}
|
||||||
|
// Preload images with throttling to avoid overwhelming the browser
|
||||||
|
preloadImages(e) {
|
||||||
|
let i = 0, n = 0;
|
||||||
|
const s = /* @__PURE__ */ new Set(), o = () => {
|
||||||
|
for (; n < 3 && i < e.length; ) {
|
||||||
|
const r = e[i];
|
||||||
|
i++, !s.has(r) && (n++, this.preloadSingleImage(r).then(() => {
|
||||||
|
s.add(r), console.log(`Preloaded: ${r} (${s.size}/${e.length})`);
|
||||||
|
}).catch((l) => {
|
||||||
|
console.warn(`Failed to preload: ${r}`, l);
|
||||||
|
}).finally(() => {
|
||||||
|
n--, i < e.length || n > 0 ? setTimeout(o, 100) : console.log(`Preloading complete: ${s.size}/${e.length} images loaded`);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
o();
|
||||||
|
}
|
||||||
|
// Preload a single image
|
||||||
|
preloadSingleImage(e) {
|
||||||
|
return new Promise((t, i) => {
|
||||||
|
const n = new Image();
|
||||||
|
n.onload = () => t(n), n.onerror = () => i(new Error(`Failed to load ${e}`)), n.src = e;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
customElements.define("single-page-viewer", J);
|
customElements.define("single-page-viewer", J);
|
||||||
document.body.addEventListener("htmx:beforeRequest", function(a) {
|
document.body.addEventListener("htmx:beforeRequest", function(a) {
|
||||||
@@ -1710,13 +1756,14 @@ function te(a, e, t, i = null) {
|
|||||||
let l = null, c = null;
|
let l = null, c = null;
|
||||||
if (r) {
|
if (r) {
|
||||||
l = r.getAttribute("data-page-icon-type"), r.querySelector(".part-number") && (l = "part-number");
|
l = r.getAttribute("data-page-icon-type"), r.querySelector(".part-number") && (l = "part-number");
|
||||||
const d = r.querySelector(".page-indicator");
|
const h = r.querySelector(".page-indicator");
|
||||||
if (d) {
|
if (h) {
|
||||||
const h = d.cloneNode(!0);
|
const g = h.cloneNode(!0);
|
||||||
h.querySelectorAll("i").forEach((m) => m.remove()), h.querySelectorAll('[class*="target-page-dot"], .target-page-indicator').forEach((m) => m.remove()), c = h.textContent.trim();
|
g.querySelectorAll("i").forEach((m) => m.remove()), g.querySelectorAll('[class*="target-page-dot"], .target-page-indicator').forEach((m) => m.remove()), c = g.textContent.trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
n.show(a.src, a.alt, e, s, o, i, l, c);
|
const u = a.getAttribute("data-full-image") || a.src;
|
||||||
|
n.show(u, a.alt, e, s, o, i, l, c);
|
||||||
}
|
}
|
||||||
function H() {
|
function H() {
|
||||||
document.getElementById("pageModal").classList.add("hidden");
|
document.getElementById("pageModal").classList.add("hidden");
|
||||||
@@ -1981,7 +2028,7 @@ function q() {
|
|||||||
a.key === "Escape" && H();
|
a.key === "Escape" && H();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function B() {
|
function N() {
|
||||||
const a = window.location.pathname, e = document.querySelector("main .grid.grid-cols-1.lg\\:grid-cols-3") !== null, t = document.querySelector("main h3 u") !== null;
|
const a = window.location.pathname, e = document.querySelector("main .grid.grid-cols-1.lg\\:grid-cols-3") !== null, t = document.querySelector("main h3 u") !== null;
|
||||||
if (a.includes("/search") || a.includes("/suche") || e || t) {
|
if (a.includes("/search") || a.includes("/suche") || e || t) {
|
||||||
document.querySelectorAll(".citation-link[data-citation-url]").forEach((o) => {
|
document.querySelectorAll(".citation-link[data-citation-url]").forEach((o) => {
|
||||||
@@ -2004,7 +2051,7 @@ function B() {
|
|||||||
r ? (s.classList.add("text-red-700", "pointer-events-none"), s.setAttribute("aria-current", "page")) : (s.classList.remove("text-red-700", "pointer-events-none"), s.removeAttribute("aria-current"));
|
r ? (s.classList.add("text-red-700", "pointer-events-none"), s.setAttribute("aria-current", "page")) : (s.classList.remove("text-red-700", "pointer-events-none"), s.removeAttribute("aria-current"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function N() {
|
function B() {
|
||||||
const a = window.location.pathname, e = document.body;
|
const a = window.location.pathname, e = document.body;
|
||||||
e.classList.remove(
|
e.classList.remove(
|
||||||
"page-akteure",
|
"page-akteure",
|
||||||
@@ -2025,11 +2072,11 @@ window.shareCurrentPage = re;
|
|||||||
window.generateCitation = ae;
|
window.generateCitation = ae;
|
||||||
window.copyPagePermalink = le;
|
window.copyPagePermalink = le;
|
||||||
window.generatePageCitation = ce;
|
window.generatePageCitation = ce;
|
||||||
N();
|
|
||||||
B();
|
B();
|
||||||
|
N();
|
||||||
document.querySelector(".newspaper-page-container") && q();
|
document.querySelector(".newspaper-page-container") && q();
|
||||||
let de = function(a) {
|
let de = function(a) {
|
||||||
N(), B(), I(), setTimeout(() => {
|
B(), N(), I(), setTimeout(() => {
|
||||||
document.querySelector(".newspaper-page-container") && q();
|
document.querySelector(".newspaper-page-container") && q();
|
||||||
}, 50);
|
}, 50);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -155,7 +155,17 @@
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
<div class="single-page bg-white p-4 rounded border {{ $borderColor }} {{ $hoverColor }} transition-colors duration-200">
|
<div class="single-page bg-white p-4 rounded border {{ $borderColor }} {{ $hoverColor }} transition-colors duration-200">
|
||||||
<img src="{{ $page.ImagePath }}" alt="{{ if $isBeilage }}Beilage 1, {{ end }}Seite {{ $page.PageNumber }}" class="newspaper-page-image cursor-zoom-in rounded-sm hover:scale-[1.02] transition-transform duration-200" onclick="enlargePage(this, {{ $page.PageNumber }}, false)" data-page="{{ $page.PageNumber }}" loading="lazy" />
|
<picture>
|
||||||
|
{{- if ne $page.PreviewPath "" -}}
|
||||||
|
<source srcset="{{ $page.PreviewPath }}" type="image/webp">
|
||||||
|
{{- end -}}
|
||||||
|
<img src="{{ if ne $page.PreviewPath "" }}{{ $page.PreviewPath }}{{ else }}{{ $page.ImagePath }}{{ end }}"
|
||||||
|
alt="{{ if $isBeilage }}Beilage 1, {{ end }}Seite {{ $page.PageNumber }}"
|
||||||
|
class="newspaper-page-image cursor-zoom-in rounded-sm hover:scale-[1.02] transition-transform duration-200"
|
||||||
|
onclick="enlargePage(this, {{ $page.PageNumber }}, false)"
|
||||||
|
data-page="{{ $page.PageNumber }}"
|
||||||
|
data-full-image="{{ $page.ImagePath }}" />
|
||||||
|
</picture>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
@@ -36,12 +36,17 @@
|
|||||||
<!-- Page image -->
|
<!-- Page image -->
|
||||||
<div class="single-page bg-white p-4 rounded border border-slate-200 hover:border-slate-300 transition-colors duration-200">
|
<div class="single-page bg-white p-4 rounded border border-slate-200 hover:border-slate-300 transition-colors duration-200">
|
||||||
{{ if $page.Available }}
|
{{ if $page.Available }}
|
||||||
<img src="{{ $page.ImagePath }}"
|
<picture>
|
||||||
alt="Seite {{ $page.PageNumber }} ({{ $issueContext }})"
|
{{- if ne $page.PreviewPath "" -}}
|
||||||
class="piece-page-image newspaper-page-image cursor-zoom-in rounded-sm hover:scale-[1.02] transition-transform duration-200 w-full"
|
<source srcset="{{ $page.PreviewPath }}" type="image/webp">
|
||||||
onclick="enlargePage(this, {{ $page.PageNumber }}, false, {{ $pageEntry.PartNumber }})"
|
{{- end -}}
|
||||||
data-page="{{ $page.PageNumber }}"
|
<img src="{{ if ne $page.PreviewPath "" }}{{ $page.PreviewPath }}{{ else }}{{ $page.ImagePath }}{{ end }}"
|
||||||
loading="lazy" />
|
alt="Seite {{ $page.PageNumber }} ({{ $issueContext }})"
|
||||||
|
class="piece-page-image newspaper-page-image cursor-zoom-in rounded-sm hover:scale-[1.02] transition-transform duration-200 w-full"
|
||||||
|
onclick="enlargePage(this, {{ $page.PageNumber }}, false, {{ $pageEntry.PartNumber }})"
|
||||||
|
data-page="{{ $page.PageNumber }}"
|
||||||
|
data-full-image="{{ $page.ImagePath }}" />
|
||||||
|
</picture>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<div class="bg-slate-100 border border-slate-200 rounded p-8 text-center">
|
<div class="bg-slate-100 border border-slate-200 rounded p-8 text-center">
|
||||||
<i class="ri-image-line text-4xl text-slate-400 mb-2 block"></i>
|
<i class="ri-image-line text-4xl text-slate-400 mb-2 block"></i>
|
||||||
|
|||||||
@@ -53,8 +53,11 @@ export function enlargePage(imgElement, pageNumber, isFromSpread, partNumber = n
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use full-quality image if available, otherwise use current src
|
||||||
|
const fullImageSrc = imgElement.getAttribute('data-full-image') || imgElement.src;
|
||||||
|
|
||||||
// Show the page in the viewer with extracted data
|
// Show the page in the viewer with extracted data
|
||||||
viewer.show(imgElement.src, imgElement.alt, pageNumber, isBeilage, targetPage, partNumber, extractedIconType, extractedHeading);
|
viewer.show(fullImageSrc, imgElement.alt, pageNumber, isBeilage, targetPage, partNumber, extractedIconType, extractedHeading);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function closeModal() {
|
export function closeModal() {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export class SinglePageViewer extends HTMLElement {
|
|||||||
super();
|
super();
|
||||||
// No shadow DOM - use regular DOM to allow Tailwind CSS
|
// No shadow DOM - use regular DOM to allow Tailwind CSS
|
||||||
this.resizeObserver = null;
|
this.resizeObserver = null;
|
||||||
|
this.hasStartedPreloading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dynamically detect sidebar width in pixels
|
// Dynamically detect sidebar width in pixels
|
||||||
@@ -260,6 +261,9 @@ export class SinglePageViewer extends HTMLElement {
|
|||||||
detail: { pageNumber: this.currentPageNumber, isBeilage: this.currentIsBeilage },
|
detail: { pageNumber: this.currentPageNumber, isBeilage: this.currentIsBeilage },
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Start preloading all high-quality images in the background
|
||||||
|
this.startImagePreloading();
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
@@ -674,6 +678,105 @@ export class SinglePageViewer extends HTMLElement {
|
|||||||
// Ultimate fallback
|
// Ultimate fallback
|
||||||
return "KGPZ";
|
return "KGPZ";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start preloading all high-quality images in the background
|
||||||
|
startImagePreloading() {
|
||||||
|
// Only preload once per session
|
||||||
|
if (this.hasStartedPreloading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.hasStartedPreloading = true;
|
||||||
|
|
||||||
|
// Collect all high-quality image URLs from the current page
|
||||||
|
const imageUrls = this.collectAllImageUrls();
|
||||||
|
|
||||||
|
if (imageUrls.length === 0) {
|
||||||
|
console.log("No images to preload");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Starting background preload of ${imageUrls.length} high-quality images`);
|
||||||
|
|
||||||
|
// Start preloading with a slight delay to not interfere with the current image load
|
||||||
|
setTimeout(() => {
|
||||||
|
this.preloadImages(imageUrls);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect all high-quality image URLs from the current page
|
||||||
|
collectAllImageUrls() {
|
||||||
|
const imageUrls = [];
|
||||||
|
|
||||||
|
// Find all newspaper page images (both main pages and beilage)
|
||||||
|
const pageImages = document.querySelectorAll('.newspaper-page-image, .piece-page-image');
|
||||||
|
|
||||||
|
pageImages.forEach(img => {
|
||||||
|
// Get the high-quality image URL from data-full-image attribute
|
||||||
|
const fullImageUrl = img.getAttribute('data-full-image');
|
||||||
|
if (fullImageUrl && !imageUrls.includes(fullImageUrl)) {
|
||||||
|
imageUrls.push(fullImageUrl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return imageUrls;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preload images with throttling to avoid overwhelming the browser
|
||||||
|
preloadImages(imageUrls) {
|
||||||
|
const CONCURRENT_LOADS = 3; // Maximum concurrent image loads
|
||||||
|
const DELAY_BETWEEN_BATCHES = 1000; // 1 second between batches
|
||||||
|
|
||||||
|
let currentIndex = 0;
|
||||||
|
let activeLoads = 0;
|
||||||
|
const preloadedImages = new Set();
|
||||||
|
|
||||||
|
const loadNextBatch = () => {
|
||||||
|
while (activeLoads < CONCURRENT_LOADS && currentIndex < imageUrls.length) {
|
||||||
|
const imageUrl = imageUrls[currentIndex];
|
||||||
|
currentIndex++;
|
||||||
|
|
||||||
|
// Skip if already preloaded
|
||||||
|
if (preloadedImages.has(imageUrl)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeLoads++;
|
||||||
|
this.preloadSingleImage(imageUrl)
|
||||||
|
.then(() => {
|
||||||
|
preloadedImages.add(imageUrl);
|
||||||
|
console.log(`Preloaded: ${imageUrl} (${preloadedImages.size}/${imageUrls.length})`);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn(`Failed to preload: ${imageUrl}`, error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
activeLoads--;
|
||||||
|
// Continue loading if more images remain
|
||||||
|
if (currentIndex < imageUrls.length || activeLoads > 0) {
|
||||||
|
setTimeout(loadNextBatch, 100);
|
||||||
|
} else {
|
||||||
|
console.log(`Preloading complete: ${preloadedImages.size}/${imageUrls.length} images loaded`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start the first batch
|
||||||
|
loadNextBatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preload a single image
|
||||||
|
preloadSingleImage(imageUrl) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = new Image();
|
||||||
|
|
||||||
|
img.onload = () => resolve(img);
|
||||||
|
img.onerror = () => reject(new Error(`Failed to load ${imageUrl}`));
|
||||||
|
|
||||||
|
// Set src to start loading
|
||||||
|
img.src = imageUrl;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the web component
|
// Register the web component
|
||||||
|
|||||||
Reference in New Issue
Block a user