Removal OF files
2
.gitattributes
vendored
@@ -1,2 +0,0 @@
|
|||||||
# Auto detect text files and perform LF normalization
|
|
||||||
* text=auto
|
|
||||||
38
.gitignore
vendored
@@ -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.
|
|
||||||
@@ -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,
|
|
||||||
)
|
|
||||||
37
build.py
@@ -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()
|
|
||||||
@@ -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}")
|
|
||||||
154
db/db_init.py
@@ -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)
|
|
||||||
702
db/db_queries.py
@@ -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
|
|
||||||
559
gui/card_view.py
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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")
|
|
||||||
@@ -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)
|
|
||||||
@@ -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"
|
|
||||||
)
|
|
||||||
@@ -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()
|
|
||||||
461
gui/theme.py
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
|
Before Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 98 KiB |