diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..45f246d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__ +.idea/ +bonus +icons +icons-shadowless diff --git a/gui/application-export.png b/gui/application-export.png new file mode 100644 index 0000000..555887a Binary files /dev/null and b/gui/application-export.png differ diff --git a/gui/application-import.png b/gui/application-import.png new file mode 100644 index 0000000..922cb07 Binary files /dev/null and b/gui/application-import.png differ diff --git a/gui/arrow-circle-double.png b/gui/arrow-circle-double.png new file mode 100644 index 0000000..ba5ebd1 Binary files /dev/null and b/gui/arrow-circle-double.png differ diff --git a/gui/comic_model.py b/gui/comic_model.py new file mode 100644 index 0000000..2472670 --- /dev/null +++ b/gui/comic_model.py @@ -0,0 +1,89 @@ +from datetime import datetime + +import mariadb +from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt +from PySide6.QtGui import QColor + + +class ComicTableModel(QAbstractTableModel): + + def __init__(self, db_config, main_window): + super().__init__() + self.main_window = main_window + self._data = [] + self.status_bar = main_window.statusBar + self.mariadb_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'] + ) + self.refresh() + + def refresh(self): + data = [] + cursor = self.mariadb_conn.cursor() + cursor.execute("SELECT id, created_date, last_modified_date, title, publisher_id FROM comic") + rows = cursor.fetchall() + for row in rows: + data.append(list(row)) + self.status_bar.showMessage(f"{len(rows)} Einträge geladen", 3000) + self._data = data + + def rowCount(self, parent=QModelIndex()): + # The length of the outer list. + return len(self._data) + + def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole): + if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: + match col: + case 0: + return "ID" + case 1: + return "Created" + case 2: + return "Updated" + case 3: + return "Title" + case 4: + return "Verlag" + if orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole: + return str(col + 1) + + def data(self, index, role=Qt.ItemDataRole.DisplayRole): + value = self._data[index.row()][index.column()] + if role == Qt.ItemDataRole.DisplayRole: + 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 "True" + return "False" + return value + if role == Qt.ItemDataRole.DecorationRole: + if isinstance(value, bytes): + # print('{}: {}'.format(value, type(value))) + if value == b'\x01': + 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) + return len(self._data[0]) + + def setData(self, index, value, role=Qt.ItemDataRole.EditRole): + if role == Qt.ItemDataRole.EditRole: + self._data[index.row()][index.column()] = value + if role == Qt.ItemDataRole.CheckStateRole: + checked = value == Qt.CheckState.Checked + self._data[index.row()][index.column()] = checked + return True + + def flags(self, index): + return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsUserTristate diff --git a/gui/cross.png b/gui/cross.png new file mode 100644 index 0000000..6b9fa6d Binary files /dev/null and b/gui/cross.png differ diff --git a/gui/kontor.py b/gui/kontor.py new file mode 100644 index 0000000..a435fe6 --- /dev/null +++ b/gui/kontor.py @@ -0,0 +1,131 @@ +""" +PyQT6 GUI for Kontor +""" +import sys +from pathlib import Path + +import yaml +from PySide6.QtGui import QAction, QIcon +from PySide6.QtSql import QSqlDatabase +from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView +from PySide6.QtWidgets import QApplication, QLabel, QMainWindow +from platformdirs import PlatformDirs + +from comic_model import ComicTableModel +from media_file_model import MediaFileTableModel + + +class MainWindow(QMainWindow): + + + def __init__(self, config): + super().__init__() + + self.tick = QIcon('tick.png') + self.cross = QIcon('cross.png') + self.import_icon = QIcon("application-import.png") + self.export_icon = QIcon("application-export.png") + self.circle_icon = QIcon("arrow-circle-double.png") + + self.setWindowTitle("Kontor") + self.setMinimumSize(800, 500) + self._createActions() + self._createMenuBar() + self._createToolBars() + self._createStatusBar() + + self.data = [] + self.central_widget = QWidget() + parent_layout = QVBoxLayout() + self.central_widget.setLayout(parent_layout) + self.tabs = QTabWidget() + self.tabs.addTab(self.generate_tab_comics(config), "Comics") + self.tabs.addTab(self.generate_tab_media_file(config), "MediaFile") + self.tabs.currentChanged.connect(self._tab_changed) + #label.setAlignment(Qt.AlignmentFlag.AlignCenter) + parent_layout.addWidget(self.tabs) + + self.setCentralWidget(self.central_widget) + + def _createActions(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.exportAction = QAction(self.export_icon, "&Export", self) + self.refreshAction = QAction(self.circle_icon, "&Refresh", self) + self.refreshAction.triggered.connect(self.refresh) + self.exitAction = QAction("&Beenden", self) + self.exitAction.setShortcut("Alt+F4") + self.exitAction.triggered.connect(self.close) + + def _createMenuBar(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) + # Help menu + help_menu = QMenu("&Hilfe") + menu_bar.addMenu(help_menu) + help_menu.addAction(self.aboutAction) + + def _createToolBars(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 _createStatusBar(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 refresh(self): + self.data[self.tabs.currentIndex()].refresh() + + def _tab_changed(self, tab_index): + self.data[tab_index].refresh() + + def generate_tab_comics(self, db_configuration): + comic_tab = QWidget() + layout = QVBoxLayout() + comic_tab.setLayout(layout) + model = ComicTableModel(db_configuration, self) + self.data.append(model) + table_view = QTableView() + table_view.setModel(model) + layout.addWidget(table_view) + return comic_tab + + def generate_tab_media_file(self, db_configuration): + media_file_tab = QWidget() + layout = QVBoxLayout() + model = MediaFileTableModel(db_configuration, self) + self.data.append(model) + media_file_tab.setLayout(layout) + table_view = QTableView() + table_view.setModel(model) + layout.addWidget(table_view) + return media_file_tab + + +if __name__ == '__main__': + app = QApplication(sys.argv) + dirs = PlatformDirs("kontor") + database_config = Path(dirs.user_config_dir, 'database-config.yaml') + with open(database_config, 'rt') as f: + db_config = yaml.safe_load(f.read()) + window = MainWindow(db_config) + window.show() + app.exec() diff --git a/gui/media_file_model.py b/gui/media_file_model.py new file mode 100644 index 0000000..619ce2e --- /dev/null +++ b/gui/media_file_model.py @@ -0,0 +1,91 @@ +from datetime import datetime + +import mariadb +from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt +from PySide6.QtGui import QColor + + +class MediaFileTableModel(QAbstractTableModel): + + def __init__(self, db_config, main_window): + super().__init__() + self.main_window = main_window + self._data = [] + self.status_bar = main_window.statusBar + self.mariadb_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'] + ) + self.refresh() + + def refresh(self): + data = [] + cursor = self.mariadb_conn.cursor() + cursor.execute("SELECT id, url, review, should_download, file_name, cloud_link FROM media_file") + rows = cursor.fetchall() + for row in rows: + data.append(list(row)) + self.status_bar.showMessage(f"{len(rows)} Einträge geladen", 3000) + self._data = data + + def rowCount(self, parent=QModelIndex()): + # The length of the outer list. + return len(self._data) + + def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole): + if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: + match col: + case 0: + return "ID" + case 1: + return "URL" + case 2: + return "Review" + case 3: + return "Download" + case 4: + return "Filename" + case 5: + return "Cloud Link" + if orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole: + return str(col+1) + + def data(self, index, role=Qt.ItemDataRole.DisplayRole): + value = self._data[index.row()][index.column()] + if role == Qt.ItemDataRole.DisplayRole: + 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 + return value + if role == Qt.ItemDataRole.DecorationRole: + if isinstance(value, bytes): + # print('{}: {}'.format(value, type(value))) + if value == b'\x01': + 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) + return len(self._data[0]) + + def setData(self, index, value, role=Qt.ItemDataRole.EditRole): + if role == Qt.ItemDataRole.EditRole: + self._data[index.row()][index.column()] = value + if role == Qt.ItemDataRole.CheckStateRole: + checked = value == Qt.CheckState.Checked + self._data[index.row()][index.column()] = checked + return True + + def flags(self, index): + return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsUserTristate diff --git a/gui/resources.py b/gui/resources.py new file mode 100644 index 0000000..c135d07 --- /dev/null +++ b/gui/resources.py @@ -0,0 +1,7 @@ +from PySide6.QtGui import QIcon + +tick = QIcon('tick.png') +cross = QIcon('cross.png') +import_icon = QIcon("application-import.png") +export_icon = QIcon("application-export.png") +circle_icon = QIcon("arrow-circle-double.png") diff --git a/gui/tick.png b/gui/tick.png new file mode 100644 index 0000000..2414885 Binary files /dev/null and b/gui/tick.png differ