You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
450 lines
18 KiB
450 lines
18 KiB
|
1 year ago
|
import sys
|
||
|
|
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QListWidget, QLineEdit,
|
||
|
|
QLabel, QCheckBox, QPushButton, QFormLayout, QListWidgetItem, QSplitter, QTreeWidget,
|
||
|
|
QTreeWidgetItem, QDialog, QDialogButtonBox, QComboBox, QMessageBox)
|
||
|
|
from PyQt6.QtCore import Qt, QSize
|
||
|
|
from PyQt6.QtGui import QPixmap, QFontMetrics, QColor
|
||
|
|
import sqlite3
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
|
||
|
|
class EvolutionEditDialog(QDialog):
|
||
|
|
def __init__(self, parent=None, from_pfic=None, to_pfic=None, method=None):
|
||
|
|
super().__init__(parent)
|
||
|
|
self.setWindowTitle("Edit Evolution")
|
||
|
|
self.setModal(True)
|
||
|
|
|
||
|
|
layout = QVBoxLayout(self)
|
||
|
|
|
||
|
|
self.from_combo = QComboBox()
|
||
|
|
self.to_combo = QComboBox()
|
||
|
|
self.method_edit = QLineEdit(method)
|
||
|
|
|
||
|
|
layout.addWidget(QLabel("From Pokémon:"))
|
||
|
|
layout.addWidget(self.from_combo)
|
||
|
|
layout.addWidget(QLabel("To Pokémon:"))
|
||
|
|
layout.addWidget(self.to_combo)
|
||
|
|
layout.addWidget(QLabel("Evolution Method:"))
|
||
|
|
layout.addWidget(self.method_edit)
|
||
|
|
|
||
|
|
self.button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
||
|
|
self.button_box.accepted.connect(self.accept)
|
||
|
|
self.button_box.rejected.connect(self.reject)
|
||
|
|
layout.addWidget(self.button_box)
|
||
|
|
|
||
|
|
self.delete_button = QPushButton("Delete Evolution")
|
||
|
|
self.delete_button.clicked.connect(self.delete_evolution)
|
||
|
|
layout.addWidget(self.delete_button)
|
||
|
|
|
||
|
|
self.populate_combos(from_pfic, to_pfic)
|
||
|
|
|
||
|
|
def populate_combos(self, from_pfic, to_pfic):
|
||
|
|
cursor = self.parent().cursor
|
||
|
|
cursor.execute('SELECT PFIC, name, form_name FROM pokemon_forms ORDER BY name, form_name')
|
||
|
|
for pfic, name, form_name in cursor.fetchall():
|
||
|
|
display_name = f"{name} ({form_name})" if form_name else name
|
||
|
|
self.from_combo.addItem(display_name, pfic)
|
||
|
|
self.to_combo.addItem(display_name, pfic)
|
||
|
|
|
||
|
|
if from_pfic:
|
||
|
|
self.from_combo.setCurrentIndex(self.from_combo.findData(from_pfic))
|
||
|
|
if to_pfic:
|
||
|
|
self.to_combo.setCurrentIndex(self.to_combo.findData(to_pfic))
|
||
|
|
|
||
|
|
def delete_evolution(self):
|
||
|
|
self.done(2) # Use a custom return code for delete action
|
||
|
|
|
||
|
|
class DBEditor(QMainWindow):
|
||
|
|
def __init__(self):
|
||
|
|
super().__init__()
|
||
|
|
self.setWindowTitle("Pokémon Database Editor")
|
||
|
|
self.setGeometry(100, 100, 1000, 600)
|
||
|
|
|
||
|
|
self.conn = sqlite3.connect(':memory:') # Use in-memory database for runtime
|
||
|
|
self.cursor = self.conn.cursor()
|
||
|
|
self.init_database()
|
||
|
|
self.patches = self.load_and_apply_patches()
|
||
|
|
|
||
|
|
self.init_ui()
|
||
|
|
|
||
|
|
def init_database(self):
|
||
|
|
# Copy the original database to the in-memory database
|
||
|
|
disk_conn = sqlite3.connect('pokemon_forms.db')
|
||
|
|
disk_conn.backup(self.conn)
|
||
|
|
disk_conn.close()
|
||
|
|
|
||
|
|
def load_and_apply_patches(self):
|
||
|
|
try:
|
||
|
|
with open('patches.json', 'r') as f:
|
||
|
|
patches = json.load(f)
|
||
|
|
except FileNotFoundError:
|
||
|
|
patches = {}
|
||
|
|
|
||
|
|
# Apply patches to the in-memory database
|
||
|
|
for patch_key, patch in patches.items():
|
||
|
|
if patch_key.startswith('evolution_'):
|
||
|
|
from_pfic, to_pfic = patch_key.split('_')[1:]
|
||
|
|
if patch['action'] == 'delete':
|
||
|
|
self.cursor.execute('''
|
||
|
|
DELETE FROM evolution_chains
|
||
|
|
WHERE from_pfic = ? AND to_pfic = ?
|
||
|
|
''', (from_pfic, to_pfic))
|
||
|
|
elif patch['action'] == 'update':
|
||
|
|
self.cursor.execute('''
|
||
|
|
UPDATE evolution_chains
|
||
|
|
SET from_pfic = ?, to_pfic = ?, method = ?
|
||
|
|
WHERE from_pfic = ? AND to_pfic = ?
|
||
|
|
''', (patch['new_from_pfic'], patch['new_to_pfic'], patch['new_method'], from_pfic, to_pfic))
|
||
|
|
elif patch['action'] == 'add':
|
||
|
|
self.cursor.execute('''
|
||
|
|
INSERT OR REPLACE INTO evolution_chains (from_pfic, to_pfic, method)
|
||
|
|
VALUES (?, ?, ?)
|
||
|
|
''', (patch['from_pfic'], patch['to_pfic'], patch['method']))
|
||
|
|
else: # pokemon_storage patches
|
||
|
|
self.cursor.execute('''
|
||
|
|
UPDATE pokemon_storage
|
||
|
|
SET storable_in_home = ?
|
||
|
|
WHERE PFIC = ?
|
||
|
|
''', (patch['storable_in_home'], patch_key))
|
||
|
|
|
||
|
|
self.conn.commit()
|
||
|
|
return patches
|
||
|
|
|
||
|
|
def save_patches(self):
|
||
|
|
with open('patches.json', 'w') as f:
|
||
|
|
json.dump(self.patches, f)
|
||
|
|
|
||
|
|
def init_ui(self):
|
||
|
|
central_widget = QWidget()
|
||
|
|
self.setCentralWidget(central_widget)
|
||
|
|
main_layout = QHBoxLayout(central_widget)
|
||
|
|
|
||
|
|
# Left side: Search and List
|
||
|
|
left_layout = QVBoxLayout()
|
||
|
|
self.search_bar = QLineEdit()
|
||
|
|
self.search_bar.setPlaceholderText("Search Pokémon...")
|
||
|
|
self.search_bar.textChanged.connect(self.filter_pokemon_list)
|
||
|
|
left_layout.addWidget(self.search_bar)
|
||
|
|
|
||
|
|
self.pokemon_list = QListWidget()
|
||
|
|
self.pokemon_list.currentItemChanged.connect(self.load_pokemon_details)
|
||
|
|
left_layout.addWidget(self.pokemon_list)
|
||
|
|
|
||
|
|
# Right side: Edit panel
|
||
|
|
right_layout = QHBoxLayout()
|
||
|
|
|
||
|
|
# Left side of right panel: Text information
|
||
|
|
text_layout = QVBoxLayout()
|
||
|
|
self.edit_form = QFormLayout()
|
||
|
|
self.name_label = QLabel()
|
||
|
|
self.form_name_label = QLabel()
|
||
|
|
self.national_dex_label = QLabel()
|
||
|
|
self.generation_label = QLabel()
|
||
|
|
self.home_checkbox = QCheckBox("Available in Home")
|
||
|
|
|
||
|
|
self.edit_form.addRow("Name:", self.name_label)
|
||
|
|
self.edit_form.addRow("Form:", self.form_name_label)
|
||
|
|
self.edit_form.addRow("National Dex:", self.national_dex_label)
|
||
|
|
self.edit_form.addRow("Generation:", self.generation_label)
|
||
|
|
self.edit_form.addRow(self.home_checkbox)
|
||
|
|
|
||
|
|
text_layout.addLayout(self.edit_form)
|
||
|
|
|
||
|
|
# Evolution chain tree
|
||
|
|
self.evolution_tree = QTreeWidget()
|
||
|
|
self.evolution_tree.setHeaderLabels(["Pokémon", "Evolution Method"])
|
||
|
|
self.evolution_tree.setColumnWidth(0, 200)
|
||
|
|
text_layout.addWidget(self.evolution_tree)
|
||
|
|
|
||
|
|
# Move buttons to the bottom
|
||
|
|
text_layout.addStretch(1)
|
||
|
|
|
||
|
|
self.save_button = QPushButton("Save Changes")
|
||
|
|
self.save_button.clicked.connect(self.save_changes)
|
||
|
|
text_layout.addWidget(self.save_button)
|
||
|
|
|
||
|
|
self.export_button = QPushButton("Export Database")
|
||
|
|
self.export_button.clicked.connect(self.export_database)
|
||
|
|
text_layout.addWidget(self.export_button)
|
||
|
|
|
||
|
|
# Add New Evolution button
|
||
|
|
self.add_evolution_button = QPushButton("Add New Evolution")
|
||
|
|
self.add_evolution_button.clicked.connect(self.add_new_evolution)
|
||
|
|
text_layout.addWidget(self.add_evolution_button)
|
||
|
|
|
||
|
|
# Right side of right panel: Image
|
||
|
|
image_layout = QVBoxLayout()
|
||
|
|
self.image_label = QLabel()
|
||
|
|
self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
|
|
self.image_label.setFixedSize(200, 200)
|
||
|
|
image_layout.addWidget(self.image_label)
|
||
|
|
image_layout.addStretch(1)
|
||
|
|
|
||
|
|
right_layout.addLayout(text_layout)
|
||
|
|
right_layout.addLayout(image_layout)
|
||
|
|
|
||
|
|
main_layout.addLayout(left_layout, 1)
|
||
|
|
main_layout.addLayout(right_layout, 1)
|
||
|
|
|
||
|
|
self.load_pokemon_list()
|
||
|
|
self.adjust_list_width()
|
||
|
|
|
||
|
|
def adjust_list_width(self):
|
||
|
|
max_width = 0
|
||
|
|
font_metrics = QFontMetrics(self.pokemon_list.font())
|
||
|
|
for i in range(self.pokemon_list.count()):
|
||
|
|
item_width = font_metrics.horizontalAdvance(self.pokemon_list.item(i).text())
|
||
|
|
max_width = max(max_width, item_width)
|
||
|
|
|
||
|
|
# Add some padding to the width
|
||
|
|
list_width = max_width + 50
|
||
|
|
self.pokemon_list.setFixedWidth(list_width)
|
||
|
|
self.search_bar.setFixedWidth(list_width)
|
||
|
|
|
||
|
|
def load_pokemon_list(self):
|
||
|
|
self.cursor.execute('''
|
||
|
|
SELECT pf.PFIC, pf.name, pf.form_name, pf.national_dex
|
||
|
|
FROM pokemon_forms pf
|
||
|
|
ORDER BY pf.national_dex, pf.form_name
|
||
|
|
''')
|
||
|
|
pokemon_data = self.cursor.fetchall()
|
||
|
|
|
||
|
|
for pfic, name, form_name, national_dex in pokemon_data:
|
||
|
|
display_name = f"{national_dex:04d} - {name}"
|
||
|
|
if form_name:
|
||
|
|
display_name += f" ({form_name})"
|
||
|
|
item = QListWidgetItem(display_name)
|
||
|
|
item.setData(Qt.ItemDataRole.UserRole, pfic)
|
||
|
|
self.pokemon_list.addItem(item)
|
||
|
|
|
||
|
|
def filter_pokemon_list(self):
|
||
|
|
search_text = self.search_bar.text().lower()
|
||
|
|
for i in range(self.pokemon_list.count()):
|
||
|
|
item = self.pokemon_list.item(i)
|
||
|
|
item.setHidden(search_text not in item.text().lower())
|
||
|
|
|
||
|
|
def load_pokemon_details(self, current, previous):
|
||
|
|
if not current:
|
||
|
|
return
|
||
|
|
|
||
|
|
pfic = current.data(Qt.ItemDataRole.UserRole)
|
||
|
|
self.cursor.execute('''
|
||
|
|
SELECT pf.name, pf.form_name, pf.national_dex, pf.generation, ps.storable_in_home
|
||
|
|
FROM pokemon_forms pf
|
||
|
|
LEFT JOIN pokemon_storage ps ON pf.PFIC = ps.PFIC
|
||
|
|
WHERE pf.PFIC = ?
|
||
|
|
''', (pfic,))
|
||
|
|
pokemon_data = self.cursor.fetchone()
|
||
|
|
|
||
|
|
if pokemon_data:
|
||
|
|
name, form_name, national_dex, generation, storable_in_home = pokemon_data
|
||
|
|
self.name_label.setText(name)
|
||
|
|
self.form_name_label.setText(form_name if form_name else "")
|
||
|
|
self.national_dex_label.setText(str(national_dex))
|
||
|
|
self.generation_label.setText(str(generation))
|
||
|
|
self.home_checkbox.setChecked(bool(storable_in_home))
|
||
|
|
|
||
|
|
# Load and display the image
|
||
|
|
image_path = f"images-new/{pfic}.png"
|
||
|
|
if os.path.exists(image_path):
|
||
|
|
pixmap = QPixmap(image_path)
|
||
|
|
self.image_label.setPixmap(pixmap.scaled(200, 200, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation))
|
||
|
|
else:
|
||
|
|
self.image_label.setText("Image not found")
|
||
|
|
|
||
|
|
# Load and display evolution chain
|
||
|
|
self.load_evolution_chain(pfic)
|
||
|
|
|
||
|
|
self.current_pfic = pfic
|
||
|
|
self.add_evolution_button.setEnabled(True) # Enable the button when a Pokémon is selected
|
||
|
|
|
||
|
|
def edit_evolution(self, item, column):
|
||
|
|
parent = item.parent()
|
||
|
|
if not parent:
|
||
|
|
return # Don't edit the root item
|
||
|
|
|
||
|
|
from_pfic = parent.data(0, Qt.ItemDataRole.UserRole)
|
||
|
|
to_pfic = item.data(0, Qt.ItemDataRole.UserRole)
|
||
|
|
method = item.text(1)
|
||
|
|
|
||
|
|
dialog = EvolutionEditDialog(self, from_pfic, to_pfic, method)
|
||
|
|
result = dialog.exec()
|
||
|
|
|
||
|
|
if result == QDialog.DialogCode.Accepted:
|
||
|
|
new_from_pfic = dialog.from_combo.currentData()
|
||
|
|
new_to_pfic = dialog.to_combo.currentData()
|
||
|
|
new_method = dialog.method_edit.text()
|
||
|
|
|
||
|
|
# Update the in-memory database
|
||
|
|
self.cursor.execute('''
|
||
|
|
UPDATE evolution_chains
|
||
|
|
SET from_pfic = ?, to_pfic = ?, method = ?
|
||
|
|
WHERE from_pfic = ? AND to_pfic = ?
|
||
|
|
''', (new_from_pfic, new_to_pfic, new_method, from_pfic, to_pfic))
|
||
|
|
|
||
|
|
# Create or update the patch
|
||
|
|
patch_key = f"evolution_{from_pfic}_{to_pfic}"
|
||
|
|
self.patches[patch_key] = {
|
||
|
|
'action': 'update',
|
||
|
|
'new_from_pfic': new_from_pfic,
|
||
|
|
'new_to_pfic': new_to_pfic,
|
||
|
|
'new_method': new_method
|
||
|
|
}
|
||
|
|
self.save_patches()
|
||
|
|
|
||
|
|
# Refresh the evolution chain display
|
||
|
|
self.load_evolution_chain(self.current_pfic)
|
||
|
|
|
||
|
|
elif result == 2: # Delete action
|
||
|
|
confirm = QMessageBox.question(self, "Confirm Deletion", "Are you sure you want to delete this evolution?",
|
||
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
||
|
|
if confirm == QMessageBox.StandardButton.Yes:
|
||
|
|
# Delete from the in-memory database
|
||
|
|
self.cursor.execute('''
|
||
|
|
DELETE FROM evolution_chains
|
||
|
|
WHERE from_pfic = ? AND to_pfic = ?
|
||
|
|
''', (from_pfic, to_pfic))
|
||
|
|
|
||
|
|
# Create a delete patch
|
||
|
|
patch_key = f"evolution_{from_pfic}_{to_pfic}"
|
||
|
|
self.patches[patch_key] = {'action': 'delete'}
|
||
|
|
self.save_patches()
|
||
|
|
|
||
|
|
# Refresh the evolution chain display
|
||
|
|
self.load_evolution_chain(self.current_pfic)
|
||
|
|
|
||
|
|
def get_evolution_chain(self, pfic):
|
||
|
|
def follow_chain(start_pfic, visited=None):
|
||
|
|
if visited is None:
|
||
|
|
visited = set()
|
||
|
|
|
||
|
|
chain = []
|
||
|
|
stack = [(start_pfic, None)]
|
||
|
|
|
||
|
|
while stack:
|
||
|
|
current_pfic, method = stack.pop()
|
||
|
|
if current_pfic in visited:
|
||
|
|
continue
|
||
|
|
|
||
|
|
visited.add(current_pfic)
|
||
|
|
self.cursor.execute('SELECT name, form_name FROM pokemon_forms WHERE PFIC = ?', (current_pfic,))
|
||
|
|
name, form_name = self.cursor.fetchone()
|
||
|
|
|
||
|
|
chain.append((current_pfic, name, form_name, method))
|
||
|
|
|
||
|
|
# Get previous evolutions
|
||
|
|
self.cursor.execute('SELECT from_pfic, method FROM evolution_chains WHERE to_pfic = ?', (current_pfic,))
|
||
|
|
prev_evolutions = self.cursor.fetchall()
|
||
|
|
for prev_pfic, prev_method in prev_evolutions:
|
||
|
|
if prev_pfic not in visited:
|
||
|
|
stack.append((prev_pfic, prev_method))
|
||
|
|
|
||
|
|
# Get next evolutions
|
||
|
|
self.cursor.execute('SELECT to_pfic, method FROM evolution_chains WHERE from_pfic = ?', (current_pfic,))
|
||
|
|
next_evolutions = self.cursor.fetchall()
|
||
|
|
for next_pfic, next_method in next_evolutions:
|
||
|
|
if next_pfic not in visited:
|
||
|
|
stack.append((next_pfic, next_method))
|
||
|
|
|
||
|
|
return chain
|
||
|
|
|
||
|
|
return follow_chain(pfic)
|
||
|
|
|
||
|
|
def load_evolution_chain(self, pfic):
|
||
|
|
self.evolution_tree.clear()
|
||
|
|
evolution_chain = self.get_evolution_chain(pfic)
|
||
|
|
|
||
|
|
# Create a dictionary to store tree items
|
||
|
|
tree_items = {}
|
||
|
|
|
||
|
|
# First pass: create all tree items
|
||
|
|
for current_pfic, name, form_name, method in evolution_chain:
|
||
|
|
display_name = f"{name} ({form_name})" if form_name else name
|
||
|
|
item = QTreeWidgetItem([display_name, method if method else ""])
|
||
|
|
item.setData(0, Qt.ItemDataRole.UserRole, current_pfic)
|
||
|
|
tree_items[current_pfic] = item
|
||
|
|
|
||
|
|
if current_pfic == pfic:
|
||
|
|
item.setBackground(0, QColor(255, 255, 0, 100)) # Highlight selected Pokémon
|
||
|
|
|
||
|
|
# Second pass: build the tree structure
|
||
|
|
root = None
|
||
|
|
for current_pfic, name, form_name, method in evolution_chain:
|
||
|
|
item = tree_items[current_pfic]
|
||
|
|
|
||
|
|
# Find the parent of this item
|
||
|
|
self.cursor.execute('SELECT from_pfic FROM evolution_chains WHERE to_pfic = ?', (current_pfic,))
|
||
|
|
parent_pfic = self.cursor.fetchone()
|
||
|
|
|
||
|
|
if parent_pfic:
|
||
|
|
parent_item = tree_items.get(parent_pfic[0])
|
||
|
|
if parent_item:
|
||
|
|
parent_item.addChild(item)
|
||
|
|
elif not root:
|
||
|
|
root = item
|
||
|
|
self.evolution_tree.addTopLevelItem(root)
|
||
|
|
|
||
|
|
# Expand the entire tree
|
||
|
|
self.evolution_tree.expandAll()
|
||
|
|
|
||
|
|
# Scroll to and select the current Pokémon
|
||
|
|
current_item = tree_items[pfic]
|
||
|
|
self.evolution_tree.scrollToItem(current_item)
|
||
|
|
self.evolution_tree.setCurrentItem(current_item)
|
||
|
|
|
||
|
|
# Connect double-click signal
|
||
|
|
self.evolution_tree.itemDoubleClicked.connect(self.edit_evolution)
|
||
|
|
|
||
|
|
def save_changes(self):
|
||
|
|
if hasattr(self, 'current_pfic'):
|
||
|
|
storable_in_home = self.home_checkbox.isChecked()
|
||
|
|
self.patches[self.current_pfic] = {'storable_in_home': storable_in_home}
|
||
|
|
|
||
|
|
with open('patches.json', 'w') as f:
|
||
|
|
json.dump(self.patches, f)
|
||
|
|
|
||
|
|
def export_database(self):
|
||
|
|
export_conn = sqlite3.connect('pokemon_forms_production.db')
|
||
|
|
self.conn.backup(export_conn)
|
||
|
|
export_conn.close()
|
||
|
|
|
||
|
|
def closeEvent(self, event):
|
||
|
|
self.conn.close()
|
||
|
|
event.accept()
|
||
|
|
|
||
|
|
def add_new_evolution(self):
|
||
|
|
if not hasattr(self, 'current_pfic'):
|
||
|
|
return
|
||
|
|
|
||
|
|
dialog = EvolutionEditDialog(self, self.current_pfic, None, "")
|
||
|
|
result = dialog.exec()
|
||
|
|
|
||
|
|
if result == QDialog.DialogCode.Accepted:
|
||
|
|
new_from_pfic = dialog.from_combo.currentData()
|
||
|
|
new_to_pfic = dialog.to_combo.currentData()
|
||
|
|
new_method = dialog.method_edit.text()
|
||
|
|
|
||
|
|
# Update the in-memory database
|
||
|
|
self.cursor.execute('''
|
||
|
|
INSERT INTO evolution_chains (from_pfic, to_pfic, method)
|
||
|
|
VALUES (?, ?, ?)
|
||
|
|
''', (new_from_pfic, new_to_pfic, new_method))
|
||
|
|
|
||
|
|
# Create a new patch
|
||
|
|
patch_key = f"evolution_{new_from_pfic}_{new_to_pfic}"
|
||
|
|
self.patches[patch_key] = {
|
||
|
|
'action': 'add',
|
||
|
|
'from_pfic': new_from_pfic,
|
||
|
|
'to_pfic': new_to_pfic,
|
||
|
|
'method': new_method
|
||
|
|
}
|
||
|
|
self.save_patches()
|
||
|
|
|
||
|
|
# Refresh the evolution chain display
|
||
|
|
self.load_evolution_chain(self.current_pfic)
|
||
|
|
|
||
|
|
if __name__ == '__main__':
|
||
|
|
app = QApplication(sys.argv)
|
||
|
|
editor = DBEditor()
|
||
|
|
editor.show()
|
||
|
|
sys.exit(app.exec())
|