Failed to save changes.
'charmap' codec can't encode characters in position 9-10: character maps to <undefined> in this code import os
import re
import tkinter as tk
from tkinter import filedialog, messagebox, ttk

class DFMModelManager:
    def __init__(self, root):
        self.root = root
        self.root.title("DFM Models Manager")
        self.models = {}
        self.dfmpath = ""
        self.create_widgets()
        self.setup_grid_weights()

    def create_widgets(self):
        # Frame for path selection
        path_frame = ttk.Frame(self.root, padding="10")
        path_frame.grid(row=0, column=0, sticky="ew")

        self.path_var = tk.StringVar()
        path_label = ttk.Label(path_frame, text="DFM Models Path:")
        path_label.pack(side=tk.LEFT, padx=(0,5))

        path_entry = ttk.Entry(path_frame, textvariable=self.path_var)
        path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0,5))

        browse_button = ttk.Button(path_frame, text="Browse", command=self.browse_path)
        browse_button.pack(side=tk.LEFT, padx=(0,5))

        load_button = ttk.Button(path_frame, text="Load Models", command=self.load_models)
        load_button.pack(side=tk.LEFT)

        # Frame for search
        search_frame = ttk.Frame(self.root, padding="10")
        search_frame.grid(row=1, column=0, sticky="ew")

        search_label = ttk.Label(search_frame, text="Search:")
        search_label.pack(side=tk.LEFT, padx=(0,5))

        self.search_var = tk.StringVar()
        search_entry = ttk.Entry(search_frame, textvariable=self.search_var)
        search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0,5))
        search_entry.bind("<KeyRelease>", self.update_search)

        clear_search_button = ttk.Button(search_frame, text="Clear", command=self.clear_search)
        clear_search_button.pack(side=tk.LEFT)

        # Notebook for tabs
        notebook = ttk.Notebook(self.root)
        notebook.grid(row=2, column=0, sticky="nsew", padx=10, pady=5)

        # Completed Models Tab
        self.completed_frame = ttk.Frame(notebook)
        notebook.add(self.completed_frame, text="Completed Models")

        self.create_treeview(self.completed_frame, is_completed=True)

        # Pending Models Tab
        self.pending_frame = ttk.Frame(notebook)
        notebook.add(self.pending_frame, text="Pending Models")

        self.create_treeview(self.pending_frame, is_completed=False)

        # Frame for updating resolution
        update_frame = ttk.Frame(self.root, padding="10")
        update_frame.grid(row=3, column=0, sticky="ew")

        size_label = ttk.Label(update_frame, text="Update Size (width, height):")
        size_label.pack(side=tk.LEFT, padx=(0,5))

        self.size_entry = ttk.Entry(update_frame)
        self.size_entry.pack(side=tk.LEFT, padx=(0,5))
        self.size_entry.insert(0, "e.g., 256, 256")  # Placeholder text
        self.size_entry.bind("<FocusIn>", self.clear_placeholder)

        update_button = ttk.Button(update_frame, text="Update Selected", command=self.update_selected)
        update_button.pack(side=tk.LEFT, padx=(5,0))

        # Frame for save button
        save_frame = ttk.Frame(self.root, padding="10")
        save_frame.grid(row=4, column=0, sticky="ew")

        save_button = ttk.Button(save_frame, text="Save Changes", command=self.save_changes)
        save_button.pack(side=tk.RIGHT)

    def clear_placeholder(self, event):
        if self.size_entry.get() == "e.g., 256, 256":
            self.size_entry.delete(0, tk.END)

    def setup_grid_weights(self):
        # Configure grid weights for responsiveness
        self.root.rowconfigure(2, weight=1)  # Notebook row
        self.root.columnconfigure(0, weight=1)  # Single column

    def create_treeview(self, parent, is_completed=True):
        columns = ("Model Name", "Hash URL", "Source URL", "Template", "Size")
        tree = ttk.Treeview(parent, columns=columns, show='headings', selectmode="extended")
        for col in columns:
            tree.heading(col, text=col)
            tree.column(col, anchor='center', width=200)
        tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=tree.yview)
        tree.configure(yscroll=scrollbar.set)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

        if is_completed:
            self.tree_completed = tree
        else:
            self.tree_pending = tree

    def browse_path(self):
        directory = filedialog.askdirectory()
        if directory:
            self.path_var.set(directory)

    def load_models(self):
        self.dfmpath = self.path_var.get()
        if not os.path.isdir(self.dfmpath):
            messagebox.showerror("Error", "Invalid directory path.")
            return

        # Clear existing models
        self.models.clear()
        for item in self.tree_completed.get_children():
            self.tree_completed.delete(item)
        for item in self.tree_pending.get_children():
            self.tree_pending.delete(item)

        # Define possible resolutions
        # Resolutions are any 3 or 4-digit numbers not followed by 'k' or 'K'
        resolution_pattern = re.compile(r'(\d{3,4})(?![kK])')

        # Uniform placeholder URLs
        placeholder_hash_url = "https://invalid-url.com/placeholder.hash"
        placeholder_source_url = "https://invalid-url.com/placeholder.dfm"

        # Walk through the directory to find .dfm files
        for root_dir, dirs, files in os.walk(self.dfmpath):
            for file in files:
                if file.lower().endswith(".dfm"):
                    full_path = os.path.join(root_dir, file)
                    relative_path = os.path.relpath(full_path, self.dfmpath)
                    # Replace backslashes with forward slashes for consistency
                    relative_path = relative_path.replace("\\", "/")
                    model_name = os.path.splitext(relative_path)[0]  # Removes .dfm extension

                    dfm_path = full_path
                    hash_file = model_name + ".hash"
                    hash_path = os.path.join(self.dfmpath, hash_file).replace("/", "\\")

                    if os.path.exists(hash_path):
                        # Attempt to detect resolution(s) from model name
                        matches = resolution_pattern.findall(model_name)
                        # Take the first detected resolution
                        if matches:
                            res_value = int(matches[0])
                            size = (res_value, res_value)
                            completed = True
                        else:
                            size = (224, 224)  # Default size if no resolution detected
                            completed = False

                        # Assign uniform placeholder URLs
                        hash_url = placeholder_hash_url
                        source_url = placeholder_source_url

                        model_key = model_name  # No 'iperov/' prefix added here

                        model_data = {
                            "hashes": {
                                "deep_swapper": {
                                    "url": hash_url,
                                    "path": hash_path
                                }
                            },
                            "sources": {
                                "deep_swapper": {
                                    "url": source_url,
                                    "path": dfm_path.replace("/", "\\")
                                }
                            },
                            "template": "arcface_128_v2",
                            "size": size,
                            "completed": completed
                        }

                        self.models[model_key] = model_data

                        # Insert into appropriate treeview
                        if completed:
                            self.tree_completed.insert("", "end", iid=model_key, values=(
                                model_key,
                                model_data["hashes"]["deep_swapper"]["url"],
                                model_data["sources"]["deep_swapper"]["url"],
                                model_data["template"],
                                model_data["size"]
                            ))
                        else:
                            self.tree_pending.insert("", "end", iid=model_key, values=(
                                model_key,
                                model_data["hashes"]["deep_swapper"]["url"],
                                model_data["sources"]["deep_swapper"]["url"],
                                model_data["template"],
                                model_data["size"]
                            ))

        if not self.models:
            messagebox.showinfo("Info", "No models found in the specified directory.")
        else:
            messagebox.showinfo("Success", f"Loaded {len(self.models)} models successfully.")

    def update_search(self, event=None):
        query = self.search_var.get().lower()
        # Clear existing treeviews
        for item in self.tree_completed.get_children():
            self.tree_completed.delete(item)
        for item in self.tree_pending.get_children():
            self.tree_pending.delete(item)

        # Re-insert based on search
        for model_key, model_data in self.models.items():
            if query in model_key.lower():
                if model_data.get("completed", False):
                    self.tree_completed.insert("", "end", iid=model_key, values=(
                        model_key,
                        model_data["hashes"]["deep_swapper"]["url"],
                        model_data["sources"]["deep_swapper"]["url"],
                        model_data["template"],
                        model_data["size"]
                    ))
                else:
                    self.tree_pending.insert("", "end", iid=model_key, values=(
                        model_key,
                        model_data["hashes"]["deep_swapper"]["url"],
                        model_data["sources"]["deep_swapper"]["url"],
                        model_data["template"],
                        model_data["size"]
                    ))

    def clear_search(self):
        self.search_var.set("")
        # Reload models to reset the treeviews
        self.load_models()

    def update_selected(self):
        # Determine which tab is currently active
        current_tab = self.get_current_tab()
        if current_tab == "Completed Models":
            tree = self.tree_completed
        else:
            tree = self.tree_pending

        selected = tree.selection()
        if not selected:
            messagebox.showwarning("Warning", "No model selected.")
            return
        new_size = self.size_entry.get().strip()
        if not new_size or new_size == "e.g., 256, 256":
            messagebox.showwarning("Warning", "Size cannot be empty.")
            return
        try:
            # Extract all numbers from the input
            numbers = re.findall(r'\d+', new_size)
            if len(numbers) != 2:
                raise ValueError
            size_tuple = tuple(map(int, numbers))
            if any(n <= 0 for n in size_tuple):
                raise ValueError
        except ValueError:
            messagebox.showerror("Error", "Invalid size format. Enter two positive integers separated by a comma (e.g., 256, 256).")
            return

        for item in selected:
            self.models[item]["size"] = size_tuple
            self.models[item]["completed"] = True  # Mark as completed

            # Update the treeview item
            # Remove from current treeview and add to Completed if not already there
            tree.delete(item)
            self.tree_completed.insert("", "end", iid=item, values=(
                item,
                self.models[item]["hashes"]["deep_swapper"]["url"],
                self.models[item]["sources"]["deep_swapper"]["url"],
                self.models[item]["template"],
                self.models[item]["size"]
            ))

        messagebox.showinfo("Success", "Selected models have been updated and moved to Completed Models.")

    def get_current_tab(self):
        # Get the name of the currently selected tab
        notebook = self.completed_frame.master  # Assuming notebook is the parent of completed_frame
        return notebook.tab(notebook.select(), "text")

    def save_changes(self):
        if not self.dfmpath:
            messagebox.showerror("Error", "DFM Models Path not set.")
            return

        # Prepare data to save with 'iperov/' prefix if present in model_key
        save_data = {}
        for model_key, model_data in self.models.items():
            # Check if 'iperov/' is part of the model_key
            if os.path.sep in model_key:
                # Replace os.path.sep with '/' for consistent formatting
                prefixed_key = model_key.replace(os.path.sep, '/')
            else:
                prefixed_key = model_key

            # Add 'iperov/' prefix only if it's already part of the relative path
            if '/' in prefixed_key and prefixed_key.startswith('iperov/'):
                final_key = prefixed_key
            elif '/' in prefixed_key and not prefixed_key.startswith('iperov/'):
                final_key = prefixed_key
            else:
                final_key = prefixed_key  # No prefix added

            save_data[final_key] = {
                "hashes": model_data["hashes"],
                "sources": model_data["sources"],
                "template": model_data["template"],
                "size": model_data["size"]
            }

        # Define the path with .txt extension to maintain Python dictionary format
        save_path = os.path.join(self.dfmpath, "updated_models.txt")
        try:
            with open(save_path, 'w') as f:
                f.write("{\n")
                for key, value in save_data.items():
                    f.write(f"    '{key}': {{\n")
                    for sub_key, sub_value in value.items():
                        if sub_key in ["hashes", "sources"]:
                            f.write(f"        '{sub_key}': {{\n")
                            for inner_key, inner_val in sub_value.items():
                                f.write(f"            '{inner_key}': '{inner_val}',\n")
                            f.write("        },\n")
                        elif sub_key == "template":
                            f.write(f"        '{sub_key}': '{value['template']}',\n")
                        elif sub_key == "size":
                            f.write(f"        '{sub_key}': {value['size']}\n")
                    f.write("    },\n")
                f.write("}\n")
            messagebox.showinfo("Success", f"Changes saved to {save_path}")
        except Exception as e:
            messagebox.showerror("Error", f"Failed to save changes.\n{e}")

def main():
    root = tk.Tk()
    root.geometry("1200x700")
    root.minsize(800, 600)
    app = DFMModelManager(root)
    root.mainloop()

if __name__ == "__main__":
    main()