@ -0,0 +1,3 @@ |
|||||
|
pokemon_cache.db |
||||
|
pokemon_database.db |
||||
|
Utilities/venv/ |
||||
@ -0,0 +1,30 @@ |
|||||
|
import csv |
||||
|
from flask import Flask, render_template |
||||
|
|
||||
|
app = Flask(__name__) |
||||
|
|
||||
|
def load_pokemon_data(): |
||||
|
pokemon_list = [] |
||||
|
earliest_games = {} |
||||
|
|
||||
|
# Load Pokemon Home list |
||||
|
with open('pokemon_home_list.csv', 'r') as file: |
||||
|
reader = csv.DictReader(file) |
||||
|
for row in reader: |
||||
|
pokemon_list.append(row) |
||||
|
|
||||
|
# Load earliest games data |
||||
|
with open('pokemon_earliest_games.csv', 'r') as file: |
||||
|
reader = csv.DictReader(file) |
||||
|
for row in reader: |
||||
|
earliest_games[row['Pokemon']] = row['Earliest Game'] |
||||
|
|
||||
|
return pokemon_list, earliest_games |
||||
|
|
||||
|
@app.route('/') |
||||
|
def index(): |
||||
|
pokemon_list, earliest_games = load_pokemon_data() |
||||
|
return render_template('index.html', pokemon_list=pokemon_list, earliest_games=earliest_games) |
||||
|
|
||||
|
if __name__ == '__main__': |
||||
|
app.run(debug=True) |
||||
@ -0,0 +1,585 @@ |
|||||
|
import sys |
||||
|
import sqlite3 |
||||
|
import json |
||||
|
import os |
||||
|
from datetime import datetime |
||||
|
from PyQt6.QtWidgets import (QApplication, QMainWindow, QListWidget, QLabel, |
||||
|
QVBoxLayout, QHBoxLayout, QWidget, QPushButton, |
||||
|
QTableWidget, QTableWidgetItem, QHeaderView, |
||||
|
QLineEdit, QCheckBox, QFormLayout, QMessageBox, |
||||
|
QDialog, QComboBox, QFileDialog, QTabWidget) |
||||
|
from PyQt6.QtGui import QPixmap |
||||
|
from PyQt6.QtCore import Qt |
||||
|
|
||||
|
class PokemonDatabaseApp(QMainWindow): |
||||
|
def __init__(self): |
||||
|
super().__init__() |
||||
|
self.setWindowTitle("Pokémon Database Viewer") |
||||
|
self.setGeometry(100, 100, 1200, 600) |
||||
|
|
||||
|
# Create an in-memory database |
||||
|
self.conn = sqlite3.connect(':memory:') |
||||
|
self.cursor = self.conn.cursor() |
||||
|
|
||||
|
# Load the database from disk into memory |
||||
|
self.load_database() |
||||
|
|
||||
|
# Apply all existing patches |
||||
|
self.apply_all_patches() |
||||
|
|
||||
|
self.current_pokemon_id = None |
||||
|
self.current_pokemon_name = None |
||||
|
self.current_form_id = None |
||||
|
self.all_pokemon = [] |
||||
|
self.patches = {} |
||||
|
self.init_ui() |
||||
|
self.load_locations_list() |
||||
|
|
||||
|
def init_ui(self): |
||||
|
main_layout = QVBoxLayout() |
||||
|
|
||||
|
# Create tab widget |
||||
|
self.tab_widget = QTabWidget() |
||||
|
|
||||
|
# Create and add Pokemon tab |
||||
|
pokemon_tab = self.create_pokemon_tab() |
||||
|
self.tab_widget.addTab(pokemon_tab, "Pokémon") |
||||
|
|
||||
|
# Create and add Locations tab |
||||
|
locations_tab = self.create_locations_tab() |
||||
|
self.tab_widget.addTab(locations_tab, "Locations") |
||||
|
|
||||
|
main_layout.addWidget(self.tab_widget) |
||||
|
|
||||
|
container = QWidget() |
||||
|
container.setLayout(main_layout) |
||||
|
self.setCentralWidget(container) |
||||
|
|
||||
|
self.load_pokemon_list() |
||||
|
|
||||
|
def create_pokemon_tab(self): |
||||
|
pokemon_tab = QWidget() |
||||
|
tab_layout = QHBoxLayout() |
||||
|
|
||||
|
# Pokémon list section |
||||
|
pokemon_list_layout = QVBoxLayout() |
||||
|
|
||||
|
# Search box |
||||
|
self.search_box = QLineEdit() |
||||
|
self.search_box.setPlaceholderText("Search Pokémon...") |
||||
|
self.search_box.textChanged.connect(self.filter_pokemon_list) |
||||
|
pokemon_list_layout.addWidget(self.search_box) |
||||
|
|
||||
|
# Pokémon list |
||||
|
self.pokemon_list = QListWidget() |
||||
|
self.pokemon_list.itemClicked.connect(self.load_pokemon_data) |
||||
|
pokemon_list_layout.addWidget(self.pokemon_list) |
||||
|
|
||||
|
tab_layout.addLayout(pokemon_list_layout, 1) |
||||
|
|
||||
|
# Pokémon details |
||||
|
details_layout = QVBoxLayout() |
||||
|
|
||||
|
image_and_form_layout = QHBoxLayout() |
||||
|
|
||||
|
# Image |
||||
|
self.image_label = QLabel() |
||||
|
self.image_label.setFixedSize(200, 200) |
||||
|
self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter) |
||||
|
image_and_form_layout.addWidget(self.image_label) |
||||
|
|
||||
|
# Form details |
||||
|
form_details_layout = QFormLayout() |
||||
|
self.form_name_edit = QLineEdit() |
||||
|
self.is_default_checkbox = QCheckBox() |
||||
|
self.image_path_edit = QLineEdit() |
||||
|
form_details_layout.addRow("Form Name:", self.form_name_edit) |
||||
|
form_details_layout.addRow("Default Form:", self.is_default_checkbox) |
||||
|
form_details_layout.addRow("Image Path:", self.image_path_edit) |
||||
|
|
||||
|
# Save button |
||||
|
self.save_button = QPushButton("Save Form Changes") |
||||
|
self.save_button.clicked.connect(self.save_form_changes) |
||||
|
form_details_layout.addRow(self.save_button) |
||||
|
|
||||
|
image_and_form_layout.addLayout(form_details_layout) |
||||
|
details_layout.addLayout(image_and_form_layout) |
||||
|
|
||||
|
# Forms list and add new form button |
||||
|
forms_layout = QHBoxLayout() |
||||
|
self.forms_list = QListWidget() |
||||
|
self.forms_list.itemClicked.connect(self.load_form_data) |
||||
|
forms_layout.addWidget(self.forms_list) |
||||
|
|
||||
|
add_form_button = QPushButton("Add New Form") |
||||
|
add_form_button.clicked.connect(self.add_new_form) |
||||
|
details_layout.addWidget(add_form_button) |
||||
|
|
||||
|
details_layout.addWidget(QLabel("Forms:")) |
||||
|
details_layout.addLayout(forms_layout) |
||||
|
|
||||
|
# Encounters table and add new encounter button |
||||
|
self.encounters_table = QTableWidget() |
||||
|
self.encounters_table.setColumnCount(3) |
||||
|
self.encounters_table.setHorizontalHeaderLabels(['Game', 'Location', 'Method']) |
||||
|
self.encounters_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) |
||||
|
details_layout.addWidget(QLabel("Encounters:")) |
||||
|
details_layout.addWidget(self.encounters_table) |
||||
|
|
||||
|
add_encounter_button = QPushButton("Add New Encounter") |
||||
|
add_encounter_button.clicked.connect(self.add_new_encounter) |
||||
|
details_layout.addWidget(add_encounter_button) |
||||
|
|
||||
|
tab_layout.addLayout(details_layout, 2) |
||||
|
|
||||
|
pokemon_tab.setLayout(tab_layout) |
||||
|
return pokemon_tab |
||||
|
|
||||
|
def create_locations_tab(self): |
||||
|
locations_tab = QWidget() |
||||
|
tab_layout = QHBoxLayout() |
||||
|
|
||||
|
# Locations list |
||||
|
locations_list_layout = QVBoxLayout() |
||||
|
|
||||
|
# Search box for locations |
||||
|
self.locations_search_box = QLineEdit() |
||||
|
self.locations_search_box.setPlaceholderText("Search Locations...") |
||||
|
self.locations_search_box.textChanged.connect(self.filter_locations_list) |
||||
|
locations_list_layout.addWidget(self.locations_search_box) |
||||
|
|
||||
|
# Locations list |
||||
|
self.locations_list = QListWidget() |
||||
|
self.locations_list.itemClicked.connect(self.load_location_data) |
||||
|
locations_list_layout.addWidget(self.locations_list) |
||||
|
|
||||
|
# Add new location button |
||||
|
add_location_button = QPushButton("Add New Location") |
||||
|
add_location_button.clicked.connect(self.add_new_location) |
||||
|
locations_list_layout.addWidget(add_location_button) |
||||
|
|
||||
|
tab_layout.addLayout(locations_list_layout, 1) |
||||
|
|
||||
|
# Location details |
||||
|
location_details_layout = QFormLayout() |
||||
|
|
||||
|
self.location_name_edit = QLineEdit() |
||||
|
self.location_description_edit = QLineEdit() |
||||
|
|
||||
|
location_details_layout.addRow("Name:", self.location_name_edit) |
||||
|
location_details_layout.addRow("description:", self.location_description_edit) |
||||
|
|
||||
|
# Save location changes button |
||||
|
save_location_button = QPushButton("Save Location Changes") |
||||
|
save_location_button.clicked.connect(self.save_location_changes) |
||||
|
location_details_layout.addRow(save_location_button) |
||||
|
|
||||
|
tab_layout.addLayout(location_details_layout, 2) |
||||
|
|
||||
|
locations_tab.setLayout(tab_layout) |
||||
|
return locations_tab |
||||
|
|
||||
|
def load_database(self): |
||||
|
disk_conn = sqlite3.connect('pokemon_database.db') |
||||
|
disk_conn.backup(self.conn) |
||||
|
disk_conn.close() |
||||
|
|
||||
|
def apply_all_patches(self): |
||||
|
patches_dir = "patches" |
||||
|
if not os.path.exists(patches_dir): |
||||
|
return |
||||
|
|
||||
|
patch_files = sorted([f for f in os.listdir(patches_dir) if f.endswith('.json')]) |
||||
|
for patch_file in patch_files: |
||||
|
with open(os.path.join(patches_dir, patch_file), 'r') as f: |
||||
|
patches = json.load(f) |
||||
|
self.apply_patches(patches) |
||||
|
|
||||
|
def apply_patches(self, patches): |
||||
|
try: |
||||
|
for timestamp, patch in patches.items(): |
||||
|
if patch["type"] == "form_update": |
||||
|
self.cursor.execute(""" |
||||
|
UPDATE pokemon_forms |
||||
|
SET form_name = ?, is_default = ?, image_path = ? |
||||
|
WHERE id = ? |
||||
|
""", (patch["form_name"], patch["is_default"], patch["image_path"], patch["form_id"])) |
||||
|
elif patch["type"] == "new_form": |
||||
|
self.cursor.execute(""" |
||||
|
INSERT INTO pokemon_forms (pokemon_id, form_name, is_default, image_path) |
||||
|
VALUES (?, ?, ?, ?) |
||||
|
""", (patch["pokemon_id"], patch["form_name"], patch["is_default"], patch["image_path"])) |
||||
|
elif patch["type"] == "new_encounter": |
||||
|
self.cursor.execute(""" |
||||
|
INSERT INTO form_encounters (form_id, game_id, location_id, encounter_method_id) |
||||
|
VALUES (?, |
||||
|
(SELECT id FROM games WHERE name = ?), |
||||
|
(SELECT id FROM locations WHERE name = ?), |
||||
|
(SELECT id FROM encounter_methods WHERE name = ?)) |
||||
|
""", (patch["form_id"], patch["game"], patch["location"], patch["method"])) |
||||
|
elif patch["type"] == "location_update": |
||||
|
self.cursor.execute(""" |
||||
|
UPDATE locations |
||||
|
SET name = ?, description = ? |
||||
|
WHERE name = ? |
||||
|
""", (patch["new_name"], patch["description"], patch["old_name"])) |
||||
|
elif patch["type"] == "new_location": |
||||
|
self.cursor.execute(""" |
||||
|
INSERT INTO locations (name, description) |
||||
|
VALUES (?, ?) |
||||
|
""", (patch["name"], patch["description"])) |
||||
|
|
||||
|
self.conn.commit() |
||||
|
except sqlite3.Error as e: |
||||
|
print(f"An error occurred while applying patches: {e}") |
||||
|
|
||||
|
def load_pokemon_list(self): |
||||
|
self.cursor.execute("SELECT national_dex_number, name FROM pokemon ORDER BY national_dex_number") |
||||
|
self.all_pokemon = [f"{row[0]:03d} - {row[1]}" for row in self.cursor.fetchall()] |
||||
|
self.pokemon_list.addItems(self.all_pokemon) |
||||
|
|
||||
|
def filter_pokemon_list(self): |
||||
|
search_text = self.search_box.text().lower() |
||||
|
self.pokemon_list.clear() |
||||
|
for pokemon in self.all_pokemon: |
||||
|
if search_text in pokemon.lower(): |
||||
|
self.pokemon_list.addItem(pokemon) |
||||
|
|
||||
|
def load_pokemon_data(self, item): |
||||
|
self.current_pokemon_id = int(item.text().split('-')[0]) |
||||
|
self.current_pokemon_name = item.text().split('-')[1] |
||||
|
# Load forms |
||||
|
self.forms_list.clear() |
||||
|
self.cursor.execute(""" |
||||
|
SELECT form_name FROM pokemon_forms |
||||
|
WHERE pokemon_id = ? |
||||
|
ORDER BY is_default DESC, form_name |
||||
|
""", (self.current_pokemon_id,)) |
||||
|
for row in self.cursor.fetchall(): |
||||
|
self.forms_list.addItem(row[0]) |
||||
|
|
||||
|
# Load default form data |
||||
|
self.forms_list.setCurrentRow(0) |
||||
|
self.load_form_data(self.forms_list.item(0)) |
||||
|
|
||||
|
def load_form_data(self, item): |
||||
|
if not item: |
||||
|
return |
||||
|
|
||||
|
form_name = item.text() |
||||
|
|
||||
|
# Load form data |
||||
|
self.cursor.execute(""" |
||||
|
SELECT id, form_name, is_default, image_path FROM pokemon_forms |
||||
|
WHERE pokemon_id = ? AND form_name = ? |
||||
|
""", (self.current_pokemon_id, form_name)) |
||||
|
form_data = self.cursor.fetchone() |
||||
|
if form_data: |
||||
|
self.current_form_id, form_name, is_default, image_path = form_data |
||||
|
|
||||
|
# Update form details |
||||
|
self.form_name_edit.setText(form_name) |
||||
|
self.is_default_checkbox.setChecked(bool(is_default)) |
||||
|
self.image_path_edit.setText(image_path) |
||||
|
|
||||
|
# Load image |
||||
|
pixmap = QPixmap(image_path) |
||||
|
self.image_label.setPixmap(pixmap.scaled(200, 200, Qt.AspectRatioMode.KeepAspectRatio)) |
||||
|
|
||||
|
# Load encounters |
||||
|
self.encounters_table.setRowCount(0) |
||||
|
self.cursor.execute(""" |
||||
|
SELECT g.name, l.name, em.name |
||||
|
FROM form_encounters fe |
||||
|
JOIN games g ON fe.game_id = g.id |
||||
|
JOIN locations l ON fe.location_id = l.id |
||||
|
JOIN encounter_methods em ON fe.encounter_method_id = em.id |
||||
|
WHERE fe.form_id = ? |
||||
|
ORDER BY g.name, l.name |
||||
|
""", (self.current_form_id,)) |
||||
|
for row in self.cursor.fetchall(): |
||||
|
current_row = self.encounters_table.rowCount() |
||||
|
self.encounters_table.insertRow(current_row) |
||||
|
for col, value in enumerate(row): |
||||
|
self.encounters_table.setItem(current_row, col, QTableWidgetItem(str(value))) |
||||
|
|
||||
|
def save_form_changes(self): |
||||
|
if not self.current_form_id: |
||||
|
return |
||||
|
|
||||
|
new_form_name = self.form_name_edit.text() |
||||
|
new_is_default = self.is_default_checkbox.isChecked() |
||||
|
new_image_path = self.image_path_edit.text() |
||||
|
|
||||
|
# Add changes to patches |
||||
|
patch = { |
||||
|
"type": "form_update", |
||||
|
"form_id": self.current_form_id, |
||||
|
"form_name": new_form_name, |
||||
|
"is_default": new_is_default, |
||||
|
"image_path": new_image_path |
||||
|
} |
||||
|
self.add_to_patches(patch) |
||||
|
|
||||
|
try: |
||||
|
self.cursor.execute(""" |
||||
|
UPDATE pokemon_forms |
||||
|
SET form_name = ?, is_default = ?, image_path = ? |
||||
|
WHERE id = ? |
||||
|
""", (new_form_name, new_is_default, new_image_path, self.current_form_id)) |
||||
|
self.conn.commit() |
||||
|
QMessageBox.information(self, "Success", "Form data updated successfully!") |
||||
|
|
||||
|
# Refresh the forms list and current form data |
||||
|
self.load_pokemon_data(self.pokemon_list.currentItem()) |
||||
|
except sqlite3.Error as e: |
||||
|
QMessageBox.warning(self, "Error", f"An error occurred: {e}") |
||||
|
|
||||
|
def add_new_form(self): |
||||
|
if not self.current_pokemon_id: |
||||
|
QMessageBox.warning(self, "Error", "Please select a Pokémon first.") |
||||
|
return |
||||
|
|
||||
|
dialog = QDialog(self) |
||||
|
dialog.setWindowTitle("Add New Form") |
||||
|
layout = QFormLayout(dialog) |
||||
|
|
||||
|
form_name_edit = QLineEdit() |
||||
|
layout.addRow("Form Name:", form_name_edit) |
||||
|
|
||||
|
buttons = QHBoxLayout() |
||||
|
save_button = QPushButton("Save") |
||||
|
save_button.clicked.connect(dialog.accept) |
||||
|
cancel_button = QPushButton("Cancel") |
||||
|
cancel_button.clicked.connect(dialog.reject) |
||||
|
buttons.addWidget(save_button) |
||||
|
buttons.addWidget(cancel_button) |
||||
|
layout.addRow(buttons) |
||||
|
|
||||
|
if dialog.exec() == QDialog.DialogCode.Accepted: |
||||
|
form_name = form_name_edit.text() |
||||
|
|
||||
|
try: |
||||
|
self.cursor.execute(""" |
||||
|
INSERT INTO pokemon_forms (pokemon_id, form_name, is_default, image_path) |
||||
|
VALUES (?, ?, ?, ?) |
||||
|
""", (self.current_pokemon_id, form_name, False, f"images/pokemon/{self.current_pokemon_id:04d}_{self.current_pokemon_name}_({form_name}).png".replace(" ", "_"))) |
||||
|
self.conn.commit() |
||||
|
new_form_id = self.cursor.lastrowid |
||||
|
|
||||
|
# Add new form to patches |
||||
|
patch = { |
||||
|
"type": "new_form", |
||||
|
"form_id": new_form_id, |
||||
|
"pokemon_id": self.current_pokemon_id, |
||||
|
"form_name": form_name, |
||||
|
"is_default": False, |
||||
|
"image_path": f"images/pokemon/{self.current_pokemon_id:04d}_{self.current_pokemon_name}_({form_name}).png".replace(" ", "_") |
||||
|
} |
||||
|
self.add_to_patches(patch) |
||||
|
|
||||
|
QMessageBox.information(self, "Success", "New form added successfully!") |
||||
|
self.load_pokemon_data(self.pokemon_list.currentItem()) |
||||
|
except sqlite3.Error as e: |
||||
|
QMessageBox.warning(self, "Error", f"An error occurred: {e}") |
||||
|
|
||||
|
def add_new_encounter(self): |
||||
|
if not self.current_form_id: |
||||
|
QMessageBox.warning(self, "Error", "Please select a form first.") |
||||
|
return |
||||
|
|
||||
|
dialog = QDialog(self) |
||||
|
dialog.setWindowTitle("Add New Encounter") |
||||
|
layout = QFormLayout(dialog) |
||||
|
|
||||
|
game_combo = QComboBox() |
||||
|
self.cursor.execute("SELECT name FROM games ORDER BY name") |
||||
|
games = [row[0] for row in self.cursor.fetchall()] |
||||
|
game_combo.addItems(games) |
||||
|
layout.addRow("Game:", game_combo) |
||||
|
|
||||
|
location_combo = QComboBox() |
||||
|
self.cursor.execute("SELECT name FROM locations ORDER BY name") |
||||
|
locations = [row[0] for row in self.cursor.fetchall()] |
||||
|
location_combo.addItems(locations) |
||||
|
layout.addRow("Location:", location_combo) |
||||
|
|
||||
|
method_combo = QComboBox() |
||||
|
self.cursor.execute("SELECT name FROM encounter_methods ORDER BY name") |
||||
|
methods = [row[0] for row in self.cursor.fetchall()] |
||||
|
method_combo.addItems(methods) |
||||
|
layout.addRow("Method:", method_combo) |
||||
|
|
||||
|
buttons = QHBoxLayout() |
||||
|
save_button = QPushButton("Save") |
||||
|
save_button.clicked.connect(dialog.accept) |
||||
|
cancel_button = QPushButton("Cancel") |
||||
|
cancel_button.clicked.connect(dialog.reject) |
||||
|
buttons.addWidget(save_button) |
||||
|
buttons.addWidget(cancel_button) |
||||
|
layout.addRow(buttons) |
||||
|
|
||||
|
if dialog.exec() == QDialog.DialogCode.Accepted: |
||||
|
game = game_combo.currentText() |
||||
|
location = location_combo.currentText() |
||||
|
method = method_combo.currentText() |
||||
|
|
||||
|
try: |
||||
|
self.cursor.execute(""" |
||||
|
INSERT INTO form_encounters (form_id, game_id, location_id, encounter_method_id) |
||||
|
VALUES (?, |
||||
|
(SELECT id FROM games WHERE name = ?), |
||||
|
(SELECT id FROM locations WHERE name = ?), |
||||
|
(SELECT id FROM encounter_methods WHERE name = ?)) |
||||
|
""", (self.current_form_id, game, location, method)) |
||||
|
self.conn.commit() |
||||
|
new_encounter_id = self.cursor.lastrowid |
||||
|
|
||||
|
# Add new encounter to patches |
||||
|
patch = { |
||||
|
"type": "new_encounter", |
||||
|
"encounter_id": new_encounter_id, |
||||
|
"form_id": self.current_form_id, |
||||
|
"game": game, |
||||
|
"location": location, |
||||
|
"method": method |
||||
|
} |
||||
|
self.add_to_patches(patch) |
||||
|
|
||||
|
QMessageBox.information(self, "Success", "New encounter added successfully!") |
||||
|
self.load_form_data(self.forms_list.currentItem()) |
||||
|
except sqlite3.Error as e: |
||||
|
QMessageBox.warning(self, "Error", f"An error occurred: {e}") |
||||
|
|
||||
|
def add_to_patches(self, patch): |
||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") |
||||
|
self.patches[f"{timestamp}_{len(self.patches)}"] = patch |
||||
|
self.auto_save_patches() |
||||
|
|
||||
|
def auto_save_patches(self): |
||||
|
patches_dir = "patches" |
||||
|
if not os.path.exists(patches_dir): |
||||
|
os.makedirs(patches_dir) |
||||
|
|
||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") |
||||
|
file_name = f"{patches_dir}/patch_{timestamp}.json" |
||||
|
|
||||
|
with open(file_name, 'w') as f: |
||||
|
json.dump(self.patches, f, indent=2) |
||||
|
|
||||
|
print(f"Patches auto-saved to {file_name}") |
||||
|
|
||||
|
def closeEvent(self, event): |
||||
|
reply = QMessageBox.question(self, 'Save Changes', |
||||
|
"Do you want to save changes to the disk database?", |
||||
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, |
||||
|
QMessageBox.StandardButton.No) |
||||
|
|
||||
|
if reply == QMessageBox.StandardButton.Yes: |
||||
|
# Save changes to disk |
||||
|
disk_conn = sqlite3.connect('pokemon_database.db') |
||||
|
self.conn.backup(disk_conn) |
||||
|
disk_conn.close() |
||||
|
QMessageBox.information(self, "Success", "Changes saved to disk database.") |
||||
|
|
||||
|
self.conn.close() |
||||
|
event.accept() |
||||
|
|
||||
|
def filter_locations_list(self): |
||||
|
search_text = self.locations_search_box.text().lower() |
||||
|
for i in range(self.locations_list.count()): |
||||
|
item = self.locations_list.item(i) |
||||
|
if search_text in item.text().lower(): |
||||
|
item.setHidden(False) |
||||
|
else: |
||||
|
item.setHidden(True) |
||||
|
|
||||
|
def load_location_data(self, item): |
||||
|
location_name = item.text() |
||||
|
self.cursor.execute("SELECT name, description FROM locations WHERE name = ?", (location_name,)) |
||||
|
location_data = self.cursor.fetchone() |
||||
|
if location_data: |
||||
|
self.location_name_edit.setText(location_data[0]) |
||||
|
self.location_description_edit.setText(location_data[1]) |
||||
|
|
||||
|
def save_location_changes(self): |
||||
|
old_name = self.locations_list.currentItem().text() |
||||
|
new_name = self.location_name_edit.text() |
||||
|
new_description = self.location_description_edit.text() |
||||
|
|
||||
|
try: |
||||
|
self.cursor.execute(""" |
||||
|
UPDATE locations |
||||
|
SET name = ?, description = ? |
||||
|
WHERE name = ? |
||||
|
""", (new_name, new_description, old_name)) |
||||
|
self.conn.commit() |
||||
|
|
||||
|
# Add changes to patches |
||||
|
patch = { |
||||
|
"type": "location_update", |
||||
|
"old_name": old_name, |
||||
|
"new_name": new_name, |
||||
|
"description": new_description |
||||
|
} |
||||
|
self.add_to_patches(patch) |
||||
|
|
||||
|
QMessageBox.information(self, "Success", "Location data updated successfully!") |
||||
|
self.load_locations_list() |
||||
|
except sqlite3.Error as e: |
||||
|
QMessageBox.warning(self, "Error", f"An error occurred: {e}") |
||||
|
|
||||
|
def add_new_location(self): |
||||
|
dialog = QDialog(self) |
||||
|
dialog.setWindowTitle("Add New Location") |
||||
|
layout = QFormLayout(dialog) |
||||
|
|
||||
|
name_edit = QLineEdit() |
||||
|
description_edit = QLineEdit() |
||||
|
layout.addRow("Name:", name_edit) |
||||
|
layout.addRow("description:", description_edit) |
||||
|
|
||||
|
buttons = QHBoxLayout() |
||||
|
save_button = QPushButton("Save") |
||||
|
save_button.clicked.connect(dialog.accept) |
||||
|
cancel_button = QPushButton("Cancel") |
||||
|
cancel_button.clicked.connect(dialog.reject) |
||||
|
buttons.addWidget(save_button) |
||||
|
buttons.addWidget(cancel_button) |
||||
|
layout.addRow(buttons) |
||||
|
|
||||
|
if dialog.exec() == QDialog.DialogCode.Accepted: |
||||
|
name = name_edit.text() |
||||
|
description = description_edit.text() |
||||
|
|
||||
|
try: |
||||
|
self.cursor.execute(""" |
||||
|
INSERT INTO locations (name, description) |
||||
|
VALUES (?, ?) |
||||
|
""", (name, description)) |
||||
|
self.conn.commit() |
||||
|
|
||||
|
# Add new location to patches |
||||
|
patch = { |
||||
|
"type": "new_location", |
||||
|
"name": name, |
||||
|
"description": description |
||||
|
} |
||||
|
self.add_to_patches(patch) |
||||
|
|
||||
|
QMessageBox.information(self, "Success", "New location added successfully!") |
||||
|
self.load_locations_list() |
||||
|
except sqlite3.Error as e: |
||||
|
QMessageBox.warning(self, "Error", f"An error occurred: {e}") |
||||
|
|
||||
|
def load_locations_list(self): |
||||
|
self.locations_list.clear() |
||||
|
self.cursor.execute("SELECT name FROM locations ORDER BY name") |
||||
|
locations = [row[0] for row in self.cursor.fetchall()] |
||||
|
self.locations_list.addItems(locations) |
||||
|
|
||||
|
if __name__ == '__main__': |
||||
|
app = QApplication(sys.argv) |
||||
|
window = PokemonDatabaseApp() |
||||
|
window.show() |
||||
|
sys.exit(app.exec()) |
||||
@ -0,0 +1,354 @@ |
|||||
|
import sqlite3 |
||||
|
import csv |
||||
|
import re |
||||
|
|
||||
|
def create_connection(): |
||||
|
conn = sqlite3.connect('pokemon_database.db') |
||||
|
return conn |
||||
|
|
||||
|
def create_tables(conn): |
||||
|
cursor = conn.cursor() |
||||
|
|
||||
|
# Create games table |
||||
|
cursor.execute(''' |
||||
|
CREATE TABLE IF NOT EXISTS games ( |
||||
|
id INTEGER PRIMARY KEY, |
||||
|
name TEXT NOT NULL, |
||||
|
generation INTEGER NOT NULL |
||||
|
) |
||||
|
''') |
||||
|
|
||||
|
# Create marks table |
||||
|
cursor.execute(''' |
||||
|
CREATE TABLE IF NOT EXISTS marks ( |
||||
|
id INTEGER PRIMARY KEY, |
||||
|
name TEXT NOT NULL, |
||||
|
icon_path TEXT NOT NULL |
||||
|
) |
||||
|
''') |
||||
|
|
||||
|
# Create mark_game_associations table |
||||
|
cursor.execute(''' |
||||
|
CREATE TABLE IF NOT EXISTS mark_game_associations ( |
||||
|
mark_id INTEGER, |
||||
|
game_id INTEGER, |
||||
|
FOREIGN KEY (mark_id) REFERENCES marks (id), |
||||
|
FOREIGN KEY (game_id) REFERENCES games (id), |
||||
|
PRIMARY KEY (mark_id, game_id) |
||||
|
) |
||||
|
''') |
||||
|
|
||||
|
# Create pokemon table |
||||
|
cursor.execute(''' |
||||
|
CREATE TABLE IF NOT EXISTS pokemon ( |
||||
|
national_dex_number INTEGER PRIMARY KEY, |
||||
|
name TEXT NOT NULL |
||||
|
) |
||||
|
''') |
||||
|
|
||||
|
# Create pokemon_forms table |
||||
|
cursor.execute(''' |
||||
|
CREATE TABLE IF NOT EXISTS pokemon_forms ( |
||||
|
id INTEGER PRIMARY KEY, |
||||
|
pokemon_id INTEGER NOT NULL, |
||||
|
form_name TEXT NOT NULL, |
||||
|
is_default BOOLEAN NOT NULL, |
||||
|
image_path TEXT NOT NULL, |
||||
|
FOREIGN KEY (pokemon_id) REFERENCES pokemon (national_dex_number), |
||||
|
UNIQUE (pokemon_id, form_name) |
||||
|
) |
||||
|
''') |
||||
|
|
||||
|
# Create encounter_methods table |
||||
|
cursor.execute(''' |
||||
|
CREATE TABLE IF NOT EXISTS encounter_methods ( |
||||
|
id INTEGER PRIMARY KEY, |
||||
|
name TEXT UNIQUE NOT NULL |
||||
|
) |
||||
|
''') |
||||
|
|
||||
|
# Create locations table |
||||
|
cursor.execute(''' |
||||
|
CREATE TABLE IF NOT EXISTS locations ( |
||||
|
id INTEGER PRIMARY KEY, |
||||
|
name TEXT UNIQUE NOT NULL, |
||||
|
description TEXT |
||||
|
) |
||||
|
''') |
||||
|
|
||||
|
# Create form_encounters table |
||||
|
cursor.execute(''' |
||||
|
CREATE TABLE IF NOT EXISTS form_encounters ( |
||||
|
id INTEGER PRIMARY KEY, |
||||
|
form_id INTEGER NOT NULL, |
||||
|
game_id INTEGER NOT NULL, |
||||
|
location_id INTEGER NOT NULL, |
||||
|
encounter_method_id INTEGER NOT NULL, |
||||
|
FOREIGN KEY (form_id) REFERENCES pokemon_forms (id), |
||||
|
FOREIGN KEY (game_id) REFERENCES games (id), |
||||
|
FOREIGN KEY (location_id) REFERENCES locations (id), |
||||
|
FOREIGN KEY (encounter_method_id) REFERENCES encounter_methods (id), |
||||
|
UNIQUE (form_id, game_id, location_id, encounter_method_id) |
||||
|
) |
||||
|
''') |
||||
|
|
||||
|
# Create alternate_game_names table |
||||
|
cursor.execute(''' |
||||
|
CREATE TABLE IF NOT EXISTS alternate_game_names ( |
||||
|
id INTEGER PRIMARY KEY, |
||||
|
game_id INTEGER NOT NULL, |
||||
|
alternate_name TEXT NOT NULL, |
||||
|
FOREIGN KEY (game_id) REFERENCES games (id), |
||||
|
UNIQUE (alternate_name COLLATE NOCASE) |
||||
|
) |
||||
|
''') |
||||
|
|
||||
|
conn.commit() |
||||
|
|
||||
|
def load_game_data(conn): |
||||
|
cursor = conn.cursor() |
||||
|
|
||||
|
games = [ |
||||
|
("Red", 1, ["Red Version"]), |
||||
|
("Blue", 1, ["Blue Version"]), |
||||
|
("Yellow", 1, ["Yellow Version"]), |
||||
|
("Gold", 2, ["Gold Version"]), |
||||
|
("Silver", 2, ["Silver Version"]), |
||||
|
("Crystal", 2, ["Crystal Version"]), |
||||
|
("Ruby", 3, ["Ruby Version"]), |
||||
|
("Sapphire", 3, ["Sapphire Version"]), |
||||
|
("Emerald", 3, ["Emerald Version"]), |
||||
|
("FireRed", 3, ["Fire Red", "Fire-Red"]), |
||||
|
("LeafGreen", 3, ["Leaf Green", "Leaf-Green"]), |
||||
|
("Diamond", 4, ["Diamond Version"]), |
||||
|
("Pearl", 4, ["Pearl Version"]), |
||||
|
("Platinum", 4, ["Platinum Version"]), |
||||
|
("HeartGold", 4, ["Heart Gold", "Heart-Gold"]), |
||||
|
("SoulSilver", 4, ["Soul Silver", "Soul-Silver"]), |
||||
|
("Black", 5, ["Black Version"]), |
||||
|
("White", 5, ["White Version"]), |
||||
|
("Black 2", 5, ["Black Version 2", "Black-2"]), |
||||
|
("White 2", 5, ["White Version 2", "White-2"]), |
||||
|
("X", 6, ["X Version"]), |
||||
|
("Y", 6, ["Y Version"]), |
||||
|
("Omega Ruby", 6, ["Omega Ruby Version", "Omega-Ruby"]), |
||||
|
("Alpha Sapphire", 6, ["Alpha Sapphire Version", "Alpha-Sapphire"]), |
||||
|
("Sun", 7, ["Sun Version"]), |
||||
|
("Moon", 7, ["Moon Version"]), |
||||
|
("Ultra Sun", 7, ["Ultra Sun Version", "Ultra-Sun"]), |
||||
|
("Ultra Moon", 7, ["Ultra Moon Version", "Ultra-Moon"]), |
||||
|
("Let's Go Pikachu", 7, ["Let's Go, Pikachu!", "Lets Go Pikachu"]), |
||||
|
("Let's Go Eevee", 7, ["Let's Go, Eevee!", "Lets Go Eevee"]), |
||||
|
("Sword", 8, ["Sword Version"]), |
||||
|
("Shield", 8, ["Shield Version"]), |
||||
|
("Brilliant Diamond", 8, ["Brilliant Diamond Version", "Brilliant-Diamond"]), |
||||
|
("Shining Pearl", 8, ["Shining Pearl Version", "Shining-Pearl"]), |
||||
|
("Legends Arceus", 8, ["Legends: Arceus", "Legends-Arceus"]), |
||||
|
("Scarlet", 9, ["Scarlet Version"]), |
||||
|
("Violet", 9, ["Violet Version"]), |
||||
|
] |
||||
|
|
||||
|
for game in games: |
||||
|
cursor.execute(''' |
||||
|
INSERT OR IGNORE INTO games (name, generation) |
||||
|
VALUES (?, ?) |
||||
|
''', (game[0], game[1])) |
||||
|
|
||||
|
game_id = cursor.lastrowid |
||||
|
|
||||
|
# Insert alternate names |
||||
|
for alt_name in game[2]: |
||||
|
cursor.execute(''' |
||||
|
INSERT OR IGNORE INTO alternate_game_names (game_id, alternate_name) |
||||
|
VALUES (?, ?) |
||||
|
''', (game_id, alt_name)) |
||||
|
|
||||
|
conn.commit() |
||||
|
|
||||
|
def load_mark_data(conn): |
||||
|
cursor = conn.cursor() |
||||
|
|
||||
|
marks = [ |
||||
|
("Game Boy", "images/marks/GB_icon_HOME.png", ["Red", "Blue", "Yellow", "Gold", "Silver", "Crystal", "Ruby", "Sapphire", "Emerald", "FireRed", "LeafGreen"]), |
||||
|
("Kalos", "images/marks/Blue_pentagon_HOME.png", ["X", "Y", "Omega Ruby", "Alpha Sapphire"]), |
||||
|
("Alola", "images/marks/Black_clover_HOME.png", ["Sun", "Moon", "Ultra Sun", "Ultra Moon"]), |
||||
|
("Let's Go", "images/marks/Let's_Go_icon_HOME.png", ["Let's Go Pikachu", "Let's Go Eevee"]), |
||||
|
("Galar", "images/marks/Galar_symbol_HOME.png", ["Sword", "Shield"]), |
||||
|
("Sinnoh", "images/marks/BDSP_icon_HOME.png", ["Brilliant Diamond", "Shining Pearl"]), |
||||
|
("Hisui", "images/marks/Arceus_mark_HOME.png", ["Legends Arceus"]), |
||||
|
("Paldea", "images/marks/Paldea_icon_HOME.png", ["Scarlet", "Violet"]), |
||||
|
] |
||||
|
|
||||
|
for mark in marks: |
||||
|
cursor.execute(''' |
||||
|
INSERT OR IGNORE INTO marks (name, icon_path) |
||||
|
VALUES (?, ?) |
||||
|
''', (mark[0], mark[1])) |
||||
|
|
||||
|
mark_id = cursor.lastrowid |
||||
|
|
||||
|
for game_name in mark[2]: |
||||
|
cursor.execute(''' |
||||
|
INSERT OR IGNORE INTO mark_game_associations (mark_id, game_id) |
||||
|
SELECT ?, id FROM games WHERE name = ? |
||||
|
''', (mark_id, game_name)) |
||||
|
|
||||
|
conn.commit() |
||||
|
|
||||
|
def load_pokemon_data(conn): |
||||
|
cursor = conn.cursor() |
||||
|
|
||||
|
with open('pokemon_home_list.csv', 'r') as f: |
||||
|
reader = csv.reader(f) |
||||
|
next(reader) # Skip header row if it exists |
||||
|
for row in reader: |
||||
|
national_dex_number = int(row[0]) |
||||
|
full_name = row[1] |
||||
|
|
||||
|
# Extract the base name and form name |
||||
|
match = re.match(r'([^(]+)(?:\s*\(([^)]+)\))?', full_name) |
||||
|
if match: |
||||
|
base_name = match.group(1).strip() |
||||
|
form_name = match.group(2).strip() if match.group(2) else "Default" |
||||
|
else: |
||||
|
base_name = full_name |
||||
|
form_name = "Default" |
||||
|
|
||||
|
# Insert or update the pokemon entry |
||||
|
cursor.execute(''' |
||||
|
INSERT OR IGNORE INTO pokemon (national_dex_number, name) |
||||
|
VALUES (?, ?) |
||||
|
''', (national_dex_number, base_name)) |
||||
|
|
||||
|
# Create the image path |
||||
|
padded_dex = f"{national_dex_number:04d}" |
||||
|
if form_name == "Default": |
||||
|
image_path = f"images/pokemon/{padded_dex}_{base_name}.png" |
||||
|
else: |
||||
|
image_path = f"images/pokemon/{padded_dex}_{base_name}_({form_name}).png".replace(" ", "_") |
||||
|
|
||||
|
# Insert the form entry |
||||
|
cursor.execute(''' |
||||
|
INSERT OR IGNORE INTO pokemon_forms (pokemon_id, form_name, is_default, image_path) |
||||
|
VALUES (?, ?, ?, ?) |
||||
|
''', (national_dex_number, form_name, form_name == "Default", image_path)) |
||||
|
|
||||
|
conn.commit() |
||||
|
|
||||
|
def load_encounter_data(conn): |
||||
|
cursor = conn.cursor() |
||||
|
|
||||
|
with open('pokemon_earliest_games.csv', 'r') as f: |
||||
|
reader = csv.DictReader(f) |
||||
|
for row in reader: |
||||
|
national_dex_number = int(row['number']) |
||||
|
full_name = row['name'] |
||||
|
earliest_game = row['earliest_game'] |
||||
|
obtain_method = row['obtain_method'] |
||||
|
encounter_locations = row['encounter_locations'] |
||||
|
|
||||
|
# Extract the base name and form name |
||||
|
match = re.match(r'([^(]+)(?:\s*\(([^)]+)\))?', full_name) |
||||
|
if match: |
||||
|
base_name = match.group(1).strip() |
||||
|
form_name = match.group(2).strip() if match.group(2) else "Default" |
||||
|
else: |
||||
|
base_name = full_name |
||||
|
form_name = "Default" |
||||
|
|
||||
|
# Ensure the Pokémon and form exist in the database |
||||
|
cursor.execute(''' |
||||
|
INSERT OR IGNORE INTO pokemon (national_dex_number, name) |
||||
|
VALUES (?, ?) |
||||
|
''', (national_dex_number, base_name)) |
||||
|
|
||||
|
cursor.execute(''' |
||||
|
INSERT OR IGNORE INTO pokemon_forms (pokemon_id, form_name, is_default, image_path) |
||||
|
VALUES (?, ?, ?, ?) |
||||
|
''', (national_dex_number, form_name, form_name == "Default", f"images/pokemon/{national_dex_number:04d}_{base_name}.png")) |
||||
|
|
||||
|
# Skip encounter data if it's unknown or N/A |
||||
|
if earliest_game == "Unknown" or obtain_method == "Unknown": |
||||
|
continue |
||||
|
|
||||
|
# Get the form_id |
||||
|
cursor.execute(''' |
||||
|
SELECT id FROM pokemon_forms |
||||
|
WHERE pokemon_id = ? AND form_name = ? |
||||
|
''', (national_dex_number, form_name)) |
||||
|
form_id = cursor.fetchone()[0] |
||||
|
|
||||
|
# Get the game_id (now case-insensitive and including alternate names) |
||||
|
cursor.execute(''' |
||||
|
SELECT g.id FROM games g |
||||
|
LEFT JOIN alternate_game_names agn ON g.id = agn.game_id |
||||
|
WHERE g.name = ? COLLATE NOCASE OR agn.alternate_name = ? COLLATE NOCASE |
||||
|
''', (earliest_game, earliest_game)) |
||||
|
result = cursor.fetchone() |
||||
|
if result: |
||||
|
game_id = result[0] |
||||
|
else: |
||||
|
print(f"Warning: Game '{earliest_game}' not found for {full_name}") |
||||
|
continue |
||||
|
|
||||
|
# Handle gift Pokémon |
||||
|
if obtain_method.lower() == "gift" and (encounter_locations == "N/A" or not encounter_locations): |
||||
|
# Insert or get the "Gift" location |
||||
|
cursor.execute(''' |
||||
|
INSERT OR IGNORE INTO locations (name, description) |
||||
|
VALUES (?, ?) |
||||
|
''', ("Gift", "Pokémon received as a gift")) |
||||
|
cursor.execute('SELECT id FROM locations WHERE name = ?', ("Gift",)) |
||||
|
location_id = cursor.fetchone()[0] |
||||
|
|
||||
|
# Insert or get the "gift" encounter method |
||||
|
cursor.execute('INSERT OR IGNORE INTO encounter_methods (name) VALUES (?)', ("gift",)) |
||||
|
cursor.execute('SELECT id FROM encounter_methods WHERE name = ?', ("gift",)) |
||||
|
method_id = cursor.fetchone()[0] |
||||
|
|
||||
|
# Insert form_encounter for gift Pokémon |
||||
|
cursor.execute(''' |
||||
|
INSERT OR IGNORE INTO form_encounters |
||||
|
(form_id, game_id, location_id, encounter_method_id) |
||||
|
VALUES (?, ?, ?, ?) |
||||
|
''', (form_id, game_id, location_id, method_id)) |
||||
|
elif encounter_locations != "N/A" and encounter_locations: |
||||
|
# Process each encounter location |
||||
|
for location_info in encounter_locations.split('|'): |
||||
|
location, method = location_info.strip().rsplit(' ', 1) |
||||
|
method = method.strip('()') |
||||
|
|
||||
|
# Insert or get location_id |
||||
|
cursor.execute(''' |
||||
|
INSERT OR IGNORE INTO locations (name, description) |
||||
|
VALUES (?, ?) |
||||
|
''', (location, None)) |
||||
|
cursor.execute('SELECT id FROM locations WHERE name = ?', (location,)) |
||||
|
location_id = cursor.fetchone()[0] |
||||
|
|
||||
|
# Insert or get encounter_method_id |
||||
|
cursor.execute('INSERT OR IGNORE INTO encounter_methods (name) VALUES (?)', (method,)) |
||||
|
cursor.execute('SELECT id FROM encounter_methods WHERE name = ?', (method,)) |
||||
|
method_id = cursor.fetchone()[0] |
||||
|
|
||||
|
# Insert form_encounter |
||||
|
cursor.execute(''' |
||||
|
INSERT OR IGNORE INTO form_encounters |
||||
|
(form_id, game_id, location_id, encounter_method_id) |
||||
|
VALUES (?, ?, ?, ?) |
||||
|
''', (form_id, game_id, location_id, method_id)) |
||||
|
|
||||
|
conn.commit() |
||||
|
|
||||
|
def main(): |
||||
|
conn = create_connection() |
||||
|
create_tables(conn) |
||||
|
load_game_data(conn) |
||||
|
load_mark_data(conn) |
||||
|
load_pokemon_data(conn) |
||||
|
load_encounter_data(conn) |
||||
|
conn.close() |
||||
|
print("All data has been successfully added to the database.") |
||||
|
|
||||
|
if __name__ == "__main__": |
||||
|
main() |
||||
@ -0,0 +1,386 @@ |
|||||
|
import csv |
||||
|
import requests |
||||
|
import time |
||||
|
import json |
||||
|
import os |
||||
|
import re |
||||
|
import sqlite3 |
||||
|
|
||||
|
# Initialize the database connection |
||||
|
conn = sqlite3.connect('pokemon_cache.db') |
||||
|
cursor = conn.cursor() |
||||
|
|
||||
|
# Create the cache table if it doesn't exist |
||||
|
cursor.execute(''' |
||||
|
CREATE TABLE IF NOT EXISTS cache ( |
||||
|
key TEXT PRIMARY KEY, |
||||
|
value TEXT |
||||
|
) |
||||
|
''') |
||||
|
conn.commit() |
||||
|
|
||||
|
# List of all main series Pokémon games in chronological order, with special games first in each generation |
||||
|
all_games = [ |
||||
|
"Yellow", "Red", "Blue", |
||||
|
"Crystal", "Gold", "Silver", |
||||
|
"Emerald", "FireRed", "LeafGreen", "Ruby", "Sapphire", |
||||
|
"Platinum", "HeartGold", "SoulSilver", "Diamond", "Pearl", |
||||
|
"Black-2", "White-2", "Black", "White", |
||||
|
"X", "Y", "Omega-Ruby", "Alpha-Sapphire", |
||||
|
"Ultra-Sun", "Ultra-Moon", "Sun", "Moon", |
||||
|
"Sword", "Shield", |
||||
|
"Brilliant-Diamond", "Shining-Pearl", |
||||
|
"Legends-Arceus", |
||||
|
"Scarlet", "Violet", |
||||
|
"Unknown" |
||||
|
] |
||||
|
|
||||
|
cache = {} |
||||
|
new_entries_count = 0 |
||||
|
|
||||
|
def get_cached_data(): |
||||
|
global cache |
||||
|
cursor.execute("SELECT key, value FROM cache") |
||||
|
for key, value in cursor.fetchall(): |
||||
|
cache[key] = json.loads(value) |
||||
|
|
||||
|
def save_cached_data(): |
||||
|
global cache, new_entries_count |
||||
|
if new_entries_count > 0: |
||||
|
for key, value in cache.items(): |
||||
|
cursor.execute("INSERT OR REPLACE INTO cache (key, value) VALUES (?, ?)", |
||||
|
(key, json.dumps(value))) |
||||
|
conn.commit() |
||||
|
new_entries_count = 0 |
||||
|
|
||||
|
def update_cache(key, value): |
||||
|
global cache, new_entries_count |
||||
|
if key not in cache: |
||||
|
cache[key] = value |
||||
|
new_entries_count += 1 |
||||
|
if new_entries_count >= 10: |
||||
|
save_cached_data() |
||||
|
time.sleep(1) |
||||
|
|
||||
|
def read_pokemon_list(filename, limit=50): |
||||
|
pokemon_list = [] |
||||
|
with open(filename, 'r', newline='', encoding='utf-8') as csvfile: |
||||
|
reader = csv.DictReader(csvfile) |
||||
|
for i, row in enumerate(reader): |
||||
|
if i >= limit: |
||||
|
break |
||||
|
# Split the name into base name and form |
||||
|
match = re.match(r'(.*?)\s*(\(.*\))?$', row['name']) |
||||
|
base_name, form = match.groups() if match else (row['name'], None) |
||||
|
row['base_name'] = base_name.strip() |
||||
|
row['form'] = form.strip('() ') if form else None |
||||
|
pokemon_list.append(row) |
||||
|
return pokemon_list |
||||
|
|
||||
|
def sanitize_name_and_form(name, form): |
||||
|
adjusted_form = None |
||||
|
if form: |
||||
|
adjusted_form = form.lower() |
||||
|
#Some stupid special cases |
||||
|
if name.lower() == 'tauros': |
||||
|
if adjusted_form == 'paldean form': |
||||
|
adjusted_form = 'paldea combat breed' |
||||
|
elif 'blaze' in adjusted_form: |
||||
|
adjusted_form = 'paldea blaze breed' |
||||
|
elif 'aqua' in adjusted_form: |
||||
|
adjusted_form = 'paldea aqua breed' |
||||
|
|
||||
|
replacements = {'forme': '', |
||||
|
'form': '', |
||||
|
'alolan': 'alola', |
||||
|
'galarian': 'galar', |
||||
|
'hisuian': 'hisui', |
||||
|
'paldean': 'paldea', |
||||
|
'size': '', |
||||
|
'10%': '10 power construct', |
||||
|
'hoopa': '', |
||||
|
'style': '', |
||||
|
'core': '', |
||||
|
'color': '', |
||||
|
'blood moon': 'bloodmoon'}; |
||||
|
for old, new in replacements.items(): |
||||
|
adjusted_form = adjusted_form.replace(old, new).strip() |
||||
|
|
||||
|
missing_forms = ['burmy', |
||||
|
'shellos', |
||||
|
'gastrodon', |
||||
|
'wormadam', |
||||
|
'unown', |
||||
|
"deerling", |
||||
|
"sawsbuck", |
||||
|
"vivillon", |
||||
|
"flabébé", |
||||
|
"floette", |
||||
|
"florges", |
||||
|
"furfrou", |
||||
|
"sinistea", |
||||
|
"polteageist", |
||||
|
"alcremie", |
||||
|
"poltchageist", |
||||
|
"sinistcha"] |
||||
|
|
||||
|
if name.lower() in missing_forms: |
||||
|
adjusted_form = None |
||||
|
|
||||
|
if name.lower() == 'wormadam': |
||||
|
adjusted_form = adjusted_form.replace('cloak', '').strip() |
||||
|
if name.lower() == 'rotom': |
||||
|
adjusted_form = adjusted_form.replace('rotom', '').strip() |
||||
|
if name.lower() == 'darmanitan': |
||||
|
adjusted_form = adjusted_form + ' standard' |
||||
|
|
||||
|
else: |
||||
|
default_forms = {'deoxys': 'normal', |
||||
|
'wormadam': 'plant', |
||||
|
'giratina': 'origin', |
||||
|
'tornadus': 'incarnate', |
||||
|
'shaymin': 'land', |
||||
|
'basculin': 'red-striped', |
||||
|
'darmanitan': 'standard', |
||||
|
'thundurus': 'incarnate', |
||||
|
'landorus': 'incarnate', |
||||
|
'enamorus': 'incarnate', |
||||
|
'keldeo': 'ordinary', |
||||
|
'meloetta': 'aria', |
||||
|
'meowstic': 'male', |
||||
|
'aegislash': 'shield', |
||||
|
'pumpkaboo': 'average', |
||||
|
'gourgeist': 'average', |
||||
|
'minior': 'red-meteor', |
||||
|
'zygarde': '50 power construct', |
||||
|
'oricorio': 'baile', |
||||
|
'lycanroc': 'midday', |
||||
|
'wishiwashi': 'solo', |
||||
|
'mimikyu': 'disguised', |
||||
|
'cramorant': 'gulping', |
||||
|
'toxtricity': 'low-key', |
||||
|
'eiscue': 'ice', |
||||
|
'indeedee': 'male', |
||||
|
'urshifu': 'single-strike', |
||||
|
'morpeko': 'full belly', |
||||
|
'oinkologne': 'male', |
||||
|
'maushold': 'family of three', |
||||
|
'squawkabilly': 'green plumage', |
||||
|
'palafin': 'zero', |
||||
|
'tatsugiri': 'curly', |
||||
|
'dudunsparce': 'two segment', |
||||
|
'basculegion': 'male'} |
||||
|
|
||||
|
if name.lower() in default_forms: |
||||
|
adjusted_form = default_forms[name.lower()] |
||||
|
|
||||
|
if adjusted_form: |
||||
|
api_name = f"{name.lower()}-{adjusted_form}" |
||||
|
else: |
||||
|
api_name = name.lower() |
||||
|
|
||||
|
api_name = api_name.replace(' ', '-').replace("'", "").replace(".", "").replace('é', 'e').replace(':', '') |
||||
|
|
||||
|
#more special cases |
||||
|
if api_name == 'oinkologne-male': |
||||
|
api_name = '916' |
||||
|
|
||||
|
return api_name |
||||
|
|
||||
|
def get_pokemon_data(pokemon_name, form, cache): |
||||
|
cache_key = f"pokemon_{pokemon_name}_{form}" if form else f"pokemon_{pokemon_name}" |
||||
|
if cache_key in cache: |
||||
|
return cache[cache_key] |
||||
|
|
||||
|
api_name = sanitize_name_and_form(pokemon_name, form) |
||||
|
|
||||
|
url = f"https://pokeapi.co/api/v2/pokemon/{api_name}" |
||||
|
print(f"Fetching Pokémon data for {pokemon_name}: {url}") |
||||
|
response = requests.get(url) |
||||
|
if response.status_code == 200: |
||||
|
data = response.json() |
||||
|
update_cache(cache_key, data) |
||||
|
return data |
||||
|
return None |
||||
|
|
||||
|
def get_pokemon_encounter_data(pokemon_name, form, cache): |
||||
|
cache_key = f"pokemon_encounter_{pokemon_name}_{form}" if form else f"pokemon_encounter_{pokemon_name}" |
||||
|
if cache_key in cache: |
||||
|
return cache[cache_key] |
||||
|
|
||||
|
api_name = sanitize_name_and_form(pokemon_name, form) |
||||
|
|
||||
|
url = f"https://pokeapi.co/api/v2/pokemon/{api_name}/encounters" |
||||
|
print(f"Fetching encounter data for {pokemon_name}: {url}") |
||||
|
response = requests.get(url) |
||||
|
if response.status_code == 200: |
||||
|
data = response.json() |
||||
|
update_cache(cache_key, data) |
||||
|
return data |
||||
|
else: |
||||
|
return None |
||||
|
|
||||
|
def get_earliest_game(encounter_data): |
||||
|
if not encounter_data: |
||||
|
return "Unknown", "Unknown" |
||||
|
|
||||
|
game_methods = {} |
||||
|
for location_area in encounter_data: |
||||
|
for version_detail in location_area['version_details']: |
||||
|
game = version_detail['version']['name'] |
||||
|
is_gift = any(method['method']['name'] == 'gift' for method in version_detail['encounter_details']) |
||||
|
|
||||
|
if game not in game_methods: |
||||
|
game_methods[game] = "Gift" if is_gift else "Catchable" |
||||
|
elif game_methods[game] == "Gift" and not is_gift: |
||||
|
game_methods[game] = "Catchable" |
||||
|
|
||||
|
for game in all_games: |
||||
|
if game.lower() in game_methods: |
||||
|
return game, game_methods[game.lower()] |
||||
|
|
||||
|
return "Unknown", "Unknown" |
||||
|
|
||||
|
def determine_earliest_games(pokemon_list, cache): |
||||
|
for pokemon in pokemon_list: |
||||
|
pokemon_data = get_pokemon_data(pokemon['base_name'], pokemon['form'], cache) |
||||
|
encounter_data = get_pokemon_encounter_data(pokemon['base_name'], pokemon['form'], cache) |
||||
|
pokemon['earliest_game'], pokemon['obtain_method'] = get_earliest_game(encounter_data) |
||||
|
print(f"Processed {pokemon['name']} (#{pokemon['number']}): {pokemon['earliest_game']} ({pokemon['obtain_method']})") |
||||
|
return pokemon_list |
||||
|
|
||||
|
def get_species_data(pokemon_name, cache): |
||||
|
cache_key = f"species_{pokemon_name}" |
||||
|
if cache_key in cache: |
||||
|
return cache[cache_key] |
||||
|
|
||||
|
api_name = sanitize_name_and_form(pokemon_name, None) |
||||
|
|
||||
|
url = f"https://pokeapi.co/api/v2/pokemon-species/{api_name}/" |
||||
|
print(f"Fetching species data for {pokemon_name}: {url}") |
||||
|
response = requests.get(url) |
||||
|
if response.status_code == 200: |
||||
|
data = response.json() |
||||
|
update_cache(cache_key, data) |
||||
|
return data |
||||
|
return None |
||||
|
|
||||
|
def get_evolution_chain(pokemon_name, cache): |
||||
|
species_data = get_species_data(pokemon_name, cache) |
||||
|
if not species_data: |
||||
|
return None |
||||
|
|
||||
|
cache_key = f"evolution_{species_data['evolution_chain']['url']}" |
||||
|
if cache_key in cache: |
||||
|
return cache[cache_key] |
||||
|
|
||||
|
evolution_response = requests.get(species_data['evolution_chain']['url']) |
||||
|
if evolution_response.status_code == 200: |
||||
|
evolution_data = evolution_response.json() |
||||
|
update_cache(cache_key, evolution_data) |
||||
|
return evolution_data |
||||
|
return None |
||||
|
|
||||
|
def get_base_form(evolution_chain, cache): |
||||
|
if not evolution_chain or 'chain' not in evolution_chain: |
||||
|
return None |
||||
|
|
||||
|
current = evolution_chain['chain'] |
||||
|
while current: |
||||
|
species_name = current['species']['name'] |
||||
|
species_data = get_species_data(species_name, cache) |
||||
|
|
||||
|
if species_data and not species_data.get('is_baby', False): |
||||
|
return species_name |
||||
|
|
||||
|
if not current['evolves_to']: |
||||
|
return species_name |
||||
|
|
||||
|
current = current['evolves_to'][0] |
||||
|
|
||||
|
return None |
||||
|
|
||||
|
def adjust_for_evolution(pokemon_list, cache): |
||||
|
pokemon_dict = {f"{pokemon['base_name']}_{pokemon['form']}".lower(): pokemon for pokemon in pokemon_list} |
||||
|
|
||||
|
for pokemon in pokemon_list: |
||||
|
species_data = get_species_data(pokemon['base_name'], cache) |
||||
|
evolution_chain = get_evolution_chain(pokemon['base_name'], cache) |
||||
|
base_form = get_base_form(evolution_chain, cache) |
||||
|
|
||||
|
# Check if the Pokémon is a baby |
||||
|
if species_data and species_data.get('is_baby', False): |
||||
|
pokemon['obtain_method'] = 'Breed' |
||||
|
elif base_form: |
||||
|
base_key = f"{base_form}_{pokemon['form']}".lower() |
||||
|
if base_key in pokemon_dict: |
||||
|
base_pokemon = pokemon_dict[base_key] |
||||
|
if all_games.index(base_pokemon['earliest_game']) <= all_games.index(pokemon['earliest_game']) and base_pokemon['number'] != pokemon['number']: |
||||
|
pokemon['earliest_game'] = base_pokemon['earliest_game'] |
||||
|
pokemon['obtain_method'] = 'Evolve' |
||||
|
|
||||
|
print(f"Adjusted {pokemon['name']} (#{pokemon['number']}): {pokemon['earliest_game']} ({pokemon['obtain_method']})") |
||||
|
|
||||
|
return pokemon_list |
||||
|
|
||||
|
def save_to_csv(pokemon_list, filename='pokemon_earliest_games.csv'): |
||||
|
with open(filename, 'w', newline='', encoding='utf-8') as csvfile: |
||||
|
fieldnames = ['number', 'name', 'earliest_game', 'obtain_method', 'encounter_locations'] |
||||
|
writer = csv.DictWriter(csvfile, fieldnames=fieldnames) |
||||
|
|
||||
|
writer.writeheader() |
||||
|
for pokemon in pokemon_list: |
||||
|
writer.writerow({ |
||||
|
'number': pokemon['number'], |
||||
|
'name': pokemon['name'], |
||||
|
'earliest_game': pokemon['earliest_game'], |
||||
|
'obtain_method': pokemon['obtain_method'], |
||||
|
'encounter_locations': pokemon['encounter_locations'] |
||||
|
}) |
||||
|
|
||||
|
def parse_encounter_locations(encounter_data, game): |
||||
|
locations = [] |
||||
|
for location_area in encounter_data: |
||||
|
for version_detail in location_area['version_details']: |
||||
|
if version_detail['version']['name'] == game.lower(): |
||||
|
location_name = location_area['location_area']['name'] |
||||
|
for encounter_detail in version_detail['encounter_details']: |
||||
|
method = encounter_detail['method']['name'] |
||||
|
condition = encounter_detail.get('condition', 'Any') |
||||
|
time = ', '.join(encounter_detail.get('time', ['Any'])) |
||||
|
|
||||
|
encounter_info = f"{location_name} ({method}" |
||||
|
if condition != 'Any': |
||||
|
encounter_info += f", {condition}" |
||||
|
if time != 'Any': |
||||
|
encounter_info += f", {time}" |
||||
|
encounter_info += ")" |
||||
|
|
||||
|
if encounter_info not in locations: |
||||
|
locations.append(encounter_info) |
||||
|
return locations |
||||
|
|
||||
|
def add_encounter_locations(pokemon_list, cache): |
||||
|
for pokemon in pokemon_list: |
||||
|
if pokemon['obtain_method'] == 'Catchable': |
||||
|
encounter_data = get_pokemon_encounter_data(pokemon['base_name'], pokemon['form'], cache) |
||||
|
locations = parse_encounter_locations(encounter_data, pokemon['earliest_game']) |
||||
|
pokemon['encounter_locations'] = ' | '.join(locations) if locations else 'Unknown' |
||||
|
else: |
||||
|
pokemon['encounter_locations'] = 'N/A' |
||||
|
print(f"Added encounter locations for {pokemon['name']} (#{pokemon['number']}) in {pokemon['earliest_game']}") |
||||
|
return pokemon_list |
||||
|
|
||||
|
# Update the main function |
||||
|
if __name__ == "__main__": |
||||
|
get_cached_data() |
||||
|
|
||||
|
pokemon_list = read_pokemon_list('pokemon_home_list.csv', limit=3000) |
||||
|
pokemon_list_with_games = determine_earliest_games(pokemon_list, cache) |
||||
|
pokemon_list_adjusted = adjust_for_evolution(pokemon_list_with_games, cache) |
||||
|
pokemon_list_with_locations = add_encounter_locations(pokemon_list_adjusted, cache) |
||||
|
save_to_csv(pokemon_list_with_locations) |
||||
|
|
||||
|
save_cached_data() # Save any remaining new entries |
||||
|
conn.close() # Close the database connection |
||||
|
print(f"Earliest obtainable games and encounter locations determined for {len(pokemon_list)} Pokémon and saved to pokemon_earliest_games.csv") |
||||
@ -0,0 +1,111 @@ |
|||||
|
import requests |
||||
|
from bs4 import BeautifulSoup |
||||
|
import csv |
||||
|
import os |
||||
|
import time |
||||
|
import re |
||||
|
|
||||
|
def scrape_serebii_region_pokemon(url): |
||||
|
response = requests.get(url) |
||||
|
soup = BeautifulSoup(response.content, 'html.parser') |
||||
|
|
||||
|
pokemon_list = [] |
||||
|
|
||||
|
# Find the main table containing Pokémon data |
||||
|
table = soup.find('table', class_='dextable') |
||||
|
|
||||
|
if table: |
||||
|
rows = table.find_all('tr')[2:] # Skip the header row and the game intro row |
||||
|
for row in rows: |
||||
|
cells = row.find_all('td') |
||||
|
if len(cells) <= 5: # Ensure we have enough cells to check depositability. if only 5 then its not depositable in any game. |
||||
|
continue |
||||
|
|
||||
|
number = cells[0].text.strip().lstrip('#') |
||||
|
name = cells[2].text.strip() |
||||
|
|
||||
|
# Get the image URL |
||||
|
img_url = cells[1].find('img')['src'] |
||||
|
full_img_url = f"https://www.serebii.net{img_url}" |
||||
|
|
||||
|
pokemon_list.append({ |
||||
|
'number': number, |
||||
|
'name': name, |
||||
|
'image_url': full_img_url |
||||
|
}) |
||||
|
|
||||
|
return pokemon_list |
||||
|
|
||||
|
def download_image(url, filename): |
||||
|
response = requests.get(url) |
||||
|
if response.status_code == 200: |
||||
|
with open(filename, 'wb') as f: |
||||
|
f.write(response.content) |
||||
|
|
||||
|
def sanitize_filename(filename): |
||||
|
# Define a dictionary of symbol replacements |
||||
|
symbol_replacements = { |
||||
|
'?': 'questionmark', |
||||
|
'*': 'asterisk', |
||||
|
':': 'colon', |
||||
|
'/': 'slash', |
||||
|
'\\': 'backslash', |
||||
|
'|': 'pipe', |
||||
|
'<': 'lessthan', |
||||
|
'>': 'greaterthan', |
||||
|
'"': 'quote', |
||||
|
' ': '_' |
||||
|
} |
||||
|
|
||||
|
# Replace symbols with their word equivalents |
||||
|
for symbol, word in symbol_replacements.items(): |
||||
|
filename = filename.replace(symbol, word) |
||||
|
|
||||
|
# Remove any remaining invalid characters |
||||
|
return re.sub(r'[<>:"/\\|?*]', '', filename) |
||||
|
|
||||
|
def scrape_all_regions(): |
||||
|
base_url = "https://www.serebii.net/pokemonhome/" |
||||
|
regions = ["kanto", "johto", "hoenn", "sinnoh", "unova", "kalos", "alola", "galar", "paldea", "hisui", "unknown"] |
||||
|
all_pokemon = [] |
||||
|
|
||||
|
for region in regions: |
||||
|
url = f"{base_url}{region}pokemon.shtml" |
||||
|
region_pokemon = scrape_serebii_region_pokemon(url) |
||||
|
all_pokemon.extend(region_pokemon) |
||||
|
print(f"Scraped {len(region_pokemon)} Pokémon from {region.capitalize()} region") |
||||
|
time.sleep(1) # Be nice to the server |
||||
|
|
||||
|
return all_pokemon |
||||
|
|
||||
|
def save_to_csv(pokemon_list, filename='pokemon_home_list.csv'): |
||||
|
with open(filename, 'w', newline='', encoding='utf-8') as csvfile: |
||||
|
fieldnames = ['number', 'name'] |
||||
|
writer = csv.DictWriter(csvfile, fieldnames=fieldnames) |
||||
|
|
||||
|
writer.writeheader() |
||||
|
for pokemon in pokemon_list: |
||||
|
writer.writerow({k: pokemon[k] for k in fieldnames}) |
||||
|
|
||||
|
if __name__ == "__main__": |
||||
|
all_pokemon = scrape_all_regions() |
||||
|
save_to_csv(all_pokemon) |
||||
|
print(f"Scraped a total of {len(all_pokemon)} Pokémon and saved to pokemon_home_list.csv") |
||||
|
|
||||
|
# Create 'images' directory if it doesn't exist |
||||
|
if not os.path.exists('images'): |
||||
|
os.makedirs('images') |
||||
|
|
||||
|
# Download images |
||||
|
for pokemon in all_pokemon: |
||||
|
sanitized_name = sanitize_filename(pokemon['name']) |
||||
|
filename = f"images/{pokemon['number']}_{sanitized_name}.png" |
||||
|
|
||||
|
if os.path.exists(filename): |
||||
|
print(f"Image for {pokemon['name']} already exists, skipping download") |
||||
|
else: |
||||
|
download_image(pokemon['image_url'], filename) |
||||
|
print(f"Downloaded image for {pokemon['name']}") |
||||
|
time.sleep(0.5) # Be nice to the server |
||||
|
|
||||
|
print("All images downloaded successfully.") |
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1005 B |
|
After Width: | Height: | Size: 789 B |
|
After Width: | Height: | Size: 749 B |
|
After Width: | Height: | Size: 780 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 959 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 11 KiB |