#!/usr/bin/env python3 import os import zipfile import io import tkinter as tk from tkinter import messagebox, ttk from PIL import Image, ImageTk import threading #Place this file in vam root folder !! # Set the directory where your .var files are located. VAR_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "AddonPackages") # Maximum size for thumbnail images THUMB_MAX_WIDTH = 150 THUMB_MAX_HEIGHT = 150 # Fixed number of items per page ITEMS_PER_PAGE = 100 class VarGalleryApp(tk.Tk): def __init__(self): super().__init__() self.title(".var File Gallery") self.geometry("800x600") self.resizable(True, True) # Create a frame with a scrollbar for the gallery self.gallery_frame = ttk.Frame(self) self.gallery_frame.pack(fill=tk.BOTH, expand=True) # Add a search field and button at the top self.search_frame = ttk.Frame(self.gallery_frame) self.search_frame.pack(side="top", pady=10, fill="x") self.search_entry = ttk.Entry(self.search_frame, width=30) self.search_entry.pack(side="left", padx=5) self.search_button = ttk.Button(self.search_frame, text="Search", command=self.perform_search) self.search_button.pack(side="left", padx=5) # Bind the event to dynamically filter files self.search_entry.bind("", lambda _: self.perform_search()) # Add a "Delete Selected" button at the top self.delete_button = ttk.Button(self.gallery_frame, text="Delete Selected", command=self.delete_selected_files) self.delete_button.pack(side="top", pady=10, fill="x") # Pagination controls self.pagination_frame = ttk.Frame(self.gallery_frame) self.pagination_frame.pack(side="top", pady=10, fill="x") self.prev_button = ttk.Button(self.pagination_frame, text="Previous", command=self.go_to_previous_page, state="disabled") self.prev_button.pack(side="left", padx=5) self.page_label = ttk.Label(self.pagination_frame, text="Page 1 / 1") self.page_label.pack(side="left", padx=5) self.next_button = ttk.Button(self.pagination_frame, text="Next", command=self.go_to_next_page, state="disabled") self.next_button.pack(side="left", padx=5) # Go To Page input field and button self.goto_entry = ttk.Entry(self.pagination_frame, width=5) self.goto_entry.pack(side="left", padx=5) self.goto_button = ttk.Button(self.pagination_frame, text="Go To", command=self.go_to_page) self.goto_button.pack(side="left", padx=5) # Progress label self.progress_label = ttk.Label(self.gallery_frame, text="Loading files...") self.progress_label.pack(pady=10) # Setup canvas for scrolling self.canvas = tk.Canvas(self.gallery_frame, borderwidth=0) self.scrollbar = ttk.Scrollbar(self.gallery_frame, orient="vertical", command=self.canvas.yview) self.scrollable_frame = ttk.Frame(self.canvas) self.scrollable_frame.bind( "", lambda e: self.canvas.configure( scrollregion=self.canvas.bbox("all") ) ) self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw") self.canvas.configure(yscrollcommand=self.scrollbar.set) self.canvas.pack(side="left", fill="both", expand=True) self.scrollbar.pack(side="right", fill="y") # Dictionary to keep references to image objects (to prevent GC) self.image_refs = {} # Track the current column count self.columns = 10 # Fixed number of columns for consistent layout # Dictionary to store checkbox variables and their corresponding file names self.checkbox_vars = {} # Pagination variables self.var_files = [] self.filtered_var_files = [] # Stores the filtered list of files after search self.current_page = 1 # Start lazy loading in a background thread threading.Thread(target=self.lazy_load_gallery, daemon=True).start() # Bind mouse scroll events self.bind_mousewheel(self.canvas) def perform_search(self): """Filter the .var files based on the search query.""" query = self.search_entry.get().strip().lower() if query: # Filter files whose names contain the query self.filtered_var_files = [f for f in self.var_files if query in f.lower()] else: # If the search field is empty, show all files self.filtered_var_files = self.var_files # Reset pagination to the first page self.current_page = 1 self.load_current_page() def lazy_load_gallery(self): """Lazy load the gallery by processing files in a background thread.""" self.var_files = self.get_var_files() self.filtered_var_files = self.var_files # Initially, show all files if not self.var_files: self.after(0, self.update_progress_label, "No .var files found.") return # Load the first page self.load_current_page() def load_current_page(self): """Load the current page of .var files into the gallery.""" start_index = (self.current_page - 1) * ITEMS_PER_PAGE end_index = min(start_index + ITEMS_PER_PAGE, len(self.filtered_var_files)) page_files = self.filtered_var_files[start_index:end_index] # Clear existing widgets in the scrollable_frame for widget in self.scrollable_frame.winfo_children(): widget.destroy() self.image_refs = {} self.checkbox_vars = {} # Configure the grid layout for the scrollable_frame for col in range(self.columns): self.scrollable_frame.columnconfigure(col, weight=1) # Add images for the current page row = 0 col = 0 valid_images_count = 0 for var_file in page_files: thumb = self.extract_thumbnail(var_file) if thumb is not None: self.add_image_to_gallery(var_file, thumb, row, col) valid_images_count += 1 col += 1 if col >= self.columns: col = 0 row += 1 # Update the page label and pagination controls total_pages = max(1, (len(self.filtered_var_files) + ITEMS_PER_PAGE - 1) // ITEMS_PER_PAGE) self.page_label.config(text=f"Page {self.current_page} / {total_pages}") # Enable/disable pagination buttons if self.current_page > 1: self.prev_button.config(state="normal") else: self.prev_button.config(state="disabled") if self.current_page < total_pages: self.next_button.config(state="normal") else: self.next_button.config(state="disabled") # Hide the progress label after loading is complete self.after(0, self.update_progress_label, "") def add_image_to_gallery(self, var_file, thumb, row, col): """Add an image to the gallery.""" frame = ttk.Frame(self.scrollable_frame, borderwidth=1, relief="solid") frame.grid(row=row, column=col, padx=5, pady=5, sticky="nsew") file_label = ttk.Label(frame, text=var_file, wraplength=THUMB_MAX_WIDTH) file_label.pack(pady=(0, 5)) # Resize thumbnail to fit within max dimensions thumb.thumbnail((THUMB_MAX_WIDTH, THUMB_MAX_HEIGHT)) tk_image = ImageTk.PhotoImage(thumb) # Save reference so image isn't garbage-collected self.image_refs[var_file] = tk_image image_label = ttk.Label(frame, image=tk_image) image_label.pack() # Add a checkbox for selecting the file checkbox_var = tk.BooleanVar() checkbox = ttk.Checkbutton(frame, variable=checkbox_var) checkbox.pack(pady=(5, 0)) # Store the checkbox variable and its corresponding file name self.checkbox_vars[var_file] = checkbox_var def update_progress_label(self, text): """Update the progress label text.""" self.progress_label.config(text=text) def get_var_files(self): """ Return a list of .var file names (not full paths) within VAR_DIR and its subdirectories. """ if not os.path.isdir(VAR_DIR): os.makedirs(VAR_DIR, exist_ok=True) var_files = [] for root, _, files in os.walk(VAR_DIR): # Walk through all subdirectories for file in files: if file.lower().endswith('.var'): # Store the relative path of the file (relative to VAR_DIR) var_files.append(os.path.relpath(os.path.join(root, file), VAR_DIR)) return var_files def extract_thumbnail(self, var_filename): """ Attempt to extract the first .jpg file from the .var (zip) archive. Returns a PIL Image object if found, else None. """ full_path = os.path.join(VAR_DIR, var_filename) if not os.path.isfile(full_path): return None try: with zipfile.ZipFile(full_path, 'r') as varzip: # Look for a jpg in the archive that has "Saves/scene" in its path. candidates = [name for name in varzip.namelist() if name.lower().endswith('.jpg') and "Saves/scene" in name] if candidates: with varzip.open(candidates[0]) as img_file: data = img_file.read() image = Image.open(io.BytesIO(data)) image = image.convert("RGB") return image except Exception as e: print(f"Error reading thumbnail from {var_filename}: {e}") return None def delete_selected_files(self): """Delete all selected .var files.""" selected_files = [var_file for var_file, var in self.checkbox_vars.items() if var.get()] if not selected_files: messagebox.showinfo("No Selection", "No files selected for deletion.") return # Confirm deletion if not messagebox.askyesno("Confirm Delete", f"Are you sure you want to delete {len(selected_files)} file(s)?"): return # Delete the selected files for var_file in selected_files: full_path = os.path.join(VAR_DIR, var_file) try: os.remove(full_path) print(f"Deleted: {var_file}") except Exception as e: messagebox.showerror("Error", f"Error deleting {var_file}:\n{e}") # Refresh the gallery after deletion self.refresh_gallery() def refresh_gallery(self): """Refresh the gallery by clearing and repopulating the scrollable frame.""" self.var_files = self.get_var_files() self.filtered_var_files = self.var_files # Reset filtered files to all files # Ensure the current page is within valid bounds total_pages = max(1, (len(self.filtered_var_files) + ITEMS_PER_PAGE - 1) // ITEMS_PER_PAGE) self.current_page = max(1, min(self.current_page, total_pages)) # Load the current page self.load_current_page() def go_to_previous_page(self): """Go to the previous page.""" if self.current_page > 1: self.current_page -= 1 self.load_current_page() def go_to_next_page(self): """Go to the next page.""" total_pages = max(1, (len(self.filtered_var_files) + ITEMS_PER_PAGE - 1) // ITEMS_PER_PAGE) if self.current_page < total_pages: self.current_page += 1 self.load_current_page() def go_to_page(self): """Navigate to the page specified in the input field.""" try: target_page = int(self.goto_entry.get()) total_pages = max(1, (len(self.filtered_var_files) + ITEMS_PER_PAGE - 1) // ITEMS_PER_PAGE) if 1 <= target_page <= total_pages: self.current_page = target_page self.load_current_page() else: messagebox.showerror("Invalid Page", f"Please enter a page number between 1 and {total_pages}.") except ValueError: messagebox.showerror("Invalid Input", "Please enter a valid page number.") def bind_mousewheel(self, widget): """Bind mouse wheel events to the given widget.""" widget.bind("", self.on_mousewheel) # For Windows and macOS widget.bind("", self.on_mousewheel) # For Linux (scroll up) widget.bind("", self.on_mousewheel) # For Linux (scroll down) # Ensure the widget has focus to receive events widget.bind("", lambda _: widget.focus_set()) def on_mousewheel(self, event): """Handle mouse wheel scrolling.""" if event.num == 4 or (event.delta > 0 and event.num == 0): # Scroll up self.canvas.yview_scroll(-1, "units") elif event.num == 5 or (event.delta < 0 and event.num == 0): # Scroll down self.canvas.yview_scroll(1, "units") if __name__ == "__main__": app = VarGalleryApp() app.mainloop()