sync
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user