diff --git a/bricktracker/app.py b/bricktracker/app.py index d993d99..5f57bd8 100644 --- a/bricktracker/app.py +++ b/bricktracker/app.py @@ -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, diff --git a/bricktracker/instructions.py b/bricktracker/instructions.py index e0c4521..0b640ce 100644 --- a/bricktracker/instructions.py +++ b/bricktracker/instructions.py @@ -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 - Open PDF' ) # 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()}) - Open PDF' ) except Exception as e: diff --git a/bricktracker/peeron_instructions.py b/bricktracker/peeron_instructions.py index 2a3e302..e4c2e35 100644 --- a/bricktracker/peeron_instructions.py +++ b/bricktracker/peeron_instructions.py @@ -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 diff --git a/bricktracker/peeron_pdf.py b/bricktracker/peeron_pdf.py index 75d5d08..19e249b 100644 --- a/bricktracker/peeron_pdf.py +++ b/bricktracker/peeron_pdf.py @@ -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 - Open PDF' ) # 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 - Open PDF' ) 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: diff --git a/bricktracker/socket.py b/bricktracker/socket.py index fd64389..873f066 100644 --- a/bricktracker/socket.py +++ b/bricktracker/socket.py @@ -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) diff --git a/templates/instructions/peeron_socket.html b/templates/instructions/peeron_socket.html index 6fecd0c..136ca38 100644 --- a/templates/instructions/peeron_socket.html +++ b/templates/instructions/peeron_socket.html @@ -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 = ' 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 ? - ' Select All' : - ' Deselect All'; - }); - this.html_button.parentNode.insertBefore(selectAllButton, this.html_button); + // Update button text and icon + if (allChecked) { + selectAllButton.innerHTML = ' Select All'; + } else { + selectAllButton.innerHTML = ' 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(); diff --git a/templates/peeron_select.html b/templates/peeron_select.html index 9ee8657..6245188 100644 --- a/templates/peeron_select.html +++ b/templates/peeron_select.html @@ -35,7 +35,12 @@