Changed Hints and Skills to Skill Search
This commit is contained in:
@@ -700,3 +700,102 @@ def get_database_stats():
|
||||
|
||||
conn.close()
|
||||
return stats
|
||||
|
||||
# ============================================
|
||||
# Skill Search Queries
|
||||
# ============================================
|
||||
|
||||
def get_all_unique_skills():
|
||||
"""Get a sorted list of all unique skills from hints and events"""
|
||||
conn = get_conn()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Get skills from hints
|
||||
cur.execute("SELECT DISTINCT hint_name FROM support_hints")
|
||||
hint_skills = {row[0] for row in cur.fetchall() if row[0]}
|
||||
|
||||
# Get skills from events
|
||||
cur.execute("SELECT DISTINCT skill_name FROM event_skills")
|
||||
event_skills = {row[0] for row in cur.fetchall() if row[0]}
|
||||
|
||||
# Combine and sort
|
||||
all_skills = sorted(list(hint_skills.union(event_skills)))
|
||||
|
||||
conn.close()
|
||||
return all_skills
|
||||
|
||||
def get_cards_with_skill(skill_name):
|
||||
"""
|
||||
Find all cards that have a specific skill.
|
||||
Returns list of dicts:
|
||||
{
|
||||
'card_id': int,
|
||||
'name': str,
|
||||
'rarity': str,
|
||||
'type': str,
|
||||
'image_path': str,
|
||||
'source': str ('Hint' or 'Event: [Name]'),
|
||||
'details': str (description or event name)
|
||||
}
|
||||
"""
|
||||
conn = get_conn()
|
||||
cur = conn.cursor()
|
||||
|
||||
results = []
|
||||
seen_entries = set() # To avoid duplicates if same skill in multiple events
|
||||
|
||||
# 1. Check Hints
|
||||
cur.execute("""
|
||||
SELECT sc.card_id, sc.name, sc.rarity, sc.card_type, sc.image_path, sh.hint_description
|
||||
FROM support_hints sh
|
||||
JOIN support_cards sc ON sh.card_id = sc.card_id
|
||||
WHERE sh.hint_name = ?
|
||||
""", (skill_name,))
|
||||
|
||||
for row in cur.fetchall():
|
||||
entry_key = (row[0], 'Hint')
|
||||
if entry_key not in seen_entries:
|
||||
results.append({
|
||||
'card_id': row[0],
|
||||
'name': row[1],
|
||||
'rarity': row[2],
|
||||
'type': row[3],
|
||||
'image_path': row[4],
|
||||
'source': 'Training Hint',
|
||||
'details': row[5] or "Random hint event"
|
||||
})
|
||||
seen_entries.add(entry_key)
|
||||
|
||||
# 2. Check Event Skills
|
||||
cur.execute("""
|
||||
SELECT sc.card_id, sc.name, sc.rarity, sc.card_type, sc.image_path, se.event_name
|
||||
FROM event_skills es
|
||||
JOIN support_events se ON es.event_id = se.event_id
|
||||
JOIN support_cards sc ON se.card_id = sc.card_id
|
||||
WHERE es.skill_name = ?
|
||||
""", (skill_name,))
|
||||
|
||||
for row in cur.fetchall():
|
||||
# Clean event name if it has newlines or excessive spaces
|
||||
event_name = row[5].replace('\n', ' ').strip()
|
||||
entry_key = (row[0], f'Event: {event_name}')
|
||||
|
||||
if entry_key not in seen_entries:
|
||||
results.append({
|
||||
'card_id': row[0],
|
||||
'name': row[1],
|
||||
'rarity': row[2],
|
||||
'type': row[3],
|
||||
'image_path': row[4],
|
||||
'source': 'Event',
|
||||
'details': event_name
|
||||
})
|
||||
seen_entries.add(entry_key)
|
||||
|
||||
conn.close()
|
||||
|
||||
# Sort by Rarity (SSR first), then Name
|
||||
rarity_map = {'SSR': 3, 'SR': 2, 'R': 1}
|
||||
results.sort(key=lambda x: (rarity_map.get(x['rarity'], 0), x['name']), reverse=True)
|
||||
|
||||
return results
|
||||
|
||||
@@ -1,180 +1,200 @@
|
||||
"""
|
||||
Hints and Skills View - Display support hints and event skills
|
||||
Skill Search View - Find cards by the skills they teach
|
||||
"""
|
||||
|
||||
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_hints, get_events, get_all_event_skills, get_card_by_id
|
||||
from db.db_queries import get_all_unique_skills, get_cards_with_skill
|
||||
from utils import resolve_image_path
|
||||
from gui.theme import (
|
||||
BG_DARK, BG_MEDIUM, BG_LIGHT,
|
||||
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_TERTIARY, ACCENT_SUCCESS,
|
||||
BG_DARK, BG_MEDIUM, BG_LIGHT, BG_HIGHLIGHT,
|
||||
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_TERTIARY,
|
||||
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
|
||||
FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL,
|
||||
create_styled_text, create_card_frame
|
||||
create_card_frame, get_type_icon, create_styled_button
|
||||
)
|
||||
|
||||
|
||||
class HintsSkillsFrame(ttk.Frame):
|
||||
"""Frame for viewing support hints and event skills"""
|
||||
class SkillSearchFrame(ttk.Frame):
|
||||
"""Frame for searching skills and finding cards that have them"""
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self.current_card_id = None
|
||||
self.current_card_name = None
|
||||
self.all_skills = []
|
||||
self.icon_cache = {}
|
||||
|
||||
self.create_widgets()
|
||||
self.load_skills()
|
||||
|
||||
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)
|
||||
"""Create the skill search interface"""
|
||||
# Main split container
|
||||
main_pane = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
|
||||
main_pane.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
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)
|
||||
# === Left Panel: Skill List ===
|
||||
left_frame = tk.Frame(main_pane, bg=BG_DARK, width=300)
|
||||
main_pane.add(left_frame, weight=1)
|
||||
|
||||
# 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,
|
||||
# Search Header
|
||||
header = tk.Frame(left_frame, bg=BG_DARK)
|
||||
header.pack(fill=tk.X, pady=(0, 10))
|
||||
tk.Label(header, text="🔍 Search Skills", font=FONT_HEADER,
|
||||
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)
|
||||
# Search Entry
|
||||
self.search_var = tk.StringVar()
|
||||
self.search_var.trace('w', self.filter_skills)
|
||||
|
||||
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)
|
||||
search_entry = ttk.Entry(left_frame, textvariable=self.search_var)
|
||||
search_entry.pack(fill=tk.X, padx=(0, 5), pady=(0, 10))
|
||||
|
||||
# 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)
|
||||
# Skill Listbox
|
||||
list_container = create_card_frame(left_frame)
|
||||
list_container.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 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)
|
||||
scrollbar = ttk.Scrollbar(list_container, orient=tk.VERTICAL)
|
||||
self.skill_listbox = tk.Listbox(list_container,
|
||||
bg=BG_MEDIUM, fg=TEXT_SECONDARY,
|
||||
selectbackground=ACCENT_PRIMARY,
|
||||
selectforeground=TEXT_PRIMARY,
|
||||
highlightthickness=0, bd=0,
|
||||
font=FONT_BODY,
|
||||
yscrollcommand=scrollbar.set)
|
||||
|
||||
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)
|
||||
scrollbar.config(command=self.skill_listbox.yview)
|
||||
|
||||
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)
|
||||
self.skill_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=2, pady=2)
|
||||
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.skill_listbox.bind('<<ListboxSelect>>', self.on_skill_selected)
|
||||
|
||||
self.summary_label = tk.Label(summary_frame, text="", font=FONT_SMALL,
|
||||
bg=BG_MEDIUM, fg=TEXT_SECONDARY)
|
||||
self.summary_label.pack()
|
||||
|
||||
# === Right Panel: Results ===
|
||||
right_frame = tk.Frame(main_pane, bg=BG_DARK)
|
||||
main_pane.add(right_frame, weight=3)
|
||||
|
||||
# Result Header
|
||||
self.result_header = tk.Label(right_frame, text="Select a skill to see who has it",
|
||||
font=FONT_HEADER, bg=BG_DARK, fg=ACCENT_TERTIARY)
|
||||
self.result_header.pack(anchor='w', pady=(0, 15), padx=10)
|
||||
|
||||
# Results Treeview
|
||||
tree_frame = create_card_frame(right_frame)
|
||||
tree_frame.pack(fill=tk.BOTH, expand=True, padx=10)
|
||||
|
||||
cols = ('name', 'rarity', 'type', 'source', 'details')
|
||||
self.tree = ttk.Treeview(tree_frame, columns=cols, show='tree headings',
|
||||
style="Treeview")
|
||||
|
||||
self.tree.heading('#0', text='')
|
||||
self.tree.heading('name', text='Card Name')
|
||||
self.tree.heading('rarity', text='Rarity')
|
||||
self.tree.heading('type', text='Type')
|
||||
self.tree.heading('source', text='Source')
|
||||
self.tree.heading('details', text='Details')
|
||||
|
||||
self.tree.column('#0', width=50, anchor='center')
|
||||
self.tree.column('name', width=180)
|
||||
self.tree.column('rarity', width=50, anchor='center')
|
||||
self.tree.column('type', width=80, anchor='center')
|
||||
self.tree.column('source', width=100)
|
||||
self.tree.column('details', width=250)
|
||||
|
||||
vsb = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=self.tree.yview)
|
||||
self.tree.configure(yscrollcommand=vsb.set)
|
||||
|
||||
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=2, pady=2)
|
||||
vsb.pack(side=tk.RIGHT, fill=tk.Y, pady=2)
|
||||
|
||||
# Stats footer
|
||||
self.stats_label = tk.Label(right_frame, text="", font=FONT_SMALL,
|
||||
bg=BG_DARK, fg=TEXT_MUTED)
|
||||
self.stats_label.pack(anchor='e', pady=5, padx=10)
|
||||
|
||||
def load_skills(self):
|
||||
"""Load all unique skills into listbox"""
|
||||
self.all_skills = get_all_unique_skills()
|
||||
self.update_listbox(self.all_skills)
|
||||
|
||||
def update_listbox(self, items):
|
||||
"""Update listbox content"""
|
||||
self.skill_listbox.delete(0, tk.END)
|
||||
for item in items:
|
||||
self.skill_listbox.insert(tk.END, item)
|
||||
|
||||
def filter_skills(self, *args):
|
||||
"""Filter skills based on search text"""
|
||||
search = self.search_var.get().lower()
|
||||
if not search:
|
||||
self.update_listbox(self.all_skills)
|
||||
return
|
||||
|
||||
filtered = [s for s in self.all_skills if search in s.lower()]
|
||||
self.update_listbox(filtered)
|
||||
|
||||
def on_skill_selected(self, event):
|
||||
"""Handle skill selection"""
|
||||
selection = self.skill_listbox.curselection()
|
||||
if not selection:
|
||||
return
|
||||
|
||||
skill_name = self.skill_listbox.get(selection[0])
|
||||
self.show_cards_for_skill(skill_name)
|
||||
|
||||
def show_cards_for_skill(self, skill_name):
|
||||
"""Fetch and display cards with the selected skill"""
|
||||
self.result_header.config(text=f"Cards with skill: {skill_name}")
|
||||
|
||||
# Clear tree
|
||||
for item in self.tree.get_children():
|
||||
self.tree.delete(item)
|
||||
|
||||
cards = get_cards_with_skill(skill_name)
|
||||
|
||||
for card in cards:
|
||||
# Load Icon
|
||||
card_id = card['card_id']
|
||||
img = self.icon_cache.get(card_id)
|
||||
if not img:
|
||||
resolved_path = resolve_image_path(card['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
|
||||
|
||||
type_display = f"{get_type_icon(card['type'])} {card['type']}"
|
||||
|
||||
values = (
|
||||
card['name'],
|
||||
card['rarity'],
|
||||
type_display,
|
||||
card['source'],
|
||||
card['details']
|
||||
)
|
||||
|
||||
if img:
|
||||
self.tree.insert('', tk.END, text='', image=img, values=values)
|
||||
else:
|
||||
self.tree.insert('', tk.END, text='?', values=values)
|
||||
|
||||
self.stats_label.config(text=f"Found {len(cards)} cards")
|
||||
|
||||
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"
|
||||
)
|
||||
"""
|
||||
Legacy method compatibility for main window calls.
|
||||
We don't need to do anything here since we are skill-centric now.
|
||||
But main_window might call it when card is selected in tab 1.
|
||||
We could potentially auto-search a skill from that card?
|
||||
For now, just ignore it.
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -13,7 +13,7 @@ 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.hints_skills_view import SkillSearchFrame
|
||||
from gui.deck_builder import DeckBuilderFrame
|
||||
from gui.update_dialog import show_update_dialog
|
||||
from gui.theme import (
|
||||
@@ -33,7 +33,7 @@ class MainWindow:
|
||||
def __init__(self):
|
||||
self.root = tk.Tk()
|
||||
self.root.title("Umamusume Support Card Manager")
|
||||
self.root.geometry("1350x800")
|
||||
self.root.geometry("1400x850") # Slightly larger for new UI
|
||||
self.root.minsize(1350, 800)
|
||||
|
||||
# Set icon
|
||||
@@ -157,9 +157,9 @@ class MainWindow:
|
||||
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 ")
|
||||
# Skill Search Tab (Replaces Hints & Skills)
|
||||
self.hints_frame = SkillSearchFrame(self.notebook)
|
||||
self.notebook.add(self.hints_frame, text=" 🔍 Skill Search ")
|
||||
|
||||
def create_status_bar(self, parent):
|
||||
"""Create status bar at bottom"""
|
||||
|
||||
Reference in New Issue
Block a user