Files

906 lines
42 KiB
Python

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}')