Readding Files
This commit is contained in:
559
gui/card_view.py
Normal file
559
gui/card_view.py
Normal file
@@ -0,0 +1,559 @@
|
||||
"""
|
||||
Card List View - Browse and search support cards with ownership management
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
import sys
|
||||
import os
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from db.db_queries import get_all_cards, get_card_by_id, get_effects_at_level, set_card_owned, is_card_owned, update_owned_card_level
|
||||
from utils import resolve_image_path
|
||||
from gui.theme import (
|
||||
BG_DARK, BG_MEDIUM, BG_LIGHT, BG_HIGHLIGHT,
|
||||
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_SUCCESS,
|
||||
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
|
||||
FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL, FONT_MONO,
|
||||
RARITY_COLORS, TYPE_COLORS, TYPE_ICONS,
|
||||
create_styled_button, create_styled_text, create_card_frame,
|
||||
get_rarity_color, get_type_color, get_type_icon,
|
||||
EFFECT_DESCRIPTIONS, Tooltip
|
||||
)
|
||||
|
||||
|
||||
class CardListFrame(ttk.Frame):
|
||||
"""Frame containing card list with search/filter, ownership, and details panel"""
|
||||
|
||||
def __init__(self, parent, on_card_selected_callback=None):
|
||||
super().__init__(parent)
|
||||
self.on_card_selected = on_card_selected_callback
|
||||
self.cards = []
|
||||
self.current_card_id = None
|
||||
self.card_image = None # Keep reference to prevent garbage collection
|
||||
self.icon_cache = {} # Cache for list icons
|
||||
|
||||
# Create main layout
|
||||
self.create_widgets()
|
||||
self.load_cards()
|
||||
|
||||
def create_widgets(self):
|
||||
"""Create the card list interface"""
|
||||
# Main horizontal layout
|
||||
main_pane = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
|
||||
main_pane.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Left panel - Card list with filters
|
||||
left_frame = ttk.Frame(main_pane, width=420)
|
||||
main_pane.add(left_frame, weight=1)
|
||||
|
||||
# Right panel - Card details
|
||||
self.details_frame = ttk.Frame(main_pane)
|
||||
main_pane.add(self.details_frame, weight=2)
|
||||
|
||||
# === Left Panel Contents ===
|
||||
|
||||
# Initialize filter variables FIRST (before search trace can trigger filter_cards)
|
||||
self.rarity_var = tk.StringVar(value="All")
|
||||
self.type_var = tk.StringVar(value="All")
|
||||
self.owned_only_var = tk.BooleanVar(value=False)
|
||||
|
||||
# Search bar with modern styling
|
||||
search_frame = tk.Frame(left_frame, bg=BG_DARK)
|
||||
search_frame.pack(fill=tk.X, padx=10, pady=10)
|
||||
|
||||
search_icon = tk.Label(search_frame, text="🔍", font=FONT_BODY, bg=BG_DARK, fg=TEXT_MUTED)
|
||||
search_icon.pack(side=tk.LEFT, padx=(0, 5))
|
||||
|
||||
self.search_var = tk.StringVar()
|
||||
self.search_entry = ttk.Entry(search_frame, textvariable=self.search_var, width=35)
|
||||
self.search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||||
|
||||
# Set placeholder BEFORE adding the trace (so it doesn't trigger filter)
|
||||
self.search_entry.insert(0, "Search cards...")
|
||||
self.search_entry.config(foreground=TEXT_MUTED)
|
||||
self.search_entry.bind('<FocusIn>', self._on_search_focus_in)
|
||||
self.search_entry.bind('<FocusOut>', self._on_search_focus_out)
|
||||
|
||||
# NOW add the trace (after placeholder is set)
|
||||
self.search_var.trace('w', lambda *args: self.filter_cards())
|
||||
|
||||
# Filter dropdowns
|
||||
filter_frame = tk.Frame(left_frame, bg=BG_DARK)
|
||||
filter_frame.pack(fill=tk.X, padx=10, pady=(0, 10))
|
||||
|
||||
# Rarity filter
|
||||
tk.Label(filter_frame, text="Rarity:", font=FONT_SMALL, bg=BG_DARK, fg=TEXT_MUTED).pack(side=tk.LEFT)
|
||||
rarity_combo = ttk.Combobox(filter_frame, textvariable=self.rarity_var,
|
||||
values=["All", "SSR", "SR", "R"], width=7, state='readonly')
|
||||
rarity_combo.pack(side=tk.LEFT, padx=(5, 15))
|
||||
rarity_combo.bind('<<ComboboxSelected>>', lambda e: self.filter_cards())
|
||||
|
||||
# Type filter
|
||||
tk.Label(filter_frame, text="Type:", font=FONT_SMALL, bg=BG_DARK, fg=TEXT_MUTED).pack(side=tk.LEFT)
|
||||
type_combo = ttk.Combobox(filter_frame, textvariable=self.type_var,
|
||||
values=["All", "Speed", "Stamina", "Power", "Guts", "Wisdom", "Friend", "Group"],
|
||||
width=10, state='readonly')
|
||||
type_combo.pack(side=tk.LEFT, padx=5)
|
||||
type_combo.bind('<<ComboboxSelected>>', lambda e: self.filter_cards())
|
||||
|
||||
# Owned only filter
|
||||
owned_check = ttk.Checkbutton(filter_frame, text="Owned Only",
|
||||
variable=self.owned_only_var, command=self.filter_cards)
|
||||
owned_check.pack(side=tk.LEFT, padx=15)
|
||||
|
||||
# Reset Button
|
||||
ttk.Button(filter_frame, text="Reset", command=self.reset_filters,
|
||||
style='Small.TButton', width=7).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# Shortcuts
|
||||
self.bind_all('<Control-f>', lambda e: self.search_entry.focus_set())
|
||||
|
||||
# Card count label
|
||||
self.count_label = tk.Label(left_frame, text="0 cards", font=FONT_SMALL,
|
||||
bg=BG_DARK, fg=ACCENT_PRIMARY)
|
||||
self.count_label.pack(pady=5)
|
||||
|
||||
# Card list (Treeview)
|
||||
list_frame = tk.Frame(left_frame, bg=BG_DARK)
|
||||
list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
|
||||
|
||||
self.tree = ttk.Treeview(list_frame, columns=('owned', 'name', 'rarity', 'type'),
|
||||
show='tree headings', selectmode='browse',
|
||||
style="CardList.Treeview")
|
||||
|
||||
self.tree.heading('#0', text='')
|
||||
self.tree.column('#0', width=45, anchor='center')
|
||||
|
||||
self.tree.heading('owned', text='★', command=lambda: self.sort_column('owned', False))
|
||||
self.tree.heading('name', text='Name', anchor='w', command=lambda: self.sort_column('name', False))
|
||||
self.tree.heading('rarity', text='Rarity', command=lambda: self.sort_column('rarity', False))
|
||||
self.tree.heading('type', text='Type', command=lambda: self.sort_column('type', False))
|
||||
|
||||
self.tree.column('owned', width=30, anchor='center')
|
||||
self.tree.column('name', width=180, minwidth=150)
|
||||
self.tree.column('rarity', width=55, anchor='center')
|
||||
self.tree.column('type', width=90, anchor='center')
|
||||
|
||||
scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.tree.yview)
|
||||
self.tree.configure(yscrollcommand=scrollbar.set)
|
||||
|
||||
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
self.tree.bind('<<TreeviewSelect>>', self.on_select)
|
||||
|
||||
# Tag for owned cards
|
||||
self.tree.tag_configure('owned', background='#1a3a2e')
|
||||
|
||||
# === Right Panel Contents (Details) ===
|
||||
self.create_details_panel()
|
||||
|
||||
def create_details_panel(self):
|
||||
"""Create the card details panel"""
|
||||
# Container with card-like appearance
|
||||
details_container = tk.Frame(self.details_frame, bg=BG_DARK)
|
||||
details_container.pack(fill=tk.BOTH, expand=True, padx=15, pady=10)
|
||||
|
||||
# Image area with card frame
|
||||
image_frame = create_card_frame(details_container, padx=10, pady=10)
|
||||
image_frame.pack(pady=10)
|
||||
|
||||
self.image_label = tk.Label(image_frame, text="", bg=BG_MEDIUM)
|
||||
self.image_label.pack(padx=5, pady=5)
|
||||
|
||||
# Header with card name
|
||||
self.detail_name = tk.Label(details_container, text="Select a card",
|
||||
font=FONT_HEADER, bg=BG_DARK, fg=ACCENT_PRIMARY)
|
||||
self.detail_name.pack(pady=(10, 5))
|
||||
|
||||
self.detail_info = tk.Label(details_container, text="",
|
||||
font=FONT_SMALL, bg=BG_DARK, fg=TEXT_MUTED)
|
||||
self.detail_info.pack()
|
||||
|
||||
# Owned checkbox with emphasis
|
||||
owned_frame = tk.Frame(details_container, bg=BG_DARK)
|
||||
owned_frame.pack(pady=15)
|
||||
|
||||
self.owned_var = tk.BooleanVar(value=False)
|
||||
self.owned_checkbox = ttk.Checkbutton(owned_frame, text="✨ I Own This Card",
|
||||
variable=self.owned_var,
|
||||
command=self.toggle_owned,
|
||||
style='Large.TCheckbutton')
|
||||
self.owned_checkbox.pack(side=tk.LEFT)
|
||||
|
||||
# Level selector with button-based control (no slider)
|
||||
level_frame = tk.Frame(details_container, bg=BG_DARK)
|
||||
level_frame.pack(fill=tk.X, padx=30, pady=10)
|
||||
|
||||
tk.Label(level_frame, text="Card Level:", font=FONT_BODY,
|
||||
bg=BG_DARK, fg=TEXT_SECONDARY).pack(side=tk.LEFT)
|
||||
|
||||
# Level display with increment/decrement
|
||||
level_ctrl = tk.Frame(level_frame, bg=BG_DARK)
|
||||
level_ctrl.pack(side=tk.LEFT, padx=15)
|
||||
|
||||
self.level_var = tk.IntVar(value=50)
|
||||
self.max_level = 50
|
||||
self.valid_levels = [30, 35, 40, 45, 50] # Default SSR
|
||||
|
||||
# Decrement button
|
||||
dec_btn = tk.Button(level_ctrl, text="−", font=FONT_HEADER,
|
||||
bg=BG_LIGHT, fg=TEXT_PRIMARY, bd=0, width=2,
|
||||
activebackground=BG_HIGHLIGHT, cursor='hand2',
|
||||
command=self.decrement_level)
|
||||
dec_btn.pack(side=tk.LEFT)
|
||||
|
||||
self.level_label = tk.Label(level_ctrl, text="50", width=4, font=FONT_HEADER,
|
||||
bg=BG_MEDIUM, fg=ACCENT_PRIMARY, padx=10)
|
||||
self.level_label.pack(side=tk.LEFT, padx=2)
|
||||
|
||||
# Increment button
|
||||
inc_btn = tk.Button(level_ctrl, text="+", font=FONT_HEADER,
|
||||
bg=BG_LIGHT, fg=TEXT_PRIMARY, bd=0, width=2,
|
||||
activebackground=BG_HIGHLIGHT, cursor='hand2',
|
||||
command=self.increment_level)
|
||||
inc_btn.pack(side=tk.LEFT)
|
||||
|
||||
# Quick level buttons container
|
||||
self.level_btn_frame = tk.Frame(level_frame, bg=BG_DARK)
|
||||
self.level_btn_frame.pack(side=tk.LEFT, padx=20)
|
||||
|
||||
self.level_buttons = {}
|
||||
# Initial population
|
||||
self.update_level_buttons('SSR', 50)
|
||||
|
||||
# Effects display header
|
||||
effects_header = tk.Frame(details_container, bg=BG_DARK)
|
||||
effects_header.pack(fill=tk.X, padx=20, pady=(20, 10))
|
||||
|
||||
tk.Label(effects_header, text="📊 Effects at Current Level",
|
||||
font=FONT_SUBHEADER, bg=BG_DARK, fg=TEXT_PRIMARY).pack(side=tk.LEFT)
|
||||
|
||||
# Effects text area with modern styling
|
||||
effects_frame = create_card_frame(details_container)
|
||||
effects_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=(0, 15))
|
||||
|
||||
self.effects_text = create_styled_text(effects_frame, height=10)
|
||||
self.effects_text.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
|
||||
self.effects_text.config(state=tk.DISABLED)
|
||||
|
||||
def load_cards(self):
|
||||
"""Load all cards from database"""
|
||||
self.cards = get_all_cards()
|
||||
self.populate_tree(self.cards)
|
||||
|
||||
def reset_filters(self):
|
||||
"""Reset all filters to default"""
|
||||
self.search_var.set("")
|
||||
self.rarity_var.set("All")
|
||||
self.type_var.set("All")
|
||||
self.owned_only_var.set(False)
|
||||
# Reset placeholder
|
||||
self.search_entry.delete(0, tk.END)
|
||||
self.search_entry.insert(0, "Search cards...")
|
||||
self.search_entry.config(foreground=TEXT_MUTED)
|
||||
self.filter_cards()
|
||||
|
||||
def _on_search_focus_in(self, event):
|
||||
"""Clear placeholder on focus"""
|
||||
if self.search_entry.get() == "Search cards...":
|
||||
self.search_entry.delete(0, tk.END)
|
||||
self.search_entry.config(foreground=TEXT_PRIMARY)
|
||||
|
||||
def _on_search_focus_out(self, event):
|
||||
"""Show placeholder if empty"""
|
||||
if not self.search_entry.get():
|
||||
self.search_entry.insert(0, "Search cards...")
|
||||
self.search_entry.config(foreground=TEXT_MUTED)
|
||||
|
||||
def filter_cards(self):
|
||||
"""Filter cards based on search and dropdown values"""
|
||||
rarity = self.rarity_var.get() if self.rarity_var.get() != "All" else None
|
||||
card_type = self.type_var.get() if self.type_var.get() != "All" else None
|
||||
# Ignore placeholder text
|
||||
search_text = self.search_var.get().strip()
|
||||
search = search_text if search_text and search_text != "Search cards..." else None
|
||||
owned_only = self.owned_only_var.get()
|
||||
|
||||
self.cards = get_all_cards(rarity_filter=rarity, type_filter=card_type,
|
||||
search_term=search, owned_only=owned_only)
|
||||
self.populate_tree(self.cards)
|
||||
|
||||
def sort_column(self, col, reverse):
|
||||
"""Sort treeview by column"""
|
||||
l = [(self.tree.set(k, col), k) for k in self.tree.get_children('')]
|
||||
|
||||
# Custom sort logic
|
||||
if col == 'owned':
|
||||
# Sort by star/empty
|
||||
l.sort(key=lambda t: t[0] if t[0] else "", reverse=reverse)
|
||||
elif col == 'rarity':
|
||||
# Sort by rarity rank (SSR > SR > R)
|
||||
rarity_map = {'SSR': 3, 'SR': 2, 'R': 1}
|
||||
l.sort(key=lambda t: rarity_map.get(t[0], 0), reverse=reverse)
|
||||
else:
|
||||
# Default string sort
|
||||
l.sort(reverse=reverse)
|
||||
|
||||
# Rearrange items
|
||||
for index, (val, k) in enumerate(l):
|
||||
self.tree.move(k, '', index)
|
||||
|
||||
# Reverse sort next time
|
||||
self.tree.heading(col, command=lambda: self.sort_column(col, not reverse))
|
||||
|
||||
def populate_tree(self, cards):
|
||||
"""Populate treeview with cards"""
|
||||
self.tree.delete(*self.tree.get_children())
|
||||
|
||||
for card in cards:
|
||||
card_id, name, rarity, card_type, max_level, image_path, is_owned, owned_level = card
|
||||
type_icon = get_type_icon(card_type)
|
||||
owned_mark = "★" if is_owned else ""
|
||||
tag = 'owned' if is_owned else ''
|
||||
|
||||
# Show level for owned cards
|
||||
display_name = name
|
||||
if is_owned and owned_level:
|
||||
display_name = f"{name} (Lv{owned_level})"
|
||||
|
||||
# Load Icon
|
||||
img = self.icon_cache.get(card_id)
|
||||
if not img:
|
||||
resolved_path = resolve_image_path(image_path)
|
||||
if resolved_path and os.path.exists(resolved_path):
|
||||
try:
|
||||
pil_img = Image.open(resolved_path)
|
||||
pil_img.thumbnail((32, 32), Image.Resampling.LANCZOS)
|
||||
img = ImageTk.PhotoImage(pil_img)
|
||||
self.icon_cache[card_id] = img
|
||||
except:
|
||||
pass
|
||||
|
||||
if img:
|
||||
self.tree.insert('', tk.END, iid=card_id, text='', image=img,
|
||||
values=(owned_mark, display_name, rarity, f"{type_icon} {card_type}"),
|
||||
tags=(tag,))
|
||||
else:
|
||||
self.tree.insert('', tk.END, iid=card_id, text='',
|
||||
values=(owned_mark, display_name, rarity, f"{type_icon} {card_type}"),
|
||||
tags=(tag,))
|
||||
|
||||
self.count_label.config(text=f"✨ {len(cards)} cards")
|
||||
|
||||
def on_select(self, event):
|
||||
"""Handle card selection"""
|
||||
selection = self.tree.selection()
|
||||
if not selection:
|
||||
return
|
||||
|
||||
card_id = int(selection[0])
|
||||
card = get_card_by_id(card_id)
|
||||
|
||||
if card:
|
||||
card_id, name, rarity, card_type, max_level, url, image_path, is_owned, owned_level = card
|
||||
|
||||
# Update owned checkbox
|
||||
self.owned_var.set(bool(is_owned))
|
||||
|
||||
# Load card image if available
|
||||
self.load_card_image(image_path)
|
||||
|
||||
# Use owned level if owned, otherwise max level or default 50
|
||||
initial_level = owned_level if is_owned and owned_level else max_level
|
||||
|
||||
# Update level controls
|
||||
self.max_level = max_level
|
||||
self.update_level_buttons(rarity, max_level)
|
||||
|
||||
# Snap initial level to valid levels
|
||||
if initial_level not in self.valid_levels:
|
||||
# Find closest or default to max
|
||||
initial_level = max_level
|
||||
|
||||
self.level_var.set(initial_level)
|
||||
self.level_label.config(text=str(initial_level))
|
||||
|
||||
# Update details display with colors
|
||||
type_icon = get_type_icon(card_type)
|
||||
type_color = get_type_color(card_type)
|
||||
rarity_color = get_rarity_color(rarity)
|
||||
|
||||
self.detail_name.config(text=f"{type_icon} {name}", fg=ACCENT_PRIMARY)
|
||||
self.detail_info.config(text=f"{rarity} │ {card_type} │ Max Level: {max_level}")
|
||||
|
||||
# Load effects
|
||||
self.current_card_id = card_id
|
||||
self.update_effects_display()
|
||||
|
||||
# Notify parent window
|
||||
if self.on_card_selected:
|
||||
self.on_card_selected(card_id, name)
|
||||
|
||||
def load_card_image(self, image_path):
|
||||
"""Load and display card image"""
|
||||
resolved_path = resolve_image_path(image_path)
|
||||
|
||||
if resolved_path and os.path.exists(resolved_path):
|
||||
try:
|
||||
img = Image.open(resolved_path)
|
||||
img.thumbnail((130, 130)) # Slightly larger
|
||||
self.card_image = ImageTk.PhotoImage(img)
|
||||
self.image_label.config(image=self.card_image)
|
||||
except Exception as e:
|
||||
self.image_label.config(image='', text="[Image not found]")
|
||||
else:
|
||||
self.image_label.config(image='', text="")
|
||||
|
||||
def toggle_owned(self):
|
||||
"""Toggle owned status for current card"""
|
||||
if self.current_card_id:
|
||||
owned = self.owned_var.get()
|
||||
level = int(self.level_var.get())
|
||||
set_card_owned(self.current_card_id, owned, level)
|
||||
self.filter_cards() # Refresh list to update owned markers
|
||||
|
||||
def update_level_buttons(self, rarity, max_level):
|
||||
"""Update quick level buttons based on rarity/max level"""
|
||||
# Determine valid levels
|
||||
if max_level == 50: # SSR
|
||||
self.valid_levels = [30, 35, 40, 45, 50]
|
||||
elif max_level == 45: # SR
|
||||
self.valid_levels = [25, 30, 35, 40, 45]
|
||||
else: # R (max 40)
|
||||
self.valid_levels = [20, 25, 30, 35, 40]
|
||||
|
||||
# Clear existing buttons
|
||||
for widget in self.level_btn_frame.winfo_children():
|
||||
widget.destroy()
|
||||
self.level_buttons = {}
|
||||
|
||||
# Create new buttons
|
||||
for lvl in self.valid_levels:
|
||||
btn = create_styled_button(self.level_btn_frame, text=f"Lv{lvl}",
|
||||
command=lambda l=lvl: self.set_level(l),
|
||||
style_type='default')
|
||||
btn.config(width=5, padx=6, pady=3, font=FONT_SMALL)
|
||||
btn.pack(side=tk.LEFT, padx=2)
|
||||
self.level_buttons[lvl] = btn
|
||||
|
||||
def set_level(self, level):
|
||||
"""Set level from quick button"""
|
||||
self.level_var.set(level)
|
||||
self.level_label.config(text=str(level))
|
||||
self.update_effects_display()
|
||||
|
||||
# Save level if owned
|
||||
if self.current_card_id and self.owned_var.get():
|
||||
update_owned_card_level(self.current_card_id, level)
|
||||
self.update_tree_item_level(self.current_card_id, level)
|
||||
|
||||
def increment_level(self):
|
||||
"""Increase level to next valid step"""
|
||||
current = self.level_var.get()
|
||||
# Find next level in valid_levels
|
||||
for lvl in self.valid_levels:
|
||||
if lvl > current:
|
||||
self.set_level(lvl)
|
||||
return
|
||||
|
||||
def decrement_level(self):
|
||||
"""Decrease level to previous valid step"""
|
||||
current = self.level_var.get()
|
||||
# Find previous level in valid_levels
|
||||
for lvl in reversed(self.valid_levels):
|
||||
if lvl < current:
|
||||
self.set_level(lvl)
|
||||
return
|
||||
|
||||
def update_tree_item_level(self, card_id, level):
|
||||
"""Update visible name in tree without full reload"""
|
||||
if self.tree.exists(card_id):
|
||||
current_values = self.tree.item(card_id, 'values')
|
||||
if current_values:
|
||||
# current_values is a tuple: (owned_mark, name, rarity, type)
|
||||
# We need to strip existing " (LvXX)" from name if present
|
||||
name = current_values[1]
|
||||
base_name = name.split(" (Lv")[0]
|
||||
new_name = f"{base_name} (Lv{level})"
|
||||
|
||||
# Make new values tuple preserving other columns
|
||||
new_values = (current_values[0], new_name, current_values[2], current_values[3])
|
||||
self.tree.item(card_id, values=new_values)
|
||||
|
||||
def update_effects_display(self):
|
||||
"""Update the effects display for current card and level"""
|
||||
if not self.current_card_id:
|
||||
return
|
||||
|
||||
level = int(self.level_var.get())
|
||||
effects = get_effects_at_level(self.current_card_id, level)
|
||||
|
||||
self.effects_text.config(state=tk.NORMAL)
|
||||
self.effects_text.delete('1.0', tk.END)
|
||||
|
||||
# Configure tags for styling
|
||||
self.effects_text.tag_configure('header', font=FONT_SUBHEADER, foreground=ACCENT_PRIMARY)
|
||||
self.effects_text.tag_configure('highlight', foreground=ACCENT_SUCCESS)
|
||||
self.effects_text.tag_configure('effect_name', foreground=TEXT_SECONDARY)
|
||||
self.effects_text.tag_configure('effect_value', foreground=TEXT_PRIMARY)
|
||||
self.effects_text.tag_configure('effect_tooltip', underline=False)
|
||||
|
||||
if effects:
|
||||
self.effects_text.insert(tk.END, f"━━━ Level {level} ━━━\n\n", 'header')
|
||||
for name, value in effects:
|
||||
# Highlight high values
|
||||
prefix = ""
|
||||
if '%' in str(value):
|
||||
try:
|
||||
num = int(str(value).replace('%', '').replace('+', ''))
|
||||
if num >= 20:
|
||||
prefix = "★ "
|
||||
except:
|
||||
pass
|
||||
if prefix:
|
||||
self.effects_text.insert(tk.END, prefix, 'highlight')
|
||||
|
||||
# Insert effect name with tooltip tag
|
||||
tag_name = f"tooltip_{name.replace(' ', '_')}"
|
||||
self.effects_text.insert(tk.END, f"{name}: ", ('effect_name', tag_name))
|
||||
|
||||
# Bind tooltip events
|
||||
self.effects_text.tag_bind(tag_name, "<Enter>", lambda e, n=name: self.show_effect_tooltip(e, n))
|
||||
self.effects_text.tag_bind(tag_name, "<Leave>", self.hide_effect_tooltip)
|
||||
|
||||
self.effects_text.insert(tk.END, f"{value}\n", 'effect_value')
|
||||
else:
|
||||
self.effects_text.insert(tk.END, f"No effects data for Level {level}\n\n")
|
||||
self.effects_text.insert(tk.END, "Available levels: 1, 25, 40, 50\n", 'effect_name')
|
||||
|
||||
self.effects_text.config(state=tk.DISABLED)
|
||||
|
||||
def show_effect_tooltip(self, event, effect_name):
|
||||
"""Show tooltip for effect"""
|
||||
if effect_name in EFFECT_DESCRIPTIONS:
|
||||
text = EFFECT_DESCRIPTIONS[effect_name]
|
||||
x = event.x_root + 15
|
||||
y = event.y_root + 10
|
||||
|
||||
# Close existing if any
|
||||
self.hide_effect_tooltip(None)
|
||||
|
||||
self.tooltip_window = tk.Toplevel(self)
|
||||
self.tooltip_window.wm_overrideredirect(True)
|
||||
self.tooltip_window.wm_geometry(f"+{x}+{y}")
|
||||
|
||||
label = tk.Label(self.tooltip_window, text=text, justify=tk.LEFT,
|
||||
background=BG_LIGHT, foreground=TEXT_PRIMARY,
|
||||
relief=tk.SOLID, borderwidth=1, font=FONT_SMALL,
|
||||
padx=10, pady=5, wraplength=250)
|
||||
label.pack()
|
||||
|
||||
def hide_effect_tooltip(self, event):
|
||||
"""Hide tooltip"""
|
||||
if hasattr(self, 'tooltip_window') and self.tooltip_window:
|
||||
self.tooltip_window.destroy()
|
||||
self.tooltip_window = None
|
||||
547
gui/deck_builder.py
Normal file
547
gui/deck_builder.py
Normal file
@@ -0,0 +1,547 @@
|
||||
"""
|
||||
Deck Builder Frame
|
||||
Build decks with 6 cards and view combined effects with breakdown
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
import sys
|
||||
import os
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from db.db_queries import (
|
||||
get_all_cards, get_all_decks, create_deck, delete_deck,
|
||||
add_card_to_deck, remove_card_from_deck, get_deck_cards,
|
||||
get_effects_at_level
|
||||
)
|
||||
from utils import resolve_image_path
|
||||
from gui.theme import (
|
||||
BG_DARK, BG_DARKEST, BG_MEDIUM, BG_LIGHT, BG_HIGHLIGHT,
|
||||
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_SUCCESS, ACCENT_ERROR,
|
||||
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
|
||||
FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL, FONT_TINY,
|
||||
TYPE_COLORS, get_type_color, get_type_icon,
|
||||
create_styled_button, create_styled_text, create_card_frame
|
||||
)
|
||||
|
||||
|
||||
class CardSlot(tk.Frame):
|
||||
"""Visual component for a single card slot"""
|
||||
def __init__(self, parent, index, remove_callback, level_callback):
|
||||
super().__init__(parent, bg=BG_MEDIUM, highlightthickness=2, highlightbackground=BG_LIGHT)
|
||||
self.index = index
|
||||
self.remove_callback = remove_callback
|
||||
self.level_callback = level_callback
|
||||
self.image_ref = None # Keep reference to prevent GC
|
||||
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
# Configure grid weight
|
||||
self.columnconfigure(1, weight=1)
|
||||
|
||||
# Slot number indicator
|
||||
slot_label = tk.Label(self, text=f"#{self.index + 1}", font=FONT_TINY,
|
||||
bg=BG_LIGHT, fg=TEXT_MUTED, padx=4, pady=2)
|
||||
slot_label.place(x=2, y=2)
|
||||
|
||||
# Image Area (Left)
|
||||
self.image_label = tk.Label(self, bg=BG_MEDIUM, text="📭", fg=TEXT_MUTED,
|
||||
font=('Segoe UI', 20))
|
||||
self.image_label.grid(row=0, column=0, rowspan=3, padx=8, pady=8)
|
||||
|
||||
# Details Area (Right)
|
||||
self.name_label = tk.Label(self, text="Empty Slot", bg=BG_MEDIUM, fg=TEXT_MUTED,
|
||||
font=FONT_BODY_BOLD, anchor='w', wraplength=130)
|
||||
self.name_label.grid(row=0, column=1, sticky='w', padx=8, pady=(10, 0))
|
||||
|
||||
self.meta_label = tk.Label(self, text="", bg=BG_MEDIUM, fg=TEXT_MUTED,
|
||||
font=FONT_SMALL, anchor='w')
|
||||
self.meta_label.grid(row=1, column=1, sticky='w', padx=8)
|
||||
|
||||
# Controls (Bottom Right)
|
||||
ctrl_frame = tk.Frame(self, bg=BG_MEDIUM)
|
||||
ctrl_frame.grid(row=2, column=1, sticky='ew', padx=8, pady=8)
|
||||
|
||||
# Level Selector
|
||||
tk.Label(ctrl_frame, text="Lv:", bg=BG_MEDIUM, fg=TEXT_MUTED,
|
||||
font=FONT_SMALL).pack(side=tk.LEFT)
|
||||
|
||||
self.level_var = tk.StringVar(value="50")
|
||||
self.level_combo = ttk.Combobox(ctrl_frame, textvariable=self.level_var,
|
||||
values=[], width=4, state='readonly')
|
||||
self.level_combo.pack(side=tk.LEFT, padx=4)
|
||||
self.level_combo.bind('<<ComboboxSelected>>', self._on_level_change)
|
||||
|
||||
# Remove Button
|
||||
self.remove_btn = tk.Button(ctrl_frame, text="✕", bg=BG_LIGHT, fg=ACCENT_ERROR,
|
||||
bd=0, font=FONT_BODY_BOLD, width=2,
|
||||
activebackground=ACCENT_ERROR, activeforeground=TEXT_PRIMARY,
|
||||
cursor='hand2',
|
||||
command=lambda: self.remove_callback(self.index))
|
||||
self.remove_btn.pack(side=tk.RIGHT)
|
||||
|
||||
# Hide controls initially
|
||||
self.toggle_controls(False)
|
||||
|
||||
def toggle_controls(self, visible):
|
||||
state = 'normal' if visible else 'disabled'
|
||||
self.level_combo.config(state='readonly' if visible else 'disabled')
|
||||
if not visible:
|
||||
self.remove_btn.pack_forget()
|
||||
else:
|
||||
self.remove_btn.pack(side=tk.RIGHT)
|
||||
|
||||
def set_card(self, card_data):
|
||||
"""Set card data: (id, name, rarity, type, image_path, level)"""
|
||||
if not card_data:
|
||||
self.reset()
|
||||
return
|
||||
|
||||
card_id, name, rarity, card_type, image_path, level = card_data
|
||||
|
||||
# Calculate valid levels based on rarity
|
||||
if rarity == 'SSR':
|
||||
valid_levels = [50, 45, 40, 35, 30]
|
||||
max_lvl = 50
|
||||
elif rarity == 'SR':
|
||||
valid_levels = [45, 40, 35, 30, 25]
|
||||
max_lvl = 45
|
||||
else: # R
|
||||
valid_levels = [40, 35, 30, 25, 20]
|
||||
max_lvl = 40
|
||||
|
||||
self.level_combo['values'] = [str(l) for l in valid_levels]
|
||||
|
||||
# Snap level to valid value if not present (e.g. old data)
|
||||
if level not in valid_levels:
|
||||
level = max_lvl
|
||||
|
||||
# Update styling based on type
|
||||
color = get_type_color(card_type)
|
||||
type_icon = get_type_icon(card_type)
|
||||
|
||||
self.name_label.config(text=name, fg=TEXT_PRIMARY)
|
||||
self.meta_label.config(text=f"{type_icon} {rarity} │ {card_type}", fg=color)
|
||||
self.level_var.set(str(level))
|
||||
|
||||
# Update border color based on rarity
|
||||
rarity_borders = {'SSR': '#ffd700', 'SR': '#c0c0c0', 'R': '#cd853f'}
|
||||
self.config(highlightbackground=rarity_borders.get(rarity, BG_LIGHT))
|
||||
|
||||
# Load Image
|
||||
self._load_image(image_path)
|
||||
|
||||
self.toggle_controls(True)
|
||||
|
||||
def reset(self):
|
||||
self.name_label.config(text="Empty Slot", fg=TEXT_MUTED)
|
||||
self.meta_label.config(text="Click a card to add")
|
||||
self.image_label.config(image='', text="📭", font=('Segoe UI', 20))
|
||||
self.config(highlightbackground=BG_LIGHT)
|
||||
self.image_ref = None
|
||||
self.toggle_controls(False)
|
||||
|
||||
def _load_image(self, path):
|
||||
resolved_path = resolve_image_path(path)
|
||||
if resolved_path and os.path.exists(resolved_path):
|
||||
try:
|
||||
pil_img = Image.open(resolved_path)
|
||||
pil_img.thumbnail((65, 65), Image.Resampling.LANCZOS)
|
||||
self.image_ref = ImageTk.PhotoImage(pil_img)
|
||||
self.image_label.config(image=self.image_ref, text='')
|
||||
except Exception as e:
|
||||
print(f"Failed to load image: {e}")
|
||||
self.image_label.config(image='', text="⚠️")
|
||||
else:
|
||||
self.image_label.config(image='', text="🖼️")
|
||||
|
||||
def _on_level_change(self, event):
|
||||
self.level_callback(self.index, int(self.level_var.get()))
|
||||
|
||||
|
||||
class DeckBuilderFrame(ttk.Frame):
|
||||
"""Deck builder with combined effects breakdown"""
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self.current_deck_id = None
|
||||
self.deck_slots = [None] * 6 # 6 card slots
|
||||
self.setup_ui()
|
||||
self.refresh_decks()
|
||||
|
||||
def setup_ui(self):
|
||||
# Main container with split view
|
||||
main_split = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
|
||||
main_split.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
# === Left Panel: Card Browser ===
|
||||
left_panel = ttk.Frame(main_split)
|
||||
main_split.add(left_panel, weight=1)
|
||||
|
||||
# Header
|
||||
header = tk.Frame(left_panel, bg=BG_DARK)
|
||||
header.pack(fill=tk.X, pady=(0, 10))
|
||||
tk.Label(header, text="📋 Available Cards", font=FONT_SUBHEADER,
|
||||
bg=BG_DARK, fg=TEXT_PRIMARY).pack(side=tk.LEFT)
|
||||
|
||||
# Filters
|
||||
filter_frame = tk.Frame(left_panel, bg=BG_DARK)
|
||||
filter_frame.pack(fill=tk.X, pady=(0, 8))
|
||||
|
||||
# Filters - Initialize vars FIRST
|
||||
self.type_var = tk.StringVar(value="All")
|
||||
self.owned_only_var = tk.BooleanVar(value=False)
|
||||
self.search_var = tk.StringVar()
|
||||
|
||||
# Search Entry
|
||||
self.search_entry = ttk.Entry(filter_frame, textvariable=self.search_var, width=18)
|
||||
self.search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 8))
|
||||
|
||||
# Placeholder behavior (before trace)
|
||||
self.search_entry.insert(0, "Search...")
|
||||
self.search_entry.config(foreground=TEXT_MUTED)
|
||||
self.search_entry.bind('<FocusIn>', self._on_search_focus_in)
|
||||
self.search_entry.bind('<FocusOut>', self._on_search_focus_out)
|
||||
|
||||
# Add trace AFTER placeholder is set
|
||||
self.search_var.trace('w', lambda *args: self.filter_cards())
|
||||
|
||||
types = ["All", "Speed", "Stamina", "Power", "Guts", "Wisdom", "Friend", "Group"]
|
||||
type_combo = ttk.Combobox(filter_frame, textvariable=self.type_var,
|
||||
values=types, width=9, state='readonly')
|
||||
type_combo.pack(side=tk.LEFT)
|
||||
type_combo.bind('<<ComboboxSelected>>', lambda e: self.filter_cards())
|
||||
|
||||
ttk.Checkbutton(filter_frame, text="Owned", variable=self.owned_only_var,
|
||||
command=self.filter_cards).pack(side=tk.LEFT, padx=8)
|
||||
|
||||
# Card List
|
||||
list_frame = tk.Frame(left_panel, bg=BG_DARK)
|
||||
list_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
self.card_tree = ttk.Treeview(list_frame, columns=('name', 'rarity', 'type'),
|
||||
show='tree headings', style="DeckList.Treeview")
|
||||
self.card_tree.heading('#0', text='')
|
||||
self.card_tree.column('#0', width=45, anchor='center')
|
||||
|
||||
self.card_tree.heading('name', text='Name')
|
||||
self.card_tree.heading('rarity', text='Rarity')
|
||||
self.card_tree.heading('type', text='Type')
|
||||
self.card_tree.column('name', width=130)
|
||||
self.card_tree.column('rarity', width=45, anchor='center')
|
||||
self.card_tree.column('type', width=65, anchor='center')
|
||||
|
||||
scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.card_tree.yview)
|
||||
self.card_tree.configure(yscrollcommand=scrollbar.set)
|
||||
|
||||
self.card_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
# Double-click to add
|
||||
self.card_tree.bind('<Double-1>', lambda e: self.add_selected_to_deck())
|
||||
|
||||
# Add Button
|
||||
add_btn = create_styled_button(left_panel, text="➕ Add to Deck",
|
||||
command=self.add_selected_to_deck,
|
||||
style_type='accent')
|
||||
add_btn.pack(fill=tk.X, pady=10)
|
||||
|
||||
# === Right Panel: Deck & Stats ===
|
||||
right_panel = ttk.Frame(main_split)
|
||||
main_split.add(right_panel, weight=2)
|
||||
|
||||
# Deck Controls
|
||||
deck_ctrl = tk.Frame(right_panel, bg=BG_DARK)
|
||||
deck_ctrl.pack(fill=tk.X, pady=(0, 15))
|
||||
|
||||
tk.Label(deck_ctrl, text="🎴 Current Deck:", font=FONT_BODY,
|
||||
bg=BG_DARK, fg=TEXT_SECONDARY).pack(side=tk.LEFT)
|
||||
self.deck_combo = ttk.Combobox(deck_ctrl, width=25, state='readonly')
|
||||
self.deck_combo.pack(side=tk.LEFT, padx=10)
|
||||
self.deck_combo.bind('<<ComboboxSelected>>', self.on_deck_selected)
|
||||
|
||||
ttk.Button(deck_ctrl, text="+ New", command=self.create_new_deck,
|
||||
style='Small.TButton').pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(deck_ctrl, text="🗑️ Delete", command=self.delete_current_deck,
|
||||
style='Small.TButton').pack(side=tk.LEFT)
|
||||
|
||||
# Card count indicator
|
||||
self.deck_count_label = tk.Label(deck_ctrl, text="0/6 cards",
|
||||
font=FONT_SMALL, bg=BG_DARK, fg=ACCENT_PRIMARY)
|
||||
self.deck_count_label.pack(side=tk.LEFT, padx=15)
|
||||
|
||||
# Deck Grid (3x2)
|
||||
self.slots_frame = tk.Frame(right_panel, bg=BG_DARK)
|
||||
self.slots_frame.pack(fill=tk.X)
|
||||
|
||||
self.card_slots = []
|
||||
for i in range(6):
|
||||
slot = CardSlot(self.slots_frame, i, self.remove_from_slot, self.on_slot_level_changed)
|
||||
r, c = divmod(i, 3)
|
||||
slot.grid(row=r, column=c, padx=6, pady=6, sticky='nsew')
|
||||
self.slots_frame.columnconfigure(c, weight=1)
|
||||
self.card_slots.append(slot)
|
||||
|
||||
# Stats / Effects Area
|
||||
effects_header = tk.Frame(right_panel, bg=BG_DARK)
|
||||
effects_header.pack(fill=tk.X, pady=(20, 10))
|
||||
tk.Label(effects_header, text="📊 Combined Effects Breakdown",
|
||||
font=FONT_SUBHEADER, bg=BG_DARK, fg=TEXT_PRIMARY).pack(side=tk.LEFT)
|
||||
|
||||
effects_frame = create_card_frame(right_panel)
|
||||
effects_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
self.effects_tree = ttk.Treeview(effects_frame,
|
||||
columns=('effect', 'total', 'c1', 'c2', 'c3', 'c4', 'c5', 'c6'),
|
||||
show='headings', height=8)
|
||||
|
||||
self.effects_tree.heading('effect', text='Effect')
|
||||
self.effects_tree.heading('total', text='TOTAL')
|
||||
self.effects_tree.column('effect', width=140)
|
||||
self.effects_tree.column('total', width=60, anchor='center')
|
||||
|
||||
for i in range(1, 7):
|
||||
self.effects_tree.heading(f'c{i}', text=f'#{i}')
|
||||
self.effects_tree.column(f'c{i}', width=45, anchor='center')
|
||||
|
||||
vsb = ttk.Scrollbar(effects_frame, orient=tk.VERTICAL, command=self.effects_tree.yview)
|
||||
self.effects_tree.configure(yscrollcommand=vsb.set)
|
||||
|
||||
self.effects_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=2, pady=2)
|
||||
vsb.pack(side=tk.RIGHT, fill=tk.Y, pady=2)
|
||||
|
||||
# Unique Effects Area
|
||||
unique_header = tk.Frame(right_panel, bg=BG_DARK)
|
||||
unique_header.pack(fill=tk.X, pady=(15, 8))
|
||||
tk.Label(unique_header, text="✨ Unique Effects", font=FONT_BODY_BOLD,
|
||||
bg=BG_DARK, fg=ACCENT_SECONDARY).pack(side=tk.LEFT)
|
||||
|
||||
unique_frame = create_card_frame(right_panel)
|
||||
unique_frame.pack(fill=tk.X)
|
||||
|
||||
self.unique_text = create_styled_text(unique_frame, height=5)
|
||||
self.unique_text.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
|
||||
self.unique_text.config(state=tk.DISABLED)
|
||||
|
||||
self.icon_cache = {}
|
||||
self.filter_cards()
|
||||
|
||||
|
||||
# Helper methods for placeholder
|
||||
def _on_search_focus_in(self, event):
|
||||
"""Clear placeholder on focus"""
|
||||
if self.search_entry.get() == "Search...":
|
||||
self.search_entry.delete(0, tk.END)
|
||||
self.search_entry.config(foreground=TEXT_PRIMARY)
|
||||
|
||||
def _on_search_focus_out(self, event):
|
||||
"""Show placeholder if empty"""
|
||||
if not self.search_entry.get():
|
||||
self.search_entry.insert(0, "Search...")
|
||||
self.search_entry.config(foreground=TEXT_MUTED)
|
||||
|
||||
# --- Logic Methods ---
|
||||
|
||||
def filter_cards(self):
|
||||
for item in self.card_tree.get_children():
|
||||
self.card_tree.delete(item)
|
||||
|
||||
type_filter = self.type_var.get() if self.type_var.get() != "All" else None
|
||||
|
||||
# Ignore placeholder
|
||||
search_text = self.search_var.get()
|
||||
search = search_text if search_text and search_text != "Search..." else None
|
||||
|
||||
owned_only = self.owned_only_var.get()
|
||||
|
||||
cards = get_all_cards(type_filter=type_filter, search_term=search, owned_only=owned_only)
|
||||
|
||||
for card in cards:
|
||||
card_id, name, rarity, card_type, max_level, image_path, is_owned, owned_level = card
|
||||
|
||||
# Load Icon
|
||||
img = self.icon_cache.get(card_id)
|
||||
resolved_path = resolve_image_path(image_path)
|
||||
|
||||
if not img and resolved_path and os.path.exists(resolved_path):
|
||||
try:
|
||||
pil_img = Image.open(resolved_path)
|
||||
pil_img.thumbnail((32, 32), Image.Resampling.LANCZOS)
|
||||
img = ImageTk.PhotoImage(pil_img)
|
||||
self.icon_cache[card_id] = img
|
||||
except:
|
||||
pass
|
||||
|
||||
type_icon = get_type_icon(card_type)
|
||||
if img:
|
||||
self.card_tree.insert('', tk.END, text='', image=img,
|
||||
values=(name, rarity, f"{type_icon}"), iid=str(card_id))
|
||||
else:
|
||||
self.card_tree.insert('', tk.END, text='?',
|
||||
values=(name, rarity, f"{type_icon}"), iid=str(card_id))
|
||||
|
||||
def refresh_decks(self):
|
||||
decks = get_all_decks()
|
||||
self.deck_combo['values'] = [f"{d[0]}: {d[1]}" for d in decks]
|
||||
if decks and not self.current_deck_id:
|
||||
self.deck_combo.current(0)
|
||||
self.on_deck_selected(None)
|
||||
|
||||
def on_deck_selected(self, event):
|
||||
selection = self.deck_combo.get()
|
||||
if selection:
|
||||
self.current_deck_id = int(selection.split(':')[0])
|
||||
self.load_deck()
|
||||
|
||||
def load_deck(self):
|
||||
if not self.current_deck_id:
|
||||
return
|
||||
|
||||
# Reset visual slots
|
||||
for s in self.card_slots:
|
||||
s.reset()
|
||||
self.deck_slots = [None] * 6
|
||||
|
||||
# Load from DB
|
||||
deck_cards = get_deck_cards(self.current_deck_id)
|
||||
|
||||
for card in deck_cards:
|
||||
slot_pos, level, card_id, name, rarity, card_type, image_path = card
|
||||
if 0 <= slot_pos < 6:
|
||||
self.deck_slots[slot_pos] = card_id
|
||||
self.card_slots[slot_pos].set_card((card_id, name, rarity, card_type, image_path, level))
|
||||
|
||||
self.update_deck_count()
|
||||
self.update_effects_breakdown()
|
||||
|
||||
def create_new_deck(self):
|
||||
name = tk.simpledialog.askstring("New Deck", "Enter deck name:")
|
||||
if name:
|
||||
deck_id = create_deck(name)
|
||||
self.current_deck_id = deck_id
|
||||
self.refresh_decks()
|
||||
self.deck_combo.set(f"{deck_id}: {name}")
|
||||
self.load_deck()
|
||||
|
||||
def delete_current_deck(self):
|
||||
if self.current_deck_id:
|
||||
if messagebox.askyesno("Delete Deck", "Are you sure you want to delete this deck?"):
|
||||
delete_deck(self.current_deck_id)
|
||||
self.current_deck_id = None
|
||||
self.deck_combo.set('')
|
||||
self.refresh_decks()
|
||||
self.load_deck()
|
||||
|
||||
def add_selected_to_deck(self):
|
||||
if not self.current_deck_id:
|
||||
messagebox.showwarning("No Deck", "Select or create a deck first.")
|
||||
return
|
||||
|
||||
selection = self.card_tree.selection()
|
||||
if not selection:
|
||||
return
|
||||
card_id = int(selection[0])
|
||||
|
||||
# Check for duplicates
|
||||
if card_id in self.deck_slots:
|
||||
messagebox.showinfo("Duplicate Card", "This card is already in the deck.")
|
||||
return
|
||||
|
||||
# Find empty slot
|
||||
for i in range(6):
|
||||
if self.deck_slots[i] is None:
|
||||
add_card_to_deck(self.current_deck_id, card_id, i, 50)
|
||||
self.load_deck()
|
||||
return
|
||||
|
||||
messagebox.showinfo("Deck Full", "Remove a card first to add a new one.")
|
||||
|
||||
def remove_from_slot(self, index):
|
||||
if self.current_deck_id and self.deck_slots[index]:
|
||||
remove_card_from_deck(self.current_deck_id, index)
|
||||
self.deck_slots[index] = None
|
||||
self.card_slots[index].reset()
|
||||
self.update_deck_count()
|
||||
self.update_effects_breakdown()
|
||||
|
||||
def update_deck_count(self):
|
||||
"""Update the X/6 cards display"""
|
||||
count = sum(1 for slot in self.deck_slots if slot is not None)
|
||||
self.deck_count_label.config(text=f"{count}/6 cards")
|
||||
|
||||
def on_slot_level_changed(self, index, new_level):
|
||||
if self.current_deck_id and self.deck_slots[index]:
|
||||
card_id = self.deck_slots[index]
|
||||
add_card_to_deck(self.current_deck_id, card_id, index, new_level)
|
||||
self.update_effects_breakdown()
|
||||
|
||||
def update_effects_breakdown(self):
|
||||
for item in self.effects_tree.get_children():
|
||||
self.effects_tree.delete(item)
|
||||
|
||||
# Clear Unique Text
|
||||
self.unique_text.config(state=tk.NORMAL)
|
||||
self.unique_text.delete('1.0', tk.END)
|
||||
|
||||
if not self.current_deck_id:
|
||||
self.unique_text.insert(tk.END, "No deck selected")
|
||||
self.unique_text.config(state=tk.DISABLED)
|
||||
return
|
||||
|
||||
# Prepare data for calculation
|
||||
card_info = []
|
||||
for i in range(6):
|
||||
if self.deck_slots[i]:
|
||||
level = int(self.card_slots[i].level_var.get())
|
||||
card_info.append((self.deck_slots[i], level))
|
||||
else:
|
||||
card_info.append(None)
|
||||
|
||||
# Gather effects
|
||||
all_effects = {}
|
||||
unique_effects_list = []
|
||||
|
||||
for i, info in enumerate(card_info):
|
||||
if info:
|
||||
card_id, level = info
|
||||
card_name = self.card_slots[i].name_label.cget("text")
|
||||
|
||||
effects = get_effects_at_level(card_id, level)
|
||||
for name, value in effects:
|
||||
if name == "Unique Effect":
|
||||
unique_effects_list.append(f"• {card_name}: {value}")
|
||||
continue
|
||||
|
||||
if name not in all_effects:
|
||||
all_effects[name] = [''] * 6
|
||||
all_effects[name][i] = value
|
||||
|
||||
# Configure tags
|
||||
self.unique_text.tag_configure('card_name', foreground=ACCENT_PRIMARY)
|
||||
|
||||
# Fill Unique Effects
|
||||
if unique_effects_list:
|
||||
self.unique_text.insert(tk.END, "\n".join(unique_effects_list))
|
||||
else:
|
||||
self.unique_text.insert(tk.END, "No unique effects in this deck", 'card_name')
|
||||
self.unique_text.config(state=tk.DISABLED)
|
||||
|
||||
# Sum totals
|
||||
for effect_name, values in sorted(all_effects.items()):
|
||||
total = 0
|
||||
is_percent = False
|
||||
for v in values:
|
||||
if v:
|
||||
if '%' in str(v): is_percent = True
|
||||
try:
|
||||
total += float(str(v).replace('%','').replace('+',''))
|
||||
except: pass
|
||||
|
||||
total_str = f"{total:.0f}%" if is_percent else (f"+{total:.0f}" if total > 0 else str(int(total)))
|
||||
row_vals = [effect_name, total_str] + values
|
||||
self.effects_tree.insert('', tk.END, values=row_vals)
|
||||
|
||||
import tkinter.simpledialog
|
||||
23
gui/deck_view.py
Normal file
23
gui/deck_view.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import tkinter as tk
|
||||
from db.db_queries import get_deck_bonus
|
||||
|
||||
class DeckView(tk.Toplevel):
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self.title("Deck Builder")
|
||||
self.geometry("500x400")
|
||||
|
||||
self.deck_id = 1 # Default deck
|
||||
|
||||
tk.Button(self, text="Calculate Deck Bonuses", command=self.calculate).pack(pady=10)
|
||||
self.output = tk.Text(self, height=20)
|
||||
self.output.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
def calculate(self):
|
||||
self.output.delete("1.0", tk.END)
|
||||
bonuses = get_deck_bonus(self.deck_id)
|
||||
if not bonuses:
|
||||
self.output.insert(tk.END, "No bonuses found for this deck.\n")
|
||||
return
|
||||
for bonus, total in bonuses:
|
||||
self.output.insert(tk.END, f"{bonus}: +{total}\n")
|
||||
298
gui/effects_view.py
Normal file
298
gui/effects_view.py
Normal file
@@ -0,0 +1,298 @@
|
||||
"""
|
||||
Effects View - Display support effects at all levels with interactive slider
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from db.db_queries import get_all_effects, get_effects_at_level, get_unique_effect_names, get_card_by_id
|
||||
from gui.theme import (
|
||||
BG_DARK, BG_MEDIUM, BG_LIGHT, BG_HIGHLIGHT,
|
||||
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_SUCCESS, ACCENT_TERTIARY,
|
||||
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
|
||||
FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL, FONT_MONO,
|
||||
create_styled_button, create_styled_text, create_card_frame,
|
||||
EFFECT_DESCRIPTIONS
|
||||
)
|
||||
|
||||
|
||||
class EffectsFrame(ttk.Frame):
|
||||
"""Frame for viewing support effects at different levels"""
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self.current_card_id = None
|
||||
self.current_card_name = None
|
||||
self.max_level = 50
|
||||
|
||||
self.create_widgets()
|
||||
|
||||
def create_widgets(self):
|
||||
"""Create the effects view interface"""
|
||||
# Header
|
||||
header_frame = tk.Frame(self, bg=BG_DARK)
|
||||
header_frame.pack(fill=tk.X, padx=20, pady=15)
|
||||
|
||||
self.card_label = tk.Label(header_frame, text="📊 Select a card from the Card List tab",
|
||||
font=FONT_HEADER, bg=BG_DARK, fg=ACCENT_PRIMARY)
|
||||
self.card_label.pack(side=tk.LEFT)
|
||||
|
||||
# Legend Button
|
||||
legend_btn = create_styled_button(header_frame, text="❓ Legend",
|
||||
command=self.show_legend, style_type='default')
|
||||
legend_btn.config(font=FONT_SMALL, padx=10, pady=4)
|
||||
legend_btn.pack(side=tk.RIGHT)
|
||||
|
||||
# Level control frame
|
||||
control_frame = tk.Frame(self, bg=BG_MEDIUM, padx=15, pady=12)
|
||||
control_frame.pack(fill=tk.X, padx=20)
|
||||
|
||||
# Level label
|
||||
tk.Label(control_frame, text="Level:", font=FONT_BODY,
|
||||
bg=BG_MEDIUM, fg=TEXT_SECONDARY).pack(side=tk.LEFT)
|
||||
|
||||
# Level display with increment/decrement buttons
|
||||
level_ctrl = tk.Frame(control_frame, bg=BG_MEDIUM)
|
||||
level_ctrl.pack(side=tk.LEFT, padx=15)
|
||||
|
||||
# Decrement button
|
||||
dec_btn = tk.Button(level_ctrl, text="−", font=FONT_HEADER,
|
||||
bg=BG_LIGHT, fg=TEXT_PRIMARY, bd=0, width=2,
|
||||
activebackground=BG_HIGHLIGHT, cursor='hand2',
|
||||
command=self.decrement_level)
|
||||
dec_btn.pack(side=tk.LEFT)
|
||||
|
||||
self.level_var = tk.IntVar(value=50)
|
||||
self.level_display = tk.Label(level_ctrl, text="50", width=4, font=FONT_HEADER,
|
||||
bg=BG_LIGHT, fg=ACCENT_PRIMARY, padx=10)
|
||||
self.level_display.pack(side=tk.LEFT, padx=2)
|
||||
|
||||
# Increment button
|
||||
inc_btn = tk.Button(level_ctrl, text="+", font=FONT_HEADER,
|
||||
bg=BG_LIGHT, fg=TEXT_PRIMARY, bd=0, width=2,
|
||||
activebackground=BG_HIGHLIGHT, cursor='hand2',
|
||||
command=self.increment_level)
|
||||
inc_btn.pack(side=tk.LEFT)
|
||||
|
||||
# Quick level buttons
|
||||
button_frame = tk.Frame(control_frame, bg=BG_MEDIUM)
|
||||
button_frame.pack(side=tk.LEFT, padx=25)
|
||||
|
||||
quick_levels = [1, 25, 40, 50]
|
||||
for lvl in quick_levels:
|
||||
btn = create_styled_button(button_frame, text=f"Lv{lvl}",
|
||||
command=lambda l=lvl: self.set_level(l),
|
||||
style_type='default')
|
||||
btn.config(width=5, font=FONT_SMALL, padx=6, pady=3)
|
||||
btn.pack(side=tk.LEFT, padx=3)
|
||||
|
||||
# Main content area
|
||||
content_frame = tk.Frame(self, bg=BG_DARK)
|
||||
content_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=15)
|
||||
|
||||
# Left: Current level effects
|
||||
left_frame = ttk.LabelFrame(content_frame, text=" Current Level Effects ", padding=12)
|
||||
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 10))
|
||||
|
||||
self.current_effects = create_styled_text(left_frame, height=15)
|
||||
self.current_effects.pack(fill=tk.BOTH, expand=True)
|
||||
self.current_effects.config(state=tk.DISABLED)
|
||||
|
||||
# Right: Effect progression table
|
||||
right_frame = ttk.LabelFrame(content_frame, text=" Effect Progression ", padding=12)
|
||||
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
|
||||
|
||||
# Treeview for effect table
|
||||
columns = ('effect', 'lv1', 'lv25', 'lv40', 'lv50')
|
||||
self.effects_tree = ttk.Treeview(right_frame, columns=columns, show='headings', height=12)
|
||||
|
||||
self.effects_tree.heading('effect', text='Effect', anchor='w')
|
||||
self.effects_tree.column('effect', width=140, minwidth=120)
|
||||
|
||||
for col in columns[1:]:
|
||||
level = col.replace('lv', 'Lv ')
|
||||
self.effects_tree.heading(col, text=level)
|
||||
self.effects_tree.column(col, width=65, anchor='center')
|
||||
|
||||
scrollbar = ttk.Scrollbar(right_frame, orient=tk.VERTICAL, command=self.effects_tree.yview)
|
||||
self.effects_tree.configure(yscrollcommand=scrollbar.set)
|
||||
|
||||
self.effects_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
def show_legend(self):
|
||||
"""Show effect explanations"""
|
||||
legend = {
|
||||
"Friendship Bonus": "Increases stats gained when training with this support card during Friendship Training (orange aura).",
|
||||
"Motivation Bonus": "Increases stats gained based on your Uma's motivation level.",
|
||||
"Specialty Rate": "Increases the chance of this card appearing in its specialty training.",
|
||||
"Training Bonus": "Flat percentage increase to stats gained in training where this card is present.",
|
||||
"Initial Bond": "Starting gauge value for this card.",
|
||||
"Race Bonus": "Increases stats gained from racing.",
|
||||
"Fan Count Bonus": "Increases fans gained from racing.",
|
||||
"Skill Pt Bonus": "Bonus skill points gained when training with this card.",
|
||||
"Hint Lv": "Starting level of skills taught by this card's hints.",
|
||||
"Hint Rate": "Increases chance of getting a hint event."
|
||||
}
|
||||
|
||||
text = "📖 Effect Explanations:\n\n"
|
||||
for name, desc in legend.items():
|
||||
text += f"• {name}:\n {desc}\n\n"
|
||||
|
||||
messagebox.showinfo("Effect Legend", text)
|
||||
|
||||
def set_card(self, card_id):
|
||||
"""Load a card's effects"""
|
||||
self.current_card_id = card_id
|
||||
|
||||
# Get card info for max level
|
||||
card = get_card_by_id(card_id)
|
||||
if card:
|
||||
self.current_card_name = card[1]
|
||||
self.max_level = card[4]
|
||||
if self.level_var.get() > self.max_level:
|
||||
self.level_var.set(self.max_level)
|
||||
self.level_display.config(text=str(self.max_level))
|
||||
|
||||
self.card_label.config(text=f"📊 {self.current_card_name}")
|
||||
|
||||
# Update displays
|
||||
self.update_current_effects()
|
||||
self.update_progression_table()
|
||||
|
||||
def set_level(self, level):
|
||||
"""Set level from quick button"""
|
||||
if level <= self.max_level:
|
||||
self.level_var.set(level)
|
||||
self.level_display.config(text=str(level))
|
||||
self.update_current_effects()
|
||||
|
||||
def increment_level(self):
|
||||
"""Increase level by 1"""
|
||||
current = self.level_var.get()
|
||||
if current < self.max_level:
|
||||
self.set_level(current + 1)
|
||||
|
||||
def decrement_level(self):
|
||||
"""Decrease level by 1"""
|
||||
current = self.level_var.get()
|
||||
if current > 1:
|
||||
self.set_level(current - 1)
|
||||
|
||||
def update_current_effects(self):
|
||||
"""Update the current level effects display"""
|
||||
self.current_effects.config(state=tk.NORMAL)
|
||||
self.current_effects.delete('1.0', tk.END)
|
||||
|
||||
# Configure tags
|
||||
self.current_effects.tag_configure('header', font=FONT_SUBHEADER, foreground=ACCENT_PRIMARY)
|
||||
self.current_effects.tag_configure('highlight', foreground=ACCENT_SUCCESS)
|
||||
self.current_effects.tag_configure('effect_name', foreground=TEXT_SECONDARY)
|
||||
self.current_effects.tag_configure('effect_value', foreground=TEXT_PRIMARY, font=FONT_BODY_BOLD)
|
||||
self.current_effects.tag_configure('effect_tooltip', underline=False)
|
||||
|
||||
if not self.current_card_id:
|
||||
self.current_effects.insert(tk.END, "No card selected\n\n", 'effect_name')
|
||||
self.current_effects.insert(tk.END, "Select a card from the Card List tab to view its effects.", 'effect_name')
|
||||
self.current_effects.config(state=tk.DISABLED)
|
||||
return
|
||||
|
||||
level = self.level_var.get()
|
||||
effects = get_effects_at_level(self.current_card_id, level)
|
||||
|
||||
self.current_effects.insert(tk.END, f"━━━ Level {level} ━━━\n\n", 'header')
|
||||
|
||||
if effects:
|
||||
for name, value in effects:
|
||||
# Highlight high values
|
||||
prefix = ""
|
||||
if '%' in str(value):
|
||||
try:
|
||||
num = int(str(value).replace('%', '').replace('+', ''))
|
||||
if num >= 20:
|
||||
prefix = "★ "
|
||||
except:
|
||||
pass
|
||||
|
||||
if prefix:
|
||||
self.current_effects.insert(tk.END, prefix, 'highlight')
|
||||
|
||||
# Insert effect name with tooltip tag
|
||||
tag_name = f"tooltip_{name.replace(' ', '_')}"
|
||||
self.current_effects.insert(tk.END, f"{name}: ", ('effect_name', tag_name))
|
||||
|
||||
# Bind tooltip events
|
||||
self.current_effects.tag_bind(tag_name, "<Enter>", lambda e, n=name: self.show_effect_tooltip(e, n))
|
||||
self.current_effects.tag_bind(tag_name, "<Leave>", self.hide_effect_tooltip)
|
||||
|
||||
self.current_effects.insert(tk.END, f"{value}\n", 'effect_value')
|
||||
else:
|
||||
self.current_effects.insert(tk.END, "No effect data available for this level.\n\n", 'effect_name')
|
||||
self.current_effects.insert(tk.END, "Try selecting: Lv 1, 25, 40, or 50", 'effect_name')
|
||||
|
||||
self.current_effects.config(state=tk.DISABLED)
|
||||
|
||||
def show_effect_tooltip(self, event, effect_name):
|
||||
"""Show tooltip for effect"""
|
||||
if effect_name in EFFECT_DESCRIPTIONS:
|
||||
text = EFFECT_DESCRIPTIONS[effect_name]
|
||||
x = event.x_root + 15
|
||||
y = event.y_root + 10
|
||||
|
||||
# Close existing if any
|
||||
self.hide_effect_tooltip(None)
|
||||
|
||||
self.tooltip_window = tk.Toplevel(self)
|
||||
self.tooltip_window.wm_overrideredirect(True)
|
||||
self.tooltip_window.wm_geometry(f"+{x}+{y}")
|
||||
|
||||
label = tk.Label(self.tooltip_window, text=text, justify=tk.LEFT,
|
||||
background=BG_LIGHT, foreground=TEXT_PRIMARY,
|
||||
relief=tk.SOLID, borderwidth=1, font=FONT_SMALL,
|
||||
padx=10, pady=5, wraplength=250)
|
||||
label.pack()
|
||||
|
||||
def hide_effect_tooltip(self, event):
|
||||
"""Hide tooltip"""
|
||||
if hasattr(self, 'tooltip_window') and self.tooltip_window:
|
||||
self.tooltip_window.destroy()
|
||||
self.tooltip_window = None
|
||||
|
||||
def update_progression_table(self):
|
||||
"""Update the effect progression table"""
|
||||
self.effects_tree.delete(*self.effects_tree.get_children())
|
||||
|
||||
if not self.current_card_id:
|
||||
return
|
||||
|
||||
# Get all effects
|
||||
all_effects = get_all_effects(self.current_card_id)
|
||||
|
||||
# Organize by effect name
|
||||
effect_by_level = {}
|
||||
for level, effect_name, effect_value in all_effects:
|
||||
if effect_name not in effect_by_level:
|
||||
effect_by_level[effect_name] = {}
|
||||
effect_by_level[effect_name][level] = effect_value
|
||||
|
||||
# Key levels for the table
|
||||
key_levels = [1, 25, 40, 50]
|
||||
|
||||
# Add rows
|
||||
for effect_name, levels in sorted(effect_by_level.items()):
|
||||
row = [effect_name]
|
||||
for lvl in key_levels:
|
||||
# Find closest level we have data for
|
||||
value = levels.get(lvl, '')
|
||||
if not value:
|
||||
# Try to find nearest
|
||||
for l in sorted(levels.keys()):
|
||||
if l <= lvl:
|
||||
value = levels[l]
|
||||
row.append(value)
|
||||
|
||||
self.effects_tree.insert('', tk.END, values=row)
|
||||
180
gui/hints_skills_view.py
Normal file
180
gui/hints_skills_view.py
Normal file
@@ -0,0 +1,180 @@
|
||||
"""
|
||||
Hints and Skills View - Display support hints and event skills
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from db.db_queries import get_hints, get_events, get_all_event_skills, get_card_by_id
|
||||
from gui.theme import (
|
||||
BG_DARK, BG_MEDIUM, BG_LIGHT,
|
||||
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_TERTIARY, ACCENT_SUCCESS,
|
||||
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
|
||||
FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL,
|
||||
create_styled_text, create_card_frame
|
||||
)
|
||||
|
||||
|
||||
class HintsSkillsFrame(ttk.Frame):
|
||||
"""Frame for viewing support hints and event skills"""
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self.current_card_id = None
|
||||
self.current_card_name = None
|
||||
|
||||
self.create_widgets()
|
||||
|
||||
def create_widgets(self):
|
||||
"""Create the hints and skills interface"""
|
||||
# Header
|
||||
header_frame = tk.Frame(self, bg=BG_DARK)
|
||||
header_frame.pack(fill=tk.X, padx=20, pady=15)
|
||||
|
||||
self.card_label = tk.Label(header_frame,
|
||||
text="💡 Select a card from the Card List tab",
|
||||
font=FONT_HEADER, bg=BG_DARK, fg=ACCENT_PRIMARY)
|
||||
self.card_label.pack(side=tk.LEFT)
|
||||
|
||||
# Main content with two columns
|
||||
content_frame = tk.Frame(self, bg=BG_DARK)
|
||||
content_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=(0, 15))
|
||||
|
||||
# Left column: Hints
|
||||
left_container = tk.Frame(content_frame, bg=BG_DARK)
|
||||
left_container.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 10))
|
||||
|
||||
hints_header = tk.Frame(left_container, bg=BG_DARK)
|
||||
hints_header.pack(fill=tk.X, pady=(0, 8))
|
||||
tk.Label(hints_header, text="🎯 Training Hints", font=FONT_SUBHEADER,
|
||||
bg=BG_DARK, fg=TEXT_PRIMARY).pack(side=tk.LEFT)
|
||||
|
||||
hints_frame = create_card_frame(left_container)
|
||||
hints_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
self.hints_text = create_styled_text(hints_frame, height=18)
|
||||
self.hints_text.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
|
||||
self.hints_text.config(state=tk.DISABLED)
|
||||
|
||||
# Configure tags for hints
|
||||
self.hints_text.tag_configure('header', font=FONT_SUBHEADER, foreground=ACCENT_PRIMARY)
|
||||
self.hints_text.tag_configure('skill', foreground=ACCENT_TERTIARY, font=FONT_BODY_BOLD)
|
||||
self.hints_text.tag_configure('desc', foreground=TEXT_MUTED)
|
||||
self.hints_text.tag_configure('number', foreground=ACCENT_SECONDARY)
|
||||
|
||||
# Right column: Events and Skills
|
||||
right_container = tk.Frame(content_frame, bg=BG_DARK)
|
||||
right_container.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
|
||||
|
||||
events_header = tk.Frame(right_container, bg=BG_DARK)
|
||||
events_header.pack(fill=tk.X, pady=(0, 8))
|
||||
tk.Label(events_header, text="📅 Training Events & Skills", font=FONT_SUBHEADER,
|
||||
bg=BG_DARK, fg=TEXT_PRIMARY).pack(side=tk.LEFT)
|
||||
|
||||
events_frame = create_card_frame(right_container)
|
||||
events_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
tree_container = tk.Frame(events_frame, bg=BG_MEDIUM)
|
||||
tree_container.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
|
||||
|
||||
# Treeview for events
|
||||
self.events_tree = ttk.Treeview(tree_container, columns=('event', 'skills'), show='tree headings')
|
||||
self.events_tree.heading('#0', text='')
|
||||
self.events_tree.heading('event', text='Event/Skill')
|
||||
self.events_tree.heading('skills', text='Details')
|
||||
|
||||
self.events_tree.column('#0', width=35)
|
||||
self.events_tree.column('event', width=240)
|
||||
self.events_tree.column('skills', width=180)
|
||||
|
||||
scrollbar = ttk.Scrollbar(tree_container, orient=tk.VERTICAL, command=self.events_tree.yview)
|
||||
self.events_tree.configure(yscrollcommand=scrollbar.set)
|
||||
|
||||
self.events_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
# Summary section at bottom
|
||||
summary_frame = tk.Frame(self, bg=BG_MEDIUM, padx=15, pady=10)
|
||||
summary_frame.pack(fill=tk.X, padx=20, pady=(0, 10))
|
||||
|
||||
self.summary_label = tk.Label(summary_frame, text="", font=FONT_SMALL,
|
||||
bg=BG_MEDIUM, fg=TEXT_SECONDARY)
|
||||
self.summary_label.pack()
|
||||
|
||||
def set_card(self, card_id):
|
||||
"""Load a card's hints and skills"""
|
||||
self.current_card_id = card_id
|
||||
|
||||
# Get card info
|
||||
card = get_card_by_id(card_id)
|
||||
if card:
|
||||
self.current_card_name = card[1]
|
||||
self.card_label.config(text=f"💡 {self.current_card_name}")
|
||||
|
||||
self.update_hints_display()
|
||||
self.update_events_display()
|
||||
|
||||
def update_hints_display(self):
|
||||
"""Update the hints display"""
|
||||
self.hints_text.config(state=tk.NORMAL)
|
||||
self.hints_text.delete('1.0', tk.END)
|
||||
|
||||
if not self.current_card_id:
|
||||
self.hints_text.insert(tk.END, "No card selected\n\n", 'desc')
|
||||
self.hints_text.insert(tk.END, "Select a card from the Card List tab to view its hints.", 'desc')
|
||||
self.hints_text.config(state=tk.DISABLED)
|
||||
return
|
||||
|
||||
hints = get_hints(self.current_card_id)
|
||||
|
||||
self.hints_text.insert(tk.END, "Training Skills this card can teach:\n\n", 'header')
|
||||
|
||||
if hints:
|
||||
for i, (hint_name, hint_desc) in enumerate(hints, 1):
|
||||
self.hints_text.insert(tk.END, f" {i}. ", 'number')
|
||||
self.hints_text.insert(tk.END, f"{hint_name}\n", 'skill')
|
||||
if hint_desc:
|
||||
self.hints_text.insert(tk.END, f" {hint_desc}\n", 'desc')
|
||||
self.hints_text.insert(tk.END, "\n")
|
||||
else:
|
||||
self.hints_text.insert(tk.END, " No hints/skills data available.\n\n", 'desc')
|
||||
self.hints_text.insert(tk.END, " This may mean:\n", 'desc')
|
||||
self.hints_text.insert(tk.END, " • Card hasn't been scraped yet\n", 'desc')
|
||||
self.hints_text.insert(tk.END, " • Card has no trainable skills\n", 'desc')
|
||||
|
||||
self.hints_text.config(state=tk.DISABLED)
|
||||
|
||||
def update_events_display(self):
|
||||
"""Update the events tree display"""
|
||||
self.events_tree.delete(*self.events_tree.get_children())
|
||||
|
||||
if not self.current_card_id:
|
||||
return
|
||||
|
||||
events = get_events(self.current_card_id)
|
||||
events_with_skills = get_all_event_skills(self.current_card_id)
|
||||
|
||||
# Add events as parent nodes
|
||||
for event_id, event_name, event_type in events:
|
||||
skills = events_with_skills.get(event_name, [])
|
||||
skill_count = f"{len(skills)} skills" if skills else "No skills"
|
||||
|
||||
event_node = self.events_tree.insert('', tk.END, text='📅',
|
||||
values=(event_name, skill_count))
|
||||
|
||||
# Add skills as children
|
||||
for skill in skills:
|
||||
self.events_tree.insert(event_node, tk.END, text='⭐',
|
||||
values=(skill, ''))
|
||||
|
||||
# Update summary
|
||||
hint_count = len(get_hints(self.current_card_id))
|
||||
event_count = len(events)
|
||||
|
||||
self.summary_label.config(
|
||||
text=f"📊 Summary: {hint_count} hints │ {event_count} events"
|
||||
)
|
||||
239
gui/main_window.py
Normal file
239
gui/main_window.py
Normal file
@@ -0,0 +1,239 @@
|
||||
"""
|
||||
Main Window for Umamusume Support Card Manager
|
||||
Tabbed interface for card browsing, effects, deck builder, and hints
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from db.db_queries import get_database_stats, get_owned_count
|
||||
from gui.card_view import CardListFrame
|
||||
from gui.effects_view import EffectsFrame
|
||||
from gui.hints_skills_view import HintsSkillsFrame
|
||||
from gui.deck_builder import DeckBuilderFrame
|
||||
from gui.update_dialog import show_update_dialog
|
||||
from gui.theme import (
|
||||
configure_styles, create_styled_button,
|
||||
BG_DARK, BG_MEDIUM, BG_LIGHT, BG_HIGHLIGHT,
|
||||
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_TERTIARY,
|
||||
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
|
||||
FONT_TITLE, FONT_HEADER, FONT_BODY, FONT_SMALL
|
||||
)
|
||||
from utils import resolve_image_path
|
||||
from version import VERSION
|
||||
|
||||
|
||||
class MainWindow:
|
||||
"""Main application window with tabbed interface"""
|
||||
|
||||
def __init__(self):
|
||||
self.root = tk.Tk()
|
||||
self.root.title("Umamusume Support Card Manager")
|
||||
self.root.geometry("1350x800")
|
||||
self.root.minsize(1350, 800)
|
||||
|
||||
# Set icon
|
||||
try:
|
||||
icon_path = resolve_image_path("1_Special Week.png")
|
||||
if icon_path and os.path.exists(icon_path):
|
||||
icon_img = tk.PhotoImage(file=icon_path)
|
||||
self.root.iconphoto(True, icon_img)
|
||||
except Exception as e:
|
||||
print(f"Failed to set icon: {e}")
|
||||
|
||||
# Configure all styles using centralized theme
|
||||
configure_styles(self.root)
|
||||
|
||||
# Create main container
|
||||
main_container = ttk.Frame(self.root)
|
||||
main_container.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Header with stats
|
||||
self.create_header(main_container)
|
||||
|
||||
# Status bar - Create BEFORE notebook to anchor it to bottom
|
||||
self.create_status_bar(main_container)
|
||||
|
||||
# Tabbed notebook
|
||||
self.notebook = ttk.Notebook(main_container)
|
||||
self.notebook.pack(fill=tk.BOTH, expand=True, padx=15, pady=8)
|
||||
|
||||
# Create tabs
|
||||
self.create_tabs()
|
||||
|
||||
def create_header(self, parent):
|
||||
"""Create header with database statistics and update button"""
|
||||
# Header container with subtle bottom border effect
|
||||
header_outer = tk.Frame(parent, bg=BG_DARK)
|
||||
header_outer.pack(fill=tk.X)
|
||||
|
||||
header_frame = tk.Frame(header_outer, bg=BG_DARK)
|
||||
header_frame.pack(fill=tk.X, padx=20, pady=15)
|
||||
|
||||
# Left side: Title and version
|
||||
title_frame = tk.Frame(header_frame, bg=BG_DARK)
|
||||
title_frame.pack(side=tk.LEFT)
|
||||
|
||||
# App icon and title
|
||||
title_label = tk.Label(
|
||||
title_frame,
|
||||
text="🏇 Umamusume Support Card Manager",
|
||||
font=FONT_TITLE,
|
||||
bg=BG_DARK,
|
||||
fg=ACCENT_PRIMARY
|
||||
)
|
||||
title_label.pack(side=tk.LEFT)
|
||||
|
||||
# Version badge
|
||||
version_frame = tk.Frame(title_frame, bg=ACCENT_SECONDARY, padx=8, pady=2)
|
||||
version_frame.pack(side=tk.LEFT, padx=12)
|
||||
version_label = tk.Label(
|
||||
version_frame,
|
||||
text=f"v{VERSION}",
|
||||
font=FONT_SMALL,
|
||||
bg=ACCENT_SECONDARY,
|
||||
fg=TEXT_PRIMARY
|
||||
)
|
||||
version_label.pack()
|
||||
|
||||
# Right side: Update button and stats
|
||||
right_frame = tk.Frame(header_frame, bg=BG_DARK)
|
||||
right_frame.pack(side=tk.RIGHT)
|
||||
|
||||
# Update button with modern styling
|
||||
self.update_button = create_styled_button(
|
||||
right_frame,
|
||||
text="🔄 Check for Updates",
|
||||
command=self.show_update_dialog,
|
||||
style_type='default'
|
||||
)
|
||||
self.update_button.pack(side=tk.RIGHT, padx=(15, 0))
|
||||
|
||||
# Stats panel with card-like appearance
|
||||
stats_frame = tk.Frame(right_frame, bg=BG_MEDIUM, padx=15, pady=8)
|
||||
stats_frame.pack(side=tk.RIGHT)
|
||||
|
||||
stats = get_database_stats()
|
||||
owned = get_owned_count()
|
||||
|
||||
# Build stats text with better formatting
|
||||
stats_parts = [
|
||||
f"📊 {stats.get('total_cards', 0)} Cards",
|
||||
f"✨ {owned} Owned",
|
||||
f"🏆 {stats.get('by_rarity', {}).get('SSR', 0)} SSR",
|
||||
f"⭐ {stats.get('by_rarity', {}).get('SR', 0)} SR",
|
||||
f"● {stats.get('by_rarity', {}).get('R', 0)} R"
|
||||
]
|
||||
stats_text = " │ ".join(stats_parts)
|
||||
|
||||
self.stats_label = tk.Label(
|
||||
stats_frame,
|
||||
text=stats_text,
|
||||
font=FONT_SMALL,
|
||||
bg=BG_MEDIUM,
|
||||
fg=TEXT_SECONDARY
|
||||
)
|
||||
self.stats_label.pack()
|
||||
|
||||
# Subtle separator line
|
||||
separator = tk.Frame(header_outer, bg=BG_LIGHT, height=1)
|
||||
separator.pack(fill=tk.X, padx=15)
|
||||
|
||||
def create_tabs(self):
|
||||
"""Create all tab frames"""
|
||||
# Card List Tab
|
||||
self.card_frame = CardListFrame(self.notebook, on_card_selected_callback=self.on_card_selected)
|
||||
self.notebook.add(self.card_frame, text=" 📋 Card List ")
|
||||
|
||||
# Effects Tab
|
||||
self.effects_frame = EffectsFrame(self.notebook)
|
||||
self.notebook.add(self.effects_frame, text=" 📊 Effects ")
|
||||
|
||||
# Deck Builder Tab
|
||||
self.deck_frame = DeckBuilderFrame(self.notebook)
|
||||
self.notebook.add(self.deck_frame, text=" 🎴 Deck Builder ")
|
||||
|
||||
# Hints & Skills Tab
|
||||
self.hints_frame = HintsSkillsFrame(self.notebook)
|
||||
self.notebook.add(self.hints_frame, text=" 💡 Hints & Skills ")
|
||||
|
||||
def create_status_bar(self, parent):
|
||||
"""Create status bar at bottom"""
|
||||
status_outer = tk.Frame(parent, bg=BG_MEDIUM)
|
||||
status_outer.pack(fill=tk.X, side=tk.BOTTOM)
|
||||
|
||||
status_frame = tk.Frame(status_outer, bg=BG_MEDIUM)
|
||||
status_frame.pack(fill=tk.X, padx=15, pady=8)
|
||||
|
||||
self.status_label = tk.Label(
|
||||
status_frame,
|
||||
text="✓ Ready",
|
||||
font=FONT_SMALL,
|
||||
bg=BG_MEDIUM,
|
||||
fg=TEXT_MUTED
|
||||
)
|
||||
self.status_label.pack(side=tk.LEFT)
|
||||
|
||||
tk.Label(
|
||||
status_frame,
|
||||
text="Data from gametora.com",
|
||||
font=FONT_SMALL,
|
||||
bg=BG_MEDIUM,
|
||||
fg=TEXT_MUTED
|
||||
).pack(side=tk.RIGHT)
|
||||
|
||||
tk.Label(
|
||||
status_frame,
|
||||
text="Made by Kiyreload │ ",
|
||||
font=FONT_SMALL,
|
||||
bg=BG_MEDIUM,
|
||||
fg=ACCENT_TERTIARY
|
||||
).pack(side=tk.RIGHT)
|
||||
|
||||
def on_card_selected(self, card_id, card_name):
|
||||
"""Handle card selection from card list"""
|
||||
# Update other tabs with selected card
|
||||
if hasattr(self, 'effects_frame'):
|
||||
self.effects_frame.set_card(card_id)
|
||||
if hasattr(self, 'hints_frame'):
|
||||
self.hints_frame.set_card(card_id)
|
||||
|
||||
self.status_label.config(text=f"📌 Selected: {card_name}")
|
||||
|
||||
def refresh_stats(self):
|
||||
"""Refresh the statistics display"""
|
||||
stats = get_database_stats()
|
||||
owned = get_owned_count()
|
||||
|
||||
stats_parts = [
|
||||
f"📊 {stats.get('total_cards', 0)} Cards",
|
||||
f"✨ {owned} Owned",
|
||||
f"🏆 {stats.get('by_rarity', {}).get('SSR', 0)} SSR",
|
||||
f"⭐ {stats.get('by_rarity', {}).get('SR', 0)} SR",
|
||||
f"● {stats.get('by_rarity', {}).get('R', 0)} R"
|
||||
]
|
||||
stats_text = " │ ".join(stats_parts)
|
||||
|
||||
self.stats_label.config(text=stats_text)
|
||||
|
||||
def show_update_dialog(self):
|
||||
"""Show the update dialog"""
|
||||
show_update_dialog(self.root)
|
||||
|
||||
def run(self):
|
||||
"""Start the application"""
|
||||
self.root.mainloop()
|
||||
|
||||
|
||||
def main():
|
||||
"""Entry point for GUI"""
|
||||
app = MainWindow()
|
||||
app.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
461
gui/theme.py
Normal file
461
gui/theme.py
Normal file
@@ -0,0 +1,461 @@
|
||||
"""
|
||||
Centralized Theme Module for Umamusume Support Card Manager
|
||||
Modern glassmorphism-inspired dark theme with consistent styling
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# COLOR PALETTE
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# Primary backgrounds (rich purplish-blues with depth)
|
||||
BG_DARKEST = '#0d0d1a' # Deepest background
|
||||
BG_DARK = '#151528' # Main application background
|
||||
BG_MEDIUM = '#1e1e3f' # Card/panel backgrounds
|
||||
BG_LIGHT = '#2a2a5a' # Elevated elements, hover states
|
||||
BG_HIGHLIGHT = '#3d3d7a' # Active/selected backgrounds
|
||||
|
||||
# Accents (vibrant but refined)
|
||||
ACCENT_PRIMARY = '#ff6b9d' # Pink accent (main action color)
|
||||
ACCENT_SECONDARY = '#7c5cff' # Purple accent (secondary actions)
|
||||
ACCENT_TERTIARY = '#5ce1e6' # Cyan accent (info/highlights)
|
||||
ACCENT_SUCCESS = '#4ade80' # Green for success states
|
||||
ACCENT_WARNING = '#fbbf24' # Amber for warnings
|
||||
ACCENT_ERROR = '#ff6b6b' # Red for errors
|
||||
|
||||
# Text colors
|
||||
TEXT_PRIMARY = '#ffffff' # Primary text (headings, important)
|
||||
TEXT_SECONDARY = '#e0e0f0' # Secondary text (body text)
|
||||
TEXT_MUTED = '#9090b0' # Muted text (labels, hints)
|
||||
TEXT_DISABLED = '#606080' # Disabled text
|
||||
|
||||
# Rarity colors (enhanced with glow effect potential)
|
||||
RARITY_SSR = '#ffd700' # Gold
|
||||
RARITY_SR = '#c0c0c0' # Silver
|
||||
RARITY_R = '#cd853f' # Bronze (warmer)
|
||||
|
||||
RARITY_COLORS = {
|
||||
'SSR': RARITY_SSR,
|
||||
'SR': RARITY_SR,
|
||||
'R': RARITY_R
|
||||
}
|
||||
|
||||
# Type colors (for card types)
|
||||
TYPE_COLORS = {
|
||||
'Speed': '#3b82f6', # Blue
|
||||
'Stamina': '#f97316', # Orange
|
||||
'Power': '#eab308', # Yellow
|
||||
'Guts': '#ef4444', # Red
|
||||
'Wisdom': '#22c55e', # Green
|
||||
'Friend': '#a855f7', # Purple
|
||||
'Group': '#f59e0b' # Amber
|
||||
}
|
||||
|
||||
# Type icons
|
||||
TYPE_ICONS = {
|
||||
'Speed': '🏃',
|
||||
'Stamina': '💚',
|
||||
'Power': '💪',
|
||||
'Guts': '🔥',
|
||||
'Wisdom': '🧠',
|
||||
'Friend': '💜',
|
||||
'Group': '👥'
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# FONTS
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
FONT_FAMILY = 'Segoe UI'
|
||||
FONT_FAMILY_MONO = 'Consolas'
|
||||
|
||||
FONT_TITLE = (FONT_FAMILY, 18, 'bold')
|
||||
FONT_HEADER = (FONT_FAMILY, 14, 'bold')
|
||||
FONT_SUBHEADER = (FONT_FAMILY, 12, 'bold')
|
||||
FONT_BODY = (FONT_FAMILY, 11)
|
||||
FONT_BODY_BOLD = (FONT_FAMILY, 11, 'bold')
|
||||
FONT_SMALL = (FONT_FAMILY, 10)
|
||||
FONT_TINY = (FONT_FAMILY, 9)
|
||||
FONT_MONO = (FONT_FAMILY_MONO, 11)
|
||||
FONT_MONO_SMALL = (FONT_FAMILY_MONO, 10)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# STYLE CONFIGURATION
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def configure_styles(root: tk.Tk):
|
||||
"""Configure all ttk styles for the application"""
|
||||
style = ttk.Style()
|
||||
|
||||
# Use clam theme as base for better customization
|
||||
style.theme_use('clam')
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# General Frame and Label styles
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
style.configure('TFrame', background=BG_DARK)
|
||||
style.configure('TLabel', background=BG_DARK, foreground=TEXT_SECONDARY, font=FONT_BODY)
|
||||
style.configure('TLabelframe', background=BG_DARK, foreground=TEXT_SECONDARY)
|
||||
style.configure('TLabelframe.Label', background=BG_DARK, foreground=ACCENT_PRIMARY, font=FONT_SUBHEADER)
|
||||
|
||||
# Header styles
|
||||
style.configure('Title.TLabel', font=FONT_TITLE, foreground=TEXT_PRIMARY, background=BG_DARK)
|
||||
style.configure('Header.TLabel', font=FONT_HEADER, foreground=ACCENT_PRIMARY, background=BG_DARK)
|
||||
style.configure('Subheader.TLabel', font=FONT_SUBHEADER, foreground=TEXT_PRIMARY, background=BG_DARK)
|
||||
style.configure('Subtitle.TLabel', font=FONT_SMALL, foreground=TEXT_MUTED, background=BG_DARK)
|
||||
style.configure('Stats.TLabel', font=FONT_SMALL, foreground=TEXT_SECONDARY, background=BG_MEDIUM, padding=8)
|
||||
style.configure('Accent.TLabel', font=FONT_BODY, foreground=ACCENT_PRIMARY, background=BG_DARK)
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Button styles
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
style.configure('TButton',
|
||||
padding=(12, 6),
|
||||
font=FONT_BODY,
|
||||
background=BG_LIGHT,
|
||||
foreground=TEXT_PRIMARY)
|
||||
style.map('TButton',
|
||||
background=[('active', BG_HIGHLIGHT), ('pressed', ACCENT_PRIMARY)],
|
||||
foreground=[('active', TEXT_PRIMARY), ('pressed', TEXT_PRIMARY)])
|
||||
|
||||
style.configure('Accent.TButton',
|
||||
padding=(12, 6),
|
||||
font=FONT_BODY_BOLD,
|
||||
background=ACCENT_PRIMARY,
|
||||
foreground=TEXT_PRIMARY)
|
||||
style.map('Accent.TButton',
|
||||
background=[('active', '#ff8ab5'), ('pressed', '#e55a88')])
|
||||
|
||||
style.configure('Small.TButton',
|
||||
padding=(8, 4),
|
||||
font=FONT_SMALL)
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Checkbutton styles
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
style.configure('TCheckbutton',
|
||||
background=BG_DARK,
|
||||
foreground=TEXT_SECONDARY,
|
||||
font=FONT_BODY)
|
||||
style.map('TCheckbutton',
|
||||
background=[('active', BG_DARK)],
|
||||
foreground=[('active', TEXT_PRIMARY)])
|
||||
|
||||
style.configure('Large.TCheckbutton',
|
||||
font=FONT_BODY_BOLD,
|
||||
background=BG_DARK,
|
||||
foreground=TEXT_PRIMARY)
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Entry and Combobox styles
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
style.configure('TEntry',
|
||||
fieldbackground=BG_MEDIUM,
|
||||
foreground=TEXT_PRIMARY,
|
||||
insertcolor=TEXT_PRIMARY,
|
||||
padding=6)
|
||||
|
||||
style.configure('TCombobox',
|
||||
fieldbackground=BG_MEDIUM,
|
||||
background=BG_LIGHT,
|
||||
foreground=TEXT_PRIMARY,
|
||||
arrowcolor=TEXT_MUTED,
|
||||
padding=4)
|
||||
style.map('TCombobox',
|
||||
fieldbackground=[('readonly', BG_MEDIUM)],
|
||||
selectbackground=[('readonly', BG_HIGHLIGHT)])
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Notebook (Tab) styles
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
style.configure('TNotebook',
|
||||
background=BG_DARK,
|
||||
borderwidth=0)
|
||||
style.configure('TNotebook.Tab',
|
||||
padding=(20, 10),
|
||||
font=FONT_BODY_BOLD,
|
||||
background=BG_MEDIUM,
|
||||
foreground=TEXT_MUTED)
|
||||
style.map('TNotebook.Tab',
|
||||
background=[('selected', BG_LIGHT), ('active', BG_HIGHLIGHT)],
|
||||
foreground=[('selected', ACCENT_PRIMARY), ('active', TEXT_PRIMARY)],
|
||||
expand=[('selected', (0, 0, 0, 2))])
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Treeview styles
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
style.configure('Treeview',
|
||||
background=BG_MEDIUM,
|
||||
foreground=TEXT_SECONDARY,
|
||||
fieldbackground=BG_MEDIUM,
|
||||
font=FONT_BODY,
|
||||
rowheight=28)
|
||||
style.configure('Treeview.Heading',
|
||||
font=FONT_BODY_BOLD,
|
||||
background=BG_LIGHT,
|
||||
foreground=TEXT_PRIMARY,
|
||||
padding=6)
|
||||
style.map('Treeview',
|
||||
background=[('selected', ACCENT_PRIMARY)],
|
||||
foreground=[('selected', TEXT_PRIMARY)])
|
||||
style.map('Treeview.Heading',
|
||||
background=[('active', BG_HIGHLIGHT)])
|
||||
|
||||
# Card list with larger rows for thumbnails
|
||||
style.configure('CardList.Treeview',
|
||||
background=BG_MEDIUM,
|
||||
foreground=TEXT_SECONDARY,
|
||||
fieldbackground=BG_MEDIUM,
|
||||
font=FONT_BODY,
|
||||
rowheight=40)
|
||||
|
||||
# Deck list style
|
||||
style.configure('DeckList.Treeview',
|
||||
background=BG_MEDIUM,
|
||||
foreground=TEXT_SECONDARY,
|
||||
fieldbackground=BG_MEDIUM,
|
||||
font=FONT_BODY,
|
||||
rowheight=40)
|
||||
style.map('DeckList.Treeview',
|
||||
background=[('selected', ACCENT_PRIMARY)])
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Scale (Slider) styles
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
style.configure('TScale',
|
||||
background=BG_DARK,
|
||||
troughcolor=BG_MEDIUM,
|
||||
sliderthickness=18)
|
||||
style.configure('Horizontal.TScale',
|
||||
background=BG_DARK)
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Progressbar styles
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
style.configure('TProgressbar',
|
||||
background=ACCENT_PRIMARY,
|
||||
troughcolor=BG_MEDIUM,
|
||||
borderwidth=0,
|
||||
thickness=8)
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Scrollbar styles
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
style.configure('TScrollbar',
|
||||
background=BG_LIGHT,
|
||||
troughcolor=BG_MEDIUM,
|
||||
borderwidth=0,
|
||||
arrowsize=14)
|
||||
style.map('TScrollbar',
|
||||
background=[('active', BG_HIGHLIGHT), ('pressed', ACCENT_PRIMARY)])
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# PanedWindow styles
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
style.configure('TPanedwindow', background=BG_DARK)
|
||||
|
||||
# Set root background
|
||||
root.configure(bg=BG_DARK)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# WIDGET HELPER FUNCTIONS
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def create_styled_button(parent, text, command=None, style_type='default', **kwargs):
|
||||
"""Create a styled tk.Button with modern appearance"""
|
||||
bg_colors = {
|
||||
'default': BG_LIGHT,
|
||||
'accent': ACCENT_PRIMARY,
|
||||
'secondary': ACCENT_SECONDARY,
|
||||
'success': ACCENT_SUCCESS,
|
||||
'warning': ACCENT_WARNING,
|
||||
'danger': ACCENT_ERROR
|
||||
}
|
||||
hover_colors = {
|
||||
'default': BG_HIGHLIGHT,
|
||||
'accent': '#ff8ab5',
|
||||
'secondary': '#9580ff',
|
||||
'success': '#6ee7a0',
|
||||
'warning': '#fcd34d',
|
||||
'danger': '#ff8a8a'
|
||||
}
|
||||
|
||||
bg = bg_colors.get(style_type, BG_LIGHT)
|
||||
hover_bg = hover_colors.get(style_type, BG_HIGHLIGHT)
|
||||
|
||||
btn = tk.Button(
|
||||
parent,
|
||||
text=text,
|
||||
command=command,
|
||||
bg=bg,
|
||||
fg=TEXT_PRIMARY,
|
||||
font=FONT_BODY_BOLD if style_type == 'accent' else FONT_BODY,
|
||||
activebackground=hover_bg,
|
||||
activeforeground=TEXT_PRIMARY,
|
||||
bd=0,
|
||||
padx=16,
|
||||
pady=8,
|
||||
cursor='hand2',
|
||||
relief=tk.FLAT,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
# Add hover effect
|
||||
def on_enter(e):
|
||||
btn.configure(bg=hover_bg)
|
||||
def on_leave(e):
|
||||
btn.configure(bg=bg)
|
||||
|
||||
btn.bind('<Enter>', on_enter)
|
||||
btn.bind('<Leave>', on_leave)
|
||||
|
||||
return btn
|
||||
|
||||
|
||||
def create_styled_text(parent, height=10, **kwargs):
|
||||
"""Create a styled tk.Text widget with modern appearance"""
|
||||
text = tk.Text(
|
||||
parent,
|
||||
bg=BG_MEDIUM,
|
||||
fg=TEXT_SECONDARY,
|
||||
font=FONT_MONO,
|
||||
insertbackground=TEXT_PRIMARY,
|
||||
selectbackground=ACCENT_PRIMARY,
|
||||
selectforeground=TEXT_PRIMARY,
|
||||
relief=tk.FLAT,
|
||||
padx=12,
|
||||
pady=12,
|
||||
height=height,
|
||||
wrap=tk.WORD,
|
||||
**kwargs
|
||||
)
|
||||
return text
|
||||
|
||||
|
||||
def create_card_frame(parent, **kwargs):
|
||||
"""Create a styled frame that looks like a card"""
|
||||
frame = tk.Frame(
|
||||
parent,
|
||||
bg=BG_MEDIUM,
|
||||
highlightthickness=1,
|
||||
highlightbackground=BG_LIGHT,
|
||||
**kwargs
|
||||
)
|
||||
return frame
|
||||
|
||||
|
||||
def get_rarity_color(rarity):
|
||||
"""Get the color for a card rarity"""
|
||||
return RARITY_COLORS.get(rarity, TEXT_SECONDARY)
|
||||
|
||||
|
||||
def get_type_color(card_type):
|
||||
"""Get the color for a card type"""
|
||||
return TYPE_COLORS.get(card_type, TEXT_SECONDARY)
|
||||
|
||||
|
||||
def get_type_icon(card_type):
|
||||
"""Get the emoji icon for a card type"""
|
||||
return TYPE_ICONS.get(card_type, '')
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# TOOLTIPS & HELPERS
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
EFFECT_DESCRIPTIONS = {
|
||||
"Friendship Bonus": "Increases stats gained when training with this support card during Friendship Training (orange aura).",
|
||||
"Motivation Bonus": "Increases stats gained based on your Uma's motivation level.",
|
||||
"Specialty Rate": "Increases the chance of this card appearing in its specialty training.",
|
||||
"Training Bonus": "Flat percentage increase to stats gained in training where this card is present.",
|
||||
"Initial Bond": "Starting gauge value for this card.",
|
||||
"Race Bonus": "Increases stats gained from racing.",
|
||||
"Fan Count Bonus": "Increases fans gained from racing.",
|
||||
"Skill Pt Bonus": "Bonus skill points gained when training with this card.",
|
||||
"Hint Lv": "Starting level of skills taught by this card's hints.",
|
||||
"Hint Rate": "Increases chance of getting a hint event.",
|
||||
"Minigame Fail Rate": "Reduces chance of failing training.",
|
||||
"Energy Usage": "Reduces energy consumed during training.",
|
||||
"Current Energy": "Increases starting energy in scenario.",
|
||||
"Vitality": "Increases vitality gain from events.",
|
||||
"Stamina": "Increases stamina gain from training.",
|
||||
"Speed": "Increases speed gain from training.",
|
||||
"Power": "Increases power gain from training.",
|
||||
"Guts": "Increases guts gain from training.",
|
||||
"Wisdom": "Increases wisdom gain from training.",
|
||||
"Logic": "Custom logic effect.",
|
||||
"Starting Stats": "Increases initial stats at start of scenario."
|
||||
}
|
||||
|
||||
class Tooltip:
|
||||
"""
|
||||
Creates a tooltip for a given widget as the mouse hovers above it.
|
||||
"""
|
||||
def __init__(self, widget, text):
|
||||
self.widget = widget
|
||||
self.text = text
|
||||
self.tip_window = None
|
||||
self.id = None
|
||||
self.x = self.y = 0
|
||||
self._id1 = self.widget.bind("<Enter>", self.enter)
|
||||
self._id2 = self.widget.bind("<Leave>", self.leave)
|
||||
self._id3 = self.widget.bind("<ButtonPress>", self.leave)
|
||||
|
||||
def enter(self, event=None):
|
||||
self.schedule()
|
||||
|
||||
def leave(self, event=None):
|
||||
self.unschedule()
|
||||
self.hidetip()
|
||||
|
||||
def schedule(self):
|
||||
self.unschedule()
|
||||
self.id = self.widget.after(500, self.showtip)
|
||||
|
||||
def unschedule(self):
|
||||
id = self.id
|
||||
self.id = None
|
||||
if id:
|
||||
self.widget.after_cancel(id)
|
||||
|
||||
def showtip(self, event=None):
|
||||
x = y = 0
|
||||
try:
|
||||
x, y, cx, cy = self.widget.bbox("insert")
|
||||
except:
|
||||
pass
|
||||
x += self.widget.winfo_rootx() + 25
|
||||
y += self.widget.winfo_rooty() + 20
|
||||
|
||||
# Creates a toplevel window
|
||||
self.tip_window = tk.Toplevel(self.widget)
|
||||
|
||||
# Leaves only the label and removes the app window
|
||||
self.tip_window.wm_overrideredirect(True)
|
||||
self.tip_window.wm_geometry(f"+{x}+{y}")
|
||||
|
||||
label = tk.Label(
|
||||
self.tip_window,
|
||||
text=self.text,
|
||||
justify=tk.LEFT,
|
||||
background=BG_LIGHT,
|
||||
foreground=TEXT_PRIMARY,
|
||||
relief=tk.SOLID,
|
||||
borderwidth=1,
|
||||
font=FONT_SMALL,
|
||||
padx=10,
|
||||
pady=5
|
||||
)
|
||||
label.pack(ipadx=1)
|
||||
|
||||
def hidetip(self):
|
||||
tw = self.tip_window
|
||||
self.tip_window = None
|
||||
if tw:
|
||||
tw.destroy()
|
||||
|
||||
def create_tooltip(widget, text):
|
||||
"""Create a tooltip for a widget"""
|
||||
return Tooltip(widget, text)
|
||||
344
gui/update_dialog.py
Normal file
344
gui/update_dialog.py
Normal file
@@ -0,0 +1,344 @@
|
||||
"""
|
||||
Update Dialog for UmamusumeCardManager
|
||||
Provides a modal dialog for the update process.
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox, scrolledtext
|
||||
import threading
|
||||
import webbrowser
|
||||
from typing import Optional, Callable
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from updater.update_checker import check_for_updates, download_update, apply_update, get_current_version
|
||||
from gui.theme import (
|
||||
BG_DARK, BG_DARKEST, BG_MEDIUM, BG_LIGHT, BG_HIGHLIGHT,
|
||||
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_SUCCESS, ACCENT_ERROR,
|
||||
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
|
||||
FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL,
|
||||
create_styled_button
|
||||
)
|
||||
|
||||
|
||||
class UpdateDialog:
|
||||
"""Modal dialog for checking and applying updates."""
|
||||
|
||||
def __init__(self, parent: tk.Tk, on_close_callback: Optional[Callable] = None):
|
||||
self.parent = parent
|
||||
self.on_close_callback = on_close_callback
|
||||
self.update_info = None
|
||||
self.download_thread = None
|
||||
self.is_downloading = False
|
||||
|
||||
# Create the dialog window
|
||||
self.dialog = tk.Toplevel(parent)
|
||||
self.dialog.title("Check for Updates")
|
||||
self.dialog.geometry("520x600")
|
||||
self.dialog.resizable(True, True)
|
||||
self.dialog.minsize(480, 500)
|
||||
self.dialog.transient(parent)
|
||||
self.dialog.grab_set()
|
||||
|
||||
# Center on parent
|
||||
self.center_on_parent()
|
||||
|
||||
self.dialog.configure(bg=BG_DARK)
|
||||
|
||||
# Set up the UI
|
||||
self.setup_ui()
|
||||
|
||||
# Start checking for updates
|
||||
self.check_for_updates()
|
||||
|
||||
def center_on_parent(self):
|
||||
"""Center the dialog on the parent window."""
|
||||
self.dialog.update_idletasks()
|
||||
parent_x = self.parent.winfo_x()
|
||||
parent_y = self.parent.winfo_y()
|
||||
parent_w = self.parent.winfo_width()
|
||||
parent_h = self.parent.winfo_height()
|
||||
|
||||
dialog_w = 520
|
||||
dialog_h = 600
|
||||
|
||||
x = parent_x + (parent_w - dialog_w) // 2
|
||||
y = parent_y + (parent_h - dialog_h) // 2
|
||||
|
||||
self.dialog.geometry(f"{dialog_w}x{dialog_h}+{x}+{y}")
|
||||
|
||||
def setup_ui(self):
|
||||
"""Set up the dialog UI."""
|
||||
# Button frame (Create first to pack at bottom)
|
||||
self.button_frame = tk.Frame(self.dialog, bg=BG_DARK, pady=20, padx=20)
|
||||
self.button_frame.pack(side=tk.BOTTOM, fill=tk.X)
|
||||
|
||||
# Close button
|
||||
self.close_button = create_styled_button(
|
||||
self.button_frame,
|
||||
text="Close",
|
||||
command=self.close,
|
||||
style_type='default'
|
||||
)
|
||||
self.close_button.pack(side=tk.RIGHT)
|
||||
|
||||
# Update button (hidden initially)
|
||||
self.update_button = create_styled_button(
|
||||
self.button_frame,
|
||||
text="⬇️ Download & Install",
|
||||
command=self.start_download,
|
||||
style_type='accent'
|
||||
)
|
||||
# We don't pack it yet
|
||||
|
||||
# Main container
|
||||
main_frame = tk.Frame(self.dialog, bg=BG_DARK, padx=25, pady=20)
|
||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Title
|
||||
self.title_label = tk.Label(
|
||||
main_frame,
|
||||
text="🔄 Checking for Updates...",
|
||||
font=FONT_HEADER,
|
||||
bg=BG_DARK,
|
||||
fg=ACCENT_PRIMARY
|
||||
)
|
||||
self.title_label.pack(pady=(0, 10))
|
||||
|
||||
# Status message
|
||||
self.status_label = tk.Label(
|
||||
main_frame,
|
||||
text="Connecting to GitHub...",
|
||||
font=FONT_BODY,
|
||||
bg=BG_DARK,
|
||||
fg=TEXT_MUTED,
|
||||
wraplength=460
|
||||
)
|
||||
self.status_label.pack(pady=(0, 10))
|
||||
|
||||
# Version info frame
|
||||
self.version_frame = tk.Frame(main_frame, bg=BG_MEDIUM, padx=15, pady=10)
|
||||
self.version_frame.pack(fill=tk.X, pady=(0, 15))
|
||||
|
||||
self.current_version_label = tk.Label(
|
||||
self.version_frame,
|
||||
text=f"Current Version: v{get_current_version()}",
|
||||
font=FONT_BODY,
|
||||
bg=BG_MEDIUM,
|
||||
fg=TEXT_SECONDARY
|
||||
)
|
||||
self.current_version_label.pack(anchor='w')
|
||||
|
||||
self.new_version_label = tk.Label(
|
||||
self.version_frame,
|
||||
text="Latest Version: Checking...",
|
||||
font=FONT_BODY,
|
||||
bg=BG_MEDIUM,
|
||||
fg=TEXT_SECONDARY
|
||||
)
|
||||
self.new_version_label.pack(anchor='w')
|
||||
|
||||
# Release Notes Area
|
||||
self.notes_label = tk.Label(
|
||||
main_frame,
|
||||
text="What's New:",
|
||||
font=FONT_BODY_BOLD,
|
||||
bg=BG_DARK,
|
||||
fg=TEXT_PRIMARY
|
||||
)
|
||||
self.notes_label.pack(anchor='w', pady=(0, 5))
|
||||
|
||||
# Text box for release notes
|
||||
self.notes_text = scrolledtext.ScrolledText(
|
||||
main_frame,
|
||||
height=10,
|
||||
bg=BG_MEDIUM,
|
||||
fg=TEXT_SECONDARY,
|
||||
font=FONT_SMALL,
|
||||
borderwidth=0,
|
||||
highlightthickness=0,
|
||||
padx=10,
|
||||
pady=10
|
||||
)
|
||||
self.notes_text.pack(fill=tk.BOTH, expand=True, pady=(0, 15))
|
||||
self.notes_text.insert(tk.END, "Checking for release notes...")
|
||||
self.notes_text.config(state=tk.DISABLED)
|
||||
|
||||
# Progress bar (hidden initially)
|
||||
self.progress_frame = tk.Frame(main_frame, bg=BG_DARK)
|
||||
self.progress_frame.pack(fill=tk.X, pady=(0, 10))
|
||||
|
||||
self.progress_label = tk.Label(
|
||||
self.progress_frame,
|
||||
text="",
|
||||
font=FONT_SMALL,
|
||||
bg=BG_DARK,
|
||||
fg=TEXT_MUTED
|
||||
)
|
||||
self.progress_label.pack(anchor='w', pady=(0, 5))
|
||||
|
||||
self.progress_bar = ttk.Progressbar(
|
||||
self.progress_frame,
|
||||
mode='indeterminate',
|
||||
length=460
|
||||
)
|
||||
self.progress_bar.pack(fill=tk.X)
|
||||
self.progress_bar.start(10)
|
||||
|
||||
def check_for_updates(self):
|
||||
"""Check for updates in a background thread."""
|
||||
def check():
|
||||
self.update_info = check_for_updates()
|
||||
self.dialog.after(0, self.update_check_complete)
|
||||
|
||||
thread = threading.Thread(target=check, daemon=True)
|
||||
thread.start()
|
||||
|
||||
def update_check_complete(self):
|
||||
"""Called when the update check is complete."""
|
||||
self.progress_bar.stop()
|
||||
self.progress_frame.pack_forget() # Hide progress bar when check is done
|
||||
|
||||
# Enable text box to update it
|
||||
self.notes_text.config(state=tk.NORMAL)
|
||||
self.notes_text.delete(1.0, tk.END)
|
||||
|
||||
if self.update_info:
|
||||
# Update available!
|
||||
self.title_label.config(text="🎉 Update Available!")
|
||||
self.status_label.config(
|
||||
text="A new version is available.",
|
||||
fg=ACCENT_SUCCESS
|
||||
)
|
||||
self.new_version_label.config(
|
||||
text=f"Latest Version: {self.update_info['new_version']}",
|
||||
fg=ACCENT_SUCCESS
|
||||
)
|
||||
|
||||
# Show Release Notes
|
||||
notes = self.update_info.get('release_notes', 'No release notes available.')
|
||||
self.notes_text.insert(tk.END, notes)
|
||||
|
||||
# Show update button
|
||||
self.update_button.pack(side=tk.RIGHT, padx=(0, 10))
|
||||
else:
|
||||
# Up to date or error
|
||||
self.title_label.config(text="✅ You're Up to Date!")
|
||||
self.status_label.config(
|
||||
text=f"You are running the latest version.",
|
||||
fg=TEXT_SECONDARY
|
||||
)
|
||||
self.new_version_label.config(
|
||||
text=f"Latest Version: v{get_current_version()}",
|
||||
fg=ACCENT_SUCCESS
|
||||
)
|
||||
self.notes_text.insert(tk.END, "You are using the latest version of Umamusume Support Card Manager.\n\nEnjoy!")
|
||||
|
||||
self.notes_text.config(state=tk.DISABLED)
|
||||
|
||||
def start_download(self):
|
||||
"""Start downloading the update."""
|
||||
if self.is_downloading or not self.update_info:
|
||||
return
|
||||
|
||||
self.is_downloading = True
|
||||
self.update_button.config(state=tk.DISABLED, text="Downloading...")
|
||||
self.close_button.config(state=tk.DISABLED)
|
||||
|
||||
self.title_label.config(text="⬇️ Downloading Update...")
|
||||
self.status_label.config(text="Please wait...", fg=TEXT_MUTED)
|
||||
|
||||
# Configure progress bar for determinate mode
|
||||
self.progress_frame.pack(fill=tk.X, pady=(0, 10)) # Show progress frame again
|
||||
self.progress_bar.config(mode='determinate', maximum=100)
|
||||
self.progress_bar.pack(fill=tk.X)
|
||||
self.progress_bar['value'] = 0
|
||||
|
||||
def download():
|
||||
def progress_callback(downloaded, total):
|
||||
if total > 0:
|
||||
percent = int((downloaded / total) * 100)
|
||||
mb_downloaded = downloaded / (1024 * 1024)
|
||||
mb_total = total / (1024 * 1024)
|
||||
self.dialog.after(0, lambda: self.update_progress(percent, mb_downloaded, mb_total))
|
||||
|
||||
download_path = download_update(self.update_info['download_url'], progress_callback)
|
||||
self.dialog.after(0, lambda: self.download_complete(download_path))
|
||||
|
||||
self.download_thread = threading.Thread(target=download, daemon=True)
|
||||
self.download_thread.start()
|
||||
|
||||
def update_progress(self, percent: int, downloaded_mb: float, total_mb: float):
|
||||
"""Update the progress bar."""
|
||||
self.progress_bar['value'] = percent
|
||||
self.progress_label.config(text=f"Downloaded: {downloaded_mb:.1f} MB / {total_mb:.1f} MB ({percent}%)")
|
||||
|
||||
def download_complete(self, download_path: Optional[str]):
|
||||
"""Called when the download is complete."""
|
||||
self.is_downloading = False
|
||||
|
||||
if download_path:
|
||||
self.title_label.config(text="✅ Download Complete!")
|
||||
self.status_label.config(
|
||||
text="Update ready to install.",
|
||||
fg=ACCENT_SUCCESS
|
||||
)
|
||||
|
||||
# Change button to install
|
||||
self.update_button.config(
|
||||
state=tk.NORMAL,
|
||||
text="🔄 Install & Restart",
|
||||
command=lambda: self.install_update(download_path)
|
||||
)
|
||||
self.close_button.config(state=tk.NORMAL)
|
||||
else:
|
||||
self.title_label.config(text="❌ Download Failed")
|
||||
self.status_label.config(
|
||||
text="Failed not download update.",
|
||||
fg=ACCENT_ERROR
|
||||
)
|
||||
self.update_button.config(state=tk.NORMAL, text="⬇️ Retry Download")
|
||||
self.close_button.config(state=tk.NORMAL)
|
||||
|
||||
def install_update(self, download_path: str):
|
||||
"""Install the downloaded update."""
|
||||
self.title_label.config(text="🔄 Installing Update...")
|
||||
self.status_label.config(text="Applying update...", fg=TEXT_MUTED)
|
||||
self.update_button.config(state=tk.DISABLED)
|
||||
self.close_button.config(state=tk.DISABLED)
|
||||
|
||||
if apply_update(download_path):
|
||||
# Exit the application - the updater script will restart it
|
||||
self.dialog.after(1000, lambda: self.parent.quit())
|
||||
else:
|
||||
messagebox.showinfo(
|
||||
"Manual Update Required",
|
||||
f"The update was downloaded but cannot be applied automatically.\n\n"
|
||||
f"Downloaded file location:\n{download_path}\n\n"
|
||||
f"Please replace the current executable manually.",
|
||||
parent=self.dialog
|
||||
)
|
||||
self.close()
|
||||
|
||||
|
||||
def close(self):
|
||||
"""Close the dialog."""
|
||||
if self.on_close_callback:
|
||||
self.on_close_callback()
|
||||
self.dialog.destroy()
|
||||
|
||||
|
||||
def show_update_dialog(parent: tk.Tk, on_close_callback: Optional[Callable] = None) -> UpdateDialog:
|
||||
"""
|
||||
Show the update dialog.
|
||||
|
||||
Args:
|
||||
parent: The parent Tk window
|
||||
on_close_callback: Optional callback when dialog is closed
|
||||
|
||||
Returns:
|
||||
The UpdateDialog instance
|
||||
"""
|
||||
return UpdateDialog(parent, on_close_callback)
|
||||
Reference in New Issue
Block a user