Addition Of Files
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto
|
||||||
28
.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Ignore Windows executables
|
||||||
|
*.exe
|
||||||
|
|
||||||
|
# Ignore Python bytecode
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
|
||||||
|
# Ignore virtual environment
|
||||||
|
venv
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Ignore temporary files
|
||||||
|
*.tmp
|
||||||
|
*.log
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Ignore build files
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
.build
|
||||||
|
.toc
|
||||||
|
.zip
|
||||||
|
.pkg
|
||||||
|
.txt
|
||||||
|
.html
|
||||||
|
|
||||||
38
UmamusumeCardManager.spec
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['main.py'],
|
||||||
|
pathex=[],
|
||||||
|
binaries=[],
|
||||||
|
datas=[('images', 'images'), ('database/umamusume_seed.db', 'database'), ('version.py', '.'), ('updater', 'updater')],
|
||||||
|
hiddenimports=['requests'],
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
noarchive=False,
|
||||||
|
optimize=0,
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name='UmamusumeCardManager',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
runtime_tmpdir=None,
|
||||||
|
console=False,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
)
|
||||||
37
build.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("🚀 Starting Build Process...\n")
|
||||||
|
|
||||||
|
# Get python interpreter
|
||||||
|
python_exe = sys.executable
|
||||||
|
|
||||||
|
# 1. Prepare Release Database
|
||||||
|
print("--------------------------------")
|
||||||
|
print("Step 1/2: Preparing Database")
|
||||||
|
print("--------------------------------")
|
||||||
|
script_path = os.path.join("scripts", "prepare_release_db.py")
|
||||||
|
ret = subprocess.call([python_exe, script_path])
|
||||||
|
if ret != 0:
|
||||||
|
print("❌ Error: Database preparation failed.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("\n")
|
||||||
|
|
||||||
|
# 2. Run PyInstaller
|
||||||
|
print("--------------------------------")
|
||||||
|
print("Step 2/2: Building Executable")
|
||||||
|
print("--------------------------------")
|
||||||
|
# We use 'pyinstaller' as a command. If it's not in path, we might need to assume it's a module
|
||||||
|
# Try running as module first for safety: python -m PyInstaller
|
||||||
|
ret = subprocess.call([python_exe, "-m", "PyInstaller", "UmamusumeCardManager.spec", "--noconfirm"])
|
||||||
|
if ret != 0:
|
||||||
|
print("❌ Error: PyInstaller build failed.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("\n✅ Build Complete! Executable is in in 'dist' folder.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
20
check_paths.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
|
||||||
|
DB_PATH = r"y:\Keith\umamusuma card application\database\umamusume.db"
|
||||||
|
print(f"Checking DB at: {DB_PATH}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("SELECT card_id, name, image_path FROM support_cards LIMIT 5")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
|
||||||
|
print("\nSample Card Data:")
|
||||||
|
for row in rows:
|
||||||
|
print(f"ID: {row[0]}, Name: {row[1]}, Path: {row[2]}")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
BIN
database/umamusume.db
Normal file
BIN
database/umamusume_seed.db
Normal file
154
db/db_init.py
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
"""
|
||||||
|
Database initialization module
|
||||||
|
Creates the SQLite database schema for Umamusume support cards
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
|
||||||
|
DB_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "database", "umamusume.db")
|
||||||
|
|
||||||
|
def get_conn():
|
||||||
|
"""Get database connection"""
|
||||||
|
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
||||||
|
return sqlite3.connect(DB_PATH)
|
||||||
|
|
||||||
|
def init_db(reset=False):
|
||||||
|
"""
|
||||||
|
Initialize the database with schema
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reset: If True, drops all existing tables first
|
||||||
|
"""
|
||||||
|
conn = get_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
if reset:
|
||||||
|
print("Resetting database...")
|
||||||
|
cur.execute("DROP TABLE IF EXISTS deck_slots")
|
||||||
|
cur.execute("DROP TABLE IF EXISTS user_decks")
|
||||||
|
cur.execute("DROP TABLE IF EXISTS event_skills")
|
||||||
|
cur.execute("DROP TABLE IF EXISTS support_events")
|
||||||
|
cur.execute("DROP TABLE IF EXISTS support_hints")
|
||||||
|
cur.execute("DROP TABLE IF EXISTS support_effects")
|
||||||
|
cur.execute("DROP TABLE IF EXISTS owned_cards")
|
||||||
|
cur.execute("DROP TABLE IF EXISTS support_cards")
|
||||||
|
|
||||||
|
# Support Cards - main card info
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS support_cards (
|
||||||
|
card_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
rarity TEXT,
|
||||||
|
card_type TEXT,
|
||||||
|
max_level INTEGER DEFAULT 50,
|
||||||
|
gametora_url TEXT UNIQUE,
|
||||||
|
image_path TEXT
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Effects by level - stores effect values at each level
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS support_effects (
|
||||||
|
effect_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
card_id INTEGER,
|
||||||
|
level INTEGER,
|
||||||
|
effect_name TEXT,
|
||||||
|
effect_value TEXT,
|
||||||
|
FOREIGN KEY (card_id) REFERENCES support_cards(card_id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Support Hints - training skills that can be learned
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS support_hints (
|
||||||
|
hint_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
card_id INTEGER,
|
||||||
|
hint_name TEXT,
|
||||||
|
hint_description TEXT,
|
||||||
|
FOREIGN KEY (card_id) REFERENCES support_cards(card_id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Training Events
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS support_events (
|
||||||
|
event_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
card_id INTEGER,
|
||||||
|
event_name TEXT,
|
||||||
|
event_type TEXT,
|
||||||
|
FOREIGN KEY (card_id) REFERENCES support_cards(card_id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Event Skills
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS event_skills (
|
||||||
|
skill_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
event_id INTEGER,
|
||||||
|
skill_name TEXT,
|
||||||
|
FOREIGN KEY (event_id) REFERENCES support_events(event_id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# User's owned cards - which cards the user personally owns
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS owned_cards (
|
||||||
|
owned_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
card_id INTEGER UNIQUE,
|
||||||
|
level INTEGER DEFAULT 50,
|
||||||
|
limit_break INTEGER DEFAULT 0,
|
||||||
|
owned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (card_id) REFERENCES support_cards(card_id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# User's saved decks
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS user_decks (
|
||||||
|
deck_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
deck_name TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Cards in each deck (6 slots max)
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS deck_slots (
|
||||||
|
slot_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
deck_id INTEGER,
|
||||||
|
card_id INTEGER,
|
||||||
|
slot_position INTEGER,
|
||||||
|
level INTEGER DEFAULT 50,
|
||||||
|
FOREIGN KEY (deck_id) REFERENCES user_decks(deck_id),
|
||||||
|
FOREIGN KEY (card_id) REFERENCES support_cards(card_id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Create indexes for better query performance
|
||||||
|
cur.execute("CREATE INDEX IF NOT EXISTS idx_effects_card_level ON support_effects(card_id, level)")
|
||||||
|
cur.execute("CREATE INDEX IF NOT EXISTS idx_hints_card ON support_hints(card_id)")
|
||||||
|
cur.execute("CREATE INDEX IF NOT EXISTS idx_events_card ON support_events(card_id)")
|
||||||
|
cur.execute("CREATE INDEX IF NOT EXISTS idx_cards_type ON support_cards(card_type)")
|
||||||
|
cur.execute("CREATE INDEX IF NOT EXISTS idx_cards_rarity ON support_cards(rarity)")
|
||||||
|
cur.execute("CREATE INDEX IF NOT EXISTS idx_owned_card ON owned_cards(card_id)")
|
||||||
|
cur.execute("CREATE INDEX IF NOT EXISTS idx_deck_slots ON deck_slots(deck_id)")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
print("Database initialized successfully!")
|
||||||
|
|
||||||
|
def migrate_add_image_path():
|
||||||
|
"""Add image_path column if it doesn't exist"""
|
||||||
|
conn = get_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
try:
|
||||||
|
cur.execute("ALTER TABLE support_cards ADD COLUMN image_path TEXT")
|
||||||
|
conn.commit()
|
||||||
|
print("Added image_path column")
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
pass # Column already exists
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
init_db(reset=True)
|
||||||
702
db/db_queries.py
Normal file
@@ -0,0 +1,702 @@
|
|||||||
|
"""
|
||||||
|
Database query functions for Umamusume support cards
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
# In frozen state (exe), we need to ensure the database is in a writable location
|
||||||
|
# sys.executable points to the .exe file
|
||||||
|
base_dir = os.path.dirname(sys.executable)
|
||||||
|
db_dir = os.path.join(base_dir, "database")
|
||||||
|
DB_PATH = os.path.join(db_dir, "umamusume.db")
|
||||||
|
|
||||||
|
# Function to check if DB is effectively empty
|
||||||
|
def is_db_empty(path):
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(path)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("SELECT COUNT(*) FROM support_cards")
|
||||||
|
count = cur.fetchone()[0]
|
||||||
|
conn.close()
|
||||||
|
return count == 0
|
||||||
|
except:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check state: Missing OR Empty
|
||||||
|
should_copy_seed = False
|
||||||
|
if not os.path.exists(DB_PATH):
|
||||||
|
should_copy_seed = True
|
||||||
|
elif is_db_empty(DB_PATH):
|
||||||
|
# exists but empty - overwrite it
|
||||||
|
should_copy_seed = True
|
||||||
|
|
||||||
|
if should_copy_seed:
|
||||||
|
try:
|
||||||
|
# Ensure directory exists
|
||||||
|
os.makedirs(db_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Check for bundled seed database
|
||||||
|
bundled_seed_path = os.path.join(sys._MEIPASS, "database", "umamusume_seed.db")
|
||||||
|
|
||||||
|
if os.path.exists(bundled_seed_path):
|
||||||
|
# Copy seed database to user location (overwrite if exists)
|
||||||
|
shutil.copy2(bundled_seed_path, DB_PATH)
|
||||||
|
# Else: will be initialized by get_conn -> init_database
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
DB_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "database", "umamusume.db")
|
||||||
|
|
||||||
|
# Add VERSION import
|
||||||
|
if not getattr(sys, 'frozen', False):
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
try:
|
||||||
|
from version import VERSION
|
||||||
|
except ImportError:
|
||||||
|
VERSION = "2.1.0" # Fallback
|
||||||
|
|
||||||
|
def get_conn():
|
||||||
|
"""Get database connection"""
|
||||||
|
# Initialize if missing
|
||||||
|
if not os.path.exists(DB_PATH):
|
||||||
|
init_database()
|
||||||
|
|
||||||
|
# Check for updates and migrate if needed
|
||||||
|
check_for_updates()
|
||||||
|
|
||||||
|
return sqlite3.connect(DB_PATH)
|
||||||
|
|
||||||
|
def check_for_updates():
|
||||||
|
"""Check if database version matches app version, sync if outdated"""
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
bundled_seed_path = os.path.join(sys._MEIPASS, "database", "umamusume_seed.db")
|
||||||
|
if not os.path.exists(bundled_seed_path):
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# Check for metadata table
|
||||||
|
cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='system_metadata'")
|
||||||
|
if not cur.fetchone():
|
||||||
|
# No metadata, likely old version. Create it.
|
||||||
|
cur.execute("CREATE TABLE IF NOT EXISTS system_metadata (key TEXT PRIMARY KEY, value TEXT)")
|
||||||
|
cur.execute("INSERT OR REPLACE INTO system_metadata (key, value) VALUES (?, ?)", ('app_version', "0.0.0"))
|
||||||
|
conn.commit()
|
||||||
|
db_version = "0.0.0"
|
||||||
|
else:
|
||||||
|
cur.execute("SELECT value FROM system_metadata WHERE key='app_version'")
|
||||||
|
row = cur.fetchone()
|
||||||
|
db_version = row[0] if row else "0.0.0"
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Compare versions (simple string compare works for semver if zero-padded, but valid enough here)
|
||||||
|
# Or just check inequality. If different, try to update.
|
||||||
|
if db_version != VERSION:
|
||||||
|
sync_from_seed(bundled_seed_path)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Update check failed: {e}")
|
||||||
|
|
||||||
|
def sync_from_seed(seed_path):
|
||||||
|
"""Merge new data from seed into user database"""
|
||||||
|
print(f"Syncing database from {seed_path}...")
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# Attach seed database
|
||||||
|
cur.execute("ATTACH DATABASE ? AS seed", (seed_path,))
|
||||||
|
|
||||||
|
# 1. Insert New Cards (match by gametora_url)
|
||||||
|
# We assume gametora_url is unique and stable
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO main.support_cards (name, rarity, card_type, max_level, gametora_url, image_path)
|
||||||
|
SELECT name, rarity, card_type, max_level, gametora_url, image_path
|
||||||
|
FROM seed.support_cards
|
||||||
|
WHERE gametora_url NOT IN (SELECT gametora_url FROM main.support_cards)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# 2. Sync Child Tables
|
||||||
|
# Since effects/hints/events don't have stable IDs, we wipe and re-import them for ALL cards.
|
||||||
|
# But we must map seed_card_id to main_card_id.
|
||||||
|
|
||||||
|
# First, ensure we don't break foreign keys temporarily
|
||||||
|
cur.execute("PRAGMA foreign_keys = OFF")
|
||||||
|
|
||||||
|
tables_to_sync = ['support_effects', 'support_hints', 'support_events', 'event_skills']
|
||||||
|
for table in tables_to_sync:
|
||||||
|
cur.execute(f"DELETE FROM main.{table}")
|
||||||
|
|
||||||
|
# Migrate Support Effects
|
||||||
|
# Map: seed.card_id -> gametora_url -> main.card_id
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO main.support_effects (card_id, level, effect_name, effect_value)
|
||||||
|
SELECT m.card_id, s.level, s.effect_name, s.effect_value
|
||||||
|
FROM seed.support_effects s
|
||||||
|
JOIN seed.support_cards sc ON s.card_id = sc.card_id
|
||||||
|
JOIN main.support_cards m ON sc.gametora_url = m.gametora_url
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Migrate Support Hints
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO main.support_hints (card_id, hint_name, hint_description)
|
||||||
|
SELECT m.card_id, s.hint_name, s.hint_description
|
||||||
|
FROM seed.support_hints s
|
||||||
|
JOIN seed.support_cards sc ON s.card_id = sc.card_id
|
||||||
|
JOIN main.support_cards m ON sc.gametora_url = m.gametora_url
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Migrate Support Events
|
||||||
|
# We need to preserve event_id mapping for event_skills?
|
||||||
|
# Actually no, we deleted event_skills too.
|
||||||
|
# But we need to insert events first to get new IDs, then insert skills linking to those new IDs?
|
||||||
|
# That's tricky in SQL.
|
||||||
|
# Easier: Insert events, then resolving event_id is hard without a map.
|
||||||
|
# Alternative: Just copy the tables matching on card_id if we assume card_ids are consistent?
|
||||||
|
# If user has same cards as seed, IDs might be consistent.
|
||||||
|
# But if we added a card in the middle, IDs shift.
|
||||||
|
# Let's assume we can just drop event/skills for now or try to match them.
|
||||||
|
# The logic below is complex for events+skills because of the 2-level hierarchy.
|
||||||
|
|
||||||
|
# Strategy for Events/Skills:
|
||||||
|
# Since we just deleted them, we can re-insert.
|
||||||
|
# But main.event_id will be auto-incremented.
|
||||||
|
# We need to insert event, get ID, then insert skill? No, bulk insert.
|
||||||
|
# We can't easily map seed.event_id to main.event_id in bulk SQL across DBs easily without a temp table.
|
||||||
|
|
||||||
|
# Simplified Approach for Events/Skills:
|
||||||
|
# Iterate in Python? Slower but safer.
|
||||||
|
pass # Placeholder for python logic below
|
||||||
|
|
||||||
|
# Update Version
|
||||||
|
cur.execute("INSERT OR REPLACE INTO system_metadata (key, value) VALUES (?, ?)", ('app_version', VERSION))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# Python Logic for Events/Skills
|
||||||
|
# Fetch all events from seed with their card's gametora_url
|
||||||
|
cur.execute("""
|
||||||
|
SELECT sc.gametora_url, se.event_name, se.event_type, se.event_id
|
||||||
|
FROM seed.support_events se
|
||||||
|
JOIN seed.support_cards sc ON se.card_id = sc.card_id
|
||||||
|
""")
|
||||||
|
seed_events = cur.fetchall()
|
||||||
|
|
||||||
|
# Prepare Skill map: seed_event_id -> list of (skill_name)
|
||||||
|
cur.execute("SELECT event_id, skill_name FROM seed.event_skills")
|
||||||
|
seed_skills = {}
|
||||||
|
for ev_id, sk_name in cur.fetchall():
|
||||||
|
if ev_id not in seed_skills: seed_skills[ev_id] = []
|
||||||
|
seed_skills[ev_id].append(sk_name)
|
||||||
|
|
||||||
|
# Main Card Map: gametora_url -> main_card_id
|
||||||
|
cur.execute("SELECT gametora_url, card_id FROM main.support_cards")
|
||||||
|
url_to_main_id = dict(cur.fetchall())
|
||||||
|
|
||||||
|
for url, ev_name, ev_type, seed_ev_id in seed_events:
|
||||||
|
if url in url_to_main_id:
|
||||||
|
main_card_id = url_to_main_id[url]
|
||||||
|
# Insert Event
|
||||||
|
cur.execute("INSERT INTO main.support_events (card_id, event_name, event_type) VALUES (?, ?, ?)",
|
||||||
|
(main_card_id, ev_name, ev_type))
|
||||||
|
new_event_id = cur.lastrowid
|
||||||
|
|
||||||
|
# Insert Skills
|
||||||
|
if seed_ev_id in seed_skills:
|
||||||
|
for sk_name in seed_skills[seed_ev_id]:
|
||||||
|
cur.execute("INSERT INTO main.event_skills (event_id, skill_name) VALUES (?, ?)",
|
||||||
|
(new_event_id, sk_name))
|
||||||
|
|
||||||
|
cur.execute("PRAGMA foreign_keys = ON")
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
print(f"Database sync complete. Updated to version {VERSION}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Sync failed: {e}")
|
||||||
|
|
||||||
|
def init_database():
|
||||||
|
"""Initialize fresh database with schema"""
|
||||||
|
# Ensure directory exists
|
||||||
|
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
||||||
|
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# Create tables
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS support_cards (
|
||||||
|
card_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
rarity TEXT,
|
||||||
|
card_type TEXT,
|
||||||
|
max_level INTEGER DEFAULT 50,
|
||||||
|
gametora_url TEXT UNIQUE,
|
||||||
|
image_path TEXT
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS support_effects (
|
||||||
|
effect_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
card_id INTEGER,
|
||||||
|
level INTEGER,
|
||||||
|
effect_name TEXT,
|
||||||
|
effect_value TEXT,
|
||||||
|
FOREIGN KEY (card_id) REFERENCES support_cards(card_id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS support_hints (
|
||||||
|
hint_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
card_id INTEGER,
|
||||||
|
hint_name TEXT,
|
||||||
|
hint_description TEXT,
|
||||||
|
FOREIGN KEY (card_id) REFERENCES support_cards(card_id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS support_events (
|
||||||
|
event_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
card_id INTEGER,
|
||||||
|
event_name TEXT,
|
||||||
|
event_type TEXT,
|
||||||
|
FOREIGN KEY (card_id) REFERENCES support_cards(card_id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS event_skills (
|
||||||
|
skill_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
event_id INTEGER,
|
||||||
|
skill_name TEXT,
|
||||||
|
FOREIGN KEY (event_id) REFERENCES support_events(event_id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# User tables
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS owned_cards (
|
||||||
|
owned_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
card_id INTEGER UNIQUE,
|
||||||
|
level INTEGER DEFAULT 50,
|
||||||
|
limit_break INTEGER DEFAULT 0,
|
||||||
|
FOREIGN KEY (card_id) REFERENCES support_cards(card_id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS user_decks (
|
||||||
|
deck_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
deck_name TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS deck_slots (
|
||||||
|
slot_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
deck_id INTEGER,
|
||||||
|
card_id INTEGER,
|
||||||
|
slot_position INTEGER,
|
||||||
|
level INTEGER DEFAULT 50,
|
||||||
|
FOREIGN KEY (deck_id) REFERENCES user_decks(deck_id),
|
||||||
|
FOREIGN KEY (card_id) REFERENCES support_cards(card_id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Create indexes for performance
|
||||||
|
cur.execute("CREATE INDEX IF NOT EXISTS idx_effects_card_level ON support_effects(card_id, level)")
|
||||||
|
cur.execute("CREATE INDEX IF NOT EXISTS idx_hints_card ON support_hints(card_id)")
|
||||||
|
cur.execute("CREATE INDEX IF NOT EXISTS idx_events_card ON support_events(card_id)")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Card Queries
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
def get_all_cards(rarity_filter=None, type_filter=None, search_term=None, owned_only=False):
|
||||||
|
"""
|
||||||
|
Get all support cards with optional filtering
|
||||||
|
"""
|
||||||
|
conn = get_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
query = """
|
||||||
|
SELECT sc.card_id, sc.name, sc.rarity, sc.card_type, sc.max_level, sc.image_path,
|
||||||
|
CASE WHEN oc.card_id IS NOT NULL THEN 1 ELSE 0 END as is_owned,
|
||||||
|
oc.level as owned_level
|
||||||
|
FROM support_cards sc
|
||||||
|
LEFT JOIN owned_cards oc ON sc.card_id = oc.card_id
|
||||||
|
WHERE 1=1
|
||||||
|
"""
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if rarity_filter:
|
||||||
|
query += " AND sc.rarity = ?"
|
||||||
|
params.append(rarity_filter)
|
||||||
|
|
||||||
|
if type_filter:
|
||||||
|
query += " AND sc.card_type = ?"
|
||||||
|
params.append(type_filter)
|
||||||
|
|
||||||
|
if search_term:
|
||||||
|
query += " AND sc.name LIKE ?"
|
||||||
|
params.append(f"%{search_term}%")
|
||||||
|
|
||||||
|
if owned_only:
|
||||||
|
query += " AND oc.card_id IS NOT NULL"
|
||||||
|
|
||||||
|
query += " ORDER BY sc.rarity DESC, sc.name"
|
||||||
|
|
||||||
|
cur.execute(query, params)
|
||||||
|
rows = cur.fetchall()
|
||||||
|
conn.close()
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def get_card_by_id(card_id):
|
||||||
|
"""Get a single card by ID"""
|
||||||
|
conn = get_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("""
|
||||||
|
SELECT sc.card_id, sc.name, sc.rarity, sc.card_type, sc.max_level, sc.gametora_url, sc.image_path,
|
||||||
|
CASE WHEN oc.card_id IS NOT NULL THEN 1 ELSE 0 END as is_owned,
|
||||||
|
oc.level as owned_level
|
||||||
|
FROM support_cards sc
|
||||||
|
LEFT JOIN owned_cards oc ON sc.card_id = oc.card_id
|
||||||
|
WHERE sc.card_id = ?
|
||||||
|
""", (card_id,))
|
||||||
|
row = cur.fetchone()
|
||||||
|
conn.close()
|
||||||
|
return row
|
||||||
|
|
||||||
|
def get_card_count():
|
||||||
|
"""Get total number of cards in database"""
|
||||||
|
conn = get_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("SELECT COUNT(*) FROM support_cards")
|
||||||
|
count = cur.fetchone()[0]
|
||||||
|
conn.close()
|
||||||
|
return count
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Effect Queries
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
def get_effects_at_level(card_id, level):
|
||||||
|
"""Get all effects for a card at a specific level"""
|
||||||
|
conn = get_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("""
|
||||||
|
SELECT effect_name, effect_value
|
||||||
|
FROM support_effects
|
||||||
|
WHERE card_id = ? AND level = ?
|
||||||
|
ORDER BY effect_name
|
||||||
|
""", (card_id, level))
|
||||||
|
rows = cur.fetchall()
|
||||||
|
conn.close()
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def get_all_effects(card_id):
|
||||||
|
"""Get all effects for a card at all levels"""
|
||||||
|
conn = get_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("""
|
||||||
|
SELECT level, effect_name, effect_value
|
||||||
|
FROM support_effects
|
||||||
|
WHERE card_id = ?
|
||||||
|
ORDER BY level, effect_name
|
||||||
|
""", (card_id,))
|
||||||
|
rows = cur.fetchall()
|
||||||
|
conn.close()
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def get_unique_effect_names(card_id):
|
||||||
|
"""Get list of unique effect names for a card"""
|
||||||
|
conn = get_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("""
|
||||||
|
SELECT DISTINCT effect_name
|
||||||
|
FROM support_effects
|
||||||
|
WHERE card_id = ?
|
||||||
|
ORDER BY effect_name
|
||||||
|
""", (card_id,))
|
||||||
|
rows = [r[0] for r in cur.fetchall()]
|
||||||
|
conn.close()
|
||||||
|
return rows
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Hint Queries
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
def get_hints(card_id):
|
||||||
|
"""Get all hints for a card"""
|
||||||
|
conn = get_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("""
|
||||||
|
SELECT hint_name, hint_description
|
||||||
|
FROM support_hints
|
||||||
|
WHERE card_id = ?
|
||||||
|
ORDER BY hint_name
|
||||||
|
""", (card_id,))
|
||||||
|
rows = cur.fetchall()
|
||||||
|
conn.close()
|
||||||
|
return rows
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Event Queries
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
def get_events(card_id):
|
||||||
|
"""Get all events for a card"""
|
||||||
|
conn = get_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("""
|
||||||
|
SELECT event_id, event_name, event_type
|
||||||
|
FROM support_events
|
||||||
|
WHERE card_id = ?
|
||||||
|
ORDER BY event_name
|
||||||
|
""", (card_id,))
|
||||||
|
rows = cur.fetchall()
|
||||||
|
conn.close()
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def get_all_event_skills(card_id):
|
||||||
|
"""Get all events and their skills for a card"""
|
||||||
|
conn = get_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("""
|
||||||
|
SELECT se.event_name, es.skill_name
|
||||||
|
FROM support_events se
|
||||||
|
LEFT JOIN event_skills es ON se.event_id = es.event_id
|
||||||
|
WHERE se.card_id = ?
|
||||||
|
ORDER BY se.event_name, es.skill_name
|
||||||
|
""", (card_id,))
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
for event_name, skill_name in cur.fetchall():
|
||||||
|
if event_name not in result:
|
||||||
|
result[event_name] = []
|
||||||
|
if skill_name:
|
||||||
|
result[event_name].append(skill_name)
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
return result
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Owned Cards (Collection) Queries
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
def is_card_owned(card_id):
|
||||||
|
"""Check if a card is owned"""
|
||||||
|
conn = get_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("SELECT 1 FROM owned_cards WHERE card_id = ?", (card_id,))
|
||||||
|
result = cur.fetchone() is not None
|
||||||
|
conn.close()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def set_card_owned(card_id, owned=True, level=50):
|
||||||
|
"""Set a card as owned or not owned"""
|
||||||
|
conn = get_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
if owned:
|
||||||
|
cur.execute("""
|
||||||
|
INSERT OR REPLACE INTO owned_cards (card_id, level)
|
||||||
|
VALUES (?, ?)
|
||||||
|
""", (card_id, level))
|
||||||
|
else:
|
||||||
|
cur.execute("DELETE FROM owned_cards WHERE card_id = ?", (card_id,))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def update_owned_card_level(card_id, level):
|
||||||
|
"""Update the level of an owned card"""
|
||||||
|
conn = get_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("UPDATE owned_cards SET level = ? WHERE card_id = ?", (level, card_id))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def get_owned_cards():
|
||||||
|
"""Get all owned cards"""
|
||||||
|
conn = get_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("""
|
||||||
|
SELECT sc.card_id, sc.name, sc.rarity, sc.card_type, oc.level, sc.image_path
|
||||||
|
FROM owned_cards oc
|
||||||
|
JOIN support_cards sc ON oc.card_id = sc.card_id
|
||||||
|
ORDER BY sc.rarity DESC, sc.name
|
||||||
|
""")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
conn.close()
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def get_owned_count():
|
||||||
|
"""Get count of owned cards"""
|
||||||
|
conn = get_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("SELECT COUNT(*) FROM owned_cards")
|
||||||
|
count = cur.fetchone()[0]
|
||||||
|
conn.close()
|
||||||
|
return count
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Deck Queries
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
def create_deck(name):
|
||||||
|
"""Create a new deck"""
|
||||||
|
conn = get_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("INSERT INTO user_decks (deck_name) VALUES (?)", (name,))
|
||||||
|
deck_id = cur.lastrowid
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return deck_id
|
||||||
|
|
||||||
|
def get_all_decks():
|
||||||
|
"""Get all saved decks"""
|
||||||
|
conn = get_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("SELECT deck_id, deck_name, created_at FROM user_decks ORDER BY created_at DESC")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
conn.close()
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def delete_deck(deck_id):
|
||||||
|
"""Delete a deck and its slots"""
|
||||||
|
conn = get_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("DELETE FROM deck_slots WHERE deck_id = ?", (deck_id,))
|
||||||
|
cur.execute("DELETE FROM user_decks WHERE deck_id = ?", (deck_id,))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def add_card_to_deck(deck_id, card_id, slot_position, level=50):
|
||||||
|
"""Add a card to a deck slot"""
|
||||||
|
conn = get_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
# Remove existing card in that slot
|
||||||
|
cur.execute("DELETE FROM deck_slots WHERE deck_id = ? AND slot_position = ?", (deck_id, slot_position))
|
||||||
|
# Add new card
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO deck_slots (deck_id, card_id, slot_position, level)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
""", (deck_id, card_id, slot_position, level))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def remove_card_from_deck(deck_id, slot_position):
|
||||||
|
"""Remove a card from a deck slot"""
|
||||||
|
conn = get_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("DELETE FROM deck_slots WHERE deck_id = ? AND slot_position = ?", (deck_id, slot_position))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def get_deck_cards(deck_id):
|
||||||
|
"""Get all cards in a deck with their effects"""
|
||||||
|
conn = get_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("""
|
||||||
|
SELECT ds.slot_position, ds.level, sc.card_id, sc.name, sc.rarity, sc.card_type, sc.image_path
|
||||||
|
FROM deck_slots ds
|
||||||
|
JOIN support_cards sc ON ds.card_id = sc.card_id
|
||||||
|
WHERE ds.deck_id = ?
|
||||||
|
ORDER BY ds.slot_position
|
||||||
|
""", (deck_id,))
|
||||||
|
rows = cur.fetchall()
|
||||||
|
conn.close()
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def get_deck_combined_effects(deck_id):
|
||||||
|
"""
|
||||||
|
Get combined effects for all cards in a deck
|
||||||
|
Returns dict: {effect_name: {'total': value, 'breakdown': [(card_name, value), ...]}}
|
||||||
|
"""
|
||||||
|
conn = get_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# Get cards in deck with their levels
|
||||||
|
cur.execute("""
|
||||||
|
SELECT ds.card_id, ds.level, sc.name
|
||||||
|
FROM deck_slots ds
|
||||||
|
JOIN support_cards sc ON ds.card_id = sc.card_id
|
||||||
|
WHERE ds.deck_id = ?
|
||||||
|
""", (deck_id,))
|
||||||
|
deck_cards = cur.fetchall()
|
||||||
|
|
||||||
|
combined = {}
|
||||||
|
|
||||||
|
for card_id, level, card_name in deck_cards:
|
||||||
|
# Get effects for this card at this level
|
||||||
|
cur.execute("""
|
||||||
|
SELECT effect_name, effect_value
|
||||||
|
FROM support_effects
|
||||||
|
WHERE card_id = ? AND level = ?
|
||||||
|
""", (card_id, level))
|
||||||
|
|
||||||
|
for effect_name, effect_value in cur.fetchall():
|
||||||
|
if effect_name not in combined:
|
||||||
|
combined[effect_name] = {'total': 0, 'breakdown': []}
|
||||||
|
|
||||||
|
# Parse value (remove % and convert to number)
|
||||||
|
try:
|
||||||
|
num_value = float(effect_value.replace('%', '').replace('+', ''))
|
||||||
|
except:
|
||||||
|
num_value = 0
|
||||||
|
|
||||||
|
combined[effect_name]['total'] += num_value
|
||||||
|
combined[effect_name]['breakdown'].append((card_name, effect_value))
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
return combined
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Statistics
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
def get_database_stats():
|
||||||
|
"""Get statistics about the database"""
|
||||||
|
conn = get_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
stats = {}
|
||||||
|
|
||||||
|
cur.execute("SELECT COUNT(*) FROM support_cards")
|
||||||
|
stats['total_cards'] = cur.fetchone()[0]
|
||||||
|
|
||||||
|
cur.execute("SELECT rarity, COUNT(*) FROM support_cards GROUP BY rarity")
|
||||||
|
stats['by_rarity'] = dict(cur.fetchall())
|
||||||
|
|
||||||
|
cur.execute("SELECT card_type, COUNT(*) FROM support_cards GROUP BY card_type")
|
||||||
|
stats['by_type'] = dict(cur.fetchall())
|
||||||
|
|
||||||
|
cur.execute("SELECT COUNT(*) FROM support_effects")
|
||||||
|
stats['total_effects'] = cur.fetchone()[0]
|
||||||
|
|
||||||
|
cur.execute("SELECT COUNT(*) FROM owned_cards")
|
||||||
|
stats['owned_cards'] = cur.fetchone()[0]
|
||||||
|
|
||||||
|
cur.execute("SELECT COUNT(*) FROM user_decks")
|
||||||
|
stats['saved_decks'] = cur.fetchone()[0]
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
return stats
|
||||||
559
gui/card_view.py
Normal file
@@ -0,0 +1,559 @@
|
|||||||
|
"""
|
||||||
|
Card List View - Browse and search support cards with ownership management
|
||||||
|
"""
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from PIL import Image, ImageTk
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from db.db_queries import get_all_cards, get_card_by_id, get_effects_at_level, set_card_owned, is_card_owned, update_owned_card_level
|
||||||
|
from utils import resolve_image_path
|
||||||
|
from gui.theme import (
|
||||||
|
BG_DARK, BG_MEDIUM, BG_LIGHT, BG_HIGHLIGHT,
|
||||||
|
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_SUCCESS,
|
||||||
|
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
|
||||||
|
FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL, FONT_MONO,
|
||||||
|
RARITY_COLORS, TYPE_COLORS, TYPE_ICONS,
|
||||||
|
create_styled_button, create_styled_text, create_card_frame,
|
||||||
|
get_rarity_color, get_type_color, get_type_icon,
|
||||||
|
EFFECT_DESCRIPTIONS, Tooltip
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CardListFrame(ttk.Frame):
|
||||||
|
"""Frame containing card list with search/filter, ownership, and details panel"""
|
||||||
|
|
||||||
|
def __init__(self, parent, on_card_selected_callback=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.on_card_selected = on_card_selected_callback
|
||||||
|
self.cards = []
|
||||||
|
self.current_card_id = None
|
||||||
|
self.card_image = None # Keep reference to prevent garbage collection
|
||||||
|
self.icon_cache = {} # Cache for list icons
|
||||||
|
|
||||||
|
# Create main layout
|
||||||
|
self.create_widgets()
|
||||||
|
self.load_cards()
|
||||||
|
|
||||||
|
def create_widgets(self):
|
||||||
|
"""Create the card list interface"""
|
||||||
|
# Main horizontal layout
|
||||||
|
main_pane = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
|
||||||
|
main_pane.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# Left panel - Card list with filters
|
||||||
|
left_frame = ttk.Frame(main_pane, width=420)
|
||||||
|
main_pane.add(left_frame, weight=1)
|
||||||
|
|
||||||
|
# Right panel - Card details
|
||||||
|
self.details_frame = ttk.Frame(main_pane)
|
||||||
|
main_pane.add(self.details_frame, weight=2)
|
||||||
|
|
||||||
|
# === Left Panel Contents ===
|
||||||
|
|
||||||
|
# Initialize filter variables FIRST (before search trace can trigger filter_cards)
|
||||||
|
self.rarity_var = tk.StringVar(value="All")
|
||||||
|
self.type_var = tk.StringVar(value="All")
|
||||||
|
self.owned_only_var = tk.BooleanVar(value=False)
|
||||||
|
|
||||||
|
# Search bar with modern styling
|
||||||
|
search_frame = tk.Frame(left_frame, bg=BG_DARK)
|
||||||
|
search_frame.pack(fill=tk.X, padx=10, pady=10)
|
||||||
|
|
||||||
|
search_icon = tk.Label(search_frame, text="🔍", font=FONT_BODY, bg=BG_DARK, fg=TEXT_MUTED)
|
||||||
|
search_icon.pack(side=tk.LEFT, padx=(0, 5))
|
||||||
|
|
||||||
|
self.search_var = tk.StringVar()
|
||||||
|
self.search_entry = ttk.Entry(search_frame, textvariable=self.search_var, width=35)
|
||||||
|
self.search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||||||
|
|
||||||
|
# Set placeholder BEFORE adding the trace (so it doesn't trigger filter)
|
||||||
|
self.search_entry.insert(0, "Search cards...")
|
||||||
|
self.search_entry.config(foreground=TEXT_MUTED)
|
||||||
|
self.search_entry.bind('<FocusIn>', self._on_search_focus_in)
|
||||||
|
self.search_entry.bind('<FocusOut>', self._on_search_focus_out)
|
||||||
|
|
||||||
|
# NOW add the trace (after placeholder is set)
|
||||||
|
self.search_var.trace('w', lambda *args: self.filter_cards())
|
||||||
|
|
||||||
|
# Filter dropdowns
|
||||||
|
filter_frame = tk.Frame(left_frame, bg=BG_DARK)
|
||||||
|
filter_frame.pack(fill=tk.X, padx=10, pady=(0, 10))
|
||||||
|
|
||||||
|
# Rarity filter
|
||||||
|
tk.Label(filter_frame, text="Rarity:", font=FONT_SMALL, bg=BG_DARK, fg=TEXT_MUTED).pack(side=tk.LEFT)
|
||||||
|
rarity_combo = ttk.Combobox(filter_frame, textvariable=self.rarity_var,
|
||||||
|
values=["All", "SSR", "SR", "R"], width=7, state='readonly')
|
||||||
|
rarity_combo.pack(side=tk.LEFT, padx=(5, 15))
|
||||||
|
rarity_combo.bind('<<ComboboxSelected>>', lambda e: self.filter_cards())
|
||||||
|
|
||||||
|
# Type filter
|
||||||
|
tk.Label(filter_frame, text="Type:", font=FONT_SMALL, bg=BG_DARK, fg=TEXT_MUTED).pack(side=tk.LEFT)
|
||||||
|
type_combo = ttk.Combobox(filter_frame, textvariable=self.type_var,
|
||||||
|
values=["All", "Speed", "Stamina", "Power", "Guts", "Wisdom", "Friend", "Group"],
|
||||||
|
width=10, state='readonly')
|
||||||
|
type_combo.pack(side=tk.LEFT, padx=5)
|
||||||
|
type_combo.bind('<<ComboboxSelected>>', lambda e: self.filter_cards())
|
||||||
|
|
||||||
|
# Owned only filter
|
||||||
|
owned_check = ttk.Checkbutton(filter_frame, text="Owned Only",
|
||||||
|
variable=self.owned_only_var, command=self.filter_cards)
|
||||||
|
owned_check.pack(side=tk.LEFT, padx=15)
|
||||||
|
|
||||||
|
# Reset Button
|
||||||
|
ttk.Button(filter_frame, text="Reset", command=self.reset_filters,
|
||||||
|
style='Small.TButton', width=7).pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
# Shortcuts
|
||||||
|
self.bind_all('<Control-f>', lambda e: self.search_entry.focus_set())
|
||||||
|
|
||||||
|
# Card count label
|
||||||
|
self.count_label = tk.Label(left_frame, text="0 cards", font=FONT_SMALL,
|
||||||
|
bg=BG_DARK, fg=ACCENT_PRIMARY)
|
||||||
|
self.count_label.pack(pady=5)
|
||||||
|
|
||||||
|
# Card list (Treeview)
|
||||||
|
list_frame = tk.Frame(left_frame, bg=BG_DARK)
|
||||||
|
list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
|
||||||
|
|
||||||
|
self.tree = ttk.Treeview(list_frame, columns=('owned', 'name', 'rarity', 'type'),
|
||||||
|
show='tree headings', selectmode='browse',
|
||||||
|
style="CardList.Treeview")
|
||||||
|
|
||||||
|
self.tree.heading('#0', text='')
|
||||||
|
self.tree.column('#0', width=45, anchor='center')
|
||||||
|
|
||||||
|
self.tree.heading('owned', text='★', command=lambda: self.sort_column('owned', False))
|
||||||
|
self.tree.heading('name', text='Name', anchor='w', command=lambda: self.sort_column('name', False))
|
||||||
|
self.tree.heading('rarity', text='Rarity', command=lambda: self.sort_column('rarity', False))
|
||||||
|
self.tree.heading('type', text='Type', command=lambda: self.sort_column('type', False))
|
||||||
|
|
||||||
|
self.tree.column('owned', width=30, anchor='center')
|
||||||
|
self.tree.column('name', width=180, minwidth=150)
|
||||||
|
self.tree.column('rarity', width=55, anchor='center')
|
||||||
|
self.tree.column('type', width=90, anchor='center')
|
||||||
|
|
||||||
|
scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.tree.yview)
|
||||||
|
self.tree.configure(yscrollcommand=scrollbar.set)
|
||||||
|
|
||||||
|
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
|
|
||||||
|
self.tree.bind('<<TreeviewSelect>>', self.on_select)
|
||||||
|
|
||||||
|
# Tag for owned cards
|
||||||
|
self.tree.tag_configure('owned', background='#1a3a2e')
|
||||||
|
|
||||||
|
# === Right Panel Contents (Details) ===
|
||||||
|
self.create_details_panel()
|
||||||
|
|
||||||
|
def create_details_panel(self):
|
||||||
|
"""Create the card details panel"""
|
||||||
|
# Container with card-like appearance
|
||||||
|
details_container = tk.Frame(self.details_frame, bg=BG_DARK)
|
||||||
|
details_container.pack(fill=tk.BOTH, expand=True, padx=15, pady=10)
|
||||||
|
|
||||||
|
# Image area with card frame
|
||||||
|
image_frame = create_card_frame(details_container, padx=10, pady=10)
|
||||||
|
image_frame.pack(pady=10)
|
||||||
|
|
||||||
|
self.image_label = tk.Label(image_frame, text="", bg=BG_MEDIUM)
|
||||||
|
self.image_label.pack(padx=5, pady=5)
|
||||||
|
|
||||||
|
# Header with card name
|
||||||
|
self.detail_name = tk.Label(details_container, text="Select a card",
|
||||||
|
font=FONT_HEADER, bg=BG_DARK, fg=ACCENT_PRIMARY)
|
||||||
|
self.detail_name.pack(pady=(10, 5))
|
||||||
|
|
||||||
|
self.detail_info = tk.Label(details_container, text="",
|
||||||
|
font=FONT_SMALL, bg=BG_DARK, fg=TEXT_MUTED)
|
||||||
|
self.detail_info.pack()
|
||||||
|
|
||||||
|
# Owned checkbox with emphasis
|
||||||
|
owned_frame = tk.Frame(details_container, bg=BG_DARK)
|
||||||
|
owned_frame.pack(pady=15)
|
||||||
|
|
||||||
|
self.owned_var = tk.BooleanVar(value=False)
|
||||||
|
self.owned_checkbox = ttk.Checkbutton(owned_frame, text="✨ I Own This Card",
|
||||||
|
variable=self.owned_var,
|
||||||
|
command=self.toggle_owned,
|
||||||
|
style='Large.TCheckbutton')
|
||||||
|
self.owned_checkbox.pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
# Level selector with button-based control (no slider)
|
||||||
|
level_frame = tk.Frame(details_container, bg=BG_DARK)
|
||||||
|
level_frame.pack(fill=tk.X, padx=30, pady=10)
|
||||||
|
|
||||||
|
tk.Label(level_frame, text="Card Level:", font=FONT_BODY,
|
||||||
|
bg=BG_DARK, fg=TEXT_SECONDARY).pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
# Level display with increment/decrement
|
||||||
|
level_ctrl = tk.Frame(level_frame, bg=BG_DARK)
|
||||||
|
level_ctrl.pack(side=tk.LEFT, padx=15)
|
||||||
|
|
||||||
|
self.level_var = tk.IntVar(value=50)
|
||||||
|
self.max_level = 50
|
||||||
|
self.valid_levels = [30, 35, 40, 45, 50] # Default SSR
|
||||||
|
|
||||||
|
# Decrement button
|
||||||
|
dec_btn = tk.Button(level_ctrl, text="−", font=FONT_HEADER,
|
||||||
|
bg=BG_LIGHT, fg=TEXT_PRIMARY, bd=0, width=2,
|
||||||
|
activebackground=BG_HIGHLIGHT, cursor='hand2',
|
||||||
|
command=self.decrement_level)
|
||||||
|
dec_btn.pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
self.level_label = tk.Label(level_ctrl, text="50", width=4, font=FONT_HEADER,
|
||||||
|
bg=BG_MEDIUM, fg=ACCENT_PRIMARY, padx=10)
|
||||||
|
self.level_label.pack(side=tk.LEFT, padx=2)
|
||||||
|
|
||||||
|
# Increment button
|
||||||
|
inc_btn = tk.Button(level_ctrl, text="+", font=FONT_HEADER,
|
||||||
|
bg=BG_LIGHT, fg=TEXT_PRIMARY, bd=0, width=2,
|
||||||
|
activebackground=BG_HIGHLIGHT, cursor='hand2',
|
||||||
|
command=self.increment_level)
|
||||||
|
inc_btn.pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
# Quick level buttons container
|
||||||
|
self.level_btn_frame = tk.Frame(level_frame, bg=BG_DARK)
|
||||||
|
self.level_btn_frame.pack(side=tk.LEFT, padx=20)
|
||||||
|
|
||||||
|
self.level_buttons = {}
|
||||||
|
# Initial population
|
||||||
|
self.update_level_buttons('SSR', 50)
|
||||||
|
|
||||||
|
# Effects display header
|
||||||
|
effects_header = tk.Frame(details_container, bg=BG_DARK)
|
||||||
|
effects_header.pack(fill=tk.X, padx=20, pady=(20, 10))
|
||||||
|
|
||||||
|
tk.Label(effects_header, text="📊 Effects at Current Level",
|
||||||
|
font=FONT_SUBHEADER, bg=BG_DARK, fg=TEXT_PRIMARY).pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
# Effects text area with modern styling
|
||||||
|
effects_frame = create_card_frame(details_container)
|
||||||
|
effects_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=(0, 15))
|
||||||
|
|
||||||
|
self.effects_text = create_styled_text(effects_frame, height=10)
|
||||||
|
self.effects_text.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
|
||||||
|
self.effects_text.config(state=tk.DISABLED)
|
||||||
|
|
||||||
|
def load_cards(self):
|
||||||
|
"""Load all cards from database"""
|
||||||
|
self.cards = get_all_cards()
|
||||||
|
self.populate_tree(self.cards)
|
||||||
|
|
||||||
|
def reset_filters(self):
|
||||||
|
"""Reset all filters to default"""
|
||||||
|
self.search_var.set("")
|
||||||
|
self.rarity_var.set("All")
|
||||||
|
self.type_var.set("All")
|
||||||
|
self.owned_only_var.set(False)
|
||||||
|
# Reset placeholder
|
||||||
|
self.search_entry.delete(0, tk.END)
|
||||||
|
self.search_entry.insert(0, "Search cards...")
|
||||||
|
self.search_entry.config(foreground=TEXT_MUTED)
|
||||||
|
self.filter_cards()
|
||||||
|
|
||||||
|
def _on_search_focus_in(self, event):
|
||||||
|
"""Clear placeholder on focus"""
|
||||||
|
if self.search_entry.get() == "Search cards...":
|
||||||
|
self.search_entry.delete(0, tk.END)
|
||||||
|
self.search_entry.config(foreground=TEXT_PRIMARY)
|
||||||
|
|
||||||
|
def _on_search_focus_out(self, event):
|
||||||
|
"""Show placeholder if empty"""
|
||||||
|
if not self.search_entry.get():
|
||||||
|
self.search_entry.insert(0, "Search cards...")
|
||||||
|
self.search_entry.config(foreground=TEXT_MUTED)
|
||||||
|
|
||||||
|
def filter_cards(self):
|
||||||
|
"""Filter cards based on search and dropdown values"""
|
||||||
|
rarity = self.rarity_var.get() if self.rarity_var.get() != "All" else None
|
||||||
|
card_type = self.type_var.get() if self.type_var.get() != "All" else None
|
||||||
|
# Ignore placeholder text
|
||||||
|
search_text = self.search_var.get().strip()
|
||||||
|
search = search_text if search_text and search_text != "Search cards..." else None
|
||||||
|
owned_only = self.owned_only_var.get()
|
||||||
|
|
||||||
|
self.cards = get_all_cards(rarity_filter=rarity, type_filter=card_type,
|
||||||
|
search_term=search, owned_only=owned_only)
|
||||||
|
self.populate_tree(self.cards)
|
||||||
|
|
||||||
|
def sort_column(self, col, reverse):
|
||||||
|
"""Sort treeview by column"""
|
||||||
|
l = [(self.tree.set(k, col), k) for k in self.tree.get_children('')]
|
||||||
|
|
||||||
|
# Custom sort logic
|
||||||
|
if col == 'owned':
|
||||||
|
# Sort by star/empty
|
||||||
|
l.sort(key=lambda t: t[0] if t[0] else "", reverse=reverse)
|
||||||
|
elif col == 'rarity':
|
||||||
|
# Sort by rarity rank (SSR > SR > R)
|
||||||
|
rarity_map = {'SSR': 3, 'SR': 2, 'R': 1}
|
||||||
|
l.sort(key=lambda t: rarity_map.get(t[0], 0), reverse=reverse)
|
||||||
|
else:
|
||||||
|
# Default string sort
|
||||||
|
l.sort(reverse=reverse)
|
||||||
|
|
||||||
|
# Rearrange items
|
||||||
|
for index, (val, k) in enumerate(l):
|
||||||
|
self.tree.move(k, '', index)
|
||||||
|
|
||||||
|
# Reverse sort next time
|
||||||
|
self.tree.heading(col, command=lambda: self.sort_column(col, not reverse))
|
||||||
|
|
||||||
|
def populate_tree(self, cards):
|
||||||
|
"""Populate treeview with cards"""
|
||||||
|
self.tree.delete(*self.tree.get_children())
|
||||||
|
|
||||||
|
for card in cards:
|
||||||
|
card_id, name, rarity, card_type, max_level, image_path, is_owned, owned_level = card
|
||||||
|
type_icon = get_type_icon(card_type)
|
||||||
|
owned_mark = "★" if is_owned else ""
|
||||||
|
tag = 'owned' if is_owned else ''
|
||||||
|
|
||||||
|
# Show level for owned cards
|
||||||
|
display_name = name
|
||||||
|
if is_owned and owned_level:
|
||||||
|
display_name = f"{name} (Lv{owned_level})"
|
||||||
|
|
||||||
|
# Load Icon
|
||||||
|
img = self.icon_cache.get(card_id)
|
||||||
|
if not img:
|
||||||
|
resolved_path = resolve_image_path(image_path)
|
||||||
|
if resolved_path and os.path.exists(resolved_path):
|
||||||
|
try:
|
||||||
|
pil_img = Image.open(resolved_path)
|
||||||
|
pil_img.thumbnail((32, 32), Image.Resampling.LANCZOS)
|
||||||
|
img = ImageTk.PhotoImage(pil_img)
|
||||||
|
self.icon_cache[card_id] = img
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if img:
|
||||||
|
self.tree.insert('', tk.END, iid=card_id, text='', image=img,
|
||||||
|
values=(owned_mark, display_name, rarity, f"{type_icon} {card_type}"),
|
||||||
|
tags=(tag,))
|
||||||
|
else:
|
||||||
|
self.tree.insert('', tk.END, iid=card_id, text='',
|
||||||
|
values=(owned_mark, display_name, rarity, f"{type_icon} {card_type}"),
|
||||||
|
tags=(tag,))
|
||||||
|
|
||||||
|
self.count_label.config(text=f"✨ {len(cards)} cards")
|
||||||
|
|
||||||
|
def on_select(self, event):
|
||||||
|
"""Handle card selection"""
|
||||||
|
selection = self.tree.selection()
|
||||||
|
if not selection:
|
||||||
|
return
|
||||||
|
|
||||||
|
card_id = int(selection[0])
|
||||||
|
card = get_card_by_id(card_id)
|
||||||
|
|
||||||
|
if card:
|
||||||
|
card_id, name, rarity, card_type, max_level, url, image_path, is_owned, owned_level = card
|
||||||
|
|
||||||
|
# Update owned checkbox
|
||||||
|
self.owned_var.set(bool(is_owned))
|
||||||
|
|
||||||
|
# Load card image if available
|
||||||
|
self.load_card_image(image_path)
|
||||||
|
|
||||||
|
# Use owned level if owned, otherwise max level or default 50
|
||||||
|
initial_level = owned_level if is_owned and owned_level else max_level
|
||||||
|
|
||||||
|
# Update level controls
|
||||||
|
self.max_level = max_level
|
||||||
|
self.update_level_buttons(rarity, max_level)
|
||||||
|
|
||||||
|
# Snap initial level to valid levels
|
||||||
|
if initial_level not in self.valid_levels:
|
||||||
|
# Find closest or default to max
|
||||||
|
initial_level = max_level
|
||||||
|
|
||||||
|
self.level_var.set(initial_level)
|
||||||
|
self.level_label.config(text=str(initial_level))
|
||||||
|
|
||||||
|
# Update details display with colors
|
||||||
|
type_icon = get_type_icon(card_type)
|
||||||
|
type_color = get_type_color(card_type)
|
||||||
|
rarity_color = get_rarity_color(rarity)
|
||||||
|
|
||||||
|
self.detail_name.config(text=f"{type_icon} {name}", fg=ACCENT_PRIMARY)
|
||||||
|
self.detail_info.config(text=f"{rarity} │ {card_type} │ Max Level: {max_level}")
|
||||||
|
|
||||||
|
# Load effects
|
||||||
|
self.current_card_id = card_id
|
||||||
|
self.update_effects_display()
|
||||||
|
|
||||||
|
# Notify parent window
|
||||||
|
if self.on_card_selected:
|
||||||
|
self.on_card_selected(card_id, name)
|
||||||
|
|
||||||
|
def load_card_image(self, image_path):
|
||||||
|
"""Load and display card image"""
|
||||||
|
resolved_path = resolve_image_path(image_path)
|
||||||
|
|
||||||
|
if resolved_path and os.path.exists(resolved_path):
|
||||||
|
try:
|
||||||
|
img = Image.open(resolved_path)
|
||||||
|
img.thumbnail((130, 130)) # Slightly larger
|
||||||
|
self.card_image = ImageTk.PhotoImage(img)
|
||||||
|
self.image_label.config(image=self.card_image)
|
||||||
|
except Exception as e:
|
||||||
|
self.image_label.config(image='', text="[Image not found]")
|
||||||
|
else:
|
||||||
|
self.image_label.config(image='', text="")
|
||||||
|
|
||||||
|
def toggle_owned(self):
|
||||||
|
"""Toggle owned status for current card"""
|
||||||
|
if self.current_card_id:
|
||||||
|
owned = self.owned_var.get()
|
||||||
|
level = int(self.level_var.get())
|
||||||
|
set_card_owned(self.current_card_id, owned, level)
|
||||||
|
self.filter_cards() # Refresh list to update owned markers
|
||||||
|
|
||||||
|
def update_level_buttons(self, rarity, max_level):
|
||||||
|
"""Update quick level buttons based on rarity/max level"""
|
||||||
|
# Determine valid levels
|
||||||
|
if max_level == 50: # SSR
|
||||||
|
self.valid_levels = [30, 35, 40, 45, 50]
|
||||||
|
elif max_level == 45: # SR
|
||||||
|
self.valid_levels = [25, 30, 35, 40, 45]
|
||||||
|
else: # R (max 40)
|
||||||
|
self.valid_levels = [20, 25, 30, 35, 40]
|
||||||
|
|
||||||
|
# Clear existing buttons
|
||||||
|
for widget in self.level_btn_frame.winfo_children():
|
||||||
|
widget.destroy()
|
||||||
|
self.level_buttons = {}
|
||||||
|
|
||||||
|
# Create new buttons
|
||||||
|
for lvl in self.valid_levels:
|
||||||
|
btn = create_styled_button(self.level_btn_frame, text=f"Lv{lvl}",
|
||||||
|
command=lambda l=lvl: self.set_level(l),
|
||||||
|
style_type='default')
|
||||||
|
btn.config(width=5, padx=6, pady=3, font=FONT_SMALL)
|
||||||
|
btn.pack(side=tk.LEFT, padx=2)
|
||||||
|
self.level_buttons[lvl] = btn
|
||||||
|
|
||||||
|
def set_level(self, level):
|
||||||
|
"""Set level from quick button"""
|
||||||
|
self.level_var.set(level)
|
||||||
|
self.level_label.config(text=str(level))
|
||||||
|
self.update_effects_display()
|
||||||
|
|
||||||
|
# Save level if owned
|
||||||
|
if self.current_card_id and self.owned_var.get():
|
||||||
|
update_owned_card_level(self.current_card_id, level)
|
||||||
|
self.update_tree_item_level(self.current_card_id, level)
|
||||||
|
|
||||||
|
def increment_level(self):
|
||||||
|
"""Increase level to next valid step"""
|
||||||
|
current = self.level_var.get()
|
||||||
|
# Find next level in valid_levels
|
||||||
|
for lvl in self.valid_levels:
|
||||||
|
if lvl > current:
|
||||||
|
self.set_level(lvl)
|
||||||
|
return
|
||||||
|
|
||||||
|
def decrement_level(self):
|
||||||
|
"""Decrease level to previous valid step"""
|
||||||
|
current = self.level_var.get()
|
||||||
|
# Find previous level in valid_levels
|
||||||
|
for lvl in reversed(self.valid_levels):
|
||||||
|
if lvl < current:
|
||||||
|
self.set_level(lvl)
|
||||||
|
return
|
||||||
|
|
||||||
|
def update_tree_item_level(self, card_id, level):
|
||||||
|
"""Update visible name in tree without full reload"""
|
||||||
|
if self.tree.exists(card_id):
|
||||||
|
current_values = self.tree.item(card_id, 'values')
|
||||||
|
if current_values:
|
||||||
|
# current_values is a tuple: (owned_mark, name, rarity, type)
|
||||||
|
# We need to strip existing " (LvXX)" from name if present
|
||||||
|
name = current_values[1]
|
||||||
|
base_name = name.split(" (Lv")[0]
|
||||||
|
new_name = f"{base_name} (Lv{level})"
|
||||||
|
|
||||||
|
# Make new values tuple preserving other columns
|
||||||
|
new_values = (current_values[0], new_name, current_values[2], current_values[3])
|
||||||
|
self.tree.item(card_id, values=new_values)
|
||||||
|
|
||||||
|
def update_effects_display(self):
|
||||||
|
"""Update the effects display for current card and level"""
|
||||||
|
if not self.current_card_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
level = int(self.level_var.get())
|
||||||
|
effects = get_effects_at_level(self.current_card_id, level)
|
||||||
|
|
||||||
|
self.effects_text.config(state=tk.NORMAL)
|
||||||
|
self.effects_text.delete('1.0', tk.END)
|
||||||
|
|
||||||
|
# Configure tags for styling
|
||||||
|
self.effects_text.tag_configure('header', font=FONT_SUBHEADER, foreground=ACCENT_PRIMARY)
|
||||||
|
self.effects_text.tag_configure('highlight', foreground=ACCENT_SUCCESS)
|
||||||
|
self.effects_text.tag_configure('effect_name', foreground=TEXT_SECONDARY)
|
||||||
|
self.effects_text.tag_configure('effect_value', foreground=TEXT_PRIMARY)
|
||||||
|
self.effects_text.tag_configure('effect_tooltip', underline=False)
|
||||||
|
|
||||||
|
if effects:
|
||||||
|
self.effects_text.insert(tk.END, f"━━━ Level {level} ━━━\n\n", 'header')
|
||||||
|
for name, value in effects:
|
||||||
|
# Highlight high values
|
||||||
|
prefix = ""
|
||||||
|
if '%' in str(value):
|
||||||
|
try:
|
||||||
|
num = int(str(value).replace('%', '').replace('+', ''))
|
||||||
|
if num >= 20:
|
||||||
|
prefix = "★ "
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if prefix:
|
||||||
|
self.effects_text.insert(tk.END, prefix, 'highlight')
|
||||||
|
|
||||||
|
# Insert effect name with tooltip tag
|
||||||
|
tag_name = f"tooltip_{name.replace(' ', '_')}"
|
||||||
|
self.effects_text.insert(tk.END, f"{name}: ", ('effect_name', tag_name))
|
||||||
|
|
||||||
|
# Bind tooltip events
|
||||||
|
self.effects_text.tag_bind(tag_name, "<Enter>", lambda e, n=name: self.show_effect_tooltip(e, n))
|
||||||
|
self.effects_text.tag_bind(tag_name, "<Leave>", self.hide_effect_tooltip)
|
||||||
|
|
||||||
|
self.effects_text.insert(tk.END, f"{value}\n", 'effect_value')
|
||||||
|
else:
|
||||||
|
self.effects_text.insert(tk.END, f"No effects data for Level {level}\n\n")
|
||||||
|
self.effects_text.insert(tk.END, "Available levels: 1, 25, 40, 50\n", 'effect_name')
|
||||||
|
|
||||||
|
self.effects_text.config(state=tk.DISABLED)
|
||||||
|
|
||||||
|
def show_effect_tooltip(self, event, effect_name):
|
||||||
|
"""Show tooltip for effect"""
|
||||||
|
if effect_name in EFFECT_DESCRIPTIONS:
|
||||||
|
text = EFFECT_DESCRIPTIONS[effect_name]
|
||||||
|
x = event.x_root + 15
|
||||||
|
y = event.y_root + 10
|
||||||
|
|
||||||
|
# Close existing if any
|
||||||
|
self.hide_effect_tooltip(None)
|
||||||
|
|
||||||
|
self.tooltip_window = tk.Toplevel(self)
|
||||||
|
self.tooltip_window.wm_overrideredirect(True)
|
||||||
|
self.tooltip_window.wm_geometry(f"+{x}+{y}")
|
||||||
|
|
||||||
|
label = tk.Label(self.tooltip_window, text=text, justify=tk.LEFT,
|
||||||
|
background=BG_LIGHT, foreground=TEXT_PRIMARY,
|
||||||
|
relief=tk.SOLID, borderwidth=1, font=FONT_SMALL,
|
||||||
|
padx=10, pady=5, wraplength=250)
|
||||||
|
label.pack()
|
||||||
|
|
||||||
|
def hide_effect_tooltip(self, event):
|
||||||
|
"""Hide tooltip"""
|
||||||
|
if hasattr(self, 'tooltip_window') and self.tooltip_window:
|
||||||
|
self.tooltip_window.destroy()
|
||||||
|
self.tooltip_window = None
|
||||||
547
gui/deck_builder.py
Normal file
@@ -0,0 +1,547 @@
|
|||||||
|
"""
|
||||||
|
Deck Builder Frame
|
||||||
|
Build decks with 6 cards and view combined effects with breakdown
|
||||||
|
"""
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk, messagebox
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from PIL import Image, ImageTk
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from db.db_queries import (
|
||||||
|
get_all_cards, get_all_decks, create_deck, delete_deck,
|
||||||
|
add_card_to_deck, remove_card_from_deck, get_deck_cards,
|
||||||
|
get_effects_at_level
|
||||||
|
)
|
||||||
|
from utils import resolve_image_path
|
||||||
|
from gui.theme import (
|
||||||
|
BG_DARK, BG_DARKEST, BG_MEDIUM, BG_LIGHT, BG_HIGHLIGHT,
|
||||||
|
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_SUCCESS, ACCENT_ERROR,
|
||||||
|
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
|
||||||
|
FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL, FONT_TINY,
|
||||||
|
TYPE_COLORS, get_type_color, get_type_icon,
|
||||||
|
create_styled_button, create_styled_text, create_card_frame
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CardSlot(tk.Frame):
|
||||||
|
"""Visual component for a single card slot"""
|
||||||
|
def __init__(self, parent, index, remove_callback, level_callback):
|
||||||
|
super().__init__(parent, bg=BG_MEDIUM, highlightthickness=2, highlightbackground=BG_LIGHT)
|
||||||
|
self.index = index
|
||||||
|
self.remove_callback = remove_callback
|
||||||
|
self.level_callback = level_callback
|
||||||
|
self.image_ref = None # Keep reference to prevent GC
|
||||||
|
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
# Configure grid weight
|
||||||
|
self.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
|
# Slot number indicator
|
||||||
|
slot_label = tk.Label(self, text=f"#{self.index + 1}", font=FONT_TINY,
|
||||||
|
bg=BG_LIGHT, fg=TEXT_MUTED, padx=4, pady=2)
|
||||||
|
slot_label.place(x=2, y=2)
|
||||||
|
|
||||||
|
# Image Area (Left)
|
||||||
|
self.image_label = tk.Label(self, bg=BG_MEDIUM, text="📭", fg=TEXT_MUTED,
|
||||||
|
font=('Segoe UI', 20))
|
||||||
|
self.image_label.grid(row=0, column=0, rowspan=3, padx=8, pady=8)
|
||||||
|
|
||||||
|
# Details Area (Right)
|
||||||
|
self.name_label = tk.Label(self, text="Empty Slot", bg=BG_MEDIUM, fg=TEXT_MUTED,
|
||||||
|
font=FONT_BODY_BOLD, anchor='w', wraplength=130)
|
||||||
|
self.name_label.grid(row=0, column=1, sticky='w', padx=8, pady=(10, 0))
|
||||||
|
|
||||||
|
self.meta_label = tk.Label(self, text="", bg=BG_MEDIUM, fg=TEXT_MUTED,
|
||||||
|
font=FONT_SMALL, anchor='w')
|
||||||
|
self.meta_label.grid(row=1, column=1, sticky='w', padx=8)
|
||||||
|
|
||||||
|
# Controls (Bottom Right)
|
||||||
|
ctrl_frame = tk.Frame(self, bg=BG_MEDIUM)
|
||||||
|
ctrl_frame.grid(row=2, column=1, sticky='ew', padx=8, pady=8)
|
||||||
|
|
||||||
|
# Level Selector
|
||||||
|
tk.Label(ctrl_frame, text="Lv:", bg=BG_MEDIUM, fg=TEXT_MUTED,
|
||||||
|
font=FONT_SMALL).pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
self.level_var = tk.StringVar(value="50")
|
||||||
|
self.level_combo = ttk.Combobox(ctrl_frame, textvariable=self.level_var,
|
||||||
|
values=[], width=4, state='readonly')
|
||||||
|
self.level_combo.pack(side=tk.LEFT, padx=4)
|
||||||
|
self.level_combo.bind('<<ComboboxSelected>>', self._on_level_change)
|
||||||
|
|
||||||
|
# Remove Button
|
||||||
|
self.remove_btn = tk.Button(ctrl_frame, text="✕", bg=BG_LIGHT, fg=ACCENT_ERROR,
|
||||||
|
bd=0, font=FONT_BODY_BOLD, width=2,
|
||||||
|
activebackground=ACCENT_ERROR, activeforeground=TEXT_PRIMARY,
|
||||||
|
cursor='hand2',
|
||||||
|
command=lambda: self.remove_callback(self.index))
|
||||||
|
self.remove_btn.pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
# Hide controls initially
|
||||||
|
self.toggle_controls(False)
|
||||||
|
|
||||||
|
def toggle_controls(self, visible):
|
||||||
|
state = 'normal' if visible else 'disabled'
|
||||||
|
self.level_combo.config(state='readonly' if visible else 'disabled')
|
||||||
|
if not visible:
|
||||||
|
self.remove_btn.pack_forget()
|
||||||
|
else:
|
||||||
|
self.remove_btn.pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
def set_card(self, card_data):
|
||||||
|
"""Set card data: (id, name, rarity, type, image_path, level)"""
|
||||||
|
if not card_data:
|
||||||
|
self.reset()
|
||||||
|
return
|
||||||
|
|
||||||
|
card_id, name, rarity, card_type, image_path, level = card_data
|
||||||
|
|
||||||
|
# Calculate valid levels based on rarity
|
||||||
|
if rarity == 'SSR':
|
||||||
|
valid_levels = [50, 45, 40, 35, 30]
|
||||||
|
max_lvl = 50
|
||||||
|
elif rarity == 'SR':
|
||||||
|
valid_levels = [45, 40, 35, 30, 25]
|
||||||
|
max_lvl = 45
|
||||||
|
else: # R
|
||||||
|
valid_levels = [40, 35, 30, 25, 20]
|
||||||
|
max_lvl = 40
|
||||||
|
|
||||||
|
self.level_combo['values'] = [str(l) for l in valid_levels]
|
||||||
|
|
||||||
|
# Snap level to valid value if not present (e.g. old data)
|
||||||
|
if level not in valid_levels:
|
||||||
|
level = max_lvl
|
||||||
|
|
||||||
|
# Update styling based on type
|
||||||
|
color = get_type_color(card_type)
|
||||||
|
type_icon = get_type_icon(card_type)
|
||||||
|
|
||||||
|
self.name_label.config(text=name, fg=TEXT_PRIMARY)
|
||||||
|
self.meta_label.config(text=f"{type_icon} {rarity} │ {card_type}", fg=color)
|
||||||
|
self.level_var.set(str(level))
|
||||||
|
|
||||||
|
# Update border color based on rarity
|
||||||
|
rarity_borders = {'SSR': '#ffd700', 'SR': '#c0c0c0', 'R': '#cd853f'}
|
||||||
|
self.config(highlightbackground=rarity_borders.get(rarity, BG_LIGHT))
|
||||||
|
|
||||||
|
# Load Image
|
||||||
|
self._load_image(image_path)
|
||||||
|
|
||||||
|
self.toggle_controls(True)
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.name_label.config(text="Empty Slot", fg=TEXT_MUTED)
|
||||||
|
self.meta_label.config(text="Click a card to add")
|
||||||
|
self.image_label.config(image='', text="📭", font=('Segoe UI', 20))
|
||||||
|
self.config(highlightbackground=BG_LIGHT)
|
||||||
|
self.image_ref = None
|
||||||
|
self.toggle_controls(False)
|
||||||
|
|
||||||
|
def _load_image(self, path):
|
||||||
|
resolved_path = resolve_image_path(path)
|
||||||
|
if resolved_path and os.path.exists(resolved_path):
|
||||||
|
try:
|
||||||
|
pil_img = Image.open(resolved_path)
|
||||||
|
pil_img.thumbnail((65, 65), Image.Resampling.LANCZOS)
|
||||||
|
self.image_ref = ImageTk.PhotoImage(pil_img)
|
||||||
|
self.image_label.config(image=self.image_ref, text='')
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to load image: {e}")
|
||||||
|
self.image_label.config(image='', text="⚠️")
|
||||||
|
else:
|
||||||
|
self.image_label.config(image='', text="🖼️")
|
||||||
|
|
||||||
|
def _on_level_change(self, event):
|
||||||
|
self.level_callback(self.index, int(self.level_var.get()))
|
||||||
|
|
||||||
|
|
||||||
|
class DeckBuilderFrame(ttk.Frame):
|
||||||
|
"""Deck builder with combined effects breakdown"""
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.current_deck_id = None
|
||||||
|
self.deck_slots = [None] * 6 # 6 card slots
|
||||||
|
self.setup_ui()
|
||||||
|
self.refresh_decks()
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
# Main container with split view
|
||||||
|
main_split = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
|
||||||
|
main_split.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||||
|
|
||||||
|
# === Left Panel: Card Browser ===
|
||||||
|
left_panel = ttk.Frame(main_split)
|
||||||
|
main_split.add(left_panel, weight=1)
|
||||||
|
|
||||||
|
# Header
|
||||||
|
header = tk.Frame(left_panel, bg=BG_DARK)
|
||||||
|
header.pack(fill=tk.X, pady=(0, 10))
|
||||||
|
tk.Label(header, text="📋 Available Cards", font=FONT_SUBHEADER,
|
||||||
|
bg=BG_DARK, fg=TEXT_PRIMARY).pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
# Filters
|
||||||
|
filter_frame = tk.Frame(left_panel, bg=BG_DARK)
|
||||||
|
filter_frame.pack(fill=tk.X, pady=(0, 8))
|
||||||
|
|
||||||
|
# Filters - Initialize vars FIRST
|
||||||
|
self.type_var = tk.StringVar(value="All")
|
||||||
|
self.owned_only_var = tk.BooleanVar(value=False)
|
||||||
|
self.search_var = tk.StringVar()
|
||||||
|
|
||||||
|
# Search Entry
|
||||||
|
self.search_entry = ttk.Entry(filter_frame, textvariable=self.search_var, width=18)
|
||||||
|
self.search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 8))
|
||||||
|
|
||||||
|
# Placeholder behavior (before trace)
|
||||||
|
self.search_entry.insert(0, "Search...")
|
||||||
|
self.search_entry.config(foreground=TEXT_MUTED)
|
||||||
|
self.search_entry.bind('<FocusIn>', self._on_search_focus_in)
|
||||||
|
self.search_entry.bind('<FocusOut>', self._on_search_focus_out)
|
||||||
|
|
||||||
|
# Add trace AFTER placeholder is set
|
||||||
|
self.search_var.trace('w', lambda *args: self.filter_cards())
|
||||||
|
|
||||||
|
types = ["All", "Speed", "Stamina", "Power", "Guts", "Wisdom", "Friend", "Group"]
|
||||||
|
type_combo = ttk.Combobox(filter_frame, textvariable=self.type_var,
|
||||||
|
values=types, width=9, state='readonly')
|
||||||
|
type_combo.pack(side=tk.LEFT)
|
||||||
|
type_combo.bind('<<ComboboxSelected>>', lambda e: self.filter_cards())
|
||||||
|
|
||||||
|
ttk.Checkbutton(filter_frame, text="Owned", variable=self.owned_only_var,
|
||||||
|
command=self.filter_cards).pack(side=tk.LEFT, padx=8)
|
||||||
|
|
||||||
|
# Card List
|
||||||
|
list_frame = tk.Frame(left_panel, bg=BG_DARK)
|
||||||
|
list_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
self.card_tree = ttk.Treeview(list_frame, columns=('name', 'rarity', 'type'),
|
||||||
|
show='tree headings', style="DeckList.Treeview")
|
||||||
|
self.card_tree.heading('#0', text='')
|
||||||
|
self.card_tree.column('#0', width=45, anchor='center')
|
||||||
|
|
||||||
|
self.card_tree.heading('name', text='Name')
|
||||||
|
self.card_tree.heading('rarity', text='Rarity')
|
||||||
|
self.card_tree.heading('type', text='Type')
|
||||||
|
self.card_tree.column('name', width=130)
|
||||||
|
self.card_tree.column('rarity', width=45, anchor='center')
|
||||||
|
self.card_tree.column('type', width=65, anchor='center')
|
||||||
|
|
||||||
|
scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.card_tree.yview)
|
||||||
|
self.card_tree.configure(yscrollcommand=scrollbar.set)
|
||||||
|
|
||||||
|
self.card_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
|
|
||||||
|
# Double-click to add
|
||||||
|
self.card_tree.bind('<Double-1>', lambda e: self.add_selected_to_deck())
|
||||||
|
|
||||||
|
# Add Button
|
||||||
|
add_btn = create_styled_button(left_panel, text="➕ Add to Deck",
|
||||||
|
command=self.add_selected_to_deck,
|
||||||
|
style_type='accent')
|
||||||
|
add_btn.pack(fill=tk.X, pady=10)
|
||||||
|
|
||||||
|
# === Right Panel: Deck & Stats ===
|
||||||
|
right_panel = ttk.Frame(main_split)
|
||||||
|
main_split.add(right_panel, weight=2)
|
||||||
|
|
||||||
|
# Deck Controls
|
||||||
|
deck_ctrl = tk.Frame(right_panel, bg=BG_DARK)
|
||||||
|
deck_ctrl.pack(fill=tk.X, pady=(0, 15))
|
||||||
|
|
||||||
|
tk.Label(deck_ctrl, text="🎴 Current Deck:", font=FONT_BODY,
|
||||||
|
bg=BG_DARK, fg=TEXT_SECONDARY).pack(side=tk.LEFT)
|
||||||
|
self.deck_combo = ttk.Combobox(deck_ctrl, width=25, state='readonly')
|
||||||
|
self.deck_combo.pack(side=tk.LEFT, padx=10)
|
||||||
|
self.deck_combo.bind('<<ComboboxSelected>>', self.on_deck_selected)
|
||||||
|
|
||||||
|
ttk.Button(deck_ctrl, text="+ New", command=self.create_new_deck,
|
||||||
|
style='Small.TButton').pack(side=tk.LEFT, padx=5)
|
||||||
|
ttk.Button(deck_ctrl, text="🗑️ Delete", command=self.delete_current_deck,
|
||||||
|
style='Small.TButton').pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
# Card count indicator
|
||||||
|
self.deck_count_label = tk.Label(deck_ctrl, text="0/6 cards",
|
||||||
|
font=FONT_SMALL, bg=BG_DARK, fg=ACCENT_PRIMARY)
|
||||||
|
self.deck_count_label.pack(side=tk.LEFT, padx=15)
|
||||||
|
|
||||||
|
# Deck Grid (3x2)
|
||||||
|
self.slots_frame = tk.Frame(right_panel, bg=BG_DARK)
|
||||||
|
self.slots_frame.pack(fill=tk.X)
|
||||||
|
|
||||||
|
self.card_slots = []
|
||||||
|
for i in range(6):
|
||||||
|
slot = CardSlot(self.slots_frame, i, self.remove_from_slot, self.on_slot_level_changed)
|
||||||
|
r, c = divmod(i, 3)
|
||||||
|
slot.grid(row=r, column=c, padx=6, pady=6, sticky='nsew')
|
||||||
|
self.slots_frame.columnconfigure(c, weight=1)
|
||||||
|
self.card_slots.append(slot)
|
||||||
|
|
||||||
|
# Stats / Effects Area
|
||||||
|
effects_header = tk.Frame(right_panel, bg=BG_DARK)
|
||||||
|
effects_header.pack(fill=tk.X, pady=(20, 10))
|
||||||
|
tk.Label(effects_header, text="📊 Combined Effects Breakdown",
|
||||||
|
font=FONT_SUBHEADER, bg=BG_DARK, fg=TEXT_PRIMARY).pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
effects_frame = create_card_frame(right_panel)
|
||||||
|
effects_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
self.effects_tree = ttk.Treeview(effects_frame,
|
||||||
|
columns=('effect', 'total', 'c1', 'c2', 'c3', 'c4', 'c5', 'c6'),
|
||||||
|
show='headings', height=8)
|
||||||
|
|
||||||
|
self.effects_tree.heading('effect', text='Effect')
|
||||||
|
self.effects_tree.heading('total', text='TOTAL')
|
||||||
|
self.effects_tree.column('effect', width=140)
|
||||||
|
self.effects_tree.column('total', width=60, anchor='center')
|
||||||
|
|
||||||
|
for i in range(1, 7):
|
||||||
|
self.effects_tree.heading(f'c{i}', text=f'#{i}')
|
||||||
|
self.effects_tree.column(f'c{i}', width=45, anchor='center')
|
||||||
|
|
||||||
|
vsb = ttk.Scrollbar(effects_frame, orient=tk.VERTICAL, command=self.effects_tree.yview)
|
||||||
|
self.effects_tree.configure(yscrollcommand=vsb.set)
|
||||||
|
|
||||||
|
self.effects_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=2, pady=2)
|
||||||
|
vsb.pack(side=tk.RIGHT, fill=tk.Y, pady=2)
|
||||||
|
|
||||||
|
# Unique Effects Area
|
||||||
|
unique_header = tk.Frame(right_panel, bg=BG_DARK)
|
||||||
|
unique_header.pack(fill=tk.X, pady=(15, 8))
|
||||||
|
tk.Label(unique_header, text="✨ Unique Effects", font=FONT_BODY_BOLD,
|
||||||
|
bg=BG_DARK, fg=ACCENT_SECONDARY).pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
unique_frame = create_card_frame(right_panel)
|
||||||
|
unique_frame.pack(fill=tk.X)
|
||||||
|
|
||||||
|
self.unique_text = create_styled_text(unique_frame, height=5)
|
||||||
|
self.unique_text.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
|
||||||
|
self.unique_text.config(state=tk.DISABLED)
|
||||||
|
|
||||||
|
self.icon_cache = {}
|
||||||
|
self.filter_cards()
|
||||||
|
|
||||||
|
|
||||||
|
# Helper methods for placeholder
|
||||||
|
def _on_search_focus_in(self, event):
|
||||||
|
"""Clear placeholder on focus"""
|
||||||
|
if self.search_entry.get() == "Search...":
|
||||||
|
self.search_entry.delete(0, tk.END)
|
||||||
|
self.search_entry.config(foreground=TEXT_PRIMARY)
|
||||||
|
|
||||||
|
def _on_search_focus_out(self, event):
|
||||||
|
"""Show placeholder if empty"""
|
||||||
|
if not self.search_entry.get():
|
||||||
|
self.search_entry.insert(0, "Search...")
|
||||||
|
self.search_entry.config(foreground=TEXT_MUTED)
|
||||||
|
|
||||||
|
# --- Logic Methods ---
|
||||||
|
|
||||||
|
def filter_cards(self):
|
||||||
|
for item in self.card_tree.get_children():
|
||||||
|
self.card_tree.delete(item)
|
||||||
|
|
||||||
|
type_filter = self.type_var.get() if self.type_var.get() != "All" else None
|
||||||
|
|
||||||
|
# Ignore placeholder
|
||||||
|
search_text = self.search_var.get()
|
||||||
|
search = search_text if search_text and search_text != "Search..." else None
|
||||||
|
|
||||||
|
owned_only = self.owned_only_var.get()
|
||||||
|
|
||||||
|
cards = get_all_cards(type_filter=type_filter, search_term=search, owned_only=owned_only)
|
||||||
|
|
||||||
|
for card in cards:
|
||||||
|
card_id, name, rarity, card_type, max_level, image_path, is_owned, owned_level = card
|
||||||
|
|
||||||
|
# Load Icon
|
||||||
|
img = self.icon_cache.get(card_id)
|
||||||
|
resolved_path = resolve_image_path(image_path)
|
||||||
|
|
||||||
|
if not img and resolved_path and os.path.exists(resolved_path):
|
||||||
|
try:
|
||||||
|
pil_img = Image.open(resolved_path)
|
||||||
|
pil_img.thumbnail((32, 32), Image.Resampling.LANCZOS)
|
||||||
|
img = ImageTk.PhotoImage(pil_img)
|
||||||
|
self.icon_cache[card_id] = img
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
type_icon = get_type_icon(card_type)
|
||||||
|
if img:
|
||||||
|
self.card_tree.insert('', tk.END, text='', image=img,
|
||||||
|
values=(name, rarity, f"{type_icon}"), iid=str(card_id))
|
||||||
|
else:
|
||||||
|
self.card_tree.insert('', tk.END, text='?',
|
||||||
|
values=(name, rarity, f"{type_icon}"), iid=str(card_id))
|
||||||
|
|
||||||
|
def refresh_decks(self):
|
||||||
|
decks = get_all_decks()
|
||||||
|
self.deck_combo['values'] = [f"{d[0]}: {d[1]}" for d in decks]
|
||||||
|
if decks and not self.current_deck_id:
|
||||||
|
self.deck_combo.current(0)
|
||||||
|
self.on_deck_selected(None)
|
||||||
|
|
||||||
|
def on_deck_selected(self, event):
|
||||||
|
selection = self.deck_combo.get()
|
||||||
|
if selection:
|
||||||
|
self.current_deck_id = int(selection.split(':')[0])
|
||||||
|
self.load_deck()
|
||||||
|
|
||||||
|
def load_deck(self):
|
||||||
|
if not self.current_deck_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Reset visual slots
|
||||||
|
for s in self.card_slots:
|
||||||
|
s.reset()
|
||||||
|
self.deck_slots = [None] * 6
|
||||||
|
|
||||||
|
# Load from DB
|
||||||
|
deck_cards = get_deck_cards(self.current_deck_id)
|
||||||
|
|
||||||
|
for card in deck_cards:
|
||||||
|
slot_pos, level, card_id, name, rarity, card_type, image_path = card
|
||||||
|
if 0 <= slot_pos < 6:
|
||||||
|
self.deck_slots[slot_pos] = card_id
|
||||||
|
self.card_slots[slot_pos].set_card((card_id, name, rarity, card_type, image_path, level))
|
||||||
|
|
||||||
|
self.update_deck_count()
|
||||||
|
self.update_effects_breakdown()
|
||||||
|
|
||||||
|
def create_new_deck(self):
|
||||||
|
name = tk.simpledialog.askstring("New Deck", "Enter deck name:")
|
||||||
|
if name:
|
||||||
|
deck_id = create_deck(name)
|
||||||
|
self.current_deck_id = deck_id
|
||||||
|
self.refresh_decks()
|
||||||
|
self.deck_combo.set(f"{deck_id}: {name}")
|
||||||
|
self.load_deck()
|
||||||
|
|
||||||
|
def delete_current_deck(self):
|
||||||
|
if self.current_deck_id:
|
||||||
|
if messagebox.askyesno("Delete Deck", "Are you sure you want to delete this deck?"):
|
||||||
|
delete_deck(self.current_deck_id)
|
||||||
|
self.current_deck_id = None
|
||||||
|
self.deck_combo.set('')
|
||||||
|
self.refresh_decks()
|
||||||
|
self.load_deck()
|
||||||
|
|
||||||
|
def add_selected_to_deck(self):
|
||||||
|
if not self.current_deck_id:
|
||||||
|
messagebox.showwarning("No Deck", "Select or create a deck first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
selection = self.card_tree.selection()
|
||||||
|
if not selection:
|
||||||
|
return
|
||||||
|
card_id = int(selection[0])
|
||||||
|
|
||||||
|
# Check for duplicates
|
||||||
|
if card_id in self.deck_slots:
|
||||||
|
messagebox.showinfo("Duplicate Card", "This card is already in the deck.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find empty slot
|
||||||
|
for i in range(6):
|
||||||
|
if self.deck_slots[i] is None:
|
||||||
|
add_card_to_deck(self.current_deck_id, card_id, i, 50)
|
||||||
|
self.load_deck()
|
||||||
|
return
|
||||||
|
|
||||||
|
messagebox.showinfo("Deck Full", "Remove a card first to add a new one.")
|
||||||
|
|
||||||
|
def remove_from_slot(self, index):
|
||||||
|
if self.current_deck_id and self.deck_slots[index]:
|
||||||
|
remove_card_from_deck(self.current_deck_id, index)
|
||||||
|
self.deck_slots[index] = None
|
||||||
|
self.card_slots[index].reset()
|
||||||
|
self.update_deck_count()
|
||||||
|
self.update_effects_breakdown()
|
||||||
|
|
||||||
|
def update_deck_count(self):
|
||||||
|
"""Update the X/6 cards display"""
|
||||||
|
count = sum(1 for slot in self.deck_slots if slot is not None)
|
||||||
|
self.deck_count_label.config(text=f"{count}/6 cards")
|
||||||
|
|
||||||
|
def on_slot_level_changed(self, index, new_level):
|
||||||
|
if self.current_deck_id and self.deck_slots[index]:
|
||||||
|
card_id = self.deck_slots[index]
|
||||||
|
add_card_to_deck(self.current_deck_id, card_id, index, new_level)
|
||||||
|
self.update_effects_breakdown()
|
||||||
|
|
||||||
|
def update_effects_breakdown(self):
|
||||||
|
for item in self.effects_tree.get_children():
|
||||||
|
self.effects_tree.delete(item)
|
||||||
|
|
||||||
|
# Clear Unique Text
|
||||||
|
self.unique_text.config(state=tk.NORMAL)
|
||||||
|
self.unique_text.delete('1.0', tk.END)
|
||||||
|
|
||||||
|
if not self.current_deck_id:
|
||||||
|
self.unique_text.insert(tk.END, "No deck selected")
|
||||||
|
self.unique_text.config(state=tk.DISABLED)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Prepare data for calculation
|
||||||
|
card_info = []
|
||||||
|
for i in range(6):
|
||||||
|
if self.deck_slots[i]:
|
||||||
|
level = int(self.card_slots[i].level_var.get())
|
||||||
|
card_info.append((self.deck_slots[i], level))
|
||||||
|
else:
|
||||||
|
card_info.append(None)
|
||||||
|
|
||||||
|
# Gather effects
|
||||||
|
all_effects = {}
|
||||||
|
unique_effects_list = []
|
||||||
|
|
||||||
|
for i, info in enumerate(card_info):
|
||||||
|
if info:
|
||||||
|
card_id, level = info
|
||||||
|
card_name = self.card_slots[i].name_label.cget("text")
|
||||||
|
|
||||||
|
effects = get_effects_at_level(card_id, level)
|
||||||
|
for name, value in effects:
|
||||||
|
if name == "Unique Effect":
|
||||||
|
unique_effects_list.append(f"• {card_name}: {value}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if name not in all_effects:
|
||||||
|
all_effects[name] = [''] * 6
|
||||||
|
all_effects[name][i] = value
|
||||||
|
|
||||||
|
# Configure tags
|
||||||
|
self.unique_text.tag_configure('card_name', foreground=ACCENT_PRIMARY)
|
||||||
|
|
||||||
|
# Fill Unique Effects
|
||||||
|
if unique_effects_list:
|
||||||
|
self.unique_text.insert(tk.END, "\n".join(unique_effects_list))
|
||||||
|
else:
|
||||||
|
self.unique_text.insert(tk.END, "No unique effects in this deck", 'card_name')
|
||||||
|
self.unique_text.config(state=tk.DISABLED)
|
||||||
|
|
||||||
|
# Sum totals
|
||||||
|
for effect_name, values in sorted(all_effects.items()):
|
||||||
|
total = 0
|
||||||
|
is_percent = False
|
||||||
|
for v in values:
|
||||||
|
if v:
|
||||||
|
if '%' in str(v): is_percent = True
|
||||||
|
try:
|
||||||
|
total += float(str(v).replace('%','').replace('+',''))
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
total_str = f"{total:.0f}%" if is_percent else (f"+{total:.0f}" if total > 0 else str(int(total)))
|
||||||
|
row_vals = [effect_name, total_str] + values
|
||||||
|
self.effects_tree.insert('', tk.END, values=row_vals)
|
||||||
|
|
||||||
|
import tkinter.simpledialog
|
||||||
23
gui/deck_view.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import tkinter as tk
|
||||||
|
from db.db_queries import get_deck_bonus
|
||||||
|
|
||||||
|
class DeckView(tk.Toplevel):
|
||||||
|
def __init__(self, parent):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.title("Deck Builder")
|
||||||
|
self.geometry("500x400")
|
||||||
|
|
||||||
|
self.deck_id = 1 # Default deck
|
||||||
|
|
||||||
|
tk.Button(self, text="Calculate Deck Bonuses", command=self.calculate).pack(pady=10)
|
||||||
|
self.output = tk.Text(self, height=20)
|
||||||
|
self.output.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
def calculate(self):
|
||||||
|
self.output.delete("1.0", tk.END)
|
||||||
|
bonuses = get_deck_bonus(self.deck_id)
|
||||||
|
if not bonuses:
|
||||||
|
self.output.insert(tk.END, "No bonuses found for this deck.\n")
|
||||||
|
return
|
||||||
|
for bonus, total in bonuses:
|
||||||
|
self.output.insert(tk.END, f"{bonus}: +{total}\n")
|
||||||
298
gui/effects_view.py
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
"""
|
||||||
|
Effects View - Display support effects at all levels with interactive slider
|
||||||
|
"""
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk, messagebox
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
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 gui.theme import (
|
||||||
|
BG_DARK, BG_MEDIUM, BG_LIGHT, BG_HIGHLIGHT,
|
||||||
|
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_SUCCESS, ACCENT_TERTIARY,
|
||||||
|
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
|
||||||
|
FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL, FONT_MONO,
|
||||||
|
create_styled_button, create_styled_text, create_card_frame,
|
||||||
|
EFFECT_DESCRIPTIONS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EffectsFrame(ttk.Frame):
|
||||||
|
"""Frame for viewing support effects at different levels"""
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.current_card_id = None
|
||||||
|
self.current_card_name = None
|
||||||
|
self.max_level = 50
|
||||||
|
|
||||||
|
self.create_widgets()
|
||||||
|
|
||||||
|
def create_widgets(self):
|
||||||
|
"""Create the effects view interface"""
|
||||||
|
# Header
|
||||||
|
header_frame = tk.Frame(self, bg=BG_DARK)
|
||||||
|
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",
|
||||||
|
font=FONT_HEADER, bg=BG_DARK, fg=ACCENT_PRIMARY)
|
||||||
|
self.card_label.pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
# Legend Button
|
||||||
|
legend_btn = create_styled_button(header_frame, text="❓ Legend",
|
||||||
|
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
|
||||||
|
control_frame = tk.Frame(self, bg=BG_MEDIUM, padx=15, pady=12)
|
||||||
|
control_frame.pack(fill=tk.X, padx=20)
|
||||||
|
|
||||||
|
# Level label
|
||||||
|
tk.Label(control_frame, text="Level:", font=FONT_BODY,
|
||||||
|
bg=BG_MEDIUM, fg=TEXT_SECONDARY).pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
# Level display with increment/decrement buttons
|
||||||
|
level_ctrl = tk.Frame(control_frame, bg=BG_MEDIUM)
|
||||||
|
level_ctrl.pack(side=tk.LEFT, padx=15)
|
||||||
|
|
||||||
|
# Decrement button
|
||||||
|
dec_btn = tk.Button(level_ctrl, text="−", font=FONT_HEADER,
|
||||||
|
bg=BG_LIGHT, fg=TEXT_PRIMARY, bd=0, width=2,
|
||||||
|
activebackground=BG_HIGHLIGHT, cursor='hand2',
|
||||||
|
command=self.decrement_level)
|
||||||
|
dec_btn.pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
self.level_var = tk.IntVar(value=50)
|
||||||
|
self.level_display = tk.Label(level_ctrl, text="50", width=4, font=FONT_HEADER,
|
||||||
|
bg=BG_LIGHT, fg=ACCENT_PRIMARY, padx=10)
|
||||||
|
self.level_display.pack(side=tk.LEFT, padx=2)
|
||||||
|
|
||||||
|
# Increment button
|
||||||
|
inc_btn = tk.Button(level_ctrl, text="+", font=FONT_HEADER,
|
||||||
|
bg=BG_LIGHT, fg=TEXT_PRIMARY, bd=0, width=2,
|
||||||
|
activebackground=BG_HIGHLIGHT, cursor='hand2',
|
||||||
|
command=self.increment_level)
|
||||||
|
inc_btn.pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
# Quick level buttons
|
||||||
|
button_frame = tk.Frame(control_frame, bg=BG_MEDIUM)
|
||||||
|
button_frame.pack(side=tk.LEFT, padx=25)
|
||||||
|
|
||||||
|
quick_levels = [1, 25, 40, 50]
|
||||||
|
for lvl in quick_levels:
|
||||||
|
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
|
||||||
|
content_frame = tk.Frame(self, bg=BG_DARK)
|
||||||
|
content_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=15)
|
||||||
|
|
||||||
|
# Left: Current level effects
|
||||||
|
left_frame = ttk.LabelFrame(content_frame, text=" Current Level Effects ", padding=12)
|
||||||
|
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 10))
|
||||||
|
|
||||||
|
self.current_effects = create_styled_text(left_frame, height=15)
|
||||||
|
self.current_effects.pack(fill=tk.BOTH, expand=True)
|
||||||
|
self.current_effects.config(state=tk.DISABLED)
|
||||||
|
|
||||||
|
# Right: Effect progression table
|
||||||
|
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)
|
||||||
|
|
||||||
|
def show_legend(self):
|
||||||
|
"""Show effect explanations"""
|
||||||
|
legend = {
|
||||||
|
"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"
|
||||||
|
for name, desc in legend.items():
|
||||||
|
text += f"• {name}:\n {desc}\n\n"
|
||||||
|
|
||||||
|
messagebox.showinfo("Effect Legend", text)
|
||||||
|
|
||||||
|
def set_card(self, card_id):
|
||||||
|
"""Load a card's effects"""
|
||||||
|
self.current_card_id = card_id
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
level = self.level_var.get()
|
||||||
|
effects = get_effects_at_level(self.current_card_id, level)
|
||||||
|
|
||||||
|
self.current_effects.insert(tk.END, f"━━━ Level {level} ━━━\n\n", 'header')
|
||||||
|
|
||||||
|
if effects:
|
||||||
|
for name, value in effects:
|
||||||
|
# Highlight high values
|
||||||
|
prefix = ""
|
||||||
|
if '%' in str(value):
|
||||||
|
try:
|
||||||
|
num = int(str(value).replace('%', '').replace('+', ''))
|
||||||
|
if num >= 20:
|
||||||
|
prefix = "★ "
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if prefix:
|
||||||
|
self.current_effects.insert(tk.END, prefix, 'highlight')
|
||||||
|
|
||||||
|
# Insert effect name with tooltip tag
|
||||||
|
tag_name = f"tooltip_{name.replace(' ', '_')}"
|
||||||
|
self.current_effects.insert(tk.END, f"{name}: ", ('effect_name', tag_name))
|
||||||
|
|
||||||
|
# Bind tooltip events
|
||||||
|
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')
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
180
gui/hints_skills_view.py
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
"""
|
||||||
|
Hints and Skills View - Display support hints and event skills
|
||||||
|
"""
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from db.db_queries import get_hints, get_events, get_all_event_skills, get_card_by_id
|
||||||
|
from gui.theme import (
|
||||||
|
BG_DARK, BG_MEDIUM, BG_LIGHT,
|
||||||
|
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_TERTIARY, ACCENT_SUCCESS,
|
||||||
|
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
|
||||||
|
FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL,
|
||||||
|
create_styled_text, create_card_frame
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HintsSkillsFrame(ttk.Frame):
|
||||||
|
"""Frame for viewing support hints and event skills"""
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.current_card_id = None
|
||||||
|
self.current_card_name = None
|
||||||
|
|
||||||
|
self.create_widgets()
|
||||||
|
|
||||||
|
def create_widgets(self):
|
||||||
|
"""Create the hints and skills interface"""
|
||||||
|
# Header
|
||||||
|
header_frame = tk.Frame(self, bg=BG_DARK)
|
||||||
|
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",
|
||||||
|
font=FONT_HEADER, bg=BG_DARK, fg=ACCENT_PRIMARY)
|
||||||
|
self.card_label.pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
# Main content with two columns
|
||||||
|
content_frame = tk.Frame(self, bg=BG_DARK)
|
||||||
|
content_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=(0, 15))
|
||||||
|
|
||||||
|
# Left column: Hints
|
||||||
|
left_container = tk.Frame(content_frame, bg=BG_DARK)
|
||||||
|
left_container.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 10))
|
||||||
|
|
||||||
|
hints_header = tk.Frame(left_container, bg=BG_DARK)
|
||||||
|
hints_header.pack(fill=tk.X, pady=(0, 8))
|
||||||
|
tk.Label(hints_header, text="🎯 Training Hints", font=FONT_SUBHEADER,
|
||||||
|
bg=BG_DARK, fg=TEXT_PRIMARY).pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
hints_frame = create_card_frame(left_container)
|
||||||
|
hints_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
self.hints_text = create_styled_text(hints_frame, height=18)
|
||||||
|
self.hints_text.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
|
||||||
|
self.hints_text.config(state=tk.DISABLED)
|
||||||
|
|
||||||
|
# Configure tags for hints
|
||||||
|
self.hints_text.tag_configure('header', font=FONT_SUBHEADER, foreground=ACCENT_PRIMARY)
|
||||||
|
self.hints_text.tag_configure('skill', foreground=ACCENT_TERTIARY, font=FONT_BODY_BOLD)
|
||||||
|
self.hints_text.tag_configure('desc', foreground=TEXT_MUTED)
|
||||||
|
self.hints_text.tag_configure('number', foreground=ACCENT_SECONDARY)
|
||||||
|
|
||||||
|
# Right column: Events and Skills
|
||||||
|
right_container = tk.Frame(content_frame, bg=BG_DARK)
|
||||||
|
right_container.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
events_header = tk.Frame(right_container, bg=BG_DARK)
|
||||||
|
events_header.pack(fill=tk.X, pady=(0, 8))
|
||||||
|
tk.Label(events_header, text="📅 Training Events & Skills", font=FONT_SUBHEADER,
|
||||||
|
bg=BG_DARK, fg=TEXT_PRIMARY).pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
events_frame = create_card_frame(right_container)
|
||||||
|
events_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
tree_container = tk.Frame(events_frame, bg=BG_MEDIUM)
|
||||||
|
tree_container.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
|
||||||
|
|
||||||
|
# Treeview for events
|
||||||
|
self.events_tree = ttk.Treeview(tree_container, columns=('event', 'skills'), show='tree headings')
|
||||||
|
self.events_tree.heading('#0', text='')
|
||||||
|
self.events_tree.heading('event', text='Event/Skill')
|
||||||
|
self.events_tree.heading('skills', text='Details')
|
||||||
|
|
||||||
|
self.events_tree.column('#0', width=35)
|
||||||
|
self.events_tree.column('event', width=240)
|
||||||
|
self.events_tree.column('skills', width=180)
|
||||||
|
|
||||||
|
scrollbar = ttk.Scrollbar(tree_container, orient=tk.VERTICAL, command=self.events_tree.yview)
|
||||||
|
self.events_tree.configure(yscrollcommand=scrollbar.set)
|
||||||
|
|
||||||
|
self.events_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
|
|
||||||
|
# Summary section at bottom
|
||||||
|
summary_frame = tk.Frame(self, bg=BG_MEDIUM, padx=15, pady=10)
|
||||||
|
summary_frame.pack(fill=tk.X, padx=20, pady=(0, 10))
|
||||||
|
|
||||||
|
self.summary_label = tk.Label(summary_frame, text="", font=FONT_SMALL,
|
||||||
|
bg=BG_MEDIUM, fg=TEXT_SECONDARY)
|
||||||
|
self.summary_label.pack()
|
||||||
|
|
||||||
|
def set_card(self, card_id):
|
||||||
|
"""Load a card's hints and skills"""
|
||||||
|
self.current_card_id = card_id
|
||||||
|
|
||||||
|
# Get card info
|
||||||
|
card = get_card_by_id(card_id)
|
||||||
|
if card:
|
||||||
|
self.current_card_name = card[1]
|
||||||
|
self.card_label.config(text=f"💡 {self.current_card_name}")
|
||||||
|
|
||||||
|
self.update_hints_display()
|
||||||
|
self.update_events_display()
|
||||||
|
|
||||||
|
def update_hints_display(self):
|
||||||
|
"""Update the hints display"""
|
||||||
|
self.hints_text.config(state=tk.NORMAL)
|
||||||
|
self.hints_text.delete('1.0', tk.END)
|
||||||
|
|
||||||
|
if not self.current_card_id:
|
||||||
|
self.hints_text.insert(tk.END, "No card selected\n\n", 'desc')
|
||||||
|
self.hints_text.insert(tk.END, "Select a card from the Card List tab to view its hints.", 'desc')
|
||||||
|
self.hints_text.config(state=tk.DISABLED)
|
||||||
|
return
|
||||||
|
|
||||||
|
hints = get_hints(self.current_card_id)
|
||||||
|
|
||||||
|
self.hints_text.insert(tk.END, "Training Skills this card can teach:\n\n", 'header')
|
||||||
|
|
||||||
|
if hints:
|
||||||
|
for i, (hint_name, hint_desc) in enumerate(hints, 1):
|
||||||
|
self.hints_text.insert(tk.END, f" {i}. ", 'number')
|
||||||
|
self.hints_text.insert(tk.END, f"{hint_name}\n", 'skill')
|
||||||
|
if hint_desc:
|
||||||
|
self.hints_text.insert(tk.END, f" {hint_desc}\n", 'desc')
|
||||||
|
self.hints_text.insert(tk.END, "\n")
|
||||||
|
else:
|
||||||
|
self.hints_text.insert(tk.END, " No hints/skills data available.\n\n", 'desc')
|
||||||
|
self.hints_text.insert(tk.END, " This may mean:\n", 'desc')
|
||||||
|
self.hints_text.insert(tk.END, " • Card hasn't been scraped yet\n", 'desc')
|
||||||
|
self.hints_text.insert(tk.END, " • Card has no trainable skills\n", 'desc')
|
||||||
|
|
||||||
|
self.hints_text.config(state=tk.DISABLED)
|
||||||
|
|
||||||
|
def update_events_display(self):
|
||||||
|
"""Update the events tree display"""
|
||||||
|
self.events_tree.delete(*self.events_tree.get_children())
|
||||||
|
|
||||||
|
if not self.current_card_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
events = get_events(self.current_card_id)
|
||||||
|
events_with_skills = get_all_event_skills(self.current_card_id)
|
||||||
|
|
||||||
|
# Add events as parent nodes
|
||||||
|
for event_id, event_name, event_type in events:
|
||||||
|
skills = events_with_skills.get(event_name, [])
|
||||||
|
skill_count = f"{len(skills)} skills" if skills else "No skills"
|
||||||
|
|
||||||
|
event_node = self.events_tree.insert('', tk.END, text='📅',
|
||||||
|
values=(event_name, skill_count))
|
||||||
|
|
||||||
|
# Add skills as children
|
||||||
|
for skill in skills:
|
||||||
|
self.events_tree.insert(event_node, tk.END, text='⭐',
|
||||||
|
values=(skill, ''))
|
||||||
|
|
||||||
|
# Update summary
|
||||||
|
hint_count = len(get_hints(self.current_card_id))
|
||||||
|
event_count = len(events)
|
||||||
|
|
||||||
|
self.summary_label.config(
|
||||||
|
text=f"📊 Summary: {hint_count} hints │ {event_count} events"
|
||||||
|
)
|
||||||
239
gui/main_window.py
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
"""
|
||||||
|
Main Window for Umamusume Support Card Manager
|
||||||
|
Tabbed interface for card browsing, effects, deck builder, and hints
|
||||||
|
"""
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from db.db_queries import get_database_stats, get_owned_count
|
||||||
|
from gui.card_view import CardListFrame
|
||||||
|
from gui.effects_view import EffectsFrame
|
||||||
|
from gui.hints_skills_view import HintsSkillsFrame
|
||||||
|
from gui.deck_builder import DeckBuilderFrame
|
||||||
|
from gui.update_dialog import show_update_dialog
|
||||||
|
from gui.theme import (
|
||||||
|
configure_styles, create_styled_button,
|
||||||
|
BG_DARK, BG_MEDIUM, BG_LIGHT, BG_HIGHLIGHT,
|
||||||
|
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_TERTIARY,
|
||||||
|
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
|
||||||
|
FONT_TITLE, FONT_HEADER, FONT_BODY, FONT_SMALL
|
||||||
|
)
|
||||||
|
from utils import resolve_image_path
|
||||||
|
from version import VERSION
|
||||||
|
|
||||||
|
|
||||||
|
class MainWindow:
|
||||||
|
"""Main application window with tabbed interface"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.root = tk.Tk()
|
||||||
|
self.root.title("Umamusume Support Card Manager")
|
||||||
|
self.root.geometry("1350x800")
|
||||||
|
self.root.minsize(1350, 800)
|
||||||
|
|
||||||
|
# Set icon
|
||||||
|
try:
|
||||||
|
icon_path = resolve_image_path("1_Special Week.png")
|
||||||
|
if icon_path and os.path.exists(icon_path):
|
||||||
|
icon_img = tk.PhotoImage(file=icon_path)
|
||||||
|
self.root.iconphoto(True, icon_img)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to set icon: {e}")
|
||||||
|
|
||||||
|
# Configure all styles using centralized theme
|
||||||
|
configure_styles(self.root)
|
||||||
|
|
||||||
|
# Create main container
|
||||||
|
main_container = ttk.Frame(self.root)
|
||||||
|
main_container.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# Header with stats
|
||||||
|
self.create_header(main_container)
|
||||||
|
|
||||||
|
# Status bar - Create BEFORE notebook to anchor it to bottom
|
||||||
|
self.create_status_bar(main_container)
|
||||||
|
|
||||||
|
# Tabbed notebook
|
||||||
|
self.notebook = ttk.Notebook(main_container)
|
||||||
|
self.notebook.pack(fill=tk.BOTH, expand=True, padx=15, pady=8)
|
||||||
|
|
||||||
|
# Create tabs
|
||||||
|
self.create_tabs()
|
||||||
|
|
||||||
|
def create_header(self, parent):
|
||||||
|
"""Create header with database statistics and update button"""
|
||||||
|
# Header container with subtle bottom border effect
|
||||||
|
header_outer = tk.Frame(parent, bg=BG_DARK)
|
||||||
|
header_outer.pack(fill=tk.X)
|
||||||
|
|
||||||
|
header_frame = tk.Frame(header_outer, bg=BG_DARK)
|
||||||
|
header_frame.pack(fill=tk.X, padx=20, pady=15)
|
||||||
|
|
||||||
|
# Left side: Title and version
|
||||||
|
title_frame = tk.Frame(header_frame, bg=BG_DARK)
|
||||||
|
title_frame.pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
# App icon and title
|
||||||
|
title_label = tk.Label(
|
||||||
|
title_frame,
|
||||||
|
text="🏇 Umamusume Support Card Manager",
|
||||||
|
font=FONT_TITLE,
|
||||||
|
bg=BG_DARK,
|
||||||
|
fg=ACCENT_PRIMARY
|
||||||
|
)
|
||||||
|
title_label.pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
# Version badge
|
||||||
|
version_frame = tk.Frame(title_frame, bg=ACCENT_SECONDARY, padx=8, pady=2)
|
||||||
|
version_frame.pack(side=tk.LEFT, padx=12)
|
||||||
|
version_label = tk.Label(
|
||||||
|
version_frame,
|
||||||
|
text=f"v{VERSION}",
|
||||||
|
font=FONT_SMALL,
|
||||||
|
bg=ACCENT_SECONDARY,
|
||||||
|
fg=TEXT_PRIMARY
|
||||||
|
)
|
||||||
|
version_label.pack()
|
||||||
|
|
||||||
|
# Right side: Update button and stats
|
||||||
|
right_frame = tk.Frame(header_frame, bg=BG_DARK)
|
||||||
|
right_frame.pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
# Update button with modern styling
|
||||||
|
self.update_button = create_styled_button(
|
||||||
|
right_frame,
|
||||||
|
text="🔄 Check for Updates",
|
||||||
|
command=self.show_update_dialog,
|
||||||
|
style_type='default'
|
||||||
|
)
|
||||||
|
self.update_button.pack(side=tk.RIGHT, padx=(15, 0))
|
||||||
|
|
||||||
|
# Stats panel with card-like appearance
|
||||||
|
stats_frame = tk.Frame(right_frame, bg=BG_MEDIUM, padx=15, pady=8)
|
||||||
|
stats_frame.pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
stats = get_database_stats()
|
||||||
|
owned = get_owned_count()
|
||||||
|
|
||||||
|
# Build stats text with better formatting
|
||||||
|
stats_parts = [
|
||||||
|
f"📊 {stats.get('total_cards', 0)} Cards",
|
||||||
|
f"✨ {owned} Owned",
|
||||||
|
f"🏆 {stats.get('by_rarity', {}).get('SSR', 0)} SSR",
|
||||||
|
f"⭐ {stats.get('by_rarity', {}).get('SR', 0)} SR",
|
||||||
|
f"● {stats.get('by_rarity', {}).get('R', 0)} R"
|
||||||
|
]
|
||||||
|
stats_text = " │ ".join(stats_parts)
|
||||||
|
|
||||||
|
self.stats_label = tk.Label(
|
||||||
|
stats_frame,
|
||||||
|
text=stats_text,
|
||||||
|
font=FONT_SMALL,
|
||||||
|
bg=BG_MEDIUM,
|
||||||
|
fg=TEXT_SECONDARY
|
||||||
|
)
|
||||||
|
self.stats_label.pack()
|
||||||
|
|
||||||
|
# Subtle separator line
|
||||||
|
separator = tk.Frame(header_outer, bg=BG_LIGHT, height=1)
|
||||||
|
separator.pack(fill=tk.X, padx=15)
|
||||||
|
|
||||||
|
def create_tabs(self):
|
||||||
|
"""Create all tab frames"""
|
||||||
|
# Card List Tab
|
||||||
|
self.card_frame = CardListFrame(self.notebook, on_card_selected_callback=self.on_card_selected)
|
||||||
|
self.notebook.add(self.card_frame, text=" 📋 Card List ")
|
||||||
|
|
||||||
|
# Effects Tab
|
||||||
|
self.effects_frame = EffectsFrame(self.notebook)
|
||||||
|
self.notebook.add(self.effects_frame, text=" 📊 Effects ")
|
||||||
|
|
||||||
|
# Deck Builder Tab
|
||||||
|
self.deck_frame = DeckBuilderFrame(self.notebook)
|
||||||
|
self.notebook.add(self.deck_frame, text=" 🎴 Deck Builder ")
|
||||||
|
|
||||||
|
# Hints & Skills Tab
|
||||||
|
self.hints_frame = HintsSkillsFrame(self.notebook)
|
||||||
|
self.notebook.add(self.hints_frame, text=" 💡 Hints & Skills ")
|
||||||
|
|
||||||
|
def create_status_bar(self, parent):
|
||||||
|
"""Create status bar at bottom"""
|
||||||
|
status_outer = tk.Frame(parent, bg=BG_MEDIUM)
|
||||||
|
status_outer.pack(fill=tk.X, side=tk.BOTTOM)
|
||||||
|
|
||||||
|
status_frame = tk.Frame(status_outer, bg=BG_MEDIUM)
|
||||||
|
status_frame.pack(fill=tk.X, padx=15, pady=8)
|
||||||
|
|
||||||
|
self.status_label = tk.Label(
|
||||||
|
status_frame,
|
||||||
|
text="✓ Ready",
|
||||||
|
font=FONT_SMALL,
|
||||||
|
bg=BG_MEDIUM,
|
||||||
|
fg=TEXT_MUTED
|
||||||
|
)
|
||||||
|
self.status_label.pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
tk.Label(
|
||||||
|
status_frame,
|
||||||
|
text="Data from gametora.com",
|
||||||
|
font=FONT_SMALL,
|
||||||
|
bg=BG_MEDIUM,
|
||||||
|
fg=TEXT_MUTED
|
||||||
|
).pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
tk.Label(
|
||||||
|
status_frame,
|
||||||
|
text="Made by Kiyreload │ ",
|
||||||
|
font=FONT_SMALL,
|
||||||
|
bg=BG_MEDIUM,
|
||||||
|
fg=ACCENT_TERTIARY
|
||||||
|
).pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
def on_card_selected(self, card_id, card_name):
|
||||||
|
"""Handle card selection from card list"""
|
||||||
|
# Update other tabs with selected card
|
||||||
|
if hasattr(self, 'effects_frame'):
|
||||||
|
self.effects_frame.set_card(card_id)
|
||||||
|
if hasattr(self, 'hints_frame'):
|
||||||
|
self.hints_frame.set_card(card_id)
|
||||||
|
|
||||||
|
self.status_label.config(text=f"📌 Selected: {card_name}")
|
||||||
|
|
||||||
|
def refresh_stats(self):
|
||||||
|
"""Refresh the statistics display"""
|
||||||
|
stats = get_database_stats()
|
||||||
|
owned = get_owned_count()
|
||||||
|
|
||||||
|
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.config(text=stats_text)
|
||||||
|
|
||||||
|
def show_update_dialog(self):
|
||||||
|
"""Show the update dialog"""
|
||||||
|
show_update_dialog(self.root)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Start the application"""
|
||||||
|
self.root.mainloop()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Entry point for GUI"""
|
||||||
|
app = MainWindow()
|
||||||
|
app.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
461
gui/theme.py
Normal file
@@ -0,0 +1,461 @@
|
|||||||
|
"""
|
||||||
|
Centralized Theme Module for Umamusume Support Card Manager
|
||||||
|
Modern glassmorphism-inspired dark theme with consistent styling
|
||||||
|
"""
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# COLOR PALETTE
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
# Primary backgrounds (rich purplish-blues with depth)
|
||||||
|
BG_DARKEST = '#0d0d1a' # Deepest background
|
||||||
|
BG_DARK = '#151528' # Main application background
|
||||||
|
BG_MEDIUM = '#1e1e3f' # Card/panel backgrounds
|
||||||
|
BG_LIGHT = '#2a2a5a' # Elevated elements, hover states
|
||||||
|
BG_HIGHLIGHT = '#3d3d7a' # Active/selected backgrounds
|
||||||
|
|
||||||
|
# Accents (vibrant but refined)
|
||||||
|
ACCENT_PRIMARY = '#ff6b9d' # Pink accent (main action color)
|
||||||
|
ACCENT_SECONDARY = '#7c5cff' # Purple accent (secondary actions)
|
||||||
|
ACCENT_TERTIARY = '#5ce1e6' # Cyan accent (info/highlights)
|
||||||
|
ACCENT_SUCCESS = '#4ade80' # Green for success states
|
||||||
|
ACCENT_WARNING = '#fbbf24' # Amber for warnings
|
||||||
|
ACCENT_ERROR = '#ff6b6b' # Red for errors
|
||||||
|
|
||||||
|
# Text colors
|
||||||
|
TEXT_PRIMARY = '#ffffff' # Primary text (headings, important)
|
||||||
|
TEXT_SECONDARY = '#e0e0f0' # Secondary text (body text)
|
||||||
|
TEXT_MUTED = '#9090b0' # Muted text (labels, hints)
|
||||||
|
TEXT_DISABLED = '#606080' # Disabled text
|
||||||
|
|
||||||
|
# Rarity colors (enhanced with glow effect potential)
|
||||||
|
RARITY_SSR = '#ffd700' # Gold
|
||||||
|
RARITY_SR = '#c0c0c0' # Silver
|
||||||
|
RARITY_R = '#cd853f' # Bronze (warmer)
|
||||||
|
|
||||||
|
RARITY_COLORS = {
|
||||||
|
'SSR': RARITY_SSR,
|
||||||
|
'SR': RARITY_SR,
|
||||||
|
'R': RARITY_R
|
||||||
|
}
|
||||||
|
|
||||||
|
# Type colors (for card types)
|
||||||
|
TYPE_COLORS = {
|
||||||
|
'Speed': '#3b82f6', # Blue
|
||||||
|
'Stamina': '#f97316', # Orange
|
||||||
|
'Power': '#eab308', # Yellow
|
||||||
|
'Guts': '#ef4444', # Red
|
||||||
|
'Wisdom': '#22c55e', # Green
|
||||||
|
'Friend': '#a855f7', # Purple
|
||||||
|
'Group': '#f59e0b' # Amber
|
||||||
|
}
|
||||||
|
|
||||||
|
# Type icons
|
||||||
|
TYPE_ICONS = {
|
||||||
|
'Speed': '🏃',
|
||||||
|
'Stamina': '💚',
|
||||||
|
'Power': '💪',
|
||||||
|
'Guts': '🔥',
|
||||||
|
'Wisdom': '🧠',
|
||||||
|
'Friend': '💜',
|
||||||
|
'Group': '👥'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# FONTS
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
FONT_FAMILY = 'Segoe UI'
|
||||||
|
FONT_FAMILY_MONO = 'Consolas'
|
||||||
|
|
||||||
|
FONT_TITLE = (FONT_FAMILY, 18, 'bold')
|
||||||
|
FONT_HEADER = (FONT_FAMILY, 14, 'bold')
|
||||||
|
FONT_SUBHEADER = (FONT_FAMILY, 12, 'bold')
|
||||||
|
FONT_BODY = (FONT_FAMILY, 11)
|
||||||
|
FONT_BODY_BOLD = (FONT_FAMILY, 11, 'bold')
|
||||||
|
FONT_SMALL = (FONT_FAMILY, 10)
|
||||||
|
FONT_TINY = (FONT_FAMILY, 9)
|
||||||
|
FONT_MONO = (FONT_FAMILY_MONO, 11)
|
||||||
|
FONT_MONO_SMALL = (FONT_FAMILY_MONO, 10)
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# STYLE CONFIGURATION
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def configure_styles(root: tk.Tk):
|
||||||
|
"""Configure all ttk styles for the application"""
|
||||||
|
style = ttk.Style()
|
||||||
|
|
||||||
|
# Use clam theme as base for better customization
|
||||||
|
style.theme_use('clam')
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
# General Frame and Label styles
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
style.configure('TFrame', background=BG_DARK)
|
||||||
|
style.configure('TLabel', background=BG_DARK, foreground=TEXT_SECONDARY, font=FONT_BODY)
|
||||||
|
style.configure('TLabelframe', background=BG_DARK, foreground=TEXT_SECONDARY)
|
||||||
|
style.configure('TLabelframe.Label', background=BG_DARK, foreground=ACCENT_PRIMARY, font=FONT_SUBHEADER)
|
||||||
|
|
||||||
|
# Header styles
|
||||||
|
style.configure('Title.TLabel', font=FONT_TITLE, foreground=TEXT_PRIMARY, background=BG_DARK)
|
||||||
|
style.configure('Header.TLabel', font=FONT_HEADER, foreground=ACCENT_PRIMARY, background=BG_DARK)
|
||||||
|
style.configure('Subheader.TLabel', font=FONT_SUBHEADER, foreground=TEXT_PRIMARY, background=BG_DARK)
|
||||||
|
style.configure('Subtitle.TLabel', font=FONT_SMALL, foreground=TEXT_MUTED, background=BG_DARK)
|
||||||
|
style.configure('Stats.TLabel', font=FONT_SMALL, foreground=TEXT_SECONDARY, background=BG_MEDIUM, padding=8)
|
||||||
|
style.configure('Accent.TLabel', font=FONT_BODY, foreground=ACCENT_PRIMARY, background=BG_DARK)
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
# Button styles
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
style.configure('TButton',
|
||||||
|
padding=(12, 6),
|
||||||
|
font=FONT_BODY,
|
||||||
|
background=BG_LIGHT,
|
||||||
|
foreground=TEXT_PRIMARY)
|
||||||
|
style.map('TButton',
|
||||||
|
background=[('active', BG_HIGHLIGHT), ('pressed', ACCENT_PRIMARY)],
|
||||||
|
foreground=[('active', TEXT_PRIMARY), ('pressed', TEXT_PRIMARY)])
|
||||||
|
|
||||||
|
style.configure('Accent.TButton',
|
||||||
|
padding=(12, 6),
|
||||||
|
font=FONT_BODY_BOLD,
|
||||||
|
background=ACCENT_PRIMARY,
|
||||||
|
foreground=TEXT_PRIMARY)
|
||||||
|
style.map('Accent.TButton',
|
||||||
|
background=[('active', '#ff8ab5'), ('pressed', '#e55a88')])
|
||||||
|
|
||||||
|
style.configure('Small.TButton',
|
||||||
|
padding=(8, 4),
|
||||||
|
font=FONT_SMALL)
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
# Checkbutton styles
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
style.configure('TCheckbutton',
|
||||||
|
background=BG_DARK,
|
||||||
|
foreground=TEXT_SECONDARY,
|
||||||
|
font=FONT_BODY)
|
||||||
|
style.map('TCheckbutton',
|
||||||
|
background=[('active', BG_DARK)],
|
||||||
|
foreground=[('active', TEXT_PRIMARY)])
|
||||||
|
|
||||||
|
style.configure('Large.TCheckbutton',
|
||||||
|
font=FONT_BODY_BOLD,
|
||||||
|
background=BG_DARK,
|
||||||
|
foreground=TEXT_PRIMARY)
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
# Entry and Combobox styles
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
style.configure('TEntry',
|
||||||
|
fieldbackground=BG_MEDIUM,
|
||||||
|
foreground=TEXT_PRIMARY,
|
||||||
|
insertcolor=TEXT_PRIMARY,
|
||||||
|
padding=6)
|
||||||
|
|
||||||
|
style.configure('TCombobox',
|
||||||
|
fieldbackground=BG_MEDIUM,
|
||||||
|
background=BG_LIGHT,
|
||||||
|
foreground=TEXT_PRIMARY,
|
||||||
|
arrowcolor=TEXT_MUTED,
|
||||||
|
padding=4)
|
||||||
|
style.map('TCombobox',
|
||||||
|
fieldbackground=[('readonly', BG_MEDIUM)],
|
||||||
|
selectbackground=[('readonly', BG_HIGHLIGHT)])
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
# Notebook (Tab) styles
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
style.configure('TNotebook',
|
||||||
|
background=BG_DARK,
|
||||||
|
borderwidth=0)
|
||||||
|
style.configure('TNotebook.Tab',
|
||||||
|
padding=(20, 10),
|
||||||
|
font=FONT_BODY_BOLD,
|
||||||
|
background=BG_MEDIUM,
|
||||||
|
foreground=TEXT_MUTED)
|
||||||
|
style.map('TNotebook.Tab',
|
||||||
|
background=[('selected', BG_LIGHT), ('active', BG_HIGHLIGHT)],
|
||||||
|
foreground=[('selected', ACCENT_PRIMARY), ('active', TEXT_PRIMARY)],
|
||||||
|
expand=[('selected', (0, 0, 0, 2))])
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
# Treeview styles
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
style.configure('Treeview',
|
||||||
|
background=BG_MEDIUM,
|
||||||
|
foreground=TEXT_SECONDARY,
|
||||||
|
fieldbackground=BG_MEDIUM,
|
||||||
|
font=FONT_BODY,
|
||||||
|
rowheight=28)
|
||||||
|
style.configure('Treeview.Heading',
|
||||||
|
font=FONT_BODY_BOLD,
|
||||||
|
background=BG_LIGHT,
|
||||||
|
foreground=TEXT_PRIMARY,
|
||||||
|
padding=6)
|
||||||
|
style.map('Treeview',
|
||||||
|
background=[('selected', ACCENT_PRIMARY)],
|
||||||
|
foreground=[('selected', TEXT_PRIMARY)])
|
||||||
|
style.map('Treeview.Heading',
|
||||||
|
background=[('active', BG_HIGHLIGHT)])
|
||||||
|
|
||||||
|
# Card list with larger rows for thumbnails
|
||||||
|
style.configure('CardList.Treeview',
|
||||||
|
background=BG_MEDIUM,
|
||||||
|
foreground=TEXT_SECONDARY,
|
||||||
|
fieldbackground=BG_MEDIUM,
|
||||||
|
font=FONT_BODY,
|
||||||
|
rowheight=40)
|
||||||
|
|
||||||
|
# Deck list style
|
||||||
|
style.configure('DeckList.Treeview',
|
||||||
|
background=BG_MEDIUM,
|
||||||
|
foreground=TEXT_SECONDARY,
|
||||||
|
fieldbackground=BG_MEDIUM,
|
||||||
|
font=FONT_BODY,
|
||||||
|
rowheight=40)
|
||||||
|
style.map('DeckList.Treeview',
|
||||||
|
background=[('selected', ACCENT_PRIMARY)])
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
# Scale (Slider) styles
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
style.configure('TScale',
|
||||||
|
background=BG_DARK,
|
||||||
|
troughcolor=BG_MEDIUM,
|
||||||
|
sliderthickness=18)
|
||||||
|
style.configure('Horizontal.TScale',
|
||||||
|
background=BG_DARK)
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
# Progressbar styles
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
style.configure('TProgressbar',
|
||||||
|
background=ACCENT_PRIMARY,
|
||||||
|
troughcolor=BG_MEDIUM,
|
||||||
|
borderwidth=0,
|
||||||
|
thickness=8)
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
# Scrollbar styles
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
style.configure('TScrollbar',
|
||||||
|
background=BG_LIGHT,
|
||||||
|
troughcolor=BG_MEDIUM,
|
||||||
|
borderwidth=0,
|
||||||
|
arrowsize=14)
|
||||||
|
style.map('TScrollbar',
|
||||||
|
background=[('active', BG_HIGHLIGHT), ('pressed', ACCENT_PRIMARY)])
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
# PanedWindow styles
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
style.configure('TPanedwindow', background=BG_DARK)
|
||||||
|
|
||||||
|
# Set root background
|
||||||
|
root.configure(bg=BG_DARK)
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# WIDGET HELPER FUNCTIONS
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def create_styled_button(parent, text, command=None, style_type='default', **kwargs):
|
||||||
|
"""Create a styled tk.Button with modern appearance"""
|
||||||
|
bg_colors = {
|
||||||
|
'default': BG_LIGHT,
|
||||||
|
'accent': ACCENT_PRIMARY,
|
||||||
|
'secondary': ACCENT_SECONDARY,
|
||||||
|
'success': ACCENT_SUCCESS,
|
||||||
|
'warning': ACCENT_WARNING,
|
||||||
|
'danger': ACCENT_ERROR
|
||||||
|
}
|
||||||
|
hover_colors = {
|
||||||
|
'default': BG_HIGHLIGHT,
|
||||||
|
'accent': '#ff8ab5',
|
||||||
|
'secondary': '#9580ff',
|
||||||
|
'success': '#6ee7a0',
|
||||||
|
'warning': '#fcd34d',
|
||||||
|
'danger': '#ff8a8a'
|
||||||
|
}
|
||||||
|
|
||||||
|
bg = bg_colors.get(style_type, BG_LIGHT)
|
||||||
|
hover_bg = hover_colors.get(style_type, BG_HIGHLIGHT)
|
||||||
|
|
||||||
|
btn = tk.Button(
|
||||||
|
parent,
|
||||||
|
text=text,
|
||||||
|
command=command,
|
||||||
|
bg=bg,
|
||||||
|
fg=TEXT_PRIMARY,
|
||||||
|
font=FONT_BODY_BOLD if style_type == 'accent' else FONT_BODY,
|
||||||
|
activebackground=hover_bg,
|
||||||
|
activeforeground=TEXT_PRIMARY,
|
||||||
|
bd=0,
|
||||||
|
padx=16,
|
||||||
|
pady=8,
|
||||||
|
cursor='hand2',
|
||||||
|
relief=tk.FLAT,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
def create_styled_text(parent, height=10, **kwargs):
|
||||||
|
"""Create a styled tk.Text widget with modern appearance"""
|
||||||
|
text = tk.Text(
|
||||||
|
parent,
|
||||||
|
bg=BG_MEDIUM,
|
||||||
|
fg=TEXT_SECONDARY,
|
||||||
|
font=FONT_MONO,
|
||||||
|
insertbackground=TEXT_PRIMARY,
|
||||||
|
selectbackground=ACCENT_PRIMARY,
|
||||||
|
selectforeground=TEXT_PRIMARY,
|
||||||
|
relief=tk.FLAT,
|
||||||
|
padx=12,
|
||||||
|
pady=12,
|
||||||
|
height=height,
|
||||||
|
wrap=tk.WORD,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def create_card_frame(parent, **kwargs):
|
||||||
|
"""Create a styled frame that looks like a card"""
|
||||||
|
frame = tk.Frame(
|
||||||
|
parent,
|
||||||
|
bg=BG_MEDIUM,
|
||||||
|
highlightthickness=1,
|
||||||
|
highlightbackground=BG_LIGHT,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
return frame
|
||||||
|
|
||||||
|
|
||||||
|
def get_rarity_color(rarity):
|
||||||
|
"""Get the color for a card rarity"""
|
||||||
|
return RARITY_COLORS.get(rarity, TEXT_SECONDARY)
|
||||||
|
|
||||||
|
|
||||||
|
def get_type_color(card_type):
|
||||||
|
"""Get the color for a card type"""
|
||||||
|
return TYPE_COLORS.get(card_type, TEXT_SECONDARY)
|
||||||
|
|
||||||
|
|
||||||
|
def get_type_icon(card_type):
|
||||||
|
"""Get the emoji icon for a card type"""
|
||||||
|
return TYPE_ICONS.get(card_type, '')
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# TOOLTIPS & HELPERS
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
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:
|
||||||
|
"""
|
||||||
|
Creates a tooltip for a given widget as the mouse hovers above it.
|
||||||
|
"""
|
||||||
|
def __init__(self, widget, text):
|
||||||
|
self.widget = widget
|
||||||
|
self.text = text
|
||||||
|
self.tip_window = None
|
||||||
|
self.id = None
|
||||||
|
self.x = self.y = 0
|
||||||
|
self._id1 = self.widget.bind("<Enter>", self.enter)
|
||||||
|
self._id2 = self.widget.bind("<Leave>", self.leave)
|
||||||
|
self._id3 = 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
|
||||||
|
|
||||||
|
# Creates a toplevel window
|
||||||
|
self.tip_window = tk.Toplevel(self.widget)
|
||||||
|
|
||||||
|
# Leaves only the label and removes the app window
|
||||||
|
self.tip_window.wm_overrideredirect(True)
|
||||||
|
self.tip_window.wm_geometry(f"+{x}+{y}")
|
||||||
|
|
||||||
|
label = tk.Label(
|
||||||
|
self.tip_window,
|
||||||
|
text=self.text,
|
||||||
|
justify=tk.LEFT,
|
||||||
|
background=BG_LIGHT,
|
||||||
|
foreground=TEXT_PRIMARY,
|
||||||
|
relief=tk.SOLID,
|
||||||
|
borderwidth=1,
|
||||||
|
font=FONT_SMALL,
|
||||||
|
padx=10,
|
||||||
|
pady=5
|
||||||
|
)
|
||||||
|
label.pack(ipadx=1)
|
||||||
|
|
||||||
|
def hidetip(self):
|
||||||
|
tw = self.tip_window
|
||||||
|
self.tip_window = None
|
||||||
|
if tw:
|
||||||
|
tw.destroy()
|
||||||
|
|
||||||
|
def create_tooltip(widget, text):
|
||||||
|
"""Create a tooltip for a widget"""
|
||||||
|
return Tooltip(widget, text)
|
||||||
344
gui/update_dialog.py
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
"""
|
||||||
|
Update Dialog for UmamusumeCardManager
|
||||||
|
Provides a modal dialog for the update process.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk, messagebox, scrolledtext
|
||||||
|
import threading
|
||||||
|
import webbrowser
|
||||||
|
from typing import Optional, Callable
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from updater.update_checker import check_for_updates, download_update, apply_update, get_current_version
|
||||||
|
from gui.theme import (
|
||||||
|
BG_DARK, BG_DARKEST, BG_MEDIUM, BG_LIGHT, BG_HIGHLIGHT,
|
||||||
|
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_SUCCESS, ACCENT_ERROR,
|
||||||
|
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
|
||||||
|
FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL,
|
||||||
|
create_styled_button
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateDialog:
|
||||||
|
"""Modal dialog for checking and applying updates."""
|
||||||
|
|
||||||
|
def __init__(self, parent: tk.Tk, on_close_callback: Optional[Callable] = None):
|
||||||
|
self.parent = parent
|
||||||
|
self.on_close_callback = on_close_callback
|
||||||
|
self.update_info = None
|
||||||
|
self.download_thread = None
|
||||||
|
self.is_downloading = False
|
||||||
|
|
||||||
|
# Create the dialog window
|
||||||
|
self.dialog = tk.Toplevel(parent)
|
||||||
|
self.dialog.title("Check for Updates")
|
||||||
|
self.dialog.geometry("520x600")
|
||||||
|
self.dialog.resizable(True, True)
|
||||||
|
self.dialog.minsize(480, 500)
|
||||||
|
self.dialog.transient(parent)
|
||||||
|
self.dialog.grab_set()
|
||||||
|
|
||||||
|
# Center on parent
|
||||||
|
self.center_on_parent()
|
||||||
|
|
||||||
|
self.dialog.configure(bg=BG_DARK)
|
||||||
|
|
||||||
|
# Set up the UI
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
# Start checking for updates
|
||||||
|
self.check_for_updates()
|
||||||
|
|
||||||
|
def center_on_parent(self):
|
||||||
|
"""Center the dialog on the parent window."""
|
||||||
|
self.dialog.update_idletasks()
|
||||||
|
parent_x = self.parent.winfo_x()
|
||||||
|
parent_y = self.parent.winfo_y()
|
||||||
|
parent_w = self.parent.winfo_width()
|
||||||
|
parent_h = self.parent.winfo_height()
|
||||||
|
|
||||||
|
dialog_w = 520
|
||||||
|
dialog_h = 600
|
||||||
|
|
||||||
|
x = parent_x + (parent_w - dialog_w) // 2
|
||||||
|
y = parent_y + (parent_h - dialog_h) // 2
|
||||||
|
|
||||||
|
self.dialog.geometry(f"{dialog_w}x{dialog_h}+{x}+{y}")
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
"""Set up the dialog UI."""
|
||||||
|
# Button frame (Create first to pack at bottom)
|
||||||
|
self.button_frame = tk.Frame(self.dialog, bg=BG_DARK, pady=20, padx=20)
|
||||||
|
self.button_frame.pack(side=tk.BOTTOM, fill=tk.X)
|
||||||
|
|
||||||
|
# Close button
|
||||||
|
self.close_button = create_styled_button(
|
||||||
|
self.button_frame,
|
||||||
|
text="Close",
|
||||||
|
command=self.close,
|
||||||
|
style_type='default'
|
||||||
|
)
|
||||||
|
self.close_button.pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
# Update button (hidden initially)
|
||||||
|
self.update_button = create_styled_button(
|
||||||
|
self.button_frame,
|
||||||
|
text="⬇️ Download & Install",
|
||||||
|
command=self.start_download,
|
||||||
|
style_type='accent'
|
||||||
|
)
|
||||||
|
# We don't pack it yet
|
||||||
|
|
||||||
|
# Main container
|
||||||
|
main_frame = tk.Frame(self.dialog, bg=BG_DARK, padx=25, pady=20)
|
||||||
|
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
self.title_label = tk.Label(
|
||||||
|
main_frame,
|
||||||
|
text="🔄 Checking for Updates...",
|
||||||
|
font=FONT_HEADER,
|
||||||
|
bg=BG_DARK,
|
||||||
|
fg=ACCENT_PRIMARY
|
||||||
|
)
|
||||||
|
self.title_label.pack(pady=(0, 10))
|
||||||
|
|
||||||
|
# Status message
|
||||||
|
self.status_label = tk.Label(
|
||||||
|
main_frame,
|
||||||
|
text="Connecting to GitHub...",
|
||||||
|
font=FONT_BODY,
|
||||||
|
bg=BG_DARK,
|
||||||
|
fg=TEXT_MUTED,
|
||||||
|
wraplength=460
|
||||||
|
)
|
||||||
|
self.status_label.pack(pady=(0, 10))
|
||||||
|
|
||||||
|
# Version info frame
|
||||||
|
self.version_frame = tk.Frame(main_frame, bg=BG_MEDIUM, padx=15, pady=10)
|
||||||
|
self.version_frame.pack(fill=tk.X, pady=(0, 15))
|
||||||
|
|
||||||
|
self.current_version_label = tk.Label(
|
||||||
|
self.version_frame,
|
||||||
|
text=f"Current Version: v{get_current_version()}",
|
||||||
|
font=FONT_BODY,
|
||||||
|
bg=BG_MEDIUM,
|
||||||
|
fg=TEXT_SECONDARY
|
||||||
|
)
|
||||||
|
self.current_version_label.pack(anchor='w')
|
||||||
|
|
||||||
|
self.new_version_label = tk.Label(
|
||||||
|
self.version_frame,
|
||||||
|
text="Latest Version: Checking...",
|
||||||
|
font=FONT_BODY,
|
||||||
|
bg=BG_MEDIUM,
|
||||||
|
fg=TEXT_SECONDARY
|
||||||
|
)
|
||||||
|
self.new_version_label.pack(anchor='w')
|
||||||
|
|
||||||
|
# Release Notes Area
|
||||||
|
self.notes_label = tk.Label(
|
||||||
|
main_frame,
|
||||||
|
text="What's New:",
|
||||||
|
font=FONT_BODY_BOLD,
|
||||||
|
bg=BG_DARK,
|
||||||
|
fg=TEXT_PRIMARY
|
||||||
|
)
|
||||||
|
self.notes_label.pack(anchor='w', pady=(0, 5))
|
||||||
|
|
||||||
|
# Text box for release notes
|
||||||
|
self.notes_text = scrolledtext.ScrolledText(
|
||||||
|
main_frame,
|
||||||
|
height=10,
|
||||||
|
bg=BG_MEDIUM,
|
||||||
|
fg=TEXT_SECONDARY,
|
||||||
|
font=FONT_SMALL,
|
||||||
|
borderwidth=0,
|
||||||
|
highlightthickness=0,
|
||||||
|
padx=10,
|
||||||
|
pady=10
|
||||||
|
)
|
||||||
|
self.notes_text.pack(fill=tk.BOTH, expand=True, pady=(0, 15))
|
||||||
|
self.notes_text.insert(tk.END, "Checking for release notes...")
|
||||||
|
self.notes_text.config(state=tk.DISABLED)
|
||||||
|
|
||||||
|
# Progress bar (hidden initially)
|
||||||
|
self.progress_frame = tk.Frame(main_frame, bg=BG_DARK)
|
||||||
|
self.progress_frame.pack(fill=tk.X, pady=(0, 10))
|
||||||
|
|
||||||
|
self.progress_label = tk.Label(
|
||||||
|
self.progress_frame,
|
||||||
|
text="",
|
||||||
|
font=FONT_SMALL,
|
||||||
|
bg=BG_DARK,
|
||||||
|
fg=TEXT_MUTED
|
||||||
|
)
|
||||||
|
self.progress_label.pack(anchor='w', pady=(0, 5))
|
||||||
|
|
||||||
|
self.progress_bar = ttk.Progressbar(
|
||||||
|
self.progress_frame,
|
||||||
|
mode='indeterminate',
|
||||||
|
length=460
|
||||||
|
)
|
||||||
|
self.progress_bar.pack(fill=tk.X)
|
||||||
|
self.progress_bar.start(10)
|
||||||
|
|
||||||
|
def check_for_updates(self):
|
||||||
|
"""Check for updates in a background thread."""
|
||||||
|
def check():
|
||||||
|
self.update_info = check_for_updates()
|
||||||
|
self.dialog.after(0, self.update_check_complete)
|
||||||
|
|
||||||
|
thread = threading.Thread(target=check, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def update_check_complete(self):
|
||||||
|
"""Called when the update check is complete."""
|
||||||
|
self.progress_bar.stop()
|
||||||
|
self.progress_frame.pack_forget() # Hide progress bar when check is done
|
||||||
|
|
||||||
|
# Enable text box to update it
|
||||||
|
self.notes_text.config(state=tk.NORMAL)
|
||||||
|
self.notes_text.delete(1.0, tk.END)
|
||||||
|
|
||||||
|
if self.update_info:
|
||||||
|
# Update available!
|
||||||
|
self.title_label.config(text="🎉 Update Available!")
|
||||||
|
self.status_label.config(
|
||||||
|
text="A new version is available.",
|
||||||
|
fg=ACCENT_SUCCESS
|
||||||
|
)
|
||||||
|
self.new_version_label.config(
|
||||||
|
text=f"Latest Version: {self.update_info['new_version']}",
|
||||||
|
fg=ACCENT_SUCCESS
|
||||||
|
)
|
||||||
|
|
||||||
|
# Show Release Notes
|
||||||
|
notes = self.update_info.get('release_notes', 'No release notes available.')
|
||||||
|
self.notes_text.insert(tk.END, notes)
|
||||||
|
|
||||||
|
# Show update button
|
||||||
|
self.update_button.pack(side=tk.RIGHT, padx=(0, 10))
|
||||||
|
else:
|
||||||
|
# Up to date or error
|
||||||
|
self.title_label.config(text="✅ You're Up to Date!")
|
||||||
|
self.status_label.config(
|
||||||
|
text=f"You are running the latest version.",
|
||||||
|
fg=TEXT_SECONDARY
|
||||||
|
)
|
||||||
|
self.new_version_label.config(
|
||||||
|
text=f"Latest Version: v{get_current_version()}",
|
||||||
|
fg=ACCENT_SUCCESS
|
||||||
|
)
|
||||||
|
self.notes_text.insert(tk.END, "You are using the latest version of Umamusume Support Card Manager.\n\nEnjoy!")
|
||||||
|
|
||||||
|
self.notes_text.config(state=tk.DISABLED)
|
||||||
|
|
||||||
|
def start_download(self):
|
||||||
|
"""Start downloading the update."""
|
||||||
|
if self.is_downloading or not self.update_info:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.is_downloading = True
|
||||||
|
self.update_button.config(state=tk.DISABLED, text="Downloading...")
|
||||||
|
self.close_button.config(state=tk.DISABLED)
|
||||||
|
|
||||||
|
self.title_label.config(text="⬇️ Downloading Update...")
|
||||||
|
self.status_label.config(text="Please wait...", fg=TEXT_MUTED)
|
||||||
|
|
||||||
|
# Configure progress bar for determinate mode
|
||||||
|
self.progress_frame.pack(fill=tk.X, pady=(0, 10)) # Show progress frame again
|
||||||
|
self.progress_bar.config(mode='determinate', maximum=100)
|
||||||
|
self.progress_bar.pack(fill=tk.X)
|
||||||
|
self.progress_bar['value'] = 0
|
||||||
|
|
||||||
|
def download():
|
||||||
|
def progress_callback(downloaded, total):
|
||||||
|
if total > 0:
|
||||||
|
percent = int((downloaded / total) * 100)
|
||||||
|
mb_downloaded = downloaded / (1024 * 1024)
|
||||||
|
mb_total = total / (1024 * 1024)
|
||||||
|
self.dialog.after(0, lambda: self.update_progress(percent, mb_downloaded, mb_total))
|
||||||
|
|
||||||
|
download_path = download_update(self.update_info['download_url'], progress_callback)
|
||||||
|
self.dialog.after(0, lambda: self.download_complete(download_path))
|
||||||
|
|
||||||
|
self.download_thread = threading.Thread(target=download, daemon=True)
|
||||||
|
self.download_thread.start()
|
||||||
|
|
||||||
|
def update_progress(self, percent: int, downloaded_mb: float, total_mb: float):
|
||||||
|
"""Update the progress bar."""
|
||||||
|
self.progress_bar['value'] = percent
|
||||||
|
self.progress_label.config(text=f"Downloaded: {downloaded_mb:.1f} MB / {total_mb:.1f} MB ({percent}%)")
|
||||||
|
|
||||||
|
def download_complete(self, download_path: Optional[str]):
|
||||||
|
"""Called when the download is complete."""
|
||||||
|
self.is_downloading = False
|
||||||
|
|
||||||
|
if download_path:
|
||||||
|
self.title_label.config(text="✅ Download Complete!")
|
||||||
|
self.status_label.config(
|
||||||
|
text="Update ready to install.",
|
||||||
|
fg=ACCENT_SUCCESS
|
||||||
|
)
|
||||||
|
|
||||||
|
# Change button to install
|
||||||
|
self.update_button.config(
|
||||||
|
state=tk.NORMAL,
|
||||||
|
text="🔄 Install & Restart",
|
||||||
|
command=lambda: self.install_update(download_path)
|
||||||
|
)
|
||||||
|
self.close_button.config(state=tk.NORMAL)
|
||||||
|
else:
|
||||||
|
self.title_label.config(text="❌ Download Failed")
|
||||||
|
self.status_label.config(
|
||||||
|
text="Failed not download update.",
|
||||||
|
fg=ACCENT_ERROR
|
||||||
|
)
|
||||||
|
self.update_button.config(state=tk.NORMAL, text="⬇️ Retry Download")
|
||||||
|
self.close_button.config(state=tk.NORMAL)
|
||||||
|
|
||||||
|
def install_update(self, download_path: str):
|
||||||
|
"""Install the downloaded update."""
|
||||||
|
self.title_label.config(text="🔄 Installing Update...")
|
||||||
|
self.status_label.config(text="Applying update...", fg=TEXT_MUTED)
|
||||||
|
self.update_button.config(state=tk.DISABLED)
|
||||||
|
self.close_button.config(state=tk.DISABLED)
|
||||||
|
|
||||||
|
if apply_update(download_path):
|
||||||
|
# Exit the application - the updater script will restart it
|
||||||
|
self.dialog.after(1000, lambda: self.parent.quit())
|
||||||
|
else:
|
||||||
|
messagebox.showinfo(
|
||||||
|
"Manual Update Required",
|
||||||
|
f"The update was downloaded but cannot be applied automatically.\n\n"
|
||||||
|
f"Downloaded file location:\n{download_path}\n\n"
|
||||||
|
f"Please replace the current executable manually.",
|
||||||
|
parent=self.dialog
|
||||||
|
)
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Close the dialog."""
|
||||||
|
if self.on_close_callback:
|
||||||
|
self.on_close_callback()
|
||||||
|
self.dialog.destroy()
|
||||||
|
|
||||||
|
|
||||||
|
def show_update_dialog(parent: tk.Tk, on_close_callback: Optional[Callable] = None) -> UpdateDialog:
|
||||||
|
"""
|
||||||
|
Show the update dialog.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parent: The parent Tk window
|
||||||
|
on_close_callback: Optional callback when dialog is closed
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The UpdateDialog instance
|
||||||
|
"""
|
||||||
|
return UpdateDialog(parent, on_close_callback)
|
||||||
BIN
images/1000_Chrono Genesis.png
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
images/1001_Calstone Light O.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
images/1002_Durandal.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
images/1003_Dantsu Flame.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
images/1004_Daiichi Ruby.png
Normal file
|
After Width: | Height: | Size: 136 KiB |
BIN
images/1005_Fuji Kiseki.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
images/1006_Eishin Flash.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
images/1007_Tosen Jordan.png
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
images/1008_Curren Bouquetd'or.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
images/1009_Tokai Teio.png
Normal file
|
After Width: | Height: | Size: 123 KiB |
BIN
images/100_Royce and Royce.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
images/1010_Kiyoko Hoshina.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
images/1011_Mihono Bourbon.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
images/1012_Gold Ship.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
images/1013_Fenomeno.png
Normal file
|
After Width: | Height: | Size: 131 KiB |
BIN
images/1014_Orfevre.png
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
images/1015_Inari One.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
images/1016_Air Groove.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
images/1017_Fine Motion.png
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
images/1018_Bubble Gum Fellow.png
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
images/101_Duramente.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
images/102_North Flight.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
images/103_Orfevre.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
images/104_Ryoka Tsurugi.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
images/105_Cheval Grand.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
images/106_Neo Universe.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
images/107_Hishi Miracle.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
images/108_Dantsu Flame.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
images/109_Yayoi Akikawa.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
images/10_Mejiro McQueen.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
images/110_Espoir City.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
images/111_Bubble Gum Fellow.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
images/112_Gentildonna.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
images/113_Rhein Kraft.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
images/114_Cesario.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
images/115_Blast Onepiece.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
images/116_No Reason.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
images/117_Buena Vista.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
images/118_Dream Journey.png
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
images/119_Daring Tact.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
images/11_El Condor Pasa.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
images/120_Daring Heart.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
images/121_Almond Eye.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
images/122_Lucky Lilac.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
images/123_Gran Alegria.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
images/124_Transcend.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
images/125_Curren Bouquetd'or.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
images/126_Air Messiah.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
images/127_Tucker Bryne.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
images/128_Fusaichi Pandora.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
images/129_Win Variation.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
images/12_TM Opera O.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
images/130_Stay Gold.png
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
images/131_Admire Groove.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
images/132_Chrono Genesis.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
images/133_Calstone Light O.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
images/134_Durandal.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
images/135_Sakura Chitose O.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
images/136_Kiyoko Hoshina.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
images/137_Fenomeno.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
images/138_Fuji Kiseki.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
images/139_Daiwa Scarlet.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
images/13_Symboli Rudolf.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
images/140_Hishi Amazon.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
images/141_Air Groove.png
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
images/142_Agnes Digital.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
images/143_Biwa Hayahide.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
images/144_Mayano Top Gun.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
images/145_Manhattan Cafe.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
images/146_Mihono Bourbon.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
images/147_Mejiro Ryan.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
images/148_Yukino Bijin.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
images/149_Agnes Tachyon.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
images/14_Seiun Sky.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
images/150_Eishin Flash.png
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
images/151_Narita Taishin.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
images/152_Marvelous Sunday.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
images/153_Matikanefukukitaru.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
images/154_Meisho Doto.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
images/155_Mejiro Dober.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
images/156_Nice Nature.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
images/157_King Halo.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
images/158_Aoi Kiryuin.png
Normal file
|
After Width: | Height: | Size: 98 KiB |