diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4b39bbb..16ebb7a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -23,6 +23,7 @@ This is a small Python GUI tool for managing Android device files via ADB (Andro - Include type hints for all function parameters and return values. - Use f-strings for string formatting. - No single letter variable names except for `e` when appropriate. +- Do not recreate files I have deleted. ## UI guidelines diff --git a/src/adb_manager.py b/src/adb_manager.py index 72bd09a..ef09300 100644 --- a/src/adb_manager.py +++ b/src/adb_manager.py @@ -195,11 +195,15 @@ class ADBManager: return False # For Windows root drives, ensure proper formatting - if os.name == "nt" and len(local_path) == 3 and local_path.endswith(":\\"): - # Root drive path like C:\ - this might cause issues with ADB - self._update_status( - "Warning: Transferring to root drive. Consider using a subfolder." - ) + if os.name == "nt": + # Check if this is a root drive (like C:\, D:\, etc.) + normalized_path = os.path.abspath(local_path) + drive_root = os.path.splitdrive(normalized_path)[0] + os.sep + if normalized_path == drive_root: + # Root drive path like C:\ - this might cause issues with ADB + self._update_status( + "Warning: Transferring to root drive. Consider using a subfolder." + ) cmd = [ADB_BINARY_PATH, "pull", remote_path, local_path] @@ -315,11 +319,15 @@ class ADBManager: return False # For Windows root drives, ensure proper formatting - if os.name == "nt" and len(local_path) == 3 and local_path.endswith(":\\"): - # Root drive path like C:\ - this might cause issues with ADB - self._update_status( - "Warning: Pushing from root drive. Consider using a subfolder." - ) + if os.name == "nt": + # Check if this is a root drive (like C:\, D:\, etc.) + normalized_path = os.path.abspath(local_path) + drive_root = os.path.splitdrive(normalized_path)[0] + os.sep + if normalized_path == drive_root: + # Root drive path like C:\ - this might cause issues with ADB + self._update_status( + "Warning: Pushing from root drive. Consider using a subfolder." + ) cmd = [ADB_BINARY_PATH, "push", local_path, remote_path] diff --git a/src/gui.py b/src/gui.py deleted file mode 100644 index fe976f0..0000000 --- a/src/gui.py +++ /dev/null @@ -1,728 +0,0 @@ -""" -Android File Handler - GUI Module -Provides the user interface for the Android file transfer application. -""" - -import os -import sys -import threading -import tkinter as tk -from tkinter import messagebox, filedialog, ttk, simpledialog -from typing import Optional - -try: - # Try relative import first (when used as module) - from .adb_manager import ( - ADBManager, - is_adb_available, - ) -except ImportError: - # Fall back to direct import (when run directly) - from adb_manager import ( - ADBManager, - is_adb_available, - ) - - -class AndroidFileHandlerGUI(tk.Tk): - """Main GUI application for Android file transfers.""" - - def __init__(self): - super().__init__() - - # Initialize business logic - self.adb_manager = ADBManager() - - # Transfer tracking for thread safety - self.current_transfer_id = 0 - - # Setup UI - self._setup_ui() - self._setup_progress_handling() - self._initialize_app() - - def _setup_progress_handling(self): - """Set up progress handling callbacks.""" - # Set up ADB callbacks using our own methods for this legacy version - self.adb_manager.set_progress_callback(self.update_progress) - self.adb_manager.set_status_callback(self.set_status) - - def _setup_ui(self): - """Setup the user interface.""" - # Window configuration - self.title("Android File Handler") - self.geometry("520x320") - self.minsize(520, 320) - self.resizable(True, True) - - # Direction selection - self.direction_var = tk.StringVar(value="pull") - direction_frame = tk.Frame(self) - direction_frame.pack(anchor="w", padx=10, pady=(10, 0)) - tk.Radiobutton( - direction_frame, - text="Pull (Android → Computer)", - variable=self.direction_var, - value="pull", - ).pack(side="left") - tk.Radiobutton( - direction_frame, - text="Push (Computer → Android)", - variable=self.direction_var, - value="push", - ).pack(side="left", padx=(20, 0)) - - # Remote folder path - tk.Label(self, text="Remote folder path (Android device):").pack( - anchor="w", padx=10, pady=(10, 0) - ) - self.remote_path_var = tk.StringVar() - remote_path_frame = tk.Frame(self) - remote_path_frame.pack(fill="x", padx=10) - self.remote_path_entry = tk.Entry( - remote_path_frame, textvariable=self.remote_path_var, width=50 - ) - self.remote_path_entry.pack(side="left", fill="x", expand=True) - - # Browse button for remote path - tk.Button( - remote_path_frame, text="Browse...", command=self.browse_remote_folder - ).pack(side="right", padx=(5, 0)) - - # Local folder path - tk.Label(self, text="Local destination folder (Computer):").pack( - anchor="w", padx=10, pady=(10, 0) - ) - self.local_path_var = tk.StringVar() - local_path_frame = tk.Frame(self) - local_path_frame.pack(fill="x", padx=10) - self.local_path_entry = tk.Entry( - local_path_frame, textvariable=self.local_path_var, width=50 - ) - self.local_path_entry.pack(side="left", fill="x", expand=True) - tk.Button( - local_path_frame, text="Browse...", command=self.browse_local_folder - ).pack(side="right", padx=(5, 0)) - - # Progress bar - self.progress = ttk.Progressbar(self, orient="horizontal", mode="indeterminate") - self.progress.pack(fill="x", padx=10, pady=(20, 5)) - - # Status label - self.status_label = tk.Label(self, text="Status: Idle") - self.status_label.pack(anchor="w", padx=10, fill="x") - - # Start button - self.start_btn = tk.Button( - self, text="Start Transfer", command=self.start_transfer - ) - self.start_btn.pack(pady=10) - - # Window close protocol - self.protocol("WM_DELETE_WINDOW", self.on_close) - - def _initialize_app(self): - """Initialize the application - check ADB and device.""" - # Check adb availability - if not is_adb_available(): - self.disable_controls() - self.set_status("ADB not found locally. Downloading...") - self.update() - success = self.adb_manager.download_and_extract_adb() - if success: - self.set_status("ADB downloaded and ready.") - self.enable_controls() - else: - self.set_status( - "Failed to download ADB. Please check your internet and restart." - ) - messagebox.showerror("Error", "Failed to download ADB tools. Exiting.") - self.quit() - return - - # Check device connected - self.set_status("Checking for connected device...") - self.update() - device = self.adb_manager.check_device() - if not device: - self.disable_controls() - self.set_status( - "No device detected. Enable USB debugging and connect your device." - ) - self.show_enable_debugging_instructions() - else: - self.set_status(f"Device detected: {device}") - - def browse_remote_folder(self): - """Browse remote Android folders.""" - messagebox.showinfo( - "Feature Not Available", - "Remote folder browsing is not available in this version. Please manually enter the path." - ) - - def _show_android_filesystem_tree(self): - """Show a browsable Android folder tree for Windows users.""" - # Check if device is connected - device = self.adb_manager.check_device() - if not device: - messagebox.showerror( - "No Device", - "No Android device connected. Please connect your device and enable USB debugging.", - ) - return - - # Create browsable folder dialog - browser_window = tk.Toplevel(self) - browser_window.title("Browse Android Folders") - browser_window.geometry("500x400") - browser_window.transient(self) - browser_window.grab_set() - - tk.Label( - browser_window, - text="Browse Android device folders:", - font=("Arial", 10, "bold"), - ).pack(pady=10) - - # Create treeview for folder browsing - tree_frame = tk.Frame(browser_window) - tree_frame.pack(fill="both", expand=True, padx=10) - - # Treeview with scrollbars - tree_scroll_y = tk.Scrollbar(tree_frame) - tree_scroll_y.pack(side="right", fill="y") - - tree_scroll_x = tk.Scrollbar(tree_frame, orient="horizontal") - tree_scroll_x.pack(side="bottom", fill="x") - - tree = ttk.Treeview( - tree_frame, - yscrollcommand=tree_scroll_y.set, - xscrollcommand=tree_scroll_x.set, - ) - tree.pack(side="left", fill="both", expand=True) - - tree_scroll_y.config(command=tree.yview) - tree_scroll_x.config(command=tree.xview) - - # Current path display - path_frame = tk.Frame(browser_window) - path_frame.pack(fill="x", padx=10, pady=5) - tk.Label(path_frame, text="Current path:").pack(side="left") - current_path_var = tk.StringVar(value="/sdcard") - current_path_label = tk.Label( - path_frame, textvariable=current_path_var, font=("Courier", 9) - ) - current_path_label.pack(side="left", padx=(5, 0)) - - def load_folders_async(parent_item, path): - """Load folders asynchronously to avoid UI freezing.""" - - def load_in_thread(): - try: - # Ensure path ends with / for proper directory listing - list_path = path if path.endswith("/") else path + "/" - - # Use ls -la to get detailed listing with file type information - result = self.adb_manager.run_adb_command( - ["shell", "ls", "-la", list_path] - ) - - if not isinstance(result, tuple) or len(result) != 3: - self.after( - 0, - lambda: tree.insert( - parent_item, - "end", - text="(Error loading folders)", - values=[""], - ), - ) - return - - stdout, stderr, returncode = result - - # Update UI in main thread - always remove Loading... first - def update_tree(): - # First, remove any existing dummy children - children = tree.get_children(parent_item) - for child in children: - child_text = tree.item(child, "text") - if child_text in [ - "Loading...", - "(No Folders)", - "(Error loading folders)", - "(Permission denied)", - ]: - tree.delete(child) - - # Check for errors - if returncode != 0: - if stderr and "Permission denied" in stderr: - tree.insert( - parent_item, - "end", - text="(Permission denied)", - values=[""], - ) - else: - tree.insert( - parent_item, - "end", - text="(Error loading folders)", - values=[""], - ) - return - - if not stdout or not stdout.strip(): - tree.insert( - parent_item, "end", text="(No Folders)", values=[""] - ) - return - - # Parse ls -la output to find directories - folders = [] - lines = stdout.strip().split("\n") - for line in lines: - if line.startswith("d"): - # Extract folder name (last part after spaces) - parts = line.split() - if ( - len(parts) >= 8 - ): # Android ls -la typically has 8+ parts - folder_name = " ".join( - parts[7:] - ) # Handle names with spaces (start from column 8) - if folder_name not in [ - ".", - "..", - ] and not folder_name.startswith("."): - folders.append(folder_name) - - # Add folders to tree - if folders: - for folder in sorted(folders): - folder_path = f"{path.rstrip('/')}/{folder}" - item = tree.insert( - parent_item, - "end", - text=folder, - values=[folder_path], - ) - # Add a dummy child to make it expandable - tree.insert(item, "end", text="Loading...") - else: - # No folders found, show indicator - tree.insert( - parent_item, "end", text="(No Folders)", values=[""] - ) - - self.after(0, update_tree) - - except Exception as e: - print(f"Error loading folders from {path}: {e}") - - def error_update(): - # Remove Loading... even on error - children = tree.get_children(parent_item) - for child in children: - child_text = tree.item(child, "text") - if child_text == "Loading...": - tree.delete(child) - tree.insert( - parent_item, - "end", - text="(Error loading folders)", - values=[""], - ) - - self.after(0, error_update) - - # Run in background thread - threading.Thread(target=load_in_thread, daemon=True).start() - - def load_folders(parent_item, path): - """Load folders from Android device using ADB (legacy sync version for initial load).""" - # First, remove any existing dummy children - children = tree.get_children(parent_item) - for child in children: - child_text = tree.item(child, "text") - if child_text in [ - "Loading...", - "(No Folders)", - "(Error loading folders)", - "(Permission denied)", - ]: - tree.delete(child) - - try: - # Ensure path ends with / for proper directory listing - list_path = path if path.endswith("/") else path + "/" - - # Use ls -la to get detailed listing with file type information - result = self.adb_manager.run_adb_command( - ["shell", "ls", "-la", list_path] - ) - - if not isinstance(result, tuple) or len(result) != 3: - tree.insert( - parent_item, "end", text="(Error loading folders)", values=[""] - ) - return [] - - stdout, stderr, returncode = result - if returncode != 0: - if stderr and "Permission denied" in stderr: - tree.insert( - parent_item, "end", text="(Permission denied)", values=[""] - ) - else: - tree.insert( - parent_item, - "end", - text="(Error loading folders)", - values=[""], - ) - return [] - - if not stdout or not stdout.strip(): - tree.insert(parent_item, "end", text="(No Folders)", values=[""]) - return [] - - # Parse ls -la output to find directories - folders = [] - lines = stdout.strip().split("\n") - for line in lines: - if line.startswith("d"): - # Extract folder name (last part after spaces) - parts = line.split() - if len(parts) >= 8: # Android ls -la typically has 8+ parts - folder_name = " ".join( - parts[7:] - ) # Handle names with spaces (start from column 8) - if folder_name not in [ - ".", - "..", - ] and not folder_name.startswith("."): - folders.append(folder_name) - - # Add folders to tree - if folders: - for folder in sorted(folders): - folder_path = f"{path.rstrip('/')}/{folder}" - item = tree.insert( - parent_item, "end", text=folder, values=[folder_path] - ) - # Add a dummy child to make it expandable - tree.insert(item, "end", text="Loading...") - else: - # No folders found, show indicator - tree.insert(parent_item, "end", text="(No Folders)", values=[""]) - - return folders - except Exception as e: - print(f"Error loading folders from {path}: {e}") - tree.insert( - parent_item, "end", text="(Error loading folders)", values=[""] - ) - return [] - - def on_tree_expand(event): - """Handle tree expansion - load subfolders dynamically.""" - item = tree.selection()[0] if tree.selection() else tree.focus() - if not item: - return - - # Get the path - folder_path = ( - tree.item(item, "values")[0] if tree.item(item, "values") else None - ) - if not folder_path: # Skip items without valid paths (like "(No Folders)") - return - - current_path_var.set(folder_path) - - # Check if we need to load subfolders - children = tree.get_children(item) - has_loading = any( - tree.item(child, "text") == "Loading..." for child in children - ) - - # Only load if we have a "Loading..." placeholder - use async version - if has_loading: - load_folders_async(item, folder_path) - - def on_tree_select(event): - """Handle tree selection - update current path.""" - item = tree.selection()[0] if tree.selection() else None - if item: - folder_path = ( - tree.item(item, "values")[0] if tree.item(item, "values") else None - ) - if folder_path: # Only update if valid path - current_path_var.set(folder_path) - - # Bind events - tree.bind("<>", on_tree_expand) - tree.bind("<>", on_tree_select) - - # Determine which path to use - prefer /sdcard, fallback to /storage/emulated/0 - primary_path = "/sdcard" - fallback_path = "/storage/emulated/0" - - # Test if /sdcard is accessible (with trailing slash for directory listing) - test_result = self.adb_manager.run_adb_command( - ["shell", "ls", "-la", primary_path + "/"] - ) - if ( - isinstance(test_result, tuple) - and len(test_result) == 3 - and test_result[2] == 0 - ): - # /sdcard is accessible - android_path = primary_path - else: - # /sdcard not accessible, use fallback - android_path = fallback_path - - # Create single "Android" root item - android_item = tree.insert("", "end", text="Android", values=[android_path]) - tree.insert(android_item, "end", text="Loading...") - - # Set initial path - current_path_var.set(android_path) - - # Expand and load the Android root immediately - tree.item(android_item, open=True) - load_folders(android_item, android_path) - - # Select the Android item - tree.selection_set(android_item) - - # Buttons - button_frame = tk.Frame(browser_window) - button_frame.pack(pady=10) - - def select_current_folder(): - """Select the currently highlighted folder.""" - current_path = current_path_var.get() - if current_path and current_path.strip(): - self.remote_path_var.set(current_path) - browser_window.destroy() - else: - messagebox.showwarning("No Selection", "Please select a valid folder.") - - def enter_custom_path(): - """Allow user to enter a custom path.""" - browser_window.destroy() - custom_path = simpledialog.askstring( - "Custom Path", - "Enter custom Android folder path:", - initialvalue="/sdcard/", - ) - if custom_path: - self.remote_path_var.set(custom_path.strip()) - - tk.Button( - button_frame, text="Select This Folder", command=select_current_folder - ).pack(side="left", padx=5) - tk.Button( - button_frame, text="Enter Custom Path", command=enter_custom_path - ).pack(side="left", padx=5) - tk.Button(button_frame, text="Cancel", command=browser_window.destroy).pack( - side="left", padx=5 - ) - - def browse_local_folder(self): - """Browse for local folder.""" - folder = filedialog.askdirectory() - if folder: - self.local_path_var.set(folder) - - def disable_controls(self): - """Disable UI controls during operations.""" - self.remote_path_entry.config(state="disabled") - self.local_path_entry.config(state="disabled") - self.start_btn.config(state="disabled") - - def enable_controls(self): - """Enable UI controls after operations (thread-safe).""" - # Schedule UI update on main thread - self.after(0, self._enable_controls_ui) - - def _enable_controls_ui(self): - """Internal method to enable controls on main thread.""" - self.remote_path_entry.config(state="normal") - self.local_path_entry.config(state="normal") - self.start_btn.config(state="normal") - - def show_enable_debugging_instructions(self): - """Show instructions for enabling USB debugging.""" - msg = ( - "To enable USB debugging:\n" - "1. Open Settings → About phone.\n" - "2. Tap 'Build number' seven times to unlock Developer Options.\n" - "3. Go back to Settings → Developer Options.\n" - "4. Enable 'USB debugging'.\n" - "5. Connect your phone via USB and accept the prompt to allow debugging.\n\n" - "After enabling, restart this app." - ) - messagebox.showinfo("Enable USB Debugging", msg) - - def show_disable_debugging_reminder(self): - """Show reminder to disable USB debugging after transfer (thread-safe).""" - # Schedule UI update on main thread - self.after(0, self._show_debugging_reminder_ui) - - def _show_debugging_reminder_ui(self): - """Internal method to show debugging reminder on main thread.""" - msg = ( - "Transfer completed.\n\n" - "For security, disable USB debugging when done:\n" - "Settings → Developer Options → disable 'USB debugging'." - ) - messagebox.showinfo("Disable USB Debugging", msg) - - def start_transfer(self): - """Start the file transfer operation.""" - remote_path = self.remote_path_var.get().strip() - local_path = self.local_path_var.get().strip() - direction = self.direction_var.get() - - # Validate inputs - if not remote_path: - messagebox.showerror("Input Error", "Remote folder path cannot be empty.") - return - if not local_path or not os.path.isdir(local_path): - messagebox.showerror( - "Input Error", "Please select a valid local destination folder." - ) - return - - # Start transfer - self.current_transfer_id += 1 - transfer_id = self.current_transfer_id - - self.disable_controls() - # Reset progress bar in thread-safe way - self._update_progress_ui(0) - - if direction == "pull": - self.set_status("Starting pull transfer...") - threading.Thread( - target=self._pull_thread, - args=(remote_path, local_path, transfer_id), - daemon=True, - ).start() - elif direction == "push": - self.set_status("Starting push transfer...") - threading.Thread( - target=self._push_thread, - args=(local_path, remote_path, transfer_id), - daemon=True, - ).start() - else: - self.report_error("Invalid transfer direction selected.") - self.enable_controls() - - def _pull_thread(self, remote_path: str, local_path: str, transfer_id: int): - """Thread function for pull operations.""" - try: - # Check if this transfer is still current - if self.current_transfer_id != transfer_id: - return - - success = self.adb_manager.pull_folder(remote_path, local_path) - if success and self.current_transfer_id == transfer_id: - self.show_disable_debugging_reminder() - except Exception as e: - if self.current_transfer_id == transfer_id: - self.report_error(f"Pull operation failed: {e}") - finally: - if self.current_transfer_id == transfer_id: - self.enable_controls() - - def _push_thread(self, local_path: str, remote_path: str, transfer_id: int): - """Thread function for push operations.""" - try: - # Check if this transfer is still current - if self.current_transfer_id != transfer_id: - return - - success = self.adb_manager.push_folder(local_path, remote_path) - if success and self.current_transfer_id == transfer_id: - self.show_disable_debugging_reminder() - except Exception as e: - if self.current_transfer_id == transfer_id: - self.report_error(f"Push operation failed: {e}") - finally: - if self.current_transfer_id == transfer_id: - self.enable_controls() - - def update_progress(self, percentage: int): - """Update the progress bar (thread-safe).""" - - # Schedule UI update on main thread using a proper closure - def update_ui(): - self._update_progress_ui(percentage) - - self.after(0, update_ui) - - def _update_progress_ui(self, percentage: int): - """Internal method to update progress bar on main thread.""" - # For indeterminate mode, start/stop animation based on progress - try: - if percentage > 0 and percentage < 100: - self.progress.start(10) # Start animation - elif percentage >= 100: - self.progress.stop() # Stop animation when complete - self.update_idletasks() - except Exception as e: - print(f"[DEBUG] Error updating progress bar: {e}") - - def set_status(self, message: str): - """Update the status label (thread-safe).""" - - # Schedule UI update on main thread using a proper closure - def update_ui(): - self._set_status_ui(message) - - self.after(0, update_ui) - - def _set_status_ui(self, message: str): - """Internal method to update status on main thread.""" - self.status_label.config(text=f"Status: {message}") - self.update_idletasks() - - def report_error(self, message: str): - """Report an error to the user (thread-safe).""" - # Schedule UI update on main thread - self.after(0, lambda: self._report_error_ui(message)) - - def _report_error_ui(self, message: str): - """Internal method to report error on main thread.""" - self.status_label.config(text=f"Error: {message}") - messagebox.showerror("Error", message) - self.enable_controls() - - def on_close(self): - """Handle window close event.""" - self.destroy() - - -def main(): - """Main function to run the application.""" - if sys.platform not in ["win32", "linux"]: - root = tk.Tk() - root.withdraw() - messagebox.showerror( - "Unsupported OS", "This application only supports Windows or Linux." - ) - sys.exit(1) - - app = AndroidFileHandlerGUI() - app.mainloop() - - -if __name__ == "__main__": - main() diff --git a/src/gui/main_window.py b/src/gui/main_window.py index 9e24b5a..670d0d8 100644 --- a/src/gui/main_window.py +++ b/src/gui/main_window.py @@ -80,46 +80,55 @@ class AndroidFileHandlerGUI(tk.Tk): text="Pull (Android → Computer)", variable=self.direction_var, value="pull", + command=self._on_direction_change, ).pack(side="left") tk.Radiobutton( direction_frame, text="Push (Computer → Android)", variable=self.direction_var, value="push", + command=self._on_direction_change, ).pack(side="left", padx=(20, 0)) - # Remote folder path - tk.Label(self, text="Remote folder path (Android device):").pack( - anchor="w", padx=10, pady=(10, 0) - ) + # Create a container frame for the path sections that can be reordered + self.path_container = tk.Frame(self) + self.path_container.pack(fill="x", padx=10, pady=(10, 0)) + + # Create the Android device path section + self.android_frame = tk.Frame(self.path_container) + self.android_label = tk.Label(self.android_frame, text="Android device:") + self.android_label.pack(anchor="w") + + android_path_frame = tk.Frame(self.android_frame) + android_path_frame.pack(fill="x", pady=(0, 10)) self.remote_path_var = tk.StringVar() - remote_path_frame = tk.Frame(self) - remote_path_frame.pack(fill="x", padx=10) self.remote_path_entry = tk.Entry( - remote_path_frame, textvariable=self.remote_path_var, width=50 + android_path_frame, textvariable=self.remote_path_var, width=50 ) self.remote_path_entry.pack(side="left", fill="x", expand=True) - - # Browse button for remote path tk.Button( - remote_path_frame, text="Browse...", command=self.browse_remote_folder + android_path_frame, text="Browse...", command=self.browse_remote_folder ).pack(side="right", padx=(5, 0)) - # Local folder path - tk.Label(self, text="Local destination folder (Computer):").pack( - anchor="w", padx=10, pady=(10, 0) - ) + # Create the Computer path section + self.computer_frame = tk.Frame(self.path_container) + self.computer_label = tk.Label(self.computer_frame, text="Computer:") + self.computer_label.pack(anchor="w") + + computer_path_frame = tk.Frame(self.computer_frame) + computer_path_frame.pack(fill="x", pady=(0, 10)) self.local_path_var = tk.StringVar() - local_path_frame = tk.Frame(self) - local_path_frame.pack(fill="x", padx=10) self.local_path_entry = tk.Entry( - local_path_frame, textvariable=self.local_path_var, width=50 + computer_path_frame, textvariable=self.local_path_var, width=50 ) self.local_path_entry.pack(side="left", fill="x", expand=True) tk.Button( - local_path_frame, text="Browse...", command=self.browse_local_folder + computer_path_frame, text="Browse...", command=self.browse_local_folder ).pack(side="right", padx=(5, 0)) + # Initially arrange for pull (Android on top) + self._arrange_path_sections() + # Status label self.status_label = tk.Label(self, text="Status: Idle") self.status_label.pack(anchor="w", padx=10, fill="x", pady=(20, 5)) @@ -260,6 +269,26 @@ class AndroidFileHandlerGUI(tk.Tk): self.local_path_entry.config(state="normal") # Button state is handled by mode switching methods + def _on_direction_change(self): + """Handle radio button direction change.""" + self._arrange_path_sections() + + def _arrange_path_sections(self): + """Arrange the Android and Computer path sections based on direction.""" + # Remove both frames from container + self.android_frame.pack_forget() + self.computer_frame.pack_forget() + + direction = self.direction_var.get() + if direction == "pull": + # Pull: Android → Computer (Android on top) + self.android_frame.pack(fill="x", pady=(0, 5)) + self.computer_frame.pack(fill="x") + else: # push + # Push: Computer → Android (Computer on top) + self.computer_frame.pack(fill="x", pady=(0, 5)) + self.android_frame.pack(fill="x") + def _switch_to_recheck_mode(self): """Switch button to recheck device mode.""" self.start_btn.config(