Addition Of Files

This commit is contained in:
kiyreload27
2025-12-28 17:05:14 +00:00
commit 79fa803fa2
1047 changed files with 4783 additions and 0 deletions

559
gui/card_view.py Normal file
View File

@@ -0,0 +1,559 @@
"""
Card List View - Browse and search support cards with ownership management
"""
import tkinter as tk
from tkinter import ttk
import sys
import os
from PIL import Image, ImageTk
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from db.db_queries import get_all_cards, get_card_by_id, get_effects_at_level, set_card_owned, is_card_owned, update_owned_card_level
from utils import resolve_image_path
from gui.theme import (
BG_DARK, BG_MEDIUM, BG_LIGHT, BG_HIGHLIGHT,
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_SUCCESS,
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL, FONT_MONO,
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
)
class CardListFrame(ttk.Frame):
"""Frame containing card list with search/filter, ownership, and details panel"""
def __init__(self, parent, on_card_selected_callback=None):
super().__init__(parent)
self.on_card_selected = on_card_selected_callback
self.cards = []
self.current_card_id = None
self.card_image = None # Keep reference to prevent garbage collection
self.icon_cache = {} # Cache for list icons
# Create main layout
self.create_widgets()
self.load_cards()
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)
# Left panel - Card list with filters
left_frame = ttk.Frame(main_pane, width=420)
main_pane.add(left_frame, weight=1)
# Right panel - Card details
self.details_frame = ttk.Frame(main_pane)
main_pane.add(self.details_frame, weight=2)
# === Left Panel Contents ===
# Initialize filter variables FIRST (before search trace can trigger filter_cards)
self.rarity_var = tk.StringVar(value="All")
self.type_var = tk.StringVar(value="All")
self.owned_only_var = tk.BooleanVar(value=False)
# 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_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())
# Filter dropdowns
filter_frame = tk.Frame(left_frame, bg=BG_DARK)
filter_frame.pack(fill=tk.X, padx=10, 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())
# 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())
# 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
ttk.Button(filter_frame, text="Reset", command=self.reset_filters,
style='Small.TButton', width=7).pack(side=tk.LEFT, padx=5)
# Shortcuts
self.bind_all('<Control-f>', lambda e: self.search_entry.focus_set())
# 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)
# 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))
self.tree = ttk.Treeview(list_frame, columns=('owned', 'name', 'rarity', 'type'),
show='tree headings', selectmode='browse',
style="CardList.Treeview")
self.tree.heading('#0', text='')
self.tree.column('#0', width=45, anchor='center')
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.bind('<<TreeviewSelect>>', self.on_select)
# Tag for owned cards
self.tree.tag_configure('owned', background='#1a3a2e')
# === 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)
# Image area with card frame
image_frame = create_card_frame(details_container, padx=10, pady=10)
image_frame.pack(pady=10)
self.image_label = tk.Label(image_frame, text="", bg=BG_MEDIUM)
self.image_label.pack(padx=5, pady=5)
# 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_info = tk.Label(details_container, text="",
font=FONT_SMALL, bg=BG_DARK, fg=TEXT_MUTED)
self.detail_info.pack()
# Owned checkbox with emphasis
owned_frame = tk.Frame(details_container, bg=BG_DARK)
owned_frame.pack(pady=15)
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.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)
tk.Label(level_frame, text="Card Level:", font=FONT_BODY,
bg=BG_DARK, fg=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)
self.level_var = tk.IntVar(value=50)
self.max_level = 50
self.valid_levels = [30, 35, 40, 45, 50] # Default SSR
# 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)
# 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)
# Quick level buttons container
self.level_btn_frame = tk.Frame(level_frame, bg=BG_DARK)
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))
tk.Label(effects_header, text="📊 Effects at Current Level",
font=FONT_SUBHEADER, bg=BG_DARK, fg=TEXT_PRIMARY).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)
def load_cards(self):
"""Load all cards from database"""
self.cards = get_all_cards()
self.populate_tree(self.cards)
def reset_filters(self):
"""Reset all filters to default"""
self.search_var.set("")
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):
"""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
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)
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):
"""Populate treeview with cards"""
self.tree.delete(*self.tree.get_children())
for card in cards:
card_id, name, rarity, card_type, max_level, image_path, is_owned, owned_level = card
type_icon = get_type_icon(card_type)
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
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((32, 32), 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,))
self.count_label.config(text=f"{len(cards)} cards")
def on_select(self, event):
"""Handle card selection"""
selection = self.tree.selection()
if not selection:
return
card_id = int(selection[0])
card = get_card_by_id(card_id)
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
self.load_card_image(image_path)
# Use owned level if owned, otherwise max level or default 50
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))
# Update details display with colors
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}")
# 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)
def load_card_image(self, image_path):
"""Load and display card image"""
resolved_path = resolve_image_path(image_path)
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)
except Exception as e:
self.image_label.config(image='', text="[Image not found]")
else:
self.image_label.config(image='', text="")
def toggle_owned(self):
"""Toggle owned status for current card"""
if self.current_card_id:
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
def update_level_buttons(self, rarity, max_level):
"""Update quick level buttons based on rarity/max level"""
# Determine valid levels
if max_level == 50: # SSR
self.valid_levels = [30, 35, 40, 45, 50]
elif max_level == 45: # SR
self.valid_levels = [25, 30, 35, 40, 45]
else: # R (max 40)
self.valid_levels = [20, 25, 30, 35, 40]
# Clear existing buttons
for widget in self.level_btn_frame.winfo_children():
widget.destroy()
self.level_buttons = {}
# Create new buttons
for lvl in self.valid_levels:
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)
self.level_buttons[lvl] = btn
def set_level(self, level):
"""Set level from quick button"""
self.level_var.set(level)
self.level_label.config(text=str(level))
self.update_effects_display()
# 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)
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:
return
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.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)
if effects:
self.effects_text.insert(tk.END, f"━━━ Level {level} ━━━\n\n", 'header')
for name, value in effects:
# Highlight high values
prefix = ""
if '%' in str(value):
try:
num = int(str(value).replace('%', '').replace('+', ''))
if num >= 20:
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')
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.config(state=tk.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