feat: Implement initial GUI for deck, card, and skill management with CustomTkinter components.
This commit is contained in:
507
gui/card_view.py
507
gui/card_view.py
@@ -1,9 +1,11 @@
|
||||
"""
|
||||
Card List View - Browse and search support cards with ownership management
|
||||
Updated for CustomTkinter
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
import customtkinter as ctk
|
||||
import sys
|
||||
import os
|
||||
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 gui.theme import (
|
||||
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,
|
||||
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,
|
||||
create_styled_button, create_styled_text, create_card_frame,
|
||||
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"""
|
||||
|
||||
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_stats_updated = on_stats_updated_callback
|
||||
self.cards = []
|
||||
@@ -46,203 +48,250 @@ class CardListFrame(ttk.Frame):
|
||||
def create_widgets(self):
|
||||
"""Create the card list interface"""
|
||||
# Main horizontal layout
|
||||
main_pane = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
|
||||
main_pane.pack(fill=tk.BOTH, expand=True)
|
||||
# CTk doesn't have PanedWindow, so we'll use a grid or pack with frames
|
||||
# We can simulate split view with two frames
|
||||
|
||||
# Left panel - Card list with filters
|
||||
left_frame = ttk.Frame(main_pane, width=420)
|
||||
main_pane.add(left_frame, weight=1)
|
||||
left_frame = ctk.CTkFrame(self, width=420, corner_radius=10)
|
||||
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=False, padx=(0, 10))
|
||||
|
||||
# Right panel - Card details
|
||||
self.details_frame = ttk.Frame(main_pane)
|
||||
main_pane.add(self.details_frame, weight=2)
|
||||
self.details_frame = ctk.CTkFrame(self, corner_radius=10, fg_color="transparent")
|
||||
self.details_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
|
||||
|
||||
# === 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.type_var = tk.StringVar(value="All")
|
||||
self.owned_only_var = tk.BooleanVar(value=False)
|
||||
self.search_var = tk.StringVar(value="")
|
||||
|
||||
# Search bar with modern styling
|
||||
search_frame = tk.Frame(left_frame, bg=BG_DARK)
|
||||
search_frame.pack(fill=tk.X, padx=10, pady=10)
|
||||
# Search bar
|
||||
search_frame = ctk.CTkFrame(left_frame, fg_color="transparent")
|
||||
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)
|
||||
search_icon.pack(side=tk.LEFT, padx=(0, 5))
|
||||
|
||||
self.search_var = tk.StringVar()
|
||||
self.search_entry = ttk.Entry(search_frame, textvariable=self.search_var, width=35)
|
||||
self.search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||||
|
||||
# Set placeholder BEFORE adding the trace (so it doesn't trigger filter)
|
||||
self.search_entry.insert(0, "Search 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)
|
||||
|
||||
# NOW add the trace (after placeholder is set)
|
||||
self.search_var.trace('w', lambda *args: self.filter_cards())
|
||||
self.search_entry = ctk.CTkEntry(
|
||||
search_frame,
|
||||
textvariable=self.search_var,
|
||||
placeholder_text="🔍 Search cards...",
|
||||
width=200,
|
||||
height=36
|
||||
)
|
||||
self.search_entry.pack(fill=tk.X, expand=True)
|
||||
self.search_entry.bind('<KeyRelease>', lambda e: self.filter_cards()) # Trace didn't work smoothly with CTkVar sometimes
|
||||
|
||||
# Filter dropdowns
|
||||
filter_frame = tk.Frame(left_frame, bg=BG_DARK)
|
||||
filter_frame.pack(fill=tk.X, padx=10, pady=(0, 10))
|
||||
filter_frame = ctk.CTkFrame(left_frame, fg_color="transparent")
|
||||
filter_frame.pack(fill=tk.X, padx=15, pady=(0, 10))
|
||||
|
||||
# Rarity filter
|
||||
tk.Label(filter_frame, text="Rarity:", font=FONT_SMALL, bg=BG_DARK, fg=TEXT_MUTED).pack(side=tk.LEFT)
|
||||
rarity_combo = ttk.Combobox(filter_frame, textvariable=self.rarity_var,
|
||||
values=["All", "SSR", "SR", "R"], width=7, state='readonly')
|
||||
rarity_combo.pack(side=tk.LEFT, padx=(5, 15))
|
||||
rarity_combo.bind('<<ComboboxSelected>>', lambda e: self.filter_cards())
|
||||
# ctk.CTkLabel(filter_frame, text="Rarity:", font=FONT_TINY).pack(side=tk.LEFT)
|
||||
rarity_combo = ctk.CTkComboBox(
|
||||
filter_frame,
|
||||
variable=self.rarity_var,
|
||||
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
|
||||
tk.Label(filter_frame, text="Type:", font=FONT_SMALL, bg=BG_DARK, fg=TEXT_MUTED).pack(side=tk.LEFT)
|
||||
type_combo = ttk.Combobox(filter_frame, textvariable=self.type_var,
|
||||
values=["All", "Speed", "Stamina", "Power", "Guts", "Wisdom", "Friend", "Group"],
|
||||
width=10, state='readonly')
|
||||
type_combo.pack(side=tk.LEFT, padx=5)
|
||||
type_combo.bind('<<ComboboxSelected>>', lambda e: self.filter_cards())
|
||||
# ctk.CTkLabel(filter_frame, text="Type:", font=FONT_TINY).pack(side=tk.LEFT)
|
||||
type_combo = ctk.CTkComboBox(
|
||||
filter_frame,
|
||||
variable=self.type_var,
|
||||
values=["All", "Speed", "Stamina", "Power", "Guts", "Wisdom", "Friend", "Group"],
|
||||
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
|
||||
owned_check = ttk.Checkbutton(filter_frame, text="Owned Only",
|
||||
variable=self.owned_only_var, command=self.filter_cards)
|
||||
owned_check.pack(side=tk.LEFT, padx=15)
|
||||
# Reset Button (Icon only maybe? or small text)
|
||||
ctk.CTkButton(
|
||||
filter_frame,
|
||||
text="✕",
|
||||
width=32,
|
||||
height=32,
|
||||
fg_color=BG_LIGHT,
|
||||
hover_color=ACCENT_ERROR,
|
||||
command=self.reset_filters
|
||||
).pack(side=tk.LEFT)
|
||||
|
||||
# Owned Only Checkbox (Below filters for spacing)
|
||||
owned_frame = ctk.CTkFrame(left_frame, fg_color="transparent")
|
||||
owned_frame.pack(fill=tk.X, padx=15, pady=(0, 10))
|
||||
|
||||
# Reset Button
|
||||
ttk.Button(filter_frame, text="Reset", command=self.reset_filters,
|
||||
style='Small.TButton', width=7).pack(side=tk.LEFT, padx=5)
|
||||
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
|
||||
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
|
||||
self.count_label = tk.Label(left_frame, text="0 cards", font=FONT_SMALL,
|
||||
bg=BG_DARK, fg=ACCENT_PRIMARY)
|
||||
self.count_label.pack(pady=5)
|
||||
self.count_label = ctk.CTkLabel(
|
||||
left_frame,
|
||||
text="0 cards",
|
||||
font=FONT_SMALL,
|
||||
text_color=TEXT_MUTED
|
||||
)
|
||||
self.count_label.pack(pady=(5, 5))
|
||||
|
||||
# Card list (Treeview)
|
||||
list_frame = tk.Frame(left_frame, bg=BG_DARK)
|
||||
list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
|
||||
# Card list (Treeview wrapped in Frame)
|
||||
# We use a standard Frame to hold the Treeview because Treeview is a tk widget
|
||||
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'),
|
||||
show='tree headings', selectmode='browse',
|
||||
style="CardList.Treeview")
|
||||
# Scrollbar
|
||||
scrollbar = ttk.Scrollbar(tree_container, orient=tk.VERTICAL)
|
||||
|
||||
self.tree = ttk.Treeview(
|
||||
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.heading('#0', text='')
|
||||
self.tree.column('#0', width=45, anchor='center')
|
||||
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('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('type', text='Type', command=lambda: self.sort_column('type', False))
|
||||
|
||||
self.tree.column('owned', width=30, anchor='center')
|
||||
self.tree.column('name', width=180, minwidth=150)
|
||||
self.tree.column('rarity', width=55, anchor='center')
|
||||
self.tree.column('type', width=90, 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.column('owned', width=50, anchor='center')
|
||||
self.tree.column('name', width=350, minwidth=200)
|
||||
self.tree.column('rarity', width=80, anchor='center')
|
||||
self.tree.column('type', width=100, anchor='center')
|
||||
|
||||
self.tree.bind('<<TreeviewSelect>>', self.on_select)
|
||||
|
||||
# Tag for owned cards
|
||||
self.tree.tag_configure('owned', background='#1a3a2e')
|
||||
self.tree.tag_configure('owned', background='#1a3a2e') # Kept legacy tag, might need update if theme changes
|
||||
|
||||
# === Right Panel Contents (Details) ===
|
||||
self.create_details_panel()
|
||||
|
||||
def create_details_panel(self):
|
||||
"""Create the card details panel"""
|
||||
# Container with card-like appearance
|
||||
details_container = tk.Frame(self.details_frame, bg=BG_DARK)
|
||||
details_container.pack(fill=tk.BOTH, expand=True, padx=15, pady=10)
|
||||
# Container
|
||||
details_container = ctk.CTkFrame(self.details_frame, corner_radius=12)
|
||||
details_container.pack(fill=tk.BOTH, expand=True, padx=0, pady=0) # Flush with right panel
|
||||
|
||||
# Image area with card frame
|
||||
image_frame = create_card_frame(details_container, padx=10, pady=10)
|
||||
# Content scrolling container (Optional, but good for small screens)
|
||||
# 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)
|
||||
|
||||
self.image_label = tk.Label(image_frame, text="", bg=BG_MEDIUM)
|
||||
self.image_label.pack(padx=5, pady=5)
|
||||
self.image_label = ctk.CTkLabel(image_frame, text="", height=180, width=180)
|
||||
self.image_label.pack(padx=10, pady=10)
|
||||
|
||||
# Header with card name
|
||||
self.detail_name = tk.Label(details_container, text="Select a card",
|
||||
font=FONT_HEADER, bg=BG_DARK, fg=ACCENT_PRIMARY)
|
||||
self.detail_name.pack(pady=(10, 5))
|
||||
self.detail_name = ctk.CTkLabel(
|
||||
details_container,
|
||||
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="",
|
||||
font=FONT_SMALL, bg=BG_DARK, fg=TEXT_MUTED)
|
||||
self.detail_info = ctk.CTkLabel(
|
||||
details_container,
|
||||
text="",
|
||||
font=FONT_SUBHEADER,
|
||||
text_color=TEXT_MUTED
|
||||
)
|
||||
self.detail_info.pack()
|
||||
|
||||
# Owned checkbox with emphasis
|
||||
owned_frame = tk.Frame(details_container, bg=BG_DARK)
|
||||
owned_frame.pack(pady=15)
|
||||
# Owned checkbox
|
||||
owned_frame = ctk.CTkFrame(details_container, fg_color="transparent")
|
||||
owned_frame.pack(pady=10)
|
||||
|
||||
self.owned_var = tk.BooleanVar(value=False)
|
||||
self.owned_checkbox = ttk.Checkbutton(owned_frame, text="✨ I Own This Card",
|
||||
variable=self.owned_var,
|
||||
command=self.toggle_owned,
|
||||
style='Large.TCheckbutton')
|
||||
self.owned_checkbox = ctk.CTkCheckBox(
|
||||
owned_frame,
|
||||
text="✨ I Own This Card",
|
||||
variable=self.owned_var,
|
||||
command=self.toggle_owned,
|
||||
font=FONT_HEADER,
|
||||
checkbox_width=28, checkbox_height=28
|
||||
)
|
||||
self.owned_checkbox.pack(side=tk.LEFT)
|
||||
|
||||
# Level selector with button-based control (no slider)
|
||||
level_frame = tk.Frame(details_container, bg=BG_DARK)
|
||||
level_frame.pack(fill=tk.X, padx=30, pady=10)
|
||||
# Level selector
|
||||
level_frame = ctk.CTkFrame(details_container, fg_color="transparent")
|
||||
level_frame.pack(fill=tk.X, padx=40, pady=10)
|
||||
|
||||
tk.Label(level_frame, text="Card Level:", font=FONT_BODY,
|
||||
bg=BG_DARK, fg=TEXT_SECONDARY).pack(side=tk.LEFT)
|
||||
ctk.CTkLabel(level_frame, text="Card Level:", font=FONT_SUBHEADER, text_color=TEXT_SECONDARY).pack(side=tk.LEFT)
|
||||
|
||||
# Level display with increment/decrement
|
||||
level_ctrl = tk.Frame(level_frame, bg=BG_DARK)
|
||||
level_ctrl.pack(side=tk.LEFT, padx=15)
|
||||
# Level items
|
||||
level_ctrl = ctk.CTkFrame(level_frame, fg_color="transparent")
|
||||
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.max_level = 50
|
||||
self.valid_levels = [30, 35, 40, 45, 50] # Default SSR
|
||||
self.valid_levels = [30, 35, 40, 45, 50]
|
||||
|
||||
# Decrement button
|
||||
dec_btn = tk.Button(level_ctrl, text="−", font=FONT_HEADER,
|
||||
bg=BG_LIGHT, fg=TEXT_PRIMARY, bd=0, width=2,
|
||||
activebackground=BG_HIGHLIGHT, cursor='hand2',
|
||||
command=self.decrement_level)
|
||||
dec_btn.pack(side=tk.LEFT)
|
||||
|
||||
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)
|
||||
self.level_label = ctk.CTkLabel(
|
||||
level_ctrl,
|
||||
text="50", width=60,
|
||||
font=(FONT_FAMILY, 24, 'bold'),
|
||||
text_color=ACCENT_PRIMARY
|
||||
)
|
||||
self.level_label.pack(side=tk.LEFT, padx=10)
|
||||
|
||||
# Increment button
|
||||
inc_btn = tk.Button(level_ctrl, text="+", font=FONT_HEADER,
|
||||
bg=BG_LIGHT, fg=TEXT_PRIMARY, bd=0, width=2,
|
||||
activebackground=BG_HIGHLIGHT, cursor='hand2',
|
||||
command=self.increment_level)
|
||||
inc_btn.pack(side=tk.LEFT)
|
||||
create_styled_button(
|
||||
level_ctrl, text="+",
|
||||
width=36, height=36,
|
||||
command=self.increment_level
|
||||
).pack(side=tk.LEFT)
|
||||
|
||||
# Quick level buttons container
|
||||
self.level_btn_frame = tk.Frame(level_frame, bg=BG_DARK)
|
||||
# Quick level buttons
|
||||
self.level_btn_frame = ctk.CTkFrame(level_frame, fg_color="transparent")
|
||||
self.level_btn_frame.pack(side=tk.LEFT, padx=20)
|
||||
|
||||
self.level_buttons = {}
|
||||
# Initial population
|
||||
self.update_level_buttons('SSR', 50)
|
||||
|
||||
# Effects display header
|
||||
effects_header = tk.Frame(details_container, bg=BG_DARK)
|
||||
effects_header.pack(fill=tk.X, padx=20, pady=(20, 10))
|
||||
# Effects display
|
||||
effects_header = ctk.CTkFrame(details_container, fg_color="transparent")
|
||||
effects_header.pack(fill=tk.X, padx=30, pady=(10, 5))
|
||||
|
||||
tk.Label(effects_header, text="📊 Effects at Current Level",
|
||||
font=FONT_SUBHEADER, bg=BG_DARK, fg=TEXT_PRIMARY).pack(side=tk.LEFT)
|
||||
ctk.CTkLabel(effects_header, text="📊 Effects at Current Level", font=FONT_SUBHEADER).pack(side=tk.LEFT)
|
||||
|
||||
# Effects text area with modern styling
|
||||
effects_frame = create_card_frame(details_container)
|
||||
effects_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=(0, 15))
|
||||
|
||||
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)
|
||||
# Effects text area
|
||||
self.effects_text = create_styled_text(details_container, height=30)
|
||||
self.effects_text.pack(fill=tk.BOTH, expand=True, padx=30, pady=(0, 30))
|
||||
self.effects_text.configure(state="disabled")
|
||||
|
||||
def load_cards(self):
|
||||
"""Load all cards from database"""
|
||||
@@ -255,58 +304,37 @@ class CardListFrame(ttk.Frame):
|
||||
self.rarity_var.set("All")
|
||||
self.type_var.set("All")
|
||||
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()
|
||||
|
||||
def _on_search_focus_in(self, event):
|
||||
"""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):
|
||||
def filter_cards(self, *args):
|
||||
"""Filter cards based on search and dropdown values"""
|
||||
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
|
||||
# Ignore placeholder text
|
||||
|
||||
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()
|
||||
|
||||
self.cards = get_all_cards(rarity_filter=rarity, type_filter=card_type,
|
||||
search_term=search, owned_only=owned_only)
|
||||
self.populate_tree(self.cards)
|
||||
self.count_label.configure(text=f"{len(self.cards)} cards")
|
||||
|
||||
def sort_column(self, col, reverse):
|
||||
"""Sort treeview by column"""
|
||||
l = [(self.tree.set(k, col), k) for k in self.tree.get_children('')]
|
||||
|
||||
# Custom sort logic
|
||||
if col == 'owned':
|
||||
# Sort by star/empty
|
||||
l.sort(key=lambda t: t[0] if t[0] else "", reverse=reverse)
|
||||
elif col == 'rarity':
|
||||
# Sort by rarity rank (SSR > SR > R)
|
||||
rarity_map = {'SSR': 3, 'SR': 2, 'R': 1}
|
||||
l.sort(key=lambda t: rarity_map.get(t[0], 0), reverse=reverse)
|
||||
else:
|
||||
# Default string sort
|
||||
l.sort(reverse=reverse)
|
||||
|
||||
# Rearrange items
|
||||
for index, (val, k) in enumerate(l):
|
||||
self.tree.move(k, '', index)
|
||||
|
||||
# Reverse sort next time
|
||||
self.tree.heading(col, command=lambda: self.sort_column(col, not reverse))
|
||||
|
||||
def populate_tree(self, cards):
|
||||
@@ -319,34 +347,34 @@ class CardListFrame(ttk.Frame):
|
||||
owned_mark = "★" if is_owned else ""
|
||||
tag = 'owned' if is_owned else ''
|
||||
|
||||
# Show level for owned cards
|
||||
display_name = name
|
||||
if is_owned and 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)
|
||||
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)
|
||||
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)
|
||||
self.icon_cache[card_id] = img
|
||||
except:
|
||||
pass
|
||||
|
||||
if img:
|
||||
self.tree.insert('', tk.END, iid=card_id, text='', image=img,
|
||||
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,))
|
||||
# Only use image if cached/loaded
|
||||
kv = {'image': img} if img else {}
|
||||
|
||||
self.tree.insert('', tk.END, iid=card_id, text='',
|
||||
values=(owned_mark, display_name, rarity, f"{type_icon} {card_type}"),
|
||||
tags=(tag,), **kv)
|
||||
|
||||
self.count_label.config(text=f"✨ {len(cards)} cards")
|
||||
if hasattr(self, 'count_label'):
|
||||
self.count_label.configure(text=f"{len(cards)} cards")
|
||||
|
||||
def on_select(self, event):
|
||||
"""Handle card selection"""
|
||||
@@ -360,41 +388,32 @@ class CardListFrame(ttk.Frame):
|
||||
if 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))
|
||||
|
||||
# Load card image if available
|
||||
# Load card image
|
||||
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
|
||||
|
||||
# Update level controls
|
||||
self.max_level = max_level
|
||||
self.update_level_buttons(rarity, max_level)
|
||||
|
||||
# Snap initial level to valid levels
|
||||
if initial_level not in self.valid_levels:
|
||||
# Find closest or default to max
|
||||
initial_level = max_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
|
||||
|
||||
# Update details display with colors
|
||||
# Update details text
|
||||
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_info.config(text=f"{rarity} │ {card_type} │ Max Level: {max_level}")
|
||||
self.detail_name.configure(text=f"{type_icon} {name}")
|
||||
self.detail_info.configure(text=f"{rarity} │ {card_type} │ Max Level: {max_level}")
|
||||
|
||||
# Load effects
|
||||
self.current_card_id = card_id
|
||||
self.update_effects_display()
|
||||
|
||||
# Notify parent window
|
||||
if self.on_card_selected:
|
||||
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):
|
||||
try:
|
||||
img = Image.open(resolved_path)
|
||||
img.thumbnail((130, 130)) # Slightly larger
|
||||
self.card_image = ImageTk.PhotoImage(img)
|
||||
self.image_label.config(image=self.card_image)
|
||||
img = ctk.CTkImage(light_image=Image.open(resolved_path),
|
||||
dark_image=Image.open(resolved_path),
|
||||
size=(180, 180))
|
||||
self.image_label.configure(image=img, text="")
|
||||
except Exception as e:
|
||||
self.image_label.config(image='', text="[Image not found]")
|
||||
self.image_label.configure(image=None, text="[Image Error]")
|
||||
else:
|
||||
self.image_label.config(image='', text="")
|
||||
self.image_label.configure(image=None, text="[No Image]")
|
||||
|
||||
def toggle_owned(self):
|
||||
"""Toggle owned status for current card"""
|
||||
@@ -419,15 +438,13 @@ class CardListFrame(ttk.Frame):
|
||||
owned = self.owned_var.get()
|
||||
level = int(self.level_var.get())
|
||||
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:
|
||||
self.on_stats_updated()
|
||||
|
||||
def update_level_buttons(self, rarity, max_level):
|
||||
"""Update quick level buttons based on rarity/max level"""
|
||||
# Determine valid levels
|
||||
"""Update quick level buttons"""
|
||||
if max_level == 50: # SSR
|
||||
self.valid_levels = [30, 35, 40, 45, 50]
|
||||
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}",
|
||||
command=lambda l=lvl: self.set_level(l),
|
||||
style_type='default')
|
||||
btn.config(width=5, padx=6, pady=3, font=FONT_SMALL)
|
||||
btn.pack(side=tk.LEFT, padx=2)
|
||||
btn.configure(width=45, height=36, font=FONT_BODY_BOLD)
|
||||
btn.pack(side=tk.LEFT, padx=3)
|
||||
self.level_buttons[lvl] = btn
|
||||
|
||||
def set_level(self, level):
|
||||
@@ -454,10 +471,9 @@ class CardListFrame(ttk.Frame):
|
||||
if self.current_card_id:
|
||||
self.selected_level = level
|
||||
self.level_var.set(level)
|
||||
self.level_label.config(text=str(level))
|
||||
self.level_label.configure(text=str(level))
|
||||
self.update_effects_display()
|
||||
|
||||
# Notify parent window about level change
|
||||
if self.on_card_selected:
|
||||
card = get_card_by_id(self.current_card_id)
|
||||
if card:
|
||||
@@ -466,41 +482,24 @@ class CardListFrame(ttk.Frame):
|
||||
# Save level if owned
|
||||
if self.current_card_id and self.owned_var.get():
|
||||
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):
|
||||
"""Increase level to next valid step"""
|
||||
current = self.level_var.get()
|
||||
# Find next level in valid_levels
|
||||
for lvl in self.valid_levels:
|
||||
if lvl > current:
|
||||
self.set_level(lvl)
|
||||
return
|
||||
|
||||
def decrement_level(self):
|
||||
"""Decrease level to previous valid step"""
|
||||
current = self.level_var.get()
|
||||
# Find previous level in valid_levels
|
||||
for lvl in reversed(self.valid_levels):
|
||||
if lvl < current:
|
||||
self.set_level(lvl)
|
||||
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):
|
||||
"""Update the effects display for current card and level"""
|
||||
if not self.current_card_id:
|
||||
@@ -509,21 +508,29 @@ class CardListFrame(ttk.Frame):
|
||||
level = int(self.level_var.get())
|
||||
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)
|
||||
|
||||
# Configure tags for styling
|
||||
self.effects_text.tag_configure('header', font=FONT_SUBHEADER, foreground=ACCENT_PRIMARY)
|
||||
self.effects_text.tag_configure('highlight', foreground=ACCENT_SUCCESS)
|
||||
self.effects_text.tag_configure('effect_name', foreground=TEXT_SECONDARY)
|
||||
self.effects_text.tag_configure('effect_value', foreground=TEXT_PRIMARY)
|
||||
self.effects_text.tag_configure('effect_tooltip', underline=False)
|
||||
# Note: CTkTextbox tags are minimal (no foreground color support per tag as detailed as tk usually)
|
||||
# But we can try basic insert.
|
||||
# CTkTextbox does not support color tags in the same way `tag_configure` does for Text.
|
||||
# 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.
|
||||
# However, `create_styled_text` in theme.py is now returning a CTkTextbox.
|
||||
# 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:
|
||||
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:
|
||||
# Highlight high values
|
||||
# Basic formatting
|
||||
prefix = ""
|
||||
# Logic for starring high values
|
||||
if '%' in str(value):
|
||||
try:
|
||||
num = int(str(value).replace('%', '').replace('+', ''))
|
||||
@@ -531,46 +538,10 @@ class CardListFrame(ttk.Frame):
|
||||
prefix = "★ "
|
||||
except:
|
||||
pass
|
||||
if prefix:
|
||||
self.effects_text.insert(tk.END, prefix, 'highlight')
|
||||
|
||||
# Insert effect name with tooltip tag
|
||||
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')
|
||||
self.effects_text.insert(tk.END, f"{prefix}{name}: {value}\n")
|
||||
else:
|
||||
self.effects_text.insert(tk.END, f"No effects data for Level {level}\n\n")
|
||||
self.effects_text.insert(tk.END, "Available levels: 1, 25, 40, 50\n", 'effect_name')
|
||||
self.effects_text.insert(tk.END, f"No effects data for Level {level}\n\nAvailable levels: {self.valid_levels}")
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user