* Elected to go with simple text animation progress symbol for the moment
* Unified around one folder browser approach * Added cancellation functionality
This commit is contained in:
@@ -26,4 +26,8 @@ This is a small Python GUI tool for managing Android device files via ADB (Andro
|
||||
|
||||
## UI guidelines
|
||||
|
||||
- Application should have a modern and clean design.
|
||||
- Application should have a modern and clean design.
|
||||
|
||||
## Runtime Instructions
|
||||
- Always run Python commands through Poetry to ensure the correct environment is used.
|
||||
- Use `poetry run python src/main.py` to start the application.
|
||||
@@ -44,6 +44,7 @@ class ADBManager:
|
||||
def __init__(self):
|
||||
self.progress_callback: Optional[Callable[[int], None]] = None
|
||||
self.status_callback: Optional[Callable[[str], None]] = None
|
||||
self.current_process: Optional[subprocess.Popen] = None
|
||||
|
||||
def set_progress_callback(self, callback: Callable[[int], None]):
|
||||
"""Set callback function for progress updates."""
|
||||
@@ -217,6 +218,7 @@ class ADBManager:
|
||||
text=True,
|
||||
bufsize=1,
|
||||
)
|
||||
self.current_process = proc
|
||||
|
||||
line_count = 0
|
||||
last_progress = 0
|
||||
@@ -280,6 +282,7 @@ class ADBManager:
|
||||
if proc.returncode == 0:
|
||||
self._update_progress(100)
|
||||
self._update_status("Transfer completed successfully.")
|
||||
self.current_process = None
|
||||
return True
|
||||
else:
|
||||
# Capture error output for better debugging
|
||||
@@ -292,10 +295,12 @@ class ADBManager:
|
||||
except:
|
||||
pass
|
||||
self._update_status(error_msg)
|
||||
self.current_process = None
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self._update_status(f"Transfer error: {e}")
|
||||
self.current_process = None
|
||||
return False
|
||||
|
||||
def push_folder(self, local_path: str, remote_path: str) -> bool:
|
||||
@@ -333,6 +338,7 @@ class ADBManager:
|
||||
text=True,
|
||||
bufsize=1,
|
||||
)
|
||||
self.current_process = proc
|
||||
except Exception as e:
|
||||
self._update_status(f"Failed to start adb: {e}")
|
||||
return False
|
||||
@@ -399,6 +405,7 @@ class ADBManager:
|
||||
if proc.returncode == 0:
|
||||
self._update_progress(100)
|
||||
self._update_status("Transfer completed successfully.")
|
||||
self.current_process = None
|
||||
return True
|
||||
else:
|
||||
# Capture error output for better debugging
|
||||
@@ -411,8 +418,31 @@ class ADBManager:
|
||||
except:
|
||||
pass
|
||||
self._update_status(error_msg)
|
||||
self.current_process = None
|
||||
return False
|
||||
|
||||
def cancel_transfer(self) -> bool:
|
||||
"""Cancel the current transfer operation."""
|
||||
if self.current_process is not None:
|
||||
try:
|
||||
# Terminate the process
|
||||
self.current_process.terminate()
|
||||
# Give it a moment to terminate gracefully
|
||||
try:
|
||||
self.current_process.wait(timeout=2)
|
||||
except subprocess.TimeoutExpired:
|
||||
# Force kill if it doesn't terminate gracefully
|
||||
self.current_process.kill()
|
||||
self.current_process.wait()
|
||||
|
||||
self.current_process = None
|
||||
self._update_status("Transfer cancelled by user")
|
||||
return True
|
||||
except Exception as e:
|
||||
self._update_status(f"Error cancelling transfer: {e}")
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
class LinuxMTPManager:
|
||||
"""Manages MTP operations on Linux systems."""
|
||||
|
||||
+23
-63
@@ -14,16 +14,12 @@ try:
|
||||
# Try relative import first (when used as module)
|
||||
from .adb_manager import (
|
||||
ADBManager,
|
||||
LinuxMTPManager,
|
||||
get_platform_type,
|
||||
is_adb_available,
|
||||
)
|
||||
except ImportError:
|
||||
# Fall back to direct import (when run directly)
|
||||
from adb_manager import (
|
||||
ADBManager,
|
||||
LinuxMTPManager,
|
||||
get_platform_type,
|
||||
is_adb_available,
|
||||
)
|
||||
|
||||
@@ -36,25 +32,25 @@ class AndroidFileHandlerGUI(tk.Tk):
|
||||
|
||||
# Initialize business logic
|
||||
self.adb_manager = ADBManager()
|
||||
self.adb_manager.set_progress_callback(self.update_progress)
|
||||
self.adb_manager.set_status_callback(self.set_status)
|
||||
|
||||
# Transfer tracking for thread safety
|
||||
self.current_transfer_id = 0
|
||||
|
||||
if get_platform_type().startswith("linux"):
|
||||
self.mtp_manager = LinuxMTPManager()
|
||||
else:
|
||||
self.mtp_manager = None
|
||||
|
||||
# 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 Folder Puller")
|
||||
self.title("Android File Handler")
|
||||
self.geometry("520x320")
|
||||
self.minsize(520, 320)
|
||||
self.resizable(True, True)
|
||||
@@ -109,7 +105,7 @@ class AndroidFileHandlerGUI(tk.Tk):
|
||||
).pack(side="right", padx=(5, 0))
|
||||
|
||||
# Progress bar
|
||||
self.progress = ttk.Progressbar(self, orient="horizontal", mode="determinate")
|
||||
self.progress = ttk.Progressbar(self, orient="horizontal", mode="indeterminate")
|
||||
self.progress.pack(fill="x", padx=10, pady=(20, 5))
|
||||
|
||||
# Status label
|
||||
@@ -158,54 +154,11 @@ class AndroidFileHandlerGUI(tk.Tk):
|
||||
self.set_status(f"Device detected: {device}")
|
||||
|
||||
def browse_remote_folder(self):
|
||||
"""Browse remote Android folders via MTP (Linux)"""
|
||||
# For Linux - use MTP browsing
|
||||
if get_platform_type().startswith("linux") and self.mtp_manager:
|
||||
# First try using existing GVFS mount
|
||||
gvfs_mount = self.mtp_manager.find_gvfs_mtp_mount()
|
||||
if gvfs_mount:
|
||||
try:
|
||||
folder = filedialog.askdirectory(
|
||||
initialdir=gvfs_mount, title="Select Android folder"
|
||||
)
|
||||
if folder:
|
||||
# Convert filesystem path back to Android path
|
||||
relative_path = os.path.relpath(folder, gvfs_mount)
|
||||
if relative_path == ".":
|
||||
android_path = "/sdcard"
|
||||
else:
|
||||
android_path = f"/sdcard/{relative_path}".replace("\\", "/")
|
||||
self.remote_path_var.set(android_path)
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"GVFS browse failed: {e}")
|
||||
|
||||
# Fallback to jmtpfs
|
||||
mount_point = self.mtp_manager.mount_mtp_device()
|
||||
if mount_point:
|
||||
try:
|
||||
folder = filedialog.askdirectory(
|
||||
initialdir=mount_point, title="Select Android folder"
|
||||
)
|
||||
if folder:
|
||||
# Convert filesystem path back to Android path
|
||||
relative_path = os.path.relpath(folder, mount_point)
|
||||
if relative_path == ".":
|
||||
android_path = "/sdcard"
|
||||
else:
|
||||
android_path = f"/sdcard/{relative_path}".replace("\\", "/")
|
||||
self.remote_path_var.set(android_path)
|
||||
finally:
|
||||
self.mtp_manager.unmount_mtp_device()
|
||||
else:
|
||||
messagebox.showerror(
|
||||
"Error",
|
||||
"Could not mount Android device via MTP. "
|
||||
"Make sure it's connected and set to 'File Transfer' mode.",
|
||||
)
|
||||
else:
|
||||
# For Windows - show common Android paths dialog
|
||||
self._show_android_filesystem_tree()
|
||||
"""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."""
|
||||
@@ -717,8 +670,15 @@ class AndroidFileHandlerGUI(tk.Tk):
|
||||
|
||||
def _update_progress_ui(self, percentage: int):
|
||||
"""Internal method to update progress bar on main thread."""
|
||||
self.progress["value"] = percentage
|
||||
self.update_idletasks()
|
||||
# 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)."""
|
||||
|
||||
+2
-6
@@ -4,14 +4,10 @@ Provides modular GUI components for the Android file transfer application.
|
||||
"""
|
||||
|
||||
from .main_window import AndroidFileHandlerGUI, main
|
||||
from .progress_handler import ProgressHandler
|
||||
from .windows_browser import WindowsAndroidBrowser
|
||||
from .linux_browser import LinuxAndroidBrowser
|
||||
from .file_browser import AndroidFileBrowser
|
||||
|
||||
__all__ = [
|
||||
"AndroidFileHandlerGUI",
|
||||
"main",
|
||||
"ProgressHandler",
|
||||
"WindowsAndroidBrowser",
|
||||
"LinuxAndroidBrowser",
|
||||
"AndroidFileBrowser",
|
||||
]
|
||||
|
||||
@@ -8,7 +8,7 @@ import tkinter as tk
|
||||
from tkinter import messagebox, ttk
|
||||
|
||||
|
||||
class WindowsAndroidBrowser:
|
||||
class AndroidFileBrowser:
|
||||
"""Android filesystem browser for Windows using ADB."""
|
||||
|
||||
def __init__(self, parent_window, adb_manager, remote_path_var):
|
||||
@@ -1,68 +0,0 @@
|
||||
"""
|
||||
Linux Android Browser Module
|
||||
Provides Android filesystem browsing capabilities for Linux using MTP.
|
||||
"""
|
||||
|
||||
import os
|
||||
from tkinter import messagebox, filedialog
|
||||
|
||||
|
||||
class LinuxAndroidBrowser:
|
||||
"""Android filesystem browser for Linux using MTP."""
|
||||
|
||||
def __init__(self, parent_window, mtp_manager, remote_path_var):
|
||||
self.parent = parent_window
|
||||
self.mtp_manager = mtp_manager
|
||||
self.remote_path_var = remote_path_var
|
||||
|
||||
def show_browser(self):
|
||||
"""Browse remote Android folders via MTP (Linux)"""
|
||||
if not self.mtp_manager:
|
||||
messagebox.showerror(
|
||||
"Error",
|
||||
"MTP manager not available. This feature is only supported on Linux.",
|
||||
)
|
||||
return
|
||||
|
||||
# First try using existing GVFS mount
|
||||
gvfs_mount = self.mtp_manager.find_gvfs_mtp_mount()
|
||||
if gvfs_mount:
|
||||
try:
|
||||
folder = filedialog.askdirectory(
|
||||
initialdir=gvfs_mount, title="Select Android folder"
|
||||
)
|
||||
if folder:
|
||||
# Convert filesystem path back to Android path
|
||||
relative_path = os.path.relpath(folder, gvfs_mount)
|
||||
if relative_path == ".":
|
||||
android_path = "/sdcard"
|
||||
else:
|
||||
android_path = f"/sdcard/{relative_path}".replace("\\", "/")
|
||||
self.remote_path_var.set(android_path)
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"GVFS browse failed: {e}")
|
||||
|
||||
# Fallback to jmtpfs
|
||||
mount_point = self.mtp_manager.mount_mtp_device()
|
||||
if mount_point:
|
||||
try:
|
||||
folder = filedialog.askdirectory(
|
||||
initialdir=mount_point, title="Select Android folder"
|
||||
)
|
||||
if folder:
|
||||
# Convert filesystem path back to Android path
|
||||
relative_path = os.path.relpath(folder, mount_point)
|
||||
if relative_path == ".":
|
||||
android_path = "/sdcard"
|
||||
else:
|
||||
android_path = f"/sdcard/{relative_path}".replace("\\", "/")
|
||||
self.remote_path_var.set(android_path)
|
||||
finally:
|
||||
self.mtp_manager.unmount_mtp_device()
|
||||
else:
|
||||
messagebox.showerror(
|
||||
"Error",
|
||||
"Could not mount Android device via MTP. "
|
||||
"Make sure it's connected and set to 'File Transfer' mode.",
|
||||
)
|
||||
+112
-51
@@ -13,29 +13,21 @@ try:
|
||||
# Try relative import first (when used as module)
|
||||
from ..adb_manager import (
|
||||
ADBManager,
|
||||
LinuxMTPManager,
|
||||
get_platform_type,
|
||||
is_adb_available,
|
||||
)
|
||||
except ImportError:
|
||||
# Fall back to direct import (when run directly)
|
||||
from adb_manager import (
|
||||
ADBManager,
|
||||
LinuxMTPManager,
|
||||
get_platform_type,
|
||||
is_adb_available,
|
||||
)
|
||||
|
||||
try:
|
||||
# Try relative imports first
|
||||
from .progress_handler import ProgressHandler
|
||||
from .windows_browser import WindowsAndroidBrowser
|
||||
from .linux_browser import LinuxAndroidBrowser
|
||||
from .file_browser import AndroidFileBrowser
|
||||
except ImportError:
|
||||
# Fall back to direct imports
|
||||
from progress_handler import ProgressHandler
|
||||
from windows_browser import WindowsAndroidBrowser
|
||||
from linux_browser import LinuxAndroidBrowser
|
||||
from src.gui.file_browser import AndroidFileBrowser
|
||||
|
||||
|
||||
class AndroidFileHandlerGUI(tk.Tk):
|
||||
@@ -51,11 +43,6 @@ class AndroidFileHandlerGUI(tk.Tk):
|
||||
self.current_transfer_id = 0
|
||||
self.device_connected = False # Track device connection state
|
||||
|
||||
if get_platform_type().startswith("linux"):
|
||||
self.mtp_manager = LinuxMTPManager()
|
||||
else:
|
||||
self.mtp_manager = None
|
||||
|
||||
# Setup UI
|
||||
self._setup_ui()
|
||||
self._initialize_components()
|
||||
@@ -64,7 +51,7 @@ class AndroidFileHandlerGUI(tk.Tk):
|
||||
def _setup_ui(self):
|
||||
"""Setup the user interface."""
|
||||
# Window configuration
|
||||
self.title("Android Folder Puller")
|
||||
self.title("Android File Handler")
|
||||
self.geometry("520x320")
|
||||
self.minsize(520, 320)
|
||||
self.resizable(True, True)
|
||||
@@ -118,13 +105,9 @@ class AndroidFileHandlerGUI(tk.Tk):
|
||||
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="determinate")
|
||||
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")
|
||||
self.status_label.pack(anchor="w", padx=10, fill="x", pady=(20, 5))
|
||||
|
||||
# Start/Recheck button (will change based on device state)
|
||||
self.start_btn = tk.Button(
|
||||
@@ -137,34 +120,76 @@ class AndroidFileHandlerGUI(tk.Tk):
|
||||
|
||||
def _initialize_components(self):
|
||||
"""Initialize GUI components and handlers."""
|
||||
# Progress handler
|
||||
self.progress_handler = ProgressHandler(self, self.progress, self.status_label)
|
||||
# Transfer animation state
|
||||
self.transfer_animation_job = None
|
||||
self.transfer_dots = 0
|
||||
|
||||
# Set up ADB callbacks to our own methods
|
||||
self.adb_manager.set_progress_callback(self._update_progress)
|
||||
self.adb_manager.set_status_callback(self._update_status)
|
||||
|
||||
# Set up ADB callbacks
|
||||
self.adb_manager.set_progress_callback(self.progress_handler.update_progress)
|
||||
self.adb_manager.set_status_callback(self.progress_handler.set_status)
|
||||
|
||||
# Browser components
|
||||
self.windows_browser = WindowsAndroidBrowser(
|
||||
# Browser component
|
||||
self.browser = AndroidFileBrowser(
|
||||
self, self.adb_manager, self.remote_path_var
|
||||
)
|
||||
self.linux_browser = LinuxAndroidBrowser(
|
||||
self, self.mtp_manager, self.remote_path_var
|
||||
)
|
||||
|
||||
def _update_progress(self, percentage):
|
||||
"""Handle progress updates (simplified)."""
|
||||
# We ignore the percentage and just rely on status updates
|
||||
pass
|
||||
|
||||
def _update_status(self, message: str):
|
||||
"""Update status label (thread-safe)."""
|
||||
# Don't update status if transfer animation is running
|
||||
if self.transfer_animation_job is not None:
|
||||
return # Ignore ADB status updates during transfer animation
|
||||
|
||||
def update_ui():
|
||||
self.status_label.config(text=message)
|
||||
self.update_idletasks()
|
||||
|
||||
self.after(0, update_ui)
|
||||
|
||||
def _start_transfer_animation(self):
|
||||
"""Start the 'Transferring...' animation."""
|
||||
print("[DEBUG] Starting transfer animation")
|
||||
self.transfer_dots = 0
|
||||
self.transfer_animation_job = True # Mark as active before starting
|
||||
self._animate_transfer_text()
|
||||
|
||||
def _animate_transfer_text(self):
|
||||
"""Animate the transfer text with dots."""
|
||||
if self.transfer_animation_job is not None:
|
||||
dots = "." * (self.transfer_dots + 1)
|
||||
status_text = f"Transferring{dots}"
|
||||
self.status_label.config(text=status_text)
|
||||
print(f"[DEBUG] Animation update: {status_text}") # Debug output
|
||||
self.update_idletasks() # Force immediate UI update
|
||||
self.transfer_dots = (self.transfer_dots + 1) % 5 # Cycle 0-4 dots
|
||||
# Schedule next update in 500ms
|
||||
self.transfer_animation_job = self.after(500, self._animate_transfer_text)
|
||||
|
||||
def _stop_transfer_animation(self):
|
||||
"""Stop the transfer animation."""
|
||||
print("[DEBUG] Stopping transfer animation")
|
||||
if self.transfer_animation_job is not None:
|
||||
if isinstance(self.transfer_animation_job, str): # It's an after job ID
|
||||
self.after_cancel(self.transfer_animation_job)
|
||||
self.transfer_animation_job = None
|
||||
|
||||
def _initialize_app(self):
|
||||
"""Initialize the application - check ADB and device."""
|
||||
# Check adb availability
|
||||
if not is_adb_available():
|
||||
self.disable_controls()
|
||||
self.progress_handler.set_status("ADB not found locally. Downloading...")
|
||||
self._update_status("ADB not found locally. Downloading...")
|
||||
self.update()
|
||||
success = self.adb_manager.download_and_extract_adb()
|
||||
if success:
|
||||
self.progress_handler.set_status("ADB downloaded and ready.")
|
||||
self._update_status("ADB downloaded and ready.")
|
||||
self.enable_controls()
|
||||
else:
|
||||
self.progress_handler.set_status(
|
||||
self._update_status(
|
||||
"Failed to download ADB. Please check your internet and restart."
|
||||
)
|
||||
messagebox.showerror("Error", "Failed to download ADB tools. Exiting.")
|
||||
@@ -176,31 +201,26 @@ class AndroidFileHandlerGUI(tk.Tk):
|
||||
|
||||
def check_device_connection(self):
|
||||
"""Check for device connection and update UI accordingly."""
|
||||
self.progress_handler.set_status("Checking for connected device...")
|
||||
self._update_status("Checking for connected device...")
|
||||
self.update()
|
||||
device = self.adb_manager.check_device()
|
||||
if not device:
|
||||
self.device_connected = False
|
||||
self.disable_controls()
|
||||
self.progress_handler.set_status(
|
||||
self._update_status(
|
||||
"No device detected. Enable USB debugging and connect your device."
|
||||
)
|
||||
self._switch_to_recheck_mode()
|
||||
self.show_enable_debugging_instructions()
|
||||
else:
|
||||
self.device_connected = True
|
||||
self.progress_handler.set_status(f"Device detected: {device}")
|
||||
self._update_status(f"Device detected: {device}")
|
||||
self._switch_to_transfer_mode()
|
||||
self.enable_controls()
|
||||
|
||||
def browse_remote_folder(self):
|
||||
"""Browse remote Android folders."""
|
||||
if get_platform_type().startswith("linux") and self.mtp_manager:
|
||||
# Use Linux MTP browser
|
||||
self.linux_browser.show_browser()
|
||||
else:
|
||||
# Use Windows ADB browser
|
||||
self.windows_browser.show_browser()
|
||||
self.browser.show_browser()
|
||||
|
||||
def browse_local_folder(self):
|
||||
"""Browse for local folder."""
|
||||
@@ -239,6 +259,12 @@ class AndroidFileHandlerGUI(tk.Tk):
|
||||
text="Start Transfer", command=self.start_transfer, state="normal"
|
||||
)
|
||||
|
||||
def _switch_to_cancel_mode(self):
|
||||
"""Switch button to cancel transfer mode."""
|
||||
self.start_btn.config(
|
||||
text="Cancel Transfer", command=self.cancel_transfer, state="normal"
|
||||
)
|
||||
|
||||
def handle_button_click(self):
|
||||
"""Handle button click - delegates to appropriate method based on device state."""
|
||||
if self.device_connected:
|
||||
@@ -259,6 +285,36 @@ class AndroidFileHandlerGUI(tk.Tk):
|
||||
"""Perform the actual device recheck."""
|
||||
self.check_device_connection()
|
||||
|
||||
def cancel_transfer(self):
|
||||
"""Cancel the current transfer."""
|
||||
print("[DEBUG] Cancel transfer requested")
|
||||
|
||||
# Cancel the actual ADB process
|
||||
cancelled = self.adb_manager.cancel_transfer()
|
||||
|
||||
# Increment transfer ID to invalidate current transfer
|
||||
self.current_transfer_id += 1
|
||||
|
||||
# Stop animation and restore UI
|
||||
self._stop_transfer_animation()
|
||||
|
||||
# Use after() to ensure status update happens after animation stops
|
||||
def update_cancelled_status():
|
||||
if cancelled:
|
||||
self.status_label.config(text="Transfer cancelled by user.")
|
||||
else:
|
||||
self.status_label.config(text="Transfer cancellation failed.")
|
||||
self.update_idletasks()
|
||||
|
||||
self.after(0, update_cancelled_status)
|
||||
self.enable_controls()
|
||||
|
||||
# Restore proper button state
|
||||
if self.device_connected:
|
||||
self._switch_to_transfer_mode()
|
||||
else:
|
||||
self._switch_to_recheck_mode()
|
||||
|
||||
def show_enable_debugging_instructions(self):
|
||||
"""Show instructions to connect device, enable file transfer, and to enable USB debugging."""
|
||||
msg = (
|
||||
@@ -355,21 +411,18 @@ class AndroidFileHandlerGUI(tk.Tk):
|
||||
transfer_id = self.current_transfer_id
|
||||
|
||||
self.disable_controls()
|
||||
self.start_btn.config(
|
||||
state="disabled"
|
||||
) # Disable transfer button during transfer
|
||||
# Reset progress bar in thread-safe way
|
||||
self.progress_handler.reset_progress()
|
||||
# Switch button to cancel mode during transfer
|
||||
self._switch_to_cancel_mode()
|
||||
# Start the transfer animation
|
||||
self._start_transfer_animation()
|
||||
|
||||
if direction == "pull":
|
||||
self.progress_handler.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.progress_handler.set_status("Starting push transfer...")
|
||||
threading.Thread(
|
||||
target=self._push_thread,
|
||||
args=(local_path, remote_path, transfer_id),
|
||||
@@ -388,12 +441,16 @@ class AndroidFileHandlerGUI(tk.Tk):
|
||||
|
||||
success = self.adb_manager.pull_folder(remote_path, local_path)
|
||||
if success and self.current_transfer_id == transfer_id:
|
||||
self._stop_transfer_animation()
|
||||
self._update_status("Transfer completed successfully.")
|
||||
self.show_disable_debugging_reminder()
|
||||
except Exception as e:
|
||||
if self.current_transfer_id == transfer_id:
|
||||
self._stop_transfer_animation()
|
||||
self.report_error(f"Pull operation failed: {e}")
|
||||
finally:
|
||||
if self.current_transfer_id == transfer_id:
|
||||
self._stop_transfer_animation()
|
||||
self.enable_controls()
|
||||
# Restore proper button state after transfer
|
||||
self.after(0, self._restore_button_state)
|
||||
@@ -407,12 +464,16 @@ class AndroidFileHandlerGUI(tk.Tk):
|
||||
|
||||
success = self.adb_manager.push_folder(local_path, remote_path)
|
||||
if success and self.current_transfer_id == transfer_id:
|
||||
self._stop_transfer_animation()
|
||||
self._update_status("Transfer completed successfully.")
|
||||
self.show_disable_debugging_reminder()
|
||||
except Exception as e:
|
||||
if self.current_transfer_id == transfer_id:
|
||||
self._stop_transfer_animation()
|
||||
self.report_error(f"Push operation failed: {e}")
|
||||
finally:
|
||||
if self.current_transfer_id == transfer_id:
|
||||
self._stop_transfer_animation()
|
||||
self.enable_controls()
|
||||
# Restore proper button state after transfer
|
||||
self.after(0, self._restore_button_state)
|
||||
|
||||
+91
-43
@@ -11,68 +11,116 @@ class ProgressHandler:
|
||||
"""Handles progress bar updates and status messages."""
|
||||
|
||||
def __init__(
|
||||
self, parent_widget, progress_bar: ttk.Progressbar, status_label: tk.Label
|
||||
):
|
||||
self, parent_widget: tk.Widget, progress_bar: ttk.Progressbar, status_label: tk.Label
|
||||
) -> None:
|
||||
"""Initialize the progress handler."""
|
||||
self.parent = parent_widget
|
||||
self.progress_bar = progress_bar
|
||||
self.progress_bar = progress_bar # Don't call as function - it's already created
|
||||
self.status_label = status_label
|
||||
self._last_percentage: float = 0.0
|
||||
self._transfer_active: bool = False # Track if a transfer is actually active
|
||||
|
||||
def update_progress(self, percentage: int):
|
||||
"""Update the progress bar (thread-safe)."""
|
||||
def update_progress(self, bytes_transferred_or_percentage, bytes_total=None) -> None:
|
||||
"""Update the progress bar (thread-safe).
|
||||
|
||||
Args:
|
||||
bytes_transferred_or_percentage: Either bytes transferred (if bytes_total provided) or percentage (0-100)
|
||||
bytes_total: Total number of bytes to transfer (optional)
|
||||
"""
|
||||
if bytes_total is not None:
|
||||
# Called with bytes_transferred and bytes_total
|
||||
bytes_transferred = bytes_transferred_or_percentage
|
||||
if bytes_total > 0:
|
||||
percentage = (bytes_transferred / bytes_total) * 100.0
|
||||
else:
|
||||
percentage = 0.0
|
||||
else:
|
||||
# Called with just percentage
|
||||
percentage = float(bytes_transferred_or_percentage)
|
||||
|
||||
# Ensure percentage is within valid range
|
||||
percentage = max(0.0, min(100.0, percentage))
|
||||
|
||||
# Debug output for large transfer troubleshooting
|
||||
print(f"[DEBUG] Progress update called: {percentage}%")
|
||||
if bytes_total is not None:
|
||||
print(f"[DEBUG] Progress update called: {percentage:.1f}% ({bytes_transferred_or_percentage}/{bytes_total} bytes)")
|
||||
else:
|
||||
print(f"[DEBUG] Progress update called: {percentage:.1f}%")
|
||||
|
||||
# Schedule UI update on main thread using a proper closure
|
||||
def update_ui():
|
||||
def update_ui() -> None:
|
||||
self._update_progress_ui(percentage)
|
||||
|
||||
self.parent.after(0, update_ui)
|
||||
|
||||
def _update_progress_ui(self, percentage: int):
|
||||
"""Internal method to update progress bar on main thread."""
|
||||
# Ensure percentage is within valid range
|
||||
percentage = max(0, min(100, percentage))
|
||||
|
||||
def _update_progress_ui(self, percentage: float) -> None:
|
||||
"""Internal method to update progress bar on main thread.
|
||||
|
||||
Args:
|
||||
percentage: Progress percentage (0.0 to 100.0) - used for logging only in indeterminate mode
|
||||
"""
|
||||
# Debug output for large transfer troubleshooting
|
||||
current_value = self.progress_bar["value"]
|
||||
print(f"[DEBUG] UI Progress update: {current_value} -> {percentage}%")
|
||||
print(f"[DEBUG] Progress update: {percentage:.1f}% (indeterminate mode, transfer_active: {self._transfer_active})")
|
||||
|
||||
if (
|
||||
hasattr(self, "_last_percentage")
|
||||
and abs(percentage - self._last_percentage) >= 10
|
||||
):
|
||||
print(
|
||||
f"[DEBUG] Major progress jump: {self._last_percentage}% -> {percentage}%"
|
||||
)
|
||||
# Only log significant progress jumps (10% or more)
|
||||
if abs(percentage - self._last_percentage) >= 10.0:
|
||||
print(f"[DEBUG] Major progress jump: {self._last_percentage:.1f}% -> {percentage:.1f}%")
|
||||
|
||||
self._last_percentage = percentage
|
||||
|
||||
try:
|
||||
self.progress_bar["value"] = percentage
|
||||
self.progress_bar.update() # Force immediate update
|
||||
# Only start animation if we're in an active transfer and progress > 0
|
||||
if self._transfer_active and percentage > 0 and percentage < 100:
|
||||
self.progress_bar.start(10) # 10ms interval for smooth animation
|
||||
print(f"[DEBUG] Progress bar animation started")
|
||||
elif percentage >= 100:
|
||||
self.progress_bar.stop() # Stop animation when complete
|
||||
self._transfer_active = False # Transfer is done
|
||||
print(f"[DEBUG] Progress bar animation stopped (transfer complete)")
|
||||
|
||||
self.parent.update_idletasks()
|
||||
print(f"[DEBUG] Progress bar updated successfully to {percentage}%")
|
||||
except Exception as e:
|
||||
print(f"[DEBUG] Error updating progress bar: {e}")
|
||||
except Exception as exception_error:
|
||||
print(f"[DEBUG] Error updating progress bar: {exception_error}")
|
||||
|
||||
def set_status(self, message: str):
|
||||
"""Update the status label (thread-safe)."""
|
||||
def reset_progress(self) -> None:
|
||||
"""Reset progress bar to 0 (thread-safe)."""
|
||||
def update_ui() -> None:
|
||||
try:
|
||||
self.progress_bar.stop() # Stop any animation
|
||||
self._last_percentage = 0.0
|
||||
self._transfer_active = False # Not in a transfer
|
||||
print(f"[DEBUG] Progress bar reset (stopped animation)")
|
||||
self.parent.update_idletasks()
|
||||
except Exception as exception_error:
|
||||
print(f"[DEBUG] Error resetting progress bar: {exception_error}")
|
||||
|
||||
# Schedule UI update on main thread using a proper closure
|
||||
def update_ui():
|
||||
self.parent.after(0, update_ui)
|
||||
|
||||
def start_transfer(self) -> None:
|
||||
"""Mark that a transfer is starting (enables progress animation)."""
|
||||
self._transfer_active = True
|
||||
print(f"[DEBUG] Transfer marked as active")
|
||||
|
||||
def set_status(self, message: str) -> None:
|
||||
"""Set the status label text (thread-safe).
|
||||
|
||||
Args:
|
||||
message: The status message to display
|
||||
"""
|
||||
# Schedule UI update on main thread
|
||||
def update_ui() -> None:
|
||||
self._set_status_ui(message)
|
||||
|
||||
self.parent.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.parent.update_idletasks()
|
||||
|
||||
def reset_progress(self):
|
||||
"""Reset progress bar to 0 (thread-safe)."""
|
||||
|
||||
# Schedule UI update on main thread using a proper closure
|
||||
def update_ui():
|
||||
self._update_progress_ui(0)
|
||||
|
||||
self.parent.after(0, update_ui)
|
||||
def _set_status_ui(self, message: str) -> None:
|
||||
"""Internal method to set status label on main thread.
|
||||
|
||||
Args:
|
||||
message: The status message to display
|
||||
"""
|
||||
try:
|
||||
self.status_label.config(text=message)
|
||||
self.parent.update_idletasks()
|
||||
except Exception as exception_error:
|
||||
print(f"[DEBUG] Error updating status label: {exception_error}")
|
||||
+1
-21
@@ -9,27 +9,7 @@ try:
|
||||
from .gui.main_window import main
|
||||
except ImportError:
|
||||
# Fall back to direct import (when run directly)
|
||||
try:
|
||||
from gui.main_window import main
|
||||
except ImportError:
|
||||
from gui import AndroidFileHandlerGUI
|
||||
import sys
|
||||
|
||||
def main():
|
||||
"""Main function to run the application."""
|
||||
if sys.platform not in ["win32", "linux"]:
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox
|
||||
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
messagebox.showerror(
|
||||
"Unsupported OS", "This application only supports Windows or Linux."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
app = AndroidFileHandlerGUI()
|
||||
app.mainloop()
|
||||
from gui.main_window import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user