From 276302570f38e525a6a813232dc246cc400e95ba Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Tue, 14 Jan 2025 13:10:24 +0100 Subject: [PATCH] merged command line and gui app in one command --- python/{cli => }/.gitignore | 3 + python/{cli => }/CHANGELOG.md | 0 python/{cli => }/Dockerfile | 0 python/{cli => }/LICENSE.md | 0 python/{cli => }/MANIFEST.in | 0 python/{cli => }/Makefile | 0 python/{cli => }/README.md | 0 python/cli/kontor/controllers/clibase.py | 60 -------- python/cli/kontor/database/base.py | 20 --- python/{cli => }/config/kontor.yml.example | 0 python/{cli => }/docs/.gitkeep | 0 python/{cli => }/kontor/__init__.py | 0 .../{cli => }/kontor/controllers/__init__.py | 0 python/kontor/controllers/clibase.py | 42 ++++++ python/kontor/controllers/database.py | 32 ++++ python/{cli => }/kontor/core/__init__.py | 0 python/{cli => }/kontor/core/exc.py | 0 python/{cli => }/kontor/core/version.py | 0 python/{cli => }/kontor/database/__init__.py | 123 ++++++---------- python/kontor/database/base.py | 5 + python/{cli => }/kontor/database/comic.py | 0 python/{cli => }/kontor/database/media.py | 0 python/{cli => }/kontor/database/metadata.py | 1 + python/{qt => kontor}/database/tysc.py | 0 python/{cli => }/kontor/ext/__init__.py | 0 .../kontor/plugins => kontor/gui}/__init__.py | 0 python/{qt => kontor}/gui/data_view.py | 0 python/{qt => kontor}/gui/data_view_model.py | 0 python/{qt => kontor}/gui/dialogs.py | 0 python/kontor/gui/main_window.py | 139 ++++++++++++++++++ python/kontor/gui/model_config.py | 50 +++++++ python/kontor/gui/table_model.py | 108 ++++++++++++++ python/{cli => }/kontor/main.py | 14 +- .../templates => kontor/plugins}/__init__.py | 0 .../{qt => kontor}/res/application-export.png | Bin .../{qt => kontor}/res/application-import.png | Bin .../res/arrow-circle-double.png | Bin python/{qt => kontor}/res/cross.png | Bin python/{qt => kontor}/res/tick.png | Bin .../{qt/gui => kontor/templates}/__init__.py | 0 .../kontor/templates/command1.jinja2 | 0 python/main.py | 16 -- python/{cli => }/requirements-dev.txt | 0 python/{cli => }/requirements.txt | 1 + python/{cli => }/setup.cfg | 0 python/{cli => }/setup.py | 0 python/{cli => }/tests/conftest.py | 0 python/{cli => }/tests/test_kontor.py | 0 {python/qt => qt}/.gitignore | 0 {python/qt => qt}/database/__init__.py | 0 {python/qt => qt}/database/base.py | 0 {python/qt => qt}/database/comic.py | 0 {python/qt => qt}/database/media.py | 0 {python/qt => qt}/database/metadata.py | 0 {python/cli/kontor => qt}/database/tysc.py | 8 +- qt/gui/__init__.py | 0 qt/gui/data_view.py | 12 ++ qt/gui/data_view_model.py | 32 ++++ qt/gui/dialogs.py | 106 +++++++++++++ {python/qt => qt}/gui/main_window.py | 0 {python/qt => qt}/gui/model_config.py | 0 {python/qt => qt}/gui/table_model.py | 0 {python/qt => qt}/kontor.py | 0 {python/qt => qt}/pysidedeploy.spec | 0 qt/res/application-export.png | Bin 0 -> 513 bytes qt/res/application-import.png | Bin 0 -> 524 bytes qt/res/arrow-circle-double.png | Bin 0 -> 836 bytes qt/res/cross.png | Bin 0 -> 544 bytes qt/res/tick.png | Bin 0 -> 634 bytes 69 files changed, 589 insertions(+), 183 deletions(-) rename python/{cli => }/.gitignore (98%) rename python/{cli => }/CHANGELOG.md (100%) rename python/{cli => }/Dockerfile (100%) rename python/{cli => }/LICENSE.md (100%) rename python/{cli => }/MANIFEST.in (100%) rename python/{cli => }/Makefile (100%) rename python/{cli => }/README.md (100%) delete mode 100644 python/cli/kontor/controllers/clibase.py delete mode 100644 python/cli/kontor/database/base.py rename python/{cli => }/config/kontor.yml.example (100%) rename python/{cli => }/docs/.gitkeep (100%) rename python/{cli => }/kontor/__init__.py (100%) rename python/{cli => }/kontor/controllers/__init__.py (100%) create mode 100644 python/kontor/controllers/clibase.py create mode 100644 python/kontor/controllers/database.py rename python/{cli => }/kontor/core/__init__.py (100%) rename python/{cli => }/kontor/core/exc.py (100%) rename python/{cli => }/kontor/core/version.py (100%) rename python/{cli => }/kontor/database/__init__.py (55%) create mode 100644 python/kontor/database/base.py rename python/{cli => }/kontor/database/comic.py (100%) rename python/{cli => }/kontor/database/media.py (100%) rename python/{cli => }/kontor/database/metadata.py (99%) rename python/{qt => kontor}/database/tysc.py (100%) rename python/{cli => }/kontor/ext/__init__.py (100%) rename python/{cli/kontor/plugins => kontor/gui}/__init__.py (100%) rename python/{qt => kontor}/gui/data_view.py (100%) rename python/{qt => kontor}/gui/data_view_model.py (100%) rename python/{qt => kontor}/gui/dialogs.py (100%) create mode 100644 python/kontor/gui/main_window.py create mode 100644 python/kontor/gui/model_config.py create mode 100644 python/kontor/gui/table_model.py rename python/{cli => }/kontor/main.py (91%) rename python/{cli/kontor/templates => kontor/plugins}/__init__.py (100%) rename python/{qt => kontor}/res/application-export.png (100%) rename python/{qt => kontor}/res/application-import.png (100%) rename python/{qt => kontor}/res/arrow-circle-double.png (100%) rename python/{qt => kontor}/res/cross.png (100%) rename python/{qt => kontor}/res/tick.png (100%) rename python/{qt/gui => kontor/templates}/__init__.py (100%) rename python/{cli => }/kontor/templates/command1.jinja2 (100%) delete mode 100644 python/main.py rename python/{cli => }/requirements-dev.txt (100%) rename python/{cli => }/requirements.txt (90%) rename python/{cli => }/setup.cfg (100%) rename python/{cli => }/setup.py (100%) rename python/{cli => }/tests/conftest.py (100%) rename python/{cli => }/tests/test_kontor.py (100%) rename {python/qt => qt}/.gitignore (100%) rename {python/qt => qt}/database/__init__.py (100%) rename {python/qt => qt}/database/base.py (100%) rename {python/qt => qt}/database/comic.py (100%) rename {python/qt => qt}/database/media.py (100%) rename {python/qt => qt}/database/metadata.py (100%) rename {python/cli/kontor => qt}/database/tysc.py (94%) create mode 100644 qt/gui/__init__.py create mode 100644 qt/gui/data_view.py create mode 100644 qt/gui/data_view_model.py create mode 100644 qt/gui/dialogs.py rename {python/qt => qt}/gui/main_window.py (100%) rename {python/qt => qt}/gui/model_config.py (100%) rename {python/qt => qt}/gui/table_model.py (100%) rename {python/qt => qt}/kontor.py (100%) rename {python/qt => qt}/pysidedeploy.spec (100%) mode change 100755 => 100644 create mode 100644 qt/res/application-export.png create mode 100644 qt/res/application-import.png create mode 100644 qt/res/arrow-circle-double.png create mode 100644 qt/res/cross.png create mode 100644 qt/res/tick.png diff --git a/python/cli/.gitignore b/python/.gitignore similarity index 98% rename from python/cli/.gitignore rename to python/.gitignore index a74b246..0f47561 100644 --- a/python/cli/.gitignore +++ b/python/.gitignore @@ -95,6 +95,9 @@ venv.bak/ .spyderproject .spyproject +# PyCharm +.idea/ + # Rope project settings .ropeproject diff --git a/python/cli/CHANGELOG.md b/python/CHANGELOG.md similarity index 100% rename from python/cli/CHANGELOG.md rename to python/CHANGELOG.md diff --git a/python/cli/Dockerfile b/python/Dockerfile similarity index 100% rename from python/cli/Dockerfile rename to python/Dockerfile diff --git a/python/cli/LICENSE.md b/python/LICENSE.md similarity index 100% rename from python/cli/LICENSE.md rename to python/LICENSE.md diff --git a/python/cli/MANIFEST.in b/python/MANIFEST.in similarity index 100% rename from python/cli/MANIFEST.in rename to python/MANIFEST.in diff --git a/python/cli/Makefile b/python/Makefile similarity index 100% rename from python/cli/Makefile rename to python/Makefile diff --git a/python/cli/README.md b/python/README.md similarity index 100% rename from python/cli/README.md rename to python/README.md diff --git a/python/cli/kontor/controllers/clibase.py b/python/cli/kontor/controllers/clibase.py deleted file mode 100644 index 8d69fb2..0000000 --- a/python/cli/kontor/controllers/clibase.py +++ /dev/null @@ -1,60 +0,0 @@ - -from cement import Controller, ex -from cement.utils.version import get_version_banner -from ..core.version import get_version - -VERSION_BANNER = """ -Kontor CLI Tool %s -%s -""" % (get_version(), get_version_banner()) - - -class CliBase(Controller): - class Meta: - label = 'base' - - # text displayed at the top of --help output - description = 'Kontor CLI Tool' - - # text displayed at the bottom of --help output - epilog = 'Usage: kontor command1 --foo bar' - - # controller level arguments. ex: 'kontor --version' - arguments = [ - ### add a version banner - ( [ '-v', '--version' ], - { 'action' : 'version', - 'version' : VERSION_BANNER } ), - ] - - - def _default(self): - """Default action if no sub-command is passed.""" - - self.app.args.print_help() - - - @ex( - help='example sub command1', - - # sub-command level arguments. ex: 'kontor command1 --foo bar' - arguments=[ - ### add a sample foo option under subcommand namespace - ( [ '-f', '--foo' ], - { 'help' : 'notorious foo option', - 'action' : 'store', - 'dest' : 'foo' } ), - ], - ) - def command1(self): - """Example sub-command.""" - - data = { - 'foo' : 'bar', - } - - ### do something with arguments - if self.app.pargs.foo is not None: - data['foo'] = self.app.pargs.foo - - self.app.render(data, 'command1.jinja2') diff --git a/python/cli/kontor/database/base.py b/python/cli/kontor/database/base.py deleted file mode 100644 index f97d724..0000000 --- a/python/cli/kontor/database/base.py +++ /dev/null @@ -1,20 +0,0 @@ -from sqlalchemy import Column, String, DateTime, Integer -from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker, declarative_base - - -# class Base(DeclarativeBase): -# pass - -class BaseModel: - - @classmethod - def model_lookup_by_table_name(cls, table_name): - registry_instance = getattr(cls, "registry") - for mapper_ in registry_instance.mappers: - model = mapper_.class_ - model_class_name = model.__tablename__ - if model_class_name == table_name: - return model - - -Base = declarative_base(cls=BaseModel) \ No newline at end of file diff --git a/python/cli/config/kontor.yml.example b/python/config/kontor.yml.example similarity index 100% rename from python/cli/config/kontor.yml.example rename to python/config/kontor.yml.example diff --git a/python/cli/docs/.gitkeep b/python/docs/.gitkeep similarity index 100% rename from python/cli/docs/.gitkeep rename to python/docs/.gitkeep diff --git a/python/cli/kontor/__init__.py b/python/kontor/__init__.py similarity index 100% rename from python/cli/kontor/__init__.py rename to python/kontor/__init__.py diff --git a/python/cli/kontor/controllers/__init__.py b/python/kontor/controllers/__init__.py similarity index 100% rename from python/cli/kontor/controllers/__init__.py rename to python/kontor/controllers/__init__.py diff --git a/python/kontor/controllers/clibase.py b/python/kontor/controllers/clibase.py new file mode 100644 index 0000000..568a546 --- /dev/null +++ b/python/kontor/controllers/clibase.py @@ -0,0 +1,42 @@ +from PySide6.QtWidgets import QApplication +from cement import Controller, ex +from cement.utils.version import get_version_banner +from ..core.version import get_version +from ..gui.main_window import MainWindow + +VERSION_BANNER = """ +Kontor CLI Tool %s +%s +""" % (get_version(), get_version_banner()) + + +class CliBase(Controller): + class Meta: + label = 'base' + + # text displayed at the top of --help output + description = 'Kontor CLI Tool' + + # text displayed at the bottom of --help output + epilog = 'Usage: kontor gui|database' + + # controller level arguments. ex: 'kontor --version' + arguments = [ + ### add a version banner + (['-v', '--version'], + {'action': 'version', + 'version': VERSION_BANNER}), + ] + + def _default(self): + """Default action if no sub-command is passed.""" + self.gui() + + @ex( + help='start GUI' + ) + def gui(self): + application = QApplication([]) + window = MainWindow(self.app.session, self.app.log) + window.show() + application.exec() diff --git a/python/kontor/controllers/database.py b/python/kontor/controllers/database.py new file mode 100644 index 0000000..246d4f8 --- /dev/null +++ b/python/kontor/controllers/database.py @@ -0,0 +1,32 @@ +from cement import Controller, ex + +from ..database import KontorDB + + +class Database(Controller): + + class Meta: + label = 'database' + stacked_type = 'nested' + stacked_on = 'base' + + @ex( + help='export database to given file', + arguments=[ + (['-f', '--file'], + {'help': 'file to store database content', + 'action': 'store', + 'dest': 'db_file'}) + ], + ) + def export(self): + data = { + 'db_file': 'data.json', + 'export_type': 'JSON', + } + if self.app.pargs.db_file is not None: + data['db_file'] = self.app.pargs.db_file + kontor_db = KontorDB(self.app.session, self.app.log) + table_list = kontor_db.get_table_names() + kontor_db.export_db(data['export_type'], data['db_file'], table_list) + self.app.render(data, 'command1.jinja2') diff --git a/python/cli/kontor/core/__init__.py b/python/kontor/core/__init__.py similarity index 100% rename from python/cli/kontor/core/__init__.py rename to python/kontor/core/__init__.py diff --git a/python/cli/kontor/core/exc.py b/python/kontor/core/exc.py similarity index 100% rename from python/cli/kontor/core/exc.py rename to python/kontor/core/exc.py diff --git a/python/cli/kontor/core/version.py b/python/kontor/core/version.py similarity index 100% rename from python/cli/kontor/core/version.py rename to python/kontor/core/version.py diff --git a/python/cli/kontor/database/__init__.py b/python/kontor/database/__init__.py similarity index 55% rename from python/cli/kontor/database/__init__.py rename to python/kontor/database/__init__.py index 95ce1d8..c7e6cb5 100644 --- a/python/cli/kontor/database/__init__.py +++ b/python/kontor/database/__init__.py @@ -2,37 +2,40 @@ import json from datetime import datetime from pathlib import Path -import mariadb -from sqlalchemy import create_engine, select, text, MetaData, join -from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker - from .base import Base -from .comic import Comic +from .comic import Comic, Artist, Publisher, Issue, StoryArc, TradePaperback, Volume, ComicWork, WorkType from .metadata import MetaDataTable, MetaDataColumn +from .tysc import Card, CardSet, Sport, Team, FieldPosition, Rooster, Player, Vendor +from .media import MediaFile class KontorDB: - def __init__(self, db_config): - self.db_conn = mariadb.connect( - host=db_config['mariadb']['host'], - port=db_config['mariadb']['port'], - user=db_config['mariadb']['user'], - password=db_config['mariadb']['password'], - database=db_config['mariadb']['database'] - ) - connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}' - .format( - db_config['mariadb']['user'], - db_config['mariadb']['password'], - db_config['mariadb']['host'], - db_config['mariadb']['port'], - db_config['mariadb']['database'])) - # engine = create_engine(connect_string, echo=True) - engine = create_engine(connect_string) - Base.metadata.create_all(bind=engine) - __session__ = sessionmaker(bind=engine) - self.session = __session__() + def __init__(self, db_session, log): + self.session = db_session + self.log = log + self.registry = {} + self.init_registry() + + def init_registry(self): + self.registry['card'] = Card + self.registry['card_set'] = CardSet + self.registry['sport'] = Sport + self.registry['team'] = Team + self.registry['field_position'] = FieldPosition + self.registry['rooster'] = Rooster + self.registry['player'] = Player + self.registry['vendor'] = Vendor + self.registry['artist'] = Artist + self.registry['publisher'] = Publisher + self.registry['comic'] = Comic + self.registry['issue'] = Issue + self.registry['story_arc'] = StoryArc + self.registry['trade_paperback'] = TradePaperback + self.registry['volume'] = Volume + self.registry['comic_work'] = ComicWork + self.registry['worktype'] = WorkType + self.registry['media_file'] = MediaFile def get_table_names(self) -> list: tables = self.session.query(MetaDataTable).all() @@ -69,7 +72,7 @@ class KontorDB: filter(MetaDataTable.table_name == table_name). filter(MetaDataColumn.show_filter == 1).all()): _filter_map[column.column_name] = {'label': column.filter_label, 'widget': None} - print(f"retrieved {len(_filter_map)} filters: {_filter_map}") + self.log.info(f"retrieved {len(_filter_map)} filters: {_filter_map}") return _filter_map def data(self, table, columns: dict, filters) -> list: @@ -96,67 +99,27 @@ class KontorDB: data.append(row) return data - def get_data(self, table_name: str, columns: dict, where_clause: str) -> list: - data = [] - cursor = self.db_conn.cursor() - cursor.execute(self.get_statement(table_name, columns, where_clause)) - rows = cursor.fetchall() - print(len(rows)) - for row in rows: - # print(f"KontorDB.get_data: {row}") - data.append(list(row)) - cursor.close() - # print(f"KontorDB.getData: return {len(data)}") - if table_name == 'comic' and len(where_clause) == 0: - data.clear() - comics = self.session.query(Comic).all() - for item in comics: - # print(item) - row = [] - for order in columns.keys(): - column_name = columns[order]['column'] - if str(column_name).endswith("_id"): - ref_table = column_name[:-3] - # print(f"{ref_table=}") - ref = getattr(item, ref_table) - value = getattr(ref, "name") - # print(f"{value=}") - row.append(value) - else: - row.append(getattr(item, column_name)) - # print(repr(row)) - data.append(row) - return data - - def get_statement(self, table: str, header: dict, where_clause): - columns = "" - for index, column in header.items(): - if index > 0: - columns += ", " - columns += column['column'] - if len(columns) == 0: - columns = "*" - statement = f"SELECT {columns} FROM {table} {where_clause}" - print(f"{statement=}") - return statement - def export_db(self, export_type: str, export_file_name: str, export_table_list: list): - print(f"export DB to {export_file_name} as {export_type}") + self.log.info(f"export DB to {export_file_name} as {export_type}") db = {} for table in export_table_list: columns = self.get_column_meta_data(table, view_only=False) - model = Base.model_lookup_by_table_name(table) + if table in self.registry: + model = self.registry[table] + else: + print(f"table {table} is not registered") + continue rows = self.session.query(model).all() entries = [] - print(f"found {len(rows)} entries") - print(f"found {len(columns)} columns") + self.log.debug(f"found {len(rows)} entries") + self.log.debug(f"found {len(columns)} columns") for row in rows: - print(row) + # print(row) entry = {} for order in columns: - print(columns[order]) + # print(columns[order]) column_name = columns[order]['column'] - print(f"get value {column_name} from {row} of table {table}") + # print(f"get value {column_name} from {row} of table {table}") try: value = getattr(row, column_name) if isinstance(value, datetime): @@ -164,7 +127,7 @@ class KontorDB: else: entry[column_name] = value except AttributeError as error: - print("could not get value") + self.log.debug("could not get value") entries.append(entry) db[table] = entries export_file = Path(export_file_name) @@ -178,6 +141,6 @@ class KontorDB: case "SQLite": export_file = Path(export_file_name) case _: - print("unknown export type") + self.log.debug("unknown export type") if export_file.exists(): - print(f"{export_file} exists") + self.log.debug(f"{export_file} exists") diff --git a/python/kontor/database/base.py b/python/kontor/database/base.py new file mode 100644 index 0000000..fa2b68a --- /dev/null +++ b/python/kontor/database/base.py @@ -0,0 +1,5 @@ +from sqlalchemy.orm import DeclarativeBase + + +class Base(DeclarativeBase): + pass diff --git a/python/cli/kontor/database/comic.py b/python/kontor/database/comic.py similarity index 100% rename from python/cli/kontor/database/comic.py rename to python/kontor/database/comic.py diff --git a/python/cli/kontor/database/media.py b/python/kontor/database/media.py similarity index 100% rename from python/cli/kontor/database/media.py rename to python/kontor/database/media.py diff --git a/python/cli/kontor/database/metadata.py b/python/kontor/database/metadata.py similarity index 99% rename from python/cli/kontor/database/metadata.py rename to python/kontor/database/metadata.py index 2975e10..d7dd4c0 100644 --- a/python/cli/kontor/database/metadata.py +++ b/python/kontor/database/metadata.py @@ -20,6 +20,7 @@ class MetaDataTable(Base): def __str__(self): return f'{self.table_name}({self.id})' + class MetaDataColumn(Base): __tablename__ = 'meta_data_column' id = Column(String, primary_key=True) diff --git a/python/qt/database/tysc.py b/python/kontor/database/tysc.py similarity index 100% rename from python/qt/database/tysc.py rename to python/kontor/database/tysc.py diff --git a/python/cli/kontor/ext/__init__.py b/python/kontor/ext/__init__.py similarity index 100% rename from python/cli/kontor/ext/__init__.py rename to python/kontor/ext/__init__.py diff --git a/python/cli/kontor/plugins/__init__.py b/python/kontor/gui/__init__.py similarity index 100% rename from python/cli/kontor/plugins/__init__.py rename to python/kontor/gui/__init__.py diff --git a/python/qt/gui/data_view.py b/python/kontor/gui/data_view.py similarity index 100% rename from python/qt/gui/data_view.py rename to python/kontor/gui/data_view.py diff --git a/python/qt/gui/data_view_model.py b/python/kontor/gui/data_view_model.py similarity index 100% rename from python/qt/gui/data_view_model.py rename to python/kontor/gui/data_view_model.py diff --git a/python/qt/gui/dialogs.py b/python/kontor/gui/dialogs.py similarity index 100% rename from python/qt/gui/dialogs.py rename to python/kontor/gui/dialogs.py diff --git a/python/kontor/gui/main_window.py b/python/kontor/gui/main_window.py new file mode 100644 index 0000000..24a84a6 --- /dev/null +++ b/python/kontor/gui/main_window.py @@ -0,0 +1,139 @@ +from PySide6.QtGui import QAction, QIcon +from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView +from PySide6.QtWidgets import QLabel, QMainWindow + +from ..database import KontorDB +from ..database.media import MediaFile +from ..database.comic import Comic +from .dialogs import ExportKontorDialog, ImportKontorDialog +from .model_config import KontorModelConfig +from .table_model import KontorTableModel + + +class MainWindow(QMainWindow): + + def __init__(self, session, log): + super().__init__() + + self.tick = QIcon('kontor/res/tick.png') + self.cross = QIcon('kontor/res/cross.png') + self.import_icon = QIcon("kontor/res/application-import.png") + self.export_icon = QIcon("kontor/res/application-export.png") + self.circle_icon = QIcon("kontor/res/arrow-circle-double.png") + + self.setWindowTitle("Kontor") + self.setMinimumSize(800, 500) + self._create_actions() + self._create_menubar() + self._create_toolbars() + self._create_statusbar() + + self.data = [] + self.filter = {} + self.kontor_db = KontorDB(session, log) + self.log = log + self.central_widget = QWidget() + parent_layout = QVBoxLayout() + self.central_widget.setLayout(parent_layout) + self.tabs = QTabWidget() + self.tabs.addTab(self.generate_data_tab("comic", Comic), "Comics") + self.tabs.addTab(self.generate_data_tab("media_file", MediaFile), "MediaFile") + self.tabs.currentChanged.connect(self._tab_changed) + #label.setAlignment(Qt.AlignmentFlag.AlignCenter) + parent_layout.addWidget(self.tabs) + + self.setCentralWidget(self.central_widget) + + def _create_actions(self): + self.newAction = QAction("&New", self) + self.aboutAction = QAction("&Über...", self) + self.aboutAction.triggered.connect(self.about) + self.importAction = QAction(self.import_icon, "&Import", self) + self.importAction.triggered.connect(self.import_from_file) + self.exportAction = QAction(self.export_icon, "&Export", self) + self.exportAction.triggered.connect(self.export_to_file) + self.refreshAction = QAction(self.circle_icon, "&Refresh", self) + self.refreshAction.triggered.connect(self.refresh) + self.updateTitleAction = QAction("&Update Titles", self) + self.downloadAction = QAction("&Download Videos", self) + self.exitAction = QAction("&Beenden", self) + self.exitAction.setShortcut("Alt+F4") + self.exitAction.triggered.connect(self.close) + + def _create_menubar(self): + menu_bar = self.menuBar() + # File menu + file_menu = QMenu("&Datei") + menu_bar.addMenu(file_menu) + file_menu.addAction(self.exitAction) + # Kontor menu + kontor_menu = QMenu("&Kontor") + menu_bar.addMenu(kontor_menu) + kontor_menu.addAction(self.importAction) + kontor_menu.addAction(self.exportAction) + comic_menu = QMenu("&Comic") + tysc_menu = QMenu("&TradeYourSportCards") + media_file_menu = QMenu("&MediaFile") + media_file_menu.addAction(self.updateTitleAction) + media_file_menu.addAction(self.downloadAction) + kontor_menu.addMenu(comic_menu) + kontor_menu.addMenu(tysc_menu) + kontor_menu.addMenu(media_file_menu) + # Help menu + help_menu = QMenu("&Hilfe") + menu_bar.addMenu(help_menu) + help_menu.addAction(self.aboutAction) + + def _create_toolbars(self): + # Kontor toolbar + kontor_tool_bar = self.addToolBar("Kontor") + kontor_tool_bar.addAction(self.importAction) + kontor_tool_bar.addAction(self.exportAction) + kontor_tool_bar.addAction(self.refreshAction) + + def _create_statusbar(self): + self.statusBar = self.statusBar() + self.statusBar.showMessage("Kontor ready", 6000) + self.status_label = QLabel("") + self.statusBar.addPermanentWidget(self.status_label) + + def about(self): + QMessageBox.about(self.central_widget, "Über Kontor", f"Python: 3.11\nKontor: 0.1.0") + + def import_from_file(self): + import_dlg = ImportKontorDialog(self) + if import_dlg.exec(): + print(f"import DB from file {import_dlg.file_name}") + else: + print("no nothing for import") + pass + + def export_to_file(self): + export_dlg = ExportKontorDialog(self, self.kontor_db) + if export_dlg.exec(): + self.log.info(export_dlg.get_tables_to_export()) + self.log.info(f"export DB to {export_dlg.file_name}") + self.statusBar.showMessage(f"export DB to {export_dlg.file_name}", 3000) + self.kontor_db.export_db(export_dlg.current_export_type, export_dlg.file_name, export_dlg.get_tables_to_export()) + else: + self.statusBar.showMessage("Export cancelled", 3000) + + def refresh(self): + self.data[self.tabs.currentIndex()].refresh() + + def _tab_changed(self, tab_index): + self.data[tab_index].refresh() + + def generate_data_tab(self, table_name, table): + data_tab = QWidget() + table_config = KontorModelConfig(self.kontor_db, self, table_name, table) + model = KontorTableModel(table_config) + layout = QVBoxLayout() + self.data.append(model) + data_tab.setLayout(layout) + table_view = QTableView() + table_view.setModel(model) + layout.addLayout(table_config.get_filter_layout()) + layout.addWidget(table_view) + model.refresh() + return data_tab diff --git a/python/kontor/gui/model_config.py b/python/kontor/gui/model_config.py new file mode 100644 index 0000000..ce61107 --- /dev/null +++ b/python/kontor/gui/model_config.py @@ -0,0 +1,50 @@ +import mariadb +from PySide6.QtWidgets import QHBoxLayout, QCheckBox + +from ..database import KontorDB + + +class KontorModelConfig: + + def __init__(self, kontor_db: KontorDB, main_window, table_name: str, table): + self.header = {} + self.filter = {} + self.main_window = main_window + self._table_name = table_name + self._table = table + self.kontor_db = kontor_db + self.get_table_config() + + def get_table_config(self): + self.header = self.kontor_db.get_column_meta_data(self._table_name) + self.filter = self.kontor_db.get_filters(self._table_name) + + def filters(self) -> dict: + _filters = {} + # print(self.filter["download"].isChecked()) + for column, filter_info in self.filter.items(): + # print(column, filter_info) + if filter_info['widget'].isChecked(): + _filters[column] = True + # print(f"{filter_rule=}") + return _filters + + def get_data(self) -> list: + # data = self.kontor_db.get_data(self._table_name, self.header, self.get_filter()) + # data.clear() + data = self.kontor_db.data(self._table, self.header, self.filters()) + # print(f"KontorModelConfig.get_data: {len(data)}") + # comics = self.kontor_db.session.query(Comic).all() + # print(f'{len(comics)} Comics loaded') + return data + + def get_filter_layout(self) -> QHBoxLayout: + filter_layout = QHBoxLayout() + for column, filter_info in self.filter.items(): + filter_checkbox = QCheckBox() + filter_checkbox.setText(filter_info['label']) + filter_checkbox.checkStateChanged.connect(self.main_window.refresh) + self.filter[column]['widget'] = filter_checkbox + filter_layout.addWidget(filter_checkbox) + filter_layout.addStretch() + return filter_layout diff --git a/python/kontor/gui/table_model.py b/python/kontor/gui/table_model.py new file mode 100644 index 0000000..cd08ce1 --- /dev/null +++ b/python/kontor/gui/table_model.py @@ -0,0 +1,108 @@ +from datetime import datetime + +from PySide6.QtCore import QAbstractTableModel, QModelIndex +from PySide6.QtGui import Qt + +from .model_config import KontorModelConfig + + +class KontorTableModel(QAbstractTableModel): + + def __init__(self, model_config: KontorModelConfig): + super().__init__() + self._main_window = model_config.main_window + self._config = model_config + self._data = [] + + def refresh(self): + data = self._config.get_data() + count = 0 + # print(data) + if data is not None: + self.beginResetModel() + self._data.clear() + self._data = data + self.endResetModel() + count = len(data) + # print(data) + # print(self._data) + self.layoutChanged.emit() + self._main_window.statusBar.showMessage(f"{count} Einträge geladen", 3000) + + def rowCount(self, parent=QModelIndex()): + # The length of the outer list. + if self._data is None: + return 0 + return len(self._data) + + def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole): + if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: + return self._config.header[col]['label'] + if orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole: + return str(col+1) + + def data(self, index, role=Qt.ItemDataRole.DisplayRole): + if self._data is None: + return None + value = self._data[index.row()][index.column()] + # print('{}:: {}:: {}: {}'.format(index, role, value, type(value))) + if role == Qt.ItemDataRole.DisplayRole or role == Qt.ItemDataRole.EditRole: + if isinstance(value, datetime): + return value.strftime("%Y-%m-%d %M:%M:%S") + if isinstance(value, str): + return value + if isinstance(value, bytes): + if value == b'\x01': + return self._main_window.tick + else: + return self._main_window.cross + if isinstance(value, int): + # print('{}:: {}: {}'.format(index, value, type(value))) + if value == 1: + return self._main_window.tick + else: + return self._main_window.cross + if isinstance(value, bool): + if value: + return self._main_window.tick + else: + return self._main_window.cross + return str(value) + if role == Qt.ItemDataRole.DecorationRole: + if isinstance(value, bytes): + if value == b'\x01': + return self._main_window.tick + else: + return self._main_window.cross + if isinstance(value, int): + if value == 1: + return self._main_window.tick + else: + return self._main_window.cross + if isinstance(value, bool): + if value: + return self._main_window.tick + else: + return self._main_window.cross + + def columnCount(self, index=QModelIndex()): + # The following takes the first sub-list, and returns + # the length (only works if all rows are an equal length) + # print(f"Header count: {len(self._config.get_header())}") + return len(self._config.header) + + def setData(self, index, value, role: int) -> bool: + print(index, role) + if role == Qt.ItemDataRole.EditRole: + self._data[index.row()][index.column()] = value + print(self._data[index.row()][index.column()]) + self.dataChanged.emit(index, index) + return True + if role == Qt.ItemDataRole.CheckStateRole: + print("role == Qt.ItemDataRole.CheckStateRole") + checked = value == Qt.CheckState.Checked + self._data[index.row()][index.column()] = checked + return False + + def flags(self, index): + return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsUserTristate diff --git a/python/cli/kontor/main.py b/python/kontor/main.py similarity index 91% rename from python/cli/kontor/main.py rename to python/kontor/main.py index 4c71aab..aa71e06 100644 --- a/python/cli/kontor/main.py +++ b/python/kontor/main.py @@ -1,4 +1,3 @@ - from cement import App, TestApp, init_defaults from cement.core.exc import CaughtSignal from sqlalchemy import create_engine @@ -7,6 +6,7 @@ from sqlalchemy.orm import sessionmaker from .core.exc import KontorError from .database.base import Base from .controllers.clibase import CliBase +from .controllers.database import Database # configuration defaults CONFIG = init_defaults('kontor', 'mariadb') @@ -17,6 +17,7 @@ CONFIG['mariadb']['host'] = '127.0.0.1' CONFIG['mariadb']['port'] = '3306' CONFIG['mariadb']['database'] = 'kontor' + def extend_sqlalchemy(app): app.log.info('extending kontor application with sqlalchemy') connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}'.format( @@ -33,6 +34,11 @@ def extend_sqlalchemy(app): app.extend('session', __session__()) +def close_session(app): + app.log.info('close session') + app.session.close() + + class Kontor(App): """Kontor primary application.""" @@ -66,14 +72,16 @@ class Kontor(App): hooks = [ ('post_setup', extend_sqlalchemy), + ('pre_close', close_session), ] # register handlers handlers = [ - CliBase + CliBase, + Database, ] -class KontorTest(TestApp,Kontor): +class KontorTest(TestApp, Kontor): """A sub-class of Kontor that is better suited for testing.""" class Meta: diff --git a/python/cli/kontor/templates/__init__.py b/python/kontor/plugins/__init__.py similarity index 100% rename from python/cli/kontor/templates/__init__.py rename to python/kontor/plugins/__init__.py diff --git a/python/qt/res/application-export.png b/python/kontor/res/application-export.png similarity index 100% rename from python/qt/res/application-export.png rename to python/kontor/res/application-export.png diff --git a/python/qt/res/application-import.png b/python/kontor/res/application-import.png similarity index 100% rename from python/qt/res/application-import.png rename to python/kontor/res/application-import.png diff --git a/python/qt/res/arrow-circle-double.png b/python/kontor/res/arrow-circle-double.png similarity index 100% rename from python/qt/res/arrow-circle-double.png rename to python/kontor/res/arrow-circle-double.png diff --git a/python/qt/res/cross.png b/python/kontor/res/cross.png similarity index 100% rename from python/qt/res/cross.png rename to python/kontor/res/cross.png diff --git a/python/qt/res/tick.png b/python/kontor/res/tick.png similarity index 100% rename from python/qt/res/tick.png rename to python/kontor/res/tick.png diff --git a/python/qt/gui/__init__.py b/python/kontor/templates/__init__.py similarity index 100% rename from python/qt/gui/__init__.py rename to python/kontor/templates/__init__.py diff --git a/python/cli/kontor/templates/command1.jinja2 b/python/kontor/templates/command1.jinja2 similarity index 100% rename from python/cli/kontor/templates/command1.jinja2 rename to python/kontor/templates/command1.jinja2 diff --git a/python/main.py b/python/main.py deleted file mode 100644 index 76d0e8c..0000000 --- a/python/main.py +++ /dev/null @@ -1,16 +0,0 @@ -# This is a sample Python script. - -# Press Umschalt+F10 to execute it or replace it with your code. -# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings. - - -def print_hi(name): - # Use a breakpoint in the code line below to debug your script. - print(f'Hi, {name}') # Press Strg+F8 to toggle the breakpoint. - - -# Press the green button in the gutter to run the script. -if __name__ == '__main__': - print_hi('PyCharm') - -# See PyCharm help at https://www.jetbrains.com/help/pycharm/ diff --git a/python/cli/requirements-dev.txt b/python/requirements-dev.txt similarity index 100% rename from python/cli/requirements-dev.txt rename to python/requirements-dev.txt diff --git a/python/cli/requirements.txt b/python/requirements.txt similarity index 90% rename from python/cli/requirements.txt rename to python/requirements.txt index bee2df1..06a15f9 100644 --- a/python/cli/requirements.txt +++ b/python/requirements.txt @@ -4,3 +4,4 @@ cement[yaml] cement[colorlog] mariadb sqlalchemy +PySide6 diff --git a/python/cli/setup.cfg b/python/setup.cfg similarity index 100% rename from python/cli/setup.cfg rename to python/setup.cfg diff --git a/python/cli/setup.py b/python/setup.py similarity index 100% rename from python/cli/setup.py rename to python/setup.py diff --git a/python/cli/tests/conftest.py b/python/tests/conftest.py similarity index 100% rename from python/cli/tests/conftest.py rename to python/tests/conftest.py diff --git a/python/cli/tests/test_kontor.py b/python/tests/test_kontor.py similarity index 100% rename from python/cli/tests/test_kontor.py rename to python/tests/test_kontor.py diff --git a/python/qt/.gitignore b/qt/.gitignore similarity index 100% rename from python/qt/.gitignore rename to qt/.gitignore diff --git a/python/qt/database/__init__.py b/qt/database/__init__.py similarity index 100% rename from python/qt/database/__init__.py rename to qt/database/__init__.py diff --git a/python/qt/database/base.py b/qt/database/base.py similarity index 100% rename from python/qt/database/base.py rename to qt/database/base.py diff --git a/python/qt/database/comic.py b/qt/database/comic.py similarity index 100% rename from python/qt/database/comic.py rename to qt/database/comic.py diff --git a/python/qt/database/media.py b/qt/database/media.py similarity index 100% rename from python/qt/database/media.py rename to qt/database/media.py diff --git a/python/qt/database/metadata.py b/qt/database/metadata.py similarity index 100% rename from python/qt/database/metadata.py rename to qt/database/metadata.py diff --git a/python/cli/kontor/database/tysc.py b/qt/database/tysc.py similarity index 94% rename from python/cli/kontor/database/tysc.py rename to qt/database/tysc.py index 54cacea..733d7ed 100644 --- a/python/cli/kontor/database/tysc.py +++ b/qt/database/tysc.py @@ -28,7 +28,7 @@ class Team(Base): name = Column(String(255), nullable=False, index=True, unique=True) short_name = Column(String(255), nullable=False, ) sport_id = Column(String, ForeignKey("sport.id"), nullable=False) - sport = relationship("Sport", back_populates="positions") + sport = relationship("Sport", back_populates="teams") roosters = relationship("Rooster") @@ -81,7 +81,7 @@ class Rooster(Base): player_id = Column(String, ForeignKey("player.id"), nullable=False, index=True) player = relationship("Player", back_populates="roosters") position_id = Column(String, ForeignKey("field_position.id"), nullable=False, index=True) - position = relationship("roosters") + position = relationship("FieldPosition", back_populates="roosters") cards = relationship("Card") class Vendor(Base): @@ -124,8 +124,8 @@ class Card(Base): card_number = Column(Integer, index=True) year = Column(Integer, index=True) card_set_id = Column(String, ForeignKey("card_set.id"), nullable=False) - card_set = relationship("cards") + card_set = relationship("CardSet", back_populates="cards") rooster_id = Column(String, ForeignKey("rooster.id"), nullable=False) - rooster = relationship("cards") + rooster = relationship("Rooster", back_populates="cards") vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False) vendor = relationship("Vendor", back_populates="cards") diff --git a/qt/gui/__init__.py b/qt/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/qt/gui/data_view.py b/qt/gui/data_view.py new file mode 100644 index 0000000..91c66d2 --- /dev/null +++ b/qt/gui/data_view.py @@ -0,0 +1,12 @@ +from abc import ABC, abstractmethod + + +class DataViewMeta(ABC): + @abstractmethod + def get_header(self): + pass + + +class ComicView(DataViewMeta): + def get_header(self): + pass diff --git a/qt/gui/data_view_model.py b/qt/gui/data_view_model.py new file mode 100644 index 0000000..4ffdc8c --- /dev/null +++ b/qt/gui/data_view_model.py @@ -0,0 +1,32 @@ +from typing import List + +from PySide6.QtCore import QModelIndex, QAbstractTableModel +from PySide6.QtGui import Qt + +from gui.data_view import DataViewMeta + + +class DataViewModel(QAbstractTableModel): + def __init__(self): + super().__init__() + self.main_window = None + self._config = None + self._data = List[DataViewMeta] + + def rowCount(self, parent=QModelIndex()): + return len(self._data) + + def columnCount(self, parent=QModelIndex()): + return 0 + + def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole): + return None + + def data(self, index, role=Qt.ItemDataRole.DisplayRole): + return None + + def setData(self, index, value, role=Qt.ItemDataRole.EditRole): + return False + + def flags(self, index): + return None diff --git a/qt/gui/dialogs.py b/qt/gui/dialogs.py new file mode 100644 index 0000000..21dbe92 --- /dev/null +++ b/qt/gui/dialogs.py @@ -0,0 +1,106 @@ +from pathlib import Path + +from PySide6.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QFileDialog, \ + QCheckBox, QComboBox + + +class ExportKontorDialog(QDialog): + def __init__(self, parent=None, kontor_db=None): + super().__init__(parent) + + self.parent = parent + self.kontor_db = kontor_db + self.file_name = "data.json" + self.tables = [] + self._table_options = {} + + self.export_options = {"JSON": {"ext": ".json"}, "YAML": {"ext": ".yaml"}, "SQLite": {"ext": ".db"}} + self.current_export_type = "JSON" + + buttons = (QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + + self.buttonBox = QDialogButtonBox(buttons) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + + layout = QVBoxLayout() + + self.label = QLabel() + self.label.setText("Export DB to data.json") + + self.combo_box = QComboBox() + self.combo_box.addItems(["JSON", "YAML", "SQLite"]) + self.combo_box.currentTextChanged.connect(self.change_export_type) + file_layout = QHBoxLayout() + file_layout.addWidget(self.label) + file_layout.addWidget(self.combo_box) + file_button = QPushButton("Select file") + file_button.clicked.connect(self.select_file) + file_layout.addWidget(file_button) + layout.addLayout(file_layout) + + for table_name in self.kontor_db.get_table_names(): + check_box = QCheckBox(table_name) + check_box.setChecked(True) + self.tables.append(table_name) + self._table_options[table_name] = check_box + check_box.stateChanged.connect(self.change_selection) + layout.addWidget(check_box) + layout.addWidget(self.buttonBox) + self.setLayout(layout) + + def change_selection(self): + self.tables.clear() + for (name, box) in self._table_options.items(): + if box.isChecked(): + self.tables.append(name) + + def change_export_type(self, text): + self.current_export_type = text + self.label.setText(f'Export DB to data.{self.export_options[text]["ext"]}') + + def select_file(self): + file_dialog = QFileDialog() + file_dialog.setFileMode(QFileDialog.FileMode.AnyFile) + file_dialog.setDefaultSuffix(self.export_options[self.current_export_type]["ext"]) + file_dialog.setNameFilter(f'*{self.export_options[self.current_export_type]["ext"]}') + if file_dialog.exec(): + self.file_name = file_dialog.selectedFiles()[0] + export_file = Path(self.file_name) + self.file_name = export_file.with_suffix(self.export_options[self.current_export_type]["ext"]) + self.label.setText(f"Export DB to {self.file_name}") + + def get_tables_to_export(self) -> list: + return self.tables + + +class ImportKontorDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + + self.file_name = None + + QBtn = (QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + + self.buttonBox = QDialogButtonBox(QBtn) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + + self.label = QLabel() + self.label.setText("Import DB from data.json") + layout = QVBoxLayout() + file_layout = QHBoxLayout() + file_layout.addWidget(self.label) + file_button = QPushButton("Select file") + file_button.clicked.connect(self.select_file) + file_layout.addWidget(file_button) + layout.addLayout(file_layout) + layout.addWidget(self.buttonBox) + self.setLayout(layout) + + def select_file(self): + file_dialog = QFileDialog() + file_dialog.setFileMode(QFileDialog.FileMode.ExistingFile) + if file_dialog.exec(): + self.file_name = file_dialog.selectedFiles()[0] + self.label.setText(f"Import DB from {self.file_name}") diff --git a/python/qt/gui/main_window.py b/qt/gui/main_window.py similarity index 100% rename from python/qt/gui/main_window.py rename to qt/gui/main_window.py diff --git a/python/qt/gui/model_config.py b/qt/gui/model_config.py similarity index 100% rename from python/qt/gui/model_config.py rename to qt/gui/model_config.py diff --git a/python/qt/gui/table_model.py b/qt/gui/table_model.py similarity index 100% rename from python/qt/gui/table_model.py rename to qt/gui/table_model.py diff --git a/python/qt/kontor.py b/qt/kontor.py similarity index 100% rename from python/qt/kontor.py rename to qt/kontor.py diff --git a/python/qt/pysidedeploy.spec b/qt/pysidedeploy.spec old mode 100755 new mode 100644 similarity index 100% rename from python/qt/pysidedeploy.spec rename to qt/pysidedeploy.spec diff --git a/qt/res/application-export.png b/qt/res/application-export.png new file mode 100644 index 0000000000000000000000000000000000000000..555887a28d64bc812c4dfa98a6ff1da1927b7792 GIT binary patch literal 513 zcmV+c0{;DpP)A0|fCeqz@KJVX8nnt2D9k2UrOajFq_SwR4OUbbUJ;HgeDQF66oE777mBiWdd2F(NKOO zk$Ax17hTui{#6jPXfz7dY87I!*bgRr4TVA=afmd=7~nkYBq$b(su#^>Q?2y;%rJ~= zA;j4sgM^B|5YOb(M4cc`5q!^h_wPg5is0Dq{42l!6{r`}{QE*r00000NkvXXu0mjf Djqud07&@TR)18#v5Hqj8@|Bnpn>z;V`CueWd< z7oI05i2^A#D9Qz9v#hs`;>*D7{eB7>0ppH2SLstJO-8BwR~M`A7kF ztyTlI66tgrtyT-MSPa~yU>3AuKR54&OXJih@;oco-=1sDLQa}`@LC7Iy> O0000dz zNLw#yfl;S$OS+NxP!T0`q27a_KcR=-dJ5{Xm+WEf!J>k|BqE5#C}ImGwQ$$<(wdh& z>AIJ5zVFx9lo8J{@aWY%oIP6CAZKYHol)uRf|554m_YTnpAlGl zL9{5M4)qVt#u>|mLQVrh46q|<)MP|~t8EE-8crSv>~io{4+abW2A~lV0z>OM)mbBD zmWtqV;+?9YGlhpQQ@M9zi{*KRG_T5PQ_nvC9tMo_mT6XEIU5;J+S?d(HD_0f!1ES7 z7@Uj?4!kD>&?r8e&c1jan^9Cs!}3z0d7Izafj)-}rbYZR}EdihS6`M#Fcy= zaw{u#mV;BNWe3{Sqiq;{_M>cOP*8MhP1CLQf3t|C2tpM55ycYh7RL%}Yf+Bp#e!~C z2(WG^ZnlxC>igv!sW2GM8SslVJ(oFwkp~5*6E^0rT$Q zya}^2lB>ITrX0zGEa!lc90$HC4~#^g#%ZB2j3vzHhi;TO$n{?Wwx(^0>^QA_x~H^z zTf=2SxY0rCf|fl@rb7TXYr%pE(0=u@G literal 0 HcmV?d00001 diff --git a/qt/res/cross.png b/qt/res/cross.png new file mode 100644 index 0000000000000000000000000000000000000000..6b9fa6dd36ee8165272a13dd263f573507c78ca6 GIT binary patch literal 544 zcmV+*0^j|KP)L-ku(! z6_?D-?!0+#=VtbVZQGdTnZt~apI_HPK#=zVadHM((E`e*C+RoFb)Qo8-U{Lb7{`Tz zw4B8FZ|o?Wox+p=1wp47C;7ar*Xu~;a?<=sjPv>+otDjJ6FaGt!YnNyxQQhp)G1V! zk;r6ZtyV)c8pTbiRAnHRNXTxti%=+p$4aG2*+mMM&xrdiU^`VPk^N*+HX98^7!HT% zwA%;A{T)GxeQ|RcxyzV%! z$AbYD+)i1R64zB?q}NmTz}5|04~J#YG_goA*Eq(QJvp7pG14hUj1pZ^t>3S*xqHUO ze~r;}zTY_XkZ*}d@gm!;N90h8nBQenBUZ_ukZKNicn*hc_Pmc!Jn|2=s<~}>!Sb>Qm3!QP46b_JFw5YU70Tu$X}=T}ez@@t%j iG9vD)nDux55?}x$+|UyQVK_bj0000tYd4K$mX5uyr2F@fdffp{&DSHtl4|Bn9=ukpCx`#%PTUr-D(@b7;C zhJXJjrnw{;1KBM=6&|E`fsNrGL!Y6%zUh}QUl`(@V)PmQFtotEKmafTHP0uP}7&%m4pcKQ#X)BiAJ2y+PrtBI>9eEIt2-_c7)?*Lsf z5vX5*Af@m-wBJRV%#Fiz=BdPr0!2^a2d-yv7gSU);Z6{`~Oy?E5qSnHUln4*Y%!){F!Y1~5WXoC44g zb7l_)X~q^V6MmWR7e3wj|L|Wb!|^}Y86N$^h+kv_Kw-fP!~#If$6(B4$)LlK#&G6; zC&ShMSAb$5tA9Xg5dOsg@-z^@3;;QS9f&!gG&3|;{Da~@Q2ZB({s%XJ5&#fj0J|In U+D>(nEdT%j07*qoM6N<$g0@&8X#fBK literal 0 HcmV?d00001