From ebc0f132db159b81ad6569c48f08220b8b732abd Mon Sep 17 00:00:00 2001 From: kiyreload27 Date: Fri, 2 Jan 2026 02:34:34 +0000 Subject: [PATCH] feat: Introduce `db_queries` for card data, migrations, and updates, and `effects_view` and `theme` for GUI components. --- db/db_queries.py | 43 ++++++ gui/effects_view.py | 359 ++++++++++++-------------------------------- gui/theme.py | 11 ++ 3 files changed, 154 insertions(+), 259 deletions(-) diff --git a/db/db_queries.py b/db/db_queries.py index d8802c3..0741dd8 100644 --- a/db/db_queries.py +++ b/db/db_queries.py @@ -522,6 +522,49 @@ def get_unique_effect_names(card_id): conn.close() return rows +def search_owned_effects(search_term): + """ + Search for effects among owned cards. + Returns list of (card_id, card_name, image_path, effect_name, effect_value, level) + """ + conn = get_conn() + cur = conn.cursor() + + # We need to join support_effects with owned_cards to get the level + # But wait, owned_cards has a level column. support_effects stores effects for specific levels. + # So we need to match support_effects.level with owned_cards.level + # OR find the effect for the closest level <= owned level (if effects aren't stored for every single level) + # The current DB schema seems to store effects for specific levels (1, 5, 10, ...). + # If a card is level 49, `get_effects_at_level` usually queries for exact level match. + # Let's check `get_effects_at_level` implementation: "WHERE card_id = ? AND level = ?" + # So if I have a card at level 49, and effects are only defined at 45 and 50, query for 49 returns nothing? + # That would be a bug or assumption in the current app. + # Let's look at `update_progression_table` in `effects_view.py`. It does some "nearest level" logic. + # For this search feature, to be robust, we should probably fetch ALL effects for the card + # and filter for the one active at the owned level. + # OR, assuming the scraper/DB populates "current" effects effectively. + # Actually, the most robust way in SQL for "value at level X" given sparse data is complex. + # However, let's assume for now we want exact matches or we'll handle the "effective level" logic in Python? + # No, that's too slow for search. + # Let's look at how `get_effects_at_level` is used. + # It is used in `update_current_effects` with `self.level_var.get()`. + # It expects an exact match. + # So we should probably join on `oc.level`. + + query = """ + SELECT sc.card_id, sc.name, sc.image_path, se.effect_name, se.effect_value, oc.level + FROM owned_cards oc + JOIN support_cards sc ON oc.card_id = sc.card_id + JOIN support_effects se ON oc.card_id = se.card_id AND oc.level = se.level + WHERE se.effect_name LIKE ? + ORDER BY sc.name + """ + + cur.execute(query, (f"%{search_term}%",)) + rows = cur.fetchall() + conn.close() + return rows + # ============================================ # Hint Queries # ============================================ diff --git a/gui/effects_view.py b/gui/effects_view.py index 8bc6e94..cce628e 100644 --- a/gui/effects_view.py +++ b/gui/effects_view.py @@ -1,298 +1,139 @@ """ -Effects View - Display support effects at all levels with interactive slider +Effects Search View - Search for effects across all owned cards """ import tkinter as tk from tkinter import ttk, messagebox import sys import os +import re sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from db.db_queries import get_all_effects, get_effects_at_level, get_unique_effect_names, get_card_by_id +from db.db_queries import search_owned_effects from gui.theme import ( 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 + FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL, + create_styled_button, create_styled_entry ) - +from utils import resolve_image_path class EffectsFrame(ttk.Frame): - """Frame for viewing support effects at different levels""" + """Frame for searching effects across owned cards""" 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 + """Create the effects search interface""" + # Header / Search Bar 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) + # Search container + search_container = tk.Frame(header_frame, bg=BG_DARK) + search_container.pack(fill=tk.X) - # 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) + tk.Label(search_container, text="🔍 Search Effect:", + font=FONT_HEADER, bg=BG_DARK, fg=TEXT_PRIMARY).pack(side=tk.LEFT, padx=(0, 10)) - # Level control frame - control_frame = tk.Frame(self, bg=BG_MEDIUM, padx=15, pady=12) - control_frame.pack(fill=tk.X, padx=20) + self.search_var = tk.StringVar() + self.search_entry = create_styled_entry(search_container, textvariable=self.search_var) + self.search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10)) + self.search_entry.bind('', lambda e: self.perform_search()) - # Level label - tk.Label(control_frame, text="Level:", font=FONT_BODY, - bg=BG_MEDIUM, fg=TEXT_SECONDARY).pack(side=tk.LEFT) + search_btn = create_styled_button(search_container, text="Search", + command=self.perform_search, style_type='primary') + search_btn.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) + # Example/Help text + help_frame = tk.Frame(header_frame, bg=BG_DARK) + help_frame.pack(fill=tk.X, pady=(5, 0)) + tk.Label(help_frame, text="Examples: Friendship, Motivation, Race Bonus, Skill Pt", + font=FONT_SMALL, bg=BG_DARK, fg=TEXT_MUTED).pack(side=tk.LEFT) + + # Results Area + results_frame = ttk.LabelFrame(self, text=" Search Results (Owned Cards) ", padding=10) + results_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=(0, 20)) - # 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) + # Treeview + columns = ('card', 'level', 'current_value', 'effect_name') + self.tree = ttk.Treeview(results_frame, columns=columns, show='headings', selectmode='browse') - 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) + self.tree.heading('card', text='Card Name', anchor='w') + self.tree.heading('level', text='Level', anchor='center') + self.tree.heading('current_value', text='Value', anchor='center') + self.tree.heading('effect_name', text='Effect Name', anchor='w') - # 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) + self.tree.column('card', width=250) + self.tree.column('level', width=60, anchor='center') + self.tree.column('current_value', width=80, anchor='center') + self.tree.column('effect_name', width=150) - # Quick level buttons - button_frame = tk.Frame(control_frame, bg=BG_MEDIUM) - button_frame.pack(side=tk.LEFT, padx=25) + scrollbar = ttk.Scrollbar(results_frame, orient=tk.VERTICAL, command=self.tree.yview) + self.tree.configure(yscrollcommand=scrollbar.set) - quick_levels = [25, 30, 35, 40, 45, 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) + self.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) + # Status Label + self.status_label = tk.Label(results_frame, text="", bg=BG_MEDIUM, fg=TEXT_SECONDARY, font=FONT_SMALL) + self.status_label.pack(side=tk.BOTTOM, fill=tk.X, pady=(5, 0)) + def parse_value(self, value_str): + """Parse effect value string to float for sorting""" + try: + # Extract number from string (e.g. "20%" -> 20, "+15" -> 15) + # Remove non-numeric characters except . and - + clean = re.sub(r'[^\d.-]', '', str(value_str)) + return float(clean) + except: + return -999999.0 # Sort to bottom if invalid + + def perform_search(self): + """Execute search and update results""" + term = self.search_var.get().strip() + if not term: + messagebox.showwarning("Search", "Please enter a search term") + return + + # clear current + for item in self.tree.get_children(): + self.tree.delete(item) + + # Query DB + results = search_owned_effects(term) + + if not results: + self.status_label.config(text="No matching effects found among owned cards.") + return + + # Process and Sort + # Row: (card_id, card_name, image_path, effect_name, effect_value, level) + processed_results = [] + for r in results: + val_num = self.parse_value(r[4]) + processed_results.append({ + 'data': r, + 'sort_val': val_num + }) + + # Sort by value descending + processed_results.sort(key=lambda x: x['sort_val'], reverse=True) + + # Populate Tree + for item in processed_results: + r = item['data'] + #Columns: card, level, current_value, effect_name + values = (r[1], f"Lv {r[5]}", r[4], r[3]) + self.tree.insert('', tk.END, values=values) + + self.status_label.config(text=f"Found {len(processed_results)} owned cards with matching effects.") + + # Compatibility methods for main_window integration (empty as we don't need them anymore) 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, "", lambda e, n=name: self.show_effect_tooltip(e, n)) - self.current_effects.tag_bind(tag_name, "", 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) + pass - def show_effect_tooltip(self, event, effect_name): - """Show tooltip for effect""" - if effect_name in EFFECT_DESCRIPTIONS: - text = EFFECT_DESCRIPTIONS[effect_name] - x = event.x_root + 15 - y = event.y_root + 10 - - # Close existing if any - self.hide_effect_tooltip(None) - - self.tooltip_window = tk.Toplevel(self) - self.tooltip_window.wm_overrideredirect(True) - self.tooltip_window.wm_geometry(f"+{x}+{y}") - - label = tk.Label(self.tooltip_window, text=text, justify=tk.LEFT, - background=BG_LIGHT, foreground=TEXT_PRIMARY, - relief=tk.SOLID, borderwidth=1, font=FONT_SMALL, - padx=10, pady=5, wraplength=250) - label.pack() - - def hide_effect_tooltip(self, event): - """Hide tooltip""" - if hasattr(self, 'tooltip_window') and self.tooltip_window: - self.tooltip_window.destroy() - self.tooltip_window = None - - def update_progression_table(self): - """Update the effect progression table""" - self.effects_tree.delete(*self.effects_tree.get_children()) - - if not self.current_card_id: - return - - # Get all effects - all_effects = get_all_effects(self.current_card_id) - - # Organize by effect name - effect_by_level = {} - for level, effect_name, effect_value in all_effects: - if effect_name not in effect_by_level: - effect_by_level[effect_name] = {} - effect_by_level[effect_name][level] = effect_value - - # Key levels for the table - key_levels = [1, 25, 40, 50] - - # Add rows - for effect_name, levels in sorted(effect_by_level.items()): - row = [effect_name] - for lvl in key_levels: - # Find closest level we have data for - value = levels.get(lvl, '') - if not value: - # Try to find nearest - for l in sorted(levels.keys()): - if l <= lvl: - value = levels[l] - row.append(value) - - self.effects_tree.insert('', tk.END, values=row) diff --git a/gui/theme.py b/gui/theme.py index 1e669cf..a4a9c88 100644 --- a/gui/theme.py +++ b/gui/theme.py @@ -256,6 +256,17 @@ def configure_styles(root: tk.Tk): # WIDGET HELPER FUNCTIONS # ═══════════════════════════════════════════════════════════════════════════════ + +def create_styled_entry(parent, textvariable=None, **kwargs): + """Create a styled tk.Entry with modern appearance""" + entry = ttk.Entry( + parent, + textvariable=textvariable, + font=FONT_BODY, + **kwargs + ) + return entry + def create_styled_button(parent, text, command=None, style_type='default', **kwargs): """Create a styled tk.Button with modern appearance""" bg_colors = {