import tkinter as tk from tkinter import ttk, messagebox, filedialog, simpledialog import uuid import webbrowser import os import random import threading import time import json import subprocess # Ensure this import is present for launching external processes import shutil # Import shutil for directory removal # --- Configuration --- APP_ID = os.environ.get('__app_id', 'default-app-id') # Define the path for the local profile data file PROFILE_FILE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'chrome_profiles.json') # Define a temporary file path for atomic writes PROFILE_FILE_TEMP_PATH = PROFILE_FILE_PATH + '.tmp' # Define a base directory for Chrome profile data managed by this app # This will create a 'chrome_profile_data' folder next to your script CHROME_PROFILES_BASE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'chrome_profile_data') # --- Global Variables --- auth_user_id = None # Global variable to store the Chrome executable path CHROME_EXE_PATH = "" # Global variable to store explicitly known groups (can exist without profiles) KNOWN_GROUPS = [] # --- User Agent Simulation Data --- USER_AGENTS = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Mozilla/5.0 (Linux; Android 15; moto g - 2025 Build/UPB5.230623.007) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36", "Mozilla/5.0 (iPhone17,1; CPU iPhone OS 18_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1", "Mozilla/5.0 (iPad; CPU OS 18_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Mozilla/5.0 (Windows Phone 10.0; Android 6.0.1; Microsoft; Lumia 950 XL) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Mobile Safari/537.36 Edge/14.14393", "Mozilla/5.0 (Linux; Android 12; Redmi Note 9 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36", "Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_1 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/53.0.2785.109 Mobile/14A403 Safari/602.1", "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36", ] def get_random_user_agent(): """Returns a random user agent string.""" return random.choice(USER_AGENTS) # --- Application Data Initialization (non-blocking) --- def initialize_app_data_async(callback): """ Initializes application data (e.g., user ID) in a separate thread. Calls the callback function with (user_id) on completion. """ global auth_user_id try: auth_user_id = str(uuid.uuid4()) print("Application data initialized successfully.") time.sleep(1) callback(auth_user_id) except Exception as e: messagebox.showerror("Initialization Error", f"Failed to initialize application data: {e}") callback(None) # --- Local File Storage Operations --- def _load_app_settings(): """Loads application settings (including Chrome EXE path, profiles, and known groups) from the local JSON file.""" global CHROME_EXE_PATH, KNOWN_GROUPS settings = {"chrome_exe_path": "", "profiles": [], "known_groups": []} # Prioritize loading from the main file, but check for temp file if main is missing/corrupt file_to_load = PROFILE_FILE_PATH if not os.path.exists(PROFILE_FILE_PATH) and os.path.exists(PROFILE_FILE_TEMP_PATH): print(f"Main profile file not found, attempting to load from temporary file: {PROFILE_FILE_TEMP_PATH}") file_to_load = PROFILE_FILE_TEMP_PATH elif os.path.exists(PROFILE_FILE_PATH) and os.path.exists(PROFILE_FILE_TEMP_PATH): # If both exist, it means a previous atomic write might have failed at the rename step. # We can try to clean up the temp file or decide which one is newer/more complete. # For simplicity, we'll just use the main file if it exists. print(f"Both main and temp profile files exist. Using main: {PROFILE_FILE_PATH}") if os.path.exists(PROFILE_FILE_TEMP_PATH): try: os.remove(PROFILE_FILE_TEMP_PATH) # Clean up orphaned temp file print(f"Removed orphaned temporary file: {PROFILE_FILE_TEMP_PATH}") except Exception as e: print(f"Error removing orphaned temporary file {PROFILE_FILE_TEMP_PATH}: {e}") if os.path.exists(file_to_load): try: with open(file_to_load, 'r', encoding='utf-8') as f: loaded_data = json.load(f) if isinstance(loaded_data, dict): settings["chrome_exe_path"] = loaded_data.get("chrome_exe_path", "") settings["profiles"] = loaded_data.get("profiles", []) settings["known_groups"] = loaded_data.get("known_groups", []) elif isinstance(loaded_data, list): # Handle case where old file might just be a list of profiles settings["profiles"] = loaded_data settings["chrome_exe_path"] = "" # Reset path if format is old/unknown settings["known_groups"] = [] # Initialize empty for old format else: print(f"Warning: Unexpected data format in {file_to_load}. Starting with empty profiles and groups.") # Ensure each loaded profile has a user_data_dir, create if missing (for older saved profiles) for profile in settings["profiles"]: if "user_data_dir" not in profile: profile["user_data_dir"] = os.path.join(CHROME_PROFILES_BASE_DIR, f"profile_{profile['id']}") os.makedirs(profile["user_data_dir"], exist_ok=True) # Ensure directory exists # Also add any group from profiles to known_groups if not already present group_name = profile.get('group') if group_name and group_name not in settings["known_groups"]: settings["known_groups"].append(group_name) except json.JSONDecodeError as e: print(f"Error decoding JSON from {file_to_load}: {e}") messagebox.showwarning("Data Load Error", f"Could not read profile data from file: {e}\nStarting with empty profiles and groups.") except Exception as e: print(f"Error reading profile file {file_to_load}: {e}") messagebox.showwarning("Data Load Error", f"Could not read profile data from file: {e}\nStarting with empty profiles and groups.") CHROME_EXE_PATH = settings["chrome_exe_path"] KNOWN_GROUPS = sorted(list(set(settings["known_groups"]))) # Ensure unique and sorted return settings["profiles"] def _save_app_settings(profiles_list): """Saves application settings (including Chrome EXE path, profiles, and known groups) to the local JSON file using atomic write.""" data_to_save = { "chrome_exe_path": CHROME_EXE_PATH, "profiles": profiles_list, "known_groups": KNOWN_GROUPS # Save the known groups list } try: # Write to a temporary file first with open(PROFILE_FILE_TEMP_PATH, 'w', encoding='utf-8') as f: json.dump(data_to_save, f, indent=4) # If the write to temp file is successful, replace the main file if os.path.exists(PROFILE_FILE_PATH): os.remove(PROFILE_FILE_PATH) # Delete old file os.rename(PROFILE_FILE_TEMP_PATH, PROFILE_FILE_PATH) # Rename temp to main print(f"Profiles and settings saved to {PROFILE_FILE_PATH}") except Exception as e: print(f"Error saving profiles to {PROFILE_FILE_PATH}: {e}") messagebox.showerror("Data Save Error", f"Failed to save profiles to file: {e}") # --- In-Memory Profile Operations (now also trigger file save) --- def get_all_profiles_in_memory(profiles_list): """Fetches all profiles from the in-memory list.""" time.sleep(0.1) return list(profiles_list) def add_profile_in_memory(profiles_list, profile_data): """Adds a new profile to the in-memory list and saves to file.""" new_id = str(uuid.uuid4()) # Generate a unique user_data_dir for the new profile profile_data_with_id = { "id": new_id, "user_data_dir": os.path.join(CHROME_PROFILES_BASE_DIR, f"profile_{new_id}"), **profile_data } profiles_list.append(profile_data_with_id) os.makedirs(profile_data_with_id["user_data_dir"], exist_ok=True) # Ensure directory exists print(f"Profile '{profile_data.get('name', 'Unnamed')}' added with ID: {new_id}") # Add the new profile's group to known groups if it's new group_name = profile_data.get('group') if group_name and group_name not in KNOWN_GROUPS: KNOWN_GROUPS.append(group_name) KNOWN_GROUPS.sort() # Keep it sorted _save_app_settings(profiles_list) # Save after adding return new_id def update_profile_in_memory(profiles_list, profile_id, new_data): """Updates an existing profile in the in-memory list and saves to file.""" for i, profile in enumerate(profiles_list): if profile['id'] == profile_id: profiles_list[i].update(new_data) print(f"Profile with ID '{profile_id}' updated successfully.") # Add the updated profile's group to known groups if it's new group_name = new_data.get('group') if group_name and group_name not in KNOWN_GROUPS: KNOWN_GROUPS.append(group_name) KNOWN_GROUPS.sort() # Keep it sorted _save_app_settings(profiles_list) # Save after updating return True print(f"Profile with ID '{profile_id}' not found for update.") return False def delete_profile_in_memory(profiles_list, profile_id): """Deletes a profile from the in-memory list and saves to file.""" initial_len = len(profiles_list) profile_to_delete = next((p for p in profiles_list if p['id'] == profile_id), None) if profile_to_delete: # Optionally, remove the user data directory when deleting a profile if os.path.exists(profile_to_delete["user_data_dir"]): try: shutil.rmtree(profile_to_delete["user_data_dir"]) print(f"Removed user data directory: {profile_to_delete['user_data_dir']}") except Exception as e: print(f"Error removing user data directory {profile_to_delete['user_data_dir']}: {e}") messagebox.showwarning("Directory Deletion Error", f"Could not remove profile data directory: {e}") profiles_list[:] = [profile for profile in profiles_list if profile['id'] != profile_id] if len(profiles_list) < initial_len: print(f"Profile with ID '{profile_id}' deleted successfully.") _save_app_settings(profiles_list) # Save after deleting return True print(f"Profile with ID '{profile_id}' not found for deletion.") return False def add_group_to_known_list(group_name): """Adds a new group name to the global KNOWN_GROUPS list if it's not already present.""" global KNOWN_GROUPS if group_name and group_name not in KNOWN_GROUPS: KNOWN_GROUPS.append(group_name) KNOWN_GROUPS.sort() # Keep it sorted _save_app_settings([]) # Save just the settings (including groups), profiles don't change here # --- Tkinter GUI Application --- class ChromeProfileManagerApp: def __init__(self, root): self.root = root self.root.title("Chrome Profile Manager v1.2 (Tkinter)") self.root.geometry("900x700") self.root.resizable(True, True) self.root.state('zoomed') # Maximize the window on startup # Configure the root window's grid to expand self.root.grid_rowconfigure(0, weight=1) self.root.grid_columnconfigure(0, weight=1) self.style = ttk.Style() self.style.theme_use('clam') # Configure general button style self.style.configure('TButton', font=('Inter', 10, 'bold'), padding=8, relief='flat', borderwidth=0, foreground='black') # Default button text to black self.style.map('TButton', background=[('active', '#e0e0e0'), ('!disabled', '#dddddd')]) # Removed foreground from map # Configure specific button styles with white foreground for better contrast self.style.configure('Green.TButton', background='#4CAF50', foreground='white') self.style.map('Green.TButton', background=[('active', '#43A047'), ('!disabled', '#4CAF50')]) self.style.configure('Orange.TButton', background='#FF9800', foreground='white') self.style.map('Orange.TButton', background=[('active', '#FB8C00'), ('!disabled', '#FF9800')]) self.style.configure('Blue.TButton', background='#2196F3', foreground='white') self.style.map('Blue.TButton', background=[('active', '#1E88E5'), ('!disabled', '#2196F3')]) self.style.configure('Red.TButton', background='#F44336', foreground='white') self.style.map('Red.TButton', background=[('active', '#E53935'), ('!disabled', '#F44336')]) self.style.configure('Gray.TButton', background='#9E9E9E', foreground='white') self.style.map('Gray.TButton', background=[('active', '#757575'), ('!disabled', '#9E9E9E')]) self.style.configure('TFrame', background='#f0f2f5') self.style.configure('TLabel', background='#f0f2f5', foreground='#333', font=('Inter', 10)) self.style.configure('TEntry', fieldbackground='white', borderwidth=1, relief='solid') self.style.configure('TCheckbutton', background='#f0f2f5', foreground='#333') self.style.configure('Treeview.Heading', font=('Inter', 10, 'bold'), background='#f5f5f5', foreground='#555') self.style.configure('Treeview', font=('Inter', 10), rowheight=25, background='white', foreground='#333') self.style.map('Treeview', background=[('selected', '#A0D8F7')], foreground=[('selected', 'black')]) self.selected_profile_id = None # Load profiles and global settings from file at startup self.profiles_data = _load_app_settings() # Create widgets first so they exist when _disable_ui is called self._create_widgets() # Call _disable_ui after all widgets are created self._disable_ui() self._show_loading_screen() threading.Thread(target=initialize_app_data_async, args=(self._on_app_data_ready,)).start() def _show_loading_screen(self): self.loading_frame = ttk.Frame(self.root, padding=20) self.loading_frame.grid(row=0, column=0, sticky='nsew') # Use grid for loading frame self.loading_label = ttk.Label(self.loading_frame, text="Initializing application...", font=('Inter', 14, 'bold')) self.loading_label.pack(pady=20) self.progress_bar = ttk.Progressbar(self.loading_frame, mode='indeterminate', length=200) self.progress_bar.pack(pady=10) self.progress_bar.start() def _hide_loading_screen(self): self.progress_bar.stop() self.loading_frame.destroy() self._enable_ui() def _on_app_data_ready(self, user_id): global auth_user_id, CHROME_EXE_PATH auth_user_id = user_id if auth_user_id: self.root.after(0, self._hide_loading_screen) self.root.after(0, self.user_id_label.config, {'text': f"User ID: {auth_user_id} (Local file storage)"}) self.root.after(0, self.chrome_exe_path_entry.delete, 0, tk.END) self.root.after(0, self.chrome_exe_path_entry.insert, 0, CHROME_EXE_PATH) self.root.after(0, self.load_profiles) else: self.root.after(0, self._hide_loading_screen) messagebox.showerror("Initialization Failed", "Failed to initialize application data.") self._disable_ui() def _disable_ui(self): # List of widget attribute names to process widget_names_to_process = [ 'profile_name_entry', 'proxy_entry', 'startup_url_entry', 'num_profile_combo', 'create_button', 'incognito_check', 'user_agent_check', 'open_browser_button', 'update_profile_details_button', 'delete_button', 'refresh_button', 'folder_button', 'profile_tree', 'chrome_exe_path_entry', 'browse_chrome_exe_button', 'group_name_entry', 'assign_to_group_button', 'group_filter_combo', 'delete_group_button', # Added delete_group_button 'add_group_button', # Added add_group_button 'recover_profiles_button', # Added recover_profiles_button 'backup_button', # Added backup_button 'note_entry' # Added note_entry ] for widget_name in widget_names_to_process: widget = getattr(self, widget_name, None) # Get widget by name, return None if not found if widget: # Check if the widget attribute exists try: if isinstance(widget, ttk.Treeview): widget.config(selectmode='none') # Disable selection for Treeview elif hasattr(widget, 'config') and 'state' in widget.config(): widget.config(state='disabled') except tk.TclError as e: # This block catches the 'unknown option "-state"' error for widgets that don't have it. print(f"Warning: Could not set state for widget {widget_name} ({type(widget).__name__}): {e}") def _enable_ui(self): # List of widget attribute names to process widget_names_to_process = [ 'profile_name_entry', 'proxy_entry', 'startup_url_entry', 'num_profile_combo', 'create_button', 'incognito_check', 'user_agent_check', 'open_browser_button', 'update_profile_details_button', 'delete_button', 'refresh_button', 'folder_button', 'profile_tree', 'chrome_exe_path_entry', 'browse_chrome_exe_button', 'group_name_entry', 'assign_to_group_button', 'group_filter_combo', 'delete_group_button', # Added delete_group_button 'add_group_button', # Added add_group_button 'recover_profiles_button', # Added recover_profiles_button 'backup_button', # Added backup_button 'note_entry' # Added note_entry ] for widget_name in widget_names_to_process: widget = getattr(self, widget_name, None) # Get widget by name, return None if not found if widget: # Check if the widget attribute exists try: if isinstance(widget, ttk.Treeview): widget.config(selectmode='browse') # Enable selection for Treeview elif hasattr(widget, 'config') and 'state' in widget.config(): widget.config(state='!disabled') except tk.TclError as e: print(f"Warning: Could not set state for widget {widget_name} ({type(widget).__name__}): {e}") def _get_unique_groups(self): """Extracts unique group names from the current profiles data and known groups.""" all_groups = set(KNOWN_GROUPS) all_groups.update(profile.get('group', 'Default Group') for profile in self.profiles_data) return sorted(list(all_groups)) def _create_widgets(self): # Create a main container frame that will hold all other frames # This frame will be placed in the root window's grid and will expand main_container = ttk.Frame(self.root, style='TFrame') main_container.grid(row=0, column=0, sticky='nsew') # Configure main_container's grid to allow its rows to expand main_container.grid_rowconfigure(5, weight=1) # Row containing the Treeview main_container.grid_columnconfigure(0, weight=1) # Allow columns to expand # --- Frames (now children of main_container) --- header_frame = ttk.Frame(main_container, padding="15 15 15 10", style='TFrame') header_frame.grid(row=0, column=0, columnspan=2, sticky='ew') chrome_path_frame = ttk.Frame(main_container, padding="15 0 15 10", style='TFrame') chrome_path_frame.grid(row=1, column=0, columnspan=2, sticky='ew') chrome_path_frame.columnconfigure(0, weight=1) # Entry takes most space chrome_path_frame.columnconfigure(1, weight=0) # Button takes fixed space input_frame = ttk.Frame(main_container, padding="15 10", style='TFrame') input_frame.grid(row=2, column=0, columnspan=2, sticky='ew') input_frame.columnconfigure(0, weight=1) input_frame.columnconfigure(1, weight=1) action_buttons_frame = ttk.Frame(main_container, padding="10 10", style='TFrame') action_buttons_frame.grid(row=3, column=0, columnspan=2, sticky='ew') filter_frame = ttk.Frame(main_container, padding="15 5", style='TFrame') filter_frame.grid(row=4, column=0, columnspan=2, sticky='ew') tree_frame = ttk.Frame(main_container, padding="15 10", style='TFrame') tree_frame.grid(row=5, column=0, columnspan=2, sticky='nsew') # This frame expands tree_frame.grid_rowconfigure(0, weight=1) tree_frame.grid_columnconfigure(0, weight=1) # --- Widgets (assigned to their respective frames) --- # Logo (assuming a GIF file named 'logo.gif' in the same directory) # For PNG/JPG, you would need to install 'Pillow': pip install Pillow # Then use: from PIL import Image, ImageTk; self.logo_image = ImageTk.PhotoImage(Image.open("logo.png")) try: # Attempt to load a GIF logo self.logo_image = tk.PhotoImage(file="logo.gif") self.logo_label = ttk.Label(header_frame, image=self.logo_image, background='#f0f2f5') self.logo_label.pack(side='left', padx=(0, 10)) except tk.TclError: # If logo.gif is not found or is not a valid GIF, print a message print("Warning: 'logo.gif' not found or invalid. Skipping logo display.") self.logo_image = None # Ensure it's None if loading failed ttk.Label(header_frame, text="Chrome Profile Manager v1.2", font=('Inter', 16, 'bold'), background='#f0f2f5', foreground='#333').pack(side='left') # Chrome Executable Path input ttk.Label(chrome_path_frame, text="Chrome Executable Path:", style='TLabel').grid(row=0, column=0, sticky='w', pady=(0,2)) self.chrome_exe_path_entry = ttk.Entry(chrome_path_frame, width=60) self.chrome_exe_path_entry.grid(row=1, column=0, sticky='ew', padx=(0, 10), pady=(0,10)) self.browse_chrome_exe_button = ttk.Button(chrome_path_frame, text="Browse...", command=self.browse_chrome_executable, style='Blue.TButton') self.browse_chrome_exe_button.grid(row=1, column=1, sticky='e', pady=(0,10)) # Profile Name and Group Name ttk.Label(input_frame, text="Profile Name:", style='TLabel').grid(row=0, column=0, sticky='w', pady=(0,2)) self.profile_name_entry = ttk.Entry(input_frame, width=30) self.profile_name_entry.insert(0, "Gemini") self.profile_name_entry.grid(row=1, column=0, sticky='ew', padx=(0, 10), pady=(0,10)) ttk.Label(input_frame, text="Group Name (Type to Add):", style='TLabel').grid(row=0, column=1, sticky='w', pady=(0,2)) self.group_name_entry = ttk.Combobox(input_frame, width=30) self.group_name_entry.grid(row=1, column=1, sticky='ew', padx=(0, 10), pady=(0,10)) self.group_name_entry.set("Default Group") # Set a default value # Number of Profiles and Proxy ttk.Label(input_frame, text="Number of Profile:", style='TLabel').grid(row=2, column=0, sticky='w', pady=(0,2)) self.num_profile_combo = ttk.Combobox(input_frame, values=[str(i) for i in range(1, 101)], width=10) self.num_profile_combo.set("1") # Set default value to 1 self.num_profile_combo.grid(row=3, column=0, sticky='w', padx=(0, 10), pady=(0,10)) ttk.Label(input_frame, text="Proxy (Ex. 127.0.0.1:8080):", style='TLabel').grid(row=2, column=1, sticky='w', pady=(0,2)) self.proxy_entry = ttk.Entry(input_frame, width=30) self.proxy_entry.grid(row=3, column=1, sticky='ew', padx=(0, 10), pady=(0,10)) # Startup URL ttk.Label(input_frame, text="Startup URL (e.g., www.google.com):", style='TLabel').grid(row=4, column=0, sticky='w', pady=(0,2)) self.startup_url_entry = ttk.Entry(input_frame, width=30) self.startup_url_entry.insert(0, "www.google.com") self.startup_url_entry.grid(row=5, column=0, sticky='ew', padx=(0, 10), pady=(0,10)) # Note Field (New) ttk.Label(input_frame, text="Note:", style='TLabel').grid(row=4, column=1, sticky='w', pady=(0,2)) self.note_entry = ttk.Entry(input_frame, width=30) self.note_entry.grid(row=5, column=1, sticky='ew', padx=(0, 10), pady=(0,10)) # Action Buttons self.create_button = ttk.Button(action_buttons_frame, text="Create Chrome", command=self.create_profile, style='Green.TButton') self.create_button.pack(side='left', padx=5, pady=5) self.incognito_var = tk.BooleanVar(value=False) self.incognito_check = ttk.Checkbutton(action_buttons_frame, text="Incognito", variable=self.incognito_var, style='TCheckbutton') self.incognito_check.pack(side='left', padx=5, pady=5) self.user_agent_var = tk.BooleanVar(value=True) self.user_agent_check = ttk.Checkbutton(action_buttons_frame, text="User-Agent", variable=self.user_agent_var, style='TCheckbutton') self.user_agent_check.pack(side='left', padx=5, pady=5) self.open_browser_button = ttk.Button(action_buttons_frame, text="Open Browser", command=self.open_browser, style='Orange.TButton') self.open_browser_button.pack(side='left', padx=5, pady=5) self.update_profile_details_button = ttk.Button(action_buttons_frame, text="Update Profile", command=self.update_profile_details, style='Blue.TButton') self.update_profile_details_button.pack(side='left', padx=5, pady=5) self.assign_to_group_button = ttk.Button(action_buttons_frame, text="Assign to Group", command=self.assign_profile_to_group, style='Blue.TButton') self.assign_to_group_button.pack(side='left', padx=5, pady=5) self.delete_button = ttk.Button(action_buttons_frame, text="Delete Profile", command=self.delete_profile, style='Red.TButton') self.delete_button.pack(side='left', padx=5, pady=5) self.delete_group_button = ttk.Button(action_buttons_frame, text="Delete Group", command=self.delete_group, style='Red.TButton') # New button self.delete_group_button.pack(side='left', padx=5, pady=5) self.add_group_button = ttk.Button(action_buttons_frame, text="Add Group", command=self.add_group_dialog, style='Green.TButton') # New Add Group button self.add_group_button.pack(side='left', padx=5, pady=5) self.refresh_button = ttk.Button(action_buttons_frame, text="Refresh", command=self.load_profiles, style='Gray.TButton') self.refresh_button.pack(side='left', padx=5, pady=5) self.folder_button = ttk.Button(action_buttons_frame, text="Folder", command=self.open_profile_folder, style='Blue.TButton') self.folder_button.pack(side='left', padx=5, pady=5) # New Recover Profiles Button self.recover_profiles_button = ttk.Button(action_buttons_frame, text="Recover Profiles", command=self.recover_profiles, style='Orange.TButton') self.recover_profiles_button.pack(side='left', padx=5, pady=5) # New Backup Profiles Button self.backup_button = ttk.Button(action_buttons_frame, text="Backup Profiles", command=self.backup_profiles_data, style='Blue.TButton') self.backup_button.pack(side='left', padx=5, pady=5) # Filter section ttk.Label(filter_frame, text="Filter by Group:", style='TLabel').pack(side='left', padx=(0, 5)) self.group_filter_combo = ttk.Combobox(filter_frame, width=25) # Instantiated here self.group_filter_combo.pack(side='left', padx=(0, 10)) self.group_filter_combo.set("All Groups") # Default filter self.group_filter_combo.bind("<>", self.load_profiles) # Reload profiles when filter changes # Profile Treeview # Added "Note" to the columns columns = ("Profile Name", "Group", "User-Agent", "Startup URL", "Proxy", "Data Directory", "Note") self.profile_tree = ttk.Treeview(tree_frame, columns=columns, show="headings", selectmode="browse") for col in columns: self.profile_tree.heading(col, text=col, anchor='w') self.profile_tree.column(col, width=150, stretch=tk.YES) self.profile_tree.grid(row=0, column=0, sticky='nsew') # Use grid for treeview inside tree_frame scrollbar = ttk.Scrollbar(tree_frame, orient="vertical", command=self.profile_tree.yview) scrollbar.grid(row=0, column=1, sticky='ns') # Use grid for scrollbar self.profile_tree.configure(yscrollcommand=scrollbar.set) self.profile_tree.bind("<>", self.on_profile_select) self.user_id_label = ttk.Label(main_container, text="User ID: Initializing...", font=('Inter', 9), background='#f0f2f5', foreground='#777') self.user_id_label.grid(row=6, column=0, columnspan=2, sticky='ew', pady=(10, 5)) # Placed in main_container's grid # Call this after both group_name_entry and group_filter_combo are initialized self._update_group_combobox_values() def _update_group_combobox_values(self): """Updates the values available in the group_name_entry and group_filter_combo comboboxes.""" unique_groups = self._get_unique_groups() # Update main group name combobox self.group_name_entry['values'] = unique_groups if not self.group_name_entry.get() and unique_groups: # Only set if current value is not in unique_groups or is empty if self.group_name_entry.get() not in unique_groups and self.group_name_entry.get() != "": pass # Keep what the user typed if it's a new group else: self.group_name_entry.set(unique_groups[0]) elif not self.group_name_entry.get(): self.group_name_entry.set("Default Group") # Update filter combobox filter_values = ["All Groups"] + unique_groups self.group_filter_combo['values'] = filter_values if self.group_filter_combo.get() not in filter_values: # Ensure selected value is still valid self.group_filter_combo.set("All Groups") def browse_chrome_executable(self): """Opens a file dialog to select the Chrome executable.""" global CHROME_EXE_PATH file_path = filedialog.askopenfilename( title="Select Chrome Executable", filetypes=[("Chrome Executable", "chrome.exe"), ("All Files", "*.*")] ) if file_path: CHROME_EXE_PATH = file_path self.chrome_exe_path_entry.delete(0, tk.END) self.chrome_exe_path_entry.insert(0, CHROME_EXE_PATH) _save_app_settings(self.profiles_data) # Save the updated path def load_profiles(self, event=None): # Added event=None to handle direct calls and event calls """Loads profiles from the in-memory list and updates the Treeview, applying filters.""" for item in self.profile_tree.get_children(): self.profile_tree.delete(item) selected_filter_group = self.group_filter_combo.get() def fetch_and_display(): current_profiles = get_all_profiles_in_memory(self.profiles_data) filtered_profiles = [] if selected_filter_group == "All Groups": filtered_profiles = current_profiles else: filtered_profiles = [p for p in current_profiles if p.get('group', 'Default Group') == selected_filter_group] if filtered_profiles: for profile in filtered_profiles: self.profile_tree.insert("", "end", iid=profile['id'], values=(profile.get('name', 'N/A'), profile.get('group', 'Default Group'), # Display group profile.get('userAgent', 'N/A'), profile.get('startupUrl', 'N/A'), profile.get('proxy', 'N/A'), profile.get('user_data_dir', 'N/A'), profile.get('note', 'N/A'))) # Display note else: self.profile_tree.insert("", "end", values=("No profiles found for this group.", "", "", "", "", "", "")) self.root.after(0, self.profile_tree.selection_remove, self.profile_tree.selection()) self.selected_profile_id = None self.root.after(0, self._update_group_combobox_values) # Refresh combobox values after loading profiles threading.Thread(target=fetch_and_display).start() def on_profile_select(self, event): """Handles selection of a profile in the Treeview.""" selected_items = self.profile_tree.selection() if selected_items: self.selected_profile_id = selected_items[0] selected_profile = next((p for p in self.profiles_data if p['id'] == self.selected_profile_id), None) if selected_profile: self.profile_name_entry.delete(0, tk.END) self.profile_name_entry.insert(0, selected_profile.get('name', '')) # Set the combobox value self.group_name_entry.set(selected_profile.get('group', 'Default Group')) self.proxy_entry.delete(0, tk.END) self.proxy_entry.insert(0, selected_profile.get('proxy', '')) self.startup_url_entry.delete(0, tk.END) self.startup_url_entry.insert(0, selected_profile.get('startupUrl', '')) self.note_entry.delete(0, tk.END) # Clear and insert note self.note_entry.insert(0, selected_profile.get('note', '')) else: self.selected_profile_id = None self.profile_name_entry.delete(0, tk.END) self.group_name_entry.set("Default Group") # Clear group name to default self.proxy_entry.delete(0, tk.END) self.startup_url_entry.delete(0, tk.END) self.note_entry.delete(0, tk.END) # Clear note def create_profile(self): """Creates a new profile and adds it to the in-memory list.""" if not auth_user_id: messagebox.showerror("Error", "Application not initialized. Please wait.") return profile_name = self.profile_name_entry.get().strip() group_name = self.group_name_entry.get().strip() or "Default Group" # Get group name from combobox startup_url = self.startup_url_entry.get().strip() or "https://www.google.com" proxy = self.proxy_entry.get().strip() use_user_agent = self.user_agent_var.get() note = self.note_entry.get().strip() # Get the note if not profile_name: messagebox.showwarning("Input Required", "Please enter a Profile Name.") return new_profile_data = { "name": profile_name, "group": group_name, # Include group in data "userAgent": get_random_user_agent() if use_user_agent else 'Default', "startupUrl": startup_url, "proxy": proxy, "note": note, # Include the note "createdAt": time.time() } def add_and_refresh(): add_profile_in_memory(self.profiles_data, new_profile_data) self.root.after(0, self.load_profiles) self.root.after(0, self._update_group_combobox_values) # Refresh combobox values after adding threading.Thread(target=add_and_refresh).start() def update_profile_details(self): """Updates details of the selected profile in the in-memory list.""" if not auth_user_id: messagebox.showerror("Error", "Application not initialized. Please wait.") return if not self.selected_profile_id: messagebox.showwarning("No Profile Selected", "Please select a profile from the list to update.") return new_name = self.profile_name_entry.get().strip() new_group = self.group_name_entry.get().strip() or "Default Group" # Get new group name from combobox new_proxy = self.proxy_entry.get().strip() new_startup_url = self.startup_url_entry.get().strip() or "https://www.google.com" new_note = self.note_entry.get().strip() # Get the updated note if not new_name: messagebox.showwarning("Input Required", "Profile Name cannot be empty.") return updated_data = { "name": new_name, "group": new_group, "proxy": new_proxy, "startupUrl": new_startup_url, "note": new_note # Include the updated note } def update_and_refresh(): update_profile_in_memory(self.profiles_data, self.selected_profile_id, updated_data) self.root.after(0, self.load_profiles) self.root.after(0, self._update_group_combobox_values) # Refresh combobox values after updating threading.Thread(target=update_and_refresh).start() def assign_profile_to_group(self): """Opens a dialog to assign the selected profile to a new group.""" if not self.selected_profile_id: messagebox.showwarning("No Profile Selected", "Please select a profile from the list to assign to a group.") return # Create a Toplevel window for group input group_dialog = tk.Toplevel(self.root) group_dialog.title("Assign to Group") group_dialog.transient(self.root) # Make it appear on top of the main window group_dialog.grab_set() # Make it modal group_dialog.geometry("350x180") # Set a fixed size dialog_frame = ttk.Frame(group_dialog, padding=10) dialog_frame.pack(padx=10, pady=10) ttk.Label(dialog_frame, text="Select or Enter Group Name:", style='TLabel').pack(pady=5) # Changed to Combobox for selecting existing groups group_combobox_dialog = ttk.Combobox(dialog_frame, width=30) group_combobox_dialog.pack(pady=5) # Populate with unique groups unique_groups = self._get_unique_groups() group_combobox_dialog['values'] = unique_groups # Pre-fill with current group if available selected_profile = next((p for p in self.profiles_data if p['id'] == self.selected_profile_id), None) if selected_profile and 'group' in selected_profile: group_combobox_dialog.set(selected_profile['group']) elif unique_groups: group_combobox_dialog.set(unique_groups[0]) # Set default to first group if profile has no group else: group_combobox_dialog.set("Default Group") # Fallback if no groups exist def on_assign(): new_group_name = group_combobox_dialog.get().strip() if not new_group_name: messagebox.showwarning("Input Required", "Group Name cannot be empty.", parent=group_dialog) return def update_and_refresh_group(): update_profile_in_memory(self.profiles_data, self.selected_profile_id, {"group": new_group_name}) self.root.after(0, self.load_profiles) self.root.after(0, self._update_group_combobox_values) # Refresh combobox values after assigning group_dialog.destroy() threading.Thread(target=update_and_refresh_group).start() def on_cancel(): group_dialog.destroy() button_frame = ttk.Frame(dialog_frame) button_frame.pack(pady=10) ttk.Button(button_frame, text="Assign", command=on_assign, style='Green.TButton').pack(side='left', padx=5) ttk.Button(button_frame, text="Cancel", command=on_cancel, style='Red.TButton').pack(side='left', padx=5) # Center the dialog on the main window self.root.update_idletasks() x = self.root.winfo_x() + (self.root.winfo_width() // 2) - (group_dialog.winfo_width() // 2) y = self.root.winfo_y() + (self.root.winfo_height() // 2) - (group_dialog.winfo_height() // 2) group_dialog.geometry(f"+{x}+{y}") self.root.wait_window(group_dialog) # Wait for the dialog to close def delete_profile(self): """Deletes the selected profile from the in-memory list.""" if not auth_user_id: messagebox.showerror("Error", "Application not initialized. Please wait.") return if not self.selected_profile_id: messagebox.showwarning("No Profile Selected", "Please select a profile from the list to delete.") return confirm = messagebox.askyesno("Confirm Deletion", "Are you sure you want to delete the selected profile? This action cannot be undone. This will also attempt to delete its associated Chrome user data directory.") if confirm: def delete_and_refresh(): delete_profile_in_memory(self.profiles_data, self.selected_profile_id) self.root.after(0, self.load_profiles) self.root.after(0, self._update_group_combobox_values) # Refresh combobox values after deleting threading.Thread(target=delete_and_refresh).start() def delete_group(self): """Deletes all profiles belonging to a selected group and their associated data.""" if not auth_user_id: messagebox.showerror("Error", "Application not initialized. Please wait.") return unique_groups = self._get_unique_groups() if not unique_groups: messagebox.showinfo("No Groups", "There are no groups to delete.") return # Create a Toplevel window for group selection group_delete_dialog = tk.Toplevel(self.root) group_delete_dialog.title("Delete Group") group_delete_dialog.transient(self.root) group_delete_dialog.grab_set() group_delete_dialog.geometry("400x200") # Set a fixed size dialog_frame = ttk.Frame(group_delete_dialog, padding=10) dialog_frame.pack(padx=10, pady=10) ttk.Label(dialog_frame, text="Select Group to Delete:", style='TLabel').pack(pady=5) group_combo = ttk.Combobox(dialog_frame, values=unique_groups, width=30, state='readonly') group_combo.pack(pady=5) if unique_groups: group_combo.set(unique_groups[0]) # Pre-select the first group def on_confirm_delete_group(): selected_group = group_combo.get().strip() if not selected_group: messagebox.showwarning("No Group Selected", "Please select a group to delete.", parent=group_delete_dialog) return confirm = messagebox.askyesno("Confirm Group Deletion", f"Are you sure you want to delete the group '{selected_group}' and ALL its associated profiles and user data directories? This action cannot be undone.", parent=group_delete_dialog) if confirm: def perform_group_deletion(): global KNOWN_GROUPS profiles_to_keep = [] deleted_count = 0 for profile in self.profiles_data: if profile.get('group', 'Default Group') == selected_group: # Delete associated user data directory if os.path.exists(profile["user_data_dir"]): try: shutil.rmtree(profile["user_data_dir"]) print(f"Removed user data directory for deleted group: {profile['user_data_dir']}") except Exception as e: print(f"Error removing user data directory {profile['user_data_dir']}: {e}") self.root.after(0, messagebox.showwarning, "Directory Deletion Error", f"Could not remove profile data directory for '{profile.get('name')}': {e}") deleted_count += 1 else: profiles_to_keep.append(profile) self.profiles_data[:] = profiles_to_keep # Update the in-memory list # Remove the deleted group from KNOWN_GROUPS if selected_group in KNOWN_GROUPS: KNOWN_GROUPS.remove(selected_group) KNOWN_GROUPS.sort() # Keep it sorted _save_app_settings(self.profiles_data) # Save changes to file self.root.after(0, self.load_profiles) self.root.after(0, self._update_group_combobox_values) self.root.after(0, messagebox.showinfo, "Group Deleted", f"Group '{selected_group}' and {deleted_count} profiles deleted successfully.", parent=group_delete_dialog) group_delete_dialog.destroy() threading.Thread(target=perform_group_deletion).start() def on_cancel_delete_group(): group_delete_dialog.destroy() button_frame = ttk.Frame(dialog_frame) button_frame.pack(pady=10) ttk.Button(button_frame, text="Delete", command=on_confirm_delete_group, style='Red.TButton').pack(side='left', padx=5) ttk.Button(button_frame, text="Cancel", command=on_cancel_delete_group, style='Gray.TButton').pack(side='left', padx=5) self.root.update_idletasks() x = self.root.winfo_x() + (self.root.winfo_width() // 2) - (group_delete_dialog.winfo_width() // 2) y = self.root.winfo_y() + (self.root.winfo_height() // 2) - (group_delete_dialog.winfo_height() // 2) group_delete_dialog.geometry(f"+{x}+{y}") self.root.wait_window(group_delete_dialog) def add_group_dialog(self): """Opens a dialog to add a new group.""" group_add_dialog = tk.Toplevel(self.root) group_add_dialog.title("Add New Group") group_add_dialog.transient(self.root) group_add_dialog.geometry("350x180") # Set a fixed size # Removed grab_set() to make it non-modal dialog_frame = ttk.Frame(group_add_dialog, padding=10) dialog_frame.pack(padx=10, pady=10) ttk.Label(dialog_frame, text="Enter New Group Name:", style='TLabel').pack(pady=5) new_group_entry = ttk.Entry(dialog_frame, width=30) new_group_entry.pack(pady=5) new_group_entry.focus_set() def on_add_group(): group_name = new_group_entry.get().strip() if not group_name: messagebox.showwarning("Input Required", "Group Name cannot be empty.", parent=group_add_dialog) return if group_name in KNOWN_GROUPS: messagebox.showinfo("Group Exists", f"Group '{group_name}' already exists.", parent=group_add_dialog) group_add_dialog.destroy() return def perform_add_group(): add_group_to_known_list(group_name) self.root.after(0, self._update_group_combobox_values) self.root.after(0, messagebox.showinfo, "Group Added", f"Group '{group_name}' added successfully.", parent=group_add_dialog) group_add_dialog.destroy() threading.Thread(target=perform_add_group).start() def on_cancel_add_group(): group_add_dialog.destroy() button_frame = ttk.Frame(dialog_frame) button_frame.pack(pady=10) ttk.Button(button_frame, text="Add", command=on_add_group, style='Green.TButton').pack(side='left', padx=5) ttk.Button(button_frame, text="Cancel", command=on_cancel_add_group, style='Red.TButton').pack(side='left', padx=5) self.root.update_idletasks() x = self.root.winfo_x() + (self.root.winfo_width() // 2) - (group_add_dialog.winfo_width() // 2) y = self.root.winfo_y() + (self.root.winfo_height() // 2) - (group_add_dialog.winfo_height() // 2) group_add_dialog.geometry(f"+{x}+{y}") # Removed self.root.wait_window(group_add_dialog) to make it non-modal def open_browser(self): """ Launches Chrome with the selected profile's user data directory and startup URL. Uses subprocess to control Chrome directly. """ if not self.selected_profile_id: messagebox.showwarning("No Profile Selected", "Please select a profile from the list to open.") return if not CHROME_EXE_PATH or not os.path.exists(CHROME_EXE_PATH): messagebox.showerror("Chrome Path Error", "Chrome executable path is not set or invalid. Please set it using the 'Browse...' button.") return selected_profile = next((p for p in self.profiles_data if p['id'] == self.selected_profile_id), None) if selected_profile: startup_url = selected_profile.get('startupUrl', 'about:blank') if not startup_url.startswith('http://') and not startup_url.startswith('https://'): startup_url = 'https://' + startup_url user_data_dir = selected_profile.get('user_data_dir') if not user_data_dir: messagebox.showerror("Profile Error", "Selected profile does not have a user data directory specified.") return # Ensure the user data directory exists os.makedirs(user_data_dir, exist_ok=True) chrome_command = [ CHROME_EXE_PATH, f"--user-data-dir={user_data_dir}", "--new-window", # Open in a new window startup_url ] if self.incognito_var.get(): chrome_command.append("--incognito") try: # Use subprocess.Popen to launch Chrome without blocking the GUI subprocess.Popen(chrome_command) messagebox.showinfo("Launching Chrome", f"Launching Chrome for profile '{selected_profile.get('name')}'...\n" f"User Data Dir: {user_data_dir}\n" f"Startup URL: {startup_url}\n\n" "Please note: User-Agent and Proxy settings are managed by Chrome's profile itself, or require advanced automation libraries.") except FileNotFoundError: messagebox.showerror("Launch Error", f"Chrome executable not found at: {CHROME_EXE_PATH}\nPlease verify the path.") except Exception as e: messagebox.showerror("Launch Error", f"Failed to launch Chrome: {e}") else: messagebox.showerror("Error", "Selected profile data not found.") def open_profile_folder(self): """Opens the actual user data directory for the selected profile in the main treeview.""" if not self.selected_profile_id: messagebox.showwarning("No Profile Selected", "Please select a profile to open its folder.") return selected_profile = next((p for p in self.profiles_data if p['id'] == self.selected_profile_id), None) if selected_profile and 'user_data_dir' in selected_profile: folder_path = selected_profile['user_data_dir'] self._open_folder_in_explorer(folder_path) else: messagebox.showerror("Error", "Selected profile does not have a user data directory.") def _open_folder_in_explorer(self, folder_path): """Helper function to open a given folder path in the system's file explorer.""" if os.path.exists(folder_path): try: if os.name == 'nt': # Windows subprocess.Popen(['explorer', folder_path]) elif os.uname().sysname == 'Darwin': # macOS subprocess.Popen(['open', folder_path]) else: # Linux/Unix subprocess.Popen(['xdg-open', folder_path]) # messagebox.showinfo("Open Folder", f"Opening folder:\n{folder_path}") # Removed to avoid double message except Exception as e: messagebox.showerror("Folder Error", f"Could not open folder: {e}") else: messagebox.showwarning("Folder Not Found", f"The directory does not exist:\n{folder_path}\nIt will be created when the profile is launched for the first time.") def recover_profiles(self): """ Presents a choice to the user: scan for unlisted profile folders or load from a backup JSON file. """ choice_dialog = tk.Toplevel(self.root) choice_dialog.title("Recover Profiles Option") choice_dialog.transient(self.root) choice_dialog.grab_set() # Increased height to ensure both buttons are visible choice_dialog.geometry("350x180") choice_dialog.resizable(False, False) dialog_frame = ttk.Frame(choice_dialog, padding=20) dialog_frame.pack(expand=True, fill='both') ttk.Label(dialog_frame, text="How would you like to recover profiles?", font=('Inter', 11, 'bold'), style='TLabel').pack(pady=10) def on_scan_folders(): choice_dialog.destroy() self._scan_and_recover_folders() def on_load_backup(): choice_dialog.destroy() self._load_profiles_from_backup_file() ttk.Button(dialog_frame, text="Scan for Unlisted Folders", command=on_scan_folders, style='Green.TButton').pack(pady=5, fill='x') ttk.Button(dialog_frame, text="Load from Backup File", command=on_load_backup, style='Blue.TButton').pack(pady=5, fill='x') choice_dialog.update_idletasks() x = self.root.winfo_x() + (self.root.winfo_width() // 2) - (choice_dialog.winfo_width() // 2) y = self.root.winfo_y() + (self.root.winfo_height() // 2) - (choice_dialog.winfo_height() // 2) choice_dialog.geometry(f"+{x}+{y}") self.root.wait_window(choice_dialog) def _scan_and_recover_folders(self): """ Scans the CHROME_PROFILES_BASE_DIR for existing profile folders and allows the user to re-add them to the application's profile list if they are not already present. """ existing_profile_ids_in_app = {p['id'] for p in self.profiles_data} found_profile_folders = [] if not os.path.exists(CHROME_PROFILES_BASE_DIR): messagebox.showinfo("No Profile Data", f"The base Chrome profile data directory does not exist: {CHROME_PROFILES_BASE_DIR}") return # Scan for profile directories for item_name in os.listdir(CHROME_PROFILES_BASE_DIR): full_path = os.path.join(CHROME_PROFILES_BASE_DIR, item_name) if os.path.isdir(full_path) and item_name.startswith('profile_'): try: # Extract UUID from folder name (e.g., 'profile_UUID') profile_uuid = item_name[len('profile_'):] # Validate if it's a valid UUID uuid.UUID(profile_uuid) if profile_uuid not in existing_profile_ids_in_app: found_profile_folders.append((profile_uuid, full_path)) except ValueError: print(f"Skipping non-UUID folder in profile base dir: {item_name}") continue # Not a valid profile folder name if not found_profile_folders: messagebox.showinfo("No Profiles to Recover", "No unlisted Chrome profile data folders were found to recover.") return # Prompt user to select profiles to recover recovery_dialog = tk.Toplevel(self.root) recovery_dialog.title("Recover Profiles (Scan Folders)") recovery_dialog.transient(self.root) recovery_dialog.grab_set() recovery_dialog.geometry("700x450") # Adjusted size for more columns dialog_frame = ttk.Frame(recovery_dialog, padding=10) dialog_frame.pack(expand=True, fill='both', padx=10, pady=10) ttk.Label(dialog_frame, text="Found the following unlisted profile folders. Select which ones to re-add (double-click to open folder):", style='TLabel').pack(pady=5) # Use a Treeview for selecting profiles to recover # Updated columns for more detailed display tree_columns = ("Profile Name", "Group", "User-Agent", "Startup URL", "Proxy", "Data Directory", "Note") # Added Note recovery_tree = ttk.Treeview(dialog_frame, columns=tree_columns, show="headings", selectmode="extended") recovery_tree.heading("Profile Name", text="Profile Name") recovery_tree.heading("Group", text="Group") recovery_tree.heading("User-Agent", text="User-Agent") recovery_tree.heading("Startup URL", text="Startup URL") recovery_tree.heading("Proxy", text="Proxy") recovery_tree.heading("Data Directory", text="Data Directory") recovery_tree.heading("Note", text="Note") # Added Note heading recovery_tree.column("Profile Name", width=120, stretch=tk.YES) recovery_tree.column("Group", width=100, stretch=tk.YES) recovery_tree.column("User-Agent", width=100, stretch=tk.YES) recovery_tree.column("Startup URL", width=100, stretch=tk.YES) recovery_tree.column("Proxy", width=80, stretch=tk.YES) recovery_tree.column("Data Directory", width=150, stretch=tk.YES) recovery_tree.column("Note", width=100, stretch=tk.YES) # Added Note column width for p_uuid, p_path in found_profile_folders: # Populate with default/N/A values for recovery view recovery_tree.insert("", "end", iid=p_uuid, values=(f"Recovered Profile {p_uuid[:8]}", "Recovered Profiles", "N/A", "N/A", "N/A", p_path, "N/A")) # Added "N/A" for note recovery_tree.pack(expand=True, fill='both', pady=10) # Bind double-click event to open the folder def _on_recovered_profile_double_click(event): selected_items = recovery_tree.selection() if selected_items: item_id = selected_items[0] item_values = recovery_tree.item(item_id, 'values') folder_path = item_values[5] # Data Directory is the 6th column (index 5) self._open_folder_in_explorer(folder_path) recovery_tree.bind("", _on_recovered_profile_double_click) def perform_recovery(): selected_items = recovery_tree.selection() if not selected_items: messagebox.showwarning("No Selection", "Please select at least one profile to recover.", parent=recovery_dialog) return recovered_count = 0 for item_id in selected_items: # Get the original path from the treeview item's values (Data Directory column) item_values = recovery_tree.item(item_id, 'values') original_user_data_dir = item_values[5] # Data Directory is the 6th column (index 5) # Use the UUID from the item_id as the profile ID profile_uuid = item_id # Check if a profile with this UUID already exists in self.profiles_data # This is a double-check, as we filtered them out earlier, but good for robustness if any(p['id'] == profile_uuid for p in self.profiles_data): print(f"Profile with ID {profile_uuid} already exists in app data. Skipping recovery.") continue # Prompt for a new name for the recovered profile new_name = simpledialog.askstring("Profile Name", f"Enter a name for recovered profile '{os.path.basename(original_user_data_dir)}':", initialvalue=f"Recovered Profile {profile_uuid[:8]}", parent=recovery_dialog) if new_name is None: # User cancelled continue if not new_name.strip(): new_name = f"Recovered Profile {profile_uuid[:8]}" # Fallback if empty name recovered_profile_data = { "id": profile_uuid, # Use the existing UUID as the ID "name": new_name.strip(), "group": "Recovered Profiles", # Assign to a default group "userAgent": "Default", # Default, user can change later "startupUrl": "https://www.google.com", # Default, user can change later "proxy": "", # Default, user can change later "user_data_dir": original_user_data_dir, # Crucially, link to existing folder "note": "", # Default empty note for recovered profiles "createdAt": time.time(), "recovered": True # Mark as recovered } # Add to in-memory list and save to file self.profiles_data.append(recovered_profile_data) recovered_count += 1 if recovered_count > 0: _save_app_settings(self.profiles_data) # Save all recovered profiles self.root.after(0, self.load_profiles) # Refresh main table messagebox.showinfo("Recovery Complete", f"Successfully recovered {recovered_count} profile(s).", parent=recovery_dialog) else: messagebox.showinfo("No Profiles Recovered", "No profiles were selected or recovered.", parent=recovery_dialog) recovery_dialog.destroy() def cancel_recovery(): recovery_dialog.destroy() button_frame = ttk.Frame(dialog_frame) button_frame.pack(pady=10) ttk.Button(button_frame, text="Recover Selected", command=perform_recovery, style='Green.TButton').pack(side='left', padx=5) ttk.Button(button_frame, text="Cancel", command=cancel_recovery, style='Red.TButton').pack(side='left', padx=5) recovery_dialog.update_idletasks() x = self.root.winfo_x() + (self.root.winfo_width() // 2) - (recovery_dialog.winfo_width() // 2) y = self.root.winfo_y() + (self.root.winfo_height() // 2) - (recovery_dialog.winfo_height() // 2) recovery_dialog.geometry(f"+{x}+{y}") self.root.wait_window(recovery_dialog) def _load_profiles_from_backup_file(self): """ Allows the user to select a backup JSON file and loads profiles from it. """ file_path = filedialog.askopenfilename( title="Select Profile Backup File", filetypes=[("JSON files", "*.json"), ("All files", "*.*")] ) if not file_path: return # User cancelled try: with open(file_path, 'r', encoding='utf-8') as f: backup_data = json.load(f) profiles_from_backup = [] if isinstance(backup_data, dict) and "profiles" in backup_data: profiles_from_backup = backup_data["profiles"] elif isinstance(backup_data, list): # Handle older backups that might just be a list profiles_from_backup = backup_data else: messagebox.showerror("Invalid Backup File", "The selected file does not appear to be a valid profile backup (expected a dictionary with 'profiles' key or a list of profiles).") return if not profiles_from_backup: messagebox.showinfo("No Profiles in Backup", "The selected backup file contains no profiles.") return # Ask user how to handle existing profiles confirm_merge = messagebox.askyesno( "Merge or Replace?", "Do you want to MERGE these profiles with your existing ones, or REPLACE all existing profiles with the backup profiles?\n\n" " - Yes: Merge (add new profiles, update existing ones)\n" " - No: Replace (delete all current profiles and use only backup profiles)" ) if confirm_merge: # Merge newly_added_count = 0 updated_count = 0 for backup_profile in profiles_from_backup: # Check if profile with this ID already exists existing_profile = next((p for p in self.profiles_data if p['id'] == backup_profile.get('id')), None) if existing_profile: # Update existing profile existing_profile.update(backup_profile) updated_count += 1 print(f"Updated existing profile: {backup_profile.get('name', 'Unnamed')}") else: # Add new profile # Ensure user_data_dir is set, create if it doesn't exist if "user_data_dir" not in backup_profile: backup_profile["user_data_dir"] = os.path.join(CHROME_PROFILES_BASE_DIR, f"profile_{backup_profile['id']}") os.makedirs(backup_profile["user_data_dir"], exist_ok=True) self.profiles_data.append(backup_profile) newly_added_count += 1 print(f"Added new profile from backup: {backup_profile.get('name', 'Unnamed')}") # Ensure group is added to KNOWN_GROUPS group_name = backup_profile.get('group') if group_name and group_name not in KNOWN_GROUPS: KNOWN_GROUPS.append(group_name) KNOWN_GROUPS.sort() messagebox.showinfo("Backup Loaded", f"Merged profiles successfully:\nAdded {newly_added_count} new profiles.\nUpdated {updated_count} existing profiles.") else: # Replace # Confirm replacement, as it's destructive final_confirm = messagebox.askyesno( "Confirm Replacement", "WARNING: Replacing will DELETE ALL your current profiles and replace them with the backup. Are you absolutely sure?", icon='warning' ) if not final_confirm: messagebox.showinfo("Replacement Cancelled", "Profile replacement cancelled.") return # Clear current profiles self.profiles_data.clear() # Add profiles from backup for backup_profile in profiles_from_backup: # Ensure user_data_dir is set, create if it doesn't exist if "user_data_dir" not in backup_profile: backup_profile["user_data_dir"] = os.path.join(CHROME_PROFILES_BASE_DIR, f"profile_{backup_profile['id']}") os.makedirs(backup_profile["user_data_dir"], exist_ok=True) self.profiles_data.append(backup_profile) # Ensure group is added to KNOWN_GROUPS group_name = backup_profile.get('group') if group_name and group_name not in KNOWN_GROUPS: KNOWN_GROUPS.append(group_name) KNOWN_GROUPS.sort() messagebox.showinfo("Backup Loaded", f"Replaced all profiles with {len(profiles_from_backup)} profiles from backup.") _save_app_settings(self.profiles_data) # Save changes after merge/replace self.load_profiles() # Refresh the UI except json.JSONDecodeError as e: messagebox.showerror("File Error", f"Failed to read backup file: Invalid JSON format.\nError: {e}") except Exception as e: messagebox.showerror("File Error", f"An unexpected error occurred while loading backup: {e}") def backup_profiles_data(self): """Allows the user to select a location and save a backup of chrome_profiles.json.""" if not os.path.exists(PROFILE_FILE_PATH): messagebox.showwarning("No Data to Backup", "No profile data file (chrome_profiles.json) found to backup.") return backup_file_name = f"chrome_profiles_backup_{time.strftime('%Y%m%d_%H%M%S')}.json" file_path = filedialog.asksaveasfilename( defaultextension=".json", initialfile=backup_file_name, title="Save Profile Data Backup", filetypes=[("JSON files", "*.json"), ("All files", "*.*")] ) if file_path: try: shutil.copy(PROFILE_FILE_PATH, file_path) messagebox.showinfo("Backup Successful", f"Profile data backed up to:\n{file_path}") except Exception as e: messagebox.showerror("Backup Failed", f"Failed to create backup: {e}") if __name__ == "__main__": # Ensure the base directory for Chrome profiles exists os.makedirs(CHROME_PROFILES_BASE_DIR, exist_ok=True) root = tk.Tk() app = ChromeProfileManagerApp(root) root.mainloop()