222 lines
8.1 KiB
Python
222 lines
8.1 KiB
Python
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
|