diff --git a/api/woocommerce_api.py b/api/woocommerce_api.py index 28bcb43..e88973d 100644 --- a/api/woocommerce_api.py +++ b/api/woocommerce_api.py @@ -1,24 +1,43 @@ -""" -Module for WooCommerce API interactions and image processing. -""" - import json import os import base64 import tempfile -import pprint -from tkinter import messagebox -from cryptography.fernet import Fernet import requests +from tkinter import messagebox from woocommerce import API +from cryptography.fernet import Fernet from utils.image_processing import ImageProcessor from config.encrypt_config import ConfigEncryptor from utils.file_operations import FileProcessor +import hashlib +import pprint CREDENTIALS_FILE = "credentials.json" # Hardcoded key (replace with your generated key) KEY = b"u4xTBY5Ns4WYdLvqMjEr138mpMmDEhhqTszKCcDy2cI=" +def save_active_credential_set(active_set_name): + """ + Update the active credential set in the saved credentials file. + + Args: + active_set_name (str): The name of the active credential set. + """ + if not os.path.exists(CREDENTIALS_FILE): + return + + with open(CREDENTIALS_FILE, 'r+') as file: + data = json.load(file) + + # Find the credential set and mark it as active + for cred in data.get('credentials', []): + cred['active'] = (cred['name'] == active_set_name) + + # Rewrite the updated data back to the file + file.seek(0) + json.dump(data, file, indent=4) + file.truncate() + def save_credentials(url, consumer_key, consumer_secret, username, password): """ @@ -39,7 +58,7 @@ def save_credentials(url, consumer_key, consumer_secret, username, password): "password": password, } - ConfigEncryptor(KEY).save_credentials(credentials) + ConfigEncryptor(KEY).save_credentials(consumer_key, consumer_secret, username, password) def load_credentials(): @@ -49,37 +68,30 @@ def load_credentials(): Returns: dict: The decrypted credentials, or None if the file does not exist. """ - if not os.path.exists("config.enc"): - return None - fernet = Fernet(KEY) - with open("config.enc", "rb") as file: - encrypted = file.read() - decrypted = fernet.decrypt(encrypted).decode() - return json.loads(decrypted).get("credentials") + creds = ConfigEncryptor(KEY).load_credentials() + return creds def get_wcapi(): """ - Get a WooCommerce API client instance. + Get a WooCommerce API client instance using the active credentials. Returns: woocommerce.API: The WooCommerce API client instance, or None if credentials are missing. """ - credentials = load_credentials() - if not credentials: - messagebox.showerror( - "Error", - "No WooCommerce credentials found. Please set them in the settings.", - ) - return None + active_credentials = load_credentials() + + pprint.pprint(active_credentials) + return API( - url=credentials["url"], - consumer_key=credentials["consumer_key"], - consumer_secret=credentials["consumer_secret"], + url=active_credentials["url"], + consumer_key=active_credentials["consumer_key"], + consumer_secret=active_credentials["consumer_secret"], version="wc/v3", ) + def get_product(product_id): """ Get a WooCommerce product and download its images. @@ -95,8 +107,13 @@ def get_product(product_id): return None result = wcapi.get(f"products/{product_id}") - image_paths = {} + product = result.json() + + return product + +def get_images(product, limit = 0): + image_paths = {} if product.get("images"): images = product.get("images") @@ -116,14 +133,19 @@ def get_product(product_id): print( f"Image {index + 1}/{len(images)} downloaded and saved: {file_path}" ) + if limit and limit >= index +1: + break else: print(f"Failed to download image {index + 1}/{len(images)}") + + return image_paths + else: if product.get("name"): print(f"No images found for {product.get('name')}") else: print("No images found") - return image_paths, product + return [] def upload_image(img_path): @@ -139,6 +161,8 @@ def upload_image(img_path): with open(img_path, "rb") as img_file: data = img_file.read() file_name = os.path.basename(img_path) + file_name = file_name.replace("–", "-") + credentials = load_credentials() if not credentials: messagebox.showerror( @@ -155,7 +179,7 @@ def upload_image(img_path): "Content-Disposition": f"attachment; filename={file_name}", "Authorization": f"basic {credentials_base64.decode()}", } - + print(f"Uploading image {img_path}") try: res = requests.post(url=url, data=data, headers=headers, timeout=10) res.raise_for_status() @@ -180,6 +204,7 @@ def delete_img(image_id): Args: image_id (int): The ID of the image to delete. """ + credentials = load_credentials() if not credentials: messagebox.showerror( @@ -205,64 +230,114 @@ def delete_img(image_id): print(f"Failed to delete image with ID {image_id}. Error: {res.text}") -def update_product(image_ids, product_id): + + + + +def update_product(product_id, new_list, old_list, options): """ - Update a WooCommerce product with new image IDs. + Update the images and meta data of a WooCommerce product. Args: image_ids (list): A list of new image IDs. product_id (int): The ID of the WooCommerce product. """ + wcapi = get_wcapi() if not wcapi: return - product = wcapi.get(f"products/{product_id}").json() - product["images"] = [{"id": image_id} for image_id in image_ids] - response = wcapi.put(f"products/{product_id}", data=product) + # Prepare the data with images and meta data fields + product_data = { + "images": [{"id": image_id} for image_id in new_list], + "meta_data": [ + { + "key": "_image_processed", + "value": options['hash_string'] + }, + { + "key": "_old_image_ids", + "value": [{"id": image_id} for image_id in old_list] + } + + ] + } + + # Print product data for debugging + print(f"Updating product {product_id} with the following data:") + print(json.dumps(product_data, indent=2)) + + # Send the update request with images and meta data fields + response = wcapi.put(f"products/{product_id}", data=product_data) # Using 'json' to pass data + if response.status_code == 200: - print( - f"Product with ID {product_id} updated successfully with new image IDs.") + print(f"Product with ID {product_id} updated successfully with new image IDs and meta data.") else: - print( - f"Failed to update product with ID {product_id}. Error: {response.text}") + print(f"Failed to update product with ID {product_id}. Error: {response.text}") -def process_product_images( options): + +def process_product_images(options): """ Process images for a WooCommerce product by resizing and uploading them. Args: - product_id (int): The ID of the WooCommerce product. - name_template (str): The template for generating image filenames. - canvas_width (int): The width of the canvas for resizing images. - canvas_height (int): The height of the canvas for resizing images. + options (dict): Contains options such as product_id, name_template, canvas_width, canvas_height. """ + + # Concatenate the values into a string + hash_input = f"{options['background_color']}_{options['canvas_height']}_{options['canvas_width']}_{options['image_format']}_{options['image_size']}" + + # Create a SHA256 hash from the concatenated string + hash_object = hashlib.sha256(hash_input.encode()) + hash_string = hash_object.hexdigest() + options['hash_string'] = hash_string + pprint.pprint(hash_string) product_id = options.get("product_id") if not product_id: + print("No product ID") return - image_paths, product = get_product(product_id) + product = options.get("product") + # Check if the product meta_data contains _image_processed with the current hash + if product['meta_data']: + for meta in product['meta_data']: + if meta['key'] == '_image_processed' and meta['value'] == hash_string: + print(f"Skipping product {product_id}, already processed with the current hash.") + return + image_paths = get_images(product) if not image_paths: return + + + + with tempfile.TemporaryDirectory() as temp_output_directory: print(f"Using temporary directory: {temp_output_directory}") old_list = [] new_list = [] - + pprint.pprint ( list(image_paths.values())) + file = FileProcessor() + log = options.get("log_message", None) + + for image_id, file_path in image_paths.items(): - file = FileProcessor() - img = ImageProcessor() - output_path = file.generate_output_path(temp_output_directory, file_path, options, product) - - img.resize_image(file_path, output_path, options) - new_id = upload_image(output_path) + + processed = file.process_images([file_path], temp_output_directory, options, log, product) + + + new_id = upload_image(processed[0]) + if new_id: old_list.append(image_id) new_list.append(new_id) - update_product(new_list, product_id) + if new_list: + options["image_ids"] = new_list # Store new image IDs in options + update_product(product_id, new_list, old_list, options) # Pass new image IDs here + for old in old_list: + delete_img(old) print("Temporary files processed and uploaded successfully.") @@ -294,35 +369,95 @@ def generate_output_path( ) return os.path.join(temp_output_directory, new_filename + ext) - -def process_all_products(options): +def get_first_image_path(product): + images = get_images(product, 1) + # Loop through the dictionary + if images: + for image_id, file_path in images.items(): + print(f"Processing Image ID: {image_id}") + print(f"File Path: {file_path}") + return file_path +def get_first_image(): """ Process images for all WooCommerce products by resizing and uploading them. Args: - name_template (str): The template for generating image filenames. - canvas_width (int): The width of the canvas for resizing images. - canvas_height (int): The height of the canvas for resizing images. + options (dict): Contains options such as name_template, canvas_width, canvas_height. """ wcapi = get_wcapi() if not wcapi: return page = 1 + total_products = 0 # Initialize the counter for total products + + + products = wcapi.get("products", params={"per_page": 5, "page": page}).json() + if not products: + return + for product in products: + total_products += 1 # Update the total count + return get_first_image_path(product) + +def search_product(search): + """ + Process images for all WooCommerce products by resizing and uploading them. + + Args: + options (dict): Contains options such as name_template, canvas_width, canvas_height. + """ + wcapi = get_wcapi() + if not wcapi: + return + + page = 1 + total_products = 0 # Initialize the counter for total products + while True: - products = wcapi.get("products", params={ - "per_page": 100, "page": page}).json() + products = wcapi.get("products", params={"per_page": 100, "page": page, "search": search}).json() + if not products: + break + return products + +def process_all_products(options): + """ + Process images for all WooCommerce products by resizing and uploading them. + + Args: + options (dict): Contains options such as name_template, canvas_width, canvas_height. + """ + wcapi = get_wcapi() + if not wcapi: + return + + page = 1 + total_products = 0 # Initialize the counter for total products + + while True: + products = wcapi.get("products", params={"per_page": 100, "page": page}).json() if not products: break + product_count = len(products) # Get the count of products on the current page + + for product in products: + total_products += 1 # Update the total count options["product_id"] = product["id"] - process_product_images( - options - ) + options["product"] = product + log = options.get("log_message", None) + if log: + if product: + name = product.get("name", "") + log.log_message(f"#{total_products} Processing {name} ") # Log the product name + process_product_images(options) page += 1 + # Log the total number of products processed + log(f"Total products processed: {total_products}") + + # Show completion message messagebox.showinfo( - "Process Complete", "All product images processing is complete." + "Process Complete", f"All product images processing is complete. Total products processed: {total_products}" ) diff --git a/config/encrypt_config.py b/config/encrypt_config.py index 764e960..0af9e0d 100644 --- a/config/encrypt_config.py +++ b/config/encrypt_config.py @@ -1,5 +1,6 @@ from cryptography.fernet import Fernet import json +import os class ConfigEncryptor: @@ -9,51 +10,153 @@ class ConfigEncryptor: self.fernet = Fernet(self.key) def encrypt_config(self, data): - json_data = json.dumps(data) - encrypted_data = self.fernet.encrypt(json_data.encode()) - with open(self.filename, "wb") as encrypted_file: - encrypted_file.write(encrypted_data) - print("Credentials saved") + """ + Encrypt the given data and save it to a file. + + Args: + data (dict): The dictionary containing credentials and options to encrypt and save. + """ + try: + json_data = json.dumps(data) + encrypted_data = self.fernet.encrypt(json_data.encode()) + with open(self.filename, "wb") as encrypted_file: + encrypted_file.write(encrypted_data) + print(f"Encrypted configuration saved to {self.filename}") + except Exception as e: + print(f"Error encrypting config: {e}") def get_key(self): + """ + Return the encryption key. + + Returns: + str: The encryption key as a string. + """ return self.key.decode() def save_credentials(self, credentials): - config = self.load_config() - if not config: - config = {"credentials": {}, "options": {}} - config["credentials"] = credentials + """ + Save WooCommerce credentials to the config file, handling multiple credential sets. + + Args: + credentials (dict): Dictionary containing WooCommerce credentials. + """ + # Load the existing configuration + config = self.load_config() or {"credentials": [], "options": {}} + + # Ensure credentials is a list of dictionaries (if this is the first time saving, initialize it) + if not isinstance(config.get("credentials"), list): + config["credentials"] = [] + + # Check if the credential with the same 'name' or 'nice_name' already exists and update it + existing_credential = None + for cred in config["credentials"]: + print(credentials) + if cred.get("nice_name") == credentials.get("nice_name"): + existing_credential = cred + break + + if existing_credential: + # Update the existing credential set + existing_credential.update(credentials) + else: + # Add new credentials if they don't exist + config["credentials"].append(credentials) + + # Set 'active' flag to True for this credential and False for others + for cred in config["credentials"]: + cred['active'] = cred.get("nice_name") == credentials.get("nice_name") + + # Encrypt and save the updated config + self.encrypt_config(config) + print(f"Credentials for {credentials.get('nice_name', 'Unnamed')} saved successfully.") + + def delete_credentials(self, credentials): + """ + Save WooCommerce credentials to the config file, handling multiple credential sets. + + Args: + credentials (dict): Dictionary containing WooCommerce credentials. + """ + # Load the existing configuration + config = self.load_config() or {"credentials": [], "options": {}} + + new_config = [] + for credi in config["credentials"]: + + if credi.get("nice_name") != credentials: + new_config.append(credi) + config["credentials"] = new_config + print(config) + # Encrypt and save the updated config self.encrypt_config(config) + + def save_options(self, options): - config = self.load_config() - if not config: - config = {"credentials": {}, "options": {}} - # Ensure options only contains serializable data + """ + Save options to the config file. Filters out non-serializable data. + + Args: + options (dict): Dictionary containing options such as canvas width, height, etc. + """ + config = self.load_config() or {"credentials": {}, "options": {}} serializable_options = {k: v for k, v in options.items() if self.is_json_serializable(v)} config["options"] = serializable_options self.encrypt_config(config) def load_config(self): + """ + Load and decrypt the config file. + + Returns: + dict: Decrypted configuration data containing credentials and options, or None if file not found. + """ + if not os.path.exists(self.filename): + print(f"Config file {self.filename} not found.") + return None + try: with open(self.filename, "rb") as encrypted_file: encrypted_data = encrypted_file.read() decrypted_data = self.fernet.decrypt(encrypted_data).decode() config = json.loads(decrypted_data) - - # Filter only relevant keys - keys_to_return = ["credentials", "options"] - return {key: config[key] for key in keys_to_return if key in config} - except FileNotFoundError: + return config + except Exception as e: + print(f"Error loading or decrypting config: {e}") return None + def load_credentials(self): + """ + Load the active WooCommerce credentials from the config file. + + Returns: + dict: The active WooCommerce credentials if found, otherwise None. + """ config = self.load_config() if config: - return config.get("credentials") + # Check if credentials exist and search for the one marked as 'active' + credentials_list = config.get("credentials", []) + if isinstance(credentials_list, list): + for credentials in credentials_list: + if credentials.get("active"): + return credentials + elif isinstance(credentials_list, dict): + return credentials_list return None + @staticmethod def is_json_serializable(value): + """ + Check if a value is JSON serializable. + + Args: + value: The value to check. + + Returns: + bool: True if value is serializable, False otherwise. + """ try: json.dumps(value) return True @@ -62,7 +165,6 @@ class ConfigEncryptor: # Define your key here -# Replace with your actual key key = b"u4xTBY5Ns4WYdLvqMjEr138mpMmDEhhqTszKCcDy2cI=" if __name__ == "__main__": @@ -84,4 +186,3 @@ if __name__ == "__main__": } encryptor = ConfigEncryptor(key) encryptor.encrypt_config(config_data) - diff --git a/controller.py b/controller.py new file mode 100644 index 0000000..b6b3100 --- /dev/null +++ b/controller.py @@ -0,0 +1,619 @@ +import tempfile +import threading +from utils.file_operations import FileProcessor +from utils.image_processing import ImageProcessor +from ui.options_window import OptionsWindow +from config.encrypt_config import ConfigEncryptor +from api.woocommerce_api import get_first_image +from PIL import Image, ImageTk +from pprint import pformat +from api.woocommerce_api import process_product_images, process_all_products, search_product, get_first_image_path +import customtkinter as ctk +import os + +class AppController: + """ + The controller class for managing the overall state and interactions of the application. + """ + + def __init__(self, root): + """ + Initialize the AppController. + + Args: + root (ctk.CTk): The root CustomTkinter window. + """ + key = b"u4xTBY5Ns4WYdLvqMjEr138mpMmDEhhqTszKCcDy2cI=" + self.root = root + self.file = FileProcessor() + self.image = ImageProcessor() + self.menu_bar = None + self.local_processing_tab = None + self.settings_tab = None + self.preview_bar = None + self.log = None + self.canvas_width = 900 + self.canvas_height = 900 + self.template = "{slug}_{sku}_{width}x{height}" + self.delete_images = False + self.transparent = True + self.background_color = "#000000" + self.image_format = "AUTO" + self.image_size = "contain" + self.config = ConfigEncryptor(key) + self.type = None + self.destination_path = None + self.found_products = None + self.selected_directory = None + self.current_product = 0 + self.status = "stopped" + self.load_config() + + def load_config(self): + config = self.config.load_config() + if config: + if options := config.get("options"): + self.canvas_width = options.get("canvas_width", 900) + self.canvas_height = options.get("canvas_height", 900) + self.template = options.get("template", "{slug}_{sku}_{width}x{height}") + self.delete_images = options.get("delete_images", False) + self.transparent = options.get("transparent", True) + self.background_color = options.get("background_color", "#000000") + self.image_format = options.get("image_format", "AUTO") + self.image_size = options.get("image_size", "contain") + + def set_menu_bar(self, menu_bar): + """ + Set the MenuBar for the application. + + Args: + menu_bar (MenuBar): The MenuBar instance. + """ + self.log_message("Init menu bar") + self.menu_bar = menu_bar + + def set_log(self, log): + """ + Set the MenuBar for the application. + + Args: + menu_bar (MenuBar): The MenuBar instance. + """ + self.log = log + self.log_message("Init Logs") + + def set_local_processing_tab(self, local_processing_tab): + """ + Set the LocalProcessingTab for the application. + + Args: + local_processing_tab (LocalProcessingTab): The LocalProcessingTab instance. + """ + self.local_processing_tab = local_processing_tab + self.log_message("Init main") + + def set_preview_bar(self, preview): + """ + Set the MenuBar for the application. + + Args: + menu_bar (MenuBar): The MenuBar instance. + """ + self.preview_bar = preview + self.log_message("Init previews") + + def set_info_bar(self, info): + """ + Set the MenuBar for the application. + + Args: + menu_bar (MenuBar): The MenuBar instance. + """ + self.info_bar = info + self.log_message("Init info") + + def set_settings_tab(self, settings_tab): + """ + Set the SettingsTab for the application. + + Args: + settings_tab (SettingsTab): The SettingsTab instance. + """ + self.settings_tab = settings_tab + self.log_message("Init settings") + + def show_local_processing_tab(self): + """ + Display the local processing tab. + """ + if self.local_processing_tab: + self.log_message("Show main tab") + self.local_processing_tab.tkraise() # Make sure to raise the correct tab frame + + def show_settings_tab(self): + """ + Display the settings tab. + """ + if self.settings_tab: + self.log_message("Show settings tab") + self.settings_tab.tab.tkraise() # Make sure to raise the correct tab frame + + def show_local_processing_options(self): + """ + Display the options window in the local processing tab. + """ + if self.local_processing_tab: + self.log_message("Open options") + self.open_options_window() + + def log_message(self, obj): + """ + Log a formatted message to the log window using pprint. + + Args: + obj (object): The object to format and log. + """ + if self.log: + formatted_message = pformat(obj) + self.log.log_message(formatted_message) + + import threading + + def start_processing(self): + """ + Start the image processing based on the selected options. + """ + source = self.type + options = self.get_options() + self.log_message(f"Start import source: {source}") + self.status = "started" + self.menu_bar.start_button.configure(fg_color="red", text="Running") + + # Wrapper to process and update status after completion + def process_and_update_status(target_func, *args): + try: + # Execute the actual processing function + target_func(*args) + finally: + # Update status to 'stopped' after processing is done + self.status = "stopped" + self.menu_bar.start_button.configure(fg_color="#008000", text="Start") + self.log_message(f"Processing completed for source: {source}") + + if source == "directory": + threading.Thread( + target=process_and_update_status, args=(self.file.process_directory_with_logging, options) + ).start() + elif source == "product": + threading.Thread( + target=process_and_update_status, args=(process_product_images, options) + ).start() + elif source == "file": + threading.Thread( + target=process_and_update_status, args=(self.file.proces_single_image, options) + ).start() + elif source == "all_products": + threading.Thread( + target=process_and_update_status, args=(process_all_products, options) + ).start() + + + def update_options(self, text=None): + """ + Update the UI elements based on the selected source type. + """ + self.type = text + self.log_message(f"Update options {text}") + if text: + self.update_info(text) + # if self.local_processing_tab: + # self.local_processing_tab.product_id_button.grid_remove() + # self.local_processing_tab.product_id_entry.grid_remove() + # self.local_processing_tab.additional_name_label.grid_remove() + # self.local_processing_tab.additional_name_entry.grid_remove() + # self.local_processing_tab.browse_button.grid_remove() + # self.local_processing_tab.browse_file_button.grid_remove() + # if self.type == "directory": + # self.local_processing_tab.browse_button.grid() + # elif self.type == "file": + # self.local_processing_tab.browse_button.grid() + # elif self.type == "all_products": + # pass + # elif self.type == "wp_image": + # self.local_processing_tab.product_id_button.grid() + # self.local_processing_tab.product_id_entry.grid() + # elif self.type == "product": + # self.local_processing_tab.product_id_button.grid() + # self.local_processing_tab.product_id_entry.grid() + + self.update_previews() + + def update_previews(self, before_path=None, after_path=None): + """ + Update the image previews. + + Args: + before_path (str, optional): The path to the 'before' image. + after_path (str, optional): The path to the 'after' image. + """ + first_image_path = False + if self.status != "started": + if self.type == "all_products": + first_image_path = get_first_image() + + elif self.type == "product" and self.found_products: + first_image_path = get_first_image_path(self.found_products[self.current_product]) + else: + + print("getting first path") + first_image_path = self.file.get_first_image_path() + + if before_path : + before_img = Image.open(before_path) + before_img.thumbnail((200, 200)) + before_photo = ImageTk.PhotoImage(before_img) + self.preview_bar.before_image_label.configure(image=before_photo) + self.preview_bar.before_image_label.image = before_photo + dir_name = os.path.basename(before_path) + if len(dir_name) > 35: + dir_name = f"...{dir_name[-35:]}" + self.preview_bar.before_filename_label.configure(text=dir_name) + + if after_path: + after_img = Image.open(after_path) + after_img.thumbnail((200, 200)) + after_photo = ImageTk.PhotoImage(after_img) + self.preview_bar.after_image_label.configure(image=after_photo) + self.preview_bar.after_image_label.image = after_photo + dir_name = os.path.basename(after_path) + if len(dir_name) > 35: + dir_name = f"...{dir_name[-35:]}" + self.preview_bar.before_filename_label.configure(text=dir_name) + + if first_image_path: + with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as temp_file: + output_path = temp_file.name + self.image.resize_image( + first_image_path, output_path, self.get_options() + ) + before_img = Image.open(first_image_path) + before_img.thumbnail((200, 200)) + before_photo = ImageTk.PhotoImage(before_img) + self.preview_bar.before_image_label.configure(image=before_photo) + self.preview_bar.before_image_label.image = before_photo + + after_img = Image.open(output_path) + after_img.thumbnail((200, 200)) + after_photo = ImageTk.PhotoImage(after_img) + self.preview_bar.after_image_label.configure(image=after_photo) + self.preview_bar.after_image_label.image = after_photo + + name = self.file.generate_output_path("/",first_image_path,self.get_options()) + dir_name = os.path.basename(name) + if len(dir_name) > 35: + dir_name = f"...{dir_name[-35:]}" + self.preview_bar.after_filename_label.configure(text=dir_name) + dir_name = os.path.basename(first_image_path) + if len(dir_name) > 35: + dir_name = f"...{dir_name[-35:]}" + self.preview_bar.before_filename_label.configure(text=dir_name) + + def set_image_preview(self, image_path, label): + """ + Set the image preview for a given label. + + Args: + image_path (str): The path to the image file. + label (ctk.CTkLabel): The label to set the image on. + """ + img = Image.open(image_path) + img.thumbnail((150, 150)) + photo = ImageTk.PhotoImage(img) + label.configure(image=photo) + label.image = photo + + def browse_file_command(self): + """ + Command to browse for a file. + """ + file = self.file.browse_files() + if file: + file_name = os.path.basename(file) + if len(file_name) > 35: + file_name = f"...{file_name[-35:]}" + self.info_bar.selected_button_label.configure(text=file_name) + self.apply_options(self.get_options()) + self.update_previews() + + def browse_directory_command(self): + """ + Command to browse for a directory. + """ + directory = self.file.browse_directory() + if directory: + dir_name = os.path.basename(directory) + if len(dir_name) > 35: + dir_name = f"...{dir_name[-35:]}" + # self.browse_button.configure(text=dir_name) + self.selected_directory = directory + self.apply_options(self.get_options()) + self.update_previews() + + def browse_destination_command(self): + """ + Open directory dialog to select a destination directory. + """ + destination_path = self.file.browse_directory() + if destination_path: + self.info_bar.destination_label.configure(text=f"Destination: {destination_path}") + print(f"Selected destination: {destination_path}") + self.destination_path = destination_path + + def apply_canvas_size(self): + """ + Apply the canvas size settings and update previews. + """ + self.image.set_canvas_size(self.canvas_width, self.canvas_height) + + def apply_image_size(self): + """ + Apply the canvas size settings and update previews. + """ + self.image.set_image_size(self.image_size) + + def apply_background_color(self): + """ + Apply the canvas size settings and update previews. + """ + self.image.set_background_color(self.background_color) + + def get_options(self) -> dict: + """ + Get the current processing options. + + Returns: + dict: The current processing options. + """ + if not self.destination_path: + self.destination_path = False + product = None + product_id = 0 + if self.found_products and len(self.found_products) >= self.current_product: + product_id = self.found_products[self.current_product]['id'] + product = self.found_products[self.current_product] + options = { + # "selected_directory": self.local_processing_tab.browse_button.cget("text"), + "canvas_width": self.canvas_width, + "canvas_height": self.canvas_height, + "log_message": self.log, # Use the log method from the log_window + "format_log_message": self.log_message, + "update_previews": self.update_previews, + "product_id": product_id, + "product": product, + "template": self.template, + "delete_images": self.delete_images, + "background_color": self.background_color, + "image_format": self.image_format, + "image_size": self.image_size, + "selected_directory": self.selected_directory, + "destination_path" : self.destination_path + } + return options + + def open_options_window(self): + """ + Open the options window. + """ + current_options = { + "canvas_width": { + "type": "number", + "label": "Width:", + "default": self.canvas_width, + "min": 1, + "max": 2540, + }, + "canvas_height": { + "type": "number", + "label": "Height:", + "default": self.canvas_height, + "min": 1, + "max": 2540, + }, + "template": { + "type": "text", + "label": "Filename Template:", + "default": self.template, + }, + "delete_images": { + "type": "checkbox", + "label": "Delete image when done", + "default": self.delete_images, + }, + "background_color": { + "type": "color", + "label": "Background Color:", + "default": self.background_color, + }, + "image_format": { + "type": "dropdown", + "label": "Image Format:", + "options": ["AUTO", "JPEG", "PNG", "GIF", "DZI", "AVIF", "WEBP"], + "default": self.image_format, + }, + "image_size": { + "type": "dropdown", + "label": "Image Size:", + "options": ["contain", "cover"], + "default": self.image_size, + }, + } + + OptionsWindow(self.root, self.apply_options, current_options) + + def apply_options(self, options): + """ + Apply the selected options from the options window. + + Args: + options (dict): The options to apply. + """ + # if self.log_window: + # self.log_window.clear() # Clear the log window if it exists + self.canvas_width = options["canvas_width"] + self.canvas_height = options["canvas_height"] + self.template = options["template"] + self.delete_images = options["delete_images"] + self.background_color = options["background_color"] + self.image_size = options["image_size"] + self.image_format = options["image_format"] + self.apply_canvas_size() + self.apply_background_color() + self.apply_image_size() + key = b"u4xTBY5Ns4WYdLvqMjEr138mpMmDEhhqTszKCcDy2cI=" + self.config.save_options(self.get_options()) + self.update_previews() + + def process_product(self, input): + self.found_products = search_product(input) + if self.found_products: + count_products = len(self.found_products) + print(f"Found {count_products} products") + print(f"Current product {self.current_product}") + print(self.found_products[self.current_product]) + self.info_bar.selected_button_label.configure(text=self.found_products[self.current_product]['name'] + " (id: "+ str(self.found_products[self.current_product]['id']) +")" ) + number = self.current_product + number += 1 + text = f"Viewing product {number}/{count_products}" + self.info_bar.destination_label.configure(text=text) + self.update_previews() + return self.found_products[self.current_product] + pass + + def change_product(self, data): + self.current_product += data + if self.found_products and len(self.found_products) >= self.current_product: + count_products = len(self.found_products) + print(self.found_products[self.current_product]) + self.info_bar.selected_button_label.configure(text=self.found_products[self.current_product]['name'] + " (id: "+ str(self.found_products[self.current_product]['id'])+")" ) + number = self.current_product + number += 1 + text = f"Viewing product {number}/{count_products}" + self.info_bar.destination_label.configure(text=text) + self.update_previews() + pass + + def update_info(self, selected_option): + """ + Update the info frame based on the selected option. + + Args: + selected_option (str): The currently selected option (e.g., "product", "file", etc.). + """ + + # Clear previous description and input fields + if self.info_bar.input_field: + self.info_bar.input_field.grid_forget() + if self.info_bar.input_button: + self.info_bar.input_button.grid_forget() + if self.info_bar.next_button: + self.info_bar.next_button.grid_forget() + if self.info_bar.prev_button: + self.info_bar.prev_button.grid_forget() + if self.info_bar.destination_button: + self.info_bar.destination_button.grid_forget() + if self.info_bar.destination_label: + self.info_bar.destination_label.grid_forget() + + display_label = selected_option.replace("_", " ").title() + self.info_bar.selected_button_label.configure(text=display_label) + + # Update the description and input fields based on the selected option + if selected_option == "product": + self.info_bar.description_label.configure(text="Search") + self.info_bar.input_field = ctk.CTkEntry(self.info_bar.parent_frame) + self.info_bar.input_field.grid(row=1, column=1, columnspan=2, padx=5, pady=5, sticky="ew") + + self.info_bar.input_button = ctk.CTkButton( + self.info_bar.parent_frame, + text="Search product:", + command=lambda: self.process_product(self.info_bar.input_field.get()) + ) + self.info_bar.input_button.grid(row=1, column=3, padx=5, pady=5, sticky="ew") + + self.info_bar.next_button = ctk.CTkButton( + self.info_bar.parent_frame, + text="Next", + command=lambda: self.change_product(1) + ) + self.info_bar.next_button.grid(row=2, column=3, padx=5, pady=5, sticky="ew") + + self.info_bar.prev_button = ctk.CTkButton( + self.info_bar.parent_frame, + text="Prev", + command=lambda: self.change_product(-1) + ) + self.info_bar.prev_button.grid(row=2, column=2, padx=5, pady=5, sticky="ew") + + # Destination Directory Label (to show the selected destination) + self.info_bar.destination_label = ctk.CTkLabel( + self.info_bar.parent_frame, text="No products found" + ) + self.info_bar.destination_label.grid(row=2, column=0, columnspan=2, padx=5, pady=5, sticky="w") + + elif selected_option == "file": + self.info_bar.description_label.configure(text="Choose a file to process:") + + # Browse File Button + self.info_bar.input_button = ctk.CTkButton( + self.info_bar.parent_frame, + text="Browse File", + command=self.browse_file_command + ) + self.info_bar.input_button.grid(row=1, column=1, padx=5, pady=5, sticky="ew") + + # Destination Directory Button + self.info_bar.destination_button = ctk.CTkButton( + self.info_bar.parent_frame, + text="Select Destination", + command=self.browse_destination_command # Command to browse destination directory + ) + self.info_bar.destination_button.grid(row=2, column=1, padx=5, pady=5, sticky="ew") + + # Destination Directory Label (to show the selected destination) + self.info_bar.destination_label = ctk.CTkLabel( + self.info_bar.parent_frame, text="No destination selected" + ) + self.info_bar.destination_label.grid(row=2, column=0, padx=5, pady=5, sticky="w") + + elif selected_option == "directory": + self.info_bar.description_label.configure(text="Choose a directory to process:") + + # Browse Directory Button + self.info_bar.input_button = ctk.CTkButton( + self.info_bar.parent_frame, + text="Browse Directory", + command=self.browse_directory_command + ) + self.info_bar.input_button.grid(row=1, column=1, padx=5, pady=5, sticky="ew") + + # Destination Directory Button + self.info_bar.destination_button = ctk.CTkButton( + self.info_bar.parent_frame, + text="Select Destination", + command=self.browse_destination_command # Command to browse destination directory + ) + self.info_bar.destination_button.grid(row=2, column=1, padx=5, pady=5, sticky="ew") + + # Destination Directory Label (to show the selected destination) + self.info_bar.destination_label = ctk.CTkLabel( + self.info_bar.parent_frame, text="No destination selected" + ) + self.info_bar.destination_label.grid(row=2, column=0, padx=5, pady=5, sticky="w") + + + + def run(self): + """ + Run the main event loop. + """ + self.root.mainloop() diff --git a/images/image-7.jpg b/images/image-7.jpg deleted file mode 100644 index 504d5c9..0000000 Binary files a/images/image-7.jpg and /dev/null differ diff --git a/main.py b/main.py index 1579c34..c319361 100644 --- a/main.py +++ b/main.py @@ -1,19 +1,28 @@ """ Main module for the Image Processor application. """ - +from PIL import Image import customtkinter as ctk -from ui.log_window import LogWindow -from ui.local_processing_tab import LocalProcessingTab +from ui.menu import MenuBar # Import the new MenuBar class +from ui.log_frame import LogWindow +from ui.button_frame import ButtonFrame +from ui.frame_info import InfoFrame from ui.settings_tab import SettingsTab from config.decrypt_config import ConfigDecryptor, DECRYPTION_KEY from config.encrypt_config import ConfigEncryptor +from controller import AppController + +from ui.preview_frame import PreviewFrame # Import the new PreviewFrame class + + class ImageProcessorApp: """ Main application class for the Image Processor. """ + + def __init__(self, root): """ Initialize the ImageProcessorApp. @@ -23,46 +32,82 @@ class ImageProcessorApp: """ self.root = root self.root.title("Image Processor") - self.root.geometry("480x800") - # Create menu frame at the top - menu_frame = ctk.CTkFrame(self.root) - menu_frame.pack(side="top", fill="x") + self.root.geometry("553x800") - local_processing_button = ctk.CTkButton(menu_frame, text="Local Processing", command=self.show_local_processing_tab) - local_processing_button.pack(side="left", padx=5, pady=5) + # Initialize the controller + self.controller = AppController(self.root) - settings_button = ctk.CTkButton(menu_frame, text="Settings", command=self.show_settings_tab) - settings_button.pack(side="left", padx=5, pady=5) + # Create the menu bar + self.menu_bar = MenuBar(self.root, self.controller) - # Create main frame to hold tabs and log window + # Create the main frame to hold tabs, log window, and other sections main_frame = ctk.CTkFrame(self.root) - main_frame.pack(expand=True, fill="x") + main_frame.pack(expand=True, fill="both") # Ensure the main frame expands both vertically and horizontally - self.tab_parent = ctk.CTkFrame(main_frame) - self.tab_parent.grid(row=0, column=0, sticky="nsew") + # Configure row and column to expand and fill available space + self.root.grid_rowconfigure(0, weight=1) # Ensures the frames expand vertically + self.root.grid_columnconfigure(0, weight=1) # Ensures the frames expand horizontally - self.log_frame = ctk.CTkFrame(main_frame) - self.log_frame.grid(row=1, column=0, sticky="nsew") - - main_frame.grid_rowconfigure(0, weight=1) - - main_frame.grid_columnconfigure(0, weight=1) + # Create a master frame to hold all the other frames + self.master_main_frame = ctk.CTkFrame(main_frame) + self.master_main_frame.grid(row=0, column=0, sticky="nsew") + self.master_main_frame.grid_rowconfigure(0, weight=1) + self.master_main_frame.grid_columnconfigure(0, weight=1) # Ensure full-width spanning + # Log Frame (appears at the bottom) + self.log_frame = ctk.CTkFrame(self.master_main_frame) + self.log_frame.grid(row=3, column=0, sticky="ew") # Set sticky to "ew" to expand horizontally + self.log_frame.grid_columnconfigure(0, weight=1) self.log_window = LogWindow(self.log_frame) + self.controller.set_log(self.log_window) + # Button Frame + self.button_frame = ctk.CTkFrame(self.master_main_frame, height=250) + self.button_frame.grid(row=0, column=0, sticky="nsew") # Set sticky to "ew" to expand horizontally + self.button_frame.grid_columnconfigure(0, weight=1) + self.button_frame = ButtonFrame(self.button_frame, self.controller, None) - self.local_processing_tab = LocalProcessingTab(self.tab_parent, self.log_window) - self.settings_tab = SettingsTab(self.tab_parent) + # Info Frame + self.info_frame = ctk.CTkFrame(self.master_main_frame) + self.info_frame.grid(row=1, column=0, sticky="ew") # Set sticky to "ew" to expand horizontally + self.info_frame.grid_columnconfigure(0, weight=1) + self.info_frame = InfoFrame(self.info_frame) - self.local_processing_tab.tab.grid(row=0, column=0, sticky="nsew") + # Preview Frame + self.preview_frame = ctk.CTkFrame(self.master_main_frame) + self.preview_frame.grid(row=2, column=0, sticky="nsew") # Expand both horizontally and vertically + self.preview_frame.grid_columnconfigure(0, weight=1) + self.preview_frame = PreviewFrame(self.preview_frame) # Initialize the PreviewFrame + + + + # Settings Tab + self.settings_tab = SettingsTab(main_frame, self.controller) + + # Register the tabs and preview frame with the controller + self.controller.set_local_processing_tab(self.master_main_frame) + self.controller.set_settings_tab(self.settings_tab) + self.controller.set_preview_bar(self.preview_frame) + self.controller.set_info_bar(self.info_frame) + self.controller.set_menu_bar( self.menu_bar) + # Position the tabs + self.master_main_frame.grid(row=0, column=0, sticky="nsew") # Make sure master_main_frame expands self.settings_tab.tab.grid(row=0, column=0, sticky="nsew") - self.show_local_processing_tab() + # Show the default tab (Local Processing Tab) + self.controller.update_options() + self.open_local_processing_tab() - def show_local_processing_tab(self): + def open_local_processing_tab(self): """ Show the Local Processing tab. """ - self.local_processing_tab.tab.tkraise() + self.master_main_frame.tkraise() + + def show_local_processing_options(self): + """ + Show the Local Processing tab. + """ + self.master_main_frame.open_options_window() def show_settings_tab(self): """ @@ -76,19 +121,27 @@ class ImageProcessorApp: """ self.root.mainloop() + + if __name__ == "__main__": try: decryptor = ConfigEncryptor(DECRYPTION_KEY) + # Load the active credentials config = decryptor.load_credentials() + print(config) if config: - wc_url = config["url"] - wc_consumer_key = config["consumer_key"] - wc_consumer_secret = config["consumer_secret"] - wp_username = config["username"] - wp_password = config["password"] + wc_url = config.get("url", "") + wc_consumer_key = config.get("consumer_key", "") + wc_consumer_secret = config.get("consumer_secret", "") + wp_username = config.get("username", "") + wp_password = config.get("password", "") + else: + print("No active credentials found.") except FileNotFoundError as e: print(f"File not found: {e}") + except Exception as e: + print(f"An error occurred: {e}") root = ctk.CTk() ctk.set_appearance_mode("dark") diff --git a/main.spec b/main.spec index c55496f..7618bee 100644 --- a/main.spec +++ b/main.spec @@ -1,25 +1,36 @@ +# Import necessary modules +import glob +import os # -*- mode: python ; coding: utf-8 -*- +# Collect all PNG and JPG images in the ui/images directory +image_files = [(file, "ui/images") for file in glob.glob("ui/images/*.*") if file.endswith(('.png', '.jpg', '.jpeg'))] + +block_cipher = None + a = Analysis( ['main.py'], pathex=[], binaries=[], - datas=[], + datas=image_files, hiddenimports=[], hookspath=[], hooksconfig={}, runtime_hooks=[], excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, noarchive=False, - optimize=0, ) -pyz = PYZ(a.pure) +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE( pyz, a.scripts, a.binaries, + a.zipfiles, a.datas, [], name='main', @@ -29,7 +40,7 @@ exe = EXE( upx=True, upx_exclude=[], runtime_tmpdir=None, - console=False, + disable_windowed_traceback=False, argv_emulation=False, target_arch=None, diff --git a/speelgoed-config.enc b/speelgoed-config.enc new file mode 100644 index 0000000..5a9a116 --- /dev/null +++ b/speelgoed-config.enc @@ -0,0 +1 @@ +gAAAAABm7tsOIxGHffAAnXR8MylfB10oI10SSNuhkirXHBYDu8UcAHDD3S1ADs1NEteVhFUnDZTCKe8AZuFdy5jyMPg9UzoxPL1_7Sbn5_10sfiOyOJHStm-7fx-4WmRrQyaeToAOS0fJYBe_Vg2AGndAB5vu7zPQ_X56jEmNzBzepdMcFtEUMscJB1Aud--p9QAg6KdZSZzmIks8QybBhAEqPifBKPwjxIivY_vx3l7p2lg7D7T1XfWHqufpTYqrDM_FTHy0wKnUJb_sHZCyuD2FNu9WhrqbZ-WYzHRTLEc0TJc1KoICiRHxWHwvZqiG1RCR8HBY1lXXOdYUv-1M-CtFQKD7U5xHKfFHC5wPF-Z682vL3-XJN5QfVXWQ27ua-TCilJF5NUetAXIBuxB2QjrntgugcsW3UxAZivcZk2Ux8hh12FYG3PDE-BUjg4ZK0gIuJ0LpJpUoYnz7CTmHLJ8FhWE1dBJoBAykyxoa5495DJGPuk_farzr9L3UTkfwuxOMkO7cip2rYOFgms8I0yxaBCFag3BdLLez9rIwghouwVzwgxMvHlN7toztQ9C6Dgu6e2bYimkz1JFyvFYO5YZLpX56It1yRaAgs7hwfhK5KOw9HcVEmKr4ipWXZe5rnnblW5NZ3HfsqwuPpjsg29F2g3AwlZlhc-Xl6D53g-2ydEnkhyvCePy7lZmIb_PB-MS4UzBpMFYDGfqRkoaLGNy8s0nelfmZM5i4ZKJPJGtulsIeyyU6rdmwYNf3S6f87CMNLhXYEE0 \ No newline at end of file diff --git a/test-config.enc b/test-config.enc new file mode 100644 index 0000000..1b18e48 --- /dev/null +++ b/test-config.enc @@ -0,0 +1 @@ +gAAAAABm7sewlH0ntIM9w6KP4wBUv-nUiW51UId3_8KCRSWM0OptC6n1VnAZukXHo5ZMx1fLf8OzQwWS5LWb0Cwjnauq3U2iB9Ck4DWklNoHNbE2L7T3yGPrxldSnkUU26hpYlELG5kbk5LMr55Y3JOhgsA75VKfv4X0OeAegngLYqfaV0uTA6Xw4WDz6ZDR4lBNpTfEPIPsUmEwuUNgYHCEwyRVEZXXWTK-bIaYhEAojd5Ecn5f09f9-qx_C7zd84335pa28FfrkQIn_-Ms8iosCw_ZOhh7_DRJrCfI5ursfhsbf9_Nk9QMFXPfFgpHx3zcDSslEcTvoMnVQqKgLesvwY_Mg1ISX6ZnDAWIoz0CfPD6jC0LPQXya1nFWSWHEx1FG_mebFrElDLRZO_hHLZFyrOcjuKHSwUKJq58nYUcw0hpmABzN1SUn7ofbp-XlSQpbE8DLH3OeM-WQYqPk0rQpKF19-cGNF6jxK8XzqI7cgONy3SQDEyB7ImKusxKyovF-A7TvGJcL2Nfuiz1zDODHJ771jLUfCO0Ho9GBvmJ3ZXzOn7zYCWjHc4Pt2zO6MCpbkuXVxYgXFLYaWNLE9lVxlT5JZKlKoD2Clg4Fkik1e9eVU7opWhB4DW4rLvdMgtzwCyVp5F-TnoOBsAqwHpD44Q90ZwC0m0BfRd6yt5aedYCeDZ_h5JftCz-_KvASFRXCXJpIwxOa2cStw5qoewv_Ebb5ep9-l4imWODZRVvlbHcB37iHn0ux2vAzGWkCVbV568HiqmH \ No newline at end of file diff --git a/ui/button_frame.py b/ui/button_frame.py new file mode 100644 index 0000000..d99b0c5 --- /dev/null +++ b/ui/button_frame.py @@ -0,0 +1,93 @@ +import customtkinter as ctk +from PIL import Image +from tkinter import StringVar +import os +import sys + +def resource_path(relative_path): + """ Get the absolute path to a resource, whether we're running in development or a PyInstaller package. """ + try: + # PyInstaller stores files in _MEIPASS when built + base_path = sys._MEIPASS + except Exception: + base_path = os.path.abspath(".") + + return os.path.join(base_path, 'ui/images/'+ relative_path +'.png') +class ButtonFrame: + """ + Class for creating and managing the button frame. + """ + + def __init__(self, parent_frame, controller, log_window): + """ + Initialize the ButtonFrame. + + Args: + parent_frame (ctk.CTkFrame): The parent frame where the buttons will be placed. + controller (AppController): The controller to handle logic and updates. + log_window (LogWindow): The log window to display log messages. + """ + self.parent_frame = parent_frame + self.controller = controller + self.log_window = log_window + # self.log = self.log_window.log_message + + self.buttons = {"directory", "file", "wp_image", "product", "all_products"} + self.source_buttons = {} + self.selected_button = StringVar(value="") # To store the selected button + + self.setup_ui() + + def setup_ui(self): + """ + Set up the UI for the button frame. + """ + row = 0 + self.create_buttons(self.parent_frame, self.buttons, self.source_buttons, row) + + def create_buttons(self, frame, button_data, button_store, row): + """ + Create buttons from the button_data list and store them in button_store. + """ + + col_index = 0 + for label in button_data: + path = resource_path(label) + display_label = label.replace("_", " ").title() + icon_path = path + if icon_path: + icon_image = ctk.CTkImage( + light_image=Image.open(icon_path), size=(24, 24) + ) + else: + icon_image = None + + button = ctk.CTkButton( + frame, + image=icon_image, + text=display_label, + font=("Helvetica", 12, "bold"), + command=lambda l=label, s=button_store: self.set_active_button(l, s), + fg_color="#666666", + hover_color="#0f4d0f", + compound="top", + width=100, + + ) + button.grid(row=row, column=col_index,columnspan=1, padx=5, pady=5, sticky="ew") + button_store[label] = button + col_index += 1 + + def set_active_button(self, active_label, button_store): + """ + Set the clicked button to green and the rest to gray for a specific button store. + Also update the description and input fields based on the active button. + """ + if self.controller.status != "started": + for label, button in button_store.items(): + if label == active_label: + self.controller.update_options(active_label) + button.configure(fg_color="#008000") + else: + button.configure(fg_color="#666666") + diff --git a/ui/frame_info.py b/ui/frame_info.py new file mode 100644 index 0000000..52011a5 --- /dev/null +++ b/ui/frame_info.py @@ -0,0 +1,60 @@ +# info_frame.py + +import customtkinter as ctk +from tkinter import filedialog + + +class InfoFrame: + """ + Class for managing the info frame where descriptions and input fields are shown. + """ + + def __init__(self, parent_frame): + """ + Initialize the InfoFrame. + + Args: + parent_frame (ctk.CTkFrame): The parent frame for the info section. + log_window (LogWindow): The log window to display log messages. + """ + self.parent_frame = parent_frame + + self.selected_button_label = None + self.description_label = None + self.input_field = None + self.input_button = None + self.prev_button = None + self.next_button = None + self.destination_button = None + self.destination_label = None + self.setup_ui() + + def setup_ui(self): + """ + Set up the UI for the info frame. + """ + # Label to display the selected button name + self.selected_button_label = ctk.CTkLabel( + self.parent_frame, text="", font=("Helvetica", 12, "bold") + ) + self.selected_button_label.grid(row=0, column=0, columnspan=12, padx=5, pady=5, sticky="w") + + # Description label to provide info about the selected button + self.description_label = ctk.CTkLabel( + self.parent_frame, text="", font=("Helvetica", 10) + ) + self.description_label.grid(row=1, column=0, columnspan=3, padx=5, pady=5, sticky="w") + + def process_product(self, product_id): + # Handle product processing logic here + self.log(f"Processing product with ID: {product_id}") + + def browse_file(self): + # Open file dialog to select a file + file_path = filedialog.askopenfilename() + + + def browse_directory(self): + # Open directory dialog to select a directory + directory_path = filedialog.askdirectory() + diff --git a/ui/images/all_products.png b/ui/images/all_products.png new file mode 100644 index 0000000..f9f0c60 Binary files /dev/null and b/ui/images/all_products.png differ diff --git a/ui/images/cogs.png b/ui/images/cogs.png new file mode 100644 index 0000000..1dd4f94 Binary files /dev/null and b/ui/images/cogs.png differ diff --git a/ui/images/directory.png b/ui/images/directory.png new file mode 100644 index 0000000..e6cd516 Binary files /dev/null and b/ui/images/directory.png differ diff --git a/ui/images/file.png b/ui/images/file.png new file mode 100644 index 0000000..2a07388 Binary files /dev/null and b/ui/images/file.png differ diff --git a/ui/images/filters.png b/ui/images/filters.png new file mode 100644 index 0000000..324480b Binary files /dev/null and b/ui/images/filters.png differ diff --git a/ui/images/house-user-solid.png b/ui/images/house-user-solid.png new file mode 100644 index 0000000..52d6f91 Binary files /dev/null and b/ui/images/house-user-solid.png differ diff --git a/ui/images/play.png b/ui/images/play.png new file mode 100644 index 0000000..d5410fe Binary files /dev/null and b/ui/images/play.png differ diff --git a/ui/images/product.png b/ui/images/product.png new file mode 100644 index 0000000..75f0935 Binary files /dev/null and b/ui/images/product.png differ diff --git a/ui/images/save.png b/ui/images/save.png new file mode 100644 index 0000000..b9e1768 Binary files /dev/null and b/ui/images/save.png differ diff --git a/ui/images/trash.png b/ui/images/trash.png new file mode 100644 index 0000000..a2923b8 Binary files /dev/null and b/ui/images/trash.png differ diff --git a/ui/images/wp_image.png b/ui/images/wp_image.png new file mode 100644 index 0000000..23b1d9a Binary files /dev/null and b/ui/images/wp_image.png differ diff --git a/ui/local_processing_tab.py b/ui/local_processing_tab.py deleted file mode 100644 index 53fc6e4..0000000 --- a/ui/local_processing_tab.py +++ /dev/null @@ -1,423 +0,0 @@ -import tempfile -import threading -import customtkinter as ctk -from tkinter.scrolledtext import ScrolledText -from tkinter import StringVar, BooleanVar -from PIL import Image, ImageTk -from utils.file_operations import FileProcessor -from utils.image_processing import ImageProcessor -from api.woocommerce_api import process_product_images, process_all_products -from ui.options_window import OptionsWindow -from pprint import pformat -from config.encrypt_config import ConfigEncryptor -import os - - -class LocalProcessingTab: - """ - Class for the Local Processing Tab in the Image Processor application. - """ - - def __init__(self, tab_parent, log_window): - """ - Initialize the LocalProcessingTab. - - Args: - tab_parent (ctk.CTkFrame): The parent frame widget. - log_window (LogWindow): The log window frame. - """ - key = b"u4xTBY5Ns4WYdLvqMjEr138mpMmDEhhqTszKCcDy2cI=" - - self.log_window = log_window - self.log = self.log_window.log_message - self.tab = ctk.CTkFrame(tab_parent) - self.root = self.tab.winfo_toplevel() # Store the root window reference - self.config = ConfigEncryptor(key) - - self.canvas_width = 900 - self.canvas_height = 900 - self.template = "{slug}_{sku}_{width}x{height}" - self.delete_images = False - self.transparent = True - self.background_color = "#000000" - self.image_format = "AUTO" - self.image_size = "contain" - self.load_config() - self.source_type = StringVar(value="directory") - self.checkbox_var = BooleanVar(value=False) - self.file = FileProcessor() - self.image = ImageProcessor() - # Automatically open the options window with default options - - self.setup_ui() - self.update_options() - - def load_config(self): - config = self.config.load_config() - if config: - if options := config.get("options"): - self.canvas_width = options.get("canvas_width", 900) - self.canvas_height = options.get("canvas_height", 900) - self.template = options.get("template", "{slug}_{sku}_{width}x{height}") - self.delete_images = options.get("delete_images", False) - self.transparent = options.get("transparent", True) - self.background_color = options.get("background_color", "#000000") - self.image_format = options.get("image_format", "AUTO") - self.image_size = options.get("image_size", "contain") - - def setup_ui(self): - """ - Set up the user interface for the tab. - """ - current_row = 0 - start_options_frame = ctk.CTkFrame(self.tab, bg_color="gray30") - start_options_frame.grid(row=current_row, column=0, columnspan=6, padx=5, pady=5, sticky="ew") - - self.options_button = ctk.CTkButton( - start_options_frame, text="Options", command=self.open_options_window - ) - self.options_button.grid(row=0, column=0, columnspan=2, padx=5, pady=5, sticky="w") - - self.button_start = ctk.CTkButton( - start_options_frame, text="Start Processing", command=self.start_processing - ) - self.button_start.grid(row=0, column=2, columnspan=2, padx=5, pady=5, sticky="w") - - # Image previews section - current_row += 1 - - # Source selection section - source_frame = ctk.CTkFrame(self.tab, bg_color="gray20") - source_frame.grid(row=current_row, column=0, columnspan=6, padx=5, pady=5, sticky="ew") - - source_label = ctk.CTkLabel(source_frame, anchor="w", text="Source Type:") - source_label.grid(row=0, column=0, columnspan=6, padx=5, pady=5, sticky="w") - - self.source_dropdown = ctk.CTkComboBox( - source_frame, - variable=self.source_type, - values=["directory", "file", "wp_image", "product", "all_products"], - state="readonly", - command=self.update_options - ) - self.source_dropdown.grid(row=1, column=0, columnspan=2, padx=5, pady=5, sticky="w") - self.source_dropdown.bind( - "<>", lambda e: self.update_options() - ) - - self.browse_button = ctk.CTkButton( - source_frame, text="Browse directory", command=self.browse_directory_command - ) - self.browse_button.grid(row=2, column=0, columnspan=2, padx=5, pady=5, sticky="w") - - self.browse_file_button = ctk.CTkButton( - source_frame, text="Browse file", command=self.browse_file_command - ) - self.browse_file_button.grid(row=2, column=2, columnspan=2, padx=5, pady=5, sticky="w") - - self.product_id_button = ctk.CTkButton(source_frame, text="Get", width=25) - self.product_id_button.grid(row=2, column=4, columnspan=1, padx=5, pady=5, sticky="w") - - self.product_id_entry = ctk.CTkEntry(source_frame) - self.product_id_entry.grid(row=2, column=5, columnspan=2, padx=5, pady=5, sticky="w") - - self.additional_name_label = ctk.CTkLabel(source_frame, text="Add suffix:") - self.additional_name_label.grid(row=2, column=7, padx=5, pady=5, sticky="w") - - self.additional_name_entry = ctk.CTkEntry(source_frame) - self.additional_name_entry.grid(row=2, column=8, padx=5, pady=5, sticky="w") - - # Destination selection section - current_row += 1 - # destination_frame = ctk.CTkFrame(self.tab, bg_color="gray25") - # destination_frame.grid(row=current_row, column=0, columnspan=6, padx=5, pady=5, sticky="ew") - - # destination_label = ctk.CTkLabel(destination_frame, anchor="w", text="Destination Type:") - # destination_label.grid(row=0, column=0, columnspan=6, padx=5, pady=5, sticky="w") - - # self.destination_dropdown = ctk.CTkComboBox( - # destination_frame, - # variable=self.source_type, - # values=["auto", "directory", "file", "wp_image", "product"], - # state="readonly", - # command=self.update_options - # ) - # self.destination_dropdown.grid(row=1, column=0, columnspan=2, padx=5, pady=5, sticky="w") - - # # Start and Options section - # current_row += 1 - - preview_frame = ctk.CTkFrame(self.tab, bg_color="gray35") - preview_frame.grid(row=current_row, column=0, columnspan=6, padx=5, pady=5, sticky="ew") - - self.before_label = ctk.CTkLabel(preview_frame, text="Before:") - self.before_label.grid(row=0, column=0, padx=5, pady=5, sticky="w") - - self.after_label = ctk.CTkLabel(preview_frame, text="After:") - self.after_label.grid(row=0, column=3, padx=5, pady=5, sticky="w") - - self.before_image_label = ctk.CTkLabel(preview_frame, text="") - self.before_image_label.grid(row=1, column=0, columnspan=3, padx=5, pady=5, sticky="w") - - self.after_image_label = ctk.CTkLabel(preview_frame, text="") - self.after_image_label.grid(row=1, column=3, columnspan=3, padx=5, pady=5, sticky="w") - - # Configure grid weights to make frames span the full width - self.tab.grid_columnconfigure(0, weight=1) - source_frame.grid_columnconfigure(0, weight=1) - - start_options_frame.grid_columnconfigure(0, weight=1) - preview_frame.grid_columnconfigure(0, weight=1) - - def update_options(self, text=None): - """ - Update the UI elements based on the selected source type. - """ - self.product_id_button.grid_remove() - self.product_id_entry.grid_remove() - self.additional_name_label.grid_remove() - self.additional_name_entry.grid_remove() - self.browse_button.grid_remove() - self.browse_file_button.grid_remove() - if self.source_type.get() == "directory": - self.browse_button.grid() - elif self.source_type.get() == "product": - self.product_id_button.grid() - self.product_id_entry.grid() - elif self.source_type.get() == "file": - self.browse_file_button.grid() - self.update_previews() - - def update_previews(self, before_path=None, after_path=None): - """ - Update the image previews. - - Args: - before_path (str, optional): The path to the 'before' image. - after_path (str, optional): The path to the 'after' image. - """ - first_image_path = self.file.get_first_image_path() - if before_path and after_path: - before_img = Image.open(before_path) - before_img.thumbnail((200, 200)) - before_photo = ImageTk.PhotoImage(before_img) - self.before_image_label.configure(image=before_photo) - self.before_image_label.image = before_photo - - after_img = Image.open(after_path) - after_img.thumbnail((200, 200)) - after_photo = ImageTk.PhotoImage(after_img) - self.after_image_label.configure(image=after_photo) - self.after_image_label.image = after_photo - elif first_image_path: - with tempfile.NamedTemporaryFile( - suffix=".jpg", delete=False - ) as temp_file: - output_path = temp_file.name - self.image.resize_image( - first_image_path, output_path, self.get_options() - ) - before_img = Image.open(first_image_path) - before_img.thumbnail((200, 200)) - before_photo = ImageTk.PhotoImage(before_img) - self.before_image_label.configure(image=before_photo) - self.before_image_label.image = before_photo - - after_img = Image.open(output_path) - after_img.thumbnail((200, 200)) - after_photo = ImageTk.PhotoImage(after_img) - self.after_image_label.configure(image=after_photo) - self.after_image_label.image = after_photo - - def set_image_preview(self, image_path, label): - """ - Set the image preview for a given label. - - Args: - image_path (str): The path to the image file. - label (ctk.CTkLabel): The label to set the image on. - """ - img = Image.open(image_path) - img.thumbnail((150, 150)) - photo = ImageTk.PhotoImage(img) - label.configure(image=photo) - label.image = photo - - def browse_file_command(self): - """ - Command to browse for a file. - """ - file = self.file.browse_files() - if file: - file_name = os.path.basename(file) - if len(file_name) > 20: - file_name = f"...{file_name[-20:]}" - self.browse_file_button.configure(text=file_name) - self.apply_options(self.get_options()) - self.update_previews() - - def browse_directory_command(self): - """ - Command to browse for a directory. - """ - directory = self.file.browse_directory() - if directory: - dir_name = os.path.basename(directory) - if len(dir_name) > 20: - dir_name = f"...{dir_name[-20:]}" - self.browse_button.configure(text=dir_name) - self.apply_options(self.get_options()) - self.update_previews() - - def apply_canvas_size(self): - """ - Apply the canvas size settings and update previews. - """ - self.image.set_canvas_size(self.canvas_width, self.canvas_height) - - def apply_image_size(self): - """ - Apply the canvas size settings and update previews. - """ - self.image.set_image_size(self.image_size) - - def apply_background_color(self): - """ - Apply the canvas size settings and update previews. - """ - self.image.set_background_color(self.background_color) - - def get_options(self) -> dict: - """ - Get the current processing options. - - Returns: - dict: The current processing options. - """ - options = { - "selected_directory": self.browse_button.cget("text"), - "canvas_width": self.canvas_width, - "canvas_height": self.canvas_height, - "log_message": self.log, # Use the log method from the log_window - "format_log_message": self.pprint_log_message, - "update_previews": self.update_previews, - "product_id": self.product_id_entry.get(), - "template": self.template, - "delete_images": self.delete_images, - "background_color": self.background_color, - "image_format": self.image_format, - "image_size": self.image_size, - } - return options - - def open_options_window(self): - """ - Open the options window. - """ - current_options = { - "canvas_width": { - "type": "number", - "label": "Width:", - "default": self.canvas_width, - "min": 1, - "max": 2540, - }, - "canvas_height": { - "type": "number", - "label": "Height:", - "default": self.canvas_height, - "min": 1, - "max": 2540, - }, - "template": { - "type": "text", - "label": "Filename Template:", - "default": self.template, - }, - "delete_images": { - "type": "checkbox", - "label": "Delete image when done", - "default": self.delete_images, - }, - "background_color": { - "type": "color", - "label": "Background Color:", - "default": self.background_color - }, - "image_format": { - "type": "dropdown", - "label": "Image Format:", - "options": ["AUTO", "JPEG", "PNG", "GIF", "DZI"], - "default": self.image_format - }, - "image_size": { - "type": "dropdown", - "label": "Image Size:", - "options": ["contain", "cover"], - "default": self.image_size - } - - } - - OptionsWindow(self.root, self.apply_options, current_options) - - def apply_options(self, options): - """ - Apply the selected options from the options window. - - Args: - options (dict): The options to apply. - """ - if self.log_window: - self.log_window.clear() # Clear the log window if it exists - self.canvas_width = options["canvas_width"] - self.canvas_height = options["canvas_height"] - self.template = options["template"] - self.delete_images = options["delete_images"] - self.background_color = options["background_color"] - self.image_size = options["image_size"] - self.image_format = options["image_format"] - self.apply_canvas_size() - self.apply_background_color() - self.apply_image_size() - key = b"u4xTBY5Ns4WYdLvqMjEr138mpMmDEhhqTszKCcDy2cI=" - self.config.save_options(self.get_options()) - self.update_previews() - - def pprint_log_message(self, obj): - """ - Log a formatted message to the log window using pprint. - - Args: - obj (object): The object to format and log. - """ - formatted_message = pformat(obj) - self.log(formatted_message) - - def start_processing(self): - """ - Start the image processing based on the selected options. - """ - source = self.source_type.get() - options = self.get_options() - - if source == "directory": - threading.Thread( - target=self.file.process_directory_with_logging, args=(options,) - ).start() - elif source == "product": - threading.Thread( - target=process_product_images, - args=(options,) - ).start() - elif source == "file": - threading.Thread( - target=self.file.proces_single_image, - args=(options,) - ).start() - elif source == "all_products": - threading.Thread( - target=process_all_products, - args=(options,) - ).start() - self.update_previews() diff --git a/ui/log_window.py b/ui/log_frame.py similarity index 72% rename from ui/log_window.py rename to ui/log_frame.py index 06e7e0f..37154e1 100644 --- a/ui/log_window.py +++ b/ui/log_frame.py @@ -1,5 +1,5 @@ import customtkinter as ctk - +from datetime import datetime class LogWindow: def __init__(self, parent): self.frame = ctk.CTkFrame(parent) @@ -14,8 +14,18 @@ class LogWindow: self.log_text.configure(yscrollcommand=self.scrollbar.set) def log_message(self, message): + """ + Log a message to the log window with the current timestamp. + """ + # Get the current time in the desired format (e.g., HH:MM:SS) + current_time = datetime.now().strftime("%H:%M:%S") + + # Prepend the current time to the message + full_message = f"[{current_time}] {message}" + + # Log the message self.log_text.configure(state="normal") - self.log_text.insert(ctk.END, message + "\n") + self.log_text.insert(ctk.END, full_message + "\n") self.log_text.see(ctk.END) self.log_text.configure(state="disabled") self.log_text.update_idletasks() diff --git a/ui/menu.py b/ui/menu.py new file mode 100644 index 0000000..ee86106 --- /dev/null +++ b/ui/menu.py @@ -0,0 +1,103 @@ +from PIL import Image +import customtkinter as ctk +import os +import sys + +def resource_path(relative_path): + """ Get the absolute path to a resource, whether we're running in development or a PyInstaller package. """ + try: + # PyInstaller stores files in _MEIPASS when built + base_path = sys._MEIPASS + except Exception: + base_path = os.path.abspath(".") + + return os.path.join(base_path, 'ui/images/'+ relative_path +'.png') + +class MenuBar: + def __init__(self, parent, controller): + """ + Initialize the MenuBar. + + Args: + parent (ctk.CTkFrame): The parent frame for the menu. + controller (AppController): The controller instance to manage the app. + """ + self.parent = parent + self.controller = controller + self.setup_ui() + + def setup_ui(self): + + button_width = 40 + icon_size = 24 + # Create menu frame + self.menu_frame = ctk.CTkFrame(self.parent) + self.menu_frame.pack(side="top", fill="x") + + # Create the buttons with icons + self.create_menu_button( + "house-user-solid", + "#363636", + "", + self.controller.show_local_processing_tab, + button_width, + icon_size, + ) + self.create_menu_button( + "filters", + "#363636", + "", + self.controller.show_local_processing_options, + button_width, + icon_size, + ) + self.create_menu_button( + "cogs", + "#363636", + "", + self.controller.show_settings_tab, + button_width, + icon_size, + ) + + self.start_button = self.create_menu_button( + "play", + "#008000", + "Start", + self.controller.start_processing, + button_width, + icon_size, + side="right", + ) + + + def create_menu_button( + self, icon_path, bg_color, text, command, button_width, icon_size, side="left" + ): + """ + Create a button with an icon for the menu. + + Args: + icon_path (str): Path to the icon. + command (callable): The function to call when the button is pressed. + button_width (int): The width of the button. + icon_size (int): The size of the icon. + side (str): Where to place the button ('left' or 'right'). + """ + + if icon_path: + path = resource_path(icon_path) + icon_image = ctk.CTkImage( + light_image=Image.open(path), size=(icon_size, icon_size) + ) + + button = ctk.CTkButton( + self.menu_frame, + image=icon_image, + text=text, + fg_color=bg_color, + command=command, + width=button_width, + ) + button.pack(side=side, padx=5, pady=5) + return button \ No newline at end of file diff --git a/ui/preview_frame.py b/ui/preview_frame.py new file mode 100644 index 0000000..f915609 --- /dev/null +++ b/ui/preview_frame.py @@ -0,0 +1,84 @@ +import customtkinter as ctk +from PIL import Image + +class PreviewFrame: + """ + Class to handle the preview frames (Before and After) for image processing. + """ + + def __init__(self, parent): + """ + Initialize the PreviewFrame. + + Args: + parent (ctk.CTkFrame): The parent frame where the preview frames will be placed. + """ + + self.parent = parent + self.setup_ui() + + def setup_ui(self): + """ + Set up the user interface for the preview frames. + """ + row = 0 + start_row = row + # Creating the main preview frame + preview_frame = ctk.CTkFrame(self.parent, bg_color="gray35") + preview_frame.grid(row=row, column=0, columnspan=6, padx=5, pady=5, sticky="ew") + + # Ensure the preview_frame expands to the full width + preview_frame.grid_columnconfigure(0, weight=1) + preview_frame.grid_columnconfigure(1, weight=1) + + # Creating the "Before" frame + before_frame = ctk.CTkFrame(preview_frame) + before_frame.grid(row=row, column=0, padx=5, pady=5, sticky="ew") + + # Adding "Before" label and image label to the before_frame + self.before_label = ctk.CTkLabel(before_frame, text="Before:") + self.before_label.grid(row=row, column=0, padx=5, pady=5, sticky="w") + row += 1 + self.before_image_label = ctk.CTkLabel(before_frame, text="", height=175) + self.before_image_label.grid(row=row, column=0, padx=5, pady=5, sticky="w") + row += 1 + # Adding a filename label under the "Before" image label + self.before_filename_label = ctk.CTkLabel(before_frame, text="Filename") + self.before_filename_label.grid(row=row, column=0, padx=5, pady=5, sticky="w") + + # Creating the "After" frame + after_frame = ctk.CTkFrame(preview_frame) + after_frame.grid(row=start_row, column=1, padx=5, pady=5, sticky="ew") + + # Adding "After" label and image label to the after_frame + self.after_label = ctk.CTkLabel(after_frame, text="After:") + self.after_label.grid(row=start_row, column=0, padx=5, pady=5, sticky="w") + start_row += 1 + self.after_image_label = ctk.CTkLabel(after_frame, text="", height=175) + self.after_image_label.grid(row=start_row, column=0, padx=5, pady=5, sticky="w") + start_row += 1 + # Adding a filename label under the "After" image label + self.after_filename_label = ctk.CTkLabel(after_frame, text="Filename") + self.after_filename_label.grid(row=start_row, column=0, padx=5, pady=5, sticky="w") + + # def update_before_image(self, image, filename=""): + # """ + # Update the before image and filename label. + + # Args: + # image (PIL.Image): The image to display. + # filename (str): The filename to display. + # """ + # self.before_image_label.config(image=image) + # self.before_filename_label.config(text=filename) + + # def update_after_image(self, image, filename=""): + # """ + # Update the after image and filename label. + + # Args: + # image (PIL.Image): The image to display. + # filename (str): The filename to display. + # """ + # self.after_image_label.config(image=image) + # self.after_filename_label.config(text=filename) diff --git a/ui/settings_tab.py b/ui/settings_tab.py index 8baf02a..048e727 100644 --- a/ui/settings_tab.py +++ b/ui/settings_tab.py @@ -1,58 +1,234 @@ import customtkinter as ctk -from api.woocommerce_api import save_credentials, load_credentials +from api.woocommerce_api import ( + load_credentials, + save_active_credential_set, +) +from config.encrypt_config import ConfigEncryptor +from PIL import Image, ImageTk + +import os +import sys +KEY = b"u4xTBY5Ns4WYdLvqMjEr138mpMmDEhhqTszKCcDy2cI=" +def resource_path(relative_path): + """ Get the absolute path to a resource, whether we're running in development or a PyInstaller package. """ + try: + # PyInstaller stores files in _MEIPASS when built + base_path = sys._MEIPASS + except Exception: + base_path = os.path.abspath(".") + + return os.path.join(base_path, 'ui/images/'+ relative_path +'.png') class SettingsTab: - def __init__(self, tab_parent): + def __init__(self, tab_parent, controller): self.tab = ctk.CTkFrame(tab_parent) self.tab.grid(row=0, column=0, sticky="nsew") - self.credentials = load_credentials() + # Initialize an instance of ConfigEncryptor + self.config_encryptor = ConfigEncryptor(KEY) # Ensure you pass any required arguments in the constructor if necessary + config = self.config_encryptor.load_config() + self.credentials_list = [] + if config: + self.credentials_list = self.config_encryptor.load_config().get('credentials') + self.active_credential_set = ( + self.get_active_credential_set() + ) # Fetch active credentials + else: + self.active_credential_set = { + 'nice_name': "Default" + } self.inputs = {} self.setup_ui() - def setup_ui(self): - if self.credentials: - settings_options = { - "url": {"type": "text", "label": "WooCommerce URL:", "default": self.credentials.get('url', '')}, - "consumer_key": {"type": "text", "label": "Consumer Key:", "default": self.credentials.get('consumer_key', '')}, - "consumer_secret": {"type": "text", "label": "Consumer Secret:", "default": self.credentials.get('consumer_secret', '')}, - "username": {"type": "text", "label": "Username:", "default": self.credentials.get('username', '')}, - "password": {"type": "text", "label": "Password:", "default": self.credentials.get('password', ''), "show": "*"} - } - else: - settings_options = { - "url": {"type": "text", "label": "WooCommerce URL:", "default": ""}, - "consumer_key": {"type": "text", "label": "Consumer Key:", "default": ""}, - "consumer_secret": {"type": "text", "label": "Consumer Secret:", "default": ""}, - "username": {"type": "text", "label": "Username:", "default": ""}, - "password": {"type": "text", "label": "Password:", "default": "", "show": "*"} - } + def get_active_credential_set(self): + """Retrieve active credential set from saved data and convert to new format if needed.""" + if isinstance(self.credentials_list, list) and all( + isinstance(cred, dict) for cred in self.credentials_list + ): + for cred in self.credentials_list: + if cred.get("active", False): + return cred + return self.credentials_list[0] if self.credentials_list else None - row_index = 0 + elif isinstance(self.credentials_list, dict): + self.credentials_list = [self.convert_to_new_format(self.credentials_list)] + return self.credentials_list[0] + + elif isinstance(self.credentials_list, str): + self.credentials_list = [ + self.convert_to_new_format({"url": self.credentials_list}) + ] + return self.credentials_list[0] + + return None + + def convert_to_new_format(self, old_credential): + return { + "url": old_credential.get("url", ""), + "consumer_key": old_credential.get("consumer_key", ""), + "consumer_secret": old_credential.get("consumer_secret", ""), + "username": old_credential.get("username", ""), + "password": old_credential.get("password", ""), + "name": old_credential.get("name", "Default Credential Set"), + "nice_name": old_credential.get("nice_name", old_credential.get("url", "Unnamed Credential")), + "active": True, + } + + def setup_ui(self): + # Dropdown to select active credentials + self.credential_var = ctk.StringVar() + self.credential_var.set( + self.active_credential_set.get("nice_name", "Default") + ) + credential_options = [ + cred.get("nice_name", "Unnamed Credential") + for cred in self.credentials_list + ] + + dropdown_label = ctk.CTkLabel(self.tab, text="Select Active Credentials:") + dropdown_label.grid(row=0, column=0, padx=5, columnspan=2 , pady=5, sticky="w") + self.credential_dropdown = ctk.CTkComboBox( + self.tab, + variable=self.credential_var, + values=credential_options, + command=self.load_selected_credential, + ) + self.credential_dropdown.grid(row=0, column=2, columnspan=2, padx=5, pady=5, sticky="w") + + # Show fields for credentials + self.create_credentials_form(self.active_credential_set, row_index=1) + + icon_path = resource_path("save") + icon_image = ctk.CTkImage(light_image=Image.open(icon_path), size=(24, 24)) if icon_path else None + + save_button = ctk.CTkButton( + self.tab, width=100,fg_color="green",image=icon_image, text="Save", command=self.save_credentials + ) + save_button.grid(row=7, column=0, columnspan=1, pady=10) + + new_button = ctk.CTkButton( + self.tab, width=100, fg_color="green", image=icon_image, text="New", command=self.add_new_credential_set + ) + new_button.grid(row=7, column=1, columnspan=1, pady=10) + + # Trash icon for delete button + trash_icon_path = resource_path("trash") + trash_icon_image = ctk.CTkImage(light_image=Image.open(trash_icon_path), size=(24, 24)) if trash_icon_path else None + + delete_button = ctk.CTkButton( + self.tab, width=100, fg_color="red", image=trash_icon_image, text="Delete", command=self.delete_selected_credential + ) + delete_button.grid(row=7, column=2, columnspan=1, pady=10) + + def create_credentials_form(self, credentials, row_index): + settings_options = { + "url": { + "type": "text", + "label": "WooCommerce URL:", + "default": credentials.get("url", ""), + }, + "consumer_key": { + "type": "text", + "label": "Consumer Key:", + "default": credentials.get("consumer_key", ""), + }, + "consumer_secret": { + "type": "text", + "label": "Consumer Secret:", + "default": credentials.get("consumer_secret", ""), + }, + "username": { + "type": "text", + "label": "Username:", + "default": credentials.get("username", ""), + }, + "password": { + "type": "text", + "label": "Password:", + "default": credentials.get("password", ""), + "show": "*", + }, + "nice_name": { + "type": "text", + "label": "Nice Name:", + "default": credentials.get("nice_name", "Unnamed Credential"), + }, + } + + self.inputs = {} # Reset inputs for new credentials set for name, details in settings_options.items(): self.create_setting(name, details, row_index) row_index += 1 - save_button = ctk.CTkButton(self.tab, text="Save Credentials", command=self.save_credentials) - save_button.grid(row=row_index, column=0, columnspan=2, pady=10) - def create_setting(self, name, details, row_index): - """ - Create a setting based on its type. - """ lbl = ctk.CTkLabel(self.tab, text=details["label"]) - lbl.grid(row=row_index, column=0, padx=5, pady=5, sticky="w") + lbl.grid(row=row_index, column=0,columnspan=2, padx=5, pady=5, sticky="w") - if details["type"] == "text": - entry = ctk.CTkEntry(self.tab, show=details.get("show", None)) - entry.insert(0, details["default"]) - entry.grid(row=row_index, column=1, padx=5, pady=5, sticky="w") - self.inputs[name] = entry + entry = ctk.CTkEntry(self.tab, show=details.get("show", None)) + entry.insert(0, details["default"]) + entry.grid(row=row_index, column=2,columnspan=2, padx=5, pady=5, sticky="w") + self.inputs[name] = entry + + def load_selected_credential(self, selected_name): + for cred in self.credentials_list: + if cred.get("nice_name", "Unnamed Credential") == selected_name: + self.active_credential_set = cred + self.create_credentials_form(self.active_credential_set, row_index=1) + break def save_credentials(self): - save_credentials( - self.inputs["url"].get(), - self.inputs["consumer_key"].get(), - self.inputs["consumer_secret"].get(), - self.inputs["username"].get(), - self.inputs["password"].get() + credentials = { + "url": self.inputs["url"].get(), + "consumer_key": self.inputs["consumer_key"].get(), + "consumer_secret": self.inputs["consumer_secret"].get(), + "username": self.inputs["username"].get(), + "password": self.inputs["password"].get(), + "name": self.inputs["nice_name"].get(), + "nice_name": self.inputs["nice_name"].get(), + "active": True, + } + + ConfigEncryptor(KEY).save_credentials(credentials) + save_active_credential_set(credentials["name"]) + + self.credentials_list.append(credentials) + self.credential_dropdown.configure( + values=[cred.get("nice_name", "Unnamed Credential") for cred in self.credentials_list] + ) + + def add_new_credential_set(self): + self.active_credential_set = { + "url": "", + "consumer_key": "", + "consumer_secret": "", + "username": "", + "password": "", + "name": "New Credential Set", + "nice_name": "New Credential Set", + "active": False, + } + self.create_credentials_form(self.active_credential_set, row_index=1) + self.credential_var.set(self.active_credential_set["nice_name"]) + + def delete_selected_credential(self): + selected_name = self.credential_var.get() + + # Find and remove the selected credential from the list + self.credentials_list = [ + cred for cred in self.credentials_list if cred.get("nice_name") != selected_name + ] + + # Save updated credentials list to storage + ConfigEncryptor(KEY).delete_credentials(selected_name) + + # Update the dropdown and form after deletion + if self.credentials_list: + self.active_credential_set = self.credentials_list[0] # Load first available credential + self.credential_var.set(self.active_credential_set["nice_name"]) + self.create_credentials_form(self.active_credential_set, row_index=1) + else: + self.active_credential_set = {} + self.create_credentials_form({}, row_index=1) # Clear form if no credentials are left + + self.credential_dropdown.configure( + values=[cred.get("nice_name", "Unnamed Credential") for cred in self.credentials_list] ) diff --git a/utils/file_operations.py b/utils/file_operations.py index 45c4d60..45358e1 100644 --- a/utils/file_operations.py +++ b/utils/file_operations.py @@ -44,17 +44,16 @@ class FileProcessor: if self.selected_file: return self.selected_file return None - if not self.selected_directory: - return None - - for root, dirs, files in os.walk(self.selected_directory): - if "ProcessedImages" in dirs: - dirs.remove("ProcessedImages") - for file in files: - if file.lower().endswith( - (".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif") - ): - return os.path.join(root, file) + if self.selected_directory: + + for root, dirs, files in os.walk(self.selected_directory): + if "ProcessedImages" in dirs: + dirs.remove("ProcessedImages") + for file in files: + if file.lower().endswith( + (".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif") + ): + return os.path.join(root, file) return None def log_message(self, message, log=None): @@ -66,7 +65,7 @@ class FileProcessor: log (function, optional): The log function to use. Defaults to None. """ if log: - log(message) + log.log_message(message) else: print(message) @@ -77,6 +76,8 @@ class FileProcessor: Args: options (dict): Processing options. """ + if options.get("selected_directory"): + self.selected_directory = options.get("selected_directory") if not self.selected_directory: messagebox.showwarning( "No Directory", "Please select a directory.") @@ -85,8 +86,12 @@ class FileProcessor: self.log_message( f"Processing started for directory: {self.selected_directory}", log ) - - output_directory = self.create_output_directory(log) + self.log_message( + options, log + ) + output_directory = options.get('destination_path') + if not output_directory: + output_directory = self.create_output_directory(log) image_paths = self.collect_image_paths(log) self.process_images(image_paths, output_directory, options, log) @@ -106,7 +111,7 @@ class FileProcessor: str: The path to the output directory. """ output_directory = os.path.join( - self.selected_directory, "ProcessedImages") + self.selected_directory) if os.path.exists(output_directory): shutil.rmtree(output_directory) self.log_message("Existing directory removed.", log) @@ -138,7 +143,7 @@ class FileProcessor: self.log_message(f"Total images found: {len(image_paths)}", log) return image_paths - def process_images(self, image_paths, output_directory, options, log): + def process_images(self, image_paths, output_directory, options, log, product = None): """ Process each image by resizing and saving it to the output directory. @@ -147,31 +152,45 @@ class FileProcessor: output_directory (str): The path to the output directory. options (dict): Processing options. log (function): The log function to use. + + Returns: + list: A list of output image paths. """ from utils.image_processing import ImageProcessor + processed_images = [] image = ImageProcessor() image.set_background_color(options.get("background_color", "transparent")) image.set_image_size(options.get("image_size", "contain")) + image.set_canvas_size( options.get("canvas_width"), options.get("canvas_height")) format = options.get("image_format") + for file_path in image_paths: - # output_path = os.path.join( - # output_directory, os.path.relpath( - # file_path, self.selected_directory) - # ) - output_path = self.generate_output_path(output_directory, file_path, options) + output_path = self.generate_output_path(output_directory, file_path, options, product) + previews = options.get("update_previews") + previews(file_path) os.makedirs(os.path.dirname(output_path), exist_ok=True) - self.log_message(f"Running: {file_path}", log) + log.log_message(f"Running: {file_path}") + # Check if the image is JPG and set background color accordingly + if file_path.lower().endswith(".jpg") or file_path.lower().endswith(".jpeg"): + image.set_background_color("white") + else: + image.set_background_color(options.get("background_color", "transparent")) + if format == "DZI": DZI(file_path, output_path, options) else: - image.resize_image( - file_path, output_path, options - ) + image.resize_image(file_path, output_path, options) + # Collect the processed output path + processed_images.append(output_path) + previews(None, output_path) if os.path.exists(file_path) and options.get("delete_images", False): self.log_message(f"Removing: {file_path}", log) os.remove(file_path) self.log_message(f"Processed: {file_path}", log) + + return processed_images + def proces_single_image(self, options): """ @@ -229,3 +248,5 @@ class FileProcessor: return os.path.join(output_directory, new_filename + ".jpg") elif imgf == "DZI": return os.path.join(output_directory, new_filename + ".dzi") + elif imgf == "WEBP": + return os.path.join(output_directory, new_filename + ".webp") diff --git a/utils/image_processing.py b/utils/image_processing.py index b2e3581..305b23f 100644 --- a/utils/image_processing.py +++ b/utils/image_processing.py @@ -119,7 +119,7 @@ class ImageProcessor: log (function, optional): The log function to use. Defaults to None. """ if log: - log(message) + log.log_message(message) else: print(message)