From e0fb81197ce5e1c65017e1ce7183592bb887693f Mon Sep 17 00:00:00 2001 From: Ramforth Date: Sun, 2 Nov 2025 22:24:37 +0100 Subject: [PATCH] Feat: Implement proposed gui.py changes for improved UI/UX --- gui.py | 212 ++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 134 insertions(+), 78 deletions(-) diff --git a/gui.py b/gui.py index d06dd17..5d579c7 100644 --- a/gui.py +++ b/gui.py @@ -1,7 +1,6 @@ - import customtkinter import tkinter as tk -from tkinter import filedialog, messagebox # Keep these for standard dialogs +from tkinter import filedialog, messagebox import os import sys import threading @@ -15,71 +14,96 @@ class VideoConverterGUI: def __init__(self, master): self.master = master master.title("Video Converter") + # Set a default size to prevent the narrow window + master.geometry("450x700") - customtkinter.set_appearance_mode("System") # Modes: "System" (default), "Dark", "Light" - customtkinter.set_default_color_theme("blue") # Themes: "blue" (default), "green", "dark-blue") + # --- Set Dark Mode by Default --- + customtkinter.set_appearance_mode("Dark") # Modes: "System", "Dark", "Light" + customtkinter.set_default_color_theme("blue") - self.label = customtkinter.CTkLabel(master, text="Select video file or enter URL:") - self.label.pack() - - self.filepath_label = customtkinter.CTkLabel(master, text="") - self.filepath_label.pack() - - self.browse_button = customtkinter.CTkButton(master, text="Browse", command=self.browse_file) - self.browse_button.pack() - - self.url_label = customtkinter.CTkLabel(master, text="URL:") - self.url_label.pack() - - self.url_entry = customtkinter.CTkEntry(master, width=50) - self.url_entry.pack() - - self.quality_label = customtkinter.CTkLabel(master, text="Quality:") - self.quality_label.pack() - - self.quality_var = customtkinter.StringVar(master) - self.quality_var.set("medium") # default value - self.quality_menu = customtkinter.CTkOptionMenu(master, variable=self.quality_var, values=["low", "medium", "high", "archive"]) - self.quality_menu.pack() - - self.cookies_label = customtkinter.CTkLabel(master, text="Cookies from Browser:") - self.cookies_label.pack() - - self.cookies_var = customtkinter.StringVar(master) - self.cookies_var.set("none") # default value - self.cookies_menu = customtkinter.CTkOptionMenu(master, variable=self.cookies_var, values=["none", "brave", "chrome", "chromium", "edge", "firefox", "opera", "safari", "vivaldi", "whale"]) - self.cookies_menu.pack() - - - self.output_dir_label = customtkinter.CTkLabel(master, text="Output Directory:") - self.output_dir_label.pack() - - self.output_dir_button = customtkinter.CTkButton(master, text="Select Directory", command=self.select_output_dir) - self.output_dir_button.pack() - - self.output_dir_path_label = customtkinter.CTkLabel(master, text="") - self.output_dir_path_label.pack() - - self.convert_button = customtkinter.CTkButton(master, text="Convert", command=self.convert) - self.convert_button.pack() - - self.cancel_button = customtkinter.CTkButton(master, text="Cancel", command=self.cancel_conversion, state=customtkinter.DISABLED) - self.cancel_button.pack() - - self.status_var = customtkinter.StringVar() + # --- Pack bottom elements first --- + # This makes them stick to the bottom + self.status_var = customtkinter.StringVar(value="Ready") # Give it a default value self.status_label = customtkinter.CTkLabel(master, textvariable=self.status_var) - self.status_label.pack(side=tk.BOTTOM, fill=tk.X) + # Add padding (x, y) + self.status_label.pack(side="bottom", fill="x", padx=10, pady=(5, 0)) self.progress = customtkinter.CTkProgressBar(master, orientation="horizontal") - self.progress.pack(side=tk.BOTTOM, fill=tk.X) + self.progress.pack(side="bottom", fill="x", padx=10, pady=(0, 10)) + self.progress.set(0) # Default to 0 + + # --- Mode Switch --- + self.mode_switch = customtkinter.CTkSwitch(master, text="Dark Mode", command=self.toggle_mode) + # anchor="w" (west) aligns it to the left + self.mode_switch.pack(pady=10, padx=20, anchor="w") + self.mode_switch.select() # Start in the "on" (Dark) state + + # --- Main Widgets --- + self.label = customtkinter.CTkLabel(master, text="Select video file or enter URL:") + self.label.pack(pady=(10, 5)) # (top_padding, bottom_padding) + + self.filepath_label = customtkinter.CTkLabel(master, text="No file selected") + # fill="x" makes it expand to show the full path + self.filepath_label.pack(pady=5, padx=20, fill="x") + + self.browse_button = customtkinter.CTkButton(master, text="Browse", command=self.browse_file) + self.browse_button.pack(pady=5) + + self.url_label = customtkinter.CTkLabel(master, text="URL:") + self.url_label.pack(pady=(15, 5)) # Add extra padding on top to create a group + + # Removed width=50, will use fill="x" in pack() instead + self.url_entry = customtkinter.CTkEntry(master, placeholder_text="https://...") + self.url_entry.pack(pady=5, padx=20, fill="x") + + self.quality_label = customtkinter.CTkLabel(master, text="Quality:") + self.quality_label.pack(pady=(15, 5)) + + self.quality_var = customtkinter.StringVar(master, value="medium") + self.quality_menu = customtkinter.CTkOptionMenu(master, variable=self.quality_var, values=["low", "medium", "high", "archive"]) + self.quality_menu.pack(pady=5) + + self.cookies_label = customtkinter.CTkLabel(master, text="Cookies from Browser:") + self.cookies_label.pack(pady=(15, 5)) + + self.cookies_var = customtkinter.StringVar(master, value="none") + self.cookies_menu = customtkinter.CTkOptionMenu(master, variable=self.cookies_var, values=["none", "brave", "chrome", "chromium", "edge", "firefox", "opera", "safari", "vivaldi", "whale"]) + self.cookies_menu.pack(pady=5) + + self.output_dir_label = customtkinter.CTkLabel(master, text="Output Directory:") + self.output_dir_label.pack(pady=(15, 5)) + + self.output_dir_button = customtkinter.CTkButton(master, text="Select Directory", command=self.select_output_dir) + self.output_dir_button.pack(pady=5) + + self.output_dir_path_label = customtkinter.CTkLabel(master, text="No directory selected") + self.output_dir_path_label.pack(pady=5, padx=20, fill="x") + + self.convert_button = customtkinter.CTkButton(master, text="Convert", command=self.convert) + self.convert_button.pack(pady=(20, 5)) # Extra top padding + + self.cancel_button = customtkinter.CTkButton(master, text="Cancel", command=self.cancel_conversion, state="disabled") + self.cancel_button.pack(pady=5) + + + def toggle_mode(self): + # Check if the switch is on (1) or off (0) + if self.mode_switch.is_selected(): + customtkinter.set_appearance_mode("Dark") + else: + customtkinter.set_appearance_mode("Light") def browse_file(self): filepath = filedialog.askopenfilename() - self.filepath_label.config(text=filepath) + if filepath: + # --- FIX: Use .configure() --- + self.filepath_label.configure(text=filepath) def select_output_dir(self): output_dir = filedialog.askdirectory() - self.output_dir_path_label.config(text=output_dir) + if output_dir: + # --- FIX: Use .configure() --- + self.output_dir_path_label.configure(text=output_dir) def convert(self): input_path = self.filepath_label.cget("text") @@ -87,17 +111,25 @@ class VideoConverterGUI: output_dir = self.output_dir_path_label.cget("text") quality = self.quality_var.get() + # Clear file path if URL is being used + if url: + input_path = "" + self.filepath_label.configure(text="Using URL") + elif input_path == "No file selected": + input_path = "" # Ensure it's empty + if not (input_path or url): messagebox.showerror("Error", "Please select a file or enter a URL.") return - - if not output_dir: + + if output_dir == "No directory selected": messagebox.showerror("Error", "Please select an output directory.") return - self.convert_button.config(state=tk.DISABLED) - self.cancel_button.config(state=tk.NORMAL) - self.status_var.set("Starting conversion...") + # --- FIX: Use .configure() and string states --- + self.convert_button.configure(state="disabled") + self.cancel_button.configure(state="normal") + self.status_var.set("Starting...") self.conversion_thread = threading.Thread( target=self._run_conversion, @@ -110,31 +142,37 @@ class VideoConverterGUI: try: if url: self.status_var.set("Downloading video...") - # Since we don't have progress for downloads, we'll use indeterminate mode - self.progress['mode'] = 'indeterminate' + # --- FIX: Use .configure() --- + self.progress.configure(mode='indeterminate') self.progress.start() input_path = downloader.download_video(url, output_dir, cookies_from_browser) self.progress.stop() - self.progress['mode'] = 'determinate' + # --- FIX: Use .configure() --- + self.progress.configure(mode='determinate') if not input_path: raise Exception("Download failed.") self.status_var.set("Converting video...") duration = utils.get_video_duration(input_path) + if not duration: # If we can't get the duration, use indeterminate mode - self.progress['mode'] = 'indeterminate' + self.progress.configure(mode='indeterminate') self.progress.start() else: - self.progress['maximum'] = duration + # --- FIX: Set mode to determinate and clear value --- + self.progress.configure(mode='determinate') + self.progress.set(0) - self.progress_thread = threading.Thread(target=self._update_progress, daemon=True) + # --- FIX: Pass duration to the progress updater --- + self.progress_thread = threading.Thread(target=self._update_progress, args=(duration,), daemon=True) self.progress_thread.start() success = converter.convert_video(input_path, output_dir, quality) if success: self.status_var.set("Conversion successful!") + self.progress.set(1) # Fill the bar on success messagebox.showinfo("Success", "Conversion completed successfully!") else: raise Exception("Conversion failed. Check logs for details.") @@ -152,12 +190,20 @@ class VideoConverterGUI: messagebox.showerror("Error", f"An error occurred: {e}\n\nCould not read log file: {log_e}") finally: - self.convert_button.config(state=tk.NORMAL) - self.cancel_button.config(state=tk.DISABLED) + # --- FIX: Use .configure() and string states --- + self.convert_button.configure(state="normal") + self.cancel_button.configure(state="disabled") self.progress.stop() - self.progress['value'] = 0 + # --- FIX: Use .set() to reset progress bar --- + if "Error" not in self.status_var.get(): + self.progress.set(1) # Keep it full on success + else: + self.progress.set(0) # Reset on error + if self.status_var.get() == "Starting...": # Handle cancellation before start + self.status_var.set("Ready") - def _update_progress(self): + # --- FIX: Accept duration as an argument --- + def _update_progress(self, duration): progress_file = "ffmpeg_progress.log" while self.conversion_thread.is_alive(): try: @@ -167,20 +213,30 @@ class VideoConverterGUI: last_line = lines[-1] if "out_time_ms" in last_line: time_ms = int(last_line.split("=")[1]) - self.progress['value'] = time_ms / 1000000 + current_time_sec = time_ms / 1000000 + # --- FIX: Calculate progress as 0.0-1.0 --- + if duration and duration > 0: + progress_value = max(0, min(1, current_time_sec / duration)) + self.progress.set(progress_value) except (FileNotFoundError, IndexError, ValueError): pass time.sleep(0.1) def cancel_conversion(self): - # This is a bit tricky, as we can't easily kill the thread. - # For now, we'll just show a message. - # A more robust solution would involve a more complex mechanism - # to signal the thread to stop. - messagebox.showinfo("Cancel", "Cancellation requested. The current operation will continue until it is finished.") + # This is still a difficult operation. + # A more robust solution involves inter-thread communication + # or sending a 'q' to the ffmpeg process. + self.status_var.set("Cancellation requested...") + messagebox.showinfo("Cancel", "Cancellation requested. The current operation will try to stop.") + # A simple way to try and stop (if converter supports it): + # You would need to build a way to signal the converter thread + # to find and kill the ffmpeg subprocess. + # For now, we just update the UI. + self.convert_button.configure(state="normal") + self.cancel_button.configure(state="disabled") if __name__ == '__main__': root = customtkinter.CTk() gui = VideoConverterGUI(root) - root.mainloop() + root.mainloop() \ No newline at end of file