Compare commits

...

8 Commits

33 changed files with 1495 additions and 1345 deletions

View File

@@ -1,14 +1,15 @@
# Umamusume Support Card Manager # Umamusume Support Card Manager
A tool for managing support cards and their effects in Umamusume (Granblue Fantasy Relink). A tool for managing support cards and their effects in Umamusume.
## Features ## Features
- Web scraping of support card data from GameTora - **Cards Management**: View and manage your support cards collection.
- Database storage of card information including effects at different levels - **Deck Builder**: Build and optimize decks with your owned cards.
- GUI application for viewing and managing support cards - **Effects Search**: Search for specific effects across your owned cards (e.g., "Friendship Bonus", "Skill Pt Bonus").
- Deck building functionality - **Web Scraping**: Integrated GameTora scraper to fetch the latest card data.
- Character art downloading - **Auto-Updater**: Automatically improved application updates.
- **Maintenance Scripts**: Suite of scripts for database repair and deep scraping.
## Project Structure ## Project Structure
@@ -23,8 +24,15 @@ A tool for managing support cards and their effects in Umamusume (Granblue Fanta
│ ├── db_init.py # Database initialization │ ├── db_init.py # Database initialization
│ └── db_queries.py # Database queries │ └── db_queries.py # Database queries
├── gui/ # GUI components ├── gui/ # GUI components
│ ├── main_window.py # Main application window
│ ├── card_view.py # Card list and details view
│ ├── deck_builder.py # Deck construction view
│ ├── effects_view.py # Effects search view
│ └── ...
├── updater/ # Update checking functionality ├── updater/ # Update checking functionality
├── database/ # Database files ├── maintenance_scripts/ # Database repair and utility scripts
├── config/ # Configuration files
├── database/ # Database files storage
├── images/ # Character art images ├── images/ # Character art images
├── build/ # Build artifacts ├── build/ # Build artifacts
└── dist/ # Distribution files └── dist/ # Distribution files
@@ -32,9 +40,9 @@ A tool for managing support cards and their effects in Umamusume (Granblue Fanta
## Installation ## Installation
1. Clone the repository 1. Clone the repository.
2. Install dependencies: `pip install -r requirements.txt` 2. Install dependencies: `pip install -r requirements.txt`.
3. Run the application: `python main.py` 3. Run the application: `python main.py`.
## Usage ## Usage
@@ -44,6 +52,7 @@ python main.py
``` ```
### Scraping Mode ### Scraping Mode
To manually run the scraper:
```bash ```bash
python main.py --scrape python main.py --scrape
``` ```
@@ -51,17 +60,17 @@ python main.py --scrape
## Development ## Development
### Code Structure ### Code Structure
- `main.py`: Entry point and argument parsing - `main.py`: Entry point and argument parsing.
- `scraper/gametora_scraper.py`: Web scraping logic - `gui/`: Contains all CustomTkinter-based UI components.
- `db/db_init.py`: Database schema initialization - `db/`: Handles SQLite database interactions.
- `gui/`: GUI components (MainWindow, views, etc.) - `scraper/`: Logic for fetching data from GameTora.
- `updater/update_checker.py`: Update checking functionality - `maintenance_scripts/`: Tools for fixing database inconsistencies or re-fetching data.
### Contributing ### Contributing
1. Fork the repository 1. Fork the repository.
2. Create a feature branch 2. Create a feature branch.
3. Make your changes 3. Make your changes.
4. Submit a pull request 4. Submit a pull request.
## License ## License
MIT MIT

Binary file not shown.

View File

@@ -108,8 +108,44 @@ def run_migrations():
pass # Column already exists pass # Column already exists
conn.commit() conn.commit()
repair_image_paths(conn)
conn.close() conn.close()
def repair_image_paths(conn):
"""Attempt to populate missing image_path for existing cards in old databases"""
print("Checking for missing image paths to repair...")
cur = conn.cursor()
# Find cards with missing image paths but have a URL
cur.execute("SELECT card_id, name, gametora_url FROM support_cards WHERE image_path IS NULL OR image_path = ''")
to_repair = cur.fetchall()
if not to_repair:
return
import re
repaired_count = 0
for card_id, name, url in to_repair:
if not url: continue
# Extract ID from URL (e.g., 30154 from .../supports/30154-mejiro-ramonu)
match = re.search(r'/supports/(\d+)-', url)
if match:
stable_id = match.group(1)
# Create safe filename matching scraper logic
safe_name = re.sub(r'[<>:"/\\\\|?*]', '_', name)
filename = f"{stable_id}_{safe_name}.png"
# Update DB with images/filename
cur.execute("UPDATE support_cards SET image_path = ? WHERE card_id = ?",
(f"images/{filename}", card_id))
repaired_count += 1
if repaired_count > 0:
conn.commit()
print(f"Successfully repaired {repaired_count} image paths!")
def check_for_updates(): def check_for_updates():
"""Check if database version matches app version, sync if outdated""" """Check if database version matches app version, sync if outdated"""
if getattr(sys, 'frozen', False): if getattr(sys, 'frozen', False):
@@ -486,6 +522,49 @@ def get_unique_effect_names(card_id):
conn.close() conn.close()
return rows return rows
def search_owned_effects(search_term):
"""
Search for effects among owned cards.
Returns list of (card_id, card_name, image_path, effect_name, effect_value, level)
"""
conn = get_conn()
cur = conn.cursor()
# We need to join support_effects with owned_cards to get the level
# But wait, owned_cards has a level column. support_effects stores effects for specific levels.
# So we need to match support_effects.level with owned_cards.level
# OR find the effect for the closest level <= owned level (if effects aren't stored for every single level)
# The current DB schema seems to store effects for specific levels (1, 5, 10, ...).
# If a card is level 49, `get_effects_at_level` usually queries for exact level match.
# Let's check `get_effects_at_level` implementation: "WHERE card_id = ? AND level = ?"
# So if I have a card at level 49, and effects are only defined at 45 and 50, query for 49 returns nothing?
# That would be a bug or assumption in the current app.
# Let's look at `update_progression_table` in `effects_view.py`. It does some "nearest level" logic.
# For this search feature, to be robust, we should probably fetch ALL effects for the card
# and filter for the one active at the owned level.
# OR, assuming the scraper/DB populates "current" effects effectively.
# Actually, the most robust way in SQL for "value at level X" given sparse data is complex.
# However, let's assume for now we want exact matches or we'll handle the "effective level" logic in Python?
# No, that's too slow for search.
# Let's look at how `get_effects_at_level` is used.
# It is used in `update_current_effects` with `self.level_var.get()`.
# It expects an exact match.
# So we should probably join on `oc.level`.
query = """
SELECT sc.card_id, sc.name, sc.image_path, se.effect_name, se.effect_value, oc.level
FROM owned_cards oc
JOIN support_cards sc ON oc.card_id = sc.card_id
JOIN support_effects se ON oc.card_id = se.card_id AND oc.level = se.level
WHERE se.effect_name LIKE ?
ORDER BY sc.name
"""
cur.execute(query, (f"%{search_term}%",))
rows = cur.fetchall()
conn.close()
return rows
# ============================================ # ============================================
# Hint Queries # Hint Queries
# ============================================ # ============================================

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(
filter_frame,
variable=self.type_var,
values=["All", "Speed", "Stamina", "Power", "Guts", "Wisdom", "Friend", "Group"], values=["All", "Speed", "Stamina", "Power", "Guts", "Wisdom", "Friend", "Group"],
width=10, state='readonly') width=100,
type_combo.pack(side=tk.LEFT, padx=5) height=32,
type_combo.bind('<<ComboboxSelected>>', lambda e: self.filter_cards()) 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(
owned_frame,
text="✨ I Own This Card",
variable=self.owned_var, variable=self.owned_var,
command=self.toggle_owned, command=self.toggle_owned,
style='Large.TCheckbutton') 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='', self.tree.insert('', tk.END, iid=card_id, text='',
values=(owned_mark, display_name, rarity, f"{type_icon} {card_type}"), values=(owned_mark, display_name, rarity, f"{type_icon} {card_type}"),
tags=(tag,)) 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): 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,158 +41,165 @@ 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, # Initial placeholder
font=('Segoe UI', 32)) self.image_label = ctk.CTkLabel(self, fg_color="transparent", text="📭", text_color=TEXT_MUTED,
self.image_label.grid(row=0, column=0, rowspan=3, padx=12, pady=12) font=('Segoe UI', 32), width=90, height=90)
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.config(image='', text="📭", font=('Segoe UI', 32)) # Recreate label to avoid TclError with missing images
self.config(highlightbackground=BG_LIGHT) if hasattr(self, 'image_label') and self.image_label:
self.image_label.destroy()
self.image_label = ctk.CTkLabel(self, fg_color="transparent", text="📭", text_color=TEXT_MUTED,
font=('Segoe UI', 32), width=90, height=90)
self.image_label.grid(row=0, column=0, padx=5, pady=(5,0))
self.configure(border_color=BG_LIGHT)
self.image_ref = None self.image_ref = None
self.toggle_controls(False) self.toggle_controls(False)
def _load_image(self, path): def _load_image(self, path):
resolved_path = resolve_image_path(path) resolved_path = resolve_image_path(path)
# Prepare new image first
new_image = None
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) new_image = ctk.CTkImage(light_image=pil_img, dark_image=pil_img, size=(90, 90))
self.image_ref = ImageTk.PhotoImage(pil_img) except Exception:
self.image_label.config(image=self.image_ref, text='') pass
except Exception as e:
print(f"Failed to load image: {e}") # Recreate label
self.image_label.config(image='', text="⚠️") if hasattr(self, 'image_label') and self.image_label:
self.image_label.destroy()
if new_image:
self.image_ref = new_image # Keep ref
self.image_label = ctk.CTkLabel(self, fg_color="transparent", text="", image=new_image)
else: else:
self.image_label.config(image='', text="🖼️") self.image_ref = None
self.image_label = ctk.CTkLabel(self, fg_color="transparent", text="⚠️" if resolved_path else "🖼️",
text_color=TEXT_MUTED, font=('Segoe UI', 32), width=90, height=90)
def _on_level_change(self, event): self.image_label.grid(row=0, column=0, padx=5, pady=(5,0))
self.level_callback(self.index, int(self.level_var.get()))
def _on_level_change(self, value):
# 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 +207,31 @@ 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 # Add Button (Packed first to stick to bottom)
list_frame = tk.Frame(left_panel, bg=BG_DARK) add_btn = create_styled_button(left_panel, text=" Add to Deck",
list_frame.pack(fill=tk.BOTH, expand=True) command=self.add_selected_to_deck,
style_type='accent')
add_btn.pack(side=tk.BOTTOM, fill=tk.X, pady=10, padx=10)
self.card_tree = ttk.Treeview(list_frame, columns=('name', 'rarity', 'type'), # Card List Treeview
list_container = ctk.CTkFrame(left_panel, fg_color=BG_MEDIUM)
list_container.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
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,69 +243,67 @@ 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())
# Add Button
add_btn = create_styled_button(left_panel, text=" Add to Deck",
command=self.add_selected_to_deck,
style_type='accent')
add_btn.pack(fill=tk.X, pady=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 +314,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 +346,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 +371,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 +388,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 +438,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 +465,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 +486,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 +499,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 +523,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 +536,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,298 +1,180 @@
""" """
Effects View - Display support effects at all levels with interactive slider 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
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__))))
from db.db_queries import get_all_effects, get_effects_at_level, get_unique_effect_names, get_card_by_id from db.db_queries import search_owned_effects
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_TERTIARY, ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_SUCCESS, 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_MONO, FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL,
create_styled_button, create_styled_text, create_card_frame, create_styled_button, create_styled_entry
EFFECT_DESCRIPTIONS
) )
from utils import resolve_image_path
class EffectsFrame(ctk.CTkFrame):
class EffectsFrame(ttk.Frame): """Frame for searching effects across owned cards"""
"""Frame for viewing support effects at different levels"""
def __init__(self, parent): def __init__(self, parent):
super().__init__(parent) super().__init__(parent, fg_color="transparent")
self.current_card_id = None self.icon_cache = {}
self.current_card_name = None
self.max_level = 50
self.create_widgets() self.create_widgets()
def create_widgets(self): def create_widgets(self):
"""Create the effects view interface""" """Create the effects search interface"""
# Header # 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)
self.card_label = tk.Label(header_frame, text="📊 Select a card from the Card List tab", # Search container
font=FONT_HEADER, bg=BG_DARK, fg=ACCENT_PRIMARY) search_container = ctk.CTkFrame(header_frame, fg_color="transparent")
self.card_label.pack(side=tk.LEFT) search_container.pack(fill=tk.X)
# Legend Button ctk.CTkLabel(search_container, text="🔍 Search Effect:",
legend_btn = create_styled_button(header_frame, text="❓ Legend", font=FONT_HEADER, text_color=TEXT_PRIMARY).pack(side=tk.LEFT, padx=(0, 10))
command=self.show_legend, style_type='default')
legend_btn.config(font=FONT_SMALL, padx=10, pady=4)
legend_btn.pack(side=tk.RIGHT)
# Level control frame self.search_var = tk.StringVar()
control_frame = tk.Frame(self, bg=BG_MEDIUM, padx=15, pady=12) self.search_entry = ctk.CTkEntry(search_container, textvariable=self.search_var, width=300)
control_frame.pack(fill=tk.X, padx=20) 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())
# Level label search_btn = create_styled_button(search_container, text="Search",
tk.Label(control_frame, text="Level:", font=FONT_BODY, command=self.perform_search, style_type='accent')
bg=BG_MEDIUM, fg=TEXT_SECONDARY).pack(side=tk.LEFT) search_btn.pack(side=tk.LEFT)
# Level display with increment/decrement buttons # Example/Help text
level_ctrl = tk.Frame(control_frame, bg=BG_MEDIUM) help_frame = ctk.CTkFrame(header_frame, fg_color="transparent")
level_ctrl.pack(side=tk.LEFT, padx=15) help_frame.pack(fill=tk.X, pady=(5, 0))
ctk.CTkLabel(help_frame, text="Examples: Friendship, Motivation, Race Bonus, Skill Pt",
font=FONT_SMALL, text_color=TEXT_MUTED).pack(side=tk.LEFT)
# Decrement button # Results Area
dec_btn = tk.Button(level_ctrl, text="", font=FONT_HEADER, results_container = ctk.CTkFrame(self, fg_color="transparent")
bg=BG_LIGHT, fg=TEXT_PRIMARY, bd=0, width=2, results_container.pack(fill=tk.BOTH, expand=True, padx=20, pady=(0, 20))
activebackground=BG_HIGHLIGHT, cursor='hand2',
command=self.decrement_level)
dec_btn.pack(side=tk.LEFT)
self.level_var = tk.IntVar(value=50) # Label for the frame
self.level_display = tk.Label(level_ctrl, text="50", width=4, font=FONT_HEADER, ctk.CTkLabel(results_container, text="Search Results (Owned Cards)",
bg=BG_LIGHT, fg=ACCENT_PRIMARY, padx=10) font=FONT_SUBHEADER, text_color=ACCENT_PRIMARY).pack(pady=(10, 5))
self.level_display.pack(side=tk.LEFT, padx=2)
# Increment button # Treeview Container
inc_btn = tk.Button(level_ctrl, text="+", font=FONT_HEADER, tree_frame = ctk.CTkFrame(results_container, fg_color="transparent")
bg=BG_LIGHT, fg=TEXT_PRIMARY, bd=0, width=2, tree_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
activebackground=BG_HIGHLIGHT, cursor='hand2',
command=self.increment_level)
inc_btn.pack(side=tk.LEFT)
# Quick level buttons # Treeview - ADDING IMAGE COLUMN
button_frame = tk.Frame(control_frame, bg=BG_MEDIUM) # Note: Treeview column #0 is the tree column where icons live.
button_frame.pack(side=tk.LEFT, padx=25) # 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")
quick_levels = [25, 30, 35, 40, 45, 50] self.tree.heading('#0', text='Image')
for lvl in quick_levels: self.tree.column('#0', width=100, anchor='center')
btn = create_styled_button(button_frame, text=f"Lv{lvl}",
command=lambda l=lvl: self.set_level(l),
style_type='default')
btn.config(width=5, font=FONT_SMALL, padx=6, pady=3)
btn.pack(side=tk.LEFT, padx=3)
# Main content area self.tree.heading('card_name', text='Card Name', anchor='w')
content_frame = tk.Frame(self, bg=BG_DARK) self.tree.heading('level', text='Level', anchor='center')
content_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=15) self.tree.heading('current_value', text='Value', anchor='center')
self.tree.heading('effect_name', text='Effect Name', anchor='w')
# Left: Current level effects self.tree.column('card_name', width=200)
left_frame = ttk.LabelFrame(content_frame, text=" Current Level Effects ", padding=12) self.tree.column('level', width=60, anchor='center')
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 10)) self.tree.column('current_value', width=80, anchor='center')
self.tree.column('effect_name', width=200)
self.current_effects = create_styled_text(left_frame, height=15) scrollbar = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=self.tree.yview)
self.current_effects.pack(fill=tk.BOTH, expand=True) # ... (rest of scrollbar setup) ...
self.current_effects.config(state=tk.DISABLED) self.tree.configure(yscrollcommand=scrollbar.set)
# Right: Effect progression table self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
right_frame = ttk.LabelFrame(content_frame, text=" Effect Progression ", padding=12)
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
# Treeview for effect table
columns = ('effect', 'lv1', 'lv25', 'lv40', 'lv50')
self.effects_tree = ttk.Treeview(right_frame, columns=columns, show='headings', height=12)
self.effects_tree.heading('effect', text='Effect', anchor='w')
self.effects_tree.column('effect', width=140, minwidth=120)
for col in columns[1:]:
level = col.replace('lv', 'Lv ')
self.effects_tree.heading(col, text=level)
self.effects_tree.column(col, width=65, anchor='center')
scrollbar = ttk.Scrollbar(right_frame, orient=tk.VERTICAL, command=self.effects_tree.yview)
self.effects_tree.configure(yscrollcommand=scrollbar.set)
self.effects_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)
def show_legend(self): # Status Label
"""Show effect explanations""" self.status_label = ctk.CTkLabel(results_container, text="", font=FONT_SMALL, text_color=TEXT_SECONDARY)
legend = { self.status_label.pack(side=tk.BOTTOM, fill=tk.X, pady=(5, 10))
"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."
}
text = "📖 Effect Explanations:\n\n" def parse_value(self, value_str):
for name, desc in legend.items(): """Parse effect value string to float for sorting"""
text += f"{name}:\n {desc}\n\n" try:
clean = re.sub(r'[^\d.-]', '', str(value_str))
return float(clean)
except:
return -999999.0
messagebox.showinfo("Effect Legend", text) def perform_search(self):
"""Execute search and update results"""
def set_card(self, card_id): term = self.search_var.get().strip()
"""Load a card's effects""" if not term:
self.current_card_id = card_id messagebox.showwarning("Search", "Please enter a search term")
# Get card info for max level
card = get_card_by_id(card_id)
if card:
self.current_card_name = card[1]
self.max_level = card[4]
if self.level_var.get() > self.max_level:
self.level_var.set(self.max_level)
self.level_display.config(text=str(self.max_level))
self.card_label.config(text=f"📊 {self.current_card_name}")
# Update displays
self.update_current_effects()
self.update_progression_table()
def set_level(self, level):
"""Set level from quick button"""
if level <= self.max_level:
self.level_var.set(level)
self.level_display.config(text=str(level))
self.update_current_effects()
def increment_level(self):
"""Increase level by 1"""
current = self.level_var.get()
if current < self.max_level:
self.set_level(current + 1)
def decrement_level(self):
"""Decrease level by 1"""
current = self.level_var.get()
if current > 1:
self.set_level(current - 1)
def update_current_effects(self):
"""Update the current level effects display"""
self.current_effects.config(state=tk.NORMAL)
self.current_effects.delete('1.0', tk.END)
# Configure tags
self.current_effects.tag_configure('header', font=FONT_SUBHEADER, foreground=ACCENT_PRIMARY)
self.current_effects.tag_configure('highlight', foreground=ACCENT_SUCCESS)
self.current_effects.tag_configure('effect_name', foreground=TEXT_SECONDARY)
self.current_effects.tag_configure('effect_value', foreground=TEXT_PRIMARY, font=FONT_BODY_BOLD)
self.current_effects.tag_configure('effect_tooltip', underline=False)
if not self.current_card_id:
self.current_effects.insert(tk.END, "No card selected\n\n", 'effect_name')
self.current_effects.insert(tk.END, "Select a card from the Card List tab to view its effects.", 'effect_name')
self.current_effects.config(state=tk.DISABLED)
return return
level = self.level_var.get() # clear current
effects = get_effects_at_level(self.current_card_id, level) for item in self.tree.get_children():
self.tree.delete(item)
self.current_effects.insert(tk.END, f"━━━ Level {level} ━━━\n\n", 'header') # Query DB
results = search_owned_effects(term)
if effects: if not results:
for name, value in effects: self.status_label.configure(text="No matching effects found among owned cards.")
# Highlight high values return
prefix = ""
if '%' in str(value): # Process and Sort
# Row: (card_id, card_name, image_path, effect_name, effect_value, level)
processed_results = []
for r in results:
val_num = self.parse_value(r[4])
processed_results.append({
'data': r,
'sort_val': val_num
})
# Sort by value descending
processed_results.sort(key=lambda x: x['sort_val'], reverse=True)
# Populate Tree
for item in processed_results:
r = item['data']
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: try:
num = int(str(value).replace('%', '').replace('+', '')) pil_img = Image.open(resolved_path)
if num >= 20: # Match CardList size
prefix = "" pil_img.thumbnail((78, 78), Image.Resampling.LANCZOS)
img = ImageTk.PhotoImage(pil_img)
self.icon_cache[card_id] = img
except: except:
pass pass
if prefix: kv = {'image': img} if img else {}
self.current_effects.insert(tk.END, prefix, 'highlight')
# Insert effect name with tooltip tag # Insert into tree
tag_name = f"tooltip_{name.replace(' ', '_')}" # #0 = Image (Text '')
self.current_effects.insert(tk.END, f"{name}: ", ('effect_name', tag_name)) # Cols = Name, Level, Value, Effect
values = (r[1], f"Lv {r[5]}", r[4], r[3])
# Bind tooltip events self.tree.insert('', tk.END, text='', values=values, **kv)
self.current_effects.tag_bind(tag_name, "<Enter>", lambda e, n=name: self.show_effect_tooltip(e, n))
self.current_effects.tag_bind(tag_name, "<Leave>", self.hide_effect_tooltip)
self.current_effects.insert(tk.END, f"{value}\n", 'effect_value') self.status_label.configure(text=f"Found {len(processed_results)} owned cards with matching effects.")
else:
self.current_effects.insert(tk.END, "No effect data available for this level.\n\n", 'effect_name')
self.current_effects.insert(tk.END, "Try selecting: Lv 1, 25, 40, or 50", 'effect_name')
self.current_effects.config(state=tk.DISABLED) # Compatibility methods for main_window integration
def set_card(self, card_id):
pass
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
def update_progression_table(self):
"""Update the effect progression table"""
self.effects_tree.delete(*self.effects_tree.get_children())
if not self.current_card_id:
return
# Get all effects
all_effects = get_all_effects(self.current_card_id)
# Organize by effect name
effect_by_level = {}
for level, effect_name, effect_value in all_effects:
if effect_name not in effect_by_level:
effect_by_level[effect_name] = {}
effect_by_level[effect_name][level] = effect_value
# Key levels for the table
key_levels = [1, 25, 40, 50]
# Add rows
for effect_name, levels in sorted(effect_by_level.items()):
row = [effect_name]
for lvl in key_levels:
# Find closest level we have data for
value = levels.get(lvl, '')
if not value:
# Try to find nearest
for l in sorted(levels.keys()):
if l <= lvl:
value = levels[l]
row.append(value)
self.effects_tree.insert('', tk.END, values=row)

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, status_frame,
text="🔍 Diagnostics", text="VibeCoded by Kiyreload │ ",
font=FONT_SMALL, font=FONT_SMALL,
bg=BG_MEDIUM, text_color=ACCENT_TERTIARY
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,
text="Made by Kiyreload │ ",
font=FONT_SMALL,
bg=BG_MEDIUM,
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)])
@@ -256,8 +568,29 @@ def configure_styles(root: tk.Tk):
# WIDGET HELPER FUNCTIONS # WIDGET HELPER FUNCTIONS
# ═══════════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════════
def create_styled_entry(parent, textvariable=None, **kwargs):
"""Create a styled ctk.CTkEntry with modern appearance"""
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,
textvariable=textvariable,
font=FONT_BODY,
fg_color=bg_color,
text_color=fg_color,
border_width=1,
corner_radius=6,
**safe_kwargs
)
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,
@@ -275,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)

View File

@@ -1,7 +1,7 @@
import sqlite3 import sqlite3
import os import os
DB_PATH = os.path.join("database", "umamusume.db") DB_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "database", "umamusume.db")
def check_schema(): def check_schema():
if not os.path.exists(DB_PATH): if not os.path.exists(DB_PATH):

View File

@@ -2,7 +2,7 @@ import sqlite3
import os import os
def debug_db(): def debug_db():
db_path = "database/umamusume.db" db_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "database", "umamusume.db")
if not os.path.exists(db_path): if not os.path.exists(db_path):
print(f"Database not found at {db_path}") print(f"Database not found at {db_path}")
return return

View File

@@ -3,7 +3,7 @@ import sys
from playwright.sync_api import sync_playwright from playwright.sync_api import sync_playwright
# Add parent dir to path # Add parent dir to path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
def debug_kitasan_scrape(): def debug_kitasan_scrape():
url = "https://gametora.com/umamusume/supports/30028-kitasan-black" url = "https://gametora.com/umamusume/supports/30028-kitasan-black"

View File

@@ -3,7 +3,7 @@ import os
import sys import sys
# Ensure we can import from the project # Ensure we can import from the project
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from scraper.gametora_scraper import scrape_support_card, sync_playwright from scraper.gametora_scraper import scrape_support_card, sync_playwright
from db.db_queries import DB_PATH, repair_orphaned_data, cleanup_orphaned_data from db.db_queries import DB_PATH, repair_orphaned_data, cleanup_orphaned_data

View File

@@ -3,7 +3,7 @@ import os
import sys import sys
# Add parent dir to path # Add parent dir to path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from scraper.gametora_scraper import scrape_support_card, sync_playwright from scraper.gametora_scraper import scrape_support_card, sync_playwright

View File

@@ -3,7 +3,7 @@ import os
import sys import sys
# Add parent dir to path # Add parent dir to path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from scraper.gametora_scraper import scrape_support_card, sync_playwright from scraper.gametora_scraper import scrape_support_card, sync_playwright

View File

@@ -3,7 +3,7 @@ import os
import sys import sys
# Add parent dir to path # Add parent dir to path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from scraper.gametora_scraper import scrape_support_card, sync_playwright from scraper.gametora_scraper import scrape_support_card, sync_playwright
from db.db_queries import get_conn from db.db_queries import get_conn

View File

@@ -5,7 +5,7 @@ from playwright.sync_api import sync_playwright
import sys import sys
# Add project root to path # Add project root to path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
def test_scrape_events(url): def test_scrape_events(url):
with sync_playwright() as p: with sync_playwright() as p:

View File

@@ -549,242 +549,200 @@ def scrape_hints(page, card_id, cur):
print(f" Found {len(hints)} hints") print(f" Found {len(hints)} hints")
def scrape_events(page, card_id, cur): def scrape_events(page, card_id, cur):
"""Scrape the LAST chain event (Golden Perk) with OR options""" """Scrape all events including Chain, Dates, Random and Special"""
# Use a flag to avoid adding multiple console listeners # Use a flag to avoid adding multiple console listeners
if not hasattr(page, "_console_attached"): if not hasattr(page, "_console_attached"):
page.on("console", lambda msg: print(f" [JS Console] {msg.text}") if "scrapping" not in msg.text.lower() else None) page.on("console", lambda msg: print(f" [JS Console] {msg.text}") if "scrapping" not in msg.text.lower() else None)
page._console_attached = True page._console_attached = True
# 1. First, build a map of skills from the 'Skills from events' summary section # 1. Build a map of skills from the 'Skills from events' summary section
# This helps us identify which skills are Rare (Gold) # This remains useful for identifying golden skills.
skill_rarity_map = page.evaluate(""" skill_rarity_map = page.evaluate("""
() => { () => {
const map = {}; const map = {};
console.log("Building Skill Rarity Map...");
// 1. Find all skill containers. They usually have a name and a 'Details' button.
// In the "Skills from events" or "Support hints" sections.
const containers = Array.from(document.querySelectorAll('div')).filter(d => const containers = Array.from(document.querySelectorAll('div')).filter(d =>
(d.innerText.includes('Details') || d.innerText.includes('Reward')) && d.innerText.length < 500 (d.innerText.includes('Details') || d.innerText.includes('Reward')) && d.innerText.length < 500
); );
containers.forEach(c => { containers.forEach(c => {
// Try to extract the skill name. It's usually the first text node or a bold tag.
const nameNode = c.querySelector('b, span[font-weight="bold"], div[font-weight="bold"]'); const nameNode = c.querySelector('b, span[font-weight="bold"], div[font-weight="bold"]');
let name = ""; let name = nameNode ? nameNode.innerText.trim() : c.innerText.split('Details')[0].replace(/\\n/g, ' ').trim();
if (nameNode) {
name = nameNode.innerText.trim();
} else {
// Fallback to text before 'Details'
name = c.innerText.split('Details')[0].replace(/\\n/g, ' ').trim();
}
if (name && name.length > 2) { if (name && name.length > 2) {
const style = window.getComputedStyle(c); const style = window.getComputedStyle(c);
const nameStyle = nameNode ? window.getComputedStyle(nameNode) : null; const nameStyle = nameNode ? window.getComputedStyle(nameNode) : null;
// Golden skills have a specific background
const isGold = style.backgroundImage.includes('linear-gradient') || const isGold = style.backgroundImage.includes('linear-gradient') ||
style.backgroundColor.includes('rgb(255, 193, 7)') || style.backgroundColor.includes('rgb(255, 193, 7)') ||
(nameStyle && nameStyle.color === 'rgb(255, 193, 7)') || (nameStyle && nameStyle.color === 'rgb(255, 193, 7)') ||
c.className.includes('kkspcu') || c.className.includes('kkspcu');
c.innerHTML.includes('kkspcu');
const normalized = name.toLowerCase().replace(/\\s+/g, ' ').replace(/[()-]/g, '').trim(); const normalized = name.toLowerCase().replace(/\\s+/g, ' ').replace(/[()-]/g, '').trim();
map[normalized] = isGold; map[normalized] = isGold;
console.log(`Mapped Skill: "${name}" [${normalized}] -> Gold: ${isGold}`);
} }
}); });
return map; return map;
} }
""") """)
# Scroll to the Events section specifically # 2. Scrape all event types
print(" Ensuring events are loaded...") print(" Scraping all event categories...")
page.evaluate("() => { const h = Array.from(document.querySelectorAll('h2, h1, div')).find(el => el.innerText.toLowerCase().includes('training events')); if (h) h.scrollIntoView(); }") all_events_data = page.evaluate("""
page.wait_for_timeout(1000)
# 2. Scrape ONLY the LAST chain event (Golden Perk) with OR options
golden_perk_data = page.evaluate("""
async () => { async () => {
console.log("Scraping Golden Perk (last chain event)..."); const results = [];
// Find all chain event buttons // Define categories to look for
const getChainEventButtons = () => { const categories = [
const buttons = []; { label: 'Chain Events', type: 'Chain' },
// Look for "Chain Events" text (case-insensitive substring) { label: 'Dates', type: 'Date' },
const labels = Array.from(document.querySelectorAll('div, span, h2, h3, h4')).filter(el => { label: 'Random Events', type: 'Random' },
el.innerText.toLowerCase().includes('chain events') && el.innerText.trim().length < 20 { label: 'Special Events', type: 'Special' }
];
for (const cat of categories) {
// Find category headers
const headers = Array.from(document.querySelectorAll('div, span, h2, h3, h4')).filter(el =>
el.innerText.trim() === cat.label && el.children.length === 0
); );
labels.forEach(label => { for (const header of headers) {
// The buttons are usually in the same container or next container // Find buttons in the following siblings or parent siblings
let container = label.parentElement; let container = header.parentElement;
let foundButtons = [];
let attempts = 0; let attempts = 0;
while (container && container.querySelectorAll('button').length === 0 && attempts < 5) {
container = container.nextElementSibling || container.parentElement;
attempts++;
if (container && container.tagName === 'BODY') break;
}
if (container) { while (container && foundButtons.length === 0 && attempts < 5) {
const btns = Array.from(container.querySelectorAll('button')); foundButtons = Array.from(container.querySelectorAll('button')).filter(btn => {
btns.forEach(btn => {
const text = btn.innerText.trim();
const style = window.getComputedStyle(btn); const style = window.getComputedStyle(btn);
const isVisible = style.display !== 'none' && style.visibility !== 'hidden'; return style.display !== 'none' && style.visibility !== 'hidden';
// Look for arrows (regular or heavy)
if (isVisible && (text.includes('>') || text.includes(''))) {
buttons.push(btn);
}
}); });
}
});
return buttons;
};
const buttons = getChainEventButtons(); if (foundButtons.length === 0) {
console.log(`Found ${buttons.length} chain event buttons`); // Check next siblings of the header's ancestors
let sibling = header;
if (buttons.length === 0) { let parent = header.parentElement;
return null; while(parent && parent.tagName !== 'BODY') {
} if (parent.innerText.includes(cat.label)) {
const next = parent.nextElementSibling;
let goldenPerkButton = null; if (next) {
let maxArrows = 0; const nextBtns = Array.from(next.querySelectorAll('button'));
if (nextBtns.length > 0) {
for (const btn of buttons) { foundButtons = nextBtns;
const text = btn.innerText.trim();
// Count both regular and heavy arrows
const arrowCount = (text.match(/>|/g) || []).length;
// If it has three heavy arrows, it's almost certainly the golden perk
if (text.includes('')) {
goldenPerkButton = btn;
break; break;
} }
if (arrowCount > maxArrows) {
maxArrows = arrowCount;
goldenPerkButton = btn;
} }
} }
parent = parent.parentElement;
if (!goldenPerkButton) { }
console.log("No golden perk button found"); }
return null; container = container.parentElement;
attempts++;
} }
const eventName = goldenPerkButton.innerText.trim(); // Scrape each button in the category
console.log(`Found Golden Perk: ${eventName} (${maxArrows} arrows)`); for (const btn of foundButtons) {
const eventName = btn.innerText.trim();
if (!eventName || results.some(r => r.name === eventName)) continue;
// Count arrows for chain/date importance
const arrows = (eventName.match(/>|/g) || []).length;
try { try {
// Click to open popover btn.scrollIntoViewIfNeeded ? btn.scrollIntoViewIfNeeded() : null;
goldenPerkButton.scrollIntoViewIfNeeded ? goldenPerkButton.scrollIntoViewIfNeeded() : null;
await new Promise(r => setTimeout(r, 100)); await new Promise(r => setTimeout(r, 100));
goldenPerkButton.click(); btn.click();
await new Promise(r => setTimeout(r, 600)); await new Promise(r => setTimeout(r, 500));
// Find popover
const popovers = Array.from(document.querySelectorAll('div')).filter(d => const popovers = Array.from(document.querySelectorAll('div')).filter(d =>
d.innerText.includes(eventName) && d.innerText.includes(eventName) &&
window.getComputedStyle(d).zIndex > 50 && window.getComputedStyle(d).zIndex > 50 &&
d.innerText.length < 2500 d.innerText.length < 2500
); );
if (popovers.length === 0) { if (popovers.length > 0) {
console.log(`Popover NOT found for ${eventName}`);
document.body.click();
return { name: eventName, type: 'Chain', skills: [] };
}
const pop = popovers[popovers.length - 1]; const pop = popovers[popovers.length - 1];
console.log(`Found popover for ${eventName}`);
// Check for OR structure - look for "Randomly either" or "or" divider
const hasOrDivider = pop.querySelector('[class*="divider_or"]') !== null || const hasOrDivider = pop.querySelector('[class*="divider_or"]') !== null ||
pop.innerText.includes('Randomly either') || pop.innerText.includes('Randomly either') ||
pop.innerText.toLowerCase().includes(' or '); pop.innerText.toLowerCase().includes(' or ');
// Find all skill names (purple/blue links)
const skillLinks = Array.from(pop.querySelectorAll('span, a')).filter(el => const skillLinks = Array.from(pop.querySelectorAll('span, a')).filter(el =>
el.innerText.length > 2 && el.innerText.length > 2 &&
!el.innerText.includes('Energy') && !el.innerText.includes('Energy') &&
!el.innerText.includes('bond') &&
(window.getComputedStyle(el).color === 'rgb(102, 107, 255)' || (window.getComputedStyle(el).color === 'rgb(102, 107, 255)' ||
el.className.includes('linkcolor')) el.className.includes('linkcolor'))
); );
console.log(`Found ${skillLinks.length} potential skills in popover`);
const skills = []; const skills = [];
skillLinks.forEach(link => { skillLinks.forEach(link => {
const skillName = link.innerText.trim(); const sName = link.innerText.trim();
if (skillName && skillName.length > 2 && !skills.some(s => s.name === skillName)) { if (sName && !skills.some(s => s.name === sName)) {
// If there's an OR divider, all skills in this popover are part of OR groups skills.push({ name: sName, is_or: hasOrDivider });
const isOr = hasOrDivider;
skills.push({ name: skillName, is_or: isOr });
} }
}); });
// Close popover results.push({
document.body.click(); name: eventName,
await new Promise(r => setTimeout(r, 200)); type: cat.type,
skills: skills,
return { name: eventName, type: 'Chain', skills: skills }; arrows: arrows
});
} catch (err) {
console.log(`Error clicking ${eventName}: ${err.message}`);
return { name: eventName, type: 'Chain', skills: [] };
} }
document.body.click();
await new Promise(r => setTimeout(r, 100));
} catch (err) {
console.log(`Failed to scrape event ${eventName}: ${err.message}`);
}
}
}
}
return results;
} }
""") """)
# 3. Store ONLY the golden perk in database # 3. Store all found events and identify golden skills
if golden_perk_data: if all_events_data:
cur.execute(""" # Determine max arrows for Chain and Dates to identify final step
INSERT INTO support_events (card_id, event_name, event_type) max_arrows = {
VALUES (?, ?, ?) 'Chain': max([e['arrows'] for e in all_events_data if e['type'] == 'Chain'] + [0]),
""", (card_id, golden_perk_data['name'], golden_perk_data['type'])) 'Date': max([e['arrows'] for e in all_events_data if e['type'] == 'Date'] + [0])
}
for event in all_events_data:
cur.execute("INSERT INTO support_events (card_id, event_name, event_type) VALUES (?, ?, ?)",
(card_id, event['name'], event['type']))
event_id = cur.lastrowid event_id = cur.lastrowid
for skill in golden_perk_data['skills']: for skill in event['skills']:
# Normalization helper
def normalize(s): def normalize(s):
return s.lower().replace(" hint +1", "").replace(" hint +3", "").replace(" hint +5", "").replace(" hint +", "").strip().replace(" ", " ").replace("-", "").replace("(", "").replace(")", "").replace(" ", "") # Remove hint suffix and special characters
s = s.lower().split(' hint +')[0]
return re.sub(r'[()\-\s\+]', '', s).strip()
skill_name = normalize(skill['name']) n_name = normalize(skill['name'])
# Use extra aggressive name matching against the map values
# (The map keys are already normalized)
is_gold = 0 is_gold = 0
for k, gold in skill_rarity_map.items(): for k, gold in skill_rarity_map.items():
if normalize(k) == skill_name: if normalize(k) == n_name:
is_gold = 1 if gold else 0 is_gold = 1 if gold else 0
break break
# Fallback 1: If it's a chain event and specifically the last one, it's almost certainly gold # Heuristic: If it's the last step of a Chain or Date, it's likely gold
if not is_gold and golden_perk_data.get('type') == 'Chain': if not is_gold and event['type'] in ['Chain', 'Date']:
# Check for "hint" patterns which usually accompany gold perks in chain events if event['arrows'] >= 3 and event['arrows'] == max_arrows[event['type']]:
if "hint +" in skill['name'].lower() or len(golden_perk_data['skills']) <= 2: if len(event['skills']) <= 2 or "hint +" in skill['name'].lower():
is_gold = 1 is_gold = 1
print(f"Golden Skill Fallback (Last Chain Event): {skill['name']}") print(f"Heuristic Gold: {skill['name']} in {event['name']}")
if is_gold: if is_gold: print(f" ✨ Verified Gold: {skill['name']}")
print(f" ✨ Golden Skill Verified: {skill['name']}")
cur.execute(""" cur.execute("""
INSERT INTO event_skills (event_id, skill_name, is_gold, is_or) INSERT INTO event_skills (event_id, skill_name, is_gold, is_or)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?)
""", (event_id, skill['name'], is_gold, 1 if skill['is_or'] else 0)) """, (event_id, skill['name'], is_gold, 1 if skill['is_or'] else 0))
skill_count = len(golden_perk_data['skills']) print(f" Scraped {len(all_events_data)} total events.")
or_count = sum(1 for s in golden_perk_data['skills'] if s['is_or'])
print(f" Golden Perk: {golden_perk_data['name']} ({skill_count} skills, {or_count} with OR)")
else: else:
print(f" No Golden Perk found for this card") print(f" No events found.")
def run_scraper(): def run_scraper():
""" Run the web scraper to fetch card data from GameTora.com """ """ Run the web scraper to fetch card data from GameTora.com """

View File

@@ -4,7 +4,7 @@ This file is the single source of truth for the application version.
""" """
# Semantic versioning: MAJOR.MINOR.PATCH # Semantic versioning: MAJOR.MINOR.PATCH
VERSION: str = "13.0.3" VERSION: str = "14.0.1"
# Application metadata # Application metadata
APP_NAME: str = "UmamusumeCardManager" APP_NAME: str = "UmamusumeCardManager"