Removal OF files

This commit is contained in:
kiyreload27
2025-12-28 17:10:23 +00:00
parent d03f317d3e
commit 72dfc34893
1047 changed files with 0 additions and 4793 deletions

2
.gitattributes vendored
View File

@@ -1,2 +0,0 @@
# Auto detect text files and perform LF normalization
* text=auto

38
.gitignore vendored
View File

@@ -1,38 +0,0 @@
# General
__pycache__/
*.py[cod]
*$py.class
*.swp
*.swo
*.tmp
*.log
.DS_Store
Thumbs.db
# Virtual Environment
venv/
.venv/
env/
.env
# Build and Distribution
build/
dist/
*.spec
# Exception: We want to keep our custom spec file
!UmamusumeCardManager.spec
*.exe
*.zip
*.pkg
# Database
# Ignore the running database, but keep the seed/template
database/*.db
!database/umamusume_seed.db
# VS Code
.vscode/
# Project Specific
# We are allowing .txt and .html now (removing previous blocks)
# If there are specific large generated files, add them here.

View File

@@ -1,38 +0,0 @@
# -*- 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,
)

View File

@@ -1,37 +0,0 @@
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()

View File

@@ -1,20 +0,0 @@
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}")

Binary file not shown.

Binary file not shown.

View File

@@ -1,154 +0,0 @@
"""
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)

View File

@@ -1,702 +0,0 @@
"""
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

View File

@@ -1,559 +0,0 @@
"""
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

View File

@@ -1,547 +0,0 @@
"""
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

View File

@@ -1,23 +0,0 @@
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")

View File

@@ -1,298 +0,0 @@
"""
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)

View File

@@ -1,180 +0,0 @@
"""
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"
)

View File

@@ -1,239 +0,0 @@
"""
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()

View File

@@ -1,461 +0,0 @@
"""
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)

View File

@@ -1,344 +0,0 @@
"""
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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Some files were not shown because too many files have changed in this diff Show More