from typing import List import validators import os import logging import re import json from PyQt5 import QtWidgets from PyQt5.QtCore import Qt, QSize, QUrl from PyQt5.QtGui import QPixmap, QResizeEvent, QKeyEvent, QStandardItemModel, QStandardItem, QMovie, QDesktopServices, \ QIcon, QCursor from PyQt5 import QtMultimedia from PyQt5.QtMultimediaWidgets import QVideoWidget from ArtNet.gui.windows.importer.picture_importer import Ui_MainWindow from ArtNet.gui.windows.artnet_mainwindow import ArtnetMainWindow from ArtNet.gui.dialogs.db_connection_dialog.db_dialog import DBDialog from ArtNet.gui.dialogs.tag_modify_dialog.tag_mod_dialog import TagModifyDialog from ArtNet.gui.dialogs.tag_select_dialog.tag_select_dialog import TagSelectDialog from ArtNet.gui.dockers.presence.presence_dock import PresenceDocker from ArtNet.gui.dialogs.category_modify_dialog.category_mod_dialog import CategoryModDialog from ArtNet.gui.dialogs.tag_import_dialog.tag_imp_dialog import TagImportDialog from ArtNet.gui.dialogs.link_input_dialog.link_dialog import LinkInputDialog from ArtNet.gui.dialogs.collection_select_dialog.collection_sel_dialog import CollectionSelectDialog from ArtNet.gui.dialogs.collection_modify_dialog.collection_dialog import CollectionModDialog from ArtNet.web.link_generator import LinkGenerator class ImporterWindow(ArtnetMainWindow): UNKNOWN_TITLE = "-Title Unknown-" UNKNOWN_PRESENCE = "(Not in Database)" UNKNOWN_LINK = "No Source Available" NO_COLLECTION = "No Collections" def __init__(self, main): super().__init__(main) self.__pixmap: QPixmap = None self.__video: QVideoWidget = None self.__player: QtMultimedia.QMediaPlayer = None self.__text_player: QtWidgets.QTextEdit = None self.__showing_video: bool = False self.__showing_text: bool = False self.__tmp_imageid_spinbox: int = None self.presence_docker_open: bool = False self.presence_docker: PresenceDocker = None self.curr_art_id: int = None self.curr_image_title: str = None self.curr_link: str = None self.curr_art_path: str = None self.curr_file_name: str = None self.curr_presences: list = list() self.curr_tags: list = list() self.curr_implied_tags: list = list() self.curr_tag_aliases: list = list() self.curr_collections: list = list() self.__data_changed: bool = False self.ui = Ui_MainWindow() self.ui.setupUi(self) self.main_title = "ArtNet Picture Importer" self.ui.actionChange_ArtNet_Root_Folder.triggered.connect(self.on_artnet_root_change_clicked) self.ui.actionChange_Connection_Details.triggered.connect(self.on_db_connection_change_clicked) self.ui.actionCreate_New_Tag.triggered.connect(self.on_tag_creation_clicked) self.ui.actionEdit_a_Tag.triggered.connect(self.on_tag_edit_clicked) self.ui.actionDelete_a_Tag.triggered.connect(self.on_tag_deletion_clicked) self.ui.actionCreate_New_Category_2.triggered.connect(self.on_category_creation_clicked) self.ui.actionDelete_a_Category_2.triggered.connect(self.on_category_deletion_clicked) self.ui.actionArtNet_Browser.triggered.connect(self.on_browser_clicked) self.ui.imageNumberSpinBox.valueChanged.connect(self.on_image_id_spinbox_changed) self.ui.next_image_button.clicked.connect(self.on_next_clicked) self.ui.prev_image_button.clicked.connect(self.on_previous_clicked) self.ui.save_button.clicked.connect(self.on_save_clicked) self.ui.import_button.clicked.connect(self.on_import_tags_clicked) self.ui.prev_unknown_image_button.clicked.connect(self.on_prev_unknown_image_clicked) self.ui.next_unknown_image_button.clicked.connect(self.on_next_unknown_image_clicked) self.ui.collections_button.clicked.connect(self.on_collection_button_clicked) self.ui.delete_button.clicked.connect(self.on_delete_image_clicked) self.ui.link_button.clicked.connect(self.on_source_link_button_clicked) self.ui.link_label.linkActivated.connect(self.on_link_label_activated) self.ui.image_file_label.linkActivated.connect(self.on_link_file_label_activated) self.ui.image_author_label.linkActivated.connect(self.on_image_author_label_activated) self.ui.presence_docker_button.clicked.connect(self.toggle_presence_docker) self.ui.tag_search_bar.textChanged.connect(self.on_tag_search_change) self.ui.image_title_line.textChanged.connect(self.on_image_title_change) self.ui.description_edit.textChanged.connect(self.on_description_change) self.ui.link_label.setText(ImporterWindow.UNKNOWN_LINK) self.ui.description_edit.setReadOnly(False) self.ui.image_label.setContextMenuPolicy(Qt.CustomContextMenu) self.ui.image_label.customContextMenuRequested.connect(self.on_custom_context_menu_requested) self.on_tag_search_change() self.center() if os.path.isfile("./application_icon.png"): self.setWindowIcon(QIcon("./application_icon.png")) else: logging.warning("Didn't find application icon!") def center(self): """ Centers the window in the middle of the screen Note: actually not the center but a good position due to images changing size! :return: """ screen = QtWidgets.QDesktopWidget().screenGeometry() size = self.geometry() self.move(int((screen.width() - size.width()) / 3), int((screen.height() - size.height()) / 5)) def check_save_changes(self): """ Check if there were changes to image settings. If yes ask for confirmation to save them. :return: """ if self.data_changed: answer = QtWidgets.QMessageBox.question(self, "Save Changes?", "There have been changes. Do you wish to save them?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) if answer == QtWidgets.QMessageBox.Yes: if not self.save_changes(): return False return True def save_changes(self) -> bool: """ Save the changes to image data to the DB. :return: """ new_title = self.curr_image_title new_description = self.ui.description_edit.toPlainText().strip() if new_title == self.curr_file_name or len(new_title) == 0 or new_title == ImporterWindow.UNKNOWN_TITLE: new_title = None if new_description is None or len(new_description) == 0: new_description = None else: new_description = new_description.strip() image_data = { "ID": self.curr_art_id, "title": new_title, "authors": self.curr_presences, "path": self.curr_art_path, "tags": self.curr_tags, "link": self.curr_link, "md5_hash": self.main.get_md5_of_image(self.curr_art_path), "description": new_description, "collections": self.curr_collections, } for presence in self.curr_presences: if presence[-1] == ImporterWindow.UNKNOWN_PRESENCE: msg = QtWidgets.QMessageBox() msg.setWindowTitle("Invalid Presence Domain") msg.setInformativeText("You've tried to save with a not working presence entry! " + "Please add one from the database!") msg.setIcon(QtWidgets.QMessageBox.Warning) msg.exec_() return False for coll_id, coll_name in self.curr_collections: msg = QtWidgets.QMessageBox() msg.setWindowTitle("Could not save collection!") msg.setInformativeText("The saving and proper selection of collections is not supported as of now!") msg.setIcon(QtWidgets.QMessageBox.Warning) msg.exec_() break self.main.db_connection.save_image(ID=image_data["ID"], title=image_data["title"], authors=image_data["authors"], path=image_data["path"], tags=image_data["tags"], link=image_data["link"], md5_hash=image_data["md5_hash"], desc=image_data["description"], collections=image_data["collections"]) self.set_temporary_status_message("Saved {0} ({1}) to ArtNet DB!" .format(image_data["title"], image_data["ID"]), 5000) self.update_window_title() self.data_changed = False return True def set_temporary_status_message(self, text: str, duration: int): """ Set a temporary status message (bottom left) for the given duration in milliseconds. :param text: :param duration: :return: """ self.ui.statusbar.showMessage(text, duration) def create_presence(self, name: str, domain: str, artist: tuple, link: str): """ Create a new Presence Entry with the given data :param name: :param domain: :param artist: :param link: :return: """ if len(name) == 0 or len(domain) == 0 or artist is None: return self.main.db_connection.save_presence(name=name, domain=domain, artist_ID=artist[0], link=link) def get_authors(self, presence_name: str, presence_domain: str) -> list: """ Query a search for the authors fitting the given strings :param presence_name: :param presence_domain: :return: a list of tuples of (presence_name, presence_domain) """ return self.main.db_connection.search_fuzzy_presence(presence_name, presence_domain, all_if_empty=True) def create_artist(self, ID: int, description: str): """ Create a new artist with the given data (or update an exisitng one if ID is already taken :param ID: :param description: :return: """ self.main.db_connection.save_artist(ID, description) self.set_temporary_status_message("Created Artist {0}!".format(description), 3000) def get_artists(self, search: str) -> list: """ Query a search for the artists fitting the given data best. Search is fuzzy. :param search: either an ID (int) or the description :return: """ try: ID_int = int(search) description = None except ValueError: ID_int = None description = search return self.main.db_connection.search_fuzzy_artists(ID_int, description) def get_artist(self, id: int) -> list: """ Query for the artist matching id. Returns None if the data does not exactly fit. :param id: :return: """ return self.main.db_connection.get_artist(id) def remove_artist(self, id: int): """ Delte the given artist from the database. :param id: :return: """ self.main.db_connection.remove_artist(id) def get_artist_presences(self, id: int) -> list: """ Query for all presences associated with the given artist. :param id: :return: """ return self.main.db_connection.get_artist_presences(id) def get_all_artists(self) -> list: """ Queries the database for a list of all available arists (not presences). :return: """ return self.main.db_connection.get_all_artists() def get_presence(self, name: str, domain: str): """ Query a search for the presence fitting the data :param name: :param domain: :return: """ result = self.main.db_connection.get_presence(name, domain) return result if len(result) != 0 else None def remove_presence(self, name: str, domain: str): """ Deletes a presence from the database and removes all Art_Author entries containing this presence. :param name: :param domain: :return: """ self.main.db_connection.remove_presence(name, domain) def get_presences_art(self, name: str, domain: str): """ Query a list of art owned by the given presence :param name: :param domain: :return: """ return self.main.db_connection.get_presences_art(name, domain) def get_current_presences(self) -> list: """ Get the presences currently associated with the current art :return: """ return self.curr_presences def set_current_presences(self, presences: list): """ Set the presences associated with the current art :param presences: list of tuples of (name, domain) """ if len(presences) > 1: for name, domain in presences: if domain == ImporterWindow.UNKNOWN_PRESENCE: presences.remove((name, domain)) elif len(presences) == 0: presences = [(self.curr_art_path.split("/")[0], ImporterWindow.UNKNOWN_PRESENCE)] self.curr_presences = presences if self.curr_presences is not None: self.set_presence_label_text(self.curr_presences) self.data_changed = True def get_current_collections(self) -> list: """ Get the collections currently associated with the current art """ return self.curr_collections def set_current_collections(self, collections: list): """ Set the collections associated with the current art :param collections: List[Tuple[coll_id, coll_name]] """ self.curr_collections = collections if len(collections) == 0: self.ui.collections_label.setText(ImporterWindow.NO_COLLECTION) else: s = "" for coll_id, coll_name, coll_desc in collections: s += "{1}".format("", f"#{coll_id}:{coll_name}") # currently not linking to anything s += "|" s = s[:-1] self.ui.collections_label.setText(s) self.data_changed = True def get_categories(self, search: str, all_if_empty: bool = False): """ Fuzzy Query for categories in the database. all_if_empty causes an empty search to return all categories instead of none :param search: :param all_if_empty: :return: """ if all_if_empty and len(search) == 0: return self.main.db_connection.get_all_categories() return self.main.db_connection.search_fuzzy_categories(search) def create_collection(self, collection_name: str, description: str = None) -> int: """ Create a new collection with the given name and description Returns the assigned ID of the collection """ return self.main.db_connection.create_collection(collection_name, description) def get_collection(self, collection_id: int): """ Fetch the collection given by the id. If not present None is returned. """ return self.main.db_connection.get_collection(collection_id) def delete_collection(self, collection_id: int): """ Delete the collection given by the id from the database. """ self.main.db_connection.delete_collection(collection_id) def search_collections(self, collection_name: str = None, presence_name: str = None, presence_domain: str = None, art_name: str = None): """ Search for collections that fit the given parameters. All parameters are searched fuzzy. """ return self.main.db_connection.search_collections(collection_name=collection_name, presence_name=presence_name, presence_domain=presence_domain, art_name=art_name) def get_image_link_from_label(self) -> str: """ Gets the image link from the label if it is a valid link. Otherwise an empty string :return: """ result = re.match('[ a-zA-Z<=>"]*((https|http)://[a-zA-Z0-9]+\.[a-zA-Z0-9]+/[a-zA-Z0-9/]+)', self.ui.link_label.text()) if result is not None: result = result.groups()[0] else: result = "" return result def set_presence_label_text(self, presences: list): """ Set the label listing all current presences and include links if possible. :param presences: :return: """ s = "" for name, domain in presences: full_data = self.get_presence(name, domain) if full_data is None: link = "" else: name, domain, _, link = full_data[0] text = name + ":" + domain if link is None or len(link) == 0: # no link, then just do plain text hyperlink = text else: hyperlink = "{1}".format(link, text) s += hyperlink s += "|" s = s[:-1] self.ui.image_author_label.setText(s) def set_image_title_link(self, link: str) -> str: """ Sets the Image title to a link if there is link data given for this image. :return: Returns the result that has been set to see if a successful link was constructed """ self.ui.link_label.setText(ImporterWindow.UNKNOWN_LINK) if link is not None and validators.url(link): self.curr_link = link self.data_changed = True hyperlink = "{1}".format(link, "Source") self.ui.link_label.setText(hyperlink) self.ui.link_label.setToolTip(link) return link elif link is None or len(link) == 0: return "" else: self.ui.link_label.setText(ImporterWindow.UNKNOWN_LINK) self.set_temporary_status_message("Invalid link \"{0}\" detected!".format(link), 5000) return "" def get_tag(self, name: str) -> list: """ Query a search for the tag to the DB and return the result :param name: :return: """ return self.main.db_connection.get_tag_by_name(name) def get_tag_aliases(self, name: str) -> list: """ Query a search for the tag's aliases to the DB Note: Returns all aliases as a list of their IDs :param name: :return: """ return self.main.db_connection.get_tag_aliases_by_name(name) def get_tag_implications(self, name: str) -> list: """ Query a search for the tag's implications to the DB :param name: :return: """ return self.main.db_connection.get_tag_implications(name) def get_tag_search_result(self, name: str) -> list: """ Query a search for tags to the DB that are like name :return: """ return self.main.db_connection.search_fuzzy_tag(name, all_if_empty=True) def set_tag_search_result_list(self, tags: List[dict]): """ Set the tags in the search result list to tags :param tags: :return: """ tags.sort(key=lambda x: x["name"]) item_model = QStandardItemModel(self.ui.search_result_list) for tag in tags: tag_name = tag["name"] item = QStandardItem(tag_name) flags = Qt.ItemIsEnabled if tag_name not in self.curr_implied_tags: # new tag and not implied yet item.setData(Qt.Unchecked, Qt.CheckStateRole) if 'id' in tag.keys() and 'description' in tag.keys() and 'category' in tag.keys(): s = f"Tag ID: {tag['id']}\n\n" if len(tag["description"]) != 0: s += tag["description"]+"\n\n" s += f"Category: {tag['category']}" item.setToolTip(s) item.setData(json.dumps(tag), Qt.UserRole) flags |= Qt.ItemIsUserCheckable is_in_tags = False for i in range(len((self.curr_tags + self.curr_implied_tags + self.curr_tag_aliases))): if item.text() == (self.curr_tags + self.curr_implied_tags + self.curr_tag_aliases)[i]["name"]: is_in_tags = True if self.curr_tags is not None and is_in_tags: # already selected, implied or aliased tags item.setCheckState(Qt.Checked) item.setFlags(flags) item_model.appendRow(item) item_model.itemChanged.connect(self.on_tag_search_item_changed) self.ui.search_result_list.setModel(item_model) def set_description_text(self, text: str): """ Set the text for the description field """ self.ui.description_edit.setText(text) def set_tag_list(self, tags: list, set_checked: bool = True, no_implication: bool = False): """ Set the tags in the tag list to this. Also updates the tag implication list if no_implication is False :param tags: :param set_checked: :param no_implication: not used :return: """ tags.sort(key=lambda x: x["name"]) self.curr_tags = tags item_model = QStandardItemModel(self.ui.tag_list) implied_tags = [] for x in self.curr_tags: # collect all implied tags into a list implied_tags += self.main.db_connection.get_all_tag_implications_by_name(x["name"]) self.set_implied_list(implied_tags) for tag in tags: tag_name = tag["name"] item = QStandardItem(tag_name) flags = Qt.ItemIsEnabled if tag_name not in self.curr_implied_tags: # new tag and not implied yet item.setData(Qt.Unchecked, Qt.CheckStateRole) if 'id' in tag.keys() and 'description' in tag.keys() and 'category' in tag.keys(): s = f"Tag ID: {tag['id']}\n\n" if len(tag["description"]) != 0: s += tag["description"] + "\n\n" s += f"Category: {tag['category']}" item.setToolTip(s) flags |= Qt.ItemIsUserCheckable if set_checked: if tag_name not in self.curr_implied_tags: item.setCheckState(Qt.Checked) else: item.setCheckState(Qt.Unchecked) item.setFlags(flags) item_model.appendRow(item) item_model.itemChanged.connect(self.on_tag_item_changed) self.ui.tag_list.setModel(item_model) self.data_changed = True def set_implied_list(self, tags: list): """ Sets the implied tags in the imply list :param tags: :return: """ self.curr_implied_tags = tags item_model = QStandardItemModel(self.ui.implied_tag_list) done = [] for tag in tags: tag_name = tag["name"] if tag_name in done: continue else: done.append(tag_name) item = QStandardItem(tag_name) if 'id' in tag.keys() and 'description' in tag.keys() and 'category' in tag.keys(): s = f"Tag ID: {tag['id']}\n\n" if len(tag["description"]) != 0: s += tag["description"] + "\n\n" s += f"Category: {tag['category']}" item.setToolTip(s) item_model.appendRow(item) self.ui.implied_tag_list.setModel(item_model) self.data_changed = True def display_image(self, image_title: str, image_authors: list, full_path: str, relative_path: str, art_ID: int, link: str, file_name: str, description: str, collections: list): """ Display an image in the central widget :param image_authors: :param image_title: :param full_path: :param relative_path: :param art_ID: :param link: :param file_name: :param description: :param collections: :return: """ self.curr_art_id = art_ID self.curr_art_path = relative_path self.curr_image_title = image_title self.curr_file_name = os.path.basename(full_path) self.curr_link = link self.set_current_collections(collections) self.set_current_presences(image_authors) file_ending = relative_path.split(".")[-1] if self.__showing_video: # remove old video from image layout # self.ui.image_frame.layout().removeWidget(self.__video) self.__video.hide() self.ui.image_label.show() if self.__showing_text: # remove text are from image layout self.__text_player.hide() self.ui.image_label.show() if file_ending in ["gif"]: self.__showing_video = False self.__pixmap = QMovie(full_path) self.ui.image_label.setMovie(self.__pixmap) self.__pixmap.start() self.__pixmap.frameChanged.connect(self.on_movie_frame_changed) elif file_ending in ["webm", "mp4", "mov", "mkv"]: self.__showing_video = True if isinstance(self.__video, QVideoWidget): self.__video.show() else: self.__video = QVideoWidget() if self.__video is None else self.__video self.__player = QtMultimedia.QMediaPlayer(None, QtMultimedia.QMediaPlayer.VideoSurface) self.__player.setVideoOutput(self.__video) self.__player.setMedia(QtMultimedia.QMediaContent(QUrl.fromLocalFile(full_path))) self.__player.play() self.ui.image_frame.layout().addWidget(self.__video) self.ui.image_label.hide() self.__player.stateChanged.connect(self.on_movie_player_state_changed) self.__player.positionChanged.connect(self.on_movie_position_changed) elif file_ending in ["txt"]: # for stories or text files self.__showing_text = True self.ui.image_label.hide() self.__text_player = QtWidgets.QTextEdit() if self.__text_player is None else self.__text_player with open(full_path, "r") as text_file: story = text_file.read() text_file.close() self.__text_player.setText(story) self.__text_player.setReadOnly(True) self.ui.image_frame.layout().addWidget(self.__text_player) self.__text_player.show() else: self.__showing_video = False self.__pixmap = QPixmap(full_path) self.ui.image_label.setPixmap(self.__pixmap) self.ui.image_label.setScaledContents(True) self.ui.image_label.setFixedSize(0, 0) self.__image_resize() self.ui.image_label.setAlignment(Qt.AlignCenter) self.ui.image_title_line.setText(image_title) self.update_window_title() self.ui.image_file_label.setText("{1}" .format(f"file:///{full_path[:-1 * len(self.curr_file_name)]}", file_name)) self.ui.image_file_label.setToolTip(relative_path) self.ui.description_edit.setText(description) self.set_image_title_link(link) self.set_image_id_spinbox() self.data_changed = False # reset any triggered change detection def update_window_title(self): """ Update the title of the window with the newest image title as given in text field :return: """ image_title = self.ui.image_title_line.text() self.setWindowTitle(self.main_title + " - " + image_title + f" ({round(self.main.known_image_amount / len(self.main.all_images), 5)}%)") def set_image_id_spinbox(self): """ Sets the imageIDSpinBox to the image ID of the currently displayed image :return: """ self.ui.imageNumberSpinBox.setMinimum(0) self.ui.imageNumberSpinBox.setMaximum(len(self.main.all_images) - 1) self.ui.imageNumberSpinBox.setValue(self.main.curr_image_index) def __image_resize(self): """ Resize the given pixmap so that we're not out of the desktop. :return: new scaled QPixmap """ if self.ui.image_label.movie() is not None or self.__showing_video: # if QMovie was used instead of image rect = self.geometry() size = QSize(min(rect.width(), rect.height()), min(rect.width(), rect.height())) if type(self.__pixmap) != QMovie: # using QVideoWidget? pass # self.__player.setScaledSize(size) else: self.__pixmap.setScaledSize(size) return size = self.__pixmap.size() screen_rect = QtWidgets.QDesktopWidget().screenGeometry() size.scale(int(screen_rect.width() * 0.6), int(screen_rect.height() * 0.6), Qt.KeepAspectRatio) self.ui.image_label.setFixedSize(size) def resizeEvent(self, a0: QResizeEvent) -> None: self.__image_resize() def keyPressEvent(self, a0: QKeyEvent) -> None: super(ImporterWindow, self).keyPressEvent(a0) if a0.key() == Qt.Key_Left: self.on_previous_clicked() elif a0.key() == Qt.Key_Right: self.on_next_clicked() elif a0.key() == Qt.Key_Return: logging.debug("Pressed Enter!") if self.__showing_video: s = self.__player.state() if self.__player.state() == QtMultimedia.QMediaPlayer.PlayingState: self.__player.pause() self.set_temporary_status_message("Paused the Video!", 3000) elif self.__player.state() == QtMultimedia.QMediaPlayer.PausedState: self.__player.play() self.set_temporary_status_message("Started the Video!", 3000) elif self.__player.state() == QtMultimedia.QMediaPlayer.StoppedState: self.__player.play() self.set_temporary_status_message("Restarted the Video!", 3000) elif type(self.__pixmap) == QMovie: if self.__pixmap.state() == QMovie.Paused: self.__pixmap.start() self.set_temporary_status_message("Started the Video!", 3000) elif self.__pixmap.state() == QMovie.Running: self.__pixmap.setPaused(True) self.set_temporary_status_message("Paused the Video!", 3000) elif QtWidgets.QApplication.focusWidget() == self.ui.tag_search_bar: # quick select for the search bar if self.ui.search_result_list.model().rowCount() == 1: # only 1 search result left model_index = self.ui.search_result_list.model().index(0, 0) item_data = json.loads(self.ui.search_result_list.model().itemData(model_index)[Qt.UserRole]) if item_data not in self.curr_tags: # add/remove new selected tag to the lists self.curr_tags.append(item_data) else: self.curr_tags.remove(item_data) self.set_tag_list(self.curr_tags) # update relevant lists self.set_tag_search_result_list([item_data]) logging.debug(item_data) def on_movie_player_state_changed(self, state: int): self.__image_resize() if QtMultimedia.QMediaPlayer.StoppedState == state: # player stopped self.set_temporary_status_message("Reached end of Video!", 2000) def on_movie_position_changed(self, position): pass def on_movie_frame_changed(self, frame_number: int): if type(self.__pixmap) != QMovie: return if frame_number == 0: self.set_temporary_status_message("Reached end of Video!", 2000) self.__pixmap.setPaused(True) def on_save_clicked(self): logging.info("Clicked Save!") self.save_changes() def on_import_tags_clicked(self): logging.info("Clicked Import!") dialog = TagImportDialog(self) if len(self.get_image_link_from_label()) == 0 \ or self.get_image_link_from_label() == ImporterWindow.UNKNOWN_LINK: url = LinkGenerator.get_instance().construct_link(self.curr_file_name, LinkGenerator.get_instance() .predict_domain(self.curr_file_name)) self.set_image_title_link(url) # Update no link to the predicted link else: url = self.get_image_link_from_label() r = self.main.scrape_tags(self.curr_file_name, url=url, art_ID=self.curr_art_id) if r is None: msg = QtWidgets.QMessageBox() msg.setWindowTitle("Unsupported Domain") msg.setInformativeText("Could not predict a supported domain!") msg.setIcon(QtWidgets.QMessageBox.Warning) msg.exec_() return self.set_image_title_link(url) tags, artists = r i = 0 while i < len(tags): # workaround for an issue with altering lists during iteration r = self.main.db_connection.get_tag_by_name(tags[i]) if r is not None: self.curr_tags.append(r) self.data_changed = True tags.remove(tags[i]) continue else: i += 1 self.set_tag_list(self.curr_tags) if len(tags) == 0: msg = QtWidgets.QMessageBox() msg.setWindowTitle("Nothing to import!") msg.setInformativeText("There were no tags to import for this art!") msg.setIcon(QtWidgets.QMessageBox.Information) msg.exec_() return dialog.set_import_tag_list(tags) dialog.set_detected_artists(artists) dialog.set_used_link(url) dialog.to_import = tags result = dialog.exec_() if result is None: self.set_tag_list(self.curr_tags) return self.main.import_tags(result) self.set_tag_list(self.curr_tags) def on_next_clicked(self): logging.info("Clicked Next!") if not self.check_save_changes(): return self.main.curr_image_index += 1 if self.main.curr_image_index >= len(self.main.all_images): self.main.curr_image_index = 0 self.main.refresh_shown_image() if self.presence_docker_open: self.toggle_presence_docker() self.on_tag_search_change() def on_image_title_change(self): self.data_changed = True self.curr_image_title = self.ui.image_title_line.text() def on_previous_clicked(self): logging.info("Clicked previous!") if not self.check_save_changes(): return self.main.curr_image_index -= 1 if self.main.curr_image_index < 0: self.main.curr_image_index += len(self.main.all_images) self.main.refresh_shown_image() if self.presence_docker_open: self.toggle_presence_docker() self.on_tag_search_change() def toggle_presence_docker(self): if not self.presence_docker_open: logging.info("Opened presence docker!") self.presence_docker = PresenceDocker(self) self.ui.presence_docker_layout.addWidget(self.presence_docker) self.presence_docker.set_selected_presences_list(self.get_current_presences()) self.ui.presence_docker_button.setArrowType(Qt.RightArrow) self.presence_docker_open = True else: logging.info("Closed presence docker!") self.presence_docker.setParent(None) self.ui.presence_docker_button.setArrowType(Qt.LeftArrow) self.presence_docker.destroy() self.presence_docker = None self.presence_docker_open = False def on_artnet_root_change_clicked(self): logging.info("Clicked changing ArtNet root!") dialog = QtWidgets.QFileDialog(self, 'Choose new ArtNet root:') dialog.setFileMode(QtWidgets.QFileDialog.Directory) dialog.setOptions(QtWidgets.QFileDialog.ShowDirsOnly) directory = dialog.getExistingDirectory() self.main.change_root(directory) def on_db_connection_change_clicked(self): logging.info("Clicked db connection change!") dialog = DBDialog(self) prev_db_data = self.main.get_db_connection_details() dialog.ui.user_line_edit.setText(prev_db_data["user"]) dialog.ui.password_line_edit.setText(prev_db_data["password"]) dialog.ui.host_line_edit.setText(prev_db_data["host"]) dialog.ui.database_line_edit.setText(prev_db_data["database"]) dialog.ui.port_line_edit.setText(str(prev_db_data["port"])) db_data: dict = dialog.exec_() if db_data is None or len(db_data.keys()) == 0: return try: self.main.change_db_connection(host=db_data["host"], port=db_data["port"], user=db_data["user"], password=db_data["password"], database=db_data["database"]) except ValueError as e: # details were wrong (probably) QtWidgets.QMessageBox.warning(self, "Wrong Credentials?", f"Error: {e}") def on_tag_creation_clicked(self): logging.info("Clicked Tag Creation!") dialog = TagModifyDialog(self, create_tag=True) tag_data: dict = dialog.exec_() logging.debug(f"Got Tag data: {tag_data}") if tag_data is None or len(tag_data.keys()) == 0: # got canceled? return if self.get_tag(tag_data["name"]) is not None: # check if tag exists QtWidgets.QMessageBox.information(self, "Duplicate Tag", "The tag \"{0}\" already exists in the db!" .format(tag_data["name"])) return self.main.db_connection.create_tag(name=tag_data["name"], description=tag_data["description"], aliases=tag_data["aliases"], implications=tag_data["implications"], category=tag_data["category"]) self.on_tag_search_change() implied_tags = [] # refresh implied tags, the edit/addition might have changed smth for x in self.curr_tags: # collect all implied tags into a list implied_tags += self.main.db_connection.get_all_tag_implications_by_name(x) def on_tag_deletion_clicked(self): logging.info("Clicked Tag Deletion!") dialog = TagSelectDialog(self, delete_tag=True) tag = dialog.exec_() logging.debug("Got Tag", tag) if tag is None or len(tag) == 0: return confirmation_reply = QtWidgets.QMessageBox.question(self, "Delete Tag \"{0}\"".format(tag["name"]), "Are you sure you want to delete this Tag?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) if confirmation_reply == QtWidgets.QMessageBox.No: return self.main.db_connection.remove_tag_by_name(tag["name"]) self.on_tag_search_change() def force_edit_tag_dialog(self, name: str): edit_dialog = TagModifyDialog(self, create_tag=True) edit_dialog.ui.tag_name_line.setText(name) edit_dialog.ui.tag_description_area.setText("") edit_dialog.set_selected_alias_tags([], set_checked=True) edit_dialog.alias_selection = [] edit_dialog.set_selected_implicated_tags([], set_checked=True) edit_dialog.implication_selection = [] edit_dialog.category_selection = [] edit_dialog.set_all_categories() tag_data = edit_dialog.exec_() logging.debug(f"Got Tag data: {tag_data}") if tag_data is None or len(tag_data.keys()) == 0: return None if len(tag_data["category"]) == 0: answer = QtWidgets.QMessageBox.information(self, "No Category", "There has been no Category selected for this tag! " "No tag is allowed without a category!") return None if self.get_tag(tag_data['name']) is not None: QtWidgets.QMessageBox.information(self, "Tag already exists", f"The Tag \"{tag_data['name']}\" you wanted to create already exists! Skipping...") return None else: self.main.db_connection.create_tag(name=tag_data["name"], description=tag_data["description"], aliases=tag_data["aliases"], implications=tag_data["implications"], category=tag_data["category"]) self.on_tag_search_change() return tag_data def on_tag_edit_clicked(self): logging.info("Clicked Tag Editing!") select_dialog = TagSelectDialog(self, delete_tag=False) tag = select_dialog.exec_() if tag is None or len(tag) == 0: return #tag['aliases'] = self.main.db_connection.get_tag_aliases_by_name(tag["name"]) #tag['implications'] = self.main.db_connection.get_tag_implications(tag["name"]) edit_dialog = TagModifyDialog(self, create_tag=False) edit_dialog.ui.tag_name_line.setText(tag["name"]) edit_dialog.ui.tag_description_area.setText(tag["description"]) edit_dialog.set_selected_alias_tags(tag["aliases"], set_checked=True) edit_dialog.alias_selection = tag["aliases"] edit_dialog.set_selected_implicated_tags(tag["implications"], set_checked=True) edit_dialog.implication_selection = tag["implications"] edit_dialog.category_selection = self.main.db_connection.get_category_by_ID(tag["category_id"])[1] edit_dialog.set_all_categories() tag_data = edit_dialog.exec_() logging.debug(f"Got Tag data: {tag_data}") if tag_data is None or len(tag_data.keys()) == 0: return if "old_tag_name" not in tag_data.keys(): tag_data["old_tag_name"] = None self.main.db_connection.edit_tag(tag_id=tag["ID"], name=tag_data["name"], description=tag_data["description"], aliases=tag_data["aliases"], implications=tag_data["implications"], category_id=self.main.db_connection.get_category_by_name(tag_data["category"])[ 0]) self.on_tag_search_change() def on_tag_search_item_changed(self, item: QStandardItem): if item.checkState() == Qt.Checked: tag_data = self.main.db_connection.get_tag_by_name(item.text()) self.curr_tags.append(tag_data) aliases = self.main.db_connection.get_tag_aliases_by_name(item.text()) for alias in aliases: self.curr_tags.append(alias) implications = self.main.db_connection.get_all_tag_implications_by_name(item.text()) for implication in implications: self.curr_tags.append(implication) if item.checkState() == Qt.Unchecked: is_in_tags = False for i in range(len(self.curr_tags)): if item.text() == self.curr_tags[i]["name"]: tags_index = i is_in_tags = True if is_in_tags: self.curr_tags.pop(tags_index) aliases = self.main.db_connection.get_tag_aliases_by_name(item.text()) for alias in aliases: if alias in self.curr_tags: self.curr_tags.remove(alias) implications = self.main.db_connection.get_all_tag_implications_by_name(item.text()) for implication in implications: self.curr_tags.remove({"name": implication}) else: raise Exception("Something went terribly wrong!") self.set_tag_list(self.curr_tags) self.on_tag_search_change() def on_tag_item_changed(self, item: QStandardItem): logging.debug("Item {0} has changed!".format(item.text())) if item.checkState() == Qt.Unchecked: is_in_tags = False for i in range(len(self.curr_tags)): if item.text() == self.curr_tags[i]["name"]: tags_index = i is_in_tags = True if is_in_tags: aliases = self.main.db_connection.get_tag_aliases_by_name(item.text()) if len(aliases) > 0: # tag has aliases, might need to also remove them for alias in aliases: self.curr_tags.remove(alias) for i in range(len(self.curr_tags)): if item.text() == self.curr_tags[i]["name"]: tags_index = i self.curr_tags.pop(tags_index) self.set_tag_list(self.curr_tags) self.on_tag_search_change() else: raise Exception("Something went terribly wrong!") def on_tag_search_change(self): tags = self.main.db_connection.search_fuzzy_tag(self.ui.tag_search_bar.text(), all_if_empty=True) self.set_tag_search_result_list(tags) def on_category_creation_clicked(self): dialog = CategoryModDialog(self, delete_category=False) data = dialog.exec_() if data is None: return self.main.db_connection.save_category(data["name"]) def on_category_deletion_clicked(self): dialog = CategoryModDialog(self, delete_category=True) data = dialog.exec_() if data is None: return self.main.db_connection.remove_category(data["name"]) def on_prev_unknown_image_clicked(self): unknown_image_index = self.main.get_prev_unknown_image() logging.info("Previous unknown image clicked!") result = QtWidgets.QMessageBox.question(self, "Switch Image?", "Do you really want to skip to image #{1} \"{0}\"?" .format(self.main.all_images[unknown_image_index], unknown_image_index)) if result == QtWidgets.QMessageBox.Yes: self.main.curr_image_index = unknown_image_index self.main.refresh_shown_image() def on_next_unknown_image_clicked(self): unknown_image_index = self.main.get_next_unknown_image() logging.info("Next unknown image clicked!") result = QtWidgets.QMessageBox.question(self, "Switch Image?", "Do you really want to skip to image #{1} \"{0}\"?" .format(self.main.all_images[unknown_image_index], unknown_image_index)) if result == QtWidgets.QMessageBox.Yes: self.main.curr_image_index = unknown_image_index self.main.refresh_shown_image() def on_image_id_spinbox_changed(self, v: int): if self.__tmp_imageid_spinbox == v: logging.info("SpinBox change detected!") result = QtWidgets.QMessageBox.question(self, "Switch Image?", "Do you really want to skip to image #{1} \"{0}\"?" .format(self.main.all_images[v], v)) if result == QtWidgets.QMessageBox.Yes: self.main.curr_image_index = v self.main.refresh_shown_image() self.__tmp_imageid_spinbox: int = v def on_delete_image_clicked(self): logging.info("Delete clicked!") art_hash = self.main.get_md5_of_image(self.curr_art_path) if self.main.db_connection.get_art_by_hash(art_hash) is not None: logging.debug("Delete on known image") confirm_result = QtWidgets.QMessageBox.question(self, "Delete data?", "Do you really wish to delete all " "data from the DB about this image?") if confirm_result == QtWidgets.QMessageBox.Yes: logging.info(f"deleting image data of \"{self.curr_image_title}\"") self.main.db_connection.remove_image(art_hash) else: return else: logging.debug("Delete on unknown image") confirm_result = QtWidgets.QMessageBox.question(self, "Delete image?", "Do you really wish to delete this " "image?") if confirm_result == QtWidgets.QMessageBox.Yes: logging.info(f"deleting image file {self.curr_art_path}") self.main.delete_image(self.curr_art_path) else: return self.main.refresh_shown_image() def on_link_label_activated(self, link: str): logging.debug(f"Source link activated! {link}") QDesktopServices.openUrl(QUrl(link)) def on_image_author_label_activated(self, link: str): logging.debug(f"Image author link activated! {link}") QDesktopServices.openUrl(QUrl(link)) def on_link_file_label_activated(self, link: str): logging.debug(f"File label link activated! {link}") QDesktopServices.openUrl(QUrl(link)) def on_description_change(self): self.data_changed = True def on_browser_clicked(self): logging.debug("Clicked on open ArtNet browser!") self.main.switch_to_browser() def on_source_link_button_clicked(self): logging.debug("Clicked on link button!") dialog = LinkInputDialog(self, self.curr_link) link = dialog.exec_() if link is None: # dialog was cancelled logging.debug("Cancelled link dialog.") return logging.info(f"Setting source link to \"{link}\"") self.set_image_title_link(link) def on_collection_button_clicked(self): logging.debug("Clicked on collection button!") QtWidgets.QMessageBox.information(self, "Not Implemented", "This feature has not been implemented yet!") selected_collections = CollectionSelectDialog(self).exec_(self.curr_collections) selected_collections = [self.get_collection(collection_id=c[0]) for c in selected_collections] # TODO verify that it returns the art-collection details if there are any for selected_collection in selected_collections: # allow editing the collection rankings result = CollectionModDialog(self, db_connection=self.main.db_connection, edit_collection=False)\ .exec_(selected_collection) print() self.set_current_collections(list(set(list(self.curr_collections + selected_collections)))) def on_custom_context_menu_requested(self): action = QtWidgets.QAction('Copy URL to clipboard') action.triggered.connect(self.on_copy_url_to_clipboard) menu = QtWidgets.QMenu() menu.addAction(action) menu.exec_(QCursor.pos()) def on_copy_url_to_clipboard(self): QtWidgets.QApplication.clipboard().setText(os.path.join(self.main.config.data['file_root'], self.curr_art_path))