16 Commits

Author SHA1 Message Date
Ramforth
b4815a4a19 Fix: Correct mode switch text and remove main frame padding 2025-11-03 01:28:29 +01:00
Ramforth
7283966b2e Feat: Implement main content frame and dynamic mode switch text 2025-11-03 01:18:38 +01:00
Ramforth
5ee4b3e8f3 Revert: Revert gui.py and custom theme JSON files to previous state for debugging segmentation fault 2025-11-03 01:00:27 +01:00
Ramforth
603ba2f679 Fix: Add missing border_width to CTkFrame in custom themes 2025-11-03 00:55:06 +01:00
Ramforth
46bb80338a Fix: Add missing border_width to CTkFrame in custom themes 2025-11-03 00:52:17 +01:00
Ramforth
2f773657a1 Fix: Add missing border_width to CTkFrame in custom themes 2025-11-03 00:49:59 +01:00
Ramforth
dd37b38498 Feat: Implement user's proposed gui.py changes, add footer, and refine theme JSON files 2025-11-03 00:48:03 +01:00
Ramforth
3b4890c8e8 Feat: Implement user's proposed gui.py changes, add footer, and refine theme JSON files 2025-11-03 00:45:18 +01:00
Ramforth
55a3838be7 Feat: Implement user's proposed gui.py changes, add footer, and refine theme JSON files 2025-11-03 00:37:54 +01:00
Ramforth
a791f4d8ca Fix: Implement user's proposed gui.py changes and correct theme JSON files 2025-11-03 00:29:05 +01:00
Ramforth
4f61d86e2a Feat: Implement user's proposed gui.py changes for improved UI/UX and functionality 2025-11-03 00:24:53 +01:00
Ramforth
710f77f403 Fix: Include custom theme JSON files in PyInstaller bundle 2025-11-03 00:16:14 +01:00
Ramforth
b4ffdfa977 Fix: Resolve CTkSwitch AttributeError and implement custom themes 2025-11-02 22:40:24 +01:00
Ramforth
1250afc9e5 Fix: Resolve AttributeError in CTkSwitch and ensure initial dark mode 2025-11-02 22:27:28 +01:00
Ramforth
e0fb81197c Feat: Implement proposed gui.py changes for improved UI/UX 2025-11-02 22:24:37 +01:00
Ramforth
0a49866662 Fix: Re-add tkinter import to gui.py to resolve NameError 2025-11-02 22:06:24 +01:00
5 changed files with 308 additions and 67 deletions

56
custom_theme_dark.json Normal file
View File

@@ -0,0 +1,56 @@
{
"CTk": {
"fg_color": ["#121212", "#121212"]
},
"CTkFrame": {
"fg_color": ["#121212", "#121212"],
"border_color": ["#333333", "#333333"],
"corner_radius": 8,
"border_width": 0
},
"CTkLabel": {
"text_color": ["#ecd7b2", "#ecd7b2"],
"fg_color": "transparent",
"corner_radius": 0
},
"CTkButton": {
"fg_color": ["#21498a", "#21498a"],
"hover_color": ["#2a5c9e", "#2a5c9e"],
"corner_radius": 8,
"border_color": ["#333333", "#333333"]
},
"CTkEntry": {
"fg_color": ["#1e1e1e", "#1e1e1e"],
"text_color": ["#ecd7b2", "#ecd7b2"],
"placeholder_text_color": ["#888888", "#888888"],
"corner_radius": 8,
"border_width": 2,
"border_color": ["#333333", "#333333"]
},
"CTkOptionMenu": {
"fg_color": ["#21498a", "#21498a"],
"button_color": ["#21498a", "#21498a"],
"button_hover_color": ["#2a5c9e", "#2a5c9e"],
"text_color": ["#ecd7b2", "#ecd7b2"],
"corner_radius": 8,
"border_color": ["#333333", "#333333"]
},
"CTkProgressBar": {
"fg_color": ["#1e1e1e", "#1e1e1e"],
"progress_color": ["#21498a", "#21498a"],
"corner_radius": 8,
"border_color": ["#333333", "#333333"]
},
"CTkSwitch": {
"fg_color": ["#1e1e1e", "#1e1e1e"],
"progress_color": ["#21498a", "#21498a"],
"text_color": ["#ecd7b2", "#ecd7b2"],
"corner_radius": 8,
"border_color": ["#333333", "#333333"]
},
"CTkFont": {
"family": ["Roboto", "Roboto"],
"size": [13, 13],
"weight": ["normal", "normal"]
}
}

56
custom_theme_light.json Normal file
View File

@@ -0,0 +1,56 @@
{
"CTk": {
"fg_color": ["#ecd7b2", "#ecd7b2"]
},
"CTkFrame": {
"fg_color": ["#ecd7b2", "#ecd7b2"],
"border_color": ["#cccccc", "#cccccc"],
"corner_radius": 8,
"border_width": 0
},
"CTkLabel": {
"text_color": ["#000000", "#000000"],
"fg_color": "transparent",
"corner_radius": 0
},
"CTkButton": {
"fg_color": ["#4077d1", "#4077d1"],
"hover_color": ["#5a8ee6", "#5a8ee6"],
"corner_radius": 8,
"border_color": ["#cccccc", "#cccccc"]
},
"CTkEntry": {
"fg_color": ["#ffffff", "#ffffff"],
"text_color": ["#000000", "#000000"],
"placeholder_text_color": ["#888888", "#888888"],
"corner_radius": 8,
"border_width": 2,
"border_color": ["#cccccc", "#cccccc"]
},
"CTkOptionMenu": {
"fg_color": ["#4077d1", "#4077d1"],
"button_color": ["#4077d1", "#4077d1"],
"button_hover_color": ["#5a8ee6", "#5a8ee6"],
"text_color": ["#000000", "#000000"],
"corner_radius": 8,
"border_color": ["#cccccc", "#cccccc"]
},
"CTkProgressBar": {
"fg_color": ["#ffffff", "#ffffff"],
"progress_color": ["#4077d1", "#4077d1"],
"corner_radius": 8,
"border_color": ["#cccccc", "#cccccc"]
},
"CTkSwitch": {
"fg_color": ["#ffffff", "#ffffff"],
"progress_color": ["#4077d1", "#4077d1"],
"text_color": ["#000000", "#000000"],
"corner_radius": 8,
"border_color": ["#cccccc", "#cccccc"]
},
"CTkFont": {
"family": ["Roboto", "Roboto"],
"size": [13, 13],
"weight": ["normal", "normal"]
}
}

205
gui.py
View File

@@ -1,11 +1,12 @@
import customtkinter
from tkinter import filedialog, messagebox # Keep these for standard dialogs
import tkinter as tk
from tkinter import filedialog, messagebox
import os
import sys
import threading
import time
import converter
import webbrowser
import downloader
import utils
import config
@@ -14,71 +15,129 @@ class VideoConverterGUI:
def __init__(self, master):
self.master = master
master.title("Video Converter")
master.geometry("600x750+100+100")
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")
customtkinter.set_default_color_theme("blue") # Temporarily using built-in theme for build stability
self.label = customtkinter.CTkLabel(master, text="Select video file or enter URL:")
self.label.pack()
# Create the main content frame
self.main_frame = customtkinter.CTkFrame(master, fg_color="transparent") # Make it transparent to root
self.main_frame.pack(fill="both", expand=True) # Add some padding to the main frame
self.filepath_label = customtkinter.CTkLabel(master, text="")
self.filepath_label.pack()
# --- Footer Frame (packed into main_frame) ---
self.footer_frame = customtkinter.CTkFrame(self.main_frame, fg_color="transparent")
self.footer_frame.pack(side="bottom", fill="x", pady=(5, 10))
self.browse_button = customtkinter.CTkButton(master, text="Browse", command=self.browse_file)
self.browse_button.pack()
self.nickname_label = customtkinter.CTkLabel(self.footer_frame, text="by ramforth")
self.nickname_label.pack(side="left", padx=(10, 0))
self.url_label = customtkinter.CTkLabel(master, text="URL:")
self.url_label.pack()
self.gitea_link = customtkinter.CTkLabel(self.footer_frame, text="https://gitea.ramforth.net/ramforth", text_color="#4077d1", cursor="hand2")
self.gitea_link.pack(side="right", padx=(0, 10))
self.gitea_link.bind("<Button-1>", lambda e: self.open_link("https://gitea.ramforth.net/ramforth"))
underline_font = customtkinter.CTkFont(family="sans-serif", size=12, underline=True)
self.gitea_link.configure(font=underline_font)
self.url_entry = customtkinter.CTkEntry(master, width=50)
self.url_entry.pack()
# --- Status and Progress (packed into main_frame) ---
self.status_var = customtkinter.StringVar(value="Ready")
self.status_label = customtkinter.CTkLabel(self.main_frame, textvariable=self.status_var)
self.status_label.pack(side="bottom", fill="x", padx=10, pady=(5, 0))
self.quality_label = customtkinter.CTkLabel(master, text="Quality:")
self.quality_label.pack()
self.progress = customtkinter.CTkProgressBar(self.main_frame, orientation="horizontal")
self.progress.pack(side="bottom", fill="x", padx=10, pady=(0, 10))
self.progress.set(0)
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()
# --- Mode Switch (packed into main_frame) ---
self.mode_switch = customtkinter.CTkSwitch(self.main_frame, command=self.toggle_mode)
self.mode_switch.pack(pady=10, padx=20, anchor="w")
self.mode_switch.select()
self.cookies_label = customtkinter.CTkLabel(master, text="Cookies from Browser:")
self.cookies_label.pack()
self.mode_label = customtkinter.CTkLabel(self.main_frame)
self.mode_label.pack(pady=0, padx=20, anchor="w")
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.toggle_mode() # Ensure initial mode is set based on switch state
# --- Main Widgets (packed into main_frame) ---
self.input_label_frame = customtkinter.CTkFrame(self.main_frame, fg_color="transparent")
self.input_label_frame.pack(pady=(10, 5))
self.output_dir_label = customtkinter.CTkLabel(master, text="Output Directory:")
self.output_dir_label.pack()
self.label_part1 = customtkinter.CTkLabel(self.input_label_frame, text="Select video file ")
self.label_part1.pack(side="left")
self.output_dir_button = customtkinter.CTkButton(master, text="Select Directory", command=self.select_output_dir)
self.output_dir_button.pack()
or_font = customtkinter.CTkFont(family="sans-serif", weight="bold", underline=True)
self.label_or = customtkinter.CTkLabel(self.input_label_frame, text="or", font=or_font)
self.label_or.pack(side="left")
self.output_dir_path_label = customtkinter.CTkLabel(master, text="")
self.output_dir_path_label.pack()
self.label_part2 = customtkinter.CTkLabel(self.input_label_frame, text=" enter URL:")
self.label_part2.pack(side="left")
self.convert_button = customtkinter.CTkButton(master, text="Convert", command=self.convert)
self.convert_button.pack()
self.filepath_label = customtkinter.CTkLabel(self.main_frame, text="No file selected", fg_color=("gray75", "gray25"))
self.filepath_label.pack(pady=5, padx=20, fill="x")
self.cancel_button = customtkinter.CTkButton(master, text="Cancel", command=self.cancel_conversion, state=customtkinter.DISABLED)
self.cancel_button.pack()
self.browse_button = customtkinter.CTkButton(self.main_frame, text="Browse", command=self.browse_file)
self.browse_button.pack(pady=5)
self.status_var = customtkinter.StringVar()
self.status_label = customtkinter.CTkLabel(master, textvariable=self.status_var)
self.status_label.pack(side=tk.BOTTOM, fill=tk.X)
self.url_label = customtkinter.CTkLabel(self.main_frame, text="URL:")
self.url_label.pack(pady=(15, 5))
self.progress = customtkinter.CTkProgressBar(master, orientation="horizontal")
self.progress.pack(side=tk.BOTTOM, fill=tk.X)
self.url_entry = customtkinter.CTkEntry(self.main_frame, placeholder_text="https://...")
self.url_entry.pack(pady=5, padx=20, fill="x")
self.quality_label = customtkinter.CTkLabel(self.main_frame, text="Quality:", fg_color="transparent")
self.quality_label.pack(pady=(15, 5))
self.quality_var = customtkinter.StringVar(master, value="medium")
self.quality_menu = customtkinter.CTkOptionMenu(self.main_frame, variable=self.quality_var, values=["low", "medium", "high", "archive"])
self.quality_menu.pack(pady=5)
self.cookies_label = customtkinter.CTkLabel(self.main_frame, text="Cookies from Browser:", fg_color="transparent")
self.cookies_label.pack(pady=(15, 5))
self.cookies_var = customtkinter.StringVar(master, value="none")
self.cookies_menu = customtkinter.CTkOptionMenu(self.main_frame, 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(self.main_frame, text="Output Directory:", fg_color="transparent")
self.output_dir_label.pack(pady=(15, 5))
self.output_dir_button = customtkinter.CTkButton(self.main_frame, text="Select Directory", command=self.select_output_dir)
self.output_dir_button.pack(pady=5)
self.output_dir_path_label = customtkinter.CTkLabel(self.main_frame, text="No directory selected", fg_color=("gray75", "gray25"))
self.output_dir_path_label.pack(pady=5, padx=20, fill="x")
self.convert_button = customtkinter.CTkButton(self.main_frame, text="Convert", command=self.convert)
self.convert_button.pack(pady=(20, 5))
self.cancel_button = customtkinter.CTkButton(self.main_frame, text="Cancel", command=self.cancel_conversion, state="disabled")
self.cancel_button.pack(pady=5)
def open_link(self, url):
"""Opens the given URL in a web browser."""
webbrowser.open_new(url)
def toggle_mode(self):
# Check if the switch is on (1) or off (0)
if self.mode_switch.get() == 1:
customtkinter.set_appearance_mode("Dark")
customtkinter.set_default_color_theme("blue") # Temporarily using built-in theme
self.main_frame.configure(fg_color="#121212") # Set main frame background
self.mode_label.configure(text="Dark Mode")
else:
customtkinter.set_appearance_mode("Light")
customtkinter.set_default_color_theme("blue") # Temporarily using built-in theme
self.main_frame.configure(fg_color="#ecd7b2") # Set main frame background
self.mode_label.configure(text="Light Mode")
def browse_file(self):
filepath = filedialog.askopenfilename()
self.filepath_label.config(text=filepath)
if filepath:
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:
self.output_dir_path_label.configure(text=output_dir)
def convert(self):
input_path = self.filepath_label.cget("text")
@@ -86,17 +145,23 @@ class VideoConverterGUI:
output_dir = self.output_dir_path_label.cget("text")
quality = self.quality_var.get()
if url:
input_path = ""
self.filepath_label.configure(text="Using URL")
elif input_path == "No file selected":
input_path = ""
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...")
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,
@@ -109,31 +174,32 @@ 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'
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'
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
self.progress.configure(mode='determinate')
self.progress.set(0)
self.progress_thread = threading.Thread(target=self._update_progress, daemon=True)
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)
messagebox.showinfo("Success", "Conversion completed successfully!")
else:
raise Exception("Conversion failed. Check logs for details.")
@@ -151,12 +217,17 @@ 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)
self.convert_button.configure(state="normal")
self.cancel_button.configure(state="disabled")
self.progress.stop()
self.progress['value'] = 0
if "Error" not in self.status_var.get():
self.progress.set(1)
else:
self.progress.set(0)
if self.status_var.get() == "Starting...":
self.status_var.set("Ready")
def _update_progress(self):
def _update_progress(self, duration):
progress_file = "ffmpeg_progress.log"
while self.conversion_thread.is_alive():
try:
@@ -166,20 +237,22 @@ 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
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.")
self.status_var.set("Cancellation requested...")
messagebox.showinfo("Cancel", "Cancellation requested. The current operation will try to stop.")
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()

56
test.css Normal file
View File

@@ -0,0 +1,56 @@
:root {
/** Base colors */
--clr-dark-a0: #000000;
--clr-light-a0: #ffffff;
/** Theme primary colors */
--clr-primary-a0: #759542;
--clr-primary-a10: #84a057;
--clr-primary-a20: #94ac6b;
--clr-primary-a30: #a3b77f;
--clr-primary-a40: #b2c394;
--clr-primary-a50: #c1cfa9;
/** Theme surface colors */
--clr-surface-a0: #121212;
--clr-surface-a10: #282828;
--clr-surface-a20: #3f3f3f;
--clr-surface-a30: #575757;
--clr-surface-a40: #717171;
--clr-surface-a50: #8b8b8b;
/** Theme tonal surface colors */
--clr-surface-tonal-a0: #1c1e17;
--clr-surface-tonal-a10: #31332c;
--clr-surface-tonal-a20: #474943;
--clr-surface-tonal-a30: #5f605b;
--clr-surface-tonal-a40: #777974;
--clr-surface-tonal-a50: #91928e;
/** Success colors */
--clr-success-a0: #22946e;
--clr-success-a10: #47d5a6;
--clr-success-a20: #9ae8ce;
/** Warning colors */
--clr-warning-a0: #a87a2a;
--clr-warning-a10: #d7ac61;
--clr-warning-a20: #ecd7b2;
/** Danger colors */
--clr-danger-a0: #9c2121;
--clr-danger-a10: #d94a4a;
--clr-danger-a20: #eb9e9e;
/** Info colors */
--clr-info-a0: #21498a;
--clr-info-a10: #4077d1;
--clr-info-a20: #92b2e5;
}
/** Examples */
.bg-primary {
color: var(--clr-primary-a50);
background-color: var(--clr-surface-a0);
}

View File

@@ -5,7 +5,7 @@ block_cipher = None
a = Analysis(['main.py'],
pathex=['/home/joe/Cloud9/Documents/Obisdian/projects/Video Converter'],
binaries=[],
datas=[('gui.py', '.')],
datas=[('gui.py', '.'), ('custom_theme_dark.json', '.'), ('custom_theme_light.json', '.')],
hiddenimports=[],
hookspath=[],
hooksconfig={},