Added rotation, moved select all, added link after download

This commit is contained in:
2025-09-25 20:47:41 +02:00
parent 787624c432
commit 74fe14f09b
7 changed files with 174 additions and 44 deletions
+2 -1
View File
@@ -60,7 +60,8 @@ def setup_app(app: Flask) -> None:
# Setup the login manager
LoginManager(app)
# I don't know :-)
# Configure proxy header handling for reverse proxy deployments (nginx, Apache, etc.)
# This ensures proper client IP detection and HTTPS scheme recognition
app.wsgi_app = ProxyFix(
app.wsgi_app,
x_for=1,
+4 -2
View File
@@ -101,8 +101,9 @@ class BrickInstructions(object):
# Skip if we already have it
if os.path.isfile(target):
pdf_url = self.url()
return self.socket.complete(
message=f"File {self.filename} already exists, skipped"
message=f'File {self.filename} already exists, skipped - <a href="{pdf_url}" target="_blank" class="btn btn-sm btn-primary ms-2"><i class="ri-external-link-line"></i> Open PDF</a>'
)
# Fetch PDF via cloudscraper (to bypass Cloudflare)
@@ -141,8 +142,9 @@ class BrickInstructions(object):
# Done!
logger.info(f"Downloaded {self.filename}")
pdf_url = self.url()
self.socket.complete(
message=f"File {self.filename} downloaded ({self.human_size()})"
message=f'File {self.filename} downloaded ({self.human_size()}) - <a href="{pdf_url}" target="_blank" class="btn btn-sm btn-primary ms-2"><i class="ri-external-link-line"></i> Open PDF</a>'
)
except Exception as e:
+1
View File
@@ -63,6 +63,7 @@ class PeeronPage(NamedTuple):
thumbnail_url: str
image_url: str
alt_text: str
rotation: int = 0 # Rotation in degrees (0, 90, 180, 270)
# Peeron instruction scraper
+53 -23
View File
@@ -56,8 +56,11 @@ class PeeronPDF(object):
# Skip if we already have it
if os.path.isfile(target_path):
# Create BrickInstructions instance to get PDF URL
instructions = BrickInstructions(self.filename)
pdf_url = instructions.url()
return self.socket.complete(
message=f"File {self.filename} already exists, skipped"
message=f'File {self.filename} already exists, skipped - <a href="{pdf_url}" target="_blank" class="btn btn-sm btn-primary ms-2"><i class="ri-external-link-line"></i> Open PDF</a>'
)
# Set up progress tracking
@@ -78,8 +81,8 @@ class PeeronPDF(object):
except Exception as e:
logger.warning(f"Failed to visit main page: {e}")
# Download images to temporary files
temp_files = []
# Download images to temporary files with rotation info
temp_files_with_rotation = []
failed_pages = []
try:
@@ -91,11 +94,11 @@ class PeeronPDF(object):
temp_file = self._download_page_image(page, i + 1, scraper)
if temp_file:
temp_files.append(temp_file)
temp_files_with_rotation.append((temp_file, page.rotation))
else:
failed_pages.append(page.page_number)
if not temp_files:
if not temp_files_with_rotation:
# Collect detailed error information
error_msg = f"Failed to download any instruction pages for set {self.set_number}-{self.version_number}."
@@ -116,25 +119,30 @@ class PeeronPDF(object):
raise DownloadException(error_msg)
elif len(temp_files) < total_pages:
elif len(temp_files_with_rotation) < total_pages:
# Partial success
error_msg = f"Only downloaded {len(temp_files)}/{total_pages} pages successfully."
error_msg = f"Only downloaded {len(temp_files_with_rotation)}/{total_pages} pages successfully."
if failed_pages:
error_msg += f" Failed pages: {', '.join(failed_pages)}."
logger.warning(error_msg)
# Create PDF from downloaded images
self._create_pdf_from_images(temp_files, target_path)
# Create PDF from downloaded images with rotation
self._create_pdf_from_images(temp_files_with_rotation, target_path)
# Success
logger.info(f"Created PDF {self.filename} with {len(temp_files)} pages")
logger.info(f"Created PDF {self.filename} with {len(temp_files_with_rotation)} pages")
# Create BrickInstructions instance to get PDF URL
instructions = BrickInstructions(self.filename)
pdf_url = instructions.url()
self.socket.complete(
message=f"PDF {self.filename} created with {len(temp_files)} pages"
message=f'PDF {self.filename} created with {len(temp_files_with_rotation)} pages - <a href="{pdf_url}" target="_blank" class="btn btn-sm btn-primary ms-2"><i class="ri-external-link-line"></i> Open PDF</a>'
)
finally:
# Cleanup temporary files
for temp_file in temp_files:
for temp_file, _ in temp_files_with_rotation:
try:
os.remove(temp_file)
except Exception as e:
@@ -219,8 +227,8 @@ class PeeronPDF(object):
return None
# Create PDF from downloaded images
def _create_pdf_from_images(self, image_paths: list[str], output_path: str, /) -> None:
"""Create a PDF from a list of image files"""
def _create_pdf_from_images(self, image_paths_and_rotations: list[tuple[str, int]], output_path: str, /) -> None:
"""Create a PDF from a list of image files with their rotations"""
try:
# Import FPDF (should be available from requirements)
from fpdf import FPDF
@@ -229,22 +237,44 @@ class PeeronPDF(object):
pdf = FPDF()
for i, img_path in enumerate(image_paths):
for i, (img_path, rotation) in enumerate(image_paths_and_rotations):
try:
# Open image to get dimensions
# Open image and apply rotation if needed
with Image.open(img_path) as image:
# Apply rotation if specified
if rotation != 0:
# PIL rotation is counter-clockwise, so we negate for clockwise rotation
image = image.rotate(-rotation, expand=True)
width, height = image.size
# Add page with image dimensions (convert pixels to mm)
# 1 pixel = 0.264583 mm (assuming 96 DPI)
page_width = width * 0.264583
page_height = height * 0.264583
# Add page with image dimensions (convert pixels to mm)
# 1 pixel = 0.264583 mm (assuming 96 DPI)
page_width = width * 0.264583
page_height = height * 0.264583
pdf.add_page(format=(page_width, page_height))
pdf.image(img_path, x=0, y=0, w=page_width, h=page_height)
pdf.add_page(format=(page_width, page_height))
# Save rotated image to temporary file for FPDF
temp_rotated_path = None
if rotation != 0:
import tempfile
temp_fd, temp_rotated_path = tempfile.mkstemp(suffix='.jpg', prefix=f'peeron_rotated_{i}_')
try:
os.close(temp_fd) # Close file descriptor, we'll use the path
image.save(temp_rotated_path, 'JPEG', quality=95)
pdf.image(temp_rotated_path, x=0, y=0, w=page_width, h=page_height)
finally:
# Clean up rotated temp file
if temp_rotated_path and os.path.exists(temp_rotated_path):
os.remove(temp_rotated_path)
else:
pdf.image(img_path, x=0, y=0, w=page_width, h=page_height)
# Update progress
progress_msg = f"Processing page {i + 1}/{len(image_paths)} into PDF"
progress_msg = f"Processing page {i + 1}/{len(image_paths_and_rotations)} into PDF"
if rotation != 0:
progress_msg += f" (rotated {rotation}°)"
self.socket.progress(message=progress_msg)
except Exception as e:
+2 -1
View File
@@ -144,7 +144,8 @@ class BrickSocket(object):
page_number=page_data.get('page_number', ''),
thumbnail_url=page_data.get('thumbnail_url', ''),
image_url=page_data.get('image_url', ''),
alt_text=page_data.get('alt_text', '')
alt_text=page_data.get('alt_text', ''),
rotation=page_data.get('rotation', 0)
)
pages.append(page)
+50 -14
View File
@@ -22,32 +22,67 @@ window.addEventListener('load', () => {
});
}
// Add select all button
this.add_select_all_button();
// Setup select all button (now in template)
this.setup_select_all_button();
// Setup rotation buttons
this.setup_rotation_buttons();
// Setup the socket
this.setup();
}
add_select_all_button() {
if (this.html_button) {
const selectAllButton = document.createElement('button');
selectAllButton.type = 'button';
selectAllButton.className = 'btn btn-sm btn-outline-secondary me-2';
selectAllButton.innerHTML = '<i class="ri-checkbox-multiple-line"></i> Select All';
setup_select_all_button() {
const selectAllButton = document.getElementById('peeron-select-all');
if (selectAllButton) {
selectAllButton.addEventListener('click', () => {
const checkboxes = this.get_files();
const allChecked = checkboxes.every(cb => cb.checked);
checkboxes.forEach(cb => cb.checked = !allChecked);
selectAllButton.innerHTML = allChecked ?
'<i class="ri-checkbox-multiple-line"></i> Select All' :
'<i class="ri-checkbox-blank-line"></i> Deselect All';
});
this.html_button.parentNode.insertBefore(selectAllButton, this.html_button);
// Update button text and icon
if (allChecked) {
selectAllButton.innerHTML = '<i class="ri-checkbox-multiple-line"></i> Select All';
} else {
selectAllButton.innerHTML = '<i class="ri-checkbox-blank-line"></i> Deselect All';
}
});
}
}
setup_rotation_buttons() {
document.querySelectorAll('.peeron-rotate-btn').forEach(button => {
button.addEventListener('click', (e) => {
e.preventDefault(); // Prevent label click
e.stopPropagation(); // Stop event bubbling
const targetId = button.getAttribute('data-target');
const checkboxId = button.getAttribute('data-checkbox');
const targetImg = document.getElementById(targetId);
const checkbox = document.getElementById(checkboxId);
if (targetImg && checkbox) {
let currentRotation = parseInt(button.getAttribute('data-rotation') || '0');
currentRotation = (currentRotation + 90) % 360;
// Update image rotation
targetImg.style.transform = `rotate(${currentRotation}deg)`;
button.setAttribute('data-rotation', currentRotation.toString());
// Store rotation in checkbox data for later use
checkbox.setAttribute('data-rotation', currentRotation.toString());
// Update the rotation icon to indicate current state
const icon = button.querySelector('i');
if (icon) {
// Rotate the icon to match the image rotation
icon.style.transform = `rotate(${currentRotation}deg)`;
}
}
});
});
}
complete(data) {
super.complete(data);
@@ -100,7 +135,8 @@ window.addEventListener('load', () => {
page_number: checkbox.getAttribute('data-page-number'),
thumbnail_url: checkbox.getAttribute('data-thumbnail-url'),
image_url: checkbox.getAttribute('data-image-url'),
alt_text: checkbox.getAttribute('data-alt-text')
alt_text: checkbox.getAttribute('data-alt-text'),
rotation: parseInt(checkbox.getAttribute('data-rotation') || '0')
}));
this.clear();
+62 -3
View File
@@ -35,7 +35,12 @@
<div class="mb-3">
<div id="peeron-download-fail" class="alert alert-danger d-none" role="alert"></div>
<div id="peeron-download-complete"></div>
<h5 class="border-bottom">Available Instructions</h5>
<div class="d-flex justify-content-between align-items-center border-bottom mb-3">
<h5 class="mb-0">Available Instructions</h5>
<button id="peeron-select-all" type="button" class="btn btn-sm btn-outline-secondary">
<i class="ri-checkbox-multiple-line"></i> Select All
</button>
</div>
<div id="peeron-download-files" class="row g-2">
{% for page in pages %}
<div class="col-12 col-md-6 col-lg-4">
@@ -47,11 +52,22 @@
data-thumbnail-url="{{ page.thumbnail_url }}"
data-image-url="{{ page.image_url }}"
data-alt-text="{{ page.alt_text }}"
data-rotation="0"
autocomplete="off">
<label class="form-check-label w-100" for="peeron-page-{{ loop.index }}">
<div class="text-center">
<img src="{{ page.thumbnail_url }}" alt="{{ page.alt_text }}" class="img-fluid mb-2 border rounded" style="max-height: 150px;">
<div class="text-center position-relative">
<div class="position-relative d-inline-block">
<img id="peeron-img-{{ loop.index }}" src="{{ page.thumbnail_url }}" alt="{{ page.alt_text }}"
class="img-fluid mb-2 border rounded" style="max-height: 150px; transform: rotate(0deg); transition: transform 0.3s ease;"
data-dims-target="peeron-dims-{{ loop.index }}">
<button type="button" class="btn btn-sm btn-light position-absolute top-0 end-0 p-1 me-1 mt-1 peeron-rotate-btn"
data-target="peeron-img-{{ loop.index }}" data-checkbox="peeron-page-{{ loop.index }}" data-rotation="0"
title="Rotate page" style="font-size: 0.7rem; line-height: 1;">
<i class="ri-refresh-line"></i>
</button>
</div>
<div class="small fw-bold">Page {{ page.page_number }}</div>
<div id="peeron-dims-{{ loop.index }}" class="small text-muted">Loading dimensions...</div>
</div>
</label>
</div>
@@ -85,4 +101,47 @@
</div>
</div>
</div>
<script>
// Function to show image dimensions
function showImageDimensions(img, dimElementId) {
const dimElement = document.getElementById(dimElementId);
if (dimElement && img.naturalWidth && img.naturalHeight) {
dimElement.textContent = `${img.naturalWidth} × ${img.naturalHeight} px`;
dimElement.classList.remove('text-muted');
dimElement.classList.add('text-info');
} else if (dimElement) {
dimElement.textContent = 'Dimensions unavailable';
}
}
// Initialize dimensions for all images when page loads
document.addEventListener('DOMContentLoaded', function() {
// Wait for all images to load and then show dimensions
const images = document.querySelectorAll('img[data-dims-target]');
images.forEach(img => {
const targetId = img.getAttribute('data-dims-target');
// Check if already loaded
if (img.complete && img.naturalWidth && img.naturalHeight) {
showImageDimensions(img, targetId);
} else {
// Wait for load
img.addEventListener('load', function() {
showImageDimensions(this, targetId);
});
// Handle error case
img.addEventListener('error', function() {
const dimElement = document.getElementById(targetId);
if (dimElement) {
dimElement.textContent = 'Failed to load';
dimElement.classList.remove('text-muted');
dimElement.classList.add('text-danger');
}
});
}
});
});
</script>
{% endblock %}