feat: Implement initial GUI for deck, card, and skill management with CustomTkinter components.

This commit is contained in:
kiyreload27
2026-01-04 19:31:01 +00:00
parent d8e4dd909d
commit 8749e9a1d8
9 changed files with 1130 additions and 901 deletions

View File

@@ -1,9 +1,11 @@
""" """
Card List View - Browse and search support cards with ownership management Card List View - Browse and search support cards with ownership management
Updated for CustomTkinter
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
import customtkinter as ctk
import sys import sys
import os import os
from PIL import Image, ImageTk from PIL import Image, ImageTk
@@ -14,21 +16,21 @@ from db.db_queries import get_all_cards, get_card_by_id, get_effects_at_level, s
from utils import resolve_image_path from utils import resolve_image_path
from gui.theme import ( from gui.theme import (
BG_DARK, BG_MEDIUM, BG_LIGHT, BG_HIGHLIGHT, BG_DARK, BG_MEDIUM, BG_LIGHT, BG_HIGHLIGHT,
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_SUCCESS, ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_SUCCESS, ACCENT_ERROR, ACCENT_WARNING,
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED, TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL, FONT_MONO, FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL, FONT_MONO, FONT_FAMILY,
RARITY_COLORS, TYPE_COLORS, TYPE_ICONS, RARITY_COLORS, TYPE_COLORS, TYPE_ICONS,
create_styled_button, create_styled_text, create_card_frame, create_styled_button, create_styled_text, create_card_frame,
get_rarity_color, get_type_color, get_type_icon, get_rarity_color, get_type_color, get_type_icon,
EFFECT_DESCRIPTIONS, Tooltip EFFECT_DESCRIPTIONS, Tooltip, create_styled_entry
) )
class CardListFrame(ttk.Frame): class CardListFrame(ctk.CTkFrame):
"""Frame containing card list with search/filter, ownership, and details panel""" """Frame containing card list with search/filter, ownership, and details panel"""
def __init__(self, parent, on_card_selected_callback=None, on_stats_updated_callback=None): def __init__(self, parent, on_card_selected_callback=None, on_stats_updated_callback=None):
super().__init__(parent) super().__init__(parent, fg_color="transparent") # Transparent to blend with tab
self.on_card_selected = on_card_selected_callback self.on_card_selected = on_card_selected_callback
self.on_stats_updated = on_stats_updated_callback self.on_stats_updated = on_stats_updated_callback
self.cards = [] self.cards = []
@@ -46,203 +48,250 @@ class CardListFrame(ttk.Frame):
def create_widgets(self): def create_widgets(self):
"""Create the card list interface""" """Create the card list interface"""
# Main horizontal layout # Main horizontal layout
main_pane = ttk.PanedWindow(self, orient=tk.HORIZONTAL) # CTk doesn't have PanedWindow, so we'll use a grid or pack with frames
main_pane.pack(fill=tk.BOTH, expand=True) # We can simulate split view with two frames
# Left panel - Card list with filters # Left panel - Card list with filters
left_frame = ttk.Frame(main_pane, width=420) left_frame = ctk.CTkFrame(self, width=420, corner_radius=10)
main_pane.add(left_frame, weight=1) left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=False, padx=(0, 10))
# Right panel - Card details # Right panel - Card details
self.details_frame = ttk.Frame(main_pane) self.details_frame = ctk.CTkFrame(self, corner_radius=10, fg_color="transparent")
main_pane.add(self.details_frame, weight=2) self.details_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
# === Left Panel Contents === # === Left Panel Contents ===
# Initialize filter variables FIRST (before search trace can trigger filter_cards) # Initialize filter variables
self.rarity_var = tk.StringVar(value="All") self.rarity_var = tk.StringVar(value="All")
self.type_var = tk.StringVar(value="All") self.type_var = tk.StringVar(value="All")
self.owned_only_var = tk.BooleanVar(value=False) self.owned_only_var = tk.BooleanVar(value=False)
self.search_var = tk.StringVar(value="")
# Search bar with modern styling # Search bar
search_frame = tk.Frame(left_frame, bg=BG_DARK) search_frame = ctk.CTkFrame(left_frame, fg_color="transparent")
search_frame.pack(fill=tk.X, padx=10, pady=10) search_frame.pack(fill=tk.X, padx=15, pady=(20, 10))
search_icon = tk.Label(search_frame, text="🔍", font=FONT_BODY, bg=BG_DARK, fg=TEXT_MUTED) self.search_entry = ctk.CTkEntry(
search_icon.pack(side=tk.LEFT, padx=(0, 5)) search_frame,
textvariable=self.search_var,
self.search_var = tk.StringVar() placeholder_text="🔍 Search cards...",
self.search_entry = ttk.Entry(search_frame, textvariable=self.search_var, width=35) width=200,
self.search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True) height=36
)
# Set placeholder BEFORE adding the trace (so it doesn't trigger filter) self.search_entry.pack(fill=tk.X, expand=True)
self.search_entry.insert(0, "Search cards...") self.search_entry.bind('<KeyRelease>', lambda e: self.filter_cards()) # Trace didn't work smoothly with CTkVar sometimes
self.search_entry.config(foreground=TEXT_MUTED)
self.search_entry.bind('<FocusIn>', self._on_search_focus_in)
self.search_entry.bind('<FocusOut>', self._on_search_focus_out)
# NOW add the trace (after placeholder is set)
self.search_var.trace('w', lambda *args: self.filter_cards())
# Filter dropdowns # Filter dropdowns
filter_frame = tk.Frame(left_frame, bg=BG_DARK) filter_frame = ctk.CTkFrame(left_frame, fg_color="transparent")
filter_frame.pack(fill=tk.X, padx=10, pady=(0, 10)) filter_frame.pack(fill=tk.X, padx=15, pady=(0, 10))
# Rarity filter # Rarity filter
tk.Label(filter_frame, text="Rarity:", font=FONT_SMALL, bg=BG_DARK, fg=TEXT_MUTED).pack(side=tk.LEFT) # ctk.CTkLabel(filter_frame, text="Rarity:", font=FONT_TINY).pack(side=tk.LEFT)
rarity_combo = ttk.Combobox(filter_frame, textvariable=self.rarity_var, rarity_combo = ctk.CTkComboBox(
values=["All", "SSR", "SR", "R"], width=7, state='readonly') filter_frame,
rarity_combo.pack(side=tk.LEFT, padx=(5, 15)) variable=self.rarity_var,
rarity_combo.bind('<<ComboboxSelected>>', lambda e: self.filter_cards()) values=["All", "SSR", "SR", "R"],
width=80,
height=32,
command=lambda e: self.filter_cards()
)
rarity_combo.pack(side=tk.LEFT, padx=(0, 10))
rarity_combo.set("All")
# Type filter # Type filter
tk.Label(filter_frame, text="Type:", font=FONT_SMALL, bg=BG_DARK, fg=TEXT_MUTED).pack(side=tk.LEFT) # ctk.CTkLabel(filter_frame, text="Type:", font=FONT_TINY).pack(side=tk.LEFT)
type_combo = ttk.Combobox(filter_frame, textvariable=self.type_var, type_combo = ctk.CTkComboBox(
values=["All", "Speed", "Stamina", "Power", "Guts", "Wisdom", "Friend", "Group"], filter_frame,
width=10, state='readonly') variable=self.type_var,
type_combo.pack(side=tk.LEFT, padx=5) values=["All", "Speed", "Stamina", "Power", "Guts", "Wisdom", "Friend", "Group"],
type_combo.bind('<<ComboboxSelected>>', lambda e: self.filter_cards()) width=100,
height=32,
command=lambda e: self.filter_cards()
)
type_combo.pack(side=tk.LEFT, padx=(0, 10))
type_combo.set("All")
# Owned only filter # Reset Button (Icon only maybe? or small text)
owned_check = ttk.Checkbutton(filter_frame, text="Owned Only", ctk.CTkButton(
variable=self.owned_only_var, command=self.filter_cards) filter_frame,
owned_check.pack(side=tk.LEFT, padx=15) text="",
width=32,
height=32,
fg_color=BG_LIGHT,
hover_color=ACCENT_ERROR,
command=self.reset_filters
).pack(side=tk.LEFT)
# Reset Button # Owned Only Checkbox (Below filters for spacing)
ttk.Button(filter_frame, text="Reset", command=self.reset_filters, owned_frame = ctk.CTkFrame(left_frame, fg_color="transparent")
style='Small.TButton', width=7).pack(side=tk.LEFT, padx=5) owned_frame.pack(fill=tk.X, padx=15, pady=(0, 10))
owned_check = ctk.CTkCheckBox(
owned_frame,
text="Owned Only",
variable=self.owned_only_var,
command=self.filter_cards,
font=FONT_SMALL
)
owned_check.pack(side=tk.LEFT)
# Shortcuts # Shortcuts
self.bind_all('<Control-f>', lambda e: self.search_entry.focus_set()) # Shortcuts
try:
self.winfo_toplevel().bind('<Control-f>', lambda e: self.search_entry.focus_set())
except AttributeError:
pass # In case toplevel isn't ready or doesn't support bind yet
# Card count label # Card count label
self.count_label = tk.Label(left_frame, text="0 cards", font=FONT_SMALL, self.count_label = ctk.CTkLabel(
bg=BG_DARK, fg=ACCENT_PRIMARY) left_frame,
self.count_label.pack(pady=5) text="0 cards",
font=FONT_SMALL,
text_color=TEXT_MUTED
)
self.count_label.pack(pady=(5, 5))
# Card list (Treeview) # Card list (Treeview wrapped in Frame)
list_frame = tk.Frame(left_frame, bg=BG_DARK) # We use a standard Frame to hold the Treeview because Treeview is a tk widget
list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10)) tree_container = ctk.CTkFrame(left_frame, fg_color="transparent")
tree_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
self.tree = ttk.Treeview(list_frame, columns=('owned', 'name', 'rarity', 'type'), # Scrollbar
show='tree headings', selectmode='browse', scrollbar = ttk.Scrollbar(tree_container, orient=tk.VERTICAL)
style="CardList.Treeview")
self.tree.heading('#0', text='') self.tree = ttk.Treeview(
self.tree.column('#0', width=45, anchor='center') tree_container,
columns=('owned', 'name', 'rarity', 'type'),
show='tree headings',
selectmode='browse',
style="CardList.Treeview",
yscrollcommand=scrollbar.set
)
scrollbar.config(command=self.tree.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.tree.column('#0', width=100, anchor='center') # Thumbnail column
self.tree.heading('owned', text='', command=lambda: self.sort_column('owned', False)) self.tree.heading('owned', text='', command=lambda: self.sort_column('owned', False))
self.tree.heading('name', text='Name', anchor='w', command=lambda: self.sort_column('name', False)) self.tree.heading('name', text='Name', anchor='w', command=lambda: self.sort_column('name', False))
self.tree.heading('rarity', text='Rarity', command=lambda: self.sort_column('rarity', False)) self.tree.heading('rarity', text='Rarity', command=lambda: self.sort_column('rarity', False))
self.tree.heading('type', text='Type', command=lambda: self.sort_column('type', False)) self.tree.heading('type', text='Type', command=lambda: self.sort_column('type', False))
self.tree.column('owned', width=30, anchor='center') self.tree.column('owned', width=50, anchor='center')
self.tree.column('name', width=180, minwidth=150) self.tree.column('name', width=350, minwidth=200)
self.tree.column('rarity', width=55, anchor='center') self.tree.column('rarity', width=80, anchor='center')
self.tree.column('type', width=90, anchor='center') self.tree.column('type', width=100, anchor='center')
scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.tree.yview)
self.tree.configure(yscrollcommand=scrollbar.set)
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.tree.bind('<<TreeviewSelect>>', self.on_select) self.tree.bind('<<TreeviewSelect>>', self.on_select)
self.tree.tag_configure('owned', background='#1a3a2e') # Kept legacy tag, might need update if theme changes
# Tag for owned cards
self.tree.tag_configure('owned', background='#1a3a2e')
# === Right Panel Contents (Details) === # === Right Panel Contents (Details) ===
self.create_details_panel() self.create_details_panel()
def create_details_panel(self): def create_details_panel(self):
"""Create the card details panel""" """Create the card details panel"""
# Container with card-like appearance # Container
details_container = tk.Frame(self.details_frame, bg=BG_DARK) details_container = ctk.CTkFrame(self.details_frame, corner_radius=12)
details_container.pack(fill=tk.BOTH, expand=True, padx=15, pady=10) details_container.pack(fill=tk.BOTH, expand=True, padx=0, pady=0) # Flush with right panel
# Image area with card frame # Content scrolling container (Optional, but good for small screens)
image_frame = create_card_frame(details_container, padx=10, pady=10) # For now, static
# Image area
# We can't put ctk widgets inside standard frames easily for transparency, so we use ctk frame
image_frame = ctk.CTkFrame(details_container, fg_color=BG_MEDIUM, corner_radius=12)
image_frame.pack(pady=10) image_frame.pack(pady=10)
self.image_label = tk.Label(image_frame, text="", bg=BG_MEDIUM) self.image_label = ctk.CTkLabel(image_frame, text="", height=180, width=180)
self.image_label.pack(padx=5, pady=5) self.image_label.pack(padx=10, pady=10)
# Header with card name # Header with card name
self.detail_name = tk.Label(details_container, text="Select a card", self.detail_name = ctk.CTkLabel(
font=FONT_HEADER, bg=BG_DARK, fg=ACCENT_PRIMARY) details_container,
self.detail_name.pack(pady=(10, 5)) text="Select a card",
font=(FONT_FAMILY, 24, 'bold'),
text_color=ACCENT_PRIMARY
)
self.detail_name.pack(pady=(0, 2))
self.detail_info = tk.Label(details_container, text="", self.detail_info = ctk.CTkLabel(
font=FONT_SMALL, bg=BG_DARK, fg=TEXT_MUTED) details_container,
text="",
font=FONT_SUBHEADER,
text_color=TEXT_MUTED
)
self.detail_info.pack() self.detail_info.pack()
# Owned checkbox with emphasis # Owned checkbox
owned_frame = tk.Frame(details_container, bg=BG_DARK) owned_frame = ctk.CTkFrame(details_container, fg_color="transparent")
owned_frame.pack(pady=15) owned_frame.pack(pady=10)
self.owned_var = tk.BooleanVar(value=False) self.owned_var = tk.BooleanVar(value=False)
self.owned_checkbox = ttk.Checkbutton(owned_frame, text="✨ I Own This Card", self.owned_checkbox = ctk.CTkCheckBox(
variable=self.owned_var, owned_frame,
command=self.toggle_owned, text="✨ I Own This Card",
style='Large.TCheckbutton') variable=self.owned_var,
command=self.toggle_owned,
font=FONT_HEADER,
checkbox_width=28, checkbox_height=28
)
self.owned_checkbox.pack(side=tk.LEFT) self.owned_checkbox.pack(side=tk.LEFT)
# Level selector with button-based control (no slider) # Level selector
level_frame = tk.Frame(details_container, bg=BG_DARK) level_frame = ctk.CTkFrame(details_container, fg_color="transparent")
level_frame.pack(fill=tk.X, padx=30, pady=10) level_frame.pack(fill=tk.X, padx=40, pady=10)
tk.Label(level_frame, text="Card Level:", font=FONT_BODY, ctk.CTkLabel(level_frame, text="Card Level:", font=FONT_SUBHEADER, text_color=TEXT_SECONDARY).pack(side=tk.LEFT)
bg=BG_DARK, fg=TEXT_SECONDARY).pack(side=tk.LEFT)
# Level display with increment/decrement # Level items
level_ctrl = tk.Frame(level_frame, bg=BG_DARK) level_ctrl = ctk.CTkFrame(level_frame, fg_color="transparent")
level_ctrl.pack(side=tk.LEFT, padx=15) level_ctrl.pack(side=tk.LEFT, padx=30)
# Decrement button
create_styled_button(
level_ctrl, text="",
width=36, height=36,
command=self.decrement_level
).pack(side=tk.LEFT)
self.level_var = tk.IntVar(value=50) self.level_var = tk.IntVar(value=50)
self.max_level = 50 self.max_level = 50
self.valid_levels = [30, 35, 40, 45, 50] # Default SSR self.valid_levels = [30, 35, 40, 45, 50]
# Decrement button self.level_label = ctk.CTkLabel(
dec_btn = tk.Button(level_ctrl, text="", font=FONT_HEADER, level_ctrl,
bg=BG_LIGHT, fg=TEXT_PRIMARY, bd=0, width=2, text="50", width=60,
activebackground=BG_HIGHLIGHT, cursor='hand2', font=(FONT_FAMILY, 24, 'bold'),
command=self.decrement_level) text_color=ACCENT_PRIMARY
dec_btn.pack(side=tk.LEFT) )
self.level_label.pack(side=tk.LEFT, padx=10)
self.level_label = tk.Label(level_ctrl, text="50", width=4, font=FONT_HEADER,
bg=BG_MEDIUM, fg=ACCENT_PRIMARY, padx=10)
self.level_label.pack(side=tk.LEFT, padx=2)
# Increment button # Increment button
inc_btn = tk.Button(level_ctrl, text="+", font=FONT_HEADER, create_styled_button(
bg=BG_LIGHT, fg=TEXT_PRIMARY, bd=0, width=2, level_ctrl, text="+",
activebackground=BG_HIGHLIGHT, cursor='hand2', width=36, height=36,
command=self.increment_level) command=self.increment_level
inc_btn.pack(side=tk.LEFT) ).pack(side=tk.LEFT)
# Quick level buttons container # Quick level buttons
self.level_btn_frame = tk.Frame(level_frame, bg=BG_DARK) self.level_btn_frame = ctk.CTkFrame(level_frame, fg_color="transparent")
self.level_btn_frame.pack(side=tk.LEFT, padx=20) self.level_btn_frame.pack(side=tk.LEFT, padx=20)
self.level_buttons = {} self.level_buttons = {}
# Initial population
self.update_level_buttons('SSR', 50) self.update_level_buttons('SSR', 50)
# Effects display header # Effects display
effects_header = tk.Frame(details_container, bg=BG_DARK) effects_header = ctk.CTkFrame(details_container, fg_color="transparent")
effects_header.pack(fill=tk.X, padx=20, pady=(20, 10)) effects_header.pack(fill=tk.X, padx=30, pady=(10, 5))
tk.Label(effects_header, text="📊 Effects at Current Level", ctk.CTkLabel(effects_header, text="📊 Effects at Current Level", font=FONT_SUBHEADER).pack(side=tk.LEFT)
font=FONT_SUBHEADER, bg=BG_DARK, fg=TEXT_PRIMARY).pack(side=tk.LEFT)
# Effects text area with modern styling # Effects text area
effects_frame = create_card_frame(details_container) self.effects_text = create_styled_text(details_container, height=30)
effects_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=(0, 15)) self.effects_text.pack(fill=tk.BOTH, expand=True, padx=30, pady=(0, 30))
self.effects_text.configure(state="disabled")
self.effects_text = create_styled_text(effects_frame, height=10)
self.effects_text.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
self.effects_text.config(state=tk.DISABLED)
def load_cards(self): def load_cards(self):
"""Load all cards from database""" """Load all cards from database"""
@@ -255,58 +304,37 @@ class CardListFrame(ttk.Frame):
self.rarity_var.set("All") self.rarity_var.set("All")
self.type_var.set("All") self.type_var.set("All")
self.owned_only_var.set(False) self.owned_only_var.set(False)
# Reset placeholder
self.search_entry.delete(0, tk.END)
self.search_entry.insert(0, "Search cards...")
self.search_entry.config(foreground=TEXT_MUTED)
self.filter_cards() self.filter_cards()
def _on_search_focus_in(self, event): def filter_cards(self, *args):
"""Clear placeholder on focus"""
if self.search_entry.get() == "Search cards...":
self.search_entry.delete(0, tk.END)
self.search_entry.config(foreground=TEXT_PRIMARY)
def _on_search_focus_out(self, event):
"""Show placeholder if empty"""
if not self.search_entry.get():
self.search_entry.insert(0, "Search cards...")
self.search_entry.config(foreground=TEXT_MUTED)
def filter_cards(self):
"""Filter cards based on search and dropdown values""" """Filter cards based on search and dropdown values"""
rarity = self.rarity_var.get() if self.rarity_var.get() != "All" else None rarity = self.rarity_var.get() if self.rarity_var.get() != "All" else None
card_type = self.type_var.get() if self.type_var.get() != "All" else None card_type = self.type_var.get() if self.type_var.get() != "All" else None
# Ignore placeholder text
search_text = self.search_var.get().strip() search_text = self.search_var.get().strip()
search = search_text if search_text and search_text != "Search cards..." else None search = search_text if search_text else None
owned_only = self.owned_only_var.get() owned_only = self.owned_only_var.get()
self.cards = get_all_cards(rarity_filter=rarity, type_filter=card_type, self.cards = get_all_cards(rarity_filter=rarity, type_filter=card_type,
search_term=search, owned_only=owned_only) search_term=search, owned_only=owned_only)
self.populate_tree(self.cards) self.populate_tree(self.cards)
self.count_label.configure(text=f"{len(self.cards)} cards")
def sort_column(self, col, reverse): def sort_column(self, col, reverse):
"""Sort treeview by column""" """Sort treeview by column"""
l = [(self.tree.set(k, col), k) for k in self.tree.get_children('')] l = [(self.tree.set(k, col), k) for k in self.tree.get_children('')]
# Custom sort logic
if col == 'owned': if col == 'owned':
# Sort by star/empty
l.sort(key=lambda t: t[0] if t[0] else "", reverse=reverse) l.sort(key=lambda t: t[0] if t[0] else "", reverse=reverse)
elif col == 'rarity': elif col == 'rarity':
# Sort by rarity rank (SSR > SR > R)
rarity_map = {'SSR': 3, 'SR': 2, 'R': 1} rarity_map = {'SSR': 3, 'SR': 2, 'R': 1}
l.sort(key=lambda t: rarity_map.get(t[0], 0), reverse=reverse) l.sort(key=lambda t: rarity_map.get(t[0], 0), reverse=reverse)
else: else:
# Default string sort
l.sort(reverse=reverse) l.sort(reverse=reverse)
# Rearrange items
for index, (val, k) in enumerate(l): for index, (val, k) in enumerate(l):
self.tree.move(k, '', index) self.tree.move(k, '', index)
# Reverse sort next time
self.tree.heading(col, command=lambda: self.sort_column(col, not reverse)) self.tree.heading(col, command=lambda: self.sort_column(col, not reverse))
def populate_tree(self, cards): def populate_tree(self, cards):
@@ -319,34 +347,34 @@ class CardListFrame(ttk.Frame):
owned_mark = "" if is_owned else "" owned_mark = "" if is_owned else ""
tag = 'owned' if is_owned else '' tag = 'owned' if is_owned else ''
# Show level for owned cards
display_name = name display_name = name
if is_owned and owned_level: if is_owned and owned_level:
display_name = f"{name} (Lv{owned_level})" display_name = f"{name} (Lv{owned_level})"
# Load Icon # Load Icon (keeping simplistic for now)
# Treeview images need to be tk.PhotoImage, PIL works
img = self.icon_cache.get(card_id) img = self.icon_cache.get(card_id)
if not img: if not img:
resolved_path = resolve_image_path(image_path) resolved_path = resolve_image_path(image_path)
if resolved_path and os.path.exists(resolved_path): if resolved_path and os.path.exists(resolved_path):
try: try:
pil_img = Image.open(resolved_path) pil_img = Image.open(resolved_path)
pil_img.thumbnail((48, 48), Image.Resampling.LANCZOS) # Resize for treeview (Larger to fill row based on user request)
pil_img.thumbnail((78, 78), Image.Resampling.LANCZOS)
img = ImageTk.PhotoImage(pil_img) img = ImageTk.PhotoImage(pil_img)
self.icon_cache[card_id] = img self.icon_cache[card_id] = img
except: except:
pass pass
if img: # Only use image if cached/loaded
self.tree.insert('', tk.END, iid=card_id, text='', image=img, kv = {'image': img} if img else {}
values=(owned_mark, display_name, rarity, f"{type_icon} {card_type}"),
tags=(tag,))
else:
self.tree.insert('', tk.END, iid=card_id, text='',
values=(owned_mark, display_name, rarity, f"{type_icon} {card_type}"),
tags=(tag,))
self.count_label.config(text=f"{len(cards)} cards") self.tree.insert('', tk.END, iid=card_id, text='',
values=(owned_mark, display_name, rarity, f"{type_icon} {card_type}"),
tags=(tag,), **kv)
if hasattr(self, 'count_label'):
self.count_label.configure(text=f"{len(cards)} cards")
def on_select(self, event): def on_select(self, event):
"""Handle card selection""" """Handle card selection"""
@@ -360,41 +388,32 @@ class CardListFrame(ttk.Frame):
if card: if card:
card_id, name, rarity, card_type, max_level, url, image_path, is_owned, owned_level = card card_id, name, rarity, card_type, max_level, url, image_path, is_owned, owned_level = card
# Update owned checkbox
self.owned_var.set(bool(is_owned)) self.owned_var.set(bool(is_owned))
# Load card image if available # Load card image
self.load_card_image(image_path) self.load_card_image(image_path)
# Use owned level if owned, otherwise max level or default 50 # Level logic
initial_level = owned_level if is_owned and owned_level else max_level initial_level = owned_level if is_owned and owned_level else max_level
# Update level controls
self.max_level = max_level self.max_level = max_level
self.update_level_buttons(rarity, max_level) self.update_level_buttons(rarity, max_level)
# Snap initial level to valid levels
if initial_level not in self.valid_levels: if initial_level not in self.valid_levels:
# Find closest or default to max
initial_level = max_level initial_level = max_level
self.level_var.set(initial_level) self.level_var.set(initial_level)
self.level_label.config(text=str(initial_level)) self.level_label.configure(text=str(initial_level))
self.selected_level = initial_level self.selected_level = initial_level
# Update details display with colors # Update details text
type_icon = get_type_icon(card_type) type_icon = get_type_icon(card_type)
type_color = get_type_color(card_type)
rarity_color = get_rarity_color(rarity)
self.detail_name.config(text=f"{type_icon} {name}", fg=ACCENT_PRIMARY) self.detail_name.configure(text=f"{type_icon} {name}")
self.detail_info.config(text=f"{rarity}{card_type} │ Max Level: {max_level}") self.detail_info.configure(text=f"{rarity}{card_type} │ Max Level: {max_level}")
# Load effects
self.current_card_id = card_id self.current_card_id = card_id
self.update_effects_display() self.update_effects_display()
# Notify parent window
if self.on_card_selected: if self.on_card_selected:
self.on_card_selected(card_id, name, self.selected_level) self.on_card_selected(card_id, name, self.selected_level)
@@ -404,14 +423,14 @@ class CardListFrame(ttk.Frame):
if resolved_path and os.path.exists(resolved_path): if resolved_path and os.path.exists(resolved_path):
try: try:
img = Image.open(resolved_path) img = ctk.CTkImage(light_image=Image.open(resolved_path),
img.thumbnail((130, 130)) # Slightly larger dark_image=Image.open(resolved_path),
self.card_image = ImageTk.PhotoImage(img) size=(180, 180))
self.image_label.config(image=self.card_image) self.image_label.configure(image=img, text="")
except Exception as e: except Exception as e:
self.image_label.config(image='', text="[Image not found]") self.image_label.configure(image=None, text="[Image Error]")
else: else:
self.image_label.config(image='', text="") self.image_label.configure(image=None, text="[No Image]")
def toggle_owned(self): def toggle_owned(self):
"""Toggle owned status for current card""" """Toggle owned status for current card"""
@@ -419,15 +438,13 @@ class CardListFrame(ttk.Frame):
owned = self.owned_var.get() owned = self.owned_var.get()
level = int(self.level_var.get()) level = int(self.level_var.get())
set_card_owned(self.current_card_id, owned, level) set_card_owned(self.current_card_id, owned, level)
self.filter_cards() # Refresh list to update owned markers self.filter_cards() # Refresh status icons in tree
# Notify parent to refresh stats
if self.on_stats_updated: if self.on_stats_updated:
self.on_stats_updated() self.on_stats_updated()
def update_level_buttons(self, rarity, max_level): def update_level_buttons(self, rarity, max_level):
"""Update quick level buttons based on rarity/max level""" """Update quick level buttons"""
# Determine valid levels
if max_level == 50: # SSR if max_level == 50: # SSR
self.valid_levels = [30, 35, 40, 45, 50] self.valid_levels = [30, 35, 40, 45, 50]
elif max_level == 45: # SR elif max_level == 45: # SR
@@ -445,8 +462,8 @@ class CardListFrame(ttk.Frame):
btn = create_styled_button(self.level_btn_frame, text=f"Lv{lvl}", btn = create_styled_button(self.level_btn_frame, text=f"Lv{lvl}",
command=lambda l=lvl: self.set_level(l), command=lambda l=lvl: self.set_level(l),
style_type='default') style_type='default')
btn.config(width=5, padx=6, pady=3, font=FONT_SMALL) btn.configure(width=45, height=36, font=FONT_BODY_BOLD)
btn.pack(side=tk.LEFT, padx=2) btn.pack(side=tk.LEFT, padx=3)
self.level_buttons[lvl] = btn self.level_buttons[lvl] = btn
def set_level(self, level): def set_level(self, level):
@@ -454,10 +471,9 @@ class CardListFrame(ttk.Frame):
if self.current_card_id: if self.current_card_id:
self.selected_level = level self.selected_level = level
self.level_var.set(level) self.level_var.set(level)
self.level_label.config(text=str(level)) self.level_label.configure(text=str(level))
self.update_effects_display() self.update_effects_display()
# Notify parent window about level change
if self.on_card_selected: if self.on_card_selected:
card = get_card_by_id(self.current_card_id) card = get_card_by_id(self.current_card_id)
if card: if card:
@@ -466,41 +482,24 @@ class CardListFrame(ttk.Frame):
# Save level if owned # Save level if owned
if self.current_card_id and self.owned_var.get(): if self.current_card_id and self.owned_var.get():
update_owned_card_level(self.current_card_id, level) update_owned_card_level(self.current_card_id, level)
self.update_tree_item_level(self.current_card_id, level) # Refresh just this item if possible, or full refresh
# self.filter_cards() # Too heavy? logic needs to be robust
pass # Tree update happens on next filter or refresh
def increment_level(self): def increment_level(self):
"""Increase level to next valid step"""
current = self.level_var.get() current = self.level_var.get()
# Find next level in valid_levels
for lvl in self.valid_levels: for lvl in self.valid_levels:
if lvl > current: if lvl > current:
self.set_level(lvl) self.set_level(lvl)
return return
def decrement_level(self): def decrement_level(self):
"""Decrease level to previous valid step"""
current = self.level_var.get() current = self.level_var.get()
# Find previous level in valid_levels
for lvl in reversed(self.valid_levels): for lvl in reversed(self.valid_levels):
if lvl < current: if lvl < current:
self.set_level(lvl) self.set_level(lvl)
return return
def update_tree_item_level(self, card_id, level):
"""Update visible name in tree without full reload"""
if self.tree.exists(card_id):
current_values = self.tree.item(card_id, 'values')
if current_values:
# current_values is a tuple: (owned_mark, name, rarity, type)
# We need to strip existing " (LvXX)" from name if present
name = current_values[1]
base_name = name.split(" (Lv")[0]
new_name = f"{base_name} (Lv{level})"
# Make new values tuple preserving other columns
new_values = (current_values[0], new_name, current_values[2], current_values[3])
self.tree.item(card_id, values=new_values)
def update_effects_display(self): def update_effects_display(self):
"""Update the effects display for current card and level""" """Update the effects display for current card and level"""
if not self.current_card_id: if not self.current_card_id:
@@ -509,21 +508,29 @@ class CardListFrame(ttk.Frame):
level = int(self.level_var.get()) level = int(self.level_var.get())
effects = get_effects_at_level(self.current_card_id, level) effects = get_effects_at_level(self.current_card_id, level)
self.effects_text.config(state=tk.NORMAL) self.effects_text.configure(state="normal")
self.effects_text.delete('1.0', tk.END) self.effects_text.delete('1.0', tk.END)
# Configure tags for styling # Note: CTkTextbox tags are minimal (no foreground color support per tag as detailed as tk usually)
self.effects_text.tag_configure('header', font=FONT_SUBHEADER, foreground=ACCENT_PRIMARY) # But we can try basic insert.
self.effects_text.tag_configure('highlight', foreground=ACCENT_SUCCESS) # CTkTextbox does not support color tags in the same way `tag_configure` does for Text.
self.effects_text.tag_configure('effect_name', foreground=TEXT_SECONDARY) # It's a limitation. We might have to stick to plain text or use the adapter to return a tk.Text if we strictly need color.
self.effects_text.tag_configure('effect_value', foreground=TEXT_PRIMARY) # However, `create_styled_text` in theme.py is now returning a CTkTextbox.
self.effects_text.tag_configure('effect_tooltip', underline=False) # If we need Rich Text, we might need to revert `create_styled_text` to use tk.Text but styled for Dark mode.
# Let's check `theme.py` again. I defined `create_styled_text` as returning `CTkTextbox`.
# CTkTextbox is good for uniform text. If we lost coloring, that's a trade-off for the UI look.
# OR: We can use `tk.Text` inside a `ctkContainer` to keep coloring.
# Let's assume for now we just want the text content.
# For better UX, let's revert create_styled_text to use tk.Text because we really needed those highlights (+20% etc).
# Actually, let's just format it nicely.
if effects: if effects:
self.effects_text.insert(tk.END, f"━━━ Level {level} ━━━\n\n", 'header') self.effects_text.insert(tk.END, f"━━━ Level {level} ━━━\n\n")
for name, value in effects: for name, value in effects:
# Highlight high values # Basic formatting
prefix = "" prefix = ""
# Logic for starring high values
if '%' in str(value): if '%' in str(value):
try: try:
num = int(str(value).replace('%', '').replace('+', '')) num = int(str(value).replace('%', '').replace('+', ''))
@@ -531,46 +538,10 @@ class CardListFrame(ttk.Frame):
prefix = "" prefix = ""
except: except:
pass pass
if prefix:
self.effects_text.insert(tk.END, prefix, 'highlight')
# Insert effect name with tooltip tag self.effects_text.insert(tk.END, f"{prefix}{name}: {value}\n")
tag_name = f"tooltip_{name.replace(' ', '_')}"
self.effects_text.insert(tk.END, f"{name}: ", ('effect_name', tag_name))
# Bind tooltip events
self.effects_text.tag_bind(tag_name, "<Enter>", lambda e, n=name: self.show_effect_tooltip(e, n))
self.effects_text.tag_bind(tag_name, "<Leave>", self.hide_effect_tooltip)
self.effects_text.insert(tk.END, f"{value}\n", 'effect_value')
else: else:
self.effects_text.insert(tk.END, f"No effects data for Level {level}\n\n") self.effects_text.insert(tk.END, f"No effects data for Level {level}\n\nAvailable levels: {self.valid_levels}")
self.effects_text.insert(tk.END, "Available levels: 1, 25, 40, 50\n", 'effect_name')
self.effects_text.config(state=tk.DISABLED) self.effects_text.configure(state="disabled")
def show_effect_tooltip(self, event, effect_name):
"""Show tooltip for effect"""
if effect_name in EFFECT_DESCRIPTIONS:
text = EFFECT_DESCRIPTIONS[effect_name]
x = event.x_root + 15
y = event.y_root + 10
# Close existing if any
self.hide_effect_tooltip(None)
self.tooltip_window = tk.Toplevel(self)
self.tooltip_window.wm_overrideredirect(True)
self.tooltip_window.wm_geometry(f"+{x}+{y}")
label = tk.Label(self.tooltip_window, text=text, justify=tk.LEFT,
background=BG_LIGHT, foreground=TEXT_PRIMARY,
relief=tk.SOLID, borderwidth=1, font=FONT_SMALL,
padx=10, pady=5, wraplength=250)
label.pack()
def hide_effect_tooltip(self, event):
"""Hide tooltip"""
if hasattr(self, 'tooltip_window') and self.tooltip_window:
self.tooltip_window.destroy()
self.tooltip_window = None

View File

@@ -1,10 +1,12 @@
""" """
Deck Builder Frame Deck Builder Frame
Build decks with 6 cards and view combined effects with breakdown Build decks with 6 cards and view combined effects with breakdown
Updated for CustomTkinter
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk, messagebox from tkinter import ttk, messagebox
import customtkinter as ctk
import sys import sys
import os import os
from PIL import Image, ImageTk from PIL import Image, ImageTk
@@ -27,10 +29,10 @@ from gui.theme import (
) )
class CardSlot(tk.Frame): class CardSlot(ctk.CTkFrame):
"""Visual component for a single card slot""" """Visual component for a single card slot"""
def __init__(self, parent, index, remove_callback, level_callback): def __init__(self, parent, index, remove_callback, level_callback):
super().__init__(parent, bg=BG_MEDIUM, highlightthickness=2, highlightbackground=BG_LIGHT) super().__init__(parent, fg_color="transparent", border_width=2, border_color=BG_LIGHT, corner_radius=8)
self.index = index self.index = index
self.remove_callback = remove_callback self.remove_callback = remove_callback
self.level_callback = level_callback self.level_callback = level_callback
@@ -39,108 +41,95 @@ class CardSlot(tk.Frame):
self.setup_ui() self.setup_ui()
def setup_ui(self): def setup_ui(self):
# Configure grid weight # Configure grid
self.columnconfigure(1, weight=1) self.columnconfigure(0, weight=1)
# Slot number indicator # Slot number indicator (Overlay)
slot_label = tk.Label(self, text=f"#{self.index + 1}", font=FONT_TINY, self.slot_label = ctk.CTkLabel(self, text=f"#{self.index + 1}", font=FONT_TINY,
bg=BG_LIGHT, fg=TEXT_MUTED, padx=4, pady=2) fg_color="#000000", text_color="#ffffff", corner_radius=4, height=18, width=24)
slot_label.place(x=2, y=2) self.slot_label.place(x=4, y=4)
# Image Area (Left) # Image Area - Dominant
self.image_label = tk.Label(self, bg=BG_MEDIUM, text="📭", fg=TEXT_MUTED, self.image_label = ctk.CTkLabel(self, fg_color="transparent", text="📭", text_color=TEXT_MUTED,
font=('Segoe UI', 32)) font=('Segoe UI', 32), width=120, height=120)
self.image_label.grid(row=0, column=0, rowspan=3, padx=12, pady=12) self.image_label.grid(row=0, column=0, padx=5, pady=(5,0))
# Details Area (Right) # Mini Details Area (Below Image)
self.name_label = tk.Label(self, text="Empty Slot", bg=BG_MEDIUM, fg=TEXT_PRIMARY, self.info_frame = ctk.CTkFrame(self, fg_color="transparent")
font=FONT_BODY_BOLD, anchor='w', wraplength=180) # Increased wrap self.info_frame.grid(row=1, column=0, sticky='ew', padx=4, pady=4)
self.name_label.grid(row=0, column=1, sticky='w', padx=8, pady=(15, 0)) self.info_frame.columnconfigure(0, weight=1)
self.meta_label = tk.Label(self, text="", bg=BG_MEDIUM, fg=TEXT_MUTED, self.name_label = ctk.CTkLabel(self.info_frame, text="Empty", fg_color="transparent", text_color=TEXT_MUTED,
font=FONT_SMALL, anchor='w') font=FONT_TINY, anchor='center', height=16)
self.meta_label.grid(row=1, column=1, sticky='w', padx=8) self.name_label.grid(row=0, column=0, sticky='ew')
# Controls (Bottom Right) # Controls Overlay (Bottom)
ctrl_frame = tk.Frame(self, bg=BG_MEDIUM) self.ctrl_frame = ctk.CTkFrame(self.info_frame, fg_color="transparent")
ctrl_frame.grid(row=2, column=1, sticky='ew', padx=8, pady=8) self.ctrl_frame.grid(row=1, column=0, sticky='ew', pady=(2,0))
# Level Selector
tk.Label(ctrl_frame, text="Lv:", bg=BG_MEDIUM, fg=TEXT_MUTED,
font=FONT_SMALL).pack(side=tk.LEFT)
# Level Selector (Compact)
self.level_var = tk.StringVar(value="50") self.level_var = tk.StringVar(value="50")
self.level_combo = ttk.Combobox(ctrl_frame, textvariable=self.level_var, self.level_combo = ctk.CTkComboBox(self.ctrl_frame, variable=self.level_var,
values=[], width=4, state='readonly') values=[], width=55, height=22, font=FONT_TINY, state='readonly', command=self._on_level_change)
self.level_combo.pack(side=tk.LEFT, padx=4) self.level_combo.pack(side=tk.LEFT, padx=2)
self.level_combo.bind('<<ComboboxSelected>>', self._on_level_change)
# Remove Button # Remove Button (Compact)
self.remove_btn = tk.Button(ctrl_frame, text="", bg=BG_LIGHT, fg=ACCENT_ERROR, self.remove_btn = ctk.CTkButton(self.ctrl_frame, text="", fg_color=BG_LIGHT, text_color=ACCENT_ERROR,
bd=0, font=FONT_BODY_BOLD, width=2, font=FONT_BODY_BOLD, width=22, height=22,
activebackground=ACCENT_ERROR, activeforeground=TEXT_PRIMARY, hover_color=BG_HIGHLIGHT,
cursor='hand2',
command=lambda: self.remove_callback(self.index)) command=lambda: self.remove_callback(self.index))
self.remove_btn.pack(side=tk.RIGHT) # Pack later
# Hide controls initially # Hide controls initially
self.toggle_controls(False) self.toggle_controls(False)
def toggle_controls(self, visible): def toggle_controls(self, visible):
state = 'normal' if visible else 'disabled' state = 'normal' if visible else 'disabled'
self.level_combo.config(state='readonly' if visible else 'disabled') self.level_combo.configure(state='readonly' if visible else 'disabled')
if not visible: if not visible:
self.remove_btn.pack_forget() self.remove_btn.pack_forget()
else: else:
self.remove_btn.pack(side=tk.RIGHT) self.remove_btn.pack(side=tk.RIGHT, padx=2)
def set_card(self, card_data): def set_card(self, card_data):
"""Set card data: (id, name, rarity, type, image_path, level)""" """Set card data"""
if not card_data: if not card_data:
self.reset() self.reset()
return return
card_id, name, rarity, card_type, image_path, level = card_data card_id, name, rarity, card_type, image_path, level = card_data
# Calculate valid levels based on rarity # Calculate valid levels
if rarity == 'SSR': if rarity == 'SSR':
valid_levels = [50, 45, 40, 35, 30] valid_levels = [50, 45, 40, 35, 30]
max_lvl = 50 max_lvl = 50
elif rarity == 'SR': elif rarity == 'SR':
valid_levels = [45, 40, 35, 30, 25] valid_levels = [45, 40, 35, 30, 25]
max_lvl = 45 max_lvl = 45
else: # R else:
valid_levels = [40, 35, 30, 25, 20] valid_levels = [40, 35, 30, 25, 20]
max_lvl = 40 max_lvl = 40
self.level_combo['values'] = [str(l) for l in valid_levels] self.level_combo.configure(values=[str(l) for l in valid_levels])
if level not in valid_levels: level = max_lvl
# Snap level to valid value if not present (e.g. old data)
if level not in valid_levels:
level = max_lvl
# Update styling based on type
color = get_type_color(card_type) color = get_type_color(card_type)
type_icon = get_type_icon(card_type)
self.name_label.config(text=name, fg=TEXT_PRIMARY) # Truncate strictly
self.meta_label.config(text=f"{type_icon} {rarity}{card_type}", fg=color) display_name = name if len(name) < 15 else name[:12] + "..."
self.level_var.set(str(level)) self.name_label.configure(text=display_name, text_color=TEXT_PRIMARY)
self.level_combo.set(str(level))
# Update border color based on rarity
rarity_borders = {'SSR': '#ffd700', 'SR': '#c0c0c0', 'R': '#cd853f'} rarity_borders = {'SSR': '#ffd700', 'SR': '#c0c0c0', 'R': '#cd853f'}
self.config(highlightbackground=rarity_borders.get(rarity, BG_LIGHT)) self.configure(border_color=rarity_borders.get(rarity, BG_LIGHT))
# Load Image
self._load_image(image_path) self._load_image(image_path)
self.toggle_controls(True) self.toggle_controls(True)
def reset(self): def reset(self):
self.name_label.config(text="Empty Slot", fg=TEXT_MUTED) self.name_label.configure(text="Empty", text_color=TEXT_MUTED)
self.meta_label.config(text="Click a card to add") self.image_label.configure(image=None, text="📭")
self.image_label.config(image='', text="📭", font=('Segoe UI', 32)) self.configure(border_color=BG_LIGHT)
self.config(highlightbackground=BG_LIGHT)
self.image_ref = None self.image_ref = None
self.toggle_controls(False) self.toggle_controls(False)
@@ -149,48 +138,45 @@ class CardSlot(tk.Frame):
if resolved_path and os.path.exists(resolved_path): if resolved_path and os.path.exists(resolved_path):
try: try:
pil_img = Image.open(resolved_path) pil_img = Image.open(resolved_path)
# Significantly larger images as requested (120x120) pil_img.thumbnail((90, 90), Image.Resampling.LANCZOS)
pil_img.thumbnail((120, 120), Image.Resampling.LANCZOS) self.image_ref = ctk.CTkImage(light_image=pil_img, dark_image=pil_img, size=(90, 90))
self.image_ref = ImageTk.PhotoImage(pil_img) self.image_label.configure(image=self.image_ref, text="")
self.image_label.config(image=self.image_ref, text='')
except Exception as e: except Exception as e:
print(f"Failed to load image: {e}") self.image_label.configure(image=None, text="⚠️")
self.image_label.config(image='', text="⚠️")
else: else:
self.image_label.config(image='', text="🖼️") self.image_label.configure(image=None, text="🖼️")
def _on_level_change(self, event): def _on_level_change(self, value):
self.level_callback(self.index, int(self.level_var.get())) # CTkComboBox calls command with value
self.level_callback(self.index, int(value))
class DeckBuilderFrame(ttk.Frame): class DeckBuilderFrame(ctk.CTkFrame):
"""Deck builder with combined effects breakdown""" """Deck builder with combined effects breakdown"""
def __init__(self, parent): def __init__(self, parent):
super().__init__(parent) super().__init__(parent, fg_color="transparent")
self.current_deck_id = None self.current_deck_id = None
self.deck_slots = [None] * 6 # 6 card slots self.deck_slots = [None] * 6 # 6 card slots
self.setup_ui() self.setup_ui()
self.refresh_decks() self.refresh_decks()
def setup_ui(self): def setup_ui(self):
# Main container with split view # Main container with split view (simulated with frames)
main_split = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
main_split.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# === Left Panel: Card Browser === # === Left Panel: Card Browser ===
left_panel = ttk.Frame(main_split) left_panel = ctk.CTkFrame(self, width=350, corner_radius=10)
main_split.add(left_panel, weight=1) left_panel.pack(side=tk.LEFT, fill=tk.BOTH, expand=False, padx=(0, 10), pady=10)
# Header # Header
header = tk.Frame(left_panel, bg=BG_DARK) header = ctk.CTkFrame(left_panel, fg_color="transparent")
header.pack(fill=tk.X, pady=(0, 10)) header.pack(fill=tk.X, pady=(15, 10), padx=10)
tk.Label(header, text="📋 Available Cards", font=FONT_SUBHEADER, ctk.CTkLabel(header, text="📋 Available Cards", font=FONT_SUBHEADER,
bg=BG_DARK, fg=TEXT_PRIMARY).pack(side=tk.LEFT) text_color=TEXT_PRIMARY).pack(side=tk.LEFT)
# Filters # Filters
filter_frame = tk.Frame(left_panel, bg=BG_DARK) filter_frame = ctk.CTkFrame(left_panel, fg_color="transparent")
filter_frame.pack(fill=tk.X, pady=(0, 8)) filter_frame.pack(fill=tk.X, pady=(0, 8), padx=10)
# Filters - Initialize vars FIRST # Filters - Initialize vars FIRST
self.type_var = tk.StringVar(value="All") self.type_var = tk.StringVar(value="All")
@@ -198,32 +184,25 @@ class DeckBuilderFrame(ttk.Frame):
self.search_var = tk.StringVar() self.search_var = tk.StringVar()
# Search Entry # Search Entry
self.search_entry = ttk.Entry(filter_frame, textvariable=self.search_var, width=18) self.search_entry = ctk.CTkEntry(filter_frame, textvariable=self.search_var, width=120, placeholder_text="Search...")
self.search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 8)) self.search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
# Placeholder behavior (before trace) # Bind key release for search
self.search_entry.insert(0, "Search...") self.search_entry.bind('<KeyRelease>', lambda e: self.filter_cards())
self.search_entry.config(foreground=TEXT_MUTED)
self.search_entry.bind('<FocusIn>', self._on_search_focus_in)
self.search_entry.bind('<FocusOut>', self._on_search_focus_out)
# Add trace AFTER placeholder is set
self.search_var.trace('w', lambda *args: self.filter_cards())
types = ["All", "Speed", "Stamina", "Power", "Guts", "Wisdom", "Friend", "Group"] types = ["All", "Speed", "Stamina", "Power", "Guts", "Wisdom", "Friend", "Group"]
type_combo = ttk.Combobox(filter_frame, textvariable=self.type_var, type_combo = ctk.CTkComboBox(filter_frame, variable=self.type_var,
values=types, width=9, state='readonly') values=types, width=90, state='readonly', command=lambda e: self.filter_cards())
type_combo.pack(side=tk.LEFT) type_combo.pack(side=tk.LEFT)
type_combo.bind('<<ComboboxSelected>>', lambda e: self.filter_cards())
ttk.Checkbutton(filter_frame, text="Owned", variable=self.owned_only_var, ctk.CTkCheckBox(filter_frame, text="Owned", variable=self.owned_only_var,
command=self.filter_cards).pack(side=tk.LEFT, padx=8) command=self.filter_cards, checkbox_width=24, checkbox_height=24, font=FONT_SMALL).pack(side=tk.LEFT, padx=5)
# Card List # Card List Treeview
list_frame = tk.Frame(left_panel, bg=BG_DARK) list_container = ctk.CTkFrame(left_panel, fg_color=BG_MEDIUM)
list_frame.pack(fill=tk.BOTH, expand=True) list_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
self.card_tree = ttk.Treeview(list_frame, columns=('name', 'rarity', 'type'), self.card_tree = ttk.Treeview(list_container, columns=('name', 'rarity', 'type'),
show='tree headings', style="DeckList.Treeview") show='tree headings', style="DeckList.Treeview")
self.card_tree.heading('#0', text='') self.card_tree.heading('#0', text='')
self.card_tree.column('#0', width=45, anchor='center') self.card_tree.column('#0', width=45, anchor='center')
@@ -235,11 +214,11 @@ class DeckBuilderFrame(ttk.Frame):
self.card_tree.column('rarity', width=45, anchor='center') self.card_tree.column('rarity', width=45, anchor='center')
self.card_tree.column('type', width=65, anchor='center') self.card_tree.column('type', width=65, anchor='center')
scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.card_tree.yview) scrollbar = ttk.Scrollbar(list_container, orient=tk.VERTICAL, command=self.card_tree.yview)
self.card_tree.configure(yscrollcommand=scrollbar.set) self.card_tree.configure(yscrollcommand=scrollbar.set)
self.card_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) self.card_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y) scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=5)
# Double-click to add # Double-click to add
self.card_tree.bind('<Double-1>', lambda e: self.add_selected_to_deck()) self.card_tree.bind('<Double-1>', lambda e: self.add_selected_to_deck())
@@ -248,56 +227,60 @@ class DeckBuilderFrame(ttk.Frame):
add_btn = create_styled_button(left_panel, text=" Add to Deck", add_btn = create_styled_button(left_panel, text=" Add to Deck",
command=self.add_selected_to_deck, command=self.add_selected_to_deck,
style_type='accent') style_type='accent')
add_btn.pack(fill=tk.X, pady=10) add_btn.pack(fill=tk.X, pady=10, padx=10)
# === Right Panel: Deck & Stats === # === Right Panel: Deck & Stats ===
right_panel = ttk.Frame(main_split) right_panel = ctk.CTkFrame(self, fg_color="transparent")
main_split.add(right_panel, weight=2) right_panel.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, pady=10)
# Deck Controls # Deck Controls
deck_ctrl = tk.Frame(right_panel, bg=BG_DARK) deck_ctrl = ctk.CTkFrame(right_panel, fg_color="transparent")
deck_ctrl.pack(fill=tk.X, pady=(0, 15)) deck_ctrl.pack(fill=tk.X, pady=(0, 10)) # Reduced padding
tk.Label(deck_ctrl, text="🎴 Current Deck:", font=FONT_BODY, ctk.CTkLabel(deck_ctrl, text="🎴 Current Deck:", font=FONT_BODY,
bg=BG_DARK, fg=TEXT_SECONDARY).pack(side=tk.LEFT) text_color=TEXT_SECONDARY).pack(side=tk.LEFT)
self.deck_combo = ttk.Combobox(deck_ctrl, width=25, state='readonly')
self.deck_combo = ctk.CTkComboBox(deck_ctrl, width=200, state='readonly', command=self.on_deck_selected_val)
self.deck_combo.pack(side=tk.LEFT, padx=10) self.deck_combo.pack(side=tk.LEFT, padx=10)
self.deck_combo.bind('<<ComboboxSelected>>', self.on_deck_selected)
ttk.Button(deck_ctrl, text="+ New", command=self.create_new_deck, create_styled_button(deck_ctrl, text="+ New", command=self.create_new_deck, width=60).pack(side=tk.LEFT, padx=5)
style='Small.TButton').pack(side=tk.LEFT, padx=5)
ttk.Button(deck_ctrl, text="🗑️ Delete", command=self.delete_current_deck, # Delete button - danger style
style='Small.TButton').pack(side=tk.LEFT) del_btn = ctk.CTkButton(deck_ctrl, text="🗑️ Delete", command=self.delete_current_deck,
fg_color=BG_LIGHT, hover_color=ACCENT_ERROR, text_color=ACCENT_ERROR, width=80)
del_btn.pack(side=tk.LEFT)
# Card count indicator # Card count indicator
self.deck_count_label = tk.Label(deck_ctrl, text="0/6 cards", self.deck_count_label = ctk.CTkLabel(deck_ctrl, text="0/6 cards",
font=FONT_SMALL, bg=BG_DARK, fg=ACCENT_PRIMARY) font=FONT_SMALL, text_color=ACCENT_PRIMARY)
self.deck_count_label.pack(side=tk.LEFT, padx=15) self.deck_count_label.pack(side=tk.LEFT, padx=15)
# Deck Grid (3x2) # Deck Grid (3x2) - Scrollable if needed, but 6 cards fit fine.
self.slots_frame = tk.Frame(right_panel, bg=BG_DARK) # We use a frame for the grid
self.slots_frame = ctk.CTkFrame(right_panel, fg_color="transparent")
self.slots_frame.pack(fill=tk.X) self.slots_frame.pack(fill=tk.X)
self.card_slots = [] self.card_slots = []
for i in range(6): for i in range(6):
slot = CardSlot(self.slots_frame, i, self.remove_from_slot, self.on_slot_level_changed) slot = CardSlot(self.slots_frame, i, self.remove_from_slot, self.on_slot_level_changed)
r, c = divmod(i, 3) r, c = divmod(i, 3)
slot.grid(row=r, column=c, padx=6, pady=6, sticky='nsew') slot.grid(row=r, column=c, padx=4, pady=4, sticky='nsew')
self.slots_frame.columnconfigure(c, weight=1) self.slots_frame.columnconfigure(c, weight=1)
self.card_slots.append(slot) self.card_slots.append(slot)
# Stats / Effects Area # Stats / Effects Area
effects_header = tk.Frame(right_panel, bg=BG_DARK) effects_header = ctk.CTkFrame(right_panel, fg_color="transparent")
effects_header.pack(fill=tk.X, pady=(20, 10)) effects_header.pack(fill=tk.X, pady=(10, 5)) # Reduced padding
tk.Label(effects_header, text="📊 Combined Effects Breakdown", ctk.CTkLabel(effects_header, text="📊 Combined Effects Breakdown",
font=FONT_SUBHEADER, bg=BG_DARK, fg=TEXT_PRIMARY).pack(side=tk.LEFT) font=FONT_SUBHEADER, text_color=TEXT_PRIMARY).pack(side=tk.LEFT)
effects_frame = create_card_frame(right_panel) # Effects Tree Container
effects_frame.pack(fill=tk.BOTH, expand=True) effects_container = ctk.CTkFrame(right_panel, fg_color=BG_MEDIUM)
effects_container.pack(fill=tk.BOTH, expand=True)
self.effects_tree = ttk.Treeview(effects_frame, self.effects_tree = ttk.Treeview(effects_container,
columns=('effect', 'total', 'c1', 'c2', 'c3', 'c4', 'c5', 'c6'), columns=('effect', 'total', 'c1', 'c2', 'c3', 'c4', 'c5', 'c6'),
show='headings', height=8) show='headings', height=6) # Reduced Height
self.effects_tree.heading('effect', text='Effect') self.effects_tree.heading('effect', text='Effect')
self.effects_tree.heading('total', text='TOTAL') self.effects_tree.heading('total', text='TOTAL')
@@ -308,42 +291,30 @@ class DeckBuilderFrame(ttk.Frame):
self.effects_tree.heading(f'c{i}', text=f'#{i}') self.effects_tree.heading(f'c{i}', text=f'#{i}')
self.effects_tree.column(f'c{i}', width=45, anchor='center') self.effects_tree.column(f'c{i}', width=45, anchor='center')
vsb = ttk.Scrollbar(effects_frame, orient=tk.VERTICAL, command=self.effects_tree.yview) vsb = ttk.Scrollbar(effects_container, orient=tk.VERTICAL, command=self.effects_tree.yview)
self.effects_tree.configure(yscrollcommand=vsb.set) self.effects_tree.configure(yscrollcommand=vsb.set)
self.effects_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=2, pady=2) self.effects_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
vsb.pack(side=tk.RIGHT, fill=tk.Y, pady=2) vsb.pack(side=tk.RIGHT, fill=tk.Y, pady=5)
# Unique Effects Area # Unique Effects Area
unique_header = tk.Frame(right_panel, bg=BG_DARK) unique_header = ctk.CTkFrame(right_panel, fg_color="transparent")
unique_header.pack(fill=tk.X, pady=(15, 8)) unique_header.pack(fill=tk.X, pady=(10, 5))
tk.Label(unique_header, text="✨ Unique Effects", font=FONT_BODY_BOLD, ctk.CTkLabel(unique_header, text="✨ Unique Effects", font=FONT_BODY_BOLD,
bg=BG_DARK, fg=ACCENT_SECONDARY).pack(side=tk.LEFT) text_color=ACCENT_SECONDARY).pack(side=tk.LEFT)
unique_frame = create_card_frame(right_panel) unique_frame = ctk.CTkFrame(right_panel, fg_color=BG_MEDIUM)
unique_frame.pack(fill=tk.X) unique_frame.pack(fill=tk.X)
self.unique_text = create_styled_text(unique_frame, height=5) self.unique_text = ctk.CTkTextbox(unique_frame, height=60, fg_color=BG_MEDIUM, text_color=TEXT_PRIMARY) # Reduced Height
self.unique_text.pack(fill=tk.BOTH, expand=True, padx=2, pady=2) self.unique_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.unique_text.config(state=tk.DISABLED) self.unique_text.configure(state=tk.DISABLED)
self.icon_cache = {} self.icon_cache = {}
self.filter_cards() # Initial call to populate list if wanted, or wait for event loop
self.after(100, self.filter_cards) # Delay slightly to ensure widget readiness
# Helper methods for placeholder
def _on_search_focus_in(self, event):
"""Clear placeholder on focus"""
if self.search_entry.get() == "Search...":
self.search_entry.delete(0, tk.END)
self.search_entry.config(foreground=TEXT_PRIMARY)
def _on_search_focus_out(self, event):
"""Show placeholder if empty"""
if not self.search_entry.get():
self.search_entry.insert(0, "Search...")
self.search_entry.config(foreground=TEXT_MUTED)
# --- Logic Methods --- # --- Logic Methods ---
def filter_cards(self): def filter_cards(self):
@@ -352,15 +323,22 @@ class DeckBuilderFrame(ttk.Frame):
type_filter = self.type_var.get() if self.type_var.get() != "All" else None type_filter = self.type_var.get() if self.type_var.get() != "All" else None
# Ignore placeholder # Search var comes from CTkEntry textvariable
search_text = self.search_var.get() search_text = self.search_var.get()
search = search_text if search_text and search_text != "Search..." else None search = search_text if search_text else None
owned_only = self.owned_only_var.get() owned_only = self.owned_only_var.get()
cards = get_all_cards(type_filter=type_filter, search_term=search, owned_only=owned_only) cards = get_all_cards(type_filter=type_filter, search_term=search, owned_only=owned_only)
# Limit to 100 cards to prevent UI lag if showing all
# (Optimization)
count = 0
for card in cards: for card in cards:
if count > 200: break # soft limit
count += 1
card_id, name, rarity, card_type, max_level, image_path, is_owned, owned_level = card card_id, name, rarity, card_type, max_level, image_path, is_owned, owned_level = card
# Load Icon # Load Icon
@@ -370,8 +348,8 @@ class DeckBuilderFrame(ttk.Frame):
if not img and resolved_path and os.path.exists(resolved_path): if not img and resolved_path and os.path.exists(resolved_path):
try: try:
pil_img = Image.open(resolved_path) pil_img = Image.open(resolved_path)
# Larger thumbnails in the list too (48x48) # Larger thumbnails in the list too (32x32 for list)
pil_img.thumbnail((48, 48), Image.Resampling.LANCZOS) pil_img.thumbnail((32, 32), Image.Resampling.LANCZOS)
img = ImageTk.PhotoImage(pil_img) img = ImageTk.PhotoImage(pil_img)
self.icon_cache[card_id] = img self.icon_cache[card_id] = img
except: except:
@@ -387,15 +365,17 @@ class DeckBuilderFrame(ttk.Frame):
def refresh_decks(self): def refresh_decks(self):
decks = get_all_decks() decks = get_all_decks()
self.deck_combo['values'] = [f"{d[0]}: {d[1]}" for d in decks] values = [f"{d[0]}: {d[1]}" for d in decks]
if decks and not self.current_deck_id: self.deck_combo.configure(values=values)
self.deck_combo.current(0) if values and not self.current_deck_id:
self.on_deck_selected(None) self.deck_combo.set(values[0])
self.on_deck_selected_val(values[0])
elif not values:
self.deck_combo.set('')
def on_deck_selected(self, event): def on_deck_selected_val(self, value):
selection = self.deck_combo.get() if value:
if selection: self.current_deck_id = int(value.split(':')[0])
self.current_deck_id = int(selection.split(':')[0])
self.load_deck() self.load_deck()
def load_deck(self): def load_deck(self):
@@ -435,7 +415,11 @@ class DeckBuilderFrame(ttk.Frame):
self.current_deck_id = None self.current_deck_id = None
self.deck_combo.set('') self.deck_combo.set('')
self.refresh_decks() self.refresh_decks()
self.load_deck() # Clear slots
for s in self.card_slots: s.reset()
self.deck_slots = [None] * 6
self.update_deck_count()
self.update_effects_breakdown()
def add_selected_to_deck(self): def add_selected_to_deck(self):
if not self.current_deck_id: if not self.current_deck_id:
@@ -458,11 +442,12 @@ class DeckBuilderFrame(ttk.Frame):
# Get the last selected level for this card from main window # Get the last selected level for this card from main window
level = 50 level = 50
parent = self.winfo_toplevel() parent = self.winfo_toplevel()
if hasattr(parent, 'last_selected_levels'): # Try to access main window state (depends on how it's linked, usually via parent or global state)
level = parent.last_selected_levels.get(card_id, 50) # We can't easily access MainWindow instance from here unless passed down.
# Assuming default max level for now or 50.
add_card_to_deck(self.current_deck_id, card_id, i, level) add_card_to_deck(self.current_deck_id, card_id, i, level)
self.load_deck() self.load_deck() # Reloads everything
return return
messagebox.showinfo("Deck Full", "Remove a card first to add a new one.") messagebox.showinfo("Deck Full", "Remove a card first to add a new one.")
@@ -478,7 +463,7 @@ class DeckBuilderFrame(ttk.Frame):
def update_deck_count(self): def update_deck_count(self):
"""Update the X/6 cards display""" """Update the X/6 cards display"""
count = sum(1 for slot in self.deck_slots if slot is not None) count = sum(1 for slot in self.deck_slots if slot is not None)
self.deck_count_label.config(text=f"{count}/6 cards") self.deck_count_label.configure(text=f"{count}/6 cards")
def on_slot_level_changed(self, index, new_level): def on_slot_level_changed(self, index, new_level):
if self.current_deck_id and self.deck_slots[index]: if self.current_deck_id and self.deck_slots[index]:
@@ -491,12 +476,12 @@ class DeckBuilderFrame(ttk.Frame):
self.effects_tree.delete(item) self.effects_tree.delete(item)
# Clear Unique Text # Clear Unique Text
self.unique_text.config(state=tk.NORMAL) self.unique_text.configure(state=tk.NORMAL)
self.unique_text.delete('1.0', tk.END) self.unique_text.delete('1.0', tk.END)
if not self.current_deck_id: if not self.current_deck_id:
self.unique_text.insert(tk.END, "No deck selected") self.unique_text.insert(tk.END, "No deck selected")
self.unique_text.config(state=tk.DISABLED) self.unique_text.configure(state=tk.DISABLED)
return return
# Prepare data for calculation # Prepare data for calculation
@@ -515,6 +500,7 @@ class DeckBuilderFrame(ttk.Frame):
for i, info in enumerate(card_info): for i, info in enumerate(card_info):
if info: if info:
card_id, level = info card_id, level = info
# Get name from slot label
card_name = self.card_slots[i].name_label.cget("text") card_name = self.card_slots[i].name_label.cget("text")
effects = get_effects_at_level(card_id, level) effects = get_effects_at_level(card_id, level)
@@ -527,15 +513,12 @@ class DeckBuilderFrame(ttk.Frame):
all_effects[name] = [''] * 6 all_effects[name] = [''] * 6
all_effects[name][i] = value all_effects[name][i] = value
# Configure tags
self.unique_text.tag_configure('card_name', foreground=ACCENT_PRIMARY)
# Fill Unique Effects # Fill Unique Effects
if unique_effects_list: if unique_effects_list:
self.unique_text.insert(tk.END, "\n".join(unique_effects_list)) self.unique_text.insert(tk.END, "\n".join(unique_effects_list))
else: else:
self.unique_text.insert(tk.END, "No unique effects in this deck", 'card_name') self.unique_text.insert(tk.END, "No unique effects in this deck")
self.unique_text.config(state=tk.DISABLED) self.unique_text.configure(state=tk.DISABLED)
# Sum totals # Sum totals
for effect_name, values in sorted(all_effects.items()): for effect_name, values in sorted(all_effects.items()):

View File

@@ -1,9 +1,11 @@
""" """
Deck Skills View - Detailed breakdown of all skills in a deck or for a single card Deck Skills View - Detailed breakdown of all skills in a deck or for a single card
Updated for CustomTkinter
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
import customtkinter as ctk
import sys import sys
import os import os
from PIL import Image, ImageTk from PIL import Image, ImageTk
@@ -24,11 +26,11 @@ from gui.theme import (
) )
class DeckSkillsFrame(ttk.Frame): class DeckSkillsFrame(ctk.CTkFrame):
"""Frame for viewing combined skills of a deck or individual cards""" """Frame for viewing combined skills of a deck or individual cards"""
def __init__(self, parent): def __init__(self, parent):
super().__init__(parent) super().__init__(parent, fg_color="transparent")
self.icon_cache = {} self.icon_cache = {}
self.current_mode = "Deck" # or "Single" self.current_mode = "Deck" # or "Single"
@@ -38,31 +40,35 @@ class DeckSkillsFrame(ttk.Frame):
def create_widgets(self): def create_widgets(self):
"""Create the deck skills interface""" """Create the deck skills interface"""
# Header / Controls # Header / Controls
ctrl_frame = tk.Frame(self, bg=BG_DARK) ctrl_frame = ctk.CTkFrame(self, fg_color="transparent")
ctrl_frame.pack(fill=tk.X, padx=20, pady=15) ctrl_frame.pack(fill=tk.X, padx=20, pady=15)
# Left side: Mode/Deck selection # Left side: Mode/Deck selection
selection_frame = tk.Frame(ctrl_frame, bg=BG_DARK) selection_frame = ctk.CTkFrame(ctrl_frame, fg_color="transparent")
selection_frame.pack(side=tk.LEFT) selection_frame.pack(side=tk.LEFT)
tk.Label(selection_frame, text="🎴 Select Deck:", font=FONT_BODY, ctk.CTkLabel(selection_frame, text="🎴 Select Deck:", font=FONT_BODY,
bg=BG_DARK, fg=TEXT_SECONDARY).pack(side=tk.LEFT) text_color=TEXT_SECONDARY).pack(side=tk.LEFT)
self.deck_combo = ttk.Combobox(selection_frame, width=30, state='readonly') self.deck_combo = ctk.CTkComboBox(selection_frame, width=200, state='readonly',
command=self.on_deck_selected_val)
self.deck_combo.pack(side=tk.LEFT, padx=10) self.deck_combo.pack(side=tk.LEFT, padx=10)
self.deck_combo.bind('<<ComboboxSelected>>', self.on_deck_selected)
# Mode indicator/Description # Mode indicator/Description
self.mode_label = tk.Label(ctrl_frame, text="Showing skills for selected deck", self.mode_label = ctk.CTkLabel(ctrl_frame, text="Showing skills for selected deck",
font=FONT_HEADER, bg=BG_DARK, fg=ACCENT_PRIMARY) font=FONT_HEADER, text_color=ACCENT_PRIMARY)
self.mode_label.pack(side=tk.RIGHT) self.mode_label.pack(side=tk.RIGHT)
# Main Results Tree # Main Results Tree
tree_container = create_card_frame(self) tree_container = ctk.CTkFrame(self, fg_color="transparent")
tree_container.pack(fill=tk.BOTH, expand=True, padx=20, pady=(0, 15)) tree_container.pack(fill=tk.BOTH, expand=True, padx=20, pady=(0, 15))
# Treeview inner frame
tree_inner = ctk.CTkFrame(tree_container, fg_color="transparent")
tree_inner.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
cols = ('skill', 'rarity', 'source', 'details') cols = ('skill', 'rarity', 'source', 'details')
self.tree = ttk.Treeview(tree_container, columns=cols, show='tree headings', self.tree = ttk.Treeview(tree_inner, columns=cols, show='tree headings',
style="Treeview") style="Treeview")
self.tree.heading('#0', text='★ Card / Skill') self.tree.heading('#0', text='★ Card / Skill')
@@ -77,37 +83,41 @@ class DeckSkillsFrame(ttk.Frame):
self.tree.column('source', width=100) self.tree.column('source', width=100)
self.tree.column('details', width=450) self.tree.column('details', width=450)
scrollbar = ttk.Scrollbar(tree_container, orient=tk.VERTICAL, command=self.tree.yview) scrollbar = ttk.Scrollbar(tree_inner, orient=tk.VERTICAL, command=self.tree.yview)
self.tree.configure(yscrollcommand=scrollbar.set) self.tree.configure(yscrollcommand=scrollbar.set)
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=2, pady=2) self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=2, pady=2)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=2) scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=2)
# Footer # Footer
self.stats_label = tk.Label(self, text="", font=FONT_SMALL, self.stats_label = ctk.CTkLabel(self, text="", font=FONT_SMALL,
bg=BG_DARK, fg=TEXT_MUTED) text_color=TEXT_MUTED)
self.stats_label.pack(anchor='e', pady=5, padx=20) self.stats_label.pack(anchor='e', pady=5, padx=20)
def refresh_decks(self): def refresh_decks(self):
"""Load decks into combobox""" """Load decks into combobox"""
decks = get_all_decks() decks = get_all_decks()
self.deck_combo['values'] = [f"{d[0]}: {d[1]}" for d in decks] values = [f"{d[0]}: {d[1]}" for d in decks]
if decks: self.deck_combo.configure(values=values)
self.deck_combo.current(0) if values:
self.on_deck_selected(None) self.deck_combo.set(values[0])
self.on_deck_selected_val(values[0])
def on_deck_selected(self, event): def on_deck_selected_val(self, value):
"""Handle deck selection""" """Handle deck selection from combobox values"""
selection = self.deck_combo.get() if not value: return
if not selection: return
deck_id = int(selection.split(':')[0]) deck_id = int(value.split(':')[0])
deck_name = selection.split(': ')[1] deck_name = value.split(': ')[1]
self.current_mode = "Deck" self.current_mode = "Deck"
self.mode_label.config(text=f"Deck: {deck_name}", fg=ACCENT_PRIMARY) self.mode_label.configure(text=f"Deck: {deck_name}", text_color=ACCENT_PRIMARY)
self.show_deck_skills(deck_id) self.show_deck_skills(deck_id)
def on_deck_selected(self, event):
"""Legacy bind handler if needed"""
self.on_deck_selected_val(self.deck_combo.get())
def show_deck_skills(self, deck_id): def show_deck_skills(self, deck_id):
"""Fetch and display all skills from a deck""" """Fetch and display all skills from a deck"""
# Clear tree # Clear tree
@@ -116,7 +126,7 @@ class DeckSkillsFrame(ttk.Frame):
deck_cards = get_deck_cards(deck_id) deck_cards = get_deck_cards(deck_id)
if not deck_cards: if not deck_cards:
self.stats_label.config(text="Deck is empty") self.stats_label.configure(text="Deck is empty")
return return
total_skills = 0 total_skills = 0
@@ -142,7 +152,7 @@ class DeckSkillsFrame(ttk.Frame):
self.add_skill_row(parent_id, event['skill_name'], "Event", event['details']) self.add_skill_row(parent_id, event['skill_name'], "Event", event['details'])
total_skills += 1 total_skills += 1
self.stats_label.config(text=f"Found {total_skills} total skill sources in deck") self.stats_label.configure(text=f"Found {total_skills} total skill sources in deck")
def add_card_node(self, card_id, owned_mark, name, rarity, card_type, image_path): def add_card_node(self, card_id, owned_mark, name, rarity, card_type, image_path):
"""Add a parent node for a card""" """Add a parent node for a card"""
@@ -152,7 +162,7 @@ class DeckSkillsFrame(ttk.Frame):
if resolved_path and os.path.exists(resolved_path): if resolved_path and os.path.exists(resolved_path):
try: try:
pil_img = Image.open(resolved_path) pil_img = Image.open(resolved_path)
pil_img.thumbnail((48, 48), Image.Resampling.LANCZOS) pil_img.thumbnail((32, 32), Image.Resampling.LANCZOS)
img = ImageTk.PhotoImage(pil_img) img = ImageTk.PhotoImage(pil_img)
self.icon_cache[card_id] = img self.icon_cache[card_id] = img
except: pass except: pass
@@ -185,7 +195,7 @@ class DeckSkillsFrame(ttk.Frame):
card_id, name, rarity, card_type, max_level, url, image_path, is_owned, owned_level = card card_id, name, rarity, card_type, max_level, url, image_path, is_owned, owned_level = card
self.current_mode = "Single" self.current_mode = "Single"
self.mode_label.config(text=f"Card: {name}", fg=ACCENT_SECONDARY) self.mode_label.configure(text=f"Card: {name}", text_color=ACCENT_SECONDARY)
# Clear tree # Clear tree
for item in self.tree.get_children(): for item in self.tree.get_children():
@@ -209,4 +219,4 @@ class DeckSkillsFrame(ttk.Frame):
self.add_skill_row(parent_id, event['skill_name'], "Event", event['details']) self.add_skill_row(parent_id, event['skill_name'], "Event", event['details'])
total_skills += 1 total_skills += 1
self.stats_label.config(text=f"Showing {total_skills} skill sources for {name}") self.stats_label.configure(text=f"Showing {total_skills} skill sources for {name}")

View File

@@ -1,7 +1,9 @@
import tkinter as tk import tkinter as tk
import customtkinter as ctk
from db.db_queries import get_deck_bonus from db.db_queries import get_deck_bonus
from gui.theme import BG_MEDIUM, TEXT_PRIMARY
class DeckView(tk.Toplevel): class DeckView(ctk.CTkToplevel):
def __init__(self, parent): def __init__(self, parent):
super().__init__(parent) super().__init__(parent)
self.title("Deck Builder") self.title("Deck Builder")
@@ -9,9 +11,9 @@ class DeckView(tk.Toplevel):
self.deck_id = 1 # Default deck self.deck_id = 1 # Default deck
tk.Button(self, text="Calculate Deck Bonuses", command=self.calculate).pack(pady=10) ctk.CTkButton(self, text="Calculate Deck Bonuses", command=self.calculate).pack(pady=10)
self.output = tk.Text(self, height=20) self.output = ctk.CTkTextbox(self, height=300, fg_color=BG_MEDIUM, text_color=TEXT_PRIMARY)
self.output.pack(fill=tk.BOTH, expand=True) self.output.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
def calculate(self): def calculate(self):
self.output.delete("1.0", tk.END) self.output.delete("1.0", tk.END)

View File

@@ -1,12 +1,15 @@
""" """
Effects Search View - Search for effects across all owned cards Effects Search View - Search for effects across all owned cards
Updated for CustomTkinter
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk, messagebox from tkinter import ttk, messagebox
import customtkinter as ctk
import sys import sys
import os import os
import re import re
from PIL import Image, ImageTk # Added missing import
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@@ -20,78 +23,93 @@ from gui.theme import (
) )
from utils import resolve_image_path from utils import resolve_image_path
class EffectsFrame(ttk.Frame): class EffectsFrame(ctk.CTkFrame):
"""Frame for searching effects across owned cards""" """Frame for searching effects across owned cards"""
def __init__(self, parent): def __init__(self, parent):
super().__init__(parent) super().__init__(parent, fg_color="transparent")
self.icon_cache = {}
self.create_widgets() self.create_widgets()
def create_widgets(self): def create_widgets(self):
"""Create the effects search interface""" """Create the effects search interface"""
# Header / Search Bar # Header / Search Bar
header_frame = tk.Frame(self, bg=BG_DARK) header_frame = ctk.CTkFrame(self, fg_color="transparent")
header_frame.pack(fill=tk.X, padx=20, pady=15) header_frame.pack(fill=tk.X, padx=20, pady=15)
# Search container # Search container
search_container = tk.Frame(header_frame, bg=BG_DARK) search_container = ctk.CTkFrame(header_frame, fg_color="transparent")
search_container.pack(fill=tk.X) search_container.pack(fill=tk.X)
tk.Label(search_container, text="🔍 Search Effect:", ctk.CTkLabel(search_container, text="🔍 Search Effect:",
font=FONT_HEADER, bg=BG_DARK, fg=TEXT_PRIMARY).pack(side=tk.LEFT, padx=(0, 10)) font=FONT_HEADER, text_color=TEXT_PRIMARY).pack(side=tk.LEFT, padx=(0, 10))
self.search_var = tk.StringVar() self.search_var = tk.StringVar()
self.search_entry = create_styled_entry(search_container, textvariable=self.search_var) self.search_entry = ctk.CTkEntry(search_container, textvariable=self.search_var, width=300)
self.search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10)) self.search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
self.search_entry.bind('<Return>', lambda e: self.perform_search()) self.search_entry.bind('<Return>', lambda e: self.perform_search())
search_btn = create_styled_button(search_container, text="Search", search_btn = create_styled_button(search_container, text="Search",
command=self.perform_search, style_type='primary') command=self.perform_search, style_type='accent')
search_btn.pack(side=tk.LEFT) search_btn.pack(side=tk.LEFT)
# Example/Help text # Example/Help text
help_frame = tk.Frame(header_frame, bg=BG_DARK) help_frame = ctk.CTkFrame(header_frame, fg_color="transparent")
help_frame.pack(fill=tk.X, pady=(5, 0)) help_frame.pack(fill=tk.X, pady=(5, 0))
tk.Label(help_frame, text="Examples: Friendship, Motivation, Race Bonus, Skill Pt", ctk.CTkLabel(help_frame, text="Examples: Friendship, Motivation, Race Bonus, Skill Pt",
font=FONT_SMALL, bg=BG_DARK, fg=TEXT_MUTED).pack(side=tk.LEFT) font=FONT_SMALL, text_color=TEXT_MUTED).pack(side=tk.LEFT)
# Results Area # Results Area
results_frame = ttk.LabelFrame(self, text=" Search Results (Owned Cards) ", padding=10) results_container = ctk.CTkFrame(self, fg_color="transparent")
results_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=(0, 20)) results_container.pack(fill=tk.BOTH, expand=True, padx=20, pady=(0, 20))
# Treeview # Label for the frame
columns = ('card', 'level', 'current_value', 'effect_name') ctk.CTkLabel(results_container, text="Search Results (Owned Cards)",
self.tree = ttk.Treeview(results_frame, columns=columns, show='headings', selectmode='browse') font=FONT_SUBHEADER, text_color=ACCENT_PRIMARY).pack(pady=(10, 5))
self.tree.heading('card', text='Card Name', anchor='w') # Treeview Container
tree_frame = ctk.CTkFrame(results_container, fg_color="transparent")
tree_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
# Treeview - ADDING IMAGE COLUMN
# Note: Treeview column #0 is the tree column where icons live.
# We will put the image in #0 and text in #0 if possible, or name in #1
# Treeview - ADDING IMAGE COLUMN
# Use #0 for Icon only, like Card View
columns = ('card_name', 'level', 'current_value', 'effect_name')
self.tree = ttk.Treeview(tree_frame, columns=columns, show='tree headings', selectmode='browse', style="CardList.Treeview")
self.tree.heading('#0', text='Image')
self.tree.column('#0', width=100, anchor='center')
self.tree.heading('card_name', text='Card Name', anchor='w')
self.tree.heading('level', text='Level', anchor='center') self.tree.heading('level', text='Level', anchor='center')
self.tree.heading('current_value', text='Value', anchor='center') self.tree.heading('current_value', text='Value', anchor='center')
self.tree.heading('effect_name', text='Effect Name', anchor='w') self.tree.heading('effect_name', text='Effect Name', anchor='w')
self.tree.column('card', width=250) self.tree.column('card_name', width=200)
self.tree.column('level', width=60, anchor='center') self.tree.column('level', width=60, anchor='center')
self.tree.column('current_value', width=80, anchor='center') self.tree.column('current_value', width=80, anchor='center')
self.tree.column('effect_name', width=150) self.tree.column('effect_name', width=200)
scrollbar = ttk.Scrollbar(results_frame, orient=tk.VERTICAL, command=self.tree.yview) scrollbar = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=self.tree.yview)
# ... (rest of scrollbar setup) ...
self.tree.configure(yscrollcommand=scrollbar.set) self.tree.configure(yscrollcommand=scrollbar.set)
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y) scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# Status Label # Status Label
self.status_label = tk.Label(results_frame, text="", bg=BG_MEDIUM, fg=TEXT_SECONDARY, font=FONT_SMALL) self.status_label = ctk.CTkLabel(results_container, text="", font=FONT_SMALL, text_color=TEXT_SECONDARY)
self.status_label.pack(side=tk.BOTTOM, fill=tk.X, pady=(5, 0)) self.status_label.pack(side=tk.BOTTOM, fill=tk.X, pady=(5, 10))
def parse_value(self, value_str): def parse_value(self, value_str):
"""Parse effect value string to float for sorting""" """Parse effect value string to float for sorting"""
try: try:
# Extract number from string (e.g. "20%" -> 20, "+15" -> 15)
# Remove non-numeric characters except . and -
clean = re.sub(r'[^\d.-]', '', str(value_str)) clean = re.sub(r'[^\d.-]', '', str(value_str))
return float(clean) return float(clean)
except: except:
return -999999.0 # Sort to bottom if invalid return -999999.0
def perform_search(self): def perform_search(self):
"""Execute search and update results""" """Execute search and update results"""
@@ -108,7 +126,7 @@ class EffectsFrame(ttk.Frame):
results = search_owned_effects(term) results = search_owned_effects(term)
if not results: if not results:
self.status_label.config(text="No matching effects found among owned cards.") self.status_label.configure(text="No matching effects found among owned cards.")
return return
# Process and Sort # Process and Sort
@@ -127,13 +145,36 @@ class EffectsFrame(ttk.Frame):
# Populate Tree # Populate Tree
for item in processed_results: for item in processed_results:
r = item['data'] r = item['data']
#Columns: card, level, current_value, effect_name
card_id = r[0]
image_path = r[2]
# Load Image
img = self.icon_cache.get(card_id)
if not img:
resolved_path = resolve_image_path(image_path)
if resolved_path and os.path.exists(resolved_path):
try:
pil_img = Image.open(resolved_path)
# Match CardList size
pil_img.thumbnail((78, 78), Image.Resampling.LANCZOS)
img = ImageTk.PhotoImage(pil_img)
self.icon_cache[card_id] = img
except:
pass
kv = {'image': img} if img else {}
# Insert into tree
# #0 = Image (Text '')
# Cols = Name, Level, Value, Effect
values = (r[1], f"Lv {r[5]}", r[4], r[3]) values = (r[1], f"Lv {r[5]}", r[4], r[3])
self.tree.insert('', tk.END, values=values)
self.status_label.config(text=f"Found {len(processed_results)} owned cards with matching effects.") self.tree.insert('', tk.END, text='', values=values, **kv)
# Compatibility methods for main_window integration (empty as we don't need them anymore) self.status_label.configure(text=f"Found {len(processed_results)} owned cards with matching effects.")
# Compatibility methods for main_window integration
def set_card(self, card_id): def set_card(self, card_id):
pass pass

View File

@@ -1,9 +1,11 @@
""" """
Skill Search View - Find cards by the skills they teach Skill Search View - Find cards by the skills they teach
Updated for CustomTkinter
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
import customtkinter as ctk
import sys import sys
import os import os
from PIL import Image, ImageTk from PIL import Image, ImageTk
@@ -17,15 +19,15 @@ from gui.theme import (
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_TERTIARY, ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_TERTIARY,
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED, TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL, FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL,
create_card_frame, get_type_icon, create_styled_button create_card_frame, get_type_icon, create_styled_button, create_styled_entry
) )
class SkillSearchFrame(ttk.Frame): class SkillSearchFrame(ctk.CTkFrame):
"""Frame for searching skills and finding cards that have them""" """Frame for searching skills and finding cards that have them"""
def __init__(self, parent): def __init__(self, parent):
super().__init__(parent) super().__init__(parent, fg_color="transparent")
self.all_skills = [] self.all_skills = []
self.icon_cache = {} self.icon_cache = {}
self.current_skill = None self.current_skill = None
@@ -36,30 +38,32 @@ class SkillSearchFrame(ttk.Frame):
def create_widgets(self): def create_widgets(self):
"""Create the skill search interface""" """Create the skill search interface"""
# Main split container # Main split container
main_pane = ttk.PanedWindow(self, orient=tk.HORIZONTAL) # Use two frames instead of PanedWindow
main_pane.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# === Left Panel: Skill List === # === Left Panel: Skill List ===
left_frame = tk.Frame(main_pane, bg=BG_DARK, width=300) left_frame = ctk.CTkFrame(self, width=390, corner_radius=10)
main_pane.add(left_frame, weight=1) left_frame.pack_propagate(False) # Force width to stay 600
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=False, padx=(0, 10))
# Search Header # Search Header
header = tk.Frame(left_frame, bg=BG_DARK) header = ctk.CTkFrame(left_frame, fg_color="transparent")
header.pack(fill=tk.X, pady=(0, 10)) header.pack(fill=tk.X, pady=(15, 10), padx=10)
tk.Label(header, text="🔍 Search Skills", font=FONT_HEADER, ctk.CTkLabel(header, text="🔍 Search Skills", font=FONT_HEADER,
bg=BG_DARK, fg=TEXT_PRIMARY).pack(side=tk.LEFT) text_color=TEXT_PRIMARY).pack(side=tk.LEFT)
# Search Entry # Search Entry
self.search_var = tk.StringVar() self.search_var = tk.StringVar()
self.search_var.trace('w', self.filter_skills) self.search_var.trace('w', self.filter_skills)
search_entry = ttk.Entry(left_frame, textvariable=self.search_var) # Use styled entry
search_entry.pack(fill=tk.X, padx=(0, 5), pady=(0, 10)) search_entry = ctk.CTkEntry(left_frame, textvariable=self.search_var, placeholder_text="Type to filter...")
search_entry.pack(fill=tk.X, padx=10, pady=(0, 10))
# Skill Listbox # Skill Listbox Container (Styled)
list_container = create_card_frame(left_frame) list_container = ctk.CTkFrame(left_frame, fg_color="transparent")
list_container.pack(fill=tk.BOTH, expand=True) list_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
# Using tk.Listbox because CTk doesn't have one and ScrollableFrame is harder to manage for simple selection list
scrollbar = ttk.Scrollbar(list_container, orient=tk.VERTICAL) scrollbar = ttk.Scrollbar(list_container, orient=tk.VERTICAL)
self.skill_listbox = tk.Listbox(list_container, self.skill_listbox = tk.Listbox(list_container,
bg=BG_MEDIUM, fg=TEXT_SECONDARY, bg=BG_MEDIUM, fg=TEXT_SECONDARY,
@@ -77,27 +81,28 @@ class SkillSearchFrame(ttk.Frame):
self.skill_listbox.bind('<<ListboxSelect>>', self.on_skill_selected) self.skill_listbox.bind('<<ListboxSelect>>', self.on_skill_selected)
# === Right Panel: Results === # === Right Panel: Results ===
right_frame = tk.Frame(main_pane, bg=BG_DARK) right_frame = ctk.CTkFrame(self, corner_radius=10, fg_color="transparent")
main_pane.add(right_frame, weight=3) right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
# Search Row (Search + Filter) # Search Row (Search + Filter)
search_frame = tk.Frame(right_frame, bg=BG_DARK) search_frame = ctk.CTkFrame(right_frame, fg_color="transparent")
search_frame.pack(fill=tk.X, padx=10, pady=10) search_frame.pack(fill=tk.X, padx=10, pady=15)
self.result_header = tk.Label(search_frame, text="Select a skill to see cards", self.result_header = ctk.CTkLabel(search_frame, text="Select a skill to see cards",
font=FONT_SUBHEADER, bg=BG_DARK, fg=ACCENT_PRIMARY) font=FONT_SUBHEADER, text_color=ACCENT_PRIMARY)
self.result_header.pack(side=tk.LEFT) self.result_header.pack(side=tk.LEFT)
# Owned Filter # Owned Filter
self.owned_only_var = tk.BooleanVar(value=False) self.owned_only_var = tk.BooleanVar(value=False)
self.owned_check = ttk.Checkbutton(search_frame, text="Show Owned Only", self.owned_check = ctk.CTkCheckBox(search_frame, text="Show Owned Only",
variable=self.owned_only_var, variable=self.owned_only_var,
command=self.on_filter_changed) command=self.on_filter_changed,
font=FONT_SMALL)
self.owned_check.pack(side=tk.RIGHT, padx=10) self.owned_check.pack(side=tk.RIGHT, padx=10)
# Results Treeview # Results Treeview Container
tree_frame = create_card_frame(right_frame) tree_frame = ctk.CTkFrame(right_frame, fg_color="transparent")
tree_frame.pack(fill=tk.BOTH, expand=True, padx=10) tree_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
cols = ('owned', 'name', 'rarity', 'type', 'source', 'details') cols = ('owned', 'name', 'rarity', 'type', 'source', 'details')
self.tree = ttk.Treeview(tree_frame, columns=cols, show='tree headings', self.tree = ttk.Treeview(tree_frame, columns=cols, show='tree headings',
@@ -122,12 +127,12 @@ class SkillSearchFrame(ttk.Frame):
vsb = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=self.tree.yview) vsb = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=self.tree.yview)
self.tree.configure(yscrollcommand=vsb.set) self.tree.configure(yscrollcommand=vsb.set)
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=2, pady=2) self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
vsb.pack(side=tk.RIGHT, fill=tk.Y, pady=2) vsb.pack(side=tk.RIGHT, fill=tk.Y, pady=5)
# Stats footer # Stats footer
self.stats_label = tk.Label(right_frame, text="", font=FONT_SMALL, self.stats_label = ctk.CTkLabel(right_frame, text="", font=FONT_SMALL,
bg=BG_DARK, fg=TEXT_MUTED) text_color=TEXT_MUTED)
self.stats_label.pack(anchor='e', pady=5, padx=10) self.stats_label.pack(anchor='e', pady=5, padx=10)
def load_skills(self): def load_skills(self):
@@ -194,7 +199,7 @@ class SkillSearchFrame(ttk.Frame):
def show_cards_for_skill(self, skill_name): def show_cards_for_skill(self, skill_name):
"""Fetch and display cards with the selected skill""" """Fetch and display cards with the selected skill"""
self.current_skill = skill_name self.current_skill = skill_name
self.result_header.config(text=f"Cards with skill: {skill_name}") self.result_header.configure(text=f"Cards with skill: {skill_name}")
# Clear tree # Clear tree
for item in self.tree.get_children(): for item in self.tree.get_children():
@@ -204,10 +209,12 @@ class SkillSearchFrame(ttk.Frame):
owned_only = self.owned_only_var.get() owned_only = self.owned_only_var.get()
display_count = 0
for card in cards: for card in cards:
if owned_only and not card.get('is_owned'): if owned_only and not card.get('is_owned'):
continue continue
display_count += 1
# Load Icon # Load Icon
card_id = card['card_id'] card_id = card['card_id']
img = self.icon_cache.get(card_id) img = self.icon_cache.get(card_id)
@@ -216,7 +223,7 @@ class SkillSearchFrame(ttk.Frame):
if resolved_path and os.path.exists(resolved_path): if resolved_path and os.path.exists(resolved_path):
try: try:
pil_img = Image.open(resolved_path) pil_img = Image.open(resolved_path)
pil_img.thumbnail((48, 48), Image.Resampling.LANCZOS) pil_img.thumbnail((32, 32), Image.Resampling.LANCZOS)
img = ImageTk.PhotoImage(pil_img) img = ImageTk.PhotoImage(pil_img)
self.icon_cache[card_id] = img self.icon_cache[card_id] = img
except: except:
@@ -246,12 +253,10 @@ class SkillSearchFrame(ttk.Frame):
card_details card_details
) )
if img: kv = {'image': img} if img else {}
self.tree.insert('', tk.END, text='', image=img, values=values) self.tree.insert('', tk.END, text='', values=values, **kv)
else:
self.tree.insert('', tk.END, text='?', values=values)
self.stats_label.config(text=f"Found {len(cards)} cards") self.stats_label.configure(text=f"Found {display_count} cards")
def set_card(self, card_id): def set_card(self, card_id):
"""No longer responsive to card selection in this tab""" """No longer responsive to card selection in this tab"""

View File

@@ -4,7 +4,7 @@ Tabbed interface for card browsing, effects, deck builder, and hints
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk import customtkinter as ctk
import sys import sys
import os import os
@@ -32,27 +32,29 @@ class MainWindow:
"""Main application window with tabbed interface""" """Main application window with tabbed interface"""
def __init__(self): def __init__(self):
self.root = tk.Tk() # Initialize CTk root
self.root = ctk.CTk()
self.root.title("Umamusume Support Card Manager") self.root.title("Umamusume Support Card Manager")
self.root.geometry("1400x850") self.root.geometry("1400x850")
self.root.minsize(1350, 800) self.root.minsize(1350, 800)
# Set icon # Set icon
try: try:
icon_path = resolve_image_path("1_Special Week.png") icon_path = resolve_image_path("1_Special Week.png")
if icon_path and os.path.exists(icon_path): if icon_path and os.path.exists(icon_path):
# ctk uses iconbitmap for windows usually, but iconphoto works too
icon_img = tk.PhotoImage(file=icon_path) icon_img = tk.PhotoImage(file=icon_path)
self.root.iconphoto(True, icon_img) self.root.iconphoto(True, icon_img)
except Exception as e: except Exception as e:
print(f"Failed to set icon: {e}") print(f"Failed to set icon: {e}")
# Configure all styles using centralized theme # Configure styles for legacy widgets
configure_styles(self.root) configure_styles(self.root)
# Create main container # Create main container
main_container = ttk.Frame(self.root) # Note: CTk already has a main frame in a way, but we'll use a container for padding
main_container.pack(fill=tk.BOTH, expand=True) main_container = ctk.CTkFrame(self.root, fg_color="transparent")
main_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# State # State
self.last_selected_levels = {} # card_id -> level self.last_selected_levels = {} # card_id -> level
@@ -60,56 +62,53 @@ class MainWindow:
# Header with stats # Header with stats
self.create_header(main_container) self.create_header(main_container)
# Status bar - Create BEFORE notebook to anchor it to bottom # Status bar
self.create_status_bar(main_container) self.create_status_bar(main_container)
# Tabbed notebook # Tabbed notebook -> CTkTabview
self.notebook = ttk.Notebook(main_container) self.tabview = ctk.CTkTabview(main_container)
self.notebook.pack(fill=tk.BOTH, expand=True, padx=15, pady=8) self.tabview.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# Create tabs # Create tabs
self.create_tabs() self.create_tabs()
def create_header(self, parent): def create_header(self, parent):
"""Create header with database statistics and update button""" """Create header with database statistics and update button"""
# Header container with subtle bottom border effect # Header container
header_outer = tk.Frame(parent, bg=BG_DARK) header_frame = ctk.CTkFrame(parent, fg_color="transparent")
header_outer.pack(fill=tk.X) header_frame.pack(fill=tk.X, padx=10, pady=(0, 10))
header_frame = tk.Frame(header_outer, bg=BG_DARK)
header_frame.pack(fill=tk.X, padx=20, pady=15)
# Left side: Title and version # Left side: Title and version
title_frame = tk.Frame(header_frame, bg=BG_DARK) title_frame = ctk.CTkFrame(header_frame, fg_color="transparent")
title_frame.pack(side=tk.LEFT) title_frame.pack(side=tk.LEFT)
# App icon and title # App icon and title
title_label = tk.Label( title_label = ctk.CTkLabel(
title_frame, title_frame,
text="🏇 Umamusume Support Card Manager", text="🏇 Umamusume Support Card Manager",
font=FONT_TITLE, font=FONT_TITLE,
bg=BG_DARK, text_color=ACCENT_PRIMARY
fg=ACCENT_PRIMARY
) )
title_label.pack(side=tk.LEFT) title_label.pack(side=tk.LEFT, padx=(0, 10))
# Version badge # Version badge
version_frame = tk.Frame(title_frame, bg=ACCENT_SECONDARY, padx=8, pady=2) version_label = ctk.CTkLabel(
version_frame.pack(side=tk.LEFT, padx=12) title_frame,
version_label = tk.Label(
version_frame,
text=f"v{VERSION}", text=f"v{VERSION}",
font=FONT_SMALL, font=FONT_SMALL,
bg=ACCENT_SECONDARY, fg_color=ACCENT_SECONDARY,
fg=TEXT_PRIMARY text_color=TEXT_PRIMARY,
corner_radius=6,
height=24,
width=60
) )
version_label.pack() version_label.pack(side=tk.LEFT)
# Right side: Update button and stats # Right side: Update button and stats
right_frame = tk.Frame(header_frame, bg=BG_DARK) right_frame = ctk.CTkFrame(header_frame, fg_color="transparent")
right_frame.pack(side=tk.RIGHT) right_frame.pack(side=tk.RIGHT)
# Update button with modern styling # Update button
self.update_button = create_styled_button( self.update_button = create_styled_button(
right_frame, right_frame,
text="🔄 Check for Updates", text="🔄 Check for Updates",
@@ -118,122 +117,96 @@ class MainWindow:
) )
self.update_button.pack(side=tk.RIGHT, padx=(15, 0)) self.update_button.pack(side=tk.RIGHT, padx=(15, 0))
# Stats panel with card-like appearance # Stats panel
stats_frame = tk.Frame(right_frame, bg=BG_MEDIUM, padx=15, pady=8) self.stats_label = ctk.CTkLabel(
stats_frame.pack(side=tk.RIGHT) right_frame,
text="Loading stats...",
stats = get_database_stats()
owned = get_owned_count()
# Build stats text with better formatting
stats_parts = [
f"📊 {stats.get('total_cards', 0)} Cards",
f"{owned} Owned",
f"🏆 {stats.get('by_rarity', {}).get('SSR', 0)} SSR",
f"{stats.get('by_rarity', {}).get('SR', 0)} SR",
f"{stats.get('by_rarity', {}).get('R', 0)} R"
]
stats_text = "".join(stats_parts)
self.stats_label = tk.Label(
stats_frame,
text=stats_text,
font=FONT_SMALL, font=FONT_SMALL,
bg=BG_MEDIUM, fg_color=BG_MEDIUM,
fg=TEXT_SECONDARY text_color=TEXT_SECONDARY,
corner_radius=8,
padx=15,
pady=5
) )
self.stats_label.pack() self.stats_label.pack(side=tk.RIGHT)
# Subtle separator line # Initial stats load
separator = tk.Frame(header_outer, bg=BG_LIGHT, height=1) self.refresh_stats()
separator.pack(fill=tk.X, padx=15)
def create_tabs(self): def create_tabs(self):
"""Create all tab frames""" """Create all tab frames"""
# Add tabs
tab_cards = self.tabview.add(" 📋 Card List ")
tab_effects = self.tabview.add(" 📊 Search Effects ")
tab_deck = self.tabview.add(" 🎴 Deck Builder ")
tab_search = self.tabview.add(" 🔍 Skill Search ")
tab_skills = self.tabview.add(" 📜 Deck Skills ")
# Card List Tab # Card List Tab
self.card_frame = CardListFrame(self.notebook, # Note: CardListFrame and others inherit from ttk.Frame/tk.Frame.
# We need to make sure they can be packed into a CTkFrame (the tab).
self.card_frame = CardListFrame(tab_cards,
on_card_selected_callback=self.on_card_selected, on_card_selected_callback=self.on_card_selected,
on_stats_updated_callback=self.refresh_stats) on_stats_updated_callback=self.refresh_stats)
self.notebook.add(self.card_frame, text=" 📋 Card List ") self.card_frame.pack(fill=tk.BOTH, expand=True)
# Effects Tab # Effects Tab
self.effects_frame = EffectsFrame(self.notebook) self.effects_frame = EffectsFrame(tab_effects)
self.notebook.add(self.effects_frame, text=" 📊 Effects ") self.effects_frame.pack(fill=tk.BOTH, expand=True)
# Deck Builder Tab # Deck Builder Tab
self.deck_frame = DeckBuilderFrame(self.notebook) self.deck_frame = DeckBuilderFrame(tab_deck)
self.notebook.add(self.deck_frame, text=" 🎴 Deck Builder ") self.deck_frame.pack(fill=tk.BOTH, expand=True)
# Skill Search Tab # Skill Search Tab
self.hints_frame = SkillSearchFrame(self.notebook) self.hints_frame = SkillSearchFrame(tab_search)
self.notebook.add(self.hints_frame, text=" 🔍 Skill Search ") self.hints_frame.pack(fill=tk.BOTH, expand=True)
# Deck Skills Tab # Deck Skills Tab
self.deck_skills_frame = DeckSkillsFrame(self.notebook) self.deck_skills_frame = DeckSkillsFrame(tab_skills)
self.notebook.add(self.deck_skills_frame, text=" 📜 Deck Skills ") self.deck_skills_frame.pack(fill=tk.BOTH, expand=True)
def create_status_bar(self, parent): def create_status_bar(self, parent):
"""Create status bar at bottom""" """Create status bar at bottom"""
status_outer = tk.Frame(parent, bg=BG_MEDIUM) # Using pack side=BOTTOM relative to the main container
status_outer.pack(fill=tk.X, side=tk.BOTTOM) status_frame = ctk.CTkFrame(parent, height=30, fg_color="transparent")
status_frame.pack(fill=tk.X, side=tk.BOTTOM, pady=(10, 0))
status_frame = tk.Frame(status_outer, bg=BG_MEDIUM) self.status_label = ctk.CTkLabel(
status_frame.pack(fill=tk.X, padx=15, pady=8)
self.status_label = tk.Label(
status_frame, status_frame,
text="✓ Ready", text="✓ Ready",
font=FONT_SMALL, font=FONT_SMALL,
bg=BG_MEDIUM, text_color=TEXT_MUTED
fg=TEXT_MUTED
) )
self.status_label.pack(side=tk.LEFT) self.status_label.pack(side=tk.LEFT, padx=10)
tk.Label( ctk.CTkLabel(
status_frame, status_frame,
text="Data from gametora.com", text="Data from gametora.com",
font=FONT_SMALL, font=FONT_SMALL,
bg=BG_MEDIUM, text_color=TEXT_MUTED
fg=TEXT_MUTED
).pack(side=tk.RIGHT) ).pack(side=tk.RIGHT)
# Diagnostics Button ctk.CTkLabel(
diag_btn = tk.Button(
status_frame,
text="🔍 Diagnostics",
font=FONT_SMALL,
bg=BG_MEDIUM,
fg=ACCENT_TERTIARY,
bd=0,
activebackground=BG_LIGHT,
activeforeground=TEXT_PRIMARY,
cursor='hand2',
command=self.show_diagnostics
)
diag_btn.pack(side=tk.RIGHT, padx=15)
tk.Label(
status_frame, status_frame,
text="VibeCoded by Kiyreload │ ", text="VibeCoded by Kiyreload │ ",
font=FONT_SMALL, font=FONT_SMALL,
bg=BG_MEDIUM, text_color=ACCENT_TERTIARY
fg=ACCENT_TERTIARY
).pack(side=tk.RIGHT) ).pack(side=tk.RIGHT)
def on_card_selected(self, card_id, card_name, level=None): def on_card_selected(self, card_id, card_name, level=None):
"""Handle card selection from card list""" """Handle card selection from card list"""
# Store level if provided
if level is not None: if level is not None:
self.last_selected_levels[card_id] = level self.last_selected_levels[card_id] = level
self.selected_card_id = card_id # Update selected_card_id self.selected_card_id = card_id
# Update other tabs with selected card # Update other tabs
if hasattr(self, 'effects_frame'): if hasattr(self, 'effects_frame'):
self.effects_frame.set_card(card_id) self.effects_frame.set_card(card_id)
if hasattr(self, 'deck_skills_frame'): if hasattr(self, 'deck_skills_frame'):
self.deck_skills_frame.set_card(card_id) self.deck_skills_frame.set_card(card_id)
self.status_label.config(text=f"📌 Selected: {card_name}") self.status_label.configure(text=f"📌 Selected: {card_name}")
def refresh_stats(self): def refresh_stats(self):
"""Refresh the statistics display""" """Refresh the statistics display"""
@@ -249,70 +222,13 @@ class MainWindow:
] ]
stats_text = "".join(stats_parts) stats_text = "".join(stats_parts)
self.stats_label.config(text=stats_text) if hasattr(self, 'stats_label'):
self.stats_label.configure(text=stats_text)
def show_update_dialog(self): def show_update_dialog(self):
"""Show the update dialog""" """Show the update dialog"""
show_update_dialog(self.root) show_update_dialog(self.root)
def show_diagnostics(self):
"""Show diagnostics information for debugging"""
diag_win = tk.Toplevel(self.root)
diag_win.title("System Diagnostics")
diag_win.geometry("700x500")
diag_win.configure(bg=BG_DARK)
from db.db_queries import DB_PATH
import platform
# Info text
info = [
f"--- Application Info ---",
f"Version: {VERSION}",
f"Frozen (EXE): {getattr(sys, 'frozen', False)}",
f"Python: {sys.version}",
f"Platform: {platform.platform()}",
"",
f"--- Database ---",
f"DB Path: {DB_PATH}",
f"DB Exists: {os.path.exists(DB_PATH)}",
"",
f"--- Search Paths ---",
f"Executable: {sys.executable}",
f"Script Root: {os.path.dirname(os.path.abspath(__file__))}",
f"MEIPASS (Temp): {getattr(sys, '_MEIPASS', 'N/A')}",
"",
f"--- Image Check ---"
]
# Check some images
img_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'images')
info.append(f"Images Dir (Source): {img_dir}")
info.append(f"Exists: {os.path.exists(img_dir)}")
if os.path.exists(img_dir):
files = os.listdir(img_dir)
info.append(f"Files found: {len(files)}")
if len(files) > 0:
info.append(f"Sample: {files[0]}")
content = "\n".join(info)
# Display area
text_frame = tk.Frame(diag_win, bg=BG_DARK, padx=20, pady=20)
text_frame.pack(fill=tk.BOTH, expand=True)
from gui.theme import create_styled_text
text_area = create_styled_text(text_frame)
text_area.pack(fill=tk.BOTH, expand=True)
text_area.insert(tk.END, content)
text_area.config(state=tk.DISABLED)
# Close button
btn_frame = tk.Frame(diag_win, bg=BG_DARK, pady=15)
btn_frame.pack(fill=tk.X)
create_styled_button(btn_frame, text="Close", command=diag_win.destroy).pack()
def run(self): def run(self):
""" """
Start the GUI application and display the main window. Start the GUI application and display the main window.

View File

@@ -1,21 +1,333 @@
""" """
Centralized Theme Module for Umamusume Support Card Manager Centralized Theme Module for Umamusume Support Card Manager
Modern glassmorphism-inspired dark theme with consistent styling Modern glassmorphism-inspired dark theme with consistent styling using CustomTkinter
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
import customtkinter as ctk
# ═══════════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════════
# COLOR PALETTE # CONFIGURATION
# ═══════════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════════
# Primary backgrounds (rich purplish-blues with depth) # Set default theme
BG_DARKEST = '#0d0d1a' # Deepest background ctk.set_appearance_mode("Dark")
BG_DARK = '#151528' # Main application background ctk.set_default_color_theme("blue") # We can use 'dark-blue' or 'green' too
BG_MEDIUM = '#1e1e3f' # Card/panel backgrounds
BG_LIGHT = '#2a2a5a' # Elevated elements, hover states # ═══════════════════════════════════════════════════════════════════════════════
BG_HIGHLIGHT = '#3d3d7a' # Active/selected backgrounds # COLOR PALETTE (Expanded for CTk)
# ═══════════════════════════════════════════════════════════════════════════════
# Primary backgrounds
BG_DARKEST = '#0B0B15' # Deepest background
BG_DARK = '#1a1a1a' # Main application background (CTk default is ~#2b2b2b, we can match or override)
BG_MEDIUM = '#2b2b2b' # Card/panel backgrounds
BG_LIGHT = '#3A3A4F' # Elevated elements
BG_HIGHLIGHT = '#1F6AA5' # Active/selected (matches 'blue' theme)
# Accents
ACCENT_PRIMARY = '#1F6AA5' # CTk Blue
ACCENT_SECONDARY = '#7c5cff' # Purple accent
ACCENT_TERTIARY = '#5ce1e6' # Cyan accent
ACCENT_SUCCESS = '#2CC985' # Green for success
ACCENT_WARNING = '#E9B949' # Amber for warning
ACCENT_ERROR = '#C33737' # Red for errors
# Text colors
TEXT_PRIMARY = '#DCE4EE' # CTk default text color
TEXT_SECONDARY = '#949A9F' # Secondary text
TEXT_MUTED = '#6B7075' # Muted text
# Rarity colors
RARITY_COLORS = {
'SSR': '#FFD700',
'SR': '#C0C0C0',
'R': '#CD853F'
}
# Type colors
TYPE_COLORS = {
'Speed': '#3b82f6',
'Stamina': '#f97316',
'Power': '#eab308',
'Guts': '#ef4444',
'Wisdom': '#22c55e',
'Friend': '#a855f7',
'Group': '#f59e0b'
}
TYPE_ICONS = {
'Speed': '🏃',
'Stamina': '💚',
'Power': '💪',
'Guts': '🔥',
'Wisdom': '🧠',
'Friend': '💜',
'Group': '👥'
}
# ═══════════════════════════════════════════════════════════════════════════════
# FONTS
# ═══════════════════════════════════════════════════════════════════════════════
FONT_FAMILY = 'Roboto Medium' # CTk Default
FONT_FAMILY_MONO = 'Consolas'
FONT_TITLE = (FONT_FAMILY, 24, 'bold')
FONT_HEADER = (FONT_FAMILY, 20, 'bold')
FONT_SUBHEADER = (FONT_FAMILY, 16, 'bold')
FONT_BODY = (FONT_FAMILY, 14)
FONT_BODY_BOLD = (FONT_FAMILY, 14, 'bold')
FONT_SMALL = (FONT_FAMILY, 12)
FONT_TINY = (FONT_FAMILY, 10)
FONT_MONO = (FONT_FAMILY_MONO, 13)
# ═══════════════════════════════════════════════════════════════════════════════
# STYLE CONFIGURATION
# ═══════════════════════════════════════════════════════════════════════════════
def configure_styles(root: tk.Tk):
"""
Configure ttk styles for legacy widgets (like Treeview)
Note: Standard TK configurations don't affect CTk widgets
"""
style = ttk.Style()
style.theme_use('clam')
# Configure Treeview to look dark
style.configure('Treeview',
background=BG_MEDIUM,
foreground=TEXT_PRIMARY,
fieldbackground=BG_MEDIUM,
font=FONT_BODY,
rowheight=36,
borderwidth=0)
style.configure('Treeview.Heading',
font=FONT_BODY_BOLD,
background='#333333',
foreground=TEXT_PRIMARY,
padding=8,
borderwidth=0)
style.map('Treeview',
background=[('selected', ACCENT_PRIMARY)],
foreground=[('selected', 'white')])
style.map('Treeview.Heading',
background=[('active', '#404040')])
# Card List specific
style.configure('CardList.Treeview',
background=BG_MEDIUM,
foreground=TEXT_PRIMARY,
rowheight=60)
# Scrollbar (for Treeview only)
style.configure('Vertical.TScrollbar',
background=BG_LIGHT,
troughcolor=BG_DARK,
borderwidth=0,
arrowsize=12)
# ═══════════════════════════════════════════════════════════════════════════════
# WIDGET FACTORIES (ADAPTERS)
# ═══════════════════════════════════════════════════════════════════════════════
def create_styled_button(parent, text, command=None, style_type='default', **kwargs):
"""Create a styled CTkButton"""
# Map old style types to colors
fg_color = None # Default
hover_color = None
if style_type == 'accent':
pass # Uses default blue
elif style_type == 'secondary':
fg_color = ACCENT_SECONDARY
hover_color = '#6040e0'
elif style_type == 'danger':
fg_color = ACCENT_ERROR
hover_color = '#a02020'
elif style_type == 'default':
fg_color = 'transparent'
border_width = 1
kwargs['border_width'] = 1
kwargs['border_color'] = TEXT_SECONDARY
# Filter out incompatible kwargs from tk
safe_kwargs = {k: v for k, v in kwargs.items() if k not in ['padx', 'pady', 'bg', 'fg', 'bd', 'relief', 'activebackground', 'activeforeground']}
btn = ctk.CTkButton(
parent,
text=text,
command=command,
font=FONT_BODY_BOLD if style_type == 'accent' else FONT_BODY,
height=36,
corner_radius=8,
**safe_kwargs
)
if fg_color:
btn.configure(fg_color=fg_color)
if hover_color:
btn.configure(hover_color=hover_color)
return btn
def create_styled_entry(parent, textvariable=None, **kwargs):
"""Create a styled CTkEntry"""
safe_kwargs = {k: v for k, v in kwargs.items() if k not in ['bg', 'fg', 'bd', 'relief']}
return ctk.CTkEntry(
parent,
textvariable=textvariable,
font=FONT_BODY,
height=36,
corner_radius=6,
border_color=BG_LIGHT,
**safe_kwargs
)
def create_card_frame(parent, **kwargs):
"""Create a styled CTkFrame"""
# Filter tk params
safe_kwargs = {k: v for k, v in kwargs.items() if k not in ['bg', 'highlightthickness', 'highlightbackground']}
return ctk.CTkFrame(
parent,
corner_radius=12,
fg_color=BG_MEDIUM, # Card background
**safe_kwargs
)
def create_styled_text(parent, height=10, **kwargs):
"""
Create a styled CTkTextbox
Note: CTkTextbox API is slightly different from tk.Text
"""
safe_kwargs = {k: v for k, v in kwargs.items() if k not in ['bg', 'fg', 'selectbackground', 'selectforeground', 'relief']}
return ctk.CTkTextbox(
parent,
height=height * 20, # Approx pixel height
font=FONT_MONO,
corner_radius=10,
text_color=TEXT_PRIMARY,
fg_color=BG_DARK,
border_color=BG_LIGHT,
border_width=1,
**safe_kwargs
)
def get_rarity_color(rarity):
return RARITY_COLORS.get(rarity, TEXT_SECONDARY)
def get_type_color(card_type):
return TYPE_COLORS.get(card_type, TEXT_SECONDARY)
def get_type_icon(card_type):
return TYPE_ICONS.get(card_type, '')
# ═══════════════════════════════════════════════════════════════════════════════
# TOOLTIPS (Legacy/Wrapper)
# ═══════════════════════════════════════════════════════════════════════════════
EFFECT_DESCRIPTIONS = {
"Friendship Bonus": "Increases stats gained when training with this support card during Friendship Training (orange aura).",
"Motivation Bonus": "Increases stats gained based on your Uma's motivation level.",
"Specialty Rate": "Increases the chance of this card appearing in its specialty training.",
"Training Bonus": "Flat percentage increase to stats gained in training where this card is present.",
"Initial Bond": "Starting gauge value for this card.",
"Race Bonus": "Increases stats gained from racing.",
"Fan Count Bonus": "Increases fans gained from racing.",
"Skill Pt Bonus": "Bonus skill points gained when training with this card.",
"Hint Lv": "Starting level of skills taught by this card's hints.",
"Hint Rate": "Increases chance of getting a hint event.",
"Minigame Fail Rate": "Reduces chance of failing training.",
"Energy Usage": "Reduces energy consumed during training.",
"Current Energy": "Increases starting energy in scenario.",
"Vitality": "Increases vitality gain from events.",
"Stamina": "Increases stamina gain from training.",
"Speed": "Increases speed gain from training.",
"Power": "Increases power gain from training.",
"Guts": "Increases guts gain from training.",
"Wisdom": "Increases wisdom gain from training.",
"Logic": "Custom logic effect.",
"Starting Stats": "Increases initial stats at start of scenario."
}
class Tooltip:
"""
Simple Tooltip adapted for CTk widgets
"""
def __init__(self, widget, text):
self.widget = widget
self.text = text
self.tip_window = None
self.id = None
self.x = self.y = 0
self.widget.bind("<Enter>", self.enter)
self.widget.bind("<Leave>", self.leave)
self.widget.bind("<ButtonPress>", self.leave)
def enter(self, event=None):
self.schedule()
def leave(self, event=None):
self.unschedule()
self.hidetip()
def schedule(self):
self.unschedule()
self.id = self.widget.after(500, self.showtip)
def unschedule(self):
id = self.id
self.id = None
if id:
self.widget.after_cancel(id)
def showtip(self, event=None):
x = y = 0
try:
x, y, cx, cy = self.widget.bbox("insert")
except:
pass
x += self.widget.winfo_rootx() + 25
y += self.widget.winfo_rooty() + 20
self.tip_window = tk.Toplevel(self.widget)
self.tip_window.wm_overrideredirect(True)
self.tip_window.wm_geometry(f"+{x}+{y}")
# CTkLabel inside the tooltip (if possible) or standard Label
# We'll use standard Label for the tooltip window content to be safe
label = tk.Label(
self.tip_window,
text=self.text,
justify=tk.LEFT,
background="#2b2b2b",
foreground="#DCE4EE",
relief=tk.SOLID,
borderwidth=1,
font=("Segoe UI", 10),
padx=10,
pady=5
)
label.pack(ipadx=1)
def hidetip(self):
tw = self.tip_window
self.tip_window = None
if tw:
tw.destroy()
# Primary backgrounds (Neutral Dark)
BG_DARKEST = '#0f0f0f' # Deepest background (Material Darker)
BG_DARK = '#141414' # Main application background
BG_MEDIUM = '#1f1f1f' # Card/panel backgrounds
BG_LIGHT = '#2d2d2d' # Elevated elements, hover states
BG_HIGHLIGHT = '#3d3d3d' # Active/selected backgrounds
# Accents (vibrant but refined) # Accents (vibrant but refined)
ACCENT_PRIMARY = '#ff6b9d' # Pink accent (main action color) ACCENT_PRIMARY = '#ff6b9d' # Pink accent (main action color)
@@ -198,7 +510,7 @@ def configure_styles(root: tk.Tk):
foreground=TEXT_PRIMARY, foreground=TEXT_PRIMARY,
padding=6) padding=6)
style.map('Treeview', style.map('Treeview',
background=[('selected', ACCENT_PRIMARY)], background=[('selected', BG_HIGHLIGHT)],
foreground=[('selected', TEXT_PRIMARY)]) foreground=[('selected', TEXT_PRIMARY)])
style.map('Treeview.Heading', style.map('Treeview.Heading',
background=[('active', BG_HIGHLIGHT)]) background=[('active', BG_HIGHLIGHT)])
@@ -209,7 +521,7 @@ def configure_styles(root: tk.Tk):
foreground=TEXT_SECONDARY, foreground=TEXT_SECONDARY,
fieldbackground=BG_MEDIUM, fieldbackground=BG_MEDIUM,
font=FONT_BODY, font=FONT_BODY,
rowheight=60) rowheight=80)
# Deck list style # Deck list style
style.configure('DeckList.Treeview', style.configure('DeckList.Treeview',
@@ -217,7 +529,7 @@ def configure_styles(root: tk.Tk):
foreground=TEXT_SECONDARY, foreground=TEXT_SECONDARY,
fieldbackground=BG_MEDIUM, fieldbackground=BG_MEDIUM,
font=FONT_BODY, font=FONT_BODY,
rowheight=60) rowheight=80)
style.map('DeckList.Treeview', style.map('DeckList.Treeview',
background=[('selected', ACCENT_PRIMARY)]) background=[('selected', ACCENT_PRIMARY)])
@@ -258,17 +570,27 @@ def configure_styles(root: tk.Tk):
def create_styled_entry(parent, textvariable=None, **kwargs): def create_styled_entry(parent, textvariable=None, **kwargs):
"""Create a styled tk.Entry with modern appearance""" """Create a styled ctk.CTkEntry with modern appearance"""
entry = ttk.Entry( bg_color = kwargs.pop('bg', BG_MEDIUM)
fg_color = kwargs.pop('fg', TEXT_PRIMARY)
# Filter tk args
safe_kwargs = {k: v for k, v in kwargs.items() if k not in ['bg', 'fg', 'bd', 'relief', 'insertbackground', 'selectbackground', 'selectforeground', 'highlightthickness']}
entry = ctk.CTkEntry(
parent, parent,
textvariable=textvariable, textvariable=textvariable,
font=FONT_BODY, font=FONT_BODY,
**kwargs fg_color=bg_color,
text_color=fg_color,
border_width=1,
corner_radius=6,
**safe_kwargs
) )
return entry return entry
def create_styled_button(parent, text, command=None, style_type='default', **kwargs): def create_styled_button(parent, text, command=None, style_type='default', **kwargs):
"""Create a styled tk.Button with modern appearance""" """Create a styled ctk.CTkButton with modern appearance"""
bg_colors = { bg_colors = {
'default': BG_LIGHT, 'default': BG_LIGHT,
'accent': ACCENT_PRIMARY, 'accent': ACCENT_PRIMARY,
@@ -286,35 +608,27 @@ def create_styled_button(parent, text, command=None, style_type='default', **kwa
'danger': '#ff8a8a' 'danger': '#ff8a8a'
} }
bg = bg_colors.get(style_type, BG_LIGHT) fg_color = bg_colors.get(style_type, BG_LIGHT)
hover_bg = hover_colors.get(style_type, BG_HIGHLIGHT) hover_color = hover_colors.get(style_type, BG_HIGHLIGHT)
btn = tk.Button( # Filter out tk-specific args that might crash CTk
safe_kwargs = {k: v for k, v in kwargs.items() if k not in ['bg', 'bd', 'relief', 'activebackground', 'activeforeground', 'padx', 'pady']}
# Handle width/height specifics if needed, though CTk handles pixel width natively
btn = ctk.CTkButton(
parent, parent,
text=text, text=text,
command=command, command=command,
bg=bg, fg_color=fg_color,
fg=TEXT_PRIMARY, text_color=TEXT_PRIMARY,
font=FONT_BODY_BOLD if style_type == 'accent' else FONT_BODY, font=FONT_BODY_BOLD if style_type == 'accent' else FONT_BODY,
activebackground=hover_bg, hover_color=hover_color,
activeforeground=TEXT_PRIMARY, corner_radius=8,
bd=0, border_width=0,
padx=16, **safe_kwargs
pady=8,
cursor='hand2',
relief=tk.FLAT,
**kwargs
) )
# Add hover effect
def on_enter(e):
btn.configure(bg=hover_bg)
def on_leave(e):
btn.configure(bg=bg)
btn.bind('<Enter>', on_enter)
btn.bind('<Leave>', on_leave)
return btn return btn

View File

@@ -1,21 +1,22 @@
""" """
Update Dialog for UmamusumeCardManager Update Dialog for UmamusumeCardManager
Provides a modal dialog for the update process. Provides a modal dialog for the update process.
Updated for CustomTkinter
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext from tkinter import ttk, messagebox
import customtkinter as ctk
import threading import threading
import webbrowser
from typing import Optional, Callable
import sys import sys
import os import os
from typing import Optional, Callable
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from updater.update_checker import check_for_updates, download_update, apply_update, get_current_version from updater.update_checker import check_for_updates, download_update, apply_update, get_current_version
from gui.theme import ( from gui.theme import (
BG_DARK, BG_DARKEST, BG_MEDIUM, BG_LIGHT, BG_HIGHLIGHT, BG_DARK, BG_MEDIUM, BG_LIGHT, BG_HIGHLIGHT,
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_SUCCESS, ACCENT_ERROR, ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_SUCCESS, ACCENT_ERROR,
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED, TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL, FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL,
@@ -26,7 +27,7 @@ from gui.theme import (
class UpdateDialog: class UpdateDialog:
"""Modal dialog for checking and applying updates.""" """Modal dialog for checking and applying updates."""
def __init__(self, parent: tk.Tk, on_close_callback: Optional[Callable] = None): def __init__(self, parent: ctk.CTk, on_close_callback: Optional[Callable] = None):
self.parent = parent self.parent = parent
self.on_close_callback = on_close_callback self.on_close_callback = on_close_callback
self.update_info = None self.update_info = None
@@ -34,25 +35,28 @@ class UpdateDialog:
self.is_downloading = False self.is_downloading = False
# Create the dialog window # Create the dialog window
self.dialog = tk.Toplevel(parent) self.dialog = ctk.CTkToplevel(parent)
self.dialog.title("Check for Updates") self.dialog.title("Check for Updates")
self.dialog.geometry("520x600") self.dialog.geometry("520x600")
self.dialog.resizable(True, True) self.dialog.resizable(True, True)
self.dialog.minsize(480, 500) self.dialog.minsize(480, 500)
# Set transient/grab to make it modal
self.dialog.transient(parent) self.dialog.transient(parent)
self.dialog.grab_set() self.dialog.grab_set()
# Center on parent # Center on parent
self.center_on_parent() self.center_on_parent()
self.dialog.configure(bg=BG_DARK)
# Set up the UI # Set up the UI
self.setup_ui() self.setup_ui()
# Start checking for updates # Start checking for updates
self.check_for_updates() self.check_for_updates()
# Handle close window event
self.dialog.protocol("WM_DELETE_WINDOW", self.close)
def center_on_parent(self): def center_on_parent(self):
"""Center the dialog on the parent window.""" """Center the dialog on the parent window."""
self.dialog.update_idletasks() self.dialog.update_idletasks()
@@ -71,9 +75,94 @@ class UpdateDialog:
def setup_ui(self): def setup_ui(self):
"""Set up the dialog UI.""" """Set up the dialog UI."""
# Button frame (Create first to pack at bottom) # Main container (CTk has its own bg, so we don't strictly need a frame, but for padding)
self.button_frame = tk.Frame(self.dialog, bg=BG_DARK, pady=20, padx=20) main_frame = ctk.CTkFrame(self.dialog, fg_color="transparent")
self.button_frame.pack(side=tk.BOTTOM, fill=tk.X) main_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)
# Title
self.title_label = ctk.CTkLabel(
main_frame,
text="🔄 Checking for Updates...",
font=FONT_HEADER,
text_color=ACCENT_PRIMARY
)
self.title_label.pack(pady=(0, 10))
# Status message
self.status_label = ctk.CTkLabel(
main_frame,
text="Connecting to GitHub...",
font=FONT_BODY,
text_color=TEXT_MUTED,
wraplength=460
)
self.status_label.pack(pady=(0, 10))
# Version info frame
self.version_frame = ctk.CTkFrame(main_frame, fg_color=BG_MEDIUM)
self.version_frame.pack(fill=tk.X, pady=(0, 15), padx=5)
self.current_version_label = ctk.CTkLabel(
self.version_frame,
text=f"Current Version: v{get_current_version()}",
font=FONT_BODY,
text_color=TEXT_SECONDARY
)
self.current_version_label.pack(anchor='w', padx=15, pady=5)
self.new_version_label = ctk.CTkLabel(
self.version_frame,
text="Latest Version: Checking...",
font=FONT_BODY,
text_color=TEXT_SECONDARY
)
self.new_version_label.pack(anchor='w', padx=15, pady=5)
# Release Notes Area
self.notes_label = ctk.CTkLabel(
main_frame,
text="What's New:",
font=FONT_BODY_BOLD,
text_color=TEXT_PRIMARY
)
self.notes_label.pack(anchor='w', pady=(0, 5))
# Text box for release notes
self.notes_text = ctk.CTkTextbox(
main_frame,
height=200,
fg_color=BG_MEDIUM,
text_color=TEXT_SECONDARY,
font=FONT_SMALL,
border_width=0
)
self.notes_text.pack(fill=tk.BOTH, expand=True, pady=(0, 15))
self.notes_text.insert("1.0", "Checking for release notes...")
self.notes_text.configure(state=tk.DISABLED)
# Progress bar (hidden initially)
self.progress_frame = ctk.CTkFrame(main_frame, fg_color="transparent")
self.progress_frame.pack(fill=tk.X, pady=(0, 10))
self.progress_label = ctk.CTkLabel(
self.progress_frame,
text="",
font=FONT_SMALL,
text_color=TEXT_MUTED
)
self.progress_label.pack(anchor='w', pady=(0, 5))
self.progress_bar = ctk.CTkProgressBar(
self.progress_frame,
mode='indeterminate',
width=400
)
self.progress_bar.pack(fill=tk.X)
self.progress_bar.start()
# Button frame
self.button_frame = ctk.CTkFrame(self.dialog, fg_color="transparent")
self.button_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=20, pady=20)
# Close button # Close button
self.close_button = create_styled_button( self.close_button = create_styled_button(
@@ -91,101 +180,7 @@ class UpdateDialog:
command=self.start_download, command=self.start_download,
style_type='accent' style_type='accent'
) )
# We don't pack it yet # Pack when ready
# Main container
main_frame = tk.Frame(self.dialog, bg=BG_DARK, padx=25, pady=20)
main_frame.pack(fill=tk.BOTH, expand=True)
# Title
self.title_label = tk.Label(
main_frame,
text="🔄 Checking for Updates...",
font=FONT_HEADER,
bg=BG_DARK,
fg=ACCENT_PRIMARY
)
self.title_label.pack(pady=(0, 10))
# Status message
self.status_label = tk.Label(
main_frame,
text="Connecting to GitHub...",
font=FONT_BODY,
bg=BG_DARK,
fg=TEXT_MUTED,
wraplength=460
)
self.status_label.pack(pady=(0, 10))
# Version info frame
self.version_frame = tk.Frame(main_frame, bg=BG_MEDIUM, padx=15, pady=10)
self.version_frame.pack(fill=tk.X, pady=(0, 15))
self.current_version_label = tk.Label(
self.version_frame,
text=f"Current Version: v{get_current_version()}",
font=FONT_BODY,
bg=BG_MEDIUM,
fg=TEXT_SECONDARY
)
self.current_version_label.pack(anchor='w')
self.new_version_label = tk.Label(
self.version_frame,
text="Latest Version: Checking...",
font=FONT_BODY,
bg=BG_MEDIUM,
fg=TEXT_SECONDARY
)
self.new_version_label.pack(anchor='w')
# Release Notes Area
self.notes_label = tk.Label(
main_frame,
text="What's New:",
font=FONT_BODY_BOLD,
bg=BG_DARK,
fg=TEXT_PRIMARY
)
self.notes_label.pack(anchor='w', pady=(0, 5))
# Text box for release notes
self.notes_text = scrolledtext.ScrolledText(
main_frame,
height=10,
bg=BG_MEDIUM,
fg=TEXT_SECONDARY,
font=FONT_SMALL,
borderwidth=0,
highlightthickness=0,
padx=10,
pady=10
)
self.notes_text.pack(fill=tk.BOTH, expand=True, pady=(0, 15))
self.notes_text.insert(tk.END, "Checking for release notes...")
self.notes_text.config(state=tk.DISABLED)
# Progress bar (hidden initially)
self.progress_frame = tk.Frame(main_frame, bg=BG_DARK)
self.progress_frame.pack(fill=tk.X, pady=(0, 10))
self.progress_label = tk.Label(
self.progress_frame,
text="",
font=FONT_SMALL,
bg=BG_DARK,
fg=TEXT_MUTED
)
self.progress_label.pack(anchor='w', pady=(0, 5))
self.progress_bar = ttk.Progressbar(
self.progress_frame,
mode='indeterminate',
length=460
)
self.progress_bar.pack(fill=tk.X)
self.progress_bar.start(10)
def check_for_updates(self): def check_for_updates(self):
"""Check for updates in a background thread.""" """Check for updates in a background thread."""
@@ -202,19 +197,19 @@ class UpdateDialog:
self.progress_frame.pack_forget() # Hide progress bar when check is done self.progress_frame.pack_forget() # Hide progress bar when check is done
# Enable text box to update it # Enable text box to update it
self.notes_text.config(state=tk.NORMAL) self.notes_text.configure(state=tk.NORMAL)
self.notes_text.delete(1.0, tk.END) self.notes_text.delete("1.0", tk.END)
if self.update_info: if self.update_info:
# Update available! # Update available!
self.title_label.config(text="🎉 Update Available!") self.title_label.configure(text="🎉 Update Available!")
self.status_label.config( self.status_label.configure(
text="A new version is available.", text="A new version is available.",
fg=ACCENT_SUCCESS text_color=ACCENT_SUCCESS
) )
self.new_version_label.config( self.new_version_label.configure(
text=f"Latest Version: {self.update_info['new_version']}", text=f"Latest Version: {self.update_info['new_version']}",
fg=ACCENT_SUCCESS text_color=ACCENT_SUCCESS
) )
# Show Release Notes # Show Release Notes
@@ -225,18 +220,18 @@ class UpdateDialog:
self.update_button.pack(side=tk.RIGHT, padx=(0, 10)) self.update_button.pack(side=tk.RIGHT, padx=(0, 10))
else: else:
# Up to date or error # Up to date or error
self.title_label.config(text="✅ You're Up to Date!") self.title_label.configure(text="✅ You're Up to Date!")
self.status_label.config( self.status_label.configure(
text=f"You are running the latest version.", text=f"You are running the latest version.",
fg=TEXT_SECONDARY text_color=TEXT_SECONDARY
) )
self.new_version_label.config( self.new_version_label.configure(
text=f"Latest Version: v{get_current_version()}", text=f"Latest Version: v{get_current_version()}",
fg=ACCENT_SUCCESS text_color=ACCENT_SUCCESS
) )
self.notes_text.insert(tk.END, "You are using the latest version of Umamusume Support Card Manager.\n\nEnjoy!") self.notes_text.insert(tk.END, "You are using the latest version of Umamusume Support Card Manager.\n\nEnjoy!")
self.notes_text.config(state=tk.DISABLED) self.notes_text.configure(state=tk.DISABLED)
def start_download(self): def start_download(self):
"""Start downloading the update.""" """Start downloading the update."""
@@ -244,22 +239,22 @@ class UpdateDialog:
return return
self.is_downloading = True self.is_downloading = True
self.update_button.config(state=tk.DISABLED, text="Downloading...") self.update_button.configure(state=tk.DISABLED, text="Downloading...")
self.close_button.config(state=tk.DISABLED) self.close_button.configure(state=tk.DISABLED)
self.title_label.config(text="⬇️ Downloading Update...") self.title_label.configure(text="⬇️ Downloading Update...")
self.status_label.config(text="Please wait...", fg=TEXT_MUTED) self.status_label.configure(text="Please wait...", text_color=TEXT_MUTED)
# Configure progress bar for determinate mode # Configure progress bar for determinate mode
self.progress_frame.pack(fill=tk.X, pady=(0, 10)) # Show progress frame again self.progress_frame.pack(fill=tk.X, pady=(0, 10))
self.progress_bar.config(mode='determinate', maximum=100) self.progress_bar.configure(mode='determinate')
self.progress_bar.set(0)
self.progress_bar.pack(fill=tk.X) self.progress_bar.pack(fill=tk.X)
self.progress_bar['value'] = 0
def download(): def download():
def progress_callback(downloaded, total): def progress_callback(downloaded, total):
if total > 0: if total > 0:
percent = int((downloaded / total) * 100) percent = downloaded / total # 0.0 to 1.0 for CTk
mb_downloaded = downloaded / (1024 * 1024) mb_downloaded = downloaded / (1024 * 1024)
mb_total = total / (1024 * 1024) mb_total = total / (1024 * 1024)
self.dialog.after(0, lambda: self.update_progress(percent, mb_downloaded, mb_total)) self.dialog.after(0, lambda: self.update_progress(percent, mb_downloaded, mb_total))
@@ -270,44 +265,44 @@ class UpdateDialog:
self.download_thread = threading.Thread(target=download, daemon=True) self.download_thread = threading.Thread(target=download, daemon=True)
self.download_thread.start() self.download_thread.start()
def update_progress(self, percent: int, downloaded_mb: float, total_mb: float): def update_progress(self, percent: float, downloaded_mb: float, total_mb: float):
"""Update the progress bar.""" """Update the progress bar."""
self.progress_bar['value'] = percent self.progress_bar.set(percent)
self.progress_label.config(text=f"Downloaded: {downloaded_mb:.1f} MB / {total_mb:.1f} MB ({percent}%)") self.progress_label.configure(text=f"Downloaded: {downloaded_mb:.1f} MB / {total_mb:.1f} MB ({int(percent*100)}%)")
def download_complete(self, download_path: Optional[str]): def download_complete(self, download_path: Optional[str]):
"""Called when the download is complete.""" """Called when the download is complete."""
self.is_downloading = False self.is_downloading = False
if download_path: if download_path:
self.title_label.config(text="✅ Download Complete!") self.title_label.configure(text="✅ Download Complete!")
self.status_label.config( self.status_label.configure(
text="Update ready to install.", text="Update ready to install.",
fg=ACCENT_SUCCESS text_color=ACCENT_SUCCESS
) )
# Change button to install # Change button to install
self.update_button.config( self.update_button.configure(
state=tk.NORMAL, state=tk.NORMAL,
text="🔄 Install & Restart", text="🔄 Install & Restart",
command=lambda: self.install_update(download_path) command=lambda: self.install_update(download_path)
) )
self.close_button.config(state=tk.NORMAL) self.close_button.configure(state=tk.NORMAL)
else: else:
self.title_label.config(text="❌ Download Failed") self.title_label.configure(text="❌ Download Failed")
self.status_label.config( self.status_label.configure(
text="Failed not download update.", text="Failed not download update.",
fg=ACCENT_ERROR text_color=ACCENT_ERROR
) )
self.update_button.config(state=tk.NORMAL, text="⬇️ Retry Download") self.update_button.configure(state=tk.NORMAL, text="⬇️ Retry Download")
self.close_button.config(state=tk.NORMAL) self.close_button.configure(state=tk.NORMAL)
def install_update(self, download_path: str): def install_update(self, download_path: str):
"""Install the downloaded update.""" """Install the downloaded update."""
self.title_label.config(text="🔄 Installing Update...") self.title_label.configure(text="🔄 Installing Update...")
self.status_label.config(text="Applying update...", fg=TEXT_MUTED) self.status_label.configure(text="Applying update...", text_color=TEXT_MUTED)
self.update_button.config(state=tk.DISABLED) self.update_button.configure(state=tk.DISABLED)
self.close_button.config(state=tk.DISABLED) self.close_button.configure(state=tk.DISABLED)
if apply_update(download_path): if apply_update(download_path):
# Exit the application - the updater script will restart it # Exit the application - the updater script will restart it
@@ -322,7 +317,6 @@ class UpdateDialog:
) )
self.close() self.close()
def close(self): def close(self):
"""Close the dialog.""" """Close the dialog."""
if self.on_close_callback: if self.on_close_callback:
@@ -330,15 +324,8 @@ class UpdateDialog:
self.dialog.destroy() self.dialog.destroy()
def show_update_dialog(parent: tk.Tk, on_close_callback: Optional[Callable] = None) -> UpdateDialog: def show_update_dialog(parent: ctk.CTk, on_close_callback: Optional[Callable] = None) -> UpdateDialog:
""" """
Show the update dialog. Show the update dialog.
Args:
parent: The parent Tk window
on_close_callback: Optional callback when dialog is closed
Returns:
The UpdateDialog instance
""" """
return UpdateDialog(parent, on_close_callback) return UpdateDialog(parent, on_close_callback)