feat: Add new GUI effects view and numerous image assets.
67
README.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# Umamusume Support Card Manager
|
||||||
|
|
||||||
|
A tool for managing support cards and their effects in Umamusume (Granblue Fantasy Relink).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Web scraping of support card data from GameTora
|
||||||
|
- Database storage of card information including effects at different levels
|
||||||
|
- GUI application for viewing and managing support cards
|
||||||
|
- Deck building functionality
|
||||||
|
- Character art downloading
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── main.py # Main entry point
|
||||||
|
├── version.py # Version information
|
||||||
|
├── requirements.txt # Python dependencies
|
||||||
|
├── scraper/ # Web scraping modules
|
||||||
|
│ └── gametora_scraper.py # GameTora scraper implementation
|
||||||
|
├── db/ # Database modules
|
||||||
|
│ ├── db_init.py # Database initialization
|
||||||
|
│ └── db_queries.py # Database queries
|
||||||
|
├── gui/ # GUI components
|
||||||
|
├── updater/ # Update checking functionality
|
||||||
|
├── database/ # Database files
|
||||||
|
├── images/ # Character art images
|
||||||
|
├── build/ # Build artifacts
|
||||||
|
└── dist/ # Distribution files
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Clone the repository
|
||||||
|
2. Install dependencies: `pip install -r requirements.txt`
|
||||||
|
3. Run the application: `python main.py`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### GUI Mode (default)
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scraping Mode
|
||||||
|
```bash
|
||||||
|
python main.py --scrape
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Code Structure
|
||||||
|
- `main.py`: Entry point and argument parsing
|
||||||
|
- `scraper/gametora_scraper.py`: Web scraping logic
|
||||||
|
- `db/db_init.py`: Database schema initialization
|
||||||
|
- `gui/`: GUI components (MainWindow, views, etc.)
|
||||||
|
- `updater/update_checker.py`: Update checking functionality
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Make your changes
|
||||||
|
4. Submit a pull request
|
||||||
|
|
||||||
|
## License
|
||||||
|
MIT
|
||||||
0
config/__init__.py
Normal file
35
config/settings.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
"""
|
||||||
|
Application settings and configuration
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Base directory of the application
|
||||||
|
BASE_DIR = Path(__file__).parent.parent
|
||||||
|
|
||||||
|
# Database configuration
|
||||||
|
DATABASE_PATH = BASE_DIR / "database" / "umamusume.db"
|
||||||
|
DATABASE_SEED_PATH = BASE_DIR / "database" / "umamusume_seed.db"
|
||||||
|
|
||||||
|
# Image directory
|
||||||
|
IMAGES_DIR = BASE_DIR / "images"
|
||||||
|
|
||||||
|
# Application settings
|
||||||
|
APP_NAME = "UmamusumeCardManager"
|
||||||
|
VERSION = "7.0.0"
|
||||||
|
|
||||||
|
# Scraping settings
|
||||||
|
SCRAPING_DELAY_MIN = 0.2
|
||||||
|
SCRAPING_DELAY_MAX = 0.5
|
||||||
|
MAX_RETRIES = 3
|
||||||
|
|
||||||
|
# Browser settings
|
||||||
|
HEADLESS_MODE = True
|
||||||
|
BROWSER_TIMEOUT = 60000
|
||||||
|
|
||||||
|
# Database schema version
|
||||||
|
DB_SCHEMA_VERSION = "1.0"
|
||||||
|
"""
|
||||||
|
Database schema version for migration tracking
|
||||||
|
"""
|
||||||
@@ -33,6 +33,10 @@ def init_db(reset=False):
|
|||||||
cur.execute("DROP TABLE IF EXISTS support_effects")
|
cur.execute("DROP TABLE IF EXISTS support_effects")
|
||||||
cur.execute("DROP TABLE IF EXISTS owned_cards")
|
cur.execute("DROP TABLE IF EXISTS owned_cards")
|
||||||
cur.execute("DROP TABLE IF EXISTS support_cards")
|
cur.execute("DROP TABLE IF EXISTS support_cards")
|
||||||
|
else:
|
||||||
|
# Run migrations for existing database
|
||||||
|
migrate_add_image_path()
|
||||||
|
migrate_event_skills_columns()
|
||||||
|
|
||||||
# Support Cards - main card info
|
# Support Cards - main card info
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
@@ -87,6 +91,8 @@ def init_db(reset=False):
|
|||||||
skill_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
skill_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
event_id INTEGER,
|
event_id INTEGER,
|
||||||
skill_name TEXT,
|
skill_name TEXT,
|
||||||
|
is_gold INTEGER DEFAULT 0,
|
||||||
|
is_or INTEGER DEFAULT 0,
|
||||||
FOREIGN KEY (event_id) REFERENCES support_events(event_id)
|
FOREIGN KEY (event_id) REFERENCES support_events(event_id)
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
@@ -150,5 +156,24 @@ def migrate_add_image_path():
|
|||||||
pass # Column already exists
|
pass # Column already exists
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
def migrate_event_skills_columns():
|
||||||
|
"""Add is_gold and is_or columns to event_skills if they don't exist"""
|
||||||
|
conn = get_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
try:
|
||||||
|
cur.execute("ALTER TABLE event_skills ADD COLUMN is_gold INTEGER DEFAULT 0")
|
||||||
|
print("Added is_gold column to event_skills")
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
pass # Column already exists
|
||||||
|
|
||||||
|
try:
|
||||||
|
cur.execute("ALTER TABLE event_skills ADD COLUMN is_or INTEGER DEFAULT 0")
|
||||||
|
print("Added is_or column to event_skills")
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
pass # Column already exists
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
init_db(reset=True)
|
init_db(reset=True)
|
||||||
|
|||||||
@@ -780,7 +780,7 @@ def cleanup_orphaned_data():
|
|||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
def get_all_unique_skills():
|
def get_all_unique_skills():
|
||||||
"""Get a sorted list of all unique skills from hints and events"""
|
"""Get a sorted list of all unique skills from hints and events, with golden indicator"""
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|
||||||
@@ -788,12 +788,22 @@ def get_all_unique_skills():
|
|||||||
cur.execute("SELECT DISTINCT hint_name FROM support_hints")
|
cur.execute("SELECT DISTINCT hint_name FROM support_hints")
|
||||||
hint_skills = {row[0] for row in cur.fetchall() if row[0]}
|
hint_skills = {row[0] for row in cur.fetchall() if row[0]}
|
||||||
|
|
||||||
# Get skills from events
|
# Get skills from events, marking which are golden
|
||||||
cur.execute("SELECT DISTINCT skill_name FROM event_skills")
|
cur.execute("SELECT DISTINCT skill_name, MAX(is_gold) as is_gold FROM event_skills GROUP BY skill_name")
|
||||||
event_skills = {row[0] for row in cur.fetchall() if row[0]}
|
event_skills_data = cur.fetchall()
|
||||||
|
event_skills = {}
|
||||||
|
for skill_name, is_gold in event_skills_data:
|
||||||
|
if skill_name:
|
||||||
|
event_skills[skill_name] = bool(is_gold)
|
||||||
|
|
||||||
# Combine and sort
|
# Combine and mark golden skills
|
||||||
all_skills = sorted(list(hint_skills.union(event_skills)))
|
all_skills = []
|
||||||
|
for skill in sorted(list(hint_skills.union(event_skills.keys()))):
|
||||||
|
if skill in event_skills and event_skills[skill]:
|
||||||
|
# Mark as golden
|
||||||
|
all_skills.append((skill, True)) # (skill_name, is_golden)
|
||||||
|
else:
|
||||||
|
all_skills.append((skill, False))
|
||||||
|
|
||||||
conn.close()
|
conn.close()
|
||||||
return all_skills
|
return all_skills
|
||||||
@@ -833,9 +843,10 @@ def get_cards_with_skill(skill_name):
|
|||||||
if entry_key not in seen_entries:
|
if entry_key not in seen_entries:
|
||||||
results.append({
|
results.append({
|
||||||
'card_id': row[0],
|
'card_id': row[0],
|
||||||
'name': row[1],
|
'name': row[1] or 'Unknown',
|
||||||
'rarity': row[2],
|
'rarity': row[2] or 'Unknown',
|
||||||
'type': row[3],
|
'type': row[3] or 'Unknown', # Also include 'card_type' for compatibility
|
||||||
|
'card_type': row[3] or 'Unknown',
|
||||||
'image_path': row[4],
|
'image_path': row[4],
|
||||||
'source': 'Training Hint',
|
'source': 'Training Hint',
|
||||||
'details': row[5] or "Random hint event",
|
'details': row[5] or "Random hint event",
|
||||||
@@ -843,10 +854,11 @@ def get_cards_with_skill(skill_name):
|
|||||||
})
|
})
|
||||||
seen_entries.add(entry_key)
|
seen_entries.add(entry_key)
|
||||||
|
|
||||||
# 2. Check Event Skills
|
# 2. Check Event Skills (including golden perks)
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT sc.card_id, sc.name, sc.rarity, sc.card_type, sc.image_path, se.event_name, se.event_id,
|
SELECT sc.card_id, sc.name, sc.rarity, sc.card_type, sc.image_path, se.event_name, se.event_id,
|
||||||
CASE WHEN oc.card_id IS NOT NULL THEN 1 ELSE 0 END as is_owned
|
CASE WHEN oc.card_id IS NOT NULL THEN 1 ELSE 0 END as is_owned,
|
||||||
|
es.is_gold
|
||||||
FROM event_skills es
|
FROM event_skills es
|
||||||
JOIN support_events se ON es.event_id = se.event_id
|
JOIN support_events se ON es.event_id = se.event_id
|
||||||
JOIN support_cards sc ON se.card_id = sc.card_id
|
JOIN support_cards sc ON se.card_id = sc.card_id
|
||||||
@@ -856,7 +868,7 @@ def get_cards_with_skill(skill_name):
|
|||||||
|
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
for row in rows:
|
for row in rows:
|
||||||
card_id, name, rarity, card_type, image_path, event_name, event_id, is_owned = row
|
card_id, name, rarity, card_type, image_path, event_name, event_id, is_owned, is_gold = row
|
||||||
event_name = event_name.replace('\n', ' ').strip()
|
event_name = event_name.replace('\n', ' ').strip()
|
||||||
|
|
||||||
# Format event skills (handle OR groups and gold skills)
|
# Format event skills (handle OR groups and gold skills)
|
||||||
@@ -884,20 +896,30 @@ def get_cards_with_skill(skill_name):
|
|||||||
formatted_event_skills.extend(other_event_skills)
|
formatted_event_skills.extend(other_event_skills)
|
||||||
|
|
||||||
# Create a nice string like "Event Name (Skill1, Skill2)"
|
# Create a nice string like "Event Name (Skill1, Skill2)"
|
||||||
details = f"{event_name} ({', '.join(formatted_event_skills)})" if formatted_event_skills else event_name
|
if formatted_event_skills:
|
||||||
|
details = f"{event_name} ({', '.join(formatted_event_skills)})"
|
||||||
|
elif event_name:
|
||||||
|
details = f"{event_name} (Golden Perk)"
|
||||||
|
else:
|
||||||
|
details = "Golden Perk Event"
|
||||||
|
|
||||||
|
# Mark source as GOLDEN if this is a golden skill
|
||||||
|
source = "✨ GOLDEN Event" if is_gold else "Event"
|
||||||
|
|
||||||
entry_key = (card_id, f'Event: {event_name}')
|
entry_key = (card_id, f'Event: {event_name}')
|
||||||
|
|
||||||
if entry_key not in seen_entries:
|
if entry_key not in seen_entries:
|
||||||
results.append({
|
results.append({
|
||||||
'card_id': card_id,
|
'card_id': card_id,
|
||||||
'name': name,
|
'name': name or 'Unknown',
|
||||||
'rarity': rarity,
|
'rarity': rarity or 'Unknown',
|
||||||
'type': card_type,
|
'type': card_type or 'Unknown', # Also include 'card_type' for compatibility
|
||||||
|
'card_type': card_type or 'Unknown',
|
||||||
'image_path': image_path,
|
'image_path': image_path,
|
||||||
'source': 'Event',
|
'source': source,
|
||||||
'details': details,
|
'details': details or 'No details available',
|
||||||
'is_owned': bool(is_owned)
|
'is_owned': bool(is_owned),
|
||||||
|
'is_gold': bool(is_gold)
|
||||||
})
|
})
|
||||||
seen_entries.add(entry_key)
|
seen_entries.add(entry_key)
|
||||||
|
|
||||||
|
|||||||
@@ -331,7 +331,7 @@ class CardListFrame(ttk.Frame):
|
|||||||
if resolved_path and os.path.exists(resolved_path):
|
if resolved_path and os.path.exists(resolved_path):
|
||||||
try:
|
try:
|
||||||
pil_img = Image.open(resolved_path)
|
pil_img = Image.open(resolved_path)
|
||||||
pil_img.thumbnail((32, 32), Image.Resampling.LANCZOS)
|
pil_img.thumbnail((48, 48), Image.Resampling.LANCZOS)
|
||||||
img = ImageTk.PhotoImage(pil_img)
|
img = ImageTk.PhotoImage(pil_img)
|
||||||
self.icon_cache[card_id] = img
|
self.icon_cache[card_id] = img
|
||||||
except:
|
except:
|
||||||
|
|||||||
@@ -49,13 +49,13 @@ class CardSlot(tk.Frame):
|
|||||||
|
|
||||||
# Image Area (Left)
|
# Image Area (Left)
|
||||||
self.image_label = tk.Label(self, bg=BG_MEDIUM, text="📭", fg=TEXT_MUTED,
|
self.image_label = tk.Label(self, bg=BG_MEDIUM, text="📭", fg=TEXT_MUTED,
|
||||||
font=('Segoe UI', 20))
|
font=('Segoe UI', 32))
|
||||||
self.image_label.grid(row=0, column=0, rowspan=3, padx=8, pady=8)
|
self.image_label.grid(row=0, column=0, rowspan=3, padx=12, pady=12)
|
||||||
|
|
||||||
# Details Area (Right)
|
# Details Area (Right)
|
||||||
self.name_label = tk.Label(self, text="Empty Slot", bg=BG_MEDIUM, fg=TEXT_MUTED,
|
self.name_label = tk.Label(self, text="Empty Slot", bg=BG_MEDIUM, fg=TEXT_PRIMARY,
|
||||||
font=FONT_BODY_BOLD, anchor='w', wraplength=130)
|
font=FONT_BODY_BOLD, anchor='w', wraplength=180) # Increased wrap
|
||||||
self.name_label.grid(row=0, column=1, sticky='w', padx=8, pady=(10, 0))
|
self.name_label.grid(row=0, column=1, sticky='w', padx=8, pady=(15, 0))
|
||||||
|
|
||||||
self.meta_label = tk.Label(self, text="", bg=BG_MEDIUM, fg=TEXT_MUTED,
|
self.meta_label = tk.Label(self, text="", bg=BG_MEDIUM, fg=TEXT_MUTED,
|
||||||
font=FONT_SMALL, anchor='w')
|
font=FONT_SMALL, anchor='w')
|
||||||
@@ -139,7 +139,7 @@ class CardSlot(tk.Frame):
|
|||||||
def reset(self):
|
def reset(self):
|
||||||
self.name_label.config(text="Empty Slot", fg=TEXT_MUTED)
|
self.name_label.config(text="Empty Slot", fg=TEXT_MUTED)
|
||||||
self.meta_label.config(text="Click a card to add")
|
self.meta_label.config(text="Click a card to add")
|
||||||
self.image_label.config(image='', text="📭", font=('Segoe UI', 20))
|
self.image_label.config(image='', text="📭", font=('Segoe UI', 32))
|
||||||
self.config(highlightbackground=BG_LIGHT)
|
self.config(highlightbackground=BG_LIGHT)
|
||||||
self.image_ref = None
|
self.image_ref = None
|
||||||
self.toggle_controls(False)
|
self.toggle_controls(False)
|
||||||
@@ -149,7 +149,8 @@ class CardSlot(tk.Frame):
|
|||||||
if resolved_path and os.path.exists(resolved_path):
|
if resolved_path and os.path.exists(resolved_path):
|
||||||
try:
|
try:
|
||||||
pil_img = Image.open(resolved_path)
|
pil_img = Image.open(resolved_path)
|
||||||
pil_img.thumbnail((65, 65), Image.Resampling.LANCZOS)
|
# Significantly larger images as requested (120x120)
|
||||||
|
pil_img.thumbnail((120, 120), Image.Resampling.LANCZOS)
|
||||||
self.image_ref = ImageTk.PhotoImage(pil_img)
|
self.image_ref = ImageTk.PhotoImage(pil_img)
|
||||||
self.image_label.config(image=self.image_ref, text='')
|
self.image_label.config(image=self.image_ref, text='')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -369,7 +370,8 @@ class DeckBuilderFrame(ttk.Frame):
|
|||||||
if not img and resolved_path and os.path.exists(resolved_path):
|
if not img and resolved_path and os.path.exists(resolved_path):
|
||||||
try:
|
try:
|
||||||
pil_img = Image.open(resolved_path)
|
pil_img = Image.open(resolved_path)
|
||||||
pil_img.thumbnail((32, 32), Image.Resampling.LANCZOS)
|
# Larger thumbnails in the list too (48x48)
|
||||||
|
pil_img.thumbnail((48, 48), Image.Resampling.LANCZOS)
|
||||||
img = ImageTk.PhotoImage(pil_img)
|
img = ImageTk.PhotoImage(pil_img)
|
||||||
self.icon_cache[card_id] = img
|
self.icon_cache[card_id] = img
|
||||||
except:
|
except:
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ class DeckSkillsFrame(ttk.Frame):
|
|||||||
if resolved_path and os.path.exists(resolved_path):
|
if resolved_path and os.path.exists(resolved_path):
|
||||||
try:
|
try:
|
||||||
pil_img = Image.open(resolved_path)
|
pil_img = Image.open(resolved_path)
|
||||||
pil_img.thumbnail((32, 32), Image.Resampling.LANCZOS)
|
pil_img.thumbnail((48, 48), Image.Resampling.LANCZOS)
|
||||||
img = ImageTk.PhotoImage(pil_img)
|
img = ImageTk.PhotoImage(pil_img)
|
||||||
self.icon_cache[card_id] = img
|
self.icon_cache[card_id] = img
|
||||||
except: pass
|
except: pass
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ class EffectsFrame(ttk.Frame):
|
|||||||
button_frame = tk.Frame(control_frame, bg=BG_MEDIUM)
|
button_frame = tk.Frame(control_frame, bg=BG_MEDIUM)
|
||||||
button_frame.pack(side=tk.LEFT, padx=25)
|
button_frame.pack(side=tk.LEFT, padx=25)
|
||||||
|
|
||||||
quick_levels = [1, 25, 40, 50]
|
quick_levels = [25, 30, 35, 40, 45, 50]
|
||||||
for lvl in quick_levels:
|
for lvl in quick_levels:
|
||||||
btn = create_styled_button(button_frame, text=f"Lv{lvl}",
|
btn = create_styled_button(button_frame, text=f"Lv{lvl}",
|
||||||
command=lambda l=lvl: self.set_level(l),
|
command=lambda l=lvl: self.set_level(l),
|
||||||
|
|||||||
@@ -132,13 +132,22 @@ class SkillSearchFrame(ttk.Frame):
|
|||||||
|
|
||||||
def load_skills(self):
|
def load_skills(self):
|
||||||
"""Load all unique skills into listbox"""
|
"""Load all unique skills into listbox"""
|
||||||
self.all_skills = get_all_unique_skills()
|
skills_data = get_all_unique_skills()
|
||||||
self.update_listbox(self.all_skills)
|
# Store as list of (skill_name, is_golden) tuples
|
||||||
|
self.all_skills = skills_data
|
||||||
|
self.update_listbox(skills_data)
|
||||||
|
|
||||||
def update_listbox(self, items):
|
def update_listbox(self, items):
|
||||||
"""Update listbox content"""
|
"""Update listbox content"""
|
||||||
self.skill_listbox.delete(0, tk.END)
|
self.skill_listbox.delete(0, tk.END)
|
||||||
for item in items:
|
for item in items:
|
||||||
|
if isinstance(item, tuple):
|
||||||
|
skill_name, is_golden = item
|
||||||
|
# Display with golden indicator
|
||||||
|
display_name = f"✨ GOLDEN {skill_name}" if is_golden else skill_name
|
||||||
|
self.skill_listbox.insert(tk.END, display_name)
|
||||||
|
else:
|
||||||
|
# Backward compatibility
|
||||||
self.skill_listbox.insert(tk.END, item)
|
self.skill_listbox.insert(tk.END, item)
|
||||||
|
|
||||||
def filter_skills(self, *args):
|
def filter_skills(self, *args):
|
||||||
@@ -148,7 +157,17 @@ class SkillSearchFrame(ttk.Frame):
|
|||||||
self.update_listbox(self.all_skills)
|
self.update_listbox(self.all_skills)
|
||||||
return
|
return
|
||||||
|
|
||||||
filtered = [s for s in self.all_skills if search in s.lower()]
|
# Filter skills - handle both tuple format and string format
|
||||||
|
filtered = []
|
||||||
|
for item in self.all_skills:
|
||||||
|
if isinstance(item, tuple):
|
||||||
|
skill_name, is_golden = item
|
||||||
|
if search in skill_name.lower() or (search == "golden" and is_golden):
|
||||||
|
filtered.append(item)
|
||||||
|
else:
|
||||||
|
if search in item.lower():
|
||||||
|
filtered.append(item)
|
||||||
|
|
||||||
self.update_listbox(filtered)
|
self.update_listbox(filtered)
|
||||||
|
|
||||||
def on_filter_changed(self):
|
def on_filter_changed(self):
|
||||||
@@ -162,7 +181,13 @@ class SkillSearchFrame(ttk.Frame):
|
|||||||
if not selection:
|
if not selection:
|
||||||
return
|
return
|
||||||
|
|
||||||
skill_name = self.skill_listbox.get(selection[0])
|
display_name = self.skill_listbox.get(selection[0])
|
||||||
|
# Extract actual skill name (remove "✨ GOLDEN " prefix if present)
|
||||||
|
if display_name.startswith("✨ GOLDEN "):
|
||||||
|
skill_name = display_name.replace("✨ GOLDEN ", "", 1)
|
||||||
|
else:
|
||||||
|
skill_name = display_name
|
||||||
|
|
||||||
self.current_skill = skill_name
|
self.current_skill = skill_name
|
||||||
self.show_cards_for_skill(skill_name)
|
self.show_cards_for_skill(skill_name)
|
||||||
|
|
||||||
@@ -191,22 +216,34 @@ class SkillSearchFrame(ttk.Frame):
|
|||||||
if resolved_path and os.path.exists(resolved_path):
|
if resolved_path and os.path.exists(resolved_path):
|
||||||
try:
|
try:
|
||||||
pil_img = Image.open(resolved_path)
|
pil_img = Image.open(resolved_path)
|
||||||
pil_img.thumbnail((32, 32), Image.Resampling.LANCZOS)
|
pil_img.thumbnail((48, 48), Image.Resampling.LANCZOS)
|
||||||
img = ImageTk.PhotoImage(pil_img)
|
img = ImageTk.PhotoImage(pil_img)
|
||||||
self.icon_cache[card_id] = img
|
self.icon_cache[card_id] = img
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
type_display = f"{get_type_icon(card['type'])} {card['type']}"
|
# Handle both 'type' and 'card_type' keys for compatibility
|
||||||
|
card_type = card.get('type') or card.get('card_type') or 'Unknown'
|
||||||
|
type_display = f"{get_type_icon(card_type)} {card_type}"
|
||||||
owned_mark = "★" if card.get('is_owned') else ""
|
owned_mark = "★" if card.get('is_owned') else ""
|
||||||
|
|
||||||
|
# Highlight golden skills in source column
|
||||||
|
source = card.get('source', 'Event')
|
||||||
|
if card.get('is_gold', False):
|
||||||
|
source = f"✨ GOLDEN {source.replace('✨ GOLDEN ', '')}" # Ensure no double prefix
|
||||||
|
|
||||||
|
# Handle potential None values
|
||||||
|
card_name = card.get('name') or 'Unknown'
|
||||||
|
card_rarity = card.get('rarity') or 'Unknown'
|
||||||
|
card_details = card.get('details') or 'No details available'
|
||||||
|
|
||||||
values = (
|
values = (
|
||||||
owned_mark,
|
owned_mark,
|
||||||
card['name'],
|
card_name,
|
||||||
card['rarity'],
|
card_rarity,
|
||||||
type_display,
|
type_display,
|
||||||
card['source'],
|
source,
|
||||||
card['details']
|
card_details
|
||||||
)
|
)
|
||||||
|
|
||||||
if img:
|
if img:
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class MainWindow:
|
|||||||
self.root.geometry("1400x850")
|
self.root.geometry("1400x850")
|
||||||
self.root.minsize(1350, 800)
|
self.root.minsize(1350, 800)
|
||||||
|
|
||||||
|
|
||||||
# Set icon
|
# Set icon
|
||||||
try:
|
try:
|
||||||
icon_path = resolve_image_path("1_Special Week.png")
|
icon_path = resolve_image_path("1_Special Week.png")
|
||||||
@@ -240,7 +241,9 @@ class MainWindow:
|
|||||||
show_update_dialog(self.root)
|
show_update_dialog(self.root)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Start the application"""
|
"""
|
||||||
|
Start the GUI application and display the main window.
|
||||||
|
"""
|
||||||
self.root.mainloop()
|
self.root.mainloop()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
12
gui/theme.py
@@ -209,7 +209,7 @@ def configure_styles(root: tk.Tk):
|
|||||||
foreground=TEXT_SECONDARY,
|
foreground=TEXT_SECONDARY,
|
||||||
fieldbackground=BG_MEDIUM,
|
fieldbackground=BG_MEDIUM,
|
||||||
font=FONT_BODY,
|
font=FONT_BODY,
|
||||||
rowheight=40)
|
rowheight=60)
|
||||||
|
|
||||||
# Deck list style
|
# Deck list style
|
||||||
style.configure('DeckList.Treeview',
|
style.configure('DeckList.Treeview',
|
||||||
@@ -217,7 +217,7 @@ def configure_styles(root: tk.Tk):
|
|||||||
foreground=TEXT_SECONDARY,
|
foreground=TEXT_SECONDARY,
|
||||||
fieldbackground=BG_MEDIUM,
|
fieldbackground=BG_MEDIUM,
|
||||||
font=FONT_BODY,
|
font=FONT_BODY,
|
||||||
rowheight=40)
|
rowheight=60)
|
||||||
style.map('DeckList.Treeview',
|
style.map('DeckList.Treeview',
|
||||||
background=[('selected', ACCENT_PRIMARY)])
|
background=[('selected', ACCENT_PRIMARY)])
|
||||||
|
|
||||||
@@ -231,14 +231,6 @@ def configure_styles(root: tk.Tk):
|
|||||||
style.configure('Horizontal.TScale',
|
style.configure('Horizontal.TScale',
|
||||||
background=BG_DARK)
|
background=BG_DARK)
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
# Progressbar styles
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
style.configure('TProgressbar',
|
|
||||||
background=ACCENT_PRIMARY,
|
|
||||||
troughcolor=BG_MEDIUM,
|
|
||||||
borderwidth=0,
|
|
||||||
thickness=8)
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
# Scrollbar styles
|
# Scrollbar styles
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |