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 import customtkinter
from tkinter import filedialog, messagebox # Keep these for standard dialogs import tkinter as tk
from tkinter import filedialog, messagebox
import os import os
import sys import sys
import threading import threading
import time import time
import converter import converter
import webbrowser
import downloader import downloader
import utils import utils
import config import config
@@ -14,71 +15,129 @@ class VideoConverterGUI:
def __init__(self, master): def __init__(self, master):
self.master = master self.master = master
master.title("Video Converter") master.title("Video Converter")
master.geometry("600x750+100+100")
customtkinter.set_appearance_mode("System") # Modes: "System" (default), "Dark", "Light" # --- Set Dark Mode by Default ---
customtkinter.set_default_color_theme("blue") # Themes: "blue" (default), "green", "dark-blue") 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:") # Create the main content frame
self.label.pack() 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="") # --- Footer Frame (packed into main_frame) ---
self.filepath_label.pack() 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.nickname_label = customtkinter.CTkLabel(self.footer_frame, text="by ramforth")
self.browse_button.pack() self.nickname_label.pack(side="left", padx=(10, 0))
self.url_label = customtkinter.CTkLabel(master, text="URL:") self.gitea_link = customtkinter.CTkLabel(self.footer_frame, text="https://gitea.ramforth.net/ramforth", text_color="#4077d1", cursor="hand2")
self.url_label.pack() 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) # --- Status and Progress (packed into main_frame) ---
self.url_entry.pack() 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.progress = customtkinter.CTkProgressBar(self.main_frame, orientation="horizontal")
self.quality_label.pack() self.progress.pack(side="bottom", fill="x", padx=10, pady=(0, 10))
self.progress.set(0)
self.quality_var = customtkinter.StringVar(master) # --- Mode Switch (packed into main_frame) ---
self.quality_var.set("medium") # default value self.mode_switch = customtkinter.CTkSwitch(self.main_frame, command=self.toggle_mode)
self.quality_menu = customtkinter.CTkOptionMenu(master, variable=self.quality_var, values=["low", "medium", "high", "archive"]) self.mode_switch.pack(pady=10, padx=20, anchor="w")
self.quality_menu.pack() self.mode_switch.select()
self.cookies_label = customtkinter.CTkLabel(master, text="Cookies from Browser:") self.mode_label = customtkinter.CTkLabel(self.main_frame)
self.cookies_label.pack() self.mode_label.pack(pady=0, padx=20, anchor="w")
self.cookies_var = customtkinter.StringVar(master) self.toggle_mode() # Ensure initial mode is set based on switch state
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()
# --- 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.label_part1 = customtkinter.CTkLabel(self.input_label_frame, text="Select video file ")
self.output_dir_label.pack() self.label_part1.pack(side="left")
self.output_dir_button = customtkinter.CTkButton(master, text="Select Directory", command=self.select_output_dir) or_font = customtkinter.CTkFont(family="sans-serif", weight="bold", underline=True)
self.output_dir_button.pack() 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.label_part2 = customtkinter.CTkLabel(self.input_label_frame, text=" enter URL:")
self.output_dir_path_label.pack() self.label_part2.pack(side="left")
self.convert_button = customtkinter.CTkButton(master, text="Convert", command=self.convert) self.filepath_label = customtkinter.CTkLabel(self.main_frame, text="No file selected", fg_color=("gray75", "gray25"))
self.convert_button.pack() 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.browse_button = customtkinter.CTkButton(self.main_frame, text="Browse", command=self.browse_file)
self.cancel_button.pack() self.browse_button.pack(pady=5)
self.status_var = customtkinter.StringVar() self.url_label = customtkinter.CTkLabel(self.main_frame, text="URL:")
self.status_label = customtkinter.CTkLabel(master, textvariable=self.status_var) self.url_label.pack(pady=(15, 5))
self.status_label.pack(side=tk.BOTTOM, fill=tk.X)
self.progress = customtkinter.CTkProgressBar(master, orientation="horizontal") self.url_entry = customtkinter.CTkEntry(self.main_frame, placeholder_text="https://...")
self.progress.pack(side=tk.BOTTOM, fill=tk.X) 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): def browse_file(self):
filepath = filedialog.askopenfilename() filepath = filedialog.askopenfilename()
self.filepath_label.config(text=filepath) if filepath:
self.filepath_label.configure(text=filepath)
def select_output_dir(self): def select_output_dir(self):
output_dir = filedialog.askdirectory() 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): def convert(self):
input_path = self.filepath_label.cget("text") input_path = self.filepath_label.cget("text")
@@ -86,17 +145,23 @@ class VideoConverterGUI:
output_dir = self.output_dir_path_label.cget("text") output_dir = self.output_dir_path_label.cget("text")
quality = self.quality_var.get() 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): if not (input_path or url):
messagebox.showerror("Error", "Please select a file or enter a URL.") messagebox.showerror("Error", "Please select a file or enter a URL.")
return return
if not output_dir: if output_dir == "No directory selected":
messagebox.showerror("Error", "Please select an output directory.") messagebox.showerror("Error", "Please select an output directory.")
return return
self.convert_button.config(state=tk.DISABLED) self.convert_button.configure(state="disabled")
self.cancel_button.config(state=tk.NORMAL) self.cancel_button.configure(state="normal")
self.status_var.set("Starting conversion...") self.status_var.set("Starting...")
self.conversion_thread = threading.Thread( self.conversion_thread = threading.Thread(
target=self._run_conversion, target=self._run_conversion,
@@ -109,31 +174,32 @@ class VideoConverterGUI:
try: try:
if url: if url:
self.status_var.set("Downloading video...") self.status_var.set("Downloading video...")
# Since we don't have progress for downloads, we'll use indeterminate mode self.progress.configure(mode='indeterminate')
self.progress['mode'] = 'indeterminate'
self.progress.start() self.progress.start()
input_path = downloader.download_video(url, output_dir, cookies_from_browser) input_path = downloader.download_video(url, output_dir, cookies_from_browser)
self.progress.stop() self.progress.stop()
self.progress['mode'] = 'determinate' self.progress.configure(mode='determinate')
if not input_path: if not input_path:
raise Exception("Download failed.") raise Exception("Download failed.")
self.status_var.set("Converting video...") self.status_var.set("Converting video...")
duration = utils.get_video_duration(input_path) duration = utils.get_video_duration(input_path)
if not duration: if not duration:
# If we can't get the duration, use indeterminate mode self.progress.configure(mode='indeterminate')
self.progress['mode'] = 'indeterminate'
self.progress.start() self.progress.start()
else: 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() self.progress_thread.start()
success = converter.convert_video(input_path, output_dir, quality) success = converter.convert_video(input_path, output_dir, quality)
if success: if success:
self.status_var.set("Conversion successful!") self.status_var.set("Conversion successful!")
self.progress.set(1)
messagebox.showinfo("Success", "Conversion completed successfully!") messagebox.showinfo("Success", "Conversion completed successfully!")
else: else:
raise Exception("Conversion failed. Check logs for details.") 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}") messagebox.showerror("Error", f"An error occurred: {e}\n\nCould not read log file: {log_e}")
finally: finally:
self.convert_button.config(state=tk.NORMAL) self.convert_button.configure(state="normal")
self.cancel_button.config(state=tk.DISABLED) self.cancel_button.configure(state="disabled")
self.progress.stop() 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" progress_file = "ffmpeg_progress.log"
while self.conversion_thread.is_alive(): while self.conversion_thread.is_alive():
try: try:
@@ -166,20 +237,22 @@ class VideoConverterGUI:
last_line = lines[-1] last_line = lines[-1]
if "out_time_ms" in last_line: if "out_time_ms" in last_line:
time_ms = int(last_line.split("=")[1]) 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): except (FileNotFoundError, IndexError, ValueError):
pass pass
time.sleep(0.1) time.sleep(0.1)
def cancel_conversion(self): def cancel_conversion(self):
# This is a bit tricky, as we can't easily kill the thread. self.status_var.set("Cancellation requested...")
# For now, we'll just show a message. messagebox.showinfo("Cancel", "Cancellation requested. The current operation will try to stop.")
# A more robust solution would involve a more complex mechanism self.convert_button.configure(state="normal")
# to signal the thread to stop. self.cancel_button.configure(state="disabled")
messagebox.showinfo("Cancel", "Cancellation requested. The current operation will continue until it is finished.")
if __name__ == '__main__': if __name__ == '__main__':
root = customtkinter.CTk() root = customtkinter.CTk()
gui = VideoConverterGUI(root) 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'], a = Analysis(['main.py'],
pathex=['/home/joe/Cloud9/Documents/Obisdian/projects/Video Converter'], pathex=['/home/joe/Cloud9/Documents/Obisdian/projects/Video Converter'],
binaries=[], binaries=[],
datas=[('gui.py', '.')], datas=[('gui.py', '.'), ('custom_theme_dark.json', '.'), ('custom_theme_light.json', '.')],
hiddenimports=[], hiddenimports=[],
hookspath=[], hookspath=[],
hooksconfig={}, hooksconfig={},