import validators import os import logging import re 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): """ Set the tags in the search result list to tags :param tags: :return: """ tags.sort() item_model = QStandardItemModel(self.ui.search_result_list) for tag in tags: item = QStandardItem(tag) flags = Qt.ItemIsEnabled if tag not in self.curr_implied_tags: # new tag and not implied yet item.setData(Qt.Unchecked, Qt.CheckStateRole) flags |= Qt.ItemIsUserCheckable if self.curr_tags is not None and tag in (self.curr_tags + self.curr_implied_tags + self.curr_tag_aliases): # 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() 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) self.set_implied_list(implied_tags) for tag in tags: item = QStandardItem(tag) flags = Qt.ItemIsEnabled if tag not in self.curr_implied_tags: # new tag and not implied yet item.setData(Qt.Unchecked, Qt.CheckStateRole) flags |= Qt.ItemIsUserCheckable if set_checked: if tag 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: if tag in done: continue else: done.append(tag) item = QStandardItem(tag) 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 = self.ui.search_result_list.model().itemData(model_index) if item_data[0] not in self.curr_tags: # add/remove new selected tag to the lists self.curr_tags.append(item_data[0]) else: self.curr_tags.remove(item_data[0]) self.set_tag_list(self.curr_tags) # update relevant lists self.set_tag_search_result_list([item_data[0]]) 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 len(r) > 0: self.curr_tags.append(tags[i]) 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 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 len(self.get_tag(tag_data["name"])) > 0: 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 len(self.get_tag(tag_data['name'])) > 0: QtWidgets.QMessageBox.information(self, "Tag already exists", "The Tag \"{0}\" you wanted to create already exists! Skipping...") 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: self.curr_tags.append(item.text()) 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: if item.text() in self.curr_tags: self.curr_tags.remove(item.text()) 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(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: if item.text() in self.curr_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) self.curr_tags.remove(item.text()) 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) result = [] for tag_name, tag_desc, tag_category, tag_id in tags: result.append(tag_name) self.set_tag_search_result_list(result) 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))