906 lines
42 KiB
Python
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}')
|