This commit is contained in:
2026-01-03 13:30:35 -05:00
parent 4f4e5276a6
commit 0e1a1e9a1d
80 changed files with 42107 additions and 0 deletions
View File
+187
View File
@@ -0,0 +1,187 @@
import threading
import customtkinter as ctk
import webbrowser
import requests
from PIL import Image
class AboutWindow:
def __init__(self, parent, translate, version):
self.parent = parent
self.translate = translate
self.version = version
def get_github_data(self):
url = "https://api.github.com/repos/Emy69/CoomerDL"
try:
response = requests.get(url)
response.raise_for_status()
repo_data = response.json()
# Obtener la fecha de creación y las descargas totales (si existen releases)
created_at = repo_data.get("created_at", "N/A")
created_date = created_at.split("T")[0] if created_at != "N/A" else "N/A"
releases_url = repo_data.get("releases_url", "").replace("{/id}", "")
releases_response = requests.get(releases_url)
releases_response.raise_for_status()
releases_data = releases_response.json()
total_downloads = sum(
asset["download_count"] for release in releases_data for asset in release.get("assets", [])
) if releases_data else 0
return created_date, total_downloads
except Exception as e:
print(f"Error fetching GitHub data: {e}")
return "N/A", 0
def show_about(self):
# Crear una nueva ventana
about_window = ctk.CTkToplevel(self.parent)
about_window.title(self.translate("About"))
about_window.geometry("300x600")
about_window.resizable(False, False)
# Centrar la ventana
self.center_window(about_window, 300, 600)
# Hacer que la ventana aparezca al frente
about_window.transient(self.parent)
about_window.lift()
about_window.grab_set()
# placeholders mientras carga
created_date = self.translate("Loading...")
total_downloads = self.translate("Loading...")
# Crear un marco para el contenido
about_frame = ctk.CTkFrame(about_window, corner_radius=15)
about_frame.pack(fill="both", expand=True, padx=10, pady=10)
# Encabezado estilizado
header_label = ctk.CTkLabel(
about_frame,
text=self.translate("About This App"),
font=("Helvetica", 20, "bold"),
text_color="white",
anchor="w"
)
header_label.pack(pady=(10, 5), padx=10, anchor="w")
# Cargar las imágenes de los íconos
developer_icon = ctk.CTkImage(Image.open("resources/img/iconos/about/user-account-solid-24.png"), size=(20, 20))
version_icon = ctk.CTkImage(Image.open("resources/img/iconos/about/git-branch-line.png"), size=(20, 20))
downloads_icon = ctk.CTkImage(Image.open("resources/img/iconos/about/download_icon.png"), size=(20, 20))
date_icon = ctk.CTkImage(Image.open("resources/img/iconos/about/calendar-event-line.png"), size=(20, 20))
# labels que se actualizarán
self.downloads_label = None
self.date_label = None
details = [
(developer_icon, f"{self.translate('Developer')}: Emy69", None),
(version_icon, f"{self.translate('Version')}: {self.version}", None),
(downloads_icon, f"{self.translate('Total Downloads')}: {total_downloads}", "downloads"),
(date_icon, f"{self.translate('Release Date')}: {created_date}", "date")
]
for icon, text, key in details:
detail_frame = ctk.CTkFrame(about_frame, fg_color="transparent")
detail_frame.pack(anchor="w", pady=5, padx=30)
icon_label = ctk.CTkLabel(detail_frame, text="", image=icon)
icon_label.pack(side="left", padx=(0, 10))
text_label = ctk.CTkLabel(
detail_frame,
text=text,
font=("Helvetica", 14),
text_color="white",
justify="left"
)
text_label.pack(side="left")
if key == "downloads": # agregado
self.downloads_label = text_label
elif key == "date": # agregado
self.date_label = text_label
separator = ctk.CTkFrame(about_frame, height=1, fg_color="gray")
separator.pack(fill="x", padx=10, pady=10)
# Sección de plataformas soportadas
supported_label = ctk.CTkLabel(
about_frame,
text=self.translate("Supported Platforms"),
font=("Helvetica", 16, "bold"),
text_color="white",
anchor="w"
)
supported_label.pack(pady=(10, 5), padx=10, anchor="w")
# Lista de plataformas con íconos
platforms = [
("coomer.su", "https://coomer.su"),
("kemono.su", "https://kemono.su"),
("erome.com", "https://erome.com"),
("bunkr-albums.io", "https://bunkr-albums.io"),
("simpcity.su", "https://simpcity.su"),
("jpg5.su", "https://jpg5.su")
]
for name, url in platforms:
# Crear el marco para cada plataforma
platform_frame = ctk.CTkFrame(about_frame, fg_color="transparent")
platform_frame.pack(anchor="w", pady=5, padx=10)
icon_image = ctk.CTkImage(Image.open("resources/img/iconos/about/global-line.png"), size=(20, 20))
icon_label = ctk.CTkLabel(platform_frame, text="", image=icon_image)
icon_label.pack(side="left")
# Botón con el nombre de la plataforma
platform_button = ctk.CTkButton(
platform_frame,
text=name,
font=("Helvetica", 14),
fg_color="transparent",
hover_color="gray25",
command=lambda u=url: webbrowser.open(u)
)
platform_button.pack(side="left")
# Footer
footer_label = ctk.CTkLabel(
about_frame,
text=self.translate("Thank you for using our app!"),
font=("Helvetica", 12, "italic"),
text_color="white",
anchor="w"
)
footer_label.pack(pady=(10, 10), padx=10, anchor="w")
# hilo para cargar datos
def fetch_and_update():
cd, td = self.get_github_data()
def safe_update():
try:
if not about_window.winfo_exists():
return
if self.date_label and self.date_label.winfo_exists():
self.date_label.configure(text=f"{self.translate('Release Date')}: {cd}")
if self.downloads_label and self.downloads_label.winfo_exists():
self.downloads_label.configure(text=f"{self.translate('Total Downloads')}: {td}")
except Exception:
pass
about_window.after(0, safe_update)
threading.Thread(target=fetch_and_update, daemon=True).start()
def center_window(self, window, width, height):
screen_width = window.winfo_screenwidth()
screen_height = window.winfo_screenheight()
x = int((screen_width / 2) - (width / 2))
y = int((screen_height / 2) - (height / 2))
window.geometry(f'{width}x{height}+{x}+{y}')
+221
View File
@@ -0,0 +1,221 @@
import os
import customtkinter as ctk
import requests
import threading
import json
import tkinter as tk
from PIL import Image
class DonorsModal(ctk.CTkToplevel):
def __init__(self, parent, tr):
super().__init__(parent)
self.parent = parent
self.tr = tr # Translation function
self.title(self.tr("Patreons"))
self.geometry("600x600")
self.resizable(False, False)
self.transient(parent)
# Centrar la ventana y crear la UI
self.center_window()
self.create_ui()
self.after_idle(self._safe_grab)
# Start loading data in a separate thread to prevent UI freeze
threading.Thread(target=self._load_donors, daemon=True).start()
def _safe_grab(self):
"""Intenta grab_set hasta que la ventana sea visible."""
try:
self.grab_set()
except tk.TclError:
# Aún no es “viewable”; reintenta en 20 ms
self.after(20, self._safe_grab)
def center_window(self):
"""Centers the modal window relative to the parent window."""
self.update_idletasks()
parent_x = self.parent.winfo_x()
parent_y = self.parent.winfo_y()
parent_w = self.parent.winfo_width()
parent_h = self.parent.winfo_height()
window_w = self.winfo_width()
window_h = self.winfo_height()
cx = parent_x + (parent_w // 2) - (window_w // 2)
cy = parent_y + (parent_h // 2) - (window_h // 2)
self.geometry(f"{window_w}x{window_h}+{cx}+{cy}")
def create_ui(self):
"""Creates the UI elements for the Top Donors modal."""
main_frame = ctk.CTkFrame(self)
main_frame.pack(fill="both", expand=True, padx=20, pady=20)
title_label = ctk.CTkLabel(
main_frame,
text=self.tr("Patreons"),
font=ctk.CTkFont(size=24, weight="bold")
)
title_label.pack(pady=(0, 20))
self.donors_frame = ctk.CTkScrollableFrame(main_frame, fg_color="transparent")
self.donors_frame.pack(fill="both", expand=True, padx=10, pady=(0, 10))
self.grid_container = ctk.CTkFrame(self.donors_frame, fg_color="transparent")
self.grid_container.pack(fill="both", expand=True)
self.status_label = ctk.CTkLabel(
self.grid_container,
text=self.tr("Loading Patreons..."),
font=ctk.CTkFont(size=14),
text_color="gray"
)
self.status_label.grid(pady=20)
close_button = ctk.CTkButton(
main_frame,
text=self.tr("Close"),
command=self.destroy
)
close_button.pack(pady=(10, 0))
def _load_donors(self):
"""Fetches donor data from the API."""
donors = []
error_message = ""
try:
# Explicitly set Accept and User-Agent headers
resp = requests.get(
'https://emydevs.com/coomer/donadores.php',
headers={
'Accept': 'application/json',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36'
},
timeout=10
)
resp.raise_for_status()
donors = resp.json()
except requests.exceptions.RequestException as req_e:
error_message = str(req_e)
print(f"Error fetching donors: {error_message}")
self.after(0, lambda msg=error_message: self.status_label.configure(text=self.tr("Error fetching donors: {error}").format(error=msg), text_color="red"))
return
except json.JSONDecodeError as json_e:
error_message = str(json_e)
print(f"Error decoding JSON: {error_message}")
self.after(0, lambda msg=error_message: self.status_label.configure(text=self.tr("Error processing donor data: {error}").format(error=msg), text_color="red"))
return
# Update the UI on the main thread
self.after(0, lambda: self._show_donors(donors))
def _show_donors(self, donors):
"""Displays donor data in the modal."""
# Clear status label
self.status_label.grid_forget()
# Clear existing data
for widget in self.grid_container.winfo_children():
if widget is self.status_label:
continue
widget.destroy()
columns = 2
for c in range(columns):
self.grid_container.grid_columnconfigure(c, weight=1)
if not donors:
empty = ctk.CTkLabel(self.grid_container, text=self.tr("No donors found."), text_color="gray")
empty.grid(row=0, column=0, columnspan=columns, pady=20)
return
# Sort by amount in descending order, ensuring numeric comparison
def _to_float(v):
try:
return float(v)
except Exception:
return 0.0
donors.sort(key=lambda x: _to_float(x.get("donated_amount", 0)), reverse=True)
# Cache icons once (PNG medals and default)
if not hasattr(self, "_donor_icons"):
icon_dir = os.path.join("resources", "img", "iconos", "donors")
def _load_icon(fname):
path = os.path.join(icon_dir, fname)
if os.path.exists(path):
return ctk.CTkImage(
light_image=Image.open(path),
dark_image=Image.open(path),
size=(20, 20)
)
return None
# Keep a dict for quick access and to hold references
self._donor_icons = {
"gold": _load_icon("gold.png"),
"silver": _load_icon("silver.png"),
"bronze": _load_icon("bronze.png"),
"default": _load_icon("default.png"),
}
# Info note about donors data update
info_label = ctk.CTkLabel(
self.grid_container,
text=self.tr(
"Note: Donor information is updated every 10th of each month.\n"
"Names and donation amounts are retrieved from my Patreon page."
),
font=ctk.CTkFont(size=12, slant="italic"),
text_color="gray",
justify="center",
wraplength=350 # wrap text to fit in ~350px width
)
info_label.grid(row=0, column=0, columnspan=columns, pady=(0, 10), sticky="ew")
for i, donor in enumerate(donors):
donor_name = donor.get("name", self.tr("Unknown Donor"))
# Select icon and colors
icon_key = "default"
font_size = 14
font_weight = "normal"
name_color = "#E0E0E0"
icon_img = self._donor_icons.get(icon_key)
# Row container
donor_row_frame = ctk.CTkFrame(self.grid_container, fg_color="transparent", corner_radius=8)
donor_row_frame.grid_columnconfigure(0, weight=0)
donor_row_frame.grid_columnconfigure(1, weight=1)
row = (i // columns) + 1
col = i % columns
donor_row_frame.grid(row=row, column=col, padx=8, pady=8, sticky="nsew")
# Rank label with icon
rank_label = ctk.CTkLabel(
donor_row_frame,
text="", # remove numeric order
image=icon_img,
compound="left",
font=ctk.CTkFont(size=font_size, weight=font_weight),
width=40,
anchor="w",
text_color=name_color
)
rank_label.grid(row=0, column=0, padx=(5, 5), pady=5, sticky="w")
# Name label
name_label = ctk.CTkLabel(
donor_row_frame,
text=donor_name,
font=ctk.CTkFont(size=font_size, weight=font_weight),
anchor="w",
text_color=name_color
)
name_label.grid(row=0, column=1, padx=(0, 10), pady=5, sticky="ew")
def update_donor_data(self, new_donors):
self._show_donors(new_donors) # Call _show_donors with new data
+128
View File
@@ -0,0 +1,128 @@
import tkinter as tk
import requests
from tkinterweb import HtmlFrame
import customtkinter as ctk
import markdown2
class PatchNotes:
WINDOW_WIDTH = 900
WINDOW_HEIGHT = 800
def __init__(self, parent, translations_func):
self.parent = parent
self.tr = translations_func
self.patch_notes_window = None
def show_patch_notes(self):
# Cerrar la ventana si ya existe
if self.patch_notes_window is not None and tk.Toplevel.winfo_exists(self.patch_notes_window):
self.patch_notes_window.destroy()
self.patch_notes_window = ctk.CTkToplevel(self.parent)
self.patch_notes_window.title(self.tr("Patch Notes"))
self.patch_notes_window.transient(self.parent)
# Centrar la ventana
self.center_window(self.patch_notes_window, self.WINDOW_WIDTH, self.WINDOW_HEIGHT)
# Forzar visibilidad y foco
self.patch_notes_window.deiconify()
self.patch_notes_window.focus_force()
main_frame = ctk.CTkFrame(self.patch_notes_window)
main_frame.pack(expand=True, fill="both", padx=20, pady=20)
# Obtener el release más reciente desde GitHub
latest_release = self.get_latest_github_release('Emy69', 'CoomerDL')
# Crear un HtmlFrame para mostrar el contenido HTML
html_frame = HtmlFrame(main_frame, messages_enabled=False)
html_frame.pack(fill="both", expand=True, padx=10, pady=10)
# Cargar el contenido HTML
if latest_release:
patch_notes_html = self.get_patch_notes_html(latest_release)
html_frame.load_html(patch_notes_html)
else:
html_frame.load_html("<p>Failed to load patch notes.</p>")
# Añadir un frame inferior para el botón y opción de "No mostrar de nuevo"
bottom_frame = ctk.CTkFrame(self.patch_notes_window)
bottom_frame.pack(fill="x", padx=20, pady=(0, 20))
dont_show_again_var = tk.IntVar()
dont_show_again_check = ctk.CTkCheckBox(bottom_frame, text=self.tr("Don't show again"), variable=dont_show_again_var)
dont_show_again_check.pack(side="left", padx=5)
ok_button = ctk.CTkButton(bottom_frame, text=self.tr("OK"), command=lambda: self.close_patch_notes(dont_show_again_var))
ok_button.pack(side="right", padx=5)
@staticmethod
def get_latest_github_release(repo_owner, repo_name):
url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest"
response = requests.get(url)
if response.status_code == 200:
return response.json()
else:
return None
def center_window(self, window, width, height):
screen_width = window.winfo_screenwidth()
screen_height = window.winfo_screenheight()
x = int((screen_width / 2) - (width / 2))
y = int((screen_height / 2) - (height / 2))
window.geometry(f'{width}x{height}+{x}+{y}')
def get_patch_notes_html(self, latest_release):
# Convertir el cuerpo del release de Markdown a HTML
release_body_html = markdown2.markdown(latest_release['body'])
html_content = f"""
<html>
<head>
<style>
body {{
font-family: 'Arial', sans-serif;
background-color: #1e1e1e;
color: #f5f5f5;
padding: 20px;
margin: 0;
}}
h1 {{
color: #ffcc00;
text-align: center;
margin-top: 10px;
}}
h2 {{
color: #ff6600;
margin-bottom: 5px;
text-align: center;
}}
p {{
margin-bottom: 15px;
line-height: 1.6;
text-align: justify;
}}
.footer {{
text-align: center;
font-size: 12px;
color: #888;
margin-top: 20px;
}}
</style>
</head>
<body>
<h1>{self.tr('Patch Notes')}</h1>
<h2>{latest_release['name']}</h2>
{release_body_html}
</body>
</html>
"""
return html_content
def close_patch_notes(self, dont_show_again_var):
if self.patch_notes_window is not None:
self.patch_notes_window.destroy()
self.patch_notes_window = None
+192
View File
@@ -0,0 +1,192 @@
import customtkinter as ctk
import os
class ProgressManager:
def __init__(self, root, icons, footer_speed_label, footer_eta_label, progress_bar, progress_percentage):
self.root = root
self.icons = icons
self.footer_speed_label = footer_speed_label
self.footer_eta_label = footer_eta_label
self.progress_bar = progress_bar
self.progress_percentage = progress_percentage
self.progress_bars = {}
self.progress_window = None
self.no_downloads_label = None
self.progress_details_frame = None
self.auto_show_details = False
def create_progress_window(self):
if self.progress_window is None or not self.progress_window.winfo_exists():
self.progress_window = ctk.CTkToplevel(self.root)
self.progress_window.title("Detalles de Descarga")
self.progress_window.geometry("600x500")
self.progress_window.resizable(True, True)
self.progress_window.protocol("WM_DELETE_WINDOW", self.close_progress_window)
self.progress_details_frame = ctk.CTkFrame(self.progress_window)
self.progress_details_frame.pack(fill='both', expand=True, padx=10, pady=10)
# Crear el mensaje de "No hay descargas"
self.no_downloads_label = ctk.CTkLabel(self.progress_details_frame, text="No hay descargas en cola", font=("Arial", 14))
self.no_downloads_label.pack(pady=20)
# Hacer que la ventana aparezca al frente
self.progress_window.transient(self.root)
self.progress_window.grab_set()
def close_progress_window(self):
if self.progress_window is not None and self.progress_window.winfo_exists():
self.progress_window.grab_release()
self.progress_window.withdraw()
def update_progress(self, downloaded, total, file_id=None, file_path=None, speed=None, eta=None, status: str | None = None):
if status is not None:
original = self.footer_eta_label.cget("text")
base_eta = original.split(" | ")[0]
self.footer_eta_label.configure(text=f"{base_eta} | STATUS:{status}")
# Oculta el estado tras 5 s
self.footer_eta_label.after(
5000,
lambda: self.footer_eta_label.configure(text=base_eta)
)
return
if total > 0:
percentage = (downloaded / total) * 100
remaining = total - downloaded
if file_id is None:
if self.progress_bar.winfo_exists():
self.progress_bar.set(downloaded / total)
self.progress_percentage.configure(text=f"{percentage:.2f}%")
else:
if self.progress_window is None or not self.progress_window.winfo_exists():
return
if file_id not in self.progress_bars:
if self.no_downloads_label and self.no_downloads_label.winfo_exists():
self.no_downloads_label.pack_forget()
file_name = os.path.basename(file_path)
file_extension = os.path.splitext(file_path)[1].lower()
# Determinar el icono
if file_extension in ['.jpg', '.jpeg', '.png', '.gif']:
icon = self.icons['image']
elif file_extension in ['.mp4', '.avi', '.mkv']:
icon = self.icons['video']
elif file_extension in ['.zip', '.rar']:
icon = self.icons['zip']
else:
icon = self.icons['default']
progress_bar_frame = ctk.CTkFrame(self.progress_details_frame)
progress_bar_frame.pack(fill='x', padx=5, pady=5)
# Crear un contenedor para el icono y el texto
icon_and_text_frame = ctk.CTkFrame(progress_bar_frame)
icon_and_text_frame.pack(side='left', padx=5)
# Crear el icono con ctk.CTkLabel
icon_label = ctk.CTkLabel(icon_and_text_frame, image=icon, text="")
icon_label.pack(side='left')
# Limitar el texto y mostrar puntos suspensivos si excede el límite
max_text_length = 30
if len(file_name) > max_text_length:
displayed_file_name = file_name[:max_text_length] + '...'
else:
displayed_file_name = file_name
progress_label = ctk.CTkLabel(icon_and_text_frame, text=displayed_file_name, anchor='w')
progress_label.pack(side='left', padx=5)
# Crear barra de progreso y etiquetas de porcentaje y ETA
progress_bar = ctk.CTkProgressBar(progress_bar_frame)
progress_bar.pack(fill='x', padx=5, pady=5)
# Mostrar porcentaje y megabytes restantes
percentage_label = ctk.CTkLabel(progress_bar_frame, text=f"{percentage:.2f}% ({downloaded / 1048576:.2f} MB / {total / 1048576:.2f} MB)")
percentage_label.pack(side='left', padx=5)
eta_label = ctk.CTkLabel(progress_bar_frame, text=f"ETA: N/A")
eta_label.pack(side='right', padx=5)
self.progress_bars[file_id] = (progress_bar, percentage_label, eta_label, progress_bar_frame)
if file_id in self.progress_bars and self.progress_bars[file_id][0].winfo_exists():
self.progress_bars[file_id][0].set(downloaded / total)
self.progress_bars[file_id][1].configure(text=f"{percentage:.2f}% ({downloaded / 1048576:.2f} MB / {total / 1048576:.2f} MB)")
if eta is not None:
eta_text = f"ETA: {int(eta // 60)}m {int(eta % 60)}s"
self.progress_bars[file_id][2].configure(text=eta_text)
# Eliminar el frame si la descarga está completa
if downloaded >= total:
self.remove_progress_bar(file_id)
else:
if file_id is None:
if self.progress_bar.winfo_exists():
self.progress_bar.set(0)
self.progress_percentage.configure(text="0%")
else:
if file_id in self.progress_bars and self.progress_bars[file_id][0].winfo_exists():
self.progress_bars[file_id][0].set(0)
self.progress_bars[file_id][1].configure(text="0%")
self.progress_bars[file_id][2].configure(text="ETA: N/A")
# Actualizar velocidad en el footer (si corresponde)
if speed is not None:
if speed < 1_048_576:
speed_text = f"Speed: {speed / 1024:.2f} KB/s"
else:
speed_text = f"Speed: {speed / 1_048_576:.2f} MB/s"
self.footer_speed_label.configure(text=speed_text)
if eta is not None:
mins, secs = divmod(int(eta), 60)
self.footer_eta_label.configure(text=f"ETA: {mins}m {secs}s")
def remove_progress_bar(self, file_id):
if file_id in self.progress_bars and self.progress_bars[file_id][3].winfo_exists():
# Programar la eliminación del frame después de 2 segundos
self.progress_bars[file_id][3].after(2000, lambda: self._forget_and_delete(file_id))
def _forget_and_delete(self, file_id):
if file_id in self.progress_bars and self.progress_bars[file_id][3].winfo_exists():
self.progress_bars[file_id][3].pack_forget()
del self.progress_bars[file_id]
# Si ya no quedan descargas, mostrar mensaje y restablecer el footer
if not self.progress_bars:
self.no_downloads_label.pack(pady=20)
# Restablecer valores por defecto en el footer
self.footer_speed_label.configure(text="Speed: 0 KB/s")
self.footer_eta_label.configure(text="ETA: N/A")
def update_global_progress(self, completed_files, total_files):
if total_files > 0:
percentage = (completed_files / total_files) * 100
if self.progress_bar.winfo_exists():
self.progress_bar.set(completed_files / total_files)
self.progress_percentage.configure(text=f"{percentage:.2f}%")
def toggle_progress_details(self):
self.create_progress_window()
if self.progress_window.winfo_viewable():
self.close_progress_window()
else:
self.center_progress_details_frame()
self.progress_window.deiconify()
self.progress_window.lift()
def center_progress_details_frame(self):
if self.progress_window is not None and self.progress_window.winfo_exists():
self.progress_window.update_idletasks()
width = self.progress_window.winfo_width()
height = self.progress_window.winfo_height()
x = (self.root.winfo_screenwidth() // 2) - (width // 2)
y = (self.root.winfo_screenheight() // 2) - (height // 2)
self.progress_window.geometry(f"{width}x{height}+{x}+{y}")
self.progress_window.minsize(width, height)
+905
View File
@@ -0,0 +1,905 @@
import json
import os
import sqlite3
import threading
from tkinter import filedialog, messagebox, ttk
import customtkinter as ctk
import tkinter as tk
from PIL import Image, ImageTk
from PIL import Image as PilImage
import requests
class SettingsWindow:
CONFIG_PATH = 'resources/config/settings.json'
def __init__(self, parent, translate, load_translations_func, update_ui_texts_func, save_language_preference_func, version, downloader, check_for_new_version_func,on_settings_changed=None):
self.parent = parent
self.translate = translate
self.load_translations = load_translations_func
self.update_ui_texts = update_ui_texts_func
self.save_language_preference = save_language_preference_func
self.version = version
self.downloader = downloader
self.check_for_new_version = check_for_new_version_func
self.languages = {
"Español": "es",
"English": "en",
"日本語": "ja",
"中文": "zh",
"Français": "fr",
"Русский": "ru"
}
self.settings = self.load_settings()
self.folder_structure_icons = self.load_icons()
self.on_settings_changed = on_settings_changed
self.site_status_labels = {}
self.site_textboxes = {}
def load_settings(self):
if not os.path.exists(self.CONFIG_PATH):
return {'max_downloads': 3, 'folder_structure': 'default', 'language': 'en', 'theme': 'System'}
try:
with open(self.CONFIG_PATH, 'r') as file:
return json.load(file)
except (FileNotFoundError, json.JSONDecodeError):
return {'max_downloads': 3, 'folder_structure': 'default', 'language': 'en', 'theme': 'System'}
def save_settings(self):
os.makedirs(os.path.dirname(self.CONFIG_PATH), exist_ok=True)
with open(self.CONFIG_PATH, 'w') as file:
json.dump(self.settings, file, indent=4)
if callable(self.on_settings_changed):
self.on_settings_changed(self.settings)
def load_icons(self):
icons = {}
try:
icons['folder'] = ImageTk.PhotoImage(PilImage.open("resources/img/iconos/settings/folder.png").resize((20, 20), PilImage.Resampling.LANCZOS))
except Exception as e:
messagebox.showerror(self.translate("Error"), self.translate(f"Error loading icons: {e}"))
icons['folder'] = None
return icons
def open_settings(self):
self.settings_window = ctk.CTkToplevel(self.parent)
self.settings_window.title(self.translate("Settings"))
self.settings_window.geometry("800x600")
self.settings_window.transient(self.parent)
self.settings_window.deiconify()
self.settings_window.after_idle(self.settings_window.grab_set)
self.center_window(self.settings_window, 800, 600)
self.main_frame = ctk.CTkFrame(self.settings_window)
self.main_frame.pack(fill="both", expand=True, padx=10, pady=10)
self.tabview = ctk.CTkTabview(self.main_frame, width=700, height=500)
self.tabview.pack(fill="both", expand=True)
# Existing tabs
self.general_tab = self.tabview.add(self.translate("General"))
self.downloads_tab = self.tabview.add(self.translate("Downloads"))
self.structure_tab = self.tabview.add(self.translate("Structure"))
self.db_tab = self.tabview.add(self.translate("Database"))
self.cookies_tab = self.tabview.add(self.translate("Cookies"))
# New tab for database
self.render_general_tab(self.general_tab)
self.render_downloads_tab(self.downloads_tab)
self.render_structure_tab(self.structure_tab)
self.render_db_tab(self.db_tab)
self.render_cookies_tab(self.cookies_tab)
def render_db_tab(self, tab):
tab.grid_columnconfigure(0, weight=1)
tab.grid_rowconfigure(0, weight=1)
db_frame = ctk.CTkFrame(tab, fg_color="transparent")
db_frame.pack(fill="both", expand=True, padx=20, pady=20)
header_label = ctk.CTkLabel(db_frame, text=self.translate("Database Management"), font=("Helvetica", 16, "bold"))
header_label.pack(pady=(0, 10))
# Configurar un estilo personalizado para el Treeview
style = ttk.Style()
style.theme_use('clam') # O puedes elegir 'default' o 'alt'
style.configure("Custom.Treeview",
background="#2e2e2e",
foreground="white",
fieldbackground="#2e2e2e",
rowheight=25,
font=('Helvetica', 10))
style.configure("Custom.Treeview.Heading",
background="#1e1e1e",
foreground="white",
font=('Helvetica', 10, 'bold'))
# Para filas alternas (opcional)
style.map("Custom.Treeview", background=[("selected", "#4a6984")])
# Frame para el Treeview y su scrollbar
table_frame = tk.Frame(db_frame)
table_frame.pack(fill="both", expand=True)
# Definir las columnas que se mostrarán en la parte derecha del Treeview
columns = ("id", "file_name", "type", "size", "downloaded_at")
self.db_tree = ttk.Treeview(table_frame, style="Custom.Treeview", columns=columns, show="tree headings", height=15, selectmode="extended")
# Configurar encabezados
self.db_tree.heading("#0", text=self.translate("User/Post"), anchor="w")
self.db_tree.heading("id", text="ID", anchor="center")
self.db_tree.heading("file_name", text=self.translate("File Name"), anchor="w")
self.db_tree.heading("type", text=self.translate("Type"), anchor="center")
self.db_tree.heading("size", text=self.translate("Size"), anchor="center")
self.db_tree.heading("downloaded_at", text=self.translate("Downloaded At"), anchor="center")
# Configurar ancho de columnas
self.db_tree.column("#0", width=180)
self.db_tree.column("id", width=30, anchor="center")
self.db_tree.column("file_name", width=200)
self.db_tree.column("type", width=80, anchor="center")
self.db_tree.column("size", width=80, anchor="center")
self.db_tree.column("downloaded_at", width=150, anchor="center")
# Scrollbar vertical
vsb = ttk.Scrollbar(table_frame, orient="vertical", command=self.db_tree.yview)
self.db_tree.configure(yscrollcommand=vsb.set)
self.db_tree.grid(row=0, column=0, sticky="nsew")
vsb.grid(row=0, column=1, sticky="ns")
table_frame.grid_rowconfigure(0, weight=1)
table_frame.grid_columnconfigure(0, weight=1)
# Botones para exportar y limpiar
btn_frame = ctk.CTkFrame(db_frame, fg_color="transparent")
btn_frame.pack(pady=10)
export_button = ctk.CTkButton(btn_frame, text=self.translate("Export Database"), command=self.export_db)
export_button.pack(side="left", padx=10)
clear_button = ctk.CTkButton(btn_frame, text=self.translate("Clear Database"), command=self.clear_db)
clear_button.pack(side="left", padx=10)
delete_users_button = ctk.CTkButton(
btn_frame,
text=self.translate("Delete Selected Users"),
command=self.delete_selected_users
)
delete_users_button.pack(side="left", padx=10)
# Cargar los registros en el Treeview con agrupación por usuario y post
self.load_db_records()
def render_cookies_tab(self, tab):
tab.grid_columnconfigure(0, weight=1)
tab.grid_rowconfigure(1, weight=1)
# Header
header_frame = ctk.CTkFrame(tab, fg_color="transparent")
header_frame.grid(row=0, column=0, sticky="ew", padx=20, pady=(20, 10))
title_label = ctk.CTkLabel(
header_frame,
text="Cookie Management",
font=("Helvetica", 18, "bold")
)
title_label.pack(anchor="w")
desc_label = ctk.CTkLabel(
header_frame,
text="Paste or import cookies in JSON format (list of objects with name, value, domain, path). "
"They will be stored by site in resources/config/cookies/{sitio}.json",
font=("Helvetica", 12),
text_color="gray",
wraplength=800,
justify="left"
)
desc_label.pack(anchor="w", pady=(5, 0))
main_frame = ctk.CTkScrollableFrame(tab)
main_frame.grid(row=1, column=0, sticky="nsew", padx=20, pady=(0, 20))
main_frame.grid_columnconfigure(0, weight=1)
sites_config = [
{"key": "simpcity", "name": "SimpCity", "color": "#FF6B6B", "icon": "🌐",
"description": "Cookies for accessing SimCity"},
]
def cookie_path_for(site_key: str) -> str:
base = "resources/config/cookies"
os.makedirs(base, exist_ok=True)
return os.path.join(base, f"{site_key}.json")
for i, cfg in enumerate(sites_config):
site_key = cfg["key"]
site_name = cfg["name"]
site_color = cfg["color"]
site_icon = cfg["icon"]
site_desc = cfg["description"]
# --- Container ---
site_container = ctk.CTkFrame(main_frame)
site_container.grid(row=i, column=0, sticky="ew", pady=(0, 22), padx=10)
site_container.grid_columnconfigure(0, weight=1)
accent = ctk.CTkFrame(site_container, height=4, fg_color=site_color)
accent.grid(row=0, column=0, sticky="ew")
# Header
header = ctk.CTkFrame(site_container, fg_color="transparent")
header.grid(row=1, column=0, sticky="ew", padx=15, pady=(10, 6))
header.grid_columnconfigure(1, weight=1)
title = ctk.CTkLabel(header, text=f"{site_icon} {site_name}", font=("Helvetica", 16, "bold"))
title.grid(row=0, column=0, sticky="w")
status_exists = os.path.exists(cookie_path_for(site_key))
status_label = ctk.CTkLabel(
header,
text=("✓ Configuradas" if status_exists else "⚠ Sin configurar"),
font=("Helvetica", 11),
text_color=("#4CAF50" if status_exists else "#FF9800")
)
status_label.grid(row=0, column=1, sticky="e")
self.site_status_labels[site_key] = status_label
if site_desc:
desc = ctk.CTkLabel(
header, text=site_desc, font=("Helvetica", 11),
text_color="gray", wraplength=600, justify="left"
)
desc.grid(row=1, column=0, columnspan=2, sticky="w", pady=(4, 0))
instructions_frame = ctk.CTkFrame(site_container, fg_color=("gray90", "gray20"))
instructions_frame.grid(row=2, column=0, sticky="ew", padx=15, pady=(0, 8))
instructions = (
"📋 How to obtain cookies (JSON format):\n"
"1) Open the browser Developer Tools (F12).\n"
"2) Go to Application/Storage → Cookies and select the site.\n"
"3) Use an extension to export cookies as JSON (e.g., EditThisCookie), or copy the fields manually.\n"
"4) Paste the JSON below or import a .json file.\n"
" Expected format: an array of objects with at least name, value, domain, path."
)
ctk.CTkLabel(instructions_frame, text=instructions, font=("Helvetica", 10),
justify="left", anchor="w").pack(padx=12, pady=8, fill="x")
# Textbox
text_frame = ctk.CTkFrame(site_container, fg_color="transparent")
text_frame.grid(row=3, column=0, sticky="ew", padx=15, pady=(2, 6))
text_frame.grid_columnconfigure(0, weight=1)
textbox = ctk.CTkTextbox(text_frame, height=160, wrap="none")
textbox.grid(row=0, column=0, sticky="ew")
pre_path = cookie_path_for(site_key)
if os.path.exists(pre_path):
try:
with open(pre_path, "r", encoding="utf-8") as f:
textbox.delete("1.0", "end")
textbox.insert("1.0", f.read())
except Exception:
pass
self.site_textboxes[site_key] = textbox
buttons = ctk.CTkFrame(site_container, fg_color="transparent")
buttons.grid(row=4, column=0, sticky="ew", padx=15, pady=(4, 12))
buttons.grid_columnconfigure((0,1,2,3), weight=0)
buttons.grid_columnconfigure(4, weight=1)
def do_import(sk=site_key, tb=textbox):
file = filedialog.askopenfilename(
title=f"Import cookies JSON for {sk}",
filetypes=[("JSON files", "*.json"), ("All files", "*.*")]
)
if not file:
return
try:
with open(file, "r", encoding="utf-8") as f:
content = f.read()
json.loads(content) # valida
tb.delete("1.0", "end")
tb.insert("1.0", content)
messagebox.showinfo("OK", f"Cookies loaded into editor for {sk}. Don't forget to Save!")
except Exception as e:
messagebox.showerror("Error", f"Invalid JSON: {e}")
def do_save(sk=site_key, tb=textbox, sl=status_label):
content = tb.get("1.0", "end").strip()
if not content:
messagebox.showwarning("Empty", "Nothing to save.")
return
try:
data = json.loads(content)
if isinstance(data, dict):
data = [data]
if not isinstance(data, list):
raise ValueError("JSON must be an object or an array of objects.")
for idx, c in enumerate(data, start=1):
if not isinstance(c, dict) or "name" not in c or "value" not in c:
raise ValueError(f"Item {idx} is missing 'name' or 'value'.")
save_path = cookie_path_for(sk)
os.makedirs(os.path.dirname(save_path), exist_ok=True)
with open(save_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
sl.configure(text="✓ Configured", text_color="#4CAF50")
messagebox.showinfo("OK", f"Cookies saved to:\n{save_path}")
except Exception as e:
messagebox.showerror("Error", f"Could not save: {e}")
def do_test(sk=site_key, sl=status_label):
urls = {
"simpcity": ["https://www.simpcity.cr/"]
}
test_path = cookie_path_for(sk)
if not os.path.exists(test_path):
messagebox.showwarning("Missing file", "You haven't saved cookies for this site yet.")
return
try:
with open(test_path, "r", encoding="utf-8") as f:
cookies = json.load(f)
s = requests.Session()
jar = requests.cookies.RequestsCookieJar()
for c in cookies if isinstance(cookies, list) else [cookies]:
jar.set(c["name"], c["value"], domain=c.get("domain", None), path=c.get("path", "/"))
s.cookies = jar
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
}
ok_any, codes = False, []
for test_url in urls.get(sk, []):
try:
r = s.get(test_url, headers=headers, timeout=12)
codes.append(r.status_code)
if 200 <= r.status_code < 400:
ok_any = True
except Exception:
codes.append("ERR")
if ok_any:
sl.configure(text="✓ Configured (test OK)", text_color="#4CAF50")
messagebox.showinfo("OK", f"Response from {sk}: {codes}")
else:
sl.configure(text="⚠ Tested (non-OK response)", text_color="#FFC107")
messagebox.showwarning("Heads up", f"Response from {sk}: {codes}\n"
"Cookie format looks fine but the site returned non-OK status codes.\n"
"If content requires login, try a protected URL inside the downloader.")
except Exception as e:
sl.configure(text="⚠ Not configured", text_color="#FF9800")
messagebox.showerror("Error", f"Test failed: {e}")
def do_clear(sk=site_key, tb=textbox, sl=status_label):
del_path = cookie_path_for(sk)
if os.path.exists(del_path):
try:
os.remove(del_path)
except Exception as e:
messagebox.showerror("Error", f"Could not delete file: {e}")
return
tb.delete("1.0", "end")
sl.configure(text="⚠ Not configured", text_color="#FF9800")
messagebox.showinfo("OK", f"Cookies for {sk} deleted.")
ctk.CTkButton(buttons, text="Import JSON...", command=do_import).grid(row=0, column=0, padx=(0, 6))
ctk.CTkButton(buttons, text="Save",command=do_save).grid( row=0, column=1, padx=6)
ctk.CTkButton(buttons, text="Test",command=do_test).grid( row=0, column=2, padx=6)
ctk.CTkButton(buttons, text="Delete",command=do_clear, fg_color="#E53935", hover_color="#C62828").grid(row=0, column=3, padx=6)
def delete_selected_users(self):
selected = self.db_tree.selection()
if not selected:
messagebox.showwarning(
self.translate("Warning"),
self.translate("Please select at least one user to delete.")
)
return
user_ids = [
self.db_tree.item(node, 'text')
for node in selected
if self.db_tree.parent(node) == ''
]
if not user_ids:
messagebox.showwarning(
self.translate("Warning"),
self.translate("Please select valid user entries (not posts/files).")
)
return
confirm = messagebox.askyesno(
self.translate("Confirm"),
self.translate(
"Are you sure you want to delete all records for user(s): {}?".format(
", ".join(user_ids)
)
)
)
if not confirm:
return
try:
conn = sqlite3.connect(self.downloader.db_path)
cursor = conn.cursor()
for uid in user_ids:
cursor.execute("DELETE FROM downloads WHERE user_id = ?", (uid,))
conn.commit()
conn.close()
messagebox.showinfo(
self.translate("Success"),
self.translate("Selected user(s) and their records were deleted.")
)
self.load_db_records()
except Exception as e:
messagebox.showerror(
self.translate("Error"),
self.translate(f"Error deleting user(s): {e}")
)
def load_db_records(self):
"""Consulta la base de datos y carga los registros en el Treeview agrupados por usuario y, opcionalmente, por post."""
db_path = self.downloader.db_path
if not os.path.exists(db_path):
tk.messagebox.showwarning(self.translate("Warning"), self.translate("Database not found."))
return
try:
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute("SELECT id, media_url, file_path, file_size, user_id, post_id, downloaded_at FROM downloads")
rows = cursor.fetchall()
conn.close()
except Exception as e:
tk.messagebox.showerror(self.translate("Error"), self.translate("Error loading database: {e}", e=e))
return
# Limpiar el Treeview
for child in self.db_tree.get_children():
self.db_tree.delete(child)
# Función auxiliar para determinar el tipo de archivo según la extensión
def get_file_type(file_path):
ext = os.path.splitext(file_path)[1].lower()
if ext in ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff']:
return self.translate("Image")
elif ext in ['.mp4', '.mkv', '.webm', '.mov', '.avi', '.flv', '.wmv', '.m4v']:
return self.translate("Video")
elif ext in ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx']:
return self.translate("Document")
elif ext in ['.zip', '.rar', '.7z', '.tar', '.gz']:
return self.translate("Compressed")
else:
return self.translate("Other")
# Función para formatear el tamaño
def format_size(size):
if size < 1024:
return f"{size} B"
elif size < 1024**2:
return f"{size/1024:.2f} KB"
else:
return f"{size/1024**2:.2f} MB"
# Agrupar registros por usuario
usuarios = {}
for rec in rows:
rec_id, media_url, file_path, file_size, user_id, post_id, downloaded_at = rec
usuarios.setdefault(user_id, []).append(rec)
# Insertar cada usuario como nodo padre en el Treeview
for user, registros in usuarios.items():
user_node = self.db_tree.insert("", "end", text=user, open=False)
# Dentro de cada usuario, agrupar los registros que tienen post_id y los que no
posts = {}
sin_post = []
for rec in registros:
rec_id, media_url, file_path, file_size, user_id, post_id, downloaded_at = rec
if post_id:
posts.setdefault(post_id, []).append(rec)
else:
sin_post.append(rec)
# Insertar nodos para cada post
for post, recs in posts.items():
post_node = self.db_tree.insert(user_node, "end", text=post, open=False)
for rec in recs:
rec_id, media_url, file_path, file_size, user_id, post_id, downloaded_at = rec
file_name = os.path.basename(file_path)
file_type = get_file_type(file_path)
size_str = format_size(file_size)
self.db_tree.insert(post_node, "end", values=(rec_id, file_name, file_type, size_str, downloaded_at))
# Insertar los registros sin post en un nodo especial
if sin_post:
sin_post_node = self.db_tree.insert(user_node, "end", text=self.translate("No Post"), open=False)
for rec in sin_post:
rec_id, media_url, file_path, file_size, user_id, post_id, downloaded_at = rec
file_name = os.path.basename(file_path)
file_type = get_file_type(file_path)
size_str = format_size(file_size)
self.db_tree.insert(sin_post_node, "end", values=(rec_id, file_name, file_type, size_str, downloaded_at))
def render_general_tab(self, tab):
tab.grid_columnconfigure(0, weight=1)
tab.grid_rowconfigure(0, weight=0)
# General frame
general_frame = ctk.CTkFrame(tab, fg_color="transparent")
general_frame.grid(row=0, column=0, sticky="ew", padx=20, pady=20)
general_frame.grid_columnconfigure(1, weight=1)
# Main label
general_label = ctk.CTkLabel(general_frame, text=self.translate("General Options"), font=("Helvetica", 16, "bold"))
general_label.grid(row=0, column=0, columnspan=3, sticky="w")
# Section description
description_label = ctk.CTkLabel(general_frame, text=self.translate("Here you can change the appearance and language of the application."),
font=("Helvetica", 11), text_color="gray")
description_label.grid(row=1, column=0, columnspan=3, sticky="w", pady=(0, 15))
# Theme
theme_label = ctk.CTkLabel(general_frame, text=self.translate("Theme"), font=("Helvetica", 14))
theme_label.grid(row=2, column=0, pady=5, sticky="w")
theme_combobox = ctk.CTkComboBox(general_frame, values=["Light", "Dark", "System"], state='readonly', width=120)
theme_combobox.set(self.settings.get('theme', 'System'))
theme_combobox.grid(row=2, column=1, pady=5, padx=(10,0), sticky="w")
apply_theme_button = ctk.CTkButton(general_frame, text=self.translate("Apply Theme"),
command=lambda: self.change_theme_in_thread(theme_combobox.get()))
apply_theme_button.grid(row=2, column=2, pady=5, sticky="e")
# Separator
separator_1 = ttk.Separator(general_frame, orient="horizontal")
separator_1.grid(row=3, column=0, columnspan=3, sticky="ew", pady=15)
# Language
language_label = ctk.CTkLabel(general_frame, text=self.translate("Language"), font=("Helvetica", 14))
language_label.grid(row=4, column=0, pady=5, sticky="w")
language_combobox = ctk.CTkComboBox(general_frame, values=list(self.languages.keys()), state='readonly', width=120)
language_combobox.set(self.get_language_name(self.settings.get('language', 'en')))
language_combobox.grid(row=4, column=1, pady=5, padx=(10,0), sticky="w")
apply_language_button = ctk.CTkButton(general_frame, text=self.translate("Apply Language"),
command=lambda: self.apply_language_settings(language_combobox.get()))
apply_language_button.grid(row=4, column=2, pady=5, sticky="e")
# Separator for updates
separator_2 = ttk.Separator(general_frame, orient="horizontal")
separator_2.grid(row=5, column=0, columnspan=3, sticky="ew", pady=15)
# Check for Updates
update_label = ctk.CTkLabel(general_frame, text=self.translate("Check for Updates"), font=("Helvetica", 14))
update_label.grid(row=6, column=0, pady=5, sticky="w")
check_update_button = ctk.CTkButton(general_frame, text=self.translate("Check Now"),
command=lambda: threading.Thread(target=self.check_for_new_version, args=(False,)).start())
check_update_button.grid(row=6, column=1, pady=5, padx=(10,0), sticky="w")
def render_downloads_tab(self, tab):
tab.grid_columnconfigure(0, weight=1)
tab.grid_rowconfigure(0, weight=1)
# Frame for the "Downloads" tab
downloads_frame = ctk.CTkFrame(tab, fg_color="transparent")
downloads_frame.grid(row=0, column=0, sticky="nsew", padx=20, pady=20)
downloads_frame.grid_columnconfigure(1, weight=1)
download_label = ctk.CTkLabel(downloads_frame, text=self.translate("Download Options"), font=("Helvetica", 16, "bold"))
download_label.grid(row=0, column=0, columnspan=2, sticky="w")
description_label = ctk.CTkLabel(
downloads_frame,
text=self.translate("Here you can adjust the number of simultaneous downloads, file naming, retries, and more."),
font=("Helvetica", 11),
text_color="gray"
)
description_label.grid(row=1, column=0, columnspan=2, sticky="w", pady=(0, 15))
# ----------------------------
# Simultaneous downloads
# ----------------------------
max_downloads_label = ctk.CTkLabel(downloads_frame, text=self.translate("Simultaneous Downloads"))
max_downloads_label.grid(row=2, column=0, pady=5, sticky="w")
max_downloads_combobox = ctk.CTkComboBox(
downloads_frame,
values=[str(i) for i in range(1, 11)],
state='readonly',
width=80
)
max_downloads_combobox.set(str(self.settings.get('max_downloads', 3)))
max_downloads_combobox.grid(row=2, column=1, pady=5, padx=(10, 0), sticky="w")
# ----------------------------
# Folder structure
# ----------------------------
folder_structure_label = ctk.CTkLabel(downloads_frame, text=self.translate("Folder Structure"))
folder_structure_label.grid(row=3, column=0, pady=5, sticky="w")
folder_structure_combobox = ctk.CTkComboBox(
downloads_frame,
values=["default", "post_number"],
state='readonly',
width=150
)
folder_structure_combobox.set(self.settings.get('folder_structure', 'default'))
folder_structure_combobox.grid(row=3, column=1, pady=5, padx=(10, 0), sticky="w")
# ----------------------------
# Max retries
# ----------------------------
retry_label = ctk.CTkLabel(downloads_frame, text=self.translate("Max Retries"))
retry_label.grid(row=4, column=0, pady=5, sticky="w")
retry_combobox = ctk.CTkComboBox(
downloads_frame,
values=[str(i) for i in range(0, 11)] + ['999999'],
state='readonly',
width=80
)
retry_combobox.set(str(self.settings.get('max_retries', 3)))
retry_combobox.grid(row=4, column=1, pady=5, padx=(10, 0), sticky="w")
# ----------------------------
# Retry interval
# ----------------------------
retry_interval_label = ctk.CTkLabel(downloads_frame, text=self.translate("Retry Interval (seconds)"))
retry_interval_label.grid(row=5, column=0, pady=5, sticky="w")
retry_interval_entry = ctk.CTkEntry(downloads_frame, width=80)
retry_interval_entry.insert(0, str(self.settings.get('retry_interval', 2.0)))
retry_interval_entry.grid(row=5, column=1, pady=5, padx=(10, 0), sticky="w")
# ----------------------------
# File naming mode
# ----------------------------
naming_label = ctk.CTkLabel(downloads_frame, text=self.translate("File Naming Mode"))
naming_label.grid(row=6, column=0, pady=5, sticky="w")
naming_options = [
"Use File ID (default)",
"Use Sanitized Post Name",
"Post Name + Post ID Suffix",
"Post Date/Time + Post Name"
]
# 1) Crear PRIMERO el combobox
file_naming_combobox = ctk.CTkComboBox(
downloads_frame,
values=naming_options,
state='readonly',
width=200
)
# 2) Recuperar el valor desde settings (puede ser int o str).
# Si es int (0,1,2,3) mapeamos a la cadena; si es str, la usamos tal cual.
naming_mode = self.settings.get('file_naming_mode', 0)
if isinstance(naming_mode, int):
# Asegurarnos de no pasarnos de índice
if 0 <= naming_mode < len(naming_options):
file_naming_combobox.set(naming_options[naming_mode])
else:
file_naming_combobox.set(naming_options[0])
else:
# Aquí asumes que es una cadena. Si no, fallback a la primera
if naming_mode in naming_options:
file_naming_combobox.set(naming_mode)
else:
file_naming_combobox.set(naming_options[0])
file_naming_combobox.grid(row=6, column=1, pady=5, padx=(10, 0), sticky="w")
# ----------------------------
# Botón para aplicar configuración
# ----------------------------
apply_download_button = ctk.CTkButton(
downloads_frame,
text=self.translate("Apply Download Settings"),
command=lambda: self.apply_download_settings(
max_downloads_combobox,
folder_structure_combobox,
retry_combobox,
retry_interval_entry,
file_naming_combobox
)
)
apply_download_button.grid(row=7, column=1, pady=10, sticky="e")
def render_structure_tab(self, tab):
tab.grid_columnconfigure(0, weight=1)
tab.grid_rowconfigure(1, weight=1)
structure_label = ctk.CTkLabel(
tab,
text=self.translate("Folder Structure Preview"),
font=("Helvetica", 16, "bold")
)
structure_label.grid(row=0, column=0, pady=(20,10), sticky="w")
# Descripción principal
description_label = ctk.CTkLabel(
tab,
text=self.translate("Here you can see how your downloaded files will be organized on disk."),
font=("Helvetica", 11),
text_color="gray"
)
description_label.grid(row=1, column=0, sticky="w", padx=20, pady=(0,15))
# -------- BLOQUE DE EJEMPLO: Ejemplos de nombres de archivos según la opción --------
examples_text = (
"Ejemplos de nombres según File Naming Mode:\n\n"
" • [Modo 0] Use File ID (default): \n"
" 123456.mp4 (usa el nombre/ID original del archivo)\n\n"
" • [Modo 1] Use Sanitized Post Name: \n"
" Mi_Post_Ejemplo_1_ab12.mp4 (nombre del post + índice del adjunto + hash único)\n\n"
" • [Modo 2] Post Name + Post ID Suffix: \n"
" Mi_Post_Ejemplo - 98765_1.mp4 (nombre del post + ID del post + índice del adjunto)\n"
" • [Modo 3] Post Time + Post Name: \n"
" 2000-01-01T00_00_00 - Mi_Post_Ejemplo - 98765_1.mp4 (post date/time + nombre del post + índice del adjunto + hash único)\n"
)
examples_label = ctk.CTkLabel(
tab,
text=self.translate(examples_text),
font=("Helvetica", 11),
text_color="gray",
justify="left"
)
examples_label.grid(row=2, column=0, sticky="w", padx=20, pady=(0,15))
# ------------------------------------------------------------------------------------
treeview_frame = ctk.CTkFrame(tab, fg_color="transparent")
treeview_frame.grid(row=3, column=0, sticky="nsew", padx=20, pady=20)
treeview_frame.grid_rowconfigure(0, weight=1)
treeview_frame.grid_columnconfigure(0, weight=1)
treeview_frame.grid_columnconfigure(1, weight=1)
self.default_treeview = ttk.Treeview(treeview_frame, show="tree")
self.default_treeview.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
self.post_treeview = ttk.Treeview(treeview_frame, show="tree")
self.post_treeview.grid(row=0, column=1, sticky="nsew", padx=5, pady=5)
self.update_treeview()
def export_db(self):
db_path = self.downloader.db_path
if os.path.exists(db_path):
export_path = filedialog.asksaveasfilename(defaultextension=".db",
filetypes=[("SQLite DB", "*.db")],
title=self.translate("Export Database"))
if export_path:
try:
import shutil
shutil.copy(db_path, export_path)
messagebox.showinfo(self.translate("Success"), self.translate("The database was exported successfully."))
except Exception as e:
messagebox.showerror(self.translate("Error"), self.translate(f"Error exporting database: {e}"))
else:
messagebox.showwarning(self.translate("Warning"), self.translate("Database not found."))
def clear_db(self):
confirm = messagebox.askyesno(self.translate("Confirm"),
self.translate("Are you sure you want to clear the database? This will delete all download records."))
if confirm:
try:
self.downloader.clear_database()
messagebox.showinfo(self.translate("Success"), self.translate("The database was cleared successfully."))
except Exception as e:
messagebox.showerror(self.translate("Error"), self.translate(f"Error clearing database: {e}"))
def apply_download_settings(self,max_downloads_combobox,folder_structure_combobox,retry_combobox,retry_interval_entry,file_naming_combobox):
try:
max_downloads = int(max_downloads_combobox.get())
folder_structure = folder_structure_combobox.get()
max_retries = int(retry_combobox.get())
retry_interval = float(retry_interval_entry.get())
file_naming_mode_str = file_naming_combobox.get()
mapping = {
"Use File ID (default)": 0,
"Use Sanitized Post Name": 1,
"Post Name + Post ID Suffix": 2,
"Post Date/Time + Post Name": 3
}
numeric_mode = mapping.get(file_naming_mode_str, 0)
self.downloader.file_naming_mode = numeric_mode
self.settings['max_downloads'] = max_downloads
self.settings['folder_structure'] = folder_structure
self.settings['max_retries'] = max_retries
self.settings['retry_interval'] = retry_interval
self.settings['file_naming_mode'] = numeric_mode
self.save_settings()
if self.downloader:
self.downloader.update_max_downloads(max_downloads)
self.downloader.max_retries = max_retries
self.downloader.retry_interval = retry_interval
self.downloader.file_naming_mode = numeric_mode
messagebox.showinfo(
self.translate("Éxito"),
self.translate("La configuración de descargas se aplicó correctamente.")
)
except ValueError:
messagebox.showerror(
self.translate("Error"),
self.translate("Por favor, ingresa valores numéricos válidos.")
)
def apply_language_settings(self, selected_language_name):
if selected_language_name in self.languages:
selected_language_code = self.languages[selected_language_name]
self.settings['language'] = selected_language_code
self.save_settings()
self.save_language_preference(selected_language_code)
self.load_translations(selected_language_code)
self.update_ui_texts()
messagebox.showinfo(self.translate("Success"), self.translate("The language was applied successfully."))
else:
messagebox.showwarning(self.translate("Warning"), self.translate("Please select a language."))
def update_treeview(self):
"""Update the TreeViews to display the folder structure."""
if hasattr(self, 'default_treeview') and hasattr(self, 'post_treeview'):
self.default_treeview.delete(*self.default_treeview.get_children())
self.post_treeview.delete(*self.post_treeview.get_children())
root = self.default_treeview.insert("", "end", text="User", image=self.folder_structure_icons.get('folder'))
self.default_treeview.insert(root, "end", text="images", image=self.folder_structure_icons.get('folder'))
self.default_treeview.insert(root, "end", text="videos", image=self.folder_structure_icons.get('folder'))
self.default_treeview.insert(root, "end", text="documents", image=self.folder_structure_icons.get('folder'))
self.default_treeview.insert(root, "end", text="compressed", image=self.folder_structure_icons.get('folder'))
self.default_treeview.item(root, open=True)
post_root = self.post_treeview.insert("", "end", text="User", image=self.folder_structure_icons.get('folder'))
post = self.post_treeview.insert(post_root, "end", text="post_id", image=self.folder_structure_icons.get('folder'))
self.post_treeview.insert(post, "end", text="images", image=self.folder_structure_icons.get('folder'))
self.post_treeview.insert(post, "end", text="videos", image=self.folder_structure_icons.get('folder'))
self.post_treeview.insert(post, "end", text="documents", image=self.folder_structure_icons.get('folder'))
self.post_treeview.insert(post, "end", text="compressed", image=self.folder_structure_icons.get('folder'))
self.post_treeview.item(post_root, open=True)
def clear_frame(self, frame):
for widget in frame.winfo_children():
widget.destroy()
def get_language_name(self, lang_code):
for name, code in self.languages.items():
if code == lang_code:
return name
return "English"
def change_theme_in_thread(self, theme_name):
threading.Thread(target=self.apply_theme, args=(theme_name,)).start()
def apply_theme(self, theme_name):
if theme_name.lower() == "light":
ctk.set_appearance_mode("light")
elif theme_name.lower() == "dark":
ctk.set_appearance_mode("dark")
else:
ctk.set_appearance_mode("system")
self.settings['theme'] = theme_name
self.save_settings()
messagebox.showinfo(self.translate("Success"), self.translate("The theme was applied successfully."))
def center_window(self, window, width, height):
screen_width = window.winfo_screenwidth()
screen_height = window.winfo_screenheight()
x = int((screen_width / 2) - (width / 2))
y = int((screen_height / 2) - (height / 2))
window.geometry(f'{width}x{height}+{x}+{y}')
File diff suppressed because it is too large Load Diff