Compare commits
8 Commits
1207a62437
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d9a3da4cc | ||
|
|
40047ee145 | ||
|
|
9cbfb29477 | ||
|
|
3a50c93466 | ||
|
|
8749e9a1d8 | ||
|
|
d8e4dd909d | ||
|
|
ebc0f132db | ||
|
|
6e2fe461ae |
47
README.md
47
README.md
@@ -1,14 +1,15 @@
|
|||||||
# Umamusume Support Card Manager
|
# Umamusume Support Card Manager
|
||||||
|
|
||||||
A tool for managing support cards and their effects in Umamusume (Granblue Fantasy Relink).
|
A tool for managing support cards and their effects in Umamusume.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Web scraping of support card data from GameTora
|
- **Cards Management**: View and manage your support cards collection.
|
||||||
- Database storage of card information including effects at different levels
|
- **Deck Builder**: Build and optimize decks with your owned cards.
|
||||||
- GUI application for viewing and managing support cards
|
- **Effects Search**: Search for specific effects across your owned cards (e.g., "Friendship Bonus", "Skill Pt Bonus").
|
||||||
- Deck building functionality
|
- **Web Scraping**: Integrated GameTora scraper to fetch the latest card data.
|
||||||
- Character art downloading
|
- **Auto-Updater**: Automatically improved application updates.
|
||||||
|
- **Maintenance Scripts**: Suite of scripts for database repair and deep scraping.
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
@@ -23,8 +24,15 @@ A tool for managing support cards and their effects in Umamusume (Granblue Fanta
|
|||||||
│ ├── db_init.py # Database initialization
|
│ ├── db_init.py # Database initialization
|
||||||
│ └── db_queries.py # Database queries
|
│ └── db_queries.py # Database queries
|
||||||
├── gui/ # GUI components
|
├── gui/ # GUI components
|
||||||
|
│ ├── main_window.py # Main application window
|
||||||
|
│ ├── card_view.py # Card list and details view
|
||||||
|
│ ├── deck_builder.py # Deck construction view
|
||||||
|
│ ├── effects_view.py # Effects search view
|
||||||
|
│ └── ...
|
||||||
├── updater/ # Update checking functionality
|
├── updater/ # Update checking functionality
|
||||||
├── database/ # Database files
|
├── maintenance_scripts/ # Database repair and utility scripts
|
||||||
|
├── config/ # Configuration files
|
||||||
|
├── database/ # Database files storage
|
||||||
├── images/ # Character art images
|
├── images/ # Character art images
|
||||||
├── build/ # Build artifacts
|
├── build/ # Build artifacts
|
||||||
└── dist/ # Distribution files
|
└── dist/ # Distribution files
|
||||||
@@ -32,9 +40,9 @@ A tool for managing support cards and their effects in Umamusume (Granblue Fanta
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Clone the repository
|
1. Clone the repository.
|
||||||
2. Install dependencies: `pip install -r requirements.txt`
|
2. Install dependencies: `pip install -r requirements.txt`.
|
||||||
3. Run the application: `python main.py`
|
3. Run the application: `python main.py`.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -44,6 +52,7 @@ python main.py
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Scraping Mode
|
### Scraping Mode
|
||||||
|
To manually run the scraper:
|
||||||
```bash
|
```bash
|
||||||
python main.py --scrape
|
python main.py --scrape
|
||||||
```
|
```
|
||||||
@@ -51,17 +60,17 @@ python main.py --scrape
|
|||||||
## Development
|
## Development
|
||||||
|
|
||||||
### Code Structure
|
### Code Structure
|
||||||
- `main.py`: Entry point and argument parsing
|
- `main.py`: Entry point and argument parsing.
|
||||||
- `scraper/gametora_scraper.py`: Web scraping logic
|
- `gui/`: Contains all CustomTkinter-based UI components.
|
||||||
- `db/db_init.py`: Database schema initialization
|
- `db/`: Handles SQLite database interactions.
|
||||||
- `gui/`: GUI components (MainWindow, views, etc.)
|
- `scraper/`: Logic for fetching data from GameTora.
|
||||||
- `updater/update_checker.py`: Update checking functionality
|
- `maintenance_scripts/`: Tools for fixing database inconsistencies or re-fetching data.
|
||||||
|
|
||||||
### Contributing
|
### Contributing
|
||||||
1. Fork the repository
|
1. Fork the repository.
|
||||||
2. Create a feature branch
|
2. Create a feature branch.
|
||||||
3. Make your changes
|
3. Make your changes.
|
||||||
4. Submit a pull request
|
4. Submit a pull request.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
MIT
|
MIT
|
||||||
Binary file not shown.
@@ -108,8 +108,44 @@ def run_migrations():
|
|||||||
pass # Column already exists
|
pass # Column already exists
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
repair_image_paths(conn)
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
def repair_image_paths(conn):
|
||||||
|
"""Attempt to populate missing image_path for existing cards in old databases"""
|
||||||
|
print("Checking for missing image paths to repair...")
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# Find cards with missing image paths but have a URL
|
||||||
|
cur.execute("SELECT card_id, name, gametora_url FROM support_cards WHERE image_path IS NULL OR image_path = ''")
|
||||||
|
to_repair = cur.fetchall()
|
||||||
|
|
||||||
|
if not to_repair:
|
||||||
|
return
|
||||||
|
|
||||||
|
import re
|
||||||
|
repaired_count = 0
|
||||||
|
|
||||||
|
for card_id, name, url in to_repair:
|
||||||
|
if not url: continue
|
||||||
|
|
||||||
|
# Extract ID from URL (e.g., 30154 from .../supports/30154-mejiro-ramonu)
|
||||||
|
match = re.search(r'/supports/(\d+)-', url)
|
||||||
|
if match:
|
||||||
|
stable_id = match.group(1)
|
||||||
|
# Create safe filename matching scraper logic
|
||||||
|
safe_name = re.sub(r'[<>:"/\\\\|?*]', '_', name)
|
||||||
|
filename = f"{stable_id}_{safe_name}.png"
|
||||||
|
|
||||||
|
# Update DB with images/filename
|
||||||
|
cur.execute("UPDATE support_cards SET image_path = ? WHERE card_id = ?",
|
||||||
|
(f"images/{filename}", card_id))
|
||||||
|
repaired_count += 1
|
||||||
|
|
||||||
|
if repaired_count > 0:
|
||||||
|
conn.commit()
|
||||||
|
print(f"Successfully repaired {repaired_count} image paths!")
|
||||||
|
|
||||||
def check_for_updates():
|
def check_for_updates():
|
||||||
"""Check if database version matches app version, sync if outdated"""
|
"""Check if database version matches app version, sync if outdated"""
|
||||||
if getattr(sys, 'frozen', False):
|
if getattr(sys, 'frozen', False):
|
||||||
@@ -486,6 +522,49 @@ def get_unique_effect_names(card_id):
|
|||||||
conn.close()
|
conn.close()
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
def search_owned_effects(search_term):
|
||||||
|
"""
|
||||||
|
Search for effects among owned cards.
|
||||||
|
Returns list of (card_id, card_name, image_path, effect_name, effect_value, level)
|
||||||
|
"""
|
||||||
|
conn = get_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# We need to join support_effects with owned_cards to get the level
|
||||||
|
# But wait, owned_cards has a level column. support_effects stores effects for specific levels.
|
||||||
|
# So we need to match support_effects.level with owned_cards.level
|
||||||
|
# OR find the effect for the closest level <= owned level (if effects aren't stored for every single level)
|
||||||
|
# The current DB schema seems to store effects for specific levels (1, 5, 10, ...).
|
||||||
|
# If a card is level 49, `get_effects_at_level` usually queries for exact level match.
|
||||||
|
# Let's check `get_effects_at_level` implementation: "WHERE card_id = ? AND level = ?"
|
||||||
|
# So if I have a card at level 49, and effects are only defined at 45 and 50, query for 49 returns nothing?
|
||||||
|
# That would be a bug or assumption in the current app.
|
||||||
|
# Let's look at `update_progression_table` in `effects_view.py`. It does some "nearest level" logic.
|
||||||
|
# For this search feature, to be robust, we should probably fetch ALL effects for the card
|
||||||
|
# and filter for the one active at the owned level.
|
||||||
|
# OR, assuming the scraper/DB populates "current" effects effectively.
|
||||||
|
# Actually, the most robust way in SQL for "value at level X" given sparse data is complex.
|
||||||
|
# However, let's assume for now we want exact matches or we'll handle the "effective level" logic in Python?
|
||||||
|
# No, that's too slow for search.
|
||||||
|
# Let's look at how `get_effects_at_level` is used.
|
||||||
|
# It is used in `update_current_effects` with `self.level_var.get()`.
|
||||||
|
# It expects an exact match.
|
||||||
|
# So we should probably join on `oc.level`.
|
||||||
|
|
||||||
|
query = """
|
||||||
|
SELECT sc.card_id, sc.name, sc.image_path, se.effect_name, se.effect_value, oc.level
|
||||||
|
FROM owned_cards oc
|
||||||
|
JOIN support_cards sc ON oc.card_id = sc.card_id
|
||||||
|
JOIN support_effects se ON oc.card_id = se.card_id AND oc.level = se.level
|
||||||
|
WHERE se.effect_name LIKE ?
|
||||||
|
ORDER BY sc.name
|
||||||
|
"""
|
||||||
|
|
||||||
|
cur.execute(query, (f"%{search_term}%",))
|
||||||
|
rows = cur.fetchall()
|
||||||
|
conn.close()
|
||||||
|
return rows
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Hint Queries
|
# Hint Queries
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|||||||
505
gui/card_view.py
505
gui/card_view.py
@@ -1,9 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
Card List View - Browse and search support cards with ownership management
|
Card List View - Browse and search support cards with ownership management
|
||||||
|
Updated for CustomTkinter
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
|
import customtkinter as ctk
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from PIL import Image, ImageTk
|
from PIL import Image, ImageTk
|
||||||
@@ -14,21 +16,21 @@ from db.db_queries import get_all_cards, get_card_by_id, get_effects_at_level, s
|
|||||||
from utils import resolve_image_path
|
from utils import resolve_image_path
|
||||||
from gui.theme import (
|
from gui.theme import (
|
||||||
BG_DARK, BG_MEDIUM, BG_LIGHT, BG_HIGHLIGHT,
|
BG_DARK, BG_MEDIUM, BG_LIGHT, BG_HIGHLIGHT,
|
||||||
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_SUCCESS,
|
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_SUCCESS, ACCENT_ERROR, ACCENT_WARNING,
|
||||||
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
|
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
|
||||||
FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL, FONT_MONO,
|
FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL, FONT_MONO, FONT_FAMILY,
|
||||||
RARITY_COLORS, TYPE_COLORS, TYPE_ICONS,
|
RARITY_COLORS, TYPE_COLORS, TYPE_ICONS,
|
||||||
create_styled_button, create_styled_text, create_card_frame,
|
create_styled_button, create_styled_text, create_card_frame,
|
||||||
get_rarity_color, get_type_color, get_type_icon,
|
get_rarity_color, get_type_color, get_type_icon,
|
||||||
EFFECT_DESCRIPTIONS, Tooltip
|
EFFECT_DESCRIPTIONS, Tooltip, create_styled_entry
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CardListFrame(ttk.Frame):
|
class CardListFrame(ctk.CTkFrame):
|
||||||
"""Frame containing card list with search/filter, ownership, and details panel"""
|
"""Frame containing card list with search/filter, ownership, and details panel"""
|
||||||
|
|
||||||
def __init__(self, parent, on_card_selected_callback=None, on_stats_updated_callback=None):
|
def __init__(self, parent, on_card_selected_callback=None, on_stats_updated_callback=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent, fg_color="transparent") # Transparent to blend with tab
|
||||||
self.on_card_selected = on_card_selected_callback
|
self.on_card_selected = on_card_selected_callback
|
||||||
self.on_stats_updated = on_stats_updated_callback
|
self.on_stats_updated = on_stats_updated_callback
|
||||||
self.cards = []
|
self.cards = []
|
||||||
@@ -46,203 +48,250 @@ class CardListFrame(ttk.Frame):
|
|||||||
def create_widgets(self):
|
def create_widgets(self):
|
||||||
"""Create the card list interface"""
|
"""Create the card list interface"""
|
||||||
# Main horizontal layout
|
# Main horizontal layout
|
||||||
main_pane = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
|
# CTk doesn't have PanedWindow, so we'll use a grid or pack with frames
|
||||||
main_pane.pack(fill=tk.BOTH, expand=True)
|
# We can simulate split view with two frames
|
||||||
|
|
||||||
# Left panel - Card list with filters
|
# Left panel - Card list with filters
|
||||||
left_frame = ttk.Frame(main_pane, width=420)
|
left_frame = ctk.CTkFrame(self, width=420, corner_radius=10)
|
||||||
main_pane.add(left_frame, weight=1)
|
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=False, padx=(0, 10))
|
||||||
|
|
||||||
# Right panel - Card details
|
# Right panel - Card details
|
||||||
self.details_frame = ttk.Frame(main_pane)
|
self.details_frame = ctk.CTkFrame(self, corner_radius=10, fg_color="transparent")
|
||||||
main_pane.add(self.details_frame, weight=2)
|
self.details_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
# === Left Panel Contents ===
|
# === Left Panel Contents ===
|
||||||
|
|
||||||
# Initialize filter variables FIRST (before search trace can trigger filter_cards)
|
# Initialize filter variables
|
||||||
self.rarity_var = tk.StringVar(value="All")
|
self.rarity_var = tk.StringVar(value="All")
|
||||||
self.type_var = tk.StringVar(value="All")
|
self.type_var = tk.StringVar(value="All")
|
||||||
self.owned_only_var = tk.BooleanVar(value=False)
|
self.owned_only_var = tk.BooleanVar(value=False)
|
||||||
|
self.search_var = tk.StringVar(value="")
|
||||||
|
|
||||||
# Search bar with modern styling
|
# Search bar
|
||||||
search_frame = tk.Frame(left_frame, bg=BG_DARK)
|
search_frame = ctk.CTkFrame(left_frame, fg_color="transparent")
|
||||||
search_frame.pack(fill=tk.X, padx=10, pady=10)
|
search_frame.pack(fill=tk.X, padx=15, pady=(20, 10))
|
||||||
|
|
||||||
search_icon = tk.Label(search_frame, text="🔍", font=FONT_BODY, bg=BG_DARK, fg=TEXT_MUTED)
|
self.search_entry = ctk.CTkEntry(
|
||||||
search_icon.pack(side=tk.LEFT, padx=(0, 5))
|
search_frame,
|
||||||
|
textvariable=self.search_var,
|
||||||
self.search_var = tk.StringVar()
|
placeholder_text="🔍 Search cards...",
|
||||||
self.search_entry = ttk.Entry(search_frame, textvariable=self.search_var, width=35)
|
width=200,
|
||||||
self.search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
height=36
|
||||||
|
)
|
||||||
# Set placeholder BEFORE adding the trace (so it doesn't trigger filter)
|
self.search_entry.pack(fill=tk.X, expand=True)
|
||||||
self.search_entry.insert(0, "Search cards...")
|
self.search_entry.bind('<KeyRelease>', lambda e: self.filter_cards()) # Trace didn't work smoothly with CTkVar sometimes
|
||||||
self.search_entry.config(foreground=TEXT_MUTED)
|
|
||||||
self.search_entry.bind('<FocusIn>', self._on_search_focus_in)
|
|
||||||
self.search_entry.bind('<FocusOut>', self._on_search_focus_out)
|
|
||||||
|
|
||||||
# NOW add the trace (after placeholder is set)
|
|
||||||
self.search_var.trace('w', lambda *args: self.filter_cards())
|
|
||||||
|
|
||||||
# Filter dropdowns
|
# Filter dropdowns
|
||||||
filter_frame = tk.Frame(left_frame, bg=BG_DARK)
|
filter_frame = ctk.CTkFrame(left_frame, fg_color="transparent")
|
||||||
filter_frame.pack(fill=tk.X, padx=10, pady=(0, 10))
|
filter_frame.pack(fill=tk.X, padx=15, pady=(0, 10))
|
||||||
|
|
||||||
# Rarity filter
|
# Rarity filter
|
||||||
tk.Label(filter_frame, text="Rarity:", font=FONT_SMALL, bg=BG_DARK, fg=TEXT_MUTED).pack(side=tk.LEFT)
|
# ctk.CTkLabel(filter_frame, text="Rarity:", font=FONT_TINY).pack(side=tk.LEFT)
|
||||||
rarity_combo = ttk.Combobox(filter_frame, textvariable=self.rarity_var,
|
rarity_combo = ctk.CTkComboBox(
|
||||||
values=["All", "SSR", "SR", "R"], width=7, state='readonly')
|
filter_frame,
|
||||||
rarity_combo.pack(side=tk.LEFT, padx=(5, 15))
|
variable=self.rarity_var,
|
||||||
rarity_combo.bind('<<ComboboxSelected>>', lambda e: self.filter_cards())
|
values=["All", "SSR", "SR", "R"],
|
||||||
|
width=80,
|
||||||
|
height=32,
|
||||||
|
command=lambda e: self.filter_cards()
|
||||||
|
)
|
||||||
|
rarity_combo.pack(side=tk.LEFT, padx=(0, 10))
|
||||||
|
rarity_combo.set("All")
|
||||||
|
|
||||||
# Type filter
|
# Type filter
|
||||||
tk.Label(filter_frame, text="Type:", font=FONT_SMALL, bg=BG_DARK, fg=TEXT_MUTED).pack(side=tk.LEFT)
|
# ctk.CTkLabel(filter_frame, text="Type:", font=FONT_TINY).pack(side=tk.LEFT)
|
||||||
type_combo = ttk.Combobox(filter_frame, textvariable=self.type_var,
|
type_combo = ctk.CTkComboBox(
|
||||||
values=["All", "Speed", "Stamina", "Power", "Guts", "Wisdom", "Friend", "Group"],
|
filter_frame,
|
||||||
width=10, state='readonly')
|
variable=self.type_var,
|
||||||
type_combo.pack(side=tk.LEFT, padx=5)
|
values=["All", "Speed", "Stamina", "Power", "Guts", "Wisdom", "Friend", "Group"],
|
||||||
type_combo.bind('<<ComboboxSelected>>', lambda e: self.filter_cards())
|
width=100,
|
||||||
|
height=32,
|
||||||
|
command=lambda e: self.filter_cards()
|
||||||
|
)
|
||||||
|
type_combo.pack(side=tk.LEFT, padx=(0, 10))
|
||||||
|
type_combo.set("All")
|
||||||
|
|
||||||
# Owned only filter
|
# Reset Button (Icon only maybe? or small text)
|
||||||
owned_check = ttk.Checkbutton(filter_frame, text="Owned Only",
|
ctk.CTkButton(
|
||||||
variable=self.owned_only_var, command=self.filter_cards)
|
filter_frame,
|
||||||
owned_check.pack(side=tk.LEFT, padx=15)
|
text="✕",
|
||||||
|
width=32,
|
||||||
|
height=32,
|
||||||
|
fg_color=BG_LIGHT,
|
||||||
|
hover_color=ACCENT_ERROR,
|
||||||
|
command=self.reset_filters
|
||||||
|
).pack(side=tk.LEFT)
|
||||||
|
|
||||||
# Reset Button
|
# Owned Only Checkbox (Below filters for spacing)
|
||||||
ttk.Button(filter_frame, text="Reset", command=self.reset_filters,
|
owned_frame = ctk.CTkFrame(left_frame, fg_color="transparent")
|
||||||
style='Small.TButton', width=7).pack(side=tk.LEFT, padx=5)
|
owned_frame.pack(fill=tk.X, padx=15, pady=(0, 10))
|
||||||
|
|
||||||
|
owned_check = ctk.CTkCheckBox(
|
||||||
|
owned_frame,
|
||||||
|
text="Owned Only",
|
||||||
|
variable=self.owned_only_var,
|
||||||
|
command=self.filter_cards,
|
||||||
|
font=FONT_SMALL
|
||||||
|
)
|
||||||
|
owned_check.pack(side=tk.LEFT)
|
||||||
|
|
||||||
# Shortcuts
|
# Shortcuts
|
||||||
self.bind_all('<Control-f>', lambda e: self.search_entry.focus_set())
|
# Shortcuts
|
||||||
|
try:
|
||||||
|
self.winfo_toplevel().bind('<Control-f>', lambda e: self.search_entry.focus_set())
|
||||||
|
except AttributeError:
|
||||||
|
pass # In case toplevel isn't ready or doesn't support bind yet
|
||||||
|
|
||||||
# Card count label
|
# Card count label
|
||||||
self.count_label = tk.Label(left_frame, text="0 cards", font=FONT_SMALL,
|
self.count_label = ctk.CTkLabel(
|
||||||
bg=BG_DARK, fg=ACCENT_PRIMARY)
|
left_frame,
|
||||||
self.count_label.pack(pady=5)
|
text="0 cards",
|
||||||
|
font=FONT_SMALL,
|
||||||
|
text_color=TEXT_MUTED
|
||||||
|
)
|
||||||
|
self.count_label.pack(pady=(5, 5))
|
||||||
|
|
||||||
# Card list (Treeview)
|
# Card list (Treeview wrapped in Frame)
|
||||||
list_frame = tk.Frame(left_frame, bg=BG_DARK)
|
# We use a standard Frame to hold the Treeview because Treeview is a tk widget
|
||||||
list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
|
tree_container = ctk.CTkFrame(left_frame, fg_color="transparent")
|
||||||
|
tree_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
|
||||||
|
|
||||||
self.tree = ttk.Treeview(list_frame, columns=('owned', 'name', 'rarity', 'type'),
|
# Scrollbar
|
||||||
show='tree headings', selectmode='browse',
|
scrollbar = ttk.Scrollbar(tree_container, orient=tk.VERTICAL)
|
||||||
style="CardList.Treeview")
|
|
||||||
|
|
||||||
self.tree.heading('#0', text='')
|
self.tree = ttk.Treeview(
|
||||||
self.tree.column('#0', width=45, anchor='center')
|
tree_container,
|
||||||
|
columns=('owned', 'name', 'rarity', 'type'),
|
||||||
|
show='tree headings',
|
||||||
|
selectmode='browse',
|
||||||
|
style="CardList.Treeview",
|
||||||
|
yscrollcommand=scrollbar.set
|
||||||
|
)
|
||||||
|
scrollbar.config(command=self.tree.yview)
|
||||||
|
|
||||||
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
|
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
self.tree.column('#0', width=100, anchor='center') # Thumbnail column
|
||||||
|
|
||||||
self.tree.heading('owned', text='★', command=lambda: self.sort_column('owned', False))
|
self.tree.heading('owned', text='★', command=lambda: self.sort_column('owned', False))
|
||||||
self.tree.heading('name', text='Name', anchor='w', command=lambda: self.sort_column('name', False))
|
self.tree.heading('name', text='Name', anchor='w', command=lambda: self.sort_column('name', False))
|
||||||
self.tree.heading('rarity', text='Rarity', command=lambda: self.sort_column('rarity', False))
|
self.tree.heading('rarity', text='Rarity', command=lambda: self.sort_column('rarity', False))
|
||||||
self.tree.heading('type', text='Type', command=lambda: self.sort_column('type', False))
|
self.tree.heading('type', text='Type', command=lambda: self.sort_column('type', False))
|
||||||
|
|
||||||
self.tree.column('owned', width=30, anchor='center')
|
self.tree.column('owned', width=50, anchor='center')
|
||||||
self.tree.column('name', width=180, minwidth=150)
|
self.tree.column('name', width=350, minwidth=200)
|
||||||
self.tree.column('rarity', width=55, anchor='center')
|
self.tree.column('rarity', width=80, anchor='center')
|
||||||
self.tree.column('type', width=90, anchor='center')
|
self.tree.column('type', width=100, anchor='center')
|
||||||
|
|
||||||
scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.tree.yview)
|
|
||||||
self.tree.configure(yscrollcommand=scrollbar.set)
|
|
||||||
|
|
||||||
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
||||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
||||||
|
|
||||||
self.tree.bind('<<TreeviewSelect>>', self.on_select)
|
self.tree.bind('<<TreeviewSelect>>', self.on_select)
|
||||||
|
self.tree.tag_configure('owned', background='#1a3a2e') # Kept legacy tag, might need update if theme changes
|
||||||
# Tag for owned cards
|
|
||||||
self.tree.tag_configure('owned', background='#1a3a2e')
|
|
||||||
|
|
||||||
# === Right Panel Contents (Details) ===
|
# === Right Panel Contents (Details) ===
|
||||||
self.create_details_panel()
|
self.create_details_panel()
|
||||||
|
|
||||||
def create_details_panel(self):
|
def create_details_panel(self):
|
||||||
"""Create the card details panel"""
|
"""Create the card details panel"""
|
||||||
# Container with card-like appearance
|
# Container
|
||||||
details_container = tk.Frame(self.details_frame, bg=BG_DARK)
|
details_container = ctk.CTkFrame(self.details_frame, corner_radius=12)
|
||||||
details_container.pack(fill=tk.BOTH, expand=True, padx=15, pady=10)
|
details_container.pack(fill=tk.BOTH, expand=True, padx=0, pady=0) # Flush with right panel
|
||||||
|
|
||||||
# Image area with card frame
|
# Content scrolling container (Optional, but good for small screens)
|
||||||
image_frame = create_card_frame(details_container, padx=10, pady=10)
|
# For now, static
|
||||||
|
|
||||||
|
# Image area
|
||||||
|
# We can't put ctk widgets inside standard frames easily for transparency, so we use ctk frame
|
||||||
|
image_frame = ctk.CTkFrame(details_container, fg_color=BG_MEDIUM, corner_radius=12)
|
||||||
image_frame.pack(pady=10)
|
image_frame.pack(pady=10)
|
||||||
|
|
||||||
self.image_label = tk.Label(image_frame, text="", bg=BG_MEDIUM)
|
self.image_label = ctk.CTkLabel(image_frame, text="", height=180, width=180)
|
||||||
self.image_label.pack(padx=5, pady=5)
|
self.image_label.pack(padx=10, pady=10)
|
||||||
|
|
||||||
# Header with card name
|
# Header with card name
|
||||||
self.detail_name = tk.Label(details_container, text="Select a card",
|
self.detail_name = ctk.CTkLabel(
|
||||||
font=FONT_HEADER, bg=BG_DARK, fg=ACCENT_PRIMARY)
|
details_container,
|
||||||
self.detail_name.pack(pady=(10, 5))
|
text="Select a card",
|
||||||
|
font=(FONT_FAMILY, 24, 'bold'),
|
||||||
|
text_color=ACCENT_PRIMARY
|
||||||
|
)
|
||||||
|
self.detail_name.pack(pady=(0, 2))
|
||||||
|
|
||||||
self.detail_info = tk.Label(details_container, text="",
|
self.detail_info = ctk.CTkLabel(
|
||||||
font=FONT_SMALL, bg=BG_DARK, fg=TEXT_MUTED)
|
details_container,
|
||||||
|
text="",
|
||||||
|
font=FONT_SUBHEADER,
|
||||||
|
text_color=TEXT_MUTED
|
||||||
|
)
|
||||||
self.detail_info.pack()
|
self.detail_info.pack()
|
||||||
|
|
||||||
# Owned checkbox with emphasis
|
# Owned checkbox
|
||||||
owned_frame = tk.Frame(details_container, bg=BG_DARK)
|
owned_frame = ctk.CTkFrame(details_container, fg_color="transparent")
|
||||||
owned_frame.pack(pady=15)
|
owned_frame.pack(pady=10)
|
||||||
|
|
||||||
self.owned_var = tk.BooleanVar(value=False)
|
self.owned_var = tk.BooleanVar(value=False)
|
||||||
self.owned_checkbox = ttk.Checkbutton(owned_frame, text="✨ I Own This Card",
|
self.owned_checkbox = ctk.CTkCheckBox(
|
||||||
variable=self.owned_var,
|
owned_frame,
|
||||||
command=self.toggle_owned,
|
text="✨ I Own This Card",
|
||||||
style='Large.TCheckbutton')
|
variable=self.owned_var,
|
||||||
|
command=self.toggle_owned,
|
||||||
|
font=FONT_HEADER,
|
||||||
|
checkbox_width=28, checkbox_height=28
|
||||||
|
)
|
||||||
self.owned_checkbox.pack(side=tk.LEFT)
|
self.owned_checkbox.pack(side=tk.LEFT)
|
||||||
|
|
||||||
# Level selector with button-based control (no slider)
|
# Level selector
|
||||||
level_frame = tk.Frame(details_container, bg=BG_DARK)
|
level_frame = ctk.CTkFrame(details_container, fg_color="transparent")
|
||||||
level_frame.pack(fill=tk.X, padx=30, pady=10)
|
level_frame.pack(fill=tk.X, padx=40, pady=10)
|
||||||
|
|
||||||
tk.Label(level_frame, text="Card Level:", font=FONT_BODY,
|
ctk.CTkLabel(level_frame, text="Card Level:", font=FONT_SUBHEADER, text_color=TEXT_SECONDARY).pack(side=tk.LEFT)
|
||||||
bg=BG_DARK, fg=TEXT_SECONDARY).pack(side=tk.LEFT)
|
|
||||||
|
|
||||||
# Level display with increment/decrement
|
# Level items
|
||||||
level_ctrl = tk.Frame(level_frame, bg=BG_DARK)
|
level_ctrl = ctk.CTkFrame(level_frame, fg_color="transparent")
|
||||||
level_ctrl.pack(side=tk.LEFT, padx=15)
|
level_ctrl.pack(side=tk.LEFT, padx=30)
|
||||||
|
|
||||||
|
# Decrement button
|
||||||
|
create_styled_button(
|
||||||
|
level_ctrl, text="−",
|
||||||
|
width=36, height=36,
|
||||||
|
command=self.decrement_level
|
||||||
|
).pack(side=tk.LEFT)
|
||||||
|
|
||||||
self.level_var = tk.IntVar(value=50)
|
self.level_var = tk.IntVar(value=50)
|
||||||
self.max_level = 50
|
self.max_level = 50
|
||||||
self.valid_levels = [30, 35, 40, 45, 50] # Default SSR
|
self.valid_levels = [30, 35, 40, 45, 50]
|
||||||
|
|
||||||
# Decrement button
|
self.level_label = ctk.CTkLabel(
|
||||||
dec_btn = tk.Button(level_ctrl, text="−", font=FONT_HEADER,
|
level_ctrl,
|
||||||
bg=BG_LIGHT, fg=TEXT_PRIMARY, bd=0, width=2,
|
text="50", width=60,
|
||||||
activebackground=BG_HIGHLIGHT, cursor='hand2',
|
font=(FONT_FAMILY, 24, 'bold'),
|
||||||
command=self.decrement_level)
|
text_color=ACCENT_PRIMARY
|
||||||
dec_btn.pack(side=tk.LEFT)
|
)
|
||||||
|
self.level_label.pack(side=tk.LEFT, padx=10)
|
||||||
self.level_label = tk.Label(level_ctrl, text="50", width=4, font=FONT_HEADER,
|
|
||||||
bg=BG_MEDIUM, fg=ACCENT_PRIMARY, padx=10)
|
|
||||||
self.level_label.pack(side=tk.LEFT, padx=2)
|
|
||||||
|
|
||||||
# Increment button
|
# Increment button
|
||||||
inc_btn = tk.Button(level_ctrl, text="+", font=FONT_HEADER,
|
create_styled_button(
|
||||||
bg=BG_LIGHT, fg=TEXT_PRIMARY, bd=0, width=2,
|
level_ctrl, text="+",
|
||||||
activebackground=BG_HIGHLIGHT, cursor='hand2',
|
width=36, height=36,
|
||||||
command=self.increment_level)
|
command=self.increment_level
|
||||||
inc_btn.pack(side=tk.LEFT)
|
).pack(side=tk.LEFT)
|
||||||
|
|
||||||
# Quick level buttons container
|
# Quick level buttons
|
||||||
self.level_btn_frame = tk.Frame(level_frame, bg=BG_DARK)
|
self.level_btn_frame = ctk.CTkFrame(level_frame, fg_color="transparent")
|
||||||
self.level_btn_frame.pack(side=tk.LEFT, padx=20)
|
self.level_btn_frame.pack(side=tk.LEFT, padx=20)
|
||||||
|
|
||||||
self.level_buttons = {}
|
self.level_buttons = {}
|
||||||
# Initial population
|
|
||||||
self.update_level_buttons('SSR', 50)
|
self.update_level_buttons('SSR', 50)
|
||||||
|
|
||||||
# Effects display header
|
# Effects display
|
||||||
effects_header = tk.Frame(details_container, bg=BG_DARK)
|
effects_header = ctk.CTkFrame(details_container, fg_color="transparent")
|
||||||
effects_header.pack(fill=tk.X, padx=20, pady=(20, 10))
|
effects_header.pack(fill=tk.X, padx=30, pady=(10, 5))
|
||||||
|
|
||||||
tk.Label(effects_header, text="📊 Effects at Current Level",
|
ctk.CTkLabel(effects_header, text="📊 Effects at Current Level", font=FONT_SUBHEADER).pack(side=tk.LEFT)
|
||||||
font=FONT_SUBHEADER, bg=BG_DARK, fg=TEXT_PRIMARY).pack(side=tk.LEFT)
|
|
||||||
|
|
||||||
# Effects text area with modern styling
|
# Effects text area
|
||||||
effects_frame = create_card_frame(details_container)
|
self.effects_text = create_styled_text(details_container, height=30)
|
||||||
effects_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=(0, 15))
|
self.effects_text.pack(fill=tk.BOTH, expand=True, padx=30, pady=(0, 30))
|
||||||
|
self.effects_text.configure(state="disabled")
|
||||||
self.effects_text = create_styled_text(effects_frame, height=10)
|
|
||||||
self.effects_text.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
|
|
||||||
self.effects_text.config(state=tk.DISABLED)
|
|
||||||
|
|
||||||
def load_cards(self):
|
def load_cards(self):
|
||||||
"""Load all cards from database"""
|
"""Load all cards from database"""
|
||||||
@@ -255,58 +304,37 @@ class CardListFrame(ttk.Frame):
|
|||||||
self.rarity_var.set("All")
|
self.rarity_var.set("All")
|
||||||
self.type_var.set("All")
|
self.type_var.set("All")
|
||||||
self.owned_only_var.set(False)
|
self.owned_only_var.set(False)
|
||||||
# Reset placeholder
|
|
||||||
self.search_entry.delete(0, tk.END)
|
|
||||||
self.search_entry.insert(0, "Search cards...")
|
|
||||||
self.search_entry.config(foreground=TEXT_MUTED)
|
|
||||||
self.filter_cards()
|
self.filter_cards()
|
||||||
|
|
||||||
def _on_search_focus_in(self, event):
|
def filter_cards(self, *args):
|
||||||
"""Clear placeholder on focus"""
|
|
||||||
if self.search_entry.get() == "Search cards...":
|
|
||||||
self.search_entry.delete(0, tk.END)
|
|
||||||
self.search_entry.config(foreground=TEXT_PRIMARY)
|
|
||||||
|
|
||||||
def _on_search_focus_out(self, event):
|
|
||||||
"""Show placeholder if empty"""
|
|
||||||
if not self.search_entry.get():
|
|
||||||
self.search_entry.insert(0, "Search cards...")
|
|
||||||
self.search_entry.config(foreground=TEXT_MUTED)
|
|
||||||
|
|
||||||
def filter_cards(self):
|
|
||||||
"""Filter cards based on search and dropdown values"""
|
"""Filter cards based on search and dropdown values"""
|
||||||
rarity = self.rarity_var.get() if self.rarity_var.get() != "All" else None
|
rarity = self.rarity_var.get() if self.rarity_var.get() != "All" else None
|
||||||
card_type = self.type_var.get() if self.type_var.get() != "All" else None
|
card_type = self.type_var.get() if self.type_var.get() != "All" else None
|
||||||
# Ignore placeholder text
|
|
||||||
search_text = self.search_var.get().strip()
|
search_text = self.search_var.get().strip()
|
||||||
search = search_text if search_text and search_text != "Search cards..." else None
|
search = search_text if search_text else None
|
||||||
owned_only = self.owned_only_var.get()
|
owned_only = self.owned_only_var.get()
|
||||||
|
|
||||||
self.cards = get_all_cards(rarity_filter=rarity, type_filter=card_type,
|
self.cards = get_all_cards(rarity_filter=rarity, type_filter=card_type,
|
||||||
search_term=search, owned_only=owned_only)
|
search_term=search, owned_only=owned_only)
|
||||||
self.populate_tree(self.cards)
|
self.populate_tree(self.cards)
|
||||||
|
self.count_label.configure(text=f"{len(self.cards)} cards")
|
||||||
|
|
||||||
def sort_column(self, col, reverse):
|
def sort_column(self, col, reverse):
|
||||||
"""Sort treeview by column"""
|
"""Sort treeview by column"""
|
||||||
l = [(self.tree.set(k, col), k) for k in self.tree.get_children('')]
|
l = [(self.tree.set(k, col), k) for k in self.tree.get_children('')]
|
||||||
|
|
||||||
# Custom sort logic
|
|
||||||
if col == 'owned':
|
if col == 'owned':
|
||||||
# Sort by star/empty
|
|
||||||
l.sort(key=lambda t: t[0] if t[0] else "", reverse=reverse)
|
l.sort(key=lambda t: t[0] if t[0] else "", reverse=reverse)
|
||||||
elif col == 'rarity':
|
elif col == 'rarity':
|
||||||
# Sort by rarity rank (SSR > SR > R)
|
|
||||||
rarity_map = {'SSR': 3, 'SR': 2, 'R': 1}
|
rarity_map = {'SSR': 3, 'SR': 2, 'R': 1}
|
||||||
l.sort(key=lambda t: rarity_map.get(t[0], 0), reverse=reverse)
|
l.sort(key=lambda t: rarity_map.get(t[0], 0), reverse=reverse)
|
||||||
else:
|
else:
|
||||||
# Default string sort
|
|
||||||
l.sort(reverse=reverse)
|
l.sort(reverse=reverse)
|
||||||
|
|
||||||
# Rearrange items
|
|
||||||
for index, (val, k) in enumerate(l):
|
for index, (val, k) in enumerate(l):
|
||||||
self.tree.move(k, '', index)
|
self.tree.move(k, '', index)
|
||||||
|
|
||||||
# Reverse sort next time
|
|
||||||
self.tree.heading(col, command=lambda: self.sort_column(col, not reverse))
|
self.tree.heading(col, command=lambda: self.sort_column(col, not reverse))
|
||||||
|
|
||||||
def populate_tree(self, cards):
|
def populate_tree(self, cards):
|
||||||
@@ -319,34 +347,34 @@ class CardListFrame(ttk.Frame):
|
|||||||
owned_mark = "★" if is_owned else ""
|
owned_mark = "★" if is_owned else ""
|
||||||
tag = 'owned' if is_owned else ''
|
tag = 'owned' if is_owned else ''
|
||||||
|
|
||||||
# Show level for owned cards
|
|
||||||
display_name = name
|
display_name = name
|
||||||
if is_owned and owned_level:
|
if is_owned and owned_level:
|
||||||
display_name = f"{name} (Lv{owned_level})"
|
display_name = f"{name} (Lv{owned_level})"
|
||||||
|
|
||||||
# Load Icon
|
# Load Icon (keeping simplistic for now)
|
||||||
|
# Treeview images need to be tk.PhotoImage, PIL works
|
||||||
img = self.icon_cache.get(card_id)
|
img = self.icon_cache.get(card_id)
|
||||||
if not img:
|
if not img:
|
||||||
resolved_path = resolve_image_path(image_path)
|
resolved_path = resolve_image_path(image_path)
|
||||||
if resolved_path and os.path.exists(resolved_path):
|
if resolved_path and os.path.exists(resolved_path):
|
||||||
try:
|
try:
|
||||||
pil_img = Image.open(resolved_path)
|
pil_img = Image.open(resolved_path)
|
||||||
pil_img.thumbnail((48, 48), Image.Resampling.LANCZOS)
|
# Resize for treeview (Larger to fill row based on user request)
|
||||||
|
pil_img.thumbnail((78, 78), Image.Resampling.LANCZOS)
|
||||||
img = ImageTk.PhotoImage(pil_img)
|
img = ImageTk.PhotoImage(pil_img)
|
||||||
self.icon_cache[card_id] = img
|
self.icon_cache[card_id] = img
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if img:
|
# Only use image if cached/loaded
|
||||||
self.tree.insert('', tk.END, iid=card_id, text='', image=img,
|
kv = {'image': img} if img else {}
|
||||||
values=(owned_mark, display_name, rarity, f"{type_icon} {card_type}"),
|
|
||||||
tags=(tag,))
|
|
||||||
else:
|
|
||||||
self.tree.insert('', tk.END, iid=card_id, text='',
|
|
||||||
values=(owned_mark, display_name, rarity, f"{type_icon} {card_type}"),
|
|
||||||
tags=(tag,))
|
|
||||||
|
|
||||||
self.count_label.config(text=f"✨ {len(cards)} cards")
|
self.tree.insert('', tk.END, iid=card_id, text='',
|
||||||
|
values=(owned_mark, display_name, rarity, f"{type_icon} {card_type}"),
|
||||||
|
tags=(tag,), **kv)
|
||||||
|
|
||||||
|
if hasattr(self, 'count_label'):
|
||||||
|
self.count_label.configure(text=f"{len(cards)} cards")
|
||||||
|
|
||||||
def on_select(self, event):
|
def on_select(self, event):
|
||||||
"""Handle card selection"""
|
"""Handle card selection"""
|
||||||
@@ -360,41 +388,32 @@ class CardListFrame(ttk.Frame):
|
|||||||
if card:
|
if card:
|
||||||
card_id, name, rarity, card_type, max_level, url, image_path, is_owned, owned_level = card
|
card_id, name, rarity, card_type, max_level, url, image_path, is_owned, owned_level = card
|
||||||
|
|
||||||
# Update owned checkbox
|
|
||||||
self.owned_var.set(bool(is_owned))
|
self.owned_var.set(bool(is_owned))
|
||||||
|
|
||||||
# Load card image if available
|
# Load card image
|
||||||
self.load_card_image(image_path)
|
self.load_card_image(image_path)
|
||||||
|
|
||||||
# Use owned level if owned, otherwise max level or default 50
|
# Level logic
|
||||||
initial_level = owned_level if is_owned and owned_level else max_level
|
initial_level = owned_level if is_owned and owned_level else max_level
|
||||||
|
|
||||||
# Update level controls
|
|
||||||
self.max_level = max_level
|
self.max_level = max_level
|
||||||
self.update_level_buttons(rarity, max_level)
|
self.update_level_buttons(rarity, max_level)
|
||||||
|
|
||||||
# Snap initial level to valid levels
|
|
||||||
if initial_level not in self.valid_levels:
|
if initial_level not in self.valid_levels:
|
||||||
# Find closest or default to max
|
|
||||||
initial_level = max_level
|
initial_level = max_level
|
||||||
|
|
||||||
self.level_var.set(initial_level)
|
self.level_var.set(initial_level)
|
||||||
self.level_label.config(text=str(initial_level))
|
self.level_label.configure(text=str(initial_level))
|
||||||
self.selected_level = initial_level
|
self.selected_level = initial_level
|
||||||
|
|
||||||
# Update details display with colors
|
# Update details text
|
||||||
type_icon = get_type_icon(card_type)
|
type_icon = get_type_icon(card_type)
|
||||||
type_color = get_type_color(card_type)
|
|
||||||
rarity_color = get_rarity_color(rarity)
|
|
||||||
|
|
||||||
self.detail_name.config(text=f"{type_icon} {name}", fg=ACCENT_PRIMARY)
|
self.detail_name.configure(text=f"{type_icon} {name}")
|
||||||
self.detail_info.config(text=f"{rarity} │ {card_type} │ Max Level: {max_level}")
|
self.detail_info.configure(text=f"{rarity} │ {card_type} │ Max Level: {max_level}")
|
||||||
|
|
||||||
# Load effects
|
|
||||||
self.current_card_id = card_id
|
self.current_card_id = card_id
|
||||||
self.update_effects_display()
|
self.update_effects_display()
|
||||||
|
|
||||||
# Notify parent window
|
|
||||||
if self.on_card_selected:
|
if self.on_card_selected:
|
||||||
self.on_card_selected(card_id, name, self.selected_level)
|
self.on_card_selected(card_id, name, self.selected_level)
|
||||||
|
|
||||||
@@ -404,14 +423,14 @@ class CardListFrame(ttk.Frame):
|
|||||||
|
|
||||||
if resolved_path and os.path.exists(resolved_path):
|
if resolved_path and os.path.exists(resolved_path):
|
||||||
try:
|
try:
|
||||||
img = Image.open(resolved_path)
|
img = ctk.CTkImage(light_image=Image.open(resolved_path),
|
||||||
img.thumbnail((130, 130)) # Slightly larger
|
dark_image=Image.open(resolved_path),
|
||||||
self.card_image = ImageTk.PhotoImage(img)
|
size=(180, 180))
|
||||||
self.image_label.config(image=self.card_image)
|
self.image_label.configure(image=img, text="")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.image_label.config(image='', text="[Image not found]")
|
self.image_label.configure(image=None, text="[Image Error]")
|
||||||
else:
|
else:
|
||||||
self.image_label.config(image='', text="")
|
self.image_label.configure(image=None, text="[No Image]")
|
||||||
|
|
||||||
def toggle_owned(self):
|
def toggle_owned(self):
|
||||||
"""Toggle owned status for current card"""
|
"""Toggle owned status for current card"""
|
||||||
@@ -419,15 +438,13 @@ class CardListFrame(ttk.Frame):
|
|||||||
owned = self.owned_var.get()
|
owned = self.owned_var.get()
|
||||||
level = int(self.level_var.get())
|
level = int(self.level_var.get())
|
||||||
set_card_owned(self.current_card_id, owned, level)
|
set_card_owned(self.current_card_id, owned, level)
|
||||||
self.filter_cards() # Refresh list to update owned markers
|
self.filter_cards() # Refresh status icons in tree
|
||||||
|
|
||||||
# Notify parent to refresh stats
|
|
||||||
if self.on_stats_updated:
|
if self.on_stats_updated:
|
||||||
self.on_stats_updated()
|
self.on_stats_updated()
|
||||||
|
|
||||||
def update_level_buttons(self, rarity, max_level):
|
def update_level_buttons(self, rarity, max_level):
|
||||||
"""Update quick level buttons based on rarity/max level"""
|
"""Update quick level buttons"""
|
||||||
# Determine valid levels
|
|
||||||
if max_level == 50: # SSR
|
if max_level == 50: # SSR
|
||||||
self.valid_levels = [30, 35, 40, 45, 50]
|
self.valid_levels = [30, 35, 40, 45, 50]
|
||||||
elif max_level == 45: # SR
|
elif max_level == 45: # SR
|
||||||
@@ -445,8 +462,8 @@ class CardListFrame(ttk.Frame):
|
|||||||
btn = create_styled_button(self.level_btn_frame, text=f"Lv{lvl}",
|
btn = create_styled_button(self.level_btn_frame, text=f"Lv{lvl}",
|
||||||
command=lambda l=lvl: self.set_level(l),
|
command=lambda l=lvl: self.set_level(l),
|
||||||
style_type='default')
|
style_type='default')
|
||||||
btn.config(width=5, padx=6, pady=3, font=FONT_SMALL)
|
btn.configure(width=45, height=36, font=FONT_BODY_BOLD)
|
||||||
btn.pack(side=tk.LEFT, padx=2)
|
btn.pack(side=tk.LEFT, padx=3)
|
||||||
self.level_buttons[lvl] = btn
|
self.level_buttons[lvl] = btn
|
||||||
|
|
||||||
def set_level(self, level):
|
def set_level(self, level):
|
||||||
@@ -454,10 +471,9 @@ class CardListFrame(ttk.Frame):
|
|||||||
if self.current_card_id:
|
if self.current_card_id:
|
||||||
self.selected_level = level
|
self.selected_level = level
|
||||||
self.level_var.set(level)
|
self.level_var.set(level)
|
||||||
self.level_label.config(text=str(level))
|
self.level_label.configure(text=str(level))
|
||||||
self.update_effects_display()
|
self.update_effects_display()
|
||||||
|
|
||||||
# Notify parent window about level change
|
|
||||||
if self.on_card_selected:
|
if self.on_card_selected:
|
||||||
card = get_card_by_id(self.current_card_id)
|
card = get_card_by_id(self.current_card_id)
|
||||||
if card:
|
if card:
|
||||||
@@ -466,41 +482,24 @@ class CardListFrame(ttk.Frame):
|
|||||||
# Save level if owned
|
# Save level if owned
|
||||||
if self.current_card_id and self.owned_var.get():
|
if self.current_card_id and self.owned_var.get():
|
||||||
update_owned_card_level(self.current_card_id, level)
|
update_owned_card_level(self.current_card_id, level)
|
||||||
self.update_tree_item_level(self.current_card_id, level)
|
# Refresh just this item if possible, or full refresh
|
||||||
|
# self.filter_cards() # Too heavy? logic needs to be robust
|
||||||
|
pass # Tree update happens on next filter or refresh
|
||||||
|
|
||||||
def increment_level(self):
|
def increment_level(self):
|
||||||
"""Increase level to next valid step"""
|
|
||||||
current = self.level_var.get()
|
current = self.level_var.get()
|
||||||
# Find next level in valid_levels
|
|
||||||
for lvl in self.valid_levels:
|
for lvl in self.valid_levels:
|
||||||
if lvl > current:
|
if lvl > current:
|
||||||
self.set_level(lvl)
|
self.set_level(lvl)
|
||||||
return
|
return
|
||||||
|
|
||||||
def decrement_level(self):
|
def decrement_level(self):
|
||||||
"""Decrease level to previous valid step"""
|
|
||||||
current = self.level_var.get()
|
current = self.level_var.get()
|
||||||
# Find previous level in valid_levels
|
|
||||||
for lvl in reversed(self.valid_levels):
|
for lvl in reversed(self.valid_levels):
|
||||||
if lvl < current:
|
if lvl < current:
|
||||||
self.set_level(lvl)
|
self.set_level(lvl)
|
||||||
return
|
return
|
||||||
|
|
||||||
def update_tree_item_level(self, card_id, level):
|
|
||||||
"""Update visible name in tree without full reload"""
|
|
||||||
if self.tree.exists(card_id):
|
|
||||||
current_values = self.tree.item(card_id, 'values')
|
|
||||||
if current_values:
|
|
||||||
# current_values is a tuple: (owned_mark, name, rarity, type)
|
|
||||||
# We need to strip existing " (LvXX)" from name if present
|
|
||||||
name = current_values[1]
|
|
||||||
base_name = name.split(" (Lv")[0]
|
|
||||||
new_name = f"{base_name} (Lv{level})"
|
|
||||||
|
|
||||||
# Make new values tuple preserving other columns
|
|
||||||
new_values = (current_values[0], new_name, current_values[2], current_values[3])
|
|
||||||
self.tree.item(card_id, values=new_values)
|
|
||||||
|
|
||||||
def update_effects_display(self):
|
def update_effects_display(self):
|
||||||
"""Update the effects display for current card and level"""
|
"""Update the effects display for current card and level"""
|
||||||
if not self.current_card_id:
|
if not self.current_card_id:
|
||||||
@@ -509,21 +508,29 @@ class CardListFrame(ttk.Frame):
|
|||||||
level = int(self.level_var.get())
|
level = int(self.level_var.get())
|
||||||
effects = get_effects_at_level(self.current_card_id, level)
|
effects = get_effects_at_level(self.current_card_id, level)
|
||||||
|
|
||||||
self.effects_text.config(state=tk.NORMAL)
|
self.effects_text.configure(state="normal")
|
||||||
self.effects_text.delete('1.0', tk.END)
|
self.effects_text.delete('1.0', tk.END)
|
||||||
|
|
||||||
# Configure tags for styling
|
# Note: CTkTextbox tags are minimal (no foreground color support per tag as detailed as tk usually)
|
||||||
self.effects_text.tag_configure('header', font=FONT_SUBHEADER, foreground=ACCENT_PRIMARY)
|
# But we can try basic insert.
|
||||||
self.effects_text.tag_configure('highlight', foreground=ACCENT_SUCCESS)
|
# CTkTextbox does not support color tags in the same way `tag_configure` does for Text.
|
||||||
self.effects_text.tag_configure('effect_name', foreground=TEXT_SECONDARY)
|
# It's a limitation. We might have to stick to plain text or use the adapter to return a tk.Text if we strictly need color.
|
||||||
self.effects_text.tag_configure('effect_value', foreground=TEXT_PRIMARY)
|
# However, `create_styled_text` in theme.py is now returning a CTkTextbox.
|
||||||
self.effects_text.tag_configure('effect_tooltip', underline=False)
|
# If we need Rich Text, we might need to revert `create_styled_text` to use tk.Text but styled for Dark mode.
|
||||||
|
# Let's check `theme.py` again. I defined `create_styled_text` as returning `CTkTextbox`.
|
||||||
|
# CTkTextbox is good for uniform text. If we lost coloring, that's a trade-off for the UI look.
|
||||||
|
# OR: We can use `tk.Text` inside a `ctkContainer` to keep coloring.
|
||||||
|
# Let's assume for now we just want the text content.
|
||||||
|
|
||||||
|
# For better UX, let's revert create_styled_text to use tk.Text because we really needed those highlights (+20% etc).
|
||||||
|
# Actually, let's just format it nicely.
|
||||||
|
|
||||||
if effects:
|
if effects:
|
||||||
self.effects_text.insert(tk.END, f"━━━ Level {level} ━━━\n\n", 'header')
|
self.effects_text.insert(tk.END, f"━━━ Level {level} ━━━\n\n")
|
||||||
for name, value in effects:
|
for name, value in effects:
|
||||||
# Highlight high values
|
# Basic formatting
|
||||||
prefix = ""
|
prefix = ""
|
||||||
|
# Logic for starring high values
|
||||||
if '%' in str(value):
|
if '%' in str(value):
|
||||||
try:
|
try:
|
||||||
num = int(str(value).replace('%', '').replace('+', ''))
|
num = int(str(value).replace('%', '').replace('+', ''))
|
||||||
@@ -531,46 +538,10 @@ class CardListFrame(ttk.Frame):
|
|||||||
prefix = "★ "
|
prefix = "★ "
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
if prefix:
|
|
||||||
self.effects_text.insert(tk.END, prefix, 'highlight')
|
|
||||||
|
|
||||||
# Insert effect name with tooltip tag
|
self.effects_text.insert(tk.END, f"{prefix}{name}: {value}\n")
|
||||||
tag_name = f"tooltip_{name.replace(' ', '_')}"
|
|
||||||
self.effects_text.insert(tk.END, f"{name}: ", ('effect_name', tag_name))
|
|
||||||
|
|
||||||
# Bind tooltip events
|
|
||||||
self.effects_text.tag_bind(tag_name, "<Enter>", lambda e, n=name: self.show_effect_tooltip(e, n))
|
|
||||||
self.effects_text.tag_bind(tag_name, "<Leave>", self.hide_effect_tooltip)
|
|
||||||
|
|
||||||
self.effects_text.insert(tk.END, f"{value}\n", 'effect_value')
|
|
||||||
else:
|
else:
|
||||||
self.effects_text.insert(tk.END, f"No effects data for Level {level}\n\n")
|
self.effects_text.insert(tk.END, f"No effects data for Level {level}\n\nAvailable levels: {self.valid_levels}")
|
||||||
self.effects_text.insert(tk.END, "Available levels: 1, 25, 40, 50\n", 'effect_name')
|
|
||||||
|
|
||||||
self.effects_text.config(state=tk.DISABLED)
|
self.effects_text.configure(state="disabled")
|
||||||
|
|
||||||
def show_effect_tooltip(self, event, effect_name):
|
|
||||||
"""Show tooltip for effect"""
|
|
||||||
if effect_name in EFFECT_DESCRIPTIONS:
|
|
||||||
text = EFFECT_DESCRIPTIONS[effect_name]
|
|
||||||
x = event.x_root + 15
|
|
||||||
y = event.y_root + 10
|
|
||||||
|
|
||||||
# Close existing if any
|
|
||||||
self.hide_effect_tooltip(None)
|
|
||||||
|
|
||||||
self.tooltip_window = tk.Toplevel(self)
|
|
||||||
self.tooltip_window.wm_overrideredirect(True)
|
|
||||||
self.tooltip_window.wm_geometry(f"+{x}+{y}")
|
|
||||||
|
|
||||||
label = tk.Label(self.tooltip_window, text=text, justify=tk.LEFT,
|
|
||||||
background=BG_LIGHT, foreground=TEXT_PRIMARY,
|
|
||||||
relief=tk.SOLID, borderwidth=1, font=FONT_SMALL,
|
|
||||||
padx=10, pady=5, wraplength=250)
|
|
||||||
label.pack()
|
|
||||||
|
|
||||||
def hide_effect_tooltip(self, event):
|
|
||||||
"""Hide tooltip"""
|
|
||||||
if hasattr(self, 'tooltip_window') and self.tooltip_window:
|
|
||||||
self.tooltip_window.destroy()
|
|
||||||
self.tooltip_window = None
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
"""
|
"""
|
||||||
Deck Builder Frame
|
Deck Builder Frame
|
||||||
Build decks with 6 cards and view combined effects with breakdown
|
Build decks with 6 cards and view combined effects with breakdown
|
||||||
|
Updated for CustomTkinter
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, messagebox
|
from tkinter import ttk, messagebox
|
||||||
|
import customtkinter as ctk
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from PIL import Image, ImageTk
|
from PIL import Image, ImageTk
|
||||||
@@ -27,10 +29,10 @@ from gui.theme import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CardSlot(tk.Frame):
|
class CardSlot(ctk.CTkFrame):
|
||||||
"""Visual component for a single card slot"""
|
"""Visual component for a single card slot"""
|
||||||
def __init__(self, parent, index, remove_callback, level_callback):
|
def __init__(self, parent, index, remove_callback, level_callback):
|
||||||
super().__init__(parent, bg=BG_MEDIUM, highlightthickness=2, highlightbackground=BG_LIGHT)
|
super().__init__(parent, fg_color="transparent", border_width=2, border_color=BG_LIGHT, corner_radius=8)
|
||||||
self.index = index
|
self.index = index
|
||||||
self.remove_callback = remove_callback
|
self.remove_callback = remove_callback
|
||||||
self.level_callback = level_callback
|
self.level_callback = level_callback
|
||||||
@@ -39,158 +41,165 @@ class CardSlot(tk.Frame):
|
|||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
|
|
||||||
def setup_ui(self):
|
def setup_ui(self):
|
||||||
# Configure grid weight
|
# Configure grid
|
||||||
self.columnconfigure(1, weight=1)
|
self.columnconfigure(0, weight=1)
|
||||||
|
|
||||||
# Slot number indicator
|
# Slot number indicator (Overlay)
|
||||||
slot_label = tk.Label(self, text=f"#{self.index + 1}", font=FONT_TINY,
|
self.slot_label = ctk.CTkLabel(self, text=f"#{self.index + 1}", font=FONT_TINY,
|
||||||
bg=BG_LIGHT, fg=TEXT_MUTED, padx=4, pady=2)
|
fg_color="#000000", text_color="#ffffff", corner_radius=4, height=18, width=24)
|
||||||
slot_label.place(x=2, y=2)
|
self.slot_label.place(x=4, y=4)
|
||||||
|
|
||||||
# Image Area (Left)
|
# Image Area - Dominant
|
||||||
self.image_label = tk.Label(self, bg=BG_MEDIUM, text="📭", fg=TEXT_MUTED,
|
# Initial placeholder
|
||||||
font=('Segoe UI', 32))
|
self.image_label = ctk.CTkLabel(self, fg_color="transparent", text="📭", text_color=TEXT_MUTED,
|
||||||
self.image_label.grid(row=0, column=0, rowspan=3, padx=12, pady=12)
|
font=('Segoe UI', 32), width=90, height=90)
|
||||||
|
self.image_label.grid(row=0, column=0, padx=5, pady=(5,0))
|
||||||
|
|
||||||
# Details Area (Right)
|
# Mini Details Area (Below Image)
|
||||||
self.name_label = tk.Label(self, text="Empty Slot", bg=BG_MEDIUM, fg=TEXT_PRIMARY,
|
self.info_frame = ctk.CTkFrame(self, fg_color="transparent")
|
||||||
font=FONT_BODY_BOLD, anchor='w', wraplength=180) # Increased wrap
|
self.info_frame.grid(row=1, column=0, sticky='ew', padx=4, pady=4)
|
||||||
self.name_label.grid(row=0, column=1, sticky='w', padx=8, pady=(15, 0))
|
self.info_frame.columnconfigure(0, weight=1)
|
||||||
|
|
||||||
self.meta_label = tk.Label(self, text="", bg=BG_MEDIUM, fg=TEXT_MUTED,
|
self.name_label = ctk.CTkLabel(self.info_frame, text="Empty", fg_color="transparent", text_color=TEXT_MUTED,
|
||||||
font=FONT_SMALL, anchor='w')
|
font=FONT_TINY, anchor='center', height=16)
|
||||||
self.meta_label.grid(row=1, column=1, sticky='w', padx=8)
|
self.name_label.grid(row=0, column=0, sticky='ew')
|
||||||
|
|
||||||
# Controls (Bottom Right)
|
# Controls Overlay (Bottom)
|
||||||
ctrl_frame = tk.Frame(self, bg=BG_MEDIUM)
|
self.ctrl_frame = ctk.CTkFrame(self.info_frame, fg_color="transparent")
|
||||||
ctrl_frame.grid(row=2, column=1, sticky='ew', padx=8, pady=8)
|
self.ctrl_frame.grid(row=1, column=0, sticky='ew', pady=(2,0))
|
||||||
|
|
||||||
# Level Selector
|
|
||||||
tk.Label(ctrl_frame, text="Lv:", bg=BG_MEDIUM, fg=TEXT_MUTED,
|
|
||||||
font=FONT_SMALL).pack(side=tk.LEFT)
|
|
||||||
|
|
||||||
|
# Level Selector (Compact)
|
||||||
self.level_var = tk.StringVar(value="50")
|
self.level_var = tk.StringVar(value="50")
|
||||||
self.level_combo = ttk.Combobox(ctrl_frame, textvariable=self.level_var,
|
self.level_combo = ctk.CTkComboBox(self.ctrl_frame, variable=self.level_var,
|
||||||
values=[], width=4, state='readonly')
|
values=[], width=55, height=22, font=FONT_TINY, state='readonly', command=self._on_level_change)
|
||||||
self.level_combo.pack(side=tk.LEFT, padx=4)
|
self.level_combo.pack(side=tk.LEFT, padx=2)
|
||||||
self.level_combo.bind('<<ComboboxSelected>>', self._on_level_change)
|
|
||||||
|
|
||||||
# Remove Button
|
# Remove Button (Compact)
|
||||||
self.remove_btn = tk.Button(ctrl_frame, text="✕", bg=BG_LIGHT, fg=ACCENT_ERROR,
|
self.remove_btn = ctk.CTkButton(self.ctrl_frame, text="✕", fg_color=BG_LIGHT, text_color=ACCENT_ERROR,
|
||||||
bd=0, font=FONT_BODY_BOLD, width=2,
|
font=FONT_BODY_BOLD, width=22, height=22,
|
||||||
activebackground=ACCENT_ERROR, activeforeground=TEXT_PRIMARY,
|
hover_color=BG_HIGHLIGHT,
|
||||||
cursor='hand2',
|
|
||||||
command=lambda: self.remove_callback(self.index))
|
command=lambda: self.remove_callback(self.index))
|
||||||
self.remove_btn.pack(side=tk.RIGHT)
|
# Pack later
|
||||||
|
|
||||||
# Hide controls initially
|
# Hide controls initially
|
||||||
self.toggle_controls(False)
|
self.toggle_controls(False)
|
||||||
|
|
||||||
def toggle_controls(self, visible):
|
def toggle_controls(self, visible):
|
||||||
state = 'normal' if visible else 'disabled'
|
state = 'normal' if visible else 'disabled'
|
||||||
self.level_combo.config(state='readonly' if visible else 'disabled')
|
self.level_combo.configure(state='readonly' if visible else 'disabled')
|
||||||
if not visible:
|
if not visible:
|
||||||
self.remove_btn.pack_forget()
|
self.remove_btn.pack_forget()
|
||||||
else:
|
else:
|
||||||
self.remove_btn.pack(side=tk.RIGHT)
|
self.remove_btn.pack(side=tk.RIGHT, padx=2)
|
||||||
|
|
||||||
def set_card(self, card_data):
|
def set_card(self, card_data):
|
||||||
"""Set card data: (id, name, rarity, type, image_path, level)"""
|
"""Set card data"""
|
||||||
if not card_data:
|
if not card_data:
|
||||||
self.reset()
|
self.reset()
|
||||||
return
|
return
|
||||||
|
|
||||||
card_id, name, rarity, card_type, image_path, level = card_data
|
card_id, name, rarity, card_type, image_path, level = card_data
|
||||||
|
|
||||||
# Calculate valid levels based on rarity
|
# Calculate valid levels
|
||||||
if rarity == 'SSR':
|
if rarity == 'SSR':
|
||||||
valid_levels = [50, 45, 40, 35, 30]
|
valid_levels = [50, 45, 40, 35, 30]
|
||||||
max_lvl = 50
|
max_lvl = 50
|
||||||
elif rarity == 'SR':
|
elif rarity == 'SR':
|
||||||
valid_levels = [45, 40, 35, 30, 25]
|
valid_levels = [45, 40, 35, 30, 25]
|
||||||
max_lvl = 45
|
max_lvl = 45
|
||||||
else: # R
|
else:
|
||||||
valid_levels = [40, 35, 30, 25, 20]
|
valid_levels = [40, 35, 30, 25, 20]
|
||||||
max_lvl = 40
|
max_lvl = 40
|
||||||
|
|
||||||
self.level_combo['values'] = [str(l) for l in valid_levels]
|
self.level_combo.configure(values=[str(l) for l in valid_levels])
|
||||||
|
if level not in valid_levels: level = max_lvl
|
||||||
|
|
||||||
# Snap level to valid value if not present (e.g. old data)
|
|
||||||
if level not in valid_levels:
|
|
||||||
level = max_lvl
|
|
||||||
|
|
||||||
# Update styling based on type
|
|
||||||
color = get_type_color(card_type)
|
color = get_type_color(card_type)
|
||||||
type_icon = get_type_icon(card_type)
|
|
||||||
|
|
||||||
self.name_label.config(text=name, fg=TEXT_PRIMARY)
|
# Truncate strictly
|
||||||
self.meta_label.config(text=f"{type_icon} {rarity} │ {card_type}", fg=color)
|
display_name = name if len(name) < 15 else name[:12] + "..."
|
||||||
self.level_var.set(str(level))
|
self.name_label.configure(text=display_name, text_color=TEXT_PRIMARY)
|
||||||
|
self.level_combo.set(str(level))
|
||||||
|
|
||||||
# Update border color based on rarity
|
|
||||||
rarity_borders = {'SSR': '#ffd700', 'SR': '#c0c0c0', 'R': '#cd853f'}
|
rarity_borders = {'SSR': '#ffd700', 'SR': '#c0c0c0', 'R': '#cd853f'}
|
||||||
self.config(highlightbackground=rarity_borders.get(rarity, BG_LIGHT))
|
self.configure(border_color=rarity_borders.get(rarity, BG_LIGHT))
|
||||||
|
|
||||||
# Load Image
|
|
||||||
self._load_image(image_path)
|
self._load_image(image_path)
|
||||||
|
|
||||||
self.toggle_controls(True)
|
self.toggle_controls(True)
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self.name_label.config(text="Empty Slot", fg=TEXT_MUTED)
|
self.name_label.configure(text="Empty", text_color=TEXT_MUTED)
|
||||||
self.meta_label.config(text="Click a card to add")
|
|
||||||
self.image_label.config(image='', text="📭", font=('Segoe UI', 32))
|
# Recreate label to avoid TclError with missing images
|
||||||
self.config(highlightbackground=BG_LIGHT)
|
if hasattr(self, 'image_label') and self.image_label:
|
||||||
|
self.image_label.destroy()
|
||||||
|
|
||||||
|
self.image_label = ctk.CTkLabel(self, fg_color="transparent", text="📭", text_color=TEXT_MUTED,
|
||||||
|
font=('Segoe UI', 32), width=90, height=90)
|
||||||
|
self.image_label.grid(row=0, column=0, padx=5, pady=(5,0))
|
||||||
|
|
||||||
|
self.configure(border_color=BG_LIGHT)
|
||||||
self.image_ref = None
|
self.image_ref = None
|
||||||
self.toggle_controls(False)
|
self.toggle_controls(False)
|
||||||
|
|
||||||
def _load_image(self, path):
|
def _load_image(self, path):
|
||||||
resolved_path = resolve_image_path(path)
|
resolved_path = resolve_image_path(path)
|
||||||
|
|
||||||
|
# Prepare new image first
|
||||||
|
new_image = None
|
||||||
if resolved_path and os.path.exists(resolved_path):
|
if resolved_path and os.path.exists(resolved_path):
|
||||||
try:
|
try:
|
||||||
pil_img = Image.open(resolved_path)
|
pil_img = Image.open(resolved_path)
|
||||||
# Significantly larger images as requested (120x120)
|
pil_img.thumbnail((90, 90), Image.Resampling.LANCZOS)
|
||||||
pil_img.thumbnail((120, 120), Image.Resampling.LANCZOS)
|
new_image = ctk.CTkImage(light_image=pil_img, dark_image=pil_img, size=(90, 90))
|
||||||
self.image_ref = ImageTk.PhotoImage(pil_img)
|
except Exception:
|
||||||
self.image_label.config(image=self.image_ref, text='')
|
pass
|
||||||
except Exception as e:
|
|
||||||
print(f"Failed to load image: {e}")
|
# Recreate label
|
||||||
self.image_label.config(image='', text="⚠️")
|
if hasattr(self, 'image_label') and self.image_label:
|
||||||
|
self.image_label.destroy()
|
||||||
|
|
||||||
|
if new_image:
|
||||||
|
self.image_ref = new_image # Keep ref
|
||||||
|
self.image_label = ctk.CTkLabel(self, fg_color="transparent", text="", image=new_image)
|
||||||
else:
|
else:
|
||||||
self.image_label.config(image='', text="🖼️")
|
self.image_ref = None
|
||||||
|
self.image_label = ctk.CTkLabel(self, fg_color="transparent", text="⚠️" if resolved_path else "🖼️",
|
||||||
|
text_color=TEXT_MUTED, font=('Segoe UI', 32), width=90, height=90)
|
||||||
|
|
||||||
def _on_level_change(self, event):
|
self.image_label.grid(row=0, column=0, padx=5, pady=(5,0))
|
||||||
self.level_callback(self.index, int(self.level_var.get()))
|
|
||||||
|
def _on_level_change(self, value):
|
||||||
|
# CTkComboBox calls command with value
|
||||||
|
self.level_callback(self.index, int(value))
|
||||||
|
|
||||||
|
|
||||||
class DeckBuilderFrame(ttk.Frame):
|
class DeckBuilderFrame(ctk.CTkFrame):
|
||||||
"""Deck builder with combined effects breakdown"""
|
"""Deck builder with combined effects breakdown"""
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
super().__init__(parent)
|
super().__init__(parent, fg_color="transparent")
|
||||||
self.current_deck_id = None
|
self.current_deck_id = None
|
||||||
self.deck_slots = [None] * 6 # 6 card slots
|
self.deck_slots = [None] * 6 # 6 card slots
|
||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
self.refresh_decks()
|
self.refresh_decks()
|
||||||
|
|
||||||
def setup_ui(self):
|
def setup_ui(self):
|
||||||
# Main container with split view
|
# Main container with split view (simulated with frames)
|
||||||
main_split = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
|
|
||||||
main_split.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|
||||||
|
|
||||||
# === Left Panel: Card Browser ===
|
# === Left Panel: Card Browser ===
|
||||||
left_panel = ttk.Frame(main_split)
|
left_panel = ctk.CTkFrame(self, width=350, corner_radius=10)
|
||||||
main_split.add(left_panel, weight=1)
|
left_panel.pack(side=tk.LEFT, fill=tk.BOTH, expand=False, padx=(0, 10), pady=10)
|
||||||
|
|
||||||
# Header
|
# Header
|
||||||
header = tk.Frame(left_panel, bg=BG_DARK)
|
header = ctk.CTkFrame(left_panel, fg_color="transparent")
|
||||||
header.pack(fill=tk.X, pady=(0, 10))
|
header.pack(fill=tk.X, pady=(15, 10), padx=10)
|
||||||
tk.Label(header, text="📋 Available Cards", font=FONT_SUBHEADER,
|
ctk.CTkLabel(header, text="📋 Available Cards", font=FONT_SUBHEADER,
|
||||||
bg=BG_DARK, fg=TEXT_PRIMARY).pack(side=tk.LEFT)
|
text_color=TEXT_PRIMARY).pack(side=tk.LEFT)
|
||||||
|
|
||||||
# Filters
|
# Filters
|
||||||
filter_frame = tk.Frame(left_panel, bg=BG_DARK)
|
filter_frame = ctk.CTkFrame(left_panel, fg_color="transparent")
|
||||||
filter_frame.pack(fill=tk.X, pady=(0, 8))
|
filter_frame.pack(fill=tk.X, pady=(0, 8), padx=10)
|
||||||
|
|
||||||
# Filters - Initialize vars FIRST
|
# Filters - Initialize vars FIRST
|
||||||
self.type_var = tk.StringVar(value="All")
|
self.type_var = tk.StringVar(value="All")
|
||||||
@@ -198,32 +207,31 @@ class DeckBuilderFrame(ttk.Frame):
|
|||||||
self.search_var = tk.StringVar()
|
self.search_var = tk.StringVar()
|
||||||
|
|
||||||
# Search Entry
|
# Search Entry
|
||||||
self.search_entry = ttk.Entry(filter_frame, textvariable=self.search_var, width=18)
|
self.search_entry = ctk.CTkEntry(filter_frame, textvariable=self.search_var, width=120, placeholder_text="Search...")
|
||||||
self.search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 8))
|
self.search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
|
||||||
|
|
||||||
# Placeholder behavior (before trace)
|
# Bind key release for search
|
||||||
self.search_entry.insert(0, "Search...")
|
self.search_entry.bind('<KeyRelease>', lambda e: self.filter_cards())
|
||||||
self.search_entry.config(foreground=TEXT_MUTED)
|
|
||||||
self.search_entry.bind('<FocusIn>', self._on_search_focus_in)
|
|
||||||
self.search_entry.bind('<FocusOut>', self._on_search_focus_out)
|
|
||||||
|
|
||||||
# Add trace AFTER placeholder is set
|
|
||||||
self.search_var.trace('w', lambda *args: self.filter_cards())
|
|
||||||
|
|
||||||
types = ["All", "Speed", "Stamina", "Power", "Guts", "Wisdom", "Friend", "Group"]
|
types = ["All", "Speed", "Stamina", "Power", "Guts", "Wisdom", "Friend", "Group"]
|
||||||
type_combo = ttk.Combobox(filter_frame, textvariable=self.type_var,
|
type_combo = ctk.CTkComboBox(filter_frame, variable=self.type_var,
|
||||||
values=types, width=9, state='readonly')
|
values=types, width=90, state='readonly', command=lambda e: self.filter_cards())
|
||||||
type_combo.pack(side=tk.LEFT)
|
type_combo.pack(side=tk.LEFT)
|
||||||
type_combo.bind('<<ComboboxSelected>>', lambda e: self.filter_cards())
|
|
||||||
|
|
||||||
ttk.Checkbutton(filter_frame, text="Owned", variable=self.owned_only_var,
|
ctk.CTkCheckBox(filter_frame, text="Owned", variable=self.owned_only_var,
|
||||||
command=self.filter_cards).pack(side=tk.LEFT, padx=8)
|
command=self.filter_cards, checkbox_width=24, checkbox_height=24, font=FONT_SMALL).pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
# Card List
|
# Add Button (Packed first to stick to bottom)
|
||||||
list_frame = tk.Frame(left_panel, bg=BG_DARK)
|
add_btn = create_styled_button(left_panel, text="➕ Add to Deck",
|
||||||
list_frame.pack(fill=tk.BOTH, expand=True)
|
command=self.add_selected_to_deck,
|
||||||
|
style_type='accent')
|
||||||
|
add_btn.pack(side=tk.BOTTOM, fill=tk.X, pady=10, padx=10)
|
||||||
|
|
||||||
self.card_tree = ttk.Treeview(list_frame, columns=('name', 'rarity', 'type'),
|
# Card List Treeview
|
||||||
|
list_container = ctk.CTkFrame(left_panel, fg_color=BG_MEDIUM)
|
||||||
|
list_container.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
|
||||||
|
|
||||||
|
self.card_tree = ttk.Treeview(list_container, columns=('name', 'rarity', 'type'),
|
||||||
show='tree headings', style="DeckList.Treeview")
|
show='tree headings', style="DeckList.Treeview")
|
||||||
self.card_tree.heading('#0', text='')
|
self.card_tree.heading('#0', text='')
|
||||||
self.card_tree.column('#0', width=45, anchor='center')
|
self.card_tree.column('#0', width=45, anchor='center')
|
||||||
@@ -235,69 +243,67 @@ class DeckBuilderFrame(ttk.Frame):
|
|||||||
self.card_tree.column('rarity', width=45, anchor='center')
|
self.card_tree.column('rarity', width=45, anchor='center')
|
||||||
self.card_tree.column('type', width=65, anchor='center')
|
self.card_tree.column('type', width=65, anchor='center')
|
||||||
|
|
||||||
scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.card_tree.yview)
|
scrollbar = ttk.Scrollbar(list_container, orient=tk.VERTICAL, command=self.card_tree.yview)
|
||||||
self.card_tree.configure(yscrollcommand=scrollbar.set)
|
self.card_tree.configure(yscrollcommand=scrollbar.set)
|
||||||
|
|
||||||
self.card_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
self.card_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=5)
|
||||||
|
|
||||||
# Double-click to add
|
# Double-click to add
|
||||||
self.card_tree.bind('<Double-1>', lambda e: self.add_selected_to_deck())
|
self.card_tree.bind('<Double-1>', lambda e: self.add_selected_to_deck())
|
||||||
|
|
||||||
# Add Button
|
|
||||||
add_btn = create_styled_button(left_panel, text="➕ Add to Deck",
|
|
||||||
command=self.add_selected_to_deck,
|
|
||||||
style_type='accent')
|
|
||||||
add_btn.pack(fill=tk.X, pady=10)
|
|
||||||
|
|
||||||
# === Right Panel: Deck & Stats ===
|
# === Right Panel: Deck & Stats ===
|
||||||
right_panel = ttk.Frame(main_split)
|
right_panel = ctk.CTkFrame(self, fg_color="transparent")
|
||||||
main_split.add(right_panel, weight=2)
|
right_panel.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, pady=10)
|
||||||
|
|
||||||
# Deck Controls
|
# Deck Controls
|
||||||
deck_ctrl = tk.Frame(right_panel, bg=BG_DARK)
|
deck_ctrl = ctk.CTkFrame(right_panel, fg_color="transparent")
|
||||||
deck_ctrl.pack(fill=tk.X, pady=(0, 15))
|
deck_ctrl.pack(fill=tk.X, pady=(0, 10)) # Reduced padding
|
||||||
|
|
||||||
tk.Label(deck_ctrl, text="🎴 Current Deck:", font=FONT_BODY,
|
ctk.CTkLabel(deck_ctrl, text="🎴 Current Deck:", font=FONT_BODY,
|
||||||
bg=BG_DARK, fg=TEXT_SECONDARY).pack(side=tk.LEFT)
|
text_color=TEXT_SECONDARY).pack(side=tk.LEFT)
|
||||||
self.deck_combo = ttk.Combobox(deck_ctrl, width=25, state='readonly')
|
|
||||||
|
self.deck_combo = ctk.CTkComboBox(deck_ctrl, width=200, state='readonly', command=self.on_deck_selected_val)
|
||||||
self.deck_combo.pack(side=tk.LEFT, padx=10)
|
self.deck_combo.pack(side=tk.LEFT, padx=10)
|
||||||
self.deck_combo.bind('<<ComboboxSelected>>', self.on_deck_selected)
|
|
||||||
|
|
||||||
ttk.Button(deck_ctrl, text="+ New", command=self.create_new_deck,
|
create_styled_button(deck_ctrl, text="+ New", command=self.create_new_deck, width=60).pack(side=tk.LEFT, padx=5)
|
||||||
style='Small.TButton').pack(side=tk.LEFT, padx=5)
|
|
||||||
ttk.Button(deck_ctrl, text="🗑️ Delete", command=self.delete_current_deck,
|
# Delete button - danger style
|
||||||
style='Small.TButton').pack(side=tk.LEFT)
|
del_btn = ctk.CTkButton(deck_ctrl, text="🗑️ Delete", command=self.delete_current_deck,
|
||||||
|
fg_color=BG_LIGHT, hover_color=ACCENT_ERROR, text_color=ACCENT_ERROR, width=80)
|
||||||
|
del_btn.pack(side=tk.LEFT)
|
||||||
|
|
||||||
# Card count indicator
|
# Card count indicator
|
||||||
self.deck_count_label = tk.Label(deck_ctrl, text="0/6 cards",
|
self.deck_count_label = ctk.CTkLabel(deck_ctrl, text="0/6 cards",
|
||||||
font=FONT_SMALL, bg=BG_DARK, fg=ACCENT_PRIMARY)
|
font=FONT_SMALL, text_color=ACCENT_PRIMARY)
|
||||||
self.deck_count_label.pack(side=tk.LEFT, padx=15)
|
self.deck_count_label.pack(side=tk.LEFT, padx=15)
|
||||||
|
|
||||||
# Deck Grid (3x2)
|
# Deck Grid (3x2) - Scrollable if needed, but 6 cards fit fine.
|
||||||
self.slots_frame = tk.Frame(right_panel, bg=BG_DARK)
|
# We use a frame for the grid
|
||||||
|
self.slots_frame = ctk.CTkFrame(right_panel, fg_color="transparent")
|
||||||
self.slots_frame.pack(fill=tk.X)
|
self.slots_frame.pack(fill=tk.X)
|
||||||
|
|
||||||
self.card_slots = []
|
self.card_slots = []
|
||||||
for i in range(6):
|
for i in range(6):
|
||||||
slot = CardSlot(self.slots_frame, i, self.remove_from_slot, self.on_slot_level_changed)
|
slot = CardSlot(self.slots_frame, i, self.remove_from_slot, self.on_slot_level_changed)
|
||||||
r, c = divmod(i, 3)
|
r, c = divmod(i, 3)
|
||||||
slot.grid(row=r, column=c, padx=6, pady=6, sticky='nsew')
|
slot.grid(row=r, column=c, padx=4, pady=4, sticky='nsew')
|
||||||
self.slots_frame.columnconfigure(c, weight=1)
|
self.slots_frame.columnconfigure(c, weight=1)
|
||||||
self.card_slots.append(slot)
|
self.card_slots.append(slot)
|
||||||
|
|
||||||
# Stats / Effects Area
|
# Stats / Effects Area
|
||||||
effects_header = tk.Frame(right_panel, bg=BG_DARK)
|
effects_header = ctk.CTkFrame(right_panel, fg_color="transparent")
|
||||||
effects_header.pack(fill=tk.X, pady=(20, 10))
|
effects_header.pack(fill=tk.X, pady=(10, 5)) # Reduced padding
|
||||||
tk.Label(effects_header, text="📊 Combined Effects Breakdown",
|
ctk.CTkLabel(effects_header, text="📊 Combined Effects Breakdown",
|
||||||
font=FONT_SUBHEADER, bg=BG_DARK, fg=TEXT_PRIMARY).pack(side=tk.LEFT)
|
font=FONT_SUBHEADER, text_color=TEXT_PRIMARY).pack(side=tk.LEFT)
|
||||||
|
|
||||||
effects_frame = create_card_frame(right_panel)
|
# Effects Tree Container
|
||||||
effects_frame.pack(fill=tk.BOTH, expand=True)
|
effects_container = ctk.CTkFrame(right_panel, fg_color=BG_MEDIUM)
|
||||||
|
effects_container.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
self.effects_tree = ttk.Treeview(effects_frame,
|
self.effects_tree = ttk.Treeview(effects_container,
|
||||||
columns=('effect', 'total', 'c1', 'c2', 'c3', 'c4', 'c5', 'c6'),
|
columns=('effect', 'total', 'c1', 'c2', 'c3', 'c4', 'c5', 'c6'),
|
||||||
show='headings', height=8)
|
show='headings', height=6) # Reduced Height
|
||||||
|
|
||||||
self.effects_tree.heading('effect', text='Effect')
|
self.effects_tree.heading('effect', text='Effect')
|
||||||
self.effects_tree.heading('total', text='TOTAL')
|
self.effects_tree.heading('total', text='TOTAL')
|
||||||
@@ -308,42 +314,30 @@ class DeckBuilderFrame(ttk.Frame):
|
|||||||
self.effects_tree.heading(f'c{i}', text=f'#{i}')
|
self.effects_tree.heading(f'c{i}', text=f'#{i}')
|
||||||
self.effects_tree.column(f'c{i}', width=45, anchor='center')
|
self.effects_tree.column(f'c{i}', width=45, anchor='center')
|
||||||
|
|
||||||
vsb = ttk.Scrollbar(effects_frame, orient=tk.VERTICAL, command=self.effects_tree.yview)
|
vsb = ttk.Scrollbar(effects_container, orient=tk.VERTICAL, command=self.effects_tree.yview)
|
||||||
self.effects_tree.configure(yscrollcommand=vsb.set)
|
self.effects_tree.configure(yscrollcommand=vsb.set)
|
||||||
|
|
||||||
self.effects_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=2, pady=2)
|
self.effects_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
vsb.pack(side=tk.RIGHT, fill=tk.Y, pady=2)
|
vsb.pack(side=tk.RIGHT, fill=tk.Y, pady=5)
|
||||||
|
|
||||||
# Unique Effects Area
|
# Unique Effects Area
|
||||||
unique_header = tk.Frame(right_panel, bg=BG_DARK)
|
unique_header = ctk.CTkFrame(right_panel, fg_color="transparent")
|
||||||
unique_header.pack(fill=tk.X, pady=(15, 8))
|
unique_header.pack(fill=tk.X, pady=(10, 5))
|
||||||
tk.Label(unique_header, text="✨ Unique Effects", font=FONT_BODY_BOLD,
|
ctk.CTkLabel(unique_header, text="✨ Unique Effects", font=FONT_BODY_BOLD,
|
||||||
bg=BG_DARK, fg=ACCENT_SECONDARY).pack(side=tk.LEFT)
|
text_color=ACCENT_SECONDARY).pack(side=tk.LEFT)
|
||||||
|
|
||||||
unique_frame = create_card_frame(right_panel)
|
unique_frame = ctk.CTkFrame(right_panel, fg_color=BG_MEDIUM)
|
||||||
unique_frame.pack(fill=tk.X)
|
unique_frame.pack(fill=tk.X)
|
||||||
|
|
||||||
self.unique_text = create_styled_text(unique_frame, height=5)
|
self.unique_text = ctk.CTkTextbox(unique_frame, height=60, fg_color=BG_MEDIUM, text_color=TEXT_PRIMARY) # Reduced Height
|
||||||
self.unique_text.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
|
self.unique_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
self.unique_text.config(state=tk.DISABLED)
|
self.unique_text.configure(state=tk.DISABLED)
|
||||||
|
|
||||||
self.icon_cache = {}
|
self.icon_cache = {}
|
||||||
self.filter_cards()
|
# Initial call to populate list if wanted, or wait for event loop
|
||||||
|
self.after(100, self.filter_cards) # Delay slightly to ensure widget readiness
|
||||||
|
|
||||||
|
|
||||||
# Helper methods for placeholder
|
|
||||||
def _on_search_focus_in(self, event):
|
|
||||||
"""Clear placeholder on focus"""
|
|
||||||
if self.search_entry.get() == "Search...":
|
|
||||||
self.search_entry.delete(0, tk.END)
|
|
||||||
self.search_entry.config(foreground=TEXT_PRIMARY)
|
|
||||||
|
|
||||||
def _on_search_focus_out(self, event):
|
|
||||||
"""Show placeholder if empty"""
|
|
||||||
if not self.search_entry.get():
|
|
||||||
self.search_entry.insert(0, "Search...")
|
|
||||||
self.search_entry.config(foreground=TEXT_MUTED)
|
|
||||||
|
|
||||||
# --- Logic Methods ---
|
# --- Logic Methods ---
|
||||||
|
|
||||||
def filter_cards(self):
|
def filter_cards(self):
|
||||||
@@ -352,15 +346,22 @@ class DeckBuilderFrame(ttk.Frame):
|
|||||||
|
|
||||||
type_filter = self.type_var.get() if self.type_var.get() != "All" else None
|
type_filter = self.type_var.get() if self.type_var.get() != "All" else None
|
||||||
|
|
||||||
# Ignore placeholder
|
# Search var comes from CTkEntry textvariable
|
||||||
search_text = self.search_var.get()
|
search_text = self.search_var.get()
|
||||||
search = search_text if search_text and search_text != "Search..." else None
|
search = search_text if search_text else None
|
||||||
|
|
||||||
owned_only = self.owned_only_var.get()
|
owned_only = self.owned_only_var.get()
|
||||||
|
|
||||||
cards = get_all_cards(type_filter=type_filter, search_term=search, owned_only=owned_only)
|
cards = get_all_cards(type_filter=type_filter, search_term=search, owned_only=owned_only)
|
||||||
|
|
||||||
|
# Limit to 100 cards to prevent UI lag if showing all
|
||||||
|
# (Optimization)
|
||||||
|
|
||||||
|
count = 0
|
||||||
for card in cards:
|
for card in cards:
|
||||||
|
if count > 200: break # soft limit
|
||||||
|
count += 1
|
||||||
|
|
||||||
card_id, name, rarity, card_type, max_level, image_path, is_owned, owned_level = card
|
card_id, name, rarity, card_type, max_level, image_path, is_owned, owned_level = card
|
||||||
|
|
||||||
# Load Icon
|
# Load Icon
|
||||||
@@ -370,8 +371,8 @@ class DeckBuilderFrame(ttk.Frame):
|
|||||||
if not img and resolved_path and os.path.exists(resolved_path):
|
if not img and resolved_path and os.path.exists(resolved_path):
|
||||||
try:
|
try:
|
||||||
pil_img = Image.open(resolved_path)
|
pil_img = Image.open(resolved_path)
|
||||||
# Larger thumbnails in the list too (48x48)
|
# Larger thumbnails in the list too (32x32 for list)
|
||||||
pil_img.thumbnail((48, 48), Image.Resampling.LANCZOS)
|
pil_img.thumbnail((32, 32), Image.Resampling.LANCZOS)
|
||||||
img = ImageTk.PhotoImage(pil_img)
|
img = ImageTk.PhotoImage(pil_img)
|
||||||
self.icon_cache[card_id] = img
|
self.icon_cache[card_id] = img
|
||||||
except:
|
except:
|
||||||
@@ -387,15 +388,17 @@ class DeckBuilderFrame(ttk.Frame):
|
|||||||
|
|
||||||
def refresh_decks(self):
|
def refresh_decks(self):
|
||||||
decks = get_all_decks()
|
decks = get_all_decks()
|
||||||
self.deck_combo['values'] = [f"{d[0]}: {d[1]}" for d in decks]
|
values = [f"{d[0]}: {d[1]}" for d in decks]
|
||||||
if decks and not self.current_deck_id:
|
self.deck_combo.configure(values=values)
|
||||||
self.deck_combo.current(0)
|
if values and not self.current_deck_id:
|
||||||
self.on_deck_selected(None)
|
self.deck_combo.set(values[0])
|
||||||
|
self.on_deck_selected_val(values[0])
|
||||||
|
elif not values:
|
||||||
|
self.deck_combo.set('')
|
||||||
|
|
||||||
def on_deck_selected(self, event):
|
def on_deck_selected_val(self, value):
|
||||||
selection = self.deck_combo.get()
|
if value:
|
||||||
if selection:
|
self.current_deck_id = int(value.split(':')[0])
|
||||||
self.current_deck_id = int(selection.split(':')[0])
|
|
||||||
self.load_deck()
|
self.load_deck()
|
||||||
|
|
||||||
def load_deck(self):
|
def load_deck(self):
|
||||||
@@ -435,7 +438,11 @@ class DeckBuilderFrame(ttk.Frame):
|
|||||||
self.current_deck_id = None
|
self.current_deck_id = None
|
||||||
self.deck_combo.set('')
|
self.deck_combo.set('')
|
||||||
self.refresh_decks()
|
self.refresh_decks()
|
||||||
self.load_deck()
|
# Clear slots
|
||||||
|
for s in self.card_slots: s.reset()
|
||||||
|
self.deck_slots = [None] * 6
|
||||||
|
self.update_deck_count()
|
||||||
|
self.update_effects_breakdown()
|
||||||
|
|
||||||
def add_selected_to_deck(self):
|
def add_selected_to_deck(self):
|
||||||
if not self.current_deck_id:
|
if not self.current_deck_id:
|
||||||
@@ -458,11 +465,12 @@ class DeckBuilderFrame(ttk.Frame):
|
|||||||
# Get the last selected level for this card from main window
|
# Get the last selected level for this card from main window
|
||||||
level = 50
|
level = 50
|
||||||
parent = self.winfo_toplevel()
|
parent = self.winfo_toplevel()
|
||||||
if hasattr(parent, 'last_selected_levels'):
|
# Try to access main window state (depends on how it's linked, usually via parent or global state)
|
||||||
level = parent.last_selected_levels.get(card_id, 50)
|
# We can't easily access MainWindow instance from here unless passed down.
|
||||||
|
# Assuming default max level for now or 50.
|
||||||
|
|
||||||
add_card_to_deck(self.current_deck_id, card_id, i, level)
|
add_card_to_deck(self.current_deck_id, card_id, i, level)
|
||||||
self.load_deck()
|
self.load_deck() # Reloads everything
|
||||||
return
|
return
|
||||||
|
|
||||||
messagebox.showinfo("Deck Full", "Remove a card first to add a new one.")
|
messagebox.showinfo("Deck Full", "Remove a card first to add a new one.")
|
||||||
@@ -478,7 +486,7 @@ class DeckBuilderFrame(ttk.Frame):
|
|||||||
def update_deck_count(self):
|
def update_deck_count(self):
|
||||||
"""Update the X/6 cards display"""
|
"""Update the X/6 cards display"""
|
||||||
count = sum(1 for slot in self.deck_slots if slot is not None)
|
count = sum(1 for slot in self.deck_slots if slot is not None)
|
||||||
self.deck_count_label.config(text=f"{count}/6 cards")
|
self.deck_count_label.configure(text=f"{count}/6 cards")
|
||||||
|
|
||||||
def on_slot_level_changed(self, index, new_level):
|
def on_slot_level_changed(self, index, new_level):
|
||||||
if self.current_deck_id and self.deck_slots[index]:
|
if self.current_deck_id and self.deck_slots[index]:
|
||||||
@@ -491,12 +499,12 @@ class DeckBuilderFrame(ttk.Frame):
|
|||||||
self.effects_tree.delete(item)
|
self.effects_tree.delete(item)
|
||||||
|
|
||||||
# Clear Unique Text
|
# Clear Unique Text
|
||||||
self.unique_text.config(state=tk.NORMAL)
|
self.unique_text.configure(state=tk.NORMAL)
|
||||||
self.unique_text.delete('1.0', tk.END)
|
self.unique_text.delete('1.0', tk.END)
|
||||||
|
|
||||||
if not self.current_deck_id:
|
if not self.current_deck_id:
|
||||||
self.unique_text.insert(tk.END, "No deck selected")
|
self.unique_text.insert(tk.END, "No deck selected")
|
||||||
self.unique_text.config(state=tk.DISABLED)
|
self.unique_text.configure(state=tk.DISABLED)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Prepare data for calculation
|
# Prepare data for calculation
|
||||||
@@ -515,6 +523,7 @@ class DeckBuilderFrame(ttk.Frame):
|
|||||||
for i, info in enumerate(card_info):
|
for i, info in enumerate(card_info):
|
||||||
if info:
|
if info:
|
||||||
card_id, level = info
|
card_id, level = info
|
||||||
|
# Get name from slot label
|
||||||
card_name = self.card_slots[i].name_label.cget("text")
|
card_name = self.card_slots[i].name_label.cget("text")
|
||||||
|
|
||||||
effects = get_effects_at_level(card_id, level)
|
effects = get_effects_at_level(card_id, level)
|
||||||
@@ -527,15 +536,12 @@ class DeckBuilderFrame(ttk.Frame):
|
|||||||
all_effects[name] = [''] * 6
|
all_effects[name] = [''] * 6
|
||||||
all_effects[name][i] = value
|
all_effects[name][i] = value
|
||||||
|
|
||||||
# Configure tags
|
|
||||||
self.unique_text.tag_configure('card_name', foreground=ACCENT_PRIMARY)
|
|
||||||
|
|
||||||
# Fill Unique Effects
|
# Fill Unique Effects
|
||||||
if unique_effects_list:
|
if unique_effects_list:
|
||||||
self.unique_text.insert(tk.END, "\n".join(unique_effects_list))
|
self.unique_text.insert(tk.END, "\n".join(unique_effects_list))
|
||||||
else:
|
else:
|
||||||
self.unique_text.insert(tk.END, "No unique effects in this deck", 'card_name')
|
self.unique_text.insert(tk.END, "No unique effects in this deck")
|
||||||
self.unique_text.config(state=tk.DISABLED)
|
self.unique_text.configure(state=tk.DISABLED)
|
||||||
|
|
||||||
# Sum totals
|
# Sum totals
|
||||||
for effect_name, values in sorted(all_effects.items()):
|
for effect_name, values in sorted(all_effects.items()):
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
Deck Skills View - Detailed breakdown of all skills in a deck or for a single card
|
Deck Skills View - Detailed breakdown of all skills in a deck or for a single card
|
||||||
|
Updated for CustomTkinter
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
|
import customtkinter as ctk
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from PIL import Image, ImageTk
|
from PIL import Image, ImageTk
|
||||||
@@ -24,11 +26,11 @@ from gui.theme import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DeckSkillsFrame(ttk.Frame):
|
class DeckSkillsFrame(ctk.CTkFrame):
|
||||||
"""Frame for viewing combined skills of a deck or individual cards"""
|
"""Frame for viewing combined skills of a deck or individual cards"""
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
super().__init__(parent)
|
super().__init__(parent, fg_color="transparent")
|
||||||
self.icon_cache = {}
|
self.icon_cache = {}
|
||||||
self.current_mode = "Deck" # or "Single"
|
self.current_mode = "Deck" # or "Single"
|
||||||
|
|
||||||
@@ -38,31 +40,35 @@ class DeckSkillsFrame(ttk.Frame):
|
|||||||
def create_widgets(self):
|
def create_widgets(self):
|
||||||
"""Create the deck skills interface"""
|
"""Create the deck skills interface"""
|
||||||
# Header / Controls
|
# Header / Controls
|
||||||
ctrl_frame = tk.Frame(self, bg=BG_DARK)
|
ctrl_frame = ctk.CTkFrame(self, fg_color="transparent")
|
||||||
ctrl_frame.pack(fill=tk.X, padx=20, pady=15)
|
ctrl_frame.pack(fill=tk.X, padx=20, pady=15)
|
||||||
|
|
||||||
# Left side: Mode/Deck selection
|
# Left side: Mode/Deck selection
|
||||||
selection_frame = tk.Frame(ctrl_frame, bg=BG_DARK)
|
selection_frame = ctk.CTkFrame(ctrl_frame, fg_color="transparent")
|
||||||
selection_frame.pack(side=tk.LEFT)
|
selection_frame.pack(side=tk.LEFT)
|
||||||
|
|
||||||
tk.Label(selection_frame, text="🎴 Select Deck:", font=FONT_BODY,
|
ctk.CTkLabel(selection_frame, text="🎴 Select Deck:", font=FONT_BODY,
|
||||||
bg=BG_DARK, fg=TEXT_SECONDARY).pack(side=tk.LEFT)
|
text_color=TEXT_SECONDARY).pack(side=tk.LEFT)
|
||||||
|
|
||||||
self.deck_combo = ttk.Combobox(selection_frame, width=30, state='readonly')
|
self.deck_combo = ctk.CTkComboBox(selection_frame, width=200, state='readonly',
|
||||||
|
command=self.on_deck_selected_val)
|
||||||
self.deck_combo.pack(side=tk.LEFT, padx=10)
|
self.deck_combo.pack(side=tk.LEFT, padx=10)
|
||||||
self.deck_combo.bind('<<ComboboxSelected>>', self.on_deck_selected)
|
|
||||||
|
|
||||||
# Mode indicator/Description
|
# Mode indicator/Description
|
||||||
self.mode_label = tk.Label(ctrl_frame, text="Showing skills for selected deck",
|
self.mode_label = ctk.CTkLabel(ctrl_frame, text="Showing skills for selected deck",
|
||||||
font=FONT_HEADER, bg=BG_DARK, fg=ACCENT_PRIMARY)
|
font=FONT_HEADER, text_color=ACCENT_PRIMARY)
|
||||||
self.mode_label.pack(side=tk.RIGHT)
|
self.mode_label.pack(side=tk.RIGHT)
|
||||||
|
|
||||||
# Main Results Tree
|
# Main Results Tree
|
||||||
tree_container = create_card_frame(self)
|
tree_container = ctk.CTkFrame(self, fg_color="transparent")
|
||||||
tree_container.pack(fill=tk.BOTH, expand=True, padx=20, pady=(0, 15))
|
tree_container.pack(fill=tk.BOTH, expand=True, padx=20, pady=(0, 15))
|
||||||
|
|
||||||
|
# Treeview inner frame
|
||||||
|
tree_inner = ctk.CTkFrame(tree_container, fg_color="transparent")
|
||||||
|
tree_inner.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||||
|
|
||||||
cols = ('skill', 'rarity', 'source', 'details')
|
cols = ('skill', 'rarity', 'source', 'details')
|
||||||
self.tree = ttk.Treeview(tree_container, columns=cols, show='tree headings',
|
self.tree = ttk.Treeview(tree_inner, columns=cols, show='tree headings',
|
||||||
style="Treeview")
|
style="Treeview")
|
||||||
|
|
||||||
self.tree.heading('#0', text='★ Card / Skill')
|
self.tree.heading('#0', text='★ Card / Skill')
|
||||||
@@ -77,37 +83,41 @@ class DeckSkillsFrame(ttk.Frame):
|
|||||||
self.tree.column('source', width=100)
|
self.tree.column('source', width=100)
|
||||||
self.tree.column('details', width=450)
|
self.tree.column('details', width=450)
|
||||||
|
|
||||||
scrollbar = ttk.Scrollbar(tree_container, orient=tk.VERTICAL, command=self.tree.yview)
|
scrollbar = ttk.Scrollbar(tree_inner, orient=tk.VERTICAL, command=self.tree.yview)
|
||||||
self.tree.configure(yscrollcommand=scrollbar.set)
|
self.tree.configure(yscrollcommand=scrollbar.set)
|
||||||
|
|
||||||
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=2, pady=2)
|
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=2, pady=2)
|
||||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=2)
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=2)
|
||||||
|
|
||||||
# Footer
|
# Footer
|
||||||
self.stats_label = tk.Label(self, text="", font=FONT_SMALL,
|
self.stats_label = ctk.CTkLabel(self, text="", font=FONT_SMALL,
|
||||||
bg=BG_DARK, fg=TEXT_MUTED)
|
text_color=TEXT_MUTED)
|
||||||
self.stats_label.pack(anchor='e', pady=5, padx=20)
|
self.stats_label.pack(anchor='e', pady=5, padx=20)
|
||||||
|
|
||||||
def refresh_decks(self):
|
def refresh_decks(self):
|
||||||
"""Load decks into combobox"""
|
"""Load decks into combobox"""
|
||||||
decks = get_all_decks()
|
decks = get_all_decks()
|
||||||
self.deck_combo['values'] = [f"{d[0]}: {d[1]}" for d in decks]
|
values = [f"{d[0]}: {d[1]}" for d in decks]
|
||||||
if decks:
|
self.deck_combo.configure(values=values)
|
||||||
self.deck_combo.current(0)
|
if values:
|
||||||
self.on_deck_selected(None)
|
self.deck_combo.set(values[0])
|
||||||
|
self.on_deck_selected_val(values[0])
|
||||||
|
|
||||||
def on_deck_selected(self, event):
|
def on_deck_selected_val(self, value):
|
||||||
"""Handle deck selection"""
|
"""Handle deck selection from combobox values"""
|
||||||
selection = self.deck_combo.get()
|
if not value: return
|
||||||
if not selection: return
|
|
||||||
|
|
||||||
deck_id = int(selection.split(':')[0])
|
deck_id = int(value.split(':')[0])
|
||||||
deck_name = selection.split(': ')[1]
|
deck_name = value.split(': ')[1]
|
||||||
|
|
||||||
self.current_mode = "Deck"
|
self.current_mode = "Deck"
|
||||||
self.mode_label.config(text=f"Deck: {deck_name}", fg=ACCENT_PRIMARY)
|
self.mode_label.configure(text=f"Deck: {deck_name}", text_color=ACCENT_PRIMARY)
|
||||||
self.show_deck_skills(deck_id)
|
self.show_deck_skills(deck_id)
|
||||||
|
|
||||||
|
def on_deck_selected(self, event):
|
||||||
|
"""Legacy bind handler if needed"""
|
||||||
|
self.on_deck_selected_val(self.deck_combo.get())
|
||||||
|
|
||||||
def show_deck_skills(self, deck_id):
|
def show_deck_skills(self, deck_id):
|
||||||
"""Fetch and display all skills from a deck"""
|
"""Fetch and display all skills from a deck"""
|
||||||
# Clear tree
|
# Clear tree
|
||||||
@@ -116,7 +126,7 @@ class DeckSkillsFrame(ttk.Frame):
|
|||||||
|
|
||||||
deck_cards = get_deck_cards(deck_id)
|
deck_cards = get_deck_cards(deck_id)
|
||||||
if not deck_cards:
|
if not deck_cards:
|
||||||
self.stats_label.config(text="Deck is empty")
|
self.stats_label.configure(text="Deck is empty")
|
||||||
return
|
return
|
||||||
|
|
||||||
total_skills = 0
|
total_skills = 0
|
||||||
@@ -142,7 +152,7 @@ class DeckSkillsFrame(ttk.Frame):
|
|||||||
self.add_skill_row(parent_id, event['skill_name'], "Event", event['details'])
|
self.add_skill_row(parent_id, event['skill_name'], "Event", event['details'])
|
||||||
total_skills += 1
|
total_skills += 1
|
||||||
|
|
||||||
self.stats_label.config(text=f"Found {total_skills} total skill sources in deck")
|
self.stats_label.configure(text=f"Found {total_skills} total skill sources in deck")
|
||||||
|
|
||||||
def add_card_node(self, card_id, owned_mark, name, rarity, card_type, image_path):
|
def add_card_node(self, card_id, owned_mark, name, rarity, card_type, image_path):
|
||||||
"""Add a parent node for a card"""
|
"""Add a parent node for a card"""
|
||||||
@@ -152,7 +162,7 @@ class DeckSkillsFrame(ttk.Frame):
|
|||||||
if resolved_path and os.path.exists(resolved_path):
|
if resolved_path and os.path.exists(resolved_path):
|
||||||
try:
|
try:
|
||||||
pil_img = Image.open(resolved_path)
|
pil_img = Image.open(resolved_path)
|
||||||
pil_img.thumbnail((48, 48), Image.Resampling.LANCZOS)
|
pil_img.thumbnail((32, 32), Image.Resampling.LANCZOS)
|
||||||
img = ImageTk.PhotoImage(pil_img)
|
img = ImageTk.PhotoImage(pil_img)
|
||||||
self.icon_cache[card_id] = img
|
self.icon_cache[card_id] = img
|
||||||
except: pass
|
except: pass
|
||||||
@@ -185,7 +195,7 @@ class DeckSkillsFrame(ttk.Frame):
|
|||||||
card_id, name, rarity, card_type, max_level, url, image_path, is_owned, owned_level = card
|
card_id, name, rarity, card_type, max_level, url, image_path, is_owned, owned_level = card
|
||||||
|
|
||||||
self.current_mode = "Single"
|
self.current_mode = "Single"
|
||||||
self.mode_label.config(text=f"Card: {name}", fg=ACCENT_SECONDARY)
|
self.mode_label.configure(text=f"Card: {name}", text_color=ACCENT_SECONDARY)
|
||||||
|
|
||||||
# Clear tree
|
# Clear tree
|
||||||
for item in self.tree.get_children():
|
for item in self.tree.get_children():
|
||||||
@@ -209,4 +219,4 @@ class DeckSkillsFrame(ttk.Frame):
|
|||||||
self.add_skill_row(parent_id, event['skill_name'], "Event", event['details'])
|
self.add_skill_row(parent_id, event['skill_name'], "Event", event['details'])
|
||||||
total_skills += 1
|
total_skills += 1
|
||||||
|
|
||||||
self.stats_label.config(text=f"Showing {total_skills} skill sources for {name}")
|
self.stats_label.configure(text=f"Showing {total_skills} skill sources for {name}")
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
|
import customtkinter as ctk
|
||||||
from db.db_queries import get_deck_bonus
|
from db.db_queries import get_deck_bonus
|
||||||
|
from gui.theme import BG_MEDIUM, TEXT_PRIMARY
|
||||||
|
|
||||||
class DeckView(tk.Toplevel):
|
class DeckView(ctk.CTkToplevel):
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.title("Deck Builder")
|
self.title("Deck Builder")
|
||||||
@@ -9,9 +11,9 @@ class DeckView(tk.Toplevel):
|
|||||||
|
|
||||||
self.deck_id = 1 # Default deck
|
self.deck_id = 1 # Default deck
|
||||||
|
|
||||||
tk.Button(self, text="Calculate Deck Bonuses", command=self.calculate).pack(pady=10)
|
ctk.CTkButton(self, text="Calculate Deck Bonuses", command=self.calculate).pack(pady=10)
|
||||||
self.output = tk.Text(self, height=20)
|
self.output = ctk.CTkTextbox(self, height=300, fg_color=BG_MEDIUM, text_color=TEXT_PRIMARY)
|
||||||
self.output.pack(fill=tk.BOTH, expand=True)
|
self.output.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||||
|
|
||||||
def calculate(self):
|
def calculate(self):
|
||||||
self.output.delete("1.0", tk.END)
|
self.output.delete("1.0", tk.END)
|
||||||
|
|||||||
@@ -1,298 +1,180 @@
|
|||||||
"""
|
"""
|
||||||
Effects View - Display support effects at all levels with interactive slider
|
Effects Search View - Search for effects across all owned cards
|
||||||
|
Updated for CustomTkinter
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, messagebox
|
from tkinter import ttk, messagebox
|
||||||
|
import customtkinter as ctk
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
from PIL import Image, ImageTk # Added missing import
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
from db.db_queries import get_all_effects, get_effects_at_level, get_unique_effect_names, get_card_by_id
|
from db.db_queries import search_owned_effects
|
||||||
from gui.theme import (
|
from gui.theme import (
|
||||||
BG_DARK, BG_MEDIUM, BG_LIGHT, BG_HIGHLIGHT,
|
BG_DARK, BG_MEDIUM, BG_LIGHT, BG_HIGHLIGHT,
|
||||||
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_SUCCESS, ACCENT_TERTIARY,
|
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_SUCCESS, ACCENT_TERTIARY,
|
||||||
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
|
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
|
||||||
FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL, FONT_MONO,
|
FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL,
|
||||||
create_styled_button, create_styled_text, create_card_frame,
|
create_styled_button, create_styled_entry
|
||||||
EFFECT_DESCRIPTIONS
|
|
||||||
)
|
)
|
||||||
|
from utils import resolve_image_path
|
||||||
|
|
||||||
|
class EffectsFrame(ctk.CTkFrame):
|
||||||
class EffectsFrame(ttk.Frame):
|
"""Frame for searching effects across owned cards"""
|
||||||
"""Frame for viewing support effects at different levels"""
|
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
super().__init__(parent)
|
super().__init__(parent, fg_color="transparent")
|
||||||
self.current_card_id = None
|
self.icon_cache = {}
|
||||||
self.current_card_name = None
|
|
||||||
self.max_level = 50
|
|
||||||
|
|
||||||
self.create_widgets()
|
self.create_widgets()
|
||||||
|
|
||||||
def create_widgets(self):
|
def create_widgets(self):
|
||||||
"""Create the effects view interface"""
|
"""Create the effects search interface"""
|
||||||
# Header
|
# Header / Search Bar
|
||||||
header_frame = tk.Frame(self, bg=BG_DARK)
|
header_frame = ctk.CTkFrame(self, fg_color="transparent")
|
||||||
header_frame.pack(fill=tk.X, padx=20, pady=15)
|
header_frame.pack(fill=tk.X, padx=20, pady=15)
|
||||||
|
|
||||||
self.card_label = tk.Label(header_frame, text="📊 Select a card from the Card List tab",
|
# Search container
|
||||||
font=FONT_HEADER, bg=BG_DARK, fg=ACCENT_PRIMARY)
|
search_container = ctk.CTkFrame(header_frame, fg_color="transparent")
|
||||||
self.card_label.pack(side=tk.LEFT)
|
search_container.pack(fill=tk.X)
|
||||||
|
|
||||||
# Legend Button
|
ctk.CTkLabel(search_container, text="🔍 Search Effect:",
|
||||||
legend_btn = create_styled_button(header_frame, text="❓ Legend",
|
font=FONT_HEADER, text_color=TEXT_PRIMARY).pack(side=tk.LEFT, padx=(0, 10))
|
||||||
command=self.show_legend, style_type='default')
|
|
||||||
legend_btn.config(font=FONT_SMALL, padx=10, pady=4)
|
|
||||||
legend_btn.pack(side=tk.RIGHT)
|
|
||||||
|
|
||||||
# Level control frame
|
self.search_var = tk.StringVar()
|
||||||
control_frame = tk.Frame(self, bg=BG_MEDIUM, padx=15, pady=12)
|
self.search_entry = ctk.CTkEntry(search_container, textvariable=self.search_var, width=300)
|
||||||
control_frame.pack(fill=tk.X, padx=20)
|
self.search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
|
||||||
|
self.search_entry.bind('<Return>', lambda e: self.perform_search())
|
||||||
|
|
||||||
# Level label
|
search_btn = create_styled_button(search_container, text="Search",
|
||||||
tk.Label(control_frame, text="Level:", font=FONT_BODY,
|
command=self.perform_search, style_type='accent')
|
||||||
bg=BG_MEDIUM, fg=TEXT_SECONDARY).pack(side=tk.LEFT)
|
search_btn.pack(side=tk.LEFT)
|
||||||
|
|
||||||
# Level display with increment/decrement buttons
|
# Example/Help text
|
||||||
level_ctrl = tk.Frame(control_frame, bg=BG_MEDIUM)
|
help_frame = ctk.CTkFrame(header_frame, fg_color="transparent")
|
||||||
level_ctrl.pack(side=tk.LEFT, padx=15)
|
help_frame.pack(fill=tk.X, pady=(5, 0))
|
||||||
|
ctk.CTkLabel(help_frame, text="Examples: Friendship, Motivation, Race Bonus, Skill Pt",
|
||||||
|
font=FONT_SMALL, text_color=TEXT_MUTED).pack(side=tk.LEFT)
|
||||||
|
|
||||||
# Decrement button
|
# Results Area
|
||||||
dec_btn = tk.Button(level_ctrl, text="−", font=FONT_HEADER,
|
results_container = ctk.CTkFrame(self, fg_color="transparent")
|
||||||
bg=BG_LIGHT, fg=TEXT_PRIMARY, bd=0, width=2,
|
results_container.pack(fill=tk.BOTH, expand=True, padx=20, pady=(0, 20))
|
||||||
activebackground=BG_HIGHLIGHT, cursor='hand2',
|
|
||||||
command=self.decrement_level)
|
|
||||||
dec_btn.pack(side=tk.LEFT)
|
|
||||||
|
|
||||||
self.level_var = tk.IntVar(value=50)
|
# Label for the frame
|
||||||
self.level_display = tk.Label(level_ctrl, text="50", width=4, font=FONT_HEADER,
|
ctk.CTkLabel(results_container, text="Search Results (Owned Cards)",
|
||||||
bg=BG_LIGHT, fg=ACCENT_PRIMARY, padx=10)
|
font=FONT_SUBHEADER, text_color=ACCENT_PRIMARY).pack(pady=(10, 5))
|
||||||
self.level_display.pack(side=tk.LEFT, padx=2)
|
|
||||||
|
|
||||||
# Increment button
|
# Treeview Container
|
||||||
inc_btn = tk.Button(level_ctrl, text="+", font=FONT_HEADER,
|
tree_frame = ctk.CTkFrame(results_container, fg_color="transparent")
|
||||||
bg=BG_LIGHT, fg=TEXT_PRIMARY, bd=0, width=2,
|
tree_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
|
||||||
activebackground=BG_HIGHLIGHT, cursor='hand2',
|
|
||||||
command=self.increment_level)
|
|
||||||
inc_btn.pack(side=tk.LEFT)
|
|
||||||
|
|
||||||
# Quick level buttons
|
# Treeview - ADDING IMAGE COLUMN
|
||||||
button_frame = tk.Frame(control_frame, bg=BG_MEDIUM)
|
# Note: Treeview column #0 is the tree column where icons live.
|
||||||
button_frame.pack(side=tk.LEFT, padx=25)
|
# We will put the image in #0 and text in #0 if possible, or name in #1
|
||||||
|
# Treeview - ADDING IMAGE COLUMN
|
||||||
|
# Use #0 for Icon only, like Card View
|
||||||
|
columns = ('card_name', 'level', 'current_value', 'effect_name')
|
||||||
|
self.tree = ttk.Treeview(tree_frame, columns=columns, show='tree headings', selectmode='browse', style="CardList.Treeview")
|
||||||
|
|
||||||
quick_levels = [25, 30, 35, 40, 45, 50]
|
self.tree.heading('#0', text='Image')
|
||||||
for lvl in quick_levels:
|
self.tree.column('#0', width=100, anchor='center')
|
||||||
btn = create_styled_button(button_frame, text=f"Lv{lvl}",
|
|
||||||
command=lambda l=lvl: self.set_level(l),
|
|
||||||
style_type='default')
|
|
||||||
btn.config(width=5, font=FONT_SMALL, padx=6, pady=3)
|
|
||||||
btn.pack(side=tk.LEFT, padx=3)
|
|
||||||
|
|
||||||
# Main content area
|
self.tree.heading('card_name', text='Card Name', anchor='w')
|
||||||
content_frame = tk.Frame(self, bg=BG_DARK)
|
self.tree.heading('level', text='Level', anchor='center')
|
||||||
content_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=15)
|
self.tree.heading('current_value', text='Value', anchor='center')
|
||||||
|
self.tree.heading('effect_name', text='Effect Name', anchor='w')
|
||||||
|
|
||||||
# Left: Current level effects
|
self.tree.column('card_name', width=200)
|
||||||
left_frame = ttk.LabelFrame(content_frame, text=" Current Level Effects ", padding=12)
|
self.tree.column('level', width=60, anchor='center')
|
||||||
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 10))
|
self.tree.column('current_value', width=80, anchor='center')
|
||||||
|
self.tree.column('effect_name', width=200)
|
||||||
|
|
||||||
self.current_effects = create_styled_text(left_frame, height=15)
|
scrollbar = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=self.tree.yview)
|
||||||
self.current_effects.pack(fill=tk.BOTH, expand=True)
|
# ... (rest of scrollbar setup) ...
|
||||||
self.current_effects.config(state=tk.DISABLED)
|
self.tree.configure(yscrollcommand=scrollbar.set)
|
||||||
|
|
||||||
# Right: Effect progression table
|
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
right_frame = ttk.LabelFrame(content_frame, text=" Effect Progression ", padding=12)
|
|
||||||
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
# Treeview for effect table
|
|
||||||
columns = ('effect', 'lv1', 'lv25', 'lv40', 'lv50')
|
|
||||||
self.effects_tree = ttk.Treeview(right_frame, columns=columns, show='headings', height=12)
|
|
||||||
|
|
||||||
self.effects_tree.heading('effect', text='Effect', anchor='w')
|
|
||||||
self.effects_tree.column('effect', width=140, minwidth=120)
|
|
||||||
|
|
||||||
for col in columns[1:]:
|
|
||||||
level = col.replace('lv', 'Lv ')
|
|
||||||
self.effects_tree.heading(col, text=level)
|
|
||||||
self.effects_tree.column(col, width=65, anchor='center')
|
|
||||||
|
|
||||||
scrollbar = ttk.Scrollbar(right_frame, orient=tk.VERTICAL, command=self.effects_tree.yview)
|
|
||||||
self.effects_tree.configure(yscrollcommand=scrollbar.set)
|
|
||||||
|
|
||||||
self.effects_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
||||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
|
|
||||||
def show_legend(self):
|
# Status Label
|
||||||
"""Show effect explanations"""
|
self.status_label = ctk.CTkLabel(results_container, text="", font=FONT_SMALL, text_color=TEXT_SECONDARY)
|
||||||
legend = {
|
self.status_label.pack(side=tk.BOTTOM, fill=tk.X, pady=(5, 10))
|
||||||
"Friendship Bonus": "Increases stats gained when training with this support card during Friendship Training (orange aura).",
|
|
||||||
"Motivation Bonus": "Increases stats gained based on your Uma's motivation level.",
|
|
||||||
"Specialty Rate": "Increases the chance of this card appearing in its specialty training.",
|
|
||||||
"Training Bonus": "Flat percentage increase to stats gained in training where this card is present.",
|
|
||||||
"Initial Bond": "Starting gauge value for this card.",
|
|
||||||
"Race Bonus": "Increases stats gained from racing.",
|
|
||||||
"Fan Count Bonus": "Increases fans gained from racing.",
|
|
||||||
"Skill Pt Bonus": "Bonus skill points gained when training with this card.",
|
|
||||||
"Hint Lv": "Starting level of skills taught by this card's hints.",
|
|
||||||
"Hint Rate": "Increases chance of getting a hint event."
|
|
||||||
}
|
|
||||||
|
|
||||||
text = "📖 Effect Explanations:\n\n"
|
def parse_value(self, value_str):
|
||||||
for name, desc in legend.items():
|
"""Parse effect value string to float for sorting"""
|
||||||
text += f"• {name}:\n {desc}\n\n"
|
try:
|
||||||
|
clean = re.sub(r'[^\d.-]', '', str(value_str))
|
||||||
|
return float(clean)
|
||||||
|
except:
|
||||||
|
return -999999.0
|
||||||
|
|
||||||
messagebox.showinfo("Effect Legend", text)
|
def perform_search(self):
|
||||||
|
"""Execute search and update results"""
|
||||||
def set_card(self, card_id):
|
term = self.search_var.get().strip()
|
||||||
"""Load a card's effects"""
|
if not term:
|
||||||
self.current_card_id = card_id
|
messagebox.showwarning("Search", "Please enter a search term")
|
||||||
|
|
||||||
# Get card info for max level
|
|
||||||
card = get_card_by_id(card_id)
|
|
||||||
if card:
|
|
||||||
self.current_card_name = card[1]
|
|
||||||
self.max_level = card[4]
|
|
||||||
if self.level_var.get() > self.max_level:
|
|
||||||
self.level_var.set(self.max_level)
|
|
||||||
self.level_display.config(text=str(self.max_level))
|
|
||||||
|
|
||||||
self.card_label.config(text=f"📊 {self.current_card_name}")
|
|
||||||
|
|
||||||
# Update displays
|
|
||||||
self.update_current_effects()
|
|
||||||
self.update_progression_table()
|
|
||||||
|
|
||||||
def set_level(self, level):
|
|
||||||
"""Set level from quick button"""
|
|
||||||
if level <= self.max_level:
|
|
||||||
self.level_var.set(level)
|
|
||||||
self.level_display.config(text=str(level))
|
|
||||||
self.update_current_effects()
|
|
||||||
|
|
||||||
def increment_level(self):
|
|
||||||
"""Increase level by 1"""
|
|
||||||
current = self.level_var.get()
|
|
||||||
if current < self.max_level:
|
|
||||||
self.set_level(current + 1)
|
|
||||||
|
|
||||||
def decrement_level(self):
|
|
||||||
"""Decrease level by 1"""
|
|
||||||
current = self.level_var.get()
|
|
||||||
if current > 1:
|
|
||||||
self.set_level(current - 1)
|
|
||||||
|
|
||||||
def update_current_effects(self):
|
|
||||||
"""Update the current level effects display"""
|
|
||||||
self.current_effects.config(state=tk.NORMAL)
|
|
||||||
self.current_effects.delete('1.0', tk.END)
|
|
||||||
|
|
||||||
# Configure tags
|
|
||||||
self.current_effects.tag_configure('header', font=FONT_SUBHEADER, foreground=ACCENT_PRIMARY)
|
|
||||||
self.current_effects.tag_configure('highlight', foreground=ACCENT_SUCCESS)
|
|
||||||
self.current_effects.tag_configure('effect_name', foreground=TEXT_SECONDARY)
|
|
||||||
self.current_effects.tag_configure('effect_value', foreground=TEXT_PRIMARY, font=FONT_BODY_BOLD)
|
|
||||||
self.current_effects.tag_configure('effect_tooltip', underline=False)
|
|
||||||
|
|
||||||
if not self.current_card_id:
|
|
||||||
self.current_effects.insert(tk.END, "No card selected\n\n", 'effect_name')
|
|
||||||
self.current_effects.insert(tk.END, "Select a card from the Card List tab to view its effects.", 'effect_name')
|
|
||||||
self.current_effects.config(state=tk.DISABLED)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
level = self.level_var.get()
|
# clear current
|
||||||
effects = get_effects_at_level(self.current_card_id, level)
|
for item in self.tree.get_children():
|
||||||
|
self.tree.delete(item)
|
||||||
|
|
||||||
self.current_effects.insert(tk.END, f"━━━ Level {level} ━━━\n\n", 'header')
|
# Query DB
|
||||||
|
results = search_owned_effects(term)
|
||||||
|
|
||||||
if effects:
|
if not results:
|
||||||
for name, value in effects:
|
self.status_label.configure(text="No matching effects found among owned cards.")
|
||||||
# Highlight high values
|
return
|
||||||
prefix = ""
|
|
||||||
if '%' in str(value):
|
# Process and Sort
|
||||||
|
# Row: (card_id, card_name, image_path, effect_name, effect_value, level)
|
||||||
|
processed_results = []
|
||||||
|
for r in results:
|
||||||
|
val_num = self.parse_value(r[4])
|
||||||
|
processed_results.append({
|
||||||
|
'data': r,
|
||||||
|
'sort_val': val_num
|
||||||
|
})
|
||||||
|
|
||||||
|
# Sort by value descending
|
||||||
|
processed_results.sort(key=lambda x: x['sort_val'], reverse=True)
|
||||||
|
|
||||||
|
# Populate Tree
|
||||||
|
for item in processed_results:
|
||||||
|
r = item['data']
|
||||||
|
|
||||||
|
card_id = r[0]
|
||||||
|
image_path = r[2]
|
||||||
|
|
||||||
|
# Load Image
|
||||||
|
img = self.icon_cache.get(card_id)
|
||||||
|
if not img:
|
||||||
|
resolved_path = resolve_image_path(image_path)
|
||||||
|
if resolved_path and os.path.exists(resolved_path):
|
||||||
try:
|
try:
|
||||||
num = int(str(value).replace('%', '').replace('+', ''))
|
pil_img = Image.open(resolved_path)
|
||||||
if num >= 20:
|
# Match CardList size
|
||||||
prefix = "★ "
|
pil_img.thumbnail((78, 78), Image.Resampling.LANCZOS)
|
||||||
|
img = ImageTk.PhotoImage(pil_img)
|
||||||
|
self.icon_cache[card_id] = img
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if prefix:
|
kv = {'image': img} if img else {}
|
||||||
self.current_effects.insert(tk.END, prefix, 'highlight')
|
|
||||||
|
|
||||||
# Insert effect name with tooltip tag
|
# Insert into tree
|
||||||
tag_name = f"tooltip_{name.replace(' ', '_')}"
|
# #0 = Image (Text '')
|
||||||
self.current_effects.insert(tk.END, f"{name}: ", ('effect_name', tag_name))
|
# Cols = Name, Level, Value, Effect
|
||||||
|
values = (r[1], f"Lv {r[5]}", r[4], r[3])
|
||||||
|
|
||||||
# Bind tooltip events
|
self.tree.insert('', tk.END, text='', values=values, **kv)
|
||||||
self.current_effects.tag_bind(tag_name, "<Enter>", lambda e, n=name: self.show_effect_tooltip(e, n))
|
|
||||||
self.current_effects.tag_bind(tag_name, "<Leave>", self.hide_effect_tooltip)
|
|
||||||
|
|
||||||
self.current_effects.insert(tk.END, f"{value}\n", 'effect_value')
|
self.status_label.configure(text=f"Found {len(processed_results)} owned cards with matching effects.")
|
||||||
else:
|
|
||||||
self.current_effects.insert(tk.END, "No effect data available for this level.\n\n", 'effect_name')
|
|
||||||
self.current_effects.insert(tk.END, "Try selecting: Lv 1, 25, 40, or 50", 'effect_name')
|
|
||||||
|
|
||||||
self.current_effects.config(state=tk.DISABLED)
|
# Compatibility methods for main_window integration
|
||||||
|
def set_card(self, card_id):
|
||||||
|
pass
|
||||||
|
|
||||||
def show_effect_tooltip(self, event, effect_name):
|
|
||||||
"""Show tooltip for effect"""
|
|
||||||
if effect_name in EFFECT_DESCRIPTIONS:
|
|
||||||
text = EFFECT_DESCRIPTIONS[effect_name]
|
|
||||||
x = event.x_root + 15
|
|
||||||
y = event.y_root + 10
|
|
||||||
|
|
||||||
# Close existing if any
|
|
||||||
self.hide_effect_tooltip(None)
|
|
||||||
|
|
||||||
self.tooltip_window = tk.Toplevel(self)
|
|
||||||
self.tooltip_window.wm_overrideredirect(True)
|
|
||||||
self.tooltip_window.wm_geometry(f"+{x}+{y}")
|
|
||||||
|
|
||||||
label = tk.Label(self.tooltip_window, text=text, justify=tk.LEFT,
|
|
||||||
background=BG_LIGHT, foreground=TEXT_PRIMARY,
|
|
||||||
relief=tk.SOLID, borderwidth=1, font=FONT_SMALL,
|
|
||||||
padx=10, pady=5, wraplength=250)
|
|
||||||
label.pack()
|
|
||||||
|
|
||||||
def hide_effect_tooltip(self, event):
|
|
||||||
"""Hide tooltip"""
|
|
||||||
if hasattr(self, 'tooltip_window') and self.tooltip_window:
|
|
||||||
self.tooltip_window.destroy()
|
|
||||||
self.tooltip_window = None
|
|
||||||
|
|
||||||
def update_progression_table(self):
|
|
||||||
"""Update the effect progression table"""
|
|
||||||
self.effects_tree.delete(*self.effects_tree.get_children())
|
|
||||||
|
|
||||||
if not self.current_card_id:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Get all effects
|
|
||||||
all_effects = get_all_effects(self.current_card_id)
|
|
||||||
|
|
||||||
# Organize by effect name
|
|
||||||
effect_by_level = {}
|
|
||||||
for level, effect_name, effect_value in all_effects:
|
|
||||||
if effect_name not in effect_by_level:
|
|
||||||
effect_by_level[effect_name] = {}
|
|
||||||
effect_by_level[effect_name][level] = effect_value
|
|
||||||
|
|
||||||
# Key levels for the table
|
|
||||||
key_levels = [1, 25, 40, 50]
|
|
||||||
|
|
||||||
# Add rows
|
|
||||||
for effect_name, levels in sorted(effect_by_level.items()):
|
|
||||||
row = [effect_name]
|
|
||||||
for lvl in key_levels:
|
|
||||||
# Find closest level we have data for
|
|
||||||
value = levels.get(lvl, '')
|
|
||||||
if not value:
|
|
||||||
# Try to find nearest
|
|
||||||
for l in sorted(levels.keys()):
|
|
||||||
if l <= lvl:
|
|
||||||
value = levels[l]
|
|
||||||
row.append(value)
|
|
||||||
|
|
||||||
self.effects_tree.insert('', tk.END, values=row)
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
Skill Search View - Find cards by the skills they teach
|
Skill Search View - Find cards by the skills they teach
|
||||||
|
Updated for CustomTkinter
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
|
import customtkinter as ctk
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from PIL import Image, ImageTk
|
from PIL import Image, ImageTk
|
||||||
@@ -17,15 +19,15 @@ from gui.theme import (
|
|||||||
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_TERTIARY,
|
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_TERTIARY,
|
||||||
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
|
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
|
||||||
FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL,
|
FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL,
|
||||||
create_card_frame, get_type_icon, create_styled_button
|
create_card_frame, get_type_icon, create_styled_button, create_styled_entry
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SkillSearchFrame(ttk.Frame):
|
class SkillSearchFrame(ctk.CTkFrame):
|
||||||
"""Frame for searching skills and finding cards that have them"""
|
"""Frame for searching skills and finding cards that have them"""
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
super().__init__(parent)
|
super().__init__(parent, fg_color="transparent")
|
||||||
self.all_skills = []
|
self.all_skills = []
|
||||||
self.icon_cache = {}
|
self.icon_cache = {}
|
||||||
self.current_skill = None
|
self.current_skill = None
|
||||||
@@ -36,30 +38,32 @@ class SkillSearchFrame(ttk.Frame):
|
|||||||
def create_widgets(self):
|
def create_widgets(self):
|
||||||
"""Create the skill search interface"""
|
"""Create the skill search interface"""
|
||||||
# Main split container
|
# Main split container
|
||||||
main_pane = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
|
# Use two frames instead of PanedWindow
|
||||||
main_pane.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|
||||||
|
|
||||||
# === Left Panel: Skill List ===
|
# === Left Panel: Skill List ===
|
||||||
left_frame = tk.Frame(main_pane, bg=BG_DARK, width=300)
|
left_frame = ctk.CTkFrame(self, width=390, corner_radius=10)
|
||||||
main_pane.add(left_frame, weight=1)
|
left_frame.pack_propagate(False) # Force width to stay 600
|
||||||
|
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=False, padx=(0, 10))
|
||||||
|
|
||||||
# Search Header
|
# Search Header
|
||||||
header = tk.Frame(left_frame, bg=BG_DARK)
|
header = ctk.CTkFrame(left_frame, fg_color="transparent")
|
||||||
header.pack(fill=tk.X, pady=(0, 10))
|
header.pack(fill=tk.X, pady=(15, 10), padx=10)
|
||||||
tk.Label(header, text="🔍 Search Skills", font=FONT_HEADER,
|
ctk.CTkLabel(header, text="🔍 Search Skills", font=FONT_HEADER,
|
||||||
bg=BG_DARK, fg=TEXT_PRIMARY).pack(side=tk.LEFT)
|
text_color=TEXT_PRIMARY).pack(side=tk.LEFT)
|
||||||
|
|
||||||
# Search Entry
|
# Search Entry
|
||||||
self.search_var = tk.StringVar()
|
self.search_var = tk.StringVar()
|
||||||
self.search_var.trace('w', self.filter_skills)
|
self.search_var.trace('w', self.filter_skills)
|
||||||
|
|
||||||
search_entry = ttk.Entry(left_frame, textvariable=self.search_var)
|
# Use styled entry
|
||||||
search_entry.pack(fill=tk.X, padx=(0, 5), pady=(0, 10))
|
search_entry = ctk.CTkEntry(left_frame, textvariable=self.search_var, placeholder_text="Type to filter...")
|
||||||
|
search_entry.pack(fill=tk.X, padx=10, pady=(0, 10))
|
||||||
|
|
||||||
# Skill Listbox
|
# Skill Listbox Container (Styled)
|
||||||
list_container = create_card_frame(left_frame)
|
list_container = ctk.CTkFrame(left_frame, fg_color="transparent")
|
||||||
list_container.pack(fill=tk.BOTH, expand=True)
|
list_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
|
||||||
|
|
||||||
|
# Using tk.Listbox because CTk doesn't have one and ScrollableFrame is harder to manage for simple selection list
|
||||||
scrollbar = ttk.Scrollbar(list_container, orient=tk.VERTICAL)
|
scrollbar = ttk.Scrollbar(list_container, orient=tk.VERTICAL)
|
||||||
self.skill_listbox = tk.Listbox(list_container,
|
self.skill_listbox = tk.Listbox(list_container,
|
||||||
bg=BG_MEDIUM, fg=TEXT_SECONDARY,
|
bg=BG_MEDIUM, fg=TEXT_SECONDARY,
|
||||||
@@ -77,27 +81,28 @@ class SkillSearchFrame(ttk.Frame):
|
|||||||
self.skill_listbox.bind('<<ListboxSelect>>', self.on_skill_selected)
|
self.skill_listbox.bind('<<ListboxSelect>>', self.on_skill_selected)
|
||||||
|
|
||||||
# === Right Panel: Results ===
|
# === Right Panel: Results ===
|
||||||
right_frame = tk.Frame(main_pane, bg=BG_DARK)
|
right_frame = ctk.CTkFrame(self, corner_radius=10, fg_color="transparent")
|
||||||
main_pane.add(right_frame, weight=3)
|
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
# Search Row (Search + Filter)
|
# Search Row (Search + Filter)
|
||||||
search_frame = tk.Frame(right_frame, bg=BG_DARK)
|
search_frame = ctk.CTkFrame(right_frame, fg_color="transparent")
|
||||||
search_frame.pack(fill=tk.X, padx=10, pady=10)
|
search_frame.pack(fill=tk.X, padx=10, pady=15)
|
||||||
|
|
||||||
self.result_header = tk.Label(search_frame, text="Select a skill to see cards",
|
self.result_header = ctk.CTkLabel(search_frame, text="Select a skill to see cards",
|
||||||
font=FONT_SUBHEADER, bg=BG_DARK, fg=ACCENT_PRIMARY)
|
font=FONT_SUBHEADER, text_color=ACCENT_PRIMARY)
|
||||||
self.result_header.pack(side=tk.LEFT)
|
self.result_header.pack(side=tk.LEFT)
|
||||||
|
|
||||||
# Owned Filter
|
# Owned Filter
|
||||||
self.owned_only_var = tk.BooleanVar(value=False)
|
self.owned_only_var = tk.BooleanVar(value=False)
|
||||||
self.owned_check = ttk.Checkbutton(search_frame, text="Show Owned Only",
|
self.owned_check = ctk.CTkCheckBox(search_frame, text="Show Owned Only",
|
||||||
variable=self.owned_only_var,
|
variable=self.owned_only_var,
|
||||||
command=self.on_filter_changed)
|
command=self.on_filter_changed,
|
||||||
|
font=FONT_SMALL)
|
||||||
self.owned_check.pack(side=tk.RIGHT, padx=10)
|
self.owned_check.pack(side=tk.RIGHT, padx=10)
|
||||||
|
|
||||||
# Results Treeview
|
# Results Treeview Container
|
||||||
tree_frame = create_card_frame(right_frame)
|
tree_frame = ctk.CTkFrame(right_frame, fg_color="transparent")
|
||||||
tree_frame.pack(fill=tk.BOTH, expand=True, padx=10)
|
tree_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||||
|
|
||||||
cols = ('owned', 'name', 'rarity', 'type', 'source', 'details')
|
cols = ('owned', 'name', 'rarity', 'type', 'source', 'details')
|
||||||
self.tree = ttk.Treeview(tree_frame, columns=cols, show='tree headings',
|
self.tree = ttk.Treeview(tree_frame, columns=cols, show='tree headings',
|
||||||
@@ -122,12 +127,12 @@ class SkillSearchFrame(ttk.Frame):
|
|||||||
vsb = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=self.tree.yview)
|
vsb = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=self.tree.yview)
|
||||||
self.tree.configure(yscrollcommand=vsb.set)
|
self.tree.configure(yscrollcommand=vsb.set)
|
||||||
|
|
||||||
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=2, pady=2)
|
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
vsb.pack(side=tk.RIGHT, fill=tk.Y, pady=2)
|
vsb.pack(side=tk.RIGHT, fill=tk.Y, pady=5)
|
||||||
|
|
||||||
# Stats footer
|
# Stats footer
|
||||||
self.stats_label = tk.Label(right_frame, text="", font=FONT_SMALL,
|
self.stats_label = ctk.CTkLabel(right_frame, text="", font=FONT_SMALL,
|
||||||
bg=BG_DARK, fg=TEXT_MUTED)
|
text_color=TEXT_MUTED)
|
||||||
self.stats_label.pack(anchor='e', pady=5, padx=10)
|
self.stats_label.pack(anchor='e', pady=5, padx=10)
|
||||||
|
|
||||||
def load_skills(self):
|
def load_skills(self):
|
||||||
@@ -194,7 +199,7 @@ class SkillSearchFrame(ttk.Frame):
|
|||||||
def show_cards_for_skill(self, skill_name):
|
def show_cards_for_skill(self, skill_name):
|
||||||
"""Fetch and display cards with the selected skill"""
|
"""Fetch and display cards with the selected skill"""
|
||||||
self.current_skill = skill_name
|
self.current_skill = skill_name
|
||||||
self.result_header.config(text=f"Cards with skill: {skill_name}")
|
self.result_header.configure(text=f"Cards with skill: {skill_name}")
|
||||||
|
|
||||||
# Clear tree
|
# Clear tree
|
||||||
for item in self.tree.get_children():
|
for item in self.tree.get_children():
|
||||||
@@ -204,10 +209,12 @@ class SkillSearchFrame(ttk.Frame):
|
|||||||
|
|
||||||
owned_only = self.owned_only_var.get()
|
owned_only = self.owned_only_var.get()
|
||||||
|
|
||||||
|
display_count = 0
|
||||||
for card in cards:
|
for card in cards:
|
||||||
if owned_only and not card.get('is_owned'):
|
if owned_only and not card.get('is_owned'):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
display_count += 1
|
||||||
# Load Icon
|
# Load Icon
|
||||||
card_id = card['card_id']
|
card_id = card['card_id']
|
||||||
img = self.icon_cache.get(card_id)
|
img = self.icon_cache.get(card_id)
|
||||||
@@ -216,7 +223,7 @@ class SkillSearchFrame(ttk.Frame):
|
|||||||
if resolved_path and os.path.exists(resolved_path):
|
if resolved_path and os.path.exists(resolved_path):
|
||||||
try:
|
try:
|
||||||
pil_img = Image.open(resolved_path)
|
pil_img = Image.open(resolved_path)
|
||||||
pil_img.thumbnail((48, 48), Image.Resampling.LANCZOS)
|
pil_img.thumbnail((32, 32), Image.Resampling.LANCZOS)
|
||||||
img = ImageTk.PhotoImage(pil_img)
|
img = ImageTk.PhotoImage(pil_img)
|
||||||
self.icon_cache[card_id] = img
|
self.icon_cache[card_id] = img
|
||||||
except:
|
except:
|
||||||
@@ -246,12 +253,10 @@ class SkillSearchFrame(ttk.Frame):
|
|||||||
card_details
|
card_details
|
||||||
)
|
)
|
||||||
|
|
||||||
if img:
|
kv = {'image': img} if img else {}
|
||||||
self.tree.insert('', tk.END, text='', image=img, values=values)
|
self.tree.insert('', tk.END, text='', values=values, **kv)
|
||||||
else:
|
|
||||||
self.tree.insert('', tk.END, text='?', values=values)
|
|
||||||
|
|
||||||
self.stats_label.config(text=f"Found {len(cards)} cards")
|
self.stats_label.configure(text=f"Found {display_count} cards")
|
||||||
|
|
||||||
def set_card(self, card_id):
|
def set_card(self, card_id):
|
||||||
"""No longer responsive to card selection in this tab"""
|
"""No longer responsive to card selection in this tab"""
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Tabbed interface for card browsing, effects, deck builder, and hints
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
import customtkinter as ctk
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -32,27 +32,29 @@ class MainWindow:
|
|||||||
"""Main application window with tabbed interface"""
|
"""Main application window with tabbed interface"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.root = tk.Tk()
|
# Initialize CTk root
|
||||||
|
self.root = ctk.CTk()
|
||||||
self.root.title("Umamusume Support Card Manager")
|
self.root.title("Umamusume Support Card Manager")
|
||||||
self.root.geometry("1400x850")
|
self.root.geometry("1400x850")
|
||||||
self.root.minsize(1350, 800)
|
self.root.minsize(1350, 800)
|
||||||
|
|
||||||
|
|
||||||
# Set icon
|
# Set icon
|
||||||
try:
|
try:
|
||||||
icon_path = resolve_image_path("1_Special Week.png")
|
icon_path = resolve_image_path("1_Special Week.png")
|
||||||
if icon_path and os.path.exists(icon_path):
|
if icon_path and os.path.exists(icon_path):
|
||||||
|
# ctk uses iconbitmap for windows usually, but iconphoto works too
|
||||||
icon_img = tk.PhotoImage(file=icon_path)
|
icon_img = tk.PhotoImage(file=icon_path)
|
||||||
self.root.iconphoto(True, icon_img)
|
self.root.iconphoto(True, icon_img)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to set icon: {e}")
|
print(f"Failed to set icon: {e}")
|
||||||
|
|
||||||
# Configure all styles using centralized theme
|
# Configure styles for legacy widgets
|
||||||
configure_styles(self.root)
|
configure_styles(self.root)
|
||||||
|
|
||||||
# Create main container
|
# Create main container
|
||||||
main_container = ttk.Frame(self.root)
|
# Note: CTk already has a main frame in a way, but we'll use a container for padding
|
||||||
main_container.pack(fill=tk.BOTH, expand=True)
|
main_container = ctk.CTkFrame(self.root, fg_color="transparent")
|
||||||
|
main_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||||
|
|
||||||
# State
|
# State
|
||||||
self.last_selected_levels = {} # card_id -> level
|
self.last_selected_levels = {} # card_id -> level
|
||||||
@@ -60,56 +62,53 @@ class MainWindow:
|
|||||||
# Header with stats
|
# Header with stats
|
||||||
self.create_header(main_container)
|
self.create_header(main_container)
|
||||||
|
|
||||||
# Status bar - Create BEFORE notebook to anchor it to bottom
|
# Status bar
|
||||||
self.create_status_bar(main_container)
|
self.create_status_bar(main_container)
|
||||||
|
|
||||||
# Tabbed notebook
|
# Tabbed notebook -> CTkTabview
|
||||||
self.notebook = ttk.Notebook(main_container)
|
self.tabview = ctk.CTkTabview(main_container)
|
||||||
self.notebook.pack(fill=tk.BOTH, expand=True, padx=15, pady=8)
|
self.tabview.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||||
|
|
||||||
# Create tabs
|
# Create tabs
|
||||||
self.create_tabs()
|
self.create_tabs()
|
||||||
|
|
||||||
def create_header(self, parent):
|
def create_header(self, parent):
|
||||||
"""Create header with database statistics and update button"""
|
"""Create header with database statistics and update button"""
|
||||||
# Header container with subtle bottom border effect
|
# Header container
|
||||||
header_outer = tk.Frame(parent, bg=BG_DARK)
|
header_frame = ctk.CTkFrame(parent, fg_color="transparent")
|
||||||
header_outer.pack(fill=tk.X)
|
header_frame.pack(fill=tk.X, padx=10, pady=(0, 10))
|
||||||
|
|
||||||
header_frame = tk.Frame(header_outer, bg=BG_DARK)
|
|
||||||
header_frame.pack(fill=tk.X, padx=20, pady=15)
|
|
||||||
|
|
||||||
# Left side: Title and version
|
# Left side: Title and version
|
||||||
title_frame = tk.Frame(header_frame, bg=BG_DARK)
|
title_frame = ctk.CTkFrame(header_frame, fg_color="transparent")
|
||||||
title_frame.pack(side=tk.LEFT)
|
title_frame.pack(side=tk.LEFT)
|
||||||
|
|
||||||
# App icon and title
|
# App icon and title
|
||||||
title_label = tk.Label(
|
title_label = ctk.CTkLabel(
|
||||||
title_frame,
|
title_frame,
|
||||||
text="🏇 Umamusume Support Card Manager",
|
text="🏇 Umamusume Support Card Manager",
|
||||||
font=FONT_TITLE,
|
font=FONT_TITLE,
|
||||||
bg=BG_DARK,
|
text_color=ACCENT_PRIMARY
|
||||||
fg=ACCENT_PRIMARY
|
|
||||||
)
|
)
|
||||||
title_label.pack(side=tk.LEFT)
|
title_label.pack(side=tk.LEFT, padx=(0, 10))
|
||||||
|
|
||||||
# Version badge
|
# Version badge
|
||||||
version_frame = tk.Frame(title_frame, bg=ACCENT_SECONDARY, padx=8, pady=2)
|
version_label = ctk.CTkLabel(
|
||||||
version_frame.pack(side=tk.LEFT, padx=12)
|
title_frame,
|
||||||
version_label = tk.Label(
|
|
||||||
version_frame,
|
|
||||||
text=f"v{VERSION}",
|
text=f"v{VERSION}",
|
||||||
font=FONT_SMALL,
|
font=FONT_SMALL,
|
||||||
bg=ACCENT_SECONDARY,
|
fg_color=ACCENT_SECONDARY,
|
||||||
fg=TEXT_PRIMARY
|
text_color=TEXT_PRIMARY,
|
||||||
|
corner_radius=6,
|
||||||
|
height=24,
|
||||||
|
width=60
|
||||||
)
|
)
|
||||||
version_label.pack()
|
version_label.pack(side=tk.LEFT)
|
||||||
|
|
||||||
# Right side: Update button and stats
|
# Right side: Update button and stats
|
||||||
right_frame = tk.Frame(header_frame, bg=BG_DARK)
|
right_frame = ctk.CTkFrame(header_frame, fg_color="transparent")
|
||||||
right_frame.pack(side=tk.RIGHT)
|
right_frame.pack(side=tk.RIGHT)
|
||||||
|
|
||||||
# Update button with modern styling
|
# Update button
|
||||||
self.update_button = create_styled_button(
|
self.update_button = create_styled_button(
|
||||||
right_frame,
|
right_frame,
|
||||||
text="🔄 Check for Updates",
|
text="🔄 Check for Updates",
|
||||||
@@ -118,122 +117,96 @@ class MainWindow:
|
|||||||
)
|
)
|
||||||
self.update_button.pack(side=tk.RIGHT, padx=(15, 0))
|
self.update_button.pack(side=tk.RIGHT, padx=(15, 0))
|
||||||
|
|
||||||
# Stats panel with card-like appearance
|
# Stats panel
|
||||||
stats_frame = tk.Frame(right_frame, bg=BG_MEDIUM, padx=15, pady=8)
|
self.stats_label = ctk.CTkLabel(
|
||||||
stats_frame.pack(side=tk.RIGHT)
|
right_frame,
|
||||||
|
text="Loading stats...",
|
||||||
stats = get_database_stats()
|
|
||||||
owned = get_owned_count()
|
|
||||||
|
|
||||||
# Build stats text with better formatting
|
|
||||||
stats_parts = [
|
|
||||||
f"📊 {stats.get('total_cards', 0)} Cards",
|
|
||||||
f"✨ {owned} Owned",
|
|
||||||
f"🏆 {stats.get('by_rarity', {}).get('SSR', 0)} SSR",
|
|
||||||
f"⭐ {stats.get('by_rarity', {}).get('SR', 0)} SR",
|
|
||||||
f"● {stats.get('by_rarity', {}).get('R', 0)} R"
|
|
||||||
]
|
|
||||||
stats_text = " │ ".join(stats_parts)
|
|
||||||
|
|
||||||
self.stats_label = tk.Label(
|
|
||||||
stats_frame,
|
|
||||||
text=stats_text,
|
|
||||||
font=FONT_SMALL,
|
font=FONT_SMALL,
|
||||||
bg=BG_MEDIUM,
|
fg_color=BG_MEDIUM,
|
||||||
fg=TEXT_SECONDARY
|
text_color=TEXT_SECONDARY,
|
||||||
|
corner_radius=8,
|
||||||
|
padx=15,
|
||||||
|
pady=5
|
||||||
)
|
)
|
||||||
self.stats_label.pack()
|
self.stats_label.pack(side=tk.RIGHT)
|
||||||
|
|
||||||
# Subtle separator line
|
# Initial stats load
|
||||||
separator = tk.Frame(header_outer, bg=BG_LIGHT, height=1)
|
self.refresh_stats()
|
||||||
separator.pack(fill=tk.X, padx=15)
|
|
||||||
|
|
||||||
def create_tabs(self):
|
def create_tabs(self):
|
||||||
"""Create all tab frames"""
|
"""Create all tab frames"""
|
||||||
|
# Add tabs
|
||||||
|
tab_cards = self.tabview.add(" 📋 Card List ")
|
||||||
|
tab_effects = self.tabview.add(" 📊 Search Effects ")
|
||||||
|
tab_deck = self.tabview.add(" 🎴 Deck Builder ")
|
||||||
|
tab_search = self.tabview.add(" 🔍 Skill Search ")
|
||||||
|
tab_skills = self.tabview.add(" 📜 Deck Skills ")
|
||||||
|
|
||||||
# Card List Tab
|
# Card List Tab
|
||||||
self.card_frame = CardListFrame(self.notebook,
|
# Note: CardListFrame and others inherit from ttk.Frame/tk.Frame.
|
||||||
|
# We need to make sure they can be packed into a CTkFrame (the tab).
|
||||||
|
self.card_frame = CardListFrame(tab_cards,
|
||||||
on_card_selected_callback=self.on_card_selected,
|
on_card_selected_callback=self.on_card_selected,
|
||||||
on_stats_updated_callback=self.refresh_stats)
|
on_stats_updated_callback=self.refresh_stats)
|
||||||
self.notebook.add(self.card_frame, text=" 📋 Card List ")
|
self.card_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
# Effects Tab
|
# Effects Tab
|
||||||
self.effects_frame = EffectsFrame(self.notebook)
|
self.effects_frame = EffectsFrame(tab_effects)
|
||||||
self.notebook.add(self.effects_frame, text=" 📊 Effects ")
|
self.effects_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
# Deck Builder Tab
|
# Deck Builder Tab
|
||||||
self.deck_frame = DeckBuilderFrame(self.notebook)
|
self.deck_frame = DeckBuilderFrame(tab_deck)
|
||||||
self.notebook.add(self.deck_frame, text=" 🎴 Deck Builder ")
|
self.deck_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
# Skill Search Tab
|
# Skill Search Tab
|
||||||
self.hints_frame = SkillSearchFrame(self.notebook)
|
self.hints_frame = SkillSearchFrame(tab_search)
|
||||||
self.notebook.add(self.hints_frame, text=" 🔍 Skill Search ")
|
self.hints_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
# Deck Skills Tab
|
# Deck Skills Tab
|
||||||
self.deck_skills_frame = DeckSkillsFrame(self.notebook)
|
self.deck_skills_frame = DeckSkillsFrame(tab_skills)
|
||||||
self.notebook.add(self.deck_skills_frame, text=" 📜 Deck Skills ")
|
self.deck_skills_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
def create_status_bar(self, parent):
|
def create_status_bar(self, parent):
|
||||||
"""Create status bar at bottom"""
|
"""Create status bar at bottom"""
|
||||||
status_outer = tk.Frame(parent, bg=BG_MEDIUM)
|
# Using pack side=BOTTOM relative to the main container
|
||||||
status_outer.pack(fill=tk.X, side=tk.BOTTOM)
|
status_frame = ctk.CTkFrame(parent, height=30, fg_color="transparent")
|
||||||
|
status_frame.pack(fill=tk.X, side=tk.BOTTOM, pady=(10, 0))
|
||||||
|
|
||||||
status_frame = tk.Frame(status_outer, bg=BG_MEDIUM)
|
self.status_label = ctk.CTkLabel(
|
||||||
status_frame.pack(fill=tk.X, padx=15, pady=8)
|
|
||||||
|
|
||||||
self.status_label = tk.Label(
|
|
||||||
status_frame,
|
status_frame,
|
||||||
text="✓ Ready",
|
text="✓ Ready",
|
||||||
font=FONT_SMALL,
|
font=FONT_SMALL,
|
||||||
bg=BG_MEDIUM,
|
text_color=TEXT_MUTED
|
||||||
fg=TEXT_MUTED
|
|
||||||
)
|
)
|
||||||
self.status_label.pack(side=tk.LEFT)
|
self.status_label.pack(side=tk.LEFT, padx=10)
|
||||||
|
|
||||||
tk.Label(
|
ctk.CTkLabel(
|
||||||
status_frame,
|
status_frame,
|
||||||
text="Data from gametora.com",
|
text="Data from gametora.com",
|
||||||
font=FONT_SMALL,
|
font=FONT_SMALL,
|
||||||
bg=BG_MEDIUM,
|
text_color=TEXT_MUTED
|
||||||
fg=TEXT_MUTED
|
|
||||||
).pack(side=tk.RIGHT)
|
).pack(side=tk.RIGHT)
|
||||||
|
|
||||||
# Diagnostics Button
|
ctk.CTkLabel(
|
||||||
diag_btn = tk.Button(
|
|
||||||
status_frame,
|
status_frame,
|
||||||
text="🔍 Diagnostics",
|
text="VibeCoded by Kiyreload │ ",
|
||||||
font=FONT_SMALL,
|
font=FONT_SMALL,
|
||||||
bg=BG_MEDIUM,
|
text_color=ACCENT_TERTIARY
|
||||||
fg=ACCENT_TERTIARY,
|
|
||||||
bd=0,
|
|
||||||
activebackground=BG_LIGHT,
|
|
||||||
activeforeground=TEXT_PRIMARY,
|
|
||||||
cursor='hand2',
|
|
||||||
command=self.show_diagnostics
|
|
||||||
)
|
|
||||||
diag_btn.pack(side=tk.RIGHT, padx=15)
|
|
||||||
|
|
||||||
tk.Label(
|
|
||||||
status_frame,
|
|
||||||
text="Made by Kiyreload │ ",
|
|
||||||
font=FONT_SMALL,
|
|
||||||
bg=BG_MEDIUM,
|
|
||||||
fg=ACCENT_TERTIARY
|
|
||||||
).pack(side=tk.RIGHT)
|
).pack(side=tk.RIGHT)
|
||||||
|
|
||||||
def on_card_selected(self, card_id, card_name, level=None):
|
def on_card_selected(self, card_id, card_name, level=None):
|
||||||
"""Handle card selection from card list"""
|
"""Handle card selection from card list"""
|
||||||
# Store level if provided
|
|
||||||
if level is not None:
|
if level is not None:
|
||||||
self.last_selected_levels[card_id] = level
|
self.last_selected_levels[card_id] = level
|
||||||
self.selected_card_id = card_id # Update selected_card_id
|
self.selected_card_id = card_id
|
||||||
|
|
||||||
# Update other tabs with selected card
|
# Update other tabs
|
||||||
if hasattr(self, 'effects_frame'):
|
if hasattr(self, 'effects_frame'):
|
||||||
self.effects_frame.set_card(card_id)
|
self.effects_frame.set_card(card_id)
|
||||||
if hasattr(self, 'deck_skills_frame'):
|
if hasattr(self, 'deck_skills_frame'):
|
||||||
self.deck_skills_frame.set_card(card_id)
|
self.deck_skills_frame.set_card(card_id)
|
||||||
|
|
||||||
self.status_label.config(text=f"📌 Selected: {card_name}")
|
self.status_label.configure(text=f"📌 Selected: {card_name}")
|
||||||
|
|
||||||
def refresh_stats(self):
|
def refresh_stats(self):
|
||||||
"""Refresh the statistics display"""
|
"""Refresh the statistics display"""
|
||||||
@@ -249,70 +222,13 @@ class MainWindow:
|
|||||||
]
|
]
|
||||||
stats_text = " │ ".join(stats_parts)
|
stats_text = " │ ".join(stats_parts)
|
||||||
|
|
||||||
self.stats_label.config(text=stats_text)
|
if hasattr(self, 'stats_label'):
|
||||||
|
self.stats_label.configure(text=stats_text)
|
||||||
|
|
||||||
def show_update_dialog(self):
|
def show_update_dialog(self):
|
||||||
"""Show the update dialog"""
|
"""Show the update dialog"""
|
||||||
show_update_dialog(self.root)
|
show_update_dialog(self.root)
|
||||||
|
|
||||||
def show_diagnostics(self):
|
|
||||||
"""Show diagnostics information for debugging"""
|
|
||||||
diag_win = tk.Toplevel(self.root)
|
|
||||||
diag_win.title("System Diagnostics")
|
|
||||||
diag_win.geometry("700x500")
|
|
||||||
diag_win.configure(bg=BG_DARK)
|
|
||||||
|
|
||||||
from db.db_queries import DB_PATH
|
|
||||||
import platform
|
|
||||||
|
|
||||||
# Info text
|
|
||||||
info = [
|
|
||||||
f"--- Application Info ---",
|
|
||||||
f"Version: {VERSION}",
|
|
||||||
f"Frozen (EXE): {getattr(sys, 'frozen', False)}",
|
|
||||||
f"Python: {sys.version}",
|
|
||||||
f"Platform: {platform.platform()}",
|
|
||||||
"",
|
|
||||||
f"--- Database ---",
|
|
||||||
f"DB Path: {DB_PATH}",
|
|
||||||
f"DB Exists: {os.path.exists(DB_PATH)}",
|
|
||||||
"",
|
|
||||||
f"--- Search Paths ---",
|
|
||||||
f"Executable: {sys.executable}",
|
|
||||||
f"Script Root: {os.path.dirname(os.path.abspath(__file__))}",
|
|
||||||
f"MEIPASS (Temp): {getattr(sys, '_MEIPASS', 'N/A')}",
|
|
||||||
"",
|
|
||||||
f"--- Image Check ---"
|
|
||||||
]
|
|
||||||
|
|
||||||
# Check some images
|
|
||||||
img_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'images')
|
|
||||||
info.append(f"Images Dir (Source): {img_dir}")
|
|
||||||
info.append(f"Exists: {os.path.exists(img_dir)}")
|
|
||||||
|
|
||||||
if os.path.exists(img_dir):
|
|
||||||
files = os.listdir(img_dir)
|
|
||||||
info.append(f"Files found: {len(files)}")
|
|
||||||
if len(files) > 0:
|
|
||||||
info.append(f"Sample: {files[0]}")
|
|
||||||
|
|
||||||
content = "\n".join(info)
|
|
||||||
|
|
||||||
# Display area
|
|
||||||
text_frame = tk.Frame(diag_win, bg=BG_DARK, padx=20, pady=20)
|
|
||||||
text_frame.pack(fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
from gui.theme import create_styled_text
|
|
||||||
text_area = create_styled_text(text_frame)
|
|
||||||
text_area.pack(fill=tk.BOTH, expand=True)
|
|
||||||
text_area.insert(tk.END, content)
|
|
||||||
text_area.config(state=tk.DISABLED)
|
|
||||||
|
|
||||||
# Close button
|
|
||||||
btn_frame = tk.Frame(diag_win, bg=BG_DARK, pady=15)
|
|
||||||
btn_frame.pack(fill=tk.X)
|
|
||||||
create_styled_button(btn_frame, text="Close", command=diag_win.destroy).pack()
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""
|
"""
|
||||||
Start the GUI application and display the main window.
|
Start the GUI application and display the main window.
|
||||||
|
|||||||
393
gui/theme.py
393
gui/theme.py
@@ -1,21 +1,333 @@
|
|||||||
"""
|
"""
|
||||||
Centralized Theme Module for Umamusume Support Card Manager
|
Centralized Theme Module for Umamusume Support Card Manager
|
||||||
Modern glassmorphism-inspired dark theme with consistent styling
|
Modern glassmorphism-inspired dark theme with consistent styling using CustomTkinter
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
|
import customtkinter as ctk
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
# COLOR PALETTE
|
# CONFIGURATION
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
# Primary backgrounds (rich purplish-blues with depth)
|
# Set default theme
|
||||||
BG_DARKEST = '#0d0d1a' # Deepest background
|
ctk.set_appearance_mode("Dark")
|
||||||
BG_DARK = '#151528' # Main application background
|
ctk.set_default_color_theme("blue") # We can use 'dark-blue' or 'green' too
|
||||||
BG_MEDIUM = '#1e1e3f' # Card/panel backgrounds
|
|
||||||
BG_LIGHT = '#2a2a5a' # Elevated elements, hover states
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
BG_HIGHLIGHT = '#3d3d7a' # Active/selected backgrounds
|
# COLOR PALETTE (Expanded for CTk)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
# Primary backgrounds
|
||||||
|
BG_DARKEST = '#0B0B15' # Deepest background
|
||||||
|
BG_DARK = '#1a1a1a' # Main application background (CTk default is ~#2b2b2b, we can match or override)
|
||||||
|
BG_MEDIUM = '#2b2b2b' # Card/panel backgrounds
|
||||||
|
BG_LIGHT = '#3A3A4F' # Elevated elements
|
||||||
|
BG_HIGHLIGHT = '#1F6AA5' # Active/selected (matches 'blue' theme)
|
||||||
|
|
||||||
|
# Accents
|
||||||
|
ACCENT_PRIMARY = '#1F6AA5' # CTk Blue
|
||||||
|
ACCENT_SECONDARY = '#7c5cff' # Purple accent
|
||||||
|
ACCENT_TERTIARY = '#5ce1e6' # Cyan accent
|
||||||
|
ACCENT_SUCCESS = '#2CC985' # Green for success
|
||||||
|
ACCENT_WARNING = '#E9B949' # Amber for warning
|
||||||
|
ACCENT_ERROR = '#C33737' # Red for errors
|
||||||
|
|
||||||
|
# Text colors
|
||||||
|
TEXT_PRIMARY = '#DCE4EE' # CTk default text color
|
||||||
|
TEXT_SECONDARY = '#949A9F' # Secondary text
|
||||||
|
TEXT_MUTED = '#6B7075' # Muted text
|
||||||
|
|
||||||
|
# Rarity colors
|
||||||
|
RARITY_COLORS = {
|
||||||
|
'SSR': '#FFD700',
|
||||||
|
'SR': '#C0C0C0',
|
||||||
|
'R': '#CD853F'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Type colors
|
||||||
|
TYPE_COLORS = {
|
||||||
|
'Speed': '#3b82f6',
|
||||||
|
'Stamina': '#f97316',
|
||||||
|
'Power': '#eab308',
|
||||||
|
'Guts': '#ef4444',
|
||||||
|
'Wisdom': '#22c55e',
|
||||||
|
'Friend': '#a855f7',
|
||||||
|
'Group': '#f59e0b'
|
||||||
|
}
|
||||||
|
|
||||||
|
TYPE_ICONS = {
|
||||||
|
'Speed': '🏃',
|
||||||
|
'Stamina': '💚',
|
||||||
|
'Power': '💪',
|
||||||
|
'Guts': '🔥',
|
||||||
|
'Wisdom': '🧠',
|
||||||
|
'Friend': '💜',
|
||||||
|
'Group': '👥'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# FONTS
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
FONT_FAMILY = 'Roboto Medium' # CTk Default
|
||||||
|
FONT_FAMILY_MONO = 'Consolas'
|
||||||
|
|
||||||
|
FONT_TITLE = (FONT_FAMILY, 24, 'bold')
|
||||||
|
FONT_HEADER = (FONT_FAMILY, 20, 'bold')
|
||||||
|
FONT_SUBHEADER = (FONT_FAMILY, 16, 'bold')
|
||||||
|
FONT_BODY = (FONT_FAMILY, 14)
|
||||||
|
FONT_BODY_BOLD = (FONT_FAMILY, 14, 'bold')
|
||||||
|
FONT_SMALL = (FONT_FAMILY, 12)
|
||||||
|
FONT_TINY = (FONT_FAMILY, 10)
|
||||||
|
FONT_MONO = (FONT_FAMILY_MONO, 13)
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# STYLE CONFIGURATION
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def configure_styles(root: tk.Tk):
|
||||||
|
"""
|
||||||
|
Configure ttk styles for legacy widgets (like Treeview)
|
||||||
|
Note: Standard TK configurations don't affect CTk widgets
|
||||||
|
"""
|
||||||
|
style = ttk.Style()
|
||||||
|
style.theme_use('clam')
|
||||||
|
|
||||||
|
# Configure Treeview to look dark
|
||||||
|
style.configure('Treeview',
|
||||||
|
background=BG_MEDIUM,
|
||||||
|
foreground=TEXT_PRIMARY,
|
||||||
|
fieldbackground=BG_MEDIUM,
|
||||||
|
font=FONT_BODY,
|
||||||
|
rowheight=36,
|
||||||
|
borderwidth=0)
|
||||||
|
style.configure('Treeview.Heading',
|
||||||
|
font=FONT_BODY_BOLD,
|
||||||
|
background='#333333',
|
||||||
|
foreground=TEXT_PRIMARY,
|
||||||
|
padding=8,
|
||||||
|
borderwidth=0)
|
||||||
|
style.map('Treeview',
|
||||||
|
background=[('selected', ACCENT_PRIMARY)],
|
||||||
|
foreground=[('selected', 'white')])
|
||||||
|
style.map('Treeview.Heading',
|
||||||
|
background=[('active', '#404040')])
|
||||||
|
|
||||||
|
# Card List specific
|
||||||
|
style.configure('CardList.Treeview',
|
||||||
|
background=BG_MEDIUM,
|
||||||
|
foreground=TEXT_PRIMARY,
|
||||||
|
rowheight=60)
|
||||||
|
|
||||||
|
# Scrollbar (for Treeview only)
|
||||||
|
style.configure('Vertical.TScrollbar',
|
||||||
|
background=BG_LIGHT,
|
||||||
|
troughcolor=BG_DARK,
|
||||||
|
borderwidth=0,
|
||||||
|
arrowsize=12)
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# WIDGET FACTORIES (ADAPTERS)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def create_styled_button(parent, text, command=None, style_type='default', **kwargs):
|
||||||
|
"""Create a styled CTkButton"""
|
||||||
|
|
||||||
|
# Map old style types to colors
|
||||||
|
fg_color = None # Default
|
||||||
|
hover_color = None
|
||||||
|
|
||||||
|
if style_type == 'accent':
|
||||||
|
pass # Uses default blue
|
||||||
|
elif style_type == 'secondary':
|
||||||
|
fg_color = ACCENT_SECONDARY
|
||||||
|
hover_color = '#6040e0'
|
||||||
|
elif style_type == 'danger':
|
||||||
|
fg_color = ACCENT_ERROR
|
||||||
|
hover_color = '#a02020'
|
||||||
|
elif style_type == 'default':
|
||||||
|
fg_color = 'transparent'
|
||||||
|
border_width = 1
|
||||||
|
kwargs['border_width'] = 1
|
||||||
|
kwargs['border_color'] = TEXT_SECONDARY
|
||||||
|
|
||||||
|
# Filter out incompatible kwargs from tk
|
||||||
|
safe_kwargs = {k: v for k, v in kwargs.items() if k not in ['padx', 'pady', 'bg', 'fg', 'bd', 'relief', 'activebackground', 'activeforeground']}
|
||||||
|
|
||||||
|
btn = ctk.CTkButton(
|
||||||
|
parent,
|
||||||
|
text=text,
|
||||||
|
command=command,
|
||||||
|
font=FONT_BODY_BOLD if style_type == 'accent' else FONT_BODY,
|
||||||
|
height=36,
|
||||||
|
corner_radius=8,
|
||||||
|
**safe_kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
if fg_color:
|
||||||
|
btn.configure(fg_color=fg_color)
|
||||||
|
if hover_color:
|
||||||
|
btn.configure(hover_color=hover_color)
|
||||||
|
|
||||||
|
return btn
|
||||||
|
|
||||||
|
def create_styled_entry(parent, textvariable=None, **kwargs):
|
||||||
|
"""Create a styled CTkEntry"""
|
||||||
|
safe_kwargs = {k: v for k, v in kwargs.items() if k not in ['bg', 'fg', 'bd', 'relief']}
|
||||||
|
|
||||||
|
return ctk.CTkEntry(
|
||||||
|
parent,
|
||||||
|
textvariable=textvariable,
|
||||||
|
font=FONT_BODY,
|
||||||
|
height=36,
|
||||||
|
corner_radius=6,
|
||||||
|
border_color=BG_LIGHT,
|
||||||
|
**safe_kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_card_frame(parent, **kwargs):
|
||||||
|
"""Create a styled CTkFrame"""
|
||||||
|
# Filter tk params
|
||||||
|
safe_kwargs = {k: v for k, v in kwargs.items() if k not in ['bg', 'highlightthickness', 'highlightbackground']}
|
||||||
|
|
||||||
|
return ctk.CTkFrame(
|
||||||
|
parent,
|
||||||
|
corner_radius=12,
|
||||||
|
fg_color=BG_MEDIUM, # Card background
|
||||||
|
**safe_kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_styled_text(parent, height=10, **kwargs):
|
||||||
|
"""
|
||||||
|
Create a styled CTkTextbox
|
||||||
|
Note: CTkTextbox API is slightly different from tk.Text
|
||||||
|
"""
|
||||||
|
safe_kwargs = {k: v for k, v in kwargs.items() if k not in ['bg', 'fg', 'selectbackground', 'selectforeground', 'relief']}
|
||||||
|
|
||||||
|
return ctk.CTkTextbox(
|
||||||
|
parent,
|
||||||
|
height=height * 20, # Approx pixel height
|
||||||
|
font=FONT_MONO,
|
||||||
|
corner_radius=10,
|
||||||
|
text_color=TEXT_PRIMARY,
|
||||||
|
fg_color=BG_DARK,
|
||||||
|
border_color=BG_LIGHT,
|
||||||
|
border_width=1,
|
||||||
|
**safe_kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_rarity_color(rarity):
|
||||||
|
return RARITY_COLORS.get(rarity, TEXT_SECONDARY)
|
||||||
|
|
||||||
|
def get_type_color(card_type):
|
||||||
|
return TYPE_COLORS.get(card_type, TEXT_SECONDARY)
|
||||||
|
|
||||||
|
def get_type_icon(card_type):
|
||||||
|
return TYPE_ICONS.get(card_type, '')
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# TOOLTIPS (Legacy/Wrapper)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
EFFECT_DESCRIPTIONS = {
|
||||||
|
"Friendship Bonus": "Increases stats gained when training with this support card during Friendship Training (orange aura).",
|
||||||
|
"Motivation Bonus": "Increases stats gained based on your Uma's motivation level.",
|
||||||
|
"Specialty Rate": "Increases the chance of this card appearing in its specialty training.",
|
||||||
|
"Training Bonus": "Flat percentage increase to stats gained in training where this card is present.",
|
||||||
|
"Initial Bond": "Starting gauge value for this card.",
|
||||||
|
"Race Bonus": "Increases stats gained from racing.",
|
||||||
|
"Fan Count Bonus": "Increases fans gained from racing.",
|
||||||
|
"Skill Pt Bonus": "Bonus skill points gained when training with this card.",
|
||||||
|
"Hint Lv": "Starting level of skills taught by this card's hints.",
|
||||||
|
"Hint Rate": "Increases chance of getting a hint event.",
|
||||||
|
"Minigame Fail Rate": "Reduces chance of failing training.",
|
||||||
|
"Energy Usage": "Reduces energy consumed during training.",
|
||||||
|
"Current Energy": "Increases starting energy in scenario.",
|
||||||
|
"Vitality": "Increases vitality gain from events.",
|
||||||
|
"Stamina": "Increases stamina gain from training.",
|
||||||
|
"Speed": "Increases speed gain from training.",
|
||||||
|
"Power": "Increases power gain from training.",
|
||||||
|
"Guts": "Increases guts gain from training.",
|
||||||
|
"Wisdom": "Increases wisdom gain from training.",
|
||||||
|
"Logic": "Custom logic effect.",
|
||||||
|
"Starting Stats": "Increases initial stats at start of scenario."
|
||||||
|
}
|
||||||
|
|
||||||
|
class Tooltip:
|
||||||
|
"""
|
||||||
|
Simple Tooltip adapted for CTk widgets
|
||||||
|
"""
|
||||||
|
def __init__(self, widget, text):
|
||||||
|
self.widget = widget
|
||||||
|
self.text = text
|
||||||
|
self.tip_window = None
|
||||||
|
self.id = None
|
||||||
|
self.x = self.y = 0
|
||||||
|
self.widget.bind("<Enter>", self.enter)
|
||||||
|
self.widget.bind("<Leave>", self.leave)
|
||||||
|
self.widget.bind("<ButtonPress>", self.leave)
|
||||||
|
|
||||||
|
def enter(self, event=None):
|
||||||
|
self.schedule()
|
||||||
|
|
||||||
|
def leave(self, event=None):
|
||||||
|
self.unschedule()
|
||||||
|
self.hidetip()
|
||||||
|
|
||||||
|
def schedule(self):
|
||||||
|
self.unschedule()
|
||||||
|
self.id = self.widget.after(500, self.showtip)
|
||||||
|
|
||||||
|
def unschedule(self):
|
||||||
|
id = self.id
|
||||||
|
self.id = None
|
||||||
|
if id:
|
||||||
|
self.widget.after_cancel(id)
|
||||||
|
|
||||||
|
def showtip(self, event=None):
|
||||||
|
x = y = 0
|
||||||
|
try:
|
||||||
|
x, y, cx, cy = self.widget.bbox("insert")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
x += self.widget.winfo_rootx() + 25
|
||||||
|
y += self.widget.winfo_rooty() + 20
|
||||||
|
|
||||||
|
self.tip_window = tk.Toplevel(self.widget)
|
||||||
|
self.tip_window.wm_overrideredirect(True)
|
||||||
|
self.tip_window.wm_geometry(f"+{x}+{y}")
|
||||||
|
|
||||||
|
# CTkLabel inside the tooltip (if possible) or standard Label
|
||||||
|
# We'll use standard Label for the tooltip window content to be safe
|
||||||
|
label = tk.Label(
|
||||||
|
self.tip_window,
|
||||||
|
text=self.text,
|
||||||
|
justify=tk.LEFT,
|
||||||
|
background="#2b2b2b",
|
||||||
|
foreground="#DCE4EE",
|
||||||
|
relief=tk.SOLID,
|
||||||
|
borderwidth=1,
|
||||||
|
font=("Segoe UI", 10),
|
||||||
|
padx=10,
|
||||||
|
pady=5
|
||||||
|
)
|
||||||
|
label.pack(ipadx=1)
|
||||||
|
|
||||||
|
def hidetip(self):
|
||||||
|
tw = self.tip_window
|
||||||
|
self.tip_window = None
|
||||||
|
if tw:
|
||||||
|
tw.destroy()
|
||||||
|
# Primary backgrounds (Neutral Dark)
|
||||||
|
BG_DARKEST = '#0f0f0f' # Deepest background (Material Darker)
|
||||||
|
BG_DARK = '#141414' # Main application background
|
||||||
|
BG_MEDIUM = '#1f1f1f' # Card/panel backgrounds
|
||||||
|
BG_LIGHT = '#2d2d2d' # Elevated elements, hover states
|
||||||
|
BG_HIGHLIGHT = '#3d3d3d' # Active/selected backgrounds
|
||||||
|
|
||||||
# Accents (vibrant but refined)
|
# Accents (vibrant but refined)
|
||||||
ACCENT_PRIMARY = '#ff6b9d' # Pink accent (main action color)
|
ACCENT_PRIMARY = '#ff6b9d' # Pink accent (main action color)
|
||||||
@@ -198,7 +510,7 @@ def configure_styles(root: tk.Tk):
|
|||||||
foreground=TEXT_PRIMARY,
|
foreground=TEXT_PRIMARY,
|
||||||
padding=6)
|
padding=6)
|
||||||
style.map('Treeview',
|
style.map('Treeview',
|
||||||
background=[('selected', ACCENT_PRIMARY)],
|
background=[('selected', BG_HIGHLIGHT)],
|
||||||
foreground=[('selected', TEXT_PRIMARY)])
|
foreground=[('selected', TEXT_PRIMARY)])
|
||||||
style.map('Treeview.Heading',
|
style.map('Treeview.Heading',
|
||||||
background=[('active', BG_HIGHLIGHT)])
|
background=[('active', BG_HIGHLIGHT)])
|
||||||
@@ -209,7 +521,7 @@ def configure_styles(root: tk.Tk):
|
|||||||
foreground=TEXT_SECONDARY,
|
foreground=TEXT_SECONDARY,
|
||||||
fieldbackground=BG_MEDIUM,
|
fieldbackground=BG_MEDIUM,
|
||||||
font=FONT_BODY,
|
font=FONT_BODY,
|
||||||
rowheight=60)
|
rowheight=80)
|
||||||
|
|
||||||
# Deck list style
|
# Deck list style
|
||||||
style.configure('DeckList.Treeview',
|
style.configure('DeckList.Treeview',
|
||||||
@@ -217,7 +529,7 @@ def configure_styles(root: tk.Tk):
|
|||||||
foreground=TEXT_SECONDARY,
|
foreground=TEXT_SECONDARY,
|
||||||
fieldbackground=BG_MEDIUM,
|
fieldbackground=BG_MEDIUM,
|
||||||
font=FONT_BODY,
|
font=FONT_BODY,
|
||||||
rowheight=60)
|
rowheight=80)
|
||||||
style.map('DeckList.Treeview',
|
style.map('DeckList.Treeview',
|
||||||
background=[('selected', ACCENT_PRIMARY)])
|
background=[('selected', ACCENT_PRIMARY)])
|
||||||
|
|
||||||
@@ -256,8 +568,29 @@ def configure_styles(root: tk.Tk):
|
|||||||
# WIDGET HELPER FUNCTIONS
|
# WIDGET HELPER FUNCTIONS
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
|
||||||
|
def create_styled_entry(parent, textvariable=None, **kwargs):
|
||||||
|
"""Create a styled ctk.CTkEntry with modern appearance"""
|
||||||
|
bg_color = kwargs.pop('bg', BG_MEDIUM)
|
||||||
|
fg_color = kwargs.pop('fg', TEXT_PRIMARY)
|
||||||
|
|
||||||
|
# Filter tk args
|
||||||
|
safe_kwargs = {k: v for k, v in kwargs.items() if k not in ['bg', 'fg', 'bd', 'relief', 'insertbackground', 'selectbackground', 'selectforeground', 'highlightthickness']}
|
||||||
|
|
||||||
|
entry = ctk.CTkEntry(
|
||||||
|
parent,
|
||||||
|
textvariable=textvariable,
|
||||||
|
font=FONT_BODY,
|
||||||
|
fg_color=bg_color,
|
||||||
|
text_color=fg_color,
|
||||||
|
border_width=1,
|
||||||
|
corner_radius=6,
|
||||||
|
**safe_kwargs
|
||||||
|
)
|
||||||
|
return entry
|
||||||
|
|
||||||
def create_styled_button(parent, text, command=None, style_type='default', **kwargs):
|
def create_styled_button(parent, text, command=None, style_type='default', **kwargs):
|
||||||
"""Create a styled tk.Button with modern appearance"""
|
"""Create a styled ctk.CTkButton with modern appearance"""
|
||||||
bg_colors = {
|
bg_colors = {
|
||||||
'default': BG_LIGHT,
|
'default': BG_LIGHT,
|
||||||
'accent': ACCENT_PRIMARY,
|
'accent': ACCENT_PRIMARY,
|
||||||
@@ -275,35 +608,27 @@ def create_styled_button(parent, text, command=None, style_type='default', **kwa
|
|||||||
'danger': '#ff8a8a'
|
'danger': '#ff8a8a'
|
||||||
}
|
}
|
||||||
|
|
||||||
bg = bg_colors.get(style_type, BG_LIGHT)
|
fg_color = bg_colors.get(style_type, BG_LIGHT)
|
||||||
hover_bg = hover_colors.get(style_type, BG_HIGHLIGHT)
|
hover_color = hover_colors.get(style_type, BG_HIGHLIGHT)
|
||||||
|
|
||||||
btn = tk.Button(
|
# Filter out tk-specific args that might crash CTk
|
||||||
|
safe_kwargs = {k: v for k, v in kwargs.items() if k not in ['bg', 'bd', 'relief', 'activebackground', 'activeforeground', 'padx', 'pady']}
|
||||||
|
|
||||||
|
# Handle width/height specifics if needed, though CTk handles pixel width natively
|
||||||
|
|
||||||
|
btn = ctk.CTkButton(
|
||||||
parent,
|
parent,
|
||||||
text=text,
|
text=text,
|
||||||
command=command,
|
command=command,
|
||||||
bg=bg,
|
fg_color=fg_color,
|
||||||
fg=TEXT_PRIMARY,
|
text_color=TEXT_PRIMARY,
|
||||||
font=FONT_BODY_BOLD if style_type == 'accent' else FONT_BODY,
|
font=FONT_BODY_BOLD if style_type == 'accent' else FONT_BODY,
|
||||||
activebackground=hover_bg,
|
hover_color=hover_color,
|
||||||
activeforeground=TEXT_PRIMARY,
|
corner_radius=8,
|
||||||
bd=0,
|
border_width=0,
|
||||||
padx=16,
|
**safe_kwargs
|
||||||
pady=8,
|
|
||||||
cursor='hand2',
|
|
||||||
relief=tk.FLAT,
|
|
||||||
**kwargs
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add hover effect
|
|
||||||
def on_enter(e):
|
|
||||||
btn.configure(bg=hover_bg)
|
|
||||||
def on_leave(e):
|
|
||||||
btn.configure(bg=bg)
|
|
||||||
|
|
||||||
btn.bind('<Enter>', on_enter)
|
|
||||||
btn.bind('<Leave>', on_leave)
|
|
||||||
|
|
||||||
return btn
|
return btn
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
"""
|
"""
|
||||||
Update Dialog for UmamusumeCardManager
|
Update Dialog for UmamusumeCardManager
|
||||||
Provides a modal dialog for the update process.
|
Provides a modal dialog for the update process.
|
||||||
|
Updated for CustomTkinter
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, messagebox, scrolledtext
|
from tkinter import ttk, messagebox
|
||||||
|
import customtkinter as ctk
|
||||||
import threading
|
import threading
|
||||||
import webbrowser
|
|
||||||
from typing import Optional, Callable
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
from typing import Optional, Callable
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
from updater.update_checker import check_for_updates, download_update, apply_update, get_current_version
|
from updater.update_checker import check_for_updates, download_update, apply_update, get_current_version
|
||||||
from gui.theme import (
|
from gui.theme import (
|
||||||
BG_DARK, BG_DARKEST, BG_MEDIUM, BG_LIGHT, BG_HIGHLIGHT,
|
BG_DARK, BG_MEDIUM, BG_LIGHT, BG_HIGHLIGHT,
|
||||||
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_SUCCESS, ACCENT_ERROR,
|
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_SUCCESS, ACCENT_ERROR,
|
||||||
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
|
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
|
||||||
FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL,
|
FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL,
|
||||||
@@ -26,7 +27,7 @@ from gui.theme import (
|
|||||||
class UpdateDialog:
|
class UpdateDialog:
|
||||||
"""Modal dialog for checking and applying updates."""
|
"""Modal dialog for checking and applying updates."""
|
||||||
|
|
||||||
def __init__(self, parent: tk.Tk, on_close_callback: Optional[Callable] = None):
|
def __init__(self, parent: ctk.CTk, on_close_callback: Optional[Callable] = None):
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.on_close_callback = on_close_callback
|
self.on_close_callback = on_close_callback
|
||||||
self.update_info = None
|
self.update_info = None
|
||||||
@@ -34,25 +35,28 @@ class UpdateDialog:
|
|||||||
self.is_downloading = False
|
self.is_downloading = False
|
||||||
|
|
||||||
# Create the dialog window
|
# Create the dialog window
|
||||||
self.dialog = tk.Toplevel(parent)
|
self.dialog = ctk.CTkToplevel(parent)
|
||||||
self.dialog.title("Check for Updates")
|
self.dialog.title("Check for Updates")
|
||||||
self.dialog.geometry("520x600")
|
self.dialog.geometry("520x600")
|
||||||
self.dialog.resizable(True, True)
|
self.dialog.resizable(True, True)
|
||||||
self.dialog.minsize(480, 500)
|
self.dialog.minsize(480, 500)
|
||||||
|
|
||||||
|
# Set transient/grab to make it modal
|
||||||
self.dialog.transient(parent)
|
self.dialog.transient(parent)
|
||||||
self.dialog.grab_set()
|
self.dialog.grab_set()
|
||||||
|
|
||||||
# Center on parent
|
# Center on parent
|
||||||
self.center_on_parent()
|
self.center_on_parent()
|
||||||
|
|
||||||
self.dialog.configure(bg=BG_DARK)
|
|
||||||
|
|
||||||
# Set up the UI
|
# Set up the UI
|
||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
|
|
||||||
# Start checking for updates
|
# Start checking for updates
|
||||||
self.check_for_updates()
|
self.check_for_updates()
|
||||||
|
|
||||||
|
# Handle close window event
|
||||||
|
self.dialog.protocol("WM_DELETE_WINDOW", self.close)
|
||||||
|
|
||||||
def center_on_parent(self):
|
def center_on_parent(self):
|
||||||
"""Center the dialog on the parent window."""
|
"""Center the dialog on the parent window."""
|
||||||
self.dialog.update_idletasks()
|
self.dialog.update_idletasks()
|
||||||
@@ -71,9 +75,94 @@ class UpdateDialog:
|
|||||||
|
|
||||||
def setup_ui(self):
|
def setup_ui(self):
|
||||||
"""Set up the dialog UI."""
|
"""Set up the dialog UI."""
|
||||||
# Button frame (Create first to pack at bottom)
|
# Main container (CTk has its own bg, so we don't strictly need a frame, but for padding)
|
||||||
self.button_frame = tk.Frame(self.dialog, bg=BG_DARK, pady=20, padx=20)
|
main_frame = ctk.CTkFrame(self.dialog, fg_color="transparent")
|
||||||
self.button_frame.pack(side=tk.BOTTOM, fill=tk.X)
|
main_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
self.title_label = ctk.CTkLabel(
|
||||||
|
main_frame,
|
||||||
|
text="🔄 Checking for Updates...",
|
||||||
|
font=FONT_HEADER,
|
||||||
|
text_color=ACCENT_PRIMARY
|
||||||
|
)
|
||||||
|
self.title_label.pack(pady=(0, 10))
|
||||||
|
|
||||||
|
# Status message
|
||||||
|
self.status_label = ctk.CTkLabel(
|
||||||
|
main_frame,
|
||||||
|
text="Connecting to GitHub...",
|
||||||
|
font=FONT_BODY,
|
||||||
|
text_color=TEXT_MUTED,
|
||||||
|
wraplength=460
|
||||||
|
)
|
||||||
|
self.status_label.pack(pady=(0, 10))
|
||||||
|
|
||||||
|
# Version info frame
|
||||||
|
self.version_frame = ctk.CTkFrame(main_frame, fg_color=BG_MEDIUM)
|
||||||
|
self.version_frame.pack(fill=tk.X, pady=(0, 15), padx=5)
|
||||||
|
|
||||||
|
self.current_version_label = ctk.CTkLabel(
|
||||||
|
self.version_frame,
|
||||||
|
text=f"Current Version: v{get_current_version()}",
|
||||||
|
font=FONT_BODY,
|
||||||
|
text_color=TEXT_SECONDARY
|
||||||
|
)
|
||||||
|
self.current_version_label.pack(anchor='w', padx=15, pady=5)
|
||||||
|
|
||||||
|
self.new_version_label = ctk.CTkLabel(
|
||||||
|
self.version_frame,
|
||||||
|
text="Latest Version: Checking...",
|
||||||
|
font=FONT_BODY,
|
||||||
|
text_color=TEXT_SECONDARY
|
||||||
|
)
|
||||||
|
self.new_version_label.pack(anchor='w', padx=15, pady=5)
|
||||||
|
|
||||||
|
# Release Notes Area
|
||||||
|
self.notes_label = ctk.CTkLabel(
|
||||||
|
main_frame,
|
||||||
|
text="What's New:",
|
||||||
|
font=FONT_BODY_BOLD,
|
||||||
|
text_color=TEXT_PRIMARY
|
||||||
|
)
|
||||||
|
self.notes_label.pack(anchor='w', pady=(0, 5))
|
||||||
|
|
||||||
|
# Text box for release notes
|
||||||
|
self.notes_text = ctk.CTkTextbox(
|
||||||
|
main_frame,
|
||||||
|
height=200,
|
||||||
|
fg_color=BG_MEDIUM,
|
||||||
|
text_color=TEXT_SECONDARY,
|
||||||
|
font=FONT_SMALL,
|
||||||
|
border_width=0
|
||||||
|
)
|
||||||
|
self.notes_text.pack(fill=tk.BOTH, expand=True, pady=(0, 15))
|
||||||
|
self.notes_text.insert("1.0", "Checking for release notes...")
|
||||||
|
self.notes_text.configure(state=tk.DISABLED)
|
||||||
|
|
||||||
|
# Progress bar (hidden initially)
|
||||||
|
self.progress_frame = ctk.CTkFrame(main_frame, fg_color="transparent")
|
||||||
|
self.progress_frame.pack(fill=tk.X, pady=(0, 10))
|
||||||
|
|
||||||
|
self.progress_label = ctk.CTkLabel(
|
||||||
|
self.progress_frame,
|
||||||
|
text="",
|
||||||
|
font=FONT_SMALL,
|
||||||
|
text_color=TEXT_MUTED
|
||||||
|
)
|
||||||
|
self.progress_label.pack(anchor='w', pady=(0, 5))
|
||||||
|
|
||||||
|
self.progress_bar = ctk.CTkProgressBar(
|
||||||
|
self.progress_frame,
|
||||||
|
mode='indeterminate',
|
||||||
|
width=400
|
||||||
|
)
|
||||||
|
self.progress_bar.pack(fill=tk.X)
|
||||||
|
self.progress_bar.start()
|
||||||
|
|
||||||
|
# Button frame
|
||||||
|
self.button_frame = ctk.CTkFrame(self.dialog, fg_color="transparent")
|
||||||
|
self.button_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=20, pady=20)
|
||||||
|
|
||||||
# Close button
|
# Close button
|
||||||
self.close_button = create_styled_button(
|
self.close_button = create_styled_button(
|
||||||
@@ -91,101 +180,7 @@ class UpdateDialog:
|
|||||||
command=self.start_download,
|
command=self.start_download,
|
||||||
style_type='accent'
|
style_type='accent'
|
||||||
)
|
)
|
||||||
# We don't pack it yet
|
# Pack when ready
|
||||||
|
|
||||||
# Main container
|
|
||||||
main_frame = tk.Frame(self.dialog, bg=BG_DARK, padx=25, pady=20)
|
|
||||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
# Title
|
|
||||||
self.title_label = tk.Label(
|
|
||||||
main_frame,
|
|
||||||
text="🔄 Checking for Updates...",
|
|
||||||
font=FONT_HEADER,
|
|
||||||
bg=BG_DARK,
|
|
||||||
fg=ACCENT_PRIMARY
|
|
||||||
)
|
|
||||||
self.title_label.pack(pady=(0, 10))
|
|
||||||
|
|
||||||
# Status message
|
|
||||||
self.status_label = tk.Label(
|
|
||||||
main_frame,
|
|
||||||
text="Connecting to GitHub...",
|
|
||||||
font=FONT_BODY,
|
|
||||||
bg=BG_DARK,
|
|
||||||
fg=TEXT_MUTED,
|
|
||||||
wraplength=460
|
|
||||||
)
|
|
||||||
self.status_label.pack(pady=(0, 10))
|
|
||||||
|
|
||||||
# Version info frame
|
|
||||||
self.version_frame = tk.Frame(main_frame, bg=BG_MEDIUM, padx=15, pady=10)
|
|
||||||
self.version_frame.pack(fill=tk.X, pady=(0, 15))
|
|
||||||
|
|
||||||
self.current_version_label = tk.Label(
|
|
||||||
self.version_frame,
|
|
||||||
text=f"Current Version: v{get_current_version()}",
|
|
||||||
font=FONT_BODY,
|
|
||||||
bg=BG_MEDIUM,
|
|
||||||
fg=TEXT_SECONDARY
|
|
||||||
)
|
|
||||||
self.current_version_label.pack(anchor='w')
|
|
||||||
|
|
||||||
self.new_version_label = tk.Label(
|
|
||||||
self.version_frame,
|
|
||||||
text="Latest Version: Checking...",
|
|
||||||
font=FONT_BODY,
|
|
||||||
bg=BG_MEDIUM,
|
|
||||||
fg=TEXT_SECONDARY
|
|
||||||
)
|
|
||||||
self.new_version_label.pack(anchor='w')
|
|
||||||
|
|
||||||
# Release Notes Area
|
|
||||||
self.notes_label = tk.Label(
|
|
||||||
main_frame,
|
|
||||||
text="What's New:",
|
|
||||||
font=FONT_BODY_BOLD,
|
|
||||||
bg=BG_DARK,
|
|
||||||
fg=TEXT_PRIMARY
|
|
||||||
)
|
|
||||||
self.notes_label.pack(anchor='w', pady=(0, 5))
|
|
||||||
|
|
||||||
# Text box for release notes
|
|
||||||
self.notes_text = scrolledtext.ScrolledText(
|
|
||||||
main_frame,
|
|
||||||
height=10,
|
|
||||||
bg=BG_MEDIUM,
|
|
||||||
fg=TEXT_SECONDARY,
|
|
||||||
font=FONT_SMALL,
|
|
||||||
borderwidth=0,
|
|
||||||
highlightthickness=0,
|
|
||||||
padx=10,
|
|
||||||
pady=10
|
|
||||||
)
|
|
||||||
self.notes_text.pack(fill=tk.BOTH, expand=True, pady=(0, 15))
|
|
||||||
self.notes_text.insert(tk.END, "Checking for release notes...")
|
|
||||||
self.notes_text.config(state=tk.DISABLED)
|
|
||||||
|
|
||||||
# Progress bar (hidden initially)
|
|
||||||
self.progress_frame = tk.Frame(main_frame, bg=BG_DARK)
|
|
||||||
self.progress_frame.pack(fill=tk.X, pady=(0, 10))
|
|
||||||
|
|
||||||
self.progress_label = tk.Label(
|
|
||||||
self.progress_frame,
|
|
||||||
text="",
|
|
||||||
font=FONT_SMALL,
|
|
||||||
bg=BG_DARK,
|
|
||||||
fg=TEXT_MUTED
|
|
||||||
)
|
|
||||||
self.progress_label.pack(anchor='w', pady=(0, 5))
|
|
||||||
|
|
||||||
self.progress_bar = ttk.Progressbar(
|
|
||||||
self.progress_frame,
|
|
||||||
mode='indeterminate',
|
|
||||||
length=460
|
|
||||||
)
|
|
||||||
self.progress_bar.pack(fill=tk.X)
|
|
||||||
self.progress_bar.start(10)
|
|
||||||
|
|
||||||
def check_for_updates(self):
|
def check_for_updates(self):
|
||||||
"""Check for updates in a background thread."""
|
"""Check for updates in a background thread."""
|
||||||
@@ -202,19 +197,19 @@ class UpdateDialog:
|
|||||||
self.progress_frame.pack_forget() # Hide progress bar when check is done
|
self.progress_frame.pack_forget() # Hide progress bar when check is done
|
||||||
|
|
||||||
# Enable text box to update it
|
# Enable text box to update it
|
||||||
self.notes_text.config(state=tk.NORMAL)
|
self.notes_text.configure(state=tk.NORMAL)
|
||||||
self.notes_text.delete(1.0, tk.END)
|
self.notes_text.delete("1.0", tk.END)
|
||||||
|
|
||||||
if self.update_info:
|
if self.update_info:
|
||||||
# Update available!
|
# Update available!
|
||||||
self.title_label.config(text="🎉 Update Available!")
|
self.title_label.configure(text="🎉 Update Available!")
|
||||||
self.status_label.config(
|
self.status_label.configure(
|
||||||
text="A new version is available.",
|
text="A new version is available.",
|
||||||
fg=ACCENT_SUCCESS
|
text_color=ACCENT_SUCCESS
|
||||||
)
|
)
|
||||||
self.new_version_label.config(
|
self.new_version_label.configure(
|
||||||
text=f"Latest Version: {self.update_info['new_version']}",
|
text=f"Latest Version: {self.update_info['new_version']}",
|
||||||
fg=ACCENT_SUCCESS
|
text_color=ACCENT_SUCCESS
|
||||||
)
|
)
|
||||||
|
|
||||||
# Show Release Notes
|
# Show Release Notes
|
||||||
@@ -225,18 +220,18 @@ class UpdateDialog:
|
|||||||
self.update_button.pack(side=tk.RIGHT, padx=(0, 10))
|
self.update_button.pack(side=tk.RIGHT, padx=(0, 10))
|
||||||
else:
|
else:
|
||||||
# Up to date or error
|
# Up to date or error
|
||||||
self.title_label.config(text="✅ You're Up to Date!")
|
self.title_label.configure(text="✅ You're Up to Date!")
|
||||||
self.status_label.config(
|
self.status_label.configure(
|
||||||
text=f"You are running the latest version.",
|
text=f"You are running the latest version.",
|
||||||
fg=TEXT_SECONDARY
|
text_color=TEXT_SECONDARY
|
||||||
)
|
)
|
||||||
self.new_version_label.config(
|
self.new_version_label.configure(
|
||||||
text=f"Latest Version: v{get_current_version()}",
|
text=f"Latest Version: v{get_current_version()}",
|
||||||
fg=ACCENT_SUCCESS
|
text_color=ACCENT_SUCCESS
|
||||||
)
|
)
|
||||||
self.notes_text.insert(tk.END, "You are using the latest version of Umamusume Support Card Manager.\n\nEnjoy!")
|
self.notes_text.insert(tk.END, "You are using the latest version of Umamusume Support Card Manager.\n\nEnjoy!")
|
||||||
|
|
||||||
self.notes_text.config(state=tk.DISABLED)
|
self.notes_text.configure(state=tk.DISABLED)
|
||||||
|
|
||||||
def start_download(self):
|
def start_download(self):
|
||||||
"""Start downloading the update."""
|
"""Start downloading the update."""
|
||||||
@@ -244,22 +239,22 @@ class UpdateDialog:
|
|||||||
return
|
return
|
||||||
|
|
||||||
self.is_downloading = True
|
self.is_downloading = True
|
||||||
self.update_button.config(state=tk.DISABLED, text="Downloading...")
|
self.update_button.configure(state=tk.DISABLED, text="Downloading...")
|
||||||
self.close_button.config(state=tk.DISABLED)
|
self.close_button.configure(state=tk.DISABLED)
|
||||||
|
|
||||||
self.title_label.config(text="⬇️ Downloading Update...")
|
self.title_label.configure(text="⬇️ Downloading Update...")
|
||||||
self.status_label.config(text="Please wait...", fg=TEXT_MUTED)
|
self.status_label.configure(text="Please wait...", text_color=TEXT_MUTED)
|
||||||
|
|
||||||
# Configure progress bar for determinate mode
|
# Configure progress bar for determinate mode
|
||||||
self.progress_frame.pack(fill=tk.X, pady=(0, 10)) # Show progress frame again
|
self.progress_frame.pack(fill=tk.X, pady=(0, 10))
|
||||||
self.progress_bar.config(mode='determinate', maximum=100)
|
self.progress_bar.configure(mode='determinate')
|
||||||
|
self.progress_bar.set(0)
|
||||||
self.progress_bar.pack(fill=tk.X)
|
self.progress_bar.pack(fill=tk.X)
|
||||||
self.progress_bar['value'] = 0
|
|
||||||
|
|
||||||
def download():
|
def download():
|
||||||
def progress_callback(downloaded, total):
|
def progress_callback(downloaded, total):
|
||||||
if total > 0:
|
if total > 0:
|
||||||
percent = int((downloaded / total) * 100)
|
percent = downloaded / total # 0.0 to 1.0 for CTk
|
||||||
mb_downloaded = downloaded / (1024 * 1024)
|
mb_downloaded = downloaded / (1024 * 1024)
|
||||||
mb_total = total / (1024 * 1024)
|
mb_total = total / (1024 * 1024)
|
||||||
self.dialog.after(0, lambda: self.update_progress(percent, mb_downloaded, mb_total))
|
self.dialog.after(0, lambda: self.update_progress(percent, mb_downloaded, mb_total))
|
||||||
@@ -270,44 +265,44 @@ class UpdateDialog:
|
|||||||
self.download_thread = threading.Thread(target=download, daemon=True)
|
self.download_thread = threading.Thread(target=download, daemon=True)
|
||||||
self.download_thread.start()
|
self.download_thread.start()
|
||||||
|
|
||||||
def update_progress(self, percent: int, downloaded_mb: float, total_mb: float):
|
def update_progress(self, percent: float, downloaded_mb: float, total_mb: float):
|
||||||
"""Update the progress bar."""
|
"""Update the progress bar."""
|
||||||
self.progress_bar['value'] = percent
|
self.progress_bar.set(percent)
|
||||||
self.progress_label.config(text=f"Downloaded: {downloaded_mb:.1f} MB / {total_mb:.1f} MB ({percent}%)")
|
self.progress_label.configure(text=f"Downloaded: {downloaded_mb:.1f} MB / {total_mb:.1f} MB ({int(percent*100)}%)")
|
||||||
|
|
||||||
def download_complete(self, download_path: Optional[str]):
|
def download_complete(self, download_path: Optional[str]):
|
||||||
"""Called when the download is complete."""
|
"""Called when the download is complete."""
|
||||||
self.is_downloading = False
|
self.is_downloading = False
|
||||||
|
|
||||||
if download_path:
|
if download_path:
|
||||||
self.title_label.config(text="✅ Download Complete!")
|
self.title_label.configure(text="✅ Download Complete!")
|
||||||
self.status_label.config(
|
self.status_label.configure(
|
||||||
text="Update ready to install.",
|
text="Update ready to install.",
|
||||||
fg=ACCENT_SUCCESS
|
text_color=ACCENT_SUCCESS
|
||||||
)
|
)
|
||||||
|
|
||||||
# Change button to install
|
# Change button to install
|
||||||
self.update_button.config(
|
self.update_button.configure(
|
||||||
state=tk.NORMAL,
|
state=tk.NORMAL,
|
||||||
text="🔄 Install & Restart",
|
text="🔄 Install & Restart",
|
||||||
command=lambda: self.install_update(download_path)
|
command=lambda: self.install_update(download_path)
|
||||||
)
|
)
|
||||||
self.close_button.config(state=tk.NORMAL)
|
self.close_button.configure(state=tk.NORMAL)
|
||||||
else:
|
else:
|
||||||
self.title_label.config(text="❌ Download Failed")
|
self.title_label.configure(text="❌ Download Failed")
|
||||||
self.status_label.config(
|
self.status_label.configure(
|
||||||
text="Failed not download update.",
|
text="Failed not download update.",
|
||||||
fg=ACCENT_ERROR
|
text_color=ACCENT_ERROR
|
||||||
)
|
)
|
||||||
self.update_button.config(state=tk.NORMAL, text="⬇️ Retry Download")
|
self.update_button.configure(state=tk.NORMAL, text="⬇️ Retry Download")
|
||||||
self.close_button.config(state=tk.NORMAL)
|
self.close_button.configure(state=tk.NORMAL)
|
||||||
|
|
||||||
def install_update(self, download_path: str):
|
def install_update(self, download_path: str):
|
||||||
"""Install the downloaded update."""
|
"""Install the downloaded update."""
|
||||||
self.title_label.config(text="🔄 Installing Update...")
|
self.title_label.configure(text="🔄 Installing Update...")
|
||||||
self.status_label.config(text="Applying update...", fg=TEXT_MUTED)
|
self.status_label.configure(text="Applying update...", text_color=TEXT_MUTED)
|
||||||
self.update_button.config(state=tk.DISABLED)
|
self.update_button.configure(state=tk.DISABLED)
|
||||||
self.close_button.config(state=tk.DISABLED)
|
self.close_button.configure(state=tk.DISABLED)
|
||||||
|
|
||||||
if apply_update(download_path):
|
if apply_update(download_path):
|
||||||
# Exit the application - the updater script will restart it
|
# Exit the application - the updater script will restart it
|
||||||
@@ -322,7 +317,6 @@ class UpdateDialog:
|
|||||||
)
|
)
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Close the dialog."""
|
"""Close the dialog."""
|
||||||
if self.on_close_callback:
|
if self.on_close_callback:
|
||||||
@@ -330,15 +324,8 @@ class UpdateDialog:
|
|||||||
self.dialog.destroy()
|
self.dialog.destroy()
|
||||||
|
|
||||||
|
|
||||||
def show_update_dialog(parent: tk.Tk, on_close_callback: Optional[Callable] = None) -> UpdateDialog:
|
def show_update_dialog(parent: ctk.CTk, on_close_callback: Optional[Callable] = None) -> UpdateDialog:
|
||||||
"""
|
"""
|
||||||
Show the update dialog.
|
Show the update dialog.
|
||||||
|
|
||||||
Args:
|
|
||||||
parent: The parent Tk window
|
|
||||||
on_close_callback: Optional callback when dialog is closed
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The UpdateDialog instance
|
|
||||||
"""
|
"""
|
||||||
return UpdateDialog(parent, on_close_callback)
|
return UpdateDialog(parent, on_close_callback)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
import os
|
import os
|
||||||
|
|
||||||
DB_PATH = os.path.join("database", "umamusume.db")
|
DB_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "database", "umamusume.db")
|
||||||
|
|
||||||
def check_schema():
|
def check_schema():
|
||||||
if not os.path.exists(DB_PATH):
|
if not os.path.exists(DB_PATH):
|
||||||
@@ -2,7 +2,7 @@ import sqlite3
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
def debug_db():
|
def debug_db():
|
||||||
db_path = "database/umamusume.db"
|
db_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "database", "umamusume.db")
|
||||||
if not os.path.exists(db_path):
|
if not os.path.exists(db_path):
|
||||||
print(f"Database not found at {db_path}")
|
print(f"Database not found at {db_path}")
|
||||||
return
|
return
|
||||||
@@ -3,7 +3,7 @@ import sys
|
|||||||
from playwright.sync_api import sync_playwright
|
from playwright.sync_api import sync_playwright
|
||||||
|
|
||||||
# Add parent dir to path
|
# Add parent dir to path
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
def debug_kitasan_scrape():
|
def debug_kitasan_scrape():
|
||||||
url = "https://gametora.com/umamusume/supports/30028-kitasan-black"
|
url = "https://gametora.com/umamusume/supports/30028-kitasan-black"
|
||||||
@@ -3,7 +3,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
# Ensure we can import from the project
|
# Ensure we can import from the project
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
from scraper.gametora_scraper import scrape_support_card, sync_playwright
|
from scraper.gametora_scraper import scrape_support_card, sync_playwright
|
||||||
from db.db_queries import DB_PATH, repair_orphaned_data, cleanup_orphaned_data
|
from db.db_queries import DB_PATH, repair_orphaned_data, cleanup_orphaned_data
|
||||||
@@ -3,7 +3,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
# Add parent dir to path
|
# Add parent dir to path
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
from scraper.gametora_scraper import scrape_support_card, sync_playwright
|
from scraper.gametora_scraper import scrape_support_card, sync_playwright
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
# Add parent dir to path
|
# Add parent dir to path
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
from scraper.gametora_scraper import scrape_support_card, sync_playwright
|
from scraper.gametora_scraper import scrape_support_card, sync_playwright
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
# Add parent dir to path
|
# Add parent dir to path
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
from scraper.gametora_scraper import scrape_support_card, sync_playwright
|
from scraper.gametora_scraper import scrape_support_card, sync_playwright
|
||||||
from db.db_queries import get_conn
|
from db.db_queries import get_conn
|
||||||
@@ -5,7 +5,7 @@ from playwright.sync_api import sync_playwright
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
# Add project root to path
|
# Add project root to path
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
def test_scrape_events(url):
|
def test_scrape_events(url):
|
||||||
with sync_playwright() as p:
|
with sync_playwright() as p:
|
||||||
@@ -549,242 +549,200 @@ def scrape_hints(page, card_id, cur):
|
|||||||
print(f" Found {len(hints)} hints")
|
print(f" Found {len(hints)} hints")
|
||||||
|
|
||||||
def scrape_events(page, card_id, cur):
|
def scrape_events(page, card_id, cur):
|
||||||
"""Scrape the LAST chain event (Golden Perk) with OR options"""
|
"""Scrape all events including Chain, Dates, Random and Special"""
|
||||||
|
|
||||||
# Use a flag to avoid adding multiple console listeners
|
# Use a flag to avoid adding multiple console listeners
|
||||||
if not hasattr(page, "_console_attached"):
|
if not hasattr(page, "_console_attached"):
|
||||||
page.on("console", lambda msg: print(f" [JS Console] {msg.text}") if "scrapping" not in msg.text.lower() else None)
|
page.on("console", lambda msg: print(f" [JS Console] {msg.text}") if "scrapping" not in msg.text.lower() else None)
|
||||||
page._console_attached = True
|
page._console_attached = True
|
||||||
|
|
||||||
# 1. First, build a map of skills from the 'Skills from events' summary section
|
# 1. Build a map of skills from the 'Skills from events' summary section
|
||||||
# This helps us identify which skills are Rare (Gold)
|
# This remains useful for identifying golden skills.
|
||||||
skill_rarity_map = page.evaluate("""
|
skill_rarity_map = page.evaluate("""
|
||||||
() => {
|
() => {
|
||||||
const map = {};
|
const map = {};
|
||||||
console.log("Building Skill Rarity Map...");
|
|
||||||
|
|
||||||
// 1. Find all skill containers. They usually have a name and a 'Details' button.
|
|
||||||
// In the "Skills from events" or "Support hints" sections.
|
|
||||||
const containers = Array.from(document.querySelectorAll('div')).filter(d =>
|
const containers = Array.from(document.querySelectorAll('div')).filter(d =>
|
||||||
(d.innerText.includes('Details') || d.innerText.includes('Reward')) && d.innerText.length < 500
|
(d.innerText.includes('Details') || d.innerText.includes('Reward')) && d.innerText.length < 500
|
||||||
);
|
);
|
||||||
|
|
||||||
containers.forEach(c => {
|
containers.forEach(c => {
|
||||||
// Try to extract the skill name. It's usually the first text node or a bold tag.
|
|
||||||
const nameNode = c.querySelector('b, span[font-weight="bold"], div[font-weight="bold"]');
|
const nameNode = c.querySelector('b, span[font-weight="bold"], div[font-weight="bold"]');
|
||||||
let name = "";
|
let name = nameNode ? nameNode.innerText.trim() : c.innerText.split('Details')[0].replace(/\\n/g, ' ').trim();
|
||||||
if (nameNode) {
|
|
||||||
name = nameNode.innerText.trim();
|
|
||||||
} else {
|
|
||||||
// Fallback to text before 'Details'
|
|
||||||
name = c.innerText.split('Details')[0].replace(/\\n/g, ' ').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name && name.length > 2) {
|
if (name && name.length > 2) {
|
||||||
const style = window.getComputedStyle(c);
|
const style = window.getComputedStyle(c);
|
||||||
const nameStyle = nameNode ? window.getComputedStyle(nameNode) : null;
|
const nameStyle = nameNode ? window.getComputedStyle(nameNode) : null;
|
||||||
|
|
||||||
// Golden skills have a specific background
|
|
||||||
const isGold = style.backgroundImage.includes('linear-gradient') ||
|
const isGold = style.backgroundImage.includes('linear-gradient') ||
|
||||||
style.backgroundColor.includes('rgb(255, 193, 7)') ||
|
style.backgroundColor.includes('rgb(255, 193, 7)') ||
|
||||||
(nameStyle && nameStyle.color === 'rgb(255, 193, 7)') ||
|
(nameStyle && nameStyle.color === 'rgb(255, 193, 7)') ||
|
||||||
c.className.includes('kkspcu') ||
|
c.className.includes('kkspcu');
|
||||||
c.innerHTML.includes('kkspcu');
|
|
||||||
|
|
||||||
const normalized = name.toLowerCase().replace(/\\s+/g, ' ').replace(/[()()-]/g, '').trim();
|
const normalized = name.toLowerCase().replace(/\\s+/g, ' ').replace(/[()()-]/g, '').trim();
|
||||||
map[normalized] = isGold;
|
map[normalized] = isGold;
|
||||||
console.log(`Mapped Skill: "${name}" [${normalized}] -> Gold: ${isGold}`);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# Scroll to the Events section specifically
|
# 2. Scrape all event types
|
||||||
print(" Ensuring events are loaded...")
|
print(" Scraping all event categories...")
|
||||||
page.evaluate("() => { const h = Array.from(document.querySelectorAll('h2, h1, div')).find(el => el.innerText.toLowerCase().includes('training events')); if (h) h.scrollIntoView(); }")
|
all_events_data = page.evaluate("""
|
||||||
page.wait_for_timeout(1000)
|
|
||||||
|
|
||||||
# 2. Scrape ONLY the LAST chain event (Golden Perk) with OR options
|
|
||||||
golden_perk_data = page.evaluate("""
|
|
||||||
async () => {
|
async () => {
|
||||||
console.log("Scraping Golden Perk (last chain event)...");
|
const results = [];
|
||||||
|
|
||||||
// Find all chain event buttons
|
// Define categories to look for
|
||||||
const getChainEventButtons = () => {
|
const categories = [
|
||||||
const buttons = [];
|
{ label: 'Chain Events', type: 'Chain' },
|
||||||
// Look for "Chain Events" text (case-insensitive substring)
|
{ label: 'Dates', type: 'Date' },
|
||||||
const labels = Array.from(document.querySelectorAll('div, span, h2, h3, h4')).filter(el =>
|
{ label: 'Random Events', type: 'Random' },
|
||||||
el.innerText.toLowerCase().includes('chain events') && el.innerText.trim().length < 20
|
{ label: 'Special Events', type: 'Special' }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const cat of categories) {
|
||||||
|
// Find category headers
|
||||||
|
const headers = Array.from(document.querySelectorAll('div, span, h2, h3, h4')).filter(el =>
|
||||||
|
el.innerText.trim() === cat.label && el.children.length === 0
|
||||||
);
|
);
|
||||||
|
|
||||||
labels.forEach(label => {
|
for (const header of headers) {
|
||||||
// The buttons are usually in the same container or next container
|
// Find buttons in the following siblings or parent siblings
|
||||||
let container = label.parentElement;
|
let container = header.parentElement;
|
||||||
|
let foundButtons = [];
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
while (container && container.querySelectorAll('button').length === 0 && attempts < 5) {
|
|
||||||
container = container.nextElementSibling || container.parentElement;
|
|
||||||
attempts++;
|
|
||||||
if (container && container.tagName === 'BODY') break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (container) {
|
while (container && foundButtons.length === 0 && attempts < 5) {
|
||||||
const btns = Array.from(container.querySelectorAll('button'));
|
foundButtons = Array.from(container.querySelectorAll('button')).filter(btn => {
|
||||||
btns.forEach(btn => {
|
|
||||||
const text = btn.innerText.trim();
|
|
||||||
const style = window.getComputedStyle(btn);
|
const style = window.getComputedStyle(btn);
|
||||||
const isVisible = style.display !== 'none' && style.visibility !== 'hidden';
|
return style.display !== 'none' && style.visibility !== 'hidden';
|
||||||
|
|
||||||
// Look for arrows (regular or heavy)
|
|
||||||
if (isVisible && (text.includes('>') || text.includes('❯'))) {
|
|
||||||
buttons.push(btn);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (foundButtons.length === 0) {
|
||||||
|
// Check next siblings of the header's ancestors
|
||||||
|
let sibling = header;
|
||||||
|
let parent = header.parentElement;
|
||||||
|
while(parent && parent.tagName !== 'BODY') {
|
||||||
|
if (parent.innerText.includes(cat.label)) {
|
||||||
|
const next = parent.nextElementSibling;
|
||||||
|
if (next) {
|
||||||
|
const nextBtns = Array.from(next.querySelectorAll('button'));
|
||||||
|
if (nextBtns.length > 0) {
|
||||||
|
foundButtons = nextBtns;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parent = parent.parentElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
container = container.parentElement;
|
||||||
|
attempts++;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
return buttons;
|
|
||||||
};
|
|
||||||
|
|
||||||
const buttons = getChainEventButtons();
|
// Scrape each button in the category
|
||||||
console.log(`Found ${buttons.length} chain event buttons`);
|
for (const btn of foundButtons) {
|
||||||
|
const eventName = btn.innerText.trim();
|
||||||
|
if (!eventName || results.some(r => r.name === eventName)) continue;
|
||||||
|
|
||||||
if (buttons.length === 0) {
|
// Count arrows for chain/date importance
|
||||||
return null;
|
const arrows = (eventName.match(/>|❯/g) || []).length;
|
||||||
}
|
|
||||||
|
|
||||||
let goldenPerkButton = null;
|
try {
|
||||||
let maxArrows = 0;
|
btn.scrollIntoViewIfNeeded ? btn.scrollIntoViewIfNeeded() : null;
|
||||||
|
await new Promise(r => setTimeout(r, 100));
|
||||||
|
btn.click();
|
||||||
|
await new Promise(r => setTimeout(r, 500));
|
||||||
|
|
||||||
for (const btn of buttons) {
|
const popovers = Array.from(document.querySelectorAll('div')).filter(d =>
|
||||||
const text = btn.innerText.trim();
|
d.innerText.includes(eventName) &&
|
||||||
// Count both regular and heavy arrows
|
window.getComputedStyle(d).zIndex > 50 &&
|
||||||
const arrowCount = (text.match(/>|❯/g) || []).length;
|
d.innerText.length < 2500
|
||||||
|
);
|
||||||
|
|
||||||
// If it has three heavy arrows, it's almost certainly the golden perk
|
if (popovers.length > 0) {
|
||||||
if (text.includes('❯❯❯')) {
|
const pop = popovers[popovers.length - 1];
|
||||||
goldenPerkButton = btn;
|
const hasOrDivider = pop.querySelector('[class*="divider_or"]') !== null ||
|
||||||
break;
|
pop.innerText.includes('Randomly either') ||
|
||||||
}
|
pop.innerText.toLowerCase().includes(' or ');
|
||||||
|
|
||||||
if (arrowCount > maxArrows) {
|
const skillLinks = Array.from(pop.querySelectorAll('span, a')).filter(el =>
|
||||||
maxArrows = arrowCount;
|
el.innerText.length > 2 &&
|
||||||
goldenPerkButton = btn;
|
!el.innerText.includes('Energy') &&
|
||||||
}
|
(window.getComputedStyle(el).color === 'rgb(102, 107, 255)' ||
|
||||||
}
|
el.className.includes('linkcolor'))
|
||||||
|
);
|
||||||
|
|
||||||
if (!goldenPerkButton) {
|
const skills = [];
|
||||||
console.log("No golden perk button found");
|
skillLinks.forEach(link => {
|
||||||
return null;
|
const sName = link.innerText.trim();
|
||||||
}
|
if (sName && !skills.some(s => s.name === sName)) {
|
||||||
|
skills.push({ name: sName, is_or: hasOrDivider });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const eventName = goldenPerkButton.innerText.trim();
|
results.push({
|
||||||
console.log(`Found Golden Perk: ${eventName} (${maxArrows} arrows)`);
|
name: eventName,
|
||||||
|
type: cat.type,
|
||||||
|
skills: skills,
|
||||||
|
arrows: arrows
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
document.body.click();
|
||||||
// Click to open popover
|
await new Promise(r => setTimeout(r, 100));
|
||||||
goldenPerkButton.scrollIntoViewIfNeeded ? goldenPerkButton.scrollIntoViewIfNeeded() : null;
|
} catch (err) {
|
||||||
await new Promise(r => setTimeout(r, 100));
|
console.log(`Failed to scrape event ${eventName}: ${err.message}`);
|
||||||
goldenPerkButton.click();
|
}
|
||||||
await new Promise(r => setTimeout(r, 600));
|
|
||||||
|
|
||||||
// Find popover
|
|
||||||
const popovers = Array.from(document.querySelectorAll('div')).filter(d =>
|
|
||||||
d.innerText.includes(eventName) &&
|
|
||||||
window.getComputedStyle(d).zIndex > 50 &&
|
|
||||||
d.innerText.length < 2500
|
|
||||||
);
|
|
||||||
|
|
||||||
if (popovers.length === 0) {
|
|
||||||
console.log(`Popover NOT found for ${eventName}`);
|
|
||||||
document.body.click();
|
|
||||||
return { name: eventName, type: 'Chain', skills: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const pop = popovers[popovers.length - 1];
|
|
||||||
console.log(`Found popover for ${eventName}`);
|
|
||||||
|
|
||||||
// Check for OR structure - look for "Randomly either" or "or" divider
|
|
||||||
const hasOrDivider = pop.querySelector('[class*="divider_or"]') !== null ||
|
|
||||||
pop.innerText.includes('Randomly either') ||
|
|
||||||
pop.innerText.toLowerCase().includes(' or ');
|
|
||||||
|
|
||||||
// Find all skill names (purple/blue links)
|
|
||||||
const skillLinks = Array.from(pop.querySelectorAll('span, a')).filter(el =>
|
|
||||||
el.innerText.length > 2 &&
|
|
||||||
!el.innerText.includes('Energy') &&
|
|
||||||
!el.innerText.includes('bond') &&
|
|
||||||
(window.getComputedStyle(el).color === 'rgb(102, 107, 255)' ||
|
|
||||||
el.className.includes('linkcolor'))
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`Found ${skillLinks.length} potential skills in popover`);
|
|
||||||
|
|
||||||
const skills = [];
|
|
||||||
skillLinks.forEach(link => {
|
|
||||||
const skillName = link.innerText.trim();
|
|
||||||
if (skillName && skillName.length > 2 && !skills.some(s => s.name === skillName)) {
|
|
||||||
// If there's an OR divider, all skills in this popover are part of OR groups
|
|
||||||
const isOr = hasOrDivider;
|
|
||||||
skills.push({ name: skillName, is_or: isOr });
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
// Close popover
|
|
||||||
document.body.click();
|
|
||||||
await new Promise(r => setTimeout(r, 200));
|
|
||||||
|
|
||||||
return { name: eventName, type: 'Chain', skills: skills };
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.log(`Error clicking ${eventName}: ${err.message}`);
|
|
||||||
return { name: eventName, type: 'Chain', skills: [] };
|
|
||||||
}
|
}
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# 3. Store ONLY the golden perk in database
|
# 3. Store all found events and identify golden skills
|
||||||
if golden_perk_data:
|
if all_events_data:
|
||||||
cur.execute("""
|
# Determine max arrows for Chain and Dates to identify final step
|
||||||
INSERT INTO support_events (card_id, event_name, event_type)
|
max_arrows = {
|
||||||
VALUES (?, ?, ?)
|
'Chain': max([e['arrows'] for e in all_events_data if e['type'] == 'Chain'] + [0]),
|
||||||
""", (card_id, golden_perk_data['name'], golden_perk_data['type']))
|
'Date': max([e['arrows'] for e in all_events_data if e['type'] == 'Date'] + [0])
|
||||||
event_id = cur.lastrowid
|
}
|
||||||
|
|
||||||
for skill in golden_perk_data['skills']:
|
for event in all_events_data:
|
||||||
# Normalization helper
|
cur.execute("INSERT INTO support_events (card_id, event_name, event_type) VALUES (?, ?, ?)",
|
||||||
def normalize(s):
|
(card_id, event['name'], event['type']))
|
||||||
return s.lower().replace(" hint +1", "").replace(" hint +3", "").replace(" hint +5", "").replace(" hint +", "").strip().replace(" ", " ").replace("-", "").replace("(", "").replace(")", "").replace(" ", "")
|
event_id = cur.lastrowid
|
||||||
|
|
||||||
skill_name = normalize(skill['name'])
|
for skill in event['skills']:
|
||||||
|
def normalize(s):
|
||||||
|
# Remove hint suffix and special characters
|
||||||
|
s = s.lower().split(' hint +')[0]
|
||||||
|
return re.sub(r'[()()\-\s\+]', '', s).strip()
|
||||||
|
|
||||||
# Use extra aggressive name matching against the map values
|
n_name = normalize(skill['name'])
|
||||||
# (The map keys are already normalized)
|
is_gold = 0
|
||||||
is_gold = 0
|
for k, gold in skill_rarity_map.items():
|
||||||
for k, gold in skill_rarity_map.items():
|
if normalize(k) == n_name:
|
||||||
if normalize(k) == skill_name:
|
is_gold = 1 if gold else 0
|
||||||
is_gold = 1 if gold else 0
|
break
|
||||||
break
|
|
||||||
|
|
||||||
# Fallback 1: If it's a chain event and specifically the last one, it's almost certainly gold
|
# Heuristic: If it's the last step of a Chain or Date, it's likely gold
|
||||||
if not is_gold and golden_perk_data.get('type') == 'Chain':
|
if not is_gold and event['type'] in ['Chain', 'Date']:
|
||||||
# Check for "hint" patterns which usually accompany gold perks in chain events
|
if event['arrows'] >= 3 and event['arrows'] == max_arrows[event['type']]:
|
||||||
if "hint +" in skill['name'].lower() or len(golden_perk_data['skills']) <= 2:
|
if len(event['skills']) <= 2 or "hint +" in skill['name'].lower():
|
||||||
is_gold = 1
|
is_gold = 1
|
||||||
print(f" ✨ Golden Skill Fallback (Last Chain Event): {skill['name']}")
|
print(f" ✨ Heuristic Gold: {skill['name']} in {event['name']}")
|
||||||
|
|
||||||
if is_gold:
|
if is_gold: print(f" ✨ Verified Gold: {skill['name']}")
|
||||||
print(f" ✨ Golden Skill Verified: {skill['name']}")
|
|
||||||
|
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO event_skills (event_id, skill_name, is_gold, is_or)
|
INSERT INTO event_skills (event_id, skill_name, is_gold, is_or)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?)
|
||||||
""", (event_id, skill['name'], is_gold, 1 if skill['is_or'] else 0))
|
""", (event_id, skill['name'], is_gold, 1 if skill['is_or'] else 0))
|
||||||
|
|
||||||
skill_count = len(golden_perk_data['skills'])
|
print(f" Scraped {len(all_events_data)} total events.")
|
||||||
or_count = sum(1 for s in golden_perk_data['skills'] if s['is_or'])
|
|
||||||
print(f" Golden Perk: {golden_perk_data['name']} ({skill_count} skills, {or_count} with OR)")
|
|
||||||
else:
|
else:
|
||||||
print(f" No Golden Perk found for this card")
|
print(f" No events found.")
|
||||||
|
|
||||||
def run_scraper():
|
def run_scraper():
|
||||||
""" Run the web scraper to fetch card data from GameTora.com """
|
""" Run the web scraper to fetch card data from GameTora.com """
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ This file is the single source of truth for the application version.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Semantic versioning: MAJOR.MINOR.PATCH
|
# Semantic versioning: MAJOR.MINOR.PATCH
|
||||||
VERSION: str = "13.0.3"
|
VERSION: str = "14.0.1"
|
||||||
|
|
||||||
# Application metadata
|
# Application metadata
|
||||||
APP_NAME: str = "UmamusumeCardManager"
|
APP_NAME: str = "UmamusumeCardManager"
|
||||||
|
|||||||
Reference in New Issue
Block a user