Compare commits

...

5 Commits

31 changed files with 1196 additions and 935 deletions

View File

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

Binary file not shown.

View File

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

View File

@@ -1,10 +1,12 @@
"""
Deck Builder Frame
Build decks with 6 cards and view combined effects with breakdown
Updated for CustomTkinter
"""
import tkinter as tk
from tkinter import ttk, messagebox
import customtkinter as ctk
import sys
import os
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"""
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.remove_callback = remove_callback
self.level_callback = level_callback
@@ -39,158 +41,165 @@ class CardSlot(tk.Frame):
self.setup_ui()
def setup_ui(self):
# Configure grid weight
self.columnconfigure(1, weight=1)
# Configure grid
self.columnconfigure(0, weight=1)
# Slot number indicator
slot_label = tk.Label(self, text=f"#{self.index + 1}", font=FONT_TINY,
bg=BG_LIGHT, fg=TEXT_MUTED, padx=4, pady=2)
slot_label.place(x=2, y=2)
# Slot number indicator (Overlay)
self.slot_label = ctk.CTkLabel(self, text=f"#{self.index + 1}", font=FONT_TINY,
fg_color="#000000", text_color="#ffffff", corner_radius=4, height=18, width=24)
self.slot_label.place(x=4, y=4)
# Image Area (Left)
self.image_label = tk.Label(self, bg=BG_MEDIUM, text="📭", fg=TEXT_MUTED,
font=('Segoe UI', 32))
self.image_label.grid(row=0, column=0, rowspan=3, padx=12, pady=12)
# Image Area - Dominant
# Initial placeholder
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))
# Details Area (Right)
self.name_label = tk.Label(self, text="Empty Slot", bg=BG_MEDIUM, fg=TEXT_PRIMARY,
font=FONT_BODY_BOLD, anchor='w', wraplength=180) # Increased wrap
self.name_label.grid(row=0, column=1, sticky='w', padx=8, pady=(15, 0))
# Mini Details Area (Below Image)
self.info_frame = ctk.CTkFrame(self, fg_color="transparent")
self.info_frame.grid(row=1, column=0, sticky='ew', padx=4, pady=4)
self.info_frame.columnconfigure(0, weight=1)
self.meta_label = tk.Label(self, text="", bg=BG_MEDIUM, fg=TEXT_MUTED,
font=FONT_SMALL, anchor='w')
self.meta_label.grid(row=1, column=1, sticky='w', padx=8)
self.name_label = ctk.CTkLabel(self.info_frame, text="Empty", fg_color="transparent", text_color=TEXT_MUTED,
font=FONT_TINY, anchor='center', height=16)
self.name_label.grid(row=0, column=0, sticky='ew')
# Controls (Bottom Right)
ctrl_frame = tk.Frame(self, bg=BG_MEDIUM)
ctrl_frame.grid(row=2, column=1, sticky='ew', padx=8, pady=8)
# Level Selector
tk.Label(ctrl_frame, text="Lv:", bg=BG_MEDIUM, fg=TEXT_MUTED,
font=FONT_SMALL).pack(side=tk.LEFT)
# Controls Overlay (Bottom)
self.ctrl_frame = ctk.CTkFrame(self.info_frame, fg_color="transparent")
self.ctrl_frame.grid(row=1, column=0, sticky='ew', pady=(2,0))
# Level Selector (Compact)
self.level_var = tk.StringVar(value="50")
self.level_combo = ttk.Combobox(ctrl_frame, textvariable=self.level_var,
values=[], width=4, state='readonly')
self.level_combo.pack(side=tk.LEFT, padx=4)
self.level_combo.bind('<<ComboboxSelected>>', self._on_level_change)
self.level_combo = ctk.CTkComboBox(self.ctrl_frame, variable=self.level_var,
values=[], width=55, height=22, font=FONT_TINY, state='readonly', command=self._on_level_change)
self.level_combo.pack(side=tk.LEFT, padx=2)
# Remove Button
self.remove_btn = tk.Button(ctrl_frame, text="", bg=BG_LIGHT, fg=ACCENT_ERROR,
bd=0, font=FONT_BODY_BOLD, width=2,
activebackground=ACCENT_ERROR, activeforeground=TEXT_PRIMARY,
cursor='hand2',
# Remove Button (Compact)
self.remove_btn = ctk.CTkButton(self.ctrl_frame, text="", fg_color=BG_LIGHT, text_color=ACCENT_ERROR,
font=FONT_BODY_BOLD, width=22, height=22,
hover_color=BG_HIGHLIGHT,
command=lambda: self.remove_callback(self.index))
self.remove_btn.pack(side=tk.RIGHT)
# Pack later
# Hide controls initially
self.toggle_controls(False)
def toggle_controls(self, visible):
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:
self.remove_btn.pack_forget()
else:
self.remove_btn.pack(side=tk.RIGHT)
self.remove_btn.pack(side=tk.RIGHT, padx=2)
def set_card(self, card_data):
"""Set card data: (id, name, rarity, type, image_path, level)"""
"""Set card data"""
if not card_data:
self.reset()
return
card_id, name, rarity, card_type, image_path, level = card_data
# Calculate valid levels based on rarity
# Calculate valid levels
if rarity == 'SSR':
valid_levels = [50, 45, 40, 35, 30]
max_lvl = 50
elif rarity == 'SR':
valid_levels = [45, 40, 35, 30, 25]
max_lvl = 45
else: # R
else:
valid_levels = [40, 35, 30, 25, 20]
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)
type_icon = get_type_icon(card_type)
self.name_label.config(text=name, fg=TEXT_PRIMARY)
self.meta_label.config(text=f"{type_icon} {rarity}{card_type}", fg=color)
self.level_var.set(str(level))
# Truncate strictly
display_name = name if len(name) < 15 else name[:12] + "..."
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'}
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.toggle_controls(True)
def reset(self):
self.name_label.config(text="Empty Slot", fg=TEXT_MUTED)
self.meta_label.config(text="Click a card to add")
self.image_label.config(image='', text="📭", font=('Segoe UI', 32))
self.config(highlightbackground=BG_LIGHT)
self.name_label.configure(text="Empty", text_color=TEXT_MUTED)
# Recreate label to avoid TclError with missing images
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.toggle_controls(False)
def _load_image(self, path):
resolved_path = resolve_image_path(path)
# Prepare new image first
new_image = None
if resolved_path and os.path.exists(resolved_path):
try:
pil_img = Image.open(resolved_path)
# Significantly larger images as requested (120x120)
pil_img.thumbnail((120, 120), Image.Resampling.LANCZOS)
self.image_ref = ImageTk.PhotoImage(pil_img)
self.image_label.config(image=self.image_ref, text='')
except Exception as e:
print(f"Failed to load image: {e}")
self.image_label.config(image='', text="⚠️")
pil_img.thumbnail((90, 90), Image.Resampling.LANCZOS)
new_image = ctk.CTkImage(light_image=pil_img, dark_image=pil_img, size=(90, 90))
except Exception:
pass
# Recreate label
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:
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.level_callback(self.index, int(self.level_var.get()))
self.image_label.grid(row=0, column=0, padx=5, pady=(5,0))
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"""
def __init__(self, parent):
super().__init__(parent)
super().__init__(parent, fg_color="transparent")
self.current_deck_id = None
self.deck_slots = [None] * 6 # 6 card slots
self.setup_ui()
self.refresh_decks()
def setup_ui(self):
# Main container with split view
main_split = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
main_split.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# Main container with split view (simulated with frames)
# === Left Panel: Card Browser ===
left_panel = ttk.Frame(main_split)
main_split.add(left_panel, weight=1)
left_panel = ctk.CTkFrame(self, width=350, corner_radius=10)
left_panel.pack(side=tk.LEFT, fill=tk.BOTH, expand=False, padx=(0, 10), pady=10)
# Header
header = tk.Frame(left_panel, bg=BG_DARK)
header.pack(fill=tk.X, pady=(0, 10))
tk.Label(header, text="📋 Available Cards", font=FONT_SUBHEADER,
bg=BG_DARK, fg=TEXT_PRIMARY).pack(side=tk.LEFT)
header = ctk.CTkFrame(left_panel, fg_color="transparent")
header.pack(fill=tk.X, pady=(15, 10), padx=10)
ctk.CTkLabel(header, text="📋 Available Cards", font=FONT_SUBHEADER,
text_color=TEXT_PRIMARY).pack(side=tk.LEFT)
# Filters
filter_frame = tk.Frame(left_panel, bg=BG_DARK)
filter_frame.pack(fill=tk.X, pady=(0, 8))
filter_frame = ctk.CTkFrame(left_panel, fg_color="transparent")
filter_frame.pack(fill=tk.X, pady=(0, 8), padx=10)
# Filters - Initialize vars FIRST
self.type_var = tk.StringVar(value="All")
@@ -198,32 +207,31 @@ class DeckBuilderFrame(ttk.Frame):
self.search_var = tk.StringVar()
# Search Entry
self.search_entry = ttk.Entry(filter_frame, textvariable=self.search_var, width=18)
self.search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 8))
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, 5))
# Placeholder behavior (before trace)
self.search_entry.insert(0, "Search...")
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())
# Bind key release for search
self.search_entry.bind('<KeyRelease>', lambda e: self.filter_cards())
types = ["All", "Speed", "Stamina", "Power", "Guts", "Wisdom", "Friend", "Group"]
type_combo = ttk.Combobox(filter_frame, textvariable=self.type_var,
values=types, width=9, state='readonly')
type_combo = ctk.CTkComboBox(filter_frame, variable=self.type_var,
values=types, width=90, state='readonly', command=lambda e: self.filter_cards())
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,
command=self.filter_cards).pack(side=tk.LEFT, padx=8)
ctk.CTkCheckBox(filter_frame, text="Owned", variable=self.owned_only_var,
command=self.filter_cards, checkbox_width=24, checkbox_height=24, font=FONT_SMALL).pack(side=tk.LEFT, padx=5)
# Card List
list_frame = tk.Frame(left_panel, bg=BG_DARK)
list_frame.pack(fill=tk.BOTH, expand=True)
# Add Button (Packed first to stick to bottom)
add_btn = create_styled_button(left_panel, text=" Add to Deck",
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")
self.card_tree.heading('#0', text='')
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('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.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.card_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=5)
# Double-click to add
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 = ttk.Frame(main_split)
main_split.add(right_panel, weight=2)
right_panel = ctk.CTkFrame(self, fg_color="transparent")
right_panel.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, pady=10)
# Deck Controls
deck_ctrl = tk.Frame(right_panel, bg=BG_DARK)
deck_ctrl.pack(fill=tk.X, pady=(0, 15))
deck_ctrl = ctk.CTkFrame(right_panel, fg_color="transparent")
deck_ctrl.pack(fill=tk.X, pady=(0, 10)) # Reduced padding
tk.Label(deck_ctrl, text="🎴 Current Deck:", font=FONT_BODY,
bg=BG_DARK, fg=TEXT_SECONDARY).pack(side=tk.LEFT)
self.deck_combo = ttk.Combobox(deck_ctrl, width=25, state='readonly')
ctk.CTkLabel(deck_ctrl, text="🎴 Current Deck:", font=FONT_BODY,
text_color=TEXT_SECONDARY).pack(side=tk.LEFT)
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.bind('<<ComboboxSelected>>', self.on_deck_selected)
ttk.Button(deck_ctrl, text="+ New", command=self.create_new_deck,
style='Small.TButton').pack(side=tk.LEFT, padx=5)
ttk.Button(deck_ctrl, text="🗑️ Delete", command=self.delete_current_deck,
style='Small.TButton').pack(side=tk.LEFT)
create_styled_button(deck_ctrl, text="+ New", command=self.create_new_deck, width=60).pack(side=tk.LEFT, padx=5)
# Delete button - danger style
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
self.deck_count_label = tk.Label(deck_ctrl, text="0/6 cards",
font=FONT_SMALL, bg=BG_DARK, fg=ACCENT_PRIMARY)
self.deck_count_label = ctk.CTkLabel(deck_ctrl, text="0/6 cards",
font=FONT_SMALL, text_color=ACCENT_PRIMARY)
self.deck_count_label.pack(side=tk.LEFT, padx=15)
# Deck Grid (3x2)
self.slots_frame = tk.Frame(right_panel, bg=BG_DARK)
# Deck Grid (3x2) - Scrollable if needed, but 6 cards fit fine.
# 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.card_slots = []
for i in range(6):
slot = CardSlot(self.slots_frame, i, self.remove_from_slot, self.on_slot_level_changed)
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.card_slots.append(slot)
# Stats / Effects Area
effects_header = tk.Frame(right_panel, bg=BG_DARK)
effects_header.pack(fill=tk.X, pady=(20, 10))
tk.Label(effects_header, text="📊 Combined Effects Breakdown",
font=FONT_SUBHEADER, bg=BG_DARK, fg=TEXT_PRIMARY).pack(side=tk.LEFT)
effects_header = ctk.CTkFrame(right_panel, fg_color="transparent")
effects_header.pack(fill=tk.X, pady=(10, 5)) # Reduced padding
ctk.CTkLabel(effects_header, text="📊 Combined Effects Breakdown",
font=FONT_SUBHEADER, text_color=TEXT_PRIMARY).pack(side=tk.LEFT)
effects_frame = create_card_frame(right_panel)
effects_frame.pack(fill=tk.BOTH, expand=True)
# Effects Tree Container
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'),
show='headings', height=8)
show='headings', height=6) # Reduced Height
self.effects_tree.heading('effect', text='Effect')
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.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.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=2, pady=2)
vsb.pack(side=tk.RIGHT, fill=tk.Y, 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=5)
# Unique Effects Area
unique_header = tk.Frame(right_panel, bg=BG_DARK)
unique_header.pack(fill=tk.X, pady=(15, 8))
tk.Label(unique_header, text="✨ Unique Effects", font=FONT_BODY_BOLD,
bg=BG_DARK, fg=ACCENT_SECONDARY).pack(side=tk.LEFT)
unique_header = ctk.CTkFrame(right_panel, fg_color="transparent")
unique_header.pack(fill=tk.X, pady=(10, 5))
ctk.CTkLabel(unique_header, text="✨ Unique Effects", font=FONT_BODY_BOLD,
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)
self.unique_text = create_styled_text(unique_frame, height=5)
self.unique_text.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
self.unique_text.config(state=tk.DISABLED)
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=5, pady=5)
self.unique_text.configure(state=tk.DISABLED)
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 ---
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
# Ignore placeholder
# Search var comes from CTkEntry textvariable
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()
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:
if count > 200: break # soft limit
count += 1
card_id, name, rarity, card_type, max_level, image_path, is_owned, owned_level = card
# Load Icon
@@ -370,8 +371,8 @@ class DeckBuilderFrame(ttk.Frame):
if not img and resolved_path and os.path.exists(resolved_path):
try:
pil_img = Image.open(resolved_path)
# Larger thumbnails in the list too (48x48)
pil_img.thumbnail((48, 48), Image.Resampling.LANCZOS)
# Larger thumbnails in the list too (32x32 for list)
pil_img.thumbnail((32, 32), Image.Resampling.LANCZOS)
img = ImageTk.PhotoImage(pil_img)
self.icon_cache[card_id] = img
except:
@@ -387,15 +388,17 @@ class DeckBuilderFrame(ttk.Frame):
def refresh_decks(self):
decks = get_all_decks()
self.deck_combo['values'] = [f"{d[0]}: {d[1]}" for d in decks]
if decks and not self.current_deck_id:
self.deck_combo.current(0)
self.on_deck_selected(None)
values = [f"{d[0]}: {d[1]}" for d in decks]
self.deck_combo.configure(values=values)
if values and not self.current_deck_id:
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):
selection = self.deck_combo.get()
if selection:
self.current_deck_id = int(selection.split(':')[0])
def on_deck_selected_val(self, value):
if value:
self.current_deck_id = int(value.split(':')[0])
self.load_deck()
def load_deck(self):
@@ -435,7 +438,11 @@ class DeckBuilderFrame(ttk.Frame):
self.current_deck_id = None
self.deck_combo.set('')
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):
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
level = 50
parent = self.winfo_toplevel()
if hasattr(parent, 'last_selected_levels'):
level = parent.last_selected_levels.get(card_id, 50)
# Try to access main window state (depends on how it's linked, usually via parent or global state)
# 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)
self.load_deck()
self.load_deck() # Reloads everything
return
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):
"""Update the X/6 cards display"""
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):
if self.current_deck_id and self.deck_slots[index]:
@@ -491,12 +499,12 @@ class DeckBuilderFrame(ttk.Frame):
self.effects_tree.delete(item)
# 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)
if not self.current_deck_id:
self.unique_text.insert(tk.END, "No deck selected")
self.unique_text.config(state=tk.DISABLED)
self.unique_text.configure(state=tk.DISABLED)
return
# Prepare data for calculation
@@ -515,6 +523,7 @@ class DeckBuilderFrame(ttk.Frame):
for i, info in enumerate(card_info):
if info:
card_id, level = info
# Get name from slot label
card_name = self.card_slots[i].name_label.cget("text")
effects = get_effects_at_level(card_id, level)
@@ -527,15 +536,12 @@ class DeckBuilderFrame(ttk.Frame):
all_effects[name] = [''] * 6
all_effects[name][i] = value
# Configure tags
self.unique_text.tag_configure('card_name', foreground=ACCENT_PRIMARY)
# Fill Unique Effects
if unique_effects_list:
self.unique_text.insert(tk.END, "\n".join(unique_effects_list))
else:
self.unique_text.insert(tk.END, "No unique effects in this deck", 'card_name')
self.unique_text.config(state=tk.DISABLED)
self.unique_text.insert(tk.END, "No unique effects in this deck")
self.unique_text.configure(state=tk.DISABLED)
# Sum totals
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
Updated for CustomTkinter
"""
import tkinter as tk
from tkinter import ttk
import customtkinter as ctk
import sys
import os
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"""
def __init__(self, parent):
super().__init__(parent)
super().__init__(parent, fg_color="transparent")
self.icon_cache = {}
self.current_mode = "Deck" # or "Single"
@@ -38,31 +40,35 @@ class DeckSkillsFrame(ttk.Frame):
def create_widgets(self):
"""Create the deck skills interface"""
# 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)
# 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)
tk.Label(selection_frame, text="🎴 Select Deck:", font=FONT_BODY,
bg=BG_DARK, fg=TEXT_SECONDARY).pack(side=tk.LEFT)
ctk.CTkLabel(selection_frame, text="🎴 Select Deck:", font=FONT_BODY,
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.bind('<<ComboboxSelected>>', self.on_deck_selected)
# Mode indicator/Description
self.mode_label = tk.Label(ctrl_frame, text="Showing skills for selected deck",
font=FONT_HEADER, bg=BG_DARK, fg=ACCENT_PRIMARY)
self.mode_label = ctk.CTkLabel(ctrl_frame, text="Showing skills for selected deck",
font=FONT_HEADER, text_color=ACCENT_PRIMARY)
self.mode_label.pack(side=tk.RIGHT)
# 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))
# 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')
self.tree = ttk.Treeview(tree_container, columns=cols, show='tree headings',
self.tree = ttk.Treeview(tree_inner, columns=cols, show='tree headings',
style="Treeview")
self.tree.heading('#0', text='★ Card / Skill')
@@ -77,37 +83,41 @@ class DeckSkillsFrame(ttk.Frame):
self.tree.column('source', width=100)
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.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=2, pady=2)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=2)
# Footer
self.stats_label = tk.Label(self, text="", font=FONT_SMALL,
bg=BG_DARK, fg=TEXT_MUTED)
self.stats_label = ctk.CTkLabel(self, text="", font=FONT_SMALL,
text_color=TEXT_MUTED)
self.stats_label.pack(anchor='e', pady=5, padx=20)
def refresh_decks(self):
"""Load decks into combobox"""
decks = get_all_decks()
self.deck_combo['values'] = [f"{d[0]}: {d[1]}" for d in decks]
if decks:
self.deck_combo.current(0)
self.on_deck_selected(None)
values = [f"{d[0]}: {d[1]}" for d in decks]
self.deck_combo.configure(values=values)
if values:
self.deck_combo.set(values[0])
self.on_deck_selected_val(values[0])
def on_deck_selected(self, event):
"""Handle deck selection"""
selection = self.deck_combo.get()
if not selection: return
def on_deck_selected_val(self, value):
"""Handle deck selection from combobox values"""
if not value: return
deck_id = int(selection.split(':')[0])
deck_name = selection.split(': ')[1]
deck_id = int(value.split(':')[0])
deck_name = value.split(': ')[1]
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)
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):
"""Fetch and display all skills from a deck"""
# Clear tree
@@ -116,7 +126,7 @@ class DeckSkillsFrame(ttk.Frame):
deck_cards = get_deck_cards(deck_id)
if not deck_cards:
self.stats_label.config(text="Deck is empty")
self.stats_label.configure(text="Deck is empty")
return
total_skills = 0
@@ -142,7 +152,7 @@ class DeckSkillsFrame(ttk.Frame):
self.add_skill_row(parent_id, event['skill_name'], "Event", event['details'])
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):
"""Add a parent node for a card"""
@@ -152,7 +162,7 @@ class DeckSkillsFrame(ttk.Frame):
if resolved_path and os.path.exists(resolved_path):
try:
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)
self.icon_cache[card_id] = img
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
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
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'])
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 customtkinter as ctk
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):
super().__init__(parent)
self.title("Deck Builder")
@@ -9,9 +11,9 @@ class DeckView(tk.Toplevel):
self.deck_id = 1 # Default deck
tk.Button(self, text="Calculate Deck Bonuses", command=self.calculate).pack(pady=10)
self.output = tk.Text(self, height=20)
self.output.pack(fill=tk.BOTH, expand=True)
ctk.CTkButton(self, text="Calculate Deck Bonuses", command=self.calculate).pack(pady=10)
self.output = ctk.CTkTextbox(self, height=300, fg_color=BG_MEDIUM, text_color=TEXT_PRIMARY)
self.output.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
def calculate(self):
self.output.delete("1.0", tk.END)

View File

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

View File

@@ -1,9 +1,11 @@
"""
Skill Search View - Find cards by the skills they teach
Updated for CustomTkinter
"""
import tkinter as tk
from tkinter import ttk
import customtkinter as ctk
import sys
import os
from PIL import Image, ImageTk
@@ -17,15 +19,15 @@ from gui.theme import (
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_TERTIARY,
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
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"""
def __init__(self, parent):
super().__init__(parent)
super().__init__(parent, fg_color="transparent")
self.all_skills = []
self.icon_cache = {}
self.current_skill = None
@@ -36,30 +38,32 @@ class SkillSearchFrame(ttk.Frame):
def create_widgets(self):
"""Create the skill search interface"""
# Main split container
main_pane = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
main_pane.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# Use two frames instead of PanedWindow
# === Left Panel: Skill List ===
left_frame = tk.Frame(main_pane, bg=BG_DARK, width=300)
main_pane.add(left_frame, weight=1)
left_frame = ctk.CTkFrame(self, width=390, corner_radius=10)
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
header = tk.Frame(left_frame, bg=BG_DARK)
header.pack(fill=tk.X, pady=(0, 10))
tk.Label(header, text="🔍 Search Skills", font=FONT_HEADER,
bg=BG_DARK, fg=TEXT_PRIMARY).pack(side=tk.LEFT)
header = ctk.CTkFrame(left_frame, fg_color="transparent")
header.pack(fill=tk.X, pady=(15, 10), padx=10)
ctk.CTkLabel(header, text="🔍 Search Skills", font=FONT_HEADER,
text_color=TEXT_PRIMARY).pack(side=tk.LEFT)
# Search Entry
self.search_var = tk.StringVar()
self.search_var.trace('w', self.filter_skills)
search_entry = ttk.Entry(left_frame, textvariable=self.search_var)
search_entry.pack(fill=tk.X, padx=(0, 5), pady=(0, 10))
# Use styled entry
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
list_container = create_card_frame(left_frame)
list_container.pack(fill=tk.BOTH, expand=True)
# Skill Listbox Container (Styled)
list_container = ctk.CTkFrame(left_frame, fg_color="transparent")
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)
self.skill_listbox = tk.Listbox(list_container,
bg=BG_MEDIUM, fg=TEXT_SECONDARY,
@@ -77,27 +81,28 @@ class SkillSearchFrame(ttk.Frame):
self.skill_listbox.bind('<<ListboxSelect>>', self.on_skill_selected)
# === Right Panel: Results ===
right_frame = tk.Frame(main_pane, bg=BG_DARK)
main_pane.add(right_frame, weight=3)
right_frame = ctk.CTkFrame(self, corner_radius=10, fg_color="transparent")
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
# Search Row (Search + Filter)
search_frame = tk.Frame(right_frame, bg=BG_DARK)
search_frame.pack(fill=tk.X, padx=10, pady=10)
search_frame = ctk.CTkFrame(right_frame, fg_color="transparent")
search_frame.pack(fill=tk.X, padx=10, pady=15)
self.result_header = tk.Label(search_frame, text="Select a skill to see cards",
font=FONT_SUBHEADER, bg=BG_DARK, fg=ACCENT_PRIMARY)
self.result_header = ctk.CTkLabel(search_frame, text="Select a skill to see cards",
font=FONT_SUBHEADER, text_color=ACCENT_PRIMARY)
self.result_header.pack(side=tk.LEFT)
# Owned Filter
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,
command=self.on_filter_changed)
command=self.on_filter_changed,
font=FONT_SMALL)
self.owned_check.pack(side=tk.RIGHT, padx=10)
# Results Treeview
tree_frame = create_card_frame(right_frame)
tree_frame.pack(fill=tk.BOTH, expand=True, padx=10)
# Results Treeview Container
tree_frame = ctk.CTkFrame(right_frame, fg_color="transparent")
tree_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
cols = ('owned', 'name', 'rarity', 'type', 'source', 'details')
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)
self.tree.configure(yscrollcommand=vsb.set)
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=2, pady=2)
vsb.pack(side=tk.RIGHT, fill=tk.Y, 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=5)
# Stats footer
self.stats_label = tk.Label(right_frame, text="", font=FONT_SMALL,
bg=BG_DARK, fg=TEXT_MUTED)
self.stats_label = ctk.CTkLabel(right_frame, text="", font=FONT_SMALL,
text_color=TEXT_MUTED)
self.stats_label.pack(anchor='e', pady=5, padx=10)
def load_skills(self):
@@ -194,7 +199,7 @@ class SkillSearchFrame(ttk.Frame):
def show_cards_for_skill(self, skill_name):
"""Fetch and display cards with the selected skill"""
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
for item in self.tree.get_children():
@@ -204,10 +209,12 @@ class SkillSearchFrame(ttk.Frame):
owned_only = self.owned_only_var.get()
display_count = 0
for card in cards:
if owned_only and not card.get('is_owned'):
continue
display_count += 1
# Load Icon
card_id = card['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):
try:
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)
self.icon_cache[card_id] = img
except:
@@ -246,12 +253,10 @@ class SkillSearchFrame(ttk.Frame):
card_details
)
if img:
self.tree.insert('', tk.END, text='', image=img, values=values)
else:
self.tree.insert('', tk.END, text='?', values=values)
kv = {'image': img} if img else {}
self.tree.insert('', tk.END, text='', values=values, **kv)
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):
"""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
from tkinter import ttk
import customtkinter as ctk
import sys
import os
@@ -32,27 +32,29 @@ class MainWindow:
"""Main application window with tabbed interface"""
def __init__(self):
self.root = tk.Tk()
# Initialize CTk root
self.root = ctk.CTk()
self.root.title("Umamusume Support Card Manager")
self.root.geometry("1400x850")
self.root.minsize(1350, 800)
# Set icon
try:
icon_path = resolve_image_path("1_Special Week.png")
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)
self.root.iconphoto(True, icon_img)
except Exception as e:
print(f"Failed to set icon: {e}")
# Configure all styles using centralized theme
# Configure styles for legacy widgets
configure_styles(self.root)
# Create main container
main_container = ttk.Frame(self.root)
main_container.pack(fill=tk.BOTH, expand=True)
# Note: CTk already has a main frame in a way, but we'll use a container for padding
main_container = ctk.CTkFrame(self.root, fg_color="transparent")
main_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# State
self.last_selected_levels = {} # card_id -> level
@@ -60,56 +62,53 @@ class MainWindow:
# Header with stats
self.create_header(main_container)
# Status bar - Create BEFORE notebook to anchor it to bottom
# Status bar
self.create_status_bar(main_container)
# Tabbed notebook
self.notebook = ttk.Notebook(main_container)
self.notebook.pack(fill=tk.BOTH, expand=True, padx=15, pady=8)
# Tabbed notebook -> CTkTabview
self.tabview = ctk.CTkTabview(main_container)
self.tabview.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# Create tabs
self.create_tabs()
def create_header(self, parent):
"""Create header with database statistics and update button"""
# Header container with subtle bottom border effect
header_outer = tk.Frame(parent, bg=BG_DARK)
header_outer.pack(fill=tk.X)
header_frame = tk.Frame(header_outer, bg=BG_DARK)
header_frame.pack(fill=tk.X, padx=20, pady=15)
# Header container
header_frame = ctk.CTkFrame(parent, fg_color="transparent")
header_frame.pack(fill=tk.X, padx=10, pady=(0, 10))
# 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)
# App icon and title
title_label = tk.Label(
title_label = ctk.CTkLabel(
title_frame,
text="🏇 Umamusume Support Card Manager",
font=FONT_TITLE,
bg=BG_DARK,
fg=ACCENT_PRIMARY
text_color=ACCENT_PRIMARY
)
title_label.pack(side=tk.LEFT)
title_label.pack(side=tk.LEFT, padx=(0, 10))
# Version badge
version_frame = tk.Frame(title_frame, bg=ACCENT_SECONDARY, padx=8, pady=2)
version_frame.pack(side=tk.LEFT, padx=12)
version_label = tk.Label(
version_frame,
version_label = ctk.CTkLabel(
title_frame,
text=f"v{VERSION}",
font=FONT_SMALL,
bg=ACCENT_SECONDARY,
fg=TEXT_PRIMARY
fg_color=ACCENT_SECONDARY,
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_frame = tk.Frame(header_frame, bg=BG_DARK)
right_frame = ctk.CTkFrame(header_frame, fg_color="transparent")
right_frame.pack(side=tk.RIGHT)
# Update button with modern styling
# Update button
self.update_button = create_styled_button(
right_frame,
text="🔄 Check for Updates",
@@ -118,122 +117,96 @@ class MainWindow:
)
self.update_button.pack(side=tk.RIGHT, padx=(15, 0))
# Stats panel with card-like appearance
stats_frame = tk.Frame(right_frame, bg=BG_MEDIUM, padx=15, pady=8)
stats_frame.pack(side=tk.RIGHT)
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,
# Stats panel
self.stats_label = ctk.CTkLabel(
right_frame,
text="Loading stats...",
font=FONT_SMALL,
bg=BG_MEDIUM,
fg=TEXT_SECONDARY
fg_color=BG_MEDIUM,
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
separator = tk.Frame(header_outer, bg=BG_LIGHT, height=1)
separator.pack(fill=tk.X, padx=15)
# Initial stats load
self.refresh_stats()
def create_tabs(self):
"""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
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_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
self.effects_frame = EffectsFrame(self.notebook)
self.notebook.add(self.effects_frame, text=" 📊 Effects ")
self.effects_frame = EffectsFrame(tab_effects)
self.effects_frame.pack(fill=tk.BOTH, expand=True)
# Deck Builder Tab
self.deck_frame = DeckBuilderFrame(self.notebook)
self.notebook.add(self.deck_frame, text=" 🎴 Deck Builder ")
self.deck_frame = DeckBuilderFrame(tab_deck)
self.deck_frame.pack(fill=tk.BOTH, expand=True)
# Skill Search Tab
self.hints_frame = SkillSearchFrame(self.notebook)
self.notebook.add(self.hints_frame, text=" 🔍 Skill Search ")
self.hints_frame = SkillSearchFrame(tab_search)
self.hints_frame.pack(fill=tk.BOTH, expand=True)
# Deck Skills Tab
self.deck_skills_frame = DeckSkillsFrame(self.notebook)
self.notebook.add(self.deck_skills_frame, text=" 📜 Deck Skills ")
self.deck_skills_frame = DeckSkillsFrame(tab_skills)
self.deck_skills_frame.pack(fill=tk.BOTH, expand=True)
def create_status_bar(self, parent):
"""Create status bar at bottom"""
status_outer = tk.Frame(parent, bg=BG_MEDIUM)
status_outer.pack(fill=tk.X, side=tk.BOTTOM)
# Using pack side=BOTTOM relative to the main container
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)
status_frame.pack(fill=tk.X, padx=15, pady=8)
self.status_label = tk.Label(
self.status_label = ctk.CTkLabel(
status_frame,
text="✓ Ready",
font=FONT_SMALL,
bg=BG_MEDIUM,
fg=TEXT_MUTED
text_color=TEXT_MUTED
)
self.status_label.pack(side=tk.LEFT)
self.status_label.pack(side=tk.LEFT, padx=10)
tk.Label(
ctk.CTkLabel(
status_frame,
text="Data from gametora.com",
font=FONT_SMALL,
bg=BG_MEDIUM,
fg=TEXT_MUTED
text_color=TEXT_MUTED
).pack(side=tk.RIGHT)
# Diagnostics Button
diag_btn = tk.Button(
status_frame,
text="🔍 Diagnostics",
font=FONT_SMALL,
bg=BG_MEDIUM,
fg=ACCENT_TERTIARY,
bd=0,
activebackground=BG_LIGHT,
activeforeground=TEXT_PRIMARY,
cursor='hand2',
command=self.show_diagnostics
)
diag_btn.pack(side=tk.RIGHT, padx=15)
tk.Label(
ctk.CTkLabel(
status_frame,
text="VibeCoded by Kiyreload │ ",
font=FONT_SMALL,
bg=BG_MEDIUM,
fg=ACCENT_TERTIARY
text_color=ACCENT_TERTIARY
).pack(side=tk.RIGHT)
def on_card_selected(self, card_id, card_name, level=None):
"""Handle card selection from card list"""
# Store level if provided
if level is not None:
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'):
self.effects_frame.set_card(card_id)
if hasattr(self, 'deck_skills_frame'):
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):
"""Refresh the statistics display"""
@@ -249,70 +222,13 @@ class MainWindow:
]
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):
"""Show the update dialog"""
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):
"""
Start the GUI application and display the main window.

View File

@@ -1,21 +1,333 @@
"""
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
from tkinter import ttk
import customtkinter as ctk
# ═══════════════════════════════════════════════════════════════════════════════
# COLOR PALETTE
# CONFIGURATION
# ═══════════════════════════════════════════════════════════════════════════════
# Primary backgrounds (rich purplish-blues with depth)
BG_DARKEST = '#0d0d1a' # Deepest background
BG_DARK = '#151528' # Main application background
BG_MEDIUM = '#1e1e3f' # Card/panel backgrounds
BG_LIGHT = '#2a2a5a' # Elevated elements, hover states
BG_HIGHLIGHT = '#3d3d7a' # Active/selected backgrounds
# Set default theme
ctk.set_appearance_mode("Dark")
ctk.set_default_color_theme("blue") # We can use 'dark-blue' or 'green' too
# ═══════════════════════════════════════════════════════════════════════════════
# 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)
ACCENT_PRIMARY = '#ff6b9d' # Pink accent (main action color)
@@ -198,7 +510,7 @@ def configure_styles(root: tk.Tk):
foreground=TEXT_PRIMARY,
padding=6)
style.map('Treeview',
background=[('selected', ACCENT_PRIMARY)],
background=[('selected', BG_HIGHLIGHT)],
foreground=[('selected', TEXT_PRIMARY)])
style.map('Treeview.Heading',
background=[('active', BG_HIGHLIGHT)])
@@ -209,7 +521,7 @@ def configure_styles(root: tk.Tk):
foreground=TEXT_SECONDARY,
fieldbackground=BG_MEDIUM,
font=FONT_BODY,
rowheight=60)
rowheight=80)
# Deck list style
style.configure('DeckList.Treeview',
@@ -217,7 +529,7 @@ def configure_styles(root: tk.Tk):
foreground=TEXT_SECONDARY,
fieldbackground=BG_MEDIUM,
font=FONT_BODY,
rowheight=60)
rowheight=80)
style.map('DeckList.Treeview',
background=[('selected', ACCENT_PRIMARY)])
@@ -258,17 +570,27 @@ def configure_styles(root: tk.Tk):
def create_styled_entry(parent, textvariable=None, **kwargs):
"""Create a styled tk.Entry with modern appearance"""
entry = ttk.Entry(
"""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,
**kwargs
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):
"""Create a styled tk.Button with modern appearance"""
"""Create a styled ctk.CTkButton with modern appearance"""
bg_colors = {
'default': BG_LIGHT,
'accent': ACCENT_PRIMARY,
@@ -286,35 +608,27 @@ def create_styled_button(parent, text, command=None, style_type='default', **kwa
'danger': '#ff8a8a'
}
bg = bg_colors.get(style_type, BG_LIGHT)
hover_bg = hover_colors.get(style_type, BG_HIGHLIGHT)
fg_color = bg_colors.get(style_type, BG_LIGHT)
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,
text=text,
command=command,
bg=bg,
fg=TEXT_PRIMARY,
fg_color=fg_color,
text_color=TEXT_PRIMARY,
font=FONT_BODY_BOLD if style_type == 'accent' else FONT_BODY,
activebackground=hover_bg,
activeforeground=TEXT_PRIMARY,
bd=0,
padx=16,
pady=8,
cursor='hand2',
relief=tk.FLAT,
**kwargs
hover_color=hover_color,
corner_radius=8,
border_width=0,
**safe_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

View File

@@ -1,21 +1,22 @@
"""
Update Dialog for UmamusumeCardManager
Provides a modal dialog for the update process.
Updated for CustomTkinter
"""
import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext
from tkinter import ttk, messagebox
import customtkinter as ctk
import threading
import webbrowser
from typing import Optional, Callable
import sys
import os
from typing import Optional, Callable
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 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,
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL,
@@ -26,7 +27,7 @@ from gui.theme import (
class UpdateDialog:
"""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.on_close_callback = on_close_callback
self.update_info = None
@@ -34,25 +35,28 @@ class UpdateDialog:
self.is_downloading = False
# Create the dialog window
self.dialog = tk.Toplevel(parent)
self.dialog = ctk.CTkToplevel(parent)
self.dialog.title("Check for Updates")
self.dialog.geometry("520x600")
self.dialog.resizable(True, True)
self.dialog.minsize(480, 500)
# Set transient/grab to make it modal
self.dialog.transient(parent)
self.dialog.grab_set()
# Center on parent
self.center_on_parent()
self.dialog.configure(bg=BG_DARK)
# Set up the UI
self.setup_ui()
# Start checking for updates
self.check_for_updates()
# Handle close window event
self.dialog.protocol("WM_DELETE_WINDOW", self.close)
def center_on_parent(self):
"""Center the dialog on the parent window."""
self.dialog.update_idletasks()
@@ -71,9 +75,94 @@ class UpdateDialog:
def setup_ui(self):
"""Set up the dialog UI."""
# Button frame (Create first to pack at bottom)
self.button_frame = tk.Frame(self.dialog, bg=BG_DARK, pady=20, padx=20)
self.button_frame.pack(side=tk.BOTTOM, fill=tk.X)
# Main container (CTk has its own bg, so we don't strictly need a frame, but for padding)
main_frame = ctk.CTkFrame(self.dialog, fg_color="transparent")
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
self.close_button = create_styled_button(
@@ -91,101 +180,7 @@ class UpdateDialog:
command=self.start_download,
style_type='accent'
)
# We don't pack it yet
# 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)
# Pack when ready
def check_for_updates(self):
"""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
# Enable text box to update it
self.notes_text.config(state=tk.NORMAL)
self.notes_text.delete(1.0, tk.END)
self.notes_text.configure(state=tk.NORMAL)
self.notes_text.delete("1.0", tk.END)
if self.update_info:
# Update available!
self.title_label.config(text="🎉 Update Available!")
self.status_label.config(
self.title_label.configure(text="🎉 Update Available!")
self.status_label.configure(
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']}",
fg=ACCENT_SUCCESS
text_color=ACCENT_SUCCESS
)
# Show Release Notes
@@ -225,18 +220,18 @@ class UpdateDialog:
self.update_button.pack(side=tk.RIGHT, padx=(0, 10))
else:
# Up to date or error
self.title_label.config(text="✅ You're Up to Date!")
self.status_label.config(
self.title_label.configure(text="✅ You're Up to Date!")
self.status_label.configure(
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()}",
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.config(state=tk.DISABLED)
self.notes_text.configure(state=tk.DISABLED)
def start_download(self):
"""Start downloading the update."""
@@ -244,22 +239,22 @@ class UpdateDialog:
return
self.is_downloading = True
self.update_button.config(state=tk.DISABLED, text="Downloading...")
self.close_button.config(state=tk.DISABLED)
self.update_button.configure(state=tk.DISABLED, text="Downloading...")
self.close_button.configure(state=tk.DISABLED)
self.title_label.config(text="⬇️ Downloading Update...")
self.status_label.config(text="Please wait...", fg=TEXT_MUTED)
self.title_label.configure(text="⬇️ Downloading Update...")
self.status_label.configure(text="Please wait...", text_color=TEXT_MUTED)
# Configure progress bar for determinate mode
self.progress_frame.pack(fill=tk.X, pady=(0, 10)) # Show progress frame again
self.progress_bar.config(mode='determinate', maximum=100)
self.progress_frame.pack(fill=tk.X, pady=(0, 10))
self.progress_bar.configure(mode='determinate')
self.progress_bar.set(0)
self.progress_bar.pack(fill=tk.X)
self.progress_bar['value'] = 0
def download():
def progress_callback(downloaded, total):
if total > 0:
percent = int((downloaded / total) * 100)
percent = downloaded / total # 0.0 to 1.0 for CTk
mb_downloaded = downloaded / (1024 * 1024)
mb_total = total / (1024 * 1024)
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.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."""
self.progress_bar['value'] = percent
self.progress_label.config(text=f"Downloaded: {downloaded_mb:.1f} MB / {total_mb:.1f} MB ({percent}%)")
self.progress_bar.set(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]):
"""Called when the download is complete."""
self.is_downloading = False
if download_path:
self.title_label.config(text="✅ Download Complete!")
self.status_label.config(
self.title_label.configure(text="✅ Download Complete!")
self.status_label.configure(
text="Update ready to install.",
fg=ACCENT_SUCCESS
text_color=ACCENT_SUCCESS
)
# Change button to install
self.update_button.config(
self.update_button.configure(
state=tk.NORMAL,
text="🔄 Install & Restart",
command=lambda: self.install_update(download_path)
)
self.close_button.config(state=tk.NORMAL)
self.close_button.configure(state=tk.NORMAL)
else:
self.title_label.config(text="❌ Download Failed")
self.status_label.config(
self.title_label.configure(text="❌ Download Failed")
self.status_label.configure(
text="Failed not download update.",
fg=ACCENT_ERROR
text_color=ACCENT_ERROR
)
self.update_button.config(state=tk.NORMAL, text="⬇️ Retry Download")
self.close_button.config(state=tk.NORMAL)
self.update_button.configure(state=tk.NORMAL, text="⬇️ Retry Download")
self.close_button.configure(state=tk.NORMAL)
def install_update(self, download_path: str):
"""Install the downloaded update."""
self.title_label.config(text="🔄 Installing Update...")
self.status_label.config(text="Applying update...", fg=TEXT_MUTED)
self.update_button.config(state=tk.DISABLED)
self.close_button.config(state=tk.DISABLED)
self.title_label.configure(text="🔄 Installing Update...")
self.status_label.configure(text="Applying update...", text_color=TEXT_MUTED)
self.update_button.configure(state=tk.DISABLED)
self.close_button.configure(state=tk.DISABLED)
if apply_update(download_path):
# Exit the application - the updater script will restart it
@@ -322,7 +317,6 @@ class UpdateDialog:
)
self.close()
def close(self):
"""Close the dialog."""
if self.on_close_callback:
@@ -330,15 +324,8 @@ class UpdateDialog:
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.
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)

View File

@@ -1,7 +1,7 @@
import sqlite3
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():
if not os.path.exists(DB_PATH):

View File

@@ -2,7 +2,7 @@ import sqlite3
import os
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):
print(f"Database not found at {db_path}")
return

View File

@@ -3,7 +3,7 @@ import sys
from playwright.sync_api import sync_playwright
# 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():
url = "https://gametora.com/umamusume/supports/30028-kitasan-black"

View File

@@ -3,7 +3,7 @@ import os
import sys
# 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 db.db_queries import DB_PATH, repair_orphaned_data, cleanup_orphaned_data

View File

@@ -3,7 +3,7 @@ import os
import sys
# 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

View File

@@ -3,7 +3,7 @@ import os
import sys
# 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

View File

@@ -3,7 +3,7 @@ import os
import sys
# 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 db.db_queries import get_conn

View File

@@ -5,7 +5,7 @@ from playwright.sync_api import sync_playwright
import sys
# 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):
with sync_playwright() as p:

View File

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