Feat: Implement proposed gui.py changes for improved UI/UX
This commit is contained in:
208
gui.py
208
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,17 +213,27 @@ 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__':
|
||||
|
||||
Reference in New Issue
Block a user