Added rotation, moved select all, added link after download
This commit is contained in:
+2
-1
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user